mirror of
https://github.com/AsgardEternal/SquadJS.git
synced 2024-09-28 06:04:23 -05:00
Merge remote-tracking branch 'davidrepos/eos-integration'
# Conflicts: # squad-server/index.js # squad-server/log-parser/index.js # squad-server/log-parser/player-disconnected.js # squad-server/log-parser/player-possess.js # squad-server/rcon.js
This commit is contained in:
commit
cb11120d7c
14
README.md
14
README.md
@ -329,7 +329,7 @@ Interested in creating your own plugin? [See more here](./squad-server/plugins/r
|
||||
<h6>Description</h6>
|
||||
<p>Message SquadJS will send to players warning them they will be kicked</p>
|
||||
<h6>Default</h6>
|
||||
<pre><code>Join a squad, you are are unassigned and will be kicked</code></pre></li>
|
||||
<pre><code>Join a squad, you are unassigned and will be kicked</code></pre></li>
|
||||
<li><h4>kickMessage</h4>
|
||||
<h6>Description</h6>
|
||||
<p>Message to send to players when they are kicked</p>
|
||||
@ -952,6 +952,18 @@ Grafana:
|
||||
<pre><code>300000</code></pre></li></ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>PersistentEOSIDtoSteamID</summary>
|
||||
<h2>PersistentEOSIDtoSteamID</h2>
|
||||
<p>Stores into a DB every association of SteamID-EOSID</p>
|
||||
<h3>Options</h3>
|
||||
<ul><li><h4>database (Required)</h4>
|
||||
<h6>Description</h6>
|
||||
<p>The Sequelize connector.</p>
|
||||
<h6>Default</h6>
|
||||
<pre><code>sqlite</code></pre></li></ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>SeedingMode</summary>
|
||||
<h2>SeedingMode</h2>
|
||||
|
@ -7,6 +7,9 @@
|
||||
"queryPort": 27165,
|
||||
"rconPort": 21114,
|
||||
"rconPassword": "password",
|
||||
"rconPassThrough": true,
|
||||
"rconPassThroughPort": 8124,
|
||||
"dumpRconResponsesToFile": false,
|
||||
"logReaderMode": "tail",
|
||||
"logDir": "C:/path/to/squad/log/folder",
|
||||
"ftp": {
|
||||
@ -45,7 +48,7 @@
|
||||
{
|
||||
"plugin": "AutoKickUnassigned",
|
||||
"enabled": true,
|
||||
"warningMessage": "Join a squad, you are are unassigned and will be kicked",
|
||||
"warningMessage": "Join a squad, you are unassigned and will be kicked",
|
||||
"kickMessage": "Unassigned - automatically removed",
|
||||
"frequencyOfWarnings": 30,
|
||||
"unassignedTimer": 360,
|
||||
@ -236,6 +239,11 @@
|
||||
"broadcasts": [],
|
||||
"interval": 300000
|
||||
},
|
||||
{
|
||||
"plugin": "PersistentEOSIDtoSteamID",
|
||||
"enabled": true,
|
||||
"database": "sqlite"
|
||||
},
|
||||
{
|
||||
"plugin": "SeedingMode",
|
||||
"enabled": true,
|
||||
|
@ -16,9 +16,13 @@ export default class LogParser extends EventEmitter {
|
||||
|
||||
this.eventStore = {
|
||||
disconnected: {}, // holding area, cleared on map change.
|
||||
players: {}, // persistent data, steamid, controller, suffix.
|
||||
players: [], // persistent data, steamid, controller, suffix.
|
||||
playersEOS: [], // proxies from EOSID to persistent data, steamid, controller, suffix.
|
||||
connectionIdToSteamID: new Map(),
|
||||
session: {}, // old eventstore, nonpersistent data
|
||||
clients: {} // used in the connection chain before we resolve a player.
|
||||
clients: {}, // used in the connection chain before we resolve a player.
|
||||
lastConnection: {}, // used to store the last client connection data to then associate a steamid
|
||||
joinRequests: []
|
||||
};
|
||||
|
||||
this.linesPerMinute = 0;
|
||||
|
892
core/rcon.js
892
core/rcon.js
@ -1,367 +1,613 @@
|
||||
import EventEmitter from 'events';
|
||||
import net from 'net';
|
||||
import util from 'util';
|
||||
|
||||
/* eslint-disable */
|
||||
import { EventEmitter } from 'node:events';
|
||||
import net from 'node:net';
|
||||
import Logger from './logger.js';
|
||||
|
||||
const SERVERDATA_EXECCOMMAND = 0x02;
|
||||
const SERVERDATA_RESPONSE_VALUE = 0x00;
|
||||
const SERVERDATA_AUTH = 0x03;
|
||||
const SERVERDATA_AUTH_RESPONSE = 0x02;
|
||||
const SERVERDATA_CHAT_VALUE = 0x01;
|
||||
|
||||
const MID_PACKET_ID = 0x01;
|
||||
const END_PACKET_ID = 0x02;
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
const RCON_LOG_FILEPATH = 'RCON_RECEIVED_MESSAGES.log';
|
||||
export default class Rcon extends EventEmitter {
|
||||
constructor(options = {}) {
|
||||
super();
|
||||
|
||||
// store config
|
||||
for (const option of ['host', 'port', 'password'])
|
||||
if (!(option in options)) throw new Error(`${option} must be specified.`);
|
||||
|
||||
this.host = options.host;
|
||||
this.port = options.port;
|
||||
this.password = options.password;
|
||||
this.autoReconnectDelay = options.autoReconnectDelay || 5000;
|
||||
|
||||
// bind methods
|
||||
this.connect = this.connect.bind(this); // we bind this as we call it on the auto reconnect timeout
|
||||
this.onData = this.onData.bind(this);
|
||||
this.onClose = this.onClose.bind(this);
|
||||
this.onError = this.onError.bind(this);
|
||||
|
||||
// setup socket
|
||||
this.client = new net.Socket();
|
||||
this.client.on('data', this.onData);
|
||||
this.client.on('close', this.onClose);
|
||||
this.client.on('error', this.onError);
|
||||
|
||||
// constants
|
||||
this.maximumPacketSize = 4096;
|
||||
|
||||
// internal variables
|
||||
this.client = null;
|
||||
this.stream = new Buffer.alloc(0);
|
||||
this.type = { auth: 0x03, command: 0x02, response: 0x00, server: 0x01 };
|
||||
this.soh = { size: 7, id: 0, type: this.type.response, body: '' };
|
||||
this.responseString = { id: 0, body: '' };
|
||||
this.connected = false;
|
||||
this.autoReconnect = false;
|
||||
this.autoReconnectTimeout = null;
|
||||
this.autoReconnectDelay = options.autoReconnectDelay || 1000;
|
||||
this.connectionRetry;
|
||||
this.msgIdLow = 6;
|
||||
this.msgIdHigh = 16;
|
||||
this.specialId = 19;
|
||||
this.msgId = this.msgIdLow;
|
||||
this.passThrough = options.passThrough ? true : false;
|
||||
this.passThroughPort = options.passThroughPort || 8124;
|
||||
this.passThroughTimeOut = options.passThroughTimeOut || 60000;
|
||||
this.passThroughMaxClients = 1; //options.passThroughMaxClients || 10;
|
||||
this.passThroughChallenge = options.passThroughChallenge || options.password;
|
||||
this.dumpRconResponsesToFile = options.dumpRconResponsesToFile || false;
|
||||
this.rconClients = {};
|
||||
for (let i = 1; i <= this.passThroughMaxClients; i++) this.rconClients[`${i}`] = null;
|
||||
this.ptServer = null;
|
||||
|
||||
this.incomingData = Buffer.from([]);
|
||||
this.incomingResponse = [];
|
||||
this.steamIndex = { '76561198799344716': '00026e21ce3d43c792613bdbb6dec1ba' }; // example dtata
|
||||
this.eosIndex = { '00026e21ce3d43c792613bdbb6dec1ba': '76561198799344716' }; // example dtata
|
||||
|
||||
this.responseCallbackQueue = [];
|
||||
this.rotateLogFile(RCON_LOG_FILEPATH);
|
||||
}
|
||||
|
||||
onData(data) {
|
||||
Logger.verbose('RCON', 4, `Got data: ${this.bufToHexString(data)}`);
|
||||
|
||||
// the logic in this method simply splits data sent via the data event into packets regardless of how they're
|
||||
// distributed in the event calls
|
||||
const packets = this.decodeData(data);
|
||||
|
||||
for (const packet of packets) {
|
||||
Logger.verbose('RCON', 4, `Processing packet: ${this.bufToHexString(packet)}`);
|
||||
|
||||
const decodedPacket = this.decodePacket(packet);
|
||||
Logger.verbose(
|
||||
'RCON',
|
||||
3,
|
||||
`Processing decoded packet: ${this.decodedPacketToString(decodedPacket)}`
|
||||
);
|
||||
|
||||
switch (decodedPacket.type) {
|
||||
case SERVERDATA_RESPONSE_VALUE:
|
||||
case SERVERDATA_AUTH_RESPONSE:
|
||||
switch (decodedPacket.id) {
|
||||
case MID_PACKET_ID:
|
||||
this.incomingResponse.push(decodedPacket);
|
||||
break;
|
||||
case END_PACKET_ID:
|
||||
this.responseCallbackQueue.shift()(
|
||||
this.incomingResponse.map((packet) => packet.body).join()
|
||||
);
|
||||
this.incomingResponse = [];
|
||||
break;
|
||||
default:
|
||||
Logger.verbose(
|
||||
'RCON',
|
||||
1,
|
||||
`Unknown packet ID ${decodedPacket.id} in: ${this.decodedPacketToString(
|
||||
decodedPacket
|
||||
)}`
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case SERVERDATA_CHAT_VALUE:
|
||||
this.processChatPacket(decodedPacket);
|
||||
break;
|
||||
|
||||
default:
|
||||
Logger.verbose(
|
||||
'RCON',
|
||||
1,
|
||||
`Unknown packet type ${decodedPacket.type} in: ${this.decodedPacketToString(
|
||||
decodedPacket
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
decodeData(data) {
|
||||
this.incomingData = Buffer.concat([this.incomingData, data]);
|
||||
|
||||
const packets = [];
|
||||
|
||||
// we check that it's greater than 4 as if it's not then the length header is not fully present which breaks the
|
||||
// rest of the code. We just need to wait for more data.
|
||||
while (this.incomingData.byteLength >= 4) {
|
||||
const size = this.incomingData.readInt32LE(0);
|
||||
const packetSize = size + 4;
|
||||
|
||||
// The packet following an empty packet will report to be 10 long (14 including the size header bytes), but in
|
||||
// it should report 17 long (21 including the size header bytes). Therefore, if the packet is 10 in size
|
||||
// and there's enough data for it to be a longer packet then we need to probe to check it's this broken packet.
|
||||
const probeSize = 17;
|
||||
const probePacketSize = 21;
|
||||
|
||||
if (size === 10 && this.incomingData.byteLength >= probeSize) {
|
||||
// copy the section of the incoming data of interest
|
||||
const probeBuf = this.incomingData.slice(0, probePacketSize);
|
||||
// decode it
|
||||
const decodedProbePacket = this.decodePacket(probeBuf);
|
||||
// check whether body matches
|
||||
if (decodedProbePacket.body === '\x00\x00\x00\x01\x00\x00\x00') {
|
||||
// it does so it's the broken packet
|
||||
// remove the broken packet from the incoming data
|
||||
this.incomingData = this.incomingData.slice(probePacketSize);
|
||||
Logger.verbose('RCON', 4, `Ignoring some data: ${this.bufToHexString(probeBuf)}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.incomingData.byteLength < packetSize) {
|
||||
Logger.verbose('RCON', 4, `Waiting for more data...`);
|
||||
break;
|
||||
}
|
||||
|
||||
const packet = this.incomingData.slice(0, packetSize);
|
||||
packets.push(packet);
|
||||
|
||||
this.incomingData = this.incomingData.slice(packetSize);
|
||||
}
|
||||
|
||||
return packets;
|
||||
}
|
||||
|
||||
decodePacket(packet) {
|
||||
return {
|
||||
size: packet.readInt32LE(0),
|
||||
id: packet.readInt32LE(4),
|
||||
type: packet.readInt32LE(8),
|
||||
body: packet.toString('utf8', 12, packet.byteLength - 2)
|
||||
};
|
||||
}
|
||||
|
||||
processChatPacket(decodedPacket) {}
|
||||
|
||||
onClose(hadError) {
|
||||
this.connected = false;
|
||||
|
||||
Logger.verbose('RCON', 1, `Socket closed ${hadError ? 'without' : 'with'} an error.`);
|
||||
|
||||
if (this.autoReconnect) {
|
||||
Logger.verbose('RCON', 1, `Sleeping ${this.autoReconnectDelay}ms before reconnecting.`);
|
||||
setTimeout(this.connect, this.autoReconnectDelay);
|
||||
}
|
||||
}
|
||||
|
||||
onError(err) {
|
||||
Logger.verbose('RCON', 1, `Socket had error:`, err);
|
||||
this.emit('RCON_ERROR', err);
|
||||
}
|
||||
|
||||
connect() {
|
||||
processChatPacket(decodedPacket) {
|
||||
console.log(decodedPacket.body);
|
||||
} //
|
||||
async connect() {
|
||||
return new Promise((resolve, reject) => {
|
||||
Logger.verbose('RCON', 1, `Connecting to: ${this.host}:${this.port}`);
|
||||
|
||||
const onConnect = async () => {
|
||||
this.client.removeListener('error', onError);
|
||||
this.connected = true;
|
||||
|
||||
if (this.client && this.connected && !this.client.destroyed)
|
||||
return reject(new Error('Rcon.connect() Rcon already connected.'));
|
||||
this.removeAllListeners('server');
|
||||
this.removeAllListeners('auth');
|
||||
this.on('server', (pkt) => this.processChatPacket(pkt));
|
||||
this.once('auth', () => {
|
||||
Logger.verbose('RCON', 1, `Connected to: ${this.host}:${this.port}`);
|
||||
|
||||
try {
|
||||
// connected successfully, now try auth...
|
||||
await this.write(SERVERDATA_AUTH, this.password);
|
||||
|
||||
// connected and authed successfully
|
||||
this.autoReconnect = true;
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
};
|
||||
|
||||
const onError = (err) => {
|
||||
this.client.removeListener('connect', onConnect);
|
||||
|
||||
Logger.verbose('RCON', 1, `Failed to connect to: ${this.host}:${this.port}`, err);
|
||||
|
||||
reject(err);
|
||||
};
|
||||
|
||||
this.client.once('connect', onConnect);
|
||||
this.client.once('error', onError);
|
||||
|
||||
this.client.connect(this.port, this.host);
|
||||
clearTimeout(this.connectionRetry);
|
||||
this.connected = true;
|
||||
if (this.passThrough) this.createServer();
|
||||
resolve();
|
||||
});
|
||||
Logger.verbose('RCON', 1, `Connecting to: ${this.host}:${this.port}`);
|
||||
this.connectionRetry = setTimeout(() => this.connect(), this.autoReconnectDelay);
|
||||
this.autoReconnect = true;
|
||||
this.client = net
|
||||
.createConnection({ port: this.port, host: this.host }, () => this.sendAuth())
|
||||
.on('data', (data) => this.onData(data))
|
||||
.on('end', () => this.onClose())
|
||||
.on('error', () => this.onNetError());
|
||||
}).catch((error) => {
|
||||
Logger.verbose('RCON', 1, `Rcon.connect() ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
async disconnect() {
|
||||
return new Promise((resolve, reject) => {
|
||||
Logger.verbose('RCON', 1, `Disconnecting from: ${this.host}:${this.port}`);
|
||||
|
||||
const onClose = () => {
|
||||
this.client.removeListener('error', onError);
|
||||
|
||||
Logger.verbose('RCON', 1, `Disconnected from: ${this.host}:${this.port}`);
|
||||
|
||||
resolve();
|
||||
};
|
||||
|
||||
const onError = (err) => {
|
||||
this.client.removeListener('close', onClose);
|
||||
|
||||
Logger.verbose('RCON', 1, `Failed to disconnect from: ${this.host}:${this.port}`, err);
|
||||
|
||||
reject(err);
|
||||
};
|
||||
|
||||
this.client.once('close', onClose);
|
||||
this.client.once('error', onError);
|
||||
|
||||
// prevent any auto reconnection happening
|
||||
clearTimeout(this.connectionRetry);
|
||||
this.removeAllListeners('server');
|
||||
this.removeAllListeners('auth');
|
||||
this.autoReconnect = false;
|
||||
// clear the timeout just in case the socket closed and then we DCed
|
||||
clearTimeout(this.autoReconnectTimeout);
|
||||
|
||||
this.client.end();
|
||||
this.connected = false;
|
||||
this.closeServer();
|
||||
resolve();
|
||||
}).catch((error) => {
|
||||
Logger.verbose('RCON', 1, `Rcon.disconnect() ${error}`);
|
||||
});
|
||||
}
|
||||
async execute(body) {
|
||||
let steamID = body.match(/\d{17}/);
|
||||
if (steamID) {
|
||||
steamID = steamID[0];
|
||||
body = body.replace(/\d{17}/, this.steamIndex[steamID]);
|
||||
}
|
||||
|
||||
execute(command) {
|
||||
return this.write(SERVERDATA_EXECCOMMAND, command);
|
||||
}
|
||||
|
||||
write(type, body) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.connected) {
|
||||
reject(new Error('Not connected.'));
|
||||
return;
|
||||
if (!this.connected) return reject(new Error('Rcon not connected.'));
|
||||
if (!this.client.writable) return reject(new Error('Unable to write to node:net socket'));
|
||||
const string = String(body);
|
||||
const length = Buffer.from(string).length;
|
||||
if (length > 4154) Logger.verbose('RCON', 1, `Error occurred. Oversize, "${length}" > 4154`);
|
||||
else {
|
||||
const outputData = (data) => {
|
||||
clearTimeout(timeOut);
|
||||
resolve(data);
|
||||
};
|
||||
const timedOut = () => {
|
||||
console.warn('MISSED', listenerId);
|
||||
this.removeListener(listenerId, outputData);
|
||||
return reject(new Error(`Rcon response timed out`));
|
||||
};
|
||||
if (this.msgId > this.msgIdHigh - 2) this.msgId = this.msgIdLow;
|
||||
const listenerId = `response${this.msgId}`;
|
||||
const timeOut = setTimeout(timedOut, 10000);
|
||||
this.once(listenerId, outputData);
|
||||
this.send(string, this.msgId);
|
||||
this.msgId++;
|
||||
}
|
||||
|
||||
if (!this.client.writable) {
|
||||
reject(new Error('Unable to write to socket.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const encodedPacket = this.encodePacket(
|
||||
type,
|
||||
type !== SERVERDATA_AUTH ? MID_PACKET_ID : END_PACKET_ID,
|
||||
body
|
||||
);
|
||||
|
||||
const encodedEmptyPacket = this.encodePacket(type, END_PACKET_ID, '');
|
||||
|
||||
if (this.maximumPacketSize < encodedPacket.length) {
|
||||
reject(new Error('Packet too long.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const onError = (err) => {
|
||||
Logger.verbose('RCON', 1, 'Error occurred. Wiping response action queue.', err);
|
||||
this.responseCallbackQueue = [];
|
||||
reject(err);
|
||||
};
|
||||
|
||||
// the auth packet also sends a normal response, so we add an extra empty action to ignore it
|
||||
if (type === SERVERDATA_AUTH) {
|
||||
Logger.verbose('RCON', 2, `Writing Auth Packet`);
|
||||
Logger.verbose('RCON', 4, `Writing packet with type "${type}" and body "${body}".`);
|
||||
this.responseCallbackQueue.push(() => {});
|
||||
this.responseCallbackQueue.push((decodedPacket) => {
|
||||
this.client.removeListener('error', onError);
|
||||
if (decodedPacket.id === -1) {
|
||||
Logger.verbose('RCON', 1, 'Authentication failed.');
|
||||
reject(new Error('Authentication failed.'));
|
||||
} else {
|
||||
Logger.verbose('RCON', 1, 'Authentication succeeded.');
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Logger.verbose('RCON', 2, `Writing packet with type "${type}" and body "${body}".`);
|
||||
this.responseCallbackQueue.push((response) => {
|
||||
this.client.removeListener('error', onError);
|
||||
|
||||
Logger.verbose(
|
||||
'RCON',
|
||||
2,
|
||||
`Returning complete response: ${response.replace(/\r\n|\r|\n/g, '\\n')}`
|
||||
);
|
||||
|
||||
resolve(response);
|
||||
});
|
||||
}
|
||||
|
||||
this.client.once('error', onError);
|
||||
|
||||
Logger.verbose('RCON', 4, `Sending packet: ${this.bufToHexString(encodedPacket)}`);
|
||||
this.client.write(encodedPacket);
|
||||
|
||||
if (type !== SERVERDATA_AUTH) {
|
||||
}).catch((error) => {
|
||||
Logger.verbose('RCON', 1, `Rcon.execute() ${error}`);
|
||||
});
|
||||
}
|
||||
sendAuth() {
|
||||
Logger.verbose('RCON', 1, `Sending Token to: ${this.host}:${this.port}`);
|
||||
this.client.write(this.encode(this.type.auth, 0, this.password).toString('binary'), 'binary'); //2147483647
|
||||
}
|
||||
send(body, id = 99) {
|
||||
this.write(this.type.command, id, body);
|
||||
this.write(this.type.command, id + 2);
|
||||
}
|
||||
write(type, id, body) {
|
||||
Logger.verbose(
|
||||
'RCON',
|
||||
2,
|
||||
`Writing packet with type "${type}", id "${id}" and body "${body || ''}"`
|
||||
);
|
||||
this.client.write(this.encode(type, id, body).toString('binary'), 'binary');
|
||||
}
|
||||
encode(type, id, body = '') {
|
||||
const size = Buffer.byteLength(body) + 14;
|
||||
const buffer = new Buffer.alloc(size);
|
||||
buffer.writeInt32LE(size - 4, 0);
|
||||
buffer.writeInt32LE(id, 4);
|
||||
buffer.writeInt32LE(type, 8);
|
||||
buffer.write(body, 12, size - 2, 'utf8');
|
||||
buffer.writeInt16LE(0, size - 2);
|
||||
return buffer;
|
||||
}
|
||||
onData(data) {
|
||||
Logger.verbose('RCON', 4, `Got data: ${this.bufToHexString(data)}`);
|
||||
this.stream = Buffer.concat([this.stream, data], this.stream.byteLength + data.byteLength);
|
||||
while (this.stream.byteLength >= 7) {
|
||||
const packet = this.decode();
|
||||
if (!packet) break;
|
||||
else
|
||||
Logger.verbose(
|
||||
'RCON',
|
||||
4,
|
||||
`Sending empty packet: ${this.bufToHexString(encodedEmptyPacket)}`
|
||||
3,
|
||||
`Processing decoded packet: Size: ${packet.size}, ID: ${packet.id}, Type: ${packet.type}, Body: ${packet.body}`
|
||||
);
|
||||
this.client.write(encodedEmptyPacket);
|
||||
this.appendToFile(RCON_LOG_FILEPATH, packet.body);
|
||||
|
||||
if (packet.id > this.msgIdHigh) this.emit(`responseForward_1`, packet);
|
||||
else if (packet.type === this.type.response) this.onResponse(packet);
|
||||
else if (packet.type === this.type.server) this.onServer(packet);
|
||||
else if (packet.type === this.type.command) this.emit('auth');
|
||||
}
|
||||
}
|
||||
onServer(packet) {
|
||||
this.emit('server', packet);
|
||||
for (const client in this.rconClients)
|
||||
if (this.rconClients[client]) {
|
||||
this.emit(`serverForward_${this.rconClients[client].rconIdClient}`, packet.body);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
encodePacket(type, id, body, encoding = 'utf8') {
|
||||
const size = Buffer.byteLength(body) + 14;
|
||||
const buf = Buffer.alloc(size);
|
||||
|
||||
buf.writeInt32LE(size - 4, 0);
|
||||
buf.writeInt32LE(id, 4);
|
||||
buf.writeInt32LE(type, 8);
|
||||
buf.write(body, 12, size - 2, encoding);
|
||||
buf.writeInt16LE(0, size - 2);
|
||||
|
||||
return buf;
|
||||
decode() {
|
||||
if (
|
||||
this.stream[0] === 0 &&
|
||||
this.stream[1] === 1 &&
|
||||
this.stream[2] === 0 &&
|
||||
this.stream[3] === 0 &&
|
||||
this.stream[4] === 0 &&
|
||||
this.stream[5] === 0 &&
|
||||
this.stream[6] === 0
|
||||
) {
|
||||
this.stream = this.stream.subarray(7);
|
||||
return this.soh;
|
||||
}
|
||||
const bufSize = this.stream.readInt32LE(0);
|
||||
if (bufSize > 4154 || bufSize < 10) return this.badPacket();
|
||||
else if (bufSize <= this.stream.byteLength - 4 && this.stream.byteLength >= 12) {
|
||||
const bufId = this.stream.readInt32LE(4);
|
||||
const bufType = this.stream.readInt32LE(8);
|
||||
if (
|
||||
this.stream[bufSize + 2] !== 0 ||
|
||||
this.stream[bufSize + 3] !== 0 ||
|
||||
bufId < 0 ||
|
||||
bufType < 0 ||
|
||||
bufType > 5
|
||||
)
|
||||
return this.badPacket();
|
||||
else {
|
||||
const response = {
|
||||
size: bufSize,
|
||||
id: bufId,
|
||||
type: bufType,
|
||||
body: this.stream.toString('utf8', 12, bufSize + 2)
|
||||
};
|
||||
this.stream = this.stream.subarray(bufSize + 4);
|
||||
if (
|
||||
response.body === '' &&
|
||||
this.stream[0] === 0 &&
|
||||
this.stream[1] === 1 &&
|
||||
this.stream[2] === 0 &&
|
||||
this.stream[3] === 0 &&
|
||||
this.stream[4] === 0 &&
|
||||
this.stream[5] === 0 &&
|
||||
this.stream[6] === 0
|
||||
) {
|
||||
this.stream = this.stream.subarray(7);
|
||||
response.body = '';
|
||||
}
|
||||
return response;
|
||||
}
|
||||
} else return null;
|
||||
}
|
||||
onResponse(packet) {
|
||||
if (packet.body === '') {
|
||||
this.emit(`response${this.responseString.id - 2}`, this.responseString.body);
|
||||
this.responseString.body = '';
|
||||
} else if (!packet.body.includes('')) {
|
||||
this.responseString.body = this.responseString.body += packet.body;
|
||||
this.responseString.id = packet.id;
|
||||
} else this.badPacket();
|
||||
}
|
||||
badPacket() {
|
||||
Logger.verbose(
|
||||
'RCON',
|
||||
1,
|
||||
`Bad packet, clearing: ${this.bufToHexString(this.stream)} Pending string: ${
|
||||
this.responseString
|
||||
}`
|
||||
);
|
||||
this.stream = Buffer.alloc(0);
|
||||
this.responseString = '';
|
||||
return null;
|
||||
}
|
||||
onClose() {
|
||||
Logger.verbose('RCON', 1, `Socket closed`);
|
||||
this.cleanUp();
|
||||
}
|
||||
onNetError(error) {
|
||||
Logger.verbose('RCON', 1, `node:net error:`, error);
|
||||
this.emit('RCON_ERROR', error);
|
||||
this.cleanUp();
|
||||
}
|
||||
cleanUp() {
|
||||
this.closeServer();
|
||||
this.connected = false;
|
||||
this.removeAllListeners();
|
||||
clearTimeout(this.connectionRetry);
|
||||
if (this.autoReconnect) {
|
||||
Logger.verbose('RCON', 1, `Sleeping ${this.autoReconnectDelay}ms before reconnecting`);
|
||||
this.connectionRetry = setTimeout(() => this.connect(), this.autoReconnectDelay);
|
||||
}
|
||||
}
|
||||
createServer() {
|
||||
this.ptServer = net.createServer((client) => this.onNewClient(client));
|
||||
this.ptServer.maxConnections = this.passThroughMaxClients;
|
||||
this.ptServer.on('error', (error) => this.onSerErr(error));
|
||||
this.ptServer.on('drop', () =>
|
||||
Logger.verbose(
|
||||
'RCON',
|
||||
1,
|
||||
`Pass-through Server: Max Clients Reached (${this.passThroughMaxClients}) rejecting new connection`
|
||||
)
|
||||
);
|
||||
this.ptServer.listen(this.passThroughPort, () =>
|
||||
Logger.verbose('RCON', 1, `Pass-through Server: Listening on port ${this.passThroughPort}`)
|
||||
);
|
||||
}
|
||||
closeServer() {
|
||||
for (const client in this.rconClients)
|
||||
if (this.rconClients[client]) this.rconClients[client].end();
|
||||
if (!this.ptServer) return;
|
||||
this.ptServer.close(() => this.onServerClose());
|
||||
}
|
||||
onServerClose() {
|
||||
if (!this.ptServer) return;
|
||||
this.ptServer.removeAllListeners();
|
||||
this.ptServer = null;
|
||||
Logger.verbose('RCON', 1, `Pass-through Server: Closed`);
|
||||
}
|
||||
onNewClient(client) {
|
||||
client.setTimeout(this.passThroughTimeOut);
|
||||
client.on('end', () => this.onClientEnd(client));
|
||||
client.on('error', () => this.onClientEnd(client));
|
||||
client.on('timeout', () => this.onClientTimeOut(client));
|
||||
client.on('data', (data) => this.onClientData(client, data));
|
||||
Logger.verbose('RCON', 1, `Pass-through Server: Client connecting`);
|
||||
}
|
||||
onSerErr(error) {
|
||||
this.closeServer();
|
||||
Logger.verbose('RCON', 1, `Pass-through Server: ${error}`);
|
||||
}
|
||||
onClientEnd(client) {
|
||||
if (!client.rconIdClient) return;
|
||||
this.removeAllListeners(`serverForward_${client.rconIdClient}`);
|
||||
this.removeAllListeners(`responseForward_${client.rconIdClient}`);
|
||||
this.rconClients[`${client.rconIdClient}`] = null;
|
||||
Logger.verbose('RCON', 1, `Pass-through Server: Client-${client.rconIdClient} Disconnected`);
|
||||
}
|
||||
onClientTimeOut(client) {
|
||||
client.end();
|
||||
Logger.verbose('RCON', 1, `Pass-through Server: Client-${client.rconIdClient} Timed Out`);
|
||||
}
|
||||
onClientData(client, data) {
|
||||
if (!client.rconStream) client.rconStream = new Buffer.alloc(0);
|
||||
client.rconStream = Buffer.concat(
|
||||
[client.rconStream, data],
|
||||
client.rconStream.byteLength + data.byteLength
|
||||
);
|
||||
while (client.rconStream.byteLength >= 4) {
|
||||
const packet = this.decodeClient(client);
|
||||
if (!packet) break;
|
||||
if (!client.rconHasAuthed) this.authClient(client, packet);
|
||||
else {
|
||||
if (!client.rconWheel || client.rconWheel > 20) client.rconWheel = 0;
|
||||
else client.rconWheel++;
|
||||
|
||||
client.rconIdQueueNEW[`${client.rconWheel}`] = packet.id;
|
||||
|
||||
const encoded = this.encode(
|
||||
packet.type,
|
||||
this.specialId + client.rconWheel,
|
||||
this.steamToEosClient(packet.body)
|
||||
); ////////////////////////////////////////////////
|
||||
this.client.write(encoded.toString('binary'), 'binary');
|
||||
// this.client.write(this.encode(packet.type, this.specialId * client.rconIdClient).toString("binary"), "binary")
|
||||
}
|
||||
}
|
||||
}
|
||||
decodeClient(client) {
|
||||
const bufSize = client.rconStream.readInt32LE(0);
|
||||
if (bufSize <= client.rconStream.byteLength - 4) {
|
||||
const response = {
|
||||
size: bufSize,
|
||||
id: client.rconStream.readInt32LE(4),
|
||||
type: client.rconStream.readInt32LE(8),
|
||||
body: client.rconStream.toString('utf8', 12, bufSize + 2)
|
||||
};
|
||||
client.rconStream = client.rconStream.subarray(bufSize + 4);
|
||||
return response;
|
||||
} else return null;
|
||||
}
|
||||
authClient(client, packet) {
|
||||
if (packet.body !== this.passThroughChallenge) {
|
||||
client.end();
|
||||
Logger.verbose('RCON', 1, `Pass-through Server: Client [Rejected] Password not matched`);
|
||||
} else {
|
||||
client.rconHasAuthed = true;
|
||||
client.rconIdQueueNEW = {};
|
||||
for (let i = 1; i <= this.passThroughMaxClients; i++) {
|
||||
if (this.rconClients[`${i}`] === null) {
|
||||
client.rconIdClient = i;
|
||||
this.rconClients[`${i}`] = client;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.on(`serverForward_${client.rconIdClient}`, (body) =>
|
||||
client.write(this.encode(1, 0, this.eosToSteam(body)).toString('binary'), 'binary')
|
||||
);
|
||||
this.on(`responseForward_${client.rconIdClient}`, (packet) => this.onForward(client, packet));
|
||||
client.write(this.encode(0, packet.id));
|
||||
client.write(this.encode(2, packet.id));
|
||||
Logger.verbose('RCON', 1, `Pass-through Server: Client-${client.rconIdClient} Connected`);
|
||||
}
|
||||
}
|
||||
onForward(client, packet) {
|
||||
if (packet.body !== '' && packet.body !== '') {
|
||||
const int = packet.id - this.specialId;
|
||||
|
||||
//console.log(client.rconIdQueueNEW);//////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
client.write(
|
||||
this.encode(packet.type, client.rconIdQueueNEW[int], this.eosToSteam(packet.body)).toString(
|
||||
'binary'
|
||||
),
|
||||
'binary'
|
||||
);
|
||||
} else if (packet.body != '') {
|
||||
const int = packet.id - this.specialId;
|
||||
client.write(this.encode(0, client.rconIdQueueNEW[int]).toString('binary'), 'binary');
|
||||
client.write(this.encodeSpecial(client.rconIdQueueNEW[int]).toString('binary'), 'binary');
|
||||
}
|
||||
}
|
||||
encodeSpecial(id) {
|
||||
const buffer = new Buffer.alloc(21);
|
||||
buffer.writeInt32LE(10, 0);
|
||||
buffer.writeInt32LE(id, 4);
|
||||
buffer.writeInt32LE(0, 8);
|
||||
buffer.writeInt32LE(1, 15);
|
||||
return buffer;
|
||||
}
|
||||
bufToHexString(buf) {
|
||||
return buf.toString('hex').match(/../g).join(' ');
|
||||
}
|
||||
|
||||
decodedPacketToString(decodedPacket) {
|
||||
return util.inspect(decodedPacket, { breakLength: Infinity });
|
||||
}
|
||||
|
||||
async warn(steamID, message) {
|
||||
await this.execute(`AdminWarn "${steamID}" ${message}`);
|
||||
this.execute(`AdminWarn "${steamID}" ${message}`);
|
||||
}
|
||||
|
||||
async kick(steamID, reason) {
|
||||
await this.execute(`AdminKick "${steamID}" ${reason}`);
|
||||
this.execute(`AdminKick "${steamID}" ${reason}`);
|
||||
}
|
||||
async forceTeamChange(steamID) {
|
||||
this.execute(`AdminForceTeamChange "${steamID}"`);
|
||||
}
|
||||
|
||||
async forceTeamChange(steamID) {
|
||||
await this.execute(`AdminForceTeamChange "${steamID}"`);
|
||||
addIds(steamId, eosId) {
|
||||
this.steamIndex[steamId] = eosId; // { "76561198799344716": "00026e21ce3d43c792613bdbb6dec1ba" };
|
||||
this.eosIndex[eosId] = steamId;
|
||||
}
|
||||
|
||||
removeIds(eosId) {
|
||||
// clean up ids on leave
|
||||
}
|
||||
|
||||
steamToEosClient(body) {
|
||||
//assume client does not send more than 1 steamId per msg
|
||||
const m = body.match(/[0-9]{17}/);
|
||||
if (m && m[1] in this.steamIndex) return body.replaceAll(`${m[0]}`, this.steamIndex[m[0]]);
|
||||
return body;
|
||||
}
|
||||
|
||||
eosToSteam(body) {
|
||||
//split body to lines for matching (1 steamId per line)
|
||||
const lines = body.split('\n');
|
||||
const nBody = [];
|
||||
for (let line of lines) nBody.push(this.matchRcon(line));
|
||||
return nBody.join('\n');
|
||||
}
|
||||
|
||||
matchRcon(line) {
|
||||
for (const r of defs) {
|
||||
const match = line.match(r.regex);
|
||||
if (match && (match.groups.eosId in this.eosIndex || match.groups.steamId)) {
|
||||
return r.rep(
|
||||
line,
|
||||
match.groups.steamId || this.eosIndex[match.groups.eosId],
|
||||
match.groups.eosId
|
||||
);
|
||||
}
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
appendToFile(filePath, content) {
|
||||
if (!this.dumpRconResponsesToFile) return;
|
||||
const dir = path.dirname(filePath);
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.appendFile(filePath, content + '\n', (err) => {
|
||||
if (err) throw err;
|
||||
});
|
||||
}
|
||||
rotateLogFile(logFile) {
|
||||
if (!this.dumpRconResponsesToFile) return;
|
||||
if (fs.existsSync(logFile)) {
|
||||
const ext = path.extname(logFile);
|
||||
const base = path.basename(logFile, ext);
|
||||
const dir = path.dirname(logFile);
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const newFile = path.join(dir, `${base}_${timestamp}${ext}`);
|
||||
|
||||
fs.renameSync(logFile, newFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
const defs = [
|
||||
//strict matching to avoid 'name as steamId errors'
|
||||
{
|
||||
regex:
|
||||
/^ID: [0-9]+ \| Online IDs: EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\d{17}) \| Name: .+ \| Team ID: (1|2|N\/A) \| Squad ID: ([0-9]+|N\/A) \| Is Leader: (True|False|N\/A) \| Role: .+$/,
|
||||
rep: (line, steamId, eosId) => {
|
||||
return line.replace(
|
||||
/\| Online IDs: EOS: [\w\d]{32} steam: \d{17} \|/,
|
||||
`| SteamID: ${steamId} |`
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
regex:
|
||||
/^ID: (?<Id>[0-9]+) \| Online IDs: EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\d{17}) \| Since Disconnect: (?<SinceDc>.+) \| Name: (?<name>.+)$/,
|
||||
rep: (line, steamId, eosId) => {
|
||||
return line.replace(
|
||||
/Online IDs: EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\d{17})/,
|
||||
`SteamID: ${steamId}`
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
regex:
|
||||
/^ID: (?<sqdId>[0-9]+) \| Name: (?<sqdName>.+) \| Size: (?<sqdSize>[0-9]) \| Locked: (?<locked>True|False) \| Creator Name: (?<creatorName>.+) \| Creator Online IDs: EOS: (?<eosID>[\d\w]{32}) steam: (?<steamId>\d{17})/,
|
||||
rep: (line, steamId, eosId) => {
|
||||
console.log(line, steamId, eosId);
|
||||
const ret = line.replace(
|
||||
/\| Creator Online IDs: EOS: [\w\d]{32} steam: \d{17}/,
|
||||
`| Creator Steam ID: ${steamId}`
|
||||
);
|
||||
return ret;
|
||||
}
|
||||
},
|
||||
{
|
||||
regex:
|
||||
/^Forced team change for player (?<id>[0-9]+). \[Online IDs= EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\d{17})] (.+)/,
|
||||
rep: (line, steamId, eosId) => {
|
||||
return line.replace(
|
||||
/Online IDs= EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\d{17})/,
|
||||
`steamid=${steamId}`
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
regex: /^Could not find player (?<eosId>[0-9a-f]{32})/,
|
||||
rep: (line, steamId, eosId) => {
|
||||
return line.replace(`Could not find player ${eosId}`, `Could not find player ${steamId}`);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
regex:
|
||||
/^\[Chat(All|Team|Squad|Admin)] \[Online IDs:EOS: (?<eosId>[\d\w]{32}) steam: (?<steamId>\d{17})] (?<name>.+) : (?<msg>.+)/,
|
||||
rep: (line, steamId, eosId) => {
|
||||
return line.replace(/Online IDs:EOS: [\d\w]{32} steam: \d{17}/, `SteamID:${steamId}`);
|
||||
}
|
||||
},
|
||||
{
|
||||
regex:
|
||||
/^(?<name>.+) \(Online IDs: EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\d+)\) has created Squad (?<squadNum>[0-9]+) \(Squad Name: (?<squadName>.+)\) on (?<teamName>.+)/,
|
||||
rep: (line, steamId, eosId) => {
|
||||
return line.replace(
|
||||
/Online IDs: EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\d+)/,
|
||||
`Steam ID: ${steamId}`
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
regex:
|
||||
/^Kicked player (?<Id>[0-9]+). \[Online IDs= EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\d{17})] (?<name>.+)/,
|
||||
rep: (line, steamId, eosId) => {
|
||||
return line.replace(
|
||||
/Online IDs= EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>\d{17})/,
|
||||
`steamid=${steamId}`
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
regex: /^ERROR: Unable to find player with name or id \((?<eosId>[0-9a-f]{32})\)$/,
|
||||
rep: (line, steamId, eosId) => {
|
||||
return line.replace(`name or id (${eosId})`, `name or id (${steamId})`);
|
||||
}
|
||||
},
|
||||
{
|
||||
regex:
|
||||
/^\[Online I(d|D)s:EOS: (?<eosId>[0-9a-f]{32}) steam: (?<steamId>)\d{17}] (?<name>.+) has (un)?possessed admin camera\./,
|
||||
rep: (line, steamId, eosId) => {
|
||||
return line.replace(/Online I(d|D)s:EOS: [\w\d]{32} steam: \d{17}/, `SteamID:${steamId}`);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
//////////////////////////////////////////////////ALL BELOW IS FOR STANDALONE TESTING/RUNNING
|
||||
// const Logger = {
|
||||
// level: 1,
|
||||
// verbose(type, lvl, msg, msg1 = "") {
|
||||
// if (lvl > this.level) return;
|
||||
// console.log(type, lvl, msg, msg1);
|
||||
// },
|
||||
// };
|
||||
|
||||
// const squadJsStyle = async () => {
|
||||
// const getCurrentMap = async () => {
|
||||
// const response = await rcon.execute("ShowCurrentMap");
|
||||
// const match = response.match(/^Current level is (?<level>.+), layer is (?<layer>.+)/);
|
||||
// if (!match) {
|
||||
// debugger
|
||||
// }
|
||||
// return [match.groups.level, match.groups.layer];
|
||||
// };
|
||||
|
||||
// const rcon = new Rcon({ port: "port", host: "ip", password: "password", passThrough: true, passThroughPort: "8124", passThroughTimeOut: 30000, passThroughChallenge: "password" }); //
|
||||
|
||||
// try {
|
||||
// await rcon.connect();
|
||||
// } catch (e) {
|
||||
// console.warn(e);
|
||||
// }
|
||||
|
||||
// rcon.interval = setInterval(async () => {
|
||||
|
||||
// try {
|
||||
// const currentMap = await getCurrentMap();
|
||||
// console.log(currentMap);
|
||||
// } catch (e) {
|
||||
// console.warn(e);
|
||||
// }
|
||||
// }, 5000);
|
||||
// };
|
||||
|
||||
// squadJsStyle();
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "SquadJS",
|
||||
"version": "3.8.1",
|
||||
"version": "3.8.2",
|
||||
"repository": "https://github.com/Team-Silver-Sphere/SquadJS.git",
|
||||
"author": "Thomas Smyth <https://github.com/Thomas-Smyth>",
|
||||
"license": "BSL-1.0",
|
||||
|
@ -1,7 +1,6 @@
|
||||
import EventEmitter from 'events';
|
||||
|
||||
import axios from 'axios';
|
||||
import Gamedig from 'gamedig';
|
||||
|
||||
import Logger from 'core/logger';
|
||||
import {SQUADJS_API_DOMAIN} from 'core/constants';
|
||||
@ -19,7 +18,7 @@ export default class SquadServer extends EventEmitter {
|
||||
constructor(options = {}) {
|
||||
super();
|
||||
|
||||
for (const option of ['host', 'queryPort'])
|
||||
for (const option of ['host'])
|
||||
if (!(option in options)) throw new Error(`${option} must be specified.`);
|
||||
|
||||
this.id = options.id;
|
||||
@ -74,11 +73,10 @@ 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();
|
||||
await this.updatePlayerList();
|
||||
await this.updatePlayerList(this);
|
||||
await this.updateLayerInformation();
|
||||
await this.updateA2SInformation();
|
||||
|
||||
@ -97,7 +95,10 @@ export default class SquadServer extends EventEmitter {
|
||||
host: this.options.rconHost || this.options.host,
|
||||
port: this.options.rconPort,
|
||||
password: this.options.rconPassword,
|
||||
autoReconnectInterval: this.options.rconAutoReconnectInterval
|
||||
autoReconnectInterval: this.options.rconAutoReconnectInterval,
|
||||
dumpRconResponsesToFile: this.options.dumpRconResponsesToFile,
|
||||
passThroughPort: this.options.rconPassThroughPort,
|
||||
passThrough: this.options.rconPassThrough
|
||||
});
|
||||
|
||||
this.rcon.on('CHAT_MESSAGE', async (data) => {
|
||||
@ -229,55 +230,74 @@ export default class SquadServer extends EventEmitter {
|
||||
});
|
||||
|
||||
this.logParser.on('PLAYER_CONNECTED', async (data) => {
|
||||
data.player = await this.getPlayerBySteamID(data.steamID);
|
||||
if (data.player) {
|
||||
data.player.suffix = data.playerSuffix;
|
||||
} else {
|
||||
data.player = {
|
||||
steamID: data.steamID,
|
||||
name: data.playerSuffix
|
||||
};
|
||||
Logger.verbose(
|
||||
'SquadServer',
|
||||
1,
|
||||
`Player connected ${data.playerSuffix} - SteamID: ${data.steamID} - EOSID: ${data.eosID}`
|
||||
);
|
||||
|
||||
this.rcon.addIds(data.steamID, data.eosID);
|
||||
|
||||
data.player = await this.getPlayerByEOSID(data.eosID);
|
||||
if (data.player) data.player.suffix = data.playerSuffix;
|
||||
else {
|
||||
data.player = {
|
||||
steamID: data.steamID,
|
||||
name: data.playerSuffix,
|
||||
eosID: data.eosID
|
||||
}
|
||||
}
|
||||
|
||||
delete data.steamID;
|
||||
delete data.playerSuffix;
|
||||
|
||||
this.emit('PLAYER_CONNECTED', data);
|
||||
});
|
||||
|
||||
this.logParser.on('PLAYER_DISCONNECTED', async (data) => {
|
||||
Logger.verbose('PlayerBugFix', 1, `player ${data.steamID} disconnect with playerinfo: ${JSON.stringify(Array.from(this.playerinfo.entries()))}`);
|
||||
data.player = await this.getPlayerBySteamID(data.steamID);
|
||||
if (!data.player) {
|
||||
Logger.verbose('PlayerBugFix', 1, `Bug detected, using playerinfo data for ${data.steamID}`);
|
||||
data.player = this.playerinfo.get(data.steamID);
|
||||
}
|
||||
if (!data.player) {
|
||||
Logger.verbose('PlayerBugFix', 1, `Bug detected, FAILED, falling back for ${data.steamID}`);
|
||||
data.player = {
|
||||
steamID: data.steamID
|
||||
};
|
||||
}
|
||||
this.playerinfo.delete(data.steamID);
|
||||
Logger.verbose('PlayerBugFix', 1, `player ${data.playerEOSID} disconnect with playerinfo: ${JSON.stringify(Array.from(this.playerinfo.entries()))}`);
|
||||
data.player = await this.getPlayerByEOSID(data.playerEOSID);
|
||||
|
||||
if(!data.player){
|
||||
Logger.verbose('PlayerBugFix', 1, `Bug detected, using playerinfo data for ${data.steamID}`);
|
||||
data.player = this.playerinfo.get(data.playerEOSID);
|
||||
}
|
||||
if (!data.player){
|
||||
Logger.verbose('PlayerBugFix', 1, `Bug detected, FAILED, falling back for ${data.steamID}`);
|
||||
data.player = {
|
||||
steamID: data.steamID,
|
||||
eosID: data.playerEOSID
|
||||
};
|
||||
}
|
||||
this.playerinfo.delete(data.playerEOSID);
|
||||
|
||||
this.emit('PLAYER_DISCONNECTED', data);
|
||||
});
|
||||
|
||||
this.logParser.on('PLAYER_DAMAGED', async (data) => {
|
||||
data.victim = await this.getPlayerByName(data.victimName);
|
||||
data.attacker = await this.getPlayerByName(data.attackerName);
|
||||
data.attacker = await this.getPlayerByEOSID(data.attackerEOSID);
|
||||
|
||||
if (data.victim && data.attacker)
|
||||
if (!data.attacker.playercontroller) data.attacker.playercontroller = data.attackerController;
|
||||
|
||||
if (data.victim && data.attacker) {
|
||||
if (!data.victim.playercontroller) data.victim.playercontroller = data.attackerController;
|
||||
data.teamkill =
|
||||
data.victim.teamID === data.attacker.teamID &&
|
||||
data.victim.steamID !== data.attacker.steamID;
|
||||
}
|
||||
|
||||
delete data.victimName;
|
||||
delete data.attackerName;
|
||||
|
||||
console.log('player damage', data);
|
||||
|
||||
this.emit('PLAYER_DAMAGED', data);
|
||||
});
|
||||
|
||||
this.logParser.on('PLAYER_WOUNDED', async (data) => {
|
||||
data.victim = await this.getPlayerByName(data.victimName);
|
||||
data.attacker = await this.getPlayerByName(data.attackerName);
|
||||
data.attacker = await this.getPlayerByEOSID(data.attackerEOSID);
|
||||
if (!data.attacker)
|
||||
data.attacker = await this.getPlayerByController(data.attackerPlayerController);
|
||||
|
||||
@ -291,9 +311,8 @@ export default class SquadServer extends EventEmitter {
|
||||
});
|
||||
|
||||
this.logParser.on('PLAYER_DIED', async (data) => {
|
||||
// console.log(data);
|
||||
data.victim = await this.getPlayerByName(data.victimName);
|
||||
data.attacker = await this.getPlayerByName(data.attackerName);
|
||||
data.attacker = await this.getPlayerByEOSID(data.attackerEOSID);
|
||||
if (!data.attacker)
|
||||
data.attacker = await this.getPlayerByController(data.attackerPlayerController);
|
||||
|
||||
@ -308,9 +327,9 @@ export default class SquadServer extends EventEmitter {
|
||||
});
|
||||
|
||||
this.logParser.on('PLAYER_REVIVED', async (data) => {
|
||||
data.victim = await this.getPlayerByName(data.victimName);
|
||||
data.attacker = await this.getPlayerByName(data.attackerName);
|
||||
data.reviver = await this.getPlayerByName(data.reviverName);
|
||||
data.victim = await this.getPlayerByEOSID(data.victimEOSID);
|
||||
data.attacker = await this.getPlayerByEOSID(data.attackerEOSID);
|
||||
data.reviver = await this.getPlayerByEOSID(data.reviverEOSID);
|
||||
|
||||
delete data.victimName;
|
||||
delete data.attackerName;
|
||||
@ -320,7 +339,7 @@ export default class SquadServer extends EventEmitter {
|
||||
});
|
||||
|
||||
this.logParser.on('PLAYER_POSSESS', async (data) => {
|
||||
data.player = await this.getPlayerByNameSuffix(data.playerSuffix);
|
||||
data.player = await this.getPlayerByEOSID(data.playerEOSID);
|
||||
if (data.player) data.player.possessClassname = data.possessClassname;
|
||||
if (data.player) data.player.characterClassname = data.characterClassname;
|
||||
|
||||
@ -330,7 +349,7 @@ export default class SquadServer extends EventEmitter {
|
||||
});
|
||||
|
||||
this.logParser.on('PLAYER_UNPOSSESS', async (data) => {
|
||||
data.player = await this.getPlayerByNameSuffix(data.playerSuffix);
|
||||
data.player = await this.getPlayerByEOSID(data.playerEOSID);
|
||||
|
||||
delete data.playerSuffix;
|
||||
|
||||
@ -369,6 +388,23 @@ export default class SquadServer extends EventEmitter {
|
||||
this.logParser.on('TICK_RATE', (data) => {
|
||||
this.emit('TICK_RATE', data);
|
||||
});
|
||||
|
||||
this.logParser.on('CLIENT_EXTERNAL_ACCOUNT_INFO', (data) => {
|
||||
this.rcon.addIds(data.steamID, data.eosID);
|
||||
});
|
||||
// this.logParser.on('CLIENT_CONNECTED', (data) => {
|
||||
// Logger.verbose("SquadServer", 1, `Client connected. Connection: ${data.connection} - SteamID: ${data.steamID}`)
|
||||
// })
|
||||
// this.logParser.on('CLIENT_LOGIN_REQUEST', (data) => {
|
||||
// Logger.verbose("SquadServer", 1, `Login request. ChainID: ${data.chainID} - Suffix: ${data.suffix} - EOSID: ${data.eosID}`)
|
||||
|
||||
// })
|
||||
// this.logParser.on('RESOLVED_EOS_ID', (data) => {
|
||||
// Logger.verbose("SquadServer", 1, `Resolved EOSID. ChainID: ${data.chainID} - Suffix: ${data.suffix} - EOSID: ${data.eosID}`)
|
||||
// })
|
||||
// this.logParser.on('ADDING_CLIENT_CONNECTION', (data) => {
|
||||
// Logger.verbose("SquadServer", 1, `Adding client connection`, data)
|
||||
// })
|
||||
}
|
||||
|
||||
async restartLogParser() {
|
||||
@ -407,13 +443,13 @@ export default class SquadServer extends EventEmitter {
|
||||
try {
|
||||
const oldPlayerInfo = new Map();
|
||||
for (const player of this.players) {
|
||||
oldPlayerInfo.set(player.steamID, player);
|
||||
oldPlayerInfo.set(player.eosID, player);
|
||||
}
|
||||
|
||||
const players = [];
|
||||
for (const player of await this.rcon.getListPlayers()) {
|
||||
for (const player of await this.rcon.getListPlayers(this)){
|
||||
players.push({
|
||||
...oldPlayerInfo.get(player.steamID),
|
||||
...oldPlayerInfo.get(player.eosID),
|
||||
...player,
|
||||
playercont: this.logParser.eventStore.players[player.steamID]
|
||||
? this.logParser.eventStore.players[player.steamID].controller
|
||||
@ -539,39 +575,47 @@ 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
|
||||
});
|
||||
// const data = await Gamedig.query({
|
||||
// type: 'squad',
|
||||
// host: this.options.host,
|
||||
// port: this.options.queryPort
|
||||
// });
|
||||
|
||||
// console.log(data);
|
||||
const rawData = await this.rcon.execute(`ShowServerInfo`);
|
||||
Logger.verbose('SquadServer', 3, `A2S raw data`, rawData);
|
||||
const data = JSON.parse(rawData);
|
||||
Logger.verbose('SquadServer', 2, `A2S data`, JSON.data);
|
||||
// Logger.verbose("SquadServer", 1, `A2S data`, JSON.stringify(data, null, 2))
|
||||
|
||||
const info = {
|
||||
raw: data.raw,
|
||||
serverName: data.name,
|
||||
raw: data,
|
||||
serverName: data.ServerName_s,
|
||||
|
||||
maxPlayers: parseInt(data.maxplayers),
|
||||
publicSlots: parseInt(data.raw.rules.NUMPUBCONN),
|
||||
reserveSlots: parseInt(data.raw.rules.NUMPRIVCONN),
|
||||
maxPlayers: parseInt(data.MaxPlayers),
|
||||
publicQueueLimit: parseInt(data.PublicQueueLimit_I),
|
||||
reserveSlots: parseInt(data.PlayerReserveCount_I),
|
||||
|
||||
a2sPlayerCount: parseInt(data.raw.rules.PlayerCount_i),
|
||||
publicQueue: parseInt(data.raw.rules.PublicQueue_i),
|
||||
reserveQueue: parseInt(data.raw.rules.ReservedQueue_i),
|
||||
playerCount: parseInt(data.PlayerCount_I),
|
||||
publicQueue: parseInt(data.PublicQueue_I),
|
||||
reserveQueue: parseInt(data.ReservedQueue_I),
|
||||
|
||||
matchTimeout: parseFloat(data.raw.rules.MatchTimeout_f),
|
||||
gameVersion: data.raw.version,
|
||||
currentLayer: data.map
|
||||
currentLayer: data.MapName_s,
|
||||
nextLayer: data.NextLayer_s,
|
||||
|
||||
teamOne: data.TeamOne_s?.replace(new RegExp(data.MapName_s, 'i'), '') || '',
|
||||
teamTwo: data.TeamTwo_s?.replace(new RegExp(data.MapName_s, 'i'), '') || '',
|
||||
|
||||
matchTimeout: parseFloat(data.MatchTimeout_d),
|
||||
gameVersion: data.GameVersion_s
|
||||
};
|
||||
|
||||
this.serverName = info.serverName;
|
||||
|
||||
this.maxPlayers = info.maxPlayers;
|
||||
this.publicSlots = info.publicSlots;
|
||||
this.publicSlots = info.maxPlayers - info.reserveSlots;
|
||||
this.reserveSlots = info.reserveSlots;
|
||||
|
||||
this.a2sPlayerCount = info.a2sPlayerCount;
|
||||
this.a2sPlayerCount = info.playerCount;
|
||||
this.publicQueue = info.publicQueue;
|
||||
this.reserveQueue = info.reserveQueue;
|
||||
|
||||
@ -594,6 +638,7 @@ export default class SquadServer extends EventEmitter {
|
||||
}
|
||||
|
||||
this.emit('UPDATED_A2S_INFORMATION', info);
|
||||
this.emit('UPDATED_SERVER_INFORMATION', info);
|
||||
} catch (err) {
|
||||
Logger.verbose('SquadServer', 1, 'Failed to update A2S information.', err);
|
||||
}
|
||||
@ -735,6 +780,10 @@ export default class SquadServer extends EventEmitter {
|
||||
return this.getPlayerByCondition((player) => player.steamID === steamID, forceUpdate);
|
||||
}
|
||||
|
||||
async getPlayerByEOSID(eosID, forceUpdate) {
|
||||
return this.getPlayerByCondition((player) => player.EOSID === eosID, forceUpdate);
|
||||
}
|
||||
|
||||
async getPlayerByName(name, forceUpdate) {
|
||||
return this.getPlayerByCondition((player) => player.name === name, forceUpdate);
|
||||
}
|
||||
|
20
squad-server/log-parser/adding-client-connection.js
Normal file
20
squad-server/log-parser/adding-client-connection.js
Normal file
@ -0,0 +1,20 @@
|
||||
export default {
|
||||
regex:
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: AddClientConnection: Added client connection: \[UNetConnection\] RemoteAddr: ([\d.]+):[0-9]+, Name: (EOSIpNetConnection_[0-9]+), Driver: GameNetDriver (EOSNetDriver_[0-9]+), IsServer: YES, PC: NULL, Owner: NULL, UniqueId: INVALID/,
|
||||
onMatch: (args, logParser) => {
|
||||
const data = {
|
||||
raw: args[0],
|
||||
time: args[1],
|
||||
chainID: args[2],
|
||||
// steamID: args[ 3 ],
|
||||
ip: args[3],
|
||||
connection: args[4],
|
||||
driver: args[5]
|
||||
};
|
||||
/* This is Called when unreal engine adds a client connection
|
||||
First Step in Adding a Player to server
|
||||
*/
|
||||
logParser.eventStore['last-connection'] = data;
|
||||
logParser.emit('ADDING_CLIENT_CONNECTION', data);
|
||||
}
|
||||
};
|
15
squad-server/log-parser/check-permission-resolve-eosid.js
Normal file
15
squad-server/log-parser/check-permission-resolve-eosid.js
Normal file
@ -0,0 +1,15 @@
|
||||
export default {
|
||||
regex:
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadCommon: SQCommonStatics Check Permissions, UniqueId:([\da-f]+)$/,
|
||||
onMatch: (args, logParser) => {
|
||||
const data = {
|
||||
raw: args[0],
|
||||
time: args[1],
|
||||
chainID: +args[2],
|
||||
eosID: args[3]
|
||||
};
|
||||
|
||||
logParser.eventStore.joinRequests[data.chainID].eosID = data.eosID;
|
||||
logParser.emit('RESOLVED_EOS_ID', { ...logParser.eventStore.joinRequests[data.chainID] });
|
||||
}
|
||||
};
|
21
squad-server/log-parser/client-external-account-info.js
Normal file
21
squad-server/log-parser/client-external-account-info.js
Normal file
@ -0,0 +1,21 @@
|
||||
export default {
|
||||
regex:
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]+)]LogEOS: Verbose: \[LogEOSConnect] FConnectClient::CacheExternalAccountInfo - ProductUserId: (?<eosId>[0-9a-f]{32}), AccountType: (\d), AccountId: (?<steamId>[0-9]{17}), DisplayName: <Redacted>/,
|
||||
onMatch: (args, logParser) => {
|
||||
const data = {
|
||||
raw: args[0],
|
||||
time: args[1],
|
||||
chainID: args[2],
|
||||
eosID: args.groups.eosId,
|
||||
steamID: args.groups.steamId
|
||||
};
|
||||
|
||||
logParser.eventStore.players[data.steamID] = {
|
||||
eosID: data.eosID,
|
||||
steamID: data.steamID
|
||||
};
|
||||
logParser.eventStore.playersEOS[data.eosID] = logParser.eventStore.players[data.steamID];
|
||||
|
||||
logParser.emit('CLIENT_EXTERNAL_ACCOUNT_INFO', data);
|
||||
}
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
export default {
|
||||
regex:
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: Login: NewPlayer: SteamNetConnection \/Engine\/Transient\.(SteamNetConnection_[0-9]+)/,
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: Login: NewPlayer: EOSIpNetConnection \/Engine\/Transient\.(EOSIpNetConnection_[0-9]+)/,
|
||||
onMatch: (args, logParser) => {
|
||||
const data = {
|
||||
raw: args[0],
|
||||
@ -13,7 +13,7 @@ export default {
|
||||
2nd Step in player connected path
|
||||
*/
|
||||
|
||||
logParser.eventStore['client-login'] = logParser.eventStore.clients[args[3]];
|
||||
logParser.eventStore.joinRequests[data.chainID].connection = data.connection;
|
||||
delete logParser.eventStore.clients[args[3]];
|
||||
logParser.emit('CLIENT_LOGIN', data);
|
||||
}
|
||||
|
@ -16,10 +16,16 @@ import RoundEnded from './round-ended.js';
|
||||
import RoundTickets from './round-tickets.js';
|
||||
import RoundWinner from './round-winner.js';
|
||||
import ServerTickRate from './server-tick-rate.js';
|
||||
import ClientConnected from './client-connected.js';
|
||||
import AddingClientConnection from './adding-client-connection.js';
|
||||
import ClientLogin from './client-login.js';
|
||||
import PendingConnectionDestroyed from './pending-connection-destroyed.js';
|
||||
import BadPlayerMovement from './BadPlayerMovement.js';
|
||||
import ClientExternalAccountInfo from './client-external-account-info.js';
|
||||
import SendingAuthResult from './sending-auth-result.js';
|
||||
import LoginRequest from './login-request.js';
|
||||
import JoinRequest from './join-request.js';
|
||||
import PlayerJoinSucceeded from './player-join-succeeded.js';
|
||||
import CheckPermissionResolveEosid from './check-permission-resolve-eosid.js';
|
||||
|
||||
export default class SquadLogParser extends LogParser {
|
||||
constructor(options) {
|
||||
@ -44,10 +50,17 @@ export default class SquadLogParser extends LogParser {
|
||||
RoundTickets,
|
||||
RoundWinner,
|
||||
ServerTickRate,
|
||||
ClientConnected,
|
||||
AddingClientConnection,
|
||||
ClientLogin,
|
||||
PendingConnectionDestroyed,
|
||||
BadPlayerMovement
|
||||
BadPlayerMovement,
|
||||
PendingConnectionDestroyed,
|
||||
ClientExternalAccountInfo,
|
||||
SendingAuthResult,
|
||||
LoginRequest,
|
||||
JoinRequest,
|
||||
PlayerJoinSucceeded,
|
||||
CheckPermissionResolveEosid
|
||||
];
|
||||
}
|
||||
}
|
||||
|
15
squad-server/log-parser/join-request.js
Normal file
15
squad-server/log-parser/join-request.js
Normal file
@ -0,0 +1,15 @@
|
||||
export default {
|
||||
regex: /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: Join request: .+\?Name=(.+)\?SplitscreenCount=\d$/,
|
||||
onMatch: (args, logParser) => {
|
||||
const data = {
|
||||
raw: args[0],
|
||||
time: args[1],
|
||||
chainID: +args[2],
|
||||
suffix: args[3]
|
||||
};
|
||||
|
||||
logParser.eventStore.joinRequests[data.chainID] = data;
|
||||
// console.log(logParser.eventStore.loginRequests[ data.chainID ])
|
||||
logParser.emit('CLIENT_JOIN_REQUEST', data);
|
||||
}
|
||||
};
|
17
squad-server/log-parser/login-request.js
Normal file
17
squad-server/log-parser/login-request.js
Normal file
@ -0,0 +1,17 @@
|
||||
export default {
|
||||
regex:
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: Login request: \?Name=(.+) userId: RedpointEOS:([\da-f]{32}) platform: RedpointEOS/,
|
||||
onMatch: (args, logParser) => {
|
||||
const data = {
|
||||
raw: args[0],
|
||||
time: args[1],
|
||||
chainID: +args[2],
|
||||
suffix: args[3],
|
||||
eosID: args[4]
|
||||
};
|
||||
|
||||
// logParser.eventStore.loginRequests[ data.chainID ] = data;
|
||||
// console.log(logParser.eventStore.loginRequests[ data.chainID ])
|
||||
logParser.emit('CLIENT_LOGIN_REQUEST', data);
|
||||
}
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
export default {
|
||||
regex:
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: UNetConnection::PendingConnectionLost\. \[UNetConnection\] RemoteAddr: ([0-9]{17}):[0-9]+, Name: (SteamNetConnection_[0-9]+), Driver: GameNetDriver (SteamNetDriver_[0-9]+), IsServer: YES, PC: NULL, Owner: NULL, UniqueId: (?:Steam:UNKNOWN \[.+\]|INVALID) bPendingDestroy=0/,
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: UNetConnection::PendingConnectionLost\. \[UNetConnection\] RemoteAddr: ([0-9a-f]{32}):[0-9]+, Name: (SteamNetConnection_[0-9]+), Driver: GameNetDriver (SteamNetDriver_[0-9]+), IsServer: YES, PC: NULL, Owner: NULL, UniqueId: (?:Steam:UNKNOWN \[.+\]|INVALID) bPendingDestroy=0/,
|
||||
onMatch: (args, logParser) => {
|
||||
const data = {
|
||||
raw: args[0],
|
||||
|
@ -1,27 +1,20 @@
|
||||
export default {
|
||||
regex: /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: Join succeeded: (.+)/,
|
||||
regex:
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: PostLogin: NewPlayer: BP_PlayerController_C .+PersistentLevel\.([^\s]+) \(IP: ([\d.]+) \| Online IDs: EOS: ([0-9a-f]{32}) steam: (\d+)\)/,
|
||||
onMatch: (args, logParser) => {
|
||||
const data = {
|
||||
raw: args[0],
|
||||
time: args[1],
|
||||
chainID: args[2],
|
||||
playerSuffix: args[3],
|
||||
steamID: logParser.eventStore['client-login'], // player connected
|
||||
controller: logParser.eventStore['player-controller'] // playercontroller connected
|
||||
chainID: +args[2],
|
||||
ip: args[4],
|
||||
eosID: args[5],
|
||||
steamID: args[6]
|
||||
};
|
||||
|
||||
delete logParser.eventStore['client-login'];
|
||||
delete logParser.eventStore['player-controller'];
|
||||
const joinRequestData = logParser.eventStore.joinRequests[+args[2]];
|
||||
data.connection = joinRequestData.connection;
|
||||
data.playerSuffix = joinRequestData.suffix;
|
||||
|
||||
// Handle Reconnecting players
|
||||
if (logParser.eventStore.disconnected[data.steamID]) {
|
||||
delete logParser.eventStore.disconnected[data.steamID];
|
||||
}
|
||||
logParser.emit('PLAYER_CONNECTED', data);
|
||||
logParser.eventStore.players[data.steamID] = {
|
||||
steamID: data.steamID,
|
||||
suffix: data.playerSuffix,
|
||||
controller: data.controller
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
export default {
|
||||
regex:
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: Player:(.+) ActualDamage=([0-9.]+) from (.+) caused by ([A-z_0-9-]+)_C/,
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: Player:(.+) ActualDamage=([0-9.]+) from (.+) \(Online IDs: EOS: ([0-9a-f]{32}) steam: (\d{17}) \| Player Controller ID: ([^ ]+)\)caused by ([A-z_0-9-]+)_C/,
|
||||
onMatch: (args, logParser) => {
|
||||
const data = {
|
||||
raw: args[0],
|
||||
@ -9,7 +9,10 @@ export default {
|
||||
victimName: args[3],
|
||||
damage: parseFloat(args[4]),
|
||||
attackerName: args[5],
|
||||
weapon: args[6]
|
||||
attackerEOSID: args[6],
|
||||
attackerSteamID: args[7],
|
||||
attackerController: args[8],
|
||||
weapon: args[9]
|
||||
};
|
||||
|
||||
logParser.eventStore.session[args[3]] = data;
|
||||
|
@ -1,6 +1,6 @@
|
||||
export default {
|
||||
regex:
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQSoldier::)?Die\(\): Player:(.+) KillingDamage=(?:-)*([0-9.]+) from ([A-z_0-9]+) caused by ([A-z_0-9-]+)_C/,
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQSoldier::)?Die\(\): Player:(.+) KillingDamage=(?:-)*([0-9.]+) from ([A-z_0-9]+) \(Online IDs: EOS: ([\w\d]{32}) steam: (\d{17}) \| Contoller ID: ([\w\d]+)\) caused by ([A-z_0-9-]+)_C/,
|
||||
onMatch: (args, logParser) => {
|
||||
const data = {
|
||||
...logParser.eventStore.session[args[3]],
|
||||
@ -11,11 +11,15 @@ export default {
|
||||
victimName: args[3],
|
||||
damage: parseFloat(args[4]),
|
||||
attackerPlayerController: args[5],
|
||||
weapon: args[6]
|
||||
attackerEOSID: args[6],
|
||||
attackerSteamID: args[7],
|
||||
weapon: args[9]
|
||||
};
|
||||
|
||||
logParser.eventStore.session[args[3]] = data;
|
||||
|
||||
console.log('Die', data);
|
||||
|
||||
logParser.emit('PLAYER_DIED', data);
|
||||
}
|
||||
};
|
||||
|
@ -1,16 +1,18 @@
|
||||
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]+/,
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: UChannel::Close: Sending CloseBunch\. ChIndex == [0-9]+\. Name: \[UChannel\] ChIndex: [0-9]+, Closing: [0-9]+ \[UNetConnection\] RemoteAddr: ([\d.]+):[\d]+, Name: EOSIpNetConnection_[0-9]+, Driver: GameNetDriver EOSNetDriver_[0-9]+, IsServer: YES, PC: ([^ ]+PlayerController_C_[0-9]+), Owner: [^ ]+PlayerController_C_[0-9]+, UniqueId: RedpointEOS:([\d\w]+)/,
|
||||
onMatch: (args, logParser) => {
|
||||
const data = {
|
||||
raw: args[0],
|
||||
time: args[1],
|
||||
chainID: args[2],
|
||||
steamID: args[3],
|
||||
playerController: args[4]
|
||||
ip: args[3],
|
||||
playerController: args[4],
|
||||
playerEOSID: args[5]
|
||||
};
|
||||
|
||||
logParser.eventStore.disconnected[data.steamID] = true;
|
||||
|
||||
logParser.emit('PLAYER_DISCONNECTED', data);
|
||||
}
|
||||
};
|
||||
|
28
squad-server/log-parser/player-join-succeeded.js
Normal file
28
squad-server/log-parser/player-join-succeeded.js
Normal file
@ -0,0 +1,28 @@
|
||||
export default {
|
||||
regex: /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: Join succeeded: (.+)/,
|
||||
onMatch: (args, logParser) => {
|
||||
const data = {
|
||||
raw: args[0],
|
||||
time: args[1],
|
||||
chainID: +args[2],
|
||||
playerSuffix: args[3]
|
||||
};
|
||||
|
||||
const joinRequestsData = { ...logParser.eventStore.joinRequests[data.chainID] };
|
||||
|
||||
data.eosID = joinRequestsData.eosID;
|
||||
data.controller = joinRequestsData.controller;
|
||||
data.steamID = `${logParser.eventStore.connectionIdToSteamID.get(joinRequestsData.connection)}`;
|
||||
|
||||
logParser.eventStore.connectionIdToSteamID.delete(joinRequestsData.connection);
|
||||
|
||||
delete logParser.eventStore.joinRequests[+data.chainID];
|
||||
|
||||
// Handle Reconnecting players
|
||||
if (logParser.eventStore.disconnected[data.steamID]) {
|
||||
delete logParser.eventStore.disconnected[data.steamID];
|
||||
}
|
||||
|
||||
logParser.emit('JOIN_SUCCEEDED', data);
|
||||
}
|
||||
};
|
@ -1,15 +1,16 @@
|
||||
export default {
|
||||
regex:
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQPlayerController::)?OnPossess\(\): PC=(.+) Pawn=(([A-z0-9_]+)_C_[0-9]+)/,
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQPlayerController::)?OnPossess\(\): PC=(.+) \(Online IDs: EOS: ([\w\d]{32}) steam: (\d{17})\) Pawn=([A-z0-9_]+)_C/,
|
||||
onMatch: (args, logParser) => {
|
||||
const data = {
|
||||
raw: args[0],
|
||||
time: args[1],
|
||||
chainID: args[2],
|
||||
playerSuffix: args[3],
|
||||
characterClassname: args[4],
|
||||
possessClassname: args[5],
|
||||
pawn: args[6]
|
||||
playerEOSID: args[4],
|
||||
playerSteamID: args[5],
|
||||
possessClassname: args[6],
|
||||
pawn: args[5]
|
||||
};
|
||||
|
||||
logParser.eventStore.session[args[3]] = args[2];
|
||||
|
@ -1,6 +1,7 @@
|
||||
export default {
|
||||
// the names are currently the wrong way around in these logs
|
||||
regex: /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: (.+) has revived (.+)\./,
|
||||
regex:
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: (.+) \(Online IDs: EOS: ([0-9a-f]{32}) steam: (\d{17})\) has revived (.+) \(Online IDs: EOS: ([0-9a-f]{32}) steam: (\d{17})\)\./,
|
||||
onMatch: (args, logParser) => {
|
||||
const data = {
|
||||
...logParser.eventStore.session[args[3]],
|
||||
@ -8,7 +9,11 @@ export default {
|
||||
time: args[1],
|
||||
chainID: args[2],
|
||||
reviverName: args[3],
|
||||
victimName: args[4]
|
||||
reviverEOSID: args[4],
|
||||
reviverSteamID: args[5],
|
||||
victimName: args[6],
|
||||
victimEOSID: args[7],
|
||||
victimSteamID: args[8]
|
||||
};
|
||||
|
||||
logParser.emit('PLAYER_REVIVED', data);
|
||||
|
@ -1,14 +1,16 @@
|
||||
export default {
|
||||
regex:
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQPlayerController::)?OnUnPossess\(\): PC=(.+)/,
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQPlayerController::)?OnUnPossess\(\): PC=(.+) \(Online IDs: EOS: ([\w\d]{32}) steam: (\d{17})\)/,
|
||||
onMatch: (args, logParser) => {
|
||||
const data = {
|
||||
raw: args[0],
|
||||
time: args[1],
|
||||
chainID: args[2],
|
||||
playerSuffix: args[3],
|
||||
playerEOSID: args[4],
|
||||
playerSteamID: args[5],
|
||||
switchPossess:
|
||||
args[3] in logParser.eventStore.session && logParser.eventStore.session[args[3]] === args[2]
|
||||
args[4] in logParser.eventStore.session && logParser.eventStore.session[args[4]] === args[2]
|
||||
};
|
||||
|
||||
delete logParser.eventStore.session[args[3]];
|
||||
|
@ -1,6 +1,6 @@
|
||||
export default {
|
||||
regex:
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQSoldier::)?Wound\(\): Player:(.+) KillingDamage=(?:-)*([0-9.]+) from ([A-z_0-9]+) caused by ([A-z_0-9-]+)_C/,
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQSoldier::)?Wound\(\): Player:(.+) KillingDamage=(?:-)*([0-9.]+) from ([A-z_0-9]+) \(Online IDs: EOS: ([\w\d]{32}) steam: (\d{17}) \| Controller ID: ([\w\d]+)\) caused by ([A-z_0-9-]+)_C/,
|
||||
onMatch: (args, logParser) => {
|
||||
const data = {
|
||||
...logParser.eventStore.session[args[3]],
|
||||
@ -10,7 +10,9 @@ export default {
|
||||
victimName: args[3],
|
||||
damage: parseFloat(args[4]),
|
||||
attackerPlayerController: args[5],
|
||||
weapon: args[6]
|
||||
attackerEOSID: args[6],
|
||||
attackerSteamID: args[7],
|
||||
weapon: args[9]
|
||||
};
|
||||
|
||||
logParser.eventStore.session[args[3]] = data;
|
||||
|
@ -5,12 +5,11 @@ export default {
|
||||
const data = {
|
||||
raw: args[0],
|
||||
time: args[1],
|
||||
chainID: args[2],
|
||||
chainID: +args[2],
|
||||
controller: args[3]
|
||||
};
|
||||
|
||||
logParser.eventStore['player-controller'] = args[3];
|
||||
|
||||
logParser.eventStore.joinRequests[data.chainID].controller = data.controller;
|
||||
logParser.emit('PLAYER_CONTROLLER_CONNECTED', data);
|
||||
}
|
||||
};
|
||||
|
21
squad-server/log-parser/sending-auth-result.js
Normal file
21
squad-server/log-parser/sending-auth-result.js
Normal file
@ -0,0 +1,21 @@
|
||||
export default {
|
||||
regex:
|
||||
/^\[([0-9.:-]+)]\[([ 0-9]*)]LogOnline: STEAM: AUTH HANDLER: Sending auth result to user (\d{17}) with flag success\? 1/,
|
||||
onMatch: (args, logParser) => {
|
||||
if (!logParser.eventStore['last-connection']) return;
|
||||
|
||||
const data = {
|
||||
...logParser.eventStore['last-connection'],
|
||||
steamID: args[3]
|
||||
};
|
||||
/* This is Called when unreal engine adds a client connection
|
||||
First Step in Adding a Player to server
|
||||
*/
|
||||
|
||||
logParser.eventStore.clients[data.connection] = data.steamID;
|
||||
logParser.eventStore.connectionIdToSteamID.set(data.connection, data.steamID);
|
||||
logParser.emit('CLIENT_CONNECTED', data);
|
||||
|
||||
delete logParser.eventStore['last-connection'];
|
||||
}
|
||||
};
|
@ -17,7 +17,7 @@ export default class AutoKickUnassigned extends BasePlugin {
|
||||
warningMessage: {
|
||||
required: false,
|
||||
description: 'Message SquadJS will send to players warning them they will be kicked',
|
||||
default: 'Join a squad, you are are unassigned and will be kicked'
|
||||
default: 'Join a squad, you are unassigned and will be kicked'
|
||||
},
|
||||
kickMessage: {
|
||||
required: false,
|
||||
|
91
squad-server/plugins/persistent-eosid-to-steamid.js
Normal file
91
squad-server/plugins/persistent-eosid-to-steamid.js
Normal file
@ -0,0 +1,91 @@
|
||||
import Sequelize from 'sequelize';
|
||||
|
||||
import BasePlugin from './base-plugin.js';
|
||||
|
||||
const { DataTypes } = Sequelize;
|
||||
|
||||
export default class PersistentEOSIDtoSteamID extends BasePlugin {
|
||||
static get description() {
|
||||
return 'Stores into a DB every association of SteamID-EOSID';
|
||||
}
|
||||
|
||||
static get defaultEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static get optionsSpecification() {
|
||||
return {
|
||||
database: {
|
||||
required: true,
|
||||
connector: 'sequelize',
|
||||
description: 'The Sequelize connector.',
|
||||
default: 'sqlite'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
constructor(server, options, connectors) {
|
||||
super(server, options, connectors);
|
||||
|
||||
this.models = {};
|
||||
|
||||
this.createModel(
|
||||
'SteamIDtoEOSID',
|
||||
{
|
||||
steamID: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true
|
||||
},
|
||||
eosID: {
|
||||
type: DataTypes.STRING
|
||||
}
|
||||
},
|
||||
{
|
||||
charset: 'utf8mb4',
|
||||
collate: 'utf8mb4_unicode_ci'
|
||||
}
|
||||
);
|
||||
|
||||
this.onPlayerConnected = this.onPlayerConnected.bind(this);
|
||||
}
|
||||
|
||||
createModel(name, schema) {
|
||||
this.models[name] = this.options.database.define(`EOS_${name}`, schema, {
|
||||
timestamps: false,
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['eosID']
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
async prepareToMount() {
|
||||
await this.models.SteamIDtoEOSID.sync();
|
||||
}
|
||||
|
||||
async mount() {
|
||||
this.server.on('PLAYER_CONNECTED', this.onPlayerConnected);
|
||||
this.verbose(1, 'Mounted');
|
||||
}
|
||||
|
||||
async unmount() {
|
||||
this.server.removeEventListener('PLAYER_CONNECTED', this.onPlayerConnected);
|
||||
}
|
||||
|
||||
async onPlayerConnected(info) {
|
||||
await this.models.SteamIDtoEOSID.upsert({
|
||||
steamID: info.player.steamID,
|
||||
eosID: info.eosID
|
||||
});
|
||||
}
|
||||
|
||||
async getByEOSID(eosID) {
|
||||
return await this.models.SteamIDtoEOSID.findOne({ where: { eosID: eosID } });
|
||||
}
|
||||
|
||||
async getBySteamID(steamID) {
|
||||
return await this.models.SteamIDtoEOSID.findOne({ where: { steamID: steamID } });
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ import Rcon from 'core/rcon';
|
||||
export default class SquadRcon extends Rcon {
|
||||
processChatPacket(decodedPacket) {
|
||||
const matchChat = decodedPacket.body.match(
|
||||
/\[(ChatAll|ChatTeam|ChatSquad|ChatAdmin)] \[SteamID:([0-9]{17})] (.+?) : (.*)/
|
||||
/\[(ChatAll|ChatTeam|ChatSquad|ChatAdmin)] \[Online IDs:EOS: ([0-9a-f]{32}) steam: (\d{17})\] (.+?) : (.*)/
|
||||
);
|
||||
if (matchChat) {
|
||||
Logger.verbose('SquadRcon', 2, `Matched chat message: ${decodedPacket.body}`);
|
||||
@ -12,9 +12,10 @@ export default class SquadRcon extends Rcon {
|
||||
this.emit('CHAT_MESSAGE', {
|
||||
raw: decodedPacket.body,
|
||||
chat: matchChat[1],
|
||||
steamID: matchChat[2],
|
||||
name: matchChat[3],
|
||||
message: matchChat[4],
|
||||
eosID: matchChat[2],
|
||||
steamID: matchChat[3],
|
||||
name: matchChat[4],
|
||||
message: matchChat[5],
|
||||
time: new Date()
|
||||
});
|
||||
|
||||
@ -22,14 +23,14 @@ export default class SquadRcon extends Rcon {
|
||||
}
|
||||
|
||||
const matchPossessedAdminCam = decodedPacket.body.match(
|
||||
/\[SteamID:([0-9]{17})] (.+?) has possessed admin camera./
|
||||
/\[Online Ids:EOS: ([0-9a-f]{32}) steam: (\d{17})\] (.+) has possessed admin camera\./
|
||||
);
|
||||
if (matchPossessedAdminCam) {
|
||||
Logger.verbose('SquadRcon', 2, `Matched admin camera possessed: ${decodedPacket.body}`);
|
||||
this.emit('POSSESSED_ADMIN_CAMERA', {
|
||||
raw: decodedPacket.body,
|
||||
steamID: matchPossessedAdminCam[1],
|
||||
name: matchPossessedAdminCam[2],
|
||||
steamID: matchPossessedAdminCam[2],
|
||||
name: matchPossessedAdminCam[3],
|
||||
time: new Date()
|
||||
});
|
||||
|
||||
@ -37,14 +38,14 @@ export default class SquadRcon extends Rcon {
|
||||
}
|
||||
|
||||
const matchUnpossessedAdminCam = decodedPacket.body.match(
|
||||
/\[SteamID:([0-9]{17})] (.+?) has unpossessed admin camera./
|
||||
/\[Online IDs:EOS: ([0-9a-f]{32}) steam: (\d{17})\] (.+) has unpossessed admin camera\./
|
||||
);
|
||||
if (matchUnpossessedAdminCam) {
|
||||
Logger.verbose('SquadRcon', 2, `Matched admin camera possessed: ${decodedPacket.body}`);
|
||||
this.emit('UNPOSSESSED_ADMIN_CAMERA', {
|
||||
raw: decodedPacket.body,
|
||||
steamID: matchUnpossessedAdminCam[1],
|
||||
name: matchUnpossessedAdminCam[2],
|
||||
steamID: matchUnpossessedAdminCam[2],
|
||||
name: matchUnpossessedAdminCam[3],
|
||||
time: new Date()
|
||||
});
|
||||
|
||||
@ -68,7 +69,7 @@ export default class SquadRcon extends Rcon {
|
||||
}
|
||||
|
||||
const matchKick = decodedPacket.body.match(
|
||||
/Kicked player ([0-9]+)\. \[steamid=([0-9]{17})] (.*)/
|
||||
/Kicked player ([0-9]+)\. \[Online IDs= EOS: ([0-9a-f]{32}) steam: (\d{17})] (.*)/
|
||||
);
|
||||
if (matchKick) {
|
||||
Logger.verbose('SquadRcon', 2, `Matched kick message: ${decodedPacket.body}`);
|
||||
@ -76,8 +77,8 @@ export default class SquadRcon extends Rcon {
|
||||
this.emit('PLAYER_KICKED', {
|
||||
raw: decodedPacket.body,
|
||||
playerID: matchKick[1],
|
||||
steamID: matchKick[2],
|
||||
name: matchKick[3],
|
||||
steamID: matchKick[3],
|
||||
name: matchKick[4],
|
||||
time: new Date()
|
||||
});
|
||||
|
||||
@ -85,7 +86,7 @@ export default class SquadRcon extends Rcon {
|
||||
}
|
||||
|
||||
const matchSqCreated = decodedPacket.body.match(
|
||||
/(.+) \(Steam ID: ([0-9]{17})\) has created Squad (\d+) \(Squad Name: (.+)\) on (.+)/
|
||||
/(.+) \(Online IDs: EOS: ([0-9a-f]{32}) steam: (\d{17})\) has created Squad (\d+) \(Squad Name: (.+)\) on (.+)/
|
||||
);
|
||||
if (matchSqCreated) {
|
||||
Logger.verbose('SquadRcon', 2, `Matched Squad Created: ${decodedPacket.body}`);
|
||||
@ -93,10 +94,10 @@ export default class SquadRcon extends Rcon {
|
||||
this.emit('SQUAD_CREATED', {
|
||||
time: new Date(),
|
||||
playerName: matchSqCreated[1],
|
||||
playerSteamID: matchSqCreated[2],
|
||||
squadID: matchSqCreated[3],
|
||||
squadName: matchSqCreated[4],
|
||||
teamName: matchSqCreated[5]
|
||||
playerSteamID: matchSqCreated[3],
|
||||
squadID: matchSqCreated[4],
|
||||
squadName: matchSqCreated[5],
|
||||
teamName: matchSqCreated[6]
|
||||
});
|
||||
|
||||
return;
|
||||
@ -134,27 +135,31 @@ export default class SquadRcon extends Rcon {
|
||||
};
|
||||
}
|
||||
|
||||
async getListPlayers() {
|
||||
async getListPlayers(server) {
|
||||
const response = await this.execute('ListPlayers');
|
||||
|
||||
const players = [];
|
||||
|
||||
if(!response || response.length < 1) return 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: (.+)/
|
||||
/ID: ([0-9]+) \| Online IDs: EOS: ([0-9a-f]{32}) steam: (\d{17}) \| Name: (.+) \| Team ID: ([0-9]+) \| Squad ID: ([0-9]+|N\/A) \| Is Leader: (True|False) \| Role: ([A-Za-z0-9_]*)\b/
|
||||
);
|
||||
if (!match) continue;
|
||||
|
||||
server.rcon.addIds(match[3], match[2]);
|
||||
players.push({
|
||||
playerID: match[1],
|
||||
steamID: match[2],
|
||||
name: match[3],
|
||||
teamID: match[4],
|
||||
squadID: match[5] !== 'N/A' ? match[5] : null,
|
||||
isSquadLeader: match[6] === 'True',
|
||||
rconRole: match[7]
|
||||
EOSID: match[2],
|
||||
steamID: match[3],
|
||||
name: match[4],
|
||||
teamID: match[5],
|
||||
squadID: match[6] !== 'N/A' ? match[5] : null,
|
||||
isLeader: match[7] === 'True',
|
||||
isSquadLeader: match[7] === 'True',
|
||||
role: match[8],
|
||||
rconRole: match[8]
|
||||
});
|
||||
}
|
||||
|
||||
@ -168,11 +173,11 @@ export default class SquadRcon extends Rcon {
|
||||
let teamName;
|
||||
let teamID;
|
||||
|
||||
if(!responseSquad || responseSquad.length < 1) return squads;
|
||||
if (!responseSquad || responseSquad.length < 1) return squads;
|
||||
|
||||
for (const line of responseSquad.split('\n')) {
|
||||
const match = line.match(
|
||||
/ID: ([0-9]+) \| Name: (.+) \| Size: ([0-9]+) \| Locked: (True|False) \| Creator Name: (.+) \| Creator Steam ID: ([0-9]{17})/
|
||||
/ID: ([0-9]+) \| Name: (.+) \| Size: ([0-9]+) \| Locked: (True|False) \| Creator Name: (.+) \| Creator Online IDs: EOS: ([0-9a-f]{32}) steam: (\d{17})/
|
||||
);
|
||||
const matchSide = line.match(/Team ID: (1|2) \((.+)\)/);
|
||||
if (matchSide) {
|
||||
@ -186,7 +191,7 @@ export default class SquadRcon extends Rcon {
|
||||
size: match[3],
|
||||
locked: match[4],
|
||||
creatorName: match[5],
|
||||
creatorSteamID: match[6],
|
||||
creatorSteamID: match[7],
|
||||
teamID: teamID,
|
||||
teamName: teamName
|
||||
});
|
||||
|
@ -5,6 +5,9 @@
|
||||
"queryPort": 27165,
|
||||
"rconPort": 21114,
|
||||
"rconPassword": "password",
|
||||
"rconPassThrough": true,
|
||||
"rconPassThroughPort": 8124,
|
||||
"dumpRconResponsesToFile": false,
|
||||
"logReaderMode": "tail",
|
||||
"logDir": "C:/path/to/squad/log/folder",
|
||||
"ftp": {
|
||||
|
Loading…
Reference in New Issue
Block a user