mirror of
https://github.com/AsgardEternal/SquadJS.git
synced 2024-09-28 08:24:23 -05:00
Merge branch 'master' into multiconfig
# Conflicts: # .gitignore # squad-server/rcon.js
This commit is contained in:
commit
7117459599
2
.gitignore
vendored
2
.gitignore
vendored
@ -12,6 +12,8 @@ yarn.lock
|
||||
# IDEs
|
||||
.idea/
|
||||
.vs/
|
||||
/squad-server/plugins/db-log-addOn.js
|
||||
/squad-server/plugins/mapvote.js
|
||||
|
||||
config.json
|
||||
config.*.json
|
||||
|
60
README.md
60
README.md
@ -426,6 +426,43 @@ Interested in creating your own plugin? [See more here](./squad-server/plugins/r
|
||||
]</code></pre></li></ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>DBLogPlayerTime</summary>
|
||||
<h2>DBLogPlayerTime</h2>
|
||||
<p>replacement add-on to dblog for player join/seeding times</p>
|
||||
<h3>Options</h3>
|
||||
<ul><li><h4>database (Required)</h4>
|
||||
<h6>Description</h6>
|
||||
<p>The Sequelize connector to log server information to.</p>
|
||||
<h6>Default</h6>
|
||||
<pre><code>mysql</code></pre></li>
|
||||
<li><h4>overrideServerID</h4>
|
||||
<h6>Description</h6>
|
||||
<p>A overridden server ID.</p>
|
||||
<h6>Default</h6>
|
||||
<pre><code>null</code></pre></li>
|
||||
<li><h4>seedingThreshold</h4>
|
||||
<h6>Description</h6>
|
||||
<p>seeding Threshold.</p>
|
||||
<h6>Default</h6>
|
||||
<pre><code>50</code></pre></li>
|
||||
<li><h4>whitelistfilepath</h4>
|
||||
<h6>Description</h6>
|
||||
<p>path to a file to write out auto-wl</p>
|
||||
<h6>Default</h6>
|
||||
<pre><code>null</code></pre></li>
|
||||
<li><h4>incseed</h4>
|
||||
<h6>Description</h6>
|
||||
<p>rate of increase as a percentage to whitelist</p>
|
||||
<h6>Default</h6>
|
||||
<pre><code>0</code></pre></li>
|
||||
<li><h4>decseed</h4>
|
||||
<h6>Description</h6>
|
||||
<p>rate of decrease as a percentage to whitelist</p>
|
||||
<h6>Default</h6>
|
||||
<pre><code>0</code></pre></li></ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>DBLog</summary>
|
||||
<h2>DBLog</h2>
|
||||
@ -601,6 +638,29 @@ Grafana:
|
||||
]</code></pre></li></ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>DiscordCheater</summary>
|
||||
<h2>DiscordCheater</h2>
|
||||
<p>The <code>DiscordCheater</code> plugin will send any suspected cheating to a Discord channel.</p>
|
||||
<h3>Options</h3>
|
||||
<ul><li><h4>discordClient (Required)</h4>
|
||||
<h6>Description</h6>
|
||||
<p>Discord connector name.</p>
|
||||
<h6>Default</h6>
|
||||
<pre><code>discord</code></pre></li>
|
||||
<li><h4>channelID (Required)</h4>
|
||||
<h6>Description</h6>
|
||||
<p>The ID of the channel to log admin broadcasts to.</p>
|
||||
<h6>Default</h6>
|
||||
<pre><code></code></pre></li><h6>Example</h6>
|
||||
<pre><code>667741905228136459</code></pre>
|
||||
<li><h4>color</h4>
|
||||
<h6>Description</h6>
|
||||
<p>The color of the embed.</p>
|
||||
<h6>Default</h6>
|
||||
<pre><code>16711680</code></pre></li></ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>DiscordDebug</summary>
|
||||
<h2>DiscordDebug</h2>
|
||||
|
@ -77,6 +77,16 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"plugin": "DBLogPlayerTime",
|
||||
"enabled": false,
|
||||
"database": "mysql",
|
||||
"overrideServerID": null,
|
||||
"seedingThreshold": 50,
|
||||
"whitelistfilepath": null,
|
||||
"incseed": 0,
|
||||
"decseed": 0
|
||||
},
|
||||
{
|
||||
"plugin": "DBLog",
|
||||
"enabled": false,
|
||||
@ -122,6 +132,13 @@
|
||||
"ChatSquad"
|
||||
]
|
||||
},
|
||||
{
|
||||
"plugin": "DiscordCheater",
|
||||
"enabled": true,
|
||||
"discordClient": "discord",
|
||||
"channelID": "",
|
||||
"color": 16711680
|
||||
},
|
||||
{
|
||||
"plugin": "DiscordDebug",
|
||||
"enabled": false,
|
||||
|
@ -105,12 +105,16 @@ export default class SquadServerFactory {
|
||||
Logger.verbose('SquadServerFactory', 1, `Starting ${type} connector ${connectorName}...`);
|
||||
|
||||
if (type === 'discord') {
|
||||
Logger.verbose('SquadServerFactory', 1, `Connector is Discord Type`);
|
||||
const connector = new Discord.Client();
|
||||
Logger.verbose('SquadServerFactory', 1, `Connector created Discord client`);
|
||||
await connector.login(connectorConfig);
|
||||
Logger.verbose('SquadServerFactory', 1, `Connector Logged into Discord`);
|
||||
return connector;
|
||||
}
|
||||
|
||||
if (type === 'sequelize') {
|
||||
Logger.verbose('SquadServerFactory', 1, `Connector is SQL Type`);
|
||||
let connector;
|
||||
|
||||
if (typeof connectorConfig === 'string') {
|
||||
@ -129,8 +133,10 @@ export default class SquadServerFactory {
|
||||
} else {
|
||||
throw new Error('Unknown sequelize connector config type.');
|
||||
}
|
||||
Logger.verbose('SquadServerFactory', 1, `Connector created SQL client`);
|
||||
|
||||
await connector.authenticate();
|
||||
Logger.verbose('SquadServerFactory', 1, `Connector Logged into SQL`);
|
||||
return connector;
|
||||
}
|
||||
|
||||
|
@ -73,6 +73,7 @@ export default class SquadServer extends EventEmitter {
|
||||
this.admins = await fetchAdminLists(this.options.adminLists);
|
||||
|
||||
await this.rcon.connect();
|
||||
await this.updateLayerList();
|
||||
await this.logParser.watch();
|
||||
|
||||
await this.updateSquadList();
|
||||
@ -206,20 +207,46 @@ export default class SquadServer extends EventEmitter {
|
||||
this.emit('NEW_GAME', data);
|
||||
});
|
||||
|
||||
this.logParser.on('ROUND_ENDED', async (data) => {
|
||||
const datalayer = data.winner ? await Layers.getLayerById(data.winner.layer) : null;
|
||||
const outdata = {
|
||||
rawData: data,
|
||||
rawLayer: data.winner ? data.winner.layer : null,
|
||||
rawLevel: data.winner ? data.winner.level : null,
|
||||
time: data.time,
|
||||
winnerId: data.winner ? data.winner.team : null,
|
||||
winnerFaction: data.winner ? data.winner.faction : null,
|
||||
winnerTickets: data.winner ? data.winner.tickets : null,
|
||||
loserId: data.loser ? data.loser.team : null,
|
||||
loserFaction: data.loser ? data.loser.faction : null,
|
||||
loserTickets: data.loser ? data.loser.tickets : null,
|
||||
layer: datalayer
|
||||
};
|
||||
|
||||
this.emit('ROUND_ENDED', outdata);
|
||||
});
|
||||
|
||||
this.logParser.on('PLAYER_CONNECTED', async (data) => {
|
||||
data.player = await this.getPlayerBySteamID(data.steamID);
|
||||
if (data.player) data.player.suffix = data.playerSuffix;
|
||||
|
||||
delete data.steamID;
|
||||
delete data.playerSuffix;
|
||||
if (data.player) {
|
||||
data.player.suffix = data.playerSuffix;
|
||||
} else {
|
||||
data.player = {
|
||||
steamID: data.steamID,
|
||||
name: data.playerSuffix
|
||||
};
|
||||
}
|
||||
|
||||
this.emit('PLAYER_CONNECTED', data);
|
||||
});
|
||||
|
||||
this.logParser.on('PLAYER_DISCONNECTED', async (data) => {
|
||||
data.player = await this.getPlayerBySteamID(data.steamID);
|
||||
|
||||
delete data.steamID;
|
||||
if (!data.player) {
|
||||
data.player = {
|
||||
steamID: data.steamID
|
||||
};
|
||||
}
|
||||
|
||||
this.emit('PLAYER_DISCONNECTED', data);
|
||||
});
|
||||
@ -250,14 +277,12 @@ export default class SquadServer extends EventEmitter {
|
||||
data.victim.teamID === data.attacker.teamID &&
|
||||
data.victim.steamID !== data.attacker.steamID;
|
||||
|
||||
delete data.victimName;
|
||||
delete data.attackerName;
|
||||
|
||||
this.emit('PLAYER_WOUNDED', data);
|
||||
if (data.teamkill) this.emit('TEAMKILL', data);
|
||||
});
|
||||
|
||||
this.logParser.on('PLAYER_DIED', async (data) => {
|
||||
// console.log(data);
|
||||
data.victim = await this.getPlayerByName(data.victimName);
|
||||
data.attacker = await this.getPlayerByName(data.attackerName);
|
||||
if (!data.attacker)
|
||||
@ -268,8 +293,7 @@ export default class SquadServer extends EventEmitter {
|
||||
data.victim.teamID === data.attacker.teamID &&
|
||||
data.victim.steamID !== data.attacker.steamID;
|
||||
|
||||
delete data.victimName;
|
||||
delete data.attackerName;
|
||||
// console.log(data);
|
||||
|
||||
this.emit('PLAYER_DIED', data);
|
||||
});
|
||||
@ -289,6 +313,7 @@ export default class SquadServer extends EventEmitter {
|
||||
this.logParser.on('PLAYER_POSSESS', async (data) => {
|
||||
data.player = await this.getPlayerByNameSuffix(data.playerSuffix);
|
||||
if (data.player) data.player.possessClassname = data.possessClassname;
|
||||
if (data.player) data.player.characterClassname = data.characterClassname;
|
||||
|
||||
delete data.playerSuffix;
|
||||
|
||||
@ -303,8 +328,33 @@ export default class SquadServer extends EventEmitter {
|
||||
this.emit('PLAYER_UNPOSSESS', data);
|
||||
});
|
||||
|
||||
this.logParser.on('ROUND_ENDED', async (data) => {
|
||||
this.emit('ROUND_ENDED', data);
|
||||
this.logParser.on('SERVER-MOVE-WARN', async (data) => {
|
||||
const tsd = data.tse - data.cts;
|
||||
Logger.verbose('ServerMoveWarn', 1, 'tsd value: ' + tsd);
|
||||
|
||||
const outdata = {
|
||||
raw: data.raw,
|
||||
time: data.time,
|
||||
rawID: data.characterName,
|
||||
cheatType: 'Remote Actions',
|
||||
player: await this.getPlayerByCondition((p) => p.characterClassname === data.characterName),
|
||||
probcheat: data.cts < 2 ? 'unlikely' : null,
|
||||
probcolor: data.cts < 2 ? 0xffff00 : null
|
||||
};
|
||||
|
||||
if ((tsd < 235 && tsd > 0) || tsd < -100) this.emit('PLAYER-CHEAT', outdata);
|
||||
});
|
||||
|
||||
this.logParser.on('EXPLODE-ATTACK', async (data) => {
|
||||
const outdata = {
|
||||
raw: data.raw,
|
||||
time: data.time,
|
||||
rawID: data.playercont,
|
||||
cheatType: 'Explosion attack',
|
||||
player: await this.getPlayerByController(data.playercont)
|
||||
};
|
||||
|
||||
this.emit('PLAYER-CHEAT', outdata);
|
||||
});
|
||||
|
||||
this.logParser.on('TICK_RATE', (data) => {
|
||||
@ -352,20 +402,27 @@ export default class SquadServer extends EventEmitter {
|
||||
}
|
||||
|
||||
const players = [];
|
||||
for (const player of await this.rcon.getListPlayers())
|
||||
for (const player of await this.rcon.getListPlayers()) {
|
||||
players.push({
|
||||
...oldPlayerInfo[player.steamID],
|
||||
...player,
|
||||
playercontroller: this.logParser.eventStore.players[player.steamID]
|
||||
playercont: this.logParser.eventStore.players[player.steamID]
|
||||
? this.logParser.eventStore.players[player.steamID].controller
|
||||
: null,
|
||||
squad: await this.getSquadByID(player.teamID, player.squadID)
|
||||
});
|
||||
}
|
||||
|
||||
this.players = players;
|
||||
|
||||
for (const player of this.players) {
|
||||
if (typeof oldPlayerInfo[player.steamID] === 'undefined') continue;
|
||||
if (player.name !== oldPlayerInfo[player.steamID].name)
|
||||
this.emit('PLAYER_NAME_CHANGE', {
|
||||
player: player,
|
||||
oldName: oldPlayerInfo[player.steamID].name,
|
||||
newName: player.name
|
||||
});
|
||||
if (player.teamID !== oldPlayerInfo[player.steamID].teamID)
|
||||
this.emit('PLAYER_TEAM_CHANGE', {
|
||||
player: player,
|
||||
@ -412,11 +469,45 @@ export default class SquadServer extends EventEmitter {
|
||||
Logger.verbose('SquadServer', 1, `Updating layer information...`);
|
||||
|
||||
try {
|
||||
let currentLayer = this.currentLayer;
|
||||
const currentMap = await this.rcon.getCurrentMap();
|
||||
const nextMap = await this.rcon.getNextMap();
|
||||
const nextMapToBeVoted = nextMap.layer === 'To be voted';
|
||||
|
||||
const currentLayer = await Layers.getLayerByName(currentMap.layer);
|
||||
Logger.verbose('RCON', 1, "curlay name:" + currentLayer?.name + ", rcon name:" + currentMap.layer);
|
||||
if (currentLayer?.name !== currentMap.layer){
|
||||
let rconlayer = await Layers.getLayerByName(currentMap.layer);
|
||||
if (!rconlayer) rconlayer = await Layers.getLayerById(currentMap.layer);
|
||||
if (!rconlayer) rconlayer = await Layers.getLayerByClassname(currentMap.layer);
|
||||
if (!rconlayer) {
|
||||
if (currentMap.layer === "Jensen's Training Range")
|
||||
rconlayer = await Layers.getLayerById('JensensRange_ADF-PLA')
|
||||
}
|
||||
if (!rconlayer) {
|
||||
const cleanrconmap = currentMap.layer.toLowerCase().replace(/[ _]/gi, '');
|
||||
rconlayer = await Layers.getLayerByCondition(
|
||||
(l) =>
|
||||
cleanrconmap.includes(l.map.name.toLowerCase().replace(/[ _]/gi, '')) &&
|
||||
cleanrconmap.includes(l.gamemode.toLowerCase().replace(/[ _]/gi, '')) &&
|
||||
cleanrconmap.includes(l.version.toLowerCase().replace(/[ _]/gi, '')) &&
|
||||
cleanrconmap.includes(l.modName.toLowerCase().replace(/[ _]/gi, ''))
|
||||
);
|
||||
}
|
||||
if (!rconlayer)
|
||||
currentLayer = await Layers.getLayerByCondition(
|
||||
(l) =>
|
||||
cleanrconmap.includes(l.map.name.toLowerCase().replace(/[ _]/gi, '')) &&
|
||||
cleanrconmap.includes(l.gamemode.toLowerCase().replace(/[ _]/gi, '')) &&
|
||||
cleanrconmap.includes(l.version.toLowerCase().replace(/[ _]/gi, ''))
|
||||
);
|
||||
|
||||
if (rconlayer && currentMap.layer !== "Jensen's Training Range"){
|
||||
currentLayer = rconlayer;
|
||||
}
|
||||
}
|
||||
if (currentLayer) Logger.verbose('SquadServer', 1, 'Found Current layer');
|
||||
else Logger.verbose('SquadServer', 1, 'WARNING: Could not find layer from RCON');
|
||||
|
||||
const nextLayer = nextMapToBeVoted ? null : await Layers.getLayerByName(nextMap.layer);
|
||||
|
||||
if (this.layerHistory.length === 0) {
|
||||
@ -447,12 +538,15 @@ export default class SquadServer extends EventEmitter {
|
||||
Logger.verbose('SquadServer', 1, `Updating A2S information...`);
|
||||
|
||||
try {
|
||||
const serverlayer = this.currentLayer;
|
||||
const data = await Gamedig.query({
|
||||
type: 'squad',
|
||||
host: this.options.host,
|
||||
port: this.options.queryPort
|
||||
});
|
||||
|
||||
// console.log(data);
|
||||
|
||||
const info = {
|
||||
raw: data.raw,
|
||||
serverName: data.name,
|
||||
@ -466,7 +560,8 @@ export default class SquadServer extends EventEmitter {
|
||||
reserveQueue: parseInt(data.raw.rules.ReservedQueue_i),
|
||||
|
||||
matchTimeout: parseFloat(data.raw.rules.MatchTimeout_f),
|
||||
gameVersion: data.raw.version
|
||||
gameVersion: data.raw.version,
|
||||
currentLayer: data.map
|
||||
};
|
||||
|
||||
this.serverName = info.serverName;
|
||||
@ -482,6 +577,12 @@ export default class SquadServer extends EventEmitter {
|
||||
this.matchTimeout = info.matchTimeout;
|
||||
this.gameVersion = info.gameVersion;
|
||||
|
||||
Logger.verbose('SquadServer', 1, 'a2smsg' + info.currentLayer + ", current id:" + serverlayer?.layerid);
|
||||
if (info.currentLayer !== serverlayer?.layerid) {
|
||||
const a2slayer = await Layers.getLayerById(info.currentLayer);
|
||||
this.currentLayer = a2slayer ? a2slayer : this.currentLayer;
|
||||
}
|
||||
|
||||
this.emit('UPDATED_A2S_INFORMATION', info);
|
||||
} catch (err) {
|
||||
Logger.verbose('SquadServer', 1, 'Failed to update A2S information.', err);
|
||||
@ -495,6 +596,88 @@ export default class SquadServer extends EventEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
async updateLayerList() {
|
||||
// update expected list from http source
|
||||
await Layers.pull();
|
||||
|
||||
// grab layers actually available through rcon
|
||||
const rconRaw = (await this.rcon.execute('ListLayers'))?.split('\n') || [];
|
||||
// take out first result, not actual layer just a header
|
||||
rconRaw.shift();
|
||||
|
||||
// filter out raw result from RCON, modded layers have a suffix that needs filtering
|
||||
const rconLayers = [];
|
||||
for (const raw of rconRaw) {
|
||||
rconLayers.push(raw.split(' ')[0]);
|
||||
}
|
||||
|
||||
// go through http layers and delete any that don't show up in rcon
|
||||
for (const layer of Layers.layers) {
|
||||
if (!rconLayers.find((e) => e === layer.layerid)) Layers._layers.delete(layer.layerid);
|
||||
}
|
||||
|
||||
// add layers that are in RCON that we did not find in the http list
|
||||
for (const layer of rconLayers) {
|
||||
if (!Layers.layers.find((e) => e?.layerid === layer)) {
|
||||
const newLayer = this.mapLayer(layer);
|
||||
if (!newLayer) continue;
|
||||
// Logger.verbose('LayerUpdater', 1, 'Created RCON Layer: ', newLayer);
|
||||
Layers._layers.set(newLayer.layerid, newLayer);
|
||||
}
|
||||
}
|
||||
|
||||
for (const layer of Layers.layers) {
|
||||
Logger.verbose('LayerUpdater', 1, 'Found layer: ' + layer.layerid + ' - ' + layer.name);
|
||||
}
|
||||
}
|
||||
|
||||
// helper for updateLayerList
|
||||
mapLayer(layid) {
|
||||
layid = layid.replace(/[^\da-z_-]/gi, '');
|
||||
const gl =
|
||||
/^((?<mod>[A-Z]+)_)?(?<level>[A-Za-z]+)_((?<gamemode>[A-Za-z]+)(_|$))?((?<version>[vV][0-9]+)(_|$))?((?<team1>[a-zA-Z0-9]+)[-v](?<team2>[a-zA-Z0-9]+))?/gm.exec(
|
||||
layid
|
||||
)?.groups;
|
||||
if (!gl) return;
|
||||
|
||||
const teams = [];
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
for (const t of ['team1', 'team2']) {
|
||||
teams.push({
|
||||
tickets: 0,
|
||||
commander: false,
|
||||
vehicles: [],
|
||||
numberOfTanks: 0,
|
||||
numberOfHelicopters: 0
|
||||
});
|
||||
}
|
||||
teams[0].faction = gl.team1 ? gl.team1 : 'Unknown';
|
||||
teams[0].name = gl.team1 ? gl.team1 : 'Unknown';
|
||||
teams[1].faction = gl.team2 ? gl.team2 : 'Unknown';
|
||||
teams[1].name = gl.team2 ? gl.team2 : 'Unknown';
|
||||
|
||||
return {
|
||||
name: layid.replace(/_/g, ' '),
|
||||
classname: gl.level,
|
||||
layerid: layid,
|
||||
modName: gl.mod ? gl.mod : 'Vanilla',
|
||||
map: {
|
||||
name: gl.level
|
||||
},
|
||||
gamemode: gl.gamemode ? gl.gamemode : 'Training',
|
||||
gamemodeType: gl.gamemode ? gl.gamemode : 'Training',
|
||||
version: gl.version ? gl.version : 'v0',
|
||||
size: '0.0x0.0 km',
|
||||
sizeType: 'Playable Area',
|
||||
numberOfCapturePoints: 0,
|
||||
lighting: {
|
||||
name: 'Unknown',
|
||||
classname: 'Unknown'
|
||||
},
|
||||
teams: teams
|
||||
};
|
||||
}
|
||||
|
||||
async getPlayerByCondition(condition, forceUpdate = false, retry = true) {
|
||||
let matches;
|
||||
|
||||
@ -551,10 +734,7 @@ export default class SquadServer extends EventEmitter {
|
||||
}
|
||||
|
||||
async getPlayerByController(controller, forceUpdate) {
|
||||
return this.getPlayerByCondition(
|
||||
(player) => player.playercontroller === controller,
|
||||
forceUpdate
|
||||
);
|
||||
return this.getPlayerByCondition((player) => player.playercont === controller, forceUpdate);
|
||||
}
|
||||
|
||||
async pingSquadJSAPI() {
|
||||
|
@ -3,6 +3,8 @@ export default class Layer {
|
||||
this.name = data.Name;
|
||||
this.classname = data.levelName;
|
||||
this.layerid = data.rawName;
|
||||
const mod = /^(?<name>[A-Z0-9]+)_.*/g.exec(data.rawName)?.groups;
|
||||
this.modName = mod ? mod.name : 'Vanilla';
|
||||
this.map = {
|
||||
name: data.mapName
|
||||
};
|
||||
|
@ -6,27 +6,38 @@ import Layer from './layer.js';
|
||||
|
||||
class Layers {
|
||||
constructor() {
|
||||
this.layers = [];
|
||||
this._layers = new Map();
|
||||
|
||||
this.pulled = false;
|
||||
}
|
||||
|
||||
get layers() {
|
||||
return [...this._layers.values()];
|
||||
}
|
||||
|
||||
async pull(force = false) {
|
||||
if (this.pulled && !force) {
|
||||
Logger.verbose('Layers', 2, 'Already pulled layers.');
|
||||
return;
|
||||
return this.layers;
|
||||
}
|
||||
if (force) Logger.verbose('Layers', 1, 'Forcing update to layer information...');
|
||||
|
||||
this.layers = [];
|
||||
this._layers = new Map();
|
||||
|
||||
Logger.verbose('Layers', 1, 'Pulling layers...');
|
||||
const response = await axios.get(
|
||||
'https://raw.githubusercontent.com/Squad-Wiki/squad-wiki-pipeline-map-data/master/completed_output/_Current%20Version/finished.json'
|
||||
const response = await axios.post(
|
||||
// Change get to post for mod support
|
||||
'http://hub.afocommunity.com/api/layers.json',
|
||||
[0, 2891780963, 1959152751, 2428425228]
|
||||
);
|
||||
|
||||
// const response = await axios.get(
|
||||
// 'https://raw.githubusercontent.com/Squad-Wiki/squad-wiki-pipeline-map-data/master/completed_output/_Current%20Version/finished.json'
|
||||
// );
|
||||
|
||||
for (const layer of response.data.Maps) {
|
||||
this.layers.push(new Layer(layer));
|
||||
const newLayer = new Layer(layer);
|
||||
this._layers.set(newLayer.layerid, newLayer);
|
||||
}
|
||||
|
||||
Logger.verbose('Layers', 1, `Pulled ${this.layers.length} layers.`);
|
||||
@ -40,17 +51,25 @@ class Layers {
|
||||
await this.pull();
|
||||
|
||||
const matches = this.layers.filter(condition);
|
||||
if (matches.length === 1) return matches[0];
|
||||
if (matches.length >= 1) return matches[0];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async getLayerById(layerId) {
|
||||
await this.pull();
|
||||
return this._layers.get(layerId) ?? null;
|
||||
}
|
||||
|
||||
getLayerByName(name) {
|
||||
return this.getLayerByCondition((layer) => layer.name === name);
|
||||
}
|
||||
|
||||
getLayerByClassname(classname) {
|
||||
return this.getLayerByCondition((layer) => layer.classname === classname);
|
||||
return this.getLayerByCondition(
|
||||
(layer) =>
|
||||
layer.classname.replace(/_/, '').toLowerCase() === classname.replace(/_/, '').toLowerCase()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
23
squad-server/log-parser/BadPlayerMovement.js
Normal file
23
squad-server/log-parser/BadPlayerMovement.js
Normal file
@ -0,0 +1,23 @@
|
||||
export default {
|
||||
regex:
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogNetPlayerMovement: Warning: ServerMove: TimeStamp expired: ([0-9.]+), CurrentTimeStamp: ([0-9.]+), Character: ([a-zA-Z0-9_]+)/,
|
||||
onMatch: (args, logParser) => {
|
||||
// try not to spam events
|
||||
if (logParser.eventStore.session['last-move-chain']) {
|
||||
if (logParser.eventStore.session['last-move-chain'] === args[2]) return;
|
||||
}
|
||||
|
||||
logParser.eventStore.session['last-move-chain'] = args[2];
|
||||
|
||||
const data = {
|
||||
raw: args[0],
|
||||
time: args[1],
|
||||
chainID: args[2],
|
||||
characterName: args[5],
|
||||
tse: parseFloat(args[3]),
|
||||
cts: parseFloat(args[4])
|
||||
};
|
||||
|
||||
logParser.emit('SERVER-MOVE-WARN', data);
|
||||
}
|
||||
};
|
25
squad-server/log-parser/apply-explosive-damage.js
Normal file
25
squad-server/log-parser/apply-explosive-damage.js
Normal file
@ -0,0 +1,25 @@
|
||||
export default {
|
||||
regex:
|
||||
/^\[(([0-9.-]+):[0-9]+)]\[([ 0-9]+)]LogSquadTrace: \[DedicatedServer]ApplyExplosiveDamage\(\): HitActor=nullptr DamageCauser=[A-z0-9_]+ DamageInstigator=([A-z0-9_]+)/,
|
||||
onMatch: (args, logParser) => {
|
||||
const data = {
|
||||
raw: args[0],
|
||||
time: args[1],
|
||||
chainID: args[3],
|
||||
sectime: args[2],
|
||||
playercont: args[4]
|
||||
};
|
||||
|
||||
if (logParser.eventStore.lastexplode) {
|
||||
if (logParser.eventStore.lastexplode.sectime === args[2]) {
|
||||
if (logParser.eventStore.lastexplode.chainID === args[3]) {
|
||||
if (logParser.eventStore.lastexplode.playercont === args[4]) {
|
||||
logParser.emit('EXPLODE-ATTACK', data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logParser.eventStore.lastexplode = data;
|
||||
}
|
||||
};
|
@ -19,6 +19,7 @@ import ServerTickRate from './server-tick-rate.js';
|
||||
import ClientConnected from './client-connected.js';
|
||||
import ClientLogin from './client-login.js';
|
||||
import PendingConnectionDestroyed from './pending-connection-destroyed.js';
|
||||
import BadPlayerMovement from './BadPlayerMovement.js';
|
||||
|
||||
export default class SquadLogParser extends LogParser {
|
||||
constructor(options) {
|
||||
@ -45,7 +46,8 @@ export default class SquadLogParser extends LogParser {
|
||||
ServerTickRate,
|
||||
ClientConnected,
|
||||
ClientLogin,
|
||||
PendingConnectionDestroyed
|
||||
PendingConnectionDestroyed,
|
||||
BadPlayerMovement
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,16 @@
|
||||
export default {
|
||||
regex:
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: UChannel::Close: Sending CloseBunch\. ChIndex == [0-9]+\. Name: \[UChannel\] ChIndex: [0-9]+, Closing: [0-9]+ \[UNetConnection\] RemoteAddr: ([0-9]{17}):[0-9]+, Name: SteamNetConnection_[0-9]+, Driver: GameNetDriver SteamNetDriver_[0-9]+, IsServer: YES, PC: ([^ ]+PlayerController_C_[0-9]+), Owner: [^ ]+PlayerController_C_[0-9]+/,
|
||||
onMatch: (args, logParser) => {
|
||||
const data = {
|
||||
raw: args[0],
|
||||
time: args[1],
|
||||
chainID: args[2],
|
||||
steamID: args[3],
|
||||
playerController: args[4]
|
||||
};
|
||||
onMatch: (args, logParser) => {
|
||||
const data = {
|
||||
raw: args[0],
|
||||
time: args[1],
|
||||
chainID: args[2],
|
||||
steamID: args[3],
|
||||
playerController: args[4]
|
||||
};
|
||||
|
||||
logParser.eventStore.disconnected[data.steamID] = true;
|
||||
logParser.emit('PLAYER_DISCONNECTED', data);
|
||||
}
|
||||
logParser.eventStore.disconnected[data.steamID] = true;
|
||||
logParser.emit('PLAYER_DISCONNECTED', data);
|
||||
}
|
||||
};
|
||||
|
@ -1,14 +1,15 @@
|
||||
export default {
|
||||
regex:
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQPlayerController::)?OnPossess\(\): PC=(.+) Pawn=([A-z0-9_]+)_C/,
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQPlayerController::)?OnPossess\(\): PC=(.+) Pawn=(([A-z0-9_]+)_C_[0-9]+)/,
|
||||
onMatch: (args, logParser) => {
|
||||
const data = {
|
||||
raw: args[0],
|
||||
time: args[1],
|
||||
chainID: args[2],
|
||||
playerSuffix: args[3],
|
||||
possessClassname: args[4],
|
||||
pawn: args[5]
|
||||
characterClassname: args[4],
|
||||
possessClassname: args[5],
|
||||
pawn: args[6]
|
||||
};
|
||||
|
||||
logParser.eventStore.session[args[3]] = args[2];
|
||||
|
@ -14,8 +14,8 @@ export default {
|
||||
loser: logParser.eventStore.ROUND_LOSER ? logParser.eventStore.ROUND_LOSER : null,
|
||||
time: args[1]
|
||||
};
|
||||
logParser.emit('ROUND_ENDED', data);
|
||||
delete logParser.eventStore.ROUND_WINNER;
|
||||
delete logParser.eventStore.ROUND_LOSER;
|
||||
logParser.emit('ROUND_ENDED', data);
|
||||
}
|
||||
};
|
||||
|
@ -21,6 +21,13 @@ export default {
|
||||
};
|
||||
if (data.action === 'won') {
|
||||
logParser.eventStore.ROUND_WINNER = data;
|
||||
logParser.eventStore.WON = {
|
||||
raw: data.raw,
|
||||
time: data.time,
|
||||
chainID: data.chainID,
|
||||
winner: data.subfaction,
|
||||
layer: data.level
|
||||
};
|
||||
} else {
|
||||
logParser.eventStore.ROUND_LOSER = data;
|
||||
}
|
||||
|
@ -39,10 +39,18 @@ export default class AutoTKWarn extends BasePlugin {
|
||||
}
|
||||
|
||||
async onTeamkill(info) {
|
||||
if (info.attacker && this.options.attackerMessage) {
|
||||
let displaymsg = true;
|
||||
if (this.server.currentLayer) {
|
||||
if (this.server.currentLayer.gamemode === 'Seed') displaymsg = false;
|
||||
if (this.server.currentLayer.gamemode === 'Training') displaymsg = false;
|
||||
} else {
|
||||
if (this.server.currentLayerRcon.layer.includes('Seed')) displaymsg = false;
|
||||
if (this.server.currentLayerRcon.layer.includes('Training')) displaymsg = false;
|
||||
}
|
||||
if (info.attacker && this.options.attackerMessage && displaymsg) {
|
||||
this.server.rcon.warn(info.attacker.steamID, this.options.attackerMessage);
|
||||
}
|
||||
if (info.victim && this.options.victimMessage) {
|
||||
if (info.victim && this.options.victimMessage && displaymsg) {
|
||||
this.server.rcon.warn(info.victim.steamID, this.options.victimMessage);
|
||||
}
|
||||
}
|
||||
|
@ -138,6 +138,9 @@ export default class DBLog extends BasePlugin {
|
||||
},
|
||||
lastName: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
discordID: {
|
||||
type: DataTypes.BIGINT
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -392,6 +395,8 @@ export default class DBLog extends BasePlugin {
|
||||
this.onTickRate = this.onTickRate.bind(this);
|
||||
this.onUpdatedA2SInformation = this.onUpdatedA2SInformation.bind(this);
|
||||
this.onNewGame = this.onNewGame.bind(this);
|
||||
this.onRoundEnd = this.onRoundEnd.bind(this);
|
||||
this.onPlayerNameChange = this.onPlayerNameChange.bind(this);
|
||||
this.onPlayerWounded = this.onPlayerWounded.bind(this);
|
||||
this.onPlayerDied = this.onPlayerDied.bind(this);
|
||||
this.onPlayerRevived = this.onPlayerRevived.bind(this);
|
||||
@ -420,22 +425,30 @@ export default class DBLog extends BasePlugin {
|
||||
name: this.server.serverName
|
||||
});
|
||||
|
||||
this.match = await this.models.Match.findOne({
|
||||
where: { server: this.options.overrideServerID || this.server.id, endTime: null }
|
||||
});
|
||||
await this.repairDB();
|
||||
|
||||
this.server.on('TICK_RATE', this.onTickRate);
|
||||
this.server.on('UPDATED_A2S_INFORMATION', this.onUpdatedA2SInformation);
|
||||
this.server.on('NEW_GAME', this.onNewGame);
|
||||
this.server.on('ROUND_ENDED', this.onRoundEnd);
|
||||
this.server.on('PLAYER_NAME_CHANGE', this.onPlayerNameChange);
|
||||
this.server.on('PLAYER_WOUNDED', this.onPlayerWounded);
|
||||
this.server.on('PLAYER_DIED', this.onPlayerDied);
|
||||
this.server.on('PLAYER_REVIVED', this.onPlayerRevived);
|
||||
}
|
||||
|
||||
async repairDB() {
|
||||
this.match = await this.models.Match.findOne({
|
||||
where: { server: this.options.overrideServerID || this.server.id, endTime: null }
|
||||
});
|
||||
}
|
||||
|
||||
async unmount() {
|
||||
this.server.removeEventListener('TICK_RATE', this.onTickRate);
|
||||
this.server.removeEventListener('UPDATED_A2S_INFORMATION', this.onTickRate);
|
||||
this.server.removeEventListener('NEW_GAME', this.onNewGame);
|
||||
this.server.removeEventListener('ROUND_ENDED', this.onRoundEnd);
|
||||
this.server.removeEventListener('PLAYER_NAME_CHANGE', this.onPlayerNameChange);
|
||||
this.server.removeEventListener('PLAYER_WOUNDED', this.onPlayerWounded);
|
||||
this.server.removeEventListener('PLAYER_DIED', this.onPlayerDied);
|
||||
this.server.removeEventListener('PLAYER_REVIVED', this.onPlayerRevived);
|
||||
@ -461,6 +474,7 @@ export default class DBLog extends BasePlugin {
|
||||
}
|
||||
|
||||
async onNewGame(info) {
|
||||
this.verbose(1, 'New Game');
|
||||
await this.models.Match.update(
|
||||
{ endTime: info.time, winner: info.winner },
|
||||
{ where: { server: this.options.overrideServerID || this.server.id, endTime: null } }
|
||||
@ -477,6 +491,22 @@ export default class DBLog extends BasePlugin {
|
||||
});
|
||||
}
|
||||
|
||||
async onRoundEnd(info) {
|
||||
this.verbose(1, 'Round End');
|
||||
await this.models.Match.update(
|
||||
{ endTime: info.time, winner: info.winnerFaction },
|
||||
{ where: { server: this.options.overrideServerID || this.server.id, endTime: null } }
|
||||
);
|
||||
}
|
||||
|
||||
async onPlayerNameChange(info) {
|
||||
if (info.player)
|
||||
await this.models.SteamUser.upsert({
|
||||
steamID: info.player.steamID,
|
||||
lastName: info.player.name
|
||||
});
|
||||
}
|
||||
|
||||
async onPlayerWounded(info) {
|
||||
if (info.attacker)
|
||||
await this.models.SteamUser.upsert({
|
||||
|
@ -36,6 +36,10 @@ export default class DiscordBasePlugin extends BasePlugin {
|
||||
if (typeof message === 'object' && 'embed' in message)
|
||||
message.embed.footer = message.embed.footer || { text: COPYRIGHT_MESSAGE };
|
||||
|
||||
await this.channel.send(message);
|
||||
try {
|
||||
await this.channel.send(message);
|
||||
} catch (error) {
|
||||
this.verbose(1, 'discordjs cache error caught!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
88
squad-server/plugins/discord-cheater.js
Normal file
88
squad-server/plugins/discord-cheater.js
Normal file
@ -0,0 +1,88 @@
|
||||
import DiscordBasePlugin from './discord-base-plugin.js';
|
||||
|
||||
export default class DiscordCheater extends DiscordBasePlugin {
|
||||
static get description() {
|
||||
return 'The <code>DiscordCheater</code> plugin will send any suspected cheating to a Discord channel.';
|
||||
}
|
||||
|
||||
static get defaultEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static get optionsSpecification() {
|
||||
return {
|
||||
...DiscordBasePlugin.optionsSpecification,
|
||||
channelID: {
|
||||
required: true,
|
||||
description: 'The ID of the channel to log admin broadcasts to.',
|
||||
default: '',
|
||||
example: '667741905228136459'
|
||||
},
|
||||
color: {
|
||||
required: false,
|
||||
description: 'The color of the embed.',
|
||||
default: 0xff0000
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
constructor(server, options, connectors) {
|
||||
super(server, options, connectors);
|
||||
|
||||
this.cheat = this.cheat.bind(this);
|
||||
}
|
||||
|
||||
async mount() {
|
||||
this.server.on('PLAYER-CHEAT', this.cheat);
|
||||
}
|
||||
|
||||
async unmount() {
|
||||
this.server.removeEventListener('PLAYER-CHEAT', this.cheat);
|
||||
}
|
||||
|
||||
async cheat(info) {
|
||||
await this.sendDiscordMessage({
|
||||
embed: {
|
||||
title: 'Suspected Cheater',
|
||||
color: info.probcolor ? info.probcolor : this.options.color,
|
||||
fields: [
|
||||
{
|
||||
name: 'Player Name',
|
||||
value: info.player ? info.player.name : 'Unkown Name',
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'SteamID',
|
||||
value: info.player
|
||||
? `[${info.player.steamID}](https://steamcommunity.com/profiles/${info.player.steamID})`
|
||||
: 'Unkown steamID',
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Player Raw ID (give to Skillet)',
|
||||
value: info.rawID ? info.rawID : 'Unknown ID'
|
||||
},
|
||||
{
|
||||
name: 'raw log string (give to Skillet)',
|
||||
value: info.raw ? info.raw : 'Unkown'
|
||||
},
|
||||
{
|
||||
name: 'Type of Cheating',
|
||||
value: info.cheatType
|
||||
},
|
||||
{
|
||||
name: 'Probibility of cheating',
|
||||
value: info.probcheat ? info.probcheat : 'high',
|
||||
inline: true
|
||||
}
|
||||
],
|
||||
timestamp: info.time ? info.time.toISOString() : 'Unkown'
|
||||
}
|
||||
});
|
||||
if (info.probcheat ? info.probcheat === 'high' : true) {
|
||||
if (info.player){
|
||||
await this.server.rcon.kick(info.player.steamID, 'R14 | Cheating - highly suspected');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -143,7 +143,7 @@ export default class SquadRcon extends Rcon {
|
||||
|
||||
for (const line of response.split('\n')) {
|
||||
const match = line.match(
|
||||
/ID: ([0-9]+) \| SteamID: ([0-9]{17}) \| Name: (.+) \| Team ID: ([0-9]+) \| Squad ID: ([0-9]+|N\/A) \| Is Leader: (True|False) \| Role: ([A-Za-z0-9_]*)\b/
|
||||
/ID: ([0-9]+) \| SteamID: ([0-9]{17}) \| Name: (.+) \| Team ID: ([0-9]+) \| Squad ID: ([0-9]+|N\/A) \| Is Leader: (True|False) \| Role: (.+)/
|
||||
);
|
||||
if (!match) continue;
|
||||
|
||||
@ -153,8 +153,8 @@ export default class SquadRcon extends Rcon {
|
||||
name: match[3],
|
||||
teamID: match[4],
|
||||
squadID: match[5] !== 'N/A' ? match[5] : null,
|
||||
isLeader: match[6] === 'True',
|
||||
role: match[7]
|
||||
isSquadLeader: match[6] === 'True',
|
||||
rconRole: match[7]
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -44,7 +44,7 @@ export default async function fetchAdminLists(adminLists) {
|
||||
}
|
||||
|
||||
const groupRgx = /(?<=^Group=)(?<groupID>.*?):(?<groupPerms>.*?)(?=(?:\r\n|\r|\n|\s+\/\/))/gm;
|
||||
const adminRgx = /(?<=^Admin=)(?<steamID>\d+):(?<groupID>\S+)/gm;
|
||||
const adminRgx = /(?<=^Admin=)(?<steamID>\d+):(?<groupID>[^(//)]+?)\s*(\/\/|$)/gm;
|
||||
|
||||
for (const m of data.matchAll(groupRgx)) {
|
||||
groups[`${idx}-${m.groups.groupID}`] = m.groups.groupPerms.split(',');
|
||||
|
Loading…
Reference in New Issue
Block a user