From c8809735f375300acbd6acdb5521b6e88072face Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 21 Jun 2026 20:52:23 +0200 Subject: [PATCH] =?UTF-8?q?fix(teleport=20#145):=20IsLandblockLoaded=20key?= =?UTF-8?q?=20mismatch=20=E2=80=94=20outdoor=20gate=20was=20permanently=20?= =?UTF-8?q?NotReady?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/AcDream.Core/Physics/PhysicsEngine.cs | 20 ++++++++--- .../PhysicsEngineIsLandblockLoadedTests.cs | 33 +++++++++++++++++++ 2 files changed, 48 insertions(+), 5 deletions(-) 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)); + } }