Retail IconData::RenderIcons (decomp 407524) builds the icon layer stack bottom→top: type-default underlay (OPAQUE, Blit_Normal) first, then custom underlay, base icon, custom overlay. acdream's IconComposer omitted the type-default underlay, leaving filled toolbar slots with a transparent background. Resolution via the two-level EnumIDMap chain that retail uses (DBCache::GetDIDFromEnum 0x413940): Portal.Header.MasterMapId (0x25000000) → master[0x10000004] → submap DID (0x25000008) → submap[LSB(itemType)+1] → 0x06 RenderSurface underlay DID. Golden values confirmed against the live dats: MeleeWeapon→0x060011CB, Armor→0x060011CF, Clothing→0x060011F3, Jewelry→0x060011D5, None(fallback 0x21)→0x060011D4. Changes: - IconComposer: add ResolveUnderlayDid(ItemType)/EnsureUnderlaySubMap (memoised); widen cache key from (uint,uint,uint)→(uint,uint,uint,uint); GetIcon gains ItemType param and prepends the opaque underlay as layer 0 (Compose sizes to it → fully opaque) - ToolbarController: widen _iconIds Func from 3-arg to 4-arg; Populate passes item.Type - GameWindow: update toolbar mount lambda to 4-arg form - Tests: update ToolbarController test stubs to (_,_,_,_); add Compose_opaqueUnderlayFirst_resultIsFullyOpaque (dat-free) and ResolveUnderlayDid_goldenValues_matchDat (dat-gated, skip when dats absent) No divergence-register row existed for this omission; none added (fully ported now). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
196 lines
8.9 KiB
C#
196 lines
8.9 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using AcDream.Core.Combat;
|
|
using AcDream.Core.Items;
|
|
using AcDream.Core.Net.Messages;
|
|
|
|
namespace AcDream.App.UI.Layout;
|
|
|
|
/// <summary>
|
|
/// Binds the imported gmToolbarUI window (LayoutDesc 0x21000016) to live data —
|
|
/// the gm*UI::PostInit analogue. Finds the 18 shortcut slots (UiItemList) by id,
|
|
/// populates them from the persisted PlayerDescription shortcuts
|
|
/// (UpdateFromPlayerDesc), re-binds deferred slots when an item's CreateObject
|
|
/// arrives (SetDelayedShortcutNum), and on click uses the bound item
|
|
/// (UseShortcut -> ItemHolder::UseObject -> use-item callback).
|
|
///
|
|
/// <para>
|
|
/// Retail reference: <c>gmToolbarUI::PostInit</c> grabs each slot widget by its
|
|
/// id, calls <c>UpdateFromPlayerDesc</c> to flush-and-bind shortcuts from the
|
|
/// PlayerDescription trailer, and hooks <c>OnEvent</c> for the Click case to fire
|
|
/// <c>UseShortcut</c>. The deferred-rebind path matches
|
|
/// <c>gmToolbarUI::SetDelayedShortcutNum</c> which re-tries binding after
|
|
/// <c>CreateObject</c> resolves a formerly-unknown guid.
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class ToolbarController
|
|
{
|
|
// Slot element ids in slot-index order (toolbar LayoutDesc 0x21000016, pre-dump).
|
|
// Row 1 = slots 0-8 (0x100001A7..0x100001AF), Row 2 = slots 9-17 (0x100006B7..0x100006BF).
|
|
private static readonly uint[] SlotIds =
|
|
{
|
|
0x100001A7, 0x100001A8, 0x100001A9, 0x100001AA, 0x100001AB,
|
|
0x100001AC, 0x100001AD, 0x100001AE, 0x100001AF,
|
|
0x100006B7, 0x100006B8, 0x100006B9, 0x100006BA, 0x100006BB,
|
|
0x100006BC, 0x100006BD, 0x100006BE, 0x100006BF,
|
|
};
|
|
|
|
// Elements hidden by default in retail gmToolbarUI::PostInit: the selected-object
|
|
// vitals meters (health/stamina/mana bars that track your target) and the stack slider.
|
|
// Ids confirmed from the toolbar LayoutDesc dump.
|
|
private static readonly uint[] HiddenIds = { 0x100001A1, 0x100001A2, 0x100001A4 };
|
|
|
|
// Four mutually-exclusive combat-mode indicator elements — exactly one visible at a time.
|
|
// Index 0 = NonCombat (peace), 1 = Melee, 2 = Missile, 3 = Magic.
|
|
// Retail ref: gmToolbarUI::RecvNotice_SetCombatMode (acclient_2013_pseudo_c.txt:196632-196669)
|
|
// SetVisible's exactly one element depending on the incoming mode.
|
|
private static readonly uint[] CombatIndicatorIds =
|
|
{ 0x10000192u, 0x10000193u, 0x10000194u, 0x10000195u };
|
|
|
|
private readonly UiItemList?[] _slots = new UiItemList?[SlotIds.Length];
|
|
private readonly UiElement?[] _combatIndicators = new UiElement?[CombatIndicatorIds.Length];
|
|
private readonly ItemRepository _repo;
|
|
private readonly Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> _shortcuts;
|
|
private readonly Func<ItemType, uint, uint, uint, uint> _iconIds; // (itemType, iconId, underlayId, overlayId) → GL tex
|
|
private readonly Action<uint> _useItem; // guid → fire UseObject
|
|
|
|
private ToolbarController(
|
|
ImportedLayout layout,
|
|
ItemRepository repo,
|
|
Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> shortcuts,
|
|
Func<ItemType, uint, uint, uint, uint> iconIds,
|
|
Action<uint> useItem,
|
|
CombatState? combatState)
|
|
{
|
|
_repo = repo;
|
|
_shortcuts = shortcuts;
|
|
_iconIds = iconIds;
|
|
_useItem = useItem;
|
|
|
|
for (int i = 0; i < SlotIds.Length; i++)
|
|
{
|
|
_slots[i] = layout.FindElement(SlotIds[i]) as UiItemList;
|
|
if (_slots[i] is { } list)
|
|
WireClick(list);
|
|
}
|
|
|
|
// Cache the four mutually-exclusive combat-mode indicator elements.
|
|
for (int i = 0; i < CombatIndicatorIds.Length; i++)
|
|
_combatIndicators[i] = layout.FindElement(CombatIndicatorIds[i]);
|
|
|
|
// Hide target-object meters + stack slider (gmToolbarUI::PostInit).
|
|
foreach (var id in HiddenIds)
|
|
if (layout.FindElement(id) is { } e) e.Visible = false;
|
|
|
|
// Port of gmToolbarUI::RecvNotice_SetCombatMode (acclient_2013_pseudo_c.txt:196632-196669):
|
|
// exactly one indicator visible at a time. Default to NonCombat (peace) — the player
|
|
// always spawns in peace mode; retail has not yet called SetVisible when PostInit runs.
|
|
SetCombatMode(CombatMode.NonCombat);
|
|
|
|
// Wire live combat-mode changes if a CombatState was provided.
|
|
if (combatState is not null)
|
|
combatState.CombatModeChanged += SetCombatMode;
|
|
|
|
// Re-bind any deferred slot whenever the repo learns about a new/updated item.
|
|
repo.ItemAdded += _ => Populate();
|
|
repo.ItemPropertiesUpdated += _ => Populate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create and bind a <see cref="ToolbarController"/> to <paramref name="layout"/>.
|
|
/// Calls <see cref="Populate"/> immediately (binds whatever items are in the repo now).
|
|
/// Returns the controller so the caller can call <see cref="Populate"/> again
|
|
/// if the shortcut list is refreshed outside the repo-event path.
|
|
/// </summary>
|
|
/// <param name="layout">Imported toolbar layout (LayoutDesc 0x21000016).</param>
|
|
/// <param name="repo">Live item repository — must stay alive for the controller's lifetime.</param>
|
|
/// <param name="shortcuts">Provider for the current shortcut bar list.</param>
|
|
/// <param name="iconIds">Resolves (itemType, iconId, underlayId, overlayId) → GL texture handle.</param>
|
|
/// <param name="useItem">Callback fired when a bound slot is clicked; receives the item guid.</param>
|
|
/// <param name="combatState">
|
|
/// Optional live combat state — when provided, the toolbar subscribes to
|
|
/// <see cref="CombatState.CombatModeChanged"/> and updates the four mutually-exclusive
|
|
/// combat-mode indicator elements accordingly.
|
|
/// Pass null to skip live wiring (e.g. in unit tests that don't exercise the indicator).
|
|
/// </param>
|
|
public static ToolbarController Bind(
|
|
ImportedLayout layout,
|
|
ItemRepository repo,
|
|
Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> shortcuts,
|
|
Func<ItemType, uint, uint, uint, uint> iconIds,
|
|
Action<uint> useItem,
|
|
CombatState? combatState = null)
|
|
{
|
|
var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState);
|
|
c.Populate();
|
|
return c;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Port of <c>gmToolbarUI::UpdateFromPlayerDesc</c>: clear all slots, then bind
|
|
/// each shortcut entry that has a resolved item in the repository.
|
|
/// Entries whose item is not yet in the repo are silently skipped here; the
|
|
/// <c>ItemAdded</c> event re-fires this method when the item arrives
|
|
/// (matching retail's <c>SetDelayedShortcutNum</c> deferred-rebind path).
|
|
/// </summary>
|
|
public void Populate()
|
|
{
|
|
// Clear all slot cells first (flush).
|
|
foreach (var list in _slots) list?.Cell.Clear();
|
|
|
|
foreach (var sc in _shortcuts())
|
|
{
|
|
if (sc.ObjectGuid == 0) continue; // spell-only shortcut — inventory phase
|
|
if (sc.Index >= (uint)_slots.Length) continue;
|
|
var list = _slots[(int)sc.Index];
|
|
if (list is null) continue;
|
|
|
|
var item = _repo.GetItem(sc.ObjectGuid);
|
|
if (item is null) continue; // deferred: ItemAdded will re-call Populate
|
|
|
|
uint tex = _iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId);
|
|
list.Cell.SetItem(sc.ObjectGuid, tex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Port of <c>gmToolbarUI::RecvNotice_SetCombatMode</c>
|
|
/// (acclient_2013_pseudo_c.txt:196632-196669): show exactly one of the four
|
|
/// mutually-exclusive combat-mode indicator elements and hide the other three.
|
|
/// Called at bind-time with <see cref="CombatMode.NonCombat"/> (the player
|
|
/// always starts in peace mode) and subsequently whenever
|
|
/// <see cref="CombatState.CombatModeChanged"/> fires.
|
|
/// </summary>
|
|
public void SetCombatMode(CombatMode mode)
|
|
{
|
|
// Index → mode mapping matches CombatIndicatorIds declaration order:
|
|
// 0 = NonCombat (peace), 1 = Melee, 2 = Missile, 3 = Magic.
|
|
bool[] show =
|
|
{
|
|
mode == CombatMode.NonCombat,
|
|
mode == CombatMode.Melee,
|
|
mode == CombatMode.Missile,
|
|
mode == CombatMode.Magic,
|
|
};
|
|
|
|
for (int i = 0; i < _combatIndicators.Length; i++)
|
|
{
|
|
if (_combatIndicators[i] is { } e)
|
|
e.Visible = show[i];
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wire the <see cref="UiItemSlot.Clicked"/> callback on a slot cell so that
|
|
/// clicking a bound item fires <see cref="_useItem"/> with the slot's current guid.
|
|
/// Mirrors retail's <c>gmToolbarUI</c> click → <c>UseShortcut</c> dispatch.
|
|
/// </summary>
|
|
private void WireClick(UiItemList list)
|
|
{
|
|
list.Cell.Clicked = () =>
|
|
{
|
|
if (list.Cell.ItemId != 0)
|
|
_useItem(list.Cell.ItemId);
|
|
};
|
|
}
|
|
}
|