refactor(runtime): own communication and social state

Construct chat history, negotiated channels, friends, squelch, and reply targets in one presentation-independent Runtime owner. Make live routing, retained UI, devtools, and current-runtime projections borrow the exact instances, preserve reconnect reset semantics, and publish failure-isolated reentrant-safe chat commits.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-26 07:50:16 +02:00
parent e321410770
commit c9d25ade50
19 changed files with 684 additions and 117 deletions

View file

@ -27,6 +27,8 @@ public sealed class ChatVM : IDisposable
public const int DefaultDisplayLimit = 20;
private readonly ChatLog _log;
private readonly ChatCommandTargetState _commandTargets;
private readonly bool _ownsCommandTargets;
private readonly int _displayLimit;
private bool _disposed;
@ -41,7 +43,8 @@ public sealed class ChatVM : IDisposable
/// <c>chat.rs::ChatState::last_incoming_tell_sender</c> (line 74 +
/// the assignment at line 152).
/// </summary>
public string? LastIncomingTellSender { get; private set; }
public string? LastIncomingTellSender =>
_commandTargets.LastIncomingTellSender;
/// <summary>
/// Target of the most recent OUTGOING Tell (the player's own
@ -52,7 +55,8 @@ public sealed class ChatVM : IDisposable
/// <c>SenderGuid == 0</c> and the target name in <c>Sender</c> —
/// that's the discriminator we capture here.
/// </summary>
public string? LastOutgoingTellTarget { get; private set; }
public string? LastOutgoingTellTarget =>
_commandTargets.LastOutgoingTellTarget;
/// <summary>
/// Optional callback exposing the live framerate. Wired by
@ -81,35 +85,25 @@ public sealed class ChatVM : IDisposable
/// <see cref="RecentLines"/> call. Must be &gt;= 1. Defaults to
/// <see cref="DefaultDisplayLimit"/>.
/// </param>
public ChatVM(ChatLog log, int displayLimit = DefaultDisplayLimit)
public ChatVM(
ChatLog log,
int displayLimit = DefaultDisplayLimit,
ChatCommandTargetState? commandTargets = null)
{
_log = log ?? throw new ArgumentNullException(nameof(log));
if (displayLimit < 1)
throw new ArgumentOutOfRangeException(nameof(displayLimit), displayLimit, "must be >= 1");
_displayLimit = displayLimit;
_log.EntryAppended += OnEntryAppended;
}
private void OnEntryAppended(ChatEntry entry)
{
// Tell-tracking discriminator (SenderGuid):
// != 0 = real incoming whisper; capture sender for /reply.
// == 0 = our own outgoing echo via OnSelfSent; capture the
// target (Sender field) for /retell.
if (entry.Kind != ChatKind.Tell) return;
if (string.IsNullOrEmpty(entry.Sender)) return;
if (entry.SenderGuid != 0)
LastIncomingTellSender = entry.Sender;
else
LastOutgoingTellTarget = entry.Sender;
_commandTargets = commandTargets ?? new ChatCommandTargetState(_log);
_ownsCommandTargets = commandTargets is null;
}
public void Dispose()
{
if (_disposed)
return;
_log.EntryAppended -= OnEntryAppended;
if (_ownsCommandTargets)
_commandTargets.Dispose();
_disposed = true;
}
@ -132,8 +126,7 @@ public sealed class ChatVM : IDisposable
/// </summary>
public void ResetSessionTargets()
{
LastIncomingTellSender = null;
LastOutgoingTellTarget = null;
_commandTargets.ResetSession();
}
/// <summary>