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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion quasar.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions src/boot/ecosystem.ts
Original file line number Diff line number Diff line change
@@ -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);
Comment on lines +8 to +11
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really awkward but I presume it's necessary?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically no, because we bind to the same provider later anyway, it's more just to keep a separation of concerns so that providers are all bound in the place you'd expect.

On the flip side, this (#2116) removes the need entirely and we can have all providers set before this is even ran.

Comment on lines +8 to +11
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't cause any issues because the intent is to set it when needed, then allow it to be re-set as part of the ordinary workflow.

A separate PR addresses the concern and removes the need for the unset.

});
4 changes: 2 additions & 2 deletions src/installers/InstallRulePluginInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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}`);
}
Expand Down
4 changes: 2 additions & 2 deletions src/model/game/GameManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down
65 changes: 9 additions & 56 deletions src/model/schema/ThunderstoreSchema.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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<ModloaderPackage[]>([]);

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;
}
9 changes: 6 additions & 3 deletions src/pages/GameSelectionScreen.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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<State>();
const router = useRouter();
Expand All @@ -203,7 +204,6 @@ const settings = ref<ManagerSettings | undefined>(undefined);
const isSettingDefaultPlatform = ref<boolean>(false);
const viewMode = ref<GameSelectionViewMode>(GameSelectionViewMode.LIST);
const activeTab = ref<GameInstanceType>(GameInstanceType.GAME);
const gameImages = reactive({});

const filteredGameList = computed(() => {
const displayNameInAdditionalSearch = (game: Game, filterText: string): boolean => {
Expand Down Expand Up @@ -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 || [];
Expand All @@ -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();
}
})

Expand Down
111 changes: 111 additions & 0 deletions src/r2mm/ecosystem/EcosystemSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
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<string> {
return path.join(PathResolver.ROOT, "latest-ecosystem-schema.json");
}
Comment on lines +18 to +20
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated PR description


export async function updateLatestEcosystemSchema(): Promise<void> {
const latestSchema = await fetchLatestSchema();
await writeLatestEcosystemSchema(latestSchema);
await internalUpdateEcosystemReactives(latestSchema);
}

async function writeLatestEcosystemSchema(schema: ThunderstoreEcosystem): Promise<void> {
const asMergedSchema: VersionedThunderstoreEcosystem = {
...schema,
version: ManagerInformation.VERSION.toString(),
};
const writable = JSON.stringify(asMergedSchema);
return FsProvider.instance.writeFile(await getMergedEcosystemPath(), writable);
}

async function getLastSavedEcosystemSchema(): Promise<VersionedThunderstoreEcosystem> {
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<void> {
const ajv = new Ajv();
addFormats(ajv);

const validate = ajv.compile(jsonSchema);
const isOk = validate(bundledEcosystem);

if (!isOk) {
throw new R2Error("Schema validation error", ajv.errorsText(validate.errors));
}
}

async function loadBundledSchema(): Promise<ThunderstoreEcosystem> {
await validateSchema(bundledEcosystem);
return bundledEcosystem as ThunderstoreEcosystem;
}

async function fetchLatestSchema(): Promise<ThunderstoreEcosystem> {
// TODO - Implement fetching of latest resource
return {
schemaVersion: "",
communities: {},
games: {},
modloaderPackages: [],
packageInstallers: {},
};
}

async function resolveCachedEcosystemSchema(): Promise<VersionedThunderstoreEcosystem> {
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) {
Comment on lines +79 to +85
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();
Comment on lines +86 to +91
}
}

async function internalUpdateEcosystemReactives(schema: ThunderstoreEcosystem): Promise<void> {
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);
}
4 changes: 2 additions & 2 deletions src/r2mm/installing/InstallationRules.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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,
Expand Down
41 changes: 21 additions & 20 deletions src/r2mm/installing/profile_installers/ModLoaderVariantRecord.ts
Original file line number Diff line number Diff line change
@@ -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<string, ModLoaderPackageMapping[]>;

Expand All @@ -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
])
);
Comment on lines +28 to +37
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not an issue because as part of the Quasar boot files, we initialise this.

It isn't until render + load of the GameSelectionScreen.vue that this is then applicable.

}

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;
Expand Down
Loading
Loading