using System.Text.Json.Serialization; using SptCommon.Annotations; using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; using Core.Models.Eft.Inventory; using Core.Models.Eft.ItemEvent; using Core.Models.Spt.Config; using Core.Models.Spt.Inventory; using Core.Models.Utils; using Core.Services; using Core.Models.Eft.Player; using System.ComponentModel; using Core.Models.Eft.Hideout; using Core.Models.Enums; using Core.Models.Spt.Bots; using Core.Models.Spt.Services; namespace Core.Helpers; [Injectable] public class InventoryHelper( ISptLogger _logger, ProfileHelper _profileHelper, DialogueHelper _dialogueHelper, ItemHelper _itemHelper, LocalisationService _localisationService ) { /// /// Add multiple items to player stash (assuming they all fit) /// /// Session id /// AddItemsDirectRequest request /// Player profile /// Client response object public void AddItemsToStash( string sessionId, AddItemsDirectRequest request, PmcData pmcData, ItemEventRouterResponse output) { throw new NotImplementedException(); } /// /// Add whatever is passed in request.itemWithModsToAdd into player inventory (if it fits) /// /// Session id /// AddItemDirect request /// Player profile /// Client response object public void AddItemToStash( string sessionId, AddItemDirectRequest request, PmcData pmcData, ItemEventRouterResponse output) { throw new NotImplementedException(); } /// /// Set FiR status for an item + its children /// /// An item /// Item was found in raid protected void SetFindInRaidStatusForItem(List itemWithChildren, bool foundInRaid) { throw new NotImplementedException(); } /// /// Remove properties from a Upd object used by a trader/ragfair that are unnecessary to a player /// /// Object to update protected void RemoveTraderRagfairRelatedUpdProperties(Upd upd) { throw new NotImplementedException(); } /// /// Can all provided items be added into player inventory /// /// Player id /// Array of items with children to try and fit /// True all items fit public bool CanPlaceItemsInInventory(string sessionId, List> itemsWithChildren) { throw new NotImplementedException(); } /// /// Do the provided items all fit into the grid /// /// Container grid to fit items into /// Items to try and fit into grid /// True all fit public bool CanPlaceItemsInContainer(List>? containerFS2D, List> itemsWithChildren) { throw new NotImplementedException(); } /// /// Does an item fit into a container grid /// /// Container grid /// Item to check fits /// True it fits public bool CanPlaceItemInContainer(List>? containerFS2D, List itemWithChildren) { throw new NotImplementedException(); } /// /// Find a free location inside a container to fit the item /// /// Container grid to add item to /// Item to add to grid /// Id of the container we're fitting item into /// Slot id value to use, default is "hideout" public void PlaceItemInContainer( List> containerFS2D, List itemWithChildren, string containerId, string desiredSlotId = "hideout") { throw new NotImplementedException(); } /// /// Find a location to place an item into inventory and place it /// /// 2-dimensional representation of the container slots /// 2-dimensional representation of the sorting table slots /// Item to place with children /// Players inventory /// Should sorting table to be used if main stash has no space /// Output to send back to client protected void PlaceItemInInventory( List> stashFS2D, List> sortingTableFS2D, List itemWithChildren, BotBaseInventory playerInventory, bool useSortingTable, ItemEventRouterResponse output) { throw new NotImplementedException(); } /// /// Handle Remove event /// Remove item from player inventory + insured items array /// Also deletes child items /// /// Profile to remove item from (pmc or scav) /// Items id to remove /// Session id /// OPTIONAL - ItemEventRouterResponse public void RemoveItem(PmcData profile, string itemId, string sessionId, ItemEventRouterResponse output = null) { throw new NotImplementedException(); } /// /// Delete desired item from a player profiles mail /// /// Session id /// Remove request /// OPTIONAL - ItemEventRouterResponse public void RemoveItemAndChildrenFromMailRewards(string sessionId, InventoryRemoveRequestData removeRequest, ItemEventRouterResponse output = null) { throw new NotImplementedException(); } /// /// Find item by id in player inventory and remove x of its count /// /// player profile /// Item id to decrement StackObjectsCount of /// Number of item to remove /// Session id /// ItemEventRouterResponse /// ItemEventRouterResponse public ItemEventRouterResponse RemoveItemByCount(PmcData pmcData, string itemId, int countToRemove, string sessionId, ItemEventRouterResponse output = null) { throw new NotImplementedException(); } /// /// Get the height and width of an item - can have children that alter size /// /// Item to get size of /// Items id to get size of /// /// [width, height] public List GetItemSize(string? itemTpl, string itemId, List inventoryItems) { // -> Prepares item Width and height returns [sizeX, sizeY] return GetSizeByInventoryItemHash(itemTpl, itemId, GetInventoryItemHash(inventoryItems)); } /// /// Calculates the size of an item including attachments /// takes into account if item is folded /// /// Items template id /// Items id /// Hashmap of inventory items /// An array representing the [width, height] of the item protected List GetSizeByInventoryItemHash(string itemTpl, string itemID, InventoryItemHash inventoryItemHash) { var toDo = new List { itemID }; var result = _itemHelper.GetItem(itemTpl); var tmpItem = result.Value; // Invalid item if (!result.Key) { _logger.Error(_localisationService.GetText("inventory-invalid_item_missing_from_db", itemTpl)); } // Item found but no _props property if (tmpItem is not null && tmpItem.Properties is null) { _localisationService.GetText("inventory-item_missing_props_property", new { itemTpl = itemTpl, itemName = tmpItem?.Name }); } // No item object or getItem() returned false if (tmpItem is null && result.Value is null) { // return default size of 1x1 _logger.Error(_localisationService.GetText("inventory-return_default_size", itemTpl)); return [1, 1]; // Invalid input data, return defaults } var rootItem = inventoryItemHash.ByItemId[itemID]; var foldableWeapon = tmpItem.Properties.Foldable; var foldedSlot = tmpItem.Properties.FoldedSlot; var sizeUp = 0; var sizeDown = 0; var sizeLeft = 0; var sizeRight = 0; var forcedUp = 0; var forcedDown = 0; var forcedLeft = 0; var forcedRight = 0; var outX = (int)tmpItem.Properties.Width; var outY = (int)tmpItem.Properties.Height; // Item types to ignore var skipThisItems = new List { BaseClasses.BACKPACK, BaseClasses.SEARCHABLE_ITEM, BaseClasses.SIMPLE_CONTAINER }; var rootFolded = rootItem?.Upd?.Foldable?.Folded == true; // The item itself is collapsible if (foldableWeapon is not null && string.IsNullOrEmpty(foldedSlot) && rootFolded) { outX -= tmpItem.Properties.SizeReduceRight.Value; } // Calculate size contribution from child items/attachments if (!skipThisItems.Contains(tmpItem.Parent)) { while (toDo.Count > 0) { if (inventoryItemHash.ByParentId.ContainsKey(toDo[0])) { foreach (var item in inventoryItemHash.ByParentId[toDo[0]]) { // Filtering child items outside of mod slots, such as those inside containers, without counting their ExtraSize attribute if (item.SlotId.IndexOf("mod_") < 0) { continue; } toDo.Add(item.Id); // If the barrel is folded the space in the barrel is not counted var itemResult = _itemHelper.GetItem(item.Template); if (!itemResult.Key) { _logger.Error( _localisationService.GetText("inventory-get_item_size_item_not_found_by_tpl", item.Template)); } var itm = itemResult.Value; var childFoldable = itm.Properties.Foldable.GetValueOrDefault(false); var childFolded = item.Upd?.Foldable is not null && item.Upd.Foldable.Folded == true; if (foldableWeapon is true && foldedSlot == item.SlotId && (rootFolded || childFolded)) { continue; } if (childFoldable && rootFolded && childFolded) { continue; } // Calculating child ExtraSize if (itm.Properties.ExtraSizeForceAdd == true) { forcedUp += itm.Properties.ExtraSizeUp.Value; forcedDown += itm.Properties.ExtraSizeDown.Value; forcedLeft += itm.Properties.ExtraSizeLeft.Value; forcedRight += itm.Properties.ExtraSizeRight.Value; } else { sizeUp = sizeUp < itm.Properties.ExtraSizeUp ? itm.Properties.ExtraSizeUp.Value : sizeUp; sizeDown = sizeDown < itm.Properties.ExtraSizeDown ? itm.Properties.ExtraSizeDown.Value : sizeDown; sizeLeft = sizeLeft < itm.Properties.ExtraSizeLeft ? itm.Properties.ExtraSizeLeft.Value : sizeLeft; sizeRight = sizeRight < itm.Properties.ExtraSizeRight ? itm.Properties.ExtraSizeRight.Value : sizeRight; } } } toDo.RemoveAt(0); } } return [ outX + sizeLeft + sizeRight + forcedLeft + forcedRight, outY + sizeUp + sizeDown + forcedUp + forcedDown, ]; } /// /// Get a blank two-dimensional representation of a container /// /// Horizontal size of container /// Vertical size of container /// Two-dimensional representation of container protected int[][] GetBlankContainerMap(int containerH, int containerY) { return Enumerable.Repeat(Enumerable.Repeat(0, containerH).ToArray(), containerY).ToArray(); } /// /// Get a 2d mapping of a container with what grid slots are filled /// /// Horizontal size of container /// Vertical size of container /// Players inventory items /// Id of the container /// Two-dimensional representation of container public int[][] GetContainerMap(int containerH, int containerV, List itemList, string containerId) { // Create blank 2d map of container var container2D = GetBlankContainerMap(containerH, containerV); // Get all items in players inventory keyed by their parentId and by ItemId var inventoryItemHash = GetInventoryItemHash(itemList); // Get subset of items that belong to the desired container if (!inventoryItemHash.ByParentId.TryGetValue(containerId, out List containerItemHash)) { // No items in container, exit early return container2D; } // Check each item in container foreach (var item in containerItemHash) { var itemLocation = item?.Location as ItemLocation; if (itemLocation is null) { // item has no location property _logger.Error("Unable to find 'location' property on item with id: ${ item._id}, skipping"); continue; } // Get x/y size of item var tmpSize = GetSizeByInventoryItemHash(item.Template, item.Id, inventoryItemHash); var iW = tmpSize[0]; // x var iH = tmpSize[1]; // y var fH = IsVertical(itemLocation) ? iW : iH; var fW = IsVertical(itemLocation) ? iH : iW; // Find the ending x coord of container var fillTo = itemLocation.X + fW; for (var y = 0; y < fH; y++) { try { var rowIndex = itemLocation.Y + y; var containerRow = container2D[rowIndex.Value]; if (containerRow is null) { _logger.Error("Unable to find container: { containerId} row line: { itemLocation.y + y}"); } // Fill the corresponding cells in the container map to show the slot is taken Array.Fill(containerRow, 1, itemLocation.X.Value, fillTo.Value); } catch (Exception ex) { _logger.Error( _localisationService.GetText("inventory-unable_to_fill_container", new { id = item.Id, error = ex.Message }) ); } } } return container2D; } protected bool IsVertical(ItemLocation itemLocation) { throw new NotImplementedException(); } protected InventoryItemHash GetInventoryItemHash(List inventoryItems) { var inventoryItemHash = new InventoryItemHash { ByItemId = new(), ByParentId = new() }; foreach (var item in inventoryItems) { inventoryItemHash.ByItemId.TryAdd(item.Id, item); if (item.ParentId is null) { continue; } if (!inventoryItemHash.ByParentId.ContainsKey(item.ParentId)) { inventoryItemHash.ByParentId[item.ParentId] = []; } inventoryItemHash.ByParentId[item.ParentId].Add(item); } return inventoryItemHash; } /// /// Return the inventory that needs to be modified (scav/pmc etc) /// Changes made to result apply to character inventory /// Based on the item action, determine whose inventories we should be looking at for from and to. /// /// Item interaction request /// Item being moved/split/etc to inventory /// Session id / players Id /// OwnerInventoryItems with inventory of player/scav to adjust public OwnerInventoryItems GetOwnerInventoryItems( InventoryBaseActionRequestData request, string? item, string sessionId) { var pmcItems = _profileHelper.GetPmcProfile(sessionId).Inventory.Items; var scavProfile = _profileHelper.GetScavProfile(sessionId); var fromInventoryItems = pmcItems; var fromType = "pmc"; if (request.FromOwner is not null) { if (request.FromOwner.Id == scavProfile.Id) { fromInventoryItems = scavProfile.Inventory.Items; fromType = "scav"; } else if (request.FromOwner.Type.ToLower() == "mail") { // Split requests don't use 'use' but 'splitItem' property fromInventoryItems = _dialogueHelper.GetMessageItemContents(request.FromOwner.Id, sessionId, item); fromType = "mail"; } } // Don't need to worry about mail for destination because client doesn't allow // users to move items back into the mail stash. var toInventoryItems = pmcItems; var toType = "pmc"; // Destination is scav inventory, update values if (request.ToOwner?.Id == scavProfile.Id) { toInventoryItems = scavProfile.Inventory.Items; toType = "scav"; } // From and To types match, same inventory var movingToSameInventory = fromType == toType; return new OwnerInventoryItems { From = fromInventoryItems, To = toInventoryItems, SameInventory = movingToSameInventory, IsMail = fromType == "mail", }; } /// /// Get a two-dimensional array to represent stash slots /// 0 value = free, 1 = taken /// /// Player profile /// session id /// 2-dimensional array protected int[,] GetStashSlotMap(PmcData pmcData, string sessionID) { throw new NotImplementedException(); } /// /// Get a blank two-dimensional array representation of a container /// /// Container to get data for /// blank two-dimensional array public List> GetContainerSlotMap(string containerTpl) { throw new NotImplementedException(); } /// /// Get a two-dimensional array representation of the players sorting table /// /// Player profile /// two-dimensional array protected List> GetSortingTableSlotMap(PmcData pmcData) { throw new NotImplementedException(); } /// /// Get Players Stash Size /// /// Players id /// Dictionary of 2 values, horizontal and vertical stash size protected Dictionary GetPlayerStashSize(string sessionID) { throw new NotImplementedException(); } /// /// Get the players stash items tpl /// /// Player id /// Stash tpl protected string GetStashType(string sessionID) { throw new NotImplementedException(); } /// /// Internal helper function to transfer an item + children from one profile to another. /// /// Inventory of the source (can be non-player) /// Inventory of the destination /// Move request public void MoveItemToProfile(List sourceItems, List toItems, InventoryMoveRequestData request) { throw new NotImplementedException(); } /// /// Internal helper function to move item within the same profile. /// /// profile to edit /// /// client move request /// /// True if move was successful public bool MoveItemInternal( PmcData pmcData, List inventoryItems, InventoryMoveRequestData moveRequest, out string errorMessage) { errorMessage = string.Empty; HandleCartridges(inventoryItems, moveRequest); // Find item we want to 'move' var matchingInventoryItem = inventoryItems.FirstOrDefault((item) => item.Id == moveRequest.Item); if (matchingInventoryItem is null) { var noMatchingItemMesage = $"Unable to move item: {moveRequest.Item}, cannot find in inventory"; _logger.Error(noMatchingItemMesage); errorMessage = noMatchingItemMesage; return false; } _logger.Debug( $"{ moveRequest.Action} item: ${ moveRequest.Item} from slotid: { matchingInventoryItem.SlotId} to container: { moveRequest.To.Container}" ); // Don't move shells from camora to cartridges (happens when loading shells into mts-255 revolver shotgun) if (matchingInventoryItem.SlotId?.Contains("camora_") is null && moveRequest.To.Container == "cartridges") { _logger.Warning( _localisationService.GetText( "inventory-invalid_move_to_container", new { slotId = matchingInventoryItem.SlotId, container = moveRequest.To.Container, } ) ); return true; } // Edit items details to match its new location matchingInventoryItem.ParentId = moveRequest.To.Id; matchingInventoryItem.SlotId = moveRequest.To.Container; // Ensure fastpanel dict updates when item was moved out of fast-panel-accessible slot UpdateFastPanelBinding(pmcData, matchingInventoryItem); // Item has location property, ensure its value is handled if (moveRequest.To.Location is not null) { matchingInventoryItem.Location = moveRequest.To.Location; } else { // Moved from slot with location to one without, clean up if (matchingInventoryItem.Location is not null) { matchingInventoryItem.Location = null; } } return true; } /// /// Update fast panel bindings when an item is moved into a container that doesn't allow quick slot access /// /// Player profile /// item being moved protected void UpdateFastPanelBinding(PmcData pmcData, Item itemBeingMoved) { // Find matching _id in fast panel if (!pmcData.Inventory.FastPanel.TryGetValue(itemBeingMoved.Id, out var fastPanelSlot)) { return; } // Get moved items parent (should be container item was put into) var itemParent = pmcData.Inventory.Items.FirstOrDefault((item) => item.Id == itemBeingMoved.ParentId); if (itemParent is null) { return; } // Reset fast panel value if item was moved to a container other than pocket/rig (cant be used from fastpanel) List slots = ["pockets", "tacticalvest"]; var wasMovedToFastPanelAccessibleContainer = slots.Contains( itemParent?.SlotId?.ToLower() ?? "" ); if (!wasMovedToFastPanelAccessibleContainer) { pmcData.Inventory.FastPanel[fastPanelSlot[0].ToString()] = ""; } } /// /// Internal helper function to handle cartridges in inventory if any of them exist. /// protected void HandleCartridges(List items, InventoryMoveRequestData request) { throw new NotImplementedException(); } /// /// Get details for how a random loot container should be handled, max rewards, possible reward tpls /// /// Container being opened /// Reward details public RewardDetails GetRandomLootContainerRewardDetails(string itemTpl) { throw new NotImplementedException(); } /// /// Get inventory configuration /// /// Inventory configuration public InventoryConfig GetInventoryConfig() { throw new NotImplementedException(); } /// /// Recursively checks if the given item is /// inside the stash, that is it has the stash as /// ancestor with slotId=hideout /// /// Player profile /// Item to look for /// True if item exists inside stash public bool IsItemInStash(PmcData pmcData, Item itemToCheck) { throw new NotImplementedException(); } public void ValidateInventoryUsesMongoIds(List itemsToValidate) { throw new NotImplementedException(); } /// /// Does the provided item have a root item with the provided id /// /// Profile with items /// Item to check /// Root item id to check for /// True when item has rootId, false when not public bool DoesItemHaveRootId(PmcData pmcData, Item item, string rootId) { throw new NotImplementedException(); } } public class InventoryItemHash { [JsonPropertyName("byItemId")] public Dictionary ByItemId { get; set; } [JsonPropertyName("byParentId")] public Dictionary> ByParentId { get; set; } }