feat: port retail magic lifecycle and retained spell UI
Complete the retail cast-intent, target, component, enchantment, and busy-state paths; mount the DAT-authored spell bar, spellbook, component book, effects panels, and shared panel lifecycle; and add scoped input plus conformance coverage. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
parent
7b7ffcd278
commit
07be994d97
84 changed files with 17822 additions and 1051 deletions
144
src/AcDream.App/UI/Layout/RetailPanelUiController.cs
Normal file
144
src/AcDream.App/UI/Layout/RetailPanelUiController.cs
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Retained-window port of retail <c>gmPanelUI::RecvNotice_SetPanelVisibility</c>
|
||||
/// at <c>0x004BC6F0</c>. The original owns one active child panel and an optional
|
||||
/// deferred child; this owner applies the same lifecycle to registered retained
|
||||
/// windows without making the toolbar authoritative for non-toolbar panels.
|
||||
/// </summary>
|
||||
public sealed class RetailPanelUiController
|
||||
{
|
||||
public const uint RestorePreviousPropertyId = 0x10000049u;
|
||||
|
||||
private readonly Func<string, bool> _isVisible;
|
||||
private readonly Func<string, bool> _show;
|
||||
private readonly Func<string, bool> _hide;
|
||||
private readonly Dictionary<uint, PanelEntry> _byPanel = new();
|
||||
private readonly Dictionary<string, uint> _byWindow = new(StringComparer.Ordinal);
|
||||
private uint? _activePanel;
|
||||
private uint? _deferredPanel;
|
||||
private bool _applying;
|
||||
|
||||
public RetailPanelUiController(
|
||||
Func<string, bool> isVisible,
|
||||
Func<string, bool> show,
|
||||
Func<string, bool> hide)
|
||||
{
|
||||
_isVisible = isVisible ?? throw new ArgumentNullException(nameof(isVisible));
|
||||
_show = show ?? throw new ArgumentNullException(nameof(show));
|
||||
_hide = hide ?? throw new ArgumentNullException(nameof(hide));
|
||||
}
|
||||
|
||||
public uint? ActivePanelId => _activePanel;
|
||||
|
||||
/// <param name="restorePrevious">
|
||||
/// Effective DAT bool property <c>0x10000049</c>. Retail retains the previous
|
||||
/// panel only when the newly shown panel has this property and the previous
|
||||
/// panel does not.
|
||||
/// </param>
|
||||
public void Register(uint panelId, string windowName, bool restorePrevious = false)
|
||||
{
|
||||
if (panelId == 0) throw new ArgumentOutOfRangeException(nameof(panelId));
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(windowName);
|
||||
if (_byPanel.ContainsKey(panelId) || _byWindow.ContainsKey(windowName))
|
||||
throw new InvalidOperationException(
|
||||
$"Panel {panelId} or retained window '{windowName}' is already registered.");
|
||||
|
||||
_byPanel.Add(panelId, new PanelEntry(windowName, restorePrevious));
|
||||
_byWindow.Add(windowName, panelId);
|
||||
if (_isVisible(windowName))
|
||||
ObserveWindowVisibility(windowName, visible: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a panel directly from its fully inherited retail root so the
|
||||
/// authored deferred-panel behavior cannot be dropped at the mount seam.
|
||||
/// </summary>
|
||||
public void Register(uint panelId, string windowName, ElementInfo authoredRoot)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(authoredRoot);
|
||||
Register(
|
||||
panelId,
|
||||
windowName,
|
||||
authoredRoot.TryGetEffectiveBool(
|
||||
RestorePreviousPropertyId,
|
||||
out bool restorePrevious)
|
||||
&& restorePrevious);
|
||||
}
|
||||
|
||||
public bool IsPanelVisible(uint panelId)
|
||||
=> _byPanel.TryGetValue(panelId, out PanelEntry entry)
|
||||
&& _isVisible(entry.WindowName);
|
||||
|
||||
public bool TogglePanel(uint panelId)
|
||||
{
|
||||
bool visible = !IsPanelVisible(panelId);
|
||||
return SetPanelVisibility(panelId, visible) && visible;
|
||||
}
|
||||
|
||||
public bool SetPanelVisibility(uint panelId, bool visible)
|
||||
{
|
||||
if (!_byPanel.TryGetValue(panelId, out PanelEntry requested)) return false;
|
||||
|
||||
_applying = true;
|
||||
try
|
||||
{
|
||||
if (visible)
|
||||
{
|
||||
if (_activePanel == panelId)
|
||||
return _show(requested.WindowName);
|
||||
|
||||
uint? previousId = _activePanel;
|
||||
PanelEntry? previous = previousId is uint id
|
||||
&& _byPanel.TryGetValue(id, out PanelEntry found)
|
||||
&& _isVisible(found.WindowName)
|
||||
? found
|
||||
: null;
|
||||
|
||||
_deferredPanel = requested.RestorePrevious
|
||||
&& previous is { RestorePrevious: false }
|
||||
? previousId
|
||||
: null;
|
||||
_activePanel = panelId;
|
||||
if (previous is not null)
|
||||
_hide(previous.Value.WindowName);
|
||||
return _show(requested.WindowName);
|
||||
}
|
||||
|
||||
if (_activePanel != panelId)
|
||||
{
|
||||
if (_deferredPanel == panelId) _deferredPanel = null;
|
||||
return _hide(requested.WindowName);
|
||||
}
|
||||
|
||||
bool hidden = _hide(requested.WindowName);
|
||||
_activePanel = null;
|
||||
if (_deferredPanel is not uint deferredId
|
||||
|| !_byPanel.TryGetValue(deferredId, out PanelEntry deferred))
|
||||
return hidden;
|
||||
|
||||
_deferredPanel = null;
|
||||
_activePanel = deferredId;
|
||||
return _show(deferred.WindowName) || hidden;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_applying = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Feeds visibility changes made by persistence or another retained-window
|
||||
/// owner back through the canonical panel lifecycle.
|
||||
/// </summary>
|
||||
public void ObserveWindowVisibility(string windowName, bool visible)
|
||||
{
|
||||
if (_applying || !_byWindow.TryGetValue(windowName, out uint panelId)) return;
|
||||
SetPanelVisibility(panelId, visible);
|
||||
}
|
||||
|
||||
private readonly record struct PanelEntry(string WindowName, bool RestorePrevious);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue