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>
615 lines
41 KiB
Markdown
615 lines
41 KiB
Markdown
# 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:
|
||
|
||
```c
|
||
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
|
||
```
|
||
|
||
`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'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 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`/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.
|
||
* **Wire** — **S** = `SetSingleCharacterOption 0x0005`, sent immediately
|
||
on change (auto-save). **B** = batched into `SetCharacterOptions 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 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: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`/`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: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):**
|
||
|
||
1. The six `Hear*Chat` bits → `TurbineChatMembershipGate.cs:109-129`.
|
||
2. `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** — `DragItemOnPlayerOpensSecureTrade` 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 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 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.
|