Merge pull request #1354 from 1Jamie/fix/save-editor-lifecycle

This commit is contained in:
bryanthaboi
2026-08-15 13:59:32 -04:00
committed by GitHub
9 changed files with 221 additions and 106 deletions
+13 -5
View File
@@ -126,11 +126,19 @@ bundled game, in that case.
already driving the frame. A payload that must change `love.run` itself already driving the frame. A payload that must change `love.run` itself
needs a `minShell` bump so an older shell refuses to chainload it rather needs a `minShell` bump so an older shell refuses to chainload it rather
than running with half its intended behavior. than running with half its intended behavior.
- **Android has no in-app download transport yet.** `check_worker.lua` - **Android and iOS use the native download bridge, not curl.** Neither
shells out to curl for both the release check and the download; curl is platform ships curl, so the old `check_worker.lua` path (shell out to curl)
absent on Android, so `Check` degrades to `status = "error"` there (the always landed on `error` and the launcher chip's "Check for updates" tap
launcher UI hides on that status) and the player is directed to the was a no-op. The worker now talks through `HostShell`, the same transport
releases page via `Check.releaseUrl()` instead. as the mod catalog: curl on desktop, `love.system.httpDownload` on mobile.
On Android that is the GameActivity JNI/`HttpsURLConnection` bridge; on
iOS it is `GRPickerBridge.httpDownload` (`URLSession`). A fused sideloaded
APK or IPA can therefore check GitHub and fetch the `.love` payload
in-app. If neither transport exists, the worker reports `needs_full` and
the launcher chip opens `Check.releaseUrl()`. Native package-only changes
still need a full reinstall (`minShell` / `payloadHost` gate →
`needs_full`). Applying a downloaded payload on Android relaunches via
`love.system.restartApp`; iOS still uses in-process `quit("restart")`.
- **Dev/source runs never self-update.** `Boot.run` returns immediately when - **Dev/source runs never self-update.** `Boot.run` returns immediately when
`love.filesystem.isFused()` is false, and a working tree's `engine` is the `love.filesystem.isFused()` is false, and a working tree's `engine` is the
`"0.0.0-dev"` placeholder that always reports up to date, so a source `"0.0.0-dev"` placeholder that always reports up to date, so a source
+43 -3
View File
@@ -148,9 +148,44 @@ local function openEditor(version, slotId)
editorMode = true editorMode = true
resizeForEditor() resizeForEditor()
addEditorRequirePath() addEditorRequirePath()
EditorApp = require("App") local okReq, appOrErr = pcall(require, "App")
EditorApp.load(path, { version = version, slotId = slotId, embedded = true, if not okReq then
onClose = function() closeEditor() end }) editorMode = false
if version then
require("src.import.CacheFs").unmountVersion(version)
end
restoreWindow()
Importer = editorHost
editorHost = nil
editorVersion = nil
if Importer and Importer.resumeAfterOverlay then
Importer:resumeAfterOverlay()
end
refuse("Could not open the save editor (" .. tostring(appOrErr) .. ").")
return
end
EditorApp = appOrErr
local okLoad, loadErr = pcall(EditorApp.load, path, {
version = version, slotId = slotId, embedded = true,
onClose = function() closeEditor() end,
})
if not okLoad then
editorMode = false
if EditorApp.unload then pcall(EditorApp.unload) end
EditorApp = nil
if version then
require("src.import.CacheFs").unmountVersion(version)
require("src.core.Data"):unloadGenerated()
end
restoreWindow()
Importer = editorHost
editorHost = nil
editorVersion = nil
if Importer and Importer.resumeAfterOverlay then
Importer:resumeAfterOverlay()
end
refuse("Could not open the save editor (" .. tostring(loadErr) .. ").")
end
end end
-- Back to the launcher. Everything the editor mounted or cached has to come -- Back to the launcher. Everything the editor mounted or cached has to come
@@ -166,6 +201,11 @@ function closeEditor()
require("src.import.CacheFs").unmountVersion(version) require("src.import.CacheFs").unmountVersion(version)
require("src.core.Data"):unloadGenerated() require("src.core.Data"):unloadGenerated()
end end
for k in pairs(package.loaded) do
if type(k) == "string" and (k:find("save%-editor") or k == "App" or k == "Kit" or k == "State" or k == "Catalog" or k == "SaveIO" or k == "Ops" or k == "MonOps" or k == "ItemOps" or k == "PadInput" or k == "Gen" or k == "Theme") then
package.loaded[k] = nil
end
end
editorVersion = nil editorVersion = nil
restoreWindow() restoreWindow()
Importer = editorHost Importer = editorHost
+39 -15
View File
@@ -14,6 +14,14 @@ local MODULES = {
-- Optional for compatibility with developer and stale caches. -- Optional for compatibility with developer and stale caches.
local OPTIONAL = { "audio", "palettes", "icons" } local OPTIONAL = { "audio", "palettes", "icons" }
-- Gold's extractor never writes these Gen 1 tables (RomExtractorGen2 has
-- maps/text/pokemon/items, not text_pointers / trainer_headers / field).
-- Desktop can still `require` Red's copies from the source tree, so Gold
-- Edit appeared to work there; an Android APK has only the per-version
-- cache, so Data:load used to throw on the first Gold Edit and take the
-- activity down. Empty tables are enough for seedDefaults / the editor.
local GEN2_OPTIONAL = { text_pointers = true, trainer_headers = true, field = true }
-- Vanilla defaults for rules exposed through the constants registry. A -- Vanilla defaults for rules exposed through the constants registry. A
-- value has to exist before a mod can patch it; each one matches the -- value has to exist before a mod can patch it; each one matches the
-- engine's no-mod behavior, so seeding them changes nothing on a vanilla -- engine's no-mod behavior, so seeding them changes nothing on a vanilla
@@ -99,7 +107,11 @@ end
-- Fills only what the cache is missing, so an importer that learns to -- Fills only what the cache is missing, so an importer that learns to
-- stamp one of these keys silently takes over from the engine. -- stamp one of these keys silently takes over from the engine.
function Data:seedDefaults() function Data:seedDefaults()
local constants = self.constants local constants = self.constants or {}
self.constants = constants
self.field = self.field or {}
self.maps = self.maps or {}
self.pokemon = self.pokemon or {}
for key, value in pairs(CONSTANT_DEFAULTS) do for key, value in pairs(CONSTANT_DEFAULTS) do
if constants[key] == nil then constants[key] = copy(value) end if constants[key] == nil then constants[key] = copy(value) end
end end
@@ -108,7 +120,11 @@ function Data:seedDefaults()
if constants.dexSize == nil then if constants.dexSize == nil then
local highest = 0 local highest = 0
for _, def in pairs(self.pokemon) do for _, def in pairs(self.pokemon) do
if def.dex and def.dex > highest then highest = def.dex end -- Gold's pokemon.lua also carries growthRates / tmhmMoves / generation
-- scalars beside species rows.
if type(def) == "table" and def.dex and def.dex > highest then
highest = def.dex
end
end end
constants.dexSize = highest constants.dexSize = highest
end end
@@ -212,36 +228,41 @@ local function loadModule(dir, name)
if not chunk then return false, err end if not chunk then return false, err end
return pcall(chunk) return pcall(chunk)
end end
local ok, mod = pcall(require, "data.generated." .. name)
if ok then return true, mod end
-- Fused PhysFS / Blue|Yellow prefix: load bytes from the active version's
-- cache explicitly when require cannot see the mounted tree.
local CacheFs = require("src.import.CacheFs") local CacheFs = require("src.import.CacheFs")
local GameVersion = require("src.core.GameVersion") local GameVersion = require("src.core.GameVersion")
local path = "data/generated/" .. name .. ".lua" local path = "data/generated/" .. name .. ".lua"
local bytes = CacheFs.readActive(path) local bytes = CacheFs.readActive(path)
if type(bytes) == "string" then if type(bytes) == "string" then
local chunk, err = loadstring(bytes, "@" .. GameVersion.cachePrefix() .. path) local chunk = loadstring(bytes, "@" .. GameVersion.cachePrefix() .. path)
if not chunk then return false, err or mod end if chunk then
return pcall(chunk) local ok, res = pcall(chunk)
if ok then return true, res end
end
end end
return false, mod local ok, mod = pcall(require, "data.generated." .. name)
if ok then return true, mod end
return false, nil
end end
function Data:load() function Data:load()
local dir = os.getenv("POKEPORT_DATA_DIR") local dir = os.getenv("POKEPORT_DATA_DIR")
local gen2 = require("src.core.GameVersion").generation() == 2
for _, name in ipairs(MODULES) do for _, name in ipairs(MODULES) do
local ok, mod = loadModule(dir, name) local ok, mod = loadModule(dir, name)
if not ok then if not ok then
if dir then if gen2 and GEN2_OPTIONAL[name] then
self[name] = {}
elseif dir then
error(("missing data module '%s/%s.lua' (POKEPORT_DATA_DIR).\n(%s)") error(("missing data module '%s/%s.lua' (POKEPORT_DATA_DIR).\n(%s)")
:format(dir, name, mod)) :format(dir, name, mod))
else
error(("missing generated data module 'data/generated/%s.lua'.\n" ..
"Import the ROM again or rebuild developer data.\n(%s)")
:format(name, mod))
end end
error(("missing generated data module 'data/generated/%s.lua'.\n" .. else
"Import the ROM again or rebuild developer data.\n(%s)") self[name] = mod
:format(name, mod))
end end
self[name] = mod
end end
for _, name in ipairs(OPTIONAL) do for _, name in ipairs(OPTIONAL) do
local ok, mod = loadModule(dir, name) local ok, mod = loadModule(dir, name)
@@ -280,11 +301,14 @@ function Data:unloadGenerated()
if not pristine[key] then self[key] = nil end if not pristine[key] then self[key] = nil end
end end
end end
self._pristineKeys = nil
for _, name in ipairs(MODULES) do for _, name in ipairs(MODULES) do
package.loaded["data.generated." .. name] = nil package.loaded["data.generated." .. name] = nil
self[name] = nil
end end
for _, name in ipairs(OPTIONAL) do for _, name in ipairs(OPTIONAL) do
package.loaded["data.generated." .. name] = nil package.loaded["data.generated." .. name] = nil
self[name] = nil
end end
end end
+3 -3
View File
@@ -8,9 +8,9 @@
-- "update_check_state" worker -> main: { status, latest, progress, error } -- "update_check_state" worker -> main: { status, latest, progress, error }
-- --
-- Nothing here ever blocks or throws into the game loop: when love.thread is -- Nothing here ever blocks or throws into the game loop: when love.thread is
-- absent (the headless test stub) or the worker cannot run (no curl, Android), -- absent (the headless test stub) or the worker cannot run, state() reports
-- state() simply reports "error" and the UI hides itself. See the shared -- "error" (or the worker reports "needs_full" when there is no transport).
-- contract in the task brief for the status vocabulary and the file layout. -- See the shared contract in the task brief for the status vocabulary.
-- --
-- The release-JSON extraction and the sums parsing are exported as pure -- The release-JSON extraction and the sums parsing are exported as pure
-- functions (no love.* calls) so plain-Lua tests can cover them, and so the -- functions (no love.* calls) so plain-Lua tests can cover them, and so the
+65 -62
View File
@@ -5,11 +5,10 @@
-- "update_check_cmd" in: { cmd = "check" | "download" | "quit" } -- "update_check_cmd" in: { cmd = "check" | "download" | "quit" }
-- "update_check_state" out: { status, latest, progress, error } -- "update_check_state" out: { status, latest, progress, error }
-- --
-- Transport is curl shelled out via io.popen (curl ships on macOS, Windows 10+ -- Transport is HostShell: curl via io.popen on desktop, the JNI
-- and desktop Linux). Everything is wrapped so a missing curl, an HTTP error, -- love.system.httpDownload bridge on Android (same path the mod catalog
-- or a hung download degrades to a "error"/"needs_full" state rather than -- already uses). A missing transport, an HTTP error, or a hung download
-- blocking or crashing the game. On Android curl is absent and the check -- degrades to "error"/"needs_full" rather than blocking or crashing the game.
-- soft-fails to "error", which the UI hides.
-- --
-- Fresh love threads do not carry the "src.*" package searcher, so sibling -- Fresh love threads do not carry the "src.*" package searcher, so sibling
-- modules are pulled in with love.filesystem.load exactly like -- modules are pulled in with love.filesystem.load exactly like
@@ -63,9 +62,12 @@ local API_URL = "https://api.github.com/repos/bryanthaboi/gen1recomp/releases/la
local pending = nil local pending = nil
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
-- shell / curl -- shell / fetch
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
local UA = "gen1recomp-updater"
local GH_ACCEPT = "application/vnd.github+json"
local function shq(s) local function shq(s)
s = tostring(s) s = tostring(s)
if isWindows then if isWindows then
@@ -74,31 +76,17 @@ local function shq(s)
return "'" .. s:gsub("'", "'\\''") .. "'" return "'" .. s:gsub("'", "'\\''") .. "'"
end end
-- run curl and return its response body (text), or nil on any failure. Used -- Small text resources (release JSON, sums file) through HostShell so Android
-- for the small text resources (release JSON, sums file); -f makes curl exit -- hits the JNI bridge instead of a curl binary that is never on the device.
-- non-zero and emit nothing on an HTTP error, so an empty read is a failure. local function fetchText(url, accept)
local function curlCapture(url) if not HostShell then return nil end
local cmd = "curl -fsSL --connect-timeout 10 --max-time 40 " local body = HostShell.httpGet(url, UA, accept)
.. "-H " .. shq("User-Agent: gen1recomp-updater") .. " " if type(body) ~= "string" or body == "" then return nil end
.. "-H " .. shq("Accept: application/vnd.github+json") .. " " return body
.. shq(url)
local pipe = HostShell.popen(cmd)
if not pipe then return nil end
local out = pipe:read("*a")
-- HostShell.pclose, not pipe:close(): a close outside the spawn lock can
-- free a FILE while another thread's popen walks the stream list, which
-- deadlocks that thread permanently (see HostShell's popen notes).
HostShell.pclose(pipe)
if not out or out == "" then return nil end
return out
end end
local function haveCurl() local function canFetch()
local pipe = HostShell.popen("curl --version") return HostShell and HostShell.canFetch()
if not pipe then return false end
local out = pipe:read("*a")
HostShell.pclose(pipe)
return out ~= nil and out:find("curl", 1, true) ~= nil
end end
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
@@ -170,12 +158,14 @@ end
local function doCheck() local function doCheck()
post({ status = "checking" }) post({ status = "checking" })
if not haveCurl() then if not canFetch() then
post({ status = "error", error = "curl not available" }) -- No curl and no JNI bridge: the chip becomes "Open releases" so a tap
-- still does something instead of retrying a check that cannot succeed.
post({ status = "needs_full" })
return return
end end
local body = curlCapture(API_URL) local body = fetchText(API_URL, GH_ACCEPT)
if not body then if not body then
post({ status = "error", error = "release check failed" }) post({ status = "error", error = "release check failed" })
return return
@@ -212,7 +202,7 @@ local function doCheck()
-- pulling the bytes again. -- pulling the bytes again.
local finalRel = "updates/" .. rel.payloadName local finalRel = "updates/" .. rel.payloadName
if love.filesystem.getInfo(finalRel) then if love.filesystem.getInfo(finalRel) then
local sums = curlCapture(rel.sums.url) local sums = fetchText(rel.sums.url)
if sums and verifyPayload(finalRel, rel.payloadName, sums) then if sums and verifyPayload(finalRel, rel.payloadName, sums) then
if gatePasses(finalRel) == false then if gatePasses(finalRel) == false then
love.filesystem.remove(finalRel) love.filesystem.remove(finalRel)
@@ -279,39 +269,52 @@ local function doDownload()
local doneAbs = saveDir .. "/updates/" .. rel.payloadName .. ".done" local doneAbs = saveDir .. "/updates/" .. rel.payloadName .. ".done"
local size = rel.payload.size or 0 local size = rel.payload.size or 0
launchDownload(rel.payload.url, partAbs, doneAbs) if HostShell and HostShell.haveCurl() then
launchDownload(rel.payload.url, partAbs, doneAbs)
-- poll the .part size for progress until curl drops the done-marker; a -- poll the .part size for progress until curl drops the done-marker; a
-- stalled or run-away transfer breaks out and lets verification fail cleanly -- stalled or run-away transfer breaks out and lets verification fail cleanly
local waited, lastSize, lastChange = 0, -1, 0 local waited, lastSize, lastChange = 0, -1, 0
while true do while true do
-- A queued quit means the window already closed. Bail so the join in -- A queued quit means the window already closed. Bail so the join in
-- Check.shutdown does not hold the dead window's process (and, on -- Check.shutdown does not hold the dead window's process (and, on
-- Windows, its folder) open for up to the whole transfer (#727). The -- Windows, its folder) open for up to the whole transfer (#727). The
-- quit stays on the channel for the command loop; the detached curl -- quit stays on the channel for the command loop; the detached curl
-- times out on its own and the next launch's doCheck verifies and -- times out on its own and the next launch's doCheck verifies and
-- re-offers whatever landed. -- re-offers whatever landed.
local peeked = cmdCh:peek() local peeked = cmdCh:peek()
if type(peeked) == "table" and peeked.cmd == "quit" then return end if type(peeked) == "table" and peeked.cmd == "quit" then return end
if love.filesystem.getInfo(doneRel) then break end if love.filesystem.getInfo(doneRel) then break end
local pinfo = love.filesystem.getInfo(partRel) local pinfo = love.filesystem.getInfo(partRel)
local cur = (pinfo and pinfo.size) or 0 local cur = (pinfo and pinfo.size) or 0
if size > 0 then if size > 0 then
local p = cur / size local p = cur / size
if p > 0.999 then p = 0.999 end -- 1.0 is reserved for "ready" if p > 0.999 then p = 0.999 end -- 1.0 is reserved for "ready"
post({ status = "downloading", latest = rel.version, progress = p }) post({ status = "downloading", latest = rel.version, progress = p })
else else
post({ status = "downloading", latest = rel.version }) post({ status = "downloading", latest = rel.version })
end
if cur ~= lastSize then lastSize, lastChange = cur, waited end
if waited - lastChange > 60 then break end -- 60s with no growth: give up
if waited > 960 then break end -- absolute ceiling
love.timer.sleep(0.25)
waited = waited + 0.25
end end
if cur ~= lastSize then lastSize, lastChange = cur, waited end love.filesystem.remove(doneRel)
if waited - lastChange > 60 then break end -- 60s with no growth: give up else
if waited > 960 then break end -- absolute ceiling -- Android JNI bridge: blocking write, same as fetch_worker. Progress
love.timer.sleep(0.25) -- cannot be sampled from inside httpDownload.
waited = waited + 0.25 local ok = HostShell and HostShell.httpDownload(
rel.payload.url, partAbs, UA, nil, 900)
if not ok then
love.filesystem.remove(partRel)
post({ status = "error", error = "download failed" })
return
end
post({ status = "downloading", latest = rel.version, progress = 0.999 })
end end
love.filesystem.remove(doneRel)
local sums = curlCapture(rel.sums and rel.sums.url or "") local sums = fetchText(rel.sums and rel.sums.url or "")
if not sums then if not sums then
love.filesystem.remove(partRel) love.filesystem.remove(partRel)
post({ status = "error", error = "checksum fetch failed" }) post({ status = "error", error = "checksum fetch failed" })
+20
View File
@@ -373,5 +373,25 @@ do
check(main ~= nil, "saveFilename resolves for gold") check(main ~= nil, "saveFilename resolves for gold")
end end
-- Gold's cache has no text_pointers / trainer_headers / field. Data:load
-- used to throw in seedDefaults (self.field.boot) after filling pokemon
-- with provenance scalars. That is the Android first-Edit CTD: the APK
-- cannot fall back to Red's source-tree copies the way a desktop checkout
-- can.
do
GameVersion.set("gold")
local Data = require("src.core.Data")
Data.constants = {}
Data.pokemon = { generation = 2, CYNDAQUIL = { dex = 155 } }
Data.maps = {}
Data.field = nil
Data.trainer_headers = nil
local ok, err = pcall(function() Data:seedDefaults() end)
check(ok, "gold seedDefaults survives a Gold-shaped cache: " .. tostring(err))
check(type(Data.field) == "table", "seedDefaults creates field when Gold omitted it")
eq(Data.constants.dexSize, 155, "dexSize ignores pokemon.generation scalar")
GameVersion.set("red")
end
print(string.format("save editor gen2 tests: %d passed, %d failed", passed, failed)) print(string.format("save editor gen2 tests: %d passed, %d failed", passed, failed))
if failed > 0 then os.exit(1) end if failed > 0 then os.exit(1) end
+1 -1
View File
@@ -146,7 +146,7 @@ function App.load(pathOverride, opts)
-- the same mod set the game loads, merged into Data before the catalogs -- the same mod set the game loads, merged into Data before the catalogs
-- build, so modded species/items/moves are editable and MonOps stops -- build, so modded species/items/moves are editable and MonOps stops
-- asserting on them -- asserting on them
if not mods then if not mods or App.dataVersion ~= opts.version then
-- One loader per editor session. A previous session leaves Data holding -- One loader per editor session. A previous session leaves Data holding
-- that session's merged registries (and possibly the other game's cache), -- that session's merged registries (and possibly the other game's cache),
-- and a second builtin registration over them collides -- "statuses -- and a second builtin registration over them collides -- "statuses
+10 -8
View File
@@ -40,10 +40,11 @@ end
local function shellListLua(dir) local function shellListLua(dir)
local out = {} local out = {}
if not (io and io.popen) then return out end
if package.config:sub(1, 1) == "\\" then if package.config:sub(1, 1) == "\\" then
-- cmd has no ls; dir /b prints bare names, so re-attach the directory -- cmd has no ls; dir /b prints bare names, so re-attach the directory
local p = io.popen(string.format('dir /b "%s\\*.lua" 2>nul', dir)) local ok, p = pcall(io.popen, string.format('dir /b "%s\\*.lua" 2>nul', dir))
if p then if ok and p then
for line in p:lines() do for line in p:lines() do
if line ~= "" then table.insert(out, dir .. "/" .. line) end if line ~= "" then table.insert(out, dir .. "/" .. line) end
end end
@@ -51,8 +52,8 @@ local function shellListLua(dir)
end end
return out return out
end end
local p = io.popen(string.format('ls "%s"/*.lua 2>/dev/null', dir)) local ok, p = pcall(io.popen, string.format('ls "%s"/*.lua 2>/dev/null', dir))
if p then if ok and p then
for line in p:lines() do for line in p:lines() do
table.insert(out, line) table.insert(out, line)
end end
@@ -64,8 +65,8 @@ end
local function readText(path) local function readText(path)
local fs = love and love.filesystem local fs = love and love.filesystem
if fs and fs.read and fs.getInfo and fs.getInfo(path) then if fs and fs.read and fs.getInfo and fs.getInfo(path) then
local body = fs.read(path) local ok, body = pcall(fs.read, path)
if body then return body end if ok and body then return body end
end end
local f = io.open(path, "r") local f = io.open(path, "r")
if not f then return nil end if not f then return nil end
@@ -78,7 +79,7 @@ end
-- scripts show up beside the vanilla EVENT_ ones -- scripts show up beside the vanilla EVENT_ ones
function Catalog.scrapeEvents(scriptDir, headerPath, listFiles, extraDirs) function Catalog.scrapeEvents(scriptDir, headerPath, listFiles, extraDirs)
listFiles = listFiles or function(dir) listFiles = listFiles or function(dir)
return loveListLua(dir) or shellListLua(dir) return loveListLua(dir) or shellListLua(dir) or {}
end end
local found = {} local found = {}
@@ -97,7 +98,8 @@ function Catalog.scrapeEvents(scriptDir, headerPath, listFiles, extraDirs)
dirs[#dirs + 1] = dir dirs[#dirs + 1] = dir
end end
for _, dir in ipairs(dirs) do for _, dir in ipairs(dirs) do
for _, path in ipairs(listFiles(dir)) do local files = listFiles(dir) or {}
for _, path in ipairs(files) do
local body = readText(path) local body = readText(path)
if body then eat(body) end if body then eat(body) end
end end
+27 -9
View File
@@ -78,15 +78,28 @@ function Gen.bindGoldData(data)
if data.palettes and data.gen2Palettes == nil then if data.palettes and data.gen2Palettes == nil then
data.gen2Palettes = data.palettes data.gen2Palettes = data.palettes
end end
if data.gen2Roofs == nil and data.roofs == nil then
local ok, roofs = pcall(require, "data.generated.roofs") local loadGen = function(rel)
if ok and type(roofs) == "table" then local CacheFs = require("src.import.CacheFs")
data.roofs = roofs local bytes = CacheFs.readActive("data/generated/" .. rel .. ".lua")
data.gen2Roofs = roofs if type(bytes) == "string" then
local chunk = loadstring(bytes, "@gold/data/generated/" .. rel .. ".lua")
if chunk then
local ok, res = pcall(chunk)
if ok and type(res) == "table" then return res end
end
end end
elseif data.roofs and data.gen2Roofs == nil then local ok, res = pcall(require, "data.generated." .. rel)
data.gen2Roofs = data.roofs if ok and type(res) == "table" then return res end
return nil
end end
data.gen2Palettes = data.gen2Palettes or loadGen("palettes")
data.gen2Icons = data.gen2Icons or loadGen("icons")
data.gen2Pokedex = data.gen2Pokedex or loadGen("pokedex")
data.gen2Landmarks = data.gen2Landmarks or loadGen("landmarks")
data.gen2Roofs = data.gen2Roofs or loadGen("roofs") or data.roofs
data.gen2Sprites = data.gen2Sprites or loadGen("sprites")
return data return data
end end
@@ -213,10 +226,15 @@ function Gen.playerMap(save)
if Gen.of(save) == 2 then if Gen.of(save) == 2 then
local p = save.position local p = save.position
if p and p.map then return p.map, p.x or 0, p.y or 0, p.facing end if p and p.map then return p.map, p.x or 0, p.y or 0, p.facing end
return save.spawn, 0, 0 if type(save.spawn) == "table" then
return save.spawn.map or "PLAYERS_HOUSE_2F", save.spawn.x or 0, save.spawn.y or 0, save.spawn.facing
elseif type(save.spawn) == "string" then
return save.spawn, 0, 0
end
return "PLAYERS_HOUSE_2F", 3, 3
end end
local p = save.player or {} local p = save.player or {}
return p.map, p.x or 0, p.y or 0 return p.map or "REDS_HOUSE_2F", p.x or 0, p.y or 0
end end
function Gen.setPlayerHere(save, mapId, x, y, facing) function Gen.setPlayerHere(save, mapId, x, y, facing)