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
@@ -1,10 +1,10 @@
{
"name": "weapon-customizer",
"version": "1.0.1",
"version": "2.0.1",
"main": "src/mod.js",
"license": "MIT",
"author": "Tyfon",
"sptVersion": "~3.10",
"sptVersion": "~3.11",
"loadBefore": [],
"loadAfter": [],
"incompatibilities": [],
@@ -25,7 +25,6 @@
"eslint-plugin-prettier": "^5.1.3",
"fs-extra": "11.2",
"ignore": "^5.2",
"os": "^0.1",
"tsyringe": "4.8.0",
"typescript": "5.4",
"winston": "3.12"
@@ -1,13 +1,16 @@
import type { DependencyContainer } from "tsyringe";
import type { ItemHelper } from "@spt/helpers/ItemHelper";
import type { ProfileHelper } from "@spt/helpers/ProfileHelper";
import type { IItem } from "@spt/models/eft/common/tables/IItem";
import type { IPostSptLoadMod } from "@spt/models/external/IPostSptLoadMod";
import type { IPreSptLoadMod } from "@spt/models/external/IPreSptLoadMod";
import { LogTextColor } from "@spt/models/spt/logging/LogTextColor";
import type { ILogger } from "@spt/models/spt/utils/ILogger";
import type { StaticRouterModService } from "@spt/services/mod/staticRouter/StaticRouterModService";
import { VFS } from "@spt/utils/VFS";
import fs from "node:fs";
import type { ICloner } from "@spt/utils/cloners/ICloner";
import type { FileSystem } from "@spt/utils/FileSystem";
import path from "node:path";
type Vector3 = {
@@ -16,30 +19,50 @@ type Vector3 = {
z: number;
};
type CustomPosition = {
original: Vector3;
modified: Vector3;
type Quaternion = {
w: number;
x: number;
y: number;
z: number;
};
type CustomizePayload = {
weaponId: string;
slots: Record<string, CustomPosition>;
type Customization = {
position?: Vector3;
rotation?: Quaternion;
};
type Customizations = Record<string, Record<string, CustomPosition>>;
type CustomizedObject = {
id: string;
type: "weapon" | "preset" | "unknown";
name?: string;
slots: Record<string, Customization>;
};
type Customizations = Record<string, CustomizedObject>;
type FileFormat = {
description: string;
version: number;
customizations: Customizations;
};
type V1FileFormat = Record<string, Record<string, Customization>>;
const currentSaveFormatVersion = 2;
class WeaponCustomizer implements IPreSptLoadMod, IPostSptLoadMod {
private logger: ILogger;
private vfs: VFS;
private fileSystem: FileSystem;
private profileHelper: ProfileHelper;
private customizations: Customizations = null;
private filepath: string;
public preSptLoad(container: DependencyContainer): void {
this.logger = container.resolve<ILogger>("PrimaryLogger");
this.vfs = container.resolve<VFS>("VFS");
this.fileSystem = container.resolve<FileSystem>("FileSystem");
const staticRouterModService = container.resolve<StaticRouterModService>("StaticRouterModService");
const cloner = container.resolve<ICloner>("RecursiveCloner");
this.filepath = path.resolve(__dirname, "../customizations.json");
this.load();
@@ -49,7 +72,7 @@ class WeaponCustomizer implements IPreSptLoadMod, IPostSptLoadMod {
[
{
url: "/weaponcustomizer/save",
action: async (url, info: CustomizePayload, sessionId, output) => this.saveCustomization(info)
action: async (url, info: CustomizedObject[], sessionId, output) => this.saveCustomization(info)
},
{
url: "/weaponcustomizer/load",
@@ -58,42 +81,96 @@ class WeaponCustomizer implements IPreSptLoadMod, IPostSptLoadMod {
],
"custom-static-weapon-customizer"
);
// listen to ItemHelper.replaceIDs to keep in sync
container.afterResolution(
"ItemHelper",
(_, itemHelper: ItemHelper) => {
const originalReplaceIDs = itemHelper.replaceIDs;
itemHelper.replaceIDs = (originalItems, pmcData, insuredItems, fastPanel) => {
const results: IItem[] = originalReplaceIDs.call(
itemHelper,
originalItems,
pmcData,
insuredItems,
fastPanel
);
let dirty = false;
for (let i = 0; i < originalItems.length; i++) {
const oldId = originalItems[i]._id;
if (oldId in this.customizations) {
const newId = results[i]._id;
this.customizations[newId] = cloner.clone(this.customizations[oldId]);
dirty = true;
this.logger.logWithColor(
`WeaponCustomizer: Weapon ${oldId} is now ${newId}, customizations copied`,
LogTextColor.CYAN
);
}
}
if (dirty) {
this.save();
}
return results;
};
},
{ frequency: "Always" }
);
}
public postSptLoad(container: DependencyContainer): void {
this.profileHelper = container.resolve<ProfileHelper>("ProfileHelper");
this.clean();
const count = Object.keys(this.customizations).length;
if (count > 0) {
this.logger.logWithColor(`WeaponCustomizer: ${count} weapon customizations loaded`, LogTextColor.CYAN);
}
}
private async saveCustomization(payload: CustomizePayload): Promise<string> {
//this.logger.info(`WeaponCustomizer: Saving customization for weapon ${payload.weaponId}`);
if (Object.keys(payload.slots).length === 0) {
delete this.customizations[payload.weaponId];
} else {
this.customizations[payload.weaponId] = payload.slots;
private async saveCustomization(payload: CustomizedObject[]): Promise<string> {
for (const customizedObject of payload) {
if (!customizedObject || !customizedObject.id) {
this.logger.error("WeaponCustomizer: Bad save payload!");
return;
}
if (Object.keys(customizedObject.slots).length === 0) {
delete this.customizations[customizedObject.id];
} else {
this.customizations[customizedObject.id] = customizedObject;
}
}
await this.save();
return JSON.stringify({ success: true });
}
private load() {
private async load() {
try {
if (this.vfs.exists(this.filepath)) {
this.customizations = JSON.parse(this.vfs.readFile(this.filepath));
if (await this.fileSystem.exists(this.filepath)) {
const file = await this.fileSystem.readJson(this.filepath);
switch (file.version) {
case undefined:
this.customizations = this.convertV1ToCurrent(file);
this.save();
break;
case currentSaveFormatVersion:
this.customizations = file.customizations;
break;
default:
throw "Unknown file version!";
}
} else {
this.customizations = {};
// Create the file with fs - vfs.writeFile pukes on windows paths if it needs to create the file
fs.writeFileSync(this.filepath, JSON.stringify(this.customizations));
}
const count = Object.keys(this.customizations).length;
if (count > 0) {
this.logger.logWithColor(`WeaponCustomizer: ${count} weapon customizations loaded.`, LogTextColor.CYAN);
this.save();
}
} catch (error) {
this.logger.error("WeaponCustomizer: Failed to load weapon customization! " + error);
this.logger.error("WeaponCustomizer: Failed to load weapon customizations! " + error);
this.customizations = {};
}
}
@@ -106,38 +183,74 @@ class WeaponCustomizer implements IPreSptLoadMod, IPostSptLoadMod {
}
for (const profile of Object.values(this.profileHelper.getProfiles())) {
for (const item of profile.characters.pmc.Inventory.items) {
const items = profile.characters?.pmc?.Inventory?.items ?? [];
for (const item of items) {
if (map.has(item._id)) {
map.set(item._id, true);
}
}
const presets = profile.userbuilds?.weaponBuilds ?? [];
for (const preset of presets) {
if (map.has(preset.Id)) {
map.set(preset.Id, true);
}
}
for (const insurance of profile.insurance ?? []) {
for (const item of insurance.items ?? []) {
if (map.has(item._id)) {
map.set(item._id, true);
}
}
}
}
let dirtyCount = 0;
for (const [weaponId, found] of Object.entries(map)) {
for (const [id, found] of Object.entries(map)) {
if (!found) {
delete this.customizations[weaponId];
delete this.customizations[id];
dirtyCount++;
}
}
if (dirtyCount > 0) {
this.logger.logWithColor(
`WeaponCustomizer: Cleaned up ${dirtyCount} customizations for weapons that no longer exist`,
LogTextColor.CYAN
);
// this.logger.logWithColor(
// `WeaponCustomizer: Cleaned up ${dirtyCount} customizations for weapons/presets that no longer exist`,
// LogTextColor.CYAN
// );
await this.save();
}
}
private async save() {
const file: FileFormat = {
description:
"This is a record of all customizations that WeaponCustomizer has made. You can delete this file and restart your server to reset all customizations. Modify this file at your own risk.",
version: currentSaveFormatVersion,
customizations: this.customizations
};
try {
await this.vfs.writeFileAsync(this.filepath, JSON.stringify(this.customizations, null, 2));
await this.fileSystem.writeJson(this.filepath, file, 2);
} catch (error) {
this.logger.error("WeaponCustomizer: Failed to save weapon customization! " + error);
this.logger.error("WeaponCustomizer: Failed to save weapon customizations! " + error);
}
}
private convertV1ToCurrent(file: V1FileFormat): Customizations {
const result: Customizations = {};
for (const [key, value] of Object.entries(file)) {
result[key] = {
id: key,
type: "unknown",
slots: value
};
}
return result;
}
}
export const mod = new WeaponCustomizer();