From 4aebf444d910f8b0e18bfc642974750a49f0c117 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 22 Jun 2026 19:51:20 +0200 Subject: [PATCH] feat(D.2b): wire ConfirmMove (0x0022 echo) + RollbackMove (0x00A0) for optimistic inventory moves Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.Core.Net/GameEventWiring.cs | 17 ++++++++------- .../GameEventWiringTests.cs | 21 +++++++++++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/AcDream.Core.Net/GameEventWiring.cs b/src/AcDream.Core.Net/GameEventWiring.cs index fc425532..8f660948 100644 --- a/src/AcDream.Core.Net/GameEventWiring.cs +++ b/src/AcDream.Core.Net/GameEventWiring.cs @@ -246,8 +246,9 @@ public static class GameEventWiring dispatcher.Register(GameEventType.InventoryPutObjInContainer, e => { var p = GameEvents.ParsePutObjInContainer(e.Payload.Span); - if (p is not null) items.MoveItem(p.Value.ItemGuid, p.Value.ContainerGuid, - newSlot: (int)p.Value.Placement); + if (p is null) return; + items.MoveItem(p.Value.ItemGuid, p.Value.ContainerGuid, newSlot: (int)p.Value.Placement); + items.ConfirmMove(p.Value.ItemGuid); // B-Drag: the server confirmed our optimistic move }); // ViewContents (0x0196) — the server's AUTHORITATIVE full contents list for a container you @@ -273,14 +274,16 @@ public static class GameEventWiring if (guid is not null) items.MoveItem(guid.Value, newContainerId: 0u); }); - // B-Wire: InventoryServerSaveFailed (0x00A0) — rollback of a speculative move. - // acdream does not do speculative moves yet (read-only inventory); parse + log - // so the wire is correct. B-Drag wires the rollback behavior. (Divergence note.) + // B-Drag: InventoryServerSaveFailed (0x00A0) — server rejected an optimistic move. + // Snap the item back to its pre-move slot. Log only when there was no pending move + // (a server-initiated failure on a non-optimistic path). dispatcher.Register(GameEventType.InventoryServerSaveFailed, e => { var p = GameEvents.ParseInventoryServerSaveFailed(e.Payload.Span); - if (p is not null) - Console.WriteLine($"[B-Wire] InventoryServerSaveFailed guid=0x{p.Value.ItemGuid:X8} err=0x{p.Value.WeenieError:X}"); + if (p is null) return; + // B-Drag: the server rejected an optimistic move — snap the item back to its pre-move slot. + if (!items.RollbackMove(p.Value.ItemGuid)) + Console.WriteLine($"[B-Drag] InventoryServerSaveFailed guid=0x{p.Value.ItemGuid:X8} err=0x{p.Value.WeenieError:X} (no pending move)"); }); // B-Wire: CloseGroundContainer (0x0052) — server closed a ground-container diff --git a/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs b/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs index 24d868f3..92d7b471 100644 --- a/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs +++ b/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs @@ -585,6 +585,27 @@ public sealed class GameEventWiringTests d.Dispatch(env!.Value); } + [Fact] + public void WireAll_InventoryServerSaveFailed_RollsBackOptimisticMove() + { + var (d, items, _, _, _) = MakeAll(); + // An item known to be in container 0xC0 at slot 2, then optimistically moved to 0xC1. + items.RecordMembership(0x50000B01u, containerId: 0x500000C0u); + items.MoveItem(0x50000B01u, 0x500000C0u, 2); + items.MoveItemOptimistic(0x50000B01u, 0x500000C1u, 0); + Assert.Equal(0x500000C1u, items.Get(0x50000B01u)!.ContainerId); // moved (optimistic) + + // Server bounces it: InventoryServerSaveFailed (itemGuid, weenieError). + byte[] payload = new byte[8]; + BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(0), 0x50000B01u); + BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), 0x1Au); // some WeenieError + var env = GameEventEnvelope.TryParse(WrapEnvelope(GameEventType.InventoryServerSaveFailed, payload)); + d.Dispatch(env!.Value); + + Assert.Equal(0x500000C0u, items.Get(0x50000B01u)!.ContainerId); // snapped back + Assert.Equal(2, items.Get(0x50000B01u)!.ContainerSlot); + } + [Fact] public void WireAll_InventoryPutObjectIn3D_UnparentsFromContainer() {