From b19310aa20718654cde2958576b0c55607e8a88c Mon Sep 17 00:00:00 2001 From: Davide Fantino <80767709+fantinodavide@users.noreply.github.com> Date: Mon, 21 Nov 2022 15:15:11 +0100 Subject: [PATCH] feat: data persistency --- README.MD | 10 ++++++ mapvote.js | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 101 insertions(+), 6 deletions(-) diff --git a/README.MD b/README.MD index 3017e43..66ef761 100644 --- a/README.MD +++ b/README.MD @@ -183,6 +183,15 @@ The ID of the channel to log votes to. ```json "112233445566778899" ``` +#### persistentDataFile +###### Description +Path to file in which to store important data that should be restored after a restart. +###### Important +Make sure the persistent data file is not inside the plugin folder. +###### Default +```json +"" +``` #### timezone ###### Description Timezone relative to UTC time. 0 for UTC, 2 for CEST (UTC+2), -1 (UTC-1) @@ -235,6 +244,7 @@ Array of timeframes that allows to override options based on local time. See exa "allowedSameMapEntries": 1, "logToDiscord": true, "channelID": "112233445566778899", + "persistentDataFile": "", "timezone": 2, "timeFrames": [ { diff --git a/mapvote.js b/mapvote.js index e2b4a31..a6f5a16 100644 --- a/mapvote.js +++ b/mapvote.js @@ -5,6 +5,8 @@ import DiscordBasePlugin from './discord-base-plugin.js'; import { Layers } from "../layers/index.js" import axios from "axios" import Layer from '../layers/layer.js'; +import fs from 'fs' +import process from 'process' export default class MapVote extends DiscordBasePlugin { static get description() { @@ -134,6 +136,11 @@ export default class MapVote extends DiscordBasePlugin { default: '', example: '112233445566778899' }, + persistentDataFile: { + required: false, + description: 'Path to file in which to store important data that should be restored after a restart', + default: "" + }, timezone: { required: false, description: "Timezone relative to UTC time. 0 for UTC, 2 for CEST (UTC+2), -1 (UTC-1) ", @@ -159,7 +166,6 @@ export default class MapVote extends DiscordBasePlugin { this.trackedVotes = {}; //player votes, keyed by steam id this.tallies = []; //votes per layer, parellel with nominations this.votingEnabled = false; - this.onConnectBound = false; this.broadcastIntervalTask = null; this.firstBroadcast = true; this.newVoteTimeout = null; @@ -181,13 +187,21 @@ export default class MapVote extends DiscordBasePlugin { this.setSeedingMode = this.setSeedingMode.bind(this); this.logVoteToDiscord = this.logVoteToDiscord.bind(this); this.timeframeOptionOverrider = this.timeframeOptionOverrider.bind(this); + this.savePersistentData = this.savePersistentData.bind(this) + this.restorePersistentData = this.restorePersistentData.bind(this) this.broadcast = (msg) => { this.server.rcon.broadcast(msg); }; this.warn = (steamid, msg) => { this.server.rcon.warn(steamid, msg); }; + + process.on('uncaughtException', function (err) { + this.savePersistentData(); + throw err; + }); } async mount() { await this.updateLayerList(); + this.restorePersistentData(); this.server.on('NEW_GAME', this.onNewGame); this.server.on('CHAT_MESSAGE', this.onChatMessage); this.server.on('PLAYER_DISCONNECTED', this.onPlayerDisconnected); @@ -200,6 +214,7 @@ export default class MapVote extends DiscordBasePlugin { // await this.checkUpdates(); this.timeframeOptionOverrider(); setInterval(this.timeframeOptionOverrider, 1 * 60 * 1000) + setInterval(this.savePersistentData, 20 * 1000) } async unmount() { @@ -399,6 +414,15 @@ export default class MapVote extends DiscordBasePlugin { await this.warn(steamID, msg + `\nMapVote SquadJS plugin built by JetDave`); return; + case "endsqjs": + case "closesqjs": + case "stopesqjs": + case "restartsqjs": + if (!isAdmin) return; + this.warn(steamID, "Saving persistent data.\nTerminating SquadJS process.\nIf managed by a process manager it will automatically restart.") + this.savePersistentData(); + process.exit(0); + return; default: //give them an error await this.warn(steamID, `Unknown vote subcommand: ${subCommand}`); @@ -495,10 +519,11 @@ export default class MapVote extends DiscordBasePlugin { const sanitizedLayers = Layers.layers.filter((l) => l.layerid && l.map); const maxOptions = this.options.showRerollOption ? 5 : 6; - if (!cmdLayers || cmdLayers.length == 0) { - const recentlyPlayedMaps = this.objArrToValArr(this.server.layerHistory.slice(0, this.options.numberRecentMapsToExlude), "layer", "map", "name"); - this.verbose(1, "Recently played maps: " + recentlyPlayedMaps.filter((l) => l && l.map && l.map.name).map((l) => l.map.name).join(', ')) + const recentlyPlayedMaps = this.objArrToValArr(this.server.layerHistory.slice(0, this.options.numberRecentMapsToExlude), "layer", "map", "name"); + this.verbose(1, "Recently played maps: " + recentlyPlayedMaps.join(', '));//recentlyPlayedMaps.filter((l) => l && l.map && l.map.name).map((l) => l.map.name).join(', ')) + + if (!cmdLayers || cmdLayers.length == 0) { const all_layers = sanitizedLayers.filter((l) => this.options.gamemodeWhitelist.includes(l.gamemode.toUpperCase()) && ![ this.server.currentLayer ? this.server.currentLayer.map.name : null, ...recentlyPlayedMaps ].includes(l.map.name) && @@ -531,7 +556,7 @@ export default class MapVote extends DiscordBasePlugin { return; } } else { - if (cmdLayers.length == 1) for (let i = 0; i < maxOptions; i++) cmdLayers.push(cmdLayers[ 0 ]) + if (cmdLayers.length == 1) while (cmdLayers.length < maxOptions) cmdLayers.push(cmdLayers[ 0 ]) if (cmdLayers.length <= maxOptions) { let i = 1; @@ -764,6 +789,66 @@ export default class MapVote extends DiscordBasePlugin { } } + restorePersistentData() { + this.verbose(1, `Restoring persistent data from: ${this.options.persistentDataFile}`) + + if (this.options.persistentDataFile == "") return; + + if (!fs.existsSync(this.options.persistentDataFile)) return; + + let bkData = fs.readFileSync(this.options.persistentDataFile); + if (bkData == "") return; + + try { + bkData = JSON.parse(bkData) + } catch (e) { + this.verbose(1, "Error restoring persistent data", e) + return + } + + for (let k in bkData.server) this.server[ k ] = bkData.server[ k ]; + + const maxSecondsDiffierence = 60 + if ((new Date() - new Date(bkData.saveDateTime)) / 1000 > maxSecondsDiffierence) return + + this.verbose(1, "Restoring data:", bkData) + + // if (bkData.custom.layerHistory) this.server.layerHistory = Layers.layers.filter(l => bkData.custom.layerHistory.includes(l.layerid)); + this.verbose(1, "Recently played maps: " + this.server.layerHistory.filter((l) => l && l.map && l.map.name).map((l) => l.layer.map.name).join(', ')) + + for (let k in bkData.plugin) this[ k ] = bkData.plugin[ k ]; + if (this.votingEnabled) { + this.broadcastIntervalTask = setInterval(this.broadcastNominations, toMils(this.options.voteBroadcastInterval)); + } + } + + + savePersistentData() { + if (this.options.persistentDataFile == "") return; + + + const saveDt = { + custom: { + // layerHistory: this.server.layerHistory.slice(0, this.options.numberRecentMapsToExlude * 2).filter(l => l && l.layerid).map(l => l.layerid), + }, + server: { + layerHistory: this.server.layerHistory + }, + plugin: { + nominations: this.nominations, + trackedVotes: this.trackedVotes, + tallies: this.tallies, + votingEnabled: this.votingEnabled, + factionStrings: this.factionStrings, + firstBroadcast: this.firstBroadcast + }, + saveDateTime: new Date() + } + // this.verbose(1, `Saving persistent data to: ${this.options.persistentDataFile}\n`, saveDt.server.layerHistory) + + fs.writeFileSync(this.options.persistentDataFile, JSON.stringify(saveDt, null, 2)) + } + //calculates the current winner(s) of the vote and returns thier strings in an array get currentWinners() { const ties = []; @@ -812,4 +897,4 @@ function formatChoice(choiceIndex, mapString, currentVotes, firstBroadcast) { function toMils(min) { return min * 60 * 1000; -} \ No newline at end of file +}