Pokemon Silver as a full launcher version, plus launcher mods-list and title-tempo fixes

Silver: derived import manifest (tools/make_silver_manifest.py re-resolves
the Gold manifest's symbols from pokesilver.sym), silver GameVersion row,
generation-keyed extractor routing, required-files override, edition save
stamping (a Silver playthrough no longer writes into the Gold save),
checkver-driven edition data, SILVER/KAMON/OSCAR/MAX presets, GOLD rival
default, edition credits banner, Lugia title screen (OAM layouts, bob,
trail, palettes as title.lua data keys with Gold defaults so old caches
need no re-import), packaging for every build target, docs, and tests.

Launcher: the installed-mods list is one continuous scroll (rows culled to
the viewport) instead of a pager with an inner scroll viewport; the pad
cursor's edge-scroll no longer runs it to the bottom. The game dropdown
shows just the initial and caret. Find-tab behavior unchanged.

Title tempo: a sprite-anim frame shows duration+1 ticks
(engine/sprite_anims/core.asm GetSpriteAnimFrame), which locks both
editions' 64-tick wing beat to the 64-tick sine bob; the title screens no
longer run fast and out of phase.
This commit is contained in:
bryanthaboi
2026-08-20 08:27:25 -04:00
parent 4c8c1cf36b
commit ec9dc29646
68 changed files with 18617 additions and 466 deletions
+24 -12
View File
@@ -103,7 +103,7 @@ Save.PLAYER_STATES = {
}
local function saveNames(version)
version = version or "gold"
version = version or GameVersion.get()
-- Resolve the ACTIVE SLOT the same way SaveData does, and only fall back to
-- the flat save_<version>.lua when no slot is registered.
--
@@ -133,16 +133,22 @@ local function fs()
return love.filesystem
end
-- A fresh Gold save. `opts` carries what the intro collected: player name,
-- The blank-name fallback is the first PlayerNameArray row, which differs
-- per edition -- data/player_names.asm:12-23.
function Save.defaultPlayerName(version)
return (version or GameVersion.get()) == "silver" and "SILVER" or "GOLD"
end
-- A fresh Gen 2 save. `opts` carries what the intro collected: player name,
-- rival name, and the options the OPTION screen was left on.
function Save.newGame(opts)
opts = opts or {}
local save = {
format = Save.FORMAT,
version = "gold",
version = GameVersion.get(),
generation = 2,
player = {
name = opts.playerName or "GOLD",
name = opts.playerName or Save.defaultPlayerName(),
-- _ResetWRAM rolls wPlayerID out of hRandomSub/hRandomAdd
-- (engine/menus/intro_menu.asm:41-49).
id = opts.trainerId or rand(0, 65535),
@@ -374,10 +380,13 @@ end
function Save.normalize(save)
if type(save) ~= "table" then return nil end
save.format = save.format or Save.FORMAT
save.version = "gold"
if not (GameVersion.VERSIONS[save.version]
and GameVersion.generation(save.version) == 2) then
save.version = GameVersion.get()
end
save.generation = 2
save.player = save.player or {}
save.player.name = save.player.name or "GOLD"
save.player.name = save.player.name or Save.defaultPlayerName(save.version)
save.player.id = save.player.id or rand(0, 65535)
save.player.money = math.max(0, math.min(save.player.money or 0, Save.MAX_MONEY))
save.player.coins = math.max(0, math.min(save.player.coins or 0, Save.MAX_COINS))
@@ -753,21 +762,24 @@ end
-- copy is the witness that survives a crash mid-replace.
function Save.save(save)
if type(save) ~= "table" then return false, "no save" end
if (save.version or "gold") == "gold" then
Save.normalize(save)
local version = save.version
do
local ok, SaveData = pcall(require, "src.core.SaveData")
if ok and SaveData.activeSlot and not SaveData.activeSlot("gold") then
local id = SaveData.createSlot and SaveData.createSlot("gold")
if id and SaveData.setActiveSlot then SaveData.setActiveSlot("gold", id) end
if ok and SaveData.activeSlot and not SaveData.activeSlot(version) then
local id = SaveData.createSlot and SaveData.createSlot(version)
if id and SaveData.setActiveSlot then
SaveData.setActiveSlot(version, id)
end
end
end
local main, backup, tmp = saveNames(save.version)
local main, backup, tmp = saveNames(version)
local f = fs()
if not f then return false, "no filesystem" end
-- saveNames may now return a saves/<version>/<slot>.lua path, and
-- love.filesystem.write does not create missing parent directories.
local dir = main:match("^(.*)/[^/]+$")
if dir and f.createDirectory then f.createDirectory(dir) end
Save.normalize(save)
save.savedAt = os.time()
local encoded = SaveSerializer.encode(save)
if f.getInfo(main) then