diff --git a/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs index 296a1059..efa69730 100644 --- a/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/InventoryControllerTests.cs @@ -484,6 +484,36 @@ public class InventoryControllerTests Assert.Equal(Player, objects.Get(0xAu)!.ContainerId); // NOT removed on lift (unlike the toolbar) } + [Fact] + public void OnDragOver_closedBag_unknownCount_advisoryAccepts() // AP-61 + { + var (layout, _, containers, _, _, _, _, _) = BuildLayout(); + var objects = new ClientObjectTable(); + SeedBag(objects, 0xC, slot: 0, itemsCapacity: 24); // a bag with capacity but NO indexed contents (closed) + objects.AddOrUpdate(new ClientObject { ObjectId = 0xFFFFu }); + var ctrl = (IItemListDragHandler)Bind(layout, objects); + + // GetContents(0xC) is empty (a closed bag's contents aren't indexed until opened) → not known-full → accept. + Assert.True(ctrl.OnDragOver(containers, containers.GetItem(0)!, Payload(0xFFFFu))); + } + + [Fact] + public void Drop_thenServerRollback_revertsTheMove() // optimistic + InventoryServerSaveFailed snap-back + { + var (layout, _, containers, _, _, _, _, _) = BuildLayout(); + var objects = new ClientObjectTable(); + SeedContained(objects, 0xA, Player, slot: 3); // item starts in the main pack at slot 3 + SeedBag(objects, 0xC, slot: 0, itemsCapacity: 24); + var ctrl = Bind(layout, objects); + + ((IItemListDragHandler)ctrl).HandleDropRelease(containers, containers.GetItem(0)!, Payload(0xAu)); + Assert.Equal(0xCu, objects.Get(0xAu)!.ContainerId); // moved into the bag optimistically (instant) + + objects.RollbackMove(0xAu); // server rejected (InventoryServerSaveFailed) + Assert.Equal(Player, objects.Get(0xAu)!.ContainerId); // snapped back to the main pack + Assert.Equal(3, objects.Get(0xAu)!.ContainerSlot); // and the original slot + } + // Reads the text of the UiText caption child attached by the controller. private static string CaptionText(UiElement host) { diff --git a/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs b/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs index 92d7b471..ae191438 100644 --- a/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs +++ b/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs @@ -606,6 +606,28 @@ public sealed class GameEventWiringTests Assert.Equal(2, items.Get(0x50000B01u)!.ContainerSlot); } + [Fact] + public void WireAll_InventoryPutObjInContainer_ConfirmsOptimisticMove_soNoRollback() + { + var (d, items, _, _, _) = MakeAll(); + items.RecordMembership(0x50000B02u, containerId: 0x500000C0u); + items.MoveItem(0x50000B02u, 0x500000C0u, 2); + items.MoveItemOptimistic(0x50000B02u, 0x500000C1u, 0); // optimistic move (pending snapshot) + + // Server CONFIRMS via InventoryPutObjInContainer (item, container, placement, containerType). + byte[] payload = new byte[16]; + BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(0), 0x50000B02u); + BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), 0x500000C1u); + BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(8), 0u); // placement + BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(12), 0u); // containerType + var env = GameEventEnvelope.TryParse(WrapEnvelope(GameEventType.InventoryPutObjInContainer, payload)); + d.Dispatch(env!.Value); + + // ConfirmMove cleared the pending snapshot → a later rollback is a no-op (the move stuck). + Assert.False(items.RollbackMove(0x50000B02u)); + Assert.Equal(0x500000C1u, items.Get(0x50000B02u)!.ContainerId); + } + [Fact] public void WireAll_InventoryPutObjectIn3D_UnparentsFromContainer() {