Files
bryanthaboi af47e19e1a Rebuild the launcher and save editor on a small immediate-mode UI kit
The launcher spent ~9ms per frame building and drawing, and the Find Mods
tab could hang the window for minutes. Both had the same root cause: a
retained UI tree rebuilt every frame, and blocking curl calls made from the
draw path.

Replace the vendored FlexLove engine (28.5k lines) with src/ui/kit/ (Kit,
Theme, Layout, Loader). The kit caches Text objects and all measurement,
allocates nothing in the steady state, and draws flat. Build+draw is now
under 1ms at every window size and on every tab (POKEPORT_LAUNCHER_PROF).

Move every network call off the render thread onto a love.thread pool
(src/net/Fetch.lua): mod index fetches, per-mod release checks, find-tab
stats, thumbnails and mod installs. Mod indexes prewarm at boot so the
Find Mods tab is populated before it is opened.

Paginate every list -- mods, find, save slots, settings, release notes,
versions -- with the page size derived from the real viewport height, so a
500-mod index costs what a 10-mod one does. Scrolling is gone.

Anything that waits now raises a non-dismissable loader; per-row background
work shows an inline spinner instead. The in-app updater moves to the top
right beside the settings gear and pulses when an update is waiting.

Theme is black with white outlines, no gradients or glows, and solid
colour-coded embossed buttons with bold labels. The game tabs keep their
cartridge colours. Everything is 1.3x larger. The save editor shares the
theme, and adding an item there is now a searchable pop-up like adding a
Pokemon.

Also:
- Reset rebinds, in Settings and under Touch Controls. Rebinds are additive
  (Input:applyBindings layers them over the defaults), so there was no
  in-game way to undo one.
- Launch options: --game red [--slot N] / POKEPORT_GAME boots straight into
  a game for shortcuts and frontends, falling back to that game's tab when
  its ROM is not imported.

Fixes found while porting:
- Ellipsis and letterspacing truncated bytes, not codepoints, so a
  multi-byte mod name crashed the first frame on a Japanese index.
  Measurement no longer throws on malformed input either.
- The new font set missed UiFont's kana fallback, rendering translated
  builds as tofu.
- Fetch workers idle in Channel:demand() and LOVE waits for live threads at
  exit, so the process outlived the window; quitting mid-download also
  waited on curl's 300s ceiling. Shut the pool down in love.quit and bound
  its transfer timeouts.
- In one column the save-slot card drew below the fold, over the footer,
  with no scrollbar left to reach it.

The two FlexLove engine tests guarded a scroll manager and an auto-height
propagation bug that no longer exist; replace them with a kit suite covering
page bounds, viewport sizing and UTF-8 truncation, and retarget the NX test
to assert the dependency is gone rather than that its perf guards are set.
2026-08-04 15:17:09 -04:00
..
2026-07-17 20:30:02 -04:00
2026-07-19 16:18:18 -04:00

Save Editor

Ships inside every build. Two ways in:

From the launcher. Every SAVE SLOT row that holds a save carries an Edit label next to Delete. Edit suspends the launcher and opens that slot's file; Close hands the process back with the slot list re-read. This is the path most people use, and it is wired in main.lua (openEditor / closeEditor).

Standalone, where Close quits instead:

# from repo root, game closed
love . --editor
# or
POKEPORT_EDITOR=1 love .
# open a specific save (any path)
love . --editor --save "/path/to/save.lua"

By default it loads the game's active save slot under the LÖVE save directory (same identity as the game, deliberately: the editor edits the game's saves and reads the game's ROM cache):

  • macOS: ~/Library/Application Support/LOVE/pokemon-love2d/
  • Linux: ~/.local/share/love/pokemon-love2d/
  • Windows: %APPDATA%\love\pokemon-love2d\

If the file isn't there (or you want another copy), use Open..., drop a save.lua onto the window, or pass --save. Each write makes save.lua.bak-YYYYMMDD-HHMMSS first.

Layout

file role
Theme.lua the launcher's palette + drawing primitives (cards, glow, dashed outlines, letterspaced captions)
Kit.lua immediate-mode widgets built on Theme: buttons, rows, meters, chips, checkboxes, a real text field, pagers
PadInput.lua virtual cursor for Switch / gamepads (stick move, A click, B close, shoulders cycle tabs)
Ops.lua every mutation, behind one funnel that sets dirty + status together
App.lua chrome (version rail, title bar, tab rail, status bar) and the panel router
panels/ one file per tab; pure layout that dispatches into Ops

panels/SpeciesPicker.lua is the one exception to "one file per tab": it is the modal species search the inspector opens, drawn by App.draw after the panel rather than routed through the tab table. Kit has no z-order, so while it is up Kit.blockClicks shields every widget underneath it.

Switch / gamepad

On Nintendo Switch (and any gamepad without a mouse), the editor uses the same virtual-cursor idea as the launcher: left stick / D-pad moves a pointer, A clicks, B closes (with the usual unsaved confirm), L/R cycle tabs, and the right stick scrolls lists. Touch taps forward as clicks. Without that path the editor soft-locked until HOME — main.lua used to drop all pad/touch events while editorMode was set.

The design reference is the SaveEditor.dc.html mockup that this port transcribes; its measurements are in the same pixel space App.lua draws in.

Two rules the code enforces and the tests assert:

  1. Ops.mark(S, msg) is the only thing that sets S.dirty, and it always writes the status line at the same time. Refusals go through Ops.say, which speaks without dirtying. No branch may silently no-op.
  2. Destructive verbs go through Ops.arm(S, id, msg): the first call arms and returns false, a second within Ops.ARM_SECONDS commits. Ops.armLabel relabels the button to Confirm? in between.

Headless tests

Run from repo root (luajit, or the same interpreter as tests/run_tests.lua). All four run in CI as their own tiers in scripts/test.sh:

luajit tests/run_save_editor_tests.lua      # SaveIO, App load/save/close, party + inspector, all-tab draw smoke
luajit tests/save_editor_task6_tests.lua    # Boxes + Items rules
luajit tests/save_editor_task7_tests.lua    # Events + Dex rules
luajit tests/save_editor_task8_tests.lua    # map browser + spawn points
luajit tests/save_editor_mod_tests.lua      # modded species/items stay editable
luajit tests/save_editor_pad_input_test.lua # pad cursor / NX input routing

They drive Ops.lua rather than clicking pixel coordinates. The panels are layout over Ops, so a redesign moves every coordinate but none of the rules; asserting against Ops is what keeps the suites meaningful across one.