Port the retail m_bShortcutGhosted meaning instead of treating SetShortcutNum's boolean as peace versus war. Keep occupied shortcut numbers on the gold sheet in NonCombat, Melee, and Missile, reserve the gray sheet for Magic, and lock the four-mode matrix with conformance tests and corrected research. Co-Authored-By: Codex <codex@openai.com>
384 lines
19 KiB
C#
384 lines
19 KiB
C#
using System;
|
||
using System.Numerics;
|
||
using AcDream.Core.Items;
|
||
|
||
namespace AcDream.App.UI;
|
||
|
||
/// <summary>
|
||
/// One item-in-a-slot cell (port of retail UIElement_UIItem, class 0x10000032).
|
||
/// A behavioral LEAF: it draws the empty-slot sprite when unbound, else a
|
||
/// pre-composited icon texture (set by the controller). Holds the bound weenie
|
||
/// guid (retail UIElement_UIItem::itemID, +0x5FC).
|
||
/// </summary>
|
||
public sealed class UiItemSlot : UiElement
|
||
{
|
||
public UiItemSlot() { ClickThrough = false; }
|
||
|
||
public override bool ConsumesDatChildren => true;
|
||
|
||
/// <summary>Bound weenie guid (0 = empty). Retail UIElement_UIItem::itemID.</summary>
|
||
public uint ItemId { get; private set; }
|
||
|
||
/// <summary>Pre-composited icon GL texture for the bound item (0 = none).</summary>
|
||
public uint IconTexture { get; private set; }
|
||
|
||
/// <summary>
|
||
/// Underlay-free cursor graphic for the bound item (retail <c>IconData::m_pDragIcon</c>).
|
||
/// Falls back to <see cref="IconTexture"/> only for callers that have not supplied the
|
||
/// dedicated composite.
|
||
/// </summary>
|
||
public uint DragIconTexture { get; private set; }
|
||
|
||
/// <summary>
|
||
/// Lossless shortcut record when this cell belongs to the toolbar. Physical item
|
||
/// lists leave it null. Snapshotted into the drag payload before remove-on-lift.
|
||
/// </summary>
|
||
public ShortcutEntry? Shortcut { get; private set; }
|
||
|
||
/// <summary>This cell's own index within its panel (0..17 toolbar; container slot
|
||
/// for inventory). Distinct from <see cref="ShortcutNum"/> (the 1–9 label, -1 on the
|
||
/// bottom row). Set by the controller; used as the drag payload's SourceSlot and to
|
||
/// identify the drop TARGET slot.</summary>
|
||
public int SlotIndex { get; set; } = -1;
|
||
|
||
/// <summary>What kind of slot this is, for the drag payload (retail InqDropIconInfo
|
||
/// flags). Controller overrides; default Inventory.</summary>
|
||
public ItemDragSource SourceKind { get; set; } = ItemDragSource.Inventory;
|
||
|
||
/// <summary>Drag-rollover accept frame (retail ItemSlot_DragOver_Accept 0x060011F9,
|
||
/// state id 0x10000041). Configurable; guard id != 0 before resolving.</summary>
|
||
public uint DragAcceptSprite { get; set; } = 0x060011F9u;
|
||
/// <summary>Drag-rollover reject frame (retail ItemSlot_DragOver_Reject 0x060011F8,
|
||
/// state id 0x10000040).</summary>
|
||
public uint DragRejectSprite { get; set; } = 0x060011F8u;
|
||
|
||
/// <summary>True when this cell is the OPEN container (its contents fill the grid). Draws the
|
||
/// open-container triangle. Port of UIElement_ItemList::UpdateOpenContainerIndicator
|
||
/// (0x004e3070) → SetOpenContainerState (0x004e1200): shown when item.itemID == openContainerId.</summary>
|
||
public bool IsOpenContainer { get; set; }
|
||
/// <summary>Open-container triangle sprite (element 0x10000450 on the container prototype
|
||
/// 0x1000033F). Configurable; guard id != 0 before resolving.</summary>
|
||
public uint OpenContainerSprite { get; set; } = 0x06005D9Cu;
|
||
|
||
/// <summary>True when this cell is the SELECTED item. Draws the green/yellow selection square.
|
||
/// Port of UIElement_ItemList::ItemList_SetSelectedItem (0x004e2fe0) → SetSelectedState
|
||
/// (0x004e1240): shown when item.itemID == selectedItemId. Uniform across item + container cells.</summary>
|
||
public bool Selected { get; set; }
|
||
/// <summary>Selected-item square sprite (element 0x10000342 on the 32×32 item prototype
|
||
/// 0x10000341; pixel-confirmed green/yellow frame). Drawn as a procedural overlay so it renders
|
||
/// on the 36×36 container cell too (whose prototype lacks the square child).</summary>
|
||
public uint SelectedSprite { get; set; } = 0x06004D21u;
|
||
|
||
/// <summary>Container fullness [0..1], or -1 = hidden (the cell is not a container, or its
|
||
/// itemsCapacity is unknown/0). Port of UIElement_UIItem::UpdateCapacityDisplay (0x004e16e0): a
|
||
/// per-cell vertical UIElement_Meter (element 0x10000347, 5×30 at x=26,y=1) shown only when
|
||
/// isContainer && itemsCapacity > 0, fill = numContainedItems / itemsCapacity (meter attr 0x69).</summary>
|
||
public float CapacityFill { get; set; } = -1f;
|
||
/// <summary>Capacity-bar track sprite (meter 0x10000347 DirectState).</summary>
|
||
public uint CapacityBackSprite { get; set; } = 0x06004D22u;
|
||
/// <summary>Capacity-bar fill sprite (meter 0x10000347 front child 0x00000002 DirectState).</summary>
|
||
public uint CapacityFrontSprite { get; set; } = 0x06004D23u;
|
||
|
||
/// <summary>Accept/reject overlay state while a drag hovers this cell.</summary>
|
||
public enum DragAcceptState { None, Accept, Reject }
|
||
private DragAcceptState _dragAccept = DragAcceptState.None;
|
||
/// <summary>Current overlay state — internal so unit tests can assert it (InternalsVisibleTo).</summary>
|
||
internal DragAcceptState DragAcceptVisual => _dragAccept;
|
||
|
||
/// <summary>Empty-slot sprite. Default = the generic toolbar empty-slot border
|
||
/// 0x060074CF (uiitem template 0x21000037, state ItemSlot_Empty). Configurable so
|
||
/// paperdoll equip slots can use their per-slot silhouettes later.</summary>
|
||
public uint EmptySprite { get; set; } = 0x060074CFu;
|
||
|
||
/// <summary>
|
||
/// Authored grey mesh shown while a physical item is being carried by drag-and-drop.
|
||
/// Retail <c>m_elem_Icon_Ghosted</c> (element 0x10000349, DirectState 0x0600109A),
|
||
/// toggled by <c>UIElement_UIItem::SetWaitingState</c> @ 0x004e11b0.
|
||
/// </summary>
|
||
public uint WaitingSprite { get; set; } = 0x0600109Au;
|
||
|
||
private bool _waiting;
|
||
internal bool WaitingVisual => _waiting;
|
||
|
||
/// <summary>RenderSurface id -> (GL texture, w, h). Set by the factory/controller.</summary>
|
||
public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }
|
||
|
||
public void SetItem(
|
||
uint itemId,
|
||
uint iconTexture,
|
||
ShortcutEntry? shortcut = null,
|
||
uint dragIconTexture = 0)
|
||
{
|
||
ItemId = itemId;
|
||
IconTexture = iconTexture;
|
||
DragIconTexture = dragIconTexture;
|
||
Shortcut = shortcut;
|
||
}
|
||
|
||
public void Clear()
|
||
{
|
||
ItemId = 0;
|
||
IconTexture = 0;
|
||
DragIconTexture = 0;
|
||
Shortcut = null;
|
||
_waiting = false;
|
||
}
|
||
|
||
/// <inheritdoc/>
|
||
public override object? GetDragPayload()
|
||
=> ItemId != 0 ? new ItemDragPayload(ItemId, SourceKind, SlotIndex, this, Shortcut) : null;
|
||
|
||
/// <inheritdoc/>
|
||
public override (uint tex, int w, int h)? GetDragGhost()
|
||
{
|
||
if (ItemId == 0) return null;
|
||
// RenderIcons creates m_pDragIcon on a fixed 0x20 × 0x20 surface, even when its
|
||
// containing bag cell is 36 × 36. The cursor hotspot is correspondingly (16,16).
|
||
if (DragIconTexture != 0) return (DragIconTexture, 32, 32);
|
||
return IconTexture != 0 ? (IconTexture, (int)Width, (int)Height) : null;
|
||
}
|
||
|
||
/// <inheritdoc/>
|
||
internal override void SetDragSourceActive(bool active, object? payload)
|
||
{
|
||
// ItemList_BeginDrag ghosts physical lists, but explicitly excludes shortcut lists
|
||
// (along with vendor/salvage lists, which acdream does not model as ItemDragSource).
|
||
// Keep the source's full cell icon in place and reveal the authored grey mesh over it.
|
||
_waiting = active && ItemId != 0 && SourceKind != ItemDragSource.ShortcutBar;
|
||
}
|
||
|
||
/// <summary>An OCCUPIED slot is a drag source — a press-and-move picks up the item
|
||
/// rather than moving the toolbar window. An EMPTY slot is NOT a drag source, so a
|
||
/// press-and-move there falls through to the IA-12 whole-window-drag, keeping the bar
|
||
/// movable by its empty cells / chrome. Drives <see cref="UiRoot"/>'s mousedown
|
||
/// window-vs-item disambiguation (retail moves the window via a dragbar, never cells;
|
||
/// our whole-window-drag approximation reconciles by gating on occupancy).</summary>
|
||
public override bool IsDragSource => ItemId != 0;
|
||
|
||
/// <summary>Walk up to the containing <see cref="UiItemList"/> (the drop handler owner).</summary>
|
||
private UiItemList? FindList()
|
||
{
|
||
UiElement? e = Parent;
|
||
while (e is not null) { if (e is UiItemList l) return l; e = e.Parent; }
|
||
return null;
|
||
}
|
||
|
||
// ── Shortcut number (slot label) ─────────────────────────────────────────
|
||
// Port of UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465).
|
||
// Retail draws the digit on the cell's ShortcutNum sub-element, picking the
|
||
// digit image from a DID-array property: 0x10000042 (regular) / 0x10000043 (ghosted),
|
||
// indexed by slot position. Each digit is a 32×32 PFID_A8R8G8B8 RenderSurface
|
||
// with the digit baked into the top-left corner (rest alpha=0), drawn Alphablend.
|
||
|
||
/// <summary>Slot position in the shortcut bar (0-indexed). -1 = no number (retail
|
||
/// SetVisible(0) when edi < 0). Top row: 0..8 → digits 1..9. Bottom row: -1.</summary>
|
||
public int ShortcutNum { get; private set; } = -1;
|
||
|
||
/// <summary>True when retail marks the physical-item shortcut as ghosted.</summary>
|
||
public bool ShortcutGhosted { get; private set; }
|
||
|
||
/// <summary>Regular digit DID array. Index i → digit (i+1) sprite RenderSurface id.
|
||
/// Injected by the controller after reading LayoutDesc 0x21000037.
|
||
/// Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481) — occupied slot picks
|
||
/// property 0x10000042 when the shortcut is not ghosted.</summary>
|
||
public uint[]? RegularDigits { get; set; }
|
||
|
||
/// <summary>Ghosted digit DID array. Same layout as RegularDigits.
|
||
/// Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229485-229488).</summary>
|
||
public uint[]? GhostedDigits { get; set; }
|
||
|
||
/// <summary>Empty-slot digit DID array (property 0x1000005e, stance-independent).
|
||
/// Used when the slot is EMPTY (ItemId == 0). Retail ref: UIElement_UIItem::SetShortcutNum
|
||
/// (decomp 229481) — else branch when m_elem_Icon->m_state == 0x1000001c (empty).</summary>
|
||
public uint[]? EmptyDigits { get; set; }
|
||
|
||
/// <summary>Set the slot's shortcut position and ghosted state so the correct digit
|
||
/// is drawn. Call with index 0..8 for the top row.</summary>
|
||
public void SetShortcutNum(int index, bool ghosted)
|
||
{
|
||
ShortcutNum = index;
|
||
ShortcutGhosted = ghosted;
|
||
}
|
||
|
||
/// <summary>Clear the shortcut number label (hides the digit).</summary>
|
||
public void ClearShortcutNum() { ShortcutNum = -1; }
|
||
|
||
/// <summary>
|
||
/// Returns the digit DID array that OnDraw will use, following the retail occupancy
|
||
/// branch in UIElement_UIItem::SetShortcutNum (decomp 229481):
|
||
/// occupied (ItemId != 0) → ShortcutGhosted ? GhostedDigits : RegularDigits (0x10000043/42)
|
||
/// empty (ItemId == 0) → EmptyDigits (0x1000005e, stance-independent)
|
||
/// Exposed as an internal method so unit tests can assert array selection without
|
||
/// needing a real render context.
|
||
/// </summary>
|
||
internal uint[]? ActiveDigitArray()
|
||
{
|
||
bool occupied = ItemId != 0;
|
||
return occupied ? (ShortcutGhosted ? GhostedDigits : RegularDigits) : EmptyDigits;
|
||
}
|
||
|
||
// ── Events / draw ─────────────────────────────────────────────────────────
|
||
|
||
/// <summary>Invoked by <see cref="OnEvent"/> when a left-button-down lands on
|
||
/// a bound slot. Wired by <c>ToolbarController</c> to the use-item callback.</summary>
|
||
public Action? Clicked { get; set; }
|
||
|
||
/// <summary>Invoked by <see cref="OnEvent"/> when a double-click lands on a bound slot.</summary>
|
||
public Action? DoubleClicked { get; set; }
|
||
|
||
/// <inheritdoc/>
|
||
public override bool OnEvent(in UiEvent e)
|
||
{
|
||
switch (e.Type)
|
||
{
|
||
// Use fires on CLICK (mouse-up over the same cell), not MouseDown — so a
|
||
// drag (press + >3px move) does NOT also use the item. UiRoot suppresses the
|
||
// post-drag Click (UiRoot.cs FinishDrag returns before the Click emit).
|
||
case UiEventType.MouseDown:
|
||
return true; // consume the press; no use here
|
||
case UiEventType.Click:
|
||
Clicked?.Invoke();
|
||
return true;
|
||
case UiEventType.DoubleClick:
|
||
DoubleClicked?.Invoke();
|
||
return true;
|
||
|
||
case UiEventType.DragBegin:
|
||
// Notify the source list's handler so it can lift (remove + wire) — retail
|
||
// RecvNotice_ItemListBeginDrag → RemoveShortcut. UiRoot snapshotted the ghost first.
|
||
if (FindList() is { DragHandler: { } lh } liftList && e.Payload is ItemDragPayload lp)
|
||
lh.OnDragLift(liftList, this, lp);
|
||
return true;
|
||
|
||
case UiEventType.DragEnter: // pointer entered me mid-drag → ask the list's handler
|
||
_dragAccept = FindList() is { DragHandler: { } h } list
|
||
&& e.Payload is ItemDragPayload p
|
||
? h.OnDragOver(list, this, p) switch
|
||
{
|
||
ItemDragAcceptance.Accept => DragAcceptState.Accept,
|
||
ItemDragAcceptance.Reject => DragAcceptState.Reject,
|
||
_ => DragAcceptState.None,
|
||
}
|
||
: DragAcceptState.Reject;
|
||
return true;
|
||
|
||
case UiEventType.DragOver: // UiRoot fires this on LEAVE → neutral
|
||
_dragAccept = DragAcceptState.None;
|
||
return true;
|
||
|
||
case UiEventType.DropReleased:
|
||
_dragAccept = DragAcceptState.None;
|
||
if (FindList() is { DragHandler: { } dh } dl && e.Payload is ItemDragPayload dp)
|
||
dh.HandleDropRelease(dl, this, dp);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
protected override void OnDraw(UiRenderContext ctx)
|
||
{
|
||
// Draw the icon (filled slot) or the empty-slot border. Both paths fall through
|
||
// to the digit draw below; the slot label always shows on top-row slots.
|
||
if (ItemId != 0 && IconTexture != 0)
|
||
{
|
||
ctx.DrawSprite(IconTexture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||
}
|
||
else if (SpriteResolve is not null && EmptySprite != 0)
|
||
{
|
||
var (tex, _, _) = SpriteResolve(EmptySprite);
|
||
if (tex != 0)
|
||
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||
}
|
||
|
||
// Digit overlay: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465).
|
||
// Occupancy branch (decomp 229481):
|
||
// occupied (ItemId != 0) → regular/ghosted digit set 0x10000042/43
|
||
// empty (ItemId == 0) → background digit set 0x1000005e, stance-independent
|
||
// Each digit image is corner-baked (glyph in top-left, rest alpha=0); drawn
|
||
// full-cell Alphablend so the transparent region is invisible.
|
||
if (ShortcutNum >= 0 && SpriteResolve is not null)
|
||
{
|
||
var arr = ActiveDigitArray();
|
||
if (arr is not null && ShortcutNum < arr.Length)
|
||
{
|
||
uint did = arr[ShortcutNum];
|
||
if (did != 0)
|
||
{
|
||
var (tex, _, _) = SpriteResolve(did);
|
||
if (tex != 0)
|
||
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Container capacity bar — UIElement_UIItem::UpdateCapacityDisplay (0x004e16e0): a vertical
|
||
// UIElement_Meter (element 0x10000347, 5×30 at x=26,y=1) shown only for container cells
|
||
// (CapacityFill >= 0). Track drawn full; fill clipped to the fraction from the BOTTOM (the
|
||
// "how full" direction). Procedural — UiItemSlot is a behavioral leaf. Guard id != 0 first.
|
||
if (CapacityFill >= 0f && SpriteResolve is not null)
|
||
{
|
||
const float by = 1f, bw = 5f, bh = 30f; // element 0x10000347 size (dat 5×30 at y=1)
|
||
float bx = Width - bw; // flush to the cell's right edge (visual gate: the dat X=26 sat ~5px off the edge)
|
||
if (CapacityBackSprite != 0)
|
||
{
|
||
var (bt, _, _) = SpriteResolve(CapacityBackSprite);
|
||
if (bt != 0) ctx.DrawSprite(bt, bx, by, bw, bh, 0f, 0f, 1f, 1f, Vector4.One);
|
||
}
|
||
float f = Math.Clamp(CapacityFill, 0f, 1f);
|
||
if (f > 0f && CapacityFrontSprite != 0)
|
||
{
|
||
var (ft, _, _) = SpriteResolve(CapacityFrontSprite);
|
||
if (ft != 0)
|
||
{
|
||
// Bottom-up fill: draw the bottom f-fraction of the bar, sampling the matching
|
||
// bottom slice of the front sprite (UV v from 1-f to 1).
|
||
float fh = bh * f;
|
||
ctx.DrawSprite(ft, bx, by + (bh - fh), bw, fh, 0f, 1f - f, 1f, 1f, Vector4.One);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Pending/drag-source mesh — retail UIElement_UIItem::SetWaitingState (0x004e11b0)
|
||
// reveals m_elem_Icon_Ghosted (0x10000349). ItemList_BeginDrag (0x004e32d0)
|
||
// sets it for physical inventory/equipment cells while leaving their icon in place.
|
||
// Draw it before the persistent selected/open indicators: retail keeps selection visible
|
||
// while the source is waiting, so the grey mesh must not erase the selected square.
|
||
if (_waiting && SpriteResolve is not null && WaitingSprite != 0)
|
||
{
|
||
var (tex, _, _) = SpriteResolve(WaitingSprite);
|
||
if (tex != 0)
|
||
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||
}
|
||
|
||
// Open-container triangle (retail SetOpenContainerState 0x004e1200) + selected-item square
|
||
// (retail SetSelectedState 0x004e1240). Drawn procedurally like the digit/drag overlays;
|
||
// guard id != 0 BEFORE resolving (feedback_ui_resolve_zero_magenta). Triangle under the
|
||
// square; both remain above the transient waiting mesh and below drag-accept feedback.
|
||
if (IsOpenContainer && SpriteResolve is not null && OpenContainerSprite != 0)
|
||
{
|
||
var (tex, _, _) = SpriteResolve(OpenContainerSprite);
|
||
if (tex != 0)
|
||
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||
}
|
||
if (Selected && SpriteResolve is not null && SelectedSprite != 0)
|
||
{
|
||
var (tex, _, _) = SpriteResolve(SelectedSprite);
|
||
if (tex != 0)
|
||
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||
}
|
||
|
||
// Drag-rollover accept/reject frame (retail SetDragAcceptState 0x10000041/40).
|
||
// Guard id != 0 BEFORE resolving — resolve(0) returns the 1×1 magenta placeholder
|
||
// with a non-zero GL handle (feedback_ui_resolve_zero_magenta).
|
||
if (_dragAccept != DragAcceptState.None && SpriteResolve is not null)
|
||
{
|
||
uint id = _dragAccept == DragAcceptState.Accept ? DragAcceptSprite : DragRejectSprite;
|
||
if (id != 0)
|
||
{
|
||
var (tex, _, _) = SpriteResolve(id);
|
||
if (tex != 0)
|
||
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||
}
|
||
}
|
||
}
|
||
}
|