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:
TheSparta 2023-10-18 14:44:29 +00:00 committed by chomp
parent 1adeed29ef
commit 6231d73109
4 changed files with 176 additions and 82 deletions

View File

@ -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 });
} }

View 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);
}
}

View File

@ -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)

View File

@ -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[];
} }