From e3639a4c1829a1dd1df86c7c83ca2cd7b88f5c2e Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 7 Sep 2026 08:13:24 +0200 Subject: [PATCH] fix(headless): N5 dim only lifecycle/command/portal lines HeadlessConsoleRenderer dimmed every line uniformly, so chat and interface text (player-visible content) read the same washed-out weight as scheduling/session-status noise like "entered world" or "command rejected: ...". Only lifecycle, command, and portal lines are scheduling noise; chat and interface text now print at the terminal's default weight. ChatAndInterfaceTextPrintAtDefaultWeightNeverDimmed was shown to fail against the prior dim-everything WriteLine (mutation: dim parameter not yet threaded through, every call still unconditionally wrapped in the ANSI dim/reset pair) -- the chat line and interface text both carried the dim escape sequence. Co-Authored-By: Claude Fable 5.1 --- .../Hosting/HeadlessConsoleRenderer.cs | 43 ++++++++---- .../HeadlessConsoleTests.cs | 66 +++++++++++++++++++ 2 files changed, 96 insertions(+), 13 deletions(-) diff --git a/src/AcDream.Headless/Hosting/HeadlessConsoleRenderer.cs b/src/AcDream.Headless/Hosting/HeadlessConsoleRenderer.cs index 8832c5bb2..3e60e3a6c 100644 --- a/src/AcDream.Headless/Hosting/HeadlessConsoleRenderer.cs +++ b/src/AcDream.Headless/Hosting/HeadlessConsoleRenderer.cs @@ -28,32 +28,35 @@ internal sealed class HeadlessConsoleRenderer : IRuntimeEventObserver { string? line = HeadlessConsoleChatFormatter.Format(delta.Entry); if (!string.IsNullOrEmpty(line)) - WriteLine(line); + WriteLine(line, dim: false); } /// /// Retail's transient "interface text" (SpewBox, ClientLocal /// type) never touches — /// see RuntimeCommunicationState.AddText — so it never reaches - /// . calls - /// this directly for the SAME text - /// ChatCommandRouter.Submit's ShowInterfaceText path would - /// otherwise only enqueue into the polled SpewBoxState. + /// . HeadlessConsoleSpewBoxPump calls this + /// directly, once per console tick, for whatever text is newly visible + /// in the polled — the + /// SAME seam the graphical overlay's own SpewBox controller reads, so + /// server- and plugin-driven interface text prints here too, not only + /// the console's own submissions. Default weight (N5) — this is + /// player-visible interface text, not scheduling noise. /// - internal void WriteInterfaceText(string text) => WriteLine(text); + internal void WriteInterfaceText(string text) => WriteLine(text, dim: false); public void OnLifecycle(in RuntimeLifecycleDelta delta) { switch (delta.Current) { case RuntimeLifecycleState.InWorld: - WriteLine("entered world"); + WriteLine("entered world", dim: true); break; case RuntimeLifecycleState.Stopping: - WriteLine("disconnecting"); + WriteLine("disconnecting", dim: true); break; case RuntimeLifecycleState.Faulted: - WriteLine("session faulted"); + WriteLine("session faulted", dim: true); break; } } @@ -61,13 +64,21 @@ internal sealed class HeadlessConsoleRenderer : IRuntimeEventObserver public void OnCommand(in RuntimeCommandDelta delta) { if (delta.Status == RuntimeCommandStatus.Rejected) - WriteLine($"command rejected: {delta.Domain} {delta.Text}".TrimEnd()); + { + WriteLine( + $"command rejected: {delta.Domain} {delta.Text}".TrimEnd(), + dim: true); + } } public void OnPortal(in RuntimePortalDelta delta) { if (delta.Portal.IsMaterialized) - WriteLine($"portal -> cell 0x{delta.Portal.DestinationCell:X8}"); + { + WriteLine( + $"portal -> cell 0x{delta.Portal.DestinationCell:X8}", + dim: true); + } } public void OnEntity(in RuntimeEntityDelta delta) @@ -86,9 +97,15 @@ internal sealed class HeadlessConsoleRenderer : IRuntimeEventObserver { } - private void WriteLine(string text) + /// + /// N5 (2026-09-07 review round): only lifecycle/command/portal lines are + /// dimmed — scheduling and session-status noise, not player-visible + /// content. Chat and interface text print at the terminal's default + /// weight. + /// + private void WriteLine(string text, bool dim) { - _output.WriteLine(_useColor ? Dim + text + Reset : text); + _output.WriteLine(_useColor && dim ? Dim + text + Reset : text); _output.Flush(); } } diff --git a/tests/AcDream.Headless.Tests/HeadlessConsoleTests.cs b/tests/AcDream.Headless.Tests/HeadlessConsoleTests.cs index d8991b58f..b36cea786 100644 --- a/tests/AcDream.Headless.Tests/HeadlessConsoleTests.cs +++ b/tests/AcDream.Headless.Tests/HeadlessConsoleTests.cs @@ -365,6 +365,72 @@ public sealed class HeadlessConsoleTests Assert.Equal(expected, HeadlessConsoleChatFormatter.Format(entry)); } + // ── HeadlessConsoleRenderer: N5 dim-weight rules ───────────────────── + + /// + /// N5: chat and interface text are player-visible content, not + /// scheduling noise — they must print at the terminal's default weight, + /// never dimmed, even when color is enabled. + /// + [Fact] + public void ChatAndInterfaceTextPrintAtDefaultWeightNeverDimmed() + { + var output = new StringWriter(); + var renderer = new HeadlessConsoleRenderer(output, useColor: true); + var entry = new RuntimeChatEntry( + Revision: 1, + SenderGuid: 0x50000010u, + Kind: (int)ChatKind.LocalSpeech, + Sender: "Bob", + Text: "hi", + ChannelName: string.Empty); + + renderer.OnChat(new RuntimeChatDelta(default, entry)); + renderer.WriteInterfaceText("Unknown command: /x"); + + string text = output.ToString(); + Assert.DoesNotContain("[2m", text); + Assert.Contains("[Local] Bob: hi", text); + Assert.Contains("Unknown command: /x", text); + } + + /// + /// N5: lifecycle, command, and portal lines are scheduling/session- + /// status noise, not player-visible content — dimmed when color is + /// enabled. + /// + [Fact] + public void LifecycleCommandAndPortalLinesAreDimmedWhenColorIsEnabled() + { + var output = new StringWriter(); + var renderer = new HeadlessConsoleRenderer(output, useColor: true); + + renderer.OnLifecycle(new RuntimeLifecycleDelta( + default, RuntimeLifecycleState.Starting, RuntimeLifecycleState.InWorld)); + renderer.OnCommand(new RuntimeCommandDelta( + default, RuntimeCommandDomain.Chat, 0, RuntimeCommandStatus.Rejected, Text: "boom")); + renderer.OnPortal(new RuntimePortalDelta( + default, + new RuntimePortalSnapshot( + Generation: 1, + RuntimePortalKind.Portal, + Readiness: new RuntimeDestinationReadiness( + 1, 0x12345678u, false, false, 0, true, true, true), + Materialized: true, + Completed: false, + Cancelled: false, + WorldViewportObserved: true, + WorldSimulationAvailable: true, + InvariantFailureCount: 0, + WaitCueShown: false, + PortalMaterializationCount: 1))); + + string[] lines = output.ToString() + .Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); + Assert.Equal(3, lines.Length); + Assert.All(lines, line => Assert.Contains("[2m", line)); + } + // ── HeadlessSessionHost.SubmitConsoleLine: the real dispatch pipeline ─ [Fact]