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);
|
ArgumentNullException.ThrowIfNull(payload);
|
||||||
uint target = _query.PickAt(mouseX, mouseY, includeSelf: true) ?? 0u;
|
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)
|
if (target != 0u)
|
||||||
_query.BeginLightingPulse(target);
|
_query.BeginLightingPulse(target);
|
||||||
_items.PlaceIn3D(payload, target);
|
_items.PlaceIn3D(payload, target);
|
||||||
|
|
@ -278,6 +281,17 @@ internal sealed class SelectionInteractionController
|
||||||
{
|
{
|
||||||
CancelPendingApproach();
|
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 ownedByPlayer = _items.IsOwnedByPlayer(serverGuid);
|
||||||
bool useable = ownedByPlayer || _query.IsUseable(serverGuid);
|
bool useable = ownedByPlayer || _query.IsUseable(serverGuid);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1033,6 +1033,41 @@ public sealed class ItemInteractionController : IDisposable
|
||||||
ClearTargetMode();
|
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)
|
public bool DropToWorld(ItemDragPayload payload)
|
||||||
=> PlaceIn3D(payload, targetGuid: 0u);
|
=> PlaceIn3D(payload, targetGuid: 0u);
|
||||||
|
|
||||||
|
|
@ -1184,6 +1219,10 @@ public sealed class ItemInteractionController : IDisposable
|
||||||
// (ItemHolder::AttemptPlaceIn3D @ 0x00588600's option branch
|
// (ItemHolder::AttemptPlaceIn3D @ 0x00588600's option branch
|
||||||
// → ClientTradeSystem::AttemptToTradeItem @ 0x0056DF80).
|
// → ClientTradeSystem::AttemptToTradeItem @ 0x0056DF80).
|
||||||
// ObjectId = the dragged item, TargetId = the player.
|
// 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);
|
SecureTradeRequested?.Invoke(action.TargetId, action.ObjectId);
|
||||||
break;
|
break;
|
||||||
case ItemPolicyActionKind.DropToWorld:
|
case ItemPolicyActionKind.DropToWorld:
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,12 @@ public sealed class SecureTradeUiController : IRetainedPanelController
|
||||||
Action<bool /*selfAccepted*/, bool /*partnerAccepted*/, uint /*partner*/> AcceptTrade,
|
Action<bool /*selfAccepted*/, bool /*partnerAccepted*/, uint /*partner*/> AcceptTrade,
|
||||||
Action DeclineTrade,
|
Action DeclineTrade,
|
||||||
Action ResetTrade,
|
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 Bindings _bindings;
|
||||||
private readonly UiText? _partnerName;
|
private readonly UiText? _partnerName;
|
||||||
|
|
@ -122,9 +127,34 @@ public sealed class SecureTradeUiController : IRetainedPanelController
|
||||||
// dropped on the grid stages it.
|
// dropped on the grid stages it.
|
||||||
_selfList?.RegisterDragHandler(new SelfGridDropHandler(this));
|
_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);
|
_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(
|
public static SecureTradeUiController? Bind(
|
||||||
ImportedLayout layout, Bindings bindings)
|
ImportedLayout layout, Bindings bindings)
|
||||||
{
|
{
|
||||||
|
|
@ -148,6 +178,10 @@ public sealed class SecureTradeUiController : IRetainedPanelController
|
||||||
public void RequestSecureTrade(uint partnerGuid, uint itemGuid)
|
public void RequestSecureTrade(uint partnerGuid, uint itemGuid)
|
||||||
{
|
{
|
||||||
if (_disposed || partnerGuid == 0u) return;
|
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;
|
RuntimeTradeSnapshot snapshot = _bindings.Trade.Snapshot;
|
||||||
if (snapshot.IsOpen && snapshot.PartnerGuid == partnerGuid)
|
if (snapshot.IsOpen && snapshot.PartnerGuid == partnerGuid)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -3489,6 +3489,8 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
ImportedLayout? layout;
|
ImportedLayout? layout;
|
||||||
|
uint selfEmptySlotSprite;
|
||||||
|
uint partnerEmptySlotSprite;
|
||||||
lock (_bindings.Assets.DatLock)
|
lock (_bindings.Assets.DatLock)
|
||||||
{
|
{
|
||||||
layout = LayoutImporter.Import(
|
layout = LayoutImporter.Import(
|
||||||
|
|
@ -3498,6 +3500,16 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
_bindings.Assets.ResolveSprite,
|
_bindings.Assets.ResolveSprite,
|
||||||
_bindings.Assets.DefaultFont,
|
_bindings.Assets.DefaultFont,
|
||||||
_bindings.Assets.ResolveFont);
|
_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)
|
if (layout is null)
|
||||||
{
|
{
|
||||||
|
|
@ -3529,7 +3541,9 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
{
|
{
|
||||||
if (visible) Host.ShowWindow(WindowNames.SecureTrade);
|
if (visible) Host.ShowWindow(WindowNames.SecureTrade);
|
||||||
else Host.HideWindow(WindowNames.SecureTrade);
|
else Host.HideWindow(WindowNames.SecureTrade);
|
||||||
}));
|
},
|
||||||
|
SelfEmptySlotSprite: selfEmptySlotSprite,
|
||||||
|
PartnerEmptySlotSprite: partnerEmptySlotSprite));
|
||||||
if (controller is null)
|
if (controller is null)
|
||||||
{
|
{
|
||||||
Console.WriteLine("[M4] secure trade: required authored grids are missing.");
|
Console.WriteLine("[M4] secure trade: required authored grids are missing.");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue