diff --git a/src/App.vue b/src/App.vue index d5049e349..4eb53e9d7 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. @@ -143,7 +139,6 @@ onMounted(async () => { }); }); - store.commit('updateModLoaderPackageNames'); store.dispatch('tsMods/updateExclusions'); }); 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/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/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/InstallRulePluginInstaller.ts b/src/installers/InstallRulePluginInstaller.ts index e91fe7745..dd7420fd9 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,28 @@ 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,8 +45,68 @@ type InstallRuleArgs = { mod: ManifestV2, }; +/** + * Produce a flattened structure of all navigable paths maintained by the install rules. + */ +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; +} -async function installUntracked(profile: ImmutableProfile, rule: ManagedRule, installSources: string[], mod: ManifestV2) { +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 +): Promise { // Functionally identical to the install method of subdir, minus the subdirectory. const ruleDir = profile.joinToProfilePath(rule.route); await FileUtils.ensureDirectory(ruleDir); @@ -44,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) { @@ -72,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 @@ -89,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'); @@ -116,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 }; } @@ -158,7 +251,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,19 +274,15 @@ 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); } 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()) { @@ -202,7 +291,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); @@ -211,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`))) { @@ -231,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) { @@ -258,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) => { @@ -276,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)); @@ -318,7 +410,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) { @@ -407,10 +499,17 @@ 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 = InstallationRules.getManagedRuleForSubtype(this.rule, rule); + const managedRule = getManagedRuleForSubtype(this.rule, rule); + const args: InstallRuleArgs = { profile, coreRule: this.rule, 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/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; 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/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 60a62535b..977c6027b 100644 --- a/src/model/schema/ThunderstoreSchema.ts +++ b/src/model/schema/ThunderstoreSchema.ts @@ -2,7 +2,11 @@ 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"; @@ -67,4 +71,32 @@ export class EcosystemSchema { return config ? config[1] : undefined; } + + /** + * @param packageId Package's name in "TeamName-PackageName" format excluding version number. + */ + static getModLoaderMapping(packageId: string): ModLoaderPackage|undefined { + return this.modloaderPackages.find(pkg => pkg.packageId.toLowerCase() === packageId.toLowerCase()); + } + + + /** + * @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. + */ + static isModLoaderPackage(packageId: string): boolean { + return this.modloaderPackages.some(pkg => pkg.packageId.toLowerCase() === packageId.toLowerCase()); + } } 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/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; } diff --git a/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts b/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts deleted file mode 100644 index d79f1144d..000000000 --- a/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts +++ /dev/null @@ -1,44 +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. - */ -export 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 - ]) -); - -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( 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(); } 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"); }