acdream/docs/research/2026-08-10-character-options-map.md
Erik cf6ef8b4b4 docs: Campaign OP research — character option map (lane B) + 0x01A1 wire (lane C)
Two of the four Opus research lanes for the retail Options panel campaign
(docs/research/2026-08-10-settings-track-handoff.md):

- Lane B: the complete Character-tab option map. 50 rows / 6 groups
  decomp-authored from gmCharacterSettingsUI::InitOptions @0x004a02f0
  (the screenshots' PK-death row is 2015-client-only; the 2013 enum caps
  at 0x33). Wire routing is retail's byte-verified lookup table
  CPlayerModule::IsAutoSaveOption @0x0059a600 — ~21 ids send 0x0005
  immediately, the rest dirty the module for the batched 0x01A1. Retail's
  Defaults-button table reconstructs Options1 = 0x50C4A54A exactly
  (independent confirmation of ACE's constant) and Options2 = 0x00008700
  vs ACE creation's 0x00948700 (a real client-vs-server distinction, not
  a bug). Per-option ACE handling + acdream consumer inventory included.

- Lane C: the real 0x01A1 body is PlayerModule::Pack @0x005D45C0
  (builder CM_Character::Event_CharacterOptionsEvent @0x006A10C0), flag
  enum PlayerModulePackHeader verbatim at acclient.h:7835;
  SetPackHeader @0x005D44A0 always sets 0x460 and never 0x02/0x04/0x10/
  0x80, so ACE's extra reader branches are dead legacy. Flush triggers:
  Apply, logout, 480 s autosave. ACE stores options words raw, discards
  the rest, refuses only pre-LoginComplete; unknown option ids THROW.
  CH3 post-mortem: the deleted 16-byte builder put a CharacterOptions1
  word in the section-flag slot.

Both lanes independently converged on the same latent defect: the
headless DirectGameRuntimeCommandAdapter.SetSingleOption sends the wire
but skips the local Options.SetOptionBit write the graphical path does
(LiveSessionRuntimeFactory.cs:348) — the CH4 stale-membership-gate bug
class reproduced on the bot side. Flagged for the campaign plan, not
fixed here (research-only lanes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 22:37:46 +02:00

41 KiB
Raw Permalink Blame History

Character-tab option map — settings-track research lane B

Campaign: settings track (retail four-tab Options panel). Handoff: docs/research/2026-08-10-settings-track-handoff.md. Lane: B — handoff research questions Q2 (complete Character-tab option map), Q7 (which options have live acdream consumers), and the bot-relevance half of Q8. Date: 2026-08-10. Mode: research only — no code changed, nothing built, nothing launched.

Sources. docs/research/named-retail/acclient.h (verbatim retail PlayerOption enum + PlayerModule/UIOption_Checkbox structs), docs/research/named-retail/acclient_2013_pseudo_c.txt (BN pseudo-C), the PDB-paired binary at C:\Users\erikn\Downloads\acclient.exe (check_exe_pdb.py=== MATCH ===, GUID 9e847e2f-777c-4bd9-886c-22256bb87f32, linker 2013-09-06T00:17:56Z), vendored ACE at references/ACE/, and acdream src/ in this worktree.


0. Verdict summary

Question Answer
Do we have the complete Character-tab row list? Yes, and it is decomp-authored, not inferred. gmCharacterSettingsUI::InitOptions @0x004a02f0 emits exactly 50 toggle rows in 6 header groups, in an order that matches the user's three screenshots row-for-row.
Is ACE's option-id → bit map right? Yes, byte-verified. The client's own PlayerModule::GetOption @0x005d3aa0 switch assigns the same bit to all 52 options present in the 2013 build. CharacterOptions1.Default = 0x50C4A54A also reconstructs exactly from retail's own per-option default table (§1.4).
0x0005 vs 0x01A1 — what decides? CPlayerModule::IsAutoSaveOption @0x0059a600, a 0x34-byte lookup table (byte-verified, §1.3). 21 of 53 options send SetSingleCharacterOption (0x0005) immediately; the other 32 only mark the module dirty and ride the batched SetCharacterOptions (0x01A1) blob 480 s later or at forced save.
Does the Apply button send the blob? Not necessarily. Each checkbox applies through PlayerModule::SetOption the moment Apply runs; auto-save ids leave as 0x0005 and never enter the blob. ACE's own header comment in GameActionSetCharacterOptions.cs:11-16 independently describes this behaviour (and its stated rule — "options with a value set in the enum" — is wrong; all 53 have values. The real rule is the IsAutoSaveOption table).
Does acdream track these today? Two bits are live; everything else is either a local settings.json bool the server never sees, or absent. 6 Hear*Chat bits drive TurbineChatMembershipGate; DragItemOnPlayerOpensSecureTrade drives item interaction. That is the whole list.
Is there a shared Runtime seam for bots? Yes, it already exists: IRuntimeCharacterCommands.SetSingleOption (src/AcDream.Runtime/GameRuntimeCommands.cs:249), implemented by both hosts. It has one defect (§4.4): the headless implementation skips the local option-bit write the graphical one does.
Does ACE reject option changes from a live session? 0x0005: no gate at all. 0x01A1: refused before FirstEnterWorldDone. But an unknown option id makes ACE throw (§5.3) — a real bot-safety constraint.

Two divergences worth naming immediately, because they are behavioural and currently unfiled:

  1. /acceptcorpselooting toggles a local bool the server never sees. ClientCommandController.cs:444-450 flips GameplaySettings.AcceptLootPermits only. Retail sends 0x0005 (0x10) immediately (auto-save), and ACE honours the bit at Player_Death.cs:755. Today the corpse permission is a lie.
  2. AutoRepeatAttack is client-local in acdream but server-authoritative in ACE. LiveCombatAttackOperations.cs:95,169 reads the local setting; ACE independently re-attacks based on its own stored bit (Player_Melee.cs:375, Player_Missile.cs:284). The two can disagree.

1. The retail mechanism (settled, byte-verified)

1.1 Storage

PlayerModule (acclient.h:36507) holds the two option words plus the non-boolean option payloads:

struct __cppobj PlayerModule : PackObj {
  ShortCutManager *shortcuts_;
  PackableList<unsigned long> favorite_spells_[8];
  PackableHashTable<IDClass<_tagDataID,32,0>,long> *desired_comps_;
  unsigned int options_;                 // CharacterOptions1
  unsigned int options2_;                // CharacterOptions2
  unsigned int spell_filters_;
  GenericQualitiesData *m_pPlayerOptionsData;
  PackObjPropertyCollection m_colGameplayOptions;
  AC1Legacy::PStringBase<char> m_TimeStampFormat;
};

The linear id space is enum PlayerOption (acclient.h:4162-4217), 0x00..0x33, terminated by TotalNumberOfPlayerOptions_PlayerOption = 0x34. The 2013 build has no 0x34. ACE's ListenToPKDeathMessages = 0x34 is a post-PDB addition (ACE says so itself at PlayerFactory.cs:659"possibly was added to Defaults post PDB we have").

1.2 Two kinds of option row — the tab discriminator

UIOption_Checkbox (acclient.h:6108) carries both a PlayerOption m_playerOption and a PStringBase<char> m_prefName / unsigned int m_propName. UIOption_Checkbox::GetValue @0x00486f60 branches on which is set:

  • m_playerOption != Invalid (0xFFFFFFFF)PlayerModule::GetOption → the server-synced character option.
  • otherwise → UIPreferences::InqPreferenceValue(m_prefName) → a client-local preference, applied via UIPreferences::ModifyPreference + CM_UI::SendNotice_UserPreferenceChanged (the apply path at 0x00486e2b-0x00486e49).

Cross-lane fact, cheap to state here: gmConfigUI::InitOptions @0x0049e400 and gmChatOptionsUI::InitOptions @0x0049fc60 contain zero AddToggleOption(<X>_PlayerOption) calls (12 AddHeader + 12 AddSeperator between them, no PlayerOption rows). Every server-synced character option in retail lives on the Character tab; Config and Chat are m_prefName/per-window-blob territory.

1.3 Apply → wire: IsAutoSaveOption

PlayerModule::SetOption(opt, v)            @0x005d3eb0
  → writes the bit into options_ / options2_
  → virtual vtable+0x14 == CPlayerModule::OnChanged(PlayerOption)   @0x0059a8e0
        → CM_UI::SendNotice_PlayerOptionChanged(opt)     // local UI fan-out
        → local side-effect switch (6 cases only — §1.5)
        → if (CPlayerModule::IsAutoSaveOption(opt))              @0x0059a600
              CM_Character::Event_PlayerOptionChangedEvent(opt, GetOption(opt))
              // == GameAction opcode 5, 0x14-byte body: u32 option, u32 value
              return;
          else
              m_bDirty = 1; m_timeFirstDirtied = Timer::cur_time;

CPlayerModule::UseTime      @0x0059a710 : dirty && (cur_time - firstDirtied) > 480.0
                                          → CM_Character::Event_CharacterOptionsEvent
CPlayerModule::SaveToServer @0x0059a660 : (dirty || force) → same
CM_Character::Event_CharacterOptionsEvent @0x006a10c0 : opcode 0x1A1, body is
                                          literally PlayerModule::Pack(...)

So the 0x01A1 body is the packed PlayerModule — flags-driven, exactly what ACE's GameActionSetCharacterOptions reader parses. (Lane for Q4; recorded here because the builder address is the answer.)

IsAutoSaveOption lookup table — byte-verified from the PDB-paired binary at VA 0x0059a62c (file offset 0x19a62c, .text), 0x34 bytes:

00 00 00 01 01 01 01 01 01 01 01 01 01 01 01 00 00 00 00 01 01 01 01 01
01 00 01 00 01 01 01 01 01 01 01 00 00 00 00 00 01 01 00 00 01 01 00 00
01 00 00 00

0x00jump_table[0]return 1 (auto-save). The 21 auto-save ids are therefore: 0x00, 0x01, 0x02, 0x0F, 0x10, 0x11, 0x12, 0x19, 0x1B, 0x23, 0x24, 0x25, 0x26, 0x27, 0x2A, 0x2B, 0x2E, 0x2F, 0x31, 0x32, 0x33. Every other id (0x030x0E, 0x130x18, 0x1A, 0x1C0x22, 0x28, 0x29, 0x2C, 0x2D, 0x30) is batched.

Sanity check against an independent source: ACE's comment says "Auto Repeat Attacks → SetSingleCharacterOption; Disable Most Weather Effects → won't trigger". Table: 0x00 = auto-save ✔, 0x04 = batched ✔.

1.4 The Defaults button — retail's own per-option default table

PlayerModule::GetDefaultOptionValue @0x005d2a30. The UIOption_Checkbox caches it into m_default at SetPlayerOption @0x00486f2d, so this is what the Character tab's Defaults button restores.

Byte-verified at VA 0x005d2a5c (file 0x1d2a5c), 0x2B bytes — indices 0x00..0x2A only, followed by 90 padding:

00 01 00 01 01 01 00 01 00 01 00 01 01 00 00 00 01 01 01 01 00 00 01 01
01 00 01 00 01 01 01 01 01 01 01 00 00 00 01 01 01 01 00

0x00 → default true. Options 0x2B..0x33 fall off the end of the table and default to false (UseFastMissiles, FilterLanguage, ConfirmVolatileRareUse, HearSocietyChat, ShowHelm, DisableDistanceFog, UseMouseTurning, ShowCloak, LockUI) — the table was never extended when those options were added.

Reconstructing the words from the true entries:

  • Options1 = 0x02|0x08|0x40|0x100|0x400|0x2000|0x8000|0x40000|0x400000|0x800000|0x10000000|0x40000000 = 0x50C4A54Aidentical to ACE's CharacterOptions1.Default. This is an independent binary confirmation of that constant and of the id→bit map for those 12 ids.
  • Options2 = 0x100|0x200|0x400|0x8000 = 0x00008700, whereas ACE's CharacterOptions2.Default = 0x00948700. The three extra ACE bits (ConfirmVolatileRareUse 0x40000, ShowHelm 0x100000, ShowCloak 0x800000) are exactly three of the ids past the table's end. This is not a divergence to file — retail's table is the client Defaults button, ACE's constant is the server character-creation value; they are different mechanisms and retail's own server-side creation value is not observable to us. It IS a behavioural difference the panel must reproduce: our Defaults button must restore 0x50C4A54A / 0x00008700, not ACE's creation mask.

1.5 Retail's immediate local side effects (only six)

CPlayerModule::OnChanged(PlayerOption) switch — lookup table at 0x0059aa04 (0x2F bytes, index = arg2 - 2), byte-verified:

Option Effect
0x02 IgnoreFellowshipRequests if set → clears FellowshipAutoAcceptRequests
0x04 DisableMostWeatherEffects SmartBox::EnableWeather(!value)
0x05 PersistentAtDay LScape::SetDay(value)
0x07 ViewCombatTarget ClientCombatSystem::TrackTarget(value)
0x12 FellowshipAutoAcceptRequests if set → clears IgnoreFellowshipRequests
0x30 DisableDistanceFog LScape::m_fFogEnabled = !value

CPlayerModule::OnInitialize @0x0059a690 applies four of these at login (PersistentAtDay, DisableDistanceFog, DisableMostWeatherEffects, ViewCombatTarget). Everything else is consumed lazily at its point of use (§2's "retail consumer" column).

BN artifact note. OnInitialize/OnChanged render the logical NOT as eax = -(eax); x = ((eax - eax) + 1) — that is neg / sbb eax,eax / add eax,1, i.e. x = !value. Reading it literally as arithmetic would invert the weather and fog semantics. This is the exact artifact class the chat digest's DO-NOT-RETRY table warns about.

1.6 Every option is also a bindable keyboard action

CPlayerSystem::OnAction(InputEvent*) @0x00561890 contains one case per option that does Set<Option>(!Get<Option>()) — e.g. case 0x17AutoRepeatAttack, case 0x21ToggleRun, case 0x22StayInChatMode. That single function is the source of the 0x00562xxx call sites that appear against nearly every accessor, and it is why an accessor with only a 0x00562xxx reference has no real consumer — it is just bindable. Relevant to the campaign's Configure-Keyboard question (Q5): the keymap and the option map are coupled.


2. The map

Row order is retail's authored order from gmCharacterSettingsUI::InitOptions @0x004a02f0; group boundaries are its AddHeader/AddSeperator calls. Group names come from the user's screenshots (the header string ids render as 0 in BN — see §6 unknowns).

Column legend

  • Storageid = PlayerOption/ACE CharacterOption value; O1/O2 = CharacterOptions1/CharacterOptions2; bit verified against PlayerModule::GetOption @0x005d3aa0.
  • Def — retail client Defaults-button value (§1.4). F* = falls past the end of retail's default table (id ≥ 0x2B), so false by omission.
  • WireS = SetSingleCharacterOption 0x0005, sent immediately on change (auto-save). B = batched into SetCharacterOptions 0x01A1 (480 s dirty timer / forced save).
  • ACEHonors = the server changes real behaviour from the stored bit (cite); Stores = persisted + echoed in PlayerDescription, never read.
  • Retail consumer — where the 2013 client actually reads it ( = nowhere but §1.6's keybind toggle, i.e. server-only).
  • acdreamLIVE(server) = driven by the server bit; LIVE(local) = a live consumer exists but reads settings.json, not the server bit; state-only = persisted, no consumer; missing = nothing at all.
  • Bot — would a headless bot ever meaningfully set it.

2.1 User Interface Behavior (3 rows)

Retail label Storage Def Wire ACE Retail consumer acdream Bot
Keep Combat Targets in View 0x07 · O1 0x00000080 F B Stores ClientCombatSystem::TrackTarget via OnChanged @0x0059a954 / OnInitialize @0x0059a6ee LIVE(local)src/AcDream.App/Combat/CombatCameraTargetSource.cs:44RuntimeSettingsController.cs:229GameplaySettings.ViewCombatTarget no
Salvage Multiple Materials at Once 0x22 · O2 0x00000080 F B Stores gmSalvageUI::IsItemSuitable @0x004cb040 missing low
Use Main Pack as Default for Picking Up Items 0x29 · O2 0x00004000 F B Stores CPlayerSystem::PlaceInBackpack @0x0055d8c0 missing yes

2.2 User Interface Display (15 rows)

Retail label Storage Def Wire ACE Retail consumer acdream Bot
Vivid Targeting Indicator 0x0E · O1 0x00008000 T B Stores VividTargetIndicator::UpdateDisplayState @0x004f5fb0 LIVE(local)src/AcDream.App/Composition/InteractionRetainedUiComposition.cs:675 no
Display 3D Tooltips 0x08 · O1 0x00000100 T B Stores UIElement_SmartBoxWrapper::RecvNotice_SmartBoxObjectFound @0x004e5ad0 state-only — GameplaySettings.cs:32, no consumer no
Show Coordinates By the Radar 0x14 · O1 0x00400000 T B Stores gmRadarUI::UpdateCoordinates @0x004d8c80 LIVE(local)src/AcDream.App/Composition/LivePresentationComposition.cs:836 no
Side By Side Vitals 0x13 · O1 0x00200000 F B Stores gmGamePlayUI::RecvNotice_PlayerOptionChanged @0x004e9da0; vitals layout @0x004cf160, @0x004d0830 state-only — GameplaySettings.cs:34 no
Display Spell Durations 0x15 · O1 0x00800000 T B Stores EffectInfoRegion::Update @0x004f1c00 (+ @0x004f2522) state-only — GameplaySettings.cs:36 no
Disable Most Weather Effects 0x04 · O1 0x00010000 F B Stores SmartBox::EnableWeather via OnChanged @0x0059a921 missing — seam is RuntimeWorldEnvironmentState.Weather (src/AcDream.Runtime/World/RuntimeWorldEnvironmentState.cs:149) no
Disable Distance Fog 0x30 · O2 0x00200000 F* B Stores LScape::m_fFogEnabled via OnChanged @0x0059a940 missing — seam is SceneLightingUbo.cs:127-128 / WorldRenderFrameBuilder.cs:489-492 no
Always Daylight Outdoors 0x05 · O2 0x00000001 F B Stores LScape::SetDay via OnChanged @0x0059a916 missing — seam is RuntimeWorldEnvironmentDefinition.ForcedDayGroupIndex (RuntimeWorldEnvironmentState.cs:50) no
Disable House Restriction Effects 0x16 · O1 0x02000000 F B Stores ACCWeenieObject::CanMoveInto @0x0058da40 missing no
Use Crafting Chance of Success Dialog 0x1A · O1 0x80000000 F B HonorsACE.Server/Managers/RecipeManager.cs:94 ItemHolder::TargetAcquired @0x00588ef0 missing yes
Confirm Use of Rare Gems 0x2D · O2 0x00040000 F* B HonorsACE.Server/WorldObjects/Gem.cs:76 ItemHolder::UseObject @0x00588a80 missing yes
Display Timestamps 0x21 · O2 0x00000040 F B Stores ClientSystem::AddTextToScroll @0x00563c50 (line 368476) missing — chokepoint is RuntimeCommunicationState.AddText (src/AcDream.Runtime/Gameplay/RuntimeCommunicationState.cs:178) no
Filter Language 0x2C · O2 0x00020000 F* B Stores ClientSystem::AddTextToScroll @0x00563c50 (line 368389) missing no
Show Your Helm or Head Gear 0x2F · O2 0x00100000 F* S HonorsCreature_Networking.cs:49; the 0x0005 handler also re-broadcasts GameMessageObjDescEvent — (server-driven appearance) state-only — GameplaySettings.cs:39 low
Show Your Cloak 0x32 · O2 0x00800000 F* S HonorsCreature_Networking.cs:50; same ObjDesc rebroadcast — (zero client reads in the whole binary) state-only — GameplaySettings.cs:40 low

2.3 Grouping (6 rows)

Retail label Storage Def Wire ACE Retail consumer acdream Bot
Ignore Allegiance Requests 0x01 · O1 0x00000004 F S HonorsPlayer_Allegiance.cs:317 missing yes
Ignore Fellowship Requests 0x02 · O1 0x00000008 T S HonorsPlayer_Fellowship.cs:98 mutual exclusion with 0x12, OnChanged @0x0059a96a missing yes
Show Allegiance Logons 0x18 · O1 0x08000000 F B HonorsPlayer_Allegiance.cs:454,466 missing yes
Share Fellowship Experience and Luminance 0x0F · O1 0x00040000 T S Client-sourced — ACE reads the shareXP field of the create-fellowship action, not the option (Entity/Fellowship.cs:31,53-54) gmFellowshipUI::CreateFellowship @0x0048f730 reads the option into the packet missing yes (if the bot creates fellowships)
Share Fellowship Loot 0x11 · O1 0x00100000 F S HonorsEntity/Fellowship.cs:57 (leader's option at creation) missing yes
Automatically Accept Fellowship Requests 0x12 · O1 0x20000000 F S HonorsEntity/Fellowship.cs:120 mutual exclusion with 0x02, OnChanged @0x0059a980 missing yes

2.4 Other Players (11 rows)

Retail label Storage Def Wire ACE Retail consumer acdream Bot
Accept Corpse Looting Permissions 0x10 · O1 0x00080000 F S HonorsPlayer_Death.cs:755 LIVE(local), never sentClientCommandController.cs:444-450LiveSessionRuntimeFactory.cs:431-432GameplaySettings.AcceptLootPermits. Divergence, unfiled yes
Attempt to Deceive Other Players 0x09 · O1 0x00000200 F B HonorsWorldObjects/Player.cs:324 missing (GameplaySettings does not model it) no
Let Other Players Give You Items 0x06 · O1 0x00000040 T B HonorsPlayer_Inventory.cs:3288 (raw bit test, not GetCharacterOption) state-only — GameplaySettings.cs:37 yes
Ignore All Trade Requests 0x03 · O1 0x00020000 F B HonorsPlayer_Trade.cs:49 missing yes
Drag Item to Player Opens Trade 0x17 · O1 0x04000000 F B Stores (pure client behaviour) ItemHolder::AttemptPlaceIn3D @0x00588600 LIVE(server)RuntimeCharacterState.cs:616-619,644-645InteractionRetainedUiComposition.cs:318; register row TS-48 no
Allow Others to See Your Date of Birth 0x1C · O2 0x00000002 F B HonorsNetwork/Structure/AppraiseInfo.cs:355 missing no
…Your Age 0x1D · O2 0x00000020 F B HonorsAppraiseInfo.cs:357 missing no
…Your Chess Rank 0x1E · O2 0x00000004 F B HonorsAppraiseInfo.cs:359 missing no
…Your Fishing Skill 0x1F · O2 0x00000008 F B HonorsAppraiseInfo.cs:361 missing no
…Your Number of Deaths 0x20 · O2 0x00000010 F B HonorsAppraiseInfo.cs:363 missing no
…Your Number of Titles 0x28 · O2 0x00002000 F B HonorsAppraiseInfo.cs:365 missing no

2.5 Character Behavior (7 rows)

Retail label Storage Def Wire ACE Retail consumer acdream Bot
Run as Default Movement 0x0A · O1 0x00000400 T B Stores ACCmdInterp::UITogglesRun @0x0058b180 state-only — GameplaySettings.cs:30. (RuntimeMovementCommand.ToggleRunLock is a keybind latch, not this option) yes
Advanced Combat Interface 0x0C · O1 0x00001000 F B Stores gmCombatUI::RecvNotice_SetCombatMode @0x004cc620; @0x0056bf66 state-only — GameplaySettings.cs:31 no
Auto Target 0x0D · O1 0x00002000 T B Stores ClientCombatSystem @0x0056be07, @0x0056bfeb, @0x0056c754, @0x0056d4fc LIVE(local)LiveCombatAttackOperations.cs:184, InteractionRetainedUiComposition.cs:285 yes
Automatically Repeat Attacks 0x00 · O1 0x00000002 T S HonorsPlayer_Melee.cs:375, Player_Missile.cs:284 ClientCombatSystem::RepeatAttackInProgress @0x0056a860 LIVE(local), never sentLiveCombatAttackOperations.cs:95,169; mirrored into the retail combat panel at CombatUiController.cs:192. Divergence, unfiled yes
Use Charge Attack 0x19 · O1 0x10000000 T S HonorsPlayer_Melee.cs:193 missing yes
Lead Missile Targets 0x2A · O2 0x00008000 T S HonorsCreature_Missile.cs:318 missing yes
Use Fast Missiles 0x2B · O2 0x00010000 F* S HonorsCreature_Missile.cs:223 missing yes

2.6 Chat (7 rows in the 2013 build; the screenshot shows 8)

Retail label Storage Def Wire ACE Retail consumer acdream Bot
Stay in Chat Mode After Sending a Message 0x0B · O1 0x00000800 F B Stores ChatInterface::HandleEnterKey @0x004f52d0 missing no
Listen to Allegiance Chat 0x1B · O1 0x40000000 T S Honors — join/leave in GameActionSetSingleCharacterOption.cs; Player_Networking.cs:96, Player_Allegiance.cs:136,276 SendTurbineChat @0x0057db10 refuses locally when clear LIVE(server)TurbineChatMembershipGate.cs:109; wire via SetSingleOption yes
Listen to General Chat 0x23 · O2 0x00000100 T S HonorsTurbineChatHandler.cs:293, Player_Networking.cs:98 same LIVE(server)TurbineChatMembershipGate.cs:113 yes
Listen to Trade Chat 0x24 · O2 0x00000200 T S HonorsTurbineChatHandler.cs:294, Player_Networking.cs:100 same LIVE(server):117 yes
Listen to LFG Chat 0x25 · O2 0x00000400 T S HonorsTurbineChatHandler.cs:295, Player_Networking.cs:102 same LIVE(server):121 yes
Listen to Roleplay Chat 0x26 · O2 0x00000800 F S HonorsTurbineChatHandler.cs:296, Player_Networking.cs:104 same LIVE(server):125 yes
Listen to Society Chat 0x2E · O2 0x00080000 F* S HonorsTurbineChatHandler.cs:237, Player_Networking.cs:106 same LIVE(server):129 yes
Listen to PK death messages 0x34 · O2 0x02000000 (ACE HearPKDeath) n/a UNKNOWN Stores only — set once at creation (PlayerFactory.cs:660), never read anywhere in ACE NOT PRESENT in the 2013 build (TotalNumberOfPlayerOptions = 0x34) missing no

2.7 Options that exist but are NOT on the Character tab

Three of the 53 ids have no AddToggleOption row anywhere in the 2013 build's option pages. They are real, wire-capable options.

Option Storage Wire ACE Notes
AppearOffline 0x27 O2 0x00001000 S HonorsPlayer_Character.cs:215; the 0x0005 handler routes it to Player.SetAppearOffline No UI row and no caller of SetAppearOffline outside SetOption in the 2013 build. Probably a later addition or a chat-command path. UNKNOWN how retail 2013 sets it. Bot-relevant.
UseMouseTurning 0x31 O2 0x00400000 S Stores Only setter is the keybind toggle (CPlayerSystem::OnAction @0x00562617). The Gameplay-Options tab's "Use Mouse Turning Settings" button is a different thing (lane A).
LockUI 0x33 O2 0x01000000 S Stores Set from UI at @0x004d7630 and @0x00570434; read widely (gmFloatyToolbarUI::UpdateLockedStatus @0x004cf490 and 4 more). acdream has this LIVE(local)InteractionRetainedUiComposition.cs:476, LivePresentationComposition.cs:837, LiveSessionRuntimeFactory.cs:365.

3. Cross-checks and caveats

  • Row order and grouping are decomp-authored. The six AddHeader/AddSeperator boundaries in InitOptions reproduce the screenshots exactly — 3 / 15 / 6 / 11 / 7 / 7 rows. The screenshot's extra 8th Chat row (PK deaths) is the only difference, and it is explained by the build gap.
  • The user's screenshots are from a later client than our PDB. The installed C:\Turbine\Asheron's Call\acclient.exe is a 2015 build (CLAUDE.md's debugger section). Any 2015-only row (PK deaths today; possibly others on the unscreenshotted tabs) will not be in the 2013 decomp. Treat the screenshots as the acceptance target and the decomp as the mechanism.
  • ACE's GetCharacterOption grep is not a complete "honors" sweep. AllowGive is read as a raw bit at Player_Inventory.cs:3288. A future audit must grep both GetCharacterOption( and CharacterOptions1 & / CharacterOptions2 &.
  • ACE's /option <name> [on|off] command (Command/Handlers/PlayerCommands.cs:196-330) can set any option by a GDLE-compatible name. It calls SetCharacterOption directly, so it skips the 0x0005 handler's side effects — /option HearGeneralChat on sets the bit but does not join the Turbine room, and /option ShowHelm on does not rebroadcast the ObjDesc. Not a substitute for the wire message.
  • ShareFellowshipExpAndLuminance is the one client-sourced option. ACE takes XP-sharing from the create-fellowship packet field, not from the stored bit — matching retail's gmFellowshipUI::CreateFellowship, which reads the option and puts it in the packet. Any acdream fellowship-create must do the same.

4. acdream inventory (Q7)

4.1 What exists

Piece Where
Canonical option words + revision src/AcDream.Runtime/Gameplay/RuntimeCharacterState.cs:628-712 (RuntimeCharacterOptionsState, DefaultOptions1 = 0x50C4A54A, DefaultOptions2 = 0x00948700)
Seeded from every PlayerDescription PlayerDescriptionParser.cs:430RuntimeCharacterStateLiveSessionRuntimeFactory.cs:323 (OnCharacterOptionsChanged)
Local bit write (6 ids only) RuntimeCharacterState.cs:671-704 (SetOptionBit) — silently no-ops for every id outside the six ListenTo*Chat
Wire builder src/AcDream.Core.Net/Messages/SocialActions.cs:151-160 (BuildSetSingleCharacterOption, 20-byte body — byte-identical to retail's Event_PlayerOptionChangedEvent @0x006a1510)
Id enum SocialActions.cs:199-207 (CharacterOptionId6 of 53 ids modelled)
Bitfield enums PlayerDescriptionParser.cs:206-237 (CharacterOptions1: 4 named bits + Default; CharacterOptions2: 5 Hear* bits)
Shared Runtime command src/AcDream.Runtime/GameRuntimeCommands.cs:236-252 (IRuntimeCharacterCommands.SetSingleOption)
Graphical implementation src/AcDream.App/Runtime/CurrentGameRuntimeCommandAdapter.cs:668-681SetSingleCharacterOptionRuntimeCmdLiveSessionCommandRouter.cs:477LiveSessionRuntimeFactory.cs:344-350 (the local-write-then-send chokepoint)
Headless implementation src/AcDream.Runtime/Session/DirectGameRuntimeCommandAdapter.cs:652-665
Server→UI reseed precedent src/AcDream.App/Settings/RuntimeSettingsController.cs:583-605 (SyncChatFromServerOptions) + :560-565 (PublishHearOptionChange — the diff-then-send pattern the bot config wants)
The non-retail local mirror src/AcDream.UI.Abstractions/Panels/Settings/GameplaySettings.cs — 16 options as settings.json booleans, explicitly documented (:11-16) as local-only, server sync deferred

4.2 Live consumers today — the complete list

Driven by the server bit (2):

  1. The six Hear*Chat bits → TurbineChatMembershipGate.cs:109-129.
  2. DragItemOnPlayerOpensSecureTradeRuntimeCharacterState.cs:616-619InteractionRetainedUiComposition.cs:318.

Driven by the local settings.json copy (7): AutoTarget, AutoRepeatAttack, ViewCombatTarget (combat — RuntimeSettingsController.cs:225-229, LiveCombatAttackOperations.cs, CombatCameraTargetSource.cs:44), LockUI (InteractionRetainedUiComposition.cs:476, LivePresentationComposition.cs:837), VividTargetingIndicator (InteractionRetainedUiComposition.cs:675), CoordinatesOnRadar (LivePresentationComposition.cs:836), AcceptLootPermits (LiveSessionRuntimeFactory.cs:431).

State-only, no consumer (9): ShowTooltips, SideBySideVitals, SpellDuration, AllowGive, ShowHelm, ShowCloak, ToggleRun, AdvancedCombatUI, UseMouseTurning (all in GameplaySettings.cs).

Absent entirely (~34): everything else in §2, including all six privacy rows, all four fellowship/allegiance grouping rows, weather / fog / daylight, timestamps, language filter, salvage, main-pack, charge attack, missile options, crafting/rare-gem confirmations, and stay-in-chat.

4.3 Existing register coverage

  • TS-48DragItemOnPlayerOpensSecureTrade honoured but the secure-trade subsystem is unported.
  • AP-187 — per-window chat filters persist locally because the 0x1000008C GameplayOptions blob is unparsed.
  • No row exists for "16 character options live in settings.json and never reach the server", nor for the two behavioural divergences in §0. COORDINATOR TODO: the implementing slice must add them (or retire them by shipping the wire).

4.4 One defect found while inventorying

DirectGameRuntimeCommandAdapter.SetSingleOption (src/AcDream.Runtime/Session/DirectGameRuntimeCommandAdapter.cs:652-665) sends SendSetSingleCharacterOption without the local Options.SetOptionBit(...) write that the graphical path performs at LiveSessionRuntimeFactory.cs:348. That local write is the CH4 REJECT-review fix documented in RuntimeCharacterState.cs:660-669 — its absence on the headless path means a bot that turns on ListenToGeneralChat still has TurbineChatMembershipGate refusing /g until the next PlayerDescription arrives. Same bug, same shape, different host. COORDINATOR TODO: fix in the bot-settings slice; a Runtime-level test can cover it without a client.


5. Bot relevance (Q8, research half)

5.1 Does ACE reject option changes from a logged-in session?

  • SetSingleCharacterOption (0x0005) — no session-state gate at all. GameActionAttribute carries only an opcode (ACE.Server/Network/GameAction/GameActionAttribute.cs), and GameActionSetSingleCharacterOption.Handle has no precondition. Any in-world session may change any option at any time.
  • SetCharacterOptions (0x01A1)refused before FirstEnterWorldDone with a logged warning (GameActionSetCharacterOptions.cs:25-35). ACE added that guard specifically because a client logging out from the pink-bubble state would overwrite stored options with defaults.
  • No per-option rejection exists. The handler's default: branch stores whatever it is given.

5.2 Which options can a bot meaningfully hold?

Tier 1 — server-honoured AND plausibly bot-relevant (a bot would actually want to declare these; AttemptToDeceiveOtherPlayers and the six privacy rows are also server-honoured but pointless for a bot, so they sit in tier 3): IgnoreAllegianceRequests 0x01, IgnoreFellowshipRequests 0x02, IgnoreAllTradeRequests 0x03, LetOtherPlayersGiveYouItems 0x06, ShareFellowshipExpAndLuminance 0x0F*, AcceptCorpseLootingPermissions 0x10, ShareFellowshipLoot 0x11, AutomaticallyAcceptFellowshipRequests 0x12, ShowAllegianceLogons 0x18, UseChargeAttack 0x19, UseCraftingChanceOfSuccessDialog 0x1A, AutoRepeatAttacks 0x00, LeadMissileTargets 0x2A, UseFastMissiles 0x2B, ConfirmUseOfRareGems 0x2D, AppearOffline 0x27, plus all six ListenTo*Chat (0x1B, 0x23, 0x24, 0x25, 0x26, 0x2E). (* client-sourced — the bot must put it in the fellowship-create action, not just in the option word.)

Tier 2 — client-side but a bot still wants them: UseMainPackAsDefaultForPickingUpItems 0x29 (pickup routing), RunAsDefaultMovement 0x0A (movement default), AutoTarget 0x0D, SalvageMultipleMaterialsAtOnce 0x22. These need an acdream-side consumer before they mean anything headless.

Tier 3 — presentation-only; must stay out of the bot schema by construction: every row in §2.2 except the two confirmation dialogs and helm/cloak, plus SideBySideVitals, AdvancedCombatInterface, StayInChatMode, LockUI, UseMouseTurning, and the six privacy rows (harmless but pointless for a bot).

5.3 Bot-safety constraints (hard)

  1. Never send an unmodelled id. ACE's CharacterOptionExtensions.GetCharacterOptions1Attribute is a Dictionary[val] lookup over Enum.GetValues — an out-of-range CharacterOption cast throws KeyNotFoundException inside the game-action handler. Validate against the id table before sending.
  2. Never send 0x35/0x36. CharacterOptions1Default and CharacterOptions2Default are real enum entries whose "bit" is the whole default mask; SetCharacterOption(0x35, true) ORs 0x50C4A54A into the player's word in one shot.
  3. Use 0x0005 per change, not the blob. The blob's FirstEnterWorldDone guard, its PlayerModule::Pack body, and the fact that it carries shortcuts/favourite-spells/desired-comps make it the wrong tool for a declarative bot diff.
  4. Idempotence comes free. Seed from PlayerDescription (already parsed), diff declared-vs-actual, send only differences — the exact shape RuntimeSettingsController.PublishHearOptionChange already uses. Re-running it on reconnect is a no-op.

5.4 Headless config today

src/AcDream.Headless/Configuration/HeadlessConfiguration.cs has no characterOptions block; the schema is strict ([JsonRequired] + a validating loader), so presentation settings are excluded by construction today and would stay excluded if the new block only admits the id names in §5.2 tiers 12.


6. Open unknowns (explicitly not guessed)

# Unknown Why it is open
U1 The six group-header strings and their string ids PlayerOptionPage::AddHeader(this, 0) renders its second argument as 0 at all six sites. Either BN elided a pushed immediate or the header text is resolved elsewhere. The screenshots give us the labels; the ids are unresolved. Lane A territory.
U2 The per-row label and tooltip strings InitOptions hashes string keys (ID_PlayerOption_<Name> / _Help) via compute_str_hash. Some previews are truncated at ~33 chars and some literal operands render as pooled vftable members (a known BN artifact — e.g. ID_PlayerOption_HearGeneralChat = compute_str_hash(&…RecvNotice_UserPreferenceChanged_Menu)). The key names are recoverable from the global symbol names; the displayed English must come from the string table / screenshots, not from these previews.
U3 Whether the 2015 client added Character-tab rows beyond PK deaths Only the 2013 PDB is paired. The user's screenshots are the acceptance target.
U4 How retail 2013 sets AppearOffline 0x27 Wire-capable, ACE-honoured, auto-save — but no UI row and no setter call site.
U5 Exact 0x01A1 field order for the non-boolean sections Out of lane-B scope (handoff Q4). The answer is PlayerModule::Pack at the CM_Character::Event_CharacterOptionsEvent @0x006a10c0 call site, cross-read against GameActionSetCharacterOptions.cs.
U6 Retail's server-side character-creation option defaults Not observable from the client. ACE's 0x50C4A54A / 0x00948700 are ACE's choice; retail's client Defaults button is 0x50C4A54A / 0x00008700 (§1.4). Do not "fix" one to match the other.

7. For the planner

7.1 Implement-vs-store split

Group A — wire + state only (no client consumer needed). The panel toggles them, the option word tracks them, 0x0005/0x01A1 carries them, ACE does the rest. This is the cheapest and largest group, and it is where most of the retail-parity value is.

IgnoreAllegianceRequests, IgnoreFellowshipRequests, IgnoreAllTradeRequests, LetOtherPlayersGiveYouItems, AcceptCorpseLootingPermissions, ShareFellowshipLoot, AutomaticallyAcceptFellowshipRequests, ShowAllegianceLogons, AttemptToDeceiveOtherPlayers, UseChargeAttack, UseCraftingChanceOfSuccessDialog, ConfirmUseOfRareGems, LeadMissileTargets, UseFastMissiles, ShowYourHelmOrHeadGear, ShowYourCloak, all six AllowOthersToSeeYour*, AppearOffline.

Group B — needs a client consumer, and the seam already exists. Small, well-understood bindings; each is a one-line read at a known site.

Option Bind at
Display Timestamps RuntimeCommunicationState.AddText (:178) — retail binds at the same chokepoint
Filter Language same chokepoint
Always Daylight Outdoors RuntimeWorldEnvironmentDefinition.ForcedDayGroupIndex
Disable Most Weather Effects RuntimeWorldEnvironmentState.Weather
Disable Distance Fog SceneLightingUbo fog start/end
Use Main Pack as Default the pickup/placement path (SendPickup / preferred-container choice)
Run as Default Movement RuntimeLocalPlayerMovementState (distinct from the ToggleRunLock keybind latch)
Side By Side Vitals / Display Spell Durations / Display 3D Tooltips / Vivid Targeting Indicator / Show Coordinates / Advanced Combat Interface / Stay in Chat Mode their existing retail-UI controllers

Group C — already live, but must be re-pointed from settings.json to the server bit. This is the highest-risk work in the campaign because it changes behaviour that currently works: AutoTarget, AutoRepeatAttacks, KeepCombatTargetsInView, LockUI, VividTargetingIndicator, ShowCoordinatesByTheRadar, AcceptCorpseLootingPermissions. The precedent is CH3/CH4: server bit is authoritative, the panel reseeds from PlayerDescription, the toggle writes locally then sends.

Group D — defer with a register row. SalvageMultipleMaterialsAtOnce (no salvage UI), DisableHouseRestrictionEffects (no housing), ShareFellowshipExpAndLuminance (needs the fellowship-create field, not just the bit), ListenToPKDeathMessages (not in our decomp; ACE never reads it), UseMouseTurning (belongs to the Gameplay-Options tab's mouse dialog).

7.2 Structural recommendations

  1. Promote the map into one typed table in Runtime. Today CharacterOptionId models 6 of 53 ids and SetOptionBit no-ops the rest — a panel wired to the current seam would silently fail on 47 rows. The table needs: id → (word, mask, auto-save?, client default). All four columns are in §2 and all four are byte-verified.
  2. Keep SetSingleOption as the single seam and fix the headless local-write gap (§4.4) in the same slice — one shared helper on the Runtime side rather than the write living in LiveSessionRuntimeFactory.
  3. Honour IsAutoSaveOption. Sending 0x0005 for every toggle would be simpler but is a wire divergence; sending nothing for the batched 32 loses them. Retail's own dirty-timer + forced-save-at-logout is the faithful shape and needs a PlayerModule::Pack-equivalent (handoff Q4) before the batched set can round-trip. If Q4's blob is not ready when the panel ships, sending 0x0005 for batched options too is the pragmatic interim — but it must be a register row, not a silent choice.
  4. The Defaults button restores 0x50C4A54A / 0x00008700, not ACE's creation mask, and not GameplaySettings.Default (whose own doc comment at :19-23 admits it is not bit-exact).
  5. GameplaySettings should die as options move to server truth — or be explicitly re-scoped to acdream-only client settings. Leaving two writable copies of the same 16 booleans is how the CH3 ChatSettings lie happened.

7.3 Bot-meaningful subset (short answer)

22 options in tier 1 (§5.2) plus 4 in tier 2. The strict headless schema should accept exactly those id names, reject everything else at load, and diff-then-send through IRuntimeCharacterCommands.SetSingleOption after PlayerDescription seeds actual state.