diff --git a/conf.lua b/conf.lua index 0016b486..e26a7eea 100644 --- a/conf.lua +++ b/conf.lua @@ -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 diff --git a/main.lua b/main.lua index 476cd271..e761b1ac 100644 --- a/main.lua +++ b/main.lua @@ -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) diff --git a/src/core/SaveData.lua b/src/core/SaveData.lua index 55c21dd3..efd1594f 100644 --- a/src/core/SaveData.lua +++ b/src/core/SaveData.lua @@ -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, diff --git a/src/core/Version.lua b/src/core/Version.lua index 08e14f6b..1a6f0aee 100644 --- a/src/core/Version.lua +++ b/src/core/Version.lua @@ -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) } diff --git a/src/import/RomImporter.lua b/src/import/RomImporter.lua index beedb5f1..3afd2ab0 100644 --- a/src/import/RomImporter.lua +++ b/src/import/RomImporter.lua @@ -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 " 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 diff --git a/tests/mod_save_tests.lua b/tests/mod_save_tests.lua index 32a2bc81..609fa56d 100644 --- a/tests/mod_save_tests.lua +++ b/tests/mod_save_tests.lua @@ -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