acdream/docs/plans/2026-08-21-journal-campaign.md
Erik 57084b5372 docs(quest): Campaigns QT and QJ closed user-accepted
The Journal panel is complete: contracts from the server, a per-character
notebook, and its searchable index.

The gate ledger is recorded rather than smoothed over, because the pattern is
the point — four rounds, and every defect was one mistake wearing different
clothes: an element bound as the wrong thing, or a binding never tested. One
reported failure was not a defect at all; the character was indoors, where
retail records nothing either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 18:31:25 +02:00

5.1 KiB

Campaign QJ — the Journal and Page List tabs

Status: CLOSED USER-ACCEPTED 2026-08-21. All five slices landed and the connected gate passed.

The gate took four rounds, and every defect it found was the same mistake in a different place — an element bound as the wrong thing, or a binding never tested:

  1. Button property 0x0D read as "starts disabled", which killed every button on the panel (register QJ-2).
  2. The location readout is authored EDITABLE, so it is a UiField; bound as UiText it silently discarded every write — the value reached the model and the file and never the screen.
  3. Handlers deferred their redraw to the next frame's Tick where retail redraws at the click.
  4. The timer's unit labels stayed visible behind the running readout, because retail's ShowEditableTimer toggles each box AND its label.

Round 3's "Record does nothing" turned out not to be a defect at all: the character was indoors, where retail's own gid_to_lcoord fails and nothing is recorded. Faithful, and now commented so it does not read as a gap.

The durable outcome is JournalPanelLiveBindTests — see claude-memory/feedback_test_the_binding_seam.md. 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.