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:
parent
dc1e927080
commit
a9f8775110
3 changed files with 85 additions and 0 deletions
|
|
@ -459,6 +459,24 @@ public sealed class PhysicsDataCache
|
||||||
_buildings.TryRemove(key, out _);
|
_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)
|
public BuildingPhysics? GetBuilding(uint landcellId)
|
||||||
=> _buildings.TryGetValue(landcellId, out var b) ? b : null;
|
=> _buildings.TryGetValue(landcellId, out var b) ? b : null;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,7 @@ public sealed class PhysicsEngine
|
||||||
{
|
{
|
||||||
_landblocks.Remove(landblockId);
|
_landblocks.Remove(landblockId);
|
||||||
ShadowObjects.RemoveLandblock(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
|
// #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
|
// drops the stale source center via OnLivePositionUpdated), clear it. Otherwise CurrCell
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Tests.Physics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// D8 (2026-06-24): verify that <see cref="PhysicsDataCache.RemoveCellsForLandblock"/>
|
||||||
|
/// evicts every cached cell belonging to the given landblock prefix and leaves
|
||||||
|
/// cells belonging to other landblocks untouched — symmetric with
|
||||||
|
/// <see cref="PhysicsDataCache.RemoveBuildingsForLandblock"/> (#146).
|
||||||
|
/// </summary>
|
||||||
|
public class RemoveCellsForLandblockTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Build the absolute minimum <see cref="CellPhysics"/> that satisfies the
|
||||||
|
/// <c>required</c> init property. BSP is left null because the test exercises
|
||||||
|
/// only the cache-eviction key logic, not BSP traversal.
|
||||||
|
/// </summary>
|
||||||
|
private static CellPhysics BuildMinimalCell() => new()
|
||||||
|
{
|
||||||
|
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue