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>
169 lines
5.8 KiB
C#
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);
|
|
}
|