diff --git a/Libraries/Core/Generators/LocationLootGenerator.cs b/Libraries/Core/Generators/LocationLootGenerator.cs index ea385116..564013e8 100644 --- a/Libraries/Core/Generators/LocationLootGenerator.cs +++ b/Libraries/Core/Generators/LocationLootGenerator.cs @@ -1,75 +1,376 @@ -using System.Text.Json.Serialization; -using SptCommon.Annotations; +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 +) { - private LocationConfig _locationConfig; - private SeasonalEventConfig _seasonalEventConfig; + 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) + public List GenerateStaticContainers(LocationBase locationBase, + Dictionary> staticAmmoDist) { - throw new NotImplementedException(); + 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 + /// 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) { - throw new NotImplementedException(); + 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 + /// Get containers with 100% spawn rate or have a type on the randomistion ignore list /// /// /// IStaticContainerData array protected List GetGuaranteedContainers(List staticContainersOnMap) { - throw new NotImplementedException(); + 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 + /// 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 GetContainersByProbabilty(string groupId, ContainerGroupCount containerData) + protected List GetContainersByProbability(string groupId, ContainerGroupCount containerData) { - throw new NotImplementedException(); + 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 + /// 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( - object staticContainerGroupData, // TODO: Type fuckery staticContainerGroupData was IStaticContainer | Record + StaticContainer staticContainerGroupData, List staticContainersOnMap) { - throw new NotImplementedException(); + // 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 + /// 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 @@ -77,38 +378,76 @@ public class LocationLootGenerator( /// 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 + 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 + /// Get a 2D grid of a container's item slots /// /// Tpl id of the container - /// List> - protected List> GetContainerMapping(string containerTpl) + protected int[][] GetContainerMapping(string containerTpl) { - throw new NotImplementedException(); + // 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 + /// 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) + protected int GetWeightedCountOfContainerItems(string containerTypeId, + Dictionary staticLootDist, string locationName) { - throw new NotImplementedException(); + // 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 + /// 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 @@ -116,46 +455,80 @@ public class LocationLootGenerator( protected object GetPossibleLootItemsForContainer(string containerTypeId, Dictionary staticLootDist) // TODO: Type Fuckery, return type was ProbabilityObjectArray { - throw new NotImplementedException(); + 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 GetLooseLootMultiplerForLocation(string location) + protected double GetLooseLootMultiplierForLocation(string location) { - throw new NotImplementedException(); + return _locationConfig.LooseLootMultiplier[location]; } protected double GetStaticLootMultiplierForLocation(string location) { - throw new NotImplementedException(); + return _locationConfig.StaticLootMultiplier[location]; } /// - /// Create array of loose + forced loot using probability system + /// 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, + public List GenerateDynamicLoot(LooseLoot dynamicLootDist, + Dictionary> staticAmmoDist, string locationName) { throw new NotImplementedException(); } /// - /// Add forced spawn point loot into loot parameter list + /// 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, + 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, + protected ContainerItem CreateStaticLootItem(string chosenTemplate, + Dictionary> staticAmmoDistribution, string? parentIdentifier = null) { throw new NotImplementedException(); diff --git a/Libraries/Core/Helpers/InventoryHelper.cs b/Libraries/Core/Helpers/InventoryHelper.cs index 6cda24c1..b6711e6c 100644 --- a/Libraries/Core/Helpers/InventoryHelper.cs +++ b/Libraries/Core/Helpers/InventoryHelper.cs @@ -731,7 +731,7 @@ public class InventoryHelper( /// Horizontal size of container /// Vertical size of container /// Two-dimensional representation of container - protected int[][] GetBlankContainerMap(int containerH, int containerY) + public int[][] GetBlankContainerMap(int containerH, int containerY) { //var x = new int[containerY][]; //for (int i = 0; i < containerY; i++) diff --git a/Libraries/Core/Models/Eft/Common/Location.cs b/Libraries/Core/Models/Eft/Common/Location.cs index 1b5c4ff2..77fdc68b 100644 --- a/Libraries/Core/Models/Eft/Common/Location.cs +++ b/Libraries/Core/Models/Eft/Common/Location.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; using Core.Models.Eft.Common.Tables; using Core.Utils.Json; @@ -33,9 +33,8 @@ public record Location [JsonPropertyName("allExtracts")] public Exit[] AllExtracts { get; set; } - // TODO: talk to chomp about this type! - [JsonPropertyName("statics")] - public Dictionary? Statics { get; set; } + [Obsolete("USE StaticContainer INSTEAD")] + public object Statics { get; set; } } public record StaticContainer @@ -131,6 +130,7 @@ public record StaticPropsBase public Item[] Items { get; set; } } +[Obsolete("use SpawnpointTemplate")] public record StaticWeaponProps : StaticPropsBase { [JsonPropertyName("Items")] @@ -140,13 +140,13 @@ public record StaticWeaponProps : StaticPropsBase public record StaticContainerDetails { [JsonPropertyName("staticWeapons")] - public StaticWeaponProps[] StaticWeapons { get; set; } + public List StaticWeapons { get; set; } [JsonPropertyName("staticContainers")] - public StaticContainerData[] StaticContainers { get; set; } + public List StaticContainers { get; set; } [JsonPropertyName("staticForced")] - public StaticForcedProps[] StaticForced { get; set; } + public List StaticForced { get; set; } } public record StaticContainerData @@ -155,7 +155,7 @@ public record StaticContainerData public float? Probability { get; set; } [JsonPropertyName("template")] - public StaticContainerProps? Template { get; set; } + public SpawnpointTemplate? Template { get; set; } } public record StaticAmmoDetails @@ -176,6 +176,7 @@ public record StaticForcedProps public string? ItemTpl { get; set; } } +[Obsolete("use SpawnpointTemplate")] public record StaticContainerProps : StaticPropsBase { [JsonPropertyName("Items")] diff --git a/Libraries/Core/Models/Spt/Config/LocationConfig.cs b/Libraries/Core/Models/Spt/Config/LocationConfig.cs index 8344748f..9d599699 100644 --- a/Libraries/Core/Models/Spt/Config/LocationConfig.cs +++ b/Libraries/Core/Models/Spt/Config/LocationConfig.cs @@ -22,10 +22,10 @@ public record LocationConfig : BaseConfig public SplitWaveSettings SplitWaveIntoSingleSpawnsSettings { get; set; } [JsonPropertyName("looseLootMultiplier")] - public LootMultiplier LooseLootMultiplier { get; set; } + public Dictionary LooseLootMultiplier { get; set; } [JsonPropertyName("staticLootMultiplier")] - public LootMultiplier StaticLootMultiplier { get; set; } + public Dictionary StaticLootMultiplier { get; set; } /// /// Custom bot waves to add to a locations base json on game start if addCustomBotWavesToMaps is true diff --git a/Libraries/Core/Services/RaidTimeAdjustmentService.cs b/Libraries/Core/Services/RaidTimeAdjustmentService.cs index 2e433dc4..8aa636d9 100644 --- a/Libraries/Core/Services/RaidTimeAdjustmentService.cs +++ b/Libraries/Core/Services/RaidTimeAdjustmentService.cs @@ -1,4 +1,4 @@ -using Core.Context; +using Core.Context; using Core.Helpers; using SptCommon.Annotations; using Core.Models.Eft.Common; @@ -8,6 +8,7 @@ using Core.Models.Spt.Location; using Core.Models.Utils; using Core.Servers; using Core.Utils; +using Microsoft.AspNetCore.DataProtection.KeyManagement; namespace Core.Services; @@ -35,7 +36,7 @@ public class RaidTimeAdjustmentService( $"Adjusting dynamic loot multipliers to {raidAdjustments.DynamicLootPercent}% and static loot multipliers to {raidAdjustments.StaticLootPercent}% of original" ); - // Change loot multipler values before they're used below + // Change loot multiplier values before they're used below AdjustLootMultipliers(_locationConfig.LooseLootMultiplier, raidAdjustments.DynamicLootPercent); AdjustLootMultipliers(_locationConfig.StaticLootMultiplier, raidAdjustments.StaticLootPercent); @@ -50,15 +51,13 @@ public class RaidTimeAdjustmentService( /// /// Adjust the loot multiplier values passed in to be a % of their original value /// - /// Multipliers to adjust + /// Multipliers to adjust /// Percent to change values to - protected void AdjustLootMultipliers(LootMultiplier mapLootMultiplers, double? loosePercent) + protected void AdjustLootMultipliers(Dictionary mapLootMultiplers, double? loosePercent) { - var props = mapLootMultiplers.GetType().GetProperties(); - foreach (var multiplier in props) + foreach (var location in mapLootMultiplers) { - var propValue = (double)multiplier.GetValue(mapLootMultiplers); - multiplier.SetValue(mapLootMultiplers, _randomUtil.GetPercentOfValue(propValue, loosePercent ?? 1)); + mapLootMultiplers[location.Key] = _randomUtil.GetPercentOfValue(mapLootMultiplers[location.Key], loosePercent ?? 1); } }