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);
});
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
24 changes: 24 additions & 0 deletions src/model/game/GameSupportStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {Loader, ModloaderPackage, Platform, R2Modman} from "../../assets/data/ecosystemTypes";

export type GameSupportStatus = "supported" | "unsupported-loader" | "unsupported-store";

const KNOWN_PLATFORMS = new Set<string>(Object.values(Platform));

export function getGameSupportStatus(
game: R2Modman,
modloaderPackages: ModloaderPackage[]
): GameSupportStatus {
if (game.packageLoader !== Loader.NONE) {
const hasLoader = modloaderPackages.some(p => p.loader === game.packageLoader);
if (!hasLoader) {
return "unsupported-loader";
}
}

const hasKnownPlatform = game.distributions.some(d => KNOWN_PLATFORMS.has(d.platform));
if (!hasKnownPlatform) {
return "unsupported-store";
}

return "supported";
}
55 changes: 4 additions & 51 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, ThunderstoreEcosystem } from "../../assets/data/ecosystemTypes";
import jsonSchema from "../../assets/data/ecosystemJsonSchema.json";
import R2Error from "../errors/R2Error";
import {ModloaderPackage, R2Modman} 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,47 +11,5 @@ 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, r2modman] entries i.e. games supported by the mod manager.
*/
static get supportedGames() {
const result: [string, R2Modman][] = []
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, R2Modman][]>([]);
export const EcosystemModloaderPackages = ref<ModloaderPackage[]>([]);
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
109 changes: 109 additions & 0 deletions src/r2mm/ecosystem/EcosystemSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
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 {getGameSupportStatus} from "../../model/game/GameSupportStatus";
import LoggerProvider, {LogSeverity} from "src/providers/ror2/logging/LoggerProvider";

export type VersionedThunderstoreEcosystem = ThunderstoreEcosystem & {version: string};

async function getMergedEcosystemPath(): Promise<string> {
return path.join(PathResolver.ROOT, "latest-ecosystem-schema.json");
}

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");
return JSON.parse(content);
}

async function loadBundledSchema(): Promise<ThunderstoreEcosystem> {
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));
}

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();
}
let content: VersionedThunderstoreEcosystem;
try {
content = await getLastSavedEcosystemSchema();
} 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();
}
if (!new VersionNumber(content.version).isEqualTo(ManagerInformation.VERSION)) {
return bundledSchema();
}
return content;
}

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) {
if (getGameSupportStatus(entry, schema.modloaderPackages) !== "supported") continue;
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,6 +1,6 @@
import { getPluginInstaller } from '../../installers/registry';
import GameManager from '../../model/game/GameManager';
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 @@ -39,7 +39,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
])
);
}

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