mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 15:01:11 +02:00
Centralize ROM cache publication contract
This commit is contained in:
@@ -0,0 +1,196 @@
|
|||||||
|
-- Engine-owned contract for generated ROM caches.
|
||||||
|
--
|
||||||
|
-- A cache is playable only when its versioned marker matches the ROM and every
|
||||||
|
-- required output for that version exists. Extraction writers may differ by
|
||||||
|
-- platform, but they must publish through this contract so partial staging
|
||||||
|
-- cannot look ready to the runtime.
|
||||||
|
local GameVersion = require("src.core.GameVersion")
|
||||||
|
|
||||||
|
local CacheContract = {}
|
||||||
|
|
||||||
|
CacheContract.FORMAT = "rom-cache-v10:"
|
||||||
|
CacheContract.MARKER_PATH = "rom-cache.complete"
|
||||||
|
|
||||||
|
CacheContract.REQUIRED_FILES = {
|
||||||
|
"data/generated/constants.lua",
|
||||||
|
"data/generated/maps.lua",
|
||||||
|
"data/generated/text.lua",
|
||||||
|
"data/generated/field.lua",
|
||||||
|
"data/generated/battle_anims.lua",
|
||||||
|
"assets/generated/title/pokemon_logo.png",
|
||||||
|
"assets/generated/fonts/font.png",
|
||||||
|
"assets/generated/battle/front/pikachu.png",
|
||||||
|
"assets/generated/battle/anims/move_anim_0.png",
|
||||||
|
"assets/generated/battle/anims/move_anim_1.png",
|
||||||
|
"assets/generated/audio/programs.bin",
|
||||||
|
"assets/generated/trade/game_boy.png",
|
||||||
|
}
|
||||||
|
|
||||||
|
CacheContract.VERSION_REQUIRED_FILES = {
|
||||||
|
yellow = {
|
||||||
|
"assets/generated/battle/trainers/jessie_james.png",
|
||||||
|
"assets/generated/battle/profoakb.png",
|
||||||
|
"assets/generated/pikachu/pikapic_1.png",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
CacheContract.VERSION_REQUIRED_FILES_OVERRIDE = {
|
||||||
|
gold = {
|
||||||
|
"data/generated/constants.lua",
|
||||||
|
"data/generated/maps.lua",
|
||||||
|
"data/generated/roofs.lua",
|
||||||
|
"data/generated/sprites.lua",
|
||||||
|
"data/generated/scripts.lua",
|
||||||
|
"data/generated/text.lua",
|
||||||
|
-- The engine's label-keyed strings are separate from Gen 2 script text.
|
||||||
|
-- Caches made before RomExtractorGen2:extractText must be rebuilt so
|
||||||
|
-- src/core/RomText.lua does not silently fall back to built-in wording.
|
||||||
|
"data/generated/rom_text.lua",
|
||||||
|
"data/generated/pokemon.lua",
|
||||||
|
"data/generated/tilesets.lua",
|
||||||
|
"data/generated/audio.lua",
|
||||||
|
"data/generated/marts.lua",
|
||||||
|
"assets/generated/fonts/font.png",
|
||||||
|
"assets/generated/fonts/frames.png",
|
||||||
|
"assets/generated/title/pokemon_logo.png",
|
||||||
|
"assets/generated/title/title_screen.png",
|
||||||
|
"assets/generated/title/hooh.png",
|
||||||
|
"assets/generated/title/hooh_5.png",
|
||||||
|
"assets/generated/title/clouds.png",
|
||||||
|
"assets/generated/title/copyright_splash.png",
|
||||||
|
"data/generated/oak_speech.lua",
|
||||||
|
"assets/generated/intro/oak.png",
|
||||||
|
"assets/generated/intro/cal.png",
|
||||||
|
"assets/generated/tilesets/johto.png",
|
||||||
|
"assets/generated/tilesets/roofs/new_bark.png",
|
||||||
|
"assets/generated/sprites/chris.png",
|
||||||
|
"assets/generated/battle/front/chikorita.png",
|
||||||
|
"assets/generated/battle/front/pikachu.png",
|
||||||
|
"assets/generated/battle/front/marill.png",
|
||||||
|
"assets/generated/battle/trainers/falkner.png",
|
||||||
|
"assets/generated/battle/hud/balls.png",
|
||||||
|
"assets/generated/audio/programs.bin",
|
||||||
|
"assets/generated/slots/gold_slots_1.png",
|
||||||
|
"assets/generated/card_flip/card_flip_1.png",
|
||||||
|
"assets/generated/pc/mail_item.png",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
CacheContract.VERSION_REQUIRED_FILES_OVERRIDE.silver =
|
||||||
|
CacheContract.VERSION_REQUIRED_FILES_OVERRIDE.gold
|
||||||
|
|
||||||
|
function CacheContract.requiredFilesFor(version)
|
||||||
|
local override = CacheContract.VERSION_REQUIRED_FILES_OVERRIDE[version]
|
||||||
|
if override then return override, true end
|
||||||
|
return CacheContract.REQUIRED_FILES, false
|
||||||
|
end
|
||||||
|
|
||||||
|
function CacheContract.markerFor(version)
|
||||||
|
return CacheContract.FORMAT .. GameVersion.info(version).sha1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Keep the process-global CacheFs prefix isolated even when a filesystem
|
||||||
|
-- adapter raises while probing or publishing. The real CacheFs methods
|
||||||
|
-- return errors, but this also makes the contract safe for platform adapters
|
||||||
|
-- that surface I/O failures as Lua errors.
|
||||||
|
local function withVersionPrefix(version, fs, action)
|
||||||
|
local saved = fs.prefix
|
||||||
|
fs.prefix = GameVersion.cachePrefix(version)
|
||||||
|
local ok, first, second = pcall(action)
|
||||||
|
fs.prefix = saved
|
||||||
|
if not ok then return false, first end
|
||||||
|
return true, first, second
|
||||||
|
end
|
||||||
|
|
||||||
|
function CacheContract.allRequiredFilesExist(version, fs)
|
||||||
|
fs = fs or require("src.import.CacheFs")
|
||||||
|
local ok, complete, missing = withVersionPrefix(version, fs, function()
|
||||||
|
local required, isOverride = CacheContract.requiredFilesFor(version)
|
||||||
|
local missingPath
|
||||||
|
for _, path in ipairs(required) do
|
||||||
|
if not fs.exists(path) then missingPath = path; break end
|
||||||
|
end
|
||||||
|
if not missingPath and not isOverride then
|
||||||
|
for _, path in ipairs(CacheContract.VERSION_REQUIRED_FILES[version] or {}) do
|
||||||
|
if not fs.exists(path) then missingPath = path; break end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return missingPath == nil, missingPath
|
||||||
|
end)
|
||||||
|
if not ok then return false, complete end
|
||||||
|
return complete, missing
|
||||||
|
end
|
||||||
|
|
||||||
|
function CacheContract.readMarker(version, fs)
|
||||||
|
fs = fs or require("src.import.CacheFs")
|
||||||
|
local ok, marker, readError = withVersionPrefix(version, fs, function()
|
||||||
|
return fs.read(CacheContract.MARKER_PATH)
|
||||||
|
end)
|
||||||
|
if not ok then return nil, marker end
|
||||||
|
-- LÖVE may return contents plus a byte count; only a nil contents result
|
||||||
|
-- makes the auxiliary value an error. CacheFs' portable reader returns
|
||||||
|
-- just the contents, so this remains adapter-neutral.
|
||||||
|
if marker == nil then return nil, readError end
|
||||||
|
return marker
|
||||||
|
end
|
||||||
|
|
||||||
|
function CacheContract.isReady(version, fs)
|
||||||
|
fs = fs or require("src.import.CacheFs")
|
||||||
|
if CacheContract.sourceTreeHasData(version) then return true end
|
||||||
|
local marker, readError = CacheContract.readMarker(version, fs)
|
||||||
|
if readError or marker ~= CacheContract.markerFor(version) then return false end
|
||||||
|
return CacheContract.allRequiredFilesExist(version, fs)
|
||||||
|
end
|
||||||
|
|
||||||
|
function CacheContract.publish(version, fs)
|
||||||
|
fs = fs or require("src.import.CacheFs")
|
||||||
|
local complete, missing = CacheContract.allRequiredFilesExist(version, fs)
|
||||||
|
if not complete then
|
||||||
|
-- A caller may be retrying over a partially replaced cache. Do not
|
||||||
|
-- leave its old marker advertising readiness after this failed check.
|
||||||
|
local removed, removeError = withVersionPrefix(version, fs, function()
|
||||||
|
if not fs.remove then
|
||||||
|
error("cache filesystem cannot remove the completion marker")
|
||||||
|
end
|
||||||
|
return fs.remove(CacheContract.MARKER_PATH)
|
||||||
|
end)
|
||||||
|
if not removed then
|
||||||
|
return false, "cache is incomplete; missing " .. tostring(missing)
|
||||||
|
.. "; could not remove completion marker: " .. tostring(removeError)
|
||||||
|
end
|
||||||
|
return false, "cache is incomplete; missing " .. tostring(missing)
|
||||||
|
end
|
||||||
|
local changed, ok, err = withVersionPrefix(version, fs, function()
|
||||||
|
return fs.write(CacheContract.MARKER_PATH, CacheContract.markerFor(version))
|
||||||
|
end)
|
||||||
|
if not changed then return false, tostring(ok) end
|
||||||
|
return ok, err
|
||||||
|
end
|
||||||
|
|
||||||
|
function CacheContract.sourceTreeHasData(version)
|
||||||
|
if not (love and love.filesystem and love.filesystem.getInfo
|
||||||
|
and love.filesystem.getRealDirectory and love.filesystem.getSource) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local prefix = version == "red" and "" or GameVersion.cachePrefix(version)
|
||||||
|
local required, isOverride = CacheContract.requiredFilesFor(version)
|
||||||
|
local source = love.filesystem.getSource()
|
||||||
|
for _, path in ipairs(required) do
|
||||||
|
local fullPath = prefix .. path
|
||||||
|
if love.filesystem.getInfo(fullPath, "file") == nil
|
||||||
|
or love.filesystem.getRealDirectory(fullPath) ~= source then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not isOverride then
|
||||||
|
for _, path in ipairs(CacheContract.VERSION_REQUIRED_FILES[version] or {}) do
|
||||||
|
local fullPath = prefix .. path
|
||||||
|
if love.filesystem.getInfo(fullPath, "file") == nil
|
||||||
|
or love.filesystem.getRealDirectory(fullPath) ~= source then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return CacheContract
|
||||||
+14
-195
@@ -28,137 +28,12 @@ local function pickFile(...)
|
|||||||
return fn(...) and true or false
|
return fn(...) and true or false
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Cache generation tag; bump to force every imported version to re-extract.
|
local CacheContract = require("src.import.CacheContract")
|
||||||
-- v9: Yellow audio re-anchored on pokeyellow.sym (#522) -- stale caches
|
|
||||||
-- carry Red's bank $1f header, wave-table, and CryData offsets.
|
|
||||||
-- v10: maps carry their raw map-header/connection/object bytes and tilesets
|
|
||||||
-- their Tilesets row (#889), which a .sav export replays so a Continue on
|
|
||||||
-- real hardware has a map to load; a v9 cache has none of them and exports
|
|
||||||
-- the same unbootable save as before.
|
|
||||||
-- Deliberately NOT bumped for the Gold trainer-pic gap: this tag invalidates
|
|
||||||
-- every version at once, and that gap is Gold-only. A per-version marker in
|
|
||||||
-- VERSION_REQUIRED_FILES_OVERRIDE.gold re-imports exactly the caches that lack
|
|
||||||
-- the stage, which is what the Yellow markers below already do for #439/#557.
|
|
||||||
-- Reach for a bump when the change spans versions or has no single file to
|
|
||||||
-- point at.
|
|
||||||
local CACHE_FORMAT = "rom-cache-v10:"
|
|
||||||
-- The completion marker is written under each version's cache prefix
|
|
||||||
-- (red/rom-cache.complete, blue/rom-cache.complete, ...).
|
|
||||||
local MARKER_PATH = "rom-cache.complete"
|
|
||||||
|
|
||||||
-- The marker a finished import writes for a version: the generation tag plus
|
|
||||||
-- that version's ROM hash, so both a format bump and a swapped ROM invalidate.
|
|
||||||
local function markerFor(version)
|
|
||||||
return CACHE_FORMAT .. GameVersion.info(version).sha1
|
|
||||||
end
|
|
||||||
local COMMUNITY_URL = "https://bois.icu"
|
local COMMUNITY_URL = "https://bois.icu"
|
||||||
local TRUST_WARNING = "if you did not get this from bryanthaboi's github " ..
|
local TRUST_WARNING = "if you did not get this from bryanthaboi's github " ..
|
||||||
"or a link from the discord that bryanthaboi himself posted, just know " ..
|
"or a link from the discord that bryanthaboi himself posted, just know " ..
|
||||||
"it might have been tampered with. go to the discord to verify " ..
|
"it might have been tampered with. go to the discord to verify " ..
|
||||||
COMMUNITY_URL .. " (or click the logo above)"
|
COMMUNITY_URL .. " (or click the logo above)"
|
||||||
local REQUIRED_FILES = {
|
|
||||||
"data/generated/constants.lua",
|
|
||||||
"data/generated/maps.lua",
|
|
||||||
"data/generated/text.lua",
|
|
||||||
"data/generated/field.lua",
|
|
||||||
"data/generated/battle_anims.lua",
|
|
||||||
"assets/generated/title/pokemon_logo.png",
|
|
||||||
"assets/generated/fonts/font.png",
|
|
||||||
"assets/generated/battle/front/pikachu.png",
|
|
||||||
"assets/generated/battle/anims/move_anim_0.png",
|
|
||||||
"assets/generated/battle/anims/move_anim_1.png",
|
|
||||||
"assets/generated/audio/programs.bin",
|
|
||||||
-- The trade cinematic's Game Boy / cable art. Caches built before #750
|
|
||||||
-- carry none of it and fall back to plain rectangles, so listing one of
|
|
||||||
-- the files re-imports them without a CACHE_FORMAT bump.
|
|
||||||
"assets/generated/trade/game_boy.png",
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Files only one version's cache carries. A version that predates one of
|
|
||||||
-- them re-imports on its own, without dragging the other versions through a
|
|
||||||
-- CACHE_FORMAT bump.
|
|
||||||
local VERSION_REQUIRED_FILES = {
|
|
||||||
yellow = {
|
|
||||||
"assets/generated/battle/trainers/jessie_james.png", -- #439
|
|
||||||
-- Oak's own back pic and the pikapic base frames only exist in caches
|
|
||||||
-- built after their manifest symbols landed, so an older Yellow cache
|
|
||||||
-- has to re-import to stop falling back to the old man's back pic and
|
|
||||||
-- to the battle front pic (#557, #561). Both are gated on manifest
|
|
||||||
-- symbols in RomExtractor, so these markers must only ever list files
|
|
||||||
-- tools/rom_manifest_yellow.json can actually produce -- otherwise the
|
|
||||||
-- cache reads as incomplete and re-importing cannot clear it.
|
|
||||||
"assets/generated/battle/profoakb.png",
|
|
||||||
"assets/generated/pikachu/pikapic_1.png",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Gold Phase 1 writes a thinner cache than Gen 1 (no battle anim sheets,
|
|
||||||
-- trade art, or field.lua payload yet -- see docs/gold-phase1.md). This
|
|
||||||
-- list replaces REQUIRED_FILES entirely for that version so a successful
|
|
||||||
-- Gen 2 extract is not stuck as "incomplete" waiting on Gen 1 markers.
|
|
||||||
local VERSION_REQUIRED_FILES_OVERRIDE = {
|
|
||||||
gold = {
|
|
||||||
"data/generated/constants.lua",
|
|
||||||
"data/generated/maps.lua",
|
|
||||||
"data/generated/roofs.lua", -- Phase 2: forces re-import of Phase 1 caches
|
|
||||||
"data/generated/sprites.lua", -- OW sheets (Chris + NPCs)
|
|
||||||
"data/generated/scripts.lua", -- disassembled map scripts
|
|
||||||
"data/generated/text.lua", -- decoded Gen 2 dialogue strings
|
|
||||||
-- The engine's own strings, keyed by label rather than by address. A
|
|
||||||
-- cache built before RomExtractorGen2:extractText has none, and every
|
|
||||||
-- line that reads through src/core/RomText.lua would silently keep
|
|
||||||
-- printing its Lua fallback, so this re-imports those caches rather than
|
|
||||||
-- bumping CACHE_FORMAT and dragging Red, Blue and Yellow through it too.
|
|
||||||
"data/generated/rom_text.lua",
|
|
||||||
"data/generated/pokemon.lua",
|
|
||||||
"data/generated/tilesets.lua",
|
|
||||||
"data/generated/audio.lua",
|
|
||||||
-- Mart shelves + the heal machine art ride the same import, so listing
|
|
||||||
-- marts.lua alone re-imports the caches from before either existed
|
|
||||||
-- (empty shop shelves, no Pokecenter light show).
|
|
||||||
"data/generated/marts.lua",
|
|
||||||
"assets/generated/fonts/font.png",
|
|
||||||
"assets/generated/fonts/frames.png", -- the seven other OPTION textbox frames
|
|
||||||
"assets/generated/title/pokemon_logo.png",
|
|
||||||
"assets/generated/title/title_screen.png", -- TitleScreenTilemap composition
|
|
||||||
"assets/generated/title/hooh.png",
|
|
||||||
"assets/generated/title/hooh_5.png", -- wing-flap frames force re-import
|
|
||||||
"assets/generated/title/clouds.png",
|
|
||||||
"assets/generated/title/copyright_splash.png",
|
|
||||||
"data/generated/oak_speech.lua", -- Oak texts + trainer pics
|
|
||||||
"assets/generated/intro/oak.png",
|
|
||||||
"assets/generated/intro/cal.png",
|
|
||||||
"assets/generated/tilesets/johto.png",
|
|
||||||
"assets/generated/tilesets/roofs/new_bark.png",
|
|
||||||
"assets/generated/sprites/chris.png",
|
|
||||||
"assets/generated/battle/front/chikorita.png",
|
|
||||||
"assets/generated/battle/front/pikachu.png",
|
|
||||||
"assets/generated/battle/front/marill.png", -- Oak speech demo mon
|
|
||||||
-- The trainer class pics (TrainerPicPointers). FALKNER is row 0 of that
|
|
||||||
-- table, so a cache that produced any class pic at all produced this one.
|
|
||||||
-- Listed for the reason the Yellow markers above are: a cache built before
|
|
||||||
-- the stage existed reads as INCOMPLETE and re-imports itself, so this
|
|
||||||
-- particular gap cannot survive a tag bump being forgotten again. It
|
|
||||||
-- costs nothing on a current cache and is the difference between every
|
|
||||||
-- trainer battle opening with a picture and opening with none.
|
|
||||||
"assets/generated/battle/trainers/falkner.png",
|
|
||||||
-- BattleStart_TrainerHuds cannot draw its party rows from a cache made
|
|
||||||
-- before the four ball tiles were extracted (#1502).
|
|
||||||
"assets/generated/battle/hud/balls.png",
|
|
||||||
"assets/generated/audio/programs.bin",
|
|
||||||
-- Goldenrod Game Corner reel + board art (#1581). menu_gfx.lua used to
|
|
||||||
-- advertise these paths even when Slots*LZ / CardFlip* were absent from
|
|
||||||
-- the manifest, so a cache that never wrote the PNGs still looked
|
|
||||||
-- complete and SlotMachine crashed on its labelled-cell fallback.
|
|
||||||
"assets/generated/slots/gold_slots_1.png",
|
|
||||||
"assets/generated/card_flip/card_flip_1.png",
|
|
||||||
-- PCMailGFX (engine/pokemon/bills_pc.asm:2170-2173)
|
|
||||||
"assets/generated/pc/mail_item.png",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
-- Same Gen 2 extract, so a Silver cache is complete when the same files exist.
|
|
||||||
VERSION_REQUIRED_FILES_OVERRIDE.silver = VERSION_REQUIRED_FILES_OVERRIDE.gold
|
|
||||||
|
|
||||||
-- "Split-screen ROM selector" first-run palette (matches the FirstRun mockup):
|
-- "Split-screen ROM selector" first-run palette (matches the FirstRun mockup):
|
||||||
-- a dark neon arcade panel, one column per game.
|
-- a dark neon arcade panel, one column per game.
|
||||||
-- Red, Blue, and Yellow share the same importer flow once listed in
|
-- Red, Blue, and Yellow share the same importer flow once listed in
|
||||||
@@ -211,58 +86,6 @@ local PAL = {
|
|||||||
chipInkGold = { 58, 44, 0 }, -- #3a2c00 dark "Y" on the gold chip
|
chipInkGold = { 58, 44, 0 }, -- #3a2c00 dark "Y" on the gold chip
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Per-version required cache files. Gold replaces the Gen 1 list entirely
|
|
||||||
-- (VERSION_REQUIRED_FILES_OVERRIDE); Yellow adds a few extra markers.
|
|
||||||
local function requiredFilesFor(version)
|
|
||||||
local override = VERSION_REQUIRED_FILES_OVERRIDE[version]
|
|
||||||
if override then return override, true end
|
|
||||||
return REQUIRED_FILES, false
|
|
||||||
end
|
|
||||||
|
|
||||||
-- CacheFs.exists checks the game folder directly for a portable install,
|
|
||||||
-- otherwise the save directory through love.filesystem. It honors
|
|
||||||
-- CacheFs.prefix, so we point it at the version's cache subtree (red/,
|
|
||||||
-- blue/, yellow/, gold/).
|
|
||||||
local function allRequiredFilesExist(version)
|
|
||||||
local CacheFs = require("src.import.CacheFs")
|
|
||||||
local saved = CacheFs.prefix
|
|
||||||
CacheFs.prefix = GameVersion.cachePrefix(version)
|
|
||||||
local ok = true
|
|
||||||
local required, isOverride = requiredFilesFor(version)
|
|
||||||
for _, path in ipairs(required) do
|
|
||||||
if not CacheFs.exists(path) then ok = false; break end
|
|
||||||
end
|
|
||||||
if ok and not isOverride then
|
|
||||||
for _, path in ipairs(VERSION_REQUIRED_FILES[version] or {}) do
|
|
||||||
if not CacheFs.exists(path) then ok = false; break end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
CacheFs.prefix = saved
|
|
||||||
return ok
|
|
||||||
end
|
|
||||||
|
|
||||||
-- A developer checkout / Python build leaves generated data in the physfs
|
|
||||||
-- source: Red at the historical root, Blue/Yellow/Gold in their versioned
|
|
||||||
-- trees. Imported Red caches still live under red/. Check source paths
|
|
||||||
-- directly so that cache prefix cannot hide Red's source tree, and keep
|
|
||||||
-- save-dir caches from counting as current source data.
|
|
||||||
local function sourceTreeHasData(version)
|
|
||||||
if not love.filesystem.getRealDirectory then return false end
|
|
||||||
local prefix = version == "red" and "" or GameVersion.cachePrefix(version)
|
|
||||||
local required, isOverride = requiredFilesFor(version)
|
|
||||||
for _, path in ipairs(required) do
|
|
||||||
if love.filesystem.getInfo(prefix .. path, "file") == nil then return false end
|
|
||||||
end
|
|
||||||
if not isOverride then
|
|
||||||
for _, path in ipairs(VERSION_REQUIRED_FILES[version] or {}) do
|
|
||||||
if love.filesystem.getInfo(prefix .. path, "file") == nil then return false end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local path = prefix .. required[1]
|
|
||||||
local real = love.filesystem.getRealDirectory(path)
|
|
||||||
return real == love.filesystem.getSource()
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ------- ROM cache location
|
-- ------- ROM cache location
|
||||||
--
|
--
|
||||||
-- The extracted cache (data/generated, assets/generated) plus the
|
-- The extracted cache (data/generated, assets/generated) plus the
|
||||||
@@ -318,10 +141,15 @@ local function purgeSaveDirCache()
|
|||||||
-- / yellow/ / gold/ prefix) so it cannot shadow the portable game-folder cache.
|
-- / yellow/ / gold/ prefix) so it cannot shadow the portable game-folder cache.
|
||||||
for _, version in ipairs(GameVersion.ORDER) do
|
for _, version in ipairs(GameVersion.ORDER) do
|
||||||
local prefix = GameVersion.cachePrefix(version)
|
local prefix = GameVersion.cachePrefix(version)
|
||||||
if saveDirHas(prefix .. MARKER_PATH) or saveDirHas(prefix .. REQUIRED_FILES[1]) then
|
local required = CacheContract.requiredFilesFor(version)
|
||||||
|
local hasRequired = false
|
||||||
|
for _, path in ipairs(required) do
|
||||||
|
if saveDirHas(prefix .. path) then hasRequired = true; break end
|
||||||
|
end
|
||||||
|
if saveDirHas(prefix .. CacheContract.MARKER_PATH) or hasRequired then
|
||||||
removeTree(prefix .. "data/generated")
|
removeTree(prefix .. "data/generated")
|
||||||
removeTree(prefix .. "assets/generated")
|
removeTree(prefix .. "assets/generated")
|
||||||
love.filesystem.remove(prefix .. MARKER_PATH)
|
love.filesystem.remove(prefix .. CacheContract.MARKER_PATH)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -336,13 +164,7 @@ function RomImporter.isReady(version)
|
|||||||
-- save-directory copy that would otherwise shadow it at runtime.
|
-- save-directory copy that would otherwise shadow it at runtime.
|
||||||
purgeSaveDirCache()
|
purgeSaveDirCache()
|
||||||
end
|
end
|
||||||
-- Generated data in a developer checkout / Python build is always current.
|
return CacheContract.isReady(version, CacheFs)
|
||||||
if sourceTreeHasData(version) then return true end
|
|
||||||
local saved = CacheFs.prefix
|
|
||||||
CacheFs.prefix = GameVersion.cachePrefix(version)
|
|
||||||
local marker = CacheFs.read(MARKER_PATH)
|
|
||||||
CacheFs.prefix = saved
|
|
||||||
return marker == markerFor(version) and allRequiredFilesExist(version)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function RomImporter.syncAndroidShortcuts(activeVersion)
|
function RomImporter.syncAndroidShortcuts(activeVersion)
|
||||||
@@ -1574,12 +1396,9 @@ function RomImporter.new(onComplete, opts)
|
|||||||
self.ready[version] = ready
|
self.ready[version] = ready
|
||||||
-- a marker present but for an older cache generation / different ROM means
|
-- a marker present but for an older cache generation / different ROM means
|
||||||
-- "update required" (re-import) rather than a clean first-run choose
|
-- "update required" (re-import) rather than a clean first-run choose
|
||||||
local saved = CacheFs.prefix
|
local marker = CacheContract.readMarker(version, CacheFs)
|
||||||
CacheFs.prefix = info.cachePrefix
|
|
||||||
local marker = CacheFs.read(MARKER_PATH)
|
|
||||||
CacheFs.prefix = saved
|
|
||||||
self.returning[version] =
|
self.returning[version] =
|
||||||
(not ready) and marker ~= nil and marker ~= markerFor(version)
|
(not ready) and marker ~= nil and marker ~= CacheContract.markerFor(version)
|
||||||
self.romName[version] = "pokemon_" .. info.id
|
self.romName[version] = "pokemon_" .. info.id
|
||||||
.. ((info.id == "yellow" or GameVersion.generation(version) == 2)
|
.. ((info.id == "yellow" or GameVersion.generation(version) == 2)
|
||||||
and ".gbc" or ".gb")
|
and ".gbc" or ".gb")
|
||||||
@@ -1889,10 +1708,10 @@ function RomImporter:startData(data, displayName)
|
|||||||
local cleared, clearError = pcall(function()
|
local cleared, clearError = pcall(function()
|
||||||
removeTree(prefix .. "data/generated")
|
removeTree(prefix .. "data/generated")
|
||||||
removeTree(prefix .. "assets/generated")
|
removeTree(prefix .. "assets/generated")
|
||||||
love.filesystem.remove(prefix .. MARKER_PATH)
|
love.filesystem.remove(prefix .. CacheContract.MARKER_PATH)
|
||||||
CacheFs.removeTree("data/generated")
|
CacheFs.removeTree("data/generated")
|
||||||
CacheFs.removeTree("assets/generated")
|
CacheFs.removeTree("assets/generated")
|
||||||
CacheFs.remove(MARKER_PATH)
|
CacheFs.remove(CacheContract.MARKER_PATH)
|
||||||
end)
|
end)
|
||||||
CacheFs.prefix = savedPrefix
|
CacheFs.prefix = savedPrefix
|
||||||
if not cleared then
|
if not cleared then
|
||||||
@@ -1964,7 +1783,7 @@ function RomImporter:_completeImport(version, prefix, displayName)
|
|||||||
-- appear once every required file is in place.
|
-- appear once every required file is in place.
|
||||||
local savedPrefix = CacheFs.prefix
|
local savedPrefix = CacheFs.prefix
|
||||||
CacheFs.prefix = prefix
|
CacheFs.prefix = prefix
|
||||||
local ok, writeError = CacheFs.write(MARKER_PATH, markerFor(version))
|
local ok, writeError = CacheContract.publish(version, CacheFs)
|
||||||
CacheFs.prefix = savedPrefix
|
CacheFs.prefix = savedPrefix
|
||||||
if not ok then
|
if not ok then
|
||||||
error("could not finish the private cache: " .. tostring(writeError))
|
error("could not finish the private cache: " .. tostring(writeError))
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
-- The cache contract is the shared Lua-side publication boundary. A writer
|
||||||
|
-- may stage outputs in any order, but readiness is published only after the
|
||||||
|
-- version-specific required set exists.
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
|
||||||
|
local T = require("tests.harness")
|
||||||
|
local check = T.check
|
||||||
|
local eq = T.eq
|
||||||
|
local CacheContract = require("src.import.CacheContract")
|
||||||
|
|
||||||
|
local fs = { prefix = "initial/", files = {} }
|
||||||
|
local writes = {}
|
||||||
|
function fs.exists(path)
|
||||||
|
return fs.files[fs.prefix .. path] ~= nil
|
||||||
|
end
|
||||||
|
function fs.read(path)
|
||||||
|
return fs.files[fs.prefix .. path]
|
||||||
|
end
|
||||||
|
function fs.write(path, value)
|
||||||
|
writes[#writes + 1] = fs.prefix .. path
|
||||||
|
fs.files[fs.prefix .. path] = value
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
function fs.remove(path)
|
||||||
|
fs.files[fs.prefix .. path] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local required, isOverride = CacheContract.requiredFilesFor("red")
|
||||||
|
check(not isOverride, "Red uses the shared required-file list")
|
||||||
|
check(#required > 0, "Red has required outputs")
|
||||||
|
eq(CacheContract.markerFor("red"),
|
||||||
|
CacheContract.FORMAT .. "ea9bcae617fdf159b045185467ae58b2e4a48b9a",
|
||||||
|
"marker contains format and Red SHA-1")
|
||||||
|
|
||||||
|
for index = 1, #required - 1 do
|
||||||
|
fs.files["red/" .. required[index]] = true
|
||||||
|
end
|
||||||
|
local complete, missing = CacheContract.allRequiredFilesExist("red", fs)
|
||||||
|
check(not complete, "missing output keeps cache incomplete")
|
||||||
|
eq(missing, required[#required], "missing output is reported")
|
||||||
|
local published, publishError = CacheContract.publish("red", fs)
|
||||||
|
check(not published, "incomplete cache is not published")
|
||||||
|
check(publishError ~= nil, "incomplete publication explains the missing output")
|
||||||
|
check(fs.files["red/" .. CacheContract.MARKER_PATH] == nil,
|
||||||
|
"incomplete cache has no completion marker")
|
||||||
|
|
||||||
|
-- Publication must remove a stale marker left by an interrupted replacement,
|
||||||
|
-- and must restore the caller prefix on both the success and failure paths.
|
||||||
|
fs.files["red/" .. CacheContract.MARKER_PATH] = "old-marker"
|
||||||
|
local removedMarker = CacheContract.publish("red", fs)
|
||||||
|
check(not removedMarker, "incomplete retry is still rejected")
|
||||||
|
check(fs.files["red/" .. CacheContract.MARKER_PATH] == nil,
|
||||||
|
"incomplete retry removes a stale completion marker")
|
||||||
|
eq(fs.prefix, "initial/", "incomplete publication restores the caller prefix")
|
||||||
|
|
||||||
|
fs.files["red/" .. required[#required]] = true
|
||||||
|
fs.prefix = "caller/prefix/"
|
||||||
|
fs.files["red/" .. required[#required]] = true
|
||||||
|
for _, path in ipairs(required) do fs.files["red/" .. path] = true end
|
||||||
|
published, publishError = CacheContract.publish("red", fs)
|
||||||
|
check(published, "complete cache is published")
|
||||||
|
eq(publishError, nil, "complete publication has no error")
|
||||||
|
eq(fs.prefix, "caller/prefix/", "publication restores the caller prefix")
|
||||||
|
eq(fs.files["red/" .. CacheContract.MARKER_PATH], CacheContract.markerFor("red"),
|
||||||
|
"marker is written under the version prefix")
|
||||||
|
local marker = CacheContract.readMarker("red", fs)
|
||||||
|
eq(marker, CacheContract.markerFor("red"), "marker reads through the version prefix")
|
||||||
|
eq(writes[#writes], "red/" .. CacheContract.MARKER_PATH,
|
||||||
|
"the marker is the only publication write and comes last")
|
||||||
|
|
||||||
|
-- Every supported version gets its own marker and complete cache semantics;
|
||||||
|
-- Yellow adds its three outputs, while Gold/Silver replace the Gen 1 set.
|
||||||
|
for _, version in ipairs({ "red", "blue", "yellow", "gold", "silver" }) do
|
||||||
|
local versionFiles, override = CacheContract.requiredFilesFor(version)
|
||||||
|
for _, path in ipairs(versionFiles) do
|
||||||
|
fs.files[version .. "/" .. path] = true
|
||||||
|
end
|
||||||
|
if not override then
|
||||||
|
for _, path in ipairs(CacheContract.VERSION_REQUIRED_FILES[version] or {}) do
|
||||||
|
fs.files[version .. "/" .. path] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local ready, missing = CacheContract.allRequiredFilesExist(version, fs)
|
||||||
|
check(ready, version .. " complete cache is ready (" .. tostring(missing) .. ")")
|
||||||
|
local didPublish = CacheContract.publish(version, fs)
|
||||||
|
check(didPublish, version .. " complete cache publishes")
|
||||||
|
eq(fs.files[version .. "/" .. CacheContract.MARKER_PATH],
|
||||||
|
CacheContract.markerFor(version), version .. " marker is version-scoped")
|
||||||
|
eq(fs.prefix, "caller/prefix/", version .. " publication restores prefix")
|
||||||
|
check(CacheContract.isReady(version, fs), version .. " complete cache is ready")
|
||||||
|
end
|
||||||
|
|
||||||
|
local gold, goldOverride = CacheContract.requiredFilesFor("gold")
|
||||||
|
check(goldOverride, "Gold uses a version-specific required set")
|
||||||
|
local goldSet = {}
|
||||||
|
for _, path in ipairs(gold) do goldSet[path] = true end
|
||||||
|
check(goldSet["assets/generated/battle/hud/balls.png"],
|
||||||
|
"Gold required set includes trainer HUD art")
|
||||||
|
check(not goldSet["assets/generated/trade/game_boy.png"],
|
||||||
|
"Gold required set excludes Gen 1 trade art")
|
||||||
|
check(goldSet["data/generated/rom_text.lua"],
|
||||||
|
"Gold required set includes the Gen 2 engine text table")
|
||||||
|
local silver = CacheContract.requiredFilesFor("silver")
|
||||||
|
local silverSet = {}
|
||||||
|
for _, path in ipairs(silver) do silverSet[path] = true end
|
||||||
|
check(silverSet["data/generated/rom_text.lua"],
|
||||||
|
"Silver required set includes the Gen 2 engine text table")
|
||||||
|
check(not silverSet["assets/generated/trade/game_boy.png"],
|
||||||
|
"Silver required set excludes Gen 1 trade art")
|
||||||
|
check(CacheContract.VERSION_REQUIRED_FILES.yellow ~= nil,
|
||||||
|
"Yellow has version-specific required outputs")
|
||||||
|
|
||||||
|
-- A throwing adapter must not strand the process in its temporary prefix.
|
||||||
|
local throwingFs = { prefix = "before/" }
|
||||||
|
function throwingFs.exists() error("probe failed") end
|
||||||
|
local probed, probeError = CacheContract.allRequiredFilesExist("blue", throwingFs)
|
||||||
|
check(not probed and probeError ~= nil, "filesystem probe errors are returned")
|
||||||
|
eq(throwingFs.prefix, "before/", "probe errors restore the caller prefix")
|
||||||
|
function throwingFs.write() error("write failed") end
|
||||||
|
function throwingFs.remove() end
|
||||||
|
for _, path in ipairs(CacheContract.REQUIRED_FILES) do
|
||||||
|
throwingFs.files = throwingFs.files or {}
|
||||||
|
throwingFs.files["blue/" .. path] = true
|
||||||
|
end
|
||||||
|
function throwingFs.exists(path)
|
||||||
|
return throwingFs.files[throwingFs.prefix .. path] ~= nil
|
||||||
|
end
|
||||||
|
local wrote = CacheContract.publish("blue", throwingFs)
|
||||||
|
check(not wrote, "write errors are returned")
|
||||||
|
eq(throwingFs.prefix, "before/", "write errors restore the caller prefix")
|
||||||
|
|
||||||
|
-- Source-tree readiness must use the same version lists and reject a cache
|
||||||
|
-- when LÖVE cannot identify a real source directory.
|
||||||
|
local oldLove = love
|
||||||
|
love = nil
|
||||||
|
check(not CacheContract.sourceTreeHasData("red"),
|
||||||
|
"source-tree check is safe without LÖVE")
|
||||||
|
local sourceFiles = {}
|
||||||
|
love = {
|
||||||
|
filesystem = {
|
||||||
|
getRealDirectory = function(path) return sourceFiles[path] end,
|
||||||
|
getSource = function() return "/source" end,
|
||||||
|
getInfo = function(path)
|
||||||
|
return sourceFiles[path] and { type = "file" } or nil
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, path in ipairs(CacheContract.REQUIRED_FILES) do sourceFiles[path] = "/source" end
|
||||||
|
check(CacheContract.sourceTreeHasData("red"),
|
||||||
|
"Red source tree uses the shared required set")
|
||||||
|
sourceFiles[CacheContract.REQUIRED_FILES[2]] = "/save"
|
||||||
|
check(not CacheContract.sourceTreeHasData("red"),
|
||||||
|
"source-tree readiness rejects a cache-overlaid required file")
|
||||||
|
sourceFiles = {}
|
||||||
|
local goldFiles = CacheContract.requiredFilesFor("gold")
|
||||||
|
for _, path in ipairs(goldFiles) do sourceFiles["gold/" .. path] = "/source" end
|
||||||
|
check(CacheContract.sourceTreeHasData("gold"),
|
||||||
|
"Gold source tree uses its override set")
|
||||||
|
love = oldLove
|
||||||
|
|
||||||
|
-- Both importer completion paths must call the shared publication boundary.
|
||||||
|
local importerFile = assert(io.open("src/import/RomImporter.lua", "r"))
|
||||||
|
local importerSource = importerFile:read("*a")
|
||||||
|
importerFile:close()
|
||||||
|
local completionCalls = 0
|
||||||
|
for _ in importerSource:gmatch("CacheContract%.publish%(%s*version") do
|
||||||
|
completionCalls = completionCalls + 1
|
||||||
|
end
|
||||||
|
eq(completionCalls, 1,
|
||||||
|
"both thread and coroutine paths converge on one publishing helper")
|
||||||
|
check(importerSource:find("self:_completeImport%(version, prefix, displayName%)")
|
||||||
|
~= nil, "coroutine completion uses the shared helper")
|
||||||
|
check(importerSource:find("pcall%(self%._completeImport") ~= nil,
|
||||||
|
"thread completion uses the shared helper")
|
||||||
|
|
||||||
|
T.finish("rom cache contract")
|
||||||
@@ -1,32 +1,34 @@
|
|||||||
-- sourceTreeHasData must use each version's required-file list. Gold's
|
-- sourceTreeHasData must use the engine-owned cache contract. Gold's cache has
|
||||||
-- cache has no Gen 1 trade art / pikachu.png; validating it against
|
-- no Gen 1 trade art; validating it against the Gen 1 list made a Gold source
|
||||||
-- REQUIRED_FILES made a Gold source tree look incomplete forever.
|
-- tree look incomplete forever.
|
||||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
|
||||||
local T = require("tests.harness")
|
local T = require("tests.harness")
|
||||||
local check = T.check
|
local check = T.check
|
||||||
|
local CacheContract = require("src.import.CacheContract")
|
||||||
|
|
||||||
local f = assert(io.open("src/import/RomImporter.lua", "r"))
|
local f = assert(io.open("src/import/RomImporter.lua", "r"))
|
||||||
local src = f:read("*a")
|
local src = f:read("*a")
|
||||||
f:close()
|
f:close()
|
||||||
|
|
||||||
local start = src:find("local function sourceTreeHasData", 1, true)
|
local readyStart = src:find("function RomImporter.isReady", 1, true)
|
||||||
check(start ~= nil, "sourceTreeHasData is defined")
|
check(readyStart ~= nil, "isReady is defined")
|
||||||
local finish = src:find("\nfunction RomImporter.isReady", start, true)
|
local readyEnd = src:find("\nfunction RomImporter.syncAndroidShortcuts", readyStart, true)
|
||||||
check(finish ~= nil, "sourceTreeHasData ends before isReady")
|
check(readyEnd ~= nil, "isReady ends before the next importer helper")
|
||||||
local body = src:sub(start, finish)
|
local readyBody = src:sub(readyStart, readyEnd)
|
||||||
|
|
||||||
check(body:find("requiredFilesFor", 1, true) ~= nil,
|
check(readyBody:find("CacheContract.isReady", 1, true) ~= nil,
|
||||||
"sourceTreeHasData uses requiredFilesFor (Gold override, not Gen 1 only)")
|
"isReady delegates source-tree and cache readiness to the contract")
|
||||||
check(body:find("ipairs(REQUIRED_FILES)", 1, true) == nil,
|
check(readyBody:find("ipairs(REQUIRED_FILES)", 1, true) == nil,
|
||||||
"sourceTreeHasData does not iterate the Gen 1 REQUIRED_FILES list raw")
|
"isReady does not iterate the Gen 1 REQUIRED_FILES list raw")
|
||||||
|
|
||||||
local helperStart = src:find("local function requiredFilesFor", 1, true)
|
local required, isOverride = CacheContract.requiredFilesFor("gold")
|
||||||
check(helperStart ~= nil, "requiredFilesFor helper exists")
|
check(isOverride, "Gold uses the override required-file list")
|
||||||
local helper = src:sub(helperStart, start)
|
local requiredSet = {}
|
||||||
check(helper:find("VERSION_REQUIRED_FILES_OVERRIDE", 1, true) ~= nil,
|
for _, path in ipairs(required) do requiredSet[path] = true end
|
||||||
"requiredFilesFor consults VERSION_REQUIRED_FILES_OVERRIDE")
|
check(requiredSet["assets/generated/battle/hud/balls.png"],
|
||||||
check(src:find('"assets/generated/battle/hud/balls.png"', 1, true) ~= nil,
|
|
||||||
"Gold caches require the trainer HUD ball sheet")
|
"Gold caches require the trainer HUD ball sheet")
|
||||||
|
check(not requiredSet["assets/generated/trade/game_boy.png"],
|
||||||
|
"Gold does not inherit the Gen 1 trade-art requirement")
|
||||||
|
|
||||||
T.finish()
|
T.finish()
|
||||||
|
|||||||
@@ -84,15 +84,15 @@ if extractor then
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- a cache imported before #750 has none of the art; listing one of the
|
-- a cache imported before #750 has none of the art; listing one of the
|
||||||
-- files in REQUIRED_FILES is what makes it re-import
|
-- files in the engine-owned cache contract is what makes it re-import
|
||||||
local importer = readFile("src/import/RomImporter.lua")
|
local contract = readFile("src/import/CacheContract.lua")
|
||||||
T.check(importer ~= nil, "src/import/RomImporter.lua is readable")
|
T.check(contract ~= nil, "src/import/CacheContract.lua is readable")
|
||||||
if importer then
|
if contract then
|
||||||
local required = importer:match("local REQUIRED_FILES = {(.-)\n}")
|
local required = contract:match("CacheContract.REQUIRED_FILES = {(.-)\n}")
|
||||||
T.check(required ~= nil, "REQUIRED_FILES parses")
|
T.check(required ~= nil, "CacheContract.REQUIRED_FILES parses")
|
||||||
T.check(required ~= nil and required:find(
|
T.check(required ~= nil and required:find(
|
||||||
'"assets/generated/trade/game_boy.png"', 1, true) ~= nil,
|
'"assets/generated/trade/game_boy.png"', 1, true) ~= nil,
|
||||||
"REQUIRED_FILES makes pre-#750 caches re-import the trade art")
|
"cache contract makes pre-#750 caches re-import the trade art")
|
||||||
end
|
end
|
||||||
|
|
||||||
T.finish("trade art import")
|
T.finish("trade art import")
|
||||||
|
|||||||
Reference in New Issue
Block a user