staged-item trading marker - Cancel text: ClientTradeSystem::Handle_Trade__Recv_CloseTrade @0x0056DE30 shows "The trade has been cancelled." UNCONDITIONALLY (every close reason) as 0x1A ClientLocal - the yellow top-center SpewBox line. Wired at the router's onTradeClose beside ApplyClose; the string lives in ClientTextRefusals with its citation. - Staged-item marker: retail's mechanism decoded end-to-end - the UIItem prototype (catalog 0x21000037) authors overlay child 0x10000438 (sprite 0x06001DAE, the green frame + corner trade icon), bound @0x004E18FC and SetVisible(tradeState != 0) @0x004E2420; gmSecureTradeUI::AddItem @0x004CA801 sets ACCWeenieObject::SetTradeState(1) on YOUR staged items. Ported as: UiItemSlot.ShowTradeOverlay + TradeOverlaySprite (drawn over the icon), set on the trade window's self-grid cells; and RuntimeTradeState now borrows the canonical object table and maintains ClientObject.TradeState (1 at stage, 0 at remove/failure/ reset/close/clear) - which also brings the ALREADY-PORTED placement policy's "You cannot move an item while it is being traded" refusal to life (its input field previously had no live producer). Runtime 1,626, App 4,992/3 - green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
227 lines
9.4 KiB
C#
227 lines
9.4 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");
|
|
}
|
|
|
|
/// <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");
|
|
}
|
|
}
|
|
|
|
/// <summary>2026-08-14 trade gate round 4: dump the shared UIItem
|
|
/// catalog 0x21000037 tree — find the trading-overlay art (retail marks
|
|
/// staged cells via ACCWeenieObject::SetTradeState @ 0x004CA801; the
|
|
/// UIItem draw reads it).</summary>
|
|
[Fact]
|
|
public void ProbeUiItemCatalog()
|
|
{
|
|
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, 0x21000037u);
|
|
Assert.NotNull(root);
|
|
DumpElement(strings, root!, 0);
|
|
}
|
|
|
|
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);
|
|
string media = string.Join(
|
|
" ",
|
|
e.StateMedia
|
|
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
|
|
.Select(static pair => $"'{pair.Key}'=0x{pair.Value.File:X8}"));
|
|
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")} "
|
|
+ $"media[{media}]");
|
|
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);
|
|
}
|
|
}
|