Three fixes from the first connected round. The location readout is authored EDITABLE (0x16), so it builds as a UiField — not the UiText its "00.0S, 00.0W" placeholder suggests. The controller resolved it as text, got null, and threw every write away in silence: Record reached the model and reached the FILE, and never reached the screen. That is exactly what was reported, and it is a whole class of bug, so the sweep that found it is now a test over every element all three controllers bind. The handlers mutated the model and left redrawing to the next frame's Tick. Retail's ListenToElementMessage @0x004968D0 ends every one of them in Update() instead — at the moment of the click. The deferred version happened to work in the client and made the behaviour untestable and a frame late; the notes-page tests I had not written until now fail against it. Abandon is wired. "Retail's abandon path is a contract-registry command we have not ported" was wrong — it is game action 0x0316 with a single contract id, and ACE replies with the 0x0315 delete QT3 already handles. Nothing is removed locally, so a refusal leaves the quest visibly intact rather than vanishing it optimistically and having it reappear on the next full table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4.2 KiB
Campaign QJ — the Journal and Page List tabs
Status: CODE-COMPLETE 2026-08-21. All five slices landed, plus the first
connected round's three fixes (button property 0x0D, the location readout's
widget type, refresh-at-the-click). Abandon is now wired too — it turned out to
have a real wire action after all. The connected re-gate is owed. Completes the panel Campaign QT mounted: QT
shipped the Contracts tab and left the other two inert by design.
Scope: retail's gmJournalUI (element type 0x10000048, page
0x10000563) and gmPageListUI (type 0x10000049, page 0x10000564).
What this actually is
A per-character notebook, entirely client-side. No wire, no server involvement, no dat content — the player writes the pages. Each page carries a label, a title, free-form notes, a recorded LOCATION, and a countdown TIMER. The Page List tab is a searchable index over those pages.
Nothing about it depends on quests; it shares the panel with Contracts and nothing else. That it is called "Journal" while the panel is also called "Journal" is retail's own naming, not a mistake here.
Measured ground truth
The file format
gmJournalUI::SavePages @0x00497270 / LoadPages @0x00496AC0. A plain tagged
text file, fopen mode w+. Both call sites pass the literal prefix
"Journal"; the path template is %s%s-%s-%s.txt, i.e.
{dir}Journal-{server}-{character}.txt.
<NEWP> begins a page (a file that does not open with one is refused)
<PNUM> %d page number
<LABE> %s label (authored max length 16)
<TITL> %s title (32)
<NOTE> %s notes (2048)
<DAYS> %d timer days
<HOUR> %d timer hours
<MINU> %d timer minutes
<LOCX> %f recorded location
<LOCY> %f
<TIME> %f running-timer value
Retail's own load error, byte-decoded: "Problem loading journal: Your journal file does not create a new page!"
The Journal page (0x10000563)
| Element | Role |
|---|---|
0x10000567 |
"New" button |
0x10000569 |
label edit box (0x1E = 16) |
0x1000056A / 0x1000056B |
"Title:" / title edit box (32) |
0x1000056C / 0x1000056D |
"Notes:" / notes edit box (2048), scrollbar 0x1000056E |
0x1000056F / 0x10000570 / 0x10000571 |
"First" / "~ 1 ~" / "Last" |
0x10000572 / 0x10000573 / 0x10000574 |
"Location:" / "00.0S, 00.0W" / "Record" |
0x10000575 |
"Timer:" |
0x10000576 0x10000577 |
days field, "d" |
0x10000578 0x10000579 |
hours field, "h" |
0x1000057A 0x1000057B |
minutes field, "m" |
0x1000057C |
running-timer text — OVERLAPS the three fields at x=84 |
0x1000057D |
"Start" button |
0x10000566 |
bottom-right button (65x32) |
0x1000057C sharing x=84 with the day/hour/minute fields is the authored form
of ShowEditableTimer @0x00495770 vs ShowRunningTimer: the same strip is
either three editable numbers or one running readout, never both.
The Page List page (0x10000564)
| Element | Role |
|---|---|
0x1000057F 0x10000580 0x10000581 0x10000582 |
headers "#" / "Title" / "Timer" / "Label" |
0x10000583 |
the list, scrollbar 0x10000584 |
0x10000585 |
"Delete" |
0x10000586 / 0x10000587 / 0x10000588 |
"Search:" / search box / "Reset" |
gmPageListUI::PageContainsString @0x00493B60 is the search predicate;
CheckForDoubleClick @0x00493140 opens the page
(gmJournalUI::GotoPage @0x00496430).
Slices
- QJ1 — the page model and its file.
JournalPageplus a faithful reader/writer for the tagged format, including retail's refusal of a file that does not open with<NEWP>. Pure; no UI, no state ownership. - QJ2 — the owner.
RuntimeJournalState: the page collection, the current page, new/delete/goto, and the timer. Per-character. - QJ3 — the Journal page. Edit boxes, page navigation, Record, and the editable/running timer swap.
- QJ4 — the Page List page. The list, the search, delete, and double-click-to-open.
- QJ5 — persistence. Load on character enter, save on exit, under the client's own data directory.
Definition of done
- A page written in one session is there in the next.
- Every ported algorithm cites its retail address.
- Every slice has a test that would catch its regression.