User reported wanting to mark text in-game and copy it out (item names,
coordinates, NPC dialogue, etc). ImGui doesn't natively let you select
across multiple TextColored widgets, but a read-only multi-line
InputText is fully click-drag selectable + Ctrl+C copyable. This
commit adds a "Copy mode" toggle to ChatPanel that swaps the chat
tail's render path between the colored-line view and a single
selectable text region.
New IPanelRenderer primitive:
void TextMultilineReadOnly(string id, string content, Vector2 size);
ImGui maps this to InputTextMultiline with the ReadOnly flag — same
selection + Ctrl+C UX a user expects from any text-input widget.
FakePanelRenderer records the call for tests. The future D.2b
custom retail-look backend implements its own equivalent (likely
the same widget pattern with retail font/skin).
ChatPanel rendering:
· A "Copy mode (select text to Ctrl+C)" Checkbox at the top of
the panel toggles _copyMode.
· Off (default) — current per-line render with colored combat
entries. Visually unchanged from before.
· On — the chat tail becomes a single TextMultilineReadOnly
widget holding every visible line joined with newlines. Loses
per-line color, gains arbitrary-span text selection.
· Footer (separator + input field) renders identically in both
modes so the user can still type while in copy mode.
Existing ChatPanelLayoutTests's footer-separator probe was using
IndexOf("Separator") — which now matches the new pre-tail separator
between the Checkbox and the chat tail. Switched to LastIndexOf
which still pins the footer separator (between EndChild and
InputTextSubmit). Behaviour and intent unchanged.
DisplaySettingsTests' With_expression test was still asserting the
old "1920x1080" Default.Resolution; updated to the new "1280x720"
that the previous wire-up commit introduced (the earlier commit
forgot this one).
dotnet build green (0 warnings); dotnet test 1,309 / 1,309 green
(243 Core.Net + 393 UI.Abstractions + 673 Core).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five changes:
1. PlayerModeAutoEntry — testable guard class that fires once after
EnterWorld + WorldSession.State.InWorld + player entity present +
PlayerController.State == InWorld. GameWindow arms the entry
after EnterWorld; per-frame Tick checks all four guards and
invokes the same fly-to-player transition the Tab handler runs.
User-initiated fly toggle (DebugPanel button) Cancel()s pending
entry. Skip in offline mode (no ACDREAM_LIVE) — Holtburg orbit
stays default for testing.
2. MouseLookState + KeyBindings.RetailDefaults() binds MMB Hold to
InputAction.CameraInstantMouseLook. GameWindow subscribes:
- Press: hide cursor, capture position, _mouseLookActive = true.
- Release: restore cursor, deactivate.
- WantCaptureMouse=true while held → suspend (release cursor).
- MouseMove while active: combined drive — chase camera yaw +
character heading move together (retail's signature mouse-look
behavior). Camera Y still pitches camera-only.
3. DebugPanel "Toggle Free-Fly Mode" button via DebugVM.ToggleFlyMode
action delegate — replaces the F-key as the primary discovery
path for free-fly. Gated on DevToolsEnabled.
4. ChatPanel.FocusInput() one-shot + IPanelRenderer.SetKeyboardFocusHere
primitive. GameWindow's ToggleChatEntry (Tab) subscriber calls
_chatPanel.FocusInput() so Tab moves focus to the chat input
field. Replaces the K.1c TODO stub.
5. WantCaptureMouse gating reinforcement on surviving mouse handlers
(no new code; verified intact from K.1b).
21 new tests (8 PlayerModeAutoEntry, 10 MouseLookState, 3 ChatPanel
focus). 1183 total green. 0 warnings, 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User reported typing /ls (a command-style request, not chat) gets
echoed by the server as "You say, \"/ls\"". Slash-prefix is a
COMMAND surface, never a chat surface. Filed after the same flow
that produced @help and the welcome-message work.
Behavior change at the ChatPanel submit layer:
- Any /-prefixed input whose verb isn't in our alias tables now
renders a local "[System] Unknown command: /foo. Type /help for
the list." line and is NEVER published to the bus. No SendChatCmd,
no Talk packet. The server never sees /foo.
- Known /-verbs (/say /tell /reply /retell /general /allegiance
/patron /vassals /monarch /covassals /fellowship /lookingforgroup
/trade /roleplay /society /olthoi /help /clear /framerate /loc
and friends) still flow through ChatInputParser.Parse → SendChatCmd
exactly as before.
- @-prefix unchanged: ACE's CommandManager handles unknown @ verbs
server-side and replies via SystemChat ("Unknown command: foo")
per ACE GameActionTalk.cs:21. Our @ -> / normalization for known
verbs (Phase J Tier 1) and the @-passthrough fallthrough for
unknown verbs both still apply.
ChatInputParser now exposes:
- IsKnownVerb(string verb): query against the union of every alias
table. Used by ChatPanel to discriminate "unknown verb" from
"known verb with bad args".
- GetVerbToken(string command): public alias of the existing
ExtractVerb so callers can pull the first whitespace token without
reproducing the helper.
Parse itself is unchanged — its existing fall-through (Say with
literal text) still applies for unknown /-verbs called directly via
the parser, but ChatPanel intercepts before reaching that path so
the fall-through never fires through the live submit pipeline. Tests
that directly call Parse continue to pass; the new ChatPanel-level
tests pin the unknown-command rejection.
19 new tests:
- ChatInputParserTests: 10 IsKnownVerb Theory cases + 4 GetVerbToken
Theory cases.
- ChatPanelInputTests: 5 Theory cases for Submit_UnknownSlashCommand
covering /foo, /ls, /mp <path>, /genio, and bare /.
Solution total: 1086 green (243 Core.Net + 183 UI + 660 Core),
0 warnings.
Acceptance: type /ls, /mp /path, /anything-not-known — see local
"[System] Unknown command: /xxx. Type /help for the list of
supported commands." Nothing reaches the wire.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User reported the chat input field disappearing when the chat
window was resized smaller — older entries pushed it past the
visible area. Standard ImGui chat-window pattern fixes it: scrollable
nested region for the chat tail, fixed footer for the
separator + input field below it.
IPanelRenderer extensions (Phase J Tier 3):
- BeginChild(string id, Vector2 size, bool border = false) — opens
a nested scrollable region. Size follows ImGui semantics:
0 = fill available, negative = fill available minus this much.
- EndChild() — closes the nested region.
- FrameHeightWithSpacing() — single-line widget height incl. frame
padding + item spacing. Lets panels compute footer reservations
without hardcoding pixel constants.
- SetScrollHereY(float ratio) — forces scroll within current region;
pass 1.0f to keep the latest line visible after new entries
arrive.
ImGuiPanelRenderer impls. ImGui.NET's BeginChild signature changed
across versions (third arg moved from `bool border` to
`ImGuiChildFlags`); we cast a numeric literal (0x01 = Border bit)
to sidestep the rename. FrameHeightWithSpacing maps to
ImGui.GetFrameHeightWithSpacing(); SetScrollHereY to ImGui.SetScrollHereY.
ChatPanel restructured:
- Reserves footer height = FrameHeightWithSpacing() + 6f (small pad
for the separator above the input).
- Wraps the chat tail in BeginChild("##chattail", (0, -footer))
so the inner region scrolls independently of the window.
- Tracks _lastRenderedCount across frames and calls SetScrollHereY(1f)
only when new entries appended — manual scroll-up isn't fought
against; new messages jump the view back down only when they
actually arrive.
- Header Separator removed (the BeginChild border is enough).
FakePanelRenderer extended with the four new methods + recording.
4 new tests in ChatPanelLayoutTests pin the layout invariants:
- Render order: Begin → BeginChild → ... → EndChild → Separator
→ InputTextSubmit → End.
- BeginChild size has X=0 + negative Y at least matching the
injected FrameHeightWithSpacingValue.
- SetScrollHereY fires when entries grow.
- SetScrollHereY does NOT fire when entries don't grow.
Solution total: 1067 green (243 Core.Net + 164 UI + 660 Core),
0 warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <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>
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>