feat(chat): Phase J - welcome message + own-echo dedup + long-form slash aliases + WeenieError templates

Six fixes from the 2026-04-25 live verify session.

1. ServerMessage (0xF7E0) wired to ChatLog. ACE's
   GameMessageSystemChat - used for the login banner "Welcome to
   Asheron's Call ... powered by ACEmulator ... type @acehelp" plus
   any future server broadcast - rides opcode 0xF7E0. The parser
   shipped in I.5 but the WorldSession.ServerMessageReceived event
   was never subscribed by GameWindow, so the welcome line was
   silently dropped. Subscribed now; same wave wires the missing
   EmoteHeard / SoulEmoteHeard / PlayerKilledReceived events that
   I.5 also left orphan.

2. Drop optimistic /say echo + plumb local-player-guid into ChatLog.
   ACE's HandleActionTalk broadcasts a HearSpeech back to the sender
   too, so we were double-printing every /say (own optimistic +
   server echo). New ChatLog.SetLocalPlayerGuid() pushes the chosen
   character guid in (mirrors VitalsVM pattern); OnLocalSpeech
   detects own-guid match and substitutes Sender="" so the formatter
   's IsOwnSpeaker path renders "You say, ..." instead of
   "+Acdream says, ...". Single line per /say.

3. IsOwnSpeaker check now applies to ChatKind.Channel too. Empty/
   "You" sender -> "[Allegiance] You say, \"text\"" instead of the
   "[Allegiance]  says, \"text\"" double-space hole that Phase I.6's
   OnSelfSent left when echoing legacy ChatChannel sends.

4. Long-form slash aliases: /general /allegiance /patron /vassals
   /monarch /covassals /fellowship /fellow /lookingforgroup
   /roleplay /rp /tr /gen, plus /s as alias for /say. Retail muscle
   memory expected these; the prior parser only recognized /g /a /p
   /v /m /cv /lfg /role and friends, so "/patron hello" fell
   through as /say with the literal "/patron" prefix.

5. WeenieError templates filled in for the codes the user hit:
   - 0x0414 YouAreNotInAllegiance  -> "You are not in an allegiance!"
   - 0x050F YouDoNotBelongToAFellowship -> "You do not belong to a Fellowship."
   Replaces the cryptic "WeenieError 0x0414" / "0x050F" lines.

6. @ command pass-through: ACE handles @help / @acehelp / @tele etc.
   server-side by intercepting Talk text with @ prefix; the user's
   message isn't broadcast and ACE replies via SystemChat. Drop the
   optimistic /say echo so the chat shows only the server's response
   (the SystemChat wiring from #1 surfaces it as [System] {help}).

Tests:
- 11 long-form-alias Theory cases on ChatInputParser.
- 3 own-guid-substitution cases on ChatLog (own match, different
  guid, pre-login fallback).
- Existing PrefixSubstring test refactored to "/genio" since the
  previous "/general" stub is now a real verb.

Solution total: 1021 green (243 Core.Net + 125 UI + 653 Core),
0 warnings, 0 errors. +14 tests.

Acceptance: at login, [System] Welcome to Asheron's Call appears.
Single "You say, \"hi\"" per /say. /allegiance with no allegiance
shows [Allegiance] You say, ... + [System] You are not in an
allegiance!. /patron / /vassals / /monarch route correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-25 21:07:56 +02:00
parent 3f7821c18d
commit 7726f62528
7 changed files with 177 additions and 20 deletions

View file

@ -24,6 +24,7 @@ public sealed class ChatLog
{
private readonly ConcurrentQueue<ChatEntry> _buffer = new();
private readonly int _maxEntries;
private uint _localPlayerGuid;
public ChatLog(int maxEntries = 500)
{
@ -39,6 +40,16 @@ public sealed class ChatLog
public int Count => _buffer.Count;
/// <summary>
/// Push the authoritative local-player GUID from <c>WorldSession</c>.
/// One-way setter — only <c>GameWindow</c> should call it, exactly
/// once per live session. Used by <see cref="OnLocalSpeech"/> to
/// recognize ACE's HearSpeech echo of our own /say (server's
/// HandleActionTalk broadcasts to all in range INCLUDING the
/// sender) and re-render it as <c>"You say, ..."</c>.
/// </summary>
public void SetLocalPlayerGuid(uint guid) => _localPlayerGuid = guid;
// ── Inbound adapters ─────────────────────────────────────────────────────
/// <summary>Local or ranged HearSpeech (0x02BB / 0x02BC).</summary>
@ -51,7 +62,13 @@ public sealed class ChatLog
/// </remarks>
public void OnLocalSpeech(string sender, string text, uint senderGuid, bool isRanged)
{
string effectiveSender = string.IsNullOrEmpty(sender) ? "You" : sender;
// Phase J: ACE's HandleActionTalk broadcasts a HearSpeech echo
// back to the sender too. Detect own echo by guid match and
// substitute "" so the formatter renders "You say, ..." (single
// first-person echo) instead of "+Acdream says, ..."
// (third-person duplicate of our optimistic, already dropped).
bool isOwnEcho = _localPlayerGuid != 0 && senderGuid == _localPlayerGuid;
string effectiveSender = (isOwnEcho || string.IsNullOrEmpty(sender)) ? "You" : sender;
Append(new ChatEntry(
Kind: isRanged ? ChatKind.RangedSpeech : ChatKind.LocalSpeech,
Sender: effectiveSender,

View file

@ -77,6 +77,11 @@ public static class WeenieErrorMessages
// Trade
[0x0529] = "Trade Complete!", // TradeComplete
// Allegiance / Fellowship membership errors (high-frequency
// when player isn't in either group and tries the channel)
[0x0414] = "You are not in an allegiance!", // YouAreNotInAllegiance
[0x050F] = "You do not belong to a Fellowship.", // YouDoNotBelongToAFellowship
// Allegiance
[0x0496] = "Your Allegiance has been dissolved!", // YourAllegianceHasBeenDissolved
[0x0497] = "Your patron's Allegiance to you has been broken!",