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 <u>__MOD1__</u> that has `loadAfter` = `[ "MOD2" ]` the loading order would be: 1 - MOD2 2 - MOD1 if we have <u>__MOD2__</u> 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 <thesparta@noreply.dev.sp-tarkov.com> Co-committed-by: TheSparta <thesparta@noreply.dev.sp-tarkov.com>
This commit is contained in:
parent
1adeed29ef
commit
6231d73109
@ -122,6 +122,7 @@ import { TraderHelper } from "../helpers/TraderHelper";
|
|||||||
import { UtilityHelper } from "../helpers/UtilityHelper";
|
import { UtilityHelper } from "../helpers/UtilityHelper";
|
||||||
import { WeightedRandomHelper } from "../helpers/WeightedRandomHelper";
|
import { WeightedRandomHelper } from "../helpers/WeightedRandomHelper";
|
||||||
import { BundleLoader } from "../loaders/BundleLoader";
|
import { BundleLoader } from "../loaders/BundleLoader";
|
||||||
|
import { ModLoadOrder } from "../loaders/ModLoadOrder";
|
||||||
import { ModTypeCheck } from "../loaders/ModTypeCheck";
|
import { ModTypeCheck } from "../loaders/ModTypeCheck";
|
||||||
import { PostAkiModLoader } from "../loaders/PostAkiModLoader";
|
import { PostAkiModLoader } from "../loaders/PostAkiModLoader";
|
||||||
import { PostDBModLoader } from "../loaders/PostDBModLoader";
|
import { PostDBModLoader } from "../loaders/PostDBModLoader";
|
||||||
@ -383,6 +384,7 @@ export class Container
|
|||||||
depContainer.register<IAsyncQueue>("AsyncQueue", AsyncQueue, { lifecycle: Lifecycle.Singleton });
|
depContainer.register<IAsyncQueue>("AsyncQueue", AsyncQueue, { lifecycle: Lifecycle.Singleton });
|
||||||
depContainer.register<IUUidGenerator>("UUidGenerator", UUidGenerator, { lifecycle: Lifecycle.Singleton });
|
depContainer.register<IUUidGenerator>("UUidGenerator", UUidGenerator, { lifecycle: Lifecycle.Singleton });
|
||||||
depContainer.register<HttpFileUtil>("HttpFileUtil", HttpFileUtil, { lifecycle: Lifecycle.Singleton });
|
depContainer.register<HttpFileUtil>("HttpFileUtil", HttpFileUtil, { lifecycle: Lifecycle.Singleton });
|
||||||
|
depContainer.register<ModLoadOrder>("ModLoadOrder", ModLoadOrder, { lifecycle: Lifecycle.Singleton });
|
||||||
depContainer.register<ModTypeCheck>("ModTypeCheck", ModTypeCheck, { lifecycle: Lifecycle.Singleton });
|
depContainer.register<ModTypeCheck>("ModTypeCheck", ModTypeCheck, { lifecycle: Lifecycle.Singleton });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
152
project/src/loaders/ModLoadOrder.ts
Normal file
152
project/src/loaders/ModLoadOrder.ts
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import { injectable } from "tsyringe";
|
||||||
|
import { IPackageJsonData } from "../models/spt/mod/IPackageJsonData";
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export class ModLoadOrder
|
||||||
|
{
|
||||||
|
protected mods = new Map<string, IPackageJsonData>();
|
||||||
|
protected modsAvailable = new Map<string, IPackageJsonData>();
|
||||||
|
protected loadOrder = new Set<string>();
|
||||||
|
|
||||||
|
public setModList(mods: Record<string, IPackageJsonData>): void
|
||||||
|
{
|
||||||
|
this.mods = new Map<string, IPackageJsonData>(Object.entries(mods));
|
||||||
|
this.modsAvailable = structuredClone(this.mods);
|
||||||
|
this.loadOrder = new Set<string>();
|
||||||
|
|
||||||
|
const visited = new Set<string>();
|
||||||
|
|
||||||
|
//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<string>
|
||||||
|
{
|
||||||
|
if (!this.mods.has(mod))
|
||||||
|
{
|
||||||
|
throw new Error(`Mod: ${mod} isn't present.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = this.mods.get(mod);
|
||||||
|
|
||||||
|
const loadBefore = new Set<string>(config.loadBefore);
|
||||||
|
|
||||||
|
for (const loadBeforeMod of loadBefore)
|
||||||
|
{
|
||||||
|
if (!this.mods.has(loadBeforeMod))
|
||||||
|
{
|
||||||
|
loadBefore.delete(loadBeforeMod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadBefore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getModsOnLoadAfter(mod: string): Set<string>
|
||||||
|
{
|
||||||
|
if (!this.mods.has(mod))
|
||||||
|
{
|
||||||
|
throw new Error(`Mod: ${mod} isn't present.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = this.mods.get(mod);
|
||||||
|
|
||||||
|
const loadAfter = new Set<string>(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<string>): 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<string>(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);
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,7 @@ import { ModCompilerService } from "../services/ModCompilerService";
|
|||||||
import { JsonUtil } from "../utils/JsonUtil";
|
import { JsonUtil } from "../utils/JsonUtil";
|
||||||
import { VFS } from "../utils/VFS";
|
import { VFS } from "../utils/VFS";
|
||||||
import { BundleLoader } from "./BundleLoader";
|
import { BundleLoader } from "./BundleLoader";
|
||||||
|
import { ModLoadOrder } from "./ModLoadOrder";
|
||||||
import { ModTypeCheck } from "./ModTypeCheck";
|
import { ModTypeCheck } from "./ModTypeCheck";
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
@ -40,6 +41,7 @@ export class PreAkiModLoader implements IModLoader
|
|||||||
@inject("BundleLoader") protected bundleLoader: BundleLoader,
|
@inject("BundleLoader") protected bundleLoader: BundleLoader,
|
||||||
@inject("LocalisationService") protected localisationService: LocalisationService,
|
@inject("LocalisationService") protected localisationService: LocalisationService,
|
||||||
@inject("ConfigServer") protected configServer: ConfigServer,
|
@inject("ConfigServer") protected configServer: ConfigServer,
|
||||||
|
@inject("ModLoadOrder") protected modLoadOrder: ModLoadOrder,
|
||||||
@inject("ModTypeCheck") protected modTypeCheck: ModTypeCheck
|
@inject("ModTypeCheck") protected modTypeCheck: ModTypeCheck
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@ -206,6 +208,8 @@ export class PreAkiModLoader implements IModLoader
|
|||||||
{
|
{
|
||||||
await this.addMod(mod);
|
await this.addMod(mod);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.modLoadOrder.setModList(this.imported);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected sortMods(prev: string, next: string, missingFromOrderJSON: Record<string, boolean>): number
|
protected sortMods(prev: string, next: string, missingFromOrderJSON: Record<string, boolean>): number
|
||||||
@ -380,7 +384,7 @@ export class PreAkiModLoader implements IModLoader
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return Object.keys(this.getLoadOrder(this.imported));
|
return this.modLoadOrder.getLoadOrder();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -623,72 +627,6 @@ export class PreAkiModLoader implements IModLoader
|
|||||||
return !issue;
|
return !issue;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getLoadOrderRecursive(mod: string, result: Record<string, string>, visited: Record<string, string>): 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<string, string> = 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<string, IPackageJsonData>): Record<string, string>
|
|
||||||
{
|
|
||||||
const result: Record<string, string> = {};
|
|
||||||
const visited: Record<string, string> = {};
|
|
||||||
|
|
||||||
for (const mod in mods)
|
|
||||||
{
|
|
||||||
if (mods[mod][0] in result)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getLoadOrderRecursive(mod, result, visited);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getContainer(): DependencyContainer
|
public getContainer(): DependencyContainer
|
||||||
{
|
{
|
||||||
if (PreAkiModLoader.container)
|
if (PreAkiModLoader.container)
|
||||||
@ -700,4 +638,4 @@ export class PreAkiModLoader implements IModLoader
|
|||||||
throw new Error(this.localisationService.getText("modloader-dependency_container_not_initalized"));
|
throw new Error(this.localisationService.getText("modloader-dependency_container_not_initalized"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
export interface IPackageJsonData
|
export interface IPackageJsonData
|
||||||
{
|
{
|
||||||
incompatibilities?: string[]
|
incompatibilities?: string[];
|
||||||
dependencies?: Record<string, string>
|
loadBefore?: string[];
|
||||||
modDependencies?: Record<string, string>
|
loadAfter?: string[];
|
||||||
name: string
|
dependencies?: Record<string, string>;
|
||||||
author: string
|
modDependencies?: Record<string, string>;
|
||||||
version: string
|
name: string;
|
||||||
akiVersion: string
|
author: string;
|
||||||
|
version: string;
|
||||||
|
akiVersion: string;
|
||||||
/** We deliberately purge this data */
|
/** We deliberately purge this data */
|
||||||
scripts: Record<string, string>
|
scripts: Record<string, string>;
|
||||||
devDependencies: Record<string, string>
|
devDependencies: Record<string, string>;
|
||||||
licence: string
|
licence: string;
|
||||||
main: string
|
main: string;
|
||||||
isBundleMod: boolean
|
isBundleMod: boolean;
|
||||||
contributors: string[]
|
contributors: string[];
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user