feat(physics): port canonical retail set-position core

This commit is contained in:
Erik 2026-07-31 20:44:03 +02:00
parent 6b28ff999c
commit e84a388e6f
6 changed files with 1952 additions and 21 deletions

View file

@ -89,6 +89,14 @@ public sealed class PhysicsEngine
TransitionState,
TransitionState>? TransitionCellCollisionTestHook { get; set; }
/// <summary>
/// Deterministic seam for retail <c>Random::RollDice(-1, 1)</c> used by
/// SetPosition scatter. Values are expected in [0,1); production uses the
/// process RNG and tests inject a fixed sequence.
/// </summary>
internal Func<double> SetPositionRandomUnit { get; set; } =
Random.Shared.NextDouble;
/// <summary>
/// True once the landblock covering <paramref name="cellOrLandblockId"/> has had its
/// terrain + cells registered via <see cref="AddLandblock"/>. Accepts a canonical
@ -1530,6 +1538,391 @@ public sealed class PhysicsEngine
/// green).
/// </para>
/// </summary>
private readonly record struct AdjustedSetPosition(
uint CellId,
Vector3 CellLocalPosition,
bool Resident);
/// <summary>
/// SetPosition's exact AdjustPosition input/output shape. Unlike the
/// camera helper above, this carries retail's block-local frame and can
/// therefore run outdoor <c>LandDefs::adjust_to_outside</c> without
/// consulting the resident-landblock registry. A valid adjusted id with
/// no visible cell is retained for the lost-cell path.
/// </summary>
private AdjustedSetPosition AdjustSetPosition(
uint seedCellId,
Vector3 cellLocalPosition,
Vector3 firstWorldSphereCenter)
{
uint low = seedCellId & 0xFFFFu;
bool lowInRange = low is (>= 1u and <= 0x40u)
or (>= 0x0100u and <= 0xFFFDu)
or 0xFFFFu;
if (!lowInRange)
return new AdjustedSetPosition(
seedCellId,
cellLocalPosition,
Resident: false);
uint adjustedCell = seedCellId;
Vector3 adjustedLocal = cellLocalPosition;
if (low >= 0x0100u)
{
PhysicsDataCache? cache = DataCache;
if (cache is null || cache.GetCellStruct(seedCellId) is null)
{
return new AdjustedSetPosition(
seedCellId,
cellLocalPosition,
Resident: false);
}
uint child = CellTransit.FindVisibleChildCell(
cache,
seedCellId,
firstWorldSphereCenter,
useStabList: true);
if (child != 0u)
{
return new AdjustedSetPosition(
child,
adjustedLocal,
Resident: cache.GetCellStruct(child) is not null);
}
CellPhysics? claimed = cache.GetCellStruct(seedCellId);
if (claimed is null || !claimed.SeenOutside)
{
return new AdjustedSetPosition(
seedCellId,
adjustedLocal,
Resident: false);
}
}
// Outdoor adjustment is pure cell-relative LandDefs math. Residency
// is observed only after the id/frame have been mutated.
bool adjusted = LandDefs.AdjustToOutside(
ref adjustedCell,
ref adjustedLocal);
bool resident = adjusted
&& IsLandblockTerrainResident(adjustedCell);
return new AdjustedSetPosition(
adjustedCell,
adjustedLocal,
resident);
}
/// Canonical retail placement transaction:
/// <c>CPhysicsObj::SetPosition</c> (0x005160C0) -&gt;
/// <c>SetPositionInternal</c> (0x00515BD0) -&gt;
/// <c>AdjustPosition</c> (0x00511D80) -&gt;
/// <c>CheckPositionInternal</c> (0x00511E90) -&gt;
/// <c>CTransition::find_valid_position</c> (0x0050C310).
/// A destination whose cell is not resident returns DeferredCell; callers
/// must retain the authoritative frame rather than demoting it outdoors.
/// </summary>
internal PhysicsSetPositionResult SetPosition(
in PhysicsSetPositionRequest request,
Func<PhysicsSetPositionCollisionReport, bool>? handleCollisions = null)
{
if (_transitionScratch?.ActiveDepth >= TransitionScratchArena.Capacity)
{
return ErrorResult(
request,
PhysicsSetPositionError.GeneralFailure);
}
Transition transition = RentTransition();
try
{
InitializeSetPositionTransition(transition, request);
bool randomOnly = request.Flags.HasFlag(
PhysicsSetPositionFlags.RandomScatter);
if (randomOnly)
{
return SetScatterPositionInternal(
transition,
request,
handleCollisions);
}
PhysicsSetPositionResult result =
SetPositionInternal(transition, request, handleCollisions);
if (result.Error != PhysicsSetPositionError.Ok
&& request.Flags.HasFlag(PhysicsSetPositionFlags.Scatter))
{
return SetScatterPositionInternal(
transition,
request,
handleCollisions);
}
return result;
}
finally
{
ReturnTransition(transition);
}
}
private void InitializeSetPositionTransition(
Transition transition,
in PhysicsSetPositionRequest request)
{
transition.ObjectInfo.StepUpHeight = request.StepUpHeight;
transition.ObjectInfo.StepDownHeight = request.StepDownHeight;
transition.ObjectInfo.StepDown =
!request.MoverPhysicsState.HasFlag(PhysicsStateFlags.Missile);
transition.ObjectInfo.MoverPhysicsState = request.MoverPhysicsState;
transition.ObjectInfo.SelfEntityId = request.MovingEntityId;
transition.ObjectInfo.State = request.MoverFlags;
transition.ObjectInfo.Ethereal = request.MoverPhysicsState.HasFlag(
PhysicsStateFlags.Ethereal);
transition.SpherePath.PlacementAllowsSliding =
request.Flags.HasFlag(PhysicsSetPositionFlags.Slide);
}
private PhysicsSetPositionResult SetScatterPositionInternal(
Transition transition,
in PhysicsSetPositionRequest request,
Func<PhysicsSetPositionCollisionReport, bool>? handleCollisions)
{
PhysicsSetPositionResult result = ErrorResult(
request,
PhysicsSetPositionError.GeneralFailure);
for (uint attempt = 0u; attempt < request.ScatterAttempts; attempt++)
{
float dx = ((float)((SetPositionRandomUnit() * 2d) - 1d))
* request.ScatterRadiusX;
float dy = ((float)((SetPositionRandomUnit() * 2d) - 1d))
* request.ScatterRadiusY;
var scattered = request with
{
Position = request.Position + new Vector3(dx, dy, 0f),
CellLocalPosition = request.CellLocalPosition
+ new Vector3(dx, dy, 0f),
};
result = SetPositionInternal(
transition,
scattered,
handleCollisions);
if (result.Error == PhysicsSetPositionError.Ok)
break;
}
return result;
}
private PhysicsSetPositionResult SetPositionInternal(
Transition transition,
in PhysicsSetPositionRequest request,
Func<PhysicsSetPositionCollisionReport, bool>? handleCollisions)
{
transition.SpherePath.CellCandidates.Clear();
transition.SpherePath.ClearWalkable();
ImmutableArray<FlatCollisionSphere> spheres = request.Spheres;
float sphereScale = spheres.IsDefaultOrEmpty ? 1f : request.Scale;
Vector3 firstLocalCenter = spheres.IsDefaultOrEmpty
? new Vector3(0f, 0f, PhysicsGlobals.DummySphereRadius)
: spheres[0].Origin * sphereScale;
Vector3 firstWorldCenter =
Vector3.Transform(firstLocalCenter, request.Orientation)
+ request.Position;
AdjustedSetPosition adjusted = AdjustSetPosition(
request.CellId,
request.CellLocalPosition,
firstWorldCenter);
if (!adjusted.Resident)
{
return new PhysicsSetPositionResult(
PhysicsSetPositionError.Ok,
PhysicsResidenceDisposition.DeferredCell,
request.Position,
request.Orientation,
adjusted.CellId,
adjusted.CellLocalPosition,
CrossCellIds: ImmutableArray<uint>.Empty,
CollidedObjectIds: ImmutableArray<uint>.Empty);
}
bool forceIntoCell = request.PlacementClass is
PhysicsPlacementClass.Hook
or PhysicsPlacementClass.Storage
or PhysicsPlacementClass.Corpse;
if (forceIntoCell)
{
if (adjusted.CellId == 0u)
{
return ErrorResult(
request,
PhysicsSetPositionError.NoCell);
}
bool changedCell = request.CurrentCellId is null
|| request.CurrentCellId.Value != adjusted.CellId;
return new PhysicsSetPositionResult(
PhysicsSetPositionError.Ok,
PhysicsResidenceDisposition.Committed,
request.Position,
request.Orientation,
adjusted.CellId,
adjusted.CellLocalPosition,
CellChanged: changedCell,
ShadowAction: changedCell
? PhysicsShadowCommitAction.Recalculate
: PhysicsShadowCommitAction.None,
CrossCellIds: ImmutableArray<uint>.Empty,
CollidedObjectIds: ImmutableArray<uint>.Empty);
}
transition.SpherePath.InitPath(
request.Position,
request.Position,
adjusted.CellId,
spheres,
sphereScale,
request.Orientation,
request.Orientation);
transition.SpherePath.InsertType = InsertType.Placement;
transition.SpherePath.PlacementAllowsSliding =
request.Flags.HasFlag(PhysicsSetPositionFlags.Slide);
bool valid = transition.FindValidPosition(this);
SpherePath spherePath = transition.SpherePath;
if (valid
&& !request.Flags.HasFlag(PhysicsSetPositionFlags.Slide))
{
valid = AcceptNoSlidePlacement(
spherePath.CurPos,
request.Position,
spherePath.CurCellId,
adjusted.CellId);
}
CollisionInfo collision = transition.CollisionInfo;
var collisionReport = new PhysicsSetPositionCollisionReport(
collision.ContactPlaneValid,
collision.ContactPlane,
collision.ContactPlaneCellId,
collision.ContactPlaneIsWater,
collision.LastKnownContactPlaneValid,
collision.LastKnownContactPlane,
collision.LastKnownContactPlaneCellId,
collision.LastKnownContactPlaneIsWater,
collision.SlidingNormalValid,
collision.SlidingNormal,
collision.CollisionNormalValid,
collision.CollisionNormal,
collision.CollidedWithEnvironment,
collision.FramesStationaryFall,
collision.AdjustOffset,
collision.LastCollidedObjectGuid,
collision.CollideObjectGuids.ToImmutableArray());
bool collisionHandlerResult = !valid
&& handleCollisions?.Invoke(collisionReport) == true;
if (!valid)
{
return new PhysicsSetPositionResult(
collisionHandlerResult
? PhysicsSetPositionError.Collided
: PhysicsSetPositionError.NoValidPosition,
PhysicsResidenceDisposition.Unchanged,
request.Position,
request.Orientation,
request.CellId,
request.CellLocalPosition,
InContact: collision.ContactPlaneValid,
OnWalkable: PhysicsObjUpdate.IsWalkableContact(
collision.ContactPlaneValid,
collision.ContactPlane.Normal),
ContactPlane: collision.ContactPlane,
ContactPlaneCellId: collision.ContactPlaneCellId,
ContactPlaneIsWater: collision.ContactPlaneIsWater,
SlidingNormalValid: collision.SlidingNormalValid,
SlidingNormal: collision.SlidingNormal,
CollisionNormalValid: collision.CollisionNormalValid,
CollisionNormal: collision.CollisionNormal,
FramesStationaryFall: collision.FramesStationaryFall,
CollisionHandlerResult: collisionHandlerResult,
CollidedWithEnvironment: collision.CollidedWithEnvironment,
CrossCellIds: ImmutableArray<uint>.Empty,
CollidedObjectIds: collisionReport.CollidedObjectIds);
}
if (spherePath.CurCellId == 0u)
{
return ErrorResult(request, PhysicsSetPositionError.NoCell);
}
bool inContact = collision.ContactPlaneValid;
bool onWalkable = PhysicsObjUpdate.IsWalkableContact(
inContact,
collision.ContactPlane.Normal);
Vector3 resultLocal = adjusted.CellLocalPosition
+ (spherePath.CurPos - request.Position)
- LandDefs.GetBlockOffset(adjusted.CellId, spherePath.CurCellId);
bool hasPhysicsBsp = request.MoverPhysicsState.HasFlag(
PhysicsStateFlags.HasPhysicsBsp);
ImmutableArray<uint> transitionCells =
spherePath.CellCandidates.OrderedIds.ToImmutableArray();
PhysicsShadowCommitAction shadowAction = hasPhysicsBsp
? PhysicsShadowCommitAction.Recalculate
: transitionCells.Length != 0
? PhysicsShadowCommitAction.Replace
: PhysicsShadowCommitAction.Preserve;
return new PhysicsSetPositionResult(
PhysicsSetPositionError.Ok,
PhysicsResidenceDisposition.Committed,
spherePath.CurPos,
// CheckPositionInternal mutates only origin in no-slide mode;
// SetPosition retains the requested frame orientation.
request.Orientation,
spherePath.CurCellId,
resultLocal,
inContact,
onWalkable,
collision.ContactPlane,
collision.ContactPlaneCellId,
collision.ContactPlaneIsWater,
collision.SlidingNormalValid,
collision.SlidingNormal,
collision.CollisionNormalValid,
collision.CollisionNormal,
collision.FramesStationaryFall,
collision.CollidedWithEnvironment,
collisionHandlerResult,
CellChanged: request.CurrentCellId is null
|| request.CurrentCellId.Value != spherePath.CurCellId,
ShadowAction: shadowAction,
CrossCellIds: shadowAction == PhysicsShadowCommitAction.Replace
? transitionCells
: ImmutableArray<uint>.Empty,
CollidedObjectIds:
collision.CollideObjectGuids.ToImmutableArray());
}
private static PhysicsSetPositionResult ErrorResult(
in PhysicsSetPositionRequest request,
PhysicsSetPositionError error) => new(
error,
PhysicsResidenceDisposition.Unchanged,
request.Position,
request.Orientation,
request.CellId,
request.CellLocalPosition,
CrossCellIds: ImmutableArray<uint>.Empty,
CollidedObjectIds: ImmutableArray<uint>.Empty);
internal static bool AcceptNoSlidePlacement(
Vector3 resolvedPosition,
Vector3 requestedPosition,
uint resolvedCellId,
uint adjustedCellId)
{
Vector3 displacement = resolvedPosition - requestedPosition;
return displacement.X <= 0.0500000007f
&& displacement.Y <= 0.0500000007f
&& resolvedCellId == adjustedCellId;
}
/// <summary>
/// #111: the walkable floor Z of <paramref name="cellId"/>'s PHYSICS
/// polygons under the world XY, nearest to <paramref name="referenceZ"/>.

View file

@ -0,0 +1,155 @@
using System.Collections.Immutable;
using System.Numerics;
namespace AcDream.Core.Physics;
/// <summary>
/// Retail <c>SetPositionError</c> (<c>acclient.h</c>, enum 491). This is
/// deliberately independent of <see cref="PhysicsResidenceDisposition"/>:
/// losing the destination cell is a successful SetPosition operation whose
/// residence is deferred, not a placement error.
/// </summary>
internal enum PhysicsSetPositionError
{
Ok = 0,
GeneralFailure = 1,
NoValidPosition = 2,
NoCell = 3,
Collided = 4,
InvalidArguments = 0x100,
}
internal enum PhysicsResidenceDisposition
{
Committed,
DeferredCell,
Unchanged,
}
/// <summary>
/// Shadow-list operation performed by retail's SetPosition commit tail.
/// Recalculate delegates to the canonical live shadow-shape owner (the
/// PhysicsBSP/bounding-box path cannot be reconstructed from placement
/// spheres); Replace consumes <see cref="PhysicsSetPositionResult.CrossCellIds"/>;
/// Preserve intentionally leaves the existing list untouched.
/// </summary>
internal enum PhysicsShadowCommitAction
{
None,
Recalculate,
Replace,
Preserve,
}
internal readonly record struct PhysicsSetPositionCollisionReport(
bool ContactPlaneValid,
Plane ContactPlane,
uint ContactPlaneCellId,
bool ContactPlaneIsWater,
bool LastKnownContactPlaneValid,
Plane LastKnownContactPlane,
uint LastKnownContactPlaneCellId,
bool LastKnownContactPlaneIsWater,
bool SlidingNormalValid,
Vector3 SlidingNormal,
bool CollisionNormalValid,
Vector3 CollisionNormal,
bool CollidedWithEnvironment,
int FramesStationaryFall,
Vector3 AdjustOffset,
uint? LastCollidedObjectId,
ImmutableArray<uint> CollidedObjectIds);
[Flags]
internal enum PhysicsSetPositionFlags : uint
{
None = 0,
Placement = 0x001,
Teleport = 0x002,
Restore = 0x004,
Slide = 0x010,
DoNotCreateCells = 0x020,
Scatter = 0x100,
RandomScatter = 0x200,
Line = 0x400,
SendPositionEvent = 0x1000,
}
/// <summary>
/// The three retail weenie classifications which bypass placement collision
/// in <c>CPhysicsObj::SetPositionInternal</c> after AdjustPosition succeeds.
/// Runtime derives this from the canonical object record; callers cannot use a
/// free boolean to force ordinary objects through geometry.
/// </summary>
internal enum PhysicsPlacementClass
{
Ordinary,
Hook,
Storage,
Corpse,
}
/// <summary>
/// Complete immutable input to retail <c>CPhysicsObj::SetPosition</c>. World
/// position feeds acdream's flat collision representation; cell-local position
/// is retail's <c>Position.frame.origin</c> and is the only input to
/// <c>LandDefs::adjust_to_outside</c>.
/// </summary>
internal readonly record struct PhysicsSetPositionRequest(
Vector3 Position,
Quaternion Orientation,
uint CellId,
Vector3 CellLocalPosition,
ImmutableArray<FlatCollisionSphere> Spheres,
float Scale,
float StepUpHeight,
float StepDownHeight,
PhysicsStateFlags MoverPhysicsState = PhysicsStateFlags.None,
ObjectInfoState MoverFlags = ObjectInfoState.None,
uint MovingEntityId = 0u,
PhysicsPlacementClass PlacementClass = PhysicsPlacementClass.Ordinary,
PhysicsSetPositionFlags Flags = PhysicsSetPositionFlags.Placement,
Vector3 Line = default,
float ScatterRadiusX = 0f,
float ScatterRadiusY = 0f,
uint ScatterAttempts = 0u,
// Null models retail's distinct `this->cell == nullptr` state. The
// retained Position cell id may equal the destination while the object is
// still in the lost-cell list; that wake must change_cell and reflood.
uint? CurrentCellId = null);
/// <summary>
/// Immutable commit packet produced by the pure placement transaction. Runtime
/// installs this packet atomically into the canonical body/spatial owner before
/// publishing one presentation delta.
/// </summary>
internal readonly record struct PhysicsSetPositionResult(
PhysicsSetPositionError Error,
PhysicsResidenceDisposition Residence,
Vector3 Position,
Quaternion Orientation,
uint CellId,
Vector3 CellLocalPosition,
bool InContact = false,
bool OnWalkable = false,
Plane ContactPlane = default,
uint ContactPlaneCellId = 0u,
bool ContactPlaneIsWater = false,
bool SlidingNormalValid = false,
Vector3 SlidingNormal = default,
bool CollisionNormalValid = false,
Vector3 CollisionNormal = default,
int FramesStationaryFall = 0,
bool CollidedWithEnvironment = false,
bool CollisionHandlerResult = false,
bool CellChanged = false,
PhysicsShadowCommitAction ShadowAction = PhysicsShadowCommitAction.None,
ImmutableArray<uint> CrossCellIds = default,
ImmutableArray<uint> CollidedObjectIds = default)
{
internal bool IsSuccessful => Error == PhysicsSetPositionError.Ok;
internal bool IsCommitted =>
IsSuccessful && Residence == PhysicsResidenceDisposition.Committed;
internal bool IsDeferred =>
IsSuccessful && Residence == PhysicsResidenceDisposition.DeferredCell;
}

View file

@ -1575,6 +1575,79 @@ public sealed class Transition
return transitionState == TransitionState.OK;
}
/// <summary>
/// Retail <c>CTransition::find_valid_position</c> (0x0050C310). Placement
/// insertions use the complete outer <c>find_placement_position</c>
/// transaction; ordinary movement uses the transitional sweep.
/// </summary>
internal bool FindValidPosition(PhysicsEngine engine) =>
SpherePath.InsertType == InsertType.Transition
? FindTransitionalPosition(engine)
: FindPlacementPosition(engine);
/// <summary>
/// Retail <c>CTransition::find_placement_position</c> (0x0050C170):
/// INITIAL_PLACEMENT insertion, other-cell validation, the inner compass
/// search, optional placement step-down, and final validation.
/// </summary>
internal bool FindPlacementPosition(PhysicsEngine engine)
{
SpherePath sp = SpherePath;
sp.SetCheckPos(sp.CurPos, sp.CurCellId);
sp.InsertType = InsertType.InitialPlacement;
TransitionState initial = ValidatePlacement(
engine,
InitialPlacementInsert(engine),
retryPlacement: true);
if (initial != TransitionState.OK)
return false;
sp.InsertType = InsertType.Placement;
if (!FindPlacementPos(engine))
return false;
if (ObjectInfo.StepDown)
{
const float placementWalkableAllowance = 0.0871556997f;
float stepDownHeight = ObjectInfo.StepDownHeight;
sp.WalkableAllowance = placementWalkableAllowance;
sp.SaveCheckPos();
InsertType savedInsert = sp.InsertType;
sp.InsertType = InsertType.Transition;
(float probeHeight, int probeCount) = GetStepDownProbePlan(
sp.NumSphere,
sp.GlobalSphere[0].Radius,
stepDownHeight);
bool stepped = DoStepDown(
probeHeight,
placementWalkableAllowance,
engine);
if (!stepped && probeCount > 1)
{
stepped = DoStepDown(
probeHeight,
placementWalkableAllowance,
engine);
}
if (!stepped)
{
sp.RestoreCheckPos();
CollisionInfo.ContactPlaneValid = false;
CollisionInfo.ContactPlaneIsWater = false;
}
sp.InsertType = savedInsert;
sp.ClearWalkable();
}
return ValidatePlacement(
engine,
TransitionState.OK,
retryPlacement: true)
== TransitionState.OK;
}
/// <summary>
/// Retail <c>CTransition::find_placement_pos</c> (0x0050BA50).
/// Tests the requested position first, then searches concentric rings up
@ -1613,15 +1686,19 @@ public sealed class Transition
sphereRadius = 0.48f;
}
float stepCountExact = 4f / sphereRadius;
double stepCountExact = 4d / (double)sphereRadius;
if (fakeSphere)
stepCountExact *= 0.5f;
if (stepCountExact <= 1f)
stepCountExact *= 0.5d;
if (stepCountExact <= 1d)
return false;
int stepCount = (int)MathF.Ceiling(stepCountExact);
float distancePerStep = adjustRadius / stepCount;
float radiansPerStep = MathF.PI * distancePerStep / sphereRadius;
int stepCount = (int)Math.Ceiling(stepCountExact);
float distancePerStep = (float)((double)adjustRadius / stepCount);
// Retail stores the literal 3.14159989f here; do not substitute the
// BCL's more precise PI because it changes late compass samples.
float radiansPerStep = (float)(
((double)distancePerStep / sphereRadius)
* 3.14159989f);
float totalDistance = 0f;
float totalRadians = 0f;
@ -1630,23 +1707,25 @@ public sealed class Transition
totalDistance += distancePerStep;
totalRadians += radiansPerStep;
int sampleCount = (int)MathF.Ceiling(totalRadians) * 2;
float headingStep = 360f / sampleCount;
int sampleCount = (int)Math.Ceiling((double)totalRadians) * 2;
float headingStep = (float)(360d / sampleCount);
for (int sample = 0; sample < sampleCount; sample++)
{
sp.SetCheckPos(sp.CurPos, sp.CurCellId);
// Frame::set_heading/get_vector_heading: 0 degrees is +Y,
// 90 degrees is +X in AC's compass convention.
float headingRadians = headingStep * sample * (MathF.PI / 180f);
var offset = new Vector3(
MathF.Sin(headingRadians) * totalDistance,
MathF.Cos(headingRadians) * totalDistance,
0f);
// 90 degrees is +X in AC's compass convention. Retail promotes
// the float heading to x87 precision, multiplies by this exact
// degree-to-radian constant, then rounds sin/cos back to float.
float heading = headingStep * sample;
Vector3 offset = GetPlacementCompassOffset(
heading,
totalDistance);
sp.GlobalOffset = AdjustOffset(offset);
if (sp.GlobalOffset.Length() < PhysicsGlobals.EPSILON)
if (sp.GlobalOffset.LengthSquared()
< PhysicsGlobals.EpsilonSq)
continue;
sp.AddOffsetToCheckPos(sp.GlobalOffset);
@ -1664,7 +1743,20 @@ public sealed class Transition
return false;
}
private TransitionState ValidatePlacementTransition(TransitionState transitionState)
internal static Vector3 GetPlacementCompassOffset(
float headingDegrees,
float distance)
{
const double DegreesToRadians = 0.017453292519943295d;
double radians = (double)headingDegrees * DegreesToRadians;
return new Vector3(
(float)Math.Sin(radians) * distance,
(float)Math.Cos(radians) * distance,
0f);
}
private TransitionState ValidatePlacementTransition(
TransitionState transitionState)
{
var sp = SpherePath;
if (sp.CheckCellId == 0)
@ -1682,7 +1774,9 @@ public sealed class Transition
sp.GlobalCurrCenter[i].Radius = sp.LocalSphere[i].Radius;
}
}
else if (sp.PlacementAllowsSliding)
else if (transitionState > TransitionState.OK
&& transitionState <= TransitionState.Slid
&& sp.PlacementAllowsSliding)
{
// COLLISIONINFO::init at retail 0x0050B052. Placement probes are
// independent; a failed compass sample must not bias the next one.
@ -1692,6 +1786,58 @@ public sealed class Transition
return transitionState;
}
internal TransitionState ValidatePlacementTransitionForTest(
TransitionState transitionState) =>
ValidatePlacementTransition(transitionState);
/// <summary>
/// Retail <c>CTransition::validate_placement</c> (0x0050B210), used only
/// by the outer placement transaction. Adjusted/Slid receives one
/// placement_insert retry when requested; Collided never retries and this
/// validator never clears CollisionInfo.
/// </summary>
private TransitionState ValidatePlacement(
PhysicsEngine engine,
TransitionState transitionState,
bool retryPlacement)
{
SpherePath sp = SpherePath;
if (sp.CheckCellId == 0u)
return TransitionState.Collided;
if (transitionState == TransitionState.OK)
{
sp.CurPos = sp.CheckPos;
sp.CurCellId = sp.CheckCellId;
sp.CurOrientation = sp.CheckOrientation;
for (int i = 0; i < sp.NumSphere; i++)
{
sp.GlobalCurrCenter[i].Origin =
Vector3.Transform(
sp.LocalSphere[i].Origin,
sp.CurOrientation)
+ sp.CurPos;
sp.GlobalCurrCenter[i].Radius = sp.LocalSphere[i].Radius;
}
}
else if ((transitionState is TransitionState.Adjusted
or TransitionState.Slid)
&& retryPlacement)
{
return ValidatePlacement(
engine,
PlacementInsert(engine),
retryPlacement: false);
}
return transitionState;
}
internal TransitionState ValidatePlacementForTest(
PhysicsEngine engine,
TransitionState transitionState,
bool retryPlacement) =>
ValidatePlacement(engine, transitionState, retryPlacement);
// -----------------------------------------------------------------------
// Per-step collision check
// -----------------------------------------------------------------------
@ -1749,10 +1895,10 @@ public sealed class Transition
float diameter = sphereRadius * 2f;
float probeHeight = requestedHeight;
if (numSpheres < 2 && diameter < probeHeight)
if (numSpheres < 2 && diameter <= probeHeight)
probeHeight = sphereRadius * 0.5f;
if (diameter >= probeHeight)
if (diameter > probeHeight)
return (probeHeight, 1);
return (probeHeight * 0.5f, 2);
@ -2176,6 +2322,54 @@ public sealed class Transition
return state;
}
/// <summary>
/// The initial half of retail <c>find_placement_position</c>. This is
/// deliberately not <see cref="TransitionalInsert"/>: initial placement
/// performs one primary insert and, only on OK, the other-cell pass. It
/// never enters transitional collide/step/edge response branches.
/// </summary>
private TransitionState InitialPlacementInsert(PhysicsEngine engine)
{
SpherePath sp = SpherePath;
if (sp.CheckCellId == 0u)
return TransitionState.Collided;
TransitionState result = InsertIntoCell(
engine,
sp.CheckCellId,
numAttempts: 3);
if (result == TransitionState.OK)
{
result = RunCheckOtherCellsAndAdvance(
engine,
sp.GlobalSphere[0].Origin,
sp.GlobalSphere[0].Radius);
}
return result;
}
/// <summary>
/// Retail <c>CTransition::placement_insert</c> (0x0050B1D0), used by
/// <c>validate_placement</c> for exactly one Adjusted/Slid retry.
/// </summary>
private TransitionState PlacementInsert(PhysicsEngine engine)
{
SpherePath sp = SpherePath;
if (sp.CheckCellId == 0u)
return TransitionState.Collided;
TransitionState result = InsertIntoCell(
engine,
sp.CheckCellId,
numAttempts: 3);
return result == TransitionState.OK
? RunCheckOtherCellsAndAdvance(
engine,
sp.GlobalSphere[0].Origin,
sp.GlobalSphere[0].Radius)
: result;
}
/// <summary>
/// Primary-cell virtual <c>find_collisions</c> composition. A non-OK
/// response terminates this pass, so an inner retry always restarts from