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:
Erik 2026-06-17 18:40:37 +02:00
parent 3e019e408a
commit e0dce5aa9f
4 changed files with 49 additions and 20 deletions

View file

@ -33,7 +33,7 @@ public sealed class IconComposer
{
private readonly DatCollection _dats;
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) ─────────────────
// Portal MasterMap (0x25000000) maps enum 0x10000004 → submap DID (0x25000008).
@ -210,26 +210,42 @@ public sealed class IconComposer
}
/// <summary>
/// Resolve (and cache) the composited GL texture for an item's icon layers.
/// 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>
/// Resolve (and cache) the composited GL texture for an item's icon state.
/// Returns 0 if no base icon. Mirrors retail IconData::RenderIcons (0x0058d180):
/// a DRAG composite (base + custom overlay + effect recolor) blitted over the
/// type-default underlay + custom underlay. The effect tile (enum 0x10000005) is a
/// ReplaceColor tint SOURCE, not a blit layer (DR-1). The 2-stage form is
/// associative-equivalent to a single Compose when effects==0, so D.5.1 visuals are
/// unchanged for non-effect items.
/// </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;
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;
// 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)>();
AddLayer(layers, typeUnderlayDid); // OPAQUE bottom; sizes the 32x32 output
AddLayer(layers, typeUnderlayDid);
AddLayer(layers, underlayId);
AddLayer(layers, iconId);
AddLayer(layers, overlayId);
if (drag is { } d) layers.Add(d);
if (layers.Count == 0) return 0;
var (rgba, w, h) = Compose(layers);