diff --git a/Source/ACE.Server/WorldObjects/Player_Inventory.cs b/Source/ACE.Server/WorldObjects/Player_Inventory.cs index 4c079aff05..9c9ea303a7 100644 --- a/Source/ACE.Server/WorldObjects/Player_Inventory.cs +++ b/Source/ACE.Server/WorldObjects/Player_Inventory.cs @@ -2541,6 +2541,15 @@ public void HandleActionStackableSplitTo3D(uint stackId, int amount) return; } + // We make sure the stack is still valid. It could have changed during our pickup animation + if (FindObject(stack.Guid, SearchLocations.MyInventory | SearchLocations.MyEquippedItems, out stackFoundInContainer, out stackRootOwner, out _) != stack || stack.StackSize <= amount) + { + log.DebugFormat("Player 0x{0:X8}:{1} tried to split an item that's no longer valid 0x{2:X8}:{3}.", Guid.Full, Name, stack.Guid.Full, stack.Name); + Session.Network.EnqueueSend(new GameEventCommunicationTransientString(Session, "Split failed!")); // Custom error message + Session.Network.EnqueueSend(new GameEventInventoryServerSaveFailed(Session, stackId, WeenieError.ActionCancelled)); + return; + } + if (!AdjustStack(stack, -amount, stackFoundInContainer, stackRootOwner)) { Session.Network.EnqueueSend(new GameEventInventoryServerSaveFailed(Session, stackId, WeenieError.ActionCancelled)); @@ -3249,6 +3258,15 @@ public void HandleActionGiveObjectRequest(uint targetGuid, uint itemGuid, int am return; } + // We make sure the item is still valid. It could have changed during our movement + if (FindObject(item.Guid, SearchLocations.MyInventory | SearchLocations.MyEquippedItems, out itemFoundInContainer, out itemRootOwner, out itemWasEquipped) != item || item.StackSize < amount) + { + log.DebugFormat("Player 0x{0:X8}:{1} tried to give an item that's no longer valid 0x{2:X8}:{3}.", Guid.Full, Name, item.Guid.Full, item.Name); + Session.Network.EnqueueSend(new GameEventCommunicationTransientString(Session, "Give failed!")); // Custom error message + Session.Network.EnqueueSend(new GameEventInventoryServerSaveFailed(Session, itemGuid, WeenieError.ActionCancelled)); + return; + } + if (target is Player targetAsPlayer) GiveObjectToPlayer(targetAsPlayer, item, itemFoundInContainer, itemRootOwner, itemWasEquipped, amount); else diff --git a/Source/ACE.Server/WorldObjects/Player_Trade.cs b/Source/ACE.Server/WorldObjects/Player_Trade.cs index f1f71acf66..41da05ab27 100644 --- a/Source/ACE.Server/WorldObjects/Player_Trade.cs +++ b/Source/ACE.Server/WorldObjects/Player_Trade.cs @@ -259,10 +259,16 @@ private void FinalizeTrade(Player target) actionChain.AddAction(CurrentLandblock, () => { foreach (var wo in myEscrow) - TryCreateInInventoryWithNetworking(wo); + { + if (!TryCreateInInventoryWithNetworking(wo) && !target.TryCreateInInventoryWithNetworking(wo)) + log.WarnFormat("Item 0x{0:X8}:{1} for player {2} lost from FinalizeTrade failure.", wo.Guid.Full, wo.Name, target.Name); + } foreach (var wo in targetEscrow) - target.TryCreateInInventoryWithNetworking(wo); + { + if (!target.TryCreateInInventoryWithNetworking(wo) && !TryCreateInInventoryWithNetworking(wo)) + log.WarnFormat("Item 0x{0:X8}:{1} for player {2} lost from FinalizeTrade failure.", wo.Guid.Full, wo.Name, Name); + } Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.TradeComplete)); target.Session.Network.EnqueueSend(new GameEventWeenieError(target.Session, WeenieError.TradeComplete)); diff --git a/Source/ACE.Server/WorldObjects/Vendor.cs b/Source/ACE.Server/WorldObjects/Vendor.cs index 0ae968610a..f9ebcd6fcc 100644 --- a/Source/ACE.Server/WorldObjects/Vendor.cs +++ b/Source/ACE.Server/WorldObjects/Vendor.cs @@ -440,6 +440,8 @@ public bool BuyItems_ValidateTransaction(List itemProfiles, Player var defaultItemProfiles = new List(); var uniqueItems = new List(); + var requestedGuids = new HashSet(); + // find item profiles in default and unique items foreach (var itemProfile in itemProfiles) { @@ -450,11 +452,25 @@ public bool BuyItems_ValidateTransaction(List itemProfiles, Player return false; } + if (!requestedGuids.Add(itemProfile.ObjectGuid)) + { + log.Warn($"[VENDOR] {player.Name} tried to buy duplicate item {itemProfile.ObjectGuid:X8} from {Name}"); + player.SendTransientError($"Invalid item"); + return false; + } + var itemGuid = new ObjectGuid(itemProfile.ObjectGuid); // check default items if (DefaultItemsForSale.TryGetValue(itemGuid, out var defaultItemForSale)) { + // services are not limited by pack space, so only allow one per request + if (itemProfile.Amount > 1 && (defaultItemForSale.GetProperty(PropertyBool.VendorService) ?? false)) + { + player.SendTransientError($"Invalid amount"); + return false; + } + itemProfile.WeenieClassId = defaultItemForSale.WeenieClassId; itemProfile.Palette = defaultItemForSale.PaletteTemplate; itemProfile.Shade = defaultItemForSale.Shade; diff --git a/docker-compose.yml b/docker-compose.yml index 384e14e8e9..5cf5c225ec 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: volumes: - ./db-data:/var/lib/mysql ports: - - "3306:3306/tcp" + - "127.0.0.1:3306:3306/tcp" restart: unless-stopped healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]