Pre-Phase K research artifact. Captures the AC retail default keymap
in two complementary forms so the upcoming InputAction enum + retail
preset (Phase K.1c) can be built byte-precise.
- docs/research/named-retail/retail-default.keymap.txt — verbatim
copy of the user's test.keymap from
~/Documents/Asheron's Call/. Human-readable text format with
every binding categorized: MovementCommands (W/X/A/D/Z/C/Q/Space/
LShift/S + Y/G/H/B postures), ItemSelectionCommands (F/T/P + 18
punctuation keys for compass/item/monster/player/fellow targeting),
UICommands (F1-F12 panel toggles, R=USE, E=Examine, Esc=close,
Shift+Esc=Logout), QuickslotCommands (1-9 + Ctrl/Alt variants for
hotbar pages), Combat / MeleeCombat / MissileCombat / MagicCombat
(mode-dependent Insert/PgUp/Delete/End/PgDn), Emotes
(U=Cry, I=Laugh, J=Wave, O=Cheer, K=Point), CameraControls (numpad
cluster), MouseCommands, ScrollableControls, EditControls,
CopyAndPasteControls, DialogBoxes. 346 lines.
- docs/research/named-retail/keymap-default.txt — binary dump of
the gmDefaultMap MasterInputMap from client_portal.dat at file id
0x14000000. Decoded via the new tools/dump-keymap utility:
scancodes + modifier flags + action IDs + activation phase per
context. Confirms the text file's bindings against the dat-shipped
default. Cross-referenced against
acclient_2013_pseudo_c.txt:405510 (ACCmdInterp::OnAction) for the
movement dispatch logic and :365889 (CPlayerSystem::OnAction) for
the targeting dispatch.
- tools/dump-keymap/ — dotnet console tool referencing
references/DatReaderWriter. Reads MasterInputMap entries from a
dat directory + emits human-readable per-context binding tables.
Reusable for future custom keymap analysis. Run with:
dotnet run --project tools/dump-keymap/dump-keymap.csproj -c Release
Default dat dir is %USERPROFILE%/Documents/Asheron's Call.
Foundation for Phase K — control system overhaul. Plan documented at
~/.claude/plans/ticklish-conjuring-cake.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
212 lines
8 KiB
C#
212 lines
8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using DatReaderWriter;
|
|
using DatReaderWriter.DBObjs;
|
|
|
|
// Dumps the retail-default keymap (gmDefaultMap @ 0x14000000) from
|
|
// client_portal.dat. Used for the Phase K control overhaul — extracts
|
|
// the canonical "default bindings" so we can ship a Retail preset
|
|
// that exactly matches what AC1 players were trained on.
|
|
|
|
string datDir = args.Length > 0
|
|
? args[0]
|
|
: Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile),
|
|
"Documents", "Asheron's Call");
|
|
|
|
if (!Directory.Exists(datDir))
|
|
{
|
|
Console.Error.WriteLine($"dat dir not found: {datDir}");
|
|
return 1;
|
|
}
|
|
|
|
Console.WriteLine($"# Reading dats from: {datDir}");
|
|
using var dat = new DatCollection(datDir);
|
|
|
|
// gmDefaultMap is at 0x14000000 per DatReaderWriter test
|
|
// CanReadEORKeymaps. The test also references 0x14000002 (unnamed).
|
|
foreach (uint id in new uint[] { 0x14000000u, 0x14000002u })
|
|
{
|
|
Console.WriteLine();
|
|
Console.WriteLine($"## MasterInputMap 0x{id:X8}");
|
|
var map = dat.Get<MasterInputMap>(id);
|
|
if (map is null)
|
|
{
|
|
Console.WriteLine($" (not found)");
|
|
continue;
|
|
}
|
|
Console.WriteLine($" Name: {map.Name}");
|
|
Console.WriteLine($" GuidMap: {map.GuidMap}");
|
|
Console.WriteLine($" Devices: {map.Devices.Count}");
|
|
foreach (var d in map.Devices)
|
|
{
|
|
Console.WriteLine($" {d.Type} {d.Guid}");
|
|
}
|
|
Console.WriteLine($" MetaKeys ({map.MetaKeys.Count}):");
|
|
foreach (var mk in map.MetaKeys)
|
|
{
|
|
var (scan, dev) = SplitKey(mk.Key);
|
|
Console.WriteLine($" {Dik(scan),-12} (scan=0x{scan:X2}, dev={dev}) ModifierFlag=0x{mk.Modifier:X8}");
|
|
}
|
|
Console.WriteLine($" InputMaps ({map.InputMaps.Count}):");
|
|
// Sort contexts by ID for stable output
|
|
var sortedCtx = new List<uint>(map.InputMaps.Keys);
|
|
sortedCtx.Sort();
|
|
foreach (var ctxId in sortedCtx)
|
|
{
|
|
var inputMap = map.InputMaps[ctxId];
|
|
Console.WriteLine();
|
|
Console.WriteLine($" Context 0x{ctxId:X8} ({inputMap.Mappings.Count} bindings):");
|
|
// Sort bindings by scan code for readability
|
|
var sortedBindings = new List<DatReaderWriter.Types.QualifiedControl>(inputMap.Mappings);
|
|
sortedBindings.Sort((a, b) => a.Key.Key.CompareTo(b.Key.Key));
|
|
foreach (var m in sortedBindings)
|
|
{
|
|
var (scan, dev) = SplitKey(m.Key.Key);
|
|
string mods = ModifierString(m.Key.Modifier);
|
|
Console.WriteLine(
|
|
$" {Dik(scan),-12}{(mods.Length > 0 ? "+" + mods : " ")} " +
|
|
$"(scan=0x{scan:X2}, dev={dev}) " +
|
|
$"Action=0x{m.Unknown:X8} " +
|
|
$"Activation=0x{m.Activation:X2}");
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
|
|
// ── Key decoding (scan code in high word, device id in low word) ──────
|
|
static (uint scan, uint dev) SplitKey(uint key) => ((key >> 16) & 0xFFFFu, key & 0xFFFFu);
|
|
|
|
// ── Modifier flag bitfield ────────────────────────────────────────────
|
|
static string ModifierString(uint flag) =>
|
|
flag == 0 ? "" : string.Join("|", new[]
|
|
{
|
|
(flag & 0x80000000u) != 0 ? "Shift" : null,
|
|
(flag & 0x40000000u) != 0 ? "Ctrl" : null,
|
|
(flag & 0x20000000u) != 0 ? "Alt" : null,
|
|
(flag & 0x10000000u) != 0 ? "Win" : null,
|
|
(flag & 0x08000000u) != 0 ? "Meta4" : null,
|
|
(flag & 0x04000000u) != 0 ? "Meta5" : null,
|
|
}.Where(s => s is not null));
|
|
|
|
// ── DirectInput scan-code → name (DIK_*) ──────────────────────────────
|
|
//
|
|
// Verified subset against acclient_2013_pseudo_c.txt
|
|
// ControlNameMapper::AddKeySemantic calls (lines 656172-656272). Rest
|
|
// from standard Microsoft DirectInput dinput.h DIK_* table.
|
|
static string Dik(uint code) => code switch
|
|
{
|
|
0x01 => "ESCAPE",
|
|
0x02 => "1", 0x03 => "2", 0x04 => "3", 0x05 => "4", 0x06 => "5",
|
|
0x07 => "6", 0x08 => "7", 0x09 => "8", 0x0a => "9", 0x0b => "0",
|
|
0x0c => "MINUS", 0x0d => "EQUALS", 0x0e => "BACK", 0x0f => "TAB",
|
|
0x10 => "Q", 0x11 => "W", 0x12 => "E", 0x13 => "R", 0x14 => "T",
|
|
0x15 => "Y", 0x16 => "U", 0x17 => "I", 0x18 => "O", 0x19 => "P",
|
|
0x1a => "LBRACKET", 0x1b => "RBRACKET", 0x1c => "RETURN", 0x1d => "LCONTROL",
|
|
0x1e => "A", 0x1f => "S", 0x20 => "D", 0x21 => "F", 0x22 => "G",
|
|
0x23 => "H", 0x24 => "J", 0x25 => "K", 0x26 => "L",
|
|
0x27 => "SEMICOLON", 0x28 => "APOSTROPHE", 0x29 => "GRAVE",
|
|
0x2a => "LSHIFT", 0x2b => "BACKSLASH",
|
|
0x2c => "Z", 0x2d => "X", 0x2e => "C", 0x2f => "V", 0x30 => "B",
|
|
0x31 => "N", 0x32 => "M", 0x33 => "COMMA", 0x34 => "PERIOD",
|
|
0x35 => "SLASH", 0x36 => "RSHIFT", 0x37 => "MULTIPLY",
|
|
0x38 => "LMENU", 0x39 => "SPACE", 0x3a => "CAPITAL",
|
|
0x3b => "F1", 0x3c => "F2", 0x3d => "F3", 0x3e => "F4", 0x3f => "F5",
|
|
0x40 => "F6", 0x41 => "F7", 0x42 => "F8", 0x43 => "F9", 0x44 => "F10",
|
|
0x45 => "NUMLOCK", 0x46 => "SCROLL",
|
|
0x47 => "NUMPAD7", 0x48 => "NUMPAD8", 0x49 => "NUMPAD9",
|
|
0x4a => "SUBTRACT",
|
|
0x4b => "NUMPAD4", 0x4c => "NUMPAD5", 0x4d => "NUMPAD6",
|
|
0x4e => "ADD",
|
|
0x4f => "NUMPAD1", 0x50 => "NUMPAD2", 0x51 => "NUMPAD3",
|
|
0x52 => "NUMPAD0", 0x53 => "DECIMAL",
|
|
0x57 => "F11", 0x58 => "F12",
|
|
0x9c => "NUMPADENTER", 0x9d => "RCONTROL",
|
|
0xb5 => "DIVIDE", 0xb7 => "SYSRQ", 0xb8 => "RMENU",
|
|
0xc7 => "HOME", 0xc8 => "UP", 0xc9 => "PRIOR" /*PgUp*/,
|
|
0xcb => "LEFT", 0xcd => "RIGHT",
|
|
0xcf => "END", 0xd0 => "DOWN", 0xd1 => "NEXT" /*PgDn*/,
|
|
0xd2 => "INSERT", 0xd3 => "DELETE",
|
|
_ => $"?0x{code:X2}",
|
|
};
|
|
|
|
// ── Action ID → name ──────────────────────────────────────────────────
|
|
//
|
|
// Extracted verbatim from acclient_2013_pseudo_c.txt
|
|
// command_strings table at 0x00803df0 (lines 1067906-1068117).
|
|
// Subset — covers indices 0x000-0x0d5; the rest are mostly emote
|
|
// variants (Cheer, ChestBeat, FallDown, etc.) we don't need for
|
|
// keymap analysis.
|
|
static string Action(uint id) => id switch
|
|
{
|
|
0x000 => "Invalid",
|
|
0x001 => "HoldRun",
|
|
0x002 => "HoldSidestep",
|
|
0x003 => "Ready",
|
|
0x004 => "Stop",
|
|
0x005 => "WalkForward",
|
|
0x006 => "WalkBackwards",
|
|
0x007 => "RunForward",
|
|
0x008 => "Fallen",
|
|
0x009 => "Interpolating",
|
|
0x00a => "Hover",
|
|
0x00d => "TurnRight",
|
|
0x00e => "TurnLeft",
|
|
0x00f => "SideStepRight",
|
|
0x010 => "SideStepLeft",
|
|
0x011 => "Dead",
|
|
0x012 => "Crouch",
|
|
0x013 => "Sitting",
|
|
0x014 => "Sleeping",
|
|
0x015 => "Falling",
|
|
0x018 => "Pickup",
|
|
0x01d => "JumpCharging",
|
|
0x03b => "Jump",
|
|
0x03c => "HandCombat",
|
|
0x03d => "NonCombat",
|
|
0x0a2 => "Cancel",
|
|
0x0a3 => "UseSelected",
|
|
0x0a4 => "AutosortSelected",
|
|
0x0a5 => "DropSelected",
|
|
0x0a6 => "GiveSelected",
|
|
0x0a7 => "SplitSelected",
|
|
0x0a8 => "ExamineSelected",
|
|
0x0a9 => "CreateShortcutToSelected",
|
|
0x0aa => "PreviousCompassItem",
|
|
0x0ab => "NextCompassItem",
|
|
0x0ac => "ClosestCompassItem",
|
|
0x0ad => "PreviousSelection",
|
|
0x0ae => "LastAttacker",
|
|
0x0af => "PreviousFellow",
|
|
0x0b0 => "NextFellow",
|
|
0x0b1 => "ToggleCombat",
|
|
0x0b2 => "HighAttack",
|
|
0x0b3 => "MediumAttack",
|
|
0x0b4 => "LowAttack",
|
|
0x0b5 => "EnterChat",
|
|
0x0b6 => "ToggleChat",
|
|
0x0b7 => "SavePosition",
|
|
0x0b8 => "OptionsPanel",
|
|
0x0b9 => "ResetView",
|
|
0x0ba => "CameraLeftRotate",
|
|
0x0bb => "CameraRightRotate",
|
|
0x0bc => "CameraRaise",
|
|
0x0bd => "CameraLower",
|
|
0x0be => "CameraCloser",
|
|
0x0bf => "CameraFarther",
|
|
0x0c0 => "FloorView",
|
|
0x0c1 => "MouseLook",
|
|
0x0c2 => "PreviousItem",
|
|
0x0c3 => "NextItem",
|
|
0x0c4 => "ClosestItem",
|
|
0x0c5 => "ShiftView",
|
|
0x0c6 => "MapView",
|
|
0x0c7 => "AutoRun",
|
|
0x0c8 => "DecreasePowerSetting",
|
|
0x0c9 => "IncreasePowerSetting",
|
|
0x0d3 => "CastSpell",
|
|
0x0d5 => "FirstPersonView",
|
|
_ => $"?0x{id:X4}",
|
|
};
|