Initial Squad v2 Layer Support

This commit is contained in:
Thomas Smyth 2021-02-03 17:33:01 +00:00
parent 69096b00af
commit b5a3f4c59b
10 changed files with 133 additions and 339 deletions

View File

@ -35,9 +35,6 @@ export default class SquadServerFactory {
Logger.verbose('SquadServerFactory', 1, 'Creating SquadServer...');
const server = new SquadServer(config.server);
// pull layers read to use to create layer filter connectors
await server.squadLayers.pull();
// initialise connectors
Logger.verbose('SquadServerFactory', 1, 'Preparing connectors...');
const connectors = {};
@ -102,13 +99,6 @@ export default class SquadServerFactory {
static async createConnector(server, type, connectorName, connectorConfig) {
Logger.verbose('SquadServerFactory', 1, `Starting ${type} connector ${connectorName}...`);
if (type === 'squadlayerpool') {
return server.squadLayers[connectorConfig.type](
connectorConfig.filter,
connectorConfig.activeLayerFilter
);
}
if (type === 'discord') {
const connector = new Discord.Client();
await connector.login(connectorConfig);

View File

@ -6,11 +6,12 @@ import Gamedig from 'gamedig';
import Logger from 'core/logger';
import { SQUADJS_API_DOMAIN } from 'core/constants';
import { Layers } from './layers/index.js';
import LogParser from './log-parser/index.js';
import Rcon from './rcon.js';
import { SQUADJS_VERSION } from './utils/constants.js';
import { SquadLayers } from './utils/squad-layers.js';
import fetchAdminLists from './utils/admin-lists.js';
@ -33,8 +34,6 @@ export default class SquadServer extends EventEmitter {
this.plugins = [];
this.squadLayers = new SquadLayers(options.squadLayersSource);
this.setupRCON();
this.setupLogParser();
@ -61,7 +60,9 @@ export default class SquadServer extends EventEmitter {
1,
`Beginning to watch ${this.options.host}:${this.options.queryPort}...`
);
await this.squadLayers.pull();
await Layers.pull();
this.admins = await fetchAdminLists(this.options.adminLists);
await this.rcon.connect();
@ -131,12 +132,13 @@ export default class SquadServer extends EventEmitter {
this.emit('ADMIN_BROADCAST', data);
});
this.logParser.on('NEW_GAME', (data) => {
data.layer = this.squadLayers.getLayerByLayerClassname(data.layerClassname);
this.logParser.on('NEW_GAME', async (data) => {
data.layer = await Layers.getLayerByClassname(data.layerClassname);
this.layerHistory.unshift({ ...data.layer, time: data.time });
this.layerHistory.unshift({ layer: data.layer, time: data.time });
this.layerHistory = this.layerHistory.slice(0, this.layerHistoryMaxLength);
this.currentLayer = data.layer;
this.emit('NEW_GAME', data);
});
@ -301,16 +303,21 @@ export default class SquadServer extends EventEmitter {
Logger.verbose('SquadServer', 1, `Updating layer information...`);
try {
const layerInfo = await this.rcon.getLayerInfo();
const currentMap = await this.rcon.getCurrentMap();
const nextMap = await this.rcon.getNextMap();
const nextMapToBeVoted = nextMap === 'To be voted';
const currentLayer = await Layers.getLayerByName(currentMap.layer);
const nextLayer = nextMapToBeVoted ? null : await Layers.getLayerByName(nextMap.layer);
if (this.layerHistory.length === 0) {
const layer = this.squadLayers.getLayerByLayerName(layerInfo.currentLayer);
this.layerHistory.unshift({ ...layer, time: Date.now() });
this.layerHistory.unshift({ layer: currentLayer, time: Date.now() });
this.layerHistory = this.layerHistory.slice(0, this.layerHistoryMaxLength);
}
this.nextLayer = layerInfo.nextLayer;
this.currentLayer = currentLayer;
this.nextLayer = nextLayer;
this.nextLayerToBeVoted = nextMapToBeVoted;
this.emit('UPDATED_LAYER_INFORMATION');
} catch (err) {

View File

@ -0,0 +1,4 @@
import Layer from './layer.js';
import Layers from './layers.js';
export { Layer, Layers };

View File

@ -0,0 +1,47 @@
export default class Layer {
constructor(data) {
this.name = data.Name;
this.classname = data.rawName;
this.map = {
name: data.mapName
};
this.gamemode = data.gamemode;
this.gamemodeType = data.type;
this.version = data.layerVersion;
this.size = data.mapSize;
this.sizeType = data.mapSizeType;
this.numberOfCapturePoints = parseInt(data.capturePoints);
this.lighting = {
name: data.lighting,
classname: data.lightingLevel
};
this.teams = [
{
faction: data.team1.faction,
name: data.team1.teamSetupName,
tickets: data.team1.tickets,
commander: data.team1.commander,
vehicles: (data.team1.vehicles || []).map((vehicle) => ({
name: vehicle.type,
classname: vehicle.rawType,
count: vehicle.count,
spawnDelay: vehicle.delay,
respawnDelay: vehicle.respawnTime
}))
},
{
faction: data.team2.faction,
name: data.team2.teamSetupName,
tickets: data.team2.tickets,
commander: data.team2.commander,
vehicles: (data.team2.vehicles || []).map((vehicle) => ({
name: vehicle.type,
classname: vehicle.rawType,
count: vehicle.count,
spawnDelay: vehicle.delay,
respawnDelay: vehicle.respawnTime
}))
}
];
}
}

View File

@ -0,0 +1,44 @@
import axios from 'axios';
import Layer from './layer.js';
class Layers {
constructor() {
this.layers = [];
this.pulled = false;
}
async pull(force = false) {
if (this.pulled && !force) return;
const response = await axios.get(
'https://raw.githubusercontent.com/Squad-Wiki-Editorial/squad-wiki-pipeline-map-data/dev/completed_output/2.0/finished_2.0.json'
);
for (const layer of response.data.Maps) {
this.layers.push(new Layer(layer));
}
return this.layers;
}
async getLayerByCondition(condition) {
await this.pull();
const matches = this.layers.filter(condition);
if (matches.length === 1) return matches[0];
return null;
}
getLayerByName(name) {
return this.getLayerByCondition((layer) => layer.name === name);
}
getLayerByClassname(classname) {
return this.getLayerByCondition((layer) => layer.classname === classname);
}
}
export default new Layers();

View File

@ -48,7 +48,7 @@ export default class DiscordRoundWinner extends DiscordBasePlugin {
fields: [
{
name: 'Message',
value: `${info.winner} won on ${info.layer}.`
value: `${info.winner} won on ${info.layer.name}.`
}
],
timestamp: info.time.toISOString()

View File

@ -92,12 +92,14 @@ export default class DiscordServerStatus extends BasePlugin {
},
{
name: 'Current Layer',
value: `\`\`\`${this.server.layerHistory[0].layer || 'Unknown'}\`\`\``,
value: `\`\`\`${this.server.currentLayer.name || 'Unknown'}\`\`\``,
inline: true
},
{
name: 'Next Layer',
value: `\`\`\`${this.server.nextLayer || 'Unknown'}\`\`\``,
value: `\`\`\`${
this.server.nextLayer.name || (this.server.nextLayerToBeVoted ? 'To be voted' : 'Unknown')
}\`\`\``,
inline: true
}
];

View File

@ -16,14 +16,19 @@ export default class SquadRcon extends Rcon {
});
}
async broadcast(message) {
await this.execute(`AdminBroadcast ${message}`);
async getCurrentMap() {
const response = await this.execute('ShowCurrentMap');
const match = response.match(/^Current level is (.*), layer is (.*)/);
return { level: match[1], layer: match[2] };
}
async getLayerInfo() {
async getNextMap() {
const response = await this.execute('ShowNextMap');
const match = response.match(/^Current map is (.+), Next map is (.*)/);
return { currentLayer: match[1], nextLayer: match[2].length === 0 ? null : match[2] };
const match = response.match(/^Next level is (.*), layer is (.*)/);
return {
level: match[1] !== '' ? match[1] : null,
layer: match[2] !== 'To be voted' ? match[2] : null
};
}
async getListPlayers() {
@ -49,6 +54,10 @@ export default class SquadRcon extends Rcon {
return players;
}
async broadcast(message) {
await this.execute(`AdminBroadcast ${message}`);
}
async warn(steamID, message) {
await this.execute(`AdminWarn "${steamID}" ${message}`);
}

View File

@ -22,41 +22,6 @@
},
"connectors": {
"discord": "Discord Login Token",
"squadlayerpool": {
"type": "buildPoolFromFilter",
"filter": {
"whitelistedLayers": null,
"blacklistedLayers": null,
"whitelistedMaps": null,
"blacklistedMaps": null,
"whitelistedGamemodes": null,
"blacklistedGamemodes": ["Training"],
"flagCountMin": null,
"flagCountMax": null,
"hasCommander": null,
"hasTanks": null,
"hasHelicopters": null
},
"activeLayerFilter": {
"historyResetTime": 18000000,
"layerHistoryTolerance": 8,
"mapHistoryTolerance": 4,
"gamemodeHistoryTolerance": {
"Invasion": 4
},
"gamemodeRepetitiveTolerance": {
"Invasion": 4
},
"playerCountComplianceEnabled": true,
"factionComplianceEnabled": true,
"factionHistoryTolerance": {
"RUS": 4
},
"factionRepetitiveTolerance": {
"RUS": 4
}
}
},
"mysql": {
"host": "host",
"port": 3306,

View File

@ -1,274 +0,0 @@
import axios from 'axios';
import didYouMean from 'didyoumean';
import fs from 'fs';
class SquadLayersBase {
get layerNames() {
return this.layers.map((layer) => layer.name);
}
getLayerByCondition(condition) {
const results = this.layers.filter(condition);
return results.length === 1 ? results[0] : null;
}
getLayerByLayerName(layerName) {
return this.getLayerByCondition((layer) => layer.layer === layerName);
}
getLayerByLayerClassname(layerClassname) {
return this.getLayerByCondition((layer) => layer.layerClassname === layerClassname);
}
getLayerByLayerNameAutoCorrection(layerName) {
return this.getLayerByLayerName(didYouMean(layerName, this.layerNames()));
}
getLayerByNumber(layerNumber) {
return this.getLayerByCondition((layer) => layer.layerNumber === layerNumber);
}
}
class SquadLayers extends SquadLayersBase {
constructor(source) {
super();
this.source =
source || 'https://raw.githubusercontent.com/Thomas-Smyth/squad-layers/master/layers.json';
this.pulled = false;
}
async pull(force = false) {
if (this.pulled && !force) return;
this.layers = (await axios.get(this.source)).data;
for (let i = 0; i < this.layers.length; i++) this.layers[i].layerNumber = i + 1;
}
buildPoolFromLayerNames(layerNames, activeFilter) {
return new SquadLayersPool(
this.layers.filter((layer) => layerNames.includes(layer.layer)),
activeFilter
);
}
buildPoolFromLayerNamesAutoCorrection(layerNames, activeFilter) {
return this.buildPoolFromLayerNames(
layerNames.map((layerName) => this.getLayerByLayerNameAutoCorrection(layerName)),
activeFilter
);
}
buildPoolFromFile(path, activeFilter, delimiter = '\n') {
return this.buildPoolFromLayerNames(
fs.readFileSync(path, 'utf8').split(delimiter),
activeFilter
);
}
buildPoolFromFilter(filter, activeFilter) {
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);
}
return new SquadLayersPool(layers, activeFilter);
}
}
class SquadLayersPool extends SquadLayersBase {
constructor(layers, activeFilter = null) {
super();
this.layers = layers;
for (let i = 0; i < this.layers.length; i++) this.layers[i].layerNumber = i + 1;
this.activeFilter = activeFilter;
}
inPool(layer) {
if (typeof layer === 'object') layer = layer.layer;
return super.layerNames.includes(layer);
}
isHistoryCompliant(layerHistory, layer) {
if (this.activeFilter === null) return true;
if (typeof layer === 'object') layer = layer.layer;
for (
let i = 0;
i < Math.min(layerHistory.length, this.activeFilter.layerHistoryTolerance);
i++
) {
if (new Date() - layerHistory[i].time > this.activeFilter.historyResetTime) return true;
if (layerHistory[i].layer === layer) return false;
}
return true;
}
isMapHistoryCompliant(layerHistory, layer) {
if (this.activeFilter === null) return true;
if (typeof layer === 'string') layer = this.getLayerByLayerName(layer);
for (let i = 0; i < Math.min(layerHistory.length, this.activeFilter.mapHistoryTolerance); i++) {
if (new Date() - layerHistory[i].time > this.activeFilter.historyResetTime) return true;
if (layerHistory[i].map === layer.map) return false;
}
return true;
}
isGamemodeHistoryCompliant(layerHistory, layer) {
if (this.activeFilter === null) return true;
if (typeof layer === 'string') layer = this.getLayerByLayerName(layer);
const gamemodeHistoryTolerance = this.activeFilter.gamemodeHistoryTolerance[layer.gamemode];
if (!gamemodeHistoryTolerance) return true;
for (let i = 0; i < Math.min(layerHistory.length, gamemodeHistoryTolerance); i++) {
if (new Date() - layerHistory[i].time > this.activeFilter.historyResetTime) return true;
if (layerHistory[i].gamemode === layer.gamemode) return false;
}
return true;
}
isGamemodeRepetitiveCompliant(layerHistory, layer) {
if (this.activeFilter === null) return true;
if (typeof layer === 'string') layer = this.getLayerByLayerName(layer);
const gamemodeRepetitiveTolerance = this.activeFilter.gamemodeRepetitiveTolerance[
layer.gamemode
];
if (!gamemodeRepetitiveTolerance) return true;
for (let i = 0; i < Math.min(layerHistory.length, gamemodeRepetitiveTolerance); i++) {
if (new Date() - layerHistory[i].time > this.activeFilter.historyResetTime) return true;
if (layerHistory[i].gamemode.gamemode !== layer.gamemode) return true;
}
return false;
}
isFactionCompliant(layerHistory, layer) {
if (this.activeFilter === null || this.activeFilter.factionComplianceEnabled === false)
return true;
if (layerHistory.length === 0) return true;
if (typeof layer === 'string') layer = this.getLayerByLayerName(layer);
return (
!layerHistory[0] ||
(layerHistory[0].teamOne.faction !== layer.teamTwo.faction &&
layerHistory[0].teamTwo.faction !== layer.teamOne.faction)
);
}
isFactionHistoryCompliant(layerHistory, layer, faction = null) {
if (this.activeFilter === null) return true;
if (typeof layer === 'string') layer = SquadLayers.getLayerByLayerName(layer);
if (faction === null) {
return (
this.isFactionHistoryCompliant(layerHistory, layer, layer.teamOne.faction) &&
this.isFactionHistoryCompliant(layerHistory, layer, layer.teamTwo.faction)
);
} else {
const factionThreshold = this.activeFilter.factionHistoryTolerance[faction];
if (!factionThreshold) return true;
for (let i = 0; i < Math.min(layerHistory.length, factionThreshold); i++) {
if (new Date() - layerHistory[i].time > this.activeFilter.historyResetTime) return true;
if (
layerHistory[i].teamOne.faction === faction ||
layerHistory[i].teamTwo.faction === faction
)
return false;
}
return true;
}
}
isFactionRepetitiveCompliant(layerHistory, layer, faction = null) {
if (this.activeFilter === null) return true;
if (typeof layer === 'string') layer = SquadLayers.getLayerByLayerName(layer);
if (faction === null) {
return (
this.isFactionRepetitiveCompliant(layerHistory, layer, layer.teamOne.faction) &&
this.isFactionRepetitiveCompliant(layerHistory, layer, layer.teamTwo.faction)
);
} else {
const factionThreshold = this.activeFilter.factionRepetitiveTolerance[faction];
if (!factionThreshold) return true;
for (let i = 0; i < Math.min(layerHistory.length, factionThreshold); i++) {
if (new Date() - layerHistory[i].time > this.activeFilter.historyResetTime) return true;
if (
layerHistory[i].teamOne.faction !== faction &&
layerHistory[i].teamTwo.faction !== faction
)
return true;
}
return false;
}
}
isPlayerCountCompliant(server, layer) {
if (this.activeFilter === null || this.activeFilter.playerCountComplianceEnabled === false)
return true;
if (typeof layer === 'string') layer = this.getLayerByLayerName(layer);
return !(
server.players.length > layer.estimatedSuitablePlayerCount.max ||
server.players.length < layer.estimatedSuitablePlayerCount.min
);
}
}
export { SquadLayers, SquadLayersPool };