acdream/docs/plans/2026-08-21-journal-campaign.md
Erik c5cc8ae5fc feat(journal): QJ3/QJ4/QJ5 — both remaining tabs are live
The Journal panel now has all three tabs working: contracts from the server,
and a per-character notebook with its searchable index.

Two ported details that a reimplementation would get wrong in a way nobody
notices until they lose work:

Every navigation button commits the current page FIRST. Retail's
ListenToElementMessage @0x004968D0 calls SaveThisPage on the way out of all
five of them, which is why paging away never eats what you just typed. And the
file is written when the notes page is HIDDEN, not only at exit — a crash then
costs at most the page in front of you.

The search is CASE-SENSITIVE across label, title and notes: retail compares
with wcsstr and lowercases neither side. Making it insensitive would be
friendlier and would be a divergence, so it is ported as-is with a test naming
the reason. The double-click window is a full SECOND (m_LastClickTime + 1.0,
@0x00493158) rather than the 500 ms the item-interaction path uses, and firing
it clears the tracker so a third click does not re-open.

Two unlabelled buttons on the notes page turned out to be prev/next: retail
switches on (idElement - 0x10000565), which names them without a caption. The
running-timer readout is authored at the same x as the three day/hour/minute
boxes, so the strip is one or the other — that overlap is the data form of
ShowEditableTimer versus ShowRunningTimer, not a layout bug.

DeltaTimeToString moved out of the contract code into AcDream.Core.Ui. It is
ClientUISystem's, not gmContractsUI's — the journal timer and the contract
repeat countdown both call it, and it only lived under Quests because that was
its first caller. A bridge class to reach it across features would have been
the wrong answer to the same observation.

The journal file lives in the client's data directory rather than beside the
executable, for the same reason the chat log does. Register QJ-1.

Campaign QJ slices 3, 4 and 5 of 5 — code-complete, connected gate owed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 15:42:05 +02:00

100 lines
4 KiB
Markdown

# Campaign QJ — the Journal and Page List tabs
**Status:** CODE-COMPLETE 2026-08-21. All five slices landed; the connected
user 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.