refactor(runtime): unify generation reset for direct hosts

Move canonical per-session teardown into one retryable Runtime transaction, reduce App reset to projection acknowledgements, and prove the same GameRuntime graph through deterministic no-window lifecycle, gameplay, portal, fault, reconnect, and isolation gates.\n\nCo-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-27 00:43:26 +02:00
parent 7818494116
commit a9a822f206
28 changed files with 2707 additions and 345 deletions

View file

@ -58,7 +58,7 @@ public sealed record LiveSessionStartResult(
public interface ILiveSessionLifecycleHost
{
LiveSessionBinding BindSession(WorldSession session);
void ResetSessionState();
void ResetSessionState(RuntimeGenerationToken retiringGeneration);
void ReportConnecting(string host, int port, string user);
void ReportConnected();
void ApplySelectedCharacter(LiveSessionCharacterSelection selection);
@ -185,12 +185,14 @@ public sealed class LiveSessionController
{
private sealed class SessionScope(
WorldSession session,
ILiveSessionLifecycleHost host)
ILiveSessionLifecycleHost host,
RuntimeGenerationToken generation)
{
private int _teardownStage;
public WorldSession Session { get; } = session;
public ILiveSessionLifecycleHost Host { get; } = host;
public RuntimeGenerationToken Generation { get; } = generation;
public LiveSessionBinding? Binding { get; set; }
public bool HostAttached { get; set; }
public RuntimeTeardownStage CompletedStages { get; private set; }
@ -226,7 +228,7 @@ public sealed class LiveSessionController
}
if (_teardownStage == 3)
{
Host.ResetSessionState();
Host.ResetSessionState(Generation);
CompletedStages |= RuntimeTeardownStage.HostReset;
_teardownStage = 4;
}
@ -247,11 +249,15 @@ public sealed class LiveSessionController
LiveSessionConnectOptions? Options = null,
ILiveSessionLifecycleHost? Host = null);
private sealed record PendingHostReset(
ILiveSessionLifecycleHost Host,
RuntimeGenerationToken Generation);
private readonly object _gate = new();
private readonly ILiveSessionOperations _operations;
private SessionScope? _scope;
private SessionScope? _retiredScope;
private ILiveSessionLifecycleHost? _pendingInitialResetHost;
private PendingHostReset? _pendingInitialReset;
private PendingOperation? _pendingOperation;
private int _operationDepth;
private bool _inWorld;
@ -306,7 +312,7 @@ public sealed class LiveSessionController
_inWorld,
_scope is not null,
_retiredScope is not null,
_pendingInitialResetHost is not null,
_pendingInitialReset is not null,
_pendingOperation is not null,
_operationDepth,
_generation,
@ -460,7 +466,9 @@ public sealed class LiveSessionController
ILiveSessionLifecycleHost host)
{
ILiveSessionLifecycleHost? oldHost =
_scope?.Host ?? _retiredScope?.Host ?? _pendingInitialResetHost;
_scope?.Host
?? _retiredScope?.Host
?? _pendingInitialReset?.Host;
ulong generationBeforeStop = _generation;
try
{
@ -480,6 +488,7 @@ public sealed class LiveSessionController
ILiveSessionLifecycleHost host,
bool resetHost)
{
RuntimeGenerationToken resetGeneration = new(_generation);
ulong generation = ++_generation;
try
{
@ -487,7 +496,12 @@ public sealed class LiveSessionController
if (_generation != generation)
return new LiveSessionStartResult(LiveSessionStartStatus.Deferred);
if (resetHost)
ResetHostBeforeStart(host, generation);
{
ResetHostBeforeStart(
host,
generation,
resetGeneration);
}
if (_generation != generation)
return new LiveSessionStartResult(LiveSessionStartStatus.Deferred);
}
@ -509,7 +523,10 @@ public sealed class LiveSessionController
return new LiveSessionStartResult(LiveSessionStartStatus.Deferred);
Console.WriteLine($"live: connecting to {endpoint} as {options.User}");
WorldSession session = _operations.CreateSession(endpoint);
scope = new SessionScope(session, host);
scope = new SessionScope(
session,
host,
new RuntimeGenerationToken(generation));
_scope = scope;
_inWorld = false;
if (!IsCurrent(scope, generation))
@ -614,7 +631,7 @@ public sealed class LiveSessionController
}
DrainRetiredScope();
DrainPendingInitialReset();
if (_retiredScope is null && _pendingInitialResetHost is null)
if (_retiredScope is null && _pendingInitialReset is null)
_lastTeardownStages = RuntimeTeardownStage.Complete;
}
@ -632,35 +649,38 @@ public sealed class LiveSessionController
private void ResetHostBeforeStart(
ILiveSessionLifecycleHost host,
ulong generation)
ulong generation,
RuntimeGenerationToken resetGeneration)
{
bool requestedHostAlreadyReset = false;
if (_pendingInitialResetHost is { } pending)
if (_pendingInitialReset is { } pending)
{
pending.ResetSessionState();
_pendingInitialResetHost = null;
requestedHostAlreadyReset = ReferenceEquals(pending, host);
pending.Host.ResetSessionState(pending.Generation);
_pendingInitialReset = null;
requestedHostAlreadyReset =
ReferenceEquals(pending.Host, host);
}
if (requestedHostAlreadyReset || _generation != generation)
return;
try
{
host.ResetSessionState();
host.ResetSessionState(resetGeneration);
}
catch
{
_pendingInitialResetHost = host;
_pendingInitialReset =
new PendingHostReset(host, resetGeneration);
throw;
}
}
private void DrainPendingInitialReset()
{
if (_pendingInitialResetHost is not { } host)
if (_pendingInitialReset is not { } pending)
return;
host.ResetSessionState();
_pendingInitialResetHost = null;
pending.Host.ResetSessionState(pending.Generation);
_pendingInitialReset = null;
}
private void Schedule(PendingOperation operation)

View file

@ -24,7 +24,7 @@ public sealed record LiveSessionEnteredWorldBindings(
public sealed record LiveSessionHostBindings(
LiveSessionRoutingFactories Routing,
Action Reset,
Action<RuntimeGenerationToken> Reset,
LiveSessionSelectionBindings Selection,
LiveSessionEnteredWorldBindings EnteredWorld,
Action<string, int, string> Connecting,
@ -84,7 +84,7 @@ public sealed class LiveSessionHost : IRuntimeSessionCommands
private readonly LiveSessionRoutingFactories _routing;
private readonly LiveSessionSelectionBindings _selection;
private readonly LiveSessionEnteredWorldBindings _enteredWorld;
private readonly Action _reset;
private readonly Action<RuntimeGenerationToken> _reset;
private readonly LiveSessionLifecycleHost _lifecycle;
private PendingRouteRollback? _pendingRouteRollback;
@ -188,13 +188,14 @@ public sealed class LiveSessionHost : IRuntimeSessionCommands
}
}
private void ResetSessionState()
private void ResetSessionState(
RuntimeGenerationToken retiringGeneration)
{
// An incompletely detached route can still deliver callbacks into the
// state below. Treat physical route convergence as the same hard
// barrier used by normal LiveSessionBinding teardown.
DrainPendingRouteRollback();
_reset();
_reset(retiringGeneration);
}
private void ApplySelection(LiveSessionCharacterSelection selection)

View file

@ -4,7 +4,7 @@ namespace AcDream.Runtime.Session;
public sealed record LiveSessionLifecycleBindings(
Func<WorldSession, LiveSessionBinding> Bind,
Action Reset,
Action<RuntimeGenerationToken> Reset,
Action<string, int, string> Connecting,
Action Connected,
Action<LiveSessionCharacterSelection> Selected,
@ -42,7 +42,9 @@ public sealed class LiveSessionLifecycleHost : ILiveSessionLifecycleHost
return binding;
}
public void ResetSessionState() => _bindings.Reset();
public void ResetSessionState(
RuntimeGenerationToken retiringGeneration) =>
_bindings.Reset(retiringGeneration);
public void ReportConnecting(string host, int port, string user) =>
_bindings.Connecting(host, port, user);