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>
85 lines
4.5 KiB
C#
85 lines
4.5 KiB
C#
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));
|
||
}
|
||
}
|