feat(ui): port retail dialog factory lifecycle

Replace the single mutable confirmation service with retail's property-backed DialogFactory model: fresh DAT roots, context ids, queue groups, priority preemption, callback/close-notice ordering, and context cancellation. Route /die, server confirmation aborts, and guarded item use through focused semantic owners.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-13 16:30:05 +02:00
parent 43f7c7807c
commit 66bdae7a83
22 changed files with 1442 additions and 220 deletions

View file

@ -107,6 +107,9 @@ public sealed record RetailUiCursorBindings(
CursorFeedbackController Feedback,
RetailCursorManager Manager);
public sealed record ConfirmationRuntimeBindings(
Action<uint, uint, bool> SendResponse);
public sealed record RetailUiRuntimeBindings(
UiHost Host,
RetailUiAssets Assets,
@ -120,6 +123,7 @@ public sealed record RetailUiRuntimeBindings(
CharacterRuntimeBindings Character,
InventoryRuntimeBindings Inventory,
RetailUiCursorBindings Cursor,
ConfirmationRuntimeBindings Confirmations,
StackSplitQuantityState StackSplitQuantity,
BufferedUiRegistry? Plugins,
RetailUiPersistenceBindings? Persistence,
@ -136,6 +140,8 @@ public sealed class RetailUiRuntime : IDisposable
private StackSplitQuantityState StackSplitQuantity => _bindings.StackSplitQuantity;
private readonly RetailWindowLayoutPersistence? _persistence;
private readonly RetailUiAutomationScriptRunner? _automation;
private GameplayConfirmationController? _gameplayConfirmationController;
private RetailItemConfirmationController? _itemConfirmationController;
private bool _disposed;
private RetailUiRuntime(RetailUiRuntimeBindings bindings)
@ -148,7 +154,7 @@ public sealed class RetailUiRuntime : IDisposable
MountToolbar();
MountCombat();
MountJumpPowerbar();
MountConfirmationDialogs();
MountDialogFactory();
MountCharacter();
MountPlugins();
MountInventory();
@ -191,7 +197,7 @@ public sealed class RetailUiRuntime : IDisposable
public SelectedObjectController? SelectedObjectController { get; private set; }
public UiViewport? PaperdollViewportWidget { get; private set; }
public UiNineSlicePanel? InventoryFrame { get; private set; }
public RetailConfirmationDialogService? ConfirmationDialogs { get; private set; }
public RetailDialogFactory? DialogFactory { get; private set; }
public static RetailUiRuntime Mount(RetailUiRuntimeBindings bindings)
{
@ -212,7 +218,7 @@ public sealed class RetailUiRuntime : IDisposable
FpsController?.Tick();
JumpPowerbarController?.Tick();
SelectedObjectController?.Tick(deltaSeconds);
ConfirmationDialogs?.Tick();
DialogFactory?.Tick();
Host.Tick(deltaSeconds);
_automation?.Tick(deltaSeconds);
}
@ -222,6 +228,21 @@ public sealed class RetailUiRuntime : IDisposable
public bool HandleInputAction(AcDream.UI.Abstractions.Input.InputAction action)
=> ToolbarInputController?.Handle(action) == true;
public bool HandleConfirmationRequest(GameEvents.CharacterConfirmationRequest request)
=> _gameplayConfirmationController?.HandleRequest(request) == true;
public bool HandleConfirmationDone(GameEvents.CharacterConfirmationDone done)
=> _gameplayConfirmationController?.HandleDone(done) == true;
public uint ShowConfirmation(string message, Action<bool> completed)
{
ArgumentNullException.ThrowIfNull(message);
ArgumentNullException.ThrowIfNull(completed);
return DialogFactory?.MakeConfirmation(
message,
data => completed(data.GetBoolean(RetailDialogProperty.ConfirmationResult))) ?? 0u;
}
public void UpdateCursor(IEnumerable<IMouse> mice)
{
CursorFeedback feedback = _bindings.Cursor.Feedback.Update(Host.Root);
@ -645,34 +666,49 @@ public sealed class RetailUiRuntime : IDisposable
Console.WriteLine("[D.6] retail jump bar from gmFloatyPowerBarUI LayoutDesc 0x21000072.");
}
private void MountConfirmationDialogs()
private void MountDialogFactory()
{
ImportedLayout? layout;
uint layoutId;
lock (_bindings.Assets.DatLock)
{
// DialogFactory::CreateDialog_ @ 0x00477AD0 calls
// GetDIDByEnum(2, 5), then creates root element 0x15.
// DialogFactory::CreateDialog_ @ 0x00477AD0 resolves the shared
// catalog through GetDIDByEnum(2, 5). Each shown DialogInfo then
// creates a fresh type-specific root from that catalog.
layoutId = RetailDataIdResolver.Resolve(_bindings.Assets.Dats, 2u, 5u);
layout = layoutId == 0u
? null
: LayoutImporter.Import(
_bindings.Assets.Dats,
layoutId,
RetailConfirmationDialogService.RootElementId,
_bindings.Assets.ResolveSprite,
_bindings.Assets.DefaultFont,
_bindings.Assets.ResolveFont);
}
if (layout is null)
if (layoutId == 0u)
{
Console.WriteLine("[UI] confirmation dialog catalog could not be resolved.");
Console.WriteLine("[UI] retail dialog catalog could not be resolved.");
return;
}
ConfirmationDialogs = new RetailConfirmationDialogService(Host.Root, layout);
Console.WriteLine($"[UI] retail confirmation dialogs from LayoutDesc 0x{layoutId:X8} element 0x15.");
ImportedLayout? CreateLayout(RetailDialogType type)
{
uint rootElementId = RetailDialogFactory.RootElementId(type);
if (rootElementId == 0u)
return null;
lock (_bindings.Assets.DatLock)
{
return LayoutImporter.Import(
_bindings.Assets.Dats,
layoutId,
rootElementId,
_bindings.Assets.ResolveSprite,
_bindings.Assets.DefaultFont,
_bindings.Assets.ResolveFont);
}
}
DialogFactory = new RetailDialogFactory(Host.Root, CreateLayout);
_gameplayConfirmationController = new GameplayConfirmationController(
DialogFactory,
_bindings.Confirmations.SendResponse);
_itemConfirmationController = new RetailItemConfirmationController(
DialogFactory,
ItemInteraction);
Console.WriteLine(
$"[UI] retail DialogFactory from LayoutDesc 0x{layoutId:X8}; confirmation root 0x15.");
}
private (uint[] Regular, uint[] Ghosted, uint[]? Empty) LoadToolbarDigits()
@ -889,6 +925,9 @@ public sealed class RetailUiRuntime : IDisposable
_disposed = true;
_persistence?.Dispose();
Host.WindowManager.WindowVisibilityChanged -= OnWindowVisibilityChanged;
_itemConfirmationController?.Dispose();
_gameplayConfirmationController?.Dispose();
DialogFactory?.Dispose();
Host.Dispose();
}
}