diff --git a/Libraries/SPTarkov.Server.Core/Generators/PMCLootGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/PMCLootGenerator.cs index d77057ff..ead27e71 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/PMCLootGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/PMCLootGenerator.cs @@ -10,49 +10,27 @@ using SPTarkov.Server.Core.Services; namespace SPTarkov.Server.Core.Generators; [Injectable] -public class PMCLootGenerator +public class PMCLootGenerator( + ISptLogger logger, + DatabaseService databaseService, + ItemHelper itemHelper, + ItemFilterService itemFilterService, + RagfairPriceService ragfairPriceService, + SeasonalEventService seasonalEventService, + WeightedRandomHelper weightedRandomHelper, + ConfigServer configServer) { - private readonly ConfigServer _configServer; - private readonly DatabaseService _databaseService; - private readonly ItemFilterService _itemFilterService; - private readonly ItemHelper _itemHelper; - private readonly ISptLogger _logger; - private readonly PmcConfig _pmcConfig; - private readonly RagfairPriceService _ragfairPriceService; - private readonly SeasonalEventService _seasonalEventService; - private readonly WeightedRandomHelper _weightedRandomHelper; + private readonly PmcConfig _pmcConfig = configServer.GetConfig(); - private Dictionary? _backpackLootPool; - private Dictionary? _pocketLootPool; - private Dictionary? _vestLootPool; + // Store loot against its type, usec/bear + private readonly Dictionary>? _backpackLootPool = []; + private readonly Dictionary>? _pocketLootPool = []; + private readonly Dictionary>? _vestLootPool = []; protected readonly Lock BackpackLock = new(); protected readonly Lock PocketLock = new(); protected readonly Lock VestLock = new(); - public PMCLootGenerator( - ISptLogger logger, - DatabaseService databaseService, - ItemHelper itemHelper, - ItemFilterService itemFilterService, - RagfairPriceService ragfairPriceService, - SeasonalEventService seasonalEventService, - WeightedRandomHelper weightedRandomHelper, - ConfigServer configServer - ) - { - _logger = logger; - _databaseService = databaseService; - _itemHelper = itemHelper; - _itemFilterService = itemFilterService; - _ragfairPriceService = ragfairPriceService; - _seasonalEventService = seasonalEventService; - _weightedRandomHelper = weightedRandomHelper; - _configServer = configServer; - - _pmcConfig = _configServer.GetConfig(); - } - /// /// Create a List of loot items a PMC can have in their pockets /// @@ -62,76 +40,26 @@ public class PMCLootGenerator { lock (PocketLock) { - // Hydrate loot dictionary if empty - if (_pocketLootPool is not null) + // Already exists, return values + if (_pocketLootPool.TryGetValue(pmcRole, out var existingLootPool)) { - return _pocketLootPool; + return existingLootPool; } - _pocketLootPool = new Dictionary(); - var items = _databaseService.GetItems(); - var pmcPriceOverrides = - _databaseService.GetBots().Types[string.Equals(pmcRole, "pmcbear", StringComparison.OrdinalIgnoreCase) ? "bear" : "usec"].BotInventory.Items - .Pockets; - + // Get a set of item types we want to generate var allowedItemTypeWhitelist = _pmcConfig.PocketLoot.Whitelist; + // Get a set of ids we don't want to generate var blacklist = GetContainerLootBlacklist(); - var itemsToAdd = items.Where(item => - allowedItemTypeWhitelist.Contains(item.Value.Parent) && - _itemHelper.IsValidItem(item.Value.Id) && - !blacklist.Contains(item.Value.Id) && - !blacklist.Contains(item.Value.Parent) && - ItemFitsInto1By2Slot(item.Value) - ).Select(x => x.Key); - - foreach (var tpl in itemsToAdd) - // If pmc has price override, use that. Otherwise, use flea price - { - if (pmcPriceOverrides.TryGetValue(tpl, out var priceOverride)) - { - _pocketLootPool.TryAdd(tpl, priceOverride); - } - else - { - // Set price of item as its weight - var price = _ragfairPriceService.GetDynamicItemPrice(tpl, Money.ROUBLES); - _pocketLootPool[tpl] = price ?? 0; - } - } - - var highestPrice = _pocketLootPool.Max(price => price.Value); - foreach (var (key, _) in _pocketLootPool) - // Invert price so cheapest has a larger weight - // Times by highest price so most expensive item has weight of 1 - { - _pocketLootPool[key] = Math.Round(1 / _pocketLootPool[key] * highestPrice); - } - - _weightedRandomHelper.ReduceWeightValues(_pocketLootPool); - - return _pocketLootPool; + // Generate loot and cache - Also pass check to ensure only 1x2 items are allowed (Unheard bots have big pockets, hence the need for 1x2) + var pool = GenerateLootPool(pmcRole, allowedItemTypeWhitelist, blacklist, ItemFitsInto1By2Slot); + _pocketLootPool.TryAdd(pmcRole, pool); + return pool; } } - /// - /// Get a generic all-container blacklist - /// - /// Hashset of blacklisted items - protected HashSet GetContainerLootBlacklist() - { - var blacklist = new HashSet(); - blacklist.UnionWith(_pmcConfig.PocketLoot.Blacklist); - blacklist.UnionWith(_pmcConfig.GlobalLootBlacklist); - blacklist.UnionWith(_itemFilterService.GetBlacklistedItems()); - blacklist.UnionWith(_itemFilterService.GetBlacklistedLootableItems()); - blacklist.UnionWith(_seasonalEventService.GetInactiveSeasonalEventItems()); - - return blacklist; - } - /// /// Create a dictionary of loot items a PMC can have in their vests with a corresponding weight of being picked to spawn /// @@ -141,66 +69,160 @@ public class PMCLootGenerator { lock (VestLock) { - // Hydrate loot dictionary if empty - if (_vestLootPool is not null) + // Already exists, return values + if (_vestLootPool.TryGetValue(pmcRole, out var existingLootPool)) { - return _vestLootPool; + return existingLootPool; } - // Create dictionary to hold vest loot - _vestLootPool = new Dictionary(); - - // Get all items from database - var items = _databaseService.GetItems(); - - // Grab price overrides if they exist for the pmcRole passed in - var pmcPriceOverrides = - _databaseService.GetBots().Types[string.Equals(pmcRole, "pmcbear", StringComparison.OrdinalIgnoreCase) ? "bear" : "usec"].BotInventory.Items - .TacticalVest; + // Get a set of item types we want to generate + var allowedItemTypeWhitelist = _pmcConfig.VestLoot.Whitelist; + // Get a set of ids we don't want to generate var blacklist = GetContainerLootBlacklist(); - blacklist.UnionWith(_pmcConfig.VestLoot.Blacklist); // Add vest-specific blacklist + blacklist.UnionWith(_pmcConfig.VestLoot.Blacklist); // Include vest-specific blacklist - var itemTplsToAdd = items.Where(item => - _pmcConfig.VestLoot.Whitelist.Contains(item.Value.Parent) && // A whitelist of item types the PMC is allowed to have - !blacklist.Contains(item.Value.Id) && - !blacklist.Contains(item.Value.Parent) && - _itemHelper.IsValidItem(item.Value.Id) && - ItemFitsInto2By2Slot(item.Value) - ).Select(x => x.Key); + // Generate loot and cache - Also pass check to ensure items up to 2x2 are allowed, some vests have big slots + var pool = GenerateLootPool(pmcRole, allowedItemTypeWhitelist, blacklist, ItemFitsInto2By2Slot); + _vestLootPool.TryAdd(pmcRole, pool); - foreach (var tpl in itemTplsToAdd) - // If PMC has price override, use that. Otherwise, use flea price - { - if (pmcPriceOverrides.TryGetValue(tpl, out var overridePrice)) - { - // There's a price override for this item, use override instead of default price - _vestLootPool.TryAdd(tpl, overridePrice); - } - else - { - // Store items price so we can turn it into a weighting later - var price = _ragfairPriceService.GetDynamicItemPrice(tpl, Money.ROUBLES); - _vestLootPool[tpl] = price ?? 0; - } - } - - // Find the highest priced item added to vest pool - var highestPrice = _vestLootPool.Max(price => price.Value); - foreach (var (key, _) in _vestLootPool) - // Invert price so cheapest has a larger weight, giving us a weighting of low-priced items being more common - // Times by highest price so most expensive item has weight of 1 - { - _vestLootPool[key] = Math.Round(1 / _vestLootPool[key] * highestPrice); - } - - // Find the greatest common divisor between all the prices and apply it to reduce the values for better readability of weights - _weightedRandomHelper.ReduceWeightValues(_vestLootPool); - - return _vestLootPool; + return pool; } } + /// + /// Create a List of loot items a PMC can have in their backpack + /// + /// Role of PMC having loot generated (bear or usec) + /// Dictionary of string and number + public Dictionary GeneratePMCBackpackLootPool(string pmcRole) + { + lock (BackpackLock) + { + + // Already exists, return values + if (_backpackLootPool.TryGetValue(pmcRole, out var existingLootPool)) + { + return existingLootPool; + } + + var allowedItemTypeWhitelist = _pmcConfig.BackpackLoot.Whitelist; + var blacklist = GetContainerLootBlacklist(); + blacklist.UnionWith(_pmcConfig.BackpackLoot.Blacklist); // Include backpack-specific blacklist + + // Generate loot and cache + var pool = GenerateLootPool(pmcRole, allowedItemTypeWhitelist, blacklist, null); + _backpackLootPool.TryAdd(pmcRole, pool); + + return pool; + } + } + + /// + /// Helper method to generate a loot pool of item tpls based on the inputs provided + /// + /// Role of PMC to generate loot for (pmcBEAR or pmcUSEC) + /// A list of item types the pmc can spawn + /// Item and parent blacklist + /// An optional delegate to validate the TemplateItem object being processed + /// Dictionary of items and weights inversely tied to the items price + protected Dictionary GenerateLootPool(string pmcRole, HashSet allowedItemTypeWhitelist, HashSet itemTplAndParentBlacklist, Func? genericItemCheck) + { + var lootPool = new Dictionary(); + var items = databaseService.GetItems(); + + // Grab price overrides if they exist for the pmcRole passed in + var pmcPriceOverrides = GetPMCPriceOverrides(pmcRole); + + // Filter all items in DB to ones we want with passed in whitelist + blacklist + generic 'IsValidItem' check + // Also run Delegate if it's not null + var itemTplsToAdd = items.Where(item => + allowedItemTypeWhitelist.Contains(item.Value.Parent) && + itemHelper.IsValidItem(item.Value.Id) && + !itemTplAndParentBlacklist.Contains(item.Value.Id) && + !itemTplAndParentBlacklist.Contains(item.Value.Parent) && + (genericItemCheck?.Invoke(item.Value) ?? true) // if delegate is null, force check to be true + ).Select(x => x.Key); + + // Store all items + price in above lootPool dictionary + foreach (var tpl in itemTplsToAdd) + { + // If PMC has price override, use that. Otherwise, use flea price + lootPool.TryAdd(tpl, GetItemPrice(tpl, pmcPriceOverrides)); + } + + // Get the highest priced item being stored in loot pool + var highestPrice = lootPool.Max(price => price.Value); + foreach (var (key, _) in lootPool) + // Invert price so cheapest has a larger weight + // Times by highest price so most expensive item has weight of 1 + { + // This results in cheap items having higher weighting and thus a higher chance of being picked + lootPool[key] = Math.Round(1 / lootPool[key] * highestPrice); + } + + // Get the greatest common divisor for all items in pool, use it to reduce the weight value and get more readable numbers + weightedRandomHelper.ReduceWeightValues(lootPool); + + return lootPool; + } + + /// + /// Get a generic all-container blacklist + /// + /// Hashset of blacklisted items + protected HashSet GetContainerLootBlacklist() + { + var blacklist = new HashSet(); + blacklist.UnionWith(_pmcConfig.PocketLoot.Blacklist); + blacklist.UnionWith(_pmcConfig.GlobalLootBlacklist); + blacklist.UnionWith(itemFilterService.GetBlacklistedItems()); + blacklist.UnionWith(itemFilterService.GetBlacklistedLootableItems()); + blacklist.UnionWith(seasonalEventService.GetInactiveSeasonalEventItems()); + + return blacklist; + } + + /// + /// Convert a PMC role "pmcBEAR/pmcUSEC" into a type and get price overrides if they exist + /// + /// role of PMC to look up + /// Dictionary of overrides + protected Dictionary? GetPMCPriceOverrides(string pmcRole) + { + var pmcType = string.Equals(pmcRole, "pmcbear", StringComparison.OrdinalIgnoreCase) + ? "bear" + : "usec"; + + if (databaseService.GetBots().Types.TryGetValue(pmcType, out var priceOverrides)) + { + return priceOverrides?.BotInventory?.Items?.TacticalVest; + } + + logger.Error($"Unable to find price overrides for PMC: {pmcRole}"); + + return null; + + } + + /// + /// Get an items price from db or override if it exists + /// + /// Item tpl to get price of + /// + /// Rouble price + protected double GetItemPrice(string tpl, Dictionary? pmcPriceOverrides = null) + { + if (pmcPriceOverrides is not null && pmcPriceOverrides.TryGetValue(tpl, out var overridePrice)) + { + // There's a price override for this item, use override instead of default price + return overridePrice; + } + + // Store items price so we can turn it into a weighting later + return ragfairPriceService.GetDynamicItemPrice(tpl, Money.ROUBLES) ?? 0; + } + /// /// Check if item has a width/height that lets it fit into a 2x2 slot /// 1x1 / 1x2 / 2x1 / 2x2 @@ -226,66 +248,4 @@ public class PMCLootGenerator _ => false }; } - - /// - /// Create a List of loot items a PMC can have in their backpack - /// - /// Role of PMC having loot generated (bear or usec) - /// Dictionary of string and number - public Dictionary GeneratePMCBackpackLootPool(string botRole) - { - lock (BackpackLock) - { - // Hydrate loot dictionary if empty - if (_backpackLootPool is not null) - { - return _backpackLootPool; - } - - _backpackLootPool = new Dictionary(); - var items = _databaseService.GetItems(); - var pmcPriceOverrides = - _databaseService.GetBots().Types[string.Equals(botRole, "pmcbear", StringComparison.OrdinalIgnoreCase) ? "bear" : "usec"].BotInventory.Items - .Backpack; - - var allowedItemTypeWhitelist = _pmcConfig.BackpackLoot.Whitelist; - - var blacklist = GetContainerLootBlacklist(); - blacklist.UnionWith(_pmcConfig.BackpackLoot.Blacklist); // Add backpack-specific blacklist - - var itemsToAdd = items.Where(item => - allowedItemTypeWhitelist.Contains(item.Value.Parent) && - _itemHelper.IsValidItem(item.Value.Id) && - !blacklist.Contains(item.Value.Id) && - !blacklist.Contains(item.Value.Parent) - ).Select(x => x.Key); - - foreach (var tpl in itemsToAdd) - // If pmc has price override, use that. Otherwise, use flea price - { - if (pmcPriceOverrides.TryGetValue(tpl, out var priceOverride)) - { - _backpackLootPool.TryAdd(tpl, priceOverride); - } - else - { - // Set price of item as its weight - var price = _ragfairPriceService.GetDynamicItemPrice(tpl, Money.ROUBLES); - _backpackLootPool[tpl] = price ?? 0; - } - } - - var highestPrice = _backpackLootPool.Max(price => price.Value); - foreach (var (key, _) in _backpackLootPool) - // Invert price so cheapest has a larger weight - // Times by highest price so most expensive item has weight of 1 - { - _backpackLootPool[key] = Math.Round(1 / _backpackLootPool[key] * highestPrice); - } - - _weightedRandomHelper.ReduceWeightValues(_backpackLootPool); - - return _backpackLootPool; - } - } }