- Added dedicated profile control in the MODS panel for easier profile management.
- Implemented dependency checking during mod installation and updates to ensure compatibility.
- Improved manifest parsing to support GitHub repository hints for dependencies.
- Enhanced UI interactions for saving and renaming profiles.
- Updated tests to cover new functionality and ensure stability.
The sandbox blocks love.system and love.filesystem, which orphans the
native step bridge (#452, #489): its one consumer can no longer call
syncHealthSteps or read steps_pending.json (#1186).
Adds a "steps" manifest permission (shown to the player like the
others) gating a mod.steps facade: available() probes the bridge
quietly, sync() forwards the async refresh, poll() hands the mod its
copy of a delivery. The engine owns the pending file -- mods never name
a path and receive only { steps, from, to }. Without the permission the
acting calls name it, following the network gate. No new events, hooks
or registries; nothing removed.
RFC 0009. Tests: tests/modkit/cases/steps_bridge.lua (no-mod cold
bridge, permissioned sync/poll, per-mod copies, contract-field
filtering, malformed-delivery drop, unpermissioned refusal, bridgeless
build).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A platform-specific launcher wrapper (a native shell embedding this engine,
owning its own UI around the game window) needs to pause the simulation
while its own UI is on top, live-reload options it wrote outside any Lua
UI, and veto main.lua's "closing the window returns to the Lua launcher"
behavior when it owns that job itself. Implementing this by hand-patching
main.lua's love.update/love.quit directly ties every such integration to
editing the one file every other engine change also touches, guaranteeing
merge conflicts. No existing hook covers "should the per-frame simulation
step run" or "should closing the window return to the Lua launcher."
Adds two generic, additive hooks (src/core/PlatformHooks.lua): core.update
and core.quit_to_launcher, replacing what would otherwise be inline
main.lua special-casing. Also adds Manifest.force_enable_env, letting a
mod that cannot function disabled on the one build where its env var is
set (a platform-bridge mod bundled only with that build) re-enable itself
regardless of a saved disable.
RFC 0006 status: Proposed.
trainers.battleTheme validated and merged onto the trainer record but was
never read: battle music came solely from data.audio.battle[kind] where
kind is computeMusicKind()'s final/gym/trainer/wild. Route both battle-
theme start sites through a single choke point:
- BattleState:playBattleTheme() cues Music.playBattle with the override
(self.trainer.battleTheme via battleTheme()), defaulting to the kind
when unset, so vanilla fights and #782's non-gym Giovanni are unchanged.
- BattleState:enter() and OverworldController:pushBattle() both call it.
- Music.playBattle gains an optional 4th song arg that overrides the kind
default, and real call sites now populate the music.select trainerId.
- Victory jingles stay kind-based: a custom battle theme has no derivable
win-variant.
New ROM-free T2 suite tests/engine/trainer_battle_theme_bug945.lua covers
mod load, override resolution, the choke point, and the nil-override
parity gate.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The mod update and Find Mods feeds feed the raw HTTP body straight to
Json.decode. When the endpoint hands back something that is not JSON
(an HTML error page, a proxy/captive prompt, or a plain-text outage
message like "Exceeded secondary rate limit" -- usually still HTTP 200),
the decoder's "unexpected character 'E'" assert escaped through the
pcall and became the error message, blaming the parser instead of the
response.
Add Json.describeUnexpected() as a pre-decode content-type guard: it
returns nil for body shapes the endpoints actually publish (JSON object
or array) and otherwise a short message naming what the server sent
(HTML page / plain text / empty, with a preview). Wire it into
ModUpdate.parseReleases and ModIndex.parse, so both the sync and async
update-check paths surface the real answer instead of the parse error.
HTTP status was already checked upstream by HostShell.httpGet (non-2xx
becomes "HTTP <code> from <url> (...)"); this closes the remaining
"2xx but not JSON" gap everywhere, including bridge platforms that
expose no status or headers.
Add regression tests for plain-text, HTML, and empty bodies; strengthen
the ModIndex HTML soft-fail test.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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.
* Resolve Find Mods stats from each mod's GitHub repo when the feed lacks them
A FIND MODS row now shows download/date stats even when its feed publishes
none: the row fetches the mod's own GitHub releases through the same
cached ModUpdate.fetchReleases the MODS tab uses (six-hour options cache,
so an installed mod's repo is instant). Feed-published stats still win
when present; otherwise one repo is fetched per frame -- the thumbnail
budget pattern -- so opening the tab never stalls for the whole listing.
ModUpdate.statsForReleases is the shared resolver.
* Fix crash opening the Find Mods tab: rename the stats cache field
The resolver stored results in self._findStats, which collides with the
method of the same name: self._findStats resolves through the metatable to
the function, so the or {} guard never fired and indexing it crashed the
launcher the moment the panel built. State now lives in _findStatsCache.
* Fix Find Mods crash: require ModUpdate in the find panel
buildFindPanel called ModUpdate.statsLine without a local require --
only buildModsPanel had one -- so opening the tab indexed a nil global.
* Retry Find Mods stats after failed repo fetches
A failed repo fetch (hourly GitHub API rate limit, transient network error)
was memoized as resolved, so a rate-limited first visit left those rows
empty for the whole session. Failures now schedule a 60s retry; a 404 is
still permanent so a renamed or vanished repo is fetched once.
* Add the MODS tab sort options to the Find Mods tab
the launcher runs before Game:load, so #767's Strings hooks had nothing
filling the catalog and a restart could not help - the ordering is the same
every launch. reads lang/strings.lua from enabled mods only, data not the
entry chunk, sandboxed.
plus a plain pixel fallback on the ui faces, or the kana draw as tofu.
lets a translation keep chosen characters on the rom tiles instead of the ttf.
needed for japanese: sizing the font for kana makes latin narrower, which
knocks the party menu numbers out of line.
A mod index can now publish three optional per-entry fields -- downloads
(total across every release), first_release and last_release (ISO days) --
which the FIND MODS listing shows in the same gold line the MODS tab uses.
The fields are additive by design: feeds that carry them stay readable by
every build that predates them (schema_version stays 1), and feeds that do
not render exactly as before. ModUpdate.statsLine builds the shared line;
the MODS tab reuses it. Parser, formatting, and parse coverage are tested.