diff --git a/Libraries/Core/Controllers/LocationController.cs b/Libraries/Core/Controllers/LocationController.cs index 0c2708ec..4b8a0342 100644 --- a/Libraries/Core/Controllers/LocationController.cs +++ b/Libraries/Core/Controllers/LocationController.cs @@ -61,13 +61,7 @@ public class LocationController( /// public GetAirdropLootResponse? GetAirDropLoot(GetAirdropLootRequest? request) { - if (request is null) - { - // client sometimes requests this after a raid has ended, just return null - return null; - } - - if (request.ContainerId is not null) + if (request?.ContainerId is not null) { return _airdropService.GenerateCustomAirdropLoot(request); } diff --git a/Libraries/Core/Generators/BotWeaponGenerator.cs b/Libraries/Core/Generators/BotWeaponGenerator.cs index 7a625bda..7746a9dd 100644 --- a/Libraries/Core/Generators/BotWeaponGenerator.cs +++ b/Libraries/Core/Generators/BotWeaponGenerator.cs @@ -33,7 +33,7 @@ public class BotWeaponGenerator( IEnumerable inventoryMagGenComponents ) { - protected List _inventoryMagGenComponents = MagGenSetUp(inventoryMagGenComponents); + protected IEnumerable _inventoryMagGenComponents = MagGenSetUp(inventoryMagGenComponents); protected BotConfig _botConfig = _configServer.GetConfig(); protected PmcConfig _pmcConfig = _configServer.GetConfig(); protected RepairConfig _repairConfig = _configServer.GetConfig(); @@ -42,13 +42,8 @@ public class BotWeaponGenerator( private static List MagGenSetUp(IEnumerable components) { var inventoryMagGens = components.ToList(); - inventoryMagGens.ToList() - .Sort( - (a, b) => - a.GetPriority() - - b.GetPriority() - ); - return inventoryMagGens.ToList(); + inventoryMagGens.Sort((a, b) => a.GetPriority() - b.GetPriority()); + return inventoryMagGens; } /// @@ -398,7 +393,7 @@ public class BotWeaponGenerator( return; } - + var isInternalMag = magTemplate.Properties.ReloadMagType == "InternalMagazine"; var ammoTemplate = _itemHelper.GetItem(generatedWeaponResult.ChosenAmmoTemplate).Value; if (ammoTemplate is null) { @@ -722,7 +717,6 @@ public class BotWeaponGenerator( /// Weapon items list to amend /// Magazine item details we're adding cartridges to /// Cartridge to put into the magazine - /// How many cartridges should go into the magazine /// Magazines db template protected void AddOrUpdateMagazinesChildWithAmmo(List weaponWithMods, Item magazine, string chosenAmmoTpl, TemplateItem magazineTemplate) { @@ -736,8 +730,7 @@ public class BotWeaponGenerator( } // Create array with just magazine - List magazineWithCartridges = new(); - magazineWithCartridges.AddRange(magazine); + List magazineWithCartridges = [magazine]; // Add full cartridge child items to above array _itemHelper.FillMagazineWithCartridge(magazineWithCartridges, magazineTemplate, chosenAmmoTpl, 1); diff --git a/Libraries/Core/Generators/LootGenerator.cs b/Libraries/Core/Generators/LootGenerator.cs index a9af4b6a..3e29b8da 100644 --- a/Libraries/Core/Generators/LootGenerator.cs +++ b/Libraries/Core/Generators/LootGenerator.cs @@ -21,8 +21,10 @@ public class LootGenerator( ItemHelper _itemHelper, PresetHelper _presetHelper, DatabaseService _databaseService, - ItemFilterService _itemFilterService - + ItemFilterService _itemFilterService, + LocalisationService _localisationService, + WeightedRandomHelper _weightedRandomHelper, + RagfairLinkedItemService _ragfairLinkedItemService ) { @@ -278,8 +280,8 @@ public class LootGenerator( { var randomItem = _randomUtil.GetArrayValue(items); - var itemLimitCount = itemTypeCounts[randomItem.Parent]; - if (itemLimitCount is not null && itemLimitCount.Current > itemLimitCount.Max) { + var itemLimitCount = itemTypeCounts.TryGetValue(randomItem.Parent, out var randomItemLimitCount); + if (!itemLimitCount && randomItemLimitCount?.Current > randomItemLimitCount?.Max) { return false; } @@ -291,7 +293,7 @@ public class LootGenerator( var newLootItem = new Item { Id = _hashUtil.Generate(), Template = randomItem.Id, - Upd = { + Upd = new Upd { StackObjectsCount = 1, SpawnedInSession = true, }, @@ -305,9 +307,9 @@ public class LootGenerator( newLootItem.Template = randomItem.Id; result.Add(newLootItem); - if (itemLimitCount is not null) { + if (randomItemLimitCount is not null) { // Increment item count as it's in limit array - itemLimitCount.Current++; + randomItemLimitCount.Current++; } // Item added okay @@ -322,7 +324,15 @@ public class LootGenerator( /// stack count protected int GetRandomisedStackCount(TemplateItem item, LootRequest options) { - throw new NotImplementedException(); + var min = item.Properties.StackMinRandom; + var max = item.Properties.StackMaxSize; + + if (options.ItemStackLimits.TryGetValue(item.Id, out var itemLimits)) { + min = itemLimits.Min; + max = (int?)itemLimits.Max; + } + + return _randomUtil.GetInt((int)(min ?? 1), max ?? 1); } /// @@ -338,7 +348,61 @@ public class LootGenerator( HashSet itemBlacklist, List result) { - throw new NotImplementedException(); + // Choose random preset and get details from item db using encyclopedia value (encyclopedia === tplId) + var chosenPreset = _randomUtil.GetArrayValue(presetPool); + if (chosenPreset is null ) { + _logger.Warning("Unable to find random preset in given presets, skipping"); + + return false; + } + + // No `_encyclopedia` property, not possible to reliably get root item tpl + if (chosenPreset.Encyclopedia is null) { + _logger.Debug("$Preset with id: {chosenPreset?.Id} lacks encyclopedia property, skipping"); + + return false; + } + + // Get preset root item db details via its `_encyclopedia` property + var itemDbDetails = _itemHelper.GetItem(chosenPreset.Encyclopedia); + if (!itemDbDetails.Key) { + _logger.Debug($"$Unable to find preset with tpl: {chosenPreset.Encyclopedia}, skipping"); + + return false; + } + + // Skip preset if root item is blacklisted + if (itemBlacklist.Contains(chosenPreset.Items[0].Template)) { + return false; + } + + // Some custom mod items lack a parent property + if (itemDbDetails.Value.Parent is null) { + _logger.Error(_localisationService.GetText("loot-item_missing_parentid", itemDbDetails.Value?.Name)); + + return false; + } + + // Check chosen preset hasn't exceeded spawn limit + var hasItemLimitCount = itemTypeCounts.TryGetValue(itemDbDetails.Value.Parent, out var itemLimitCount); + if (!hasItemLimitCount && itemLimitCount?.Current > itemLimitCount?.Max) { + return false; + } + + var presetAndMods = _itemHelper.ReplaceIDs(chosenPreset.Items); + _itemHelper.RemapRootItemId(presetAndMods); + // Add chosen preset tpl to result array + foreach (var item in presetAndMods) { + result.Add(item); + } + + if (itemLimitCount is not null) { + // Increment item count as item has been chosen and its inside itemLimitCount dictionary + itemLimitCount.Current++; + } + + // Item added okay + return true; } /// @@ -348,7 +412,52 @@ public class LootGenerator( /// List of items with children lists public List> GetSealedWeaponCaseLoot(SealedAirdropContainerSettings containerSettings) { - throw new NotImplementedException(); + List> itemsToReturn = []; + + // Choose a weapon to give to the player (weighted) + var chosenWeaponTpl = _weightedRandomHelper.GetWeightedValue( + containerSettings.WeaponRewardWeight + ); + + // Get itemDb details of weapon + var weaponDetailsDb = _itemHelper.GetItem(chosenWeaponTpl); + if (!weaponDetailsDb.Key) { + _logger.Error( + _localisationService.GetText("loot-non_item_picked_as_sealed_weapon_crate_reward", chosenWeaponTpl) + ); + + return itemsToReturn; + } + + // Get weapon preset - default or choose a random one from globals.json preset pool + var chosenWeaponPreset = containerSettings.DefaultPresetsOnly + ? _presetHelper.GetDefaultPreset(chosenWeaponTpl) + : _randomUtil.GetArrayValue(_presetHelper.GetPresets(chosenWeaponTpl)); + + // No default preset found for weapon, choose a random one + if (chosenWeaponPreset is null) { + _logger.Warning( + _localisationService.GetText("loot-default_preset_not_found_using_random", chosenWeaponTpl) + ); + chosenWeaponPreset = _randomUtil.GetArrayValue(_presetHelper.GetPresets(chosenWeaponTpl)); + } + + // Clean up Ids to ensure they're all unique and prevent collisions + var presetAndMods = _itemHelper.ReplaceIDs(chosenWeaponPreset.Items); + _itemHelper.RemapRootItemId(presetAndMods); + + // Add preset to return object + itemsToReturn.Add(presetAndMods); + + // Get a random collection of weapon mods related to chosen weawpon and add them to result array + var linkedItemsToWeapon = _ragfairLinkedItemService.GetLinkedDbItems(chosenWeaponTpl); + itemsToReturn.AddRange(GetSealedContainerWeaponModRewards(containerSettings, linkedItemsToWeapon, chosenWeaponPreset) + ); + + // Handle non-weapon mod reward types + itemsToReturn.AddRange((GetSealedContainerNonWeaponModRewards(containerSettings, weaponDetailsDb.Value))); + + return itemsToReturn; } /// @@ -360,7 +469,69 @@ public class LootGenerator( protected List> GetSealedContainerNonWeaponModRewards(SealedAirdropContainerSettings containerSettings, TemplateItem weaponDetailsDb) { - throw new NotImplementedException(); + List> rewards = []; + + foreach (var (rewardKey,settings) in containerSettings.RewardTypeLimits) { + var rewardCount = _randomUtil.GetDouble(settings.Min.Value, settings.Max.Value); + + if (rewardCount == 0) { + continue; + } + + // Edge case - ammo boxes + if (rewardKey == BaseClasses.AMMO_BOX) { + // Get ammoboxes from db + var ammoBoxesDetails = containerSettings.AmmoBoxWhitelist.Select((tpl) => { + var itemDetails = _itemHelper.GetItem(tpl); + return itemDetails.Value; + }); + + // Need to find boxes that matches weapons caliber + var weaponCaliber = weaponDetailsDb.Properties.AmmoCaliber; + var ammoBoxesMatchingCaliber = ammoBoxesDetails.Where((x) => + x.Properties.AmmoCaliber == weaponCaliber); + if (!ammoBoxesMatchingCaliber.Any()) { + _logger.Debug($"No ammo box with caliber {weaponCaliber} found, skipping"); + + continue; + } + + for (var index = 0; index < rewardCount; index++) { + var chosenAmmoBox = _randomUtil.GetArrayValue(ammoBoxesMatchingCaliber); + var ammoBoxReward = new List { new() { Id = _hashUtil.Generate(), Template = chosenAmmoBox.Id } }; + _itemHelper.AddCartridgesToAmmoBox(ammoBoxReward, chosenAmmoBox); + rewards.Add(ammoBoxReward); + } + + continue; + } + + // Get all items of the desired type + not quest items + not globally blacklisted + var rewardItemPool = _databaseService.GetItems().Values.Where( + (item) => + item.Parent == rewardKey && + item.Type.ToLower() == "item" && + _itemFilterService.IsItemBlacklisted(item.Id) && + !(containerSettings.AllowBossItems || _itemFilterService.IsBossItem(item.Id)) && + item.Properties.QuestItem is null + ); + + if (rewardItemPool.Count() == 0) { + _logger.Debug($"No items with base type of {rewardKey} found, skipping"); + + continue; + } + + for (var index = 0; index < rewardCount; index++) { + // Choose a random item from pool + var chosenRewardItem = _randomUtil.GetArrayValue(rewardItemPool); + var rewardItem = new List { new() { Id = _hashUtil.Generate(), Template = chosenRewardItem.Id } }; + + rewards.Add(rewardItem); + } + } + + return rewards; } /// @@ -373,7 +544,37 @@ public class LootGenerator( protected List> GetSealedContainerWeaponModRewards(SealedAirdropContainerSettings containerSettings, List linkedItemsToWeapon, Preset chosenWeaponPreset) { - throw new NotImplementedException(); + List> modRewards = []; + + foreach (var (rewardKey,settings) in containerSettings.WeaponModRewardLimits) { + var rewardCount = _randomUtil.GetDouble(settings.Min.Value, settings.Max.Value); + + // Nothing to add, skip reward type + if (rewardCount == 0) { + continue; + } + + // Get items that fulfil reward type criteria from items that fit on gun + var relatedItems = linkedItemsToWeapon?.Where( + (item) => item?.Parent == rewardKey && !_itemFilterService.IsItemBlacklisted(item.Id) + ); + if (relatedItems is null || relatedItems.Count() == 0) { + _logger.Debug( + $"No items found to fulfil reward type: {rewardKey} for weapon: {chosenWeaponPreset.Name}, skipping type" + ); + continue; + } + + // Find a random item of the desired type and add as reward + for (var index = 0; index < rewardCount; index++) { + var chosenItem = _randomUtil.DrawRandomFromList(relatedItems.ToList()); + var reward = new List { new Item() { Id = _hashUtil.Generate(), Template = chosenItem[0].Id } }; + + modRewards.Add(reward); + } + } + + return modRewards; } /// diff --git a/Libraries/Core/Models/Spt/Config/ItemConfig.cs b/Libraries/Core/Models/Spt/Config/ItemConfig.cs index 2c962201..e57d2418 100644 --- a/Libraries/Core/Models/Spt/Config/ItemConfig.cs +++ b/Libraries/Core/Models/Spt/Config/ItemConfig.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; using Core.Models.Eft.Common; namespace Core.Models.Spt.Config; @@ -10,23 +10,23 @@ public record ItemConfig : BaseConfig /** Items that should be globally blacklisted */ [JsonPropertyName("blacklist")] - public List Blacklist { get; set; } + public HashSet Blacklist { get; set; } /** Items that should not be lootable from any location */ [JsonPropertyName("lootableItemBlacklist")] - public List LootableItemBlacklist { get; set; } + public HashSet LootableItemBlacklist { get; set; } /** items that should not be given as rewards */ [JsonPropertyName("rewardItemBlacklist")] - public List RewardItemBlacklist { get; set; } + public HashSet RewardItemBlacklist { get; set; } /** Item base types that should not be given as rewards */ [JsonPropertyName("rewardItemTypeBlacklist")] - public List RewardItemTypeBlacklist { get; set; } + public HashSet RewardItemTypeBlacklist { get; set; } /** Items that can only be found on bosses */ [JsonPropertyName("bossItems")] - public List BossItems { get; set; } + public HashSet BossItems { get; set; } [JsonPropertyName("handbookPriceOverride")] public Dictionary HandbookPriceOverride { get; set; } diff --git a/Libraries/Core/Services/AirdropService.cs b/Libraries/Core/Services/AirdropService.cs index 8abe2d67..1683d61b 100644 --- a/Libraries/Core/Services/AirdropService.cs +++ b/Libraries/Core/Services/AirdropService.cs @@ -51,7 +51,7 @@ public class AirdropService( _logger.Debug($"Chose: {airdropType} for airdrop loot"); // Common/weapon/etc - var airdropConfig = GetAirdropLootConfigByType((AirdropTypeEnum)airdropType); + var airdropConfig = GetAirdropLootConfigByType(airdropType); // generate loot to put into airdrop crate var crateLoot = airdropConfig.UseForcedLoot.GetValueOrDefault(false) @@ -138,7 +138,7 @@ public class AirdropService( /// /// Type of airdrop to get settings for /// LootRequest - protected AirdropLootRequest GetAirdropLootConfigByType(AirdropTypeEnum airdropType) + protected AirdropLootRequest GetAirdropLootConfigByType(SptAirdropTypeEnum? airdropType) { var lootSettingsByType = _airdropConfig.Loot[airdropType.ToString()]; if (lootSettingsByType is null) { @@ -146,6 +146,7 @@ public class AirdropService( _localisationService.GetText("location-unable_to_find_airdrop_drop_config_of_type", airdropType) ); + // TODO: Get Radar airdrop to work. Atm Radar will default to common supply drop (mixed) // Default to common lootSettingsByType = _airdropConfig.Loot[AirdropTypeEnum.Common.ToString()]; } @@ -153,8 +154,9 @@ public class AirdropService( // Get all items that match the blacklisted types and fold into item blacklist var itemTypeBlacklist = _itemFilterService.GetItemRewardBaseTypeBlacklist(); var itemsMatchingTypeBlacklist = _itemHelper.GetItems() - .Where((templateItem) => _itemHelper.IsOfBaseclasses(templateItem.Parent, itemTypeBlacklist)) - .Select((templateItem) => templateItem.Id); + .Where(templateItem => !string.IsNullOrEmpty(templateItem.Parent)) + .Where(templateItem => _itemHelper.IsOfBaseclasses(templateItem.Parent, itemTypeBlacklist)) + .Select(templateItem => templateItem.Id); var itemBlacklist = new HashSet(); itemBlacklist.UnionWith(lootSettingsByType.ItemBlacklist); itemBlacklist.UnionWith(_itemFilterService.GetItemRewardBlacklist()); diff --git a/Libraries/Core/Services/ItemBaseClassService.cs b/Libraries/Core/Services/ItemBaseClassService.cs index a8b2e3f6..b26f66b6 100644 --- a/Libraries/Core/Services/ItemBaseClassService.cs +++ b/Libraries/Core/Services/ItemBaseClassService.cs @@ -68,7 +68,7 @@ public class ItemBaseClassService( HydrateItemBaseClassCache(); } - if (itemTpl is null) + if (string.IsNullOrEmpty(itemTpl)) { _logger.Warning("Unable to check itemTpl base class as value passed is null"); diff --git a/Libraries/Core/Services/ItemFilterService.cs b/Libraries/Core/Services/ItemFilterService.cs index affd997e..6ad30f38 100644 --- a/Libraries/Core/Services/ItemFilterService.cs +++ b/Libraries/Core/Services/ItemFilterService.cs @@ -10,14 +10,13 @@ namespace Core.Services; public class ItemFilterService( ISptLogger _logger, ICloner _cloner, - DatabaseServer _databaseServer, ConfigServer _configServer ) { protected ItemConfig _itemConfig = _configServer.GetConfig(); - protected HashSet? _lootableItemBlacklistCache = new HashSet(); - protected HashSet? _itemBlacklistCache = new HashSet(); + protected HashSet? _lootableItemBlacklistCache = []; + protected HashSet? _itemBlacklistCache = []; /** * Check if the provided template id is blacklisted in config/item.json/blacklist @@ -26,7 +25,14 @@ public class ItemFilterService( */ public bool ItemBlacklisted(string tpl) { - throw new NotImplementedException(); + if (_itemBlacklistCache.Count == 0) + { + foreach (var item in _itemConfig.Blacklist) { + _itemBlacklistCache.Add(item); + } + } + + return _itemBlacklistCache.Contains(tpl); } /** @@ -36,7 +42,14 @@ public class ItemFilterService( */ public bool LootableItemBlacklisted(string tpl) { - throw new NotImplementedException(); + if (_lootableItemBlacklistCache.Count == 0) + { + foreach (var item in _itemConfig.LootableItemBlacklist) { + _itemBlacklistCache.Add(item); + } + } + + return _lootableItemBlacklistCache.Contains(tpl); } /** @@ -46,7 +59,7 @@ public class ItemFilterService( */ public bool ItemRewardBlacklisted(string tpl) { - throw new NotImplementedException(); + return _itemConfig.RewardItemBlacklist.Contains(tpl); } /** @@ -55,7 +68,7 @@ public class ItemFilterService( */ public List GetItemRewardBlacklist() { - throw new NotImplementedException(); + return _cloner.Clone(_itemConfig.RewardItemBlacklist).ToList(); } /** @@ -64,7 +77,7 @@ public class ItemFilterService( */ public List GetItemRewardBaseTypeBlacklist() { - throw new NotImplementedException(); + return _cloner.Clone(_itemConfig.RewardItemTypeBlacklist).ToList(); } /** @@ -73,7 +86,7 @@ public class ItemFilterService( */ public List GetBlacklistedItems() { - return _cloner.Clone(_itemConfig.Blacklist); + return _cloner.Clone(_itemConfig.Blacklist).ToList(); } /** @@ -82,7 +95,7 @@ public class ItemFilterService( */ public List GetBlacklistedLootableItems() { - throw new NotImplementedException(); + return _cloner.Clone(_itemConfig.LootableItemBlacklist).ToList(); } /** @@ -92,7 +105,7 @@ public class ItemFilterService( */ public bool BossItem(string tpl) { - throw new NotImplementedException(); + return _itemConfig.BossItems.Contains(tpl); } /** @@ -101,7 +114,8 @@ public class ItemFilterService( */ public List GetBossItems() { - throw new NotImplementedException(); + + return _cloner.Clone(_itemConfig.BossItems).ToList(); } /** diff --git a/Libraries/Core/Services/LocationLifecycleService.cs b/Libraries/Core/Services/LocationLifecycleService.cs index 08567b7d..9830ae08 100644 --- a/Libraries/Core/Services/LocationLifecycleService.cs +++ b/Libraries/Core/Services/LocationLifecycleService.cs @@ -485,6 +485,11 @@ public class LocationLifecycleService */ protected void HandleCarExtract(string extractName, PmcData pmcData, string sessionId) { + pmcData.CarExtractCounts?.TryAdd(extractName, 0); + + // Increment extract count value + pmcData.CarExtractCounts[extractName] += 1; + var newFenceStanding = GetFenceStandingAfterExtract( pmcData, _inRaidConfig.CarExtractBaseStandingGain, @@ -513,10 +518,14 @@ public class LocationLifecycleService */ protected void HandleCoopExtract(string sessionId, PmcData pmcData, string extractName) { + pmcData.CoopExtractCounts?.TryAdd(extractName, 0); + + pmcData.CoopExtractCounts[extractName] += 1; + var newFenceStanding = GetFenceStandingAfterExtract( pmcData, - _inRaidConfig.CarExtractBaseStandingGain, - pmcData.CarExtractCounts[extractName]); + _inRaidConfig.CoopExtractBaseStandingGain, + pmcData.CoopExtractCounts[extractName]); var fenceId = Traders.FENCE; pmcData.TradersInfo[fenceId].Standing = newFenceStanding; @@ -525,8 +534,6 @@ public class LocationLifecycleService _traderHelper.LevelUp(fenceId, pmcData); pmcData.TradersInfo[fenceId].LoyaltyLevel = Math.Max((int)pmcData.TradersInfo[fenceId].LoyaltyLevel, 1); - _logger.Debug($"Car extract: {extractName} used, total times taken: {pmcData.CarExtractCounts[extractName]}"); - // Copy updated fence rep values into scav profile to ensure consistency var scavData = _profileHelper.GetScavProfile(sessionId); scavData.TradersInfo[fenceId].Standing = pmcData.TradersInfo[fenceId].Standing; diff --git a/Libraries/Core/Services/RagfairPriceService.cs b/Libraries/Core/Services/RagfairPriceService.cs index 7b35d94d..266c6e7e 100644 --- a/Libraries/Core/Services/RagfairPriceService.cs +++ b/Libraries/Core/Services/RagfairPriceService.cs @@ -34,12 +34,13 @@ public class RagfairPriceService( /// public async Task OnLoadAsync() { - throw new NotImplementedException(); + RefreshStaticPrices(); + RefreshDynamicPrices(); } public string GetRoute() { - throw new NotImplementedException(); + return "RagfairPriceService"; } /// @@ -58,7 +59,7 @@ public class RagfairPriceService( /// public void RefreshDynamicPrices() { - throw new NotImplementedException(); + // TODO: remove as redundant? } /// @@ -95,7 +96,13 @@ public class RagfairPriceService( /// Rouble price public double GetFleaPriceForOfferItems(List offerItems) { - throw new NotImplementedException(); + // Preset weapons take the direct prices.json value, otherwise they're massivly inflated + if (_itemHelper.IsOfBaseclass(offerItems[0].Template, BaseClasses.WEAPON)) + { + return GetFleaPriceForItem(offerItems[0].Template); + } + + return offerItems.Sum(item => GetFleaPriceForItem(item.Template)); } ///