fix #394 #395 #396: OP8 re-gate round — caption font, retail key names, capture dialog

Three findings from the user's first Configure Keyboard look (OP8 gate,
2026-08-14), each root-caused against the named retail decomp:

- #394 row-caption font: the synthesized action-label UiText never set
  DatFont and fell to the debug bitmap font. The authored row template
  (0x21000009/0x1000002F, retail UIOption_ActionKeyMap) carries FontDid
  0x4000000A (18px serif) — Bind now takes resolveTemplateFont and applies
  the template's own authored font, resolved once per template pair.

- #395 key captions: raw enum spellings ("Shift+ShiftLeft") replaced by the
  port of CInputManager_WIN32::GetNameFromKey @0x00687F40 /
  GetNameFromKey_Internal @0x00687800 (RetailKeyNames): DAT string-table
  override by DIK-name hash (key enum 4 -> 0x2300000A, meta enum 5 ->
  0x2300000B, delimiter enum 3 -> 0x23000007 — GetDIDByEnum category 4,
  live-probed), else the OS keyboard layout's own key name ("SKIFT") via
  PlatformKeyNameProvider (Win32 GetKeyNameTextW — register row AD-96 for
  the DirectInput-vs-GetKeyNameText adaptation), else the DIK-suffix
  spelling. Bare modifier-key bindings show only the key name.

- #396 capture feedback: clicking a mapping button now opens retail's
  instruction dialog (InitiateBinding @0x004899D0 -> OpenMapWarnDialog
  @0x00488A00): a type-2 WAIT dialog on retail's MapWarn queue key
  0x10000001 with ID_ActionKeyMap_MapInstructions (0x23000004, ACTION
  variable interpolated), closed on key hit or ESC through the capture
  callback; capture is not armed if the dialog cannot open, matching
  retail. New RetailWaitDialogView (wait root 0x31 — same authored
  popup/message pair 0x3D/0x3E as the confirmation root, live-DAT probed)
  behind a shared IRetailDialogView presenter seam.

Probe evidence (env-gated, kept):
KeyboardConfigLiveMountProbeTests.ProbeKeyboardFontsAndKeyNameStrings.
Register: AD-96 filed. Gate script OP8 section updated (step 4 rewritten;
the "pressed/active state is enough" contract is retired).

Full Release solution suite green (13,424 passed / 4 skips).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-14 13:45:55 +02:00
parent 1528e5693b
commit 30fa6ee507
16 changed files with 1105 additions and 24 deletions

View file

@ -143,6 +143,152 @@ public sealed class KeyboardConfigLiveMountProbeTests
}
}
/// <summary>
/// TEMPORARY OP8 re-gate probe (2026-08-14): three user findings — wrong
/// row/button fonts, raw enum key captions ("Shift+ShiftLeft" vs retail's
/// OS-localized "SKIFT"), and no capture-instruction dialog. This dumps the
/// facts the fixes need from the INSTALLED dat: (a) authored FontDids on
/// the header/row templates + key buttons and the Font DBObj metrics behind
/// them, (b) which font DIDs the production template build actually
/// requests, (c) which string table holds ID_ActionKeyMap_MapInstructions /
/// ID_KeyDescDelimiter / ID_KeyNameWithSubControl and the DIK_* key names
/// retail's GetNameFromKey_Internal @0x687800 looks up by hash.
/// </summary>
[Fact]
public void ProbeKeyboardFontsAndKeyNameStrings()
{
if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_LIVE_MOUNT") != "1")
return;
var datDir = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR")
?? Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Documents",
"Asheron's Call");
using var dats = new DatCollection(datDir, DatAccessType.Read);
var strings = new DatStringResolver(dats);
// (a) Font DBObj metrics for every DID the fixture shows in the
// template region (+ the 0x40000000 default the mount falls back to).
foreach (uint did in new[] { 0x40000000u, 0x40000001u, 0x4000000Au, 0x4000000Fu })
{
if (dats.TryGet<DatReaderWriter.DBObjs.Font>(did, out var font) && font is not null)
Console.WriteLine(
$"[kbfont] font 0x{did:X8} MaxCharHeight={font.MaxCharHeight} "
+ $"glyphs={font.CharDescs.Count} fg=0x{font.ForegroundSurfaceDataId:X8} "
+ $"bg=0x{font.BackgroundSurfaceDataId:X8}");
else
Console.WriteLine($"[kbfont] font 0x{did:X8} -> MISSING from dat");
}
// Authored FontDids on the live-imported templates.
foreach (uint templateId in new[] { 0x1000002Eu, 0x1000002Fu })
{
ElementInfo? tInfo = LayoutImporter.ImportInfos(
dats, KeyboardConfigController.LayoutId, templateId);
if (tInfo is null)
{
Console.WriteLine($"[kbfont] template 0x{templateId:X8} -> IMPORT MISSING");
continue;
}
DumpFontDids(tInfo, 0);
}
// (b) Which font DIDs the production-shaped template build requests.
{
ElementInfo? rowInfo = LayoutImporter.ImportInfos(
dats, KeyboardConfigController.LayoutId, 0x1000002Fu);
Assert.NotNull(rowInfo);
var requested = new List<uint>();
UiElement built = LayoutImporter.Build(
rowInfo!, _ => (0u, 0, 0), null,
did => { requested.Add(did); return null; },
strings.Resolve).Root;
Console.WriteLine(
"[kbfont] row-template build requested fonts: "
+ string.Join(", ", requested.Select(d => $"0x{d:X8}")));
foreach (uint keyBtn in new[] { 0x10000030u, 0x10000031u, 0x10000032u })
{
if (UiElement.FindDescendant(built, keyBtn) is UiButton b)
Console.WriteLine(
$"[kbfont] key-button 0x{keyBtn:X8} LabelFont={(b.LabelFont is null ? "<null>" : "set")}");
}
}
// (c) String sweep: which table answers the hashes retail uses.
string[] keys =
{
"ID_ActionKeyMap_MapInstructions",
"ID_KeyDescDelimiter",
"ID_KeyNameWithSubControl",
"ID_KeyMapCantOverwriteReadOnlyKeymap_Label",
"DIK_W", "DIK_X", "DIK_S", "DIK_LSHIFT", "DIK_UP", "DIK_LCONTROL",
"DIK_LMENU", "DIK_RSHIFT", "DIK_RCONTROL", "DIK_RMENU",
"DIK_NUMPADENTER", "DIK_DELETE", "DIK_INSERT", "DIK_PRIOR", "DIK_NEXT",
"MOUSE_B1", "SHIFT", "CTRL", "ALT",
};
for (uint table = 0x23000001u; table <= 0x2300000Cu; table++)
{
DatReaderWriter.DBObjs.StringTable? st = null;
try { st = dats.Get<DatReaderWriter.DBObjs.StringTable>(table); }
catch { /* absent table id — sweep continues */ }
if (st is null) continue;
foreach (string key in keys)
{
if (!st.Strings.TryGetValue(DatStringResolver.ComputeHash(key), out var entry)
|| entry.Strings.Count == 0)
continue;
string fragments = string.Join(
"¦", entry.Strings.Select(s => s.Value));
string variables = entry.Variables.Count == 0
? ""
: " vars=[" + string.Join(",", entry.Variables.Select(v => $"0x{v:X8}")) + "]";
Console.WriteLine(
$"[kbstr] table 0x{table:X8} '{key}' -> '{fragments}'{variables}");
}
}
// Candidate variable-name hashes for the MapInstructions template slot.
foreach (string candidate in new[] { "ACTION", "NAME", "KEY", "SUBCONTROL", "PLAYER", "COMMAND" })
Console.WriteLine(
$"[kbstr] hash('{candidate}') = 0x{DatStringResolver.ComputeHash(candidate):X8}");
// GetDIDByEnum sweep: which category/enum resolves the string-table
// DIDs retail's GetNameFromKey_Internal passes as "table enum" 4/5
// (and InitiateBinding's 0x10000004)?
foreach (uint category in new uint[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 })
{
foreach (uint enumValue in new uint[] { 3, 4, 5, 0x10000004 })
{
uint did = AcDream.Content.RetailDataIdResolver.Resolve(dats, enumValue, category);
if (did != 0)
Console.WriteLine(
$"[kbenum] category={category} enum=0x{enumValue:X} -> DID 0x{did:X8}");
}
}
// The Wait dialog (retail MapWarn/capture-instruction dialog, type 2,
// root 0x31 per DialogFactory::CreateDialog_ @ 0x00477AD0) — the
// committed dialogs_2100003C.json fixture only carries the
// confirmation root, so dump the wait root's authored subtree here.
{
ElementInfo? waitInfo = LayoutImporter.ImportInfos(dats, 0x2100003Cu, 0x31u);
if (waitInfo is null)
Console.WriteLine("[kbwait] wait root 0x31 -> IMPORT MISSING from 0x2100003C");
else
DumpFontDids(waitInfo, 0);
}
}
private static void DumpFontDids(ElementInfo info, int depth)
{
Console.WriteLine(
$"[kbfont] {new string(' ', depth * 2)}0x{info.Id:X8} type={info.Type} "
+ $"FontDid=0x{info.FontDid:X8} rect=({info.X},{info.Y} {info.Width}x{info.Height})");
foreach (ElementInfo child in info.Children)
DumpFontDids(child, depth + 1);
}
private static void DumpCaptions(string tag, UiElement root)
{
Walk(root, el =>