acdream/docs/superpowers/specs/2026-05-21-cylinder-fallback-dedup-design.md
Erik 5f2b545979 fix(physics): skip mesh-AABB-fallback cylinder for landblock stabs
ISSUES #83 Phase A1. Landblock stabs (entity.Id 0xC0XXYY00+n per
LandblockLoader.cs:55) were being registered with TWO collision
shadows: the correct per-part BSP at `entity.Id*256 + partIdx`, AND a
redundant mesh-AABB-fallback cylinder at `entity.Id`. The fallback
clamped to 1.5m radius, centered at the building's mesh origin,
producing user-reported "thin air" collisions inside cottages and
within 2m of building exteriors.

The fallback was originally designed for canopy-only-BSP procedural
scenery (0x80XXYY00+n) — trees whose BSP covers the canopy but not
the trunk. Landblock stabs have full BSP coverage and don't need it.

Probe evidence (launch-thinair capture):
- 0xC0A9B479 cylinder fallback (Holtburg cottage): 104 hits in a
  short capture session, all inside the cottage main room
  (cell=0xA9B4013F), ~2m from the building's mesh origin.
- 0xA9B47900 BSP (the actual cottage walls): 52 legitimate hits.

Fix: one new bool _isLandblockStab + one clause in the existing
mesh-AABB-fallback gate.

Spec: docs/superpowers/specs/2026-05-21-cylinder-fallback-dedup-design.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 11:42:13 +02:00

6.2 KiB

Cylinder fallback dedupe for landblock stabs (Phase A1)

Date: 2026-05-21 Status: Spec — awaiting user approval before plan-writing. Phase: ISSUES #83 fix sequence — Phase A1 of A1→A2→A3→A4. Author: Claude Opus 4.7.

Summary

Landblock stabs (entity.Id 0xC0XXYY00+n — buildings + structures from LandBlockInfo.Objects and LandBlockInfo.Buildings) are registered with TWO collision shadows in GameWindow.RegisterShadowsForLandblock:

  1. BSP shadow per part (id = entity.Id * 256 + partIdx, truncates to e.g. 0xA9B47900 for the Holtburg cottage) — the correct wall-by-wall collision.
  2. Mesh-AABB-fallback cylinder (id = entity.Id = 0xC0A9B479) — a 1.5 m-clamped invisible disc at the mesh origin. This is the bug: it blocks the player in "thin air" inside cottages and near building exteriors.

The fallback path was originally designed to cover canopy-only-BSP trees (0x80XXYY00+n procedural scenery) where the visible trunk isn't covered by the BSP. For landblock stabs the BSP covers the whole structure; the fallback is redundant and incorrect.

Fix: gate the mesh-AABB-fallback registration path on "NOT a landblock stab." One extra clause in the existing if condition; ~5 lines of code; no math changes.

Evidence

From the 2026-05-21 thin-air-collision capture (launch-thinair.utf8.log):

[entity-source] id=0xA9B47900 entityId=0xC0A9B479 type=BSP      note=partIdx=0  hasPhys=true
[entity-source] id=0xC0A9B479 entityId=0xC0A9B479 type=Cylinder note=mesh-aabb-fallback

Same entityId, same gfxObj=0x01000A2B, same landblock 0xA9B4FFFF. The 1.5 m cylinder is the user-reported "thin air" collider.

Hit attribution counts (this capture):

obj id type hits role
0xC0A9B479 Cylinder (fallback) 104 invisible disc — the bug
0xA9B47900 BSP (correct walls) 52 real cottage walls
0xB5001600 BSP (interior static) 97 inn fireplace / furniture

The 104 fallback hits happen inside cell 0xA9B4013F (cottage main room) at ~2 m from the building's mesh origin entOrigin_lb=(130.5, 11.5, 94.0).

Root cause

src/AcDream.App/Rendering/GameWindow.cs:5826-5828:

bool _isOutdoorMesh = ((entity.Id & 0x80000000u) != 0)  // scenery
    || ((entity.Id < 0x40000000u)                        // stab
        && (_srcPrefix == 0x01000000u || _srcPrefix == 0x02000000u));

(entity.Id & 0x80000000u) != 0 matches BOTH 0x80... (procedural scenery) AND 0xC0... (landblock stabs). The mesh-AABB-fallback gate at line 6050-6052:

if (!isPhantomSetup
    && (_isOutdoorMesh || (entityBsp == 0 && entityCyl == 0))
    && entity.MeshRefs.Count > 0)

triggers the fallback for both classes. For stabs (which have full BSP coverage) this is wrong.

Fix

Add _isLandblockStab flag near the existing _isOutdoorMesh definition, and AND it into the fallback gate:

bool _isLandblockStab = (entity.Id & 0xFF000000u) == 0xC0000000u;

Then in the gate:

if (!isPhantomSetup
    && !_isLandblockStab   // ← NEW: stabs have BSP; fallback is redundant
    && (_isOutdoorMesh || (entityBsp == 0 && entityCyl == 0))
    && entity.MeshRefs.Count > 0)

That's the entire code change. ~3 lines added.

Acceptance criteria

  1. dotnet build green.
  2. Re-launch with ACDREAM_PROBE_BUILDING=1 ACDREAM_DEVTOOLS=1. Open the log and grep [entity-source].*note=mesh-aabb-fallback. For every match, the entityId must NOT start with 0xC0.
  3. Visual verification (the acceptance test): walk inside a Holtburg cottage; the player should be able to walk anywhere in the room without being blocked by invisible walls in "thin air." Walking near the outside walls of buildings should not show invisible barriers 2 m before the visible wall.
  4. The procedural scenery cylinder coverage stays — walking up to a tree trunk should still stop at the trunk (not pass through), confirming we didn't regress the original purpose of the fallback.

Out of scope

  • PHSP inversion fix (Phase A2) — separate spec.
  • Synthesis removal (Phase A3) — separate spec; needs A1+A2 first.
  • Multi-cell iteration (Phase A4) — separate spec.
  • Distinguish CScenery vs CBuildingObj at retail level — retail has two different classes; we use one path here. Refactoring to match retail's class layout is post-M2 polish.
  • 0xB5001600 interior static collisions (97 hits) — these attribute to a 0x40-prefix interior entity (probably inn fireplace or furniture). Whether they're legitimate or bugged is a separate investigation. Out of this phase's scope.

Risks

  • R1: Trees with canopy-only BSP regress (player walks through trunk). The fallback was added for this case. Mitigation: unaffected — trees are 0x80xxxxxx scenery, not 0xC0xxxxxx stabs. The fix is precisely scoped to stabs. Verified by the entity-ID layout in src/AcDream.Core/World/LandblockLoader.cs:55.

  • R2: Some buildings might have INCOMPLETE BSP (e.g., a roof with no floor BSP). After fix, those buildings lose the fallback cylinder coverage. Mitigation: none in this phase. If a user reports a building with no collision after this fix, we add per- building inspection. Retail design is "BSP only for stabs"; this is the correct path.

  • R3: The 0xB5xx interior statics (entity.Id 0x40xxxxxx) may have the same doubled-collision bug (BSP + cylinder fallback). They don't have the 0xC0 prefix so our fix wouldn't apply. Mitigation: out of scope. Investigated as a follow-up if visual regression surfaces.

References