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>
12 KiB
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?"
PhysicsStateflags onCPhysicsObj(grepacclient.hfor thePhysicsStateenum). 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]],BspOnlyDispatchflag0x00010000).- 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 aCylSphere? Retail uses whatever the dat/weenie says — it does NOT fabricate collision from a render mesh AABB. (acdream'sIsPhantomGfxObjSource/ 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-cellshadow_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-7ShadowObjectRegistryflood — see[[project_physics_collision_digest]].)CBuildingObj::find_building_collisions(0x006b5300) — the building shell BSP, tested onpart_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 atTransitionTypes.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:
- Terrain —
PhysicsEngine.SampleTerrainWalkable/TransitionTypes.cs:2229(// no terrain loaded → allow pass-through,:2247). Re-bases per apply viaAddLandblock(origin)(so no #146-style staleness). Filter: terrain residency only. - 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, logsbldOrigin) is the live lens. - Shadow objects (statics, scenery, server weenies) —
Core/Physics/ShadowObjectRegistry.cs(per-cell flood, BR-7). Registration sites:GameWindow.csShadowObjects.Register*(~:7196–7526),RefloodLandblock(:7595),RegisterMultiPart(:3835). Server weenies register viaOnLiveEntitySpawnedLocked→ Register. Filters to scrutinize for divergence:LandblockLoader.IsSupported(GfxObj/Setup mask only) — does it match retail's "has a physics shape"? What about0x0D/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 matchesHAS_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?
- EnvCell (indoor) — cell BSP via the transition;
CellTransitper-cell iteration. - 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)
superpowers:brainstormingto scope the phase + agree the unified model BEFORE code.- 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. - Cross-reference ACE
Physics/Common(PhysicsObj, ObjCell, shadow lists) + ACViewerPhysics/Common+ holtburgerspatial/physics.rsfor the client-side read. - 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 logsbldOrigin) +[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; theanalyze_*streaming-tally pattern works (255k records in seconds).ACDREAM_PROBE_ENT/EntityVanishProbe→ avatar draw-set lifecycle (#138-B).Issue147ArwicBuildingsDumpTests→ the dat-dump pattern forLandBlockInfo.Buildings/.Objects(portal counts, model ids); clone it for any landblock/object-class inventory.- Live client: user
notan/MittSnus81!, ACE127.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 (thePhysicsState/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).