Commit graph

16 commits

Author SHA1 Message Date
Erik
7aae5ba939 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>
2026-08-21 10:27:43 +02:00
Erik
ac0304dcf0 fix(ui,runtime): OP4 re-review residuals R1-R4 (coordinator pass) — OP4 CLOSED
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>
2026-08-11 06:36:14 +02:00
Erik
bc43fb1d1d fix(ui,runtime): OP4 review fixes — live re-seed, enable-gating, Combat panel re-point, universal timestamps
Both OP4 reviews converged on one headline bug (Character-tab rows never
re-read live server truth after their pre-login constructor-word seed) plus
overlapping MUST-FIXes. All ten converged/consolidated findings land here:

MUST-FIX:
- BoolOptionRow.SaveCurrentValue now re-reads its live binding (retail's
  GetValue()-into-SaveCurrentValue) on every OnShown — panel open, tab
  switch in, initial activation — instead of trusting the pre-login
  constructor word it was built with. Reset/tab-switch can now only
  restore values that were actually live at the last show. LockUI's
  host.Root.UiLocked one-shot mount seed now also converges on every
  PlayerDescription via the existing OnCharacterOptionsChanged hook.
- Apply/Reset are wired to OptionPage.OnOptionChanged in production
  (Ghosted when nothing changed, Normal when dirty, run once at bind so
  both start disabled per retail's PostInit); Defaults stays ungated.
- The Combat panel's three LEDs (Repeat Attacks/Auto Target/Keep in View)
  now read/write the same RuntimeCharacterOptionsState seam the Character
  tab uses instead of a disconnected client-local GameplaySettings copy —
  closes the "two writable copies" divergence. The three now-orphaned
  GameplaySettings fields and RuntimeSettingsController's mirror
  properties/SetCombatGameplay are deleted outright; the headless host's
  hardcoded AutoRepeatAttack/AutoTarget now read the live option bit.
- RuntimeSettingsController.SetUiLocked's convergence guard now compares
  against the last value actually applied to the runtime target instead
  of the persisted GameplaySettings.LockUI snapshot, which could already
  match a server-derived request without ever having been pushed.

SHOULD-FIX:
- DisplayTimeStamps now prefixes every chat producer (ChatLog.Append is
  the one seam all of them funnel through), not just AddText's own
  callers — heard speech, emotes, Turbine channels, and combat text were
  previously missed. The prefix format escapes its colons and forces
  InvariantCulture instead of the culture-dependent TimeSeparator
  placeholder.
- sky.frag now honors uFogParams.w (fog mode) like the mesh/terrain
  shaders, so Disable Distance Fog stops the sky dome's horizon band from
  blending toward fog color too.
- Corrected the "byte-verified" overclaim on the timestamp format string
  doc comment (BN-sourced, wire doc U6) and the AP-194 anchor-column
  class-name typo; the RunAsDefaultMovement doc comments now cite retail's
  actual acclient.h enumerator name.
- Added: DispatcherMovementInputSource's option x modifier truth table
  (incl. || AutoRunActive with the option off), the per-page Apply/Reset
  enable-gate tests, a real checkbox.OnClick/ToggleBehavior-driven click
  test, and hash-pins for the six header string keys.
- Gate script step 8 corrected for the logout-flush false-failure
  (closing the panel before relogging is load-bearing); a new step
  documents the enable-gate sequence and the Combat-panel/Character-tab
  cross-check.

Register: AP-196 (the Group-C default-source change + GameplaySettings
retirement) and AP-197 (the ignored per-character timestamp format
override) filed in this commit.

Full Release suite: 13,044 passed / 4 skipped / 0 failed (was 13,008/4/0;
net +36 tests from new coverage and legitimate assertion updates from the
GameplaySettings retirement).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 05:30:26 +02:00
Erik
e0e7888308 fix(chat): CH2 rework — SpewBox tick-driven visibility + binary-derived error table
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>
2026-08-09 18:14:26 +02:00
Erik
34d8a3c0e7 fix(chat): CH1 review fixes — sbb-idiom channel catch-all, command-output typing
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>
2026-08-09 16:03:13 +02:00
Erik
172c6f9aa3 feat(chat): Campaign CH slice CH1 — retail LogTextType color table
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>
2026-08-09 15:24:09 +02:00
Erik
4f31a5085f refactor(net): converge live session state reset 2026-07-21 12:00:48 +02:00
Erik
749e8ceeb1 fix(rendering): bound portal resource lifetime
Separate logical ownership, render publication, and GPU retirement across live entities, landblocks, particles, textures, mesh arenas, portal/UI teardown, and per-frame scratch storage. Add bounded DAT/texture caches, upload budgets, three-frame fence retirement, exact-incarnation appearance reconciliation, frame pacing, and extensive lifetime conformance coverage.\n\nThe seven-destination connected route now cuts peak working/private memory roughly in half, returns Caul to 125-153 FPS locally, and produces no WER or AMD reset.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
2026-07-18 21:35:16 +02:00
Erik
4b1bceefbb fix(world): preserve portal materialization effects
Route accepted Hidden and UnHide transitions through retail's internal typed-script path so their DAT scripts queue while the owner is cell-less and resume at the destination. Consume ACE's successful teleport control statuses silently, matching retail HandleFailureEvent.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-07-16 08:14:48 +02:00
Erik
3501194083 fix(chat): /help client-side handler + System dedup + ThatIsNotAValidCommand template
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>
2026-04-25 21:22:07 +02:00
Erik
7726f62528 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>
2026-04-25 21:07:56 +02:00
Erik
3f7821c18d fix(chat): BuildTell wire field order + retail-style FormatEntry + suppress duplicate Channel echo
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>
2026-04-25 20:49:02 +02:00
Erik
e17caa2942 fix(chat): translate WeenieError templates + strip Tell target punctuation + Turbine routing diagnostics
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>
2026-04-25 20:31:23 +02:00
Erik
3d26c8efde feat(chat): #20 CombatChatTranslator - retail-faithful combat -> ChatLog templates
Subscribes to CombatState's DamageDealtAccepted / DamageTaken /
MissedOutgoing / EvadedIncoming / AttackDone / KillLanded events
and emits chat-line text into ChatLog.OnCombatLine, mirroring
holtburger's templates verbatim from references/holtburger/apps/
holtburger-cli/src/pages/game/panels/chat.rs:221-308.

Pieces:
- ChatLog: new ChatKind.Combat value; new CombatLineKind enum
  (Info / Warning / Error) on ChatEntry; OnCombatLine(text, kind)
  adapter.
- CombatChatTranslator (Core, IDisposable). Static formatters:
  FormatDamageType (slashing/piercing/bludgeoning/fire/cold/acid/
  electric/nether), FormatDamageLocation (head/chest/abdomen/
  upper arm/lower arm/hand/upper leg/lower leg/foot), FormatPercent,
  FormatAttackConditionsSuffix.
- ChatVM.RecentLinesDetailed() returns FormattedLine records with
  kind metadata so panels can render combat lines colored.
- ChatPanel switches on Kind/CombatKind: combat-Info -> yellow,
  combat-Warning -> red incoming-damage, combat-Error -> deep red,
  all others -> existing renderer.Text path.
- GameWindow constructs translator after GameEventWiring.WireAll;
  disposes in OnClosing + live-session failure path.

Templates landed:
  Attacker:  "You hit {def} for {dmg} {dtype} damage ({hp%}). [Crit]{suffix}"
  Defender:  "{atk} hit you for {dmg} {dtype} damage to your {loc} ({hp%})..."
  Evade-out: "{def} evaded your attack."
  Evade-in:  "You evaded {atk}'s attack."
  AttackErr: "Attack sequence finished with {error}."
  Kill:      synthesized "You killed {name}." + server PlayerKilled
             death-message arrives separately via ChatLog.OnPlayerKilled.

Deviations from holtburger templates (documented in source):
- DamageDealt omits Critical-hit suffix until CombatState.DamageDealt
  carries the flag (defender-side has it; attacker-side doesn't yet).
- DamageTaken omits (health%) until CombatState.DamageIncoming
  parses the wire health-percent field.
- AttackConditions suffix is implemented but always empty until the
  bitflag is plumbed into CombatState records.

18 new tests (12 translator + 4 ChatVMCombat + 2 ChatLog).
Solution total: 978 green (243 Core.Net + 639 Core + 96 UI).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 19:55:15 +02:00
Erik
ff5ed9ec0b feat(net): #18 holtburger inbound chat parity - EmoteText, SoulEmote, ServerMessage, PlayerKilled, WeenieError + Windows-1252 codec
Five sub-changes:

1. Windows-1252 codec switch (global). Every Encoding.ASCII call site
   in src/AcDream.Core.Net/Messages/ -> Encoding.GetEncoding(1252).
   Touched HearSpeech, ChatRequests, GameEvents, AppraiseInfoParser,
   CharacterList, CreateObject, PlayerDescriptionParser, SocialActions.
   New Encodings.cs module-init registers CodePagesEncodingProvider
   (System.Text.Encoding.CodePages ships with .NET 10 SDK but isn't
   auto-registered). Matches retail + holtburger; accented names
   no longer round-trip-broken.

2. New parsers (opcodes confirmed against holtburger opcodes.rs):
   - EmoteText (0x01E0)     { u32 senderGuid, string16 senderName, string16 text }
   - SoulEmote (0x01E2)     same wire layout as EmoteText
   - ServerMessage (0xF7E0) { string16 message, u32 chatType }
   - PlayerKilled (0x019E)  { string16 deathMessage, u32 victimGuid, u32 killerGuid }
   Shared StringReader.cs has the CP1252 String16L primitive.

3. WorldSession dispatch. ProcessDatagram adds branches for the four
   new top-level opcodes + fires session-level events (EmoteHeard,
   SoulEmoteHeard, ServerMessageReceived, PlayerKilledReceived).
   0x0295 SetTurbineChatChannels stubbed with TODO for parallel I.6.

4. GameEventWiring routes WeenieError + WeenieErrorWithString
   (parsers existed but were unrouted) -> chat.OnWeenieError.

5. ChatLog adapters: Emote / SoulEmote ChatKind values, OnEmote,
   OnSoulEmote, OnPlayerKilled, OnWeenieError. OnLocalSpeech now
   substitutes empty sender -> "You" per holtburger client/messages.rs.
   ChatVM.FormatEntry handles new kinds (asterisk + sender + text).

22 new tests covering parser round-trips + reject-bad-opcode +
ChatLog adapter coverage + Win-1252 round-trip with non-ASCII chars.
Solution total: 881 green (210->225 in Core.Net.Tests, 606->613 in Core.Tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 19:06:01 +02:00
Erik
404cab55ba feat(chat): Phase H.1 Talk/Tell/ChatChannel + HearSpeech + ChatLog
Completes the chat-wire layer end-to-end: outbound Talk (/say), Tell
(/tell), ChatChannel, + inbound HearSpeech (0x02BB) / HearRangedSpeech
(0x02BC) routed into a unified ChatLog that also consumes the already-
parsed GameEvent ChannelBroadcast / Tell / TransientMessage / Popup.

Wire layer (AcDream.Core.Net/Messages):
- ChatRequests.BuildTalk (0x0015, inside 0xF7B1): gameActionSequence
  + string16L message. PackString16L helper with 4-byte pad.
- ChatRequests.BuildTell (0x005D): targetName + message, each
  string16L with its own padding.
- ChatRequests.BuildChatChannel (0x0147): channelId + message.
- HearSpeech.TryParse handles BOTH 0x02BB local AND 0x02BC ranged —
  single parser with IsRanged flag in the returned record. Standalone
  GameMessage (NOT wrapped in 0xF7B0).

WorldSession integration:
- ProcessDatagram branch for HearSpeech.LocalOpcode /
  HearSpeech.RangedOpcode; fires new SpeechHeard event.
- Places the new branch before the 0xF7B0 GameEvent branch so ordering
  stays stable.

Core layer (AcDream.Core/Chat):
- ChatEntry record: (Kind, Sender, Text, SenderGuid, ChannelId, Received).
- ChatKind enum: LocalSpeech, RangedSpeech, Channel, Tell, System, Popup.
- ChatLog: ring-buffer (default 500) of entries; adapters for every
  inbound source (OnLocalSpeech, OnChannelBroadcast, OnTellReceived,
  OnSystemMessage, OnPopup) plus OnSelfSent for echoing outbound.
  Fires EntryAppended so UI panel can scroll / highlight.

Tests (15 new):
- ChatRequests: Talk / Tell / ChatChannel byte-exact encoding (including
  string16L padding edge cases).
- HearSpeech: local + ranged round-trip, wrong-opcode returns null.
- ChatLog: local / ranged / channel / tell / system / self echo,
  ring-buffer drops oldest, Clear empties.

Build green, 570 tests pass (up from 555).

With the chat wire layer in place, Phase H.1's "chat window panel"
(UI slice 05) is purely a UI task: instantiate ChatLog, bind to
EntryAppended, feed rows into the retail-UI widget toolkit. No more
protocol gaps.

Ref: r08 §3 (opcodes 0x0015, 0x005D, 0x0147), §2 (0x02BB, 0x02BC).
Ref: ACE GameMessageHearSpeech.cs + GameActionChannelBroadcast.cs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:03:45 +02:00