Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 33 additions & 4 deletions src/installers/InstallRulePluginInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import ModFileTracker from "../model/installing/ModFileTracker";
import ConflictManagementProvider from "../providers/generic/installing/ConflictManagementProvider";
import PathResolver from "../r2mm/manager/PathResolver";
import ZipProvider from "../providers/generic/zip/ZipProvider";
import { TrackingMethod } from "../model/schema/ThunderstoreSchema";
import { EcosystemSchema, TrackingMethod } from "../model/schema/ThunderstoreSchema";
import ModMode from "../model/enums/ModMode";
import GameManager from "../model/game/GameManager";

type InstallRuleArgs = {
profile: ImmutableProfile,
Expand Down Expand Up @@ -362,10 +363,38 @@ export async function applyModeState(mod: ManifestV2, profile: ImmutableProfile,
}

export class InstallRulePluginInstaller implements PackageInstaller {
public readonly rule: CoreRuleType;
private ruleOverride: CoreRuleType|undefined;

constructor(rules: CoreRuleType) {
this.rule = rules;
/**
* @param ruleOverride can be used to ignore installation rules provided by
* Thunderstore ecosystem.
*
* This can be used e.g. in PackageInstaller implementations or test cases.
* Note that the override last the instance's whole lifetime, causing it to
* ignore changes in the active game. Therefore it shouldn't be used in the
* shared instance initiated in registry.ts.
*/
constructor(ruleOverride?: CoreRuleType) {
this.ruleOverride = ruleOverride;
}

private get rule(): CoreRuleType {
if (this.ruleOverride !== undefined) {
return this.ruleOverride;
}

// While it's not ideal that this same method is called repeatedly,
// the code path below currently takes <1ms to execute so we should be fine.
const gameConfig = EcosystemSchema.getGameConfigBySettingsIdentifier(GameManager.activeGame.settingsIdentifier);
if (gameConfig === undefined) {
throw new Error(`Game config not found for ${GameManager.activeGame.settingsIdentifier}`);
}

return {
gameName: gameConfig.internalFolderName,
rules: gameConfig.installRules,
relativeFileExclusions: gameConfig.relativeFileExclusions
};
}

/**
Expand Down
12 changes: 11 additions & 1 deletion src/installers/PackageInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import GameManager from "../model/game/GameManager";
import { ImmutableProfile } from "../model/Profile";
import ManifestV2 from "../model/ManifestV2";
import FsProvider from "../providers/generic/file/FsProvider";
import path from "../providers/node/path/path";
import { MOD_LOADER_VARIANTS } from "../r2mm/installing/profile_installers/ModLoaderVariantRecord";
import PathResolver from "../r2mm/manager/PathResolver";

export type InstallArgs = {
mod: ManifestV2;
Expand All @@ -15,7 +17,9 @@ export type InstallArgs = {

export interface PackageInstaller {
install(args: InstallArgs): Promise<void>;
uninstall?(args: InstallArgs): Promise<void>;
uninstall(args: InstallArgs): Promise<void>;

// Plugin installers only.
enable?(args: InstallArgs): Promise<void>;
disable?(args: InstallArgs): Promise<void>;
}
Expand Down Expand Up @@ -49,6 +53,12 @@ export async function enableModByRenamingFiles(folderName: string) {
}
}

export function getInstallArgs(mod: ManifestV2, profile: ImmutableProfile): InstallArgs {
const cacheDirectory = path.join(PathResolver.MOD_ROOT, "cache");
const packagePath = path.join(cacheDirectory, mod.getName(), mod.getVersionNumber().toString());
return {mod, profile, packagePath};
}

// Implementation shared by BepInExInstaller, MelonLoaderInstaller and others.
export async function uninstallModLoader(mod: ManifestV2, profile: ImmutableProfile) {
const fs = FsProvider.instance;
Expand Down
47 changes: 10 additions & 37 deletions src/installers/registry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BepInExInstaller } from './BepInExInstaller';
import { GodotMLInstaller } from './GodotMLInstaller';
import { InstallRulePluginInstaller } from './InstallRulePluginInstaller';
import { MelonLoaderInstaller } from './MelonLoaderInstaller';
import { NoOpInstaller } from './NoOpInstaller';
import { PackageInstaller } from './PackageInstaller';
Expand All @@ -15,10 +16,6 @@ import { UMMInstaller } from './UMMInstaller';
import { RivetInstaller, RivetPluginInstaller } from './RivetInstaller';
import { PackageLoader } from '../model/schema/ThunderstoreSchema';

/**
* Package loader installer registry
*/

export const PackageLoaderInstallers: Record<PackageLoader, PackageInstaller> = {
[PackageLoader.BEPINEX]: new BepInExInstaller(),
[PackageLoader.BEPISLOADER]: new BepisLoaderInstaller(),
Expand All @@ -35,44 +32,20 @@ export const PackageLoaderInstallers: Record<PackageLoader, PackageInstaller> =
[PackageLoader.UMM]: new UMMInstaller(),
};

/**
* Plugin installer registry
*/
type InstallRuleInstallers =
| PackageLoader.BEPINEX
| PackageLoader.BEPISLOADER
| PackageLoader.GODOTML
| PackageLoader.MELONLOADER
| PackageLoader.NORTHSTAR
| PackageLoader.SHIMLOADER
| PackageLoader.UMM;
type PluginInstallers = Exclude<PackageLoader, InstallRuleInstallers>;
const installRulePluginInstaller = new InstallRulePluginInstaller();

const PluginInstallers: Record<PluginInstallers, PackageInstaller> = {
export const PluginInstallers: Record<PackageLoader, PackageInstaller> = {
[PackageLoader.BEPINEX]: installRulePluginInstaller,
[PackageLoader.BEPISLOADER]: installRulePluginInstaller,
[PackageLoader.GDWEAVE]: new GDWeavePluginInstaller(),
[PackageLoader.GODOTML]: installRulePluginInstaller,
[PackageLoader.LOVELY]: new LovelyPluginInstaller(),
[PackageLoader.MELONLOADER]: installRulePluginInstaller,
[PackageLoader.NORTHSTAR]: installRulePluginInstaller,
[PackageLoader.NONE]: new DirectCopyInstaller(),
[PackageLoader.RECURSIVE_MELONLOADER]: new RecursiveMelonLoaderPluginInstaller(),
[PackageLoader.RETURN_OF_MODDING]: new ReturnOfModdingPluginInstaller(),
[PackageLoader.RIVET]: new RivetPluginInstaller(),
[PackageLoader.SHIMLOADER]: installRulePluginInstaller,
[PackageLoader.UMM]: installRulePluginInstaller,
};

function isPluginInstaller(loader: PackageLoader): loader is PluginInstallers {
return !(
loader === PackageLoader.BEPINEX ||
loader === PackageLoader.BEPISLOADER ||
loader === PackageLoader.GODOTML ||
loader === PackageLoader.MELONLOADER ||
loader === PackageLoader.NORTHSTAR ||
loader === PackageLoader.SHIMLOADER ||
loader === PackageLoader.UMM
);
}

export function getPluginInstaller(loader: PackageLoader): PackageInstaller|null {
if (!isPluginInstaller(loader)) {
return null;
}

return PluginInstallers[loader];
}
14 changes: 11 additions & 3 deletions src/model/schema/ThunderstoreSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Ajv from "ajv";
import addFormats from "ajv-formats";

import ecosystem from "../../assets/data/ecosystem.json";
import { R2Modman, ThunderstoreEcosystem } from "../../assets/data/ecosystemTypes";
import { R2Modman as GameConfig, ThunderstoreEcosystem } from "../../assets/data/ecosystemTypes";
import jsonSchema from "../../assets/data/ecosystemJsonSchema.json";
import R2Error from "../errors/R2Error";

Expand Down Expand Up @@ -43,10 +43,10 @@ export class EcosystemSchema {
}

/**
* Get a list of [identifier, r2modman] entries i.e. games supported by the mod manager.
* Get a list of [identifier, GameConfig] entries i.e. games supported by the mod manager.
*/
static get supportedGames() {
const result: [string, R2Modman][] = []
const result: [string, GameConfig][] = []
for (const [identifier, game] of Object.entries(this.ecosystem.games)) {
if (game.r2modman == null) continue;
for (const entry of game.r2modman) {
Expand All @@ -59,4 +59,12 @@ export class EcosystemSchema {
static get modloaderPackages() {
return this.ecosystem.modloaderPackages;
}

static getGameConfigBySettingsIdentifier(settingsIdentifier: string): GameConfig | undefined {
const config = this.supportedGames.find(
([_id, config]) => config.settingsIdentifier === settingsIdentifier
);

return config ? config[1] : undefined;
}
}
26 changes: 17 additions & 9 deletions src/r2mm/installing/InstallationRules.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { getPluginInstaller } from '../../installers/registry';
import GameManager from '../../model/game/GameManager';
import { EcosystemSchema, TrackingMethod } from '../../model/schema/ThunderstoreSchema';
import path from '../../providers/node/path/path';

Expand Down Expand Up @@ -47,13 +45,23 @@ export default class InstallationRules {
}

public static validate() {
GameManager.gameList.forEach(value => {
if (this._RULES.find(rule => rule.gameName === value.internalFolderName) === undefined) {
if (getPluginInstaller(value.packageLoader) === null) {
throw new Error(`Missing installation rule for game: ${value.internalFolderName}`);
}
}
})
// Initially this used to test that each game has InstallationRules defined.
// It was later changed to ignore games that have PackagaInstaller support.
// Now PackageInstallers are all there is, and the install rules are defined
// by Thunderstore, so this validation makes little sense.
// If we'd want to make some sanity check here, it would be to validate that
// all PackageLoader types defined by the ecosystem are mapped to mod loader
// and plugin installation implemenations in registry.ts. This probably ins't
// the correct place for that though. This well be cleaned in future commit
// to avoid scope creep within the current commit.

// GameManager.gameList.forEach(value => {
// if (this._RULES.find(rule => rule.gameName === value.internalFolderName) === undefined) {
// if (getPluginInstaller(value.packageLoader) === null) {
// throw new Error(`Missing installation rule for game: ${value.internalFolderName}`);
// }
// }
// })
}

/**
Expand Down
Loading
Loading