Skip to content

Commit

Permalink
Merge pull request #1583 from ebkr/new-melonloader
Browse files Browse the repository at this point in the history
Add RecursiveMelonLoaderInstaller
  • Loading branch information
anttimaki authored Feb 19, 2025
2 parents eb4dd72 + b73c3fe commit 11279f2
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 5 deletions.
138 changes: 138 additions & 0 deletions src/installers/RecursiveMelonLoaderInstaller.ts
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');
}
}
}
3 changes: 3 additions & 0 deletions src/installers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { LovelyInstaller, LovelyPluginInstaller } from './LovelyInstaller';
import { NorthstarInstaller } from './NorthstarInstaller';
import { ReturnOfModdingInstaller, ReturnOfModdingPluginInstaller } from './ReturnOfModdingInstaller';
import { GDWeaveInstaller, GDWeavePluginInstaller } from './GDWeaveInstaller';
import { RecursiveMelonLoaderInstaller, RecursiveMelonLoaderPluginInstaller } from './RecursiveMelonLoaderInstaller';

const _PackageInstallers = {
// "legacy": new InstallRuleInstaller(), // TODO: Enable
Expand All @@ -22,6 +23,8 @@ const _PackageInstallers = {
"returnofmodding-plugin": new ReturnOfModdingPluginInstaller(),
"gdweave": new GDWeaveInstaller(),
"gdweave-plugin": new GDWeavePluginInstaller(),
"recursive-melonloader": new RecursiveMelonLoaderInstaller(),
"recursive-melonloader-plugin": new RecursiveMelonLoaderPluginInstaller(),
}

export type PackageInstallerId = keyof typeof _PackageInstallers;
Expand Down
3 changes: 3 additions & 0 deletions src/model/installing/PackageLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export enum PackageLoader {
LOVELY,
RETURN_OF_MODDING,
GDWEAVE,
RECURSIVE_MELON_LOADER,
}

export function GetInstallerIdForLoader(loader: PackageLoader): PackageInstallerId | null {
Expand All @@ -24,6 +25,7 @@ export function GetInstallerIdForLoader(loader: PackageLoader): PackageInstaller
case PackageLoader.LOVELY: return "lovely";
case PackageLoader.RETURN_OF_MODDING: return "returnofmodding";
case PackageLoader.GDWEAVE: return "gdweave";
case PackageLoader.RECURSIVE_MELON_LOADER: return "recursive-melonloader";
case PackageLoader.ANCIENT_DUNGEON_VR: return null;
}
}
Expand All @@ -34,6 +36,7 @@ export function GetInstallerIdForPlugin(loader: PackageLoader): PackageInstaller
case PackageLoader.LOVELY: return "lovely-plugin";
case PackageLoader.RETURN_OF_MODDING: return "returnofmodding-plugin";
case PackageLoader.GDWEAVE: return "gdweave-plugin";
case PackageLoader.RECURSIVE_MELON_LOADER: return "recursive-melonloader-plugin";
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ function buildRunners(runners: PlatformRunnersType): LoaderRunnersType {
[PackageLoader.LOVELY]: runners,
[PackageLoader.RETURN_OF_MODDING]: runners,
[PackageLoader.GDWEAVE]: runners,
[PackageLoader.RECURSIVE_MELON_LOADER]: runners,
}
}

Expand Down
11 changes: 6 additions & 5 deletions src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ export const MODLOADER_PACKAGES = [
new ModLoaderPackageMapping("BepInEx-BepInExPack_AmongUs", "BepInExPack_AmongUs", PackageLoader.BEPINEX),
];

const DEFAULT_MELONLOADER_MAPPING = [new ModLoaderPackageMapping("LavaGang-MelonLoader", "", PackageLoader.MELON_LOADER)];
const LEGACY_MELONLOADER_MAPPING = [new ModLoaderPackageMapping("LavaGang-MelonLoader", "", PackageLoader.MELON_LOADER)];
const RECURSIVE_MELONLOADER_MAPPING = [new ModLoaderPackageMapping("LavaGang-MelonLoader", "", PackageLoader.RECURSIVE_MELON_LOADER)];

/**
* While this object is a bit silly given that all the keys are pointing to the
Expand Down Expand Up @@ -115,11 +116,11 @@ const VARIANTS = {
Titanfall2: MODLOADER_PACKAGES,
Peglin: MODLOADER_PACKAGES,
VRising: MODLOADER_PACKAGES,
HardBullet: DEFAULT_MELONLOADER_MAPPING,
HardBullet: LEGACY_MELONLOADER_MAPPING,
GreenHellVR: MODLOADER_PACKAGES,
"20MinutesTillDawn": MODLOADER_PACKAGES,
VTOL_VR: MODLOADER_PACKAGES,
BackpackHero: DEFAULT_MELONLOADER_MAPPING,
BackpackHero: LEGACY_MELONLOADER_MAPPING,
Stacklands: MODLOADER_PACKAGES,
ETG: MODLOADER_PACKAGES,
Ravenfield: MODLOADER_PACKAGES,
Expand All @@ -135,15 +136,15 @@ const VARIANTS = {
AtrioTheDarkWild: MODLOADER_PACKAGES,
AncientDungeonVR: MODLOADER_PACKAGES,
Brotato: MODLOADER_PACKAGES,
RUMBLE: DEFAULT_MELONLOADER_MAPPING,
RUMBLE: LEGACY_MELONLOADER_MAPPING,
DomeKeeper: MODLOADER_PACKAGES,
SkulTheHeroSlayer: MODLOADER_PACKAGES,
SonsOfTheForest: MODLOADER_PACKAGES,
TheOuroborosKing: MODLOADER_PACKAGES,
WrestlingEmpire: MODLOADER_PACKAGES,
Receiver2: MODLOADER_PACKAGES,
ThePlanetCrafter: MODLOADER_PACKAGES,
PatchQuest: DEFAULT_MELONLOADER_MAPPING,
PatchQuest: LEGACY_MELONLOADER_MAPPING,
ShadowsOverLoathing: MODLOADER_PACKAGES,
WestofLoathing: MODLOADER_PACKAGES,
SunHaven: MODLOADER_PACKAGES,
Expand Down
1 change: 1 addition & 0 deletions src/r2mm/launching/instructions/GameInstructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default class GameInstructions {
public static LOADER_INSTRUCTIONS: Map<PackageLoader, GameInstructionGenerator> = new Map([
[PackageLoader.BEPINEX, new BepInExGameInstructions()],
[PackageLoader.MELON_LOADER, new MelonLoaderGameInstructions()],
[PackageLoader.RECURSIVE_MELON_LOADER, new MelonLoaderGameInstructions()],
[PackageLoader.NORTHSTAR, new NorthstarGameInstructions()],
[PackageLoader.GODOT_ML, new GodotMLGameInstructions()],
[PackageLoader.ANCIENT_DUNGEON_VR, new AncientVRGameInstructions()],
Expand Down

0 comments on commit 11279f2

Please sign in to comment.