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>
This commit is contained in:
Erik 2026-07-22 02:50:15 +02:00
parent c557038353
commit eeb0f6b45c
29 changed files with 3311 additions and 1073 deletions

View file

@ -193,6 +193,36 @@ public sealed class EntityPhysicsHost : IPhysicsObjHost
return existing;
}
/// <summary>
/// Validates an exact incarnation and returns the manager-stable host that
/// a later <see cref="InstallOrRebind"/> will publish, without changing any
/// live delegates. Player-mode construction uses this preparation edge so
/// DAT/physics/camera failures cannot expose a half-rebound host.
/// </summary>
internal static EntityPhysicsHost SelectStableHostWithoutRebind(
LiveEntityRuntime runtime,
LiveEntityRecord expectedRecord,
EntityPhysicsHost configuration)
{
ArgumentNullException.ThrowIfNull(runtime);
ArgumentNullException.ThrowIfNull(expectedRecord);
ArgumentNullException.ThrowIfNull(configuration);
if (!runtime.TryGetRecord(expectedRecord.ServerGuid, out LiveEntityRecord current)
|| !ReferenceEquals(current, expectedRecord))
{
throw new InvalidOperationException(
$"Live entity 0x{expectedRecord.ServerGuid:X8} changed incarnation during physics-host preparation.");
}
return current.PhysicsHost switch
{
null => configuration,
EntityPhysicsHost existing => existing,
_ => throw new InvalidOperationException(
$"Live entity 0x{expectedRecord.ServerGuid:X8} owns an incompatible physics-host implementation."),
};
}
/// <summary>
/// Builds the position-only CPhysicsObj facade used before an animated or
/// player movement owner exists. The delegates capture the exact live

View file

@ -48,8 +48,8 @@ internal sealed class LiveEntityNetworkUpdateController
private readonly IAnimationLoader _animLoader;
private readonly CombatTargetController? _combatTargetController;
private readonly LiveWorldOriginState _origin;
private readonly AcDream.App.Streaming.TeleportTransitCoordinator<
WorldSession.EntityPositionUpdate> _teleportTransit;
private readonly AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink
_localPlayerTeleport;
private readonly Func<PlayerMovementController?> _playerControllerSource;
private readonly LocalPlayerOutboundController _localPlayerOutbound;
private readonly Func<EntityPhysicsHost?> _playerHostSource;
@ -57,7 +57,6 @@ internal sealed class LiveEntityNetworkUpdateController
private readonly IPhysicsScriptTimeSource _gameTime;
private readonly Func<WorldSession?> _session;
private readonly LiveEntityInboundAuthorityGate _authorityGate;
private readonly Action<WorldSession.EntityPositionUpdate> _aimTeleportDestination;
private readonly IMovementTruthDiagnosticSink _movementTruthDiagnostics;
private PlayerMovementController? _playerController => _playerControllerSource();
@ -96,7 +95,7 @@ internal sealed class LiveEntityNetworkUpdateController
IAnimationLoader animLoader,
CombatTargetController? combatTargetController,
LiveWorldOriginState origin,
AcDream.App.Streaming.TeleportTransitCoordinator<WorldSession.EntityPositionUpdate> teleportTransit,
AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink localPlayerTeleport,
Func<PlayerMovementController?> playerControllerSource,
LocalPlayerOutboundController localPlayerOutbound,
Func<EntityPhysicsHost?> playerHostSource,
@ -104,7 +103,6 @@ internal sealed class LiveEntityNetworkUpdateController
IPhysicsScriptTimeSource gameTime,
Func<WorldSession?> session,
Action<uint, AcceptedPhysicsTimestamps> publishTimestamps,
Action<WorldSession.EntityPositionUpdate> aimTeleportDestination,
IMovementTruthDiagnosticSink movementTruthDiagnostics)
{
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
@ -127,7 +125,8 @@ internal sealed class LiveEntityNetworkUpdateController
_animLoader = animLoader ?? throw new ArgumentNullException(nameof(animLoader));
_combatTargetController = combatTargetController;
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
_teleportTransit = teleportTransit ?? throw new ArgumentNullException(nameof(teleportTransit));
_localPlayerTeleport = localPlayerTeleport
?? throw new ArgumentNullException(nameof(localPlayerTeleport));
_playerControllerSource = playerControllerSource ?? throw new ArgumentNullException(nameof(playerControllerSource));
_localPlayerOutbound = localPlayerOutbound
?? throw new ArgumentNullException(nameof(localPlayerOutbound));
@ -138,7 +137,6 @@ internal sealed class LiveEntityNetworkUpdateController
_authorityGate = new LiveEntityInboundAuthorityGate(
liveEntities,
publishTimestamps);
_aimTeleportDestination = aimTeleportDestination ?? throw new ArgumentNullException(nameof(aimTeleportDestination));
_movementTruthDiagnostics = movementTruthDiagnostics
?? throw new ArgumentNullException(nameof(movementTruthDiagnostics));
}
@ -1776,14 +1774,11 @@ internal sealed class LiveEntityNetworkUpdateController
// packet first. The presentation coordinator exposes exactly one
// sequence-correlated destination without reordering that state.
if (timestampDisposition is AcDream.Core.Physics.PositionTimestampDisposition.Apply
&& update.Guid == _playerServerGuid
&& _teleportTransit.OfferDestination(
update.TeleportSequence,
timestamps.TeleportAdvanced,
update,
out var teleportDestination))
&& update.Guid == _playerServerGuid)
{
_aimTeleportDestination(teleportDestination);
_localPlayerTeleport.OfferDestination(
update,
timestamps.TeleportAdvanced);
}
}

View file

@ -0,0 +1,117 @@
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();
}