feat(vfx): port retail hidden and teleport presentation

Preserve canonical live-object ownership across Hidden transitions and remote teleport placement so effects, collision, streaming, and targeting remain synchronized.
This commit is contained in:
Erik 2026-07-14 14:59:48 +02:00
parent a51ebc66e9
commit 1e98d81448
46 changed files with 4883 additions and 127 deletions

View file

@ -251,7 +251,7 @@ internal sealed class ProjectileController
entity.MeshRefs.Count > 0);
_liveEntities.SetProjectileRuntime(record.ServerGuid, runtime);
if (HasVisibleCell(record))
if (HasVisibleCell(record) && !IsHidden(record))
{
ShadowPositionSynchronizer.Sync(
_shadows,
@ -262,6 +262,15 @@ internal sealed class ProjectileController
liveCenterX,
liveCenterY);
}
else if (HasVisibleCell(record))
{
// Hidden is a retained in-world CPhysicsObj, not leave_world.
// Keep Active/identity but remove collision rows and consume the
// hidden clock so UnHide cannot replay a time backlog.
body.InWorld = true;
body.LastUpdateTime = currentTime;
_shadows.Suspend(entity.Id);
}
else
{
SuspendOutsideWorld(runtime, entity.Id);
@ -300,7 +309,8 @@ internal sealed class ProjectileController
SetVelocity(runtime.Body, velocity, currentTime);
runtime.Body.Omega = angularVelocity;
if (!_liveEntities.ShouldAdvanceRootRuntime(serverGuid))
if (!_liveEntities.ShouldAdvanceRootRuntime(serverGuid)
&& !IsHidden(record))
Deactivate(runtime.Body);
return true;
}
@ -425,7 +435,7 @@ internal sealed class ProjectileController
entity.Rotation = orientation;
entity.ParentCellId = canonicalCellId;
_liveEntities.RebucketLiveEntity(serverGuid, canonicalCellId);
if (HasVisibleCell(record))
if (HasVisibleCell(record) && !IsHidden(record))
{
if (!wasInWorld)
Activate(body, currentTime);
@ -439,6 +449,12 @@ internal sealed class ProjectileController
liveCenterX,
liveCenterY);
}
else if (HasVisibleCell(record))
{
body.InWorld = true;
body.LastUpdateTime = currentTime;
_shadows.Suspend(entity.Id);
}
else
{
SuspendOutsideWorld(runtime, entity.Id);
@ -496,6 +512,27 @@ internal sealed class ProjectileController
|| record.WorldEntity is not { } entity)
continue;
runtime.Body.State = record.FinalPhysicsState;
if (IsHidden(record))
{
if (!HasVisibleCell(record))
{
SuspendOutsideWorld(runtime, entity.Id);
}
else
{
// UpdatePositionInternal skips root physics/PartArray
// while Hidden, but update_object still advances its
// clock and does not clear Active.
runtime.Body.InWorld = true;
runtime.Body.LastUpdateTime = currentTime;
if ((record.FinalPhysicsState & PhysicsStateFlags.Frozen) != 0)
Deactivate(runtime.Body);
_shadows.Suspend(entity.Id);
}
continue;
}
bool isMissile =
(record.FinalPhysicsState & PhysicsStateFlags.Missile) != 0;
if (!isMissile)
@ -530,7 +567,6 @@ internal sealed class ProjectileController
continue;
}
runtime.Body.State = record.FinalPhysicsState;
if (!_liveEntities.ShouldAdvanceRootRuntime(record.ServerGuid))
{
if (!HasVisibleCell(record))
@ -637,6 +673,9 @@ internal sealed class ProjectileController
&& record.IsSpatiallyVisible
&& record.FullCellId != 0;
private static bool IsHidden(LiveEntityRecord record) =>
(record.FinalPhysicsState & PhysicsStateFlags.Hidden) != 0;
private void SuspendOutsideWorld(Runtime runtime, uint localEntityId)
{
runtime.Body.InWorld = false;

View file

@ -11,6 +11,8 @@ namespace AcDream.App.Physics;
/// frame, from inside the same guard the body used to live under
/// (<c>ae.Sequencer != null &amp;&amp; serverGuid != 0 &amp;&amp; serverGuid != _playerServerGuid
/// &amp;&amp; rm.LastServerPosTime &gt; 0</c>).
/// Hidden remotes use a separate live-entity pass because retail keeps their
/// PositionManager alive even when the object has no render-animation owner.
///
/// <para>Slice 2a extracted this verbatim (fork intact). Slice 2b then COLLAPSED
/// the player/NPC fork: the former Path A (grounded PLAYER remotes advanced by the
@ -55,6 +57,35 @@ internal sealed class RemotePhysicsUpdater
// Duplicated one-liner (GameWindow keeps its own copy — many callers there).
private static bool IsPlayerGuid(uint guid) => (guid & 0xFF000000u) == 0x50000000u;
/// <summary>
/// Advances the retained PositionManager path for every hidden remote,
/// including objects without an <c>AnimatedEntity</c> (missiles commonly
/// have no PartArray). Retail gates this path on the live CPhysicsObj, not
/// on whether a render-animation owner exists.
/// </summary>
public void TickHiddenEntities(
AcDream.App.World.LiveEntityRuntime liveEntities,
uint localPlayerServerGuid,
float dt,
System.Action<AcDream.Core.World.WorldEntity> publishRootPose)
{
ArgumentNullException.ThrowIfNull(liveEntities);
ArgumentNullException.ThrowIfNull(publishRootPose);
foreach (AcDream.App.World.LiveEntityRecord record in liveEntities.Records.ToArray())
{
if (record.ServerGuid == localPlayerServerGuid
|| (record.FinalPhysicsState & AcDream.Core.Physics.PhysicsStateFlags.Hidden) == 0
|| !liveEntities.ShouldAdvanceRootRuntime(record.ServerGuid)
|| record.RemoteMotionRuntime is not RemoteMotion remote
|| record.WorldEntity is not { } entity)
continue;
TickHidden(remote, entity, dt);
publishRootPose(entity);
}
}
/// <summary>
/// #184 Slice 2a — the per-remote DR tick (retail <c>UpdateObjectInternal</c>
/// shape), verbatim from the former <c>GameWindow.TickAnimations</c> guard
@ -556,6 +587,99 @@ internal sealed class RemotePhysicsUpdater
rm.Host?.PositionManager.UseTime();
}
/// <summary>
/// Retail hidden-object slice of <c>CPhysicsObj::UpdatePositionInternal</c>
/// (<c>0x00512C30</c>) plus the manager tail of
/// <c>UpdateObjectInternal</c> (<c>0x005156B0</c>). Hidden skips
/// <c>CPartArray::Update</c> and <c>UpdatePhysicsInternal</c>, but the
/// PositionManager offset is still composed and the target, movement, and
/// position managers still consume time. Physics-script and particle owners
/// tick later in the shared frame pipeline.
/// </summary>
public void TickHidden(
RemoteMotion rm,
AcDream.Core.World.WorldEntity entity,
float dt)
{
ArgumentNullException.ThrowIfNull(rm);
ArgumentNullException.ThrowIfNull(entity);
System.Numerics.Vector3 preComposePosition = rm.Body.Position;
// The part-array contribution is the identity frame while Hidden.
// Interpolation is the first PositionManager stage in retail; acdream
// retains it in RemoteMotionCombiner, ahead of Sticky/Constraint.
System.Numerics.Vector3 interpolationOffset = rm.Position.ComputeOffset(
dt,
rm.Body.Position,
System.Numerics.Vector3.Zero,
rm.Body.Orientation,
rm.Interp,
rm.Motion.GetMaxSpeed());
var positionDelta = new AcDream.Core.Physics.Motion.MotionDeltaFrame
{
Origin = AcDream.Core.Physics.Motion.MoveToMath.GlobalToLocalVec(
rm.Body.Orientation,
interpolationOffset),
};
rm.Host?.PositionManager.AdjustOffset(positionDelta, dt);
ApplyPositionManagerDelta(rm.Body, positionDelta);
System.Numerics.Vector3 composedPosition = rm.Body.Position;
if (rm.CellId != 0
&& composedPosition != preComposePosition
&& _physicsEngine.LandblockCount > 0)
{
var (radius, height) = _getSetupCylinder(entity.ServerGuid, entity);
if (radius < 0.05f)
{
radius = 0.48f;
height = 1.835f;
}
bool previousContact = rm.Body.InContact;
bool previousOnWalkable = rm.Body.OnWalkable;
var resolved = _physicsEngine.ResolveWithTransition(
preComposePosition,
composedPosition,
rm.CellId,
radius,
height,
stepUpHeight: 0.4f,
stepDownHeight: 0.4f,
isOnGround: previousOnWalkable,
body: rm.Body,
moverFlags: IsPlayerGuid(entity.ServerGuid)
? AcDream.Core.Physics.ObjectInfoState.IsPlayer
| AcDream.Core.Physics.ObjectInfoState.EdgeSlide
: AcDream.Core.Physics.ObjectInfoState.EdgeSlide,
movingEntityId: entity.Id);
rm.Body.Position = resolved.Position;
if (resolved.CellId != 0)
rm.CellId = resolved.CellId;
AcDream.Core.Physics.PhysicsObjUpdate.CommitSetPositionTransition(
rm.Body,
resolved.InContact,
resolved.OnWalkable,
resolved.CollisionNormalValid,
resolved.CollisionNormal,
previousContact,
previousOnWalkable,
rm.Movement.HitGround,
rm.Motion.LeaveGround);
rm.Airborne = !rm.Body.OnWalkable;
}
entity.SetPosition(rm.Body.Position);
if (rm.CellId != 0)
entity.ParentCellId = rm.CellId;
entity.Rotation = rm.Body.Orientation;
rm.Host?.HandleTargetting();
TickRemoteMoveTo(rm);
rm.Host?.PositionManager.UseTime();
}
/// <summary>
/// R4-V5 / R5-V2: the per-tick <see cref="AcDream.Core.Physics.Motion.MovementManager"/>
/// drive (retail <c>MovementManager::UseTime</c> 0x005242f0 — the moveto
@ -605,16 +729,36 @@ internal sealed class RemotePhysicsUpdater
/// Moved from GameWindow (#184 Slice 2a); called by the DR tick AND the NPC
/// UP-branch tail.
/// </summary>
public void SyncRemoteShadowToBody(uint entityId, RemoteMotion rm, int liveCenterX, int liveCenterY)
public void SyncRemoteShadowToBody(
uint entityId,
AcDream.App.World.ILiveEntityRemotePlacementRuntime rm,
int liveCenterX,
int liveCenterY,
uint? authoritativeCellId = null)
{
SyncRemoteShadowToBody(
entityId,
rm.Body,
liveCenterX,
liveCenterY,
authoritativeCellId ?? rm.CellId);
rm.LastShadowSyncPosition = rm.Body.Position;
}
public void SyncRemoteShadowToBody(
uint entityId,
AcDream.Core.Physics.PhysicsBody body,
int liveCenterX,
int liveCenterY,
uint authoritativeCellId)
{
ShadowPositionSynchronizer.Sync(
_physicsEngine.ShadowObjects,
entityId,
rm.Body.Position,
rm.Body.Orientation,
rm.CellId,
body.Position,
body.Orientation,
authoritativeCellId,
liveCenterX,
liveCenterY);
rm.LastShadowSyncPos = rm.Body.Position;
}
}

View file

@ -0,0 +1,444 @@
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.World;
using AcDream.Core.Physics;
using AcDream.Core.World;
namespace AcDream.App.Physics;
/// <summary>
/// Owns the SetPosition half of retail remote
/// <c>CPhysicsObj::MoveOrTeleport</c> (0x00516330). It resolves the
/// destination through the placement transition, commits that transition to
/// the existing body, and synchronizes the remote movement/contact state.
/// Logical identity, target-hook actions, rebucketing callbacks, and render
/// presentation remain with their existing owners.
/// </summary>
internal sealed class RemoteTeleportController : IDisposable
{
internal delegate ResolveResult PlacementResolver(
Vector3 position,
uint cellId,
float radius,
float height,
ObjectInfoState moverFlags,
uint movingEntityId);
private readonly PhysicsEngine _physics;
private readonly LiveEntityRuntime _liveEntities;
private readonly Func<uint, WorldEntity, (float Radius, float Height)> _getSetupCylinder;
private readonly Func<Vector3, uint, Vector3> _cellLocalForSeed;
private readonly Action<WorldEntity, PhysicsBody, uint> _syncResolvedShadow;
private readonly Action<uint, ushort, bool> _completeAuthoritativePlacement;
private readonly Action<uint, ushort> _beginAuthoritativePlacement;
private readonly PlacementResolver _resolvePlacement;
private readonly Dictionary<uint, PendingPlacement> _pending = new();
internal RemoteTeleportController(
PhysicsEngine physics,
LiveEntityRuntime liveEntities,
Func<uint, WorldEntity, (float Radius, float Height)> getSetupCylinder,
Func<Vector3, uint, Vector3> cellLocalForSeed,
Action<WorldEntity, PhysicsBody, uint> syncResolvedShadow,
Action<uint, ushort, bool> completeAuthoritativePlacement,
Action<uint, ushort> beginAuthoritativePlacement,
PlacementResolver? resolvePlacement = null)
{
_physics = physics ?? throw new ArgumentNullException(nameof(physics));
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
_getSetupCylinder = getSetupCylinder
?? throw new ArgumentNullException(nameof(getSetupCylinder));
_cellLocalForSeed = cellLocalForSeed
?? throw new ArgumentNullException(nameof(cellLocalForSeed));
_syncResolvedShadow = syncResolvedShadow
?? throw new ArgumentNullException(nameof(syncResolvedShadow));
_completeAuthoritativePlacement = completeAuthoritativePlacement
?? throw new ArgumentNullException(nameof(completeAuthoritativePlacement));
_beginAuthoritativePlacement = beginAuthoritativePlacement
?? throw new ArgumentNullException(nameof(beginAuthoritativePlacement));
_resolvePlacement = resolvePlacement ?? ResolvePlacement;
_liveEntities.ProjectionVisibilityChanged += OnProjectionVisibilityChanged;
}
internal readonly record struct Result(
bool Applied,
bool ContactResolved,
Vector3 Position,
uint CellId,
Quaternion Orientation);
private readonly record struct PendingPlacement(
ILiveEntityRemotePlacementRuntime Remote,
PhysicsBody Body,
WorldEntity Entity,
Vector3 RequestedWorldPosition,
uint RequestedCellId,
Quaternion RequestedOrientation,
double GameTime,
ushort Generation,
ushort PositionSequence,
bool WasInContact,
bool WasOnWalkable,
RollbackPlacement Rollback);
private readonly record struct RollbackPlacement(
Vector3 Position,
uint CellId,
Vector3 CellLocalPosition,
Quaternion Orientation,
TransientStateFlags TransientState,
Plane ContactPlane,
bool ContactPlaneValid,
uint ContactPlaneCellId,
bool ContactPlaneIsWater,
double LastUpdateTime,
bool Airborne,
Vector3 LastServerPosition,
double LastServerPositionTime,
Vector3 LastShadowSyncPosition);
internal Result TryApply(
ILiveEntityRemotePlacementRuntime remote,
WorldEntity entity,
Vector3 requestedWorldPosition,
uint requestedCellId,
Vector3 requestedCellLocalPosition,
Quaternion requestedOrientation,
double gameTime,
bool destinationProjectionVisible,
ushort generation,
ushort positionSequence)
{
ArgumentNullException.ThrowIfNull(remote);
ArgumentNullException.ThrowIfNull(entity);
if (!_liveEntities.TryGetRecord(entity.ServerGuid, out LiveEntityRecord liveRecord)
|| liveRecord.Generation != generation
|| liveRecord.PhysicsBody is null
|| !ReferenceEquals(liveRecord.PhysicsBody, remote.Body))
{
throw new InvalidOperationException(
$"Remote placement for 0x{entity.ServerGuid:X8} must use its incarnation's canonical physics body.");
}
bool wasInContact = liveRecord.PhysicsBody.InContact;
bool wasOnWalkable = liveRecord.PhysicsBody.OnWalkable;
RollbackPlacement rollback = CaptureRollback(remote, liveRecord.PhysicsBody);
if (_pending.TryGetValue(entity.ServerGuid, out PendingPlacement prior)
&& prior.Generation == generation)
{
wasInContact = prior.WasInContact;
wasOnWalkable = prior.WasOnWalkable;
rollback = prior.Rollback;
}
PendingPlacement request = new(
remote,
liveRecord.PhysicsBody,
entity,
requestedWorldPosition,
requestedCellId,
requestedOrientation,
gameTime,
generation,
positionSequence,
wasInContact,
wasOnWalkable,
rollback);
if (!destinationProjectionVisible)
{
ParkPending(request, requestedCellLocalPosition);
return new Result(
true,
false,
requestedWorldPosition,
requestedCellId,
requestedOrientation);
}
ResolveResult placement = Resolve(request);
if (!placement.Ok)
{
_pending.Remove(entity.ServerGuid);
RollBackDeferredPlacement(request);
return new Result(
false,
false,
request.Body.Position,
remote.CellId,
request.Body.Orientation);
}
_pending.Remove(entity.ServerGuid);
return CommitResolved(request, placement);
}
internal void Forget(uint serverGuid)
{
_pending.Remove(serverGuid);
}
internal void Clear()
{
_pending.Clear();
}
internal bool HasPending(uint serverGuid) => _pending.ContainsKey(serverGuid);
/// <summary>
/// Transfers any older deferred shadow restoration before the accepted
/// destination is rebucketed and can publish a visibility edge.
/// </summary>
internal void BeginPlacement(uint serverGuid, ushort generation) =>
_beginAuthoritativePlacement(serverGuid, generation);
public void Dispose()
{
_liveEntities.ProjectionVisibilityChanged -= OnProjectionVisibilityChanged;
Clear();
}
private ResolveResult Resolve(PendingPlacement request)
{
var (radius, height) = _getSetupCylinder(
request.Entity.ServerGuid,
request.Entity);
if (radius < 0.05f)
{
radius = 0.48f;
height = 1.835f;
}
return _resolvePlacement(
request.RequestedWorldPosition,
request.RequestedCellId,
radius,
height,
IsPlayerGuid(request.Entity.ServerGuid)
? ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide
: ObjectInfoState.EdgeSlide,
request.Entity.Id);
}
private ResolveResult ResolvePlacement(
Vector3 position,
uint cellId,
float radius,
float height,
ObjectInfoState moverFlags,
uint movingEntityId) =>
_physics.ResolvePlacement(
position,
cellId,
radius,
height,
stepUpHeight: 0.4f,
stepDownHeight: 0.4f,
moverFlags: moverFlags,
movingEntityId: movingEntityId);
private Result CommitResolved(PendingPlacement request, ResolveResult placement)
{
RemoteTeleportPlacement.Apply(
request.Remote,
request.Body,
placement,
_cellLocalForSeed(placement.Position, placement.CellId),
request.RequestedOrientation,
request.GameTime,
request.WasInContact,
request.WasOnWalkable);
if (request.Remote.CellId != placement.CellId)
request.Remote.CellId = placement.CellId;
request.Remote.LastServerPosition = placement.Position;
request.Remote.LastServerPositionTime = request.GameTime;
request.Remote.LastShadowSyncPosition = Vector3.Zero;
request.Entity.SetPosition(request.Body.Position);
request.Entity.ParentCellId = placement.CellId;
request.Entity.Rotation = request.Body.Orientation;
if (_liveEntities.TryGetRecord(request.Entity.ServerGuid, out LiveEntityRecord record))
{
bool deferShadowRestore =
record.FinalPhysicsState.HasFlag(PhysicsStateFlags.Hidden)
|| !record.IsSpatiallyVisible;
_completeAuthoritativePlacement(
request.Entity.ServerGuid,
request.Generation,
deferShadowRestore);
if (!deferShadowRestore)
{
request.Remote.LastShadowSyncPosition = request.Body.Position;
_syncResolvedShadow(request.Entity, request.Body, placement.CellId);
}
}
return new Result(
true,
true,
request.Body.Position,
placement.CellId,
request.Body.Orientation);
}
private void ParkPending(PendingPlacement request, Vector3 requestedCellLocalPosition)
{
request.Body.Orientation = request.RequestedOrientation;
request.Body.SnapToCell(
request.RequestedCellId,
request.RequestedWorldPosition,
requestedCellLocalPosition);
request.Body.LastUpdateTime = request.GameTime;
request.Body.ContactPlaneValid = false;
request.Body.ContactPlaneCellId = 0u;
request.Body.ContactPlaneIsWater = false;
PhysicsObjUpdate.ApplySetPositionContact(
request.Body,
inContact: false,
onWalkable: false);
request.Remote.Airborne = true;
if (request.Remote.CellId != request.RequestedCellId)
request.Remote.CellId = request.RequestedCellId;
request.Remote.LastServerPosition = request.RequestedWorldPosition;
request.Remote.LastServerPositionTime = request.GameTime;
request.Remote.LastShadowSyncPosition = Vector3.Zero;
request.Entity.SetPosition(request.RequestedWorldPosition);
request.Entity.ParentCellId = request.RequestedCellId;
request.Entity.Rotation = request.RequestedOrientation;
_pending[request.Entity.ServerGuid] = request;
}
private void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible)
{
if (!visible)
return;
if (_pending.TryGetValue(record.ServerGuid, out PendingPlacement pending))
{
if (record.WorldEntity is null
|| !ReferenceEquals(record.WorldEntity, pending.Entity)
|| record.Generation != pending.Generation)
{
_pending.Remove(record.ServerGuid);
return;
}
if (record.RemoteMotionRuntime is not ILiveEntityRemotePlacementRuntime currentRemote
|| record.PhysicsBody is null
|| !ReferenceEquals(record.PhysicsBody, pending.Body)
|| !ReferenceEquals(currentRemote.Body, record.PhysicsBody))
{
_pending.Remove(record.ServerGuid);
if (record.RemoteMotionRuntime is not null
&& (record.PhysicsBody is null
|| !ReferenceEquals(record.RemoteMotionRuntime.Body, record.PhysicsBody)))
{
_liveEntities.ClearRemoteMotionRuntime(record.ServerGuid);
}
RollBackDeferredPlacement(pending);
return;
}
if (!ReferenceEquals(currentRemote, pending.Remote))
{
pending = pending with { Remote = currentRemote };
_pending[record.ServerGuid] = pending;
}
if (record.Snapshot.PositionSequence != pending.PositionSequence)
{
// A newer accepted UpdatePosition rebucketed the projection
// before its TryApply call could replace this request. Keep
// the original rollback alive for that synchronous call.
return;
}
_pending.Remove(record.ServerGuid);
ResolveResult placement = Resolve(pending);
if (!placement.Ok)
RollBackDeferredPlacement(pending);
else
CommitResolved(pending, placement);
return;
}
}
private static RollbackPlacement CaptureRollback(
ILiveEntityRemotePlacementRuntime remote,
PhysicsBody body)
{
return new RollbackPlacement(
body.Position,
body.CellPosition.ObjCellId,
body.CellPosition.Frame.Origin,
body.Orientation,
body.TransientState,
body.ContactPlane,
body.ContactPlaneValid,
body.ContactPlaneCellId,
body.ContactPlaneIsWater,
body.LastUpdateTime,
remote.Airborne,
remote.LastServerPosition,
remote.LastServerPositionTime,
remote.LastShadowSyncPosition);
}
private void RollBackDeferredPlacement(PendingPlacement pending)
{
RollbackPlacement rollback = pending.Rollback;
PhysicsBody body = pending.Body;
body.Orientation = rollback.Orientation;
body.SnapToCell(
rollback.CellId,
rollback.Position,
rollback.CellLocalPosition);
body.TransientState = rollback.TransientState;
body.ContactPlane = rollback.ContactPlane;
body.ContactPlaneValid = rollback.ContactPlaneValid;
body.ContactPlaneCellId = rollback.ContactPlaneCellId;
body.ContactPlaneIsWater = rollback.ContactPlaneIsWater;
body.LastUpdateTime = rollback.LastUpdateTime;
body.calc_acceleration();
pending.Remote.Airborne = rollback.Airborne;
pending.Remote.CellId = rollback.CellId;
pending.Remote.LastServerPosition = rollback.LastServerPosition;
pending.Remote.LastServerPositionTime = rollback.LastServerPositionTime;
pending.Remote.LastShadowSyncPosition = rollback.LastShadowSyncPosition;
pending.Entity.SetPosition(rollback.Position);
pending.Entity.ParentCellId = rollback.CellId;
pending.Entity.Rotation = rollback.Orientation;
if (rollback.CellId == 0)
_liveEntities.WithdrawLiveEntityProjectionToCellless(pending.Entity.ServerGuid);
else
_liveEntities.RebucketLiveEntity(pending.Entity.ServerGuid, rollback.CellId);
if (!_liveEntities.TryGetRecord(
pending.Entity.ServerGuid,
out LiveEntityRecord restored)
|| restored.Generation != pending.Generation)
{
return;
}
if (rollback.CellId == 0)
{
_completeAuthoritativePlacement(
pending.Entity.ServerGuid,
pending.Generation,
false);
return;
}
bool deferShadowRestore =
restored.FinalPhysicsState.HasFlag(PhysicsStateFlags.Hidden)
|| !restored.IsSpatiallyVisible;
_completeAuthoritativePlacement(
pending.Entity.ServerGuid,
pending.Generation,
deferShadowRestore);
if (deferShadowRestore)
return;
pending.Remote.LastShadowSyncPosition = Vector3.Zero;
_syncResolvedShadow(pending.Entity, pending.Body, rollback.CellId);
}
private static bool IsPlayerGuid(uint guid) =>
(guid & 0xFF000000u) == 0x50000000u;
}

View file

@ -0,0 +1,39 @@
using AcDream.Core.Physics;
namespace AcDream.App.Physics;
/// <summary>
/// Ordered App seam for retail <c>CPhysicsObj::teleport_hook</c>
/// (<c>0x00514ED0</c>). The action bundle keeps the port independently
/// testable while the concrete movement/interpolation/target owners remain
/// in the composition root.
/// </summary>
public static class RemoteTeleportHook
{
private const WeenieError TeleportCancelContext = (WeenieError)0x3Cu;
public static void Execute(RemoteTeleportHookActions actions)
{
ArgumentNullException.ThrowIfNull(actions.CancelMoveTo);
ArgumentNullException.ThrowIfNull(actions.UnStick);
ArgumentNullException.ThrowIfNull(actions.StopInterpolating);
ArgumentNullException.ThrowIfNull(actions.UnConstrain);
ArgumentNullException.ThrowIfNull(actions.NotifyTeleported);
ArgumentNullException.ThrowIfNull(actions.ReportCollisionEnd);
actions.CancelMoveTo(TeleportCancelContext);
actions.UnStick();
actions.StopInterpolating();
actions.UnConstrain();
actions.NotifyTeleported();
actions.ReportCollisionEnd();
}
}
public sealed record RemoteTeleportHookActions(
Action<WeenieError> CancelMoveTo,
Action UnStick,
Action StopInterpolating,
Action UnConstrain,
Action NotifyTeleported,
Action ReportCollisionEnd);

View file

@ -0,0 +1,77 @@
using System.Numerics;
using AcDream.App.World;
using AcDream.Core.Physics;
namespace AcDream.App.Physics;
/// <summary>
/// Commits the placement half of retail <c>CPhysicsObj::MoveOrTeleport</c>
/// Branch A (<c>0x00516330</c>). The caller runs
/// <see cref="RemoteTeleportHook"/> first, then this exact frame/cell snap,
/// before considering contact-driven interpolation.
/// </summary>
internal static class RemoteTeleportPlacement
{
internal static void Apply(
ILiveEntityRemotePlacementRuntime remote,
PhysicsBody body,
ResolveResult placement,
Vector3 cellLocalPosition,
Quaternion orientation,
double gameTime,
bool previousContact,
bool previousOnWalkable)
{
ArgumentNullException.ThrowIfNull(remote);
ArgumentNullException.ThrowIfNull(body);
if (!placement.Ok
|| !IsFinite(placement.Position)
|| !IsFinite(cellLocalPosition)
|| !PositionFrameValidation.IsValid(
placement.CellId,
cellLocalPosition,
orientation)
|| !double.IsFinite(gameTime))
{
throw new ArgumentOutOfRangeException(
nameof(placement),
"A remote teleport placement requires an accepted finite frame, clock, and nonzero cell.");
}
body.Orientation = orientation;
body.SnapToCell(placement.CellId, placement.Position, cellLocalPosition);
body.LastUpdateTime = gameTime;
body.ContactPlaneValid = placement.InContact;
if (placement.InContact)
{
body.ContactPlane = placement.ContactPlane;
body.ContactPlaneCellId = placement.ContactPlaneCellId;
body.ContactPlaneIsWater = placement.ContactPlaneIsWater;
}
else
{
body.ContactPlaneCellId = 0u;
body.ContactPlaneIsWater = false;
}
PhysicsObjUpdate.CommitSetPositionTransition(
body,
placement.InContact,
placement.OnWalkable,
placement.CollisionNormalValid,
placement.CollisionNormal,
previousContact,
previousOnWalkable,
remote.HitGround,
remote.LeaveGround);
remote.Airborne = !body.OnWalkable;
}
private static bool IsFinite(Vector3 value) =>
float.IsFinite(value.X)
&& float.IsFinite(value.Y)
&& float.IsFinite(value.Z);
}