acdream/docs/research/2026-08-11-campaign-op-test-script.md
Erik f5ac1742ba feat(ui): Campaign OP slice OP6 — the Config tab
Binds the retail Options panel's Config tab (LayoutDesc 0x21000029, 27
authored rows across 6 sections) through OP2's template mechanism and
OP3's per-page OptionPage model, matching the Character/Chat tab
controllers' established pattern.

The row table is transcribed directly from two decompiled sources —
gmConfigUI::InitOptions @0x0049E400 (row order, widget shape, defaults)
and gmClient::InitUIPreferences @0x004035b0 (the complete
UIPreferences::AttachPreference registration: every label/tooltip key,
every slider's real-unit range, every menu's enum choices) — which
resolves the research docs' own "U4" unverified slider-caption pairing:
retail ships ZERO range captions on this tab (every SetSliderLabel call
passes literal string id 0).

Consumer disposition: LIVE — Sound/Ambient volume-trio sliders and their
toggle halves (AudioSettings.SfxDisabled/AmbientDisabled now gate the
already-live engine write; RuntimeSettingsController.SaveAudio newly
pushes into OpenAlAudioEngine on every change, not just at startup),
Resolution/Full Screen (immediate window resize on save). NEXT-LAUNCH
(pre-existing precedent): Sync To Refresh, Field of View. STORE-ONLY
(register rows AP-198/199/200, TS-74 extended): Sound Features/Interface
trio/Play-Only-When-Active, the nine Graphics/Rendering-Quality rows
(Vulkan has no per-feature render knobs), Camera/Input's six rows and
Use Mouse Turning (no persistent mouse-turning camera mode), Chat Font
Face/Size (distinct new fields from the existing live ChatSettings.FontSize).

AudioSettings/DisplaySettings/CameraTurningSettings/ChatSettings each
gain new fields for their slice of the 27 rows, backed by SettingsStore
round-trips. A real bug caught by testing: the scrollbar scope lookup
used the standalone-layout root id (0x100001FF), which does not survive
base-merge into the host-mounted tree — fixed to scope from the tab
host's own page-slot id (0x10000213), matching Chat's established
pattern for the same shared-scrollbar-id hazard (0x10000201, authored by
both the Chat and Config ListBoxes).

30 new tests (27 authored rows register as 30 IOptionRow instances — the
three toggle+slider trios each register two). Full Release suite:
13,107 passed / 4 skipped / 0 failed (was 13,083/4/0 — net +24, the one
existing RuntimeSettingsControllerTests case updated for SaveAudio's new
live-apply call, not a regression).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 07:16:35 +02:00

44 KiB
Raw Blame History

Campaign OP connected-gate test script

Status: OP3, OP4, OP5, OP6, and OP7 sections. Later slices (OP8) append their own sections here as they land; the campaign's OP9 closeout gate is this document complete plus every slice code-complete.

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

  1. 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".
  2. Switch away from Gameplay and back. No crash, no stuck state.

The seven Gameplay-tab buttons

  1. 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).
  2. 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.
  3. 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.
  4. 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.
  5. 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).
  6. 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.
  7. Report Abuse. Click it. Same shape as Urgent Assistance (including the same legibility check), with its own retail text ("...to submit an abuse report...").
  8. 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)

  1. 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.
  2. 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.
  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

  1. 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.
  2. 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.
  3. 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

  1. 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).
  2. 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

  1. 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).
  2. 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.
  3. 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).
  4. 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

  1. 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.
  2. 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.
  3. 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).
  4. "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

  1. **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).
  2. 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)

  1. 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)

  1. 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 clipping and no dead space below the last row (AP-195's self-sizing).
  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

  1. 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 registered window, e.g. the toolbar or a floating chat window, at its UNFOCUSED opacity). Watch that OTHER window's transparency change LIVE, continuously, as you drag — not just on release. This is retail's SetCurrentValue -> Apply(1) immediate-apply semantic.
  2. 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.
  3. 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.
  4. 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

  1. 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.
  2. 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).
  3. 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

  1. 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).
  2. 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).
  3. 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

  1. 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).
  2. 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 whose height looks clipped or has a large empty gap below its last row (a self-sizing regression).
  • 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; OP6 resolved it: retail ships zero range captions on this tab — do not expect "Dark/Bright"-style low/high labels on any slider).

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.

Live rows — audio

  1. 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.
  2. Check the "Disable Sound Effects" checkbox (the SAME row's toggle half). Sound effects should go SILENT immediately, regardless of where the slider is set. Uncheck it — sound effects resume at the slider's current level.
  3. Repeat steps 3-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-check behaviour.
  4. Note the retail-faithful surprise: on a FRESH character (never touched these settings), BOTH "Disable Sound Effects" and "Disable Ambient Sound" checkboxes are CHECKED by default — i.e., sound starts MUTED out of the box. This is retail's own byte-verified 2013 EoR default (gmConfigUI::InitOptions's SetDefaultValue(1, ...) on both trios), reproduced faithfully — not a bug, however counterintuitive. Uncheck both to hear anything.
  5. 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

  1. Open the Resolution menu (Graphics Options, first row) and pick a different resolution. The window should resize IMMEDIATELY, live, no restart needed.
  2. Toggle "Full Screen". The window should switch between windowed and fullscreen IMMEDIATELY, live.
  3. 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

  1. 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.
  2. 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.
  3. 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

  1. 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.
  2. Click Defaults. Every row jumps LIVE to retail's byte-verified default (Sound/Ambient trios back to muted+100% volume, Resolution to 800×600, Full Screen on, Camera Stiffness to 0.45, etc.) — Apply/Reset light up afterward if anything actually changed (Defaults never commits by itself).
  3. 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

  1. 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.
  2. 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 a slider showing a low/high range caption (retail ships none on this tab — see this section's own intro).
  • 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).
  • 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.:
    {
      "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

  1. 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).
  2. 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).