acdream/docs/research/2026-08-10-keyboard-config-and-gameplay-tab.md
Erik 29138430ef docs: Campaign OP research — Configure Keyboard + Gameplay-tab actions (lane D)
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>
2026-08-10 22:43:45 +02:00

59 KiB
Raw Permalink Blame History

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 0x10000203CM_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 0x10000027gmGamePlayUI::HandleKeyPressQueueUIMode(0x10000009) (epilogue) → LogOffCharacter(1) 0xF653 (have it) existsIGameplayWindowCommands/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 0x100005CCBroadcastGlobalMessage(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 0x10000206ShellExecuteA("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 0x10000207the 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 1confirmation 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 airbornetransient_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 (CharacterLogOffsession.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 0x004E9B7Aimmediate = 1ExecuteLogOff 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

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.

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 @ 0x004BC5C0gmAbuseUI::ReportAbuse @ 0x004BC380 CM_Character::Event_AbuseLogRequest(name, 1, complaint) @ 0x006A2A30GameAction 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 0x0049EB57the 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 50jnp 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 15 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 @ 0x004DD160SaveKeymap(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) @ 0x00686A90PFileParser::LoadFromFileCMasterInputMap::FromFileNodeCMasterInputMap::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:56DevToolsGameplayCommands 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 RequestLogOff0xF653 → 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 0x100002030x10000207, 0x100005CC, 0x10000617, 0x1000049D/9F/A1/A3/A5, 0x10000211, and attributes 0x100000180x1000001F 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
0x007BE45C0x007BE648 0x3BE45C0x3BE648 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.