Implement retail character management screen

This commit is contained in:
Erik 2026-08-14 20:29:19 +02:00
parent 5535d0adac
commit 6cfab727f1
19 changed files with 2435 additions and 6 deletions

View file

@ -16,6 +16,7 @@ using AcDream.Core.Selection;
using AcDream.Core.Spells;
using AcDream.Runtime;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Session;
using AcDream.Content;
using AcDream.Core.Input;
using AcDream.UI.Abstractions;
@ -369,6 +370,21 @@ public sealed record KeyboardRuntimeBindings(
InputDispatcher? Dispatcher,
string KeyBindingsFilePath);
/// <summary>
/// Borrowed LA7b character-selection projection and its generation-capturing
/// typed command routes. App owns no roster, selection, operation, or error
/// mirror; an absent view means the current adapter has not bound (or has
/// already been released).
/// </summary>
public sealed record CharacterSelectionRuntimeBindings(
Func<IRuntimeCharacterSelectionView?> View,
Func<uint, RuntimeCommandResult> Highlight,
Func<RuntimeCommandResult> Enter,
Func<RuntimeCommandResult> RequestDelete,
Func<RuntimeCommandResult> ConfirmDelete,
Func<RuntimeCommandResult> Restore,
Func<RuntimeCommandResult> Cancel);
public sealed record RetailUiRuntimeBindings(
UiHost Host,
RetailUiAssets Assets,
@ -395,7 +411,8 @@ public sealed record RetailUiRuntimeBindings(
BufferedUiRegistry? Plugins,
RetailUiPersistenceBindings? Persistence,
RetailUiProbeBindings Probe,
KeyboardRuntimeBindings? Keyboard = null);
KeyboardRuntimeBindings? Keyboard = null,
CharacterSelectionRuntimeBindings? CharacterSelection = null);
/// <summary>
/// Composition owner for the production retained gameplay UI. GameWindow supplies
@ -483,6 +500,7 @@ public sealed class RetailUiRuntime : IDisposable
MountVendor();
MountSecureTrade();
MountItemCooldowns();
MountCharacterManagement();
Host.WindowManager.WindowVisibilityChanged += OnWindowVisibilityChanged;
BindToolbarPanelButtons();
SyncToolbarWindowButtons();
@ -577,6 +595,7 @@ public sealed class RetailUiRuntime : IDisposable
public VendorUiController? VendorController { get; private set; }
public OptionsPanelController? OptionsPanelController { get; private set; }
public SocialPanelController? SocialPanelController { get; private set; }
internal CharacterManagementUiController? CharacterManagementController { get; private set; }
public static RetailUiRuntime Mount(RetailUiRuntimeBindings bindings)
{
@ -622,6 +641,7 @@ public sealed class RetailUiRuntime : IDisposable
ExternalContainerController?.Tick();
SocialPanelController?.Tick();
_itemCooldownController?.Tick();
CharacterManagementController?.Tick();
DialogFactory?.Tick();
Host.Tick(deltaSeconds);
_automation?.Tick(deltaSeconds);
@ -788,6 +808,7 @@ public sealed class RetailUiRuntime : IDisposable
{
try
{
CharacterManagementController?.ResetSession();
DialogFactory?.Reset();
}
finally
@ -3669,6 +3690,144 @@ public sealed class RetailUiRuntime : IDisposable
"[M4] retail secure trade panel mounted from LayoutDesc 0x2100000D.");
}
private void MountCharacterManagement()
{
CharacterSelectionRuntimeBindings? bindings =
_bindings.CharacterSelection;
if (bindings is null)
return;
if (DialogFactory is null)
{
Console.WriteLine(
"[UI] character management: retail DialogFactory is unavailable.");
return;
}
const uint stringTableId = 0x23000002u;
uint layoutId;
ImportedLayout? layout;
var strings = new DatStringResolver(_bindings.Assets.Dats);
lock (_bindings.Assets.DatLock)
{
// gmCharacterManagementUI's framework call passes enum
// 0x10000005 and category/table 5, then selects root 0x1000039A.
layoutId = RetailDataIdResolver.Resolve(
_bindings.Assets.Dats,
CharacterManagementUiController.RootEnum,
5u);
layout = layoutId == 0u
? null
: LayoutImporter.Import(
_bindings.Assets.Dats,
layoutId,
CharacterManagementUiController.RootElementId,
_bindings.Assets.ResolveSprite,
_bindings.Assets.DefaultFont,
_bindings.Assets.ResolveFont);
}
if (layout is null)
{
Console.WriteLine(
"[UI] character management: enum-table-5 root could not be imported.");
return;
}
string? deleteResponse;
string? deleteConfirmationProbe;
string? pleaseWait;
string? enteringWorld;
lock (_bindings.Assets.DatLock)
{
deleteConfirmationProbe = strings.ResolveTemplate(
stringTableId,
"ID_CharacterManagement_DeleteCharacterConfirmation",
new Dictionary<uint, string>
{
[DatStringResolver.PlayerVariable] = string.Empty,
});
deleteResponse = ResolveCharacterManagementString(
strings,
stringTableId,
"ID_CharacterManagement_DeleteCharacterResponse");
pleaseWait = ResolveCharacterManagementString(
strings,
stringTableId,
"ID_CharacterManagement_PleaseWait");
enteringWorld = ResolveCharacterManagementString(
strings,
stringTableId,
"ID_Character_EnteringWorld");
}
if (deleteConfirmationProbe is null
|| deleteResponse is null
|| pleaseWait is null
|| enteringWorld is null)
{
Console.WriteLine(
"[UI] character management: required retail strings are unavailable.");
return;
}
UiElement? ResolveTemplate(uint templateLayoutId, uint templateElementId)
{
lock (_bindings.Assets.DatLock)
{
return LayoutImporter.Import(
_bindings.Assets.Dats,
templateLayoutId,
templateElementId,
_bindings.Assets.ResolveSprite,
_bindings.Assets.DefaultFont,
_bindings.Assets.ResolveFont)?.Root;
}
}
string ComposeDeleteConfirmation(string characterName)
{
lock (_bindings.Assets.DatLock)
{
return NormalizeRetailNewlines(strings.ResolveTemplate(
stringTableId,
"ID_CharacterManagement_DeleteCharacterConfirmation",
new Dictionary<uint, string>
{
[DatStringResolver.PlayerVariable] = characterName,
})!);
}
}
CharacterManagementController = CharacterManagementUiController.Bind(
Host.Root,
layout,
ResolveTemplate,
DialogFactory,
bindings,
new CharacterManagementUiController.DialogStrings(
ComposeDeleteConfirmation,
deleteResponse,
pleaseWait,
enteringWorld));
if (CharacterManagementController is null)
return;
Console.WriteLine(
$"[UI] retail character management from enum table 5 "
+ $"(0x10000005 -> 0x{layoutId:X8}, root 0x1000039A; flat list, no viewport).");
}
private static string? ResolveCharacterManagementString(
DatStringResolver strings,
uint tableId,
string key) =>
strings.Resolve(tableId, DatStringResolver.ComputeHash(key)) is { } value
? NormalizeRetailNewlines(value)
: null;
private static string NormalizeRetailNewlines(string value) =>
value.Replace("\\n", "\n", StringComparison.Ordinal);
private void MountItemCooldowns()
{
ItemCooldownAssets? assets;
@ -3712,7 +3871,11 @@ public sealed class RetailUiRuntime : IDisposable
}
},
() => _itemConfirmationController?.Dispose(),
() => _gameplayConfirmationController?.Dispose(),
() =>
{
CharacterManagementController?.Dispose();
_gameplayConfirmationController?.Dispose();
},
() => DialogFactory?.Dispose(),
_panelUi.Dispose,
Host.Dispose);