acdream/tests/AcDream.App.Tests/UI/Layout/PowerbarLayoutProbeTests.cs
Erik 067cbea8a5 feat: secure trade with other players - wire, RuntimeTradeState, the
authored gmSecureTradeUI window, and both retail open paths

Three-lane research first (docs/research/2026-08-14-trade-lane{A,B,C}):
retail gmSecureTradeUI decode, the byte-exact ACE/decomp/holtburger
three-way wire agreement, and the acdream seam map (which found both
open paths ALREADY classified by the ported policy - OpenSecureTrade on
Use-a-player, StartSecureTrade on drag-item-onto-player with the
DragItemOnPlayerOpensSecureTrade option - dead-ending at a stub toast).

- Core.Net: TradeRequests builders (0x1F6-0x204, retail's CM_Trade
  senders byte-checked against ACE's readers; the ACE-discarded
  AcceptTrade echo carries zero-count item lists - AD-94), corrected +
  completed inbound parsers (0x1FD-0x208; the old AddToTrade parser
  missed the SIDE dword, TradeFailure missed the reason), delegate-hole
  registrars, six WorldSession sends. 10 golden-byte tests.
- Runtime: RuntimeTradeState, the third sibling J-owner (fellowship/
  allegiance shape): session-scoped, clears at generation reset (new
  stage Trade=14), staged teardown stage 11 (Identity/EntityObjects
  shift 12/13, TeardownStageCount 14 - the FA2-era per-stage-flag test
  caught the mapping exactly as designed), combined ownership ledger,
  event routing with ACE's wrong-initiator RegisterTrade landmine
  honored (partner = whichever guid is not mine). 7 conformance tests.
- App: SecureTradeUiController binds the dedicated authored LayoutDesc
  0x2100000D (root 0x1000007A - gmSecureTradeUI::PostInit's exact ids):
  partner name/status/count/grid, the authored 'Trade' accept toggle
  (accept <-> decline withdraw), 'Clear All' (ACE clears BOTH sides -
  surfaced honestly), the X close, drop-on-your-grid staging, per-mode
  accept cues (partner icon's authored Highlight state + Trade button
  Selected latch). Mounted via the vendor recipe (nine-slice chrome,
  hidden until RegisterTrade). ItemInteractionController's two policy
  arms now raise SecureTradeRequested instead of the stub toast; the
  drag path queues the dragged item until the window registers
  (ClientTradeSystem::AttemptToTradeItem @0x0056DF80's shape).

Register: AD-94 (accept-echo zero-count lists), AD-95 (numeric-only
count texts pending template verification).

Suites: App 4,990/3, Core.Net 905, Runtime 1,626 - all green. The
panel itself is user-gate acceptance (two-client connected trade), the
#372-class lesson: fixture-green alone is not acceptance for a mount.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 11:49:13 +02:00

160 lines
6.5 KiB
C#

using System.IO;
using AcDream.App.UI.Layout;
using DatReaderWriter;
using DatReaderWriter.Options;
namespace AcDream.App.Tests.UI.Layout;
/// <summary>
/// 2026-08-14 gate probe (the ACDREAM_PROBE_LIVE_MOUNT pattern): dumps the
/// authored per-state strings of the floaty powerbar layout (0x21000072 —
/// gmPowerbarUI's mode states 0x10000042 jump / 0x10000043 melee /
/// 0x10000044 missile / 0x10000045 DDD, RecvNotice_BeginPowerbar
/// @ 0x004DA730) and the combat panel's Power label (0x21000073 element
/// 0x10000052, root states 0x10000003 melee / 0x10000004 missile) from the
/// INSTALLED DAT, so the "Height"/"Power"/"Accuracy" fix binds exactly what
/// retail authors instead of guessing.
/// </summary>
public sealed class PowerbarLayoutProbeTests
{
[Fact]
public void ProbePowerbarAuthoredStrings()
{
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);
var strings = new DatStringResolver(dats);
Dump(dats, strings, JumpPowerbarController.LayoutId, "floaty-powerbar");
Dump(dats, strings, CombatUiController.LayoutId, "combat-panel");
}
/// <summary>2026-08-14 friends gate: dump the friends row TEMPLATE tree
/// (ListBox 0x10000517's authored template list) so the status cell binds
/// the exact authored element instead of a guess.</summary>
[Fact]
public void ProbeFriendsRowTemplate()
{
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);
var strings = new DatStringResolver(dats);
ElementInfo? root = LayoutImporter.ImportInfos(
dats, SocialPanelController.HostLayoutId, SocialPanelController.SlotElementId);
Assert.NotNull(root);
ElementInfo? listBox = FindById(root!, 0x10000517u);
Assert.NotNull(listBox);
Console.WriteLine(
$"[pbprobe] friends listbox 0x10000517 templates={listBox!.TemplateList.Count}");
foreach (UiTemplateListEntry template in listBox.TemplateList)
{
Console.WriteLine(
$"[pbprobe] template layout=0x{template.TemplateLayoutId:X8} "
+ $"element=0x{template.TemplateElementId:X8}");
ElementInfo? row = LayoutImporter.ImportInfos(
dats, template.TemplateLayoutId, template.TemplateElementId);
if (row is not null)
DumpElement(strings, row, 1);
}
}
/// <summary>2026-08-14 trade research: locate the LayoutDesc that authors
/// gmSecureTradeUI's element tree (lane A gave element ids 0x10000085-8B
/// but not the layout id) and dump it.</summary>
[Fact]
public void ProbeSecureTradeLayout()
{
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);
var strings = new DatStringResolver(dats);
int scanned = 0;
for (uint layoutId = 0x21000000u; layoutId <= 0x210001FFu; layoutId++)
{
ElementInfo? root = LayoutImporter.ImportInfos(dats, layoutId);
if (root is null)
continue;
scanned++;
if (FindById(root, 0x10000088u) is null)
continue;
Console.WriteLine($"[pbprobe] SECURE TRADE LAYOUT = 0x{layoutId:X8}");
DumpElement(strings, root, 0);
}
Console.WriteLine($"[pbprobe] scanned {scanned} layouts");
}
private static ElementInfo? FindById(ElementInfo element, uint id)
{
if (element.Id == id) return element;
foreach (ElementInfo child in element.Children)
if (FindById(child, id) is { } found)
return found;
return null;
}
private static void Dump(
DatCollection dats,
DatStringResolver strings,
uint layoutId,
string label)
{
ElementInfo? root = LayoutImporter.ImportInfos(dats, layoutId);
if (root is null)
{
Console.WriteLine($"[pbprobe] {label} 0x{layoutId:X8}: import FAILED");
return;
}
Console.WriteLine($"[pbprobe] === {label} 0x{layoutId:X8} ===");
DumpElement(strings, root, 0);
}
private static void DumpElement(DatStringResolver strings, ElementInfo e, int depth)
{
string indent = new(' ', depth * 2);
Console.WriteLine(
$"[pbprobe] {indent}0x{e.Id:X8} type={e.Type} ({e.X},{e.Y} {e.Width}x{e.Height}) "
+ $"defaultState=0x{e.DefaultStateId:X8}('{e.DefaultStateName}') states={e.States.Count} "
+ $"hjustify={e.HJustify} fontColor={(e.FontColor is { } fc ? fc.ToString() : "none")}");
foreach (var (stateId, state) in e.States)
{
string text = "";
if (state.Properties.Values.TryGetValue(0x17u, out var p)
&& p.Kind == UiPropertyKind.StringInfo)
{
text = strings.Resolve(p.StringInfoValue) ?? "<unresolved>";
text = $" text='{text}'";
}
string properties = string.Join(
" ",
state.Properties.Values
.OrderBy(static pair => pair.Key)
.Select(static pair =>
$"0x{pair.Key:X2}:{pair.Value.Kind}=0x{pair.Value.UnsignedValue:X}"));
Console.WriteLine(
$"[pbprobe] {indent} state 0x{stateId:X8} '{state.Name}'"
+ $" passToChildren={state.PassToChildren}{text} props[{properties}]");
}
foreach (ElementInfo child in e.Children)
DumpElement(strings, child, depth + 1);
}
}