fix(chat): Campaign CH round 3 — SpewBox flush-top/font, /help exact print sequence
User-gate round 3 findings (a)-(c):
(a) SpewBox: TopOffset moves from the round-1 60px placeholder to 0 (flush
to the viewport top). SpewBoxController never wired DatFont/Font at all
before this round, so it silently rendered through the 15px debug
BitmapFont fallback; it now resolves retail dat Font 0x40000025
(MaxCharHeight=11px) through a new RetailUiRuntime.Assets accessor —
the smallest font id confirmed in use by any currently-imported retail
LayoutDesc fixture, cross-referenced against every
tests/AcDream.App.Tests/UI/Layout/fixtures/*.json dump and confirmed
against the installed DAT via AcDream.Cli dump-font-atlas. It is also the
chat window's own smallest font (the 0x2100006F floating-window 1/2/3/4
indicator badges), so both selection criteria the brief offered agree.
Both remain best-available approximations, not resolved retail values —
register row AP-178 updated accordingly.
(b)/(c) /help and /help death: round 2 extracted the individual retail
strings byte-exact but never traced ClientCommunicationSystem::DoHelp's
complete print sequence. Byte-swept DoHelp's own range plus the five
Summary-branch functions it calls into (HelpEmote/HelpSquelch/
HelpStatusGroup/HelpTextGroup/HelpAllGroup) against the PDB-paired
acclient.exe. Retail's real shape: bare /help prints exactly TWO scroll
entries (HelpPrefixNote, then the 13-item AvailableHelpListing built from
DoHelp's own literals and each group's Summary_HelpType branch, in exact
source order) — not the acdream-invented cheat sheet BuildHelpText()
built before. Any resolved /help <verb> gets the SAME two-entry shape:
HelpPrefixNote, then ForMoreInformationPrefix concatenated directly onto
the verb's own Detail text (retail's own unsubstituted "<command>"
literal, ported verbatim). ChatCommandRouter.EmitVerbHelp applies this
uniformly to every resolved verb, not just death. An unresolved verb now
shows retail's real "Unknown command" fallback text; that fallback types
0x1A (ClientLocal), which retail routes to the SpewBox exclusively — a
gap ChatVM's UI.Abstractions layer can't yet reach, filed as ISSUES #367
/ register AP-186 rather than left silently unregistered.
Jump-in-air (round 2's open item 1) was root-caused and fixed separately
at a5a7eb4f between rounds — recorded in the campaign ledger.
Debug suite (all projects): 12,329 passed / 4 skipped / 1 failed — the
one failure is issue #351, a pre-existing Debug-only streaming flake
confirmed reproducing identically on the pristine pre-round-3 commit via
git stash, not a regression. Release verification covers every project
reachable without rebuilding AcDream.App: a live client process (PID
15064) held its own Release binaries locked for the session and was not
killed per project policy — AcDream.UI.Abstractions.Tests (867/867, the
layer both /help fixes live in) plus every other non-App-dependent
project, all 0 failed. AcDream.App/AcDream.App.Tests/AcDream.Core.Tests
(the SpewBox fix's layer) are green in Debug only this session.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
a5a7eb4fb6
commit
98de4f5ab3
14 changed files with 665 additions and 105 deletions
|
|
@ -192,6 +192,49 @@ click — not decoded by CH6a.
|
||||||
`src/AcDream.App/UI/Layout/LayoutImporter.cs`
|
`src/AcDream.App/UI/Layout/LayoutImporter.cs`
|
||||||
(`BuildWidget`/`ConsumesDatChildren` handling); `src/AcDream.App/UI/UiText.cs`.
|
(`BuildWidget`/`ConsumesDatChildren` handling); `src/AcDream.App/UI/UiText.cs`.
|
||||||
|
|
||||||
|
## #367 — ChatCommandRouter's local-presentation fallbacks type-0x1A text still lands in the chat scroll, never the SpewBox
|
||||||
|
|
||||||
|
**Status:** OPEN — filed 2026-08-10, Campaign CH user-gate round 3, while
|
||||||
|
tracing `ClientCommunicationSystem::DoHelp @0x0057f9e0`'s complete print
|
||||||
|
sequence for findings (b)/(c). Retail's DoHelp fallback for an unresolved
|
||||||
|
`/help <verb>` is `AddTextToScroll(u"Unknown command", 0x1A, 1, 0)` — type
|
||||||
|
`0x1A` (`ClientLocal`) is HARDCODED to the SpewBox, never the chat window
|
||||||
|
(`docs/research/2026-08-09-chat-retail-interface-text.md` §2.1/§2.2, the
|
||||||
|
same routing rule Campaign CH user-gate round 2 item 2 already ported for
|
||||||
|
the portal-space notice). `RuntimeCommunicationState.AddText` (Runtime
|
||||||
|
layer) implements this rule correctly — `type == ClientLocal` routes to
|
||||||
|
`SpewBox` only. But `ChatCommandRouter`/`ChatVM` live in
|
||||||
|
`AcDream.UI.Abstractions`, a layer beneath Runtime that must stay
|
||||||
|
presentation/Runtime-independent (Code Structure Rules), so they have no
|
||||||
|
path to the SpewBox at all — every local-presentation fallback
|
||||||
|
(`RetailCommandHelpTable.UnknownCommand` now; also the pre-existing
|
||||||
|
"Unknown command: {verb}." command-shaped-input refusal in
|
||||||
|
`ChatCommandRouter.Submit`'s main body) still writes through
|
||||||
|
`ChatVM.ShowSystemMessage`, which only ever reaches `ChatLog`. Not a
|
||||||
|
regression this round — the fallback text was already wrong AND
|
||||||
|
already routed to the chat window before this round's fix; this round
|
||||||
|
corrected the TEXT ("Unknown command", byte-exact) and traced the
|
||||||
|
routing divergence clearly enough to file it. Register row AP-186.
|
||||||
|
|
||||||
|
**Fix shape:** either (a) give `ChatVM` (or a sibling in UI.Abstractions)
|
||||||
|
an optional `Action<string>? OnClientLocalText` hook the App-layer host
|
||||||
|
wires to `RuntimeCommunicationState.AddText(text, RetailLogTextType.ClientLocal)`
|
||||||
|
the same way `ChatWindowController`/other retained-UI controllers already
|
||||||
|
receive delegates from the composition layer, or (b) accept the
|
||||||
|
divergence permanently as an acdream simplification (all local-presentation
|
||||||
|
refusals show in the chat window instead of splitting across two surfaces)
|
||||||
|
and retire AP-186 as an accepted Intentional Architecture row instead. Needs
|
||||||
|
a product decision, not just an implementation — small either way once
|
||||||
|
decided.
|
||||||
|
|
||||||
|
**Where:** `src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs`;
|
||||||
|
`src/AcDream.UI.Abstractions/Panels/Chat/ChatVM.cs`;
|
||||||
|
`src/AcDream.Runtime/Gameplay/RuntimeCommunicationState.cs` (`AddText`, the
|
||||||
|
correctly-implemented Runtime-layer oracle this should eventually reach).
|
||||||
|
|
||||||
|
**Campaign:** `docs/plans/2026-08-09-chat-parity-campaign.md` (Campaign CH,
|
||||||
|
user gate round 3).
|
||||||
|
|
||||||
## #364 — Three `/help` group topics still partial: HelpStupidChannelHack unresolved
|
## #364 — Three `/help` group topics still partial: HelpStupidChannelHack unresolved
|
||||||
|
|
||||||
**Status:** OPEN — filed 2026-08-09, Campaign CH user-gate round 2, item 3.
|
**Status:** OPEN — filed 2026-08-09, Campaign CH user-gate round 2, item 3.
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -33,10 +33,20 @@ character on the probe account), and two temporary graphical-only probes
|
||||||
are left behind for the next round. **CH6a (main-window layout + 8-grip
|
are left behind for the next round. **CH6a (main-window layout + 8-grip
|
||||||
resize) landed CODE-COMPLETE the same day** — see its ledger row and the
|
resize) landed CODE-COMPLETE the same day** — see its ledger row and the
|
||||||
updated round-1 items H/I and round-2 item 6 dispositions below; CH6b
|
updated round-1 items H/I and round-2 item 6 dispositions below; CH6b
|
||||||
(floating windows 1–4) and CH6c (opacity) remain not started. Status
|
(floating windows 1–4) and CH6c (opacity) remain not started. Round 2's
|
||||||
stays CODE-COMPLETE pending the next user gate round (still needed for
|
open item 1 (jump-in-air) was root-caused between rounds from live probe
|
||||||
item 1, CH6a's own visual confirmation, CH6b/CH6c, and a final in-client
|
evidence and FIXED at `a5a7eb4f` — a production-controller install path
|
||||||
visual pass on everything fixed so far).
|
never wired `OnInterfaceText`. **User gate round 3 ran 2026-08-10 and
|
||||||
|
found three more findings, all presentation; see "User gate — round 3"
|
||||||
|
below.** All three are fixed in this round's commit: the SpewBox now
|
||||||
|
mounts flush to the viewport top and resolves a real (smaller) retail dat
|
||||||
|
font instead of the unwired 15px debug fallback; bare `/help` and
|
||||||
|
`/help <verb>` (including `/help death`) now print retail's exact
|
||||||
|
`DoHelp` shape — two scroll entries in the right order, not one
|
||||||
|
acdream-invented blob. Status stays CODE-COMPLETE pending the next user
|
||||||
|
gate round (still needed for CH6a's own visual confirmation, CH6b/CH6c,
|
||||||
|
round 3's fixes, and a final in-client visual pass on everything fixed so
|
||||||
|
far).
|
||||||
|
|
||||||
**Why now:** first track of the alpha-release program (chat is the most
|
**Why now:** first track of the alpha-release program (chat is the most
|
||||||
visible daily surface for the friend-alpha). User-directed 2026-08-09.
|
visible daily surface for the friend-alpha). User-directed 2026-08-09.
|
||||||
|
|
@ -201,6 +211,9 @@ implementer per slice against a pinned contract (per
|
||||||
| CH6a main-window layout + 8-grip resize | (this commit) | 12,317 passed / 4 skipped / 0 failed | pending (no subagent review pass this session — implementer-only) | pending — needs the next in-client round (items H/I round 1, item 6 round 2) |
|
| CH6a main-window layout + 8-grip resize | (this commit) | 12,317 passed / 4 skipped / 0 failed | pending (no subagent review pass this session — implementer-only) | pending — needs the next in-client round (items H/I round 1, item 6 round 2) |
|
||||||
| CH6b/CH6c floating windows + opacity | not started | — | — | not started |
|
| CH6b/CH6c floating windows + opacity | not started | — | — | not started |
|
||||||
| User gate round 2 | (this commit) | 12,267 passed / 4 skipped / 0 failed | — | items 2/4/5 fixed this commit, item 3 confirmed-fixed, item 6 folded into CH6a's spec, item 1 NOT reproduced (see "User gate — round 2" below) |
|
| User gate round 2 | (this commit) | 12,267 passed / 4 skipped / 0 failed | — | items 2/4/5 fixed this commit, item 3 confirmed-fixed, item 6 folded into CH6a's spec, item 1 NOT reproduced (see "User gate — round 2" below) |
|
||||||
|
| CH6a main-window layout + 8-grip resize | `1fd51543` | 12,317 passed / 4 skipped / 0 failed | pending | pending — landed same day as round 2 |
|
||||||
|
| Jump-in-air root cause (round-2 item 1, resolved) | `a5a7eb4f` | Runtime tests 1,323/0 | — | round-3 probe evidence pinpointed a missing `OnInterfaceText` wire on the production controller-commit path (`RuntimeLocalPlayerMovementState.CommitRuntimeOwnedController`); FIXED, regression test added |
|
||||||
|
| User gate round 3 | (this commit) | Debug (all projects): 12,329 passed / 4 skipped / 1 failed (pre-existing #351 Debug-only flake — reproduces identically on the pristine pre-round-3 commit, not a regression); Release (every project reachable while a live `AcDream.App.exe` client — PID 15064, must not be killed per project policy — holds its own Release binaries locked, blocking `AcDream.App`/`AcDream.App.Tests`/`AcDream.Core.Tests` specifically): `AcDream.UI.Abstractions.Tests` (the layer this round's `/help` fix lives in) 867/867, plus `Core.Net.Tests` 823/823, `Runtime.Tests` 1,323/1,323, `Content.Tests` 130/130, `Headless.Tests` 89/89, `Bake.Tests` 15/15, `Cli.Tests` 4/4 — all 0 failed | — | findings (a)-(c) fixed this commit — SpewBox flush-top + retail dat font, `/help`/`/help death` exact retail print sequence (see "User gate — round 3" below) |
|
||||||
|
|
||||||
### CH4 closeout (2026-08-09)
|
### CH4 closeout (2026-08-09)
|
||||||
|
|
||||||
|
|
@ -590,3 +603,86 @@ chrome mount), plus new/updated tests in
|
||||||
`tests/AcDream.App.Tests/UI/UiRootInputTests.cs`,
|
`tests/AcDream.App.Tests/UI/UiRootInputTests.cs`,
|
||||||
`tests/AcDream.App.Tests/UI/CursorFeedbackControllerTests.cs`, and
|
`tests/AcDream.App.Tests/UI/CursorFeedbackControllerTests.cs`, and
|
||||||
`tests/AcDream.App.Tests/UI/Layout/ChatLayoutConformanceTests.cs`.
|
`tests/AcDream.App.Tests/UI/Layout/ChatLayoutConformanceTests.cs`.
|
||||||
|
|
||||||
|
## User gate — round 3 (2026-08-10)
|
||||||
|
|
||||||
|
The user tested round 2's fixes live and reported three more findings, all
|
||||||
|
presentation. Between rounds, the main session also root-caused round 2's
|
||||||
|
open item 1 (jump-in-air silence) from live probe evidence and landed the
|
||||||
|
fix at `a5a7eb4f` — see the standalone ledger row above; not repeated here
|
||||||
|
since it needed no further work this round.
|
||||||
|
|
||||||
|
| # | User finding (condensed) | Disposition |
|
||||||
|
|---|---|---|
|
||||||
|
| (a) | The SpewBox yellow text is "still not aligned all the way to the top" and not "the correct font and size" (retail's is SMALLER than ours). | **FIXED this SHA.** Two independent sub-fixes, both best-available APPROXIMATIONS (the true retail values remain unmeasurable — `SpewBoxLayoutDumpDiagnostic` re-run this round still finds no `FontDid`/colour property on element `0x10000048` or its ListBox child): **position** — `SpewBoxController.TopOffset` moves from round 1's 60px placeholder to `0` (flush to the viewport top), per the user's explicit direction. **Font** — the controller never wired `DatFont`/`Font` at all before this round, so it silently fell through to the retained-UI host's 15px debug `BitmapFont`; it now resolves retail dat Font `0x40000025` (`MaxCharHeight=11px`) through `RetailUiRuntime.Assets.ResolveFont` (a new public accessor), the SAME memoized resolver the rest of the retained UI uses. `0x40000025` was picked by cross-referencing every currently-imported retail LayoutDesc fixture (`tests/AcDream.App.Tests/UI/Layout/fixtures/*.json`) for the smallest FontDid actually in use — it is 11px, smaller than every other font found (`0x40000000`=16px, `0x40000002`=14px, etc.), confirmed against the installed DAT via `AcDream.Cli dump-font-atlas` sweeping every populated font id `0x40000000`-`0x40000032`. It is ALSO the chat window's own smallest font (the `0x2100006F` floating-window 1/2/3/4 indicator badges use the same id) — both selection criteria the round-3 brief offered landed on the same answer, no tie to break. Register row AP-178 updated (not retired — position/font remain approximations, not resolved retail values; only vertical content flow stays fully OPEN). |
|
||||||
|
| (b) | `/help` output is "still not what retail displays". | **FIXED this SHA.** Round 2 extracted the individual STRINGS byte-exact but never traced `ClientCommunicationSystem::DoHelp @0x0057f9e0`'s complete PRINT SEQUENCE, so the bare `/help` listing was still an acdream-invented cheat sheet ("Chat: /say...", "Channels: /general...", etc. — none of it retail text). Traced this round via a byte-sweep of DoHelp's own range (`0x57f9e0`-`0x57fe7e`) plus the five Summary-branch functions it calls into, against the PDB-paired `C:\Users\erikn\Downloads\acclient.exe` (verified MATCH). Retail's real bare-`/help` output is exactly TWO scroll entries — `RetailCommandHelpTable.HelpPrefixNote` (now re-swept with its leading blank line and trailing double newline, previously dropped) then `RetailCommandHelpTable.AvailableHelpListing`, a 13-item straight-line concatenation of retail's real topic-group one-liners (allegiances/channels/chatting/death/emote/fillcomps/friends/house/squelch/status/text/commands) in DoHelp's exact source order — never one concatenated blob. `ChatCommandRouter.EmitBareHelp` now emits two `ShowSystemMessage` calls, matching. The acdream-only `RetailClientCommandCatalog.BuildHelpText()`/router `BuildHelpText()` methods that built the old fabricated listing are deleted outright (dead code once nothing calls them). |
|
||||||
|
| (c) | `/help death` is "still not formatted correctly". | **FIXED this SHA.** The CONTENT (`DeathGroupDetail`'s 8 lines) was already byte-exact from round 2 — what was still wrong was the SHAPE. DoHelp wraps EVERY successfully-resolved `/help <verb>` in the SAME two-entry shape as the bare listing: `HelpPrefixNote` as its own entry, then a SECOND entry that is `ForMoreInformationPrefix` ("For more information, type @help <command>.\n" — retail's own literal; "<command>" is NOT a substituted placeholder, confirmed by the absence of any sprintf/substitution call in the decomp) concatenated DIRECTLY onto the verb's own detail text — no blank line, no third entry, because retail's own handler call appends into the SAME string accumulator the prefix was built into. `ChatCommandRouter.EmitVerbHelp` now applies this wrap UNIFORMLY to every resolved verb (both `RetailClientCommandCatalog` and `RetailCommandHelpTable` lookups), not just death — the general fix, not a death-specific patch, per CLAUDE.md's root-cause discipline. An unresolved verb now shows retail's real fallback text, `RetailCommandHelpTable.UnknownCommand` ("Unknown command", swept verbatim) instead of the acdream-invented "No help available for '{verb}'." — this surfaced a SEPARATE finding: retail types this fallback `0x1A` (`ClientLocal`), which routes to the SpewBox exclusively, never the chat window; `ChatCommandRouter`/`ChatVM` live in `AcDream.UI.Abstractions`, a layer beneath Runtime with no SpewBox access, so the fallback still renders in the chat scroll — not a regression (it was already there, just with fabricated text), now tracked as ISSUES.md #367 / register row AP-186 instead of silently continuing unregistered. |
|
||||||
|
|
||||||
|
Evidence: this commit's diff + `tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandHelpTableTests.cs`
|
||||||
|
(new pinning tests for `HelpPrefixNote`, `ForMoreInformationPrefix`,
|
||||||
|
`UnknownCommand`, `AvailableHelpListing`, and the router-level `/help death`
|
||||||
|
two-entry shape) + `tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatCommandRouterTests.cs`
|
||||||
|
(updated `/help` tests for the new shape) +
|
||||||
|
`tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatPanelInputTests.cs`
|
||||||
|
(updated entry-count assertions) + `tests/AcDream.App.Tests/UI/SpewBoxControllerTests.cs`
|
||||||
|
(new flush-top / resize / font-wiring tests). Debug suite (all projects)
|
||||||
|
green — 12,329 passed / 4 skipped / 1 failed, the single failure being
|
||||||
|
issue #351 (a pre-existing, load-sensitive Debug-only flake in an
|
||||||
|
unrelated streaming test, confirmed reproducing identically on the
|
||||||
|
pristine pre-round-3 commit via `git stash`, not a regression from this
|
||||||
|
work). Release verification was possible for every project reachable
|
||||||
|
without rebuilding `AcDream.App` — a live client process (PID 15064) held
|
||||||
|
its own Release binaries locked for the whole session and was not killed
|
||||||
|
per project policy (`feedback_dont_kill_clients_before_launch`) —
|
||||||
|
including `AcDream.UI.Abstractions.Tests` (867/867, the layer both `/help`
|
||||||
|
fixes live in) and every other non-App-dependent test project, all 0
|
||||||
|
failed. `AcDream.App`/`AcDream.App.Tests`/`AcDream.Core.Tests` (which the
|
||||||
|
SpewBox fix and the SpewBox tests live in) could not be Release-verified
|
||||||
|
this session; they are green in Debug.
|
||||||
|
|
||||||
|
### Byte-sweep method for findings (b)/(c)
|
||||||
|
|
||||||
|
`ClientCommunicationSystem::DoHelp @0x0057f9e0` has two branches
|
||||||
|
(`arg2 > 0` = verb specified, `arg2 <= 0` = bare `/help`). Both ALWAYS
|
||||||
|
print via exactly two `AddTextToScroll` calls, both typed `0`
|
||||||
|
(informational) — never a single concatenated string:
|
||||||
|
|
||||||
|
1. The SAME `HelpPrefixNote` line, unconditionally, in both branches.
|
||||||
|
2. Bare: `AvailableHelpListing` — 13 items, 8 inline literals DoHelp
|
||||||
|
builds itself (allegiances/channels/chatting/death/fillcomps/friends/
|
||||||
|
house, plus the header) interleaved with 5 items DoHelp gets by calling
|
||||||
|
each group's own `Summary_HelpType` branch (`HelpEmote`/`HelpSquelch`/
|
||||||
|
`HelpStatusGroup`/`HelpTextGroup`/`HelpAllGroup` — every one of those 5
|
||||||
|
functions has the identical `if (arg2 != Summary_HelpType) {Detail}
|
||||||
|
else {"@help X - ..."}` shape `HelpEmote` makes explicit at
|
||||||
|
`acclient_2013_pseudo_c.txt:388664`). Verb-specified (verb resolves):
|
||||||
|
`ForMoreInformationPrefix` immediately followed (string concatenation,
|
||||||
|
no separator) by the verb's own Detail-branch text.
|
||||||
|
|
||||||
|
When the verb does NOT resolve, DoHelp instead prints ONE entry,
|
||||||
|
`UnknownCommand`, typed `0x1A` — the SpewBox-exclusive routing type CH
|
||||||
|
user-gate round 2 item 2 already ported for the portal-space notice.
|
||||||
|
|
||||||
|
Tool: `py tools/pdb-extract/sweep_weenie_strings.py
|
||||||
|
C:/Users/erikn/Downloads/acclient.exe --range 0x57f9e0 0x57fe7e
|
||||||
|
--ascii-only --min-len 3` (DoHelp's own range) plus the same tool against
|
||||||
|
each of the five Summary-branch functions' own ranges (`HelpEmote`
|
||||||
|
`0x578b80`-`0x578c60`, `HelpSquelch` `0x57c190`-`0x57c2d0`,
|
||||||
|
`HelpStatusGroup` `0x57c410`-`0x57c5a0`, `HelpTextGroup`
|
||||||
|
`0x57c6c0`-`0x57c860`, `HelpAllGroup` `0x57e7f0`-`0x57eba0`), each range
|
||||||
|
read from the pseudo-C's own function-header addresses. `check_exe_pdb.py`
|
||||||
|
confirmed the candidate binary MATCH before any of this.
|
||||||
|
|
||||||
|
### SpewBox font selection method for finding (a)
|
||||||
|
|
||||||
|
`AcDream.Cli dump-font-atlas <dat-dir> 0x400000XX` (already-existing
|
||||||
|
tooling, extended nowhere — used as-is) against every populated font id
|
||||||
|
from `0x40000000` to `0x40000032` (38 of 50 candidate ids populated in the
|
||||||
|
installed `client_portal.dat`), reading each `Font` DBObj's own
|
||||||
|
`MaxCharHeight`/`BaselineOffset`/glyph-table-size fields. Cross-referenced
|
||||||
|
against every `FontDid` value appearing in
|
||||||
|
`tests/AcDream.App.Tests/UI/Layout/fixtures/*.json` (24 already-imported
|
||||||
|
retail LayoutDesc dumps) to find the smallest font id actually CONFIRMED
|
||||||
|
in use by any retail UI import acdream has ported, rather than merely
|
||||||
|
present in the DAT. `0x40000025` (11px) was both the global minimum across
|
||||||
|
those fixtures AND the chat window's own minimum — no tie-break needed.
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,18 @@ FEEL right".
|
||||||
## 1. On-screen interface text (CH2 — the SpewBox)
|
## 1. On-screen interface text (CH2 — the SpewBox)
|
||||||
|
|
||||||
- Jump, and press jump again while airborne → **"You can't jump while in
|
- Jump, and press jump again while airborne → **"You can't jump while in
|
||||||
the air"** appears as transient text near the top of the viewport, NOT
|
the air"** appears as transient text flush to the very TOP of the
|
||||||
in the chat window. Chat gets no line at all.
|
viewport (round 3, 2026-08-10 — moved off the earlier 60px-down
|
||||||
|
placeholder), NOT in the chat window. Chat gets no line at all.
|
||||||
- Spam it 5+ times fast: the line refreshes in place (no stacking of
|
- Spam it 5+ times fast: the line refreshes in place (no stacking of
|
||||||
identical text); distinct refusals stack newest-on-top, max 4 lines.
|
identical text); distinct refusals stack newest-on-top, max 4 lines.
|
||||||
- Try `@version` → its output goes to CHAT (green), not the SpewBox.
|
- Try `@version` → its output goes to CHAT (green), not the SpewBox.
|
||||||
- **Report presentation impressions**: position, color, and how long lines
|
- The SpewBox text should now read visibly SMALLER than round 2 (retail
|
||||||
linger are placeholders pending measurement (register AP-177/AP-178) —
|
dat Font `0x40000025`, 11px, replacing the earlier 15px debug font).
|
||||||
say what looks wrong vs your retail memory.
|
- **Report presentation impressions**: position and font are now
|
||||||
|
best-available approximations, not resolved retail values (register
|
||||||
|
AP-178); line lifetime is still a placeholder (AP-177) — say what looks
|
||||||
|
wrong vs your retail memory.
|
||||||
|
|
||||||
## 2. Chat colors (CH1)
|
## 2. Chat colors (CH1)
|
||||||
|
|
||||||
|
|
@ -58,11 +62,31 @@ Side-by-side vs retail if possible:
|
||||||
- An unknown verb like `@somenonsense` → passes through to ACE (server
|
- An unknown verb like `@somenonsense` → passes through to ACE (server
|
||||||
answers, client does not swallow it).
|
answers, client does not swallow it).
|
||||||
|
|
||||||
|
## 5. Help text (round 3, 2026-08-10)
|
||||||
|
|
||||||
|
- `/help` (no args) → TWO lines in the chat window: a "Note: You may
|
||||||
|
substitute a forward slash..." line, then "Available help:" followed by
|
||||||
|
13 real retail topic one-liners (allegiances/channels/chatting/death/
|
||||||
|
emote/fillcomps/friends/house/squelch/status/text/commands). This
|
||||||
|
replaced an acdream-invented cheat sheet ("Chat: /say...", "Client:
|
||||||
|
/help...") — that text should no longer appear at all.
|
||||||
|
- `/help death` → the SAME "Note:" line first, then a SECOND line starting
|
||||||
|
"For more information, type @help <command>." immediately followed by
|
||||||
|
the 8-line corpse/death command listing — not just the 8 lines alone.
|
||||||
|
- `/help somenonsenseverb` → "Unknown command" (no "Note:" line before it;
|
||||||
|
retail's fallback skips the wrapper). This still shows in the CHAT
|
||||||
|
window rather than the SpewBox — a known, tracked gap (#367/AP-186), not
|
||||||
|
a new bug to report.
|
||||||
|
|
||||||
## Known-open, do not report as new
|
## Known-open, do not report as new
|
||||||
|
|
||||||
- Ctrl+M mute chord (#358) — still broken, separate from this campaign.
|
- Ctrl+M mute chord (#358) — still broken, separate from this campaign.
|
||||||
- SpewBox lifetime/position/color are registered placeholders
|
- SpewBox line lifetime is still a placeholder (AP-177) pending a retail
|
||||||
(AP-177/AP-178) pending a retail measurement session.
|
measurement session; position/font are now best-available
|
||||||
|
approximations (AP-178), not confirmed retail pixel values.
|
||||||
|
- `/help <verb>` fallback ("Unknown command") shows in the chat window,
|
||||||
|
not the SpewBox — retail types it for the SpewBox exclusively, but
|
||||||
|
`ChatCommandRouter` has no path there yet (#367/AP-186).
|
||||||
- Allegiance management subcommands print the help refusal instead of
|
- Allegiance management subcommands print the help refusal instead of
|
||||||
executing (#360); `@day`/`@log`/`@render` deferred (#361); four request
|
executing (#360); `@day`/`@log`/`@render` deferred (#361); four request
|
||||||
commands send but responses aren't rendered yet (#362).
|
commands send but responses aren't rendered yet (#362).
|
||||||
|
|
|
||||||
|
|
@ -1091,11 +1091,22 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
SpewBoxController>? spewBoxLease = null;
|
SpewBoxController>? spewBoxLease = null;
|
||||||
if (interaction.RetainedUi is { } spewBoxRetainedUi)
|
if (interaction.RetainedUi is { } spewBoxRetainedUi)
|
||||||
{
|
{
|
||||||
|
// Campaign CH user-gate round 3: resolve the SpewBox's own
|
||||||
|
// retail dat font through the SAME memoized resolver the rest
|
||||||
|
// of the retained UI uses (RetailUiRuntime.Assets), rather than
|
||||||
|
// silently falling back to the debug bitmap font every prior
|
||||||
|
// round shipped with. See SpewBoxController's class remarks
|
||||||
|
// and register row AP-178.
|
||||||
|
RetailUiAssets spewBoxAssets = spewBoxRetainedUi.Runtime.Assets;
|
||||||
|
UiDatFont? spewBoxFont =
|
||||||
|
spewBoxAssets.ResolveFont(SpewBoxController.RetailFontId);
|
||||||
spewBoxLease = scope.Acquire(
|
spewBoxLease = scope.Acquire(
|
||||||
"spew box",
|
"spew box",
|
||||||
() => new SpewBoxController(
|
() => new SpewBoxController(
|
||||||
spewBoxRetainedUi.Host.Root,
|
spewBoxRetainedUi.Host.Root,
|
||||||
new SpewBoxVM(d.Runtime.CommunicationOwner.SpewBox)),
|
new SpewBoxVM(d.Runtime.CommunicationOwner.SpewBox),
|
||||||
|
spewBoxFont,
|
||||||
|
spewBoxAssets.DebugFont),
|
||||||
static value => value.Dispose());
|
static value => value.Dispose());
|
||||||
}
|
}
|
||||||
CompositionAcquisitionScope.CompositionAcquisitionLease<
|
CompositionAcquisitionScope.CompositionAcquisitionLease<
|
||||||
|
|
|
||||||
|
|
@ -321,6 +321,17 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
public UiHost Host => _bindings.Host;
|
public UiHost Host => _bindings.Host;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shared dat/sprite/font resolvers this runtime was built with.
|
||||||
|
/// Campaign CH user-gate round 3: lets a controller built OUTSIDE this
|
||||||
|
/// runtime's own <c>Mount*</c> graph (e.g. <c>SpewBoxController</c>,
|
||||||
|
/// composed directly in <c>LivePresentationComposition</c>) resolve the
|
||||||
|
/// SAME memoized <see cref="UiDatFont"/> instances instead of loading
|
||||||
|
/// its own copy.
|
||||||
|
/// </summary>
|
||||||
|
public RetailUiAssets Assets => _bindings.Assets;
|
||||||
|
|
||||||
public ItemInteractionController ItemInteraction => _bindings.Inventory.ItemInteraction;
|
public ItemInteractionController ItemInteraction => _bindings.Inventory.ItemInteraction;
|
||||||
public CharacterSheetProvider CharacterSheetProvider => _bindings.Character.Provider;
|
public CharacterSheetProvider CharacterSheetProvider => _bindings.Character.Provider;
|
||||||
public ToolbarController? ToolbarController { get; private set; }
|
public ToolbarController? ToolbarController { get; private set; }
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.UI.Layout;
|
using AcDream.App.UI.Layout;
|
||||||
using AcDream.UI.Abstractions.Panels.SpewBox;
|
using AcDream.UI.Abstractions.Panels.SpewBox;
|
||||||
|
|
||||||
|
|
@ -67,19 +68,73 @@ namespace AcDream.App.UI;
|
||||||
/// the divergence register rows this class cites for each remaining
|
/// the divergence register rows this class cites for each remaining
|
||||||
/// placeholder.
|
/// placeholder.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
|
/// <remarks>
|
||||||
|
/// <b>Campaign CH user-gate round 3 (2026-08-10), finding (a) — position
|
||||||
|
/// and font.</b> The user reported live: "still not aligned all the way to
|
||||||
|
/// the top" and "not the correct font and size (retail's is SMALLER than
|
||||||
|
/// ours)". Two changes, both still user-DIRECTED approximations (not
|
||||||
|
/// resolved retail values — the true absolute position/parent and the true
|
||||||
|
/// retail font remain unmeasurable statically; <c>SpewBoxLayoutDumpDiagnostic</c>
|
||||||
|
/// re-run this round still finds no <c>FontDid</c>/colour property on
|
||||||
|
/// element <c>0x10000048</c> or its ListBox child):
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><b>Position:</b> <see cref="TopOffset"/> is now <c>0</c> — flush
|
||||||
|
/// to the viewport top, per the user's explicit direction ("mount at
|
||||||
|
/// viewport top-center, exactly"). The centered-X, recompute-every-frame
|
||||||
|
/// behavior from CH2 nit 1 is unchanged.</item>
|
||||||
|
/// <item><b>Font:</b> <see cref="SpewBoxController"/> now accepts a
|
||||||
|
/// resolved <see cref="UiDatFont"/> (retail font id
|
||||||
|
/// <see cref="RetailFontId"/>, <c>0x40000025</c>) instead of silently
|
||||||
|
/// falling back to the debug <see cref="BitmapFont"/> at its ad hoc 15px
|
||||||
|
/// pixel height (the pre-round-3 behavior — no <c>DatFont</c>/<c>Font</c>
|
||||||
|
/// was ever wired here at all). <c>0x40000025</c> is <c>MaxCharHeight=11</c>
|
||||||
|
/// px (confirmed via <c>AcDream.Cli dump-font-atlas</c> against the
|
||||||
|
/// installed DAT, sweeping every populated font id
|
||||||
|
/// <c>0x40000000</c>-<c>0x40000032</c>) — the SMALLEST font id actually
|
||||||
|
/// confirmed IN USE by any of acdream's currently-imported retail
|
||||||
|
/// LayoutDesc fixtures (cross-referenced across every
|
||||||
|
/// <c>tests/AcDream.App.Tests/UI/Layout/fixtures/*.json</c> dump), and
|
||||||
|
/// it is ALSO the chat window's own smallest font — the same
|
||||||
|
/// <c>0x2100006F</c> floating-window 1/2/3/4 indicator badges
|
||||||
|
/// (<c>ChatWindowController.Indicator1-4Id</c>) use it. Both selection
|
||||||
|
/// criteria from the round-3 brief ("smallest DAT font used by retail UI
|
||||||
|
/// imports" vs "the chat window's own font, whichever is smaller") land
|
||||||
|
/// on the SAME id, so there was no tension to resolve. This is visibly
|
||||||
|
/// smaller than the previous 15px debug font, matching the user's
|
||||||
|
/// report. Falls back to the debug font only if the dat resolve fails
|
||||||
|
/// (matching every other retained-UI controller's pattern, e.g.
|
||||||
|
/// <c>ChatWindowController.Bind</c>).</item>
|
||||||
|
/// </list>
|
||||||
|
/// Register row AP-178 updated to record both dispositions.
|
||||||
|
/// </remarks>
|
||||||
internal sealed class SpewBoxController : IDisposable
|
internal sealed class SpewBoxController : IDisposable
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retail dat Font id this controller resolves for its text
|
||||||
|
/// (Campaign CH user-gate round 3 — see the class remarks). Not
|
||||||
|
/// retail's own measured SpewBox font (unmeasurable — no FontDid
|
||||||
|
/// property was found on the authored element); the smallest DAT font
|
||||||
|
/// confirmed in use by any currently-imported retail LayoutDesc,
|
||||||
|
/// chosen so the rendered text is visibly smaller than the prior debug
|
||||||
|
/// fallback, per the user's report.
|
||||||
|
/// </summary>
|
||||||
|
internal const uint RetailFontId = 0x40000025u;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Register row AP-178 (screen position): retail's authored ABSOLUTE
|
/// Register row AP-178 (screen position): retail's authored ABSOLUTE
|
||||||
/// screen position is still unknown — the LayoutDesc dump (see class
|
/// screen position is still unknown — the LayoutDesc dump (see class
|
||||||
/// remarks) recovered the element's position as <c>(0,0)</c> relative
|
/// remarks) recovered the element's position as <c>(0,0)</c> relative
|
||||||
/// to a PARENT this sweep could not identify, so this centered-top
|
/// to a PARENT this sweep could not identify. Campaign CH user-gate
|
||||||
/// placement remains acdream's own choice, not a resolved retail value.
|
/// round 3 (2026-08-10): the user reported the box was not flush to
|
||||||
/// (The SIBLING row AP-177 — the invented line-lifetime timeout — lives
|
/// the very top of the screen; mounted at <c>0</c> now, per explicit
|
||||||
/// in <see cref="SpewBoxState.DefaultLifetime"/>'s own doc comment, not
|
/// user direction — still acdream's own placement choice pending the
|
||||||
|
/// true retail parent/offset, but now matching the user's live report
|
||||||
|
/// instead of an arbitrary 60px placeholder. (The SIBLING row AP-177 —
|
||||||
|
/// the invented line-lifetime timeout — lives in
|
||||||
|
/// <see cref="SpewBoxState.DefaultLifetime"/>'s own doc comment, not
|
||||||
/// here; this controller does not own that concern.)
|
/// here; this controller does not own that concern.)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const float TopOffset = 60f;
|
private const float TopOffset = 0f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Register row AP-178 (extent): AUTHORED, not a placeholder — the
|
/// Register row AP-178 (extent): AUTHORED, not a placeholder — the
|
||||||
|
|
@ -121,9 +176,10 @@ internal sealed class SpewBoxController : IDisposable
|
||||||
/// (research doc §3.2.3); the LayoutDesc dump (see class remarks) also
|
/// (research doc §3.2.3); the LayoutDesc dump (see class remarks) also
|
||||||
/// never surfaced a colour property for this element. The exact retail
|
/// never surfaced a colour property for this element. The exact retail
|
||||||
/// value simply happens to coincide with the Tell colour, per the user's
|
/// value simply happens to coincide with the Tell colour, per the user's
|
||||||
/// live observation. SIZE/POSITION/FONT remain OPEN — the user reports
|
/// live observation. POSITION and FONT were re-addressed at Campaign CH
|
||||||
/// all three still differ from retail; user gate round 1: differs,
|
/// user-gate round 3 (2026-08-10) — see the class remarks and the
|
||||||
/// iterating.
|
/// <see cref="TopOffset"/>/<see cref="RetailFontId"/> comments; both
|
||||||
|
/// remain acdream-directed approximations, not resolved retail values.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static readonly Vector4 SpewBoxColor = new(1f, 1f, 0.247f, 1f);
|
private static readonly Vector4 SpewBoxColor = new(1f, 1f, 0.247f, 1f);
|
||||||
|
|
||||||
|
|
@ -134,7 +190,18 @@ internal sealed class SpewBoxController : IDisposable
|
||||||
private UiText.Line[] _lines = Array.Empty<UiText.Line>();
|
private UiText.Line[] _lines = Array.Empty<UiText.Line>();
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
public SpewBoxController(UiRoot root, SpewBoxVM vm)
|
/// <param name="root">Retained-UI root this element mounts under.</param>
|
||||||
|
/// <param name="vm">SpewBox view-model (bounded, newest-on-top queue).</param>
|
||||||
|
/// <param name="font">
|
||||||
|
/// Resolved retail dat font (<see cref="RetailFontId"/>) — Campaign CH
|
||||||
|
/// user-gate round 3. Null falls back to <paramref name="debugFont"/>,
|
||||||
|
/// matching every other retained-UI controller's dat-font pattern (e.g.
|
||||||
|
/// <c>ChatWindowController.Bind</c>).
|
||||||
|
/// </param>
|
||||||
|
/// <param name="debugFont">Fallback bitmap font, used only when
|
||||||
|
/// <paramref name="font"/> is null.</param>
|
||||||
|
public SpewBoxController(
|
||||||
|
UiRoot root, SpewBoxVM vm, UiDatFont? font = null, BitmapFont? debugFont = null)
|
||||||
{
|
{
|
||||||
_root = root ?? throw new ArgumentNullException(nameof(root));
|
_root = root ?? throw new ArgumentNullException(nameof(root));
|
||||||
_vm = vm ?? throw new ArgumentNullException(nameof(vm));
|
_vm = vm ?? throw new ArgumentNullException(nameof(vm));
|
||||||
|
|
@ -152,6 +219,12 @@ internal sealed class SpewBoxController : IDisposable
|
||||||
Height = SpewBoxHeight,
|
Height = SpewBoxHeight,
|
||||||
Anchors = AnchorEdges.None,
|
Anchors = AnchorEdges.None,
|
||||||
Centered = true,
|
Centered = true,
|
||||||
|
// Campaign CH user-gate round 3: retail dat font (RetailFontId)
|
||||||
|
// when resolved, falling back to the debug bitmap font exactly
|
||||||
|
// like every other retained-UI controller (ChatWindowController
|
||||||
|
// et al.) — see the class remarks.
|
||||||
|
DatFont = font,
|
||||||
|
Font = debugFont,
|
||||||
// AUTHORED MaxConcurrentItems is 4, not retail's code-default 1
|
// AUTHORED MaxConcurrentItems is 4, not retail's code-default 1
|
||||||
// (see SpewBoxState.MaxConcurrentItems) — OneLine=true would
|
// (see SpewBoxState.MaxConcurrentItems) — OneLine=true would
|
||||||
// silently collapse the box back down to showing only the
|
// silently collapse the box back down to showing only the
|
||||||
|
|
|
||||||
|
|
@ -184,7 +184,7 @@ public static class ChatCommandRouter
|
||||||
{
|
{
|
||||||
if (EqAny(trimmed, "/help", "/?", "@help", "@?"))
|
if (EqAny(trimmed, "/help", "/?", "@help", "@?"))
|
||||||
{
|
{
|
||||||
vm.ShowSystemMessage(BuildHelpText());
|
EmitBareHelp(vm);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,25 +195,64 @@ public static class ChatCommandRouter
|
||||||
if (StartsWithAny(trimmed, "/help ", "@help ", "/? ", "@? "))
|
if (StartsWithAny(trimmed, "/help ", "@help ", "/? ", "@? "))
|
||||||
{
|
{
|
||||||
string verb = trimmed[(trimmed.IndexOf(' ') + 1)..].Trim();
|
string verb = trimmed[(trimmed.IndexOf(' ') + 1)..].Trim();
|
||||||
vm.ShowSystemMessage(BuildVerbHelpText(verb));
|
EmitVerbHelp(verb, vm);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildVerbHelpText(string verb)
|
/// <summary>
|
||||||
|
/// Bare <c>/help</c> — retail's <c>DoHelp</c> arg2<=0 branch. TWO
|
||||||
|
/// separate scroll entries, never one concatenated blob (Campaign CH
|
||||||
|
/// user-gate round 3, finding (b) — see <see cref="RetailCommandHelpTable"/>'s
|
||||||
|
/// class remarks for the full print-sequence trace).
|
||||||
|
/// </summary>
|
||||||
|
private static void EmitBareHelp(ChatVM vm)
|
||||||
|
{
|
||||||
|
vm.ShowSystemMessage(RetailCommandHelpTable.HelpPrefixNote);
|
||||||
|
vm.ShowSystemMessage(RetailCommandHelpTable.AvailableHelpListing);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>/help <verb></c> — retail's <c>DoHelp</c> arg2>0 branch.
|
||||||
|
/// Campaign CH user-gate round 3, finding (c): a resolved verb gets the
|
||||||
|
/// SAME two-entry shape as the bare listing (Note, then
|
||||||
|
/// <see cref="RetailCommandHelpTable.ForMoreInformationPrefix"/>
|
||||||
|
/// immediately concatenated — NOT a third entry — with the verb's own
|
||||||
|
/// Detail text); an unresolved verb gets retail's real
|
||||||
|
/// <see cref="RetailCommandHelpTable.UnknownCommand"/> text instead of
|
||||||
|
/// an acdream-invented "No help available" message.
|
||||||
|
/// </summary>
|
||||||
|
private static void EmitVerbHelp(string verb, ChatVM vm)
|
||||||
{
|
{
|
||||||
if (verb.Length == 0)
|
if (verb.Length == 0)
|
||||||
return BuildHelpText();
|
{
|
||||||
|
EmitBareHelp(vm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
string normalized = verb.TrimStart('/', '@');
|
string normalized = verb.TrimStart('/', '@');
|
||||||
if (RetailClientCommandCatalog.TryGetHelpText(normalized, out string catalogText))
|
if (RetailClientCommandCatalog.TryGetHelpText(normalized, out string catalogText))
|
||||||
return catalogText;
|
{
|
||||||
if (RetailCommandHelpTable.TryGetHelpText(normalized, out string tableText))
|
vm.ShowSystemMessage(RetailCommandHelpTable.HelpPrefixNote);
|
||||||
return tableText;
|
vm.ShowSystemMessage(RetailCommandHelpTable.ForMoreInformationPrefix + catalogText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return $"No help available for '{verb}'.";
|
if (RetailCommandHelpTable.TryGetHelpText(normalized, out string tableText))
|
||||||
|
{
|
||||||
|
vm.ShowSystemMessage(RetailCommandHelpTable.HelpPrefixNote);
|
||||||
|
vm.ShowSystemMessage(RetailCommandHelpTable.ForMoreInformationPrefix + tableText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retail types this 0x1A (ClientLocal) -> SpewBox-only; ChatVM has
|
||||||
|
// no SpewBox routing capability yet, so this still renders via the
|
||||||
|
// chat scroll — a pre-existing gap, not new this round. See the
|
||||||
|
// class remarks on RetailCommandHelpTable.UnknownCommand and
|
||||||
|
// ISSUES.md #367.
|
||||||
|
vm.ShowSystemMessage(RetailCommandHelpTable.UnknownCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool EqAny(string value, params string[] options)
|
private static bool EqAny(string value, params string[] options)
|
||||||
|
|
@ -237,14 +276,4 @@ public static class ChatCommandRouter
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildHelpText() =>
|
|
||||||
$"{RetailCommandHelpTable.HelpPrefixNote}\n" +
|
|
||||||
"Chat: /say (default), /tell <name>, <text>, /reply, /retell\n" +
|
|
||||||
"Channels: /general /trade /fellowship /a (allegiance room)\n" +
|
|
||||||
" /patron /vassals /monarch /covassals\n" +
|
|
||||||
" /lfg /roleplay /society /olthoi\n" +
|
|
||||||
"Client: /help [command] (this) /clear /framerate /loc\n" +
|
|
||||||
$" {RetailClientCommandCatalog.BuildHelpText()}\n" +
|
|
||||||
"Server: type @acehelp or @acecommands for ACE's full list.";
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -221,7 +221,7 @@ public static class RetailClientCommandCatalog
|
||||||
// (data_7de2f8) — the first paragraph only; the full multi-paragraph
|
// (data_7de2f8) — the first paragraph only; the full multi-paragraph
|
||||||
// block is reproduced verbatim by DoEndurance itself and is long
|
// block is reproduced verbatim by DoEndurance itself and is long
|
||||||
// enough that only the opening line is duplicated here as a teaser —
|
// enough that only the opening line is duplicated here as a teaser —
|
||||||
// BuildHelpText below uses the SAME text callers already see through
|
// this is the SAME text callers already see through
|
||||||
// ClientCommandController.
|
// ClientCommandController.
|
||||||
private static readonly Definition Endurance = NoArguments(
|
private static readonly Definition Endurance = NoArguments(
|
||||||
ClientCommandId.Endurance,
|
ClientCommandId.Endurance,
|
||||||
|
|
@ -668,52 +668,6 @@ public static class RetailClientCommandCatalog
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Help line generated from the same definition routing uses.</summary>
|
|
||||||
public static string BuildHelpText() => string.Join("\n ",
|
|
||||||
Lifestone.HelpText,
|
|
||||||
Marketplace.HelpText,
|
|
||||||
PkArena.HelpText,
|
|
||||||
PkLiteArena.HelpText,
|
|
||||||
PkLite.HelpText,
|
|
||||||
HouseRecall.HelpText,
|
|
||||||
MansionRecall.HelpText,
|
|
||||||
HouseAbandon.HelpText,
|
|
||||||
QueryAge.HelpText,
|
|
||||||
QueryBirth.HelpText,
|
|
||||||
FrameRate.HelpText,
|
|
||||||
LockUi.HelpText,
|
|
||||||
Version.HelpText,
|
|
||||||
Location.HelpText,
|
|
||||||
Corpse.HelpText,
|
|
||||||
Die.HelpText,
|
|
||||||
Clear.HelpText,
|
|
||||||
SaveUi.HelpText,
|
|
||||||
LoadUi.HelpText,
|
|
||||||
SaveAutoUi.HelpText,
|
|
||||||
LoadAutoUi.HelpText,
|
|
||||||
Away.HelpText,
|
|
||||||
Consent.HelpText,
|
|
||||||
Emote.HelpText,
|
|
||||||
Emotes.HelpText,
|
|
||||||
Friends.HelpText,
|
|
||||||
Squelch.HelpText,
|
|
||||||
Unsquelch.HelpText,
|
|
||||||
Filter.HelpText,
|
|
||||||
Unfilter.HelpText,
|
|
||||||
MessageTypes.HelpText,
|
|
||||||
FillComponents.HelpText,
|
|
||||||
Endurance.HelpText,
|
|
||||||
Speaker.HelpText,
|
|
||||||
SetTitle.HelpText,
|
|
||||||
ChatToggle.HelpText,
|
|
||||||
NoTellToggle.HelpText,
|
|
||||||
JoinChannel.HelpText,
|
|
||||||
LeaveChannel.HelpText,
|
|
||||||
Permit.HelpText,
|
|
||||||
HouseAvailableList.HelpText,
|
|
||||||
AllegianceHometown.HelpText,
|
|
||||||
AllegianceInfo.HelpText);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Every verb string this catalog dispatches, INCLUDING the
|
/// Every verb string this catalog dispatches, INCLUDING the
|
||||||
/// specially-parsed "house"/"hou"/"allegiance"/"all" verbs (which are
|
/// specially-parsed "house"/"hou"/"allegiance"/"all" verbs (which are
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,48 @@ namespace AcDream.UI.Abstractions.Panels.Chat;
|
||||||
/// unresolved mechanism and remain acdream summaries. See the remarks on
|
/// unresolved mechanism and remain acdream summaries. See the remarks on
|
||||||
/// <see cref="ChannelsGroupSummary"/> for the full extraction method.
|
/// <see cref="ChannelsGroupSummary"/> for the full extraction method.
|
||||||
/// </para>
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// <b>Campaign CH user-gate round 3 (2026-08-10), findings (b)/(c):</b> the
|
||||||
|
/// round 2 pass extracted the individual STRINGS byte-exact but never
|
||||||
|
/// traced <c>ClientCommunicationSystem::DoHelp @0x0057f9e0</c>'s complete
|
||||||
|
/// PRINT SEQUENCE — so the CONTENT was right while the SHAPE (how many
|
||||||
|
/// transcript entries, in what order, with what wrapping) was still
|
||||||
|
/// acdream-invented. Traced this round via a byte-sweep of DoHelp's own
|
||||||
|
/// range (0x57f9e0-0x57fe7e) plus every Summary-branch it calls into
|
||||||
|
/// (HelpEmote/HelpSquelch/HelpStatusGroup/HelpTextGroup/HelpAllGroup),
|
||||||
|
/// against the PDB-paired <c>C:\Users\erikn\Downloads\acclient.exe</c>
|
||||||
|
/// (verified MATCH). DoHelp ALWAYS prints via exactly two
|
||||||
|
/// <c>AddTextToScroll</c> calls (never one concatenated blob), both typed
|
||||||
|
/// <c>0</c> (informational):
|
||||||
|
/// <list type="number">
|
||||||
|
/// <item>the SAME <see cref="HelpPrefixNote"/> line, unconditionally —
|
||||||
|
/// for the bare listing AND for every successful <c>/help <verb></c>
|
||||||
|
/// lookup;</item>
|
||||||
|
/// <item>bare <c>/help</c>: <see cref="AvailableHelpListing"/> (13 items,
|
||||||
|
/// straight-line concatenation of 8 inline literals plus 5 delegated to
|
||||||
|
/// each group's own Summary_HelpType branch, in DoHelp's exact source
|
||||||
|
/// order). <c>/help <verb></c> when the verb resolves:
|
||||||
|
/// <see cref="ForMoreInformationPrefix"/> immediately followed by the
|
||||||
|
/// verb's own Detail-branch text (e.g. <see cref="DeathGroupDetail"/>) —
|
||||||
|
/// ONE string, no blank line between the prefix and the listing, because
|
||||||
|
/// retail's own handler call APPENDS into the same accumulator the
|
||||||
|
/// prefix was built into (<c>ClientCommunicationSystem::DoHelp</c>,
|
||||||
|
/// arg2>0 branch, the <c>eax_35(2, var_18, &var_10)</c> call).</item>
|
||||||
|
/// </list>
|
||||||
|
/// When the verb does NOT resolve, DoHelp prints ONE entry,
|
||||||
|
/// <see cref="UnknownCommand"/>, typed <c>0x1A</c> (<c>ClientLocal</c>) —
|
||||||
|
/// retail routes that type to the SpewBox exclusively, never the chat
|
||||||
|
/// window (<c>docs/research/2026-08-09-chat-retail-interface-text.md</c>
|
||||||
|
/// §2.1/§2.2). <c>ChatCommandRouter</c> operates on <c>ChatVM</c>
|
||||||
|
/// (<c>AcDream.UI.Abstractions</c>), which has no SpewBox routing
|
||||||
|
/// capability — wiring that would mean threading a
|
||||||
|
/// <c>RuntimeCommunicationState</c>-shaped dependency down into a layer
|
||||||
|
/// that must stay presentation/Runtime-independent, out of this round's
|
||||||
|
/// scope. The unknown-verb fallback therefore still renders via the chat
|
||||||
|
/// scroll, a pre-existing (not newly introduced) gap now tracked at
|
||||||
|
/// ISSUES.md #367 instead of silently continuing unregistered.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class RetailCommandHelpTable
|
public static class RetailCommandHelpTable
|
||||||
{
|
{
|
||||||
|
|
@ -73,10 +115,42 @@ public static class RetailCommandHelpTable
|
||||||
public const string Retell =
|
public const string Retell =
|
||||||
"@retell <text> - Sends the text to the last person you @tell'd. You may also use @rt.";
|
"@retell <text> - Sends the text to the last person you @tell'd. You may also use @rt.";
|
||||||
|
|
||||||
// acclient_2013_pseudo_c.txt:1031564 (data_7e11e0), the "Note:" line
|
// acclient_2013_pseudo_c.txt:394980 (data_0x7e11e0). RE-SWEPT byte-exact
|
||||||
// DoHelp @0x0057F9E0 prints alongside the bare group index.
|
// Campaign CH user-gate round 3 (2026-08-10) via sweep_weenie_strings.py
|
||||||
|
// --range 0x57f9e0 0x57fe7e --ascii-only against the PDB-paired
|
||||||
|
// C:\Users\erikn\Downloads\acclient.exe (verified MATCH). The ORIGINAL
|
||||||
|
// extraction dropped the leading blank line and the trailing double
|
||||||
|
// newline that are part of retail's own literal — this is DoHelp's
|
||||||
|
// OWN, always-first scroll entry (a dedicated AddTextToScroll call,
|
||||||
|
// never concatenated with what follows), printed unconditionally both
|
||||||
|
// for the bare /help listing and for every successful /help <verb>
|
||||||
|
// lookup. See the class remarks for the complete print-sequence trace.
|
||||||
public const string HelpPrefixNote =
|
public const string HelpPrefixNote =
|
||||||
"Note: You may substitute a forward slash (/) for the at symbol (@).";
|
"\nNote: You may substitute a forward slash (/) for the at symbol (@).\n\n";
|
||||||
|
|
||||||
|
// acclient_2013_pseudo_c.txt:395087 (data_0x7e1178), swept alongside
|
||||||
|
// HelpPrefixNote above. DoHelp's arg2>0 (verb-specified) branch builds
|
||||||
|
// this as the START of its second scroll entry, then the resolved
|
||||||
|
// verb's own Detail-branch handler APPENDS its listing directly onto
|
||||||
|
// the SAME accumulator (ClientCommunicationSystem::DoHelp, the
|
||||||
|
// `eax_35(2, var_18, &var_10)` call) -- so the retail-faithful port is
|
||||||
|
// string concatenation with NO separator, not two entries. The literal
|
||||||
|
// text "<command>" is NOT a format placeholder -- the decomp shows a
|
||||||
|
// straight PStringBase construction with no sprintf/substitution call
|
||||||
|
// between this literal and its use, so retail never actually inserts
|
||||||
|
// the real verb name here. Ported verbatim per CLAUDE.md's "do not fix
|
||||||
|
// the decompiled code" rule, even though it reads like an unfinished
|
||||||
|
// dev message.
|
||||||
|
public const string ForMoreInformationPrefix =
|
||||||
|
"For more information, type @help <command>.\n";
|
||||||
|
|
||||||
|
// acclient_2013_pseudo_c.txt:395052 (u"Unknown command", UTF-16LE) --
|
||||||
|
// DoHelp's fallback when the verb hash lookup fails, or resolves to an
|
||||||
|
// entry with no registered help callback. Retail types this 0x1A
|
||||||
|
// (ClientLocal) -- SpewBox-only; see the class remarks' routing note
|
||||||
|
// and ISSUES.md #367 for why ChatCommandRouter still shows it in the
|
||||||
|
// chat scroll.
|
||||||
|
public const string UnknownCommand = "Unknown command";
|
||||||
|
|
||||||
// @mr/@pr are registered with a NULL function pointer in the 2013
|
// @mr/@pr are registered with a NULL function pointer in the 2013
|
||||||
// build (verified at 0x00583041/0x005830C1 — arg3 is 0), so they never
|
// build (verified at 0x00583041/0x005830C1 — arg3 is 0), so they never
|
||||||
|
|
@ -305,6 +379,40 @@ public static class RetailCommandHelpTable
|
||||||
public const string CommandsGroupDetail =
|
public const string CommandsGroupDetail =
|
||||||
CommandsGroupSummary + GroupDetailUnverifiedSuffix;
|
CommandsGroupSummary + GroupDetailUnverifiedSuffix;
|
||||||
|
|
||||||
|
// Campaign CH user-gate round 3 (2026-08-10), finding (b): DoHelp's
|
||||||
|
// bare-/help "else" branch (arg2<=0, acclient_2013_pseudo_c.txt:395089-
|
||||||
|
// 395268), swept whole against C:\Users\erikn\Downloads\acclient.exe
|
||||||
|
// (--range 0x57f9e0 0x57fe7e --ascii-only, verified MATCH). ONE
|
||||||
|
// straight-line concatenation of 13 items, in this exact source order:
|
||||||
|
// the "Available help:\n" header, then 8 items DoHelp builds from its
|
||||||
|
// OWN inline literals (allegiances/channels/chatting/death/fillcomps/
|
||||||
|
// friends/house — swept directly), interleaved with 5 items DoHelp
|
||||||
|
// builds by calling each group's OWN Summary_HelpType branch
|
||||||
|
// (HelpEmote/HelpSquelch/HelpStatusGroup/HelpTextGroup/HelpAllGroup —
|
||||||
|
// each independently swept from its own function range; every one of
|
||||||
|
// those 5 functions has the identical
|
||||||
|
// `if (arg2 != Summary_HelpType) {Detail} else {"@help X - ..."}`
|
||||||
|
// shape HelpEmote makes explicit at acclient_2013_pseudo_c.txt:388664).
|
||||||
|
// channels/chatting/commands' summary lines are reused from the
|
||||||
|
// consts above (independently cross-validated: both extractions agree
|
||||||
|
// byte-for-byte). Prints as DoHelp's SECOND scroll entry, right after
|
||||||
|
// HelpPrefixNote's own — see the class remarks and
|
||||||
|
// ChatCommandRouter's bare-/help handling.
|
||||||
|
public const string AvailableHelpListing =
|
||||||
|
"Available help:\n"
|
||||||
|
+ "@help allegiances - Commands to help you deal with your Allegiance.\n"
|
||||||
|
+ ChannelsGroupSummary + "\n"
|
||||||
|
+ ChattingGroupSummaryVerbatim + "\n"
|
||||||
|
+ "@help death - Commands for making, finding, and looting corpses.\n"
|
||||||
|
+ "@help emote - How to perform text and action emotes.\n"
|
||||||
|
+ "@help fillcomps - A command to help you buy components in bulk.\n"
|
||||||
|
+ "@help friends - Commands to help you manage your friends list.\n"
|
||||||
|
+ "@help house - Commands that help you manage your house, including guest and storage management.\n"
|
||||||
|
+ "@help squelch - Commands that let you block out messages from other players.\n"
|
||||||
|
+ "@help status - Commands that display useful information.\n"
|
||||||
|
+ "@help text - Commands that help you manage your text window.\n"
|
||||||
|
+ CommandsGroupSummary + "\n";
|
||||||
|
|
||||||
private static readonly FrozenDictionary<string, string> ByVerb =
|
private static readonly FrozenDictionary<string, string> ByVerb =
|
||||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
using AcDream.App.UI;
|
using AcDream.App.UI;
|
||||||
using AcDream.Core.Chat;
|
using AcDream.Core.Chat;
|
||||||
using AcDream.UI.Abstractions.Panels.SpewBox;
|
using AcDream.UI.Abstractions.Panels.SpewBox;
|
||||||
|
using DatReaderWriter.Types;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.UI;
|
namespace AcDream.App.Tests.UI;
|
||||||
|
|
||||||
|
|
@ -175,4 +177,79 @@ public sealed class SpewBoxControllerTests
|
||||||
|
|
||||||
Assert.Empty(root.Children);
|
Assert.Empty(root.Children);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Campaign CH user-gate round 3 (2026-08-10), finding (a) ─────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Construction_MountsFlushToTheViewportTop()
|
||||||
|
{
|
||||||
|
// The user reported the box "still not aligned all the way to the
|
||||||
|
// top" — TopOffset moved from the round-1 60px placeholder to 0.
|
||||||
|
var root = new UiRoot { Width = 1280f, Height = 720f };
|
||||||
|
using var controller = new SpewBoxController(root, new SpewBoxVM(new SpewBoxState()));
|
||||||
|
|
||||||
|
UiText text = Assert.IsType<UiText>(root.Children.OfType<UiText>().Single());
|
||||||
|
Assert.Equal(0f, text.Top);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Tick_KeepsTopFlushAndRecentersX_AcrossAResize()
|
||||||
|
{
|
||||||
|
var root = new UiRoot { Width = 1280f, Height = 720f };
|
||||||
|
var state = new SpewBoxState();
|
||||||
|
using var controller = new SpewBoxController(root, new SpewBoxVM(state));
|
||||||
|
|
||||||
|
state.Enqueue("resize me");
|
||||||
|
root.Tick(dt: 0d, nowMs: 1000L);
|
||||||
|
|
||||||
|
UiText text = Assert.IsType<UiText>(root.Children.OfType<UiText>().Single());
|
||||||
|
float widthBefore = root.Width;
|
||||||
|
Assert.Equal((widthBefore - 450f) / 2f, text.Left);
|
||||||
|
Assert.Equal(0f, text.Top);
|
||||||
|
|
||||||
|
// Simulate a window resize, then the next per-frame tick.
|
||||||
|
root.Width = 1920f;
|
||||||
|
root.Tick(dt: 0d, nowMs: 1016L);
|
||||||
|
|
||||||
|
Assert.Equal((1920f - 450f) / 2f, text.Left);
|
||||||
|
Assert.Equal(0f, text.Top); // top offset never depends on width
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Construction_WithResolvedRetailFont_WiresDatFontOntoTheText()
|
||||||
|
{
|
||||||
|
// Campaign CH user-gate round 3: the retail dat font (id
|
||||||
|
// SpewBoxController.RetailFontId) is now actually WIRED, where
|
||||||
|
// before the controller never set DatFont/Font at all and silently
|
||||||
|
// fell through to the render context's default debug font.
|
||||||
|
var root = new UiRoot { Width = 1280f, Height = 720f };
|
||||||
|
var font = new UiDatFont(
|
||||||
|
fgTex: 1, fgW: 64, fgH: 64,
|
||||||
|
bgTex: 0, bgW: 0, bgH: 0,
|
||||||
|
lineHeight: 11f, baselineOffset: 9f,
|
||||||
|
glyphs: new Dictionary<char, FontCharDesc>());
|
||||||
|
|
||||||
|
using var controller = new SpewBoxController(
|
||||||
|
root, new SpewBoxVM(new SpewBoxState()), font, debugFont: null);
|
||||||
|
|
||||||
|
UiText text = Assert.IsType<UiText>(root.Children.OfType<UiText>().Single());
|
||||||
|
Assert.Same(font, text.DatFont);
|
||||||
|
Assert.Equal(11f, text.DatFont!.LineHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Construction_WithoutAResolvedFont_FallsBackToTheSuppliedDebugFont()
|
||||||
|
{
|
||||||
|
// No installed-DAT font available (e.g. headless) -- the debug
|
||||||
|
// bitmap font parameter is still wired through, matching every
|
||||||
|
// other retained-UI controller's dat-font/debug-font pattern.
|
||||||
|
var root = new UiRoot { Width = 1280f, Height = 720f };
|
||||||
|
|
||||||
|
using var controller = new SpewBoxController(
|
||||||
|
root, new SpewBoxVM(new SpewBoxState()), font: null, debugFont: null);
|
||||||
|
|
||||||
|
UiText text = Assert.IsType<UiText>(root.Children.OfType<UiText>().Single());
|
||||||
|
Assert.Null(text.DatFont);
|
||||||
|
Assert.Null(text.Font);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -237,6 +237,11 @@ public class ChatCommandRouterTests
|
||||||
Assert.Contains(log.Snapshot(), entry => entry.Text.Contains("Returns you to the last lifestone"));
|
Assert.Contains(log.Snapshot(), entry => entry.Text.Contains("Returns you to the last lifestone"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Campaign CH user-gate round 3 (2026-08-10), finding (c): a resolved
|
||||||
|
// verb prints retail's DoHelp SHAPE, not just its content — the SAME
|
||||||
|
// HelpPrefixNote entry every /help path prints, then a SECOND entry
|
||||||
|
// that is ForMoreInformationPrefix concatenated directly onto the
|
||||||
|
// verb's own detail text (no blank line, no separate third entry).
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("/help mr", "@mr <text> - Sends the text to the last person who used @m to send you a message. This only works for monarchs.")]
|
[InlineData("/help mr", "@mr <text> - Sends the text to the last person who used @m to send you a message. This only works for monarchs.")]
|
||||||
[InlineData("/help pr", "@pr <text> - Sends the text to the last vassal who used @p to send you a message.")]
|
[InlineData("/help pr", "@pr <text> - Sends the text to the last vassal who used @p to send you a message.")]
|
||||||
|
|
@ -251,18 +256,47 @@ public class ChatCommandRouterTests
|
||||||
var outcome = ChatCommandRouter.Submit(input, vm, bus, ChatChannelKind.Say);
|
var outcome = ChatCommandRouter.Submit(input, vm, bus, ChatChannelKind.Say);
|
||||||
|
|
||||||
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
||||||
Assert.Contains(log.Snapshot(), entry => entry.Text == expected);
|
var entries = log.Snapshot();
|
||||||
|
Assert.Equal(2, entries.Length);
|
||||||
|
Assert.Equal(RetailCommandHelpTable.HelpPrefixNote, entries[0].Text);
|
||||||
|
Assert.Equal(RetailCommandHelpTable.ForMoreInformationPrefix + expected, entries[1].Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void HelpVerb_UnknownVerb_ShowsFallbackMessage()
|
public void HelpVerb_UnknownVerb_ShowsRetailUnknownCommandText()
|
||||||
{
|
{
|
||||||
|
// Campaign CH user-gate round 3 (2026-08-10): retail's own DoHelp
|
||||||
|
// fallback text is "Unknown command" (swept verbatim), not an
|
||||||
|
// acdream-invented "No help available" message. Retail types this
|
||||||
|
// 0x1A (ClientLocal / SpewBox-only); ChatVM has no SpewBox routing
|
||||||
|
// capability yet (ISSUES.md #367), so it still lands in the chat
|
||||||
|
// scroll here as ONE entry (no HelpPrefixNote wrapper — DoHelp's
|
||||||
|
// fallback bypasses the two-entry shape entirely).
|
||||||
var (vm, log, bus) = Fixture();
|
var (vm, log, bus) = Fixture();
|
||||||
|
|
||||||
var outcome = ChatCommandRouter.Submit("/help nonsenseverb", vm, bus, ChatChannelKind.Say);
|
var outcome = ChatCommandRouter.Submit("/help nonsenseverb", vm, bus, ChatChannelKind.Say);
|
||||||
|
|
||||||
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
||||||
Assert.Empty(bus.Published);
|
Assert.Empty(bus.Published);
|
||||||
Assert.Contains(log.Snapshot(), entry => entry.Text.Contains("No help available"));
|
var entry = Assert.Single(log.Snapshot());
|
||||||
|
Assert.Equal(RetailCommandHelpTable.UnknownCommand, entry.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HelpBare_ShowsRetailTwoEntryShape()
|
||||||
|
{
|
||||||
|
// Campaign CH user-gate round 3 (2026-08-10), finding (b): bare
|
||||||
|
// /help must emit retail's real two-entry sequence (Note, then the
|
||||||
|
// 13-item "Available help:" listing) — not the previous
|
||||||
|
// acdream-invented single-blob cheat sheet.
|
||||||
|
var (vm, log, bus) = Fixture();
|
||||||
|
|
||||||
|
var outcome = ChatCommandRouter.Submit("/help", vm, bus, ChatChannelKind.Say);
|
||||||
|
|
||||||
|
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
||||||
|
var entries = log.Snapshot();
|
||||||
|
Assert.Equal(2, entries.Length);
|
||||||
|
Assert.Equal(RetailCommandHelpTable.HelpPrefixNote, entries[0].Text);
|
||||||
|
Assert.Equal(RetailCommandHelpTable.AvailableHelpListing, entries[1].Text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,15 @@ public sealed class ChatPanelInputTests
|
||||||
public void Submit_HelpCommand_RendersLocalHelpAndDoesNotPublish()
|
public void Submit_HelpCommand_RendersLocalHelpAndDoesNotPublish()
|
||||||
{
|
{
|
||||||
// Phase J follow-up: client-side commands (/help, /?, /h) are
|
// Phase J follow-up: client-side commands (/help, /?, /h) are
|
||||||
// intercepted before the parser. They render a local cheat-sheet
|
// intercepted before the parser. They render local text via
|
||||||
// via ChatLog.OnSystemMessage and do NOT round-trip the server
|
// ChatLog.OnSystemMessage and do NOT round-trip the server — that's
|
||||||
// — that's what prevented the "Unknown command: help" duplicate
|
// what prevented the "Unknown command: help" duplicate ACE was
|
||||||
// ACE was firing back.
|
// firing back.
|
||||||
|
//
|
||||||
|
// Campaign CH user-gate round 3 (2026-08-10): retail's DoHelp
|
||||||
|
// prints via exactly TWO scroll entries (Note, then the 13-item
|
||||||
|
// "Available help:" listing), never one acdream-invented blob — see
|
||||||
|
// RetailCommandHelpTable's class remarks for the full trace.
|
||||||
var log = new ChatLog();
|
var log = new ChatLog();
|
||||||
var vm = new ChatVM(log);
|
var vm = new ChatVM(log);
|
||||||
var panel = new ChatPanel(vm);
|
var panel = new ChatPanel(vm);
|
||||||
|
|
@ -39,12 +44,11 @@ public sealed class ChatPanelInputTests
|
||||||
panel.Render(new PanelContext(0.016f, bus), renderer);
|
panel.Render(new PanelContext(0.016f, bus), renderer);
|
||||||
|
|
||||||
Assert.Empty(bus.Published);
|
Assert.Empty(bus.Published);
|
||||||
var entry = Assert.Single(log.Snapshot());
|
var entries = log.Snapshot();
|
||||||
Assert.Equal(ChatKind.System, entry.Kind);
|
Assert.Equal(2, entries.Length);
|
||||||
// Help text mentions / and @ equivalence and points at @acehelp
|
Assert.All(entries, entry => Assert.Equal(ChatKind.System, entry.Kind));
|
||||||
// for the server's full command list.
|
Assert.Equal(AcDream.UI.Abstractions.Panels.Chat.RetailCommandHelpTable.HelpPrefixNote, entries[0].Text);
|
||||||
Assert.Contains("/tell", entry.Text);
|
Assert.Equal(AcDream.UI.Abstractions.Panels.Chat.RetailCommandHelpTable.AvailableHelpListing, entries[1].Text);
|
||||||
Assert.Contains("@acehelp", entry.Text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -67,7 +71,7 @@ public sealed class ChatPanelInputTests
|
||||||
panel.Render(new PanelContext(0.016f, bus), renderer);
|
panel.Render(new PanelContext(0.016f, bus), renderer);
|
||||||
|
|
||||||
Assert.Empty(bus.Published);
|
Assert.Empty(bus.Published);
|
||||||
Assert.Single(log.Snapshot());
|
Assert.Equal(2, log.Snapshot().Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -165,4 +165,99 @@ public sealed class RetailCommandHelpTableTests
|
||||||
Assert.True(RetailCommandHelpTable.TryGetHelpText(verb, out string text));
|
Assert.True(RetailCommandHelpTable.TryGetHelpText(verb, out string text));
|
||||||
Assert.Equal(expected, text);
|
Assert.Equal(expected, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Campaign CH user-gate round 3 (2026-08-10) ──────────────────────
|
||||||
|
// The round 2 pass extracted the individual STRINGS byte-exact; this
|
||||||
|
// round traces DoHelp's complete PRINT SEQUENCE — see the class
|
||||||
|
// remarks. These pin the newly-swept literals byte-exact against
|
||||||
|
// tools/pdb-extract/sweep_weenie_strings.py's output.
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HelpPrefixNote_HasRetailsLeadingAndTrailingBlankLines()
|
||||||
|
{
|
||||||
|
// acclient_2013_pseudo_c.txt:394980 (data_0x7e11e0) -- the original
|
||||||
|
// round-2 extraction dropped the leading "\n" and the trailing
|
||||||
|
// "\n\n" that are part of retail's own literal.
|
||||||
|
Assert.Equal(
|
||||||
|
"\nNote: You may substitute a forward slash (/) for the at symbol (@).\n\n",
|
||||||
|
RetailCommandHelpTable.HelpPrefixNote);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ForMoreInformationPrefix_KeepsRetailsUnsubstitutedPlaceholderVerbatim()
|
||||||
|
{
|
||||||
|
// acclient_2013_pseudo_c.txt:395087 (data_0x7e1178) -- "<command>"
|
||||||
|
// is retail's own literal text, not a format placeholder acdream
|
||||||
|
// failed to substitute; the decomp shows no sprintf/substitution
|
||||||
|
// call between this literal and its use.
|
||||||
|
Assert.Equal(
|
||||||
|
"For more information, type @help <command>.\n",
|
||||||
|
RetailCommandHelpTable.ForMoreInformationPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UnknownCommand_MatchesRetailsExactFallbackText()
|
||||||
|
{
|
||||||
|
Assert.Equal("Unknown command", RetailCommandHelpTable.UnknownCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AvailableHelpListing_MatchesDoHelpsCompleteThirteenItemSequence()
|
||||||
|
{
|
||||||
|
// acclient_2013_pseudo_c.txt:395091-395249 (DoHelp bare-arg "else"
|
||||||
|
// branch), swept whole via sweep_weenie_strings.py --range
|
||||||
|
// 0x57f9e0 0x57fe7e --ascii-only against the PDB-paired
|
||||||
|
// C:\Users\erikn\Downloads\acclient.exe (verified MATCH). Exact
|
||||||
|
// source order: header, then allegiances/channels/chatting/death/
|
||||||
|
// emote/fillcomps/friends/house/squelch/status/text/commands.
|
||||||
|
Assert.Equal(
|
||||||
|
"Available help:\n"
|
||||||
|
+ "@help allegiances - Commands to help you deal with your Allegiance.\n"
|
||||||
|
+ "@help channels - How to communicate with people in your allegiance or fellowship.\n"
|
||||||
|
+ "@help chatting - How to chat publically and privately.\n"
|
||||||
|
+ "@help death - Commands for making, finding, and looting corpses.\n"
|
||||||
|
+ "@help emote - How to perform text and action emotes.\n"
|
||||||
|
+ "@help fillcomps - A command to help you buy components in bulk.\n"
|
||||||
|
+ "@help friends - Commands to help you manage your friends list.\n"
|
||||||
|
+ "@help house - Commands that help you manage your house, including guest and storage management.\n"
|
||||||
|
+ "@help squelch - Commands that let you block out messages from other players.\n"
|
||||||
|
+ "@help status - Commands that display useful information.\n"
|
||||||
|
+ "@help text - Commands that help you manage your text window.\n"
|
||||||
|
+ "@help commands - Lists all commands.\n",
|
||||||
|
RetailCommandHelpTable.AvailableHelpListing);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeathGroup_ThroughRouter_PrintsRetailsCompleteTwoEntryShape()
|
||||||
|
{
|
||||||
|
// Campaign CH user-gate round 3, finding (c): "/help death"'s
|
||||||
|
// CONTENT was already byte-exact (round 2); what was still wrong
|
||||||
|
// was the SHAPE. Retail's DoHelp prints two scroll entries for any
|
||||||
|
// resolved verb: HelpPrefixNote, then ForMoreInformationPrefix
|
||||||
|
// concatenated directly onto the verb's own Detail text (no blank
|
||||||
|
// line, no third entry). Exercised at the router level (not just
|
||||||
|
// the table) so this is the actual /help death user experience,
|
||||||
|
// not merely the table's stored string.
|
||||||
|
var log = new AcDream.Core.Chat.ChatLog();
|
||||||
|
var vm = new ChatVM(log, displayLimit: 50);
|
||||||
|
var bus = new RecordingCommandBus();
|
||||||
|
|
||||||
|
var outcome = ChatCommandRouter.Submit(
|
||||||
|
"/help death", vm, bus, ChatChannelKind.Say);
|
||||||
|
|
||||||
|
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
||||||
|
var entries = log.Snapshot();
|
||||||
|
Assert.Equal(2, entries.Length);
|
||||||
|
Assert.Equal(RetailCommandHelpTable.HelpPrefixNote, entries[0].Text);
|
||||||
|
Assert.Equal(
|
||||||
|
RetailCommandHelpTable.ForMoreInformationPrefix
|
||||||
|
+ RetailCommandHelpTable.DeathGroupDetail,
|
||||||
|
entries[1].Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RecordingCommandBus : ICommandBus
|
||||||
|
{
|
||||||
|
public List<object> Published { get; } = new();
|
||||||
|
public void Publish<T>(T command) where T : notnull => Published.Add(command);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue