Commit graph

11 commits

Author SHA1 Message Date
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
56037a4471 feat(ui): #15 migrate DebugOverlay to ImGui DebugPanel - 7 collapsing sections + diagnostics toggles
Replaces the 473-LOC custom-StbTrueTypeSharp DebugOverlay with an
ImGui-rendered DebugPanel using the I.1 widget extensions. Single
window with 7 CollapsingHeader sections; checkboxes are the primary
toggle surface; F-keys retained where they invoke real gameplay
actions, dropped where they only toggled panels.

Pieces:
- DebugVM (UI.Abstractions): read-through ViewModel with combat-event
  ring (cap 25), toast ring (cap 25), 4 diagnostic-flag bools
  (DumpMotion / DumpVitals / DumpOpcodes / DumpSky), 3 Action hooks
  (CycleTimeOfDay / CycleWeather / ToggleCollisionWires). Self-
  subscribes to CombatState.DamageTaken/DealtAccepted/Evaded* /
  Missed*/AttackDone/KillLanded - replaces the old BindCombat path.
- DebugPanel (UI.Abstractions): one ImGui window with sections
  Player Info, Performance, Compass (text-only - draw-list strip
  deferred to D.6), Help (BeginTable cheat-sheet), Combat events
  (TextColored by kind: Info=yellow, Warning=red, Error=deep red),
  Recent toasts, Diagnostics (Checkboxes for the 4 flags + Buttons
  for the 3 cycle/toggle actions).
- All 28 Snapshot data points covered: Fps, FrameMs, PlayerPos,
  HeadingDeg, CellId, OnGround, InPlayerMode, InFlyMode,
  VerticalVelocity, EntityCount, AnimatedCount, LandblocksVisible,
  LandblocksTotal, ShadowObjectCount, NearestObjDist, NearestObjLabel,
  Colliding, DebugWireframes, StreamingRadius, MouseSensitivity,
  ChaseDistance, RmbOrbit, HourName, DayFraction, Weather,
  ActiveLights, RegisteredLights, ParticleCount.
- GameWindow surgery (+252/-165): removed _debugOverlay field +
  snapshot builder block + Update/Draw calls; added _debugVm /
  _debugPanel construction in the if (DevToolsEnabled) block;
  added per-frame nearest-object scan cached for VM closures
  (zero cost when devtools off); helper methods CycleTimeOfDay /
  CycleWeather / ToggleCollisionWires / GetDebug* / GetActiveSensitivity.

F-key disposition:
- F1: repurposed - now toggles whole DebugPanel visibility.
- F2: kept - ToggleCollisionWires (also a Button in panel).
- F4 / F5 / F6: REMOVED - per-section toggles replaced by
  CollapsingHeader inside one window.
- F7: kept - CycleTimeOfDay (also Button).
- F8 / F9: kept - mouse-sensitivity adjust; toasts route to
  _debugVm.AddToast.
- F10: kept - CycleWeather (also Button).

DebugOverlay.cs DELETED (473 LOC). TextRenderer + BitmapFont kept
alive: UiHost references _debugFont and the future HUD-in-world
(D.6) will reuse both.

11 new DebugVM tests covering combat-event-ring subscription, toast
ring cap, diagnostic-flag toggles. UI.Abstractions.Tests: 96 -> 107.
Solution total: 989 green (243 Core.Net + 639 Core + 107 UI).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:09:26 +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
f14296c75f feat(ui): #17 ChatPanel input field + slash commands + reply-to-last-tell
ChatPanel gains an Enter-to-submit input field via the I.1
InputTextSubmit widget. Submitted text routes through ChatInputParser
to a SendChatCmd published on ctx.Commands; LiveCommandBus (I.3)
handles the wire send + ChatLog echo.

Recognised prefixes (ported from holtburger commands.rs):

  /say msg or no prefix  -> Say
  /t Name msg or /tell   -> Tell  (first whitespace token = target)
  /r msg                 -> Tell  (target = LastIncomingTellSender)
  /g msg                 -> General
  /f msg                 -> Fellowship
  /a msg                 -> Allegiance
  /m msg                 -> Monarch
  /p msg                 -> Patron
  /v msg                 -> Vassals
  /cv msg                -> CoVassals
  /lfg msg               -> Lfg
  /trade msg             -> Trade
  /role msg              -> Roleplay
  /society msg           -> Society
  /olthoi msg            -> Olthoi

Edge cases: empty / whitespace / cmd-without-message / /r without
prior tell -> null (no-op). Unknown /xyz prefix -> Say with literal
text (matches holtburger's Talk(command) default arm).

ChatVM.LastIncomingTellSender populated only on incoming Tell entries;
discriminated by SenderGuid != 0 (OnSelfSent echoes always carry 0).

32 new tests:
- ChatInputParserTests: 22 covering every prefix + edge case
- ChatVMLastTellSenderTests: 6 covering capture + skip rules
- ChatPanelInputTests: 6 using FakePanelRenderer + recording
  ICommandBus to assert publish behaviour

UI.Abstractions.Tests: 60 -> 92. Solution total: 934 green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 19:44:04 +02:00
Erik
8e6e5a0b61 feat(ui+net): #16 LiveCommandBus + WorldSession.Send{Talk,Tell,Channel} + SendChatCmd wiring
Replaces NullCommandBus.Instance in PanelContext with a real
LiveCommandBus when a live session is active. Panels publish
SendChatCmd; the host routes it to the right wire opcode + emits
a ChatLog.OnSelfSent local echo (optimistic; retail-equivalent
for Talk).

Pieces:
- ChatChannelKind enum (UI.Abstractions) - mirrors holtburger's
  ChatChannelKind (references/holtburger/.../client/types.rs:35-49).
- SendChatCmd record (UI.Abstractions) - (Channel, TargetName?, Text).
- LiveCommandBus (UI.Abstractions) - single-handler-per-type;
  Register<T> throws on double-register; Publish<T> logs missing
  handler but does not throw.
- ChannelResolver (UI.Abstractions) - port of holtburger's
  resolve_legacy_channel (client/commands.rs:50-62) mapping
  ChatChannelKind to legacy ChatChannel ids verbatim from
  holtburger-protocol/.../chat/types.rs:8-24 (Fellow=0x0800,
  AllegianceBroadcast=0x02000000, Vassals=0x1000, Patron=0x2000,
  Monarch=0x4000, CoVassals=0x01000000).
- WorldSession.SendTalk / SendTell / SendChannel - 3-line wrappers
  around existing ChatRequests.Build* + SendGameAction. Internal
  GameActionCapture seam + InternalsVisibleTo for tests.
- GameWindow registers SendChatCmd handler: Say -> SendTalk +
  ChatLog echo, Tell -> SendTell + echo, channel kinds ->
  ChannelResolver.Resolve -> SendChannel + echo.

12 new tests across SendChatCmd + LiveCommandBus + ChannelResolver
+ WorldSessionChat. NullCommandBus.Instance retained for back-compat
when no live session.

Solution total: 893 green (51 + 229 + 613).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 19:27:22 +02:00
Erik
b131514d51 feat(ui): #14 IPanelRenderer widget extension - TextColored, Checkbox, Combo, InputTextSubmit, BeginTable, etc.
Adds 14 widget signatures to IPanelRenderer + ImGuiPanelRenderer impl:
TextColored, CollapsingHeader, TreeNode/TreePop, Checkbox, Button,
Combo, SliderFloat, PlotLines, BeginTable/TableNextColumn/EndTable,
InputTextSubmit (Enter-key submit), Spacing, Dummy, TextWrapped.

InputTextSubmit uses ImGuiInputTextFlags.EnterReturnsTrue and clears
the buffer + emits via `out submitted` on the frame Enter is pressed.
PlotLines passes `ref values[0]` with empty-array guard. CollapsingHeader
defaultOpen=true uses ImGuiTreeNodeFlags.DefaultOpen (= 0x20).

FakePanelRenderer test double records (Method, Args) tuples and
exposes knobs to drive ref/out values. 17 new tests dispatch through
IPanelRenderer (not the concrete fake) so tests fail to compile when
the interface itself lacks a method - real RED -> GREEN signal.

Tests: 26 -> 43 in UI.Abstractions.Tests. Total solution 881 green.
Foundation for Phase I.2 (DebugPanel) and I.4 (ChatPanel input field).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 19:03:28 +02:00
Erik
7da2a027d4 feat(player): #5 PlayerDescription parser — Stam/Mana via attribute block
Visual-verified — Vitals window now shows three bars (HP/Stam/Mana)
with live values. Closes ISSUES.md #5; ~95% reading on Stam/Mana
traced to active buff multipliers, filed as #6.

Why the rewrite

The first attempt (commit d42bf57) routed PlayerDescription (0x0013)
through AppraiseInfoParser, trusting a misleading xmldoc claim.
Live diagnostics proved the format is wrong — ACE source
(GameEventPlayerDescription.WriteEventBody) hand-writes a body
distinct from IdentifyObjectResponse's AppraiseInfo: property
hashtables gated on DescriptionPropertyFlag, vector-flag-gated
attribute / skill / spell blocks, then a long options + inventory
trailer. Vitals only arrive via the attribute block at login.
Holtburger's events.rs:220-625 has the canonical client-side
unpacker; this commit ports the early-section walker through spells.

What landed

  PlayerDescriptionParser.cs (new — 350 LOC):
    Walks propertyFlags + weenieType, then property hashtables
    (Int32/Int64/Bool/Double/String/Did/Iid) + Position table —
    each gated on a property flag bit, header is `u16 count, u16
    buckets`. Then vectorFlags + has_health + the attribute block
    (primary attrs 1..6 = 12 B each, vitals 7..9 = 16 B with
    `current`), then optional Skill + Spell tables. Stops cleanly
    before the options/shortcuts/hotbars/inventory trailer (filed
    as #7 — heuristic alignment search needed for gameplay_options).

  PrivateUpdateVital.cs (new — 95 LOC):
    Wire parsers for the GameMessage opcodes 0x02E7 (full snapshot)
    and 0x02E9 (current-only delta), per holtburger UpdateVital +
    UpdateVitalCurrent. WorldSession dispatches each to a session-
    level event the GameWindow forwards into LocalPlayerState.

  LocalPlayerState (full redesign):
    VitalKind (Health/Stamina/Mana) + AttributeKind (six primary).
    VitalSnapshot stores ranks/start/xp/current; AttributeSnapshot
    stores ranks/start/xp with `Current = ranks+start` per
    holtburger. GetMaxApprox computes the retail formula
        vital.(ranks+start) + attribute_contribution
    where the contribution is hardcoded from retail's
    SecondaryAttributeTable: Endurance/2 for Health, Endurance for
    Stamina, Self for Mana. Enchantment buffs not yet folded in
    (filed as #6). VitalIdToKind now accepts both ID systems
    (1..6 wire, 7..9 PD attribute block); AttributeIdToKind covers
    primary attrs 1..6.

  GameEventWiring:
    PlayerDescription handler. Walks parsed.Attributes, routes
    primary attrs (id 1..6) to OnAttributeUpdate and vitals
    (id 7..9) to OnVitalUpdate. Player's full learned spellbook
    also lands here. ACDREAM_DUMP_VITALS=1 traces every PD attribute
    + every PrivateUpdateVital(Current) opcode for diagnostics.

  WorldSession:
    Dispatch chain re-ordered — the diagnostic else-if for
    ACDREAM_DUMP_OPCODES=1 was originally placed before
    GameEventEnvelope.Opcode, which silently intercepted 0xF7B0 and
    broke UpdateHealth dispatch when the env var was set. Moved to
    the very end of the chain so it only fires for genuinely
    unhandled opcodes. (Diagnostic-only regression; production
    launches without the env var were unaffected.)

Test deltas

  Added:
    - PlayerDescriptionParserTests (6 — empty header, full attribute
      block, partial flags, post-property-table walk, spell table)
    - PrivateUpdateVitalTests (7 — fixture round-trip, vital ID
      coverage, opcode rejection, truncation)
    - LocalPlayerStateTests rewritten (20 — VitalIdToKind +
      AttributeIdToKind theories, Endurance/Self formula coverage,
      delta semantics, change events)
    - GameEventWiringTests for PlayerDescription dispatch (2 —
      end-to-end populate + spellbook feed)
  Updated:
    - VitalsVMTests rephrased onto the new OnVitalUpdate API.
  Total: 765 → 817 tests passing.

Diagnostics

  ACDREAM_DUMP_VITALS=1 — log every PD attribute extracted,
    every 0x02E7/0x02E9 dispatch.
  ACDREAM_DUMP_OPCODES=1 — log first occurrence of any unhandled
    GameMessage opcode (now correctly placed at end of chain).

Visual verify

  $env:ACDREAM_DEVTOOLS = "1"
  dotnet run --project src\AcDream.App\AcDream.App.csproj -c Debug

  Vitals window shows three bars; HP at 100%, Stam/Mana at ~95%
  (the gap is buff enchantments — filed as #6 with the holtburger
  multiplier+additive aggregator pattern as the reference for the
  fix).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:42:24 +02:00
Erik
d42bf5735d feat(player): #5 LocalPlayerState — Stam/Mana wired through PlayerDescription
Closes ISSUES.md #5. The Vitals devtools window now draws three bars
(HP / Stamina / Mana) once the server sends the first PlayerDescription
(0x0013), instead of HP only. Built test-first per CLAUDE.md TDD rule —
16 new tests went red before the implementation went in.

New AcDream.Core.Player.LocalPlayerState (cache):
  - {CurrentStamina, MaxStamina, CurrentMana, MaxMana} as uint? — null
    until first received.
  - StaminaPercent / ManaPercent: 0..1 fraction or null when either
    field is missing or max is zero. Clamps to 1.0 if current > max
    (server can briefly report this during buff transitions).
  - OnPlayerDescription preserves any previously known good value when
    an incoming field is null — partial profiles don't wipe state.
  - Changed event for future subscribers.

GameEventWiring.WireAll:
  - New optional 6th parameter: LocalPlayerState? localPlayer = null.
    Existing 5-arg call sites still work; without the parameter the new
    PlayerDescription handler still parses + feeds the spellbook but
    skips the cache update.
  - PlayerDescription (0x0013) shares AppraiseInfo wire format with
    IdentifyObjectResponse (0x00C9) per AppraiseInfoParser docstring,
    so the new handler reuses the existing parser and pulls
    CreatureProfile.{Stamina, StaminaMax, Mana, ManaMax}.
  - Player's full learned spellbook also lands here (previously only
    item-scoped Identify responses fed the spellbook).

VitalsVM:
  - Constructor adds optional LocalPlayerState? parameter (default null
    keeps every existing caller compiling).
  - StaminaPercent / ManaPercent now read through to LocalPlayerState
    every access — no VM-side caching, so a server-side delta to the
    cache surfaces next frame without any explicit refresh.

GameWindow:
  - Public readonly LocalPlayer field alongside Combat / Chat / Items /
    SpellBook so plugins + future panels can bind directly.
  - WireAll call updated to pass LocalPlayer.
  - VitalsVM construction passes LocalPlayer so the existing
    VitalsPanel automatically picks up the two new bars.

Test counts:
  - AcDream.Core.Tests:           550 → 561  (+11 LocalPlayerStateTests)
  - AcDream.UI.Abstractions.Tests: 23 →  26  (+3 VitalsVM through-cache)
  - AcDream.Core.Net.Tests:       192 → 194  (+2 PlayerDescription wiring)
  - Total:                        765 → 781

Build: 0 warnings, 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 11:02:00 +02:00
Erik
9faf9d7e3a feat(ui): ChatPanel — second devtools panel proves the abstraction
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>
2026-04-25 00:48:07 +02:00
Erik
fc03fa377b test(ui): AcDream.UI.Abstractions unit tests (11 tests green)
Covers the pure-logic surface of commit 1:

  VitalsVMTests
    * HealthPercent reads from CombatState.GetHealthPercent
    * safe-default 1.0 when GUID unknown / not yet set / never updated
    * SetLocalPlayerGuid reroutes lookup to the new guid (no stale cache)
    * StaminaPercent / ManaPercent are null for D.2a scope
    * ctor throws ArgumentNullException on null CombatState

  PanelContextTests
    * record-struct fields round-trip
    * value-based equality

  NullCommandBusTests
    * Publish accepts any record type without throwing
    * Instance is a true singleton

csproj template mirrors AcDream.Core.Tests (xUnit 2.9.3, Test.Sdk 17.14.1,
runner 3.1.4, coverlet 6.0.4, implicit Using Xunit). References only
AcDream.UI.Abstractions — no runtime / GL dependency, tests run fast.
2026-04-25 00:25:26 +02:00