feat(chat): the talk-focus menu's Tell-to / Squelch entries actually work

Both entries were deliberate no-ops — the code said so — and both showed a
static label where retail shows the selected player's NAME.

Retail builds them in gmMainChatUI::InitTalkFocusMenu @0x004CDC50 and rebuilds
their labels every time the menu opens, substituting the selection through
StringInfo::AddVariable_String (@0x004CD91C / @0x004CD982). So they now read
"Tell to Dww" / "Squelch (ignore) Dww", rebuilt on open from a live selection
provider, and grey out with nothing selected — retail arms the tell slot only
for a talkable target (SetTalkFocusEnabled(2, 1) @0x004CD9B0).

Picking "Tell to X" aims the chat bar at X. That needed one piece of plumbing:
the parser's plain-speech fallthrough returned a null target, so a line typed
under a Tell focus was dropped by the router for having no one to send to.
Parse/Submit now carry an optional default tell target for exactly that case.

"Squelch X" publishes the ALREADY-REGISTERED /squelch verb rather than
reimplementing the request — the ModifyCharacterSquelch wire builder
(CM_Communication::Event_ModifyCharacterSquelch @0x006A42D0) has been there all
along; only the menu path to it was missing.

UiMenu gains an OnOpen seam, because a menu whose Items are fixed at Bind can
only ever say "Tell to Selected". It fires before _open flips so the rebuilt
rows are measured and drawn in the same opening.

Solution builds clean; 14,480 tests pass on the standard hermetic lane filter,
0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-21 06:23:46 +02:00
parent 10304f6dc2
commit 581a61ef0c
6 changed files with 209 additions and 17 deletions

View file

@ -5,6 +5,7 @@ using AcDream.App.UI.Layout;
using AcDream.Core.Chat;
using AcDream.UI.Abstractions;
using AcDream.UI.Abstractions.Panels.Chat;
using AcDream.Runtime.Chat;
namespace AcDream.App.Tests.UI.Layout;
@ -157,6 +158,76 @@ public class ChatWindowControllerTests
Assert.NotNull(ctrl);
}
// ── Talk-focus specials: "Tell to X" / "Squelch (ignore) X" ─────────────
/// <summary>
/// Both entries act on the CURRENT SELECTION and carry its name, and both
/// used to be deliberate no-ops.
/// </summary>
[Fact]
public void TalkFocusSpecials_CarryTheSelectedName_AndActOnIt()
{
var (rootInfo, layout, vm) = BuildTestTree();
var bus = new CaptureBus();
string? selected = "Dww";
ChatWindowController? ctrl = ChatWindowController.Bind(
rootInfo, layout, vm, () => bus, new ChatWindowState(), null, null, NoTex,
selectedTargetName: () => selected);
Assert.NotNull(ctrl);
UiMenu menu = Assert.IsType<UiMenu>(layout.FindElement(0x10000014u));
// Retail substitutes the name into both labels
// (StringInfo::AddVariable_String @0x004CD91C / @0x004CD982).
menu.OnOpen!.Invoke();
Assert.Equal("Squelch (ignore) Dww", menu.Items[0].Label);
Assert.Equal("Tell to Dww", menu.Items[1].Label);
// Picking "Tell to Dww" aims the chat bar at them, so an ordinary typed
// line is sent as a tell rather than being dropped for want of a target.
menu.OnSelect!.Invoke(menu.Items[1].Payload);
ctrl!.Input.OnSubmit!.Invoke("hello");
SendChatCmd tell = Assert.IsType<SendChatCmd>(Assert.Single(bus.Published));
Assert.Equal(ChatChannelKind.Tell, tell.Channel);
Assert.Equal("Dww", tell.TargetName);
Assert.Equal("hello", tell.Text);
// Squelch reaches the already-registered /squelch verb rather than
// reimplementing the request.
bus.Published.Clear();
menu.OnSelect.Invoke(menu.Items[0].Payload);
ExecuteClientCommandCmd squelch =
Assert.IsType<ExecuteClientCommandCmd>(Assert.Single(bus.Published));
Assert.Equal(ClientCommandId.Squelch, squelch.Command);
Assert.Equal("Dww", squelch.Arguments);
}
[Fact]
public void TalkFocusSpecials_AreInertAndUnnamedWithNothingSelected()
{
var (rootInfo, layout, vm) = BuildTestTree();
var bus = new CaptureBus();
ChatWindowController? ctrl = ChatWindowController.Bind(
rootInfo, layout, vm, () => bus, new ChatWindowState(), null, null, NoTex,
selectedTargetName: () => null);
Assert.NotNull(ctrl);
UiMenu menu = Assert.IsType<UiMenu>(layout.FindElement(0x10000014u));
menu.OnOpen!.Invoke();
Assert.Equal("Squelch (ignore)", menu.Items[0].Label);
Assert.Equal("Tell to Selected", menu.Items[1].Label);
// Retail arms the tell slot only once a talkable object is selected
// (SetTalkFocusEnabled(2, 1) @0x004CD9B0).
Assert.False(menu.EnabledProvider!(menu.Items[0].Payload));
Assert.False(menu.EnabledProvider(menu.Items[1].Payload));
menu.OnSelect!.Invoke(menu.Items[1].Payload);
menu.OnSelect.Invoke(menu.Items[0].Payload);
Assert.Empty(bus.Published);
}
// ── Test 2: Transcript is placed as a child of the transcript panel ──────
[Fact]