L.2d as scoped is essentially closed at the Holtburg site. The slice-1.5 trace settled the question: the "I can't walk through doorways" symptom is a closed Door entity (Setup 0x020019FF named "Door") at the building threshold, not a building-BSP-collision issue. Building BSP is healthy. The two prior framings turned out wrong: - L.2a handoff (2026-05-12): "per-cell walkability missing" — based on hit attribution pointing at the building, missed the Door cylinder also colliding per tick. - L.2d slice 1 spec (2026-05-13 morning): "BSP shape fidelity, three hypotheses X/Y/Z" — ruled out by the trace once the probe labeling bug was fixed in slice 1.5. Handoff doc captures full evidence, side findings (building double- registration latent bug, missing PhysicsState in entity-source log), and a candidates list for the next-session ordering discussion. Plan-of-record L.2d sub-direction paragraph updated to match: "watch- and-wait" mode, no more slices until a new shape-fidelity bug is observed at a different site. Door-state handling becomes its own sub-phase, scope deferred to project-ordering discussion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
282 lines
13 KiB
Markdown
282 lines
13 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.
|
|
|
|
## 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.
|