using System.Text.Json.Serialization; using Core.Helpers; using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; using Core.Models.Spt.Config; using Core.Models.Utils; using Core.Servers; using Core.Services; using Core.Utils; using Core.Utils.Cloners; using Core.Utils.Collections; using SptCommon.Annotations; namespace Core.Generators; [Injectable] public class LocationLootGenerator( ISptLogger _logger, RandomUtil _randomUtil, MathUtil _mathUtil, ItemHelper _itemHelper, InventoryHelper _inventoryHelper, DatabaseService _databaseService, LocalisationService _localisationService, SeasonalEventService _seasonalEventService, ItemFilterService _itemFilterService, ConfigServer _configServer, ICloner _cloner ) { protected LocationConfig _locationConfig = _configServer.GetConfig(); protected SeasonalEventConfig _seasonalEventConfig = _configServer.GetConfig(); /// Create a list of container objects with randomised loot /// Map base to generate containers for /// Static ammo distribution /// List of container objects public List GenerateStaticContainers(LocationBase locationBase, Dictionary> staticAmmoDist) { var staticLootItemCount = 0; var result = new List(); var locationId = locationBase.Id.ToLower(); var mapData = _databaseService.GetLocation(locationId); var staticWeaponsOnMapClone = _cloner.Clone(mapData.StaticContainers.Value.StaticWeapons); if (staticWeaponsOnMapClone is null) { _logger.Error( _localisationService.GetText("location-unable_to_find_static_weapon_for_map", locationBase.Name) ); } // Add mounted weapons to output loot result.AddRange(staticWeaponsOnMapClone); var allStaticContainersOnMapClone = _cloner.Clone(mapData.StaticContainers.Value.StaticContainers); if (allStaticContainersOnMapClone is null) { _logger.Error( _localisationService.GetText("location-unable_to_find_static_container_for_map", locationBase.Name) ); } // Containers that MUST be added to map (e.g. quest containers) var staticForcedOnMapClone = _cloner.Clone(mapData.StaticContainers.Value.StaticForced); if (staticForcedOnMapClone is null) { _logger.Error( _localisationService.GetText( "location-unable_to_find_forced_static_data_for_map", locationBase.Name ) ); } // Remove christmas items from loot data if (!_seasonalEventService.ChristmasEventEnabled()) { allStaticContainersOnMapClone = allStaticContainersOnMapClone.Where( item => !_seasonalEventConfig.ChristmasContainerIds.Contains(item.Template.Id) ) .ToList(); } var staticRandomisableContainersOnMap = GetRandomisableContainersOnMap(allStaticContainersOnMapClone); // Keep track of static loot count var staticContainerCount = 0; // Find all 100% spawn containers var staticLootDist = mapData.StaticLoot; var guaranteedContainers = GetGuaranteedContainers(allStaticContainersOnMapClone); staticContainerCount += guaranteedContainers.Count; // Add loot to guaranteed containers and add to result foreach (var container in guaranteedContainers) { var containerWithLoot = AddLootToContainer( container, staticForcedOnMapClone, staticLootDist.Value, staticAmmoDist, locationId ); result.Add(containerWithLoot.Template); staticLootItemCount += containerWithLoot.Template.Items.Count; } _logger.Debug($"Added {guaranteedContainers.Count} guaranteed containers"); // Randomisation is turned off globally or just turned off for this map if ( !( _locationConfig.ContainerRandomisationSettings.Enabled && _locationConfig.ContainerRandomisationSettings.Maps[locationId] ) ) { _logger.Debug( $"Container randomisation disabled, Adding {staticRandomisableContainersOnMap.Count} containers to {locationBase.Name}" ); foreach (var container in staticRandomisableContainersOnMap) { var containerWithLoot = AddLootToContainer( container, staticForcedOnMapClone, staticLootDist.Value, staticAmmoDist, locationId ); result.Add(containerWithLoot.Template); staticLootItemCount += containerWithLoot.Template.Items.Count; } _logger.Success($"A total of {staticLootItemCount} static items spawned"); return result; } // Group containers by their groupId if (mapData.StaticContainer is null) { _logger.Warning(_localisationService.GetText("location-unable_to_generate_static_loot", locationId)); return result; } var mapping = GetGroupIdToContainerMappings(mapData.StaticContainer, staticRandomisableContainersOnMap); // For each of the container groups, choose from the pool of containers, hydrate container with loot and add to result array foreach (var (key, data) in mapping) { // Count chosen was 0, skip if (data.ChosenCount == 0) { continue; } if (data.ContainerIdsWithProbability.Count == 0) { _logger.Debug($"`Group: {key} has no containers with< 100 % spawn chance to choose from, skipping"); continue; } // EDGE CASE: These are containers without a group and have a probability < 100% if (key == "") { var containerIdsCopy = _cloner.Clone(data.ContainerIdsWithProbability); // Roll each containers probability, if it passes, it gets added data.ContainerIdsWithProbability = new Dictionary(); foreach (var containerId in containerIdsCopy) if (_randomUtil.GetChance100(containerIdsCopy[containerId.Key] * 100)) { data.ContainerIdsWithProbability[containerId.Key] = containerIdsCopy[containerId.Key]; } // Set desired count to size of array (we want all containers chosen) data.ChosenCount = data.ContainerIdsWithProbability.Count; // EDGE CASE: chosen container count could be 0 if (data.ChosenCount == 0) { continue; } } // Pass possible containers into function to choose some var chosenContainerIds = GetContainersByProbability(key, data); foreach (var chosenContainerId in chosenContainerIds) { // Look up container object from full list of containers on map var containerObject = staticRandomisableContainersOnMap.FirstOrDefault( staticContainer => staticContainer.Template.Id == chosenContainerId ); if (containerObject is null) { _logger.Debug( $"Container: {chosenContainerId} not found in staticRandomisableContainersOnMap, this is bad" ); continue; } // Add loot to container and push into result object var containerWithLoot = AddLootToContainer( containerObject, staticForcedOnMapClone, staticLootDist.Value, staticAmmoDist, locationId ); result.Add(containerWithLoot.Template); staticContainerCount++; staticLootItemCount += containerWithLoot.Template.Items.Count; } } _logger.Success("A total of { staticLootItemCount}static items spawned"); _logger.Success( _localisationService.GetText("location-containers_generated_success", staticContainerCount) ); return result; } /// /// Get containers with a non-100% chance to spawn OR are NOT on the container type randomistion blacklist /// /// /// StaticContainerData array protected List GetRandomisableContainersOnMap(List staticContainers) { return staticContainers.Where( staticContainer => staticContainer.Probability != 1 && !staticContainer.Template.IsAlwaysSpawn.GetValueOrDefault(false) && !_locationConfig.ContainerRandomisationSettings.ContainerTypesToNotRandomise.Contains( staticContainer.Template.Items[0].Template ) ) .ToList(); } /// /// Get containers with 100% spawn rate or have a type on the randomistion ignore list /// /// /// IStaticContainerData array protected List GetGuaranteedContainers(List staticContainersOnMap) { return staticContainersOnMap.Where( staticContainer => staticContainer.Probability == 1 || staticContainer.Template.IsAlwaysSpawn.GetValueOrDefault(false) || _locationConfig.ContainerRandomisationSettings.ContainerTypesToNotRandomise.Contains( staticContainer.Template.Items[0].Template ) ) .ToList(); } /// /// Choose a number of containers based on their probabilty value to fulfil the desired count in /// containerData.chosenCount /// /// Name of the group the containers are being collected for /// Containers and probability values for a groupId /// List of chosen container Ids protected List GetContainersByProbability(string groupId, ContainerGroupCount containerData) { var chosenContainerIds = new List(); var containerIds = containerData.ContainerIdsWithProbability.Keys.ToList(); if (containerData.ChosenCount > containerIds.Count) { _logger.Debug( $"Group: {groupId} wants {containerData.ChosenCount} containers but pool only has {containerIds.Count}, add what's available" ); return containerIds; } // Create probability array with all possible container ids in this group and their relative probability of spawning var containerDistribution = new ProbabilityObjectArray, string, double>(_mathUtil, _cloner, []); foreach (var x in containerIds) { var value = containerData.ContainerIdsWithProbability[x]; containerDistribution.Add(new ProbabilityObject(x, value, value)); } chosenContainerIds.AddRange(containerDistribution.Draw(containerData.ChosenCount)); return chosenContainerIds; } /// /// Get a mapping of each groupid and the containers in that group + count of containers to spawn on map /// /// Container group values /// dictionary keyed by groupId protected Dictionary GetGroupIdToContainerMappings( StaticContainer staticContainerGroupData, List staticContainersOnMap) { // Create dictionary of all group ids and choose a count of containers the map will spawn of that group var mapping = new Dictionary(); foreach (var groupKvP in staticContainerGroupData.ContainersGroups) { var groupData = staticContainerGroupData.ContainersGroups[groupKvP.Key]; if (!mapping.ContainsKey(groupKvP.Key)) { mapping[groupKvP.Key] = new ContainerGroupCount { ContainerIdsWithProbability = new Dictionary(), ChosenCount = _randomUtil.GetInt( (int)Math.Round( groupData.MinContainers.Value * _locationConfig.ContainerRandomisationSettings.ContainerGroupMinSizeMultiplier ), (int)Math.Round( groupData.MaxContainers.Value * _locationConfig.ContainerRandomisationSettings.ContainerGroupMaxSizeMultiplier ) ) }; } } // Add an empty group for containers without a group id but still have a < 100% chance to spawn // Likely bad BSG data, will be fixed...eventually, example of the groupids: `NEED_TO_BE_FIXED1`,`NEED_TO_BE_FIXED_SE02`, `NEED_TO_BE_FIXED_NW_01` mapping[""] = new ContainerGroupCount { ChosenCount = -1 }; // Iterate over all containers and add to group keyed by groupId // Containers without a group go into a group with empty key "" foreach (var container in staticContainersOnMap) { var groupData = staticContainerGroupData.Containers[container.Template.Id]; if (groupData is null) { _logger.Error( _localisationService.GetText( "location-unable_to_find_container_in_statics_json", container.Template.Id ) ); continue; } if (container.Probability == 1) { _logger.Debug( $"Container {container.Template.Id} with group ${groupData.GroupId} had 100 % chance to spawn was picked as random container, skipping" ); continue; } mapping[groupData.GroupId].ContainerIdsWithProbability[container.Template.Id] = container.Probability.Value; } return mapping; } /// /// Choose loot to put into a static container based on weighting /// Handle forced items + seasonal item removal when not in season /// /// The container itself we will add loot to /// Loot we need to force into the container /// staticLoot.json /// staticAmmo.json /// Name of the map to generate static loot for /// StaticContainerData protected StaticContainerData AddLootToContainer(StaticContainerData staticContainer, List staticForced, Dictionary staticLootDist, Dictionary> staticAmmoDist, string locationName ) { throw new NotImplementedException(); } /// /// Get a 2D grid of a container's item slots /// /// Tpl id of the container protected int[][] GetContainerMapping(string containerTpl) { // Get template from db var containerTemplate = _itemHelper.GetItem(containerTpl).Value; // Get height/width var height = containerTemplate.Properties.Grids[0].Props.CellsV; var width = containerTemplate.Properties.Grids[0].Props.CellsH; return _inventoryHelper.GetBlankContainerMap(height.Value, width.Value); } /// /// Look up a containers itemcountDistribution data and choose an item count based on the found weights /// /// Container to get item count for /// staticLoot.json /// Map name (to get per-map multiplier for from config) /// item count protected int GetWeightedCountOfContainerItems(string containerTypeId, Dictionary staticLootDist, string locationName) { // Create probability array to calcualte the total count of lootable items inside container var itemCountArray = new ProbabilityObjectArray, int, float?>(_mathUtil, _cloner, []); var countDistribution = staticLootDist[containerTypeId]?.ItemCountDistribution; if (countDistribution is null) { _logger.Warning( _localisationService.GetText( "location-unable_to_find_count_distribution_for_container", new { containerId = containerTypeId, locationName } ) ); return 0; } foreach (var itemCountDistribution in countDistribution) // Add each count of items into array itemCountArray.Add( new ProbabilityObject( itemCountDistribution.Count.Value, itemCountDistribution.RelativeProbability.Value, null ) ); return (int)Math.Round(GetStaticLootMultiplierForLocation(locationName) * itemCountArray.Draw()[0]); } /// /// Get all possible loot items that can be placed into a container /// Do not add seasonal items if found + current date is inside seasonal event /// /// Container to get possible loot for /// staticLoot.json /// ProbabilityObjectArray of item tpls + probabilty protected object GetPossibleLootItemsForContainer(string containerTypeId, Dictionary staticLootDist) // TODO: Type Fuckery, return type was ProbabilityObjectArray { var seasonalEventActive = _seasonalEventService.SeasonalEventEnabled(); var seasonalItemTplBlacklist = _seasonalEventService.GetInactiveSeasonalEventItems(); var itemDistribution = new ProbabilityObjectArray, string, float?>(_mathUtil, _cloner, []); var itemContainerDistribution = staticLootDist[containerTypeId]?.ItemDistribution; if (itemContainerDistribution is null) { _logger.Warning(_localisationService.GetText("location-missing_item_distribution_data", containerTypeId)); return itemDistribution; } foreach (var icd in itemContainerDistribution) { if (!seasonalEventActive && seasonalItemTplBlacklist.Contains(icd.Tpl)) { // Skip seasonal event items if they're not enabled continue; } // Ensure no blacklisted lootable items are in pool if (_itemFilterService.IsLootableItemBlacklisted(icd.Tpl)) { continue; } itemDistribution.Add(new ProbabilityObject(icd.Tpl, icd.RelativeProbability.Value, null)); } return itemDistribution; } protected double GetLooseLootMultiplierForLocation(string location) { return _locationConfig.LooseLootMultiplier[location]; } protected double GetStaticLootMultiplierForLocation(string location) { return _locationConfig.StaticLootMultiplier[location]; } /// /// Create array of loose + forced loot using probability system /// /// /// /// Location to generate loot for /// Array of spawn points with loot in them public List GenerateDynamicLoot(LooseLoot dynamicLootDist, Dictionary> staticAmmoDist, string locationName) { throw new NotImplementedException(); } /// /// Add forced spawn point loot into loot parameter list /// /// List to add forced loot spawn locations to /// Forced loot locations that must be added /// Name of map currently having force loot created for protected void addForcedLoot(List lootLocationTemplates, List forcedSpawnPoints, string locationName, Dictionary> staticAmmoDist) { throw new NotImplementedException(); } // TODO: rewrite, BIG yikes protected ContainerItem CreateStaticLootItem(string chosenTemplate, Dictionary> staticAmmoDistribution, string? parentIdentifier = null) { throw new NotImplementedException(); } } public class ContainerGroupCount { [JsonPropertyName("containerIdsWithProbability")] public Dictionary ContainerIdsWithProbability { get; set; } [JsonPropertyName("chosenCount")] public int ChosenCount { get; set; } } public class ContainerItem { [JsonPropertyName("items")] public List Items { get; set; } [JsonPropertyName("width")] public int Width { get; set; } [JsonPropertyName("height")] public int Height { get; set; } }