From 6231d73109e6821d58b98755c666b33f52a3115d Mon Sep 17 00:00:00 2001 From: TheSparta Date: Wed, 18 Oct 2023 14:44:29 +0000 Subject: [PATCH] Implemented loadBefore and loadAfter (!156) This PR adds the ability to set `loadBefore` and `loadAfter` on a mod's package.json, this allows for modders to define an array of mods their current mod needs to load before or after. Examples: if we have __MOD1__ that has `loadAfter` = `[ "MOD2" ]` the loading order would be: 1 - MOD2 2 - MOD1 if we have __MOD2__ that has `loadBefore` = `[ "MOD1" ]` the loading order would also be: 1 - MOD2 2 - MOD1 Begone zzzzzz, name your mods the way you want to. Reviewed-on: https://dev.sp-tarkov.com/SPT-AKI/Server/pulls/156 Co-authored-by: TheSparta Co-committed-by: TheSparta --- project/src/di/Container.ts | 2 + project/src/loaders/ModLoadOrder.ts | 152 ++++++++++++++++++ project/src/loaders/PreAkiModLoader.ts | 74 +-------- .../src/models/spt/mod/IPackageJsonData.ts | 30 ++-- 4 files changed, 176 insertions(+), 82 deletions(-) create mode 100644 project/src/loaders/ModLoadOrder.ts diff --git a/project/src/di/Container.ts b/project/src/di/Container.ts index 99175560..1f6cbf57 100644 --- a/project/src/di/Container.ts +++ b/project/src/di/Container.ts @@ -122,6 +122,7 @@ import { TraderHelper } from "../helpers/TraderHelper"; import { UtilityHelper } from "../helpers/UtilityHelper"; import { WeightedRandomHelper } from "../helpers/WeightedRandomHelper"; import { BundleLoader } from "../loaders/BundleLoader"; +import { ModLoadOrder } from "../loaders/ModLoadOrder"; import { ModTypeCheck } from "../loaders/ModTypeCheck"; import { PostAkiModLoader } from "../loaders/PostAkiModLoader"; import { PostDBModLoader } from "../loaders/PostDBModLoader"; @@ -383,6 +384,7 @@ export class Container depContainer.register("AsyncQueue", AsyncQueue, { lifecycle: Lifecycle.Singleton }); depContainer.register("UUidGenerator", UUidGenerator, { lifecycle: Lifecycle.Singleton }); depContainer.register("HttpFileUtil", HttpFileUtil, { lifecycle: Lifecycle.Singleton }); + depContainer.register("ModLoadOrder", ModLoadOrder, { lifecycle: Lifecycle.Singleton }); depContainer.register("ModTypeCheck", ModTypeCheck, { lifecycle: Lifecycle.Singleton }); } diff --git a/project/src/loaders/ModLoadOrder.ts b/project/src/loaders/ModLoadOrder.ts new file mode 100644 index 00000000..22353373 --- /dev/null +++ b/project/src/loaders/ModLoadOrder.ts @@ -0,0 +1,152 @@ +import { injectable } from "tsyringe"; +import { IPackageJsonData } from "../models/spt/mod/IPackageJsonData"; + +@injectable() +export class ModLoadOrder +{ + protected mods = new Map(); + protected modsAvailable = new Map(); + protected loadOrder = new Set(); + + public setModList(mods: Record): void + { + this.mods = new Map(Object.entries(mods)); + this.modsAvailable = structuredClone(this.mods); + this.loadOrder = new Set(); + + const visited = new Set(); + + //invert loadBefore into loadAfter on specified mods + for (const [ modName, modConfig ] of this.modsAvailable) + { + if ((modConfig.loadBefore ?? []).length > 0) + { + this.invertLoadBefore(modName); + } + } + + for (const modName of this.modsAvailable.keys()) + { + this.getLoadOrderRecursive(modName, visited); + } + } + + public getLoadOrder(): string[] + { + return Array.from(this.loadOrder); + } + + public getModsOnLoadBefore(mod: string): Set + { + if (!this.mods.has(mod)) + { + throw new Error(`Mod: ${mod} isn't present.`); + } + + const config = this.mods.get(mod); + + const loadBefore = new Set(config.loadBefore); + + for (const loadBeforeMod of loadBefore) + { + if (!this.mods.has(loadBeforeMod)) + { + loadBefore.delete(loadBeforeMod); + } + } + + return loadBefore; + } + + public getModsOnLoadAfter(mod: string): Set + { + if (!this.mods.has(mod)) + { + throw new Error(`Mod: ${mod} isn't present.`); + } + + const config = this.mods.get(mod); + + const loadAfter = new Set(config.loadAfter); + + for (const loadAfterMod of loadAfter) + { + if (!this.mods.has(loadAfterMod)) + { + loadAfter.delete(loadAfterMod); + } + } + + return loadAfter; + } + + protected invertLoadBefore(mod: string): void + { + if (!this.modsAvailable.has(mod)) + { + console.log("missing mod", mod); + throw new Error("MISSING DEPENDENCY"); + } + + const loadBefore = this.getModsOnLoadBefore(mod); + + for (const loadBeforeMod of loadBefore) + { + const loadBeforeModConfig = this.modsAvailable.get(loadBeforeMod)!; + + loadBeforeModConfig.loadAfter ??= []; + loadBeforeModConfig.loadAfter.push(mod); + + this.modsAvailable.set(loadBeforeMod, loadBeforeModConfig); + } + } + + protected getLoadOrderRecursive(mod: string, visited: Set): void + { + // validate package + if (this.loadOrder.has(mod)) + { + return; + } + + if (visited.has(mod)) + { + console.log("current mod", mod); + console.log("result", JSON.stringify(this.loadOrder, null, "\t")); + console.log("visited", JSON.stringify(visited, null, "\t")); + throw new Error("CYCLIC DEPENDENCY"); + } + + // check dependencies + if (!this.modsAvailable.has(mod)) + { + console.log("missing mod", mod); + throw new Error("MISSING DEPENDENCY"); + } + + const config = this.modsAvailable.get(mod); + + config.loadAfter ??= []; + config.modDependencies ??= {}; + + const loadAfter = new Set(Object.keys(config.modDependencies)); + + for (const after of config.loadAfter) + { + if (this.modsAvailable.has(after)) + { + loadAfter.add(after); + } + } + + visited.add(mod); + + for (const mod of loadAfter) + { + this.getLoadOrderRecursive(mod, visited); + } + + visited.delete(mod); + this.loadOrder.add(mod); + } +} diff --git a/project/src/loaders/PreAkiModLoader.ts b/project/src/loaders/PreAkiModLoader.ts index a7417629..91e3e700 100644 --- a/project/src/loaders/PreAkiModLoader.ts +++ b/project/src/loaders/PreAkiModLoader.ts @@ -17,6 +17,7 @@ import { ModCompilerService } from "../services/ModCompilerService"; import { JsonUtil } from "../utils/JsonUtil"; import { VFS } from "../utils/VFS"; import { BundleLoader } from "./BundleLoader"; +import { ModLoadOrder } from "./ModLoadOrder"; import { ModTypeCheck } from "./ModTypeCheck"; @injectable() @@ -40,6 +41,7 @@ export class PreAkiModLoader implements IModLoader @inject("BundleLoader") protected bundleLoader: BundleLoader, @inject("LocalisationService") protected localisationService: LocalisationService, @inject("ConfigServer") protected configServer: ConfigServer, + @inject("ModLoadOrder") protected modLoadOrder: ModLoadOrder, @inject("ModTypeCheck") protected modTypeCheck: ModTypeCheck ) { @@ -206,6 +208,8 @@ export class PreAkiModLoader implements IModLoader { await this.addMod(mod); } + + this.modLoadOrder.setModList(this.imported); } protected sortMods(prev: string, next: string, missingFromOrderJSON: Record): number @@ -380,7 +384,7 @@ export class PreAkiModLoader implements IModLoader } else { - return Object.keys(this.getLoadOrder(this.imported)); + return this.modLoadOrder.getLoadOrder(); } } @@ -623,72 +627,6 @@ export class PreAkiModLoader implements IModLoader return !issue; } - protected getLoadOrderRecursive(mod: string, result: Record, visited: Record): void - { - // validate package - if (mod in result) - { - return; - } - - if (mod in visited) - { - // front: white, back: red - this.logger.error(this.localisationService.getText("modloader-cyclic_dependency")); - - // additional info - this.logger.debug(this.localisationService.getText("modloader-checking_mod", mod)); - this.logger.debug(`${this.localisationService.getText("modloader-checked")}:`); - this.logger.debug(result); - this.logger.debug(`${this.localisationService.getText("modloader-visited")}:`); - this.logger.debug(visited); - - // wait for input - process.exit(1); - } - - // check dependencies - const config = this.imported[mod]; - - if (typeof config === "undefined") - { - this.logger.error(this.localisationService.getText("modloader-missing_dependency")); - throw new Error(this.localisationService.getText("modloader-error_parsing_mod_load_order")); - } - - const dependencies: Record = config.dependencies || {}; - - visited[mod] = config.version; - - for (const dependency in dependencies) - { - this.getLoadOrderRecursive(dependency, result, visited); - } - - delete visited[mod]; - - // fully checked package - result[mod] = config.version; - } - - protected getLoadOrder(mods: Record): Record - { - const result: Record = {}; - const visited: Record = {}; - - for (const mod in mods) - { - if (mods[mod][0] in result) - { - continue; - } - - this.getLoadOrderRecursive(mod, result, visited); - } - - return result; - } - public getContainer(): DependencyContainer { if (PreAkiModLoader.container) @@ -700,4 +638,4 @@ export class PreAkiModLoader implements IModLoader throw new Error(this.localisationService.getText("modloader-dependency_container_not_initalized")); } } -} \ No newline at end of file +} diff --git a/project/src/models/spt/mod/IPackageJsonData.ts b/project/src/models/spt/mod/IPackageJsonData.ts index 11968008..74a1b9c8 100644 --- a/project/src/models/spt/mod/IPackageJsonData.ts +++ b/project/src/models/spt/mod/IPackageJsonData.ts @@ -1,17 +1,19 @@ export interface IPackageJsonData { - incompatibilities?: string[] - dependencies?: Record - modDependencies?: Record - name: string - author: string - version: string - akiVersion: string + incompatibilities?: string[]; + loadBefore?: string[]; + loadAfter?: string[]; + dependencies?: Record; + modDependencies?: Record; + name: string; + author: string; + version: string; + akiVersion: string; /** We deliberately purge this data */ - scripts: Record - devDependencies: Record - licence: string - main: string - isBundleMod: boolean - contributors: string[] -} \ No newline at end of file + scripts: Record; + devDependencies: Record; + licence: string; + main: string; + isBundleMod: boolean; + contributors: string[]; +}