From 69d22cef1193bc55bb83560ec6c427f308afb725 Mon Sep 17 00:00:00 2001 From: Chomp Date: Sat, 18 Jan 2025 18:01:40 +0000 Subject: [PATCH] partially implemented inventory item movement --- Core/Callbacks/InventoryCallbacks.cs | 7 +- Core/Callbacks/ItemEventCallbacks.cs | 4 +- Core/Controllers/InventoryController.cs | 133 ++++++++++++- Core/Helpers/InventoryHelper.cs | 159 ++++++++++++++-- .../InventoryBaseActionRequestData.cs | 4 +- Core/Models/Enums/BackendErrorCodes.cs | 174 +++++++++--------- Core/Utils/HttpResponseUtil.cs | 2 +- 7 files changed, 375 insertions(+), 108 deletions(-) diff --git a/Core/Callbacks/InventoryCallbacks.cs b/Core/Callbacks/InventoryCallbacks.cs index 02c24fbb..cfe5b7fe 100644 --- a/Core/Callbacks/InventoryCallbacks.cs +++ b/Core/Callbacks/InventoryCallbacks.cs @@ -23,10 +23,9 @@ public class InventoryCallbacks( /// public ItemEventRouterResponse MoveItem(PmcData pmcData, InventoryMoveRequestData info, string sessionID, ItemEventRouterResponse output) { - // _inventoryController.MoveItem(pmcData, info, sessionID, output); - // TODO: InventoryController is not implemented rn - // return output; - throw new NotImplementedException(); + _inventoryController.MoveItem(pmcData, info, sessionID, output); + + return output; } /// diff --git a/Core/Callbacks/ItemEventCallbacks.cs b/Core/Callbacks/ItemEventCallbacks.cs index 54e2c54f..555cc24d 100644 --- a/Core/Callbacks/ItemEventCallbacks.cs +++ b/Core/Callbacks/ItemEventCallbacks.cs @@ -27,7 +27,7 @@ public class ItemEventCallbacks(HttpResponseUtil _httpResponseUtil, ItemEventRou public bool IsCriticalError(List warnings) { // List of non-critical error codes, we return true if any error NOT included is passed in - var nonCriticalErrorCodes = new List() { BackendErrorCodes.NOTENOUGHSPACE }; + var nonCriticalErrorCodes = new List { BackendErrorCodes.NotEnoughSpace }; foreach (var warning in warnings) { @@ -44,7 +44,7 @@ public class ItemEventCallbacks(HttpResponseUtil _httpResponseUtil, ItemEventRou public int GetErrorCode(List warnings) { return int.Parse((warnings[0].Code is null - ? BackendErrorCodes.UNKNOWN_ERROR.ToString() + ? BackendErrorCodes.UnknownError.ToString() : warnings.FirstOrDefault()?.Code) ?? string.Empty); } } diff --git a/Core/Controllers/InventoryController.cs b/Core/Controllers/InventoryController.cs index 8b9d2d74..f5b72a1b 100644 --- a/Core/Controllers/InventoryController.cs +++ b/Core/Controllers/InventoryController.cs @@ -1,9 +1,140 @@ using Core.Annotations; +using Core.Generators; +using Core.Helpers; +using Core.Models.Eft.Common; +using Core.Models.Eft.Inventory; +using Core.Models.Eft.ItemEvent; +using Core.Models.Enums; +using Core.Models.Utils; +using Core.Routers; +using Core.Services; +using Core.Utils; +using Core.Utils.Cloners; namespace Core.Controllers; [Injectable] public class InventoryController { - // TODO + protected ISptLogger _logger; + protected HashUtil _hashUtil; + protected RandomUtil _randomUtil; + protected HttpResponseUtil _httpResponseUtil; + protected PresetHelper _presetHelper; + protected InventoryHelper _inventoryHelper; + protected QuestHelper _questHelper; + protected HideoutHelper _hideoutHelper; + protected ProfileHelper _profileHelper; + protected PaymentHelper _paymentHelper; + protected TraderHelper _traderHelper; + protected DatabaseService _databaseService; + protected FenceService _fenceService; + protected RagfairOfferService _ragfairOfferService; + protected MapMarkerService _mapMarkerService; + protected LocalisationService _localisationService; + protected PlayerService _playerService; + protected LootGenerator _lootGenerator; + protected EventOutputHolder _eventOutputHolder; + + public InventoryController( + ISptLogger logger, + HashUtil hashUtil, + RandomUtil randomUtil, + HttpResponseUtil httpResponseUtil, + PresetHelper presetHelper, + InventoryHelper inventoryHelper, + QuestHelper questHelper, + HideoutHelper hideoutHelper, + ProfileHelper profileHelper, + PaymentHelper paymentHelper, + TraderHelper traderHelper, + DatabaseService databaseService, + FenceService fenceService, + RagfairOfferService ragfairOfferService, + MapMarkerService mapMarkerService, + LocalisationService localisationService, + PlayerService playerService, + LootGenerator lootGenerator, + EventOutputHolder eventOutputHolder, + + ICloner cloner + ) + { + _logger = logger; + _hashUtil = hashUtil; + _randomUtil = randomUtil; + _httpResponseUtil = httpResponseUtil; + _presetHelper = presetHelper; + _inventoryHelper = inventoryHelper; + _questHelper = questHelper; + _hideoutHelper = hideoutHelper; + _profileHelper = profileHelper; + _paymentHelper = paymentHelper; + _traderHelper = traderHelper; + _databaseService = databaseService; + _fenceService = fenceService; + _ragfairOfferService = ragfairOfferService; + _mapMarkerService = mapMarkerService; + _localisationService = localisationService; + _playerService = playerService; + _lootGenerator = lootGenerator; + _eventOutputHolder = eventOutputHolder; + } + + public void MoveItem(PmcData pmcData, InventoryMoveRequestData moveRequest, string sessionID, ItemEventRouterResponse output) + { + if (output.Warnings.Count > 0) + { + return; + } + + // Changes made to result apply to character inventory + 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 + if (moveRequest.FromOwner?.Type == "Trader" && !ownerInventoryItems.IsMail.GetValueOrDefault(false)) + { + AppendTraderExploitErrorResponse(output); + return; + } + + // Check for item in inventory before allowing internal transfer + var originalItemLocation = ownerInventoryItems.From.FirstOrDefault((item) => item.Id == moveRequest.Item); + if (originalItemLocation is null) + { + // Internal item move but item never existed, possible dupe glitch + AppendTraderExploitErrorResponse(output); + return; + } + + var originalLocationSlotId = originalItemLocation?.SlotId; + + 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.StartsWith("dogtag") || originalLocationSlotId.StartsWith("dogtag")) + { + _hideoutHelper.ApplyPlaceOfFameDogtagBonus(pmcData); + } + } + else + { + _inventoryHelper.MoveItemToProfile(ownerInventoryItems.From, ownerInventoryItems.To, moveRequest); + } + } + + private void AppendTraderExploitErrorResponse(ItemEventRouterResponse output) + { + _httpResponseUtil.AppendErrorToOutput( + output, + _localisationService.GetText("inventory-edit_trader_item"), + (BackendErrorCodes)228 + ); + } } diff --git a/Core/Helpers/InventoryHelper.cs b/Core/Helpers/InventoryHelper.cs index d0746744..f76107c9 100644 --- a/Core/Helpers/InventoryHelper.cs +++ b/Core/Helpers/InventoryHelper.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; using Core.Annotations; using Core.Models.Eft.Common; using Core.Models.Eft.Common.Tables; @@ -6,12 +6,32 @@ 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; namespace Core.Helpers; [Injectable] public class InventoryHelper { + protected ISptLogger _logger; + protected ProfileHelper _profileHelper; + protected DialogueHelper _dialogueHelper; + protected LocalisationService _localisationService; + + public InventoryHelper( + ISptLogger logger, + ProfileHelper profileHelper, + DialogueHelper dialogueHelper, + LocalisationService localisationService + ) + { + _logger = logger; + _profileHelper = profileHelper; + _dialogueHelper = dialogueHelper; + _localisationService = localisationService; + } + /// /// Add multiple items to player stash (assuming they all fit) /// @@ -49,7 +69,7 @@ public class InventoryHelper /// /// An item /// Item was found in raid - protected void SetFindInRaidStatusForItem(Item[] itemWithChildren, bool foundInRaid) + protected void SetFindInRaidStatusForItem(List itemWithChildren, bool foundInRaid) { throw new NotImplementedException(); } @@ -236,17 +256,59 @@ public class InventoryHelper /// Based on the item action, determine whose inventories we should be looking at for from and to. /// /// Item interaction request - /// Session id / playerid + /// Item being moved/split/etc to inventory + /// Session id / players Id /// OwnerInventoryItems with inventory of player/scav to adjust public OwnerInventoryItems GetOwnerInventoryItems( - InventoryMoveRequestData request, + InventoryBaseActionRequestData request, + string item, string sessionId) { - throw new NotImplementedException(); + 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 + /// Get a two-dimensional array to represent stash slots /// 0 value = free, 1 = taken /// /// Player profile @@ -303,7 +365,7 @@ public class InventoryHelper /// Inventory of the source (can be non-player) /// Inventory of the destination /// Move request - public void MoveItemToProfile(Item[] sourceItems, Item[] toItems, InventoryMoveRequestData request) + public void MoveItemToProfile(List sourceItems, List toItems, InventoryMoveRequestData request) { throw new NotImplementedException(); } @@ -314,13 +376,63 @@ public class InventoryHelper /// profile to edit /// /// client move request + /// /// True if move was successful - public (bool success, string errorMessage) MoveItemInternal( + public bool MoveItemInternal( PmcData pmcData, - Item[] inventoryItems, - InventoryMoveRequestData moveRequest) + List inventoryItems, + InventoryMoveRequestData moveRequest, + out string errorMessage) { - throw new NotImplementedException(); + 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; } /// @@ -330,13 +442,34 @@ public class InventoryHelper /// item being moved protected void UpdateFastPanelBinding(PmcData pmcData, Item itemBeingMoved) { - throw new NotImplementedException(); + // 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(Item[] items, InventoryMoveRequestData request) + protected void HandleCartridges(List items, InventoryMoveRequestData request) { throw new NotImplementedException(); } diff --git a/Core/Models/Eft/Inventory/InventoryBaseActionRequestData.cs b/Core/Models/Eft/Inventory/InventoryBaseActionRequestData.cs index 810d4311..94b2de02 100644 --- a/Core/Models/Eft/Inventory/InventoryBaseActionRequestData.cs +++ b/Core/Models/Eft/Inventory/InventoryBaseActionRequestData.cs @@ -1,9 +1,9 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; using Core.Models.Eft.Common.Request; namespace Core.Models.Eft.Inventory; -public record InventoryBaseActionRequestData : BaseInteractionRequestData +public abstract record InventoryBaseActionRequestData : BaseInteractionRequestData { } diff --git a/Core/Models/Enums/BackendErrorCodes.cs b/Core/Models/Enums/BackendErrorCodes.cs index 9799f808..b06ef368 100644 --- a/Core/Models/Enums/BackendErrorCodes.cs +++ b/Core/Models/Enums/BackendErrorCodes.cs @@ -2,89 +2,93 @@ namespace Core.Models.Enums; public enum BackendErrorCodes { - NONE = 0, - UNKNOWN_ERROR = 200, - NOT_AUTHORIZED = 201, - NEED_AUTHORIZATION_CODE = 209, - WRONG_AUTHORIZATION_CODE = 211, - NEED_CAPTCHA = 214, - NO_NEED_CAPTCHA = 215, - CAPTCHA_INVALID_ANSWER = 216, - CAPTCHA_FAILED = 218, - CAPTCHA_BRUTE_FORCED = 219, - NO_ROOM_IN_STASH = 223, - NICKNAME_NOT_UNIQUE = 225, - NICKNAME_NOT_VALID = 226, - UNSUPPORTED_CLIENT_VERSION = 232, - REPORT_NOT_ALLOWED = 238, - NICKNAME_IS_ABUSIVE = 241, - NICKNAME_CHANGE_TIMEOUT = 242, - NOT_ENOUGH_SPACE_TO_UNPACK = 257, - NOT_MODIFIED = 304, - HTTP_BAD_REQUEST = 400, - HTTP_NOT_AUTHORIZED = 401, - HTTP_FORBIDDEN = 403, - HTTP_NOT_FOUND = 404, - HTTP_METHOD_NOT_ALLOWED = 405, - UNKNOWN_TRADING_ERROR = 500, - HTTPNOTIMPLEMENTED = 501, - HTTPBADGATEWAY = 502, - HTTPSERVICEUNAVAILABLE = 503, - HTTPGATEWAYTIMEOUT = 504, - TRADEROUTOFMONEY = 505, - HTTPVARIANTALSONEGOTIATES = 506, - PRICECHANGED = 509, - TRADERDISABLED = 512, - ITEMHASBEENSOLD = 513, - NOTENOUGHSPACEFORMONEY = 518, - HTTPINVALIDSSLCERTIFICATE = 526, - UNKNOWNRAGFAIRERROR = 550, - UNKNOWNRAGFAIRERROR2 = 551, - UNKNOWNMATCHMAKERERROR = 600, - SESSIONPARAMETERSERROR = 601, - SESSIONLOST = 602, - SERVERNOTREGISTERED = 604, - UNKNOWNQUESTERROR = 700, - QUESTBADPARAM = 702, - QUESTNOTFOUND = 703, - QUESTISUNAVAILABLE = 704, - NOFREESPACEFORREWARDS = 705, - WRONGQUESTSTATUS = 706, - CANTCOMPLETEQUEST = 707, - UNKNOWNMAILERROR = 900, - TOOMANYFRIENDREQUESTS = 925, - UNKNOWNSCRIPTEXECUTIONERROR = 1000, - UNKNOWNREPAIRINGERROR = 1200, - UNKNOWNINSURANCEERROR = 1300, - UNKNOWNCURRENCYEXCHANGEERROR = 1400, - OFFERNOTFOUND = 1503, - NOTENOUGHSPACE = 1505, - OFFEROUTOFSTOCK = 1506, - OFFERSOLD = 1507, - RAGFAIRUNAVAILABLE = 1511, - BANNEDERRORCODE = 1513, - INSUFFICIENTNUMBERINSTOCK = 1516, - TOOMANYITEMSTOSELL = 1517, - INCORRECTCLIENTPRICE = 1519, - EXAMINATIONFAILED = 22001, - ITEMALREADYEXAMINED = 22002, - UNKNOWNNGINXERROR = 9000, - PARSERESPONSEERROR = 9001, - UNKNOWNMATCHMAKERERROR2 = 503000, // They have two of these...why :/ - UNKNOWNGROUPERROR = 502000, - GROUPREQUESTNOTFOUND = 502002, - GROUPFULL = 502004, - PLAYERALREADYINGROUP = 502005, - PLAYERNOTINGROUP = 502006, - PLAYERNOTLEADER = 502007, - CANTCHANGEREADYSTATE = 502010, - PLAYERFORBIDDENGROUPINVITES = 502011, - LEADERALREADYREADY = 502012, - GROUPSENDINVITEERROR = 502013, - PLAYERISOFFLINE = 502014, - PLAYERISNOTSEARCHINGFORGROUP = 502018, - PLAYERALREADYLOOKINGFORGAME = 503001, - PLAYERINRAID = 503002, - LIMITFORPRESETSREACHED = 504001, - PLAYERPROFILENOTFOUND = 505001, + None, + UnknownError = 200, + NotAuthorized, + NeedAuthorizationCode = 209, + WrongAuthorizationCode = 211, + NeedCaptcha = 214, + NoNeedCaptcha, + CaptchaInvalidAnswer, + CaptchaFailed = 218, + CaptchaBruteForced, + NoRoomInStash = 223, + NicknameNotUnique = 225, + NicknameNotValid, + UnsupportedClientVersion = 232, + ReportNotAllowed = 238, + NicknameIsAbusive = 241, + NicknameChangeTimeout, + NotEnoughSpaceToUnpack = 257, + NotModified = 304, + HTTPBadRequest = 400, + HTTPNotAuthorized, + HTTPForbidden = 403, + HTTPNotFound, + HTTPMethodNotAllowed, + UnknownTradingError = 500, + HTTPNotImplemented, + HTTPBadGateway, + HTTPServiceUnavailable, + HTTPGatewayTimeout, + TraderOutOfMoney, + HTTPVariantAlsoNegotiates, + PriceChanged = 509, + TraderDisabled = 512, + ItemHasBeenSold, + NotEnoughSpaceForMoney = 518, + HTTPInvalidSSLCertificate = 526, + UnknownRagfairError = 550, + UnknownRagfairError2, + UnknownMatchMakerError = 600, + SessionParametersError, + SessionLost, + ServerNotRegistered = 604, + UnknownQuestError = 700, + QuestBadParam = 702, + QuestNotFound, + QuestIsUnavailable, + NoFreeSpaceForRewards, + WrongQuestStatus, + CantCompleteQuest, + UnknownMailError = 900, + TooManyFriendRequests = 925, + UnknownScriptExecutionError = 1000, + UnknownRepairingError = 1200, + UnknownInsuranceError = 1300, + UnknownCurrencyExchangeError = 1400, + OfferNotFound = 1503, + NotEnoughSpace = 1505, + OfferOutOfStock, + OfferSold, + RagfairUnavailable = 1511, + BannedErrorCode = 1513, + InsufficientNumberInStock = 1516, + TooManyItemsToSell, + IncorrectClientPrice = 1519, + ExaminationFailed = 22001, + ItemAlreadyExamined, + UnknownNginxError = 9000, + ParseResponseError, + UnknownMatchmakerError = 503000, + UnknownGroupError = 502000, + GroupRequestNotFound = 502002, + GroupFull = 502004, + PlayerAlreadyInGroup, + PlayerNotInGroup, + PlayerNotLeader, + CantChangeReadyState = 502010, + PlayerForbiddenGroupInvites, + LeaderAlreadyReady, + GroupSendInviteError, + PlayerIsOffline, + PlayerIsNotSearchingForGroup = 502018, + PlayerTooManyPartyRequests = 502021, + PlayerIsInRegularMode = 502032, + PlayerIsInPveMode, + PlayerAlreadyLookingForGame = 503001, + PlayerInRaid, + LimitForPresetsReached = 504001, + PlayerProfileNotFound = 505001, + NoArenaSync = 507006 } diff --git a/Core/Utils/HttpResponseUtil.cs b/Core/Utils/HttpResponseUtil.cs index 78e08d60..66c55995 100644 --- a/Core/Utils/HttpResponseUtil.cs +++ b/Core/Utils/HttpResponseUtil.cs @@ -98,7 +98,7 @@ public class HttpResponseUtil public ItemEventRouterResponse AppendErrorToOutput( ItemEventRouterResponse output, string? message = null, - BackendErrorCodes errorCode = BackendErrorCodes.NONE + BackendErrorCodes errorCode = BackendErrorCodes.None ) { if (string.IsNullOrEmpty(message))