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>
338 lines
16 KiB
Markdown
338 lines
16 KiB
Markdown
# Phase L.2 - Movement & Collision Conformance
|
|
|
|
**Status:** ACTIVE planning document, created 2026-04-29.
|
|
**Roadmap owner:** Phase L.2 in `docs/plans/2026-04-11-roadmap.md`.
|
|
**Scope:** player movement prediction, retail collision/transition behavior,
|
|
building boundaries, edge and wall sliding, cell ownership, outbound movement
|
|
packets, and server-correction diagnostics.
|
|
|
|
## Purpose
|
|
|
|
Phase B.3 shipped the first usable physics foundation: terrain contact,
|
|
basic resolver behavior, streaming-populated collision inputs, and enough
|
|
movement wire support to walk on ACE. That was not the complete retail
|
|
collision system.
|
|
|
|
Phase L.2 is the conformance program that turns that foundation into a
|
|
retail-faithful movement stack. It is the single organizing bucket for work
|
|
that otherwise looks scattered across B.3 physics, L.1 animation/motion, and
|
|
G.3 dungeon/portal space.
|
|
|
|
The active movement spine is:
|
|
|
|
```text
|
|
input + motion command
|
|
-> local body prediction / root-motion source
|
|
-> PhysicsEngine.ResolveWithTransition
|
|
-> TransitionTypes + BSPQuery + ShadowObjectRegistry contact/cell result
|
|
-> MoveToState / AutonomousPosition outbound packets
|
|
-> server echo or correction diagnostics
|
|
```
|
|
|
|
Live ACE accepting a position, or the absence of visible rubber-banding, is
|
|
not proof of retail collision parity. ACE can tolerate coarse or locally
|
|
invalid fine-grained movement. L.2 therefore requires retail-decomp evidence,
|
|
synthetic conformance tests, real-DAT fixtures, and live retail-observer checks.
|
|
|
|
## Current Foundation
|
|
|
|
Already active in acdream:
|
|
|
|
- `PhysicsEngine.ResolveWithTransition` is the local player collision path.
|
|
- `BSPQuery` contains a partial retail-style BSP dispatcher and step/contact
|
|
logic.
|
|
- `TransitionTypes` carries `SpherePath`, `CollisionInfo`, `ObjectInfo`,
|
|
transition validation, step-up/down, and partial slide behavior.
|
|
- `PhysicsDataCache` loads GfxObj, Setup, and CellStruct physics data from DATs.
|
|
- `ShadowObjectRegistry` gives the resolver a broadphase over nearby world
|
|
objects.
|
|
- `TerrainSurface` uses triangle-aware terrain sampling rather than the older
|
|
bilinear placeholder.
|
|
|
|
Known incomplete areas:
|
|
|
|
- Full `CELLARRAY` ownership and `CObjCell::find_cell_list` / adjacent-cell
|
|
checks are not ported.
|
|
- `cell_bsp` / `CellBSP` is not fully represented as a first-class runtime
|
|
owner.
|
|
- Building entry/exit and indoor/outdoor portal transit are not solved by the
|
|
normal walking path.
|
|
- Retail `edge_slide`, `cliff_slide`, and `precipice_slide` behavior is
|
|
incomplete; failed edge/step-down cases often hard-block instead of sliding.
|
|
- `NegPolyHit` handling is a stub relative to the retail transition dispatch.
|
|
- Live entities collapse to a simplified cylinder shape; exact retail
|
|
sphere/cylsphere and object-shape behavior is not yet matched.
|
|
- Outbound contact/cell fields can be too optimistic, so server agreement does
|
|
not necessarily mean local conformance.
|
|
|
|
## Lane Model
|
|
|
|
L.2 uses five working lanes. The roadmap breaks them into six sub-lanes because
|
|
real-DAT and live verification spans every lane.
|
|
|
|
| Lane | Owns | Roadmap slice |
|
|
|---|---|---|
|
|
| Diagnostics | Truth probes, dump flags, server-correction logging, retail observer harness | L.2a, L.2f |
|
|
| Transition parity | `FindTransitionalPosition`, step-up/down, edge-slide, cliff-slide, precipice-slide, `NegPolyHit` dispatch | L.2c |
|
|
| Geometry fidelity | `CSphere`, `CCylSphere`, object shape extraction, building object collision, walkable polygon context | L.2d |
|
|
| Cell/building ownership | outdoor cell seams, low-cell id updates, `CELLARRAY`, `cell_bsp`, building entry/exit | L.2e |
|
|
| Movement/network authority | contact byte, full cell id, MoveToState / AutonomousPosition cadence, root motion vs velocity prediction, correction response | L.2b, L.2f |
|
|
|
|
## Roadmap Slices
|
|
|
|
### L.2a - Truth & Diagnostics
|
|
|
|
Goal: make every bad movement outcome explainable.
|
|
|
|
- Add targeted diagnostics for local placement, contact plane, object hit,
|
|
water, cell id, outbound packet fields, server echo, and correction delta.
|
|
- Keep diagnostics opt-in via env vars and devtools panels.
|
|
- Record enough data for side-by-side retail-observer runs without drowning
|
|
normal logs.
|
|
- 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
|
|
resolver has earned.
|
|
|
|
- Fix outbound contact state so `AutonomousPosition` and `MoveToState` do not
|
|
always claim grounded contact.
|
|
- Track local result cell id and outbound full cell id separately from the last
|
|
server placement until correction proves they agree.
|
|
- Reconcile packet cadence with retail/holtburger references.
|
|
- Wire routine server correction handling and diagnostics, not only portal
|
|
reseating.
|
|
|
|
### L.2c - Transition Parity: Edge / Slide / Neg-Poly
|
|
|
|
Goal: match retail movement at walls, roof edges, step boundaries, and
|
|
precipices.
|
|
|
|
- Port and test `edge_slide`, `cliff_slide`, `precipice_slide`, and
|
|
`step_up_slide` behavior from named retail.
|
|
- Preserve walkable polygon context needed for precipice/edge decisions.
|
|
- Replace `NegPolyHit` stub behavior with the retail dispatch path.
|
|
- Confirm the user-visible rule: walk-only motion is blocked by step,
|
|
edge, walkable, and collision rules; jumping clears `OnWalkable` and only
|
|
succeeds when the airborne path actually clears geometry.
|
|
|
|
Current shipped slice (2026-04-30): wall-adjacent `step_up_slide` feels
|
|
acceptable in live testing; player/remote movers pass `EdgeSlide`; terrain and
|
|
BSP step-down/find-walkable now preserve walkable polygon vertices; failed
|
|
step-down edge cases perform the retail back-probe before
|
|
`SPHEREPATH::precipice_slide`; precipice slide results now re-enter the
|
|
`TransitionalInsert` retry loop so tangent edge motion is preserved instead of
|
|
being reverted by outer validation. Remaining L.2c work is live visual
|
|
confirmation at real building/roof edges, real-DAT building-edge fixtures,
|
|
fuller `cliff_slide` coverage, and `NegPolyHit` dispatch.
|
|
|
|
### L.2d - Shape Fidelity: Sphere / CylSphere / Building Objects
|
|
|
|
Goal: object collisions use retail shape semantics, not one simplified
|
|
fallback.
|
|
|
|
- Finish `CSphere` / `CCylSphere` parity for static and live objects.
|
|
- Stop treating all live entities as one root-centered cylinder.
|
|
- Preserve enough building identity to model `CBuildingObj` collision and
|
|
`bldg_check` behavior.
|
|
- Audit `Setup.Radius` and cylinder fallback behavior against retail before
|
|
relying on them for conformance.
|
|
|
|
Current sub-direction (revised 2026-05-13 evening after slice 1 + 1.5
|
|
shipped and Holtburg-doorway capture analyzed — third reframe):
|
|
L.2d as scoped ("shape fidelity: Sphere / CylSphere / Building Objects")
|
|
is **essentially closed at the Holtburg site that motivated this phase**.
|
|
Building BSP collision works correctly — the slice-1.5 probe captured
|
|
real triangles in plausible world positions for `gfxObj=0x01000A2B` with
|
|
`bspR=13.99m`. The 121 wall hits the L.2a probe attributed to
|
|
`obj=0xA9B47900` were **side effects of the player already being pushed
|
|
back by a separate Door cylinder entity** at the same doorway threshold.
|
|
|
|
The actual blocker is a server-spawned **Door** entity — Setup
|
|
`0x020019FF` named `"Door"` — that ACE places at each Holtburg-town
|
|
building threshold (five doors total observed across `0xA9B40029`,
|
|
`0xA9B40154`, `0xA9B40155`). It registers as a Cylinder shadow entry
|
|
via the server-spawn path; its Cylinder collision blocks the player
|
|
walking into the doorway. That's **door-state handling**, a different
|
|
class of problem from L.2d's shape-fidelity scope — it touches network
|
|
(`CreateObject` PhysicsState bits), interaction (Use action on door
|
|
entity), animation (door open/close), and collision-state-toggle.
|
|
|
|
Recommend: **leave L.2d in "watch-and-wait" mode** with slice 1's probe
|
|
infrastructure in place. No more L.2d slices until a NEW shape-fidelity
|
|
bug is observed at a different site (dungeon walls, stairs, roofs) with
|
|
the probe-armed client. The door-state work becomes its own sub-phase
|
|
(probably nested under B.4 interaction or filed as a new L.2 sub-phase
|
|
like L.2g) scoped separately.
|
|
|
|
Full slice 1 + 1.5 handoff:
|
|
[docs/research/2026-05-13-l2d-slice1-shipped-handoff.md](../research/2026-05-13-l2d-slice1-shipped-handoff.md).
|
|
Design spec (now mostly historical, framing was wrong but probe
|
|
infrastructure shipped from it):
|
|
[docs/superpowers/specs/2026-05-13-l2d-cbuildingobj-collision-design.md](../superpowers/specs/2026-05-13-l2d-cbuildingobj-collision-design.md).
|
|
Predecessor L.2a 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
|
|
must be checked.
|
|
|
|
- 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`.
|
|
- Promote `cell_bsp` / `CellBSP` from partial data to active runtime owner.
|
|
- Hand G.3 a trustworthy building/portal boundary so dungeon streaming is not
|
|
asked to solve collision ownership after the fact.
|
|
|
|
### L.2f - Real-DAT and Live Retail-Observer Conformance
|
|
|
|
Goal: prove the stack against real terrain/building/cell data and what a retail
|
|
client sees when observing acdream.
|
|
|
|
- Add real-DAT fixtures for representative movement cases.
|
|
- Use retail client observer runs to verify motion packets, animation/movement
|
|
coupling, and server-visible placement.
|
|
- Treat ACE acceptance as a coarse compatibility check only.
|
|
- Require conformance notes in tests or research docs for every AC-specific
|
|
algorithm ported under L.2.
|
|
|
|
### L.2g - Dynamic PhysicsState Toggling
|
|
|
|
Goal: server-driven post-spawn state changes (chiefly `ETHEREAL` flips) are
|
|
honored by the local collision stack.
|
|
|
|
Triggered 2026-05-12 evening by the L.2d slice 1.5 trace: the Holtburg
|
|
doorway blocker is a closed Door entity (Setup `0x020019FF`) whose
|
|
`PhysicsState.Ethereal` bit flips when the player Uses the door. The L.2d
|
|
shape-fidelity work doesn't cover this — the door's collision shape is
|
|
already correct; what's missing is honoring the *runtime* state change.
|
|
|
|
Scope is intentionally narrow:
|
|
|
|
- Parse inbound `GameMessageSetState (opcode 0xF74B)`.
|
|
- Plumb the new `PhysicsState` value into `ShadowObjectRegistry`'s cached
|
|
per-entity state so the existing `CollisionExemption.IsExempt(...)` check
|
|
sees the up-to-date bits.
|
|
- Verify the Holtburg inn-door scenario: walk into doorway → blocked, Use
|
|
door → door swings open AND player can walk through, auto-close after
|
|
30s → door closes AND player is blocked again.
|
|
- Confirm the existing `UpdateMotion` pipeline drives `(NonCombat, On/Off)`
|
|
on non-creature entities (door swing animation). If not, one-line fix.
|
|
|
|
Excluded from L.2g scope (deferred):
|
|
|
|
- Door-specific UX polish: "door is locked" sound, creature-AI bump-open.
|
|
- Any Door-specific class hierarchy — generic state-flip infrastructure
|
|
is enough; doors are the verification scenario, not a privileged case.
|
|
|
|
Lane: informal sixth lane "dynamic state." The existing five-lane table
|
|
treats per-entity state as static-after-spawn; L.2g makes it dynamic.
|
|
|
|
Full design spec:
|
|
[docs/superpowers/specs/2026-05-12-l2g-dynamic-physicsstate-design.md](../superpowers/specs/2026-05-12-l2g-dynamic-physicsstate-design.md).
|
|
|
|
M1 critical path: this slice unblocks the *"open the inn door"* demo
|
|
scenario.
|
|
|
|
Current shipped slice (2026-05-12):
|
|
|
|
| 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` |
|
|
|
|
Slice 1 is CODE-COMPLETE: parser + registry mutator + WorldSession
|
|
dispatcher + GameWindow subscriber. 6 new tests pass (3 parser + 3
|
|
registry). Build clean. Per-commit + final integration code reviews
|
|
all approved. **Visual verification deferred to Phase B.4b** — the
|
|
inbound SetState chain can't fire at runtime until B.4b finishes the
|
|
outbound Use handler. See
|
|
[docs/research/2026-05-12-l2g-slice1-shipped-handoff.md](../research/2026-05-12-l2g-slice1-shipped-handoff.md)
|
|
for full evidence + the 4 minor + 1 Important review notes.
|
|
|
|
## Named Retail Anchors
|
|
|
|
Primary source: `docs/research/named-retail/acclient_2013_pseudo_c.txt`.
|
|
Struct source: `docs/research/named-retail/acclient.h`.
|
|
Address lookup: `docs/research/named-retail/symbols.json`.
|
|
|
|
Use these names before falling back to older `docs/research/decompiled/`
|
|
chunks:
|
|
|
|
- `CTransition::find_transitional_position` - `0x0050BDF0`
|
|
- `CTransition::transitional_insert` - `0x0050B6F0`
|
|
- `CTransition::step_up` - `0x0050B610`
|
|
- `CTransition::step_down` - `0x0050B2A0`
|
|
- `CTransition::edge_slide` - `0x0050B3D0`
|
|
- `CTransition::cliff_slide` - `0x0050A6D0`
|
|
- `SPHEREPATH::step_up_slide` - `0x0050C3B0`
|
|
- `SPHEREPATH::precipice_slide` - `0x0050CC80`
|
|
- `SPHEREPATH::adjust_check_pos` - `0x0050CC00`
|
|
- `CTransition::adjust_offset` - `0x0050A370`
|
|
- `CTransition::check_other_cells` - `0x0050AE50`
|
|
- `CPhysicsObj::is_valid_walkable` - `0x0050F530`
|
|
- `CObjCell::find_cell_list` - `0x0052B4E0`
|
|
- `CBuildingObj::find_building_collisions`
|
|
- `CCellStruct::point_in_cell`
|
|
- `CCellStruct::sphere_intersects_cell`
|
|
- `CCellStruct::box_intersects_cell`
|
|
- `CCylSphere::intersects_sphere`
|
|
- `CSphere::intersects_sphere`
|
|
- `CSphere::slide_sphere`
|
|
|
|
## Implementation Order
|
|
|
|
1. Land L.2a diagnostics first. Do not make another physics change blind.
|
|
2. Fix L.2b packet/contact truth so logs and server echoes describe reality.
|
|
3. Port L.2c transition parity in narrow slices with named-retail citations and
|
|
conformance tests.
|
|
4. Improve L.2d shape fidelity where transition parity depends on object
|
|
contact semantics.
|
|
5. Land L.2e cell/building ownership before G.3 dungeon/portal work relies on
|
|
indoor/outdoor walking.
|
|
6. Promote each synthetic case to L.2f real-DAT and live observer coverage.
|
|
|
|
## Acceptance
|
|
|
|
- A developer can name the active movement path and the current incomplete
|
|
pieces without reading old chat logs.
|
|
- `dotnet build` and `dotnet test` stay green for each implementation slice.
|
|
- Every AC-specific port cites named retail decomp or a documented fallback.
|
|
- Real-DAT fixtures cover buildings, walls, roof edges, outdoor seams, and at
|
|
least one dungeon/building entrance path before L.2 is marked shipped.
|
|
- Retail observer view and acdream local view both agree on contact, position,
|
|
and movement state for the representative cases.
|