Kanto Companion's Edit Mode can't run during battle -- there's no way
to stop a D-pad press from also driving the battle menu, so the only
option today is pausing the game outright. This adds the same input
precedence a mod had before the sandbox changes: vanilla is the whole
existing callback body, and the hook fires before any of it runs.
input.wheel stays a plain observer like input.pointer, since nothing
depends on suppressing it.
`Checkpoint.restore()` rejected most valid checkpoints:
Checkpoint restoration failed: restored state differed at $.save.poisonSteps
poisonSteps is a plain step counter -- (poisonSteps + 1) % 4 on EVERY step,
not only while a mon is poisoned (OverworldController:applyFieldPoison) -- so
it is non-zero three steps out of four in ordinary play.
Installing the restored world re-enters the map, and the map-enter path zeroes
the counter, correctly mirroring ClearVariablesOnEnterMap. Checkpoint.restore
then re-captures the applied state and compares it against the checkpoint, so
the field it had just discarded failed the comparison and the whole restore
rolled back.
A restore is not a map entry from the player's point of view: the counter
belongs to the state being restored. Carry it across the push.
Why this stayed hidden: the two autosave triggers a checkpoint consumer
naturally uses, map.entered and player.warped, are emitted from inside the
very paths that zero the counter (OverworldController 377/572 and 4664/4783),
so those captures hold 0 and restore cleanly. Only a capture taken at an
arbitrary step -- a manual quicksave, or one tied to the ordinary SAVE --
carries a non-zero value. The T4 title-checkpoint tier misses it for the same
reason: its save is fresh, so the counter is already 0.
The comment above the push claimed Checkpoint.resume was this method's only
caller. Both callers arrive through Checkpoint.apply, which serves resume from
the title session and restore from a settled runtime; that is exactly why the
map-entry side effects matter here. Corrected.
tests/engine/restore_poison_steps_bug1971.lua covers 1..3 and 0 across a
restore, and pins the two behaviours that must NOT change: a plain map entry
still zeroes the counter, and a seamless connection crossing still carries it.
It fails on main (3/6, "got 0, want 3") and passes with this change.
- Introduced the `POKEPORT_IDLE_AFTER` and `POKEPORT_IDLE_FPS` environment variables to manage presentation rates on static screens, allowing game logic and audio to maintain full speed.
- Updated the vsync handling to keep it enabled across all platforms, including KMSDRM handhelds, to leverage PresentSync for improved cadence and pacing.
- Enhanced FrameCap logic to ensure proper handling of performance caps in handheld environments, preventing unnecessary software pacing when hardware capabilities are sufficient.
- Adjusted documentation to reflect these changes and their impact on power efficiency and performance.
On weak single-core handhelds (RK3326/RG351MP) the main thread sits at
100% CPU, so every millisecond of jitter drops a frame: constant
stuttering while walking even at PERFORMANCE LOW with all mods off,
while average CPU usage stays unchanged. Three sources of per-frame
garbage and pause on the hot path:
- Game:step / Game:logicSpeed passed inline closures to ModRuntime.call,
allocating a fresh function 60 times per second. Hoist both to
module-level locals; behavior is identical.
- Game:update advanced the incremental collector every rendered frame.
Space it to every 4th frame: the explicit frees the comment refers
to still do the heavy lifting, collection stays incremental, and the
stepping itself stops competing with the 16.6ms frame budget.
- checkEmergencyQuit called love.joystick.getJoysticks() (a fresh
table) twice per frame to guard a 5-second hold combo. Cache the
joystick list and refresh it once per second; a 1s hotplug delay is
irrelevant against a 5s hold.
Tested on an RG351MP (RK3326, dArkOS, v0.2.41): eliminates the
constant walking stutter with all mods off, and with 11 mods
re-enabled. Average CPU is unchanged (the cost was variance, not
load), which matches the diagnosis.
- Updated PresentSync to accurately measure present() block time, ensuring that the probe does not misclassify sync status due to FrameCap sleep artifacts.
- Enhanced FixedStep to implement a hard ceiling on catch-up debt, preventing input starvation during high-speed scenarios.
- Adjusted logic in Game and Game2 to utilize the new catchupLimit function for setting maxAccum, ensuring consistent behavior across speed multipliers.
- Improved test coverage for PresentProbe and PresentSync to validate the new logic and edge cases.
the display sync stuff i added was probing whether vsync was actually working, but it was measuring the wrong thing. during the probe we software-cap at 60 for safety, and the probe was looking at the gap between frames... which includes the sleep 😅 ..... so it always looked like sync was fine even when the driver was ignoring it. on something like the ally x on windows thats a real problem. vsync says on, probe says gated, we lift the cap and snap logic to the panel hz, then youre basically uncapped. at 2–4x that turns into hitching, dropped frames, dropped input, that weird half second freeze. speed swapping wasnt desyncing the driver, it was just making the bad path hurt more.
the solution is just we time present() itself now, not the gap after it, this way the warmup sleep cant fake a pass. if sync is unclear or broken we just stay on a capped 60 and dont snap logic. Also fixed fixedstep so it snaps wall clock dt before applying speed, so in general the 2-4x speed swaps dont screw the pacing math anymore
- Introduced PresentSync module to manage display synchronization.
- Updated love.run() to handle display changes and resizing events with PresentSync.
- Enhanced FrameCap logic to accommodate PresentSync requirements.
- Modified VSync to report effective states and handle driver quirks.
- Updated options menus to reflect PresentSync availability and restrictions.
- Added tests to validate PresentSync functionality and its interaction with VSync settings.
should address issue #1910 and issue #1958
Stop fanning out arbitrary field:release() during Game/Game2 reset — shared
modules use :release as a handle API, and that teardown left the Gen1
singleton unbootable (Game:load nil) on Play-again. Harden bootGame and
endGameSession to rebuild the module if load is missing.
- Replaced `teardownMountedSession` and `flushEditorPackageLoaded` with a unified `SessionLifecycle` approach for managing session transitions and resource cleanup.
- Implemented `SessionLifecycle.endEditorSession` and `SessionLifecycle.endGameSession` to streamline the teardown process for editor and game sessions.
- Introduced `Assets.releaseSession` to handle GPU resource release at session end, ensuring efficient memory management.
- Updated `Game` and `Game2` reset methods to include world and canvas resource releases.
- Enhanced `MapLoader` with a new `releaseAll` method for eager GPU cache cleanup.
- Added tests to verify the new session lifecycle functionality and resource management.
This commit improves the stability and performance of in-process transitions, particularly during editor and game session changes.
further improves and addresses #1662 specifically around android gc pressure.
Five small additions. None of them mention the mod that prompted them.
WorldAPI handles gain stepNow, canStep, placeAt, isMoving and setPassable.
scriptMove queues onto OverworldState.scriptMoves, and a non-empty
scriptMoves is how the overworld knows a cutscene is running, so it gates
handleInput -- an actor animated that way freezes the player's controls for
as long as it walks. Right for Oak marching to his lab, wrong for anything
moving on its own schedule. stepNow sets the same per-tile state without the
queue. It skips the collision check on purpose: the caller is replaying a
move that was decided somewhere else, and re-judging it here would let the
two copies disagree about where the actor is. canStep is there for callers
that do want the map's opinion.
OverworldController raises world.talk around the NPC talk path. An object
from spawnNpc has no TEXT_* id, so the vanilla path has nothing to say for
one; a mod that owns the object wraps this and does not call next.
LinkState.newFromSession adopts a transport that is already paired, so a
mode can tunnel a battle through a connection it already has rather than
opening a second one. The hello and fingerprint exchange still runs. When
the battle ends, link.battle_ended carries the result and both lockstep
party copies -- cable rules leave the real party alone, so that is the only
place the damage exists.
Game:startNewGame(opts) is the title's NEW GAME closure made callable, with
opts.intro=false to land straight in the world. CodeEntry.new takes an
optional {length=, charset=} so the same widget can enter an address or a
room code.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Introduced `teardownMountedSession` to handle cleanup of mounted versions, generated data, and mod states during transitions between the editor and launcher.
- Added `flushEditorPackageLoaded` to evict save-editor modules from `package.loaded` dynamically, ensuring a clean state for subsequent sessions.
- Implemented `Game:reset` and `Game2:reset` methods to clear session-specific fields, allowing for a fresh start when returning to the launcher.
- Enhanced `Renderer` and `TileRenderer` to release GPU resources immediately, preventing memory leaks during rapid transitions.
- Updated `MagnetTrainRide` to support OAM priority overlays and manage background rendering with new shader functionality.
This commit improves the stability and performance of the application during in-process transitions, particularly on Android.
fixes Yellow color palette for title screen
fixes#1662#1643#1536 and finishes fixing #1597
Continue dropped the player into the overworld with the title
screen's song still cross-fading into the map theme over ~1.2s
(Music.MAP_FADE), audibly wrong since the player already has control.
New Game never showed this because OakSpeech's own unfaded
Music.play/playMap masks it before the player is ever placed in the
overworld. The same bug was also reachable through F2 quickload
(pressed at the title screen, or mid-session -- F2 always jumps
straight to the loaded save's map/position with no walking
transition, so it needs the same instant swap as Continue rather than
an ordinary warp's crossfade either way) and through the
checkpoint-resume mod API (RFC 0006's mod.checkpoint:resume).
OverworldState:setMap now takes an opts.freshBoot flag: when set, the
map's music swaps in at once instead of cross-fading, like every
other map's PlayDefaultMusic. It is set by every real hard state
teleport -- TitleState's onContinue, New Game's push, F2 quickload,
and Game:restoreCheckpointSave (whose only caller is itself
title-gated) -- and deliberately kept separate from the pre-existing
opts.via == "boot" default, which dev tooling (the console's `warp`
verb, hot reload's map rebuild) also reuses for unrelated reasons and
must keep its ordinary crossfade.
GameSpeed is a single fast-forward multiplier applied uniformly to the
whole logic clock -- overworld walking, menu navigation and battle turns
all scale together. A player who wants 4X battles but 1X overworld (so a
cutscene or NPC dialogue doesn't blur past) has no way to get both.
Splits save.options.speed into speedOverworld/speedBattle/speedMenu, each
cycling independently, with an automatic migration so an existing save's
speed choice carries over. Game.speedCategoryInStack resolves which
category is active by walking the state stack (the same idiom
wideBattleInStack/fillScaleInStack already use), so a menu opened mid-
battle inherits battle speed rather than resetting to whatever "menu"
defaults to. Adds a new core.logic_speed hook so a mod can read or
override the resolved multiplier for the current frame regardless of
which category produced it, sitting after the link-play and run-argument
overrides so neither is a seam a mod can defeat.
RFC 0007 status: Proposed.