Adds an optional, strict `characterOptions` block to the headless bot config (D8): keys are exactly the lane-B tier-1 (22) + tier-2 (4) bot-declarable CharacterOptionId enum-member spellings; an unknown/out-of-tier name fails config load naming the offending key, before it can ever reach the wire. HeadlessCharacterOptionsSeeder diffs declared-vs-actual once both of ACE's real preconditions are known true — GameActionLoginComplete sent (the FirstEnterWorldDone gate SetCharacterOptions 0x01A1 needs) and a real PlayerDescription has seeded RuntimeCharacterOptionsState (HasServerSeed) — learned from whichever of two hooks lands second. Every differing id routes through OP1's shared IRuntimeCharacterCommands seam: auto-save ids send SetSingleOption (0x0005) immediately; batched ids also call SetSingleOption (which only dirties the module) followed by exactly one SaveOptions flush after the whole declared set has been walked. Idempotent on reconnect by construction — no dedupe latch, the diff simply finds nothing once the server agrees. RuntimeLiveEntitySessionController gains a passive onLoginCompleteSent observation hook (additive only, never changes when/whether it sends) so the headless host can learn ACE's gate opened from any of its own two internal send sites; the third site (direct first-entry completion) is already owned by HeadlessSessionHost itself. All wiring is synchronous delegate calls on Runtime's one dedicated update thread — no new async/Task continuation, honoring #368. Tests: schema (valid parse, unknown/tier-3 name rejected naming the key, non-bool rejected, empty/absent no-op), the diff engine against a fake IRuntimeCharacterCommands (nothing-to-send, auto-save-only, batched-with- flush, mixed ordering, reconnect idempotence), and two wiring integration tests — one dispatching a real PlayerDescription game event end-to-end to a captured wire action, one proving the send lands on the same dedicated thread every Tick runs on. Full solution suite: 12,935 passed / 4 skipped / 0 failed (+17 over baseline 12,918/4/0). No register row: the characterOptions bot-config surface is acdream- native tooling over retail's own wire mechanisms (both already ported by OP1), not a retail UI port with a divergence to record. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
12 KiB
Campaign OP connected-gate test script
Status: OP3 and OP7 sections. Later slices (OP4-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
- 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).
- Press F11 again. The panel closes.
- 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. - Click the panel's own close (X) button, top-right of the window chrome. The panel closes — same action as F11.
- Drag the window by its border/chrome, resize it from any edge or corner. Behaves like the CH6 chat floaties: draggable, resizable on all four edges, remembers its geometry across a close/reopen within the same session.
Tab switching
- 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".
- Switch away from Gameplay and back. No crash, no stuck state.
The seven Gameplay-tab buttons
- 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).
- 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.
- 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.
- 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.
- 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). - 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. - Report Abuse. Click it. Same shape as Urgent Assistance, with its own retail text ("...to submit an abuse report...").
- 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). 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.
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).
- Any exception/crash on any of the 15 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).
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
- Confirm the character's starting state. Before running the bot,
note (or reset) the target character's
IgnoreAllegianceRequests(0x01, auto-save,SetSingleCharacterOption 0x0005) andListenToTradeChat(0x24, auto-save, also0x0005— additionally joins/leaves the Turbine trade room) options — e.g. via an existing graphical login, or by inspecting ACE's storedCharacterOptions1/CharacterOptions2for 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. - Author a headless config with all three declared at values that
DIFFER from the character's current stored state, e.g.:
(Flip each{ "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 } }] }truetofalseinstead if the character already has that bit set — the point is a genuine diff in both directions, not specifically "everything ON".) - Run it:
acdream-headless run --config op7-bot.jsonwithOP7_BOT_PASSWORDset 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)forIgnoreAllegianceRequests(id0x01) — immediately, no batching. - One
SetSingleCharacterOption (0x0005)forListenToTradeChat(id0x24) — 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). - One
SetSingleCharacterOption (0x0005)forSalvageMultiple(id0x22) followed by exactly oneSetCharacterOptions (0x01A1)blob flush —SalvageMultipleis batched (dirties the module; OP7's diff-and-send calls the explicitSaveOptionsverb once, after every declared id has been diffed, never interleaved). - No other option bits change. ACE's stored
CharacterOptions1/CharacterOptions2for 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 realPlayerDescriptionhas seeded local state (HeadlessCharacterOptionsSeeder's own two-precondition latch).
Expected silence on reconnect
- 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/0x01A1sends for the three declared ids — the freshPlayerDescriptionnow 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). - Flip one declared value in the config (e.g.
SalvageMultipleback tofalse) and run again. Expect exactly ONE0x0005+0x01A1pair for that id only — the other two, still matching, produce nothing.
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:9000settles this precisely if ACE's own log line is ambiguous). - Whether
ListenToTradeChatactually 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 batched0x0005+0x01A1); the remaining 23 names share the same two code paths and are already covered by the id-ascending completeness assertions inCharacterOptionTableTests(OP1) and the schema tests here (OP7). - Any presentation-only (tier-3) option —
HeadlessConfigurationLoaderrefuses to load a config that declares one; there is nothing to run. - The graphical Options panel's own Character tab — OP4.