acdream/tests/AcDream.Core.Tests/Physics/CellMarchLandblockPreservationTests.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

85 lines
4.5 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;
// 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→lbX1 or lbY→lbY1
// 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));
}
}