mirror of
https://github.com/AsgardEternal/SquadJS.git
synced 2024-09-28 16:24:25 -05:00
commit
9bc60d7ee4
@ -1,3 +1,4 @@
|
||||
{
|
||||
"singleQuote": true
|
||||
"singleQuote": true,
|
||||
"printWidth": 80
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -3,10 +3,12 @@
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./squad-layers": "./squad-layers/index.js",
|
||||
"./scbl": "./scbl.js"
|
||||
"./scbl": "./scbl.js",
|
||||
"./squad-layer-filter": "./squad-layer-filter.js",
|
||||
"./squad-layers": "./squad-layers.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"didyoumean": "^1.2.1",
|
||||
"graphql-request": "^1.8.2"
|
||||
}
|
||||
}
|
||||
|
219
connectors/squad-layer-filter.js
Normal file
219
connectors/squad-layer-filter.js
Normal file
@ -0,0 +1,219 @@
|
||||
import fs from 'fs';
|
||||
import SquadLayers, {
|
||||
SquadLayers as SquadLayersClass
|
||||
} from './squad-layers.js';
|
||||
|
||||
export default class SquadLayerFilter extends SquadLayersClass {
|
||||
constructor(layers, activeLayerFilter = null) {
|
||||
super(layers);
|
||||
|
||||
if (activeLayerFilter === null) {
|
||||
this.activeLayerFilter = null;
|
||||
} else {
|
||||
this.activeLayerFilter = {
|
||||
historyResetTime: 5 * 60 * 60 * 1000,
|
||||
layerHistoryTolerance: 8,
|
||||
mapHistoryTolerance: 4,
|
||||
gamemodeHistoryTolerance: {
|
||||
// defaults as off
|
||||
...activeLayerFilter.gamemodeHistoryTolerance
|
||||
},
|
||||
playerCountComplianceEnabled: true,
|
||||
...activeLayerFilter
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static buildFromList(layerNames, activeLayerFilter) {
|
||||
return new SquadLayerFilter(layerNames, activeLayerFilter);
|
||||
}
|
||||
|
||||
static buildFromDidYouMeanList(layerNames, activeLayerFilter) {
|
||||
const layers = [];
|
||||
for (const layerName of layerNames) {
|
||||
const layer = SquadLayers.getLayerByDidYouMean(
|
||||
layerName,
|
||||
SquadLayers.getLayerNames()
|
||||
);
|
||||
if (layer) layers.push(layer);
|
||||
}
|
||||
return new SquadLayerFilter(layers, activeLayerFilter);
|
||||
}
|
||||
|
||||
static buildFromFile(filename, activeLayerFilter, delimiter = '\n') {
|
||||
const lines = fs
|
||||
.readFileSync('./connectors/data/layers.json', 'utf8')
|
||||
.split(delimiter);
|
||||
const layers = [];
|
||||
|
||||
const validLayerNames = SquadLayers.getLayerNames();
|
||||
|
||||
for (const line of lines) {
|
||||
if (validLayerNames.contains(line))
|
||||
layers.push(SquadLayers.getLayerByLayerName(line));
|
||||
}
|
||||
return new SquadLayerFilter(layers, activeLayerFilter);
|
||||
}
|
||||
|
||||
static buildFromFilter(filter = {}, activeLayerFilter) {
|
||||
const whitelistedLayers = filter.whitelistedLayers || null;
|
||||
const blacklistedLayers = filter.blacklistedLayers || null;
|
||||
const whitelistedMaps = filter.whitelistedMaps || null;
|
||||
const blacklistedMaps = filter.blacklistedMaps || null;
|
||||
const whitelistedGamemodes = filter.whitelistedGamemodes || null;
|
||||
const blacklistedGamemodes = filter.blacklistedGamemodes || ['Training'];
|
||||
const flagCountMin = filter.flagCountMin || null;
|
||||
const flagCountMax = filter.flagCountMax || null;
|
||||
const hasCommander = filter.hasCommander || null;
|
||||
const hasTanks = filter.hasTanks || null;
|
||||
const hasHelicopters = filter.hasHelicopters || null;
|
||||
|
||||
const layers = [];
|
||||
|
||||
for (const layer of SquadLayers.getLayers()) {
|
||||
// Whitelist / Blacklist Layers
|
||||
if (
|
||||
whitelistedLayers !== null &&
|
||||
!whitelistedLayers.includes(layer.layer)
|
||||
)
|
||||
continue;
|
||||
if (blacklistedLayers !== null && blacklistedLayers.includes(layer.layer))
|
||||
continue;
|
||||
|
||||
// Whitelist / Blacklist Maps
|
||||
if (whitelistedMaps !== null && !whitelistedMaps.includes(layer.map))
|
||||
continue;
|
||||
if (blacklistedMaps !== null && blacklistedMaps.includes(layer.map))
|
||||
continue;
|
||||
|
||||
// Whitelist / Blacklist Gamemodes
|
||||
if (
|
||||
whitelistedGamemodes !== null &&
|
||||
!whitelistedGamemodes.includes(layer.gamemode)
|
||||
)
|
||||
continue;
|
||||
if (
|
||||
blacklistedGamemodes !== null &&
|
||||
blacklistedGamemodes.includes(layer.gamemode)
|
||||
)
|
||||
continue;
|
||||
|
||||
// Flag Count
|
||||
if (flagCountMin !== null && layer.flagCount < flagCountMin) continue;
|
||||
if (flagCountMax !== null && layer.flagCount > flagCountMax) continue;
|
||||
|
||||
// Other Properties
|
||||
if (hasCommander !== null && layer.commander !== hasCommander) continue;
|
||||
if (hasTanks !== null && (layer.tanks !== 'N/A') !== hasTanks) continue;
|
||||
if (
|
||||
hasHelicopters !== null &&
|
||||
(layer.helicopters !== 'N/A') !== hasHelicopters
|
||||
)
|
||||
continue;
|
||||
|
||||
layers.push(layer);
|
||||
}
|
||||
|
||||
return new SquadLayerFilter(layers, activeLayerFilter);
|
||||
}
|
||||
|
||||
inLayerPool(layer) {
|
||||
if (typeof layer === 'object') layer = layer.layer;
|
||||
return super.getLayerNames().includes(layer);
|
||||
}
|
||||
|
||||
isLayerHistoryCompliant(server, layer) {
|
||||
if (this.activeLayerFilter === null) return true;
|
||||
|
||||
if (typeof layer === 'object') layer = layer.layer;
|
||||
|
||||
for (
|
||||
let i = 0;
|
||||
i <
|
||||
Math.min(
|
||||
server.layerHistory.length,
|
||||
this.activeLayerFilter.layerHistoryTolerance
|
||||
);
|
||||
i++
|
||||
) {
|
||||
if (
|
||||
new Date() - server.layerHistory[i].time >
|
||||
this.activeLayerFilter.historyResetTime
|
||||
)
|
||||
return true;
|
||||
if (server.layerHistory[i].layer === layer) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
isMapHistoryCompliant(server, layer) {
|
||||
if (this.activeLayerFilter === null) return true;
|
||||
|
||||
if (typeof layer === 'string')
|
||||
layer = SquadLayers.getLayerByLayerName(layer);
|
||||
|
||||
for (
|
||||
let i = 0;
|
||||
i <
|
||||
Math.min(
|
||||
server.layerHistory.length,
|
||||
this.activeLayerFilter.mapHistoryTolerance
|
||||
);
|
||||
i++
|
||||
) {
|
||||
if (
|
||||
new Date() - server.layerHistory[i].time >
|
||||
this.activeLayerFilter.historyResetTime
|
||||
)
|
||||
return true;
|
||||
if (server.layerHistory[i].map === layer.map) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
isGamemodeHistoryCompliant(server, layer) {
|
||||
if (this.activeLayerFilter === null) return true;
|
||||
|
||||
if (typeof layer === 'string')
|
||||
layer = SquadLayers.getLayerByLayerName(layer);
|
||||
|
||||
const gamemodeHistoryTolerance = this.activeLayerFilter
|
||||
.gamemodeHistoryTolerance[layer.gamemode];
|
||||
if (!gamemodeHistoryTolerance) return true;
|
||||
|
||||
for (
|
||||
let i = 0;
|
||||
i < Math.min(server.layerHistory.length, gamemodeHistoryTolerance);
|
||||
i++
|
||||
) {
|
||||
if (
|
||||
new Date() - server.layerHistory[i].time >
|
||||
this.activeLayerFilter.historyResetTime
|
||||
)
|
||||
return true;
|
||||
|
||||
const historyLayer = SquadLayers.getLayerByLayerName(
|
||||
server.layerHistory[i].layer
|
||||
);
|
||||
if (historyLayer && historyLayer.gamemode === layer.gamemode)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
isPlayerCountCompliant(server, layer) {
|
||||
if (
|
||||
this.activeLayerFilter === null ||
|
||||
this.playerCountComplianceEnabled === false
|
||||
)
|
||||
return true;
|
||||
|
||||
if (typeof layer === 'string')
|
||||
layer = SquadLayers.getLayerByLayerName(layer);
|
||||
|
||||
return !(
|
||||
server.players.length > layer.estimatedSuitablePlayerCount.max ||
|
||||
server.players.length < layer.estimatedSuitablePlayerCount.min
|
||||
);
|
||||
}
|
||||
}
|
57
connectors/squad-layers.js
Normal file
57
connectors/squad-layers.js
Normal file
@ -0,0 +1,57 @@
|
||||
import fs from 'fs';
|
||||
|
||||
import didYouMean from 'didyoumean';
|
||||
|
||||
class SquadLayers {
|
||||
constructor(layers) {
|
||||
if (Array.isArray(layers)) {
|
||||
this.layers = layers;
|
||||
} else {
|
||||
this.layers = JSON.parse(
|
||||
fs.readFileSync('./connectors/data/layers.json', 'utf8')
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.layers.length; i++) {
|
||||
this.layers[i] = {
|
||||
...this.layers[i],
|
||||
layerNumber: i + 1
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
getLayers() {
|
||||
return this.layers;
|
||||
}
|
||||
|
||||
getLayerNames() {
|
||||
return this.layers.map(layer => layer.layer);
|
||||
}
|
||||
|
||||
getLayerByLayerName(layerName) {
|
||||
const layer = this.layers.filter(layer => layer.layer === layerName);
|
||||
return layer.length === 1 ? layer[0] : null;
|
||||
}
|
||||
|
||||
getLayerByLayerClassname(layerClassname) {
|
||||
const layer = this.layers.filter(
|
||||
layer => layer.layerClassname === layerClassname
|
||||
);
|
||||
return layer.length === 1 ? layer[0] : null;
|
||||
}
|
||||
|
||||
getLayerByDidYouMean(layerName) {
|
||||
layerName = didYouMean(layerName, this.getLayerNames());
|
||||
|
||||
const layer = this.layers.filter(layer => layer.layer === layerName);
|
||||
return layer.length === 1 ? layer[0] : null;
|
||||
}
|
||||
|
||||
getLayerByNumber(number) {
|
||||
const layer = this.layers.filter(layer => layer.layerNumber === number);
|
||||
return layer.length === 1 ? layer[0] : null;
|
||||
}
|
||||
}
|
||||
|
||||
export { SquadLayers };
|
||||
export default new SquadLayers();
|
@ -1,117 +0,0 @@
|
||||
import fs from 'fs';
|
||||
|
||||
class SquadLayers {
|
||||
constructor() {
|
||||
this.layers = JSON.parse(
|
||||
fs.readFileSync('./connectors/squad-layers/layers.json', 'utf8')
|
||||
);
|
||||
}
|
||||
|
||||
getLayerByLayerName(layerName) {
|
||||
const layer = this.layers.filter(layer => layer.layer === layerName);
|
||||
return layer.length === 1 ? layer[0] : null;
|
||||
}
|
||||
|
||||
getLayerByLayerClassname(layerClassname) {
|
||||
const layer = this.layers.filter(
|
||||
layer => layer.layerClassname === layerClassname
|
||||
);
|
||||
return layer.length === 1 ? layer[0] : null;
|
||||
}
|
||||
|
||||
getLayerNames() {
|
||||
return this.layers.map(layer => layer.layer);
|
||||
}
|
||||
|
||||
getFilteredLayers(filter = {}) {
|
||||
const whitelistedLayers = filter.whitelistedLayers || null;
|
||||
const blacklistedLayers = filter.blacklistedLayers || null;
|
||||
const whitelistedMaps = filter.whitelistedMaps || null;
|
||||
const blacklistedMaps = filter.blacklistedMaps || null;
|
||||
const whitelistedGamemodes = filter.whitelistedGamemodes || null;
|
||||
const blacklistedGamemodes = filter.blacklistedGamemodes || ['Training'];
|
||||
const flagCountMin = filter.flagCountMin || null;
|
||||
const flagCountMax = filter.flagCountMax || null;
|
||||
const hasCommander = filter.hasCommander || null;
|
||||
const hasTanks = filter.hasTanks || null;
|
||||
const hasHelicopters = filter.hasHelicopters || null;
|
||||
|
||||
const layers = [];
|
||||
|
||||
for (const layer of this.layers) {
|
||||
// Whitelist / Blacklist Layers
|
||||
if (
|
||||
whitelistedLayers !== null &&
|
||||
!whitelistedLayers.includes(layer.layer)
|
||||
)
|
||||
continue;
|
||||
if (blacklistedLayers !== null && blacklistedLayers.includes(layer.layer))
|
||||
continue;
|
||||
|
||||
// Whitelist / Blacklist Maps
|
||||
if (whitelistedMaps !== null && !whitelistedMaps.includes(layer.map))
|
||||
continue;
|
||||
if (blacklistedMaps !== null && blacklistedMaps.includes(layer.map))
|
||||
continue;
|
||||
|
||||
// Whitelist / Blacklist Gamemodes
|
||||
if (
|
||||
whitelistedGamemodes !== null &&
|
||||
!whitelistedGamemodes.includes(layer.gamemode)
|
||||
)
|
||||
continue;
|
||||
if (
|
||||
blacklistedGamemodes !== null &&
|
||||
blacklistedGamemodes.includes(layer.gamemode)
|
||||
)
|
||||
continue;
|
||||
|
||||
// Flag Count
|
||||
if (flagCountMin !== null && layer.flagCount < flagCountMin) continue;
|
||||
if (flagCountMax !== null && layer.flagCount > flagCountMax) continue;
|
||||
|
||||
// Other Properties
|
||||
if (hasCommander !== null && layer.commander !== hasCommander) continue;
|
||||
if (hasTanks !== null && (layer.tanks !== 'N/A') !== hasTanks) continue;
|
||||
if (
|
||||
hasHelicopters !== null &&
|
||||
(layer.helicopters !== 'N/A') !== hasHelicopters
|
||||
)
|
||||
continue;
|
||||
|
||||
layers.push(layer.layer);
|
||||
}
|
||||
|
||||
return layers;
|
||||
}
|
||||
|
||||
isHistoryCompliant(layerHistory, layer, options = {}) {
|
||||
const layerTolerance = options.layerTolerance || 4;
|
||||
const mapTolerance = options.mapTolerance || 2;
|
||||
const timeTolerance = options.timeTolerance || 5 * 60 * 60 * 1000;
|
||||
|
||||
for (let i = 0; i < layerHistory.length; i++) {
|
||||
if (i >= Math.max(layerHistory, mapTolerance)) return true;
|
||||
if (new Date() - layerHistory[i].time > timeTolerance) return true;
|
||||
|
||||
if (
|
||||
i < layerTolerance &&
|
||||
layerHistory[i].map === this.getLayerByLayerName(layer).map
|
||||
)
|
||||
return false;
|
||||
if (i < layerTolerance && layerHistory[i].layer === layer) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
isPlayerCountCompliant(playerCount, layer) {
|
||||
return !(
|
||||
playerCount >
|
||||
this.getLayerByLayerName(layer).estimatedSuitablePlayerCount.max ||
|
||||
playerCount <
|
||||
this.getLayerByLayerName(layer).estimatedSuitablePlayerCount.min
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default new SquadLayers();
|
5
index.js
5
index.js
@ -3,6 +3,7 @@ import mysql from 'mysql';
|
||||
import Influx from 'influx';
|
||||
|
||||
import Server from 'squad-server';
|
||||
import SquadLayerFilter from 'connectors/squad-layer-filter';
|
||||
|
||||
import {
|
||||
discordAdminCamLogs,
|
||||
@ -36,7 +37,9 @@ async function main() {
|
||||
await discordTeamkill(server, discordClient, 'discordChannelID');
|
||||
|
||||
// in game features
|
||||
mapvote(server);
|
||||
const squadLayerFilter = SquadLayerFilter.buildFromFilter({});
|
||||
mapvote(server, 'didyoumean', squadLayerFilter, {});
|
||||
|
||||
teamRandomizer(server);
|
||||
|
||||
// MySQL Plugins
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "SquadJS",
|
||||
"version": "1.0.9",
|
||||
"version": "1.0.10",
|
||||
"repository": "https://github.com/Thomas-Smyth/SquadJS.git",
|
||||
"author": "Thomas Smyth <https://github.com/Thomas-Smyth>",
|
||||
"license": "MIT",
|
||||
@ -18,6 +18,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"connectors": "1.0.0",
|
||||
"discord.js": "^12.2.0",
|
||||
"influx": "^5.5.1",
|
||||
"mysql": "^2.18.1",
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { LOG_PARSER_TEAMKILL } from 'squad-server/events/log-parser';
|
||||
|
||||
export default async function(server, options= {}) {
|
||||
export default async function(server, options = {}) {
|
||||
if (!server)
|
||||
throw new Error(
|
||||
'DiscordAdminCamLogs must be provided with a reference to the server.'
|
||||
@ -8,8 +8,10 @@ export default async function(server, options= {}) {
|
||||
|
||||
server.on(LOG_PARSER_TEAMKILL, info => {
|
||||
// ignore suicides
|
||||
if(info.attacker.steamID === info.victim.steamID) return;
|
||||
server.rcon.execute(`AdminWarn "${info.attacker.steamID}" ${options.message || 'Please apologise for ALL TKs in ALL chat!'}`);
|
||||
if (info.attacker.steamID === info.victim.steamID) return;
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.attacker.steamID}" ${options.message ||
|
||||
'Please apologise for ALL TKs in ALL chat!'}`
|
||||
);
|
||||
});
|
||||
|
||||
}
|
||||
|
@ -1,12 +1,10 @@
|
||||
import { COPYRIGHT_MESSAGE } from 'core/config';
|
||||
import { LOG_PARSER_PLAYER_POSSESS, LOG_PARSER_PLAYER_UNPOSSESS } from 'squad-server/events/log-parser';
|
||||
import {
|
||||
LOG_PARSER_PLAYER_POSSESS,
|
||||
LOG_PARSER_PLAYER_UNPOSSESS
|
||||
} from 'squad-server/events/log-parser';
|
||||
|
||||
export default async function(
|
||||
server,
|
||||
discordClient,
|
||||
channelID,
|
||||
options = {}
|
||||
) {
|
||||
export default async function(server, discordClient, channelID, options = {}) {
|
||||
if (!server)
|
||||
throw new Error(
|
||||
'DiscordAdminCamLogs must be provided with a reference to the server.'
|
||||
@ -59,7 +57,8 @@ export default async function(
|
||||
});
|
||||
|
||||
server.on(LOG_PARSER_PLAYER_UNPOSSESS, info => {
|
||||
if (info.switchPossess === true || !(info.player.steamID in adminsInCam)) return;
|
||||
if (info.switchPossess === true || !(info.player.steamID in adminsInCam))
|
||||
return;
|
||||
|
||||
channel.send({
|
||||
embed: {
|
||||
@ -81,7 +80,7 @@ export default async function(
|
||||
value: `${Math.round(
|
||||
(info.time.getTime() -
|
||||
adminsInCam[info.player.steamID].getTime()) /
|
||||
60000
|
||||
60000
|
||||
)} mins`
|
||||
}
|
||||
],
|
||||
|
@ -1,12 +1,7 @@
|
||||
import { COPYRIGHT_MESSAGE } from 'core/config';
|
||||
import { RCON_CHAT_MESSAGE } from 'squad-server/events/rcon';
|
||||
|
||||
export default async function(
|
||||
server,
|
||||
discordClient,
|
||||
channelID,
|
||||
options = {}
|
||||
) {
|
||||
export default async function(server, discordClient, channelID, options = {}) {
|
||||
if (!server)
|
||||
throw new Error(
|
||||
'DiscordChat must be provided with a reference to the server.'
|
||||
|
@ -1,9 +1,4 @@
|
||||
export default async function(
|
||||
server,
|
||||
discordClient,
|
||||
channelID,
|
||||
events = []
|
||||
) {
|
||||
export default async function(server, discordClient, channelID, events = []) {
|
||||
if (!server)
|
||||
throw new Error(
|
||||
'DiscordDebug must be provided with a reference to the server.'
|
||||
|
@ -24,6 +24,8 @@ await discordServerStatus(
|
||||
discordClient,
|
||||
{ // options - the options included below display the defaults and can be removed for simplicity.
|
||||
color: 16761867, // color of embed
|
||||
colorGradient: true, // gradient color based on player count
|
||||
connectLink: true, // show Steam connect link
|
||||
command: '!server', // command used to send message
|
||||
disableStatus: false // disable bot status as server status
|
||||
}
|
||||
|
@ -1,7 +1,14 @@
|
||||
import { COPYRIGHT_MESSAGE } from 'core/config';
|
||||
import tinygradient from 'tinygradient';
|
||||
|
||||
import { COPYRIGHT_MESSAGE } from 'core/config';
|
||||
import { SERVER_A2S_UPDATED } from 'squad-server/events/server';
|
||||
|
||||
const gradient = tinygradient([
|
||||
{ color: '#ff0000', pos: 0 },
|
||||
{ color: '#ffff00', pos: 0.5 },
|
||||
{ color: '#00ff00', pos: 1 }
|
||||
]);
|
||||
|
||||
function makeEmbed(server, options) {
|
||||
let players = `${server.playerCount}`;
|
||||
if (server.publicQueue + server.reserveQueue > 0)
|
||||
@ -9,26 +16,39 @@ function makeEmbed(server, options) {
|
||||
players += ` / ${server.publicSlots}`;
|
||||
if (server.reserveSlots > 0) players += ` (+${server.reserveSlots})`;
|
||||
|
||||
const fields = [
|
||||
{
|
||||
name: 'Players',
|
||||
value: `\`\`\`${players}\`\`\``
|
||||
},
|
||||
{
|
||||
name: 'Current Layer',
|
||||
value: `\`\`\`${server.currentLayer}\`\`\``,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Next Layer',
|
||||
value: `\`\`\`${server.nextLayer || 'Unknown'}\`\`\``,
|
||||
inline: true
|
||||
}
|
||||
];
|
||||
|
||||
if (options.connectLink)
|
||||
fields.push({
|
||||
name: 'Join Server',
|
||||
value: `steam://connect/${server.host}:${server.queryPort}`
|
||||
});
|
||||
|
||||
return {
|
||||
embed: {
|
||||
title: server.serverName,
|
||||
color: options.color,
|
||||
fields: [
|
||||
{
|
||||
name: 'Players',
|
||||
value: `\`\`\`${players}\`\`\``
|
||||
},
|
||||
{
|
||||
name: 'Current Layer',
|
||||
value: `\`\`\`${server.currentLayer}\`\`\``,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Next Layer',
|
||||
value: `\`\`\`${server.nextLayer || 'Unknown'}\`\`\``,
|
||||
inline: true
|
||||
}
|
||||
],
|
||||
color: options.colorGradient
|
||||
? parseInt(
|
||||
gradient.rgbAt(server.playerCount / server.publicSlots).toHex(),
|
||||
16
|
||||
)
|
||||
: options.color,
|
||||
fields: fields,
|
||||
timestamp: new Date().toISOString(),
|
||||
footer: {
|
||||
text: `Server Status by ${COPYRIGHT_MESSAGE}`
|
||||
@ -48,6 +68,8 @@ export default async function(server, discordClient, options = {}) {
|
||||
|
||||
options = {
|
||||
color: 16761867,
|
||||
colorGradient: true,
|
||||
connectLink: true,
|
||||
command: '!server',
|
||||
disableStatus: false,
|
||||
...options
|
||||
@ -82,6 +104,10 @@ export default async function(server, discordClient, options = {}) {
|
||||
});
|
||||
|
||||
server.on(SERVER_A2S_UPDATED, () => {
|
||||
if(!options.disableStatus) discordClient.user.setActivity(`(${server.playerCount}/${server.publicSlots}) ${server.currentLayer}`, { type: 'WATCHING' });
|
||||
if (!options.disableStatus)
|
||||
discordClient.user.setActivity(
|
||||
`(${server.playerCount}/${server.publicSlots}) ${server.currentLayer}`,
|
||||
{ type: 'WATCHING' }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -24,7 +24,9 @@ await discordTeamkill(
|
||||
discordClient,
|
||||
'discordChannelID',
|
||||
{ // options - the options included below display the defaults and can be removed for simplicity.
|
||||
color: 16761867 // color of embed
|
||||
teamkillColor: 16761867, // colour of TK embed
|
||||
suicideColor: 16761867, // colour of suicide embed
|
||||
ignoreSuicides: false, // ignore suicide events
|
||||
}
|
||||
);
|
||||
```
|
||||
|
@ -1,12 +1,7 @@
|
||||
import { COPYRIGHT_MESSAGE } from 'core/config';
|
||||
import { LOG_PARSER_TEAMKILL } from 'squad-server/events/log-parser';
|
||||
|
||||
export default async function(
|
||||
server,
|
||||
discordClient,
|
||||
channelID,
|
||||
options = {}
|
||||
) {
|
||||
export default async function(server, discordClient, channelID, options = {}) {
|
||||
if (!server)
|
||||
throw new Error(
|
||||
'DiscordTeamKill must be provided with a reference to the server.'
|
||||
@ -21,7 +16,9 @@ export default async function(
|
||||
throw new Error('DiscordTeamkill must be provided with a channel ID.');
|
||||
|
||||
options = {
|
||||
color: 16761867,
|
||||
teamkillColor: 16761867,
|
||||
suicideColor: 16761867,
|
||||
ignoreSuicides: false,
|
||||
disableSCBL: false,
|
||||
...options
|
||||
};
|
||||
@ -30,6 +27,7 @@ export default async function(
|
||||
|
||||
server.on(LOG_PARSER_TEAMKILL, info => {
|
||||
if (!info.attacker) return;
|
||||
if (options.ignoreSuicides && info.suicide) return;
|
||||
|
||||
const fields = [
|
||||
{
|
||||
@ -58,15 +56,18 @@ export default async function(
|
||||
}
|
||||
];
|
||||
|
||||
if(!options.disableSCBL) fields.push({
|
||||
name: 'Squad Community Ban List',
|
||||
value: `[Attacker's Bans](https://squad-community-ban-list.com/search/${info.attacker.steamID})\n[Victims's Bans](https://squad-community-ban-list.com/search/${info.victim.steamID})`
|
||||
});
|
||||
if (!options.disableSCBL)
|
||||
fields.push({
|
||||
name: 'Squad Community Ban List',
|
||||
value: `[Attacker's Bans](https://squad-community-ban-list.com/search/${info.attacker.steamID})\n[Victims's Bans](https://squad-community-ban-list.com/search/${info.victim.steamID})`
|
||||
});
|
||||
|
||||
channel.send({
|
||||
embed: {
|
||||
title: `${info.attacker.steamID === info.victim.steamID ? 'Suicide' : 'Teamkill'}: ${info.attacker.name}`,
|
||||
color: options.color,
|
||||
title: `${info.suicide ? 'Suicide' : 'Teamkill'}: ${
|
||||
info.attacker.name
|
||||
}`,
|
||||
color: info.suicide ? options.suicideColor : options.teamkillColor,
|
||||
fields: fields,
|
||||
timestamp: info.time.toISOString(),
|
||||
footer: {
|
||||
|
@ -16,7 +16,7 @@ export default function(server, influxDB, options = {}) {
|
||||
|
||||
if (!influxDB)
|
||||
throw new Error('InfluxDBLog must be provided with a InfluxDB connection.');
|
||||
|
||||
|
||||
const serverID = options.overrideServerID || server.id;
|
||||
|
||||
let points = [];
|
||||
|
@ -2,35 +2,120 @@
|
||||
|
||||
<img src="../../assets/squadjs-logo.png" alt="Logo" width="500"/>
|
||||
|
||||
#### SquadJS - Mapvote
|
||||
#### SquadJS - Map Vote
|
||||
</div>
|
||||
|
||||
## About
|
||||
The mapvote plugin uses a "did you mean?" system to allow for players to vote for a wide range of layers. Command information for using the plugin in-game can be accessed by typing `!mapvote help` into in-game chat.
|
||||
## Map Vote "Did you mean?"
|
||||
### About
|
||||
Map Vote "Did you mean?" is best suited for servers who wish to allow players to vote for any layer in a large pool of options as it allows players to vote by specifying the layer name in chat. It uses a "did you mean?" algorithm to correct misspelling in layer names making it easier for players to vote.
|
||||
|
||||
## Installation
|
||||
Place the following into your `index.js` file. The filters / options below are optional and can be removed without affecting functionality, however, the default options are shown for reference.
|
||||
Commands:
|
||||
* `!mapvote help` - Shows other commands players can use.
|
||||
* `!mapvote results` - See the map vote results.
|
||||
* `!mapvote <layer name>` - Vote for a specific layer. Misspelling will be corrected where possible.
|
||||
|
||||
|
||||
* `!mapvote start` (Admin chat only) - Starts a new map vote.
|
||||
* `!mapvote restart` (Admin chat only) - Restarts a map vote.
|
||||
* `!mapvote end` (Admin chat only) - Ends a map vote and announces the winner.
|
||||
* `!mapvote destroy` (Admin chat only) - End a map vote without announcing the winner.
|
||||
|
||||
### Installation
|
||||
Add the following two lines at the top of your index.js file to import the required components:
|
||||
```js
|
||||
import SquadLayerFilter from 'connectors/squad-layer-filter';
|
||||
import { mapvote } from 'plugins';
|
||||
```
|
||||
|
||||
To control which constraints, e.g. map history and player count compliant, you need to create an active layer filter.
|
||||
```js
|
||||
const activeLayerFilter = {
|
||||
historyResetTime: 5 * 60 * 60 * 1000, // after 5 hours the layer history is ignored. null if off
|
||||
layerHistoryTolerance: 8, // a layer can be only played once every x layers. null if off
|
||||
mapHistoryTolerance: 4, // a map can only be played once every x layers. null if off
|
||||
gamemodeHistoryTolerance: {
|
||||
Invasion: 4 // invasion can only be played once every x layers
|
||||
// if not specified they will default to off
|
||||
},
|
||||
playerCountComplianceEnabled: true // filter layers based on suggested player counts if true
|
||||
};
|
||||
```
|
||||
|
||||
You can turn off all options with:
|
||||
```js
|
||||
const activeLayerFilter = null;
|
||||
```
|
||||
|
||||
Create a layer pool with one of the following options:
|
||||
```js
|
||||
// from a list
|
||||
const squadLayerFilter = SquadLayerFilter.buildFromFilter(['Layer name 1', 'layer name 2'], activeLayerFilter);
|
||||
|
||||
// from a file of layer anmes separated by new lines
|
||||
const squadLayerFilter = SquadLayerFilter.buildFromFile('filename', activeLayerFilter);
|
||||
|
||||
// from a filter
|
||||
const squadLayerFilter = SquadLayerFilter.buildFromFilter(
|
||||
{ // these options can also be turned off by replacing the value with null
|
||||
whitelistedLayers: null, // a list of layers that can be played
|
||||
blacklistedLayers: null, // a list of layers that cannot be played
|
||||
whitelistedMaps: null, // a list of maps that can be played
|
||||
blacklistedMaps: null, // a list of maps that cannot be played
|
||||
whitelistedGamemodes: null, // a list of gamemodes that can be played
|
||||
blacklistedGamemodes: ['Training'], // a list of gamemodes that cannot be played
|
||||
flagCountMin: null, // layers must have move than this number of flags
|
||||
flagCountMax: null, // layers must have less than this number of flags
|
||||
hasCommander: null, // layer must have a commander
|
||||
hasTanks: null, // layer must have tanks
|
||||
hasHelicopters: null // layer must have helicopters
|
||||
},
|
||||
activeLayerFilter
|
||||
);
|
||||
```
|
||||
|
||||
Setup the map vote plugin:
|
||||
```js
|
||||
mapvote(
|
||||
server,
|
||||
{ // layer filter to limit layers - remove or edit the below options to adjust the filter. Leaving this blank will remove all training layers as a default.
|
||||
whitelistedLayers: ['layer name'], // an array of layers that can be played
|
||||
blacklistedLayers: ['layer name'], // an array of layers that cannot be played
|
||||
whitelistedMaps: ['map name'], // an array of maps that can be played
|
||||
blacklistedMaps: ['map name'], // an array of maps that cannot be played - default removes training maps
|
||||
whitelistedGamemodes: ['gamemode name'], // an array of gamemodes that can be played
|
||||
blacklistedGamemodes: ['gamemode name'], // an array of gamemodes that cannot be played
|
||||
flagCountMin: 4, // the minimum number of flags the layer must have
|
||||
flagCountMax: 7, // the maximum number of flags the layer must have
|
||||
hasCommander: true, // has commander enabled
|
||||
hasTanks: true, // has tanks
|
||||
hasHelicopters: true // has helicopters
|
||||
},
|
||||
{ // options - remove or edit the below options. The defaults are shown.
|
||||
command: '!mapvote', // the command name used to access the vote
|
||||
layerTolerance: 4, // the number of other layers that must be played before the layer can be revoted for
|
||||
mapTolerance: 2, // the number of other maps that must be played before the layer can be revoted for
|
||||
timeTolerance: 5 * 60 * 60 * 1000 // the time that must pass before the above are ignored
|
||||
server,
|
||||
'didyoumean',
|
||||
squadLayerFilter,
|
||||
{
|
||||
alwaysOn: true, // map vote will start without admin interaction if true
|
||||
minPlayerCount: null, // this number of players must be online before they can vote. null is off
|
||||
minVoteCount: null, // this number of votes must be counted before a layer is selected. null is off
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## Map Vote "123"
|
||||
### About
|
||||
Map Vote "123" is best suited for servers who want to allow admins to create map votes that allow players to easily choose from a small selection of layers.
|
||||
|
||||
Commands:
|
||||
* `!mapvote help` - Shows other commands players can use.
|
||||
* `!mapvote results` - See the map vote results.
|
||||
* `<layer number>` - Vote for a specific layer via it's associated number.
|
||||
|
||||
|
||||
* `!mapvote start <layer name 1>, <layer name 2>` (Admin chat only) - Starts a new map vote.
|
||||
* `!mapvote restart` (Admin chat only) - Restarts a map vote with the same layers.
|
||||
* `!mapvote end` (Admin chat only) - Ends a map vote and announces the winner.
|
||||
* `!mapvote destroy` (Admin chat only) - End a map vote without announcing the winner.
|
||||
|
||||
### Installation
|
||||
Add the following two lines at the top of your index.js file to import the required components:
|
||||
```js
|
||||
import SquadLayerFilter from 'connectors/squad-layer-filter';
|
||||
import { mapvote } from 'plugins';
|
||||
```
|
||||
|
||||
Setup the map vote plugin:
|
||||
```js
|
||||
mapvote(
|
||||
server,
|
||||
'123',
|
||||
{
|
||||
minVoteCount: null, // this number of votes must be counted before a layer is selected. null is off
|
||||
}
|
||||
);
|
||||
```
|
||||
|
@ -1,145 +1,15 @@
|
||||
import didYouMean from 'didyoumean';
|
||||
import mapvoteDidYouMean from './mapvote-did-you-mean.js';
|
||||
import mapvote123 from './mapvote-123.js';
|
||||
|
||||
import { COPYRIGHT_MESSAGE } from 'core/config';
|
||||
|
||||
import SquadLayers from 'connectors/squad-layers';
|
||||
|
||||
import { RCON_CHAT_MESSAGE } from 'squad-server/events/rcon';
|
||||
import { SERVER_LAYER_CHANGE } from 'squad-server/events/server';
|
||||
|
||||
export default function(server, layerFilter = {}, options = {}) {
|
||||
if (!server)
|
||||
throw new Error('Mapvote must be provided with a reference to the server.');
|
||||
|
||||
const command = options.command || '!mapvote';
|
||||
const commandRegex = new RegExp(`^${command} ([A-z0-9'_ ]*)`, 'i');
|
||||
const rotation = SquadLayers.getFilteredLayers(layerFilter);
|
||||
|
||||
let voteCounts = {};
|
||||
let votes = {};
|
||||
let currentWinner = null;
|
||||
|
||||
function getResults() {
|
||||
let results;
|
||||
|
||||
results = Object.keys(voteCounts).map(layer => {
|
||||
return {
|
||||
layer: layer,
|
||||
voteCount: voteCounts[layer]
|
||||
};
|
||||
});
|
||||
|
||||
results = results.sort((a, b) => {
|
||||
if (a.voteCount > b.voteCount) return -1;
|
||||
if (a.voteCount < b.voteCount) return 1;
|
||||
else return Math.random() < 0.5 ? 1 : -1;
|
||||
});
|
||||
|
||||
return results;
|
||||
export default function(server, mode, ...args) {
|
||||
switch (mode) {
|
||||
case 'didyoumean':
|
||||
mapvoteDidYouMean(server, ...args);
|
||||
break;
|
||||
case '123':
|
||||
mapvote123(server, ...args);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid mode.');
|
||||
}
|
||||
|
||||
server.on(SERVER_LAYER_CHANGE, () => {
|
||||
voteCounts = {};
|
||||
votes = {};
|
||||
currentWinner = null;
|
||||
});
|
||||
|
||||
server.on(RCON_CHAT_MESSAGE, info => {
|
||||
const match = info.message.match(commandRegex);
|
||||
if (!match) return;
|
||||
|
||||
if (match[1] === 'help') {
|
||||
// show help options
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.steamID}" You may use any of the following commands in chat:`
|
||||
);
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.steamID}" !mapvote results - View the current vote counts.`
|
||||
);
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.steamID}" !mapvote <layer name> - Vote for the specified layer.`
|
||||
);
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.steamID}" When inputting a layer name, we autocorrect any miss spelling.`
|
||||
);
|
||||
} else if (match[1] === 'results') {
|
||||
// display results to player
|
||||
const results = getResults();
|
||||
|
||||
if (results.length === 0) {
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.steamID}" No one has voted yet.`
|
||||
);
|
||||
} else {
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.steamID}" The current vote counts are as follows:`
|
||||
);
|
||||
for (const result of results) {
|
||||
if (result.voteCount === 0) continue;
|
||||
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.steamID}" ${result.layer} - ${
|
||||
result.voteCount
|
||||
} vote${result.voteCount > 1 ? 's' : ''}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const layer = didYouMean(match[1], SquadLayers.getLayerNames());
|
||||
|
||||
// check layer is valid
|
||||
if (layer === null) {
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.steamID}" ${match[1]} is not a valid layer name.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rotation.includes(layer)) {
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.steamID}" ${layer} is not in the rotation.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!SquadLayers.isHistoryCompliant(server.layerHistory, layer, options)
|
||||
) {
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.steamID}" ${layer} has been played too recently.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// remove existing votes
|
||||
if (info.steamID in votes) voteCounts[votes[info.steamID]]--;
|
||||
|
||||
// add new vote
|
||||
if (layer in voteCounts) voteCounts[layer]++;
|
||||
else voteCounts[layer] = 1;
|
||||
|
||||
// save what layer they votes for
|
||||
votes[info.steamID] = layer;
|
||||
|
||||
// info them of their vote
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.steamID}" You voted for ${layer}.`
|
||||
);
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.steamID}" Powered by: ${COPYRIGHT_MESSAGE}`
|
||||
);
|
||||
|
||||
// check for new winner
|
||||
const newWinner = getResults()[0].layer;
|
||||
|
||||
if (currentWinner !== newWinner) {
|
||||
server.rcon.execute(`AdminSetNextMap ${newWinner}`);
|
||||
server.rcon.execute(`AdminBroadcast New Map Vote Winner: ${newWinner}`);
|
||||
server.rcon.execute(
|
||||
`AdminBroadcast Participate in the map vote by typing "!mapvote help" in chat.`
|
||||
);
|
||||
currentWinner = newWinner;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
157
plugins/mapvote/mapvote-123.js
Normal file
157
plugins/mapvote/mapvote-123.js
Normal file
@ -0,0 +1,157 @@
|
||||
import SquadLayerFilter from 'connectors/squad-layer-filter';
|
||||
import { COPYRIGHT_MESSAGE } from 'core/config';
|
||||
import { LOG_PARSER_NEW_GAME } from 'squad-server/events/log-parser';
|
||||
import { RCON_CHAT_MESSAGE } from 'squad-server/events/rcon';
|
||||
|
||||
import MapVote from './mapvote.js';
|
||||
|
||||
export default function(server, options = {}) {
|
||||
let mapvote = null;
|
||||
|
||||
options = {
|
||||
minVoteCount: null,
|
||||
...options
|
||||
};
|
||||
|
||||
server.on(LOG_PARSER_NEW_GAME, () => {
|
||||
mapvote = null;
|
||||
});
|
||||
|
||||
server.on(RCON_CHAT_MESSAGE, async info => {
|
||||
const voteMatch = info.message.match(/^([0-9])/);
|
||||
if (voteMatch) {
|
||||
if (!mapvote) return;
|
||||
try {
|
||||
const layerName = await mapvote.makeVoteByNumber(
|
||||
info.steamID,
|
||||
parseInt(voteMatch[1])
|
||||
);
|
||||
await server.rcon.warn(info.steamID, `You voted for ${layerName}.`);
|
||||
} catch (err) {
|
||||
await server.rcon.warn(info.steamID, err.message);
|
||||
}
|
||||
await server.rcon.warn(info.steamID, `Powered by: ${COPYRIGHT_MESSAGE}`);
|
||||
}
|
||||
|
||||
const commandMatch = info.message.match(/^!mapvote ?(.*)/);
|
||||
if (commandMatch) {
|
||||
if (commandMatch[1].startsWith('start')) {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
|
||||
if (mapvote) {
|
||||
await server.rcon.warn(info.steamID, 'A mapvote has already begun.');
|
||||
} else {
|
||||
mapvote = new MapVote(
|
||||
server,
|
||||
SquadLayerFilter.buildFromDidYouMeanList(
|
||||
commandMatch[1].replace('start ', '').split(', ')
|
||||
),
|
||||
{ minVoteCount: options.minVoteCount }
|
||||
);
|
||||
|
||||
mapvote.on('NEW_WINNER', async results => {
|
||||
await server.rcon.broadcast(
|
||||
`New Map Vote Winner: ${results[0].layer.layer}. Participate in the map vote by typing "!mapvote help" in chat.`
|
||||
);
|
||||
});
|
||||
|
||||
await server.rcon.broadcast(
|
||||
`A new map vote has started. Participate in the map vote by typing "!mapvote help" in chat.`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mapvote) {
|
||||
await server.rcon.warn(info.steamID, 'A map vote has not begun.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (commandMatch[1] === 'restart') {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
|
||||
mapvote = new MapVote(server, mapvote.squadLayerFilter, {
|
||||
minVoteCount: options.minVoteCount
|
||||
});
|
||||
|
||||
mapvote.on('NEW_WINNER', async results => {
|
||||
await server.rcon.broadcast(
|
||||
`New Map Vote Winner: ${results[0].layer}. Participate in the map vote by typing "!mapvote help" in chat.`
|
||||
);
|
||||
});
|
||||
|
||||
await server.rcon.broadcast(
|
||||
`A new map vote has started. Participate in the map vote by typing "!mapvote help" in chat.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (commandMatch[1] === 'end') {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
|
||||
const results = mapvote.getResults();
|
||||
|
||||
if (results.length === 0)
|
||||
await server.rcon.broadcast(`No layer gained enough votes to win.`);
|
||||
else
|
||||
await server.rcon.broadcast(
|
||||
`${mapvote.getResults()[0].layer.layer} won the mapvote!`
|
||||
);
|
||||
|
||||
mapvote = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (commandMatch[1] === 'destroy') {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
mapvote = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (commandMatch[1] === 'help') {
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
'To vote type the layer number into chat:'
|
||||
);
|
||||
for (const layer of mapvote.squadLayerFilter.getLayers()) {
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
`${layer.layerNumber} - ${layer.layer}`
|
||||
);
|
||||
}
|
||||
|
||||
if (options.minVoteCount !== null)
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
`${options.minVoteCount} votes need to be made for a winner to be selected.`
|
||||
);
|
||||
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
'To see current results type into chat: !mapvote results'
|
||||
);
|
||||
}
|
||||
|
||||
if (commandMatch[1] === 'results') {
|
||||
const results = mapvote.getResults();
|
||||
|
||||
if (results.length === 0) {
|
||||
await server.rcon.warn(info.steamID, 'No one has voted yet.');
|
||||
} else {
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
'The current vote counts are as follows:'
|
||||
);
|
||||
for (const result of results) {
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
`${result.layer.layerNumber} - ${result.layer.layer} (${
|
||||
result.votes
|
||||
} vote${result.votes > 1 ? 's' : ''})`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
163
plugins/mapvote/mapvote-did-you-mean.js
Normal file
163
plugins/mapvote/mapvote-did-you-mean.js
Normal file
@ -0,0 +1,163 @@
|
||||
import { COPYRIGHT_MESSAGE } from 'core/config';
|
||||
import { LOG_PARSER_NEW_GAME } from 'squad-server/events/log-parser';
|
||||
import { RCON_CHAT_MESSAGE } from 'squad-server/events/rcon';
|
||||
|
||||
import MapVote from './mapvote.js';
|
||||
|
||||
export default function(server, squadLayerFilter, options = {}) {
|
||||
options = {
|
||||
alwaysOn: true,
|
||||
minPlayerCount: null,
|
||||
minVoteCount: null,
|
||||
...options
|
||||
};
|
||||
|
||||
let mapvote;
|
||||
let manuallyCreated;
|
||||
|
||||
async function newMapvote(manuallyCreatedOption = true) {
|
||||
mapvote = new MapVote(server, squadLayerFilter, {
|
||||
minVoteCount: options.minVoteCount
|
||||
});
|
||||
|
||||
manuallyCreated = manuallyCreatedOption;
|
||||
|
||||
mapvote.on('NEW_WINNER', async results => {
|
||||
await server.rcon.broadcast(
|
||||
`New Map Vote Winner: ${results[0].layer.layer}. Participate in the map vote by typing "!mapvote help" in chat.`
|
||||
);
|
||||
});
|
||||
|
||||
if (manuallyCreated)
|
||||
await server.rcon.broadcast(
|
||||
`A new map vote has started. Participate in the map vote by typing "!mapvote help" in chat.`
|
||||
);
|
||||
}
|
||||
|
||||
if (options.alwaysOn) newMapvote(false);
|
||||
|
||||
server.on(LOG_PARSER_NEW_GAME, () => {
|
||||
if (options.alwaysOn) {
|
||||
newMapvote(false);
|
||||
} else {
|
||||
mapvote = null;
|
||||
}
|
||||
});
|
||||
|
||||
server.on(RCON_CHAT_MESSAGE, async info => {
|
||||
const match = info.message.match(/^!mapvote ?(.*)/);
|
||||
if (!match) return;
|
||||
|
||||
if (match[1] === 'help') {
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
'You may use any of the following commands in chat:'
|
||||
);
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
'!mapvote results - View the current vote counts.'
|
||||
);
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
'!mapvote <layer name> - Vote for the specified layer.'
|
||||
);
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
'When inputting a layer name, we autocorrect any miss spelling.'
|
||||
);
|
||||
|
||||
if (options.minVoteCount !== null)
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
`${options.minVoteCount} votes need to be made for a winner to be selected.`
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (match[1] === 'start') {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
|
||||
if (mapvote) {
|
||||
await server.rcon.warn(info.steamID, 'A mapvote has already begun.');
|
||||
} else {
|
||||
await newMapvote();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mapvote) {
|
||||
await server.rcon.warn(info.steamID, 'A map vote has not begun.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (match[1] === 'restart') {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
await newMapvote();
|
||||
return;
|
||||
}
|
||||
|
||||
if (match[1] === 'end') {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
|
||||
const results = mapvote.getResults(true);
|
||||
|
||||
if (results.length === 0)
|
||||
await server.rcon.broadcast(`No layer gained enough votes to win.`);
|
||||
else
|
||||
await server.rcon.broadcast(
|
||||
`${mapvote.getResults()[0].layer.layer} won the mapvote!`
|
||||
);
|
||||
|
||||
mapvote = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (match[1] === 'destroy') {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
mapvote = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (match[1] === 'results') {
|
||||
const results = mapvote.getResults();
|
||||
|
||||
if (results.length === 0) {
|
||||
await server.rcon.warn(info.steamID, 'No one has voted yet.');
|
||||
} else {
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
'The current vote counts are as follows:'
|
||||
);
|
||||
for (const result of results) {
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
`${result.layer.layer} - ${result.votes} vote${
|
||||
result.votes > 1 ? 's' : ''
|
||||
}`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!manuallyCreated && server.players.length < options.minPlayerCount) {
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
'Not enough players online to vote.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const layerName = await mapvote.makeVoteByDidYouMean(
|
||||
info.steamID,
|
||||
match[1]
|
||||
);
|
||||
await server.rcon.warn(info.steamID, `You voted for ${layerName}.`);
|
||||
} catch (err) {
|
||||
await server.rcon.warn(info.steamID, err.message);
|
||||
}
|
||||
await server.rcon.warn(info.steamID, `Powered by: ${COPYRIGHT_MESSAGE}`);
|
||||
});
|
||||
}
|
102
plugins/mapvote/mapvote.js
Normal file
102
plugins/mapvote/mapvote.js
Normal file
@ -0,0 +1,102 @@
|
||||
import EventEmitter from 'events';
|
||||
|
||||
import SquadLayers from 'connectors/squad-layers';
|
||||
|
||||
export default class MapVote extends EventEmitter {
|
||||
constructor(server, squadLayerFilter, options = {}) {
|
||||
super();
|
||||
this.server = server;
|
||||
this.squadLayerFilter = squadLayerFilter;
|
||||
|
||||
this.layerVotes = {};
|
||||
this.playerVotes = {};
|
||||
this.currentWinner = null;
|
||||
|
||||
this.minVoteCount = options.minVoteCount || null;
|
||||
}
|
||||
|
||||
addVote(identifier, layerName) {
|
||||
if (this.layerVotes[layerName]) {
|
||||
this.layerVotes[layerName] += 1;
|
||||
} else {
|
||||
this.layerVotes[layerName] = 1;
|
||||
}
|
||||
this.playerVotes[identifier] = layerName;
|
||||
}
|
||||
|
||||
removeVote(identifier) {
|
||||
if (!this.playerVotes[identifier]) return;
|
||||
|
||||
if (this.layerVotes[this.playerVotes[identifier]])
|
||||
this.layerVotes[this.playerVotes[identifier]] -= 1;
|
||||
if (this.layerVotes[this.playerVotes[identifier]] === 0)
|
||||
delete this.layerVotes[this.playerVotes[identifier]];
|
||||
|
||||
delete this.playerVotes[identifier];
|
||||
}
|
||||
|
||||
getResults(applyMinVoteCount = false) {
|
||||
if (
|
||||
!applyMinVoteCount ||
|
||||
this.minVoteCount === null ||
|
||||
Object.keys(this.playerVotes).length >= this.minVoteCount
|
||||
) {
|
||||
return Object.keys(this.layerVotes)
|
||||
.map(layerName => ({
|
||||
layer: this.squadLayerFilter.getLayerByLayerName(layerName),
|
||||
votes: this.layerVotes[layerName]
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
if (a.votes > b.votes) return -1;
|
||||
if (a.votes < b.votes) return 1;
|
||||
else return Math.random() < 0.5 ? 1 : -1;
|
||||
});
|
||||
} else return [];
|
||||
}
|
||||
|
||||
async makeVote(identifier, layer) {
|
||||
layer = SquadLayers.getLayerByLayerName(layer);
|
||||
|
||||
if (!this.squadLayerFilter.inLayerPool(layer))
|
||||
throw new Error(`${layer.layer} is not in layer pool.`);
|
||||
|
||||
if (!this.squadLayerFilter.isLayerHistoryCompliant(this.server, layer))
|
||||
throw new Error(`${layer.layer} was played too recently.`);
|
||||
if (!this.squadLayerFilter.isMapHistoryCompliant(this.server, layer))
|
||||
throw new Error(`${layer.map} was played too recently.`);
|
||||
if (!this.squadLayerFilter.isGamemodeHistoryCompliant(this.server, layer))
|
||||
throw new Error(`${layer.gamemode} was played too recently.`);
|
||||
if (!this.squadLayerFilter.isPlayerCountCompliant(this.server, layer))
|
||||
throw new Error(
|
||||
`${layer.layer} is only suitable for a player count between ${layer.estimatedSuitablePlayerCount.min} and ${layer.estimatedSuitablePlayerCount.max}.`
|
||||
);
|
||||
|
||||
this.removeVote(identifier);
|
||||
this.addVote(identifier, layer.layer);
|
||||
|
||||
const results = this.getResults(true);
|
||||
|
||||
if (results.length > 0) {
|
||||
if (results[0] !== this.currentWinner) {
|
||||
await this.server.rcon.execute(
|
||||
`AdminSetNextMap ${results[0].layer.layer}`
|
||||
);
|
||||
this.emit('NEW_WINNER', results);
|
||||
this.currentWinner = results[0];
|
||||
}
|
||||
}
|
||||
|
||||
return layer.layer;
|
||||
}
|
||||
|
||||
async makeVoteByDidYouMean(identifier, layerName) {
|
||||
const layer = SquadLayers.getLayerByDidYouMean(layerName);
|
||||
if (layer === null) throw new Error(`${layerName} is not a Squad layer.`);
|
||||
return this.makeVote(identifier, layer.layer);
|
||||
}
|
||||
|
||||
async makeVoteByNumber(identifier, number) {
|
||||
const layer = this.squadLayerFilter.getLayerByNumber(number);
|
||||
return this.makeVote(identifier, layer.layer);
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@
|
||||
"connectors": "1.0.0",
|
||||
"didyoumean": "^1.2.1",
|
||||
"influx": "^5.5.1",
|
||||
"squad-server": "1.0.0"
|
||||
"squad-server": "1.0.0",
|
||||
"tinygradient": "^1.1.2"
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ const LOG_PARSER_PLAYER_DAMAGED = 'LOG_PARSER_PLAYER_DAMAGED';
|
||||
* - attackerPlayerController - PlayerController of the attacking player.
|
||||
* - weapon - The classname of the weapon used.
|
||||
* - teamkill - Whether the kill was a teamkill.
|
||||
* - suicide - Was the kill a suicide.
|
||||
*/
|
||||
const LOG_PARSER_PLAYER_DIED = 'LOG_PARSER_PLAYER_DIED';
|
||||
|
||||
@ -63,6 +64,7 @@ const LOG_PARSER_PLAYER_POSSESS = 'LOG_PARSER_PLAYER_POSSESS';
|
||||
* - attackerPlayerController - PlayerController of the attacking player.
|
||||
* - weapon - The classname of the weapon used.
|
||||
* - teamkill - Whether the kill was a teamkill.
|
||||
* - suicide - Was the kill a suicide.
|
||||
* - reviver - PlayerObject of the reviving player.
|
||||
*/
|
||||
const LOG_PARSER_PLAYER_REVIVED = 'LOG_PARSER_PLAYER_REVIVED';
|
||||
@ -86,6 +88,7 @@ const LOG_PARSER_PLAYER_UNPOSSESS = 'LOG_PARSER_PLAYER_UNPOSSESS';
|
||||
* - attackerPlayerController - PlayerController of the attacking player.
|
||||
* - weapon - The classname of the weapon used.
|
||||
* - teamkill - Whether the kill was a teamkill.
|
||||
* - suicide - Was the kill a suicide.
|
||||
*/
|
||||
const LOG_PARSER_TEAMKILL = 'LOG_PARSER_TEAMKILL';
|
||||
|
||||
@ -99,6 +102,7 @@ const LOG_PARSER_TEAMKILL = 'LOG_PARSER_TEAMKILL';
|
||||
* - attackerPlayerController - PlayerController of the attacking player.
|
||||
* - weapon - The classname of the weapon used.
|
||||
* - teamkill - Whether the kill was a teamkill.
|
||||
* - suicide - Was the kill a suicide.
|
||||
*/
|
||||
const LOG_PARSER_PLAYER_WOUNDED = 'LOG_PARSER_PLAYER_WOUNDED';
|
||||
|
||||
|
@ -14,6 +14,7 @@ export default {
|
||||
};
|
||||
|
||||
data.teamkill = data.victim.teamID === data.attacker.teamID;
|
||||
data.suicide = data.victim.steamID === data.attacker.steamID;
|
||||
|
||||
logParser.eventStore[args[3]] = data;
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
import {
|
||||
LOG_PARSER_PLAYER_POSSESS
|
||||
} from '../../events/log-parser.js';
|
||||
import { LOG_PARSER_PLAYER_POSSESS } from '../../events/log-parser.js';
|
||||
|
||||
export default {
|
||||
regex: /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQPlayerController::)?OnPossess\(\): PC=(.+) Pawn=([A-z0-9_]+)_C/,
|
||||
|
@ -9,7 +9,7 @@ export default {
|
||||
raw: args[0],
|
||||
time: args[1],
|
||||
chainID: args[2],
|
||||
victim: await logParser.server.getPlayerByName(args[5]),
|
||||
victim: await logParser.server.getPlayerByName(args[4]),
|
||||
reviver: await logParser.server.getPlayerByName(args[3])
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
import {
|
||||
LOG_PARSER_PLAYER_UNPOSSESS
|
||||
} from '../../events/log-parser.js';
|
||||
import { LOG_PARSER_PLAYER_UNPOSSESS } from '../../events/log-parser.js';
|
||||
|
||||
export default {
|
||||
regex: /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQPlayerController::)?OnUnPossess\(\): PC=(.+)/,
|
||||
@ -13,7 +11,11 @@ export default {
|
||||
switchPossess: false
|
||||
};
|
||||
|
||||
if(args[3] in logParser.eventStore && logParser.eventStore[args[3]] === args[2]) data.switchPossess = true;
|
||||
if (
|
||||
args[3] in logParser.eventStore &&
|
||||
logParser.eventStore[args[3]] === args[2]
|
||||
)
|
||||
data.switchPossess = true;
|
||||
delete logParser.eventStore[args[3]];
|
||||
|
||||
logParser.server.emit(LOG_PARSER_PLAYER_UNPOSSESS, data);
|
||||
|
@ -57,9 +57,7 @@ export default class Rcon {
|
||||
|
||||
async getMapInfo() {
|
||||
const response = await this.execute('ShowNextMap');
|
||||
const match = response.match(
|
||||
/^Current map is (.+), Next map is (.*)/
|
||||
);
|
||||
const match = response.match(/^Current map is (.+), Next map is (.*)/);
|
||||
return {
|
||||
currentLayer: match[1],
|
||||
nextLayer: match[2].length === 0 ? null : match[2]
|
||||
@ -89,6 +87,14 @@ export default class Rcon {
|
||||
return players;
|
||||
}
|
||||
|
||||
async broadcast(message) {
|
||||
await this.execute(`AdminBroadcast ${message}`);
|
||||
}
|
||||
|
||||
async warn(steamID, message) {
|
||||
await this.execute(`AdminWarn "${steamID}" ${message}`);
|
||||
}
|
||||
|
||||
/* Core socket functionality */
|
||||
connect() {
|
||||
this.verbose('Method Exec: connect()');
|
||||
@ -166,7 +172,7 @@ export default class Rcon {
|
||||
this.client.once('close', onClose);
|
||||
this.client.once('error', onError);
|
||||
|
||||
this.client.disconnect();
|
||||
this.client.end();
|
||||
});
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user