squad-js-map-vote/mapvote.js

448 lines
18 KiB
JavaScript
Raw Normal View History

2022-03-19 18:11:24 -05:00
//Plugin by MaskedMonkeyMan
import BasePlugin from "./base-plugin.js";
import fs from "fs";
import { Layers } from "../layers/index.js"
2022-08-13 05:05:40 -05:00
function randomElement(array) {
return array[ Math.floor(Math.random() * array.length) ];
2022-03-19 18:11:24 -05:00
}
2022-08-13 05:05:40 -05:00
function formatChoice(choiceIndex, mapString, currentVotes, firstBroadcast) {
return `${choiceIndex + 1}${mapString} ` + (!firstBroadcast ? `(${currentVotes})` : "");
// return `${choiceIndex + 1}❱ ${mapString} (${currentVotes} votes)`
2022-03-19 18:11:24 -05:00
}
2022-08-13 05:05:40 -05:00
function toMils(min) {
return min * 60 * 1000;
2022-03-19 18:11:24 -05:00
}
2022-08-13 05:05:40 -05:00
export default class MapVote extends BasePlugin {
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 {
commandPrefix:
{
required: false,
description: "command name to use in chat",
default: "!vote"
},
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
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-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.onConnectBound = false;
this.broadcastIntervalTask = null;
2022-08-13 05:05:40 -05:00
this.firstBroadcast = true;
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-02 05:52:42 -05:00
this.broadcast = (msg) => { this.server.rcon.broadcast(msg); };
this.warn = (steamid, msg) => { this.server.rcon.warn(steamid, msg); };
2022-03-19 18:11:24 -05:00
}
2022-08-13 05:05:40 -05:00
async mount() {
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);
2022-08-13 05:05:40 -05:00
this.verbose(1, 'Map vote was mounted.');
this.setSeedingMode();
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() {
2022-08-25 08:43:38 -05:00
setTimeout(async () => {
this.endVoting();
this.trackedVotes = {};
this.tallies = [];
this.nominations = [];
this.factionStrings = [];
setTimeout(this.beginVoting, toMils(this.options.voteWaitTimeFromMatchStart));
setTimeout(() => { this.setSeedingMode() }, 20000);
}, 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();
this.updateNextMap();
}
2022-08-16 16:53:08 -05:00
setSeedingMode() {
2022-08-25 08:43:38 -05:00
// setTimeout(()=>{this.msgDirect('76561198419229279',"MV\ntest\ntest")},1000)
// this.msgBroadcast("[MapVote] Seeding mode active")
2022-09-02 05:52:42 -05:00
if (this && this.options && this.server && this.options.automaticSeedingMode && ((this.server.nextLayer && this.server.nextLayer.gamemode.toLowerCase() != "seed") || this.server.currentLayer.layerid == this.server.nextLayer.layerid)) {
2022-08-16 16:53:08 -05:00
const mapBlacklist = [ "BlackCoast" ];
const seedingMaps = Layers.layers.filter((l) => l.gamemode.toUpperCase() == "SEED" && !mapBlacklist.includes(l.classname) && l.layerid != this.server.currentLayer.layerid)
2022-08-25 08:43:38 -05:00
const nextMap = randomElement(seedingMaps).layerid;
2022-08-16 16:53:08 -05:00
if (this.server.players && this.server.players.length < 20) {
this.verbose(1, 'Going into seeding mode.');
this.server.rcon.execute(`AdminSetNextLayer ${nextMap}`);
}
}
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
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;
}
await this.registerVote(steamID, mapNumber, playerName);
this.updateNextMap();
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;
}
this.directMsgNominations(steamID);
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-08-13 05:05:40 -05:00
case "broadcast":
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;
case "help": //displays available commands
2022-08-31 04:24:12 -05:00
let msg = "";
msg += (`!vote <choices|number|results>\n`);
if (isAdmin) msg += (`!vote <start|restart|cancel|broadcast> (admin only)\n`);
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;
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
{
const nextMap = randomElement(this.currentWinners);
this.server.rcon.execute(`AdminSetNextLayer ${nextMap}`);
}
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-08-16 16:53:08 -05:00
populateNominations(steamid = null, cmdLayers = [], bypassRaasFilter = false) //gets nomination strings from layer options
2022-03-19 18:11:24 -05:00
{
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",
'Canadian Army': "CAF",
'Australian Defence Force': "AUS",
'Irregular Militia Forces': "IRR",
'Middle Eastern Alliance': "MEA",
'Insurgent Forces': "INS",
}
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 = [];
// let rnd_layers = [];
2022-08-25 08:43:38 -05:00
if (!cmdLayers || cmdLayers.length == 0) {
2022-08-31 04:24:12 -05:00
const all_layers = Layers.layers.filter((l) => [ 'RAAS', 'AAS', 'INVASION' ].includes(l.gamemode.toUpperCase()) && ![ this.server.currentLayer.classname, ...this.objArrToValArr(this.server.layerHistory.splice(0, this.options.numberRecentMapsToExlude), "classname") ].includes(l.classname));
2022-08-13 05:05:40 -05:00
for (let i = 0; i < 6; i++) {
// rnd_layers.push(all_layers[Math.floor(Math.random()*all_layers.length)]);
2022-08-16 16:53:08 -05:00
let l;
do l = randomElement(all_layers); while (rnd_layers.includes(l))
2022-08-13 05:05:40 -05:00
rnd_layers.push(l);
this.nominations.push(l.layerid)
this.tallies.push(0);
this.factionStrings.push(getTranslation(l.teams[ 0 ]) + "-" + getTranslation(l.teams[ 1 ]));
2022-03-19 18:11:24 -05:00
}
2022-08-13 05:05:40 -05:00
if (!bypassRaasFilter && rnd_layers.filter((l) => l.gamemode === 'RAAS').length < 3) this.populateNominations();
} else {
2022-08-25 08:43:38 -05:00
if (cmdLayers.length == 1 && cmdLayers[ 0 ].split('_')[ 0 ] == "*") for (let i = 0; i < 5; i++) cmdLayers.push(cmdLayers[ 0 ])
2022-08-13 05:05:40 -05:00
if (cmdLayers.length <= 6)
for (let cl of cmdLayers) {
const cls = cl.split('_');
2022-08-16 16:53:08 -05:00
const fLayers = Layers.layers.filter((l) => ((cls[ 0 ] == "*" || l.classname.toLowerCase().startsWith(cls[ 0 ])) && (l.gamemode.toLowerCase().startsWith(cls[ 1 ]) || (!cls[ 1 ] && [ 'RAAS', 'AAS', 'INVASION' ].includes(l.gamemode.toUpperCase()))) && (!cls[ 2 ] || l.version.toLowerCase().startsWith("v" + cls[ 2 ].replace(/v/gi, '')))));
let l;
do l = randomElement(fLayers); while (rnd_layers.includes(l))
2022-09-01 17:33:30 -05:00
if(l){
rnd_layers.push(l);
this.nominations.push(l.layerid)
this.tallies.push(0);
this.factionStrings.push(getTranslation(l.teams[ 0 ]) + "-" + getTranslation(l.teams[ 1 ]));
}
2022-08-13 05:05:40 -05:00
}
2022-09-02 05:52:42 -05:00
else if (steamid) this.warn(steamid, "You cannot start a vote with more than 6 options"); return;
2022-03-19 18:11:24 -05:00
}
2022-04-14 04:08:53 -05:00
2022-08-13 05:05:40 -05:00
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
}
}
}
//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-08-13 05:05:40 -05:00
beginVoting(force = false, steamid = null, cmdLayers = null) {
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) {
if (this.onConnectBound == false) {
2022-08-31 04:24:12 -05:00
this.server.on("PLAYER_CONNECTED", () => { this.beginVoting })
2022-03-19 18:11:24 -05:00
this.onConnectBound = true;
}
return;
}
2022-08-13 05:05:40 -05:00
if (this.onConnectBound) {
2022-08-31 04:24:12 -05:00
this.server.removeEventListener("PLAYER_CONNECTED", () => { this.beginVoting });
2022-03-19 18:11:24 -05:00
this.onConnectBound = false;
}
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
endVoting() {
2022-03-19 18:11:24 -05:00
this.votingEnabled = false;
clearInterval(this.broadcastIntervalTask);
this.broadcastIntervalTask = null;
}
2022-08-31 04:24:12 -05:00
objArrToValArr(arr, key) {
let vet = [];
2022-08-31 04:24:12 -05:00
for (let o of arr) if (arr[ key ]) vet.push(arr[ key ]);
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-02 05:52:42 -05:00
await this.broadcast("✯ MAPVOTE ✯ Vote for the next map by writing in chat the corresponding number!\n");
2022-08-13 05:05:40 -05:00
let nominationStrings = [];
for (let choice in this.nominations) {
choice = Number(choice);
nominationStrings.push(formatChoice(choice, this.nominations[ choice ].replace(/\_/gi, ' ').replace(/\sv\d{1,2}/gi, '') + ' ' + this.factionStrings[ choice ], this.tallies[ choice ], this.firstBroadcast));
}
2022-09-02 05:52:42 -05:00
await this.broadcast(nominationStrings.join("\n"));
2022-08-16 16:53:08 -05:00
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(", ")}`);
}
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);
2022-08-25 08:43:38 -05:00
// await this.msgDirect(steamID, formatChoice(choice, this.nominations[ choice ], this.tallies[ choice ]));
strMsg += (steamID, 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-02 05:52:42 -05:00
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-03-19 18:11:24 -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-02 05:52:42 -05:00
await this.warn(steamID, `Registered vote: ${this.nominations[ nominationIndex ].replace(/\_/gi, ' ').replace(/\sv\d{1,2}/gi, '')} ${this.factionStrings[ nominationIndex ]} (${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-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
}
//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 < highestScore)
2022-03-19 18:11:24 -05:00
continue;
2022-08-13 05:05:40 -05:00
else if (score > highestScore) {
2022-03-19 18:11:24 -05:00
highestScore = score;
ties.length = 0;
ties.push(choice);
}
else // equal
ties.push(choice);
}
2022-08-13 05:05:40 -05:00
return ties.map(i => this.nominations[ i ]);
2022-03-19 18:11:24 -05:00
}
}