acdream/docs/research/2026-06-24-collision-object-detection-vs-retail-deepdive-handoff.md
Erik bc4f71b932 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>
2026-06-24 16:34:13 +02:00

161 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`).