lauuncher spruce up part 1

This commit is contained in:
bryanthaboi
2026-07-23 09:13:00 -04:00
parent 2dbe21babf
commit da14b7ee3f
6 changed files with 365 additions and 132 deletions
+6 -2
View File
@@ -24,8 +24,12 @@ function love.conf(t)
local ok, Version = pcall(require, "src.core.Version")
t.window.title = ok and Version.title()
or "Pokemon Red (Gen 1 Recompilation Project)"
t.window.width = 160 * 4
t.window.height = 144 * 4
-- Open at the launcher's design size (the split-screen ROM selector is
-- laid out for 1024x768). The window is resizable and the 160x144 game
-- canvas letterboxes into whatever size it ends up, so this only sets the
-- starting size, not the game's resolution.
t.window.width = 1024
t.window.height = 768
end
t.version = "11.5"
t.window.vsync = 1
+32 -12
View File
@@ -68,20 +68,40 @@ function love.load(args)
end
local RomImporter = require("src.import.RomImporter")
if os.getenv("POKEPORT_FORCE_IMPORT") == "1" or not RomImporter.isReady() then
Importer = RomImporter.new(function()
if os.getenv("POKEPORT_IMPORT_ONLY") == "1" then
love.event.quit()
return
end
Importer = nil
bootGame()
end)
local importPath = os.getenv("POKEPORT_IMPORT_ROM")
if importPath then Importer:startPath(importPath) end
local ready = RomImporter.isReady()
local forceImport = os.getenv("POKEPORT_FORCE_IMPORT") == "1"
local importPath = os.getenv("POKEPORT_IMPORT_ROM")
-- Scripted / headless runs have to reach the game with no human pressing
-- Play: an autopilot, a frame driver, an import-only build step, or an
-- explicit ROM path all bypass the interactive launcher and keep today's
-- import-then-boot (or boot-straight-in) behavior.
local scripted = os.getenv("POKEPORT_AUTOPILOT") or os.getenv("POKEPORT_DRIVER")
or os.getenv("POKEPORT_IMPORT_ONLY") == "1" or importPath ~= nil
if scripted then
if forceImport or not ready then
Importer = RomImporter.new(function()
if os.getenv("POKEPORT_IMPORT_ONLY") == "1" then
love.event.quit()
return
end
Importer = nil
bootGame()
end)
if importPath then Importer:startPath(importPath) end
return
end
bootGame()
return
end
bootGame()
-- Interactive: the launcher always runs. Its Red column shows Play when the
-- ROM is already imported or Choose ROM / drag-drop when it is not (Blue and
-- Yellow are placeholders); pressing Play boots the chosen game.
Importer = RomImporter.new(function()
Importer = nil
bootGame()
end, { ready = ready and not forceImport, launcher = true })
end
function love.update(dt)
+14
View File
@@ -449,6 +449,17 @@ SaveData.addCoreMigration(1, function(save)
Boxes.ensure(save)
end)
-- game version (Red vs Blue) prep: saves written before Blue support
-- existed carry no `version` tag, and Red is the only game that ever
-- shipped, so default every untagged save to Red. from=2 so it catches
-- every pre-bump save (format 1 and 2) and is skipped once re-stamped to
-- the current format.
SaveData.addCoreMigration(2, function(save)
if not save.version then
save.version = "red"
end
end)
-- ------- write
-- Game progress only; options are written separately via saveOptions.
@@ -785,6 +796,9 @@ function SaveData.newGame(boot)
local heal = SaveData.defaultHeal(boot)
local save = {
meta = { format = Version.saveFormat, mods = {} },
-- which game this playthrough is (Red vs Blue). Only Red ships today;
-- boot carries the choice once Blue support lands.
version = boot.version or "red",
player = {
map = map,
x = x,
+1 -1
View File
@@ -7,7 +7,7 @@ local Version = {
engine = "1.0.0", -- game/engine release (semver triple)
modApi = 2, -- mod API major (manifest `api`)
linkProtocol = 2, -- link handshake wire version (Handshake.PROTOCOL)
saveFormat = 2, -- save.meta.format
saveFormat = 3, -- save.meta.format
cache = "rom-cache-v5", -- ROM import cache generation (RomImporter marker)
}
+301 -116
View File
@@ -23,6 +23,23 @@ local REQUIRED_FILES = {
"assets/generated/audio/programs.bin",
}
-- Split-screen ROM-selector palette (the "Split-screen ROM selector" design).
-- One column per game: Red is live, Blue and Yellow are placeholders until
-- those games are supported. Values are 0-255 RGB.
local PAL = {
bg = { 241, 243, 232 },
detail = { 74, 88, 72 },
heading = { 25, 31, 28 },
box = { 215, 220, 202 },
white = { 255, 255, 255 },
red = { 181, 35, 42 },
blue = { 30, 86, 168 },
gold = { 214, 164, 0 },
goldInk = { 160, 120, 0 },
disabled = { 146, 158, 178 },
disabledInk = { 238, 242, 247 },
}
local function allRequiredFilesExist()
-- CacheFs.exists checks the game folder directly for a portable install,
-- otherwise the save directory through love.filesystem.
@@ -205,15 +222,24 @@ local function chooseRom()
return nil
end
function RomImporter.new(onComplete)
-- onComplete hands off to the game (boot). opts:
-- ready -- this game's ROM is already imported: open on the Play state
-- launcher -- interactive launcher: a fresh import lands on the Play state
-- instead of auto-booting (headless callers omit this and keep
-- the old import-then-boot behavior)
-- romName -- filename shown next to Play when already imported
function RomImporter.new(onComplete, opts)
opts = opts or {}
local previousMarker = require("src.import.CacheFs").read(MARKER_PATH)
local returning = previousMarker ~= nil and previousMarker ~= CACHE_MARKER
local android = love.system.getOS() == "Android"
local self = setmetatable({
onComplete = onComplete,
launcher = opts.launcher or false,
logo = love.graphics.newImage("assets/logo/logo.png"),
bcg = love.graphics.newImage("assets/logo/bcg.png"),
state = "waiting",
state = opts.ready and "ready" or "waiting",
romName = opts.romName or "pokemon_red.gb",
returning = returning,
android = android,
status = returning and "More assets are needed from your ROM"
@@ -226,10 +252,11 @@ function RomImporter.new(onComplete)
stageCurrent = 0,
stageTotal = 1,
pulse = 0,
button = {},
}, RomImporter)
if android then
-- Only hunt for a ROM when one is actually needed; an already-imported
-- game opens straight on Play.
if android and self.state ~= "ready" then
self.status = returning and "More ROM assets needed" or "Get your Pokemon Red ROM (.gb) in"
self.detail = "Tap Choose ROM to pick your file"
local name = scanForRom()
@@ -247,7 +274,10 @@ end
-- it into the folder scanForRom checks, so a rescan on refocus picks it up
-- without the player needing to tap the button again.
function RomImporter:focus(f)
if not (f and self.android and self.state ~= "working") then return end
if not (f and self.android
and (self.state == "waiting" or self.state == "error")) then
return
end
local name = scanForRom()
if name then
self:startData(love.filesystem.read(name), name)
@@ -321,6 +351,14 @@ function RomImporter:startData(data, displayName)
self.status = "Ready"
self.detail = "Starting Pokemon Red..."
self.progress = 1
if self.launcher then
-- Stay on the launcher and show Play for the game just imported; the
-- player presses Play to boot it.
self.romName = (displayName and (displayName:match("[^/\\]+$") or displayName))
or self.romName
self.state = "ready"
return
end
if self.onComplete then self.onComplete() end
end)
end
@@ -391,146 +429,293 @@ function RomImporter:update(dt)
until love.timer.getTime() - started >= 0.008
end
local function setColor255(r, g, b, a)
love.graphics.setColor(r / 255, g / 255, b / 255, (a or 255) / 255)
-- Player pressed Play on a game whose ROM is imported: hand off to boot.
function RomImporter:play()
if self.onComplete then self.onComplete() end
end
local function printCentered(text, y, font, width)
love.graphics.setFont(font)
love.graphics.printf(text, 0, y, width, "center")
-- "re-import" from the Play state: drop back to the choose/drop UI so a fresh
-- ROM can be selected (the extract itself replaces the old cache).
function RomImporter:reimport()
if self.state ~= "ready" then return end
self.state = "waiting"
self.returning = false
end
local function clamp(v, lo, hi)
return math.max(lo, math.min(hi, v))
end
-- set the current draw color from a PAL triple (0-255), with optional alpha 0-1
local function col(c, a)
love.graphics.setColor(c[1] / 255, c[2] / 255, c[3] / 255, a or 1)
end
-- LOVE has no dashed-stroke primitive; step short segments along each edge so
-- the "coming soon" Blue/Yellow boxes read as placeholders
local function dashedRect(x, y, w, h, dash, gap)
local step = dash + gap
local function edge(x1, y1, x2, y2)
local dx, dy = x2 - x1, y2 - y1
local len = math.sqrt(dx * dx + dy * dy)
if len == 0 then return end
local ux, uy = dx / len, dy / len
local d = 0
while d < len do
local e = math.min(d + dash, len)
love.graphics.line(x1 + ux * d, y1 + uy * d, x1 + ux * e, y1 + uy * e)
d = d + step
end
end
edge(x, y, x + w, y)
edge(x + w, y, x + w, y + h)
edge(x + w, y + h, x, y + h)
edge(x, y + h, x, y)
end
function RomImporter:draw()
local width, height = love.graphics.getDimensions()
setColor255(241, 243, 232)
local third = width / 3
col(PAL.bg)
love.graphics.rectangle("fill", 0, 0, width, height)
setColor255(181, 35, 42)
love.graphics.rectangle("fill", 0, 0, width, math.max(8, height * 0.025))
-- top tri-colour stripe (Red | Blue | Yellow)
local stripeH = math.max(8, height * 0.02)
col(PAL.red); love.graphics.rectangle("fill", 0, 0, third, stripeH)
col(PAL.blue); love.graphics.rectangle("fill", third, 0, third, stripeH)
col(PAL.gold); love.graphics.rectangle("fill", 2 * third, 0, width - 2 * third, stripeH)
-- fonts, rebuilt only when the window size changes
local fontKey = ("%dx%d"):format(width, height)
if self.fontKey ~= fontKey then
self.fontKey = fontKey
self.bodyFont = love.graphics.newFont(
math.max(16, math.min(22, height * 0.038)))
self.smallFont = love.graphics.newFont(
math.max(13, math.min(17, height * 0.029)))
self.warningFont = love.graphics.newFont(
math.max(10, math.min(12, height * 0.022)))
local function f(frac, lo, hi) return love.graphics.newFont(clamp(height * frac, lo, hi)) end
self.pillFont = f(0.021, 12, 15)
self.headFont = f(0.030, 16, 22)
self.detailFont = f(0.023, 13, 16)
self.buttonFont = f(0.029, 16, 22)
self.hintFont = f(0.021, 12, 15)
self.warningFont = f(0.017, 10, 12)
end
local bodyFont, smallFont, warningFont =
self.bodyFont, self.smallFont, self.warningFont
local contentWidth = math.min(width - 40, 520)
local left = (width - contentWidth) / 2
local logoWidth, logoHeight = self.logo:getDimensions()
local logoScale = math.min(
math.min(width - 48, 420) / logoWidth,
height * 0.15 / logoHeight)
-- footer (Boi's Club Games logo + trust warning), pinned to the bottom and
-- measured first so the columns know where they must stop
local warningWidth = math.min(width - 32, 620)
love.graphics.setFont(self.warningFont)
local _, warningLines = self.warningFont:getWrap(TRUST_WARNING, warningWidth)
local warningH = #warningLines * self.warningFont:getHeight()
local warningY = height - warningH - 8
local bcgW, bcgH = self.bcg:getDimensions()
local bcgScale = math.min(math.min(width - 48, 200) / bcgW, height * 0.075 / bcgH)
local bcgDW, bcgDH = bcgW * bcgScale, bcgH * bcgScale
local bcgX, bcgY = (width - bcgDW) / 2, warningY - bcgDH - 8
self.bcgButton = { x = bcgX, y = bcgY, width = bcgDW, height = bcgDH }
local footerTop = bcgY - 10
-- faint per-column tints under the split
col(PAL.red, 0.07); love.graphics.rectangle("fill", 0, stripeH, third, footerTop - stripeH)
col(PAL.blue, 0.07); love.graphics.rectangle("fill", third, stripeH, third, footerTop - stripeH)
col(PAL.gold, 0.08); love.graphics.rectangle("fill", 2 * third, stripeH, width - 2 * third, footerTop - stripeH)
-- logo (larger, no subtitle), centred over the split
local logoW, logoH = self.logo:getDimensions()
local logoScale = math.min(math.min(width - 40, 560) / logoW, height * 0.20 / logoH)
local logoDW, logoDH = logoW * logoScale, logoH * logoScale
local logoY = stripeH + height * 0.02
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(
self.logo,
(width - logoWidth * logoScale) / 2,
height * 0.075,
0, logoScale, logoScale)
setColor255(74, 88, 72)
printCentered(self.returning and "UPDATE REQUIRED" or "FIRST RUN",
height * 0.205, smallFont, width)
love.graphics.draw(self.logo, (width - logoDW) / 2, logoY, 0, logoScale, logoScale)
local zoneY, zoneH = height * 0.29, math.min(180, height * 0.31)
setColor255(215, 220, 202)
love.graphics.rectangle("fill", left, zoneY, contentWidth, zoneH)
setColor255(74, 88, 72)
love.graphics.setLineWidth(2)
love.graphics.rectangle("line", left, zoneY, contentWidth, zoneH)
setColor255(25, 31, 28)
printCentered(self.status, zoneY + zoneH * 0.25, bodyFont, width)
setColor255(74, 88, 72)
love.graphics.setFont(smallFont)
local _, wrapped = smallFont:getWrap(self.detail, contentWidth - 48)
local visible = {}
for index = 1, math.min(#wrapped, 3) do visible[index] = wrapped[index] end
love.graphics.printf(table.concat(visible, "\n"),
left + 24, zoneY + zoneH * 0.52, contentWidth - 48, "center")
if self.state == "working" or self.state == "complete" then
local barY = zoneY + zoneH - 24
setColor255(164, 172, 151)
love.graphics.rectangle("fill", left + 24, barY, contentWidth - 48, 8)
setColor255(181, 35, 42)
love.graphics.rectangle("fill", left + 24, barY,
(contentWidth - 48) * self.progress, 8)
else
local buttonWidth = math.min(260, contentWidth - 80)
local buttonHeight = math.max(46, math.min(56, height * 0.09))
local buttonX = (width - buttonWidth) / 2
local buttonY = math.min(height - buttonHeight - 34, zoneY + zoneH + 36)
self.button = {
x = buttonX, y = buttonY, width = buttonWidth, height = buttonHeight,
}
setColor255(25, 31, 28)
love.graphics.rectangle("fill", buttonX, buttonY, buttonWidth, buttonHeight)
setColor255(255, 255, 255)
love.graphics.setFont(bodyFont)
love.graphics.printf("Choose ROM", buttonX,
buttonY + (buttonHeight - bodyFont:getHeight()) / 2,
buttonWidth, "center")
setColor255(74, 88, 72)
love.graphics.setFont(smallFont)
love.graphics.printf(
self.android and "or copy the .gb via USB" or "or drop the .gb file here",
0, buttonY + buttonHeight + 12, width, "center")
end
local bcgWidth, bcgHeight = self.bcg:getDimensions()
love.graphics.setFont(warningFont)
local warningWidth = math.min(width - 32, 600)
local _, warningLines = warningFont:getWrap(TRUST_WARNING, warningWidth)
local warningHeight = #warningLines * warningFont:getHeight()
local warningY = height - warningHeight - 8
local bcgScale = math.min(
math.min(width - 48, 220) / bcgWidth,
height * 0.08 / bcgHeight)
local bcgDrawWidth = bcgWidth * bcgScale
local bcgDrawHeight = bcgHeight * bcgScale
local bcgX = (width - bcgDrawWidth) / 2
local bcgY = warningY - bcgDrawHeight - 8
self.bcgButton = {
x = bcgX, y = bcgY,
width = bcgDrawWidth, height = bcgDrawHeight,
-- shared column metrics so the three columns line up exactly
local m = {
boxW = math.min(third - 32, 300),
boxH = clamp(height * 0.22, 120, 190),
pillH = self.pillFont:getHeight() + 10,
buttonH = clamp(height * 0.072, 44, 56),
gap1 = clamp(height * 0.028, 12, 22),
gap2 = clamp(height * 0.030, 14, 24),
gap3 = 10,
hintH = self.hintFont:getHeight(),
}
local stackH = m.pillH + m.gap1 + m.boxH + m.gap2 + m.buttonH + m.gap3 + m.hintH
local regionTop = logoY + logoDH + height * 0.03
local top = regionTop + math.max(0, (footerTop - regionTop - stackH) / 2)
-- draw one column and return the button/link hit rects for the caller
local function column(colX, spec)
local cx = colX + third / 2
local x = cx - m.boxW / 2
local a = spec.alpha or 1
local y = top
-- pill badge
love.graphics.setFont(self.pillFont)
local pillW = self.pillFont:getWidth(spec.label) + 28
col(spec.color, a)
love.graphics.setLineWidth(2)
love.graphics.rectangle("line", cx - pillW / 2, y, pillW, m.pillH, m.pillH / 2, m.pillH / 2)
love.graphics.printf(spec.label, cx - pillW / 2,
y + (m.pillH - self.pillFont:getHeight()) / 2, pillW, "center")
y = y + m.pillH + m.gap1
-- box
col(PAL.box, a)
love.graphics.rectangle("fill", x, y, m.boxW, m.boxH)
love.graphics.setLineWidth(2)
col(spec.color, a)
if spec.dashed then
dashedRect(x, y, m.boxW, m.boxH, 7, 5)
else
love.graphics.rectangle("line", x, y, m.boxW, m.boxH)
end
local pad = 14
love.graphics.setFont(self.headFont)
col(PAL.heading, a)
love.graphics.printf(spec.heading, x + pad, y + pad, m.boxW - 2 * pad, "center")
local _, headLines = self.headFont:getWrap(spec.heading, m.boxW - 2 * pad)
local headBlock = math.max(1, #headLines) * self.headFont:getHeight()
love.graphics.setFont(self.detailFont)
col(PAL.detail, a)
love.graphics.printf(spec.detail, x + pad, y + pad + headBlock + 8, m.boxW - 2 * pad, "center")
if spec.progress then
local barY = y + m.boxH - 20
col(PAL.disabled, a)
love.graphics.rectangle("fill", x + pad, barY, m.boxW - 2 * pad, 8)
col(spec.color, a)
love.graphics.rectangle("fill", x + pad, barY, (m.boxW - 2 * pad) * spec.progress, 8)
end
y = y + m.boxH + m.gap2
-- button
local rect
if spec.button then
rect = { x = x, y = y, width = m.boxW, height = m.buttonH }
col(spec.button.bg, a)
love.graphics.rectangle("fill", x, y, m.boxW, m.buttonH)
love.graphics.setFont(self.buttonFont)
local label = spec.button.text
if spec.button.play then
-- filled play triangle + label, centred as a group
local tw = self.buttonFont:getWidth(label)
local tri = self.buttonFont:getHeight() * 0.55
local groupW = tri + 12 + tw
local gx = x + (m.boxW - groupW) / 2
local gy = y + m.buttonH / 2
col(spec.button.fg, a)
love.graphics.polygon("fill", gx, gy - tri / 2, gx, gy + tri / 2, gx + tri * 0.9, gy)
love.graphics.print(label, gx + tri + 12,
y + (m.buttonH - self.buttonFont:getHeight()) / 2)
else
col(spec.button.fg, a)
love.graphics.printf(label, x,
y + (m.buttonH - self.buttonFont:getHeight()) / 2, m.boxW, "center")
end
end
y = y + m.buttonH + m.gap3
-- hint line, or the "<rom> imported · re-import" link
love.graphics.setFont(self.hintFont)
local reimportRect
if spec.reimport then
local prefix = spec.romName .. " imported · "
local link = "re-import"
local pw = self.hintFont:getWidth(prefix)
local lw = self.hintFont:getWidth(link)
local startX = cx - (pw + lw) / 2
col(PAL.detail, a)
love.graphics.print(prefix, startX, y)
love.graphics.print(link, startX + pw, y)
love.graphics.setLineWidth(1)
love.graphics.line(startX + pw, y + self.hintFont:getHeight(),
startX + pw + lw, y + self.hintFont:getHeight())
reimportRect = { x = startX + pw, y = y, width = lw, height = m.hintH + 2 }
elseif spec.hint then
col(PAL.detail, a)
love.graphics.printf(spec.hint, x, y, m.boxW, "center")
end
return rect, reimportRect
end
-- Red column: live, driven by the import state machine
local redHint = self.android and "or copy the .gb via USB" or "or drop the .gb file here"
local redSpec = { color = PAL.red, label = "RED", dashed = false, alpha = 1 }
if self.state == "ready" then
redSpec.heading = "Red ROM ready"
redSpec.detail = "Your ROM is verified. Press Play to start."
redSpec.button = { text = "Play Red", bg = PAL.red, fg = PAL.white, play = true }
redSpec.reimport = true
redSpec.romName = self.romName
elseif self.state == "working" or self.state == "complete" then
redSpec.heading = self.status
redSpec.detail = self.detail
redSpec.progress = self.progress or 0
elseif self.state == "error" then
redSpec.heading = "That ROM could not be imported"
redSpec.detail = self.detail
redSpec.button = { text = "Choose ROM", bg = PAL.red, fg = PAL.white }
redSpec.hint = redHint
else -- waiting
if self.android then
redSpec.heading = self.status
redSpec.detail = self.detail
elseif self.returning then
redSpec.heading = "Update required"
redSpec.detail = "This build needs a few more things from your ROM. Re-import to continue."
else
redSpec.heading = "Choose or drop a Red ROM"
redSpec.detail = "The ROM is verified before any files are created."
end
redSpec.button = { text = "Choose ROM", bg = PAL.red, fg = PAL.white }
redSpec.hint = redHint
end
self.redButton, self.reimportRect = column(0, redSpec)
-- Blue and Yellow columns: placeholders until those games are supported
column(third, {
color = PAL.blue, label = "BLUE", dashed = true, alpha = 0.72,
heading = "Choose or drop a Blue ROM", detail = "Blue support is on the way.",
button = { text = "Coming soon", bg = PAL.disabled, fg = PAL.disabledInk },
hint = "not yet available",
})
column(2 * third, {
color = PAL.gold, label = "YELLOW", dashed = true, alpha = 0.72,
heading = "Choose or drop a Yellow ROM", detail = "Yellow support is on the way.",
button = { text = "Coming soon", bg = PAL.disabled, fg = PAL.disabledInk },
hint = "not yet available",
})
-- footer
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(
self.bcg,
bcgX, bcgY,
0, bcgScale, bcgScale)
setColor255(74, 88, 72)
love.graphics.printf(
TRUST_WARNING,
(width - warningWidth) / 2, warningY,
warningWidth, "center")
love.graphics.draw(self.bcg, bcgX, bcgY, 0, bcgScale, bcgScale)
col(PAL.detail)
love.graphics.setFont(self.warningFont)
love.graphics.printf(TRUST_WARNING, (width - warningWidth) / 2, warningY, warningWidth, "center")
love.graphics.setColor(1, 1, 1, 1)
end
local function inside(r, x, y)
return r and x >= r.x and x <= r.x + r.width and y >= r.y and y <= r.y + r.height
end
function RomImporter:mousepressed(x, y, button)
if button ~= 1 then return end
local logo = self.bcgButton or {}
if x >= (logo.x or 0) and x <= (logo.x or 0) + (logo.width or 0)
and y >= (logo.y or 0) and y <= (logo.y or 0) + (logo.height or 0) then
if inside(self.bcgButton, x, y) then
love.system.openURL(COMMUNITY_URL)
return
end
if self.state == "working" then return end
local rect = self.button
if x >= (rect.x or 0) and x <= (rect.x or 0) + (rect.width or 0)
and y >= (rect.y or 0) and y <= (rect.y or 0) + (rect.height or 0) then
self:choose()
if self.state == "working" or self.state == "complete" then return end
if self.state == "ready" then
if inside(self.reimportRect, x, y) then self:reimport()
elseif inside(self.redButton, x, y) then self:play() end
return
end
if inside(self.redButton, x, y) then self:choose() end
end
function RomImporter:keypressed(key)
if (key == "return" or key == "space") and self.state ~= "working" then
self:choose()
if self.state == "working" or self.state == "complete" then return end
if key == "return" or key == "space" or key == "kpenter" then
if self.state == "ready" then self:play() else self:choose() end
end
end
+11 -1
View File
@@ -196,6 +196,7 @@ do
check(#loaded.meta.mods == 0, "old vanilla save records an empty mod set")
check(loaded.boxes and loaded.boxes[1][1].species == "PIDGEY", "box list folded into boxes[1]")
check(loaded.box == nil, "legacy box key gone")
check(loaded.version == "red", "untagged save defaults to the Red version")
local opts = SaveSerializer.decode(files["options.lua"])
check(opts and opts.musicVol == 2, "embedded options split into options.lua")
@@ -203,7 +204,16 @@ do
local current = { meta = { format = Version.saveFormat, mods = {} },
player = { map = "PALLET_TOWN", x = 1, y = 1 } }
SaveData.runMigrations(current)
check(current.player.id == nil, "format-2 save skips the id backfill")
check(current.player.id == nil, "current-format save skips the id backfill")
-- a format-2 save (Blue support not yet shipped) gains the Red tag;
-- a save that already names a version keeps it
local untagged = { meta = { format = 2, mods = {} }, player = {} }
SaveData.runMigrations(untagged)
check(untagged.version == "red", "pre-version save defaults to Red")
local tagged = { meta = { format = 2, mods = {} }, version = "blue", player = {} }
SaveData.runMigrations(tagged)
check(tagged.version == "blue", "an existing version tag is left untouched")
love.filesystem = realFS
end