using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Helpers; using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Spt.Config; using SPTarkov.Server.Core.Servers; using SPTarkov.Server.Core.Services; namespace SPTarkov.Server.Core.Generators; [Injectable] public class PMCLootGenerator( DatabaseService databaseService, ItemHelper itemHelper, ItemFilterService itemFilterService, RagfairPriceService ragfairPriceService, SeasonalEventService seasonalEventService, WeightedRandomHelper weightedRandomHelper, ConfigServer configServer ) { private readonly PmcConfig _pmcConfig = configServer.GetConfig(); // 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(); /// /// Create a List of loot items a PMC can have in their pockets /// /// Role of PMC having loot generated (bear or usec) /// Dictionary of string and number public Dictionary GeneratePMCPocketLootPool(string pmcRole) { lock (PocketLock) { // Already exists, return values if (_pocketLootPool.TryGetValue(pmcRole, out var existingLootPool)) { return existingLootPool; } // 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(); // Get pocket priceOverrides var pocketPriceOverrides = GetPMCPriceOverrides(pmcRole, "pocket"); // 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, pocketPriceOverrides, ItemFitsInto1By2Slot); _pocketLootPool.TryAdd(pmcRole, pool); return pool; } } /// /// Create a dictionary of loot items a PMC can have in their vests with a corresponding weight of being picked to spawn /// /// Role of PMC having loot generated (bear or usec) /// Dictionary item template ids and a weighted chance of being picked public Dictionary GeneratePMCVestLootPool(string pmcRole) { lock (VestLock) { // Already exists, return values if (_vestLootPool.TryGetValue(pmcRole, out var existingLootPool)) { return existingLootPool; } // 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); // Include vest-specific blacklist // Get pocket priceOverrides var vestPriceOverrides = GetPMCPriceOverrides(pmcRole, "vest"); // 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, vestPriceOverrides, ItemFitsInto2By2Slot); _vestLootPool.TryAdd(pmcRole, pool); 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 // Get pocket priceOverrides var backpackPriceOverrides = GetPMCPriceOverrides(pmcRole, "vest"); // Generate loot and cache var pool = GenerateLootPool(pmcRole, allowedItemTypeWhitelist, blacklist, backpackPriceOverrides, 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, Dictionary priceOverrides, Func? genericItemCheck ) { var lootPool = new Dictionary(); var items = databaseService.GetItems(); // 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, priceOverrides)); } // 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.GetItemRewardBlacklist()); 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 /// Container (e.g. pocket) /// Dictionary of overrides protected Dictionary GetPMCPriceOverrides(string pmcRole, string slot) { var pmcType = string.Equals(pmcRole, "pmcbear", StringComparison.OrdinalIgnoreCase) ? "bear" : "usec"; // the usec/bear.json item prices act as overrides we apply over what we dynamically generate if (databaseService.GetBots().Types.TryGetValue(pmcType, out var priceOverrides)) { var botItems = priceOverrides.BotInventory.Items; switch (slot) { case "pocket": return botItems.Pockets; case "backpack": return botItems.Backpack; case "vest": return botItems.TacticalVest; } } return []; } /// /// Get an items price from db or override if it exists /// /// Item tpl to get price of /// /// Rouble price protected double GetItemPrice(MongoId 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 /// /// Item to check size of /// true if it fits protected bool ItemFitsInto2By2Slot(TemplateItem item) { return item.Properties.Width <= 2 && item.Properties.Height <= 2; } /// /// Check if item has a width/height that lets it fit into a 1x2 slot /// 1x1 / 1x2 / 2x1 /// /// Item to check size of /// true if it fits protected bool ItemFitsInto1By2Slot(TemplateItem item) { return $"{item.Properties.Width}x{item.Properties.Height}" switch { "1x1" or "1x2" or "2x1" => true, _ => false, }; } }