feat(headless): add deterministic multi-session scheduler
This commit is contained in:
parent
b299e3738e
commit
7e8acb74dd
6 changed files with 694 additions and 54 deletions
|
|
@ -1,4 +1,3 @@
|
|||
using System.Diagnostics;
|
||||
using AcDream.Headless.Configuration;
|
||||
using AcDream.Headless.Credentials;
|
||||
using AcDream.Headless.Diagnostics;
|
||||
|
|
@ -47,12 +46,15 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
private readonly HeadlessCredentialSecret _credential;
|
||||
private readonly HeadlessDiagnosticWriter _diagnostics;
|
||||
private readonly TimeSpan _reconnectQuiescence;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly HeadlessGenerationResetHost _resetHost = new();
|
||||
private readonly IDisposable _hostLease;
|
||||
private readonly IHeadlessBotPolicy _policy;
|
||||
private readonly IDisposable _policySubscription;
|
||||
private readonly LiveSessionHost _liveSession;
|
||||
private int _disposeStage;
|
||||
private long _reconnectDeadline;
|
||||
private bool _reconnectPending;
|
||||
private ulong _stoppedGeneration;
|
||||
private bool _disposed;
|
||||
|
||||
|
|
@ -70,6 +72,7 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
?? throw new ArgumentNullException(nameof(credential));
|
||||
_diagnostics = diagnostics
|
||||
?? throw new ArgumentNullException(nameof(diagnostics));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_reconnectQuiescence = reconnectQuiescence
|
||||
?? (sessionOperations is null
|
||||
? TimeSpan.FromMilliseconds(2500)
|
||||
|
|
@ -92,7 +95,7 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
gameplay,
|
||||
gameplay,
|
||||
gameplay,
|
||||
TimeProvider: timeProvider,
|
||||
TimeProvider: _timeProvider,
|
||||
Log: message => diagnostics.Message(
|
||||
descriptor.Id,
|
||||
message),
|
||||
|
|
@ -169,9 +172,15 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
|
||||
internal GameRuntime Runtime { get; }
|
||||
internal DirectGameRuntimeCommandAdapter Commands { get; }
|
||||
internal string SessionId => _descriptor.Id;
|
||||
internal string ActiveCharacterName { get; private set; } =
|
||||
string.Empty;
|
||||
internal bool IsPolicyComplete => _policy.IsComplete;
|
||||
internal bool IsReconnectPending => _reconnectPending;
|
||||
internal long ReconnectDeadline => _reconnectPending
|
||||
? _reconnectDeadline
|
||||
: throw new InvalidOperationException(
|
||||
"The headless session has no pending reconnect.");
|
||||
|
||||
internal RuntimeSessionStartResult Start() =>
|
||||
Commands.Session.Start(Runtime.Generation);
|
||||
|
|
@ -182,6 +191,8 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
internal void Tick(double deltaSeconds)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_reconnectPending)
|
||||
return;
|
||||
_ = Runtime.Clock.Advance(deltaSeconds);
|
||||
Runtime.Session.Tick();
|
||||
Runtime.ActionOwner.CombatAttack.Tick();
|
||||
|
|
@ -191,6 +202,28 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
internal RuntimeTeardownAcknowledgement Stop() =>
|
||||
Commands.Session.Stop(Runtime.Generation);
|
||||
|
||||
internal RuntimeSessionStartResult CompletePendingReconnect(
|
||||
long nowTimestamp)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (!_reconnectPending)
|
||||
{
|
||||
return new RuntimeSessionStartResult(
|
||||
RuntimeSessionStartStatus.Inactive,
|
||||
Runtime.Generation);
|
||||
}
|
||||
if (nowTimestamp < _reconnectDeadline)
|
||||
{
|
||||
return new RuntimeSessionStartResult(
|
||||
RuntimeSessionStartStatus.Deferred,
|
||||
Runtime.Generation);
|
||||
}
|
||||
|
||||
_reconnectPending = false;
|
||||
_reconnectDeadline = 0L;
|
||||
return StartLive(reconnect: true);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
|
|
@ -202,6 +235,8 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
{
|
||||
case 0:
|
||||
{
|
||||
_reconnectPending = false;
|
||||
_reconnectDeadline = 0L;
|
||||
RuntimeTeardownAcknowledgement stopped = Stop();
|
||||
if (!stopped.IsComplete)
|
||||
{
|
||||
|
|
@ -266,6 +301,12 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
RuntimeSessionStartStatus.StaleGeneration,
|
||||
Runtime.Generation);
|
||||
}
|
||||
if (_reconnectPending)
|
||||
{
|
||||
return new RuntimeSessionStartResult(
|
||||
RuntimeSessionStartStatus.Deferred,
|
||||
Runtime.Generation);
|
||||
}
|
||||
|
||||
if (reconnect)
|
||||
{
|
||||
|
|
@ -281,19 +322,32 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
"The prior headless session did not quiesce before reconnect."));
|
||||
}
|
||||
|
||||
// ACE confirms CharacterLogOff before its account/session index
|
||||
// releases the retiring socket. Starting a replacement in that
|
||||
// short suffix makes ACE reject both sessions as Account In Use.
|
||||
// This is a one-time connection-lifecycle wait, never a steady
|
||||
// scheduler sleep; K2 moves it onto the absolute-deadline host.
|
||||
if (_reconnectQuiescence > TimeSpan.Zero)
|
||||
{
|
||||
Task.Delay(_reconnectQuiescence)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
// ACE confirms CharacterLogOff before its account/session
|
||||
// index releases the retiring socket. The process scheduler
|
||||
// owns this monotonic deadline so other sessions keep moving
|
||||
// and no command or worker thread blocks.
|
||||
_reconnectDeadline = HeadlessMonotonicTime.Add(
|
||||
_timeProvider,
|
||||
_timeProvider.GetTimestamp(),
|
||||
_reconnectQuiescence);
|
||||
_reconnectPending = true;
|
||||
_diagnostics.Lifecycle(
|
||||
_descriptor.Id,
|
||||
"reconnect-deferred",
|
||||
Runtime);
|
||||
return new RuntimeSessionStartResult(
|
||||
RuntimeSessionStartStatus.Deferred,
|
||||
Runtime.Generation);
|
||||
}
|
||||
}
|
||||
|
||||
return StartLive(reconnect);
|
||||
}
|
||||
|
||||
private RuntimeSessionStartResult StartLive(bool reconnect)
|
||||
{
|
||||
string password = _credential.Reveal();
|
||||
try
|
||||
{
|
||||
|
|
@ -366,8 +420,7 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
OnConfirmationRequest: null,
|
||||
OnConfirmationDone: null,
|
||||
ClientTime: () =>
|
||||
Stopwatch.GetTimestamp()
|
||||
/ (double)Stopwatch.Frequency),
|
||||
Runtime.Clock.SimulationTimeSeconds),
|
||||
new LiveSocialSessionBindings(
|
||||
Runtime.CommunicationOwner.Chat,
|
||||
Runtime.CommunicationOwner.TurbineChat,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue