using System.Numerics;
using AcDream.Core.Items;
using AcDream.Runtime.Gameplay;
namespace AcDream.App.UI.Layout;
///
/// Binds the imported retail gmSecureTradeUI layout
/// (LayoutDesc 0x2100000D, root 0x1000007A) to
/// 's view. No panel geometry is synthesized:
/// every control is the authored element.
///
///
/// Retail references (lane A, docs/research/2026-08-14-trade-laneA-ui.md):
/// gmSecureTradeUI::PostInit @ 0x004CA160 binds exactly these ids;
/// ListenToElementMessage @ 0x004CAE80 reacts to the Trade button
/// (0x10000086), Clear All (0x1000008A), and the close X (0x1000008B);
/// RecvNotice_RegisterTrade @ 0x004CA5C0 opens the panel. The accept
/// presentation follows lane A's recommendation: driven off the owner's own
/// accepted booleans (the partner status icon's authored 'Highlight' state;
/// the Trade button's Selected latch), not retail's ambiguous literal
/// m_state numbers. Open paths (retail's own two):
/// ItemHolder::DetermineUseResult @ 0x00588460 result 5 (Use on a
/// player) and ItemHolder::AttemptPlaceIn3D @ 0x00588600's
/// DragItemOnPlayerOpensSecureTrade branch → both surface here through
/// (the ItemInteractionController event),
/// mirroring ClientTradeSystem::AttemptToOpenTradeNegotiations
/// @ 0x0056DEE0 / AttemptToTradeItem @ 0x0056DF80 — the latter's
/// "queue the dragged item until the window registers" is the pending-stage
/// latch consumed in .
///
public sealed class SecureTradeUiController : IRetainedPanelController
{
public const uint LayoutId = 0x2100000Du;
public const uint RootId = 0x1000007Au;
public const uint PartnerNameId = 0x1000007Eu;
public const uint PartnerStatusId = 0x1000007Fu;
public const uint PartnerCountId = 0x10000080u;
public const uint PartnerListId = 0x10000081u;
public const uint SelfNameId = 0x10000085u;
public const uint TradeButtonId = 0x10000086u;
public const uint SelfCountId = 0x10000087u;
public const uint SelfListId = 0x10000088u;
public const uint ClearAllButtonId = 0x1000008Au;
public const uint CloseButtonId = 0x1000008Bu;
/// The authored partner-status accept cue (probe: element
/// 0x1000007F states '', 'Highlight', 'Ghosted').
private const string AcceptedState = "Highlight";
/// The UIItem prototype's authored trade-state overlay sprite
/// (catalog 0x21000037 element 0x10000438 — the green frame + corner
/// trade icon; bound @ 0x004E18FC, shown when tradeState != 0
/// @ 0x004E2420). Retail sets tradeState=1 on YOUR staged items
/// (gmSecureTradeUI::AddItem @ 0x004CA801), so the self grid marks.
private const uint TradeOverlaySpriteId = 0x06001DAEu;
public sealed record Bindings(
IRuntimeTradeView Trade,
ClientObjectTable Objects,
Func ResolveIcon,
Action OpenTrade,
Action CloseTrade,
Action AddToTrade,
Action AcceptTrade,
Action DeclineTrade,
Action ResetTrade,
Action SetWindowVisible,
// The authored empty-slot background for each grid, resolved via
// ItemListCellTemplate from the lists' own 0x1000000E cell-template
// attribute (0x1000033A) — same recipe as the vendor strips.
uint SelfEmptySlotSprite = 0u,
uint PartnerEmptySlotSprite = 0u,
// ID_SecureTrade_TotalItemsLabel composed per count — probe-verified
// token-free (fragments ["Total Items: ", ""], one ITEMS variable);
// null falls back to the bare number.
Func? FormatTotalItems = null);
private readonly Bindings _bindings;
private readonly UiText? _partnerName;
private readonly UiElement? _partnerStatus;
private readonly UiText? _partnerCount;
private readonly UiItemList? _partnerList;
private readonly UiText? _selfCount;
private readonly UiItemList? _selfList;
private readonly UiButton? _tradeButton;
private long _lastRevision = long.MinValue;
private bool _wasOpen;
private uint _pendingPartner;
private uint _pendingStageItem;
private bool _disposed;
private SecureTradeUiController(
ImportedLayout layout,
Bindings bindings)
{
_bindings = bindings;
_partnerName = layout.FindElement(PartnerNameId) as UiText;
_partnerStatus = layout.FindElement(PartnerStatusId);
_partnerCount = layout.FindElement(PartnerCountId) as UiText;
_partnerList = layout.FindElement(PartnerListId) as UiItemList;
_selfCount = layout.FindElement(SelfCountId) as UiText;
_selfList = layout.FindElement(SelfListId) as UiItemList;
_tradeButton = layout.FindElement(TradeButtonId) as UiButton;
if (_tradeButton is not null)
{
// Retail's accept TOGGLE: not-yet-accepted click → AcceptTrade;
// already-accepted click → DeclineTrade (withdraw). Selected is
// seeded from the store each Tick (the CH6a/b mirror discipline).
_tradeButton.SuppressSelfToggle = true;
_tradeButton.OnClick = () =>
{
RuntimeTradeSnapshot snapshot = _bindings.Trade.Snapshot;
if (!snapshot.IsOpen) return;
if (snapshot.SelfAccepted)
_bindings.DeclineTrade();
else
_bindings.AcceptTrade(
true, snapshot.PartnerAccepted, snapshot.PartnerGuid);
};
}
if (layout.FindElement(ClearAllButtonId) is UiButton clearAll)
clearAll.OnClick = () =>
{
if (_bindings.Trade.Snapshot.IsOpen) _bindings.ResetTrade();
};
if (layout.FindElement(CloseButtonId) is UiButton close)
close.OnClick = () =>
{
if (_bindings.Trade.Snapshot.IsOpen) _bindings.CloseTrade();
};
// Retail registers the drag handler on the SELF grid only
// (PostInit @ 0x004CA1F7; drops land only on your own side —
// HandleDropRelease's ancestor-chain check). An inventory item
// dropped on the grid stages it.
_selfList?.RegisterDragHandler(new SelfGridDropHandler(this));
// Gate fix (2026-08-14 round 1: "broken trade window"): the grids
// rendered their raw authored strip art with no cell layout at all —
// the same single-row 32px config + authored empty-slot fill the
// vendor/external-container strips use.
ConfigureGrid(_selfList, bindings.SelfEmptySlotSprite);
ConfigureGrid(_partnerList, bindings.PartnerEmptySlotSprite);
_bindings.SetWindowVisible(false);
}
private static void ConfigureGrid(UiItemList? list, uint emptySlotSprite)
{
if (list is null) return;
list.Columns = 1;
list.SingleRow = true;
list.HorizontalScroll = true;
list.CellWidth = 32f;
list.CellHeight = 32f;
list.FillVisibleEmptySlots = true;
if (emptySlotSprite != 0u)
list.CellEmptySprite = emptySlotSprite;
list.EmptySlotFactory = () => new UiItemSlot
{
SpriteResolve = list.SpriteResolve,
AllowDragSource = false,
};
}
public static SecureTradeUiController? Bind(
ImportedLayout layout, Bindings bindings)
{
ArgumentNullException.ThrowIfNull(layout);
ArgumentNullException.ThrowIfNull(bindings);
// The two grids are the panel's load-bearing controls; a layout
// missing either cannot present a trade honestly.
if (layout.FindElement(SelfListId) is not UiItemList
|| layout.FindElement(PartnerListId) is not UiItemList)
return null;
return new SecureTradeUiController(layout, bindings);
}
///
/// The two retail open paths (Use-on-player, drag-item-on-player) —
/// raised by ItemInteractionController. When a trade with this partner
/// is already open, a dragged item stages immediately
/// (AttemptToTradeItem's open-trade branch); otherwise open
/// negotiations and latch the item until RegisterTrade arrives.
///
public void RequestSecureTrade(uint partnerGuid, uint itemGuid)
{
if (_disposed || partnerGuid == 0u) return;
RuntimeTradeSnapshot snapshot = _bindings.Trade.Snapshot;
if (snapshot.IsOpen && snapshot.PartnerGuid == partnerGuid)
{
if (itemGuid != 0u) _bindings.AddToTrade(itemGuid);
return;
}
_pendingPartner = partnerGuid;
_pendingStageItem = itemGuid;
_bindings.OpenTrade(partnerGuid);
}
/// Applies the latest owner snapshot (revision-gated).
public void Tick()
{
if (_disposed) return;
RuntimeTradeSnapshot snapshot = _bindings.Trade.Snapshot;
if (snapshot.IsOpen && !_wasOpen)
{
_wasOpen = true;
_bindings.SetWindowVisible(true);
// AttemptToTradeItem's queued item — stage it now that the
// window registered, if the register matched the request.
if (_pendingStageItem != 0u
&& (_pendingPartner == 0u
|| snapshot.PartnerGuid == _pendingPartner))
{
_bindings.AddToTrade(_pendingStageItem);
}
_pendingStageItem = 0u;
_pendingPartner = 0u;
}
else if (!snapshot.IsOpen && _wasOpen)
{
_wasOpen = false;
_bindings.SetWindowVisible(false);
}
if (snapshot.Revision == _lastRevision) return;
_lastRevision = snapshot.Revision;
if (_partnerName is not null)
{
string name = _bindings.Objects.Get(snapshot.PartnerGuid)
?.GetAppropriateName() ?? string.Empty;
_partnerName.LinesProvider =
() => [new UiText.Line(name, Vector4.One)];
}
// Accept cues: the partner icon's authored Highlight state (the same
// ActiveState flip the fellowship row's amber selection uses); the
// Trade button's Selected latch for the local player's own accept.
if (_partnerStatus is UiDatElement status)
status.ActiveState = snapshot.PartnerAccepted ? AcceptedState : "";
if (_tradeButton is not null)
_tradeButton.Selected = snapshot.SelfAccepted;
SetCount(_selfCount, snapshot.SelfItemCount);
SetCount(_partnerCount, snapshot.PartnerItemCount);
Populate(_selfList, RuntimeTradeSide.Self);
Populate(_partnerList, RuntimeTradeSide.Partner);
}
public void SyncVisibility()
{
_wasOpen = !_bindings.Trade.Snapshot.IsOpen; // force re-evaluate
Tick();
}
public void OnShown() => Tick();
private void SetCount(UiText? text, int count)
{
if (text is null) return;
// Retail's exact ID_SecureTrade_TotalItemsLabel — probe-verified
// token-free (gate round 3), composed through the same
// ResolveTemplate the confirmation dialogs use.
string line = _bindings.FormatTotalItems?.Invoke(count)
?? count.ToString();
text.LinesProvider = () => [new UiText.Line(line, Vector4.One)];
}
private void Populate(UiItemList? list, RuntimeTradeSide side)
{
if (list is null) return;
using (list.DeferLayout())
{
list.Flush();
foreach (uint guid in _bindings.Trade.GetItems(side))
{
ClientObject? item = _bindings.Objects.Get(guid);
uint icon = item is null ? 0u : _bindings.ResolveIcon(
item.Type,
item.IconId,
item.IconUnderlayId,
item.IconOverlayId,
item.Effects);
var cell = new UiItemSlot
{
SpriteResolve = list.SpriteResolve,
SlotIndex = list.GetNumUIItems(),
// Staged rows are not drag sources — ACE has no
// per-item removal (only Clear All / reset).
AllowDragSource = false,
// Your staged items carry retail's trading marker.
ShowTradeOverlay = side == RuntimeTradeSide.Self,
TradeOverlaySprite = TradeOverlaySpriteId,
TooltipTextResolve = g => _bindings.Objects.Get(g)?.GetTooltipDisplayName(),
};
cell.SetItem(guid, icon);
list.AddItem(cell);
}
}
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
if (_tradeButton is not null) _tradeButton.OnClick = null;
}
/// Drops on the SELF grid stage the dragged inventory item
/// (retail's AcceptDragObject → AddToTrade path).
private sealed class SelfGridDropHandler(SecureTradeUiController owner)
: IItemListDragHandler
{
public void OnDragLift(
UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload)
{
// The trade grids are never drag SOURCES (AllowDragSource=false
// on every staged cell) — nothing to lift.
}
public ItemDragAcceptance OnDragOver(
UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
=> payload.SourceKind == ItemDragSource.Inventory
? ItemDragAcceptance.Accept
: ItemDragAcceptance.Reject;
public void HandleDropRelease(
UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
{
if (payload.SourceKind != ItemDragSource.Inventory) return;
if (owner._bindings.Trade.Snapshot.IsOpen)
owner._bindings.AddToTrade(payload.ObjId);
}
}
}