revert(teleport): drop the Slice 2 outdoor readiness-gate hold
User-tested: the Slice 2 'hold outdoor until landblock loaded' gate made EVERY outdoor teleport a ~10 s freeze, because the destination landblock does NOT load fast during the hold (lbs=0 the whole time — the #138 streaming gap + _datLock starvation from the CreateObject flood). The hold was band-aiding a broken/slow foundation rather than fixing it, and it never actually prevented the #145 edge cascade anyway (it force-snapped onto NO-LANDBLOCK after the timeout regardless). Reverts ad8c24e..c880973 to the pre-Slice-2 state (00ef47e): outdoor places immediately again (fast teleports). The genuine bug found along the way — IsLandblockLoaded queried the wrong key form (& 0xFFFF0000 vs the stored | 0xFFFF) — is preserved in the history (c880973) and will be re-applied when we re-introduce a proper hold ON A FIXED FOUNDATION. Decision (user, 2026-06-21): fix the foundation FIRST — fast/complete streaming during teleport (#138), the post-teleport lost-collision bug, and the FPS leak (Work item C) — then revisit the teleport-flow animation. Slice 1 (the pure TeleportAnimSequencer) stays in (dormant, unwired, harmless). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c8809735f3
commit
dd2eb8b39d
6 changed files with 43 additions and 195 deletions
|
|
@ -5471,7 +5471,6 @@ public sealed class GameWindow : IDisposable
|
||||||
// impossible claim / timeout. PortalSpace keeps input frozen meanwhile.
|
// impossible claim / timeout. PortalSpace keeps input frozen meanwhile.
|
||||||
EnsureTeleportArrivalController();
|
EnsureTeleportArrivalController();
|
||||||
_pendingTeleportRot = rot;
|
_pendingTeleportRot = rot;
|
||||||
_lastArrivalVerdict = AcDream.App.World.ArrivalReadiness.NotReady; // reset probe state for each new hold
|
|
||||||
_teleportArrival!.BeginArrival(newWorldPos, p.LandblockId);
|
_teleportArrival!.BeginArrival(newWorldPos, p.LandblockId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5482,8 +5481,6 @@ public sealed class GameWindow : IDisposable
|
||||||
// runtime deps are wired by then).
|
// runtime deps are wired by then).
|
||||||
private AcDream.App.World.TeleportArrivalController? _teleportArrival;
|
private AcDream.App.World.TeleportArrivalController? _teleportArrival;
|
||||||
private System.Numerics.Quaternion _pendingTeleportRot = System.Numerics.Quaternion.Identity;
|
private System.Numerics.Quaternion _pendingTeleportRot = System.Numerics.Quaternion.Identity;
|
||||||
// §3.4 arrival-gate probe: tracks previous verdict so we log only the flip.
|
|
||||||
private AcDream.App.World.ArrivalReadiness _lastArrivalVerdict = AcDream.App.World.ArrivalReadiness.NotReady;
|
|
||||||
|
|
||||||
// #145: the LANDBLOCK-relative (cell-local) position used to SEED the player
|
// #145: the LANDBLOCK-relative (cell-local) position used to SEED the player
|
||||||
// body's cell-relative CellPosition. This is the ONE place the streaming center
|
// body's cell-relative CellPosition. This is the ONE place the streaming center
|
||||||
|
|
@ -5512,36 +5509,24 @@ public sealed class GameWindow : IDisposable
|
||||||
// Reuses the #107 login readiness triplet (GameWindow.cs:1010-1024), evaluated
|
// Reuses the #107 login readiness triplet (GameWindow.cs:1010-1024), evaluated
|
||||||
// against the teleport's (destPos, destCell): an impossible indoor claim short-
|
// against the teleport's (destPos, destCell): an impossible indoor claim short-
|
||||||
// circuits to immediate placement; otherwise hold until terrain is sampled and,
|
// circuits to immediate placement; otherwise hold until terrain is sampled and,
|
||||||
// for an indoor cell, the cell struct has hydrated. For an outdoor cell, hold
|
// for an indoor cell, the cell struct has hydrated.
|
||||||
// until the physics landblock is registered (§3.4 #145 residual fix).
|
|
||||||
private AcDream.App.World.ArrivalReadiness TeleportArrivalReadiness(
|
private AcDream.App.World.ArrivalReadiness TeleportArrivalReadiness(
|
||||||
System.Numerics.Vector3 destPos, uint destCell)
|
System.Numerics.Vector3 destPos, uint destCell)
|
||||||
{
|
{
|
||||||
bool claimUnhydratable = IsSpawnClaimUnhydratable(destCell);
|
bool claimUnhydratable = IsSpawnClaimUnhydratable(destCell);
|
||||||
|
|
||||||
|
// #135: an INDOOR destination (sealed dungeon / building interior) gates on the
|
||||||
|
// EnvCell FLOOR hydrating. Retail places on the cell floor. An OUTDOOR destination
|
||||||
|
// places immediately on the server-authoritative position — see TeleportArrivalRules
|
||||||
|
// (#145: holding is futile because streaming doesn't progress during a PortalSpace
|
||||||
|
// hold; the stale source landblock is dropped at recenter so the resolve trusts the
|
||||||
|
// server cell until the destination streams in).
|
||||||
bool indoor = (destCell & 0xFFFFu) >= 0x0100u;
|
bool indoor = (destCell & 0xFFFFu) >= 0x0100u;
|
||||||
bool indoorCellReady = indoor && !claimUnhydratable
|
bool indoorCellReady = indoor && !claimUnhydratable
|
||||||
&& _physicsEngine.IsSpawnCellReady(destCell);
|
&& _physicsEngine.IsSpawnCellReady(destCell);
|
||||||
// §3.4: for outdoor arrivals, gate on the physics landblock being resident.
|
|
||||||
// The landblock id is the high 16 bits of the cell id (i.e. destCell & 0xFFFF0000).
|
|
||||||
// Streaming progresses during the hold because the streaming controller runs before
|
|
||||||
// TeleportArrivalController.Tick. The 10 s _maxHoldFrames safety-net remains active.
|
|
||||||
bool outdoorReady = !indoor
|
|
||||||
&& _physicsEngine.IsLandblockLoaded(destCell & 0xFFFF0000u);
|
|
||||||
|
|
||||||
var verdict = AcDream.App.World.TeleportArrivalRules.Decide(
|
return AcDream.App.World.TeleportArrivalRules.Decide(
|
||||||
claimUnhydratable, indoor, indoorCellReady, outdoorReady);
|
claimUnhydratable, indoor, indoorCellReady);
|
||||||
|
|
||||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeArrivalGateEnabled
|
|
||||||
&& verdict != _lastArrivalVerdict
|
|
||||||
&& verdict != AcDream.App.World.ArrivalReadiness.NotReady)
|
|
||||||
{
|
|
||||||
Console.WriteLine(System.FormattableString.Invariant(
|
|
||||||
$"[arrival-gate] destCell=0x{destCell:X8} landblock=0x{(destCell & 0xFFFF0000u):X8} verdict={verdict} indoor={indoor} landblockCount={_physicsEngine.LandblockCount}"));
|
|
||||||
}
|
|
||||||
_lastArrivalVerdict = verdict;
|
|
||||||
|
|
||||||
return verdict;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The deferred snap (the original OnLivePositionUpdated steps 2-5), now run only
|
// The deferred snap (the original OnLivePositionUpdated steps 2-5), now run only
|
||||||
|
|
|
||||||
|
|
@ -116,29 +116,29 @@ public static class TeleportArrivalRules
|
||||||
/// <param name="claimUnhydratable">The destination cell can never hydrate.</param>
|
/// <param name="claimUnhydratable">The destination cell can never hydrate.</param>
|
||||||
/// <param name="indoor">Destination is an indoor cell (cell index ≥ 0x0100).</param>
|
/// <param name="indoor">Destination is an indoor cell (cell index ≥ 0x0100).</param>
|
||||||
/// <param name="indoorCellReady">For an indoor destination, the EnvCell floor has hydrated.</param>
|
/// <param name="indoorCellReady">For an indoor destination, the EnvCell floor has hydrated.</param>
|
||||||
/// <param name="outdoorReady">For an outdoor destination, the physics landblock is loaded
|
|
||||||
/// (<see cref="AcDream.Core.Physics.PhysicsEngine.IsLandblockLoaded"/> returned true for the
|
|
||||||
/// destination landblock id). Ignored for indoor destinations.</param>
|
|
||||||
public static ArrivalReadiness Decide(
|
public static ArrivalReadiness Decide(
|
||||||
bool claimUnhydratable,
|
bool claimUnhydratable,
|
||||||
bool indoor,
|
bool indoor,
|
||||||
bool indoorCellReady,
|
bool indoorCellReady)
|
||||||
bool outdoorReady)
|
|
||||||
{
|
{
|
||||||
if (claimUnhydratable)
|
if (claimUnhydratable)
|
||||||
return ArrivalReadiness.Impossible;
|
return ArrivalReadiness.Impossible;
|
||||||
|
|
||||||
// Indoor (sealed dungeon / building interior): gate on the EnvCell floor hydrating,
|
// Indoor (sealed dungeon / building interior): gate on the EnvCell floor hydrating,
|
||||||
// exactly as #135. Unaffected by the outdoor-landblock gate.
|
// exactly as #135. The dungeon-IN path pre-collapses + waits for the cell struct, and
|
||||||
|
// its placement validates the specific claimed cell (the indoor resolve is cell-keyed,
|
||||||
|
// not the grid-snap), so it is unaffected by the overlap that broke teleport-OUT.
|
||||||
if (indoor)
|
if (indoor)
|
||||||
return indoorCellReady ? ArrivalReadiness.Ready : ArrivalReadiness.NotReady;
|
return indoorCellReady ? ArrivalReadiness.Ready : ArrivalReadiness.NotReady;
|
||||||
|
|
||||||
// Outdoor (#145 §3.4): hold until the physics landblock is registered.
|
// Outdoor: place IMMEDIATELY on the server-authoritative position. #145/#138 — holding
|
||||||
// AddLandblock is atomic (terrain + cells + portals in one write — PhysicsEngine.cs:70),
|
// for the destination to stream in is futile: streaming does NOT progress while the
|
||||||
// so ContainsKey implies the collision mesh is present and resolvable. The hold is
|
// player is held in PortalSpace (the destination only loads once placement flips the
|
||||||
// self-resolving: streaming progresses unconditionally during the hold because the
|
// state to InWorld). The stale source center landblock is dropped at recenter (see
|
||||||
// streaming controller runs before TeleportArrivalController.Tick (GameWindow.cs:7420-7551).
|
// GameWindow.OnLivePositionUpdated), so the arrival resolve falls through to the
|
||||||
// The 10 s timeout safety-net (_maxHoldFrames) remains active.
|
// server-authoritative cell/position (Resolve NO-LANDBLOCK verbatim) and the player is
|
||||||
return outdoorReady ? ArrivalReadiness.Ready : ArrivalReadiness.NotReady;
|
// placed grounded there; streaming then loads the destination and the per-frame resolve
|
||||||
|
// grounds onto it. This trusts the server's teleport position — retail-faithful.
|
||||||
|
return ArrivalReadiness.Ready;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,19 +31,6 @@ public static class PhysicsDiagnostics
|
||||||
public static bool ProbeResolveEnabled { get; set; } =
|
public static bool ProbeResolveEnabled { get; set; } =
|
||||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_RESOLVE") == "1";
|
Environment.GetEnvironmentVariable("ACDREAM_PROBE_RESOLVE") == "1";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retail teleport readiness gate (§3.4, 2026-06-21). When true, every transition of
|
|
||||||
/// the <c>TeleportArrivalReadiness</c> verdict (NotReady → Ready / Impossible) emits one
|
|
||||||
/// <c>[arrival-gate]</c> line: the destination cell id, landblock id, verdict,
|
|
||||||
/// <c>PhysicsEngine.LandblockCount</c> at the moment it flips, and whether the
|
|
||||||
/// flip was forced (Impossible) or natural (Ready).
|
|
||||||
///
|
|
||||||
/// <para>Initial state from <c>ACDREAM_PROBE_RESOLVE=1</c> — shares the existing probe
|
|
||||||
/// flag because this is a readiness-path extension; no new env var needed.</para>
|
|
||||||
/// </summary>
|
|
||||||
public static bool ProbeArrivalGateEnabled { get; set; } =
|
|
||||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_RESOLVE") == "1";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When true, every change to <c>PlayerMovementController.CellId</c>
|
/// When true, every change to <c>PlayerMovementController.CellId</c>
|
||||||
/// emits one <c>[cell-transit]</c> line: old → new cell, current
|
/// emits one <c>[cell-transit]</c> line: old → new cell, current
|
||||||
|
|
@ -547,7 +534,6 @@ public static class PhysicsDiagnostics
|
||||||
public static void ResetForTest()
|
public static void ResetForTest()
|
||||||
{
|
{
|
||||||
ProbeResolveEnabled = false;
|
ProbeResolveEnabled = false;
|
||||||
ProbeArrivalGateEnabled = false;
|
|
||||||
ProbeCellEnabled = false;
|
ProbeCellEnabled = false;
|
||||||
ProbeBuildingEnabled = false;
|
ProbeBuildingEnabled = false;
|
||||||
ProbeCellSetEnabled = false;
|
ProbeCellSetEnabled = false;
|
||||||
|
|
|
||||||
|
|
@ -31,25 +31,6 @@ public sealed class PhysicsEngine
|
||||||
/// <summary>Number of registered landblocks (diagnostic).</summary>
|
/// <summary>Number of registered landblocks (diagnostic).</summary>
|
||||||
public int LandblockCount => _landblocks.Count;
|
public int LandblockCount => _landblocks.Count;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns true iff the landblock CONTAINING the given id has been registered via
|
|
||||||
/// <see cref="AddLandblock"/>. Because <see cref="AddLandblock"/> registers terrain +
|
|
||||||
/// collision cells + portals atomically, a hit implies the collision mesh is present
|
|
||||||
/// and resolvable.
|
|
||||||
/// <para>
|
|
||||||
/// 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
|
|
||||||
/// <c>destCell & 0xFFFF0000</c>), or the dat-id storage form. The low 16 bits are
|
|
||||||
/// normalized to the canonical landblock key (<c>0xFFFF</c>) that streaming stores under
|
|
||||||
/// (<see cref="AcDream.App.Streaming.StreamingRegion.EncodeLandblockId"/> ORs in 0xFFFF),
|
|
||||||
/// so all three forms resolve. The earlier raw <c>ContainsKey(id & 0xFFFF0000)</c>
|
|
||||||
/// never matched the 0xFFFF-terminated stored key, leaving the outdoor teleport gate
|
|
||||||
/// permanently NotReady (2026-06-21 #145 apparatus re-test).
|
|
||||||
/// </para>
|
|
||||||
/// </summary>
|
|
||||||
public bool IsLandblockLoaded(uint landblockId) =>
|
|
||||||
_landblocks.ContainsKey((landblockId & 0xFFFF0000u) | 0xFFFFu);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cell-based spatial index for static object collision.
|
/// Cell-based spatial index for static object collision.
|
||||||
/// Populated during landblock streaming; queried by the Transition system.
|
/// Populated during landblock streaming; queried by the Transition system.
|
||||||
|
|
|
||||||
|
|
@ -4,63 +4,49 @@ using Xunit;
|
||||||
namespace AcDream.App.Tests.World;
|
namespace AcDream.App.Tests.World;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Full truth-table for TeleportArrivalRules.Decide after the §3.4 readiness-gate change.
|
/// The teleport-arrival readiness decision (#145/#138).
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>Unhydratable claim → place immediately via the caller's safety-net (Impossible).</item>
|
||||||
|
/// <item>Indoor (sealed dungeon / building interior) → hold until the EnvCell floor hydrates.</item>
|
||||||
|
/// <item>Outdoor → place IMMEDIATELY on the server-authoritative position. Holding is futile —
|
||||||
|
/// streaming does not progress while the player is held in PortalSpace, so the
|
||||||
|
/// destination can't stream in during a hold. The stale source landblock is dropped at
|
||||||
|
/// recenter (GameWindow.OnLivePositionUpdated), so the resolve falls through to the
|
||||||
|
/// server cell (Resolve NO-LANDBLOCK verbatim) until the destination streams in.</item>
|
||||||
|
/// </list>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class TeleportArrivalRulesTests
|
public class TeleportArrivalRulesTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Unhydratable_IsImpossible_Outdoor()
|
public void Unhydratable_IsImpossible()
|
||||||
{
|
{
|
||||||
Assert.Equal(ArrivalReadiness.Impossible,
|
Assert.Equal(ArrivalReadiness.Impossible,
|
||||||
TeleportArrivalRules.Decide(
|
TeleportArrivalRules.Decide(claimUnhydratable: true, indoor: false, indoorCellReady: false));
|
||||||
claimUnhydratable: true, indoor: false,
|
// …even for an indoor claim.
|
||||||
indoorCellReady: false, outdoorReady: false));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Unhydratable_IsImpossible_Indoor()
|
|
||||||
{
|
|
||||||
Assert.Equal(ArrivalReadiness.Impossible,
|
Assert.Equal(ArrivalReadiness.Impossible,
|
||||||
TeleportArrivalRules.Decide(
|
TeleportArrivalRules.Decide(claimUnhydratable: true, indoor: true, indoorCellReady: false));
|
||||||
claimUnhydratable: true, indoor: true,
|
|
||||||
indoorCellReady: true, outdoorReady: false));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Indoor_CellReady_IsReady()
|
public void Indoor_CellReady_IsReady()
|
||||||
{
|
{
|
||||||
Assert.Equal(ArrivalReadiness.Ready,
|
Assert.Equal(ArrivalReadiness.Ready,
|
||||||
TeleportArrivalRules.Decide(
|
TeleportArrivalRules.Decide(claimUnhydratable: false, indoor: true, indoorCellReady: true));
|
||||||
claimUnhydratable: false, indoor: true,
|
|
||||||
indoorCellReady: true, outdoorReady: false));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Indoor_CellNotReady_IsNotReady()
|
public void Indoor_CellNotReady_HoldsNotReady()
|
||||||
{
|
{
|
||||||
Assert.Equal(ArrivalReadiness.NotReady,
|
Assert.Equal(ArrivalReadiness.NotReady,
|
||||||
TeleportArrivalRules.Decide(
|
TeleportArrivalRules.Decide(claimUnhydratable: false, indoor: true, indoorCellReady: false));
|
||||||
claimUnhydratable: false, indoor: true,
|
|
||||||
indoorCellReady: false, outdoorReady: false));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Outdoor_LandblockLoaded_IsReady()
|
public void Outdoor_PlacesImmediately()
|
||||||
{
|
{
|
||||||
|
// #145: never hold an outdoor arrival — place at once on the server position. The
|
||||||
|
// indoorCellReady flag is irrelevant for an outdoor destination.
|
||||||
Assert.Equal(ArrivalReadiness.Ready,
|
Assert.Equal(ArrivalReadiness.Ready,
|
||||||
TeleportArrivalRules.Decide(
|
TeleportArrivalRules.Decide(claimUnhydratable: false, indoor: false, indoorCellReady: false));
|
||||||
claimUnhydratable: false, indoor: false,
|
|
||||||
indoorCellReady: false, outdoorReady: true));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Outdoor_LandblockNotLoaded_IsNotReady()
|
|
||||||
{
|
|
||||||
// §3.4: the #145 residual — outdoor arrival on a not-yet-streamed landblock
|
|
||||||
// must hold. Previously returned Ready immediately.
|
|
||||||
Assert.Equal(ArrivalReadiness.NotReady,
|
|
||||||
TeleportArrivalRules.Decide(
|
|
||||||
claimUnhydratable: false, indoor: false,
|
|
||||||
indoorCellReady: false, outdoorReady: false));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue