Various Mod Updates for SPT 3.11

This commit is contained in:
2025-05-13 05:33:12 -04:00
parent 306af72daf
commit 37519a6093
462 changed files with 21565 additions and 4784 deletions
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -1,11 +1,11 @@
[General]
gameName=spt
modid=0
version=d2024.12.16.0
version=d2025.5.13.0
newestVersion=
category="1,2"
nexusFileStatus=1
installationFile=Corter-ModSync-v0.10.0.zip
installationFile=Corter-ModSync-v0.11.1.zip
repository=Nexus
ignoredVersion=
comments=
@@ -1,8 +1,8 @@
{
"name": "Corter-ModSync",
"version": "0.10.0",
"version": "0.11.1",
"main": "src/mod.js",
"sptVersion": "~3.10",
"sptVersion": "~3.11",
"loadBefore": [],
"loadAfter": [],
"incompatibilities": [],
@@ -17,12 +17,14 @@
},
"devDependencies": {
"@biomejs/biome": "1.9.3",
"@types/fs-extra": "^11.0.4",
"@types/node": "20.11",
"@types/shelljs": "^0.8.15",
"@typescript-eslint/eslint-plugin": "7.2",
"@typescript-eslint/parser": "7.2",
"@vitest/coverage-istanbul": "^2.1.2",
"fs-extra": "11.2",
"atomically": "^2.0.3",
"fs-extra": "~11.2.0",
"glob": "^11.0.0",
"i18n": "^0.15.1",
"ignore": "^5.2",
@@ -1,12 +1,13 @@
import path from "node:path";
import type { PreSptModLoader } from "@spt/loaders/PreSptModLoader";
import type { JsonUtil } from "@spt/utils/JsonUtil";
import type { VFS } from "@spt/utils/VFS";
import type { FileSystem } from "@spt/utils/FileSystem";
import { glob } from "./utility/glob";
import { unixPath } from "./utility/misc";
import type { ILogger } from "@spt/models/spt/utils/ILogger";
export type SyncPath = {
name?: string;
path: string;
enabled?: boolean;
enforced?: boolean;
@@ -26,6 +27,7 @@ const DEFAULT_CONFIG = `{
"BepInEx/config",
{
"enabled": false,
"name": "(Optional) Server mods",
"path": "user/mods",
"restartRequired": false
}
@@ -41,7 +43,7 @@ const DEFAULT_CONFIG = `{
// Fika
"user/mods/fika-server/types",
"user/mods/fika-server/cache",
"BepInEx/plugins/Fika.Dedicated.dll",
"BepInEx/plugins/Fika.Headless.dll",
// Live Flea Prices
"user/mods/zzDrakiaXYZ-LiveFleaPrices/config",
// Questing Bots
@@ -55,6 +57,8 @@ const DEFAULT_CONFIG = `{
// ADD MISSING QUEST WEAPON REQUIREMENTS
"user/mods/zz_guiltyman-addmissingquestweaponrequirements/log.log",
"user/mods/zz_guiltyman-addmissingquestweaponrequirements/user/logs",
// Acid's Progressive Bot System
"user/mods/acidphantasm-progressivebotsystem/logs",
// Corter ModSync
"BepInEx/patchers/Corter-ModSync-Patcher.dll",
"**/*.nosync",
@@ -65,11 +69,6 @@ const DEFAULT_CONFIG = `{
"user/mods/**/*.js",
"user/mods/**/*.js.map",
"**/*:Zone.Identifier"
// Quest Tracker
"BepInEx/config/xyz.drakia.questtracker.cfg",
"BepInEx/plugins/DrakiaXYZ-QuestTracker/config/",
// Dynamic Maps
"BepInEx/config/com.mpstark.DynamicMaps.cfg"
]
}`;
@@ -83,19 +82,16 @@ export class Config {
}
public isExcluded(filePath: string): boolean {
return this._globs.some(
(glob) =>
glob.test(unixPath(filePath)),
);
return this._globs.some((glob) => glob.test(unixPath(filePath)));
}
}
export class ConfigUtil {
constructor(
private vfs: VFS,
private vfs: FileSystem,
private jsonUtil: JsonUtil,
private modImporter: PreSptModLoader,
private logger: ILogger,
) { }
) {}
/**
* @throws {Error} If the config file does not exist
@@ -104,12 +100,12 @@ export class ConfigUtil {
const modPath = this.modImporter.getModPath("Corter-ModSync");
const configPath = path.join(modPath, "config.jsonc");
if (!this.vfs.exists(configPath))
await this.vfs.writeFilePromisify(configPath, DEFAULT_CONFIG);
if (!(await this.vfs.exists(configPath))) {
await this.vfs.write(configPath, DEFAULT_CONFIG);
}
return this.jsonUtil.deserializeJsonC(
// @ts-expect-error I am right, SPT is wrong
await this.vfs.readFilePromisify(configPath, { encoding: "utf-8" }),
await this.vfs.read(configPath),
"config.jsonc",
) as RawConfig;
}
@@ -128,7 +124,28 @@ export class ConfigUtil {
"Corter-ModSync: config.jsonc 'exclusions' is not an array. Please verify your config is correct and try again.",
);
const uniquePaths = new Set();
for (const syncPath of config.syncPaths) {
if (
typeof syncPath === "object" &&
typeof syncPath.name !== "undefined" &&
typeof syncPath.name !== "string"
) {
throw new Error(
"Corter-ModSync: config.jsonc 'syncPaths.name' must be a string. Please verify your config is correct and try again.",
);
}
if (
typeof syncPath === "object" &&
typeof syncPath.name === "string" &&
/[\n\t\\"'\[\]]/.test(syncPath.name)
) {
throw new Error(
`Corter-ModSync: config.jsonc 'syncPaths.name' contains invalid characters. Please make sure your name does not include any of the following characters: \n \t \\ " ' [ ]`,
);
}
if (typeof syncPath !== "string" && !("path" in syncPath))
throw new Error(
"Corter-ModSync: config.jsonc 'syncPaths' is missing 'path'. Please verify your config is correct and try again.",
@@ -157,6 +174,22 @@ export class ConfigUtil {
throw new Error(
`Corter-ModSync: SyncPaths must within SPT server root. Invalid path '${syncPath}'`,
);
if (
uniquePaths.has(typeof syncPath === "string" ? syncPath : syncPath.path)
)
throw new Error(
`Corter-ModSync: SyncPaths must be unique. Duplicate path '${syncPath}'`,
);
if (
config.exclusions.includes(
typeof syncPath === "string" ? syncPath : syncPath.path,
)
)
throw new Error(
`Corter-ModSync: '${syncPath}' has been added as a sync path and is also in the 'exclusions' array. This probably isn't doing what you want. If you no longer want to sync this path, remove it from the 'exclusions' and 'syncPaths' arrays.`,
);
}
}
@@ -172,6 +205,7 @@ export class ConfigUtil {
silent: true,
restartRequired: false,
path: "ModSync.Updater.exe",
name: "(Builtin) ModSync Updater",
},
{
enabled: true,
@@ -179,6 +213,7 @@ export class ConfigUtil {
silent: true,
restartRequired: true,
path: "BepInEx/plugins/Corter-ModSync.dll",
name: "(Builtin) ModSync Plugin",
},
...rawConfig.syncPaths
.map((syncPath) => ({
@@ -186,6 +221,7 @@ export class ConfigUtil {
enforced: false,
silent: false,
restartRequired: true,
name: typeof syncPath === "string" ? syncPath : syncPath.path,
...(typeof syncPath === "string" ? { path: syncPath } : syncPath),
}))
.sort((a, b) => b.path.length - a.path.length),
@@ -5,13 +5,14 @@ import type { IPreSptLoadMod } from "@spt/models/external/IPreSptLoadMod";
import type { ILogger } from "@spt/models/spt/utils/ILogger";
import type { HttpListenerModService } from "@spt/services/mod/httpListener/HttpListenerModService";
import type { HttpFileUtil } from "@spt/utils/HttpFileUtil";
import type { VFS } from "@spt/utils/VFS";
import type { FileSystem } from "@spt/utils/FileSystem";
import type { JsonUtil } from "@spt/utils/JsonUtil";
import { ConfigUtil, type Config } from "./config";
import { SyncUtil } from "./sync";
import { Router } from "./router";
import type { PreSptModLoader } from "@spt/loaders/PreSptModLoader";
import type { HttpServerHelper } from "@spt/helpers/HttpServerHelper";
import { Statter } from "./utility/statter";
class Mod implements IPreSptLoadMod {
private static container: DependencyContainer;
@@ -22,7 +23,7 @@ class Mod implements IPreSptLoadMod {
public async preSptLoad(container: DependencyContainer): Promise<void> {
Mod.container = container;
const logger = container.resolve<ILogger>("WinstonLogger");
const vfs = container.resolve<VFS>("VFS");
const vfs = container.resolve<FileSystem>("FileSystem");
const jsonUtil = container.resolve<JsonUtil>("JsonUtil");
const modImporter = container.resolve<PreSptModLoader>("PreSptModLoader");
const configUtil = new ConfigUtil(vfs, jsonUtil, modImporter, logger);
@@ -46,12 +47,16 @@ class Mod implements IPreSptLoadMod {
if (!vfs.exists("ModSync.Updater.exe")) {
Mod.loadFailed = true;
logger.error("Corter-ModSync: ModSync.Updater.exe not found! Please ensure ALL files from the release zip are extracted onto the server.");
logger.error(
"Corter-ModSync: ModSync.Updater.exe not found! Please ensure ALL files from the release zip are extracted onto the server.",
);
}
if (!vfs.exists("BepInEx/plugins/Corter-ModSync.dll")) {
Mod.loadFailed = true;
logger.error("Corter-ModSync: Corter-ModSync.dll not found! Please ensure ALL files from the release zip are extracted onto the server.");
logger.error(
"Corter-ModSync: Corter-ModSync.dll not found! Please ensure ALL files from the release zip are extracted onto the server.",
);
}
}
@@ -65,17 +70,19 @@ class Mod implements IPreSptLoadMod {
res: ServerResponse,
): Promise<void> {
const logger = Mod.container.resolve<ILogger>("WinstonLogger");
const vfs = Mod.container.resolve<VFS>("VFS");
const vfs = Mod.container.resolve<FileSystem>("FileSystem");
const httpFileUtil = Mod.container.resolve<HttpFileUtil>("HttpFileUtil");
const httpServerHelper =
Mod.container.resolve<HttpServerHelper>("HttpServerHelper");
const modImporter =
Mod.container.resolve<PreSptModLoader>("PreSptModLoader");
const syncUtil = new SyncUtil(vfs, Mod.config, logger);
const statter = new Statter();
const syncUtil = new SyncUtil(vfs, statter, Mod.config, logger);
const router = new Router(
Mod.config,
syncUtil,
vfs,
statter,
httpFileUtil,
httpServerHelper,
modImporter,
@@ -3,12 +3,13 @@ import type { SyncUtil } from "./sync";
import { glob } from "./utility/glob";
import type { IncomingMessage, ServerResponse } from "node:http";
import path from "node:path";
import type { VFS } from "@spt/utils/VFS";
import type { FileSystem } from "@spt/utils/FileSystem";
import type { Config } from "./config";
import { HttpError, winPath } from "./utility/misc";
import type { ILogger } from "@spt/models/spt/utils/ILogger";
import type { PreSptModLoader } from "@spt/loaders/PreSptModLoader";
import type { HttpServerHelper } from "@spt/helpers/HttpServerHelper";
import type { IStatter } from "./utility/statter";
const FALLBACK_SYNCPATHS: Record<string, object> = {};
@@ -20,22 +21,22 @@ FALLBACK_SYNCPATHS[undefined] = [
FALLBACK_SYNCPATHS["0.8.0"] =
FALLBACK_SYNCPATHS["0.8.1"] =
FALLBACK_SYNCPATHS["0.8.2"] =
[
{
enabled: true,
enforced: true,
path: "BepInEx\\plugins\\Corter-ModSync.dll",
restartRequired: true,
silent: false,
},
{
enabled: true,
enforced: true,
path: "ModSync.Updater.exe",
restartRequired: false,
silent: false,
},
];
[
{
enabled: true,
enforced: true,
path: "BepInEx\\plugins\\Corter-ModSync.dll",
restartRequired: true,
silent: false,
},
{
enabled: true,
enforced: true,
path: "ModSync.Updater.exe",
restartRequired: false,
silent: false,
},
];
const FALLBACK_HASHES: Record<string, object> = {};
@@ -47,28 +48,29 @@ FALLBACK_HASHES[undefined] = {
FALLBACK_HASHES["0.8.0"] =
FALLBACK_HASHES["0.8.1"] =
FALLBACK_HASHES["0.8.2"] =
{
"BepInEx\\plugins\\Corter-ModSync.dll": {
{
"BepInEx\\plugins\\Corter-ModSync.dll": {
crc: 999999999,
nosync: false,
"BepInEx\\plugins\\Corter-ModSync.dll": {
crc: 999999999,
nosync: false,
},
},
},
"ModSync.Updater.exe": {
"ModSync.Updater.exe": { crc: 999999999, nosync: false },
},
};
"ModSync.Updater.exe": {
"ModSync.Updater.exe": { crc: 999999999, nosync: false },
},
};
export class Router {
constructor(
private config: Config,
private syncUtil: SyncUtil,
private vfs: VFS,
private vfs: FileSystem,
private statter: IStatter,
private httpFileUtil: HttpFileUtil,
private httpServerHelper: HttpServerHelper,
private modImporter: PreSptModLoader,
private logger: ILogger,
) { }
) {}
/**
* @internal
@@ -80,13 +82,8 @@ export class Router {
_params: URLSearchParams,
) {
const modPath = this.modImporter.getModPath("Corter-ModSync");
const packageJson = JSON.parse(
// @ts-expect-error readFile returns a string when given a valid encoding
await this.vfs
// @ts-expect-error readFile takes in an options object, including an encoding option
.readFilePromisify(path.join(modPath, "package.json"), {
encoding: "utf-8",
}),
const packageJson = await this.vfs.readJson(
path.join(modPath, "package.json"),
);
res.setHeader("Content-Type", "application/json");
@@ -185,19 +182,19 @@ export class Router {
this.config.syncPaths,
);
if (!this.vfs.exists(sanitizedPath))
if (!(await this.vfs.exists(sanitizedPath)))
throw new HttpError(
404,
`Attempt to access non-existent path ${filePath}`,
);
try {
const fileStats = await this.vfs.statPromisify(sanitizedPath);
const fileStats = await this.statter.stat(sanitizedPath);
res.setHeader("Accept-Ranges", "bytes");
res.setHeader(
"Content-Type",
this.httpServerHelper.getMimeText(path.extname(filePath)) ||
"text/plain",
"text/plain",
);
res.setHeader("Content-Length", fileStats.size);
return this.httpFileUtil.sendFileAsync(res, sanitizedPath);
@@ -1,11 +1,11 @@
import type { VFS } from "@spt/utils/VFS";
import path from "node:path";
import { hashFile } from "./utility/imoHash";
import path from "node:path";
import type { ILogger } from "@spt/models/spt/utils/ILogger";
import type { FileSystem } from "@spt/utils/FileSystem";
import type { Config, SyncPath } from "./config";
import { hashFile } from "./utility/imoHash";
import { HttpError, winPath } from "./utility/misc";
import { Semaphore } from "./utility/semaphore";
import type { ILogger } from "@spt/models/spt/utils/ILogger";
import type { } from "node:fs";
import type { IStatter } from "./utility/statter";
type ModFile = {
hash: string;
@@ -16,24 +16,25 @@ export class SyncUtil {
private limiter = new Semaphore(1024);
constructor(
private vfs: VFS,
private vfs: FileSystem,
private statter: IStatter,
private config: Config,
private logger: ILogger,
) { }
) {}
private async getFilesInDir(baseDir: string, dir: string): Promise<string[]> {
if (!this.vfs.exists(dir)) {
if (!(await this.vfs.exists(dir))) {
this.logger.warning(
`Corter-ModSync: Directory '${dir}' does not exist, will be ignored.`,
);
return [];
}
const stats = await this.vfs.statPromisify(dir);
const stats = await this.statter.stat(dir);
if (stats.isFile()) return [dir];
const files: string[] = [];
for (const fileName of this.vfs.getFiles(dir)) {
for (const fileName of await this.vfs.getFiles(dir)) {
const file = path.join(dir, fileName);
if (this.config.isExcluded(file)) continue;
@@ -41,18 +42,27 @@ export class SyncUtil {
files.push(file);
}
for (const dirName of this.vfs.getDirs(dir)) {
for (const dirName of await this.vfs.getDirectories(dir)) {
const subDir = path.join(dir, dirName);
if (this.config.isExcluded(subDir)) continue;
const subFiles = await this.getFilesInDir(baseDir, subDir);
if (!subFiles.length) files.push(subDir)
if (
(await this.vfs.getFiles(subDir)).length === 0 &&
(await this.vfs.getDirectories(subDir)).length === 0
)
files.push(subDir);
files.push(...subFiles);
}
if (stats.isDirectory() && files.length === 0) files.push(dir);
if (
stats.isDirectory() &&
(await this.vfs.getFiles(dir)).length === 0 &&
(await this.vfs.getDirectories(dir)).length === 0
)
files.push(dir);
return files;
}
@@ -60,9 +70,9 @@ export class SyncUtil {
private async buildModFile(
file: string,
// biome-ignore lint/correctness/noEmptyPattern: <explanation>
{ }: Required<SyncPath>,
{}: Required<SyncPath>,
): Promise<ModFile> {
const stats = await this.vfs.statPromisify(file);
const stats = await this.statter.stat(file);
if (stats.isDirectory()) return { hash: "", directory: true };
let retryCount = 0;
@@ -77,8 +87,15 @@ export class SyncUtil {
directory: false,
};
} catch (e) {
if (e instanceof Error && 'code' in e && e.code === "EBUSY" && retryCount < 5) {
this.logger.error(`Error reading '${file}'. Retrying (${retryCount}/5)...`);
if (
e instanceof Error &&
"code" in e &&
e.code === "EBUSY" &&
retryCount < 5
) {
this.logger.error(
`Error reading '${file}'. Retrying (${retryCount}/5)...`,
);
await new Promise((resolve) => setTimeout(resolve, 500));
retryCount++;
continue;
@@ -86,7 +103,10 @@ export class SyncUtil {
this.logger.error(`Error reading '${file}'. Exiting...`);
this.logger.error(`${e}`);
throw new HttpError(500, `Corter-ModSync: Error reading '${file}'\n${e}`);
throw new HttpError(
500,
`Corter-ModSync: Error reading '${file}'\n${e}`,
);
}
}
}
@@ -113,7 +133,9 @@ export class SyncUtil {
result[winPath(syncPath.path)] = filesResult;
}
this.logger.info(`Corter-ModSync: Hashed ${processedFiles.size} files in ${performance.now() - startTime}ms`);
this.logger.info(
`Corter-ModSync: Hashed ${processedFiles.size} files in ${performance.now() - startTime}ms`,
);
return result;
}
@@ -69,17 +69,17 @@ const wasmInstance = new WebAssembly.Instance(wasmModule, {
takeObject(arg0);
},
__wbg_buffer_ccaed51a635d8a2d: (arg0: number) => {
__wbg_buffer_609cc3eee51ed158: (arg0: number) => {
const ret = getObject(arg0).buffer;
return addHeapObject(ret);
},
__wbg_newwithbyteoffsetandlength_7e3eb787208af730: (arg0: number, arg1: number, arg2: number) => {
__wbg_newwithbyteoffsetandlength_d97e637ebe145a9a: (arg0: number, arg1: number, arg2: number) => {
const ret = new Uint8Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0);
return addHeapObject(ret);
},
__wbg_new_fec2611eb9180f95: (arg0: number) => {
__wbg_new_a12002a7f91c75be: (arg0: number) => {
const ret = new Uint8Array(getObject(arg0));
return addHeapObject(ret);
},
@@ -0,0 +1,9 @@
import { stat, type Stats } from "fs-extra";
export interface IStatter {
stat: (path: string) => Promise<Stats>;
}
export class Statter implements IStatter {
public stat = stat;
}
Binary file not shown.