Third of four Opus research lanes for the retail Options panel campaign: - Gameplay Options tab (gmGameplayOptionsUI::ListenToElementMessage @0x0049E110): five of seven buttons have code handlers. Exit to Char Selection -> local EndCharacterSession notice -> confirm dialog -> airborne refusal (byte-verified) -> 0xF653 logoff; Exit Game -> the epilogue path, no confirmation; Urgent Assistance / Report Abuse are WEB LINKS in the EoR build (same support-site URL, byte-verified) — the legacy wire paths still exist (0x0140 AbuseLogRequest: ACE names it but has NO handler; Help channel 0x400 broadcast: ACE handles it); Use Mouse Turning Settings is a 6-option macro (SetMouseTurningDefaults @0x0049E8F0), not a screen — five client-local prefs + one server bit (PlayerOption 0x31); Configure Keyboard / In-Game Help have no element handler in the class (help = external ACHelpPlugin.dll via keystone). - Configure Keyboard (gmKeyboardUI): six ActionClass list boxes, rows from DAT ActionMaps (DBO 0x27), all 19 ID_InputMap_* strings byte-verified; N-way cross-map conflict handling; storage is a LOCAL .keymap file named in UserPreferences.ini, never wire-synced; Reset reloads the DAT master maps (DIDs 0x14000000/0x14000002, which tools/dump-keymap already extracts). retail-default.keymap.txt is a user-saved keymap, not the DAT default. - LOAD-BEARING CORRECTION (verified at the seams): the F11 SettingsPanel is NOT rendered anywhere post-V11 — ToggleSettingsPanel() is an empty no-op, the only IPanelRenderer implementation is the test fake, and SettingsDevToolsComposition documents the keybinds.json fallback. The retail Options panel is therefore acdream's FIRST shipping in-client settings surface, and Configure Keyboard is the ONLY rebind screen — it also clears Campaign V's carried panel debt (#258 adjacent). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1151 lines
59 KiB
Markdown
1151 lines
59 KiB
Markdown
# Configure Keyboard + Gameplay Options tab actions (Lane D)
|
||
|
||
Research lane D of the settings-track campaign
|
||
(`docs/research/2026-08-10-settings-track-handoff.md`), answering handoff
|
||
questions **Q5** (Configure Keyboard: retail's keymap UI + storage) and
|
||
**Q6** (what every Gameplay Options tab button does).
|
||
|
||
**Report only.** No repo code was changed. Every retail claim below carries
|
||
a named symbol + address from the Sept 2013 EoR PDB-paired build. The
|
||
PDB/binary pairing was verified first:
|
||
|
||
```
|
||
py tools/pdb-extract/check_exe_pdb.py "C:/Users/erikn/Downloads/acclient.exe"
|
||
-> linker UTC: 2013-09-06T00:17:56+00:00
|
||
-> GUID {9e847e2f-777c-4bd9-886c-22256bb87f32}
|
||
=== MATCH: this exe pairs with our acclient.pdb ===
|
||
```
|
||
|
||
Load-bearing string literals were byte-verified by push-imm32 sweep +
|
||
direct `.rdata` reads of that binary (file offsets and VAs given inline).
|
||
Ghidra MCP (port **8081**, `patchmem.gpr`) served two cross-checks where BN
|
||
pseudo-C had pooled/garbled operands.
|
||
|
||
---
|
||
|
||
## 0. TL;DR for the impatient
|
||
|
||
| Gameplay-tab button | Retail mechanism | Wire? | acdream today |
|
||
|---|---|---|---|
|
||
| Exit to Character Selection | element `0x10000203` → `CM_UI::SendNotice_EndCharacterSession(1)` → confirm dialog → `LogOffCharacter(0)` → `0xF653` → back to char-select UI | `0xF653` (have it) | **needs-new-subsystem** (no char-select UI, no in-world→char-select transition) |
|
||
| Exit Game | element `0x10000617` → global msg 1 param `0x10000027` → `gmGamePlayUI::HandleKeyPress` → `QueueUIMode(0x10000009)` (epilogue) → `LogOffCharacter(1)` | `0xF653` (have it) | **exists** — `IGameplayWindowCommands`/`Window.Close` + `WorldSession.Dispose()` graceful logoff |
|
||
| Configure Keyboard | opens `gmKeyboardUI` (element type `0x1000000E`) — **wiring not found in code**, presumed authored in the LayoutDesc | none (local `.keymap` file) | **adaptable** (bindings model + conflict UX exist; **no renderer** — see §6) |
|
||
| Use Mouse Turning Settings | element `0x100005CC` → `BroadcastGlobalMessage(0xC)` → `gmConfigUI::SetMouseTurningDefaults` — a one-shot "apply recommended values" macro over 6 Config-tab options | 1 of 6 is `CharacterOptions2.UseMouseTurning` (0x00400000) | **adaptable** (5 client-local prefs + 1 option bit) |
|
||
| In-Game Help Files | InputAction `0x7B` (ToggleHelp/F1) → `KeyStone::OpenHelp(0, 0x10000001)` → external `plugins\ACHelpPlugin.dll` via `keystone.dll` | none | **server/asset-missing** — retail help is an external plugin we do not have |
|
||
| Urgent Assistance | element `0x10000206` → `ShellExecuteA("open", "http://support.turbine.com/ics/support/ticketnewwizard.asp?style=classic")` | **none in EoR** | trivially adaptable (dead URL — needs a product decision) |
|
||
| Report Abuse | element `0x10000207` → **the same URL** (byte-verified: both push `0x007A82A0`) | **none in EoR** | same |
|
||
|
||
The two big surprises: **(a)** in the Sept 2013 build Urgent Assistance and
|
||
Report Abuse are *web links*, not wire messages — the in-client
|
||
`gmUrgentAssistanceUI` / `gmAbuseUI` classes are still linked but the tab
|
||
does not open them; **(b)** acdream's F11 Settings panel **does not render
|
||
at all today** (§6.3) — the handoff's "what already exists" list overstates
|
||
it.
|
||
|
||
---
|
||
|
||
## 1. The Gameplay Options tab class
|
||
|
||
`gmGameplayOptionsUI : UIElement_Field, gmNoticeHandler`
|
||
(`acclient.h:55857`).
|
||
|
||
| Symbol | Address | Note |
|
||
|---|---|---|
|
||
| `gmGameplayOptionsUI::Register` | `0x0049E0F0` | |
|
||
| `gmGameplayOptionsUI::Create` | `0x0049E060` | |
|
||
| `gmGameplayOptionsUI::ListenToElementMessage` | `0x0049E110` | **the whole button dispatch** |
|
||
| `gmGameplayOptionsUI::ListenToGlobalMessage` | `0x004F5860` | ICF-folded with `gmKeyboardUI`/`gmAbuseUI`/`gmUrgentAssistanceUI` — an empty body; **not** a shared behaviour |
|
||
| `gmGameplayOptionsUI::PostInit` | `0x004BFA00` | ICF-folded with `gmCGProfessionPage::PostInit` (a `UIElement::PostInit` tailcall). BN prints the *other* class's name at that address — artifact, not evidence of a relationship. |
|
||
|
||
`ListenToElementMessage` handles exactly **five** element ids on
|
||
`idMessage == 1` (left-click). Verbatim structure (Ghidra decompile of
|
||
`0x0049E110`, cross-checked against BN at
|
||
`acclient_2013_pseudo_c.txt:169249`):
|
||
|
||
```
|
||
if (idMessage == 1) {
|
||
if (idElement < 0x10000208) {
|
||
0x10000207 -> ShellExecuteA(open, <support URL>) // Report Abuse
|
||
0x10000203 -> CM_UI::SendNotice_EndCharacterSession(1)
|
||
0x10000206 -> ShellExecuteA(open, <support URL>) // Urgent Assistance
|
||
} else {
|
||
0x100005CC -> UIElementManager::BroadcastGlobalMessage(inst, 0xC, 0)
|
||
0x10000617 -> UIElementManager::BroadcastGlobalMessage(inst, 1, 0x10000027)
|
||
}
|
||
}
|
||
```
|
||
|
||
Seven buttons, five handlers. The two unaccounted-for buttons are
|
||
**Configure Keyboard** and **In-Game Help Files** (§4.3, §4.5). The
|
||
unclaimed adjacent ids `0x10000204` and `0x10000205` appear **nowhere** in
|
||
the 1.4 M-line pseudo-C — consistent with buttons wired declaratively in
|
||
the LayoutDesc rather than in code. That is an inference, marked
|
||
**UNVERIFIED**; settling it needs a LayoutDesc dump (§8, COORDINATOR TODO).
|
||
|
||
### 1.1 How the two "global message" buttons reach their handler
|
||
|
||
`UIElementManager::BroadcastGlobalMessage(this, msgId, param)` @ `0x0045B3E0`
|
||
fans a `(msgId, param)` pair to every `UIListener` registered for `msgId`
|
||
via `UIListener::RegisterForGlobalMessage` @ `0x00465E60`.
|
||
|
||
Message **1** is the *unconsumed key/action* channel:
|
||
`UIElementManager::ActionHandler` @ `0x0045CC00` calls
|
||
`UIElementManager::KeyPressEvent` @ `0x0045C300` on action-start, which
|
||
broadcasts `(1, m_InputAction)` when no focused/active element ate it, then
|
||
calls `DoVisibilityToggleAction`. So global message 1's payload is an
|
||
**InputAction id**. Buttons can inject *synthetic* action ids into that same
|
||
channel — which is exactly what `0x10000617` does with `0x10000027`.
|
||
|
||
Message **0xC** has exactly one listener:
|
||
`gmConfigUI::ListenToGlobalMessage` @ `0x0049EB90`:
|
||
|
||
```
|
||
if (arg2 == 0xC) gmConfigUI::SetMouseTurningDefaults(this);
|
||
```
|
||
|
||
`gmConfigUI::PostInit` @ `0x0049E840` registers it
|
||
(`UIListener::RegisterForGlobalMessage(this, 0xC)` @ `0x0049E8C6`).
|
||
|
||
### 1.2 Apply / Reset / Defaults (context for the planner — Q1's lane)
|
||
|
||
`gmCharacterSettingsUI::ListenToElementMessage` @ `0x0049E3A0` (element type
|
||
`0x10000027`, `gmCharacterSettingsUI::GetUIElementType` @ `0x004A01C0`):
|
||
|
||
| Element id | Action |
|
||
|---|---|
|
||
| `0x100001FC` | `SaveCurrentValues()` — **Apply** |
|
||
| `0x100001FD` | `RestoreSavedValues()` — **Reset** |
|
||
| `0x100001FE` | `RestoreDefaultValues()` — **Defaults** |
|
||
|
||
Those three vtable slots are `OptionPage`'s
|
||
(`OptionPage::SaveCurrentValues` @ `0x004F2C60`,
|
||
`RestoreSavedValues` @ `0x004F2D00`, `RestoreDefaultValues` @ `0x004F2CB0`),
|
||
so every option page — including `gmKeyboardUI` — shares the semantics.
|
||
Handed to whichever lane owns Q1.
|
||
|
||
---
|
||
|
||
## 2. Exit to Character Selection (element `0x10000203`)
|
||
|
||
### 2.1 Client-internal notice, not a wire message
|
||
|
||
`CM_UI::SendNotice_EndCharacterSession(int)` @ `0x00479B40` is a
|
||
**local notice broadcast** — it walks `GetNoticeHandlers(0x004DD1E2)` and
|
||
invokes vtable slot `+0x21C` on each. No packet.
|
||
|
||
The consumer that matters is `gmGamePlayUI::RecvNotice_EndCharacterSession`
|
||
@ `0x004EBEA0`. Ghidra decompile plus raw-byte field decoding:
|
||
|
||
```
|
||
gmGamePlayUI field offsets (from HandleKeyPress @ 0x004E9E3D, bytes
|
||
c6 86 c6 00 00 00 00 / 88 86 c7 00 00 00 / 88 86 c5 00 00 00):
|
||
+0xC5 m_doEndSession
|
||
+0xC6 m_shouldQuitOnLogout
|
||
+0xC7 m_bLogoutConfirmed
|
||
(the NoticeHandler sub-object is at +0x98, so the notice thunk's
|
||
esi+0x2D/0x2E/0x2F are the same three fields — bytes at 0x004EBEAF:
|
||
c6 46 2d 01 / c6 46 2e 00, and at 0x004EBEF9: b0 01 / 88 46 2f /
|
||
88 46 2d / 88 46 2e)
|
||
|
||
RecvNotice_EndCharacterSession(param):
|
||
param != 0: m_doEndSession = 1; m_shouldQuitOnLogout = 0;
|
||
MakeLogoutConfirmationDialog(ID_Client_EndCharacterSessionConfirm)
|
||
param == 0: m_bLogoutConfirmed = 1; m_doEndSession = 1;
|
||
m_shouldQuitOnLogout = 1 // immediate quit, no confirm
|
||
```
|
||
|
||
`ID_Client_EndCharacterSessionConfirm` byte-verified at file offset
|
||
`0x3C2CEC`, VA `0x007C2CEC`.
|
||
|
||
The button passes `1` → **confirmation dialog first**, and
|
||
`m_shouldQuitOnLogout = 0` → after confirmation the client logs the
|
||
character off but does **not** exit the process. That is the
|
||
"Exit to Character Selection" semantic.
|
||
|
||
### 2.2 The drain that actually performs it
|
||
|
||
`gmGamePlayUI::UseTime` @ `0x004EA3A0` (per-frame):
|
||
|
||
```
|
||
if (m_doEndSession && !m_endingSession) {
|
||
m_endingSession = 1;
|
||
if (m_bLogoutConfirmed) {
|
||
if (m_shouldQuitOnLogout) { UIFramework::QueueUIMode(this, 0x10000009); }
|
||
else if (smartbox->player) {
|
||
if ((player->transient_state & 1) == 0) { // not grounded
|
||
m_endingSession = 0;
|
||
ECM_UI::SendNotice_DisplayStringInfo(0x1A, "Cannot log off while in mid-air.");
|
||
} else {
|
||
CPlayerSystem::LogOffCharacter(playerSystem, 0);
|
||
}
|
||
}
|
||
}
|
||
m_doEndSession = 0;
|
||
}
|
||
```
|
||
|
||
`"Cannot log off while in mid-air."` byte-verified as UTF-16LE at VA
|
||
`0x007C29C0` (pushed at `0x004EA481`). **Retail refuses to log off while
|
||
airborne** — `transient_state & 1` is the on-contact/ON_WALKABLE bit.
|
||
`gmGamePlayUI::EndSession` @ `0x004EA1B0` is the same graph reachable from
|
||
the confirmation dialog.
|
||
|
||
### 2.3 The wire
|
||
|
||
`CPlayerSystem::LogOffCharacter(bool immediate)` @ `0x00563520`:
|
||
|
||
```
|
||
CPlayerModule::SaveToServer(&playerModule, 0); // flush dirty options blob
|
||
if (immediate) ExecuteLogOff(); // @ 0x0055D780
|
||
else if (prevRequest == IR_NONE) RequestLogOff(); // @ 0x00562DD0
|
||
else { AddTextToScroll("Logging off...\n"); loggingOff = 1; }
|
||
```
|
||
|
||
`CPlayerSystem::RequestLogOff` @ `0x00562DD0` reaches
|
||
`Proto_UI::LogOffCharacter` @ `0x00546A20`:
|
||
|
||
```
|
||
buf = new byte[8]; buf[0..3] = 0xF653; buf[4..7] = characterId;
|
||
Proto_UI::SendToLogon(buf, 8);
|
||
```
|
||
|
||
**acdream already has this exactly**:
|
||
`src/AcDream.Core.Net/Messages/CharacterLogOff.cs` (`Opcode = 0xF653`,
|
||
`BuildRequestBody(characterId)` writes the same 8 bytes and cites the same
|
||
addresses), consumed by `WorldSession.Dispose()`
|
||
(`src/AcDream.Core.Net/WorldSession.cs:2795+`) which sends the request,
|
||
waits up to 35 s for the server's opcode-only confirmation, then tears the
|
||
transport down. ACE's side is
|
||
`Source/ACE.Server/Network/Handlers/CharacterHandler.cs:266`
|
||
(`CharacterLogOff` → `session.LogOffPlayer()`; it reads no payload) and it
|
||
echoes `GameMessageCharacterLogOff` back from `Session.cs:268`.
|
||
|
||
Cross-check: holtburger packs `CharacterLogOff` as **opcode only, 4 bytes**
|
||
(`crates/holtburger-protocol/src/messages/game_message/pack.rs:76`) and ACE
|
||
accepts that too. Retail's 8-byte form is the faithful one; acdream matches
|
||
retail.
|
||
|
||
### 2.4 Why this is `needs-new-subsystem` in acdream
|
||
|
||
`WorldSession` has a `State.InCharacterSelect`
|
||
(`WorldSession.cs:85`) but it is entered exactly once
|
||
(`WorldSession.cs:997`) during `Connect()`, which blocks until
|
||
`CharacterList (0xF658)` arrives and then hands off to `EnterWorld(index)`.
|
||
There is no path from in-world back to character select — `Dispose()` tears
|
||
down the entire session and socket. Retail instead keeps the login
|
||
connection and swaps `UIFramework` mode.
|
||
|
||
**Recommended initial adaptation (planner's call):** wire the button to the
|
||
same graceful shutdown as Exit Game and add a divergence-register row in
|
||
`docs/architecture/retail-divergence-register.md` in the same commit
|
||
(register rule 1). Risk-if-assumption-breaks column: "user expects to
|
||
return to a character list and instead the client exits."
|
||
|
||
---
|
||
|
||
## 3. Exit Game (element `0x10000617`)
|
||
|
||
`BroadcastGlobalMessage(1, 0x10000027)` →
|
||
`gmGamePlayUI::HandleKeyPress(long)` @ `0x004E9DF0` (Ghidra decompile,
|
||
exact):
|
||
|
||
```
|
||
if (param_1 < 0x10000027) {
|
||
if (param_1 == 0x10000026) { m_shouldQuitOnLogout = false;
|
||
m_bLogoutConfirmed = true;
|
||
m_doEndSession = true; return; }
|
||
if (param_1 == 0x54) { Show(!Shown()); return; } // UI toggle action
|
||
if (param_1 == 0x7B) { KeyStone::OpenHelp(0, 0x10000001); return; }
|
||
} else if (param_1 == 0x10000027) {
|
||
m_shouldQuitOnLogout = true; m_bLogoutConfirmed = true; m_doEndSession = true;
|
||
}
|
||
```
|
||
|
||
`m_shouldQuitOnLogout = true` + already-confirmed → `UseTime` takes the
|
||
`QueueUIMode(this, 0x10000009)` branch → the epilogue framework.
|
||
`gmEpilogueUI::gmEpilogueUI` @ `0x004E9B00` then calls
|
||
`CPlayerSystem::LogOffCharacter(playerSystem, 1)` at `0x004E9B7A` —
|
||
`immediate = 1` → **`ExecuteLogOff` directly, no wait for the `0xF653`
|
||
echo.** (Retail's Exit Game does not wait; acdream's `Dispose()` does wait,
|
||
which is strictly safer against ACE's stale-session behaviour. Keep
|
||
acdream's.)
|
||
|
||
Note there is **no confirmation dialog** on this path in the decompiled
|
||
graph — `m_bLogoutConfirmed` is set to `true` by the button itself.
|
||
|
||
`0x10000026` (the "logout, don't quit" synthetic action) is **never
|
||
broadcast anywhere in the pseudo-C** — another point in favour of the
|
||
"unhandled ids are authored in the LayoutDesc" hypothesis, and another
|
||
reason to dump the layout.
|
||
|
||
**acdream seam that already exists:**
|
||
`IGameplayWindowCommands` /
|
||
`new GameplayWindowCommands(d.Window.Close)`
|
||
(`src/AcDream.App/Composition/SessionPlayerComposition.cs:1200`), which is
|
||
what Escape already uses (`GameplayInputCommandController.cs:249`). Window
|
||
close → the composition shutdown → `WorldSession.Dispose()` graceful
|
||
logoff. A retail Exit Game button is a one-line binding to that.
|
||
|
||
---
|
||
|
||
## 4. The other four buttons
|
||
|
||
### 4.1 Urgent Assistance (element `0x10000206`) — **web link, no wire**
|
||
|
||
Byte-verified push-imm32 sweep of `0x0049E8F0`-adjacent code
|
||
(`0x0049E110..0x0049E2C0`):
|
||
|
||
```
|
||
0049e14b push 0x007A82A0 'http://support.turbine.com/ics/support/ticketnewwizard.asp?style=classic'
|
||
0049e164 push 0x00794090 'open'
|
||
0049e18b push 0x007A81E0 'An error occurred while trying to launch your web browser. (Error code %d)\n
|
||
The web site to submit an urgent assistance request is listed below.
|
||
Please go there to complete your request.\n%s\n'
|
||
0049e19f push 0x00794078 "Asheron's Call Error"
|
||
```
|
||
|
||
`ShellExecuteA(NULL, "open", url, NULL, NULL, SW_SHOWNORMAL)`; on failure
|
||
(`ret <= 32`) a `MessageBoxA`. **The error string is what identifies this
|
||
button** — the element ids themselves carry no label in code.
|
||
|
||
### 4.2 Report Abuse (element `0x10000207`) — **same web link, no wire**
|
||
|
||
```
|
||
0049e1e7 push 0x007A82A0 'http://support.turbine.com/ics/support/ticketnewwizard.asp?style=classic'
|
||
0049e200 push 0x00794090 'open'
|
||
0049e231 push 0x007A8128 'An error occurred while trying to launch your web browser. (Error code %d)\n
|
||
The web site to submit an abuse report is listed below.
|
||
Please go there to complete your request.\n%s\n'
|
||
0049e245 push 0x00794078 "Asheron's Call Error"
|
||
```
|
||
|
||
Both buttons push the **identical pointer `0x007A82A0`** — this is real, not
|
||
BN string pooling: the two push sites are at different addresses and both
|
||
were read from the binary.
|
||
|
||
#### 4.2.1 The legacy in-client abuse/urgent flows still exist (unused by this tab)
|
||
|
||
Worth knowing, because a planner may prefer them to a dead URL:
|
||
|
||
| Class | Type id | Entry | Wire |
|
||
|---|---|---|---|
|
||
| `gmAbuseUI` | `0x10000018` (`GetUIElementType` @ `0x004BBF20`) | `gmAbuseUI::ListenToElementMessage` @ `0x004BC5C0` → `gmAbuseUI::ReportAbuse` @ `0x004BC380` | `CM_Character::Event_AbuseLogRequest(name, 1, complaint)` @ `0x006A2A30` — **GameAction opcode `0x0140`** (`*(uint32*)buf = 0x140` at `0x006A2AAE`, sent via `Proto_UI::SendToWeenie`) |
|
||
| `gmUrgentAssistanceUI` | `0x1000001F` (`GetUIElementType` @ `0x004A7830`) | `gmUrgentAssistanceUI::ListenToElementMessage` @ `0x004A7940` | `CM_Communication::Event_ChannelBroadcast(0x400, text)` @ `0x006A4030` |
|
||
|
||
`gmAbuseUI` fields (`acclient.h:55423`): name box `0x10000105`, entry box
|
||
`0x10000107`, result text `0x1000010B`, continue button `0x10000109`
|
||
(`gmAbuseUI::PostInit` @ `0x004BBF30`); response comes back through
|
||
`gmAbuseUI::RecvNotice_AbuseReportResponse` @ `0x004BC1B0`.
|
||
`gmUrgentAssistanceUI` fields (`PostInit` @ `0x004A7840`): entry box
|
||
`0x100001BA`, continue button `0x100001BD`; it enforces a non-empty entry
|
||
before sending.
|
||
|
||
**ACE cross-check:**
|
||
|
||
- `AbuseLogRequest = 0x0140` exists in
|
||
`Source/ACE.Server/Network/GameAction/GameActionType.cs:69` and in
|
||
`Source/ACE.Entity/PacketOpCodeNames.cs:168`
|
||
(`Evt_Character__AbuseLogRequest_ID`), but **there is no
|
||
`[GameAction(GameActionType.AbuseLogRequest)]` handler anywhere in
|
||
`Source/ACE.Server/`** — verified by grep. Sending it to ACE is a no-op
|
||
(server-missing).
|
||
- Channel `0x400` = `Channel.Help` (`Source/ACE.Entity/Enum/Channel.cs:86`,
|
||
`Help = 0x00000400`). ACE **does** handle it:
|
||
`GameActionChatChannel.cs` `case Channel.Help:` at line 84, reached
|
||
through `GameActionType.ChatChannel = 0x0147`. So the *urgent assistance*
|
||
path is server-supported today; the *abuse report* path is not.
|
||
- Retail's abuse responses map to
|
||
`WeenieError.AbuseNoSuchCharacter 0x04B8 / AbuseReportedSelf 0x04B9 /
|
||
AbuseComplaintHandled 0x04BA` (cross-referenced in
|
||
`references/holtburger/crates/holtburger-protocol/src/errors.rs:273-275`).
|
||
|
||
### 4.3 Configure Keyboard — **wiring UNKNOWN in code**
|
||
|
||
`gmKeyboardUI` is a registered element class:
|
||
`UIElement::RegisterElementClass(0x1000000E, gmKeyboardUI::Create)`
|
||
(`gmKeyboardUI::Register` @ `0x004DCF70`, body at `0x004DCF7A`).
|
||
`gmKeyboardUI::GetUIElementType` @ `0x004DBF90` returns `0x1000000E`.
|
||
|
||
No code path in the pseudo-C creates or shows it in response to a Gameplay
|
||
Options button. Candidate mechanisms, both authored rather than coded:
|
||
|
||
1. **`UIElementManager::DoVisibilityToggleAction(action)` @ `0x0045B660`** —
|
||
looks up `m_elementInputActionListenerTable[action]` and broadcasts
|
||
element message `0x31` to every element registered for that InputAction.
|
||
Elements declare that registration in their `ElementDesc`.
|
||
2. A plain LayoutDesc-authored state/visibility change on the button
|
||
(the same class of authoring that `CM_UI::SendNotice_SetPanelVisibility`
|
||
serves elsewhere — see `gmGamePlayUI` at `0x004E9D4C`).
|
||
|
||
**Marked UNVERIFIED.** Resolving it requires reading the Options-panel
|
||
LayoutDesc out of `client_portal.dat` — see §8.
|
||
|
||
### 4.4 Use Mouse Turning Settings (element `0x100005CC`)
|
||
|
||
`BroadcastGlobalMessage(0xC, 0)` → `gmConfigUI::SetMouseTurningDefaults`
|
||
@ `0x0049E8F0`. This is **not** a screen — it is a one-shot "apply the
|
||
recommended values" macro over six Config-tab options, each with a chat
|
||
line. All six format strings byte-verified by push-imm32 sweep of
|
||
`0x0049E8F0..0x0049EB81`:
|
||
|
||
| Order | Option (UserPreferences key) | Condition | New value | Chat line (verified VA) |
|
||
|---|---|---|---|---|
|
||
| 1 | `Camera.Stiffness` | `!= 0.95f` | `0.95f` | `0x007A8A70` `'Camera Stiffness was changed from %f to the mouse turning default of %f.'` |
|
||
| 2 | `Camera.AdjustmentSpeed` | `!= 50.0f` | `50.0f` | `0x007A8A20` `'Camera Adjustment was changed from %f to the mouse turning default of %f.'` |
|
||
| 3 | `Input.MouseLookSensitivity` | `!= 0.7f` | `0.7f` | `0x007A89D0` `'Mouse Sensitivity was changed from %f to the mouse turning default of %f.'` |
|
||
| 4 | `Camera.AlignToSlope` | `== 1` | `0` | `0x007A8980` `'Align To Slope was changed from TRUE to the mouse turning default of FALSE.'` |
|
||
| 5 | `Input.InvertMouseLookYAxis` | `== 0` | `1` | `0x007A8928` `'Invert Mouselook Axes was changed from FALSE to the mouse turning default of TRUE.'` |
|
||
| 6 | `Input.UseMouseTurning` | `== 0` | `1` | `0x007A88D0` `'Turn to Face Camera was changed from FALSE to the mouse turning default of TRUE.'` |
|
||
|
||
Then `SaveCurrentValues()` at `0x0049EB57` — **the macro commits
|
||
immediately; no Apply press needed.**
|
||
|
||
The float comparisons are the MSVC `fcomp / fnstsw ax / test ah,0x44 / jnp`
|
||
idiom. Raw bytes at `0x0049E924`: `d8 1d bc 8a 7a 00 | df e0 | f6 c4 44 |
|
||
7b 50` — `jnp` skips the body on *equal*, so the body runs when the current
|
||
value **differs** from the target. BN renders that as an unresolved
|
||
`bool p_1 = unimplemented {test ah, 0x44}` — do not trust its
|
||
`if (p_1)` polarity without this byte check.
|
||
|
||
Storage split (matters for the headless-bot constraint, handoff Q8):
|
||
|
||
- Items 1–5 are **client-local `UserPreferences`** only. Grep of
|
||
`references/ACE/Source/` finds no `AlignToSlope`, `InvertMouseLook`,
|
||
`MouseLookSensitivity`, `Stiffness`, or `AdjustmentSpeed` character
|
||
option. They are presentation settings.
|
||
- Item 6 is **server-synced**: `UseMouseTurning_PlayerOption = 0x31`
|
||
(`acclient.h:4214`), which ACE maps to
|
||
`CharacterOptions2.UseMouseTurning = 0x00400000`
|
||
(`Source/ACE.Entity/Enum/CharacterOption.cs:162-163`,
|
||
`Source/ACE.Entity/Enum/CharacterOptions2.cs:35`). Settable through the
|
||
`SetSingleCharacterOption (0x0005)` path acdream already has.
|
||
It is registered twice on the client — as a `UserPreferences` key
|
||
`"Input.UseMouseTurning"` (`0x006C374A` and four other init sites) and as
|
||
a `PlayerModule` option (`PlayerModule::UseMouseTurning` @ `0x005D3380`,
|
||
`SetUseMouseTurning` @ `0x005D3390`, dispatch at `0x005D4269`).
|
||
|
||
The Config-tab option that item 6 flips is labelled **"Turn to Face
|
||
Camera"** in the UI (per the chat string), *not* "Use Mouse Turning" —
|
||
useful when transcribing the Config tab.
|
||
|
||
For reference, the same options' *ordinary* defaults from
|
||
`gmConfigUI::InitOptions` @ `0x0049E400`: Stiffness `0x3EE66666` = 0.45,
|
||
AdjustmentSpeed `0x42200000` = 40.0, MouseLookSensitivity `0x3F0CCCCD` =
|
||
0.55, AlignToSlope = 1, InvertMouseLookYAxis = 0, UseMouseTurning = 0.
|
||
|
||
**acdream today:** `MouseLookState`
|
||
(`src/AcDream.UI.Abstractions/Input/MouseLookState.cs`) implements retail's
|
||
MMB-hold `CameraInstantMouseLook`, **not** the persistent
|
||
`Input.UseMouseTurning` "turn to face camera" mode. Whether that mode
|
||
exists at all in acdream is **UNVERIFIED by this lane** — the physics/camera
|
||
digest (`claude-memory/project_camera_visibility_coupling.md`) is the place
|
||
to check before planning it.
|
||
|
||
### 4.5 In-Game Help Files — **external plugin, not portable**
|
||
|
||
`KeyStone::OpenHelp(uint stringId, int tableEnum)` @ `0x00557010` calls
|
||
through `KeyStone::m_fnAC2HelpPluginExecute` — a function pointer resolved
|
||
in `KeyStone::Init` @ `0x00556CF0`. Byte-verified sweep of
|
||
`0x00556CF0..0x00556E20`:
|
||
|
||
```
|
||
00556d12 push 0x007CB834 'keystone.dll'
|
||
00556d1f push 0x007CB824 'KeystoneCreate'
|
||
00556d2c push 0x007CB808 'plugins\\ACHelpPlugin.dll'
|
||
00556d38 push 0x007CB7F8 'ExecutePlugin'
|
||
00556d4f push 0x007CB7E8 'TerminatePlugin'
|
||
00556d57 push 0x007CB7CC 'plugins\\ACPluginManager.dll'
|
||
```
|
||
|
||
So retail's help is a **third-party embedded help viewer** (Keystone),
|
||
loading `plugins\ACHelpPlugin.dll`. There is no DAT-resident help content
|
||
we can read and no DID to cite.
|
||
|
||
The three call sites are `gmGamePlayUI::HandleKeyPress` @ `0x004E9E16`,
|
||
`gmCharGenMainUI::ListenToGlobalMessage` @ `0x004E901C`, and
|
||
`ClientUISystem::OnAction` @ `0x00564BCE` — all keyed on
|
||
**InputAction `0x7B`**, which is `ToggleHelp` in the default keymap
|
||
(`retail-default.keymap.txt:139`, `ToggleHelp [ "" [ 0 DIK_F1 ] ]`).
|
||
|
||
**Verdict: needs-new-subsystem / asset-missing.** Any acdream
|
||
implementation is a product decision (open a URL, open bundled markdown,
|
||
open the in-repo docs) — not a port.
|
||
|
||
---
|
||
|
||
## 5. Configure Keyboard (Q5) — retail's screen
|
||
|
||
### 5.1 Class + widget inventory
|
||
|
||
`gmKeyboardUI : OptionPage, gmNoticeHandler` (`acclient.h:54467`):
|
||
|
||
```
|
||
UIElement_Button *m_pKeyboardLoadKeymapButton;
|
||
UIElement_Button *m_pKeyboardSaveKeymapButton;
|
||
UIElement_Text *m_pKeyboardCurrentKeymapLabel;
|
||
UIElement_Button *m_pKeyboardResetToDefaultsButton;
|
||
UIElement_Button *m_pKeyboardRevertToSavedButton;
|
||
UIElement_Button *m_pKeyboardOKButton;
|
||
UIElement_Button *m_pKeyboardCancelButton;
|
||
HashTable<unsigned long, UIElement_ListBox*, 0> m_hashMappingListBoxes;
|
||
unsigned int m_uiLoadKeymapDialogContext;
|
||
unsigned int m_uiSaveKeymapDialogContext;
|
||
unsigned int m_uiCantOverwriteReadOnlyKeymapDialogContext;
|
||
unsigned int m_uiOverwriteKeymapDialogContext;
|
||
List<PStringBase<char>> m_listCachedKeymapFilenames;
|
||
```
|
||
|
||
`gmKeyboardUI::PostInit` @ `0x004DB770` wires them. The **buttons come from
|
||
LayoutDesc attributes**, not hard-coded ids
|
||
(`UIElement::GetAttribute_Enum(this, <attr>, &id)` then
|
||
`GetChildRecursive(this, id)`):
|
||
|
||
| Attribute enum | Field |
|
||
|---|---|
|
||
| `0x1000001B` | Reset to Defaults button |
|
||
| `0x1000001C` | Revert to Saved button |
|
||
| `0x1000001D` | Current-keymap label |
|
||
| `0x1000001E` | Load Keymap button |
|
||
| `0x1000001F` | Save Keymap button |
|
||
| `0x10000019` | OK button |
|
||
| `0x1000001A` | Cancel button |
|
||
|
||
The **six list boxes** are hard-coded container ids, each holding a
|
||
list box found by attribute `0x10000018`:
|
||
|
||
| Container element id | `ActionClass` key |
|
||
|---|---|
|
||
| `0x1000049D` | `Movement` |
|
||
| `0x1000049F` | `Camera` |
|
||
| `0x100004A1` | `Combat` |
|
||
| `0x100004A3` | `UI` |
|
||
| `0x10000211` | `CharacterSettings` |
|
||
| `0x100004A5` | `Emote` |
|
||
|
||
(`gmKeyboardUI::PostInit` @ `0x004DB80C / 0x004DB859 / 0x004DB8A6 /
|
||
0x004DB8F3 / 0x004DB940 / 0x004DB98D`. BN prints the hash-table type as
|
||
`HashTable<ControlSpecification, enum ControlType, 0>` — an ICF/type-recovery
|
||
artifact; the declared type in `acclient.h` is
|
||
`HashTable<unsigned long, UIElement_ListBox*, 0>`. The keys `Movement`,
|
||
`Camera`, `Combat`, `UI`, `CharacterSettings`, `Emote` are named globals;
|
||
the enum they belong to is **not** in `acclient.h` — only
|
||
`unsigned int m_eActionClass` at line 27980 survives. Values UNKNOWN.)
|
||
|
||
So: **six categories → six list boxes**, i.e. six panes/tabs on the
|
||
Configure Keyboard screen.
|
||
|
||
### 5.2 How rows are built (`gmKeyboardUI::InitOptions` @ `0x004DD8B0`)
|
||
|
||
```
|
||
UpdateKeymapFilenameLabel();
|
||
flush every list box;
|
||
actionMap = ICIDM::s_cidm->GetActionMap();
|
||
for each inputMap in actionMap->m_hashInputMaps:
|
||
for each action in inputMap:
|
||
if (!ActionMap::IsUserBindable(actionMap, inputMapId, action)) continue; // 0x006854E0
|
||
cls = ActionMap::GetActionClass(actionMap, inputMapId, action); // 0x006855C0
|
||
bucket[cls][inputMapId].push(action)
|
||
for each class bucket:
|
||
listBox = m_hashMappingListBoxes[cls]
|
||
for each inputMapId in bucket:
|
||
header = listBox.AddItemFromTemplateList(0) // template 0 = header row
|
||
header.text = GetStringInfoFromInputMapID(inputMapId) // 0x004DA980
|
||
for each action:
|
||
ActionMap::GetDescripValues(actionMap, action, inputMapId, &label, &tooltip) // 0x00685690
|
||
CInputMap::FindKeysForAction(inputMap, action, &List<QualifiedControl>)
|
||
AddActionKeyMap(listBox, action, inputMapId, label, tooltip, keys) // 0x004DB2F0
|
||
```
|
||
|
||
`gmKeyboardUI::AddActionKeyMap` adds template row **1**
|
||
(`AddItemFromTemplateList(arg2, 1, nullptr)`), casts to
|
||
`UIOption_ActionKeyMap` (element type `0x10000034`), calls
|
||
`UIOption_ActionKeyMap::Init` @ `0x004888E0` and
|
||
`OptionPage::RegisterOption` @ `0x004F2E90`.
|
||
|
||
`gmKeyboardUI::ListBoxEntryType` (`acclient.h:6864`) confirms exactly two
|
||
row templates: `Header_ListBoxEntryType = 0`, `ActionKeyMap_ListBoxEntryType = 1`.
|
||
|
||
### 5.3 The header (category) names — all 19 byte-verified
|
||
|
||
`gmKeyboardUI::GetStringInfoFromInputMapID` @ `0x004DA980` maps
|
||
InputMapID → string-table id (table enum 7). All 19 literals were found in
|
||
`.rdata` at `0x007BE45C..0x007BE648`:
|
||
|
||
| InputMapID | String id |
|
||
|---|---|
|
||
| `0x00000004` | `ID_InputMap_MovementCommands` |
|
||
| `0x00000005` | `ID_InputMap_CameraControls` |
|
||
| `0x00000006` | `ID_InputMap_CameraAlternateControls` |
|
||
| `0x00000009` | `ID_InputMap_DialogBoxes` |
|
||
| `0x0000000B` | `ID_InputMap_DebugConsole` |
|
||
| `0x0000000C` | `ID_InputMap_ProfilerUI` |
|
||
| `0x0000000D` | `ID_InputMap_UIDebugger` |
|
||
| `0x0000000E` | `ID_InputMap_DebugCommands` |
|
||
| `0x10000002` | `ID_InputMap_Combat` |
|
||
| `0x10000003` | `ID_InputMap_MeleeCombat` |
|
||
| `0x10000004` | `ID_InputMap_MissileCombat` |
|
||
| `0x10000005` | `ID_InputMap_MagicCombat` |
|
||
| `0x10000006` | `ID_InputMap_Emotes` |
|
||
| `0x10000007` | `ID_InputMap_ItemSelectionCommands` |
|
||
| `0x10000008` | `ID_InputMap_CharacterOptionCommands` |
|
||
| `0x10000009` | `ID_InputMap_UICommands` |
|
||
| `0x1000000A` | `ID_InputMap_ChatCommands` |
|
||
| `0x1000000C` | `ID_InputMap_QuickslotCommands` |
|
||
| `0x1000000D` | `ID_InputMap_ToggleChatEntry` |
|
||
|
||
Cross-check against the shipped keymap file
|
||
(`docs/research/named-retail/retail-default.keymap.txt`, group headings at
|
||
lines 87/113/135/159/188/193/198/203/212/221/243/252/258/275/283/291/303/
|
||
311/331/341): the file has **20** groups. Six of them —
|
||
`TargetedUsage`, `SystemKeys`, `MouseCommands`, `ScrollableControls`,
|
||
`EditControls`, `CopyAndPasteControls` — have **no** case in
|
||
`GetStringInfoFromInputMapID` and therefore no header. Either
|
||
`IsUserBindable` filters every action in them, or they render headerless.
|
||
**UNVERIFIED which.**
|
||
|
||
Caveat to carry: the low-numbered InputMapIDs are **not globally unique
|
||
across master maps**. `tools/dump-keymap` output
|
||
(`docs/research/named-retail/keymap-default.txt`) shows two master maps —
|
||
`gmDefaultMap` @ DID `0x14000000` with contexts
|
||
`4,5,6,0x10000002..0x1000000D`, and `DefaultMap` @ DID `0x14000002` with
|
||
contexts `3,5,7,8,9,0xA,0x10`. Context `5` means `CameraControls` in the
|
||
first and something else in the second, yet `GetStringInfoFromInputMapID`
|
||
is a flat switch. The screen enumerates the **merged**
|
||
`ICIDM::GetActionMap()->m_hashInputMaps`, so this is a real (retail)
|
||
ambiguity, not a decomp artifact.
|
||
|
||
### 5.4 The row widget: `UIOption_ActionKeyMap`
|
||
|
||
`acclient.h:54392`:
|
||
|
||
```
|
||
struct UIOption_ActionKeyMap : UIOption, UIElement_Text
|
||
{
|
||
UIElement_Button *m_buttonClear;
|
||
SmartArray<UIElement_Button*,1> m_aKeyButtons;
|
||
unsigned int m_idInputAction;
|
||
unsigned int m_idInputMap;
|
||
List<QualifiedControl> m_qclDefaults;
|
||
List<QualifiedControl> m_qclSaved;
|
||
List<QualifiedControl> m_qclCurrent;
|
||
QualifiedControl m_qcBindingBeingChanged;
|
||
int m_nBindingBeingChanged;
|
||
unsigned int m_ctxtDialog;
|
||
unsigned int m_ctxtOverwriteBindingDialog;
|
||
unsigned int m_ctxtCantOverwriteBindingDialog;
|
||
int m_skipConfirmation;
|
||
};
|
||
```
|
||
|
||
**Row columns:** the action label (the `UIElement_Text` base), a tooltip
|
||
(`SetTooltip` @ `0x00487050`), **N key buttons** (one per bound chord —
|
||
`m_aKeyButtons` is a dynamic array, so an action with two default bindings
|
||
shows two), and a **Clear** button.
|
||
|
||
Three current/saved/default binding lists per row are exactly retail's
|
||
Apply/Reset/Defaults triple at row granularity (`SaveCurrentValue` @
|
||
`0x00488260`, `RestoreSavedValue` @ `0x00488280`, `RestoreDefaultValue` @
|
||
`0x004884F0`).
|
||
|
||
**Interactions** (`UIOption_ActionKeyMap::ListenToElementMessage` @
|
||
`0x00489A80`):
|
||
|
||
| Input | Behaviour |
|
||
|---|---|
|
||
| Left-click (`idMessage == 1`) on `m_buttonClear` | `ClearAllBindings` @ `0x00487ED0` |
|
||
| Left-click on key button *i* | `InitiateBinding(i)` @ `0x004899D0` |
|
||
| Right-click (`idMessage == 0x19`, `dwParam1 == 8`) on key button *i* | `EraseBinding(i)` @ `0x00487780` (only while `i < m_qclDefaults.count` and the button isn't disabled state `0xD`) |
|
||
|
||
All gated on `DialogFactory::IsDialogOpen(0x10000001) == 0`.
|
||
|
||
**Capture flow** (`InitiateBinding` @ `0x004899D0`):
|
||
|
||
```
|
||
StringInfo::SetStringIDandTableEnum(&si, hash("ID_ActionKeyMap_MapInstructions"), 0x10000004);
|
||
si.AddVariable_String(0, this->GetText()); // substitutes the action name
|
||
m_nBindingBeingChanged = slotIndex;
|
||
if (OpenMapWarnDialog(&si)) // 0x00488A00 — modal "press a key"
|
||
ICIDM::s_cidm->RegisterInputHandler(this, 0x20);
|
||
```
|
||
|
||
`ID_ActionKeyMap_MapInstructions` byte-verified at file offset `0x3A30AC`,
|
||
VA `0x007A30AC`.
|
||
|
||
**Conflict handling** (`UIOption_ActionKeyMap::KeyHitHandler` @ `0x00489570`):
|
||
|
||
```
|
||
if (qc.m_activation & 0x81) ignore; // filtered activation types
|
||
deviceType = ICIDM::GetDeviceTypeFromKey(key);
|
||
... (mouse-device special case at 0x004895AF) ...
|
||
UnregisterInputHandler(0x20); CloseMapWarnDialog();
|
||
if (key is already this exact binding) return; // QualifiedControl::IsExactlyEqual
|
||
ICIDM::FindConflictingInputMaps(qc, &maps);
|
||
for each conflicting map:
|
||
ICIDM::FindConflictingControls(qc, map, &controls);
|
||
collect unique (map, control, action) triples
|
||
if (any conflicting action is !ActionMap::IsUserBindable)
|
||
OpenCantOverwriteBindingDialog(); // 0x00489300 — refuse
|
||
else if (conflicts)
|
||
OpenOverwriteBindingDialog(&conflicts); // 0x00488BF0 — confirm, may be many
|
||
else
|
||
SetBinding(qc, slot); // 0x00487B20
|
||
```
|
||
|
||
So retail's conflict model is **N-way and cross-input-map**, with a
|
||
distinct "this key is bound to something you're not allowed to rebind"
|
||
refusal. `m_skipConfirmation` suppresses the prompt (presumably after a
|
||
"don't ask again"). String ids
|
||
`ID_KeyMapCantOverwriteReadOnlyKeymap_Label` (`0x007BEA8C`),
|
||
`ID_KeyMapOverwriteKeymap_Label` (`0x007BEAD4`),
|
||
`ID_KeyMapLoadKeymap_Label` (`0x007BEAB8`),
|
||
`ID_KeyMapSaveKeymap_Label` (`0x007BEAF4`) all byte-verified.
|
||
|
||
### 5.5 The screen's own buttons
|
||
|
||
`gmKeyboardUI::ListenToElementMessage` @ `0x004DD230`:
|
||
|
||
| Control | Behaviour |
|
||
|---|---|
|
||
| Load Keymap (left-click) | `MakeLoadKeymapDialog` @ `0x004DC0B0` — a `DialogFactory` dialog listing `m_listCachedKeymapFilenames`; close → `HandleCloseLoadKeymapDialog` @ `0x004DAC20` |
|
||
| Save Keymap (left-click) | `MakeSaveKeymapDialog` @ `0x004DC5B0` (filename entry); close → `HandleCloseSaveKeymapDialog` @ `0x004DD160` → `SaveKeymap(name, promptOnOverwrite: true)` |
|
||
| Reset to Defaults (left-click) | `RestoreDefaultValues` (§5.6) |
|
||
| Revert to Saved (left-click) | `RestoreSavedValues` |
|
||
| **OK** (right-click release, `idMessage == 0x19`, `dwParam1 == 7`) | `if (OptionPage::Changed(this)) SaveKeymap(Client::m_instance + 0x144, promptOnOverwrite: false);` then `SaveCurrentValues()` |
|
||
| **Cancel** (same message) | `RestoreSavedValues()` |
|
||
|
||
`Client::m_instance + 0x144` is the current keymap filename, written by
|
||
`gmClient::SetKeyMapFileName` @ `0x00401EF0`. Both OK/Cancel are gated on
|
||
button state `!= 0xD` (disabled).
|
||
|
||
### 5.6 Reset to Defaults reloads from the **DAT**, not from a table
|
||
|
||
`gmKeyboardUI::RestoreDefaultValues` @ `0x004DA850`:
|
||
|
||
```
|
||
ICIDM::s_cidm->ClearKeyMap();
|
||
ICIDM::s_cidm->AddKeyMap(0x10000001);
|
||
ICIDM::s_cidm->AddKeyMap(1);
|
||
return OptionPage::RestoreDefaultValues(this);
|
||
```
|
||
|
||
`CInputManager_WIN32::AddKeyMap(uint32)` @ `0x006864C0`:
|
||
|
||
```
|
||
DBObj *o = DBObj::GetByEnum(id, ...);
|
||
if (o) CMasterInputMap::Merge(&m_InputMap, o, 1);
|
||
```
|
||
|
||
The two enums resolve to the two DAT master input maps our own
|
||
`tools/dump-keymap` already extracts
|
||
(`docs/research/named-retail/keymap-default.txt`):
|
||
**`gmDefaultMap` @ DID `0x14000000`** (14 input maps, the gameplay keymap)
|
||
and **`DefaultMap` @ DID `0x14000002`** (7 input maps, the system/UI keymap).
|
||
Which enum maps to which DID is **UNVERIFIED** (the tool hard-codes the DIDs;
|
||
`DBObj::GetByEnum`'s table was not traced).
|
||
|
||
`ActionMap` itself is a DAT object too: `ActionMap::GetDBOType` @
|
||
`0x00685C00` returns **`0x27`** (raw bytes `b8 27 00 00 00 c3`), with
|
||
`ActionMap::Serialize` @ `0x00685F10` and `ReloadFromDisk` driven by
|
||
`CInputManager_WIN32::InitializeKeymap` @ `0x00686010`. **Every row label,
|
||
tooltip, user-bindable flag and action class on the Configure Keyboard
|
||
screen comes from that DAT object, not from the executable.**
|
||
|
||
### 5.7 Keymap storage — a local text file, never wire-synced
|
||
|
||
**Directory.** `gmKeyboardUI::GetKeymapDirectory` @ `0x004DA8E0` returns
|
||
`PSUtils::get_directory(UserPreferences::sm_strDefaultFile)` — i.e. the
|
||
folder holding `UserPreferences.ini`. Byte-verified format strings at
|
||
`0x00792DA7`-adjacent `.rdata`:
|
||
|
||
```
|
||
"%s\\Asheron's Call" (VA ~0x00792DBB region)
|
||
"%s\\UserPreferences.ini" ('UserPreferences.ini' at file 0x392DBB, VA 0x00792DBB)
|
||
```
|
||
|
||
i.e. `<My Documents>\Asheron's Call\`. This matches the shipped keymap
|
||
file's own header text (`retail-default.keymap.txt:9-12`).
|
||
|
||
**File name.** `UserPreferences` carries a key `"keymap"` described as
|
||
`"The filename of the keymap file to use"` — both byte-verified at
|
||
VA `0x00792FA8` and `0x00792FDA`. `gmKeyboardUI::SaveKeymap` @ `0x004DCF90`:
|
||
|
||
```
|
||
if (PSUtils::get_extension(name) != ".keymap") name.append(".keymap");
|
||
full = path_append(GetKeymapDirectory(), name);
|
||
if (!check_access(full, 0)) goto write; // doesn't exist
|
||
else if (check_access(full, 2)) { if (prompt) MakeOverwriteKeymapDialog(name); else goto write; }
|
||
else MakeCantOverwriteReadOnlyKeymapDialog(name);
|
||
write:
|
||
gmClient::SetKeyMapFileName(Client::m_instance, name);
|
||
ICIDM::s_cidm->SaveKeyMap(full);
|
||
UpdateKeymapFilenameLabel();
|
||
UserPreferences::Save(); // 0x00437F30
|
||
```
|
||
|
||
`".keymap"` byte-verified at VA `0x007BEA84`; the Load dialog's glob
|
||
`"*.keymap"` at VA `0x007BEA79` (stored as `\*.keymap`).
|
||
|
||
**Format.** `CInputManager_WIN32::SaveKeyMap(path)` @ `0x00686C20`:
|
||
`CMasterInputMap::ToFileNode(&m_InputMap, node)` then
|
||
`PFileParser::SaveToFile(node, path)`. Loading is the mirror:
|
||
`CInputManager_WIN32::AddKeyMap(PStringBase<char> path)` @ `0x00686A90` →
|
||
`PFileParser::LoadFromFile` → `CMasterInputMap::FromFileNode` →
|
||
`CMasterInputMap::Merge`. So the `.keymap` file is Turbine's generic
|
||
`PFileParser` bracket-text format — exactly the
|
||
`Name [ value [ value ] ]` shape of `retail-default.keymap.txt` — and the
|
||
client **rewrites it on shutdown** (also stated in the file's own header,
|
||
line 14-15).
|
||
|
||
**Wire: none.** No packet is built anywhere in the keymap graph. Keybinds
|
||
are purely client-local, confirmed independently by the file header
|
||
comments and by the absence of any `Proto_*`/`CM_*` call in
|
||
`gmKeyboardUI`, `CInputManager_WIN32::SaveKeyMap`, or `ActionMap`.
|
||
|
||
### 5.8 How `retail-default.keymap.txt` relates to all of this
|
||
|
||
It is a **written-out user keymap**, not the DAT default. Evidence:
|
||
|
||
- Its title line is `"User Defined Keymap" [ 0000004B-0500-0000-0004-000016000000 ]`
|
||
(line 53) — a per-install GUID, and the name a user-saved file gets.
|
||
- It enumerates *this machine's* devices, including ten joystick GUIDs
|
||
(lines 60-69) that only a live `CInputManager` enumeration produces.
|
||
- Its group names are the InputMap names, matching §5.3 one-for-one for the
|
||
19 named ones.
|
||
- The DAT defaults are the *other* file,
|
||
`docs/research/named-retail/keymap-default.txt` — our `tools/dump-keymap`
|
||
output of DIDs `0x14000000` / `0x14000002`, in raw
|
||
`(scan, dev, Action=0x…, Activation=0x…)` form.
|
||
|
||
Practical consequence for acdream: `retail-default.keymap.txt` is a
|
||
legitimate oracle for *what the retail defaults look like after a round
|
||
trip*, and `keymap-default.txt` is the oracle for the **action ids** and
|
||
**activation codes** (`Activation=0x03` on most keyboard bindings,
|
||
`0x80` on some mouse ones). `KeyBindings.RetailDefaults()` already cites
|
||
the former.
|
||
|
||
---
|
||
|
||
## 6. acdream today (Q5 design input)
|
||
|
||
### 6.1 What exists and is good
|
||
|
||
| Piece | Where | Maps to retail |
|
||
|---|---|---|
|
||
| `InputAction` enum, ~148 values, retail-named, grouped in retail's categories (MovementCommands, ItemSelectionCommands, UICommands, QuickslotCommands, Chat, Combat, Emotes, Camera, Scroll, Mouse selection) + an explicit `Acdream*` extension block | `src/AcDream.UI.Abstractions/Input/InputAction.cs` | retail's ActionMap action ids |
|
||
| `Binding(KeyChord, InputAction, ActivationType, InputScope)`; **multiple bindings per action** supported (`KeyBindings.ForAction`) | `Input/Binding.cs`, `Input/KeyBindings.cs` | retail's `List<QualifiedControl>` per row |
|
||
| `ActivationType { Press, Release, Hold, DoubleClick, Click, Analog }` explicitly modelled on the keymap's sixth field | `Input/ActivationType.cs` | retail `QualifiedControl::m_activation` |
|
||
| `ModifierMask` / `KeyChord` | `Input/KeyChord.cs`, `Input/ModifierMask.cs` | retail MetaKeys bitfield |
|
||
| `KeyBindings.RetailDefaults()` — **the only production table**; `LoadOrDefault(path)` / `SaveToFile(path)` JSON | `Input/KeyBindings.cs:147, 402, 493` | retail `AddKeyMap(dat)` + `.keymap` file |
|
||
| `InputDispatcher.BeginCapture(Action<KeyChord>)` modal capture | `Input/InputDispatcher.cs:256` | `InitiateBinding` + `RegisterInputHandler(0x20)` |
|
||
| Conflict detection with a reassign prompt (`SettingsVM.PendingConflict`, "'X' is already bound to Y. Reassign it to Z?") | `Panels/Settings/SettingsVM.cs`, `Panels/Settings/SettingsPanel.cs:56-63` | `OpenOverwriteBindingDialog` (single-conflict subset) |
|
||
| Storage path `%LOCALAPPDATA%\acdream\keybinds.json` (portable via `ApplicationPathSet.KeyBindingsFile`) | `src/AcDream.Runtime/Platform/ApplicationPathSet.cs:53` | `<Documents>\Asheron's Call\*.keymap` |
|
||
|
||
### 6.2 What retail has that acdream does not
|
||
|
||
1. **Six-category × N-input-map two-level grouping.** acdream's enum has
|
||
categories as *comments*; there is no machine-readable `ActionClass` or
|
||
`InputMap` per action, so a retail-shaped screen cannot group rows today.
|
||
2. **Per-action labels and tooltips.** Retail reads them from the DAT
|
||
ActionMap (`GetDescripValues`). acdream has XML-doc comments only.
|
||
3. **A user-bindable flag.** Retail's `IsUserBindable(inputMap, action)`
|
||
hides debug/system actions and drives the "can't overwrite" refusal.
|
||
4. **Named keymap files: Load / Save / Revert to Saved / current-file
|
||
label.** acdream has one fixed `keybinds.json`.
|
||
5. **N-way cross-map conflict listing** vs acdream's single-conflict prompt.
|
||
6. **Right-click-to-erase a single binding, Clear-all per row.**
|
||
7. **Reset to Defaults sourced from DAT** vs acdream's C# table.
|
||
|
||
### 6.3 ⚠ The correction the handoff needs
|
||
|
||
The handoff says the F11 panel *is* the current rebind surface. **It is
|
||
not rendered at all.** Verified:
|
||
|
||
- `SettingsDevToolsComposition.cs:6-14`: "The optional ImGui developer-tools
|
||
frontend that used to compose here (VitalsPanel, ChatPanel, DebugPanel,
|
||
SettingsPanel via `AcDream.UI.ImGui`) was removed at Campaign V slice V11
|
||
… **until then keybind remapping falls back to editing keybinds.json.**"
|
||
- `GameplayInputCommandController.cs:56` — `DevToolsGameplayCommands`
|
||
implements `ToggleSettingsPanel()`, `ToggleDebugPanel()` and
|
||
`FocusChatInput()` as **empty no-ops**.
|
||
- Grep for `IPanelRenderer` implementations across `src/` returns **only the
|
||
interface**; the sole implementation in the tree is
|
||
`tests/AcDream.UI.Abstractions.Tests/FakePanelRenderer.cs`.
|
||
|
||
So `SettingsPanel` / `SettingsVM` / `DebugPanel` are **live, tested,
|
||
unrendered** code. They are an asset (the VM logic, conflict model and
|
||
rebind state machine are done and unit-tested) but there is **no shipping
|
||
key-rebinding UI in acdream today**.
|
||
|
||
### 6.4 The design options (planner's choice — I am not choosing)
|
||
|
||
**Option A — Reuse `SettingsVM` behind the retail button.**
|
||
Build one retail-authored Configure Keyboard layout, bind it to the
|
||
existing `SettingsVM` rebind/conflict logic, drop Load/Save-named-keymap
|
||
and the six-category grouping.
|
||
*Cost:* one `IPanelRenderer`-equivalent retained controller + a layout.
|
||
*Gets you:* a working rebind screen and, as a side effect, un-blocks
|
||
DebugPanel/VitalsPanel/ChatPanel re-homing (a carried debt from Campaign V).
|
||
*Divergence rows needed:* no named keymap files; no category grouping; no
|
||
per-action tooltips; single-conflict prompt.
|
||
|
||
**Option B — Port retail's screen shape without retail's data.**
|
||
Add `ActionClass` + `InputMapId` + `UserBindable` + `Label`/`Tooltip` to a
|
||
static C# action-metadata table (hand-authored from
|
||
`retail-default.keymap.txt` + §5.3), render six list boxes with headers,
|
||
N key buttons per row, Clear, right-click erase, and the N-way conflict
|
||
dialog. Keep `keybinds.json` as the store but add named
|
||
save/load/revert-to-saved.
|
||
*Cost:* materially larger — the metadata table alone is ~150 rows × 5 fields,
|
||
plus a real list-box/rows layout and three dialogs.
|
||
*Divergence rows needed:* metadata hand-authored instead of DAT-sourced;
|
||
JSON instead of `.keymap`.
|
||
|
||
**Option C — Port retail's screen *with* retail's data.**
|
||
Read the DAT `ActionMap` (DBO type `0x27`) and the two master input maps
|
||
(`0x14000000` / `0x14000002`) at runtime; drive labels, tooltips,
|
||
user-bindable, action class and defaults from the dats;
|
||
read/write real `.keymap` files through a `PFileParser` port.
|
||
*Cost:* highest — a `PFileParser` text codec, an `ActionMap` DBO reader,
|
||
and a mapping from retail action ids to acdream's `InputAction` enum
|
||
(`tools/dump-keymap` already proves the DAT read is feasible and gives the
|
||
action ids).
|
||
*Gets you:* zero divergence rows on this screen, and retail `.keymap` files
|
||
become interchangeable with the real client's — a genuinely nice property.
|
||
*Risk:* the action-id → `InputAction` mapping is the whole game; every
|
||
unmapped id is an invisible dead row.
|
||
|
||
**Cross-cutting notes for whichever is chosen:**
|
||
- Retail's storage is `<Documents>\Asheron's Call\<name>.keymap` +
|
||
a `keymap` key in `UserPreferences.ini`. acdream's portable equivalent
|
||
is `ApplicationPathSet.ConfigDirectory`. Options A/B keep
|
||
`keybinds.json`; only C needs a second format.
|
||
- Retail's OK/Cancel fire on **right-click release** (`idMessage 0x19`,
|
||
`dwParam1 == 7`) and Load/Save/Reset/Revert on **left-click**
|
||
(`idMessage 1`). That asymmetry is real and byte-anchored — do not
|
||
"normalise" it without a register row.
|
||
- Retail refuses to log off in mid-air (§2.2). If acdream ever implements
|
||
Exit to Character Selection for real, that check belongs with it.
|
||
|
||
---
|
||
|
||
## 7. For the planner
|
||
|
||
### 7.1 Per-button implementability
|
||
|
||
| Button | Verdict | Cheapest honest first cut |
|
||
|---|---|---|
|
||
| **Exit Game** | **exists** | Bind to `IGameplayWindowCommands.Close` (already the Escape target). `WorldSession.Dispose()` already does retail's `RequestLogOff` → `0xF653` → confirmation. No new wire, no register row. |
|
||
| **Exit to Character Selection** | **needs-new-subsystem** | Ship as "behaves as Exit Game" + **one register row** (adaptation). Real support needs a pre-world character-select UI *and* a `WorldSession` that can return to `InCharacterSelect` without socket teardown — neither exists. Retail's confirmation dialog (`ID_Client_EndCharacterSessionConfirm`) and the mid-air refusal are the behaviours to add when it lands. |
|
||
| **Configure Keyboard** | **adaptable** (Option A) / **large** (B, C) | See §6.4. Note this is currently the *only* way a user could rebind anything in-client — §6.3. |
|
||
| **Use Mouse Turning Settings** | **adaptable** | It is a 6-line macro, not a screen. 5 of 6 targets are client-local settings acdream can own; the 6th is `CharacterOptions2.UseMouseTurning (0x00400000)` through the existing `SetSingleCharacterOption (0x0005)` path. Blocked only on whether acdream *has* a persistent mouse-turning camera mode — **UNVERIFIED**, check the camera digest first. Emit the six retail chat lines verbatim (§4.4) if the mode exists. |
|
||
| **In-Game Help Files** | **asset-missing / needs-new-subsystem** | Retail loads `plugins\ACHelpPlugin.dll` through `keystone.dll`. There is nothing to port. Product decision: open a URL, open bundled docs, or hide the button. A hidden/disabled button needs a register row. |
|
||
| **Urgent Assistance** | **adaptable, but the target is dead** | EoR opens `http://support.turbine.com/ics/support/ticketnewwizard.asp?style=classic` — a defunct Turbine URL. Alternative with **server support today**: the legacy `gmUrgentAssistanceUI` path, `ChatChannel (0x0147)` on `Channel.Help (0x400)`, which ACE handles (`GameActionChatChannel.cs:84`). |
|
||
| **Report Abuse** | **server-missing** | EoR uses the same dead URL. The legacy path is `AbuseLogRequest (0x0140)` — the opcode is *named* in ACE (`GameActionType.cs:69`) but **no handler exists**, so it is a no-op against our server. Either wire the legacy UI knowing it does nothing, or hide the button. Register row either way. |
|
||
|
||
### 7.2 Configure Keyboard design options
|
||
|
||
Summarised in §6.4: **A** reuse `SettingsVM` behind a retail-authored
|
||
layout (small, several divergence rows, also unblocks the Campaign-V panel
|
||
debt); **B** port the screen shape over a hand-authored metadata table
|
||
(medium, fewer rows); **C** port screen *and* data from the DAT ActionMap +
|
||
`.keymap` files (large, zero rows on this screen, real `.keymap`
|
||
interchange).
|
||
|
||
### 7.3 Cross-lane findings worth stealing
|
||
|
||
- `gmConfigUI::InitOptions` @ `0x0049E400` is a **complete, ordered dump of
|
||
the Config tab** (handoff Q3): Sound features menu; Sound/Ambient/Interface
|
||
toggle+slider trios (default on, 1.0); "play sound only when active";
|
||
Camera Stiffness 0.45, Adjustment Speed 40.0, FOV 90.0, Align To Slope on;
|
||
Resolution menu default `0x03200258` (800×600, with
|
||
`UIOption::SetConfirmChange`), Full Screen on, Sync To Refresh off,
|
||
Screen Brightness 0, Automatic Degrades off, Graphics Performance 0,
|
||
Degrade Distance 50.0; Landscape/Environment texture detail 2/1, Texture
|
||
Filtering 1, Landscape Draw Distance 8, Building Detail Textures on,
|
||
Multi-Pass Alpha off; Mouse Look Sensitivity 0.55, Invert Mouselook Y off,
|
||
Use Mouse Turning off; Chat Font Face 2, Chat Font Size 1. Hand this to
|
||
whoever owns Q3.
|
||
- **Q4 (the `0x01A1` blob):** `CPlayerModule::SaveToServer(force)` @
|
||
`0x0059A660` is `if (m_bDirty || force) CM_Character::Event_CharacterOptionsEvent(playerModule)`.
|
||
`CPlayerModule::UseTime` @ `0x0059A710` auto-flushes a dirty module after
|
||
**480 seconds**, and `CPlayerSystem::LogOffCharacter` forces a flush before
|
||
logging off. So retail sends the blob on an 8-minute timer and at logoff —
|
||
not (only) on Apply.
|
||
- `CPlayerModule::OnInitialize` @ `0x0059A690` shows three options applied
|
||
**client-side at load**: `PersistentAtDay → LScape::SetDay`,
|
||
`DisableDistanceFog → LScape::m_fFogEnabled`,
|
||
`DisableMostWeatherEffects → SmartBox::EnableWeather`,
|
||
`ViewCombatTarget → ClientCombatSystem::TrackTarget`. Direct input for
|
||
handoff Q7 (which options have live consumers).
|
||
|
||
### 7.4 Open unknowns (do not let these get lost)
|
||
|
||
1. **UNVERIFIED — which LayoutDesc element ids are Configure Keyboard and
|
||
In-Game Help Files.** `0x10000204` / `0x10000205` are the unclaimed
|
||
neighbours but appear nowhere in the pseudo-C. Needs a layout dump.
|
||
2. **UNVERIFIED — how the Configure Keyboard button opens `gmKeyboardUI`.**
|
||
Two candidate declarative mechanisms in §4.3; neither confirmed.
|
||
3. **UNKNOWN — the `ActionClass` enum values.** The six list-box keys are
|
||
named globals (`Movement`, `Camera`, `Combat`, `UI`, `CharacterSettings`,
|
||
`Emote`); `acclient.h` keeps only `unsigned int m_eActionClass`.
|
||
Recoverable from `.data` at the six `HashTable::add` call sites
|
||
(`0x004DB80C` etc.) if a planner needs the numbers.
|
||
4. **UNKNOWN — which `DBObj::GetByEnum` enum (`0x10000001` vs `1`) is which
|
||
DID (`0x14000000` vs `0x14000002`).**
|
||
5. **UNVERIFIED — whether the six unnamed InputMaps** (`TargetedUsage`,
|
||
`SystemKeys`, `MouseCommands`, `ScrollableControls`, `EditControls`,
|
||
`CopyAndPasteControls`) **are filtered by `IsUserBindable` or render
|
||
headerless.**
|
||
6. **UNVERIFIED — whether acdream has a persistent "turn to face camera"
|
||
mouse-turning mode** (as opposed to `MouseLookState`'s MMB-hold
|
||
`CameraInstantMouseLook`). Check the camera/physics digest.
|
||
7. **UNKNOWN — the `0x54` InputAction** handled by
|
||
`gmGamePlayUI::HandleKeyPress` (toggles the whole gameplay UI's
|
||
visibility). Not needed for this lane; noted so nobody re-derives it.
|
||
8. **UNVERIFIED — retail's Exit Game confirmation.** The decompiled graph
|
||
shows none (`m_bLogoutConfirmed` is set by the button). If the user's
|
||
retail client *does* prompt, the prompt is authored in the layout.
|
||
|
||
### 7.5 COORDINATOR TODO (things this lane is forbidden to do)
|
||
|
||
- **Dump the Options-panel and Configure-Keyboard LayoutDescs** from
|
||
`client_portal.dat` (UI Studio: `AcDream.App ui-studio --dump`) and match
|
||
element ids `0x10000203`–`0x10000207`, `0x100005CC`, `0x10000617`,
|
||
`0x1000049D/9F/A1/A3/A5`, `0x10000211`, and attributes
|
||
`0x10000018`–`0x1000001F` to actual buttons/labels. This closes unknowns
|
||
1, 2 and confirms the whole §1 mapping from the layout side rather than
|
||
from error strings.
|
||
- **Decide and record** whether the F11 Settings panel comes back
|
||
(§6.3) — it is the difference between "Configure Keyboard is a new
|
||
screen" and "Configure Keyboard is a second front-end on an existing VM".
|
||
- Any `dotnet build` / `dotnet test` / client launch implied by the above.
|
||
|
||
---
|
||
|
||
## Appendix A — every retail anchor cited
|
||
|
||
| Symbol | Address |
|
||
|---|---|
|
||
| `gmGameplayOptionsUI::Register` | `0x0049E0F0` |
|
||
| `gmGameplayOptionsUI::Create` | `0x0049E060` |
|
||
| `gmGameplayOptionsUI::ListenToElementMessage` | `0x0049E110` |
|
||
| `gmGameplayOptionsUI::ListenToGlobalMessage` (ICF-folded, empty) | `0x004F5860` |
|
||
| `gmGameplayOptionsUI::PostInit` (ICF-folded) | `0x004BFA00` |
|
||
| `gmCharacterSettingsUI::ListenToElementMessage` | `0x0049E3A0` |
|
||
| `gmCharacterSettingsUI::GetUIElementType` (`0x10000027`) | `0x004A01C0` |
|
||
| `gmConfigUI::InitOptions` | `0x0049E400` |
|
||
| `gmConfigUI::PostInit` | `0x0049E840` |
|
||
| `gmConfigUI::SetMouseTurningDefaults` | `0x0049E8F0` |
|
||
| `gmConfigUI::ListenToGlobalMessage` | `0x0049EB90` |
|
||
| `gmConfigUI::GetUIElementType` (`0x10000028`) | `0x0049E2E0` |
|
||
| `CM_UI::SendNotice_EndCharacterSession` | `0x00479B40` |
|
||
| `gmGamePlayUI::RecvNotice_EndCharacterSession` | `0x004EBEA0` |
|
||
| `gmGamePlayUI::HandleKeyPress` | `0x004E9DF0` |
|
||
| `gmGamePlayUI::EndSession` | `0x004EA1B0` |
|
||
| `gmGamePlayUI::UseTime` | `0x004EA3A0` |
|
||
| `gmEpilogueUI::gmEpilogueUI` | `0x004E9B00` |
|
||
| `gmIndicatorsUI::ListenToElementMessage` (element `0x100000FA` → same notice) | `0x004BFA90` |
|
||
| `CPlayerSystem::LogOffCharacter` | `0x00563520` |
|
||
| `CPlayerSystem::RequestLogOff` | `0x00562DD0` |
|
||
| `CPlayerSystem::ExecuteLogOff` | `0x0055D780` |
|
||
| `Proto_UI::LogOffCharacter` (`0xF653`) | `0x00546A20` |
|
||
| `CPlayerModule::SaveToServer` | `0x0059A660` |
|
||
| `CPlayerModule::OnInitialize` | `0x0059A690` |
|
||
| `CPlayerModule::UseTime` (480 s dirty flush) | `0x0059A710` |
|
||
| `KeyStone::Init` | `0x00556CF0` |
|
||
| `KeyStone::OpenHelp` | `0x00557010` |
|
||
| `ClientUISystem::OnAction` (action `0x7B` → help) | `0x00564B90` |
|
||
| `UIElementManager::BroadcastGlobalMessage` | `0x0045B3E0` |
|
||
| `UIElementManager::KeyPressEvent` | `0x0045C300` |
|
||
| `UIElementManager::ActionHandler` | `0x0045CC00` |
|
||
| `UIElementManager::DoVisibilityToggleAction` | `0x0045B660` |
|
||
| `UIListener::RegisterForGlobalMessage` | `0x00465E60` |
|
||
| `gmAbuseUI::PostInit` / `ReportAbuse` / `ListenToElementMessage` / `RecvNotice_AbuseReportResponse` | `0x004BBF30` / `0x004BC380` / `0x004BC5C0` / `0x004BC1B0` |
|
||
| `gmAbuseUI::GetUIElementType` (`0x10000018`) | `0x004BBF20` |
|
||
| `CM_Character::Event_AbuseLogRequest` (opcode `0x0140`) | `0x006A2A30` |
|
||
| `CM_Character::SendNotice_AbuseReportResponse` | `0x006A24B0` |
|
||
| `gmUrgentAssistanceUI::PostInit` / `ListenToElementMessage` | `0x004A7840` / `0x004A7940` |
|
||
| `gmUrgentAssistanceUI::GetUIElementType` (`0x1000001F`) | `0x004A7830` |
|
||
| `CM_Communication::Event_ChannelBroadcast` (channel `0x400` = Help) | `0x006A4030` |
|
||
| `gmKeyboardUI::Register` (`0x1000000E`) | `0x004DCF70` |
|
||
| `gmKeyboardUI::PostInit` | `0x004DB770` |
|
||
| `gmKeyboardUI::InitOptions` | `0x004DD8B0` |
|
||
| `gmKeyboardUI::AddActionKeyMap` | `0x004DB2F0` |
|
||
| `gmKeyboardUI::GetStringInfoFromInputMapID` | `0x004DA980` |
|
||
| `gmKeyboardUI::GetKeymapDirectory` | `0x004DA8E0` |
|
||
| `gmKeyboardUI::SaveKeymap` | `0x004DCF90` |
|
||
| `gmKeyboardUI::RestoreDefaultValues` | `0x004DA850` |
|
||
| `gmKeyboardUI::ListenToElementMessage` | `0x004DD230` |
|
||
| `gmKeyboardUI::MakeLoadKeymapDialog` / `MakeSaveKeymapDialog` | `0x004DC0B0` / `0x004DC5B0` |
|
||
| `gmKeyboardUI::MakeOverwriteKeymapDialog` / `MakeCantOverwriteReadOnlyKeymapDialog` | `0x004DCA20` / `0x004DC7B0` |
|
||
| `gmKeyboardUI::HandleCloseLoadKeymapDialog` / `HandleCloseSaveKeymapDialog` | `0x004DAC20` / `0x004DD160` |
|
||
| `gmKeyboardUI::UpdateKeymapFilenameLabel` | `0x004DB290` |
|
||
| `gmClient::SetKeyMapFileName` | `0x00401EF0` |
|
||
| `UserPreferences::Save` | `0x00437F30` |
|
||
| `UIOption_ActionKeyMap::Init` | `0x004888E0` |
|
||
| `UIOption_ActionKeyMap::InitiateBinding` | `0x004899D0` |
|
||
| `UIOption_ActionKeyMap::KeyHitHandler` | `0x00489570` |
|
||
| `UIOption_ActionKeyMap::SetBinding` | `0x00487B20` |
|
||
| `UIOption_ActionKeyMap::EraseBinding` | `0x00487780` |
|
||
| `UIOption_ActionKeyMap::ClearAllBindings` | `0x00487ED0` |
|
||
| `UIOption_ActionKeyMap::OpenMapWarnDialog` | `0x00488A00` |
|
||
| `UIOption_ActionKeyMap::OpenOverwriteBindingDialog` | `0x00488BF0` |
|
||
| `UIOption_ActionKeyMap::OpenCantOverwriteBindingDialog` | `0x00489300` |
|
||
| `UIOption_ActionKeyMap::ListenToElementMessage` | `0x00489A80` |
|
||
| `UIOption_ActionKeyMap::Save/Restore/RestoreDefault Value` | `0x00488260` / `0x00488280` / `0x004884F0` |
|
||
| `OptionPage::SaveCurrentValues` / `RestoreSavedValues` / `RestoreDefaultValues` / `Changed` / `RegisterOption` | `0x004F2C60` / `0x004F2D00` / `0x004F2CB0` / `0x004F2D60` / `0x004F2E90` |
|
||
| `ActionMap::IsUserBindable` | `0x006854E0` |
|
||
| `ActionMap::GetActionClass` | `0x006855C0` |
|
||
| `ActionMap::GetDescripValues` | `0x00685690` |
|
||
| `ActionMap::GetDBOType` (returns `0x27`) | `0x00685C00` |
|
||
| `ActionMap::Serialize` | `0x00685F10` |
|
||
| `CInputManager_WIN32::InitializeKeymap` | `0x00686010` |
|
||
| `CInputManager_WIN32::AddKeyMap(uint32)` | `0x006864C0` |
|
||
| `CInputManager_WIN32::AddKeyMap(PStringBase<char>)` | `0x00686A90` |
|
||
| `CInputManager_WIN32::SaveKeyMap` | `0x00686C20` |
|
||
|
||
## Appendix B — byte-verified literals
|
||
|
||
| VA | file offset | Content |
|
||
|---|---|---|
|
||
| `0x007A82A0` | `0x3A82A0` | `http://support.turbine.com/ics/support/ticketnewwizard.asp?style=classic` (pushed at `0x0049E14B` **and** `0x0049E1E7`) |
|
||
| `0x007A81E0` | — | urgent-assistance browser-failure message |
|
||
| `0x007A8128` | — | abuse-report browser-failure message |
|
||
| `0x00794078` | — | `Asheron's Call Error` |
|
||
| `0x007C2CEC` | `0x3C2CEC` | `ID_Client_EndCharacterSessionConfirm` |
|
||
| `0x007C29C0` | — | `Cannot log off while in mid-air.` (UTF-16LE) |
|
||
| `0x007A30AC` | `0x3A30AC` | `ID_ActionKeyMap_MapInstructions` |
|
||
| `0x007BE45C`–`0x007BE648` | `0x3BE45C`–`0x3BE648` | all 19 `ID_InputMap_*` strings (§5.3) |
|
||
| `0x007BEA79` / `0x007BEA84` | — | `*.keymap` / `.keymap` |
|
||
| `0x007BEA8C` / `0x007BEAB8` / `0x007BEAD4` / `0x007BEAF4` | — | `ID_KeyMapCantOverwriteReadOnlyKeymap_Label` / `ID_KeyMapLoadKeymap_Label` / `ID_KeyMapOverwriteKeymap_Label` / `ID_KeyMapSaveKeymap_Label` |
|
||
| `0x00792DBB` | `0x392DBB` | `UserPreferences.ini` (with `%s\Asheron's Call` adjacent) |
|
||
| `0x00792FA8` / `0x00792FDA` | `0x392FA8` | `keymap` / `The filename of the keymap file to use` |
|
||
| `0x007CB834` / `0x007CB808` / `0x007CB7CC` | — | `keystone.dll` / `plugins\ACHelpPlugin.dll` / `plugins\ACPluginManager.dll` |
|
||
| `0x007A8A70` / `0x007A8A20` / `0x007A89D0` / `0x007A8980` / `0x007A8928` / `0x007A88D0` | — | the six mouse-turning-defaults chat lines (§4.4) |
|
||
|
||
## Appendix C — BN artifact classes hit in this lane
|
||
|
||
Recorded so the next reader does not mistake them for evidence:
|
||
|
||
1. **ICF (identical-code folding) name aliasing.** `0x004F5860` is printed
|
||
as `gmKeyboardUI::ListenToGlobalMessage` *and*
|
||
`gmGameplayOptionsUI::…` *and* `gmAbuseUI::…` *and*
|
||
`gmUrgentAssistanceUI::…`; `0x004BFA00` is printed as
|
||
`gmCGProfessionPage::PostInit` when reached as
|
||
`gmGameplayOptionsUI::PostInit`. Shared address ≠ shared design.
|
||
2. **vtable-slot-as-string-pointer.** BN renders string operands it cannot
|
||
resolve as `&gmConfigUI::\`vftable'.RecvNotice_OpenTrade` etc. Five of
|
||
the six mouse-turning chat strings appear that way; all six were
|
||
recovered by the push-imm32 sweep instead.
|
||
3. **`test ah, 0x44` / `jnp` float comparisons.** BN emits
|
||
`bool p_1 = unimplemented {test ah, 0x44}` and then guesses the branch.
|
||
Polarity must be read from the bytes (§4.4).
|
||
4. **Wrong struct-field attribution.** `UIOption_ActionKeyMap`'s
|
||
`m_buttonClear` / `m_aKeyButtons` appear as
|
||
`this->m_lastCursor.m_x0 / m_y0 / m_y1`; `gmKeyboardUI`'s hash table is
|
||
typed `HashTable<ControlSpecification, enum ControlType, 0>`. Always
|
||
reconcile against `acclient.h`.
|
||
5. **~33-character string previews.** `"ID_InputMap_ItemSelectionCommand…"`,
|
||
`"ID_InputMap_CharacterOptionComma…"`,
|
||
`"ID_InputMap_CameraAlternateContr…"` — all three were truncated and all
|
||
three were completed from the binary.
|