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
|
|
@ -532,6 +532,7 @@ behavior. Estimated 17–26 days focused work, 3–5 weeks calendar.
|
||||||
- **✓ M2 local attack receive funnel (live-gated 2026-07-15).** Retail `ExecuteAttack` only sends the request; ACE chooses the concrete melee/missile action and returns it in a non-autonomous mt-0 `UpdateMotion`. The local branch now runs that state through the same constructor-defaulted `MoveToInterpretedState` funnel and 15-bit action-stamp gate as remotes, then applies sticky/long-jump tails. The old local-only direct `Commands[]` replay is deleted. Shared conversion lives in `InboundInterpretedMotionFactory`; research: `docs/research/2026-07-11-local-combat-motion-pseudocode.md`.
|
- **✓ M2 local attack receive funnel (live-gated 2026-07-15).** Retail `ExecuteAttack` only sends the request; ACE chooses the concrete melee/missile action and returns it in a non-autonomous mt-0 `UpdateMotion`. The local branch now runs that state through the same constructor-defaulted `MoveToInterpretedState` funnel and 15-bit action-stamp gate as remotes, then applies sticky/long-jump tails. The old local-only direct `Commands[]` replay is deleted. Shared conversion lives in `InboundInterpretedMotionFactory`; research: `docs/research/2026-07-11-local-combat-motion-pseudocode.md`.
|
||||||
- **✓ M2 basic retail combat bar (live-gated 2026-07-15).** Production mounts authored `gmCombatUI` LayoutDesc `0x21000073`, shows it only for Melee/Missile, and routes mouse plus keyboard through one `CombatAttackController`. Corrections include the authored wider centered dark-red child range (accepted IA-20), full-width left-to-right bright live attack-charge feedback, exact Speed-left/Power-right justification, silent control-only `AttackDone(ActionCancelled)`, target-frame Keep in View with manual orbit, and persistent corpse motion: the AP-80 velocity-only NPC adaptation can now replace only Ready/Walk/Run, never authoritative Dead/actions. `CombatTargetController` ports the selection-cleared AutoTarget consumer, so a selected creature's authoritative Dead motion clears it and selects the nearest eligible creature when enabled. The 2026-07-12 replacement-corpse correction unifies both multi-frame and static/reactive spawns behind retail's CreateObject lifecycle: apply the wire's Dead state while detached, then `MotionTableManager::HandleEnterWorld` strips Ready→Dead links before the first in-world tick; multiple corpses remaining fallen passed the live user gate that day. Follow-up #205 makes the toolbar read the final canonical selection after a reentrant Auto Target notice and restricts automatic candidates to hostile non-player monsters (intentional PK-edge divergence IA-19); that live gate also passed 2026-07-12. Research: `docs/research/2026-07-11-retail-combat-bar-pseudocode.md`, `docs/research/2026-07-11-combat-target-camera-pseudocode.md`, `docs/research/2026-07-12-death-and-auto-target-pseudocode.md`; AP-24/AP-95 retired, AP-80 narrowed, AP-110 narrowed, AP-112 records the remaining attack-start and exact trained-Recklessness seams.
|
- **✓ M2 basic retail combat bar (live-gated 2026-07-15).** Production mounts authored `gmCombatUI` LayoutDesc `0x21000073`, shows it only for Melee/Missile, and routes mouse plus keyboard through one `CombatAttackController`. Corrections include the authored wider centered dark-red child range (accepted IA-20), full-width left-to-right bright live attack-charge feedback, exact Speed-left/Power-right justification, silent control-only `AttackDone(ActionCancelled)`, target-frame Keep in View with manual orbit, and persistent corpse motion: the AP-80 velocity-only NPC adaptation can now replace only Ready/Walk/Run, never authoritative Dead/actions. `CombatTargetController` ports the selection-cleared AutoTarget consumer, so a selected creature's authoritative Dead motion clears it and selects the nearest eligible creature when enabled. The 2026-07-12 replacement-corpse correction unifies both multi-frame and static/reactive spawns behind retail's CreateObject lifecycle: apply the wire's Dead state while detached, then `MotionTableManager::HandleEnterWorld` strips Ready→Dead links before the first in-world tick; multiple corpses remaining fallen passed the live user gate that day. Follow-up #205 makes the toolbar read the final canonical selection after a reentrant Auto Target notice and restricts automatic candidates to hostile non-player monsters (intentional PK-edge divergence IA-19); that live gate also passed 2026-07-12. Research: `docs/research/2026-07-11-retail-combat-bar-pseudocode.md`, `docs/research/2026-07-11-combat-target-camera-pseudocode.md`, `docs/research/2026-07-12-death-and-auto-target-pseudocode.md`; AP-24/AP-95 retired, AP-80 narrowed, AP-110 narrowed, AP-112 records the remaining attack-start and exact trained-Recklessness seams.
|
||||||
- **M3 cast lifecycle + retained magic UI implemented 2026-07-15; visual gates pending.** `SpellCastingController` ports retail's immediate targeted/untargeted request boundary, component preflight, target eligibility, movement stop, and shared UI-busy lifetime while leaving turn/cast/fizzle/impact outcomes authoritative to ACE. Formula preflight reads decrypted portal.dat formulas, the SCID↔WCID map, canonical account randomization, exact school-focus map, infused-magic properties, and scarab-only substitution. Complete live enchantment packets preserve spell/layer identity, all StatMod fields, client-clock normalization, and exact purge buckets. Authored retained surfaces now include the Magic page of `gmCombatUI`, all eight favorite tabs, equipped caster endowment, `gmSpellbookUI`, `gmSpellComponentUI`, both `gmEffectsUI` windows, and the floating Helpful/Harmful effect indicators. The favorite bar now ports retail horizontal empty-cell padding, shared UIItem 1–9 shortcut overlays, and the Cast button's three independently reflowed DAT face segments. Spell rows resolve UIItem prototype `0x10000343`; the component page now instantiates LayoutDesc `0x21000033` category/component templates `0x10000466/67`, including localized headings, exact row textures and columns, owned/desired counts, selection, and the 0..5000 desired-value contract. The authored scrollbars, exact filters/display ordering, learned-spell drag, and shared server-authoritative delete dialog are connected. `RetailPanelUiController` ports the shared one-active-panel lifecycle so toolbar and indicator launchers converge on one owner. #L.2/#L.3 close and #L.4/AP-110 narrow. Research: `docs/research/2026-07-15-retail-magic-ui-and-casting-pseudocode.md`.
|
- **M3 cast lifecycle + retained magic UI implemented 2026-07-15; visual gates pending.** `SpellCastingController` ports retail's immediate targeted/untargeted request boundary, component preflight, target eligibility, movement stop, and shared UI-busy lifetime while leaving turn/cast/fizzle/impact outcomes authoritative to ACE. Formula preflight reads decrypted portal.dat formulas, the SCID↔WCID map, canonical account randomization, exact school-focus map, infused-magic properties, and scarab-only substitution. Complete live enchantment packets preserve spell/layer identity, all StatMod fields, client-clock normalization, and exact purge buckets. Authored retained surfaces now include the Magic page of `gmCombatUI`, all eight favorite tabs, equipped caster endowment, `gmSpellbookUI`, `gmSpellComponentUI`, both `gmEffectsUI` windows, and the floating Helpful/Harmful effect indicators. The favorite bar now ports retail horizontal empty-cell padding, shared UIItem 1–9 shortcut overlays, and the Cast button's three independently reflowed DAT face segments. Spell rows resolve UIItem prototype `0x10000343`; the component page now instantiates LayoutDesc `0x21000033` category/component templates `0x10000466/67`, including localized headings, exact row textures and columns, owned/desired counts, selection, and the 0..5000 desired-value contract. The authored scrollbars, exact filters/display ordering, learned-spell drag, and shared server-authoritative delete dialog are connected. `RetailPanelUiController` ports the shared one-active-panel lifecycle so toolbar and indicator launchers converge on one owner. #L.2/#L.3 close and #L.4/AP-110 narrow. Research: `docs/research/2026-07-15-retail-magic-ui-and-casting-pseudocode.md`.
|
||||||
|
- **M3 favorite-bar empty-state correction 2026-07-15; visual gate pending.** Shortcut-number arrays remain shared with the status toolbar, but the cell background is now resolved independently from the favorite ItemList's inherited `0x1000000E` cell prototype. LayoutDesc `0x21000010` selects prototype `0x10000341`, whose exact brown/gold `ItemSlot_Empty` surface is `0x06001A97`; the blue toolbar-style `0x060074CF` substitution is removed. The cross-layout resolver and controller presentation are pinned by live-DAT and retained-layout conformance tests.
|
||||||
- **Retail client command families implemented 2026-07-13; corrective live gate pending.** One shared typed catalog now separates retail client actions from ACE administrator commands for both chat backends. Named-decomp ports cover recall/house/PK travel; age/birth; framerate, lock, version, location, corpse, and die confirmation; clear plus named/automatic UI layouts; AFK/consent; emotes; friends; squelch/filter/message types; and fill-components. App owns execution, Core owns authoritative friends/squelch state, Core.Net owns exact UIQueue/ControlQueue packets. Confirmation reuse now ports retail `DialogFactory` contexts, queue groups/priority, fresh DAT roots, property results, callbacks/close notices, and server abort handling; `/die`, gameplay request tuples, and guarded item-use prompts share its type-1 LayoutDesc `0x2100003C` presenter. The first live gate passed the family except for raw suicide-success code `0x004A` and the title-bar-only FPS presentation; the correction maps retail's text and mounts SmartBox element `0x10000047` with live two-decimal `FPS`/`DEG`. #L.6 is closed; TS-31/TS-47 are narrowed. Research: `docs/research/2026-07-13-retail-client-command-families-pseudocode.md`, `docs/research/2026-07-13-retail-dialog-factory-pseudocode.md`.
|
- **Retail client command families implemented 2026-07-13; corrective live gate pending.** One shared typed catalog now separates retail client actions from ACE administrator commands for both chat backends. Named-decomp ports cover recall/house/PK travel; age/birth; framerate, lock, version, location, corpse, and die confirmation; clear plus named/automatic UI layouts; AFK/consent; emotes; friends; squelch/filter/message types; and fill-components. App owns execution, Core owns authoritative friends/squelch state, Core.Net owns exact UIQueue/ControlQueue packets. Confirmation reuse now ports retail `DialogFactory` contexts, queue groups/priority, fresh DAT roots, property results, callbacks/close notices, and server abort handling; `/die`, gameplay request tuples, and guarded item-use prompts share its type-1 LayoutDesc `0x2100003C` presenter. The first live gate passed the family except for raw suicide-success code `0x004A` and the title-bar-only FPS presentation; the correction maps retail's text and mounts SmartBox element `0x10000047` with live two-decimal `FPS`/`DEG`. #L.6 is closed; TS-31/TS-47 are narrowed. Research: `docs/research/2026-07-13-retail-client-command-families-pseudocode.md`, `docs/research/2026-07-13-retail-dialog-factory-pseudocode.md`.
|
||||||
- **✓ SHIPPED — Character window** (`LayoutDesc 0x2100002E`, `CharacterStatController`, 2026-06-26, same branch). **Visually user-confirmed 2026-06-26 — Attributes tab reads as retail.** Three tabs, header (name/heritage/PK), large-gold level number (dat font, `largeDatFont` 18px), "Total Experience (XP):" + "XP for next level:" captions, 9-row attribute list (icons + right-aligned values + Health/Stamina/Mana vitals), click-to-select (top/bottom selection bars + footer State-B "{Attr}: {value}" / "Experience To Raise: Infinity!" + affordability-gated raise triangles), centered footer. User noted "still needs some polish for later" — deferred to Issue #158.
|
- **✓ SHIPPED — Character window** (`LayoutDesc 0x2100002E`, `CharacterStatController`, 2026-06-26, same branch). **Visually user-confirmed 2026-06-26 — Attributes tab reads as retail.** Three tabs, header (name/heritage/PK), large-gold level number (dat font, `largeDatFont` 18px), "Total Experience (XP):" + "XP for next level:" captions, 9-row attribute list (icons + right-aligned values + Health/Stamina/Mana vitals), click-to-select (top/bottom selection bars + footer State-B "{Attr}: {value}" / "Experience To Raise: Infinity!" + affordability-gated raise triangles), centered footer. User noted "still needs some polish for later" — deferred to Issue #158.
|
||||||
- **✓ CORRECTIVE PORT — live character experience qualities** (2026-07-13, #217; connected gate passed). Added retail `PrivateUpdatePropertyInt64 (0x02CF)` so Total Experience, the level-progress meter, and Skills' Unassigned Experience refresh from the same authoritative server update; Total XP value alignment now matches the authored right-justified layout.
|
- **✓ CORRECTIVE PORT — live character experience qualities** (2026-07-13, #217; connected gate passed). Added retail `PrivateUpdatePropertyInt64 (0x02CF)` so Total Experience, the level-progress meter, and Skills' Unassigned Experience refresh from the same authoritative server update; Total XP value alignment now matches the authored right-justified layout.
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,14 @@ authored `ItemSlot_Empty` background. The count is based on the reflowed list
|
||||||
width (639 pixels at the 800-pixel design parent, 439 pixels inside the live
|
width (639 pixels at the 800-pixel design parent, 439 pixels inside the live
|
||||||
600-pixel combat page, yielding 13 whole 32-pixel cells).
|
600-pixel combat page, yielding 13 whole 32-pixel cells).
|
||||||
|
|
||||||
|
The two layers must not be conflated. The favorite ItemList is inherited through
|
||||||
|
LayoutDesc `0x21000010`; its attribute `0x1000000E` selects UIItem prototype
|
||||||
|
`0x10000341` in catalog `0x21000037`. That prototype's `m_elem_Icon`
|
||||||
|
(`0x1000033B`) maps `ItemSlot_Empty` to the brown/gold surface `0x06001A97`.
|
||||||
|
The blue toolbar-style `0x060074CF` surface is not the unnumbered favorite-cell
|
||||||
|
background. Resolve the list prototype after cross-layout inheritance, then add
|
||||||
|
the independent shortcut-number overlay only for indices zero through eight.
|
||||||
|
|
||||||
The Cast button is likewise fully DAT-authored rather than a synthesized red
|
The Cast button is likewise fully DAT-authored rather than a synthesized red
|
||||||
rectangle. Element `0x100000B2` is 75 by 32 pixels and its face is three stateful
|
rectangle. Element `0x100000B2` is 75 by 32 pixels and its face is three stateful
|
||||||
children authored against a 79-pixel design parent: left `0..31`, middle
|
children authored against a 79-pixel design parent: left `0..31`, middle
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ public static class ItemListCellTemplate
|
||||||
public const uint CatalogLayoutId = 0x21000037u;
|
public const uint CatalogLayoutId = 0x21000037u;
|
||||||
|
|
||||||
private const uint CellTemplateAttr = 0x1000000eu; // UIElement attribute: cell-template element id
|
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 uint IconChildId = 0x1000033Bu; // m_elem_Icon sub-element (carries ItemSlot_Empty)
|
||||||
private const string ItemSlotEmpty = "ItemSlot_Empty"; // UIStateId.ToString() for state 0x1000001c
|
private const string ItemSlotEmpty = "ItemSlot_Empty"; // UIStateId.ToString() for state 0x1000001c
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolve the empty-slot sprite (a 0x06xxxxxx RenderSurface id) for the cells of item-list
|
/// 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);
|
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>
|
/// <summary>
|
||||||
/// Resolves <c>ItemSlot_Empty</c> directly from an authored UIItem prototype in
|
/// Resolves <c>ItemSlot_Empty</c> directly from an authored UIItem prototype in
|
||||||
/// <see cref="CatalogLayoutId"/>. Paperdoll lists select distinct prototypes for their
|
/// <see cref="CatalogLayoutId"/>. Paperdoll lists select distinct prototypes for their
|
||||||
|
|
@ -168,4 +191,15 @@ public static class ItemListCellTemplate
|
||||||
}
|
}
|
||||||
return null;
|
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<uint> _useItem;
|
||||||
private readonly Action<int, int, uint>? _addFavorite;
|
private readonly Action<int, int, uint>? _addFavorite;
|
||||||
private readonly Action<int, uint>? _removeFavorite;
|
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[] _tabs;
|
||||||
private readonly UiElement[] _groups;
|
private readonly UiElement[] _groups;
|
||||||
private readonly UiItemList?[] _lists;
|
private readonly UiItemList?[] _lists;
|
||||||
|
|
@ -79,7 +80,8 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
||||||
UiItemList?[] lists,
|
UiItemList?[] lists,
|
||||||
UiButton cast,
|
UiButton cast,
|
||||||
UiElement endowmentHost,
|
UiElement endowmentHost,
|
||||||
UiShortcutSlotGraphics? shortcutGraphics)
|
UiShortcutDigitGraphics? shortcutDigits,
|
||||||
|
uint emptySlotSprite)
|
||||||
{
|
{
|
||||||
_spellbook = spellbook;
|
_spellbook = spellbook;
|
||||||
_casting = casting;
|
_casting = casting;
|
||||||
|
|
@ -91,7 +93,8 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
||||||
_useItem = useItem;
|
_useItem = useItem;
|
||||||
_addFavorite = addFavorite;
|
_addFavorite = addFavorite;
|
||||||
_removeFavorite = removeFavorite;
|
_removeFavorite = removeFavorite;
|
||||||
_shortcutGraphics = shortcutGraphics;
|
_shortcutDigits = shortcutDigits;
|
||||||
|
_emptySlotSprite = emptySlotSprite;
|
||||||
_tabs = tabs;
|
_tabs = tabs;
|
||||||
_groups = groups;
|
_groups = groups;
|
||||||
_lists = lists;
|
_lists = lists;
|
||||||
|
|
@ -143,7 +146,8 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
||||||
SelectionState selection,
|
SelectionState selection,
|
||||||
Action<int, int, uint>? addFavorite,
|
Action<int, int, uint>? addFavorite,
|
||||||
Action<int, uint>? removeFavorite,
|
Action<int, uint>? removeFavorite,
|
||||||
UiShortcutSlotGraphics? shortcutGraphics = null)
|
UiShortcutDigitGraphics? shortcutDigits = null,
|
||||||
|
uint emptySlotSprite = 0u)
|
||||||
{
|
{
|
||||||
if (layout.FindElement(CastButtonId) is not UiButton cast
|
if (layout.FindElement(CastButtonId) is not UiButton cast
|
||||||
|| layout.FindElement(EndowmentId) is not { } endowmentHost)
|
|| layout.FindElement(EndowmentId) is not { } endowmentHost)
|
||||||
|
|
@ -166,7 +170,8 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
||||||
layout, spellbook, casting, objects, playerGuid, resolveSpellIcon,
|
layout, spellbook, casting, objects, playerGuid, resolveSpellIcon,
|
||||||
resolveItemDragIcon, useItem, selection,
|
resolveItemDragIcon, useItem, selection,
|
||||||
addFavorite, removeFavorite,
|
addFavorite, removeFavorite,
|
||||||
tabs, groups, lists, cast, endowmentHost, shortcutGraphics);
|
tabs, groups, lists, cast, endowmentHost,
|
||||||
|
shortcutDigits, emptySlotSprite);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddFavorite(uint spellId)
|
public void AddFavorite(uint spellId)
|
||||||
|
|
@ -291,6 +296,7 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
||||||
list.SingleRow = true;
|
list.SingleRow = true;
|
||||||
list.CellWidth = 32f;
|
list.CellWidth = 32f;
|
||||||
list.CellHeight = 32f;
|
list.CellHeight = 32f;
|
||||||
|
list.CellEmptySprite = _emptySlotSprite;
|
||||||
list.EmptySlotFactory = () => CreateEmptyFavoriteSlot(list, targetTab);
|
list.EmptySlotFactory = () => CreateEmptyFavoriteSlot(list, targetTab);
|
||||||
list.FillVisibleEmptySlots = true;
|
list.FillVisibleEmptySlots = true;
|
||||||
foreach (uint spellId in favorites)
|
foreach (uint spellId in favorites)
|
||||||
|
|
@ -332,9 +338,6 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
||||||
slot = new UiCatalogSlot
|
slot = new UiCatalogSlot
|
||||||
{
|
{
|
||||||
SpriteResolve = list.SpriteResolve,
|
SpriteResolve = list.SpriteResolve,
|
||||||
EmptySprite = _shortcutGraphics is { EmptySprite: > 0u } graphics
|
|
||||||
? graphics.EmptySprite
|
|
||||||
: 0x060074CFu,
|
|
||||||
Dropped = payload =>
|
Dropped = payload =>
|
||||||
{
|
{
|
||||||
int position = Math.Max(0, list.IndexOf(slot!));
|
int position = Math.Max(0, list.IndexOf(slot!));
|
||||||
|
|
@ -362,11 +365,9 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
||||||
|
|
||||||
private void ConfigureShortcutOverlay(UiItemSlot slot, int index)
|
private void ConfigureShortcutOverlay(UiItemSlot slot, int index)
|
||||||
{
|
{
|
||||||
slot.RegularDigits = _shortcutGraphics?.RegularDigits;
|
slot.RegularDigits = _shortcutDigits?.RegularDigits;
|
||||||
slot.GhostedDigits = _shortcutGraphics?.GhostedDigits;
|
slot.GhostedDigits = _shortcutDigits?.GhostedDigits;
|
||||||
slot.EmptyDigits = _shortcutGraphics?.EmptyDigits;
|
slot.EmptyDigits = _shortcutDigits?.EmptyDigits;
|
||||||
if (_shortcutGraphics is { EmptySprite: > 0u } graphics)
|
|
||||||
slot.EmptySprite = graphics.EmptySprite;
|
|
||||||
if (index < 9)
|
if (index < 9)
|
||||||
slot.SetShortcutNum(index, ghosted: false);
|
slot.SetShortcutNum(index, ghosted: false);
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,7 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
private GameplayConfirmationController? _gameplayConfirmationController;
|
private GameplayConfirmationController? _gameplayConfirmationController;
|
||||||
private RetailItemConfirmationController? _itemConfirmationController;
|
private RetailItemConfirmationController? _itemConfirmationController;
|
||||||
private RetailSkillTrainingConfirmationController? _skillTrainingConfirmationController;
|
private RetailSkillTrainingConfirmationController? _skillTrainingConfirmationController;
|
||||||
private UiShortcutSlotGraphics? _shortcutSlotGraphics;
|
private UiShortcutDigitGraphics? _shortcutDigitGraphics;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
private RetailUiRuntime(RetailUiRuntimeBindings bindings)
|
private RetailUiRuntime(RetailUiRuntimeBindings bindings)
|
||||||
|
|
@ -559,12 +559,12 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
UiShortcutSlotGraphics shortcutGraphics = LoadShortcutSlotGraphics();
|
UiShortcutDigitGraphics shortcutDigits = LoadShortcutDigitGraphics();
|
||||||
ToolbarRuntimeBindings b = _bindings.Toolbar;
|
ToolbarRuntimeBindings b = _bindings.Toolbar;
|
||||||
ToolbarController = Layout.ToolbarController.Bind(
|
ToolbarController = Layout.ToolbarController.Bind(
|
||||||
layout, b.Objects, b.Shortcuts, b.ResolveIcon, b.UseItem, b.Combat,
|
layout, b.Objects, b.Shortcuts, b.ResolveIcon, b.UseItem, b.Combat,
|
||||||
shortcutGraphics.RegularDigits, shortcutGraphics.GhostedDigits,
|
shortcutDigits.RegularDigits, shortcutDigits.GhostedDigits,
|
||||||
shortcutGraphics.EmptyDigits, b.ItemInteraction,
|
shortcutDigits.EmptyDigits, b.ItemInteraction,
|
||||||
b.SendAddShortcut, b.SendRemoveShortcut,
|
b.SendAddShortcut, b.SendRemoveShortcut,
|
||||||
toggleCombat: b.ToggleCombat,
|
toggleCombat: b.ToggleCombat,
|
||||||
selectItem: guid => b.Selection.Select(guid, SelectionChangeSource.Toolbar),
|
selectItem: guid => b.Selection.Select(guid, SelectionChangeSource.Toolbar),
|
||||||
|
|
@ -625,9 +625,16 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
ElementInfo? info;
|
ElementInfo? info;
|
||||||
ImportedLayout? layout;
|
ImportedLayout? layout;
|
||||||
CombatUiLabels? labels;
|
CombatUiLabels? labels;
|
||||||
|
uint favoriteEmptySprite;
|
||||||
lock (_bindings.Assets.DatLock)
|
lock (_bindings.Assets.DatLock)
|
||||||
{
|
{
|
||||||
info = LayoutImporter.ImportInfos(_bindings.Assets.Dats, CombatUiController.LayoutId);
|
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);
|
var strings = new DatStringResolver(_bindings.Assets.Dats);
|
||||||
layout = info is null ? null : LayoutImporter.Build(
|
layout = info is null ? null : LayoutImporter.Build(
|
||||||
info,
|
info,
|
||||||
|
|
@ -675,7 +682,8 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
_bindings.Magic.Selection,
|
_bindings.Magic.Selection,
|
||||||
_bindings.Magic.AddFavorite,
|
_bindings.Magic.AddFavorite,
|
||||||
_bindings.Magic.RemoveFavorite,
|
_bindings.Magic.RemoveFavorite,
|
||||||
LoadShortcutSlotGraphics());
|
LoadShortcutDigitGraphics(),
|
||||||
|
favoriteEmptySprite);
|
||||||
if (spellcasting is null)
|
if (spellcasting is null)
|
||||||
Console.WriteLine("[M3] spellcasting: required controls missing in LayoutDesc 0x21000073.");
|
Console.WriteLine("[M3] spellcasting: required controls missing in LayoutDesc 0x21000073.");
|
||||||
|
|
||||||
|
|
@ -706,7 +714,8 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
controller.SyncVisibility();
|
controller.SyncVisibility();
|
||||||
Console.WriteLine(spellcasting is null
|
Console.WriteLine(spellcasting is null
|
||||||
? "[M2] retail combat from LayoutDesc 0x21000073; magic binding unavailable."
|
? "[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()
|
private void MountSpellbook()
|
||||||
|
|
@ -1016,13 +1025,12 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
$"[UI] retail DialogFactory from LayoutDesc 0x{layoutId:X8}; confirmation root 0x15.");
|
$"[UI] retail DialogFactory from LayoutDesc 0x{layoutId:X8}; confirmation root 0x15.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private UiShortcutSlotGraphics LoadShortcutSlotGraphics()
|
private UiShortcutDigitGraphics LoadShortcutDigitGraphics()
|
||||||
{
|
{
|
||||||
if (_shortcutSlotGraphics is not null)
|
if (_shortcutDigitGraphics is not null)
|
||||||
return _shortcutSlotGraphics;
|
return _shortcutDigitGraphics;
|
||||||
|
|
||||||
uint[]? regular = null, ghosted = null, empty = null;
|
uint[]? regular = null, ghosted = null, empty = null;
|
||||||
uint emptySprite = 0u;
|
|
||||||
lock (_bindings.Assets.DatLock)
|
lock (_bindings.Assets.DatLock)
|
||||||
{
|
{
|
||||||
var layout = _bindings.Assets.Dats.Get<DatReaderWriter.DBObjs.LayoutDesc>(0x21000037u);
|
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)
|
&& emptyComposite.Children.TryGetValue(0x1000034Au, out var emptyNumber)
|
||||||
&& emptyNumber.StateDesc?.Properties is { } emptyProps)
|
&& emptyNumber.StateDesc?.Properties is { } emptyProps)
|
||||||
empty = ReadDataIds(emptyProps, 0x1000005Eu);
|
empty = ReadDataIds(emptyProps, 0x1000005Eu);
|
||||||
emptySprite = ItemListCellTemplate.ResolveEmptySprite(
|
|
||||||
_bindings.Assets.Dats,
|
|
||||||
CombatUiController.LayoutId,
|
|
||||||
SpellcastingUiController.FavoriteListId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
regular ??=
|
regular ??=
|
||||||
|
|
@ -1055,13 +1059,12 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
0x06001ACCu, 0x06001ACDu, 0x06001ACEu, 0x06001ACFu, 0x06001AD0u,
|
0x06001ACCu, 0x06001ACDu, 0x06001ACEu, 0x06001ACFu, 0x06001AD0u,
|
||||||
0x06001AD1u, 0x06001AD2u, 0x06001AD3u, 0x06001AD4u,
|
0x06001AD1u, 0x06001AD2u, 0x06001AD3u, 0x06001AD4u,
|
||||||
];
|
];
|
||||||
_shortcutSlotGraphics = new UiShortcutSlotGraphics(
|
_shortcutDigitGraphics = new UiShortcutDigitGraphics(
|
||||||
regular, ghosted, empty, emptySprite);
|
regular, ghosted, empty);
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"[D.5.1] shared shortcut graphics ready: regular={regular.Length}, " +
|
$"[D.5.1] shared shortcut digits ready: regular={regular.Length}, " +
|
||||||
$"ghosted={ghosted.Length}, empty={empty?.Length ?? 0}, " +
|
$"ghosted={ghosted.Length}, empty={empty?.Length ?? 0}.");
|
||||||
$"slot=0x{emptySprite:X8}.");
|
return _shortcutDigitGraphics;
|
||||||
return _shortcutSlotGraphics;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static uint[]? ReadDataIds(
|
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);
|
|
||||||
|
|
@ -51,8 +51,8 @@ public class ItemListCellTemplateTests
|
||||||
{
|
{
|
||||||
uint sprite = ItemListCellTemplate.ResolveEmptySprite(dats, layout, elem);
|
uint sprite = ItemListCellTemplate.ResolveEmptySprite(dats, layout, elem);
|
||||||
_out.WriteLine($"{name}: list 0x{elem:X8} (layout 0x{layout:X8}) -> empty sprite 0x{sprite:X8}");
|
_out.WriteLine($"{name}: list 0x{elem:X8} (layout 0x{layout:X8}) -> empty sprite 0x{sprite:X8}");
|
||||||
Assert.True(sprite != 0u, $"{name}: resolved 0 (no 0x1000000e attr / prototype / media)");
|
Assert.True(sprite != 0u, $"{name}: resolved 0 (no 0x1000000e attr / prototype / media)");
|
||||||
Assert.True(sprite != Generic, $"{name}: resolved the generic 0x060074CF — the bug, not the fix");
|
Assert.True(sprite != Generic, $"{name}: resolved the generic 0x060074CF — the bug, not the fix");
|
||||||
Assert.True((sprite & 0xFF000000u) == 0x06000000u, $"{name}: 0x{sprite:X8} is not a 0x06 RenderSurface id");
|
Assert.True((sprite & 0xFF000000u) == 0x06000000u, $"{name}: 0x{sprite:X8} is not a 0x06 RenderSurface id");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -72,4 +72,24 @@ public class ItemListCellTemplateTests
|
||||||
Assert.Equal(0x06000F6Eu, ItemListCellTemplate.ResolveEmptySprite(dats, BackpackLayout, SideBag));
|
Assert.Equal(0x06000F6Eu, ItemListCellTemplate.ResolveEmptySprite(dats, BackpackLayout, SideBag));
|
||||||
Assert.Equal(0x06000F6Eu, ItemListCellTemplate.ResolveEmptySprite(dats, BackpackLayout, MainPack));
|
Assert.Equal(0x06000F6Eu, ItemListCellTemplate.ResolveEmptySprite(dats, BackpackLayout, MainPack));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Spell_favorite_list_resolves_its_cross_layout_pinned_empty_sprite()
|
||||||
|
{
|
||||||
|
var datDir = DatDir();
|
||||||
|
if (datDir is null) return; // CI: no live dat — skip
|
||||||
|
|
||||||
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||||
|
ElementInfo infos = Assert.IsType<ElementInfo>(
|
||||||
|
LayoutImporter.ImportInfos(dats, CombatUiController.LayoutId));
|
||||||
|
|
||||||
|
uint sprite = ItemListCellTemplate.ResolveEmptySprite(
|
||||||
|
dats, infos, SpellcastingUiController.FavoriteListId);
|
||||||
|
_out.WriteLine($"spell favorites -> empty sprite 0x{sprite:X8}");
|
||||||
|
|
||||||
|
// gmSpellcastingUI's ItemList is inherited from LayoutDesc 0x21000010.
|
||||||
|
// Its cell-template attr selects catalog prototype 0x10000341, whose
|
||||||
|
// m_elem_Icon ItemSlot_Empty state is the brown/gold 0x06001A97 surface.
|
||||||
|
Assert.Equal(0x06001A97u, sprite);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,12 @@ public sealed class SpellcastingUiControllerTests
|
||||||
uint[] regular = [101u, 102u, 103u, 104u, 105u, 106u, 107u, 108u, 109u];
|
uint[] regular = [101u, 102u, 103u, 104u, 105u, 106u, 107u, 108u, 109u];
|
||||||
uint[] ghosted = [201u, 202u, 203u, 204u, 205u, 206u, 207u, 208u, 209u];
|
uint[] ghosted = [201u, 202u, 203u, 204u, 205u, 206u, 207u, 208u, 209u];
|
||||||
uint[] empty = [301u, 302u, 303u, 304u, 305u, 306u, 307u, 308u, 309u];
|
uint[] empty = [301u, 302u, 303u, 304u, 305u, 306u, 307u, 308u, 309u];
|
||||||
var graphics = new UiShortcutSlotGraphics(regular, ghosted, empty, 0x060074CFu);
|
var digits = new UiShortcutDigitGraphics(regular, ghosted, empty);
|
||||||
|
|
||||||
using SpellcastingUiController controller = Bind(
|
using SpellcastingUiController controller = Bind(
|
||||||
layout, spellbook, objects, _ => { }, shortcutGraphics: graphics)!;
|
layout, spellbook, objects, _ => { },
|
||||||
|
shortcutDigits: digits,
|
||||||
|
emptySlotSprite: 0x06001A97u)!;
|
||||||
|
|
||||||
UiElement group = layout.FindElement(0x100000AAu)!;
|
UiElement group = layout.FindElement(0x100000AAu)!;
|
||||||
UiItemList list = Descendants(group).OfType<UiItemList>().First();
|
UiItemList list = Descendants(group).OfType<UiItemList>().First();
|
||||||
|
|
@ -46,11 +48,15 @@ public sealed class SpellcastingUiControllerTests
|
||||||
{
|
{
|
||||||
UiCatalogSlot slot = Assert.IsType<UiCatalogSlot>(list.GetItem(i));
|
UiCatalogSlot slot = Assert.IsType<UiCatalogSlot>(list.GetItem(i));
|
||||||
Assert.True(slot.IsEmptySlot);
|
Assert.True(slot.IsEmptySlot);
|
||||||
|
Assert.Equal(0x06001A97u, slot.EmptySprite);
|
||||||
Assert.Equal(i, slot.ShortcutNum);
|
Assert.Equal(i, slot.ShortcutNum);
|
||||||
Assert.Same(empty, slot.ActiveDigitArray());
|
Assert.Same(empty, slot.ActiveDigitArray());
|
||||||
}
|
}
|
||||||
for (int i = 9; i < 13; i++)
|
for (int i = 9; i < 13; i++)
|
||||||
|
{
|
||||||
|
Assert.Equal(0x06001A97u, list.GetItem(i)!.EmptySprite);
|
||||||
Assert.Equal(-1, list.GetItem(i)!.ShortcutNum);
|
Assert.Equal(-1, list.GetItem(i)!.ShortcutNum);
|
||||||
|
}
|
||||||
|
|
||||||
var cast = Assert.IsType<UiButton>(
|
var cast = Assert.IsType<UiButton>(
|
||||||
layout.FindElement(SpellcastingUiController.CastButtonId));
|
layout.FindElement(SpellcastingUiController.CastButtonId));
|
||||||
|
|
@ -222,7 +228,8 @@ public sealed class SpellcastingUiControllerTests
|
||||||
ClientObjectTable objects,
|
ClientObjectTable objects,
|
||||||
Action<uint> useItem,
|
Action<uint> useItem,
|
||||||
Action<int, int, uint>? addFavorite = null,
|
Action<int, int, uint>? addFavorite = null,
|
||||||
UiShortcutSlotGraphics? shortcutGraphics = null)
|
UiShortcutDigitGraphics? shortcutDigits = null,
|
||||||
|
uint emptySlotSprite = 0u)
|
||||||
{
|
{
|
||||||
var casting = new SpellCastingController(
|
var casting = new SpellCastingController(
|
||||||
spellbook, () => null, () => 1u, () => { }, _ => { }, (_, _) => { }, _ => { });
|
spellbook, () => null, () => 1u, () => { }, _ => { }, (_, _) => { }, _ => { });
|
||||||
|
|
@ -234,7 +241,8 @@ public sealed class SpellcastingUiControllerTests
|
||||||
new SelectionState(),
|
new SelectionState(),
|
||||||
addFavorite ?? ((_, _, _) => { }),
|
addFavorite ?? ((_, _, _) => { }),
|
||||||
(_, _) => { },
|
(_, _) => { },
|
||||||
shortcutGraphics);
|
shortcutDigits,
|
||||||
|
emptySlotSprite);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ApplyAnchors(UiElement parent)
|
private static void ApplyAnchors(UiElement parent)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue