Files
gen1recomp/src/core/Game3.lua
T
bryanthaboi c65fcf939d battle speed
2026-09-25 14:20:34 -04:00

1265 lines
43 KiB
Lua

-- 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 MapIds = require("src.core.game3.map_ids")
local Profile = require("src.core.game3.profile")
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 Help = require("src.ui.game3.help_system")
local QuestLog = require("src.ui.game3.quest_log")
local QuestRecorder = require("src.core.game3.quest_log_recorder")
local ModRuntime = require("src.mods.Runtime")
local s9Warned = {}
local function s9log(key, err)
if s9Warned[key] then return end
s9Warned[key] = true
print("[game3] hot-path pcall failed (" .. key .. "): " .. tostring(err))
end
local Game3 = {}
Game3.__index = Game3
local function noop() end
Game3.SKIN_FAST_FORWARD = 4
-- pokefirered/src/item_use.c:159 SetUpItemUseOnFieldCallback
local FIELD_CB_SILENT = { bike = true, rod = true, map = true, escape = true }
function Game3.new()
return setmetatable({
generation = 3,
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 MapIds.isGame3Map(save.map, save.version)
end
function Game3:_enterField(session, reason)
self.session = session
Options.bind(session, self.options)
-- 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
Fade.lockInput = true -- pokefirered/src/field_fadetransition.c:441
end
session._questNewScene=true
if reason == "continue" then session._questMap=session.map end
local Map = require("src.core.game3.map")
Map._announced = nil
Map._nextEnterVia = (reason == "continue") and "continue" or "new_game"
-- pokefirered/src/fieldmap.c:100
Runtime.start(nil, self, session, { reason = reason or "new_game" })
Map._nextEnterVia = nil
if reason == "continue" then
-- pokefirered/src/overworld.c:1717
local okS, Space = pcall(require, "src.core.game3.scripting.space")
if okS and Space and Space.runOnReturnToField then
Space.runOnReturnToField()
end
end
end
function Game3:load(opts)
require("src.import.gba.versions").select(require("src.core.GameVersion").get())
opts = opts or {}
self.onExit = opts.onExit or self.onExit
Input:init()
self.input = Input
Dataset.hydrate(self)
if type(self.data) == "table" then self.data.generation = 3 end
Help.reset()
Help.install(Dataset.cache())
QuestLog.install(Dataset.cache())
local TouchControls = require("src.core.TouchControls")
TouchControls:init()
self.touchControls = TouchControls
TouchControls:setHotkeyHandler(function(action, pressed)
if not action then return end
if 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
return
end
if action == "fast_forward_hold" then
if pressed then
if self._skinSpeedPrev == nil then
self._skinSpeedPrev = self.speedOverride or false
end
self.speedOverride = Game3.SKIN_FAST_FORWARD
else
local prev = self._skinSpeedPrev
self._skinSpeedPrev = nil
self.speedOverride = (prev ~= false) and prev or nil
end
elseif action == "fast_forward_toggle" then
if pressed then self:_cycleSpeed(1) end
elseif action == "soft_reset" then
if pressed and self.phase ~= "arena" then
if self.input then self.input:reset() end
TouchControls:reset()
self:returnToTitle()
end
elseif action == "menu" then
if pressed and self.phase == "field" and self.session then
require("src.ui.game3.option_menu").show({ session = self.session, game = self })
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 = (SaveData.loadOptions and SaveData.loadOptions())
or (rawSave and rawSave.options)
or (SaveData.defaultOptions and SaveData.defaultOptions())
self.options = options
self:applyOptions(options)
self:_exposeModData()
self:_loadMods(opts)
pcall(function() require("src.core.DiscordPresence").init(self) end)
if opts.arena then
FixedStep:init(function(dt)
self:fixedUpdate(dt)
self:_speedLockEdge()
end)
pcall(function()
require("src.core.PresentSync").applyFixedStepPeriod()
end)
self:enterArena(opts.arena)
return
end
-- 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)
Boot.setTextSpeed(self.boot, Options.block(self.options).textSpeed)
self.phase = "boot"
self.session = nil
FixedStep:init(function(dt)
self:fixedUpdate(dt)
self:_speedLockEdge()
end)
pcall(function()
require("src.core.PresentSync").applyFixedStepPeriod()
end)
if ModRuntime.wants("game.ready") then
ModRuntime.emit("game.ready", { game = self })
end
end
local function arenaSession(self, spec)
local session
local version = require("src.core.GameVersion").get()
if spec and spec.slotId and SaveData.setActiveSlot then
pcall(SaveData.setActiveSlot, version, spec.slotId)
end
local ok, save = pcall(SaveData.load)
if ok and type(save) == "table" and save.engine == "game3" then
local activeMods = self.modStatus and self.modStatus.loaded
if SaveData.runMigrations then
pcall(SaveData.runMigrations, save, self.mods and self.mods.migrations, activeMods)
end
local okS, loaded = pcall(Schema.fromSaveTable, save)
if okS and type(loaded) == "table" then session = loaded end
elseif spec and spec.role ~= "spectator" then
require("src.core.Logger").warn("arena3: save slot %s could not be loaded", tostring(spec and spec.slotId))
end
session = session or { party = {}, bag = {} }
if type(session.name) ~= "string" or session.name == "" then
for _, row in ipairs((spec and spec.players) or {}) do
if spec.seat ~= nil and tonumber(row.seat) == tonumber(spec.seat) then session.name = row.name end
end
end
session.store = session.store or { flags = {}, vars = {} }
return session
end
-- pokefirered/src/cable_club.c:964
function Game3:enterArena(spec)
local session = arenaSession(self, spec)
Options.bind(session, self.options)
self.session = session
self.save = Schema.toSaveTable(session)
self.boot = nil
Runtime.session = session
Runtime._game = self
self.phase = "arena"
self.arena = require("src.ui.game3.arena_state").new(self, spec)
return self.arena
end
function Game3:leaveArena()
self.arena = nil
if Runtime.session == self.session then Runtime.session = nil end
if Runtime._game == self and not Runtime.isActive() then Runtime._game = nil end
end
function Game3:_exposeModData()
local data = self.data
if type(data) ~= "table" then return end
data.gen3Pokemon = require("src.core.game3.pokemon")
local Moves = require("src.core.game3.battle.moves")
if not Moves._romLoaded then pcall(Moves.loadRomPack, Dataset.cache()) end
data.gen3Moves = Moves
local ItemsData = require("src.core.game3.items_data")
pcall(ItemsData.ensureLoaded)
data.gen3Items = ItemsData
local Encounters = require("src.core.game3.encounters")
data.gen3Encounters = Encounters._tables
local Trainers = require("src.core.game3.scripting.trainers")
local okT, pack = pcall(Trainers.pack)
data.gen3Trainers = okT and type(pack) == "table" and pack or nil
local Space = require("src.core.game3.scripting.space")
local bundle = Space.bundle
data.gen3Text = bundle and bundle.text or nil
data.gen3Scripts = bundle and bundle.scripts or nil
data.gen3RomText = require("src.core.game3.rom_text").overrides
end
function Game3:_loadMods(opts)
local modOpts = opts and opts.modOpts or nil
local ok, loader = pcall(function()
local mods = require("src.mods.Loader").new()
mods.game = self
mods:load(self.data, modOpts)
return mods
end)
if ok and loader then
self.mods = loader
self.modStatus = loader:status()
else
require("src.core.Logger").error(
"mods failed to load, continuing without them: %s", tostring(loader))
end
-- After the merge, so a translation mod's catalog is what Strings() reads,
-- as Game (src/core/Game.lua) and Game2 do. Without it the catalog the
-- launcher preloaded (every enabled mod's lang/strings.lua, whatever game
-- it targets) stayed in place for the whole FireRed session.
require("src.core.Strings").load(self.data)
local okC, Gen3Compat = pcall(require, "src.mods.Gen3Compat")
if okC and type(Gen3Compat) == "table" and Gen3Compat.applyMerged then
local okA, err = pcall(Gen3Compat.applyMerged, self)
if not okA then
require("src.core.Logger").error("Gen3Compat.applyMerged failed: %s", tostring(err))
end
end
end
function Game3:adoptSave(session, seedBuckets)
if type(session) ~= "table" then return end
if type(session.modData) ~= "table" then session.modData = {} end
local loader = self.mods
if not loader then return end
if seedBuckets then
for id, bucket in pairs(loader.modSave or {}) do
if session.modData[id] == nil then session.modData[id] = bucket end
end
end
loader.modSave = session.modData
end
function Game3:writeOptions()
if type(self.options) ~= "table" then return end
if SaveData.saveOptions then pcall(SaveData.saveOptions, self.options) end
end
Game3.persistOptions = Game3.writeOptions
function Game3:applyOptions(opts)
opts = opts or self.options or {}
self.options = opts
local function try(mod, fn, ...)
local ok, m = pcall(require, mod)
if not ok or type(m) ~= "table" then return nil end
local f = m[fn]
if type(f) ~= "function" then return nil end
local okCall, res = pcall(f, ...)
if not okCall then return nil end
return res
end
Audio.applyEngineOptions(opts)
local cartOpts = Options.block(opts)
require("src.core.game3.void_fill").setMode(cartOpts.voidFill)
pcall(function()
require("src.ui.game3.chrome").setFrameType(cartOpts.frameType)
end)
try("src.render.Tilt", "applyOptions", opts)
try("src.render.Letterbox", "applyOptions", opts)
try("src.render.Zoom", "applyOptions", opts)
try("src.core.VideoMode", "applyOptions", opts)
try("src.core.Orientation", "applyOptions", opts)
local FaithfulRes = require("src.core.FaithfulRes")
if FaithfulRes.setNativeSize then
FaithfulRes.setNativeSize(Display.W, Display.H)
end
try("src.core.FaithfulRes", "applyOptions", opts)
try("src.core.ScreenPosition", "applyOptions", opts)
try("src.core.VSync", "applyOptions", opts)
try("src.core.FrameCap", "applyOptions", opts)
try("src.core.LogicClock", "applyOptions", opts)
try("src.core.PresentSync", "applyFixedStepPeriod")
local caps = try("src.core.Performance", "applyOptions", opts)
if type(caps) == "table" then
if not caps.tilt then try("src.render.Tilt", "setLevel", 0) end
local okZ, Zoom = pcall(require, "src.render.Zoom")
if okZ and Zoom then
Zoom.allowSurvey = caps.survey
if not caps.survey and (Zoom.offset or 0) < 0 then Zoom.offset = 0 end
end
if caps.fpsMax then
try("src.core.FrameCap", "clampToPerformance", caps.fpsMax)
end
end
if self.touchControls then
self.touchControls:applyOptions({
touchControls = opts.touchControls,
haptics = opts.haptics,
hotbar = opts.hotbar,
})
end
if self.input and opts.bindings then
self.input:applyBindings(opts.bindings)
end
end
-- pokefirered/src/main.c:325
function Game3:_aliasLA()
local input = self.input
if not input or not input.setButtonAlias then return end
local on
if self.session then
on = Options.lEqualsA(self.session)
elseif type(self.options) == "table" then
on = tonumber(Options.block(self.options).buttonMode) == 2
end
input:setButtonAlias("l", on and "a" or nil)
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
-- src/item_menu.c:2025
local Map = package.loaded["src.core.game3.map"]
if Map and require("src.core.game3.link.union_room").isUnionMap(Map.current) then return end
-- src/overworld.c:2813
local Link = package.loaded["src.core.game3.link"]
if type(Link) == "table" and Link.link ~= nil and Link.inLinkRoom() == true 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 P = package.loaded["src.core.game3.player"]
if P and P.boulderPush 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
local ok, kind, text = ItemUse.useField(session, session.bag, item, nil)
if kind == "vs_seeker" then
-- pokefirered/src/item_use.c:712
if ok then
require("src.core.game3.vs_seeker").use(session, self)
elseif text then
require("src.ui.game3.hud").openMessage(self, text)
end
return
end
-- pokefirered/src/item_use.c:159 SetUpItemUseOnFieldCallback
if text and not (ok and FIELD_CB_SILENT[kind]) then
require("src.ui.game3.hud").openMessage(self, text)
end
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
if ModRuntime.wants("save.loading") then
ModRuntime.emit("save.loading", { raw = save })
end
local activeMods = self.modStatus and self.modStatus.loaded
if SaveData.runMigrations then
SaveData.runMigrations(save, self.mods and self.mods.migrations, activeMods)
end
local modsDiff = SaveData.modsDiff and SaveData.modsDiff(save, activeMods) or nil
local session = Schema.fromSaveTable(save)
Options.bind(session, self.options)
self:adoptSave(session, not self._modSaveAdopted)
self._modSaveAdopted = true
self.sessionStartedAt = os.time()
self.questPlayback = QuestLog.begin(session)
if self.questPlayback then
self.session=session
self.phase="quest_log"
Audio.stopAll()
else
self:_enterField(session, "continue")
end
if modsDiff and SaveData.modsDiffNotice then
local notice = SaveData.modsDiffNotice(modsDiff, save.meta)
if notice then require("src.core.Logger").warn("%s", notice) end
end
if ModRuntime.wants("save.loaded") then
ModRuntime.emit("save.loaded", { save = session, meta = session.meta, modsDiff = modsDiff })
end
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:adoptSave(session, not self._modSaveAdopted)
self._modSaveAdopted = true
self.sessionStartedAt = os.time()
if ModRuntime.wants("save.created") then
ModRuntime.emit("save.created", { save = session })
end
self:_enterField(session, "new_game")
return
end
if action.action == "exit" then
Audio.stopAll()
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)
self:_aliasLA()
if ModRuntime.wantsHook("input.step") then
ModRuntime.call("input.step", noop, self, dt or FixedStep.STEP)
end
if self.input and self.input.step then self.input:step() end
if self.phase == "arena" then
if self.session then Audio.applyOptions(self.session) end
if self.arena then self.arena:update(dt) end
return
end
if (self.input and self.input.softResetStep and self.input:softResetStep()) or self.softResetRequested then
self.softResetRequested = nil
if self.input then self.input:reset() end
if self.touchControls then self.touchControls:reset() end
self:returnToTitle()
return
end
if self.phase == "quest_log" then
local p=self.questPlayback
local scene=p:current()
Audio.applyOptions(self.session)
if scene and scene.song then Audio.playSong(scene.song) end
Audio.pumpBgm()
p:update({a=self.input:wasPressed("a"),b=self.input:wasPressed("b")})
if p.done then
self.questPlayback=nil
self.input:reset()
self:_enterField(self.session,"continue")
end
return
end
if Help.update(self) then
-- Keep streaming BGM fed without advancing fanfare/script callbacks.
Audio.pumpBgm()
return
end
if self.session then Audio.applyOptions(self.session) 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:_handleRegisteredItem()
if Runtime.isActive() then
Runtime.update(dt)
QuestRecorder.update(self)
end
end
end
function Game3:speedCategory()
local okB, Battle = pcall(require, "src.core.game3.battle")
if okB and Battle and Battle.isActive and Battle.isActive() then
return "battle"
end
if self.phase == "field" then return "overworld" end
return "menu"
end
function Game3:isFixedSpeed()
if self.phase == "arena" then return true end
local Link = package.loaded["src.core.game3.link"]
if type(Link) == "table" and Link.link ~= nil then return true end
local MG = package.loaded["src.core.game3.minigames.common"]
return type(MG) == "table" and MG.isActive ~= nil and MG.isActive() == true
end
local function unionRoomMap(id)
return require("src.core.game3.link.union_room").isUnionMap(id)
end
function Game3:speedLocked()
if self:isFixedSpeed() then return true, "link" end
local Battle = package.loaded["src.core.game3.battle"]
if type(Battle) == "table" and Battle.isActive and Battle.isActive() then
local st = Battle.getState and Battle.getState()
if type(st) == "table" and st.link then return true, "link" end
end
local Union = package.loaded["src.core.game3.link.union_room"]
if type(Union) == "table" and Union.isActive and Union.isActive() then
return true, "link"
end
local LinkMenu = package.loaded["src.ui.game3.link_menu"]
if type(LinkMenu) == "table" then
if LinkMenu.isOpen and LinkMenu.isOpen() then return true, "link" end
local Direct = LinkMenu.Direct
if type(Direct) == "table" and Direct.isOpen and Direct.isOpen() then
return true, "link"
end
end
if self.phase ~= "field" then return false end
local Map = package.loaded["src.core.game3.map"]
if type(Map) == "table" and unionRoomMap(Map.current) then
return true, "link"
end
local Link = package.loaded["src.core.game3.link"]
if type(Link) == "table" and Link.inLinkRoom then
local ok, inRoom = pcall(Link.inLinkRoom)
if ok and inRoom == true then return true, "link" end
end
return false
end
function Game3:_speedLockEdge()
if (self._frameSpeed or 1) > 1 and self:speedLocked() then
FixedStep:endFrame()
end
end
function Game3:logicSpeed()
if self:speedLocked() then return 1 end
local override = tonumber(self.speedOverride)
if override then return math.max(1, override) end
local b = self.phase == "boot" and self.boot
if b and (b.phase == Boot.PHASE.INTRO or b.phase == Boot.PHASE.TITLE
or b.phase == Boot.PHASE.TITLE_CRY or b.phase == Boot.PHASE.TITLE_RESTART) then
return 1
end
local GameSpeed = require("src.core.GameSpeed")
local opts = self.options
if type(opts) ~= "table" then return 1 end
local key = GameSpeed.optionKey(self:speedCategory())
return math.max(1, GameSpeed.clamp(opts[key]))
end
function Game3:_cycleSpeed(dir)
if type(self.options) ~= "table" then return end
if self:speedLocked() then return end
local GameSpeed = require("src.core.GameSpeed")
local key = GameSpeed.optionKey(self:speedCategory())
self.options[key] = GameSpeed.cycle(self.options[key], dir)
self:writeOptions()
end
function Game3:zoomGateOK()
if self.phase ~= "field" then return false end
local okB, Battle = pcall(require, "src.core.game3.battle")
if okB and Battle and Battle.isActive and Battle.isActive() then return false end
local Field = package.loaded["src.core.game3.field"]
if Field and Field.locked then return false end
local Shop = package.loaded["src.ui.game3.shop_menu"]
if Shop and Shop.isShopCamera and Shop.isShopCamera() then return false end
return true
end
function Game3:zoomStep(delta)
if not self:zoomGateOK() then return end
local Zoom = require("src.render.Zoom")
local Renderer = require("src.render.Renderer")
local offset = Zoom.step(delta, Renderer:fitScale())
if type(self.options) == "table" then
self.options.zoom = offset
self:writeOptions()
end
end
function Game3:update(dt)
local speed = self:logicSpeed()
self._frameSpeed = speed
FixedStep.maxAccum = FixedStep.catchupLimit(speed, dt)
FixedStep:update(dt, speed)
self._audioAccum = (self._audioAccum or 0) + dt
local STEP = 1 / 60
local guard = 0
while self._audioAccum >= STEP and guard < 8 do
self._audioAccum = self._audioAccum - STEP
guard = guard + 1
local okA, errA = pcall(Audio.update, STEP)
if not okA then s9log("audio", errA) end
end
if self._audioAccum > 0.25 then self._audioAccum = 0 end
local okT, errT = pcall(function() require("src.render.Tilt").update(dt) end)
if not okT then s9log("tilt", errT) end
pcall(function() require("src.core.DiscordPresence").update(dt) end)
end
function Game3:_drawHud(w, h)
if not ModRuntime.wantsHook("render.hud") then return end
local scale, ox, oy, _, _, scaleY = Display.fit(w, h)
local viewport = {
width = w, height = h,
gameX = ox, gameY = oy,
gameWidth = Display.W * scale, gameHeight = Display.H * (scaleY or scale),
scale = scale,
}
love.graphics.push("all")
local okR, errR = pcall(function() ModRuntime.call("render.hud", noop, self, viewport) end)
if not okR then s9log("render.hud", errR) end
love.graphics.pop()
end
function Game3:draw()
local w = love.graphics.getWidth()
local h = love.graphics.getHeight()
if self.phase == "arena" then
if self.arena then self.arena:draw(w, h) end
self:_drawHud(w, h)
if self.touchControls then self.touchControls:draw() end
return
end
if self.phase == "quest_log" or (self.phase == "boot" and self.boot) then
local kind = (self.phase == "quest_log") and "quest" or "boot"
local function drawBootFrame()
if self.phase == "quest_log" then QuestLog.draw(self.questPlayback,self.session)
elseif Help.isOpen() then Help.draw() else Boot.draw(self.boot) end
end
if not Display.presentUi(self, w, h, kind, drawBootFrame) then
local canvas = Display.ensureCanvas("main")
if canvas then
love.graphics.push("all")
love.graphics.setCanvas(canvas)
love.graphics.origin()
drawBootFrame()
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
drawBootFrame()
end
end
self:_drawHud(w, h)
if self.touchControls then
self.touchControls:draw()
end
return
end
if Runtime.isActive() then
if Display.present(self, w, h) then
self:_drawHud(w, h)
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")
self:_drawHud(w, h)
if self.touchControls then
self.touchControls:draw()
end
end
function Game3:_hotkey(key)
local hk = Input.hotkeyKey(key)
if key == "f1" then
if self.phase == "field" and self:saveOffered() then self:saveGame() end
return true
elseif key == "f2" then
if self.phase == "field" then
pcall(function() require("src.ui.game3.stack").clear() end)
pcall(function()
local R = require("src.core.game3.runtime")
if R.stop then R.stop(nil, self) end
end)
pcall(function() require("src.core.game3.ghosts").clear() end)
self:_handleBootAction({ action = "continue" })
end
return true
elseif hk == "1" then
self:_cycleSpeed(1)
return true
elseif hk == "3" then
if self:zoomGateOK() then
local Tilt = require("src.render.Tilt")
Tilt.cycle()
if type(self.options) == "table" then
self.options.tilt = Tilt.level
self:writeOptions()
end
end
return true
elseif hk == "4" then
if self:zoomGateOK() then
local Zoom = require("src.render.Zoom")
local Renderer = require("src.render.Renderer")
local offset = Zoom.cycle(Renderer:fitScale())
if type(self.options) == "table" then
self.options.zoom = offset
self:writeOptions()
end
end
return true
elseif key == "-" or key == "kp-" then
self:zoomStep(-1)
return true
elseif key == "=" or key == "kp+" then
self:zoomStep(1)
return true
end
return false
end
function Game3:keypressed(key)
local function vanilla()
if self:_hotkey(key) then return end
if self.input and self.input.keypressed then self.input:keypressed(key) end
end
if not ModRuntime.wantsHook("input.key") then return vanilla() end
return ModRuntime.call("input.key", vanilla, self, { phase = "pressed", key = key })
end
function Game3:keyreleased(key)
local function vanilla()
if self.input and self.input.keyreleased then self.input:keyreleased(key) end
end
if not ModRuntime.wantsHook("input.key") then return vanilla() end
return ModRuntime.call("input.key", vanilla, self, { phase = "released", key = key })
end
function Game3:_padPressedBody(joystick, button)
if self.touchControls then self.touchControls:noteGamepad() end
local Input2 = self.input
if not Input2 then return end
local selectHeld = Input2.isDown and Input2:isDown("select")
if not selectHeld and joystick and joystick.isGamepadDown then
local ok, down = pcall(function() return joystick:isGamepadDown("back") end)
selectHeld = ok and down == true
end
local isShoulder = button == "leftshoulder" or button == "rightshoulder"
if not selectHeld and not isShoulder and Input2.padAction then
local action = Input2:padAction(button)
if action == "speedUp" then
self:_cycleSpeed(1)
return
elseif action == "speedDown" then
self:_cycleSpeed(-1)
return
end
end
if selectHeld then
local GamepadMap = require("src.core.GamepadMap")
local digit = GamepadMap.displayChordDigit(button)
if digit and self:_hotkey(digit) then return end
end
if Input2.gamepadpressed then Input2:gamepadpressed(joystick, button) end
end
function Game3:_padReleasedBody(joystick, button)
if self.input and self.input.gamepadreleased then
self.input:gamepadreleased(joystick, button)
end
end
function Game3:gamepadpressed(joystick, button)
if self.input and self.input.padEventSeen then self.input:padEventSeen(button) end
local function vanilla() self:_padPressedBody(joystick, button) end
if not ModRuntime.wantsHook("input.gamepad") then return vanilla() end
return ModRuntime.call("input.gamepad", vanilla, self,
{ phase = "pressed", joystick = joystick, button = button })
end
function Game3:gamepadreleased(joystick, button)
local function vanilla() self:_padReleasedBody(joystick, button) end
if not ModRuntime.wantsHook("input.gamepad") then return vanilla() end
return ModRuntime.call("input.gamepad", vanilla, self,
{ phase = "released", joystick = joystick, button = button })
end
-- pokefirered/src/start_menu.c:198
function Game3:saveOffered()
local session = (Runtime.getSession and Runtime.getSession()) or self.session
return require("src.ui.game3.start_menu").saveOffered(session, self)
end
function Game3:saveGame()
if not self.session or self.phase == "quest_log" or self.phase == "arena" then return end
if ModRuntime.wantsHook("save.write")
and ModRuntime.call("save.write", function() return true end, self) == false then
return false
end
if Runtime.getSession then
local s = Runtime.getSession()
if s then self.session = s end
end
pcall(function()
require("src.core.game3.scripting.space").persistSession(nil, self)
end)
QuestRecorder.save(self)
local save = Schema.toSaveTable(self.session)
if SaveData.buildMeta then
save.meta = SaveData.buildMeta(
self.modStatus and self.modStatus.loaded, save.meta, self.sessionStartedAt)
self.session.meta = save.meta
end
self.save = save
if ModRuntime.wants("save.writing") then
ModRuntime.emit("save.writing", { save = save, meta = save.meta })
end
if not SaveData.save then return false end
local ok, written = pcall(SaveData.save, save)
return ok and written ~= false
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(_, dy)
if type(dy) ~= "number" then return end
local function vanilla()
if dy > 0 then
self:zoomStep(1)
elseif dy < 0 then
self:zoomStep(-1)
end
end
if not ModRuntime.wantsHook("input.wheel") then return vanilla() end
return ModRuntime.call("input.wheel", vanilla, self, dy)
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)
local function vanilla()
if math.abs(value) > 0.5 and self.touchControls then
self.touchControls:noteGamepad()
end
if self.input and self.input.triggerAxis then
local trigger, phase = self.input:triggerAxis(axis, value)
if trigger then
if phase == "pressed" then
self:_padPressedBody(joystick, trigger)
elseif phase == "released" then
self:_padReleasedBody(joystick, trigger)
end
return
end
end
if self.input and self.input.gamepadaxis then self.input:gamepadaxis(joystick, axis, value) end
end
if not ModRuntime.wantsHook("input.gamepad") then return vanilla() end
return ModRuntime.call("input.gamepad", vanilla, self,
{ phase = "axis", joystick = joystick, axis = axis, value = value })
end
function Game3:joystickpressed(joystick, button)
if self.touchControls then self.touchControls:noteGamepad() end
local Input2 = self.input
if Input2 and Input2.joyAction
and not (Input2.isDown and Input2:isDown("select")) then
local action = Input2:joyAction(button)
if action == "speedUp" then
self:_cycleSpeed(1)
return
elseif action == "speedDown" then
self:_cycleSpeed(-1)
return
end
end
if Input2 and Input2.joystickpressed then Input2: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:_releaseModInput()
if self.mods and self.mods.releaseModInput then self.mods:releaseModInput() end
end
function Game3:joystickadded()
self:_releaseModInput()
end
function Game3:joystickremoved(joystick)
self:_releaseModInput()
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
self:_releaseModInput()
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
local function closeFlag(flag)
return function(m)
m[flag] = false
for k, v in pairs(m) do
if type(k) == "string" and type(v) == "function" and k:match("^_on%u") then m[k] = nil end
end
end
end
local function dropField(field)
return function(m) m[field] = nil end
end
-- pokefirered/src/main.c:480
local SOFT_RESET = {
{ "src.core.game3.battle.init", "reset" },
{ "src.core.game3.battle_bridge", "reset" },
{ "src.core.game3.battle_transition", "abort" },
{ "src.core.game3.task", "clear" },
{ "src.core.game3.step_events", "flush" },
{ "src.core.game3.field_move_show_mon", "reset" },
{ "src.core.game3.pokecenter_heal", dropField("_fx") },
{ "src.core.game3.ss_anne_cutscene", "reset" },
{ "src.core.game3.itemfinder", "reset" },
{ "src.core.game3.special_field_anim", "stopEscalator" },
{ "src.core.game3.pc_anim", "reset" },
{ "src.core.game3.camera_object", "reset" },
{ "src.core.game3.trade_scene", "cancel" },
{ "src.core.game3.scripting.natives_listmenu", "close" },
{ "src.ui.game3.hud", "clearWaitButton" },
{ "src.ui.game3.mon_pic", "hide" },
{ "src.ui.game3.seagallop", "stop" },
{ "src.ui.game3.map_preview_screen", "reset" },
{ "src.ui.game3.cave_transition", "clear" },
{ "src.ui.game3.map_name_popup", "dismiss" },
{ "src.ui.game3.help_window", "reset" },
{ "src.ui.game3.whiteout_rush", dropField("_state") },
{ "src.ui.game3.trade_scene", "close" },
{ "src.ui.game3.shop_menu", "reset" },
{ "src.ui.game3.choice", "reset" },
{ "src.ui.game3.message", "reset" },
{ "src.ui.game3.money_box", "hide" },
{ "src.ui.game3.coins_box", "hide" },
{ "src.ui.game3.elevator_window", "hide" },
{ "src.ui.game3.berry_powder_box", "hide" },
{ "src.ui.game3.minigame_records", "reset" },
{ "src.ui.game3.hall_of_fame", "reset" },
{ "src.ui.game3.hall_of_fame_pc", "reset" },
{ "src.ui.game3.credits", "reset" },
{ "src.ui.game3.start_menu", closeFlag("open") },
{ "src.ui.game3.start_menu", "resetCursor" }, -- pokefirered/src/start_menu.c:64
{ "src.ui.game3.bag_menu", closeFlag("open") },
{ "src.ui.game3.berry_pouch", closeFlag("open") },
{ "src.ui.game3.tm_case", closeFlag("open") },
{ "src.ui.game3.party_menu", closeFlag("open") },
{ "src.ui.game3.summary_menu", closeFlag("open") },
{ "src.ui.game3.pokedex", closeFlag("open") },
{ "src.ui.game3.option_menu", closeFlag("open") },
{ "src.ui.game3.save_menu", closeFlag("open") },
{ "src.ui.game3.trainer_card", closeFlag("open") },
{ "src.ui.game3.pc_menu", closeFlag("open") },
{ "src.ui.game3.item_pc", closeFlag("open") },
{ "src.ui.game3.box_storage_ui", closeFlag("open") },
{ "src.ui.game3.region_map", closeFlag("open") },
{ "src.ui.game3.daycare_menu", closeFlag("open") },
{ "src.ui.game3.fame_checker", closeFlag("open") },
{ "src.ui.game3.move_relearner", closeFlag("open") },
{ "src.ui.game3.egg_hatch", closeFlag("open") },
{ "src.ui.game3.evolution_scene", closeFlag("open") },
{ "src.ui.game3.diploma", closeFlag("open") },
{ "src.ui.game3.trainer_tower_records", closeFlag("open") },
{ "src.ui.game3.teachy_tv", closeFlag("open") },
{ "src.ui.game3.slot_machine", closeFlag("open") },
{ "src.ui.game3.mod_manager", closeFlag("open") },
{ "src.ui.game3.prize_corner", "reset" },
{ "src.ui.game3.stat_growth", closeFlag("_open") },
{ "src.ui.game3.release_seq", function(m) m.active = false; m.onComplete = nil end },
{ "src.ui.game3.naming", closeFlag("openFlag") },
{ "src.ui.game3.easy_chat", closeFlag("openFlag") },
{ "src.ui.game3.fade", "clear" },
}
local function clearFieldScreens()
for _, e in ipairs(SOFT_RESET) do
local mod = package.loaded[e[1]]
if type(mod) == "table" then
local fn = e[2]
if type(fn) == "string" then
if mod[fn] then pcall(mod[fn]) end
else
pcall(fn, mod)
end
end
end
local LeagueLighting = package.loaded["src.core.game3.league_lighting"]
if LeagueLighting and LeagueLighting.reset then pcall(LeagueLighting.reset) end
local Map = require("src.core.game3.map")
Map.disableMusicChange = Map.MUSIC_DISABLE_OFF
local FieldView = require("src.core.game3.field_view")
FieldView.hideActors = false
FieldView.setCameraPanning(0, 0)
end
function Game3:returnToTitle()
self.questPlayback=nil
Help.reset()
Audio.stopAll()
local Stack = require("src.ui.game3.stack")
Stack.clear()
clearFieldScreens()
if Runtime.isActive() then
Runtime.stop(nil, self)
end
local Objects = package.loaded["src.core.game3.objects"]
if Objects and Objects.reset then pcall(Objects.reset) end
local Field = package.loaded["src.core.game3.field"]
if Field and Field.clearMetatiles then pcall(Field.clearMetatiles) 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)
Boot.setTextSpeed(self.boot, Options.block(self.options).textSpeed)
self.boot.phase = Boot.PHASE.TITLE
self.boot.timer = 0
local TitleScreen = require("src.ui.game3.title_screen")
TitleScreen.enter(self.boot)
end
function Game3:reset()
self.questPlayback = nil
Help.reset()
Audio.endSession()
require("src.ui.game3.stack").clear()
clearFieldScreens()
local WarpMod = package.loaded["src.core.game3.warp"]
if WarpMod and WarpMod.clear then pcall(WarpMod.clear) end
local DoorsMod = package.loaded["src.core.game3.doors"]
if DoorsMod and DoorsMod.release then pcall(DoorsMod.release) end
if Runtime.isActive() then
pcall(function() Runtime.stop(nil, self) end)
end
local Ghosts = package.loaded["src.core.game3.ghosts"]
if Ghosts and Ghosts.clear then pcall(Ghosts.clear) end
for _, name in ipairs({ "src.core.game3.oam", "src.core.game3.bg", "src.core.game3.objects" }) do
local mod = package.loaded[name]
if mod and mod.reset then pcall(mod.reset) end
end
local Field = package.loaded["src.core.game3.field"]
if Field and Field.clearMetatiles then pcall(Field.clearMetatiles) end
Display.release()
if self.touchControls then
pcall(function() self.touchControls:setHotkeyHandler(nil) end)
self.touchControls = nil
end
self.boot = nil
self.arena = nil
self.session = nil
self.data = nil
self.mods = nil
self.modStatus = nil
self._modSaveAdopted = nil
self.phase = "boot"
self.returnToLauncher = nil
self.onExit = nil
end
function Game3:quit()
self:saveGame()
end
return Game3