fix(teleport D): cell-march preserves seed landblock id when no resident LB (no more lbX=0 outbound)
BuildCellSetAndPickContaining discarded the bool from TryGetTerrainOrigin — when the current landblock's terrain hadn't been applied yet (priority-apply in flight after a teleport or dungeon exit), blockOrigin was silently set to (0,0,0). The AdjustToOutside/GetOutsideLcoord math treated world-frame sphere coordinates as block-local and marched the cell one landblock per tick in the direction of movement until lbX or lbY underflowed to 0x00. ACE rejected every subsequent move as a failed transition. Fix: honor the bool return. When terrain is unregistered for an OUTDOOR seed (low < 0x0100), return currentCellId verbatim — "no block-local frame → preserve". This mirrors the NO-LANDBLOCK verbatim contract in PhysicsEngine.Resolve and is correct: the cell stays last-known-correct until terrain registers. Indoor seeds are explicitly excluded (blockOrigin is never consumed by the indoor pick path; outdoorPickAllowed=false for indoor seeds). Reproduce + verify via CellMarchLandblockPreservationTests (two new FAILING-before tests: WestEdge and SouthEdge with empty cache, no anchor → lbX/lbY preserved). TeleportFarTownRunawayTests updated: no-anchor path now also preserves (pre-fix it marched south to 0x59; post-fix returns currentCell unchanged). CellTransitFindCellSetTests, Issue112MembershipTests, PhysicsEngineTests: added RegisterTerrain for the streaming-center block (in production it is always resident before outdoor resolves run; tests that used blockOrigin=(0,0,0) as an implicit fallback now register the block explicitly). All 1567 tests pass. Divergence AD-30 added to retail-divergence-register.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
aba882cec6
commit
02f4be72c0
7 changed files with 144 additions and 15 deletions
|
|
@ -61,7 +61,7 @@ accepted-divergence entries (#96, #49, #50).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Adaptation (AD) — 28 rows
|
## 2. Adaptation (AD) — 30 rows
|
||||||
|
|
||||||
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
|
|
@ -94,6 +94,7 @@ accepted-divergence entries (#96, #49, #50).
|
||||||
| AD-27 | Use/PickUp action re-sent on natural auto-walk arrival; retail sends the action once (server MoveToChain callback completes it) | `src/AcDream.App/Input/PlayerMovementController.cs:322` | ACE's server-side chain may have timed out by the time our body arrives; the close-range re-send hits ACE's WithinUseRadius fast-path | If the server's chain has NOT timed out, the action executes twice — door toggles open-then-closed, use-once interactions double-fire; protocol noise on non-ACE servers | ACE CreateMoveToChain / WithinUseRadius |
|
| AD-27 | Use/PickUp action re-sent on natural auto-walk arrival; retail sends the action once (server MoveToChain callback completes it) | `src/AcDream.App/Input/PlayerMovementController.cs:322` | ACE's server-side chain may have timed out by the time our body arrives; the close-range re-send hits ACE's WithinUseRadius fast-path | If the server's chain has NOT timed out, the action executes twice — door toggles open-then-closed, use-once interactions double-fire; protocol noise on non-ACE servers | ACE CreateMoveToChain / WithinUseRadius |
|
||||||
| AD-28 | Chat transcript (`UiText`) and input (`UiChatInput`) are two separate widget classes placed inside their dat-authored container panels; retail's `ChatInterface` uses a single mode-flagged `UIElement_Text` (Type-12) that switches between read and edit mode | `src/AcDream.App/UI/Layout/ChatWindowController.cs:135` (transcript) + `:150` (input) | `UIElement_Text` is inside keystone.dll with no PDB/decomp; a two-widget split is functionally equivalent (read-only scroll, editable input) and is the structural adaptation required by our UiElement architecture | A future consumer expecting a single widget for both read/write (e.g. a plugin calling the chat API and getting one widget back) must be written to the two-widget contract | `UIElement_Text` (Type-12) @ keystone.dll; `gmMainChatUI::PostInit` @0x4ce130 |
|
| AD-28 | Chat transcript (`UiText`) and input (`UiChatInput`) are two separate widget classes placed inside their dat-authored container panels; retail's `ChatInterface` uses a single mode-flagged `UIElement_Text` (Type-12) that switches between read and edit mode | `src/AcDream.App/UI/Layout/ChatWindowController.cs:135` (transcript) + `:150` (input) | `UIElement_Text` is inside keystone.dll with no PDB/decomp; a two-widget split is functionally equivalent (read-only scroll, editable input) and is the structural adaptation required by our UiElement architecture | A future consumer expecting a single widget for both read/write (e.g. a plugin calling the chat API and getting one widget back) must be written to the two-widget contract | `UIElement_Text` (Type-12) @ keystone.dll; `gmMainChatUI::PostInit` @0x4ce130 |
|
||||||
| AD-29 | `ClientObjectTable` fires global `ObjectAdded`/`ObjectUpdated`/`ObjectRemoved` events; consumers filter by guid on their end. Retail dispatches per-object via `NoticeRegistrar` observer dispatch — each UI cell observes only its specific object guid | `src/AcDream.Core/Items/ClientObjectTable.cs:48` (events); `src/AcDream.App/UI/Layout/ToolbarController.cs:115` (guid filter) | `NoticeRegistrar` is inside keystone.dll with no PDB/decomp; global broadcast + consumer-side filter is functionally equivalent for the current panel count and object volumes seen in practice | At high object counts (>1 000 objects), every `ObjectUpdated` wakes every subscribed consumer — O(n·m) notification cost instead of retail's O(1) per-observer dispatch; a consumer that forgets the guid filter processes all objects (a latent correctness bug) | `NoticeRegistrar` (keystone.dll, no PDB); retail per-object observer registration in `CObjectMaint` |
|
| AD-29 | `ClientObjectTable` fires global `ObjectAdded`/`ObjectUpdated`/`ObjectRemoved` events; consumers filter by guid on their end. Retail dispatches per-object via `NoticeRegistrar` observer dispatch — each UI cell observes only its specific object guid | `src/AcDream.Core/Items/ClientObjectTable.cs:48` (events); `src/AcDream.App/UI/Layout/ToolbarController.cs:115` (guid filter) | `NoticeRegistrar` is inside keystone.dll with no PDB/decomp; global broadcast + consumer-side filter is functionally equivalent for the current panel count and object volumes seen in practice | At high object counts (>1 000 objects), every `ObjectUpdated` wakes every subscribed consumer — O(n·m) notification cost instead of retail's O(1) per-observer dispatch; a consumer that forgets the guid filter processes all objects (a latent correctness bug) | `NoticeRegistrar` (keystone.dll, no PDB); retail per-object observer registration in `CObjectMaint` |
|
||||||
|
| AD-30 | Cell-march preserves seed landblock id when `TryGetTerrainOrigin` returns false for an outdoor seed (#145 D, 2026-06-22): `BuildCellSetAndPickContaining` returns `currentCellId` verbatim rather than marching via `blockOrigin=(0,0,0)`; retail never encounters this state (cells stored block-local, no streaming-gap concept) | `src/AcDream.Core/Physics/CellTransit.cs:765` | Equivalence argument: "preserve-verbatim when unregistered" is the same contract as `PhysicsEngine.Resolve`'s NO-LANDBLOCK branch; the player's cell stays the last known-correct cell until the landblock's terrain registers — no march, no lbX=0 wire | A body whose seed landblock is genuinely absent for >1 physics tick holds its last-known cell rather than discovering the true containing cell; transient only — corrects the instant terrain registers; an indoor seed is explicitly excluded from the guard (outdoor low < 0x100 gate) | `CObjCell::find_cell_list` + block-local storage (retail has no streaming gap); `TryGetTerrainOrigin` pc path |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -742,11 +742,32 @@ public static class CellTransit
|
||||||
//
|
//
|
||||||
// #106: blockOrigin converts the world-frame sphere coords into retail's
|
// #106: blockOrigin converts the world-frame sphere coords into retail's
|
||||||
// block-local frame for the LandDefs lcoord math.
|
// block-local frame for the LandDefs lcoord math.
|
||||||
|
//
|
||||||
|
// #145 D (2026-06-22): honor TryGetTerrainOrigin's false return for OUTDOOR
|
||||||
|
// seeds. When the current landblock has not yet been applied (priority-apply
|
||||||
|
// in flight or streaming still warm), blockOrigin=(0,0) is wrong — the pick
|
||||||
|
// treats the world-frame sphere coordinates as if they were block-local and
|
||||||
|
// marches the cell one block per tick until lbX or lbY underflows to 0x00
|
||||||
|
// (the "lbX=0" outbound wire that ACE rejects). "frame not yet authoritative
|
||||||
|
// → preserve verbatim" mirrors the NO-LANDBLOCK contract in PhysicsEngine.Resolve.
|
||||||
|
// Indoor seeds are NOT guarded here because blockOrigin is only consumed by the
|
||||||
|
// outdoor pick path (outdoorPickAllowed=false for indoor seeds); returning early
|
||||||
|
// for indoor seeds would break all interior cell-set builds (regression).
|
||||||
|
// Adaptation (not a direct retail port — retail gets it free via cell-relative
|
||||||
|
// storage); registered as divergence-register row added in this commit.
|
||||||
Vector3 blockOrigin;
|
Vector3 blockOrigin;
|
||||||
if (carriedBlockOrigin is { } carriedAnchor)
|
if (carriedBlockOrigin is { } carriedAnchor)
|
||||||
|
{
|
||||||
blockOrigin = carriedAnchor;
|
blockOrigin = carriedAnchor;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
cache.CellGraph.TryGetTerrainOrigin(currentCellId, out blockOrigin);
|
{
|
||||||
|
bool terrainResident = cache.CellGraph.TryGetTerrainOrigin(currentCellId, out blockOrigin);
|
||||||
|
// Outdoor seed with no resident terrain: no valid block-local frame → preserve seed verbatim.
|
||||||
|
// Indoor seeds proceed regardless (blockOrigin unused for indoor picks).
|
||||||
|
if (!terrainResident && currentLow < 0x0100u)
|
||||||
|
return currentCellId;
|
||||||
|
}
|
||||||
|
|
||||||
// #112 rider: outdoor candidates may win the pick only when retail would
|
// #112 rider: outdoor candidates may win the pick only when retail would
|
||||||
// have admitted them — outdoor seeds always; indoor seeds only when a
|
// have admitted them — outdoor seeds always; indoor seeds only when a
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,12 @@ public sealed class Issue112MembershipTests
|
||||||
try { ConformanceDats.LoadEnvCell(dats, cache, lbPrefix | low); }
|
try { ConformanceDats.LoadEnvCell(dats, cache, lbPrefix | low); }
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
// #145 D: register terrain for the block so TryGetTerrainOrigin succeeds for
|
||||||
|
// outdoor seeds (in production the streaming-center landblock is always resident
|
||||||
|
// before outdoor physics resolves run). Tests use block-local coordinates, so
|
||||||
|
// origin is (0,0,0). Without this, Component D's early-return for unregistered
|
||||||
|
// terrain would prevent the building-portal walk that promotes outdoor→indoor.
|
||||||
|
cache.CellGraph.RegisterTerrain(lbPrefix, new TerrainSurface(new byte[81], new float[256]), Vector3.Zero);
|
||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Tests.Physics;
|
||||||
|
|
||||||
|
// Component D — cell-march landblock preservation (#145 residual, 2026-06-22 spec §3 D).
|
||||||
|
//
|
||||||
|
// Scenario: a player arrives via teleport at outdoor cell 0xA9B40019 (lbX=0xA9, lbY=0xB4).
|
||||||
|
// The destination landblock has NOT yet been applied to the physics engine (priority-apply
|
||||||
|
// is in flight). The body's CarriedBlockOrigin is null (either the body has no seeded outdoor
|
||||||
|
// cell yet, or the incoming cell differs from the body's last-known cell).
|
||||||
|
//
|
||||||
|
// Without the fix: BuildCellSetAndPickContaining's TryGetTerrainOrigin returns false with
|
||||||
|
// blockOrigin=(0,0,0). AddAllOutsideCells treats worldSphereCenter as block-local and marches
|
||||||
|
// the cell one block in whatever direction the player is moving — lbX→lbX−1 or lbY→lbY−1
|
||||||
|
// per tick, cascading until lbX or lbY underflows to 0x00. The outbound wire then encodes
|
||||||
|
// the marched cell and ACE rejects every move (MOVEMENT SPEED / failed transition).
|
||||||
|
//
|
||||||
|
// With the fix: when TryGetTerrainOrigin returns false (block not resident), return the
|
||||||
|
// seed currentCellId verbatim — "no block-local frame → preserve" (same contract as
|
||||||
|
// PhysicsEngine.Resolve's NO-LANDBLOCK verbatim branch). The player cell stays correct
|
||||||
|
// until the landblock applies.
|
||||||
|
//
|
||||||
|
// The entry point exercised here is CellTransit.FindCellSet — the same function that
|
||||||
|
// RunCheckOtherCellsAndAdvance (TransitionTypes.cs) calls on every per-frame physics tick.
|
||||||
|
public class CellMarchLandblockPreservationTests
|
||||||
|
{
|
||||||
|
private static int LbX(uint cellId) => (int)((cellId >> 24) & 0xFFu);
|
||||||
|
private static int LbY(uint cellId) => (int)((cellId >> 16) & 0xFFu);
|
||||||
|
|
||||||
|
// ── West-edge crossing: no anchor, unstreamed terrain → must NOT march lbX ──────────────
|
||||||
|
[Fact]
|
||||||
|
public void WestEdge_UnstreamedLandblock_NoAnchor_PreservesSeedCell()
|
||||||
|
{
|
||||||
|
// Empty cache — landblock 0xA9B4 has not been applied (TryGetTerrainOrigin → false).
|
||||||
|
var cache = new PhysicsDataCache();
|
||||||
|
|
||||||
|
// Sphere just west of 0xA9B4's origin (world X = −0.088 → block-local X = −0.088
|
||||||
|
// relative to (0,0) origin → floor(−0.088/24) = −1 → lbX marches without fix).
|
||||||
|
var spheres = new[] { new DatReaderWriter.Types.Sphere { Origin = new Vector3(-0.088f, 14.8f, 12f), Radius = 0.48f } };
|
||||||
|
const uint currentCell = 0xA9B40019u;
|
||||||
|
|
||||||
|
// No carried anchor (the teleport-arrival first tick: body still carries an old
|
||||||
|
// cell or is null-seeded, so ResolveWithTransition passes null).
|
||||||
|
uint result = CellTransit.FindCellSet(cache, spheres, 1, currentCell, out _, null);
|
||||||
|
|
||||||
|
// The cell must be preserved unchanged — lbX must NOT drop below 0xA9.
|
||||||
|
Assert.Equal(0xA9, LbX(result));
|
||||||
|
// The full cell id must equal the seed (no march at all).
|
||||||
|
Assert.Equal(currentCell, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── South-edge crossing: no anchor, unstreamed terrain → must NOT march lbY ─────────────
|
||||||
|
[Fact]
|
||||||
|
public void SouthEdge_UnstreamedLandblock_NoAnchor_PreservesSeedCell()
|
||||||
|
{
|
||||||
|
var cache = new PhysicsDataCache();
|
||||||
|
var spheres = new[] { new DatReaderWriter.Types.Sphere { Origin = new Vector3(14.8f, -0.088f, 12f), Radius = 0.48f } };
|
||||||
|
const uint currentCell = 0xA9B40001u; // south-west landcell of 0xA9B4
|
||||||
|
|
||||||
|
uint result = CellTransit.FindCellSet(cache, spheres, 1, currentCell, out _, null);
|
||||||
|
|
||||||
|
// Without fix: lbY drops from 0xB4 to 0xB3 on the first tick, then cascades.
|
||||||
|
Assert.Equal(0xB4, LbY(result));
|
||||||
|
Assert.Equal(currentCell, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Carried anchor STILL works: if the anchor is supplied, the correct neighbour ────────
|
||||||
|
// landblock cell is returned even when terrain is unstreamed (the original #145 fix).
|
||||||
|
[Fact]
|
||||||
|
public void WithCarriedAnchor_UnstreamedNeighbour_ReturnsCorrectCell()
|
||||||
|
{
|
||||||
|
var cache = new PhysicsDataCache();
|
||||||
|
// Player crossed the south edge into 0xA9B3 (origin (0, −192)).
|
||||||
|
var spheres = new[] { new DatReaderWriter.Types.Sphere { Origin = new Vector3(14.8f, -0.088f, 12f), Radius = 0.48f } };
|
||||||
|
const uint currentCell = 0xA9B30001u;
|
||||||
|
var anchor = new Vector3(0f, -192f, 0f); // body.Position − CellPosition.Origin
|
||||||
|
|
||||||
|
uint result = CellTransit.FindCellSet(cache, spheres, 1, currentCell, out _, anchor);
|
||||||
|
|
||||||
|
// The anchor gives the correct block-local frame → stays in 0xA9B3 (no march back to B4).
|
||||||
|
Assert.Equal(0xB3, LbY(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -110,6 +110,9 @@ public class CellTransitFindCellSetTests
|
||||||
public void FindCellSet_OutdoorSeed_IncludesNeighbourLandcells()
|
public void FindCellSet_OutdoorSeed_IncludesNeighbourLandcells()
|
||||||
{
|
{
|
||||||
var cache = new PhysicsDataCache();
|
var cache = new PhysicsDataCache();
|
||||||
|
// #145 D: register A9B4 at origin (0,0) — in production the streaming-center
|
||||||
|
// landblock is always registered before any outdoor physics resolve runs.
|
||||||
|
cache.CellGraph.RegisterTerrain(0xA9B40000u, new TerrainSurface(new byte[81], new float[256]), Vector3.Zero);
|
||||||
// A6.P4 (2026-05-24): sphere coords are LANDBLOCK-LOCAL (X/Y in
|
// A6.P4 (2026-05-24): sphere coords are LANDBLOCK-LOCAL (X/Y in
|
||||||
// [0, 192]). Place the sphere center near the east boundary of
|
// [0, 192]). Place the sphere center near the east boundary of
|
||||||
// landcell grid(0,0) (i.e., near local X=24) so AddAllOutsideCells
|
// landcell grid(0,0) (i.e., near local X=24) so AddAllOutsideCells
|
||||||
|
|
@ -172,10 +175,13 @@ public class CellTransitFindCellSetTests
|
||||||
public void OutdoorSeed_CrossesLandblockBoundary_South()
|
public void OutdoorSeed_CrossesLandblockBoundary_South()
|
||||||
{
|
{
|
||||||
// The #106 acceptance golden: walking south out of A9B4, the outdoor
|
// The #106 acceptance golden: walking south out of A9B4, the outdoor
|
||||||
// cell must advance to the southern neighbour block's cell. Anchor
|
// cell must advance to the southern neighbour block's cell. Origin of
|
||||||
// frame (no registered terrain → origin Zero): world y = -0.2 is
|
// A9B4 is Zero (streaming center). #145 D: register A9B4 at (0,0) so
|
||||||
// 0.2 m into A9B3's row 7 under x=150 → cell 0xA9B30038.
|
// TryGetTerrainOrigin succeeds (in production the streaming-center
|
||||||
|
// landblock is always resident before any outdoor physics resolve).
|
||||||
|
// world y = -0.2 is 0.2 m into A9B3's row 7 under x=150 → cell 0xA9B30038.
|
||||||
var cache = new PhysicsDataCache();
|
var cache = new PhysicsDataCache();
|
||||||
|
cache.CellGraph.RegisterTerrain(0xA9B40000u, new TerrainSurface(new byte[81], new float[256]), Vector3.Zero);
|
||||||
|
|
||||||
uint containing = CellTransit.FindCellSet(
|
uint containing = CellTransit.FindCellSet(
|
||||||
cache, new Vector3(150f, -0.2f, 0f), sphereRadius: 0.5f,
|
cache, new Vector3(150f, -0.2f, 0f), sphereRadius: 0.5f,
|
||||||
|
|
@ -194,7 +200,9 @@ public class CellTransitFindCellSetTests
|
||||||
// neighbour (sphere overlaps it) but the centre column is still the
|
// neighbour (sphere overlaps it) but the centre column is still the
|
||||||
// current cell — membership must NOT flip early (single clean flip
|
// current cell — membership must NOT flip early (single clean flip
|
||||||
// at the line, matching the capture's 96/96 within-block behaviour).
|
// at the line, matching the capture's 96/96 within-block behaviour).
|
||||||
|
// #145 D: register A9B4 at (0,0) — streaming-center block is always resident.
|
||||||
var cache = new PhysicsDataCache();
|
var cache = new PhysicsDataCache();
|
||||||
|
cache.CellGraph.RegisterTerrain(0xA9B40000u, new TerrainSurface(new byte[81], new float[256]), Vector3.Zero);
|
||||||
|
|
||||||
uint containing = CellTransit.FindCellSet(
|
uint containing = CellTransit.FindCellSet(
|
||||||
cache, new Vector3(150f, 0.2f, 0f), sphereRadius: 0.5f,
|
cache, new Vector3(150f, 0.2f, 0f), sphereRadius: 0.5f,
|
||||||
|
|
|
||||||
|
|
@ -459,7 +459,13 @@ public class PhysicsEngineTests
|
||||||
// FindObjCollisions early-returns when DataCache is null. An empty
|
// FindObjCollisions early-returns when DataCache is null. An empty
|
||||||
// cache is enough for cylinder objects; only BSP objects look up
|
// cache is enough for cylinder objects; only BSP objects look up
|
||||||
// entries inside.
|
// entries inside.
|
||||||
engine.DataCache = new PhysicsDataCache();
|
// #145 D: register terrain for 0xA9B4 at (0,0,0) so TryGetTerrainOrigin
|
||||||
|
// succeeds for the outdoor seed cell (0xA9B40039). In production the
|
||||||
|
// streaming-center landblock is always resident before outdoor resolves run;
|
||||||
|
// we replicate that invariant here by registering a flat dummy terrain.
|
||||||
|
var freshCache = new PhysicsDataCache();
|
||||||
|
freshCache.CellGraph.RegisterTerrain(0xA9B4FFFFu, new TerrainSurface(FlatHeightmap(50), LinearHeightTable()), Vector3.Zero);
|
||||||
|
engine.DataCache = freshCache;
|
||||||
|
|
||||||
const uint movingEntityId = 0xDEADBEEFu;
|
const uint movingEntityId = 0xDEADBEEFu;
|
||||||
var bodyPos = new Vector3(96f, 96f, 50f);
|
var bodyPos = new Vector3(96f, 96f, 50f);
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,10 @@ namespace AcDream.Core.Tests.Physics;
|
||||||
// re-derives the SAME (consistent) cell and never marches.
|
// re-derives the SAME (consistent) cell and never marches.
|
||||||
//
|
//
|
||||||
// These tests exercise CellTransit.FindCellSet directly with an EMPTY cache (so the
|
// These tests exercise CellTransit.FindCellSet directly with an EMPTY cache (so the
|
||||||
// neighbour landblock is unstreamed → the legacy path's (0,0) fallback fires) and
|
// neighbour landblock is unstreamed → TryGetTerrainOrigin returns false). With
|
||||||
// compare the carried-anchor pick against the legacy pick. The cascade is direction-
|
// #145 Slice 3 (CarriedBlockOrigin), the explicit anchor prevents the march. With
|
||||||
// agnostic, so both a south and an east edge crossing are covered.
|
// #145 D (2026-06-22, the TryGetTerrainOrigin-bool fix), the no-anchor path ALSO
|
||||||
|
// preserves the seed — "no frame → preserve verbatim". Both are verified here.
|
||||||
public class TeleportFarTownRunawayTests
|
public class TeleportFarTownRunawayTests
|
||||||
{
|
{
|
||||||
private static int LbX(uint cellId) => (int)((cellId >> 24) & 0xFFu);
|
private static int LbX(uint cellId) => (int)((cellId >> 24) & 0xFFu);
|
||||||
|
|
@ -38,10 +39,10 @@ public class TeleportFarTownRunawayTests
|
||||||
uint withAnchor = CellTransit.FindCellSet(cache, spheres, 1, currentCell, out _, anchor);
|
uint withAnchor = CellTransit.FindCellSet(cache, spheres, 1, currentCell, out _, anchor);
|
||||||
Assert.Equal(0x5A, LbY(withAnchor)); // stays in 0xC95A — no march
|
Assert.Equal(0x5A, LbY(withAnchor)); // stays in 0xC95A — no march
|
||||||
|
|
||||||
// Legacy path (no anchor → TryGetTerrainOrigin returns (0,0) for the
|
// #145 D (2026-06-22): no-anchor path also preserves when terrain is unregistered.
|
||||||
// unstreamed neighbour) marches the cell one landblock south.
|
// Pre-D this marched south to 0x59; post-D returns currentCell unchanged.
|
||||||
uint legacy = CellTransit.FindCellSet(cache, spheres, 1, currentCell, out _, null);
|
uint noAnchor = CellTransit.FindCellSet(cache, spheres, 1, currentCell, out _, null);
|
||||||
Assert.True(LbY(legacy) < 0x5A, $"legacy should march south of 0x5A, got 0x{LbY(legacy):X2}");
|
Assert.Equal(currentCell, noAnchor);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -57,7 +58,8 @@ public class TeleportFarTownRunawayTests
|
||||||
uint withAnchor = CellTransit.FindCellSet(cache, spheres, 1, currentCell, out _, anchor);
|
uint withAnchor = CellTransit.FindCellSet(cache, spheres, 1, currentCell, out _, anchor);
|
||||||
Assert.Equal(0xCA, LbX(withAnchor)); // stays in 0xCA5B — no march
|
Assert.Equal(0xCA, LbX(withAnchor)); // stays in 0xCA5B — no march
|
||||||
|
|
||||||
uint legacy = CellTransit.FindCellSet(cache, spheres, 1, currentCell, out _, null);
|
// #145 D: no-anchor path also preserves when terrain is unregistered.
|
||||||
Assert.True(LbX(legacy) > 0xCA, $"legacy should march east of 0xCA, got 0x{LbX(legacy):X2}");
|
uint noAnchor = CellTransit.FindCellSet(cache, spheres, 1, currentCell, out _, null);
|
||||||
|
Assert.Equal(currentCell, noAnchor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue