feat(runtime): share chat commands and run login sequence

This commit is contained in:
Erik 2026-08-14 20:27:45 +02:00
parent 5535d0adac
commit 41b15efd4d
57 changed files with 1739 additions and 345 deletions

View file

@ -1,5 +1,6 @@
using System.Runtime.ExceptionServices;
using AcDream.Core.Net;
using AcDream.Runtime.Chat;
namespace AcDream.Runtime.Session;
@ -39,7 +40,8 @@ public sealed record LiveSessionHostBindings(
/// <see cref="EnteredWorld"/>'s narrow <c>SetActiveCharacter(string)</c>
/// fan-out, this exists so a status writer can emit the
/// <c>enteredWorld</c> event's <c>characterId</c> field.</summary>
Action<LiveSessionCharacterSelection> CharacterEntered);
Action<LiveSessionCharacterSelection> CharacterEntered,
LoginCommandSequence? LoginCommands = null);
/// <summary>
/// Runtime host for the one canonical <see cref="LiveSessionController"/>.
@ -47,7 +49,9 @@ public sealed record LiveSessionHostBindings(
/// composition, but never mirrors session, generation, identity, routing, or
/// command state.
/// </summary>
public sealed class LiveSessionHost : IRuntimeSessionCommands
public sealed class LiveSessionHost
: IRuntimeSessionCommands,
IRuntimeLiveSessionFramePhase
{
private sealed class PendingRouteRollback(
ILiveSessionCommandRouting? commands,
@ -98,6 +102,7 @@ public sealed class LiveSessionHost : IRuntimeSessionCommands
private readonly Action<LiveSessionCharacterSelection> _characterEntered;
private readonly Action<RuntimeGenerationToken> _reset;
private readonly LiveSessionLifecycleHost _lifecycle;
private readonly LoginCommandSequence? _loginCommands;
private PendingRouteRollback? _pendingRouteRollback;
public LiveSessionHost(
@ -114,6 +119,7 @@ public sealed class LiveSessionHost : IRuntimeSessionCommands
?? throw new ArgumentNullException(nameof(bindings.EnteredWorld));
_characterEntered = bindings.CharacterEntered
?? throw new ArgumentNullException(nameof(bindings.CharacterEntered));
_loginCommands = bindings.LoginCommands;
ArgumentNullException.ThrowIfNull(_routing.CreateEvents);
ArgumentNullException.ThrowIfNull(_routing.CreateCommands);
ArgumentNullException.ThrowIfNull(bindings.Reset);
@ -176,6 +182,17 @@ public sealed class LiveSessionHost : IRuntimeSessionCommands
RuntimeGenerationToken expectedGeneration) =>
_controller.Stop(expectedGeneration);
/// <summary>
/// Pumps the canonical network session first, then any due login command.
/// Both graphical and headless frame loops use this host boundary so the
/// sequencing contract cannot drift between them.
/// </summary>
public void Tick()
{
_controller.Tick();
_loginCommands?.Tick(_controller.Generation, _controller.IsInWorld);
}
private LiveSessionBinding BindSession(WorldSession session)
{
DrainPendingRouteRollback();
@ -211,6 +228,7 @@ public sealed class LiveSessionHost : IRuntimeSessionCommands
// state below. Treat physical route convergence as the same hard
// barrier used by normal LiveSessionBinding teardown.
DrainPendingRouteRollback();
_loginCommands?.Cancel(retiringGeneration);
_reset(retiringGeneration);
}
@ -234,6 +252,7 @@ public sealed class LiveSessionHost : IRuntimeSessionCommands
_enteredWorld.LoadCharacterSettings(name);
_enteredWorld.ArmPlayerModeAutoEntry();
_characterEntered(selection);
_loginCommands?.EnteredWorld(_controller.Generation);
}
private void RethrowWithRetryableRollback(

View file

@ -228,6 +228,22 @@ public sealed class SessionStatusWriter
error,
});
public void LoginCommandFailed(
string sessionId,
int commandIndex,
string command,
string error) =>
Write(new
{
v = VocabularyVersion,
e = "loginCommandFailed",
t = Now(),
sessionId,
commandIndex,
command,
error,
});
public void Disconnected(string sessionId, string reason)
{
if (!IsEnabled)