acdream/docs/research/2026-07-13-retail-dialog-factory-pseudocode.md
Erik 66bdae7a83 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>
2026-07-13 16:30:05 +02:00

6.5 KiB

Retail dialog factory and confirmation lifecycle

Date: 2026-07-13

Scope

This note pins the reusable retail dialog architecture needed by acdream's confirmation call sites. It ports the factory/context/queue lifecycle and the type-1 ConfirmationDialog. The other six catalog types are identified here so the factory can accept their presenters later, but their type-specific data contracts are outside this slice.

Named retail sources

  • Dialog::UpdatePendingDialogDisplay @ 0x00476930
  • Dialog::UpdateDialogText @ 0x00476A50
  • Dialog::SetData @ 0x00476BE0
  • DialogFactory::DialogDone @ 0x004773C0
  • DialogFactory::UpdatePendingDialogDisplay_ @ 0x00477590
  • DialogFactory::Reset @ 0x00477950
  • DialogFactory::CreateDialog_ @ 0x00477AD0
  • DialogFactory::OpenNextDialog @ 0x00477D30
  • DialogFactory::MakeDialog @ 0x00477E90
  • DialogFactory::CloseDialog @ 0x00478160
  • DialogFactory::MakeCallbackDialogInCurrentUI @ 0x00478430
  • ConfirmationDialog::SetData @ 0x004764D0
  • ConfirmationDialog::ListenToElementMessage @ 0x00476670
  • ConfirmationDialog::CancelDialog @ 0x00476770
  • ClientCommunicationSystem::DoDie @ 0x00580050
  • ClientCommunicationSystem::DieDialogCallback @ 0x0057BA70
  • ClientUISystem::UsageCallback @ 0x00565B20
  • ClientUISystem::UsageConfirmation_PKAltar @ 0x00566420
  • ClientUISystem::UsageConfirmation_NPKAltar @ 0x00566610
  • ClientUISystem::UsageConfirmation_VolatileRare @ 0x00566800
  • ClientUISystem::Handle_Character__ConfirmationRequest @ 0x005640A0
  • gmGamePlayUI::MakeGameplayConfirmationDialog @ 0x004EB890
  • gmGamePlayUI::RecvNotice_CloseDialog @ 0x004EA4F0
  • gmGamePlayUI::RecvNotice_AbortConfirmationRequest @ 0x004E9D70

Verbatim DialogInfo layout: docs/research/named-retail/acclient.h, struct 5922. Protocol payloads were independently cross-checked against ACE and ACClientLib during the command-family port; see docs/research/2026-07-13-retail-client-command-families-pseudocode.md.

Property contract

0x8D  boolean       priority/preempt-current flag
0x8E  integer       dialog type
0x90  StringInfo    optional accept caption
0x91  StringInfo    optional reject caption
0x92  boolean       confirmation result
0xAC  boolean       when true, Dialog::SetData sets UIElement attribute 0x40
0xC3  integer       queue key; default 2, value 1 is nonqueued
0xC5  StringInfo    main dialog text
0x1000003D instance item/object id carried by item confirmations

Dialog type to catalog-root/type mapping in CreateDialog_:

1 -> root 0x15, element type 0x13, ConfirmationDialog
2 -> root 0x31, element type 0x19, WaitDialog
3 -> root 0x24, element type 0x17, MessageDialog
4 -> root 0x28, element type 0x18, TextInputDialog
5 -> root 0x2C, element type 0x15, ConfirmationTextInputDialog
6 -> root 0x1B, element type 0x16, MenuDialog
7 -> root 0x1F, element type 0x14, ConfirmationMenuDialog

Factory pseudocode

make_dialog(parent, input_data):
    context = ++global_context
    info = DialogInfo(copy(input_data), pointer=null, parent, context)
    priority = input_data.bool(0x8D, false)
    queue_key = input_data.uint(0xC3, 2)

    if queue_key == 1:
        append info to active_nonqueued
        create_dialog(info, queue_key)
        return context

    if no active dialog for queue_key:
        create_dialog(info, queue_key)
    else if not priority:
        pending[queue_key].push_back(info)
        update_pending_displays()
    else:
        current = active[queue_key]
        remove current root from parent without completing it
        pending[queue_key].push_front(current)
        create_dialog(info, queue_key)

    return context

create_dialog(info, queue_key):
    store info as active nonqueued or active[queue_key]
    read type from property 0x8E
    create a FRESH root element from shared catalog enum (2, 5)
    assign context to the Dialog
    dialog.SetData(info.data)
    bring dialog to front
    update_pending_displays()
    broadcast OpenDialog(context)

update_pending_display(dialog, pending_count):
    resolve child 0x33 and its text child 0x34 once
    child_0x33.SetState(pending_count == 0 ? 0x19 : 0x18)
    // retail does not synthesize numeric count text

close_dialog(context):
    if context is active nonqueued:
        dialog_done(info)
        remove it
        return

    if context is an active queued dialog:
        remove active[queue_key]
        dialog_done(info)
        open_next_dialog(queue_key)
        return

    if context is pending:
        dialog_done(info)
        remove it from pending
        update_pending_displays()

dialog_done(info):
    if callback exists for info.context:
        callback(dialog.data)
        remove callback
    broadcast CloseDialog(info.context, dialog.data)
    remove the root element from its parent
    dialog.context = 0

open_next_dialog(queue_key):
    if pending[queue_key] has an entry:
        info = pop_front()
        create_dialog(info, queue_key)
    delete an empty pending queue

Reset runs dialog_done for active and pending dialogs before clearing all factory collections and callbacks.

Confirmation presenter pseudocode

confirmation.SetData(data):
    base.SetData(data)                   // copies collection; 0xC5 updates child 0x3E
    if data has 0x90: set child 0x17 caption
    if data has 0x91: set child 0x19 caption

on button message:
    if child is not 0x17 or 0x19: delegate to base
    data[0x92] = (child == 0x17)
    DialogFactory.CloseDialog(context)

cancel:
    data[0x92] = false
    DialogFactory.CloseDialog(context)

The catalog/root/class are reused; the live widget instance is not. Retail creates a fresh root for every displayed DialogInfo, which is required for simultaneous nonqueued dialogs and independent queue groups.

Integration ownership

/die:
    data = { 0x8E=1, 0xC5=retail message }
    MakeCallbackDialogInCurrentUI(data, DieDialogCallback)
    callback sends suicide only when 0x92 is true

item confirmation:
    data = { 0x8E=1, 0xC5=message, 0x1000003D=item id }
    MakeCallbackDialogInCurrentUI(data, UsageCallback)
    callback sends Use only when 0x92 is true

server gameplay confirmation:
    gameplay UI stores server type/context
    data = { 0x8E=1, 0xC5=message }
    context = MakeDialog(gameplay UI, data)       // no direct callback
    DialogFactory's CloseDialog notice reaches gmGamePlayUI
    gmGamePlayUI reads 0x92 and sends ConfirmationResponse(type, server context, result)
    matching ConfirmationDone closes the context through DialogFactory