acdream/docs/research/2026-08-11-campaign-op-test-script.md
Erik 09cb548a32 feat(headless): Campaign OP slice OP7 — declared characterOptions with seed-diff sends
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>
2026-08-11 02:45:08 +02:00

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

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

  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.
  7. Report Abuse. Click it. Same shape as Urgent Assistance, 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). 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

  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).
  • One SetSingleCharacterOption (0x0005) for SalvageMultiple (id 0x22) followed by exactly one SetCharacterOptions (0x01A1) blob flush — SalvageMultiple is batched (dirties the module; OP7's diff-and-send calls the explicit SaveOptions verb once, after every declared id has been diffed, never interleaved).
  • 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. Expect exactly ONE 0x0005 + 0x01A1 pair 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: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.