-
Notifications
You must be signed in to change notification settings - Fork 208
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
MelonLoader has added support for recursive loading. This means mods, plugins and userLibs can all be placed in the same folder inside the profile folder instead of needing to copy each type to a specific subfolder. This in turn means we can namespace each installed mod into its own subfolder, which makes uninstalling the mods straightforward and removes the need to track installation state on a separate file. The mod author still needs to place plugins and userLibs to specific folders in their Thunderstore packages though. In the profile, package's UserData folder (if any) gets copied to profile/UserData/[packageName] and everything else gets copied to profile/Mods/[packageName]. MelonLoader handles the rest. This will require MelonLoader v0.7.0 or newer to work.
- Loading branch information
Showing
6 changed files
with
152 additions
and
5 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import path from 'path'; | ||
|
||
import { | ||
disableModByRenamingFiles, | ||
enableModByRenamingFiles, | ||
InstallArgs, | ||
PackageInstaller | ||
} from './PackageInstaller'; | ||
import FileWriteError from '../model/errors/FileWriteError'; | ||
import R2Error from '../model/errors/R2Error'; | ||
import VersionNumber from '../model/VersionNumber'; | ||
import FsProvider from '../providers/generic/file/FsProvider'; | ||
import FileUtils from '../utils/FileUtils'; | ||
|
||
/** | ||
* Handles (un)installation of MelonLoader v0.7.0 and above. | ||
*/ | ||
export class RecursiveMelonLoaderInstaller implements PackageInstaller { | ||
private static readonly TRACKED = ['MelonLoader', 'version.dll']; | ||
|
||
async install(args: InstallArgs): Promise<void> { | ||
const { mod, packagePath, profile } = args; | ||
|
||
// The RecursiveMelonLoaderPluginInstaller places mod files into subfolders in | ||
// the profile folder. This is only supported on MelonLoader v0.7.0 and above. | ||
// Therefore a game that uses RecursiveMelonLoaderPluginInstaller should always | ||
// also use RecursiveMelonLoaderInstaller, which checks the mod loader version | ||
// before installing. | ||
if (!mod.getVersionNumber().isEqualOrNewerThan(new VersionNumber('0.7.0'))) { | ||
throw new R2Error( | ||
'MelonLoader v0.7.0 or above is required', | ||
`Choose a newer MelonLoader version to install. If newer versions | ||
are not available, this is likely a bug in the mod manager.` | ||
); | ||
} | ||
|
||
try { | ||
for (const fileOrFolder of RecursiveMelonLoaderInstaller.TRACKED) { | ||
const cachePath = path.join(packagePath, fileOrFolder); | ||
const profilePath = profile.joinToProfilePath(fileOrFolder); | ||
await FileUtils.copyFileOrFolder(cachePath, profilePath); | ||
} | ||
} catch (e) { | ||
throw FileWriteError.fromThrownValue(e, 'Failed to install MelonLoader'); | ||
} | ||
} | ||
|
||
async uninstall(args: InstallArgs): Promise<void> { | ||
try { | ||
for (const fileOrFolder of RecursiveMelonLoaderInstaller.TRACKED) { | ||
const fullPath = args.profile.joinToProfilePath(fileOrFolder); | ||
|
||
if (!(await FsProvider.instance.exists(fullPath))) { | ||
continue; | ||
} | ||
|
||
if ((await FsProvider.instance.stat(fullPath)).isDirectory()) { | ||
await FileUtils.recursiveRemoveDirectoryIfExists(fullPath); | ||
} else { | ||
await FsProvider.instance.unlink(fullPath); | ||
} | ||
} | ||
} catch (e) { | ||
throw FileWriteError.fromThrownValue(e, 'Failed to uninstall MelonLoader', 'Is the game still running?'); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Handles mod operations in a RecursiveMelonLoaderInstaller compatible way. | ||
*/ | ||
export class RecursiveMelonLoaderPluginInstaller implements PackageInstaller { | ||
private getModsPath(args: InstallArgs): string { | ||
return args.profile.joinToProfilePath('Mods', args.mod.getName()); | ||
} | ||
|
||
private getUserDataPath(args: InstallArgs): string { | ||
return args.profile.joinToProfilePath('UserData', args.mod.getName()); | ||
} | ||
|
||
private throwActionError(e: unknown, action: string): void { | ||
const name = `Failed to ${action} mod`; | ||
const solution = 'Is the game still running?'; | ||
throw FileWriteError.fromThrownValue(e, name, solution); | ||
} | ||
|
||
/** | ||
* Copy UserData as-is to UserData/<PackageName>/ and everything else | ||
* to Mods/<PackageName>/ | ||
*/ | ||
async install(args: InstallArgs): Promise<void> { | ||
const files = await FsProvider.instance.readdir(args.packagePath); | ||
const modsPath = this.getModsPath(args); | ||
await FileUtils.ensureDirectory(modsPath); | ||
|
||
try { | ||
for (const item of files) { | ||
const sourceFull = path.join(args.packagePath, item); | ||
|
||
if (item === 'UserData') { | ||
const userDataPath = this.getUserDataPath(args); | ||
await FileUtils.ensureDirectory(userDataPath); | ||
await FileUtils.copyFileOrFolder(sourceFull, userDataPath); | ||
} else { | ||
const targetPath = path.join(modsPath, item); | ||
await FileUtils.copyFileOrFolder(sourceFull, targetPath); | ||
} | ||
} | ||
} catch (e) { | ||
this.throwActionError(e, 'install'); | ||
} | ||
} | ||
|
||
async uninstall(args: InstallArgs): Promise<void> { | ||
try { | ||
FileUtils.recursiveRemoveDirectoryIfExists(this.getModsPath(args)); | ||
FileUtils.recursiveRemoveDirectoryIfExists(this.getUserDataPath(args)); | ||
} catch (e) { | ||
this.throwActionError(e, 'uninstall'); | ||
} | ||
} | ||
|
||
async disable(args: InstallArgs): Promise<void> { | ||
try { | ||
await disableModByRenamingFiles(this.getModsPath(args)); | ||
} catch (e) { | ||
this.throwActionError(e, 'disable'); | ||
} | ||
} | ||
|
||
async enable(args: InstallArgs): Promise<void> { | ||
try { | ||
await enableModByRenamingFiles(this.getModsPath(args)); | ||
} catch (e) { | ||
this.throwActionError(e, 'enable'); | ||
} | ||
} | ||
} |
This file contains 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
This file contains 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
This file contains 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
This file contains 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
This file contains 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