Fixed PMC loot pool generator returning the first cached pool regardless of what pmc type was requested

Refactored PMCLootGenerator
Made use of Primary constructor
Created helper function to generate loot pool
Cache loot data against PMC type
This commit is contained in:
Chomp
2025-06-11 13:39:24 +01:00
parent d31ebb70f3
commit 2f0bcdea25
@@ -10,49 +10,27 @@ using SPTarkov.Server.Core.Services;
namespace SPTarkov.Server.Core.Generators;
[Injectable]
public class PMCLootGenerator
public class PMCLootGenerator(
ISptLogger<PMCLootGenerator> 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<PMCLootGenerator> _logger;
private readonly PmcConfig _pmcConfig;
private readonly RagfairPriceService _ragfairPriceService;
private readonly SeasonalEventService _seasonalEventService;
private readonly WeightedRandomHelper _weightedRandomHelper;
private readonly PmcConfig _pmcConfig = configServer.GetConfig<PmcConfig>();
private Dictionary<string, double>? _backpackLootPool;
private Dictionary<string, double>? _pocketLootPool;
private Dictionary<string, double>? _vestLootPool;
// Store loot against its type, usec/bear
private readonly Dictionary<string, Dictionary<string, double>>? _backpackLootPool = [];
private readonly Dictionary<string, Dictionary<string, double>>? _pocketLootPool = [];
private readonly Dictionary<string, Dictionary<string, double>>? _vestLootPool = [];
protected readonly Lock BackpackLock = new();
protected readonly Lock PocketLock = new();
protected readonly Lock VestLock = new();
public PMCLootGenerator(
ISptLogger<PMCLootGenerator> 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<PmcConfig>();
}
/// <summary>
/// Create a List of loot items a PMC can have in their pockets
/// </summary>
@@ -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<string, double>();
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;
}
}
/// <summary>
/// Get a generic all-container blacklist
/// </summary>
/// <returns>Hashset of blacklisted items</returns>
protected HashSet<string> GetContainerLootBlacklist()
{
var blacklist = new HashSet<string>();
blacklist.UnionWith(_pmcConfig.PocketLoot.Blacklist);
blacklist.UnionWith(_pmcConfig.GlobalLootBlacklist);
blacklist.UnionWith(_itemFilterService.GetBlacklistedItems());
blacklist.UnionWith(_itemFilterService.GetBlacklistedLootableItems());
blacklist.UnionWith(_seasonalEventService.GetInactiveSeasonalEventItems());
return blacklist;
}
/// <summary>
/// Create a dictionary of loot items a PMC can have in their vests with a corresponding weight of being picked to spawn
/// </summary>
@@ -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<string, double>();
// 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;
}
}
/// <summary>
/// Create a List of loot items a PMC can have in their backpack
/// </summary>
/// <param name="pmcRole">Role of PMC having loot generated (bear or usec)</param>
/// <returns>Dictionary of string and number</returns>
public Dictionary<string, double> 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;
}
}
/// <summary>
/// Helper method to generate a loot pool of item tpls based on the inputs provided
/// </summary>
/// <param name="pmcRole">Role of PMC to generate loot for (pmcBEAR or pmcUSEC)</param>
/// <param name="allowedItemTypeWhitelist">A list of item types the pmc can spawn</param>
/// <param name="itemTplAndParentBlacklist">Item and parent blacklist</param>
/// <param name="genericItemCheck">An optional delegate to validate the TemplateItem object being processed</param>
/// <returns>Dictionary of items and weights inversely tied to the items price</returns>
protected Dictionary<string, double> GenerateLootPool(string pmcRole, HashSet<string> allowedItemTypeWhitelist, HashSet<string> itemTplAndParentBlacklist, Func<TemplateItem, bool>? genericItemCheck)
{
var lootPool = new Dictionary<string, double>();
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;
}
/// <summary>
/// Get a generic all-container blacklist
/// </summary>
/// <returns>Hashset of blacklisted items</returns>
protected HashSet<string> GetContainerLootBlacklist()
{
var blacklist = new HashSet<string>();
blacklist.UnionWith(_pmcConfig.PocketLoot.Blacklist);
blacklist.UnionWith(_pmcConfig.GlobalLootBlacklist);
blacklist.UnionWith(itemFilterService.GetBlacklistedItems());
blacklist.UnionWith(itemFilterService.GetBlacklistedLootableItems());
blacklist.UnionWith(seasonalEventService.GetInactiveSeasonalEventItems());
return blacklist;
}
/// <summary>
/// Convert a PMC role "pmcBEAR/pmcUSEC" into a type and get price overrides if they exist
/// </summary>
/// <param name="pmcRole">role of PMC to look up</param>
/// <returns>Dictionary of overrides</returns>
protected Dictionary<string, double>? 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;
}
/// <summary>
/// Get an items price from db or override if it exists
/// </summary>
/// <param name="tpl">Item tpl to get price of</param>
/// <param name="pmcPriceOverrides"></param>
/// <returns>Rouble price</returns>
protected double GetItemPrice(string tpl, Dictionary<string, double>? 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;
}
/// <summary>
/// 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
};
}
/// <summary>
/// Create a List of loot items a PMC can have in their backpack
/// </summary>
/// <param name="botRole">Role of PMC having loot generated (bear or usec)</param>
/// <returns>Dictionary of string and number</returns>
public Dictionary<string, double> GeneratePMCBackpackLootPool(string botRole)
{
lock (BackpackLock)
{
// Hydrate loot dictionary if empty
if (_backpackLootPool is not null)
{
return _backpackLootPool;
}
_backpackLootPool = new Dictionary<string, double>();
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;
}
}
}