using System.IO; using AcDream.App.UI.Layout; using DatReaderWriter; using DatReaderWriter.Options; namespace AcDream.App.Tests.UI.Layout; /// /// 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. /// 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"); } /// 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. [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); } } /// 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. [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"); } /// 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). [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(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"); } } /// 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). [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) ?? ""; 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); } }