diff --git a/src/AcDream.App/Interaction/SelectionInteractionController.cs b/src/AcDream.App/Interaction/SelectionInteractionController.cs index 930bac2b..4988a28a 100644 --- a/src/AcDream.App/Interaction/SelectionInteractionController.cs +++ b/src/AcDream.App/Interaction/SelectionInteractionController.cs @@ -99,6 +99,9 @@ internal sealed class SelectionInteractionController { ArgumentNullException.ThrowIfNull(payload); uint target = _query.PickAt(mouseX, mouseY, includeSelf: true) ?? 0u; + if (ItemInteractionController.PhysicsDiagnosticsTradeProbe) + Console.WriteLine( + $"[trade] drag-release item=0x{payload.ObjId:X8} pick=0x{target:X8}"); if (target != 0u) _query.BeginLightingPulse(target); _items.PlaceIn3D(payload, target); @@ -278,6 +281,17 @@ internal sealed class SelectionInteractionController { CancelPendingApproach(); + // Gate fix (2026-08-14 trade round 1): retail's use dispatch + // (CPlayerSystem::UsingItem @ 0x00562F70 → DetermineUseResult + // @ 0x00588460 result 5) routes "Use on another PLAYER" to secure + // trade and never sends 0x0036 for it — this path previously + // dispatched a plain Use the server ignores. + if (_items.TryOpenSecureTradeWithPlayer(serverGuid)) + { + reservation?.CancelBeforeDispatch(); + return; + } + bool ownedByPlayer = _items.IsOwnedByPlayer(serverGuid); bool useable = ownedByPlayer || _query.IsUseable(serverGuid); diff --git a/src/AcDream.App/UI/ItemInteractionController.cs b/src/AcDream.App/UI/ItemInteractionController.cs index 1cb81c22..e9fd614a 100644 --- a/src/AcDream.App/UI/ItemInteractionController.cs +++ b/src/AcDream.App/UI/ItemInteractionController.cs @@ -1033,6 +1033,41 @@ public sealed class ItemInteractionController : IDisposable ClearTargetMode(); } + /// + /// Gate fix (2026-08-14 trade round 1): the world Use path + /// (SelectionInteractionController.RequestUse) dispatches + /// 0x0036 Use directly and never consults + /// DetermineUseResult — retail's CPlayerSystem::UsingItem + /// @ 0x00562F70 DOES, and routes result 5 (target is another + /// PLAYER) to ClientTradeSystem::AttemptToOpenTradeNegotiations + /// @ 0x0056DEE0, gated on peace mode, with no Use send at all. + /// Returns true when the target is another player (the use is consumed + /// by the trade path either way; in combat mode retail's open attempt + /// early-outs silently). + /// + public bool TryOpenSecureTradeWithPlayer(uint targetGuid) + { + if (targetGuid == 0u || targetGuid == _playerGuid()) + return false; + ClientObject? target = _objects.Get(targetGuid); + if (target is null + || ((PublicWeenieFlags)(target.PublicWeenieBitfield ?? 0u) + & PublicWeenieFlags.Player) == 0) + return false; + + if (PhysicsDiagnosticsTradeProbe) + Console.WriteLine( + $"[trade] use-on-player guid=0x{targetGuid:X8} " + + $"nonCombat={_inNonCombatMode()} subscribers={SecureTradeRequested is not null}"); + if (_inNonCombatMode()) + SecureTradeRequested?.Invoke(targetGuid, 0u); + return true; + } + + /// TEMPORARY (2026-08-14 trade gate round 1): [trade] seam + /// probe lines, stripped once the two-client gate passes. + internal static bool PhysicsDiagnosticsTradeProbe = true; + public bool DropToWorld(ItemDragPayload payload) => PlaceIn3D(payload, targetGuid: 0u); @@ -1184,6 +1219,10 @@ public sealed class ItemInteractionController : IDisposable // (ItemHolder::AttemptPlaceIn3D @ 0x00588600's option branch // → ClientTradeSystem::AttemptToTradeItem @ 0x0056DF80). // ObjectId = the dragged item, TargetId = the player. + if (PhysicsDiagnosticsTradeProbe) + Console.WriteLine( + $"[trade] drag-on-player item=0x{action.ObjectId:X8} " + + $"target=0x{action.TargetId:X8} subscribers={SecureTradeRequested is not null}"); SecureTradeRequested?.Invoke(action.TargetId, action.ObjectId); break; case ItemPolicyActionKind.DropToWorld: diff --git a/src/AcDream.App/UI/Layout/SecureTradeUiController.cs b/src/AcDream.App/UI/Layout/SecureTradeUiController.cs index 34235ef9..2cedd243 100644 --- a/src/AcDream.App/UI/Layout/SecureTradeUiController.cs +++ b/src/AcDream.App/UI/Layout/SecureTradeUiController.cs @@ -58,7 +58,12 @@ public sealed class SecureTradeUiController : IRetainedPanelController Action AcceptTrade, Action DeclineTrade, Action ResetTrade, - Action SetWindowVisible); + 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); private readonly Bindings _bindings; private readonly UiText? _partnerName; @@ -122,9 +127,34 @@ public sealed class SecureTradeUiController : IRetainedPanelController // 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) { @@ -148,6 +178,10 @@ public sealed class SecureTradeUiController : IRetainedPanelController public void RequestSecureTrade(uint partnerGuid, uint itemGuid) { if (_disposed || partnerGuid == 0u) return; + if (ItemInteractionController.PhysicsDiagnosticsTradeProbe) + Console.WriteLine( + $"[trade] request partner=0x{partnerGuid:X8} item=0x{itemGuid:X8} " + + $"open={_bindings.Trade.Snapshot.IsOpen}"); RuntimeTradeSnapshot snapshot = _bindings.Trade.Snapshot; if (snapshot.IsOpen && snapshot.PartnerGuid == partnerGuid) { diff --git a/src/AcDream.App/UI/RetailUiRuntime.cs b/src/AcDream.App/UI/RetailUiRuntime.cs index 0d81bb39..a4d2c167 100644 --- a/src/AcDream.App/UI/RetailUiRuntime.cs +++ b/src/AcDream.App/UI/RetailUiRuntime.cs @@ -3489,6 +3489,8 @@ public sealed class RetailUiRuntime : IDisposable } ImportedLayout? layout; + uint selfEmptySlotSprite; + uint partnerEmptySlotSprite; lock (_bindings.Assets.DatLock) { layout = LayoutImporter.Import( @@ -3498,6 +3500,16 @@ public sealed class RetailUiRuntime : IDisposable _bindings.Assets.ResolveSprite, _bindings.Assets.DefaultFont, _bindings.Assets.ResolveFont); + // The authored empty-slot art for each grid (cell-template attr + // 0x1000000E → 0x1000033A) — same resolution the vendor uses. + selfEmptySlotSprite = ItemListCellTemplate.ResolveEmptySprite( + _bindings.Assets.Dats, + Layout.SecureTradeUiController.LayoutId, + Layout.SecureTradeUiController.SelfListId); + partnerEmptySlotSprite = ItemListCellTemplate.ResolveEmptySprite( + _bindings.Assets.Dats, + Layout.SecureTradeUiController.LayoutId, + Layout.SecureTradeUiController.PartnerListId); } if (layout is null) { @@ -3529,7 +3541,9 @@ public sealed class RetailUiRuntime : IDisposable { if (visible) Host.ShowWindow(WindowNames.SecureTrade); else Host.HideWindow(WindowNames.SecureTrade); - })); + }, + SelfEmptySlotSprite: selfEmptySlotSprite, + PartnerEmptySlotSprite: partnerEmptySlotSprite)); if (controller is null) { Console.WriteLine("[M4] secure trade: required authored grids are missing.");