From 71321bc85f298b4ed795b44eb3111e7e46bf8697 Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Thu, 22 Oct 2026 18:26:48 +0800 Subject: [PATCH] Fix CreateObject after full foreign stack merge --- .../StackableMergeMessageTests.cs | 52 +++++++++++++++++++ Source/ACE.Server/ACE.Server.csproj | 4 ++ .../WorldObjects/Player_Inventory.cs | 6 ++- .../StackableMergeMessageHelper.cs | 17 ++++++ 4 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 Source/ACE.Server.Tests/StackableMergeMessageTests.cs create mode 100644 Source/ACE.Server/WorldObjects/StackableMergeMessageHelper.cs diff --git a/Source/ACE.Server.Tests/StackableMergeMessageTests.cs b/Source/ACE.Server.Tests/StackableMergeMessageTests.cs new file mode 100644 index 0000000000..ec1666a0ea --- /dev/null +++ b/Source/ACE.Server.Tests/StackableMergeMessageTests.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; + +using ACE.Entity; +using ACE.Entity.Models; +using ACE.Server.WorldObjects; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace ACE.Server.Tests +{ + [TestClass] + public class StackableMergeMessageTests + { + [TestMethod] + public void DoesNotEnqueueCreateObjectForDestroyedForeignSourceStack() + { + var sourceStack = CreateStack(5); + sourceStack.Destroy(false); + Assert.IsTrue(sourceStack.IsDestroyed); + Assert.AreEqual(5, sourceStack.StackSize ?? -1); + var sentObjects = new List(); + + StackableMergeMessageHelper.EnqueueCreateObjectForRemainingSourceStack( + sourceStack, + sourceIsPlayerOwned: false, + sentObjects.Add); + + Assert.AreEqual(0, sentObjects.Count); + } + + [TestMethod] + public void EnqueuesCreateObjectForRemainingForeignSourceStack() + { + var sourceStack = CreateStack(3); + var sentObjects = new List(); + + StackableMergeMessageHelper.EnqueueCreateObjectForRemainingSourceStack( + sourceStack, + sourceIsPlayerOwned: false, + sentObjects.Add); + + Assert.AreEqual(1, sentObjects.Count); + Assert.AreSame(sourceStack, sentObjects[0]); + } + + private static Stackable CreateStack(int stackSize) + { + var stack = new Stackable(new Weenie(), new ObjectGuid(1)); + stack.StackSize = stackSize; + return stack; + } + } +} diff --git a/Source/ACE.Server/ACE.Server.csproj b/Source/ACE.Server/ACE.Server.csproj index 98d4fa2fda..61f799f426 100644 --- a/Source/ACE.Server/ACE.Server.csproj +++ b/Source/ACE.Server/ACE.Server.csproj @@ -242,6 +242,10 @@ + + + + diff --git a/Source/ACE.Server/WorldObjects/Player_Inventory.cs b/Source/ACE.Server/WorldObjects/Player_Inventory.cs index 9c9ea303a7..07e23234e2 100644 --- a/Source/ACE.Server/WorldObjects/Player_Inventory.cs +++ b/Source/ACE.Server/WorldObjects/Player_Inventory.cs @@ -3051,8 +3051,10 @@ public void HandleActionStackableMerge(uint mergeFromGuid, uint mergeToGuid, int if (DoHandleActionStackableMerge(sourceStack, targetStack, amount)) { // If the client used the R key to merge a partial stack from the landscape, it also tries to add the "ghosted" item of the picked up stack to the inventory as well. - if (sourceStackRootOwner != this && sourceStack.StackSize > 0) - Session.Network.EnqueueSend(new GameMessageCreateObject(sourceStack)); + StackableMergeMessageHelper.EnqueueCreateObjectForRemainingSourceStack( + sourceStack, + sourceStackRootOwner == this, + stack => Session.Network.EnqueueSend(new GameMessageCreateObject(stack))); Session.Network.EnqueueSend(new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.EncumbranceVal, EncumbranceVal ?? 0)); diff --git a/Source/ACE.Server/WorldObjects/StackableMergeMessageHelper.cs b/Source/ACE.Server/WorldObjects/StackableMergeMessageHelper.cs new file mode 100644 index 0000000000..4296f4d344 --- /dev/null +++ b/Source/ACE.Server/WorldObjects/StackableMergeMessageHelper.cs @@ -0,0 +1,17 @@ +using System; + +namespace ACE.Server.WorldObjects +{ + internal static class StackableMergeMessageHelper + { + internal static void EnqueueCreateObjectForRemainingSourceStack( + WorldObject sourceStack, + bool sourceIsPlayerOwned, + Action enqueueCreateObject) + { + // A full merge destroys the source but leaves its pre-merge StackSize intact. + if (!sourceIsPlayerOwned && !sourceStack.IsDestroyed && sourceStack.StackSize > 0) + enqueueCreateObject(sourceStack); + } + } +}