Merge pull request #22 from Thomas-Smyth/alpha

SquadJS v1.0.10 Release
This commit is contained in:
Thomas Smyth 2020-06-13 15:38:22 +01:00 committed by GitHub
commit 9bc60d7ee4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 979 additions and 590 deletions

View File

@ -1,3 +1,4 @@
{
"singleQuote": true
"singleQuote": true,
"printWidth": 80
}

View File

@ -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"
}
}

View 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
);
}
}

View 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();

View File

@ -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();

View File

@ -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

View File

@ -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",

View File

@ -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!'}`
);
});
}

View File

@ -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`
}
],

View File

@ -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.'

View File

@ -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.'

View File

@ -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
}

View File

@ -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' }
);
});
}

View File

@ -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
}
);
```

View File

@ -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: {

View File

@ -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 = [];

View File

@ -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
}
);
```

View File

@ -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;
}
}
});
}

View 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' : ''})`
);
}
}
}
}
});
}

View 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
View 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);
}
}

View File

@ -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"
}
}

View File

@ -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';

View File

@ -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;

View File

@ -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/,

View File

@ -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])
};

View File

@ -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);

View File

@ -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();
});
}