From d9ea3d4497800670ed654ecf95a59b537862e41b Mon Sep 17 00:00:00 2001 From: Chomp Date: Wed, 22 Jan 2025 12:36:34 +0000 Subject: [PATCH] Implemented InventoryHelper --- Libraries/Core/Helpers/InventoryHelper.cs | 412 +++++++++++++++++++--- 1 file changed, 355 insertions(+), 57 deletions(-) diff --git a/Libraries/Core/Helpers/InventoryHelper.cs b/Libraries/Core/Helpers/InventoryHelper.cs index 3066f8b6..77dab72e 100644 --- a/Libraries/Core/Helpers/InventoryHelper.cs +++ b/Libraries/Core/Helpers/InventoryHelper.cs @@ -3,16 +3,17 @@ using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; using Core.Models.Eft.Inventory; using Core.Models.Eft.ItemEvent; -using Core.Models.Eft.Profile; using Core.Models.Enums; using Core.Models.Spt.Config; using Core.Models.Spt.Inventory; using Core.Models.Utils; +using Core.Routers; using Core.Servers; using Core.Services; using Core.Utils; using Core.Utils.Cloners; using SptCommon.Annotations; +using Product = Core.Models.Eft.ItemEvent.Product; namespace Core.Helpers; @@ -27,12 +28,16 @@ public class InventoryHelper( DatabaseServer _databaseServer, PaymentHelper _paymentHelper, TraderAssortHelper _traderAssortHelper, + EventOutputHolder _eventOutputHolder, ProfileHelper _profileHelper, ItemHelper _itemHelper, LocalisationService _localisationService, + ConfigServer _configServer, ICloner _cloner ) { + protected InventoryConfig _inventoryConfig = _configServer.GetConfig(); + /// /// Add multiple items to player stash (assuming they all fit) /// @@ -53,24 +58,25 @@ public class InventoryHelper( _httpResponseUtil.AppendErrorToOutput( output, _localisationService.GetText("inventory-no_stash_space"), - BackendErrorCodes.NotEnoughSpace); + BackendErrorCodes.NotEnoughSpace + ); return; } - foreach (var itemToAdd in request.ItemsWithModsToAdd) { - var addItemRequest = new AddItemDirectRequest{ + foreach (var itemToAdd in request.ItemsWithModsToAdd) + { + var addItemRequest = new AddItemDirectRequest + { ItemWithModsToAdd = itemToAdd, FoundInRaid = request.FoundInRaid, UseSortingTable = request.UseSortingTable, - Callback = request.Callback }; + Callback = request.Callback + }; // Add to player inventory AddItemToStash(sessionId, addItemRequest, pmcData, output); - if (output.Warnings.Count > 0) - { - return; - } + if (output.Warnings.Count > 0) return; } } @@ -97,21 +103,21 @@ public class InventoryHelper( return; } + var sortingTableFS2D = GetSortingTableSlotMap(pmcData); - // Find empty slot in stash for item being added - adds 'location' + parentid + slotId properties to root item + // Find empty slot in stash for item being added - adds 'location' + parentId + slotId properties to root item PlaceItemInInventory( stashFS2D, sortingTableFS2D, itemWithModsToAddClone, pmcData.Inventory, request.UseSortingTable.GetValueOrDefault(false), - output); + output + ); if (output.Warnings.Count > 0) - { // Failed to place, error out return; - } // Apply/remove FiR to item + mods SetFindInRaidStatusForItem(itemWithModsToAddClone, request.FoundInRaid.GetValueOrDefault(false)); @@ -123,9 +129,7 @@ public class InventoryHelper( try { if (request.Callback is not null) - { request.Callback(itemWithModsToAddClone.FirstOrDefault().Upd.StackObjectsCount.Value); - } } catch (Exception ex) { @@ -138,10 +142,13 @@ public class InventoryHelper( } // Add item + mods to output and profile inventory - output.ProfileChanges[sessionId].Items.NewItems.AddRange(itemWithModsToAddClone.Select(x => x.ConvertToProduct())); + output.ProfileChanges[sessionId] + .Items.NewItems.AddRange(itemWithModsToAddClone.Select(x => x.ConvertToProduct())); pmcData.Inventory.Items.AddRange(itemWithModsToAddClone); - _logger.Debug($"Added ${ itemWithModsToAddClone[0].Upd?.StackObjectsCount ?? 1} item: ${itemWithModsToAddClone[0].Template } with: ${ itemWithModsToAddClone.Count - 1} mods to inventory"); + _logger.Debug( + $"Added ${itemWithModsToAddClone[0].Upd?.StackObjectsCount ?? 1} item: ${itemWithModsToAddClone[0].Template} with: ${itemWithModsToAddClone.Count - 1} mods to inventory" + ); } /// @@ -151,7 +158,13 @@ public class InventoryHelper( /// Item was found in raid protected void SetFindInRaidStatusForItem(List itemWithChildren, bool foundInRaid) { - throw new NotImplementedException(); + foreach (var item in itemWithChildren) + { + // Ensure item has upd object + _itemHelper.AddUpdObjectToItem(item); + + item.Upd.SpawnedInSession = foundInRaid; + } } /// @@ -160,7 +173,11 @@ public class InventoryHelper( /// Object to update protected void RemoveTraderRagfairRelatedUpdProperties(Upd upd) { - throw new NotImplementedException(); + if (upd.UnlimitedCount is not null) upd.UnlimitedCount = null; + + if (upd.BuyRestrictionCurrent is not null) upd.BuyRestrictionCurrent = null; + + if (upd.BuyRestrictionMax is not null) upd.BuyRestrictionMax = null; } /// @@ -171,7 +188,18 @@ public class InventoryHelper( /// True all items fit public bool CanPlaceItemsInInventory(string sessionId, List> itemsWithChildren) { - throw new NotImplementedException(); + var pmcData = _profileHelper.GetPmcProfile(sessionId); + + var stashFS2D = _cloner.Clone(GetStashSlotMap(pmcData, sessionId)); + if (stashFS2D is null) + { + _logger.Error($"Unable to get stash map for players: ${sessionId} stash"); + + return false; + } + + // False if ALL items don't fit + return itemsWithChildren.All(itemWithChildren => CanPlaceItemInContainer(stashFS2D, itemWithChildren)); } /// @@ -297,7 +325,99 @@ public class InventoryHelper( bool useSortingTable, ItemEventRouterResponse output) { - throw new NotImplementedException(); + // Get x/y size of item + var rootItem = itemWithChildren[0]; + var itemSize = GetItemSize(rootItem.Template, rootItem.Id, itemWithChildren); + + // Look for a place to slot item into + var findSlotResult = _containerHelper.FindSlotForItem(stashFS2D, itemSize[0], itemSize[1]); + if (findSlotResult.Success.Value) + { + try + { + _containerHelper.FillContainerMapWithItem( + stashFS2D, + findSlotResult.X.Value, + findSlotResult.Y.Value, + itemSize[0], + itemSize[1], + findSlotResult.Rotation.Value + ); + } + catch (Exception ex) + { + handleContainerPlacementError(ex.Message, output); + + return; + } + + // Store details for object, incuding container item will be placed in + rootItem.ParentId = playerInventory.Stash; + rootItem.SlotId = "hideout"; + rootItem.Location = new ItemLocation + { + X = findSlotResult.X, + Y = findSlotResult.Y, + R = findSlotResult.Rotation.Value ? 1 : 0, + Rotation = findSlotResult.Rotation + }; + + // Success! exit + return; + } + + // Space not found in main stash, use sorting table + if (useSortingTable) + { + var findSortingSlotResult = _containerHelper.FindSlotForItem( + sortingTableFS2D, + itemSize[0], + itemSize[1] + ); + + try + { + _containerHelper.FillContainerMapWithItem( + sortingTableFS2D, + findSortingSlotResult.X.Value, + findSortingSlotResult.Y.Value, + itemSize[0], + itemSize[1], + findSortingSlotResult.Rotation.Value + ); + } + catch (Exception ex) + { + handleContainerPlacementError(ex.Message, output); + + return; + } + + // Store details for object, incuding container item will be placed in + itemWithChildren[0].ParentId = playerInventory.SortingTable; + itemWithChildren[0].Location = new ItemLocation + { + X = findSortingSlotResult.X, + Y = findSortingSlotResult.Y, + R = findSortingSlotResult.Rotation.Value ? 1 : 0, + Rotation = findSortingSlotResult.Rotation + }; + } + else + { + _httpResponseUtil.AppendErrorToOutput( + output, + _localisationService.GetText("inventory-no_stash_space"), + BackendErrorCodes.NotEnoughSpace + ); + } + } + + private void handleContainerPlacementError(string errorText, ItemEventRouterResponse output) + { + _logger.Error(_localisationService.GetText("inventory-fill_container_failed", errorText)); + + _httpResponseUtil.AppendErrorToOutput(output, _localisationService.GetText("inventory-no_stash_space")); } /// @@ -309,9 +429,61 @@ public class InventoryHelper( /// Items id to remove /// Session id /// OPTIONAL - ItemEventRouterResponse - public void RemoveItem(PmcData profile, string itemId, string sessionId, ItemEventRouterResponse output = null) + public void RemoveItem(PmcData profile, string? itemId, string sessionId, ItemEventRouterResponse? output) { - throw new NotImplementedException(); + if (itemId is null) + { + _logger.Warning(_localisationService.GetText("inventory-unable_to_remove_item_no_id_given")); + + return; + } + + // Get children of item, they get deleted too + var itemAndChildrenToRemove = _itemHelper.FindAndReturnChildrenAsItems(profile.Inventory.Items, itemId); + if (itemAndChildrenToRemove.Count == 0) + { + _logger.Debug( + _localisationService.GetText( + "inventory-unable_to_remove_item_id_not_found", + new + { + ChildId = itemId, + ProfileId = profile.Id + } + ) + ); + + return; + } + + var inventoryItems = profile.Inventory.Items; + var insuredItems = profile.InsuredItems; + + // We have output object, inform client of root item deletion, not children + if (output is not null) output.ProfileChanges[sessionId].Items.DeletedItems.Add(new Product { Id = itemId }); + + foreach (var item in itemAndChildrenToRemove) + { + // We expect that each inventory item and each insured item has unique "_id", respective "itemId". + // Therefore, we want to use a NON-Greedy function and escape the iteration as soon as we find requested item. + var inventoryIndex = inventoryItems.FindIndex(inventoryItem => inventoryItem.Id == item.Id); + if (inventoryIndex != -1) + inventoryItems.RemoveAt(inventoryIndex); + else + _logger.Warning( + _localisationService.GetText( + "inventory-unable_to_remove_item_id_not_found", + new + { + childId = item.Id, + ProfileId = profile.Id + } + ) + ); + + var insuredItemIndex = insuredItems.FindIndex(insuredItem => insuredItem.ItemId == item.Id); + if (insuredItemIndex != -1) insuredItems.RemoveAt(insuredItemIndex); + } } /// @@ -321,9 +493,52 @@ public class InventoryHelper( /// Remove request /// OPTIONAL - ItemEventRouterResponse public void RemoveItemAndChildrenFromMailRewards(string sessionId, InventoryRemoveRequestData removeRequest, - ItemEventRouterResponse output = null) + ItemEventRouterResponse? output) { - throw new NotImplementedException(); + var fullProfile = _profileHelper.GetFullProfile(sessionId); + + // Iterate over all dialogs and look for mesasage with key from request, that has item (and maybe its children) we want to remove + var dialogs = fullProfile.DialogueRecords; + foreach (var (_, dialog) in dialogs) + { + var messageWithReward = + dialog.Messages.FirstOrDefault(message => message.Id == removeRequest.FromOwner.Id); + if (messageWithReward is not null) + { + // Find item + any possible children and remove them from mails items array + var itemWithChildern = _itemHelper.FindAndReturnChildrenAsItems( + messageWithReward.Items.Data, + removeRequest.Item + ); + foreach (var itemToDelete in itemWithChildern) + { + // Get index of item to remove from reward array + remove it + var indexOfItemToRemove = messageWithReward.Items.Data.IndexOf(itemToDelete); + if (indexOfItemToRemove == -1) + { + _logger.Error( + _localisationService.GetText( + "inventory-unable_to_remove_item_restart_immediately", + new + { + item = removeRequest.Item, + mailId = removeRequest.FromOwner.Id + } + ) + ); + + continue; + } + + messageWithReward.Items.Data.RemoveAt(indexOfItemToRemove); + } + + // Flag message as having no rewards if all removed + var hasRewardItemsRemaining = messageWithReward?.Items.Data?.Count > 0; + messageWithReward.HasRewards = hasRewardItemsRemaining; + messageWithReward.RewardCollected = !hasRewardItemsRemaining; + } + } } /// @@ -335,10 +550,38 @@ public class InventoryHelper( /// Session id /// ItemEventRouterResponse /// ItemEventRouterResponse - public ItemEventRouterResponse RemoveItemByCount(PmcData pmcData, string itemId, int countToRemove, - string sessionId, ItemEventRouterResponse output = null) + public ItemEventRouterResponse RemoveItemByCount(PmcData pmcData, string? itemId, int countToRemove, + string sessionId, ItemEventRouterResponse? output) { - throw new NotImplementedException(); + if (itemId is null) return output; + + // Goal is to keep removing items until we can remove part of an items stack + var itemsToReduce = _itemHelper.FindAndReturnChildrenAsItems(pmcData.Inventory.Items, itemId); + var remainingCount = countToRemove; + foreach (var itemToReduce in itemsToReduce) + { + var itemStackSize = _itemHelper.GetItemStackSize(itemToReduce); + + // Remove whole stack + if (remainingCount >= itemStackSize) + { + remainingCount -= itemStackSize; + RemoveItem(pmcData, itemToReduce.Id, sessionId, output); + } + else + { + itemToReduce.Upd.StackObjectsCount -= remainingCount; + remainingCount = 0; + if (output is not null) + output.ProfileChanges[sessionId].Items.ChangedItems.Add(itemToReduce.ConvertToProduct()); + } + + if (remainingCount == 0) + // Desired count of item has been removed / we ran out of items to remove + break; + } + + return output ?? _eventOutputHolder.GetOutput(sessionId); } /// @@ -517,7 +760,7 @@ public class InventoryHelper( var inventoryItemHash = GetInventoryItemHash(itemList); // Get subset of items that belong to the desired container - if (!inventoryItemHash.ByParentId.TryGetValue(containerId, out List containerItemHash)) + if (!inventoryItemHash.ByParentId.TryGetValue(containerId, out var containerItemHash)) // No items in container, exit early return container2D; @@ -666,7 +909,13 @@ public class InventoryHelper( /// 2-dimensional array protected int[][] GetStashSlotMap(PmcData pmcData, string sessionID) { - throw new NotImplementedException(); + var playerStashSize = GetPlayerStashSize(sessionID); + return GetContainerMap( + playerStashSize[0], + playerStashSize[1], + pmcData.Inventory.Items, + pmcData.Inventory.Stash + ); } /// @@ -692,27 +941,55 @@ public class InventoryHelper( /// two-dimensional array protected int[][] GetSortingTableSlotMap(PmcData pmcData) { - throw new NotImplementedException(); + return GetContainerMap(10, 45, pmcData.Inventory.Items, pmcData.Inventory.SortingTable); } /// /// Get Players Stash Size /// - /// Players id + /// Players id /// Dictionary of 2 values, horizontal and vertical stash size - protected Dictionary GetPlayerStashSize(string sessionID) + protected Dictionary GetPlayerStashSize(string sessionId) { - throw new NotImplementedException(); + var profile = _profileHelper.GetPmcProfile(sessionId); + var stashRowBonus = profile.Bonuses.FirstOrDefault(bonus => bonus.Type == BonusType.StashRows); + + // this sets automatically a stash size from items.json (it's not added anywhere yet because we still use base stash) + var stashTPL = GetStashType(sessionId); + if (stashTPL is null) _logger.Error(_localisationService.GetText("inventory-missing_stash_size")); + + var stashItemResult = _itemHelper.GetItem(stashTPL); + if (!stashItemResult.Key) + { + _logger.Error(_localisationService.GetText("inventory-stash_not_found", stashTPL)); + + return new Dictionary(); + } + + var stashItemDetails = stashItemResult.Value; + var firstStashItemGrid = stashItemDetails.Properties.Grids[0]; + + var stashH = firstStashItemGrid.Props.CellsH != 0 ? firstStashItemGrid.Props.CellsH : 10; + var stashV = firstStashItemGrid.Props.CellsV != 0 ? firstStashItemGrid.Props.CellsV : 66; + + // Player has a bonus, apply to vertical size + if (stashRowBonus is not null) stashV += (int)stashRowBonus.Value; + + return new Dictionary { { stashH.Value, stashV.Value } }; } /// /// Get the players stash items tpl /// - /// Player id + /// Player id /// Stash tpl - protected string GetStashType(string sessionID) + protected string? GetStashType(string sessionId) { - throw new NotImplementedException(); + var pmcData = _profileHelper.GetPmcProfile(sessionId); + var stashObj = pmcData.Inventory.Items.FirstOrDefault(item => item.Id == pmcData.Inventory.Stash); + if (stashObj is null) _logger.Error(_localisationService.GetText("inventory-unable_to_find_stash")); + + return stashObj?.Template; } /// @@ -727,8 +1004,9 @@ public class InventoryHelper( // Get all children item has, they need to move with item var idsToMove = _itemHelper.FindAndReturnChildrenByItems(sourceItems, request.Item); - foreach (var itemId in idsToMove) { - var itemToMove = sourceItems.FirstOrDefault((item) => item.Id == itemId); + foreach (var itemId in idsToMove) + { + var itemToMove = sourceItems.FirstOrDefault(item => item.Id == itemId); if (itemToMove is null) { _logger.Error(_localisationService.GetText("inventory-unable_to_find_item_to_move", itemId)); @@ -742,19 +1020,11 @@ public class InventoryHelper( itemToMove.SlotId = request.To.Container; if (request.To.Location is not null) - { // Update location object itemToMove.Location = request.To.Location; - } else - { // No location in request, delete it - if (itemToMove.Location is null) - { - // biome-ignore lint/performance/noDelete: Delete is fine here as we're trying to remove the entire data property. - itemToMove.Location = null; - } - } + itemToMove.Location = null; } toItems.Add(itemToMove); @@ -861,10 +1131,7 @@ public class InventoryHelper( protected void HandleCartridges(List items, InventoryMoveRequestData request) { // Not moving item into a cartridge slot, skip - if (request.To.Container != "cartridges") - { - return; - } + if (request.To.Container != "cartridges") return; // Get a count of cartridges in existing magazine var cartridgeCount = items.Count(item => item.ParentId == request.To.Id); @@ -879,7 +1146,7 @@ public class InventoryHelper( /// Reward details public RewardDetails GetRandomLootContainerRewardDetails(string itemTpl) { - throw new NotImplementedException(); + return _inventoryConfig.RandomLootContainers[itemTpl]; } /// @@ -888,7 +1155,7 @@ public class InventoryHelper( /// Inventory configuration public InventoryConfig GetInventoryConfig() { - throw new NotImplementedException(); + return _inventoryConfig; } /// @@ -901,12 +1168,33 @@ public class InventoryHelper( /// True if item exists inside stash public bool IsItemInStash(PmcData pmcData, Item itemToCheck) { - throw new NotImplementedException(); + // Start recursive check + return IsParentInStash(itemToCheck.Id, pmcData); + } + + private bool IsParentInStash(string itemId, PmcData pmcData) + { + // Item not found / has no parent + var item = pmcData.Inventory.Items.FirstOrDefault(item => item.Id == itemId); + if (item?.ParentId is null) return false; + + // Root level. Items parent is the stash with slotId "hideout" + if (item.ParentId == pmcData.Inventory.Stash && item.SlotId == "hideout") return true; + + // Recursive case: Check the items parent + return IsParentInStash(item.ParentId, pmcData); } public void ValidateInventoryUsesMongoIds(List itemsToValidate) { - throw new NotImplementedException(); + var errors = itemsToValidate.Where(item => !_hashUtil.IsValidMongoId(item.Id)) + .Select(item => $"Id: {item.Id} - tpl: {item.Template}") + .ToList(); + foreach (var message in errors) _logger.Error(message); + + throw new Exception( + "This profile is not compatible with SPT, See above for a list of incompatible IDs that is not compatible. Loading of SPT has been halted, use another profile or create a new one" + ); } /// @@ -918,7 +1206,17 @@ public class InventoryHelper( /// True when item has rootId, false when not public bool DoesItemHaveRootId(PmcData pmcData, Item item, string rootId) { - throw new NotImplementedException(); + var currentItem = item; + while (currentItem is not null) + { + // If we've found the equipment root ID, return true + if (currentItem.Id == rootId) return true; + + // Otherwise get the parent item + currentItem = pmcData.Inventory.Items.FirstOrDefault(item => item.Id == currentItem.ParentId); + } + + return false; } }