From 96af652d3bff1b7f3e2ad806295330906f4878b7 Mon Sep 17 00:00:00 2001 From: thibautbus <310327033+thibautbus@users.noreply.github.com> Date: Wed, 12 Aug 2026 11:25:07 +0200 Subject: [PATCH] Fix lingering title music after Resume Game 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. --- src/core/Game.lua | 25 +++++++++++++++++++------ src/world/OverworldController.lua | 12 +++++++++++- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/core/Game.lua b/src/core/Game.lua index 9cb05fda..0f8fb6de 100644 --- a/src/core/Game.lua +++ b/src/core/Game.lua @@ -159,14 +159,15 @@ function Game:makeTitleState() self:applyOptions(self.save.options) self.stack:push(OverworldState, self.save.player.map, self.save.player.x, self.save.player.y, - self.save.player.facing) + self.save.player.facing, + { via = "boot", freshBoot = true }) Screens.push(self, bootScreens(self).newGame or "OakSpeech", function() end) end, onContinue = function() local loaded, recovered = SaveData.load() if loaded then - self:restoreSave(loaded, recovered) + self:restoreSave(loaded, recovered, { freshBoot = true }) end end, }) @@ -639,7 +640,12 @@ function Game:keypressed(key) return elseif key == "f2" then local loaded, recovered = SaveData.load() - if loaded then self:restoreSave(loaded, recovered) end + if loaded then + -- F2 jumps straight to the loaded save's map/position, with no + -- walking transition -- a hard state teleport like Continue, not a + -- smooth warp -- whether pressed at the title screen or mid-session. + self:restoreSave(loaded, recovered, { freshBoot = true }) + end return elseif key == "-" then self:zoomStep(-1) @@ -1124,7 +1130,7 @@ function Game:applyOptions(opts) if gbcCleared then self:writeOptions() end end -function Game:restoreSave(loaded, recovered) +function Game:restoreSave(loaded, recovered, opts) if ModRuntime.wants("save.loading") then ModRuntime.emit("save.loading", { raw = loaded }) end @@ -1157,8 +1163,12 @@ function Game:restoreSave(loaded, recovered) end -- rebuild the state stack from the save while self.stack:top() do self.stack:pop() end + -- freshBoot threads through from the caller (onContinue and F2 both set + -- it); a future caller that doesn't ask for it keeps the ordinary + -- crossfade by default. self.stack:push(self.overworld, loaded.player.map, - loaded.player.x, loaded.player.y, loaded.player.facing) + loaded.player.x, loaded.player.y, loaded.player.facing, + { via = "boot", freshBoot = opts and opts.freshBoot }) self.saveReport = report if not SaveData.emptyReport(report) then -- the report screen is a Screens id so mods (or the ui milestone) own @@ -1187,9 +1197,12 @@ function Game:restoreCheckpointSave(loaded) self.save = loaded self:adoptSave(loaded) while self.stack:top() do self.stack:pop() end + -- freshBoot unconditionally: Checkpoint.resume (src/core/Checkpoint.lua) + -- is this method's only caller, and it is itself gated to the title + -- session (isTitleSession). self.stack:push(self.overworld, loaded.player.map, loaded.player.x, loaded.player.y, loaded.player.facing, - { via = "checkpoint", checkpoint = true }) + { via = "checkpoint", checkpoint = true, freshBoot = true }) end -- Install a reconstructed battle without calling BattleState:enter(), whose diff --git a/src/world/OverworldController.lua b/src/world/OverworldController.lua index a825d37f..565908a4 100644 --- a/src/world/OverworldController.lua +++ b/src/world/OverworldController.lua @@ -462,8 +462,18 @@ function OverworldState:setMap(mapId, x, y, facing, opts) if not keepMusic then -- ..(home/overworld.asm ln 2346) local Music = require("src.core.Music") + -- opts.freshBoot: switch instantly instead of cross-fading, like every + -- other map's PlayDefaultMusic on real hardware -- set only by + -- Game.lua's hard state teleports (onContinue, New Game, F2, + -- restoreCheckpointSave). Deliberately separate from opts.via == + -- "boot" itself: dev tooling (src/dev/Console.lua's warp verb, + -- src/dev/HotReload.lua's reloadMap) reuses that same default for the + -- surf-restore/fresh-npc-pool branches above and must keep the + -- ordinary crossfade. + local fade = Music.MAP_FADE + if opts and opts.freshBoot then fade = nil end Music.playMap(Game.data, mapId, Game.save.onBike, self.player.surfing, - Music.MAP_FADE) + fade) end -- forced bike/surf tiles fire the moment the player is placed on the