using System.Collections.Immutable;
using System.Numerics;
using AcDream.Core.Physics;
using DatReaderWriter.Enums;
using DatReaderWriter.Types;
namespace AcDream.Core.Tests.Physics;
///
/// Named-retail oracle fixtures for CPhysicsObj::SetPosition 0x005160C0,
/// SetPositionInternal 0x00515BD0, AdjustPosition 0x00511D80, and
/// CTransition::find_placement_position 0x0050C170.
///
public sealed class PhysicsSetPositionTests
{
private const uint Landblock = 0xA9B40000u;
private const uint Cell = Landblock | 0x0001u;
[Fact]
public void SetPositionErrorValues_MatchRetailHeader()
{
Assert.Equal(0, (int)PhysicsSetPositionError.Ok);
Assert.Equal(1, (int)PhysicsSetPositionError.GeneralFailure);
Assert.Equal(2, (int)PhysicsSetPositionError.NoValidPosition);
Assert.Equal(3, (int)PhysicsSetPositionError.NoCell);
Assert.Equal(4, (int)PhysicsSetPositionError.Collided);
Assert.Equal(0x100, (int)PhysicsSetPositionError.InvalidArguments);
}
[Fact]
public void MissingOutdoorCell_AdjustsFrameThenDefersWithOk()
{
var engine = new PhysicsEngine();
var local = new Vector3(193f, 12f, 7f);
uint expectedCell = Cell;
Vector3 expectedLocal = local;
Assert.True(AcDream.Core.Physics.LandDefs.AdjustToOutside(
ref expectedCell,
ref expectedLocal));
PhysicsSetPositionResult result = engine.SetPosition(
Request(Cell, local, position: new Vector3(193f, 12f, 7f)));
Assert.Equal(PhysicsSetPositionError.Ok, result.Error);
Assert.Equal(
PhysicsResidenceDisposition.DeferredCell,
result.Residence);
Assert.Equal(expectedCell, result.CellId);
Assert.Equal(expectedLocal, result.CellLocalPosition);
Assert.Equal(new Vector3(193f, 12f, 7f), result.Position);
Assert.Equal(
new[] { Cell, expectedCell }.Distinct(),
result.QueriedCellIds);
}
[Fact]
public void ResidentCrossLandblockPlacement_RebasesLocalFrameAndPreservesWorldPosition()
{
PhysicsEngine engine = FlatEngine();
const uint destinationLandblock = 0xAAB40000u;
AddFlatLandblock(engine, destinationLandblock, worldOffsetX: 192f);
var world = new Vector3(193f, 12f, 7f);
PhysicsSetPositionResult result = engine.SetPosition(
Request(Cell, world, world) with
{
MoverPhysicsState = PhysicsStateFlags.Missile,
});
Assert.True(result.IsCommitted);
Assert.Equal(world, result.Position);
Assert.Equal(destinationLandblock | 0x0001u, result.CellId);
Assert.Equal(new Vector3(1f, 12f, 7f), result.CellLocalPosition);
}
[Fact]
public void OutdoorMapEdgeFailure_ZeroesCellAndDefersExactFrame()
{
var engine = new PhysicsEngine();
const uint southWestCell = 0x00000001u;
var local = new Vector3(-1f, 12f, 7f);
PhysicsSetPositionResult result = engine.SetPosition(
Request(southWestCell, local, local));
Assert.True(result.IsDeferred);
Assert.Equal(0u, result.CellId);
Assert.Equal(local, result.CellLocalPosition);
Assert.Equal(local, result.Position);
Assert.Equal(new[] { southWestCell, 0u }, result.QueriedCellIds);
}
[Fact]
public void QueryFootprintIncludesAdjustPositionVisibleChildProbes()
{
const uint start = Landblock | 0x0101u;
const uint sibling = Landblock | 0x0102u;
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
engine.DataCache.RegisterCellStructForTest(
start,
ContainmentCell(
new Plane(new Vector3(0f, -1f, 0f), 3f),
[sibling]));
engine.DataCache.RegisterCellStructForTest(
sibling,
ContainmentCell(
new Plane(new Vector3(0f, 1f, 0f), -7f),
[]));
PhysicsSetPositionResult result = engine.SetPosition(
Request(
start,
new Vector3(0f, 8f, 1f),
new Vector3(0f, 8f, 1f)) with
{
MoverPhysicsState = PhysicsStateFlags.Missile,
});
Assert.Contains(start, result.QueriedCellIds);
Assert.Contains(sibling, result.QueriedCellIds);
Assert.True(
result.QueriedCellIds.IndexOf(start)
< result.QueriedCellIds.IndexOf(sibling));
}
[Fact]
public void LateralVisibleChildRecoveryRecordsRejectedAndWinningSiblingsUnionOnly()
{
const uint start = Landblock | 0x0101u;
const uint rejected = Landblock | 0x0102u;
const uint winner = Landblock | 0x0103u;
var cache = new PhysicsDataCache();
cache.RegisterCellStructForTest(
start,
ContainmentCell(
new Plane(new Vector3(0f, -1f, 0f), 3f),
[rejected, winner]));
cache.RegisterCellStructForTest(
rejected,
ContainmentCell(
new Plane(new Vector3(0f, 1f, 0f), -20f),
[]));
cache.RegisterCellStructForTest(
winner,
ContainmentCell(
new Plane(new Vector3(0f, 1f, 0f), -7f),
[]));
var candidates = new CellArray();
var queryFootprint = new CellArray();
candidates.UnionTarget = queryFootprint;
var spheres = new[]
{
new Sphere
{
Origin = new Vector3(0f, 8f, 1f),
Radius = 0.48f,
},
};
uint containing = CellTransit.FindCellSet(
cache,
spheres,
spheres.Length,
start,
candidates);
Assert.Equal(winner, containing);
Assert.Equal(new[] { start }, candidates.OrderedIds);
Assert.Equal(
new[] { start, rejected, winner },
queryFootprint.OrderedIds);
}
[Fact]
public void RejectedPortalContainmentProbeIsRecordedUnionOnly()
{
const uint start = Landblock | 0x0101u;
const uint rejected = Landblock | 0x0102u;
const ushort portalPolygonId = 10;
var portalPolygon = new ResolvedPolygon
{
Id = portalPolygonId,
Vertices =
[
new Vector3(10f, -1f, 0f),
new Vector3(10f, 1f, 0f),
new Vector3(10f, 1f, 2f),
],
Plane = new Plane(Vector3.UnitX, -10f),
NumPoints = 3,
SidesType = CullMode.None,
};
var startCell = new CellPhysics
{
BSP = new PhysicsBSPTree
{
Root = new PhysicsBSPNode { Type = BSPNodeType.Leaf },
},
WorldTransform = Matrix4x4.Identity,
InverseWorldTransform = Matrix4x4.Identity,
Resolved = new Dictionary(),
CellBSP = new CellBSPTree
{
Root = new CellBSPNode { Type = BSPNodeType.Leaf },
},
Portals =
[
new PortalInfo(
(ushort)(rejected & 0xFFFFu),
portalPolygonId,
0),
],
PortalPolygons = new Dictionary
{
[portalPolygonId] = portalPolygon,
},
};
var cache = new PhysicsDataCache();
cache.RegisterCellStructForTest(start, startCell);
cache.RegisterCellStructForTest(
rejected,
ContainmentCell(
new Plane(Vector3.UnitX, -100f),
[]));
var candidates = new CellArray();
var queryFootprint = new CellArray();
candidates.UnionTarget = queryFootprint;
var spheres = new[]
{
new Sphere
{
Origin = Vector3.Zero,
Radius = 0.48f,
},
};
uint containing = CellTransit.FindCellSet(
cache,
spheres,
spheres.Length,
start,
candidates);
Assert.Equal(start, containing);
Assert.Equal(new[] { start }, candidates.OrderedIds);
Assert.Equal(new[] { start, rejected }, queryFootprint.OrderedIds);
}
[Fact]
public void RejectedBuildingContainmentProbeIsRecordedUnionOnly()
{
const uint rejected = Landblock | 0x0102u;
var building = new BuildingPhysics
{
WorldTransform = Matrix4x4.Identity,
InverseWorldTransform = Matrix4x4.Identity,
Portals =
[
new BldPortalInfo(
rejected,
otherPortalId: 0,
flags: 0),
],
};
var candidates = new CellArray();
var queryFootprint = new CellArray();
candidates.UnionTarget = queryFootprint;
CellTransit.CheckBuildingTransit(
new PhysicsDataCache(),
building,
Vector3.Zero,
sphereRadius: 0.48f,
candidates);
Assert.Empty(candidates);
Assert.Equal(new[] { rejected }, queryFootprint.OrderedIds);
}
[Fact]
public void OutdoorBlockSentinel_IsAnIndoorShapedLostCellSentinel()
{
var engine = new PhysicsEngine();
const uint sentinel = Landblock | 0xFFFFu;
var local = new Vector3(25f, 49f, 7f);
PhysicsSetPositionResult result = engine.SetPosition(
Request(sentinel, local, local));
Assert.True(result.IsDeferred);
Assert.Equal(sentinel, result.CellId);
Assert.Equal(local, result.CellLocalPosition);
}
[Fact]
public void InvalidLowCellId_ParksWithoutRewritingAuthoritativeFrame()
{
var engine = new PhysicsEngine();
const uint invalid = Landblock | 0x0050u;
var local = new Vector3(11f, 13f, 17f);
PhysicsSetPositionResult result = engine.SetPosition(
Request(invalid, local, local));
Assert.True(result.IsDeferred);
Assert.Equal(PhysicsSetPositionError.Ok, result.Error);
Assert.Equal(invalid, result.CellId);
Assert.Equal(local, result.CellLocalPosition);
}
[Fact]
public void MissingIndoorCell_ParksExactCellAndFrame()
{
var engine = new PhysicsEngine();
const uint indoor = Landblock | 0x0107u;
var local = new Vector3(31f, -4f, 9f);
PhysicsSetPositionResult result = engine.SetPosition(
Request(indoor, local, new Vector3(31f, -4f, 9f)));
Assert.True(result.IsDeferred);
Assert.Equal(indoor, result.CellId);
Assert.Equal(local, result.CellLocalPosition);
}
[Fact]
public void EmptySphereList_UsesRetailDummyAtScaleOne()
{
PhysicsEngine engine = FlatEngine();
Vector3 capturedOrigin = default;
float capturedRadius = 0f;
engine.TransitionCellCollisionTestHook =
(transition, phase, _, observed) =>
{
if (phase == TransitionCellCollisionPhase.Environment)
{
capturedOrigin = transition.SpherePath.LocalSphere[0].Origin;
capturedRadius = transition.SpherePath.LocalSphere[0].Radius;
}
return observed;
};
_ = engine.SetPosition(Request(
Cell,
new Vector3(10f, 10f, 10f),
new Vector3(10f, 10f, 10f),
scale: -7f));
Assert.Equal(new Vector3(0f, 0f, 0.1f), capturedOrigin);
Assert.Equal(0.1f, capturedRadius);
}
[Fact]
public void AuthoredSphereList_CapsAtTwoAndPreservesNonPositiveScale()
{
PhysicsEngine engine = FlatEngine();
int count = 0;
Vector3 firstOrigin = default;
float firstRadius = 0f;
Vector3 secondOrigin = default;
float secondRadius = 0f;
engine.TransitionCellCollisionTestHook =
(transition, phase, _, _) =>
{
if (phase == TransitionCellCollisionPhase.Environment)
{
count = transition.SpherePath.NumSphere;
firstOrigin = transition.SpherePath.LocalSphere[0].Origin;
firstRadius = transition.SpherePath.LocalSphere[0].Radius;
secondOrigin = transition.SpherePath.LocalSphere[1].Origin;
secondRadius = transition.SpherePath.LocalSphere[1].Radius;
}
return TransitionState.Collided;
};
ImmutableArray spheres = ImmutableArray.Create(
new FlatCollisionSphere(new Vector3(1f, 2f, 3f), 4f),
new FlatCollisionSphere(new Vector3(5f, 6f, 7f), 8f),
new FlatCollisionSphere(new Vector3(9f), 10f));
_ = engine.SetPosition(Request(
Cell,
new Vector3(10f, 10f, 10f),
new Vector3(10f, 10f, 10f),
spheres,
scale: -2f));
Assert.Equal(2, count);
Assert.Equal(new Vector3(-2f, -4f, -6f), firstOrigin);
Assert.Equal(-8f, firstRadius);
Assert.Equal(new Vector3(-10f, -12f, -14f), secondOrigin);
Assert.Equal(-16f, secondRadius);
}
[Theory]
[InlineData(false, true)]
[InlineData(true, false)]
public void StepDown_IsDisabledOnlyForMissiles(
bool missile,
bool expectedStepDown)
{
PhysicsEngine engine = FlatEngine();
bool? captured = null;
engine.TransitionCellCollisionTestHook =
(transition, phase, _, observed) =>
{
if (phase == TransitionCellCollisionPhase.Environment)
captured = transition.ObjectInfo.StepDown;
return observed;
};
PhysicsSetPositionRequest request = Request(
Cell,
new Vector3(10f, 10f, 10f),
new Vector3(10f, 10f, 10f)) with
{
MoverPhysicsState = missile
? PhysicsStateFlags.Missile
: PhysicsStateFlags.Gravity,
};
_ = engine.SetPosition(request);
Assert.Equal(expectedStepDown, captured);
}
[Theory]
[InlineData(false, false)]
[InlineData(true, true)]
public void ObjectInfoEthereal_IsSeededFromPhysicsState(
bool ethereal,
bool expected)
{
PhysicsEngine engine = FlatEngine();
bool? captured = null;
engine.TransitionCellCollisionTestHook =
(transition, phase, _, observed) =>
{
if (phase == TransitionCellCollisionPhase.Environment)
captured = transition.ObjectInfo.Ethereal;
return observed;
};
_ = engine.SetPosition(Request(
Cell,
new Vector3(10f, 10f, 10f),
new Vector3(10f, 10f, 10f)) with
{
MoverPhysicsState = ethereal
? PhysicsStateFlags.Ethereal
: PhysicsStateFlags.Gravity,
});
Assert.Equal(expected, captured);
}
[Theory]
[InlineData(false, false)]
[InlineData(true, true)]
public void PathClipped_ComesOnlyFromExplicitObjectInfoFlags(
bool explicitPathClipped,
bool expected)
{
PhysicsEngine engine = FlatEngine();
bool? captured = null;
engine.TransitionCellCollisionTestHook =
(transition, phase, _, observed) =>
{
if (phase == TransitionCellCollisionPhase.Environment)
captured = transition.ObjectInfo.PathClipped;
return observed;
};
_ = engine.SetPosition(Request(
Cell,
new Vector3(10f, 10f, 10f),
new Vector3(10f, 10f, 10f)) with
{
MoverPhysicsState = PhysicsStateFlags.Missile,
MoverFlags = explicitPathClipped
? ObjectInfoState.PathClipped
: ObjectInfoState.None,
});
Assert.Equal(expected, captured);
}
[Fact]
public void DoNotCreateCells_MissingDestinationStillDefersWithoutCreation()
{
var engine = new PhysicsEngine();
PhysicsSetPositionResult result = engine.SetPosition(Request(
Cell,
new Vector3(10f, 10f, 10f),
new Vector3(10f, 10f, 10f)) with
{
Flags = PhysicsSetPositionFlags.Placement
| PhysicsSetPositionFlags.Slide
| PhysicsSetPositionFlags.DoNotCreateCells,
});
Assert.True(result.IsDeferred);
Assert.Equal(0, engine.LandblockCount);
}
[Fact]
public void SuccessfulPlacement_RetainsRequestedOrientation()
{
PhysicsEngine engine = FlatEngine();
PhysicsSetPositionRequest request = Request(
Cell,
new Vector3(10f, 10f, 10f),
new Vector3(10f, 10f, 10f));
PhysicsSetPositionResult result = engine.SetPosition(request);
Assert.True(result.IsCommitted);
Assert.Equal(request.Orientation, result.Orientation);
}
[Fact]
public void OuterPlacementStartsWithInitialPlacementInsert()
{
PhysicsEngine engine = FlatEngine();
InsertType? captured = null;
engine.TransitionCellCollisionTestHook =
(transition, phase, _, observed) =>
{
if (phase == TransitionCellCollisionPhase.Environment
&& captured is null)
{
captured = transition.SpherePath.InsertType;
}
return observed;
};
_ = engine.SetPosition(Request(
Cell,
new Vector3(10f, 10f, 10f),
new Vector3(10f, 10f, 10f)));
Assert.Equal(InsertType.InitialPlacement, captured);
}
[Theory]
[InlineData(1, 0.5f, 1.0f, 0.25f, 1)]
[InlineData(2, 0.5f, 1.0f, 0.5f, 2)]
[InlineData(1, 0.5f, 0.4f, 0.4f, 1)]
public void StepDownProbePlan_PreservesRetailEqualityBoundaries(
int sphereCount,
float radius,
float requested,
float expectedHeight,
int expectedCount)
{
Assert.Equal(
(expectedHeight, expectedCount),
Transition.GetStepDownProbePlan(
sphereCount,
radius,
requested));
}
[Theory]
[InlineData(-100f, 0f, 7f, 7u, true)]
[InlineData(0.0500000007f, 0.0500000007f, 500f, 7u, true)]
[InlineData(0.0501f, 0f, 0f, 7u, false)]
[InlineData(0f, 0.0501f, 0f, 7u, false)]
[InlineData(0f, 0f, 0f, 8u, false)]
public void NoSlideAcceptance_IsSignedXYSameCellAndIgnoresZ(
float dx,
float dy,
float dz,
uint resolvedCell,
bool expected)
{
Assert.Equal(
expected,
PhysicsEngine.AcceptNoSlidePlacement(
new Vector3(dx, dy, dz),
Vector3.Zero,
resolvedCell,
adjustedCellId: 7u));
}
[Fact]
public void PlacementCompass_LateSampleMatchesRetailFloatBits()
{
PhysicsEngine engine = FlatEngine();
var transition = new Transition();
transition.SpherePath.InitPath(
Vector3.Zero,
Vector3.Zero,
Cell,
sphereRadius: 0.48f);
transition.SpherePath.InsertType = InsertType.Placement;
transition.SpherePath.PlacementAllowsSliding = true;
Vector3 lastCheck = default;
engine.TransitionCellCollisionTestHook =
(observed, phase, _, _) =>
{
if (phase == TransitionCellCollisionPhase.Environment)
lastCheck = observed.SpherePath.CheckPos;
return TransitionState.Collided;
};
Assert.False(transition.FindPlacementPos(engine));
Assert.Equal(unchecked((int)0xBEEDC24F),
BitConverter.SingleToInt32Bits(lastCheck.X));
Assert.Equal(unchecked((int)0x407E44DE),
BitConverter.SingleToInt32Bits(lastCheck.Y));
}
[Theory]
[InlineData(TransitionState.Collided)]
[InlineData(TransitionState.Adjusted)]
[InlineData(TransitionState.Slid)]
public void InnerPlacementValidator_ResetsCollisionInfoForEveryFailure(
TransitionState state)
{
Transition transition = PlacementTransition();
transition.SpherePath.PlacementAllowsSliding = true;
transition.CollisionInfo.SetContactPlane(
new Plane(Vector3.UnitZ, 0f),
Cell);
transition.CollisionInfo.SetSlidingNormal(Vector3.UnitX);
TransitionState result =
transition.ValidatePlacementTransitionForTest(state);
Assert.Equal(state, result);
Assert.False(transition.CollisionInfo.ContactPlaneValid);
Assert.False(transition.CollisionInfo.SlidingNormalValid);
}
[Fact]
public void InnerPlacementValidator_DoesNotResetWhenSlidingDisabled()
{
Transition transition = PlacementTransition();
transition.SpherePath.PlacementAllowsSliding = false;
transition.CollisionInfo.SetContactPlane(
new Plane(Vector3.UnitZ, 0f),
Cell);
_ = transition.ValidatePlacementTransitionForTest(
TransitionState.Collided);
Assert.True(transition.CollisionInfo.ContactPlaneValid);
}
[Fact]
public void OuterPlacementValidator_CollidedNeverRetriesOrResets()
{
PhysicsEngine engine = FlatEngine();
int passes = 0;
engine.TransitionCellCollisionTestHook =
(_, _, _, observed) =>
{
passes++;
return observed;
};
Transition transition = PlacementTransition();
transition.CollisionInfo.SetContactPlane(
new Plane(Vector3.UnitZ, 0f),
Cell);
TransitionState result = transition.ValidatePlacementForTest(
engine,
TransitionState.Collided,
retryPlacement: true);
Assert.Equal(TransitionState.Collided, result);
Assert.Equal(0, passes);
Assert.True(transition.CollisionInfo.ContactPlaneValid);
}
[Theory]
[InlineData(TransitionState.Adjusted)]
[InlineData(TransitionState.Slid)]
public void OuterPlacementValidator_AdjustedAndSlidRetryExactlyOnce(
TransitionState state)
{
PhysicsEngine engine = FlatEngine();
int environmentPasses = 0;
engine.TransitionCellCollisionTestHook =
(_, phase, _, _) =>
{
if (phase == TransitionCellCollisionPhase.Environment)
environmentPasses++;
return TransitionState.OK;
};
Transition transition = PlacementTransition();
TransitionState result = transition.ValidatePlacementForTest(
engine,
state,
retryPlacement: true);
Assert.Equal(TransitionState.OK, result);
Assert.Equal(1, environmentPasses);
}
[Theory]
[InlineData(TransitionState.Adjusted)]
[InlineData(TransitionState.Slid)]
public void OuterPlacementValidator_RetryFalseReturnsOriginalState(
TransitionState state)
{
PhysicsEngine engine = FlatEngine();
int passes = 0;
engine.TransitionCellCollisionTestHook =
(_, _, _, observed) =>
{
passes++;
return observed;
};
Transition transition = PlacementTransition();
TransitionState result = transition.ValidatePlacementForTest(
engine,
state,
retryPlacement: false);
Assert.Equal(state, result);
Assert.Equal(0, passes);
}
[Theory]
[InlineData(false, (int)PhysicsSetPositionError.NoValidPosition)]
[InlineData(true, (int)PhysicsSetPositionError.Collided)]
public void FailedCheck_MapsCollisionHandlerResultToRetailError(
bool handled,
int expectedValue)
{
var expected = (PhysicsSetPositionError)expectedValue;
PhysicsEngine engine = FlatEngine();
engine.TransitionCellCollisionTestHook =
(transition, phase, _, _) =>
{
if (phase == TransitionCellCollisionPhase.Environment)
{
transition.CollisionInfo.CollidedWithEnvironment = true;
// #32 (2026-08-07): InitContactPlane, not SetContactPlane.
// This test asserts that the REPORT carries every field,
// including the last-known group — it is not a claim about
// which setter latches what. It used to pass because
// SetContactPlane seeded last-known as a side effect; that
// side effect was the #32 defect and is now deleted, so the
// fixture populates both groups explicitly instead of
// depending on it. Retail's own start-of-transition seed
// (CTransition::init_contact_plane @0x0050e850) is exactly
// this call.
transition.CollisionInfo.InitContactPlane(
new Plane(Vector3.UnitZ, -3f),
Cell,
isWater: true);
transition.CollisionInfo.SetSlidingNormal(Vector3.UnitX);
transition.CollisionInfo.SetCollisionNormal(-Vector3.UnitY);
transition.CollisionInfo.FramesStationaryFall = 2;
transition.CollisionInfo.AdjustOffset = new Vector3(1f, 2f, 3f);
transition.CollisionInfo.CollideObjectGuids.Add(0x12345678u);
transition.CollisionInfo.LastCollidedObjectGuid = 0x12345678u;
}
return TransitionState.Collided;
};
PhysicsSetPositionCollisionReport observedReport = default;
PhysicsSetPositionResult result = engine.SetPosition(Request(
Cell,
new Vector3(10f, 10f, 10f),
new Vector3(10f, 10f, 10f)), report =>
{
observedReport = report;
return handled;
});
Assert.Equal(expected, result.Error);
Assert.Equal(handled, result.CollisionHandlerResult);
Assert.True(observedReport.CollidedWithEnvironment);
Assert.True(observedReport.ContactPlaneValid);
Assert.Equal(new Plane(Vector3.UnitZ, -3f), observedReport.ContactPlane);
Assert.Equal(Cell, observedReport.ContactPlaneCellId);
Assert.True(observedReport.ContactPlaneIsWater);
Assert.True(observedReport.LastKnownContactPlaneValid);
Assert.Equal(
observedReport.ContactPlane,
observedReport.LastKnownContactPlane);
Assert.Equal(Cell, observedReport.LastKnownContactPlaneCellId);
Assert.True(observedReport.LastKnownContactPlaneIsWater);
Assert.True(observedReport.SlidingNormalValid);
Assert.Equal(Vector3.UnitX, observedReport.SlidingNormal);
Assert.True(observedReport.CollisionNormalValid);
Assert.Equal(-Vector3.UnitY, observedReport.CollisionNormal);
Assert.Equal(2, observedReport.FramesStationaryFall);
Assert.Equal(new Vector3(1f, 2f, 3f), observedReport.AdjustOffset);
Assert.Equal(0x12345678u, observedReport.LastCollidedObjectId);
Assert.Equal(
new[] { 0x12345678u },
observedReport.CollidedObjectIds.ToArray());
Assert.True(result.CollidedWithEnvironment);
Assert.True(result.InContact);
Assert.True(result.OnWalkable);
Assert.True(result.ContactPlaneIsWater);
Assert.True(result.SlidingNormalValid);
Assert.True(result.CollisionNormalValid);
Assert.Equal(2, result.FramesStationaryFall);
Assert.Equal(
new[] { 0x12345678u },
result.CollidedObjectIds.ToArray());
Assert.Equal(PhysicsResidenceDisposition.Unchanged, result.Residence);
}
[Theory]
[InlineData((int)PhysicsPlacementClass.Hook)]
[InlineData((int)PhysicsPlacementClass.Storage)]
[InlineData((int)PhysicsPlacementClass.Corpse)]
public void RetailForceClasses_BypassPlacementCollision(
int placementClassValue)
{
var placementClass = (PhysicsPlacementClass)placementClassValue;
PhysicsEngine engine = FlatEngine();
int collisionPasses = 0;
engine.TransitionCellCollisionTestHook =
(_, _, _, observed) =>
{
collisionPasses++;
return observed;
};
PhysicsSetPositionResult result = engine.SetPosition(
Request(
Cell,
new Vector3(10f, 10f, 10f),
new Vector3(10f, 10f, 10f)) with
{
PlacementClass = placementClass,
});
Assert.True(result.IsCommitted);
Assert.Equal(0, collisionPasses);
Assert.True(result.CellChanged);
Assert.Equal(
PhysicsShadowCommitAction.Recalculate,
result.ShadowAction);
Assert.Empty(result.CrossCellIds);
}
[Fact]
public void OrdinaryObject_CannotRequestForceByFlags()
{
PhysicsEngine engine = FlatEngine();
int collisionPasses = 0;
engine.TransitionCellCollisionTestHook =
(_, _, _, observed) =>
{
collisionPasses++;
return observed;
};
PhysicsSetPositionResult result = engine.SetPosition(
Request(
Cell,
new Vector3(10f, 10f, 10f),
new Vector3(10f, 10f, 10f)) with
{
PlacementClass = PhysicsPlacementClass.Ordinary,
Flags = (PhysicsSetPositionFlags)uint.MaxValue
& ~PhysicsSetPositionFlags.RandomScatter
& ~PhysicsSetPositionFlags.Scatter,
});
Assert.True(result.IsCommitted);
Assert.True(collisionPasses > 0);
}
[Fact]
public void ForceIntoSameCell_SetsFrameWithoutRecalculatingCrossCells()
{
PhysicsEngine engine = FlatEngine();
PhysicsSetPositionResult result = engine.SetPosition(
Request(
Cell,
new Vector3(10f, 10f, 10f),
new Vector3(10f, 10f, 10f)) with
{
PlacementClass = PhysicsPlacementClass.Corpse,
CurrentCellId = Cell,
});
Assert.True(result.IsCommitted);
Assert.False(result.CellChanged);
Assert.Equal(PhysicsShadowCommitAction.None, result.ShadowAction);
Assert.Empty(result.CrossCellIds);
}
[Fact]
public void ForceIntoChangedCell_RequestsCanonicalShadowRecalculation()
{
PhysicsEngine engine = FlatEngine();
var position = new Vector3(23.8f, 12f, 2.5f);
ImmutableArray spheres = ImmutableArray.Create(
new FlatCollisionSphere(Vector3.Zero, 0.5f));
PhysicsSetPositionResult result = engine.SetPosition(
Request(Cell, position, position, spheres) with
{
PlacementClass = PhysicsPlacementClass.Hook,
CurrentCellId = Landblock | 0x0040u,
});
Assert.True(result.IsCommitted);
Assert.True(result.CellChanged);
Assert.Equal(
PhysicsShadowCommitAction.Recalculate,
result.ShadowAction);
Assert.Empty(result.CrossCellIds);
}
[Fact]
public void ForceIntoRetainedCellWithNullCurrentPointer_RefloodsShadows()
{
PhysicsEngine engine = FlatEngine();
PhysicsSetPositionResult result = engine.SetPosition(
Request(
Cell,
new Vector3(10f, 10f, 10f),
new Vector3(10f, 10f, 10f)) with
{
PlacementClass = PhysicsPlacementClass.Corpse,
CurrentCellId = null,
});
Assert.True(result.IsCommitted);
Assert.True(result.CellChanged);
Assert.Equal(
PhysicsShadowCommitAction.Recalculate,
result.ShadowAction);
}
[Fact]
public void OrdinaryPhysicsBspPlacement_RequestsCanonicalShadowRecalculation()
{
PhysicsEngine engine = FlatEngine();
PhysicsSetPositionResult result = engine.SetPosition(
Request(
Cell,
new Vector3(10f, 10f, 10f),
new Vector3(10f, 10f, 10f)) with
{
MoverPhysicsState = PhysicsStateFlags.HasPhysicsBsp
| PhysicsStateFlags.Missile,
});
Assert.True(result.IsCommitted);
Assert.Equal(
PhysicsShadowCommitAction.Recalculate,
result.ShadowAction);
Assert.Empty(result.CrossCellIds);
}
[Fact]
public void OrdinarySpherePlacement_ReplacesShadowsWithTransitionCells()
{
PhysicsEngine engine = FlatEngine();
PhysicsSetPositionResult result = engine.SetPosition(
Request(
Cell,
new Vector3(10f, 10f, 10f),
new Vector3(10f, 10f, 10f)) with
{
MoverPhysicsState = PhysicsStateFlags.Missile,
});
Assert.True(result.IsCommitted);
Assert.Equal(PhysicsShadowCommitAction.Replace, result.ShadowAction);
Assert.Contains(Cell, result.CrossCellIds);
}
[Fact]
public void OrdinarySpherePlacementWithNoTransitionCells_PreservesShadows()
{
// The Core-only/no-cell-graph fixture is the real empty-array shape:
// terrain is resident, but async cell topology has not been published.
PhysicsEngine engine = FlatEngine(withDataCache: false);
PhysicsSetPositionResult result = engine.SetPosition(
Request(
Cell,
new Vector3(10f, 10f, 10f),
new Vector3(10f, 10f, 10f)) with
{
MoverPhysicsState = PhysicsStateFlags.Missile,
});
Assert.True(result.IsCommitted);
Assert.Equal(PhysicsShadowCommitAction.Preserve, result.ShadowAction);
Assert.Empty(result.CrossCellIds);
}
[Fact]
public void RandomScatter_IsDirectAndStopsOnDeferredOk()
{
var engine = new PhysicsEngine();
var random = new Queue(new[] { 1d, 0d, 0.25d, 0.75d });
int draws = 0;
engine.SetPositionRandomUnit = () =>
{
draws++;
return random.Dequeue();
};
PhysicsSetPositionRequest request = Request(
Cell,
new Vector3(10f, 10f, 3f),
new Vector3(10f, 10f, 3f)) with
{
Flags = PhysicsSetPositionFlags.RandomScatter,
ScatterRadiusX = 4f,
ScatterRadiusY = 2f,
ScatterAttempts = 2u,
};
PhysicsSetPositionResult result = engine.SetPosition(request);
Assert.True(result.IsDeferred);
Assert.Equal(2, draws);
Assert.Equal(new Vector3(14f, 8f, 3f), result.Position);
}
[Fact]
public void ScatterFallback_RunsOnlyAfterNormalError_AndUsesExactAttempts()
{
PhysicsEngine engine = FlatEngine();
int collisionPasses = 0;
engine.TransitionCellCollisionTestHook =
(transition, phase, _, _) =>
{
if (phase == TransitionCellCollisionPhase.Environment)
{
collisionPasses++;
transition.CollisionInfo.CollidedWithEnvironment = true;
}
return TransitionState.Collided;
};
var random = new Queue(new[] { 0d, 0d, 1d, 1d });
int draws = 0;
engine.SetPositionRandomUnit = () =>
{
draws++;
return random.Dequeue();
};
PhysicsSetPositionRequest request = Request(
Cell,
new Vector3(10f, 10f, 10f),
new Vector3(10f, 10f, 10f)) with
{
Flags = PhysicsSetPositionFlags.Placement
| PhysicsSetPositionFlags.Scatter,
ScatterRadiusX = 1f,
ScatterRadiusY = 1f,
ScatterAttempts = 2u,
};
PhysicsSetPositionResult result = engine.SetPosition(request);
Assert.Equal(PhysicsSetPositionError.NoValidPosition, result.Error);
Assert.Equal(3, collisionPasses);
Assert.Equal(4, draws);
Assert.Equal(new Vector3(11f, 11f, 10f), result.Position);
}
[Fact]
public void NormalThenScatterUnionsEveryLandblockProbeInStableOrder()
{
PhysicsEngine engine = FlatEngine();
const uint eastLandblock = 0xAAB40000u;
AddFlatLandblock(engine, eastLandblock, worldOffsetX: 192f);
int pass = 0;
engine.TransitionCellCollisionTestHook =
(_, phase, _, observed) => phase
is TransitionCellCollisionPhase.Environment
&& pass++ == 0
? TransitionState.Collided
: observed;
var random = new Queue([1d, 0.5d]);
engine.SetPositionRandomUnit = random.Dequeue;
PhysicsSetPositionResult result = engine.SetPosition(
Request(
Cell,
new Vector3(180f, 12f, 7f),
new Vector3(180f, 12f, 7f)) with
{
MoverPhysicsState = PhysicsStateFlags.Missile,
Flags = PhysicsSetPositionFlags.Placement
| PhysicsSetPositionFlags.Scatter,
ScatterRadiusX = 20f,
ScatterAttempts = 1u,
});
Assert.True(result.IsCommitted);
int source = IndexOfLandblock(result.QueriedCellIds, Landblock);
int east = IndexOfLandblock(
result.QueriedCellIds,
eastLandblock);
Assert.True(source >= 0);
Assert.True(east > source);
}
[Fact]
public void MultiScatterUnionsFailedAndSuccessfulAttemptLandblocks()
{
PhysicsEngine engine = FlatEngine();
const uint westLandblock = 0xA8B40000u;
const uint eastLandblock = 0xAAB40000u;
AddFlatLandblock(engine, westLandblock, worldOffsetX: -192f);
AddFlatLandblock(engine, eastLandblock, worldOffsetX: 192f);
int pass = 0;
engine.TransitionCellCollisionTestHook =
(_, phase, _, observed) => phase
is TransitionCellCollisionPhase.Environment
&& pass++ == 0
? TransitionState.Collided
: observed;
var random = new Queue([0d, 0.5d, 1d, 0.5d]);
engine.SetPositionRandomUnit = random.Dequeue;
PhysicsSetPositionResult result = engine.SetPosition(
Request(
Cell,
new Vector3(10f, 12f, 7f),
new Vector3(10f, 12f, 7f)) with
{
MoverPhysicsState = PhysicsStateFlags.Missile,
Flags = PhysicsSetPositionFlags.RandomScatter,
ScatterRadiusX = 200f,
ScatterAttempts = 2u,
});
Assert.True(result.IsCommitted);
int west = IndexOfLandblock(
result.QueriedCellIds,
westLandblock);
int east = IndexOfLandblock(
result.QueriedCellIds,
eastLandblock);
Assert.True(west >= 0);
Assert.True(east > west);
Assert.DoesNotContain(
result.CrossCellIds,
cell => (cell & 0xFFFF0000u) == westLandblock);
Assert.Contains(
result.CrossCellIds,
cell => (cell & 0xFFFF0000u) == eastLandblock);
}
[Fact]
public void Scatter_ReusesOneTransitionAndFailedInnerProbeCannotLeakIntoSuccess()
{
PhysicsEngine engine = FlatEngine();
int placementPasses = 0;
bool injectedFailure = false;
engine.TransitionCellCollisionTestHook =
(transition, phase, _, observed) =>
{
if (phase != TransitionCellCollisionPhase.Environment)
return observed;
if (transition.SpherePath.InsertType == InsertType.Placement)
{
placementPasses++;
// Attempt 1 reaches the inner placement probe and fails.
// The exact inner validator clears this collision scratch.
if (!injectedFailure)
{
injectedFailure = true;
transition.CollisionInfo.CollidedWithEnvironment = true;
transition.CollisionInfo.SetContactPlane(
new Plane(Vector3.UnitZ, 0f),
Cell);
transition.CollisionInfo.SetSlidingNormal(Vector3.UnitX);
return TransitionState.Collided;
}
}
return TransitionState.OK;
};
engine.SetPositionRandomUnit = () => 0.5d;
PhysicsSetPositionResult result = engine.SetPosition(
Request(
Cell,
new Vector3(10f, 10f, 10f),
new Vector3(10f, 10f, 10f)) with
{
Flags = PhysicsSetPositionFlags.RandomScatter
| PhysicsSetPositionFlags.Slide,
ScatterAttempts = 2u,
MoverPhysicsState = PhysicsStateFlags.Missile,
});
Assert.True(result.IsCommitted);
Assert.True(injectedFailure);
Assert.True(placementPasses >= 2);
Assert.False(result.InContact);
Assert.False(result.SlidingNormalValid);
Assert.False(result.CollidedWithEnvironment);
Assert.Empty(result.CollidedObjectIds);
}
[Fact]
public void EleventhNestedSetPosition_ReturnsGeneralFailureWithoutThrowing()
{
PhysicsEngine engine = FlatEngine();
PhysicsSetPositionRequest request = Request(
Cell,
new Vector3(10f, 10f, 10f),
new Vector3(10f, 10f, 10f));
PhysicsSetPositionResult capacityResult = default;
int callbackDepth = 0;
engine.TransitionCellCollisionTestHook =
(_, phase, _, observed) =>
{
if (phase != TransitionCellCollisionPhase.Environment)
return observed;
callbackDepth++;
PhysicsSetPositionResult nested = engine.SetPosition(request);
if (nested.Error == PhysicsSetPositionError.GeneralFailure)
capacityResult = nested;
return TransitionState.Collided;
};
PhysicsSetPositionResult outer = engine.SetPosition(request);
Assert.Equal(PhysicsSetPositionError.NoValidPosition, outer.Error);
Assert.Equal(10, callbackDepth);
Assert.Equal(
PhysicsSetPositionError.GeneralFailure,
capacityResult.Error);
Assert.Equal(
PhysicsResidenceDisposition.Unchanged,
capacityResult.Residence);
}
[Fact]
public void ZeroScatterAttempts_ReturnsGeneralFailureWithoutRandomDraw()
{
var engine = new PhysicsEngine();
int draws = 0;
engine.SetPositionRandomUnit = () =>
{
draws++;
return 0.5d;
};
PhysicsSetPositionResult result = engine.SetPosition(
Request(Cell, Vector3.One, Vector3.One) with
{
Flags = PhysicsSetPositionFlags.RandomScatter,
ScatterAttempts = 0u,
});
Assert.Equal(
PhysicsSetPositionError.GeneralFailure,
result.Error);
Assert.Equal(PhysicsResidenceDisposition.Unchanged, result.Residence);
Assert.Equal(0, draws);
}
private static PhysicsSetPositionRequest Request(
uint cellId,
Vector3 cellLocal,
Vector3 position,
ImmutableArray spheres = default,
float scale = 1f) => new(
position,
Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.75f),
cellId,
cellLocal,
spheres,
scale,
StepUpHeight: 0.4f,
StepDownHeight: 0.4f,
Flags: PhysicsSetPositionFlags.Placement
| PhysicsSetPositionFlags.Slide);
private static Transition PlacementTransition()
{
var transition = new Transition();
transition.SpherePath.InitPath(
new Vector3(10f, 10f, 10f),
new Vector3(10f, 10f, 10f),
Cell,
sphereRadius: 0.48f);
transition.SpherePath.InsertType = InsertType.Placement;
return transition;
}
private static PhysicsEngine FlatEngine(bool withDataCache = true)
{
var engine = new PhysicsEngine();
if (withDataCache)
engine.DataCache = new PhysicsDataCache();
engine.AddLandblock(
Landblock,
new TerrainSurface(new byte[81], new float[256]),
Array.Empty(),
Array.Empty(),
worldOffsetX: 0f,
worldOffsetY: 0f);
return engine;
}
private static void AddFlatLandblock(
PhysicsEngine engine,
uint landblock,
float worldOffsetX = 0f,
float worldOffsetY = 0f)
{
engine.AddLandblock(
landblock,
new TerrainSurface(new byte[81], new float[256]),
Array.Empty(),
Array.Empty(),
worldOffsetX,
worldOffsetY);
}
private static CellPhysics ContainmentCell(
Plane plane,
uint[] visibleCells) => new()
{
BSP = new PhysicsBSPTree
{
Root = new PhysicsBSPNode { Type = BSPNodeType.Leaf },
},
WorldTransform = Matrix4x4.Identity,
InverseWorldTransform = Matrix4x4.Identity,
Resolved = new Dictionary(),
CellBSP = new CellBSPTree
{
Root = new CellBSPNode
{
SplittingPlane = plane,
PosNode = new CellBSPNode { Type = BSPNodeType.Leaf },
},
},
Portals = [new PortalInfo(0xFFFF, 0, 0)],
PortalPolygons = new Dictionary(),
VisibleCellIds = new HashSet(visibleCells),
};
private static int IndexOfLandblock(
ImmutableArray cells,
uint landblock)
{
for (int index = 0; index < cells.Length; index++)
{
if ((cells[index] & 0xFFFF0000u)
== (landblock & 0xFFFF0000u))
{
return index;
}
}
return -1;
}
}