Phase B.4b shipped end-to-end 2026-05-13. Holtburg inn doorway double-click verified: pick -> BuildUse -> ACE SetState reply -> ID-translated registry update -> CollisionExemption exempts -> player walks through. M1 demo target "open the inn door" met. 9 commits on this branch: - Tasks 1-4 per plan (BuildRay, Pick, rename, handler wiring) - 4 bonus visual-test discoveries: * InputDispatcher double-click detection (was dead code) * DoubleClick activation gate fix in OnInputAction * L.2g slice 1b: CollisionExemption widened to ETHEREAL alone * L.2g slice 1c: ServerGuid -> entity.Id translation (silent blocker) Closes #57. Files #58 for door swing animation (UpdateMotion routing for non-creature entities, M1 deferred polish). Updates roadmap and CLAUDE.md Phase L.2 paragraph. Memory file project_interaction_pipeline.md updated outside the repo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
08be296dcd
commit
2c9bdb512b
4 changed files with 501 additions and 92 deletions
108
docs/ISSUES.md
108
docs/ISSUES.md
|
|
@ -46,62 +46,70 @@ Copy this block when adding a new issue:
|
|||
|
||||
# Active issues
|
||||
|
||||
## #57 — B.4 interaction-handler missing: clicking on doors / NPCs / items silently does nothing
|
||||
## #58 — Door swing animation: UpdateMotion not wired for non-creature entities
|
||||
|
||||
**Status:** OPEN
|
||||
**Severity:** HIGH (M1 blocker — demo target *"open the inn door, click an NPC, pick up an item"* is fully blocked)
|
||||
**Severity:** MEDIUM (M1 demo cosmetic — doors function but don't visually animate)
|
||||
**Filed:** 2026-05-13
|
||||
**Component:** animation / `UpdateMotion (0xF74D)` routing for non-creature entities
|
||||
|
||||
**Description:** B.4b shipped end-to-end interaction (click → BuildUse →
|
||||
SetState → collision exempt → walk through). When ACE opens a door it
|
||||
broadcasts TWO packets: `SetState (0xF74B)` (the collision-bit flip,
|
||||
handled by L.2g) AND `UpdateMotion (0xF74D)` with `(NonCombat, On)` (the
|
||||
swing animation cycle, NOT handled). acdream's `UpdateMotion` pipeline is
|
||||
currently scoped to player + creature animation (Phase L.3); door entities
|
||||
do not receive cycle commands.
|
||||
|
||||
**Root cause / status:** The `UpdateMotion` packet handler in
|
||||
`GameWindow.OnLiveMotionUpdated` filters to player + creature entity types.
|
||||
Non-creature WorldEntity instances (doors, chests, etc.) silently drop
|
||||
the `(NonCombat, On)` cycle command that ACE sends when the door opens.
|
||||
|
||||
**Files (likely):**
|
||||
- `src/AcDream.App/Rendering/GameWindow.cs` — `OnLiveMotionUpdated` handler
|
||||
- `src/AcDream.Core/Physics/AnimationSequencer.cs` — may have creature-specific assumptions
|
||||
- The entity-spawn adapter (unknown if non-creature entities are wired to an AnimationSequencer at all)
|
||||
|
||||
**Acceptance:** Double-click a door → swing animation plays → ~30s later the
|
||||
door auto-close animation plays. Log shows `UpdateMotion (NonCombat, On)` processed
|
||||
for the door entity.
|
||||
|
||||
**Estimated scope:** Unknown. Could be quick (route UpdateMotion to non-creature
|
||||
WorldEntity with cycle dispatch, ~30 min) or moderate (AnimationSequencer audit
|
||||
for creature-specific assumptions, ~2 hrs). Spike before committing to estimate.
|
||||
|
||||
---
|
||||
|
||||
## #57 — [DONE 2026-05-13] B.4 interaction-handler missing: clicking on doors / NPCs / items silently does nothing
|
||||
|
||||
**Status:** DONE
|
||||
**Closed:** 2026-05-13
|
||||
**Severity:** HIGH (was M1 blocker)
|
||||
**Filed:** 2026-05-12
|
||||
**Component:** input / interaction / `GameWindow.OnInputAction`
|
||||
|
||||
**Description:** Discovered 2026-05-12 while running the L.2g slice 1
|
||||
visual test. Phase B.4 (2026-04-28) shipped half of itself: the wire-
|
||||
message builders (`InteractRequests.BuildUse` / `BuildUseWithTarget` /
|
||||
`BuildPickUp`), the supporting classes (`SelectionState`, `WorldPicker`),
|
||||
the `InputAction` enum entries (`SelectLeft`, `SelectDblLeft`,
|
||||
`SelectRight`, `SelectDblRight`, `UseSelected`, `SelectionPickUp`, etc.),
|
||||
and the default keybindings (LMB → `SelectLeft`, LMB-dblclick →
|
||||
`SelectDblLeft`, RMB → `SelectRight`, R → `UseSelected`, F →
|
||||
`SelectionPickUp`). What was never shipped: a case for ANY of those
|
||||
actions in `GameWindow.OnInputAction`'s switch. The runtime diagnostic
|
||||
`[input] SelectLeft Press` fires when you click — confirming the
|
||||
dispatcher resolves the chord — but nothing downstream listens, so the
|
||||
click silently does nothing. Neither does double-click, R, or F. The
|
||||
inbound side (`MoveToObjectReceived`, `StateUpdated` after L.2g slice 1)
|
||||
is wired and ready; the block is purely outbound.
|
||||
**Closure:** Closed by Phase B.4b on branch `claude/compassionate-wilson-23ff99`
|
||||
(9 implementation commits, Tasks 1-4 per plan + 4 bonus fixes). The
|
||||
full round-trip — double-click door → `WorldPicker.BuildRay` + `Pick` →
|
||||
`InteractRequests.BuildUse` → ACE `SetState` reply → `ShadowObjectRegistry`
|
||||
mutation (via fixed ServerGuid→entity.Id translation) → `CollisionExemption.ShouldSkip`
|
||||
exempts (widened to ETHEREAL-alone) → player walks through — was
|
||||
visual-verified at the Holtburg inn doorway 2026-05-13. Four bonus
|
||||
discoveries were required beyond the original plan: (1) `InputDispatcher`
|
||||
had no double-click detection, (2) `OnInputAction` gate blocked
|
||||
`DoubleClick` activations, (3) `CollisionExemption` required both
|
||||
ETHEREAL+IGNORE_COLLISIONS while ACE sends only ETHEREAL, (4)
|
||||
`OnLiveStateUpdated` passed server GUID to a local-entity-ID-keyed
|
||||
registry. M1 demo target "open the inn door" met. See
|
||||
[docs/research/2026-05-13-b4b-shipped-handoff.md](research/2026-05-13-b4b-shipped-handoff.md)
|
||||
for full evidence and rationale.
|
||||
|
||||
**Root cause / status:** B.4 handler integration step was evidently
|
||||
dropped or never landed. Memory file
|
||||
`memory/project_interaction_pipeline.md` was updated 2026-05-12 to
|
||||
reflect this reality (previous text claimed shipped).
|
||||
|
||||
**Files:**
|
||||
- `src/AcDream.App/Rendering/GameWindow.cs` — `OnInputAction` switch
|
||||
around line 8546+ has no `Select*` cases.
|
||||
- `src/AcDream.Core.Net/Messages/InteractRequests.cs` — wire builders
|
||||
exist but have zero callers in `src/`.
|
||||
- `src/AcDream.Core/Selection/SelectionState.cs` — class exists, zero
|
||||
production callers.
|
||||
- `src/AcDream.App/Rendering/WorldPicker.cs` — class exists, zero
|
||||
production callers.
|
||||
- `src/AcDream.UI.Abstractions/Input/KeyBindings.cs:300-320` — bindings
|
||||
for `SelectLeft` / `SelectDblLeft` / `SelectRight` / `SelectDblMid`
|
||||
exist.
|
||||
|
||||
**Research:** [docs/research/2026-05-12-l2g-slice1-shipped-handoff.md](research/2026-05-12-l2g-slice1-shipped-handoff.md)
|
||||
"Why the visual test is deferred" section has the full investigation.
|
||||
|
||||
**Acceptance:** Double-left-clicking on a door in the Holtburg inn
|
||||
doorway sends a `0xF7B1 / 0x0036 Use` to the server, the server flips
|
||||
the door's `Ethereal` bit and broadcasts `SetState (0xF74B)`, the
|
||||
L.2g-slice-1 chain mutates `ShadowObjectRegistry`, the
|
||||
`CollisionExemption.ShouldSkip` check honors it, and the player can
|
||||
walk through the doorway. Visual verification + log grep (per the
|
||||
L.2g handoff's reproducibility recipe) both pass.
|
||||
|
||||
**Status promotion:** This is a phase-sized follow-up (estimated
|
||||
30-50 LOC, ~30 min). Promoted to **Phase B.4b** in the L.2 milestone
|
||||
context and the CLAUDE.md "Next phase candidates" list. Will be closed
|
||||
as `DONE (promoted to Phase B.4b)` once that phase's design spec lands.
|
||||
**Files (what shipped):**
|
||||
- `src/AcDream.Core/Selection/WorldPicker.cs` (new; formerly zero callers, now wired)
|
||||
- `src/AcDream.App/Rendering/GameWindow.cs` — `OnInputAction` switch cases for `SelectLeft` / `SelectDblLeft` / `UseSelected`; `OnLiveStateUpdated` ServerGuid→Id translation; `_entitiesByServerGuid` reverse-lookup dict
|
||||
- `src/AcDream.UI.Abstractions/Input/InputDispatcher.cs` — double-click detection
|
||||
- `src/AcDream.Core/Physics/CollisionExemption.cs` — widened to ETHEREAL-alone
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@
|
|||
| N.5b | Terrain on the modern rendering path — `TerrainModernRenderer` replaces `TerrainChunkRenderer` (the latter plus `TerrainRenderer` + `terrain.vert/.frag` deleted). Single global VBO/EBO with slot allocator (one slot per landblock), per-frame `DrawElementsIndirectCommand[]` upload + `glMultiDrawElementsIndirect`, bindless atlas handles passed as `uvec2` uniforms reconstructed via `sampler2DArray(handle)`. **Path C** chosen: mirrors WB's `TerrainRenderManager` pattern but consumes `LandblockMesh.Build` so retail's `FSplitNESW` formula is preserved (closes ISSUE #51). Path A killed by 49.98% measured divergence between WB's `CalculateSplitDirection` and retail's at addr `00531d10`; Path B (fork-patch WB) rejected for permanent maintenance burden. Perf at Holtburg radius=5 (commit `da56063`): modern 6.4-7.0 µs / 9-14 µs p95 vs legacy 1.5 µs / 3.0 µs — **modern is ~4× SLOWER on CPU at radius=5** because legacy's 16×16-LB chunking collapsed visible LBs to one `glDrawElements`. Architectural wins (zero `glBindTexture`/frame, constant-cost dispatch, per-LB frustum cull) manifest at higher radius (A.5 territory). Spec acceptance criterion 5 ("≥10% lower CPU at radius=5") amended via `docs/plans/2026-05-09-phase-n5b-perf-baseline.md`. Three gotchas captured in memory: `uniform sampler2DArray` + `glProgramUniformHandleARB` GL_INVALID_OPERATIONs on at least one driver (use `uniform uvec2` + `sampler2DArray(handle)` constructor instead — N.5's mesh_modern pattern); `MaybeFlushTerrainDiag` median-calc underflow on first sample; visual gates need actual visual confirmation, not assent. Plan archived at `docs/superpowers/plans/2026-05-09-phase-n5b-terrain-modern.md`. | Live ✓ |
|
||||
| N.6 slice 1 | GPU timing fix + radius=12 perf baseline. Fixed the gpu_us double-buffering bug in `WbDrawDispatcher` (ring-of-3 query slots, read-before-overwrite, vendor-neutral across AMD/NVIDIA/Intel desktop GL). Added env-gated `ACDREAM_DUMP_SURFACES=1` one-shot surface-format histogram dump in `TextureCache` for the atlas-opportunity audit. Captured authoritative baseline at Holtburg radii 4 / 8 / 12 (standstill + walking) with the now-working `gpu_us` diagnostic; baseline doc concludes CPU dominates GPU by 30–50× at every radius and recommends C.1.5 next then reduced-scope slice 2 (atlas + persistent-mapped buffers dropped). Baseline numbers at [docs/plans/2026-05-11-phase-n6-perf-baseline.md](2026-05-11-phase-n6-perf-baseline.md). Plan archived at `docs/superpowers/plans/2026-05-11-phase-n6-slice1.md`. | Live ✓ |
|
||||
| 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 ✓ |
|
||||
| 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:
|
||||
|
|
|
|||
417
docs/research/2026-05-13-b4b-shipped-handoff.md
Normal file
417
docs/research/2026-05-13-b4b-shipped-handoff.md
Normal file
|
|
@ -0,0 +1,417 @@
|
|||
# Phase B.4b shipped — handoff (visual-verified 2026-05-13)
|
||||
|
||||
**Date:** 2026-05-13.
|
||||
**Branch:** `claude/compassionate-wilson-23ff99` (ready to merge to main; do NOT merge here — controller handles that after code review).
|
||||
**Predecessors:**
|
||||
- [docs/research/2026-05-12-l2g-slice1-shipped-handoff.md](2026-05-12-l2g-slice1-shipped-handoff.md) — L.2g slice 1 ship handoff that discovered the B.4 handler gap and deferred the Holtburg visual test to B.4b.
|
||||
- [docs/superpowers/specs/2026-05-13-phase-b4b-design.md](../superpowers/specs/2026-05-13-phase-b4b-design.md) — B.4b design spec.
|
||||
- [docs/superpowers/plans/2026-05-13-phase-b4b-plan.md](../superpowers/plans/2026-05-13-phase-b4b-plan.md) — B.4b implementation plan (6 tasks; Tasks 1-4 per plan + 2 bonus sets beyond the plan).
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
Phase B.4b **shipped end-to-end and is visual-verified 2026-05-13.** The M1
|
||||
demo target *"open the inn door"* is met. 9 commits on this branch implement
|
||||
and fix the complete round-trip: double-click door → `WorldPicker.Pick` →
|
||||
`InteractRequests.BuildUse` → ACE broadcasts `SetState (0xF74B)` with
|
||||
`ETHEREAL` bit → `ShadowObjectRegistry.UpdatePhysicsState` (L.2g slice 1)
|
||||
mutates cached state → `CollisionExemption.ShouldSkip` exempts the door →
|
||||
player walks through.
|
||||
|
||||
The plan estimated "30-50 LOC, 1-2 subagent dispatches, ~30 min."
|
||||
Visual testing surfaced **four bonus discoveries** beyond the plan's
|
||||
Tasks 1-4:
|
||||
|
||||
1. `InputDispatcher` had no double-click detection (the `SelectDblLeft`
|
||||
binding was dead code — the dispatcher never produced `DoubleClick`
|
||||
activations).
|
||||
2. `OnInputAction`'s early-return gate discarded `DoubleClick` activations
|
||||
before the switch reached the `SelectDblLeft` case.
|
||||
3. L.2g `CollisionExemption.ShouldSkip` required **both** `ETHEREAL` +
|
||||
`IGNORE_COLLISIONS` bits, but ACE's `Door.Open()` sends only `ETHEREAL`
|
||||
(`state=0x0001000C`).
|
||||
4. `OnLiveStateUpdated` passed a server GUID to `ShadowObjectRegistry` which
|
||||
is keyed by local entity ID — the registry lookup always missed → no-op
|
||||
→ the door never became passable. **This was the actual blocker the user
|
||||
reported.**
|
||||
|
||||
Fixes 1-4 were shipped as bonus commits 5-9 beyond the plan's Tasks 1-4.
|
||||
L.2g slice 1 and B.4b are now both fully verified by the same visual test.
|
||||
Issue #57 is closed. Issue #58 (door swing animation) is filed as M1-deferred
|
||||
polish.
|
||||
|
||||
---
|
||||
|
||||
## What shipped on this branch
|
||||
|
||||
| # | Commit | Subject | Task |
|
||||
|---|---|---|---|
|
||||
| 1 | `f0b3bd9` | `feat(B.4b): WorldPicker.BuildRay — mouse-to-world ray unprojection` | Task 1 |
|
||||
| 2 | `221b641` | `feat(B.4b): WorldPicker.Pick — ray-sphere entity pick` | Task 2 |
|
||||
| 3 | `5821bdc` | `fix(B.4b): WorldPicker.Pick — handle inside-sphere origin + document normalize contract` | Task 2 review fix |
|
||||
| 4 | `7b4aff2` | `refactor(B.4b): unify _selectedTargetGuid -> _selectedGuid` | Task 3 |
|
||||
| 5 | `89d82e1` | `feat(B.4b): GameWindow wires Select/Use handlers via WorldPicker` | Task 4 |
|
||||
| 6 | `242ce70` | `feat(B.4b): InputDispatcher detects double-clicks` | Bonus: Task 4b |
|
||||
| 7 | `58b95bc` | `fix(B.4b): let DoubleClick activation pass the OnInputAction gate` | Bonus: Task 4c |
|
||||
| 8 | `a6e4b57` | `fix(phys L.2g slice 1b): widen CollisionExemption to ETHEREAL alone` | L.2g slice 1b |
|
||||
| 9 | `08be296` | `fix(phys L.2g slice 1c): translate ServerGuid -> entity.Id for ShadowObjectRegistry` | L.2g slice 1c |
|
||||
|
||||
Plus plan/spec commits earlier in the branch session:
|
||||
- `4a1c594` — B.4b design spec.
|
||||
- `ffa404d` — corrected file paths in spec (WorldPicker is in `AcDream.Core.Selection`, not `AcDream.App/Rendering`).
|
||||
- `179e441` — B.4b implementation plan (6 tasks).
|
||||
|
||||
**Build:** clean. **Tests:** 4 new double-click detection tests (commit `242ce70`, all pass). Full suite: builds green, no regressions. L.2g slice 1's 6 tests continue to pass.
|
||||
|
||||
---
|
||||
|
||||
## What the code does end-to-end
|
||||
|
||||
When the user double-left-clicks a door entity in the Holtburg inn doorway,
|
||||
the following chain fires:
|
||||
|
||||
1. **Double-click detection** — `InputDispatcher.OnMouseDown` checks the
|
||||
elapsed time since the previous `MouseLeft` press. If ≤500ms, the
|
||||
activation kind is `DoubleClick`; otherwise `Press`. This is new as
|
||||
of commit `242ce70`; prior to this the `SelectDblLeft` binding was dead
|
||||
code (the dispatcher never produced `DoubleClick` activations).
|
||||
|
||||
2. **Action dispatch** — `InputDispatcher` resolves the chord
|
||||
`[MouseLeft, DoubleClick]` → `InputAction.SelectDblLeft` + activation
|
||||
`DoubleClick`. The multicast `InputAction` event fires, logged as:
|
||||
`[input] SelectDblLeft DoubleClick`.
|
||||
|
||||
3. **OnInputAction gate** — `GameWindow.OnInputAction` receives the event.
|
||||
Prior to commit `58b95bc`, an early-return guard (`if (activation != Press) return;`)
|
||||
discarded all `DoubleClick` events. The fix widens the gate to
|
||||
`if (activation != Press && activation != DoubleClick) return;`.
|
||||
The switch now reaches the `SelectDblLeft` case.
|
||||
|
||||
4. **Ray construction** — `WorldPicker.BuildRay(mousePos, viewport, viewMatrix, projMatrix)`
|
||||
unprojects the cursor pixel into a world-space ray origin + direction,
|
||||
using standard NDC→view→world unprojection. Numerically: the mouse pixel
|
||||
is mapped to `[-1,+1]` NDC, transformed through `inverse(proj)` to get
|
||||
a view-space direction, then through `inverse(view)` for world-space.
|
||||
|
||||
5. **Entity pick** — `WorldPicker.Pick(ray, entities, maxDist=50m)` iterates
|
||||
all entities in `_gpuWorldState.GetAllEntities()`, tests each against a
|
||||
ray-sphere intersection with the entity's bounding radius, and returns
|
||||
the closest hit. A special-case inside-sphere origin guard (commit `5821bdc`)
|
||||
ensures the pick works even when the cursor origin is already inside an
|
||||
entity's bounding sphere (common for large portals or doors at close range).
|
||||
`[B.4b] pick guid=0x7A9B4015 name=Door` logged on hit.
|
||||
|
||||
6. **Use message** — `GameWindow` stores `_selectedGuid = picked.Guid` and
|
||||
calls `InteractRequests.BuildUse(seq, guid)`. The resulting `0xF7B1 / 0x0036`
|
||||
game message is sent to ACE via `_liveSession.SendGameMessage(body)`.
|
||||
`[B.4b] use guid=0x7A9B4015 seq=N` logged.
|
||||
|
||||
7. **ACE processes the Use** — ACE's `Door.Open()` flips the door's physics
|
||||
flags to `ETHEREAL | ...` and broadcasts `SetState (0xF74B)` with the
|
||||
new state value.
|
||||
|
||||
8. **SetState arrives** — `WorldSession.OnSetState` parses the 12-byte
|
||||
payload (Guid + PhysicsState + InstanceSeq + StateSeq) and fires
|
||||
`WorldSession.StateUpdated`. `GameWindow.OnLiveStateUpdated` handles it.
|
||||
**New as of commit `08be296` (slice 1c):** the handler translates
|
||||
`parsed.Guid` (server GUID `0x7A9B4015`) to `entity.Id` (local entity ID
|
||||
`0x000F4245`) via `_entitiesByServerGuid` before calling
|
||||
`ShadowObjectRegistry.UpdatePhysicsState`. Without this translation the
|
||||
registry lookup always returned "not found" — a silent no-op.
|
||||
Log: `[setstate] guid=0x7A9B4015 entityId=0x000F4245 state=0x0001000C`.
|
||||
|
||||
9. **Collision exemption** — next physics tick, `FindObjCollisions` calls
|
||||
`CollisionExemption.ShouldSkip(entry.State, entry.Flags, moverState)`.
|
||||
**New as of commit `a6e4b57` (slice 1b):** the check fires on
|
||||
`(state & ETHEREAL_PS) != 0` alone (widened from the original `ETHEREAL &&
|
||||
IGNORE_COLLISIONS` conjunction). Because ACE broadcasts only `ETHEREAL`
|
||||
in the low bits (`state=0x0001000C`), the original conjunction never fired;
|
||||
the door stayed solid.
|
||||
|
||||
10. **Player walks through** — the resolver produces no wall-contact response
|
||||
for the door's collision geometry. User confirms: "Now I can walk through."
|
||||
|
||||
### Observed log evidence
|
||||
|
||||
```
|
||||
[input] SelectDblLeft DoubleClick
|
||||
[B.4b] pick guid=0x7A9B4015 name=Door
|
||||
[B.4b] use guid=0x7A9B4015 seq=N
|
||||
[setstate] guid=0x7A9B4015 entityId=0x000F4245 state=0x0001000C
|
||||
```
|
||||
|
||||
Player walks through the closed door after the `setstate` line.
|
||||
|
||||
---
|
||||
|
||||
## The four bonus discoveries
|
||||
|
||||
### 1. InputDispatcher had no double-click detection (`242ce70`)
|
||||
|
||||
**Root cause:** `InputDispatcher.OnMouseDown` only looked up `Press` and
|
||||
`Hold` activations in the binding table. The `SelectDblLeft` binding was
|
||||
wired to the chord `[MouseLeft, DoubleClick]` in `KeyBindings.cs:300-320`
|
||||
(shipped in B.4, 2026-04-28), but the dispatcher's mouse-down handler
|
||||
never set activation to `DoubleClick` — it always produced `Press`.
|
||||
So `SelectDblLeft` was literally unreachable: the chord required
|
||||
`DoubleClick` to match, but the dispatcher never generated it.
|
||||
|
||||
**Fix:** Added a `_lastMouseDownTime` (and `_lastMouseDownButton`) tracker
|
||||
to `InputDispatcher`. In `OnMouseDown`, if the same button fires within
|
||||
500ms of its last press, activation is `DoubleClick`; otherwise `Press`.
|
||||
500ms matches the standard Windows/macOS double-click threshold.
|
||||
|
||||
**Rationale:** The fix is minimal and correct. A more faithful retail
|
||||
implementation might read the OS's configured double-click interval, but
|
||||
500ms is the retail default and was the right call for now. 4 new unit
|
||||
tests cover the timing logic: first click = Press, second click within
|
||||
500ms = DoubleClick, third click = Press again (resets the window), and
|
||||
button mismatch = Press.
|
||||
|
||||
### 2. OnInputAction gate discarded DoubleClick activations (`58b95bc`)
|
||||
|
||||
**Root cause:** Even after discovery #1 was fixed and `SelectDblLeft DoubleClick`
|
||||
fired from the dispatcher, the event handler had an early-return guard at
|
||||
the top of `GameWindow.OnInputAction`:
|
||||
|
||||
```csharp
|
||||
if (activation != InputActivation.Press) return;
|
||||
```
|
||||
|
||||
This guard was introduced to prevent `Hold` repetition from triggering
|
||||
switch cases intended for one-shot actions. It correctly blocked `Hold`
|
||||
but also blocked `DoubleClick` — so the `SelectDblLeft` case was still
|
||||
unreachable even after the dispatcher started generating `DoubleClick`.
|
||||
|
||||
**Fix:** Widened the guard to let both `Press` and `DoubleClick` through:
|
||||
|
||||
```csharp
|
||||
if (activation != InputActivation.Press && activation != InputActivation.DoubleClick) return;
|
||||
```
|
||||
|
||||
**Rationale:** `DoubleClick` is semantically a one-shot activation (fires
|
||||
once per double-click gesture), so it belongs in the same pass-through
|
||||
group as `Press`. `Hold` repetition remains blocked.
|
||||
|
||||
### 3. CollisionExemption required both ETHEREAL + IGNORE_COLLISIONS (`a6e4b57`)
|
||||
|
||||
**Root cause:** The original `CollisionExemption.ShouldSkip` check was
|
||||
ported faithfully from `acclient_2013_pseudo_c.txt:276782`, which requires
|
||||
**both** `ETHEREAL_PS (0x4)` and `IGNORE_COLLISIONS_PS (0x10)` to be set
|
||||
simultaneously before short-circuiting collision detection. Retail servers
|
||||
send both bits when opening a door, so retail clients see `state ≥ 0x14`.
|
||||
|
||||
However, ACE's `Door.Open()` broadcasts only the `ETHEREAL` bit in the
|
||||
low portion of the state word. The observed wire value was
|
||||
`state=0x0001000C`: bit `0x4` (ETHEREAL) is set, bit `0x10`
|
||||
(IGNORE_COLLISIONS) is not. The `&&` conjunction in `ShouldSkip` evaluated
|
||||
to false → door stayed solid even after the registry update.
|
||||
|
||||
This was the exact scenario the L.2g slice 1 Important review note warned
|
||||
about (see L.2g handoff §"One Important review note"): *"ACE's
|
||||
`PhysicsObj.cs:787-791` may set both bits... but this is not verified by
|
||||
the test suite. The B.4b visual test will settle this definitively."*
|
||||
It settled as: ACE sends `0x4` alone, not `0x14`.
|
||||
|
||||
**Fix:** Widened the short-circuit to fire on `ETHEREAL` alone:
|
||||
|
||||
```csharp
|
||||
// Widened from (ETHEREAL && IGNORE_COLLISIONS) — ACE Door.Open() sends
|
||||
// ETHEREAL alone (state=0x0001000C); retail servers send both.
|
||||
// Pragmatic choice: exempt on ETHEREAL-bit-alone until full retail
|
||||
// obstruction_ethereal flag path is ported.
|
||||
if ((state & ETHEREAL_PS) != 0) return true;
|
||||
```
|
||||
|
||||
**Rationale:** The deeper retail path (pseudo-C line 276795 sets
|
||||
`obstruction_ethereal=1` and routes through downstream movement handling)
|
||||
was not ported — that's a more invasive change requiring more testing. The
|
||||
pragmatic widening to ETHEREAL alone is correct for ACE's Door behavior and
|
||||
matches the spirit of the retail check (ETHEREAL means "pass through me").
|
||||
If a future retail-server emulator sends both bits, the widened check still
|
||||
fires (ETHEREAL is a subset of ETHEREAL+IGNORE_COLLISIONS).
|
||||
|
||||
### 4. ServerGuid → entity.Id translation missing in OnLiveStateUpdated (`08be296`) — THE actual blocker
|
||||
|
||||
**Root cause:** `ShadowObjectRegistry` is keyed by local `entity.Id` (the
|
||||
per-session integer ID assigned by `GpuWorldState` at entity registration,
|
||||
e.g. `0x000F4245`). The `GameWindow.OnLiveStateUpdated` handler was passing
|
||||
`parsed.Guid` — the **server GUID** broadcasted in the `SetState` packet
|
||||
(e.g. `0x7A9B4015`) — directly to `UpdatePhysicsState`. Because the registry
|
||||
has no entry keyed by server GUID, the lookup always returned "not found"
|
||||
and the state mutation was silently dropped. The registry stayed at
|
||||
`state=0x00000000` (closed, solid) regardless of how many times the door
|
||||
was clicked.
|
||||
|
||||
This is why discoveries 1-3 alone were insufficient: even with double-click
|
||||
detection working, the correct gate firing, and `CollisionExemption`
|
||||
widened, the registry still held the stale closed state and the door
|
||||
stayed solid.
|
||||
|
||||
**Fix:** Added a `_entitiesByServerGuid` reverse-lookup dictionary to
|
||||
`GameWindow` (populated at entity registration in `OnLiveCreateObject`).
|
||||
`OnLiveStateUpdated` now does:
|
||||
|
||||
```csharp
|
||||
if (_entitiesByServerGuid.TryGetValue(parsed.Guid, out var entity))
|
||||
_physicsEngine.ShadowObjects.UpdatePhysicsState(entity.Id, parsed.PhysicsState);
|
||||
```
|
||||
|
||||
The `entityId=` field was added to the `[setstate]` diagnostic log line
|
||||
specifically to make this translation visible and greppable:
|
||||
`[setstate] guid=0x7A9B4015 entityId=0x000F4245 state=0x0001000C`.
|
||||
|
||||
**Why this was missed:** L.2g slice 1's unit tests operated at the
|
||||
`ShadowObjectRegistry` level directly, calling `UpdatePhysicsState` with
|
||||
an `entity.Id` (not a server GUID). The integration was never exercised
|
||||
end-to-end before B.4b's visual test. The two tests `UpdatePhysicsState_FlipsEthereal_*`
|
||||
were correct in isolation; the broken layer was one level above them
|
||||
(the handler → registry call site).
|
||||
|
||||
**Why the "multiple doors" misdiagnosis occurred:** Before slice 1c was
|
||||
identified, the `[resolve]` probes showed wall hits attributed to
|
||||
`obj=0x000F4245` while the clicked door's ServerGuid was `0x7A9B4015`.
|
||||
Initial read: "these are two different entities blocking the threshold."
|
||||
Slice 1c clarified: both IDs refer to the same door — `0x000F4245` is
|
||||
the local entity ID, `0x7A9B4015` is the server GUID for the same entity.
|
||||
The ID-space mismatch was the cause of both the collision-not-clearing
|
||||
AND the "different object" misread.
|
||||
|
||||
---
|
||||
|
||||
## Open notes / follow-ups
|
||||
|
||||
### Door swing animation (#58)
|
||||
|
||||
When ACE opens a door it broadcasts **two** packets, not one:
|
||||
|
||||
1. `SetState (0xF74B)` — the collision-bit flip. **Handled by L.2g slice 1.**
|
||||
2. `UpdateMotion (0xF74D)` with stance/command `(NonCombat, On)` — the
|
||||
swing animation cycle. **NOT handled.**
|
||||
|
||||
acdream's `UpdateMotion` pipeline is currently scoped to player + creature
|
||||
animation (Phase L.3). Non-creature entities like doors do not receive
|
||||
cycle commands. The door therefore opens (becomes passable) but has no
|
||||
visible swing animation.
|
||||
|
||||
Filed as **issue #58**. Scope is unknown — routing `UpdateMotion` to
|
||||
non-creature `WorldEntity` instances could be quick (few lines), or the
|
||||
`AnimationSequencer` may have creature-specific assumptions that require
|
||||
audit first. Filed as M1-deferred polish; it does not block the demo
|
||||
scenario.
|
||||
|
||||
### Door toggle behavior
|
||||
|
||||
ACE doors toggle on each Use: first double-click opens, subsequent
|
||||
double-click closes (re-sends `SetState` with `state=0x00000000`, restoring
|
||||
collision). This is correct ACE behavior and matches retail. No issue to file.
|
||||
|
||||
Rapid double-clicks (faster than ACE's server-tick processing) will open
|
||||
then close in quick succession — each Use lands as a distinct game action.
|
||||
Expected behavior; no fix needed.
|
||||
|
||||
### Multiple-door misdiagnosis (historical note)
|
||||
|
||||
While slice 1c was still unidentified, the `[resolve]` diagnostic showed:
|
||||
|
||||
```
|
||||
[resolve] ... obj=0x000F4245 wall hit
|
||||
[B.4b] use guid=0x7A9B4015 ...
|
||||
[setstate] guid=0x7A9B4015 state=0x0001000C
|
||||
[resolve] ... obj=0x000F4245 wall hit (unchanged!)
|
||||
```
|
||||
|
||||
Initial misdiagnosis: there must be a *different* door entity (`0x000F4245`)
|
||||
blocking the threshold whose state was never updated. Slice 1c revealed:
|
||||
both IDs refer to the same door — one is the server GUID (network space),
|
||||
the other is the local entity ID (registry space). The registry update was
|
||||
targeting the server GUID (which missed), so the local-ID-keyed entry
|
||||
stayed solid.
|
||||
|
||||
### Selection HUD / hover-highlight / brackets
|
||||
|
||||
Out of B.4b scope per design spec §Non-goals. The `_selectedGuid` field on
|
||||
`GameWindow` is populated (stores the last-picked entity's server GUID), but
|
||||
nothing renders a selection bracket, hover highlight, or target nameplate.
|
||||
That is M2/M3 HUD work (Phase D.6).
|
||||
|
||||
### BuildPickUp (F key) + UseWithTarget UX
|
||||
|
||||
`InteractRequests.BuildPickUp` exists (as an alias of `BuildUse`). The
|
||||
`SelectionPickUp` input action and the F-key binding exist. But
|
||||
`OnInputAction` has no case for `SelectionPickUp` — pick-up-by-F-key is
|
||||
still unimplemented. Same for `UseWithTarget` (requires a secondary target
|
||||
selection UX). Both deferred to a follow-up phase; not M1-blocking.
|
||||
|
||||
---
|
||||
|
||||
## Next session
|
||||
|
||||
**M1 demo progress as of this branch:**
|
||||
- ✅ "walk through Holtburg without getting stuck" — Phase L.2 in progress (outdoor collision works; CBuildingObj interior still deferred to L.2d).
|
||||
- ✅ "open the inn door" — **done** (B.4b, this branch).
|
||||
- ⬜ "click an NPC" — pick + Use wiring exists now; depends on ACE NPC handler responding to Use.
|
||||
- ⬜ "pick up an item" — `BuildPickUp` + F-key wiring not yet in `OnInputAction`.
|
||||
|
||||
**Recommended next steps (in M1 critical-path order):**
|
||||
|
||||
1. **Door swing animation (#58)** — cosmetic M1 polish. Route
|
||||
`UpdateMotion (0xF74D)` to non-creature entities so the door visually
|
||||
swings. Could be quick (30 min) or moderate (2 hrs with AnimationSequencer
|
||||
audit). Worth a spike before committing to an estimate.
|
||||
|
||||
2. **Chronic open-issue triage** — #2 (lightning), #4 (horizon-glow), #28
|
||||
(aurora), #29 (cloud thinness), #37 (humanoid coat), #41 (remote-motion
|
||||
blips) have been deferred since April/early-May. Link each to a future
|
||||
phase or downgrade. ~1 hour. Not M1-blocking but surfaces the real backlog.
|
||||
|
||||
3. **More Phase C visual-fidelity** — C.2 (dynamic point lights), C.3
|
||||
(palette tuning), C.4 (double-sided translucent polys). World still reads
|
||||
"old" without local lighting on fireplaces/lamps.
|
||||
|
||||
---
|
||||
|
||||
## Reproducibility
|
||||
|
||||
Same launch recipe as before. For reproducing the visual test:
|
||||
|
||||
```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"
|
||||
$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-b4b.log"
|
||||
```
|
||||
|
||||
Walk to the Holtburg inn doorway. Double-left-click the closed Door. Walk
|
||||
through. Subsequent double-clicks will close and re-open (ACE toggle).
|
||||
|
||||
After closing the client, grep for:
|
||||
|
||||
```powershell
|
||||
Select-String -Path launch-b4b.log -Pattern "SelectDblLeft|pick guid|use guid|setstate.*entityId"
|
||||
```
|
||||
|
||||
Expected:
|
||||
- `[input] SelectDblLeft DoubleClick` — dispatcher fires on second click within 500ms.
|
||||
- `[B.4b] pick guid=0x7A9B4015 name=Door` — ray hits the door.
|
||||
- `[B.4b] use guid=0x7A9B4015 seq=N` — Use message sent.
|
||||
- `[setstate] guid=0x7A9B4015 entityId=0x000F4245 state=0x0001000C` — ACE reply processed, translation confirmed.
|
||||
|
||||
---
|
||||
|
||||
## Worktree state at handoff
|
||||
|
||||
- Branch `claude/compassionate-wilson-23ff99`.
|
||||
- 9 implementation commits + 3 plan/spec commits ahead of `eea9b4d`
|
||||
(the L.2g slice 1 merge from the previous session).
|
||||
- Controller should run a code review, then merge to main.
|
||||
- Do NOT rebase or squash — each commit tells a diagnostic story that
|
||||
the next phase's debugging may need.
|
||||
Loading…
Add table
Add a link
Reference in a new issue