Move the ordinary shadow-scene drain out of the conditional spatial reconciler and into an explicit final update-frame commit phase. This publishes deltas after streaming, network, teleport, camera, and conditional reconciles even while world simulation is quiesced. Release gate: 3,733 App tests / 3 skips; 8,217 complete-solution tests / 5 skips. Co-Authored-By: OpenAI Codex <codex@openai.com>
198 lines
6.6 KiB
C#
198 lines
6.6 KiB
C#
namespace AcDream.App.Update;
|
|
|
|
using AcDream.App.Streaming;
|
|
|
|
/// <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,
|
|
bool advanceScriptClock = true)
|
|
{
|
|
double deltaSeconds = NormalizeDeltaSeconds(input.HostDeltaSeconds);
|
|
if (advanceScriptClock)
|
|
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);
|
|
}
|
|
|
|
internal interface IUpdateFrameCommitPhase
|
|
{
|
|
void Commit();
|
|
}
|
|
|
|
/// <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 : AcDream.App.Rendering.IGameUpdateFrameRoot
|
|
{
|
|
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;
|
|
private readonly IUpdateFrameCommitPhase _commit;
|
|
private readonly IWorldGenerationAvailability _availability;
|
|
|
|
public UpdateFrameOrchestrator(
|
|
IUpdateFrameTeardownPhase teardown,
|
|
IUpdateFrameFailureSink failureSink,
|
|
UpdateFrameClock clock,
|
|
IUpdateFrameScriptClockPublisher scriptClockPublisher,
|
|
IStreamingFramePhase streaming,
|
|
IGameplayInputFramePhase input,
|
|
IRetailLiveFramePhase liveFrame,
|
|
ILiveEntityLivenessFramePhase liveness,
|
|
ILocalPlayerTeleportFramePhase teleport,
|
|
IPlayerModeAutoEntryFramePhase playerModeAutoEntry,
|
|
ICameraFramePhase camera,
|
|
IUpdateFrameCommitPhase commit,
|
|
IWorldGenerationAvailability? availability = null)
|
|
{
|
|
_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));
|
|
_commit = commit ?? throw new ArgumentNullException(nameof(commit));
|
|
_availability = availability ?? AlwaysAvailableWorldGeneration.Instance;
|
|
}
|
|
|
|
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,
|
|
advanceScriptClock: _availability.IsWorldAvailable);
|
|
_scriptClockPublisher.PublishTime(timing.ScriptTime);
|
|
_streaming.Tick();
|
|
_input.Tick(timing);
|
|
_liveFrame.Tick(timing.SimulationDeltaSecondsSingle);
|
|
if (_availability.IsWorldAvailable)
|
|
_liveness.Tick();
|
|
_teleport.Tick(timing.SimulationDeltaSecondsSingle);
|
|
_playerModeAutoEntry.TryEnter();
|
|
_camera.Tick(timing);
|
|
_commit.Commit();
|
|
}
|
|
}
|