using System.Text.Json;
using SPTarkov.Common.Extensions;
using SPTarkov.Server.Core.Helpers;
using SPTarkov.Server.Core.Models.Common;
using SPTarkov.Server.Core.Models.Eft.Common;
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
namespace SPTarkov.Server.Core.Extensions;
public static class ItemExtensions
{
///
/// This method will compare two items and see if they are equivalent
/// This method will NOT compare IDs on the items
///
/// first item to compare
/// second item to compare
/// Upd properties to compare between the items
/// true if they are the same
public static bool IsSameItem(this Item item1, Item item2, ISet? compareUpdProperties = null)
{
// Different tpl == different item
if (item1.Template != item2.Template)
{
return false;
}
// Both lack upd object + same tpl = same
if (item1.Upd is null && item2.Upd is null)
{
return true;
}
// item1 lacks upd, item2 has one
if (item1.Upd is null && item2.Upd is not null)
{
return false;
}
// item1 has upd, item2 lacks one
if (item1.Upd is not null && item2.Upd is null)
{
return false;
}
// key = Upd property Type as string, value = comparison function that returns bool
var comparers = new Dictionary>
{
{ "Key", (upd1, upd2) => upd1.Key?.NumberOfUsages == upd2.Key?.NumberOfUsages },
{ "Buff", (upd1, upd2) => upd1.Buff?.Value == upd2.Buff?.Value && upd1.Buff?.BuffType == upd2.Buff?.BuffType },
{ "CultistAmulet", (upd1, upd2) => upd1.CultistAmulet?.NumberOfUsages == upd2.CultistAmulet?.NumberOfUsages },
{ "Dogtag", (upd1, upd2) => upd1.Dogtag?.ProfileId == upd2.Dogtag?.ProfileId },
{ "FaceShield", (upd1, upd2) => upd1.FaceShield?.Hits == upd2.FaceShield?.Hits },
{
"Foldable",
(upd1, upd2) => upd1.Foldable?.Folded.GetValueOrDefault(false) == upd2.Foldable?.Folded.GetValueOrDefault(false)
},
{ "FoodDrink", (upd1, upd2) => upd1.FoodDrink?.HpPercent == upd2.FoodDrink?.HpPercent },
{ "MedKit", (upd1, upd2) => upd1.MedKit?.HpResource == upd2.MedKit?.HpResource },
{ "RecodableComponent", (upd1, upd2) => upd1.RecodableComponent?.IsEncoded == upd2.RecodableComponent?.IsEncoded },
{ "RepairKit", (upd1, upd2) => upd1.RepairKit?.Resource == upd2.RepairKit?.Resource },
{ "Resource", (upd1, upd2) => upd1.Resource?.UnitsConsumed == upd2.Resource?.UnitsConsumed },
};
// Choose above keys or passed in keys to compare items with
var valuesToCompare = compareUpdProperties?.Count > 0 ? compareUpdProperties : comparers.Keys.ToHashSet();
foreach (var propertyName in valuesToCompare)
{
if (!comparers.TryGetValue(propertyName, out var comparer))
// Key not found, skip
{
continue;
}
if (!comparer(item1.Upd, item2.Upd))
{
return false;
}
}
return true;
}
///
/// Check if item is stored inside a container
///
/// Item to check is inside of container
/// Name of slot to check item is in e.g. SecuredContainer/Backpack
/// Inventory with child parent items to check
/// True when item is in container
public static bool ItemIsInsideContainer(this Item itemToCheck, string desiredContainerSlotId, IEnumerable- items)
{
// Get items parent
var parent = items.FirstOrDefault(item => item.Id.Equals(itemToCheck.ParentId));
if (parent is null)
// No parent, end of line, not inside container
{
return false;
}
if (parent.SlotId == desiredContainerSlotId)
{
return true;
}
return parent.ItemIsInsideContainer(desiredContainerSlotId, items);
}
///
/// Get the size of a stack, return 1 if no stack object count property found
///
/// Item to get stack size of
/// size of stack
public static int GetItemStackSize(this Item item)
{
if (item.Upd?.StackObjectsCount is not null)
{
return (int)item.Upd.StackObjectsCount;
}
return 1;
}
///
/// Create a dictionary from a collection of items, keyed by item id
///
/// Collection of items
/// Dictionary of items
public static Dictionary GenerateItemsMap(this IEnumerable
- items)
{
// Convert list to dictionary, keyed by items Id
return items.ToDictionary(item => item.Id);
}
///
/// Adopts orphaned items by resetting them as root "hideout" items. Helpful in situations where a parent has been
/// deleted from a group of items and there are children still referencing the missing parent. This method will
/// remove the reference from the children to the parent and set item properties to root values.
///
/// The ID of the "root" of the container
/// Array of Items that should be adjusted
/// Returns Array of Items that have been adopted
public static List
- AdoptOrphanedItems(this List
- items, string rootId)
{
foreach (var item in items)
{
// Check if the item's parent exists.
var parentExists = items.Any(parentItem => parentItem.Id.Equals(item.ParentId));
// If the parent does not exist and the item is not already a 'hideout' item, adopt the orphaned item by
// setting the parent ID to the PMCs inventory equipment ID, the slot ID to 'hideout', and remove the location.
if (!parentExists && item.ParentId != rootId && item.SlotId != "hideout")
{
item.ParentId = rootId;
item.SlotId = "hideout";
item.Location = null;
}
}
return items;
}
///
/// Recursive function that looks at every item from parameter and gets their children's Ids + includes parent item in results
///
/// List of items (item + possible children)
/// Parent item's id
/// list of child item ids
public static List GetItemWithChildrenTpls(this IEnumerable
- items, MongoId baseItemId)
{
List list = [];
foreach (var childItem in items)
{
if (childItem.ParentId == baseItemId.ToString())
{
list.AddRange(GetItemWithChildrenTpls(items, childItem.Id));
}
}
list.Add(baseItemId); // Required, push original item id onto array
return list;
}
///
/// Check if the passed in item has buy count restrictions
///
/// Item to check
/// true if it has buy restrictions
public static bool HasBuyRestrictions(this Item itemToCheck)
{
return itemToCheck.Upd?.BuyRestrictionCurrent is not null && itemToCheck.Upd?.BuyRestrictionMax is not null;
}
///
/// Gets the identifier for a child using slotId, locationX and locationY.
///
/// Item.
/// SlotId OR slotId, locationX, locationY.
public static string GetChildId(this Item item)
{
if (item.Location is null)
{
return item.SlotId;
}
var LocationTyped = (ItemLocation)item.Location;
return $"{item.SlotId},{LocationTyped.X},{LocationTyped.Y}";
}
public static bool IsVertical(this ItemLocation itemLocation)
{
return itemLocation.R == ItemRotation.Vertical;
}
///
/// Update items upd.StackObjectsCount to be 1 if its upd is missing or StackObjectsCount is undefined
///
/// Item to update
/// Fixed item
public static void FixItemStackCount(this Item item)
{
// Ensure item has 'Upd' object
item.Upd ??= new Upd { StackObjectsCount = 1 };
// Ensure item has 'StackObjectsCount' property
item.Upd.StackObjectsCount ??= 1;
}
///
/// Get an item with its attachments (children)
///
/// List of items (item + possible children)
/// Parent item's id
/// OPTIONAL - Include only mod items, exclude items stored inside root item
/// list of Item objects
public static List
- GetItemWithChildren(this IEnumerable
- items, MongoId baseItemId, bool excludeStoredItems = false)
{
var childrenByParent = items.CreateParentIdLookupCache(out var rootItem, baseItemId);
if (rootItem is null)
{
// Root not found, nothing to return, exit
return [];
}
var result = new List
- ();
var processingStack = new Stack
- ();
processingStack.Push(rootItem);
while (processingStack.Count > 0)
{
var current = processingStack.Pop();
result.Add(current);
if (!childrenByParent.TryGetValue(current.Id.ToString(), out var children))
{
// No children, skip to next
continue;
}
foreach (var child in children)
{
// Child item has a location property = is stored inside parent and not a mod, skip
if (excludeStoredItems && child.Location is not null)
{
continue;
}
// Add item to stack to check if it has children we need to add to result
processingStack.Push(child);
}
}
return result;
}
///
/// Cache items by their parentId
///
/// items to process
/// Id of root item
/// Root item from inputted data
/// Dictionary of items keyed by their parentId
public static Dictionary> CreateParentIdLookupCache(
this IEnumerable
- items,
out Item? rootItem,
MongoId? baseItemId = null
)
{
rootItem = null;
// If passed in items implements ICollection, we can determine size and pre-allocate to avoid re-allocations
var capacity = items is ICollection
- collection ? collection.Count : 0;
// Create lookup of items keyed by parentId
var childrenByParent = new Dictionary>(capacity);
foreach (var item in items)
{
if (baseItemId is not null && item.Id == baseItemId)
{
// Root item found, store in out param
rootItem = item;
}
if (item.ParentId is null)
{
// no parent, nothing to key item against
continue;
}
if (!childrenByParent.TryGetValue(item.ParentId, out var children))
{
// No collection for this parentId, create
children = [];
childrenByParent[item.ParentId] = children;
}
children.Add(item);
}
return childrenByParent;
}
///
/// Convert an Item to SptLootItem
///
/// Item to convert
/// Converted SptLootItem
public static SptLootItem ToLootItem(this Item item)
{
var lootItem = new SptLootItem
{
ComposedKey = null,
Id = item.Id,
Template = item.Template,
Upd = item.Upd,
ParentId = item.ParentId,
SlotId = item.SlotId,
Location = item.Location,
Desc = item.Desc,
};
if (item.TryGetExtensionData(out var extensionData))
{
lootItem.AddAllToExtensionData(extensionData!);
}
return lootItem;
}
public static ItemLocation? GetParsedLocation(this Item item)
{
if (item.Location is null)
{
return null;
}
if (item.Location is JsonElement element)
{
// TODO: when is this true
return element.ToObject();
}
return (ItemLocation)item.Location;
}
///
/// Get a list of the item IDs (NOT tpls) inside a secure container
///
/// Inventory items to look for secure container in
/// List of ids
public static HashSet GetSecureContainerItems(this IEnumerable
- items)
{
var secureContainer = items.First(x => x.SlotId == "SecuredContainer");
// No container found, drop out
if (secureContainer is null)
{
return [];
}
var itemsInSecureContainer = items.GetItemWithChildrenTpls(secureContainer.Id);
// Return all items returned and exclude the secure container item itself
return itemsInSecureContainer.Where(x => x != secureContainer.Id).ToHashSet();
}
///
/// Regenerate all GUIDs with new IDs, except special item types (e.g. quest, sorting table, etc.)
///
///
///
public static IEnumerable
- ReplaceIDs(this IEnumerable
- items)
{
foreach (var item in items)
{
// Generate new id
var newId = new MongoId();
// Keep copy of original id
var originalId = item.Id;
// Update items id to new one we generated
item.Id = newId;
// Find all children of item and update their parent ids to match
var childItems = items.Where(item => item.ParentId == originalId.ToString());
foreach (var childItem in childItems)
{
childItem.ParentId = newId;
}
}
return items;
}
///
/// Update a root items _id property value to be unique
///
/// Item to update root items _id property
/// Optional: new id to use
/// New root id
public static MongoId RemapRootItemId(this IEnumerable
- itemWithChildren, MongoId? newId = null)
{
newId ??= new MongoId();
var rootItemExistingId = itemWithChildren.FirstOrDefault().Id;
foreach (var item in itemWithChildren)
{
// Root, update id
if (item.Id.Equals(rootItemExistingId))
{
item.Id = newId.Value;
continue;
}
// Child with parent of root, update
if (item.ParentId == rootItemExistingId)
{
item.ParentId = newId.Value;
}
}
return newId.Value;
}
///
/// Create 2 hashsets for passed in items, keyed by the items ID and by the items parentId
///
/// Items to hash
/// InventoryItemHash
public static InventoryItemHash GetInventoryItemHash(this IEnumerable
- inventoryItems)
{
Dictionary byItemId = new();
Dictionary> byParentId = new();
foreach (var item in inventoryItems)
{
// Add every item to 'byItemId'
byItemId[item.Id] = item;
if (string.IsNullOrEmpty(item.ParentId) || item.ParentId == "hideout")
{
// Inventory non-items, skip
continue;
}
var parentId = new MongoId(item.ParentId);
if (!byParentId.TryGetValue(parentId, out var childItems))
{
// Hashset doesn't exist for this parentId, create and add blank set
childItems = [];
byParentId[parentId] = childItems;
}
childItems.Add(item);
}
return new InventoryItemHash { ByItemId = byItemId, ByParentId = byParentId };
}
///
/// Remove spawned in session (FiR) status from items inside a container
///
/// Player profile
/// Container slot id to find items for and remove FiR from e.g. "Backpack"
public static void RemoveFiRStatusFromItemsInContainer(this PmcData pmcData, string containerSlotId)
{
var container = pmcData?.Inventory?.Items?.FirstOrDefault(item => item.SlotId == containerSlotId);
if (container is null)
{
return;
}
var parentItemLookup = pmcData.Inventory.Items.ToLookup(item => item.ParentId);
var parentIdsToSearch = new Queue();
parentIdsToSearch.Enqueue(container.Id);
while (parentIdsToSearch.Count > 0)
{
var currentParentId = parentIdsToSearch.Dequeue();
foreach (var childItem in parentItemLookup[currentParentId])
{
if (childItem.Upd?.SpawnedInSession != null && childItem.Upd.SpawnedInSession.Value)
{
childItem.Upd.SpawnedInSession = false;
}
parentIdsToSearch.Enqueue(childItem.Id);
}
}
}
///
/// Add a blank Upd object to an item
///
///
/// True = Upd added
public static bool AddUpd(this Item item)
{
if (item.Upd is not null)
{
// Already exists, exit early
return false;
}
item.Upd = new Upd();
return true;
}
///
/// Ensure an item has an upd object with a stack count of 1
///
/// Item to check
public static void EnsureItemHasValidStackCount(this Item item)
{
if (item.Upd is null)
{
item.AddUpd();
item.Upd.StackObjectsCount = 1;
}
if (item.Upd.StackObjectsCount is null or 0)
{
// Items pulled out of raid can have no stack count, default to 1
item.Upd.StackObjectsCount = 1;
}
}
}