feat(headless): expose the retail chat-command dispatch seam to a console
HeadlessSessionHost already built ChatCommandRouter.Submit's exact dependencies (LiveChatCommandSurface over the plugin-verb registry, a RuntimeChatCommandFeedback) for LoginCommandSequence — that pipeline is the same one every graphical chat window calls. No lift was needed; this promotes those two ctor locals to fields and adds SubmitConsoleLine, the console's one entry point for a typed line. HeadlessConsoleChatFeedback decorates the real feedback so the console ALSO sees retail's transient SpewBox/ClientLocal interface text (bad-args refusals, unknown-command text) — that path never touches ChatLog, so it never reaches the K2 event-stream renderer added in the next commit. Mutation check (reverted before commit): commenting out _onInterfaceText(text) in HeadlessConsoleChatFeedback.ShowInterfaceText, and swapping SubmitConsoleLine's ChatChannelKind.Say for .Tell, each turn a still-to-be-added HeadlessConsoleTests case red. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
7bff9c649d
commit
51f262b285
2 changed files with 104 additions and 0 deletions
43
src/AcDream.Headless/Hosting/HeadlessConsoleChatFeedback.cs
Normal file
43
src/AcDream.Headless/Hosting/HeadlessConsoleChatFeedback.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
using AcDream.Runtime.Chat;
|
||||
|
||||
namespace AcDream.Headless.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Decorates the real <see cref="IChatCommandFeedback"/> a session already
|
||||
/// builds for <c>LoginCommandSequence</c> so the console ALSO sees retail's
|
||||
/// transient "interface text" — <c>ShowInterfaceText</c>'s SpewBox
|
||||
/// (<c>ClientLocal</c>) path never touches <c>ChatLog</c> (see
|
||||
/// <c>RuntimeCommunicationState.AddText</c>), so it never reaches the
|
||||
/// console's <see cref="HeadlessConsoleRenderer"/> through the normal
|
||||
/// <see cref="AcDream.Runtime.RuntimeChatDelta"/> event stream. The real
|
||||
/// side effect (enqueuing into <c>SpewBoxState</c>, exactly what the
|
||||
/// graphical chat box's own SpewBox overlay would show) still happens via
|
||||
/// <paramref name="inner"/> — this only ADDS the console line, it never
|
||||
/// replaces the canonical behavior.
|
||||
/// </summary>
|
||||
internal sealed class HeadlessConsoleChatFeedback : IChatCommandFeedback
|
||||
{
|
||||
private readonly IChatCommandFeedback _inner;
|
||||
private readonly Action<string> _onInterfaceText;
|
||||
|
||||
internal HeadlessConsoleChatFeedback(
|
||||
IChatCommandFeedback inner,
|
||||
Action<string> onInterfaceText)
|
||||
{
|
||||
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
|
||||
_onInterfaceText = onInterfaceText
|
||||
?? throw new ArgumentNullException(nameof(onInterfaceText));
|
||||
}
|
||||
|
||||
public string? LastIncomingTellSender => _inner.LastIncomingTellSender;
|
||||
|
||||
public string? LastOutgoingTellTarget => _inner.LastOutgoingTellTarget;
|
||||
|
||||
public void ShowInterfaceText(string text)
|
||||
{
|
||||
_inner.ShowInterfaceText(text);
|
||||
_onInterfaceText(text);
|
||||
}
|
||||
|
||||
public void ShowSystemMessage(string text) => _inner.ShowSystemMessage(text);
|
||||
}
|
||||
|
|
@ -183,6 +183,25 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
private readonly IHeadlessBotPolicy _policy;
|
||||
private readonly IDisposable _policySubscription;
|
||||
private readonly HeadlessPluginSession _pluginSession;
|
||||
/// <summary>
|
||||
/// Headless console (docs/plans/2026-09-07-headless-console.md): the
|
||||
/// SAME plugin-verb registry <see cref="_chatCommandSurface"/>'s bus
|
||||
/// forwards to (via <c>TryHandlePluginCommand</c>) and
|
||||
/// <see cref="HeadlessPluginSession.Create"/> hands to every loaded
|
||||
/// plugin. Exposed only so a test can register a verb directly without
|
||||
/// loading a real plugin assembly — production callers reach it
|
||||
/// exclusively through <see cref="SubmitConsoleLine"/> /
|
||||
/// <see cref="LoginCommandSequence"/>, never this field.
|
||||
/// </summary>
|
||||
private readonly AcDream.Core.Plugins.PluginCommandRegistry _pluginCommands;
|
||||
/// <summary>
|
||||
/// Headless console: the SAME retained bus <c>LoginCommandSequence</c>
|
||||
/// submits through — see <see cref="SubmitConsoleLine"/>. One instance
|
||||
/// for the host's whole lifetime; <see cref="CreateEventRoute"/>
|
||||
/// attaches/detaches a fresh <see cref="LiveChatCommandRoute"/> to it on
|
||||
/// every (re)connect, exactly as it does today for login commands.
|
||||
/// </summary>
|
||||
private readonly LiveChatCommandSurface _chatCommandSurface;
|
||||
private readonly LiveSessionHost _liveSession;
|
||||
private readonly RuntimeLocalPlayerFrameController _localPlayerFrame;
|
||||
private readonly HeadlessProcessContentOwner.HeadlessProcessContentLease?
|
||||
|
|
@ -453,6 +472,8 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
Runtime = runtime;
|
||||
Commands = commands;
|
||||
_liveSession = liveSession;
|
||||
_pluginCommands = pluginCommands;
|
||||
_chatCommandSurface = chatCommandSurface;
|
||||
_statusWriter = statusWriter;
|
||||
_localPlayerFrame =
|
||||
runtime.CreateLocalPlayerFrameController(
|
||||
|
|
@ -521,7 +542,20 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
/// </summary>
|
||||
internal HeadlessCharacterOptionsSeeder? OptionsSeeder => _optionsSeeder;
|
||||
internal HeadlessPluginSession Plugins => _pluginSession;
|
||||
/// <summary>Test seam (mirrors <see cref="OptionsSeeder"/>'s own
|
||||
/// pattern): registers a plugin verb directly against the SAME registry
|
||||
/// a real loaded plugin would use, without loading a plugin assembly.
|
||||
/// </summary>
|
||||
internal AcDream.Core.Plugins.PluginCommandRegistry PluginCommands =>
|
||||
_pluginCommands;
|
||||
internal string SessionId => _descriptor.Id;
|
||||
/// <summary>
|
||||
/// Headless console: invoked at the end of every <see cref="Tick"/> so
|
||||
/// console input drains ON the session tick, in order, never on the
|
||||
/// reader thread. <see langword="null"/> (every non-console host) costs
|
||||
/// nothing extra per tick.
|
||||
/// </summary>
|
||||
internal Action? ConsolePump { get; set; }
|
||||
internal string ActiveCharacterName { get; private set; } =
|
||||
string.Empty;
|
||||
internal bool IsPolicyComplete =>
|
||||
|
|
@ -563,6 +597,29 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
_pendingConfirmation = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The headless console's ONE entry point for a typed line — the exact
|
||||
/// pipeline <see cref="LoginCommandSequence"/> already submits through:
|
||||
/// <see cref="ChatCommandRouter.Submit"/> against this host's retained
|
||||
/// <see cref="_chatCommandSurface"/> (plugin verb registry first, then
|
||||
/// retail client/server slash commands, then plain chat). Console
|
||||
/// output for retail's transient interface text (bad-args refusals,
|
||||
/// unknown-command text — never routed through
|
||||
/// <see cref="AcDream.Runtime.RuntimeChatDelta"/>, see
|
||||
/// <see cref="HeadlessConsoleChatFeedback"/>'s own doc) is written via
|
||||
/// <paramref name="onInterfaceText"/>.
|
||||
/// </summary>
|
||||
internal SubmitOutcome SubmitConsoleLine(
|
||||
string line,
|
||||
Action<string> onInterfaceText) =>
|
||||
ChatCommandRouter.Submit(
|
||||
line,
|
||||
new HeadlessConsoleChatFeedback(
|
||||
new RuntimeChatCommandFeedback(Runtime.CommunicationOwner),
|
||||
onInterfaceText),
|
||||
_chatCommandSurface,
|
||||
ChatChannelKind.Say);
|
||||
|
||||
internal RuntimeSessionStartResult Start()
|
||||
{
|
||||
// Campaign LA slice LA1: "started" = session host start — the
|
||||
|
|
@ -607,6 +664,10 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
_localPlayerFrame.RunPostNetworkCommandPhase();
|
||||
Runtime.ActionOwner.CombatAttack.Tick();
|
||||
_policy.Tick(Runtime, Commands);
|
||||
// Headless console: drain any input queued by the background reader
|
||||
// thread since the last tick, in order, on THIS thread — never the
|
||||
// reader thread (see HeadlessConsoleInputReader's own doc).
|
||||
ConsolePump?.Invoke();
|
||||
}
|
||||
|
||||
internal RuntimeTeardownAcknowledgement Stop(string reason = "stopped")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue