feat(D.5.2): IconComposer 2-stage effect composite + 5-arg GetIcon
Widen the cache key to (typeUnderlay, icon, underlay, overlay, effects). GetIcon is now a 2-stage composite mirroring retail IconData::RenderIcons (0x0058d180): Stage 1 builds the drag composite (base + overlay) and, when effects != 0, ReplaceColorWhite tints it with the effect tile's mean-opaque color (DR-1: tint SOURCE, not blit; DR-3: zero-effects black path skipped). Stage 2 blits typeUnderlay + custom underlay + drag into the final cached GL texture. Both callers updated: ToolbarController Func arity widened to 6-arg (passes item.Effects); GameWindow closure and OnLiveEntitySpawned EnrichItem call pass spawn.UiEffects. Tree builds with 0 warnings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3e019e408a
commit
e0dce5aa9f
4 changed files with 49 additions and 20 deletions
|
|
@ -2002,7 +2002,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: (type, icon, under, over) => iconComposer.GetIcon(type, icon, under, over),
|
iconIds: (type, icon, under, over, effects) => iconComposer.GetIcon(type, icon, under, over, effects),
|
||||||
useItem: guid => UseItemByGuid(guid),
|
useItem: guid => UseItemByGuid(guid),
|
||||||
combatState: Combat,
|
combatState: Combat,
|
||||||
peaceDigits: toolbarPeaceDigits,
|
peaceDigits: toolbarPeaceDigits,
|
||||||
|
|
@ -2646,7 +2646,7 @@ public sealed class GameWindow : IDisposable
|
||||||
// WeenieHeader tail so IconComposer composites all icon layers.
|
// WeenieHeader tail so IconComposer composites all icon layers.
|
||||||
Items.EnrichItem(spawn.Guid, spawn.IconId, spawn.Name ?? string.Empty,
|
Items.EnrichItem(spawn.Guid, spawn.IconId, spawn.Name ?? string.Empty,
|
||||||
(AcDream.Core.Items.ItemType)(spawn.ItemType ?? 0),
|
(AcDream.Core.Items.ItemType)(spawn.ItemType ?? 0),
|
||||||
spawn.IconOverlayId, spawn.IconUnderlayId);
|
spawn.IconOverlayId, spawn.IconUnderlayId, spawn.UiEffects);
|
||||||
|
|
||||||
// Phase A.1 hotfix: live CreateObject handler reads dats extensively
|
// Phase A.1 hotfix: live CreateObject handler reads dats extensively
|
||||||
// (Setup, GfxObj, Surface, SurfaceTexture) to hydrate the spawned
|
// (Setup, GfxObj, Surface, SurfaceTexture) to hydrate the spawned
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ 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), uint> _byTuple = new();
|
private readonly Dictionary<(uint, uint, uint, uint, uint), uint> _byTuple = new();
|
||||||
|
|
||||||
// ── type-default underlay resolve (EnumIDMap 0x10000004) ─────────────────
|
// ── type-default underlay resolve (EnumIDMap 0x10000004) ─────────────────
|
||||||
// Portal MasterMap (0x25000000) maps enum 0x10000004 → submap DID (0x25000008).
|
// Portal MasterMap (0x25000000) maps enum 0x10000004 → submap DID (0x25000008).
|
||||||
|
|
@ -210,26 +210,42 @@ public sealed class IconComposer
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolve (and cache) the composited GL texture for an item's icon layers.
|
/// Resolve (and cache) the composited GL texture for an item's icon state.
|
||||||
/// Returns 0 if no base icon is available.
|
/// Returns 0 if no base icon. Mirrors retail IconData::RenderIcons (0x0058d180):
|
||||||
///
|
/// a DRAG composite (base + custom overlay + effect recolor) blitted over the
|
||||||
/// <para>Layer order mirrors retail IconData::RenderIcons (decomp 407524):
|
/// type-default underlay + custom underlay. The effect tile (enum 0x10000005) is a
|
||||||
/// type-default underlay (OPAQUE) → custom underlay → base icon → custom overlay.
|
/// ReplaceColor tint SOURCE, not a blit layer (DR-1). The 2-stage form is
|
||||||
/// The type-default underlay is resolved via the EnumIDMap 0x10000004 chain;
|
/// associative-equivalent to a single Compose when effects==0, so D.5.1 visuals are
|
||||||
/// its presence ensures filled slots are never transparent.</para>
|
/// unchanged for non-effect items.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public uint GetIcon(ItemType itemType, uint iconId, uint underlayId, uint overlayId)
|
public uint GetIcon(ItemType itemType, uint iconId, uint underlayId, uint overlayId, uint effects)
|
||||||
{
|
{
|
||||||
if (iconId == 0) return 0;
|
if (iconId == 0) return 0;
|
||||||
uint typeUnderlayDid = ResolveUnderlayDid(itemType);
|
uint typeUnderlayDid = ResolveUnderlayDid(itemType);
|
||||||
var key = (typeUnderlayDid, iconId, underlayId, overlayId);
|
var key = (typeUnderlayDid, iconId, underlayId, overlayId, effects);
|
||||||
if (_byTuple.TryGetValue(key, out var tex)) return tex;
|
if (_byTuple.TryGetValue(key, out var tex)) return tex;
|
||||||
|
|
||||||
|
// Stage 1 — retail m_pDragIcon: base + custom overlay, then the effect recolor.
|
||||||
|
var dragLayers = new List<(byte[] rgba, int w, int h)>();
|
||||||
|
AddLayer(dragLayers, iconId);
|
||||||
|
AddLayer(dragLayers, overlayId);
|
||||||
|
(byte[] rgba, int w, int h)? drag = null;
|
||||||
|
if (dragLayers.Count > 0)
|
||||||
|
{
|
||||||
|
var composed = Compose(dragLayers);
|
||||||
|
// Effect recolor only when an effect bit is set. Retail nominally also runs the
|
||||||
|
// effects==0 black-fallback recolor; we skip it (DR-3: white→black on every item
|
||||||
|
// is a likely no-op but a regression risk, pending visual/cdb confirmation).
|
||||||
|
if (effects != 0 && TryGetEffectColor(effects, out var ec))
|
||||||
|
ReplaceColorWhite(composed.rgba, composed.w, composed.h, ec);
|
||||||
|
drag = composed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stage 2 — retail m_pIcon: type-default underlay (opaque) + custom underlay + drag.
|
||||||
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, typeUnderlayDid);
|
||||||
AddLayer(layers, underlayId);
|
AddLayer(layers, underlayId);
|
||||||
AddLayer(layers, iconId);
|
if (drag is { } d) layers.Add(d);
|
||||||
AddLayer(layers, overlayId);
|
|
||||||
if (layers.Count == 0) return 0;
|
if (layers.Count == 0) return 0;
|
||||||
|
|
||||||
var (rgba, w, h) = Compose(layers);
|
var (rgba, w, h) = Compose(layers);
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ 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<ItemType, uint, uint, uint, uint> _iconIds; // (itemType, iconId, underlayId, overlayId) → GL tex
|
private readonly Func<ItemType, uint, uint, uint, uint, uint> _iconIds; // (itemType, icon, underlay, overlay, effects) → GL tex
|
||||||
private readonly Action<uint> _useItem; // guid → fire UseObject
|
private readonly Action<uint> _useItem; // guid → fire UseObject
|
||||||
|
|
||||||
// Digit sprite DID arrays for slot labels (top row, numbers 1-9).
|
// Digit sprite DID arrays for slot labels (top row, numbers 1-9).
|
||||||
|
|
@ -70,7 +70,7 @@ public sealed class ToolbarController
|
||||||
ImportedLayout layout,
|
ImportedLayout layout,
|
||||||
ItemRepository repo,
|
ItemRepository repo,
|
||||||
Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> shortcuts,
|
Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> shortcuts,
|
||||||
Func<ItemType, uint, uint, uint, uint> iconIds,
|
Func<ItemType, uint, uint, uint, uint, uint> iconIds,
|
||||||
Action<uint> useItem,
|
Action<uint> useItem,
|
||||||
CombatState? combatState,
|
CombatState? combatState,
|
||||||
uint[]? peaceDigits,
|
uint[]? peaceDigits,
|
||||||
|
|
@ -123,7 +123,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 (itemType, iconId, underlayId, overlayId) → GL texture handle.</param>
|
/// <param name="iconIds">Resolves (itemType, iconId, underlayId, overlayId, effects) → 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
|
||||||
|
|
@ -148,7 +148,7 @@ public sealed class ToolbarController
|
||||||
ImportedLayout layout,
|
ImportedLayout layout,
|
||||||
ItemRepository repo,
|
ItemRepository repo,
|
||||||
Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> shortcuts,
|
Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> shortcuts,
|
||||||
Func<ItemType, uint, uint, uint, uint> iconIds,
|
Func<ItemType, uint, uint, uint, uint, uint> iconIds,
|
||||||
Action<uint> useItem,
|
Action<uint> useItem,
|
||||||
CombatState? combatState = null,
|
CombatState? combatState = null,
|
||||||
uint[]? peaceDigits = null,
|
uint[]? peaceDigits = null,
|
||||||
|
|
@ -183,7 +183,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.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId);
|
uint tex = _iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects);
|
||||||
list.Cell.SetItem(sc.ObjectGuid, tex);
|
list.Cell.SetItem(sc.ObjectGuid, tex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -144,4 +144,17 @@ public class IconComposerTests
|
||||||
Assert.Equal(new byte[] { 255, 255, 255, 0 }, px[8..12]); // untouched
|
Assert.Equal(new byte[] { 255, 255, 255, 0 }, px[8..12]); // untouched
|
||||||
Assert.Equal(new byte[] { 10, 20, 30, 255 }, px[12..16]); // replaced
|
Assert.Equal(new byte[] { 10, 20, 30, 255 }, px[12..16]); // replaced
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TwoStageWithEffect_recolorsWhiteBeforeUnderlay()
|
||||||
|
{
|
||||||
|
// drag = base (white pixel) over overlay (none); recolor white→blue; then over
|
||||||
|
// an opaque tawny underlay. The white pixel must become blue in the final.
|
||||||
|
var baseIcon = (new byte[] { 255,255,255,255 }, 1, 1); // 1x1 white opaque
|
||||||
|
var drag = IconComposer.Compose(new[] { baseIcon });
|
||||||
|
IconComposer.ReplaceColorWhite(drag.rgba, drag.w, drag.h, (0, 0, 255, 255)); // blue
|
||||||
|
var underlay = (new byte[] { 105, 70, 50, 255 }, 1, 1); // tawny opaque
|
||||||
|
var final = IconComposer.Compose(new[] { underlay, (drag.rgba, drag.w, drag.h) });
|
||||||
|
Assert.Equal(new byte[] { 0, 0, 255, 255 }, final.rgba); // blue on top
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue