feat(D.5.1): faithful item-icon type-default underlay (EnumIDMap 0x10000004) — opaque icon backing
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>
This commit is contained in:
parent
bfc452d610
commit
f21dbfad80
5 changed files with 156 additions and 23 deletions
|
|
@ -1920,7 +1920,7 @@ public sealed class GameWindow : IDisposable
|
||||||
_toolbarController = AcDream.App.UI.Layout.ToolbarController.Bind(
|
_toolbarController = AcDream.App.UI.Layout.ToolbarController.Bind(
|
||||||
toolbarLayout, Items,
|
toolbarLayout, Items,
|
||||||
() => Shortcuts,
|
() => Shortcuts,
|
||||||
iconIds: (icon, under, over) => iconComposer.GetIcon(icon, under, over),
|
iconIds: (type, icon, under, over) => iconComposer.GetIcon(type, icon, under, over),
|
||||||
useItem: guid => UseItemByGuid(guid),
|
useItem: guid => UseItemByGuid(guid),
|
||||||
combatState: Combat);
|
combatState: Combat);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.Core.Items;
|
||||||
using AcDream.Core.Textures;
|
using AcDream.Core.Textures;
|
||||||
using DatReaderWriter;
|
using DatReaderWriter;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
|
|
@ -9,18 +11,37 @@ namespace AcDream.App.UI;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds an item icon by alpha-compositing its RenderSurface layers into one 32x32
|
/// Builds an item icon by alpha-compositing its RenderSurface layers into one 32x32
|
||||||
/// texture, mirroring retail IconData::RenderIcons (decomp 407524). Each layer is a
|
/// texture, mirroring retail IconData::RenderIcons (decomp 407524) and
|
||||||
/// 0x06 RenderSurface decoded DIRECTLY (the D.2b RenderSurface-vs-Surface rule).
|
/// DBCache::GetDIDFromEnum (0x413940). Each layer is a 0x06 RenderSurface decoded
|
||||||
/// Phase 1 composites the layers ItemInstance exposes (custom underlay + base +
|
/// DIRECTLY (the D.2b RenderSurface-vs-Surface rule).
|
||||||
/// custom overlay); the GetByEnum type-default underlay, the overlay ReplaceColor
|
///
|
||||||
/// tint, and the effect overlay are deferred (see plan Task 12 / divergence rows).
|
/// Layer order (bottom → top), matching retail:
|
||||||
/// Composited textures are cached by their layer-id tuple.
|
/// 1. type-default underlay (OPAQUE backing; resolved via EnumIDMap 0x10000004 from
|
||||||
|
/// the portal MasterMap) — <see cref="ResolveUnderlayDid"/>
|
||||||
|
/// 2. item custom underlay (e.g. "magic" tint strip)
|
||||||
|
/// 3. base icon
|
||||||
|
/// 4. item custom overlay (e.g. "enchanted" sparkle)
|
||||||
|
///
|
||||||
|
/// The type-default underlay is the key to non-transparent filled slots: because it
|
||||||
|
/// is fully opaque and is layer 0, <see cref="Compose"/> sizes the output to it and
|
||||||
|
/// the alpha-over pass fills every pixel. The overlay ReplaceColor tint and the effect
|
||||||
|
/// overlay (RenderIcons 407546) remain out of scope (paperdoll phase).
|
||||||
|
///
|
||||||
|
/// Composited textures are cached by their (typeUnderlay, underlay, base, overlay) tuple.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class IconComposer
|
public sealed class IconComposer
|
||||||
{
|
{
|
||||||
private readonly DatCollection _dats;
|
private readonly DatCollection _dats;
|
||||||
private readonly TextureCache _cache;
|
private readonly TextureCache _cache;
|
||||||
private readonly Dictionary<(uint, uint, uint), uint> _byTuple = new();
|
private readonly Dictionary<(uint, uint, uint, uint), uint> _byTuple = new();
|
||||||
|
|
||||||
|
// ── type-default underlay resolve (EnumIDMap 0x10000004) ─────────────────
|
||||||
|
// Portal MasterMap (0x25000000) maps enum 0x10000004 → submap DID (0x25000008).
|
||||||
|
// Submap maps index → 0x06 RenderSurface DID. index = LSB(itemType)+1, or 0x21.
|
||||||
|
// Refs: IconData::RenderIcons 0058d214–0058d22c; DBCache::GetDIDFromEnum 0x413940.
|
||||||
|
private EnumIDMap? _underlaySubMap;
|
||||||
|
private bool _underlayResolveTried;
|
||||||
|
private readonly Dictionary<uint, uint> _underlayDidByIndex = new();
|
||||||
|
|
||||||
public IconComposer(DatCollection dats, TextureCache cache)
|
public IconComposer(DatCollection dats, TextureCache cache)
|
||||||
{
|
{
|
||||||
|
|
@ -28,6 +49,41 @@ public sealed class IconComposer
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolve the type-default underlay DID for <paramref name="itemType"/> via the
|
||||||
|
/// two-level EnumIDMap chain (retail: IconData::RenderIcons 0058d214–0058d22c +
|
||||||
|
/// DBCache::GetDIDFromEnum 0x413940).
|
||||||
|
///
|
||||||
|
/// <para>index = LowestSetBit(itemType) + 1, or 0x21 when itemType has no bits set.</para>
|
||||||
|
///
|
||||||
|
/// <para>NOTE: retail RenderIcons (407546) has a special paperdoll IsThePlayer case
|
||||||
|
/// that uses GetDIDByEnum(0x10000004, 7) + TYPE_CONTAINER for the player doll — that
|
||||||
|
/// path is out of scope here (paperdoll phase).</para>
|
||||||
|
/// </summary>
|
||||||
|
internal uint ResolveUnderlayDid(ItemType itemType)
|
||||||
|
{
|
||||||
|
uint raw = (uint)itemType;
|
||||||
|
int lsb = raw == 0 ? -1 : BitOperations.TrailingZeroCount(raw);
|
||||||
|
uint index = lsb < 0 ? 0x21u : (uint)(lsb + 1);
|
||||||
|
if (_underlayDidByIndex.TryGetValue(index, out var cached)) return cached;
|
||||||
|
EnsureUnderlaySubMap();
|
||||||
|
uint did = 0;
|
||||||
|
if (_underlaySubMap is { } sub && sub.ClientEnumToID.TryGetValue(index, out var d)) did = d;
|
||||||
|
_underlayDidByIndex[index] = did;
|
||||||
|
return did;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureUnderlaySubMap()
|
||||||
|
{
|
||||||
|
if (_underlayResolveTried) return;
|
||||||
|
_underlayResolveTried = true;
|
||||||
|
uint masterDid = (uint)_dats.Portal.Header.MasterMapId; // = 0x25000000
|
||||||
|
if (masterDid == 0) return;
|
||||||
|
if (!_dats.Portal.TryGet<EnumIDMap>(masterDid, out var master)) return;
|
||||||
|
if (!master.ClientEnumToID.TryGetValue(0x10000004u, out var subDid)) return; // → 0x25000008
|
||||||
|
if (_dats.Portal.TryGet<EnumIDMap>(subDid, out var sub)) _underlaySubMap = sub;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Pure alpha-over composite, bottom->top. Layers may differ in size;
|
/// <summary>Pure alpha-over composite, bottom->top. Layers may differ in size;
|
||||||
/// the result is sized to the FIRST (bottom) layer and upper layers are sampled
|
/// the result is sized to the FIRST (bottom) layer and upper layers are sampled
|
||||||
/// top-left aligned (all icon layers are 32x32 in practice).</summary>
|
/// top-left aligned (all icon layers are 32x32 in practice).</summary>
|
||||||
|
|
@ -56,15 +112,24 @@ public sealed class IconComposer
|
||||||
return (outp, w, h);
|
return (outp, w, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Resolve (and cache) the composited GL texture for an item's icon
|
/// <summary>
|
||||||
/// layers. Returns 0 if no base icon is available.</summary>
|
/// Resolve (and cache) the composited GL texture for an item's icon layers.
|
||||||
public uint GetIcon(uint iconId, uint underlayId, uint overlayId)
|
/// Returns 0 if no base icon is available.
|
||||||
|
///
|
||||||
|
/// <para>Layer order mirrors retail IconData::RenderIcons (decomp 407524):
|
||||||
|
/// type-default underlay (OPAQUE) → custom underlay → base icon → custom overlay.
|
||||||
|
/// The type-default underlay is resolved via the EnumIDMap 0x10000004 chain;
|
||||||
|
/// its presence ensures filled slots are never transparent.</para>
|
||||||
|
/// </summary>
|
||||||
|
public uint GetIcon(ItemType itemType, uint iconId, uint underlayId, uint overlayId)
|
||||||
{
|
{
|
||||||
if (iconId == 0) return 0;
|
if (iconId == 0) return 0;
|
||||||
var key = (iconId, underlayId, overlayId);
|
uint typeUnderlayDid = ResolveUnderlayDid(itemType);
|
||||||
|
var key = (typeUnderlayDid, iconId, underlayId, overlayId);
|
||||||
if (_byTuple.TryGetValue(key, out var tex)) return tex;
|
if (_byTuple.TryGetValue(key, out var tex)) return tex;
|
||||||
|
|
||||||
var layers = new List<(byte[] rgba, int w, int h)>();
|
var layers = new List<(byte[] rgba, int w, int h)>();
|
||||||
|
AddLayer(layers, typeUnderlayDid); // OPAQUE bottom; sizes the 32x32 output
|
||||||
AddLayer(layers, underlayId);
|
AddLayer(layers, underlayId);
|
||||||
AddLayer(layers, iconId);
|
AddLayer(layers, iconId);
|
||||||
AddLayer(layers, overlayId);
|
AddLayer(layers, overlayId);
|
||||||
|
|
|
||||||
|
|
@ -51,14 +51,14 @@ public sealed class ToolbarController
|
||||||
private readonly UiElement?[] _combatIndicators = new UiElement?[CombatIndicatorIds.Length];
|
private readonly UiElement?[] _combatIndicators = new UiElement?[CombatIndicatorIds.Length];
|
||||||
private readonly ItemRepository _repo;
|
private readonly ItemRepository _repo;
|
||||||
private readonly Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> _shortcuts;
|
private readonly Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> _shortcuts;
|
||||||
private readonly Func<uint, uint, uint, uint> _iconIds; // (iconId, underlayId, overlayId) → GL tex
|
private readonly Func<ItemType, uint, uint, uint, uint> _iconIds; // (itemType, iconId, underlayId, overlayId) → GL tex
|
||||||
private readonly Action<uint> _useItem; // guid → fire UseObject
|
private readonly Action<uint> _useItem; // guid → fire UseObject
|
||||||
|
|
||||||
private ToolbarController(
|
private ToolbarController(
|
||||||
ImportedLayout layout,
|
ImportedLayout layout,
|
||||||
ItemRepository repo,
|
ItemRepository repo,
|
||||||
Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> shortcuts,
|
Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> shortcuts,
|
||||||
Func<uint, uint, uint, uint> iconIds,
|
Func<ItemType, uint, uint, uint, uint> iconIds,
|
||||||
Action<uint> useItem,
|
Action<uint> useItem,
|
||||||
CombatState? combatState)
|
CombatState? combatState)
|
||||||
{
|
{
|
||||||
|
|
@ -105,7 +105,7 @@ public sealed class ToolbarController
|
||||||
/// <param name="layout">Imported toolbar layout (LayoutDesc 0x21000016).</param>
|
/// <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="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="shortcuts">Provider for the current shortcut bar list.</param>
|
||||||
/// <param name="iconIds">Resolves (iconId, underlayId, overlayId) → GL texture handle.</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="useItem">Callback fired when a bound slot is clicked; receives the item guid.</param>
|
||||||
/// <param name="combatState">
|
/// <param name="combatState">
|
||||||
/// Optional live combat state — when provided, the toolbar subscribes to
|
/// Optional live combat state — when provided, the toolbar subscribes to
|
||||||
|
|
@ -117,7 +117,7 @@ public sealed class ToolbarController
|
||||||
ImportedLayout layout,
|
ImportedLayout layout,
|
||||||
ItemRepository repo,
|
ItemRepository repo,
|
||||||
Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> shortcuts,
|
Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> shortcuts,
|
||||||
Func<uint, uint, uint, uint> iconIds,
|
Func<ItemType, uint, uint, uint, uint> iconIds,
|
||||||
Action<uint> useItem,
|
Action<uint> useItem,
|
||||||
CombatState? combatState = null)
|
CombatState? combatState = null)
|
||||||
{
|
{
|
||||||
|
|
@ -148,7 +148,7 @@ public sealed class ToolbarController
|
||||||
var item = _repo.GetItem(sc.ObjectGuid);
|
var item = _repo.GetItem(sc.ObjectGuid);
|
||||||
if (item is null) continue; // deferred: ItemAdded will re-call Populate
|
if (item is null) continue; // deferred: ItemAdded will re-call Populate
|
||||||
|
|
||||||
uint tex = _iconIds(item.IconId, item.IconUnderlayId, item.IconOverlayId);
|
uint tex = _iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId);
|
||||||
list.Cell.SetItem(sc.ObjectGuid, tex);
|
list.Cell.SetItem(sc.ObjectGuid, tex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
using AcDream.App.UI;
|
using AcDream.App.UI;
|
||||||
|
using AcDream.Core.Items;
|
||||||
|
using DatReaderWriter;
|
||||||
|
using DatReaderWriter.Options;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.UI;
|
namespace AcDream.App.Tests.UI;
|
||||||
|
|
||||||
|
|
@ -33,4 +38,67 @@ public class IconComposerTests
|
||||||
Assert.Equal(255, rgba[0]); // bottom red preserved
|
Assert.Equal(255, rgba[0]); // bottom red preserved
|
||||||
Assert.Equal(0, rgba[2]);
|
Assert.Equal(0, rgba[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dat-free: when an opaque type-default underlay is prepended as layer 0,
|
||||||
|
/// Compose yields a fully-opaque result even when the base icon is semi-transparent.
|
||||||
|
/// This validates the bottom-up ordering that makes filled toolbar slots non-transparent
|
||||||
|
/// (retail IconData::RenderIcons 407524: underlay is OPAQUE Blit_Normal first).
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void Compose_opaqueUnderlayFirst_resultIsFullyOpaque()
|
||||||
|
{
|
||||||
|
var underlay = (Solid(2, 2, 128, 64, 32, 255), 2, 2); // opaque tawny
|
||||||
|
var baseIcon = (Solid(2, 2, 0, 0, 0, 128), 2, 2); // semi-transparent black
|
||||||
|
var (rgba, w, h) = IconComposer.Compose(new[] { underlay, baseIcon });
|
||||||
|
Assert.Equal(2, w); Assert.Equal(2, h);
|
||||||
|
// All pixels fully opaque: underlay A=255, baseIcon blends over it.
|
||||||
|
for (int i = 0; i < w * h; i++)
|
||||||
|
Assert.Equal(255, rgba[i * 4 + 3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dat-gated golden tests ────────────────────────────────────────────────
|
||||||
|
// These tests open the real Asheron's Call dats (ACDREAM_DAT_DIR or the default
|
||||||
|
// Documents path) and verify the EnumIDMap 0x10000004 resolve chain against the
|
||||||
|
// known golden DIDs from the dat (confirmed 2026-06-17 research).
|
||||||
|
// Golden values: IconData::RenderIcons 0058d214 + DBCache::GetDIDFromEnum 0x413940.
|
||||||
|
|
||||||
|
private static string? ResolveDatDir()
|
||||||
|
{
|
||||||
|
var fromEnv = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
|
||||||
|
if (!string.IsNullOrWhiteSpace(fromEnv) && Directory.Exists(fromEnv))
|
||||||
|
return fromEnv;
|
||||||
|
var def = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
|
"Documents", "Asheron's Call");
|
||||||
|
return Directory.Exists(def) ? def : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveUnderlayDid_goldenValues_matchDat()
|
||||||
|
{
|
||||||
|
var datDir = ResolveDatDir();
|
||||||
|
if (datDir is null)
|
||||||
|
return; // dats absent (CI) — skip cleanly
|
||||||
|
|
||||||
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||||
|
// TextureCache is not needed for the resolve path; pass a null-safe stub
|
||||||
|
// via IconComposer — the underlay-resolve methods only touch _dats.
|
||||||
|
// We cannot construct TextureCache without GL, so use a bare IconComposer
|
||||||
|
// with a null cache guard: ResolveUnderlayDid is internal and pure-dat.
|
||||||
|
var composer = new IconComposer(dats, null!);
|
||||||
|
|
||||||
|
// Golden values confirmed against C:/Users/erikn/Documents/Asheron's Call
|
||||||
|
// (IconData::RenderIcons decomp 407524; DBCache::GetDIDFromEnum 0x413940):
|
||||||
|
// MeleeWeapon (0x1) → index 1 → 0x060011CB
|
||||||
|
// Armor (0x2) → index 2 → 0x060011CF
|
||||||
|
// Clothing (0x4) → index 3 → 0x060011F3
|
||||||
|
// Jewelry (0x8) → index 4 → 0x060011D5
|
||||||
|
// None (0x0) → index 0x21 (fallback) → 0x060011D4
|
||||||
|
Assert.Equal(0x060011CBu, composer.ResolveUnderlayDid(ItemType.MeleeWeapon));
|
||||||
|
Assert.Equal(0x060011CFu, composer.ResolveUnderlayDid(ItemType.Armor));
|
||||||
|
Assert.Equal(0x060011F3u, composer.ResolveUnderlayDid(ItemType.Clothing));
|
||||||
|
Assert.Equal(0x060011D5u, composer.ResolveUnderlayDid(ItemType.Jewelry));
|
||||||
|
Assert.Equal(0x060011D4u, composer.ResolveUnderlayDid(ItemType.None));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ public class ToolbarControllerTests
|
||||||
{ new(Index: 0, ObjectGuid: 0x5001u, SpellId: 0, Layer: 0) };
|
{ new(Index: 0, ObjectGuid: 0x5001u, SpellId: 0, Layer: 0) };
|
||||||
|
|
||||||
ToolbarController.Bind(layout, repo, () => shortcuts,
|
ToolbarController.Bind(layout, repo, () => shortcuts,
|
||||||
iconIds: (_,_,_) => 0x77u, useItem: _ => { });
|
iconIds: (_,_,_,_) => 0x77u, useItem: _ => { });
|
||||||
|
|
||||||
Assert.Equal(0x5001u, slots[Row1[0]].Cell.ItemId);
|
Assert.Equal(0x5001u, slots[Row1[0]].Cell.ItemId);
|
||||||
Assert.Equal(0x77u, slots[Row1[0]].Cell.IconTexture);
|
Assert.Equal(0x77u, slots[Row1[0]].Cell.IconTexture);
|
||||||
|
|
@ -69,7 +69,7 @@ public class ToolbarControllerTests
|
||||||
{ new(Index: 2, ObjectGuid: 0x5002u, SpellId: 0, Layer: 0) };
|
{ new(Index: 2, ObjectGuid: 0x5002u, SpellId: 0, Layer: 0) };
|
||||||
|
|
||||||
ToolbarController.Bind(layout, repo, () => shortcuts,
|
ToolbarController.Bind(layout, repo, () => shortcuts,
|
||||||
iconIds: (_,_,_) => 0x88u, useItem: _ => { });
|
iconIds: (_,_,_,_) => 0x88u, useItem: _ => { });
|
||||||
Assert.Equal(0u, slots[Row1[2]].Cell.ItemId); // not bound yet
|
Assert.Equal(0u, slots[Row1[2]].Cell.ItemId); // not bound yet
|
||||||
|
|
||||||
repo.AddOrUpdate(new ItemInstance { ObjectId = 0x5002u, WeenieClassId = 1u, IconId = 0x06005678u });
|
repo.AddOrUpdate(new ItemInstance { ObjectId = 0x5002u, WeenieClassId = 1u, IconId = 0x06005678u });
|
||||||
|
|
@ -88,7 +88,7 @@ public class ToolbarControllerTests
|
||||||
uint used = 0;
|
uint used = 0;
|
||||||
|
|
||||||
ToolbarController.Bind(layout, repo, () => shortcuts,
|
ToolbarController.Bind(layout, repo, () => shortcuts,
|
||||||
iconIds: (_,_,_) => 0x77u, useItem: g => used = g);
|
iconIds: (_,_,_,_) => 0x77u, useItem: g => used = g);
|
||||||
// UiEvent is a positional record struct: (SourceId, Target, Type, Data0..3, Payload)
|
// UiEvent is a positional record struct: (SourceId, Target, Type, Data0..3, Payload)
|
||||||
slots[Row1[0]].Cell.OnEvent(new UiEvent(0u, null, UiEventType.MouseDown));
|
slots[Row1[0]].Cell.OnEvent(new UiEvent(0u, null, UiEventType.MouseDown));
|
||||||
|
|
||||||
|
|
@ -110,7 +110,7 @@ public class ToolbarControllerTests
|
||||||
|
|
||||||
ToolbarController.Bind(layout, repo,
|
ToolbarController.Bind(layout, repo,
|
||||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||||
iconIds: (_,_,_) => 0u, useItem: _ => { });
|
iconIds: (_,_,_,_) =>0u, useItem: _ => { });
|
||||||
|
|
||||||
// Only peace indicator (index 0 = 0x10000192) is visible.
|
// Only peace indicator (index 0 = 0x10000192) is visible.
|
||||||
Assert.True (indicators[0x10000192u].Visible, "peace indicator should be visible after bind");
|
Assert.True (indicators[0x10000192u].Visible, "peace indicator should be visible after bind");
|
||||||
|
|
@ -130,7 +130,7 @@ public class ToolbarControllerTests
|
||||||
|
|
||||||
var ctrl = ToolbarController.Bind(layout, repo,
|
var ctrl = ToolbarController.Bind(layout, repo,
|
||||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||||
iconIds: (_,_,_) => 0u, useItem: _ => { });
|
iconIds: (_,_,_,_) =>0u, useItem: _ => { });
|
||||||
|
|
||||||
ctrl.SetCombatMode(CombatMode.Melee);
|
ctrl.SetCombatMode(CombatMode.Melee);
|
||||||
|
|
||||||
|
|
@ -152,7 +152,7 @@ public class ToolbarControllerTests
|
||||||
|
|
||||||
ToolbarController.Bind(layout, repo,
|
ToolbarController.Bind(layout, repo,
|
||||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||||
iconIds: (_,_,_) => 0u, useItem: _ => { },
|
iconIds: (_,_,_,_) =>0u, useItem: _ => { },
|
||||||
combatState: combat);
|
combatState: combat);
|
||||||
|
|
||||||
// Initially NonCombat after bind.
|
// Initially NonCombat after bind.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue