docs(research): handoff — collision-object-detection vs retail deep-dive ("1 fix for all collision")

Next-session brief: build retail's collision-inclusion model (PhysicsState/
ETHEREAL/HAS_PHYSICS_BSP predicate + per-cell shadow-list registration + the
building-shell/terrain/EnvCell channels), map acdream's per-channel ad-hoc
filters against it, enumerate deviations, and design ONE unified retail-faithful
mechanism to replace them. Motivated by this session's #146/#147 — both were the
same shape (a per-channel filter diverging from retail). Brainstorm-gated,
report-first, no guess-patches. Includes the apparatus inventory + a paste-ready
prompt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-24 16:34:13 +02:00
parent 2b48310c2a
commit bc4f71b932

View file

@ -0,0 +1,161 @@
# Handoff — collision-object-detection vs retail: a unifying audit ("1 fix for all collision") — 2026-06-24
**Branch/worktree:** `claude/thirsty-goldberg-51bb9b`. Named-retail + references live in the MAIN repo
`C:\Users\erikn\source\repos\acdream\`.
## The ask (user, 2026-06-24)
> "Deep-dive in how we detect what objects have collision compared to retail. I expect/know there are
> deviations vs retail. Thorough checking of how retail checks what objects have collision and how it
> deviates from our implementation. This should be **1 fix for all collision**."
So: **NOT another whack-a-mole symptom fix.** Build the retail model of *"which objects collide, and how
the engine decides that"*, map acdream's model against it, enumerate every deviation, and design **one
unified, retail-faithful collision-inclusion mechanism** that replaces acdream's scattered per-channel
hand-rolled filters. This is a research → brainstorm → multi-commit phase. **Brainstorm-gate it**
(`superpowers:brainstorming`) before writing the port. **No guess-patches** — this is the DO-NOT-RETRY
collision area; grep named-retail FIRST, decompile, pseudocode, port, conformance-test.
## Why now — the pattern the 2026-06-24 session exposed
This session fixed three collision bugs, and **two of them were the *same shape*: an ad-hoc per-channel
filter that diverges from retail's unified model.**
| Commit | Issue | The deviation it revealed |
|---|---|---|
| `afd5f2a` | #138-B | Avatar relocated into a dead landblock during PortalSpace (lifecycle, not inclusion) |
| `49d743f` | #146 | **Building collision shell cached at a stale `_liveCenter` frame** — terrain re-bases per apply, the building cache didn't. Streaming-relative-frame staleness. |
| `9743537` | #147 | **Portal-less buildings (city/perimeter walls) skipped** by `if (building.Portals.Count == 0) continue;` — a *transit-feature* filter that wrongly gated *collision*. We used "has a doorway" to decide "is solid". |
#147 is the smoking gun for the user's intuition: **acdream decides "does this object collide" with
per-channel attributes that have nothing to do with retail's actual predicate.** There are almost
certainly more of these. The goal is to replace the lot with retail's real rule.
## The retail model — what to research (the oracle)
Retail (`docs/research/named-retail/acclient_2013_pseudo_c.txt` — grep by `class::method`) decides
collision in two layers. Map BOTH precisely:
### A. The inclusion predicate — "does this object participate in collision at all?"
- **`PhysicsState` flags** on `CPhysicsObj` (grep `acclient.h` for the `PhysicsState` enum). The
load-bearing ones:
- **`ETHEREAL`** → no collision (pass-through). Confirm acdream honors this on EVERY channel, not just
interaction-pick. (See `[[project_interaction_pipeline]]` ETHEREAL-alone exemption — that's
*selection*, not *collision*; verify the collision side.)
- **`HAS_PHYSICS_BSP` / `HAS_PHYSICS_BSP_PS`** → the object collides via its BSP; otherwise via a
cylinder/sphere. This is the binary dispatch (`[[feedback_retail_binary_dispatch]]`,
`BspOnlyDispatch` flag `0x00010000`).
- Other gates to check: `STATIC`, `REPORT_COLLISIONS`, `IGNORE_COLLISIONS`, `MISSILE`, `NODRAW` (NODRAW
is render-only — confirm it does NOT gate collision), `SCRIPTED_COLLISION`.
- **The object's physics shape**: does it have a `CPhysicsBSP` (collision BSP) vs a `CylSphere`? Retail
uses whatever the dat/weenie says — it does NOT fabricate collision from a render mesh AABB. (acdream's
`IsPhantomGfxObjSource` / mesh-AABB-fallback is a known divergence — #101.)
### B. The registration / dispatch model — "where does the engine look for colliders?"
- **`CPhysicsObj::FindObjCollisions`** (the per-tick collider) — picks BSP-only vs cyl+sphere on the
flag above, iterates the **per-cell `shadow_object_list`**.
- **`CObjCell::add_shadows_to_cells` / `find_cell_list` / `calc_cross_cells`** — an object registers a
*shadow* into every cell its sphere overlaps; the per-cell list is the broad phase. (acdream ported
this as the A6.P4/BR-7 `ShadowObjectRegistry` flood — see `[[project_physics_collision_digest]]`.)
- **`CBuildingObj::find_building_collisions`** (`0x006b5300`) — the building **shell** BSP, tested on
`part_array->parts[0]`, **independent of the portal list**. (This session's #147 fix aligned acdream
to this.) `CLandBlock::init_buildings` (`0x0052fd80`) builds one building per origin landcell.
- **Terrain**: `CLandCell::find_env_collisions` (terrain polys) + the eye-side cull.
- **EnvCell (indoor)**: `CEnvCell::find_env_collisions` (`0x0052c100`) — the cell's own BSP; **no
building leg** (acdream matches this at `TransitionTypes.cs:2807`).
**Deliverable from this layer:** a single table — for each object class (terrain, building shell,
EnvCell shell, static stab, scenery, server weenie/door/NPC/item, missile) — *what makes it collide in
retail* (which flags, which shape, which registration), with the named-retail function + address.
## The acdream model — the code map to audit against the table
acdream has **separate channels with different inclusion filters**. Walk each and record its de-facto
"does it collide" predicate, then diff against retail's:
1. **Terrain**`PhysicsEngine.SampleTerrainWalkable` / `TransitionTypes.cs:2229` (`// no terrain loaded
→ allow pass-through`, `:2247`). Re-bases per apply via `AddLandblock(origin)` (so no #146-style
staleness). Filter: terrain residency only.
2. **Buildings**`TransitionTypes.FindBuildingCollisions` (`:2805`), `PhysicsDataCache.CacheBuilding`
(`:441`), the cache loop (`GameWindow.cs` ~`:6958`). Post-session: re-bases per apply (#146) +
includes portal-less (#147). Remaining filter: `ModelId == 0 → inert` (`:2813`); the `[bldg-channel]`
probe (`ACDREAM_PROBE_BUILDING`, logs `bldOrigin`) is the live lens.
3. **Shadow objects** (statics, scenery, server weenies) — `Core/Physics/ShadowObjectRegistry.cs`
(per-cell flood, BR-7). Registration sites: `GameWindow.cs` `ShadowObjects.Register*` (~`:71967526`),
`RefloodLandblock` (`:7595`), `RegisterMultiPart` (`:3835`). Server weenies register via
`OnLiveEntitySpawnedLocked` → Register. **Filters to scrutinize for divergence:**
- `LandblockLoader.IsSupported` (GfxObj/Setup mask only) — does it match retail's "has a physics
shape"? What about `0x0D`/other types?
- **mesh-AABB fallback** / `IsPhantomGfxObjSource` (#101) — acdream fabricates a collision box from
the render mesh when no physics BSP exists. **Retail does not.** Prime deviation.
- cyl-vs-BSP dispatch (A6.P7, `Transition.BspOnlyDispatch`) — verify it matches `HAS_PHYSICS_BSP_PS`.
- editor-marker degrade-hide (#136) — those are render-hidden; confirm collision treatment matches.
- ETHEREAL: does the live-spawn path exclude ETHEREAL weenies from shadow registration?
4. **EnvCell (indoor)** — cell BSP via the transition; `CellTransit` per-cell iteration.
5. **The streaming-relative frame**#146 proved a cached collision transform can go stale on recenter.
**Audit every cached collision transform** (shadow objects? EnvCell? portal planes?) for the same
staleness; or fold into the #145 cell-relative-frame port so collision frames can NEVER go stale.
## Suspected deviations to confirm/refute (seed list)
- **Attribute-as-filter category errors** (the #147 pattern): grep the registration/collision paths for
any `continue`/skip gated on a NON-collision property (portals, name, render flags, degrade, item
type). Each is a candidate divergence.
- **mesh-AABB phantom collision** (#101) — retail-absent; should it exist at all?
- **ETHEREAL not honored on the collision side** of one or more channels.
- **`IsSupported`/type filters** excluding objects retail collides with (or including ones it doesn't).
- **Stale cached collision transforms** beyond buildings (#146 class).
- **Static vs dynamic registration mismatch** — landblock stabs/scenery vs server weenies taking
different inclusion paths with different rules.
## The unifying target ("1 fix for all collision")
One retail-faithful **collision descriptor per object**, derived once from its `PhysicsState` + physics
shape (BSP vs cyl, or none = ethereal/no-collide), and **one registration rule** (overlap-based per-cell
shadow lists) used by **every** channel — replacing the per-channel ad-hoc filters. Concretely the
output is likely: (a) a single `ObjectCollides(state, shape)` predicate matching retail's flag logic,
(b) a single registration path all object sources funnel through, (c) deletion of the mesh-AABB phantom
fallback and the attribute-as-filter skips. Expect multi-commit; gate with brainstorming; conformance-
test against named-retail golden values + the existing replay harnesses (`CellarUp*`, `HouseExitWalk*`,
`CornerFlood*`, the new `Issue147ArwicBuildingsDumpTests` fixture).
## Method (mandatory — CLAUDE.md workflow)
1. `superpowers:brainstorming` to scope the phase + agree the unified model BEFORE code.
2. **Grep named-retail FIRST** for every function in the retail-model table; decompile via Ghidra MCP
(`patchmem.gpr`, port 8081) for any single function that's noisy in the bulk text.
3. Cross-reference **ACE** `Physics/Common` (PhysicsObj, ObjCell, shadow lists) + **ACViewer**
`Physics/Common` + **holtburger** `spatial/physics.rs` for the client-side read.
4. Write pseudocode (`docs/research/*_pseudocode.md`), port faithfully, conformance-test, then one
comprehensive user visual gate.
## Apparatus already in place (use it, don't rebuild)
- `ACDREAM_PROBE_BUILDING``[bldg-channel]` (now logs `bldOrigin`) + `[entity-source]` (BSP vs Cylinder
classification per static — directly shows what collision shape each object got).
- `ACDREAM_CAPTURE_RESOLVE=<path>` → per-player-resolve JSONL (grounded / contactPlane / collisionNormal
per frame). Heavy (FPS) — short targeted runs only; the `analyze_*` streaming-tally pattern works
(255k records in seconds).
- `ACDREAM_PROBE_ENT` / `EntityVanishProbe` → avatar draw-set lifecycle (#138-B).
- `Issue147ArwicBuildingsDumpTests` → the dat-dump pattern for `LandBlockInfo.Buildings`/`.Objects`
(portal counts, model ids); clone it for any landblock/object-class inventory.
- Live client: user `notan` / `MittSnus81!`, ACE `127.0.0.1:9000`; graceful close before rebuild.
## Context — what shipped this session (the motivation)
`afd5f2a` #138-B avatar-vanish, `49d743f` #146 building-frame re-base, `9743537` #147 portal-less-wall
collision. ISSUES #146 DONE, #147 walls DONE (terrain-3%-grounded residual re-scoped LOW — verify it's
even real before acting; see #147). The deep #145 cell-relative-frame port is still its own future phase
and is the natural home for "collision frames can never go stale."
## Paste-ready prompt for the next session
> Deep-dive + unifying fix: **how retail decides which objects have collision, vs acdream.** Read this
> handoff (`docs/research/2026-06-24-collision-object-detection-vs-retail-deepdive-handoff.md`) and the
> physics digest (`claude-memory/project_physics_collision_digest.md`) FIRST. Goal: build retail's
> collision-inclusion model (the `PhysicsState`/ETHEREAL/HAS_PHYSICS_BSP predicate + the per-cell
> shadow-list registration + the building-shell/terrain/EnvCell channels — grep named-retail, cite
> addresses), map acdream's per-channel filters against it (terrain / FindBuildingCollisions /
> ShadowObjectRegistry / EnvCell / mesh-AABB phantom / IsSupported / the cyl-BSP dispatch), enumerate
> every deviation, and propose ONE unified retail-faithful collision-inclusion mechanism to replace the
> ad-hoc filters. **Report-first / brainstorm-gated, no guess-patches** (DO-NOT-RETRY collision area).
> Recent precedent (the per-channel filter divergences this is meant to end): #146 (`49d743f`),
> #147 (`9743537`).