chore(chat): CT-D1/D2 — delete the dead ChatPanel, reconcile the stale digest

Campaign CT Group D, closing the campaign.

D1. The ImGui-era ChatPanel has not been constructed anywhere in src/ since
Campaign V deleted AcDream.UI.ImGui. Verified that directly rather than on the
audit's word, then removed it with its three panel-only test files. Those tests
passed, which is exactly the problem: they made the real input surface look
better covered than it is.

ChatVMCombatTests was KEPT — three of its four tests are genuine ChatVM
coverage and only one exercised ChatPanel, so just that method went. Deleting
the file would have quietly dropped real coverage along with the dead kind.
Three doc comments referencing the deleted type were rewritten rather than left
as dangling crefs.

D2. docs/ISSUES.md turned out to be ACCURATE already — #358 and #363 are
recorded CLOSED there, contrary to the audit's summary. What was stale was the
chat DIGEST's "Open" section, which still named four closed issues and claimed
Campaign CH's connected gate was owed. Corrected against ISSUES: genuinely open
are #359, #360, #361 and #366.

The digest also gained a Campaign CT section (the tag mechanism, the MEASURED
tag colour, and what shipped) and three DO-NOT-RETRY rows earned this session:

  - Do not model authored state media with one image per state — the unseen
    indicator's Normal state carries SIX frames and that IS retail's blink.
  - Do not read an element's role from a Binary Ninja field NAME — the names in
    ChatInterface's binder are shifted badly enough to assign a UIElement* into
    a float field.
  - Do not assume our side has a gap because retail has a mechanism. That cost
    this campaign twice in one session: the transcript was claimed unbounded
    when ChatLog has always capped at 500 entries, and C1's auto-scroll was
    planned as a port when UiScrollable already did it.

CT-C4 is deferred and marked so: pure test coverage over behaviour the audit
confirmed already works, changing nothing a user can see.

Solution builds clean; full hermetic gate green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-21 10:27:43 +02:00
parent cbab79d70c
commit 7aae5ba939
9 changed files with 30 additions and 844 deletions

View file

@ -507,8 +507,9 @@ public readonly record struct ChatEntry(
/// <summary>
/// Phase I.7: severity bucket for <see cref="ChatKind.Combat"/>
/// entries. Null for every other kind. Drives the
/// <see cref="ChatPanel"/>'s <c>TextColored</c> color choice.
/// entries. Null for every other kind. Carried through to the transcript
/// on <c>FormattedLine</c>; the per-message colour itself now comes from
/// the retail colour table keyed by <c>LogTextType</c>.
/// </summary>
public Combat.CombatLineKind? CombatKind { get; init; }

View file

@ -1,14 +1,7 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[10.0.8, )",
"resolved": "10.0.8",
"contentHash": "dVbSXGIFNR5nZcv2tOLoWI+a9T4jtFd77IYjuND+QVe360qWgAF7H0WtoopYhRw/+SgpGUTyrkrh+65+ClNnfw=="
}
},
"net10.0": {},
"net10.0/win-x64": {}
}
}

View file

@ -1,225 +0,0 @@
using System.Linq;
using System.Numerics;
using AcDream.Core.Chat;
using AcDream.Core.Combat;
namespace AcDream.UI.Abstractions.Panels.Chat;
/// <summary>
/// The chat panel. Shows the tail of <see cref="ChatLog"/> + an input
/// field at the bottom that submits on Enter.
///
/// <para>
/// Phase I.4 added the input field and slash-command parsing. Supported
/// prefixes (alias-matched against the verb token, not by string-prefix
/// — so <c>/general</c> is NOT <c>/g</c>):
/// <list type="bullet">
/// <item><c>/say &lt;msg&gt;</c> or no prefix → Say (default)</item>
/// <item><c>/t</c> / <c>/tell &lt;name&gt; &lt;msg&gt;</c> → whisper</item>
/// <item><c>/r</c> / <c>/reply &lt;msg&gt;</c> → reply to most recent
/// INCOMING Tell (uses <see cref="ChatVM.LastIncomingTellSender"/>;
/// drops the message if no Tell has arrived yet)</item>
/// <item><c>/g, /f, /a, /m, /p, /v, /cv, /lfg, /trade, /role, /society,
/// /olthoi &lt;msg&gt;</c> → corresponding channel</item>
/// <item>unknown <c>/xyz hello</c> → Say with the literal text intact
/// (matches holtburger fall-through)</item>
/// </list>
/// </para>
///
/// <para>
/// Empty / whitespace-only / target-but-no-message inputs are silently
/// dropped — the input field clears and no command goes out.
/// </para>
/// </summary>
public sealed class ChatPanel : IPanel
{
private const int InputBufferMaxLen = 512;
private readonly ChatVM _vm;
private string _input = string.Empty;
// Phase J Tier 3: tracks the chat-tail size between frames so we
// can auto-scroll the scrollable child to the bottom on new
// entries without yanking the user's manual scroll.
private int _lastRenderedCount;
// Phase K.2: one-shot focus request for the chat input. Set by
// FocusInput() (driven by Tab → ToggleChatEntry); the next Render
// call emits SetKeyboardFocusHere immediately before the input
// field and clears the flag. Without the one-shot semantics, the
// panel would steal focus on every frame and the user could never
// click into another widget.
private bool _focusRequested;
// L.0 follow-up: "Copy mode" — when true, render the chat tail as
// a read-only multi-line text widget the user can click+drag to
// select + Ctrl+C to copy. Trades per-line color for selectability;
// user toggles when they want to grab specific text out of the
// log (item names, coordinates, NPC dialogue, etc).
private bool _copyMode;
public ChatPanel(ChatVM vm)
{
_vm = vm ?? throw new ArgumentNullException(nameof(vm));
}
/// <inheritdoc />
public string Id => "acdream.chat";
/// <inheritdoc />
public string Title => "Chat";
/// <inheritdoc />
public bool IsVisible { get; set; } = true;
/// <summary>
/// Phase K.2: request keyboard focus for the chat input on the
/// NEXT <see cref="Render"/>. One-shot — fires once and resets,
/// so callers (e.g. <c>GameWindow</c>'s Tab handler subscribing to
/// <c>ToggleChatEntry</c>) can drive it on a single key press
/// without trapping the user permanently in the input field.
/// </summary>
public void FocusInput() => _focusRequested = true;
/// <inheritdoc />
public void Render(PanelContext ctx, IPanelRenderer renderer)
{
if (!renderer.Begin(Title))
{
renderer.End();
return;
}
// L.0 follow-up: wrap the entire chat panel body in a single
// outer BeginChild so empty-space clicks anywhere in the body
// (Checkbox row, between Separator and input, etc.) are
// absorbed by BeginChild's drag-trap (an InvisibleButton the
// ImGui renderer adds inside every BeginChild). Without this
// wrapper the chat panel was draggable from any empty body
// pixel — only the inner ##chattail area was protected.
if (!renderer.BeginChild("##chatbody", new System.Numerics.Vector2(0f, 0f)))
{
renderer.EndChild();
renderer.End();
return;
}
// L.0 follow-up: top-of-panel "Copy mode" toggle. When on, the
// chat tail rendering swaps to TextMultilineReadOnly so the
// user can mark + Ctrl+C any text. Off (default) preserves the
// colored per-line render with combat highlights. The checkbox
// sits ABOVE the chat tail (not in the footer) so it's always
// visible regardless of scroll position.
bool copyMode = _copyMode;
if (renderer.Checkbox("Copy mode (select text to Ctrl+C)", ref copyMode))
_copyMode = copyMode;
renderer.Separator();
// Phase J Tier 3: keep the input field at the bottom of the
// window across resizes by reserving footer space and putting
// the chat tail in a scrollable child that fills the rest.
// The reserved footer holds: one Separator + one InputText.
// FrameHeightWithSpacing covers the input; we add a small fudge
// (~6px) for the separator above it.
float footerHeight = renderer.FrameHeightWithSpacing() + 6f;
// Phase I.7: pull the typed-line view so combat entries can
// route through TextColored. Non-combat entries still take
// the plain Text path (visually identical to the I.4 panel).
var lines = _vm.RecentLinesDetailed();
if (_copyMode)
{
// Copy mode: one big read-only multiline text widget
// holding every visible line, joined with newlines. Loses
// per-line color but lets the user click+drag to select
// arbitrary spans of text + Ctrl+C to copy. Sized to fill
// the available space minus the footer.
string joined = lines.Count == 0
? "(no messages yet)"
: string.Join("\n", lines.Select(l => l.Text));
renderer.TextMultilineReadOnly(
"##chattailcopy", joined,
new System.Numerics.Vector2(0f, -footerHeight));
}
else if (renderer.BeginChild("##chattail", new System.Numerics.Vector2(0, -footerHeight)))
{
if (lines.Count == 0)
{
renderer.Text("(no messages yet)");
}
else
{
for (int i = 0; i < lines.Count; i++)
{
var line = lines[i];
if (line.Kind == ChatKind.Combat)
{
// Campaign CH slice CH1: color combat lines from the
// retail LogTextType table (Combat_Self/Combat_Enemy/
// Default per CombatChatTranslator's ACE-cited
// mapping) instead of the CombatLineKind info/
// warning/error severity bucket. Every LogTextType
// CombatChatTranslator emits is in-range (<0x22), so
// the fallback below is defensive only.
Vector4 color = RetailChatColorTable.TryGetColor(line.LogTextType, out var resolved)
? resolved
: ColorForCombat(line.CombatKind ?? CombatLineKind.Info);
renderer.TextColored(color, line.Text);
}
else
{
renderer.Text(line.Text);
}
}
}
// Auto-scroll to bottom only when a new line was appended
// since the last render. Manual user scroll-up isn't fought
// against; new messages will jump the view back down once
// they arrive.
if (lines.Count > _lastRenderedCount)
{
renderer.SetScrollHereY(1.0f);
}
_lastRenderedCount = lines.Count;
}
if (!_copyMode) renderer.EndChild();
// Phase I.4: input field. Backend implementation clears _input
// on submit per the IPanelRenderer contract.
renderer.Separator();
// Phase K.2: honor a pending FocusInput() request — emit
// SetKeyboardFocusHere immediately before the input widget so
// ImGui (or the future custom backend) applies it to that
// field. One-shot: clear the flag after firing.
if (_focusRequested)
{
renderer.SetKeyboardFocusHere();
_focusRequested = false;
}
if (renderer.InputTextSubmit("##chatinput", ref _input, InputBufferMaxLen, out var submitted)
&& submitted is not null)
{
ChatCommandRouter.Submit(submitted, _vm, ctx.Commands, ChatChannelKind.Say);
_input = string.Empty;
}
renderer.EndChild(); // outer ##chatbody
renderer.End();
}
/// <summary>
/// Phase I.7: per-severity color for combat-feedback chat lines.
/// Maps onto holtburger's <c>color_for_tags</c> at chat.rs:330-333
/// (info → yellowish, warning → red incoming, error → deep red).
/// </summary>
public static Vector4 ColorForCombat(CombatLineKind kind) => kind switch
{
CombatLineKind.Info => new Vector4(1.0f, 1.0f, 0.6f, 1.0f),
CombatLineKind.Warning => new Vector4(1.0f, 0.5f, 0.5f, 1.0f),
CombatLineKind.Error => new Vector4(1.0f, 0.3f, 0.3f, 1.0f),
_ => new Vector4(1f, 1f, 1f, 1f),
};
}

View file

@ -393,10 +393,9 @@ public sealed class ChatVM : IDisposable, IChatCommandFeedback
: entry.ChannelName;
/// <summary>
/// Phase I.7: snapshot of the chat tail with kind metadata so
/// <see cref="ChatPanel"/> can pick the right rendering primitive
/// per entry (plain <c>Text</c> for most kinds; <c>TextColored</c>
/// for combat lines, with the rgba chosen from
/// Phase I.7: snapshot of the chat tail with kind metadata so the
/// transcript can pick the right rendering per entry (most kinds take one
/// colour; combat lines take the rgba chosen from
/// <see cref="ChatEntry.CombatKind"/>). Campaign CH slice CH1 also
/// carries <see cref="ChatEntry.LogTextType"/> through — the retail
/// color key, keyed independently of <see cref="ChatKind"/>.
@ -458,10 +457,8 @@ public sealed class ChatVM : IDisposable, IChatCommandFeedback
}
/// <summary>
/// Phase I.7: formatted chat line with kind metadata. The
/// <see cref="ChatPanel"/> switches on <see cref="Kind"/> +
/// <see cref="CombatKind"/> to pick a rendering primitive
/// (<c>Text</c> vs <c>TextColored(rgba)</c>).
/// Phase I.7: formatted chat line with kind metadata. The transcript switches
/// on <see cref="Kind"/> + <see cref="CombatKind"/> to pick a colour.
/// </summary>
/// <param name="LogTextType">
/// Campaign CH slice CH1: the retail wire <c>LogTextType</c> that keys