Merge branch 'master' into multiconfig

# Conflicts:
#	.gitignore
#	squad-server/rcon.js
This commit is contained in:
Skillet 2023-11-11 14:01:59 -05:00
commit 7117459599
20 changed files with 530 additions and 56 deletions

2
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -140,10 +140,10 @@ export default class SquadRcon extends Rcon {
const players = [];
if(!response || response.length < 1) return players;
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]
});
}

View File

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