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) { // Use dictionary to make key lookup faster, convert to list before being returned var itemList = items.ToList(); OrderedDictionary result = []; // Find desired root item var desiredRootItem = itemList.FirstOrDefault(item => item.Id == baseItemId); if (desiredRootItem is null) { // Root not found, nothing to return, exit return []; } result.Add(desiredRootItem.Id, desiredRootItem); var rootItemIdString = desiredRootItem.Id.ToString(); foreach (var item in itemList) { if (result.ContainsKey(item.Id)) { // Already processed, skip continue; } // Skip items with different parentId if (item.ParentId != rootItemIdString) { continue; } // Is stored in parent and disallowed if (excludeStoredItems && item.Location is not null) { continue; } // Item may have children, check foreach (var subItem in GetItemWithChildren(itemList, item.Id)) { result.Add(subItem.Id, subItem); } } return result.Values.ToList(); } /// /// Convert an Item to SptLootItem /// /// Item to convert /// Converted SptLootItem public static SptLootItem ToLootItem(this Item item) { return 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, ExtensionData = item.ExtensionData, }; } 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 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) { // Group by parentId + turn value into mongoId as we've filtered out non-mongoId values var byParentId = inventoryItems .Where(item => !string.IsNullOrEmpty(item.ParentId) && item.ParentId != "hideout") .GroupBy(item => new MongoId(item.ParentId)) .ToDictionary(kvp => kvp.Key, group => group.ToHashSet()); return new InventoryItemHash { ByItemId = inventoryItems.ToDictionary(item => item.Id), ByParentId = byParentId }; } } }