From 8c007dc8fac33565d8b9b653df2bb343fac9ed5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Thu, 11 Sep 2025 15:45:09 +0300 Subject: [PATCH 01/10] Drop InstallationRules.ts - Keeping installation rules separately in memory is redundant as the InstallRulePluginInstaller reads them for EcosystemSchema now - The code in InstallationRules class are used by the same installer class, so placing them in the same file makes sense - Ingnored the comment about a new implementation for InstallationRules.validate as type safety will give an error if each PackageLoader doesn't define mod loader and plugin installers in the registry file --- src/App.vue | 4 - src/installers/InstallRulePluginInstaller.ts | 89 ++++++++++++- src/r2mm/installing/InstallationRules.ts | 121 ------------------ .../ModLoader/Installer.Tests.spec.ts | 6 +- .../ModLoader/Shimloader.Tests.spec.ts | 3 +- test/vitest/utils/InstallLogicUtils.ts | 2 - 6 files changed, 86 insertions(+), 139 deletions(-) delete mode 100644 src/r2mm/installing/InstallationRules.ts diff --git a/src/App.vue b/src/App.vue index d5049e349..caa216de5 100644 --- a/src/App.vue +++ b/src/App.vue @@ -46,7 +46,6 @@ import BindLoaderImpl from './providers/components/loaders/bind_impls/BindLoader import PlatformInterceptorProvider from './providers/generic/game/platform_interceptor/PlatformInterceptorProvider'; import PlatformInterceptorImpl from './providers/generic/game/platform_interceptor/PlatformInterceptorImpl'; import ProfileInstallerProvider from './providers/ror2/installing/ProfileInstallerProvider'; -import InstallationRules from './r2mm/installing/InstallationRules'; import GenericProfileInstaller from './r2mm/installing/profile_installers/GenericProfileInstaller'; import ErrorModal from './components/modals/ErrorModal.vue'; import { provideStoreImplementation } from './providers/generic/store/StoreProvider'; @@ -111,9 +110,6 @@ onMounted(async () => { hookModInstallingViaProtocol(router); await checkCdnConnection(); - InstallationRules.apply(); - InstallationRules.validate(); - window.app.getAppDataDirectory().then(async (appData: string) => { PathResolver.APPDATA_DIR = path.join(appData, 'r2modmanPlus-local'); // Legacy path. Needed for migration. diff --git a/src/installers/InstallRulePluginInstaller.ts b/src/installers/InstallRulePluginInstaller.ts index e91fe7745..fe7914158 100644 --- a/src/installers/InstallRulePluginInstaller.ts +++ b/src/installers/InstallRulePluginInstaller.ts @@ -5,7 +5,6 @@ import path from "../providers/node/path/path"; import ManifestV2 from "../model/ManifestV2"; import R2Error, { throwForR2Error } from "../model/errors/R2Error"; import FileTree from "../model/file/FileTree"; -import InstallationRules, { CoreRuleType, ManagedRule, RuleSubtype } from "../r2mm/installing/InstallationRules"; import FileUtils from "../utils/FileUtils"; import yaml from "yaml"; import ModFileTracker from "../model/installing/ModFileTracker"; @@ -16,6 +15,29 @@ import { EcosystemSchema, TrackingMethod } from "../model/schema/ThunderstoreSch import ModMode from "../model/enums/ModMode"; import GameManager from "../model/game/GameManager"; +type CoreRuleType = { + gameName: string, + rules: RuleSubtype[], + relativeFileExclusions: string[] | null, +} + + +type RuleSubtype = { + route: string, + trackingMethod: TrackingMethod, + subRoutes: RuleSubtype[], + defaultFileExtensions: string[], + isDefaultLocation?: boolean +} + +type ManagedRule = { + route: string, + ref: RuleSubtype, + trackingMethod: TrackingMethod, + extensions: string[], + isDefaultLocation: boolean +} + type InstallRuleArgs = { profile: ImmutableProfile, coreRule: CoreRuleType, @@ -24,6 +46,60 @@ type InstallRuleArgs = { mod: ManifestV2, }; +/** + * Produce a flattened structure of all navigable paths maintained by the install rules. + * @param rules + * @param pathBuilder + */ +export function getAllManagedPaths(rules: RuleSubtype[], pathBuilder?: string): ManagedRule[] { + const paths: ManagedRule[] = []; + rules.forEach(value => { + const route = !pathBuilder ? value.route : path.join(pathBuilder, value.route); + paths.push({ + route: route, + trackingMethod: value.trackingMethod, + extensions: value.defaultFileExtensions, + isDefaultLocation: value.isDefaultLocation || false, + ref: value + }); + getAllManagedPaths(value.subRoutes, route).forEach(x => paths.push(x)); + }); + return paths; +} + +function getRuleSubtypeFromManagedRule(managedRule: ManagedRule, rule: CoreRuleType): RuleSubtype { + for (const value of rule.rules) { + if (value.route === managedRule.route) { + return value; + } else { + const nested = getRuleSubtypeFromManagedRuleInner(managedRule, value, value.route); + if (nested !== undefined) { + return nested; + } + } + } + + throw new Error("RuleSubtype does not exist for ManagedRule."); +} + +function getRuleSubtypeFromManagedRuleInner(managedRule: ManagedRule, subType: RuleSubtype, realRoute: string): RuleSubtype | undefined { + if (realRoute === managedRule.route) { + return subType; + } else { + for (const subRoute of subType.subRoutes) { + const nested = getRuleSubtypeFromManagedRuleInner(managedRule, subRoute, path.join(realRoute, subRoute.route)); + if (nested !== undefined) { + return nested; + } + } + } + return; +} + +function getManagedRuleForSubtype(rule: CoreRuleType, subType: RuleSubtype): ManagedRule { + return getAllManagedPaths(rule.rules).find(value => getRuleSubtypeFromManagedRule(value, rule) === subType)!; +} + async function installUntracked(profile: ImmutableProfile, rule: ManagedRule, installSources: string[], mod: ManifestV2) { // Functionally identical to the install method of subdir, minus the subdirectory. @@ -158,7 +234,7 @@ async function buildInstallForRuleSubtype( mod: ManifestV2, tree: FileTree ): Promise> { - const flatRules = InstallationRules.getAllManagedPaths(rule.rules); + const flatRules = getAllManagedPaths(rule.rules); const installationIntent = new Map(); for (const file of tree.getFiles()) { // Find matching rule for file based on extension name. @@ -181,7 +257,7 @@ async function buildInstallForRuleSubtype( if (matchingRule === undefined) { continue; } - const subType = InstallationRules.getRuleSubtypeFromManagedRule(matchingRule, rule); + const subType = getRuleSubtypeFromManagedRule(matchingRule, rule); const updatedArray = installationIntent.get(subType) || []; updatedArray.push(file); installationIntent.set(subType, updatedArray); @@ -202,7 +278,7 @@ async function buildInstallForRuleSubtype( installationIntent.set(rule, arr); } } else { - const subType = InstallationRules.getRuleSubtypeFromManagedRule(matchingRule, rule); + const subType = getRuleSubtypeFromManagedRule(matchingRule, rule); const arr = installationIntent.get(subType) || []; arr.push(file.getTarget()); installationIntent.set(subType, arr); @@ -318,7 +394,7 @@ export async function uninstallState(mod: ManifestV2, profile: ImmutableProfile) // Enables or disables a mod installed with InstallRulePluginInstaller using SUBDIR/SUBDIR_NO_FLATTEN tracking methods. async function applyModeSubDirs(mod: ManifestV2, profile: ImmutableProfile, mode: number, rule: CoreRuleType): Promise { - const subDirPaths = InstallationRules.getAllManagedPaths(rule.rules) + const subDirPaths = getAllManagedPaths(rule.rules) .filter(value => [TrackingMethod.SUBDIR, TrackingMethod.SUBDIR_NO_FLATTEN].includes(value.trackingMethod)); for (const dir of subDirPaths) { @@ -410,7 +486,8 @@ export class InstallRulePluginInstaller implements PackageInstaller { private async resolveFileTreeInstall(profile: ImmutableProfile, location: string, folderName: string, mod: ManifestV2, tree: FileTree) { const installationIntent = await buildInstallForRuleSubtype(this.rule, location, folderName, mod, tree); for (let [rule, files] of installationIntent.entries()) { - const managedRule = InstallationRules.getManagedRuleForSubtype(this.rule, rule); + const managedRule = getManagedRuleForSubtype(this.rule, rule); + const args: InstallRuleArgs = { profile, coreRule: this.rule, diff --git a/src/r2mm/installing/InstallationRules.ts b/src/r2mm/installing/InstallationRules.ts deleted file mode 100644 index ddfef5b0a..000000000 --- a/src/r2mm/installing/InstallationRules.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { EcosystemSchema, TrackingMethod } from '../../model/schema/ThunderstoreSchema'; -import path from '../../providers/node/path/path'; - -export type CoreRuleType = { - gameName: string, - rules: RuleSubtype[], - relativeFileExclusions: string[] | null, -} - - -export type RuleSubtype = { - route: string, - trackingMethod: TrackingMethod, - subRoutes: RuleSubtype[], - defaultFileExtensions: string[], - isDefaultLocation?: boolean -} - -export type ManagedRule = { - route: string, - ref: RuleSubtype, - trackingMethod: TrackingMethod, - extensions: string[], - isDefaultLocation: boolean -} - -export default class InstallationRules { - - private static _RULES: CoreRuleType[] = []; - - static get RULES(): CoreRuleType[] { - return [...this._RULES]; - } - - static set RULES(value: CoreRuleType[]) { - this._RULES = value; - } - - public static apply() { - this._RULES = EcosystemSchema.supportedGames.map(([_, x]) => ({ - gameName: x.internalFolderName, - rules: x.installRules, - relativeFileExclusions: x.relativeFileExclusions, - })); - } - - public static validate() { - // 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}`); - // } - // } - // }) - } - - /** - * Produce a flattened structure of all navigable paths maintained by the install rules. - * @param rules - * @param pathBuilder - */ - public static getAllManagedPaths(rules: RuleSubtype[], pathBuilder?: string): ManagedRule[] { - const paths: ManagedRule[] = []; - rules.forEach(value => { - const route = !pathBuilder ? value.route : path.join(pathBuilder, value.route); - paths.push({ - route: route, - trackingMethod: value.trackingMethod, - extensions: value.defaultFileExtensions, - isDefaultLocation: value.isDefaultLocation || false, - ref: value - }); - this.getAllManagedPaths(value.subRoutes, route).forEach(x => paths.push(x)); - }); - return paths; - } - - public static getRuleSubtypeFromManagedRule(managedRule: ManagedRule, rule: CoreRuleType): RuleSubtype { - for (const value of rule.rules) { - if (value.route === managedRule.route) { - return value; - } else { - const nested = this.getRuleSubtypeFromManagedRuleInner(managedRule, value, value.route); - if (nested !== undefined) { - return nested; - } - } - } - console.log("ManagedRule:", managedRule); - throw new Error("RuleSubtype does not exist for ManagedRule."); - } - - private static getRuleSubtypeFromManagedRuleInner(managedRule: ManagedRule, subType: RuleSubtype, realRoute: string): RuleSubtype | undefined { - if (realRoute === managedRule.route) { - return subType; - } else { - for (const subRoute of subType.subRoutes) { - const nested = this.getRuleSubtypeFromManagedRuleInner(managedRule, subRoute, path.join(realRoute, subRoute.route)); - if (nested !== undefined) { - return nested; - } - } - } - return; - } - - public static getManagedRuleForSubtype(rule: CoreRuleType, subType: RuleSubtype): ManagedRule { - return this.getAllManagedPaths(rule.rules).find(value => this.getRuleSubtypeFromManagedRule(value, rule) === subType)!; - } - -} diff --git a/test/vitest/tests/unit/Installers/ModLoader/Installer.Tests.spec.ts b/test/vitest/tests/unit/Installers/ModLoader/Installer.Tests.spec.ts index be35b2508..976f719be 100644 --- a/test/vitest/tests/unit/Installers/ModLoader/Installer.Tests.spec.ts +++ b/test/vitest/tests/unit/Installers/ModLoader/Installer.Tests.spec.ts @@ -5,13 +5,12 @@ import Profile from '../../../../../../src/model/Profile'; import ProfileInstallerProvider from '../../../../../../src/providers/ror2/installing/ProfileInstallerProvider'; import GameManager from '../../../../../../src/model/game/GameManager'; import GenericProfileInstaller from '../../../../../../src/r2mm/installing/profile_installers/GenericProfileInstaller'; -import InstallationRules from '../../../../../../src/r2mm/installing/InstallationRules'; import { createManifest, installLogicBeforeEach } from '../../../../utils/InstallLogicUtils'; import { EcosystemSchema, TrackingMethod } from '../../../../../../src/model/schema/ThunderstoreSchema'; import { describe, beforeEach, test, expect } from 'vitest'; import { providePathImplementation } from '../../../../../../src/providers/node/path/path'; import { TestPathProvider } from '../../../../stubs/providers/node/Node.Path.Provider'; -import { InstallRulePluginInstaller } from '../../../../../../src/installers/InstallRulePluginInstaller'; +import { getAllManagedPaths, InstallRulePluginInstaller } from '../../../../../../src/installers/InstallRulePluginInstaller'; import { getInstallArgs } from '../../../../../../src/installers/PackageInstaller'; describe('Installer Tests', () => { @@ -191,8 +190,7 @@ describe('Installer Tests', () => { throw new Error(`Game config not found for ${GameManager.activeGame.internalFolderName}`); } - const defaultRuleSubtype = InstallationRules.getAllManagedPaths(config.installRules) - .find(value => value.isDefaultLocation)!; + const defaultRuleSubtype = getAllManagedPaths(config.installRules).find(value => value.isDefaultLocation)!; // Expect DLL to be installed as intended expect(await FsProvider.instance.exists( diff --git a/test/vitest/tests/unit/Installers/ModLoader/Shimloader.Tests.spec.ts b/test/vitest/tests/unit/Installers/ModLoader/Shimloader.Tests.spec.ts index 380f00363..e6e7618fd 100644 --- a/test/vitest/tests/unit/Installers/ModLoader/Shimloader.Tests.spec.ts +++ b/test/vitest/tests/unit/Installers/ModLoader/Shimloader.Tests.spec.ts @@ -12,11 +12,10 @@ import Profile from '../../../../../../src/model/Profile'; import R2Error from '../../../../../../src/model/errors/R2Error'; import ProfileInstallerProvider from '../../../../../../src/providers/ror2/installing/ProfileInstallerProvider'; import GenericProfileInstaller from '../../../../../../src/r2mm/installing/profile_installers/GenericProfileInstaller'; -import { RuleSubtype } from '../../../../../../src/r2mm/installing/InstallationRules'; import { TrackingMethod } from '../../../../../../src/model/schema/ThunderstoreSchema'; import {describe, beforeEach, test, expect} from 'vitest'; -function getShimloaderRules(includePakExtension: boolean): RuleSubtype[] { +function getShimloaderRules(includePakExtension: boolean) { return [ { route: 'shimloader/mod', diff --git a/test/vitest/utils/InstallLogicUtils.ts b/test/vitest/utils/InstallLogicUtils.ts index b93023435..8633a44ac 100644 --- a/test/vitest/utils/InstallLogicUtils.ts +++ b/test/vitest/utils/InstallLogicUtils.ts @@ -13,7 +13,6 @@ import ConflictManagementProvider from '../../../src/providers/generic/installin import ProfileInstallerProvider from '../../../src/providers/ror2/installing/ProfileInstallerProvider'; import ProfileProvider from '../../../src/providers/ror2/model_implementation/ProfileProvider'; import ConflictManagementProviderImpl from '../../../src/r2mm/installing/ConflictManagementProviderImpl'; -import InstallationRules from '../../../src/r2mm/installing/InstallationRules'; import GenericProfileInstaller from '../../../src/r2mm/installing/profile_installers/GenericProfileInstaller'; import PathResolver from '../../../src/r2mm/manager/PathResolver'; import {providePathImplementation} from "../../../src/providers/node/path/path"; @@ -51,7 +50,6 @@ export async function installLogicBeforeEach(internalFolderName: string) { await inMemoryFs.mkdirs(Profile.getActiveProfile().getProfilePath()); ProfileInstallerProvider.provide(() => new GenericProfileInstaller()); - InstallationRules.apply(); ConflictManagementProvider.provide(() => new ConflictManagementProviderImpl()); InMemoryFsProvider.setMatchMode("CASE_SENSITIVE"); } From 5ead22c77a60b9fdcfea05d6d83b22cb123b42f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Thu, 11 Sep 2025 16:03:37 +0300 Subject: [PATCH 02/10] Reuse GenericProfileInstaller when installing mods to profile Accessing the instance of ProfileInstallerProvider actually initiates a new object on each call. While a small overhead, it's still unnecessary as all mods being installed at one go can be assumed to use the same PackageInstaller class. --- src/utils/ProfileUtils.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/utils/ProfileUtils.ts b/src/utils/ProfileUtils.ts index 90f6f7a1e..dc48117cd 100644 --- a/src/utils/ProfileUtils.ts +++ b/src/utils/ProfileUtils.ts @@ -86,6 +86,7 @@ export async function installModsToProfile( throw profileMods; } + const installerInstance = ProfileInstallerProvider.instance; const installedVersions = profileMods.map((m) => m.getDependencyString()); const disabledMods = disabledModsOverride || profileMods.filter((m) => !m.isEnabled()).map((m) => m.getName()); let modName = 'Unknown'; @@ -104,12 +105,12 @@ export async function installModsToProfile( const positionInProfile = profileMods.findIndex((m) => m.getName() === manifestMod.getName()); // Uninstall possible different version of the mod before installing the target version. - throwForR2Error(await ProfileInstallerProvider.instance.uninstallMod(manifestMod, profile)); + throwForR2Error(await installerInstance.uninstallMod(manifestMod, profile)); if (positionInProfile >= 0) { profileMods.splice(positionInProfile, 1); // Remove from list in case the install throws. } - throwForR2Error(await ProfileInstallerProvider.instance.installMod(manifestMod, profile)); + throwForR2Error(await installerInstance.installMod(manifestMod, profile)); if (positionInProfile >= 0) { profileMods.splice(positionInProfile, 0, manifestMod); // Inject back to original position. } else { @@ -117,7 +118,7 @@ export async function installModsToProfile( } if (disabledMods.includes(manifestMod.getName())) { - throwForR2Error(await ProfileInstallerProvider.instance.disableMod(manifestMod, profile)); + throwForR2Error(await installerInstance.disableMod(manifestMod, profile)); manifestMod.disable(); } From 3eb265e1eaf2944848babc52a0430b6d860e966d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Thu, 11 Sep 2025 16:26:59 +0300 Subject: [PATCH 03/10] Drop mod loader package listing from Vuex Use Thunderstore's ecosystem schema as the source of truth. --- src/App.vue | 1 - src/components/views/LocalModList/LocalModCard.vue | 3 ++- src/model/schema/ThunderstoreSchema.ts | 7 +++++++ .../profile_installers/ModLoaderVariantRecord.ts | 7 ------- src/store/index.ts | 13 ------------- 5 files changed, 9 insertions(+), 22 deletions(-) diff --git a/src/App.vue b/src/App.vue index caa216de5..4eb53e9d7 100644 --- a/src/App.vue +++ b/src/App.vue @@ -139,7 +139,6 @@ onMounted(async () => { }); }); - store.commit('updateModLoaderPackageNames'); store.dispatch('tsMods/updateExclusions'); }); diff --git a/src/components/views/LocalModList/LocalModCard.vue b/src/components/views/LocalModList/LocalModCard.vue index 33a6af0c1..ab4bc7dd6 100644 --- a/src/components/views/LocalModList/LocalModCard.vue +++ b/src/components/views/LocalModList/LocalModCard.vue @@ -4,6 +4,7 @@ import DonateButton from '../../buttons/DonateButton.vue'; import DonateIconButton from '../../buttons/DonateIconButton.vue'; import R2Error from '../../../model/errors/R2Error'; import ManifestV2 from '../../../model/ManifestV2'; +import { EcosystemSchema } from '../../../model/schema/ThunderstoreSchema'; import VersionNumber from '../../../model/VersionNumber'; import { LogSeverity } from '../../../providers/ror2/logging/LoggerProvider'; import Dependants from '../../../r2mm/mods/Dependants'; @@ -27,7 +28,7 @@ const disableChangePending = ref(false); // Mod loader packages can't be disabled as it's hard to define // what that should even do in all cases. -const canBeDisabled = computed(() => !store.getters['isModLoader'](props.mod.getName())); +const canBeDisabled = computed(() => !EcosystemSchema.isModLoaderPackage(props.mod.getName())); const isDeprecated = computed(() => store.state.tsMods.deprecated.get(props.mod.getName()) || false); const isLatestVersion = computed(() => store.getters['tsMods/isLatestVersion'](props.mod)); diff --git a/src/model/schema/ThunderstoreSchema.ts b/src/model/schema/ThunderstoreSchema.ts index 60a62535b..cecf7a162 100644 --- a/src/model/schema/ThunderstoreSchema.ts +++ b/src/model/schema/ThunderstoreSchema.ts @@ -67,4 +67,11 @@ export class EcosystemSchema { return config ? config[1] : undefined; } + + /** + * @param packageId Package's name in "TeamName-PackageName" format excluding version number. + */ + static isModLoaderPackage(packageId: string): boolean { + return this.modloaderPackages.some(pkg => pkg.packageId.toLowerCase() === packageId.toLowerCase()); + } } diff --git a/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts b/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts index d79f1144d..3935da95c 100644 --- a/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts +++ b/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts @@ -35,10 +35,3 @@ export const MOD_LOADER_VARIANTS: Modloaders = Object.fromEntries( OVERRIDES[game.internalFolderName] || MODLOADER_PACKAGES ]) ); - -export const getModLoaderPackageNames = () => { - const deduplicated = new Set(EcosystemSchema.modloaderPackages.map((x) => x.packageId)); - const names = Array.from(deduplicated); - names.sort(); - return names; -} diff --git a/src/store/index.ts b/src/store/index.ts index 455068f65..e751b5d96 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -12,14 +12,12 @@ import { FolderMigration } from '../migrations/FolderMigration'; import Game from '../model/game/Game'; import GameManager from '../model/game/GameManager'; import R2Error from '../model/errors/R2Error'; -import { getModLoaderPackageNames } from '../r2mm/installing/profile_installers/ModLoaderVariantRecord'; import ManagerSettings from '../r2mm/manager/ManagerSettings'; import { SplashModule } from './modules/SplashModule'; export interface State { activeGame: Game; isMigrationChecked: boolean; - modLoaderPackageNames: string[]; _settings: ManagerSettings | null; } @@ -34,7 +32,6 @@ export const store = { state: { activeGame: GameManager.defaultGame, isMigrationChecked: false, - modLoaderPackageNames: [], // Access through getters to ensure the settings are loaded. _settings: null, @@ -93,19 +90,9 @@ export const store = { }, setSettings(state: State, settings: ManagerSettings) { state._settings = settings; - }, - updateModLoaderPackageNames(state: State) { - // The list is static and doesn't change during runtime. - if (!state.modLoaderPackageNames.length) { - state.modLoaderPackageNames = getModLoaderPackageNames(); - } } }, getters: { - isModLoader: (state: State) => (packageName: string): boolean => { - return state.modLoaderPackageNames.includes(packageName); - }, - settings(state: State): ManagerSettings { if (state._settings === null) { throw new R2Error( From cfc61e3d8d273951bdf40b0e886119409127512b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Fri, 12 Sep 2025 10:41:30 +0300 Subject: [PATCH 04/10] Use EcosystemSchema.isModLoaderPackage in GenericProfileInstaller --- .../profile_installers/GenericProfileInstaller.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/r2mm/installing/profile_installers/GenericProfileInstaller.ts b/src/r2mm/installing/profile_installers/GenericProfileInstaller.ts index 9c08fd75a..725d643e8 100644 --- a/src/r2mm/installing/profile_installers/GenericProfileInstaller.ts +++ b/src/r2mm/installing/profile_installers/GenericProfileInstaller.ts @@ -4,8 +4,8 @@ import R2Error from '../../../model/errors/R2Error'; import GameManager from '../../../model/game/GameManager'; import ManifestV2 from '../../../model/ManifestV2'; import { ImmutableProfile } from '../../../model/Profile'; +import { EcosystemSchema } from '../../../model/schema/ThunderstoreSchema'; import ProfileInstallerProvider from '../../../providers/ror2/installing/ProfileInstallerProvider'; -import { MOD_LOADER_VARIANTS } from '../../installing/profile_installers/ModLoaderVariantRecord'; export default class GenericProfileInstaller extends ProfileInstallerProvider { @@ -19,19 +19,15 @@ export default class GenericProfileInstaller extends ProfileInstallerProvider { this.pluginInstaller = PluginInstallers[loader]; } - private isModLoader(mod: ManifestV2): boolean { - const modLoaders = MOD_LOADER_VARIANTS[GameManager.activeGame.internalFolderName] ?? []; - return modLoaders.some(loader => loader.packageName.toLowerCase() === mod.getName().toLowerCase()); - } - private getInstallerForPackage(mod: ManifestV2): PackageInstaller { - return this.isModLoader(mod) ? this.modLoaderInstaller : this.pluginInstaller; + const isModLoader = EcosystemSchema.isModLoaderPackage(mod.getName()); + return isModLoader ? this.modLoaderInstaller : this.pluginInstaller; } async disableMod(mod: ManifestV2, profile: ImmutableProfile): Promise { try { // Mod loaders don't support disabling. - if (this.isModLoader(mod)) { + if (EcosystemSchema.isModLoaderPackage(mod.getName())) { return; } @@ -49,7 +45,7 @@ export default class GenericProfileInstaller extends ProfileInstallerProvider { async enableMod(mod: ManifestV2, profile: ImmutableProfile): Promise { try { // Mod loaders don't support enabling. - if (this.isModLoader(mod)) { + if (EcosystemSchema.isModLoaderPackage(mod.getName())) { return; } From 813d1837cb49b1a330539f9efab01663258e5779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Fri, 12 Sep 2025 10:43:22 +0300 Subject: [PATCH 05/10] Add EcosystemSchema.getModLoaderMapping Some of the usages checked that the package mapping is related to correct PackageLoader, but after the changes the each PackageInstaller should get called only with such packages anyhow, so it feels safe to drop this check. --- src/installers/BepInExInstaller.ts | 5 ++--- src/installers/GDWeaveInstaller.ts | 10 ++-------- src/installers/NorthstarInstaller.ts | 8 ++------ src/installers/ReturnOfModdingInstaller.ts | 8 ++------ src/model/schema/ThunderstoreSchema.ts | 11 +++++++++++ .../profile_installers/ModLoaderVariantRecord.ts | 2 +- 6 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/installers/BepInExInstaller.ts b/src/installers/BepInExInstaller.ts index d8c8d6315..25b1118a0 100644 --- a/src/installers/BepInExInstaller.ts +++ b/src/installers/BepInExInstaller.ts @@ -1,8 +1,7 @@ import { InstallArgs, PackageInstaller, uninstallModLoader } from "./PackageInstaller"; import path from "../providers/node/path/path"; import FsProvider from "../providers/generic/file/FsProvider"; -import { MODLOADER_PACKAGES } from "../r2mm/installing/profile_installers/ModLoaderVariantRecord"; -import { PackageLoader } from "../model/schema/ThunderstoreSchema"; +import { EcosystemSchema } from "../model/schema/ThunderstoreSchema"; const basePackageFiles = ["manifest.json", "readme.md", "icon.png"]; @@ -17,7 +16,7 @@ export class BepInExInstaller implements PackageInstaller { profile, } = args; - const mapping = MODLOADER_PACKAGES.find((entry) => entry.packageName.toLowerCase() == mod.getName().toLowerCase()); + const mapping = EcosystemSchema.getModLoaderMapping(mod.getName()); const mappingRoot = mapping ? mapping.rootFolder : ""; let bepInExRoot: string; diff --git a/src/installers/GDWeaveInstaller.ts b/src/installers/GDWeaveInstaller.ts index c31019dd7..e5b380767 100644 --- a/src/installers/GDWeaveInstaller.ts +++ b/src/installers/GDWeaveInstaller.ts @@ -7,8 +7,7 @@ import { import path from "../providers/node/path/path"; import FsProvider from '../providers/generic/file/FsProvider'; import FileUtils from '../utils/FileUtils'; -import { MODLOADER_PACKAGES } from '../r2mm/installing/profile_installers/ModLoaderVariantRecord'; -import { PackageLoader } from '../model/schema/ThunderstoreSchema'; +import { EcosystemSchema } from '../model/schema/ThunderstoreSchema'; import FileWriteError from '../model/errors/FileWriteError'; import R2Error from '../model/errors/R2Error'; @@ -16,12 +15,7 @@ export class GDWeaveInstaller implements PackageInstaller { async install(args: InstallArgs) { const { mod, packagePath, profile } = args; - const mapping = MODLOADER_PACKAGES.find( - (entry) => - entry.packageName.toLowerCase() == - mod.getName().toLowerCase() && - entry.loaderType == PackageLoader.GDWEAVE - ); + const mapping = EcosystemSchema.getModLoaderMapping(mod.getName()); if (!mapping) { throw new Error(`Missing modloader for ${mod.getName()}`); diff --git a/src/installers/NorthstarInstaller.ts b/src/installers/NorthstarInstaller.ts index ca6bf011f..bb3cd9fec 100644 --- a/src/installers/NorthstarInstaller.ts +++ b/src/installers/NorthstarInstaller.ts @@ -1,8 +1,7 @@ import { InstallArgs, PackageInstaller, uninstallModLoader } from './PackageInstaller'; import path from "../providers/node/path/path"; import FsProvider from '../providers/generic/file/FsProvider'; -import { MODLOADER_PACKAGES } from '../r2mm/installing/profile_installers/ModLoaderVariantRecord'; -import { PackageLoader } from '../model/schema/ThunderstoreSchema'; +import { EcosystemSchema } from '../model/schema/ThunderstoreSchema'; const basePackageFiles = ["manifest.json", "readme.md", "icon.png"]; @@ -17,10 +16,7 @@ export class NorthstarInstaller implements PackageInstaller { profile, } = args; - const mapping = MODLOADER_PACKAGES.find((entry) => - entry.packageName.toLowerCase() == mod.getName().toLowerCase() && - entry.loaderType == PackageLoader.NORTHSTAR, - ); + const mapping = EcosystemSchema.getModLoaderMapping(mod.getName()); const mappingRoot = mapping ? mapping.rootFolder : ""; let northstarRoot: string; diff --git a/src/installers/ReturnOfModdingInstaller.ts b/src/installers/ReturnOfModdingInstaller.ts index 881ce5dd7..fc8680147 100644 --- a/src/installers/ReturnOfModdingInstaller.ts +++ b/src/installers/ReturnOfModdingInstaller.ts @@ -2,9 +2,8 @@ import path from "../providers/node/path/path"; import { InstallRulePluginInstaller } from "./InstallRulePluginInstaller"; import { InstallArgs, PackageInstaller } from "./PackageInstaller"; import FileWriteError from "../model/errors/FileWriteError"; -import { PackageLoader } from "../model/schema/ThunderstoreSchema"; +import { EcosystemSchema } from "../model/schema/ThunderstoreSchema"; import FsProvider from "../providers/generic/file/FsProvider"; -import { MODLOADER_PACKAGES } from "../r2mm/installing/profile_installers/ModLoaderVariantRecord"; import FileUtils from "../utils/FileUtils"; import { TrackingMethod } from "../model/schema/ThunderstoreSchema"; @@ -17,10 +16,7 @@ export class ReturnOfModdingInstaller implements PackageInstaller { async install(args: InstallArgs) { const {mod, packagePath, profile} = args; - const mapping = MODLOADER_PACKAGES.find((entry) => - entry.packageName.toLowerCase() == mod.getName().toLowerCase() && - entry.loaderType == PackageLoader.RETURN_OF_MODDING, - ); + const mapping = EcosystemSchema.getModLoaderMapping(mod.getName()); if (mapping === undefined) { throw new Error(`ReturnOfModdingInstaller found no loader for ${mod.getName()}`); diff --git a/src/model/schema/ThunderstoreSchema.ts b/src/model/schema/ThunderstoreSchema.ts index cecf7a162..1cad8d02b 100644 --- a/src/model/schema/ThunderstoreSchema.ts +++ b/src/model/schema/ThunderstoreSchema.ts @@ -5,6 +5,7 @@ import ecosystem from "../../assets/data/ecosystem.json"; import { R2Modman as GameConfig, ThunderstoreEcosystem } from "../../assets/data/ecosystemTypes"; import jsonSchema from "../../assets/data/ecosystemJsonSchema.json"; import R2Error from "../errors/R2Error"; +import ModLoaderPackageMapping from "../installing/ModLoaderPackageMapping"; // Re-export generated types/Enums to avoid having the whole codebase // tightly coupled with the generated ecosystemTypes. @@ -68,6 +69,16 @@ export class EcosystemSchema { return config ? config[1] : undefined; } + /** + * @param packageId Package's name in "TeamName-PackageName" format excluding version number. + */ + static getModLoaderMapping(packageId: string): ModLoaderPackageMapping|undefined { + const pkg = this.modloaderPackages.find(pkg => pkg.packageId.toLowerCase() === packageId.toLowerCase()); + return pkg + ? new ModLoaderPackageMapping(pkg.packageId, pkg.rootFolder, pkg.loader) + : undefined; + } + /** * @param packageId Package's name in "TeamName-PackageName" format excluding version number. */ diff --git a/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts b/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts index 3935da95c..aefb960c1 100644 --- a/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts +++ b/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts @@ -5,7 +5,7 @@ import { EcosystemSchema, PackageLoader } from '../../../model/schema/Thundersto /** * A set of modloader packages read from the ecosystem schema. */ -export const MODLOADER_PACKAGES = EcosystemSchema.modloaderPackages.map((x) => +const MODLOADER_PACKAGES = EcosystemSchema.modloaderPackages.map((x) => new ModLoaderPackageMapping( x.packageId, x.rootFolder, From a1fd8e3f2a3ba6f0fa21b2295f05f64004c79626 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Fri, 12 Sep 2025 11:51:27 +0300 Subject: [PATCH 06/10] Drop checking active game when uninstalling a mod loader Checking that the package is a mod loader should suffice, as the uninstall method should be called only if the package is associated with the PackageLoader of the active game. --- src/installers/PackageInstaller.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/installers/PackageInstaller.ts b/src/installers/PackageInstaller.ts index 0149e6b08..b04b9876b 100644 --- a/src/installers/PackageInstaller.ts +++ b/src/installers/PackageInstaller.ts @@ -1,12 +1,11 @@ import FileWriteError from "../model/errors/FileWriteError"; import R2Error from "../model/errors/R2Error"; import FileTree from "../model/file/FileTree"; -import GameManager from "../model/game/GameManager"; import { ImmutableProfile } from "../model/Profile"; import ManifestV2 from "../model/ManifestV2"; +import { EcosystemSchema } from "../model/schema/ThunderstoreSchema"; 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 = { @@ -64,11 +63,7 @@ export async function uninstallModLoader(mod: ManifestV2, profile: ImmutableProf const fs = FsProvider.instance; try { - const loader = MOD_LOADER_VARIANTS[GameManager.activeGame.internalFolderName].find( - loader => loader.packageName.toLowerCase() === mod.getName().toLowerCase() - ); - - if (loader) { + if (EcosystemSchema.isModLoaderPackage(mod.getName())) { for (const file of (await fs.readdir(profile.getProfilePath()))) { if (file.toLowerCase() === "mods.yml") { continue; From b5b78723b816efa72fbf1b21bc22ad0912d45c4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Fri, 12 Sep 2025 12:20:10 +0300 Subject: [PATCH 07/10] Read recommended package versions from EcosystemSchema Ideally the data would be included in the Thunderstore API. Until that day comes, keep the hardocoded values in the EcosystemSchema. --- .../views/DownloadModVersionSelectModal.vue | 14 ++++++++------ src/model/schema/ThunderstoreSchema.ts | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/components/views/DownloadModVersionSelectModal.vue b/src/components/views/DownloadModVersionSelectModal.vue index d42a964b7..db5cecf77 100644 --- a/src/components/views/DownloadModVersionSelectModal.vue +++ b/src/components/views/DownloadModVersionSelectModal.vue @@ -55,8 +55,8 @@ import ModalCard from "../ModalCard.vue"; import R2Error from "../../model/errors/R2Error"; import ManifestV2 from "../../model/ManifestV2"; +import { EcosystemSchema } from "../../model/schema/ThunderstoreSchema"; import ThunderstoreVersion from "../../model/ThunderstoreVersion"; -import { MOD_LOADER_VARIANTS } from "../../r2mm/installing/profile_installers/ModLoaderVariantRecord"; import * as PackageDb from "../../r2mm/manager/PackageDexieStore"; import ProfileModList from "../../r2mm/mods/ProfileModList"; import Game from '../../model/game/Game'; @@ -94,15 +94,17 @@ watch(() => store.state.modals.downloadModalMod, async () => { mod.getFullName() ); - const foundRecommendedVersion = MOD_LOADER_VARIANTS[activeGame.internalFolderName] - .find(value => value.packageName === mod.getFullName()); + const foundRecommendedVersion = EcosystemSchema.getRecommendedVersion( + thunderstoreMod.value.getFullName(), + activeGame.settingsIdentifier + ); - if (foundRecommendedVersion && foundRecommendedVersion.recommendedVersion) { - recommendedVersion.value = foundRecommendedVersion.recommendedVersion.toString(); + if (foundRecommendedVersion) { + recommendedVersion.value = foundRecommendedVersion; // Auto-select recommended version if it's found. const recommendedVersionToSelect = versionNumbers.value.find( - (ver) => ver === foundRecommendedVersion.recommendedVersion!.toString() + (ver) => ver === foundRecommendedVersion ); if (recommendedVersionToSelect) { selectedVersion.value = recommendedVersionToSelect; diff --git a/src/model/schema/ThunderstoreSchema.ts b/src/model/schema/ThunderstoreSchema.ts index 1cad8d02b..cf32cb0eb 100644 --- a/src/model/schema/ThunderstoreSchema.ts +++ b/src/model/schema/ThunderstoreSchema.ts @@ -79,6 +79,20 @@ export class EcosystemSchema { : undefined; } + + /** + * @param packageId Package's name in "TeamName-PackageName" format excluding version number. + * @param settingsIdentifier Game's settings identifier. + */ + static getRecommendedVersion(packageId: string, settingsIdentifier: string): string|undefined { + // Use hardcoded values until this information available via Thunderstore Ecosystem API. + if (packageId === "LavaGang-MelonLoader" && settingsIdentifier === "BONEWORKS") { + return "0.5.4"; + } + + return undefined; + } + /** * @param packageId Package's name in "TeamName-PackageName" format excluding version number. */ From cec7244114d73912148f7d6a2fdb1b3d3f156a26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Fri, 12 Sep 2025 12:22:42 +0300 Subject: [PATCH 08/10] Drop obsolete ModLoaderVariantRecord The responsibilities have been taken over by the EcosystemSchema and the file is now unused. --- .../ModLoaderVariantRecord.ts | 37 ------------------- 1 file changed, 37 deletions(-) delete mode 100644 src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts diff --git a/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts b/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts deleted file mode 100644 index aefb960c1..000000000 --- a/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts +++ /dev/null @@ -1,37 +0,0 @@ -import ModLoaderPackageMapping from '../../../model/installing/ModLoaderPackageMapping'; -import VersionNumber from '../../../model/VersionNumber'; -import { EcosystemSchema, PackageLoader } from '../../../model/schema/ThunderstoreSchema'; - -/** - * A set of modloader packages read from the ecosystem schema. - */ -const MODLOADER_PACKAGES = EcosystemSchema.modloaderPackages.map((x) => - new ModLoaderPackageMapping( - x.packageId, - x.rootFolder, - x.loader, - ), -); - -type Modloaders = Record; - -// Overrides are needed as the "recommended version" information -// is not available in the ecosystem data. -const OVERRIDES: Modloaders = { - BONEWORKS: [ - new ModLoaderPackageMapping( - 'LavaGang-MelonLoader', - '', - PackageLoader.MELONLOADER, - new VersionNumber('0.5.4'), - ), - ], -} - -export const MOD_LOADER_VARIANTS: Modloaders = Object.fromEntries( - EcosystemSchema.supportedGames - .map(([_, game]) => [ - game.internalFolderName, - OVERRIDES[game.internalFolderName] || MODLOADER_PACKAGES - ]) -); From addc641c5630119682638991e4cda5b2de42a76a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Fri, 12 Sep 2025 12:39:45 +0300 Subject: [PATCH 09/10] Drop ModLoaderPackageMapping EcosystemSchema now provides the data using the format used by the schema. This differs a bit from the old interface, but current usage only accesses rootFolder attribute so it doesn't matter. --- .../installing/ModLoaderPackageMapping.ts | 33 ------------------- src/model/schema/ThunderstoreSchema.ts | 14 ++++---- 2 files changed, 7 insertions(+), 40 deletions(-) delete mode 100644 src/model/installing/ModLoaderPackageMapping.ts diff --git a/src/model/installing/ModLoaderPackageMapping.ts b/src/model/installing/ModLoaderPackageMapping.ts deleted file mode 100644 index 2e150a4b8..000000000 --- a/src/model/installing/ModLoaderPackageMapping.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { PackageLoader } from '../../model/schema/ThunderstoreSchema'; -import VersionNumber from '../../model/VersionNumber'; - -export default class ModLoaderPackageMapping { - - private readonly _packageName: string; - private readonly _rootFolder: string; - private readonly _loaderType: PackageLoader; - private readonly _recommendedVersion: VersionNumber | undefined; - - constructor(packageName: string, rootFolder: string, loaderType: PackageLoader, recommendedVersion?: VersionNumber) { - this._packageName = packageName; - this._rootFolder = rootFolder; - this._loaderType = loaderType; - this._recommendedVersion = recommendedVersion; - } - - get packageName() { - return this._packageName; - } - - get rootFolder() { - return this._rootFolder; - } - - get loaderType(): PackageLoader { - return this._loaderType; - } - - get recommendedVersion(): VersionNumber | undefined { - return this._recommendedVersion; - } -} diff --git a/src/model/schema/ThunderstoreSchema.ts b/src/model/schema/ThunderstoreSchema.ts index cf32cb0eb..977c6027b 100644 --- a/src/model/schema/ThunderstoreSchema.ts +++ b/src/model/schema/ThunderstoreSchema.ts @@ -2,10 +2,13 @@ import Ajv from "ajv"; import addFormats from "ajv-formats"; import ecosystem from "../../assets/data/ecosystem.json"; -import { R2Modman as GameConfig, ThunderstoreEcosystem } from "../../assets/data/ecosystemTypes"; +import { + ModloaderPackage as ModLoaderPackage, + ThunderstoreEcosystem, + R2Modman as GameConfig +} from "../../assets/data/ecosystemTypes"; import jsonSchema from "../../assets/data/ecosystemJsonSchema.json"; import R2Error from "../errors/R2Error"; -import ModLoaderPackageMapping from "../installing/ModLoaderPackageMapping"; // Re-export generated types/Enums to avoid having the whole codebase // tightly coupled with the generated ecosystemTypes. @@ -72,11 +75,8 @@ export class EcosystemSchema { /** * @param packageId Package's name in "TeamName-PackageName" format excluding version number. */ - static getModLoaderMapping(packageId: string): ModLoaderPackageMapping|undefined { - const pkg = this.modloaderPackages.find(pkg => pkg.packageId.toLowerCase() === packageId.toLowerCase()); - return pkg - ? new ModLoaderPackageMapping(pkg.packageId, pkg.rootFolder, pkg.loader) - : undefined; + static getModLoaderMapping(packageId: string): ModLoaderPackage|undefined { + return this.modloaderPackages.find(pkg => pkg.packageId.toLowerCase() === packageId.toLowerCase()); } From ee2997d3a5500a3af9f4c1f3d8178706650340e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Fri, 17 Apr 2026 12:01:56 +0300 Subject: [PATCH 10/10] InstallRulePluginInstaller cleanup - Add explicit return types to avoid accidental future changes - Simplify "best fit rule" by dropping checks for impossible undefined --- src/installers/InstallRulePluginInstaller.ts | 74 +++++++++++++------- 1 file changed, 48 insertions(+), 26 deletions(-) diff --git a/src/installers/InstallRulePluginInstaller.ts b/src/installers/InstallRulePluginInstaller.ts index fe7914158..dd7420fd9 100644 --- a/src/installers/InstallRulePluginInstaller.ts +++ b/src/installers/InstallRulePluginInstaller.ts @@ -21,7 +21,6 @@ type CoreRuleType = { relativeFileExclusions: string[] | null, } - type RuleSubtype = { route: string, trackingMethod: TrackingMethod, @@ -48,8 +47,6 @@ type InstallRuleArgs = { /** * Produce a flattened structure of all navigable paths maintained by the install rules. - * @param rules - * @param pathBuilder */ export function getAllManagedPaths(rules: RuleSubtype[], pathBuilder?: string): ManagedRule[] { const paths: ManagedRule[] = []; @@ -82,7 +79,11 @@ function getRuleSubtypeFromManagedRule(managedRule: ManagedRule, rule: CoreRuleT throw new Error("RuleSubtype does not exist for ManagedRule."); } -function getRuleSubtypeFromManagedRuleInner(managedRule: ManagedRule, subType: RuleSubtype, realRoute: string): RuleSubtype | undefined { +function getRuleSubtypeFromManagedRuleInner( + managedRule: ManagedRule, + subType: RuleSubtype, + realRoute: string +): RuleSubtype | undefined { if (realRoute === managedRule.route) { return subType; } else { @@ -100,8 +101,12 @@ function getManagedRuleForSubtype(rule: CoreRuleType, subType: RuleSubtype): Man return getAllManagedPaths(rule.rules).find(value => getRuleSubtypeFromManagedRule(value, rule) === subType)!; } - -async function installUntracked(profile: ImmutableProfile, rule: ManagedRule, installSources: string[], mod: ManifestV2) { +async function installUntracked( + profile: ImmutableProfile, + rule: ManagedRule, + installSources: string[], + mod: ManifestV2 +): Promise { // Functionally identical to the install method of subdir, minus the subdirectory. const ruleDir = profile.joinToProfilePath(rule.route); await FileUtils.ensureDirectory(ruleDir); @@ -120,13 +125,12 @@ async function installUntracked(profile: ImmutableProfile, rule: ManagedRule, in } } - async function installSubDir( profile: ImmutableProfile, rule: ManagedRule, installSources: string[], mod: ManifestV2, -) { +): Promise { const subDir = profile.joinToProfilePath(rule.route, mod.getName()); await FileUtils.ensureDirectory(subDir); for (const source of installSources) { @@ -148,8 +152,12 @@ async function installSubDir( } } - -async function installPackageZip(profile: ImmutableProfile, rule: ManagedRule, installSources: string[], mod: ManifestV2) { +async function installPackageZip( + profile: ImmutableProfile, + rule: ManagedRule, + installSources: string[], + mod: ManifestV2 +): Promise { /* This install method repackages the entire mod as-is and places it to the destination route. Essentially the same as SUBDIR_NO_FLATTEN, but as a @@ -165,8 +173,12 @@ async function installPackageZip(profile: ImmutableProfile, rule: ManagedRule, i await builder.createZip(destination); } - -async function installSubDirNoFlatten(profile: ImmutableProfile, rule: ManagedRule, installSources: string[], mod: ManifestV2) { +async function installSubDirNoFlatten( + profile: ImmutableProfile, + rule: ManagedRule, + installSources: string[], + mod: ManifestV2 +): Promise { const subDir = profile.joinToProfilePath(rule.route, mod.getName()); await FileUtils.ensureDirectory(subDir); const cacheDirectory = path.join(PathResolver.MOD_ROOT, 'cache'); @@ -192,13 +204,18 @@ async function installSubDirNoFlatten(profile: ImmutableProfile, rule: ManagedRu } } -function getBestFitRule(matchingRules: ManagedRule[], file: FileTree) { +type BestFitRule = { + rule: ManagedRule, + count: number +} + +function getBestFitRule(matchingRules: ManagedRule[], file: FileTree): BestFitRule | undefined { if (matchingRules.length === 0) { return undefined; } if (matchingRules.length === 1) { return { - rule: matchingRules[0], + rule: matchingRules[0]!, count: 0 }; } @@ -263,13 +280,9 @@ async function buildInstallForRuleSubtype( installationIntent.set(subType, updatedArray); } for (const file of tree.getDirectories()) { - let matchingRules: ManagedRule[] = flatRules.filter(value => path.basename(value.route).toLowerCase() === file.getDirectoryName().toLowerCase()); - let matchingRule: ManagedRule | undefined = undefined; + const matchingRules: ManagedRule[] = flatRules.filter(value => path.basename(value.route).toLowerCase() === file.getDirectoryName().toLowerCase()); + const matchingRule: ManagedRule | undefined = getBestFitRule(matchingRules, file)?.rule; - const bestFitRule = getBestFitRule(matchingRules, file); - if (bestFitRule) { - matchingRule = bestFitRule.rule; - } if (matchingRule === undefined) { const nested = await buildInstallForRuleSubtype(rule, path.join(location, file.getDirectoryName()), folderName, mod, file); for (let [rule, files] of nested.entries()) { @@ -287,8 +300,11 @@ async function buildInstallForRuleSubtype( return installationIntent; } - -export async function addToStateFile(mod: ManifestV2, files: Map, profile: ImmutableProfile) { +export async function addToStateFile( + mod: ManifestV2, + files: Map, + profile: ImmutableProfile +): Promise { await FileUtils.ensureDirectory(profile.joinToProfilePath("_state")); let existing: Map = new Map(); if (await FsProvider.instance.exists(profile.joinToProfilePath("_state", `${mod.getName()}-state.yml`))) { @@ -307,7 +323,7 @@ export async function addToStateFile(mod: ManifestV2, files: Map await ConflictManagementProvider.instance.overrideInstalledState(mod, profile); } -async function installState(args: InstallRuleArgs) { +async function installState(args: InstallRuleArgs): Promise { const { profile, coreRule, rule, installSources, mod } = args; const fileRelocations = new Map(); for (const source of installSources) { @@ -334,7 +350,7 @@ async function installState(args: InstallRuleArgs) { await addToStateFile(mod, fileRelocations, profile); } -async function uninstallPackageZip(mod: ManifestV2, profile: ImmutableProfile) { +async function uninstallPackageZip(mod: ManifestV2, profile: ImmutableProfile): Promise { const fs = FsProvider.instance; const recursiveDelete = async (mainPath: string, match: string) => { @@ -352,7 +368,7 @@ async function uninstallPackageZip(mod: ManifestV2, profile: ImmutableProfile) { await recursiveDelete(profile.getProfilePath(), `${mod.getName()}.ts.zip`); } -async function uninstallSubDir(mod: ManifestV2, profile: ImmutableProfile) { +async function uninstallSubDir(mod: ManifestV2, profile: ImmutableProfile): Promise { const fs = FsProvider.instance; const searchLocations = ["BepInEx", "shimloader", "UMM"].map((x) => profile.joinToProfilePath(x)); @@ -483,7 +499,13 @@ export class InstallRulePluginInstaller implements PackageInstaller { await this.resolveFileTreeInstall(profile, packagePath, path.basename(packagePath), mod, fileTree); } - private async resolveFileTreeInstall(profile: ImmutableProfile, location: string, folderName: string, mod: ManifestV2, tree: FileTree) { + private async resolveFileTreeInstall( + profile: ImmutableProfile, + location: string, + folderName: string, + mod: ManifestV2, + tree: FileTree + ): Promise { const installationIntent = await buildInstallForRuleSubtype(this.rule, location, folderName, mod, tree); for (let [rule, files] of installationIntent.entries()) { const managedRule = getManagedRuleForSubtype(this.rule, rule);