docs: Campaign OP research — Options panel structure + Chat/Config tabs (lane A)
Final of four Opus research lanes for the retail Options panel campaign:
- One LayoutDesc 0x2100002B owns the tab control (0x10000208, Type 8),
the four mounted tab pages, and the seven option-row templates. Pages:
gmGameplayOptionsUI 0x2100002A/0x10000202 (default tab),
gmCharacterSettingsUI 0x21000028/0x100001F9 (InitOptions @0x004A02F0),
gmChatOptionsUI 0x2100005C/0x1000050A (@0x0049FC60),
gmConfigUI 0x21000029/0x100001FF (@0x0049E400). Tab declarations are a
data table (property 0x2E structs {0x30 button, 0x31 page, 0x32
default}); rows build via UIElement_ListBox::AddItemFromTemplateList
against authored template lists in ListBox property 0x64 (all three
template arrays decoded).
- Open path: input action 0x1000001A ToggleOptionsPanel, retail-default
F11 (VERIFIED in retail-default.keymap.txt:148); toolbar button
0x1000019B authors P0x12=0x1000001A (VERIFIED in the committed
toolbar fixture); host is gmFloatyPanelUI 0x2100006E slot 0x1000018D —
the same floating shell CH6 ports.
- Apply/Reset/Defaults are PER TAB, and clicking an LED APPLIES
IMMEDIATELY (SetCurrentValue -> Apply(1)); Apply commits the undo
baseline + CPlayerModule::SaveToServer (flushes 0x01A1 if dirty);
Reset reverts to baseline; Defaults applies live without committing.
Hiding a page auto-reverts uncommitted edits; showing auto-applies.
- Chat tab fully enumerated (2 linked opacity sliders + 5 per-window
filter blocks, 13 checkbox masks byte-decoded; main window omits the
Gameplay checkbox - 12 rows vs floaties' 13). Config tab = 6 sections
/ 27 rows, ALL UserPreferences.ini-backed, nothing on the wire.
- Two new widgets needed: Type 8 tab control, Type 5 template-list
ListBox (both also needed by Configure Keyboard). U1: the Character
tab's Defaults behavior is genuinely unestablished (never calls
SetDefaultValue). U3: the 50th row (PK deaths) is a later-build
addition (DAT string exists, hash-verified). U7: Urgent Assistance /
Report Abuse ShellExecute dead support URLs — register-row candidates.
Research phase complete: all four lanes landed and seam-verified.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
29138430ef
commit
e3e0559f65
1 changed files with 948 additions and 0 deletions
948
docs/research/2026-08-10-options-panel-structure.md
Normal file
948
docs/research/2026-08-10-options-panel-structure.md
Normal file
|
|
@ -0,0 +1,948 @@
|
|||
# Retail Options panel — structure, open path, Apply/Reset/Defaults, Chat + Config tab contents
|
||||
|
||||
**Date:** 2026-08-10
|
||||
**Lane:** Settings-track RESEARCH LANE A (handoff questions Q1 and Q3, plus the
|
||||
Chat/Config tab derivation).
|
||||
**Status:** RESEARCH ONLY — no production code changed, nothing built, nothing launched.
|
||||
**Handoff this answers:** `docs/research/2026-08-10-settings-track-handoff.md` §"Research
|
||||
questions" items 1 and 3, with partial pre-work on items 5 and 6.
|
||||
|
||||
## Primary sources and evidence discipline
|
||||
|
||||
- `docs/research/named-retail/acclient_2013_pseudo_c.txt` (Binary Ninja pseudo-C, Sept 2013
|
||||
EoR build, PDB names) and `docs/research/named-retail/symbols.json`.
|
||||
- **Byte-verified** against the PDB-paired binary `C:\Users\erikn\Downloads\acclient.exe`.
|
||||
`py tools/pdb-extract/check_exe_pdb.py "C:/Users/erikn/Downloads/acclient.exe"` reports
|
||||
`=== MATCH: this exe pairs with our acclient.pdb ===` (linker UTC 2013-09-06T00:17:56,
|
||||
CodeView GUID `9e847e2f-777c-4bd9-886c-22256bb87f32`). Every load-bearing constant below
|
||||
that BN rendered as `0` or as a zero-idiom was re-read from the raw section bytes with a
|
||||
throwaway PE reader (scratchpad `peread.py` / `decode_addchild.py`, not committed).
|
||||
- The **installed DATs** at `%USERPROFILE%\Documents\Asheron's Call\` read through
|
||||
`Chorizite.DatReaderWriter` 2.1.7 via `DatCollection` — the same reader production uses.
|
||||
Throwaway scratchpad probe (not committed); recipe per
|
||||
`docs/research/2026-08-10-retail-ui-text-style.md` §tooling. **NOTE:** a standalone
|
||||
`PortalDatabase`/`LocalDatabase` throws on `LayoutDesc` unpack
|
||||
(`writer.Database.DatCollection is null!`) — you must open a `DatCollection`.
|
||||
- The retail `UserPreferences.ini` sitting next to the user's DATs — a live retail artifact
|
||||
that independently cross-checks the Config tab's preference keys and default values.
|
||||
|
||||
Everything marked UNKNOWN / UNVERIFIED below is genuinely not established. Nothing is guessed.
|
||||
|
||||
---
|
||||
|
||||
## 0. Headline
|
||||
|
||||
Retail's Options panel is **one LayoutDesc** — `0x2100002B` — that carries the tab control,
|
||||
the four mounted tab pages, **and** the option-row template list, all in one file. The four
|
||||
pages are four separate `UIElement` classes registered by class id, each authored in its own
|
||||
LayoutDesc and mounted into the tab host by `BaseLayoutId`/`BaseElement`. The panel is
|
||||
opened by input action **`0x1000001A` = `ToggleOptionsPanel`** (F11 by default, and the
|
||||
toolbar's options button fires the same action).
|
||||
|
||||
The Apply model is the opposite of what a modern designer would build: **every LED click
|
||||
applies immediately**. "Apply" only commits the undo baseline (and flushes the
|
||||
character-options blob); "Reset" reverts to that baseline; "Defaults" loads the retail
|
||||
default and applies it live without committing. Hiding a page auto-reverts uncommitted
|
||||
edits.
|
||||
|
||||
---
|
||||
|
||||
## 1. The class family and the layout inventory (Q1)
|
||||
|
||||
### 1.1 Classes
|
||||
|
||||
| Retail class | Element class id | `Register` | `Create` | `InitOptions` | Purpose |
|
||||
|---|---|---|---|---|---|
|
||||
| `OptionPage` | — (abstract) | — | — | — | base: registered-option array + save/restore/changed |
|
||||
| `PlayerOptionPage : OptionPage` | — (abstract) | — | — | — | base: ListBox-backed row builders |
|
||||
| `gmGameplayOptionsUI` | `0x10000029` | `0x0049E0F0` | `0x0049E060` | *(none — pure button list)* | Tab 1 "Gameplay Options" |
|
||||
| `gmCharacterSettingsUI` | `0x10000027` | `0x004A02D0` | `0x004A01D0` | `0x004A02F0` | Tab 2 "Character" |
|
||||
| `gmChatOptionsUI` | `0x10000042` | `0x0049FC40` | `0x0049FBA0` | `0x0049FC60` | Tab 3 "Chat" |
|
||||
| `gmConfigUI` | `0x10000028` | `0x0049E8D0` | `0x0049E340` | `0x0049E400` | Tab 4 "Config" |
|
||||
| `gmKeyboardUI` | `0x1000000E` | `0x004DCF70` | `0x004DC080` | `0x004DD8B0` | "Configure Keyboard" screen (separate window) |
|
||||
|
||||
`OptionPage` members (`symbols.json`): `RegisterOption 0x004F2E90`, `SaveCurrentValues
|
||||
0x004F2C60`, `RestoreSavedValues 0x004F2D00`, `RestoreDefaultValues 0x004F2CB0`, `Changed
|
||||
0x004F2D60`.
|
||||
|
||||
`PlayerOptionPage` members: `AddHeader 0x004F28B0`, `AddSeperator 0x004F2730` *(retail's own
|
||||
spelling)*, `AddToggleOption` **two overloads** `0x004F2990` (`PStringBase<char>` prefName)
|
||||
and `0x004F2A00` (`enum PlayerOption`), `AddToggleWithSliderOption 0x004F2930`,
|
||||
`AddSliderOption 0x004F2A70`, `AddMenuOption 0x004F2AF0`, `SetToggleLabel 0x004F2750`,
|
||||
`SetSliderLabel 0x004F2B80`, `OnOptionChanged 0x004F27D0`, `OnOptionChangeConfirmed
|
||||
0x004F2850`, `OnVisibilityChanged 0x004F26E0`, `SaveCurrentValues 0x004F2710`.
|
||||
|
||||
`UIOption` widget classes (the row leaves), by DynamicCast id:
|
||||
|
||||
| DynamicCast id | Class | Row child element looked up |
|
||||
|---|---|---|
|
||||
| `0x10000035` | `UIOption_Checkbox` | `0x10000219` |
|
||||
| `0x10000036` | `UIOption_CheckboxSlider` | *(the row root itself)* |
|
||||
| `0x10000037` | `UIOption_Slider` | `0x1000021C` |
|
||||
| `0x10000038` | `UIOption_Menu` | `0x10000224` |
|
||||
| `0x10000044` | `UIOption_CheckboxBitfield64` | *(the row root itself)* |
|
||||
|
||||
There is also `UIOption_ActionKeyMap` (keyboard screen) and `UIOption_CheckboxBitfield`
|
||||
(32-bit variant, `Apply 0x00486490` — **not used by any of the four Options tabs**).
|
||||
|
||||
### 1.2 LayoutDescs (all in `client_local_English.dat`)
|
||||
|
||||
| LayoutDesc | Root element | Root `Type` | Size | What it is |
|
||||
|---|---|---|---|---|
|
||||
| **`0x2100002B`** | `0x10000208` | `0x00000008` (TabControl) | **300 × 600** | **The Options panel**: tab strip + 4 mounted pages + 7 row templates |
|
||||
| `0x2100002A` | `0x10000202` | `0x10000029` | 300 × 337 | Gameplay Options page |
|
||||
| `0x21000028` | `0x100001F9` | `0x10000027` | 292 × 600 | Character page |
|
||||
| `0x2100005C` | `0x1000050A` | `0x10000042` | 292 × 600 | Chat page |
|
||||
| `0x21000029` | `0x100001FF` | `0x10000028` | 292 × 600 | Config page |
|
||||
| `0x21000009` | `0x1000001F` | `3` | 800 × 600 | Configure Keyboard full-screen screen (contains `0x10000020` `T=0x1000000E`, 590 × 462) |
|
||||
|
||||
Each page root also carries `P0x58 = enum 1` and a `P0x57` enum — see §2.3.
|
||||
|
||||
### 1.3 The tab control — element `0x10000208`, Type `8`
|
||||
|
||||
Authored properties on the tab-host root:
|
||||
|
||||
```
|
||||
P0x58 = enum 1
|
||||
P0x57 = enum 0x1000001A <- the input action that toggles this window (see §2)
|
||||
P0x2E = ARRAY of 4 StructBaseProperty (MasterPropertyId 0x2F), each:
|
||||
0x30 = <tab button element id>
|
||||
0x31 = <tab page element id>
|
||||
0x32 = bool (true on exactly one entry = the initially selected tab)
|
||||
```
|
||||
|
||||
The four decoded entries, **in authored array order**:
|
||||
|
||||
| # | `0x30` tab button | `0x31` page slot | `0x32` default | Tab label (StringId, table `0x23000001`) |
|
||||
|---|---|---|---|---|
|
||||
| 0 | `0x1000020D` (0,0) 106×25 | `0x10000212` | **True** | `0x0AFFD072` → **"Gameplay Options"** |
|
||||
| 1 | `0x1000020E` (106,0) 64×25 | `0x10000211` | False | **"Character"** |
|
||||
| 2 | `0x1000050B` (172,0) 50×25 | `0x1000050C` | False | **"Chat"** |
|
||||
| 3 | `0x1000020F` (222,0) 56×25 | `0x10000213` | False | **"Config"** |
|
||||
|
||||
Left-to-right x order = 0, 106, 172, 222 → **Gameplay Options | Character | Chat | Config**,
|
||||
exactly matching the user's screenshot. Tab buttons all inherit base `0x2100004D/0x1000043A`.
|
||||
|
||||
Additional children of `0x10000208`:
|
||||
|
||||
| Element | Geometry | Notes |
|
||||
|---|---|---|
|
||||
| `0x10000209` | (0,25) 2×575, image | left border strip beside the page area |
|
||||
| `0x10000210` | (276,0) 24×25 | **close (X) button**; `P0x12 = enum 0x1000001A`; has `Normal` + `Normal_pressed` media |
|
||||
| `0x10000211` | (2,25) 298×575 | Character page slot ← base `0x21000028/0x100001F9` |
|
||||
| `0x10000212` | (2,25) 298×575 | Gameplay page slot ← base `0x2100002A/0x10000202` |
|
||||
| `0x1000050C` | (2,25) 298×575 | Chat page slot ← base `0x2100005C/0x1000050A` |
|
||||
| `0x10000213` | (2,25) 298×575 | Config page slot ← base `0x21000029/0x100001FF` |
|
||||
|
||||
### 1.4 Where the Options panel is *hosted*
|
||||
|
||||
`0x2100002B/0x10000208` is mounted (by base reference) into exactly two host layouts, both
|
||||
at slot element `0x1000018D` inside a shared page-stack container `0x10000180`:
|
||||
|
||||
| Host LayoutDesc | Root | Root type | Root size | Container | Slot key `P0x10000029` |
|
||||
|---|---|---|---|---|---|
|
||||
| `0x21000017` | `0x1000017B` | `0x10000008` = **`gmPanelUI`** (Register `0x004BC9AA`) | 309×482 @ (491,28), ZLevel 800 | `0x10000180` (9,120) 300×362 | `0x1000018D` → **10 (0xA)** |
|
||||
| `0x2100006E` | `0x100005FE` | `0x1000004F` = **`gmFloatyPanelUI`** (Register `0x004D1A1A`) | 310×372 | `0x10000180` (5,5) 300×362 | `0x1000018D` → **10 (0xA)** |
|
||||
|
||||
`0x21000017` is the legacy docked right-hand panel (it also carries the radar,
|
||||
`0x1000003D` `T=0x10000010` = `gmRadarUI`). `0x2100006E` is the modern **floating,
|
||||
resizable** window frame — eight 5 px border strips (`0x10000653`–`0x1000065A`) plus
|
||||
`T=9` resize grips / `T=2` dragbar, which is the same shell family Campaign CH6 ported.
|
||||
The page-stack slot is authored 300×362; the panel content is authored 300×600, so retail's
|
||||
parent-resize policy scales it — and because the floaty frame is resizable, the user's tall
|
||||
screenshot is the resized state, not a third layout. The whole panel is 300 px wide and the
|
||||
option ListBox is 276 px wide with a 16 px scrollbar.
|
||||
|
||||
> The same `0x10000180` stack holds ~16 sibling panels (allegiance, skills, attributes,
|
||||
> inventory, character info …), each keyed by `P0x10000029`. The Options panel's key is 10.
|
||||
|
||||
### 1.5 The row mechanism — a ListBox with an authored template list
|
||||
|
||||
`PostInit` on each page fetches its ListBox and stores it as `m_pOptionBox`:
|
||||
|
||||
| Page | `GetChildRecursive(this, …)` | ListBox element | Cast |
|
||||
|---|---|---|---|
|
||||
| Character | `0x100001FA` (`gmCharacterSettingsUI::PostInit @0x004A0280`) | `0x100001FA` 276×560, `T=5` | `DynamicCast(5)` |
|
||||
| Config | `0x10000200` (`gmConfigUI::PostInit @0x0049E840`) | `0x10000200` 276×560, `T=5` | `DynamicCast(5)` |
|
||||
| Chat | `0x1000050D` (`gmChatOptionsUI::PostInit @0x0049F100`) | `0x1000050D` 276×560, `T=5` | `DynamicCast(5)` |
|
||||
|
||||
Every row is created by `UIElement_ListBox::AddItemFromTemplateList(m_pOptionBox, <index>,
|
||||
nullptr)`. **The template list is authored on the ListBox as property `0x64`** — an array of
|
||||
`StructBaseProperty` where `0x63` = template LayoutDesc DID and `0x62` = template element id.
|
||||
All templates live in `0x2100002B`.
|
||||
|
||||
**Template arrays as authored (index → element):**
|
||||
|
||||
| idx | Character (`0x100001FA`) | Config (`0x10000200`) | Chat (`0x1000050D`) | Template element | Builder |
|
||||
|---|---|---|---|---|---|
|
||||
| 0 | `0x10000216` | `0x10000216` | `0x10000216` | 292×22, base `0x2100003F/0x1000038C` | `AddHeader` |
|
||||
| 1 | `0x10000217` | `0x10000217` | `0x10000217` | 292×8, image | `AddSeperator` |
|
||||
| 2 | `0x10000218` | `0x10000218` | `0x10000218` | 272×20, child `0x10000219` (checkbox) | `AddToggleOption` |
|
||||
| 3 | — | `0x1000021A` | `0x1000021A` | 272×20, label `0x1000021B` + slider `0x1000021C` | `AddSliderOption(arg3 == 0)` |
|
||||
| 4 | — | `0x10000222` | `0x10000222` | 272×20, label `0x10000223` + menu `0x10000224` | `AddMenuOption` |
|
||||
| 5 | — | `0x10000220` | `0x10000220` | 272×20, `T=0x10000036` | `AddToggleWithSliderOption` |
|
||||
| 6 | — | `0x1000021D` | `0x1000021D` | 272×**36**, + range labels `0x1000021E`/`0x1000021F` | `AddSliderOption(arg3 != 0)` |
|
||||
| 7 | — | `0x10000221` | `0x10000221` | 272×**36**, `T=0x10000036` + range labels | *(unused by the four tabs)* |
|
||||
| 8 | — | — | `0x10000520` | 272×100, `T=0x10000044` | `CheckboxBitfield64` block |
|
||||
|
||||
`0x10000521` (272×20, child `0x10000219` with base `0x21000044/0x10000329`) exists in
|
||||
`0x2100002B` but is **not** in any of the three template arrays — UNKNOWN consumer.
|
||||
|
||||
**Byte-verified slider index selection.** BN renders `PlayerOptionPage::AddSliderOption`'s
|
||||
index as `(((eax - eax) & 3) + 3)`, which is the known `neg`/`sbb` zero-idiom artifact class.
|
||||
The real instruction sequence at `0x004F2A80`–`0x004F2A90` is `neg eax; sbb eax,eax; and
|
||||
eax,3; add eax,3`, i.e. **index 6 when `arg3 != 0`, index 3 when `arg3 == 0`**. This is
|
||||
corroborated behaviourally: every `AddSliderOption(..., 1)` call in `gmConfigUI::InitOptions`
|
||||
is followed by a `SetSliderLabel` (which writes children `0x1000021E`/`0x1000021F` — present
|
||||
only on template 6), and the one `AddSliderOption(..., 0)` call
|
||||
(`Input_MouseLookSensitivity`) is not.
|
||||
|
||||
`PlayerOptionPage::AddHeader` casts the created row to `0xC` (`UIElement_Text`) and writes
|
||||
`StringInfo::SetStringIDandTableEnum(&si, arg2, 0x10000003)`. **Table enum `0x10000003`
|
||||
resolves to StringTable DID `0x23000003`** — confirmed by hashing every `ID_*` label name
|
||||
below and finding it in that table. `SetToggleLabel` writes the same table for both label
|
||||
(`UIElement_Text::SetStringInfo`) and tooltip (`vtable+0x2B4`).
|
||||
|
||||
---
|
||||
|
||||
## 2. How the panel opens and closes (Q1c)
|
||||
|
||||
### 2.1 Keybind — action `0x1000001A` = `ToggleOptionsPanel`, default **F11**
|
||||
|
||||
- `docs/research/named-retail/keymap-default.txt:139` —
|
||||
`F11 (scan=0x57, dev=0) Action=0x1000001A Activation=0x03`
|
||||
- `docs/research/named-retail/retail-default.keymap.txt:148` —
|
||||
`ToggleOptionsPanel [ "" [ 0 DIK_F11 ] ]` under the `UICommands` group.
|
||||
|
||||
acdream's `KeyBindings.RetailDefaults()` already carries this row (F11 opens the current
|
||||
non-retail settings panel).
|
||||
|
||||
### 2.2 Toolbar button — `0x1000019B` in `LayoutDesc 0x21000016`
|
||||
|
||||
From the committed fixture `tests/AcDream.App.Tests/UI/Layout/fixtures/toolbar_21000016.json`:
|
||||
|
||||
```
|
||||
/0x10000191/0x1000019B T=0x00000001 (UIElement_Button) pos=(204,0) 34x27
|
||||
P0x12 = Enum 268435482 = 0x1000001A
|
||||
Normal media 0x0600051C
|
||||
Highlight media 0x0600051E
|
||||
```
|
||||
|
||||
Property `0x12` on a `UIElement_Button` is the **input-action enum the button fires** —
|
||||
already established by Campaign CH (`docs/research/2026-08-09-chat-retail-window-shell.md`
|
||||
lines 208-214, `UIElement_Button` reading its own `0x12` at `0x00471E65`). The dispatch is
|
||||
`UIElementManager::BroadcastGlobalMessage(inst, 1, <actionId>)` — global message **1 = "an
|
||||
input action fired"**, param = the action id (confirmed by `gmToolbarUI::ListenToGlobalMessage
|
||||
@0x004BE4E0`, which switches on `arg3` in `0x10000042…0x1000004D` to fire shortcut slots).
|
||||
|
||||
For orientation, the toolbar's full action row (same fixture):
|
||||
|
||||
| Element | x | Action `P0x12` |
|
||||
|---|---|---|
|
||||
| `0x10000197` | 55 | `0x1000000D` |
|
||||
| `0x10000198` | 85 | `0x10000010` |
|
||||
| `0x10000199` | 115 | `0x10000013` |
|
||||
| `0x1000055A` | 145 | `0x100001E7` |
|
||||
| `0x1000019A` | 175 | `0x10000016` |
|
||||
| **`0x1000019B`** | **204** | **`0x1000001A` — Options** |
|
||||
| `0x100001B1` | 238 | `0x10000019` |
|
||||
|
||||
### 2.3 Close — the same action
|
||||
|
||||
The tab-host close button `0x10000210` carries `P0x12 = 0x1000001A`, i.e. it fires the same
|
||||
`ToggleOptionsPanel` action. The tab-host root `0x10000208` carries `P0x57 = 0x1000001A`.
|
||||
Each page root carries its own `P0x57`: Gameplay `0x1000001B`, Character `0x1000001C`,
|
||||
Config `0x1000001D`, Chat `0x10000113`.
|
||||
|
||||
> **UNVERIFIED:** the exact semantics of property `0x57`. The correlation (host `0x57` ==
|
||||
> close button's `0x12`) makes "the toggle action this element registers itself under" the
|
||||
> obvious reading, but I did not locate the `GetAttribute_Enum(this, 0x57, …)` read site in
|
||||
> the decomp. Treat as strongly-indicated, not proven.
|
||||
|
||||
---
|
||||
|
||||
## 3. Apply / Reset / Defaults (Q3)
|
||||
|
||||
### 3.1 The three buttons
|
||||
|
||||
`gmCharacterSettingsUI::ListenToElementMessage @0x0049E3A0` — **the same address is bound to
|
||||
`gmChatOptionsUI::ListenToElementMessage` and `gmConfigUI::ListenToElementMessage`** in the
|
||||
PDB (identical-COMDAT folding; all three pages share one handler body):
|
||||
|
||||
```
|
||||
0049e3b5 if (idElement == 0x100001FC) this->vtable->SaveCurrentValues(); // APPLY
|
||||
0049e3b8 if (idElement == 0x100001FD) this->vtable->RestoreSavedValues(); // RESET
|
||||
0049e3bb if (idElement == 0x100001FE) this->vtable->RestoreDefaultValues(); // DEFAULTS
|
||||
```
|
||||
|
||||
Authored geometry (identical on all three pages that have them — Character `0x21000028`,
|
||||
Config `0x21000029`, Chat `0x2100005C`; all base `0x21000040/0x100002CD`, 80×32, y = 564):
|
||||
|
||||
| Element | x | Label |
|
||||
|---|---|---|
|
||||
| `0x100001FC` | 16 | **Apply** (`0x00A09C49` on Character, `0x09BA3C39` on Config/Chat — different StringIds, same literal) |
|
||||
| `0x100001FD` | 106 | **Reset** (`0x00D024B4` / `0x09CA84C4`) |
|
||||
| `0x100001FE` | 196 | **Defaults** (`0x051FC993` / `0x0F183833`) |
|
||||
|
||||
**The Gameplay Options page (`0x2100002A`) has no Apply/Reset/Defaults** — it is a pure
|
||||
button list. This matches the user's screenshot note ("Apply / Reset / Defaults at the bottom
|
||||
of (at least) the Character tab").
|
||||
|
||||
### 3.2 Per-tab, not global
|
||||
|
||||
Each page is its own `OptionPage` with its own `m_UIOptionArray`. Apply/Reset/Defaults act on
|
||||
**that page's registered options only**. The Character page's Apply does not touch the Config
|
||||
page's sliders.
|
||||
|
||||
### 3.3 What each one does
|
||||
|
||||
`OptionPage` (all three iterate `m_UIOptionArray` and `&=` the results, then call
|
||||
`this->vtable->OnOptionChanged(0)`):
|
||||
|
||||
| Button | `OptionPage` method | Per-option call | Gated on `Changed()`? |
|
||||
|---|---|---|---|
|
||||
| Apply | `SaveCurrentValues @0x004F2C60` | vtable `+0x2C8` = `SaveCurrentValue()` | **No** — every option |
|
||||
| Reset | `RestoreSavedValues @0x004F2D00` | vtable slot `RestoreSavedValue()` | **Yes** — only options whose `+0x2BC Changed()` is true |
|
||||
| Defaults | `RestoreDefaultValues @0x004F2CB0` | vtable `+0x2C4` = `RestoreDefaultValue()` | **No** — every option |
|
||||
|
||||
`OptionPage::Changed @0x004F2D60` returns true if **any** registered option's `Changed()` is true.
|
||||
|
||||
Leaf semantics (`UIOption_Checkbox`, the canonical case):
|
||||
|
||||
```
|
||||
Changed @0x004868C0 return m_saved != m_current;
|
||||
SaveCurrentValue @0x004868E0 m_current = GetValue(); m_saved = m_current; return 1;
|
||||
RestoreSavedValue @0x00486900 m_current = m_saved; Apply(0); Refresh();
|
||||
RestoreDefaultValue@0x00486930 m_current = m_default; Apply(0); Refresh();
|
||||
SetDefaultValue @0x00486960 m_default = arg2;
|
||||
SetCurrentValue @0x00486970 m_current = arg2; SetAttribute_Bool(this, 0xE, arg2); Apply(1);
|
||||
```
|
||||
|
||||
**The load-bearing consequence: `SetCurrentValue` — which is what the user's click on the LED
|
||||
runs — calls `Apply(1)` immediately.** The option takes effect in the live game the instant
|
||||
you click it. `Apply` is therefore *not* "now do the thing"; it is "commit this as the new
|
||||
undo baseline".
|
||||
|
||||
- **Apply** = `m_saved = m_current` per option ⇒ `Changed()` goes false ⇒ Reset can no longer
|
||||
undo. (Plus the blob flush, §3.5.)
|
||||
- **Reset** = `m_current = m_saved` and re-`Apply(0)` ⇒ reverts the live game state to the
|
||||
last committed baseline.
|
||||
- **Defaults** = `m_current = m_default` and `Apply(0)` ⇒ pushes the retail default into the
|
||||
live game state, but does **not** commit; `Changed()` becomes true, so Apply/Reset light up
|
||||
and Reset still undoes it. Defaults comes from `SetDefaultValue`, which is authored *in code*
|
||||
in each page's `InitOptions` (Config and Chat) or is UNKNOWN-defaulted (Character — see below).
|
||||
|
||||
### 3.4 Button enable/disable
|
||||
|
||||
`PlayerOptionPage::OnOptionChanged @0x004F27D0`:
|
||||
|
||||
```
|
||||
eax = GetChildRecursive(<the gm*UI element>, 0x100001FC); // Apply
|
||||
eax_1 = GetChildRecursive(<the gm*UI element>, 0x100001FD); // Reset
|
||||
if (OptionPage::Changed(...) == 0) { SetState(0x0D) on both } // disabled
|
||||
else { SetState(0x01) on both } // enabled
|
||||
```
|
||||
|
||||
**Defaults (`0x100001FE`) is never enable-gated — it is always clickable.**
|
||||
`PostInit` calls `InitOptions()` then `OnOptionChanged(0)` so the buttons start disabled.
|
||||
|
||||
`PlayerOptionPage::OnOptionChangeConfirmed @0x004F2850` calls back into the owning element
|
||||
when `arg3 != 0`.
|
||||
|
||||
### 3.5 What Apply *sends* — the character-options blob and its dirty timer
|
||||
|
||||
`PlayerOptionPage::SaveCurrentValues @0x004F2710` overrides the base and does **two** things:
|
||||
|
||||
```
|
||||
004f271d CPlayerModule::SaveToServer(&CPlayerSystem::GetPlayerSystem()->playerModule, 0);
|
||||
004f2725 return OptionPage::SaveCurrentValues(this); // tailcall
|
||||
```
|
||||
|
||||
`CPlayerModule::SaveToServer @0x0059A660`:
|
||||
|
||||
```
|
||||
if (m_bDirty != 0 || arg2 != 0)
|
||||
CM_Character::Event_CharacterOptionsEvent(&this->vtable); // 0x006A10C0 — the full blob
|
||||
m_bDirty = 0;
|
||||
```
|
||||
|
||||
`CM_Character::Event_CharacterOptionsEvent @0x006A10C0` is the full-blob
|
||||
`SetCharacterOptions` sender (**the `0x01A1` builder that CH3 deleted — exact field layout is
|
||||
Lane B / handoff Q4**).
|
||||
|
||||
The dirty flag is set by `CPlayerModule::OnChanged`:
|
||||
|
||||
- `OnChanged(BaseProperty*, uint32) @0x0059A890` — sends
|
||||
`CM_UI::SendNotice_GameplayOptionChanged`, then, **only if not already dirty**, sets
|
||||
`m_bDirty = 1` (offset `+0x1A8`) and stamps `m_timeFirstDirtied` (offset `+0x1AC`, double)
|
||||
with `Timer::cur_time`.
|
||||
- `OnChanged(PlayerOption) @0x0059A8E0` — sends `CM_UI::SendNotice_PlayerOptionChanged`,
|
||||
applies the immediate local side effects (case 4 → `SmartBox::EnableWeather`, case 5 →
|
||||
`LScape::SetDay`, case 7 → `ClientCombatSystem::TrackTarget`, case `0x30` →
|
||||
`LScape::m_fFogEnabled`, cases 2/`0x12` → the mutual exclusion between
|
||||
`IgnoreFellowshipRequests` and `FellowshipAutoAcceptRequests`), then:
|
||||
```
|
||||
0059a99d if (CPlayerModule::IsAutoSaveOption(this-4, arg2))
|
||||
0059a9ac CM_Character::Event_PlayerOptionChangedEvent(arg2, GetOption(this, arg2)); // SINGLE-option send
|
||||
0059a9c1 else { if (!dirty) { dirty = 1; m_timeFirstDirtied = Timer::cur_time; } }
|
||||
```
|
||||
**So retail has BOTH paths.** An option that `CPlayerModule::IsAutoSaveOption` returns true
|
||||
for is sent immediately as a single-option event (this is the `SetSingleCharacterOption
|
||||
0x0005` family acdream already implements). Everything else is batched into the blob.
|
||||
*Which options are auto-save is UNKNOWN here — enumerate `CPlayerModule::IsAutoSaveOption`
|
||||
in Lane B; it is the discriminator for "0x0005 per change vs one 0x01A1 blob".*
|
||||
|
||||
**Byte-verified auto-flush timer** — `CPlayerModule::UseTime @0x0059A710`:
|
||||
|
||||
```
|
||||
0059a713 mov al, [esi+0x1A8] ; m_bDirty
|
||||
0059a719 test al, al
|
||||
0059a71b jz 0059a749 ; not dirty -> return
|
||||
0059a71d fld qword [0x007E5358] ; constant
|
||||
0059a723 fadd qword [esi+0x1B0] ; + m_timeFirstDirtied
|
||||
0059a729 fcomp qword [0x008369A8] ; vs Timer::cur_time
|
||||
0059a72f fnstsw ax
|
||||
0059a731 test ah, 0x41
|
||||
0059a734 jp 0059a749 ; ST0 > cur_time (or unordered) -> skip
|
||||
0059a736 lea eax,[esi+4] / push / call CM_Character::Event_CharacterOptionsEvent
|
||||
0059a742 mov byte [esi+0x1A8], 0
|
||||
```
|
||||
|
||||
Raw bytes at `0x007E5358` = `00 00 00 00 00 00 7e 40` = **`480.0`**. So: *if any non-auto-save
|
||||
option is dirty, the blob is flushed automatically **480 seconds (8 minutes) after the first
|
||||
change**, measured from the first dirtying, not the last.*
|
||||
|
||||
**Other `SaveToServer` callers:** `CPlayerSystem::LogOffCharacter @0x00563520` calls
|
||||
`SaveToServer(&playerModule, 0)` before `ExecuteLogOff`. Those are the only two call sites
|
||||
in the binary.
|
||||
|
||||
### 3.6 Showing/hiding a page auto-applies / auto-reverts
|
||||
|
||||
`PlayerOptionPage::OnVisibilityChanged @0x004F26E0`:
|
||||
|
||||
```
|
||||
UIElement::OnVisibilityChanged(this, arg2);
|
||||
if (arg2 != 0) this->vtable->SaveCurrentValues(); // becoming visible -> APPLY
|
||||
else this->vtable->RestoreSavedValues(); // becoming hidden -> RESET
|
||||
```
|
||||
|
||||
**Switching tabs, or closing the Options window, silently reverts any uncommitted edits on
|
||||
the page you left.** This is a real, user-visible retail behaviour and must be ported, not
|
||||
"improved".
|
||||
|
||||
### 3.7 `UIOption::Apply` — the three write targets
|
||||
|
||||
`UIOption_Checkbox::Apply @0x00486C60` branches on how the option was bound:
|
||||
|
||||
1. **`m_propName != 0`** → gameplay-option property bag:
|
||||
`BaseProperty::SetPropertyName(&p, m_propName)`, set value = `m_current`, then
|
||||
`PlayerModule::SetOption(pm, &p)` (`0x005D52C0` — `HashTable::set` into
|
||||
`m_colGameplayOptions` then `OnChanged(prop, 0)`).
|
||||
2. **`m_prefName` path** → `UIPreferences::ModifyPreference(m_prefName, m_current)`, and on
|
||||
success `CM_UI::SendNotice_UserPreferenceChanged(m_prefName)`. This is the
|
||||
`UserPreferences.ini` path used by the whole Config tab. **Nothing goes over the wire.**
|
||||
*(BN renders the branch predicate as `m_playerOption == ~AutoRepeatAttack_PlayerOption`,
|
||||
which is a `not`/`cmp` artifact. The branch's BODY is unambiguous; the exact predicate is
|
||||
UNVERIFIED. It is almost certainly "no PlayerOption bound", i.e. the prefName binding.)*
|
||||
3. **else** → `PlayerModule::SetOption(pm, m_playerOption, m_current)` (`0x005D3EB0` — a
|
||||
switch over `enum PlayerOption` writing the `options_` word bits and calling
|
||||
`OnChanged(option)`). This is the Character tab path.
|
||||
|
||||
All three then call `this->vtable->HandleDialogAndNotices(arg2)`.
|
||||
|
||||
`UIOption::HandleDialogAndNotices @0x004EFB90`:
|
||||
|
||||
```
|
||||
if (m_confirmChange && arg2) UIOption::HandleDialog(this);
|
||||
if (m_shouldReloadOptions) CM_Character::SendNotice_ReloadOptions();
|
||||
if (m_shouldRefreshOptionsPanel) CM_UI::SendNotice_RefreshOptionsPanel();
|
||||
if (m_noticeFuncPtr) m_noticeFuncPtr();
|
||||
if (m_pOCH && arg2) m_pOCH->OnOptionChanged(this);
|
||||
```
|
||||
|
||||
So `Apply(1)` (user click) may raise the confirmation dialog and notifies the page (which
|
||||
re-runs `Changed()` and lights up Apply/Reset). `Apply(0)` (from Reset/Defaults) skips both —
|
||||
the page calls `OnOptionChanged(0)` itself afterwards.
|
||||
|
||||
`UIOption::SetConfirmChange` is used exactly once on the four tabs:
|
||||
`gmConfigUI::InitOptions @0x0049E5BF` sets it on the `Display_Resolution` menu.
|
||||
|
||||
`UIOption_CheckboxBitfield64::Apply @0x00485FF0` has the same shape but dispatches on
|
||||
`m_propType`:
|
||||
|
||||
```
|
||||
004860b3 if (m_propType == Normal_PropType) PlayerModule::SetOption(pm, &prop);
|
||||
004860c4 else if (m_propType == 1) PlayerModule::SetChatWindowOption(pm, m_nUserData, &prop);
|
||||
```
|
||||
|
||||
`m_nUserData` is set by `UIOption::SetUserData(option, <retail window id>)`. The other
|
||||
`SetChatWindowOption` call sites in the binary pass `this->m_eWindowID` — which independently
|
||||
confirms Campaign CH6's identity model (main = 8, floaties = 2..5) and confirms that the
|
||||
Chat tab's `SetUserData` argument *is* the retail window id.
|
||||
|
||||
---
|
||||
|
||||
## 4. The **Config** tab — complete row map (`gmConfigUI::InitOptions @0x0049E400`)
|
||||
|
||||
Six sections, **6 headers / 5 interior separators + 1 trailing / 24 option rows**. Bindings
|
||||
are `PStringBase<char>` preference-key globals whose names are `Section_Key`; they map
|
||||
one-to-one onto `UserPreferences.ini` sections (verified against the user's live retail
|
||||
`UserPreferences.ini`). All headers byte-verified from `mov eax,[global]; push eax` operands.
|
||||
|
||||
| # | Header global (VA) | StringId | Literal |
|
||||
|---|---|---|---|
|
||||
| 1 | `ID_Sound_SoundSection` (`0x0083E750`) | `0x09FB638E` | **Sound Options** |
|
||||
| 2 | `ID_Camera_CameraSection` (`0x0083E754`) | `0x08A8429E` | **Camera Options** |
|
||||
| 3 | `ID_Graphics_GraphicsSection` (`0x0083E758`) | `0x0D95A11E` | **Graphics Options** |
|
||||
| 4 | `ID_Graphics_TextureSection` (`0x0083E75C`) | `0x018580EE` | **Rendering Quality Options** |
|
||||
| 5 | `ID_Input_InputSection` (`0x0083E760`) | `0x01BAE38E` | **Input Options** |
|
||||
| 6 | `ID_UI_UISection` (`0x0083E764`) | `0x0D25AC1E` | **UI Options** |
|
||||
|
||||
| Section | # | Widget | Preference key | `SetDefaultValue` | Decomp VA |
|
||||
|---|---|---|---|---|---|
|
||||
| Sound Options | 1 | Menu (UIPreference) | `Sound_SoundFeatures` | `0` | `0x0049E418` |
|
||||
| | 2 | Toggle+Slider | `Sound_SoundDisabled` / `Sound_SoundVolume` | `1`, `0x3F800000` = 1.0f | `0x0049E435` |
|
||||
| | 3 | Toggle+Slider | `Sound_AmbientSoundDisabled` / `Sound_AmbientSoundVolume` | `1`, 1.0f | `0x0049E457` |
|
||||
| | 4 | Toggle+Slider | `Sound_InterfaceSoundDisabled` / `Sound_InterfaceSoundVolume` | `1`, 1.0f | `0x0049E479` |
|
||||
| | 5 | Toggle | `Sound_PlaySoundOnlyWhenActive` | `1` | `0x0049E496` |
|
||||
| Camera Options | 6 | Slider (labelled) | `Camera_Stiffness` | `0x3EE66666` = **0.45f** | `0x0049E4C4` |
|
||||
| | 7 | Slider (labelled) | `Camera_AdjustmentSpeed` | `0x42200000` = **40.0f** | `0x0049E501` |
|
||||
| | 8 | Slider (labelled) | `Render_FieldOfView` | `0x42B40000` = **90.0f** | `0x0049E53E` |
|
||||
| | 9 | Toggle | `Camera_AlignToSlope` | `1` | `0x0049E571` |
|
||||
| Graphics Options | 10 | Menu (**UserPreference**, `arg3 == 0`) | `Display_Resolution` | `0x03200258` = **800 × 600**; `SetConfirmChange(1)` | `0x0049E5A5` |
|
||||
| | 11 | Toggle | `Display_FullScreen` | `1` | `0x0049E5CB` |
|
||||
| | 12 | Toggle | `Display_SyncToRefresh` | `0` | `0x0049E5E3` |
|
||||
| | 13 | Slider (labelled) | `Render_ScreenBrightness` | `0` = 0.0f | `0x0049E5FD` |
|
||||
| | 14 | Toggle | `Render_AutomaticDegrades` | `0` | `0x0049E62C` |
|
||||
| | 15 | Slider (labelled) | `Render_GraphicsPerformance` | `0` = 0.0f | `0x0049E646` |
|
||||
| | 16 | Slider (labelled) | `Render_DegradeDistance` | `0x42480000` = **50.0f** | `0x0049E677` |
|
||||
| Rendering Quality | 17 | Menu | `Render_LandscapeTextureDetail` | `2` | `0x0049E6BF` |
|
||||
| | 18 | Menu | `Render_EnvironmentTextureDetail` | `1` | `0x0049E6D9` |
|
||||
| | 19 | Menu | `Render_TextureFiltering` | `1` | `0x0049E6F3` |
|
||||
| | 20 | Menu | `Render_LandscapeDrawDistance` | `8` | `0x0049E70D` |
|
||||
| | 21 | Toggle | `Render_BuildingDetailTextures` | `1` | `0x0049E725` |
|
||||
| | 22 | Toggle | `Render_MultiPassAlpha` | `0` | `0x0049E73D` |
|
||||
| Input Options | 23 | Slider (**unlabelled**, `arg3 == 0`) | `Input_MouseLookSensitivity` | `0x3F0CCCCD` = **0.55f** | `0x0049E76B` |
|
||||
| | 24 | Toggle | `Input_InvertMouseLookYAxis` | `0` | `0x0049E78C` |
|
||||
| | 25 | Toggle | `Input_UseMouseTurning` | `0` | `0x0049E7AA` |
|
||||
| UI Options | 26 | Menu | `UI_ChatFontFace` | `2` | `0x0049E7DE` |
|
||||
| | 27 | Menu | `UI_ChatFontSize` | `1` | `0x0049E7F8` |
|
||||
|
||||
*(27 `Add*` calls; the three toggle+slider rows are one widget each, so the visible row count
|
||||
is 27.)*
|
||||
|
||||
**Cross-validated against the user's live retail `UserPreferences.ini`:** `Camera Stiffness =
|
||||
0.45`, `AdjustmentSpeed = 40.00`, `FieldOfView = 90.00`, `SoundVolume/AmbientSoundVolume/
|
||||
InterfaceSoundVolume = 1.00`, `ScreenBrightness = 0.00`. Every decomp default that the ini
|
||||
still holds at its default value matches exactly.
|
||||
|
||||
Slider range labels come from `ID_Graphics_Value_*` globals at `0x0083E768`–`0x0083E794`:
|
||||
`Dark/Bright`, `Speed/Detail`, `Close/Far`, `Narrow/Wide`, `Slow/Fast`, `Soft/Hard`
|
||||
(six labelled sliders, six label pairs — the pairing order is UNVERIFIED; the exact
|
||||
`SetSliderLabel` operand pairs would need the same push-imm decode I ran on `AddHeader`).
|
||||
|
||||
`gmConfigUI` extras:
|
||||
- `PostInit` also `RegisterNoticeHandler(0x4DD276, …)` and
|
||||
`UIListener::RegisterForGlobalMessage(this, 0x0C)`.
|
||||
- `ListenToGlobalMessage @0x0049EB90`: `if (arg2 == 0x0C) SetMouseTurningDefaults(this)`.
|
||||
- `SetMouseTurningDefaults @0x0049E8F0` clamps `m_pStiffness` to `0.95f` (when below) and
|
||||
prints a chat line (`"Camera Stiffness was changed fro…"`), plus the same treatment on
|
||||
`m_pAdjustmentSpeed`, `m_pSensitivity`, `m_pInvert`, `m_pMouseTurn`, `m_pSlope`. Exact
|
||||
clamp set is out of Lane A scope.
|
||||
- `RecvNotice_UserPreferenceChanged_Menu @0x0049EBA0` reacts to
|
||||
`Render_LandscapeTextureDetail` changes.
|
||||
|
||||
**Not on the 2013 Config tab, though present in `UserPreferences.ini`:**
|
||||
`Render_SceneryDrawDistance`, `Render_DisplayAdapter`, `Render_AspectRatio`,
|
||||
`Render_LandscapeDetailTextures`, `Display_RefreshRate`, `Net_*`, `Misc_TooltipDelay`,
|
||||
`Misc_TooltipEnable` (globals exist at `0x0083EAA4`/`0x0083EAA8` and strings "Tooltip Delay"
|
||||
exists in table `0x23000003`), `International_UseIME`, `Input_MouseLookSmoothingAmount`,
|
||||
`Input_KeymapFile`. Those are either the external config/launcher's surface or a later
|
||||
build's rows. **UNKNOWN which.**
|
||||
|
||||
---
|
||||
|
||||
## 5. The **Chat** tab — complete row map (`gmChatOptionsUI::InitOptions @0x0049FC60`)
|
||||
|
||||
Six sections. Headers byte-verified from the `mov <reg>,[global]` operands:
|
||||
|
||||
| # | Header global (VA) | Section content |
|
||||
|---|---|---|
|
||||
| 1 | `ID_ChatOption_GeneralOptions_Section` (`0x0083E810`) | the two opacity sliders |
|
||||
| 2 | `ID_ChatOption_MainChatWindow_Section` (`0x0083E814`) | filter block, `SetUserData(8)` |
|
||||
| 3 | `ID_ChatOption_FloatyChatWindow1_Section` (`0x0083E818`) | filter block, `SetUserData(2)` |
|
||||
| 4 | `ID_ChatOption_FloatyChatWindow2_Section` (`0x0083E81C`) | filter block, `SetUserData(3)` |
|
||||
| 5 | `ID_ChatOption_FloatyChatWindow3_Section` (`0x0083E820`) | filter block, `SetUserData(4)` |
|
||||
| 6 | `ID_ChatOption_FloatyChatWindow4_Section` (`0x0083E824`) | filter block, `SetUserData(5)` |
|
||||
|
||||
*(Table `0x23000003` also holds "Chat Window 1"…"Chat Window 4" at `0x0DC3B9FE`/`0x0DC3B8FE`/
|
||||
`0x0DC3B7FE`/`0x0DC3B6FE`; the six headers above are the ones actually pushed.)*
|
||||
|
||||
### 5.1 General Options — two linked opacity sliders
|
||||
|
||||
| Slider | Template idx | Gameplay option property | Notes |
|
||||
|---|---|---|---|
|
||||
| 1 | **3** (unlabelled row) | `0x10000080` `Option_DefaultOpacity_Property` | `UIOption_Slider::SetGameplayOptionProperty @0x0049FCC0` |
|
||||
| 2 | **6** (labelled row) | `0x10000081` `Option_ActiveOpacity_Property` | `@0x0049FD1A`; then `SetSliderLabel(this, slider2, <min>, <max>)` |
|
||||
|
||||
Range labels come from `ID_UI_Value_Transparent` (`0x0083E828`) and `ID_UI_Value_Opaque`
|
||||
(`0x0083E82C`). The two sliders are then **linked**:
|
||||
|
||||
```
|
||||
0049fd5c DualHash<UIOption_Slider*,UIOption_Slider*>::add(&this->m_hashSliderLinks, &slider1, &slider2);
|
||||
```
|
||||
|
||||
which is the "linking DRAGS the other value (never clamps)" behaviour CH6c already ported.
|
||||
|
||||
### 5.2 The five per-window text-type filter blocks
|
||||
|
||||
Each block is one `UIOption_CheckboxBitfield64` created from **template index 8**
|
||||
(`0x10000520`, `T=0x10000044`, 272×100), bound with
|
||||
`SetGameplayOptionProperty(opt, 0x1000007F, Chat_PropType)` (`Chat_PropType == 1`, so
|
||||
`Apply` routes through `PlayerModule::SetChatWindowOption(pm, m_nUserData, prop)`),
|
||||
`SetUserData(<windowId>)`, `SetDefaultValue(<low>, <high>)`, then N `AddChild(lowMask,
|
||||
highMask, labelId, tooltipId)` and one `CreateChildren`.
|
||||
|
||||
Windows 2/3/4 go through the shared helper
|
||||
`gmChatOptionsUI::AddCheckboxBitfield64Option @0x0049EDA0`; windows 8 and 5 are written
|
||||
inline in `InitOptions` (the helper is inlined at those two sites — the helper's own switch
|
||||
covers cases 2/3/4/5/8, so the semantics are identical).
|
||||
|
||||
**Defaults, byte-verified from the helper's switch and the inline blocks:**
|
||||
|
||||
| Retail window id | Default filter (high:low) | Site |
|
||||
|---|---|---|
|
||||
| **8** (main) | `0x00000000_FBFFFFFF` | `0x0049FDD1` / helper case 8 `0x0049EE16` |
|
||||
| **2** (floaty 1) | `0x00000000_0000101C` | helper case 2 `0x0049EE2B` |
|
||||
| **3** (floaty 2) | `0x00000000_00040C00` | helper case 3 `0x0049EE34` |
|
||||
| **4** (floaty 3) | `0x00000000_00080000` | helper case 4 `0x0049EE3D` |
|
||||
| **5** (floaty 4) | `0x00000000_78000000` | helper case 5 `0x0049EE46` / inline `0x004A0010` |
|
||||
|
||||
The main window's default `0xFBFFFFFF` matches CH6's finding exactly (Society bit `0x20…`
|
||||
opt-in).
|
||||
|
||||
**The checkbox rows** — byte-decoded from the `mov <reg>,[global]; push; push; push high;
|
||||
push low; call AddChild` sequences (`decode_addchild.py` over the raw section bytes; masks
|
||||
are 64-bit, `high:low`):
|
||||
|
||||
| Row | Mask (high:low) | Label global | Label literal (table `0x23000003`) |
|
||||
|---|---|---|---|
|
||||
| **0** | `0x00000000_83912021` | `ID_ChatOption_TextFilter_Gameplay` (`0x0083E840`) | Gameplay |
|
||||
| 1 | `0x00000000_00600040` | `ID_ChatOption_TextFilter_Combat` (`0x0083E830`) | Combat |
|
||||
| 2 | `0x00000000_00020080` | `ID_ChatOption_TextFilter_Magic` (`0x0083E848`) | Magic |
|
||||
| 3 | `0x00000000_00001004` | `ID_ChatOption_TextFilter_AreaSpeech` (`0x0083E838`) | Area Speech |
|
||||
| 4 | `0x00000000_00000018` | `ID_ChatOption_TextFilter_Tells` (`0x0083E850`) | Tells |
|
||||
| 5 | `0x00000000_00040C00` | `ID_ChatOption_TextFilter_Allegience` (`0x0083E858`) | Allegiance *(retail's own misspelling in the symbol)* |
|
||||
| 6 | `0x00000000_00080000` | `ID_ChatOption_TextFilter_Fellowship` (`0x0083E860`) | Fellowship |
|
||||
| 7 | `0x00000000_08000000` | `ID_ChatOption_TextFilter_General` (`0x0083E868`) | General |
|
||||
| 8 | `0x00000000_10000000` | `ID_ChatOption_TextFilter_Trade` (`0x0083E870`) | Trade |
|
||||
| 9 | `0x00000000_20000000` | `ID_ChatOption_TextFilter_LFG` (`0x0083E878`) | LFG |
|
||||
| 10 | `0x00000000_40000000` | `ID_ChatOption_TextFilter_Roleplay` (`0x0083E880`) | Roleplay |
|
||||
| 11 | `0x00000001_00000000` | `ID_ChatOption_TextFilter_Society` (`0x0083E888`) | Society |
|
||||
| 12 | `0x00000000_04000000` | `ID_ChatOption_TextFilter_Error` (`0x0083E890`) | Error |
|
||||
|
||||
Every row's tooltip is the matching `…_Desc` global at label+4.
|
||||
|
||||
**Row 0 ("Gameplay") is present on windows 2/3/4/5 and ABSENT on window 8 (main).** Verified
|
||||
two ways: the inline main-window block at `0x0049FDF6`–`0x0049FF1C` emits exactly 12
|
||||
`AddChild` calls starting at Combat, while the inline window-5 block at `0x004A002D`–
|
||||
`0x004A016F` emits 13 starting at Gameplay; and in the helper, `case 8` breaks past the
|
||||
Gameplay `AddChild` at `0x0049EE6C` while cases 2/3/4/5 fall through the shared
|
||||
`label_49EE4F` into it. **Retail does not let you switch gameplay text off in the main chat
|
||||
window.**
|
||||
|
||||
So the Chat tab is: **2 sliders + 5 blocks × (12 or 13) checkboxes = 2 + 12 + 13×4 = 66
|
||||
interactive controls**, in 6 sections.
|
||||
|
||||
---
|
||||
|
||||
## 6. The **Gameplay Options** tab — complete map (`LayoutDesc 0x2100002A`)
|
||||
|
||||
Seven buttons, all base `0x21000040/0x10000618`, all 240×32 at x = 26, no LEDs, no
|
||||
Apply/Reset/Defaults. Handler: `gmGameplayOptionsUI::ListenToElementMessage @0x0049E110`
|
||||
(only reacts to `idMessage == 1`).
|
||||
|
||||
| y | Element | Label (table `0x23000001`) | What retail does |
|
||||
|---|---|---|---|
|
||||
| 20 | `0x10000203` | `0x071D9AC4` **Exit to Character Selection** | `CM_UI::SendNotice_EndCharacterSession(1)` @ `0x0049E1D0` |
|
||||
| 60 | `0x10000617` | `0x0BB75264` **Exit Game** | `BroadcastGlobalMessage(inst, 1, 0x10000027)` @ `0x0049E298`–`0x0049E2AB` — fires input action `0x10000027` |
|
||||
| 110 | `0x10000204` | `0x0694E944` **Configure Keyboard** | *not handled in C++*; authored `P0x12 = enum 0x1000001F` → the button self-fires action `0x1000001F` |
|
||||
| 150 | `0x100005CC` | `0x0A35EE57` **Use Mouse Turning Settings** | `BroadcastGlobalMessage(inst, 0x0C, 0)` @ `0x0049E2A1`–`0x0049E2AB` → `gmConfigUI::SetMouseTurningDefaults` |
|
||||
| 190 | `0x10000205` | `0x0BB03250` **In-Game Help Files** | *not handled in C++*; authored `P0x12 = enum 123 (0x7B)` = the `ToggleHelp` action (F1 in `keymap-default.txt:132`) |
|
||||
| 240 | `0x10000206` | `0x0145FCA5` **Urgent Assistance** | `ShellExecuteA(nullptr,"open", "http://support.turbine.com/ics/s…", …)` @ `0x0049E154`–`0x0049E16B`; on failure `MessageBoxA(…, "Asheron's Call Error", MB_OK)` |
|
||||
| 280 | `0x10000207` | `0x02F2FD45` **Report Abuse** | same `ShellExecuteA` shape @ `0x0049E1F0`–`0x0049E207`, different URL |
|
||||
|
||||
The button-branch dispatch is byte-verified (`0x0049E28C`: `sub eax,0x100005CC; jz →
|
||||
push 0 / push 0x0C; sub eax,0x4B; jnz → fall through; push 0x10000027 / push 1` —
|
||||
`0x100005CC + 0x4B == 0x10000617` ✓).
|
||||
|
||||
**Neither "Urgent Assistance" nor "Report Abuse" sends a game message.** Both are external
|
||||
browser launches. The two URLs are `support.turbine.com` support-ticket links (both truncated
|
||||
at BN's ~33-char preview; the full strings were not extracted because they are dead links
|
||||
today and acdream will not open them — **flagged for the planner as a deliberate divergence
|
||||
candidate**).
|
||||
|
||||
**UNKNOWN / UNVERIFIED:**
|
||||
- Action `0x10000027` ("Exit Game") — no default keybind, and I did not identify its
|
||||
consumer. That it means "quit" rests on the button's authored label only.
|
||||
- Action `0x1000001F` ("Configure Keyboard") — no default keybind. Strongly corroborated:
|
||||
the Configure Keyboard screen's own **OK** (`0x1000002C`) and **Cancel** (`0x1000002D`)
|
||||
buttons *also* carry `P0x12 = 0x1000001F`, which is exactly the toggle-window pattern.
|
||||
|
||||
---
|
||||
|
||||
## 7. The **Character** tab — structural inventory (full option map is Lane B / Q2)
|
||||
|
||||
`gmCharacterSettingsUI::InitOptions @0x004A02F0` — **6 headers, 5 interior separators + 1
|
||||
trailing, 49 toggle rows, no sliders, no menus.** Every row is
|
||||
`SetToggleLabel(this, AddToggleOption(this, <PlayerOption>), ID_PlayerOption_X,
|
||||
ID_PlayerOption_X_Help)` (the `enum PlayerOption` overload, `0x004F2A00`), so the tab is
|
||||
homogeneous.
|
||||
|
||||
| Header global (VA) | StringId | Literal | Rows |
|
||||
|---|---|---|---|
|
||||
| `ID_CharacterOption_UIBehavior_Section` (`0x0083EA5C`) | `0x06489B6E` | User Interface Behavior | **3** |
|
||||
| `ID_CharacterOption_UIDisplay_Section` (`0x0083EA60`) | `0x0A9BC99E` | User Interface Display | **15** |
|
||||
| `ID_CharacterOption_Grouping_Section` (`0x0083EA64`) | `0x0CBAAFAE` | Grouping | **6** |
|
||||
| `ID_CharacterOption_OtherPlayers_Section` (`0x0083EA68`) | `0x0872DFFE` | Other Players | **11** |
|
||||
| `ID_CharacterOption_CharacterBehavior_Section` (`0x0083EA6C`) | `0x08674D5E` | Character Behavior | **7** |
|
||||
| `ID_CharacterOption_Chat_Section` (`0x0083EA70`) | `0x0987FE8E` | Chat | **7** |
|
||||
|
||||
All six header globals byte-verified from their `mov eax,[0x0083EA5C+4n]; push eax` operands
|
||||
at `0x004A02EA`, `0x004A0431`, `0x004A0ABF`, `0x004A0D63`, `0x004A122A`, `0x004A154B`, and all
|
||||
six hashes verified name→`compute_str_hash`→string-table entry.
|
||||
|
||||
Rows in authored order (the `PlayerOption` enumerator names, for Lane B to bind):
|
||||
|
||||
1. `ViewCombatTarget`, `SalvageMultiple`, `MainPackPreferred`
|
||||
2. `VividTargetingIndicator`, `ShowTooltips`, `CoordinatesOnRadar`, `SideBySideVitals`,
|
||||
`SpellDuration`, `DisableMostWeatherEffects`, `DisableDistanceFog`, `PersistentAtDay`,
|
||||
`DisableHouseRestrictionEffects`, `UseCraftSuccessDialog`, `ConfirmVolatileRareUse`,
|
||||
`DisplayTimeStamps`, `FilterLanguage`, `ShowHelm`, `ShowCloak`
|
||||
3. `IgnoreAllegianceRequests`, `IgnoreFellowshipRequests`,
|
||||
`DisplayAllegianceLogonNotifications`, `FellowshipShareXP`, `FellowshipShareLoot`,
|
||||
`FellowshipAutoAcceptRequests`
|
||||
4. `AcceptLootPermits`, `UseDeception`, `AllowGive`, `IgnoreTradeRequests`,
|
||||
`DragItemOnPlayerOpensSecureTrade`, `DisplayDateOfBirth`, `DisplayAge`, `DisplayChessRank`,
|
||||
`DisplayFishingSkill`, `DisplayNumberDeaths`, `DisplayNumberCharacterTitles`
|
||||
5. `ToggleRun`, `AdvancedCombatUI`, `AutoTarget`, `AutoRepeatAttack`, `UseChargeAttack`,
|
||||
`LeadMissileTargets`, `UseFastMissiles`
|
||||
6. `StayInChatMode`, `HearAllegianceChat`, `HearGeneralChat`, `HearTradeChat`, `HearLFGChat`,
|
||||
`HearRoleplayChat`, `HearSocietyChat`
|
||||
|
||||
This is a **1:1 match with the user's transcribed screenshots — with exactly one exception.**
|
||||
|
||||
> ### ⚠ Build-version divergence: "Listen to PK death messages"
|
||||
> The user transcribed an 8th row in the Chat group. The **2013 EoR build's
|
||||
> `gmCharacterSettingsUI::InitOptions` has only 7** — it ends at `HearSocietyChat` and then
|
||||
> `AddSeperator`. However, StringTable `0x23000003` **does** contain
|
||||
> `0x0D16E9A3 = "Listen to PK death messages."`, and
|
||||
> `compute_str_hash("ID_PlayerOption_HearPKDeaths") == 0x0D16E9A3` exactly. There is **no**
|
||||
> `HearPKDeath*` reference anywhere in the 2013 pseudo-C.
|
||||
>
|
||||
> **Conclusion:** the row was added after the Sept 2013 build; the user's screenshot is from a
|
||||
> later client. The installed DATs carry the string, so acdream can render it, and ACE
|
||||
> exposes the matching option. **Lane B must decide whether acdream ships 49 or 50 rows** —
|
||||
> this is a real fork, not a transcription error.
|
||||
|
||||
Note also `ID_PlayerOption_ShowHelm` resolves differently in the two tables (`0x23000003`:
|
||||
"Show Your Helm or Head Gear" = the label; `0x23000005`: "Your helmet or head gear is
|
||||
visible." = a status line). The options UI uses table enum `0x10000003` → `0x23000003`.
|
||||
|
||||
**No `SetDefaultValue` call appears anywhere in `gmCharacterSettingsUI::InitOptions`** — so
|
||||
every Character-tab checkbox's `m_default` stays at whatever `UIOption_Checkbox`'s constructor
|
||||
leaves it. That means the **Defaults** button on the Character tab does *not* restore ACE's
|
||||
`CharacterOptions1.Default = 0x50C4A54A`; its behaviour is UNVERIFIED and must be established
|
||||
before implementing (read `UIOption_Checkbox`'s ctor, or `UIOption::InqDefaultGameplayOptionProperty
|
||||
@ symbols.json`). **This is the single biggest open unknown in the Apply/Reset/Defaults
|
||||
story.**
|
||||
|
||||
---
|
||||
|
||||
## 8. Configure Keyboard (pre-work for handoff Q5)
|
||||
|
||||
`LayoutDesc 0x21000009` — a **full-screen 800×600** layout (root `0x1000001F`, `T=3`),
|
||||
frame `0x10000451` (600×476 @ 100,62, base `0x21000042/0x100002B0`), containing
|
||||
`0x10000020` `T=0x1000000E` = `gmKeyboardUI`, 590×462 @ (5,7).
|
||||
|
||||
| Element | Geometry | Content |
|
||||
|---|---|---|
|
||||
| `0x1000049B` | 586×388, `T=8` (TabControl) | 6 tabs: **Movement** `0x05330A42`, **Camera** `0x0F5EE5E2`, **Combat** `0x0F1E0AC2`, **UI** `0x06FF16E2`, **Emotes** `0x0A3A04A2`, **CharacterSettings** `0x03765382` |
|
||||
| `0x100004A7` | 586×363 | the binding grid: column headers **Command** `0x0FDD849C`, **Mapping 1** `0x0D11C1CC`, **Mapping 2** `0x0D41C1CC`, **Mapping 3** `0x0D31C1CC`; ListBox `0x10000025` (570×345, `T=5`) + scrollbar `0x10000026` |
|
||||
| `0x1000002F` | 570×40, `T=0x10000034` | the `UIOption_ActionKeyMap` row template |
|
||||
| `0x10000027` | (50,390) 100×32 | **Load File…** `0x06E9046E` |
|
||||
| `0x10000028` | (150,390) 290×32 | current keymap filename label |
|
||||
| `0x10000029` | (440,390) 100×32 | **Save As…** `0x048840AE` |
|
||||
| `0x1000002A` | (0,430) 100×32 | **Defaults** `0x039D522E` |
|
||||
| `0x1000002B` | (100,430) 100×32 | **Revert** `0x077FC0AE` |
|
||||
| `0x1000002C` | (390,430) 100×32 | **OK** `0x0055D40E`, `P0x12 = 0x1000001F` |
|
||||
| `0x1000002D` | (490,430) 100×32 | **Cancel** `0x08653BBE`, `P0x12 = 0x1000001F` |
|
||||
|
||||
Supporting decomp: `gmKeyboardUI::AddActionKeyMap 0x004DB2F0`, `GetKeymapDirectory 0x004DA8E0`,
|
||||
`GetStringInfoFromInputMapID 0x004DA980`, `SaveKeymap 0x004DCF90` (appends `.keymap` when the
|
||||
extension is missing), `MakeLoadKeymapDialog 0x004DC0B0`, `MakeSaveKeymapDialog 0x004DC5B0`,
|
||||
`MakeOverwriteKeymapDialog 0x004DCA20`, `MakeCantOverwriteReadOnlyKeymapDialog 0x004DC7B0`,
|
||||
`RestoreDefaultValues 0x004DA850`, `UpdateKeymapFilenameLabel 0x004DB290`, and the
|
||||
`UIOption_ActionKeyMap` family (`InitiateBinding`, `SetBinding`, `EraseBinding`,
|
||||
`ClearAllBindings`, `KeyHitHandler`, `OpenOverwriteBindingDialog`, `OpenMapWarnDialog`,
|
||||
`SetMouseTurningDefaults`). Storage is the on-disk `.keymap` file named by
|
||||
`[Input] KeymapFile` in `UserPreferences.ini` — **nothing keyboard-related goes over the
|
||||
wire**. The user's own DAT dir currently holds `acclient.keymap` and `test.keymap`.
|
||||
|
||||
**Note the retail keyboard screen is NOT one of the four Options tabs** — it is a separate
|
||||
800×600 modal screen reached by the "Configure Keyboard" button.
|
||||
|
||||
---
|
||||
|
||||
## 9. Binary-Ninja artifacts encountered (add to the DO-NOT-RETRY class list)
|
||||
|
||||
1. **`AddHeader(this, 0)` is never literally 0.** BN prints the argument as `0` because it is
|
||||
`mov eax, [<global>]; push eax`. The real values are the twelve
|
||||
`ID_*_Section` string-hash globals at `0x0083E750`+, `0x0083E810`+, `0x0083EA5C`+.
|
||||
Every header in this document was recovered from the raw operand bytes.
|
||||
2. **`AddChild(..., 0, 0)` likewise drops the label/tooltip operands.** They too are
|
||||
`mov <reg>,[<global>]; push <reg>` pairs. All 13 Chat-filter labels came from a
|
||||
push-operand decode, not from the pseudo-C.
|
||||
3. **`(((eax - eax) & 3) + 3)`** in `AddSliderOption` is the `neg`/`sbb` zero-idiom class
|
||||
already in the chat digest. Real result: `arg3 ? 6 : 3`.
|
||||
4. **Identical-COMDAT folding aliases symbols.** `gmCharacterSettingsUI::ListenToElementMessage`,
|
||||
`gmChatOptionsUI::ListenToElementMessage`, and `gmConfigUI::ListenToElementMessage` all
|
||||
resolve to `0x0049E3A0`; `gmGameplayOptionsUI::ListenToGlobalMessage` and
|
||||
`gmKeyboardUI::ListenToGlobalMessage` both to `0x004F5860`; `gmGameplayOptionsUI::PostInit`
|
||||
is listed at `0x004BFA00`, which the pseudo-C prints as `gmCGProfessionPage::PostInit`.
|
||||
**A shared address does not mean the class actually implements that behaviour** — verify
|
||||
the body is generic before attributing it.
|
||||
5. **BN field-name mush.** `CPlayerModule::OnChanged` writes what BN calls
|
||||
`this->m_TimeStampFormat.m_buffer`; the byte-level truth is `mov byte [esi+0x1A8], 1` —
|
||||
the `m_bDirty` flag that `UseTime` reads. Trust the offsets, not the names.
|
||||
6. **`m_playerOption == ~AutoRepeatAttack_PlayerOption`** in `UIOption_Checkbox::Apply` is a
|
||||
rendering of a `not`/`cmp` pair. The branch body is unambiguous (the UIPreferences path);
|
||||
the predicate is not.
|
||||
|
||||
---
|
||||
|
||||
## 10. For the planner
|
||||
|
||||
### 10.1 Structural inventory in one place
|
||||
|
||||
| Thing | Value |
|
||||
|---|---|
|
||||
| Options panel LayoutDesc | **`0x2100002B`** (`client_local_English.dat`) |
|
||||
| Tab-control root element | `0x10000208`, `Type 8`, authored 300 × 600 |
|
||||
| Tab table property | `0x2E` = array of Structs `{0x30 = button id, 0x31 = page id, 0x32 = isDefault}` |
|
||||
| Tab buttons (L→R) | `0x1000020D` Gameplay(default) · `0x1000020E` Character · `0x1000050B` Chat · `0x1000020F` Config |
|
||||
| Close button | `0x10000210`, fires action `0x1000001A` |
|
||||
| Page slots | `0x10000212` · `0x10000211` · `0x1000050C` · `0x10000213`, all (2,25) 298×575 |
|
||||
| Page layouts | `0x2100002A` · `0x21000028` · `0x2100005C` · `0x21000029` |
|
||||
| Page classes | `0x10000029` · `0x10000027` · `0x10000042` · `0x10000028` |
|
||||
| Option ListBoxes | `—` · `0x100001FA` · `0x1000050D` · `0x10000200` (all 276 × 560, `T=5`) |
|
||||
| Scrollbars | `—` · `0x100001FB` · `0x10000201` · `0x10000201` (16 × 560; ListBox `P0x72` names it) |
|
||||
| Row template list | ListBox property `0x64`, entries `{0x63 = layout DID, 0x62 = element id}` |
|
||||
| Row templates (all in `0x2100002B`) | header `0x10000216` · separator `0x10000217` · toggle `0x10000218` · slider `0x1000021A`/`0x1000021D` · menu `0x10000222` · toggle+slider `0x10000220`/`0x10000221` · bitfield64 `0x10000520` |
|
||||
| Apply / Reset / Defaults | `0x100001FC` / `0x100001FD` / `0x100001FE`, 80×32 at y 564 |
|
||||
| Host windows | `0x21000017` (`gmPanelUI`, docked) and `0x2100006E` (`gmFloatyPanelUI`, floating + resizable), slot `0x1000018D`, stack key `P0x10000029 = 10` |
|
||||
| Open action | `0x1000001A` `ToggleOptionsPanel`, default **F11**; toolbar button `0x1000019B` in `0x21000016` |
|
||||
| Row counts | Gameplay **7 buttons** · Character **6 headers / 49 toggles** · Chat **6 headers / 2 sliders / 5 bitfield blocks (12+13+13+13+13 = 64 checkboxes)** · Config **6 headers / 27 rows** |
|
||||
| String tables | tab/button labels → `0x23000001`; option labels + tooltips + section headers → `0x23000003` (table enum `0x10000003`) |
|
||||
|
||||
### 10.2 What acdream can reuse from CH6 as-is
|
||||
|
||||
- **`LayoutImporter`** already handles the exact mechanisms this panel needs:
|
||||
`BaseLayoutId`/`BaseElement` cross-layout inheritance (every page mounts that way), the
|
||||
`ElementDesc::Incorporate` child merge, `ShouldMountBaseChildren` + the ZLevel correction
|
||||
for pure-container sub-window mounts (the four page slots are exactly that shape), and the
|
||||
parent-resize policy that scales a 300×600 page into a 300×362 slot.
|
||||
- **`RetailWindowManager` / `UiRoot` / the floating-window shell** — the Options panel's real
|
||||
host `0x2100006E` (`gmFloatyPanelUI`) is the same 8-border-strip + resize-grip frame CH6
|
||||
already imports for the chat windows. Opacity, all-corner resize and drag come free.
|
||||
- **`RetailLayoutFixtureGenerator`** — add `0x2100002B`, `0x21000028`, `0x21000029`,
|
||||
`0x2100002A`, `0x2100005C` and pin them, exactly as CH6 pinned `0x2100006F`/`0x2100005B`.
|
||||
- **`DatWidgetFactory` already maps `0x10000035` → `BuildCheckbox`** — the LED option row's
|
||||
leaf widget class exists today.
|
||||
- **`DatStringResolver.ComputeHash`** is already the exact `compute_str_hash`, so every
|
||||
`ID_PlayerOption_*` / `ID_ChatOption_*` / `ID_*_Section` label in this doc resolves at
|
||||
runtime by name with no hard-coded ids.
|
||||
- **CH6's window-identity model** (main = 8, floaties = 2–5) is *confirmed* by the Chat tab's
|
||||
`SetUserData` arguments — no rework needed there.
|
||||
- **`RuntimeCharacterOptionsState` + the `0x0005` codec** (CH3) is the right seam for the
|
||||
Character tab; the `0x1000008C` per-window blob (CH6) is the right seam for the Chat tab's
|
||||
filter blocks.
|
||||
|
||||
### 10.3 What acdream does NOT have yet
|
||||
|
||||
- **Element `Type 8` — the tab control.** `DatWidgetFactory` has no case for it (falls to the
|
||||
generic `UiDatElement`), and no reader for property `0x2E`'s struct array. This is the one
|
||||
genuinely new widget the campaign needs. It is also needed by the Configure Keyboard screen.
|
||||
- **Element `Type 5` — `UIElement_ListBox` with an authored template list** (property `0x64`)
|
||||
and `AddItemFromTemplateList`. Also absent; also the mechanism that makes the whole options
|
||||
page work.
|
||||
- `UIOption_Slider` (`0x10000037`), `UIOption_Menu` (`0x10000038`),
|
||||
`UIOption_CheckboxSlider` (`0x10000036`), `UIOption_CheckboxBitfield64` (`0x10000044`) —
|
||||
none mapped in `DatWidgetFactory`.
|
||||
- The `OptionPage` / `PlayerOptionPage` **model**: a per-page registered-option array with
|
||||
`m_current` / `m_saved` / `m_default` triples and the four verbs. This is small and
|
||||
self-contained; it is what makes Apply/Reset/Defaults correct rather than approximated.
|
||||
- A `UserPreferences.ini`-shaped preference store for the Config tab (acdream's settings live
|
||||
in `%LOCALAPPDATA%\acdream\`; the Config tab's 27 rows are all `Section_Key` preferences,
|
||||
not wire options). **This is where the "retail options vs acdream client settings" design
|
||||
call lands** — see §10.5.
|
||||
|
||||
### 10.4 Behaviours that MUST be ported, not improved
|
||||
|
||||
1. **Clicking an LED applies immediately** (`SetCurrentValue → Apply(1)`). Apply/Reset/Defaults
|
||||
operate on an undo baseline, not on a staging buffer.
|
||||
2. **Apply is per-tab.** Each page owns its own option array.
|
||||
3. **Defaults is never disabled**; Apply and Reset are disabled whenever nothing changed.
|
||||
4. **Hiding a page reverts uncommitted edits** (`OnVisibilityChanged(false) →
|
||||
RestoreSavedValues`). Switching tabs or closing the window discards.
|
||||
5. **Showing a page applies + commits** (`OnVisibilityChanged(true) → SaveCurrentValues`,
|
||||
which also flushes the blob).
|
||||
6. **The main chat window has no "Gameplay" filter checkbox** (12 rows), the four floaties do
|
||||
(13 rows).
|
||||
7. **The default tab is Gameplay Options**, not Character (`0x32 = true` on entry 0).
|
||||
8. Retail's spelling in the UI-facing strings: "Allegiance" in the string table but
|
||||
`Allegience` in the symbol name — use the *string table* value for anything user-visible.
|
||||
|
||||
### 10.5 Open unknowns for the planner to close or accept
|
||||
|
||||
| # | Unknown | Where to look / suggested disposition |
|
||||
|---|---|---|
|
||||
| U1 | **What `m_default` is for the Character tab's 49 checkboxes.** `InitOptions` never calls `SetDefaultValue`, so "Defaults" behaviour on the biggest tab is unestablished. | Read `UIOption_Checkbox`'s ctor and `UIOption::InqDefaultGameplayOptionProperty` / `InqGameplayOptionNameAndTooltip`. **Blocking for the Defaults button.** |
|
||||
| U2 | **Which options are "auto-save"** (immediate `0x0005`) vs batched into the `0x01A1` blob. | `CPlayerModule::IsAutoSaveOption` — enumerate it. This is exactly handoff Q4's discriminator. |
|
||||
| U3 | Whether acdream ships the **50th Character row** ("Listen to PK death messages", `ID_PlayerOption_HearPKDeaths`, string `0x0D16E9A3` present in the DAT, absent from the 2013 code). | Design call. The string and ACE support exist; only the 2013 wiring is missing. |
|
||||
| U4 | The exact **`SetSliderLabel` operand pairs** for the Config tab's six labelled sliders (which of `Dark/Bright`, `Speed/Detail`, `Close/Far`, `Narrow/Wide`, `Slow/Fast`, `Soft/Hard` goes with which). | Same push-imm decode technique as `AddHeader`, applied at the six `SetSliderLabel` sites. Cosmetic; not blocking. |
|
||||
| U5 | Semantics of layout property **`0x57`** (page/window registration) and **`0x58`** (`enum 1` on every options root). | Find the `GetAttribute_Enum(this, 0x57 …)` read. Not blocking — the toggle action can be driven from `0x12` on the buttons. |
|
||||
| U6 | Consumer of input actions **`0x10000027`** ("Exit Game") and **`0x1000001F`** ("Configure Keyboard"). | Neither has a default keybind. `0x1000001F` is well corroborated by the keyboard screen's OK/Cancel; `0x10000027` rests on the label alone. |
|
||||
| U7 | The two **`support.turbine.com` URLs** behind Urgent Assistance / Report Abuse. | Dead endpoints. Recommend a **register row**: acdream shows a "not available" notice rather than launching a browser at a dead Turbine host. Extract the exact strings only if the register row needs to quote them. |
|
||||
| U8 | `gmConfigUI::SetMouseTurningDefaults`'s **full clamp set** (only the `Camera_Stiffness → 0.95f` clamp was read). | `0x0049E8F0`. Needed only if "Use Mouse Turning Settings" ships. |
|
||||
| U9 | Whether the **Config tab's 27 preference rows** map onto acdream's existing quality/audio settings or need a new retail-shaped preference store. | Design call — see §10.3. The `UserPreferences.ini` key names are all established above, so a faithful store is cheap. |
|
||||
| U10 | Which `0x2100002B` template `0x10000521` serves (present in the layout, in no template array). | Probably a later-build or unused row style. Non-blocking. |
|
||||
|
||||
### 10.6 A suggested slice shape (non-binding)
|
||||
|
||||
The natural seams fall out of the structure: (a) the two missing widget primitives —
|
||||
`Type 8` tab control + `Type 5` ListBox-with-template-list — are a shared prerequisite for
|
||||
every tab *and* for Configure Keyboard; (b) the `OptionPage`/`PlayerOptionPage` model with its
|
||||
four verbs is one small, testable unit with no DAT dependency; (c) each tab is then a thin
|
||||
`InitOptions` transcription against a binding table. The Gameplay tab is the smallest
|
||||
end-to-end vertical (7 buttons, no option model at all) and would prove the tab host, the
|
||||
mounting, and the open path in one slice before any option plumbing exists.
|
||||
|
||||
---
|
||||
|
||||
## Appendix — reproduction notes
|
||||
|
||||
- Byte reads: a throwaway `peread.py` in the session scratchpad maps VA → file offset through
|
||||
the PE section table of `C:\Users\erikn\Downloads\acclient.exe` (image base from the
|
||||
optional header) and dumps raw bytes. `decode_addchild.py` walks a VA range decoding
|
||||
`a1 imm32` / `8b 0d|15|1d|35|3d imm32` global loads, `50..57` register pushes,
|
||||
`6a xx` / `68 imm32` immediate pushes and `e8 rel32` calls, then prints the last four
|
||||
pushed arguments per call — that is how all 25 `AddChild` rows and all 18 `AddHeader`
|
||||
operands were recovered.
|
||||
- DAT reads: a throwaway `OptProbe` console (`net8.0`, `Chorizite.DatReaderWriter` 2.1.7)
|
||||
with modes `find` (hunt element ids / types across every `0x21xxxxxx` LayoutDesc),
|
||||
`hosts` (find layouts referencing a given `BaseLayoutId`), `dump` (full recursive element +
|
||||
property dump), `str` (resolve a name or hash across every `0x23xxxxxx` StringTable) and
|
||||
`dumpstr`. **It must construct a `DatCollection`, not a bare `PortalDatabase`/
|
||||
`LocalDatabase`** — `LayoutDesc.Unpack` needs the collection to resolve master properties.
|
||||
- Nothing in the repo was modified except this document.
|
||||
Loading…
Add table
Add a link
Reference in a new issue