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
This commit is contained in:
Chomp
2025-06-14 18:56:19 +01:00
parent e596974d07
commit 1f061cfe89
6 changed files with 196 additions and 53 deletions
@@ -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": {
@@ -171,35 +171,58 @@ public class LootGenerator(
/// <summary>
/// Generate An array of items
/// TODO - handle weapon presets/ammo packs
/// TODO - handle ammo packs
/// </summary>
/// <param name="forcedLootDict">Dictionary of item tpls with minmax values</param>
/// <param name="forcedLootToAdd">Dictionary of item tpls with minmax values</param>
/// <returns>Array of Item</returns>
public List<List<Item>> CreateForcedLoot(Dictionary<string, MinMax<int>> forcedLootDict)
public List<List<Item>> CreateForcedLoot(Dictionary<string, MinMax<int>> forcedLootToAdd)
{
var result = new List<List<Item>>();
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;
@@ -948,6 +948,49 @@ public class ItemHelper(
return rootAndChildren;
}
/// <summary>
/// Splits the item stack if it exceeds its items StackMaxSize property into child items of the passed parent.
/// TODO: untested
/// </summary>
/// <param name="itemWithChildren">Item (with children) to split into smaller stacks.</param>
/// <returns>List of root item + children.</returns>
public List<List<Item>> SplitStack(List<Item> 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<List<Item>> 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;
}
/// <summary>
/// Turns items like money into separate stacks that adhere to max stack size.
/// </summary>
@@ -27,10 +27,10 @@ public class PresetHelper(
_lookup = input;
}
/**
* Get default weapon and equipment presets
* @returns Dictionary
*/
/// <summary>
/// Get weapon and armor default presets, keyed to preset id NOT item tpl
/// </summary>
/// <returns></returns>
public Dictionary<string, Preset> GetDefaultPresets()
{
var weapons = GetDefaultWeaponPresets();
@@ -39,10 +39,26 @@ public class PresetHelper(
return weapons.Union(equipment).ToDictionary();
}
/**
* Get default weapon presets
* @returns Dictionary
*/
/// <summary>
/// Get weapon and armor default presets, keyed to root items tpl
/// </summary>
/// <returns>dictionary of presets keyed by the root items tpl</returns>
public Dictionary<string, Preset> 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);
}
/// <summary>
/// Get default weapon presets
/// </summary>
/// <returns></returns>
public Dictionary<string, Preset> GetDefaultWeaponPresets()
{
if (_defaultWeaponPresets is null)
@@ -58,10 +74,10 @@ public class PresetHelper(
return _defaultWeaponPresets;
}
/**
* Get default equipment presets
* @returns Dictionary
*/
/// <summary>
/// Get default equipment presets
/// </summary>
/// <returns>Dictionary</returns>
public Dictionary<string, Preset> GetDefaultEquipmentPresets()
{
if (_defaultEquipmentPresets == null)
@@ -77,6 +93,11 @@ public class PresetHelper(
return _defaultEquipmentPresets;
}
/// <summary>
/// Is the provided id a preset id
/// </summary>
/// <param name="id">Value to check</param>
/// <returns>True = preset exists for this id</returns>
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);
}
/// <summary>
/// Does the provided tpl have a preset
/// </summary>
/// <param name="templateId">Tpl id to check</param>
/// <returns>True if preset exists for tpl</returns>
public bool HasPreset(string templateId)
{
return _lookup.ContainsKey(templateId);
@@ -108,6 +134,10 @@ public class PresetHelper(
return _cloner.Clone(_databaseService.GetGlobals().ItemPresets[id]);
}
/// <summary>
/// Get all presets from globals db
/// </summary>
/// <returns>List</returns>
public List<Preset> 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
*/
/// <summary>
/// Return the price of the preset for the given item tpl, or for the tpl itself if no preset exists
/// </summary>
/// <param name="tpl">The item template to get the price of</param>
/// <returns>The price of the given item preset, or base item if no preset exists</returns>
public double GetDefaultPresetOrItemPrice(string tpl)
{
// Get default preset if it exists
@@ -17,5 +17,6 @@ public enum SptAirdropTypeEnum
barter,
foodMedical,
weaponArmor,
radar
radar,
toiletPaper
}
@@ -25,20 +25,20 @@ public class AirdropService(
ItemFilterService _itemFilterService,
ItemHelper _itemHelper)
{
protected AirdropConfig _airdropConfig = configServer.GetConfig<AirdropConfig>();
protected readonly AirdropConfig _airdropConfig = configServer.GetConfig<AirdropConfig>();
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();
}
/// <summary>
@@ -50,7 +50,7 @@ public class AirdropService(
/// <returns>List of LootItem objects</returns>
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(
/// <returns>Items that will fit container</returns>
protected List<List<Item>> GetLootThatFitsContainer(Item container, List<List<Item>> crateLootPool)
{
// list of root item + children in list
var lootResult = new List<List<Item>>();
// 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(
/// <returns>LootRequest</returns>
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