diff --git a/Libraries/Core/Generators/PMCLootGenerator.cs b/Libraries/Core/Generators/PMCLootGenerator.cs index f540fdca..2bb2a292 100644 --- a/Libraries/Core/Generators/PMCLootGenerator.cs +++ b/Libraries/Core/Generators/PMCLootGenerator.cs @@ -1,11 +1,53 @@ using SptCommon.Annotations; using Core.Models.Eft.Common.Tables; +using Core.Models.Enums; +using Core.Models.Utils; +using Core.Helpers; +using Core.Services; +using Core.Servers; +using Core.Models.Spt.Config; namespace Core.Generators; [Injectable] -public class PMCLootGenerator() +public class PMCLootGenerator { + private readonly ISptLogger _logger; + private readonly DatabaseService _databaseService; + private readonly ItemHelper _itemHelper; + private readonly ItemFilterService _itemFilterService; + private readonly RagfairPriceService _ragfairPriceService; + private readonly SeasonalEventService _seasonalEventService; + private readonly WeightedRandomHelper _weightedRandomHelper; + private readonly ConfigServer _configServer; + + private Dictionary? _backpackLootPool; + private Dictionary? _pocketLootPool; + private Dictionary? _vestLootPool; + private readonly PmcConfig _pmcConfig; + + 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 @@ -14,7 +56,76 @@ public class PMCLootGenerator() /// Dictionary of string and number public Dictionary GeneratePMCPocketLootPool(string botRole) { - throw new NotImplementedException(); + // Hydrate loot dictionary if empty + if (_pocketLootPool is null) + { + var items = _databaseService.GetItems(); + var pmcPriceOverrides = + _databaseService.GetBots().Types[botRole.ToLower() == "pmcbear" ? "bear" : "usec"].BotInventory.Items.Pockets; + + var allowedItemTypeWhitelist = _pmcConfig.PocketLoot.Whitelist; + + var blacklist = GetLootBlacklist(); + + 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) + ); + + foreach (var (tpl, template) in itemsToAdd) + { + // If pmc has price override, use that. Otherwise, use flea price + if (pmcPriceOverrides.ContainsKey(tpl)) + { + _pocketLootPool[tpl] = pmcPriceOverrides[tpl]; + } + else + { + // Set price of item as its weight + var price = _ragfairPriceService.GetDynamicItemPrice(tpl, Money.ROUBLES); + _pocketLootPool[tpl] = price; + } + } + + 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; + } + + private HashSet GetLootBlacklist() + { + var blacklist = new HashSet(); + foreach (var blacklistedItem in _pmcConfig.PocketLoot.Blacklist) + { + blacklist.Add(blacklistedItem); + } + foreach (var blacklistedItem in _pmcConfig.GlobalLootBlacklist) + { + blacklist.Add(blacklistedItem); + } + foreach (var blacklistedItem in _itemFilterService.GetBlacklistedItems()) + { + blacklist.Add(blacklistedItem); + } + foreach (var blacklistedItem in _seasonalEventService.GetInactiveSeasonalEventItems()) + { + blacklist.Add(blacklistedItem); + } + + return blacklist; } /// @@ -24,7 +135,52 @@ public class PMCLootGenerator() /// Dictionary of string and number public Dictionary GeneratePMCVestLootPool(string botRole) { - throw new NotImplementedException(); + // Hydrate loot dictionary if empty + if (_vestLootPool is null) + { + var items = _databaseService.GetItems(); + var pmcPriceOverrides = + _databaseService.GetBots().Types[botRole.ToLower() == "pmcbear" ? "bear" : "usec"].BotInventory.Items.TacticalVest; + + var allowedItemTypeWhitelist = _pmcConfig.VestLoot.Whitelist; + + var blacklist = GetLootBlacklist(); + + 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) && + ItemFitsInto2By2Slot(item.Value)); + + foreach (var (tpl, template) in itemsToAdd) + { + // If pmc has price override, use that. Otherwise, use flea price + if (pmcPriceOverrides.ContainsKey(tpl)) + { + _vestLootPool[tpl] = pmcPriceOverrides[tpl]; + } + else + { + // Set price of item as its weight + var price = _ragfairPriceService.GetDynamicItemPrice(tpl, Money.ROUBLES); + _vestLootPool[tpl] = price; + } + } + + var highestPrice = _vestLootPool.Max(price => price.Value); + foreach (var (key, _) in _vestLootPool) + { + // Invert price so cheapest has a larger weight + // Times by highest price so most expensive item has weight of 1 + _vestLootPool[key] = Math.Round((1 / _vestLootPool[key]) * highestPrice); + } + + _weightedRandomHelper.ReduceWeightValues(_vestLootPool); + } + + return _vestLootPool; } /// @@ -35,7 +191,7 @@ public class PMCLootGenerator() /// true if it fits protected bool ItemFitsInto2By2Slot(TemplateItem item) { - throw new NotImplementedException(); + return item.Properties.Width <= 2 && item.Properties.Height <= 2; } /// @@ -46,7 +202,11 @@ public class PMCLootGenerator() /// true if it fits protected bool ItemFitsInto1By2Slot(TemplateItem item) { - throw new NotImplementedException(); + return $"{item.Properties.Width}x{item.Properties.Height}" switch + { + "1x1" or "1x2" or "2x1" => true, + _ => false + }; } /// @@ -56,6 +216,48 @@ public class PMCLootGenerator() /// Dictionary of string and number public Dictionary GeneratePMCBackpackLootPool(string botRole) { - throw new NotImplementedException(); + // Hydrate loot dictionary if empty + if (_backpackLootPool is null) + { + var items = _databaseService.GetItems(); + var pmcPriceOverrides = + _databaseService.GetBots().Types[botRole.ToLower() == "pmcbear" ? "bear" : "usec"].BotInventory.Items.Backpack; + + var allowedItemTypeWhitelist = _pmcConfig.BackpackLoot.Whitelist; + + var blacklist = GetLootBlacklist(); + + 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)); + + foreach (var (tpl, template) in itemsToAdd) { + // If pmc has price override, use that. Otherwise, use flea price + if (pmcPriceOverrides.ContainsKey(tpl)) + { + _backpackLootPool[tpl] = pmcPriceOverrides[tpl]; + } + else + { + // Set price of item as its weight + var price = _ragfairPriceService.GetDynamicItemPrice(tpl, Money.ROUBLES); + _backpackLootPool[tpl] = price; + } + } + + 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; } } diff --git a/Libraries/Core/Models/Spt/Config/RagfairConfig.cs b/Libraries/Core/Models/Spt/Config/RagfairConfig.cs index ceece30e..84055f74 100644 --- a/Libraries/Core/Models/Spt/Config/RagfairConfig.cs +++ b/Libraries/Core/Models/Spt/Config/RagfairConfig.cs @@ -1,4 +1,4 @@ -using Core.Models.Common; +using Core.Models.Common; namespace Core.Models.Spt.Config; @@ -136,7 +136,7 @@ public record Dynamic [JsonPropertyName("itemPriceMultiplier")] /** A multipler to apply to individual tpls price just prior to item quality adjustment */ - public Dictionary ItemPriceMultiplier { get; set; } + public Dictionary? ItemPriceMultiplier { get; set; } [JsonPropertyName("_currencies")] public string? CurrenciesDescription { get; set; } diff --git a/Libraries/Core/Services/ItemFilterService.cs b/Libraries/Core/Services/ItemFilterService.cs index 1f300b76..cafebcca 100644 --- a/Libraries/Core/Services/ItemFilterService.cs +++ b/Libraries/Core/Services/ItemFilterService.cs @@ -14,9 +14,11 @@ public class ItemFilterService( ConfigServer _configServer ) { - protected HashSet _lootableItemBlacklistCache = []; protected ItemConfig _itemConfig = _configServer.GetConfig(); + protected HashSet? _lootableItemBlacklistCache; + protected HashSet? _itemBlacklistCache; + /** * Check if the provided template id is blacklisted in config/item.json/blacklist * @param tpl template id @@ -104,12 +106,9 @@ public class ItemFilterService( public bool IsLootableItemBlacklisted(string itemKey) { - if (_lootableItemBlacklistCache.Count == 0) + if (_lootableItemBlacklistCache is null) { - foreach (var item in _itemConfig.LootableItemBlacklist) - { - _lootableItemBlacklistCache.Add(item); - } + HydrateLootableItemBlacklist(); } return _lootableItemBlacklistCache.Contains(itemKey); @@ -117,6 +116,27 @@ public class ItemFilterService( public bool IsItemBlacklisted(string tpl) { - throw new NotImplementedException(); + if (_itemBlacklistCache is null) + { + HydrateBlacklist(); + } + + return _itemBlacklistCache.Contains(tpl); + } + + protected void HydrateLootableItemBlacklist() + { + foreach (var item in _itemConfig.LootableItemBlacklist) + { + _lootableItemBlacklistCache.Add(item); + } + } + + protected void HydrateBlacklist() + { + _itemBlacklistCache = []; + foreach (var item in _itemConfig.Blacklist) { + _itemBlacklistCache.Add(item); + } } } diff --git a/Libraries/Core/Services/RagfairPriceService.cs b/Libraries/Core/Services/RagfairPriceService.cs index 49898c5e..24ba55fb 100644 --- a/Libraries/Core/Services/RagfairPriceService.cs +++ b/Libraries/Core/Services/RagfairPriceService.cs @@ -5,6 +5,9 @@ using Core.Models.Eft.Common.Tables; using Core.Models.Spt.Config; using Core.Models.Spt.Ragfair; using Core.Models.Utils; +using Core.Models.Enums; +using System; +using Core.Servers; namespace Core.Services; @@ -12,9 +15,15 @@ namespace Core.Services; public class RagfairPriceService( ISptLogger _logger, HandbookHelper _handbookHelper, - DatabaseService _databaseService + TraderHelper _traderHelper, + PresetHelper _presetHelper, + ItemHelper _itemHelper, + DatabaseService _databaseService, + ConfigServer _configServer ) { + RagfairConfig _ragfairConfig = _configServer.GetConfig(); + protected RagfairServerPrices _prices = new RagfairServerPrices { StaticPrices = new Dictionary(), DynamicPrices = new Dictionary() }; @@ -152,7 +161,80 @@ public class RagfairPriceService( /// public double GetDynamicItemPrice(string itemTemplateId, string desiredCurrency, Item item = null, List offerItems = null, bool? isPackOffer = null) { - throw new NotImplementedException(); + var isPreset = false; + var price = GetFleaPriceForItem(itemTemplateId); + + // Adjust price if below handbook price, based on config. + if (_ragfairConfig.Dynamic.OfferAdjustment.AdjustPriceWhenBelowHandbookPrice) + { + price = AdjustPriceIfBelowHandbook(price, itemTemplateId); + } + + // Use trader price if higher, based on config. + if (_ragfairConfig.Dynamic.UseTraderPriceForOffersIfHigher) + { + var traderPrice = _traderHelper.GetHighestSellToTraderPrice(itemTemplateId); + if (traderPrice > price) + { + price = traderPrice; + } + } + + // Prices for weapon presets are handled differently. + if ( + item?.Upd?.SptPresetId is not null && + offerItems is not null && + _presetHelper.IsPresetBaseClass(item.Upd.SptPresetId, BaseClasses.WEAPON) + ) + { + price = GetWeaponPresetPrice(item, offerItems, price); + isPreset = true; + } + + // Check for existence of manual price adjustment multiplier + if (_ragfairConfig.Dynamic.ItemPriceMultiplier.TryGetValue(itemTemplateId, out var multiplier)) + { + price *= multiplier; + } + + // The quality of the item affects the price + not on the ignore list + if (item is not null && !_ragfairConfig.Dynamic.IgnoreQualityPriceVarianceBlacklist.Contains(itemTemplateId)) + { + var qualityModifier = _itemHelper.GetItemQualityModifier(item); + price *= qualityModifier; + } + + // Make adjustments for unreasonably priced items. + foreach (var (key, value) in _ragfairConfig.Dynamic.UnreasonableModPrices) + { + if (!_itemHelper.IsOfBaseclass(itemTemplateId, key) || !value.Enabled) + { + continue; + } + + price = AdjustUnreasonablePrice( + _databaseService.GetHandbook().Items, + value, + itemTemplateId, + price); + } + + // Vary the price based on the type of offer. + var range = GetOfferTypeRangeValues(isPreset, isPackOffer ?? false); + price = RandomiseOfferPrice(price, range); + + // Convert to different currency if required. + var roublesId = Money.ROUBLES; + if (desiredCurrency != roublesId) + { + price = _handbookHelper.FromRUB(price, desiredCurrency); + } + + if (price < 1) + { + return 1; + } + return price; } /// @@ -163,11 +245,11 @@ public class RagfairPriceService( /// Item being adjusted /// Current price of item /// Adjusted price of item - protected decimal AdjustUnreasonablePrice( + protected double AdjustUnreasonablePrice( List handbookPrices, UnreasonableModPrices unreasonableItemChange, string itemTpl, - decimal price) + double price) { throw new NotImplementedException(); } @@ -189,7 +271,7 @@ public class RagfairPriceService( /// price of item /// item template Id being checked /// adjusted price value in roubles - protected decimal AdjustPriceIfBelowHandbook(decimal itemPrice, string itemTpl) + protected double AdjustPriceIfBelowHandbook(double itemPrice, string itemTpl) { throw new NotImplementedException(); } @@ -200,7 +282,7 @@ public class RagfairPriceService( /// price to alter /// min and max to adjust price by /// multiplied price - protected decimal RandomiseOfferPrice(decimal existingPrice, MinMax rangeValues) + protected double RandomiseOfferPrice(double existingPrice, MinMax rangeValues) { throw new NotImplementedException(); } @@ -212,7 +294,7 @@ public class RagfairPriceService( /// weapon plus mods /// price of existing base weapon /// price of weapon in roubles - protected decimal GetWeaponPresetPrice(Item weaponRootItem, List weaponWithChildren, decimal existingPrice) + protected double GetWeaponPresetPrice(Item weaponRootItem, List weaponWithChildren, double existingPrice) { throw new NotImplementedException(); }