diff --git a/docs/plans/2026-08-21-contract-tracker-campaign.md b/docs/plans/2026-08-21-contract-tracker-campaign.md index a02b7a12..66e7623e 100644 --- a/docs/plans/2026-08-21-contract-tracker-campaign.md +++ b/docs/plans/2026-08-21-contract-tracker-campaign.md @@ -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 diff --git a/tools/LayoutDump/Program.cs b/tools/LayoutDump/Program.cs index a64340d2..9ddb6cdf 100644 --- a/tools/LayoutDump/Program.cs +++ b/tools/LayoutDump/Program.cs @@ -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}"); } }