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>
65 lines
3.4 KiB
C#
65 lines
3.4 KiB
C#
using System.Numerics;
|
||
using AcDream.Core.Physics;
|
||
using Xunit;
|
||
|
||
namespace AcDream.Core.Tests.Physics;
|
||
|
||
// #145 — far-town teleport resolver runaway. A teleport to a far town places the
|
||
// player at a landblock EDGE; the first physics tick crosses into a neighbour that
|
||
// has NOT streamed in yet. Pre-fix, the membership pick derived the block origin from
|
||
// the terrain registry, which returns (0,0) for an unstreamed neighbour, so
|
||
// AdjustToOutside marched the cell id one landblock per tick (the cascade) until lbY
|
||
// hit 0 and the outbound wire synthesized the bogus localY (17410) that ACE rejects.
|
||
//
|
||
// The fix (Slice 3): the resolve threads the CARRIED cell-relative frame anchor
|
||
// (body.Position − body.CellPosition.Frame.Origin) into the pick. That anchor is the
|
||
// TRUE landblock world origin — correct even for an unstreamed neighbour — so the pick
|
||
// re-derives the SAME (consistent) cell and never marches.
|
||
//
|
||
// These tests exercise CellTransit.FindCellSet directly with an EMPTY cache (so the
|
||
// neighbour landblock is unstreamed → TryGetTerrainOrigin returns false). With
|
||
// #145 Slice 3 (CarriedBlockOrigin), the explicit anchor prevents the march. With
|
||
// #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
|
||
{
|
||
private static int LbX(uint cellId) => (int)((cellId >> 24) & 0xFFu);
|
||
private static int LbY(uint cellId) => (int)((cellId >> 16) & 0xFFu);
|
||
|
||
[Fact]
|
||
public void SouthEdge_UnstreamedNeighbour_CarriedAnchor_DoesNotMarch()
|
||
{
|
||
// Streaming center is the far town 0xC95B; the player just crossed its south
|
||
// edge (world Y ≈ −0.088) into 0xC95A (origin (0,−192), UNSTREAMED).
|
||
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 = 0xC95A0001u;
|
||
var anchor = new Vector3(0f, -192f, 0f); // body.Position − CellPosition.Origin
|
||
|
||
uint withAnchor = CellTransit.FindCellSet(cache, spheres, 1, currentCell, out _, anchor);
|
||
Assert.Equal(0x5A, LbY(withAnchor)); // stays in 0xC95A — no march
|
||
|
||
// #145 D (2026-06-22): no-anchor path also preserves when terrain is unregistered.
|
||
// Pre-D this marched south to 0x59; post-D returns currentCell unchanged.
|
||
uint noAnchor = CellTransit.FindCellSet(cache, spheres, 1, currentCell, out _, null);
|
||
Assert.Equal(currentCell, noAnchor);
|
||
}
|
||
|
||
[Fact]
|
||
public void EastEdge_UnstreamedNeighbour_CarriedAnchor_DoesNotMarch()
|
||
{
|
||
// Player crossed the center's east edge (world X ≈ 192.088) into 0xCA5B
|
||
// (origin (+192,0), UNSTREAMED). lbX 0xCA > 0xC9.
|
||
var cache = new PhysicsDataCache();
|
||
var spheres = new[] { new DatReaderWriter.Types.Sphere { Origin = new Vector3(192.088f, 14.8f, 12f), Radius = 0.48f } };
|
||
const uint currentCell = 0xCA5B0001u;
|
||
var anchor = new Vector3(192f, 0f, 0f);
|
||
|
||
uint withAnchor = CellTransit.FindCellSet(cache, spheres, 1, currentCell, out _, anchor);
|
||
Assert.Equal(0xCA, LbX(withAnchor)); // stays in 0xCA5B — no march
|
||
|
||
// #145 D: no-anchor path also preserves when terrain is unregistered.
|
||
uint noAnchor = CellTransit.FindCellSet(cache, spheres, 1, currentCell, out _, null);
|
||
Assert.Equal(currentCell, noAnchor);
|
||
}
|
||
}
|