acdream/src/AcDream.App/Physics/LocalPlayerShadowSynchronizer.cs
Erik eeb0f6b45c refactor(runtime): extract local teleport and player mode
Move local teleport, player-mode, animation, shadow, and sealed-dungeon lifetimes out of GameWindow. Make player-mode entry transactional and isolate approach completions by controller lifetime and approach generation so stale callbacks cannot affect replacements.

Co-authored-by: Codex <noreply@openai.com>
2026-07-22 02:50:15 +02:00

117 lines
3.8 KiB
C#

using AcDream.App.Input;
using AcDream.App.World;
using AcDream.Core.Physics;
using AcDream.Core.World;
namespace AcDream.App.Physics;
/// <summary>
/// Owns publication and suspension of the local player's collision shadow.
/// Identity/incarnation checks stay beside the cached shadow pose so player
/// mode, projection, and teleport placement cannot publish different owners.
/// </summary>
internal sealed class LocalPlayerShadowSynchronizer
{
private readonly PhysicsEngine _physics;
private readonly LiveEntityRuntime _liveEntities;
private readonly ILocalPlayerIdentitySource _identity;
private readonly LiveWorldOriginState _origin;
private readonly LocalPlayerShadowState _state;
public LocalPlayerShadowSynchronizer(
PhysicsEngine physics,
LiveEntityRuntime liveEntities,
ILocalPlayerIdentitySource identity,
LiveWorldOriginState origin,
LocalPlayerShadowState state)
{
_physics = physics ?? throw new ArgumentNullException(nameof(physics));
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
_state = state ?? throw new ArgumentNullException(nameof(state));
}
public void Sync(WorldEntity playerEntity, uint cellId, bool force = false)
=> SyncPose(
playerEntity,
playerEntity.Position,
playerEntity.Rotation,
cellId,
force);
public LocalPlayerShadowState.Snapshot? Capture() => _state.Current;
public void SyncPose(
WorldEntity playerEntity,
System.Numerics.Vector3 position,
System.Numerics.Quaternion orientation,
uint cellId,
bool force = false)
{
ArgumentNullException.ThrowIfNull(playerEntity);
if (_liveEntities.IsHidden(_identity.ServerGuid)
|| cellId == 0
|| !IsCurrentVisibleProjection(playerEntity))
{
Suspend(playerEntity);
return;
}
if (!force
&& _state.Current is { } last
&& last.CellId == cellId
&& System.Numerics.Vector3.DistanceSquared(
last.Position,
position) <= 1e-4f
&& MathF.Abs(System.Numerics.Quaternion.Dot(
last.Orientation,
orientation)) >= 0.99999f)
{
return;
}
ShadowPositionSynchronizer.Sync(
_physics.ShadowObjects,
playerEntity.Id,
position,
orientation,
cellId,
_origin.CenterX,
_origin.CenterY);
_state.Set(position, orientation, cellId);
}
public void Restore(
WorldEntity playerEntity,
LocalPlayerShadowState.Snapshot? snapshot)
{
ArgumentNullException.ThrowIfNull(playerEntity);
if (snapshot is not { } prior || !IsCurrentVisibleProjection(playerEntity))
{
Suspend(playerEntity);
return;
}
SyncPose(
playerEntity,
prior.Position,
prior.Orientation,
prior.CellId,
force: true);
}
public bool IsCurrentVisibleProjection(WorldEntity playerEntity) =>
_liveEntities.TryGetRecord(playerEntity.ServerGuid, out LiveEntityRecord record)
&& ReferenceEquals(record.WorldEntity, playerEntity)
&& _liveEntities.IsCurrentSpatialRootObject(record);
public void Suspend(WorldEntity playerEntity)
{
ArgumentNullException.ThrowIfNull(playerEntity);
_physics.ShadowObjects.Suspend(playerEntity.Id);
_state.Clear();
}
public void ResetSession() => _state.Clear();
}