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>
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:
- BSP shadow per part (
id = entity.Id * 256 + partIdx, truncates to e.g.0xA9B47900for the Holtburg cottage) — the correct wall-by-wall collision. - 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
dotnet buildgreen.- Re-launch with
ACDREAM_PROBE_BUILDING=1 ACDREAM_DEVTOOLS=1. Open the log and grep[entity-source].*note=mesh-aabb-fallback. For every match, theentityIdmust NOT start with0xC0. - 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.
- 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.
0xB5001600interior 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
0x80xxxxxxscenery, not0xC0xxxxxxstabs. The fix is precisely scoped to stabs. Verified by the entity-ID layout insrc/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
- Capture log:
launch-thinair.utf8.log(2026-05-21, uncommitted). - Entity ID layout:
src/AcDream.Core/World/LandblockLoader.cs:40-55. - Affected code:
src/AcDream.App/Rendering/GameWindow.cs:5791-6206(the per-entity registration loop inRegisterShadowsForLandblock). - Prior phase: spec
2026-05-21-indoor-walk-miss-probe-design.md. - Findings:
docs/research/2026-05-21-walk-miss-capture-findings.md.