fix(ui): resolve retail spell slot background
Keep shared shortcut digit overlays separate from the per-ItemList background. Resolve the magic favorite list through its cross-layout inherited cell prototype and use the pinned brown/gold ItemSlot_Empty surface instead of the blue toolbar slot. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
e3605672bb
commit
09612f9981
9 changed files with 128 additions and 53 deletions
|
|
@ -21,8 +21,8 @@ public static class ItemListCellTemplate
|
|||
public const uint CatalogLayoutId = 0x21000037u;
|
||||
|
||||
private const uint CellTemplateAttr = 0x1000000eu; // UIElement attribute: cell-template element id
|
||||
private const uint IconChildId = 0x1000033Bu; // m_elem_Icon sub-element (carries ItemSlot_Empty)
|
||||
private const string ItemSlotEmpty = "ItemSlot_Empty"; // UIStateId.ToString() for state 0x1000001c
|
||||
private const uint IconChildId = 0x1000033Bu; // m_elem_Icon sub-element (carries ItemSlot_Empty)
|
||||
private const string ItemSlotEmpty = "ItemSlot_Empty"; // UIStateId.ToString() for state 0x1000001c
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the empty-slot sprite (a 0x06xxxxxx RenderSurface id) for the cells of item-list
|
||||
|
|
@ -43,6 +43,29 @@ public static class ItemListCellTemplate
|
|||
return ResolvePrototypeEmptySprite(dats, protoId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve from an already inherited <see cref="ElementInfo"/> tree. Use this
|
||||
/// overload when the ItemList is contributed by a cross-layout BaseElement:
|
||||
/// the raw outer LayoutDesc does not physically contain that descendant, but
|
||||
/// <see cref="LayoutImporter.ImportInfos(DatCollection,uint)"/> has applied the
|
||||
/// same inheritance that retail uses before <c>InternalCreateItem</c> queries
|
||||
/// attribute <c>0x1000000E</c>.
|
||||
/// </summary>
|
||||
public static uint ResolveEmptySprite(
|
||||
DatCollection dats,
|
||||
ElementInfo resolvedRoot,
|
||||
uint listElementId)
|
||||
{
|
||||
ElementInfo? list = FindInfo(resolvedRoot, listElementId);
|
||||
if (list is null
|
||||
|| !list.TryGetEffectiveProperty(CellTemplateAttr, out UiPropertyValue property)
|
||||
|| property.Kind is not (UiPropertyKind.Enum or UiPropertyKind.DataId)
|
||||
|| property.UnsignedValue is 0 or > uint.MaxValue)
|
||||
return 0;
|
||||
|
||||
return ResolvePrototypeEmptySprite(dats, (uint)property.UnsignedValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves <c>ItemSlot_Empty</c> directly from an authored UIItem prototype in
|
||||
/// <see cref="CatalogLayoutId"/>. Paperdoll lists select distinct prototypes for their
|
||||
|
|
@ -168,4 +191,15 @@ public static class ItemListCellTemplate
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ElementInfo? FindInfo(ElementInfo element, uint id)
|
||||
{
|
||||
if (element.Id == id) return element;
|
||||
foreach (ElementInfo child in element.Children)
|
||||
{
|
||||
ElementInfo? found = FindInfo(child, id);
|
||||
if (found is not null) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
private readonly Action<uint> _useItem;
|
||||
private readonly Action<int, int, uint>? _addFavorite;
|
||||
private readonly Action<int, uint>? _removeFavorite;
|
||||
private readonly UiShortcutSlotGraphics? _shortcutGraphics;
|
||||
private readonly UiShortcutDigitGraphics? _shortcutDigits;
|
||||
private readonly uint _emptySlotSprite;
|
||||
private readonly UiElement[] _tabs;
|
||||
private readonly UiElement[] _groups;
|
||||
private readonly UiItemList?[] _lists;
|
||||
|
|
@ -79,7 +80,8 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
UiItemList?[] lists,
|
||||
UiButton cast,
|
||||
UiElement endowmentHost,
|
||||
UiShortcutSlotGraphics? shortcutGraphics)
|
||||
UiShortcutDigitGraphics? shortcutDigits,
|
||||
uint emptySlotSprite)
|
||||
{
|
||||
_spellbook = spellbook;
|
||||
_casting = casting;
|
||||
|
|
@ -91,7 +93,8 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
_useItem = useItem;
|
||||
_addFavorite = addFavorite;
|
||||
_removeFavorite = removeFavorite;
|
||||
_shortcutGraphics = shortcutGraphics;
|
||||
_shortcutDigits = shortcutDigits;
|
||||
_emptySlotSprite = emptySlotSprite;
|
||||
_tabs = tabs;
|
||||
_groups = groups;
|
||||
_lists = lists;
|
||||
|
|
@ -143,7 +146,8 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
SelectionState selection,
|
||||
Action<int, int, uint>? addFavorite,
|
||||
Action<int, uint>? removeFavorite,
|
||||
UiShortcutSlotGraphics? shortcutGraphics = null)
|
||||
UiShortcutDigitGraphics? shortcutDigits = null,
|
||||
uint emptySlotSprite = 0u)
|
||||
{
|
||||
if (layout.FindElement(CastButtonId) is not UiButton cast
|
||||
|| layout.FindElement(EndowmentId) is not { } endowmentHost)
|
||||
|
|
@ -166,7 +170,8 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
layout, spellbook, casting, objects, playerGuid, resolveSpellIcon,
|
||||
resolveItemDragIcon, useItem, selection,
|
||||
addFavorite, removeFavorite,
|
||||
tabs, groups, lists, cast, endowmentHost, shortcutGraphics);
|
||||
tabs, groups, lists, cast, endowmentHost,
|
||||
shortcutDigits, emptySlotSprite);
|
||||
}
|
||||
|
||||
public void AddFavorite(uint spellId)
|
||||
|
|
@ -291,6 +296,7 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
list.SingleRow = true;
|
||||
list.CellWidth = 32f;
|
||||
list.CellHeight = 32f;
|
||||
list.CellEmptySprite = _emptySlotSprite;
|
||||
list.EmptySlotFactory = () => CreateEmptyFavoriteSlot(list, targetTab);
|
||||
list.FillVisibleEmptySlots = true;
|
||||
foreach (uint spellId in favorites)
|
||||
|
|
@ -332,9 +338,6 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
slot = new UiCatalogSlot
|
||||
{
|
||||
SpriteResolve = list.SpriteResolve,
|
||||
EmptySprite = _shortcutGraphics is { EmptySprite: > 0u } graphics
|
||||
? graphics.EmptySprite
|
||||
: 0x060074CFu,
|
||||
Dropped = payload =>
|
||||
{
|
||||
int position = Math.Max(0, list.IndexOf(slot!));
|
||||
|
|
@ -362,11 +365,9 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
|
||||
private void ConfigureShortcutOverlay(UiItemSlot slot, int index)
|
||||
{
|
||||
slot.RegularDigits = _shortcutGraphics?.RegularDigits;
|
||||
slot.GhostedDigits = _shortcutGraphics?.GhostedDigits;
|
||||
slot.EmptyDigits = _shortcutGraphics?.EmptyDigits;
|
||||
if (_shortcutGraphics is { EmptySprite: > 0u } graphics)
|
||||
slot.EmptySprite = graphics.EmptySprite;
|
||||
slot.RegularDigits = _shortcutDigits?.RegularDigits;
|
||||
slot.GhostedDigits = _shortcutDigits?.GhostedDigits;
|
||||
slot.EmptyDigits = _shortcutDigits?.EmptyDigits;
|
||||
if (index < 9)
|
||||
slot.SetShortcutNum(index, ghosted: false);
|
||||
else
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
private GameplayConfirmationController? _gameplayConfirmationController;
|
||||
private RetailItemConfirmationController? _itemConfirmationController;
|
||||
private RetailSkillTrainingConfirmationController? _skillTrainingConfirmationController;
|
||||
private UiShortcutSlotGraphics? _shortcutSlotGraphics;
|
||||
private UiShortcutDigitGraphics? _shortcutDigitGraphics;
|
||||
private bool _disposed;
|
||||
|
||||
private RetailUiRuntime(RetailUiRuntimeBindings bindings)
|
||||
|
|
@ -559,12 +559,12 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
UiShortcutSlotGraphics shortcutGraphics = LoadShortcutSlotGraphics();
|
||||
UiShortcutDigitGraphics shortcutDigits = LoadShortcutDigitGraphics();
|
||||
ToolbarRuntimeBindings b = _bindings.Toolbar;
|
||||
ToolbarController = Layout.ToolbarController.Bind(
|
||||
layout, b.Objects, b.Shortcuts, b.ResolveIcon, b.UseItem, b.Combat,
|
||||
shortcutGraphics.RegularDigits, shortcutGraphics.GhostedDigits,
|
||||
shortcutGraphics.EmptyDigits, b.ItemInteraction,
|
||||
shortcutDigits.RegularDigits, shortcutDigits.GhostedDigits,
|
||||
shortcutDigits.EmptyDigits, b.ItemInteraction,
|
||||
b.SendAddShortcut, b.SendRemoveShortcut,
|
||||
toggleCombat: b.ToggleCombat,
|
||||
selectItem: guid => b.Selection.Select(guid, SelectionChangeSource.Toolbar),
|
||||
|
|
@ -625,9 +625,16 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
ElementInfo? info;
|
||||
ImportedLayout? layout;
|
||||
CombatUiLabels? labels;
|
||||
uint favoriteEmptySprite;
|
||||
lock (_bindings.Assets.DatLock)
|
||||
{
|
||||
info = LayoutImporter.ImportInfos(_bindings.Assets.Dats, CombatUiController.LayoutId);
|
||||
favoriteEmptySprite = info is null
|
||||
? 0u
|
||||
: ItemListCellTemplate.ResolveEmptySprite(
|
||||
_bindings.Assets.Dats,
|
||||
info,
|
||||
SpellcastingUiController.FavoriteListId);
|
||||
var strings = new DatStringResolver(_bindings.Assets.Dats);
|
||||
layout = info is null ? null : LayoutImporter.Build(
|
||||
info,
|
||||
|
|
@ -675,7 +682,8 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
_bindings.Magic.Selection,
|
||||
_bindings.Magic.AddFavorite,
|
||||
_bindings.Magic.RemoveFavorite,
|
||||
LoadShortcutSlotGraphics());
|
||||
LoadShortcutDigitGraphics(),
|
||||
favoriteEmptySprite);
|
||||
if (spellcasting is null)
|
||||
Console.WriteLine("[M3] spellcasting: required controls missing in LayoutDesc 0x21000073.");
|
||||
|
||||
|
|
@ -706,7 +714,8 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
controller.SyncVisibility();
|
||||
Console.WriteLine(spellcasting is null
|
||||
? "[M2] retail combat from LayoutDesc 0x21000073; magic binding unavailable."
|
||||
: "[M3] retail combat + spell bar from LayoutDesc 0x21000073.");
|
||||
: $"[M3] retail combat + spell bar from LayoutDesc 0x21000073; " +
|
||||
$"favorite empty=0x{favoriteEmptySprite:X8}.");
|
||||
}
|
||||
|
||||
private void MountSpellbook()
|
||||
|
|
@ -1016,13 +1025,12 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
$"[UI] retail DialogFactory from LayoutDesc 0x{layoutId:X8}; confirmation root 0x15.");
|
||||
}
|
||||
|
||||
private UiShortcutSlotGraphics LoadShortcutSlotGraphics()
|
||||
private UiShortcutDigitGraphics LoadShortcutDigitGraphics()
|
||||
{
|
||||
if (_shortcutSlotGraphics is not null)
|
||||
return _shortcutSlotGraphics;
|
||||
if (_shortcutDigitGraphics is not null)
|
||||
return _shortcutDigitGraphics;
|
||||
|
||||
uint[]? regular = null, ghosted = null, empty = null;
|
||||
uint emptySprite = 0u;
|
||||
lock (_bindings.Assets.DatLock)
|
||||
{
|
||||
var layout = _bindings.Assets.Dats.Get<DatReaderWriter.DBObjs.LayoutDesc>(0x21000037u);
|
||||
|
|
@ -1039,10 +1047,6 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
&& emptyComposite.Children.TryGetValue(0x1000034Au, out var emptyNumber)
|
||||
&& emptyNumber.StateDesc?.Properties is { } emptyProps)
|
||||
empty = ReadDataIds(emptyProps, 0x1000005Eu);
|
||||
emptySprite = ItemListCellTemplate.ResolveEmptySprite(
|
||||
_bindings.Assets.Dats,
|
||||
CombatUiController.LayoutId,
|
||||
SpellcastingUiController.FavoriteListId);
|
||||
}
|
||||
|
||||
regular ??=
|
||||
|
|
@ -1055,13 +1059,12 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
0x06001ACCu, 0x06001ACDu, 0x06001ACEu, 0x06001ACFu, 0x06001AD0u,
|
||||
0x06001AD1u, 0x06001AD2u, 0x06001AD3u, 0x06001AD4u,
|
||||
];
|
||||
_shortcutSlotGraphics = new UiShortcutSlotGraphics(
|
||||
regular, ghosted, empty, emptySprite);
|
||||
_shortcutDigitGraphics = new UiShortcutDigitGraphics(
|
||||
regular, ghosted, empty);
|
||||
Console.WriteLine(
|
||||
$"[D.5.1] shared shortcut graphics ready: regular={regular.Length}, " +
|
||||
$"ghosted={ghosted.Length}, empty={empty?.Length ?? 0}, " +
|
||||
$"slot=0x{emptySprite:X8}.");
|
||||
return _shortcutSlotGraphics;
|
||||
$"[D.5.1] shared shortcut digits ready: regular={regular.Length}, " +
|
||||
$"ghosted={ghosted.Length}, empty={empty?.Length ?? 0}.");
|
||||
return _shortcutDigitGraphics;
|
||||
}
|
||||
|
||||
private static uint[]? ReadDataIds(
|
||||
|
|
|
|||
12
src/AcDream.App/UI/UiShortcutDigitGraphics.cs
Normal file
12
src/AcDream.App/UI/UiShortcutDigitGraphics.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Shared DAT digit overlays used by UIItem shortcut cells. Both gmToolbarUI and
|
||||
/// gmSpellcastingUI call UIElement_UIItem::SetShortcutNum @ 0x004E1590, so they
|
||||
/// share these number arrays. The underlying ItemSlot_Empty surface remains a
|
||||
/// per-ItemList asset selected by that list's cell prototype.
|
||||
/// </summary>
|
||||
public sealed record UiShortcutDigitGraphics(
|
||||
uint[]? RegularDigits,
|
||||
uint[]? GhostedDigits,
|
||||
uint[]? EmptyDigits);
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Shared DAT presentation assets for UIItem shortcut cells. Both gmToolbarUI and
|
||||
/// gmSpellcastingUI use UIElement_UIItem::SetShortcutNum @ 0x004E1590, so their
|
||||
/// digit arrays and empty-cell prototype must come from one resolved asset set.
|
||||
/// </summary>
|
||||
public sealed record UiShortcutSlotGraphics(
|
||||
uint[]? RegularDigits,
|
||||
uint[]? GhostedDigits,
|
||||
uint[]? EmptyDigits,
|
||||
uint EmptySprite);
|
||||
Loading…
Add table
Add a link
Reference in a new issue