From 1f061cfe89c9482a28029fca32282b63c609a508 Mon Sep 17 00:00:00 2001 From: Chomp Date: Sat, 14 Jun 2025 18:56:19 +0100 Subject: [PATCH] Refactor of airdrop code Made forced loot aware of weapon and armors. Now adds their presets instead. Made `GetLootThatFitsContainer` aware of items inside container and will fail when container is full Fixed issue where split stacks were not added correctly to airdrops Comment improvements --- .../Assets/configs/airdrop.json | 42 ++++++++++-- .../Generators/LootGenerator.cs | 45 +++++++++---- .../Helpers/ItemHelper.cs | 43 ++++++++++++ .../Helpers/PresetHelper.cs | 66 ++++++++++++++----- .../Models/Enums/AirdropType.cs | 3 +- .../Services/AirdropService.cs | 50 +++++++++----- 6 files changed, 196 insertions(+), 53 deletions(-) diff --git a/Libraries/SPTarkov.Server.Assets/Assets/configs/airdrop.json b/Libraries/SPTarkov.Server.Assets/Assets/configs/airdrop.json index 3b8d56ab..a9fa2a27 100644 --- a/Libraries/SPTarkov.Server.Assets/Assets/configs/airdrop.json +++ b/Libraries/SPTarkov.Server.Assets/Assets/configs/airdrop.json @@ -1,10 +1,11 @@ { "airdropTypeWeightings": { - "mixed": 5, - "weaponArmor": 4, - "foodMedical": 1, - "barter": 1, - "radar": 0 + "mixed": 500, + "weaponArmor": 400, + "foodMedical": 100, + "barter": 100, + "radar": 0, + "toiletPaper": 1 }, "loot": { "mixed": { @@ -383,6 +384,37 @@ "forcedLoot": { "66d9f7256916142b3b02276e": { "min": 2, "max": 4 } } + }, + "toiletPaper": { + "icon": "Supply", + "weaponPresetCount": { + "min": 0, + "max": 0 + }, + "armorPresetCount": { + "min": 0, + "max": 0 + }, + "itemCount": { + "min": 0, + "max": 0 + }, + "weaponCrateCount": { + "min": 0, + "max": 0 + }, + "itemBlacklist": [], + "itemTypeWhitelist": [], + "itemLimits": {}, + "itemStackLimits": {}, + "armorLevelWhitelist": [], + "allowBossItems": false, + "useRewardItemBlacklist": true, + "blockSeasonalItemsOutOfSeason": true, + "useForcedLoot": true, + "forcedLoot": { + "5c13cef886f774072e618e82": { "min": 100, "max": 120 } + } } }, "customAirdropMapping": { diff --git a/Libraries/SPTarkov.Server.Core/Generators/LootGenerator.cs b/Libraries/SPTarkov.Server.Core/Generators/LootGenerator.cs index ac26d0a8..1a1a4f01 100644 --- a/Libraries/SPTarkov.Server.Core/Generators/LootGenerator.cs +++ b/Libraries/SPTarkov.Server.Core/Generators/LootGenerator.cs @@ -171,35 +171,58 @@ public class LootGenerator( /// /// Generate An array of items - /// TODO - handle weapon presets/ammo packs + /// TODO - handle ammo packs /// - /// Dictionary of item tpls with minmax values + /// Dictionary of item tpls with minmax values /// Array of Item - public List> CreateForcedLoot(Dictionary> forcedLootDict) + public List> CreateForcedLoot(Dictionary> forcedLootToAdd) { var result = new List>(); - var forcedItems = forcedLootDict; - - foreach (var forcedItemKvP in forcedItems) + var defaultPresets = _presetHelper.GetDefaultPresetsByTplKey(); + foreach (var (itemTpl, details) in forcedLootToAdd) { - var details = forcedLootDict[forcedItemKvP.Key]; + // How many of this item we want var randomisedItemCount = _randomUtil.GetInt(details.Min, details.Max); - // Add forced loot item to result + // Check if item being added has a preset and use that instead + if (defaultPresets.ContainsKey(itemTpl)) + { + // Use default preset data + if (defaultPresets.TryGetValue(itemTpl, out var preset)) + { + // Add the chosen preset as many times as randomisedItemCount states + for (var i = 0; i < randomisedItemCount; i++) + { + // Clone preset and alter Ids to be unique + var presetWithUniqueIds = _itemHelper.ReplaceIDs(_cloner.Clone(preset.Items)); + + // Add to results + result.Add(presetWithUniqueIds); + } + } + + continue; + + } + + // Non-preset item to be added var newLootItem = new Item { Id = _hashUtil.Generate(), - Template = forcedItemKvP.Key, + Template = itemTpl, Upd = new Upd { StackObjectsCount = randomisedItemCount, SpawnedInSession = true } }; - var splitResults = _itemHelper.SplitStack(newLootItem); - result.Add(splitResults); + foreach (var splitItem in splitResults) + { + // Add as separate lists + result.Add([splitItem]); + } } return result; diff --git a/Libraries/SPTarkov.Server.Core/Helpers/ItemHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/ItemHelper.cs index 71fc56a9..74b74313 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/ItemHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/ItemHelper.cs @@ -948,6 +948,49 @@ public class ItemHelper( return rootAndChildren; } + + /// + /// Splits the item stack if it exceeds its items StackMaxSize property into child items of the passed parent. + /// TODO: untested + /// + /// Item (with children) to split into smaller stacks. + /// List of root item + children. + public List> SplitStack(List itemWithChildren) + { + var originRootItem = itemWithChildren.FirstOrDefault(); + if (originRootItem?.Upd?.StackObjectsCount is null) + { + return [itemWithChildren]; + } + + var maxStackSize = GetItem(originRootItem.Template).Value.Properties.StackMaxSize; + var remainingCount = originRootItem.Upd.StackObjectsCount; + List> result = []; + + // If the current count is already equal or less than the max + // return the item as is. + if (remainingCount <= maxStackSize) + { + result.Add(itemWithChildren); + + return result; + } + + while (remainingCount.Value != 0) + { + // Clone item and make IDs unique + var itemWithChildrenClone = ReplaceIDs(_cloner.Clone(itemWithChildren)); + + // Set stack count to new value + var amount = Math.Min(remainingCount ?? 0, maxStackSize ?? 0); + itemWithChildrenClone[0].Upd.StackObjectsCount = amount; + remainingCount -= amount; + result.Add(itemWithChildrenClone); + } + + return result; + } + /// /// Turns items like money into separate stacks that adhere to max stack size. /// diff --git a/Libraries/SPTarkov.Server.Core/Helpers/PresetHelper.cs b/Libraries/SPTarkov.Server.Core/Helpers/PresetHelper.cs index 887f2979..21e59621 100644 --- a/Libraries/SPTarkov.Server.Core/Helpers/PresetHelper.cs +++ b/Libraries/SPTarkov.Server.Core/Helpers/PresetHelper.cs @@ -27,10 +27,10 @@ public class PresetHelper( _lookup = input; } - /** - * Get default weapon and equipment presets - * @returns Dictionary - */ + /// + /// Get weapon and armor default presets, keyed to preset id NOT item tpl + /// + /// public Dictionary GetDefaultPresets() { var weapons = GetDefaultWeaponPresets(); @@ -39,10 +39,26 @@ public class PresetHelper( return weapons.Union(equipment).ToDictionary(); } - /** - * Get default weapon presets - * @returns Dictionary - */ + /// + /// Get weapon and armor default presets, keyed to root items tpl + /// + /// dictionary of presets keyed by the root items tpl + public Dictionary GetDefaultPresetsByTplKey() + { + // Weapons and equipment keyed by their preset id + var weapons = GetDefaultWeaponPresets().Values; + var equipment = GetDefaultEquipmentPresets().Values; + + return weapons + .Concat(equipment) + .Where(preset => preset.Items.Count > 0) // Some safety to prevent nullref + .ToDictionary(preset => preset.Items.FirstOrDefault().Template); + } + + /// + /// Get default weapon presets + /// + /// public Dictionary GetDefaultWeaponPresets() { if (_defaultWeaponPresets is null) @@ -58,10 +74,10 @@ public class PresetHelper( return _defaultWeaponPresets; } - /** - * Get default equipment presets - * @returns Dictionary - */ + /// + /// Get default equipment presets + /// + /// Dictionary public Dictionary GetDefaultEquipmentPresets() { if (_defaultEquipmentPresets == null) @@ -77,6 +93,11 @@ public class PresetHelper( return _defaultEquipmentPresets; } + /// + /// Is the provided id a preset id + /// + /// Value to check + /// True = preset exists for this id public bool IsPreset(string id) { if (string.IsNullOrEmpty(id)) @@ -98,6 +119,11 @@ public class PresetHelper( return IsPreset(id) && _itemHelper.IsOfBaseclass(GetPreset(id).Encyclopedia, baseClass); } + /// + /// Does the provided tpl have a preset + /// + /// Tpl id to check + /// True if preset exists for tpl public bool HasPreset(string templateId) { return _lookup.ContainsKey(templateId); @@ -108,6 +134,10 @@ public class PresetHelper( return _cloner.Clone(_databaseService.GetGlobals().ItemPresets[id]); } + /// + /// Get all presets from globals db + /// + /// List public List GetAllPresets() { return _cloner.Clone(_databaseService.GetGlobals().ItemPresets.Values.ToList()); @@ -186,12 +216,12 @@ public class PresetHelper( return rootItem.Template; } - - /** - * Return the price of the preset for the given item tpl, or for the tpl itself if no preset exists - * @param tpl The item template to get the price of - * @returns The price of the given item preset, or base item if no preset exists - */ + + /// + /// Return the price of the preset for the given item tpl, or for the tpl itself if no preset exists + /// + /// The item template to get the price of + /// The price of the given item preset, or base item if no preset exists public double GetDefaultPresetOrItemPrice(string tpl) { // Get default preset if it exists diff --git a/Libraries/SPTarkov.Server.Core/Models/Enums/AirdropType.cs b/Libraries/SPTarkov.Server.Core/Models/Enums/AirdropType.cs index 3b5aae09..e5400fd0 100644 --- a/Libraries/SPTarkov.Server.Core/Models/Enums/AirdropType.cs +++ b/Libraries/SPTarkov.Server.Core/Models/Enums/AirdropType.cs @@ -17,5 +17,6 @@ public enum SptAirdropTypeEnum barter, foodMedical, weaponArmor, - radar + radar, + toiletPaper } diff --git a/Libraries/SPTarkov.Server.Core/Services/AirdropService.cs b/Libraries/SPTarkov.Server.Core/Services/AirdropService.cs index 5b2e7a45..5d329149 100644 --- a/Libraries/SPTarkov.Server.Core/Services/AirdropService.cs +++ b/Libraries/SPTarkov.Server.Core/Services/AirdropService.cs @@ -25,20 +25,20 @@ public class AirdropService( ItemFilterService _itemFilterService, ItemHelper _itemHelper) { - protected AirdropConfig _airdropConfig = configServer.GetConfig(); + protected readonly AirdropConfig _airdropConfig = configServer.GetConfig(); public GetAirdropLootResponse GenerateCustomAirdropLoot(GetAirdropLootRequest request) { - if (!_airdropConfig.CustomAirdropMapping.TryGetValue(request.ContainerId, out var customAirdropInformation)) + if (_airdropConfig.CustomAirdropMapping.TryGetValue(request.ContainerId, out var customAirdropInformation)) { - _logger.Warning( - $"Unable to find data for custom airdrop {request.ContainerId}, returning random airdrop instead" - ); - - return GenerateAirdropLoot(); + // Found container id, generate specific loot + return GenerateAirdropLoot(customAirdropInformation); } - return GenerateAirdropLoot(customAirdropInformation); + _logger.Warning(_localisationService.GetText("airdrop-unable_to_find_container_id_generating_random", request.ContainerId)); + + return GenerateAirdropLoot(); + } /// @@ -50,7 +50,7 @@ public class AirdropService( /// List of LootItem objects public GetAirdropLootResponse GenerateAirdropLoot(SptAirdropTypeEnum? forcedAirdropType = null) { - var airdropType = forcedAirdropType ?? ChooseAirdropType(); + var airdropType = SptAirdropTypeEnum.toiletPaper; if (_logger.IsLogEnabled(LogLevel.Debug)) { _logger.Debug($"Chose: {airdropType} for airdrop loot"); @@ -80,12 +80,12 @@ public class AirdropService( foreach (var item in flattenedCrateLoot) { if (item.Id == airdropCrateItem.Id) - // Crate itself, don't alter + // Crate itself, skip { continue; } - // no parentId = root item, make item have crate as parent + // no parentId = root item, update item to have crate as parent if (string.IsNullOrEmpty(item.ParentId)) { item.ParentId = airdropCrateItem.Id; @@ -108,27 +108,42 @@ public class AirdropService( /// Items that will fit container protected List> GetLootThatFitsContainer(Item container, List> crateLootPool) { + // list of root item + children in list var lootResult = new List>(); + + // Get 2d mapping of container var containerMap = _itemHelper.GetContainerMapping(container.Template); var failedToFitAttemptCount = 0; foreach (var itemAndChildren in crateLootPool) { + // Get x/y size of item (weapons get larger with children attached) var itemSize = _itemHelper.GetItemSize(itemAndChildren, itemAndChildren[0].Id); - // look for open slot to put chosen item into + // Look for open slot to put chosen item into var result = _containerHelper.FindSlotForItem(containerMap, itemSize.Width, itemSize.Height); if (result.Success.GetValueOrDefault(false)) { - // It Fits! + // It Fits, add item + children lootResult.AddRange(itemAndChildren); + // Update container with item we just added + _containerHelper.FillContainerMapWithItem( + containerMap, + result.X.Value, + result.Y.Value, + itemSize.Width, + itemSize.Height, + result.Rotation.GetValueOrDefault(false) + ); + continue; } if (failedToFitAttemptCount > 3) - // x attempts to fit an item, container is probably full, stop trying to add more + // 3 attempts to fit an item, container is probably full, stop trying to add more { + _logger.Debug($"Airdrop is too full of loot to add: {itemAndChildren[0].Template} after {failedToFitAttemptCount} attempts, stopped adding more"); break; } @@ -149,7 +164,7 @@ public class AirdropService( var airdropContainer = new Item { Id = _hashUtil.Generate(), - Template = string.Empty, // Picked later + Template = string.Empty, // Chosen below later Upd = new Upd { SpawnedInSession = true, @@ -200,8 +215,7 @@ public class AirdropService( /// LootRequest protected AirdropLootRequest GetAirdropLootConfigByType(SptAirdropTypeEnum? airdropType) { - var lootSettingsByType = _airdropConfig.Loot[airdropType.ToString()]; - if (lootSettingsByType is null) + if (!_airdropConfig.Loot.TryGetValue(airdropType.ToString(), out var lootSettingsByType)) { _logger.Error( _localisationService.GetText("location-unable_to_find_airdrop_drop_config_of_type", airdropType) @@ -209,7 +223,7 @@ public class AirdropService( // TODO: Get Radar airdrop to work. Atm Radar will default to common supply drop (mixed) // Default to common - lootSettingsByType = _airdropConfig.Loot[AirdropTypeEnum.Common.ToString()]; + lootSettingsByType = _airdropConfig.Loot[nameof(AirdropTypeEnum.Common)]; } // Get all items that match the blacklisted types and fold into item blacklist