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