mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 11:11:10 +02:00
Adopt mods dropped beside the game instead of ignoring them
love.filesystem looks for "mods/" in two places: the save directory, and -- portable installs only -- the game folder, which CacheFs mounts. So a player who unzips a mod next to the executable of an ordinary install, which is where very nearly every other game would want it, gets no error and no mod. The panel just comes up empty, with nothing on screen to suggest the files are sitting in the wrong folder twenty centimetres away. That is a hard failure to self-diagnose, and it is worse behind a launcher: the install lives somewhere the player never opens, so "the game's mods folder" is a guess to begin with. The mods panel now looks in those folders before its first listing and copies what it finds into the tree the game really reads, reporting what it took in the notice line. It happens on open rather than behind a button because the failure being fixed is one where nothing suggests there is anything to press. Looking is scoped: CacheFs.withMounted puts the folder on the read path at its own mount point, runs the scan, and takes it straight back off. Nothing a stray folder contains can shadow a game file or change what the running game resolves, which is what makes it safe to point at a folder whose contents nobody has validated. Adoption skips ids the game can already see, so it is idempotent and never nags twice, and it leaves the loose folder alone -- deleting files outside the save directory on the player's behalf is not this code's call to make. Which strays are worth taking is pure (LauncherMods.pickStrays), matching how deriveList and locateRoot are already split out, so the engine tier covers the rules without needing love. SaveData.gameFolders is the old detectPortable candidate list lifted out unchanged -- portable mode is just the case where one of those folders holds the marker. Claude-Session: https://claude.ai/code/session_01JvEthuoNBPfxpvHUD9Pd4N
This commit is contained in:
+30
-3
@@ -126,9 +126,9 @@ local function resolveMount()
|
||||
if okl and lib then
|
||||
local oks, fn = pcall(function() return lib.PHYSFS_mount end)
|
||||
if oks and fn then
|
||||
physfsMountFn = function(d, append)
|
||||
physfsMountFn = function(d, mountPoint, append)
|
||||
if append == nil then append = true end
|
||||
local okr, ret = pcall(fn, d, "", append and 1 or 0)
|
||||
local okr, ret = pcall(fn, d, mountPoint or "", append and 1 or 0)
|
||||
return okr and ret ~= 0
|
||||
end
|
||||
break
|
||||
@@ -145,7 +145,7 @@ end
|
||||
local function mountReadable(dir, append)
|
||||
local fn = resolveMount()
|
||||
if not fn then return false end
|
||||
return fn(dir, append)
|
||||
return fn(dir, "", append)
|
||||
end
|
||||
|
||||
-- PHYSFS_unmount, resolved the same way PHYSFS_mount is. Only
|
||||
@@ -385,4 +385,31 @@ function CacheFs.unmountVersion(version)
|
||||
return done
|
||||
end
|
||||
|
||||
-- Mount `dir` at `mountPoint` for the length of `fn()`, then take it back off
|
||||
-- the read path and hand back whatever fn returned.
|
||||
--
|
||||
-- Every other mount here is permanent and lands at the physfs root: this one
|
||||
-- exists to *look* at a folder the game has deliberately not mounted, which
|
||||
-- is a different job. The mods panel uses it to read a mods/ folder sitting
|
||||
-- beside the executable of a non-portable install (LauncherMods.strays).
|
||||
-- Because it unmounts again, and because a non-empty mountPoint keeps the
|
||||
-- tree in its own corner of the namespace while it is up, a folder inspected
|
||||
-- this way can never shadow a game file or change what the running game
|
||||
-- resolves -- which is what makes it safe to point at a folder whose contents
|
||||
-- nobody has validated.
|
||||
--
|
||||
-- Returns nil when the mount is unavailable (no ffi, no PHYSFS symbol, or the
|
||||
-- mount was refused), which callers must treat as "could not look", not as
|
||||
-- "nothing there". An error inside fn still unmounts before it propagates.
|
||||
function CacheFs.withMounted(dir, mountPoint, fn)
|
||||
if not dir or dir == "" then return nil end
|
||||
local mount, unmount = resolveMount(), resolveUnmount()
|
||||
if not (mount and unmount) then return nil end
|
||||
if not mount(dir, mountPoint, true) then return nil end
|
||||
local ok, res = pcall(fn)
|
||||
unmount(dir)
|
||||
if not ok then error(res, 0) end
|
||||
return res
|
||||
end
|
||||
|
||||
return CacheFs
|
||||
|
||||
@@ -2697,6 +2697,30 @@ end
|
||||
-- so a still list costs nothing after the first paint.
|
||||
function RomImporter:_refreshMods()
|
||||
local LauncherMods = require("src.mods.LauncherMods")
|
||||
-- Once per session, ahead of the first listing: pull in any mod the player
|
||||
-- unzipped beside the executable, which an ordinary (non-portable) install
|
||||
-- has no way to read. It happens here rather than behind a button because
|
||||
-- the failure being fixed is one where nothing on screen suggests there is
|
||||
-- anything to press -- the panel just comes up empty. Guarded so a toggle
|
||||
-- or a delete does not re-scan; adoptStrays is idempotent regardless.
|
||||
if not self.modStraysChecked then
|
||||
self.modStraysChecked = true
|
||||
local imported, failed = {}, {}
|
||||
for _, s in ipairs(LauncherMods.adoptStrays() or {}) do
|
||||
table.insert(s.err and failed or imported, s.id)
|
||||
end
|
||||
-- the failure wins the notice: an import that worked speaks for itself in
|
||||
-- the list right below it, one that did not is the only word they get
|
||||
if #imported > 0 then
|
||||
self.modNotice = { ok = true,
|
||||
text = "Imported from the game folder: " .. table.concat(imported, ", ") }
|
||||
end
|
||||
if #failed > 0 then
|
||||
self.modNotice = { ok = false,
|
||||
text = "Found beside the game but could not import: "
|
||||
.. table.concat(failed, ", ") }
|
||||
end
|
||||
end
|
||||
self.mods = LauncherMods.list() or {}
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user