Pokered Save Editor 2
Pokemon Red & Blue save file editor - Qt 6 C++/QML
Loading...
Searching...
No Matches
2026-06-13 — Session Log

A heavy day: comprehensive testing infrastructure, build/CI infrastructure (Linux Docker, SemVer, i18n), the Credits/About redesign, the Rival screen cleanup, and three real crash fixes found by the new tests.

Infrastructure: SemVer version system (single source of truth)

A real SemVer pipeline with a single source of truth — repo-root VERSION (0.7.0-alpha). CMake reads it (projects/cmake/PSEVersion.cmake), feeds project(VERSION), and generates pse_version.h (into <build>/generated/) from cmake/pse_version.h.in, appending the git commit as SemVer build metadata (0.7.0-alpha+g<hash>[.dirty]) via a build-time pse_version_gen target. boot.cpp now sets the runtime version from PSE_VERSION_FULL (was hardcoded "v1.0.0"); About.qml shows the cleaned v<SemVer>. A Windows VERSIONINFO resource (app/pse_app.rc.in) embeds the version in the .exe. Start number 0.7.0-alpha derived from project maturity. Full ctest 66/66 green. To bump: edit one line in VERSION + reconfigure. See ../../reference/versioning.md.

Infrastructure: i18n (Qt Linguist) pipeline

UI strings wrapped in qsTr()/tr(), per-locale catalogs in projects/app/translations/pse_<locale>.ts, compiled to .qm and embedded at :/i18n by qt_add_translations, with a QTranslator installed early in boot.cpp (locale from a ui/language setting, else system locale; untranslated → en_US source fallback). Source language is en_US. Covers UI chrome only — game-data names are out of scope. Initial pse_en.ts = 200 messages / 42 contexts. Language switching is deferred (a design decision) until a second locale + an Options screen exist; ui/language is the hook. (The apparent "lupdate hang" was stacked UAC dialogs, not a tooling issue.) Full guide: ../../reference/i18n.md; decision in ../../decisions/architecture.md.

Infrastructure: Linux Docker build/test env (all variants green)

A containerized Linux build/test setup under docker/ (Dockerfile + run-tests.sh + dtest.ps1 wrapper). Mirrors the kit (Qt 6.11 + clang), bind-mounts the repo read-only and rsyncs it into a persistent ext4 volume (fast FS + ccache), and exposes four variants: standard (offscreen ctest), asan (AddressSanitizer+UBSan — the thing that can't run on the Windows kit), xvfb (real virtual X server), and coverage (llvm-cov). First run: all four green — 66/66 ctest each; ASan/UBSan clean; coverage 89.73% line. Run with .\docker\dtest.ps1 [variant]. Details: ../../plans/testing.md → "Local Linux container (Docker)" and docker/README.md. Docker added to the credits (Tools Used).

Infrastructure: save-fixture corpus reorganised + repo-root tmp/ scratch

All test saves now live under assets/saves/ in three categories — natural-clean/ (game-generated: BaseSAV.sav, BaseSAV.new.sav), synthetic-clean/ (engine-built well-formed, was assets/synthetic/), synthetic-dirty/ (malformed/garbage, was assets/dirty/). The byte-map oracle moved to assets/saves/structure.bt and gained a human-readable companion structure.md (now in the Doxygen docs). All PSE_ASSETS_DIR-relative paths updated. The repo-root icon staging folder was renamed assets/icons/assets/staging/ (gitignored scratch). A tracked-but-ignored tmp/ now exists at the project root for transient files (build/run artifacts, logs, generated outputs) — same * + !.gitignore pattern as assets/staging/.

Testing: QML screen smoke test (tst_qml_screens)

projects/tests/qml/tst_qml_screens.cpp loads EVERY registered screen (from Router::loadScreens()) through a real QQmlEngine wired exactly like MainWindow (brg over the BaseSAV fixture, the tileset/font image providers, DB::qmlProtect, bootQmlLinkage(), app.qrc + the Material qtquickcontrols2.conf). Per screen it fails on any component load error AND on any qWarning/qCritical emitted while instantiating into a sized parent + running Component.onCompleted — i.e. FINAL-overrides, "Component is not ready", binding TypeErrors, missing types/providers, anchor-on-null. Runs headless (offscreen). main is now gated on it (CLAUDE.md default workflow step 2) — the C++ ctest suite never instantiates QML, so it can't catch this class (which is what let a non-opening Credits screen reach main).

Testing: comprehensive GUI suite (real-app, headless, both-platform CI)

A first full pass of true GUI/interaction testing on a shared harness projects/tests/helpers/guiapp.h, which boots the REAL app (App.qml + AppWindow + both StackViews + the live C++ Router) into an offscreen QQuickView, with navigation / item-find / real input synthesis / QML-warning-capture / save-reopen helpers. Tests (all offscreen, gating BOTH CI jobs): tst_gui_navigation (drive the Router through every screen on populated + new saves; correct push/pop + zero QML warnings), tst_gui_saveload (edit trainer card/bag/party-mon THROUGH the live screens → Save-As → reopen → assert persisted; cross-file independence; app-saved-file byte-stability), tst_gui_input (real key events into the money field → commit → persists).

Built + run + green on the Qt 6.11 kit — full ctest 61/61. First-run triage: tst_gui_input no longer skips (added objectName: "trainerMoneyField"); harness fixes in guiapp.h (keyType() for the QWindow keyClick overload; appBody() matches the StackView_QMLTYPE_N substring; navigate()/closeTop() wait on the StackView busy transition; benign offscreen font warning allowlisted); tst_gui_navigation accumulates all problem screens.

Testing: second comprehensive pass ("I really need good comprehensive testing")

(1) tst_gui_fidelity — the sacred byte-fidelity promise as a test: browse heavily (all screens, every dropdown + modal, focus/blur every field — NO edits) → flatten → assert byte-identical (over progressed + new + a synthetic maxed save). Later extended to open the Pokémon editor over a real party mon (via GuiApp::instantiate() with boxData) and browse it non-destructively — every tab (9), dropdown (9), field (22) — asserting byte-identical. (2) synthetic-dirty/ + tst_dirty_fixtures — malformed saves (empty/truncated/oversized/all-00/all-FF/garbage + checksum-flip) with a negative suite proving graceful degradation. (3) synthetic-clean/ + gen_synthetic_fixtures + tst_synthetic_fixtures — engine-generated deterministic edge-case saves (maxed/zeroed/allbadges/midgame, built FRESH so no real data), each load + round-trip byte-stable. Full ctest 64/64.

Testing: shortcuts + drag E2E flows

(1) tst_shortcuts — shortcut key sequences AND action→verb wiring extracted into app/src/boot/shortcutdefs.h (pse::shortcutKeyMap() + pse::shortcutActions()); MainWindow::setupShortcuts() builds from both (the auto os = otherShortcuts copy bug that left the member empty is fixed). The test pins every binding, proves none collide, asserts every shortcut has an action, AND fires the safe/non-dialog verbs. (2) tst_gui_drag — E2E drag flows through the live bridge models (bag item reorder/transfer/delete + PC-box Pokémon reorder/delete), each driven via the real Q_INVOKABLEs the QML drag calls, then save → reopen → asserted persisted. Full ctest 66/66.

Bug: corrupt/garbage save (all-0xFF) crashed on load

expanded/storage.cpp Storage::load()PokemonStorageSet::loadSpecific. The current-box index (save byte 0x284C & 0x7F, 0..127) was used to index boxes[] (valid 0..11) with no range check → a malformed save (127 → boxes[121]) crashed (null deref). Clamp out-of-range curBox to 0 on load — a no-op for valid saves (byte fidelity preserved), graceful default for garbage. Found by tst_dirty_fixtures. See ../../reference/fix-patterns.md.

Bug: opening the Pokemart screen aborted (empty-cart at(0) assert)

mvc/itemmarketmodel.cpp. The three aggregate accessors (moneyLeftover()/totalCartWorth()/ canAnyCheckout()) read itemListCache.at(0) with no empty guard; the Pokemart footer bindings evaluate during StackView.push before the model's pageOpening builds the list → empty list → Q_ASSERTqFatal. Root-cause fix: buildList() now runs in the ctor (model valid from construction); plus the accessors return sane empties as defense-in-depth. Found by tst_gui_navigation (approved: "guards + fix the problem"). See ../../reference/fix-patterns.md.

Bug: PlayerBasics::badgeCount() was a stub returning 8 always

Now counts the set badges. Found by the synthetic fixtures (allbadges/zeroed disagreed with the count). See ../../reference/fix-patterns.md.

Credits / About screen redesign + data-driven back end

Reworked the About/Credits modal end-to-end while keeping what worked (wallpaper, categories, font-size variation). (1) Data: credits.json restructured from an object-of-arrays into an ordered { "sections": [ {section, entries:[…]} ] } list — section ORDER now lives in the JSON; adding a credit or section is a pure JSON edit. Entries preserved verbatim (incl. the original wording/typos). (2) Back end: CreditDBEntry::process() is now a single data-driven loop over sections (was 8 hardcoded blocks); the store stays flat (section-header sentinel + entries) so the DB-entry pattern and every consumer/test is unchanged. (3) Model: CreditsModel is now section-grouped (one row per category with roles section + entries). (4) UI (About.qml): each category renders as a soft translucent rounded card over the wallpaper, with a section icon (reused Font Awesome svgs colorized to primaryColor) + heading + divider + entry Repeater; clickable URLs (<a>Qt.openUrlExternally); a "Credits" title + warm intro; a version + copyright footer ("© 2017–2026 Twilight • Apache License 2.0"). Convention: ../../reference/ui-patterns.md → "Credits / About screen".

Credits screen — two QML-load bugs found + fixed (verified on-screen)

The first build wouldn't open in-app even though all 57 C++ tests passed (the unit tests never instantiate QML). Both caught only by launching + reading stderr. (1) Page.contentWidth is FINALAbout.qml declared readonly property int contentWidth on its Page root → "Cannot override FINAL property" → "Component is not ready" → push failed → screen never opened. Renamed to colWidth. (2) id: top collides with the top anchor line — inside delegates with anchors, bare top resolved to a QQuickAnchorLine, so top.sectionIcon()/top.linkHref() threw TypeError. Renamed the Page id to root. Process lesson (valid): these are exactly what a QML-load smoke test catches, and FF-ing main on green C++ tests alone let a non-opening screen reach mainmain is now gated on tst_qml_screens. Both fixes in ../../reference/fix-patterns.md.

Rival screen cleanup — matched to the Trainer Card layout

screens/non-modal/Rival.qml was messy — it positioned name/image/starter with a 1×1 transparent anchor Rectangle and magic negative margins, and hand-rolled the starter ComboBox inline. Rewrote it to mirror CardFront: a centered bordered card, a shared fieldH: 28 knob, and a divider Spacer under the name — elements anchored below one another. kept the simple vertical stack (name → image → starter). Behavior/bindings unchanged; all model access routes through a null-guarded rival() helper. Artwork swap: repointed the Rival Image to the new colored qrc:/assets/icons/rival.png (mirrors the trainer.png swap); imported assets/staging/rival.png into projects/app/assets/icons/. FF'd to main. Credited: the rival artwork is ChatGPT's (added to the AI Assistance + Icons sections of credits.json). Standing ask: keep credits/sources current proactively.

gen1: source-verified box format/recovery findings

Box-format triggers (box-switch / new-file only) + recovery-opportunity findings were source-verified against the pret/pokered disassembly and written into ../../reference/gen1-knowledge.md.