acdream/docs/research/2026-08-11-campaign-op-test-script.md
Erik bc43fb1d1d fix(ui,runtime): OP4 review fixes — live re-seed, enable-gating, Combat panel re-point, universal timestamps
Both OP4 reviews converged on one headline bug (Character-tab rows never
re-read live server truth after their pre-login constructor-word seed) plus
overlapping MUST-FIXes. All ten converged/consolidated findings land here:

MUST-FIX:
- BoolOptionRow.SaveCurrentValue now re-reads its live binding (retail's
  GetValue()-into-SaveCurrentValue) on every OnShown — panel open, tab
  switch in, initial activation — instead of trusting the pre-login
  constructor word it was built with. Reset/tab-switch can now only
  restore values that were actually live at the last show. LockUI's
  host.Root.UiLocked one-shot mount seed now also converges on every
  PlayerDescription via the existing OnCharacterOptionsChanged hook.
- Apply/Reset are wired to OptionPage.OnOptionChanged in production
  (Ghosted when nothing changed, Normal when dirty, run once at bind so
  both start disabled per retail's PostInit); Defaults stays ungated.
- The Combat panel's three LEDs (Repeat Attacks/Auto Target/Keep in View)
  now read/write the same RuntimeCharacterOptionsState seam the Character
  tab uses instead of a disconnected client-local GameplaySettings copy —
  closes the "two writable copies" divergence. The three now-orphaned
  GameplaySettings fields and RuntimeSettingsController's mirror
  properties/SetCombatGameplay are deleted outright; the headless host's
  hardcoded AutoRepeatAttack/AutoTarget now read the live option bit.
- RuntimeSettingsController.SetUiLocked's convergence guard now compares
  against the last value actually applied to the runtime target instead
  of the persisted GameplaySettings.LockUI snapshot, which could already
  match a server-derived request without ever having been pushed.

SHOULD-FIX:
- DisplayTimeStamps now prefixes every chat producer (ChatLog.Append is
  the one seam all of them funnel through), not just AddText's own
  callers — heard speech, emotes, Turbine channels, and combat text were
  previously missed. The prefix format escapes its colons and forces
  InvariantCulture instead of the culture-dependent TimeSeparator
  placeholder.
- sky.frag now honors uFogParams.w (fog mode) like the mesh/terrain
  shaders, so Disable Distance Fog stops the sky dome's horizon band from
  blending toward fog color too.
- Corrected the "byte-verified" overclaim on the timestamp format string
  doc comment (BN-sourced, wire doc U6) and the AP-194 anchor-column
  class-name typo; the RunAsDefaultMovement doc comments now cite retail's
  actual acclient.h enumerator name.
- Added: DispatcherMovementInputSource's option x modifier truth table
  (incl. || AutoRunActive with the option off), the per-page Apply/Reset
  enable-gate tests, a real checkbox.OnClick/ToggleBehavior-driven click
  test, and hash-pins for the six header string keys.
- Gate script step 8 corrected for the logout-flush false-failure
  (closing the panel before relogging is load-bearing); a new step
  documents the enable-gate sequence and the Combat-panel/Character-tab
  cross-check.

Register: AP-196 (the Group-C default-source change + GameplaySettings
retirement) and AP-197 (the ignored per-character timestamp format
override) filed in this commit.

Full Release suite: 13,044 passed / 4 skipped / 0 failed (was 13,008/4/0;
net +36 tests from new coverage and legitimate assertion updates from the
GameplaySettings retirement).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 05:30:26 +02:00

516 lines
28 KiB
Markdown

# Campaign OP connected-gate test script
**Status:** OP3, OP4, and OP7 sections. Later slices (OP5-6, 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
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.
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.
---
## 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).