fix: trade gate round 1 - the Use-on-player route, authored grid cells,
and [trade] seam probes - Use on a selected player did nothing: the world Use path (SelectionInteractionController.RequestUse) dispatches 0x0036 directly and NEVER consults DetermineUseResult - lane C's "both paths already classified" claim described the POLICY's capability, not a live caller (the verify-subagent-claims lesson, again). Retail's CPlayerSystem::UsingItem @0x00562F70 consults it and routes result 5 (another PLAYER) to AttemptToOpenTradeNegotiations @0x0056DEE0, peace mode only, with no Use send. Ported as ItemInteractionController.TryOpenSecureTradeWithPlayer, called at RequestUse entry. - The broken window: both grids rendered their raw authored strip art with no cell layout (the gold-ring tile was the naked track). They now get the vendor strips' exact single-row 32px config + the authored empty-slot art resolved through ItemListCellTemplate (cell-template attr 0x1000000E -> 0x1000033A). - Drag-onto-player "nothing happens": the policy chain reads correct end-to-end in code (pick -> PlaceIn3D -> StartSecureTrade -> event -> cmd), so TEMPORARY [trade] probe lines now bracket every seam (drag-release pick, policy arm, controller request, use-on-player) - the next gate log pinpoints the break if it persists. Stripped once the two-client gate passes. App suite 4,990/3 skips. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
067cbea8a5
commit
22f30f291d
4 changed files with 103 additions and 2 deletions
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -1033,6 +1033,41 @@ public sealed class ItemInteractionController : IDisposable
|
|||
ClearTargetMode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gate fix (2026-08-14 trade round 1): the world Use path
|
||||
/// (<c>SelectionInteractionController.RequestUse</c>) dispatches
|
||||
/// <c>0x0036 Use</c> directly and never consults
|
||||
/// <c>DetermineUseResult</c> — retail's <c>CPlayerSystem::UsingItem
|
||||
/// @ 0x00562F70</c> DOES, and routes result 5 (target is another
|
||||
/// PLAYER) to <c>ClientTradeSystem::AttemptToOpenTradeNegotiations
|
||||
/// @ 0x0056DEE0</c>, 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).
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>TEMPORARY (2026-08-14 trade gate round 1): [trade] seam
|
||||
/// probe lines, stripped once the two-client gate passes.</summary>
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -58,7 +58,12 @@ public sealed class SecureTradeUiController : IRetainedPanelController
|
|||
Action<bool /*selfAccepted*/, bool /*partnerAccepted*/, uint /*partner*/> AcceptTrade,
|
||||
Action DeclineTrade,
|
||||
Action ResetTrade,
|
||||
Action<bool> SetWindowVisible);
|
||||
Action<bool> 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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue