acdream/docs/plans/2026-08-21-journal-campaign.md
Erik e35d9386e4 fix(journal): the location readout is a FIELD, refresh at the click, and Abandon works
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>
2026-08-21 16:19:18 +02:00

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. JournalPage plus 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

  1. A page written in one session is there in the next.
  2. Every ported algorithm cites its retail address.
  3. Every slice has a test that would catch its regression.