diff --git a/src/AcDream.Core/Physics/PhysicsDataCache.cs b/src/AcDream.Core/Physics/PhysicsDataCache.cs index d0876495..f90808fa 100644 --- a/src/AcDream.Core/Physics/PhysicsDataCache.cs +++ b/src/AcDream.Core/Physics/PhysicsDataCache.cs @@ -459,6 +459,24 @@ public sealed class PhysicsDataCache _buildings.TryRemove(key, out _); } + /// + /// D8 (2026-06-24): drop every cached cell struct belonging to a landblock so + /// the next re-bases its STREAMING-RELATIVE + /// WorldTransform against the current _liveCenter — symmetric with + /// (#146). Without this, the + /// _cellStruct 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. + /// + 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; diff --git a/src/AcDream.Core/Physics/PhysicsEngine.cs b/src/AcDream.Core/Physics/PhysicsEngine.cs index e240fec0..1090559e 100644 --- a/src/AcDream.Core/Physics/PhysicsEngine.cs +++ b/src/AcDream.Core/Physics/PhysicsEngine.cs @@ -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 diff --git a/tests/AcDream.Core.Tests/Physics/RemoveCellsForLandblockTests.cs b/tests/AcDream.Core.Tests/Physics/RemoveCellsForLandblockTests.cs new file mode 100644 index 00000000..bd12f7c8 --- /dev/null +++ b/tests/AcDream.Core.Tests/Physics/RemoveCellsForLandblockTests.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using AcDream.Core.Physics; +using Xunit; + +namespace AcDream.Core.Tests.Physics; + +/// +/// D8 (2026-06-24): verify that +/// evicts every cached cell belonging to the given landblock prefix and leaves +/// cells belonging to other landblocks untouched — symmetric with +/// (#146). +/// +public class RemoveCellsForLandblockTests +{ + /// + /// Build the absolute minimum that satisfies the + /// required init property. BSP is left null because the test exercises + /// only the cache-eviction key logic, not BSP traversal. + /// + private static CellPhysics BuildMinimalCell() => new() + { + Resolved = new Dictionary(), + }; + + [Fact] + public void RemoveCellsForLandblock_EvictsOnlyMatchingPrefix() + { + var cache = new PhysicsDataCache(); + + // Two cells from landblock 0xAAAA0000 and one from 0xBBBB0000. + cache.RegisterCellStructForTest(0xAAAA0100u, BuildMinimalCell()); + cache.RegisterCellStructForTest(0xAAAA0200u, BuildMinimalCell()); + cache.RegisterCellStructForTest(0xBBBB0100u, BuildMinimalCell()); + + cache.RemoveCellsForLandblock(0xAAAA0000u); + + // Both AAAA cells must be gone. + Assert.Null(cache.GetCellStruct(0xAAAA0100u)); + Assert.Null(cache.GetCellStruct(0xAAAA0200u)); + + // The BBBB cell must survive. + Assert.NotNull(cache.GetCellStruct(0xBBBB0100u)); + } + + [Fact] + public void RemoveCellsForLandblock_EmptyCache_DoesNotThrow() + { + // Must be a no-op on an empty cache. + var cache = new PhysicsDataCache(); + cache.RemoveCellsForLandblock(0xAAAA0000u); + Assert.Equal(0, cache.CellStructCount); + } + + [Fact] + public void RemoveCellsForLandblock_NoMatchingCells_LeavesOthersIntact() + { + var cache = new PhysicsDataCache(); + cache.RegisterCellStructForTest(0xBBBB0100u, BuildMinimalCell()); + + // Evicting a prefix that has no cached entries should not disturb others. + cache.RemoveCellsForLandblock(0xAAAA0000u); + + Assert.NotNull(cache.GetCellStruct(0xBBBB0100u)); + Assert.Equal(1, cache.CellStructCount); + } +}