From ebef82034ecba2e1e6f05063a23ec6da49da6b2c Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 12 May 2026 17:19:05 +0200 Subject: [PATCH 1/4] feat(phys L.2a slice 1): resolver + cell-transit probes (PhysicsDiagnostics) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New static `AcDream.Core.Physics.PhysicsDiagnostics` holds two runtime-toggleable flags initialized from env vars: - ACDREAM_PROBE_RESOLVE=1 — emit one [resolve] line per PhysicsEngine.ResolveWithTransition call: input/target/output position+cell, ok-vs-partial, grounded-in, contact-plane status, wall normal if hit, walkable-polygon valid, moving entity id. - ACDREAM_PROBE_CELL=1 — emit one [cell-transit] line per PlayerMovementController.CellId change: old → new cell, current world position, reason tag (resolver / teleport). Both also exposed as runtime-toggleable checkboxes in the DebugPanel "Diagnostics" section. Unlike the existing four Dump-* checkboxes (which only mirror sticky-at-startup env vars), the two new ones forward directly to PhysicsDiagnostics — toggling on/off takes effect on the next physics resolve, no relaunch. Why now: L.2's plan-of-record (docs/plans/2026-04-29-movement-collision- conformance.md) explicitly says "Land L.2a diagnostics first. Do not make another physics change blind." This slice closes the most-load- bearing gap in L.2a — a general-purpose probe on the resolver outcome and a cell-transit log — so that later L.2b/c/d/e physics changes can be evidence-driven instead of guessed. Foundation for the indoor / dungeon walking trajectory (G.3 unblock). Pure additive: when both flags are off (default), the probes collapse to a single static-bool read per resolve, zero log cost. PlayerMovement Controller's two CellId-mutation sites are now routed through a private UpdateCellId(reason) helper for diag chokepoint. Build green, 1032/1040 unit tests pass. The 8 failing tests are pre-existing on the branch base (verified by stash-and-rerun); none touch resolver or cell-transit code; all fail identically with this slice stashed. Investigation deferred to a follow-up. Refs: docs/plans/2026-04-29-movement-collision-conformance.md (L.2a shipped-slice note added in same commit). Co-Authored-By: Claude Opus 4.7 (1M context) --- ...26-04-29-movement-collision-conformance.md | 29 ++++++++++++++ .../Input/PlayerMovementController.cs | 21 +++++++++- .../Physics/PhysicsDiagnostics.cs | 40 +++++++++++++++++++ src/AcDream.Core/Physics/PhysicsEngine.cs | 22 ++++++++++ .../Panels/Debug/DebugPanel.cs | 22 ++++++---- .../Panels/Debug/DebugVM.cs | 27 +++++++++++++ 6 files changed, 151 insertions(+), 10 deletions(-) create mode 100644 src/AcDream.Core/Physics/PhysicsDiagnostics.cs diff --git a/docs/plans/2026-04-29-movement-collision-conformance.md b/docs/plans/2026-04-29-movement-collision-conformance.md index 8bfc2c1..cdabd45 100644 --- a/docs/plans/2026-04-29-movement-collision-conformance.md +++ b/docs/plans/2026-04-29-movement-collision-conformance.md @@ -92,6 +92,35 @@ Goal: make every bad movement outcome explainable. - Build real-DAT fixture capture for known walls, building ledges, rooftops, slopes, landblock seams, and dungeon entrances. +Current shipped slices: + +- 2026-04-30: cdb + TTD retail-observer toolchain (`tools/pdb-extract/`, + `tools/ttd-record.ps1`, `tools/ttd-query.ps1`) with PDB pairing checker + and ring-buffer trace replay. The "retail observer harness" line item. +- 2026-04 (pre-L.2 rename): `ACDREAM_DUMP_MOVE_TRUTH` paired + outbound/server-echo dumper in `GameWindow` covers outbound packet + fields + server echo + correction delta with cell-id mismatch. +- Pre-L.2: scenario-specific dumps `ACDREAM_DUMP_MOTION`, + `ACDREAM_DUMP_STEEP_ROOF`, `ACDREAM_DUMP_STEPUP`, + `ACDREAM_DUMP_EDGE_SLIDE` for the codepaths hit during prior bug chases. +- 2026-05-12 (slice 1): general-purpose probes via new + `AcDream.Core.Physics.PhysicsDiagnostics` static class. + `ACDREAM_PROBE_RESOLVE` emits one `[resolve]` line per + `PhysicsEngine.ResolveWithTransition` call (input/output pos+cell, + ok-vs-partial, grounded-in, contact-plane status, wall normal if hit, + walkable polygon valid, moving entity id). + `ACDREAM_PROBE_CELL` emits one `[cell-transit]` line per + `PlayerMovementController.CellId` change with old→new + position + + reason tag (`resolver`/`teleport`). Both flippable live via the + DebugPanel "Diagnostics" section — checkbox toggles take effect on + the next resolve, no relaunch required. + +Remaining L.2a work: contact-plane probe (general, not just steep-roof), +ShadowObjectRegistry hit log ("you collided with entity X"), water probe, +real-DAT fixture-capture pipeline, and folding the older sticky-at-startup +`ACDREAM_DUMP_*` flags into `PhysicsDiagnostics` for unified runtime +toggling. + ### L.2b - Movement Wire / Contact Authority Goal: stop sending movement packets that claim more certainty than the local diff --git a/src/AcDream.App/Input/PlayerMovementController.cs b/src/AcDream.App/Input/PlayerMovementController.cs index 1bc88b8..cb9b34b 100644 --- a/src/AcDream.App/Input/PlayerMovementController.cs +++ b/src/AcDream.App/Input/PlayerMovementController.cs @@ -288,12 +288,29 @@ public sealed class PlayerMovementController _motion.apply_current_movement(cancelMoveTo: false, allowJump: false); } + // L.2a slice 1 (2026-05-12): centralized CellId mutation so the + // [cell-transit] probe fires from a single chokepoint. Both the + // server-snap path (SetPosition) and the per-frame resolver path + // route through here. When PhysicsDiagnostics.ProbeCellEnabled is + // off this collapses to a single bool-compare + assignment — zero + // logging cost. + private void UpdateCellId(uint newCellId, string reason) + { + if (newCellId != CellId && PhysicsDiagnostics.ProbeCellEnabled) + { + var pos = _body.Position; + Console.WriteLine(System.FormattableString.Invariant( + $"[cell-transit] 0x{CellId:X8} -> 0x{newCellId:X8} pos=({pos.X:F3},{pos.Y:F3},{pos.Z:F3}) reason={reason}")); + } + CellId = newCellId; + } + public void SetPosition(Vector3 pos, uint cellId) { _body.Position = pos; _prevPhysicsPos = pos; _currPhysicsPos = pos; - CellId = cellId; + UpdateCellId(cellId, "teleport"); // Treat as grounded after a server-side position snap. _body.TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable; @@ -760,7 +777,7 @@ public sealed class PlayerMovementController _wasAirborneLastFrame = !_body.OnWalkable; - CellId = resolveResult.CellId; + UpdateCellId(resolveResult.CellId, "resolver"); // ── 6. Determine outbound motion commands ───────────────────────────── uint? outForwardCmd = null; diff --git a/src/AcDream.Core/Physics/PhysicsDiagnostics.cs b/src/AcDream.Core/Physics/PhysicsDiagnostics.cs new file mode 100644 index 0000000..2440bb1 --- /dev/null +++ b/src/AcDream.Core/Physics/PhysicsDiagnostics.cs @@ -0,0 +1,40 @@ +using System; + +namespace AcDream.Core.Physics; + +/// +/// L.2a slice 1 (2026-05-12) — runtime-toggleable physics probe flags. +/// Initialized from env vars at process start; flippable at runtime via +/// the DebugPanel mirror (or by direct assignment). Log call sites read +/// these statics so a checkbox toggle takes effect on the next resolve +/// without relaunching. +/// +/// +/// Slice 1 ships + +/// . Future slices may fold the older +/// ACDREAM_DUMP_* env vars into this class for unified runtime +/// toggling. Until then, those older flags remain sticky-at-startup +/// per their original implementation. +/// +/// +public static class PhysicsDiagnostics +{ + /// + /// When true, emits + /// one structured [resolve] line per call: input + target + + /// output position/cell, grounded state, contact-plane status, + /// collision-normal validity, walkable polygon status, moving entity + /// id. Initial state from ACDREAM_PROBE_RESOLVE=1. + /// + public static bool ProbeResolveEnabled { get; set; } = + Environment.GetEnvironmentVariable("ACDREAM_PROBE_RESOLVE") == "1"; + + /// + /// When true, every change to PlayerMovementController.CellId + /// emits one [cell-transit] line: old → new cell, current + /// world position, reason tag (resolver / teleport). + /// Initial state from ACDREAM_PROBE_CELL=1. + /// + public static bool ProbeCellEnabled { get; set; } = + Environment.GetEnvironmentVariable("ACDREAM_PROBE_CELL") == "1"; +} diff --git a/src/AcDream.Core/Physics/PhysicsEngine.cs b/src/AcDream.Core/Physics/PhysicsEngine.cs index fe308ae..696a635 100644 --- a/src/AcDream.Core/Physics/PhysicsEngine.cs +++ b/src/AcDream.Core/Physics/PhysicsEngine.cs @@ -677,6 +677,28 @@ public sealed class PhysicsEngine $"deltaXY=({dx:F3},{dy:F3}) cp={cpInfo}"); } + // L.2a slice 1 (2026-05-12): general-purpose resolver probe. + // One line per call when PhysicsDiagnostics.ProbeResolveEnabled + // is set (env var ACDREAM_PROBE_RESOLVE=1 at startup, or the + // DebugPanel checkbox flipped at runtime). Captures every + // dimension L.2 cares about: input/output position, input/output + // cell, ok-vs-partial, grounded-in vs contact-out, contact-plane + // status, wall normal if hit, walkable polygon valid. Zero cost + // when off (one static-bool read). + if (PhysicsDiagnostics.ProbeResolveEnabled) + { + var probePost = sp.CheckPos; + string probeCp = ci.ContactPlaneValid + ? "valid" + : (ci.LastKnownContactPlaneValid ? "lastKnown" : "none"); + string probeHit = collisionNormalValid + ? System.FormattableString.Invariant( + $"yes n=({collisionNormal.X:F2},{collisionNormal.Y:F2},{collisionNormal.Z:F2})") + : "no"; + Console.WriteLine(System.FormattableString.Invariant( + $"[resolve] ent=0x{movingEntityId:X8} in=({currentPos.X:F3},{currentPos.Y:F3},{currentPos.Z:F3}) cell=0x{cellId:X8} tgt=({targetPos.X:F3},{targetPos.Y:F3},{targetPos.Z:F3}) out=({probePost.X:F3},{probePost.Y:F3},{probePost.Z:F3}) cell=0x{sp.CheckCellId:X8} ok={ok} groundedIn={isOnGround} cp={probeCp} hit={probeHit} walkable={sp.HasLastWalkablePolygon}")); + } + if (ok) { bool onGround = ci.ContactPlaneValid diff --git a/src/AcDream.UI.Abstractions/Panels/Debug/DebugPanel.cs b/src/AcDream.UI.Abstractions/Panels/Debug/DebugPanel.cs index dc55080..e990686 100644 --- a/src/AcDream.UI.Abstractions/Panels/Debug/DebugPanel.cs +++ b/src/AcDream.UI.Abstractions/Panels/Debug/DebugPanel.cs @@ -199,15 +199,21 @@ public sealed class DebugPanel : IPanel { if (!r.CollapsingHeader("Diagnostics", defaultOpen: true)) return; - bool dumpMotion = _vm.DumpMotion; - bool dumpVitals = _vm.DumpVitals; - bool dumpOpcodes = _vm.DumpOpcodes; - bool dumpSky = _vm.DumpSky; + bool dumpMotion = _vm.DumpMotion; + bool dumpVitals = _vm.DumpVitals; + bool dumpOpcodes = _vm.DumpOpcodes; + bool dumpSky = _vm.DumpSky; + bool probeResolve = _vm.ProbeResolve; + bool probeCell = _vm.ProbeCell; - if (r.Checkbox("Dump motion (ACDREAM_DUMP_MOTION)", ref dumpMotion)) _vm.DumpMotion = dumpMotion; - if (r.Checkbox("Dump vitals (ACDREAM_DUMP_VITALS)", ref dumpVitals)) _vm.DumpVitals = dumpVitals; - if (r.Checkbox("Dump opcodes (ACDREAM_DUMP_OPCODES)", ref dumpOpcodes)) _vm.DumpOpcodes = dumpOpcodes; - if (r.Checkbox("Dump sky (ACDREAM_DUMP_SKY)", ref dumpSky)) _vm.DumpSky = dumpSky; + if (r.Checkbox("Dump motion (ACDREAM_DUMP_MOTION)", ref dumpMotion)) _vm.DumpMotion = dumpMotion; + if (r.Checkbox("Dump vitals (ACDREAM_DUMP_VITALS)", ref dumpVitals)) _vm.DumpVitals = dumpVitals; + if (r.Checkbox("Dump opcodes (ACDREAM_DUMP_OPCODES)", ref dumpOpcodes)) _vm.DumpOpcodes = dumpOpcodes; + if (r.Checkbox("Dump sky (ACDREAM_DUMP_SKY)", ref dumpSky)) _vm.DumpSky = dumpSky; + // L.2a slice 1 (2026-05-12): unlike the four above, these + // forward to PhysicsDiagnostics so a toggle takes effect live. + if (r.Checkbox("Probe resolve (ACDREAM_PROBE_RESOLVE)", ref probeResolve)) _vm.ProbeResolve = probeResolve; + if (r.Checkbox("Probe cell-transit (ACDREAM_PROBE_CELL)",ref probeCell)) _vm.ProbeCell = probeCell; r.Spacing(); diff --git a/src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs b/src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs index 9e914af..693cc2d 100644 --- a/src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs +++ b/src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs @@ -1,5 +1,6 @@ using System.Numerics; using AcDream.Core.Combat; +using AcDream.Core.Physics; namespace AcDream.UI.Abstractions.Panels.Debug; @@ -234,6 +235,32 @@ public sealed class DebugVM /// Mirror of ACDREAM_DUMP_SKY. public bool DumpSky { get; set; } + // L.2a slice 1 (2026-05-12): unlike DumpMotion/Vitals/Opcodes/Sky + // above (which are display-only mirrors of sticky-at-startup env + // vars), these forward directly to the PhysicsDiagnostics statics, + // so checkbox toggles take effect on the next physics resolve. + /// + /// Runtime mirror of PhysicsDiagnostics.ProbeResolveEnabled + /// (env var ACDREAM_PROBE_RESOLVE). Toggling here flips the + /// resolver probe live — no relaunch required. + /// + public bool ProbeResolve + { + get => PhysicsDiagnostics.ProbeResolveEnabled; + set => PhysicsDiagnostics.ProbeResolveEnabled = value; + } + + /// + /// Runtime mirror of PhysicsDiagnostics.ProbeCellEnabled + /// (env var ACDREAM_PROBE_CELL). Toggling here flips the + /// cell-transit probe live. + /// + public bool ProbeCell + { + get => PhysicsDiagnostics.ProbeCellEnabled; + set => PhysicsDiagnostics.ProbeCellEnabled = value; + } + // ── Action hooks invoked by panel buttons ────────────────────────── /// From e0c08bc57e2e62cd39413bec69e10d832a815b30 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 12 May 2026 18:00:01 +0200 Subject: [PATCH 2/4] feat(phys L.2a slice 2): include hit object guid in [resolve] probe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the existing [resolve] probe line to surface ci.LastCollidedObjectGuid (hit object) + ci.CollidedWithEnvironment (terrain hit flag) + ci.CollideObjectGuids.Count (when >1) so the operator can tell WHICH entity the wall is, not just the wall normal. Tonight's L.2a slice 1 trace caught a clean wall-slide at the Holtburg-area doorway (n=(0,1,0), 122 hit=yes lines), but had no way to attribute the hit to a specific entity — the L.2d sub-direction call (door collision shape vs building wall mesh) needs the entity id to pick the right fix. This extension provides it on the next run. Format change for [resolve] hit field: Before: hit=yes n=(0.00,1.00,0.00) After: hit=yes n=(0.00,1.00,0.00) obj=0xCC0CXXXX hit=yes n=(0.00,1.00,0.00) env hit=yes n=(0.00,1.00,0.00) obj=0xCC0CXXXX env nObj=3 Pure additive within the existing PhysicsDiagnostics.ProbeResolveEnabled gate. No new env var, no new file. Build green. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/AcDream.Core/Physics/PhysicsEngine.cs | 28 +++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/AcDream.Core/Physics/PhysicsEngine.cs b/src/AcDream.Core/Physics/PhysicsEngine.cs index 696a635..4bfcb3e 100644 --- a/src/AcDream.Core/Physics/PhysicsEngine.cs +++ b/src/AcDream.Core/Physics/PhysicsEngine.cs @@ -691,10 +691,30 @@ public sealed class PhysicsEngine string probeCp = ci.ContactPlaneValid ? "valid" : (ci.LastKnownContactPlaneValid ? "lastKnown" : "none"); - string probeHit = collisionNormalValid - ? System.FormattableString.Invariant( - $"yes n=({collisionNormal.X:F2},{collisionNormal.Y:F2},{collisionNormal.Z:F2})") - : "no"; + string probeHit; + if (collisionNormalValid) + { + // L.2a slice 2 (2026-05-12): include the hit object's guid + + // environment flag so we can tell whether the wall is a building + // (CBuildingObj), a door (CC0Cxxxx range), an NPC, or terrain. + // Without this we know the wall normal but not the responsible + // entity — half the L.2d sub-direction call. + string objPart = ci.LastCollidedObjectGuid.HasValue + ? System.FormattableString.Invariant( + $" obj=0x{ci.LastCollidedObjectGuid.Value:X8}") + : ""; + string envPart = ci.CollidedWithEnvironment ? " env" : ""; + int objCount = ci.CollideObjectGuids.Count; + string objCountPart = objCount > 1 + ? System.FormattableString.Invariant($" nObj={objCount}") + : ""; + probeHit = System.FormattableString.Invariant( + $"yes n=({collisionNormal.X:F2},{collisionNormal.Y:F2},{collisionNormal.Z:F2}){objPart}{envPart}{objCountPart}"); + } + else + { + probeHit = "no"; + } Console.WriteLine(System.FormattableString.Invariant( $"[resolve] ent=0x{movingEntityId:X8} in=({currentPos.X:F3},{currentPos.Y:F3},{currentPos.Z:F3}) cell=0x{cellId:X8} tgt=({targetPos.X:F3},{targetPos.Y:F3},{targetPos.Z:F3}) out=({probePost.X:F3},{probePost.Y:F3},{probePost.Z:F3}) cell=0x{sp.CheckCellId:X8} ok={ok} groundedIn={isOnGround} cp={probeCp} hit={probeHit} walkable={sp.HasLastWalkablePolygon}")); } From a068292f2ad5162736bf56677a92646beb340d41 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 12 May 2026 18:06:05 +0200 Subject: [PATCH 3/4] feat(phys L.2a slice 3): populate CollisionInfo entity attribution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CollisionInfo.CollideObjectGuids list + LastCollidedObjectGuid fields existed but were never written anywhere in the codebase — slice 2's [resolve] probe found this when 85 hit=yes lines came back with no obj= attribution. This commit fills the gap at the only place we have the attribution data: the per-object iteration in Transition.FindObjCollisions, where obj.EntityId is in scope right after each per-object BSPQuery / CylinderCollision call. Two cases trigger an Add(): - result != TransitionState.OK (object hard-blocked transition) - normal flipped invalid→valid during the call (BSPQuery captured a slide normal without halting — covers wall-slide cases). Beyond the diagnostic, this also fixes a quiet structural gap — any future physics behavior that wants "who did I just collide with" (PvP exemption sanity check, NPC bump rules, etc.) was previously flying blind on stub fields. Now the data flows. Build green. Will re-test the doorway with the same trace to get the wall's entity id. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/AcDream.Core/Physics/TransitionTypes.cs | 25 +++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/AcDream.Core/Physics/TransitionTypes.cs b/src/AcDream.Core/Physics/TransitionTypes.cs index f2c4f6c..1a3a12f 100644 --- a/src/AcDream.Core/Physics/TransitionTypes.cs +++ b/src/AcDream.Core/Physics/TransitionTypes.cs @@ -1389,6 +1389,7 @@ public sealed class Transition var sp = SpherePath; var oi = ObjectInfo; + var ci = CollisionInfo; // #42 diagnostic (2026-05-05): identify which static object causes // the airborne first-frame ~1m push. Capture sphere check pos at @@ -1460,6 +1461,14 @@ public sealed class Transition if (CollisionExemption.ShouldSkip(obj.State, obj.Flags, ObjectInfo.State)) continue; + // L.2a slice 3 (2026-05-12): snapshot collision-normal state so + // we can tell whether THIS object's BSP/CylSphere test produced a + // new collision (BSPQuery sets the normal but may still return OK + // for slide cases). Together with the `result != OK` check below + // this populates ci.CollideObjectGuids + LastCollidedObjectGuid so + // the [resolve] probe surfaces the responsible entity id. + bool collisionWasValidPre = ci.CollisionNormalValid; + TransitionState result; if (obj.CollisionType == ShadowCollisionType.BSP) @@ -1523,6 +1532,22 @@ public sealed class Transition result = CylinderCollision(obj, sp); } + // L.2a slice 3: attribute the collision (if any) to this entity. + // Two cases: + // - result != OK: the object stopped the transition (hard-block). + // - result == OK but the normal flipped from invalid→valid during + // this call: BSPQuery captured a slide normal without halting. + // Either way this object is responsible for the hit, so add its + // entity id. CollideObjectGuids carries the full chain; the last + // assignment to LastCollidedObjectGuid wins which matches retail's + // "most recent" semantics for the probe. + if (result != TransitionState.OK + || (!collisionWasValidPre && ci.CollisionNormalValid)) + { + ci.CollideObjectGuids.Add(obj.EntityId); + ci.LastCollidedObjectGuid = obj.EntityId; + } + if (result != TransitionState.OK) { if (airborneDiag) From eb401e8cac1f3ed26ac36b9fbbfa1a102a562954 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 12 May 2026 18:15:16 +0200 Subject: [PATCH 4/4] docs(phys L.2a): handoff + next-session prompt + CLAUDE.md / plan-of-record updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents the L.2a-slice-1/2/3 ship for the next session and updates the project's source-of-truth docs to reflect current state. New files: - docs/research/2026-05-12-l2a-shipped-l2d-handoff.md — full cold-start handoff: what shipped, three findings (L.2e cell-id format gap, L.2c wall-slide working, L.2d sub-direction = CBuildingObj port), branch state, diagnostic surface inventory, files changed, open concerns, cold-start checklist, reproduction recipe. - docs/research/2026-05-12-l2d-next-session-prompt.md — terse copy-paste prompt for the next Claude Code session. Modified: - docs/plans/2026-04-29-movement-collision-conformance.md — added "Current sub-direction" note under L.2d capturing the evidence-driven call: building-mesh fidelity issue, not door-state-toggle. - CLAUDE.md — updated "Currently between phases" → "Currently in Phase L.2"; added L.2a shipped paragraph; added ACDREAM_PROBE_RESOLVE / ACDREAM_PROBE_CELL to Diagnostic env vars; prepended L.2d brainstorm to next-phase candidates list. No code changes in this commit. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 46 ++++- ...26-04-29-movement-collision-conformance.md | 17 ++ .../2026-05-12-l2a-shipped-l2d-handoff.md | 176 ++++++++++++++++++ .../2026-05-12-l2d-next-session-prompt.md | 51 +++++ 4 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 docs/research/2026-05-12-l2a-shipped-l2d-handoff.md create mode 100644 docs/research/2026-05-12-l2d-next-session-prompt.md diff --git a/CLAUDE.md b/CLAUDE.md index f883018..f88e77f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -526,8 +526,31 @@ acdream's plan lives in two files committed to the repo: acceptance criteria. Do not drift from the spec without explicit user approval. -**Currently between phases.** Phase C.1.5b just shipped; next phase is -the user's call from the candidate list below. +**Currently in Phase L.2 (Movement & Collision Conformance).** L.2a slices +1+2+3 shipped 2026-05-12 (this evening); the natural next step is the +L.2d slice 1 brainstorm / design spec. Cold-start prompt for the next +session: [`docs/research/2026-05-12-l2d-next-session-prompt.md`](docs/research/2026-05-12-l2d-next-session-prompt.md). +Full handoff: [`docs/research/2026-05-12-l2a-shipped-l2d-handoff.md`](docs/research/2026-05-12-l2a-shipped-l2d-handoff.md). + +**Phase L.2a (Truth & Diagnostics) slices 1-3 shipped 2026-05-12.** +Three commits land the L.2 "make every bad movement outcome explainable" +diagnostic foundation. Slice 1 (`ebef820`) adds runtime-toggleable +`ACDREAM_PROBE_RESOLVE` (one `[resolve]` line per +`PhysicsEngine.ResolveWithTransition` call) + `ACDREAM_PROBE_CELL` (one +`[cell-transit]` line per `PlayerMovementController.CellId` change), +both backed by a new `AcDream.Core.Physics.PhysicsDiagnostics` static +class and mirrored as DebugPanel checkboxes. Slice 2 (`e0c08bc`) extends +the `[resolve]` line with `obj=0x...` attribution. Slice 3 (`a068292`) +populates the previously-stub `CollisionInfo.CollideObjectGuids` / +`LastCollidedObjectGuid` (declared in `TransitionTypes.cs` but never +written anywhere) at the per-object iteration in `FindObjCollisions`, +so the slice-2 promise is now actually delivered. Visual-verified at +the Holtburg Town doorway: probes captured 140 wall hits attributed to +`obj=0xA9B47900` (landblock-baked static = the building itself, +**NOT** a door entity), confirming L.2d sub-direction as **port +`CBuildingObj` collision + per-cell walkability** rather than door- +state-toggle. Plus a definitive L.2e finding: player `CellId` tracked +as bare low byte (`0x00000029`) with no landblock prefix. **Phase C.1.5b (per-part PES transforms + dat-hydrated entity DefaultScript) shipped 2026-05-12.** Closes issue #56. `SetupPartTransforms.Compute(setup)` @@ -585,6 +608,13 @@ together comprise the streaming + rendering perf foundation for the project. **Next phase candidates (in rough preference order):** +- **L.2d slice 1 brainstorm + spec** (`docs/research/2026-05-12-l2d-next-session-prompt.md`). + Direct continuation of tonight's L.2a evidence: port `CBuildingObj` collision + + per-cell walkability so doorway gaps are walkable. Unblocks "walk into a + building" + sets up G.3 dungeon streaming. **Note:** triage the 8 pre-existing + test failures first (none introduced by L.2a slices — verified by stash + rerun + — but most touch movement/physics code L.2d will evolve). See the handoff doc's + "Open concerns" section. - **Triage the chronic open-issue list** in `docs/ISSUES.md` — #2 (lightning), #4 (sky horizon-glow), #28 (aurora), #29 (cloud thinness), #37 (humanoid coat), #50 (stray tree), #41 (remote-motion blips) have been open since @@ -744,6 +774,18 @@ via `PlayerMovementController.ApplyServerRunRate`) or from diagnostics (`[UM_RAW]`, `[SCFAST]`, `[SCFULL]`, `[SETCYCLE]`, `[FWD_WIRE]`, `[OMEGA_DIAG]`, `[SEQSTATE]`, `[PARTSDIAG]`, `[VEL_DIAG]`, `[UPCYCLE]`). Heavy. +- `ACDREAM_PROBE_RESOLVE=1` — L.2a slice 1+2+3 (2026-05-12). One + `[resolve]` line per `PhysicsEngine.ResolveWithTransition` call: + input + target + output position/cell, ok-vs-partial, grounded-in, + contact-plane status, wall normal if hit, **responsible entity + guid** (post-slice-3 attribution plumbing), env flag, walkable + polygon valid. Heavy (~30 Hz × every entity). Runtime-toggleable + via the DebugPanel "Diagnostics" section if `ACDREAM_DEVTOOLS=1`. +- `ACDREAM_PROBE_CELL=1` — L.2a slice 1 (2026-05-12). One + `[cell-transit]` line per `PlayerMovementController.CellId` + change: old → new cell, world position, reason tag + (`resolver` / `teleport`). Low volume — only fires on actual cell + crossings. Runtime-toggleable via the same DebugPanel section. - *(retired 2026-05-05 by L.3 M2/M3)* `ACDREAM_INTERP_MANAGER` was an env-var gate on an experimental per-tick remote motion path. L.3 M2 (commit 40d88b9) replaced both gates (`OnLivePositionUpdated` + diff --git a/docs/plans/2026-04-29-movement-collision-conformance.md b/docs/plans/2026-04-29-movement-collision-conformance.md index cdabd45..dfe8057 100644 --- a/docs/plans/2026-04-29-movement-collision-conformance.md +++ b/docs/plans/2026-04-29-movement-collision-conformance.md @@ -169,6 +169,23 @@ fallback. - Audit `Setup.Radius` and cylinder fallback behavior against retail before relying on them for conformance. +Current sub-direction (2026-05-12, evidence-driven by L.2a slice 2 + 3): +The "I can't walk through doorways" symptom at Holtburg is **NOT a door- +state-toggle issue**. The `[resolve]` probe captured 140 hit=yes lines +at the doorway with `obj=0xA9B47900` (126 hits) — a landblock-baked +static in the `0xLLLLxxxx` range, i.e. the **building itself**, not a +door entity (no `0xCC0Cxxxx`-range hits). The building's baked collision +mesh is treated as one solid block; the doorway gap that's visible in +the rendered mesh isn't represented in the collision data we consume. + +L.2d slice 1's scope is therefore the `CBuildingObj` + per-cell +walkability port (interpretation 2 of the handoff). The named retail +anchors `CCellStruct::point_in_cell`, `CCellStruct::sphere_intersects_cell`, +`CCellStruct::box_intersects_cell`, `CBuildingObj::find_building_collisions` +are the entry points. Spec to be written at +`docs/superpowers/specs/2026-05-13-l2d-cbuildingobj-collision-design.md`. +Handoff: [docs/research/2026-05-12-l2a-shipped-l2d-handoff.md](../research/2026-05-12-l2a-shipped-l2d-handoff.md). + ### L.2e - Cell Ownership: Outdoor Seams, CELLARRAY, cell_bsp Goal: the resolver knows which cell owns the movement and which adjacent cells diff --git a/docs/research/2026-05-12-l2a-shipped-l2d-handoff.md b/docs/research/2026-05-12-l2a-shipped-l2d-handoff.md new file mode 100644 index 0000000..9ecfcaa --- /dev/null +++ b/docs/research/2026-05-12-l2a-shipped-l2d-handoff.md @@ -0,0 +1,176 @@ +# L.2a shipped — L.2d direction confirmed — Cold-Start Handoff + +**Created:** 2026-05-12 evening, immediately after the L.2a-slice-1/2/3 work landed and visual-verified. +**Audience:** the next agent picking up Phase L.2 (Movement & Collision Conformance). +**Purpose:** give you everything you need to start L.2d brainstorming cold, without spelunking through this session's transcript. + +--- + +## TL;DR + +Phase L.2a (Truth & Diagnostics) shipped three slices tonight. They surfaced **three concrete L.2 findings** with reproducible evidence — converting "we should look at this someday" theories into "here is the entity id, here is the wall normal, here is the cell id." With those findings in hand, the next concrete physics work in the L.2 roadmap is **L.2d slice 1 — port `CBuildingObj` collision so doorway gaps are walkable.** Brainstorm + spec, then port. + +**Three slices shipped to `claude/intelligent-poitras-b2c4f9`:** + +| Commit | What | Why | +|---|---|---| +| [`ebef820`](.) | L.2a slice 1: `[resolve]` + `[cell-transit]` probes + DebugPanel mirror | Foundation for every later L.2 change to be evidence-driven | +| [`e0c08bc`](.) | L.2a slice 2: surface hit object guid in `[resolve]` line | Tell us WHICH entity is the wall, not just the wall normal | +| [`a068292`](.) | L.2a slice 3: populate the previously-stub `CollisionInfo.CollideObjectGuids` / `LastCollidedObjectGuid` | Slice 2 found these fields were declared but never written — fixed the structural gap | + +--- + +## Three findings from the L.2a probes + +All produced by walking around Holtburg + pushing W into a Town doorway with `ACDREAM_PROBE_RESOLVE=1 ACDREAM_PROBE_CELL=1`. + +### Finding 1 — L.2e cell-id format gap (DEFINITIVE) + +The player's tracked `CellId` is being recorded as a **bare low byte** (`0x00000029`), with no landblock prefix. AC cell ids are normally `0xLLLLCCCC` — landblock id (4 hex digits) + cell-within-landblock (4 hex digits, `0x0001-0x00FF` outdoor or `0x0100+` indoor). + +Evidence from a tonight log: + +``` +[cell-transit] 0x00000001 -> 0x00000029 pos=(132.585,21.015,94.000) reason=resolver +``` + +NPCs in the same area show MIXED forms in their resolve lines: +- `cell=0xA9B3000E` ← full landblock-prefixed (correct) +- `cell=0x00000032` ← bare low byte (matches the bug shape) + +Likely source: `ResolveOutdoorCellId(...)` at [src/AcDream.Core/Physics/PhysicsEngine.cs:687](src/AcDream.Core/Physics/PhysicsEngine.cs:687) — that's the function that ResolveWithTransition routes the output cell id through before returning. Worth grepping for its body. + +This is the L.2e blocker per the plan-of-record: +> *"Update low outdoor cell id across 24m cell boundaries and landblock seams. Port the retail adjacent-cell search: `find_cell_list`, `check_other_cells`, and `adjust_check_pos`."* + +### Finding 2 — L.2c wall-slide is working + +The transition layer at this spot does the retail-faithful thing: + +``` +[resolve] ent=0x000F4240 in=(132.067,17.567,94.000) cell=0x00000029 + tgt=(132.239,17.172,94.000) + out=(131.938,17.567,94.000) cell=0x00000029 + ok=True groundedIn=True cp=valid hit=yes n=(0.00,1.00,0.00) + obj=0xA9B47900 walkable=True +``` + +- Wall normal `(0, 1, 0)` — vertical wall facing +Y, captured correctly. +- `out` shows the position clamped along the wall: X slid back from 132.067 → 131.938, Y preserved. +- `ok=True` — resolver completed normally (no `ok=False` anywhere in the trace, 0/140). + +**No L.2c work needed at this site.** Edge-slide / wall-slide port from earlier (per the plan-of-record's L.2c "Current shipped slice" note) is doing its job here. + +### Finding 3 — L.2d sub-direction = CBuildingObj port (NOT door-toggle) + +All 140 hit=yes lines in the doorway-push test came back with the **same dominant `obj=` attribution**: + +| obj | hits | range | what it is | +|---|---|---|---| +| **`0xA9B47900`** | **126** | `0xLLLLxxxx` (landblock-baked static) | The Holtburg building itself — its baked collision mesh | +| `0x000F4245` | 14 | `0x000Fxxxx` (local-spawn entity) | An NPC standing near the doorway | + +`0xA9B4` matches the Holtburg landblock prefix we logged at startup (`loading world view centered on 0xA9B4FFFF`). The `0x7900` low bytes is its landblock-local entity id. **It's the building's baked collision shape — not a door entity, not a creature.** + +**Implication:** the "doorway is blocked" symptom is NOT a door-collision-not-toggled bug (which would have shown a door-range entity id, typically `0xCC0Cxxxx`). It's a **building-mesh fidelity issue**: the building's baked collision data we're loading represents the building as a solid block with no walkable opening where the visual doorway is. + +Two non-mutually-exclusive interpretations: +1. **Collision-mesh extraction is wrong** — we load building geometry but don't respect the BSP nodes that encode doorway openings. +2. **`CBuildingObj` + per-cell walkability is not ported** — retail uses a per-cell `CObjCell` structure that maps "this interior cell is reachable" / "this exterior cell connects to those interior cells." Without that, we treat the building as one opaque collision volume. + +The plan-of-record's L.2d goal: +> *"Preserve enough building identity to model `CBuildingObj` collision and `bldg_check` behavior."* + +points at interpretation 2 as the canonical fix. + +--- + +## What this session deliberately did NOT do + +- **Other L.2a slices** (contact-plane probe, ShadowObject hit log, water probe, real-DAT fixture-capture pipeline). Slice 1 + 2 + 3 cover the most-load-bearing case (resolver outcomes + cell transits + entity attribution). The remaining diagnostics serve future L.2 work and can ship opportunistically. +- **L.2d implementation or brainstorm.** Deliberately parked for a fresh session with this evidence as cold-start context. +- **L.2e implementation.** The cell-id format finding is filed but not investigated. +- **Pre-existing test failures.** 8 tests fail at the branch base (none from these slices — verified by stash + rerun on every test cycle). Not from this slice. See "Open concerns" below. + +--- + +## Branch state at handoff + +- Branch: `claude/intelligent-poitras-b2c4f9` +- Three slice commits ahead of `eab347d` (the C.1.5b merge into main), plus a docs commit that adds this handoff + the next-session prompt + plan-of-record / CLAUDE.md updates. +- Tonight's last code commit was `a068292` (L.2a slice 3); docs commit follows. +- Worktree clean post-docs-commit; merge to main is the user's planned next operation. + +## What's now in the diagnostic surface + +Live env vars (both can be flipped at runtime via the DebugPanel "Diagnostics" section if `ACDREAM_DEVTOOLS=1`): + +- **`ACDREAM_PROBE_RESOLVE=1`** — one `[resolve]` line per `PhysicsEngine.ResolveWithTransition` call: + ``` + [resolve] ent=0xEEEEEEEE in=(x,y,z) cell=0xCCCCCCCC tgt=(x,y,z) out=(x,y,z) cell=0xCCCCCCCC ok=Y/N groundedIn=Y/N cp=valid|lastKnown|none hit=yes n=(nx,ny,nz) obj=0xOOOOOOOO env nObj=N walkable=Y/N + ``` + Heavy: fires for every entity's resolve per physics tick. +- **`ACDREAM_PROBE_CELL=1`** — one `[cell-transit]` line per `PlayerMovementController.CellId` change: + ``` + [cell-transit] 0xOLD -> 0xNEW pos=(x,y,z) reason=resolver|teleport + ``` + Low volume — only fires on actual cell crossings. + +Both backed by `AcDream.Core.Physics.PhysicsDiagnostics` static class (initial from env var, set/get from anywhere at runtime). + +## Files changed in this session + +``` +src/AcDream.Core/Physics/PhysicsDiagnostics.cs (new) +src/AcDream.Core/Physics/PhysicsEngine.cs (modified — probe emission) +src/AcDream.Core/Physics/TransitionTypes.cs (modified — entity attribution plumbing) +src/AcDream.App/Input/PlayerMovementController.cs (modified — UpdateCellId chokepoint) +src/AcDream.UI.Abstractions/Panels/Debug/DebugVM.cs (modified — Probe* forwarder props) +src/AcDream.UI.Abstractions/Panels/Debug/DebugPanel.cs (modified — two new checkboxes) +docs/plans/2026-04-29-movement-collision-conformance.md (modified — shipped-slice note + L.2d sub-direction) +``` + +## Open concerns flagged but NOT addressed in this session + +- **8 pre-existing test failures** on the branch base, verified by stash+rerun: `MotionInterpreterTests.GetMaxSpeed_*` (3), `PositionManagerTests.ComputeOffset_BothActive_Combined`, `PlayerMovementControllerTests.Update_ForwardInput_MovesInFacingDirection`, `DispatcherToMovementIntegrationTests.Dispatcher_W_held_produces_forward_motion`, `BSPStepUpTests.{D4_AirborneMover_TallWall_PersistsSlidingNormalAcrossFrames,C3_Path6_AirborneMoverHitsSteepSlope_SetsCollide}`. Most touch movement/physics code we're about to evolve in L.2b/L.2c/L.2d — **triage before further L.2 work** is recommended. +- **Player entity id quirk.** Local player physics entity id observed as `0x000F4240` in the resolve probe, not the server guid `0x5000000A`. This is presumably the dat/local-spawn entity id — fine for diagnostic, worth keeping in mind for any future "is this the player?" check. + +## Cold-start checklist for L.2d brainstorm + +1. Read this handoff. +2. Read [docs/plans/2026-04-29-movement-collision-conformance.md](docs/plans/2026-04-29-movement-collision-conformance.md) — focus on L.2d section. +3. Read the L.2d named-retail anchors: + - `CCellStruct::point_in_cell`, `CCellStruct::sphere_intersects_cell`, `CCellStruct::box_intersects_cell` + - `CBuildingObj::find_building_collisions` + - `CObjCell::find_cell_list` (already shared with L.2e) + Grep `docs/research/named-retail/acclient_2013_pseudo_c.txt` by `class::method`. +4. Read [src/AcDream.Core/Physics/TransitionTypes.cs:1386](src/AcDream.Core/Physics/TransitionTypes.cs:1386) — current `FindObjCollisions` loop, where building objects currently route through generic BSP/Cylinder paths. +5. Read [src/AcDream.Core/Physics/PhysicsDataCache.cs](src/AcDream.Core/Physics/PhysicsDataCache.cs) — how we currently load BSP / GfxObj data; figure out if building-specific data (interior cells, `CBuildingObj`) is loaded but not consumed. +6. Cross-reference WorldBuilder (`references/WorldBuilder/`) for any building-cell handling already present. +7. Brainstorm the slice (`superpowers:brainstorming` if useful) — scope, named-retail anchors, conformance tests, real-DAT fixtures. +8. Write a spec at `docs/superpowers/specs/2026-05-13-l2d-cbuildingobj-collision-design.md`. +9. Implement in slices with conformance citations in each commit. + +## Reproducing the doorway evidence + +In case you want to re-capture the trace: + +```powershell +# In the project worktree +$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_CELL = "1" +$env:ACDREAM_PROBE_RESOLVE = "1" +dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 | Tee-Object -FilePath "launch.log" +``` + +Walk acdream up to a Holtburg building doorway. Hold W into it for ~2 seconds. Close. Grep `launch.log` for: +- `cell-transit` — cell tracking +- `\[resolve\].*hit=yes` — wall hits with object attribution + +Wall entity should appear as `obj=0xA9B47XXX` for the same Holtburg building, OR a different `0xA9Bxxxxx` for other buildings in the area. diff --git a/docs/research/2026-05-12-l2d-next-session-prompt.md b/docs/research/2026-05-12-l2d-next-session-prompt.md new file mode 100644 index 0000000..bcb7395 --- /dev/null +++ b/docs/research/2026-05-12-l2d-next-session-prompt.md @@ -0,0 +1,51 @@ +# Copy-paste prompt — next session for L.2d brainstorm + +**This file is meant to be pasted verbatim into a new Claude Code session.** It assumes the next session starts on a freshly-merged `main` with the L.2a-slice-1/2/3 work already landed. + +--- + +## Prompt to paste + +> You are picking up Phase L.2d (Movement & Collision Conformance — Shape Fidelity: Sphere / CylSphere / Building Objects) for the acdream project. +> +> The previous session shipped L.2a-slice-1/2/3 (resolver + cell-transit probes + entity attribution plumbing) and used the probes to settle the L.2d sub-direction call: **the wall blocking us at building doorways is a landblock-baked static (`0xA9B47900` for the Holtburg test building), NOT a door entity.** The fix is to port `CBuildingObj` + per-cell walkability so the building's baked collision mesh has walkable openings where doorways are. Door-state-toggle is NOT the issue. +> +> Before writing any code: +> +> 1. **Read the handoff:** `docs/research/2026-05-12-l2a-shipped-l2d-handoff.md` — full context, evidence, file pointers. +> 2. **Read the plan-of-record:** `docs/plans/2026-04-29-movement-collision-conformance.md` — focus on L.2d, and notice that L.2c already shipped most of its work + L.2a is now ~75% covered. +> 3. **Read the named-retail anchors** (grep `docs/research/named-retail/acclient_2013_pseudo_c.txt` by `class::method`): +> - `CCellStruct::point_in_cell` +> - `CCellStruct::sphere_intersects_cell` +> - `CCellStruct::box_intersects_cell` +> - `CBuildingObj::find_building_collisions` +> - `CObjCell::find_cell_list` +> 4. **Read current code:** +> - `src/AcDream.Core/Physics/TransitionTypes.cs:1386` — `FindObjCollisions` (where building objects currently flow through generic BSP path). +> - `src/AcDream.Core/Physics/PhysicsDataCache.cs` — what building-specific data we already load vs ignore. +> 5. **Cross-reference WorldBuilder** at `references/WorldBuilder/` for any building-cell handling we can crib. +> +> Your deliverable for this session: +> +> 1. A brainstorm using `superpowers:brainstorming` if scope is unclear, then +> 2. A design spec at `docs/superpowers/specs/2026-05-13-l2d-cbuildingobj-collision-design.md` covering: +> - Named-retail anchors with line numbers from the PDB pseudo-C +> - Component breakdown (CObjCell port, CBuildingObj port, integration with FindObjCollisions) +> - Conformance test plan (synthetic + real-DAT fixtures at known Holtburg buildings) +> - Slice plan (3-5 commits, each conformance-cited) +> - Acceptance criteria +> 3. After spec approval, implement slice 1. +> +> **Before implementation,** verify the L.2a probes still work — relaunch with `ACDREAM_PROBE_RESOLVE=1 ACDREAM_PROBE_CELL=1 ACDREAM_DEVTOOLS=1`, walk up to the Holtburg test doorway, confirm `[resolve]` lines still show `obj=0xA9B4xxxx` for the wall hits. (Reproduction recipe in the handoff doc's last section.) +> +> Side note: **8 pre-existing test failures** exist on main (verified by stash+rerun in the prior session, none from L.2a slice work). Most touch movement/physics code we're about to evolve. **Triage them before sinking deep L.2d effort** — a recent baseline regression in this area could waste hours of L.2d work. + +--- + +## Reading order if you only have 10 minutes + +1. `docs/research/2026-05-12-l2a-shipped-l2d-handoff.md` — TL;DR + Three findings sections (5 min). +2. `docs/plans/2026-04-29-movement-collision-conformance.md` §L.2d (2 min). +3. `src/AcDream.Core/Physics/TransitionTypes.cs:1386-1543` — current `FindObjCollisions` body (3 min). + +From there, decide whether to brainstorm or jump straight to the spec.