refactor(runtime): define the update-frame contract
This commit is contained in:
parent
a36a7015c4
commit
99a3e819c4
8 changed files with 646 additions and 26 deletions
|
|
@ -13,7 +13,7 @@ never hidden behind a retry, delay, suppression flag, or reordered callback.
|
||||||
|
|
||||||
## Progress ledger
|
## Progress ledger
|
||||||
|
|
||||||
- [ ] A — freeze the complete production phase graph and introduce the typed
|
- [x] A — freeze the complete production phase graph and introduce the typed
|
||||||
orchestration contract plus structural/order guards.
|
orchestration contract plus structural/order guards.
|
||||||
- [ ] B — extract the pre-network live-object presentation phase and the
|
- [ ] B — extract the pre-network live-object presentation phase and the
|
||||||
non-advancing post-network spatial reconciler; remove callbacks into
|
non-advancing post-network spatial reconciler; remove callbacks into
|
||||||
|
|
@ -35,6 +35,13 @@ never hidden behind a retry, delay, suppression flag, or reordered callback.
|
||||||
Every checked checkpoint is committed as one bisectable architectural unit.
|
Every checked checkpoint is committed as one bisectable architectural unit.
|
||||||
Documentation records the exact commit and accepted test count as work lands.
|
Documentation records the exact commit and accepted test count as work lands.
|
||||||
|
|
||||||
|
Checkpoint A introduced the internal `AcDream.App.Update` contract, one typed
|
||||||
|
`UpdateFrameClock`/`IPhysicsScriptTimeSource`, the twelve-phase outer-order
|
||||||
|
oracle, exact teardown and invalid-delta tests, and guards against a transitive
|
||||||
|
`GameWindow` clock capture. Production now publishes PES time once per update;
|
||||||
|
18 focused Release tests and the full 2,709-test App project pass, and all
|
||||||
|
three corrected-diff reviews are clean.
|
||||||
|
|
||||||
## 1. Outcome and non-goals
|
## 1. Outcome and non-goals
|
||||||
|
|
||||||
At slice exit, `GameWindow.OnUpdate` starts the profiler scope and delegates one
|
At slice exit, `GameWindow.OnUpdate` starts the profiler scope and delegates one
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using AcDream.App.Interaction;
|
||||||
using AcDream.App.Physics;
|
using AcDream.App.Physics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Rendering.Vfx;
|
using AcDream.App.Rendering.Vfx;
|
||||||
|
using AcDream.App.Update;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
using AcDream.Core.Net;
|
using AcDream.Core.Net;
|
||||||
|
|
@ -52,7 +53,7 @@ internal sealed class LiveEntityNetworkUpdateController
|
||||||
private readonly LocalPlayerOutboundController _localPlayerOutbound;
|
private readonly LocalPlayerOutboundController _localPlayerOutbound;
|
||||||
private readonly Func<EntityPhysicsHost?> _playerHostSource;
|
private readonly Func<EntityPhysicsHost?> _playerHostSource;
|
||||||
private readonly Func<uint> _playerGuid;
|
private readonly Func<uint> _playerGuid;
|
||||||
private readonly Func<double> _gameTime;
|
private readonly IPhysicsScriptTimeSource _gameTime;
|
||||||
private readonly Func<WorldSession?> _session;
|
private readonly Func<WorldSession?> _session;
|
||||||
private readonly LiveEntityInboundAuthorityGate _authorityGate;
|
private readonly LiveEntityInboundAuthorityGate _authorityGate;
|
||||||
private readonly Action<WorldSession.EntityPositionUpdate> _aimTeleportDestination;
|
private readonly Action<WorldSession.EntityPositionUpdate> _aimTeleportDestination;
|
||||||
|
|
@ -62,7 +63,7 @@ internal sealed class LiveEntityNetworkUpdateController
|
||||||
private PlayerMovementController? _playerController => _playerControllerSource();
|
private PlayerMovementController? _playerController => _playerControllerSource();
|
||||||
private EntityPhysicsHost? _playerHost => _playerHostSource();
|
private EntityPhysicsHost? _playerHost => _playerHostSource();
|
||||||
private uint _playerServerGuid => _playerGuid();
|
private uint _playerServerGuid => _playerGuid();
|
||||||
private double _physicsScriptGameTime => _gameTime();
|
private double _physicsScriptGameTime => _gameTime.CurrentScriptTime;
|
||||||
private IReadOnlyDictionary<uint, WorldEntity> _entitiesByServerGuid =>
|
private IReadOnlyDictionary<uint, WorldEntity> _entitiesByServerGuid =>
|
||||||
_liveEntities.MaterializedWorldEntities;
|
_liveEntities.MaterializedWorldEntities;
|
||||||
private IReadOnlyDictionary<uint, WorldEntity> _visibleEntitiesByServerGuid =>
|
private IReadOnlyDictionary<uint, WorldEntity> _visibleEntitiesByServerGuid =>
|
||||||
|
|
@ -97,7 +98,7 @@ internal sealed class LiveEntityNetworkUpdateController
|
||||||
LocalPlayerOutboundController localPlayerOutbound,
|
LocalPlayerOutboundController localPlayerOutbound,
|
||||||
Func<EntityPhysicsHost?> playerHostSource,
|
Func<EntityPhysicsHost?> playerHostSource,
|
||||||
Func<uint> playerGuid,
|
Func<uint> playerGuid,
|
||||||
Func<double> gameTime,
|
IPhysicsScriptTimeSource gameTime,
|
||||||
Func<WorldSession?> session,
|
Func<WorldSession?> session,
|
||||||
Action<uint, AcceptedPhysicsTimestamps> publishTimestamps,
|
Action<uint, AcceptedPhysicsTimestamps> publishTimestamps,
|
||||||
Action<WorldSession.EntityPositionUpdate> aimTeleportDestination,
|
Action<WorldSession.EntityPositionUpdate> aimTeleportDestination,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using System.Numerics;
|
||||||
using AcDream.App.Physics;
|
using AcDream.App.Physics;
|
||||||
using AcDream.App.Rendering.Vfx;
|
using AcDream.App.Rendering.Vfx;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
|
using AcDream.App.Update;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
using AcDream.Core.Meshing;
|
using AcDream.Core.Meshing;
|
||||||
|
|
@ -44,7 +45,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
||||||
private readonly LiveEntityAnimationPresenter _animationPresenter;
|
private readonly LiveEntityAnimationPresenter _animationPresenter;
|
||||||
private readonly RetailStaticAnimatingObjectScheduler _staticAnimations;
|
private readonly RetailStaticAnimatingObjectScheduler _staticAnimations;
|
||||||
private readonly LiveWorldOriginState _origin;
|
private readonly LiveWorldOriginState _origin;
|
||||||
private readonly Func<double> _gameTime;
|
private readonly IPhysicsScriptTimeSource _gameTime;
|
||||||
|
|
||||||
private int _received;
|
private int _received;
|
||||||
private int _hydrated;
|
private int _hydrated;
|
||||||
|
|
@ -76,7 +77,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
||||||
LiveEntityAnimationPresenter animationPresenter,
|
LiveEntityAnimationPresenter animationPresenter,
|
||||||
RetailStaticAnimatingObjectScheduler staticAnimations,
|
RetailStaticAnimatingObjectScheduler staticAnimations,
|
||||||
LiveWorldOriginState origin,
|
LiveWorldOriginState origin,
|
||||||
Func<double> gameTime)
|
IPhysicsScriptTimeSource gameTime)
|
||||||
{
|
{
|
||||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||||
|
|
@ -791,7 +792,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
||||||
_projectiles.TryBind(
|
_projectiles.TryBind(
|
||||||
expectedRecord,
|
expectedRecord,
|
||||||
setup,
|
setup,
|
||||||
_gameTime(),
|
_gameTime.CurrentScriptTime,
|
||||||
_origin.CenterX,
|
_origin.CenterX,
|
||||||
_origin.CenterY);
|
_origin.CenterY);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
private readonly AcDream.App.Input.LocalPlayerOutboundController _localPlayerOutbound;
|
private readonly AcDream.App.Input.LocalPlayerOutboundController _localPlayerOutbound;
|
||||||
private readonly AcDream.App.Input.RetailLocalPlayerFrameController _localPlayerFrame;
|
private readonly AcDream.App.Input.RetailLocalPlayerFrameController _localPlayerFrame;
|
||||||
private readonly AcDream.App.World.RetailLiveFrameCoordinator _liveFrameCoordinator;
|
private readonly AcDream.App.World.RetailLiveFrameCoordinator _liveFrameCoordinator;
|
||||||
|
private readonly AcDream.App.Update.UpdateFrameClock _updateFrameClock = new();
|
||||||
private AcDream.App.Physics.RemoteTeleportController? _remoteTeleportController;
|
private AcDream.App.Physics.RemoteTeleportController? _remoteTeleportController;
|
||||||
// Step 7 projectile presentation. The controller owns no identity map;
|
// Step 7 projectile presentation. The controller owns no identity map;
|
||||||
// each runtime component is stored on the canonical LiveEntityRecord.
|
// each runtime component is stored on the canonical LiveEntityRecord.
|
||||||
|
|
@ -295,7 +296,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
private AcDream.Core.Vfx.PhysicsScriptRunner? _scriptRunner;
|
private AcDream.Core.Vfx.PhysicsScriptRunner? _scriptRunner;
|
||||||
private AcDream.Content.Vfx.RetailPhysicsScriptLoader? _physicsScriptLoader;
|
private AcDream.Content.Vfx.RetailPhysicsScriptLoader? _physicsScriptLoader;
|
||||||
private AcDream.App.Rendering.Vfx.EntityEffectController? _entityEffects;
|
private AcDream.App.Rendering.Vfx.EntityEffectController? _entityEffects;
|
||||||
private double _physicsScriptGameTime;
|
|
||||||
private AcDream.App.Rendering.ParticleRenderer? _particleRenderer;
|
private AcDream.App.Rendering.ParticleRenderer? _particleRenderer;
|
||||||
private readonly AcDream.App.Rendering.RetailAlphaQueue _retailAlphaQueue = new();
|
private readonly AcDream.App.Rendering.RetailAlphaQueue _retailAlphaQueue = new();
|
||||||
// Retail GameSky copies SkyObject.PesObjectId into CelestialPosition but
|
// Retail GameSky copies SkyObject.PesObjectId into CelestialPosition but
|
||||||
|
|
@ -2569,7 +2569,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
_animationPresenter,
|
_animationPresenter,
|
||||||
_staticAnimationScheduler!,
|
_staticAnimationScheduler!,
|
||||||
_liveWorldOrigin,
|
_liveWorldOrigin,
|
||||||
() => _physicsScriptGameTime);
|
_updateFrameClock);
|
||||||
var originCoordinator =
|
var originCoordinator =
|
||||||
new AcDream.App.World.LiveEntityWorldOriginCoordinator(
|
new AcDream.App.World.LiveEntityWorldOriginCoordinator(
|
||||||
_liveWorldOrigin,
|
_liveWorldOrigin,
|
||||||
|
|
@ -2642,7 +2642,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
_localPlayerOutbound,
|
_localPlayerOutbound,
|
||||||
() => _playerHost,
|
() => _playerHost,
|
||||||
() => _playerServerGuid,
|
() => _playerServerGuid,
|
||||||
() => _physicsScriptGameTime,
|
_updateFrameClock,
|
||||||
() => LiveSession,
|
() => LiveSession,
|
||||||
PublishLocalPhysicsTimestamps,
|
PublishLocalPhysicsTimestamps,
|
||||||
AimTeleportDestination,
|
AimTeleportDestination,
|
||||||
|
|
@ -3800,16 +3800,16 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
Console.Error.WriteLine($"[live-entity-teardown] {error}");
|
Console.Error.WriteLine($"[live-entity-teardown] {error}");
|
||||||
}
|
}
|
||||||
|
|
||||||
double frameSeconds =
|
AcDream.App.Update.UpdateFrameTiming frameTiming = _updateFrameClock.Advance(
|
||||||
AcDream.App.World.RetailLiveFrameCoordinator.NormalizeDeltaSeconds(dt);
|
new AcDream.App.Update.UpdateFrameInput(dt));
|
||||||
float frameDelta = (float)frameSeconds;
|
double frameSeconds = frameTiming.SimulationDeltaSeconds;
|
||||||
|
float frameDelta = frameTiming.SimulationDeltaSecondsSingle;
|
||||||
|
|
||||||
// Retail ScriptManager::AddScriptInternal (0x0051B310) stamps
|
// Retail ScriptManager::AddScriptInternal (0x0051B310) stamps
|
||||||
// Timer::cur_time at the packet/default-script call site. Publish this
|
// Timer::cur_time at the packet/default-script call site. Publish this
|
||||||
// update frame's clock before streaming and network dispatch, then
|
// update frame's clock before streaming and network dispatch, then
|
||||||
// reuse the same timestamp for animation hooks and the later drain.
|
// reuse the same timestamp for animation hooks and the later drain.
|
||||||
_physicsScriptGameTime += frameSeconds;
|
_scriptRunner?.PublishTime(frameTiming.ScriptTime);
|
||||||
_scriptRunner?.PublishTime(_physicsScriptGameTime);
|
|
||||||
|
|
||||||
// Phase A.1: advance the streaming controller FIRST so the initial
|
// Phase A.1: advance the streaming controller FIRST so the initial
|
||||||
// landblocks are loaded into GpuWorldState before live-session
|
// landblocks are loaded into GpuWorldState before live-session
|
||||||
|
|
@ -5739,7 +5739,6 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
// Use directly from Silk's key callback inverted that order and left
|
// Use directly from Silk's key callback inverted that order and left
|
||||||
// the client walking to a corpse whose server callback was cancelled.
|
// the client walking to a corpse whose server callback was cancelled.
|
||||||
_selectionInteractions?.DrainOutbound();
|
_selectionInteractions?.DrainOutbound();
|
||||||
_scriptRunner?.PublishTime(_physicsScriptGameTime);
|
|
||||||
System.Numerics.Vector3? playerPosition =
|
System.Numerics.Vector3? playerPosition =
|
||||||
_entitiesByServerGuid.TryGetValue(
|
_entitiesByServerGuid.TryGetValue(
|
||||||
_playerServerGuid,
|
_playerServerGuid,
|
||||||
|
|
@ -5792,7 +5791,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
|
||||||
// ParticleManager before ScriptManager. A particle created by a PES
|
// ParticleManager before ScriptManager. A particle created by a PES
|
||||||
// hook therefore begins simulation on the following object frame.
|
// hook therefore begins simulation on the following object frame.
|
||||||
_particleSystem?.Tick(dt);
|
_particleSystem?.Tick(dt);
|
||||||
_scriptRunner?.Tick(_physicsScriptGameTime);
|
_scriptRunner?.Tick(_updateFrameClock.CurrentScriptTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
178
src/AcDream.App/Update/UpdateFrameOrchestrator.cs
Normal file
178
src/AcDream.App/Update/UpdateFrameOrchestrator.cs
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
namespace AcDream.App.Update;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The host-supplied input for one update callback.
|
||||||
|
/// </summary>
|
||||||
|
internal readonly record struct UpdateFrameInput(double HostDeltaSeconds);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The normalized simulation time shared by every time-advancing phase in one
|
||||||
|
/// host update.
|
||||||
|
/// </summary>
|
||||||
|
internal readonly record struct UpdateFrameTiming(
|
||||||
|
double SimulationDeltaSeconds,
|
||||||
|
float SimulationDeltaSecondsSingle,
|
||||||
|
double ScriptTime);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Owns the normalized host delta and the monotonic PhysicsScript clock.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Retail <c>ScriptManager::AddScriptInternal @ 0x0051B310</c> stamps
|
||||||
|
/// PhysicsScript work from <c>Timer::cur_time</c> at the call site. acdream
|
||||||
|
/// uses one update-frame timestamp for packet/default scripts, animation
|
||||||
|
/// hooks, and the later script drain. Absolute liveness time and raw-mouse
|
||||||
|
/// idle time intentionally do not enter this clock.
|
||||||
|
/// </remarks>
|
||||||
|
internal sealed class UpdateFrameClock : IPhysicsScriptTimeSource
|
||||||
|
{
|
||||||
|
public double CurrentScriptTime { get; private set; }
|
||||||
|
|
||||||
|
public UpdateFrameTiming Advance(UpdateFrameInput input)
|
||||||
|
{
|
||||||
|
double deltaSeconds = NormalizeDeltaSeconds(input.HostDeltaSeconds);
|
||||||
|
CurrentScriptTime += deltaSeconds;
|
||||||
|
return new UpdateFrameTiming(
|
||||||
|
deltaSeconds,
|
||||||
|
(float)deltaSeconds,
|
||||||
|
CurrentScriptTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double NormalizeDeltaSeconds(double deltaSeconds) =>
|
||||||
|
double.IsFinite(deltaSeconds)
|
||||||
|
&& deltaSeconds > 0.0
|
||||||
|
&& deltaSeconds <= float.MaxValue
|
||||||
|
? deltaSeconds
|
||||||
|
: 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface IPhysicsScriptTimeSource
|
||||||
|
{
|
||||||
|
double CurrentScriptTime { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface IUpdateFrameTeardownPhase
|
||||||
|
{
|
||||||
|
void RetryPendingTeardowns();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface IUpdateFrameFailureSink
|
||||||
|
{
|
||||||
|
void ReportTeardownFailure(AggregateException error);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface IUpdateFrameScriptClockPublisher
|
||||||
|
{
|
||||||
|
void PublishTime(double scriptTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface IStreamingFramePhase
|
||||||
|
{
|
||||||
|
void Tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface IGameplayInputFramePhase
|
||||||
|
{
|
||||||
|
void Tick(UpdateFrameTiming timing);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface IRetailLiveFramePhase
|
||||||
|
{
|
||||||
|
void Tick(float deltaSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface ILiveEntityLivenessFramePhase
|
||||||
|
{
|
||||||
|
void Tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface ILocalPlayerTeleportFramePhase
|
||||||
|
{
|
||||||
|
void Tick(float deltaSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface IPlayerModeAutoEntryFramePhase
|
||||||
|
{
|
||||||
|
void TryEnter();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface ICameraFramePhase
|
||||||
|
{
|
||||||
|
void Tick(UpdateFrameTiming timing);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Owns the accepted acdream host-update phase graph.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The nested object/network/command/reconcile order remains owned by
|
||||||
|
/// <see cref="IRetailLiveFramePhase"/>. This host order is the accepted TS-53
|
||||||
|
/// adaptation and is not claimed to be the exact retail Client::UseTime order.
|
||||||
|
/// </remarks>
|
||||||
|
internal sealed class UpdateFrameOrchestrator
|
||||||
|
{
|
||||||
|
private readonly IUpdateFrameTeardownPhase _teardown;
|
||||||
|
private readonly IUpdateFrameFailureSink _failureSink;
|
||||||
|
private readonly UpdateFrameClock _clock;
|
||||||
|
private readonly IUpdateFrameScriptClockPublisher _scriptClockPublisher;
|
||||||
|
private readonly IStreamingFramePhase _streaming;
|
||||||
|
private readonly IGameplayInputFramePhase _input;
|
||||||
|
private readonly IRetailLiveFramePhase _liveFrame;
|
||||||
|
private readonly ILiveEntityLivenessFramePhase _liveness;
|
||||||
|
private readonly ILocalPlayerTeleportFramePhase _teleport;
|
||||||
|
private readonly IPlayerModeAutoEntryFramePhase _playerModeAutoEntry;
|
||||||
|
private readonly ICameraFramePhase _camera;
|
||||||
|
|
||||||
|
public UpdateFrameOrchestrator(
|
||||||
|
IUpdateFrameTeardownPhase teardown,
|
||||||
|
IUpdateFrameFailureSink failureSink,
|
||||||
|
UpdateFrameClock clock,
|
||||||
|
IUpdateFrameScriptClockPublisher scriptClockPublisher,
|
||||||
|
IStreamingFramePhase streaming,
|
||||||
|
IGameplayInputFramePhase input,
|
||||||
|
IRetailLiveFramePhase liveFrame,
|
||||||
|
ILiveEntityLivenessFramePhase liveness,
|
||||||
|
ILocalPlayerTeleportFramePhase teleport,
|
||||||
|
IPlayerModeAutoEntryFramePhase playerModeAutoEntry,
|
||||||
|
ICameraFramePhase camera)
|
||||||
|
{
|
||||||
|
_teardown = teardown ?? throw new ArgumentNullException(nameof(teardown));
|
||||||
|
_failureSink = failureSink ?? throw new ArgumentNullException(nameof(failureSink));
|
||||||
|
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
|
||||||
|
_scriptClockPublisher = scriptClockPublisher
|
||||||
|
?? throw new ArgumentNullException(nameof(scriptClockPublisher));
|
||||||
|
_streaming = streaming ?? throw new ArgumentNullException(nameof(streaming));
|
||||||
|
_input = input ?? throw new ArgumentNullException(nameof(input));
|
||||||
|
_liveFrame = liveFrame ?? throw new ArgumentNullException(nameof(liveFrame));
|
||||||
|
_liveness = liveness ?? throw new ArgumentNullException(nameof(liveness));
|
||||||
|
_teleport = teleport ?? throw new ArgumentNullException(nameof(teleport));
|
||||||
|
_playerModeAutoEntry = playerModeAutoEntry
|
||||||
|
?? throw new ArgumentNullException(nameof(playerModeAutoEntry));
|
||||||
|
_camera = camera ?? throw new ArgumentNullException(nameof(camera));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Tick(UpdateFrameInput input)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_teardown.RetryPendingTeardowns();
|
||||||
|
}
|
||||||
|
catch (AggregateException error)
|
||||||
|
{
|
||||||
|
// The canonical teardown tombstone retains unfinished work for the
|
||||||
|
// next frame. Other exception types are programming failures and
|
||||||
|
// deliberately propagate.
|
||||||
|
_failureSink.ReportTeardownFailure(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateFrameTiming timing = _clock.Advance(input);
|
||||||
|
_scriptClockPublisher.PublishTime(timing.ScriptTime);
|
||||||
|
_streaming.Tick();
|
||||||
|
_input.Tick(timing);
|
||||||
|
_liveFrame.Tick(timing.SimulationDeltaSecondsSingle);
|
||||||
|
_liveness.Tick();
|
||||||
|
_teleport.Tick(timing.SimulationDeltaSecondsSingle);
|
||||||
|
_playerModeAutoEntry.TryEnter();
|
||||||
|
_camera.Tick(timing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
using AcDream.App.Update;
|
||||||
|
|
||||||
namespace AcDream.App.World;
|
namespace AcDream.App.World;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -45,9 +47,5 @@ public sealed class RetailLiveFrameCoordinator
|
||||||
}
|
}
|
||||||
|
|
||||||
public static double NormalizeDeltaSeconds(double deltaSeconds) =>
|
public static double NormalizeDeltaSeconds(double deltaSeconds) =>
|
||||||
double.IsFinite(deltaSeconds)
|
UpdateFrameClock.NormalizeDeltaSeconds(deltaSeconds);
|
||||||
&& deltaSeconds > 0.0
|
|
||||||
&& deltaSeconds <= float.MaxValue
|
|
||||||
? deltaSeconds
|
|
||||||
: 0.0;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Update;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Content.Vfx;
|
using AcDream.Content.Vfx;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
|
|
@ -51,10 +52,10 @@ public sealed class RecallTeleportAnimationTests
|
||||||
|
|
||||||
Assert.Equal([0f, 0f, 0f], deltas);
|
Assert.Equal([0f, 0f, 0f], deltas);
|
||||||
Assert.Equal(3, networkDispatches);
|
Assert.Equal(3, networkDispatches);
|
||||||
Assert.Equal(0.0, RetailLiveFrameCoordinator.NormalizeDeltaSeconds(double.NaN));
|
Assert.Equal(0.0, UpdateFrameClock.NormalizeDeltaSeconds(double.NaN));
|
||||||
Assert.Equal(0.0, RetailLiveFrameCoordinator.NormalizeDeltaSeconds(double.NegativeInfinity));
|
Assert.Equal(0.0, UpdateFrameClock.NormalizeDeltaSeconds(double.NegativeInfinity));
|
||||||
Assert.Equal(0.0, RetailLiveFrameCoordinator.NormalizeDeltaSeconds(double.PositiveInfinity));
|
Assert.Equal(0.0, UpdateFrameClock.NormalizeDeltaSeconds(double.PositiveInfinity));
|
||||||
Assert.Equal(0.0, RetailLiveFrameCoordinator.NormalizeDeltaSeconds(double.MaxValue));
|
Assert.Equal(0.0, UpdateFrameClock.NormalizeDeltaSeconds(double.MaxValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
435
tests/AcDream.App.Tests/World/UpdateFrameOrchestratorTests.cs
Normal file
435
tests/AcDream.App.Tests/World/UpdateFrameOrchestratorTests.cs
Normal file
|
|
@ -0,0 +1,435 @@
|
||||||
|
using System.Reflection;
|
||||||
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Update;
|
||||||
|
using AcDream.App.World;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.World;
|
||||||
|
|
||||||
|
public sealed class UpdateFrameOrchestratorTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Tick_PreservesTheCompleteAcceptedPhaseGraph()
|
||||||
|
{
|
||||||
|
var calls = new List<string>();
|
||||||
|
UpdateFrameOrchestrator frame = Create(calls);
|
||||||
|
|
||||||
|
frame.Tick(new UpdateFrameInput(1.0 / 60.0));
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
[
|
||||||
|
"teardown",
|
||||||
|
"clock",
|
||||||
|
"streaming",
|
||||||
|
"input",
|
||||||
|
"objects",
|
||||||
|
"network",
|
||||||
|
"commands",
|
||||||
|
"ordinary-reconcile",
|
||||||
|
"liveness",
|
||||||
|
"teleport",
|
||||||
|
"auto-entry",
|
||||||
|
"camera",
|
||||||
|
],
|
||||||
|
calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConditionalReconciles_RemainAtTheirOwningEdges()
|
||||||
|
{
|
||||||
|
// Checkpoint A freezes only the outer ownership contract. Checkpoints
|
||||||
|
// E and F must replace these probes with production teleport/camera
|
||||||
|
// owner tests before the conditional edges can be claimed guarded.
|
||||||
|
var calls = new List<string>();
|
||||||
|
UpdateFrameOrchestrator frame = Create(
|
||||||
|
calls,
|
||||||
|
teleportPlace: true,
|
||||||
|
inboundCreatedPlayer: true);
|
||||||
|
|
||||||
|
frame.Tick(new UpdateFrameInput(1.0 / 60.0));
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
[
|
||||||
|
"teardown",
|
||||||
|
"clock",
|
||||||
|
"streaming",
|
||||||
|
"input",
|
||||||
|
"objects",
|
||||||
|
"network",
|
||||||
|
"commands",
|
||||||
|
"ordinary-reconcile",
|
||||||
|
"liveness",
|
||||||
|
"teleport-place",
|
||||||
|
"teleport-reconcile",
|
||||||
|
"teleport-reveal",
|
||||||
|
"auto-entry",
|
||||||
|
"inbound-player-projection",
|
||||||
|
"inbound-player-reconcile",
|
||||||
|
"camera",
|
||||||
|
],
|
||||||
|
calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(double.NaN)]
|
||||||
|
[InlineData(double.NegativeInfinity)]
|
||||||
|
[InlineData(double.PositiveInfinity)]
|
||||||
|
[InlineData(-1.0)]
|
||||||
|
[InlineData(0.0)]
|
||||||
|
[InlineData(double.MaxValue)]
|
||||||
|
public void InvalidSimulationDelta_BecomesZeroWithoutSuppressingOuterPhases(
|
||||||
|
double hostDelta)
|
||||||
|
{
|
||||||
|
var calls = new List<string>();
|
||||||
|
var observed = new FrameObservations();
|
||||||
|
UpdateFrameOrchestrator frame = Create(calls, observed: observed);
|
||||||
|
|
||||||
|
frame.Tick(new UpdateFrameInput(hostDelta));
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
[
|
||||||
|
"teardown", "clock", "streaming", "input", "objects",
|
||||||
|
"network", "commands", "ordinary-reconcile", "liveness",
|
||||||
|
"teleport", "auto-entry", "camera",
|
||||||
|
],
|
||||||
|
calls);
|
||||||
|
Assert.Equal([0.0], observed.PublishedTimes);
|
||||||
|
Assert.Equal([0.0], observed.Input.Select(value => value.SimulationDeltaSeconds));
|
||||||
|
Assert.Equal([0f], observed.LiveDeltas);
|
||||||
|
Assert.Equal([0f], observed.TeleportDeltas);
|
||||||
|
Assert.Equal([0.0], observed.Camera.Select(value => value.SimulationDeltaSeconds));
|
||||||
|
Assert.All(
|
||||||
|
observed.Input.Concat(observed.Camera),
|
||||||
|
timing => Assert.Equal(0.0, timing.ScriptTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Clock_AdvancesOnceAndPublishesTheSameTimeToEveryConsumer()
|
||||||
|
{
|
||||||
|
var calls = new List<string>();
|
||||||
|
var observed = new FrameObservations();
|
||||||
|
UpdateFrameOrchestrator frame = Create(calls, observed: observed);
|
||||||
|
|
||||||
|
frame.Tick(new UpdateFrameInput(0.25));
|
||||||
|
frame.Tick(new UpdateFrameInput(double.NaN));
|
||||||
|
frame.Tick(new UpdateFrameInput(0.5));
|
||||||
|
|
||||||
|
Assert.Equal([0.25, 0.25, 0.75], observed.PublishedTimes);
|
||||||
|
Assert.Equal(
|
||||||
|
[0.25, 0.25, 0.75],
|
||||||
|
observed.Input.Select(value => value.ScriptTime));
|
||||||
|
Assert.Equal(
|
||||||
|
[0.25, 0.25, 0.75],
|
||||||
|
observed.Camera.Select(value => value.ScriptTime));
|
||||||
|
Assert.Equal([0.25f, 0f, 0.5f], observed.LiveDeltas);
|
||||||
|
Assert.Equal([0.25f, 0f, 0.5f], observed.TeleportDeltas);
|
||||||
|
Assert.Equal(
|
||||||
|
[0.25, 0.0, 0.5],
|
||||||
|
observed.Input.Select(value => value.SimulationDeltaSeconds));
|
||||||
|
Assert.Equal(
|
||||||
|
[0.25, 0.0, 0.5],
|
||||||
|
observed.Camera.Select(value => value.SimulationDeltaSeconds));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Clock_RejectsFiniteDeltaAboveTheSinglePrecisionConsumerRange()
|
||||||
|
{
|
||||||
|
double tooLarge = (double)float.MaxValue * 2.0;
|
||||||
|
|
||||||
|
Assert.True(double.IsFinite(tooLarge));
|
||||||
|
Assert.Equal(0.0, UpdateFrameClock.NormalizeDeltaSeconds(tooLarge));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AggregateTeardownFailure_IsReportedAndRetriedNextFrame()
|
||||||
|
{
|
||||||
|
var calls = new List<string>();
|
||||||
|
var teardown = new RecordingTeardown(calls)
|
||||||
|
{
|
||||||
|
Failure = new AggregateException(new InvalidOperationException("transient")),
|
||||||
|
};
|
||||||
|
var failures = new RecordingFailureSink();
|
||||||
|
UpdateFrameOrchestrator frame = Create(
|
||||||
|
calls,
|
||||||
|
teardown: teardown,
|
||||||
|
failureSink: failures);
|
||||||
|
|
||||||
|
frame.Tick(new UpdateFrameInput(0.1));
|
||||||
|
teardown.Failure = null;
|
||||||
|
frame.Tick(new UpdateFrameInput(0.1));
|
||||||
|
|
||||||
|
Assert.Equal(2, teardown.Attempts);
|
||||||
|
Assert.Single(failures.Errors);
|
||||||
|
string[] oneFrame =
|
||||||
|
[
|
||||||
|
"teardown", "clock", "streaming", "input", "objects", "network",
|
||||||
|
"commands", "ordinary-reconcile", "liveness", "teleport",
|
||||||
|
"auto-entry", "camera",
|
||||||
|
];
|
||||||
|
Assert.Equal(oneFrame.Concat(oneFrame), calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NonAggregateTeardownFailure_PropagatesBeforeTimeOrLaterPhases()
|
||||||
|
{
|
||||||
|
var calls = new List<string>();
|
||||||
|
var teardown = new RecordingTeardown(calls)
|
||||||
|
{
|
||||||
|
Failure = new InvalidOperationException("fatal"),
|
||||||
|
};
|
||||||
|
UpdateFrameOrchestrator frame = Create(calls, teardown: teardown);
|
||||||
|
|
||||||
|
InvalidOperationException error = Assert.Throws<InvalidOperationException>(
|
||||||
|
() => frame.Tick(new UpdateFrameInput(0.1)));
|
||||||
|
|
||||||
|
Assert.Equal("fatal", error.Message);
|
||||||
|
Assert.Equal(["teardown"], calls);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OrchestrationTypes_UseOnlyTheExplicitTypedOwnerGraph()
|
||||||
|
{
|
||||||
|
Type[] expectedFieldTypes =
|
||||||
|
[
|
||||||
|
typeof(IUpdateFrameTeardownPhase),
|
||||||
|
typeof(IUpdateFrameFailureSink),
|
||||||
|
typeof(UpdateFrameClock),
|
||||||
|
typeof(IUpdateFrameScriptClockPublisher),
|
||||||
|
typeof(IStreamingFramePhase),
|
||||||
|
typeof(IGameplayInputFramePhase),
|
||||||
|
typeof(IRetailLiveFramePhase),
|
||||||
|
typeof(ILiveEntityLivenessFramePhase),
|
||||||
|
typeof(ILocalPlayerTeleportFramePhase),
|
||||||
|
typeof(IPlayerModeAutoEntryFramePhase),
|
||||||
|
typeof(ICameraFramePhase),
|
||||||
|
];
|
||||||
|
|
||||||
|
FieldInfo[] fields = typeof(UpdateFrameOrchestrator).GetFields(
|
||||||
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
|
Assert.Equal(
|
||||||
|
expectedFieldTypes.OrderBy(type => type.FullName),
|
||||||
|
fields.Select(field => field.FieldType).OrderBy(type => type.FullName));
|
||||||
|
Assert.All(fields, field =>
|
||||||
|
{
|
||||||
|
Assert.NotEqual(typeof(GameWindow), field.FieldType);
|
||||||
|
Assert.False(typeof(Delegate).IsAssignableFrom(field.FieldType));
|
||||||
|
});
|
||||||
|
|
||||||
|
Type[] phaseInterfaces = expectedFieldTypes.Where(type => type.IsInterface).ToArray();
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
phaseInterfaces,
|
||||||
|
phaseInterface => phaseInterface.IsAssignableFrom(typeof(GameWindow)));
|
||||||
|
|
||||||
|
Type[] productionPhaseOwners = typeof(GameWindow).Assembly.GetTypes()
|
||||||
|
.Where(type => !type.IsAbstract && !type.IsInterface)
|
||||||
|
.Where(type => phaseInterfaces.Any(contract => contract.IsAssignableFrom(type)))
|
||||||
|
.ToArray();
|
||||||
|
foreach (Type owner in productionPhaseOwners)
|
||||||
|
{
|
||||||
|
FieldInfo[] ownerFields = owner.GetFields(
|
||||||
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
|
Assert.DoesNotContain(ownerFields, field => field.FieldType == typeof(GameWindow));
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
ownerFields,
|
||||||
|
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
typeof(GameWindow).GetFields(BindingFlags.Instance | BindingFlags.NonPublic),
|
||||||
|
field => field.Name == "_physicsScriptGameTime");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PhysicsScriptClockConsumers_RetainTheTypedSourceNotGameWindowClosures()
|
||||||
|
{
|
||||||
|
Type[] consumers =
|
||||||
|
[
|
||||||
|
typeof(DatLiveEntityProjectionMaterializer),
|
||||||
|
typeof(AcDream.App.Physics.LiveEntityNetworkUpdateController),
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach (Type consumer in consumers)
|
||||||
|
{
|
||||||
|
FieldInfo clock = Assert.Single(
|
||||||
|
consumer.GetFields(BindingFlags.Instance | BindingFlags.NonPublic),
|
||||||
|
field => field.Name == "_gameTime");
|
||||||
|
Assert.Equal(typeof(IPhysicsScriptTimeSource), clock.FieldType);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
consumer.GetFields(BindingFlags.Instance | BindingFlags.NonPublic),
|
||||||
|
field => field.FieldType == typeof(GameWindow));
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.False(typeof(IPhysicsScriptTimeSource).IsAssignableFrom(typeof(GameWindow)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProductionFrame_PublishesPhysicsScriptTimeExactlyOnce()
|
||||||
|
{
|
||||||
|
string source = File.ReadAllText(Path.Combine(
|
||||||
|
FindRepoRoot(),
|
||||||
|
"src",
|
||||||
|
"AcDream.App",
|
||||||
|
"Rendering",
|
||||||
|
"GameWindow.cs"));
|
||||||
|
|
||||||
|
Assert.Equal(1, source.Split("PublishTime(", StringSplitOptions.None).Length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UpdateFrameOrchestrator Create(
|
||||||
|
List<string> calls,
|
||||||
|
RecordingTeardown? teardown = null,
|
||||||
|
RecordingFailureSink? failureSink = null,
|
||||||
|
FrameObservations? observed = null,
|
||||||
|
bool teleportPlace = false,
|
||||||
|
bool inboundCreatedPlayer = false)
|
||||||
|
{
|
||||||
|
teardown ??= new RecordingTeardown(calls);
|
||||||
|
failureSink ??= new RecordingFailureSink();
|
||||||
|
return new UpdateFrameOrchestrator(
|
||||||
|
teardown,
|
||||||
|
failureSink,
|
||||||
|
new UpdateFrameClock(),
|
||||||
|
new RecordingClockPublisher(calls, observed),
|
||||||
|
new RecordingStreaming(calls),
|
||||||
|
new RecordingInput(calls, observed),
|
||||||
|
new RecordingLiveFrame(calls, observed),
|
||||||
|
new RecordingLiveness(calls),
|
||||||
|
new RecordingTeleport(calls, observed, teleportPlace),
|
||||||
|
new RecordingAutoEntry(calls),
|
||||||
|
new RecordingCamera(calls, observed, inboundCreatedPlayer));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingTeardown(List<string> calls)
|
||||||
|
: IUpdateFrameTeardownPhase
|
||||||
|
{
|
||||||
|
public Exception? Failure { get; set; }
|
||||||
|
public int Attempts { get; private set; }
|
||||||
|
|
||||||
|
public void RetryPendingTeardowns()
|
||||||
|
{
|
||||||
|
calls.Add("teardown");
|
||||||
|
Attempts++;
|
||||||
|
if (Failure is not null)
|
||||||
|
throw Failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingFailureSink : IUpdateFrameFailureSink
|
||||||
|
{
|
||||||
|
public List<AggregateException> Errors { get; } = [];
|
||||||
|
public void ReportTeardownFailure(AggregateException error) => Errors.Add(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingClockPublisher(
|
||||||
|
List<string> calls,
|
||||||
|
FrameObservations? observed) : IUpdateFrameScriptClockPublisher
|
||||||
|
{
|
||||||
|
public void PublishTime(double scriptTime)
|
||||||
|
{
|
||||||
|
calls.Add("clock");
|
||||||
|
observed?.PublishedTimes.Add(scriptTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingStreaming(List<string> calls) : IStreamingFramePhase
|
||||||
|
{
|
||||||
|
public void Tick() => calls.Add("streaming");
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingInput(
|
||||||
|
List<string> calls,
|
||||||
|
FrameObservations? observed) : IGameplayInputFramePhase
|
||||||
|
{
|
||||||
|
public void Tick(UpdateFrameTiming timing)
|
||||||
|
{
|
||||||
|
calls.Add("input");
|
||||||
|
observed?.Input.Add(timing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingLiveFrame(
|
||||||
|
List<string> calls,
|
||||||
|
FrameObservations? observed) : IRetailLiveFramePhase
|
||||||
|
{
|
||||||
|
public void Tick(float deltaSeconds)
|
||||||
|
{
|
||||||
|
observed?.LiveDeltas.Add(deltaSeconds);
|
||||||
|
calls.Add("objects");
|
||||||
|
calls.Add("network");
|
||||||
|
calls.Add("commands");
|
||||||
|
calls.Add("ordinary-reconcile");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingLiveness(List<string> calls)
|
||||||
|
: ILiveEntityLivenessFramePhase
|
||||||
|
{
|
||||||
|
public void Tick() => calls.Add("liveness");
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingTeleport(
|
||||||
|
List<string> calls,
|
||||||
|
FrameObservations? observed,
|
||||||
|
bool place)
|
||||||
|
: ILocalPlayerTeleportFramePhase
|
||||||
|
{
|
||||||
|
public void Tick(float deltaSeconds)
|
||||||
|
{
|
||||||
|
observed?.TeleportDeltas.Add(deltaSeconds);
|
||||||
|
if (!place)
|
||||||
|
{
|
||||||
|
calls.Add("teleport");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
calls.Add("teleport-place");
|
||||||
|
calls.Add("teleport-reconcile");
|
||||||
|
calls.Add("teleport-reveal");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingAutoEntry(List<string> calls)
|
||||||
|
: IPlayerModeAutoEntryFramePhase
|
||||||
|
{
|
||||||
|
public void TryEnter() => calls.Add("auto-entry");
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingCamera(
|
||||||
|
List<string> calls,
|
||||||
|
FrameObservations? observed,
|
||||||
|
bool inboundCreatedPlayer) : ICameraFramePhase
|
||||||
|
{
|
||||||
|
public void Tick(UpdateFrameTiming timing)
|
||||||
|
{
|
||||||
|
if (inboundCreatedPlayer)
|
||||||
|
{
|
||||||
|
calls.Add("inbound-player-projection");
|
||||||
|
calls.Add("inbound-player-reconcile");
|
||||||
|
}
|
||||||
|
|
||||||
|
calls.Add("camera");
|
||||||
|
observed?.Camera.Add(timing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FrameObservations
|
||||||
|
{
|
||||||
|
public List<double> PublishedTimes { get; } = [];
|
||||||
|
public List<UpdateFrameTiming> Input { get; } = [];
|
||||||
|
public List<float> LiveDeltas { get; } = [];
|
||||||
|
public List<float> TeleportDeltas { get; } = [];
|
||||||
|
public List<UpdateFrameTiming> Camera { get; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FindRepoRoot()
|
||||||
|
{
|
||||||
|
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
||||||
|
while (directory is not null)
|
||||||
|
{
|
||||||
|
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
|
||||||
|
return directory.FullName;
|
||||||
|
directory = directory.Parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue