2023-03-03 15:23:46 +00:00
|
|
|
import semver from "semver";
|
|
|
|
import { DependencyContainer, inject, injectable } from "tsyringe";
|
|
|
|
import { ConfigTypes } from "../models/enums/ConfigTypes";
|
|
|
|
import { IPreAkiLoadMod } from "../models/external/IPreAkiLoadMod";
|
|
|
|
import { IPreAkiLoadModAsync } from "../models/external/IPreAkiLoadModAsync";
|
|
|
|
import { ICoreConfig } from "../models/spt/config/ICoreConfig";
|
|
|
|
import { IModLoader } from "../models/spt/mod/IModLoader";
|
|
|
|
import { IPackageJsonData } from "../models/spt/mod/IPackageJsonData";
|
|
|
|
import { ILogger } from "../models/spt/utils/ILogger";
|
|
|
|
import { ConfigServer } from "../servers/ConfigServer";
|
|
|
|
import { LocalisationService } from "../services/LocalisationService";
|
|
|
|
import { ModCompilerService } from "../services/ModCompilerService";
|
|
|
|
import { JsonUtil } from "../utils/JsonUtil";
|
|
|
|
import { VFS } from "../utils/VFS";
|
|
|
|
import { BundleLoader } from "./BundleLoader";
|
|
|
|
import { ModTypeCheck } from "./ModTypeCheck";
|
|
|
|
|
|
|
|
@injectable()
|
|
|
|
export class PreAkiModLoader implements IModLoader
|
|
|
|
{
|
|
|
|
protected static container: DependencyContainer;
|
|
|
|
|
|
|
|
protected readonly basepath = "user/mods/";
|
|
|
|
protected readonly modOrderPath = "user/mods/order.json";
|
|
|
|
protected order: Record<string, number> = {};
|
|
|
|
protected imported: Record<string, IPackageJsonData> = {};
|
|
|
|
protected akiConfig: ICoreConfig;
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
@inject("WinstonLogger") protected logger: ILogger,
|
|
|
|
@inject("VFS") protected vfs: VFS,
|
|
|
|
@inject("JsonUtil") protected jsonUtil: JsonUtil,
|
|
|
|
@inject("ModCompilerService") protected modCompilerService: ModCompilerService,
|
|
|
|
@inject("BundleLoader") protected bundleLoader: BundleLoader,
|
|
|
|
@inject("LocalisationService") protected localisationService: LocalisationService,
|
|
|
|
@inject("ConfigServer") protected configServer: ConfigServer,
|
|
|
|
@inject("ModTypeCheck") protected modTypeCheck: ModTypeCheck
|
|
|
|
)
|
|
|
|
{
|
|
|
|
this.akiConfig = this.configServer.getConfig<ICoreConfig>(ConfigTypes.CORE);
|
|
|
|
}
|
|
|
|
|
|
|
|
public async load(container: DependencyContainer): Promise<void>
|
|
|
|
{
|
|
|
|
if (globalThis.G_MODS_ENABLED)
|
|
|
|
{
|
|
|
|
PreAkiModLoader.container = container;
|
|
|
|
await this.importMods();
|
|
|
|
await this.executeMods(container);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a list of mods with preserved load order
|
|
|
|
* @returns Array of mod names in load order
|
|
|
|
*/
|
|
|
|
public getImportedModsNames(): string[]
|
|
|
|
{
|
|
|
|
return Object.keys(this.imported);
|
|
|
|
}
|
|
|
|
|
|
|
|
public getImportedModDetails(): Record<string, IPackageJsonData>
|
|
|
|
{
|
|
|
|
return this.imported;
|
|
|
|
}
|
|
|
|
|
|
|
|
public getModPath(mod: string): string
|
|
|
|
{
|
|
|
|
return `${this.basepath}${mod}/`;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async importMods(): Promise<void>
|
|
|
|
{
|
|
|
|
if (!this.vfs.exists(this.basepath))
|
|
|
|
{
|
|
|
|
// no mods folder found
|
|
|
|
this.logger.info(this.localisationService.getText("modloader-user_mod_folder_missing"));
|
|
|
|
this.vfs.createDir(this.basepath);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let mods: string[] = this.vfs.getDirs(this.basepath);
|
|
|
|
|
|
|
|
this.logger.info(this.localisationService.getText("modloader-loading_mods", mods.length));
|
|
|
|
|
|
|
|
// Mod order
|
|
|
|
if (!this.vfs.exists(this.modOrderPath))
|
|
|
|
{
|
|
|
|
this.logger.info(this.localisationService.getText("modloader-mod_order_missing"));
|
|
|
|
this.vfs.writeFile(this.modOrderPath, JSON.stringify({order: []}, null, 4));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
const modOrder = this.vfs.readFile(this.modOrderPath, { encoding: "utf8" });
|
|
|
|
try
|
|
|
|
{
|
|
|
|
JSON.parse(modOrder).order.forEach((mod: string, index: number) =>
|
|
|
|
{
|
|
|
|
this.order[mod] = index;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
catch (error)
|
|
|
|
{
|
|
|
|
this.logger.error(this.localisationService.getText("modloader-mod_order_error"));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Used to check all errors before stopping the load execution
|
|
|
|
let errorsFound = false;
|
|
|
|
|
|
|
|
// Validate and remove broken mods from mod list
|
|
|
|
const brokenMods: string[] = this.getBrokenMods(mods);
|
|
|
|
mods = mods.filter( ( mod ) => !brokenMods.includes( mod ) );
|
|
|
|
|
|
|
|
const modPackageData = this.getModsPackageData(mods);
|
|
|
|
this.checkForDuplicateMods(modPackageData);
|
|
|
|
for (const modFolderName in modPackageData)
|
|
|
|
{
|
|
|
|
const modToValidate = modPackageData[modFolderName];
|
|
|
|
|
|
|
|
// Returns if any mod dependency is not satisfied
|
|
|
|
if (!this.areModDependenciesFulfilled(modToValidate, modPackageData))
|
|
|
|
{
|
|
|
|
errorsFound = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns if at least two incompatible mods are found
|
|
|
|
if (!this.isModCompatible(modToValidate, modPackageData))
|
|
|
|
{
|
|
|
|
errorsFound = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns if mod isnt compatible with this verison of aki
|
|
|
|
if (!this.isModCombatibleWithAki(modToValidate))
|
|
|
|
{
|
|
|
|
errorsFound = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (errorsFound)
|
|
|
|
{
|
|
|
|
this.logger.error(this.localisationService.getText("modloader-no_mods_loaded"));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// sort mod order
|
|
|
|
const missingFromOrderJSON = {};
|
|
|
|
const sortedMods = mods.sort((prev, next) =>
|
|
|
|
{
|
|
|
|
const previndex = this.order[prev];
|
|
|
|
const nextindex = this.order[next];
|
|
|
|
// mod is not on the list, move the mod to last
|
|
|
|
if (previndex === undefined)
|
|
|
|
{
|
|
|
|
missingFromOrderJSON[prev] = true;
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
else if (nextindex === undefined)
|
|
|
|
{
|
|
|
|
missingFromOrderJSON[next] = true;
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
return previndex - nextindex;
|
|
|
|
});
|
|
|
|
|
|
|
|
// log the missing mods from order.json
|
|
|
|
Object.keys(missingFromOrderJSON).forEach((missingMod) => (this.logger.debug(this.localisationService.getText("modloader-mod_order_missing_from_json", missingMod))));
|
|
|
|
|
|
|
|
// add mods
|
|
|
|
for (const mod of sortedMods)
|
|
|
|
{
|
|
|
|
await this.addMod(mod);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check for duplciate mods loaded, show error if duplicate mod found
|
|
|
|
* @param modPackageData dictionary of mod package.json data
|
|
|
|
*/
|
|
|
|
protected checkForDuplicateMods(modPackageData: Record<string, IPackageJsonData>): void
|
|
|
|
{
|
|
|
|
const modNames = [];
|
|
|
|
for (const modKey in modPackageData)
|
|
|
|
{
|
|
|
|
const mod = modPackageData[modKey];
|
|
|
|
modNames.push(`${mod.author}-${mod.name}`);
|
|
|
|
}
|
|
|
|
const dupes = this.getDuplicates(modNames);
|
2023-06-26 07:39:22 +01:00
|
|
|
if (dupes?.length > 0)
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
2023-06-26 07:39:22 +01:00
|
|
|
this.logger.error(this.localisationService.getText("modloader-x_duplicates_found", dupes.join(",")));
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check for and return duplicate strings inside an array
|
|
|
|
* @param stringArray Array to check for duplicates
|
|
|
|
* @returns string array of duplicates, empty if none found
|
|
|
|
*/
|
|
|
|
protected getDuplicates(stringArray: string[]): string[]
|
|
|
|
{
|
|
|
|
return stringArray.filter((s => v => s.has(v) || !s.add(v))(new Set));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get an array of mods with errors that prevent them from working with SPT
|
|
|
|
* @param mods mods to validate
|
|
|
|
* @returns Mod names as array
|
|
|
|
*/
|
|
|
|
protected getBrokenMods(mods: string[]): string[]
|
|
|
|
{
|
|
|
|
const brokenMods: string[] = [];
|
|
|
|
for (const mod of mods)
|
|
|
|
{
|
|
|
|
if (!this.validMod(mod))
|
|
|
|
{
|
|
|
|
brokenMods.push(mod);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return brokenMods;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get packageJson data for mods
|
|
|
|
* @param mods mods to get packageJson for
|
|
|
|
* @returns dictionary <modName - package.json>
|
|
|
|
*/
|
|
|
|
protected getModsPackageData(mods: string[]): Record<string, IPackageJsonData>
|
|
|
|
{
|
|
|
|
const loadedMods: Record<string, IPackageJsonData> = {};
|
|
|
|
for (const mod of mods)
|
|
|
|
{
|
|
|
|
loadedMods[mod] = this.jsonUtil.deserialize(this.vfs.readFile(`${this.getModPath(mod)}/package.json`));
|
|
|
|
}
|
|
|
|
|
|
|
|
return loadedMods;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected isModCombatibleWithAki(mod: IPackageJsonData): boolean
|
|
|
|
{
|
|
|
|
const akiVersion = this.akiConfig.akiVersion;
|
|
|
|
const modName = `${mod.author}-${mod.name}`;
|
|
|
|
|
|
|
|
// Error and prevent loading If no akiVersion property exists
|
|
|
|
if (!mod.akiVersion)
|
|
|
|
{
|
|
|
|
this.logger.error(this.localisationService.getText("modloader-missing_akiversion_field", modName));
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Error and prevent loading if akiVersion property is not a valid semver string
|
|
|
|
if (!(semver.valid(mod.akiVersion) || semver.validRange(mod.akiVersion)))
|
|
|
|
{
|
|
|
|
this.logger.error(this.localisationService.getText("modloader-invalid_akiversion_field", modName));
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Warn and allow loading if semver is not satisfied
|
|
|
|
if (!semver.satisfies(akiVersion, mod.akiVersion))
|
|
|
|
{
|
|
|
|
this.logger.warning(this.localisationService.getText("modloader-outdated_akiversion_field", modName));
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async executeMods(container: DependencyContainer): Promise<void>
|
|
|
|
{
|
|
|
|
// sort mods load order
|
|
|
|
const source = this.sortModsLoadOrder();
|
|
|
|
|
|
|
|
const promiseLoad = new Array<Promise<void>>();
|
|
|
|
// import mod classes
|
|
|
|
for (const mod of source)
|
|
|
|
{
|
|
|
|
|
|
|
|
if ("main" in this.imported[mod])
|
|
|
|
{
|
|
|
|
const filepath = `${this.getModPath(mod)}${this.imported[mod].main}`;
|
|
|
|
// import class
|
|
|
|
const modFilePath = `${process.cwd()}/${filepath}`;
|
|
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
|
|
const requiredMod = require(modFilePath);
|
|
|
|
|
|
|
|
if (!this.modTypeCheck.isPostV3Compatible(requiredMod.mod))
|
|
|
|
{
|
|
|
|
this.logger.error(this.localisationService.getText("modloader-mod_incompatible", mod));
|
|
|
|
delete this.imported[mod];
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.modTypeCheck.isPreAkiLoad(requiredMod.mod))
|
|
|
|
{
|
|
|
|
(requiredMod.mod as IPreAkiLoadMod).preAkiLoad(container);
|
|
|
|
globalThis[mod] = requiredMod;
|
|
|
|
}
|
|
|
|
if (this.modTypeCheck.isPreAkiLoadAsync(requiredMod.mod))
|
|
|
|
{
|
|
|
|
promiseLoad.push(
|
|
|
|
(requiredMod.mod as IPreAkiLoadModAsync).preAkiLoadAsync(container)
|
|
|
|
.then(() => globalThis[mod] = requiredMod)
|
|
|
|
.catch((err) => this.logger.error(this.localisationService.getText("modloader-async_mod_error", `${err?.message ?? ""}\n${err.stack ?? ""}`)))
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
await Promise.all(promiseLoad);
|
|
|
|
}
|
|
|
|
|
|
|
|
public sortModsLoadOrder(): string[]
|
|
|
|
{
|
|
|
|
// if loadorder.json exists: load it, otherwise generate load order
|
|
|
|
if (this.vfs.exists(`${this.basepath}loadorder.json`))
|
|
|
|
{
|
|
|
|
return this.jsonUtil.deserialize(this.vfs.readFile(`${this.basepath}loadorder.json`));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
return Object.keys(this.getLoadOrder(this.imported));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async addMod(mod: string): Promise<void>
|
|
|
|
{
|
|
|
|
const modPath = this.getModPath(mod);
|
|
|
|
const packageData = this.jsonUtil.deserialize<IPackageJsonData>(this.vfs.readFile(`${modPath}/package.json`));
|
|
|
|
|
|
|
|
const isBundleMod = packageData.isBundleMod ?? false;
|
|
|
|
|
|
|
|
if (isBundleMod)
|
|
|
|
{
|
|
|
|
this.bundleLoader.addBundles(modPath);
|
|
|
|
}
|
|
|
|
|
|
|
|
const typeScriptFiles = this.vfs.getFilesOfType(`${modPath}src`, ".ts");
|
|
|
|
|
|
|
|
if (typeScriptFiles.length > 0)
|
|
|
|
{
|
|
|
|
if (globalThis.G_MODS_TRANSPILE_TS)
|
|
|
|
{
|
|
|
|
// compile ts into js if ts files exist and globalThis.G_MODS_TRANSPILE_TS is set to true
|
|
|
|
await this.modCompilerService.compileMod(mod, modPath, typeScriptFiles);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// rename the mod entry point to .ts if it's set to .js because G_MODS_TRANSPILE_TS is set to false
|
|
|
|
packageData.main = (packageData.main as string).replace(".js", ".ts");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// add mod to imported list
|
|
|
|
this.imported[mod] = {...packageData, dependencies: packageData.modDependencies};
|
|
|
|
this.logger.info(this.localisationService.getText("modloader-loaded_mod", {name: packageData.name, version: packageData.version, author: packageData.author}));
|
|
|
|
}
|
|
|
|
|
|
|
|
protected areModDependenciesFulfilled(pkg: IPackageJsonData, loadedMods: Record<string, IPackageJsonData>): boolean
|
|
|
|
{
|
|
|
|
if (!pkg.modDependencies)
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
const modName = `${pkg.author}-${pkg.name}`;
|
|
|
|
|
|
|
|
for (const [modDependency, requiredVersion ] of Object.entries(pkg.modDependencies))
|
|
|
|
{
|
|
|
|
// Raise dependency version incompatible if the dependency is not found in the mod list
|
|
|
|
if (!(modDependency in loadedMods))
|
|
|
|
{
|
|
|
|
this.logger.error(this.localisationService.getText("modloader-missing_dependency", {mod: modName, modDependency: modDependency}));
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!semver.satisfies(loadedMods[modDependency].version, requiredVersion))
|
|
|
|
{
|
|
|
|
this.logger.error(this.localisationService.getText("modloader-outdated_dependency", {mod: modName, modDependency: modDependency, currentVersion: loadedMods[modDependency].version, requiredVersion: requiredVersion}));
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected isModCompatible(mod: IPackageJsonData, loadedMods: Record<string, IPackageJsonData>): boolean
|
|
|
|
{
|
|
|
|
const incompatbileModsList = mod.incompatibilities;
|
|
|
|
if (!incompatbileModsList)
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const incompatibleModName of incompatbileModsList)
|
|
|
|
{
|
|
|
|
// Raise dependency version incompatible if any incompatible mod is found
|
|
|
|
if (incompatibleModName in loadedMods)
|
|
|
|
{
|
|
|
|
this.logger.error(this.localisationService.getText("modloader-incompatible_mod_found", {author: mod.author, modName: mod.name, incompatibleModName: incompatibleModName}));
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Validate a mod passes a number of checks
|
|
|
|
* @param modName name of mod in /mods/ to validate
|
|
|
|
* @returns true if valid
|
|
|
|
*/
|
|
|
|
protected validMod(modName: string): boolean
|
|
|
|
{
|
|
|
|
const modPath = this.getModPath(modName);
|
|
|
|
|
|
|
|
const modIsCalledBepinEx = modName.toLowerCase() === "bepinex";
|
|
|
|
const hasBepinExFolderStructure = this.vfs.exists(`${modPath}/plugins`);
|
|
|
|
const containsDll = this.vfs.getFiles(`${modPath}`).find(x => x.includes(".dll"));
|
|
|
|
if (modIsCalledBepinEx || hasBepinExFolderStructure || containsDll)
|
|
|
|
{
|
|
|
|
this.logger.error(this.localisationService.getText("modloader-is_client_mod", modName));
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// check if config exists
|
|
|
|
if (!this.vfs.exists(`${modPath}/package.json`))
|
|
|
|
{
|
|
|
|
this.logger.error(this.localisationService.getText("modloader-missing_package_json", modName));
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// validate mod
|
|
|
|
const config = this.jsonUtil.deserialize<IPackageJsonData>(this.vfs.readFile(`${modPath}/package.json`));
|
|
|
|
const checks = ["name", "author", "version", "license"];
|
|
|
|
let issue = false;
|
|
|
|
|
|
|
|
for (const check of checks)
|
|
|
|
{
|
|
|
|
if (!(check in config))
|
|
|
|
{
|
|
|
|
this.logger.error(this.localisationService.getText("modloader-missing_package_json_property", {modName: modName, prop: check}));
|
|
|
|
issue = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!semver.valid(config.version))
|
|
|
|
{
|
|
|
|
this.logger.error(this.localisationService.getText("modloader-invalid_version_property", modName));
|
|
|
|
issue = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ("main" in config)
|
|
|
|
{
|
|
|
|
if (config.main.split(".").pop() !== "js") // expects js file as entry
|
|
|
|
{
|
|
|
|
this.logger.error(this.localisationService.getText("modloader-main_property_not_js", modName));
|
|
|
|
issue = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!this.vfs.exists(`${modPath}/${config.main}`))
|
|
|
|
{
|
|
|
|
|
|
|
|
// If TS file exists with same name, dont perform check as we'll generate JS from TS file
|
|
|
|
const tsFileName = config.main.replace(".js", ".ts");
|
|
|
|
const tsFileExists = this.vfs.exists(`${modPath}/${tsFileName}`);
|
|
|
|
|
|
|
|
if (!tsFileExists)
|
|
|
|
{
|
|
|
|
this.logger.error(this.localisationService.getText("modloader-main_property_points_to_nothing", modName));
|
|
|
|
issue = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (config.incompatibilities && !Array.isArray(config.incompatibilities))
|
|
|
|
{
|
|
|
|
this.logger.error(this.localisationService.getText("modloader-incompatibilities_not_string_array", modName));
|
|
|
|
issue = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
{
|
|
|
|
if (PreAkiModLoader.container)
|
|
|
|
{
|
|
|
|
return PreAkiModLoader.container;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
throw new Error(this.localisationService.getText("modloader-dependency_container_not_initalized"));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|