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>
41 KiB
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:
/acceptcorpselootingtoggles a local bool the server never sees.ClientCommandController.cs:444-450flipsGameplaySettings.AcceptLootPermitsonly. Retail sends0x0005 (0x10)immediately (auto-save), and ACE honours the bit atPlayer_Death.cs:755. Today the corpse permission is a lie.AutoRepeatAttackis client-local in acdream but server-authoritative in ACE.LiveCombatAttackOperations.cs:95,169reads 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 viaUIPreferences::ModifyPreference+CM_UI::SendNotice_UserPreferenceChanged(the apply path at0x00486e2b-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
0x00 → jump_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 (0x03–0x0E, 0x13–0x18, 0x1A, 0x1C–0x22, 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=0x50C4A54A— identical to ACE'sCharacterOptions1.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'sCharacterOptions2.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 restore0x50C4A54A/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/OnChangedrender the logical NOT aseax = -(eax); x = ((eax - eax) + 1)— that isneg/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 0x17 → AutoRepeatAttack, case 0x21 → ToggleRun,
case 0x22 → StayInChatMode. 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
- Storage —
id=PlayerOption/ACECharacterOptionvalue;O1/O2=CharacterOptions1/CharacterOptions2; bit verified againstPlayerModule::GetOption @0x005d3aa0. - Def — retail client Defaults-button value (§1.4).
F*= falls past the end of retail's default table (id ≥ 0x2B), sofalseby omission. - Wire — S =
SetSingleCharacterOption 0x0005, sent immediately on change (auto-save). B = batched intoSetCharacterOptions 0x01A1(480 s dirty timer / forced save). - ACE — Honors = 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). - acdream —
LIVE(server)= driven by the server bit;LIVE(local)= a live consumer exists but readssettings.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:44 ← RuntimeSettingsController.cs:229 ← GameplaySettings.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 | Honors — ACE.Server/Managers/RecipeManager.cs:94 |
ItemHolder::TargetAcquired @0x00588ef0 |
missing | yes |
| Confirm Use of Rare Gems | 0x2D · O2 0x00040000 |
F* | B | Honors — ACE.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 | Honors — Creature_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 | Honors — Creature_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 | Honors — Player_Allegiance.cs:317 |
— | missing | yes |
| Ignore Fellowship Requests | 0x02 · O1 0x00000008 |
T | S | Honors — Player_Fellowship.cs:98 |
mutual exclusion with 0x12, OnChanged @0x0059a96a |
missing | yes |
| Show Allegiance Logons | 0x18 · O1 0x08000000 |
F | B | Honors — Player_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 | Honors — Entity/Fellowship.cs:57 (leader's option at creation) |
— | missing | yes |
| Automatically Accept Fellowship Requests | 0x12 · O1 0x20000000 |
F | S | Honors — Entity/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 | Honors — Player_Death.cs:755 |
— | LIVE(local), never sent — ClientCommandController.cs:444-450 ↔ LiveSessionRuntimeFactory.cs:431-432 ↔ GameplaySettings.AcceptLootPermits. Divergence, unfiled |
yes |
| Attempt to Deceive Other Players | 0x09 · O1 0x00000200 |
F | B | Honors — WorldObjects/Player.cs:324 |
— | missing (GameplaySettings does not model it) |
no |
| Let Other Players Give You Items | 0x06 · O1 0x00000040 |
T | B | Honors — Player_Inventory.cs:3288 (raw bit test, not GetCharacterOption) |
— | state-only — GameplaySettings.cs:37 |
yes |
| Ignore All Trade Requests | 0x03 · O1 0x00020000 |
F | B | Honors — Player_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-645 → InteractionRetainedUiComposition.cs:318; register row TS-48 |
no |
| Allow Others to See Your Date of Birth | 0x1C · O2 0x00000002 |
F | B | Honors — Network/Structure/AppraiseInfo.cs:355 |
— | missing | no |
| …Your Age | 0x1D · O2 0x00000020 |
F | B | Honors — AppraiseInfo.cs:357 |
— | missing | no |
| …Your Chess Rank | 0x1E · O2 0x00000004 |
F | B | Honors — AppraiseInfo.cs:359 |
— | missing | no |
| …Your Fishing Skill | 0x1F · O2 0x00000008 |
F | B | Honors — AppraiseInfo.cs:361 |
— | missing | no |
| …Your Number of Deaths | 0x20 · O2 0x00000010 |
F | B | Honors — AppraiseInfo.cs:363 |
— | missing | no |
| …Your Number of Titles | 0x28 · O2 0x00002000 |
F | B | Honors — AppraiseInfo.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 | Honors — Player_Melee.cs:375, Player_Missile.cs:284 |
ClientCombatSystem::RepeatAttackInProgress @0x0056a860 |
LIVE(local), never sent — LiveCombatAttackOperations.cs:95,169; mirrored into the retail combat panel at CombatUiController.cs:192. Divergence, unfiled |
yes |
| Use Charge Attack | 0x19 · O1 0x10000000 |
T | S | Honors — Player_Melee.cs:193 |
— | missing | yes |
| Lead Missile Targets | 0x2A · O2 0x00008000 |
T | S | Honors — Creature_Missile.cs:318 |
— | missing | yes |
| Use Fast Missiles | 0x2B · O2 0x00010000 |
F* | S | Honors — Creature_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 | Honors — TurbineChatHandler.cs:293, Player_Networking.cs:98 |
same | LIVE(server) — TurbineChatMembershipGate.cs:113 |
yes |
| Listen to Trade Chat | 0x24 · O2 0x00000200 |
T | S | Honors — TurbineChatHandler.cs:294, Player_Networking.cs:100 |
same | LIVE(server) — :117 |
yes |
| Listen to LFG Chat | 0x25 · O2 0x00000400 |
T | S | Honors — TurbineChatHandler.cs:295, Player_Networking.cs:102 |
same | LIVE(server) — :121 |
yes |
| Listen to Roleplay Chat | 0x26 · O2 0x00000800 |
F | S | Honors — TurbineChatHandler.cs:296, Player_Networking.cs:104 |
same | LIVE(server) — :125 |
yes |
| Listen to Society Chat | 0x2E · O2 0x00080000 |
F* | S | Honors — TurbineChatHandler.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 | Honors — Player_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/AddSeperatorboundaries inInitOptionsreproduce 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.exeis 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
GetCharacterOptiongrep is not a complete "honors" sweep.AllowGiveis read as a raw bit atPlayer_Inventory.cs:3288. A future audit must grep bothGetCharacterOption(andCharacterOptions1 &/CharacterOptions2 &. - ACE's
/option <name> [on|off]command (Command/Handlers/PlayerCommands.cs:196-330) can set any option by a GDLE-compatible name. It callsSetCharacterOptiondirectly, so it skips the 0x0005 handler's side effects —/option HearGeneralChat onsets the bit but does not join the Turbine room, and/option ShowHelm ondoes not rebroadcast the ObjDesc. Not a substitute for the wire message. ShareFellowshipExpAndLuminanceis the one client-sourced option. ACE takes XP-sharing from the create-fellowship packet field, not from the stored bit — matching retail'sgmFellowshipUI::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:430 → RuntimeCharacterState → LiveSessionRuntimeFactory.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 (CharacterOptionId — 6 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-681 → SetSingleCharacterOptionRuntimeCmd → LiveSessionCommandRouter.cs:477 → LiveSessionRuntimeFactory.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):
- The six
Hear*Chatbits →TurbineChatMembershipGate.cs:109-129. DragItemOnPlayerOpensSecureTrade→RuntimeCharacterState.cs:616-619→InteractionRetainedUiComposition.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-48 —
DragItemOnPlayerOpensSecureTradehonoured but the secure-trade subsystem is unported. - AP-187 — per-window chat filters persist locally because the
0x1000008CGameplayOptions blob is unparsed. - No row exists for "16 character options live in
settings.jsonand 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.GameActionAttributecarries only an opcode (ACE.Server/Network/GameAction/GameActionAttribute.cs), andGameActionSetSingleCharacterOption.Handlehas no precondition. Any in-world session may change any option at any time.SetCharacterOptions (0x01A1)— refused beforeFirstEnterWorldDonewith 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)
- Never send an unmodelled id. ACE's
CharacterOptionExtensions.GetCharacterOptions1Attributeis aDictionary[val]lookup overEnum.GetValues— an out-of-rangeCharacterOptioncast throwsKeyNotFoundExceptioninside the game-action handler. Validate against the id table before sending. - Never send
0x35/0x36.CharacterOptions1DefaultandCharacterOptions2Defaultare real enum entries whose "bit" is the whole default mask;SetCharacterOption(0x35, true)ORs0x50C4A54Ainto the player's word in one shot. - Use
0x0005per change, not the blob. The blob'sFirstEnterWorldDoneguard, itsPlayerModule::Packbody, and the fact that it carries shortcuts/favourite-spells/desired-comps make it the wrong tool for a declarative bot diff. - Idempotence comes free. Seed from PlayerDescription (already
parsed), diff declared-vs-actual, send only differences — the exact
shape
RuntimeSettingsController.PublishHearOptionChangealready 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 1–2.
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 sixAllowOthersToSeeYour*,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
- Promote the map into one typed table in Runtime. Today
CharacterOptionIdmodels 6 of 53 ids andSetOptionBitno-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. - Keep
SetSingleOptionas 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 inLiveSessionRuntimeFactory. - Honour
IsAutoSaveOption. Sending0x0005for 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 aPlayerModule::Pack-equivalent (handoff Q4) before the batched set can round-trip. If Q4's blob is not ready when the panel ships, sending0x0005for batched options too is the pragmatic interim — but it must be a register row, not a silent choice. - The Defaults button restores
0x50C4A54A/0x00008700, not ACE's creation mask, and notGameplaySettings.Default(whose own doc comment at:19-23admits it is not bit-exact). GameplaySettingsshould 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 CH3ChatSettingslie 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.