Adopt mods dropped beside the game instead of ignoring them - #364
Merged
Conversation
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 file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A mod unzipped next to the executable is invisible on an ordinary install, and nothing on screen says so.
love.filesystemlooks formods/in the save directory, and — portable installs only — in the game folder, whichCacheFsmounts. Withoutportable.txtthat mount never happens, so a mod sitting besidegen1recomp.exeis 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.withMountedputs 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.pickStraysis pure, matching howderiveListandlocateRootare already split out, so the engine tier covers the rules without needing love.SaveData.gameFoldersis the olddetectPortablecandidate list lifted out unchanged — portable mode is just the case where one of those folders holds the marker.CacheFs's mount closure grew amountPointargument; every existing caller passes""exactly as before.Testing
tests/run_engine.lua17/17 andtests/run_modkit.lua9/9 on this branch, rebased ondev. New coverage intests/engine/launcher_mods_tests.luafor the picker rules (installed ids skipped, duplicate ids across two folders, nil/malformed rows).tests/run_tests.luaalso reports 3 failures on my machine, alldata.generated.audiomissing — they reproduce identically on a cleandev, 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