From 2a81e813cb4c2cb4bb34b7fef65dd31346dfc060 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 14 Aug 2026 13:48:29 +0200 Subject: [PATCH] fix #396 crash: map WaitDialog class type 0x19 to UiDialogRoot; dialog-open failure lands on the contracted refusal path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The user's first click on a mapping button killed the client: the live dialog catalog's wait root 0x31 carries retail class type 0x19 (WaitDialog), which DatWidgetFactory left unmapped, so the root built as a plain UiDatElement and RetailWaitDialogView's ctor threw out of UiButton.OnClick into the render loop. The unit test missed it by standing the confirmation fixture (type 0x13, mapped) in for the wait root — the structural-false- negative class again. Pins: DatWidgetFactoryTests theory for both dialog root types, plus an installed-DAT UiDialogRoot/0x3D/0x3E assertion in the env-gated keyboard probe. OpenCaptureInstructions now converts a dialog construction failure into its contracted 0-return (log + capture refused, retail's own OpenMapWarnDialog failure shape) instead of crashing. Co-Authored-By: Claude Fable 5 --- src/AcDream.App/UI/Layout/DatWidgetFactory.cs | 1 + src/AcDream.App/UI/RetailUiRuntime.cs | 18 +++++++++++++++++- .../UI/Layout/DatWidgetFactoryTests.cs | 14 ++++++++++++++ .../KeyboardConfigLiveMountProbeTests.cs | 12 ++++++++++++ 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs index 8d6391c5..01804e44 100644 --- a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs +++ b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs @@ -122,6 +122,7 @@ public static class DatWidgetFactory 11 => BuildScrollbar(info, resolve), // UIElement_Scrollbar (reg :124137) 12 => BuildText(info, resolve, elementFont, stringResolve), // UIElement_Text 0x13 => new UiDialogRoot(), // ConfirmationDialog + 0x19 => new UiDialogRoot(), // WaitDialog (catalog root 0x31 — OP8 #396) 0x10000031u => new UiItemList(resolve), // UIElement_ItemList — toolbar/inventory/paperdoll slots 0x10000035u => BuildCheckbox( info, resolve, elementFont, fontResolve, stringResolve), // UIOption_Checkbox diff --git a/src/AcDream.App/UI/RetailUiRuntime.cs b/src/AcDream.App/UI/RetailUiRuntime.cs index b666940e..8c21f9c0 100644 --- a/src/AcDream.App/UI/RetailUiRuntime.cs +++ b/src/AcDream.App/UI/RetailUiRuntime.cs @@ -2651,7 +2651,23 @@ public sealed class RetailUiRuntime : IDisposable // convention DatWidgetFactory/IndicatorDetailText already // unescape for other DAT-authored strings. text = text.Replace("\\n", "\n", StringComparison.Ordinal); - return DialogFactory.MakeWait(text, queueKey: 0x10000001u); + try + { + return DialogFactory.MakeWait(text, queueKey: 0x10000001u); + } + catch (Exception failure) + { + // Retail's OpenMapWarnDialog returns 0 on failure and + // InitiateBinding then refuses to arm capture — a + // malformed catalog must land on that contracted path, + // not escape UiButton.OnClick into the render loop + // (the 2026-08-14 live crash: wait root type 0x19 was + // unmapped and the view ctor threw). + Console.WriteLine( + "[UI] keyboard config: capture-instruction dialog failed to " + + $"build — capture refused. {failure.Message}"); + return 0u; + } }, CloseCaptureInstructions: context => DialogFactory?.CloseDialog(context)), diff --git a/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs b/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs index bf560ce3..c06ae755 100644 --- a/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs @@ -27,6 +27,20 @@ public class DatWidgetFactoryTests Assert.IsType(e); } + // ── Dialog roots: retail class types 0x13/0x19 → UiDialogRoot ──────────── + + [Theory] + [InlineData(0x13u)] // ConfirmationDialog (catalog root 0x15) + [InlineData(0x19u)] // WaitDialog (catalog root 0x31 — OP8 #396's live + // crash: unmapped type built a plain UiDatElement and + // RetailWaitDialogView's ctor threw out of OnClick) + public void DialogRootTypes_MakeUiDialogRoot(uint type) + { + var e = DatWidgetFactory.Create( + new ElementInfo { Type = type, Width = 800, Height = 600 }, NoTex, null); + Assert.IsType(e); + } + // ── Test 2: Unknown type → UiDatElement fallback ───────────────────────── [Fact] diff --git a/tests/AcDream.App.Tests/UI/Layout/KeyboardConfigLiveMountProbeTests.cs b/tests/AcDream.App.Tests/UI/Layout/KeyboardConfigLiveMountProbeTests.cs index 2f5cf1b8..c5ec5d80 100644 --- a/tests/AcDream.App.Tests/UI/Layout/KeyboardConfigLiveMountProbeTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/KeyboardConfigLiveMountProbeTests.cs @@ -276,7 +276,19 @@ public sealed class KeyboardConfigLiveMountProbeTests if (waitInfo is null) Console.WriteLine("[kbwait] wait root 0x31 -> IMPORT MISSING from 0x2100003C"); else + { DumpFontDids(waitInfo, 0); + // The 2026-08-14 live crash pin: the wait root (retail class + // type 0x19) must build as a UiDialogRoot with the shared + // popup/message pair — the unit suite's fixture only carries + // the confirmation subtree, so the installed-DAT shape is + // asserted here. + ImportedLayout waitBuilt = LayoutImporter.Build( + waitInfo, _ => (0u, 0, 0), null, null, strings.Resolve); + Assert.IsType(waitBuilt.Root); + Assert.NotNull(waitBuilt.FindElement(0x3Du)); + Assert.IsType(waitBuilt.FindElement(0x3Eu)); + } } }