diff --git a/main.lua b/main.lua index 8b70276f..23b2385a 100644 --- a/main.lua +++ b/main.lua @@ -148,9 +148,44 @@ local function openEditor(version, slotId) editorMode = true resizeForEditor() addEditorRequirePath() - EditorApp = require("App") - EditorApp.load(path, { version = version, slotId = slotId, embedded = true, - onClose = function() closeEditor() end }) + local okReq, appOrErr = pcall(require, "App") + if not okReq then + 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 -- Back to the launcher. Everything the editor mounted or cached has to come diff --git a/src/core/Data.lua b/src/core/Data.lua index 13c997b2..31d7932a 100644 --- a/src/core/Data.lua +++ b/src/core/Data.lua @@ -14,6 +14,14 @@ local MODULES = { -- Optional for compatibility with developer and stale caches. 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 -- 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 @@ -99,7 +107,11 @@ end -- Fills only what the cache is missing, so an importer that learns to -- stamp one of these keys silently takes over from the engine. 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 if constants[key] == nil then constants[key] = copy(value) end end @@ -108,7 +120,11 @@ function Data:seedDefaults() if constants.dexSize == nil then local highest = 0 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 constants.dexSize = highest end @@ -212,36 +228,41 @@ local function loadModule(dir, name) if not chunk then return false, err end return pcall(chunk) 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 GameVersion = require("src.core.GameVersion") local path = "data/generated/" .. name .. ".lua" local bytes = CacheFs.readActive(path) if type(bytes) == "string" then - local chunk, err = loadstring(bytes, "@" .. GameVersion.cachePrefix() .. path) - if not chunk then return false, err or mod end - return pcall(chunk) + local chunk = loadstring(bytes, "@" .. GameVersion.cachePrefix() .. path) + if chunk then + local ok, res = pcall(chunk) + if ok then return true, res 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 function Data:load() local dir = os.getenv("POKEPORT_DATA_DIR") + local gen2 = require("src.core.GameVersion").generation() == 2 for _, name in ipairs(MODULES) do local ok, mod = loadModule(dir, name) 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)") :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 - error(("missing generated data module 'data/generated/%s.lua'.\n" .. - "Import the ROM again or rebuild developer data.\n(%s)") - :format(name, mod)) + else + self[name] = mod end - self[name] = mod end for _, name in ipairs(OPTIONAL) do local ok, mod = loadModule(dir, name) diff --git a/tests/save_editor_gen2_tests.lua b/tests/save_editor_gen2_tests.lua index 3114ae98..365ec70e 100644 --- a/tests/save_editor_gen2_tests.lua +++ b/tests/save_editor_gen2_tests.lua @@ -373,5 +373,25 @@ do check(main ~= nil, "saveFilename resolves for gold") 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)) if failed > 0 then os.exit(1) end diff --git a/tools/save-editor/Gen.lua b/tools/save-editor/Gen.lua index 9050fbd0..695aa9b8 100644 --- a/tools/save-editor/Gen.lua +++ b/tools/save-editor/Gen.lua @@ -78,15 +78,28 @@ function Gen.bindGoldData(data) if data.palettes and data.gen2Palettes == nil then data.gen2Palettes = data.palettes end - if data.gen2Roofs == nil and data.roofs == nil then - local ok, roofs = pcall(require, "data.generated.roofs") - if ok and type(roofs) == "table" then - data.roofs = roofs - data.gen2Roofs = roofs + + local loadGen = function(rel) + local CacheFs = require("src.import.CacheFs") + local bytes = CacheFs.readActive("data/generated/" .. rel .. ".lua") + 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 - elseif data.roofs and data.gen2Roofs == nil then - data.gen2Roofs = data.roofs + local ok, res = pcall(require, "data.generated." .. rel) + if ok and type(res) == "table" then return res end + return nil 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 end @@ -213,10 +226,15 @@ function Gen.playerMap(save) if Gen.of(save) == 2 then local p = save.position 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 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 function Gen.setPlayerHere(save, mapId, x, y, facing)