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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user