mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-01 15:41:29 -04:00
462 lines
15 KiB
Lua
462 lines
15 KiB
Lua
-- Game3 primary runtime on Sevii (P0).
|
|
-- Pauses host field locomotion (_game3FieldPause); pumps RTC / playtime (H1).
|
|
-- Game3 Player owns walk/run; dialogs + START go through game3 HUD.
|
|
|
|
local MapIds = require("src.core.game3.map_ids")
|
|
local Runtime = {}
|
|
|
|
Runtime.active = false
|
|
Runtime.session = nil
|
|
Runtime._mod = nil
|
|
Runtime._game = nil
|
|
Runtime._fieldLocked = false
|
|
Runtime._playTimeAcc = 0
|
|
|
|
local function log(msg)
|
|
print("[game3] " .. tostring(msg))
|
|
local mod = Runtime._mod
|
|
if mod and mod.log and mod.log.info then
|
|
mod.log:info("[game3] " .. tostring(msg))
|
|
end
|
|
end
|
|
Runtime.log = log
|
|
|
|
function Runtime.isActive()
|
|
return Runtime.active == true
|
|
end
|
|
|
|
function Runtime.getSession()
|
|
return Runtime.session
|
|
end
|
|
|
|
function Runtime.pumpRtc(game, dt)
|
|
local session = Runtime.session or (game and game.session)
|
|
local save = game and game.save
|
|
if not session and not save then return end
|
|
dt = tonumber(dt) or (1 / 60)
|
|
|
|
-- Standalone FRLG playtime accumulator (no hardware RTC, no Gen2 Clock).
|
|
local pt = (session and (session.playtime or session.playTime))
|
|
or (save and (save.playTime or save.playtime))
|
|
if type(pt) ~= "table" then
|
|
pt = { hours = 0, minutes = 0, seconds = 0, vblanks = 0 }
|
|
end
|
|
pt.hours = tonumber(pt.hours) or 0
|
|
pt.minutes = tonumber(pt.minutes) or 0
|
|
pt.seconds = tonumber(pt.seconds) or 0
|
|
pt.vblanks = tonumber(pt.vblanks) or 0
|
|
|
|
Runtime._playTimeAcc = (Runtime._playTimeAcc or 0) + dt
|
|
while Runtime._playTimeAcc >= 1 do
|
|
Runtime._playTimeAcc = Runtime._playTimeAcc - 1
|
|
pt.seconds = pt.seconds + 1
|
|
if pt.seconds >= 60 then
|
|
pt.seconds = 0
|
|
pt.minutes = pt.minutes + 1
|
|
if pt.minutes >= 60 then
|
|
pt.minutes = 0
|
|
pt.hours = math.min(999, pt.hours + 1)
|
|
end
|
|
end
|
|
end
|
|
|
|
if session then
|
|
session.playtime = pt
|
|
session.playTime = pt
|
|
session.playTimeHours = pt.hours
|
|
session.playTimeMinutes = pt.minutes
|
|
session.playTimeSeconds = pt.seconds
|
|
session.hours = pt.hours
|
|
session.minutes = pt.minutes
|
|
session.seconds = pt.seconds
|
|
end
|
|
if save then
|
|
save.playTime = pt
|
|
save.playtime = pt
|
|
save.playTimeHours = pt.hours
|
|
save.playTimeMinutes = pt.minutes
|
|
save.playTimeSeconds = pt.seconds
|
|
end
|
|
end
|
|
|
|
local function mark_host_game3(game, on)
|
|
local world = game and (game.overworld or game.world)
|
|
if world then
|
|
world._game3FieldPause = on and true or false
|
|
end
|
|
if game then
|
|
game._game3Active = on and true or false
|
|
end
|
|
end
|
|
|
|
--- Start Game3 session. opts.alreadyOnMap skips Map.load warp (save load / adopt).
|
|
function Runtime.start(mod, game, session, opts)
|
|
opts = opts or {}
|
|
Runtime._mod = mod
|
|
Runtime._game = game
|
|
Runtime.session = session
|
|
Runtime.active = true
|
|
mark_host_game3(game, true)
|
|
|
|
log(string.format(
|
|
"ACTIVE engine=game3 map=%s pos=%s,%s reason=%s",
|
|
tostring(session and session.map),
|
|
tostring(session and session.x),
|
|
tostring(session and session.y),
|
|
tostring(opts.reason or (opts.alreadyOnMap and "adopt" or "enter"))))
|
|
|
|
local Field = require("src.core.game3.field")
|
|
Field.start(mod, game, session)
|
|
|
|
local Dataset = require("src.core.game3.dataset")
|
|
local cache = (mod and mod.cache) or (Dataset.cache and Dataset.cache())
|
|
local okPk, Pokemon = pcall(require, "src.core.game3.pokemon")
|
|
if okPk and Pokemon and Pokemon.install then
|
|
Pokemon.install(cache)
|
|
end
|
|
local okPc, PartyChrome = pcall(require, "src.ui.game3.party_chrome")
|
|
if okPc and PartyChrome and PartyChrome.install then
|
|
PartyChrome.install(cache)
|
|
end
|
|
local okBc, BattleChrome = pcall(require, "src.ui.game3.battle_chrome")
|
|
if okBc and BattleChrome and BattleChrome.install then
|
|
BattleChrome.install(cache)
|
|
end
|
|
|
|
if opts.alreadyOnMap then
|
|
-- Stay on current host map; only ensure Space VM + depth-1 neighbors.
|
|
local Map = require("src.core.game3.map")
|
|
local def = game and game.data and game.data.maps and game.data.maps[session.map]
|
|
Map.current = session.map
|
|
Map.loadNeighborsDepth1(game, def)
|
|
-- Space.onMapEnter already ran (or will run) from afterMap — don't double.
|
|
log("adopted existing game3 map (no re-warp)")
|
|
else
|
|
local Map = require("src.core.game3.map")
|
|
Map.load(mod, game, session.map, {
|
|
x = session.x,
|
|
y = session.y,
|
|
facing = session.facing,
|
|
depth1Connections = true,
|
|
})
|
|
log("warped via game3 map loader → " .. tostring(session.map))
|
|
end
|
|
end
|
|
|
|
function Runtime.stop(mod, game)
|
|
log("INACTIVE leaving game3 field")
|
|
local Field = require("src.core.game3.field")
|
|
Field.stop()
|
|
mark_host_game3(game or Runtime._game, false)
|
|
Runtime.active = false
|
|
Runtime.session = nil
|
|
local Space = package.loaded["src.core.game3.scripting.space"]
|
|
if Space and Space.deactivate then
|
|
Space.deactivate(mod or Runtime._mod)
|
|
end
|
|
Runtime._mod = nil
|
|
Runtime._game = nil
|
|
end
|
|
|
|
function Runtime.update(dt)
|
|
if not Runtime.active then return end
|
|
local game = Runtime._game
|
|
Runtime.pumpRtc(game, dt)
|
|
|
|
local okF, Fade = pcall(require, "src.ui.game3.fade")
|
|
if okF and Fade.tick then Fade.tick(dt) end
|
|
local okTr, BattleTransition = pcall(require, "src.core.game3.battle_transition")
|
|
if okTr and BattleTransition.isActive and BattleTransition.isActive() then
|
|
BattleTransition.tick(dt)
|
|
end
|
|
local okT, Task = pcall(require, "src.core.game3.task")
|
|
if okT and Task.update then Task.update(dt) end
|
|
local okA, Audio = pcall(require, "src.core.game3.audio")
|
|
if okA and Audio.update then
|
|
-- Cry tick is folded into Audio.update (called from Game3:fixedUpdate).
|
|
elseif okA and Audio.tickCry then
|
|
Audio.tickCry(dt)
|
|
end
|
|
|
|
local Battle = require("src.core.game3.battle")
|
|
if Battle.isActive() then
|
|
Battle.update(dt, game)
|
|
-- Keep script VM + message typewriter alive while battle runs.
|
|
local Space = package.loaded["src.core.game3.scripting.space"]
|
|
if Space and Space.vm then
|
|
local ad = Space.vm.adapters
|
|
if ad and ad.pollMovement then ad.pollMovement(0) end
|
|
Space.vm:tick()
|
|
end
|
|
local Message = package.loaded["src.ui.game3.message"]
|
|
if Message and Message.tick then Message.tick() end
|
|
local Hud = require("src.ui.game3.hud")
|
|
Hud.update(game, dt)
|
|
return
|
|
end
|
|
|
|
local okN, Naming = pcall(require, "src.ui.game3.naming")
|
|
if okN and Naming.isOpen and Naming.isOpen() then
|
|
Naming.update(game and game.input, dt)
|
|
end
|
|
|
|
local Field = require("src.core.game3.field")
|
|
Field.update(dt)
|
|
local Hud = require("src.ui.game3.hud")
|
|
Hud.update(game, dt)
|
|
end
|
|
|
|
function Runtime.uiBusy()
|
|
local Hud = require("src.ui.game3.hud")
|
|
return Hud.busy()
|
|
end
|
|
|
|
function Runtime.setUi(stackTop)
|
|
Runtime._ui = stackTop
|
|
end
|
|
|
|
function Runtime.isSeviiMap(mapId)
|
|
return MapIds.isGame3Map(mapId)
|
|
end
|
|
|
|
local function current_map_id(game)
|
|
local world = game and (game.overworld or game.world)
|
|
if world and world.map and world.map.id then return world.map.id end
|
|
local pos = game and game.save and game.save.position
|
|
return pos and pos.map
|
|
end
|
|
|
|
local function player_xy(game)
|
|
-- Prefer game3 avatar when active.
|
|
local okP, Player = pcall(require, "src.core.game3.player")
|
|
if okP and Player and Runtime.active then
|
|
return Player.cellX, Player.cellY, Player.facing or "down"
|
|
end
|
|
local world = game and (game.overworld or game.world)
|
|
local p = world and world.player
|
|
if p then
|
|
return p.cellX or p.x, p.cellY or p.y, p.facing or "down"
|
|
end
|
|
local pos = game and game.save and game.save.position
|
|
if pos then return pos.x, pos.y, pos.facing or "down" end
|
|
return 0, 0, "down"
|
|
end
|
|
|
|
--- If save/load or warp lands on SEVII_* without ferry, adopt Game3 session.
|
|
function Runtime.ensureActiveForMap(mod, game, mapId)
|
|
if not MapIds.isGame3Map(mapId) then
|
|
if Runtime.active then
|
|
log("left game3 map=" .. tostring(mapId) .. " — tearing down")
|
|
local Bridge = require("src.core.game3.bridge")
|
|
Bridge.persistSessionOnly(mod, game)
|
|
Runtime.stop(mod, game)
|
|
else
|
|
log("map=" .. tostring(mapId) .. " (not a game3 map)")
|
|
end
|
|
return false
|
|
end
|
|
if Runtime.active then
|
|
local session = Runtime.session
|
|
local world = game and (game.overworld or game.world)
|
|
local p = world and world.player
|
|
local pos = game and game.save and game.save.position
|
|
-- Prefer host-placed coords when adopting.
|
|
-- stale game3 Player (Map.load no longer soft-syncs setMap).
|
|
local x, y, facing
|
|
if p and ((pos and pos.map == mapId) or (pos == nil)) then
|
|
x = tonumber(p.cellX or p.x) or 0
|
|
y = tonumber(p.cellY or p.y) or 0
|
|
facing = p.facing or "down"
|
|
elseif pos and pos.map == mapId then
|
|
x = tonumber(pos.x) or 0
|
|
y = tonumber(pos.y) or 0
|
|
facing = pos.facing or "down"
|
|
else
|
|
x, y, facing = player_xy(game)
|
|
end
|
|
if session then
|
|
session.map = mapId
|
|
session.x, session.y, session.facing = x, y, facing
|
|
end
|
|
-- Rebind EventObjects + collision for the new map. localIds are per-map.
|
|
local data = game and game.data and game.data.maps
|
|
local def = data and data[mapId]
|
|
local Map = package.loaded["src.core.game3.map"]
|
|
or require("src.core.game3.map")
|
|
Map.current = mapId
|
|
if def then
|
|
local Space = package.loaded["src.core.game3.scripting.space"]
|
|
or require("src.core.game3.scripting.space")
|
|
if Space.ensureBundle then Space.ensureBundle(mod) end
|
|
if Space.attachEventsToMaps then
|
|
Space.attachEventsToMaps({ [mapId] = def }, Space.bundle)
|
|
end
|
|
local okC, Collision = pcall(require, "src.core.game3.collision")
|
|
if okC and Collision and Collision.bindMap then
|
|
Collision.bindMap(game, mapId, def)
|
|
end
|
|
local okO, Objects = pcall(require, "src.core.game3.objects")
|
|
if okO and Objects and Objects.loadMap then
|
|
Objects.loadMap(game, mapId, def)
|
|
end
|
|
Map.loadNeighborsDepth1(game, def)
|
|
end
|
|
local okP, Player = pcall(require, "src.core.game3.player")
|
|
if okP and Player then
|
|
Player.reset(x, y, facing)
|
|
Player.syncSavePosition(game)
|
|
end
|
|
log("already ACTIVE on " .. tostring(mapId) .. " (rebound objects)")
|
|
return true
|
|
end
|
|
log("bootstrapping game3 on Sevii map load: " .. tostring(mapId))
|
|
local Bridge = require("src.core.game3.bridge")
|
|
local x, y, facing = player_xy(game)
|
|
Bridge.enterFromHost(mod, game, {
|
|
map = mapId,
|
|
x = x,
|
|
y = y,
|
|
facing = facing,
|
|
alreadyOnMap = true,
|
|
reason = "map_enter_or_load",
|
|
})
|
|
return true
|
|
end
|
|
|
|
function Runtime.install(mod)
|
|
if Runtime._installed then return end
|
|
Runtime._installed = true
|
|
Runtime._mod = mod
|
|
log("Runtime.install — display ownership + START intercept + game.ready")
|
|
|
|
local ok, World = pcall(require, "src.world.gen2.World")
|
|
if ok and World and World.step and not World._game3RuntimeStep then
|
|
local prev = World.step
|
|
World.step = function(self, ...)
|
|
if Runtime.active then
|
|
Runtime._game = self.game or Runtime._game
|
|
-- Honor field pause: do not run Gen2 locomotion / NPC AI / pollInput step.
|
|
-- Keep map fade + heal/fly anims so warps and nurse SFX still complete.
|
|
if self.mapSetup and self.updateMapSetup then
|
|
self:updateMapSetup()
|
|
end
|
|
if self.healAnim and self.stepHealAnim then self:stepHealAnim() end
|
|
if self.flyAnim and self.stepFlyAnim then self:stepFlyAnim() end
|
|
Runtime.update(1 / 60)
|
|
return
|
|
end
|
|
return prev(self, ...)
|
|
end
|
|
World._game3RuntimeStep = true
|
|
log("World:step hooked — host locomotion paused on Sevii")
|
|
end
|
|
if ok and World and World.pollInput and not World._game3PollInput then
|
|
local prevPoll = World.pollInput
|
|
World.pollInput = function(self, ...)
|
|
-- Game3 Player reads input from Field.update; never feed Gen2 heldDir.
|
|
if Runtime.active then return end
|
|
return prevPoll(self, ...)
|
|
end
|
|
World._game3PollInput = true
|
|
end
|
|
if ok and World and World.interact and not World._game3InteractGate then
|
|
local prevIx = World.interact
|
|
World.interact = function(self, ...)
|
|
-- Field.interact owns A-button talk on Sevii; never double-fire host path.
|
|
if Runtime.active then return end
|
|
return prevIx(self, ...)
|
|
end
|
|
World._game3InteractGate = true
|
|
end
|
|
|
|
-- Own the frame: replace Gen2 drawScene presentation while active.
|
|
local ok2, Game2 = pcall(require, "src.core.Game2")
|
|
if ok2 and Game2 and Game2.drawScene and not Game2._game3Display then
|
|
local prevDraw = Game2.drawScene
|
|
Game2.drawScene = function(self, w, h)
|
|
if Runtime.isActive() then
|
|
Runtime._game = self
|
|
local Display = require("src.core.game3.display")
|
|
if Display.present(self, w, h) then
|
|
return
|
|
end
|
|
end
|
|
return prevDraw(self, w, h)
|
|
end
|
|
Game2._game3Display = true
|
|
log("Game2:drawScene hooked — game3 owns FRLG 240x160 present")
|
|
end
|
|
|
|
if ok2 and Game2 and Game2.openStartMenu and not Game2._game3StartMenu then
|
|
local prevOpen = Game2.openStartMenu
|
|
Game2.openStartMenu = function(self, ...)
|
|
if Runtime.isActive() then
|
|
local Hud = require("src.ui.game3.hud")
|
|
log("START → game3 Start Menu (own display)")
|
|
Hud.openStartMenu(self, Runtime.getSession())
|
|
return
|
|
end
|
|
local mapId = current_map_id(self)
|
|
if MapIds.isGame3Map(mapId) then
|
|
Runtime.ensureActiveForMap(mod, self, mapId)
|
|
local Hud = require("src.ui.game3.hud")
|
|
Hud.openStartMenu(self, Runtime.getSession())
|
|
return
|
|
end
|
|
return prevOpen(self, ...)
|
|
end
|
|
Game2._game3StartMenu = true
|
|
end
|
|
|
|
-- Block Gen2 menu pushes on Sevii entirely.
|
|
local ok3, Screens = pcall(require, "src.ui.Screens")
|
|
if ok3 and Screens and Screens.push and not Screens._game3StartHook then
|
|
local prevPush = Screens.push
|
|
Screens.push = function(game, id, ...)
|
|
if id == "StartMenu" or id == "Gen2StartMenu" or id == "Gen2PackMenu"
|
|
or id == "PackMenu" or id == "Gen2Pokegear" then
|
|
local mapId = current_map_id(game)
|
|
if Runtime.isActive() or MapIds.isGame3Map(mapId) then
|
|
if not Runtime.isActive() and MapIds.isGame3Map(mapId) then
|
|
Runtime.ensureActiveForMap(mod, game, mapId)
|
|
end
|
|
if Runtime.isActive() then
|
|
local Hud = require("src.ui.game3.hud")
|
|
log("blocked Screens.push(" .. tostring(id) .. ") — game3 owns UI")
|
|
if id == "StartMenu" or id == "Gen2StartMenu" then
|
|
Hud.openStartMenu(game, Runtime.getSession())
|
|
elseif id == "Gen2Pokegear" or id == "PackMenu" or id == "Gen2PackMenu" then
|
|
local RegionMap = require("src.ui.game3.region_map")
|
|
RegionMap.show({ session = Runtime.getSession() })
|
|
end
|
|
return
|
|
end
|
|
end
|
|
end
|
|
return prevPush(game, id, ...)
|
|
end
|
|
Screens._game3StartHook = true
|
|
end
|
|
|
|
-- Also pump UI when Game2 logic would open start but world step is gated.
|
|
if ok2 and Game2 and Game2.update and not Game2._game3Logic then
|
|
-- openStartMenu already hooked; ensure START during uiBusy still works
|
|
-- via Game2's acceptsMenuInput path — openStartMenu intercept handles it.
|
|
Game2._game3Logic = true
|
|
end
|
|
|
|
if mod.events then
|
|
mod.events:on("game.ready", function(game)
|
|
Runtime._game = game
|
|
local mapId = current_map_id(game)
|
|
log("game.ready map=" .. tostring(mapId)
|
|
.. (MapIds.isGame3Map(mapId) and " → ensure game3" or " → host"))
|
|
if MapIds.isGame3Map(mapId) then
|
|
Runtime.ensureActiveForMap(mod, game, mapId)
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
return Runtime
|