Phase J follow-up after a 2026-04-25 trace where typing /help
produced two identical "Unknown command: help" lines (ACE fires the
text via both GameMessageSystemChat 0xF7E0 and a paired
CommunicationTransientString 0x02EB), and the server's WeenieError
0x0026 trailer rendered cryptically as "WeenieError 0x0026".
Three small changes:
1. WeenieErrorMessages: add 0x0026 ThatIsNotAValidCommand ->
"That is not a valid command." Plus 0x0414 / 0x050F that Phase J
already added are now covered by tests too.
2. ChatLog.OnSystemMessage dedup. Track last system text + arrival
time; if a second identical text shows up within 1 second,
suppress. ACE's two-path send (gag warnings, command errors,
etc.) collapses to a single chat line. Long bursts of repeated
text still skip the duplicates without resetting the timer.
3. Client-side /help and /clear in ChatPanel. Intercepted BEFORE
the parser passes to the server bus:
- /help, /?, /h (case-insensitive) -> render local cheat-sheet
listing acdream's slash prefixes via ChatLog.OnSystemMessage.
Avoids the round-trip to ACE that produced the duplicate
"Unknown command: help" lines AND gives users discoverability.
- /clear, /cls -> drains the chat log so the panel starts empty.
New ChatVM.ShowSystemMessage() + ChatVM.Clear() expose the
minimum surface the panel needs to dispatch client-only feedback
without coupling the panel to ChatLog directly.
12 new tests:
- 3 WeenieErrorMessages template adds (0x0026 / 0x0414 / 0x050F).
- 4 ChatLog dedup cases (immediate dup, different text, triplet,
bookended-by-different-text).
- 5 ChatPanel client-command cases (/help, 3 alias variants,
/clear).
Solution total: 1033 green (243 Core.Net + 130 UI + 660 Core),
0 warnings.
Acceptance: type /help in chat -> local help banner appears, no
server round-trip, no "Unknown command: help" duplicates. Type
/clear -> chat tail empty. Welcome banner + WeenieError-templated
"You are not in an allegiance!" / "You do not belong to a
Fellowship." continue rendering once each.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Three follow-up fixes from the 2026-04-25 live verify session.
1. CRITICAL: BuildTell wire field order. Our outbound layout was
[target_name, message] but ACE's GameActionTell.Handle reads
[message, target_name] (verified against
references/ACE/.../GameActionTell.cs:17-18 verbatim). Result: every
/tell since Phase I.3 has been failing with WeenieError 0x052B
(CharacterNotAvailable) because ACE was looking up the message
text as the recipient name. Swapped the field order in
ChatRequests.BuildTell so message is written first; updated the
pinned BuildTell test to expect the corrected layout. The
WorldSessionChatTests round-trip continues to pass since SendTell
delegates to BuildTell.
2. Retail-style FormatEntry. The user asked for the canonical retail
strings:
/say (own): You say, "text"
/say (incoming): Name says, "text"
/tell (own echo): You tell Caith, "text"
/tell (incoming): Caith tells you, "text"
channel: [Trade] +Acdream says, "text"
/shout (own): You shout, "text"
/shout (incoming):Name shouts, "text"
Discriminators: SenderGuid == 0 distinguishes our own outbound
echoes (set by OnSelfSent) from real incoming whispers (carry the
sender's player guid). Sender == "" or "You" distinguishes our own
/say echoes (OnLocalSpeech substitutes "You" when the wire sender
is empty per holtburger client/messages.rs:476-487).
ChatEntry gains a new ChannelName slot so Channel-kind entries
render with the friendly room name ("Trade") instead of "ch 3".
Falls back to "ch {ChannelId}" when ChannelName isn't populated
(legacy ChatChannel inbound or older callers).
3. Suppress optimistic Channel echo. The user saw duplicates per
/trade /lfg in the live trace:
[ch 0] Trade: hello <-- our optimistic
[ch 3] +Acdream: [Trade] hello <-- ACE's TurbineChat broadcast
ACE's TurbineChatHandler at Network/Handlers/TurbineChatHandler.cs
broadcasts EventSendToRoom to ALL recipients in the room including
the sender, so the canonical echo always arrives via 0xF7DE. Drop
the optimistic OnSelfSent for Turbine kinds in GameWindow's
SendChatCmd handler; trust the server. Legacy ChatChannel paths
(Fellowship / Allegiance / Patron / Monarch / Vassals / CoVassals)
keep the optimistic echo because the legacy 0x0147 broadcast may
not always come back to the sender.
Inbound TurbineChat also stops embedding "[Trade] " into the
message text — passes the friendly name out-of-band via the new
channelName parameter on ChatLog.OnChannelBroadcast.
11 tests updated for the new format strings (8 in ChatVMTests, 1 in
ChatVMCombatTests, 1 BuildTell, plus the format additions cover
incoming/outgoing variants per kind). Solution total: 1007 green
(243 + 114 + 650), 0 warnings.
Tells should now actually deliver. Channel echoes show as
[Trade] +Acdream says, "hello" without the duplicate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three post-launch fixes from the 2026-04-25 live verify session.
1. WeenieError display bug. Many ACE WeenieError / WeenieErrorWithString
codes are *informational*, not error-level — the user saw cryptic
"WeenieError 0x051B: General" / "WeenieError 0x051D" at login, but
those decode as "You have entered the General channel." and
"Turbine Chat is enabled." per ACE WeenieError(WithString).cs
templates. New static helper Core/Chat/WeenieErrorMessages.cs maps
~30 high-frequency codes to retail-faithful templates with `_`
placeholder substitution. ChatLog.OnWeenieError now routes through
Format(); unknown codes still fall back to "WeenieError 0xNNNN[: param]"
so nothing is silently lost. New codes can be added in 30 seconds
when the user reports one.
2. Tell target eats trailing punctuation. Retail muscle memory is
"/t Name, message" — comma is the separator. Our split-on-whitespace
pulled "Name," (with comma) as the target, server returned 0x052B
"That person is not available now." because no such character.
ChatInputParser.TryParseTargeted now strips a trailing ,;:.!? from
the target token so "/t Caith, hi" and "/t Caith hi" both work.
Added 7 Theory cases covering each separator + the long-form alias.
3. TurbineChat routing diagnostics. The user's ACE login showed the
"TurbineChatIsEnabled" + "YouHaveEnteredThe_Channel" notifications
for General/Trade/LFG, confirming TurbineChat IS active server-side.
But outbound /g /trade /lfg might still fall back to legacy
ChatChannel (which the server then rejects). Added diagnostic
Console.WriteLines so the next launch shows:
- "chat: SetTurbineChatChannels parsed enabled=true general=0x... ..."
(when ACE sends the 0x0295 channel-id table)
- "chat: outbound TurbineChat General room=0x... cookie=0x... len=N"
(when SendChatCmd routes a Turbine kind through 0xF7DE)
- "chat: outbound legacy ChatChannel Fellowship id=0x... len=N"
(when SendChatCmd uses the legacy 0x0147 path)
- "chat: SendChatCmd kind=General dropped (turbine.Enabled=false no legacy id)"
(when neither path can dispatch — usually means ACE didn't send
0x0295 yet and the kind is Turbine-only)
Sets up Bug 3 (proper outbound TurbineChat for /g /trade /lfg) for
a follow-up commit once the next live trace shows the actual flow.
18 new tests:
- WeenieErrorMessagesTests: 11 covering known templates + fallback.
- ChatInputParserTests: +7 Theory cases for trailing-punctuation strip.
Solution total: 1007 green (114 UI + 650 Core + 243 Core.Net), 0 warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Full port of holtburger's TurbineChat sidecar wire path:
- TurbineChat.cs: 0xF7DE codec with three payload variants
(EventSendToRoom S->C, RequestSendToRoomById C->S, Response).
10-field outer header (size_first/blob_type/dispatch_type/
target_type/target_id/transport_type/transport_id/cookie/
size_second + payload).
- UTF-16LE turbine string codec with 1-or-2 byte variable-length
prefix (high bit on first byte signals 2-byte form). Mirrors
holtburger's read_turbine_string / write_turbine_string at
references/holtburger/.../messages/chat/turbine.rs:502-544.
- SetTurbineChatChannels.cs: 0x0295 GameEvent sub-opcode parser
(10 x u32 channel ids). Wired through GameEventDispatcher in
WorldSession ctor; routes to GameEventWiring + TurbineChatState.
- ChatChannelInfo.cs (Core): unified record union with Legacy
(channel id + name) and Turbine (room id + chat type +
dispatch type + name) variants, plus IsSelfEchoChannel
predicate (Tells = false, channels = true so optimistic echo
is suppressed where the server will echo).
- TurbineChatState.cs (Core): Enabled flag + 10 cached room ids
+ NextContextId() cookie counter starting at 1.
- WorldSession adds TurbineChatReceived + TurbineChannelsReceived
events; SendTurbineChatTo outbound builds RequestSendToRoomById
+ sends through SendGameAction. ProcessDatagram dispatches
0xF7DE at the top level.
- GameWindow constructs TurbineChatState, subscribes inbound
EventSendToRoom -> ChatLog.OnChannelBroadcast; extends I.3's
SendChatCmd handler to route Turbine kinds (General/Trade/Lfg/
Roleplay/Society/Olthoi) through TurbineChat first, fall back
to legacy ChatChannel send when state.Enabled == false.
Round-trip golden fixtures from holtburger source verified for
all three payload variants + UTF-16LE strings (short + long
prefix + non-ASCII Cafe + empty) + SetTurbineChatChannels.
26 new tests:
- TurbineChatTests, SetTurbineChatChannelsTests in Core.Net.Tests
- ChatChannelInfoTests, TurbineChatStateTests in Core.Tests
Solution total: 960 green (243 Core.Net + 625 Core + 92 UI).
ACE doesn't run a TurbineChat server, so codec is "ready when
needed" for retail-server-emulating setups. Legacy ChatChannel
fallback continues to work for current ACE-against-acdream play.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>