feat(physics): port canonical retail set-position core
This commit is contained in:
parent
6b28ff999c
commit
e84a388e6f
6 changed files with 1952 additions and 21 deletions
|
|
@ -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) ->
|
||||
/// <c>SetPositionInternal</c> (0x00515BD0) ->
|
||||
/// <c>AdjustPosition</c> (0x00511D80) ->
|
||||
/// <c>CheckPositionInternal</c> (0x00511E90) ->
|
||||
/// <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"/>.
|
||||
|
|
|
|||
155
src/AcDream.Core/Physics/PhysicsSetPosition.cs
Normal file
155
src/AcDream.Core/Physics/PhysicsSetPosition.cs
Normal 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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue