From 86440ff04ab09e1ac413829a670baa4f31b0181b Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 14 May 2026 14:35:52 +0200 Subject: [PATCH 1/7] docs(B.5): fresh-session handoff for BuildPickUp + ground-item interaction Captures post-B.4c state, click-NPC investigation findings (chain already wired via Tell/CommunicationTransientString/etc; verify opportunistically during B.5 visual test), and B.5 scope decisions made in chat before the user requested a session handoff: - Trigger: F-key (SelectionPickUp action, already bound) - Target: requires _selectedGuid (no pick-under-cursor fallback) - Wire opcode 0x0019 (GameAction.PutItemInContainer) - Payload: itemGuid + containerGuid + placement (12 bytes) - Container = _playerServerGuid - Three changes in two existing files (~50 LOC total) Plus carry-overs from B.4c (#61 cycle-boundary flap, #62 PARTSDIAG null-guard), the B.4b ID-translation gotcha pattern to watch for, and the standard ACE session-race tip. Branch `claude/phase-b5-pickup` (renamed from `claude/investigate-npc-click`) is the workspace; the fresh session should start there. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/research/2026-05-13-b5-pickup-handoff.md | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 docs/research/2026-05-13-b5-pickup-handoff.md diff --git a/docs/research/2026-05-13-b5-pickup-handoff.md b/docs/research/2026-05-13-b5-pickup-handoff.md new file mode 100644 index 0000000..5013242 --- /dev/null +++ b/docs/research/2026-05-13-b5-pickup-handoff.md @@ -0,0 +1,235 @@ +# Phase B.5 — BuildPickUp + ground-item interaction — fresh-session handoff + +**Date:** 2026-05-13 evening (after B.4c ship). +**Branch:** `claude/phase-b5-pickup` (renamed from `claude/investigate-npc-click`). +**Worktree:** `C:\Users\erikn\source\repos\acdream\.claude\worktrees\investigate-npc-click` (directory name kept; only the branch was renamed). +**Predecessor on main:** `e7842e0` — `Merge branch 'claude/phase-b4c-door-anim' — Phase B.4c door swing animation`. + +--- + +## TL;DR + +After B.4c shipped (doors visibly swing open/close), three of M1's four +demo targets are met: *walk through Holtburg*, *open the inn door*, and +likely *click an NPC* (per the investigation below; not yet +visual-verified). The remaining target is *pick up an item*, which +needs a new outbound wire builder + F-key handler in `GameWindow`. + +Phase **B.5** is the slice that closes M1's "click + pickup" demo +path. Scope: ~50 LOC across two existing files (no new files). +Implementation pattern mirrors B.4b's outbound Use chain. + +--- + +## Investigation findings (carry forward) + +Before starting B.5 work, the controller agent investigated whether +B.4b's existing `BuildUse` chain already handles "click an NPC". Code +reading produced this answer: **yes, the basic chat-dialogue case +should already work end-to-end with zero new code**. Verify +opportunistically during B.5's visual test by clicking an NPC while +in-world. + +Specifically: + +- **ACE's `Creature.ActOnUse`** at + `references/ACE/Source/ACE.Server/WorldObjects/Creature.cs:334` + defers to `base.OnActivate → EmoteManager.OnUse()`. The emote + manager walks the creature's emote table and emits `Tell`, + `CommunicationTransientString`, `Motion`, and other game events. +- **All those events are already wired** in + `src/AcDream.Core.Net/GameEventWiring.cs`: + - `Tell (0x0027)` → `chat.OnTellReceived` (line 78) + - `CommunicationTransientString (0x028B)` → `chat.OnSystemMessage` (line 83) + - `WeenieError / WeenieErrorWithString` → `chat.OnSystemMessage` (lines 139, 144) + Plus `UpdateMotion (0xF74D)` is already routed for creature entities + via `OnLiveMotionUpdated`. +- **`UseDone (0x01C7)`** — the completion ack — has a parser at + `GameEvents.ParseUseDone` but is **not registered** with the + dispatcher. Silent drop. Harmless for the basic demo (the chat + events arrive independently), but worth filing as a follow-up if not + picked up by B.5. + +**Conclusion:** click-NPC chain is wired; no code change needed for the +M1 demo target 3 acceptance. Verify in-world during B.5's launch. + +--- + +## B.5 scope (decisions already made) + +| Decision | Value | Rationale | +|---|---|---| +| Trigger | F-key (`InputAction.SelectionPickUp`) | Already bound at `KeyBindings.cs:172` | +| Target selection | Requires `_selectedGuid` (B.4b's renamed field) | Mirrors retail F-key behavior + B.4b's `UseSelected` pattern. User single-clicks the ground item to select, then F to pickup. | +| Wire opcode | `GameAction.PutItemInContainer (0x0019)` | ACE source: `references/ACE/Source/ACE.Server/Network/GameAction/Actions/GameActionPutItemInContainer.cs` | +| Wire payload | 12 bytes: `itemGuid (u32) + containerGuid (u32) + placement (i32)` | Same source | +| Container destination | The player's own server guid (`_playerServerGuid`) | Single-bag pickup; bag-specific destinations are M2+ work | +| Placement value | 0 (let server pick slot) | Simplest; placement-control UI is M2+ | +| Visual feedback | Toast + `[pickup]` log line | No inventory UI yet; the existing `WieldObject` / `InventoryPutObjInContainer` server events already update `ItemRepository` so the state is correct internally | +| Pick under cursor fallback | **NO** | Out of scope per user decision. Strict select-first UX. | + +Brainstorm explicitly **NOT** done with the user (interrupted before +the design sections were presented). The new session should re-confirm +these decisions are still desired before writing the spec — or just +proceed if they remain obviously right. + +--- + +## Three changes B.5 needs to land + +1. **`src/AcDream.Core.Net/Messages/InteractRequests.cs`** — add + `BuildPickUp(uint gameActionSequence, uint itemGuid, uint containerGuid, int placement)`. + Pattern: same as the existing `BuildUseWithTarget` builder at line + 51 of that file. 20-byte total body (`0xF7B1 envelope + seq + opcode + 0x0019 + 12-byte payload`). + +2. **`src/AcDream.App/Rendering/GameWindow.cs`** — add a private helper + `SendPickUp(uint itemGuid)`: + - Gate on `_liveSession?.CurrentState == InWorld` (same pattern as + B.4b's `SendUse`). + - `seq = _liveSession.NextGameActionSequence()`. + - `body = InteractRequests.BuildPickUp(seq, itemGuid, _playerServerGuid, 0)`. + - `_liveSession.SendGameAction(body)`. + - Diagnostic: `Console.WriteLine($"[pickup] item=0x{itemGuid:X8} container=0x{_playerServerGuid:X8} seq={seq}")`. + +3. **`src/AcDream.App/Rendering/GameWindow.cs` `OnInputAction` switch** + — add `case InputAction.SelectionPickUp:` near the other `Select*` / + `UseSelected` cases (B.4b added those around line 8633+). Body: + `if (_selectedGuid is uint sel) SendPickUp(sel); else _debugVm?.AddToast("Nothing selected");`. + +That's the whole code change. ~50 LOC including diagnostics. + +--- + +## Likely ID-translation gotcha (the L.2g slice 1c pattern) + +B.4b's L.2g slice 1c surfaced an ID-space mismatch: the **`BuildUse`** +wire builder takes a `targetGuid` which is `entity.ServerGuid`, but +`ShadowObjectRegistry` keys by `entity.Id`. For `BuildPickUp`: + +- `itemGuid` argument must be `entity.ServerGuid` (the server's + identifier — ACE looks it up in its world). ✅ B.4b's picker returns + `ServerGuid`, so `_selectedGuid` already carries the right value. +- `containerGuid` argument must be `_playerServerGuid` (the server's + identifier for the player). ✅ Already a ServerGuid in `GameWindow`. + +So B.5 should NOT hit the same ID-mismatch trap L.2g slice 1c did. But +re-check at implementation time. + +--- + +## ACE inbound chain (already wired) + +After ACE processes a `BuildPickUp`, it broadcasts: + +- `0x019B InventoryPutObjInContainer` — moves the item record into the + player's container. Already wired to + `ItemRepository.MoveItem(itemGuid, containerGuid, placement)` at + `GameEventWiring.cs:239`. +- `RemoveObject` for the world-spawned item — already wired (existing + despawn path removes the ground item from view). +- Possibly `WieldObject` if the item auto-equips — already wired + (`GameEventWiring.cs:231`). + +No new inbound wiring needed for the minimum demo. The user will see: + +1. Click ground item → selection updates. +2. Press F → diagnostic logs, packet sent. +3. ACE processes; sends inventory + despawn events. +4. Item disappears from ground. +5. (No inventory UI yet, but item is in `ItemRepository`.) + +--- + +## Acceptance criteria + +- [ ] `dotnet build` green. +- [ ] `dotnet test` green: 1046 / 8 pre-existing-baseline fail + (unchanged from main HEAD). +- [ ] At Holtburg, drop a test item on the ground (via `/drop` server + command or have ACE spawn one for the test character), then: + - [ ] Single-click the item — `_selectedGuid` updates, B.4b's + `[pick]` diagnostic shows the item's guid. + - [ ] Press F — log shows `[pickup] item=0x... container=0x5000000A + seq=N`. + - [ ] Item disappears from the ground. + - [ ] No regressions on door interaction (B.4b/B.4c still work). +- [ ] **Bonus: click-NPC verification.** While in-world, single-click + an NPC and press F (or double-click). Expected: NPC chat appears in + the chat panel. If it does → M1 demo target 3 confirmed met. If not + → file the gap. +- [ ] `docs/ISSUES.md` closure entry for whatever issue (if any) was + filed for the pickup gap. +- [ ] Roadmap + CLAUDE.md updated. + +--- + +## Reproducibility + +Same launch recipe as B.4c. Per CLAUDE.md "Logout-before-reconnect", +wait 20-45s between client launches to let ACE clear stale sessions. + +```powershell +Get-Process -Name AcDream.App -ErrorAction SilentlyContinue | Stop-Process -Force +Start-Sleep -Seconds 20 + +$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call" +$env:ACDREAM_LIVE = "1" +$env:ACDREAM_TEST_HOST = "127.0.0.1" +$env:ACDREAM_TEST_PORT = "9000" +$env:ACDREAM_TEST_USER = "testaccount" +$env:ACDREAM_TEST_PASS = "testpassword" +$env:ACDREAM_DEVTOOLS = "1" +$env:ACDREAM_PROBE_BUILDING = "1" +dotnet run --project src\AcDream.App\AcDream.App.csproj -c Debug 2>&1 | + Tee-Object -FilePath "launch-b5.log" +``` + +Log grep: + +```powershell +Select-String -Path launch-b5.log -Pattern "pickup|\[pick\] guid=|UseDone|\[B.4b\] pick" +``` + +--- + +## Carry-overs from B.4c (don't lose track) + +- **#61** — AnimationSequencer link→cycle frame-0 flash on door swing. + Visible as brief flap at end of swing animation. Low-severity polish. +- **#62** — PARTSDIAG null-guard for sequencer-driven entities. + Latent; not currently reachable for doors. One-line fix. +- **Worktree at `.claude/worktrees/phase-b4c-door-anim`** still on disk + (submodules blocked `git worktree remove` per B.4b precedent). Manual + cleanup after this session: `rm -rf` the directory + `git worktree + prune` + `git branch -D claude/phase-b4c-door-anim`. + +--- + +## State at handoff + +- **Branch:** `claude/phase-b5-pickup` (renamed from + `claude/investigate-npc-click`). +- **Worktree directory:** `.claude/worktrees/investigate-npc-click` + (cosmetic mismatch with branch name; harmless). +- **Commits ahead of main:** 1 after this handoff lands. +- **Main HEAD:** `e7842e0`. +- **Build state:** worktree compiles cleanly (verified via + `dotnet build -c Debug`). Tests at 1046/8 baseline. +- **Submodule state:** `references/WorldBuilder` initialized. + `references/ACE` NOT initialized in this worktree — use the main + repo's `references/ACE` for ACE source reads, or init via + `git submodule update --init --depth=1 references/ACE` if extensive + reading is needed. + +--- + +## Why a fresh session + +This session accumulated ~10 hours of context across L.2g, B.4b, and +B.4c — the working set is large enough that starting B.5 cold lets the +new session work with a clean context budget and avoids the compaction +risk that hit the prior B.4b session. + +The prompt for the new session is in the controller's reply that +created this handoff (the chat message immediately after this commit). From e8a20f26c75bace7a1ebd42cea1f725647cac4f7 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 14 May 2026 15:01:24 +0200 Subject: [PATCH 2/7] =?UTF-8?q?feat(B.5):=20InteractRequests.BuildPickUp?= =?UTF-8?q?=20=E2=80=94=20PutItemInContainer=200x0019?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TDD: failing test first (CS0117 on BuildPickUp + PutItemInContainerOpcode), then implementation. Wire layout matches ACE GameActionPutItemInContainer: 0xF7B1 envelope + seq + 0x0019 opcode + itemGuid + containerGuid + placement (24 bytes). For F-key ground-pickup, caller passes player's server guid as containerGuid; Task 2 (GameWindow wiring) will handle that dispatch. Co-Authored-By: Claude Sonnet 4.6 --- .../Messages/InteractRequests.cs | 33 +++++++++++++++++++ .../Messages/InteractRequestsTests.cs | 24 ++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/AcDream.Core.Net/Messages/InteractRequests.cs b/src/AcDream.Core.Net/Messages/InteractRequests.cs index d9cfe7b..68b3b1e 100644 --- a/src/AcDream.Core.Net/Messages/InteractRequests.cs +++ b/src/AcDream.Core.Net/Messages/InteractRequests.cs @@ -29,6 +29,7 @@ public static class InteractRequests public const uint UseOpcode = 0x0036u; public const uint UseWithTargetOpcode = 0x0035u; public const uint TeleToLifestoneOpcode = 0x0063u; + public const uint PutItemInContainerOpcode = 0x0019u; /// /// Use an object: click a door, loot a corpse, talk to an NPC, @@ -73,4 +74,36 @@ public static class InteractRequests BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), TeleToLifestoneOpcode); return body; } + + /// + /// Pick up a ground item or move an item between containers. The + /// server places the item in at + /// the given slot (pass 0 to let the + /// server choose). For F-key ground-pickup, pass the player's own + /// server guid as . + /// + /// + /// Wire layout (ACE GameActionPutItemInContainer.Handle): + /// + /// u32 0xF7B1 + /// u32 gameActionSequence + /// u32 0x0019 // PutItemInContainer + /// u32 itemGuid // server guid of the item + /// u32 containerGuid // destination container (player or bag) + /// i32 placement // 0 = server picks slot + /// + /// + /// + public static byte[] BuildPickUp( + uint gameActionSequence, uint itemGuid, uint containerGuid, int placement) + { + byte[] body = new byte[24]; + BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope); + BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), gameActionSequence); + BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), PutItemInContainerOpcode); + BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), itemGuid); + BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(16), containerGuid); + BinaryPrimitives.WriteInt32LittleEndian (body.AsSpan(20), placement); + return body; + } } diff --git a/tests/AcDream.Core.Net.Tests/Messages/InteractRequestsTests.cs b/tests/AcDream.Core.Net.Tests/Messages/InteractRequestsTests.cs index 5e99b5d..f251cf4 100644 --- a/tests/AcDream.Core.Net.Tests/Messages/InteractRequestsTests.cs +++ b/tests/AcDream.Core.Net.Tests/Messages/InteractRequestsTests.cs @@ -42,4 +42,28 @@ public sealed class InteractRequestsTests Assert.Equal(InteractRequests.TeleToLifestoneOpcode, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8))); } + + [Fact] + public void BuildPickUp_WritesOpcode0x0019AndPayload() + { + byte[] body = InteractRequests.BuildPickUp( + gameActionSequence: 5, + itemGuid: 0xABCDu, + containerGuid: 0x5000000Au, + placement: 0); + + Assert.Equal(24, body.Length); + Assert.Equal(InteractRequests.GameActionEnvelope, + BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(0))); + Assert.Equal(5u, + BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(4))); + Assert.Equal(InteractRequests.PutItemInContainerOpcode, + BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8))); + Assert.Equal(0xABCDu, + BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12))); + Assert.Equal(0x5000000Au, + BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(16))); + Assert.Equal(0, + BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(20))); + } } From ced1b85c619a65a9de210acb2d3d150616b88457 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 14 May 2026 15:05:07 +0200 Subject: [PATCH 3/7] test(B.5): exercise i32 sign-correctness for BuildPickUp.placement The original test only used placement=0, which encodes identically under WriteInt32 and WriteUInt32. Add a -1 case so a future regression to the unsigned writer would actually fail the test. Flagged by Task 1 code review. --- .../Messages/InteractRequestsTests.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/AcDream.Core.Net.Tests/Messages/InteractRequestsTests.cs b/tests/AcDream.Core.Net.Tests/Messages/InteractRequestsTests.cs index f251cf4..c36e54f 100644 --- a/tests/AcDream.Core.Net.Tests/Messages/InteractRequestsTests.cs +++ b/tests/AcDream.Core.Net.Tests/Messages/InteractRequestsTests.cs @@ -66,4 +66,22 @@ public sealed class InteractRequestsTests Assert.Equal(0, BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(20))); } + + [Fact] + public void BuildPickUp_NegativePlacement_WritesSignedLittleEndian() + { + // Sign-correctness guard: placement is i32 on the wire (ACE + // GameActionPutItemInContainer.Handle reads ReadInt32). A + // placement=0 test would pass even if the builder used + // WriteUInt32, so we also exercise a negative value where the + // unsigned/signed encodings would diverge. + byte[] body = InteractRequests.BuildPickUp( + gameActionSequence: 1, + itemGuid: 0x1u, + containerGuid: 0x2u, + placement: -1); + + Assert.Equal(-1, + BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(20))); + } } From 54d9bb9d8dd43ee19505d005eedfb7b60df10dd5 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 14 May 2026 15:07:00 +0200 Subject: [PATCH 4/7] feat(B.5): SendPickUp helper + F-key SelectionPickUp wiring --- src/AcDream.App/Rendering/GameWindow.cs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index ecd3bda..c6fc8e8 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -8746,6 +8746,13 @@ public sealed class GameWindow : IDisposable UseCurrentSelection(); break; + case AcDream.UI.Abstractions.Input.InputAction.SelectionPickUp: + if (_selectedGuid is uint pickupTarget) + SendPickUp(pickupTarget); + else + _debugVm?.AddToast("Nothing selected"); + break; + case AcDream.UI.Abstractions.Input.InputAction.EscapeKey: if (_cameraController?.IsFlyMode == true) _cameraController.ToggleFly(); // exit fly, release cursor @@ -8881,6 +8888,21 @@ public sealed class GameWindow : IDisposable Console.WriteLine($"[B.4b] use guid=0x{guid:X8} seq={seq}"); } + private void SendPickUp(uint itemGuid) + { + if (_liveSession is null + || _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld) + { + _debugVm?.AddToast("Not in world"); + return; + } + var seq = _liveSession.NextGameActionSequence(); + var body = AcDream.Core.Net.Messages.InteractRequests.BuildPickUp( + seq, itemGuid, _playerServerGuid, placement: 0); + _liveSession.SendGameAction(body); + Console.WriteLine($"[B.5] pickup item=0x{itemGuid:X8} container=0x{_playerServerGuid:X8} seq={seq}"); + } + private uint? SelectClosestCombatTarget(bool showToast) { if (!_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var playerEntity)) From 5c24f6cafe3cd8929cf30ca376092de8ffc7ccd5 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 14 May 2026 15:10:23 +0200 Subject: [PATCH 5/7] docs(B.5): implementation plan from writing-plans skill --- .../plans/2026-05-14-phase-b5-pickup.md | 319 ++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-14-phase-b5-pickup.md diff --git a/docs/superpowers/plans/2026-05-14-phase-b5-pickup.md b/docs/superpowers/plans/2026-05-14-phase-b5-pickup.md new file mode 100644 index 0000000..32d22a6 --- /dev/null +++ b/docs/superpowers/plans/2026-05-14-phase-b5-pickup.md @@ -0,0 +1,319 @@ +# Phase B.5 — BuildPickUp + ground-item interaction — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Close M1 demo target 4/4 — make F-key pick up the currently-selected ground item by sending ACE's `PutItemInContainer (0x0019)` GameAction. + +**Architecture:** Mirror B.4b's outbound `BuildUse` chain. Add one new wire-builder (`InteractRequests.BuildPickUp`) and one new GameWindow helper (`SendPickUp`); wire the F-key action into the existing `OnInputAction` switch using the existing `_selectedGuid` field. The ACE inbound handlers (`InventoryPutObjInContainer 0x019B`, `RemoveObject`) are already wired in `GameEventWiring.cs` and will despawn the ground item once ACE acknowledges the pickup — no inbound work needed. + +**Tech Stack:** C# / .NET 10 / xUnit (existing). Wire format: `BinaryPrimitives.WriteUInt32LittleEndian` little-endian envelope same as every other `0xF7B1` GameAction in the file. + +--- + +## Predecessor & branch state + +- **Branch:** `claude/phase-b5-pickup` in worktree `.claude/worktrees/investigate-npc-click`. +- **Main HEAD at start:** `e7842e0` — Merge B.4c. +- **Existing commits on branch:** `86440ff` — the B.5 handoff doc (`docs/research/2026-05-13-b5-pickup-handoff.md`). + +--- + +## Wire format (verified against ACE source) + +`references/ACE/Source/ACE.Server/Network/GameAction/Actions/GameActionPutItemInContainer.cs`: + +```csharp +var itemGuid = message.Payload.ReadUInt32(); +var containerGuid = message.Payload.ReadUInt32(); +var placement = message.Payload.ReadInt32(); +``` + +`references/ACE/Source/ACE.Server/Network/GameAction/GameActionType.cs:13`: +``` +PutItemInContainer = 0x0019, +``` + +Therefore the full **24-byte** GameAction body is: + +| Offset | Field | Bytes | +|---|---|---| +| 0 | `0xF7B1` (GameAction envelope) | 4 | +| 4 | `gameActionSequence` | 4 | +| 8 | `0x0019` (PutItemInContainer subopcode) | 4 | +| 12 | `itemGuid` (u32, the server guid of the ground item) | 4 | +| 16 | `containerGuid` (u32, the player's server guid) | 4 | +| 20 | `placement` (i32, 0 = let server choose slot) | 4 | + +**NB:** The handoff doc (`2026-05-13-b5-pickup-handoff.md`) said "20-byte total body." That was an arithmetic error in the handoff — corrected here. + +--- + +## File structure (which files touched) + +- **Modify:** `src/AcDream.Core.Net/Messages/InteractRequests.cs` — add `PutItemInContainerOpcode` constant + `BuildPickUp(seq, itemGuid, containerGuid, placement)` builder. +- **Modify:** `tests/AcDream.Core.Net.Tests/Messages/InteractRequestsTests.cs` — add a unit test for `BuildPickUp` covering byte layout + opcode. +- **Modify:** `src/AcDream.App/Rendering/GameWindow.cs` — add private `SendPickUp(uint itemGuid)` helper next to `SendUse`; add `case InputAction.SelectionPickUp` in the `OnInputAction` switch. + +No new files. No tests for the GameWindow helper (it's a thin pass-through wrapper around `_liveSession.SendGameAction` mirroring `SendUse`, which itself has no unit test for the same reason). + +--- + +## Task 1: TDD — InteractRequests.BuildPickUp + +**Files:** +- Modify: `tests/AcDream.Core.Net.Tests/Messages/InteractRequestsTests.cs` +- Modify: `src/AcDream.Core.Net/Messages/InteractRequests.cs` + +- [ ] **Step 1: Write the failing test.** Append this new `[Fact]` immediately after `BuildUseWithTarget_WritesBothGuids` in `tests/AcDream.Core.Net.Tests/Messages/InteractRequestsTests.cs`: + +```csharp + [Fact] + public void BuildPickUp_WritesOpcode0x0019AndPayload() + { + byte[] body = InteractRequests.BuildPickUp( + gameActionSequence: 5, + itemGuid: 0xABCDu, + containerGuid: 0x5000000Au, + placement: 0); + + Assert.Equal(24, body.Length); + Assert.Equal(InteractRequests.GameActionEnvelope, + BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(0))); + Assert.Equal(5u, + BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(4))); + Assert.Equal(InteractRequests.PutItemInContainerOpcode, + BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8))); + Assert.Equal(0xABCDu, + BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12))); + Assert.Equal(0x5000000Au, + BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(16))); + Assert.Equal(0, + BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(20))); + } +``` + +- [ ] **Step 2: Run test to verify it fails.** + +```powershell +dotnet test tests/AcDream.Core.Net.Tests/AcDream.Core.Net.Tests.csproj ` + --filter "FullyQualifiedName~InteractRequestsTests.BuildPickUp" +``` + +Expected: build fails with `CS0117: 'InteractRequests' does not contain a definition for 'BuildPickUp'` (and a second error for `PutItemInContainerOpcode`). + +- [ ] **Step 3: Add the constant + builder.** Edit `src/AcDream.Core.Net/Messages/InteractRequests.cs`. Add this opcode constant immediately after the existing `TeleToLifestoneOpcode` declaration (~line 31): + +```csharp + public const uint PutItemInContainerOpcode = 0x0019u; +``` + +Then append the new builder method immediately after `BuildTeleToLifestone` (~line 75, just before the closing `}` of the class). Use the same `XmlDoc + BinaryPrimitives` style as the existing builders: + +```csharp + /// + /// Pick up a ground item or move an item between containers. The + /// server places the item in at + /// the given slot (pass 0 to let the + /// server choose). For F-key ground-pickup, pass the player's own + /// server guid as . + /// + /// + /// Wire layout (ACE GameActionPutItemInContainer.Handle): + /// + /// u32 0xF7B1 + /// u32 gameActionSequence + /// u32 0x0019 // PutItemInContainer + /// u32 itemGuid // server guid of the item + /// u32 containerGuid // destination container (player or bag) + /// i32 placement // 0 = server picks slot + /// + /// + /// + public static byte[] BuildPickUp( + uint gameActionSequence, uint itemGuid, uint containerGuid, int placement) + { + byte[] body = new byte[24]; + BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope); + BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), gameActionSequence); + BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), PutItemInContainerOpcode); + BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), itemGuid); + BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(16), containerGuid); + BinaryPrimitives.WriteInt32LittleEndian (body.AsSpan(20), placement); + return body; + } +``` + +- [ ] **Step 4: Run test to verify it passes.** + +```powershell +dotnet test tests/AcDream.Core.Net.Tests/AcDream.Core.Net.Tests.csproj ` + --filter "FullyQualifiedName~InteractRequestsTests.BuildPickUp" +``` + +Expected: 1 passing, 0 failing. + +- [ ] **Step 5: Commit.** + +```powershell +git add src/AcDream.Core.Net/Messages/InteractRequests.cs ` + tests/AcDream.Core.Net.Tests/Messages/InteractRequestsTests.cs +git commit -m "feat(B.5): InteractRequests.BuildPickUp — PutItemInContainer 0x0019" +``` + +--- + +## Task 2: GameWindow integration — SendPickUp + OnInputAction case + +**Files:** +- Modify: `src/AcDream.App/Rendering/GameWindow.cs` + +This task does NOT include new tests. `SendPickUp` is a 6-line passthrough that gates on `InWorld` state and routes to `_liveSession.SendGameAction`. It mirrors `SendUse` (also untested by unit tests) and is verified end-to-end via the in-world visual check in Task 3. + +- [ ] **Step 1: Add the `SendPickUp` helper.** Locate `SendUse` (currently around line 8870 in `src/AcDream.App/Rendering/GameWindow.cs`). Insert this new helper immediately after `SendUse`'s closing brace: + +```csharp + private void SendPickUp(uint itemGuid) + { + if (_liveSession is null + || _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld) + { + _debugVm?.AddToast("Not in world"); + return; + } + var seq = _liveSession.NextGameActionSequence(); + var body = AcDream.Core.Net.Messages.InteractRequests.BuildPickUp( + seq, itemGuid, _playerServerGuid, placement: 0); + _liveSession.SendGameAction(body); + Console.WriteLine($"[B.5] pickup item=0x{itemGuid:X8} container=0x{_playerServerGuid:X8} seq={seq}"); + } +``` + +- [ ] **Step 2: Wire the F-key action.** Locate the `OnInputAction` switch's `case InputAction.UseSelected` (currently around line 8745). Insert a new case immediately after it: + +```csharp + case AcDream.UI.Abstractions.Input.InputAction.SelectionPickUp: + if (_selectedGuid is uint pickupTarget) + SendPickUp(pickupTarget); + else + _debugVm?.AddToast("Nothing selected"); + break; +``` + +- [ ] **Step 3: Build the whole solution.** + +```powershell +dotnet build -c Debug +``` + +Expected: build succeeds with zero errors. (Existing warnings unchanged.) + +- [ ] **Step 4: Run the full test suite.** + +```powershell +dotnet test -c Debug --nologo +``` + +Expected: 1047 passing, 8 failing (8 pre-existing baseline failures from main HEAD; same count as before B.5). The new `BuildPickUp_WritesOpcode0x0019AndPayload` test contributes +1 passing. + +- [ ] **Step 5: Commit.** + +```powershell +git add src/AcDream.App/Rendering/GameWindow.cs +git commit -m "feat(B.5): SendPickUp helper + F-key SelectionPickUp wiring" +``` + +--- + +## Task 3: Visual verification in live client + +**Not a code task — user-driven acceptance test.** Run the launch recipe, drop a test item, click-then-F. + +- [ ] **Step 1: Kill stale client + wait for ACE session cleanup.** + +```powershell +Get-Process -Name AcDream.App -ErrorAction SilentlyContinue | Stop-Process -Force +Start-Sleep -Seconds 20 +``` + +- [ ] **Step 2: Launch the client (background).** + +```powershell +$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call" +$env:ACDREAM_LIVE = "1" +$env:ACDREAM_TEST_HOST = "127.0.0.1" +$env:ACDREAM_TEST_PORT = "9000" +$env:ACDREAM_TEST_USER = "testaccount" +$env:ACDREAM_TEST_PASS = "testpassword" +$env:ACDREAM_DEVTOOLS = "1" +dotnet run --project src\AcDream.App\AcDream.App.csproj -c Debug 2>&1 | + Tee-Object -FilePath "launch-b5.log" +``` + +- [ ] **Step 3: User-driven test scenario.** Hand off to user for visual verification: + + 1. ACE drops a test item near `+Acdream` (server `/drop` slash command, or whatever ACE supports for the testaccount). Specify what item type was dropped in the verification report. + 2. **Single-click the item** — `[B.4b] pick guid=0x` should appear in the log. The selection toast should show the item name. + 3. **Press F** — `[B.5] pickup item=0x container=0x5000000A seq=` should appear in the log. + 4. **Item should disappear from the ground** (ACE acks → `RemoveObject` → existing despawn path removes it from view). + 5. **No regressions on door interaction:** double-click the inn door, observe it swings open as in B.4c. + 6. **Bonus: NPC chat check.** Click an NPC, observe the chat panel. If NPC dialogue appears → M1 demo target 3 confirmed met. If silent → file an issue. + +- [ ] **Step 4: Grep the log for evidence.** + +```powershell +Select-String -Path launch-b5.log -Pattern "\[B\.5\] pickup|\[B\.4b\] pick|UseDone|InventoryPutObjInContainer|RemoveObject" +``` + +Expected: at least one `[B.5] pickup` line and a subsequent `RemoveObject` for the same guid. + +--- + +## Task 4: Ship handoff + docs + merge + +- [ ] **Step 1: Write the ship handoff doc** at `docs/research/2026-05-14-b5-shipped-handoff.md`. Follow the B.4c handoff structure: TL;DR, three-commit table (TDD builder + GameWindow integration + handoff itself), wire-format evidence, visual-verification evidence (with launch log excerpt), open issues carried forward (#61 #62 from B.4c), what shipped, what was left for later. + +- [ ] **Step 2: Update `docs/plans/2026-04-11-roadmap.md`** — move Phase B.5 from "Next phase candidates" into the shipped-table with the merge SHA placeholder + link to the handoff doc. + +- [ ] **Step 3: Update `CLAUDE.md`** — update the "Currently in Phase L.2" paragraph's M1 demo status from "3 of 4 met" to "4 of 4 met", reference the B.5 handoff, and add a fresh "Next phase candidates" list (chronic-issue triage, Phase C visual fidelity, N.6 slice 2, etc.). + +- [ ] **Step 4: Update `docs/ISSUES.md`** — if any new issues surfaced during visual verification, file them. If the click-NPC bonus check succeeded, note it in the recent-progress section. + +- [ ] **Step 5: Commit docs.** + +```powershell +git add docs/research/2026-05-14-b5-shipped-handoff.md ` + docs/plans/2026-04-11-roadmap.md ` + CLAUDE.md ` + docs/ISSUES.md +git commit -m "docs(B.5): ship handoff + roadmap/CLAUDE update + M1 4/4 met" +``` + +- [ ] **Step 6: Merge to main.** + +```powershell +git checkout main +git merge --no-ff claude/phase-b5-pickup -m "Merge branch 'claude/phase-b5-pickup' — Phase B.5 pickup" +``` + +- [ ] **Step 7: Optional worktree cleanup.** (Per B.4c precedent, submodules block `git worktree remove`; do `git worktree prune` after manually deleting the directory if disk pressure warrants. Otherwise skip — the directory is small.) + +--- + +## Acceptance criteria summary + +- [ ] `dotnet build -c Debug` green. +- [ ] `dotnet test -c Debug` shows +1 new passing (the `BuildPickUp_WritesOpcode0x0019AndPayload` test). +- [ ] Total pass count = baseline + 1; failure count unchanged (8 pre-existing). +- [ ] Visual: click ground item → F → log shows `[B.5] pickup ...` → item disappears. +- [ ] No regression on B.4c door interaction (double-click inn door still swings). +- [ ] Bonus: click NPC → chat appears in chat panel (or filed as a follow-up issue). +- [ ] Branch merged into main with non-fast-forward merge commit. + +--- + +## Carry-overs from B.4c (do not lose track) + +- **#61** — AnimationSequencer link→cycle frame-0 flash. Low severity. Not blocking M1 demo. +- **#62** — PARTSDIAG null-guard. Latent (not reachable for doors currently). One-line fix. + +Neither blocks B.5. Address before recording the M1 demo video if the door-swing flap is distracting on tape. From f7636a9e78449447b6d4f45c2ca22d55b93eabbf Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 14 May 2026 16:13:16 +0200 Subject: [PATCH 6/7] fix(B.5): handle PickupEvent 0xF74A so picked-up items despawn locally ACE sends GameMessagePickupEvent (opcode 0xF74A) instead of GameMessageDeleteObject (0xF747) for items removed via player pickup (Player_Tracking.RemoveTrackedObject with fromPickup=true). Without this handler, BuildPickUp succeeded server-side (item moved into the player's container, retail observers saw it disappear), but our local client kept rendering it on the ground because the despawn message went to the unhandled-opcode bucket. PickupEvent's wire body adds an objectPositionSequence field on top of DeleteObject's layout, so the parser is its own type. The downstream view-removal semantics are identical to DeleteObject, so the dispatcher routes both opcodes into the same EntityDeleted event via a small adapter. --- src/AcDream.Core.Net/Messages/PickupEvent.cs | 48 +++++++++++++++++++ src/AcDream.Core.Net/WorldSession.cs | 13 +++++ .../Messages/PickupEventTests.cs | 41 ++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 src/AcDream.Core.Net/Messages/PickupEvent.cs create mode 100644 tests/AcDream.Core.Net.Tests/Messages/PickupEventTests.cs diff --git a/src/AcDream.Core.Net/Messages/PickupEvent.cs b/src/AcDream.Core.Net/Messages/PickupEvent.cs new file mode 100644 index 0000000..44ff95d --- /dev/null +++ b/src/AcDream.Core.Net/Messages/PickupEvent.cs @@ -0,0 +1,48 @@ +using System.Buffers.Binary; + +namespace AcDream.Core.Net.Messages; + +/// +/// Inbound PickupEvent GameMessage (opcode 0xF74A). +/// +/// +/// ACE emits this from Player_Tracking.RemoveTrackedObject(wo, fromPickup: true) +/// when a player picks up a world item — distinguishes the despawn +/// from a generic 0xF747 DeleteObject (timeout / death / +/// out-of-LOS). Downstream effect on the client view is the same +/// (remove the entity from the world), so +/// routes both opcodes to the same EntityDeleted event. +/// +/// +/// +/// Wire layout (ACE GameMessagePickupEvent.cs): +/// +/// u32 0xF74A +/// u32 guid +/// u16 objectInstanceSequence +/// u16 objectPositionSequence +/// +/// +/// +public static class PickupEvent +{ + public const uint Opcode = 0xF74Au; + + public readonly record struct Parsed( + uint Guid, ushort InstanceSequence, ushort PositionSequence); + + public static Parsed? TryParse(ReadOnlySpan body) + { + if (body.Length < 12) + return null; + + uint opcode = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(0, 4)); + if (opcode != Opcode) + return null; + + uint guid = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(4, 4)); + ushort instanceSequence = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(8, 2)); + ushort positionSequence = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(10, 2)); + return new Parsed(guid, instanceSequence, positionSequence); + } +} diff --git a/src/AcDream.Core.Net/WorldSession.cs b/src/AcDream.Core.Net/WorldSession.cs index 85a571a..2e644c6 100644 --- a/src/AcDream.Core.Net/WorldSession.cs +++ b/src/AcDream.Core.Net/WorldSession.cs @@ -712,6 +712,19 @@ public sealed class WorldSession : IDisposable if (parsed is not null) EntityDeleted?.Invoke(parsed.Value); } + else if (op == PickupEvent.Opcode) + { + // ACE sends PickupEvent (0xF74A) instead of DeleteObject + // when a player picks up a world item (Player_Tracking + // .RemoveTrackedObject with fromPickup=true). Downstream + // view-removal semantics are identical, so we adapt to + // DeleteObject.Parsed and reuse the existing handler. + var parsed = PickupEvent.TryParse(body); + if (parsed is not null) + EntityDeleted?.Invoke( + new DeleteObject.Parsed( + parsed.Value.Guid, parsed.Value.InstanceSequence)); + } else if (op == UpdateMotion.Opcode) { // Phase 6.6: the server sends UpdateMotion (0xF74C) whenever an diff --git a/tests/AcDream.Core.Net.Tests/Messages/PickupEventTests.cs b/tests/AcDream.Core.Net.Tests/Messages/PickupEventTests.cs new file mode 100644 index 0000000..e6248ec --- /dev/null +++ b/tests/AcDream.Core.Net.Tests/Messages/PickupEventTests.cs @@ -0,0 +1,41 @@ +using System.Buffers.Binary; +using AcDream.Core.Net.Messages; +using Xunit; + +namespace AcDream.Core.Net.Tests.Messages; + +public sealed class PickupEventTests +{ + [Fact] + public void RejectsWrongOpcode() + { + Span body = stackalloc byte[12]; + BinaryPrimitives.WriteUInt32LittleEndian(body, 0xDEADBEEFu); + + Assert.Null(PickupEvent.TryParse(body)); + } + + [Fact] + public void RejectsTruncated() + { + Assert.Null(PickupEvent.TryParse(ReadOnlySpan.Empty)); + Assert.Null(PickupEvent.TryParse(new byte[11])); + } + + [Fact] + public void ParsesGuidAndSequences() + { + Span body = stackalloc byte[12]; + BinaryPrimitives.WriteUInt32LittleEndian(body, PickupEvent.Opcode); + BinaryPrimitives.WriteUInt32LittleEndian(body.Slice(4), 0x80000727u); + BinaryPrimitives.WriteUInt16LittleEndian(body.Slice(8), 0x1234); + BinaryPrimitives.WriteUInt16LittleEndian(body.Slice(10), 0x5678); + + var parsed = PickupEvent.TryParse(body); + + Assert.NotNull(parsed); + Assert.Equal(0x80000727u, parsed!.Value.Guid); + Assert.Equal((ushort)0x1234, parsed.Value.InstanceSequence); + Assert.Equal((ushort)0x5678, parsed.Value.PositionSequence); + } +} From d132fcccfbf8a2181be8b9a28d0985638829036b Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 14 May 2026 16:23:20 +0200 Subject: [PATCH 7/7] docs(B.5): ship handoff + roadmap/CLAUDE update + file #63 #64 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase B.5 (ground-item pickup, close-range path) shipped and visual-verified 2026-05-14 at Holtburg. M1 demo target 4/4 ("pick up an item") met. New ship-handoff doc captures the 5-commit history including the post-visual-test PickupEvent (0xF74A) wire-handler fix that closes the local-despawn gap. Roadmap and CLAUDE.md updated to reflect the ship + the new follow-up issues: - #63 (MEDIUM) — server-initiated MoveToObject auto-walk not honored; blocks double-click pickup + out-of-range F. Filed as candidate Phase B.6. holtburger has the reference implementation. - #64 (LOW) — local-player pickup animation does not render (retail observers see it correctly). Likely a self-echo filter dropping UpdateMotion(Pickup) on the local player. Carry-overs from B.4c (#61 link-cycle flash, #62 PARTSDIAG null-guard) unchanged. --- CLAUDE.md | 45 ++-- docs/ISSUES.md | 83 ++++++ docs/plans/2026-04-11-roadmap.md | 1 + .../research/2026-05-14-b5-shipped-handoff.md | 252 ++++++++++++++++++ 4 files changed, 364 insertions(+), 17 deletions(-) create mode 100644 docs/research/2026-05-14-b5-shipped-handoff.md diff --git a/CLAUDE.md b/CLAUDE.md index a7e4ec7..f32642b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -619,19 +619,26 @@ acdream's plan lives in two files committed to the repo: **Currently in Phase L.2 (Movement & Collision Conformance).** L.2a slices 1+2+3 + L.2d slice 1+1.5 + L.2g slice 1 + L.2g slice 1b + L.2g slice 1c + -**Phase B.4b** + **Phase B.4c** all shipped and visual-verified 2026-05-13. -The M1 demo target *"open the inn door"* is met **with full visual feedback**: -double-click a door in the Holtburg inn doorway → `WorldPicker.Pick` finds -the door entity → `BuildUse` sends `0xF7B1/0x0036` to ACE → ACE broadcasts -`SetState (0xF74B)` with `ETHEREAL` bit → `ShadowObjectRegistry.UpdatePhysicsState` -(L.2g slice 1) mutates the cached state (via fixed ServerGuid→entity.Id -translation, L.2g slice 1c) → `CollisionExemption.ShouldSkip` exempts on -ETHEREAL-alone (L.2g slice 1b) → player walks through → door swing animation -plays (B.4c: spawn-time `AnimationSequencer` registration + `OnLiveMotionUpdated` -routing for door entities). Issue #57 (B.4 handler gap) is closed. Issue #58 -(door swing animation) is closed by B.4c. Issues #61 (link→cycle boundary -flash) and #62 (PARTSDIAG null-guard) are filed as M1-deferred polish. +**Phase B.4b** + **Phase B.4c** all shipped and visual-verified 2026-05-13; +**Phase B.5** (ground-item pickup, F-key) shipped and visual-verified +2026-05-14. The M1 demo target *"pick up an item"* is met for the +close-range path — single-click a ground item to select, walk within +~0.6 m of it, press F, and the item is removed from the world and added +to the player's inventory. Wire chain: `InteractRequests.BuildPickUp` +sends `PutItemInContainer (0xF7B1/0x0019)`; ACE despawns the item with +`GameMessagePickupEvent (0xF74A)` (NOT `0xF747 DeleteObject` — the +distinction surfaced during visual testing and is fixed by the new +`PickupEvent.cs` parser routed through the shared `EntityDeleted` +event). The M1 demo target *"open the inn door"* remains met from B.4b ++ B.4c. Issue #57 (B.4 handler gap) is closed. Issue #58 (door swing +animation) is closed by B.4c. Issues #61 (link→cycle boundary flash), +#62 (PARTSDIAG null-guard), **#63 (server-initiated MoveToObject +auto-walk not honored — blocks out-of-range pickup / Use)**, and **#64 +(local-player pickup animation does not render)** are filed as +M1-deferred follow-up. +**B.5 ship handoff:** [`docs/research/2026-05-14-b5-shipped-handoff.md`](docs/research/2026-05-14-b5-shipped-handoff.md) +— full evidence for the 5 commits across InteractRequests / GameWindow / WorldSession + the bonus `PickupEvent (0xF74A)` wire-handler fix that closes the despawn gap. **B.4c ship handoff:** [`docs/research/2026-05-13-b4c-shipped-handoff.md`](docs/research/2026-05-13-b4c-shipped-handoff.md) — full evidence for the 4 commits + 2 bonus discoveries (stance-value wrong `0x01` vs `0x3D` causing underground doors; link→cycle boundary flash). @@ -725,11 +732,15 @@ project. packets, wire the handlers; if it is silent, investigate ACE's NPC handler configuration. ~30 min spike, outcome determines whether NPC interaction needs a full phase or is a one-commit fix. -- **Phase B.5 — Ground item pickup (F key) (M1 critical path).** The - `SelectionPickUp` input action + F-key binding exist in `KeyBindings` but - `OnInputAction` has no case for it. `BuildUse` IS `BuildPickUp` (same wire - format). One-commit addition: add `SelectionPickUp` case to `GameWindow. - OnInputAction` → call `InteractRequests.BuildPickUp(seq, _selectedGuid)`. ~30 min. +- **Phase B.6 — Client-side MoveToObject auto-walk handling (closes #63).** + ACE auto-walks the player to out-of-range Use / Pickup targets via + `CreateMoveToChain` + `EnqueueBroadcastMotion(MoveToObject)`, but our client + doesn't honor the inbound motion broadcast — character drifts toward the + target and snaps back, ACE's chain times out. Reference implementation + exists in `references/holtburger/crates/holtburger-core/src/client/simulation.rs` + (the `approximate_move_to_object_projection_target` + `MoveToObject` case). + Unlocks double-click pickup, F-key pickup from any distance, Use on + out-of-range NPCs / corpses. Probably 1-2 commits + visual verification. - **Triage the chronic open-issue list** in `docs/ISSUES.md` — #2 (lightning), #4 (sky horizon-glow), #28 (aurora), #29 (cloud thinness), #37 (humanoid coat), #41 (remote-motion blips) have been open since April/early-May and diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 966f401..b8265ca 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -46,6 +46,89 @@ Copy this block when adding a new issue: # Active issues +## #64 — Local-player pickup animation does not render + +**Status:** OPEN +**Severity:** LOW (visual feedback only — pickup completes correctly) +**Filed:** 2026-05-14 (B.5 visual verification) +**Component:** motion / animation routing for local player + +**Description:** When `+Acdream` picks up an item (B.5 close-range +path), retail observers see the character play the pickup animation +correctly, but the local view shows no pickup animation. The item +despawns, the inventory updates, but the character's own +bend-down-and-grab animation is missing. + +**Root cause / hypothesis:** ACE broadcasts `Motion(MotionCommand.Pickup)` +via `Player_Inventory.AddPickupChainToMoveToChain` (line 711–713, +`EnqueueBroadcastMotion(motion)`), which arrives as a normal +`UpdateMotion (0xF74D)` packet. Retail observers route it through +their remote-creature animation pipeline and render the pickup. For +the local player, our `OnLiveMotionUpdated` likely filters self-echoes +(local player drives its own motion via prediction, not server +echoes) and drops the pickup motion. The pickup is a one-shot +animation initiated by the server, so the prediction path has no +trigger — and the echo path is filtered. + +**Acceptance:** When `+Acdream` picks up an item, the local view shows +the same pickup animation retail observers see. Probably resolved by +either (a) admitting server-initiated one-shot motions through the +local-player motion filter, or (b) generating the pickup animation +locally on send (mirroring retail's client behavior). + +**Files:** `src/AcDream.App/Rendering/GameWindow.cs` `OnLiveMotionUpdated` +(motion routing); the self-echo filter is somewhere along this path. + +**Estimated scope:** Small-to-medium. Mostly investigation + +1–2 commits. + +--- + +## #63 — Server-initiated auto-walk (MoveToObject) not honored + +**Status:** OPEN +**Severity:** MEDIUM (blocks out-of-range Use + Pickup; close-range +works fine) +**Filed:** 2026-05-14 (B.5 visual verification) +**Component:** motion / inbound MoveToObject handling + +**Description:** When the player triggers a Use or PutItemInContainer +on a target outside ACE's `WithinUseRadius` (default 0.6 m), ACE +runs server-side auto-walk via `CreateMoveToChain` → +`PhysicsObj.MoveToObject` + `EnqueueBroadcastMotion(Motion(MoveToObject, target))`. +Our client receives the `UpdateMotion(MoveToObject)` broadcast for +the player but doesn't honor it: the character either visually +drifts a bit toward the target and snaps back, or just stands still. +ACE's MoveToChain then times out, the `success: false` path +broadcasts `InventoryServerSaveFailed (ActionCancelled)`, and the +pickup/use never completes. + +**User-visible symptom:** Double-click a ground item from any +distance, or F-key it from > 0.6 m: character partially walks toward +the item, then flips back to original position. No pickup. + +**Reference:** [holtburger simulation.rs:33–41 + 178–191](references/holtburger/crates/holtburger-core/src/client/simulation.rs) +already implements client-side `MoveToObject` motion projection + +auto-walk handling. That's the shape of the fix. + +**Root cause:** Our `OnLiveMotionUpdated` has no handler for the +`MoveToObject` motion type; the broadcast is silently dropped. + +**Acceptance:** Double-click a ground item from 2–5 m away. Character +auto-walks to within use radius, ACE's MoveToChain confirms success, +pickup completes (including the existing PickupEvent despawn). Same +behavior for Use on out-of-range NPCs. + +**Files:** `src/AcDream.App/Rendering/GameWindow.cs` `OnLiveMotionUpdated` +(routing); likely a new `MoveToObjectMotion` handler in the motion / +prediction layer + a server-acked position-update echo so ACE sees the +player has reached the target. + +**Estimated scope:** Medium. Probably its own phase (B.6 or similar); +not a one-commit fix. Compose from holtburger's pattern. + +--- + ## #62 — PARTSDIAG null-guard for sequencer-driven entities **Status:** OPEN diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index 233e18f..c2e403c 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -67,6 +67,7 @@ | C.1.5a | Portal PES wiring — server-spawned `WorldEntity` entities now fire their `Setup.DefaultScript` through the already-shipped `PhysicsScriptRunner` on enter-world. New ~70-line [`EntityScriptActivator`](../../src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs) class wires into `GpuWorldState`'s spawn lifecycle (`AppendLiveEntity` → `OnCreate`, `RemoveEntityByServerGuid` → `OnRemove`). Resolver lambda in `GameWindow` hits `_dats.Get(...)?.DefaultScript.DataId` with defensive try/catch returning `0u` on miss. Activator also seeds `_particleSink.SetEntityRotation` so hook offsets transform from entity-local to world space correctly. **Verified at the Holtburg Town network portal**: 10-hook portal script fires end-to-end with correct color, persistence, orientation, multi-emitter dispatch. **Known limitation surfaced and filed as issue #56**: `ParticleHookSink` ignores `CreateParticleHook.PartIndex`, so the 10 emitters collapse to one root position instead of distributing across the portal Setup's parts — visually produces a compressed, partly-ground-buried swirl. Mechanism is correct; per-part transform handling is the next vfx-pipeline work (blocks slice 2 visual delight; affects every multi-emitter PES). Spec: [`docs/superpowers/specs/2026-05-12-phase-c1.5a-portals-design.md`](../superpowers/specs/2026-05-12-phase-c1.5a-portals-design.md). Plan: [`docs/superpowers/plans/2026-05-12-phase-c1.5a-portals.md`](../superpowers/plans/2026-05-12-phase-c1.5a-portals.md). | Live ✓ (with #56) | | B.4b | Outbound Use handler wiring + 4 bonus fixes (L.2g slices 1b+1c, double-click detection, DoubleClick gate fix). Shipped 2026-05-13 (branch `claude/compassionate-wilson-23ff99`, merge pending). Closes #57. Files #58 (door swing animation, M1-deferred). `WorldPicker.BuildRay` + `Pick` (ray-sphere entity pick with inside-sphere guard); `GameWindow.OnInputAction` switch cases for `SelectLeft` / `SelectDblLeft` / `UseSelected`; `_entitiesByServerGuid` reverse-lookup dict + ServerGuid→entity.Id translation in `OnLiveStateUpdated` (L.2g slice 1c — THE actual blocker); `InputDispatcher` double-click detection 500ms threshold (binding was dead code without it); `CollisionExemption.ShouldSkip` widened to ETHEREAL-alone (ACE Door.Open() sends `state=0x0001000C`, not `0x14`). M1 demo target "open the inn door" verified at Holtburg inn doorway. Plan: [`docs/superpowers/plans/2026-05-13-phase-b4b-plan.md`](../superpowers/plans/2026-05-13-phase-b4b-plan.md). Handoff: [`docs/research/2026-05-13-b4b-shipped-handoff.md`](../research/2026-05-13-b4b-shipped-handoff.md). | Live ✓ | | B.4c | Door swing animation. Shipped 2026-05-13 (branch `claude/phase-b4c-door-anim`, merge pending). Closes #58. Files #61 (AnimationSequencer link→cycle boundary flash; low-severity polish) + #62 (PARTSDIAG null-guard; latent). Spawn-time `AnimationSequencer` registration for door entities in `GameWindow.OnLiveEntitySpawnedLocked`: initial cycle seeded from `spawn.PhysicsState` (Off for closed, On for open). Shared `IsDoorName` / `IsDoorSpawn` helpers. `[door-cycle]` diagnostic in `OnLiveMotionUpdated` (gated on `ACDREAM_PROBE_BUILDING`). Bonus stance-value fix: `NonCombat = 0x3D` not `0x01` (wrong value caused doors to render halfway underground via empty sequencer frames). Visual-verified 2026-05-13 at Holtburg inn doorway: swing-open + swing-close cycles both play. M1 demo target "open the inn door" now has full visual feedback. Plan: [`docs/superpowers/plans/2026-05-13-phase-b4c-plan.md`](../superpowers/plans/2026-05-13-phase-b4c-plan.md). Handoff: [`docs/research/2026-05-13-b4c-shipped-handoff.md`](../research/2026-05-13-b4c-shipped-handoff.md). | Live ✓ | +| B.5 | Ground-item pickup (F-key, close-range path). Shipped 2026-05-14 (branch `claude/phase-b5-pickup`, merge pending). Closes M1 demo target 4/4 *"pick up an item"*. New `InteractRequests.BuildPickUp(seq, itemGuid, containerGuid, placement)` builds the 24-byte `PutItemInContainer (0xF7B1/0x0019)` wire body verified against `references/ACE/Source/ACE.Server/Network/GameAction/Actions/GameActionPutItemInContainer.cs`. New private `GameWindow.SendPickUp(uint itemGuid)` helper mirrors `SendUse`'s gate-on-InWorld pattern; `case InputAction.SelectionPickUp` in `OnInputAction` switch routes the F-key through `_selectedGuid`. **Bonus wire-handler fix (Task 2b):** ACE despawns picked-up items via `GameMessagePickupEvent (0xF74A)`, not the `GameMessageDeleteObject (0xF747)` we already handled — surfaced during visual testing (item kept rendering on ground after successful server-side pickup). New `PickupEvent.cs` parser + `WorldSession` dispatch branch adapt to `DeleteObject.Parsed` and reuse the existing `EntityDeleted → OnLiveEntityDeleted → RemoveLiveEntityByServerGuid` chain. Files #63 (server-initiated `MoveToObject` auto-walk not honored — out-of-range pickup / double-click fails server-side timeout) + #64 (local-player pickup animation does not render). Visual-verified 2026-05-14 at Holtburg: 3 successful close-range pickups (Pink Taper + Violet Tapers), item despawns locally as ACE acks. Plan: [`docs/superpowers/plans/2026-05-14-phase-b5-pickup.md`](../superpowers/plans/2026-05-14-phase-b5-pickup.md). Handoff: [`docs/research/2026-05-14-b5-shipped-handoff.md`](../research/2026-05-14-b5-shipped-handoff.md). | Live ✓ | | C.1.5b | Per-part PES transforms + dat-hydrated entity DefaultScript dispatch. Closes issue #56. Shipped 2026-05-12 across 5 commits (`1e3c33b` docs+plan, `f3bc15e` SetupPartTransforms helper, `11521f4` ParticleHookSink applies `CreateParticleHook.PartIndex`, `5ca5827` activator refactor + GameWindow resolver lambda, `8735c39` GpuWorldState 4 new fire-sites). **Slice A** — new [`SetupPartTransforms.Compute(setup)`](../../src/AcDream.Core/Meshing/SetupPartTransforms.cs) walks `PlacementFrames[Resting]` → `[Default]` → first-available (mirrors `SetupMesh.Flatten` priority) and returns `Matrix4x4` per part; new `ParticleHookSink.SetEntityPartTransforms(entityId, partTransforms)` mirrors the existing `_rotationByEntity` pattern; `SpawnFromHook` now transforms hook offset through `partTransforms[partIndex]` before applying entity rotation. **Slice B** — activator's `ServerGuid==0` guard relaxed: keys by `entity.ServerGuid` when non-zero, else `entity.Id` (collision-free with server guids in the `0x40xxxxxx` interior / `0x80xxxxxx` scenery / `0xC0xxxxxx` ranges). Resolver delegate refactored to return `ScriptActivationInfo(ScriptId, PartTransforms)` so one dat lookup yields both pieces. `GpuWorldState` fires the activator from 4 new sites: `AddLandblock` + `AddEntitiesToExistingLandblock` (Far→Near promotion) for OnCreate, `RemoveLandblock` + `RemoveEntitiesFromLandblock` (Near→Far demotion) for OnRemove. ServerGuid==0 filter on AddLandblock avoids double-firing pending-bucket merges. **Reality discovery folded into spec §3**: EnvCell `StaticObjects` are already hydrated as `WorldEntity` instances by `GameWindow.BuildInteriorEntitiesForStreaming` (with stable `entity.Id` in `0x40xxxxxx`) — no synthetic-ID scheme or separate walker class needed (handoff §4 Q1/Q2 mooted). **Visual verification 2026-05-12**: Holtburg Town network portal swirl distributes across the arch (no ground-burial), Inn fireplace flames render over the firebox, cottage chimney smoke columns render, spell-cast animation-hook particles all match retail. 18 new + 4 updated tests, all Vfx/Meshing/Streaming/Activator green. Spec: [`docs/superpowers/specs/2026-05-13-phase-c1.5b-design.md`](../superpowers/specs/2026-05-13-phase-c1.5b-design.md). Plan: [`docs/superpowers/plans/2026-05-13-phase-c1.5b.md`](../superpowers/plans/2026-05-13-phase-c1.5b.md). | Live ✓ | Plus polish that doesn't get its own phase number: diff --git a/docs/research/2026-05-14-b5-shipped-handoff.md b/docs/research/2026-05-14-b5-shipped-handoff.md new file mode 100644 index 0000000..eca0c2d --- /dev/null +++ b/docs/research/2026-05-14-b5-shipped-handoff.md @@ -0,0 +1,252 @@ +# Phase B.5 shipped — handoff (visual-verified 2026-05-14) + +**Date:** 2026-05-14. +**Branch:** `claude/phase-b5-pickup` (ready to merge to main; controller handles the merge after this doc lands). +**Predecessors:** +- [docs/research/2026-05-13-b4c-shipped-handoff.md](2026-05-13-b4c-shipped-handoff.md) — B.4c (door swing) shipped immediately before. +- [docs/research/2026-05-13-b5-pickup-handoff.md](2026-05-13-b5-pickup-handoff.md) — fresh-session handoff that scoped this phase. +- [docs/superpowers/plans/2026-05-14-phase-b5-pickup.md](../superpowers/plans/2026-05-14-phase-b5-pickup.md) — implementation plan (2 tasks). + +--- + +## TL;DR + +Phase B.5 **shipped end-to-end and is visual-verified 2026-05-14.** The +M1 demo target *"pick up an item"* is met for the close-range path — +single-click a ground item to select, walk within ~0.6 m of it, press +F, and the item is removed from the world and added to the player's +inventory. + +The plan budgeted 2 implementation tasks (~50 LOC). Visual testing +surfaced **one wire-handler gap** that became Task 2b: ACE despawns +picked-up items via `GameMessagePickupEvent (0xF74A)`, not the +`GameMessageDeleteObject (0xF747)` we already handled — without that +fix the pickup succeeded server-side but the item kept rendering on +the ground locally. Caught and fixed in the same session. + +Two known gaps remain, filed as issues for follow-up: +- **#63 (MEDIUM)** — Server-initiated auto-walk for out-of-range Use / + PickUp not honored. Double-click a ground item from > 0.6 m and the + character partially walks then snaps back; ACE's `MoveToChain` times + out. This is a separate motion-handling phase, not a B.5 regression. +- **#64 (LOW)** — Local-player pickup animation doesn't render + (retail observers see it correctly; local view is silent). Likely a + self-echo filter dropping `UpdateMotion(Pickup)` on the local player. + +--- + +## What shipped on this branch + +| # | Commit | Subject | Task | +|---|---|---|---| +| 1 | `e8a20f2` | `feat(B.5): InteractRequests.BuildPickUp — PutItemInContainer 0x0019` | Task 1 | +| 2 | `ced1b85` | `test(B.5): exercise i32 sign-correctness for BuildPickUp.placement` | Task 1 code-review fix | +| 3 | `54d9bb9` | `feat(B.5): SendPickUp helper + F-key SelectionPickUp wiring` | Task 2 | +| 4 | `5c24f6c` | `docs(B.5): implementation plan from writing-plans skill` | Plan doc | +| 5 | `f7636a9` | `fix(B.5): handle PickupEvent 0xF74A so picked-up items despawn locally` | Task 2b (post-visual-test fix) | + +Plus the predecessor handoff (`86440ff`) that started the branch. + +**Build:** clean. +**Tests:** `dotnet test -c Debug` shows AcDream.Core.Net.Tests 290/290 +passing (was 287 at branch start; +3 from Task 2b's PickupEvent tests; +the two BuildPickUp tests landed inside the same project's existing +file). Failure count unchanged at 8 pre-existing baseline in +AcDream.Core.Tests. + +--- + +## What the code does end-to-end + +**Outbound (Tasks 1 & 2):** + +1. User single-clicks a ground item near `+Acdream`. + `case InputAction.SelectLeft → PickAndStoreSelection(useImmediately: false)` + runs B.4b's `WorldPicker.Pick`, finds the item, sets `_selectedGuid`. + Log: `[B.4b] pick guid=0x… name=…`. +2. User presses F. + `case InputAction.SelectionPickUp → SendPickUp(_selectedGuid)` builds + the wire body via `InteractRequests.BuildPickUp(seq, itemGuid, + _playerServerGuid, placement: 0)` and posts it through + `_liveSession.SendGameAction`. Log: `[B.5] pickup item=… container=… seq=…`. +3. Wire layout (24 bytes): `0xF7B1 envelope | seq | 0x0019 opcode | + itemGuid u32 | containerGuid u32 | placement i32`. Verified against + `references/ACE/Source/ACE.Server/Network/GameAction/Actions/GameActionPutItemInContainer.cs`. + +**Inbound (Task 2b — surfaced during visual test):** + +4. ACE runs `HandleActionPutItemInContainer`. If the player is within + `WithinUseRadius` (~0.6 m), the close-range branch in + `CreateMoveToChain` skips the auto-walk and runs the pickup chain + directly: server-side `Landblock.RemoveWorldObject(item.Guid, + adjacencyMove: false, fromPickup: true)` → per-player + `Player_Tracking.RemoveTrackedObject(wo, fromPickup: true)` → + broadcast `GameMessagePickupEvent (0xF74A)` to all observers. +5. Our `WorldSession.Dispatch` now routes `0xF74A` (in addition to + `0xF747 DeleteObject`) through the shared `EntityDeleted` event, + adapting the `PickupEvent.Parsed` to a `DeleteObject.Parsed` so + `OnLiveEntityDeleted → RemoveLiveEntityByServerGuid` runs unchanged. + The item disappears from the local view. + +--- + +## Wire-handler gap (Task 2b) + +ACE distinguishes two despawn opcodes: +- `0xF747 GameMessageDeleteObject` — "object is gone" (timeout / death / + out-of-LOS). Our existing handler. +- `0xF74A GameMessagePickupEvent` — "object was picked up by a player." + Sent by `Player_Tracking.RemoveTrackedObject(wo, fromPickup: true)`. + +Both are functionally identical from the client's view (remove the +entity from the world), but only one was handled. Wire format adds +one `u16 objectPositionSequence` field over DeleteObject's layout, so +`PickupEvent.cs` is its own parser; the dispatcher adapts to +`DeleteObject.Parsed` for the downstream consumer. + +This is exactly the kind of trap CLAUDE.md's reference-repo discipline +exists to prevent — the handoff spec said "the existing despawn path +removes the ground item from view," which was *almost* true. Took one +visual-verification round-trip to surface, ten minutes to fix with a +clean wire parser + 3 new unit tests. + +--- + +## Visual verification — what was observed + +**Test scenario:** ACE dropped a Pink Taper, then a Violet Taper, then +two more tapers near `+Acdream` at Holtburg. Player walked up close, +single-clicked, pressed F. Three pickups completed in the post-fix +log: items `0x80000725`, `0x8000072A`, `0x80000729`. + +**Before Task 2b:** Server-side pickup succeeded — `[B.5] pickup … +seq=46` in log; retail observer saw item disappear from world. Local +view still rendered the item on the ground. + +**After Task 2b:** Item disappears locally as soon as ACE acks the +pickup. Three successful close-range pickups recorded in the log. + +**Door-interaction regression check (B.4c carry-forward):** Not +explicitly re-tested this session; no code path touched by B.5 +affects door interaction. + +**Click-NPC bonus (M1 demo target 3 verification):** Not visually +verified this session — log shows `[B.4b] use guid=… name=Novedion +the Gem Seller seq=…` from B.4c testing but ACE response not +re-confirmed here. Carry-forward to next session. + +--- + +## What did NOT work (and why it's not B.5's bug) + +1. **Double-click on a ground item from any distance, or F from > 0.6 m.** + ACE auto-walks the player toward the item (`CreateMoveToChain` → + `PhysicsObj.MoveToObject` + `EnqueueBroadcastMotion(MoveToObject)`), + but our client doesn't handle inbound `MoveToObject` motion broadcasts. + ACE's MoveToChain times out, the chain's `success: false` path sends + `InventoryServerSaveFailed (ActionCancelled)`, and the pickup never + completes. Visible as "character drifts toward item then flips back." + **Filed as #63.** Out of B.5's stated scope (which was: select-first + + F-key wire chain). holtburger's `simulation.rs` has the reference + implementation; would be its own phase (B.6 or similar). + +2. **Local-player pickup animation doesn't render.** Retail observers + see `+Acdream` play the bend-down-and-grab animation; our local view + shows nothing. ACE broadcasts `Motion(MotionCommand.Pickup)` via + `EnqueueBroadcastMotion`, our motion routing probably filters + self-echoes for the local player (motion is normally predicted + locally, not echoed from server). Server-initiated one-shot motions + like Pickup have no local prediction trigger, so they're dropped. + **Filed as #64.** Visual feedback gap only; pickup completes + correctly. + +Both are well-defined follow-up work; neither blocks M1. + +--- + +## Carry-overs from B.4c + +Both pre-existed B.5; neither was touched. + +- **#61** — AnimationSequencer link→cycle boundary frame-0 flash on + door swing. Low severity polish. +- **#62** — PARTSDIAG null-guard for sequencer-driven entities. + Latent; not currently reachable for doors. + +--- + +## M1 status after B.5 + +Demo targets: +1. Walk through Holtburg — met (L.2a-d + L.2g shipped earlier) +2. Open the inn door — met (B.4b + B.4c shipped 2026-05-13) +3. Click an NPC — chain wired (B.4b), not visually re-verified this + session +4. Pick up an item — met, close-range path (this phase) + +Outstanding work for the M1 demo recording: +- Optionally re-verify target 3 (NPC click) once and either confirm + met or file a gap. +- Optionally resolve #63 if the demo wants to show double-click / + out-of-range pickup. The close-range path is sufficient for the + scripted demo scenario. +- Carry-overs #61, #62, #64 are polish; do before recording if + visible on tape. + +--- + +## Reproducibility + +```powershell +Get-Process -Name AcDream.App -ErrorAction SilentlyContinue | Stop-Process -Force +Start-Sleep -Seconds 20 + +$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call" +$env:ACDREAM_LIVE = "1" +$env:ACDREAM_TEST_HOST = "127.0.0.1" +$env:ACDREAM_TEST_PORT = "9000" +$env:ACDREAM_TEST_USER = "testaccount" +$env:ACDREAM_TEST_PASS = "testpassword" +$env:ACDREAM_DEVTOOLS = "1" +dotnet run --project src\AcDream.App\AcDream.App.csproj -c Debug 2>&1 | + Tee-Object -FilePath "launch-b5.log" +``` + +Log evidence: + +```powershell +Get-Content launch-b5.log -Encoding Unicode | + Select-String -Pattern "\[B\.5\] pickup|\[B\.4b\] pick" +``` + +Expected: a `[B.5] pickup item=… container=0x5000000A seq=…` line for +each successful F-press, preceded by `[B.4b] pick guid=…` from the +single-click that set the selection. + +--- + +## Files touched this session + +- New: `src/AcDream.Core.Net/Messages/PickupEvent.cs` +- New: `tests/AcDream.Core.Net.Tests/Messages/PickupEventTests.cs` +- New: `docs/superpowers/plans/2026-05-14-phase-b5-pickup.md` +- New: `docs/research/2026-05-14-b5-shipped-handoff.md` (this file) +- Modified: `src/AcDream.Core.Net/Messages/InteractRequests.cs` +- Modified: `src/AcDream.Core.Net/WorldSession.cs` +- Modified: `src/AcDream.App/Rendering/GameWindow.cs` +- Modified: `tests/AcDream.Core.Net.Tests/Messages/InteractRequestsTests.cs` +- Modified: `docs/ISSUES.md` (added #63, #64) + +--- + +## State at handoff + +- **Branch:** `claude/phase-b5-pickup`, 6 commits ahead of `main` + (predecessor handoff + 5 implementation commits + this docs commit + land in the same merge). +- **Main HEAD before merge:** `e7842e0` — Merge B.4c. +- **Build state:** worktree compiles cleanly under `dotnet build -c Debug`. +- **Tests:** baseline + 3 new (PickupEvent) + 2 new (BuildPickUp + + sign-correctness) — failure count unchanged. + +Ready for non-fast-forward merge into `main`.