The quest log is on screen. Rows come from the live tracker joined to the authored catalog, the Status column runs QT4's port of FillProgressString, and the detail pane shows contact, locations, description and the other timer. Two things measured rather than assumed, each now pinned by an installed-DAT test rather than left to the commit message: The tab pairing is read from the authored 0x2E table, not inferred from x-order — the FA campaign had to correct exactly that mistake, and Contracts turns out to be the authored DEFAULT tab (0x32 = True), so opening on the wrong one would have looked like an empty panel. The open path needed no keybind at all. Toolbar button 0x1000055A authors 0x10000029 = 0x19 and has been sitting in ToolbarController.PanelButtonIds since the toolbar was ported — it just had no panel behind it, so clicking it did nothing. Registering slot 25 finished a wiring that was already three-quarters present. The list rebuild is revision-gated while the repeat countdown is not: nothing on the wire changes as a cooldown runs down, so a rebuild-gated timer would freeze on screen, and a per-frame rebuild would reset the player's scroll under them. Both directions have a test. Deliberately inert: the Abandon button (retail's abandon path is a contract-registry command this campaign did not port — authored and visible, but wiring a no-op handler would look responsive and lie), and the Journal notes and Page List tabs, which are their own feature. Campaign QT slices 5 and 6 of 6 — code-complete, connected gate owed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
186 lines
6.4 KiB
C#
186 lines
6.4 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using AcDream.App.UI;
|
|
using AcDream.App.UI.Layout;
|
|
using AcDream.Content;
|
|
using DatReaderWriter;
|
|
using DatReaderWriter.Options;
|
|
|
|
namespace AcDream.App.Tests.UI.Layout;
|
|
|
|
/// <summary>
|
|
/// Campaign QT slice QT5: pins the authored facts the Journal panel is built
|
|
/// on, against the live installed DATs rather than a committed fixture.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// These are the facts a reader would otherwise have to take on trust from a
|
|
/// commit message: the panel's slot key, its three-tab table, and which tab is
|
|
/// the authored default. The FA campaign had to CORRECT a tab pairing that had
|
|
/// been inferred from x-order, which is why the pairing is asserted here from
|
|
/// the authored <c>0x2E</c> table itself.
|
|
/// </remarks>
|
|
[Trait("Lane", "InstalledDat")]
|
|
public sealed class JournalPanelSlotProbeTests
|
|
{
|
|
private const uint SlotKeyPropertyId = 0x10000029u;
|
|
private const uint TabTablePropertyId = 0x2Eu;
|
|
private const uint TabButtonPropertyId = 0x30u;
|
|
private const uint TabPagePropertyId = 0x31u;
|
|
private const uint TabDefaultPropertyId = 0x32u;
|
|
|
|
private const uint ToolbarLayoutId = 0x21000016u;
|
|
private const uint JournalToolbarButtonId = 0x1000055Au;
|
|
|
|
private static ElementInfo Import(uint layoutId, uint rootElementId = 0u)
|
|
{
|
|
string? datDir = ContentConformanceDatDir();
|
|
if (datDir is null)
|
|
{
|
|
Assert.Fail(
|
|
"Lane=InstalledDat requires an installed retail DAT directory; "
|
|
+ "see docs/release-gate.md.");
|
|
}
|
|
|
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
|
using var adapter = new DatCollectionAdapter(dats);
|
|
ElementInfo? info = rootElementId == 0u
|
|
? LayoutImporter.ImportInfos(adapter, layoutId)
|
|
: LayoutImporter.ImportInfos(adapter, layoutId, rootElementId);
|
|
Assert.NotNull(info);
|
|
return info!;
|
|
}
|
|
|
|
private static string? ContentConformanceDatDir()
|
|
{
|
|
string? fromEnv = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
|
|
if (!string.IsNullOrWhiteSpace(fromEnv) && Directory.Exists(fromEnv))
|
|
return fromEnv;
|
|
|
|
string def = Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
|
"Documents", "Asheron's Call");
|
|
return Directory.Exists(def) ? def : null;
|
|
}
|
|
|
|
private static UiPropertyValue? Property(ElementInfo info, uint propertyId)
|
|
{
|
|
foreach (UiStateInfo state in info.States.Values)
|
|
{
|
|
if (state.Properties.Values.TryGetValue(propertyId, out UiPropertyValue? value))
|
|
return value;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
[Fact]
|
|
public void TheSlotAuthorsTheCatalogPanelId()
|
|
{
|
|
ElementInfo slot = Import(
|
|
JournalPanelController.HostLayoutId,
|
|
JournalPanelController.SlotElementId);
|
|
|
|
UiPropertyValue? key = Property(slot, SlotKeyPropertyId);
|
|
|
|
Assert.NotNull(key);
|
|
Assert.Equal(RetailPanelCatalog.Journal, (uint)key!.UnsignedValue);
|
|
}
|
|
|
|
[Fact]
|
|
public void TheAuthoredTabTablePairsContractsWithTheGmContractsUiPage()
|
|
{
|
|
// Read from the table, never inferred from x-order.
|
|
ElementInfo slot = Import(
|
|
JournalPanelController.HostLayoutId,
|
|
JournalPanelController.SlotElementId);
|
|
|
|
UiPropertyValue? tabs = Property(slot, TabTablePropertyId);
|
|
Assert.NotNull(tabs);
|
|
Assert.Equal(3, tabs!.ArrayValue.Count);
|
|
|
|
UiPropertyValue first = tabs.ArrayValue[0];
|
|
Assert.Equal(
|
|
0x100005D3u,
|
|
(uint)first.StructValue[TabButtonPropertyId].UnsignedValue);
|
|
Assert.Equal(
|
|
JournalPanelController.ContractsPageId,
|
|
(uint)first.StructValue[TabPagePropertyId].UnsignedValue);
|
|
}
|
|
|
|
[Fact]
|
|
public void ContractsIsTheAuthoredDefaultTab()
|
|
{
|
|
// Opening on the wrong tab would look like the panel is empty.
|
|
ElementInfo slot = Import(
|
|
JournalPanelController.HostLayoutId,
|
|
JournalPanelController.SlotElementId);
|
|
|
|
UiPropertyValue tabs = Property(slot, TabTablePropertyId)!;
|
|
|
|
UiPropertyValue defaultTab = Assert.Single(
|
|
tabs.ArrayValue,
|
|
t => t.StructValue.TryGetValue(TabDefaultPropertyId, out UiPropertyValue? d)
|
|
&& d.BoolValue);
|
|
|
|
Assert.Equal(
|
|
JournalPanelController.ContractsPageId,
|
|
(uint)defaultTab.StructValue[TabPagePropertyId].UnsignedValue);
|
|
}
|
|
|
|
[Fact]
|
|
public void TheToolbarButtonAuthorsTheSamePanelId()
|
|
{
|
|
// The button was already in ToolbarController.PanelButtonIds with no
|
|
// panel behind it, so clicking it did nothing. This is the fact that
|
|
// makes it work rather than a keybind.
|
|
ElementInfo toolbar = Import(ToolbarLayoutId);
|
|
|
|
ElementInfo? button = Find(toolbar, JournalToolbarButtonId);
|
|
Assert.NotNull(button);
|
|
|
|
UiPropertyValue? key = Property(button!, SlotKeyPropertyId);
|
|
Assert.NotNull(key);
|
|
Assert.Equal(RetailPanelCatalog.Journal, (uint)key!.UnsignedValue);
|
|
}
|
|
|
|
[Fact]
|
|
public void TheContractsPageCarriesEveryChildTheControllerResolves()
|
|
{
|
|
// A renamed or missing child would silently leave a blank row rather
|
|
// than fail, because the controller binds defensively.
|
|
ElementInfo slot = Import(
|
|
JournalPanelController.HostLayoutId,
|
|
JournalPanelController.SlotElementId);
|
|
|
|
ElementInfo page = Assert.IsType<ElementInfo>(
|
|
Find(slot, JournalPanelController.ContractsPageId));
|
|
|
|
foreach (uint childId in new[]
|
|
{
|
|
0x100005CFu, // the list
|
|
0x100005DEu, // description
|
|
0x100005DFu, // status value
|
|
0x100005E0u, // contact value
|
|
0x100005E1u, // contact location
|
|
0x100005E2u, // quest location
|
|
0x100005E3u, // timed value
|
|
0x100005DCu, // Abandon
|
|
})
|
|
{
|
|
Assert.True(
|
|
Find(page, childId) is not null,
|
|
$"contracts page is missing authored child 0x{childId:X8}");
|
|
}
|
|
}
|
|
|
|
private static ElementInfo? Find(ElementInfo root, uint id)
|
|
{
|
|
if (root.Id == id) return root;
|
|
foreach (ElementInfo child in root.Children)
|
|
{
|
|
ElementInfo? hit = Find(child, id);
|
|
if (hit is not null) return hit;
|
|
}
|
|
return null;
|
|
}
|
|
}
|