/* * The Peacock Project - a HITMAN server replacement. * Copyright (C) 2021-2023 The Peacock Project Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ import { existsSync, readFileSync, writeFileSync } from "fs" import type { GameVersion, Loadout, LoadoutFile, LoadoutsGameVersion, } from "./types/types" import { Request, Router } from "express" import { json as jsonMiddleware } from "body-parser" import { writeFile } from "atomically" import { nanoid } from "nanoid" import { versions } from "./utils" const LOADOUT_PROFILES_FILE = "userdata/users/lop.json" const defaultValue: LoadoutFile = { h1: { selected: null, loadouts: [], }, h2: { selected: null, loadouts: [], }, h3: { selected: null, loadouts: [], }, } /** * A class for managing loadouts. */ export class Loadouts { private _loadouts: LoadoutFile /** * Creates a new instance of the class. */ public constructor() { this._loadouts = undefined } /** * Get the loadouts data. * * @returns The loadouts data. */ public get loadouts(): LoadoutFile { return this._loadouts } /** * Mutate the current LoadoutFile object. * * @internal Intended for internal use only. * @param newValue The object after the mutation. */ set loadouts(newValue: LoadoutFile) { this._loadouts = newValue } /** * Initializes the loadouts manager. */ public init(): void { if (!existsSync(LOADOUT_PROFILES_FILE)) { this._loadouts = defaultValue writeFileSync(LOADOUT_PROFILES_FILE, JSON.stringify(defaultValue)) return } this._loadouts = JSON.parse( readFileSync(LOADOUT_PROFILES_FILE).toString(), ) let dirty = false // make sure they all have IDs for (const gameVersion of versions) { for (const loadout of this._loadouts[gameVersion].loadouts) { if (!loadout.id) { dirty = true loadout.id = nanoid() } } // if the selected value is null/undefined or is not length 0 or 21, it's not a valid id if ( !this._loadouts[gameVersion].selected || ![0, 21].includes(this._loadouts[gameVersion].selected.length) ) { dirty = true // long story short: find a loadout with a name matching the selected value, // and if found, set selected to the id this._loadouts[gameVersion].selected = this._loadouts[gameVersion].loadouts.find( (lo) => lo.name === this._loadouts[gameVersion].selected, )?.id || "" } } if (dirty === true) { writeFileSync(LOADOUT_PROFILES_FILE, JSON.stringify(this._loadouts)) } } /** * Create the default loadout (or just a new loadout) for the specified game version, and optionally with a name. * * @param gameVersion The game version to perform the operation on. * @param name The optional name for the new loadout set, defaults to "Unnamed loadout set". * @returns The Loadout object. */ public createDefault( gameVersion: GameVersion, name = "Unnamed loadout set", ): Loadout { if (gameVersion === "scpc") { gameVersion = "h1" } const l: Loadout = { name, id: nanoid(), data: {}, } this._loadouts[gameVersion].loadouts.push(l) this._loadouts[gameVersion].selected = l.id return l } /** * Get the active loadout profile for the specified game version. May be undefined. * * @param gameVersion The game version. * @returns The loadout profile or undefined if one isn't selected or none exist. */ public getLoadoutFor(gameVersion: GameVersion): Loadout | undefined { if (gameVersion === "scpc") { gameVersion = "h1" } const theLoadouts = this._loadouts[gameVersion] as LoadoutsGameVersion return theLoadouts.loadouts.find((s) => s.id === theLoadouts.selected) } /** * Saves the loadout data to the Peacock userdata/users folder. */ public async save(): Promise<void> { await writeFile(LOADOUT_PROFILES_FILE, JSON.stringify(this._loadouts)) } } /** * A synthetic default bind to the global Loadouts instance. */ export const loadouts = new Loadouts() /** * Router object for loadout-related web requests. */ export const loadoutRouter = Router() if (PEACOCK_DEV) { loadoutRouter.use((_req, res, next) => { res.set("Access-Control-Allow-Origin", "*") res.set( "Access-Control-Allow-Methods", "GET,HEAD,PUT,PATCH,POST,DELETE", ) res.set("Access-Control-Allow-Headers", "Content-Type") next() }) } loadoutRouter.get("/all-loadouts", (req, res) => { res.json(loadouts.loadouts) }) loadoutRouter.patch("/update", jsonMiddleware(), async (req, res) => { // todo: perform validation on this loadouts.loadouts = req.body await loadouts.save() res.json({ message: "request completed" }) }) loadoutRouter.patch( "/remove", jsonMiddleware(), async ( req: Request< never, string, { gameVersion: "h1" | "h2" | "h3"; id: string } >, res, ) => { // check for gameVersion if (!req.body.gameVersion) { res.status(400).json({ error: "missing gv" }) return } // validate gameVersion if (!versions.includes(req.body.gameVersion)) { res.status(400).json({ error: "invalid gv" }) return } // check for id if (!req.body.id) { res.status(400).json({ error: "missing id" }) return } const data = loadouts.loadouts const withoutDeletionTarget = data[ req.body.gameVersion ].loadouts.filter((l) => { return l.id !== data[req.body.gameVersion].selected }) if (withoutDeletionTarget.length === 0) { data[req.body.gameVersion].loadouts = [] // we have no other loadouts, so make a default one loadouts.createDefault(req.body.gameVersion) } else { // we have other loadouts, so pick the first one data[req.body.gameVersion].loadouts = withoutDeletionTarget data[req.body.gameVersion].selected = withoutDeletionTarget[0]?.id } loadouts.loadouts = data await loadouts.save() res.json({ message: "request completed" }) }, ) loadoutRouter.post("/create", jsonMiddleware(), async (req, res) => { if (!versions.includes(req.body.gameVersion)) { res.status(400).json({ message: "invalid gv" }) return } loadouts.createDefault(req.body.gameVersion) await loadouts.save() res.json({ message: "success" }) })