-- Pokemon FireRed service owner (Gen 3 peer of Game / Game2). -- Boot: copyright → title → main menu → Oak → gender → field (FR_* maps only). local FixedStep = require("src.core.FixedStep") local Input = require("src.core.Input") local SaveData = require("src.core.SaveData") local Schema = require("src.core.game3.save_schema_firered") local Runtime = require("src.core.game3.runtime") local Audio = require("src.core.game3.audio") local Options = require("src.core.game3.options") local Dataset = require("src.core.game3.dataset") local Display = require("src.core.game3.display") local Boot = require("src.ui.game3.boot") local Game3 = {} Game3.__index = Game3 function Game3.new() return setmetatable({ input = Input, save = nil, session = nil, phase = "boot", -- boot UI | field boot = nil, returnToLauncher = nil, onExit = nil, }, Game3) end function Game3:_hasContinueSave() if not SaveData.load then return false end local ok, save = pcall(SaveData.load) if not ok or type(save) ~= "table" then return false end return save.engine == "game3" and type(save.map) == "string" and save.map:sub(1, 3) == "FR_" end function Game3:_enterField(session, reason) self.session = session -- Continue restores stream; new_game already seeded inside Schema.newGame. local Rng = require("src.core.game3.rng") if reason == "continue" then if not Rng.restoreFromSession(session) then -- Legacy saves without rng: soft-reset-ish reseed. session.trainerId = Rng.seedNewGame() Rng.captureToSession(session) else -- On GBA FRLG, continuing from title screen seeds/perturbs gRngValue with timer TM0 Rng.perturb() end elseif session.rng then Rng.restoreFromSession(session) Rng.perturb() end self.save = Schema.toSaveTable(session) self.phase = "field" self.boot = nil -- Drop boot/title BG+OAM so Display.present composites the field underlay only. local okBg, Bg = pcall(require, "src.core.game3.bg") if okBg and Bg and Bg.reset then Bg.reset() end local okOam, Oam = pcall(require, "src.core.game3.oam") if okOam and Oam and Oam.reset then Oam.reset() end local okF, Fade = pcall(require, "src.ui.game3.fade") if okF and Fade then if Fade.clear then Fade.clear() end -- Come out of Oak's black screen onto the bedroom. if Fade.begin then Fade.begin(Fade.MODE.FROM_BLACK, 1) end end Runtime.start(nil, self, session, { reason = reason or "new_game" }) -- Runtime.start already Map.loads unless alreadyOnMap; keep explicit reload for -- session x/y/facing in case start opts change. local Map = require("src.core.game3.map") Map.load(nil, self, session.map, { x = session.x, y = session.y, facing = session.facing, }) -- Map.load plays header / index mapSongs BGM. end function Game3:load(opts) opts = opts or {} self.onExit = opts.onExit or self.onExit Input:init() self.input = Input Dataset.hydrate(self) local TouchControls = require("src.core.TouchControls") TouchControls:init() self.touchControls = TouchControls TouchControls:setHotkeyHandler(function(action, pressed) if action and action:sub(1, 4) == "key:" then local key = action:sub(5) if pressed then if self.keypressed then self:keypressed(key) end else if self.keyreleased then self:keyreleased(key) end end end end) local okLoad, rawSave, recovered = false, nil, nil if SaveData.load then okLoad, rawSave, recovered = pcall(SaveData.load) end if not okLoad then rawSave, recovered = nil, nil end local saveStatus = "ok" if recovered then saveStatus = "error" -- pokefirered/src/main_menu.c:251 elseif okLoad and rawSave == nil and SaveData.persistenceFs and SaveData.saveFilename then local okFs, exists = pcall(function() local fs = SaveData.persistenceFs(nil) return fs and fs.getInfo and fs.getInfo(SaveData.saveFilename()) ~= nil end) if okFs and exists then saveStatus = "invalid" end -- pokefirered/src/main_menu.c:246 end local options = rawSave and rawSave.options or (SaveData.defaultOptions and SaveData.defaultOptions()) self.options = options self:applyOptions(options) -- Never auto-skip boot into a legacy Sevii sidecar. local continueOk = self:_hasContinueSave() require("src.ui.game3.start_menu").resetCursor() -- pokefirered/src/main.c:134 self.boot = Boot.new() Boot.setHasContinue(self.boot, continueOk) if continueOk then Boot.setContinueInfo(self.boot, Boot.continueInfoFromSave(rawSave)) end Boot.setSaveStatus(self.boot, saveStatus) self.phase = "boot" self.session = nil FixedStep:init(function(dt) self:fixedUpdate(dt) end) pcall(function() require("src.core.PresentSync").applyFixedStepPeriod() end) end function Game3:applyOptions(opts) opts = opts or {} self.options = opts if self.touchControls then self.touchControls:applyOptions({ touchControls = opts.touchControls, haptics = opts.haptics, hotbar = opts.hotbar, }) end local ScreenPosition = require("src.core.ScreenPosition") ScreenPosition.applyOptions(opts) if self.input and opts.bindings then self.input:applyBindings(opts.bindings) end end function Game3:_aliasLA() if not self.session then return end if not Options.lEqualsA(self.session) then return end local input = self.input if not input then return end if input.wasPressed and input:wasPressed("l") then input:sourcePress("a", "l_equals_a") elseif input.isDown and input:isDown("l") then input:sourcePress("a", "l_equals_a_hold") end end function Game3:_handleRegisteredItem() local input = self.input if not input or not input.wasPressed or not input:wasPressed("select") then return end local session = self.session local item = session and session.registeredItem if not item then return end local Runtime = package.loaded["src.core.game3.runtime"] if Runtime and Runtime.uiBusy and Runtime.uiBusy() then return end local Field = package.loaded["src.core.game3.field"] if Field and Field.locked then return end local Bag = require("src.core.game3.bag") local ItemUse = require("src.core.game3.item_use") if not session.bag or not Bag.has(session.bag, item, 1) then session.registeredItem = nil return end ItemUse.useField(session, session.bag, item, nil) end function Game3:_handleBootAction(action) if not action then return end if action.action == "continue" then local ok, save = pcall(SaveData.load) if ok and save and save.engine == "game3" then local session = Schema.fromSaveTable(save) -- Refuse Sevii leftovers. if type(session.map) == "string" and session.map:sub(1, 6) == "SEVII_" then print("[game3] ignoring legacy Sevii save map " .. session.map) session = Schema.newGame({ gender = 0 }) end self:_enterField(session, "continue") end return end if action.action == "new_game" then local session = Schema.newGame({ name = action.name or "RED", rivalName = action.rivalName or "BLUE", gender = action.gender or 0, start = action.start, }) self:_enterField(session, "new_game") return end if action.action == "exit" then if self.returnToLauncher then self.returnToLauncher() elseif self.onExit then self.onExit() elseif love.event and love.event.quit then love.event.quit() end return end end function Game3:fixedUpdate(dt) Audio.update(dt) if self.session then Audio.applyOptions(self.session) end if self.input and self.input.step then self.input:step() end local Rng = require("src.core.game3.rng") Rng.step() if self.phase == "boot" and self.boot then local action = Boot.update(self.boot, self.input, dt) self:_handleBootAction(action) return end if self.phase == "field" then self:_aliasLA() self:_handleRegisteredItem() if Runtime.isActive() then Runtime.update(dt) end end end function Game3:logicSpeed() return math.max(1, tonumber(self.speedOverride) or tonumber(self.options and self.options.speed) or 1) end function Game3:update(dt) local speed = self:logicSpeed() FixedStep.maxAccum = FixedStep.catchupLimit(speed) FixedStep:update(dt, speed) end function Game3:draw() local w = love.graphics.getWidth() local h = love.graphics.getHeight() if self.phase == "boot" and self.boot then local canvas = Display.ensureCanvas("main") if canvas then love.graphics.push("all") love.graphics.setCanvas(canvas) love.graphics.origin() Boot.draw(self.boot) love.graphics.setCanvas() love.graphics.pop() local scale, ox, oy, _, _, scaleY = Display.fit(w, h) love.graphics.setColor(0.02, 0.04, 0.08, 1) love.graphics.rectangle("fill", 0, 0, w, h) love.graphics.setColor(1, 1, 1, 1) love.graphics.draw(canvas, ox, oy, 0, scale, scaleY) else Boot.draw(self.boot) end if self.touchControls then self.touchControls:draw() end return end if Runtime.isActive() then if Display.present(self, w, h) then if self.touchControls then self.touchControls:draw() end return end end love.graphics.clear(0.05, 0.05, 0.12) love.graphics.setColor(0.4, 0.8, 0.4) local map = self.session and self.session.map or "?" love.graphics.printf("Fire Red field: " .. tostring(map), 0, h * 0.45, w, "center") if self.touchControls then self.touchControls:draw() end end function Game3:keypressed(key) if self.input and self.input.keypressed then self.input:keypressed(key) end end function Game3:keyreleased(key) if self.input and self.input.keyreleased then self.input:keyreleased(key) end end function Game3:gamepadpressed(joystick, button) if self.touchControls then self.touchControls:noteGamepad() end if self.input and self.input.gamepadpressed then self.input:gamepadpressed(joystick, button) end end function Game3:gamepadreleased(joystick, button) if self.input and self.input.gamepadreleased then self.input:gamepadreleased(joystick, button) end end function Game3:saveGame() if not self.session then return end if Runtime.getSession then local s = Runtime.getSession() if s then self.session = s end end self.save = Schema.toSaveTable(self.session) if SaveData.save then pcall(SaveData.save, self.save) end end function Game3:resize() end function Game3:mousepressed(x, y, button, istouch) if istouch then return end if os.getenv("POKEPORT_TOUCH") == "1" and self.touchControls then self.touchControls:touchpressed("mouse", x, y) end end function Game3:mousemoved(x, y, dx, dy, istouch) if istouch then return end if os.getenv("POKEPORT_TOUCH") == "1" and self.touchControls then self.touchControls:touchmoved("mouse", x, y) end end function Game3:mousereleased(x, y, button, istouch) if istouch then return end if os.getenv("POKEPORT_TOUCH") == "1" and self.touchControls then self.touchControls:touchreleased("mouse", x, y) end end function Game3:wheelmoved() end function Game3:textinput() end function Game3:filedropped() end function Game3:touchpressed(id, x, y, dx, dy, pressure) if self.touchControls and self.touchControls:touchpressed(id, x, y) then return end end function Game3:touchmoved(id, x, y, dx, dy, pressure) if self.touchControls then self.touchControls:touchmoved(id, x, y) end end function Game3:touchreleased(id, x, y, dx, dy, pressure) if self.touchControls then self.touchControls:touchreleased(id, x, y) end end function Game3:gamepadaxis(joystick, axis, value) if math.abs(value) > 0.5 and self.touchControls then self.touchControls:noteGamepad() end if self.input and self.input.gamepadaxis then self.input:gamepadaxis(joystick, axis, value) end end function Game3:joystickpressed(joystick, button) if self.touchControls then self.touchControls:noteGamepad() end if self.input and self.input.joystickpressed then self.input:joystickpressed(joystick, button) end end function Game3:joystickreleased(joystick, button) if self.input and self.input.joystickreleased then self.input:joystickreleased(joystick, button) end end function Game3:joystickaxis(joystick, axis, value) if math.abs(value) > 0.5 and self.touchControls then self.touchControls:noteGamepad() end if self.input and self.input.joystickaxis then self.input:joystickaxis(joystick, axis, value) end end function Game3:joystickhat(joystick, hat, direction) if direction ~= "c" and self.touchControls then self.touchControls:noteGamepad() end if self.input and self.input.joystickhat then self.input:joystickhat(joystick, hat, direction) end end function Game3:joystickadded() end function Game3:joystickremoved(joystick) if self.touchControls then self.touchControls:joystickremoved() end end function Game3:focus(f) if self.input then self.input:reset() end if self.touchControls then self.touchControls:reset() end if f then if self.input then self.input:reconcile() end Audio.onFocusGained() end end function Game3:visible(v) if v then self:onResume() else if self.input then self.input:reset() end if self.touchControls then self.touchControls:reset() end end end function Game3:onResume() if self.input then self.input:reset() self.input:reconcile() end if self.touchControls then self.touchControls:reset() end Audio.onFocusGained() end function Game3:returnToTitle() Audio.stopAll() local Stack = require("src.ui.game3.stack") Stack.clear() if Runtime.isActive() then Runtime.stop(nil, self) end self.phase = "boot" self.session = nil local okLoad, rawSave, recovered = false, nil, nil if SaveData.load then okLoad, rawSave, recovered = pcall(SaveData.load) end if not okLoad then rawSave, recovered = nil, nil end local saveStatus = "ok" if recovered then saveStatus = "error" elseif okLoad and rawSave == nil and SaveData.persistenceFs and SaveData.saveFilename then local okFs, exists = pcall(function() local fs = SaveData.persistenceFs(nil) return fs and fs.getInfo and fs.getInfo(SaveData.saveFilename()) ~= nil end) if okFs and exists then saveStatus = "invalid" end end local continueOk = self:_hasContinueSave() require("src.ui.game3.start_menu").resetCursor() self.boot = Boot.new() Boot.setHasContinue(self.boot, continueOk) if continueOk then Boot.setContinueInfo(self.boot, Boot.continueInfoFromSave(rawSave)) end Boot.setSaveStatus(self.boot, saveStatus) self.boot.phase = Boot.PHASE.TITLE self.boot.timer = 0 local TitleScreen = require("src.ui.game3.title_screen") TitleScreen.enter(self.boot) Audio.playSong(278) end function Game3:quit() self:saveGame() end return Game3