# 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`:

```csharp
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:

```csharp
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:

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

Then in the gate:

```csharp
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

- Capture log: `launch-thinair.utf8.log` (2026-05-21, uncommitted).
- Entity ID layout: [`src/AcDream.Core/World/LandblockLoader.cs:40-55`](src/AcDream.Core/World/LandblockLoader.cs:55).
- Affected code: [`src/AcDream.App/Rendering/GameWindow.cs:5791-6206`](src/AcDream.App/Rendering/GameWindow.cs:5791) (the per-entity registration loop in `RegisterShadowsForLandblock`).
- Prior phase: spec
  [`2026-05-21-indoor-walk-miss-probe-design.md`](2026-05-21-indoor-walk-miss-probe-design.md).
- Findings: [`docs/research/2026-05-21-walk-miss-capture-findings.md`](../../research/2026-05-21-walk-miss-capture-findings.md).
