acdream/docs/research/2026-08-11-campaign-op-test-script.md
Erik 30fa6ee507 fix #394 #395 #396: OP8 re-gate round — caption font, retail key names, capture dialog
Three findings from the user's first Configure Keyboard look (OP8 gate,
2026-08-14), each root-caused against the named retail decomp:

- #394 row-caption font: the synthesized action-label UiText never set
  DatFont and fell to the debug bitmap font. The authored row template
  (0x21000009/0x1000002F, retail UIOption_ActionKeyMap) carries FontDid
  0x4000000A (18px serif) — Bind now takes resolveTemplateFont and applies
  the template's own authored font, resolved once per template pair.

- #395 key captions: raw enum spellings ("Shift+ShiftLeft") replaced by the
  port of CInputManager_WIN32::GetNameFromKey @0x00687F40 /
  GetNameFromKey_Internal @0x00687800 (RetailKeyNames): DAT string-table
  override by DIK-name hash (key enum 4 -> 0x2300000A, meta enum 5 ->
  0x2300000B, delimiter enum 3 -> 0x23000007 — GetDIDByEnum category 4,
  live-probed), else the OS keyboard layout's own key name ("SKIFT") via
  PlatformKeyNameProvider (Win32 GetKeyNameTextW — register row AD-96 for
  the DirectInput-vs-GetKeyNameText adaptation), else the DIK-suffix
  spelling. Bare modifier-key bindings show only the key name.

- #396 capture feedback: clicking a mapping button now opens retail's
  instruction dialog (InitiateBinding @0x004899D0 -> OpenMapWarnDialog
  @0x00488A00): a type-2 WAIT dialog on retail's MapWarn queue key
  0x10000001 with ID_ActionKeyMap_MapInstructions (0x23000004, ACTION
  variable interpolated), closed on key hit or ESC through the capture
  callback; capture is not armed if the dialog cannot open, matching
  retail. New RetailWaitDialogView (wait root 0x31 — same authored
  popup/message pair 0x3D/0x3E as the confirmation root, live-DAT probed)
  behind a shared IRetailDialogView presenter seam.

Probe evidence (env-gated, kept):
KeyboardConfigLiveMountProbeTests.ProbeKeyboardFontsAndKeyNameStrings.
Register: AD-96 filed. Gate script OP8 section updated (step 4 rewritten;
the "pressed/active state is enough" contract is retired).

Full Release solution suite green (13,424 passed / 4 skips).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 13:45:55 +02:00

1128 lines
63 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Campaign OP connected-gate test script
**Status: COMPLETE (2026-08-11) — the campaign is code-complete and this
script is its connected-gate contract.** §OP3§OP6 and §OP8 are the gates
the user still owes their eyes; §OP7's gate already PASSED (coordinator,
evidence in-section). Launch with `ACDREAM_RETAIL_UI=1` against live ACE.
Re-gate note for the previously-blank tabs: #372's viewport fix landed
(`057d8cd7`), and every wired Gameplay-tab button now logs
`[options] gameplay button 0x... clicked` — a silent click in that log IS
evidence, report it per-button.
**Dropdown appearance (fix #385, 2026-08-13 happy-testing round).** Every
Config-tab dropdown (Sound Features, Resolution, texture menus, Chat Font
Size, …) now matches the authored retail style: the button caption and the
popup rows draw WHITE and CENTERED (not gold/left), and the popup's height
conforms to its item count (a 3-choice menu opens 3 rows tall, a 10-mode
Resolution list opens 10 rows tall — retail's `RecalculatePopupSize`
size-to-content port; there is no 6-row scroll window on these menus any
more). A clicked dropdown with NO choices no longer opens an empty popup.
Report any dropdown still showing gold/left text or a fixed-height popup.
**Caption dimming (AD-78, user-directed, gate 2).** As of this session, every
row this script marks as a store-only row (see each section's own "Store-only
rows" subsection, or the register row AD-78 for the complete four-controller
enumeration) renders its caption in a dimmed neutral grey instead of the
normal white/DAT-authored color — the Character tab, the Config tab, and
Configure Keyboard's unmapped rows. A dimmed caption = stored (and
wire-synced where the row is auto-save) but drives nothing yet — this is
CONTRACTED, not a bug; do not report it. What IS worth reporting: a row this
script or AD-78 lists as LIVE that renders dimmed (a wiring regression), or a
row listed as store-only/dimmed that visibly DOES something (its "no
consumer" premise going stale without its caption catching up).
This document is the script the user runs against the live connected
client (`ACDREAM_LIVE=1` against the local ACE server) to accept each
slice. Each item states what to do and what retail-faithful behavior to
expect. Anything marked **INERT** is authored and clickable but
deliberately does nothing yet — that is the correct, contracted behavior
for this slice, not a bug.
---
## OP3 — panel shell, open paths, Gameplay tab
### Opening and closing the panel
1. **Press F11.** The Options panel opens — a floating, resizable window
titled by its own chrome (no title-bar text is authored; the window
itself is the visual identity). The **Gameplay Options** tab is
selected by default (leftmost tab, matching the user's own retail
screenshot).
2. **Press F11 again.** The panel closes.
3. **Click the toolbar's Options button** (the icon at the right end of
the shortcut row, next to Inventory). The panel opens. If a DIFFERENT
`gmPanelUI`-family panel was already open (Character Info, Skills,
Vitae, Inventory, etc.), opening Options closes it first — this is
retail's real "one active panel" behavior (`gmPanelUI`'s shared
geometry/exclusive-child model), not new to this panel. **Note the
button's STATE change, not just its behavior:** before this slice this
button was greyed out/disabled (panel id 10 was not in the catalog) and
clicking it did nothing. It should now be fully enabled, AND it should
highlight (same visual as the Inventory/Character/Magic buttons) while
the Options panel is open, un-highlighting when it closes.
4. **Click the panel's own close (X) button**, top-right of the window
chrome. The panel closes — same action as F11.
5. **Drag the window by its border/chrome; resize it from the BOTTOM
edge only.** Options shares its geometry policy with every other
`gmPanelUI` sibling (Character/Inventory/Spellbook/the indicator-detail
panels): draggable, resizable from the bottom edge only (NOT the top,
left, or right edges, and NOT a corner — the panel does not resize
horizontally at all), and it remembers its HEIGHT across a close/reopen
within the same session, same as every sibling. This is a correction
from OP3-as-landed, which briefly gave Options all-four-edge/horizontal
resize that no sibling has and that silently reverted to 300px wide the
next time a sibling panel was shown.
### Tab switching
6. **Click each of the four tabs** (Gameplay Options, Character, Chat,
Config). Each switches the visible page; exactly one page is visible
at a time. Character/Chat/Config show their AUTHORED content (row
templates, Apply/Reset/Defaults buttons, scrollbars) but nothing on
those three tabs is wired to live data yet — that is OP4/OP5/OP6's
scope, not a bug in this slice. Only the currently-selected tab's
button highlights as "open"; the others read "closed".
7. **Switch away from Gameplay and back.** No crash, no stuck state.
### The seven Gameplay-tab buttons
8. **Exit Game.** Click it. The client logs off and closes gracefully —
same behavior as pressing Escape then confirming, or closing the
window. **No confirmation dialog** (retail has none on this path).
9. **Exit to Character Selection.** Click it. A confirmation dialog
appears ("Are you sure you want to end this character session?" or
the DAT-resolved retail equivalent). Click **Yes** while standing on
solid ground: the client logs off and closes — **today this behaves
identically to Exit Game**, NOT a return to a character-select
screen (acdream has no pre-world character-select flow yet — register
row AD-74). Click **No**: nothing happens, panel stays open.
10. **Exit to Character Selection while airborne** (jump and click the
button mid-air, or confirm while still in the air). Expect the chat
line **"Cannot log off while in mid-air."** in the SpewBox/chat
scroll instead of logging off — this is retail's own byte-verified
refusal, faithfully ported.
11. **Configure Keyboard.** Click it. **INERT — nothing happens.** This
is the contracted behavior for OP3; OP8 wires the real Configure
Keyboard screen, and OP8's own gate re-tests this exact button. Do
not report this as a bug for OP3.
12. **In-Game Help Files.** Click it. **INERT — nothing happens.**
Retail's own help viewer is a third-party plugin
(`plugins\ACHelpPlugin.dll`) acdream does not have; retail itself
fails silently with the plugin absent, and this button mirrors that
(register row AD-76).
13. **Urgent Assistance.** Click it. Expect a chat line explaining the
web browser could not be launched and pointing at
`http://support.turbine.com/ics/support/ticketnewwizard.asp?style=classic`
— retail's own byte-verified failure text, always shown (the URL is
dead in 2026, so acdream never attempts to open a browser — register
row AD-75). **No native browser window opens, no OS dialog box.**
**Legibility check (blast review SHOULD-FIX 4):** this is a
THREE-LINE, ~110-character-URL body landing in the SpewBox overlay,
which auto-expires after 5 seconds. Read the whole body BEFORE it
disappears — if you cannot finish reading it (especially the URL) in
that window, report it as a legibility problem; the honest fix would
be routing it to the scrolling chat transcript (which stays and
scrolls back) instead of shortening the timeout.
14. **Report Abuse.** Click it. Same shape as Urgent Assistance
(including the same legibility check), with its own retail text
("...to submit an abuse report...").
15. **Use Mouse Turning Settings.** Click it. Expect one retail chat
line per changed value among: Camera Stiffness, Camera Adjustment,
Mouse Sensitivity, Align To Slope, Invert Mouselook Axes, Turn to
Face Camera — a FRESH character (never touched these settings) sees
all six lines; clicking the button a second time in the same
session sees zero lines (everything already at the macro's target).
**These six lines are byte-verified `RetailLogTextType.Magic` (0x07),
NOT the client-local refusal type** — expect LIGHT BLUE text in the
SCROLLING CHAT TRANSCRIPT (not the transient SpewBox overlay). Do
NOT expect a timestamp prefix: acdream renders no chat timestamps
yet (the Display Timestamps consumer is OP4 scope, and there is no
chat-log file — TS-69), so a bare light-blue line is CORRECT here
(OP3 re-review R1). If you see bright red text in a transient
5-second overlay instead (the ClientLocal/SpewBox look used by the
mid-air refusal and UA/RA above), that is the pre-fix-round bug —
report it.
**Known, registered gap (TS-74): acdream has no persistent
"turn to face camera" mouse-turning MODE yet, so this button changes
stored preferences and sends the wire bit, but you will not see the
camera actually start turning with mouse movement** — that consumer
does not exist yet. Verify the CHAT LINES and that the button is
clickable/does not crash; do not expect a camera-behavior change.
### Cross-session persistence (mechanism review S6)
16. **Relog the SAME character** (log off through Exit Game, or
disconnect/reconnect, then log back in) after step 15 has already
shown all six lines once. Open the Options panel and click "Use
Mouse Turning Settings" again. Expect **zero** chat lines this time —
`UseMouseTurning` is an auto-save id (`SetSingleCharacterOption
0x0005`), so the bit reached ACE on the first click and the fresh
post-relog `PlayerDescription` echoes it back as already-on. Seeing
the "Turn to Face Camera was changed..." line AGAIN after a clean
relog means the bit did not actually persist server-side — report it.
17. **Fully relaunch the client** (close the window, start a new
process) after step 15. Reopen the Options panel — no button click
needed. The five CLIENT-LOCAL preferences (Camera Stiffness,
Adjustment Speed, Mouse Sensitivity, Align To Slope, Invert Mouselook
Axes) persisted to `settings.json`'s `cameraTurning` section should
still read at the macro's target values from the PREVIOUS session —
click "Use Mouse Turning Settings" once more and confirm you again
see zero lines (proving the five client-local prefs, not just the
one server bit, survived the relaunch).
### What to report
- Any of the above NOT matching (wrong text, wrong tab default, window
not draggable/resizable, close button not working, F11/toolbar not
opening the panel, opening Options not closing a sibling panel, toolbar
button not highlighting while the panel is open).
- Any exception/crash on any of the 17 steps.
- Whether the confirmation dialog's exact wording looks retail-correct
(it resolves from the DAT string table at runtime; a fallback English
string only appears if that resolution fails, which would itself be
worth reporting).
### Explicitly NOT in scope for this gate
- Character/Chat/Config tab content (rows, checkboxes, sliders, live
data) — OP4/OP5/OP6.
- Configure Keyboard's actual screen — OP8.
- Any observable camera change from "Use Mouse Turning Settings" — no
acdream consumer exists yet (TS-74).
---
## OP4 — the Character tab
The Character tab (`0x21000028`) is the biggest single tab: 6 authored
group headers and 50 toggle rows (49 from the 2013 build + D3's "Listen to
PK death messages"), each bound by its retail `PlayerOption` id through
`CharacterOptionTable` and the shared `RuntimeCharacterOptionsState.
TrySetOption` seam every other Options-panel consumer already uses.
### Opening the tab and reading the rows
1. **Open the Options panel (F11) and click the Character tab.** Six
group headers appear top-to-bottom: **User Interface Behavior** (3
rows), **User Interface Display** (15 rows), **Grouping** (6 rows),
**Other Players** (11 rows), **Character Behavior** (7 rows), **Chat**
(8 rows — 7 from retail plus "Listen to PK death messages"). Every row
has a label; hover a few and confirm a tooltip appears. If ANY row
shows no label at all, that is a DAT string-resolution miss worth
reporting (the code deliberately renders nothing rather than invented
English when a string fails to resolve — a blank row is the correct
FAILURE shape, not a crash, but still worth flagging which row).
2. **Scroll the list** via the scrollbar. All 50+6 rows are reachable;
the list does not clip or overlap the Apply/Reset/Defaults buttons at
the bottom. **Gate-4 re-test note (#381):** the footer strip behind the
three buttons previously had NO backing — scrolled row text/content was
visible bleeding through between and behind Apply/Reset/Defaults. The
footer now has an opaque backing field; report if any content is still
visible through or around the three buttons (this applies identically
on the Chat and Config tabs' own footer strips, OP5/OP6 below).
3. **Confirm the checkboxes reflect your character's actual state**
e.g. if you have `IgnoreAllegianceRequests` on from a prior session,
its row should show checked on open (seeded from the live server bit
at panel-mount time, not always-off).
### Auto-save rows — immediate `0x0005`
4. **Toggle "Listen to General Chat" off, then on again**, without
clicking Apply. Confirm the effect is IMMEDIATE and matches the
existing Settings-panel Hear\*Chat behavior: turning it off silently
drops you from general chat (no more `/g` messages appear); turning it
back on restores them. This id is auto-save — the wire send happens on
the click itself, not on Apply.
5. **Toggle "Automatically Repeat Attacks" or "Accept Corpse Looting
Permissions"** (both auto-save). These previously flipped ONLY a
client-local flag and never reached the wire (an unfiled divergence
this slice closes) — verify the *behavior* actually changes in
combat/looting, not just the checkbox art.
6. **Relog the same character** after toggling a couple of auto-save
rows. Reopen the panel — the rows should still read whatever you left
them at (server echo), confirming the send actually reached ACE and
persisted, not just the local bit.
### Batched rows — the `0x01A1` blob via Apply
7. **Toggle a BATCHED row** (e.g. "Side By Side Vitals" or "Display Date
of Birth" — anything NOT in the auto-save set) and click **Apply**.
Relog and reopen the panel: the row should still read your new value
(Apply flushed the `0x01A1` blob, ACE persisted it, the fresh
`PlayerDescription` echoes it back).
8. **Close the Options panel (F11) FIRST, then toggle a batched row
WITHOUT clicking Apply**, then relog. The row should revert to its
PRE-toggle value on reopen — an un-flushed batched change never
reached the wire (the 480 s auto-save timer is a Runtime-level
mechanism tested at OP1; do not wait 8 minutes for this gate — just
don't click Apply). **Closing the panel first is load-bearing, not
optional**: logout flushes the dirty blob (`CPlayerSystem::
LogOffCharacter` calls `SaveToServer`), so "relog without Apply
reverts" only holds because reaching Exit Game requires switching to
the Gameplay tab first — retail's own `OnVisibilityChanged(false) ->
RestoreSavedValues` — which reverts the uncommitted edit BEFORE
logout's flush ever sees it. A user who force-quits the client (or any
route that skips hiding the Character page) will see the OPPOSITE:
the un-Applied toggle DOES persist, because logout still flushes
whatever is dirty at that moment. Both are retail-correct; this step
exercises the panel-close path specifically.
### Apply / Reset / Defaults semantics
9. **Toggle several rows (mix of auto-save and batched), then click
Reset.** Every row you touched reverts to its pre-edit value; rows you
didn't touch are unaffected. Auto-save rows you toggled will have
ALREADY sent their change on the click — Reset sends the REVERTED
value as a fresh change (a second `0x0005`), which is retail-correct
(Reset re-applies live, it does not "undo" a wire message that already
happened).
10. **Click Defaults.** Every row jumps to its retail default value
LIVE — auto-save rows whose default differs from current send
immediately; batched rows just dirty the module. Apply/Reset stay (or
become) ENABLED afterward if anything actually changed — Defaults
does NOT commit a new baseline by itself.
11. **After clicking Defaults, click Apply.** The defaulted batched rows'
values now flush via `0x01A1`; the new state becomes the baseline
(Reset can no longer undo past this point).
12. **Switch to another tab (e.g. Gameplay) without clicking Apply after
an edit.** Uncommitted edits silently revert — switching tabs is a
Reset, not a save. Switch back to Character and confirm the
reverted state.
### Group B consumers — presentation bindings
13. **Toggle "Display Timestamps" on, then send/receive a new chat
line** (e.g. `/who` or any inbound tell/say). The NEW line should be
prefixed with a timestamp like `14:32:07 ` (no leading zero on the
hour). Lines already in the transcript before you toggled the option
do NOT retroactively gain a timestamp. Toggle it back off — new lines
stop getting the prefix.
14. **Toggle "Disable Distance Fog" on** while outdoors somewhere with
visible atmospheric fog at a distance (dusk/dawn or an overcast
keyframe shows this most clearly). Distant terrain/objects should
stop fading into the fog color — toggle back off and the fog returns.
15. **Toggle "Run as Default Movement" off.** Press W (or your bound
forward-movement key) alone, with no modifier held: your character
should now WALK by default instead of run. Hold the walk-mode
modifier key while this option is off: it should now temporarily make
you RUN (the modifier always inverts whichever default is active).
Toggle the option back on and confirm W-alone runs again (today's
pre-OP4 behavior).
16. **"Display 3D Tooltips", "Side By Side Vitals", "Display Spell
Durations", "Advanced Combat Interface", "Stay in Chat Mode",
"Disable Most Weather Effects", "Always Daylight Outdoors", "Filter
Language", "Use Main Pack as Default for Picking Up Items" — verify
these are TOGGLEABLE and PERSIST (store+wire only, per the plan's own
contract), but do NOT expect any observable client behavior change
from them yet.** acdream has no existing consumer surface for these
(no side-by-side vitals layout, no 3D tooltip rendering, no discrete
weather-particle system to gate, no client-side pickup-container
preference, no profanity filter) — this is an honest, registered gap,
not a bug to report. "Always Daylight Outdoors" specifically does NOT
force daytime lighting yet; do not confuse it with the unrelated
weather-VARIETY forcing mechanism already in the codebase.
### Group C — re-pointed to server truth
17. **Toggle "Auto Target", "Automatically Repeat Attacks", "Keep Combat
Targets in View", "Vivid Targeting Indicator", "Show Coordinates By
the Radar", or "Accept Corpse Looting Permissions" and confirm the
EXISTING behavior for each (auto-targeting in combat, the combat
camera tracking your target, the vivid target overlay, radar
coordinate labels, corpse-loot consent) still works exactly as
before — these now read the SERVER bit instead of the local
`settings.json` copy, so the observable behavior should be unchanged
UNLESS your character's server-side value differs from whatever
`settings.json` had (in which case the NEW, server-driven behavior is
correct and the old local-only behavior was the bug).
18. **Relog and confirm all six Group-C options above read their
server-persisted value**, not a locally-cached default.
### Apply/Reset enable-gating (MUST-FIX 2, OP4 review-fix round)
19. **On first open of the Character tab, Apply and Reset are greyed out**
(disabled — retail's `PostInit` runs `OnOptionChanged(0)` so the pair
starts disabled). **Click any LED and they light up** (enabled).
**Click Apply and they grey out again.** **Defaults is never greyed
out** — before a change, immediately after clicking it, or after
Apply — at any point in this sequence.
### Combat panel — the SAME LEDs, a second surface (MUST-FIX 3 / blast M2)
20. **Open the Combat window (its own toolbar button, not the Options
panel) and confirm its own Repeat Attacks / Auto Target / Keep in
View checkboxes match the Character tab's rows for the SAME three
options** (Automatically Repeat Attacks / Auto Target / Keep Combat
Targets in View) — both surfaces now read the identical server bit,
so toggling one and reopening/refreshing the other must show the
SAME state. Before this fix round the Combat panel read a separate,
disconnected client-local copy that could silently disagree with the
Character tab and never reached the wire for two of the three.
### What to report
- Any row with a missing label/tooltip (note which one).
- Any auto-save row whose behavior doesn't persist across a relog.
- Any batched row that persists WITHOUT clicking Apply, or reverts
DESPITE clicking Apply.
- Any Reset/Defaults button that stays permanently disabled, or Defaults
that becomes disabled (it must never be).
- Any crash, freeze, or scroll glitch anywhere in the 56-row list.
- Timestamps, fog toggling, or run-as-default NOT working as described in
13-15.
- Any Group-C option (17-18) behaving DIFFERENTLY than it did before this
slice, other than "now reads the correct server value instead of a
stale local one."
### Explicitly NOT in scope for this gate
- Chat tab / Config tab content — OP5/OP6.
- Configure Keyboard — OP8.
- Any observable change from Group A rows (privacy flags, fellowship
grouping, missile/charge-attack options, helm/cloak visibility, and
similar) beyond the checkbox itself toggling and persisting — retail's
OWN client has no local consumer for most of these either; the server
is the only place their effect is visible (and largely invisible to the
player triggering them).
- "Always Daylight Outdoors" and "Disable Most Weather Effects" actually
changing the sky/weather — no acdream consumer exists yet (see item 16
and this slice's register rows).
- "Use Main Pack as Default for Picking Up Items" actually changing
pickup routing — no client-side preferred-container consumer exists.
- "Filter Language" actually filtering profanity — no filter subsystem
exists.
---
## OP5 — the Chat tab
The Chat tab (`0x2100005C`) has six sections: **General Options** (the two
linked window-opacity sliders) and five per-window text-filter blocks — the
**Main Chat Window** (12 checkboxes, no "Gameplay" row) and four **Floaty
Chat Window 1-4** blocks (13 checkboxes each). All five blocks write
`AcDream.Core.Chat.ChatWindowState` directly — the SAME state CH6's floating
chat windows already read when deciding which lines to show.
### Opening the tab
1. **Open the Options panel (F11) and click the Chat tab.** Six group
headers appear top-to-bottom: **General Options**, **Main Chat Window**,
**Floaty Chat Window 1**, **Floaty Chat Window 2**, **Floaty Chat Window
3**, **Floaty Chat Window 4**. Under General Options, two horizontal
sliders; the second one has "Transparent"/"Opaque" range captions at its
ends. Under each window section, a block of checkboxes with labels
(Gameplay, Combat, Magic, Area Speech, Tells, Allegiance, Fellowship,
General, Trade, LFG, Roleplay, Society, Error) — the Main Chat Window
block is missing the **Gameplay** row (12 rows, not 13); every floaty
block has all 13.
2. **Scroll the list.** All six sections are reachable; the block heights
visibly differ from a flat 100px box — each block is exactly tall enough
to show all of its own rows with no dead space below the last row
(AP-195's self-sizing). **Gate-3 fix (#371, AP-201 retired): blocks now
CLIP at the viewport edges instead of vanishing whole.** At the default
scroll position you should see the Main Chat Window block in full, then
"Chat Window 1"'s header and as many of its checkboxes as fit, cut off
cleanly at the panel's bottom edge — scrolling reveals the rest
smoothly. A block that disappears ENTIRELY while its header stays (the
pre-fix void your gate-3 screenshot review caught) is a regression —
report it. **Gate-4 re-test note (#381):** see the Character-tab (OP4)
step 2 note — the SAME opaque-footer-backing fix applies to this tab's
own Apply/Reset/Defaults strip.
3. **Look closely at a row whose mask covers MULTIPLE underlying message
types** (Gameplay, Combat, Allegiance, or Fellowship — the composite-mask
rows per the research doc) versus a single-bit row (e.g. "Error" or
"General"). If your character's chat history has caused a PARTIAL match
on one of those composite rows (some but not all of its underlying types
currently enabled), the checkbox's own LED art should look visibly
different (dimmer/"ghosted") from a row that is FULLY on — this is
AP-195's LED media swap. On a fresh character every row is likely either
fully on or fully off, so this may not be observable without deliberately
creating a partial-match state via repeated single clicks inside a
composite row's underlying bits (not directly clickable per-bit through
this UI — a visual nice-to-have to note, not a blocking check).
### Opacity sliders — live drag, linked, never clamping
**Gate-4 re-test note (#380):** both sliders previously showed ONLY the
Transparent/Opaque endpoint labels on the second (Active) slider, with NO
row caption identifying which slider was which. Each slider row now shows
its own DAT-resolved caption — "Inactive Opacity" next to the first slider,
"Active Opacity" next to the second — report if either caption is missing
or shows the wrong text.
**Gate-4 re-test note (#379) — step 4 below is REWRITTEN:** the sliders
previously faded EVERY registered window (vitals, toolbar, inventory,
spellbook, radar, the Options panel itself, ...), not just chat windows —
matching retail's own bug-for-bug scope. They now affect ONLY the main
chat window and the four floating chat windows, exactly like retail's
`ChatInterface::SetDefaultOpacity`/`SetActiveOpacity` (which only
`gmMainChatUI`/`gmFloatyChatUI` ever call). **Use a chat window (main or
floating), not the toolbar/vitals/another panel, as your "other window" in
step 4** — a non-chat window's opacity should now stay FIXED (opaque)
regardless of the slider position.
4. **Drag the FIRST slider (Default Opacity) most of the way to the right**
while the Options panel itself is NOT focused/hovered by your mouse (so
you can see another CHAT window — the main chat window or a floating
chat window 1-4 — at its UNFOCUSED opacity; a NON-chat window like the
toolbar or vitals bar must stay fully opaque throughout this whole
section, per the #379 note above). Watch that other CHAT window's
transparency change LIVE, continuously, as you drag — not just on
release. This is retail's `SetCurrentValue -> Apply(1)` immediate-apply
semantic.
5. **Drag the first slider (Default) ABOVE the second slider's (Active)
current position.** Retail's link drags the SECOND slider's thumb UP to
match — watch it move on its own as you drag the first. Neither slider
should ever let Default end up above Active.
6. **Drag the second slider (Active) BELOW the first slider's (Default)
current position.** The first slider's thumb should get dragged DOWN to
match, symmetric to step 5.
7. **Focus a chat window (click into its text entry) and watch its opacity
change to the Active value; click away and watch it ease back toward
Default.** (The easing itself is AP-190's existing behavior, not new to
this slice — this step just confirms the sliders are actually driving
the SAME live opacity controller every other window already uses.)
### Filter checkboxes — live routing, per-window independence
**Gate-4 re-test note (#382):** the main chat window's own four indicator
buttons (the small LEDs that light up per floating-window 1-4 open/closed
state) previously rendered NOTHING at rest — only a mouse hover revealed
their orange numbered art. They now show their correct art immediately on
window open, with no hover needed; report if any of the four is still
blank before you've moved your mouse over it.
8. **Open a floating chat window (Alt+1 through Alt+4, or the toolbar
indicator buttons) and uncheck "Combat" in that SAME window's filter
block on the Chat tab** (e.g. Floaty Chat Window 1's block if you opened
window 1). Trigger a combat message (attack a monster, or have someone
attack you) — it should STOP appearing in that floating window while
still appearing in the MAIN chat window (whose own filter is untouched).
Re-check the box — combat messages resume in that window.
9. **Uncheck a filter row in the MAIN CHAT WINDOW's own block** (e.g.
"Trade") and generate a matching message (a `/trade` broadcast, or
whatever is easiest to trigger). It should stop appearing in the main
window specifically, while a floating window with Trade still checked
keeps showing it — proving the main window's filter is genuinely
consulted now (CH6a/b's own fixed bug: broadcast lines used to
short-circuit true for the main window regardless of its filter).
10. **Verify the five blocks are independent**: toggling a row in Floaty
Chat Window 2's block must not affect Floaty Chat Window 1, 3, 4, or the
Main window's own same-named row.
### Apply/Reset/Defaults
11. **Toggle a couple of filter checkboxes AND drag both opacity sliders,
then click Reset.** Every row you touched (checkboxes, both sliders)
reverts to its value from the last Apply/OnShown — filter writes revert
live (you should see the reverted checkbox state immediately, and the
corresponding message routing revert too).
12. **Click Defaults.** The two opacity sliders jump to their DAT-extracted
values (0.5 default / 1.0 active — the same values you'd see on a
NEVER-touched install) LIVE; every filter block reverts to its OWN
window's retail default (Main: 0xFBFFFFFF's rows — everything checked
except Error; each floaty: its own narrower default set, per the
research doc's table) — Apply/Reset light up afterward if anything
actually changed (Defaults never commits by itself).
13. **Switch to another tab (e.g. Gameplay) without clicking Apply after an
edit.** Uncommitted slider/checkbox edits silently revert — switching
tabs is a Reset, not a save. Switch back to Chat and confirm the
reverted state (both the slider positions AND the filter checkboxes).
### Persistence — local-only, survives relog AND relaunch
14. **Set a distinctive combination** (e.g. Default Opacity ~30%, Active
Opacity ~90%, Main window's "LFG" row OFF, Floaty 3's "Fellowship" row
OFF) and click Apply (or just leave the panel open — filter writes and
slider drags apply live and persist to `settings.json` on every change,
not just on Apply).
15. **Fully relaunch the client** (close the window, start a new process;
a simple relog is not required since this is LOCAL-only state, unlike
the Character tab's server-authoritative rows). Reopen the Options
panel's Chat tab — the two sliders should read back at your set values,
and the Main/Floaty-3 checkboxes should read back unchecked for the
rows you turned off. **This is expected to be LOCAL-only persistence**:
retail's own wire mechanism for this data (the `0x1000008C`
GameplayOptions blob) is explicitly out of scope for this campaign
(register row AP-187, broadened at this slice — the blob stays
unsent); acdream persists through `settings.json` instead, which is
why a relaunch (not a server round trip) is the correct way to prove
it survived.
### What to report
- Any section/row/label missing, or the Main window showing a Gameplay row
(or a floaty missing one).
- A slider that does not apply live while dragging, or that lets Default
end up above Active (or vice versa) after a drag.
- A filter checkbox whose toggle does not change live message routing in
the window it belongs to, or that leaks into a DIFFERENT window.
- Reset/Defaults not behaving as described in 11-12.
- Any block that is STILL missing rows, or leaves a large empty gap below
its last row, once fully scrolled into view (a genuine self-sizing
regression). A block that disappears ENTIRELY at any scroll position is
ALSO a regression now — the gate-3 fix (#371) made straddling blocks
clip at the viewport edge instead of vanishing whole (see step 2).
- Any setting that reverts to default after a full relaunch (a persistence
regression) — remember this is local-only, so a SERVER-side relog is not
the right test here (see item 15's note).
### Explicitly NOT in scope for this gate
- Config tab content — OP6.
- The retail `0x1000008C` wire format for this same data — explicitly
deferred (see the plan's "What is explicitly OUT of scope" section and
this slice's own register citation).
- The composite-mask LED "partial vs full" visual distinction (step 3) is a
nice-to-have confirmation, not a blocking pass/fail item — it requires a
specific partial-match state that a fresh character is unlikely to be in.
---
## OP6 — the Config tab
The Config tab (`0x21000029`) has six sections: **Sound Options** (5 rows —
a menu, three toggle+slider volume trios, one toggle), **Camera Options** (4
rows), **Graphics Options** (7 rows), **Rendering Quality Options** (6
rows), **Input Options** (3 rows), **UI Options** (2 rows) — 27 authored
rows total, byte-verified directly from `gmConfigUI::InitOptions
@0x0049E400` and `gmClient::InitUIPreferences @0x004035b0` (the retail
registration table that supplies every row's label/tooltip/range/enum-
choice — a stronger source than this campaign's own research docs, which
had left the slider-caption pairing explicitly UNVERIFIED). **CORRECTED at
the OP6 rework round (2026-08-11, review M1):** the "zero range captions"
reading was a Binary Ninja constant-folding artifact — the raw bytes at
each `SetSliderLabel` call site are reads of runtime-filled string-id
globals, not immediate zeros. **Retail DOES ship six low/high range
captions**, one per labelled slider: Camera Stiffness (Soft/Hard), Camera
Adjustment Speed (Slow/Fast), Field Of View (Narrow/Wide), Screen
Brightness (Dark/Bright), Graphics Performance (Speed/Detail), Degrade
Distance (Close/Far). Mouse Look Sensitivity is the one genuine exception —
retail's own `InitOptions` never calls `SetSliderLabel` for it, so it alone
should show no caption.
### Opening the tab and reading the rows
1. **Open the Options panel (F11) and click the Config tab.** Six section
headers appear top-to-bottom: **Sound Options**, **Camera Options**,
**Graphics Options**, **Rendering Quality Options**, **Input Options**,
**UI Options**. Every row has a label (a menu dropdown, a checkbox, or a
slider — three of the Sound rows are a combined checkbox+slider in one
row). If ANY row shows no label at all, that is a DAT string-resolution
miss worth reporting (the code deliberately renders nothing rather than
invented English when a string fails to resolve).
2. **Scroll the list** via the scrollbar. All 27 rows are reachable; the
list does not clip or overlap the Apply/Reset/Defaults buttons at the
bottom. **Gate-4 re-test note (#381):** see the Character-tab (OP4)
step 2 note above — the SAME opaque-footer-backing fix applies here.
### Live rows — audio
3. **Drag the "Sound Effects" slider (first Sound Options trio) while a
sound effect is audibly looping or repeating** (e.g. stand near an
ambient sound source, or trigger a combat/UI sound repeatedly). The
volume should change LIVE, continuously, as you drag — not just on
release.
4. **CORRECTED at the OP6 rework round (2026-08-11, review M2) — the
checkbox's stored value is ENABLED-sense, not "Disabled" as the
rejected slice's own gate script previously described.** With the
first Sound-trio checkbox CHECKED (its default state — see step
6), sound effects are AUDIBLE. **Uncheck it** — sound effects go SILENT
immediately, regardless of where the slider is set. Check it again —
sound effects resume at the slider's current level. **Also report the
checkbox's DAT-resolved caption VERBATIM** (OP6 re-review SF-2: the
authored `ID_Sound_DisableSound` string was never read during research,
so this script does not know retail's exact English — if the caption
reads as a "Disable ..." phrasing while CHECKED means audible, that
label-vs-behavior tension is retail's own, byte-verified; report the
text so the script can record it, and gate on the BEHAVIOUR).
5. **Repeat step 4 for the "Ambient Sound" trio** (the second row) using
an ambient loop (wind, water, torches) as your audible test — same live
drag + mute-on-uncheck behaviour.
6. **Note the retail-faithful default:** on a FRESH character (never
touched these settings), BOTH "Disable Sound Effects" and "Disable
Ambient Sound" checkboxes are CHECKED by default
(`gmConfigUI::InitOptions`'s `SetDefaultValue(1, ...)` on both trios) —
and CHECKED means AUDIBLE, matching retail's own byte-verified
`SoundManager::effect_sounds_enabled`/`ambient_sounds_enabled` statics
(both compiled `= 1`). **Sound starts ON out of the box, not muted.**
If you hear silence on a fresh character, that IS a defect — report it.
7. **Drag the third slider / check its toggle ("Disable Interface Sound",
third trio).** Expect NO audible change either way — this is retail's
OWN dead knob (register row AP-174/AP-199): retail registers this
preference and then never reads it; UI/interface sounds are always
scaled by the Sound Effects slider instead. Confirm the row is
clickable/draggable and does not crash; do not expect it to do anything.
### Live rows — display
8. **Open the Resolution menu (Graphics Options, first row) and pick a
different resolution — IN WINDOWED MODE.** The window should resize
IMMEDIATELY, live, no restart needed. **Gate-2 re-test note (#374):**
the first gate's failure here was a client-wide dropdown routing bug —
clicks on an open popup's items landed on the rows UNDERNEATH it (your
session's Full Screen and VSync flips were those stolen clicks). Fixed;
dropdown items now win while a popup is open, and a click outside an
open popup dismisses it without acting on what's below. While
FULLSCREEN a resolution pick cannot switch the display mode yet
(Silk API limit, #376) — it applies on the next return to windowed;
do not report that as a step-8 failure. **Gate-4 re-test note (#378):**
the SAME row (and every other Config-tab dropdown — Sound Features, the
four texture-detail menus, the two Chat Font menus) previously rendered
as bare text with no button well/arrow and no visible popup at all
(#374's fix only corrected click ROUTING, not the missing chrome). Every
dropdown now shows the sunken value-well face + green arrow cap and
opens a real bordered, scrollable popup on click — report if ANY
Config-tab dropdown still renders bare text.
9. **Toggle "Full Screen".** The window should switch between windowed and
fullscreen IMMEDIATELY, live.
10. **Toggle "Sync To Refresh" (VSync) and drag the "Field of View"
slider.** Both persist to `settings.json`, but — matching this
project's pre-existing (not new) behaviour for these two fields —
neither re-applies until the NEXT LAUNCH. Do not expect an immediate
visual change; do a full relaunch afterward (step 15) to confirm the
new value took effect at startup.
### Store-only rows — no observable effect is the CONTRACTED behaviour
11. **Sound Features menu (Stereo/Mono), Play Sound Only When Active
toggle.** No consumer exists (register row AP-199). Confirm they're
clickable and persist (step 14); expect no audible/behavioural change.
12. **Screen Brightness slider, Automatic Degrades toggle, Graphics
Performance slider, Degrade Distance slider, the four Rendering
Quality menus (Landscape/Environment Texture Detail, Texture
Filtering, Landscape Draw Distance), Building Detail Textures,
Multi-Pass Alpha.** No consumer exists — acdream's Vulkan renderer is
driven by one aggregate quality preset, not these per-feature knobs
(register row AP-198). Confirm every one is clickable/draggable and
persists; expect zero visual change from any of them. **Landscape
Draw Distance specifically may show NO highlighted item** even right
after Defaults — that is retail's own decompiled data being
ambiguous (AP-198's own sub-note), not a rendering bug.
13. **Mouse Look Sensitivity slider, Invert Mouselook Y Axis toggle, Use
Mouse Turning toggle (Input Options), Camera Stiffness/Adjustment
Speed sliders, Align To Slope toggle (Camera Options), Chat Font
Face/Size menus (UI Options).** No consumer exists for any of these
six Camera/Input rows (register row TS-74 — the SAME "no persistent
mouse-turning camera mode" gap the Gameplay tab's macro already
exposed) or the two Chat font rows (register row AP-200 — distinct
fields from the chat panel's own separately-live font-size control,
wherever that is currently exposed). Confirm all eight are
clickable/draggable and persist; expect no visual/audible change.
### Apply/Reset/Defaults
14. **Toggle a mix of row types (a checkbox, a slider, a menu selection),
then click Reset.** Every row you touched reverts to its
last-shown/Applied value — live audio/display rows revert their
ACTUAL effect too (sound un-mutes or re-mutes, resolution changes
back), not just the checkbox art.
15. **Click Defaults.** Every row jumps LIVE to retail's byte-verified
default (Sound/Ambient trios back to AUDIBLE+100% volume — CORRECTED
at the OP6 rework round, review M2; the checked-by-default checkbox
means enabled, not muted — Resolution to 800×600, now a highlighted
dropdown entry since it is one of `DisplaySettings.AvailableResolutions`
(review S4), Full Screen on, Camera Stiffness to 0.45, etc.) —
Apply/Reset light up afterward if anything actually changed (Defaults
never commits by itself).
16. **Switch to another tab (e.g. Gameplay) without clicking Apply after
an edit.** Uncommitted edits silently revert — switching tabs is a
Reset, not a save.
### Persistence — local-only, survives relaunch
17. **Set a distinctive combination** (e.g. unmute Sound Effects at 40%
volume, pick a non-default Resolution, drag Field of View partway) —
every row here writes to `settings.json` on change, not just on
Apply.
18. **Fully relaunch the client** (close the window, start a new
process — a relog is not required, this is all local-only state).
Reopen the Options panel's Config tab — every row (live AND
store-only) should read back at your set values. The Sync To
Refresh/Field Of View rows deferred to next-launch (step 10) should
now ALSO be visibly applied (window vsync behaviour / camera FOV).
### What to report
- Any section/row/label missing, OR (CORRECTED at the OP6 rework round,
review M1) any of the SIX labelled sliders (Camera Stiffness/Adjustment
Speed, Field Of View, Screen Brightness, Graphics Performance, Degrade
Distance) MISSING its low/high range caption — retail ships one on each
of those six (see this section's own intro). Mouse Look Sensitivity is
the one slider that should show none.
- A live row (Sound/Ambient volume+mute, Resolution, Full Screen) that
does not change live, or a store-only row that unexpectedly DOES change
something (a sign its "no consumer" premise is stale).
- Sound effects or ambient audio SILENT on a freshly-created character
before you touch any Config-tab control (see step 6) — that is the M2
regression, not expected behaviour.
- Reset/Defaults not behaving as described in 14-15.
- Any row that fails to persist across a full relaunch (step 18),
including the two next-launch-only rows (Sync To Refresh, Field of
View) not taking effect at the NEXT startup.
- Any crash, freeze, or scroll glitch anywhere in the 27-row list.
### Explicitly NOT in scope for this gate
- Any audible/visual change from the store-only rows listed in steps
11-13 — no acdream consumer exists for any of them (register rows
AP-198, AP-199, AP-200, TS-74).
- The camera actually turning from "Use Mouse Turning" (Config tab OR the
Gameplay tab's macro) — no persistent mouse-turning camera mode exists
(TS-74, already covered by OP3's gate).
- Configure Keyboard — OP8.
---
## OP7 — headless `characterOptions`
Unlike OP3-OP6, this is not a graphical-client gate: no window is launched.
The coordinator runs `acdream-headless run` against local ACE with a config
declaring a small `characterOptions` block, and inspects the ACE-side
persisted `CharacterOptions1`/`CharacterOptions2` (and, for the
`ListenTo*Chat` ids, the actual Turbine room membership) before and after.
Automated coverage (schema rejection, the diff-and-send engine, the wiring
from a real `PlayerDescription` game event through to a captured wire
action, and dedicated-update-thread affinity) already runs in
`tests/AcDream.Headless.Tests/HeadlessConfigurationLoaderTests.cs`,
`HeadlessCharacterOptionsSeederTests.cs`, and
`HeadlessCharacterOptionsSeederWiringTests.cs` — this script is for the ONE
thing those tests cannot prove: that a REAL ACE server actually accepts and
persists the sends.
### Recipe
1. **Confirm the character's starting state.** Before running the bot,
note (or reset) the target character's `IgnoreAllegianceRequests`
(0x01, auto-save, `SetSingleCharacterOption 0x0005`) and
`ListenToTradeChat` (0x24, auto-save, also `0x0005` — additionally
joins/leaves the Turbine trade room) options — e.g. via an existing
graphical login, or by inspecting ACE's stored `CharacterOptions1`/
`CharacterOptions2` for the character row directly. Pick a THIRD,
batched (non-auto-save) id for the blob path — `SalvageMultiple`
(0x22, tier 2) is a safe choice: it has no observable server-side
effect beyond the stored bit, so a mismatch is purely a persistence
question, not a behavior one.
2. **Author a headless config** with all three declared at values that
DIFFER from the character's current stored state, e.g.:
```json
{
"version": 1,
"sessions": [{
"id": "op7-bot",
"endpoint": { "host": "127.0.0.1", "port": 9000 },
"account": "testaccount",
"character": { "name": "+Acdream" },
"policy": { "id": "idle" },
"credential": { "provider": "environment", "reference": "OP7_BOT_PASSWORD" },
"characterOptions": {
"IgnoreAllegianceRequests": true,
"ListenToTradeChat": true,
"SalvageMultiple": true
}
}]
}
```
(Flip each `true` to `false` instead if the character already has that
bit set — the point is a genuine diff in both directions, not
specifically "everything ON".)
3. **Run it**: `acdream-headless run --config op7-bot.json` with
`OP7_BOT_PASSWORD` set in the environment. Let it sit in-world a few
seconds, then stop it (Ctrl+C — the process scheduler's graceful-
shutdown path already sends a real logoff).
### Expected wire sends on first connect
- One `SetSingleCharacterOption (0x0005)` for `IgnoreAllegianceRequests`
(id `0x01`) — immediately, no batching.
- One `SetSingleCharacterOption (0x0005)` for `ListenToTradeChat` (id
`0x24`) — immediately, and ACE's handler additionally joins the
Turbine trade-chat room server-side for this session (observable via
ACE's own Turbine-chat membership logging if available).
- **No** `SetSingleCharacterOption (0x0005)` for `SalvageMultiple` (id
`0x22`) — it is batched, not auto-save, so `TrySetOption` only marks
the module dirty and never sends `0x0005` for it. The single
`SetCharacterOptions (0x01A1)` blob flush is the ONLY wire evidence of
this id changing (OP7's diff-and-send calls the explicit `SaveOptions`
verb once, after every declared id has been diffed, never interleaved
— so this same `0x01A1` also carries whatever the loop already wrote
locally for `IgnoreAllegianceRequests`/`ListenToTradeChat`, even though
those two ALSO got their own immediate `0x0005`).
- **No other option bits change.** ACE's stored `CharacterOptions1`/
`CharacterOptions2` for every UNDECLARED id stay exactly what they
were before the run.
- **No 0x01A1 blob before the client's own `GameActionLoginComplete`.**
If ACE logs a refusal ("SetCharacterOptions received before
FirstEnterWorldDone" or equivalent), that is a real OP7 defect — the
diff-and-send is contracted to run only after both LoginComplete has
been sent AND a real `PlayerDescription` has seeded local state
(`HeadlessCharacterOptionsSeeder`'s own two-precondition latch).
### Expected silence on reconnect
4. **Run the SAME config a second time** (a fresh process, or the
process's own reconnect path if it fires) without changing anything
on the ACE side in between. Expect **zero** `0x0005`/`0x01A1` sends
for the three declared ids — the fresh `PlayerDescription` now echoes
back exactly what the first run persisted, so the declared-vs-actual
diff finds nothing (idempotent by construction: a bot that already
has what it wants sends nothing, matching retail's own "re-set to the
current value produces nothing" rule — set-character-options-wire.md
§3.1/§3.5).
5. **Flip one declared value** in the config (e.g. `SalvageMultiple` back
to `false`) and run again. `SalvageMultiple` is batched, so expect
exactly ONE `0x01A1` blob flush and **no** `0x0005` for it — the other
two declared ids, still matching, produce nothing at all (not even the
blob).
### What to report
- Any declared id that does NOT persist across a fresh reconnect (ACE
either rejected it silently, or the diff-and-send never actually ran).
- Any UNDECLARED id whose stored value changed — a sign the blob echoed
something it shouldn't have (wire research §5.3's "echo real values,
never zero them" rule, or a stale/incorrect snapshot read).
- Any 0x0005/0x01A1 send observed BEFORE the client's own LoginComplete
action on the wire (a WireMCP capture on loopback `127.0.0.1:9000`
settles this precisely if ACE's own log line is ambiguous).
- Whether `ListenToTradeChat` actually joined the Turbine trade room
server-side (not just the stored bit) — the one declared id in the
sample config with a real behavioral consumer.
### Explicitly NOT in scope for this gate
- Every other tier-1/tier-2 option name — the three above exercise both
wire paths (auto-save `0x0005`-only and batched `0x0005`+`0x01A1`);
the remaining 23 names share the same two code paths and are already
covered by the id-ascending completeness assertions in
`CharacterOptionTableTests` (OP1) and the schema tests here (OP7).
- Any presentation-only (tier-3) option — `HeadlessConfigurationLoader`
refuses to load a config that declares one; there is nothing to run.
- The graphical Options panel's own Character tab — OP4.
### OP7 gate result — PASSED (coordinator, 2026-08-11)
Three live runs against local ACE (`127.0.0.1:9000`, `+Acdream`,
`lifecycle-smoke` policy, temporary seeder diagnostics since reverted):
- **Run 1 (declared: IgnoreAllegianceRequests=true, ListenToTradeChat=true,
SalvageMultiple=true):** the seeder sent exactly two `SetSingleOption`
diffs (`IgnoreAllegianceRequests` false→true auto-save;
`SalvageMultiple` false→true batched) plus ONE `SaveOptions (0x01A1)`
flush; `ListenToTradeChat` was already true and sent nothing. After the
policy's own portal AND a genuine mid-run reconnect (fresh ACE
`PlayerDescription`), all three reported `already ..., no send` —
proving ACE accepted and persisted both wire paths within the session.
- **Run 3 (fresh process, SalvageMultiple flipped to false):** the fresh
seed echoed `SalvageMultiple=true` from run 1 — a value ONLY the run-1
`0x01A1` blob could have carried (batched id, never `0x0005`), which is
the cross-process ACE-persistence proof. The flip sent one
`SetSingleOption` + one `SaveOptions` and nothing for the other two
ids; post-portal and post-reconnect re-diffs were silent.
- Both runs: no pre-LoginComplete sends observed, no ACE refusals,
graceful zero-code exits, `disposed` with converged runtime and zero
leases. Pre-existing noise unrelated to OP7, noted for the record: an
unmapped `[weenie-error] code=0x051D`, and the K4 resource envelope's
`handle-count` violation (the `k4-linux-30-session` Linux profile
evaluated on Windows).
---
## OP8 — Configure Keyboard
> **Gate-2 re-test note (#375):** the first look at this screen was a
> visual mess — textless buttons/tabs, stray buttons above the window,
> overlapping text. Two root causes, both fixed: the screen's build was
> missing its string resolver (every authored caption rendered empty),
> and the row-template prototypes (a header + a three-button row) were
> being built as live widgets parked at the screen's top-left. Expect
> now: captioned tabs (Movement/Camera/Combat/UI/CharacterSettings/
> Emotes), captioned buttons (Defaults/Revert/OK/Cancel, Load File.../
> Save As...), Command + Mapping 1-3 column headers, and NOTHING
> rendered above or outside the framed panel.
> **Re-gate note (2026-08-14 fix round — #394/#395/#396):** three findings
> from the first OP8 look at this screen, all fixed:
> 1. **Row-caption font (#394):** the action labels ("Move Forward", …)
> now draw in the row template's authored 18px serif (FontDid
> `0x4000000A`), not the debug bitmap font. The gothic section headers
> and the key-button captions were already using their authored fonts.
> 2. **Key caption text (#395):** key buttons now show retail's localized
> key names — DAT overrides first ("Left Ctrl"/"Left Alt" are the only
> authored ones), then YOUR keyboard layout's own name (a Swedish
> layout shows "SKIFT" for the Shift key, exactly like your retail
> screenshot), modifiers joined with "+" (chords like "SKIFT+M"), and a
> binding to a bare modifier key shows just the key name — never
> "Shift+ShiftLeft". Mouse chords keep enum spellings (AD-96).
> 3. **Capture-instruction dialog (#396):** clicking a mapping button now
> opens retail's own instruction dialog ("The next key you press or
> mouse button that you click will be mapped to the '<action>'
> action. … Press the ESC key to cancel.") — the wait-dialog shape from
> your retail screenshot, with the row's action name interpolated. It
> closes when you press the new key OR press ESC. Step 4 below is
> REWRITTEN accordingly; the old "a pressed/active state is enough"
> contract is retired.
**Known, tracked behaviors — do NOT file as defects (read before testing):**
- **Shared combat keys prompt a false conflict (ISSUES #373).** Retail
legitimately shares Insert/Delete/End/PageUp/PageDown across the three
combat contexts (Melee/Missile/Magic) via the DAT's `ConflictingMaps`
table, which acdream does not read yet. Rebinding any of those keys — or
any chord one combat context already holds — will show a conflict
confirmation retail would not. DECLINE the prompt and move on; the
binding you declined stays untouched. Report only if declining CHANGES a
binding anyway.
- **Camera Alternate Controls rows are display/store-only (AP-203).** They
show retail's authored arrow-key defaults and accept edits, but do not
drive the camera — only the primary Camera scheme is live. Verify they
RENDER (arrow keys visible); do not expect camera behavior from them.
The screen is retail's own separate full-screen window (`gmKeyboardUI`,
LayoutDesc `0x21000009`), NOT a fifth tab of the Options panel. It opens
from the Gameplay tab's Configure Keyboard button — **the OP3 INERT
contract for that button is retired as of this slice**; OP3's own script
line calling it INERT no longer applies.
### Opening the screen
1. **Open the Options panel (F11), Gameplay tab, click "Configure
Keyboard."** A separate full-screen window opens (not layered inside
the Options panel) showing six tabs across the top: Movement, Camera,
Combat, UI, CharacterSettings, Emote — Movement is the default/first
tab. Each tab lists grouped rows: a bold-ish header naming the
sub-category (e.g. "Movement Commands"), then one row per action —
an action label on the left, up to three key-binding buttons to its
right (retail's "Mapping 1/2/3" columns) showing the currently bound
key(s) (e.g. "W", "Up").
2. **Not every group has a visible header/rows.** Debug/dialog/system
InputMaps (DialogBoxes, DebugConsole, ProfilerUI, UIDebugger,
DebugCommands, and six further unnamed contexts) are 100% non-user-
bindable in the shipped DAT and correctly show NOTHING — this is not
a bug, it is retail's own shipped data (byte-verified against the
live DAT, see `RetailActionMap.cs`'s class doc).
3. **The bottom-row buttons:** Load File…, a current-keymap-name label,
Save As…, Defaults, Revert, OK, Cancel. Load File / Save As are
**INERT** (D4 — no `.keymap` file interchange, AP-202) — clicking
them does nothing; this is correct, contracted behavior.
### Rebind a movement key live
4. **On the Movement tab, find "Move Forward"** (should show two bound
keys — "W" and your layout's name for the up-arrow key). Click the
SECOND key button. **Retail's capture-instruction dialog opens**
(#396 re-gate): "The next key you press or mouse button that you
click will be mapped to the 'Move Forward' action. …Press the ESC
key to cancel." — naming THIS row's action. Press ESC once first:
the dialog closes and nothing changes. Click the button again to
re-open it for step 5.
5. **Press a different key**, e.g. `U`. The button should immediately
update to show "U".
6. **Move your character forward using W and U** (both should now work
— this is the live-effect proof: the rebind reached the SAME
`InputDispatcher`/`KeyBindings` every other input path uses, not a
screen-local shadow copy). The original "Up" arrow should no longer
move the character forward.
7. **Right-click the "U" key button** you just set. It should clear
back to blank/empty — right-click erases just that one slot
(`EraseBinding`), distinct from a hypothetical "Clear all" (there is
no Clear-all button on this screen — the shipped 2013 row template
authors none; a real, DAT-verified fact, not a limitation of this
port).
### Conflict on a taken chord
8. **Still on Movement, click "Move Backward"'s first key button**
(currently "X"), then press **W** — the SAME key you just confirmed
is bound to "Move Forward."
9. **Expect**: a confirmation dialog opens (retail's own
`OpenOverwriteBindingDialog`, ported through the same
`RetailDialogFactory` confirm mechanism the game's other Yes/No
prompts already use) naming "Move Forward" as the row that currently
holds the key and asking whether to reassign it to "Move Backward."
**Click Yes**: "Move Backward" takes W, and "Move Forward" loses its
W slot (down to just "U" from step 5); both rows' key-button labels
update to reflect the swap. Repeat steps 8-9 once more but **click
No** this time: neither row should change at all — the capture is
simply abandoned, exactly like Escape.
### Non-bindable refusal
10. **Click any key button, then press Ctrl+M** (acdream's own
debug-only mute toggle — retail has no equivalent, so no DAT row on
this screen owns it). **Expect**: the rebind is REFUSED — the key
button you clicked keeps its previous value, and a system message
appears reading "Could not overwrite " (retail's own byte-verified
`ID_KeyMapCantOverwriteReadOnlyKeymap_Label` string, table
`0x23000004` — it may read as an odd sentence fragment on its own;
that is the literal stored string, not a truncation bug in this
port). Confirm Ctrl+M still mutes/unmutes audio afterward — the
acdream-only binding was NOT touched.
### Erase, then Reset to Defaults
11. **Erase a couple more bindings** via right-click (any tab).
12. **Click Defaults.** Every row on EVERY tab should snap back to its
retail-default key(s) LIVE — including the ones you just erased,
the "Move Forward"/"Move Backward" pair from steps 5-9 (both back
to their original W/Up and X/Down), and the Ctrl+M row from step 10
is untouched (it has no DAT row, so Defaults cannot and does not
touch it). The OK/Cancel/Revert buttons should now read as
"changed" (retail never gates Defaults itself, but the page as a
whole is dirty after it runs) — confirm by tabbing away and back:
edits should still be there (Defaults applies live without
committing, exactly like a manual edit would).
13. **Click Revert** (not Cancel) with SOME rows still showing your
Defaults-restored values. Expect every changed row to revert to
whatever was bound when you last clicked OK (or, if you haven't
clicked OK yet this session, back to what was loaded from
`keybinds.json` at startup) — Revert and Cancel run the identical
verb (`RestoreSavedValues`); Revert just doesn't also close the
window.
### Persistence across relaunch
14. **Rebind one distinctive key** (e.g. change "Jump" from Space to
some other free key) and **click OK.** The window closes.
15. **Close acdream gracefully and relaunch it** (see CLAUDE.md's
logout-before-reconnect discipline — wait for the graceful-close
session-clear window before reconnecting). Open Configure Keyboard
again: the rebind from step 14 should still be there — proof it
persisted to `keybinds.json` on disk, not just the in-memory
dispatcher.
16. **Open a plain text editor on `%LOCALAPPDATA%\acdream\keybinds.json`**
(or the platform-portable equivalent) and confirm the rebound
action shows the new key. A sibling `keybinds-unmapped.json` should
also exist if you rebound anything on the CharacterSettings or
Emote tabs during this session (AP-203's store-only rows — e.g. try
rebinding one "Bow Deep"-style Emote row and confirm it shows up in
THAT file, not `keybinds.json`).
### Cancel discards uncommitted edits
17. **Rebind another key WITHOUT clicking OK**, then click **Cancel.**
The window closes; reopen it — the rebind from this step should be
GONE (reverted to the last-committed/loaded state), matching step
13's Revert behavior plus the window closing.
### What to report
- Any row that shows a DIFFERENT key than what `keybinds.json` /
`RetailDefaults()` says it should (a sign the DAT-vs-InputAction
identity table mis-mapped a row — see
`RetailActionIdentityRoundTripTests` for the automated half of this
check).
- Any tab/header that renders EMPTY where it should show rows, or vice
versa (the page-scoped element-lookup trap this slice's controller
explicitly guards against — a regression here would mean one page's
rows leaked into another, or the six reused element ids resolved to
the wrong page's instance).
- Whether the confirm dialog's wording (step 9) or the refusal
message's odd phrasing (step 10, the literal DAT string) reads
awkwardly enough in practice to warrant a follow-up polish pass.
- Any row whose Emote/CharacterSettings binding visibly DOES something
in-game despite AP-203 saying it shouldn't (would mean acdream grew a
consumer for it since this table was written, and the identity table
should be updated to route it live).