Skip to content

Adopt mods dropped beside the game instead of ignoring them - #364

Merged
bryanthaboi merged 1 commit into
bryanthaboi:devfrom
johnjohto:mods-stray-folder-notice
Jul 29, 2026
Merged

Adopt mods dropped beside the game instead of ignoring them#364
bryanthaboi merged 1 commit into
bryanthaboi:devfrom
johnjohto:mods-stray-folder-notice

Conversation

@johnjohto

Copy link
Copy Markdown
Contributor

A mod unzipped next to the executable is invisible on an ordinary install, and nothing on screen says so.

love.filesystem looks for mods/ in the save directory, and — portable installs only — in the game folder, which CacheFs mounts. Without portable.txt that mount never happens, so a mod sitting beside gen1recomp.exe is never read. There is no error: the MODS panel simply comes up empty.

That is where players put mods, because it is where nearly every other game wants them. It is worse behind a launcher, where the install lives in a folder they never open and "the game's mods folder" is a guess from the start. I hit this supporting my own mods and it cost a real user a long back-and-forth before we found the files were twenty centimetres away in the wrong folder.

What this does

The mods panel now checks those folders before its first listing and copies anything valid into the tree the game really reads, naming what it took in the existing notice line. On open rather than behind a button, because the failure is one where nothing suggests there is anything to press.

Why it is safe to look

CacheFs.withMounted puts the folder on the read path at its own mount point, runs the scan, and takes it back off. While it is up the tree sits in its own corner of the namespace, and afterwards the read path is exactly as it was found — nothing in a stray folder can shadow a game file or change what the running game resolves. That 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 will not nag twice or duplicate anything. It deliberately leaves the loose folder where it is: deleting files outside the save directory on the player's behalf seemed well beyond what this should decide.

Shape

  • LauncherMods.pickStrays is pure, 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.
  • CacheFs's mount closure grew a mountPoint argument; every existing caller passes "" exactly as before.

Testing

tests/run_engine.lua 17/17 and tests/run_modkit.lua 9/9 on this branch, rebased on dev. New coverage in tests/engine/launcher_mods_tests.lua for the picker rules (installed ids skipped, duplicate ids across two folders, nil/malformed rows).

tests/run_tests.lua also reports 3 failures on my machine, all data.generated.audio missing — they reproduce identically on a clean dev, so they are a gap in my checkout rather than anything here.

Happy to move it behind a prompt or a button instead if auto-adopting on open is more than you want it doing unasked.

https://claude.ai/code/session_01JvEthuoNBPfxpvHUD9Pd4N

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
@bryanthaboi
bryanthaboi merged commit ff82956 into bryanthaboi:dev Jul 29, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants