fix: trade gate round 4 - "The trade has been cancelled." + retail's
staged-item trading marker - Cancel text: ClientTradeSystem::Handle_Trade__Recv_CloseTrade @0x0056DE30 shows "The trade has been cancelled." UNCONDITIONALLY (every close reason) as 0x1A ClientLocal - the yellow top-center SpewBox line. Wired at the router's onTradeClose beside ApplyClose; the string lives in ClientTextRefusals with its citation. - Staged-item marker: retail's mechanism decoded end-to-end - the UIItem prototype (catalog 0x21000037) authors overlay child 0x10000438 (sprite 0x06001DAE, the green frame + corner trade icon), bound @0x004E18FC and SetVisible(tradeState != 0) @0x004E2420; gmSecureTradeUI::AddItem @0x004CA801 sets ACCWeenieObject::SetTradeState(1) on YOUR staged items. Ported as: UiItemSlot.ShowTradeOverlay + TradeOverlaySprite (drawn over the icon), set on the trade window's self-grid cells; and RuntimeTradeState now borrows the canonical object table and maintains ClientObject.TradeState (1 at stage, 0 at remove/failure/ reset/close/clear) - which also brings the ALREADY-PORTED placement policy's "You cannot move an item while it is being traded" refusal to life (its input field previously had no live producer). Runtime 1,626, App 4,992/3 - green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
ffc73f80bf
commit
be3e617a2b
7 changed files with 119 additions and 6 deletions
|
|
@ -48,6 +48,13 @@ public sealed class SecureTradeUiController : IRetainedPanelController
|
|||
/// 0x1000007F states '', 'Highlight', 'Ghosted').</summary>
|
||||
private const string AcceptedState = "Highlight";
|
||||
|
||||
/// <summary>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.</summary>
|
||||
private const uint TradeOverlaySpriteId = 0x06001DAEu;
|
||||
|
||||
public sealed record Bindings(
|
||||
IRuntimeTradeView Trade,
|
||||
ClientObjectTable Objects,
|
||||
|
|
@ -289,6 +296,9 @@ public sealed class SecureTradeUiController : IRetainedPanelController
|
|||
// 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,
|
||||
};
|
||||
cell.SetItem(guid, icon);
|
||||
list.AddItem(cell);
|
||||
|
|
|
|||
|
|
@ -114,6 +114,15 @@ public class UiItemSlot : UiElement
|
|||
/// <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; }
|
||||
|
||||
/// <summary>Draw the retail trading marker over the icon — the UIItem
|
||||
/// prototype's authored overlay child 0x10000438 (sprite 0x06001DAE),
|
||||
/// shown when the underlying object's trade state is set
|
||||
/// (<c>@ 0x004E2420</c>).</summary>
|
||||
public bool ShowTradeOverlay { get; set; }
|
||||
|
||||
/// <summary>The authored overlay sprite id (0x06001DAE); 0 = no draw.</summary>
|
||||
public uint TradeOverlaySprite { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// DAT-authored 10%..100% radial cooldown overlays from element ids
|
||||
/// <c>0x1000054F..0x10000558</c>.
|
||||
|
|
@ -391,6 +400,19 @@ public class UiItemSlot : UiElement
|
|||
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
|
||||
// Trade-state overlay: UIElement_UIItem's authored child 0x10000438
|
||||
// (sprite 0x06001DAE — the green frame + corner trade icon), bound at
|
||||
// @ 0x004E18FC and SetVisible(tradeState != 0) at @ 0x004E2420.
|
||||
if (ShowTradeOverlay
|
||||
&& ItemId != 0
|
||||
&& SpriteResolve is not null
|
||||
&& TradeOverlaySprite != 0)
|
||||
{
|
||||
var (overlayTex, _, _) = SpriteResolve(TradeOverlaySprite);
|
||||
if (overlayTex != 0)
|
||||
ctx.DrawSprite(overlayTex, 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
|
||||
|
|
|
|||
|
|
@ -65,6 +65,13 @@ public static class ClientTextRefusals
|
|||
/// </summary>
|
||||
public const string CantJumpRecent = "You've jumped too recently!";
|
||||
|
||||
/// <summary>
|
||||
/// <c>ClientTradeSystem::Handle_Trade__Recv_CloseTrade @ 0x0056DE30</c>
|
||||
/// — shown UNCONDITIONALLY (every close reason) as LogTextType 0x1A
|
||||
/// ClientLocal, the yellow top-center SpewBox line.
|
||||
/// </summary>
|
||||
public const string TradeCancelled = "The trade has been cancelled.";
|
||||
|
||||
/// <summary>
|
||||
/// <c>WeenieError.YouAreTooTiredToDoThat (0x003E)</c> —
|
||||
/// <c>CommandInterpreter::MovePlayer</c>'s stamina-exhausted refusal
|
||||
|
|
|
|||
|
|
@ -255,8 +255,10 @@ public sealed class GameRuntime
|
|||
|
||||
// Secure trade (2026-08-14): third sibling J-owner —
|
||||
// session-scoped like fellowship (a disconnect closes the trade
|
||||
// server-side), clears at every generation reset.
|
||||
context.Trade = new RuntimeTradeState();
|
||||
// server-side), clears at every generation reset. Borrows the
|
||||
// canonical object table so staged items carry retail's
|
||||
// client-side trade state (SetTradeState @ 0x004CA801).
|
||||
context.Trade = new RuntimeTradeState(context.EntityObjects!.Objects);
|
||||
construction.Own(context.Trade);
|
||||
Fault(
|
||||
GameRuntimeConstructionPoint.TradeCreated,
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ public interface IRuntimeTradeView
|
|||
public sealed class RuntimeTradeState : IDisposable
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly AcDream.Core.Items.ClientObjectTable? _objects;
|
||||
private readonly List<uint> _selfItems = [];
|
||||
private readonly List<uint> _partnerItems = [];
|
||||
private bool _isOpen;
|
||||
|
|
@ -71,7 +72,17 @@ public sealed class RuntimeTradeState : IDisposable
|
|||
private long _revision;
|
||||
private bool _disposed;
|
||||
|
||||
public RuntimeTradeState() => View = new TradeView(this);
|
||||
/// <summary>Borrows the canonical object table (optional for bare
|
||||
/// fixtures) so YOUR staged items carry retail's client-side trade
|
||||
/// state — <c>ACCWeenieObject::SetTradeState(1) @ 0x004CA801</c> at
|
||||
/// stage, cleared on remove/failure/reset/close. The existing placement
|
||||
/// policy's "You cannot move an item while it is being traded" reads
|
||||
/// this exact field.</summary>
|
||||
public RuntimeTradeState(AcDream.Core.Items.ClientObjectTable? objects = null)
|
||||
{
|
||||
_objects = objects;
|
||||
View = new TradeView(this);
|
||||
}
|
||||
|
||||
public IRuntimeTradeView View { get; }
|
||||
|
||||
|
|
@ -133,6 +144,8 @@ public sealed class RuntimeTradeState : IDisposable
|
|||
: _selfItems;
|
||||
if (!items.Contains(update.ItemGuid))
|
||||
items.Add(update.ItemGuid);
|
||||
if (update.Side != (uint)RuntimeTradeSide.Partner)
|
||||
SetTradeState(update.ItemGuid, 1);
|
||||
// Staging changes invalidate prior acceptance server-side (ACE
|
||||
// re-arms via ClearTradeAcceptance; mirrored defensively here so
|
||||
// a dropped 0x0208 cannot leave a stale green check).
|
||||
|
|
@ -153,7 +166,11 @@ public sealed class RuntimeTradeState : IDisposable
|
|||
if (!_isOpen) return;
|
||||
bool removed = _selfItems.Remove(update.ItemGuid);
|
||||
removed |= _partnerItems.Remove(update.ItemGuid);
|
||||
if (removed) Bump();
|
||||
if (removed)
|
||||
{
|
||||
SetTradeState(update.ItemGuid, 0);
|
||||
Bump();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -193,6 +210,7 @@ public sealed class RuntimeTradeState : IDisposable
|
|||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (!_isOpen) return;
|
||||
ClearSelfTradeStates();
|
||||
_selfItems.Clear();
|
||||
_partnerItems.Clear();
|
||||
_selfAccepted = false;
|
||||
|
|
@ -212,6 +230,7 @@ public sealed class RuntimeTradeState : IDisposable
|
|||
if (!_isOpen) return;
|
||||
_selfItems.Remove(failure.ItemGuid);
|
||||
_partnerItems.Remove(failure.ItemGuid);
|
||||
SetTradeState(failure.ItemGuid, 0);
|
||||
_lastFailureItemGuid = failure.ItemGuid;
|
||||
_lastFailureReason = failure.Reason;
|
||||
Bump();
|
||||
|
|
@ -260,6 +279,21 @@ public sealed class RuntimeTradeState : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>Retail clears the client-side trade state on every staged
|
||||
/// item at reset/close (<c>Trade::Reset</c> tail — the same lifecycle
|
||||
/// gmSalvageUI::ClearAllTradeStates mirrors for its lists).</summary>
|
||||
private void ClearSelfTradeStates()
|
||||
{
|
||||
foreach (uint guid in _selfItems)
|
||||
SetTradeState(guid, 0);
|
||||
}
|
||||
|
||||
private void SetTradeState(uint itemGuid, int state)
|
||||
{
|
||||
if (_objects?.Get(itemGuid) is { } item)
|
||||
item.TradeState = state;
|
||||
}
|
||||
|
||||
private void ClearLocked()
|
||||
{
|
||||
bool changed = _isOpen
|
||||
|
|
@ -267,6 +301,7 @@ public sealed class RuntimeTradeState : IDisposable
|
|||
|| _partnerItems.Count != 0
|
||||
|| _selfAccepted
|
||||
|| _partnerAccepted;
|
||||
ClearSelfTradeStates();
|
||||
_isOpen = false;
|
||||
_partnerGuid = 0u;
|
||||
_selfItems.Clear();
|
||||
|
|
|
|||
|
|
@ -292,7 +292,15 @@ public sealed class LiveSessionEventRouter : ILiveSessionEventRouting
|
|||
? update => tradeRegister.ApplyRegister(update, inventory.PlayerGuid())
|
||||
: null,
|
||||
onTradeClose: social.Trade is { } tradeClose
|
||||
? _ => tradeClose.ApplyClose()
|
||||
? _ =>
|
||||
{
|
||||
tradeClose.ApplyClose();
|
||||
// Handle_Trade__Recv_CloseTrade @ 0x0056DE30 shows
|
||||
// this on EVERY close reason, 0x1A ClientLocal.
|
||||
social.AddText?.Invoke(
|
||||
AcDream.Core.Chat.ClientTextRefusals.TradeCancelled,
|
||||
RetailLogTextType.ClientLocal);
|
||||
}
|
||||
: null,
|
||||
onTradeAdd: social.Trade is { } tradeAdd
|
||||
? tradeAdd.ApplyAdd
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue