The Slice 2 outdoor readiness gate queried IsLandblockLoaded(destCell & 0xFFFF0000) = e.g. 0x7D640000, but streaming stores landblocks under the EncodeLandblockId form (low 16 = 0xFFFF), e.g. 0x7D64FFFF. The raw ContainsKey never matched, so the outdoor teleport gate could NEVER flip Ready and every outdoor arrival ran to the 600-frame (~10 s) timeout and force-placed. The cascade was still prevented (the timeout force-place lands cleanly), but the gate did no work — the 10 s freeze the apparatus showed was this bug, NOT the #138 streaming stall I first suspected. Root cause found via the apparatus re-test (3-agent investigation wf_8b67a9d1-35c, all high-confidence) + verified against StreamingRegion.cs:99 (EncodeLandblockId | 0xFFFF), PhysicsEngine.cs:79 (stores as-is), GameWindow.cs:5530 (queries & 0xFFFF0000). Fix: IsLandblockLoaded normalizes its arg to the canonical 0xFFFF landblock key, so the prefix form, any contained cell id, and the dat-id form all resolve. Added the regression test the original Slice 2 test missed (it had checked the same 0xFFFF form it added; the real caller passes the 0x..0000 form). Red on the prefix/cell forms before the fix, green after. 9/9. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
90 lines
3.4 KiB
C#
90 lines
3.4 KiB
C#
using AcDream.Core.Physics;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Tests.Physics;
|
|
|
|
public class PhysicsEngineIsLandblockLoadedTests
|
|
{
|
|
private static TerrainSurface MakeFlatTerrain()
|
|
{
|
|
var heights = new byte[81];
|
|
Array.Fill(heights, (byte)50);
|
|
var heightTable = new float[256];
|
|
for (int i = 0; i < 256; i++) heightTable[i] = i * 1f;
|
|
return new TerrainSurface(heights, heightTable);
|
|
}
|
|
|
|
[Fact]
|
|
public void IsLandblockLoaded_AbsentId_ReturnsFalse()
|
|
{
|
|
var engine = new PhysicsEngine();
|
|
Assert.False(engine.IsLandblockLoaded(0xA9B4FFFFu));
|
|
}
|
|
|
|
[Fact]
|
|
public void IsLandblockLoaded_AddedId_ReturnsTrue()
|
|
{
|
|
var engine = new PhysicsEngine();
|
|
engine.AddLandblock(0xA9B4FFFFu, MakeFlatTerrain(),
|
|
Array.Empty<CellSurface>(), Array.Empty<PortalPlane>(),
|
|
worldOffsetX: 0f, worldOffsetY: 0f);
|
|
|
|
Assert.True(engine.IsLandblockLoaded(0xA9B4FFFFu));
|
|
}
|
|
|
|
[Fact]
|
|
public void IsLandblockLoaded_AnotherIdAbsent_ReturnsFalse()
|
|
{
|
|
var engine = new PhysicsEngine();
|
|
engine.AddLandblock(0xA9B4FFFFu, MakeFlatTerrain(),
|
|
Array.Empty<CellSurface>(), Array.Empty<PortalPlane>(),
|
|
worldOffsetX: 0f, worldOffsetY: 0f);
|
|
|
|
Assert.False(engine.IsLandblockLoaded(0xDEADBEEFu));
|
|
}
|
|
|
|
[Fact]
|
|
public void IsLandblockLoaded_AfterRemove_ReturnsFalse()
|
|
{
|
|
var engine = new PhysicsEngine();
|
|
engine.AddLandblock(0xA9B4FFFFu, MakeFlatTerrain(),
|
|
Array.Empty<CellSurface>(), Array.Empty<PortalPlane>(),
|
|
worldOffsetX: 0f, worldOffsetY: 0f);
|
|
engine.RemoveLandblock(0xA9B4FFFFu);
|
|
|
|
Assert.False(engine.IsLandblockLoaded(0xA9B4FFFFu));
|
|
}
|
|
|
|
// Regression for the Slice 2 key-mismatch bug (#145 apparatus re-test, 2026-06-21):
|
|
// landblocks are stored under the EncodeLandblockId form (low 16 = 0xFFFF), but the
|
|
// teleport gate (TeleportArrivalReadiness) queries with (destCell & 0xFFFF0000) (low
|
|
// 16 = 0x0000). A raw ContainsKey never matched, so the outdoor gate was permanently
|
|
// NotReady and every outdoor teleport timed out at 10 s. IsLandblockLoaded must
|
|
// normalize so ANY id within the landblock resolves to the stored entry.
|
|
[Theory]
|
|
[InlineData(0xA9B40000u)] // landblock-prefix form — exactly what the gate passes (destCell & 0xFFFF0000)
|
|
[InlineData(0xA9B4000Du)] // an outdoor cell id within the landblock
|
|
[InlineData(0xA9B40143u)] // an indoor cell id within the landblock
|
|
[InlineData(0xA9B4FFFFu)] // the dat-id storage form
|
|
public void IsLandblockLoaded_NormalizesLow16_FindsLandblockForAnyContainedId(uint queryId)
|
|
{
|
|
var engine = new PhysicsEngine();
|
|
engine.AddLandblock(0xA9B4FFFFu, MakeFlatTerrain(),
|
|
Array.Empty<CellSurface>(), Array.Empty<PortalPlane>(),
|
|
worldOffsetX: 0f, worldOffsetY: 0f);
|
|
|
|
Assert.True(engine.IsLandblockLoaded(queryId));
|
|
}
|
|
|
|
[Fact]
|
|
public void IsLandblockLoaded_DifferentLandblockPrefix_ReturnsFalse()
|
|
{
|
|
var engine = new PhysicsEngine();
|
|
engine.AddLandblock(0xA9B4FFFFu, MakeFlatTerrain(),
|
|
Array.Empty<CellSurface>(), Array.Empty<PortalPlane>(),
|
|
worldOffsetX: 0f, worldOffsetY: 0f);
|
|
|
|
// A different landblock prefix must NOT match, even in the 0x..0000 query form.
|
|
Assert.False(engine.IsLandblockLoaded(0xA9B50000u));
|
|
}
|
|
}
|