using System;
using System.Collections.Generic;
using AcDream.App.UI;
using AcDream.Core.Items;
namespace AcDream.App.UI.Layout;
///
/// Binds the ~21 equip slots mounted under the paperdoll (gmPaperDollUI 0x21000024, nested in the
/// inventory frame 0x21000023) to live equipped-item data and makes them drag-drop WIELD targets.
/// The acdream analogue of gmPaperDollUI::PostInit + GetLocationInfoFromElementID (named-retail decomp
/// 175480 / 173620). Slice 1: equip slots only — no 3D doll viewport (that's Slice 2).
/// Unwield is handled by InventoryController (dragging an equipped item onto the pack grid).
///
public sealed class PaperdollController : IItemListDragHandler
{
// WEAPON_READY_SLOT_LOC (acclient.h:3235): the weapon-hand doll slot accepts any wieldable weapon.
private const EquipMask WeaponSlotMask =
EquipMask.MeleeWeapon | EquipMask.MissileWeapon | EquipMask.Held | EquipMask.TwoHanded;
// element-id → EquipMask (verified: dump paperdoll-0x21000024.txt ↔ deep-dive §3a ↔ acclient.h:3193).
// The 3 Aetheria sigil slots (0x10000595/96/97) are SetVisible(0) in retail — skipped (Slice 1 scope).
private static readonly (uint Element, EquipMask Mask)[] SlotMap =
{
(0x100005ABu, EquipMask.HeadWear), // 0x1
(0x100001E2u, EquipMask.ChestWear), // 0x2
(0x100001E3u, EquipMask.UpperLegWear), // 0x40
(0x100005B0u, EquipMask.HandWear), // 0x20
(0x100005B3u, EquipMask.FootWear), // 0x100
(0x100005ACu, EquipMask.ChestArmor), // 0x200
(0x100005ADu, EquipMask.AbdomenArmor), // 0x400
(0x100005AEu, EquipMask.UpperArmArmor), // 0x800
(0x100005AFu, EquipMask.LowerArmArmor), // 0x1000
(0x100005B1u, EquipMask.UpperLegArmor), // 0x2000
(0x100005B2u, EquipMask.LowerLegArmor), // 0x4000
(0x100001DAu, EquipMask.NeckWear), // 0x8000
(0x100001DBu, EquipMask.WristWearLeft), // 0x10000
(0x100001DDu, EquipMask.WristWearRight), // 0x20000
(0x100001DCu, EquipMask.FingerWearLeft), // 0x40000
(0x100001DEu, EquipMask.FingerWearRight), // 0x80000
(0x100001E1u, EquipMask.Shield), // 0x200000
(0x100001E0u, EquipMask.MissileAmmo), // 0x800000 (LIKELY — gate-verify, AP row)
(0x100001DFu, WeaponSlotMask), // 0x3500000 composite
(0x1000058Eu, EquipMask.TrinketOne), // 0x4000000
(0x100005E9u, EquipMask.Cloak), // 0x8000000
};
private readonly ClientObjectTable _objects;
private readonly Func _playerGuid;
private readonly Func _iconIds;
private readonly Action? _sendWield; // (itemGuid, equipMask) → GetAndWieldItem 0x001A
private readonly List<(EquipMask Mask, UiItemList List)> _slots = new();
private PaperdollController(
ImportedLayout layout, ClientObjectTable objects, Func playerGuid,
Func iconIds, Action? sendWield,
uint emptySlotSprite)
{
_objects = objects; _playerGuid = playerGuid; _iconIds = iconIds; _sendWield = sendWield;
for (int i = 0; i < SlotMap.Length; i++)
{
var (element, mask) = SlotMap[i];
if (layout.FindElement(element) is not UiItemList list) continue;
list.RegisterDragHandler(this);
list.Cell.SourceKind = ItemDragSource.Equipment;
list.Cell.SlotIndex = i; // SlotMap position = this equipped item's drag-payload SourceSlot when dragged out to unwield
list.Cell.EmptySprite = emptySlotSprite; // visible empty-slot frame so every position is seen + usable. The live
// 3D character (the "figure" — Slice 1/2 correction: NOT a per-slot
// silhouette) is the doll viewport, which arrives in Slice 2 with the Slots toggle.
// Cell.SpriteResolve + the default accept/reject sprites (ItemSlot_DragOver_Accept ring
// 0x060011F9 / reject circle 0x060011F8 — the discrete-slot frames, NOT the inventory grid's
// insert-arrow 0x060011F7) are already wired by DatWidgetFactory when it built the UiItemList;
// no need to re-set them here.
_slots.Add((mask, list));
}
_objects.ObjectAdded += OnObjectChanged;
_objects.ObjectMoved += OnObjectMoved;
_objects.ObjectRemoved += OnObjectChanged;
_objects.ObjectUpdated += OnObjectChanged;
Populate();
}
public static PaperdollController Bind(
ImportedLayout layout, ClientObjectTable objects, Func playerGuid,
Func iconIds, Action? sendWield = null,
uint emptySlotSprite = 0u)
=> new PaperdollController(layout, objects, playerGuid, iconIds, sendWield, emptySlotSprite);
private void OnObjectChanged(ClientObject o) { if (Concerns(o)) Populate(); }
private void OnObjectMoved(ClientObject o, uint from, uint to)
{ if (Concerns(o) || from == _playerGuid() || to == _playerGuid()) Populate(); }
/// The object belongs to the player (wielded gear or pack contents) — so a change to it may
/// add/remove/repaint a doll slot. Player-scoped: an NPC's or vendor's wielded item (which also carries
/// CurrentlyEquippedLocation from the wire) must NOT trigger a repaint. A player-equipped item always
/// has WielderId==p (login, from CreateObject) or ContainerId==p (live/optimistic wield, set by
/// WieldItemOptimistic), so the equip-location need not be tested here; OnObjectMoved adds the from/to
/// player backstop for moves that transiently satisfy neither (e.g. unwield into a side bag).
private bool Concerns(ClientObject o)
{
uint p = _playerGuid();
return o.WielderId == p || o.ContainerId == p;
}
public void Populate()
{
uint p = _playerGuid();
// The player's equipped gear (one pass). Scoped to the player so a vendor's/NPC's wielded item
// (which can also carry CurrentWieldedLocation) can't leak into the doll.
var equipped = new List();
foreach (var o in _objects.Objects)
if (o.CurrentlyEquippedLocation != EquipMask.None && (o.WielderId == p || o.ContainerId == p))
equipped.Add(o);
foreach (var (mask, list) in _slots)
{
ClientObject? worn = null;
foreach (var o in equipped)
if ((o.CurrentlyEquippedLocation & mask) != EquipMask.None) { worn = o; break; }
if (worn is null) { list.Cell.Clear(); continue; }
uint tex = _iconIds(worn.Type, worn.IconId, worn.IconUnderlayId, worn.IconOverlayId, worn.Effects);
list.Cell.SetItem(worn.ObjectId, tex);
}
}
private EquipMask MaskFor(UiItemList list)
{
foreach (var (mask, l) in _slots) if (ReferenceEquals(l, list)) return mask;
return EquipMask.None;
}
// ── IItemListDragHandler ──────────────────────────────────────────────────────────────────────
/// No-op: a wielded item stays put until the server confirms (same as the inventory grid,
/// unlike the toolbar's remove-on-lift). Unwield happens on DROP onto the pack grid — InventoryController.
public void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload) { }
/// Accept iff the dragged item can be worn here (its ValidLocations intersects the slot mask).
/// Retail: gmPaperDollUI::OnItemListDragOver (decomp 174302).
public bool OnDragOver(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
{
var item = _objects.Get(payload.ObjId);
if (item is null) return false;
return (item.ValidLocations & MaskFor(targetList)) != EquipMask.None;
}
/// Wield the dropped item: optimistic local equip (instant) + GetAndWieldItem 0x001A.
/// wieldMask = ValidLocations & slotMask resolves the specific bit (the composite weapon slot, the
/// chosen finger). Retail: gmPaperDollUI HandleDropRelease → GetAndWieldItem.
public void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
{
var item = _objects.Get(payload.ObjId);
if (item is null) return;
EquipMask wieldMask = item.ValidLocations & MaskFor(targetList);
if (wieldMask == EquipMask.None) return; // not wieldable here (defensive; OnDragOver already rejected)
_objects.WieldItemOptimistic(payload.ObjId, _playerGuid(), wieldMask);
_sendWield?.Invoke(payload.ObjId, (uint)wieldMask);
}
/// Detach event handlers (idempotent).
public void Dispose()
{
_objects.ObjectAdded -= OnObjectChanged;
_objects.ObjectMoved -= OnObjectMoved;
_objects.ObjectRemoved -= OnObjectChanged;
_objects.ObjectUpdated -= OnObjectChanged;
}
}