mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 16:31:05 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c274d969e | |||
| 4c7633b56e |
@@ -50,18 +50,20 @@ developer data build, test suites, and cache management are covered in
|
||||
By default the game keeps your save, options, and the private ROM-derived
|
||||
data cache in your OS's normal per-user app data folder. To keep everything
|
||||
next to the game instead (handy for a USB stick or portable drive you carry
|
||||
between computers), drop an empty file named `portable.txt` next to the
|
||||
executable (or next to `main.lua`/`conf.lua` when running from source), then
|
||||
launch the game.
|
||||
between computers), drop an empty file named `portable.txt` next to the app
|
||||
(next to `PokemonRed.app`/`.exe`, or next to `main.lua`/`conf.lua` when
|
||||
running from source), then launch the game. Portable mode is desktop-only
|
||||
(Windows, Linux, macOS); it has no effect on Android or iOS, where the app
|
||||
runs from a read-only package.
|
||||
|
||||
With `portable.txt` present:
|
||||
|
||||
- `save.lua`, `save.lua.bak`, and `options.lua` are read from and written to
|
||||
that same folder instead of the OS save directory.
|
||||
- After a ROM import, the generated `data/generated` and `assets/generated`
|
||||
cache is copied into that folder too, so a later launch (even on a
|
||||
different computer, as long as the same folder comes along) reuses it
|
||||
without asking for the ROM again.
|
||||
- A ROM import writes the generated `data/generated` and `assets/generated`
|
||||
cache straight into that folder too (nothing is left in the OS save
|
||||
directory), so a later launch reuses it without asking for the ROM again
|
||||
even on a different computer, as long as the same folder comes along.
|
||||
- Deleting `portable.txt` switches back to the normal OS save directory; nothing
|
||||
already written to either location is touched automatically, so copy files
|
||||
over yourself if you want to carry existing progress across the switch.
|
||||
|
||||
+33
-7
@@ -83,15 +83,41 @@ local function detectPortable()
|
||||
portableChecked = true
|
||||
portableBase = false
|
||||
if not (love and love.filesystem) then return false end
|
||||
-- Desktop only: portable mode carries the save (and, since issue #74, the
|
||||
-- ROM cache) in the game folder next to the executable/source. On
|
||||
-- Android/iOS the source is a read-only package with no such folder, so
|
||||
-- portable mode never applies there.
|
||||
if love.system and love.system.getOS then
|
||||
local osName = love.system.getOS()
|
||||
if osName ~= "Windows" and osName ~= "Linux" and osName ~= "OS X" then
|
||||
return false
|
||||
end
|
||||
end
|
||||
local src = love.filesystem.getSource and love.filesystem.getSource()
|
||||
local sbd = love.filesystem.getSourceBaseDirectory
|
||||
and love.filesystem.getSourceBaseDirectory()
|
||||
-- A packaged macOS build nests the game inside PokemonRed.app/Contents/
|
||||
-- Resources, so getSource()/getSourceBaseDirectory() point INSIDE the
|
||||
-- bundle -- not where the player drops portable.txt (next to the .app).
|
||||
-- Recover the folder containing the .app so a packaged app finds its
|
||||
-- marker. On Windows/Linux the executable is not a bundle, so this is nil
|
||||
-- and the plain source-base directory (next to the .exe/AppImage) is used.
|
||||
local function appContainer(path)
|
||||
local appPath = path and path:match("^(.*%.app)/Contents/")
|
||||
return appPath and appPath:match("^(.*)/[^/]+$") or nil
|
||||
end
|
||||
-- Order: the .app's containing folder (packaged macOS), then the
|
||||
-- source-base directory (next to a packaged .exe/AppImage), then the
|
||||
-- source itself (a `love <gamedir>` run drops portable.txt in the game
|
||||
-- folder). First one holding the marker wins. Built by appending so a
|
||||
-- nil (e.g. no .app in the path) never truncates the ipairs scan.
|
||||
local candidates = {}
|
||||
if love.filesystem.getSourceBaseDirectory then
|
||||
candidates[#candidates + 1] = love.filesystem.getSourceBaseDirectory()
|
||||
end
|
||||
if love.filesystem.getSource then
|
||||
candidates[#candidates + 1] = love.filesystem.getSource()
|
||||
end
|
||||
local appDir = appContainer(src) or appContainer(sbd)
|
||||
if appDir then candidates[#candidates + 1] = appDir end
|
||||
if sbd then candidates[#candidates + 1] = sbd end
|
||||
if src then candidates[#candidates + 1] = src end
|
||||
for _, base in ipairs(candidates) do
|
||||
if base and base ~= "" and pathExists(base .. SEP .. PORTABLE_MARKER) then
|
||||
if base ~= "" and pathExists(base .. SEP .. PORTABLE_MARKER) then
|
||||
portableBase = base
|
||||
break
|
||||
end
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
-- Routes ROM-derived cache I/O (data/generated, assets/generated and the
|
||||
-- rom-cache.complete marker) to the right place.
|
||||
--
|
||||
-- Normally the cache lives in LÖVE's per-user OS save directory and is
|
||||
-- written through love.filesystem. In portable mode it lives in the game
|
||||
-- folder next to the executable instead (the folder holding portable.txt --
|
||||
-- see SaveData), so nothing is left on the host machine. That folder is
|
||||
-- written with raw io.* (love.filesystem can only write to the save dir) and
|
||||
-- read back through love.filesystem, require and love.graphics.newImage --
|
||||
-- which works because the folder is on the physfs read path:
|
||||
--
|
||||
-- * Source runs (`love <gamedir>`, what the Play-* launchers use): the
|
||||
-- folder IS the physfs source, so it is already readable.
|
||||
-- * Fused builds (the packaged .app/.exe): the folder sits next to the
|
||||
-- executable and is NOT normally readable, so CacheFs mounts it onto the
|
||||
-- read path via PhysFS. love.filesystem.mount refuses external folders,
|
||||
-- but the underlying PHYSFS_mount (exported from love's framework) allows
|
||||
-- them; we call it through LuaJIT's FFI.
|
||||
--
|
||||
-- Directories in the portable folder are created with a plain mkdir syscall
|
||||
-- via FFI rather than os.execute, so importing never flashes a console window
|
||||
-- on Windows (issue #74 -- the old per-file `os.execute("mkdir")` froze the
|
||||
-- app behind a storm of one-frame cmd.exe windows).
|
||||
--
|
||||
-- Portable mode is desktop-only (Windows/Linux/macOS); on Android/iOS the
|
||||
-- source is a read-only package with no game folder to write into, so
|
||||
-- SaveData.isPortable() is false there and this module falls back to the
|
||||
-- ordinary love.filesystem/save-directory behaviour.
|
||||
|
||||
local CacheFs = {}
|
||||
|
||||
local SEP = package.config:sub(1, 1)
|
||||
|
||||
-- lazily-resolved windowless mkdir: function(absolutePath) or false when
|
||||
-- FFI is unavailable (the cache then stays on the save directory)
|
||||
local mkdirFn = nil
|
||||
|
||||
local function resolveMkdir()
|
||||
if mkdirFn ~= nil then return mkdirFn end
|
||||
mkdirFn = false
|
||||
local ok, ffi = pcall(require, "ffi")
|
||||
if not ok then return mkdirFn end
|
||||
if ffi.os == "Windows" then
|
||||
-- kernel32 is reliably resolvable through ffi.C on Windows (the engine
|
||||
-- already binds it in DiscordPresence); CreateDirectoryA returns
|
||||
-- nonzero on success and 0 when the directory already exists -- both
|
||||
-- fine, the result is ignored.
|
||||
pcall(ffi.cdef,
|
||||
"int CreateDirectoryA(const char *lpPathName, void *lpSecurityAttributes);")
|
||||
local resolved = pcall(function() return ffi.C.CreateDirectoryA end)
|
||||
if resolved then
|
||||
mkdirFn = function(path) pcall(ffi.C.CreateDirectoryA, path, nil) end
|
||||
end
|
||||
else
|
||||
pcall(ffi.cdef, "int mkdir(const char *pathname, unsigned int mode);")
|
||||
local resolved = pcall(function() return ffi.C.mkdir end)
|
||||
if resolved then
|
||||
mkdirFn = function(path) pcall(ffi.C.mkdir, path, 493) end -- 0755
|
||||
end
|
||||
end
|
||||
return mkdirFn
|
||||
end
|
||||
|
||||
-- Mount an external directory onto the physfs read path (appended, so the
|
||||
-- game's own source always wins a name clash). Returns true on success.
|
||||
-- Guarded and lazily bound like the mkdir helper; PHYSFS_mount is exported
|
||||
-- from love's framework, so ffi.C resolves it in the running process.
|
||||
local physfsMountFn = nil
|
||||
local function mountReadable(dir)
|
||||
if physfsMountFn == nil then
|
||||
physfsMountFn = false
|
||||
local ok, ffi = pcall(require, "ffi")
|
||||
if ok then
|
||||
pcall(ffi.cdef,
|
||||
"int PHYSFS_mount(const char *newDir, const char *mountPoint, int appendToPath);")
|
||||
if pcall(function() return ffi.C.PHYSFS_mount end) then
|
||||
physfsMountFn = function(d)
|
||||
local okc, ret = pcall(ffi.C.PHYSFS_mount, d, "", 1)
|
||||
return okc and ret ~= 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if not physfsMountFn then return false end
|
||||
return physfsMountFn(dir)
|
||||
end
|
||||
|
||||
-- The portable game folder when the cache should live there, else nil.
|
||||
-- Resolved (and, for a fused build, mounted) once and cached. Requires a
|
||||
-- desktop portable install (SaveData) and a working windowless mkdir.
|
||||
local portableRoot = nil
|
||||
local portableResolved = false
|
||||
local function resolvePortableRoot()
|
||||
if portableResolved then return portableRoot end
|
||||
portableResolved = true
|
||||
portableRoot = nil
|
||||
if not resolveMkdir() then return nil end
|
||||
local base = require("src.core.SaveData").portableBaseDir()
|
||||
if not base then return nil end
|
||||
if love.filesystem.getSource and base == love.filesystem.getSource() then
|
||||
-- source run: the folder is already the physfs source
|
||||
portableRoot = base
|
||||
elseif mountReadable(base) then
|
||||
-- fused build: base is next to the executable; mount it so io.* writes
|
||||
-- there are visible to love.filesystem/require/newImage
|
||||
portableRoot = base
|
||||
end
|
||||
return portableRoot
|
||||
end
|
||||
|
||||
function CacheFs.root()
|
||||
return resolvePortableRoot()
|
||||
end
|
||||
|
||||
local function realPath(root, rel)
|
||||
return root .. SEP .. rel:gsub("/", SEP)
|
||||
end
|
||||
|
||||
-- create every parent directory of `rel` under `root` (best effort; an
|
||||
-- already-existing directory is fine, a genuine failure surfaces when the
|
||||
-- subsequent io.open write fails)
|
||||
local function ensureParents(root, rel)
|
||||
local mkdir = resolveMkdir()
|
||||
if not mkdir then return end
|
||||
local parts = {}
|
||||
for part in rel:gmatch("[^/]+") do parts[#parts + 1] = part end
|
||||
local cur = root
|
||||
for i = 1, #parts - 1 do
|
||||
cur = cur .. SEP .. parts[i]
|
||||
mkdir(cur)
|
||||
end
|
||||
end
|
||||
|
||||
-- write cache-relative `rel` (forward-slash path) with the given bytes;
|
||||
-- returns ok, err like love.filesystem.write
|
||||
function CacheFs.write(rel, data)
|
||||
local root = CacheFs.root()
|
||||
if root then
|
||||
ensureParents(root, rel)
|
||||
local f, err = io.open(realPath(root, rel), "wb")
|
||||
if not f then return false, err end
|
||||
f:write(data)
|
||||
f:close()
|
||||
return true
|
||||
end
|
||||
local parent = rel:match("^(.*)/[^/]+$")
|
||||
if parent and not love.filesystem.createDirectory(parent) then
|
||||
local info = love.filesystem.getInfo(parent)
|
||||
local reason = info and ("a " .. info.type .. " already exists there")
|
||||
or "unknown reason"
|
||||
return false, "could not create " .. parent .. ": " .. reason
|
||||
end
|
||||
return love.filesystem.write(rel, data)
|
||||
end
|
||||
|
||||
-- read cache-relative `rel`; returns the bytes or nil
|
||||
function CacheFs.read(rel)
|
||||
local root = CacheFs.root()
|
||||
if root then
|
||||
local f = io.open(realPath(root, rel), "rb")
|
||||
if not f then return nil end
|
||||
local data = f:read("*a")
|
||||
f:close()
|
||||
return data
|
||||
end
|
||||
return love.filesystem.read(rel)
|
||||
end
|
||||
|
||||
-- does cache-relative `rel` exist as a file?
|
||||
function CacheFs.exists(rel)
|
||||
local root = CacheFs.root()
|
||||
if root then
|
||||
local f = io.open(realPath(root, rel), "rb")
|
||||
if not f then return false end
|
||||
f:close()
|
||||
return true
|
||||
end
|
||||
return love.filesystem.getInfo(rel, "file") ~= nil
|
||||
end
|
||||
|
||||
-- remove a single cache-relative file
|
||||
function CacheFs.remove(rel)
|
||||
local root = CacheFs.root()
|
||||
if root then
|
||||
os.remove(realPath(root, rel))
|
||||
return
|
||||
end
|
||||
love.filesystem.remove(rel)
|
||||
end
|
||||
|
||||
-- Remove the game-folder copy of a cache subtree before a fresh import, so a
|
||||
-- cache-format bump does not leave orphaned files behind. No-op when the
|
||||
-- portable cache is inactive (the save-directory copy is cleared by
|
||||
-- RomImporter's own removeTree). The tree is enumerated through
|
||||
-- love.filesystem (the game folder is mounted) and the real files deleted
|
||||
-- with os.remove; empty directories are harmless and left in place.
|
||||
function CacheFs.removeTree(rel)
|
||||
local root = CacheFs.root()
|
||||
if not root then return end
|
||||
local function walk(r)
|
||||
local info = love.filesystem.getInfo(r)
|
||||
if not info then return end
|
||||
if info.type == "directory" then
|
||||
for _, child in ipairs(love.filesystem.getDirectoryItems(r)) do
|
||||
walk(r .. "/" .. child)
|
||||
end
|
||||
else
|
||||
os.remove(realPath(root, r))
|
||||
end
|
||||
end
|
||||
walk(rel)
|
||||
end
|
||||
|
||||
return CacheFs
|
||||
@@ -126,14 +126,14 @@ function ImageWriter.columnsToRows(raw, tilesWide, tilesHigh, bytesPerTile)
|
||||
end
|
||||
|
||||
function ImageWriter.save(image, path)
|
||||
local parent = path:match("^(.*)/[^/]+$")
|
||||
if parent then
|
||||
local ok, err = love.filesystem.createDirectory(parent)
|
||||
if not ok then error("could not create " .. parent .. ": " .. tostring(err)) end
|
||||
end
|
||||
local ok, fileData = pcall(image.encode, image, "png")
|
||||
if not ok then error("could not encode " .. path .. ": " .. tostring(fileData)) end
|
||||
local written, writeError = love.filesystem.write(path, fileData)
|
||||
-- CacheFs routes this to the OS save directory (normal builds) or straight
|
||||
-- into the game folder (portable installs), creating parent directories as
|
||||
-- needed. io.* needs the bytes as a string; love.filesystem would also
|
||||
-- take the FileData, but getString() keeps one code path.
|
||||
local CacheFs = require("src.import.CacheFs")
|
||||
local written, writeError = CacheFs.write(path, fileData:getString())
|
||||
if not written then
|
||||
error("could not write " .. path .. ": " .. tostring(writeError))
|
||||
end
|
||||
|
||||
@@ -87,17 +87,11 @@ function LuaWriter.encode(value)
|
||||
end
|
||||
|
||||
function LuaWriter.write(path, value)
|
||||
local parent = path:match("^(.*)/[^/]+$")
|
||||
-- love.filesystem.createDirectory returns a single boolean (no error
|
||||
-- value), so a second return here is always nil; getInfo after a failure
|
||||
-- at least reports what is blocking the path (e.g. a file with that name).
|
||||
if parent and not love.filesystem.createDirectory(parent) then
|
||||
local info = love.filesystem.getInfo(parent)
|
||||
local reason = info and ("a " .. info.type .. " already exists there")
|
||||
or "unknown reason"
|
||||
error("could not create " .. parent .. ": " .. reason)
|
||||
end
|
||||
local ok, err = love.filesystem.write(path, LuaWriter.encode(value))
|
||||
-- CacheFs routes this to the OS save directory (normal builds) or straight
|
||||
-- into the game folder (portable installs); it also creates the parent
|
||||
-- directories. See src/import/CacheFs.lua.
|
||||
local CacheFs = require("src.import.CacheFs")
|
||||
local ok, err = CacheFs.write(path, LuaWriter.encode(value))
|
||||
if not ok then error("could not write " .. path .. ": " .. tostring(err)) end
|
||||
end
|
||||
|
||||
|
||||
@@ -1669,10 +1669,11 @@ function RomExtractor:extractAudio()
|
||||
chunks[index] = self.rom.data:sub(first, first + 0x3FFF)
|
||||
self:tick("Sound programs", index, #bankOrder + 2)
|
||||
end
|
||||
local ok, writeError = love.filesystem.createDirectory(
|
||||
"assets/generated/audio")
|
||||
if ok == false then error("could not create audio cache: " .. tostring(writeError)) end
|
||||
ok, writeError = love.filesystem.write(
|
||||
-- CacheFs (not love.filesystem directly) so a portable install lands this
|
||||
-- in the game folder with the rest of the cache; it creates the parent
|
||||
-- directory too.
|
||||
local CacheFs = require("src.import.CacheFs")
|
||||
local ok, writeError = CacheFs.write(
|
||||
"assets/generated/audio/programs.bin", table.concat(chunks))
|
||||
if not ok then error("could not write audio programs: " .. tostring(writeError)) end
|
||||
|
||||
|
||||
+71
-128
@@ -24,8 +24,11 @@ local REQUIRED_FILES = {
|
||||
}
|
||||
|
||||
local function allRequiredFilesExist()
|
||||
-- CacheFs.exists checks the game folder directly for a portable install,
|
||||
-- otherwise the save directory through love.filesystem.
|
||||
local CacheFs = require("src.import.CacheFs")
|
||||
for _, path in ipairs(REQUIRED_FILES) do
|
||||
if not love.filesystem.getInfo(path, "file") then return false end
|
||||
if not CacheFs.exists(path) then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
@@ -38,128 +41,21 @@ local function sourceTreeHasData()
|
||||
return real == love.filesystem.getSource()
|
||||
end
|
||||
|
||||
-- ------- portable ROM-derived asset cache
|
||||
-- ------- ROM cache location
|
||||
--
|
||||
-- The extracted cache (data/generated, assets/generated) is written
|
||||
-- exclusively through love.filesystem.write, which always targets the OS
|
||||
-- save directory -- it cannot be redirected to an arbitrary folder. So a
|
||||
-- portable install mirrors the cache both ways instead: after a fresh
|
||||
-- import, every generated file is copied out to the portable folder
|
||||
-- (SaveData.portableFs's io.* companion); on a later boot -- possibly on a
|
||||
-- different machine sharing the same USB copy -- a matching portable
|
||||
-- cache is copied back into the save directory before the normal
|
||||
-- isReady() check runs, so nothing downstream needs to know the cache
|
||||
-- ever lived anywhere but the save directory.
|
||||
local PORTABLE_CACHE_DIRS = { "data/generated", "assets/generated" }
|
||||
local PORTABLE_MANIFEST_NAME = "portable_cache_manifest.txt"
|
||||
local PORTABLE_SEP = package.config:sub(1, 1)
|
||||
|
||||
local function walkLoveDir(dir, out)
|
||||
out = out or {}
|
||||
for _, name in ipairs(love.filesystem.getDirectoryItems(dir)) do
|
||||
local full = dir .. "/" .. name
|
||||
local info = love.filesystem.getInfo(full)
|
||||
if info and info.type == "directory" then
|
||||
walkLoveDir(full, out)
|
||||
elseif info and info.type == "file" then
|
||||
out[#out + 1] = full
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
local function portablePath(base, relPath)
|
||||
return base .. PORTABLE_SEP .. relPath:gsub("/", PORTABLE_SEP)
|
||||
end
|
||||
|
||||
local function ensurePortableDir(fullDirPath)
|
||||
if love.system.getOS() == "Windows" then
|
||||
os.execute(('mkdir "%s" 2>NUL'):format(fullDirPath))
|
||||
else
|
||||
os.execute(("mkdir -p '%s' 2>/dev/null"):format(fullDirPath))
|
||||
end
|
||||
end
|
||||
|
||||
-- copies data/generated + assets/generated out to the portable folder
|
||||
-- after a fresh import; a plain-text manifest travels alongside so a
|
||||
-- later sync-in knows exactly which files to copy back without needing
|
||||
-- to list an arbitrary external directory (io.* has no listdir)
|
||||
local function syncCacheToPortable()
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local base = SaveData.portableBaseDir()
|
||||
if not base then return end
|
||||
local manifest = {}
|
||||
for _, dir in ipairs(PORTABLE_CACHE_DIRS) do
|
||||
if love.filesystem.getInfo(dir, "directory") then
|
||||
for _, relPath in ipairs(walkLoveDir(dir)) do
|
||||
local data = love.filesystem.read(relPath)
|
||||
if data then
|
||||
local outPath = portablePath(base, relPath)
|
||||
local outDir = outPath:match("^(.*)" .. PORTABLE_SEP .. "[^" .. PORTABLE_SEP .. "]+$")
|
||||
if outDir then ensurePortableDir(outDir) end
|
||||
local f, err = io.open(outPath, "wb")
|
||||
if f then
|
||||
f:write(data)
|
||||
f:close()
|
||||
manifest[#manifest + 1] = relPath
|
||||
else
|
||||
require("src.core.Logger").error(
|
||||
"portable cache: could not write %s: %s", outPath, tostring(err))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local mf = io.open(base .. PORTABLE_SEP .. PORTABLE_MANIFEST_NAME, "wb")
|
||||
if mf then
|
||||
mf:write(table.concat(manifest, "\n"))
|
||||
mf:close()
|
||||
end
|
||||
local mk = io.open(base .. PORTABLE_SEP .. MARKER_PATH, "wb")
|
||||
if mk then
|
||||
mk:write(CACHE_MARKER)
|
||||
mk:close()
|
||||
end
|
||||
end
|
||||
|
||||
-- copies a matching portable cache back into the save directory before
|
||||
-- isReady() runs its normal check; a mismatched or missing marker means
|
||||
-- either no portable cache exists yet or it belongs to an older build, so
|
||||
-- it is left alone and a fresh import proceeds as usual
|
||||
local function syncCacheFromPortable()
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local base = SaveData.portableBaseDir()
|
||||
if not base then return end
|
||||
local markerFile = io.open(base .. PORTABLE_SEP .. MARKER_PATH, "rb")
|
||||
if not markerFile then return end
|
||||
local marker = markerFile:read("*a")
|
||||
markerFile:close()
|
||||
if marker ~= CACHE_MARKER then return end
|
||||
local manifestFile = io.open(base .. PORTABLE_SEP .. PORTABLE_MANIFEST_NAME, "rb")
|
||||
if not manifestFile then return end
|
||||
local manifestBody = manifestFile:read("*a")
|
||||
manifestFile:close()
|
||||
for relPath in manifestBody:gmatch("[^\r\n]+") do
|
||||
local f = io.open(portablePath(base, relPath), "rb")
|
||||
if f then
|
||||
local data = f:read("*a")
|
||||
f:close()
|
||||
love.filesystem.write(relPath, data)
|
||||
end
|
||||
end
|
||||
love.filesystem.write(MARKER_PATH, CACHE_MARKER)
|
||||
end
|
||||
|
||||
function RomImporter.isReady()
|
||||
if sourceTreeHasData() then return true end
|
||||
if love.filesystem.read(MARKER_PATH) ~= CACHE_MARKER
|
||||
and require("src.core.SaveData").isPortable() then
|
||||
syncCacheFromPortable()
|
||||
end
|
||||
return love.filesystem.read(MARKER_PATH) == CACHE_MARKER
|
||||
and allRequiredFilesExist()
|
||||
end
|
||||
-- The extracted cache (data/generated, assets/generated) plus the
|
||||
-- rom-cache.complete marker normally live in LÖVE's per-user OS save
|
||||
-- directory. A portable install instead keeps them in the game folder next
|
||||
-- to the executable (the folder holding portable.txt), so nothing is left on
|
||||
-- the host machine. Every cache write/read/remove goes through CacheFs,
|
||||
-- which writes that folder with io.* and makes it readable (mounting it via
|
||||
-- PhysFS for a fused build) -- there is no mirror step and no per-file
|
||||
-- os.execute (issue #74: that flashed a console window per file on Windows
|
||||
-- and froze the app).
|
||||
|
||||
-- Remove a cache subtree from the OS save directory. The realDirectory
|
||||
-- guard keeps this from ever deleting the game folder (portable installs
|
||||
-- read the cache from there) or a developer's checked-out source tree.
|
||||
local function removeTree(path)
|
||||
local info = love.filesystem.getInfo(path)
|
||||
if not info then return end
|
||||
@@ -179,6 +75,48 @@ local function removeTree(path)
|
||||
end
|
||||
end
|
||||
|
||||
-- Portable installs read the cache from the game folder. Any copy an
|
||||
-- earlier non-portable run -- or the pre-#74 build, which always wrote the
|
||||
-- cache to the save directory and only mirrored it out -- left behind would
|
||||
-- shadow it, because physfs searches the save directory before the source.
|
||||
-- Clear it out once, and only when a remnant is actually present so a clean
|
||||
-- install pays nothing.
|
||||
local saveDirPurged = false
|
||||
local function purgeSaveDirCache()
|
||||
if saveDirPurged then return end
|
||||
saveDirPurged = true
|
||||
local saveDir = love.filesystem.getSaveDirectory()
|
||||
local function saveDirHas(rel)
|
||||
local f = io.open(saveDir .. "/" .. rel, "rb")
|
||||
if not f then return false end
|
||||
f:close()
|
||||
return true
|
||||
end
|
||||
if not (saveDirHas(MARKER_PATH) or saveDirHas(REQUIRED_FILES[1])) then
|
||||
return
|
||||
end
|
||||
removeTree("data/generated")
|
||||
removeTree("assets/generated")
|
||||
love.filesystem.remove(MARKER_PATH)
|
||||
end
|
||||
|
||||
function RomImporter.isReady()
|
||||
local CacheFs = require("src.import.CacheFs")
|
||||
if CacheFs.root() then
|
||||
-- Portable: the cache lives in the game folder next to the executable
|
||||
-- (mounted onto the read path for a fused build). Drop any stale
|
||||
-- save-directory copy that would otherwise shadow it at runtime -- and,
|
||||
-- for a source run, hide the game folder from sourceTreeHasData below.
|
||||
purgeSaveDirCache()
|
||||
end
|
||||
-- Generated data sitting in the physfs source -- a developer checkout, a
|
||||
-- Python/bootstrap build, or a source-run portable import -- is always
|
||||
-- current (as it has always been). A fused portable install is not the
|
||||
-- source, so it falls through to the version-marker gate.
|
||||
if sourceTreeHasData() then return true end
|
||||
return CacheFs.read(MARKER_PATH) == CACHE_MARKER and allRequiredFilesExist()
|
||||
end
|
||||
|
||||
local function decodeManifest()
|
||||
local raw, readError = love.filesystem.read("tools/rom_manifest.json")
|
||||
if not raw then error("ROM import metadata is missing: " .. tostring(readError)) end
|
||||
@@ -268,7 +206,7 @@ local function chooseRom()
|
||||
end
|
||||
|
||||
function RomImporter.new(onComplete)
|
||||
local previousMarker = love.filesystem.read(MARKER_PATH)
|
||||
local previousMarker = require("src.import.CacheFs").read(MARKER_PATH)
|
||||
local returning = previousMarker ~= nil and previousMarker ~= CACHE_MARKER
|
||||
local android = love.system.getOS() == "Android"
|
||||
local self = setmetatable({
|
||||
@@ -350,9 +288,15 @@ function RomImporter:startData(data, displayName)
|
||||
end
|
||||
self.status = "Preparing private game data"
|
||||
coroutine.yield()
|
||||
-- Clear any previous cache from both possible homes: the save directory
|
||||
-- (removeTree) and, for a portable install, the game folder (CacheFs).
|
||||
local CacheFs = require("src.import.CacheFs")
|
||||
removeTree("data/generated")
|
||||
removeTree("assets/generated")
|
||||
love.filesystem.remove(MARKER_PATH)
|
||||
CacheFs.removeTree("data/generated")
|
||||
CacheFs.removeTree("assets/generated")
|
||||
CacheFs.remove(MARKER_PATH)
|
||||
|
||||
local manifest = decodeManifest()
|
||||
local RomExtractor = require("src.import.RomExtractor")
|
||||
@@ -367,13 +311,12 @@ function RomImporter:startData(data, displayName)
|
||||
extractor:run()
|
||||
self.romData = nil
|
||||
collectgarbage("collect")
|
||||
local ok, writeError = love.filesystem.write(MARKER_PATH, CACHE_MARKER)
|
||||
-- Written last: the marker is what isReady() checks, so it must only
|
||||
-- appear once every required file is in place. CacheFs puts it beside
|
||||
-- the cache -- the game folder for a portable install, else the save
|
||||
-- directory.
|
||||
local ok, writeError = CacheFs.write(MARKER_PATH, CACHE_MARKER)
|
||||
if not ok then error("could not finish the private cache: " .. tostring(writeError)) end
|
||||
if require("src.core.SaveData").isPortable() then
|
||||
self.status = "Copying data to the portable folder"
|
||||
coroutine.yield()
|
||||
syncCacheToPortable()
|
||||
end
|
||||
self.state = "complete"
|
||||
self.status = "Ready"
|
||||
self.detail = "Starting Pokemon Red..."
|
||||
|
||||
Reference in New Issue
Block a user