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:
parent
43f7c7807c
commit
66bdae7a83
22 changed files with 1442 additions and 220 deletions
190
docs/research/2026-07-13-retail-dialog-factory-pseudocode.md
Normal file
190
docs/research/2026-07-13-retail-dialog-factory-pseudocode.md
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
# 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
|
||||
|
||||
```text
|
||||
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_`:
|
||||
|
||||
```text
|
||||
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
|
||||
|
||||
```text
|
||||
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
|
||||
|
||||
```text
|
||||
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
|
||||
|
||||
```text
|
||||
/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
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue