diff --git a/quasar.config.ts b/quasar.config.ts index 886cf8f0d..12b51338c 100644 --- a/quasar.config.ts +++ b/quasar.config.ts @@ -18,7 +18,8 @@ export default defineConfig((ctx) => { boot: [ 'i18n', // 'axios', - 'floating-vue' + 'floating-vue', + 'ecosystem' ], // https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#css diff --git a/src/assets/data/ecosystem.json b/src/assets/data/ecosystem.json index fcb503b5e..4af35522d 100644 --- a/src/assets/data/ecosystem.json +++ b/src/assets/data/ecosystem.json @@ -40322,4 +40322,4 @@ "loader": "bepinex" } ] -} \ No newline at end of file +} diff --git a/src/boot/ecosystem.ts b/src/boot/ecosystem.ts new file mode 100644 index 000000000..ccf147c5c --- /dev/null +++ b/src/boot/ecosystem.ts @@ -0,0 +1,12 @@ +import { defineBoot } from '#q-app/wrappers'; +import {updateEcosystemReactives} from "src/r2mm/ecosystem/EcosystemSchema"; +import FsProvider from "src/providers/generic/file/FsProvider"; +import {NodeFsImplementation} from "src/providers/node/fs/NodeFsImplementation"; + +// @ts-ignore +export default defineBoot(async ({ app }) => { + FsProvider.provide(() => NodeFsImplementation); + await updateEcosystemReactives(); + // @ts-ignore + FsProvider.provide(() => undefined); +}); diff --git a/src/installers/InstallRulePluginInstaller.ts b/src/installers/InstallRulePluginInstaller.ts index e91fe7745..73b70fe5a 100644 --- a/src/installers/InstallRulePluginInstaller.ts +++ b/src/installers/InstallRulePluginInstaller.ts @@ -12,7 +12,7 @@ import ModFileTracker from "../model/installing/ModFileTracker"; import ConflictManagementProvider from "../providers/generic/installing/ConflictManagementProvider"; import PathResolver from "../r2mm/manager/PathResolver"; import ZipProvider from "../providers/generic/zip/ZipProvider"; -import { EcosystemSchema, TrackingMethod } from "../model/schema/ThunderstoreSchema"; +import { getGameConfigBySettingsIdentifier, TrackingMethod } from "../model/schema/ThunderstoreSchema"; import ModMode from "../model/enums/ModMode"; import GameManager from "../model/game/GameManager"; @@ -385,7 +385,7 @@ export class InstallRulePluginInstaller implements PackageInstaller { // While it's not ideal that this same method is called repeatedly, // the code path below currently takes <1ms to execute so we should be fine. - const gameConfig = EcosystemSchema.getGameConfigBySettingsIdentifier(GameManager.activeGame.settingsIdentifier); + const gameConfig = getGameConfigBySettingsIdentifier(GameManager.activeGame.settingsIdentifier); if (gameConfig === undefined) { throw new Error(`Game config not found for ${GameManager.activeGame.settingsIdentifier}`); } diff --git a/src/model/game/GameManager.ts b/src/model/game/GameManager.ts index 757eef973..8f5196163 100644 --- a/src/model/game/GameManager.ts +++ b/src/model/game/GameManager.ts @@ -2,7 +2,7 @@ import Game from '../../model/game/Game'; import StorePlatformMetadata from '../../model/game/StorePlatformMetadata'; import PathResolver from '../../r2mm/manager/PathResolver'; import FileUtils from '../../utils/FileUtils'; -import { EcosystemSchema, Platform } from '../schema/ThunderstoreSchema'; +import {EcosystemSupportedGames, Platform} from '../schema/ThunderstoreSchema'; import path from '../../providers/node/path/path'; export default class GameManager { @@ -23,7 +23,7 @@ export default class GameManager { } static get gameList(): Game[] { - return EcosystemSchema.supportedGames.map(([identifier, game]) => new Game( + return EcosystemSupportedGames.value.map(([identifier, game]) => new Game( game.meta.displayName, game.internalFolderName, game.settingsIdentifier, diff --git a/src/model/schema/ThunderstoreSchema.ts b/src/model/schema/ThunderstoreSchema.ts index 60a62535b..cc19d164b 100644 --- a/src/model/schema/ThunderstoreSchema.ts +++ b/src/model/schema/ThunderstoreSchema.ts @@ -1,10 +1,5 @@ -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 jsonSchema from "../../assets/data/ecosystemJsonSchema.json"; -import R2Error from "../errors/R2Error"; +import {ModloaderPackage, R2Modman as GameConfig} from "../../assets/data/ecosystemTypes"; +import {ref} from "@vue/reactivity"; // Re-export generated types/Enums to avoid having the whole codebase // tightly coupled with the generated ecosystemTypes. @@ -16,55 +11,13 @@ export { Platform, } from "../../assets/data/ecosystemTypes"; -export class EcosystemSchema { - private static _isValidated: boolean = false; - - /** - * Get a validated instance of the ecosystem schema. - */ - private static get ecosystem(): ThunderstoreEcosystem { - if (this._isValidated) { - return ecosystem as ThunderstoreEcosystem; - } - - // Validate the schema via its schema schema. - const ajv = new Ajv(); - addFormats(ajv); - - const validate = ajv.compile(jsonSchema); - const isOk = validate(ecosystem); - - if (!isOk) { - throw new R2Error("Schema validation error", ajv.errorsText(validate.errors)); - } - - this._isValidated = true; - return ecosystem as ThunderstoreEcosystem; - } - - /** - * Get a list of [identifier, GameConfig] entries i.e. games supported by the mod manager. - */ - static get supportedGames() { - const result: [string, GameConfig][] = [] - for (const [identifier, game] of Object.entries(this.ecosystem.games)) { - if (game.r2modman == null) continue; - for (const entry of game.r2modman) { - result.push([identifier, entry]); - } - } - return result; - } - - static get modloaderPackages() { - return this.ecosystem.modloaderPackages; - } +export const EcosystemSupportedGames = ref<[string, GameConfig][]>([]); +export const EcosystemModloaderPackages = ref([]); - static getGameConfigBySettingsIdentifier(settingsIdentifier: string): GameConfig | undefined { - const config = this.supportedGames.find( - ([_id, config]) => config.settingsIdentifier === settingsIdentifier - ); +export function getGameConfigBySettingsIdentifier(settingsIdentifier: string): GameConfig | undefined { + const config = EcosystemSupportedGames.value.find( + ([_id, config]) => config.settingsIdentifier === settingsIdentifier + ); - return config ? config[1] : undefined; - } + return config ? config[1] : undefined; } diff --git a/src/pages/GameSelectionScreen.vue b/src/pages/GameSelectionScreen.vue index 2221ea650..a601a74f2 100644 --- a/src/pages/GameSelectionScreen.vue +++ b/src/pages/GameSelectionScreen.vue @@ -189,6 +189,7 @@ import { getStore } from '../providers/generic/store/StoreProvider'; import { State } from '../store'; import { useRouter } from 'vue-router'; import ProtocolProvider from '../providers/generic/protocol/ProtocolProvider'; +import {updateEcosystemReactives, updateLatestEcosystemSchema} from "src/r2mm/ecosystem/EcosystemSchema"; const store = getStore(); const router = useRouter(); @@ -203,7 +204,6 @@ const settings = ref(undefined); const isSettingDefaultPlatform = ref(false); const viewMode = ref(GameSelectionViewMode.LIST); const activeTab = ref(GameInstanceType.GAME); -const gameImages = reactive({}); const filteredGameList = computed(() => { const displayNameInAdditionalSearch = (game: Game, filterText: string): boolean => { @@ -369,6 +369,9 @@ onMounted(async () => { await store.dispatch('resetLocalState'); + // TODO - Enable once updating is viable + // updateLatestEcosystemSchema(); + settings.value = await ManagerSettings.getSingleton(GameManager.defaultGame); const globalSettings = settings.value.getContext().global; favourites.value = globalSettings.favouriteGames || []; @@ -384,12 +387,12 @@ onMounted(async () => { } // Skip game selection view if valid default game & platform are set. - const {defaultGame, defaultPlatform} = ManagerUtils.getDefaults(settings.value); + const {defaultGame, defaultPlatform} = ManagerUtils.getDefaults(settings.value!); if (defaultGame && defaultPlatform) { selectedGame.value = defaultGame; selectedPlatform.value = defaultPlatform; - proceed(); + return proceed(); } }) diff --git a/src/r2mm/ecosystem/EcosystemSchema.ts b/src/r2mm/ecosystem/EcosystemSchema.ts new file mode 100644 index 000000000..7764f6d87 --- /dev/null +++ b/src/r2mm/ecosystem/EcosystemSchema.ts @@ -0,0 +1,116 @@ +import bundledEcosystem from "../../assets/data/ecosystem.json"; +import {R2Modman, ThunderstoreEcosystem} from "../../assets/data/ecosystemTypes"; +import jsonSchema from "../../assets/data/ecosystemJsonSchema.json"; +import R2Error from "../../model/errors/R2Error"; +import Ajv from "ajv"; +import addFormats from "ajv-formats"; +import PathResolver from "../manager/PathResolver"; +import path from "../../providers/node/path/path"; +import FsProvider from "../../providers/generic/file/FsProvider"; +import VersionNumber from "../../model/VersionNumber"; +import ManagerInformation from "../../_managerinf/ManagerInformation"; +import {EcosystemModloaderPackages, EcosystemSupportedGames} from "../../model/schema/ThunderstoreSchema"; +import {updateModLoaderExports} from "../installing/profile_installers/ModLoaderVariantRecord"; +import LoggerProvider, {LogSeverity} from "../../providers/ror2/logging/LoggerProvider"; + +export type VersionedThunderstoreEcosystem = ThunderstoreEcosystem & {version: string}; + +async function getMergedEcosystemPath(): Promise { + return path.join(PathResolver.ROOT, "latest-ecosystem-schema.json"); +} + +export async function updateLatestEcosystemSchema(): Promise { + const latestSchema = await fetchLatestSchema(); + await writeLatestEcosystemSchema(latestSchema); + await internalUpdateEcosystemReactives(latestSchema); +} + +async function writeLatestEcosystemSchema(schema: ThunderstoreEcosystem): Promise { + const asMergedSchema: VersionedThunderstoreEcosystem = { + ...schema, + version: ManagerInformation.VERSION.toString(), + }; + const writable = JSON.stringify(asMergedSchema); + return FsProvider.instance.writeFile(await getMergedEcosystemPath(), writable); +} + +async function getLastSavedEcosystemSchema(): Promise { + const contentBuffer = await FsProvider.instance.readFile(await getMergedEcosystemPath()); + const content = contentBuffer.toString("utf8"); + const parsedContent = JSON.parse(content); + await validateSchema(parsedContent); + return parsedContent; +} + +async function validateSchema(schema: any): Promise { + const ajv = new Ajv(); + addFormats(ajv); + + const validate = ajv.compile(jsonSchema); + const isOk = validate(schema); + + if (!isOk) { + throw new R2Error("Schema validation error", ajv.errorsText(validate.errors)); + } +} + +async function loadBundledSchema(): Promise { + await validateSchema(bundledEcosystem); + return bundledEcosystem as ThunderstoreEcosystem; +} + +async function fetchLatestSchema(): Promise { + // TODO - Implement fetching of latest resource + return { + schemaVersion: "", + communities: {}, + games: {}, + modloaderPackages: [], + packageInstallers: {}, + }; +} + +async function resolveCachedEcosystemSchema(): Promise { + const mergeFilePath = await getMergedEcosystemPath(); + const bundledSchema = async () => ({...(await loadBundledSchema()), version: ManagerInformation.VERSION.toString()}); + if (!(await FsProvider.instance.exists(mergeFilePath))) { + return bundledSchema(); + } + try { + let content = await getLastSavedEcosystemSchema(); + if (!new VersionNumber(content.version).isEqualTo(ManagerInformation.VERSION)) { + return bundledSchema(); + } + return content; + } catch (e) { + const err = e as unknown as Error; + LoggerProvider.instance.Log( + LogSeverity.ERROR, + `Failed to load cached ecosystem schema, falling back to bundled schema\n${err.message}` + ); + return bundledSchema(); + } +} + +async function internalUpdateEcosystemReactives(schema: ThunderstoreEcosystem): Promise { + const result: [string, R2Modman][] = [] + for (const [identifier, game] of Object.entries(schema.games)) { + if (game.r2modman == null) continue; + for (const entry of game.r2modman) { + result.push([identifier, entry]); + } + } + EcosystemSupportedGames.value = result; + EcosystemModloaderPackages.value = schema.modloaderPackages; + updateModLoaderExports(); +} + +export async function updateEcosystemReactives() { + const mergedSchema = await resolveCachedEcosystemSchema(); + await internalUpdateEcosystemReactives(mergedSchema); +} + +export async function useBundledForEcosystemReactives() { + const bundledSchema = await loadBundledSchema(); + await internalUpdateEcosystemReactives(bundledSchema); +} diff --git a/src/r2mm/installing/InstallationRules.ts b/src/r2mm/installing/InstallationRules.ts index ddfef5b0a..5a04a61bd 100644 --- a/src/r2mm/installing/InstallationRules.ts +++ b/src/r2mm/installing/InstallationRules.ts @@ -1,4 +1,4 @@ -import { EcosystemSchema, TrackingMethod } from '../../model/schema/ThunderstoreSchema'; +import {EcosystemSupportedGames, TrackingMethod} from '../../model/schema/ThunderstoreSchema'; import path from '../../providers/node/path/path'; export type CoreRuleType = { @@ -37,7 +37,7 @@ export default class InstallationRules { } public static apply() { - this._RULES = EcosystemSchema.supportedGames.map(([_, x]) => ({ + this._RULES = EcosystemSupportedGames.value.map(([_, x]) => ({ gameName: x.internalFolderName, rules: x.installRules, relativeFileExclusions: x.relativeFileExclusions, diff --git a/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts b/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts index d79f1144d..7823399e9 100644 --- a/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts +++ b/src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts @@ -1,17 +1,10 @@ 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, - ), -); +import { + EcosystemModloaderPackages, + EcosystemSupportedGames, + PackageLoader +} from '../../../model/schema/ThunderstoreSchema'; type Modloaders = Record; @@ -28,16 +21,24 @@ const OVERRIDES: Modloaders = { ], } -export const MOD_LOADER_VARIANTS: Modloaders = Object.fromEntries( - EcosystemSchema.supportedGames - .map(([_, game]) => [ - game.internalFolderName, - OVERRIDES[game.internalFolderName] || MODLOADER_PACKAGES - ]) -); +export let MODLOADER_PACKAGES: ModLoaderPackageMapping[] = []; +export let MOD_LOADER_VARIANTS: Modloaders = {}; + +export function updateModLoaderExports() { + MODLOADER_PACKAGES = EcosystemModloaderPackages.value.map((x) => + new ModLoaderPackageMapping(x.packageId, x.rootFolder, x.loader) + ); + MOD_LOADER_VARIANTS = Object.fromEntries( + EcosystemSupportedGames.value + .map(([_, game]) => [ + game.internalFolderName, + OVERRIDES[game.internalFolderName] || MODLOADER_PACKAGES + ]) + ); +} export const getModLoaderPackageNames = () => { - const deduplicated = new Set(EcosystemSchema.modloaderPackages.map((x) => x.packageId)); + const deduplicated = new Set(EcosystemModloaderPackages.value.map((x) => x.packageId)); const names = Array.from(deduplicated); names.sort(); return names; diff --git a/test/vitest/stubs/providers/stub.LoggerProvider.ts b/test/vitest/stubs/providers/stub.LoggerProvider.ts new file mode 100644 index 000000000..525386d90 --- /dev/null +++ b/test/vitest/stubs/providers/stub.LoggerProvider.ts @@ -0,0 +1,10 @@ +import LoggerProvider, { LogSeverity } from "../../../../src/providers/ror2/logging/LoggerProvider"; + +export default class StubLoggerProvider extends LoggerProvider { + public override Log(severity: LogSeverity, error: string): void { + throw new Error("Method not implemented."); + } + override Write(): void { + throw new Error("Method not implemented."); + } +} diff --git a/test/vitest/tests/unit/EcosystemSchema/EcosystemSchema.spec.ts b/test/vitest/tests/unit/EcosystemSchema/EcosystemSchema.spec.ts new file mode 100644 index 000000000..6ffab811f --- /dev/null +++ b/test/vitest/tests/unit/EcosystemSchema/EcosystemSchema.spec.ts @@ -0,0 +1,138 @@ +import * as path from 'path'; +import {afterEach, beforeEach, describe, expect, MockInstance, test, vi} from 'vitest'; + +let mockJsonSchema: object = {}; +vi.mock('../../../../../src/assets/data/ecosystemJsonSchema.json', () => ({ + get default() { return mockJsonSchema; } +})); + +import {VersionedThunderstoreEcosystem, updateEcosystemReactives, updateLatestEcosystemSchema} from '../../../../../src/r2mm/ecosystem/EcosystemSchema'; +import {EcosystemModloaderPackages, EcosystemSupportedGames} from '../../../../../src/model/schema/ThunderstoreSchema'; +import {MODLOADER_PACKAGES, MOD_LOADER_VARIANTS, updateModLoaderExports} from '../../../../../src/r2mm/installing/profile_installers/ModLoaderVariantRecord'; +import InMemoryFsProvider from '../../../stubs/providers/InMemory.FsProvider'; +import FsProvider from '../../../../../src/providers/generic/file/FsProvider'; +import PathResolver from '../../../../../src/r2mm/manager/PathResolver'; +import {providePathImplementation} from '../../../../../src/providers/node/path/path'; +import {TestPathProvider} from '../../../stubs/providers/node/Node.Path.Provider'; +import ManagerInformation from '../../../../../src/_managerinf/ManagerInformation'; +import LoggerProvider from '../../../../../src/providers/ror2/logging/LoggerProvider'; +import StubLoggerProvider from '../../../stubs/providers/stub.LoggerProvider'; + +const TEST_ROOT = 'TEST_ROOT'; +const latestSchemaFilePath = path.join(TEST_ROOT, 'latest-ecosystem-schema.json'); + +async function writeCacheFile(schema: Partial) { + const full: VersionedThunderstoreEcosystem = { + schemaVersion: '0.0.0', + communities: {}, + games: {}, + modloaderPackages: [], + packageInstallers: {}, + version: ManagerInformation.VERSION.toString(), + ...schema, + }; + await FsProvider.instance.writeFile(latestSchemaFilePath, JSON.stringify(full)); +} + +describe('EcosystemSchema', () => { + + let spyLogger: MockInstance; + + beforeEach(async () => { + mockJsonSchema = {}; + providePathImplementation(() => TestPathProvider); + InMemoryFsProvider.clear(); + FsProvider.provide(() => new InMemoryFsProvider()); + PathResolver.ROOT = TEST_ROOT; + EcosystemSupportedGames.value = []; + EcosystemModloaderPackages.value = []; + const loggerImpl = new StubLoggerProvider(); + LoggerProvider.provide(() => loggerImpl); + spyLogger = vi.spyOn(LoggerProvider.instance, 'Log').mockImplementation(() => {}); + updateModLoaderExports(); + await FsProvider.instance.mkdirs(TEST_ROOT); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }) + + describe('updateEcosystemReactives', () => { + + test('falls back to bundled schema when no cache file exists', async () => { + + expect(EcosystemSupportedGames.value).toHaveLength(0); + expect(EcosystemModloaderPackages.value).toHaveLength(0); + expect(await FsProvider.instance.exists(latestSchemaFilePath)).toBe(false); + + await updateEcosystemReactives(); + + expect(await FsProvider.instance.exists(latestSchemaFilePath)).toBe(false); + expect(EcosystemSupportedGames.value.length).toBeGreaterThan(0); + expect(EcosystemModloaderPackages.value.length).toBeGreaterThan(0); + }); + + test('loads from cache when version matches', async () => { + expect(EcosystemSupportedGames.value).toHaveLength(0); + expect(await FsProvider.instance.exists(latestSchemaFilePath)).toBe(false); + + await writeCacheFile({games: {}, modloaderPackages: []}); + await updateEcosystemReactives(); + + expect(await FsProvider.instance.exists(latestSchemaFilePath)).toBe(true); + expect(EcosystemSupportedGames.value).toHaveLength(0); + }); + + test('falls back to bundled schema when cached version is stale', async () => { + expect(EcosystemSupportedGames.value).toHaveLength(0); + await writeCacheFile({version: '0.0.0'}); + + await updateEcosystemReactives(); + + expect(EcosystemSupportedGames.value.length).toBeGreaterThan(0); + }); + + test('falls back to bundled schema and logs error when cached content is invalid', async () => { + await FsProvider.instance.writeFile(latestSchemaFilePath, 'not valid json'); + + await updateEcosystemReactives(); + + expect(EcosystemSupportedGames.value.length).toBeGreaterThan(0); + expect(spyLogger).toHaveBeenCalledOnce(); + }); + + test('populates mod loader exports after updating reactives', async () => { + expect(MODLOADER_PACKAGES.length).toBe(0); + expect(Object.keys(MOD_LOADER_VARIANTS).length).toBe(0); + + await updateEcosystemReactives(); + + expect(MODLOADER_PACKAGES.length).toBeGreaterThan(0); + expect(Object.keys(MOD_LOADER_VARIANTS).length).toBeGreaterThan(0); + }); + + }); + + describe('updateLatestEcosystemSchema', () => { + + test('writes file to disk', async () => { + expect(await FsProvider.instance.exists(latestSchemaFilePath)).toBe(false); + + await updateLatestEcosystemSchema(); + + expect(await FsProvider.instance.exists(latestSchemaFilePath)).toBe(true); + }); + + test('written file contains current version', async () => { + expect(await FsProvider.instance.exists(latestSchemaFilePath)).toBe(false); + + await updateLatestEcosystemSchema(); + + const content = (await FsProvider.instance.readFile(latestSchemaFilePath)).toString('utf8'); + const parsed: VersionedThunderstoreEcosystem = JSON.parse(content); + expect(parsed.version).toBe(ManagerInformation.VERSION.toString()); + }); + + }); + +}); diff --git a/test/vitest/tests/unit/GameDirectoryResolver/GameDirectoryResolver.spec.ts b/test/vitest/tests/unit/GameDirectoryResolver/GameDirectoryResolver.spec.ts index 49e46bc81..d62a0a174 100644 --- a/test/vitest/tests/unit/GameDirectoryResolver/GameDirectoryResolver.spec.ts +++ b/test/vitest/tests/unit/GameDirectoryResolver/GameDirectoryResolver.spec.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, test } from 'vitest'; +import {beforeAll, beforeEach, describe, expect, test} from 'vitest'; import path from 'path'; import FsProvider from '../../../../../src/providers/generic/file/FsProvider'; import GameManager from '../../../../../src/model/game/GameManager'; @@ -6,16 +6,21 @@ import GameDirectoryResolverImpl from '../../../../../src/r2mm/manager/win32/Gam import InMemoryFsProvider from '../../../stubs/providers/InMemory.FsProvider'; import { providePathImplementation } from '../../../../../src/providers/node/path/path'; import { TestPathProvider } from '../../../stubs/providers/node/Node.Path.Provider'; +import {updateEcosystemReactives} from "src/r2mm/ecosystem/EcosystemSchema"; describe.skipIf(process.platform !== 'win32')('GameDirectoryResolver', () => { const steamRoot = 'TEST_STEAM_PATH'; let resolver: GameDirectoryResolverImpl; - beforeEach(async () => { - providePathImplementation(() => TestPathProvider); + beforeAll(async () => { const inMemoryFs = new InMemoryFsProvider(); FsProvider.provide(() => inMemoryFs); InMemoryFsProvider.clear(); + providePathImplementation(() => TestPathProvider); + await updateEcosystemReactives(); + }) + + beforeEach(async () => { resolver = new GameDirectoryResolverImpl(); await FsProvider.instance.mkdirs(path.join(steamRoot, 'steamapps', 'common')); }); @@ -55,4 +60,4 @@ describe.skipIf(process.platform !== 'win32')('GameDirectoryResolver', () => { expect(result).toBe(path.join(steamRoot, 'steamapps', 'common', 'Very Very Valet', 'windows')); }); -}); \ No newline at end of file +}); 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..4be3d8536 100644 --- a/test/vitest/tests/unit/Installers/ModLoader/Installer.Tests.spec.ts +++ b/test/vitest/tests/unit/Installers/ModLoader/Installer.Tests.spec.ts @@ -7,7 +7,7 @@ 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 { getGameConfigBySettingsIdentifier, 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'; @@ -186,7 +186,7 @@ describe('Installer Tests', () => { ProfileInstallerProvider.provide(() => new GenericProfileInstaller()); await ProfileInstallerProvider.instance.installMod(pkg, Profile.getActiveProfile().asImmutableProfile()); - const config = EcosystemSchema.getGameConfigBySettingsIdentifier(GameManager.activeGame.internalFolderName); + const config = getGameConfigBySettingsIdentifier(GameManager.activeGame.internalFolderName); if (config === undefined) { throw new Error(`Game config not found for ${GameManager.activeGame.internalFolderName}`); } diff --git a/test/vitest/tests/unit/MelonLoader/state.spec.ts b/test/vitest/tests/unit/MelonLoader/state.spec.ts index a41bc7ec3..4d4d364c5 100644 --- a/test/vitest/tests/unit/MelonLoader/state.spec.ts +++ b/test/vitest/tests/unit/MelonLoader/state.spec.ts @@ -13,10 +13,12 @@ import GenericProfileInstaller from '../../../../../src/r2mm/installing/profile_ import GameManager from '../../../../../src/model/game/GameManager'; import ConflictManagementProvider from '../../../../../src/providers/generic/installing/ConflictManagementProvider'; import { addToStateFile } from '../../../../../src/installers/InstallRulePluginInstaller'; -import { describe, beforeEach, afterEach, test, expect } from 'vitest'; +import {describe, beforeEach, afterEach, test, expect, beforeAll} from 'vitest'; import {providePathImplementation} from "../../../../../src/providers/node/path/path"; import {TestPathProvider} from "../../../stubs/providers/node/Node.Path.Provider"; import StubProfileProvider from '../../../stubs/providers/stub.ProfileProvider'; +import {updateEcosystemReactives} from "src/r2mm/ecosystem/EcosystemSchema"; +import InMemoryFsProvider from "../../../stubs/providers/InMemory.FsProvider"; providePathImplementation(() => TestPathProvider); @@ -41,6 +43,13 @@ describe("State testing", () => { describe("State file", () => { + beforeAll(async () => { + const inMemoryFs = new InMemoryFsProvider(); + FsProvider.provide(() => inMemoryFs); + InMemoryFsProvider.clear(); + await updateEcosystemReactives(); + }) + beforeEach(beforeSetup); afterEach(() => { sandbox.restore(); @@ -66,7 +75,7 @@ describe("State testing", () => { profile ); - const out = fsStub.writeFile.args[0][1] as unknown as string; + const out = fsStub.writeFile.args[0]![1] as unknown as string; const parsed = yaml.parse(out) as ModFileTracker; expect(parsed.modName).toBe(fakeMod.getName()); // JSON serialization for speed comparison @@ -135,7 +144,7 @@ describe("State testing", () => { const outputState = await processConflictManagement(modA, modFileTrackerA, modB, modFileTrackerB); - expect(outputState.currentState.join(",")).toBe([[modFileTrackerA.files[0][1], modA.getName()], [modFileTrackerB.files[0][1], modB.getName()]].join(",")); + expect(outputState.currentState.join(",")).toBe([[modFileTrackerA.files[0]![1], modA.getName()], [modFileTrackerB.files[0]![1], modB.getName()]].join(",")); }); @@ -159,7 +168,7 @@ describe("State testing", () => { const outputState = await processConflictManagement(modA, modFileTrackerA, modB, modFileTrackerB); - expect(outputState.currentState.join(",")).toBe([[modFileTrackerB.files[0][1], modB.getName()]].join(",")); + expect(outputState.currentState.join(",")).toBe([[modFileTrackerB.files[0]![1], modB.getName()]].join(",")); }); // If a mod is disabled, the mod files should not exist if highest priority for those files. @@ -182,7 +191,7 @@ describe("State testing", () => { const outputState = await processConflictManagement(modA, modFileTrackerA, modB, modFileTrackerB); - expect(outputState.currentState.join(",")).toBe([[`${modFileTrackerA.files[0][1]}.manager.disabled`, modA.getName()], [modFileTrackerB.files[0][1], modB.getName()]].join(",")); + expect(outputState.currentState.join(",")).toBe([[`${modFileTrackerA.files[0]![1]}.manager.disabled`, modA.getName()], [modFileTrackerB.files[0]![1], modB.getName()]].join(",")); }); // If a mod is disabled, any lower priority files should not be overwritten. @@ -205,7 +214,7 @@ describe("State testing", () => { const outputState = await processConflictManagement(modA, modFileTrackerA, modB, modFileTrackerB); - expect(outputState.currentState.join(",")).toBe([[modFileTrackerA.files[0][1], modA.getName()], [`${modFileTrackerB.files[0][1]}.manager.disabled`, modB.getName()]].join(",")); + expect(outputState.currentState.join(",")).toBe([[modFileTrackerA.files[0]![1], modA.getName()], [`${modFileTrackerB.files[0]![1]}.manager.disabled`, modB.getName()]].join(",")); }); }); @@ -247,5 +256,5 @@ let processConflictManagement = async (modA: ManifestV2, modFileTrackerA: ModFil await conflictManagement.resolveConflicts([modA, modB], profile.asImmutableProfile()); - return yaml.parse(fsStub.writeFile.args[0][1] as unknown as string) as StateTracker; + return yaml.parse(fsStub.writeFile.args[0]![1] as unknown as string) as StateTracker; } diff --git a/test/vitest/tests/unit/ModLinker/ModLinker.spec.ts b/test/vitest/tests/unit/ModLinker/ModLinker.spec.ts index c3a0f72e2..e1a9151f6 100644 --- a/test/vitest/tests/unit/ModLinker/ModLinker.spec.ts +++ b/test/vitest/tests/unit/ModLinker/ModLinker.spec.ts @@ -16,6 +16,7 @@ import { providePathImplementation } from '../../../../../src/providers/node/pat import { TestPathProvider } from '../../../stubs/providers/node/Node.Path.Provider'; import { provideAppWindowImplementation } from '../../../../../src/providers/node/app/app_window'; import { TestAppWindowProvider } from '../../../stubs/providers/node/AppWindow.Provider'; +import {updateEcosystemReactives} from "src/r2mm/ecosystem/EcosystemSchema"; class ProfileProviderImpl extends ProfileProvider { ensureProfileDirectory(directory: string, profile: string): void { @@ -36,6 +37,9 @@ describe.skipIf(process.platform !== 'win32')('ModLinker', async () => { const inMemoryFs = new InMemoryFsProvider(); FsProvider.provide(() => inMemoryFs); InMemoryFsProvider.clear(); + + await updateEcosystemReactives(); + PathResolver.MOD_ROOT = 'MODS'; await inMemoryFs.mkdirs(PathResolver.MOD_ROOT); ProfileProvider.provide(() => new ProfileProviderImpl()); diff --git a/test/vitest/tests/unit/Profile/Profile.ts.spec.ts b/test/vitest/tests/unit/Profile/Profile.ts.spec.ts index 3e305bf88..414dd9b9d 100644 --- a/test/vitest/tests/unit/Profile/Profile.ts.spec.ts +++ b/test/vitest/tests/unit/Profile/Profile.ts.spec.ts @@ -9,6 +9,7 @@ import PathResolver from '../../../../../src/r2mm/manager/PathResolver'; import { beforeAll, describe, expect, test } from 'vitest'; import { providePathImplementation } from '../../../../../src/providers/node/path/path'; import { TestPathProvider } from '../../../stubs/providers/node/Node.Path.Provider'; +import { updateEcosystemReactives } from "src/r2mm/ecosystem/EcosystemSchema"; class ProfileProviderImpl extends ProfileProvider { @@ -34,6 +35,7 @@ describe("ImmutableProfile", () => { FsProvider.provide(() => inMemoryFs); ProfileProvider.provide(() => new ProfileProviderImpl()); InMemoryFsProvider.clear(); + await updateEcosystemReactives(); }); test("Initializing ImmutableProfile doesn't affect Profile", () => { diff --git a/test/vitest/utils/InstallLogicUtils.ts b/test/vitest/utils/InstallLogicUtils.ts index b93023435..9d3dafe87 100644 --- a/test/vitest/utils/InstallLogicUtils.ts +++ b/test/vitest/utils/InstallLogicUtils.ts @@ -19,6 +19,7 @@ import PathResolver from '../../../src/r2mm/manager/PathResolver'; import {providePathImplementation} from "../../../src/providers/node/path/path"; import {TestPathProvider} from "../stubs/providers/node/Node.Path.Provider"; import {expect} from 'vitest'; +import {updateEcosystemReactives} from "../../../src/r2mm/ecosystem/EcosystemSchema"; class ProfileProviderImpl extends ProfileProvider { ensureProfileDirectory(directory: string, profile: string): void { @@ -36,6 +37,7 @@ export async function installLogicBeforeEach(internalFolderName: string) { providePathImplementation(() => TestPathProvider); const inMemoryFs = new InMemoryFsProvider(); FsProvider.provide(() => inMemoryFs); + await updateEcosystemReactives(); InMemoryFsProvider.clear(); PathResolver.MOD_ROOT = 'MODS'; await inMemoryFs.mkdirs(PathResolver.MOD_ROOT);