diff --git a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/location.json b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/location.json index 525c91f1..162ae848 100644 --- a/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/location.json +++ b/Libraries/SPTarkov.Server.Assets/SPT_Data/configs/location.json @@ -46,94 +46,100 @@ "normal": {} }, "openZones": {}, - "forcedLootSingleSpawnById": { - "bigmap": [ - "5ac620eb86f7743a8e6e0da0", - "5939e5a786f77461f11c0098", - "64e74a3d4d49d23b2c39d319", - "6614230055afee107f05e998", - "66b22630a6b4e5ec7c02cdb7", - "675f80d4fe1b59cf490d3527", - "67499d0eeca8acb2d2061639", - "675f7acc4076a741a3061566", - "675f80d4fe1b59cf490d3527", - "675f7f224076a741a3061568", - "675f7b168d28a25ec7007dbb" - ], - "interchange": ["64e74a5ac2b4f829615ec336", "667a8ef464eea5fdef0db135"], - "lighthouse": [ - "6331bb0d1aa9f42b804997a6", - "6398a0861c712b1e1d4dadf1", - "6399f54b0a36db13c823ad21", - "64e74a64aac4cd0a7264ecdf", - "661666458c2aa9cb1602503b" - ], - "rezervbase": [ - "64e74a4baac4cd0a7264ecdd", - "6398a072e301557ae24cec92", - "67499b3eeca8acb2d2061636", - "67499b9b909d2013670a5029" - ], - "shoreline": [ - "64e74a534d49d23b2c39d31b", - "661421c7c1f2f548c50ee649", - "6614217b6d9d5abcad0ff098", - "661423200d240a5f5d0f679b", - "6707d1f9571b50abc703b651", - "66760b3deb51b08bd40c2b08", - "67499adbeca8acb2d2061634", - "6614238e0d240a5f5d0f679d", - "666073159916667083033cb9" - ], - "tarkovstreets": [ - "638df4cc7b560b03794a18d2", - "638cbc68a63f1b49be6a3010", - "638e0057ab150a5f56238960", - "63927b29c115f907b14700b9", - "638dfc803083a019d447768e", - "638e9d5536b3b72c944e2fc7", - "6393262086e646067c176aa2", - "63989ced706b793c7d60cfef", - "63a39e1d234195315d4020bd", - "64e74a35aac4cd0a7264ecdb", - "64e74a186393886f74114a96", - "64e74a1faac4cd0a7264ecd9", - "64e73909cd54ef0580746af3", - "64e74a2fc2b4f829615ec332", - "64e74a274d49d23b2c39d317", - "64f09c02b63b74469b6c149f", - "64f07f7726cfa02c506f8ac0", - "64f69b4267e11a7c6206e010", - "64f5b4f71a5f313cb144c06c", - "657acb2ac900be5902191ac9", - "6582dbf0b8d7830efc45016f", - "66687bc89111279d600b5062" - ], - "laboratory": [ - "6398a4cfb5992f573c6562b3", - "64e74a44c2b4f829615ec334", - "6711039f9e648049e50b3307", - "6707cef3571b50abc703b64f", - "6707cd70aab679420007e018", - "6707cc67cc1667e49e0f7232", - "6707cf827d279daad80fa95f" - ], - "sandbox": ["6575a6ca8778e96ded05a802", "6582bd252b50c61c565828e2"], - "factory4_day": [ - "591093bb86f7747caa7bb2ee", - "66c0b39ca1f68fcc1d0c0cc3", - "66a0e523e749756c920d02d0", - "593a87af86f774122f54a951" - ], - "factory4_night": [ - "591093bb86f7747caa7bb2ee", - "66c0b39ca1f68fcc1d0c0cc3", - "66a0e523e749756c920d02d0", - "593a87af86f774122f54a951" - ], - "labyrinth": [ - "679b992329acd1f2f60985a5" - ] + "lootMaxSpawnLimits": { + "bigmap": { + "5ac620eb86f7743a8e6e0da0": 1, + "5939e5a786f77461f11c0098": 1, + "64e74a3d4d49d23b2c39d319": 1, + "6614230055afee107f05e998": 1, + "66b22630a6b4e5ec7c02cdb7": 1, + "675f80d4fe1b59cf490d3527": 1, + "67499d0eeca8acb2d2061639": 1, + "675f7acc4076a741a3061566": 1, + "675f80d4fe1b59cf490d3527": 1, + "675f7f224076a741a3061568": 1, + "675f7b168d28a25ec7007dbb": 1 + }, + "interchange": { + "64e74a5ac2b4f829615ec336": 1, + "667a8ef464eea5fdef0db135": 1 + }, + "lighthouse": { + "6331bb0d1aa9f42b804997a6": 1, + "6398a0861c712b1e1d4dadf1": 1, + "6399f54b0a36db13c823ad21": 1, + "64e74a64aac4cd0a7264ecdf": 1, + "661666458c2aa9cb1602503b": 1 + }, + "rezervbase": { + "64e74a4baac4cd0a7264ecdd": 1, + "6398a072e301557ae24cec92": 1, + "67499b3eeca8acb2d2061636": 1, + "67499b9b909d2013670a5029": 1 + }, + "shoreline": { + "64e74a534d49d23b2c39d31b": 1, + "661421c7c1f2f548c50ee649": 1, + "6614217b6d9d5abcad0ff098": 1, + "661423200d240a5f5d0f679b": 1, + "6707d1f9571b50abc703b651": 1, + "66760b3deb51b08bd40c2b08": 1, + "67499adbeca8acb2d2061634": 1, + "6614238e0d240a5f5d0f679d": 1, + "666073159916667083033cb9": 1 + }, + "tarkovstreets": { + "638df4cc7b560b03794a18d2": 1, + "638cbc68a63f1b49be6a3010": 1, + "638e0057ab150a5f56238960": 1, + "63927b29c115f907b14700b9": 1, + "638dfc803083a019d447768e": 1, + "638e9d5536b3b72c944e2fc7": 1, + "6393262086e646067c176aa2": 1, + "63989ced706b793c7d60cfef": 1, + "63a39e1d234195315d4020bd": 1, + "64e74a35aac4cd0a7264ecdb": 1, + "64e74a186393886f74114a96": 1, + "64e74a1faac4cd0a7264ecd9": 1, + "64e73909cd54ef0580746af3": 1, + "64e74a2fc2b4f829615ec332": 1, + "64e74a274d49d23b2c39d317": 1, + "64f09c02b63b74469b6c149f": 1, + "64f07f7726cfa02c506f8ac0": 1, + "64f69b4267e11a7c6206e010": 1, + "64f5b4f71a5f313cb144c06c": 1, + "657acb2ac900be5902191ac9": 1, + "6582dbf0b8d7830efc45016f": 1, + "66687bc89111279d600b5062": 1 + }, + "laboratory": { + "6398a4cfb5992f573c6562b3": 1, + "64e74a44c2b4f829615ec334": 1, + "6711039f9e648049e50b3307": 1, + "6707cef3571b50abc703b64f": 1, + "6707cd70aab679420007e018": 1, + "6707cc67cc1667e49e0f7232": 1, + "6707cf827d279daad80fa95f": 1 + }, + "sandbox": { + "6575a6ca8778e96ded05a802": 1, + "6582bd252b50c61c565828e2": 1 + }, + "factory4_day": { + "591093bb86f7747caa7bb2ee": 1, + "66c0b39ca1f68fcc1d0c0cc3": 1, + "66a0e523e749756c920d02d0": 1, + "593a87af86f774122f54a951": 1 + }, + "factory4_night": { + "591093bb86f7747caa7bb2ee": 1, + "66c0b39ca1f68fcc1d0c0cc3": 1, + "66a0e523e749756c920d02d0": 1, + "593a87af86f774122f54a951": 1 + }, + "labyrinth": { + "679b992329acd1f2f60985a5": 5 + } }, "rogueLighthouseSpawnTimeSettings": { "enabled": false, diff --git a/Libraries/SPTarkov.Server.Core/Generators/LocationLootGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/LocationLootGenerator.cs index 6db404b0..acc0fe51 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/LocationLootGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/LocationLootGenerator.cs @@ -29,6 +29,7 @@ public class LocationLootGenerator( SeasonalEventService _seasonalEventService, ItemFilterService _itemFilterService, ConfigServer _configServer, + CounterTrackerHelper counterTrackerHelper, ICloner _cloner ) { @@ -349,7 +350,7 @@ public class LocationLootGenerator( containerDistribution.Add(new ProbabilityObject(x, value, value)); } - chosenContainerIds.AddRange(containerDistribution.Draw((int)containerData.ChosenCount)); + chosenContainerIds.AddRange(containerDistribution.Draw((int) containerData.ChosenCount)); return chosenContainerIds; } @@ -509,7 +510,8 @@ public class LocationLootGenerator( // Filter out items picked that are already in the above `tplsForced` array var chosenTpls = containerLootPool .Draw(itemCountToAdd, _locationConfig.AllowDuplicateItemsInStaticContainers, lockList) - .Where(tpl => !tplsForced.Contains(tpl)); + .Where(tpl => !tplsForced.Contains(tpl)) + .Where(tpl => !counterTrackerHelper.IncrementCount(tpl)); // Add forced loot to chosen item pool var tplsToAddToContainer = tplsForced.Concat(chosenTpls); @@ -695,12 +697,13 @@ public class LocationLootGenerator( /// /// /// Location to generate loot for + /// /// Array of spawn points with loot in them public List GenerateDynamicLoot( LooseLoot dynamicLootDist, Dictionary> staticAmmoDist, - string locationName - ) + string locationName, + Dictionary spawnLimitedLoot) { List loot = []; List dynamicForcedSpawnPoints = []; @@ -726,28 +729,29 @@ public class LocationLootGenerator( dynamicLootDist.Spawnpoints.Where(point => point.Template.IsAlwaysSpawn ?? false) ); - // Add forced loot - AddForcedLoot(loot, dynamicForcedSpawnPoints, locationName, staticAmmoDist); - - var allDynamicSpawnPoints = dynamicLootDist.Spawnpoints; + // Add forced loot to results + AddForcedLoot(loot, dynamicForcedSpawnPoints, locationName, staticAmmoDist, spawnLimitedLoot); // Draw from random distribution var desiredSpawnPointCount = Math.Round( GetLooseLootMultiplierForLocation(locationName) * _randomUtil.GetNormallyDistributedRandomNumber( - (double)dynamicLootDist.SpawnpointCount.Mean, - (double)dynamicLootDist.SpawnpointCount.Std + dynamicLootDist.SpawnpointCount.Mean, + dynamicLootDist.SpawnpointCount.Std ) ); - // Positions not in forced but have 100% chance to spawn - List guaranteedLoosePoints = []; - var blacklistedSpawnPoints = _locationConfig.LooseLootBlacklist.GetValueOrDefault( locationName ); + + // Init empty array to hold spawn points, letting us pick them pseudo-randomly var spawnPointArray = new ProbabilityObjectArray(_mathUtil, _cloner); + // Positions not in forced but have 100% chance to spawn + List guaranteedLoosePoints = []; + + var allDynamicSpawnPoints = dynamicLootDist.Spawnpoints; foreach (var spawnPoint in allDynamicSpawnPoints) { // Point is blacklisted, skip @@ -793,7 +797,7 @@ public class LocationLootGenerator( if (randomSpawnPointCount > 0 && spawnPointArray.Count > 0) // Add randomly chosen spawn points { - foreach (var si in spawnPointArray.Draw((int)randomSpawnPointCount, false)) + foreach (var si in spawnPointArray.Draw((int) randomSpawnPointCount, false)) { chosenSpawnPoints.Add(spawnPointArray.Data(si)); } @@ -915,6 +919,12 @@ public class LocationLootGenerator( staticAmmoDist ); + // If count reaches max, skip adding item to loot + if (counterTrackerHelper.IncrementCount(createItemResult.Items.FirstOrDefault().Template)) + { + continue; + } + // Root id can change when generating a weapon, ensure ids match spawnPoint.Template.Root = createItemResult.Items.FirstOrDefault().Id; @@ -922,6 +932,7 @@ public class LocationLootGenerator( spawnPoint.Template.Items = createItemResult.Items; loot.Add(spawnPoint.Template); + } return loot; @@ -933,19 +944,20 @@ public class LocationLootGenerator( /// 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 - ) + Dictionary> staticAmmoDist, + Dictionary spawnLimitedLoot) { - var lootToForceSingleAmountOnMap = - _locationConfig.ForcedLootSingleSpawnById.GetValueOrDefault(locationName); - if (lootToForceSingleAmountOnMap is not null) + + if (spawnLimitedLoot is not null) // Process loot items defined as requiring only 1 spawn position as they appear in multiple positions on the map { - foreach (var itemTpl in lootToForceSingleAmountOnMap) + foreach (var (itemTpl, itemSpawnCountMax) in spawnLimitedLoot) { // Get all spawn positions for item tpl in forced loot array var items = forcedSpawnPoints.Where(forcedSpawnPoint => @@ -969,7 +981,7 @@ public class LocationLootGenerator( _cloner ); foreach (var si in items) - // use locationId as template.Id is the same across all items + // Use locationId as template.Id is the same across all items { spawnPointArray.Add( new ProbabilityObject( @@ -980,8 +992,8 @@ public class LocationLootGenerator( ); } - // Choose 1 out of all found spawn positions for spawn id and add to loot array - foreach (var spawnPointLocationId in spawnPointArray.Draw(1, false)) + // Choose count from config of spawn positions for spawn id and add to loot array + foreach (var spawnPointLocationId in spawnPointArray.Draw(itemSpawnCountMax, false)) { var itemToAdd = items.FirstOrDefault(item => item.LocationId == spawnPointLocationId @@ -1001,6 +1013,12 @@ public class LocationLootGenerator( staticAmmoDist ); + // If count reaches max, skip adding item to loot + if (counterTrackerHelper.IncrementCount(itemTpl)) + { + continue; + } + // Update root ID with the dynamically generated ID lootItem.Root = createItemResult.Items.FirstOrDefault().Id; lootItem.Items = createItemResult.Items; @@ -1018,7 +1036,7 @@ public class LocationLootGenerator( var firstLootItemTpl = forcedLootLocation.Template.Items.FirstOrDefault().Template; // Skip spawn positions processed already - if (lootToForceSingleAmountOnMap?.Contains(firstLootItemTpl) ?? false) + if (spawnLimitedLoot?.ContainsKey(firstLootItemTpl) ?? false) { continue; } diff --git a/Libraries/SPTarkov.Server.Core/Helpers/CounterTrackerHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/CounterTrackerHelper.cs new file mode 100644 index 00000000..c8366702 --- /dev/null +++ b/Libraries/SPTarkov.Server.Core/Helpers/CounterTrackerHelper.cs @@ -0,0 +1,46 @@ +using SPTarkov.DI.Annotations; + +namespace SPTarkov.Server.Core.Helpers +{ + [Injectable] + public class CounterTrackerHelper + { + private Dictionary _maxCounts = new(); + private readonly Dictionary _trackedCounts = new(); + + /// + /// Add dictionary of keys and their matching limits to track + /// + /// Values to store + public void AddDataToTrack(Dictionary maxCounts) + { + _maxCounts = maxCounts; + } + + /// + /// Increment the counter for passed in key, get back value determining if max value passed + /// + /// + /// + /// True = above max count + public bool IncrementCount(string key, int countToIncrementBy = 1) + { + // Not tracked, skip + if (!_maxCounts.ContainsKey(key)) + { + return false; + } + + _trackedCounts.TryAdd(key, 0); + _trackedCounts[key] += countToIncrementBy; + + return _trackedCounts[key] > _maxCounts[key]; + } + + public void Clear() + { + _trackedCounts.Clear(); + _maxCounts.Clear(); + } + } +} diff --git a/Libraries/SPTarkov.Server.Core/Models/Eft/Common/LooseLoot.cs b/Libraries/SPTarkov.Server.Core/Models/Eft/Common/LooseLoot.cs index 5cc578f1..6ec1e1d0 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Eft/Common/LooseLoot.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Eft/Common/LooseLoot.cs @@ -24,10 +24,10 @@ public record SpawnpointCount public Dictionary ExtensionData { get; set; } [JsonPropertyName("mean")] - public double? Mean { get; set; } + public required double Mean { get; set; } [JsonPropertyName("std")] - public double? Std { get; set; } + public required double Std { get; set; } } public record SpawnpointTemplate diff --git a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/LocationConfig.cs b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/LocationConfig.cs index aa37e663..cfd3f175 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Spt/Config/LocationConfig.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Spt/Config/LocationConfig.cs @@ -34,10 +34,10 @@ public record LocationConfig : BaseConfig public required Dictionary> OpenZones { get; set; } /// - /// Key = map id, value = item tpls that should only have one forced loot spawn position + /// Key = map id, value = dict of item tpls that should only have x forced loot spawn position /// - [JsonPropertyName("forcedLootSingleSpawnById")] - public required Dictionary> ForcedLootSingleSpawnById { get; set; } + [JsonPropertyName("lootMaxSpawnLimits")] + public required Dictionary> LootMaxSpawnLimits { get; set; } /// /// How many attempts should be taken to fit an item into a container before giving up diff --git a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs index e11fc45e..6797d7e6 100644 --- a/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/LocationLifecycleService.cs @@ -54,6 +54,7 @@ public class LocationLifecycleService protected TraderConfig _traderConfig; protected TraderHelper _traderHelper; protected BtrDeliveryService _btrDeliveryService; + private readonly CounterTrackerHelper _counterTrackerHelper; public LocationLifecycleService( ISptLogger logger, @@ -84,7 +85,8 @@ public class LocationLifecycleService QuestHelper questHelper, InsuranceService insuranceService, MatchBotDetailsCacheService matchBotDetailsCacheService, - BtrDeliveryService btrDeliveryService + BtrDeliveryService btrDeliveryService, + CounterTrackerHelper counterTrackerHelper ) { _logger = logger; @@ -116,6 +118,7 @@ public class LocationLifecycleService _insuranceService = insuranceService; _matchBotDetailsCacheService = matchBotDetailsCacheService; _btrDeliveryService = btrDeliveryService; + _counterTrackerHelper = counterTrackerHelper; _locationConfig = _configServer.GetConfig(); _inRaidConfig = _configServer.GetConfig(); @@ -390,13 +393,13 @@ public class LocationLifecycleService return locationBaseClone; } - // Add cusom pmcs to map every time its run + // Add custom pmcs to map every time its run _pmcWaveGenerator.ApplyWaveChangesToMap(locationBaseClone); // Adjust raid based on whether this is a scav run LocationConfig? locationConfigClone = null; var raidAdjustments = _profileActivityService - .GetProfileActivityRaidData(sessionId) + .GetProfileActivityRaidData(sessionId)? .RaidAdjustments; if (raidAdjustments is not null) { @@ -406,6 +409,11 @@ public class LocationLifecycleService var staticAmmoDist = _cloner.Clone(location.StaticAmmo); + var itemsWithSpawnCountLimits = _cloner.Clone(_locationConfig.LootMaxSpawnLimits.GetValueOrDefault(name.ToLower())); + + // Store items with spawn count limits inside so they can be accessed later inside static/dynamic loot spawn methods + _counterTrackerHelper.AddDataToTrack(itemsWithSpawnCountLimits); + // Create containers and add loot to them var staticLoot = _locationLootGenerator.GenerateStaticContainers( locationBaseClone, @@ -418,7 +426,8 @@ public class LocationLifecycleService var dynamicSpawnPoints = _locationLootGenerator.GenerateDynamicLoot( dynamicLootDistClone, staticAmmoDist, - name.ToLower() + name.ToLower(), + itemsWithSpawnCountLimits ); // Push chosen spawn points into returned object @@ -446,6 +455,9 @@ public class LocationLifecycleService _profileActivityService.GetProfileActivityRaidData(sessionId).RaidAdjustments = null; } + // Clean up tracker + _counterTrackerHelper.Clear(); + return locationBaseClone; } @@ -614,7 +626,7 @@ public class LocationLifecycleService // Check if new standing has leveled up trader _traderHelper.LevelUp(fenceId, pmcData); pmcData.TradersInfo[fenceId].LoyaltyLevel = Math.Max( - (int)pmcData.TradersInfo[fenceId].LoyaltyLevel, + (int) pmcData.TradersInfo[fenceId].LoyaltyLevel, 1 ); @@ -652,7 +664,7 @@ public class LocationLifecycleService // Check if new standing has leveled up trader _traderHelper.LevelUp(fenceId, pmcData); pmcData.TradersInfo[fenceId].LoyaltyLevel = Math.Max( - (int)pmcData.TradersInfo[fenceId].LoyaltyLevel, + (int) pmcData.TradersInfo[fenceId].LoyaltyLevel, 1 ); @@ -684,7 +696,7 @@ public class LocationLifecycleService fenceStanding += Math.Max(baseGain / extractCount, 0.01); // Ensure fence loyalty level is not above/below the range -7 to 15 - var newFenceStanding = Math.Min(Math.Max((double)fenceStanding, -7), 15); + var newFenceStanding = Math.Min(Math.Max((double) fenceStanding, -7), 15); _logger.Debug( $"Old vs new fence standing: {pmcData.TradersInfo[fenceId].Standing}, {newFenceStanding}" ); @@ -988,7 +1000,7 @@ public class LocationLifecycleService // Clamp fence standing var currentFenceStanding = postRaidProfile.TradersInfo[fenceId].Standing; pmcProfile.TradersInfo[fenceId].Standing = Math.Min( - Math.Max((double)currentFenceStanding, -7), + Math.Max((double) currentFenceStanding, -7), 15 ); // Ensure it stays between -7 and 15