Archived
Various Mod Updates for SPT 3.11
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user