R1: the timestamp prefix moves from ChatLog.Append (which stamped the
stored BODY, rendering 'Alice says, "13:05:09 hi"') to ChatVM's display
composition — FormatTimestampPrefix(entry.Received) prepends the COMPOSED
line, matching retail's separate-leading-string model (fprintf("%ls%ls",
ts, text) @0x00563e5b; AddTextToScroll receives composed lines). The
prefix renders entry.Received in LOCAL time (retail strftime), invariant
literal colons. The ten defect-pinning test cases across
ChatLogTests/RuntimeCommunicationStateTests are rewritten to pin the
corrected contract (stored bodies stay clean; the composed line carries
the stamp outside the quotes — ChatVMTests).
R2: open option-bearing panels converge on every PlayerDescription seed:
OptionPage.ReloadFromLive (per-row live re-read + gating re-eval, NO
AfterApply flush — the seed just cleared the dirty module),
OptionsPanelController.OnServerOptionsSeeded (active page),
CombatUiController.OnServerOptionsSeeded (SyncControls), wired through
RuntimeSettingsController.ServerOptionsSeeded from the same factory hook
LockUI already uses. Retail cannot reach this state (its panels close
across login); the adaptation exists because retained panels survive the
session boundary — documented at the seam.
R3: tests drive the refresh widget push (model AND checkbox converge) and
ReloadFromLive's no-flush contract. R4: AP-196 addendum names the
headless AutoRepeatAttack false->true effective-default flip and the
characterOptions escape hatch.
Full Release suite: 13,083 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ChatVM gains an OnInterfaceText hook + ShowInterfaceText(text), the
App-layer composition wires it to RuntimeCommunicationState.AddText,
and ChatCommandRouter routes every retail-0x1A command refusal through
it instead of the chat log's 0x00 sink. UI.Abstractions still never
references Runtime directly; unwired hosts (headless, tests) fall back
to the chat log tagged ClientLocal so no text is ever silently lost.
Reclassified per register row AP-183 (DoChannelList/On/Off, DoAllegiance,
DoHouseAvailableList — the last also corrected to retail's own bad-house-
type string instead of a synthesized "Usage:" line) and newly wired two
sites that previously showed nothing at all (DoStupidChannelHack's bare
legacy-channel-verb refusal, DoReply's message-but-no-last-teller
refusal). The generic bad-args fallback now resolves WeenieErrorMessages
0x026 ("That is not a valid command.", retail's HandleFailureEvent(0x26))
instead of synthesizing "Usage: {Usage}". DoSpeaker/DoEndurance/DoTitle
are untouched — already correct at 0x00.
Also closes#367 (DoHelp's "Unknown command" fallback and the degenerate-
prefix refusal now reach the SpewBox too) and retires register row
AP-186, whose own filing proposed exactly this seam shape.
Full Release suite: 12,542 passed / 4 skipped / 0 failed (baseline
12,466/4/0 at ff2784ea).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The user tested Campaign CH's CODE-COMPLETE build live and reported ten
defects (docs/plans/2026-08-09-chat-parity-campaign.md, "User gate —
round 1"). Items A-G are fixed here; the remaining three (extra chat
windows on 1/2/3/4, resize working in only one corner, transparency/
artifacts) are out of scope for a fix and filed as slice CH6.
A. Jump-in-air refusal never fired live: the jump block only ever
evaluated input.Jump inside the grounded-charge or already-charging
branches. PlayerMovementController now detects the press RISING EDGE
while airborne and reports WeenieError.NotGrounded once per press,
leaving the grounded charge/fire path untouched.
B. ChatVM's invented "[System] " prefix is dropped — retail prints
system text bare. [Popup] is unchanged (AP-175).
C. SpewBoxController's color is now the user-pinned exact value
(1, 1, 0.247, 1), the same bright yellow as an incoming Tell.
Register row AP-178 updated: color CLOSES, size/position/font stay
open per the user's live report that they still differ.
D. Closes#329: PortalTunnelPresentation now emits the portal wait cue
unconditionally on every rotation-segment boundary, matching
gmSmartBoxUI::UseTime's decompiled else-arm exactly instead of gating
on a 5-second hold local transits never reached. PortalWaitNotice
Controller now renders it in the same pinned yellow as item C.
Register row AP-150 retired.
E. Closes#362: new ClientCommandResponses.cs parses and renders the
four previously-unhandled inbound GameEvents (ChannelIndex,
ChannelList, AvailableHouses, AllegianceInfoResponse), each ported
line-for-line from the named-retail decomp's inbound handlers.
Register row TS-70 retired.
F. ChatWindowController.WrapText now splits on embedded '\n'/'\r\n'
first, then word-wraps each segment independently — server text like
/help's reply no longer collapses onto one line.
G. The chat input field's right edge no longer holds a fixed absolute
pixel position across a window resize; Bind now upgrades it to
retail edge-mode 1 (UiLayoutPolicy) or the AnchorEdges.Right stretch
fallback so it tracks the window's client width instead of
overflowing past a narrower resize.
Full Release suite: 12,247 passed / 4 skipped / 0 failed (baseline
12,221/4/0 + 26 new tests across items A, E, F, G).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reworks Campaign CH slice CH2 per the REJECT-review findings doc
(docs/research/2026-08-09-ch2-review-findings.md).
BLOCKER 1 — SpewBoxController never rendered a line and leaked its
pending queue. LinesProvider only ran through UiText.OnDraw, which
gates on Visible — and the box started invisible, so the provider (the
sole caller of SpewBoxState.Tick) never ran. Gave the controller an
explicit per-frame Tick(now) driven by UiRoot's global-message-3
broadcast (a zero-size GlobalTimeSink child, the same pattern
VendorUiController.DragOverGlobalTimeSink already uses), matching
retail's gmSpewBoxUI::Update. LinesProvider now only returns the
cache. Tests rewritten to drive root.Tick(...) instead of calling the
provider directly, plus new coverage for visibility-without-a-draw,
queue-drain-without-a-draw, and bounded-queue-across-many-ticks.
BLOCKER 2 — re-derived the HandleFailureEvent routing table from the
PDB-paired binary instead of the pseudo-C's ~33-char string previews.
tools/pdb-extract/sweep_weenie_strings.py sweeps every push imm32 in
VA 0x571990-0x575480, dereferences into .rdata/.data, and decodes the
full UTF-16LE literal. Added the 5 ids dispatched via else-if (missed
by case-label enumeration), resolved 0x4F8 (previously excluded),
fixed 18 wrong strings (16 the review flagged + 2 more — 0x4E9 and
0x518 — an automated diff between every swept literal and the landed
table found). Every changed row cross-checked against ACE's
WeenieError/WeenieErrorWithString enum doc comments; both oracles
agreed on every row, including a case where the review's own proposed
text for the new 0x4E8 row was itself wrong (it was 0x4E9's text) —
corrected via the else-if block's own instruction address plus the ACE
cross-check. Pinned table count: 344 (338 + 5 + 0x4F8).
SHOULD-FIX 1 — RuntimeCommunicationState.ResetSpewBox was dead code;
folded into the ChatIdentity generation-reset stage (same lifetime
boundary), with a reset assertion added to the existing populated-reset
test.
SHOULD-FIX 2 — AddText trimmed only the trailing end and invented an
empty-string early return; retail's AddTextToScroll trims both ends
(trim(&str, 1, 1, ws)) and has no empty guard. Both retired.
SHOULD-FIX 3 — ShowWeenieError bypassed the AddText chokepoint via
ChatLog.OnWeenieError (hardcoded LogTextType 0x00); routed through
Communication.AddText(Resolve(code, param)) instead, and
ChatLog.OnWeenieError is deleted — GameEventWiring's legacy no-router
fallback now resolves + calls OnSystemMessage directly.
SHOULD-FIX 4 — retail's HandleFailureEvent switch has no default case;
an unmapped id now resolves to a null Text (silence toward the
player) instead of the invented "WeenieError 0xNNNN" hex fallback,
with a diagnostics-only console log line for the id.
NITs — AP-TBD placeholders corrected to their real register rows
(AP-178, not the unrelated AP-177 lifetime row); filed AP-180 for the
windowId dual-destination gap and corrected three stale "lands with
CH2" comments; extended SpewBoxLayoutDumpDiagnostic from dats.Portal
to dats.Local and found the SpewBox element for real — LayoutDesc
0x21000011, element 0x10000048, size 450x72, MaxConcurrentItems
(ListBox property 0x10000028) = 4, not retail's code default of 1.
AP-178 narrowed accordingly; SpewBoxState.MaxConcurrentItems and
SpewBoxController's extent/anchor/OneLine are now authored rather than
placeholder (absolute screen position and colour remain open); fixed
the "19 ids... lists 18" miscount by retiring the stale paragraph in
the class doc rewrite; aligned the UseDone handler's silent-status
check with the other two WeenieError handlers.
Full Release suite: 11,914 passed / 4 skipped / 0 failed (build 0
errors).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Applies the Opus review findings on CH1 (172c6f9a), the exact retail chat
color table. Two blockers plus should-fixes/nits, one commit:
BLOCKER 1 — LegacyChannelChatType.Resolve's channel-bit table was wrong.
Binary Ninja renders retail's `neg esi; sbb esi, esi` idiom (a branchless
select between Channel 0x08 and Channel_Send 0x09) as the trivial pseudo-C
`esi - esi` (always 0), hiding the real values. Corrected by decoding the
raw bytes at the PDB-paired binary: HEAR sbb site VA 0x00570F0A (mask -6 ->
0x08), SEND sbb site VA 0x00570D4F (mask -5 -> 0x09). The generic
admin/audit/sentinel catch-all is Channel/Channel_Send, NOT Abuse (0x0E) —
Abuse is retail's ONLY 0x0E producer (bit 0x0001). The unnamed
FellowBroadcast bit (0x4000000) is hear=Channel(0x08)/send=Fellowship(0x13),
not a flat 0x13. ACE's PDB-sourced Channel enum corroborates. Introduces
`RetailLogTextType`, the 34-value named enum for the wire LogTextType space
(values only, no color — Core stays presentation-free).
BLOCKER 2 — three ChatLog.OnSystemMessage sinks (ChatVM.ShowSystemMessage,
LiveSessionRuntimeFactory's ShowSystemMessage delegate,
HeadlessGameplayOperations.DisplayMessage) were typing ALL
ClientCommandController output 0x1A (bright red), including informational
command output (@version, /loc, friends list, usage lines). Retail types
the great majority of that output 0x00 Default (green) and reserves 0x1A
for genuine refusals/errors. Reverted to 0x00 with a comment noting the
refusal-vs-info split lands with CH2's SpewBox producer rewiring. The five
App composition sites that pass 0x1A for actual refusal text
(InteractionRetainedUiComposition, SessionPlayerComposition) were already
correct and are untouched (aside from converting the literal to the new
enum).
Also: AP-176 divergence-register row for OnWeenieError/OnCombatLine's
single-type approximation of retail's per-code/per-message dispatch; a
carry-forward test for the out-of-range LogTextType color fallback in
ChatWindowController; decomp-confirmed anchors replacing ACE-inferred
citations in CombatChatTranslator and ChatLog.OnPlayerKilled; required
(non-optional) logTextType parameters on OnLocalSpeech/OnTellReceived/
OnCombatLine/OnSelfSent since no production caller relied on a default;
LegacyChannelChatType.Resolve's parameter renamed channelBit -> channelId
with a doc note on multi-bit ids; corrections to the color-table research
doc's §3.3 wire tables; and issue #359 for the pre-existing (not
CH1-introduced) 0x019E PlayerKilled participant-suppression gap retail has
and acdream lacks.
dotnet build clean; full Release suite 11,835 passed / 4 skipped / 0 failed
(11,839 total), up from the CH1 baseline of 11,833/4/0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Retail colors chat lines by the 34-value wire LogTextType (ACE's
ChatMessageType), NOT by acdream's synthetic 9-value ChatKind. The old
ChatWindowController.RetailChatColor(ChatKind) collapsed distinct retail
colors onto one bucket per ChatKind — e.g. every Channel line rendered
colorLightBlue (Magic's slot) when retail's actual palette spans five
different colors across the Turbine rooms and legacy allegiance family.
Ports ChatInterface::BuildChatColorLookupTable @0x004F31C0 verbatim
(RetailChatColorTable, all 34 RGBA floats read from the PDB-paired
binary's .data section) and threads a new ChatEntry.LogTextType field
through every ingestion site to the correct retail wire value:
HearSpeech/Tell pass the wire chatType through verbatim; Emote/SoulEmote
hard-code 0x0C; the Tell self-echo hard-codes 0x04; legacy ChatChannel
broadcasts derive their type from the channel bit via the new
LegacyChannelChatType helper (ported from the decompiled
Handle_Communication__ChannelBroadcast dispatch, hear vs. own-send);
TurbineChat rooms map through TurbineChatDisplayNames.LogTextType;
CombatChatTranslator's hit/miss/evade lines map to ACE's CombatSelf/
CombatEnemy per Player_Combat.cs; kill/death lines use retail's
decompiled 0x00 Default (not a combat color). ChatWindowController's
transcript now folds LogTextType through RetailChatColorTable with
retail's exact "out-of-range keeps the previous line's color" carry
rule; ChatPanel's combat highlighting sources the same table.
Corrects HearSpeech.cs's doc-comment ChatType legend (4 of 6 entries
were wrong). Adds register row AP-175 for the pre-existing (unchanged)
Popup-renders-in-chat divergence and updates AP-39's stale per-ChatKind
description. Narrows ISSUES #139 — its chat-colors half is done.
Retail renders no chat timestamp prefix path exists in acdream today,
so the "timestamp is always colorGrey 0x0C" rule has nothing to attach
to; noted here per the research doc rather than left silent.
Research: docs/research/2026-08-09-chat-retail-color-table.md
Full Release suite: 11,833 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three-tier rollout per the 2026-04-25 retail @help dump showing the
full ACE command surface. Tier 1 + most of Tier 2 in one commit.
TIER 1 - @ as / equivalent
ACE accepts both / and @ as verb prefixes (per its own help text:
"Note: You may substitute a forward slash (/) for the at symbol
(@)."). ChatInputParser now normalises @ to / for the verb-match
phase and re-enters parsing. Critical: for verbs we don't recognise
(@acehelp, @tele, @die, @version, @loc-on-server, @nonsense, ...),
the original @ is kept in the message text so ACE's CommandManager
intercepts the message server-side. If we substituted / there too,
ACE would treat it as plain Talk and broadcast it.
Result: @a hi / @tell Bob hi / @help / @clear / @reply / @retell
all route exactly like their / counterparts. @acehelp / @tele /
@version / @die etc. pass through to the server intact.
TIER 2 - client-only commands
- /retell <msg> (also @retell): resend to the last person you
tell'd. Mirrors retail @retell. ChatVM tracks
LastOutgoingTellTarget on each OnSelfSent(Tell, ...) entry —
SenderGuid==0 distinguishes outgoing echo from inbound whispers,
same way LastIncomingTellSender already worked. ChatInputParser
takes a new optional lastOutgoingTellTarget param.
- /framerate (also @framerate): prints "Framerate: 144.2 FPS"
into chat. Wired via a new ChatVM.FpsProvider Func<float>
callback set by GameWindow at construction (closes over
_lastFps). Falls back to "(provider unavailable)" if no
callback is wired (tests / pre-live).
- /loc (also @loc): prints "Location: (123.4, 567.8, 60.0)" into
chat. Wired via ChatVM.PositionProvider Func<Vector3> closing
over GetDebugPlayerPosition() in GameWindow. ACE has a server-
side @loc too; client wins here (instantaneous + uses the local
interpolated position).
ChatPanel.TryHandleClientCommand grew @ aliases for /help /clear
/framerate /loc and the new EqAny helper for case-insensitive
multi-string matching. Help text rewritten to reference the
/ <-> @ equivalence and point at @acehelp / @acecommands for ACE's
full command list.
TIER 3 - automatic (no code)
Most retail @-commands (@allegiance motd, @afk, @die, @lifestone,
@corpse, @marketplace, @pkarena, @emote/@emotes, @fillcomps,
@permit, @consent, @squelch, @unsquelch, @messagetypes, @age,
@birth, @day, @endurance, @pklite, @version, @filter, @unfilter,
@loadfile, @log, @marketplace, ...) are server-side ACE commands.
Tier 1's passthrough takes care of them automatically — they
arrive via Talk, ACE recognises the @ and intercepts, replies via
SystemChat (which our 0xF7E0 wiring renders as [System] lines).
DEFERRED
- @saveui / @loadui / @lockui: ImGui layout save/load, ~1 hr
standalone task. Filed for follow-up.
- @title <text>: rename chat window. ImGui window-id complications.
- Toggle-style @framerate (FPS overlay on/off): print-once is
simpler and matches retail's most-common usage.
30 new tests:
- ChatInputParserAtPrefixTests: 11 covering @-prefix recognition,
unknown-@ passthrough, /retell and @retell.
- ChatVMRetellAndProvidersTests: 8 covering LastOutgoingTellTarget
tracking, FpsProvider/PositionProvider callbacks, no-provider
fallback.
- ChatPanelInputTests: +3 (/framerate, @loc, @acehelp passthrough).
Solution total: 1063 green (243 Core.Net + 160 UI + 660 Core),
0 warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Adds a second real panel behind ACDREAM_DEVTOOLS=1. Shows the tail
of ChatLog (last 20 entries by default) formatted per ChatKind:
"Caith: hello" — LocalSpeech
"Regal says distantly: hi" — RangedSpeech
"[ch 7] Caith: g'day" — Channel
"[Tell] Regal: psst" — Tell
"[System] Your spell fizzled!" — System
"[Popup] A door stands..." — Popup
Why now: proves the D.2a IPanelRenderer contract survives beyond a
single progress-bar panel. ChatPanel exercises Text() + Separator()
on a variable-length list where VitalsPanel was a fixed three-widget
layout. No renderer primitives needed to grow — the contract held,
which is the whole point of the abstraction layer.
Files:
- src/AcDream.UI.Abstractions/Panels/Chat/ChatVM.cs (new)
Snapshots ChatLog tail every frame. Cheap at default 500-entry
cap. Per-kind formatting lives here (not in the panel) so the
D.2b retail-look swap inherits plain-text fallbacks.
- src/AcDream.UI.Abstractions/Panels/Chat/ChatPanel.cs (new)
IPanel implementation. Separator + N Text lines. "(no messages
yet)" fallback when the log is empty.
- src/AcDream.App/Rendering/GameWindow.cs
Registers the ChatPanel alongside VitalsPanel in the devtools
init block. Uses the existing GameWindow.Chat field already
fed by H.1's wire layer + GameEventWiring.WireAll.
- tests/AcDream.UI.Abstractions.Tests/ChatVMTests.cs (new)
12 tests covering tail selection, display-limit bounds, every
ChatKind's formatting, null-log + zero-limit guards, no stale
caching across appends.
Also fixes one stale "Hexa.NET.ImGui" mention in VitalsPanel's xmldoc
(pivoted to ImGui.NET in 55aaca7; doc needed a trailing update).
Build: 0 warnings, 0 errors. Tests: 23 UI.Abstractions (up from 11,
all Core + Core.Net still green), 0 failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>