fix(physics): D8 — RemoveCellsForLandblock; cell transforms rebase per apply

Mirrors RemoveBuildingsForLandblock (#146) for indoor CellPhysics entries.
_cellStruct is first-wins (CacheCellStruct's ContainsKey guard); without
eviction a dungeon's BSP WorldTransform is permanently locked to the
_liveCenter value at first streaming — a teleport recenter leaves cells at a
stale offset (~source↔dest distance), and foot-sphere collision queries miss
the geometry that visually renders correctly.

PhysicsDataCache.RemoveCellsForLandblock iterates _cellStruct.Keys and
TryRemove-s every entry whose high-word matches the evicted landblock prefix.
PhysicsEngine.RemoveLandblock now calls it alongside ShadowObjects.RemoveLandblock
so cell BSPs rebase on the next CacheCellStruct pass, same as buildings.

No divergence register row needed: this closes a gap introduced when the #146
building-eviction pattern was created without the symmetric cell eviction.

Tests: RemoveCellsForLandblockTests (3 cases): evicts-matching-prefix,
empty-cache no-throw, no-matching-cells leaves-others. Core suite: 1587 pass / 0 fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-24 19:34:16 +02:00
parent dc1e927080
commit a9f8775110
3 changed files with 85 additions and 0 deletions

View file

@ -459,6 +459,24 @@ public sealed class PhysicsDataCache
_buildings.TryRemove(key, out _);
}
/// <summary>
/// D8 (2026-06-24): drop every cached cell struct belonging to a landblock so
/// the next <see cref="CacheCellStruct"/> re-bases its STREAMING-RELATIVE
/// <c>WorldTransform</c> against the current <c>_liveCenter</c> — symmetric with
/// <see cref="RemoveBuildingsForLandblock"/> (#146). Without this, the
/// <c>_cellStruct</c> first-wins guard LOCKS a dungeon cell's transform at its
/// first streaming frame; a teleport recenter then leaves the cell BSP at a stale
/// world offset (~the source↔dest landblock distance), so foot-sphere collision
/// queries walk through where the wall geometry is.
/// </summary>
public void RemoveCellsForLandblock(uint landblockId)
{
uint prefix = landblockId & 0xFFFF0000u;
foreach (var key in _cellStruct.Keys)
if ((key & 0xFFFF0000u) == prefix)
_cellStruct.TryRemove(key, out _);
}
public BuildingPhysics? GetBuilding(uint landcellId)
=> _buildings.TryGetValue(landcellId, out var b) ? b : null;

View file

@ -122,6 +122,7 @@ public sealed class PhysicsEngine
{
_landblocks.Remove(landblockId);
ShadowObjects.RemoveLandblock(landblockId);
DataCache?.RemoveCellsForLandblock(landblockId); // D8: rebase cell BSP transforms on next apply
// #145: if the player's current cell belonged to the landblock being removed (a teleport
// drops the stale source center via OnLivePositionUpdated), clear it. Otherwise CurrCell