Updated Weapon Customizer

This commit is contained in:
Rage 2025-01-07 06:56:55 -05:00
parent 02aac2b4da
commit d3e8098b26
9 changed files with 208 additions and 5 deletions

View File

@ -1,11 +1,11 @@
[General] [General]
gameName=spt gameName=spt
modid=0 modid=0
version=d2025.1.6.0 version=d2025.1.7.0
newestVersion= newestVersion=
category="2," category="2,"
nexusFileStatus=1 nexusFileStatus=1
installationFile=Tyfon-WeaponCustomizer-1.0.0.zip installationFile=Tyfon-WeaponCustomizer-1.0.1.zip
repository=Nexus repository=Nexus
ignoredVersion= ignoredVersion=
comments= comments=

View File

@ -1,6 +1,6 @@
{ {
"name": "weapon-customizer", "name": "weapon-customizer",
"version": "1.0.0", "version": "1.0.1",
"main": "src/mod.js", "main": "src/mod.js",
"license": "MIT", "license": "MIT",
"author": "Tyfon", "author": "Tyfon",

View File

@ -1,5 +1,7 @@
import type { DependencyContainer } from "tsyringe"; import type { DependencyContainer } from "tsyringe";
import type { ProfileHelper } from "@spt/helpers/ProfileHelper";
import type { IPostSptLoadMod } from "@spt/models/external/IPostSptLoadMod";
import type { IPreSptLoadMod } from "@spt/models/external/IPreSptLoadMod"; import type { IPreSptLoadMod } from "@spt/models/external/IPreSptLoadMod";
import { LogTextColor } from "@spt/models/spt/logging/LogTextColor"; import { LogTextColor } from "@spt/models/spt/logging/LogTextColor";
import type { ILogger } from "@spt/models/spt/utils/ILogger"; import type { ILogger } from "@spt/models/spt/utils/ILogger";
@ -26,15 +28,17 @@ type CustomizePayload = {
type Customizations = Record<string, Record<string, CustomPosition>>; type Customizations = Record<string, Record<string, CustomPosition>>;
class WeaponCustomizer implements IPreSptLoadMod { class WeaponCustomizer implements IPreSptLoadMod, IPostSptLoadMod {
private logger: ILogger; private logger: ILogger;
private vfs: VFS; private vfs: VFS;
private profileHelper: ProfileHelper;
private customizations: Customizations = null; private customizations: Customizations = null;
private filepath: string; private filepath: string;
public preSptLoad(container: DependencyContainer): void { public preSptLoad(container: DependencyContainer): void {
this.logger = container.resolve<ILogger>("PrimaryLogger"); this.logger = container.resolve<ILogger>("PrimaryLogger");
this.vfs = container.resolve<VFS>("VFS"); this.vfs = container.resolve<VFS>("VFS");
const staticRouterModService = container.resolve<StaticRouterModService>("StaticRouterModService"); const staticRouterModService = container.resolve<StaticRouterModService>("StaticRouterModService");
this.filepath = path.resolve(__dirname, "../customizations.json"); this.filepath = path.resolve(__dirname, "../customizations.json");
@ -56,6 +60,11 @@ class WeaponCustomizer implements IPreSptLoadMod {
); );
} }
public postSptLoad(container: DependencyContainer): void {
this.profileHelper = container.resolve<ProfileHelper>("ProfileHelper");
this.clean();
}
private async saveCustomization(payload: CustomizePayload): Promise<string> { private async saveCustomization(payload: CustomizePayload): Promise<string> {
//this.logger.info(`WeaponCustomizer: Saving customization for weapon ${payload.weaponId}`); //this.logger.info(`WeaponCustomizer: Saving customization for weapon ${payload.weaponId}`);
if (Object.keys(payload.slots).length === 0) { if (Object.keys(payload.slots).length === 0) {
@ -64,6 +73,7 @@ class WeaponCustomizer implements IPreSptLoadMod {
this.customizations[payload.weaponId] = payload.slots; this.customizations[payload.weaponId] = payload.slots;
} }
await this.save(); await this.save();
return JSON.stringify({ success: true }); return JSON.stringify({ success: true });
} }
@ -88,6 +98,39 @@ class WeaponCustomizer implements IPreSptLoadMod {
} }
} }
// Remove any customizations for items that no longer exist
private async clean() {
const map = new Map<string, boolean>();
for (const weaponId of Object.keys(this.customizations)) {
map.set(weaponId, false);
}
for (const profile of Object.values(this.profileHelper.getProfiles())) {
for (const item of profile.characters.pmc.Inventory.items) {
if (map.has(item._id)) {
map.set(item._id, true);
}
}
}
let dirtyCount = 0;
for (const [weaponId, found] of Object.entries(map)) {
if (!found) {
delete this.customizations[weaponId];
dirtyCount++;
}
}
if (dirtyCount > 0) {
this.logger.logWithColor(
`WeaponCustomizer: Cleaned up ${dirtyCount} customizations for weapons that no longer exist`,
LogTextColor.CYAN
);
await this.save();
}
}
private async save() { private async save() {
try { try {
await this.vfs.writeFileAsync(this.filepath, JSON.stringify(this.customizations, null, 2)); await this.vfs.writeFileAsync(this.filepath, JSON.stringify(this.customizations, null, 2));

View File

@ -0,0 +1,28 @@
[General]
gameName=spt
modid=0
version=d2025.1.6.0
newestVersion=
category="2,"
nexusFileStatus=1
installationFile=Tyfon-WeaponCustomizer-1.0.0.zip
repository=Nexus
ignoredVersion=
comments=
notes=
nexusDescription=
url=
hasCustomURL=false
lastNexusQuery=
lastNexusUpdate=
nexusLastModified=2025-01-07T00:12:11Z
nexusCategory=0
converted=false
validated=false
color=@Variant(\0\0\0\x43\0\xff\xff\0\0\0\0\0\0\0\0)
tracked=0
[installedFiles]
1\modid=0
1\fileid=0
size=1

View File

@ -0,0 +1,33 @@
{
"name": "weapon-customizer",
"version": "1.0.0",
"main": "src/mod.js",
"license": "MIT",
"author": "Tyfon",
"sptVersion": "~3.10",
"loadBefore": [],
"loadAfter": [],
"incompatibilities": [],
"contributors": [],
"isBundleMod": false,
"scripts": {
"setup": "npm i",
"build": "node ./build.mjs",
"buildinfo": "node ./build.mjs --verbose"
},
"devDependencies": {
"@types/node": "20.11",
"@typescript-eslint/eslint-plugin": "7.2",
"@typescript-eslint/parser": "7.2",
"archiver": "^6.0",
"eslint": "8.57",
"eslint-config-prettier": "^9.1.0",
"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"
}
}

View File

@ -0,0 +1,100 @@
import type { DependencyContainer } from "tsyringe";
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 path from "node:path";
type Vector3 = {
x: number;
y: number;
z: number;
};
type CustomPosition = {
original: Vector3;
modified: Vector3;
};
type CustomizePayload = {
weaponId: string;
slots: Record<string, CustomPosition>;
};
type Customizations = Record<string, Record<string, CustomPosition>>;
class WeaponCustomizer implements IPreSptLoadMod {
private logger: ILogger;
private vfs: VFS;
private customizations: Customizations = null;
private filepath: string;
public preSptLoad(container: DependencyContainer): void {
this.logger = container.resolve<ILogger>("PrimaryLogger");
this.vfs = container.resolve<VFS>("VFS");
const staticRouterModService = container.resolve<StaticRouterModService>("StaticRouterModService");
this.filepath = path.resolve(__dirname, "../customizations.json");
this.load();
staticRouterModService.registerStaticRouter(
"WeaponCustomizerRoutes",
[
{
url: "/weaponcustomizer/save",
action: async (url, info: CustomizePayload, sessionId, output) => this.saveCustomization(info)
},
{
url: "/weaponcustomizer/load",
action: async (url, info, sessionId, output) => JSON.stringify(this.customizations)
}
],
"custom-static-weapon-customizer"
);
}
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;
}
await this.save();
return JSON.stringify({ success: true });
}
private load() {
try {
if (this.vfs.exists(this.filepath)) {
this.customizations = JSON.parse(this.vfs.readFile(this.filepath));
} 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);
}
} catch (error) {
this.logger.error("WeaponCustomizer: Failed to load weapon customization! " + error);
this.customizations = {};
}
}
private async save() {
try {
await this.vfs.writeFileAsync(this.filepath, JSON.stringify(this.customizations, null, 2));
} catch (error) {
this.logger.error("WeaponCustomizer: Failed to save weapon customization! " + error);
}
}
}
export const mod = new WeaponCustomizer();

View File

@ -1,4 +1,3 @@
# This file was automatically generated by Mod Organizer.
-Unsorted_separator -Unsorted_separator
-Visceral Combat -Visceral Combat
-SWAG + DONUTS -SWAG + DONUTS