acdream/docs/plans/2026-04-29-movement-collision-conformance.md
Erik b1af56eb19 fix(physics): L.4 — steep airborne hits slide-tangent (interim, deviates from retail)
Phase L.4 closes the "stuck in falling animation on a steep roof" bug
the user reported on 2026-04-30 ("I jump up, I land on it. It should not
even let me land, should just slide with a falling animation"). After
this commit the body no longer sticks to a steep roof when jumping
into it — it slides along the slope while keeping the falling animation.

Two pieces:

1. BSPQuery Path 6 steep-poly slide
   When an airborne sphere hits a polygon whose world normal Z is below
   FloorZ (≈ 0.6642, slope > ~49°), the previous flow was:
   Path 6 SetCollide → Path 4 set_walkable → ContactPlane committed →
   body "lands" on the steep poly with Contact bit + falling animation.
   This left the player stuck mid-slope because OnWalkable was cleared
   but Contact stayed set.

   The new branch detects the steep normal in Path 6 BEFORE SetCollide
   is called. Instead of entering the landing path, it removes the
   into-wall component of the move (project onto the steep face), sets
   CollisionNormal + SlidingNormal, and returns Slid. Same shape as
   Path 5's step-up fallback and CylinderCollision. The resolver retries;
   the sphere is now outside the poly; FindCollisions returns OK;
   ValidateTransition commits the slid position. ContactPlane is never
   set, so the body stays airborne with falling animation.

2. PlayerMovementController L.3a-bounce carve-out + Inelastic stop
   Re-enables the velocity-reflection bounce when the contact normal is
   upward-facing but steeper than walkable (0 < N.Z < FloorZ). The base
   L.3a rule suppresses bounce on landing transitions to avoid micro-
   bounce on flat terrain; that suppression also stuck the player to
   too-steep roofs they shouldn't land on. This carve-out re-enables
   the reflection specifically for the steep upward case.

Also lands related L.2c precipice / edge-slide work that was in flight:

- TransitionTypes EdgeSlideAfterStepDownFailed: walkable-poly-steep
  cliff route + steep-ContactPlane cliff route ordering, so that
  CliffSlide fires when the stored walkable polygon itself is too
  steep (Path 4 had previously accepted it as a "landing" via the
  permissive LandingZ threshold).
- CliffSlide reference-normal selection: prefer LastWalkable, fall back
  to LastKnownContactPlane only when walkable, else use world-up. This
  prevents the cross(steepN, steepN) = 0 degenerate case that left the
  cliff slide as a no-op when both current and last-known were steep.
- Phase 2 / step-down branch / edge-slide branch / cliff-slide
  diagnostic helpers gated on ACDREAM_DUMP_EDGE_SLIDE / ACDREAM_DUMP_STEEP_ROOF.
- Two new airborne-mover regression tests in BSPStepUpTests +
  PhysicsEngineTests covering wall-slide and edge tangent motion.

DEVIATION FROM RETAIL — DOCUMENTED FOR FOLLOW-UP

The Path 6 steep slide is NOT what retail does. Retail's flow on the
same hit is:

  Path 6 SetCollide (no steep check) → Path 4 find_walkable returns
  nothing for steep → Phase 3 reset path: restore_check_pos +
  kill_velocity → return COLLIDED → validate_transition reverts CheckPos
  to CurPos and forces OK.

Net retail behavior: position reverts to pre-failed-move (typically
just below the roof in the common jump-up case), velocity zeroed,
gravity rebuilds Z next frame, body falls back down naturally with
the falling animation. The "freeze" framing I used earlier was wrong;
in the typical case retail just bounces the body off and lets gravity
take over.

Strict retail behavior would match the user's intent better in the
common case AND avoid the bounce-energy-accumulation we saw with the
slide-tangent approach (V grew to ~50 m/s in continuous-contact frames).
However, retail's behavior degenerates in the edge case of an overhead
landing onto a steep slope (body would freeze mid-air above the roof).

This commit ships the slide-tangent fix as an interim "much better"
state per user verification on 2026-04-30. Follow-up work to match
retail strictly: revert Path 6 steep-slide, audit Phase 3 reset to
ensure kill_velocity (matching OBJECTINFO::kill_velocity ->
CPhysicsObj::set_velocity({0,0,0}, 0)) actually fires, and re-test.

Refs:
  - acclient_2013_pseudo_c.txt:323784-323821 (Path 6 SetCollide)
  - acclient_2013_pseudo_c.txt:273191-273239 (Phase 3 reset path)
  - acclient_2013_pseudo_c.txt:272563-272596 (validate_transition revert)
  - acclient_2013_pseudo_c.txt:274467-274475 (kill_velocity)
  - acclient_2013_pseudo_c.txt:282699-282715 (handle_all_collisions bounce)

Tests: 833/833 green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 13:22:07 +02:00

9.7 KiB

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:

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.

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.

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.