Compare commits

..

2 Commits

Author SHA1 Message Date
bryanthaboi e2f2b432ff bazinga moment 2 2026-07-22 15:42:19 -04:00
bryanthaboi 4c7633b56e PORTABLE MODE 2026-07-22 14:18:39 -04:00
12 changed files with 384 additions and 170 deletions
+9 -7
View File
@@ -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.
+13 -4
View File
@@ -37,10 +37,19 @@ function love.conf(t)
local osName = love._os
local mobile = osName == "Android" or osName == "iOS"
if mobile then
-- On Android/iOS, width/height aspect picks portrait vs landscape
-- (fullscreen alone is not enough). Use a tall portrait size; the
-- OS then resizes to the real display. highdpi is required for
-- Retina iOS (Android always behaves as highdpi).
-- resizable is what unlocks orientation. SDL's Android backend, given no
-- SDL_HINT_ORIENTATIONS (LÖVE sets none), calls setRequestedOrientation
-- at window creation -- FULL_SENSOR when the window is resizable (rotates
-- freely to portrait or landscape), otherwise locked to the window's w/h
-- aspect. So a non-resizable tall window forced portrait; resizable lets
-- the game follow the device. The renderer letterboxes the 160x144
-- viewport into whatever size results, and touch input is gesture-based,
-- so both orientations just work. iOS follows the Info.plist orientations
-- (see mobile/ios/overlays/love-ios.plist, now portrait + landscape).
t.window.resizable = true
-- Starting size is a tall portrait hint; the OS resizes to the real
-- display and rotations resize again. highdpi is required for Retina iOS
-- (Android always behaves as highdpi).
t.window.width = 1080
t.window.height = 1920
t.window.fullscreen = true
+3 -1
View File
@@ -6,7 +6,9 @@
#app.name=LÖVE for Android
app.application_id=com.theboisclub.pokemonred
app.orientation=portrait
# fullUser: allow every orientation the player's device permits (portrait and
# landscape), honouring their auto-rotate lock. Was "portrait" (locked).
app.orientation=fullUser
app.version_code=32
app.version_name=11.5a
+5
View File
@@ -62,10 +62,15 @@
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UTExportedTypeDeclarations</key>
<array>
+1 -1
View File
@@ -101,7 +101,7 @@ def set_prop(text, key, value):
text = re.sub(r"(?m)^app\.name_byte_array=.*\n?", "", text)
text = set_prop(text, "app.name", name)
text = set_prop(text, "app.application_id", app_id)
text = set_prop(text, "app.orientation", "portrait")
text = set_prop(text, "app.orientation", "fullUser")
if version:
text = set_prop(text, "app.version_name", version)
text = set_prop(text, "app.version_code", version_code)
+1 -1
View File
@@ -155,7 +155,7 @@ require_ios_libraries
apply_ios_branding() {
[ -f "$OVERLAY_PLIST" ] || fail "missing overlay plist: $OVERLAY_PLIST"
local dest="$XCODE_DIR/ios/love-ios.plist"
say "applying iOS branding (portrait-only Info.plist, display name)"
say "applying iOS branding (portrait + landscape Info.plist, display name)"
cp "$OVERLAY_PLIST" "$dest"
}
+33 -7
View File
@@ -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
+232
View File
@@ -0,0 +1,232 @@
-- 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.
--
-- PHYSFS_mount is exported by love's own binary. How ffi finds it differs
-- per platform: on macOS/Linux the symbol is in the default namespace, so
-- ffi.C resolves it; on Windows it lives in love.dll, which ffi.C does NOT
-- search, so love.dll is loaded explicitly with ffi.load("love"). Try the
-- default first, then love.
local physfsMountFn = nil
local function resolveMount()
if physfsMountFn ~= nil then return physfsMountFn end
physfsMountFn = false
local ok, ffi = pcall(require, "ffi")
if not ok then return physfsMountFn end
pcall(ffi.cdef,
"int PHYSFS_mount(const char *newDir, const char *mountPoint, int appendToPath);")
local libs = {
function() return ffi.C end,
function() return ffi.load("love") end,
}
for _, getlib in ipairs(libs) do
local okl, lib = pcall(getlib)
if okl and lib then
local oks, fn = pcall(function() return lib.PHYSFS_mount end)
if oks and fn then
physfsMountFn = function(d)
local okr, ret = pcall(fn, d, "", 1)
return okr and ret ~= 0
end
break
end
end
end
return physfsMountFn
end
local function mountReadable(dir)
local fn = resolveMount()
if not fn then return false end
return fn(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
+6 -6
View File
@@ -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
+5 -11
View File
@@ -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
+5 -4
View File
@@ -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
View File
@@ -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..."