acdream/docs/research/2026-05-12-l2g-slice1-shipped-handoff.md
Erik aba6c9ac7f docs(phys L.2g): slice 1 shipped handoff + B.4 gap discovery + plan-of-record
L.2g slice 1 is CODE-COMPLETE: parser + registry mutator + WorldSession
dispatcher + GameWindow subscriber (4 commits: 2459f28, d538915,
536a608, 108e386). Build clean, 6 new tests pass, baseline-stable
across the full suite. Per-commit + final integration code reviews
all approved.

Visual verification deferred: while running the Holtburg-doorway test,
Phase B.4's outbound Use handler turned out to be unwired. The wire
builders (InteractRequests.BuildUse), classes (SelectionState,
WorldPicker), input-action enums, and keybindings all exist — but
GameWindow.OnInputAction has no case for SelectDblLeft, so the click
silently does nothing. The inbound L.2g chain we just landed can't
fire until something sends an outbound Use.

This commit captures the handoff + reframes next-session work:

  * docs/research/2026-05-12-l2g-slice1-shipped-handoff.md (NEW)
    Full evidence: 4 shipped commits, end-to-end code flow, B.4
    discovery explanation, 4 minor + 1 Important review notes
    (the Important one is a test-coverage gap that the B.4b visual
    test will settle automatically), reproducibility recipe,
    next-session pick.

  * CLAUDE.md
    "Currently in Phase L.2" paragraph: L.2g slice 1 code shipped;
    visual test deferred to B.4b. Next-phase-candidates list:
    L.2g slice 1 (now done) replaced with the B.4b candidate
    pointing at the slice scope.

  * docs/plans/2026-04-29-movement-collision-conformance.md
    L.2g section gains a "Current shipped slice (2026-05-12):" table
    listing the 4 commits.

  * docs/plans/2026-05-12-milestones.md
    M1 phase-list updated: L.2g slice 1 (code) shipped; B.4 renamed
    "B.4 / B.4b" with the gap-discovery note + B.4b shape.

  * docs/ISSUES.md
    New issue #57 (HIGH) for the B.4 interaction-handler gap.
    Promoted to Phase B.4b; will close as
    DONE (promoted to Phase B.4b) when B.4b's design spec lands.

  * Memory file project_interaction_pipeline.md (in personal
    memory dir, not in this commit) updated to reflect reality.

Next session: Phase B.4b (~30-50 LOC, 1-2 subagent dispatches,
~30 min). Subscribe SelectDblLeft -> WorldPicker.Pick ->
InteractRequests.BuildUse -> _liveSession.SendGameMessage. Same
Holtburg-doorway visual test verifies both L.2g slice 1 and B.4b
in one pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:17:05 +02:00

11 KiB

L.2g slice 1 shipped — handoff (code-complete; visual test deferred)

Date: 2026-05-12 evening. Branch: claude/gallant-mestorf-3bf2e3 (ready to merge to main). Predecessors:


TL;DR

L.2g slice 1 code is complete and unit-tested. The four commits land the full inbound SetState (0xF74B) pipeline: parser → WorldSession event → GameWindow handler → ShadowObjectRegistry.UpdatePhysicsState. After this slice, the existing CollisionExemption.ShouldSkip short-circuit (cited at acclient_2013_pseudo_c.txt:276782) honors runtime ETHEREAL flips without any resolver-path edit.

The visual verification at Holtburg's inn doorway is deferred to the next session. Cause: Phase B.4's outbound Use handler turns out to be unwired — clicking on a door silently does nothing because no production code subscribes to the SelectLeft / SelectDblLeft input actions. Without the outbound Use, the server never sees a "open the door" request, so the inbound SetState we just ported never fires.

L.2g slice 1 is the inbound half of the round-trip. Phase B.4b (a small ~30-50 LOC slice) is the outbound half. Both halves are required for the M1 demo target "open the inn door." B.4b is the next session's work.


What shipped on this branch

Commit Subject
2459f28 feat(phys L.2g slice 1): inbound SetState (0xF74B) parser
d538915 feat(phys L.2g slice 1): ShadowObjectRegistry.UpdatePhysicsState
536a608 feat(phys L.2g slice 1): WorldSession dispatches SetState (0xF74B) + hex probe
108e386 feat(phys L.2g slice 1): GameWindow routes SetState + extends [entity-source] log

Plus docs/scaffolding earlier in the session:

  • 2c10dd4 — L.2g design spec + L.2 plan-of-record + milestones + CLAUDE.md updates.
  • 869677b — L.2g slice 1 implementation plan (this doc's companion).

Build: clean. Tests: 6 new tests pass (3 for parser, 3 for registry mutator). Full suite: 1037 pass / 8 pre-existing-baseline fail. No regressions. Per-commit + final integration code reviews all approved.


What the code now does end-to-end

When the server broadcasts a SetState (0xF74B):

  1. ParseWorldSession's dispatcher routes opcode 0xF74B into SetState.TryParse(body), which returns SetState.Parsed(Guid, PhysicsState, InstanceSequence, StateSequence).
  2. Probe (gated on ACDREAM_PROBE_BUILDING=1) — one-shot per session, dumps the first message's body bytes as [setstate-hex] body.len=N first-N-bytes: 4B F7 ... for wire-format confidence.
  3. EventWorldSession.StateUpdated fires with the parsed value.
  4. SubscribeGameWindow.OnLiveStateUpdated (added to the live- session attach block alongside OnLiveVectorUpdated) calls _physicsEngine.ShadowObjects.UpdatePhysicsState(parsed.Guid, parsed.PhysicsState).
  5. MutateShadowObjectRegistry.UpdatePhysicsState walks every per-cell list the entity occupies and rewrites list[i] with { State = newState }.
  6. Per-tick diagnostic (same probe flag) — emits [setstate] guid=0x... state=0x... instSeq=... stateSeq=... for the greppable trail.
  7. Resolver — next physics tick, FindObjCollisions calls CollisionExemption.ShouldSkip(entry.State, entry.Flags, moverState) on the entity. The check is unchanged from L.2d slice 1; it short-circuits when (state & ETHEREAL_PS) != 0 && (state & IGNORE_COLLISIONS_PS) != 0.

Slice 0.5 freebie folded in: all 6 [entity-source] probe-log sites in GameWindow.cs now emit state=0x{state:X8} flags={flags} so ETHEREAL flips are greppable end-to-end from spawn through state change.


Why the visual test is deferred — the B.4 discovery

Before launching the visual test, the user reported that right-click in-client was bound to camera orbit (correctly), and asked whether left-click should open a door. Investigation produced this finding:

Component State
InteractRequests.BuildUse(seq, guid) wire builder implemented + tested
SelectionState, WorldPicker classes exist in source
InputAction.SelectLeft / SelectDblLeft / SelectRight enum defined
KeyBindings: LMB → SelectLeft, LMB-dblclick → SelectDblLeft, RMB → SelectRight wired in KeyBindings.cs:300-320
GameWindow.OnInputAction switch case for Select* missing
Any production caller of SelectionState, WorldPicker, InteractRequests.BuildUse none in src/

The diagnostic line [input] SelectLeft Press fires on LMB-click — the dispatcher knows the action — but nothing downstream listens. The click silently does nothing. The R hotkey similarly does nothing because the corresponding UseSelected case is also absent from the switch.

So the M1 outbound Use path is half-shipped: every component below the handler exists, but the handler that ties them together was never landed (despite a 2026-04-28 memory entry claiming "B.4 shipped"). Phase B.4b is the slice that fixes this.

This is not an L.2g defect. L.2g's code path is correct and unit- tested; it just can't be exercised at runtime until the outbound Use sends a SetState-triggering request to the server.


Open notes from reviews (minor — defer to next polish pass)

The per-commit and final integration code reviews approved every commit. Four observations flagged as Minor that are worth folding into a future polish pass:

  1. SetState.cs "total body size" phrasing diverges from VectorUpdate.cs. New form: "Total body size: 16 bytes (4-byte opcode + 12-byte payload)". Sibling form: "Total body size after opcode: 32 bytes". The new form is more self-documenting, but the spec asked to align with the sibling. Cosmetic.
  2. [setstate-hex] log uses redundant Math.Min(body.Length, 32). Called twice in the same line; could be hoisted to a local. Harmless for a one-shot diagnostic.
  3. WorldSession.cs uses the fully-qualified AcDream.Core.Physics.PhysicsDiagnostics.ProbeBuildingEnabled instead of adding using AcDream.Core.Physics; and using the short form. Every other call site in GameWindow.cs and BSPQuery.cs uses the unqualified form. Style inconsistency.
  4. [setstate] diagnostic emits guid + state as hex but instSeq + stateSeq as decimal. Cosmetic.

One Important review note (worth following up explicitly)

The final integration reviewer flagged: the test UpdatePhysicsState_FlipsEthereal_NextLookupSeesNewBits asserts the cached state changes to 0x4 but does not verify the chain through CollisionExemption.ShouldSkip. That short-circuit requires both ETHEREAL_PS (0x4) AND IGNORE_COLLISIONS_PS (0x10) to be set simultaneously ((state & 0x4) && (state & 0x10)). A state of 0x4 alone does NOT exempt collision. Per the reviewer, ACE's PhysicsObj.cs:787-791 may set both bits when doors open (broadcast value 0x14 or higher) — but this is not verified by the test suite.

The B.4b visual test will settle this definitively: the slice-1 hex-dump probe will capture the real state=0x???????? wire value the first time a door opens. If ACE sends 0x14 or higher, the existing chain works as-is. If ACE sends 0x4 only, we need a tiny adjustment to CollisionExemption.cs (the && would become ||, OR we make the collision exemption fire on ETHEREAL alone, OR we widen the test).

Action for B.4b session: after the door-open visual test, grep the launch log for [setstate-hex] and the [setstate] line that fires on the Use → confirm the state bits ACE actually sends. If 0x4 only, file a tiny L.2g slice 1b to widen CollisionExemption.ShouldSkip or the test's assertion.


Next session

Pick: Phase B.4b — finish the outbound Use handler wiring.

Concretely:

  • Subscribe InputAction.SelectDblLeft in GameWindow.OnInputAction switch.
  • Build a world ray from current mouse position (WorldPicker.BuildRay(mouse, vp, view, proj)).
  • Pick the closest entity (WorldPicker.Pick(ray, entities, cache, skipGuid, maxDist)).
  • Store result in _selection (SelectionState.Set(guid)).
  • Call InteractRequests.BuildUse(seq, guid) + _liveSession.SendGameMessage(body).
  • Probably also subscribe InputAction.SelectLeft for select-without- use (single-click selects; double-click selects + uses).
  • Optionally subscribe InputAction.UseSelected (R hotkey) to send Use on the already-selected guid.
  • Sequence-number management — there's a game-action sequence counter on WorldSession already used by the outbound chat path; reuse it.

Estimate: 30-50 LOC, 1-2 subagent-driven implementations + reviews, ~30 min.

Once B.4b lands, immediately re-run the Holtburg inn doorway visual test with ACDREAM_PROBE_BUILDING=1. Both L.2g slice 1 + B.4b are verified by the same scenario; no separate L.2g visual test needed.


Reproducibility

Same launch recipe as L.2d slice 1 (see CLAUDE.md "Running the client against the live server"). For visual test once B.4b lands:

$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"
$env:ACDREAM_PROBE_RESOLVE  = "1"
dotnet run --project src\AcDream.App\AcDream.App.csproj -c Debug 2>&1 |
  Tee-Object -FilePath "launch-l2g+b4b.log"

Then walk into the Holtburg inn doorway, double-left-click the door, wait for the swing animation, walk through. After 30s, watch the auto-close fire.

After closing the client:

Select-String -Path launch-l2g+b4b.log -Pattern "setstate-hex|setstate.*guid|entity-source.*Door|input.*SelectDblLeft"

Expected matches:

  • One [setstate-hex] body.len=16 ... line (confirms holtburger's 12-byte payload).
  • One [entity-source] name=Door ... state=0x00000000 flags=None ... at spawn.
  • An [input] SelectDblLeft Press when you double-click.
  • A [setstate] guid=0x000F... state=0x???????? after the door opens.
  • A second [setstate] guid=0x000F... state=0x00000000 ~30s later when auto-close fires.

Worktree state at handoff

  • Branch claude/gallant-mestorf-3bf2e3 ready to merge to main.
  • 6 commits ahead of main: 2c10dd4 (spec + docs), 869677b (plan), 2459f28 / d538915 / 536a608 / 108e386 (L.2g slice 1 code).
  • One launch.log artifact (launch-l2g-slice1.log) in the working tree from the attempted visual test — not committed (gitignored or transient). Safe to discard; B.4b will produce a fresh log.

User wants to start a fresh session for B.4b.