acdream/tests/AcDream.Core.Tests/Physics/TeleportFarTownRunawayTests.cs
Erik 02f4be72c0 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>
2026-06-22 13:38:48 +02:00

65 lines
3.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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