fix(physics): scope interior cell shadows to ParentCellId (Phase A1.5)

ISSUES #83 Phase A1.5. ShadowObjectRegistry.Register() assigned each
entity to the outdoor landcell grid (8x8 cells, 24m square) based on
its XY position. For interior EnvCell statics (fireplace, furniture,
sign) hydrated by BuildInteriorEntitiesForStreaming with
ParentCellId = envCellId (a high-cellId interior cell like
0xA9B40121), this meant the shadow got stamped into the OUTDOOR
landcell whose XY they overlapped (e.g., 0xA9B40029).

When the player was OUTSIDE the building in 0xA9B40029, the indoor
chair/fireplace shadow fired collisions in "thin air" outdoors. The
user reported this on Holtburg cottage exteriors after the Phase A1
landblock-stab fallback fix.

Fix: add optional cellScope parameter to Register(). When non-zero
(passed as entity.ParentCellId ?? 0u from the 5 entity-loop call
sites in GameWindow), skip the XY-based landcell loop and register
the shadow ONLY in that cell. Live server-spawn registration at
GameWindow.cs:3137 keeps the XY-based behavior (live entities move
between cells).

Probe evidence (launch-a1-verify.utf8.log, post-A1 capture):
- 71 hits on 0x40B50054 (interior static) in OUTDOOR cell 0xA9B40029.
- 47 hits on 0xA9B47C00 (other Holtburg cottage BSP — legitimate).
- 31 hits on 0x40B50048 / 15 on 0x40B50018 (interior statics).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-20 11:54:29 +02:00
parent 5f2b545979
commit 4d3bf6fe37
2 changed files with 35 additions and 8 deletions

View file

@ -36,10 +36,34 @@ public sealed class ShadowObjectRegistry
ShadowCollisionType collisionType = ShadowCollisionType.BSP,
float cylHeight = 0f, float scale = 1.0f,
uint state = 0u,
EntityCollisionFlags flags = EntityCollisionFlags.None)
EntityCollisionFlags flags = EntityCollisionFlags.None,
uint cellScope = 0u)
{
Deregister(entityId);
var entry = new ShadowEntry(entityId, gfxObjId, worldPos, rotation, radius,
collisionType, cylHeight, scale, state, flags);
// ISSUES #83 / Phase A1.5 (2026-05-21): if the caller passed a
// cellScope (typically the entity's ParentCellId for an interior
// EnvCell static), scope the shadow to ONLY that cell instead of
// computing outdoor-landcell occupancy from XY. Without this,
// interior statics (a fireplace inside cell 0xA9B40121) get
// registered into the outdoor landcell whose XY they overlap
// (e.g. 0xA9B40029) and fire collisions when the player is OUTSIDE
// the building — the user-reported "thin air" collision outdoors.
if (cellScope != 0u)
{
if (!_cells.TryGetValue(cellScope, out var scopedList))
{
scopedList = new List<ShadowEntry>();
_cells[cellScope] = scopedList;
}
scopedList.Add(entry);
_entityToCells[entityId] = new List<uint> { cellScope };
return;
}
// The radius parameter should already be the WORLD-SPACE bounding
// radius (i.e., already multiplied by scale) so the broad-phase cell
// occupancy is correct. Callers are responsible for that.
@ -51,8 +75,6 @@ public sealed class ShadowObjectRegistry
int minCy = Math.Max(0, (int)((localY - radius) / 24f));
int maxCy = Math.Min(7, (int)((localY + radius) / 24f));
var entry = new ShadowEntry(entityId, gfxObjId, worldPos, rotation, radius,
collisionType, cylHeight, scale, state, flags);
var cellIds = new List<uint>();
uint lbPrefix = landblockId & 0xFFFF0000u;