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>
121 lines
5.1 KiB
Markdown
121 lines
5.1 KiB
Markdown
# 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.
|