-- Native LÖVE2D port of Pokemon Red. A packaged build creates its private -- game-data cache from a user-provided ROM on first boot. -- -- Set POKEPORT_EDITOR=1 or pass `--editor` to `love .` to boot the save -- editor tool (tools/save-editor/) instead of the game. local editorMode = os.getenv("POKEPORT_EDITOR") == "1" or POKEPORT_EDITOR_MODE == true local Game, EditorApp, Importer local autopilot -- optional scripted-input dev tool (tests/autopilot.lua) local driverCo -- optional frame-driver (POKEPORT_DRIVER=file.lua): a -- coroutine that receives `Game` and yields once per -- frame; used headless (xvfb) for scripted screenshots -- --speed N / POKEPORT_SPEED=N: run the logic clock N times faster without -- touching audio (src/core/GameSpeed.lua). Overrides the saved option so a -- bot or screenshot run is not at the mercy of the player's last choice. local speedOverride = tonumber(os.getenv("POKEPORT_SPEED")) -- How many times to run a scripted act+step loop per rendered frame. Only -- scripted runs use this; interactive play fast-forwards through -- Game.speedOverride / the GAME SPEED option instead. local function scriptedIterations() if not (autopilot or driverCo) then return 1 end return math.max(1, math.floor(require("src.core.GameSpeed").clamp(speedOverride))) end local function bootGame(version) -- The launcher hands us the chosen game (Red / Blue); scripted and headless -- runs fall back to POKEPORT_VERSION, then Red. Set the active version and -- overlay its extracted cache BEFORE anything requires generated data, so -- data/generated + assets/generated resolve to that version's files. local GameVersion = require("src.core.GameVersion") GameVersion.set(version or os.getenv("POKEPORT_VERSION") or "red") require("src.import.CacheFs").mountVersion(GameVersion.get()) if love.window and love.window.setTitle then local Version = require("src.core.Version") love.window.setTitle(Version.title( GameVersion.info().displayName .. " (Gen 1 Recompilation Project)")) end Game = require("src.core.Game") Game:load() if os.getenv("POKEPORT_AUTOPILOT") then autopilot = require("tests.autopilot") end local driverPath = os.getenv("POKEPORT_DRIVER") if driverPath then local fn = assert(loadfile(driverPath))() driverCo = coroutine.create(fn) end -- After the two above are known: a scripted run drives the multiplier -- from love.update's loop, so the in-engine one must stay at 1 or the -- two would compound (10x10 = 100 steps per observation). Game.speedOverride = (autopilot or driverCo) and 1 or speedOverride end function love.load(args) local savePath for i, a in ipairs(args or {}) do if a == "--editor" then editorMode = true elseif a == "--developer" then _G.POKEPORT_DEV_MODE = true elseif a == "--save" and args[i + 1] and args[i + 1] ~= "" then savePath = args[i + 1] elseif a == "--speed" and tonumber(args[i + 1]) then speedOverride = tonumber(args[i + 1]) end end love.graphics.setDefaultFilter("nearest", "nearest") if editorMode then package.path = love.filesystem.getSource() .. "/tools/save-editor/?.lua;" .. love.filesystem.getSource() .. "/tools/save-editor/panels/?.lua;" .. package.path EditorApp = require("App") EditorApp.load(savePath) return end local RomImporter = require("src.import.RomImporter") local forceImport = os.getenv("POKEPORT_FORCE_IMPORT") == "1" local importPath = os.getenv("POKEPORT_IMPORT_ROM") -- Scripted / headless runs pick their game from POKEPORT_VERSION (default -- Red); the launcher's per-column choice does not apply to them. local scriptedVersion = os.getenv("POKEPORT_VERSION") or "red" local ready = RomImporter.isReady(scriptedVersion) -- 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 -- The importer detects the dropped/loaded ROM's version by SHA-1 and -- passes it to onComplete; boot that version. Importer = RomImporter.new(function(version) if os.getenv("POKEPORT_IMPORT_ONLY") == "1" then love.event.quit() return end Importer = nil bootGame(version or scriptedVersion) end) if importPath then Importer:startPath(importPath) end return end bootGame(scriptedVersion) return end -- Interactive: the launcher always runs. Red and Blue are each live: a -- column shows Play when that game's ROM is already imported, or Choose ROM -- / drag-drop when it is not (Yellow is still a placeholder). Any dropped -- .gb is routed to Red or Blue by its SHA-1; pressing Play boots that game. Importer = RomImporter.new(function(version) Importer = nil bootGame(version) end, { launcher = true, forceImport = forceImport }) end function love.update(dt) if editorMode then return EditorApp.update(dt) end if Importer then return Importer:update(dt) end -- Scripted runs (autopilot / POKEPORT_DRIVER) observe and act exactly -- once per Game:update, so they must keep a 1:1 relationship with the -- logic step. Fast-forwarding them by scaling the step inside -- Game:update would run N steps per observation: a held direction walks -- through all N, the player slides past the waypoint, and the script -- re-plans from an overshot cell. So iterate the whole act+step loop -- instead -- same script, just more of it per rendered frame. local iterations = scriptedIterations() if autopilot then for _ = 1, iterations do autopilot.update() Game:update(1 / 60) -- deterministic stepping for the autopilot end return end if driverCo then for _ = 1, iterations do local ok, err = coroutine.resume(driverCo, Game) if not ok then print("driver error: " .. tostring(err)) love.event.quit(1) return end if coroutine.status(driverCo) == "dead" then love.event.quit() return end Game:update(1 / 60) end return end Game:update(dt) end function love.draw() if editorMode then return EditorApp.draw() end if Importer then return Importer:draw() end Game:draw() -- frame capture requested by a driver if Game.capturePath then local path = Game.capturePath Game.capturePath = nil love.graphics.captureScreenshot(function(imagedata) local fd = imagedata:encode("png") local f = io.open(path, "wb") if f then f:write(fd:getString()) f:close() end end) end end function love.keypressed(key, scancode, isrepeat) if editorMode then return EditorApp.keypressed(key) end if Importer then return Importer:keypressed(key) end Game:keypressed(key) end function love.keyreleased(key) if editorMode then return end if Importer then return end Game:keyreleased(key) end function love.gamepadpressed(joystick, button) if editorMode then return end if Importer then return end Game:gamepadpressed(joystick, button) end function love.gamepadreleased(joystick, button) if editorMode then return end if Importer then return end Game:gamepadreleased(joystick, button) end function love.gamepadaxis(joystick, axis, value) if editorMode then return end if Importer then return end Game:gamepadaxis(joystick, axis, value) end function love.joystickremoved(joystick) if editorMode then return end if Importer then return end Game:joystickremoved(joystick) end -- f is true on focus gained, false on focus lost (e.g. alt-tab). A held -- direction's key-up can be delivered to the OS instead of the game while -- unfocused, so reset input on either transition rather than trust it. function love.focus(f) if editorMode then return end if Importer then if Importer.focus then Importer:focus(f) end return end Game:focus(f) end -- v is true when the window becomes visible again, false on minimize. function love.visible(v) if editorMode then return end if Importer then return end Game:visible(v) end function love.touchpressed(id, x, y, dx, dy, pressure) if editorMode then return end if Importer then return Importer:mousepressed(x, y, 1) end Game:touchpressed(id, x, y) end function love.touchmoved(id, x, y, dx, dy, pressure) if editorMode then return end if Importer then return end Game:touchmoved(id, x, y) end function love.touchreleased(id, x, y, dx, dy, pressure) if editorMode then return end if Importer then return end Game:touchreleased(id, x, y) end function love.wheelmoved(x, y) if editorMode then if EditorApp.wheelmoved then return EditorApp.wheelmoved(x, y) end return end if Importer then return end Game:wheelmoved(x, y) end function love.mousepressed(x, y, button) if Importer then return Importer:mousepressed(x, y, button) end if editorMode and EditorApp.mousepressed then return EditorApp.mousepressed(x, y, button) end end function love.mousereleased(x, y, button) if Importer then return end if editorMode and EditorApp.mousereleased then return EditorApp.mousereleased(x, y, button) end end function love.textinput(text) if Importer then return end if editorMode and EditorApp.textinput then return EditorApp.textinput(text) end end function love.quit() if editorMode and EditorApp.quit then return EditorApp.quit() -- return true to abort quit end pcall(function() require("src.core.DiscordPresence").shutdown() end) end function love.filedropped(file) if editorMode and EditorApp and EditorApp.filedropped then return EditorApp.filedropped(file) end if Importer then Importer:filedropped(file) end end