# Collision-inclusion audit — retail vs acdream ("1 fix for all collision") — 2026-06-24 **Mode:** report-first / brainstorm-gated (DO-NOT-RETRY collision area). No code written. **Method:** 10 parallel named-retail readers (5 retail-model + 5 acdream-channel) → synthesis → **adversarial verification of every claimed deviation against the actual cited code** (the [[feedback_verify_subagent_claims_against_source]] discipline). Plus orchestrator first-hand reads of `CollisionExemption.cs`, `EntityCollisionFlags.cs`, `PhysicsDataCache.cs`, `TransitionTypes.cs`, `GameWindow.cs:7388-7565`, and the divergence register. **Branch:** `claude/thirsty-goldberg-51bb9b`. Named-retail + `references/{ACE,ACViewer,holtburger}` + `symbols.json` live in the MAIN repo. --- ## TL;DR — the unifying insight Retail's collision model is **two clean layers**: - **Layer B (registration / broad phase)** — every object with a *DAT physics shape* is written **unconditionally** into the per-cell `shadow_object_list` of every cell its sphere overlaps. **No attribute is tested at registration** (not STATIC, not ETHEREAL, not HIDDEN — only `PARTICLE_EMITTER_PS 0x1000` diverts to a particle list). The only thing that decides "is this registerable" is **does it have a shape** (`physics_bsp` from DAT bit-0, or a `CSetup` cylsphere/sphere). Source: `CObjCell::find_cell_list@0x0052b4e0`, `CPhysicsObj::add_shadows_to_cells@0x00514ae0`. - **Layer A (inclusion predicate / narrow phase)** — `CPhysicsObj::FindObjCollisions@0x0050f050` decides per-query whether the registered object actually blocks, via a flag predicate (ETHEREAL+IGNORE, viewer-vs-creature, IGNORE_CREATURES, PvP pool), then dispatches **one** shape branch (BSP-only iff `HAS_PHYSICS_BSP_PS 0x10000`, else CylSphere-then-Sphere). **acdream already has this two-layer shape** (per-cell `ShadowObjectRegistry` + query-time `CollisionExemption`). The user's intuition is correct, and the divergences are now **narrow and enumerable** — they are NOT a scattered mess of per-channel filters anymore (the #147 portal-count skip was the last of that family and is already deleted in `9743537`). What remains: 1. **The shape-source rule is impure** — acdream fabricates collision from the **render-mesh AABB** for scenery + outdoor meshes (D1, the prime deviation; retail *never* consults render bounds). This is the one place acdream **over-includes** vs retail. 2. **The query predicate is incomplete** — ETHEREAL-alone short-circuit (D2), no true sphere primitive (D3), PvP/missile terms hardcoded false (watch-item W1). 3. **One cached collision transform can go stale** — EnvCell cell-struct cache lacks the per-apply rebase that buildings got in #146 (D8). Plus two indoor-channel omissions that are latent today (D4 entry-restrictions, D5 obstruction_ethereal). So **"1 fix for all collision" = make the shape-source rule pure (DAT-only) + complete the query predicate + give every cached transform the same per-apply rebase.** It is a *unification of three things*, not one line — but it collapses to **one `ObjectCollides(...)` predicate + one registration rule + one rebase rule** that every channel funnels through. --- ## Verdict table (6 confirmed deviations, 2 refuted false-positives) | ID | Sev | Layer | Title | Verdict | Register | |----|-----|-------|-------|---------|----------| | **D1** | **HIGH** | shape-source | Render-mesh-AABB → synthetic collision cylinder for scenery (#101 residual) | **CONFIRMED** (high) | extend **AP-2** (currently cites only the Setup case at `PhysicsDataCache.cs:96`; the scenery `0x8…` site at `GameWindow.cs:7408` is uncovered) | | **D2** | MED | predicate | ETHEREAL-alone instant-skip; retail needs ETHEREAL **and** IGNORE_COLLISIONS | **CONFIRMED** (high) | **AD-7** exists | | **D3** | LOW | shape-source | Setup `Sphere`s coerced to short cylinders (no true sphere primitive) | **CONFIRMED** (high) | **NEW ROW** needed | | **D4** | LOW | predicate | EnvCell path omits `check_entry_restrictions` (access-locked cells) | **CONFIRMED** (high) | **NEW ROW** (no effect in dev content) | | **D5** | MED | predicate/state | EnvCell never clears `obstruction_ethereal` — **field doesn't even exist** in acdream `SpherePath` | **CONFIRMED** (high) | **NEW ROW** (couples with D2) | | **D8** | MED | stale-frame | EnvCell cell-struct `WorldTransform` never evicted/rebased (asymmetric with buildings #146) | **CONFIRMED** (med) | **NEW ROW** (latent; masked by single-block dungeon collapse) | | ~~D6~~ | — | dispatch | "EnvCell never calls `placement_insert`" | **REFUTED** | acdream *does* branch — inside `BSPQuery.FindCollisions` (`BSPQuery.cs:1717`), not at the call site. Functionally identical to retail. **No fix.** | | ~~D7~~ | — | terrain | "Terrain silently passes through when no landblock registered" | **REFUTED** | **Retail does the same** — `LScape::get_landcell@0x00505f10` returns null for an unloaded block, `transitional_insert@0x0050b6f0` returns OK_TS on null cell. This is the **#135/#138 streaming-gap free-fall**, a streaming-foundation issue, NOT collision-inclusion. | --- ## The retail model (the oracle table) ### Layer A — the inclusion predicate (`CPhysicsObj::FindObjCollisions@0x0050f050`) PhysicsState flags and whether each gates collision (verified against `acclient.h` + the decomp body, lines 276776-276996, cross-checked vs ACE `PhysicsObj.cs:381-454`): | Flag | Value | Gates collision? | Mechanism | |---|---|---|---| | `ETHEREAL_PS` | 0x4 | **Only with 0x10** | Instant-skip (return OK_TS, no shape test) requires **both** 0x4 AND 0x10 (`276782`). 0x4 alone → set `sphere_path.obstruction_ethereal=1` and **still run shape test** (`276795-276806`); downstream step-down passes through but contact is recorded. | | `IGNORE_COLLISIONS_PS` | 0x10 | with 0x4 | (above) | | `HAS_PHYSICS_BSP_PS` | 0x10000 | **dispatch** | Set (and not missile-ignored, not PvP-exempt `ebp_1==0`) → **BSP-only** via `CPartArray::FindObjCollisions` (`276858-276961`). Clear → CylSphere loop then Sphere branch. **Mutually exclusive** — never both. | | `STATIC_PS` | 0x1 | no (response only) | Decides hit is reported as `collided_with_environment` vs object collision (`276969-276981`); does not skip. | | `REPORT_COLLISIONS_PS` | 0x8 | no | Gates `DoCollisionEnd/Begin` *callback delivery* only (`278614`), not geometry. | | `NODRAW_PS` | 0x20 | **no** | Not read anywhere in the collision path. Render-only. (Confirms handoff hypothesis.) | | `MISSILE_PS` | 0x40 | no | Marks target `isCreature=true` for sphere dispatch; `missile_ignore` (`274390`) makes a missile mover skip missile targets. | | `HIDDEN_PS` | 0x4000 | indirect | `set_hidden` *also* sets 0x10 + clears 0x8 as a side-effect (`282992`,`283029`); but HIDDEN alone doesn't set ETHEREAL, so Gate-1 still needs 0x4. | | `SCRIPTED_COLLISION_PS` | 0x8000 | **no** | Never read in the physics collision path. | | `FROZEN_PS` | 0x1000000 | no | Gates `update_object` (sim skip) only (`283955`). | Plus the non-flag gates: mover≠self, not-creature-when-viewer/IGNORE_CREATURES mover, PvP pool (both-players → skip unless target Impenetrable OR both PK OR both PKLite). ### Layer B — registration / broad phase `shadow_object_list` membership predicate (`CObjCell::find_cell_list@0x0052b4e0`): - **(1)** object is NOT a pure particle emitter (`PARTICLE_EMITTER_PS 0x1000`), **(2)** at least one collision sphere overlaps the cell volume (indoor: `sphere_intersects_cell != OUTSIDE`; outdoor: sphere center maps to the cell grid square + boundary neighbours). **No other attribute tested at registration.** A single object lands in **multiple** cells' lists (the doorway-door-in-both-cells answer that the A6.P4/BR-7 flood already ports). ### The four collision channels (per-cell dispatch order: env → building → objects) | Channel | Function | Inclusion rule | |---|---|---| | **Terrain** | `CLandCell::find_env_collisions@0x00532f20` | `check_entry_restrictions` → `find_terrain_poly` (always 2 triangles) → water-skip clause → `validate_walkable`. Always solid where the landblock is loaded. | | **EnvCell (indoor)** | `CEnvCell::find_env_collisions@0x0052c130` | `check_entry_restrictions` → **zero `obstruction_ethereal`** → if `structure->physics_bsp != null`: `find_collisions` (or `placement_insert` for INITIAL_PLACEMENT). **No building leg.** | | **Building shell** | `CBuildingObj::find_building_collisions@0x006b5300` via `CSortCell::find_collisions@0x005340a0` | In `CLandBlockInfo.buildings` for an outdoor block AND `makeBuilding` succeeded AND origin landcell non-null. **Portal count / portal list / model id NOT examined.** BSP on `part_array->parts[0]`. (This is the #147 truth.) | | **Object shadows** | `CObjCell::find_obj_collisions@0x0052b750` → Layer A | Per-cell shadow list iteration. | ### Shape assignment (the rule D1 violates) A `CPhysicsObj` gets a collision shape from **DAT only**: a part's `CGfxObj.physics_bsp` (deserialized by `CGfxObj::Serialize@0x00534970` *only* when serialized-flags bit-0 / `num_physics_polygons>0`), or `CSetup` cylsphere/sphere arrays. `CPartArray::InitParts@0x00517F40` builds parts from the Setup list only. When none exist, `CPhysicsPart::find_obj_collisions@0x0050D8D0` returns OK (passable) — **no synthesis from drawing geometry.** The render-mesh `gfx_bound_box` is used for cell-registration + frustum culling, **never** collision. --- ## The acdream model (per-channel de-facto predicate) | Channel | acdream predicate (de-facto) | Code | Verdict vs retail | |---|---|---|---| | **Terrain** | foot-XY inside a registered landblock's `[0,192)²` AND outdoor cell. null landblock → pass through. | `PhysicsEngine.SampleTerrainWalkable` `:261`; `TransitionTypes.cs:2229-2247` | **Faithful** (D7 refuted — retail passes through unloaded blocks too). | | **Building** | outdoor cell AND `GetBuilding != null` AND `ModelId != 0` AND `GetGfxObj(ModelId).BSP.Root != null` | `TransitionTypes.cs:2805-2819` | **Faithful** post-#146/#147. `ModelId==0 → inert` ≈ retail "no parts[0]". Per-apply rebase via `RemoveBuildingsForLandblock` (#146). | | **Shadow objects** | source hi-byte 0x01/0x02 AND not building shell AND not phantom Setup/GfxObj AND ≥1 non-zero-radius shape **OR scenery mesh-AABB fallback** | `GameWindow.cs:7185-7565`; `ShadowObjectRegistry.cs`; `ShadowShapeBuilder.cs` | **D1 (mesh-AABB), D3 (sphere-as-cyl)** + watch-items. | | **EnvCell** | cell in BFS shadow-set AND `CellPhysics.BSP.Root != null` | `TransitionTypes.cs:2062-2207`; `CellTransit.cs` | **D4, D5, D8.** | | **Query exemption** | `CollisionExemption.ShouldSkip` — ETHEREAL(0x4) alone, viewer/creature, IGNORE_CREATURES, PvP pool | `CollisionExemption.cs:59` | **Faithful except D2** (ETHEREAL-alone). PvP block is a clean port (cross-checked vs ACE). | --- ## Confirmed deviations — detail ### D1 — Render-mesh-AABB synthetic collision cylinder (HIGH, the prime deviation) - **Retail:** shape from DAT `physics_bsp`/`CSetup` only; no shape ⇒ passable (`CPhysicsPart::find_obj_collisions@0x0050D8D0` returns OK when `physics_bsp==null`; `CGfxObj::Serialize@0x00534970` gates `physics_bsp` on flags bit-0; `CPartArray::InitParts@0x00517F40`). - **acdream:** for scenery (`0x80000000` IDs) with no CylSphere/BSP, synthesizes a clamped `[0.30,1.50]m` cylinder from the **world-space render-mesh AABB** and registers it. `GameWindow.cs:7421-7425` gate: `!isPhantomSetup && !isPhantomGfxObj && !_isLandblockStab && (_isOutdoorMesh || (entityBsp==0 && entityCyl==0))`. For scenery `_isOutdoorMesh` is always true (`:7151`); the #101 phantom gates only cover `0x01…` GfxObjs (`IsPhantomGfxObjSource`, `PhysicsDataCache.cs:399-403`) and zero-radius Setups — a Setup-sourced scenery with `Radius>0.0001` and no `physics_bsp` still gets the synthesized cylinder. - **Why it exists:** the comment admits a *gameplay* motive — "catches trees whose BSP is only on the canopy (player walks under)". This is exactly the attribute/shape category-error the user wants gone: acdream invents collision retail doesn't have. - **Register:** **AP-2** must be widened to cover `GameWindow.cs:7408` (it currently cites only `PhysicsDataCache.cs:96`). ### D2 — ETHEREAL-alone instant-skip (MED) - **Retail:** Gate-1 needs **both** 0x4 AND 0x10 (`276782`); 0x4 alone → `obstruction_ethereal=1` + still run shape test (`276795-276806`), consumed by BSP solid-containment gates (`321692`,`323742`,`324573`). - **acdream:** `CollisionExemption.cs:78` skips on 0x4 alone. Documented shim (**AD-7**) because ACE `Door.Open()` broadcasts `0x0001000C` (ETHEREAL only). The `obstruction_ethereal` set-and-continue path is unported. ### D3 — No true sphere primitive (LOW) - **Retail:** `HAS_PHYSICS_BSP_PS` clear → CylSphere then **Sphere via `CSphere::intersects_sphere`** (`276917`) — a real sphere test. - **acdream:** `ShadowShapeBuilder.cs:68-81` coerces `Setup.Spheres` → short cylinders (`height = radius*2`); `ShadowCollisionType` enum (`ShadowObjectRegistry.cs:537`) has only `{BSP, Cylinder}` — no Sphere. **New register row.** ### D4 — EnvCell omits `check_entry_restrictions` (LOW, no current effect) - **Retail:** `CEnvCell::find_env_collisions@0x0052c130` calls `check_entry_restrictions@0x0052b6d0` first; returns COLLIDED_TS when an access-locked cell's `restriction_obj` rejects the mover. - **acdream:** `TransitionTypes.cs:2062-2207` has no such gate. No access-locked cells in dev content ⇒ no observable effect, but structurally absent. **New register row.** ### D5 — `obstruction_ethereal` never cleared — field absent entirely (MED) - **Retail:** zeros `sphere_path.obstruction_ethereal` on every `find_env_collisions` call (`0x0052c144`) before BSP dispatch. - **acdream:** the field **does not exist** in `SpherePath` (`TransitionTypes.cs:315-435`) — so the retail BSP solid-weakening gate keyed on it is simply not modelled. Couples with D2 (port the set/clear/consume sites together). **New register row.** ### D8 — EnvCell cell-struct transform never evicted/rebased (MED, latent) - **Retail:** no persistent client-side streaming-relative transform cache; cell geometry rebuilt from live cell pointers (eviction analog: `CEnvCell::release`). - **acdream:** `PhysicsDataCache._cellStruct` (`:179`) is first-wins `ContainsKey` with **no eviction** — contrast `RemoveBuildingsForLandblock` (`:472`, the #146 fix). `PhysicsEngine.RemoveLandblock` (`:121-141`) clears `_landblocks`/`ShadowObjects`/`CellGraph` but never cells. A dungeon streamed out→in after a `_liveCenter` recenter would hold a stale `WorldTransform` (off by `(oldCenter−newCenter)·192m`). Masked today by the single-block collapse+recenter dungeon pattern. **New register row.** This is the #146 stale-frame class the handoff predicted "almost certainly more of these" — and there is exactly one more. --- ## Refuted (do NOT chase — anti-rabbit-hole) - **D6 (placement_insert):** acdream **does** route INITIAL_PLACEMENT to `PlacementInsert` — the branch is inside `BSPQuery.FindCollisions` (`BSPQuery.cs:1717`, `if (path.InsertType == Placement || obj.Ethereal)`), not at the `FindEnvCollisions` call site. Identical functional result. - **D7 (terrain pass-through):** retail `LScape::get_landcell@0x00505f10` returns null for an unloaded block and `transitional_insert@0x0050b6f0` returns OK_TS on a null cell — **retail passes through unloaded terrain too.** The free-fall is the **streaming-gap** (#135/#138), owned by the cell-relative-frame work, not collision-inclusion. - **Building `ModelId==0` filter** (`TransitionTypes.cs:2813`): **checked, faithful** — equals retail's "no `parts[0]` ⇒ no shell BSP". - **#147 portal-count skip:** already deleted (`9743537`). The last scattered attribute-as-filter is gone. - **Registration-time attribute filters:** acdream's registration is already overlap-based with filtering deferred to query time (`CollisionExemption`) — **architecturally already retail-shaped.** The remaining divergences are shape-source + predicate-detail + one stale cache, not a registration-filter mess. --- ## Watch-items (sub-deviations not promoted — note, don't necessarily fix now) - **W1 — PvP/missile dispatch terms hardcoded false** (`TransitionTypes.cs:2629`): the cyl-vs-BSP dispatch assumes `pvpExempt=false && missile_ignore=false`. Harmless in M1.5 (no PK/missiles), but a missile/PK mover would wrongly take BSP-only against all BSP targets. Belongs in the unified predicate when PK/missiles ship. - **W2 — `BldPortalInfo.ExactMatch`** decoded but unconsumed (`BuildingPhysics.cs:74`, `CellTransit.CheckBuildingTransit`): a *transit-feature*, not collision-inclusion — explicitly out of scope for this phase. - **W3 — `ShadowShapeBuilder` default radius** `2f` (`:97`) vs `GameWindow` stab fallback `1f` (`:7215`) when `BoundingSphere` is null — minor inconsistency. - **W4 — `CLandCell` ENTIRELY_WATER skip clause** (state 0x04 / 0x40) may be unimplemented in the terrain path — verify before deep-water content. - **W5 — static-prune `VisibleCellIds` vs retail `do_not_load` stab_list** (`CellTransit.cs:549-562`): approximation; verify against the CEnvCell dat stab_list field. --- ## The unified target — "1 fix for all collision" (PROPOSAL — brainstorm-gate before coding) One **`ObjectCollides(targetState, moverState, targetFlags, shape)`** predicate + one **registration rule** + one **rebase rule**, that every channel funnels through: **(a) Pure shape resolution (one helper, DAT-only).** Shape comes ONLY from `physics_bsp` (DAT bit-0) or `CSetup` cylsphere/sphere; an object with no DAT shape registers **no shape** and is passable. *Delete the mesh-AABB synthesis (D1).* **(b) The query predicate matching `FindObjCollisions@0x0050f050`:** instant-skip iff (0x4 AND 0x10); 0x4-alone → set `ObstructionEthereal` + continue (D2/D5); viewer/creature; IGNORE_CREATURES/creature; PvP pool; then dispatch BSP-only iff `HAS_PHYSICS_BSP_PS && !pvpExempt && !missileIgnore` else CylSphere→**Sphere** (D3). Wire the PvP/missile terms (W1). **(c) One overlap-based per-cell registration** (already mostly present via `ShadowObjectRegistry` + the A6.P4/BR-7 flood) — **no attribute filter at registration**; everything funnels through it (statics, scenery, weenies, building shell, cell BSP). **(d) One per-apply rebase** — every cached collision transform (cells, buildings, shadow positions) rebased to the current streaming center on each per-landblock apply via a symmetric `Remove*ForLandblock` pre-clear. *Add `RemoveCellsForLandblock` (D8).* (Or fold into the #145 cell-relative-frame port so collision frames can never go stale — the handoff's preferred long-term home.) ### Deletions - `GameWindow.cs:7408-7565` mesh-AABB synthesized-cylinder fallback (D1). - `CollisionExemption.cs:78` ETHEREAL-alone instant-skip → replace with both-bits Gate-1 + `ObstructionEthereal`-set-and-continue (D2); move ACE door-open compat to the **wire/adaptation layer**, not the collision predicate. - `ShadowShapeBuilder.cs:68-81` sphere→cylinder coercion → true Sphere shape + primitive (D3). - `TransitionTypes.cs:2629` hardcoded `pvpExempt/missileIgnore=false` → wire real terms (W1). ### Risks (each needs a user visual gate) - **D1 removal makes shape-less scenery walk-through** — retail-faithful, but a *visible behavior change* (trees you currently bump may become passable; trees with real canopy-only BSP regain retail behavior). **Highest-risk item; gate carefully.** - **D2 re-solidifies ACE-opened doors** unless the door-open wire path sends `ETHEREAL|IGNORE_COLLISIONS` in the **same** change — must land together or doors become impassable. - **D2/D5 obstruction_ethereal** touches BSP solid-containment gates — port set+clear+consume **together**; a partial port hardens/weakens walls unpredictably. - **W1 PvP/missile wiring** has no live M1.5 test path — risk of regressing the A6.P7 BSP-only door dispatch; gate + conformance-test the door case. - **D3 sphere primitive** is new physics code — conformance-test vs `CSphere::intersects_sphere` golden values, not eyeballed. - **D8 eviction** interacts with #135/#138 dungeon collapse/recenter — adding eviction could surface a re-stream ordering bug currently masked by "never evict"; verify against teleport-OUT. ### Suggested slicing (for the brainstorm) 1. **Shape-source purity (D1)** — delete mesh-AABB synthesis; the highest-value, most-isolated, retail-clarifying change. Visual gate: scenery + Holtburg. 2. **Sphere primitive (D3)** — unblocks faithful shape dispatch; conformance-tested. 3. **ETHEREAL/obstruction_ethereal (D2+D5)** — predicate + state field + door wire compat, landed together. 4. **Cell-transform rebase (D8)** — or fold into the #145 cell-relative-frame phase. 5. **Predicate wiring + entry-restrictions (W1+D4)** — lowest urgency (no M1.5 effect); wire as conservative no-ops now, activate when PK/missiles/locked-cells ship. --- ## Divergence-register bookkeeping required - **Widen AP-2** to cite `GameWindow.cs:7408` (scenery mesh-AABB), not only the Setup case (D1). - **AD-7** already covers D2 — keep; retire it when the obstruction_ethereal port lands. - **AP-6** already covers the analytic-cylinder-vs-retail-CylSphere dispatch — relevant to D3/W1. - **New rows:** D3 (sphere-as-cylinder), D4 (entry-restrictions omitted), D5 (obstruction_ethereal absent from SpherePath), D8 (cell-struct transform never evicted). Optional: a terrain-pass-through note for the D7 streaming-gap (clarify it's #135/#138, not a collision bug) so nobody re-files it. ## Apparatus for the implementation phase (reuse, don't rebuild) `ACDREAM_PROBE_BUILDING` (`[entity-source]` BSP-vs-Cylinder classification per static — directly shows which objects got the D1 synthetic cylinder), `ACDREAM_CAPTURE_RESOLVE`, `Issue147ArwicBuildingsDumpTests` (dat-dump pattern), the `CellarUp*`/`HouseExitWalk*`/`CornerFlood*` replay harnesses, and a new `CSphere::intersects_sphere` conformance fixture for D3. --- *Generated by a 19-agent verified workflow (`wf_b2f8da74-9fa`): 10 readers → synthesis → 8 adversarial verifiers. 6/8 candidate deviations survived adversarial verification; 2 were refuted (D6, D7) with the retail evidence corrected.*