squad-js-map-vote/mapvote.js

1144 lines
51 KiB
JavaScript
Raw Normal View History

2022-09-16 05:20:18 -05:00
//Plugin reworked by JetDave, original version by MaskedMonkeyMan
2022-03-19 18:11:24 -05:00
2022-09-16 18:10:05 -05:00
// import BasePlugin from "./base-plugin.js";
import DiscordBasePlugin from './discord-base-plugin.js';
2022-03-19 18:11:24 -05:00
import { Layers } from "../layers/index.js"
2022-09-15 07:05:06 -05:00
import axios from "axios"
2022-11-17 17:22:21 -06:00
import Layer from '../layers/layer.js';
2022-11-21 08:15:11 -06:00
import fs from 'fs'
import process from 'process'
2022-03-19 18:11:24 -05:00
2022-09-16 18:10:05 -05:00
export default class MapVote extends DiscordBasePlugin {
2022-08-13 05:05:40 -05:00
static get description() {
2022-03-19 18:11:24 -05:00
return "Map Voting plugin";
}
2022-08-13 05:05:40 -05:00
static get defaultEnabled() {
2022-03-19 18:11:24 -05:00
return true;
}
2022-08-13 05:05:40 -05:00
static get optionsSpecification() {
2022-03-19 18:11:24 -05:00
return {
2022-09-16 18:10:05 -05:00
...DiscordBasePlugin.optionsSpecification,
2022-03-19 18:11:24 -05:00
commandPrefix:
{
required: false,
description: "command name to use in chat",
default: "!vote"
},
2023-02-28 16:54:37 -06:00
entryFormat: {
required: false,
description: "The format of an entry in the voting list",
2023-02-28 16:57:08 -06:00
default: '{map_name} {gamemode} {map_version} {factions} {main_assets}'
2023-02-28 16:54:37 -06:00
},
entriesAmount: {
required: false,
description: "Amount of entries generated for automatic votes",
default: 6
},
2022-09-15 07:05:06 -05:00
automaticVoteStart: {
required: false,
description: "a map vote will automatically start after a new match if set to true",
default: true
},
2022-10-20 16:59:28 -05:00
votingDuration: {
required: false,
description: "How long the voting will be active (in minutes). Set to 0 for unlimited time.",
default: 0
},
2022-08-13 05:05:40 -05:00
minPlayersForVote:
2022-03-19 18:11:24 -05:00
{
required: false,
description: 'number of players needed on the server for a vote to start',
default: 40
2022-03-19 18:11:24 -05:00
},
voteWaitTimeFromMatchStart:
{
required: false,
description: 'time in mins from the start of a round to the start of a new map vote',
default: 15
2022-03-19 18:11:24 -05:00
},
voteBroadcastInterval:
{
required: false,
description: 'broadcast interval for vote notification in mins',
default: 7
},
automaticSeedingMode:
{
required: false,
description: 'set a seeding layer if server has less than 20 players',
default: true
},
2022-08-31 04:24:12 -05:00
numberRecentMapsToExlude: {
required: false,
description: 'random layer list will not include the n. recent maps',
default: 4
},
gamemodeWhitelist: {
required: false,
description: 'random layer list will be generated with only selected gamemodes',
default: [ "AAS", "RAAS", "INVASION" ]
},
layerFilteringMode: {
required: false,
description: "Select Whitelist mode or Blacklist mode",
default: "blacklist"
},
layerLevelWhitelist: {
required: false,
2022-09-21 09:46:11 -05:00
description: 'random layer list will include only the whitelisted layers or levels. (acceptable formats: Gorodok/Gorodok_RAAS/Gorodok_AAS_v1)',
default: []
},
layerLevelBlacklist: {
required: false,
description: 'random layer list will not include the blacklisted layers or levels. (acceptable formats: Gorodok/Gorodok_RAAS/Gorodok_AAS_v1)',
default: []
},
applyBlacklistToWhitelist: {
required: false,
description: 'if set to true the blacklisted layers won\'t be included also in whitelist mode',
default: true
},
2022-11-18 13:30:14 -06:00
factionsBlacklist: {
required: false,
description: "factions to exclude in map vote. ( ex: ['CAF'] )",
default: []
},
minRaasEntries: {
required: false,
description: 'Minimum amount of RAAS layers in the vote list.',
default: 2
},
2022-09-15 17:41:20 -05:00
hideVotesCount: {
required: false,
2022-09-15 18:07:54 -05:00
description: 'hides the number of votes a layer received in broadcast message',
2022-09-15 17:41:20 -05:00
default: false
},
showRerollOption: {
required: false,
2022-09-15 18:07:54 -05:00
description: 'vote option to restart the vote with random entries',
2022-09-15 17:41:20 -05:00
default: false
2022-09-16 15:41:55 -05:00
},
2023-01-25 17:21:34 -06:00
showRerollOptionInCustomVotes: {
required: false,
description: 'enables/disables the reroll option only in custom votes. showRerollOption must be set to true',
default: false
},
2022-09-16 15:41:55 -05:00
voteBroadcastMessage: {
required: false,
description: 'Message that is sent as broadcast to announce a vote',
2022-09-16 18:10:05 -05:00
default: "✯ MAPVOTE ✯\nVote for the next map by writing in chat the corresponding number!"
},
voteWinnerBroadcastMessage: {
required: false,
description: 'Message that is sent as broadcast to announce the winning layer',
default: "✯ MAPVOTE ✯\nThe winning layer is\n\n"
},
showWinnerBroadcastMessage: {
required: false,
description: 'Enables the broadcast at the end of the voting.',
default: true
},
allowedSameMapEntries: {
required: false,
description: 'Allowed NUMBER of duplicate map entries in vote list',
default: 1
},
logToDiscord: {
required: false,
2022-09-16 16:36:48 -05:00
description: 'Enables/disables vote logging to Discord',
default: false
},
channelID: {
required: false,
description: 'The ID of the channel to log votes to.',
default: '',
2022-09-16 16:36:48 -05:00
example: '112233445566778899'
},
2022-11-21 08:15:11 -06:00
persistentDataFile: {
required: false,
description: 'Path to file in which to store important data that should be restored after a restart',
default: ""
},
2022-09-20 16:21:05 -05:00
timezone: {
required: false,
description: "Timezone relative to UTC time. 0 for UTC, 2 for CEST (UTC+2), -1 (UTC-1) ",
default: 0
},
2023-02-28 16:54:37 -06:00
minimumVotesToAcceptResult: {
2023-01-20 17:49:06 -06:00
required: false,
2023-02-28 16:54:37 -06:00
description: "Minimum votes per map to accept result.",
default: 1
2023-01-20 17:49:06 -06:00
},
timeFrames: {
required: false,
description: 'Array of timeframes to override options',
default: []
2022-04-14 04:08:53 -05:00
}
2022-03-19 18:11:24 -05:00
};
}
2022-08-13 05:05:40 -05:00
constructor(server, options, connectors) {
2022-03-19 18:11:24 -05:00
super(server, options, connectors);
2022-08-13 05:05:40 -05:00
2022-09-20 14:29:46 -05:00
this.options.timeFrames.forEach((e, key, arr) => { arr[ key ].id = key + 1 });
2022-11-20 15:46:49 -06:00
if (this.options.allowedSameMapEntries < 1) this.options.allowedSameMapEntries = 1
2022-03-19 18:11:24 -05:00
this.voteRules = {}; //data object holding vote configs
this.nominations = []; //layer strings for the current vote choices
2022-08-13 05:05:40 -05:00
this.trackedVotes = {}; //player votes, keyed by steam id
2022-03-19 18:11:24 -05:00
this.tallies = []; //votes per layer, parellel with nominations
this.votingEnabled = false;
this.broadcastIntervalTask = null;
2022-08-13 05:05:40 -05:00
this.firstBroadcast = true;
2022-09-15 17:41:20 -05:00
this.newVoteTimeout = null;
2022-09-16 13:35:08 -05:00
this.newVoteOptions = {
steamid: null,
cmdLayers: [],
bypassRaasFilter: false
};
this.or_options = { ...this.options };
2022-09-28 15:42:53 -05:00
this.autovotestart = null;
this.lastMapUpdate = new Date();
this.endVotingTimeout = null;
2022-10-25 12:51:05 -05:00
this.timeout_ps = []
2022-08-13 05:05:40 -05:00
2022-03-19 18:11:24 -05:00
this.onNewGame = this.onNewGame.bind(this);
this.onPlayerDisconnected = this.onPlayerDisconnected.bind(this);
this.onChatMessage = this.onChatMessage.bind(this);
2022-08-13 05:05:40 -05:00
this.broadcastNominations = this.broadcastNominations.bind(this);
2022-03-19 18:11:24 -05:00
this.beginVoting = this.beginVoting.bind(this);
2022-09-07 17:46:37 -05:00
this.setSeedingMode = this.setSeedingMode.bind(this);
2022-09-16 18:33:21 -05:00
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.endVotingGently = this.endVotingGently.bind(this);
this.formatChoice = this.formatChoice.bind(this);
2023-02-28 17:36:38 -06:00
this.updateNextMap = this.updateNextMap.bind(this);
this.mapLayer = this.mapLayer.bind(this);
2022-03-19 18:11:24 -05:00
2023-01-20 17:49:06 -06:00
this.broadcast = async (msg) => { await this.server.rcon.broadcast(msg); };
this.warn = async (steamid, msg) => { await this.server.rcon.warn(steamid, msg); };
2022-11-21 08:15:11 -06:00
process.on('uncaughtException', this.savePersistentData);
2022-03-19 18:11:24 -05:00
}
2022-08-13 05:05:40 -05:00
async mount() {
2022-11-17 17:22:21 -06:00
await this.updateLayerList();
2022-11-21 08:15:11 -06:00
this.restorePersistentData();
2022-08-13 05:05:40 -05:00
this.server.on('NEW_GAME', this.onNewGame);
2022-03-19 18:11:24 -05:00
this.server.on('CHAT_MESSAGE', this.onChatMessage);
this.server.on('PLAYER_DISCONNECTED', this.onPlayerDisconnected);
this.server.on('ROUND_ENDED', this.endVotingGently)
2022-09-20 16:21:05 -05:00
setTimeout(() => {
this.verbose(1, 'Enabled late listeners.');
this.server.on('PLAYER_CONNECTED', this.setSeedingMode);
}, 15 * 1000) // wait 10 seconds to be sure to have an updated player list
2022-08-13 05:05:40 -05:00
this.verbose(1, 'Map vote was mounted.');
2022-09-06 16:21:33 -05:00
this.verbose(1, "Blacklisted Layers/Levels: " + this.options.layerLevelBlacklist.join(', '))
2022-09-15 17:41:20 -05:00
// await this.checkUpdates();
this.timeframeOptionOverrider();
2022-09-17 18:19:19 -05:00
setInterval(this.timeframeOptionOverrider, 1 * 60 * 1000)
2022-11-21 08:15:11 -06:00
setInterval(this.savePersistentData, 20 * 1000)
2022-03-19 18:11:24 -05:00
}
2022-08-13 05:05:40 -05:00
async unmount() {
this.server.removeEventListener('NEW_GAME', this.onNewGame);
2022-03-19 18:11:24 -05:00
this.server.removeEventListener('CHAT_MESSAGE', this.onChatMessage);
this.server.removeEventListener('PLAYER_DISCONNECTED', this.onPlayerDisconnected);
clearInterval(this.broadcastIntervalTask);
2022-08-13 05:05:40 -05:00
this.verbose(1, 'Map vote was un-mounted.');
2022-03-19 18:11:24 -05:00
}
2022-08-13 05:05:40 -05:00
async onNewGame() {
for (let x of this.timeout_ps)
clearTimeout(x)
this.timeout_ps = [];
if (this.options.automaticVoteStart) this.autovotestart = setTimeout(this.beginVoting, toMils(this.options.voteWaitTimeFromMatchStart));
// this.endVotingTimeout = setTimeout(async () => {
// this.endVoting();
// this.trackedVotes = {};
// this.tallies = [];
// this.nominations = [];
// this.factionStrings = [];
// // setTimeout(() => this.setSeedingMode(true), 10000);
// }, 10000)
2022-08-13 05:05:40 -05:00
}
2022-08-16 16:53:08 -05:00
2022-08-13 05:05:40 -05:00
async onPlayerDisconnected() {
if (!this.votingEnabled) return;
2022-08-13 05:05:40 -05:00
await this.server.updatePlayerList();
2022-03-19 18:11:24 -05:00
this.clearVote();
if (new Date() - this.lastMapUpdate > 5 * 1000) this.updateNextMap();
}
async timeframeOptionOverrider() {
2022-09-20 14:29:46 -05:00
const orOpt = { ...this.or_options };
2022-09-20 16:21:05 -05:00
const utcDelay = parseFloat(this.options.timezone);
let timeNow = new Date(0, 0, 0, new Date().getUTCHours() + utcDelay, new Date().getUTCMinutes());
timeNow = new Date(0, 0, 0, timeNow.getHours(), timeNow.getMinutes())
// console.log(timeNow, timeNow.toTimeString(), timeNow.toLocaleTimeString())
this.verbose(1, `Current time (UTC${(utcDelay >= 0 ? '+' : '') + utcDelay}) ${timeNow.toLocaleTimeString('en-GB').split(':').splice(0, 2).join(':')} `)
2022-09-20 14:29:46 -05:00
const activeTimeframes = orOpt.timeFrames.filter(tfFilter);
2022-09-17 18:19:19 -05:00
let logTimeframe = "Active Time Frames: ";
let activeTfIds = [];
2022-09-18 14:45:29 -05:00
this.options = { ...this.or_options };
2022-09-17 18:19:19 -05:00
for (let atfK in activeTimeframes) {
const atf = activeTimeframes[ atfK ];
activeTfIds.push(atf.name || atf.id);
2022-09-18 07:19:22 -05:00
for (let o in atf.overrides) {
this.options[ o ] = atf.overrides[ o ];
2022-09-17 18:19:19 -05:00
}
}
this.verbose(1, logTimeframe + activeTfIds.join(', '));
2022-09-20 16:21:05 -05:00
function tfFilter(tf, key, arr) {
const tfStartSplit = [ parseInt(tf.start.split(':')[ 0 ]), parseInt(tf.start.split(':')[ 1 ]) ];
const tfEndSplit = [ parseInt(tf.end.split(':')[ 0 ]), parseInt(tf.end.split(':')[ 1 ]) ];
const tfStart = new Date(0, 0, 0, ...tfStartSplit)
const tfStart2 = new Date(0, 0, 0, 0, 0)
const tfEnd = new Date(0, 0, 0, ...tfEndSplit)
const tfEnd2 = new Date(0, 0, 0, 24, 0)
// console.log(timeNow, tfStart, tfEnd, tfStart2 <= timeNow, timeNow < tfEnd)
2022-09-20 16:21:05 -05:00
return (tfStart <= timeNow && timeNow < tfEnd) || (tfStart > tfEnd && ((tfStart <= timeNow && timeNow < tfEnd2) || (tfStart2 <= timeNow && timeNow < tfEnd)))
}
}
2022-09-07 18:06:20 -05:00
setSeedingMode(isNewGameEvent = false) {
2022-08-25 08:43:38 -05:00
// this.msgBroadcast("[MapVote] Seeding mode active")
2022-09-07 17:46:37 -05:00
const baseDataExist = this && this.options && this.server && this.server.players;
if (baseDataExist) {
if (this.options.automaticSeedingMode) {
this.verbose(1, "Checking seeding mode");
2022-09-07 17:46:37 -05:00
if (this.server.players.length >= 1 && this.server.players.length < 40) {
2022-09-10 18:09:28 -05:00
const seedingMaps = Layers.layers.filter((l) => l.layerid && l.gamemode.toUpperCase() == "SEED" && !this.options.layerLevelBlacklist.find((fl) => l.layerid.toLowerCase().startsWith(fl.toLowerCase())))
2022-09-15 07:05:06 -05:00
2022-09-16 09:12:48 -05:00
const rndMap = randomElement(seedingMaps);
2022-09-07 17:46:37 -05:00
if (this.server.currentLayer) {
2022-09-10 18:09:28 -05:00
if (this.server.currentLayer.gamemode.toLowerCase() != "seed") {
if (this.server.players.length <= 5) {
const newCurrentMap = rndMap.layerid;
this.verbose(1, 'Going into seeding mode.');
this.server.rcon.execute(`AdminChangeLayer ${newCurrentMap} `);
2022-09-10 18:09:28 -05:00
}
}
2022-09-15 06:20:09 -05:00
} else this.verbose(1, "Bad data (currentLayer). Seeding mode for current layer skipped to prevent errors.");
2022-09-15 07:05:06 -05:00
if (+(new Date()) - +this.server.layerHistory[ 0 ].time > 30 * 1000) {
2023-02-25 04:45:06 -06:00
if (this.server.nextLayer) {
const nextMaps = seedingMaps.filter((l) => (!this.server.currentLayer || l.layerid != this.server.currentLayer.layerid))
let rndMap2;
do rndMap2 = randomElement(nextMaps);
while (rndMap2.layerid == rndMap.layerid)
if (this.server.players.length < 20 && this.server.nextLayer.gamemode.toLowerCase() != "seed") {
const newNextMap = rndMap2.layerid;
this.server.rcon.execute(`AdminSetNextLayer ${newNextMap} `);
}
} else this.verbose(1, "Bad data (nextLayer). Seeding mode for next layer skipped to prevent errors.");
}
2022-09-10 18:09:28 -05:00
2022-09-15 06:20:09 -05:00
} else this.verbose(1, `Player count doesn't allow seeding mode (${this.server.players.length}/20)`);
2022-09-07 17:46:37 -05:00
} else this.verbose(1, "Seeding mode disabled in config");
} else console.log("[MapVote][1] Bad data (this/this.server/this.options). Seeding mode skipped to prevent errors.");
2022-03-19 18:11:24 -05:00
}
2022-08-13 05:05:40 -05:00
async onChatMessage(info) {
const { steamID, name: playerName } = info;
2022-03-19 18:11:24 -05:00
const message = info.message.toLowerCase();
//check to see if this message has a command prefix
2022-08-13 05:05:40 -05:00
if (!message.startsWith(this.options.commandPrefix) && isNaN(message))
2022-03-19 18:11:24 -05:00
return;
2022-08-13 05:05:40 -05:00
const commandSplit = (isNaN(message) ? message.substring(this.options.commandPrefix.length).trim().split(' ') : [ message ]);
let cmdLayers = commandSplit.slice(1);
for (let k in cmdLayers) cmdLayers[ k ] = cmdLayers[ k ].toLowerCase();
const subCommand = commandSplit[ 0 ];
if (!isNaN(subCommand)) // if this succeeds player is voting for a map
2022-03-19 18:11:24 -05:00
{
2022-08-13 05:05:40 -05:00
const mapNumber = parseInt(subCommand); //try to get a vote number
2022-09-15 18:04:04 -05:00
if (this.nominations[ mapNumber ]) {
2022-09-15 17:41:20 -05:00
if (!this.votingEnabled) {
await this.warn(steamID, "There is no vote running right now");
return;
}
await this.registerVote(steamID, mapNumber, playerName);
this.updateNextMap();
} else
await this.warn(steamID, "Please vote a valid option");
2022-03-19 18:11:24 -05:00
return;
}
2022-08-13 05:05:40 -05:00
2022-03-19 18:11:24 -05:00
const isAdmin = info.chat === "ChatAdmin";
2022-08-13 05:05:40 -05:00
switch (subCommand) // select the sub command
2022-03-19 18:11:24 -05:00
{
case "choices": //sends choices to player in the from of a warning
case "results": //sends player the results in a warning
2022-08-13 05:05:40 -05:00
if (!this.votingEnabled) {
2022-09-02 05:52:42 -05:00
await this.warn(steamID, "There is no vote running right now");
2022-03-19 18:11:24 -05:00
return;
}
2023-01-30 11:34:11 -06:00
await this.directMsgNominations(steamID);
2022-03-19 18:11:24 -05:00
return;
2022-08-13 05:05:40 -05:00
case "start": //starts the vote again if it was canceled
if (!isAdmin) return;
if (this.votingEnabled) {
2022-09-02 05:52:42 -05:00
await this.warn(steamID, "Voting is already enabled");
2022-03-19 18:11:24 -05:00
return;
}
2022-08-13 05:05:40 -05:00
this.beginVoting(true, steamID, cmdLayers);
return;
case "restart": //starts the vote again if it was canceled
if (!isAdmin) return;
this.endVoting();
this.beginVoting(true, steamID, cmdLayers);
2022-03-19 18:11:24 -05:00
return;
case "cancel": //cancels the current vote and wont set next map to current winnner
2022-08-13 05:05:40 -05:00
if (!isAdmin) return;
if (!this.votingEnabled) {
2022-09-02 05:52:42 -05:00
await this.warn(steamID, "There is no vote running right now");
2022-03-19 18:11:24 -05:00
return;
}
this.endVoting();
2022-09-02 05:52:42 -05:00
await this.warn(steamID, "Ending current vote");
2022-03-19 18:11:24 -05:00
return;
2022-10-25 12:51:05 -05:00
case "end": //gently ends the current vote and announces the winner layer
if (!isAdmin) return;
if (!this.votingEnabled) {
await this.warn(steamID, "There is no vote running right now");
return;
}
2023-01-30 11:34:11 -06:00
this.endVotingGently(steamID);
2022-10-25 12:51:05 -05:00
return;
2022-09-28 15:42:53 -05:00
case "cancelauto": //cancels the current vote and wont set next map to current winnner
if (!isAdmin) return;
if (!this.autovotestart) {
await this.warn(steamID, "There is no automatic vote start scheduled");
return;
}
clearTimeout(this.autovotestart);
this.autovotestart = null;
await this.warn(steamID, "Ending current vote");
return;
2022-08-13 05:05:40 -05:00
case "broadcast":
if (!isAdmin) return;
2022-08-13 05:05:40 -05:00
if (!this.votingEnabled) {
2022-09-02 05:52:42 -05:00
await this.warn(steamID, "There is no vote running right now");
2022-08-13 05:05:40 -05:00
return;
}
this.broadcastNominations();
2022-03-19 18:11:24 -05:00
return;
2023-03-01 06:18:42 -06:00
case "endmatch":
if (!isAdmin) return;
this.server.rcon.execute(`AdminEndMatch`)
return;
2022-03-19 18:11:24 -05:00
case "help": //displays available commands
2022-08-31 04:24:12 -05:00
let msg = "";
msg += (`!vote\n > choices\n > results\n`);
2023-03-15 13:27:25 -05:00
if (isAdmin) msg += (`\n Admin only:\n > start\n > restart\n > cancel\n > broadcast\n > endmatch`);
2022-08-13 05:05:40 -05:00
2022-09-02 05:52:42 -05:00
await this.warn(steamID, msg + `\nMapVote SquadJS plugin built by JetDave`);
2022-03-19 18:11:24 -05:00
return;
2022-11-21 08:15:11 -06:00
case "endsqjs":
case "closesqjs":
case "stopesqjs":
case "restartsqjs":
if (!isAdmin) return;
await this.warn(steamID, "Saving persistent data.\nTerminating SquadJS process.\nIf managed by a process manager it will automatically restart.")
2023-01-21 14:13:07 -06:00
this.savePersistentData(steamID);
2022-11-21 08:15:11 -06:00
process.exit(0);
return;
2022-03-19 18:11:24 -05:00
default:
//give them an error
2022-09-02 05:52:42 -05:00
await this.warn(steamID, `Unknown vote subcommand: ${subCommand}`);
2022-03-19 18:11:24 -05:00
return;
}
2022-08-13 05:05:40 -05:00
2022-03-19 18:11:24 -05:00
}
2022-08-13 05:05:40 -05:00
2022-03-19 18:11:24 -05:00
updateNextMap() //sets next map to current mapvote winner, if there is a tie will pick at random
{
if (!this.votingEnabled) return;
this.lastMapUpdate = new Date();
2022-09-15 17:41:20 -05:00
let cpyWinners = this.currentWinners;
let skipSetNextMap = false;
if (cpyWinners.find(e => e == this.nominations[ 0 ])) {
2022-09-15 18:16:04 -05:00
if (cpyWinners.length > 1) {
delete cpyWinners[ cpyWinners.indexOf(this.nominations[ 0 ]) ]
cpyWinners = cpyWinners.filter(e => e != null)
}
2022-09-15 17:41:20 -05:00
else {
skipSetNextMap = true;
if (this.newVoteTimeout == null) {
this.newVoteTimeout = setTimeout(() => {
if (this.currentWinners.find(e => e == this.nominations[ 0 ]) && this.currentWinners.length == 1) {
this.newVoteTimeout = null;
this.endVoting()
this.broadcast("The previous Map Vote has been canceled and a new one has been generated!")
2022-09-16 13:35:08 -05:00
this.beginVoting(true, this.newVoteOptions.steamid, this.newVoteOptions.cmdLayers)
2022-09-15 17:41:20 -05:00
}
2022-09-15 18:04:04 -05:00
}, 2 * 60 * 1000)
2022-09-16 13:35:08 -05:00
setTimeout(this.broadcastNominations, 1 * 60 * 1000)
2022-09-15 17:41:20 -05:00
}
}
}
const nextMap = randomElement(cpyWinners);
2022-09-15 17:41:20 -05:00
if (!skipSetNextMap) {
2022-10-04 12:48:38 -05:00
const baseDataExist = this && this.server;
const layerDataExist = this.server.nextLayer && this.server.nextLayer.layerid;
if (baseDataExist && (!layerDataExist || this.server.nextLayer.layerid != nextMap))
2022-10-03 05:28:50 -05:00
this.server.rcon.execute(`AdminSetNextLayer ${nextMap}`);
2022-10-04 12:48:38 -05:00
else console.log("[MapVote][1] Bad data (this/this.server). Next layer not set to prevent errors.");
2022-09-15 17:41:20 -05:00
}
return nextMap;
2022-03-19 18:11:24 -05:00
}
2022-08-13 05:05:40 -05:00
matchLayers(builtString) {
return Layers.layers.filter(element => element.layerid.includes(builtString));
2022-04-14 04:08:53 -05:00
}
2022-08-13 05:05:40 -05:00
getMode(nomination, currentMode) {
const mapName = nomination.map;
let modes = nomination.modes;
2022-08-13 05:05:40 -05:00
let mode = modes[ 0 ];
2022-04-14 04:08:53 -05:00
if (mode === "Any")
modes = this.voteRules.modes;
2022-08-13 05:05:40 -05:00
if (this.voteRules.mode_repeat_blacklist.includes(currentMode)) {
2022-04-14 04:08:53 -05:00
modes = modes.filter(mode => !mode.includes(currentMode));
}
2022-08-13 05:05:40 -05:00
while (modes.length > 0) {
mode = randomElement(modes);
2022-04-14 04:08:53 -05:00
modes = modes.filter(elem => elem !== mode);
if (this.matchLayers(`${mapName}_${mode}`).length > 0)
2022-04-14 04:08:53 -05:00
break;
}
return mode;
}
2022-03-19 18:11:24 -05:00
//TODO: right now if version is set to "Any" no caf layers will be selected
2022-11-10 15:05:21 -06:00
populateNominations(steamid = null, cmdLayers = [], bypassRaasFilter = false, tries = 10) //gets nomination strings from layer options
2022-03-19 18:11:24 -05:00
{
this.options.gamemodeWhitelist.forEach((e, k, a) => a[ k ] = e.toUpperCase());
2022-08-13 05:05:40 -05:00
// this.nominations.push(builtLayerString);
// this.tallies.push(0);
const translations = {
'United States Army': "USA",
'United States Marine Corps': "USMC",
'Russian Ground Forces': "RUS",
'British Army': "GB",
'British Armed Forces': "GB",
2022-08-13 05:05:40 -05:00
'Canadian Army': "CAF",
'Australian Defence Force': "AUS",
'Irregular Militia Forces': "MIL",
2022-08-13 05:05:40 -05:00
'Middle Eastern Alliance': "MEA",
'Insurgent Forces': "INS",
'Unknown': "Unk"
}
2022-03-19 18:11:24 -05:00
this.nominations = [];
2022-08-13 05:05:40 -05:00
this.tallies = [];
this.factionStrings = [];
let rnd_layers = [];
const sanitizedLayers = Layers.layers.filter((l) => l.layerid && l.map);
const maxOptions = this.options.showRerollOption ? 20 : 21;
2023-01-25 17:21:34 -06:00
const optionAmount = Math.min(maxOptions, this.options.entriesAmount);
2022-11-21 08:15:11 -06:00
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(', '))
2023-01-25 17:21:34 -06:00
const isRandomVote = !cmdLayers || cmdLayers.length == 0;
if (isRandomVote) {
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) &&
(
2022-10-25 12:51:05 -05:00
(this.options.layerFilteringMode.toLowerCase() == "blacklist" && !this.options.layerLevelBlacklist.find((fl) => this.getLayersFromStringId(fl).map((e) => e.layerid).includes(l.layerid))) ||
(
this.options.layerFilteringMode.toLowerCase() == "whitelist"
2022-10-25 12:51:05 -05:00
&& this.options.layerLevelWhitelist.find((fl) => this.getLayersFromStringId(fl).map((e) => e.layerid).includes(l.layerid))
&& !(this.options.applyBlacklistToWhitelist && this.options.layerLevelBlacklist.find((fl) => this.getLayersFromStringId(fl).map((e) => e.layerid).includes(l.layerid)))
)
)
2022-11-22 12:54:51 -06:00
&& !(this.options.factionsBlacklist.find((f) => [ getTranslation(l.teams[ 0 ]), getTranslation(l.teams[ 1 ]) ].includes(f)))
);
2023-03-16 15:08:20 -05:00
for (let i = 1; i <= Math.min(optionAmount, all_layers.length); i++) {
2023-01-25 17:21:34 -06:00
const needMoreRAAS = !bypassRaasFilter && rnd_layers.filter((l) => l.gamemode.toUpperCase() === 'RAAS').length < this.options.minRaasEntries;
let l, maxtries = 20;
do l = randomElement(needMoreRAAS ? all_layers.filter((l) => l.gamemode.toLowerCase() == "raas") : all_layers); while ((rnd_layers.find(lf => lf.layerid == l.layerid) || rnd_layers.filter(lf => lf.map.name == l.map.name).length > (this.options.allowedSameMapEntries - 1)) && --maxtries >= 0)
2022-10-24 17:01:18 -05:00
if (maxtries > 0 && l) {
// this.verbose(1,"Testing layer",l, maxtries);
2022-09-08 04:49:08 -05:00
rnd_layers.push(l);
2022-09-15 17:41:20 -05:00
this.nominations[ i ] = l.layerid
this.tallies[ i ] = 0;
this.factionStrings[ i ] = getTranslation(l.teams[ 0 ]) + "-" + getTranslation(l.teams[ 1 ]);
2022-09-08 04:49:08 -05:00
}
2022-03-19 18:11:24 -05:00
}
// if (!bypassRaasFilter && this.options.gamemodeWhitelist.includes("RAAS") && rnd_layers.filter((l) => l.gamemode === 'RAAS').length < Math.floor(maxOptions / 2)) this.populateNominations();
if (this.nominations.length == 0) {
2022-11-17 17:22:21 -06:00
if (--tries > 0) this.populateNominations(steamid, cmdLayers, bypassRaasFilter, tries);
else this.warn("")
return;
}
2022-08-13 05:05:40 -05:00
} else {
2023-01-25 17:21:34 -06:00
if (cmdLayers.length == 1) while (cmdLayers.length < optionAmount) cmdLayers.push(cmdLayers[ 0 ])
if (cmdLayers.length <= maxOptions) {
2022-09-15 18:04:04 -05:00
let i = 1;
2022-08-13 05:05:40 -05:00
for (let cl of cmdLayers) {
const cls = cl.toLowerCase().split('_');
const fLayers = sanitizedLayers.filter((l) => (
2023-03-16 15:08:20 -05:00
!rnd_layers.find(l2 => l2.layerid == l.layerid) &&
(
(cls[ 0 ] == "*" || l.layerid.toLowerCase().startsWith(cls[ 0 ]))
|| (cls[ 0 ].toLowerCase().startsWith('f:') && [ getTranslation(l.teams[ 0 ]), getTranslation(l.teams[ 1 ]) ].includes(cls[ 0 ].substring(2).toUpperCase()))
)
2023-01-25 17:21:34 -06:00
&& (l.gamemode.toLowerCase().startsWith(cls[ 1 ]) || (!cls[ 1 ] && this.options.gamemodeWhitelist.includes(l.gamemode.toUpperCase())))
&& (!cls[ 2 ] || l.version.toLowerCase().startsWith("v" + cls[ 2 ].replace(/v/gi, '')))
2023-02-06 18:07:20 -06:00
// && !(this.options.factionsBlacklist.find((f) => [ getTranslation(l.teams[ 0 ]), getTranslation(l.teams[ 1 ]) ].includes(f)))
&& (cls[ 2 ] || !(
this.options.layerLevelBlacklist.find((fl) => this.getLayersFromStringId(fl).map((e) => e.layerid).includes(l.layerid))
|| this.options.factionsBlacklist.find((f) => [ getTranslation(l.teams[ 0 ]), getTranslation(l.teams[ 1 ]) ].includes(f))
))
));
2023-03-16 15:08:20 -05:00
if (fLayers.length == 0) continue;
// this.verbose(1, 'fLayers', fLayers.map(l => l.layerid));
// this.verbose(1, 'rnd_layers', rnd_layers.map(l => l.layerid));
2022-10-03 18:02:51 -05:00
let l, maxtries = 10;
2022-10-04 12:48:38 -05:00
do l = randomElement(fLayers); while ((rnd_layers.filter(lf => lf.map.name == l.map.name).length > (this.options.allowedSameMapEntries - 1)) && --maxtries >= 0)
if (l) {
2022-09-01 17:33:30 -05:00
rnd_layers.push(l);
2022-09-15 17:41:20 -05:00
this.nominations[ i ] = l.layerid
this.tallies[ i ] = 0;
this.factionStrings[ i ] = getTranslation(l.teams[ 0 ]) + "-" + getTranslation(l.teams[ 1 ]);
2022-09-15 18:04:04 -05:00
i++;
2022-09-01 17:33:30 -05:00
}
2022-08-13 05:05:40 -05:00
}
2022-09-15 18:04:04 -05:00
}
else if (steamid) {
this.warn(steamid, "You cannot start a vote with more than " + maxOptions + " options");
return;
}
2022-03-19 18:11:24 -05:00
}
2022-04-14 04:08:53 -05:00
2023-01-25 17:21:34 -06:00
if (this.options.showRerollOption && (isRandomVote || this.options.showRerollOptionInCustomVotes)) {
// if (this.nominations.length > 5) {
// this.nominations.splice(6, 1);
// this.tallies.splice(6, 1);
// this.factionStrings.splice(6, 1);
// }
2022-09-16 13:35:08 -05:00
this.newVoteOptions.steamid = steamid;
this.newVoteOptions.bypassRaasFilter = bypassRaasFilter;
this.newVoteOptions.cmdLayers = cmdLayers;
2022-09-15 18:04:04 -05:00
this.nominations[ 0 ] = "Reroll vote list with random options"
2022-09-15 17:41:20 -05:00
this.tallies[ 0 ] = 0;
this.factionStrings[ 0 ] = "";
2022-03-19 18:11:24 -05:00
}
2022-04-14 04:08:53 -05:00
2023-02-25 04:45:06 -06:00
if (this.nominations[ 1 ] != "")
this.server.rcon.execute(`AdminSetNextLayer ${this.nominations[ 1 ]} `);
2022-11-20 15:44:54 -06:00
function getTranslation(layer) {
if (translations[ layer.faction ]) return translations[ layer.faction ]
else if (layer.faction) {
const f = layer.faction.split(' ');
2022-08-13 05:05:40 -05:00
let fTag = "";
f.forEach((e) => { fTag += e[ 0 ] });
return fTag.toUpperCase();
2022-11-18 06:12:11 -06:00
} else return "Unknown"
2022-03-19 18:11:24 -05:00
}
}
//checks if there are enough players to start voting, if not binds itself to player connected
//when there are enough players it clears old votes, sets up new nominations, and starts broadcast
2022-09-08 04:51:26 -05:00
beginVoting(force = false, steamid = null, cmdLayers = []) {
if (!this.options.automaticVoteStart && !force) return;
2022-08-25 08:43:38 -05:00
this.verbose(1, "Starting vote")
2022-03-19 18:11:24 -05:00
const playerCount = this.server.players.length;
const minPlayers = this.options.minPlayersForVote;
if (this.votingEnabled) //voting has already started
return;
2022-08-13 05:05:40 -05:00
if (playerCount < minPlayers && !force) {
2022-10-20 16:59:28 -05:00
this.autovotestart = setTimeout(() => { this.beginVoting(force, steamid, cmdLayers) }, 60 * 1000)
2022-03-19 18:11:24 -05:00
return;
}
2022-10-20 16:59:28 -05:00
2022-10-25 12:51:05 -05:00
if (this.options.votingDuration > 0) this.timeout_ps.push(setTimeout(this.endVotingGently, this.options.votingDuration * 60 * 1000))
2022-03-19 18:11:24 -05:00
// these need to be reset after reenabling voting
this.trackedVotes = {};
this.tallies = [];
2022-08-13 05:05:40 -05:00
this.populateNominations(steamid, cmdLayers);
2022-03-19 18:11:24 -05:00
this.votingEnabled = true;
2022-08-13 05:05:40 -05:00
this.firstBroadcast = true;
this.broadcastNominations();
2022-03-19 18:11:24 -05:00
this.broadcastIntervalTask = setInterval(this.broadcastNominations, toMils(this.options.voteBroadcastInterval));
}
2022-08-13 05:05:40 -05:00
2023-01-30 11:34:11 -06:00
async endVotingGently(steamID = null) {
if (!this.votingEnabled) return;
2023-03-01 06:18:42 -06:00
2023-02-28 17:36:38 -06:00
const winningLayerId = this.updateNextMap();
2023-03-17 19:22:12 -05:00
if (!winningLayerId) {
this.verbose(1, 'No winning layer available', winningLayerId)
return;
}
2023-02-28 17:36:38 -06:00
const winnerLayer = Layers.layers.find((l) => l.layerid == winningLayerId);
const fancyWinner = this.formatFancyLayer(winnerLayer);
2023-01-30 11:34:11 -06:00
2023-02-28 17:36:38 -06:00
// this.verbose(1, "Winning layer", winnerLayer, fancyWinner)
2023-01-30 11:34:11 -06:00
2023-02-28 17:36:38 -06:00
if (this.options.showWinnerBroadcastMessage) this.broadcast(this.options.voteWinnerBroadcastMessage + fancyWinner);
2023-01-30 11:34:11 -06:00
if (this.options.logToDiscord) {
await this.sendDiscordMessage({
embed: {
title: `Vote winner: ${fancyWinner}`,
color: 16761867,
fields: [
{
name: 'Map',
value: winnerLayer.map.name,
inline: true
},
{
name: 'Gamemode',
value: winnerLayer.gamemode,
inline: true
},
{
name: 'Version',
value: winnerLayer.version,
inline: true
},
{
name: 'LayerID',
value: winnerLayer.layerid,
inline: false
},
{
name: 'Team 1',
value: winnerLayer.teams[ 0 ].faction,
inline: true
},
{
name: 'Team 2',
value: winnerLayer.teams[ 1 ].faction,
inline: true
},
],
image: {
url: `https://squad-data.nyc3.cdn.digitaloceanspaces.com/main/${winnerLayer.layerid}.jpg`
},
},
2023-01-30 11:34:11 -06:00
timestamp: (new Date()).toISOString()
});
}
2023-03-17 19:22:12 -05:00
this.endVoting();
if (steamID) await this.warn(steamID, "Voting terminated!");
2023-01-30 11:34:11 -06:00
return true;
2022-10-25 12:51:05 -05:00
}
2022-08-13 05:05:40 -05:00
endVoting() {
2022-03-19 18:11:24 -05:00
this.votingEnabled = false;
2023-02-25 05:03:45 -06:00
this.broadcastIntervalTask = clearInterval(this.broadcastIntervalTask);
this.newVoteTimeout = clearTimeout(this.newVoteTimeout);
this.endVotingTimeout = clearTimeout(this.endVotingTimeout);
2022-03-19 18:11:24 -05:00
}
objArrToValArr(arr, ...key) {
let vet = [];
for (let o of arr) {
let obj = o;
for (let k of key) {
if (obj[ k ])
obj = obj[ k ];
}
vet.push(obj);
}
return vet;
}
2022-03-19 18:11:24 -05:00
//sends a message about nominations through a broadcast
2022-08-13 05:05:40 -05:00
//NOTE: max squad broadcast message length appears to be 485 characters
2022-03-19 18:11:24 -05:00
//Note: broadcast strings with multi lines are very strange
2022-08-13 05:05:40 -05:00
async broadcastNominations() {
2022-08-25 08:43:38 -05:00
if (this.nominations.length > 0 && this.votingEnabled) {
2022-09-16 15:41:55 -05:00
await this.broadcast(this.options.voteBroadcastMessage);
let allNominationStrings = []
2022-08-13 05:05:40 -05:00
let nominationStrings = [];
2023-01-20 17:49:06 -06:00
2022-09-15 17:41:20 -05:00
for (let choice = 1; choice < this.nominations.length; choice++) {
2022-08-13 05:05:40 -05:00
choice = Number(choice);
2022-09-15 17:41:20 -05:00
let vLayer = Layers.layers.find(e => e.layerid == this.nominations[ choice ]);
2023-02-28 16:54:37 -06:00
const formattedChoide = this.formatChoice(choice, this.formatFancyLayer(vLayer), this.tallies[ choice ], (this.options.hideVotesCount || this.firstBroadcast))
nominationStrings.push(formattedChoide);
allNominationStrings.push(formattedChoide);
2023-01-20 17:49:06 -06:00
if (nominationStrings.length == 3) {
await this.broadcast(nominationStrings.join("\n"));
nominationStrings = [];
}
2022-08-13 05:05:40 -05:00
}
2023-01-20 17:49:06 -06:00
if (this.nominations[ 0 ]) nominationStrings.push(this.formatChoice(0, this.nominations[ 0 ], this.tallies[ 0 ], (this.options.hideVotesCount || this.firstBroadcast)))
2022-09-02 05:52:42 -05:00
await this.broadcast(nominationStrings.join("\n"));
2022-09-16 18:10:05 -05:00
if (this.firstBroadcast)
await this.logVoteToDiscord(allNominationStrings.join("\n"))
2022-08-13 05:05:40 -05:00
this.firstBroadcast = false;
}
2022-03-19 18:11:24 -05:00
//const winners = this.currentWinners;
//await this.msgBroadcast(`Current winner${winners.length > 1 ? "s" : ""}: ${winners.join(", ")}`);
}
formatFancyLayer(layer) {
const translations = {
'United States Army': "USA",
'United States Marine Corps': "USMC",
'Russian Ground Forces': "RUS",
'British Army': "GB",
2023-03-14 20:26:46 -05:00
'British Armed Forces': "GB",
'Canadian Army': "CAF",
'Australian Defence Force': "AUS",
2023-03-14 20:26:46 -05:00
'Irregular Militia Forces': "MIL",
'Middle Eastern Alliance': "MEA",
'Insurgent Forces': "INS",
2023-03-14 20:26:46 -05:00
'Unknown': "Unk"
}
const factionString = getTranslation(layer.teams[ 0 ]) + "-" + getTranslation(layer.teams[ 1 ]);
2023-02-28 16:54:37 -06:00
const helis = layer.teams[ 0 ].numberOfHelicopters + layer.teams[ 1 ].numberOfHelicopters
const tanks = layer.teams[ 0 ].numberOfTanks + layer.teams[ 1 ].numberOfTanks
let assets = [];
if (helis > 0) assets.push('Helis');
if (tanks > 0) assets.push('Tanks');
const vehiclesString = assets.join('-');
return this.options.entryFormat
.replace(/\{map_name\}/i, layer.map.name)
.replace(/\{gamemode\}/i, layer.gamemode)
.replace(/\{map_version\}/i, layer.version)
.replace(/\{factions\}/i, factionString)
.replace(/\{main_assets\}/i, vehiclesString)
function getTranslation(t) {
if (translations[ t.faction ]) return translations[ t.faction ]
else {
const f = t.faction.split(' ');
let fTag = "";
f.forEach((e) => { fTag += e[ 0 ] });
return fTag.toUpperCase();
}
}
}
2022-03-19 18:11:24 -05:00
2022-10-24 17:01:18 -05:00
getLayersFromStringId(stringid) {
const cls = stringid.toLowerCase().split('_');
const ret = Layers.layers.filter((l) => ((cls[ 0 ] == "*" || l.layerid.toLowerCase().startsWith(cls[ 0 ])) && (l.gamemode.toLowerCase().startsWith(cls[ 1 ]) || (!cls[ 1 ] && [ 'RAAS', 'AAS', 'INVASION' ].includes(l.gamemode.toUpperCase()))) && (!cls[ 2 ] || parseInt(l.version.toLowerCase().replace(/v/gi, '')) == parseInt(cls[ 2 ].replace(/v/gi, '')))));
// this.verbose(1,"layers from string",stringid,cls,ret)
return ret;
}
2022-08-13 05:05:40 -05:00
async directMsgNominations(steamID) {
2022-08-25 08:43:38 -05:00
let strMsg = "";
2022-08-13 05:05:40 -05:00
for (let choice in this.nominations) {
choice = Number(choice);
2023-01-21 14:13:07 -06:00
let vLayer = Layers.layers.find(e => e.layerid == this.nominations[ choice ]);
// const allVecs = vLayer.teams[0].vehicles.concat(vLayer.teams[1].vehicles);
// const helis = vLayer?.teams[ 0 ].numberOfHelicopters || 0 + vLayer?.teams[ 1 ].numberOfHelicopters || 0
// const tanks = vLayer?.teams[ 0 ].numberOfTanks || 0 + vLayer?.teams[ 1 ].numberOfTanks || 0
// let assets = [];
// if (helis > 0) assets.push('Helis');
// if (tanks > 0) assets.push('Tanks');
// const vehiclesString = ' ' + assets.join('-');
2022-08-25 08:43:38 -05:00
// await this.msgDirect(steamID, formatChoice(choice, this.nominations[ choice ], this.tallies[ choice ]));
strMsg += (steamID, this.formatChoice(choice, this.nominations[ choice ], this.tallies[ choice ])) + "\n";
2022-08-13 05:05:40 -05:00
}
2022-08-25 08:43:38 -05:00
strMsg.trim();
2022-09-10 18:01:43 -05:00
if (steamID) this.warn(steamID, strMsg)
2022-08-13 05:05:40 -05:00
2022-08-25 08:43:38 -05:00
// const winners = this.currentWinners;
// await this.msgDirect(steamID, `Current winner${winners.length > 1 ? "s" : ""}: ${winners.join(", ")}`);
2022-03-19 18:11:24 -05:00
}
//counts a vote from a player and adds it to tallies
2022-08-13 05:05:40 -05:00
async registerVote(steamID, nominationIndex, playerName) {
2022-09-15 17:41:20 -05:00
// nominationIndex -= 1; // shift indices from display range
2022-08-13 05:05:40 -05:00
if (nominationIndex < 0 || nominationIndex > this.nominations.length) {
2022-09-02 05:52:42 -05:00
await this.warn(steamID, `[Map Vote] ${playerName}: invalid map number, typ !vote results to see map numbers`);
2022-03-19 18:11:24 -05:00
return;
}
2022-08-13 05:05:40 -05:00
const previousVote = this.trackedVotes[ steamID ];
this.trackedVotes[ steamID ] = nominationIndex;
this.tallies[ nominationIndex ] += 1;
if (previousVote !== undefined)
this.tallies[ previousVote ] -= 1;
2022-09-15 18:04:04 -05:00
await this.warn(steamID, `Registered vote: ${this.nominations[ nominationIndex ].replace(/\_/gi, ' ').replace(/\sv\d{1,2}/gi, '')} ${this.factionStrings[ nominationIndex ]} ` + (this.options.hideVotesCount ? `` : `(${this.tallies[ nominationIndex ]} votes)`));
2022-08-13 05:05:40 -05:00
// await this.msgDirect(steamID, `Registered vote`);// ${this.nominations[ nominationIndex ]} ${this.factionStrings[ nominationIndex ]} (${this.tallies[ nominationIndex ]} votes)`);
// await this.msgDirect(steamID, `${this.nominations[ nominationIndex ]} (${this.tallies[ nominationIndex ]} votes)`);
// await this.msgDirect(steamID, `${this.factionStrings[ nominationIndex ]}`);
// await this.msgDirect(steamID, `${this.tallies[ nominationIndex ]} votes`);
2022-03-19 18:11:24 -05:00
}
2022-08-13 05:05:40 -05:00
2022-09-16 18:33:21 -05:00
async logVoteToDiscord(message) {
if (!this.options.logToDiscord) return
return await this.sendDiscordMessage({
2022-09-16 18:33:21 -05:00
embed: {
title: 'Vote Started',
color: 16761867,
fields: [
{
name: 'Options:',
value: `${message}`
}
]
2022-09-16 18:39:55 -05:00
},
timestamp: (new Date()).toISOString()
2022-09-16 18:33:21 -05:00
});
}
2022-09-16 18:10:05 -05:00
2022-03-19 18:11:24 -05:00
//removes a players vote if they disconnect from the sever
2022-08-13 05:05:40 -05:00
clearVote() {
2022-03-19 18:11:24 -05:00
const currentPlayers = this.server.players.map((p) => p.steamID);
2022-08-13 05:05:40 -05:00
for (const steamID in this.trackedVotes) {
if (!(currentPlayers.includes(steamID))) {
const vote = this.trackedVotes[ steamID ];
this.tallies[ vote ] -= 1;
delete this.trackedVotes[ steamID ];
2022-03-19 18:11:24 -05:00
}
2022-08-13 05:05:40 -05:00
}
2022-03-19 18:11:24 -05:00
}
2022-11-21 08:15:11 -06:00
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
}
if (bkData.manualRestartSender && bkData.manualRestartSender != "") {
(async () => {
await this.warn(bkData.manualRestartSender, `SquadJS has completed the restart.\nPersistent data restored.`)
this.verbose(1, `Restart confirmation sent to SteamID: "${bkData.manualRestartSender}"`)
})()
}
for (let k in bkData.server) this.server[ k ] = bkData.server[ k ];
2022-11-21 08:15:11 -06:00
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));
}
}
2023-01-21 14:13:07 -06:00
savePersistentData(steamID = null) {
2022-11-21 08:15:11 -06:00
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
},
2023-01-21 14:13:07 -06:00
manualRestartSender: steamID,
2022-11-21 08:15:11 -06:00
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))
}
2022-03-19 18:11:24 -05:00
//calculates the current winner(s) of the vote and returns thier strings in an array
2022-08-13 05:05:40 -05:00
get currentWinners() {
2022-03-19 18:11:24 -05:00
const ties = [];
2022-08-13 05:05:40 -05:00
2022-03-19 18:11:24 -05:00
let highestScore = -Infinity;
2022-08-13 05:05:40 -05:00
for (let choice in this.tallies) {
const score = this.tallies[ choice ];
if (score >= this.options.minimumVotesToAcceptResult) {
if (score < highestScore)
2023-03-17 19:22:12 -05:00
continue;
else if (score > highestScore) {
highestScore = score;
ties.length = 0;
ties.push(choice);
}
else // equal
2023-03-17 19:22:12 -05:00
ties.push(choice);
2022-03-19 18:11:24 -05:00
}
2023-03-17 19:22:12 -05:00
this.verbose(1, 'Ties', ties, ties.map(i => this.nominations[ i ]))
2022-03-19 18:11:24 -05:00
}
2022-08-13 05:05:40 -05:00
return ties.map(i => this.nominations[ i ]);
2022-03-19 18:11:24 -05:00
}
2022-11-17 17:22:21 -06:00
async updateLayerList() {
2022-11-18 06:12:11 -06:00
// Layers.layers = [];
2022-11-17 17:22:21 -06:00
this.verbose(1, 'Pulling updated layer list...');
2022-11-17 17:22:21 -06:00
const response = await axios.get(
'https://raw.githubusercontent.com/Squad-Wiki/squad-wiki-pipeline-map-data/master/completed_output/_Current%20Version/finished.json'
2022-11-17 17:22:21 -06:00
);
for (const layer of response.data.Maps) {
if (!Layers.layers.find((e) => e.layerid == layer.rawName)) Layers.layers.push(new Layer(layer));
2022-11-17 17:22:21 -06:00
}
2022-11-18 06:12:11 -06:00
2023-03-15 13:52:27 -05:00
const sheetCsv = (await axios.get('https://docs.google.com/spreadsheets/d/1OYO1IvNI0wrUZWKz_pz6Ka1xFAvBjBupddYn2E4fNFg/gviz/tq?tqx=out:csv&sheet=Map%20Layers')).data?.replace(/\"/g, '')?.split('\n') || []//.map((l) => l.split(','))
// this.verbose(1, 'Sheet', sheetCsv)
2023-03-16 15:08:20 -05:00
sheetCsv.shift();
// this.verbose(1, 'Sheet', Layers.layers.length, sheetCsv.length, sheetCsv.find(l => l.includes("Manicouagan_RAAS_v1")))
2023-03-15 13:52:27 -05:00
const rconLayers = (await this.server.rcon.execute('ListLayers'))?.split('\n') || [];
rconLayers.shift();
2023-03-16 15:08:20 -05:00
2023-03-15 13:52:27 -05:00
if (rconLayers.length > 0) Layers.layers = Layers.layers.filter((l) => l != null && rconLayers.includes(l.layerid))
2023-03-16 15:08:20 -05:00
// this.verbose(1, 'RCON Layers', rconLayers.length, this.mapLayer(rconLayers[ 0 ]))
2023-03-15 13:27:25 -05:00
if (sheetCsv.length > 0) {
for (const layer of rconLayers) {
2023-03-15 13:52:27 -05:00
if (!Layers.layers.find((e) => e?.layerid == layer)) {
2023-03-15 13:27:25 -05:00
let newLayer = this.mapLayer(layer);
2023-03-16 15:08:20 -05:00
if (!newLayer) continue;
2023-03-15 13:27:25 -05:00
2023-03-15 13:52:27 -05:00
const csvLayer = sheetCsv.find(l => l.includes(newLayer?.layerid))?.split(',');
2023-03-15 13:27:25 -05:00
// console.log(newLayer.layerid, csvLayer[ 2 ]);
if (csvLayer) {
if (csvLayer[ 6 ]) newLayer.teams[ 0 ].faction = csvLayer[ 6 ]
newLayer.teams[ 0 ].name = newLayer.teams[ 0 ].faction
if (csvLayer[ 9 ]) newLayer.teams[ 0 ].numberOfTanks = parseNumberOfAssets(csvLayer[ 9 ])
if (csvLayer[ 13 ]) newLayer.teams[ 0 ].numberOfHelicopters = parseNumberOfAssets(csvLayer[ 13 ])
if (csvLayer[ 5 ]) newLayer.teams[ 0 ].commander = csvLayer[ 5 ].toLowerCase() == 'yes'
if (csvLayer[ 10 ]) newLayer.teams[ 1 ].faction = csvLayer[ 10 ]
newLayer.teams[ 1 ].name = newLayer.teams[ 1 ].faction
newLayer.teams[ 1 ].numberOfTanks = newLayer.teams[ 0 ].numberOfTanks
newLayer.teams[ 1 ].numberOfHelicopters = newLayer.teams[ 0 ].numberOfHelicopters
newLayer.teams[ 1 ].commander = newLayer.teams[ 0 ].commander
}
2023-03-15 13:27:25 -05:00
Layers.layers.push(newLayer);
}
}
}
this.verbose(1, 'Layer list updated', Layers.layers.length, 'total layers');
// this.verbose(1, 'Layers', Layers.layers);
function parseNumberOfAssets(string) {
return /^x(\d)/.exec(string)[ 1 ]
}
2022-11-17 17:22:21 -06:00
}
2023-02-25 04:45:06 -06:00
mapLayer(l) {
l = l.replace(/[^a-z_\d]/gi, '')
// this.verbose(1, 'Parsing layer', l)
const gl = /^(?<level>\w+)_(?<gamemode>\w+)_(?<version>\w+)$/i.exec(l)?.groups
// this.verbose(1, 'Parsed layer', gl)
2023-03-15 13:52:27 -05:00
if (!gl || Object.keys(gl).length != 3) return;
2023-03-16 15:08:20 -05:00
if (!gl.level) this.verbose(1, 'Empty level', gl)
let teams = []
for (const t of [ 'team1', 'team2' ]) {
teams.push({
faction: 'Unknown',
name: 'Unknown',
tickets: 0,
commander: false,
vehicles: [],
numberOfTanks: 0,
numberOfHelicopters: 0
});
}
// this.verbose(1, 'teams', teams)
return {
name: l.replace(/_/g, ' '),
classname: gl.level,
layerid: l,
map: {
name: gl.level
},
gamemode: gl.gamemode,
gamemodeType: gl.gamemode,
version: gl.version,
size: '0.0x0.0 km',
sizeType: 'Playable Area',
numberOfCapturePoints: 0,
lighting: {
name: 'Unknown',
classname: 'Unknown'
},
teams: teams
}
}
formatChoice(choiceIndex, mapString, currentVotes, hideVoteCount) {
return `${choiceIndex}${mapString} ` + (!hideVoteCount ? `(${currentVotes})` : "");
// return `${choiceIndex + 1}❱ ${mapString} (${currentVotes} votes)`
}
2022-09-16 18:10:05 -05:00
}
2022-09-16 18:10:05 -05:00
function randomElement(array) {
return array[ Math.floor(Math.random() * array.length) ];
2022-09-06 08:32:48 -05:00
}
2022-09-16 18:10:05 -05:00
function toMils(min) {
return min * 60 * 1000;
2022-11-21 08:15:11 -06:00
}