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"/>.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue