Merge branch 'claude/phase-b5-pickup' — Phase B.5 pickup
This commit is contained in:
commit
cf22f9c031
12 changed files with 1117 additions and 17 deletions
45
CLAUDE.md
45
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Setup>(...)?.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:
|
||||
|
|
|
|||
235
docs/research/2026-05-13-b5-pickup-handoff.md
Normal file
235
docs/research/2026-05-13-b5-pickup-handoff.md
Normal file
|
|
@ -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).
|
||||
252
docs/research/2026-05-14-b5-shipped-handoff.md
Normal file
252
docs/research/2026-05-14-b5-shipped-handoff.md
Normal file
|
|
@ -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`.
|
||||
319
docs/superpowers/plans/2026-05-14-phase-b5-pickup.md
Normal file
319
docs/superpowers/plans/2026-05-14-phase-b5-pickup.md
Normal file
|
|
@ -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
|
||||
/// <summary>
|
||||
/// Pick up a ground item or move an item between containers. The
|
||||
/// server places the item in <paramref name="containerGuid"/> at
|
||||
/// the given <paramref name="placement"/> slot (pass 0 to let the
|
||||
/// server choose). For F-key ground-pickup, pass the player's own
|
||||
/// server guid as <paramref name="containerGuid"/>.
|
||||
///
|
||||
/// <para>
|
||||
/// Wire layout (ACE GameActionPutItemInContainer.Handle):
|
||||
/// <code>
|
||||
/// 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
|
||||
/// </code>
|
||||
/// </para>
|
||||
/// </summary>
|
||||
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<ITEM>` should appear in the log. The selection toast should show the item name.
|
||||
3. **Press F** — `[B.5] pickup item=0x<ITEM> container=0x5000000A seq=<N>` 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.
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pick up a ground item or move an item between containers. The
|
||||
/// server places the item in <paramref name="containerGuid"/> at
|
||||
/// the given <paramref name="placement"/> slot (pass 0 to let the
|
||||
/// server choose). For F-key ground-pickup, pass the player's own
|
||||
/// server guid as <paramref name="containerGuid"/>.
|
||||
///
|
||||
/// <para>
|
||||
/// Wire layout (ACE GameActionPutItemInContainer.Handle):
|
||||
/// <code>
|
||||
/// 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
|
||||
/// </code>
|
||||
/// </para>
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
src/AcDream.Core.Net/Messages/PickupEvent.cs
Normal file
48
src/AcDream.Core.Net/Messages/PickupEvent.cs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
using System.Buffers.Binary;
|
||||
|
||||
namespace AcDream.Core.Net.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Inbound <c>PickupEvent</c> GameMessage (opcode <c>0xF74A</c>).
|
||||
///
|
||||
/// <para>
|
||||
/// ACE emits this from <c>Player_Tracking.RemoveTrackedObject(wo, fromPickup: true)</c>
|
||||
/// when a player picks up a world item — distinguishes the despawn
|
||||
/// from a generic <c>0xF747 DeleteObject</c> (timeout / death /
|
||||
/// out-of-LOS). Downstream effect on the client view is the same
|
||||
/// (remove the entity from the world), so <see cref="WorldSession"/>
|
||||
/// routes both opcodes to the same <c>EntityDeleted</c> event.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Wire layout (ACE <c>GameMessagePickupEvent.cs</c>):
|
||||
/// <code>
|
||||
/// u32 0xF74A
|
||||
/// u32 guid
|
||||
/// u16 objectInstanceSequence
|
||||
/// u16 objectPositionSequence
|
||||
/// </code>
|
||||
/// </para>
|
||||
/// </summary>
|
||||
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<byte> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -42,4 +42,46 @@ 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)));
|
||||
}
|
||||
|
||||
[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)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
41
tests/AcDream.Core.Net.Tests/Messages/PickupEventTests.cs
Normal file
41
tests/AcDream.Core.Net.Tests/Messages/PickupEventTests.cs
Normal file
|
|
@ -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<byte> body = stackalloc byte[12];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body, 0xDEADBEEFu);
|
||||
|
||||
Assert.Null(PickupEvent.TryParse(body));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RejectsTruncated()
|
||||
{
|
||||
Assert.Null(PickupEvent.TryParse(ReadOnlySpan<byte>.Empty));
|
||||
Assert.Null(PickupEvent.TryParse(new byte[11]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesGuidAndSequences()
|
||||
{
|
||||
Span<byte> 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue