From 3989fa5e5831c9e482fae2aa60827f57c7fa3151 Mon Sep 17 00:00:00 2001 From: Chomp Date: Tue, 21 Jan 2025 20:24:09 +0000 Subject: [PATCH] Added inventory controller implementation --- .../Core/Callbacks/InventoryCallbacks.cs | 97 +-- .../Core/Controllers/InventoryController.cs | 685 ++++++++++++++++-- .../Core/Controllers/LauncherV2Controller.cs | 3 +- Libraries/Core/Helpers/InventoryHelper.cs | 301 ++++---- .../Core/Models/Eft/Common/Tables/Item.cs | 14 + .../Models/Eft/Common/Tables/TemplateItem.cs | 3 +- .../InventoryReadEncyclopediaRequestData.cs | 4 +- .../Models/Eft/Inventory/SetFavoriteItems.cs | 4 +- .../Models/Spt/Dialog/SendMessageDetails.cs | 2 +- .../Core/Services/RagfairOfferService.cs | 2 +- 10 files changed, 873 insertions(+), 242 deletions(-) diff --git a/Libraries/Core/Callbacks/InventoryCallbacks.cs b/Libraries/Core/Callbacks/InventoryCallbacks.cs index 6b70468f..cd5eb169 100644 --- a/Libraries/Core/Callbacks/InventoryCallbacks.cs +++ b/Libraries/Core/Callbacks/InventoryCallbacks.cs @@ -1,9 +1,10 @@ -using SptCommon.Annotations; using Core.Controllers; +using Core.Helpers; using Core.Models.Eft.Common; using Core.Models.Eft.Inventory; using Core.Models.Eft.ItemEvent; using Core.Models.Eft.Quests; +using SptCommon.Annotations; namespace Core.Callbacks; @@ -14,77 +15,80 @@ public class InventoryCallbacks( ) { /// - /// Handle client/game/profile/items/moving Move event + /// Handle client/game/profile/items/moving Move event /// /// /// /// /// /// - public ItemEventRouterResponse MoveItem(PmcData pmcData, InventoryMoveRequestData info, string sessionID, ItemEventRouterResponse output) + public ItemEventRouterResponse MoveItem(PmcData pmcData, InventoryMoveRequestData info, string sessionID, + ItemEventRouterResponse output) { _inventoryController.MoveItem(pmcData, info, sessionID, output); return output; } /// - /// Handle Remove event + /// Handle Remove event /// /// /// /// /// /// - public ItemEventRouterResponse RemoveItem(PmcData pmcData, InventoryRemoveRequestData info, string sessionID, ItemEventRouterResponse output) + public ItemEventRouterResponse RemoveItem(PmcData pmcData, InventoryRemoveRequestData info, string sessionID, + ItemEventRouterResponse output) { _inventoryController.RemoveItem(pmcData, info, sessionID, output); return output; } /// - /// Handle Split event + /// Handle Split event /// /// /// /// /// /// - public ItemEventRouterResponse SplitItem(PmcData pmcData, InventorySplitRequestData info, string sessionID, ItemEventRouterResponse output) + public ItemEventRouterResponse SplitItem(PmcData pmcData, InventorySplitRequestData info, string sessionID, + ItemEventRouterResponse output) { _inventoryController.SplitItem(pmcData, info, sessionID, output); return output; } /// - /// /// /// /// /// /// /// - public ItemEventRouterResponse MergeItem(PmcData pmcData, InventoryMergeRequestData info, string sessionID, ItemEventRouterResponse output) + public ItemEventRouterResponse MergeItem(PmcData pmcData, InventoryMergeRequestData info, string sessionID, + ItemEventRouterResponse output) { _inventoryController.MergeItem(pmcData, info, sessionID, output); return output; } /// - /// /// /// /// /// /// /// - public ItemEventRouterResponse TransferItem(PmcData pmcData, InventoryTransferRequestData info, string sessionID, ItemEventRouterResponse output) + public ItemEventRouterResponse TransferItem(PmcData pmcData, InventoryTransferRequestData info, string sessionID, + ItemEventRouterResponse output) { _inventoryController.TransferItem(pmcData, info, sessionID, output); return output; } /// - /// Handle Swap + /// Handle Swap /// /// /// @@ -96,7 +100,6 @@ public class InventoryCallbacks( } /// - /// /// /// /// @@ -108,7 +111,6 @@ public class InventoryCallbacks( } /// - /// /// /// /// @@ -120,136 +122,138 @@ public class InventoryCallbacks( } /// - /// /// /// - /// - /// + /// + /// /// - public ItemEventRouterResponse TagItem(PmcData pmcData, InventoryTagRequestData info, string sessionID) + public ItemEventRouterResponse TagItem(PmcData pmcData, InventoryTagRequestData request, string sessionId) { - return _inventoryController.TagItem(pmcData, info, sessionID); + return _inventoryController.TagItem(pmcData, request, sessionId); } /// - /// /// /// /// /// /// /// - public ItemEventRouterResponse BindItem(PmcData pmcData, InventoryBindRequestData info, string sessionID, ItemEventRouterResponse output) + public ItemEventRouterResponse BindItem(PmcData pmcData, InventoryBindRequestData info, string sessionID, + ItemEventRouterResponse output) { _inventoryController.BindItem(pmcData, info, sessionID, output); return output; } /// - /// /// /// /// /// /// /// - public ItemEventRouterResponse UnBindItem(PmcData pmcData, InventoryBindRequestData info, string sessionID, ItemEventRouterResponse output) + public ItemEventRouterResponse UnBindItem(PmcData pmcData, InventoryBindRequestData info, string sessionID, + ItemEventRouterResponse output) { _inventoryController.UnBindItem(pmcData, info, sessionID, output); return output; } /// - /// /// /// /// /// /// /// - public ItemEventRouterResponse ExamineItem(PmcData pmcData, InventoryExamineRequestData info, string sessionID, ItemEventRouterResponse output) + public ItemEventRouterResponse ExamineItem(PmcData pmcData, InventoryExamineRequestData info, string sessionID, + ItemEventRouterResponse output) { _inventoryController.ExamineItem(pmcData, info, sessionID, output); return output; } /// - /// Handle ReadEncyclopedia + /// Handle ReadEncyclopedia /// /// /// /// /// - public ItemEventRouterResponse ReadEncyclopedia(PmcData pmcData, InventoryReadEncyclopediaRequestData info, string sessionID) + public ItemEventRouterResponse ReadEncyclopedia(PmcData pmcData, InventoryReadEncyclopediaRequestData info, + string sessionID) { return _inventoryController.ReadEncyclopedia(pmcData, info, sessionID); } /// - /// Handle ApplyInventoryChanges + /// Handle ApplyInventoryChanges /// /// /// /// /// /// - public ItemEventRouterResponse SortInventory(PmcData pmcData, InventorySortRequestData info, string sessionID, ItemEventRouterResponse output) + public ItemEventRouterResponse SortInventory(PmcData pmcData, InventorySortRequestData info, string sessionID, + ItemEventRouterResponse output) { _inventoryController.SortInventory(pmcData, info, sessionID, output); return output; } /// - /// /// /// /// /// /// /// - public ItemEventRouterResponse CreateMapMarker(PmcData pmcData, InventoryCreateMarkerRequestData info, string sessionID, ItemEventRouterResponse output) + public ItemEventRouterResponse CreateMapMarker(PmcData pmcData, InventoryCreateMarkerRequestData info, + string sessionID, ItemEventRouterResponse output) { _inventoryController.CreateMapMarker(pmcData, info, sessionID, output); return output; } /// - /// /// /// /// /// /// /// - public ItemEventRouterResponse DeleteMapMarker(PmcData pmcData, InventoryDeleteMarkerRequestData info, string sessionID, ItemEventRouterResponse output) + public ItemEventRouterResponse DeleteMapMarker(PmcData pmcData, InventoryDeleteMarkerRequestData info, + string sessionID, ItemEventRouterResponse output) { _inventoryController.DeleteMapMarker(pmcData, info, sessionID, output); return output; } /// - /// /// /// /// /// /// /// - public ItemEventRouterResponse EditMapMarker(PmcData pmcData, InventoryEditMarkerRequestData info, string sessionID, ItemEventRouterResponse output) + public ItemEventRouterResponse EditMapMarker(PmcData pmcData, InventoryEditMarkerRequestData info, string sessionID, + ItemEventRouterResponse output) { _inventoryController.EditMapMarker(pmcData, info, sessionID, output); return output; } /// - /// Handle OpenRandomLootContainer + /// Handle OpenRandomLootContainer /// /// /// /// /// /// - public ItemEventRouterResponse OpenRandomLootContainer(PmcData pmcData, OpenRandomLootContainerRequestData info, string sessionID, + public ItemEventRouterResponse OpenRandomLootContainer(PmcData pmcData, OpenRandomLootContainerRequestData info, + string sessionID, ItemEventRouterResponse output) { _inventoryController.OpenRandomLootContainer(pmcData, info, sessionID, output); @@ -257,57 +261,58 @@ public class InventoryCallbacks( } /// - /// /// /// /// /// /// /// - public ItemEventRouterResponse RedeemProfileReward(PmcData pmcData, RedeemProfileRequestData info, string sessionID, ItemEventRouterResponse output) + public ItemEventRouterResponse RedeemProfileReward(PmcData pmcData, RedeemProfileRequestData info, string sessionID, + ItemEventRouterResponse output) { _inventoryController.RedeemProfileReward(pmcData, info, sessionID); return output; } /// - /// /// /// /// /// /// /// - public ItemEventRouterResponse SetFavoriteItem(PmcData pmcData, SetFavoriteItems info, string sessionID, ItemEventRouterResponse output) + public ItemEventRouterResponse SetFavoriteItem(PmcData pmcData, SetFavoriteItems info, string sessionID, + ItemEventRouterResponse output) { _inventoryController.SetFavoriteItem(pmcData, info, sessionID); return output; } /// - /// TODO: MOVE INTO QUEST CODE - /// Handle game/profile/items/moving - QuestFail + /// TODO: MOVE INTO QUEST CODE + /// Handle game/profile/items/moving - QuestFail /// /// /// /// /// /// - public ItemEventRouterResponse FailQuest(PmcData pmcData, FailQuestRequestData info, string sessionID, ItemEventRouterResponse output) + public ItemEventRouterResponse FailQuest(PmcData pmcData, FailQuestRequestData info, string sessionID, + ItemEventRouterResponse output) { _questController.FailQuest(pmcData, info, sessionID, output); return output; } /// - /// /// /// /// /// /// /// - public ItemEventRouterResponse PinOrLock(PmcData pmcData, PinOrLockItemRequest info, string sessionID, ItemEventRouterResponse output) + public ItemEventRouterResponse PinOrLock(PmcData pmcData, PinOrLockItemRequest info, string sessionID, + ItemEventRouterResponse output) { _inventoryController.PinOrLock(pmcData, info, sessionID, output); return output; diff --git a/Libraries/Core/Controllers/InventoryController.cs b/Libraries/Core/Controllers/InventoryController.cs index 9fa52cbe..62808b17 100644 --- a/Libraries/Core/Controllers/InventoryController.cs +++ b/Libraries/Core/Controllers/InventoryController.cs @@ -2,15 +2,18 @@ using SptCommon.Annotations; using Core.Generators; using Core.Helpers; 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.Quests; using Core.Models.Enums; using Core.Models.Utils; using Core.Routers; using Core.Services; using Core.Utils; using Core.Utils.Cloners; +using Core.Models.Eft.Profile; +using Core.Models.Spt.Dialog; +using Product = Core.Models.Eft.ItemEvent.Product; namespace Core.Controllers; @@ -27,6 +30,7 @@ public class InventoryController( ProfileHelper _profileHelper, PaymentHelper _paymentHelper, TraderHelper _traderHelper, + ItemHelper _itemHelper, DatabaseService _databaseService, FenceService _fenceService, RagfairOfferService _ragfairOfferService, @@ -38,15 +42,13 @@ public class InventoryController( ICloner _cloner ) { - public void MoveItem(PmcData pmcData, InventoryMoveRequestData moveRequest, string sessionID, ItemEventRouterResponse output) + public void MoveItem(PmcData pmcData, InventoryMoveRequestData moveRequest, string sessionId, + ItemEventRouterResponse output) { - if (output.Warnings?.Count > 0) - { - return; - } + if (output.Warnings?.Count > 0) return; // Changes made to result apply to character inventory - var ownerInventoryItems = _inventoryHelper.GetOwnerInventoryItems(moveRequest, moveRequest.Item, sessionID); + var ownerInventoryItems = _inventoryHelper.GetOwnerInventoryItems(moveRequest, moveRequest.Item, sessionId); if (ownerInventoryItems.SameInventory.GetValueOrDefault(false)) { // Don't move items from trader to profile, this can happen when editing a traders preset weapons @@ -57,7 +59,7 @@ public class InventoryController( } // Check for item in inventory before allowing internal transfer - var originalItemLocation = ownerInventoryItems.From?.FirstOrDefault((item) => item.Id == moveRequest.Item); + var originalItemLocation = ownerInventoryItems.From?.FirstOrDefault(item => item.Id == moveRequest.Item); if (originalItemLocation is null) { // Internal item move but item never existed, possible dupe glitch @@ -67,22 +69,30 @@ public class InventoryController( var originalLocationSlotId = originalItemLocation.SlotId; - var moveResult = _inventoryHelper.MoveItemInternal(pmcData, ownerInventoryItems.From ?? [], moveRequest, out var errorMessage); + var moveResult = _inventoryHelper.MoveItemInternal( + pmcData, + ownerInventoryItems.From ?? [], + moveRequest, + out var errorMessage + ); if (!moveResult) { _httpResponseUtil.AppendErrorToOutput(output, errorMessage); return; } - // Item is moving into or out of place of fame dogtag slot - if (moveRequest.To?.Container != null && (moveRequest.To.Container.StartsWith("dogtag") || originalLocationSlotId!.StartsWith("dogtag"))) - { + // Item is moving into or out of place of fame dog tag slot + if (moveRequest.To?.Container != null && + (moveRequest.To.Container.StartsWith("dogtag") || originalLocationSlotId.StartsWith("dogtag"))) _hideoutHelper.ApplyPlaceOfFameDogtagBonus(pmcData); - } } else { - _inventoryHelper.MoveItemToProfile(ownerInventoryItems.From ?? [], ownerInventoryItems.To ?? [], moveRequest); + _inventoryHelper.MoveItemToProfile( + ownerInventoryItems.From ?? [], + ownerInventoryItems.To ?? [], + moveRequest + ); } } @@ -95,103 +105,670 @@ public class InventoryController( ); } - public void PinOrLock(PmcData pmcData, PinOrLockItemRequest info, string sessionId, ItemEventRouterResponse output) + public void PinOrLock(PmcData pmcData, PinOrLockItemRequest request, string sessionId, + ItemEventRouterResponse output) { - throw new NotImplementedException(); + var itemToAdjust = pmcData.Inventory!.Items!.FirstOrDefault(item => item.Id == request.Item); + if (itemToAdjust is null) + { + _logger.Error($"Unable find item: {request.Item} to: {request.State} on player {sessionId}to: "); + + return; + } + + // Nullguard + itemToAdjust.Upd ??= new Upd(); + + itemToAdjust.Upd.PinLockState = request.State; } - public void SetFavoriteItem(PmcData pmcData, SetFavoriteItems info, string sessionId) + public void SetFavoriteItem(PmcData pmcData, SetFavoriteItems request, string sessionId) { - throw new NotImplementedException(); + // The client sends the full list of favorite items, so clear the current favorites + pmcData.Inventory.FavoriteItems = []; + pmcData.Inventory.FavoriteItems.AddRange(request.Items); } - public void RedeemProfileReward(PmcData pmcData, RedeemProfileRequestData info, string sessionId) + public void RedeemProfileReward(PmcData pmcData, RedeemProfileRequestData request, string sessionId) { - throw new NotImplementedException(); + var fullProfile = _profileHelper.GetFullProfile(sessionId); + foreach (var rewardEvent in request.Events) + { + // Hard coded to `SYSTEM` for now + // TODO: make this dynamic + var dialog = fullProfile.DialogueRecords["59e7125688a45068a6249071"]; + var mail = dialog.Messages.FirstOrDefault(message => message.Id == rewardEvent.MessageId); + var mailEvent = + mail.ProfileChangeEvents.FirstOrDefault(changeEvent => changeEvent.Id == rewardEvent.EventId); + + switch (mailEvent.Type) + { + case ProfileChangeEventType.TraderSalesSum: + pmcData.TradersInfo[mailEvent.Entity].SalesSum = mailEvent.Value; + _traderHelper.LevelUp(mailEvent.Entity, pmcData); + _logger.Success($"Set trader {mailEvent.Entity}: Sales Sum to: {mailEvent.Value}"); + break; + case ProfileChangeEventType.TraderStanding: + pmcData.TradersInfo[mailEvent.Entity].Standing = mailEvent.Value; + _traderHelper.LevelUp(mailEvent.Entity, pmcData); + _logger.Success($"Set trader {mailEvent.Entity}: Standing to: {mailEvent.Value}"); + break; + case ProfileChangeEventType.ProfileLevel: + pmcData.Info.Experience = mailEvent.Value.Value; + // Will calculate level below + _traderHelper.ValidateTraderStandingsAndPlayerLevelForProfile(sessionId); + _logger.Success($"Set profile xp to: {mailEvent.Value}"); + break; + case ProfileChangeEventType.SkillPoints: + { + var profileSkill = pmcData.Skills.Common.FirstOrDefault(x => x.Id == mailEvent.Entity); + if (profileSkill is null) + { + _logger.Warning($"Unable to find skill with name: {mailEvent.Entity}"); + continue; + } + + profileSkill.Progress = mailEvent.Value; + _logger.Success($"Set profile skill: {mailEvent.Entity} to: {mailEvent.Value}"); + break; + } + case ProfileChangeEventType.ExamineAllItems: + { + var itemsToInspect = _itemHelper.GetItems().Where(x => x.Type != "Node"); + FlagItemsAsInspectedAndRewardXp(itemsToInspect.Select(x => x.Id), fullProfile); + _logger.Success($"Flagged {itemsToInspect.Count()} items as examined"); + + break; + } + case ProfileChangeEventType.UnlockTrader: + pmcData.TradersInfo[mailEvent.Entity].Unlocked = true; + _logger.Success($"Trader {mailEvent.Entity} Unlocked"); + + break; + case ProfileChangeEventType.AssortmentUnlockRule: + fullProfile.SptData.BlacklistedItemTemplates ??= []; + fullProfile.SptData.BlacklistedItemTemplates.Add(mailEvent.Entity); + _logger.Success($"Item {mailEvent.Entity} is now blacklisted"); + + break; + case ProfileChangeEventType.HideoutAreaLevel: + { + var areaName = mailEvent.Entity; + var newValue = mailEvent.Value; + var hideoutAreaType = Enum.Parse(areaName ?? "NOTSET"); + + var desiredArea = pmcData.Hideout.Areas.FirstOrDefault(area => area.Type == hideoutAreaType); + if (desiredArea is not null) desiredArea.Level = newValue; + + break; + } + default: + _logger.Warning($"Unhandled profile reward event: {mailEvent.Type}"); + + break; + } + } } - public void OpenRandomLootContainer(PmcData pmcData, OpenRandomLootContainerRequestData info, string sessionId, ItemEventRouterResponse output) + /** + * Flag an item as seen in profiles encyclopedia + add inspect xp to profile + * @param itemTpls Inspected item tpls + * @param fullProfile Profile to add xp to + */ + protected void FlagItemsAsInspectedAndRewardXp(IEnumerable itemTpls, SptProfile fullProfile) { - throw new NotImplementedException(); + foreach (var itemTpl in itemTpls) + { + var item = _itemHelper.GetItem(itemTpl); + if (!item.Key) + { + _logger.Warning( + _localisationService.GetText("inventory-unable_to_inspect_item_not_in_db", itemTpl) + ); + + return; + } + + fullProfile.CharacterData.PmcData.Info.Experience += item.Value.Properties.ExamineExperience; + fullProfile.CharacterData.PmcData.Encyclopedia[itemTpl] = false; + + fullProfile.CharacterData.ScavData.Info.Experience += item.Value.Properties.ExamineExperience; + fullProfile.CharacterData.ScavData.Encyclopedia[itemTpl] = false; + } + + // TODO: update this with correct calculation using values from globals json + _profileHelper.AddSkillPointsToPlayer( + fullProfile.CharacterData.PmcData, + SkillTypes.Intellect, + 0.05 * itemTpls.Count() + ); } - public void EditMapMarker(PmcData pmcData, InventoryEditMarkerRequestData info, string sessionId, ItemEventRouterResponse output) + public void OpenRandomLootContainer(PmcData pmcData, OpenRandomLootContainerRequestData request, string sessionId, + ItemEventRouterResponse output) { - throw new NotImplementedException(); + /** Container player opened in their inventory */ + var openedItem = pmcData.Inventory.Items.FirstOrDefault(item => item.Id == request.Item); + var containerDetailsDb = _itemHelper.GetItem(openedItem.Template); + var isSealedWeaponBox = containerDetailsDb.Value.Name.Contains("event_container_airdrop"); + + var foundInRaid = openedItem.Upd?.SpawnedInSession; + var rewards = new List>(); + var unlockedWeaponCrates = new List + { + "665829424de4820934746ce6", + "665732e7ac60f009f270d1ef", + "665888282c4a1b73af576b77" + }; + // Temp fix for unlocked weapon crate hideout craft + if (isSealedWeaponBox || unlockedWeaponCrates.Contains(containerDetailsDb.Value.Id)) + { + var containerSettings = _inventoryHelper.GetInventoryConfig().SealedAirdropContainer; + rewards.AddRange(_lootGenerator.GetSealedWeaponCaseLoot(containerSettings)); + + if (containerSettings.FoundInRaid) foundInRaid = containerSettings.FoundInRaid; + } + else + { + var rewardContainerDetails = _inventoryHelper.GetRandomLootContainerRewardDetails(openedItem.Template); + if (rewardContainerDetails?.RewardCount == null) + { + _logger.Error($"Unable to add loot to container: {openedItem.Template}, no rewards found"); + } + else + { + rewards.AddRange(_lootGenerator.GetRandomLootContainerLoot(rewardContainerDetails)); + + if (rewardContainerDetails.FoundInRaid) foundInRaid = rewardContainerDetails.FoundInRaid; + } + } + + // Add items to player inventory + if (rewards.Count > 0) + { + var addItemsRequest = new AddItemsDirectRequest + { + ItemsWithModsToAdd = rewards, + FoundInRaid = foundInRaid, + Callback = null, + UseSortingTable = true + }; + _inventoryHelper.AddItemsToStash(sessionId, addItemsRequest, pmcData, output); + if (output.Warnings.Count > 0) return; + } + + // Find and delete opened container item from player inventory + _inventoryHelper.RemoveItem(pmcData, request.Item, sessionId, output); } - public void DeleteMapMarker(PmcData pmcData, InventoryDeleteMarkerRequestData info, string sessionId, ItemEventRouterResponse output) + public void EditMapMarker(PmcData pmcData, InventoryEditMarkerRequestData request, string sessionId, + ItemEventRouterResponse output) { - throw new NotImplementedException(); + var mapItem = _mapMarkerService.EditMarkerOnMap(pmcData, request); + + // sync with client + output.ProfileChanges[sessionId].Items.ChangedItems.Add(mapItem.ConvertToProduct()); } - public void CreateMapMarker(PmcData pmcData, InventoryCreateMarkerRequestData info, string sessionId, ItemEventRouterResponse output) + public void DeleteMapMarker(PmcData pmcData, InventoryDeleteMarkerRequestData request, string sessionId, + ItemEventRouterResponse output) { - throw new NotImplementedException(); + var mapItem = _mapMarkerService.DeleteMarkerFromMap(pmcData, request); + + // sync with client + output.ProfileChanges[sessionId].Items.ChangedItems.Add(mapItem.ConvertToProduct()); } - public void SortInventory(PmcData pmcData, InventorySortRequestData info, string sessionId, ItemEventRouterResponse output) + public void CreateMapMarker(PmcData pmcData, InventoryCreateMarkerRequestData request, string sessionId, + ItemEventRouterResponse output) { - throw new NotImplementedException(); + var adjustedMapItem = _mapMarkerService.CreateMarkerOnMap(pmcData, request); + + // Sync with client + output.ProfileChanges[sessionId].Items.ChangedItems.Add(adjustedMapItem.ConvertToProduct()); } - public ItemEventRouterResponse ReadEncyclopedia(PmcData pmcData, InventoryReadEncyclopediaRequestData info, string sessionId) + public void SortInventory(PmcData pmcData, InventorySortRequestData request, string sessionId, + ItemEventRouterResponse output) { - throw new NotImplementedException(); + foreach (var change in request.ChangedItems) + { + var inventoryItem = pmcData.Inventory.Items.FirstOrDefault(item => item.Id == change.Id); + if (inventoryItem is null) + { + _logger.Error( + _localisationService.GetText("inventory-unable_to_sort_inventory_restart_game", change.Id) + ); + + continue; + } + + inventoryItem.ParentId = change.ParentId; + inventoryItem.SlotId = change.SlotId; + if (change.Location is not null) + inventoryItem.Location = change.Location; + else + inventoryItem.Location = null; + } } - public void ExamineItem(PmcData pmcData, InventoryExamineRequestData info, string sessionId, ItemEventRouterResponse output) + public ItemEventRouterResponse ReadEncyclopedia(PmcData pmcData, InventoryReadEncyclopediaRequestData body, + string sessionId) { - throw new NotImplementedException(); + foreach (var id in body.Ids) pmcData.Encyclopedia[id] = true; + + return _eventOutputHolder.GetOutput(sessionId); } - public void UnBindItem(PmcData pmcData, InventoryBindRequestData info, string sessionId, ItemEventRouterResponse output) + public void ExamineItem(PmcData pmcData, InventoryExamineRequestData request, string sessionId, + ItemEventRouterResponse output) { - throw new NotImplementedException(); + var itemId = ""; + if (request.FromOwner is not null) + { + try + { + itemId = GetExaminedItemTpl(request); + } + catch + { + _logger.Error(_localisationService.GetText("inventory-examine_item_does_not_exist", request.Item)); + } + + // get hideout item + if (request.FromOwner.Type == "HideoutProduction") itemId = request.Item; + } + + if (itemId is null) + { + // item template + if (_databaseService.GetItems().ContainsKey(request.Item)) itemId = request.Item; + + // Player inventory + var target = pmcData.Inventory.Items.FirstOrDefault(item => item.Id == request.Item); + if (target is not null) itemId = target.Template; + } + + if (itemId is not null) + { + var fullProfile = _profileHelper.GetFullProfile(sessionId); + FlagItemsAsInspectedAndRewardXp([itemId], fullProfile); + } } - public void BindItem(PmcData pmcData, InventoryBindRequestData info, string sessionId, ItemEventRouterResponse output) + protected string? GetExaminedItemTpl(InventoryExamineRequestData request) { - throw new NotImplementedException(); + if (_presetHelper.IsPreset(request.Item)) return _presetHelper.GetBaseItemTpl(request.Item); + + if (request.FromOwner.Id == Traders.FENCE) + // Get tpl from fence assorts + return _fenceService.GetRawFenceAssorts().Items.FirstOrDefault(x => x.Id == request.Item)?.Template; + + if (request.FromOwner.Type == "Trader") + // Not fence + // get tpl from trader assort + return _databaseService + .GetTrader(request.FromOwner.Id) + .Assort.Items.FirstOrDefault(item => item.Id == request.Item) + ?.Template; + + if (request.FromOwner.Type == "RagFair") + { + // Try to get tplId from items.json first + var item = _itemHelper.GetItem(request.Item); + if (item.Key) return item.Value.Id; + + // Try alternate way of getting offer if first approach fails + var offer = _ragfairOfferService.GetOfferByOfferId(request.Item) ?? + _ragfairOfferService.GetOfferByOfferId(request.FromOwner.Id); + + // Try find examine item inside offer items array + var matchingItem = offer.Items.FirstOrDefault(offerItem => offerItem.Id == request.Item); + if (matchingItem is not null) return matchingItem.Template; + + // Unable to find item in database or ragfair + _logger.Warning(_localisationService.GetText("inventory-unable_to_find_item", request.Item)); + } + + _logger.Error($"Unable to get item with id: {request.Item}"); + + return null; } - public ItemEventRouterResponse TagItem(PmcData pmcData, InventoryTagRequestData info, string sessionId) + public void UnBindItem(PmcData pmcData, InventoryBindRequestData request, string sessionId, + ItemEventRouterResponse output) { - throw new NotImplementedException(); + // Remove kvp from requested fast panel index + + // TODO - does this work + pmcData.Inventory.FastPanel.Remove(request.Index.ToString()); } - public ItemEventRouterResponse ToggleItem(PmcData pmcData, InventoryToggleRequestData info, string sessionId) + public void BindItem(PmcData pmcData, InventoryBindRequestData bindRequest, string sessionId, + ItemEventRouterResponse output) { - throw new NotImplementedException(); + foreach (var kvp in pmcData.Inventory.FastPanel + .Where(kvp => kvp.Value == bindRequest.Index.Value.ToString())) + { + pmcData.Inventory.FastPanel.Remove(kvp.Key); + + break; + } + + // Create link between fast panel slot and requested item + pmcData.Inventory.FastPanel[bindRequest.Index.ToString()] = bindRequest.Item; } - public ItemEventRouterResponse FoldItem(PmcData pmcData, InventoryFoldRequestData info, string sessionId) + public ItemEventRouterResponse TagItem(PmcData pmcData, InventoryTagRequestData request, string sessionId) { - throw new NotImplementedException(); + var itemToTag = pmcData.Inventory.Items.FirstOrDefault(item => item.Id == request.Item); + if (itemToTag is null) + { + _logger.Warning( + $"Unable to tag item: {request.Item} as it cannot be found in player {sessionId} inventory" + ); + + return new ItemEventRouterResponse { Warnings = [], ProfileChanges = { } }; + } + + // Null guard + itemToTag.Upd ??= new Upd(); + + itemToTag.Upd.Tag = new UpdTag { Color = request.TagColor, Name = request.TagName }; + + return _eventOutputHolder.GetOutput(sessionId); } - public ItemEventRouterResponse SwapItem(PmcData pmcData, InventorySwapRequestData info, string sessionId) + public ItemEventRouterResponse ToggleItem(PmcData pmcData, InventoryToggleRequestData request, string sessionId) { - throw new NotImplementedException(); + // May need to reassign to scav profile + var playerData = pmcData; + + // Fix for toggling items while on they're in the Scav inventory + if (request.FromOwner?.Type == "Profile" && request.FromOwner.Id != playerData.Id) + playerData = _profileHelper.GetScavProfile(sessionId); + + var itemToToggle = playerData.Inventory.Items.FirstOrDefault(x => x.Id == request.Item); + if (itemToToggle is not null) + { + _itemHelper.AddUpdObjectToItem( + itemToToggle, + _localisationService.GetText("inventory-item_to_toggle_missing_upd", itemToToggle.Id) + ); + + itemToToggle.Upd.Togglable = new UpdTogglable() { On = request.Value }; + + return _eventOutputHolder.GetOutput(sessionId); + } + + _logger.Warning(_localisationService.GetText("inventory-unable_to_toggle_item_not_found", request.Item)); + + return new ItemEventRouterResponse { Warnings = [], ProfileChanges = { } }; } - public void TransferItem(PmcData pmcData, InventoryTransferRequestData info, string sessionId, ItemEventRouterResponse output) + public ItemEventRouterResponse FoldItem(PmcData pmcData, InventoryFoldRequestData request, string sessionId) { - throw new NotImplementedException(); + // May need to reassign to scav profile + var playerData = pmcData; + + // We may be folding data on scav profile, get that profile instead + if (request.FromOwner?.Type == "Profile" && request.FromOwner.Id != playerData.Id) + playerData = _profileHelper.GetScavProfile(sessionId); + + var itemToFold = playerData.Inventory.Items.FirstOrDefault(item => item?.Id == request.Item); + if (itemToFold is null) + { + // Item not found + _logger.Warning( + _localisationService.GetText("inventory-unable_to_fold_item_not_found_in_inventory", request.Item) + ); + + return new ItemEventRouterResponse { Warnings = [], ProfileChanges = { } }; + } + + // Item may not have upd object + _itemHelper.AddUpdObjectToItem(itemToFold); + + itemToFold.Upd.Foldable = new UpdFoldable { Folded = request.Value }; + + return _eventOutputHolder.GetOutput(sessionId); } - public void MergeItem(PmcData pmcData, InventoryMergeRequestData info, string sessionId, ItemEventRouterResponse output) + public ItemEventRouterResponse SwapItem(PmcData pmcData, InventorySwapRequestData request, string sessionId) { - throw new NotImplementedException(); + // During post-raid scav transfer, the swap may be in the scav inventory + var playerData = pmcData; + if (request.FromOwner?.Type == "Profile" && request.FromOwner.Id != playerData.Id) + playerData = _profileHelper.GetScavProfile(sessionId); + + var itemOne = playerData.Inventory.Items.FirstOrDefault(x => x.Id == request.Item); + if (itemOne is null) + _logger.Error( + _localisationService.GetText( + "inventory-unable_to_find_item_to_swap", + new + { + item1Id = request.Item, + item2Id = request.Item2 + } + ) + ); + + var itemTwo = playerData.Inventory.Items.FirstOrDefault(x => x.Id == request.Item2); + if (itemTwo is null) + _logger.Error( + _localisationService.GetText( + "inventory-unable_to_find_item_to_swap", + new + { + item1Id = request.Item2, + item2Id = request.Item + } + ) + ); + + // to.id is the parentid + itemOne.ParentId = request.To.Id; + + // to.container is the slotid + itemOne.SlotId = request.To.Container; + + // Request object has location data, add it in, otherwise remove existing location from object + if (request.To.Location is not null) + itemOne.Location = request.To.Location; + else + // biome-ignore lint/performance/noDelete: Delete is fine here as we entirely want to get rid of the location. + itemOne.Location = null; + + itemTwo.ParentId = request.To2.Id; + itemTwo.SlotId = request.To2.Container; + if (request.To2.Location is not null) + itemTwo.Location = request.To2.Location; + else + itemTwo.Location = null; + + // Client already informed of inventory locations, nothing for us to do + return _eventOutputHolder.GetOutput(sessionId); } - public void SplitItem(PmcData pmcData, InventorySplitRequestData info, string sessionId, ItemEventRouterResponse output) + public void TransferItem(PmcData pmcData, InventoryTransferRequestData request, string sessionId, + ItemEventRouterResponse output) { - throw new NotImplementedException(); + // TODO - check GetOwnerInventoryItems() call still works + var inventoryItems = _inventoryHelper.GetOwnerInventoryItems(request, request.Item, sessionId); + var sourceItem = inventoryItems.From.FirstOrDefault(item => item.Id == request.Item); + var destinationItem = inventoryItems.To.FirstOrDefault(item => item.Id == request.With); + + if (sourceItem is null) + { + var errorMessage = $"Unable to transfer stack, cannot find source: {request.Item}"; + _logger.Error(errorMessage); + + _httpResponseUtil.AppendErrorToOutput(output, errorMessage); + + return; + } + + if (destinationItem is not null) + { + var errorMessage = $"Unable to transfer stack, cannot find destination: {request.With}"; + _logger.Error(errorMessage); + + _httpResponseUtil.AppendErrorToOutput(output, errorMessage); + + return; + } + + sourceItem.Upd ??= new Upd { StackObjectsCount = 1 }; + + var sourceStackCount = sourceItem.Upd.StackObjectsCount; + if (sourceStackCount > request.Count) + // Source items stack count greater than new desired count + sourceItem.Upd.StackObjectsCount = sourceStackCount - request.Count; + else + // Moving a full stack onto a smaller stack + sourceItem.Upd.StackObjectsCount = sourceStackCount - 1; + + destinationItem.Upd ??= new Upd { StackObjectsCount = 1 }; + + var destinationStackCount = destinationItem.Upd.StackObjectsCount; + destinationItem.Upd.StackObjectsCount = destinationStackCount + request.Count; } - public void RemoveItem(PmcData pmcData, InventoryRemoveRequestData info, string sessionId, ItemEventRouterResponse output) + public void MergeItem(PmcData pmcData, InventoryMergeRequestData body, string sessionID, + ItemEventRouterResponse output) { - throw new NotImplementedException(); + // Changes made to result apply to character inventory + var inventoryItems = _inventoryHelper.GetOwnerInventoryItems(body, body.Item, sessionID); + + // Get source item (can be from player or trader or mail) + var sourceItem = inventoryItems.From.FirstOrDefault((x) => x.Id == body.Item); + if (sourceItem is null) + { + var errorMessage = $"Unable to merge stacks as source item: {body.With} cannot be found"; + _logger.Error(errorMessage); + + _httpResponseUtil.AppendErrorToOutput(output, errorMessage); + + return; + } + + // Get item being merged into + var destinationItem = inventoryItems.To.FirstOrDefault((x) => x.Id == body.With); + if (destinationItem is null) + { + var errorMessage = $"Unable to merge stacks as destination item: {body.With} cannot be found"; + _logger.Error(errorMessage); + + _httpResponseUtil.AppendErrorToOutput(output, errorMessage); + + return; + } + + if (destinationItem.Upd?.StackObjectsCount is null) + // No stackcount on destination, add one + destinationItem.Upd = new Upd { StackObjectsCount = 1 }; + + if (sourceItem.Upd is null) + sourceItem.Upd = new Upd { StackObjectsCount = 1 }; + else if (sourceItem.Upd.StackObjectsCount is null) + // Items pulled out of raid can have no stack count if the stack should be 1 + sourceItem.Upd.StackObjectsCount = 1; + + // Remove FiR status from destination stack when source stack has no FiR but destination does + if (!sourceItem.Upd.SpawnedInSession.GetValueOrDefault(false) && + destinationItem.Upd.SpawnedInSession.GetValueOrDefault(false)) destinationItem.Upd.SpawnedInSession = false; + + destinationItem.Upd.StackObjectsCount += + sourceItem.Upd.StackObjectsCount; // Add source stackcount to destination + output.ProfileChanges[sessionID] + .Items.DeletedItems.Add(new Product { Id = sourceItem.Id }); // Inform client source item being deleted + + var indexOfItemToRemove = inventoryItems.From.FindIndex((x) => x.Id == sourceItem.Id); + if (indexOfItemToRemove == -1) + { + var errorMessage = $"Unable to find item: {sourceItem.Id} to remove from sender inventory"; + _logger.Error(errorMessage); + + _httpResponseUtil.AppendErrorToOutput(output, errorMessage); + + return; + } + + inventoryItems.From.RemoveAt(indexOfItemToRemove); // Remove source item from 'from' inventory + } + + public void SplitItem(PmcData pmcData, InventorySplitRequestData request, string sessionID, + ItemEventRouterResponse output) + { + // Changes made to result apply to character inventory + var inventoryItems = _inventoryHelper.GetOwnerInventoryItems(request, request.NewItem, sessionID); + + // Handle cartridge edge-case + if (request.Container.Location is null && request.Container.ContainerName == "cartridges") + { + var matchingItems = inventoryItems.To.Where((x) => x.ParentId == request.Container.Id); + request.Container.Location = matchingItems.Count(); // Wrong location for first cartridge + } + + // The item being merged has three possible sources: pmc, scav or mail, getOwnerInventoryItems() handles getting correct one + var itemToSplit = inventoryItems.From.FirstOrDefault((x) => x.Id == request.SplitItem); + if (itemToSplit is null) + { + var errorMessage = $"Unable to split stack as source item: {request.SplitItem} cannot be found"; + _logger.Error(errorMessage); + + _httpResponseUtil.AppendErrorToOutput(output, errorMessage); + + return; + } + + // Create new upd object that retains properties of original upd + new stack count size + var updatedUpd = _cloner.Clone(itemToSplit.Upd); + updatedUpd.StackObjectsCount = request.Count; + + // Remove split item count from source stack + itemToSplit.Upd.StackObjectsCount -= request.Count; + + // Inform client of change + output.ProfileChanges[sessionID] + .Items.NewItems.Add( + new Product + { + Id = request.NewItem, + Template = itemToSplit.Template, + Upd = updatedUpd + } + ); + + // Update player inventory + inventoryItems.To.Add( + new Item + { + Id = request.NewItem, + Template = itemToSplit.Template, + ParentId = request.Container.Id, + SlotId = request.Container.ContainerName, + Location = request.Container.Location, + Upd = updatedUpd + } + ); + } + + public void RemoveItem(PmcData pmcData, InventoryRemoveRequestData request, string sessionId, + ItemEventRouterResponse output) + { + if (request.FromOwner?.Type == "Mail") + { + _inventoryHelper.RemoveItemAndChildrenFromMailRewards(sessionId, request, output); + + return; + } + + var profileToRemoveItemFrom = request?.FromOwner.Id == pmcData.Id + ? pmcData + : _profileHelper.GetFullProfile(sessionId).CharacterData.ScavData; + + _inventoryHelper.RemoveItem(profileToRemoveItemFrom, request.Item, sessionId, output); } } diff --git a/Libraries/Core/Controllers/LauncherV2Controller.cs b/Libraries/Core/Controllers/LauncherV2Controller.cs index 7bd220bd..ae288ced 100644 --- a/Libraries/Core/Controllers/LauncherV2Controller.cs +++ b/Libraries/Core/Controllers/LauncherV2Controller.cs @@ -1,5 +1,4 @@ -using SptCommon.Annotations; -using Core.Helpers; +using SptCommon.Annotations; using Core.Models.Eft.Common.Tables; using Core.Models.Eft.Launcher; using Core.Models.Spt.Config; diff --git a/Libraries/Core/Helpers/InventoryHelper.cs b/Libraries/Core/Helpers/InventoryHelper.cs index f69c7c1f..671483dc 100644 --- a/Libraries/Core/Helpers/InventoryHelper.cs +++ b/Libraries/Core/Helpers/InventoryHelper.cs @@ -1,26 +1,30 @@ 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.Enums; using Core.Models.Spt.Config; using Core.Models.Spt.Inventory; using Core.Models.Utils; +using Core.Servers; 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; -using System.Xml.Linq; +using Core.Utils; +using SptCommon.Annotations; namespace Core.Helpers; [Injectable] public class InventoryHelper( ISptLogger _logger, + HashUtil hashUtil, + HttpResponseUtil httpResponseUtil, + FenceService fenceService, + DialogueHelper dialogueHelper, + ContainerHelper containerHelper, + DatabaseServer databaseServer, + PaymentHelper paymentHelper, + TraderAssortHelper traderAssortHelper, ProfileHelper _profileHelper, DialogueHelper _dialogueHelper, ContainerHelper _containerHelper, @@ -29,7 +33,7 @@ public class InventoryHelper( ) { /// - /// Add multiple items to player stash (assuming they all fit) + /// Add multiple items to player stash (assuming they all fit) /// /// Session id /// AddItemsDirectRequest request @@ -45,7 +49,7 @@ public class InventoryHelper( } /// - /// Add whatever is passed in request.itemWithModsToAdd into player inventory (if it fits) + /// Add whatever is passed in request.itemWithModsToAdd into player inventory (if it fits) /// /// Session id /// AddItemDirect request @@ -61,7 +65,7 @@ public class InventoryHelper( } /// - /// Set FiR status for an item + its children + /// Set FiR status for an item + its children /// /// An item /// Item was found in raid @@ -71,7 +75,7 @@ public class InventoryHelper( } /// - /// Remove properties from a Upd object used by a trader/ragfair that are unnecessary to a player + /// 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) @@ -80,7 +84,7 @@ public class InventoryHelper( } /// - /// Can all provided items be added into player inventory + /// Can all provided items be added into player inventory /// /// Player id /// Array of items with children to try and fit @@ -91,7 +95,7 @@ public class InventoryHelper( } /// - /// Do the provided items all fit into the grid + /// Do the provided items all fit into the grid /// /// Container grid to fit items into /// Items to try and fit into grid @@ -102,7 +106,7 @@ public class InventoryHelper( } /// - /// Does an item fit into a container grid + /// Does an item fit into a container grid /// /// Container grid /// Item to check fits @@ -125,7 +129,8 @@ public class InventoryHelper( findSlotResult.Y.Value, itemSize[0], itemSize[1], - findSlotResult.Rotation.Value); + findSlotResult.Rotation.Value + ); } catch (Exception ex) { @@ -142,7 +147,7 @@ public class InventoryHelper( } /// - /// Find a free location inside a container to fit the item + /// Find a free location inside a container to fit the item /// /// Container grid to add item to /// Item to add to grid @@ -170,7 +175,8 @@ public class InventoryHelper( findSlotResult.Y.Value, itemSize[0], itemSize[1], - findSlotResult.Rotation.Value); + findSlotResult.Rotation.Value + ); } catch (Exception ex) { @@ -178,6 +184,7 @@ public class InventoryHelper( return; } + // Store details for object, incuding container item will be placed in rootItemAdded.ParentId = containerId; rootItemAdded.SlotId = desiredSlotId; @@ -186,16 +193,15 @@ public class InventoryHelper( X = findSlotResult.X, Y = findSlotResult.Y, R = findSlotResult.Rotation.GetValueOrDefault(false) ? 1 : 0, - Rotation = findSlotResult.Rotation, + Rotation = findSlotResult.Rotation }; // Success! exit - return; } } /// - /// Find a location to place an item into inventory and place it + /// 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 @@ -215,9 +221,9 @@ public class InventoryHelper( } /// - /// Handle Remove event - /// Remove item from player inventory + insured items array - /// Also deletes child items + /// 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 @@ -229,18 +235,19 @@ public class InventoryHelper( } /// - /// Delete desired item from a player profiles mail + /// Delete desired item from a player profiles mail /// /// Session id /// Remove request /// OPTIONAL - ItemEventRouterResponse - public void RemoveItemAndChildrenFromMailRewards(string sessionId, InventoryRemoveRequestData removeRequest, ItemEventRouterResponse output = null) + 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 + /// Find item by id in player inventory and remove x of its count /// /// player profile /// Item id to decrement StackObjectsCount of @@ -248,13 +255,14 @@ 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 = null) { throw new NotImplementedException(); } /// - /// Get the height and width of an item - can have children that alter size + /// 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 @@ -267,8 +275,8 @@ public class InventoryHelper( } /// - /// Calculates the size of an item including attachments - /// takes into account if item is folded + /// Calculates the size of an item including attachments + /// takes into account if item is folded /// /// Items template id /// Items id @@ -280,19 +288,18 @@ public class InventoryHelper( var (key, tmpItem) = _itemHelper.GetItem(itemTpl); // Invalid item - if (!key) - { - _logger.Error(_localisationService.GetText("inventory-invalid_item_missing_from_db", itemTpl)); - } + if (!key) _logger.Error(_localisationService.GetText("inventory-invalid_item_missing_from_db", itemTpl)); // Item found but no _props property if (key && tmpItem.Properties is null) - { - _localisationService.GetText("inventory-item_missing_props_property", new { - itemTpl = itemTpl, - itemName = tmpItem?.Name - }); - } + _localisationService.GetText( + "inventory-item_missing_props_property", + new + { + itemTpl, + itemName = tmpItem?.Name + } + ); // No item object or getItem() returned false if (!key && tmpItem is null) @@ -320,52 +327,44 @@ public class InventoryHelper( var outY = (int)tmpItem.Properties.Height; // Item types to ignore - var skipThisItems = new List { BaseClasses.BACKPACK, BaseClasses.SEARCHABLE_ITEM, BaseClasses.SIMPLE_CONTAINER }; + var skipThisItems = new List + { BaseClasses.BACKPACK, BaseClasses.SEARCHABLE_ITEM, BaseClasses.SIMPLE_CONTAINER }; var rootIsFolded = rootItem?.Upd?.Foldable?.Folded == true; // The item itself is collapsible if (isFoldable is not null && string.IsNullOrEmpty(foldedSlot) && rootIsFolded) - { 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]]) { + 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; - } + 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)); - } + _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 (isFoldable is true && foldedSlot == item.SlotId && (rootIsFolded || childFolded)) - { - continue; - } + if (isFoldable is true && foldedSlot == item.SlotId && (rootIsFolded || childFolded)) continue; - if (childFoldable && rootIsFolded && childFolded) - { - continue; - } + if (childFoldable && rootIsFolded && childFolded) continue; // Calculating child ExtraSize if (itm.Properties.ExtraSizeForceAdd == true) @@ -378,25 +377,30 @@ public class InventoryHelper( 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; + 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 [ + return + [ outX + sizeLeft + sizeRight + forcedLeft + forcedRight, - outY + sizeUp + sizeDown + forcedUp + forcedDown, + outY + sizeUp + sizeDown + forcedUp + forcedDown ]; } /// - /// Get a blank two-dimensional representation of a container + /// Get a blank two-dimensional representation of a container /// /// Horizontal size of container /// Vertical size of container @@ -417,7 +421,7 @@ public class InventoryHelper( } /// - /// Get a 2d mapping of a container with what grid slots are filled + /// Get a 2d mapping of a container with what grid slots are filled /// /// Horizontal size of container /// Vertical size of container @@ -434,13 +438,12 @@ public class InventoryHelper( // 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) { + foreach (var item in containerItemHash) + { var itemLocation = item?.Location as ItemLocation; if (itemLocation is null) { @@ -461,28 +464,29 @@ public class InventoryHelper( 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, fW); - } catch (Exception ex) { + } + catch (Exception ex) + { _logger.Error( - _localisationService.GetText("inventory-unable_to_fill_container", new { - id = item.Id, - error = $"{ex.Message} {ex.StackTrace}" - }) + _localisationService.GetText( + "inventory-unable_to_fill_container", + new + { + id = item.Id, + error = $"{ex.Message} {ex.StackTrace}" + } + ) ); } - - } } return container2D; @@ -498,29 +502,27 @@ public class InventoryHelper( { var inventoryItemHash = new InventoryItemHash { - ByItemId = new(), - ByParentId = new() + ByItemId = new Dictionary(), + ByParentId = new Dictionary>() }; foreach (var item in inventoryItems) { inventoryItemHash.ByItemId.TryAdd(item.Id, item); - if (item.ParentId is null) { - continue; - } + if (item.ParentId is null) continue; - if (!inventoryItemHash.ByParentId.ContainsKey(item.ParentId)) { + 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. + /// 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 @@ -571,13 +573,13 @@ public class InventoryHelper( From = fromInventoryItems, To = toInventoryItems, SameInventory = movingToSameInventory, - IsMail = fromType == "mail", + IsMail = fromType == "mail" }; } /// - /// Get a two-dimensional array to represent stash slots - /// 0 value = free, 1 = taken + /// Get a two-dimensional array to represent stash slots + /// 0 value = free, 1 = taken /// /// Player profile /// session id @@ -588,7 +590,7 @@ public class InventoryHelper( } /// - /// Get a blank two-dimensional array representation of a container + /// Get a blank two-dimensional array representation of a container /// /// Container to get data for /// blank two-dimensional array @@ -604,7 +606,7 @@ public class InventoryHelper( } /// - /// Get a two-dimensional array representation of the players sorting table + /// Get a two-dimensional array representation of the players sorting table /// /// Player profile /// two-dimensional array @@ -614,7 +616,7 @@ public class InventoryHelper( } /// - /// Get Players Stash Size + /// Get Players Stash Size /// /// Players id /// Dictionary of 2 values, horizontal and vertical stash size @@ -624,7 +626,7 @@ public class InventoryHelper( } /// - /// Get the players stash items tpl + /// Get the players stash items tpl /// /// Player id /// Stash tpl @@ -634,18 +636,54 @@ public class InventoryHelper( } /// - /// Internal helper function to transfer an item + children from one profile to another. + /// 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(); + HandleCartridges(sourceItems, request); + + // 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); + if (itemToMove is null) + { + _logger.Error(_localisationService.GetText("inventory-unable_to_find_item_to_move", itemId)); + continue; + } + + // Only adjust the values for parent item, not children (their values are already correctly tied to parent) + if (itemId == request.Item) + { + itemToMove.ParentId = request.To.Id; + 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; + } + } + } + + toItems.Add(itemToMove); + sourceItems.RemoveAt(sourceItems.IndexOf(itemToMove)); + } } /// - /// Internal helper function to move item within the same profile. + /// Internal helper function to move item within the same profile. /// /// profile to edit /// @@ -662,7 +700,7 @@ public class InventoryHelper( HandleCartridges(inventoryItems, moveRequest); // Find item we want to 'move' - var matchingInventoryItem = inventoryItems.FirstOrDefault((item) => item.Id == moveRequest.Item); + 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"; @@ -673,7 +711,7 @@ public class InventoryHelper( } _logger.Debug( - $"{ moveRequest.Action} item: ${ moveRequest.Item} from slotid: { matchingInventoryItem.SlotId} to container: { moveRequest.To.Container}" + $"{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) @@ -685,7 +723,7 @@ public class InventoryHelper( new { slotId = matchingInventoryItem.SlotId, - container = moveRequest.To.Container, + container = moveRequest.To.Container } ) ); @@ -708,17 +746,14 @@ public class InventoryHelper( else { // Moved from slot with location to one without, clean up - if (matchingInventoryItem.Location is not null) - { - matchingInventoryItem.Location = null; - } + 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 + /// Update fast panel bindings when an item is moved into a container that doesn't allow quick slot access /// /// Player profile /// item being moved @@ -726,39 +761,39 @@ public class InventoryHelper( { // Find matching _id in fast panel - if (!pmcData.Inventory.FastPanel.TryGetValue(itemBeingMoved.Id, out var fastPanelSlot)) - { - return; - } + 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; - } + 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()] = ""; - } + if (!wasMovedToFastPanelAccessibleContainer) pmcData.Inventory.FastPanel[fastPanelSlot[0].ToString()] = ""; } /// - /// Internal helper function to handle cartridges in inventory if any of them exist. + /// Internal helper function to handle cartridges in inventory if any of them exist. /// protected void HandleCartridges(List items, InventoryMoveRequestData request) { - throw new NotImplementedException(); + // Not moving item into a cartridge slot, skip + if (request.To.Container != "cartridges") + { + return; + } + + // Get a count of cartridges in existing magazine + var cartridgeCount = items.Count(item => item.ParentId == request.To.Id); + + request.To.Location = cartridgeCount; } /// - /// Get details for how a random loot container should be handled, max rewards, possible reward tpls + /// Get details for how a random loot container should be handled, max rewards, possible reward tpls /// /// Container being opened /// Reward details @@ -768,7 +803,7 @@ public class InventoryHelper( } /// - /// Get inventory configuration + /// Get inventory configuration /// /// Inventory configuration public InventoryConfig GetInventoryConfig() @@ -777,9 +812,9 @@ public class InventoryHelper( } /// - /// Recursively checks if the given item is - /// inside the stash, that is it has the stash as - /// ancestor with slotId=hideout + /// 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 @@ -795,7 +830,7 @@ public class InventoryHelper( } /// - /// Does the provided item have a root item with the provided id + /// Does the provided item have a root item with the provided id /// /// Profile with items /// Item to check diff --git a/Libraries/Core/Models/Eft/Common/Tables/Item.cs b/Libraries/Core/Models/Eft/Common/Tables/Item.cs index 25170621..86d4439c 100644 --- a/Libraries/Core/Models/Eft/Common/Tables/Item.cs +++ b/Libraries/Core/Models/Eft/Common/Tables/Item.cs @@ -25,6 +25,20 @@ public record Item [JsonPropertyName("upd")] public Upd? Upd { get; set; } + + public ItemEvent.Product ConvertToProduct() + { + // TODO - maybe this entire product item can be replaced with Item? + return new ItemEvent.Product + { + Id = Id, + Template = Template, + ParentId = ParentId, + SlotId = SlotId, + Upd = Upd, + Location = (ItemLocation)Location + }; + } } public record ItemLocation diff --git a/Libraries/Core/Models/Eft/Common/Tables/TemplateItem.cs b/Libraries/Core/Models/Eft/Common/Tables/TemplateItem.cs index 812940ae..79d368da 100644 --- a/Libraries/Core/Models/Eft/Common/Tables/TemplateItem.cs +++ b/Libraries/Core/Models/Eft/Common/Tables/TemplateItem.cs @@ -137,8 +137,9 @@ public record Props [JsonPropertyName("LootExperience")] public double? LootExperience { get; set; } + // Checked on live [JsonPropertyName("ExamineExperience")] - public double? ExamineExperience { get; set; } + public int? ExamineExperience { get; set; } [JsonPropertyName("HideEntrails")] public bool? HideEntrails { get; set; } diff --git a/Libraries/Core/Models/Eft/Inventory/InventoryReadEncyclopediaRequestData.cs b/Libraries/Core/Models/Eft/Inventory/InventoryReadEncyclopediaRequestData.cs index d4473d9b..5734e796 100644 --- a/Libraries/Core/Models/Eft/Inventory/InventoryReadEncyclopediaRequestData.cs +++ b/Libraries/Core/Models/Eft/Inventory/InventoryReadEncyclopediaRequestData.cs @@ -1,9 +1,9 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace Core.Models.Eft.Inventory; public record InventoryReadEncyclopediaRequestData : InventoryBaseActionRequestData { [JsonPropertyName("ids")] - public List? Ids { get; set; } + public List Ids { get; set; } } diff --git a/Libraries/Core/Models/Eft/Inventory/SetFavoriteItems.cs b/Libraries/Core/Models/Eft/Inventory/SetFavoriteItems.cs index c6a223b0..86323d83 100644 --- a/Libraries/Core/Models/Eft/Inventory/SetFavoriteItems.cs +++ b/Libraries/Core/Models/Eft/Inventory/SetFavoriteItems.cs @@ -1,11 +1,11 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace Core.Models.Eft.Inventory; public record SetFavoriteItems : InventoryBaseActionRequestData { [JsonPropertyName("items")] - public List? Items { get; set; } + public List? Items { get; set; } [JsonPropertyName("timestamp")] public long? Timestamp { get; set; } diff --git a/Libraries/Core/Models/Spt/Dialog/SendMessageDetails.cs b/Libraries/Core/Models/Spt/Dialog/SendMessageDetails.cs index ddd1615c..3bb7a19b 100644 --- a/Libraries/Core/Models/Spt/Dialog/SendMessageDetails.cs +++ b/Libraries/Core/Models/Spt/Dialog/SendMessageDetails.cs @@ -93,7 +93,7 @@ public record ProfileChangeEvent public ProfileChangeEventType? Type { get; set; } [JsonPropertyName("value")] - public double? Value { get; set; } + public int? Value { get; set; } [JsonPropertyName("entity")] public string? Entity { get; set; } diff --git a/Libraries/Core/Services/RagfairOfferService.cs b/Libraries/Core/Services/RagfairOfferService.cs index 39200382..b449e0e4 100644 --- a/Libraries/Core/Services/RagfairOfferService.cs +++ b/Libraries/Core/Services/RagfairOfferService.cs @@ -16,7 +16,7 @@ public class RagfairOfferService throw new NotImplementedException(); } - public RagfairOffer GetOfferByOfferId(string offerId) + public RagfairOffer? GetOfferByOfferId(string offerId) { throw new NotImplementedException(); }