using System; using System.Numerics; namespace AcDream.App.UI; /// /// 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). /// public sealed class UiItemSlot : UiElement { public UiItemSlot() { ClickThrough = false; } public override bool ConsumesDatChildren => true; /// Bound weenie guid (0 = empty). Retail UIElement_UIItem::itemID. public uint ItemId { get; private set; } /// Pre-composited icon GL texture for the bound item (0 = none). public uint IconTexture { get; private set; } /// This cell's own index within its panel (0..17 toolbar; container slot /// for inventory). Distinct from (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. public int SlotIndex { get; set; } = -1; /// What kind of slot this is, for the drag payload (retail InqDropIconInfo /// flags). Controller overrides; default Inventory. public ItemDragSource SourceKind { get; set; } = ItemDragSource.Inventory; /// Drag-rollover accept frame (retail ItemSlot_DragOver_Accept 0x060011F9, /// state id 0x10000041). Configurable; guard id != 0 before resolving. public uint DragAcceptSprite { get; set; } = 0x060011F9u; /// Drag-rollover reject frame (retail ItemSlot_DragOver_Reject 0x060011F8, /// state id 0x10000040). public uint DragRejectSprite { get; set; } = 0x060011F8u; /// 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. public bool IsOpenContainer { get; set; } /// Open-container triangle sprite (element 0x10000450 on the container prototype /// 0x1000033F). Configurable; guard id != 0 before resolving. public uint OpenContainerSprite { get; set; } = 0x06005D9Cu; /// 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. public bool Selected { get; set; } /// 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). public uint SelectedSprite { get; set; } = 0x06004D21u; /// 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). public float CapacityFill { get; set; } = -1f; /// Capacity-bar track sprite (meter 0x10000347 DirectState). public uint CapacityBackSprite { get; set; } = 0x06004D22u; /// Capacity-bar fill sprite (meter 0x10000347 front child 0x00000002 DirectState). public uint CapacityFrontSprite { get; set; } = 0x06004D23u; /// Accept/reject overlay state while a drag hovers this cell. public enum DragAcceptState { None, Accept, Reject } private DragAcceptState _dragAccept = DragAcceptState.None; /// Current overlay state — internal so unit tests can assert it (InternalsVisibleTo). internal DragAcceptState DragAcceptVisual => _dragAccept; /// 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. public uint EmptySprite { get; set; } = 0x060074CFu; /// RenderSurface id -> (GL texture, w, h). Set by the factory/controller. public Func? SpriteResolve { get; set; } public void SetItem(uint itemId, uint iconTexture) { ItemId = itemId; IconTexture = iconTexture; } public void Clear() { ItemId = 0; IconTexture = 0; } /// public override object? GetDragPayload() => ItemId != 0 ? new ItemDragPayload(ItemId, SourceKind, SlotIndex, this) : null; /// public override (uint tex, int w, int h)? GetDragGhost() => ItemId != 0 && IconTexture != 0 ? (IconTexture, (int)Width, (int)Height) : null; /// 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 '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). public override bool IsDragSource => ItemId != 0; /// Walk up to the containing (the drop handler owner). 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 (peace) / 0x10000043 (war), // 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. /// 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. public int ShortcutNum { get; private set; } = -1; /// True = draw peace digit set; false = war digit set. public bool ShortcutPeace { get; private set; } = true; /// Peace 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 (peace) or 0x10000043 (war) by stance. public uint[]? PeaceDigits { get; set; } /// War digit DID array. Same layout as PeaceDigits. /// Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229493) — war stance. public uint[]? WarDigits { get; set; } /// 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). public uint[]? EmptyDigits { get; set; } /// Set the slot's shortcut position and combat stance so the correct digit /// is drawn. Call with index 0..8 for the top row; pass peace=true for NonCombat. public void SetShortcutNum(int index, bool peace) { ShortcutNum = index; ShortcutPeace = peace; } /// Clear the shortcut number label (hides the digit). public void ClearShortcutNum() { ShortcutNum = -1; } /// /// Returns the digit DID array that OnDraw will use, following the retail occupancy /// branch in UIElement_UIItem::SetShortcutNum (decomp 229481): /// occupied (ItemId != 0) → ShortcutPeace ? PeaceDigits : WarDigits (0x10000042/43) /// 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. /// internal uint[]? ActiveDigitArray() { bool occupied = ItemId != 0; return occupied ? (ShortcutPeace ? PeaceDigits : WarDigits) : EmptyDigits; } // ── Events / draw ───────────────────────────────────────────────────────── /// Invoked by when a left-button-down lands on /// a bound slot. Wired by ToolbarController to the use-item callback. public Action? Clicked { get; set; } /// 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.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)) ? DragAcceptState.Accept : 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) → peace/war digit set 0x10000042/43, split by stance // 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); } } } // 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 under the transient drag overlay below. 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); } } } }