mirror of
https://github.com/AsgardEternal/SquadJS.git
synced 2024-09-28 06:04:23 -05:00
SquadJS v1
This commit is contained in:
commit
3eb53c2ada
4
.eslintignore
Normal file
4
.eslintignore
Normal file
@ -0,0 +1,4 @@
|
||||
# General
|
||||
**/node_modules/*
|
||||
|
||||
index-test.js
|
9
.eslintrc
Normal file
9
.eslintrc
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018
|
||||
},
|
||||
"extends": [
|
||||
"standard",
|
||||
"prettier"
|
||||
]
|
||||
}
|
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
# Project Files
|
||||
squad-server/log-parser/test-data/
|
||||
|
||||
index-test.js
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# IDEs
|
||||
.idea/
|
7
.lintstagedrc
Normal file
7
.lintstagedrc
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"*.js": [
|
||||
"eslint --fix .",
|
||||
"prettier --write \"./**/*.js\"",
|
||||
"git add"
|
||||
]
|
||||
}
|
3
.prettierrc
Normal file
3
.prettierrc
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"singleQuote": true
|
||||
}
|
25
LICENSE
Normal file
25
LICENSE
Normal file
@ -0,0 +1,25 @@
|
||||
Boost Software License - Version 1.0 - August 17th, 2003
|
||||
|
||||
Copyright (c) 2020 Thomas Smyth
|
||||
|
||||
Permission is hereby granted, free of charge, to any person or organization
|
||||
obtaining a copy of the software and accompanying documentation covered by
|
||||
this license (the "Software") to use, reproduce, display, distribute,
|
||||
execute, and transmit the Software, and to prepare derivative works of the
|
||||
Software, and to permit third-parties to whom the Software is furnished to
|
||||
do so, all subject to the following:
|
||||
|
||||
The copyright notices in the Software and this entire statement, including
|
||||
the above license grant, this restriction and the following disclaimer,
|
||||
must be included in all copies of the Software, in whole or in part, and
|
||||
all derivative works of the Software, unless such copies or derivative
|
||||
works are solely in the form of machine-executable object code generated by
|
||||
a source language processor.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
142
README.md
Normal file
142
README.md
Normal file
@ -0,0 +1,142 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="assets/squadjs-logo.png" alt="Logo" width="500"/>
|
||||
|
||||
#### SquadJS
|
||||
|
||||
[![GitHub release](https://img.shields.io/github/release/Thomas-Smyth/SquadJS.svg?style=flat-square)](https://github.com/Thomas-Smyth/SquadJS/releases)
|
||||
[![GitHub contributors](https://img.shields.io/github/contributors/Thomas-Smyth/SquadJS.svg?style=flat-square)](https://github.com/Thomas-Smyth/SquadJS/graphs/contributors)
|
||||
[![GitHub release](https://img.shields.io/github/license/Thomas-Smyth/SquadJS.svg?style=flat-square)](https://github.com/Thomas-Smyth/SquadJS/blob/master/LICENSE)
|
||||
|
||||
<br>
|
||||
|
||||
[![GitHub issues](https://img.shields.io/github/issues/Thomas-Smyth/SquadJS.svg?style=flat-square)](https://github.com/Thomas-Smyth/SquadJS/issues)
|
||||
[![GitHub pull requests](https://img.shields.io/github/issues-pr-raw/Thomas-Smyth/SquadJS.svg?style=flat-square)](https://github.com/Thomas-Smyth/SquadJS/pulls)
|
||||
[![GitHub issues](https://img.shields.io/github/stars/Thomas-Smyth/SquadJS.svg?style=flat-square)](https://github.com/Thomas-Smyth/SquadJS/stargazers)
|
||||
[![Discord](https://img.shields.io/discord/266210223406972928.svg?style=flat-square&logo=discord)](https://discord.gg/P7uUp5Y)
|
||||
|
||||
<br><br>
|
||||
</div>
|
||||
|
||||
## About
|
||||
SquadJS is a scripting framework, designed for Squad servers, that aims to handle all communication and data collection to and from the servers. Using SquadJS as the base to any of your scripting projects allows you to easily write complex plugins without having to worry about the hassle of RCON or log parsing. However, for your convenience SquadJS comes shipped with multiple plugins already built for you allowing you to experience the power of SquadJS right away.
|
||||
|
||||
## Using SquadJS
|
||||
SquadJS relies on being able to access the Squad server log directory in order to parse logs live to collect information. Thus, SquadJS must be hosted on the same server box as your Squad server.
|
||||
|
||||
### Prerequisites
|
||||
* [Node.js](https://nodejs.org/en/) (Current) - [Download](https://nodejs.org/en/)
|
||||
* [Yarn](https://yarnpkg.com/) (Version 1.22.0+) - [Download](https://classic.yarnpkg.com/en/docs/install)
|
||||
* Some plugins may have additional requirements.
|
||||
|
||||
### Installation
|
||||
1. Clone the repository: `git clone https://github.com/Thomas-Smyth/SquadJS`
|
||||
2. Install the dependencies: `yarn install`
|
||||
3. Configure the `index.js` file with your server information and the required plugins. Documentation for each plugin can be found in the [`plugins`](https://github.com/Thomas-Smyth/SquadJS/tree/master/plugins) folder.
|
||||
4. Start SquadJS: `node index.js`.
|
||||
|
||||
## Plugins
|
||||
* [Discord Admin Cam Logs](https://github.com/Thomas-Smyth/SquadJS/tree/master/plugins/discord-admin-cam-logs) - Log admin cam usage to Discord.
|
||||
* [Discord Chat](https://github.com/Thomas-Smyth/SquadJS/tree/master/plugins/discord-chat) - Log in game chat to Discord.
|
||||
* [Discord Teamkill](https://github.com/Thomas-Smyth/SquadJS/tree/master/plugins/discord-teamkill) - Log teamkills to Discord.
|
||||
* [Map Vote](https://github.com/Thomas-Smyth/SquadJS/tree/master/plugins/mapvote) - In-game chat map voting system.
|
||||
* [InfluxDB Log](https://github.com/Thomas-Smyth/SquadJS/tree/master/plugins/influxdb-log) - Log server and player stats to InfluxDB.
|
||||
* [MySQL Log](https://github.com/Thomas-Smyth/SquadJS/tree/master/plugins/mysql-log) - Log more in-depth server and player stats to MySQL.
|
||||
* [Seeding Message](https://github.com/Thomas-Smyth/SquadJS/tree/master/plugins/seeding-message) - Display seeding messages for seeding mode.
|
||||
* [Team Randomizer](https://github.com/Thomas-Smyth/SquadJS/tree/master/plugins/team-randomizer) - Randomize teams to help with team balance.
|
||||
|
||||
## Creating Your Own Plugins
|
||||
To create your own plugin you need a basic knowledge of JavaScript.
|
||||
|
||||
Typical plugins are functions that take the server as an argument in order to allow the plugin to access information about the server or manipulate it in some way:
|
||||
```js
|
||||
function aPluginToLogServerID(server){
|
||||
console.log(server.id);
|
||||
}
|
||||
```
|
||||
|
||||
Stored in the server object are a range of different properties that store information about the server.
|
||||
* `id` - ID of the server.
|
||||
* `serverName` - Name of the server.
|
||||
* `maxPlayers` - Maximum number of players on the server.
|
||||
* `publicSlots` - Maximum number of public slots.
|
||||
* `reserveSlots` - Maximum number of reserved slots.
|
||||
* `publicQueue` - Length of the public queue.
|
||||
* `reserveQueue` - Length of the reserved queue.
|
||||
* `matchTimeout` - Time until match ends?
|
||||
* `gameVersion` - Game version.
|
||||
* `layerHistory` - Array history of layers used with most recent at the start. Each entry is an object with layer info in.
|
||||
* `currentLayer` - The current layer.
|
||||
* `nextLayer` - The next layer.
|
||||
* `players` - Array of players. Each entry is a PlayerObject with various bits of info in.
|
||||
|
||||
One approach to making a plugin would be to run an action periodically, in the style of the original SquadJS:
|
||||
```js
|
||||
function aPluginToLogPlayerCountEvery60Seconds(server){
|
||||
setInterval(() => {
|
||||
console.log(server.players.length);
|
||||
}, 60 * 1000);
|
||||
}
|
||||
```
|
||||
|
||||
A more common approach in this version of SquadJS is to react to an event happening:
|
||||
```js
|
||||
function aPluginToLogTeamkills(server){
|
||||
server.on(LOG_PARSER_TEAMKILL, info => {
|
||||
console.log(info);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
A complete list of events that you can listen for and the information included within each is found [here](https://github.com/Thomas-Smyth/SquadJS/blob/master/squad-server/events/log-parser.js), [here](https://github.com/Thomas-Smyth/SquadJS/blob/master/squad-server/events/rcon.js) and [here](https://github.com/Thomas-Smyth/SquadJS/blob/master/squad-server/events/server.js).
|
||||
|
||||
Various actions can be completed in a plugin. Most of these will involve outside system, e.g. Discord.js to run a Discord bot, so they are not documented here. However, you may run RCON commands using `server.rcon.execute("Command");`.
|
||||
|
||||
If you're struggling to create a plugin, the existing [`plugins`](https://github.com/Thomas-Smyth/SquadJS/tree/master/plugins) are a good place to go for examples or feel free to ask for help in the Squad RCON Discord.
|
||||
|
||||
## Statement on Accuracy
|
||||
Some of the information SquadJS collects from Squad servers was never intended or designed to be collected. As a result, it is impossible for any framework to collect the same information with 100% accuracy. SquadJS aims to get as close as possible to that figure, however, it acknowledges that this is not possible in some specific scenarios.
|
||||
|
||||
Below is a list of scenarios we know may cause some information to be inaccurate:
|
||||
* Use of Realtime Server and Player Information - We update server and player information periodically every 30 seconds (by default) or when we know that it requires an update. As a result, some information about the server or players may be up to 30 seconds out of date.
|
||||
* SquadJS Restarts - If SquadJS is started during an active Squad game some information will not be lost or not collected correctly:
|
||||
- The current state of players will be lost. For example, if a player was wounded prior to the bot starting and then is revived/gives up after the bot is started information regarding who originally wounded them will not be known.
|
||||
- The accurate collection of some server log events will not occur. SquadJS collects players' "suffix" name, i.e. their Steam name without the clan tag added via the game settings, when they join the server and uses this to identify them in certain logs that do not include their full name. As a result, for players connecting prior to SquadJS starting some log events associated with their actions will show the player as `null`. We aim to implement a solution to attempt to recover players' suffix names when this occurs, but the accuracy of correctly identifying players will be decreased.
|
||||
* Duplicated Player Names - If two or more players have the same name or suffix name (see above) then SquadJS will be unable to identify them in the logs. When this occurs event logs will show the player as `null`. Be on the watch for groups of players who try to abuse this in order to TK or complete other malicious actions without being detected by SquadJS plugins.
|
||||
|
||||
## Credits
|
||||
SquadJS would not be possible without the support of so many individuals and organisations. My thanks goes out to:
|
||||
* subtlerod for proposing the initial log parsing idea, helping to design the log parsing process and for providing multiple servers to test with.
|
||||
* Fourleaf, Mex and various other members of ToG / ToG-L for helping to stage logs and participate in small scale tests.
|
||||
* The Coalition community, including those that participate in Wednesday Fight Night, for participating in larger scale tests and for providing feedback on plugins.
|
||||
* iDronee for providing Linux Squad server logs to ensure log parsing regexes support the OS.
|
||||
* Everyone in the Squad RCON Discord and others who have submitted bug reports, suggestions and feedback.
|
||||
|
||||
## License
|
||||
```
|
||||
Boost Software License - Version 1.0 - August 17th, 2003
|
||||
|
||||
Copyright (c) 2020 Thomas Smyth
|
||||
|
||||
Permission is hereby granted, free of charge, to any person or organization
|
||||
obtaining a copy of the software and accompanying documentation covered by
|
||||
this license (the "Software") to use, reproduce, display, distribute,
|
||||
execute, and transmit the Software, and to prepare derivative works of the
|
||||
Software, and to permit third-parties to whom the Software is furnished to
|
||||
do so, all subject to the following:
|
||||
|
||||
The copyright notices in the Software and this entire statement, including
|
||||
the above license grant, this restriction and the following disclaimer,
|
||||
must be included in all copies of the Software, in whole or in part, and
|
||||
all derivative works of the Software, unless such copies or derivative
|
||||
works are solely in the form of machine-executable object code generated by
|
||||
a source language processor.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
```
|
4
assets/package.json
Normal file
4
assets/package.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "assets",
|
||||
"version": "1.0.0"
|
||||
}
|
BIN
assets/squadjs logo black.xcf
Normal file
BIN
assets/squadjs logo black.xcf
Normal file
Binary file not shown.
BIN
assets/squadjs logo white.xcf
Normal file
BIN
assets/squadjs logo white.xcf
Normal file
Binary file not shown.
BIN
assets/squadjs-logo-square-white.png
Normal file
BIN
assets/squadjs-logo-square-white.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 135 KiB |
BIN
assets/squadjs-logo-white.png
Normal file
BIN
assets/squadjs-logo-white.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 130 KiB |
BIN
assets/squadjs-logo.png
Normal file
BIN
assets/squadjs-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 124 KiB |
12
connectors/package.json
Normal file
12
connectors/package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "connectors",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./squad-layers": "./squad-layers/index.js",
|
||||
"./scbl": "./scbl.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"graphql-request": "^1.8.2"
|
||||
}
|
||||
}
|
7
connectors/scbl.js
Normal file
7
connectors/scbl.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { request } from 'graphql-request';
|
||||
|
||||
const API_ENDPOINT = 'https://squad-community-ban-list.com/graphql';
|
||||
|
||||
export default function(query, variables) {
|
||||
return request(API_ENDPOINT, query, variables);
|
||||
}
|
117
connectors/squad-layers/index.js
Normal file
117
connectors/squad-layers/index.js
Normal file
@ -0,0 +1,117 @@
|
||||
import fs from 'fs';
|
||||
|
||||
class SquadLayers {
|
||||
constructor() {
|
||||
this.layers = JSON.parse(
|
||||
fs.readFileSync('./connectors/squad-layers/layers.json', 'utf8')
|
||||
);
|
||||
}
|
||||
|
||||
getLayerByLayerName(layerName) {
|
||||
const layer = this.layers.filter(layer => layer.layer === layerName);
|
||||
return layer.length === 1 ? layer[0] : null;
|
||||
}
|
||||
|
||||
getLayerByLayerClassname(layerClassname) {
|
||||
const layer = this.layers.filter(
|
||||
layer => layer.layerClassname === layerClassname
|
||||
);
|
||||
return layer.length === 1 ? layer[0] : null;
|
||||
}
|
||||
|
||||
getLayerNames() {
|
||||
return this.layers.map(layer => layer.layer);
|
||||
}
|
||||
|
||||
getFilteredLayers(filter = {}) {
|
||||
const whitelistedLayers = filter.whitelistedLayers || null;
|
||||
const blacklistedLayers = filter.blacklistedLayers || null;
|
||||
const whitelistedMaps = filter.whitelistedMaps || null;
|
||||
const blacklistedMaps = filter.blacklistedMaps || null;
|
||||
const whitelistedGamemodes = filter.whitelistedGamemodes || null;
|
||||
const blacklistedGamemodes = filter.blacklistedGamemodes || ['Training'];
|
||||
const flagCountMin = filter.flagCountMin || null;
|
||||
const flagCountMax = filter.flagCountMax || null;
|
||||
const hasCommander = filter.hasCommander || null;
|
||||
const hasTanks = filter.hasTanks || null;
|
||||
const hasHelicopters = filter.hasHelicopters || null;
|
||||
|
||||
const layers = [];
|
||||
|
||||
for (const layer of this.layers) {
|
||||
// Whitelist / Blacklist Layers
|
||||
if (
|
||||
whitelistedLayers !== null &&
|
||||
!whitelistedLayers.includes(layer.layer)
|
||||
)
|
||||
continue;
|
||||
if (blacklistedLayers !== null && blacklistedLayers.includes(layer.layer))
|
||||
continue;
|
||||
|
||||
// Whitelist / Blacklist Maps
|
||||
if (whitelistedMaps !== null && !whitelistedMaps.includes(layer.map))
|
||||
continue;
|
||||
if (blacklistedMaps !== null && blacklistedMaps.includes(layer.map))
|
||||
continue;
|
||||
|
||||
// Whitelist / Blacklist Gamemodes
|
||||
if (
|
||||
whitelistedGamemodes !== null &&
|
||||
!whitelistedGamemodes.includes(layer.gamemode)
|
||||
)
|
||||
continue;
|
||||
if (
|
||||
blacklistedGamemodes !== null &&
|
||||
blacklistedGamemodes.includes(layer.gamemode)
|
||||
)
|
||||
continue;
|
||||
|
||||
// Flag Count
|
||||
if (flagCountMin !== null && layer.flagCount < flagCountMin) continue;
|
||||
if (flagCountMax !== null && layer.flagCount > flagCountMax) continue;
|
||||
|
||||
// Other Properties
|
||||
if (hasCommander !== null && layer.commander !== hasCommander) continue;
|
||||
if (hasTanks !== null && (layer.tanks !== 'N/A') !== hasTanks) continue;
|
||||
if (
|
||||
hasHelicopters !== null &&
|
||||
(layer.helicopters !== 'N/A') !== hasHelicopters
|
||||
)
|
||||
continue;
|
||||
|
||||
layers.push(layer.layer);
|
||||
}
|
||||
|
||||
return layers;
|
||||
}
|
||||
|
||||
isHistoryCompliant(layerHistory, layer, options = {}) {
|
||||
const layerTolerance = options.layerTolerance || 4;
|
||||
const mapTolerance = options.mapTolerance || 2;
|
||||
const timeTolerance = options.timeTolerance || 5 * 60 * 60 * 1000;
|
||||
|
||||
for (let i = 0; i < layerHistory.length; i++) {
|
||||
if (i >= Math.max(layerHistory, mapTolerance)) return true;
|
||||
if (new Date() - layerHistory[i].time > timeTolerance) return true;
|
||||
|
||||
if (
|
||||
i < layerTolerance &&
|
||||
layerHistory[i].map === this.getLayerByLayerName(layer).map
|
||||
)
|
||||
return false;
|
||||
if (i < layerTolerance && layerHistory[i].layer === layer) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
isPlayerCountCompliant(playerCount, layer) {
|
||||
return !(
|
||||
playerCount >
|
||||
this.getLayerByLayerName(layer).estimatedSuitablePlayerCount.max ||
|
||||
playerCount <
|
||||
this.getLayerByLayerName(layer).estimatedSuitablePlayerCount.min
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default new SquadLayers();
|
5142
connectors/squad-layers/layers.json
Normal file
5142
connectors/squad-layers/layers.json
Normal file
File diff suppressed because it is too large
Load Diff
4
core/config.js
Normal file
4
core/config.js
Normal file
@ -0,0 +1,4 @@
|
||||
/* As set out by the terms of the license, the following should not be modified. */
|
||||
const COPYRIGHT_MESSAGE = 'SquadJS, Copyright © 2020 Thomas Smyth';
|
||||
|
||||
export { COPYRIGHT_MESSAGE };
|
10
core/package.json
Normal file
10
core/package.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "core",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./config": "./config.js",
|
||||
"./utils/print-logo": "./utils/print-logo.js",
|
||||
"./utils/sleep": "./utils/sleep.js"
|
||||
}
|
||||
}
|
18
core/utils/print-logo.js
Normal file
18
core/utils/print-logo.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { COPYRIGHT_MESSAGE } from '../config.js';
|
||||
|
||||
const LOGO = `
|
||||
_____ ____ _ _ _____ _
|
||||
/ ____|/ __ \\| | | | /\\ | __ \\ (_)
|
||||
| (___ | | | | | | | / \\ | | | | _ ___
|
||||
\\___ \\| | | | | | |/ /\\ \\ | | | || / __|
|
||||
____) | |__| | |__| / ____ \\| |__| || \\__ \\
|
||||
|_____/ \\___\\_\\\\____/_/ \\_\\_____(_) |___/
|
||||
_/ |
|
||||
|__/
|
||||
${COPYRIGHT_MESSAGE}
|
||||
GitHub: https://github.com/Thomas-Smyth/SquadJS
|
||||
`;
|
||||
|
||||
export default function() {
|
||||
console.log(LOGO);
|
||||
}
|
5
core/utils/sleep.js
Normal file
5
core/utils/sleep.js
Normal file
@ -0,0 +1,5 @@
|
||||
export default function(time) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, time);
|
||||
});
|
||||
}
|
67
index.js
Normal file
67
index.js
Normal file
@ -0,0 +1,67 @@
|
||||
import Discord from 'discord.js';
|
||||
import mysql from 'mysql';
|
||||
import Influx from 'influx';
|
||||
|
||||
import Server from 'squad-server';
|
||||
|
||||
import {
|
||||
discordAdminCamLogs,
|
||||
discordChat,
|
||||
discordServerStatus,
|
||||
discordTeamkill,
|
||||
influxdbLog,
|
||||
influxdbLogDefaultSchema,
|
||||
mapvote,
|
||||
mysqlLog,
|
||||
teamRandomizer
|
||||
} from 'plugins';
|
||||
|
||||
async function main() {
|
||||
const server = new Server({
|
||||
id: 1,
|
||||
|
||||
host: 'xxx.xxx.xxx.xxx',
|
||||
queryPort: 27165,
|
||||
rconPort: 21114,
|
||||
rconPassword: 'password',
|
||||
logDir: 'C:/path/to/squad/log/folder'
|
||||
});
|
||||
|
||||
// Discord Plugins
|
||||
const discordClient = new Discord.Client();
|
||||
await discordClient.login('Discord Login Token');
|
||||
await discordAdminCamLogs(server, discordClient, 'discordChannelID');
|
||||
await discordChat(server, discordClient, 'discordChannelID');
|
||||
await discordServerStatus(server, discordClient);
|
||||
await discordTeamkill(server, discordClient, 'discordChannelID');
|
||||
|
||||
// in game features
|
||||
mapvote(server);
|
||||
teamRandomizer(server);
|
||||
|
||||
// MySQL Plugins
|
||||
const mysqlPool = mysql.createPool({
|
||||
connectionLimit: 10,
|
||||
host: 'host',
|
||||
port: 3306,
|
||||
user: 'squadjs',
|
||||
password: 'password',
|
||||
database: 'squadjs'
|
||||
});
|
||||
mysqlLog(server, mysqlPool);
|
||||
|
||||
// Influx Plugins
|
||||
const influxDB = new Influx.InfluxDB({
|
||||
host: 'host',
|
||||
port: 8086,
|
||||
username: 'squadjs',
|
||||
password: 'password',
|
||||
database: 'squadjs',
|
||||
schema: influxdbLogDefaultSchema
|
||||
});
|
||||
influxdbLog(server, influxDB);
|
||||
|
||||
await server.watch();
|
||||
}
|
||||
|
||||
main();
|
39
package.json
Normal file
39
package.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "SquadJS",
|
||||
"version": "0.0.1",
|
||||
"repository": "https://github.com/Thomas-Smyth/SquadJS.git",
|
||||
"author": "Thomas Smyth <https://github.com/Thomas-Smyth>",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"assets",
|
||||
"connectors",
|
||||
"core",
|
||||
"plugins",
|
||||
"squad-server"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "eslint --fix . && prettier --write \"./**/*.js\"",
|
||||
"test-log-parser-coverage": "node squad-server/log-parser/test-coverage.js"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"discord.js": "^12.2.0",
|
||||
"influx": "^5.5.1",
|
||||
"mysql": "^2.18.1",
|
||||
"plugins": "1.0.0",
|
||||
"squad-server": "1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "5.12.0",
|
||||
"eslint-config-prettier": "^6.6.0",
|
||||
"eslint-config-standard": "^14.1.0",
|
||||
"eslint-plugin-import": "^2.18.2",
|
||||
"eslint-plugin-node": "^10.0.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.1",
|
||||
"husky": "^3.1.0",
|
||||
"lint-staged": "^9.4.3",
|
||||
"prettier": "^1.19.1"
|
||||
}
|
||||
}
|
30
plugins/discord-admin-cam-logs/README.md
Normal file
30
plugins/discord-admin-cam-logs/README.md
Normal file
@ -0,0 +1,30 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="../../assets/squadjs-logo.png" alt="Logo" width="500"/>
|
||||
|
||||
#### SquadJS - Discord Admin Cam Logs
|
||||
</div>
|
||||
|
||||
## About
|
||||
The Discord Admin Cam Logs plugin logs admin cam usage to a Discord channel.
|
||||
|
||||
## Installation
|
||||
```js
|
||||
// Place the following two lines at the top of your index.js file.
|
||||
import Discord from 'discord.js';
|
||||
import { discordAdminCamLogs } from 'plugins';
|
||||
|
||||
// Place the following two lines in your index.js file before using a Discord plugins.
|
||||
const discordClient = new Discord.Client();
|
||||
await discordClient.login('Discord Login Token'); // insert your Discord bot's login token here.
|
||||
|
||||
// Place the following lines after all of the above.
|
||||
await discordAdminCamLogs(
|
||||
server,
|
||||
discordClient,
|
||||
'discordChannelID',
|
||||
{ // options - the options included below display the defaults and can be removed for simplicity.
|
||||
color: 16761867 // color of embed
|
||||
}
|
||||
);
|
||||
```
|
97
plugins/discord-admin-cam-logs/index.js
Normal file
97
plugins/discord-admin-cam-logs/index.js
Normal file
@ -0,0 +1,97 @@
|
||||
import { COPYRIGHT_MESSAGE } from 'core/config';
|
||||
import { LOG_PARSER_PLAYER_POSSESS } from 'squad-server/events/log-parser';
|
||||
|
||||
export default async function plugin(
|
||||
server,
|
||||
discordClient,
|
||||
channelID,
|
||||
options = {}
|
||||
) {
|
||||
if (!server)
|
||||
throw new Error(
|
||||
'DiscordAdminCamLogs must be provided with a reference to the server.'
|
||||
);
|
||||
|
||||
if (!discordClient)
|
||||
throw new Error(
|
||||
'DiscordAdminCamLogs must be provided with a Discord.js client.'
|
||||
);
|
||||
|
||||
if (!channelID)
|
||||
throw new Error('DiscordAdminCamLogs must be provided with a channel ID.');
|
||||
|
||||
options = {
|
||||
color: 16761867,
|
||||
...options
|
||||
};
|
||||
|
||||
const channel = await discordClient.channels.fetch(channelID);
|
||||
|
||||
const adminsInCam = {};
|
||||
|
||||
server.on(LOG_PARSER_PLAYER_POSSESS, info => {
|
||||
if (info.player === null) return;
|
||||
|
||||
if (info.possessClassname === 'CameraMan') {
|
||||
adminsInCam[info.player.steamID] = info.time;
|
||||
|
||||
channel.send({
|
||||
embed: {
|
||||
title: `Admin Entered Admin Camera`,
|
||||
color: options.color,
|
||||
fields: [
|
||||
{
|
||||
name: "Admin's Name",
|
||||
value: info.player.name,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Admin's SteamID",
|
||||
value: `[${info.player.steamID}](https://steamcommunity.com/profiles/${info.player.steamID})`,
|
||||
inline: true
|
||||
}
|
||||
],
|
||||
timestamp: info.time.toISOString(),
|
||||
footer: {
|
||||
text: COPYRIGHT_MESSAGE
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (!(info.player.steamID in adminsInCam)) return;
|
||||
|
||||
channel.send({
|
||||
embed: {
|
||||
title: `Admin Left Admin Camera`,
|
||||
color: options.color,
|
||||
fields: [
|
||||
{
|
||||
name: "Admin's Name",
|
||||
value: info.player.name,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Admin's SteamID",
|
||||
value: `[${info.player.steamID}](https://steamcommunity.com/profiles/${info.player.steamID})`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Time in Admin Camera',
|
||||
value: `${Math.round(
|
||||
(info.time.getTime() -
|
||||
adminsInCam[info.player.steamID].getTime()) /
|
||||
60000
|
||||
)} mins`
|
||||
}
|
||||
],
|
||||
timestamp: info.time.toISOString(),
|
||||
footer: {
|
||||
text: COPYRIGHT_MESSAGE
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
delete adminsInCam[info.player.steamID];
|
||||
}
|
||||
});
|
||||
}
|
31
plugins/discord-chat/README.md
Normal file
31
plugins/discord-chat/README.md
Normal file
@ -0,0 +1,31 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="../../assets/squadjs-logo.png" alt="Logo" width="500"/>
|
||||
|
||||
#### SquadJS - Discord Chat Plugin
|
||||
</div>
|
||||
|
||||
## About
|
||||
The Discord Chat plugin streams in-game chat to a Discord channel. It is useful to allow those out of game to monitor in-game chat as well as to log to permanent form. It can be configured to limit access to specific chats.
|
||||
|
||||
## Installation
|
||||
```js
|
||||
// Place the following two lines at the top of your index.js file.
|
||||
import Discord from 'discord.js';
|
||||
import { discordChat } from 'plugins';
|
||||
|
||||
// Place the following two lines in your index.js file before using an Discord plugins.
|
||||
const discordClient = new Discord.Client();
|
||||
await discordClient.login('Discord Login Token'); // insert your Discord bot's login token here.
|
||||
|
||||
// Place the following lines after all of the above.
|
||||
await discordChat(
|
||||
server,
|
||||
discordClient,
|
||||
'discordChannelID',
|
||||
{ // options - the options included below display the defaults and can be removed for simplicity.
|
||||
ignoreChats: ['ChatSquad', 'ChatAdmin'], // an array of chats to not display.
|
||||
color: 16761867 // color of embed
|
||||
}
|
||||
);
|
||||
```
|
67
plugins/discord-chat/index.js
Normal file
67
plugins/discord-chat/index.js
Normal file
@ -0,0 +1,67 @@
|
||||
import { COPYRIGHT_MESSAGE } from 'core/config';
|
||||
import { RCON_CHAT_MESSAGE } from 'squad-server/events/rcon';
|
||||
|
||||
export default async function plugin(
|
||||
server,
|
||||
discordClient,
|
||||
channelID,
|
||||
options = {}
|
||||
) {
|
||||
if (!server)
|
||||
throw new Error(
|
||||
'DiscordChat must be provided with a reference to the server.'
|
||||
);
|
||||
|
||||
if (!discordClient)
|
||||
throw new Error('DiscordChat must be provided with a Discord.js client.');
|
||||
|
||||
if (!channelID)
|
||||
throw new Error('DiscordChat must be provided with a channel ID.');
|
||||
|
||||
const ignoreChats = options.ignoreChats || ['ChatSquad', 'ChatAdmin'];
|
||||
|
||||
options = {
|
||||
color: 16761867,
|
||||
...options
|
||||
};
|
||||
|
||||
const channel = await discordClient.channels.fetch(channelID);
|
||||
|
||||
server.on(RCON_CHAT_MESSAGE, async info => {
|
||||
if (ignoreChats.includes(info.chat)) return;
|
||||
|
||||
const playerInfo = await server.getPlayerBySteamID(info.steamID);
|
||||
|
||||
channel.send({
|
||||
embed: {
|
||||
title: info.chat,
|
||||
color: options.color,
|
||||
fields: [
|
||||
{
|
||||
name: 'Player',
|
||||
value: playerInfo.name,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'SteamID',
|
||||
value: `[${playerInfo.steamID}](https://steamcommunity.com/profiles/${info.steamID})`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Team & Squad',
|
||||
value: `Team: ${playerInfo.teamID}, Squad: ${playerInfo.squadID ||
|
||||
'Unassigned'}`
|
||||
},
|
||||
{
|
||||
name: 'Message',
|
||||
value: `${info.message}`
|
||||
}
|
||||
],
|
||||
timestamp: info.time.toISOString(),
|
||||
footer: {
|
||||
text: COPYRIGHT_MESSAGE
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
34
plugins/discord-debug/README.md
Normal file
34
plugins/discord-debug/README.md
Normal file
@ -0,0 +1,34 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="../../assets/squadjs-logo.png" alt="Logo" width="500"/>
|
||||
|
||||
#### SquadJS - Discord Debug
|
||||
</div>
|
||||
|
||||
## About
|
||||
The Discord Debug plugin logs all server events in a raw format for monitoring/debugging/testing purposes.
|
||||
|
||||
## Installation
|
||||
```js
|
||||
// Place the following two lines at the top of your index.js file.
|
||||
import Discord from 'discord.js';
|
||||
import { discordDebug } from 'plugins';
|
||||
|
||||
// Import the events you wish to log in your index.js file. A full list can be found in the directory specified below.
|
||||
import {
|
||||
LOG_PARSER_PLAYER_CONNECTED,
|
||||
LOG_PARSER_PLAYER_WOUNDED,
|
||||
} from 'squad-server/events/log-parser';
|
||||
|
||||
// Place the following two lines in your index.js file before using an Discord plugins.
|
||||
const discordClient = new Discord.Client();
|
||||
await discordClient.login('Discord Login Token'); // insert your Discord bot's login token here.
|
||||
|
||||
// Place the following lines after all of the above.
|
||||
await discordDebug(
|
||||
server,
|
||||
discordClient,
|
||||
'discordChannelID',
|
||||
[LOG_PARSER_PLAYER_CONNECTED, LOG_PARSER_PLAYER_WOUNDED] // List the events you wish to log.
|
||||
);
|
||||
```
|
25
plugins/discord-debug/index.js
Normal file
25
plugins/discord-debug/index.js
Normal file
@ -0,0 +1,25 @@
|
||||
export default async function plugin(
|
||||
server,
|
||||
discordClient,
|
||||
channelID,
|
||||
events = []
|
||||
) {
|
||||
if (!server)
|
||||
throw new Error(
|
||||
'DiscordDebug must be provided with a reference to the server.'
|
||||
);
|
||||
|
||||
if (!discordClient)
|
||||
throw new Error('DiscordDebug must be provided with a Discord.js client.');
|
||||
|
||||
if (!channelID)
|
||||
throw new Error('DicordDebug must be provided with a channel ID.');
|
||||
|
||||
const channel = await discordClient.channels.fetch(channelID);
|
||||
|
||||
for (const event of events) {
|
||||
server.on(event, info => {
|
||||
channel.send(`\`\`\`${JSON.stringify(info, null, 2)}\`\`\``);
|
||||
});
|
||||
}
|
||||
}
|
30
plugins/discord-server-status/README.md
Normal file
30
plugins/discord-server-status/README.md
Normal file
@ -0,0 +1,30 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="../../assets/squadjs-logo.png" alt="Logo" width="500"/>
|
||||
|
||||
#### SquadJS - Discord Server Status
|
||||
</div>
|
||||
|
||||
## About
|
||||
Display a server status embed that can be updated by clicking the refresh react.
|
||||
|
||||
## Installation
|
||||
```js
|
||||
// Place the following two lines at the top of your index.js file.
|
||||
import Discord from 'discord.js';
|
||||
import { discordServerStatus } from 'plugins';
|
||||
|
||||
// Place the following two lines in your index.js file before using an Discord plugins.
|
||||
const discordClient = new Discord.Client();
|
||||
await discordClient.login('Discord Login Token'); // insert your Discord bot's login token here.
|
||||
|
||||
// Place the following lines after all of the above.
|
||||
await discordServerStatus(
|
||||
server,
|
||||
discordClient,
|
||||
{ // options - the options included below display the defaults and can be removed for simplicity.
|
||||
color: 16761867, // color of embed
|
||||
command: '!server' // command used to send message
|
||||
}
|
||||
);
|
||||
```
|
80
plugins/discord-server-status/index.js
Normal file
80
plugins/discord-server-status/index.js
Normal file
@ -0,0 +1,80 @@
|
||||
import { COPYRIGHT_MESSAGE } from 'core/config';
|
||||
|
||||
function makeEmbed(server, options) {
|
||||
let players = `${server.a2sPlayerCount}`;
|
||||
if (server.publicQueue + server.reserveQueue > 0)
|
||||
players += ` (+${server.publicQueue + server.reserveQueue})`;
|
||||
players += ` / ${server.publicSlots}`;
|
||||
if (server.reserveSlots > 0) players += ` (+${server.reserveSlots})`;
|
||||
|
||||
return {
|
||||
embed: {
|
||||
title: server.serverName,
|
||||
color: options.color,
|
||||
fields: [
|
||||
{
|
||||
name: 'Players',
|
||||
value: `\`\`\`${players}\`\`\``
|
||||
},
|
||||
{
|
||||
name: 'Current Layer',
|
||||
value: `\`\`\`${server.currentLayer}\`\`\``,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Next Layer',
|
||||
value: `\`\`\`${server.nextLayer || 'Unknown'}\`\`\``,
|
||||
inline: true
|
||||
}
|
||||
],
|
||||
timestamp: new Date().toISOString(),
|
||||
footer: {
|
||||
text: `Server Status by ${COPYRIGHT_MESSAGE}`
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default async function plugin(server, discordClient, options = {}) {
|
||||
if (!server)
|
||||
throw new Error(
|
||||
'DiscordDebug must be provided with a reference to the server.'
|
||||
);
|
||||
|
||||
if (!discordClient)
|
||||
throw new Error('DiscordDebug must be provided with a Discord.js client.');
|
||||
|
||||
options = {
|
||||
color: 16761867,
|
||||
command: '!server',
|
||||
...options
|
||||
};
|
||||
|
||||
discordClient.on('message', async message => {
|
||||
if (message.content !== options.command) return;
|
||||
|
||||
const serverStatus = await message.channel.send(makeEmbed(server, options));
|
||||
|
||||
await serverStatus.react('🔄');
|
||||
});
|
||||
|
||||
discordClient.on('messageReactionAdd', async reaction => {
|
||||
// confirm it's a status message
|
||||
if (
|
||||
reaction.message.embeds.length !== 1 ||
|
||||
reaction.message.embeds[0].footer.text !==
|
||||
`Server Status by ${COPYRIGHT_MESSAGE}`
|
||||
)
|
||||
return;
|
||||
|
||||
// ignore bots reacting
|
||||
if (reaction.count === 1) return;
|
||||
|
||||
// remove reaction and readd it
|
||||
await reaction.remove();
|
||||
await reaction.message.react('🔄');
|
||||
|
||||
// update the message
|
||||
await reaction.message.edit(makeEmbed(server, options));
|
||||
});
|
||||
}
|
30
plugins/discord-teamkill/README.md
Normal file
30
plugins/discord-teamkill/README.md
Normal file
@ -0,0 +1,30 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="../../assets/squadjs-logo.png" alt="Logo" width="500"/>
|
||||
|
||||
#### SquadJS - Discord Teamkill
|
||||
</div>
|
||||
|
||||
## About
|
||||
The Discord Teamkill plugin logs teamkill information to a Discord channel.
|
||||
|
||||
## Installation
|
||||
```js
|
||||
// Place the following two lines at the top of your index.js file.
|
||||
import Discord from 'discord.js';
|
||||
import { discordTeamkill } from 'plugins';
|
||||
|
||||
// Place the following two lines in your index.js file before using a Discord plugins.
|
||||
const discordClient = new Discord.Client();
|
||||
await discordClient.login('Discord Login Token'); // insert your Discord bot's login token here.
|
||||
|
||||
// Place the following lines after all of the above.
|
||||
await discordTeamkill(
|
||||
server,
|
||||
discordClient,
|
||||
'discordChannelID',
|
||||
{ // options - the options included below display the defaults and can be removed for simplicity.
|
||||
color: 16761867 // color of embed
|
||||
}
|
||||
);
|
||||
```
|
74
plugins/discord-teamkill/index.js
Normal file
74
plugins/discord-teamkill/index.js
Normal file
@ -0,0 +1,74 @@
|
||||
import { COPYRIGHT_MESSAGE } from 'core/config';
|
||||
import { LOG_PARSER_TEAMKILL } from 'squad-server/events/log-parser';
|
||||
|
||||
export default async function plugin(
|
||||
server,
|
||||
discordClient,
|
||||
channelID,
|
||||
options = {}
|
||||
) {
|
||||
if (!server)
|
||||
throw new Error(
|
||||
'DiscordTeamKill must be provided with a reference to the server.'
|
||||
);
|
||||
|
||||
if (!discordClient)
|
||||
throw new Error(
|
||||
'DiscordTeamkill must be provided with a Discord.js client.'
|
||||
);
|
||||
|
||||
if (!channelID)
|
||||
throw new Error('DiscordTeamkill must be provided with a channel ID.');
|
||||
|
||||
options = {
|
||||
color: 16761867,
|
||||
...options
|
||||
};
|
||||
|
||||
const channel = await discordClient.channels.fetch(channelID);
|
||||
|
||||
server.on(LOG_PARSER_TEAMKILL, info => {
|
||||
if (!info.attacker) return;
|
||||
|
||||
channel.send({
|
||||
embed: {
|
||||
title: `Teamkill: ${info.attacker.name}`,
|
||||
color: options.color,
|
||||
fields: [
|
||||
{
|
||||
name: "Attacker's Name",
|
||||
value: info.attacker.name,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Attacker's SteamID",
|
||||
value: `[${info.attacker.steamID}](https://steamcommunity.com/profiles/${info.attacker.steamID})`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Weapon',
|
||||
value: info.weapon
|
||||
},
|
||||
{
|
||||
name: "Victim's Name",
|
||||
value: info.victim.name,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Victim's SteamID",
|
||||
value: `[${info.victim.steamID}](https://steamcommunity.com/profiles/${info.victim.steamID})`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: 'Squad Community Ban List',
|
||||
value: `[Attacker's Bans](https://squad-community-ban-list.com/search/${info.attacker.steamID})\n[Victims's Bans](https://squad-community-ban-list.com/search/${info.victim.steamID})`
|
||||
}
|
||||
],
|
||||
timestamp: info.time.toISOString(),
|
||||
footer: {
|
||||
text: COPYRIGHT_MESSAGE
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
25
plugins/index.js
Normal file
25
plugins/index.js
Normal file
@ -0,0 +1,25 @@
|
||||
import discordAdminCamLogs from './discord-admin-cam-logs/index.js';
|
||||
import discordChat from './discord-chat/index.js';
|
||||
import discordDebug from './discord-debug/index.js';
|
||||
import discordServerStatus from './discord-server-status/index.js';
|
||||
import discordTeamkill from './discord-teamkill/index.js';
|
||||
import influxdbLog from './influxdb-log/index.js';
|
||||
import influxdbLogDefaultSchema from './influxdb-log/schema.js';
|
||||
import mapvote from './mapvote/index.js';
|
||||
import mysqlLog from './mysql-log/index.js';
|
||||
import seedingMessage from './seeding-message/index.js';
|
||||
import teamRandomizer from './team-randomizer/index.js';
|
||||
|
||||
export {
|
||||
discordAdminCamLogs,
|
||||
discordChat,
|
||||
discordDebug,
|
||||
discordServerStatus,
|
||||
discordTeamkill,
|
||||
influxdbLog,
|
||||
influxdbLogDefaultSchema,
|
||||
mapvote,
|
||||
mysqlLog,
|
||||
seedingMessage,
|
||||
teamRandomizer
|
||||
};
|
30
plugins/influxdb-log/README.md
Normal file
30
plugins/influxdb-log/README.md
Normal file
@ -0,0 +1,30 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="../../assets/squadjs-logo.png" alt="Logo" width="500"/>
|
||||
|
||||
#### SquadJS - InfluxDB Log
|
||||
</div>
|
||||
|
||||
## About
|
||||
The InfluxDB log plugin logs event information into InfluxDB to allow it to be queried for analysis, monitoring, or stat tracking.. Works well with Grafana.
|
||||
|
||||
## Requirements
|
||||
* InfluxDB database.
|
||||
|
||||
## Installation
|
||||
```js
|
||||
// Place the following two lines at the top of your index.js file.
|
||||
import Influx from 'influx';
|
||||
import { influxdbLog, influxdbLogDefaultSchema } from 'plugins';
|
||||
|
||||
// Place the following lines in your index.js file. Replace the credentials with the credentials of your InfluxDB database.
|
||||
const influxDB = new Influx.InfluxDB({
|
||||
host: 'host',
|
||||
port: 8086,
|
||||
username: 'squadjs',
|
||||
password: 'password',
|
||||
database: 'squadjs',
|
||||
schema: influxdbLogDefaultSchema
|
||||
});
|
||||
influxdbLog(server, influxDB);
|
||||
```
|
125
plugins/influxdb-log/index.js
Normal file
125
plugins/influxdb-log/index.js
Normal file
@ -0,0 +1,125 @@
|
||||
import {
|
||||
LOG_PARSER_NEW_GAME,
|
||||
LOG_PARSER_PLAYER_DIED,
|
||||
LOG_PARSER_PLAYER_WOUNDED,
|
||||
LOG_PARSER_PLAYER_REVIVED,
|
||||
LOG_PARSER_SERVER_TICK_RATE
|
||||
} from 'squad-server/events/log-parser';
|
||||
|
||||
import { SERVER_PLAYERS_UPDATED } from 'squad-server/events/server';
|
||||
|
||||
export default function influxdbLog(server, influxDB, options = {}) {
|
||||
if (!server)
|
||||
throw new Error(
|
||||
'InfluxDBLog must be provided with a reference to the server.'
|
||||
);
|
||||
|
||||
if (!influxDB)
|
||||
throw new Error('InfluxDBLog must be provided with a InfluxDB connection.');
|
||||
|
||||
let points = [];
|
||||
setInterval(() => {
|
||||
influxDB.writePoints(points);
|
||||
points = [];
|
||||
}, options.writeInterval || 30 * 1000);
|
||||
|
||||
server.on(LOG_PARSER_SERVER_TICK_RATE, info => {
|
||||
points.push({
|
||||
measurement: 'ServerTickRate',
|
||||
tags: { server: server.id },
|
||||
fields: { tick_rate: info.tickRate },
|
||||
timestamp: info.time
|
||||
});
|
||||
});
|
||||
|
||||
server.on(SERVER_PLAYERS_UPDATED, players => {
|
||||
points.push({
|
||||
measurement: 'PlayerCount',
|
||||
tags: { server: server.id },
|
||||
fields: { player_count: players.length },
|
||||
timestamp: new Date()
|
||||
});
|
||||
});
|
||||
|
||||
server.on(LOG_PARSER_NEW_GAME, info => {
|
||||
points.push({
|
||||
measurement: 'Match',
|
||||
tags: { server: server.id },
|
||||
fields: {
|
||||
dlc: info.dlc,
|
||||
mapClassname: info.mapClassname,
|
||||
layerClassname: info.layerClassname,
|
||||
map: info.map,
|
||||
layer: info.layer
|
||||
},
|
||||
timestamp: info.time
|
||||
});
|
||||
});
|
||||
|
||||
server.on(LOG_PARSER_PLAYER_WOUNDED, info => {
|
||||
points.push({
|
||||
measurement: 'PlayerWounded',
|
||||
tags: { server: server.id },
|
||||
fields: {
|
||||
victim: info.victim ? info.victim.steamID : null,
|
||||
victimName: info.victim ? info.victim.name : null,
|
||||
victimTeamID: info.victim ? info.victim.teamID : null,
|
||||
victimSquadID: info.victim ? info.victim.squadID : null,
|
||||
attacker: info.attacker ? info.attacker.steamID : null,
|
||||
attackerName: info.attacker ? info.attacker.name : null,
|
||||
attackerTeamID: info.attacker ? info.attacker.teamID : null,
|
||||
attackerSquadID: info.attacker ? info.attacker.squadID : null,
|
||||
damage: info.damage,
|
||||
weapon: info.weapon,
|
||||
teamkill: info.teamkill
|
||||
},
|
||||
timestamp: info.time
|
||||
});
|
||||
});
|
||||
|
||||
server.on(LOG_PARSER_PLAYER_DIED, info => {
|
||||
points.push({
|
||||
measurement: 'PlayerDied',
|
||||
tags: { server: server.id },
|
||||
fields: {
|
||||
victim: info.victim ? info.victim.steamID : null,
|
||||
victimName: info.victim ? info.victim.name : null,
|
||||
victimTeamID: info.victim ? info.victim.teamID : null,
|
||||
victimSquadID: info.victim ? info.victim.squadID : null,
|
||||
attacker: info.attacker ? info.attacker.steamID : null,
|
||||
attackerName: info.attacker ? info.attacker.name : null,
|
||||
attackerTeamID: info.attacker ? info.attacker.teamID : null,
|
||||
attackerSquadID: info.attacker ? info.attacker.squadID : null,
|
||||
damage: info.damage,
|
||||
weapon: info.weapon,
|
||||
teamkill: info.teamkill
|
||||
},
|
||||
timestamp: info.time
|
||||
});
|
||||
});
|
||||
|
||||
server.on(LOG_PARSER_PLAYER_REVIVED, info => {
|
||||
points.push({
|
||||
measurement: 'Revived',
|
||||
tags: { server: server.id },
|
||||
fields: {
|
||||
victim: info.victim ? info.victim.steamID : null,
|
||||
victimName: info.victim ? info.victim.name : null,
|
||||
victimTeamID: info.victim ? info.victim.teamID : null,
|
||||
victimSquadID: info.victim ? info.victim.squadID : null,
|
||||
attacker: info.attacker ? info.attacker.steamID : null,
|
||||
attackerName: info.attacker ? info.attacker.name : null,
|
||||
attackerTeamID: info.attacker ? info.attacker.teamID : null,
|
||||
attackerSquadID: info.attacker ? info.attacker.squadID : null,
|
||||
damage: info.damage,
|
||||
weapon: info.weapon,
|
||||
teamkill: info.teamkill,
|
||||
reviver: info.reviver ? info.reviver.steamID : null,
|
||||
reviverName: info.reviver ? info.reviver.name : null,
|
||||
reviverTeamID: info.reviver ? info.reviver.teamID : null,
|
||||
reviverSquadID: info.reviver ? info.reviver.squadID : null
|
||||
},
|
||||
timestamp: info.time
|
||||
});
|
||||
});
|
||||
}
|
84
plugins/influxdb-log/schema.js
Normal file
84
plugins/influxdb-log/schema.js
Normal file
@ -0,0 +1,84 @@
|
||||
import Influx from 'influx';
|
||||
|
||||
export default [
|
||||
{
|
||||
measurement: 'ServerTickRate',
|
||||
fields: {
|
||||
tick_rate: Influx.FieldType.FLOAT
|
||||
},
|
||||
tags: ['server']
|
||||
},
|
||||
{
|
||||
measurement: 'PlayerCount',
|
||||
fields: {
|
||||
player_count: Influx.FieldType.INTEGER
|
||||
},
|
||||
tags: ['server']
|
||||
},
|
||||
{
|
||||
measurement: 'Match',
|
||||
fields: {
|
||||
dlc: Influx.FieldType.STRING,
|
||||
mapClassname: Influx.FieldType.STRING,
|
||||
layerClassname: Influx.FieldType.STRING,
|
||||
map: Influx.FieldType.STRING,
|
||||
layer: Influx.FieldType.STRING
|
||||
},
|
||||
tags: ['server']
|
||||
},
|
||||
{
|
||||
measurement: 'PlayerWounded',
|
||||
fields: {
|
||||
victim: Influx.FieldType.STRING,
|
||||
victimName: Influx.FieldType.STRING,
|
||||
victimTeamID: Influx.FieldType.INTEGER,
|
||||
victimSquadID: Influx.FieldType.INTEGER,
|
||||
attacker: Influx.FieldType.STRING,
|
||||
attackerName: Influx.FieldType.STRING,
|
||||
attackerTeamID: Influx.FieldType.INTEGER,
|
||||
attackerSquadID: Influx.FieldType.INTEGER,
|
||||
damage: Influx.FieldType.STRING,
|
||||
weapon: Influx.FieldType.STRING,
|
||||
teamkill: Influx.FieldType.BOOLEAN
|
||||
},
|
||||
tags: ['server']
|
||||
},
|
||||
{
|
||||
measurement: 'PlayerDied',
|
||||
fields: {
|
||||
victim: Influx.FieldType.STRING,
|
||||
victimName: Influx.FieldType.STRING,
|
||||
victimTeamID: Influx.FieldType.INTEGER,
|
||||
victimSquadID: Influx.FieldType.INTEGER,
|
||||
attacker: Influx.FieldType.STRING,
|
||||
attackerName: Influx.FieldType.STRING,
|
||||
attackerTeamID: Influx.FieldType.INTEGER,
|
||||
attackerSquadID: Influx.FieldType.INTEGER,
|
||||
damage: Influx.FieldType.STRING,
|
||||
weapon: Influx.FieldType.STRING,
|
||||
teamkill: Influx.FieldType.BOOLEAN
|
||||
},
|
||||
tags: ['server']
|
||||
},
|
||||
{
|
||||
measurement: 'PlayerRevived',
|
||||
fields: {
|
||||
victim: Influx.FieldType.STRING,
|
||||
victimName: Influx.FieldType.STRING,
|
||||
victimTeamID: Influx.FieldType.INTEGER,
|
||||
victimSquadID: Influx.FieldType.INTEGER,
|
||||
attacker: Influx.FieldType.STRING,
|
||||
attackerName: Influx.FieldType.STRING,
|
||||
attackerTeamID: Influx.FieldType.INTEGER,
|
||||
attackerSquadID: Influx.FieldType.INTEGER,
|
||||
damage: Influx.FieldType.STRING,
|
||||
weapon: Influx.FieldType.STRING,
|
||||
teamkill: Influx.FieldType.BOOLEAN,
|
||||
reviver: Influx.FieldType.STRING,
|
||||
reviverName: Influx.FieldType.STRING,
|
||||
reviverTeamID: Influx.FieldType.INTEGER,
|
||||
reviverSquadID: Influx.FieldType.INTEGER
|
||||
},
|
||||
tags: ['server']
|
||||
}
|
||||
];
|
36
plugins/mapvote/README.md
Normal file
36
plugins/mapvote/README.md
Normal file
@ -0,0 +1,36 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="../../assets/squadjs-logo.png" alt="Logo" width="500"/>
|
||||
|
||||
#### SquadJS - Mapvote
|
||||
</div>
|
||||
|
||||
## About
|
||||
The mapvote plugin uses a "did you mean?" system to allow for players to vote for a wide range of layers. Command information for using the plugin in-game can be accessed by typing `!mapvote help` into in-game chat.
|
||||
|
||||
## Installation
|
||||
Place the following into your `index.js` file. The filters / options below are optional and can be removed without affecting functionality, however, the default options are shown for reference.
|
||||
```js
|
||||
mapvote(
|
||||
server,
|
||||
{ // layer filter to limit layers - remove or edit the below options to adjust the filter. Leaving this blank will remove all training layers as a default.
|
||||
whitelistedLayers: ['layer name'], // an array of layers that can be played
|
||||
blacklistedLayers: ['layer name'], // an array of layers that cannot be played
|
||||
whitelistedMaps: ['map name'], // an array of maps that can be played
|
||||
blacklistedMaps: ['map name'], // an array of maps that cannot be played - default removes training maps
|
||||
whitelistedGamemodes: ['gamemode name'], // an array of gamemodes that can be played
|
||||
blacklistedGamemodes: ['gamemode name'], // an array of gamemodes that cannot be played
|
||||
flagCountMin: 4, // the minimum number of flags the layer must have
|
||||
flagCountMax: 7, // the maximum number of flags the layer must have
|
||||
hasCommander: true, // has commander enabled
|
||||
hasTanks: true, // has tanks
|
||||
hasHelicopters: true // has helicopters
|
||||
},
|
||||
{ // options - remove or edit the below options. The defaults are shown.
|
||||
command: '!mapvote', // the command name used to access the vote
|
||||
layerTolerance: 4, // the number of other layers that must be played before the layer can be revoted for
|
||||
mapTolerance: 2, // the number of other maps that must be played before the layer can be revoted for
|
||||
timeTolerance: 5 * 60 * 60 * 1000 // the time that must pass before the above are ignored
|
||||
}
|
||||
);
|
||||
```
|
145
plugins/mapvote/index.js
Normal file
145
plugins/mapvote/index.js
Normal file
@ -0,0 +1,145 @@
|
||||
import didYouMean from 'didyoumean';
|
||||
|
||||
import { COPYRIGHT_MESSAGE } from 'core/config';
|
||||
|
||||
import SquadLayers from 'connectors/squad-layers';
|
||||
|
||||
import { RCON_CHAT_MESSAGE } from 'squad-server/events/rcon';
|
||||
import { SERVER_LAYER_CHANGE } from 'squad-server/events/server';
|
||||
|
||||
export default function(server, layerFilter = {}, options = {}) {
|
||||
if (!server)
|
||||
throw new Error('Mapvote must be provided with a reference to the server.');
|
||||
|
||||
const command = options.command || '!mapvote';
|
||||
const commandRegex = new RegExp(`^${command} ([A-z0-9'_ ]*)`, 'i');
|
||||
const rotation = SquadLayers.getFilteredLayers(layerFilter);
|
||||
|
||||
let voteCounts = {};
|
||||
let votes = {};
|
||||
let currentWinner = null;
|
||||
|
||||
function getResults() {
|
||||
let results;
|
||||
|
||||
results = Object.keys(voteCounts).map(layer => {
|
||||
return {
|
||||
layer: layer,
|
||||
voteCount: voteCounts[layer]
|
||||
};
|
||||
});
|
||||
|
||||
results = results.sort((a, b) => {
|
||||
if (a.voteCount > b.voteCount) return -1;
|
||||
if (a.voteCount < b.voteCount) return 1;
|
||||
else return Math.random() < 0.5 ? 1 : -1;
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
server.on(SERVER_LAYER_CHANGE, () => {
|
||||
voteCounts = {};
|
||||
votes = {};
|
||||
currentWinner = null;
|
||||
});
|
||||
|
||||
server.on(RCON_CHAT_MESSAGE, info => {
|
||||
const match = info.message.match(commandRegex);
|
||||
if (!match) return;
|
||||
|
||||
if (match[1] === 'help') {
|
||||
// show help options
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.steamID}" You may use any of the following commands in chat:`
|
||||
);
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.steamID}" !mapvote results - View the current vote counts.`
|
||||
);
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.steamID}" !mapvote <layer name> - Vote for the specified layer.`
|
||||
);
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.steamID}" When inputting a layer name, we autocorrect any miss spelling.`
|
||||
);
|
||||
} else if (match[1] === 'results') {
|
||||
// display results to player
|
||||
const results = getResults();
|
||||
|
||||
if (results.length === 0) {
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.steamID}" No one has voted yet.`
|
||||
);
|
||||
} else {
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.steamID}" The current vote counts are as follows:`
|
||||
);
|
||||
for (const result of results) {
|
||||
if (result.voteCount === 0) continue;
|
||||
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.steamID}" ${result.layer} - ${
|
||||
result.voteCount
|
||||
} vote${result.voteCount > 1 ? 's' : ''}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const layer = didYouMean(match[1], SquadLayers.getLayerNames());
|
||||
|
||||
// check layer is valid
|
||||
if (layer === null) {
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.steamID}" ${match[1]} is not a valid layer name.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rotation.includes(layer)) {
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.steamID}" ${layer} is not in the rotation.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!SquadLayers.isHistoryCompliant(server.layerHistory, layer, options)
|
||||
) {
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.steamID}" ${layer} has been played too recently.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// remove existing votes
|
||||
if (info.steamID in votes) voteCounts[votes[info.steamID]]--;
|
||||
|
||||
// add new vote
|
||||
if (layer in voteCounts) voteCounts[layer]++;
|
||||
else voteCounts[layer] = 1;
|
||||
|
||||
// save what layer they votes for
|
||||
votes[info.steamID] = layer;
|
||||
|
||||
// info them of their vote
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.steamID}" You voted for ${layer}.`
|
||||
);
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.steamID}" Powered by: ${COPYRIGHT_MESSAGE}`
|
||||
);
|
||||
|
||||
// check for new winner
|
||||
const newWinner = getResults()[0].layer;
|
||||
|
||||
if (currentWinner !== newWinner) {
|
||||
server.rcon.execute(`AdminSetNextMap ${newWinner}`);
|
||||
server.rcon.execute(`AdminBroadcast New Map Vote Winner: ${newWinner}`);
|
||||
server.rcon.execute(
|
||||
`AdminBroadcast Participate in the map vote by typing "!mapvote help" in chat.`
|
||||
);
|
||||
currentWinner = newWinner;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
32
plugins/mysql-log/README.md
Normal file
32
plugins/mysql-log/README.md
Normal file
@ -0,0 +1,32 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="../../assets/squadjs-logo.png" alt="Logo" width="500"/>
|
||||
|
||||
#### SquadJS - MySQL Log
|
||||
</div>
|
||||
|
||||
## About
|
||||
The MySQL log plugin logs event information into a MySQL database to allow it to be queried for analysis, monitoring, or stat tracking. Works well with Grafana.
|
||||
|
||||
## Requirements
|
||||
* MySQL database.
|
||||
* Execute the [`mysql-schema.sql`](https://github.com/Thomas-Smyth/SquadJS/blob/master/plugins/mysql-log/mysql-schema.sql) in the database to setup the tables, etc.
|
||||
* Add your server to the database... `INSERT INTO Server (name) VALUES ("[EU] The Coalition");` Please make sure the inserted ID is the same as that of the server in the `index.js` file.
|
||||
|
||||
## Installation
|
||||
```js
|
||||
// Place the following two lines at the top of your index.js file.
|
||||
import mysql from 'mysql';
|
||||
import { mysqlLog } from 'plugins';
|
||||
|
||||
// Place the following lines in your index.js file. Replace the credentials with the credentials of your MySQL database.
|
||||
const mysqlPool = mysql.createPool({
|
||||
connectionLimit: 10,
|
||||
host: 'host',
|
||||
port: 3306,
|
||||
user: 'squadjs',
|
||||
password: 'password',
|
||||
database: 'squadjs'
|
||||
});
|
||||
mysqlLog(server, mysqlPool);
|
||||
```
|
107
plugins/mysql-log/index.js
Normal file
107
plugins/mysql-log/index.js
Normal file
@ -0,0 +1,107 @@
|
||||
import {
|
||||
LOG_PARSER_NEW_GAME,
|
||||
LOG_PARSER_PLAYER_WOUNDED,
|
||||
LOG_PARSER_PLAYER_DIED,
|
||||
LOG_PARSER_PLAYER_REVIVED,
|
||||
LOG_PARSER_SERVER_TICK_RATE
|
||||
} from 'squad-server/events/log-parser';
|
||||
import { SERVER_PLAYERS_UPDATED } from 'squad-server/events/server';
|
||||
|
||||
export default function mysqlLog(server, mysqlPool) {
|
||||
if (!server)
|
||||
throw new Error(
|
||||
'MySQLLog must be provided with a reference to the server.'
|
||||
);
|
||||
|
||||
if (!mysqlPool)
|
||||
throw new Error('MySQLLog must be provided with a mysql Pool.');
|
||||
|
||||
server.on(LOG_PARSER_SERVER_TICK_RATE, info => {
|
||||
mysqlPool.query(
|
||||
'INSERT INTO ServerTickRate(time, server, tick_rate) VALUES (?,?,?)',
|
||||
[info.time, server.id, info.tickRate]
|
||||
);
|
||||
});
|
||||
|
||||
server.on(SERVER_PLAYERS_UPDATED, players => {
|
||||
mysqlPool.query(
|
||||
'INSERT INTO PlayerCount(time, server, player_count) VALUES (NOW(),?,?)',
|
||||
[server.id, players.length]
|
||||
);
|
||||
});
|
||||
|
||||
server.on(LOG_PARSER_NEW_GAME, info => {
|
||||
mysqlPool.query('call NewMatch(?,?,?,?,?,?,?)', [
|
||||
server.id,
|
||||
info.time,
|
||||
info.dlc,
|
||||
info.mapClassname,
|
||||
info.layerClassname,
|
||||
info.map,
|
||||
info.layer
|
||||
]);
|
||||
});
|
||||
|
||||
server.on(LOG_PARSER_PLAYER_WOUNDED, info => {
|
||||
mysqlPool.query('call InsertPlayerWounded(?,?,?,?,?,?,?,?,?,?,?,?,?)', [
|
||||
server.id,
|
||||
info.time,
|
||||
info.victim ? info.victim.steamID : null,
|
||||
info.victim ? info.victim.name : null,
|
||||
info.victim ? info.victim.teamID : null,
|
||||
info.victim ? info.victim.squadID : null,
|
||||
info.attacker ? info.attacker.steamID : null,
|
||||
info.attacker ? info.attacker.name : null,
|
||||
info.attacker ? info.attacker.teamID : null,
|
||||
info.attacker ? info.attacker.squadID : null,
|
||||
info.damage,
|
||||
info.weapon,
|
||||
info.teamkill
|
||||
]);
|
||||
});
|
||||
|
||||
server.on(LOG_PARSER_PLAYER_DIED, info => {
|
||||
mysqlPool.query('call InsertPlayerDied(?,?,?,?,?,?,?,?,?,?,?,?,?,?)', [
|
||||
server.id,
|
||||
info.time,
|
||||
info.woundTime,
|
||||
info.victim ? info.victim.steamID : null,
|
||||
info.victim ? info.victim.name : null,
|
||||
info.victim ? info.victim.teamID : null,
|
||||
info.victim ? info.victim.squadID : null,
|
||||
info.attacker ? info.attacker.steamID : null,
|
||||
info.attacker ? info.attacker.name : null,
|
||||
info.attacker ? info.attacker.teamID : null,
|
||||
info.attacker ? info.attacker.squadID : null,
|
||||
info.damage,
|
||||
info.weapon,
|
||||
info.teamkill
|
||||
]);
|
||||
});
|
||||
|
||||
server.on(LOG_PARSER_PLAYER_REVIVED, info => {
|
||||
mysqlPool.query(
|
||||
'call InsertPlayerRevived(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)',
|
||||
[
|
||||
server.id,
|
||||
info.time,
|
||||
info.woundTime,
|
||||
info.victim ? info.victim.steamID : null,
|
||||
info.victim ? info.victim.name : null,
|
||||
info.victim ? info.victim.teamID : null,
|
||||
info.victim ? info.victim.squadID : null,
|
||||
info.attacker ? info.attacker.steamID : null,
|
||||
info.attacker ? info.attacker.name : null,
|
||||
info.attacker ? info.attacker.teamID : null,
|
||||
info.attacker ? info.attacker.squadID : null,
|
||||
info.damage,
|
||||
info.weapon,
|
||||
info.teamkill,
|
||||
info.reviver ? info.reviver.steamID : null,
|
||||
info.reviver ? info.reviver.name : null,
|
||||
info.reviver ? info.reviver.teamID : null,
|
||||
info.reviver ? info.reviver.squadID : null
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
361
plugins/mysql-log/mysql-schema.sql
Normal file
361
plugins/mysql-log/mysql-schema.sql
Normal file
@ -0,0 +1,361 @@
|
||||
DROP DATABASE squadjs;
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS squadjs;
|
||||
USE squadjs;
|
||||
|
||||
CREATE USER IF NOT EXISTS squadjs IDENTIFIED BY 'password';
|
||||
GRANT ALL PRIVILEGES ON squadjs.* TO squadjs;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `Server` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`name` VARCHAR(255)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `ServerTickRate` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`server` INT NOT NULL,
|
||||
`time` TIMESTAMP NOT NULL,
|
||||
`tick_rate` FLOAT NOT NULL,
|
||||
|
||||
FOREIGN KEY (`server`) REFERENCES `Server`(`id`)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `PlayerCount` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`server` INT NOT NULL,
|
||||
`time` TIMESTAMP NOT NULL,
|
||||
`player_count` FLOAT NOT NULL,
|
||||
|
||||
FOREIGN KEY (`server`) REFERENCES `Server`(`id`)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `Match` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`server` INT NOT NULL,
|
||||
`dlc` VARCHAR(255),
|
||||
`mapClassname` VARCHAR(255),
|
||||
`layerClassname` VARCHAR(255),
|
||||
`map` VARCHAR(255),
|
||||
`layer` VARCHAR(255),
|
||||
`startTime` TIMESTAMP NOT NULL,
|
||||
`endTime` TIMESTAMP NULL DEFAULT NULL,
|
||||
|
||||
FOREIGN KEY (`server`) REFERENCES `Server`(`id`)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `SteamUser` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`steamID` VARCHAR(17) NOT NULL UNIQUE,
|
||||
`lastName` VARCHAR(255)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `PlayerWounded` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`server` INT NOT NULL,
|
||||
`time` TIMESTAMP NOT NULL,
|
||||
`victim` VARCHAR(255),
|
||||
`victimName` VARCHAR(255),
|
||||
`victimTeamID` INT,
|
||||
`victimSquadID` INT,
|
||||
`attacker` VARCHAR(255),
|
||||
`attackerName` VARCHAR(255),
|
||||
`attackerTeamID` INT,
|
||||
`attackerSquadID` INT,
|
||||
`damage` FLOAT,
|
||||
`weapon` VARCHAR(255),
|
||||
`teamkill` BOOLEAN,
|
||||
|
||||
FOREIGN KEY (`server`) REFERENCES `Server`(`id`)
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY (`victim`) REFERENCES `SteamUser`(`steamID`)
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY (`attacker`) REFERENCES `SteamUser`(`steamID`)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `PlayerDied` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`server` INT NOT NULL,
|
||||
`time` TIMESTAMP NOT NULL,
|
||||
`woundTime` TIMESTAMP NOT NULL,
|
||||
`victim` VARCHAR(255),
|
||||
`victimName` VARCHAR(255),
|
||||
`victimTeamID` INT,
|
||||
`victimSquadID` INT,
|
||||
`attacker` VARCHAR(255),
|
||||
`attackerName` VARCHAR(255),
|
||||
`attackerTeamID` INT,
|
||||
`attackerSquadID` INT,
|
||||
`damage` FLOAT,
|
||||
`weapon` VARCHAR(255),
|
||||
`teamkill` BOOLEAN,
|
||||
|
||||
FOREIGN KEY (`server`) REFERENCES `Server`(`id`)
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY (`victim`) REFERENCES `SteamUser`(`steamID`)
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY (`attacker`) REFERENCES `SteamUser`(`steamID`)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `PlayerRevived` (
|
||||
`id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`server` INT NOT NULL,
|
||||
`time` TIMESTAMP NOT NULL,
|
||||
`woundTime` TIMESTAMP NOT NULL,
|
||||
`victim` VARCHAR(255),
|
||||
`victimName` VARCHAR(255),
|
||||
`victimTeamID` INT,
|
||||
`victimSquadID` INT,
|
||||
`attacker` VARCHAR(255),
|
||||
`attackerName` VARCHAR(255),
|
||||
`attackerTeamID` INT,
|
||||
`attackerSquadID` INT,
|
||||
`damage` FLOAT,
|
||||
`weapon` VARCHAR(255),
|
||||
`teamkill` BOOLEAN,
|
||||
`reviver` VARCHAR(255),
|
||||
`reviverName` VARCHAR(255),
|
||||
`reviverTeamID` INT,
|
||||
`reviverSquadID` INT,
|
||||
|
||||
FOREIGN KEY (`server`) REFERENCES `Server`(`id`)
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY (`victim`) REFERENCES `SteamUser`(`steamID`)
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY (`attacker`) REFERENCES `SteamUser`(`steamID`)
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY (`reviver`) REFERENCES `SteamUser`(`steamID`)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
DROP PROCEDURE IF EXISTS `NewMatch`;
|
||||
DELIMITER #
|
||||
CREATE PROCEDURE `NewMatch` (
|
||||
IN `p_server` INT,
|
||||
IN `p_time` TIMESTAMP,
|
||||
IN `p_dlc` VARCHAR(255),
|
||||
IN `p_mapClassname` VARCHAR(255),
|
||||
IN `p_layerClassname` VARCHAR(255),
|
||||
IN `p_map` VARCHAR(255),
|
||||
IN `p_layer` VARCHAR(255)
|
||||
)
|
||||
BEGIN
|
||||
UPDATE `Match` SET `endTime` = `p_time` WHERE `server` = `p_server` AND `endTime` IS NULL;
|
||||
INSERT INTO `Match` (
|
||||
`server`,
|
||||
`startTime`,
|
||||
`dlc`,
|
||||
`mapClassname`,
|
||||
`layerClassname`,
|
||||
`map`,
|
||||
`layer`
|
||||
) VALUES (
|
||||
`p_server`,
|
||||
`p_time`,
|
||||
`p_dlc`,
|
||||
`p_mapClassname`,
|
||||
`p_layerClassname`,
|
||||
`p_map`,
|
||||
`p_layer`
|
||||
);
|
||||
END#
|
||||
DELIMITER ;
|
||||
|
||||
DROP PROCEDURE IF EXISTS `InsertPlayerWounded`;
|
||||
DELIMITER #
|
||||
CREATE PROCEDURE `InsertPlayerWounded` (
|
||||
IN `p_server` INT,
|
||||
IN `p_time` TIMESTAMP,
|
||||
IN `P_victim` VARCHAR(255),
|
||||
IN `p_victimName` VARCHAR(255),
|
||||
IN `p_victimTeamID` INT,
|
||||
IN `p_victimSquadID` INT,
|
||||
IN `p_attacker` VARCHAR(255),
|
||||
IN `p_attackerName` VARCHAR(255),
|
||||
IN `p_attackerTeamID` INT,
|
||||
IN `p_attackerSquadID` INT,
|
||||
IN `p_damage` FLOAT,
|
||||
IN `p_weapon` VARCHAR(255),
|
||||
IN `p_teamkill` BOOLEAN
|
||||
)
|
||||
BEGIN
|
||||
-- insert players into SteamUsers table
|
||||
INSERT INTO `SteamUser` (`steamID`, `lastName`) VALUES (`p_victim`, `p_victimName`)
|
||||
ON DUPLICATE KEY UPDATE `lastName` = `p_victimName`;
|
||||
|
||||
INSERT INTO `SteamUser` (`steamID`, `lastName`) VALUES (`p_attacker`, `p_attackerName`)
|
||||
ON DUPLICATE KEY UPDATE `lastName` = `p_attackerName`;
|
||||
|
||||
-- create wound record
|
||||
INSERT INTO `PlayerWounded` (
|
||||
`server`,
|
||||
`time`,
|
||||
`victim`,
|
||||
`victimName`,
|
||||
`victimTeamID`,
|
||||
`victimSquadID`,
|
||||
`attacker`,
|
||||
`attackerName`,
|
||||
`attackerTeamID`,
|
||||
`attackerSquadID`,
|
||||
`damage`,
|
||||
`weapon`,
|
||||
`teamkill`
|
||||
) VALUES (
|
||||
`p_server`,
|
||||
`p_time`,
|
||||
`p_victim`,
|
||||
`p_victimName`,
|
||||
`p_victimTeamID`,
|
||||
`p_victimSquadID`,
|
||||
`p_attacker`,
|
||||
`p_attackerName`,
|
||||
`p_attackerTeamID`,
|
||||
`p_attackerSquadID`,
|
||||
`p_damage`,
|
||||
`p_weapon`,
|
||||
`p_teamkill`
|
||||
);
|
||||
END#
|
||||
DELIMITER ;
|
||||
|
||||
DROP PROCEDURE IF EXISTS `InsertPlayerDied`;
|
||||
DELIMITER #
|
||||
CREATE PROCEDURE `InsertPlayerDied` (
|
||||
IN `p_server` INT,
|
||||
IN `p_time` TIMESTAMP,
|
||||
IN `p_woundTime` TIMESTAMP,
|
||||
IN `P_victim` VARCHAR(255),
|
||||
IN `p_victimName` VARCHAR(255),
|
||||
IN `p_victimTeamID` INT,
|
||||
IN `p_victimSquadID` INT,
|
||||
IN `p_attacker` VARCHAR(255),
|
||||
IN `p_attackerName` VARCHAR(255),
|
||||
IN `p_attackerTeamID` INT,
|
||||
IN `p_attackerSquadID` INT,
|
||||
IN `p_damage` FLOAT,
|
||||
IN `p_weapon` VARCHAR(255),
|
||||
IN `p_teamkill` BOOLEAN
|
||||
)
|
||||
BEGIN
|
||||
-- insert players into SteamUsers table
|
||||
INSERT INTO `SteamUser` (`steamID`, `lastName`) VALUES (`p_victim`, `p_victimName`)
|
||||
ON DUPLICATE KEY UPDATE `lastName` = `p_victimName`;
|
||||
|
||||
INSERT INTO `SteamUser` (`steamID`, `lastName`) VALUES (`p_attacker`, `p_attackerName`)
|
||||
ON DUPLICATE KEY UPDATE `lastName` = `p_attackerName`;
|
||||
|
||||
-- create die record
|
||||
INSERT INTO `PlayerDied` (
|
||||
`server`,
|
||||
`time`,
|
||||
`woundTime`,
|
||||
`victim`,
|
||||
`victimName`,
|
||||
`victimTeamID`,
|
||||
`victimSquadID`,
|
||||
`attacker`,
|
||||
`attackerName`,
|
||||
`attackerTeamID`,
|
||||
`attackerSquadID`,
|
||||
`damage`,
|
||||
`weapon`,
|
||||
`teamkill`
|
||||
) VALUES (
|
||||
`p_server`,
|
||||
`p_time`,
|
||||
`p_woundTime`,
|
||||
`p_victim`,
|
||||
`p_victimName`,
|
||||
`p_victimTeamID`,
|
||||
`p_victimSquadID`,
|
||||
`p_attacker`,
|
||||
`p_attackerName`,
|
||||
`p_attackerTeamID`,
|
||||
`p_attackerSquadID`,
|
||||
`p_damage`,
|
||||
`p_weapon`,
|
||||
`p_teamkill`
|
||||
);
|
||||
END#
|
||||
DELIMITER ;
|
||||
|
||||
DROP PROCEDURE IF EXISTS `InsertPlayerRevived`;
|
||||
DELIMITER #
|
||||
CREATE PROCEDURE `InsertPlayerRevived` (
|
||||
IN `p_server` INT,
|
||||
IN `p_time` TIMESTAMP,
|
||||
IN `p_woundTime` TIMESTAMP,
|
||||
IN `P_victim` VARCHAR(255),
|
||||
IN `p_victimName` VARCHAR(255),
|
||||
IN `p_victimTeamID` INT,
|
||||
IN `p_victimSquadID` INT,
|
||||
IN `p_attacker` VARCHAR(255),
|
||||
IN `p_attackerName` VARCHAR(255),
|
||||
IN `p_attackerTeamID` INT,
|
||||
IN `p_attackerSquadID` INT,
|
||||
IN `p_damage` FLOAT,
|
||||
IN `p_weapon` VARCHAR(255),
|
||||
IN `p_teamkill` BOOLEAN,
|
||||
IN `p_reviver` VARCHAR(255),
|
||||
IN `p_reviverName` VARCHAR(255),
|
||||
IN `p_reviverTeamID` VARCHAR(255),
|
||||
IN `p_reviverSquadID` VARCHAR(255)
|
||||
)
|
||||
BEGIN
|
||||
-- insert players into SteamUsers table
|
||||
INSERT INTO `SteamUser` (`steamID`, `lastName`) VALUES (`p_victim`, `p_victimName`)
|
||||
ON DUPLICATE KEY UPDATE `lastName` = `p_victimName`;
|
||||
|
||||
INSERT INTO `SteamUser` (`steamID`, `lastName`) VALUES (`p_attacker`, `p_attackerName`)
|
||||
ON DUPLICATE KEY UPDATE `lastName` = `p_attackerName`;
|
||||
|
||||
INSERT INTO `SteamUser` (`steamID`, `lastName`) VALUES (`p_reviver`, `p_reviverName`)
|
||||
ON DUPLICATE KEY UPDATE `lastName` = `p_reviverName`;
|
||||
|
||||
-- create revive record
|
||||
INSERT INTO `PlayerRevived` (
|
||||
`server`,
|
||||
`time`,
|
||||
`woundTime`,
|
||||
`victim`,
|
||||
`victimName`,
|
||||
`victimTeamID`,
|
||||
`victimSquadID`,
|
||||
`attacker`,
|
||||
`attackerName`,
|
||||
`attackerTeamID`,
|
||||
`attackerSquadID`,
|
||||
`damage`,
|
||||
`weapon`,
|
||||
`teamkill`,
|
||||
`reviver`,
|
||||
`reviverName`,
|
||||
`reviverTeamID`,
|
||||
`reviverSquadID`
|
||||
) VALUES (
|
||||
`p_server`,
|
||||
`p_time`,
|
||||
`p_woundTime`,
|
||||
`p_victim`,
|
||||
`p_victimName`,
|
||||
`p_victimTeamID`,
|
||||
`p_victimSquadID`,
|
||||
`p_attacker`,
|
||||
`p_attackerName`,
|
||||
`p_attackerTeamID`,
|
||||
`p_attackerSquadID`,
|
||||
`p_damage`,
|
||||
`p_weapon`,
|
||||
`p_teamkill`,
|
||||
`p_reviver`,
|
||||
`p_reviverName`,
|
||||
`p_reviverTeamID`,
|
||||
`p_reviverSquadID`
|
||||
);
|
||||
END#
|
||||
DELIMITER ;
|
14
plugins/package.json
Normal file
14
plugins/package.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "plugins",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"connectors": "1.0.0",
|
||||
"didyoumean": "^1.2.1",
|
||||
"influx": "^5.5.1",
|
||||
"squad-server": "1.0.0"
|
||||
}
|
||||
}
|
27
plugins/seeding-message/README.md
Normal file
27
plugins/seeding-message/README.md
Normal file
@ -0,0 +1,27 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="../../assets/squadjs-logo.png" alt="Logo" width="500"/>
|
||||
|
||||
#### SquadJS - Seeding Message
|
||||
</div>
|
||||
|
||||
## About
|
||||
Displays a seeding message when player count is below a certain level and displays a live message when player count is slightly above that level.
|
||||
|
||||
## Installation
|
||||
Place the following into your `index.js` file. The options below are optional and can be removed without affecting functionality, however, the default options are shown for reference.
|
||||
```js
|
||||
seedingMessage(
|
||||
server,
|
||||
{ // options - remove or edit the below options. The defaults are shown.
|
||||
mode: 'interval', // interval displays every x seconds, onjoin displays x seconds after a player joins the server
|
||||
interval: 150 * 1000, // how often the seeding message is displayed in milliseconds in interval mode
|
||||
delay: 45 * 1000, // delay between player connecting and seeding message in onjoin mode
|
||||
seedingThreshold: 50, // seeding messages are displayed when player count is below this number
|
||||
seedingMessage: 'Seeding Rules Active! Fight only over the middle flags! No FOB Hunting!', // message to display in seeding mode
|
||||
liveEnabled: true, // display live message
|
||||
liveThreshold: 2, // live message will display when player count exceeds seedingThreshold by up to this amount
|
||||
liveMessage: 'Live!' // message to display when server is live
|
||||
}
|
||||
);
|
||||
```
|
62
plugins/seeding-message/index.js
Normal file
62
plugins/seeding-message/index.js
Normal file
@ -0,0 +1,62 @@
|
||||
import { LOG_PARSER_PLAYER_CONNECTED } from 'squad-server/events/log-parser';
|
||||
|
||||
export default function(server, options = {}) {
|
||||
if (!server)
|
||||
throw new Error(
|
||||
'SeedingMessage must be provided with a reference to the server.'
|
||||
);
|
||||
|
||||
const mode = options.mode || 'interval';
|
||||
const interval = options.interval || 150 * 1000;
|
||||
const delay = options.delay || 45 * 1000;
|
||||
|
||||
const seedingThreshold = options.seedingThreshold || 50;
|
||||
const seedingMessage =
|
||||
options.seedingMessage ||
|
||||
'Seeding Rules Active! Fight only over the middle flags! No FOB Hunting!';
|
||||
|
||||
const liveEnabled = options.liveEnabled || true;
|
||||
const liveThreshold = seedingThreshold + (options.liveThreshold || 2);
|
||||
const liveMessage = options.liveMessage || 'Live!';
|
||||
|
||||
switch (mode) {
|
||||
case 'interval':
|
||||
setInterval(() => {
|
||||
const playerCount = server.players.length;
|
||||
|
||||
if (playerCount === 0) return;
|
||||
|
||||
if (playerCount < seedingThreshold) {
|
||||
server.rcon.execute(`AdminBroadcast ${seedingMessage}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (liveEnabled && playerCount < liveThreshold) {
|
||||
server.rcon.execute(`AdminBroadcast ${liveMessage}`);
|
||||
}
|
||||
}, interval);
|
||||
|
||||
break;
|
||||
case 'onjoin':
|
||||
server.on(LOG_PARSER_PLAYER_CONNECTED, () => {
|
||||
setTimeout(() => {
|
||||
const playerCount = server.players.length;
|
||||
|
||||
if (playerCount === 0) return;
|
||||
|
||||
if (playerCount < seedingThreshold) {
|
||||
server.rcon.execute(`AdminBroadcast ${seedingMessage}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (liveEnabled && playerCount < liveThreshold) {
|
||||
server.rcon.execute(`AdminBroadcast ${liveMessage}`);
|
||||
}
|
||||
}, delay);
|
||||
});
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid SeedingMessage mode.');
|
||||
}
|
||||
}
|
20
plugins/team-randomizer/README.md
Normal file
20
plugins/team-randomizer/README.md
Normal file
@ -0,0 +1,20 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="../../assets/squadjs-logo.png" alt="Logo" width="500"/>
|
||||
|
||||
#### SquadJS - Team Randomizer
|
||||
</div>
|
||||
|
||||
## About
|
||||
The team randomizer randomly moves players to a team when `!randomize` is called in admin chat.
|
||||
|
||||
## Installation
|
||||
Place the following into your `index.js` file. The options below are optional and can be removed without affecting functionality, however, the default options are shown for reference.
|
||||
```js
|
||||
teamRandomizer(
|
||||
server,
|
||||
{ // options - remove or edit the below options. The defaults are shown.
|
||||
command: '!randomize', // the command name used to access the feature
|
||||
}
|
||||
);
|
||||
```
|
51
plugins/team-randomizer/index.js
Normal file
51
plugins/team-randomizer/index.js
Normal file
@ -0,0 +1,51 @@
|
||||
import { RCON_CHAT_MESSAGE } from 'squad-server/events/rcon';
|
||||
|
||||
function shuffle(array) {
|
||||
let currentIndex = array.length;
|
||||
let temporaryValue;
|
||||
let randomIndex;
|
||||
|
||||
// While there remain elements to shuffle...
|
||||
while (currentIndex !== 0) {
|
||||
// Pick a remaining element...
|
||||
randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex -= 1;
|
||||
|
||||
// And swap it with the current element.
|
||||
temporaryValue = array[currentIndex];
|
||||
array[currentIndex] = array[randomIndex];
|
||||
array[randomIndex] = temporaryValue;
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
export default function(server, options = {}) {
|
||||
if (!server)
|
||||
throw new Error(
|
||||
'TeamRandomizer must be provided with a reference to the server.'
|
||||
);
|
||||
|
||||
const command = options.command || '!randomize';
|
||||
const commandRegex = new RegExp(`^${command}`, 'i');
|
||||
|
||||
server.on(RCON_CHAT_MESSAGE, info => {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
|
||||
const match = info.message.match(commandRegex);
|
||||
if (!match) return;
|
||||
|
||||
const players = server.players.slice(0);
|
||||
shuffle(players);
|
||||
|
||||
let team = '1';
|
||||
|
||||
for (const player of players) {
|
||||
if (player.teamID !== team) {
|
||||
server.rcon.execute(`AdminForceTeamChange "${player.steamID}"`);
|
||||
}
|
||||
|
||||
team = team === '1' ? '2' : '1';
|
||||
}
|
||||
});
|
||||
}
|
123
squad-server/events/log-parser.js
Normal file
123
squad-server/events/log-parser.js
Normal file
@ -0,0 +1,123 @@
|
||||
/** Occurs when an admin enters admin camera.
|
||||
*
|
||||
* Data:
|
||||
* - time - Date object of when the event occurred.
|
||||
* - player - PlayerObject of the admin.
|
||||
*/
|
||||
const LOG_PARSER_ADMIN_POSSESS_CAMERA = 'LOG_PARSER_ADMIN_POSSESS_CAMERA';
|
||||
|
||||
/** Occurs when a new layer is loaded.
|
||||
*
|
||||
* Data:
|
||||
* - time - Date object of when the event occurred.
|
||||
* - dlc - DLC / Mod the layer was loaded from.
|
||||
* - mapClassname - Classname of the map.
|
||||
* - layerClassname - Classname of the layer.
|
||||
* - map - Map name (if known).
|
||||
* - layer - Layer name (if known).
|
||||
*/
|
||||
const LOG_PARSER_NEW_GAME = 'LOG_PARSER_NEW_GAME';
|
||||
|
||||
/** Occurs when a new player connects.
|
||||
*
|
||||
* Data:
|
||||
* - time - Date object of when the event occurred.
|
||||
* - player - PlayerObject of the player.
|
||||
*/
|
||||
const LOG_PARSER_PLAYER_CONNECTED = 'LOG_PARSER_PLAYER_CONNECTED';
|
||||
|
||||
/** Occurs when a player is damaged.
|
||||
*
|
||||
* Data:
|
||||
* - time - Date object of when the event occurred.
|
||||
* - victim - PlayerObject of the damaged player.
|
||||
* - damage - Amount of damage inflicted.
|
||||
* - attacker - PlayerObject of the attacking player.
|
||||
* - weapon - The classname of the weapon used.
|
||||
*/
|
||||
const LOG_PARSER_PLAYER_DAMAGED = 'LOG_PARSER_PLAYER_DAMAGED';
|
||||
|
||||
/** Occurs when a player dies.
|
||||
*
|
||||
* Data:
|
||||
* - time - Date object of when the event occurred.
|
||||
* - woundTime - Date object of when the wound event occurred.
|
||||
* - victim - PlayerObject of the damaged player.
|
||||
* - damage - Amount of damage inflicted.
|
||||
* - attacker - PlayerObject of the attacking player.
|
||||
* - attackerPlayerController - PlayerController of the attacking player.
|
||||
* - weapon - The classname of the weapon used.
|
||||
* - teamkill - Whether the kill was a teamkill.
|
||||
*/
|
||||
const LOG_PARSER_PLAYER_DIED = 'LOG_PARSER_PLAYER_DIED';
|
||||
|
||||
/** Occurs when a player possess a new object.
|
||||
*
|
||||
* Data:
|
||||
* - time - Date object of when the event occurred.
|
||||
* - player - PlayerObject of the admin.
|
||||
* - possessClassname - Classname of the object.
|
||||
*/
|
||||
const LOG_PARSER_PLAYER_POSSESS = 'LOG_PARSER_PLAYER_POSSESS';
|
||||
|
||||
/** Occurs when a player is revived.
|
||||
*
|
||||
* Data:
|
||||
* - time - Date object of when the event occurred.
|
||||
* - woundTime - Date object of when the wound event occurred.
|
||||
* - victim - PlayerObject of the damaged player.
|
||||
* - damage - Amount of damage inflicted.
|
||||
* - attacker - PlayerObject of the attacking player.
|
||||
* - attackerPlayerController - PlayerController of the attacking player.
|
||||
* - weapon - The classname of the weapon used.
|
||||
* - teamkill - Whether the kill was a teamkill.
|
||||
* - reviver - PlayerObject of the reviving player.
|
||||
*/
|
||||
const LOG_PARSER_PLAYER_REVIVED = 'LOG_PARSER_PLAYER_REVIVED';
|
||||
|
||||
/** Occurs when a player is teamkilled.
|
||||
*
|
||||
* Data:
|
||||
* - time - Date object of when the event occurred.
|
||||
* - victim - PlayerObject of the damaged player.
|
||||
* - damage - Amount of damage inflicted.
|
||||
* - attacker - PlayerObject of the attacking player.
|
||||
* - attackerPlayerController - PlayerController of the attacking player.
|
||||
* - weapon - The classname of the weapon used.
|
||||
* - teamkill - Whether the kill was a teamkill.
|
||||
*/
|
||||
const LOG_PARSER_TEAMKILL = 'LOG_PARSER_TEAMKILL';
|
||||
|
||||
/** Occurs when a player is wounded.
|
||||
*
|
||||
* Data:
|
||||
* - time - Date object of when the event occurred.
|
||||
* - victim - PlayerObject of the damaged player.
|
||||
* - damage - Amount of damage inflicted.
|
||||
* - attacker - PlayerObject of the attacking player.
|
||||
* - attackerPlayerController - PlayerController of the attacking player.
|
||||
* - weapon - The classname of the weapon used.
|
||||
* - teamkill - Whether the kill was a teamkill.
|
||||
*/
|
||||
const LOG_PARSER_PLAYER_WOUNDED = 'LOG_PARSER_PLAYER_WOUNDED';
|
||||
|
||||
/** Occurs when the server tick rate is updated.
|
||||
*
|
||||
* Data:
|
||||
* - time - Date object of when the event occurred.
|
||||
* - tickRate - Tick rate of the server.
|
||||
*/
|
||||
const LOG_PARSER_SERVER_TICK_RATE = 'LOG_PARSER_SERVER_TICK_RATE';
|
||||
|
||||
export {
|
||||
LOG_PARSER_ADMIN_POSSESS_CAMERA,
|
||||
LOG_PARSER_NEW_GAME,
|
||||
LOG_PARSER_PLAYER_CONNECTED,
|
||||
LOG_PARSER_PLAYER_DAMAGED,
|
||||
LOG_PARSER_PLAYER_DIED,
|
||||
LOG_PARSER_PLAYER_POSSESS,
|
||||
LOG_PARSER_PLAYER_REVIVED,
|
||||
LOG_PARSER_TEAMKILL,
|
||||
LOG_PARSER_PLAYER_WOUNDED,
|
||||
LOG_PARSER_SERVER_TICK_RATE
|
||||
};
|
19
squad-server/events/rcon.js
Normal file
19
squad-server/events/rcon.js
Normal file
@ -0,0 +1,19 @@
|
||||
/** Occurs when an RCON error occurs.
|
||||
*
|
||||
* Data:
|
||||
* - ErrorObject
|
||||
*/
|
||||
const RCON_ERROR = 'RCON_ERROR';
|
||||
|
||||
/** Occurs when an admin enters admin camera.
|
||||
*
|
||||
* Data:
|
||||
* - chat - Chat the message was sent to.
|
||||
* - steamID - Steam ID of the player.
|
||||
* - player - Name of the player.
|
||||
* - message - Message sent.
|
||||
* - time - Time message was sent, AKA now.
|
||||
*/
|
||||
const RCON_CHAT_MESSAGE = 'RCON_CHAT_MESSAGE';
|
||||
|
||||
export { RCON_ERROR, RCON_CHAT_MESSAGE };
|
47
squad-server/events/server.js
Normal file
47
squad-server/events/server.js
Normal file
@ -0,0 +1,47 @@
|
||||
/** Occurs when a new layer is loaded.
|
||||
*
|
||||
* Data:
|
||||
* - time - Date object of when the event occurred.
|
||||
* - dlc - DLC / Mod the layer was loaded from.
|
||||
* - mapClassname - Classname of the map.
|
||||
* - layerClassname - Classname of the layer.
|
||||
* - map - Map name (if known).
|
||||
* - layer - Layer name (if known).
|
||||
*/
|
||||
const SERVER_LAYER_CHANGE = 'SERVER_LAYER_CHANGE';
|
||||
|
||||
/** Occurs when the player list is updated via RCON.
|
||||
*
|
||||
* Data:
|
||||
* - Array of PlayerObjects
|
||||
*/
|
||||
const SERVER_PLAYERS_UPDATED = 'SERVER_PLAYERS_UPDATED';
|
||||
|
||||
/** Occurs when the layer info is updated via RCON.
|
||||
*
|
||||
* Data:
|
||||
* - currentLayer - Current layer.
|
||||
* - nextLayer - Next layer.
|
||||
*/
|
||||
const SERVER_LAYERS_UPDATED = 'SERVER_LAYERS_UPDATED';
|
||||
|
||||
/** Occurs when the server info is updated via A2S.
|
||||
*
|
||||
* Data:
|
||||
* - serverName - Name of the server.
|
||||
* - maxPlayers - Maximum number of players on the server.
|
||||
* - publicSlots - Maximum number of public slots.
|
||||
* - reserveSlots - Maximum number of reserved slots.
|
||||
* - publicQueue - Length of the public queue.
|
||||
* - reserveQueue - Length of the reserved queue.
|
||||
* - matchTimeout - Time until match ends?
|
||||
* - gameVersion - Game version.
|
||||
*/
|
||||
const SERVER_A2S_UPDATED = 'SERVER_A2S_UPDATED';
|
||||
|
||||
export {
|
||||
SERVER_LAYER_CHANGE,
|
||||
SERVER_PLAYERS_UPDATED,
|
||||
SERVER_LAYERS_UPDATED,
|
||||
SERVER_A2S_UPDATED
|
||||
};
|
181
squad-server/index.js
Normal file
181
squad-server/index.js
Normal file
@ -0,0 +1,181 @@
|
||||
import EventEmitter from 'events';
|
||||
|
||||
import Gamedig from 'gamedig';
|
||||
|
||||
import printLogo from 'core/utils/print-logo';
|
||||
|
||||
import LogParser from './log-parser/index.js';
|
||||
import Rcon from './rcon/index.js';
|
||||
|
||||
import {
|
||||
SERVER_LAYER_CHANGE,
|
||||
SERVER_PLAYERS_UPDATED,
|
||||
SERVER_LAYERS_UPDATED,
|
||||
SERVER_A2S_UPDATED
|
||||
} from './events/server.js';
|
||||
|
||||
import { LOG_PARSER_NEW_GAME } from './events/log-parser.js';
|
||||
|
||||
export default class Server extends EventEmitter {
|
||||
constructor(options = {}) {
|
||||
super();
|
||||
|
||||
// store options
|
||||
if (!('id' in options)) throw new Error('Server must have an ID.');
|
||||
this.id = options.id;
|
||||
|
||||
if (!('host' in options)) throw new Error('Server must have a host.');
|
||||
this.host = options.host;
|
||||
|
||||
if (!('queryPort' in options))
|
||||
throw new Error('Server must have a queryPort.');
|
||||
this.queryPort = options.queryPort;
|
||||
|
||||
this.updateInterval = options.updateInterval || 30 * 1000;
|
||||
|
||||
// setup additional classes
|
||||
this.rcon = new Rcon(options, this);
|
||||
this.logParser = new LogParser(options, this);
|
||||
|
||||
// setup internal data storage
|
||||
this.layerHistory = options.layerHistory || [];
|
||||
this.layerHistoryMaxLength = options.layerHistoryMaxLength || 20;
|
||||
|
||||
this.players = [];
|
||||
|
||||
// store additional information about players by SteamID
|
||||
this.suffixStore = {};
|
||||
|
||||
// setup internal listeners
|
||||
this.on(LOG_PARSER_NEW_GAME, this.onLayerChange.bind(this));
|
||||
|
||||
// setup period updaters
|
||||
this.updatePlayers = this.updatePlayers.bind(this);
|
||||
this.updatePlayerTimeout = setTimeout(
|
||||
this.updatePlayers,
|
||||
this.updateInterval
|
||||
);
|
||||
|
||||
setTimeout(async () => {
|
||||
const data = await this.rcon.getMapInfo();
|
||||
this.currentLayer = data.currentLayer;
|
||||
this.nextLayer = data.nextLayer;
|
||||
this.emit(SERVER_LAYERS_UPDATED, data);
|
||||
}, this.updateInterval);
|
||||
|
||||
setTimeout(async () => {
|
||||
const data = await Gamedig.query({
|
||||
type: 'squad',
|
||||
host: this.host,
|
||||
port: this.queryPort
|
||||
});
|
||||
|
||||
this.serverName = data.name;
|
||||
|
||||
this.maxPlayers = parseInt(data.maxplayers);
|
||||
this.publicSlots = parseInt(data.raw.rules.NUMPUBCONN);
|
||||
this.reserveSlots = parseInt(data.raw.rules.NUMPRIVCONN);
|
||||
|
||||
this.a2sPlayerCount = Math.min(data.players.length, this.maxPlayers);
|
||||
this.publicQueue = parseInt(data.raw.rules.PublicQueue_i);
|
||||
this.reserveQueue = parseInt(data.raw.rules.ReservedQueue_i);
|
||||
|
||||
this.matchTimeout = parseFloat(data.raw.rules.MatchTimeout_f);
|
||||
this.gameVersion = data.raw.version;
|
||||
|
||||
this.emit(SERVER_A2S_UPDATED, {
|
||||
serverName: this.serverName,
|
||||
maxPlayers: this.maxPlayers,
|
||||
publicSlots: this.publicSlots,
|
||||
reserveSlots: this.reserveSlots,
|
||||
publicQueue: this.publicQueue,
|
||||
reserveQueue: this.reserveQueue,
|
||||
matchTimeout: this.matchTimeout,
|
||||
gameVersion: this.gameVersion
|
||||
});
|
||||
}, this.updateInterval);
|
||||
}
|
||||
|
||||
async watch() {
|
||||
printLogo();
|
||||
console.log(`Watching server ${this.id}...`);
|
||||
if (this.logParser) await this.logParser.watch();
|
||||
if (this.rcon) await this.rcon.watch();
|
||||
}
|
||||
|
||||
async unwatch() {
|
||||
if (this.logParser) await this.logParser.unwatch();
|
||||
if (this.rcon) await this.rcon.unwatch();
|
||||
console.log('Stopped watching.');
|
||||
}
|
||||
|
||||
async updatePlayers() {
|
||||
clearTimeout(this.updatePlayerTimeout);
|
||||
|
||||
this.players = await this.rcon.listPlayers();
|
||||
|
||||
// readd additional information about the player we have collected
|
||||
for (let i = 0; i < this.players.length; i++) {
|
||||
this.players[i].suffix = this.suffixStore[this.players[i].steamID];
|
||||
}
|
||||
|
||||
// delay another update
|
||||
this.updatePlayerTimeout = setTimeout(
|
||||
this.updatePlayers,
|
||||
this.updateInterval
|
||||
);
|
||||
|
||||
this.emit(SERVER_PLAYERS_UPDATED, this.players);
|
||||
}
|
||||
|
||||
async getPlayerByName(name, suffix = false) {
|
||||
let matchingPlayers;
|
||||
|
||||
matchingPlayers = [];
|
||||
for (const player of this.players) {
|
||||
if (player[suffix ? 'suffix' : 'name'] !== name) continue;
|
||||
matchingPlayers.push(player);
|
||||
}
|
||||
|
||||
if (matchingPlayers.length === 0) {
|
||||
await this.updatePlayers();
|
||||
|
||||
matchingPlayers = [];
|
||||
for (const player of this.players) {
|
||||
if (player[suffix ? 'suffix' : 'name'] !== name) continue;
|
||||
matchingPlayers.push(player);
|
||||
}
|
||||
}
|
||||
|
||||
if (matchingPlayers.length === 1) return matchingPlayers[0];
|
||||
else return null;
|
||||
}
|
||||
|
||||
async getPlayerBySteamID(steamID) {
|
||||
let matchingPlayers;
|
||||
|
||||
matchingPlayers = [];
|
||||
for (const player of this.players) {
|
||||
if (player.steamID !== steamID) continue;
|
||||
matchingPlayers.push(player);
|
||||
}
|
||||
|
||||
if (matchingPlayers.length === 0) {
|
||||
await this.updatePlayers();
|
||||
|
||||
matchingPlayers = [];
|
||||
for (const player of this.players) {
|
||||
if (player.steamID !== steamID) continue;
|
||||
matchingPlayers.push(player);
|
||||
}
|
||||
}
|
||||
|
||||
return matchingPlayers[0];
|
||||
}
|
||||
|
||||
onLayerChange(info) {
|
||||
this.layerHistory.unshift(info);
|
||||
this.layerHistory = this.layerHistory.slice(0, this.layerHistoryMaxLength);
|
||||
this.emit(SERVER_LAYER_CHANGE, info);
|
||||
}
|
||||
}
|
57
squad-server/log-parser/index.js
Normal file
57
squad-server/log-parser/index.js
Normal file
@ -0,0 +1,57 @@
|
||||
import async from 'async';
|
||||
import moment from 'moment';
|
||||
|
||||
import Server from '../index.js';
|
||||
|
||||
import TailLogReader from './log-readers/tail.js';
|
||||
import FTPLogReader from './log-readers/ftp.js';
|
||||
|
||||
import rules from './rules/index.js';
|
||||
|
||||
export default class LogParser {
|
||||
constructor(options = {}, server) {
|
||||
if (!(server instanceof Server))
|
||||
throw new Error('Server not an instance of a SquadJS server.');
|
||||
this.server = server;
|
||||
this.eventStore = {};
|
||||
|
||||
this.queueLine = this.queueLine.bind(this);
|
||||
this.handleLine = this.handleLine.bind(this);
|
||||
this.queue = async.queue(this.handleLine);
|
||||
|
||||
switch (options.logReaderMode || 'tail') {
|
||||
case 'tail':
|
||||
this.logReader = new TailLogReader(this.queueLine, options);
|
||||
break;
|
||||
case 'ftp':
|
||||
this.logReader = new FTPLogReader(this.queueLine, options);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid mode.');
|
||||
}
|
||||
}
|
||||
|
||||
async watch() {
|
||||
await this.logReader.watch();
|
||||
}
|
||||
|
||||
async unwatch() {
|
||||
await this.logReader.unwatch();
|
||||
}
|
||||
|
||||
queueLine(line) {
|
||||
this.queue.push(line);
|
||||
}
|
||||
|
||||
async handleLine(line) {
|
||||
for (const rule of rules) {
|
||||
const match = line.match(rule.regex);
|
||||
if (!match) continue;
|
||||
|
||||
match[1] = moment.utc(match[1], 'YYYY.MM.DD-hh.mm.ss:SSS').toDate();
|
||||
match[2] = parseInt(match[2]);
|
||||
await rule.onMatch(match, this);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
121
squad-server/log-parser/log-readers/ftp.js
Normal file
121
squad-server/log-parser/log-readers/ftp.js
Normal file
@ -0,0 +1,121 @@
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import ftp from 'basic-ftp';
|
||||
|
||||
import sleep from 'core/utils/sleep';
|
||||
|
||||
// THIS LOG READER IS CURRENTLY UNDER DEVELOPMENT. IT IS ADVISED THAT YOU DO NOT USE IT.
|
||||
export default class FTPLogReader {
|
||||
constructor(queueLine, options = {}) {
|
||||
if (typeof queueLine !== 'function')
|
||||
throw new Error(
|
||||
'queueLine argument must be specified and be a function.'
|
||||
);
|
||||
if (!options.host) throw new Error('Host must be specified.');
|
||||
if (!options.ftpUser) throw new Error('FTP user must be specified.');
|
||||
if (!options.ftpPassword)
|
||||
throw new Error('FTP password must be specified.');
|
||||
if (!options.remotePath) throw new Error('Remote path must be specified.');
|
||||
|
||||
this.queueLine = queueLine;
|
||||
this.host = options.host;
|
||||
this.port = options.ftpPort || 21;
|
||||
this.user = options.ftpUser;
|
||||
this.password = options.ftpPassword;
|
||||
this.remotePath = options.logDir;
|
||||
this.timeout = options.ftpTimeout || 3000;
|
||||
this.encoding = 'utf8';
|
||||
this.defaultInterval = options.ftpPullInterval || 500;
|
||||
this.interval = this.defaultInterval;
|
||||
this.tempFilePath = path.join(
|
||||
process.cwd(),
|
||||
'temp',
|
||||
crypto
|
||||
.createHash('md5')
|
||||
.update(this.host.replace(/\./g, '-') + this.port + this.remotePath)
|
||||
.digest('hex') + '.tmp'
|
||||
);
|
||||
this.maxTempFileSize = 5 * 1000 * 1000; // 5 MB
|
||||
this.tailLastBytes = 100 * 1000;
|
||||
}
|
||||
|
||||
async watch() {
|
||||
this.client = new ftp.Client(this.timeout);
|
||||
this.client.ftp.encoding = this.encoding;
|
||||
|
||||
await this.client.access({
|
||||
host: this.host,
|
||||
port: this.port,
|
||||
user: this.user,
|
||||
password: this.password
|
||||
});
|
||||
|
||||
this.interval = this.defaultInterval;
|
||||
this.runLoop();
|
||||
}
|
||||
|
||||
async unwatch() {
|
||||
this.client.close();
|
||||
this.interval = -1;
|
||||
if (fs.existsSync(this.tempFilePath)) {
|
||||
fs.unlinkSync(this.tempFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
async runLoop() {
|
||||
while (this.interval !== -1) {
|
||||
const runStartTime = Date.now();
|
||||
|
||||
if (fs.existsSync(this.tempFilePath)) {
|
||||
const { size } = fs.statSync(this.tempFilePath);
|
||||
if (size > this.maxTempFileSize || !this.lastByteReceived) {
|
||||
fs.unlinkSync(this.tempFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
// If we haven't received any data yet, tail the end of the file; else download all data since last pull
|
||||
if (this.lastByteReceived == null) {
|
||||
const fileSize = await this.client.size(this.remotePath);
|
||||
this.lastByteReceived =
|
||||
fileSize -
|
||||
(this.tailLastBytes < fileSize ? this.tailLastBytes : fileSize);
|
||||
}
|
||||
|
||||
// Download the data to a temp file, overwrite any previous data
|
||||
// we overwrite previous data to calculate how much data we've received
|
||||
await this.client.downloadTo(
|
||||
fs.createWriteStream(this.tempFilePath, { flags: 'w' }),
|
||||
this.remotePath,
|
||||
this.lastByteReceived
|
||||
);
|
||||
const downloadSize = fs.statSync(this.tempFilePath).size;
|
||||
this.lastByteReceived += downloadSize; // update the last byte marker - this is so we can get data since this position on the ftp download
|
||||
|
||||
const fileData = await new Promise((resolve, reject) => {
|
||||
fs.readFile(this.tempFilePath, (err, data) => {
|
||||
if (err) reject(err);
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
fileData
|
||||
.toString('utf8')
|
||||
.split('\r\n')
|
||||
.forEach(this.queueLine);
|
||||
|
||||
const ftpDataTime = Date.now();
|
||||
const ftpDataTimeMs = ftpDataTime - runStartTime;
|
||||
|
||||
console.log('FTP Retrieve took: ' + ftpDataTimeMs + 'ms');
|
||||
|
||||
const waitTime = this.interval - ftpDataTimeMs;
|
||||
if (waitTime > 0) {
|
||||
await sleep(waitTime);
|
||||
}
|
||||
const runEndTime = Date.now();
|
||||
console.log('Run time: ' + (runEndTime - runStartTime) + 'ms');
|
||||
}
|
||||
}
|
||||
}
|
29
squad-server/log-parser/log-readers/tail.js
Normal file
29
squad-server/log-parser/log-readers/tail.js
Normal file
@ -0,0 +1,29 @@
|
||||
import path from 'path';
|
||||
import TailModule from 'tail';
|
||||
|
||||
export default class TailLogReader {
|
||||
constructor(queueLine, options = {}) {
|
||||
if (typeof queueLine !== 'function')
|
||||
throw new Error(
|
||||
'queueLine argument must be specified and be a function.'
|
||||
);
|
||||
if (!options.logDir) throw new Error('Log directory must be specified.');
|
||||
|
||||
this.reader = new TailModule.Tail(
|
||||
path.join(options.logDir, 'SquadGame.log'),
|
||||
{
|
||||
useWatchFile: true
|
||||
}
|
||||
);
|
||||
|
||||
this.reader.on('line', queueLine);
|
||||
}
|
||||
|
||||
async watch() {
|
||||
this.reader.watch();
|
||||
}
|
||||
|
||||
async unwatch() {
|
||||
this.reader.unwatch();
|
||||
}
|
||||
}
|
21
squad-server/log-parser/rules/index.js
Normal file
21
squad-server/log-parser/rules/index.js
Normal file
@ -0,0 +1,21 @@
|
||||
import NewGame from './new-game.js';
|
||||
import PlayerConnected from './player-connected.js';
|
||||
import PlayerDamaged from './player-damaged.js';
|
||||
import PlayerDied from './player-died.js';
|
||||
import PlayerPossess from './player-possess.js';
|
||||
import PlayerRevived from './player-revived.js';
|
||||
import PlayerWounded from './player-wounded.js';
|
||||
import ServerTickRate from './server-tick-rate.js';
|
||||
import SteamIDConnected from './steamid-connected.js';
|
||||
|
||||
export default [
|
||||
NewGame,
|
||||
PlayerConnected,
|
||||
PlayerDamaged,
|
||||
PlayerDied,
|
||||
PlayerPossess,
|
||||
PlayerRevived,
|
||||
PlayerWounded,
|
||||
ServerTickRate,
|
||||
SteamIDConnected
|
||||
];
|
24
squad-server/log-parser/rules/new-game.js
Normal file
24
squad-server/log-parser/rules/new-game.js
Normal file
@ -0,0 +1,24 @@
|
||||
import SquadLayers from 'connectors/squad-layers';
|
||||
|
||||
import { LOG_PARSER_NEW_GAME } from '../../events/log-parser.js';
|
||||
|
||||
export default {
|
||||
regex: /^\[([0-9.:-]+)]\[([ 0-9]*)]LogWorld: Bringing World \/([A-z]+)\/Maps\/([A-z]+)\/(?:Gameplay_Layers\/)?([A-z0-9_]+)/,
|
||||
onMatch: (args, logParser) => {
|
||||
const layer = SquadLayers.getLayerByLayerClassname(args[5]);
|
||||
|
||||
const data = {
|
||||
raw: args[0],
|
||||
time: args[1],
|
||||
chainID: args[2],
|
||||
dlc: args[3],
|
||||
mapClassname: args[4],
|
||||
layerClassname: args[5],
|
||||
map: layer ? layer.map : null,
|
||||
layer: layer ? layer.layer : null
|
||||
};
|
||||
|
||||
/* Emit new game event */
|
||||
logParser.server.emit(LOG_PARSER_NEW_GAME, data);
|
||||
}
|
||||
};
|
18
squad-server/log-parser/rules/player-connected.js
Normal file
18
squad-server/log-parser/rules/player-connected.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { LOG_PARSER_PLAYER_CONNECTED } from '../../events/log-parser.js';
|
||||
|
||||
export default {
|
||||
regex: /^\[([0-9.:-]+)]\[([ 0-9]*)]LogNet: Join succeeded: (.+)/,
|
||||
onMatch: async (args, logParser) => {
|
||||
logParser.server.suffixStore[logParser.eventStore['steamid-connected']] =
|
||||
args[3];
|
||||
|
||||
const data = {
|
||||
raw: args[0],
|
||||
time: args[1],
|
||||
chainID: args[2],
|
||||
player: await logParser.server.getPlayerByName(args[3], true)
|
||||
};
|
||||
|
||||
logParser.server.emit(LOG_PARSER_PLAYER_CONNECTED, data);
|
||||
}
|
||||
};
|
22
squad-server/log-parser/rules/player-damaged.js
Normal file
22
squad-server/log-parser/rules/player-damaged.js
Normal file
@ -0,0 +1,22 @@
|
||||
import { LOG_PARSER_PLAYER_DAMAGED } from '../../events/log-parser.js';
|
||||
|
||||
export default {
|
||||
regex: /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: Player:(.+) ActualDamage=([0-9.]+) from (.+) caused by ([A-z_0-9]+)_C/,
|
||||
onMatch: async (args, logParser) => {
|
||||
const data = {
|
||||
raw: args[0],
|
||||
time: args[1],
|
||||
chainID: args[2],
|
||||
victim: await logParser.server.getPlayerByName(args[3]),
|
||||
damage: parseFloat(args[4]),
|
||||
attacker: await logParser.server.getPlayerByName(args[5]),
|
||||
weapon: args[6]
|
||||
};
|
||||
|
||||
data.teamkill = data.victim.teamID === data.attacker.teamID;
|
||||
|
||||
logParser.eventStore[args[3]] = data;
|
||||
|
||||
logParser.server.emit(LOG_PARSER_PLAYER_DAMAGED, data);
|
||||
}
|
||||
};
|
24
squad-server/log-parser/rules/player-died.js
Normal file
24
squad-server/log-parser/rules/player-died.js
Normal file
@ -0,0 +1,24 @@
|
||||
import { LOG_PARSER_PLAYER_DIED } from '../../events/log-parser.js';
|
||||
|
||||
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/,
|
||||
onMatch: async (args, logParser) => {
|
||||
const data = {
|
||||
...logParser.eventStore[args[3]],
|
||||
raw: args[0],
|
||||
time: args[1],
|
||||
woundTime: args[1],
|
||||
chainID: args[2],
|
||||
victim: await logParser.server.getPlayerByName(args[3]),
|
||||
damage: parseFloat(args[4]),
|
||||
attackerPlayerController: args[5],
|
||||
weapon: args[6]
|
||||
};
|
||||
|
||||
data.teamkill = data.victim.teamID === data.attacker.teamID;
|
||||
|
||||
logParser.eventStore[args[3]] = data;
|
||||
|
||||
logParser.server.emit(LOG_PARSER_PLAYER_DIED, data);
|
||||
}
|
||||
};
|
22
squad-server/log-parser/rules/player-possess.js
Normal file
22
squad-server/log-parser/rules/player-possess.js
Normal file
@ -0,0 +1,22 @@
|
||||
import {
|
||||
LOG_PARSER_PLAYER_POSSESS,
|
||||
LOG_PARSER_ADMIN_POSSESS_CAMERA
|
||||
} from '../../events/log-parser.js';
|
||||
|
||||
export default {
|
||||
regex: /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquadTrace: \[DedicatedServer](?:ASQPlayerController::)?OnPossess\(\): PC=(.+) Pawn=([A-z0-9_]+)_C/,
|
||||
onMatch: async (args, logParser) => {
|
||||
const data = {
|
||||
raw: args[0],
|
||||
time: args[1],
|
||||
chainID: args[2],
|
||||
player: await logParser.server.getPlayerByName(args[3], true),
|
||||
possessClassname: args[4]
|
||||
};
|
||||
|
||||
logParser.server.emit(LOG_PARSER_PLAYER_POSSESS, data);
|
||||
|
||||
if (data.possessClassname === 'CameraMan')
|
||||
logParser.server.emit(LOG_PARSER_ADMIN_POSSESS_CAMERA, data);
|
||||
}
|
||||
};
|
18
squad-server/log-parser/rules/player-revived.js
Normal file
18
squad-server/log-parser/rules/player-revived.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { LOG_PARSER_PLAYER_REVIVED } from '../../events/log-parser.js';
|
||||
|
||||
export default {
|
||||
// the names are currently the wrong way around in these logs
|
||||
regex: /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: (.+) has revived (.+)\./,
|
||||
onMatch: async (args, logParser) => {
|
||||
const data = {
|
||||
...logParser.eventStore[args[3]],
|
||||
raw: args[0],
|
||||
time: args[1],
|
||||
chainID: args[2],
|
||||
victim: await logParser.server.getPlayerByName(args[5]),
|
||||
reviver: await logParser.server.getPlayerByName(args[3])
|
||||
};
|
||||
|
||||
logParser.server.emit(LOG_PARSER_PLAYER_REVIVED, data);
|
||||
}
|
||||
};
|
25
squad-server/log-parser/rules/player-wounded.js
Normal file
25
squad-server/log-parser/rules/player-wounded.js
Normal file
@ -0,0 +1,25 @@
|
||||
import {
|
||||
LOG_PARSER_PLAYER_WOUNDED,
|
||||
LOG_PARSER_TEAMKILL
|
||||
} from '../../events/log-parser.js';
|
||||
|
||||
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/,
|
||||
onMatch: async (args, logParser) => {
|
||||
const data = {
|
||||
...logParser.eventStore[args[3]],
|
||||
raw: args[0],
|
||||
time: args[1],
|
||||
chainID: args[2],
|
||||
victim: await logParser.server.getPlayerByName(args[3]),
|
||||
damage: parseFloat(args[4]),
|
||||
attackerPlayerController: args[5],
|
||||
weapon: args[6]
|
||||
};
|
||||
|
||||
logParser.eventStore[args[3]] = data;
|
||||
|
||||
logParser.server.emit(LOG_PARSER_PLAYER_WOUNDED, data);
|
||||
if (data.teamkill) logParser.server.emit(LOG_PARSER_TEAMKILL, data);
|
||||
}
|
||||
};
|
15
squad-server/log-parser/rules/server-tick-rate.js
Normal file
15
squad-server/log-parser/rules/server-tick-rate.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { LOG_PARSER_SERVER_TICK_RATE } from '../../events/log-parser.js';
|
||||
|
||||
export default {
|
||||
regex: /^\[([0-9.:-]+)]\[([ 0-9]*)]LogSquad: USQGameState: Server Tick Rate: ([0-9.]+)/,
|
||||
onMatch: (args, logParser) => {
|
||||
const data = {
|
||||
raw: args[0],
|
||||
time: args[1],
|
||||
chainID: args[2],
|
||||
tickRate: parseFloat(args[3])
|
||||
};
|
||||
|
||||
logParser.server.emit(LOG_PARSER_SERVER_TICK_RATE, data);
|
||||
}
|
||||
};
|
6
squad-server/log-parser/rules/steamid-connected.js
Normal file
6
squad-server/log-parser/rules/steamid-connected.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
regex: /^\[([0-9.:-]+)]\[([ 0-9]*)]LogEasyAntiCheatServer: \[[0-9:]+]\[[A-z]+]\[EAC Server] \[Info]\[RegisterClient] Client: ([A-z0-9]+) PlayerGUID: ([0-9]{17}) PlayerIP: [0-9]{17} OwnerGUID: [0-9]{17} PlayerName: (.+)/,
|
||||
onMatch: async (args, logParser) => {
|
||||
logParser.eventStore['steamid-connected'] = args[4];
|
||||
}
|
||||
};
|
76
squad-server/log-parser/test-coverage.js
Normal file
76
squad-server/log-parser/test-coverage.js
Normal file
@ -0,0 +1,76 @@
|
||||
import fs from 'fs';
|
||||
import readLine from 'readline';
|
||||
|
||||
import CLIProgress from 'cli-progress';
|
||||
|
||||
import printLogo from 'core/utils/print-logo';
|
||||
|
||||
import rules from './rules/index.js';
|
||||
|
||||
const TEST_FILE = './squad-server/log-parser/test-data/SquadGame.log';
|
||||
const EXAMPLES = 10;
|
||||
|
||||
async function main() {
|
||||
printLogo();
|
||||
const progressBar = new CLIProgress.SingleBar(
|
||||
{ format: 'Coverage Test | {bar} | {value}/{total} ({percentage}%) Lines' },
|
||||
CLIProgress.Presets.shades_classic
|
||||
);
|
||||
progressBar.start(await getTestFileLength(), 0);
|
||||
|
||||
let total = 0;
|
||||
let matched = 0;
|
||||
const unmatchedLogs = [];
|
||||
|
||||
const testFile = readLine.createInterface({
|
||||
input: fs.createReadStream(TEST_FILE)
|
||||
});
|
||||
|
||||
for await (const line of testFile) {
|
||||
total += 1;
|
||||
|
||||
let matchedLine = false;
|
||||
|
||||
for (const rule of rules) {
|
||||
if (!line.match(rule.regex)) continue;
|
||||
|
||||
matchedLine = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (matchedLine) matched += 1;
|
||||
else if (unmatchedLogs.length <= EXAMPLES) unmatchedLogs.push(line);
|
||||
|
||||
progressBar.update(total);
|
||||
}
|
||||
|
||||
progressBar.stop();
|
||||
|
||||
console.log('Done.');
|
||||
console.log();
|
||||
console.log(
|
||||
`Matched ${matched} / ${total} (${(matched / total) * 100}%) log lines.`
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
function getTestFileLength() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let lineCount = 0;
|
||||
fs.createReadStream(TEST_FILE)
|
||||
.on('data', buffer => {
|
||||
let idx = -1;
|
||||
lineCount--; // Because the loop will run once for idx=-1
|
||||
do {
|
||||
idx = buffer.indexOf(10, idx + 1);
|
||||
lineCount++;
|
||||
} while (idx !== -1);
|
||||
})
|
||||
.on('end', () => {
|
||||
resolve(lineCount);
|
||||
})
|
||||
.on('error', reject);
|
||||
});
|
||||
}
|
23
squad-server/package.json
Normal file
23
squad-server/package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "squad-server",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"async": "^3.2.0",
|
||||
"basic-ftp": "^4.5.4",
|
||||
"cli-progress": "^3.8.2",
|
||||
"core": "1.0.0",
|
||||
"gamedig": "^2.0.20",
|
||||
"moment": "^2.24.0",
|
||||
"tail": "^2.0.3"
|
||||
},
|
||||
"exports": {
|
||||
".": "./index.js",
|
||||
"./events/log-parser": "./events/log-parser.js",
|
||||
"./events/rcon": "./events/rcon.js",
|
||||
"./events/server": "./events/server.js",
|
||||
"./log-parser": "./log-parser/index.js",
|
||||
"./rcon": "./rcon/index.js",
|
||||
"./plugins": "./plugins/index.js"
|
||||
}
|
||||
}
|
308
squad-server/rcon/index.js
Normal file
308
squad-server/rcon/index.js
Normal file
@ -0,0 +1,308 @@
|
||||
import EventEmitter from 'events';
|
||||
import net from 'net';
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
import RCONProtocol from './protocol.js';
|
||||
|
||||
import { RCON_CHAT_MESSAGE, RCON_ERROR } from '../events/rcon.js';
|
||||
|
||||
export default class Rcon {
|
||||
constructor(options = {}, emitter) {
|
||||
if (!options.host) throw new Error('Host must be specified.');
|
||||
this.host = options.host;
|
||||
|
||||
if (!options.rconPort) throw new Error('RCON port must be specified.');
|
||||
this.port = options.rconPort;
|
||||
|
||||
if (!options.rconPassword)
|
||||
throw new Error('RCON password must be specified.');
|
||||
this.password = options.rconPassword;
|
||||
|
||||
this.verboseEnabled = options.rconVerbose || false;
|
||||
|
||||
this.emitter = emitter || new EventEmitter();
|
||||
|
||||
this.reconnectInterval = null;
|
||||
this.rconAutoReconnectInterval = options.rconAutoReconnectInterval || 5000;
|
||||
|
||||
this.maximumPacketSize = 4096;
|
||||
|
||||
this.client = null;
|
||||
this.connected = false;
|
||||
this.autoReconnect = true;
|
||||
|
||||
this.requestQueue = [];
|
||||
this.currentMultiPacket = [];
|
||||
this.ignoreNextEndPacket = false;
|
||||
}
|
||||
|
||||
/* RCON functionality */
|
||||
watch() {
|
||||
this.verbose('Method Exec: watch()');
|
||||
return this.connect();
|
||||
}
|
||||
|
||||
unwatch() {
|
||||
this.verbose('Method Exec: unwatch()');
|
||||
return this.disconnect();
|
||||
}
|
||||
|
||||
execute(command) {
|
||||
this.verbose(`Method Exec: execute(${command})`);
|
||||
return this.write(RCONProtocol.SERVERDATA_EXECCOMMAND, command);
|
||||
}
|
||||
|
||||
async getMapInfo() {
|
||||
const response = await this.execute('ShowNextMap');
|
||||
const match = response.match(
|
||||
/^Current map is ([A-z0-9 ]+), Next map is ([A-z0-9 ]*)/
|
||||
);
|
||||
return {
|
||||
currentLayer: match[1],
|
||||
nextLayer: match[2].length === 0 ? null : match[2]
|
||||
};
|
||||
}
|
||||
|
||||
async listPlayers() {
|
||||
const response = await this.execute('ListPlayers');
|
||||
|
||||
const 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)/
|
||||
);
|
||||
if (!match) continue;
|
||||
|
||||
players.push({
|
||||
playerID: match[1],
|
||||
steamID: match[2],
|
||||
name: match[3],
|
||||
teamID: match[4],
|
||||
squadID: match[5] !== 'N/A' ? match[5] : null
|
||||
});
|
||||
}
|
||||
|
||||
return players;
|
||||
}
|
||||
|
||||
/* Core socket functionality */
|
||||
connect() {
|
||||
this.verbose('Method Exec: connect()');
|
||||
return new Promise((resolve, reject) => {
|
||||
this.autoReconnect = true;
|
||||
|
||||
// setup socket
|
||||
this.client = new net.Socket();
|
||||
|
||||
this.client.on('data', this.onData.bind(this));
|
||||
|
||||
this.client.on('error', err => {
|
||||
this.verbose(`Socket Error: ${err.message}`);
|
||||
this.emitter.emit(RCON_ERROR, err);
|
||||
});
|
||||
|
||||
this.client.on('close', async hadError => {
|
||||
this.verbose(`Socket Closed. AutoReconnect: ${this.autoReconnect}`);
|
||||
this.connected = false;
|
||||
if (!this.autoReconnect) return;
|
||||
if (this.reconnectInterval !== null) return;
|
||||
this.reconnectInterval = setInterval(async () => {
|
||||
this.verbose('Attempting AutoReconnect.');
|
||||
try {
|
||||
await this.connect();
|
||||
clearInterval(this.reconnectInterval);
|
||||
this.reconnectInterval = null;
|
||||
this.verbose('Cleaned AutoReconnect.');
|
||||
} catch (err) {
|
||||
this.verbose('AutoReconnect Failed.');
|
||||
}
|
||||
}, this.rconAutoReconnectInterval);
|
||||
});
|
||||
|
||||
const onConnect = async () => {
|
||||
this.verbose('Socket Opened.');
|
||||
this.client.removeListener('error', onError);
|
||||
this.connected = true;
|
||||
this.verbose('Sending auth packet...');
|
||||
await this.write(RCONProtocol.SERVERDATA_AUTH, this.password);
|
||||
resolve();
|
||||
};
|
||||
|
||||
const onError = err => {
|
||||
this.verbose(`Error Opening Socket: ${err.message}`);
|
||||
this.client.removeListener('connect', onConnect);
|
||||
reject(err);
|
||||
};
|
||||
|
||||
this.client.once('connect', onConnect);
|
||||
this.client.once('error', onError);
|
||||
|
||||
this.client.connect(this.port, this.host);
|
||||
});
|
||||
}
|
||||
|
||||
async disconnect(disableAutoReconnect = true) {
|
||||
this.verbose(`Method Exec: disconnect(${disableAutoReconnect})`);
|
||||
return new Promise((resolve, reject) => {
|
||||
if (disableAutoReconnect) this.autoReconnect = false;
|
||||
|
||||
const onClose = () => {
|
||||
this.verbose('Disconnect successful.');
|
||||
this.client.removeListener('error', onError);
|
||||
resolve();
|
||||
};
|
||||
|
||||
const onError = err => {
|
||||
this.verbose(`Error disconnecting: ${err.message}`);
|
||||
this.client.removeListener('close', onClose);
|
||||
reject(err);
|
||||
};
|
||||
|
||||
this.client.once('close', onClose);
|
||||
this.client.once('error', onError);
|
||||
|
||||
this.client.disconnect();
|
||||
});
|
||||
}
|
||||
|
||||
write(type, body) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.client.writable) {
|
||||
reject(new Error('Unable to write to socket'));
|
||||
return;
|
||||
}
|
||||
if (!this.connected) {
|
||||
reject(new Error('Not connected.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// prepare packets to send
|
||||
const encodedPacket = this.encodePacket(type, RCONProtocol.ID_MID, body);
|
||||
const encodedEmptyPacket = this.encodePacket(
|
||||
RCONProtocol.SERVERDATA_EXECCOMMAND,
|
||||
RCONProtocol.ID_END,
|
||||
''
|
||||
);
|
||||
|
||||
if (
|
||||
this.maximumPacketSize > 0 &&
|
||||
encodedPacket.length > this.maximumPacketSize
|
||||
)
|
||||
reject(new Error('Packet too long.'));
|
||||
|
||||
// prepare to handle response.
|
||||
const handleAuthMultiPacket = async () => {
|
||||
this.client.removeListener('error', reject);
|
||||
|
||||
for (const packet of this.currentMultiPacket) {
|
||||
if (packet.type === RCONProtocol.SERVERDATA_RESPONSE_VALUE) continue;
|
||||
if (packet.id !== RCONProtocol.ID_MID) {
|
||||
this.verbose('Unable to authenticate.');
|
||||
await this.disconnect(false);
|
||||
reject(new Error('Unable to authenticate.'));
|
||||
}
|
||||
|
||||
this.currentMultiPacket = [];
|
||||
|
||||
this.verbose('Authenticated.');
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
const handleMultiPacket = () => {
|
||||
this.client.removeListener('error', reject);
|
||||
|
||||
let response = '';
|
||||
for (const packet of this.currentMultiPacket) {
|
||||
response += packet.body;
|
||||
}
|
||||
|
||||
this.currentMultiPacket = [];
|
||||
|
||||
resolve(response);
|
||||
};
|
||||
|
||||
if (type === RCONProtocol.SERVERDATA_AUTH)
|
||||
this.requestQueue.push(handleAuthMultiPacket);
|
||||
else this.requestQueue.push(handleMultiPacket);
|
||||
|
||||
this.client.on('error', reject);
|
||||
|
||||
// send packets
|
||||
this.client.write(encodedPacket);
|
||||
this.client.write(encodedEmptyPacket);
|
||||
});
|
||||
}
|
||||
|
||||
onData(inputBuf) {
|
||||
let offset = 0;
|
||||
|
||||
while (offset < inputBuf.byteLength) {
|
||||
const endOfPacket = offset + inputBuf.readInt32LE(offset) + 4;
|
||||
const packetBuf = inputBuf.slice(offset, endOfPacket);
|
||||
offset = endOfPacket;
|
||||
|
||||
const decodedPacket = this.decodePacket(packetBuf);
|
||||
|
||||
if (decodedPacket.type === RCONProtocol.SERVERDATA_CHAT_VALUE) {
|
||||
// emit chat messages to own event
|
||||
const message = decodedPacket.body.match(
|
||||
/\[(ChatAll|ChatTeam|ChatSquad|ChatAdmin)] \[SteamID:([0-9]{17})] (.+?) : (.*)/
|
||||
);
|
||||
|
||||
this.emitter.emit(RCON_CHAT_MESSAGE, {
|
||||
raw: decodedPacket.body,
|
||||
chat: message[1],
|
||||
steamID: message[2],
|
||||
player: message[3],
|
||||
message: message[4],
|
||||
time: moment.utc().toDate()
|
||||
});
|
||||
} else if (decodedPacket.id === RCONProtocol.ID_END) {
|
||||
if (this.ignoreNextEndPacket) {
|
||||
this.ignoreNextEndPacket = false;
|
||||
// boost the offset as the length seems wrong for this response
|
||||
offset += 7;
|
||||
continue;
|
||||
}
|
||||
this.ignoreNextEndPacket = true;
|
||||
|
||||
// at end of multipacket resolve request queue
|
||||
const func = this.requestQueue.shift();
|
||||
func();
|
||||
} else {
|
||||
// push packet to multipacket queue
|
||||
this.currentMultiPacket.push(decodedPacket);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
encodePacket(type, id, body, encoding = 'utf8') {
|
||||
const size = Buffer.byteLength(body) + 14;
|
||||
const buffer = Buffer.alloc(size);
|
||||
|
||||
buffer.writeInt32LE(size - 4, 0);
|
||||
buffer.writeInt32LE(id, 4);
|
||||
buffer.writeInt32LE(type, 8);
|
||||
buffer.write(body, 12, size - 2, encoding);
|
||||
buffer.writeInt16LE(0, size - 2);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
decodePacket(buf) {
|
||||
return {
|
||||
size: buf.readInt32LE(0),
|
||||
id: buf.readInt32LE(4),
|
||||
type: buf.readInt32LE(8),
|
||||
body: buf.toString('utf8', 12, buf.byteLength - 2)
|
||||
};
|
||||
}
|
||||
|
||||
verbose(msg) {
|
||||
if (this.verboseEnabled)
|
||||
console.log(`[${Date.now()}] RCON (Verbose): ${msg}`);
|
||||
}
|
||||
}
|
12
squad-server/rcon/protocol.js
Normal file
12
squad-server/rcon/protocol.js
Normal file
@ -0,0 +1,12 @@
|
||||
export default {
|
||||
SERVERDATA_EXECCOMMAND: 0x02,
|
||||
SERVERDATA_RESPONSE_VALUE: 0x00,
|
||||
|
||||
SERVERDATA_AUTH: 0x03,
|
||||
SERVERDATA_AUTH_RESPONSE: 0x02,
|
||||
|
||||
SERVERDATA_CHAT_VALUE: 0x01,
|
||||
|
||||
ID_MID: 0x01,
|
||||
ID_END: 0x02
|
||||
};
|
Loading…
Reference in New Issue
Block a user