acdream/tests/AcDream.Core.Tests/Physics/SpawnPlacementSettlerTests.cs
Erik fafc0b65d9 fix(physics): adopt the settle's resolved cell across an indoor seam (#276 remainder)
SpawnPlacementSettler committed settle.Position but discarded settle.CellId,
so a compressed first-gravity-frame settle that crossed a cell boundary left
the body's cell at the placement cell until some later resolve corrected it.
Now committed through the same guarded channel the per-tick resolve writeback
uses (RuntimeOrdinaryPhysicsUpdater): resolved cell when the transition
reports one, source cell otherwise, never a zeroed residency.

Retail anchor: CPhysicsObj::SetPositionInternal(CTransition const*)
0x00515330 commits both sphere_path.curr_pos.objcell_id and its frame,
including EnvCells.

WHERE THE DEFECT ACTUALLY BIT — #276's own framing is half wrong, and the
half it misses is the whole fix. PhysicsBody.Position's setter already
mirrors the world delta into the landblock-local frame and lets
LandDefs.AdjustToOutside recompute the 24 m cell index from it, so an
outdoor->outdoor settle already landed the right cell and dropping
settle.CellId cost nothing there. It cannot do that for an EnvCell: an
EnvCell id is not derivable from a position, so the mirror deliberately
PRESERVES it. settle.CellId is therefore the only carrier of a cell identity
across an indoor seam. The live defect is the issue's parenthetical
("outdoor/EnvCell seam, stacked EnvCells"), not its main clause — and the
change is consequently a no-op on the outdoor path that dominates
production, corrective only at the seam.

That finding is what made the test possible. The three existing settler
tests build bodies with NO CellPosition and pass identically with or without
this change — shipping against them would have repeated C5b finding D3, a
test that passed with its own change reverted. The new test seeds an EnvCell
id over plain outdoor terrain instead, so the stale-id preservation is the
discriminator and no EnvCell geometry fixture is needed.

Sabotage-verified: restoring `body.Position = settle.Position` fails exactly
the new test (1 failed / 4) and leaves the other three green — confirming
both that the new test discriminates and that the old ones never could.

Core suite 4,263 passed / 1 skipped / 0 failed, +1 for the new test.

Still open and unverified, deliberately not claimed closed: whether the
remote spawn-seed caller (LiveEntityNetworkUpdateController) hands in a body
that carries a CellPosition at all. CommitTransitionPosition early-returns on
a zero cell, so this fix is an inert no-op there and #276's remote half may
survive. The C3c local first-entry caller is confirmed — it passes
activation.Body.CellPosition.ObjCellId. Scoping detail in
docs/research/2026-08-06-276-remainder-scoping.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 06:37:46 +02:00

169 lines
5.8 KiB
C#

using System.Numerics;
using AcDream.Core.Physics;
namespace AcDream.Core.Tests.Physics;
/// <summary>
/// C3c-F5: moved from tests/AcDream.App.Tests/Physics/
/// RemoteSpawnPlacementSettlerTests.cs when the #270 settler moved to Core
/// (App -> <see cref="SpawnPlacementSettler"/>) so the local player's
/// Runtime first-entry activation can share it. Test bodies unchanged.
/// </summary>
public sealed class SpawnPlacementSettlerTests
{
private const uint Landblock = 0xA9B40000u;
private const uint Cell = Landblock | 0x0001u;
private const float Radius = 0.48f;
private const float Height = 1.835f;
[Fact]
public void FloorWithinFirstGravityFrame_CommitsGroundContactOnce()
{
PhysicsEngine engine = BuildFlatEngine();
PhysicsBody body = AirborneBody(new Vector3(12f, 12f, 0.25f));
int hitGround = 0;
int leaveGround = 0;
bool settled = SpawnPlacementSettler.TrySettle(
engine,
body,
body.Position,
Cell,
Radius,
Height,
ObjectInfoState.EdgeSlide,
movingEntityId: 0x70000001u,
() => hitGround++,
() => leaveGround++);
Assert.True(settled);
Assert.True(body.InContact);
Assert.True(body.OnWalkable);
Assert.Equal(1, hitGround);
Assert.Equal(0, leaveGround);
}
[Fact]
public void MissingCell_CanRetryAfterHydrationWithoutReconstructingBody()
{
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
PhysicsBody body = AirborneBody(new Vector3(12f, 12f, 0.25f));
Vector3 originalPosition = body.Position;
Assert.False(TrySettle(engine, body));
Assert.False(body.InContact);
Assert.Equal(originalPosition, body.Position);
AddFlatLandblock(engine);
Assert.True(TrySettle(engine, body));
Assert.True(body.InContact);
Assert.True(body.OnWalkable);
}
[Fact]
public void FloorOutsideSettleDistance_LeavesGenuinelyAirborneBodyUnchanged()
{
PhysicsEngine engine = BuildFlatEngine();
PhysicsBody body = AirborneBody(new Vector3(12f, 12f, 1.25f));
Vector3 originalPosition = body.Position;
Assert.False(TrySettle(engine, body));
Assert.False(body.InContact);
Assert.False(body.OnWalkable);
Assert.Equal(originalPosition, body.Position);
}
/// <summary>
/// #276: the settle must adopt the transition's RESOLVED cell, not only
/// its position. Retail's
/// <c>CPhysicsObj::SetPositionInternal(CTransition const*)</c>
/// (<c>0x00515330</c>) commits both <c>curr_pos.objcell_id</c> and the
/// frame.
///
/// <para>
/// The body is seeded with an ENVCELL id over plain outdoor terrain,
/// which is what makes this discriminating. A bare
/// <c>body.Position = settle.Position</c> mirrors the world delta into
/// the local frame and lets <c>AdjustToOutside</c> recompute the cell —
/// but only for outdoor ids; the mirror deliberately PRESERVES an
/// EnvCell id, because one cannot be derived from a position. So the old
/// code leaves the stale EnvCell in place and only the resolved-cell
/// adoption reaches the outdoor cell the body physically settled in.
/// This is the "outdoor/EnvCell seam" half of #276, in the outward
/// direction.
/// </para>
/// </summary>
[Fact]
public void SettleLeavingAnEnvCell_AdoptsTheTransitionsResolvedOutdoorCell()
{
const uint StaleEnvCell = Landblock | 0x0100u;
PhysicsEngine engine = BuildFlatEngine();
PhysicsBody body = AirborneBody(new Vector3(12f, 12f, 0.25f));
body.StageDormantCellFrame(
StaleEnvCell,
body.Position,
body.Position);
Assert.Equal(StaleEnvCell, body.CellPosition.ObjCellId);
bool settled = SpawnPlacementSettler.TrySettle(
engine,
body,
body.Position,
Cell,
Radius,
Height,
ObjectInfoState.EdgeSlide,
movingEntityId: 0x70000001u,
static () => { },
static () => { });
Assert.True(settled);
Assert.True(body.InContact);
// The discriminating assertion: the body no longer claims the EnvCell
// it never physically occupied, and its cell is an OUTDOOR landcell
// (low word 1..0x40) of the same landblock it settled in.
Assert.NotEqual(StaleEnvCell, body.CellPosition.ObjCellId);
Assert.Equal(Landblock, body.CellPosition.ObjCellId & 0xFFFF0000u);
Assert.InRange(body.CellPosition.ObjCellId & 0xFFFFu, 1u, 0x40u);
}
private static bool TrySettle(PhysicsEngine engine, PhysicsBody body) =>
SpawnPlacementSettler.TrySettle(
engine,
body,
body.Position,
Cell,
Radius,
Height,
ObjectInfoState.EdgeSlide,
movingEntityId: 0x70000001u,
static () => { },
static () => { });
private static PhysicsBody AirborneBody(Vector3 position) => new()
{
Position = position,
Orientation = Quaternion.Identity,
State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions,
TransientState = TransientStateFlags.None,
};
private static PhysicsEngine BuildFlatEngine()
{
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
AddFlatLandblock(engine);
return engine;
}
private static void AddFlatLandblock(PhysicsEngine engine) =>
engine.AddLandblock(
Landblock,
new TerrainSurface(new byte[81], new float[256]),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
worldOffsetX: 0f,
worldOffsetY: 0f);
}