refactor(update): cut over the frame orchestrator
Reduce GameWindow.OnUpdate to one typed orchestration call and remove transitive window callbacks from teardown, liveness, teleport placement, live presentation, auto-entry, and entity packet routing. Preserve the frozen retail object/network order while making the production owner graph explicit and testable.
This commit is contained in:
parent
947c61d2d7
commit
e91f310279
18 changed files with 864 additions and 348 deletions
|
|
@ -1,11 +1,14 @@
|
|||
using System;
|
||||
using AcDream.App.Net;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
|
||||
namespace AcDream.App.Input;
|
||||
|
||||
/// <summary>
|
||||
/// Phase K.2 — one-shot guard that auto-enters player mode after a
|
||||
/// successful login once every prerequisite is satisfied. Owned by
|
||||
/// <c>GameWindow</c> and ticked each frame from <c>OnUpdate</c>.
|
||||
/// successful login once every prerequisite is satisfied. The update-frame
|
||||
/// orchestrator ticks it through a typed production context.
|
||||
///
|
||||
/// <para>
|
||||
/// Why is this its own class? The auto-entry has four independent
|
||||
|
|
@ -33,20 +36,108 @@ namespace AcDream.App.Input;
|
|||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// All preconditions are passed in as predicates so the class doesn't
|
||||
/// pull in <c>WorldSession</c>, <c>PlayerMovementController</c>, or
|
||||
/// any GameWindow-internal types — the unit test wires them to plain
|
||||
/// boolean fields.
|
||||
/// Production preconditions come from focused runtime owners rather than the
|
||||
/// window host. The public delegate constructor remains a narrow test seam for
|
||||
/// plain boolean fixtures.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal interface IPlayerModeAutoEntryContext
|
||||
{
|
||||
bool IsLiveInWorld { get; }
|
||||
bool IsPlayerEntityPresent { get; }
|
||||
bool IsPlayerControllerReady { get; }
|
||||
bool IsWorldReady { get; }
|
||||
bool IsPlayerModeActive { get; }
|
||||
void EnterPlayerMode();
|
||||
}
|
||||
|
||||
/// <summary>Production auto-entry context over canonical runtime owners.</summary>
|
||||
internal sealed class LivePlayerModeAutoEntryContext
|
||||
: IPlayerModeAutoEntryContext
|
||||
{
|
||||
private readonly ILiveInWorldSource _session;
|
||||
private readonly LiveEntityRuntime _liveEntities;
|
||||
private readonly ILocalPlayerIdentitySource _identity;
|
||||
private readonly WorldRevealCoordinator _worldReveal;
|
||||
private readonly ILocalPlayerModeSource _mode;
|
||||
private readonly PlayerModeController _playerMode;
|
||||
|
||||
public LivePlayerModeAutoEntryContext(
|
||||
ILiveInWorldSource session,
|
||||
LiveEntityRuntime liveEntities,
|
||||
ILocalPlayerIdentitySource identity,
|
||||
WorldRevealCoordinator worldReveal,
|
||||
ILocalPlayerModeSource mode,
|
||||
PlayerModeController playerMode)
|
||||
{
|
||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
|
||||
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
|
||||
_worldReveal = worldReveal ?? throw new ArgumentNullException(nameof(worldReveal));
|
||||
_mode = mode ?? throw new ArgumentNullException(nameof(mode));
|
||||
_playerMode = playerMode ?? throw new ArgumentNullException(nameof(playerMode));
|
||||
}
|
||||
|
||||
public bool IsLiveInWorld => _session.IsInWorld;
|
||||
|
||||
public bool IsPlayerEntityPresent =>
|
||||
_liveEntities.MaterializedWorldEntities.ContainsKey(_identity.ServerGuid);
|
||||
|
||||
public bool IsPlayerControllerReady => true;
|
||||
|
||||
public bool IsWorldReady =>
|
||||
_liveEntities.TryGetSnapshot(
|
||||
_identity.ServerGuid,
|
||||
out AcDream.Core.Net.WorldSession.EntitySpawn player)
|
||||
&& player.Position is { LandblockId: not 0u } position
|
||||
&& _worldReveal.Evaluate(position.LandblockId).IsReady;
|
||||
|
||||
public bool IsPlayerModeActive => _mode.IsPlayerMode;
|
||||
|
||||
public void EnterPlayerMode() => _playerMode.EnterFromAutoEntry();
|
||||
}
|
||||
|
||||
public sealed class PlayerModeAutoEntry
|
||||
{
|
||||
private readonly Func<bool> _isLiveInWorld;
|
||||
private readonly Func<bool> _isPlayerEntityPresent;
|
||||
private readonly Func<bool> _isPlayerControllerReady;
|
||||
private readonly Func<bool> _isWorldReady;
|
||||
private readonly Func<bool> _isPlayerModeActive;
|
||||
private readonly Action _enterPlayerMode;
|
||||
private sealed class DelegateContext : IPlayerModeAutoEntryContext
|
||||
{
|
||||
private readonly Func<bool> _isLiveInWorld;
|
||||
private readonly Func<bool> _isPlayerEntityPresent;
|
||||
private readonly Func<bool> _isPlayerControllerReady;
|
||||
private readonly Func<bool> _isWorldReady;
|
||||
private readonly Action _enterPlayerMode;
|
||||
private readonly Func<bool> _isPlayerModeActive;
|
||||
|
||||
public DelegateContext(
|
||||
Func<bool> isLiveInWorld,
|
||||
Func<bool> isPlayerEntityPresent,
|
||||
Func<bool> isPlayerControllerReady,
|
||||
Func<bool> isWorldReady,
|
||||
Action enterPlayerMode,
|
||||
Func<bool>? isPlayerModeActive)
|
||||
{
|
||||
_isLiveInWorld = isLiveInWorld
|
||||
?? throw new ArgumentNullException(nameof(isLiveInWorld));
|
||||
_isPlayerEntityPresent = isPlayerEntityPresent
|
||||
?? throw new ArgumentNullException(nameof(isPlayerEntityPresent));
|
||||
_isPlayerControllerReady = isPlayerControllerReady
|
||||
?? throw new ArgumentNullException(nameof(isPlayerControllerReady));
|
||||
_isWorldReady = isWorldReady
|
||||
?? throw new ArgumentNullException(nameof(isWorldReady));
|
||||
_enterPlayerMode = enterPlayerMode
|
||||
?? throw new ArgumentNullException(nameof(enterPlayerMode));
|
||||
_isPlayerModeActive = isPlayerModeActive ?? (() => false);
|
||||
}
|
||||
|
||||
public bool IsLiveInWorld => _isLiveInWorld();
|
||||
public bool IsPlayerEntityPresent => _isPlayerEntityPresent();
|
||||
public bool IsPlayerControllerReady => _isPlayerControllerReady();
|
||||
public bool IsWorldReady => _isWorldReady();
|
||||
public bool IsPlayerModeActive => _isPlayerModeActive();
|
||||
public void EnterPlayerMode() => _enterPlayerMode();
|
||||
}
|
||||
|
||||
private readonly IPlayerModeAutoEntryContext _context;
|
||||
|
||||
private bool _armed;
|
||||
|
||||
|
|
@ -72,20 +163,24 @@ public sealed class PlayerModeAutoEntry
|
|||
/// player transition). Must construct the controller + chase
|
||||
/// camera and switch the active camera; the auto-entry doesn't
|
||||
/// reach inside.</param>
|
||||
internal PlayerModeAutoEntry(IPlayerModeAutoEntryContext context) =>
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
|
||||
public PlayerModeAutoEntry(
|
||||
Func<bool> isLiveInWorld,
|
||||
Func<bool> isPlayerEntityPresent,
|
||||
Func<bool> isPlayerControllerReady,
|
||||
Func<bool> isWorldReady,
|
||||
Action enterPlayerMode,
|
||||
Action enterPlayerMode,
|
||||
Func<bool>? isPlayerModeActive = null)
|
||||
: this(new DelegateContext(
|
||||
isLiveInWorld,
|
||||
isPlayerEntityPresent,
|
||||
isPlayerControllerReady,
|
||||
isWorldReady,
|
||||
enterPlayerMode,
|
||||
isPlayerModeActive))
|
||||
{
|
||||
_isLiveInWorld = isLiveInWorld ?? throw new ArgumentNullException(nameof(isLiveInWorld));
|
||||
_isPlayerEntityPresent = isPlayerEntityPresent ?? throw new ArgumentNullException(nameof(isPlayerEntityPresent));
|
||||
_isPlayerControllerReady = isPlayerControllerReady ?? throw new ArgumentNullException(nameof(isPlayerControllerReady));
|
||||
_isWorldReady = isWorldReady ?? throw new ArgumentNullException(nameof(isWorldReady));
|
||||
_enterPlayerMode = enterPlayerMode ?? throw new ArgumentNullException(nameof(enterPlayerMode));
|
||||
_isPlayerModeActive = isPlayerModeActive ?? (() => false);
|
||||
}
|
||||
|
||||
/// <summary>True iff <see cref="TryEnter"/> would still fire if the
|
||||
|
|
@ -115,18 +210,18 @@ public sealed class PlayerModeAutoEntry
|
|||
public bool TryEnter()
|
||||
{
|
||||
if (!_armed) return false;
|
||||
if (_isPlayerModeActive())
|
||||
if (_context.IsPlayerModeActive)
|
||||
{
|
||||
_armed = false;
|
||||
return false;
|
||||
}
|
||||
if (!_isLiveInWorld()) return false;
|
||||
if (!_isPlayerEntityPresent()) return false;
|
||||
if (!_isPlayerControllerReady()) return false;
|
||||
if (!_isWorldReady()) return false;
|
||||
if (!_context.IsLiveInWorld) return false;
|
||||
if (!_context.IsPlayerEntityPresent) return false;
|
||||
if (!_context.IsPlayerControllerReady) return false;
|
||||
if (!_context.IsWorldReady) return false;
|
||||
|
||||
_armed = false;
|
||||
_enterPlayerMode();
|
||||
_context.EnterPlayerMode();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
97
src/AcDream.App/Net/LiveEntitySessionController.cs
Normal file
97
src/AcDream.App/Net/LiveEntitySessionController.cs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
using AcDream.App.Physics;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
|
||||
namespace AcDream.App.Net;
|
||||
|
||||
/// <summary>
|
||||
/// Routes one live session's entity packets through the non-reentrant retail
|
||||
/// inbound envelope without retaining the window host.
|
||||
/// </summary>
|
||||
internal sealed class LiveEntitySessionController
|
||||
{
|
||||
private readonly RetailInboundEventDispatcher _inbound;
|
||||
private readonly LiveEntityHydrationController _hydration;
|
||||
private readonly LiveEntityNetworkUpdateController _updates;
|
||||
private readonly ILocalPlayerTeleportNetworkSink _teleport;
|
||||
private readonly EntityEffectController _effects;
|
||||
|
||||
public LiveEntitySessionController(
|
||||
RetailInboundEventDispatcher inbound,
|
||||
LiveEntityHydrationController hydration,
|
||||
LiveEntityNetworkUpdateController updates,
|
||||
ILocalPlayerTeleportNetworkSink teleport,
|
||||
EntityEffectController effects)
|
||||
{
|
||||
_inbound = inbound ?? throw new ArgumentNullException(nameof(inbound));
|
||||
_hydration = hydration ?? throw new ArgumentNullException(nameof(hydration));
|
||||
_updates = updates ?? throw new ArgumentNullException(nameof(updates));
|
||||
_teleport = teleport ?? throw new ArgumentNullException(nameof(teleport));
|
||||
_effects = effects ?? throw new ArgumentNullException(nameof(effects));
|
||||
}
|
||||
|
||||
public LiveEntitySessionSink CreateSink() => new(
|
||||
OnSpawned,
|
||||
OnDeleted,
|
||||
OnPickedUp,
|
||||
OnMotionUpdated,
|
||||
OnPositionUpdated,
|
||||
OnVectorUpdated,
|
||||
OnStateUpdated,
|
||||
OnParentUpdated,
|
||||
OnTeleportStarted,
|
||||
OnAppearanceUpdated,
|
||||
OnPlayPhysicsScript,
|
||||
OnPlayPhysicsScriptType);
|
||||
|
||||
private void OnSpawned(WorldSession.EntitySpawn value) =>
|
||||
_inbound.Run(_hydration, value,
|
||||
static (owner, message) => owner.OnCreate(message));
|
||||
|
||||
private void OnDeleted(DeleteObject.Parsed value) =>
|
||||
_inbound.Run(_hydration, value,
|
||||
static (owner, message) => owner.OnDelete(message));
|
||||
|
||||
private void OnPickedUp(PickupEvent.Parsed value) =>
|
||||
_inbound.Run(_hydration, value,
|
||||
static (owner, message) => owner.OnPickup(message));
|
||||
|
||||
private void OnMotionUpdated(WorldSession.EntityMotionUpdate value) =>
|
||||
_inbound.Run(_updates, value,
|
||||
static (owner, message) => owner.OnMotion(message));
|
||||
|
||||
private void OnPositionUpdated(WorldSession.EntityPositionUpdate value) =>
|
||||
_inbound.Run(_updates, value,
|
||||
static (owner, message) => owner.OnPosition(message));
|
||||
|
||||
private void OnVectorUpdated(VectorUpdate.Parsed value) =>
|
||||
_inbound.Run(_updates, value,
|
||||
static (owner, message) => owner.OnVector(message));
|
||||
|
||||
private void OnStateUpdated(SetState.Parsed value) =>
|
||||
_inbound.Run(_updates, value,
|
||||
static (owner, message) => owner.OnState(message));
|
||||
|
||||
private void OnParentUpdated(ParentEvent.Parsed value) =>
|
||||
_inbound.Run(_hydration, value,
|
||||
static (owner, message) => owner.OnParent(message));
|
||||
|
||||
private void OnTeleportStarted(uint value) =>
|
||||
_inbound.Run(_teleport, value,
|
||||
static (owner, message) => owner.OnTeleportStarted(message));
|
||||
|
||||
private void OnAppearanceUpdated(ObjDescEvent.Parsed value) =>
|
||||
_inbound.Run(_hydration, value,
|
||||
static (owner, message) => owner.OnAppearance(message));
|
||||
|
||||
private void OnPlayPhysicsScript(PlayPhysicsScript value) =>
|
||||
_inbound.Run(_effects, value,
|
||||
static (owner, message) => owner.HandleDirect(message));
|
||||
|
||||
private void OnPlayPhysicsScriptType(PlayPhysicsScriptType value) =>
|
||||
_inbound.Run(_effects, value,
|
||||
static (owner, message) => owner.HandleTyped(message));
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
using AcDream.App.Combat;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Interaction;
|
||||
using AcDream.App.Net;
|
||||
using AcDream.App.Physics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
|
|
@ -50,18 +51,18 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
private readonly LiveWorldOriginState _origin;
|
||||
private readonly AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink
|
||||
_localPlayerTeleport;
|
||||
private readonly Func<PlayerMovementController?> _playerControllerSource;
|
||||
private readonly ILocalPlayerControllerSource _playerControllerSource;
|
||||
private readonly LocalPlayerOutboundController _localPlayerOutbound;
|
||||
private readonly Func<EntityPhysicsHost?> _playerHostSource;
|
||||
private readonly Func<uint> _playerGuid;
|
||||
private readonly ILocalPlayerPhysicsHostSource _playerHostSource;
|
||||
private readonly ILocalPlayerIdentitySource _playerIdentity;
|
||||
private readonly IPhysicsScriptTimeSource _gameTime;
|
||||
private readonly Func<WorldSession?> _session;
|
||||
private readonly ILiveWorldSessionSource _session;
|
||||
private readonly LiveEntityInboundAuthorityGate _authorityGate;
|
||||
private readonly IMovementTruthDiagnosticSink _movementTruthDiagnostics;
|
||||
|
||||
private PlayerMovementController? _playerController => _playerControllerSource();
|
||||
private EntityPhysicsHost? _playerHost => _playerHostSource();
|
||||
private uint _playerServerGuid => _playerGuid();
|
||||
private PlayerMovementController? _playerController => _playerControllerSource.Controller;
|
||||
private EntityPhysicsHost? _playerHost => _playerHostSource.Host;
|
||||
private uint _playerServerGuid => _playerIdentity.ServerGuid;
|
||||
private double _physicsScriptGameTime => _gameTime.CurrentScriptTime;
|
||||
private IReadOnlyDictionary<uint, WorldEntity> _entitiesByServerGuid =>
|
||||
_liveEntities.MaterializedWorldEntities;
|
||||
|
|
@ -96,12 +97,12 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
CombatTargetController? combatTargetController,
|
||||
LiveWorldOriginState origin,
|
||||
AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink localPlayerTeleport,
|
||||
Func<PlayerMovementController?> playerControllerSource,
|
||||
ILocalPlayerControllerSource playerControllerSource,
|
||||
LocalPlayerOutboundController localPlayerOutbound,
|
||||
Func<EntityPhysicsHost?> playerHostSource,
|
||||
Func<uint> playerGuid,
|
||||
ILocalPlayerPhysicsHostSource playerHostSource,
|
||||
ILocalPlayerIdentitySource playerIdentity,
|
||||
IPhysicsScriptTimeSource gameTime,
|
||||
Func<WorldSession?> session,
|
||||
ILiveWorldSessionSource session,
|
||||
Action<uint, AcceptedPhysicsTimestamps> publishTimestamps,
|
||||
IMovementTruthDiagnosticSink movementTruthDiagnostics)
|
||||
{
|
||||
|
|
@ -131,7 +132,7 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
_localPlayerOutbound = localPlayerOutbound
|
||||
?? throw new ArgumentNullException(nameof(localPlayerOutbound));
|
||||
_playerHostSource = playerHostSource ?? throw new ArgumentNullException(nameof(playerHostSource));
|
||||
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
||||
_playerIdentity = playerIdentity ?? throw new ArgumentNullException(nameof(playerIdentity));
|
||||
_gameTime = gameTime ?? throw new ArgumentNullException(nameof(gameTime));
|
||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||
_authorityGate = new LiveEntityInboundAuthorityGate(
|
||||
|
|
@ -1029,7 +1030,7 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
p.PositionY,
|
||||
p.PositionZ)),
|
||||
() => _localPlayerOutbound.SendImmediatePosition(
|
||||
_session(),
|
||||
_session.CurrentSession,
|
||||
_playerController)))
|
||||
return;
|
||||
|
||||
|
|
|
|||
49
src/AcDream.App/Physics/RemoteShadowPlacementSynchronizer.cs
Normal file
49
src/AcDream.App/Physics/RemoteShadowPlacementSynchronizer.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
using AcDream.App.World;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Physics;
|
||||
|
||||
/// <summary>Synchronizes a resolved remote placement in the current live origin.</summary>
|
||||
internal sealed class RemoteShadowPlacementSynchronizer
|
||||
{
|
||||
private readonly RemotePhysicsUpdater _remotePhysics;
|
||||
private readonly LiveWorldOriginState _origin;
|
||||
|
||||
public RemoteShadowPlacementSynchronizer(
|
||||
RemotePhysicsUpdater remotePhysics,
|
||||
LiveWorldOriginState origin)
|
||||
{
|
||||
_remotePhysics = remotePhysics
|
||||
?? throw new ArgumentNullException(nameof(remotePhysics));
|
||||
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
|
||||
}
|
||||
|
||||
public void Sync(WorldEntity entity, PhysicsBody body, uint cellId) =>
|
||||
_remotePhysics.SyncRemoteShadowToBody(
|
||||
entity.Id,
|
||||
body,
|
||||
_origin.CenterX,
|
||||
_origin.CenterY,
|
||||
cellId);
|
||||
}
|
||||
|
||||
/// <summary>Bridges remote placement ownership to live presentation state.</summary>
|
||||
internal sealed class RemoteTeleportPlacementPresentation
|
||||
{
|
||||
private readonly LiveEntityPresentationController _presentation;
|
||||
|
||||
public RemoteTeleportPlacementPresentation(
|
||||
LiveEntityPresentationController presentation) =>
|
||||
_presentation = presentation
|
||||
?? throw new ArgumentNullException(nameof(presentation));
|
||||
|
||||
public void Complete(uint serverGuid, ushort generation, bool deferShadowRestore) =>
|
||||
_presentation.CompleteAuthoritativePlacement(
|
||||
serverGuid,
|
||||
generation,
|
||||
deferShadowRestore);
|
||||
|
||||
public void Begin(uint serverGuid, ushort generation) =>
|
||||
_presentation.BeginAuthoritativePlacement(serverGuid, generation);
|
||||
}
|
||||
|
|
@ -134,7 +134,6 @@ public sealed class GameWindow : IDisposable
|
|||
private AcDream.App.Streaming.StreamingController? _streamingController;
|
||||
private AcDream.App.Streaming.StreamingOriginRecenterCoordinator?
|
||||
_streamingOriginRecenter;
|
||||
private AcDream.App.Update.IStreamingFramePhase _streamingFrame = null!;
|
||||
private AcDream.App.Streaming.WorldRevealCoordinator? _worldReveal;
|
||||
private readonly AcDream.App.Streaming.DeferredLocalPlayerTeleportNetworkSink
|
||||
_localPlayerTeleportSink = new();
|
||||
|
|
@ -173,10 +172,7 @@ public sealed class GameWindow : IDisposable
|
|||
private readonly AcDream.App.Input.MovementTruthDiagnosticController
|
||||
_movementTruthDiagnostics;
|
||||
private readonly AcDream.App.Input.LocalPlayerOutboundController _localPlayerOutbound;
|
||||
private AcDream.App.Update.ICameraFramePhase _cameraFrame = null!;
|
||||
private AcDream.App.World.RetailLiveFrameCoordinator _liveFrameCoordinator = null!;
|
||||
private AcDream.App.Update.LiveSpatialPresentationReconciler
|
||||
_liveSpatialReconciler = null!;
|
||||
private AcDream.App.Update.UpdateFrameOrchestrator _updateFrameOrchestrator = null!;
|
||||
private readonly AcDream.App.Update.UpdateFrameClock _updateFrameClock = new();
|
||||
private AcDream.App.Physics.RemoteTeleportController? _remoteTeleportController;
|
||||
// Step 7 projectile presentation. The controller owns no identity map;
|
||||
|
|
@ -186,6 +182,7 @@ public sealed class GameWindow : IDisposable
|
|||
_liveEntityProjectionWithdrawal;
|
||||
private AcDream.App.World.LiveEntityHydrationController? _liveEntityHydration;
|
||||
private AcDream.App.Physics.LiveEntityNetworkUpdateController? _liveEntityNetworkUpdates;
|
||||
private AcDream.App.Net.LiveEntitySessionController _liveEntitySessionEvents = null!;
|
||||
private readonly AcDream.App.Physics.DeferredLiveEntityMotionRuntimeBindings
|
||||
_liveEntityMotionBindings = new();
|
||||
|
||||
|
|
@ -2170,36 +2167,32 @@ public sealed class GameWindow : IDisposable
|
|||
_entityEffects = entityEffects;
|
||||
entityEffects.DiagnosticSink = message =>
|
||||
Console.Error.WriteLine($"vfx: {message}");
|
||||
var partArrayLifecycle =
|
||||
new AcDream.App.Rendering.LiveEntityPartArrayLifecycle(
|
||||
_animatedEntities);
|
||||
_liveEntityPresentation = new AcDream.App.World.LiveEntityPresentationController(
|
||||
_liveEntities,
|
||||
_physicsEngine.ShadowObjects,
|
||||
entityEffects.PlayTypedFromHiddenTransition,
|
||||
_equippedChildRenderer.SetDirectChildrenNoDraw,
|
||||
_liveEntityMotionBindings.ClearTargetForHiddenEntity,
|
||||
() => (_liveCenterX, _liveCenterY),
|
||||
handlePartArrayEnterWorld: localEntityId =>
|
||||
{
|
||||
if (_animatedEntities.TryGetValue(localEntityId, out var animated))
|
||||
animated.Sequencer?.Manager.HandleEnterWorld();
|
||||
});
|
||||
_liveWorldOrigin.GetCenter,
|
||||
handlePartArrayEnterWorld: partArrayLifecycle.HandleEnterWorld);
|
||||
var remoteShadowPlacement =
|
||||
new AcDream.App.Physics.RemoteShadowPlacementSynchronizer(
|
||||
_remotePhysicsUpdater,
|
||||
_liveWorldOrigin);
|
||||
var remoteTeleportPresentation =
|
||||
new AcDream.App.Physics.RemoteTeleportPlacementPresentation(
|
||||
_liveEntityPresentation);
|
||||
_remoteTeleportController = new AcDream.App.Physics.RemoteTeleportController(
|
||||
_physicsEngine,
|
||||
_liveEntities,
|
||||
_liveEntityMotionBindings.GetSetupCylinder,
|
||||
CellLocalForSeed,
|
||||
(entity, remote, cellId) => _remotePhysicsUpdater.SyncRemoteShadowToBody(
|
||||
entity.Id,
|
||||
remote,
|
||||
_liveCenterX,
|
||||
_liveCenterY,
|
||||
cellId),
|
||||
(guid, generation, deferShadowRestore) =>
|
||||
_liveEntityPresentation.CompleteAuthoritativePlacement(
|
||||
guid,
|
||||
generation,
|
||||
deferShadowRestore),
|
||||
(guid, generation) =>
|
||||
_liveEntityPresentation.BeginAuthoritativePlacement(guid, generation));
|
||||
_liveWorldOrigin.CellLocalForSeed,
|
||||
remoteShadowPlacement.Sync,
|
||||
remoteTeleportPresentation.Complete,
|
||||
remoteTeleportPresentation.Begin);
|
||||
_equippedChildRenderer.ProjectionPoseReady += guid =>
|
||||
_liveEntityLights.OnAttachedPoseReady(guid);
|
||||
_hookRouter.Register(entityEffects);
|
||||
|
|
@ -2484,9 +2477,14 @@ public sealed class GameWindow : IDisposable
|
|||
_streamingController,
|
||||
_worldState,
|
||||
_worldReveal,
|
||||
() => _playerServerGuid,
|
||||
_localPlayerIdentity,
|
||||
sealedDungeonCells,
|
||||
Console.WriteLine);
|
||||
_liveSessionController = new AcDream.App.Net.LiveSessionController();
|
||||
var localPhysicsTimestamps =
|
||||
new AcDream.App.World.LiveSessionLocalPhysicsTimestampPublisher(
|
||||
_localPlayerIdentity,
|
||||
_liveSessionController);
|
||||
var liveEntityTeardown =
|
||||
new AcDream.App.World.LiveEntityRuntimeTeardownController(
|
||||
_liveEntities,
|
||||
|
|
@ -2503,8 +2501,15 @@ public sealed class GameWindow : IDisposable
|
|||
_physicsEngine.ShadowObjects,
|
||||
_liveEntityLights!,
|
||||
_classificationCache,
|
||||
() => _playerServerGuid);
|
||||
_localPlayerIdentity);
|
||||
liveEntityComponentLifecycle.Bind(liveEntityTeardown);
|
||||
var liveEntityDeletion =
|
||||
new AcDream.App.World.LiveEntityDeletionController(
|
||||
_liveEntities,
|
||||
Objects,
|
||||
liveEntityTeardown,
|
||||
_localPlayerIdentity,
|
||||
_options.DumpLiveSpawns ? Console.WriteLine : null);
|
||||
_liveEntityHydration = new AcDream.App.World.LiveEntityHydrationController(
|
||||
_liveEntities,
|
||||
Objects,
|
||||
|
|
@ -2518,9 +2523,9 @@ public sealed class GameWindow : IDisposable
|
|||
_liveEntityPresentation!),
|
||||
originCoordinator,
|
||||
networkUpdateBridge,
|
||||
liveEntityTeardown,
|
||||
PublishLocalPhysicsTimestamps,
|
||||
() => _playerServerGuid,
|
||||
localPhysicsTimestamps,
|
||||
_localPlayerIdentity,
|
||||
liveEntityDeletion,
|
||||
_options.DumpLiveSpawns ? Console.WriteLine : null);
|
||||
_liveEntityNetworkUpdates =
|
||||
new AcDream.App.Physics.LiveEntityNetworkUpdateController(
|
||||
|
|
@ -2546,18 +2551,24 @@ public sealed class GameWindow : IDisposable
|
|||
_combatTargetController,
|
||||
_liveWorldOrigin,
|
||||
_localPlayerTeleportSink,
|
||||
() => _playerController,
|
||||
_playerControllerSlot,
|
||||
_localPlayerOutbound,
|
||||
() => _playerHost,
|
||||
() => _playerServerGuid,
|
||||
_playerHostSlot,
|
||||
_localPlayerIdentity,
|
||||
_updateFrameClock,
|
||||
() => LiveSession,
|
||||
PublishLocalPhysicsTimestamps,
|
||||
_liveSessionController,
|
||||
localPhysicsTimestamps.Publish,
|
||||
_movementTruthDiagnostics);
|
||||
_liveEntityLiveness = new AcDream.App.World.LiveEntityLivenessController(
|
||||
_liveEntities,
|
||||
() => _playerServerGuid,
|
||||
candidate => _liveEntityHydration.OnPrune(candidate));
|
||||
_localPlayerIdentity,
|
||||
liveEntityDeletion);
|
||||
_liveEntitySessionEvents = new AcDream.App.Net.LiveEntitySessionController(
|
||||
_inboundEntityEvents,
|
||||
_liveEntityHydration,
|
||||
_liveEntityNetworkUpdates,
|
||||
_localPlayerTeleportSink,
|
||||
_entityEffects!);
|
||||
parentAcceptance!.Bind(_liveEntityHydration.TryAcceptParentForProjection);
|
||||
networkUpdateBridge.Bind(_liveEntityNetworkUpdates);
|
||||
_equippedChildRenderer.EntityReady += candidate =>
|
||||
|
|
@ -2573,7 +2584,6 @@ public sealed class GameWindow : IDisposable
|
|||
// CreateObject messages into _worldGameState as they arrive. Entirely
|
||||
// gated behind ACDREAM_LIVE=1 so the default run path is unchanged.
|
||||
_liveWorldOrigin.SetPlaceholder(centerX, centerY);
|
||||
_liveSessionController = new AcDream.App.Net.LiveSessionController();
|
||||
_combatAttackOperations.Bind(
|
||||
new AcDream.App.Combat.LiveCombatAttackOperations(
|
||||
Combat,
|
||||
|
|
@ -2609,7 +2619,7 @@ public sealed class GameWindow : IDisposable
|
|||
mouseLookController,
|
||||
new AcDream.App.Input.CombatAttackInputFrameAdapter(
|
||||
_combatAttackController!));
|
||||
_streamingFrame = new AcDream.App.Streaming.StreamingFrameController(
|
||||
var streamingFrame = new AcDream.App.Streaming.StreamingFrameController(
|
||||
_options.LiveMode,
|
||||
_localPlayerMode,
|
||||
_playerControllerSlot,
|
||||
|
|
@ -2638,7 +2648,7 @@ public sealed class GameWindow : IDisposable
|
|||
new AcDream.App.Update.SettingsParticleRangeSource(
|
||||
_settingsVm,
|
||||
_persistedDisplay.ParticleRange));
|
||||
_liveSpatialReconciler =
|
||||
var liveSpatialReconciler =
|
||||
new AcDream.App.Update.LiveSpatialPresentationReconciler(
|
||||
_entityEffects!,
|
||||
_equippedChildRenderer!,
|
||||
|
|
@ -2721,16 +2731,15 @@ public sealed class GameWindow : IDisposable
|
|||
_localPlayerSkills,
|
||||
_viewportAspect);
|
||||
_playerModeAutoEntry = new AcDream.App.Input.PlayerModeAutoEntry(
|
||||
isLiveInWorld: () => _liveSessionController.IsInWorld,
|
||||
isPlayerEntityPresent: () =>
|
||||
_liveEntities.MaterializedWorldEntities.ContainsKey(
|
||||
_playerServerGuid),
|
||||
isPlayerControllerReady: () => true,
|
||||
// Retail SmartBox::UseTime @ 0x00455410 completes player
|
||||
// position only after the destination cell load clears.
|
||||
isWorldReady: LoginWorldReady,
|
||||
enterPlayerMode: _playerModeController.EnterFromAutoEntry,
|
||||
isPlayerModeActive: () => _localPlayerMode.IsPlayerMode);
|
||||
new AcDream.App.Input.LivePlayerModeAutoEntryContext(
|
||||
_liveSessionController,
|
||||
_liveEntities,
|
||||
_localPlayerIdentity,
|
||||
_worldReveal
|
||||
?? throw new InvalidOperationException(
|
||||
"World reveal was not composed before player auto-entry."),
|
||||
_localPlayerMode,
|
||||
_playerModeController));
|
||||
_playerModeController.BindAutoEntry(_playerModeAutoEntry);
|
||||
var localTeleportPresentation =
|
||||
new AcDream.App.Streaming.LocalPlayerTeleportPresentation(
|
||||
|
|
@ -2758,7 +2767,7 @@ public sealed class GameWindow : IDisposable
|
|||
_playerHostSlot,
|
||||
_chaseCameraInput,
|
||||
_liveWorldOrigin,
|
||||
_liveSpatialReconciler),
|
||||
liveSpatialReconciler),
|
||||
new AcDream.App.Streaming.LocalPlayerTeleportSession(
|
||||
_liveSessionController),
|
||||
localTeleportPresentation);
|
||||
|
|
@ -2767,25 +2776,41 @@ public sealed class GameWindow : IDisposable
|
|||
// remains only as the staged-startup fallback used by shutdown when
|
||||
// composition fails before the transfer.
|
||||
_portalTunnel = null;
|
||||
_liveFrameCoordinator = new AcDream.App.World.RetailLiveFrameCoordinator(
|
||||
var liveFrameCoordinator = new AcDream.App.World.RetailLiveFrameCoordinator(
|
||||
liveObjectFrame,
|
||||
_worldState,
|
||||
_liveSessionController,
|
||||
localPlayerFrame,
|
||||
_liveSpatialReconciler);
|
||||
_cameraFrame = new AcDream.App.Rendering.CameraFrameController(
|
||||
liveSpatialReconciler);
|
||||
var cameraFrame = new AcDream.App.Rendering.CameraFrameController(
|
||||
_cameraController!,
|
||||
_inputCapture,
|
||||
_cameraInput,
|
||||
localPlayerFrameRuntime,
|
||||
_chaseCameraInput,
|
||||
localPlayerFrame,
|
||||
_liveSpatialReconciler,
|
||||
liveSpatialReconciler,
|
||||
new AcDream.App.Combat.CombatCameraTargetSource(
|
||||
_gameplaySettings,
|
||||
Combat,
|
||||
_selection,
|
||||
_worldSelectionQuery!));
|
||||
_updateFrameOrchestrator = new AcDream.App.Update.UpdateFrameOrchestrator(
|
||||
new AcDream.App.Update.LiveEntityTeardownFramePhase(
|
||||
_liveEntities),
|
||||
new AcDream.App.Update.ConsoleUpdateFrameFailureSink(),
|
||||
_updateFrameClock,
|
||||
new AcDream.App.Update.PhysicsScriptClockPublisher(_scriptRunner!),
|
||||
streamingFrame,
|
||||
_gameplayInputFrame,
|
||||
liveFrameCoordinator,
|
||||
new AcDream.App.Update.LiveEntityLivenessFramePhase(
|
||||
_liveEntityLiveness,
|
||||
new AcDream.App.Update.StopwatchClientMonotonicTimeSource()),
|
||||
_localPlayerTeleport,
|
||||
new AcDream.App.Update.PlayerModeAutoEntryFramePhase(
|
||||
_playerModeAutoEntry),
|
||||
cameraFrame);
|
||||
AcDream.App.Net.LiveSessionStartResult liveStart =
|
||||
_liveSessionController.Start(
|
||||
_options,
|
||||
|
|
@ -2958,7 +2983,7 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
events = new AcDream.App.Net.LiveSessionEventRouter(
|
||||
session,
|
||||
CreateLiveEntitySessionSink(),
|
||||
_liveEntitySessionEvents.CreateSink(),
|
||||
new AcDream.App.Net.LiveEnvironmentSessionSink(
|
||||
OnEnvironChanged,
|
||||
ticks =>
|
||||
|
|
@ -2987,44 +3012,6 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private AcDream.App.Net.LiveEntitySessionSink CreateLiveEntitySessionSink() => new(
|
||||
Spawned: spawn => _inboundEntityEvents.Run(
|
||||
_liveEntityHydration!,
|
||||
spawn,
|
||||
static (hydration, value) => hydration.OnCreate(value)),
|
||||
Deleted: deletion => _inboundEntityEvents.Run(
|
||||
_liveEntityHydration!, deletion,
|
||||
static (hydration, value) => hydration.OnDelete(value)),
|
||||
PickedUp: pickup => _inboundEntityEvents.Run(
|
||||
_liveEntityHydration!, pickup,
|
||||
static (hydration, value) => hydration.OnPickup(value)),
|
||||
MotionUpdated: motion => _inboundEntityEvents.Run(
|
||||
_liveEntityNetworkUpdates!, motion,
|
||||
static (updates, value) => updates.OnMotion(value)),
|
||||
PositionUpdated: position => _inboundEntityEvents.Run(
|
||||
_liveEntityNetworkUpdates!, position,
|
||||
static (updates, value) => updates.OnPosition(value)),
|
||||
VectorUpdated: vector => _inboundEntityEvents.Run(
|
||||
_liveEntityNetworkUpdates!, vector,
|
||||
static (updates, value) => updates.OnVector(value)),
|
||||
StateUpdated: state => _inboundEntityEvents.Run(
|
||||
_liveEntityNetworkUpdates!, state,
|
||||
static (updates, value) => updates.OnState(value)),
|
||||
ParentUpdated: parent => _inboundEntityEvents.Run(
|
||||
_liveEntityHydration!, parent,
|
||||
static (hydration, value) => hydration.OnParent(value)),
|
||||
TeleportStarted: teleport => _inboundEntityEvents.Run(
|
||||
_localPlayerTeleportSink,
|
||||
teleport,
|
||||
static (sink, value) => sink.OnTeleportStarted(value)),
|
||||
AppearanceUpdated: appearance => _inboundEntityEvents.Run(
|
||||
_liveEntityHydration!, appearance,
|
||||
static (hydration, value) => hydration.OnAppearance(value)),
|
||||
PlayPhysicsScript: script => _inboundEntityEvents.Run(
|
||||
this, script, static (window, value) => window.OnPlayScriptReceived(value)),
|
||||
PlayPhysicsScriptType: message => _inboundEntityEvents.Run(
|
||||
this, message, static (window, value) => window._entityEffects?.HandleTyped(value)));
|
||||
|
||||
private AcDream.App.Net.LiveInventorySessionBindings
|
||||
CreateLiveInventorySessionBindings() => new(
|
||||
Objects,
|
||||
|
|
@ -3276,47 +3263,6 @@ public sealed class GameWindow : IDisposable
|
|||
doll.MeshRefs = reposed;
|
||||
}
|
||||
|
||||
private void PublishLocalPhysicsTimestamps(
|
||||
uint guid,
|
||||
AcDream.App.World.AcceptedPhysicsTimestamps timestamps)
|
||||
{
|
||||
AcDream.Core.Net.WorldSession? session = LiveSession;
|
||||
if (guid != _playerServerGuid || session is null) return;
|
||||
session.PublishAcceptedLocalPhysicsTimestamps(
|
||||
timestamps.Instance,
|
||||
timestamps.ServerControlledMove,
|
||||
timestamps.Teleport,
|
||||
timestamps.ForcePosition);
|
||||
}
|
||||
|
||||
// #145: the LANDBLOCK-relative (cell-local) position used to SEED the player
|
||||
// body's cell-relative CellPosition. This is the ONE place the streaming center
|
||||
// (_liveCenter) is allowed to touch the physics frame — at the placement seam,
|
||||
// converting the render-frame world position into the wire's (cell, local). After
|
||||
// seeding, physics carries (cell, local) forward without ever reading _liveCenter.
|
||||
private System.Numerics.Vector3 CellLocalForSeed(System.Numerics.Vector3 worldPos, uint cellId)
|
||||
{
|
||||
int lbX = (int)((cellId >> 24) & 0xFFu);
|
||||
int lbY = (int)((cellId >> 16) & 0xFFu);
|
||||
var origin = new System.Numerics.Vector3(
|
||||
(lbX - _liveCenterX) * 192f,
|
||||
(lbY - _liveCenterY) * 192f,
|
||||
0f);
|
||||
return worldPos - origin;
|
||||
}
|
||||
|
||||
private bool LoginWorldReady()
|
||||
{
|
||||
return TryGetLoginWorldCell(out uint cell)
|
||||
&& EvaluateWorldRevealReadiness(cell).IsReady;
|
||||
}
|
||||
|
||||
private AcDream.App.Streaming.WorldRevealReadinessSnapshot EvaluateWorldRevealReadiness(
|
||||
uint destinationCell)
|
||||
{
|
||||
return _worldReveal?.Evaluate(destinationCell) ?? default;
|
||||
}
|
||||
|
||||
private bool TryGetLoginWorldCell(out uint cell)
|
||||
{
|
||||
cell = 0;
|
||||
|
|
@ -3329,20 +3275,6 @@ public sealed class GameWindow : IDisposable
|
|||
return cell != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-sent direct PhysicsScript (F754). EntityEffectController owns
|
||||
/// GUID translation and pre-materialization queueing.
|
||||
///
|
||||
/// <para>
|
||||
/// F754 remains a direct PhysicsScript DID. It is never resolved through a
|
||||
/// typed PhysicsScriptTable.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private void OnPlayScriptReceived(AcDream.Core.Net.Messages.PlayPhysicsScript message)
|
||||
{
|
||||
_entityEffects?.HandleDirect(message);
|
||||
}
|
||||
|
||||
private void UpdateSkyPes(
|
||||
float dayFraction,
|
||||
AcDream.Core.World.DayGroupData? dayGroup,
|
||||
|
|
@ -3512,76 +3444,10 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
private void OnUpdate(double dt)
|
||||
{
|
||||
using var _updStage = _frameProfiler.BeginStage(AcDream.App.Diagnostics.FrameStage.Update);
|
||||
|
||||
// A resource callback can request deletion while the presentation
|
||||
// owner is inside a synchronous resume/suspend transition. The first
|
||||
// unregister leaves a canonical teardown tombstone; drain it only now,
|
||||
// after that callback stack has unwound on the same update thread.
|
||||
try
|
||||
{
|
||||
_liveEntityHydration?.RetryPendingTeardowns();
|
||||
}
|
||||
catch (AggregateException error)
|
||||
{
|
||||
// The tombstone retains every unfinished owner step. Report this
|
||||
// attempt, but keep the frame advancing so the next update can
|
||||
// retry after transient renderer/plugin teardown failures.
|
||||
Console.Error.WriteLine($"[live-entity-teardown] {error}");
|
||||
}
|
||||
|
||||
AcDream.App.Update.UpdateFrameTiming frameTiming = _updateFrameClock.Advance(
|
||||
using var _updStage = _frameProfiler.BeginStage(
|
||||
AcDream.App.Diagnostics.FrameStage.Update);
|
||||
_updateFrameOrchestrator.Tick(
|
||||
new AcDream.App.Update.UpdateFrameInput(dt));
|
||||
float frameDelta = frameTiming.SimulationDeltaSecondsSingle;
|
||||
|
||||
// Retail ScriptManager::AddScriptInternal (0x0051B310) stamps
|
||||
// Timer::cur_time at the packet/default-script call site. Publish this
|
||||
// update frame's clock before streaming and network dispatch, then
|
||||
// reuse the same timestamp for animation hooks and the later drain.
|
||||
_scriptRunner?.PublishTime(frameTiming.ScriptTime);
|
||||
|
||||
// Streaming publishes before inbound CreateObject dispatch so newly
|
||||
// accepted live projections can enter a resident bucket this frame.
|
||||
_streamingFrame.Tick();
|
||||
|
||||
// Input callbacks feed the current object tick. Packets already read
|
||||
// by Core.Net remain queued until the retail object/physics phase has
|
||||
// consumed that input and published its final pose.
|
||||
_gameplayInputFrame!.Tick(frameTiming);
|
||||
|
||||
// Drain pending live-session traffic AFTER streaming so any incoming
|
||||
// CreateObject events find their landblock already loaded in
|
||||
// GpuWorldState. Non-blocking — returns immediately if no datagrams
|
||||
// are in the kernel buffer. Fires EntitySpawned events synchronously.
|
||||
// Step 2: routed through the controller; functionally identical.
|
||||
// Retail SmartBox::UseTime (0x00455410) advances CObjectMaint and
|
||||
// CPhysics before it drains the inbound event queue. Keep the complete
|
||||
// live-object phase on that side of the barrier too. In particular,
|
||||
// the recall action retires before ACE's Hidden SetState freezes its
|
||||
// PartArray at the teleport boundary.
|
||||
_liveFrameCoordinator.Tick(frameDelta);
|
||||
_liveEntityLiveness?.Tick(ClientTimerNow());
|
||||
|
||||
// Retail teleport transit runs after streaming and inbound state so a
|
||||
// newly resident destination can materialize in this same frame. The
|
||||
// focused owner also converges an F751 that arrived before the local
|
||||
// player projection/controller became available.
|
||||
_localPlayerTeleport!.Tick(frameDelta);
|
||||
// Phase K.1a — tick the input dispatcher so Hold-type bindings
|
||||
// re-fire while their chord is held. K.1b adds the subscribers
|
||||
// that actually consume the events.
|
||||
// Phase D.5.3a — advance the selected-object overlay flash (0.25s green pulse
|
||||
// on selection, then revert). No-op when nothing is flashing.
|
||||
// Retained panel-local timers advance through RetailUiRuntime.Tick on the draw seam.
|
||||
|
||||
// Phase K.2 — auto-enter player mode at login. The guard
|
||||
// returns true on the firing tick (one-shot); subsequent ticks
|
||||
// are no-ops. Skipped offline (no active session → IsLiveInWorld
|
||||
// predicate stays false). Cancelled by manual fly-toggle in
|
||||
// OnInputAction (Ctrl+Tab) or DebugPanel.
|
||||
_playerModeAutoEntry?.TryEnter();
|
||||
|
||||
_cameraFrame.Tick(frameTiming);
|
||||
}
|
||||
|
||||
private void OnCameraModeChanged(bool _modeBool)
|
||||
|
|
|
|||
19
src/AcDream.App/Rendering/LiveEntityPartArrayLifecycle.cs
Normal file
19
src/AcDream.App/Rendering/LiveEntityPartArrayLifecycle.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
using AcDream.App.World;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>Routes retail PartArray world-entry boundaries to live animation owners.</summary>
|
||||
internal sealed class LiveEntityPartArrayLifecycle
|
||||
{
|
||||
private readonly LiveEntityAnimationRuntimeView<LiveEntityAnimationState> _animations;
|
||||
|
||||
public LiveEntityPartArrayLifecycle(
|
||||
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> animations) =>
|
||||
_animations = animations ?? throw new ArgumentNullException(nameof(animations));
|
||||
|
||||
public void HandleEnterWorld(uint localEntityId)
|
||||
{
|
||||
if (_animations.TryGetValue(localEntityId, out LiveEntityAnimationState? animation))
|
||||
animation.Sequencer?.Manager.HandleEnterWorld();
|
||||
}
|
||||
}
|
||||
78
src/AcDream.App/Update/UpdateFrameRuntimeAdapters.cs
Normal file
78
src/AcDream.App/Update/UpdateFrameRuntimeAdapters.cs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
using AcDream.App.Input;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Vfx;
|
||||
|
||||
namespace AcDream.App.Update;
|
||||
|
||||
/// <summary>Bridges canonical live-entity teardown into the host frame.</summary>
|
||||
internal sealed class LiveEntityTeardownFramePhase : IUpdateFrameTeardownPhase
|
||||
{
|
||||
private readonly LiveEntityRuntime _runtime;
|
||||
|
||||
public LiveEntityTeardownFramePhase(LiveEntityRuntime runtime) =>
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
|
||||
public void RetryPendingTeardowns() => _runtime.RetryPendingTeardowns();
|
||||
}
|
||||
|
||||
internal sealed class ConsoleUpdateFrameFailureSink : IUpdateFrameFailureSink
|
||||
{
|
||||
public void ReportTeardownFailure(AggregateException error) =>
|
||||
Console.Error.WriteLine($"[live-entity-teardown] {error}");
|
||||
}
|
||||
|
||||
/// <summary>Publishes the frame clock to the retail PhysicsScript owner.</summary>
|
||||
internal sealed class PhysicsScriptClockPublisher : IUpdateFrameScriptClockPublisher
|
||||
{
|
||||
private readonly PhysicsScriptRunner _runner;
|
||||
|
||||
public PhysicsScriptClockPublisher(PhysicsScriptRunner runner) =>
|
||||
_runner = runner ?? throw new ArgumentNullException(nameof(runner));
|
||||
|
||||
public void PublishTime(double scriptTime) => _runner.PublishTime(scriptTime);
|
||||
}
|
||||
|
||||
internal interface IClientMonotonicTimeSource
|
||||
{
|
||||
double Now { get; }
|
||||
}
|
||||
|
||||
internal sealed class StopwatchClientMonotonicTimeSource
|
||||
: IClientMonotonicTimeSource
|
||||
{
|
||||
public double Now =>
|
||||
System.Diagnostics.Stopwatch.GetTimestamp()
|
||||
/ (double)System.Diagnostics.Stopwatch.Frequency;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Supplies the absolute client timer to liveness independently of the
|
||||
/// normalized simulation delta.
|
||||
/// </summary>
|
||||
internal sealed class LiveEntityLivenessFramePhase
|
||||
: ILiveEntityLivenessFramePhase
|
||||
{
|
||||
private readonly LiveEntityLivenessController _liveness;
|
||||
private readonly IClientMonotonicTimeSource _clock;
|
||||
|
||||
public LiveEntityLivenessFramePhase(
|
||||
LiveEntityLivenessController liveness,
|
||||
IClientMonotonicTimeSource clock)
|
||||
{
|
||||
_liveness = liveness ?? throw new ArgumentNullException(nameof(liveness));
|
||||
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
|
||||
}
|
||||
|
||||
public void Tick() => _liveness.Tick(_clock.Now);
|
||||
}
|
||||
|
||||
internal sealed class PlayerModeAutoEntryFramePhase
|
||||
: IPlayerModeAutoEntryFramePhase
|
||||
{
|
||||
private readonly PlayerModeAutoEntry _autoEntry;
|
||||
|
||||
public PlayerModeAutoEntryFramePhase(PlayerModeAutoEntry autoEntry) =>
|
||||
_autoEntry = autoEntry ?? throw new ArgumentNullException(nameof(autoEntry));
|
||||
|
||||
public void TryEnter() => _autoEntry.TryEnter();
|
||||
}
|
||||
69
src/AcDream.App/World/LiveEntityDeletionController.cs
Normal file
69
src/AcDream.App/World/LiveEntityDeletionController.cs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
using AcDream.App.Input;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
|
||||
namespace AcDream.App.World;
|
||||
|
||||
internal interface ILiveEntityPruneSink
|
||||
{
|
||||
bool Prune(LiveEntityPruneCandidate candidate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owns authoritative and expiry-driven live-object deletion through one
|
||||
/// generation-safe runtime transaction.
|
||||
/// </summary>
|
||||
internal sealed class LiveEntityDeletionController : ILiveEntityPruneSink
|
||||
{
|
||||
private readonly LiveEntityRuntime _runtime;
|
||||
private readonly ClientObjectTable _objects;
|
||||
private readonly ILiveEntityTeardownCoordinator _teardown;
|
||||
private readonly ILocalPlayerIdentitySource _identity;
|
||||
private readonly Action<string>? _diagnostic;
|
||||
|
||||
public LiveEntityDeletionController(
|
||||
LiveEntityRuntime runtime,
|
||||
ClientObjectTable objects,
|
||||
ILiveEntityTeardownCoordinator teardown,
|
||||
ILocalPlayerIdentitySource identity,
|
||||
Action<string>? diagnostic = null)
|
||||
{
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||
_teardown = teardown ?? throw new ArgumentNullException(nameof(teardown));
|
||||
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
|
||||
_diagnostic = diagnostic;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>SmartBox::HandleDeleteObject @ 0x00451EA0</c> rejects the
|
||||
/// player, requires the exact Instance timestamp, then performs one
|
||||
/// symmetric logical teardown.
|
||||
/// </summary>
|
||||
public bool Delete(DeleteObject.Parsed delete)
|
||||
{
|
||||
if (delete.Guid == _identity.ServerGuid)
|
||||
return false;
|
||||
|
||||
if (!_runtime.TryGetRecord(delete.Guid, out _))
|
||||
_teardown.ForgetUnknownOwner(delete.Guid);
|
||||
|
||||
bool removed = _runtime.UnregisterLiveEntity(
|
||||
delete,
|
||||
isLocalPlayer: false,
|
||||
beforeTeardown: () =>
|
||||
ObjectTableWiring.ApplyEntityDelete(_objects, delete));
|
||||
if (removed)
|
||||
{
|
||||
_diagnostic?.Invoke(
|
||||
$"live: delete guid=0x{delete.Guid:X8} instSeq={delete.InstanceSequence}");
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
public bool Prune(LiveEntityPruneCandidate candidate) =>
|
||||
Delete(new DeleteObject.Parsed(
|
||||
candidate.ServerGuid,
|
||||
candidate.Generation));
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
|
|
@ -146,9 +147,9 @@ internal sealed class LiveEntityHydrationController
|
|||
private readonly ILiveEntityReadyPublisher _ready;
|
||||
private readonly ILiveEntityWorldOriginCoordinator _origin;
|
||||
private readonly ILiveEntityNetworkUpdateSink _networkUpdates;
|
||||
private readonly ILiveEntityTeardownCoordinator _teardown;
|
||||
private readonly Action<uint, AcceptedPhysicsTimestamps> _publishTimestamps;
|
||||
private readonly Func<uint> _playerGuid;
|
||||
private readonly IAcceptedLocalPhysicsTimestampPublisher _timestamps;
|
||||
private readonly ILocalPlayerIdentitySource _identity;
|
||||
private readonly LiveEntityDeletionController _deletion;
|
||||
private readonly Action<string>? _diagnostic;
|
||||
|
||||
public LiveEntityHydrationController(
|
||||
|
|
@ -160,9 +161,9 @@ internal sealed class LiveEntityHydrationController
|
|||
ILiveEntityReadyPublisher ready,
|
||||
ILiveEntityWorldOriginCoordinator origin,
|
||||
ILiveEntityNetworkUpdateSink networkUpdates,
|
||||
ILiveEntityTeardownCoordinator teardown,
|
||||
Action<uint, AcceptedPhysicsTimestamps> publishTimestamps,
|
||||
Func<uint> playerGuid,
|
||||
IAcceptedLocalPhysicsTimestampPublisher timestamps,
|
||||
ILocalPlayerIdentitySource identity,
|
||||
LiveEntityDeletionController deletion,
|
||||
Action<string>? diagnostic = null)
|
||||
{
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
|
|
@ -173,10 +174,9 @@ internal sealed class LiveEntityHydrationController
|
|||
_ready = ready ?? throw new ArgumentNullException(nameof(ready));
|
||||
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
|
||||
_networkUpdates = networkUpdates ?? throw new ArgumentNullException(nameof(networkUpdates));
|
||||
_teardown = teardown ?? throw new ArgumentNullException(nameof(teardown));
|
||||
_publishTimestamps = publishTimestamps
|
||||
?? throw new ArgumentNullException(nameof(publishTimestamps));
|
||||
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
||||
_timestamps = timestamps ?? throw new ArgumentNullException(nameof(timestamps));
|
||||
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
|
||||
_deletion = deletion ?? throw new ArgumentNullException(nameof(deletion));
|
||||
_diagnostic = diagnostic;
|
||||
}
|
||||
|
||||
|
|
@ -207,7 +207,7 @@ internal sealed class LiveEntityHydrationController
|
|||
|
||||
try
|
||||
{
|
||||
_publishTimestamps(spawn.Guid, result.Timestamps);
|
||||
_timestamps.Publish(spawn.Guid, result.Timestamps);
|
||||
if (_runtime.IsCurrentCreateIntegration(
|
||||
record,
|
||||
createIntegrationVersion)
|
||||
|
|
@ -353,23 +353,7 @@ internal sealed class LiveEntityHydrationController
|
|||
// SmartBox::HandleDeleteObject rejects the player before resolving an
|
||||
// object or touching any queued owner state. A pre-Create player
|
||||
// Delete must therefore be side-effect-free too.
|
||||
if (delete.Guid == _playerGuid())
|
||||
return false;
|
||||
|
||||
if (!_runtime.TryGetRecord(delete.Guid, out _))
|
||||
_teardown.ForgetUnknownOwner(delete.Guid);
|
||||
|
||||
bool removed = _runtime.UnregisterLiveEntity(
|
||||
delete,
|
||||
isLocalPlayer: false,
|
||||
beforeTeardown: () =>
|
||||
ObjectTableWiring.ApplyEntityDelete(_objects, delete));
|
||||
if (removed)
|
||||
{
|
||||
_diagnostic?.Invoke(
|
||||
$"live: delete guid=0x{delete.Guid:X8} instSeq={delete.InstanceSequence}");
|
||||
}
|
||||
return removed;
|
||||
return _deletion.Delete(delete);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -377,9 +361,7 @@ internal sealed class LiveEntityHydrationController
|
|||
/// generation gate and teardown transaction as an authoritative F747.
|
||||
/// </summary>
|
||||
public bool OnPrune(LiveEntityPruneCandidate candidate) =>
|
||||
OnDelete(new DeleteObject.Parsed(
|
||||
candidate.ServerGuid,
|
||||
candidate.Generation));
|
||||
_deletion.Prune(candidate);
|
||||
|
||||
public int RetryPendingTeardowns() => _runtime.RetryPendingTeardowns();
|
||||
|
||||
|
|
@ -415,7 +397,7 @@ internal sealed class LiveEntityHydrationController
|
|||
|
||||
uint canonical = (loadedLandblockId & 0xFFFF0000u) | 0xFFFFu;
|
||||
LiveEntityRecord[] records = _runtime.Records
|
||||
.Where(record => (record.ServerGuid != _playerGuid()
|
||||
.Where(record => (record.ServerGuid != _identity.ServerGuid
|
||||
|| !record.InitialHydrationCompleted
|
||||
|| record.CreateProjectionSynchronizationPending
|
||||
|| record.AppearanceProjectionSynchronizationPending)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using AcDream.App.Input;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.App.Streaming;
|
||||
|
|
@ -113,7 +114,17 @@ internal sealed class LiveEntityWorldOriginCoordinator : ILiveEntityWorldOriginC
|
|||
private readonly StreamingController _streaming;
|
||||
private readonly GpuWorldState _worldState;
|
||||
private readonly WorldRevealCoordinator _worldReveal;
|
||||
private readonly Func<uint> _playerGuid;
|
||||
private sealed class DelegateIdentitySource : ILocalPlayerIdentitySource
|
||||
{
|
||||
private readonly Func<uint> _read;
|
||||
|
||||
public DelegateIdentitySource(Func<uint> read) =>
|
||||
_read = read ?? throw new ArgumentNullException(nameof(read));
|
||||
|
||||
public uint ServerGuid => _read();
|
||||
}
|
||||
|
||||
private readonly ILocalPlayerIdentitySource _identity;
|
||||
private readonly ISealedDungeonCellClassifier _sealedDungeonCells;
|
||||
private readonly Action<string>? _diagnostic;
|
||||
|
||||
|
|
@ -122,7 +133,7 @@ internal sealed class LiveEntityWorldOriginCoordinator : ILiveEntityWorldOriginC
|
|||
StreamingController streaming,
|
||||
GpuWorldState worldState,
|
||||
WorldRevealCoordinator worldReveal,
|
||||
Func<uint> playerGuid,
|
||||
ILocalPlayerIdentitySource identity,
|
||||
ISealedDungeonCellClassifier sealedDungeonCells,
|
||||
Action<string>? diagnostic = null)
|
||||
{
|
||||
|
|
@ -130,18 +141,37 @@ internal sealed class LiveEntityWorldOriginCoordinator : ILiveEntityWorldOriginC
|
|||
_streaming = streaming ?? throw new ArgumentNullException(nameof(streaming));
|
||||
_worldState = worldState ?? throw new ArgumentNullException(nameof(worldState));
|
||||
_worldReveal = worldReveal ?? throw new ArgumentNullException(nameof(worldReveal));
|
||||
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
||||
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
|
||||
_sealedDungeonCells = sealedDungeonCells
|
||||
?? throw new ArgumentNullException(nameof(sealedDungeonCells));
|
||||
_diagnostic = diagnostic;
|
||||
}
|
||||
|
||||
internal LiveEntityWorldOriginCoordinator(
|
||||
LiveWorldOriginState origin,
|
||||
StreamingController streaming,
|
||||
GpuWorldState worldState,
|
||||
WorldRevealCoordinator worldReveal,
|
||||
Func<uint> playerGuid,
|
||||
ISealedDungeonCellClassifier sealedDungeonCells,
|
||||
Action<string>? diagnostic = null)
|
||||
: this(
|
||||
origin,
|
||||
streaming,
|
||||
worldState,
|
||||
worldReveal,
|
||||
new DelegateIdentitySource(playerGuid),
|
||||
sealedDungeonCells,
|
||||
diagnostic)
|
||||
{
|
||||
}
|
||||
|
||||
public bool IsKnown => _origin.IsKnown;
|
||||
|
||||
public LiveEntityOriginInitialization TryInitialize(
|
||||
WorldSession.EntitySpawn spawn)
|
||||
{
|
||||
if (spawn.Guid != _playerGuid()
|
||||
if (spawn.Guid != _identity.ServerGuid
|
||||
|| spawn.Position is not { } position)
|
||||
{
|
||||
return new(_origin.IsKnown, Array.Empty<uint>());
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.App.Input;
|
||||
|
||||
namespace AcDream.App.World;
|
||||
|
||||
|
|
@ -86,19 +87,19 @@ internal sealed class LiveEntityLivenessController
|
|||
private const double MaintenanceIntervalSeconds = 1.0;
|
||||
|
||||
private readonly LiveEntityRuntime _runtime;
|
||||
private readonly Func<uint> _playerGuid;
|
||||
private readonly Action<LiveEntityPruneCandidate> _prune;
|
||||
private readonly ILocalPlayerIdentitySource _identity;
|
||||
private readonly ILiveEntityPruneSink _prune;
|
||||
private readonly LiveEntityLivenessTracker _tracker = new();
|
||||
private readonly List<LiveEntityLivenessSample> _samples = new();
|
||||
private double _nextMaintenanceAt;
|
||||
|
||||
public LiveEntityLivenessController(
|
||||
LiveEntityRuntime runtime,
|
||||
Func<uint> playerGuid,
|
||||
Action<LiveEntityPruneCandidate> prune)
|
||||
ILocalPlayerIdentitySource identity,
|
||||
ILiveEntityPruneSink prune)
|
||||
{
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
||||
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
|
||||
_prune = prune ?? throw new ArgumentNullException(nameof(prune));
|
||||
}
|
||||
|
||||
|
|
@ -108,7 +109,7 @@ internal sealed class LiveEntityLivenessController
|
|||
return;
|
||||
_nextMaintenanceAt = now + MaintenanceIntervalSeconds;
|
||||
|
||||
uint playerGuid = _playerGuid();
|
||||
uint playerGuid = _identity.ServerGuid;
|
||||
if (playerGuid == 0
|
||||
|| !_runtime.TryGetRecord(playerGuid, out LiveEntityRecord player)
|
||||
|| player.Snapshot.Position is not { } playerPosition)
|
||||
|
|
@ -144,7 +145,7 @@ internal sealed class LiveEntityLivenessController
|
|||
if (_runtime.TryGetRecord(candidate.ServerGuid, out LiveEntityRecord current)
|
||||
&& current.Generation == candidate.Generation)
|
||||
{
|
||||
_prune(candidate);
|
||||
_prune.Prune(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AcDream.App.Interaction;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Physics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
|
|
@ -38,7 +39,7 @@ internal sealed class LiveEntityRuntimeTeardownController
|
|||
private readonly ShadowObjectRegistry? _shadows;
|
||||
private readonly LiveEntityLightController? _lights;
|
||||
private readonly EntityClassificationCache? _classification;
|
||||
private readonly Func<uint>? _playerGuid;
|
||||
private readonly ILocalPlayerIdentitySource? _identity;
|
||||
private readonly Func<LiveEntityRecord, LiveEntityTeardownPlan> _createPlan;
|
||||
private readonly Action<uint> _forgetUnknownOwner;
|
||||
|
||||
|
|
@ -57,7 +58,7 @@ internal sealed class LiveEntityRuntimeTeardownController
|
|||
ShadowObjectRegistry shadows,
|
||||
LiveEntityLightController lights,
|
||||
EntityClassificationCache classification,
|
||||
Func<uint> playerGuid)
|
||||
ILocalPlayerIdentitySource identity)
|
||||
{
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
_presentation = presentation ?? throw new ArgumentNullException(nameof(presentation));
|
||||
|
|
@ -77,7 +78,7 @@ internal sealed class LiveEntityRuntimeTeardownController
|
|||
_lights = lights ?? throw new ArgumentNullException(nameof(lights));
|
||||
_classification = classification
|
||||
?? throw new ArgumentNullException(nameof(classification));
|
||||
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
||||
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
|
||||
_createPlan = CreatePlan;
|
||||
_forgetUnknownOwner = effects.ForgetUnknownOwner;
|
||||
}
|
||||
|
|
@ -158,7 +159,7 @@ internal sealed class LiveEntityRuntimeTeardownController
|
|||
cleanups.Add(() => _translucencyFades!.ClearEntity(existingEntity.Id));
|
||||
cleanups.Add(() => _projectionWithdrawal!.LeaveWorld(
|
||||
record,
|
||||
_playerGuid!()));
|
||||
_identity!.ServerGuid));
|
||||
cleanups.Add(() => _children!.OnLogicalTeardown(record));
|
||||
cleanups.Add(() => _shadows!.Deregister(existingEntity.Id));
|
||||
cleanups.Add(() => _lights!.Forget(existingEntity.Id));
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.World;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -40,6 +42,23 @@ internal sealed class LiveWorldOriginState
|
|||
CenterY = centerY;
|
||||
}
|
||||
|
||||
public (int X, int Y) GetCenter() => (CenterX, CenterY);
|
||||
|
||||
/// <summary>
|
||||
/// Converts the streamed render frame back into retail's landblock-local
|
||||
/// physics frame at the one placement seed boundary.
|
||||
/// </summary>
|
||||
public Vector3 CellLocalForSeed(Vector3 worldPosition, uint cellId)
|
||||
{
|
||||
int landblockX = (int)((cellId >> 24) & 0xFFu);
|
||||
int landblockY = (int)((cellId >> 16) & 0xFFu);
|
||||
var origin = new Vector3(
|
||||
(landblockX - CenterX) * 192f,
|
||||
(landblockY - CenterY) * 192f,
|
||||
0f);
|
||||
return worldPosition - origin;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ends the accepted-origin lifetime while retaining the last coordinates
|
||||
/// as the next placeholder. Consumers must gate them on
|
||||
|
|
|
|||
43
src/AcDream.App/World/LocalPhysicsTimestampPublisher.cs
Normal file
43
src/AcDream.App/World/LocalPhysicsTimestampPublisher.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
using AcDream.App.Input;
|
||||
using AcDream.App.Net;
|
||||
|
||||
namespace AcDream.App.World;
|
||||
|
||||
internal interface IAcceptedLocalPhysicsTimestampPublisher
|
||||
{
|
||||
void Publish(uint serverGuid, AcceptedPhysicsTimestamps timestamps);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Publishes accepted local-player physics timestamps to the current exact
|
||||
/// session without retaining the window host.
|
||||
/// </summary>
|
||||
internal sealed class LiveSessionLocalPhysicsTimestampPublisher
|
||||
: IAcceptedLocalPhysicsTimestampPublisher
|
||||
{
|
||||
private readonly ILocalPlayerIdentitySource _identity;
|
||||
private readonly ILiveWorldSessionSource _session;
|
||||
|
||||
public LiveSessionLocalPhysicsTimestampPublisher(
|
||||
ILocalPlayerIdentitySource identity,
|
||||
ILiveWorldSessionSource session)
|
||||
{
|
||||
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
|
||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||
}
|
||||
|
||||
public void Publish(uint serverGuid, AcceptedPhysicsTimestamps timestamps)
|
||||
{
|
||||
if (serverGuid != _identity.ServerGuid
|
||||
|| _session.CurrentSession is not { } session)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
session.PublishAcceptedLocalPhysicsTimestamps(
|
||||
timestamps.Instance,
|
||||
timestamps.ServerControlledMove,
|
||||
timestamps.Teleport,
|
||||
timestamps.ForcePosition);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue