using System.Numerics; using AcDream.App.Rendering; using AcDream.App.World; using AcDream.Core.Physics; using AcDream.Core.World; namespace AcDream.App.Physics; /// /// Owns the SetPosition half of retail remote /// CPhysicsObj::MoveOrTeleport (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. /// 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 _getSetupCylinder; private readonly Func _cellLocalForSeed; private readonly Action _syncResolvedShadow; private readonly Action _completeAuthoritativePlacement; private readonly Action _beginAuthoritativePlacement; private readonly PlacementResolver _resolvePlacement; private readonly Dictionary _pending = new(); internal RemoteTeleportController( PhysicsEngine physics, LiveEntityRuntime liveEntities, Func getSetupCylinder, Func cellLocalForSeed, Action syncResolvedShadow, Action completeAuthoritativePlacement, Action 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, bool Superseded = false); private readonly record struct PendingPlacement( LiveEntityRecord Record, ulong PositionAuthorityVersion, ulong VelocityAuthorityVersion, 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, Quaternion LastShadowSyncOrientation); /// /// Applies a placement to whichever exact incarnation is current at this /// call boundary. Packet handlers should use the token-bearing overload; /// this form serves non-wire orchestration and retains the same callback /// revalidation once admitted. /// internal Result TryApply( ILiveEntityRemotePlacementRuntime remote, WorldEntity entity, Vector3 requestedWorldPosition, uint requestedCellId, Vector3 requestedCellLocalPosition, Quaternion requestedOrientation, double gameTime, bool destinationProjectionVisible, ushort generation, ushort positionSequence) { if (!_liveEntities.TryGetRecord(entity.ServerGuid, out LiveEntityRecord record)) { throw new InvalidOperationException( $"Remote placement for 0x{entity.ServerGuid:X8} requires a live incarnation."); } return TryApply( record, record.PositionAuthorityVersion, record.VelocityAuthorityVersion, remote, entity, requestedWorldPosition, requestedCellId, requestedCellLocalPosition, requestedOrientation, gameTime, destinationProjectionVisible, generation, positionSequence); } internal Result TryApply( LiveEntityRecord expectedRecord, ulong expectedPositionAuthorityVersion, ulong expectedVelocityAuthorityVersion, 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); ArgumentNullException.ThrowIfNull(expectedRecord); if (!_liveEntities.TryGetRecord(entity.ServerGuid, out LiveEntityRecord liveRecord) || !ReferenceEquals(liveRecord, expectedRecord) || !_liveEntities.IsCurrentPositionAuthority( expectedRecord, expectedPositionAuthorityVersion) || liveRecord.Generation != generation || !ReferenceEquals(liveRecord.WorldEntity, entity) || !ReferenceEquals(liveRecord.RemoteMotionRuntime, remote) || 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) && ReferenceEquals(prior.Record, liveRecord) && prior.Generation == generation) { wasInContact = prior.WasInContact; wasOnWalkable = prior.WasOnWalkable; rollback = prior.Rollback; } PendingPlacement request = new( liveRecord, expectedPositionAuthorityVersion, expectedVelocityAuthorityVersion, remote, liveRecord.PhysicsBody, entity, requestedWorldPosition, requestedCellId, requestedOrientation, gameTime, generation, positionSequence, wasInContact, wasOnWalkable, rollback); if (!destinationProjectionVisible) { if (!ParkPending(request, requestedCellLocalPosition)) return Superseded(request); return new Result( true, false, requestedWorldPosition, requestedCellId, requestedOrientation); } ResolveResult placement = Resolve(request); if (!IsCurrent(request)) return Superseded(request); if (!placement.Ok) { _pending.Remove(entity.ServerGuid); if (!RollBackDeferredPlacement(request)) return Superseded(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 Forget(LiveEntityRecord record) { ArgumentNullException.ThrowIfNull(record); if (_pending.TryGetValue(record.ServerGuid, out PendingPlacement pending) && pending.Generation == record.Generation && (record.WorldEntity is null || ReferenceEquals(pending.Entity, record.WorldEntity))) { _pending.Remove(record.ServerGuid); } } internal void Clear() { _pending.Clear(); } internal bool HasPending(uint serverGuid) => _pending.ContainsKey(serverGuid); internal int PendingPlacementCount => _pending.Count; /// /// Transfers any older deferred shadow restoration before the accepted /// destination is rebucketed and can publish a visibility edge. /// 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) { if (!IsCurrent(request)) return Superseded(request); if (!RemoteTeleportPlacement.Apply( request.Remote, request.Body, placement, _cellLocalForSeed(placement.Position, placement.CellId), request.RequestedOrientation, request.GameTime, request.WasInContact, request.WasOnWalkable, () => IsCurrent(request), () => IsVelocityCurrent(request))) { return Superseded(request); } if (!IsCurrent(request)) return Superseded(request); if (request.Remote.CellId != placement.CellId) request.Remote.CellId = placement.CellId; if (!IsCurrent(request)) return Superseded(request); request.Remote.LastServerPosition = placement.Position; request.Remote.LastServerPositionTime = request.GameTime; request.Remote.LastShadowSyncPosition = Vector3.Zero; request.Remote.LastShadowSyncOrientation = Quaternion.Zero; request.Entity.SetPosition(request.Body.Position); request.Entity.ParentCellId = placement.CellId; request.Entity.Rotation = request.Body.Orientation; if (!IsCurrent(request)) return Superseded(request); if (_liveEntities.TryGetRecord(request.Entity.ServerGuid, out LiveEntityRecord record) && ReferenceEquals(record, request.Record)) { bool deferShadowRestore = record.FinalPhysicsState.HasFlag(PhysicsStateFlags.Hidden) || !record.IsSpatiallyVisible; _completeAuthoritativePlacement( request.Entity.ServerGuid, request.Generation, deferShadowRestore); if (!IsCurrent(request)) return Superseded(request); if (!deferShadowRestore) { request.Remote.LastShadowSyncPosition = request.Body.Position; request.Remote.LastShadowSyncOrientation = request.Body.Orientation; _syncResolvedShadow(request.Entity, request.Body, placement.CellId); if (!IsCurrent(request)) return Superseded(request); } } return new Result( true, true, request.Body.Position, placement.CellId, request.Body.Orientation); } private bool ParkPending(PendingPlacement request, Vector3 requestedCellLocalPosition) { if (!IsCurrent(request)) return false; 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; if (!IsCurrent(request)) return false; request.Remote.LastServerPosition = request.RequestedWorldPosition; request.Remote.LastServerPositionTime = request.GameTime; request.Remote.LastShadowSyncPosition = Vector3.Zero; request.Remote.LastShadowSyncOrientation = Quaternion.Zero; request.Entity.SetPosition(request.RequestedWorldPosition); request.Entity.ParentCellId = request.RequestedCellId; request.Entity.Rotation = request.RequestedOrientation; if (!IsCurrent(request)) return false; _pending[request.Entity.ServerGuid] = request; return true; } 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, pending.Record) || !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 (!_liveEntities.IsCurrentPositionAuthority( pending.Record, pending.PositionAuthorityVersion) || 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 (!IsCurrent(pending)) return; 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, remote.LastShadowSyncOrientation); } private bool RollBackDeferredPlacement(PendingPlacement pending) { if (!IsCurrentAuthority(pending)) return false; 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.Remote.LastShadowSyncOrientation = rollback.LastShadowSyncOrientation; pending.Entity.SetPosition(rollback.Position); pending.Entity.ParentCellId = rollback.CellId; pending.Entity.Rotation = rollback.Orientation; if (!IsCurrentAuthority(pending)) return false; if (rollback.CellId == 0) _liveEntities.WithdrawLiveEntityProjectionToCellless(pending.Entity.ServerGuid); else _liveEntities.RebucketLiveEntity(pending.Entity.ServerGuid, rollback.CellId); if (!IsCurrentAuthority(pending) || !_liveEntities.TryGetRecord( pending.Entity.ServerGuid, out LiveEntityRecord restored) || !ReferenceEquals(restored, pending.Record) || restored.Generation != pending.Generation) { return false; } if (rollback.CellId == 0) { _completeAuthoritativePlacement( pending.Entity.ServerGuid, pending.Generation, false); return IsCurrentAuthority(pending); } bool deferShadowRestore = restored.FinalPhysicsState.HasFlag(PhysicsStateFlags.Hidden) || !restored.IsSpatiallyVisible; _completeAuthoritativePlacement( pending.Entity.ServerGuid, pending.Generation, deferShadowRestore); if (!IsCurrentAuthority(pending)) return false; if (deferShadowRestore) return true; pending.Remote.LastShadowSyncPosition = Vector3.Zero; pending.Remote.LastShadowSyncOrientation = Quaternion.Zero; _syncResolvedShadow(pending.Entity, pending.Body, rollback.CellId); return IsCurrentAuthority(pending); } private bool IsCurrentAuthority(PendingPlacement request) => _liveEntities.IsCurrentPositionAuthority( request.Record, request.PositionAuthorityVersion) && request.Record.Generation == request.Generation && ReferenceEquals(request.Record.WorldEntity, request.Entity) && ReferenceEquals(request.Record.PhysicsBody, request.Body); private bool IsCurrent(PendingPlacement request) => IsCurrentAuthority(request) && ReferenceEquals(request.Record.RemoteMotionRuntime, request.Remote); private bool IsVelocityCurrent(PendingPlacement request) => _liveEntities.IsCurrentVelocityAuthority( request.Record, request.VelocityAuthorityVersion); private static Result Superseded(PendingPlacement request) => new( Applied: false, ContactResolved: false, Position: request.Body.Position, CellId: request.Remote.CellId, Orientation: request.Body.Orientation, Superseded: true); private static bool IsPlayerGuid(uint guid) => (guid & 0xFF000000u) == 0x50000000u; }