fix(diagnostics): keep UI scripts on game-loop time

Preserve sub-millisecond frame deltas with a command-local compensated clock so uncapped diagnostic routes do not outrun materialization and settling. Keep sleep boundaries inclusive, wait timeouts strict, and handle long-duration and invalid-delta cases deterministically.

Record the aligned seven-destination connected route with a full turn at every stop. Release build succeeds and all 6,308 tests pass with five intentional skips.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-19 16:38:49 +02:00
parent 3718e341be
commit b95bb3e21c
4 changed files with 336 additions and 15 deletions

View file

@ -22,8 +22,8 @@ public sealed class RetailUiAutomationScriptRunner
private readonly string? _loadError;
private int _index;
private int _activeIndex = -1;
private long _elapsedMs;
private long _commandStartMs;
private double _commandElapsedMs;
private double _commandElapsedCompensationMs;
private bool _started;
private bool _completed;
@ -65,8 +65,27 @@ public sealed class RetailUiAutomationScriptRunner
{
if (_completed) return;
long addMs = Math.Max(1, (long)Math.Round(deltaSeconds * 1000.0));
_elapsedMs += addMs;
// Preserve sub-millisecond frame deltas. Rounding every uncapped frame
// up to one millisecond makes script time run several times faster than
// wall/game-loop time in light scenes and portal space.
if (double.IsFinite(deltaSeconds) && deltaSeconds > 0d)
{
double addMs = deltaSeconds * 1000d;
double adjustedMs = addMs - _commandElapsedCompensationMs;
double nextElapsedMs = _commandElapsedMs + adjustedMs;
double nextCompensationMs = (nextElapsedMs - _commandElapsedMs) - adjustedMs;
if (_activeIndex == _index &&
double.IsFinite(addMs) &&
double.IsFinite(nextElapsedMs) &&
double.IsFinite(nextCompensationMs))
{
// Kahan summation keeps long fractional-frame waits on their
// exact boundary instead of accumulating cadence-dependent
// drift over tens of minutes.
_commandElapsedMs = nextElapsedMs;
_commandElapsedCompensationMs = nextCompensationMs;
}
}
if (!_started)
{
@ -96,7 +115,10 @@ public sealed class RetailUiAutomationScriptRunner
if (_activeIndex != _index)
{
_activeIndex = _index;
_commandStartMs = _elapsedMs;
// Command-local time avoids losing precision when a diagnostic
// script has already run for minutes or hours.
_commandElapsedMs = 0d;
_commandElapsedCompensationMs = 0d;
}
var command = _commands[_index];
@ -233,7 +255,7 @@ public sealed class RetailUiAutomationScriptRunner
: p.Length >= 2 ? p[1] : "";
if (!TryParseInt(value, out int ms) || ms < 0)
return Stop(command, "usage: sleep <milliseconds> | wait ms <milliseconds>");
return _elapsedMs - _commandStartMs >= ms;
return HasElapsed(ms);
}
private bool DoAssert(ScriptCommand command)
@ -282,10 +304,33 @@ public sealed class RetailUiAutomationScriptRunner
private bool WaitOrTimeout(ScriptCommand command, int timeoutMs, string label)
{
if (_elapsedMs - _commandStartMs <= timeoutMs) return false;
if (!HasExceeded(timeoutMs)) return false;
return Stop(command, $"timed out waiting for {label}");
}
private bool HasElapsed(int durationMs)
{
double elapsedMs = _commandElapsedMs;
if (!double.IsFinite(elapsedMs)) return false;
if (elapsedMs >= durationMs) return true;
// Repeated fractional frame deltas can land a few representable
// doubles below an exact integer-millisecond deadline (for example,
// sixty 1/60-second frames). Treat only machine-scale drift as equal;
// this tolerance is far below any observable script or frame interval.
return durationMs - elapsedMs <= TimingToleranceMs(durationMs);
}
private bool HasExceeded(int durationMs)
{
double elapsedMs = _commandElapsedMs;
if (!double.IsFinite(elapsedMs) || elapsedMs <= durationMs) return false;
return elapsedMs - durationMs > TimingToleranceMs(durationMs);
}
private static double TimingToleranceMs(int durationMs)
=> Math.Max(1e-9d, Math.Abs(durationMs) * 1e-12d);
private bool Stop(ScriptCommand command, string message)
{
_log($"line {command.LineNumber}: {message}; command: {command.Text}");