docs(quest): QT5 is specified — and it is the Journal panel, not a contract one

Measuring the host layout rather than assuming changed what this slice is.
gmContractsUI is not a panel of its own: it is tab 1 of a THREE-tab "Journal"
panel at gmPanelUI slot 25, beside a notes page and a page list. Building it as
a standalone window would have produced something retail does not have, and
the mistake would only have surfaced at a visual gate.

The other two tabs are out of scope, so the expected intermediate state is a
panel with two dead tabs — recorded here so it is not filed as a defect.

Everything else the page needs is now measured out of the dats: every authored
label, the per-row child ids RefreshContractListbox writes, and the list's
scrollbar link. The list is a UiTemplateListBox, the widget OP2 already built,
so the page is binding work rather than new widget work.

LayoutDump --props now resolves StringInfo through DatStringResolver instead of
printing the type name, which is how the labels were read at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-21 14:56:46 +02:00
parent ef6b7310c5
commit fac2dc7248
2 changed files with 70 additions and 5 deletions

View file

@ -99,6 +99,49 @@ Three things a reimplementation would get wrong:
3. **`DescriptionProgress` is a printf format** taking one integer, `stage - 4`.
It is not a literal string.
### It is not a "contract panel" — it is tab 1 of the JOURNAL panel
Measured from the installed dats. Host `0x2100006E`, `gmPanelUI` slot
`0x10000559`, whose own authored `0x10000029` is **`0x19` = 25** — the same
slot-key recipe `RetailPanelCatalog` already uses for Options (10), the social
panel (12) and Map/House (16). Three tabs:
| Tab | Caption | Page | Page type |
|---|---|---|---|
| `0x100005D3` | **Contracts** | `0x100005D4` | `0x1000004B` = `gmContractsUI` |
| `0x10000560` | **Journal** | `0x10000563` | `0x10000048` — notes: "Title:", "Notes:", "First" |
| `0x10000561` | **Page List** | — | — |
`0x10000562` (type 1, at 276,0) is the panel's own corner button.
Only the Contracts tab is in scope. The Journal notes page and Page List are
their own feature and are NOT part of Campaign QT — mounting the panel with two
dead tabs is the expected intermediate state, not a defect.
### The contracts page, resolved
Authored text read out of the dats (`LayoutDump --props`, which now resolves
`StringInfo` rather than printing the type name):
| Element | Role |
|---|---|
| `0x100005CE` / `0x100005D6` | list column headers — "Contract" / "Status" |
| `0x100005CF` (type 5) | the list, scrollbar `0x100005D0` via property `0x72` |
| `0x100005D1` / `0x100005D2` | per-ROW children: contract name / progress text |
| `0x100005D8``0x100005DF` | "Status:" → value |
| `0x100005D9``0x100005E0` | "Contact:" → value |
| `0x100005DA``0x100005E1` | "Contact Location:" → value |
| `0x100005DB``0x100005E2` | "Quest Location:" → value |
| `0x100005DE` | description block (270x52, wrapping) |
| `0x100005DD``0x100005E3` | "Timed:" → value |
| `0x100005DC` | "Abandon" button |
`gmContractsUI::RefreshContractListbox @0x00499830` walks the tracker list and,
per row, sets `0x100005D1` from the contract's name and `0x100005D2` from
`FillProgressString`, caching the result back into the row. The list is a
`UiTemplateListBox` here — the same widget OP2 built for the Options panel — so
the page is binding rather than new widget work.
## Slices
- **QT1 — wire.** Typed records + parsers for `0x0314`/`0x0315`, arrival stamp
@ -111,10 +154,18 @@ Three things a reimplementation would get wrong:
Clears at generation reset.
- **QT4 — the progress string.** Port `FillProgressString` + the retail
`DeltaTimeToString` it calls. Table-driven tests over every stage arm.
- **QT5 — the panel.** Mount `0x21000069` by the OP3/FA recipe; list, scrollbar,
the four label/value rows, description, buttons.
- **QT6 — open/close.** Whatever raises it in retail, plus the plugin-visible
read surface from `r10-quest-dialogs.md` §11.6.
- **QT5 — the panel.** Register slot 25 in `RetailPanelCatalog`, mount the
Journal panel by the OP3/FA recipe, and bind the Contracts page: rows from
`IRuntimeContractView` x `ContractCatalog`, progress from QT4, selection
driving the detail pane. The other two tabs mount empty.
- **QT6 — open/close.** The open path (no toolbar button authors slot 25, so
it is keyboard or menu — to be measured the way FA's F3/F4 was), plus the
plugin-visible read surface from `r10-quest-dialogs.md` §11.6.
### Landed so far
QT1 `ab3934e2` (wire), QT3 `f629ce7f` (state + routing), QT2/QT4 `ef6b7310`
(catalog + progress string). QT5 and QT6 are open.
## Definition of done

View file

@ -163,6 +163,8 @@ if (findAt >= 0)
}
}
var stringResolver = new DatStringResolver(adapter);
ElementInfo? root = ids.Length > 1
? LayoutImporter.ImportInfos(adapter, ids[0], ids[1])
: LayoutImporter.ImportInfos(adapter, ids[0]);
@ -326,13 +328,25 @@ void Print(ElementInfo e, int depth)
.OrderBy(kv => kv.Key)
.Select(kv => $"0x{kv.Key:X2}={Describe(kv.Value)}"));
static string Describe(UiPropertyValue v) => v.Kind switch
string Describe(UiPropertyValue v) => v.Kind switch
{
UiPropertyKind.Bool => v.BoolValue.ToString(),
UiPropertyKind.Integer => v.IntegerValue.ToString(),
UiPropertyKind.Enum => $"0x{v.UnsignedValue:X}",
// An authored StringInfo is a table id + string id, which says
// nothing on its own -- resolve it, because "what does this
// label SAY?" is the whole reason to dump properties.
UiPropertyKind.StringInfo => DescribeString(v.StringInfoValue),
_ => v.Kind.ToString(),
};
string DescribeString(UiStringInfoValue info)
{
string? resolved = stringResolver.Resolve(info.TableId, info.StringId);
return !string.IsNullOrEmpty(resolved)
? $"\"{resolved}\""
: $"StringInfo(table=0x{info.TableId:X8}, id={info.StringId})";
}
Console.WriteLine($"{pad} state {stateId}: props {ids}");
}
}