fix(ui): port retail item drag visuals
Keep retail's underlay-free m_pDragIcon separate from the full cell icon and reveal the authored ghost mesh on physical source cells for the complete drag lifecycle. This removes the backpack backing from the cursor and retires AP-47. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
3e84027885
commit
ace5880fed
14 changed files with 307 additions and 44 deletions
|
|
@ -34,6 +34,9 @@ public sealed class IconComposer
|
|||
private readonly DatCollection _dats;
|
||||
private readonly TextureCache _cache;
|
||||
private readonly Dictionary<(uint, uint, uint, uint, uint), uint> _byTuple = new();
|
||||
private readonly Dictionary<(uint, uint, uint), ComposedIcon> _dragByTuple = new();
|
||||
|
||||
private sealed record ComposedIcon(byte[] Rgba, int Width, int Height, uint Texture);
|
||||
|
||||
// ── type-default underlay resolve (EnumIDMap 0x10000004) ─────────────────
|
||||
// Portal MasterMap (0x25000000) maps enum 0x10000004 → submap DID (0x25000008).
|
||||
|
|
@ -225,32 +228,15 @@ public sealed class IconComposer
|
|||
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 — ALWAYS, matching retail IconData::RenderIcons (0x0058d180):
|
||||
// the effect tile (enum 0x10000005, lsb(effects)+1, fallback 0x21) is non-null
|
||||
// even for effects==0 (the 0x21 SOLID-BLACK tile 0x060011C5). Retail's RenderIcons
|
||||
// calls the SURFACE overload of SurfaceWindow::ReplaceColor (0x004415b0), copying
|
||||
// the textured effect tile per-pixel into the icon's pure-white pixels — so
|
||||
// magical items take the tile's GRADIENT hue and mundane items go solid black.
|
||||
// (Visually confirmed against retail 2026-06-17: the Energy Crystal's blue is a
|
||||
// gradient, not a flat tint, and the no-mana scroll's edges are black.)
|
||||
if (TryGetEffectTile(effects, out var tile))
|
||||
ReplaceWhiteFromSurface(composed.rgba, composed.w, composed.h,
|
||||
tile.Rgba8, tile.Width, tile.Height);
|
||||
drag = composed;
|
||||
}
|
||||
// RenderIcons retains this as a distinct Graphic because the cursor ghost must not
|
||||
// carry the type/custom underlay that fills an inventory cell.
|
||||
ComposedIcon? drag = GetOrCreateDragIcon(iconId, overlayId, effects);
|
||||
|
||||
// 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);
|
||||
AddLayer(layers, underlayId);
|
||||
if (drag is { } d) layers.Add(d);
|
||||
if (drag is not null) layers.Add((drag.Rgba, drag.Width, drag.Height));
|
||||
if (layers.Count == 0) return 0;
|
||||
|
||||
var (rgba, w, h) = Compose(layers);
|
||||
|
|
@ -259,6 +245,50 @@ public sealed class IconComposer
|
|||
return handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve retail's dedicated cursor-drag graphic (<c>IconData::m_pDragIcon</c>): base icon
|
||||
/// + custom overlay + effect recolor, with neither the type-default nor custom underlay.
|
||||
/// <c>UIElement_ItemList::PrepareDragIcon</c> obtains exactly this graphic through
|
||||
/// <c>ACCWeenieObject::GetDragIcon</c> (0x004e2a50 / 0x0058d180).
|
||||
/// </summary>
|
||||
public uint GetDragIcon(ItemType itemType, uint iconId, uint underlayId, uint overlayId, uint effects)
|
||||
{
|
||||
// itemType/underlayId are deliberately unused: keeping the resolver signature identical
|
||||
// to GetIcon lets every item-panel binding request the two retail siblings from one model.
|
||||
_ = itemType;
|
||||
_ = underlayId;
|
||||
return iconId == 0 ? 0u : GetOrCreateDragIcon(iconId, overlayId, effects)?.Texture ?? 0u;
|
||||
}
|
||||
|
||||
private ComposedIcon? GetOrCreateDragIcon(uint iconId, uint overlayId, uint effects)
|
||||
{
|
||||
var key = (iconId, overlayId, effects);
|
||||
if (_dragByTuple.TryGetValue(key, out var cached)) return cached;
|
||||
|
||||
var dragLayers = new List<(byte[] rgba, int w, int h)>();
|
||||
AddLayer(dragLayers, iconId);
|
||||
AddLayer(dragLayers, overlayId);
|
||||
if (dragLayers.Count == 0) return null;
|
||||
|
||||
var composed = Compose(dragLayers);
|
||||
// Effect recolor — ALWAYS, matching retail IconData::RenderIcons (0x0058d180):
|
||||
// the effect tile (enum 0x10000005, lsb(effects)+1, fallback 0x21) is non-null
|
||||
// even for effects==0 (the 0x21 SOLID-BLACK tile 0x060011C5). Retail's RenderIcons
|
||||
// calls the SURFACE overload of SurfaceWindow::ReplaceColor (0x004415b0), copying
|
||||
// the textured effect tile per-pixel into the icon's pure-white pixels — so
|
||||
// magical items take the tile's GRADIENT hue and mundane items go solid black.
|
||||
// (Visually confirmed against retail 2026-06-17: the Energy Crystal's blue is a
|
||||
// gradient, not a flat tint, and the no-mana scroll's edges are black.)
|
||||
if (TryGetEffectTile(effects, out var tile))
|
||||
ReplaceWhiteFromSurface(composed.rgba, composed.w, composed.h,
|
||||
tile.Rgba8, tile.Width, tile.Height);
|
||||
|
||||
uint texture = _cache.UploadRgba8(composed.rgba, composed.w, composed.h, nearest: true);
|
||||
var created = new ComposedIcon(composed.rgba, composed.w, composed.h, texture);
|
||||
_dragByTuple[key] = created;
|
||||
return created;
|
||||
}
|
||||
|
||||
private void AddLayer(List<(byte[], int, int)> layers, uint renderSurfaceId)
|
||||
{
|
||||
if (renderSurfaceId == 0) return;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue