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); } }