diff --git a/src/AcDream.Core/Physics/PhysicsEngine.cs b/src/AcDream.Core/Physics/PhysicsEngine.cs index 05354644..078e8ea5 100644 --- a/src/AcDream.Core/Physics/PhysicsEngine.cs +++ b/src/AcDream.Core/Physics/PhysicsEngine.cs @@ -32,13 +32,23 @@ public sealed class PhysicsEngine public int LandblockCount => _landblocks.Count; /// - /// Returns true iff a landblock with this id has been registered via - /// . Because registers - /// terrain + collision cells + portals atomically, ContainsKey implies the - /// collision mesh is present and resolvable. + /// Returns true iff the landblock CONTAINING the given id has been registered via + /// . Because registers terrain + + /// collision cells + portals atomically, a hit implies the collision mesh is present + /// and resolvable. + /// + /// The argument may be any id within the landblock — a full cell id (low 16 = cell + /// index), the landblock-prefix form (low 16 = 0x0000, as the teleport gate passes via + /// destCell & 0xFFFF0000), or the dat-id storage form. The low 16 bits are + /// normalized to the canonical landblock key (0xFFFF) that streaming stores under + /// ( ORs in 0xFFFF), + /// so all three forms resolve. The earlier raw ContainsKey(id & 0xFFFF0000) + /// never matched the 0xFFFF-terminated stored key, leaving the outdoor teleport gate + /// permanently NotReady (2026-06-21 #145 apparatus re-test). + /// /// public bool IsLandblockLoaded(uint landblockId) => - _landblocks.ContainsKey(landblockId); + _landblocks.ContainsKey((landblockId & 0xFFFF0000u) | 0xFFFFu); /// /// Cell-based spatial index for static object collision. diff --git a/tests/AcDream.Core.Tests/Physics/PhysicsEngineIsLandblockLoadedTests.cs b/tests/AcDream.Core.Tests/Physics/PhysicsEngineIsLandblockLoadedTests.cs index 727eeccf..d32b5a6d 100644 --- a/tests/AcDream.Core.Tests/Physics/PhysicsEngineIsLandblockLoadedTests.cs +++ b/tests/AcDream.Core.Tests/Physics/PhysicsEngineIsLandblockLoadedTests.cs @@ -54,4 +54,37 @@ public class PhysicsEngineIsLandblockLoadedTests 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(), Array.Empty(), + 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(), Array.Empty(), + worldOffsetX: 0f, worldOffsetY: 0f); + + // A different landblock prefix must NOT match, even in the 0x..0000 query form. + Assert.False(engine.IsLandblockLoaded(0xA9B50000u)); + } }