fix: trade gate round 3 - the mount-time-captured dead command bus (ONE
root cause for every dead interaction) + retail's Total Items caption The round-2 probes nailed it: the request seam fired for BOTH open paths (use AND drag - "drag-release pick" -> "drag-on-player" -> "request"), but no open-cmd, no wire-open, and no LiveCommandBus drop-warning ever printed. MountSecureTrade captured _bindings.Options.CommandBus() ONCE at mount time - the pre-session surface whose Publish routes into a null route silently. CommandBus is a Func for exactly this reason; the social mounts resolve it inside each lambda. Every trade command - open (use + drag), accept (the "unpressable" Trade button - the click FIRED, the publish died), Clear All, close, and drop-on-grid staging - died on that one captured bus. All six lambdas now resolve the Func per call. Also: ID_SecureTrade_TotalItemsLabel probe-verified token-free (fragments ["Total Items: ", ""], one ITEMS variable 0x004E8A23) and composed via ResolveTemplate - the count texts read retail's exact "Total Items: N". AD-95 RETIRED same-day. The pre-feature stub-toast test row (drag-on-player option-on expecting "Secure trade is not open.") now pins the SecureTradeRequested seam instead. App suite 4,991/3 skips. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f2144ab2a4
commit
ffc73f80bf
6 changed files with 97 additions and 16 deletions
File diff suppressed because one or more lines are too long
|
|
@ -63,7 +63,11 @@ public sealed class SecureTradeUiController : IRetainedPanelController
|
||||||
// ItemListCellTemplate from the lists' own 0x1000000E cell-template
|
// ItemListCellTemplate from the lists' own 0x1000000E cell-template
|
||||||
// attribute (0x1000033A) — same recipe as the vendor strips.
|
// attribute (0x1000033A) — same recipe as the vendor strips.
|
||||||
uint SelfEmptySlotSprite = 0u,
|
uint SelfEmptySlotSprite = 0u,
|
||||||
uint PartnerEmptySlotSprite = 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<int, string>? FormatTotalItems = null);
|
||||||
|
|
||||||
private readonly Bindings _bindings;
|
private readonly Bindings _bindings;
|
||||||
private readonly UiText? _partnerName;
|
private readonly UiText? _partnerName;
|
||||||
|
|
@ -255,10 +259,11 @@ public sealed class SecureTradeUiController : IRetainedPanelController
|
||||||
private void SetCount(UiText? text, int count)
|
private void SetCount(UiText? text, int count)
|
||||||
{
|
{
|
||||||
if (text is null) return;
|
if (text is null) return;
|
||||||
// Numeric-only, the AD-85 numeric-fields disposition: the authored
|
// Retail's exact ID_SecureTrade_TotalItemsLabel — probe-verified
|
||||||
// ID_SecureTrade_TotalItemsLabel template's variable shape is
|
// token-free (gate round 3), composed through the same
|
||||||
// unverified, so the DATA shows without invented surrounding words.
|
// ResolveTemplate the confirmation dialogs use.
|
||||||
string line = count.ToString();
|
string line = _bindings.FormatTotalItems?.Invoke(count)
|
||||||
|
?? count.ToString();
|
||||||
text.LinesProvider = () => [new UiText.Line(line, Vector4.One)];
|
text.LinesProvider = () => [new UiText.Line(line, Vector4.One)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3524,7 +3524,11 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var bus = _bindings.Options.CommandBus();
|
// Gate round 3 root cause: CommandBus is a Func for a REASON — the
|
||||||
|
// live route attaches at session start, AFTER this mount. Capturing
|
||||||
|
// the surface once here published every trade command into the
|
||||||
|
// pre-session null route, silently (the exact reason the social
|
||||||
|
// mount resolves the Func inside each lambda). Resolve per call.
|
||||||
Layout.SecureTradeUiController? controller =
|
Layout.SecureTradeUiController? controller =
|
||||||
Layout.SecureTradeUiController.Bind(
|
Layout.SecureTradeUiController.Bind(
|
||||||
layout,
|
layout,
|
||||||
|
|
@ -3532,24 +3536,43 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
Trade: tradeView,
|
Trade: tradeView,
|
||||||
Objects: _bindings.Inventory.Objects,
|
Objects: _bindings.Inventory.Objects,
|
||||||
ResolveIcon: _bindings.Inventory.ResolveIcon,
|
ResolveIcon: _bindings.Inventory.ResolveIcon,
|
||||||
OpenTrade: partner => bus.Publish(
|
OpenTrade: partner => _bindings.Options.CommandBus().Publish(
|
||||||
new OpenTradeNegotiationsRuntimeCmd(partner)),
|
new OpenTradeNegotiationsRuntimeCmd(partner)),
|
||||||
CloseTrade: () => bus.Publish(
|
CloseTrade: () => _bindings.Options.CommandBus().Publish(
|
||||||
new CloseTradeNegotiationsRuntimeCmd()),
|
new CloseTradeNegotiationsRuntimeCmd()),
|
||||||
AddToTrade: item => bus.Publish(
|
AddToTrade: item => _bindings.Options.CommandBus().Publish(
|
||||||
new AddToTradeRuntimeCmd(item)),
|
new AddToTradeRuntimeCmd(item)),
|
||||||
AcceptTrade: (selfAccepted, partnerAccepted, partner) =>
|
AcceptTrade: (selfAccepted, partnerAccepted, partner) =>
|
||||||
bus.Publish(new AcceptTradeRuntimeCmd(
|
_bindings.Options.CommandBus().Publish(new AcceptTradeRuntimeCmd(
|
||||||
partner, selfAccepted, partnerAccepted)),
|
partner, selfAccepted, partnerAccepted)),
|
||||||
DeclineTrade: () => bus.Publish(new DeclineTradeRuntimeCmd()),
|
DeclineTrade: () => _bindings.Options.CommandBus().Publish(
|
||||||
ResetTrade: () => bus.Publish(new ResetTradeRuntimeCmd()),
|
new DeclineTradeRuntimeCmd()),
|
||||||
|
ResetTrade: () => _bindings.Options.CommandBus().Publish(
|
||||||
|
new ResetTradeRuntimeCmd()),
|
||||||
SetWindowVisible: visible =>
|
SetWindowVisible: visible =>
|
||||||
{
|
{
|
||||||
if (visible) Host.ShowWindow(WindowNames.SecureTrade);
|
if (visible) Host.ShowWindow(WindowNames.SecureTrade);
|
||||||
else Host.HideWindow(WindowNames.SecureTrade);
|
else Host.HideWindow(WindowNames.SecureTrade);
|
||||||
},
|
},
|
||||||
SelfEmptySlotSprite: selfEmptySlotSprite,
|
SelfEmptySlotSprite: selfEmptySlotSprite,
|
||||||
PartnerEmptySlotSprite: partnerEmptySlotSprite));
|
PartnerEmptySlotSprite: partnerEmptySlotSprite,
|
||||||
|
// ID_SecureTrade_TotalItemsLabel (probe-verified
|
||||||
|
// token-free; the ITEMS variable) — retires AD-95.
|
||||||
|
FormatTotalItems: count =>
|
||||||
|
{
|
||||||
|
lock (_bindings.Assets.DatLock)
|
||||||
|
{
|
||||||
|
var strings = new DatStringResolver(_bindings.Assets.Dats);
|
||||||
|
return strings.ResolveTemplate(
|
||||||
|
0x23000001u,
|
||||||
|
"ID_SecureTrade_TotalItemsLabel",
|
||||||
|
new Dictionary<uint, string>
|
||||||
|
{
|
||||||
|
[DatStringResolver.ComputeHash("ITEMS")] =
|
||||||
|
count.ToString(),
|
||||||
|
}) ?? count.ToString();
|
||||||
|
}
|
||||||
|
}));
|
||||||
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.");
|
||||||
|
|
|
||||||
|
|
@ -610,6 +610,12 @@ public sealed class LiveSessionCommandRouterTests
|
||||||
RemoveFriend: _ => { },
|
RemoveFriend: _ => { },
|
||||||
ClearFriends: () => { },
|
ClearFriends: () => { },
|
||||||
RequestLegacyFriends: () => { },
|
RequestLegacyFriends: () => { },
|
||||||
|
OpenTradeNegotiations: _ => { },
|
||||||
|
CloseTradeNegotiations: () => { },
|
||||||
|
AddToTrade: _ => { },
|
||||||
|
AcceptTrade: (_, _, _) => { },
|
||||||
|
DeclineTrade: () => { },
|
||||||
|
ResetTrade: () => { },
|
||||||
ModifyCharacterSquelch: (_, _, _, _) => { },
|
ModifyCharacterSquelch: (_, _, _, _) => { },
|
||||||
ModifyAccountSquelch: (_, _) => { },
|
ModifyAccountSquelch: (_, _) => { },
|
||||||
ModifyGlobalSquelch: (_, _) => { },
|
ModifyGlobalSquelch: (_, _) => { },
|
||||||
|
|
|
||||||
|
|
@ -1540,15 +1540,25 @@ public sealed class ItemInteractionControllerTests
|
||||||
var payload = new ItemDragPayload(
|
var payload = new ItemDragPayload(
|
||||||
item, ItemDragSource.Inventory, SourceSlot: 0, SourceCell: new UiItemSlot());
|
item, ItemDragSource.Inventory, SourceSlot: 0, SourceCell: new UiItemSlot());
|
||||||
|
|
||||||
|
var tradeRequests = new List<(uint Partner, uint Item)>();
|
||||||
|
h.Controller.SecureTradeRequested += (partner, dragged) =>
|
||||||
|
tradeRequests.Add((partner, dragged));
|
||||||
|
|
||||||
bool result = h.Controller.PlaceIn3D(payload, targetPlayer);
|
bool result = h.Controller.PlaceIn3D(payload, targetPlayer);
|
||||||
|
|
||||||
Assert.Equal(sendsGive, result);
|
Assert.Equal(sendsGive, result);
|
||||||
if (sendsGive)
|
if (sendsGive)
|
||||||
|
{
|
||||||
Assert.Equal(new[] { (targetPlayer, item, 1u) }, h.Gives);
|
Assert.Equal(new[] { (targetPlayer, item, 1u) }, h.Gives);
|
||||||
|
Assert.Empty(tradeRequests);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// Trade feature (2026-08-14): the option-on drag now raises the
|
||||||
|
// SecureTradeRequested seam (retail's AttemptToTradeItem
|
||||||
|
// @ 0x0056DF80) instead of the pre-feature stub toast.
|
||||||
Assert.Empty(h.Gives);
|
Assert.Empty(h.Gives);
|
||||||
Assert.Contains(h.Toasts, text => text.Contains("Secure trade", StringComparison.Ordinal));
|
Assert.Equal([(targetPlayer, item)], tradeRequests);
|
||||||
}
|
}
|
||||||
Assert.Equal(Pack, h.Objects.Get(item)!.ContainerId);
|
Assert.Equal(Pack, h.Objects.Get(item)!.ContainerId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,44 @@ public sealed class PowerbarLayoutProbeTests
|
||||||
Console.WriteLine($"[pbprobe] scanned {scanned} layouts");
|
Console.WriteLine($"[pbprobe] scanned {scanned} layouts");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>2026-08-14 trade gate round 3: dump
|
||||||
|
/// ID_SecureTrade_TotalItemsLabel's fragments + variable hashes so the
|
||||||
|
/// count text can use ResolveTemplate if token-free (AD-95).</summary>
|
||||||
|
[Fact]
|
||||||
|
public void ProbeTotalItemsTemplate()
|
||||||
|
{
|
||||||
|
if (Environment.GetEnvironmentVariable("ACDREAM_PROBE_POWERBAR") != "1")
|
||||||
|
return;
|
||||||
|
|
||||||
|
var datDir = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
||||||
|
?? Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
|
"Documents",
|
||||||
|
"Asheron's Call");
|
||||||
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||||
|
|
||||||
|
uint hash = DatStringResolver.ComputeHash("ID_SecureTrade_TotalItemsLabel");
|
||||||
|
var table = dats.Get<DatReaderWriter.DBObjs.StringTable>(0x23000001u);
|
||||||
|
Console.WriteLine($"[pbprobe] key hash=0x{hash:X8} tableFound={table is not null}");
|
||||||
|
if (table is not null && table.Strings.TryGetValue(hash, out var entry))
|
||||||
|
{
|
||||||
|
for (int i = 0; i < entry.Strings.Count; i++)
|
||||||
|
Console.WriteLine($"[pbprobe] fragment[{i}]='{entry.Strings[i].Value}'");
|
||||||
|
for (int i = 0; i < entry.Variables.Count; i++)
|
||||||
|
Console.WriteLine($"[pbprobe] variable[{i}]=0x{entry.Variables[i]:X8}");
|
||||||
|
foreach (string candidate in new[]
|
||||||
|
{ "COUNT", "NUM", "NUMBER", "ITEMS", "TOTAL", "AMOUNT", "N" })
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
$"[pbprobe] hash('{candidate}')=0x{DatStringResolver.ComputeHash(candidate):X8}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("[pbprobe] entry NOT FOUND in 0x23000001");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static ElementInfo? FindById(ElementInfo element, uint id)
|
private static ElementInfo? FindById(ElementInfo element, uint id)
|
||||||
{
|
{
|
||||||
if (element.Id == id) return element;
|
if (element.Id == id) return element;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue