mirror of
https://github.com/AsgardEternal/SquadJS.git
synced 2024-09-28 06:04:23 -05:00
Add SquadJS factory and refactor existing code
This commit is contained in:
parent
9f72b3792f
commit
702a717833
@ -1,6 +1,6 @@
|
||||
{
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018
|
||||
"ecmaVersion": 2020
|
||||
},
|
||||
"extends": [
|
||||
"standard",
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,6 +2,7 @@
|
||||
*.tmp
|
||||
|
||||
index-test.js
|
||||
config-test.json
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
"printWidth": 100,
|
||||
"trailingComma": "none"
|
||||
}
|
467
README.md
467
README.md
@ -32,21 +32,464 @@ SquadJS relies on being able to access the Squad server log directory in order t
|
||||
### 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.
|
||||
3. Configure the `config.json` file. See below for more details.
|
||||
4. Start SquadJS: `node index.js`.
|
||||
|
||||
### Configuring SquadJS
|
||||
SquadJS can be configured via a JSON configuration file which, by default, is located in the SquadJS and is named [config.json](https://github.com/Thomas-Smyth/SquadJS/blob/master/config.json).
|
||||
|
||||
#### Server
|
||||
The following section of the configuration contains information about your Squad server.
|
||||
```json
|
||||
"server": {
|
||||
"id": 1,
|
||||
"host": "xxx.xxx.xxx.xxx",
|
||||
"queryPort": 27165,
|
||||
"rconPort": 21114,
|
||||
"rconPassword": "password",
|
||||
"logReaderMode": "tail",
|
||||
"logDir": "C:/path/to/squad/log/folder",
|
||||
"ftpPort": 21,
|
||||
"ftpUser": "FTP Username",
|
||||
"ftpPassword": "FTP Password",
|
||||
"rconVerbose": false
|
||||
},
|
||||
```
|
||||
* `id` - An integer ID to uniquely identify the server.
|
||||
* `host` - The IP of the server.
|
||||
* `queryPort` - The query port of the server.
|
||||
* `rconPort` - The RCON port of the server.
|
||||
* `rconPassword` - The RCON password of the server.
|
||||
* `logReaderMode` - `tail` will read from a local log file. `ftp` will read from a remote log file.
|
||||
* `ftpPort` - The FTP port of the server. Only required for `ftp` `logReaderMode`.
|
||||
* `ftpUser` - The FTP user of the server. Only required for `ftp` `logReaderMode`.
|
||||
* `ftpPassword` - The FTP password of the server. Only required for `ftp` `logReaderMode`.
|
||||
* `rconVerbose` - Enable verbose logging for RCON.
|
||||
|
||||
#### Connectors
|
||||
Connectors allow SquadJS to communicate with external resources.
|
||||
```json
|
||||
"connectors": {
|
||||
"discord": "Discord Login Token",
|
||||
},
|
||||
```
|
||||
Connectors should be named, for example the above is named `discord`, and should have the associated config against it. Configs can be specified by name in plugin options.
|
||||
|
||||
See below for more details on connectors and their associated config.
|
||||
|
||||
##### Discord
|
||||
Connects to Discord via `discord.js`.
|
||||
```json
|
||||
"discord": "Discord Login Token",
|
||||
```
|
||||
Requires a Discord bot login token.
|
||||
|
||||
##### Squad Layer Filter
|
||||
Connects to a filtered list of Squad layers and filters them either by an "initial filter" or an "active filter" that depends on current server information, e.g. player count.
|
||||
```js
|
||||
"layerFilter": {
|
||||
"type": "buildFromFilter",
|
||||
"filter": {
|
||||
"whitelistedLayers": null,
|
||||
"blacklistedLayers": null,
|
||||
"whitelistedMaps": null,
|
||||
"blacklistedMaps": null,
|
||||
"whitelistedGamemodes": null,
|
||||
"blacklistedGamemodes": [
|
||||
"Training"
|
||||
],
|
||||
"flagCountMin": null,
|
||||
"flagCountMax": null,
|
||||
"hasCommander": null,
|
||||
"hasTanks": null,
|
||||
"hasHelicopters": null
|
||||
},
|
||||
"activeLayerFilter": {
|
||||
"historyResetTime": 18000000,
|
||||
"layerHistoryTolerance": 8,
|
||||
"mapHistoryTolerance": 4,
|
||||
"gamemodeHistoryTolerance": {
|
||||
"Invasion": 4
|
||||
},
|
||||
"gamemodeRepetitiveTolerance": {
|
||||
"Invasion": 4
|
||||
},
|
||||
"playerCountComplianceEnabled": true,
|
||||
"factionComplianceEnabled": true,
|
||||
"factionHistoryTolerance": {
|
||||
"RUS": 4
|
||||
},
|
||||
"factionRepetitiveTolerance": {
|
||||
"RUS": 4
|
||||
}
|
||||
}
|
||||
},
|
||||
```
|
||||
* `type` - The type of filter builder to use. `filter` will depend on this type.
|
||||
- `buildFromFilter` - Builds the Squad layers list from a list of filters. An example `filter` with default values for this type is show above.
|
||||
- `whitelistedLayers` - List of layers to consider.
|
||||
- `blacklistLayers` - List of layers to not consider.
|
||||
- `whitelistedMaps` - List of maps to consider.
|
||||
- `blacklistedMaps` - List of maps to not consider.
|
||||
- `whitelistedGamemodes` - List of gamemodes to consider.
|
||||
- `blacklistedGamemodes` - List of gamemodes to not consider.
|
||||
- `flagCountMin` - Minimum number of flags the layer may have.
|
||||
- `flagCountMax` - Maximum number of flags the layer may have.
|
||||
- `hasCommander` - Layer must/most not have a commander. `null` for either.
|
||||
- `hasTanks` - Layer must/most not have a tanks. `null` for either.
|
||||
- `hasHelicopters` - Layer must/most not have a helicopters. `null` for either.
|
||||
- `buildFromFile` - Builds the Squad layers list from a Squad layer config file. `filter` should be the filename of the config file.
|
||||
- `buildFromList` - Builds the Squad layers list from a list of layers. `filter` should be a list of layers, e.g. `"filter": ["Sumari AAS v1", "Fool's Road AAS v1"]`.
|
||||
* `filter` - Described above.
|
||||
* `activeLayerFilter` - Filters layers live as server information updates, e.g. if the player count exceeds a certain amount a layer may no longer be in the filter.
|
||||
- `historyResetTime` - After this number of miliseconds the layer history is no longer considered.
|
||||
- `layerHistoryTolerance` - A layer can only be played again after this number of layers.
|
||||
- `mapHistoryTolerance` - A map can only be played again after this number of layers.
|
||||
- `gamemodeHistoryTolerance` - A gamemode can only be played again after this number of layers. Gamemodes can be specified individually inside the object. If they are not listed then the filter is not applied.
|
||||
- `gamemodeRepetitiveTolerance` - A gamemode can only be played this number of times in a row. Gamemodes can be specified individually inside the object. If they are not listed then the filter is not applied.
|
||||
- `playerCountComplianceEnabled` - Filter layers by player count.
|
||||
- `factionComplianceEnabled` - Filter layers so that a team cannot play the same faction twice in a row.
|
||||
- `factionHistoryTolerance` - A faction can only be played again after this number of layers. Factions can be specified individually inside the object. If they are not listed then the filter is not applied.
|
||||
- `factionRepetitiveTolerance` - A faction can only be played this number of times in a row. Factions can be specified individually inside the object. If they are not listed then the filter is not applied.
|
||||
|
||||
##### MySQL
|
||||
Connects to a MySQL database.
|
||||
```json
|
||||
"mysql": {
|
||||
"connectionLimit": 10,
|
||||
"host": "host",
|
||||
"port": 3306,
|
||||
"user": "squadjs",
|
||||
"password": "password",
|
||||
"database": "squadjs"
|
||||
}
|
||||
```
|
||||
The config is a set of pool connection options as listed in the [Node.js mysql](https://www.npmjs.com/package/mysql) documentation.
|
||||
|
||||
#### Plugins
|
||||
The `plugins` section in your config file lists all plugins built into SquadJS, e.g.:
|
||||
```json
|
||||
"plugins": [
|
||||
{
|
||||
"plugin": "auto-tk-warn",
|
||||
"disabled": false,
|
||||
"message": "Please apologise for ALL TKs in ALL chat!"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
The `disabled` field can be toggled between `true`/ `false` to enabled/disable the plugin.
|
||||
|
||||
Plugin options are also specified. A full list of plugin options can be seen below.
|
||||
|
||||
## Plugins
|
||||
* [Discord Admin Broadcast](https://github.com/Thomas-Smyth/SquadJS/tree/master/plugins/discord-admin-broadcast) - Log admin broadcasts to Discord.
|
||||
* [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 Chat Admin Request](https://github.com/Thomas-Smyth/SquadJS/tree/master/plugins/discord-chat-admin-request) - Log `!admin` alerts to Discord.
|
||||
* [Discord Teamkill](https://github.com/Thomas-Smyth/SquadJS/tree/master/plugins/discord-teamkill) - Log teamkills to Discord.
|
||||
* [Discord Server Status](https://github.com/Thomas-Smyth/SquadJS/tree/master/plugins/discord-server-status) - Add a server status embed 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.
|
||||
The following is a list of plugins built into SquadJS:
|
||||
|
||||
### auto-tk-warn
|
||||
Automatically warn players who teamkill.
|
||||
|
||||
#### Options
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Option</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>message</td><td>String</td><td>false</td><td>Please apologise for ALL TKs in ALL chat!</td><td>The message to warn players with.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### discord-admin-broadcast
|
||||
Log admin broadcasts to Discord.
|
||||
|
||||
#### Options
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Option</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>discordClient</td><td>DiscordConnector</td><td>true</td><td>discord</td><td>The name of the Discord Connector to use.</td></tr>
|
||||
<tr><td>channelID</td><td>Discord Channel ID</td><td>true</td><td>Discord Channel ID</td><td>The ID of the channel to log admin broadcasts to.</td></tr>
|
||||
<tr><td>color</td><td>Discord Color Code</td><td>false</td><td>16761867</td><td>The color of the embed.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### discord-admin-cam-logs
|
||||
Log admin cam usage to Discord.
|
||||
|
||||
#### Options
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Option</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>discordClient</td><td>DiscordConnector</td><td>true</td><td>discord</td><td>The name of the Discord Connector to use.</td></tr>
|
||||
<tr><td>channelID</td><td>Discord Channel ID</td><td>true</td><td>Discord Channel ID</td><td>The ID of the channel to log admin cam usage to.</td></tr>
|
||||
<tr><td>color</td><td>Discord Color Code</td><td>false</td><td>16761867</td><td>The color of the embed.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### discord-chat
|
||||
Log in game chat to Discord.
|
||||
|
||||
#### Options
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Option</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>discordClient</td><td>DiscordConnector</td><td>true</td><td>discord</td><td>The name of the Discord Connector to use.</td></tr>
|
||||
<tr><td>channelID</td><td>Discord Channel ID</td><td>true</td><td>Discord Channel ID</td><td>The ID of the channel to log admin broadcasts to.</td></tr>
|
||||
<tr><td>ignoreChats</td><td>Array</td><td>false</td><td>ChatSquad</td><td>A list of chat names to ignore.</td></tr>
|
||||
<tr><td>chatColors</td><td>Object</td><td>false</td><td>[object Object]</td><td>The color of the embed for each chat. Example: `{ ChatAll: 16761867 }`.</td></tr>
|
||||
<tr><td>color</td><td>Discord Color Code</td><td>false</td><td>16761867</td><td>The color of the embed.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### discord-admin-request
|
||||
Ping admins in Discord with the in game !admin command.
|
||||
|
||||
#### Options
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Option</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>discordClient</td><td>DiscordConnector</td><td>true</td><td>discord</td><td>The name of the Discord Connector to use.</td></tr>
|
||||
<tr><td>channelID</td><td>Discord Channel ID</td><td>true</td><td>Discord Channel ID</td><td>The ID of the channel to log admin broadcasts to.</td></tr>
|
||||
<tr><td>ignoreChats</td><td>Array</td><td>false</td><td>ChatSquad</td><td>A list of chat names to ignore.</td></tr>
|
||||
<tr><td>ignorePhrases</td><td>Array</td><td>false</td><td></td><td>A list of phrases to ignore.</td></tr>
|
||||
<tr><td>adminPrefix</td><td>String</td><td>false</td><td>!admin</td><td>The command that calls an admin.</td></tr>
|
||||
<tr><td>pingGroups</td><td>Array</td><td>false</td><td></td><td>A list of Discord role IDs to ping.</td></tr>
|
||||
<tr><td>pingDelay</td><td>Number</td><td>false</td><td>60000</td><td>Cooldown for pings.</td></tr>
|
||||
<tr><td>color</td><td>Discord Color Code</td><td>false</td><td>16761867</td><td>The color of the embed.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### discord-debug
|
||||
Dump SquadJS events to Discord.
|
||||
|
||||
#### Options
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Option</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>discordClient</td><td>DiscordConnector</td><td>true</td><td>discord</td><td>The name of the Discord Connector to use.</td></tr>
|
||||
<tr><td>channelID</td><td>Discord Channel ID</td><td>true</td><td>Discord Channel ID</td><td>The ID of the channel to log admin broadcasts to.</td></tr>
|
||||
<tr><td>events</td><td>Array</td><td>true</td><td></td><td>A list of events to dump.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### discord-rcon
|
||||
This plugin turns a Discord channel into a RCON console.
|
||||
|
||||
#### Options
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Option</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>discordClient</td><td>DiscordConnector</td><td>true</td><td>discord</td><td>The name of the Discord Connector to use.</td></tr>
|
||||
<tr><td>channelID</td><td>Discord Channel ID</td><td>true</td><td>Discord Channel ID</td><td>The ID of the channel you wish to turn into a RCON console.</td></tr>
|
||||
<tr><td>prependAdminNameInBroadcast</td><td>Boolean</td><td>false</td><td>false</td><td>Prepend the admin's name when he makes an announcement.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### discord-server-status
|
||||
This plugin displays server status embeds in Discord.
|
||||
|
||||
#### Options
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Option</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>discordClient</td><td>DiscordConnector</td><td>true</td><td>discord</td><td>The name of the Discord Connector to use.</td></tr>
|
||||
<tr><td>color</td><td>Discord Color Code</td><td>false</td><td>16761867</td><td>The color code of the Discord embed.</td></tr>
|
||||
<tr><td>colorGradient</td><td>Boolean</td><td>false</td><td>true</td><td>Apply gradient color to Discord embed depending on the player count.</td></tr>
|
||||
<tr><td>connectLink</td><td>Boolean</td><td>false</td><td>true</td><td>Display a Steam server connection link.</td></tr>
|
||||
<tr><td>command</td><td>String</td><td>false</td><td>!server</td><td>The command that displays the embed.</td></tr>
|
||||
<tr><td>disableStatus</td><td>Boolean</td><td>false</td><td>false</td><td>Disable the bot status.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### discord-teamkill
|
||||
Log teamkills to Discord.
|
||||
|
||||
#### Options
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Option</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>discordClient</td><td>DiscordConnector</td><td>true</td><td>discord</td><td>The name of the Discord Connector to use.</td></tr>
|
||||
<tr><td>channelID</td><td>Discord Channel ID</td><td>true</td><td>Discord Channel ID</td><td>The ID of the channel to log admin broadcasts to.</td></tr>
|
||||
<tr><td>teamkillColor</td><td>Discord Color Code</td><td>false</td><td>16761867</td><td>The color of the embed for teamkills.</td></tr>
|
||||
<tr><td>suicideColor</td><td>Discord Color Code</td><td>false</td><td>16761867</td><td>The color of the embed for suicides.</td></tr>
|
||||
<tr><td>ignoreSuicides</td><td>Boolean</td><td>false</td><td>false</td><td>Ignore suicides.</td></tr>
|
||||
<tr><td>disableSCBL</td><td>Boolean</td><td>false</td><td>false</td><td>Disable Squad Community Ban List information.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### mapvote-123
|
||||
A map voting system that uses numbers to allow players to vote on layers.
|
||||
|
||||
#### Options
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Option</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>minVoteCount</td><td>Integer</td><td>false</td><td>null</td><td>The minimum number of votes required for the vote to succeed.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### mapvote-did-you-mean
|
||||
A map voting system that uses a "Did you mean?" algorithm to allow players to vote on layers.
|
||||
|
||||
#### Options
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Option</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>layerFilter</td><td>SquadLayerFilterConnector</td><td>false</td><td>layerFilter</td><td>The layers players can choose from.</td></tr>
|
||||
<tr><td>alwaysOn</td><td>Boolean</td><td>false</td><td>true</td><td>If true then the map voting system will always be live.</td></tr>
|
||||
<tr><td>minPlayerCount</td><td>Integer</td><td>false</td><td>null</td><td>The minimum number of players required for the vote to succeed.</td></tr>
|
||||
<tr><td>minVoteCount</td><td>Integer</td><td>false</td><td>null</td><td>The minimum number of votes required for the vote to succeed.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### mysql-log
|
||||
Log server information and statistics to a MySQL DB.
|
||||
|
||||
#### Options
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Option</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>mysqlPool</td><td>MySQLPoolConnector</td><td>true</td><td>mysql</td><td>The name of the MySQL Pool Connector to use.</td></tr>
|
||||
<tr><td>overrideServerID</td><td>Int</td><td>false</td><td>null</td><td>A overridden server ID.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### seeding-message
|
||||
Display seeding messages in admin broadcasts.
|
||||
|
||||
#### Options
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Option</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>mode</td><td>`interval` or `onjoin`</td><td>false</td><td>interval</td><td>Display seeding messages at a set interval or after players join.</td></tr>
|
||||
<tr><td>interval</td><td>Number</td><td>false</td><td>150000</td><td>How frequently to display the seeding messages in seconds.</td></tr>
|
||||
<tr><td>delay</td><td>Number</td><td>false</td><td>45000</td><td>How long to wait after a player joins to display the announcement in seconds.</td></tr>
|
||||
<tr><td>seedingThreshold</td><td>Number</td><td>false</td><td>50</td><td>Number of players before the server is considered live.</td></tr>
|
||||
<tr><td>seedingMessage</td><td>String</td><td>false</td><td>Seeding Rules Active! Fight only over the middle flags! No FOB Hunting!</td><td>The seeding message to display.</td></tr>
|
||||
<tr><td>liveEnabled</td><td>String</td><td>false</td><td>true</td><td>Display a "Live" message when a certain player count is met.</td></tr>
|
||||
<tr><td>liveThreshold</td><td>Number</td><td>false</td><td>2</td><td>When above the seeding threshold, but within this number "Live" messages are displayed.</td></tr>
|
||||
<tr><td>liveMessage</td><td>String</td><td>false</td><td>Live</td><td>The "Live" message to display.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### team-randomizer
|
||||
Randomize teams with an admin command.
|
||||
|
||||
#### Options
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Option</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>command</td><td>String</td><td>false</td><td>!randomize</td><td>The command used to randomize the teams.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Creating Your Own Plugins
|
||||
To create your own plugin you need a basic knowledge of JavaScript.
|
||||
|
179
config.json
Normal file
179
config.json
Normal file
@ -0,0 +1,179 @@
|
||||
{
|
||||
"server": {
|
||||
"id": 1,
|
||||
"host": "xxx.xxx.xxx.xxx",
|
||||
"queryPort": 27165,
|
||||
"rconPort": 21114,
|
||||
"rconPassword": "password",
|
||||
"logReaderMode": "tail",
|
||||
"logDir": "C:/path/to/squad/log/folder",
|
||||
"ftpPort": 21,
|
||||
"ftpUser": "FTP Username",
|
||||
"ftpPassword": "FTP Password",
|
||||
"rconVerbose": false
|
||||
},
|
||||
"connectors": {
|
||||
"discord": "Discord Login Token",
|
||||
"layerFilter": {
|
||||
"type": "buildFromFilter",
|
||||
"filter": {
|
||||
"whitelistedLayers": null,
|
||||
"blacklistedLayers": null,
|
||||
"whitelistedMaps": null,
|
||||
"blacklistedMaps": null,
|
||||
"whitelistedGamemodes": null,
|
||||
"blacklistedGamemodes": [
|
||||
"Training"
|
||||
],
|
||||
"flagCountMin": null,
|
||||
"flagCountMax": null,
|
||||
"hasCommander": null,
|
||||
"hasTanks": null,
|
||||
"hasHelicopters": null
|
||||
},
|
||||
"activeLayerFilter": {
|
||||
"historyResetTime": 18000000,
|
||||
"layerHistoryTolerance": 8,
|
||||
"mapHistoryTolerance": 4,
|
||||
"gamemodeHistoryTolerance": {
|
||||
"Invasion": 4
|
||||
},
|
||||
"gamemodeRepetitiveTolerance": {
|
||||
"Invasion": 4
|
||||
},
|
||||
"playerCountComplianceEnabled": true,
|
||||
"factionComplianceEnabled": true,
|
||||
"factionHistoryTolerance": {
|
||||
"RUS": 4
|
||||
},
|
||||
"factionRepetitiveTolerance": {
|
||||
"RUS": 4
|
||||
}
|
||||
}
|
||||
},
|
||||
"mysql": {
|
||||
"connectionLimit": 10,
|
||||
"host": "host",
|
||||
"port": 3306,
|
||||
"user": "squadjs",
|
||||
"password": "password",
|
||||
"database": "squadjs"
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"plugin": "auto-tk-warn",
|
||||
"disabled": false,
|
||||
"message": "Please apologise for ALL TKs in ALL chat!"
|
||||
},
|
||||
{
|
||||
"plugin": "discord-admin-broadcast",
|
||||
"disabled": false,
|
||||
"discordClient": "discord",
|
||||
"channelID": "Discord Channel ID",
|
||||
"color": 16761867
|
||||
},
|
||||
{
|
||||
"plugin": "discord-admin-cam-logs",
|
||||
"disabled": false,
|
||||
"discordClient": "discord",
|
||||
"channelID": "Discord Channel ID",
|
||||
"color": 16761867
|
||||
},
|
||||
{
|
||||
"plugin": "discord-chat",
|
||||
"disabled": false,
|
||||
"discordClient": "discord",
|
||||
"channelID": "Discord Channel ID",
|
||||
"ignoreChats": [
|
||||
"ChatSquad"
|
||||
],
|
||||
"chatColors": {},
|
||||
"color": 16761867
|
||||
},
|
||||
{
|
||||
"plugin": "discord-admin-request",
|
||||
"disabled": false,
|
||||
"discordClient": "discord",
|
||||
"channelID": "Discord Channel ID",
|
||||
"ignoreChats": [
|
||||
"ChatSquad"
|
||||
],
|
||||
"ignorePhrases": [],
|
||||
"adminPrefix": "!admin",
|
||||
"pingGroups": [],
|
||||
"pingDelay": 60000,
|
||||
"color": 16761867
|
||||
},
|
||||
{
|
||||
"plugin": "discord-debug",
|
||||
"disabled": true,
|
||||
"discordClient": "discord",
|
||||
"channelID": "Discord Channel ID",
|
||||
"events": []
|
||||
},
|
||||
{
|
||||
"plugin": "discord-rcon",
|
||||
"disabled": false,
|
||||
"discordClient": "discord",
|
||||
"channelID": "Discord Channel ID",
|
||||
"prependAdminNameInBroadcast": false
|
||||
},
|
||||
{
|
||||
"plugin": "discord-server-status",
|
||||
"disabled": false,
|
||||
"discordClient": "discord",
|
||||
"color": 16761867,
|
||||
"colorGradient": true,
|
||||
"connectLink": true,
|
||||
"command": "!server",
|
||||
"disableStatus": false
|
||||
},
|
||||
{
|
||||
"plugin": "discord-teamkill",
|
||||
"disabled": false,
|
||||
"discordClient": "discord",
|
||||
"channelID": "Discord Channel ID",
|
||||
"teamkillColor": 16761867,
|
||||
"suicideColor": 16761867,
|
||||
"ignoreSuicides": false,
|
||||
"disableSCBL": false
|
||||
},
|
||||
{
|
||||
"plugin": "mapvote-123",
|
||||
"disabled": true,
|
||||
"minVoteCount": null
|
||||
},
|
||||
{
|
||||
"plugin": "mapvote-did-you-mean",
|
||||
"disabled": true,
|
||||
"layerFilter": "layerFilter",
|
||||
"alwaysOn": true,
|
||||
"minPlayerCount": null,
|
||||
"minVoteCount": null
|
||||
},
|
||||
{
|
||||
"plugin": "mysql-log",
|
||||
"disabled": true,
|
||||
"mysqlPool": "mysql",
|
||||
"overrideServerID": null
|
||||
},
|
||||
{
|
||||
"plugin": "seeding-message",
|
||||
"disabled": false,
|
||||
"mode": "interval",
|
||||
"interval": 150000,
|
||||
"delay": 45000,
|
||||
"seedingThreshold": 50,
|
||||
"seedingMessage": "Seeding Rules Active! Fight only over the middle flags! No FOB Hunting!",
|
||||
"liveEnabled": true,
|
||||
"liveThreshold": 2,
|
||||
"liveMessage": "Live"
|
||||
},
|
||||
{
|
||||
"plugin": "team-randomizer",
|
||||
"disabled": false,
|
||||
"command": "!randomize"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
{
|
||||
"name": "connectors",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./scbl": "./scbl.js",
|
||||
"./squad-layer-filter": "./squad-layer-filter.js",
|
||||
"./squad-layers": "./squad-layers.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"didyoumean": "^1.2.1",
|
||||
"graphql-request": "^1.8.2"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 135 KiB |
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
@ -1,4 +0,0 @@
|
||||
/* 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 };
|
14
core/constants.js
Normal file
14
core/constants.js
Normal file
@ -0,0 +1,14 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const SQUADJS_VERSION = JSON.parse(
|
||||
fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf8')
|
||||
).version;
|
||||
|
||||
/* As set out by the terms of the license, the following should not be modified. */
|
||||
const COPYRIGHT_MESSAGE = 'SquadJS, Copyright © 2020 Thomas Smyth';
|
||||
|
||||
export { SQUADJS_VERSION, COPYRIGHT_MESSAGE };
|
@ -3,8 +3,16 @@
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./config": "./config.js",
|
||||
"./squad-layers": "./squad-layers/index.js",
|
||||
|
||||
"./utils/print-logo": "./utils/print-logo.js",
|
||||
"./utils/sleep": "./utils/sleep.js"
|
||||
"./utils/scbl": "./utils/scbl.js",
|
||||
"./utils/sleep": "./utils/sleep.js",
|
||||
|
||||
"./constants": "./constants.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"didyoumean": "^1.2.1",
|
||||
"graphql-request": "^1.8.2"
|
||||
}
|
||||
}
|
||||
|
4
core/squad-layers/index.js
Normal file
4
core/squad-layers/index.js
Normal file
@ -0,0 +1,4 @@
|
||||
import SquadLayers from './squad-layers.js';
|
||||
import SquadLayerFilter from './squad-layer-filter.js';
|
||||
|
||||
export { SquadLayers, SquadLayerFilter };
|
@ -48,8 +48,8 @@ export default class SquadLayerFilter extends SquadLayersClass {
|
||||
return new SquadLayerFilter(layers, activeLayerFilter);
|
||||
}
|
||||
|
||||
static buildFromFile(filename, activeLayerFilter, delimiter = '\n') {
|
||||
const lines = fs.readFileSync('./connectors/data/layers.json', 'utf8').split(delimiter);
|
||||
static buildFromFile(path, activeLayerFilter, delimiter = '\n') {
|
||||
const lines = fs.readFileSync(path, 'utf8').split(delimiter);
|
||||
const layers = [];
|
||||
|
||||
const validLayerNames = SquadLayers.getLayerNames();
|
@ -1,13 +1,17 @@
|
||||
import fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import path from 'path';
|
||||
|
||||
import didYouMean from 'didyoumean';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
class SquadLayers {
|
||||
constructor(layers) {
|
||||
if (Array.isArray(layers)) {
|
||||
this.layers = layers;
|
||||
} else {
|
||||
this.layers = JSON.parse(fs.readFileSync('./connectors/data/layers.json', 'utf8'));
|
||||
this.layers = JSON.parse(fs.readFileSync(path.resolve(__dirname, './layers.json'), 'utf8'));
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.layers.length; i++) {
|
||||
@ -23,28 +27,28 @@ class SquadLayers {
|
||||
}
|
||||
|
||||
getLayerNames() {
|
||||
return this.layers.map(layer => layer.layer);
|
||||
return this.layers.map((layer) => layer.layer);
|
||||
}
|
||||
|
||||
getLayerByLayerName(layerName) {
|
||||
const layer = this.layers.filter(layer => layer.layer === 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);
|
||||
const layer = this.layers.filter((layer) => layer.layerClassname === layerClassname);
|
||||
return layer.length === 1 ? layer[0] : null;
|
||||
}
|
||||
|
||||
getLayerByDidYouMean(layerName) {
|
||||
layerName = didYouMean(layerName, this.getLayerNames());
|
||||
|
||||
const layer = this.layers.filter(layer => layer.layer === layerName);
|
||||
const layer = this.layers.filter((layer) => layer.layer === layerName);
|
||||
return layer.length === 1 ? layer[0] : null;
|
||||
}
|
||||
|
||||
getLayerByNumber(number) {
|
||||
const layer = this.layers.filter(layer => layer.layerNumber === number);
|
||||
const layer = this.layers.filter((layer) => layer.layerNumber === number);
|
||||
return layer.length === 1 ? layer[0] : null;
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { COPYRIGHT_MESSAGE } from '../config.js';
|
||||
import { SQUADJS_VERSION, COPYRIGHT_MESSAGE } from '../constants.js';
|
||||
|
||||
const LOGO = `
|
||||
_____ ____ _ _ _____ _
|
||||
@ -11,8 +11,9 @@ const LOGO = `
|
||||
|__/
|
||||
${COPYRIGHT_MESSAGE}
|
||||
GitHub: https://github.com/Thomas-Smyth/SquadJS
|
||||
Version: ${SQUADJS_VERSION}
|
||||
`;
|
||||
|
||||
export default function() {
|
||||
export default function () {
|
||||
console.log(LOGO);
|
||||
}
|
||||
|
@ -2,6 +2,6 @@ import { request } from 'graphql-request';
|
||||
|
||||
const API_ENDPOINT = 'https://squad-community-ban-list.com/graphql';
|
||||
|
||||
export default function(query, variables) {
|
||||
export default function (query, variables) {
|
||||
return request(API_ENDPOINT, query, variables);
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
export default function(time) {
|
||||
return new Promise(resolve => {
|
||||
export default function (time) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, time);
|
||||
});
|
||||
}
|
||||
|
27
factory/build-config.js
Normal file
27
factory/build-config.js
Normal file
@ -0,0 +1,27 @@
|
||||
import fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import path from 'path';
|
||||
import plugins from 'plugins';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const template = JSON.parse(
|
||||
fs.readFileSync(path.resolve(__dirname, './templates/config-template.json'), 'utf8')
|
||||
);
|
||||
|
||||
const pluginKeys = Object.keys(plugins).sort((a, b) =>
|
||||
a.name < b.name ? -1 : a.name > b.name ? 1 : 0
|
||||
);
|
||||
|
||||
for (const pluginKey of pluginKeys) {
|
||||
const plugin = plugins[pluginKey];
|
||||
|
||||
const pluginConfig = { plugin: plugin.name, disabled: plugin.defaultDisabled };
|
||||
for (const option in plugin.optionsSpec) {
|
||||
pluginConfig[option] = plugin.optionsSpec[option].default;
|
||||
}
|
||||
|
||||
template.plugins.push(pluginConfig);
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.resolve(__dirname, '../config.json'), JSON.stringify(template, null, 2));
|
46
factory/build-connectors.js
Normal file
46
factory/build-connectors.js
Normal file
@ -0,0 +1,46 @@
|
||||
import Discord from 'discord.js';
|
||||
import mysql from 'mysql';
|
||||
import { SquadLayerFilter } from 'core/squad-layers';
|
||||
|
||||
import plugins from 'plugins';
|
||||
|
||||
const connectorTypes = {
|
||||
DiscordConnector: async function (config) {
|
||||
const client = new Discord.Client();
|
||||
await client.login(config);
|
||||
return client;
|
||||
},
|
||||
MySQLPoolConnector: async function (config) {
|
||||
return mysql.createPool(config);
|
||||
},
|
||||
SquadLayerFilterConnector: async function (config) {
|
||||
return SquadLayerFilter[config.type](config.filter, config.activeLayerFilter);
|
||||
}
|
||||
};
|
||||
|
||||
export default async function (config) {
|
||||
const connectors = {};
|
||||
|
||||
for (const pluginConfig of config.plugins) {
|
||||
if (pluginConfig.disabled) continue;
|
||||
|
||||
const plugin = plugins[pluginConfig.plugin];
|
||||
|
||||
for (const optionName of Object.keys(plugin.optionsSpec)) {
|
||||
const option = plugin.optionsSpec[optionName];
|
||||
|
||||
if (!Object.keys(connectorTypes).includes(option.type)) continue;
|
||||
if (!connectorTypes[option.type]) throw new Error('Connector type not supported!');
|
||||
|
||||
if (connectors[pluginConfig[optionName]]) continue;
|
||||
if (!config.connectors[pluginConfig[optionName]])
|
||||
throw new Error(`${pluginConfig[optionName]} connector config not present!`);
|
||||
|
||||
connectors[pluginConfig[optionName]] = await connectorTypes[option.type](
|
||||
config.connectors[pluginConfig[optionName]]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return connectors;
|
||||
}
|
54
factory/build-readme.js
Normal file
54
factory/build-readme.js
Normal file
@ -0,0 +1,54 @@
|
||||
import fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import path from 'path';
|
||||
import plugins from 'plugins';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const pluginNames = Object.keys(plugins);
|
||||
const sortedPluginNames = pluginNames.sort((a, b) =>
|
||||
a.name < b.name ? -1 : a.name > b.name ? 1 : 0
|
||||
);
|
||||
|
||||
const pluginInfo = [];
|
||||
for (const pluginName of sortedPluginNames) {
|
||||
const plugin = plugins[pluginName];
|
||||
|
||||
const optionTable = [];
|
||||
for (const optionName of Object.keys(plugin.optionsSpec)) {
|
||||
const option = plugin.optionsSpec[optionName];
|
||||
optionTable.push(
|
||||
`<tr><td>${optionName}</td><td>${option.type}</td><td>${option.required}</td><td>${option.default}</td><td>${option.description}</td></tr>`
|
||||
);
|
||||
}
|
||||
|
||||
pluginInfo.push(
|
||||
`### ${plugin.name}
|
||||
${plugin.description}
|
||||
|
||||
#### Options
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Option</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${optionTable.join('\n')}
|
||||
</tbody>
|
||||
</table>`
|
||||
);
|
||||
}
|
||||
|
||||
const pluginInfoText = pluginInfo.join('\n\n');
|
||||
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, '../README.md'),
|
||||
fs
|
||||
.readFileSync(path.resolve(__dirname, './templates/readme-template.md'), 'utf8')
|
||||
.replace(/\/\/PLUGIN-INFO\/\//, pluginInfoText)
|
||||
);
|
5
factory/build-squad-server.js
Normal file
5
factory/build-squad-server.js
Normal file
@ -0,0 +1,5 @@
|
||||
import SquadServer from 'squad-server';
|
||||
|
||||
export default function (config) {
|
||||
return new SquadServer(config.server);
|
||||
}
|
48
factory/index.js
Normal file
48
factory/index.js
Normal file
@ -0,0 +1,48 @@
|
||||
import readConfig from './read-config.js';
|
||||
import buildSquadServer from './build-squad-server.js';
|
||||
import buildConnectors from './build-connectors.js';
|
||||
|
||||
import plugins from 'plugins';
|
||||
|
||||
export default async function (configPath) {
|
||||
console.log('SquadJS factory commencing building...');
|
||||
|
||||
console.log('Getting config file...');
|
||||
const config = readConfig(configPath);
|
||||
|
||||
console.log('Building Squad server...');
|
||||
const server = buildSquadServer(config);
|
||||
|
||||
console.log('Initialising connectors...');
|
||||
const connectors = await buildConnectors(config);
|
||||
|
||||
console.log('Loading plugins...');
|
||||
for (const pluginConfig of config.plugins) {
|
||||
if (pluginConfig.disabled) continue;
|
||||
|
||||
console.log(`Loading plugin ${pluginConfig.plugin}...`);
|
||||
const plugin = plugins[pluginConfig.plugin];
|
||||
|
||||
const options = {};
|
||||
for (const optionName of Object.keys(plugin.optionsSpec)) {
|
||||
const option = plugin.optionsSpec[optionName];
|
||||
|
||||
if (option.type.match(/Connector$/)) {
|
||||
options[optionName] = connectors[pluginConfig[optionName]];
|
||||
} else {
|
||||
if (option.required) {
|
||||
if (!(optionName in pluginConfig))
|
||||
throw new Error(`${plugin.name}: ${optionName} is required but missing.`);
|
||||
if (option.default === pluginConfig[optionName])
|
||||
throw new Error(`${plugin.name}: ${optionName} is required but is the default value.`);
|
||||
}
|
||||
|
||||
options[optionName] = pluginConfig[optionName] || option.default;
|
||||
}
|
||||
}
|
||||
|
||||
await plugin.init(server, options);
|
||||
}
|
||||
|
||||
return server;
|
||||
}
|
13
factory/package.json
Normal file
13
factory/package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "factory",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"core": "1.0.0",
|
||||
"discord.js": "^12.2.0",
|
||||
"mysql": "^2.18.1"
|
||||
},
|
||||
"exports": {
|
||||
".": "./index.js"
|
||||
}
|
||||
}
|
20
factory/read-config.js
Normal file
20
factory/read-config.js
Normal file
@ -0,0 +1,20 @@
|
||||
import fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import path from 'path';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export default function (configPath = './config.json') {
|
||||
const fullConfigPath = path.resolve(__dirname, '../', configPath);
|
||||
if (!fs.existsSync(fullConfigPath)) throw new Error('Config file does not exist.');
|
||||
const unparsedConfig = fs.readFileSync(fullConfigPath, 'utf8');
|
||||
|
||||
let parsedConfig;
|
||||
try {
|
||||
parsedConfig = JSON.parse(unparsedConfig);
|
||||
} catch (err) {
|
||||
throw new Error('Unable to parse config file.');
|
||||
}
|
||||
|
||||
return parsedConfig;
|
||||
}
|
66
factory/templates/config-template.json
Normal file
66
factory/templates/config-template.json
Normal file
@ -0,0 +1,66 @@
|
||||
{
|
||||
"server": {
|
||||
"id": 1,
|
||||
|
||||
"host": "xxx.xxx.xxx.xxx",
|
||||
"queryPort": 27165,
|
||||
"rconPort": 21114,
|
||||
"rconPassword": "password",
|
||||
|
||||
"logReaderMode": "tail",
|
||||
"logDir": "C:/path/to/squad/log/folder",
|
||||
|
||||
"ftpPort": 21,
|
||||
"ftpUser": "FTP Username",
|
||||
"ftpPassword": "FTP Password",
|
||||
|
||||
"rconVerbose": false
|
||||
},
|
||||
"connectors": {
|
||||
"discord": "Discord Login Token",
|
||||
"layerFilter": {
|
||||
"type": "buildFromFilter",
|
||||
"filter": {
|
||||
"whitelistedLayers": null,
|
||||
"blacklistedLayers": null,
|
||||
"whitelistedMaps": null,
|
||||
"blacklistedMaps": null,
|
||||
"whitelistedGamemodes": null,
|
||||
"blacklistedGamemodes": ["Training"],
|
||||
"flagCountMin": null,
|
||||
"flagCountMax": null,
|
||||
"hasCommander": null,
|
||||
"hasTanks": null,
|
||||
"hasHelicopters": null
|
||||
},
|
||||
"activeLayerFilter": {
|
||||
"historyResetTime": 18000000,
|
||||
"layerHistoryTolerance": 8,
|
||||
"mapHistoryTolerance": 4,
|
||||
"gamemodeHistoryTolerance": {
|
||||
"Invasion": 4
|
||||
},
|
||||
"gamemodeRepetitiveTolerance": {
|
||||
"Invasion": 4
|
||||
},
|
||||
"playerCountComplianceEnabled": true,
|
||||
"factionComplianceEnabled": true,
|
||||
"factionHistoryTolerance": {
|
||||
"RUS": 4
|
||||
},
|
||||
"factionRepetitiveTolerance": {
|
||||
"RUS": 4
|
||||
}
|
||||
}
|
||||
},
|
||||
"mysql": {
|
||||
"connectionLimit": 10,
|
||||
"host": "host",
|
||||
"port": 3306,
|
||||
"user": "squadjs",
|
||||
"password": "password",
|
||||
"database": "squadjs"
|
||||
}
|
||||
},
|
||||
"plugins": []
|
||||
}
|
284
factory/templates/readme-template.md
Normal file
284
factory/templates/readme-template.md
Normal file
@ -0,0 +1,284 @@
|
||||
<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/9F2Ng5C)
|
||||
|
||||
<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 `config.json` file. See below for more details.
|
||||
4. Start SquadJS: `node index.js`.
|
||||
|
||||
### Configuring SquadJS
|
||||
SquadJS can be configured via a JSON configuration file which, by default, is located in the SquadJS and is named [config.json](https://github.com/Thomas-Smyth/SquadJS/blob/master/config.json).
|
||||
|
||||
#### Server
|
||||
The following section of the configuration contains information about your Squad server.
|
||||
```json
|
||||
"server": {
|
||||
"id": 1,
|
||||
"host": "xxx.xxx.xxx.xxx",
|
||||
"queryPort": 27165,
|
||||
"rconPort": 21114,
|
||||
"rconPassword": "password",
|
||||
"logReaderMode": "tail",
|
||||
"logDir": "C:/path/to/squad/log/folder",
|
||||
"ftpPort": 21,
|
||||
"ftpUser": "FTP Username",
|
||||
"ftpPassword": "FTP Password",
|
||||
"rconVerbose": false
|
||||
},
|
||||
```
|
||||
* `id` - An integer ID to uniquely identify the server.
|
||||
* `host` - The IP of the server.
|
||||
* `queryPort` - The query port of the server.
|
||||
* `rconPort` - The RCON port of the server.
|
||||
* `rconPassword` - The RCON password of the server.
|
||||
* `logReaderMode` - `tail` will read from a local log file. `ftp` will read from a remote log file.
|
||||
* `ftpPort` - The FTP port of the server. Only required for `ftp` `logReaderMode`.
|
||||
* `ftpUser` - The FTP user of the server. Only required for `ftp` `logReaderMode`.
|
||||
* `ftpPassword` - The FTP password of the server. Only required for `ftp` `logReaderMode`.
|
||||
* `rconVerbose` - Enable verbose logging for RCON.
|
||||
|
||||
#### Connectors
|
||||
Connectors allow SquadJS to communicate with external resources.
|
||||
```json
|
||||
"connectors": {
|
||||
"discord": "Discord Login Token",
|
||||
},
|
||||
```
|
||||
Connectors should be named, for example the above is named `discord`, and should have the associated config against it. Configs can be specified by name in plugin options.
|
||||
|
||||
See below for more details on connectors and their associated config.
|
||||
|
||||
##### Discord
|
||||
Connects to Discord via `discord.js`.
|
||||
```json
|
||||
"discord": "Discord Login Token",
|
||||
```
|
||||
Requires a Discord bot login token.
|
||||
|
||||
##### Squad Layer Filter
|
||||
Connects to a filtered list of Squad layers and filters them either by an "initial filter" or an "active filter" that depends on current server information, e.g. player count.
|
||||
```js
|
||||
"layerFilter": {
|
||||
"type": "buildFromFilter",
|
||||
"filter": {
|
||||
"whitelistedLayers": null,
|
||||
"blacklistedLayers": null,
|
||||
"whitelistedMaps": null,
|
||||
"blacklistedMaps": null,
|
||||
"whitelistedGamemodes": null,
|
||||
"blacklistedGamemodes": [
|
||||
"Training"
|
||||
],
|
||||
"flagCountMin": null,
|
||||
"flagCountMax": null,
|
||||
"hasCommander": null,
|
||||
"hasTanks": null,
|
||||
"hasHelicopters": null
|
||||
},
|
||||
"activeLayerFilter": {
|
||||
"historyResetTime": 18000000,
|
||||
"layerHistoryTolerance": 8,
|
||||
"mapHistoryTolerance": 4,
|
||||
"gamemodeHistoryTolerance": {
|
||||
"Invasion": 4
|
||||
},
|
||||
"gamemodeRepetitiveTolerance": {
|
||||
"Invasion": 4
|
||||
},
|
||||
"playerCountComplianceEnabled": true,
|
||||
"factionComplianceEnabled": true,
|
||||
"factionHistoryTolerance": {
|
||||
"RUS": 4
|
||||
},
|
||||
"factionRepetitiveTolerance": {
|
||||
"RUS": 4
|
||||
}
|
||||
}
|
||||
},
|
||||
```
|
||||
* `type` - The type of filter builder to use. `filter` will depend on this type.
|
||||
- `buildFromFilter` - Builds the Squad layers list from a list of filters. An example `filter` with default values for this type is show above.
|
||||
- `whitelistedLayers` - List of layers to consider.
|
||||
- `blacklistLayers` - List of layers to not consider.
|
||||
- `whitelistedMaps` - List of maps to consider.
|
||||
- `blacklistedMaps` - List of maps to not consider.
|
||||
- `whitelistedGamemodes` - List of gamemodes to consider.
|
||||
- `blacklistedGamemodes` - List of gamemodes to not consider.
|
||||
- `flagCountMin` - Minimum number of flags the layer may have.
|
||||
- `flagCountMax` - Maximum number of flags the layer may have.
|
||||
- `hasCommander` - Layer must/most not have a commander. `null` for either.
|
||||
- `hasTanks` - Layer must/most not have a tanks. `null` for either.
|
||||
- `hasHelicopters` - Layer must/most not have a helicopters. `null` for either.
|
||||
- `buildFromFile` - Builds the Squad layers list from a Squad layer config file. `filter` should be the filename of the config file.
|
||||
- `buildFromList` - Builds the Squad layers list from a list of layers. `filter` should be a list of layers, e.g. `"filter": ["Sumari AAS v1", "Fool's Road AAS v1"]`.
|
||||
* `filter` - Described above.
|
||||
* `activeLayerFilter` - Filters layers live as server information updates, e.g. if the player count exceeds a certain amount a layer may no longer be in the filter.
|
||||
- `historyResetTime` - After this number of miliseconds the layer history is no longer considered.
|
||||
- `layerHistoryTolerance` - A layer can only be played again after this number of layers.
|
||||
- `mapHistoryTolerance` - A map can only be played again after this number of layers.
|
||||
- `gamemodeHistoryTolerance` - A gamemode can only be played again after this number of layers. Gamemodes can be specified individually inside the object. If they are not listed then the filter is not applied.
|
||||
- `gamemodeRepetitiveTolerance` - A gamemode can only be played this number of times in a row. Gamemodes can be specified individually inside the object. If they are not listed then the filter is not applied.
|
||||
- `playerCountComplianceEnabled` - Filter layers by player count.
|
||||
- `factionComplianceEnabled` - Filter layers so that a team cannot play the same faction twice in a row.
|
||||
- `factionHistoryTolerance` - A faction can only be played again after this number of layers. Factions can be specified individually inside the object. If they are not listed then the filter is not applied.
|
||||
- `factionRepetitiveTolerance` - A faction can only be played this number of times in a row. Factions can be specified individually inside the object. If they are not listed then the filter is not applied.
|
||||
|
||||
##### MySQL
|
||||
Connects to a MySQL database.
|
||||
```json
|
||||
"mysql": {
|
||||
"connectionLimit": 10,
|
||||
"host": "host",
|
||||
"port": 3306,
|
||||
"user": "squadjs",
|
||||
"password": "password",
|
||||
"database": "squadjs"
|
||||
}
|
||||
```
|
||||
The config is a set of pool connection options as listed in the [Node.js mysql](https://www.npmjs.com/package/mysql) documentation.
|
||||
|
||||
#### Plugins
|
||||
The `plugins` section in your config file lists all plugins built into SquadJS, e.g.:
|
||||
```json
|
||||
"plugins": [
|
||||
{
|
||||
"plugin": "auto-tk-warn",
|
||||
"disabled": false,
|
||||
"message": "Please apologise for ALL TKs in ALL chat!"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
The `disabled` field can be toggled between `true`/ `false` to enabled/disable the plugin.
|
||||
|
||||
Plugin options are also specified. A full list of plugin options can be seen below.
|
||||
|
||||
## Plugins
|
||||
The following is a list of plugins built into SquadJS:
|
||||
|
||||
//PLUGIN-INFO//
|
||||
|
||||
## 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 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.
|
||||
```
|
92
index.js
92
index.js
@ -1,87 +1,7 @@
|
||||
import Discord from 'discord.js';
|
||||
import mysql from 'mysql';
|
||||
import Influx from 'influx';
|
||||
import printLogo from 'core/utils/print-logo';
|
||||
import buildSquadJS from 'factory';
|
||||
|
||||
import Server from 'squad-server';
|
||||
import SquadLayerFilter from 'connectors/squad-layer-filter';
|
||||
|
||||
import {
|
||||
discordAdminBroadcast,
|
||||
discordAdminCamLogs,
|
||||
discordChat,
|
||||
discordChatAdminRequest,
|
||||
discordRCON,
|
||||
discordServerStatus,
|
||||
discordTeamkill,
|
||||
influxdbLog,
|
||||
influxdbLogDefaultSchema,
|
||||
mapvote,
|
||||
mysqlLog,
|
||||
teamRandomizer,
|
||||
seedingMessage
|
||||
} from 'plugins';
|
||||
|
||||
async function main() {
|
||||
const server = new Server({
|
||||
id: 1,
|
||||
|
||||
host: 'xxx.xxx.xxx.xxx',
|
||||
queryPort: 27165,
|
||||
rconPort: 21114,
|
||||
rconPassword: 'password',
|
||||
|
||||
// Uncomment the following lines to read logs over FTP.
|
||||
// ftpPort: 21,
|
||||
// ftpUser: 'FTP Username',
|
||||
// ftpPassword: 'FTP Password',
|
||||
// logReaderMode: 'ftp',
|
||||
|
||||
logDir: 'C:/path/to/squad/log/folder'
|
||||
});
|
||||
|
||||
// Discord Plugins
|
||||
const discordClient = new Discord.Client();
|
||||
await discordClient.login('Discord Login Token');
|
||||
await discordAdminBroadcast(server, discordClient, 'Discord Channel ID');
|
||||
await discordAdminCamLogs(server, discordClient, 'Discord Channel ID');
|
||||
await discordChat(server, discordClient, 'Discord Channel ID');
|
||||
await discordChatAdminRequest(server, discordClient, 'Discord Channel ID', {
|
||||
pingGroups: ['discordGroupID']
|
||||
});
|
||||
await discordRCON(server, discordClient, 'Discord Channel ID');
|
||||
await discordServerStatus(server, discordClient);
|
||||
await discordTeamkill(server, discordClient, 'Discord Channel ID');
|
||||
|
||||
// in game features
|
||||
const squadLayerFilter = SquadLayerFilter.buildFromFilter({});
|
||||
mapvote(server, 'didyoumean', squadLayerFilter, {});
|
||||
|
||||
teamRandomizer(server);
|
||||
seedingMessage(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();
|
||||
printLogo();
|
||||
buildSquadJS('./config-test.json')
|
||||
.then((server) => server.watch())
|
||||
.catch(console.log);
|
||||
|
32
package.json
32
package.json
@ -1,40 +1,38 @@
|
||||
{
|
||||
"name": "SquadJS",
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.0-beta1",
|
||||
"repository": "https://github.com/Thomas-Smyth/SquadJS.git",
|
||||
"author": "Thomas Smyth <https://github.com/Thomas-Smyth>",
|
||||
"license": "BSL-1.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"assets",
|
||||
"connectors",
|
||||
"core",
|
||||
"factory",
|
||||
"plugins",
|
||||
"squad-server"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "eslint --fix . && prettier --write \"./**/*.js\"",
|
||||
"test-log-parser-coverage": "node squad-server/log-parser/test-coverage.js"
|
||||
"build-config": "node factory/build-config.js",
|
||||
"build-readme": "node factory/build-readme.js",
|
||||
"build-all": "node factory/build-config.js && node factory/build-readme.js"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"connectors": "1.0.0",
|
||||
"discord.js": "^12.2.0",
|
||||
"influx": "^5.5.1",
|
||||
"mysql": "^2.18.1",
|
||||
"plugins": "1.0.0",
|
||||
"squad-server": "1.0.0"
|
||||
"core": "1.0.0",
|
||||
"factory": "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": "^7.6.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-config-standard": "^14.1.1",
|
||||
"eslint-plugin-import": "^2.22.0",
|
||||
"eslint-plugin-node": "^11.1.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"
|
||||
"husky": "^4.2.5",
|
||||
"lint-staged": "^10.2.11",
|
||||
"prettier": "^2.0.5"
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +0,0 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="../../assets/squadjs-logo.png" alt="Logo" width="500"/>
|
||||
|
||||
#### SquadJS - Auto Teamkill Warning
|
||||
</div>
|
||||
|
||||
## About
|
||||
Automatically sends a warning to players who teamkill to remind them to apologise in all chat.
|
||||
|
||||
## Installation
|
||||
```js
|
||||
// Place the following line at the top of your index.js file.
|
||||
import { autoTKWarn } from 'plugins';
|
||||
|
||||
// Place the following lines after all of the above.
|
||||
await autoTKWarn(
|
||||
server,
|
||||
{ // options - the options included below display the defaults and can be removed for simplicity.
|
||||
message: 'Please apologise for ALL TKs in ALL chat!' // warning to send
|
||||
}
|
||||
);
|
||||
```
|
@ -1,15 +1,24 @@
|
||||
import { LOG_PARSER_TEAMKILL } from 'squad-server/events/log-parser';
|
||||
|
||||
export default async function(server, options = {}) {
|
||||
if (!server)
|
||||
throw new Error('DiscordAdminCamLogs must be provided with a reference to the server.');
|
||||
export default {
|
||||
name: 'auto-tk-warn',
|
||||
description: 'Automatically warn players who teamkill.',
|
||||
defaultDisabled: false,
|
||||
|
||||
server.on(LOG_PARSER_TEAMKILL, info => {
|
||||
// ignore suicides
|
||||
if (info.attacker.steamID === info.victim.steamID) return;
|
||||
server.rcon.execute(
|
||||
`AdminWarn "${info.attacker.steamID}" ${options.message ||
|
||||
'Please apologise for ALL TKs in ALL chat!'}`
|
||||
);
|
||||
});
|
||||
}
|
||||
optionsSpec: {
|
||||
message: {
|
||||
type: 'String',
|
||||
required: false,
|
||||
default: 'Please apologise for ALL TKs in ALL chat!',
|
||||
description: 'The message to warn players with.'
|
||||
}
|
||||
},
|
||||
|
||||
init: async (server, connectors, options) => {
|
||||
server.on(LOG_PARSER_TEAMKILL, (info) => {
|
||||
// ignore suicides
|
||||
if (info.attacker.steamID === info.victim.steamID) return;
|
||||
server.rcon.execute(`AdminWarn "${info.attacker.steamID}" ${options.message}`);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -1,30 +0,0 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="../../assets/squadjs-logo.png" alt="Logo" width="500"/>
|
||||
|
||||
#### SquadJS - Discord Admin Broadcast Plugin
|
||||
</div>
|
||||
|
||||
## About
|
||||
The Discord Admin Broadcast plugin streams admin broadcasts logs to Discord.
|
||||
|
||||
## Installation
|
||||
```js
|
||||
// Place the following two lines at the top of your index.js file.
|
||||
import Discord from 'discord.js';
|
||||
import { discordAdminBroadcast } 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 discordAdminBroadcast(
|
||||
server,
|
||||
discordClient,
|
||||
'discordChannelID',
|
||||
{ // options - the options included below display the defaults and can be removed for simplicity.
|
||||
color: 16761867 // color of embed
|
||||
}
|
||||
);
|
||||
```
|
@ -1,36 +1,52 @@
|
||||
import { COPYRIGHT_MESSAGE } from 'core/config';
|
||||
import { COPYRIGHT_MESSAGE } from 'core/constants';
|
||||
import { LOG_PARSER_ADMIN_BROADCAST } from 'squad-server/events/log-parser';
|
||||
|
||||
export default async function(server, discordClient, channelID, options = {}) {
|
||||
if (!server) throw new Error('DiscordChat must be provided with a reference to the server.');
|
||||
export default {
|
||||
name: 'discord-admin-broadcast',
|
||||
description: 'Log admin broadcasts to Discord.',
|
||||
defaultDisabled: false,
|
||||
|
||||
if (!discordClient) throw new Error('DiscordChat must be provided with a Discord.js client.');
|
||||
optionsSpec: {
|
||||
discordClient: {
|
||||
type: 'DiscordConnector',
|
||||
required: true,
|
||||
default: 'discord',
|
||||
description: 'The name of the Discord Connector to use.'
|
||||
},
|
||||
channelID: {
|
||||
type: 'Discord Channel ID',
|
||||
required: true,
|
||||
default: 'Discord Channel ID',
|
||||
description: 'The ID of the channel to log admin broadcasts to.'
|
||||
},
|
||||
color: {
|
||||
type: 'Discord Color Code',
|
||||
required: false,
|
||||
default: 16761867,
|
||||
description: 'The color of the embed.'
|
||||
}
|
||||
},
|
||||
|
||||
if (!channelID) throw new Error('DiscordChat must be provided with a channel ID.');
|
||||
init: async (server, options) => {
|
||||
const channel = await options.discordClient.channels.fetch(options.channelID);
|
||||
|
||||
options = {
|
||||
color: 16761867,
|
||||
...options
|
||||
};
|
||||
|
||||
const channel = await discordClient.channels.fetch(channelID);
|
||||
|
||||
server.on(LOG_PARSER_ADMIN_BROADCAST, async info => {
|
||||
channel.send({
|
||||
embed: {
|
||||
title: 'Admin Broadcast',
|
||||
color: options.color,
|
||||
fields: [
|
||||
{
|
||||
name: 'Message',
|
||||
value: `${info.message}`
|
||||
server.on(LOG_PARSER_ADMIN_BROADCAST, async (info) => {
|
||||
channel.send({
|
||||
embed: {
|
||||
title: 'Admin Broadcast',
|
||||
color: options.color,
|
||||
fields: [
|
||||
{
|
||||
name: 'Message',
|
||||
value: `${info.message}`
|
||||
}
|
||||
],
|
||||
timestamp: info.time.toISOString(),
|
||||
footer: {
|
||||
text: COPYRIGHT_MESSAGE
|
||||
}
|
||||
],
|
||||
timestamp: info.time.toISOString(),
|
||||
footer: {
|
||||
text: COPYRIGHT_MESSAGE
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,30 +0,0 @@
|
||||
<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
|
||||
}
|
||||
);
|
||||
```
|
@ -1,88 +1,102 @@
|
||||
import { COPYRIGHT_MESSAGE } from 'core/config';
|
||||
import { COPYRIGHT_MESSAGE } from 'core/constants';
|
||||
import {
|
||||
LOG_PARSER_PLAYER_POSSESS,
|
||||
LOG_PARSER_PLAYER_UNPOSSESS
|
||||
} from 'squad-server/events/log-parser';
|
||||
|
||||
export default async function(server, discordClient, channelID, options = {}) {
|
||||
if (!server)
|
||||
throw new Error('DiscordAdminCamLogs must be provided with a reference to the server.');
|
||||
export default {
|
||||
name: 'discord-admin-cam-logs',
|
||||
description: 'Log admin cam usage to Discord.',
|
||||
defaultDisabled: false,
|
||||
|
||||
if (!discordClient)
|
||||
throw new Error('DiscordAdminCamLogs must be provided with a Discord.js client.');
|
||||
optionsSpec: {
|
||||
discordClient: {
|
||||
type: 'DiscordConnector',
|
||||
required: true,
|
||||
default: 'discord',
|
||||
description: 'The name of the Discord Connector to use.'
|
||||
},
|
||||
channelID: {
|
||||
type: 'Discord Channel ID',
|
||||
required: true,
|
||||
default: 'Discord Channel ID',
|
||||
description: 'The ID of the channel to log admin cam usage to.'
|
||||
},
|
||||
color: {
|
||||
type: 'Discord Color Code',
|
||||
required: false,
|
||||
default: 16761867,
|
||||
description: 'The color of the embed.'
|
||||
}
|
||||
},
|
||||
|
||||
if (!channelID) throw new Error('DiscordAdminCamLogs must be provided with a channel ID.');
|
||||
init: async (server, options) => {
|
||||
const channel = await options.discordClient.channels.fetch(options.channelID);
|
||||
|
||||
options = {
|
||||
color: 16761867,
|
||||
...options
|
||||
};
|
||||
const adminsInCam = {};
|
||||
|
||||
const channel = await discordClient.channels.fetch(channelID);
|
||||
server.on(LOG_PARSER_PLAYER_POSSESS, (info) => {
|
||||
if (info.player === null || info.possessClassname !== 'CameraMan') return;
|
||||
|
||||
const adminsInCam = {};
|
||||
adminsInCam[info.player.steamID] = info.time;
|
||||
|
||||
server.on(LOG_PARSER_PLAYER_POSSESS, info => {
|
||||
if (info.player === null || info.possessClassname !== 'CameraMan') return;
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
],
|
||||
timestamp: info.time.toISOString(),
|
||||
footer: {
|
||||
text: COPYRIGHT_MESSAGE
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
server.on(LOG_PARSER_PLAYER_UNPOSSESS, info => {
|
||||
if (info.switchPossess === true || !(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];
|
||||
});
|
||||
}
|
||||
server.on(LOG_PARSER_PLAYER_UNPOSSESS, (info) => {
|
||||
if (info.switchPossess === true || !(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];
|
||||
});
|
||||
}
|
||||
};
|
||||
|
131
plugins/discord-admin-request/index.js
Normal file
131
plugins/discord-admin-request/index.js
Normal file
@ -0,0 +1,131 @@
|
||||
import { COPYRIGHT_MESSAGE } from 'core/constants';
|
||||
import { RCON_CHAT_MESSAGE } from 'squad-server/events/rcon';
|
||||
|
||||
export default {
|
||||
name: 'discord-admin-request',
|
||||
description: 'Ping admins in Discord with the in game !admin command.',
|
||||
defaultDisabled: false,
|
||||
|
||||
optionsSpec: {
|
||||
discordClient: {
|
||||
type: 'DiscordConnector',
|
||||
required: true,
|
||||
default: 'discord',
|
||||
description: 'The name of the Discord Connector to use.'
|
||||
},
|
||||
channelID: {
|
||||
type: 'Discord Channel ID',
|
||||
required: true,
|
||||
default: 'Discord Channel ID',
|
||||
description: 'The ID of the channel to log admin broadcasts to.'
|
||||
},
|
||||
ignoreChats: {
|
||||
type: 'Array',
|
||||
required: false,
|
||||
default: ['ChatSquad'],
|
||||
description: 'A list of chat names to ignore.'
|
||||
},
|
||||
ignorePhrases: {
|
||||
type: 'Array',
|
||||
required: false,
|
||||
default: [],
|
||||
description: 'A list of phrases to ignore.'
|
||||
},
|
||||
adminPrefix: {
|
||||
type: 'String',
|
||||
required: false,
|
||||
default: '!admin',
|
||||
description: 'The command that calls an admin.'
|
||||
},
|
||||
pingGroups: {
|
||||
type: 'Array',
|
||||
required: false,
|
||||
default: [],
|
||||
description: 'A list of Discord role IDs to ping.'
|
||||
},
|
||||
pingDelay: {
|
||||
type: 'Number',
|
||||
required: false,
|
||||
default: 60 * 1000,
|
||||
description: 'Cooldown for pings.'
|
||||
},
|
||||
color: {
|
||||
type: 'Discord Color Code',
|
||||
required: false,
|
||||
default: 16761867,
|
||||
description: 'The color of the embed.'
|
||||
}
|
||||
},
|
||||
|
||||
init: async (server, options) => {
|
||||
let lastPing = null;
|
||||
|
||||
const channel = await options.discordClient.channels.fetch(options.channelID);
|
||||
|
||||
server.on(RCON_CHAT_MESSAGE, async (info) => {
|
||||
if (options.ignoreChats.includes(info.chat)) return;
|
||||
if (!info.message.startsWith(`${options.adminPrefix}`)) return;
|
||||
|
||||
for (const ignorePhrase of options.ignorePhrases) {
|
||||
if (info.message.includes(ignorePhrase)) return;
|
||||
}
|
||||
|
||||
const playerInfo = await server.getPlayerBySteamID(info.steamID);
|
||||
const trimmedMessage = info.message.replace(options.adminPrefix, '').trim();
|
||||
|
||||
if (trimmedMessage.length === 0) {
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
`Please specify what you would like help with when requesting an admin.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const message = {
|
||||
embed: {
|
||||
title: `${playerInfo.name} has requested admin support!`,
|
||||
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: trimmedMessage
|
||||
}
|
||||
],
|
||||
timestamp: info.time.toISOString(),
|
||||
footer: {
|
||||
text: COPYRIGHT_MESSAGE
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
options.pingGroups.length > 0 &&
|
||||
(lastPing === null || Date.now() - options.pingDelay > lastPing)
|
||||
) {
|
||||
message.content = options.pingGroups.map((groupID) => `<@&${groupID}>`).join(' ');
|
||||
lastPing = Date.now();
|
||||
}
|
||||
|
||||
channel.send(message);
|
||||
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
`An admin has been notified, please wait for us to get back to you.`
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
@ -1,38 +0,0 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="../../assets/squadjs-logo.png" alt="Logo" width="500"/>
|
||||
|
||||
#### SquadJS - Discord Chat Admin Request Plugin
|
||||
|
||||
</div>
|
||||
|
||||
## About
|
||||
|
||||
The Discord Chat Admin Request plugin allows players to ping for an admin in discord. 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 { discordChatAdminRequest } 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 discordChatAdminRequest(
|
||||
server,
|
||||
discordClient,
|
||||
'discordChannelID',
|
||||
{ // options - the options included below display the defaults and can be removed for simplicity.
|
||||
adminPrefix: '!admin', // prefix for an admin request.
|
||||
pingGroups: ['729853701308678154'], // Groups to ping on a request, leave empty for no ping.
|
||||
pingDelay: 60 * 1000, // number of ms between pings. other messages will still be logged just without pings.
|
||||
ignoreChats: ['ChatSquad', 'ChatAdmin'], // an array of chats to not display.
|
||||
ignorePhrases: [], // add any phrases you do not want pings for, e.g. "switch me".
|
||||
color: '#f44336' // color of embed
|
||||
}
|
||||
);
|
||||
```
|
@ -1,90 +0,0 @@
|
||||
import { COPYRIGHT_MESSAGE } from 'core/config';
|
||||
import { RCON_CHAT_MESSAGE } from 'squad-server/events/rcon';
|
||||
|
||||
export default async function(server, discordClient, channelID, options = {}) {
|
||||
if (!server)
|
||||
throw new Error('DiscordChatAdminRequest must be provided with a reference to the server.');
|
||||
if (!discordClient)
|
||||
throw new Error('DiscordChatAdminRequest must be provided with a Discord.js client.');
|
||||
if (!channelID) throw new Error('DiscordChatAdminRequest must be provided with a channel ID.');
|
||||
|
||||
options = {
|
||||
color: 16761867,
|
||||
ignoreChats: [],
|
||||
ignorePhrases: [],
|
||||
adminPrefix: '!admin',
|
||||
pingGroups: [],
|
||||
pingDelay: 60 * 1000,
|
||||
...options
|
||||
};
|
||||
|
||||
let lastPing = null;
|
||||
|
||||
const channel = await discordClient.channels.fetch(channelID);
|
||||
|
||||
server.on(RCON_CHAT_MESSAGE, async info => {
|
||||
if (options.ignoreChats.includes(info.chat)) return;
|
||||
if (!info.message.startsWith(`${options.adminPrefix}`)) return;
|
||||
|
||||
for (const ignorePhrase of options.ignorePhrases) {
|
||||
if (info.message.includes(ignorePhrase)) return;
|
||||
}
|
||||
|
||||
const playerInfo = await server.getPlayerBySteamID(info.steamID);
|
||||
const trimmedMessage = info.message.replace(options.adminPrefix, '').trim();
|
||||
|
||||
if (trimmedMessage.length === 0) {
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
`Please specify what you would like help with when requesting an admin.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const message = {
|
||||
embed: {
|
||||
title: `${playerInfo.name} has requested admin support!`,
|
||||
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: trimmedMessage
|
||||
}
|
||||
],
|
||||
timestamp: info.time.toISOString(),
|
||||
footer: {
|
||||
text: COPYRIGHT_MESSAGE
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
options.pingGroups.length > 0 &&
|
||||
(lastPing === null || Date.now() - options.pingDelay > lastPing)
|
||||
) {
|
||||
message.content = options.pingGroups.map(groupID => `<@&${groupID}>`).join(' ');
|
||||
lastPing = Date.now();
|
||||
}
|
||||
|
||||
channel.send(message);
|
||||
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
`An admin has been notified, please wait for us to get back to you.`
|
||||
);
|
||||
});
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
<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
|
||||
chatColors: { 'ChatAll': 16761867 } // change the color of chat types individually. Defaults to color above if not specified.
|
||||
}
|
||||
);
|
||||
```
|
@ -1,59 +1,82 @@
|
||||
import { COPYRIGHT_MESSAGE } from 'core/config';
|
||||
import { COPYRIGHT_MESSAGE } from 'core/constants';
|
||||
import { RCON_CHAT_MESSAGE } from 'squad-server/events/rcon';
|
||||
|
||||
export default async function(server, discordClient, channelID, options = {}) {
|
||||
if (!server) throw new Error('DiscordChat must be provided with a reference to the server.');
|
||||
export default {
|
||||
name: 'discord-chat',
|
||||
description: 'Log in game chat to Discord.',
|
||||
defaultDisabled: false,
|
||||
|
||||
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 = {
|
||||
chatColors: {
|
||||
...options.chatColors
|
||||
optionsSpec: {
|
||||
discordClient: {
|
||||
type: 'DiscordConnector',
|
||||
required: true,
|
||||
default: 'discord',
|
||||
description: 'The name of the Discord Connector to use.'
|
||||
},
|
||||
color: 16761867,
|
||||
...options
|
||||
};
|
||||
channelID: {
|
||||
type: 'Discord Channel ID',
|
||||
required: true,
|
||||
default: 'Discord Channel ID',
|
||||
description: 'The ID of the channel to log admin broadcasts to.'
|
||||
},
|
||||
ignoreChats: {
|
||||
type: 'Array',
|
||||
required: false,
|
||||
default: ['ChatSquad'],
|
||||
description: 'A list of chat names to ignore.'
|
||||
},
|
||||
chatColors: {
|
||||
type: 'Object',
|
||||
required: false,
|
||||
default: {},
|
||||
description: 'The color of the embed for each chat. Example: `{ ChatAll: 16761867 }`.'
|
||||
},
|
||||
color: {
|
||||
type: 'Discord Color Code',
|
||||
required: false,
|
||||
default: 16761867,
|
||||
description: 'The color of the embed.'
|
||||
}
|
||||
},
|
||||
|
||||
const channel = await discordClient.channels.fetch(channelID);
|
||||
init: async (server, options) => {
|
||||
const channel = await options.discordClient.channels.fetch(options.channelID);
|
||||
|
||||
server.on(RCON_CHAT_MESSAGE, async info => {
|
||||
if (ignoreChats.includes(info.chat)) return;
|
||||
server.on(RCON_CHAT_MESSAGE, async (info) => {
|
||||
if (options.ignoreChats.includes(info.chat)) return;
|
||||
|
||||
const playerInfo = await server.getPlayerBySteamID(info.steamID);
|
||||
const playerInfo = await server.getPlayerBySteamID(info.steamID);
|
||||
|
||||
channel.send({
|
||||
embed: {
|
||||
title: info.chat,
|
||||
color: options.chatColors[info.chat] || 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}`
|
||||
channel.send({
|
||||
embed: {
|
||||
title: info.chat,
|
||||
color: options.chatColors[info.chat] || 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
|
||||
}
|
||||
],
|
||||
timestamp: info.time.toISOString(),
|
||||
footer: {
|
||||
text: COPYRIGHT_MESSAGE
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,34 +0,0 @@
|
||||
<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.
|
||||
);
|
||||
```
|
@ -1,15 +1,36 @@
|
||||
export default async function(server, discordClient, channelID, events = []) {
|
||||
if (!server) throw new Error('DiscordDebug must be provided with a reference to the server.');
|
||||
export default {
|
||||
name: 'discord-debug',
|
||||
description: 'Dump SquadJS events to Discord.',
|
||||
defaultDisabled: true,
|
||||
|
||||
if (!discordClient) throw new Error('DiscordDebug must be provided with a Discord.js client.');
|
||||
optionsSpec: {
|
||||
discordClient: {
|
||||
type: 'DiscordConnector',
|
||||
required: true,
|
||||
default: 'discord',
|
||||
description: 'The name of the Discord Connector to use.'
|
||||
},
|
||||
channelID: {
|
||||
type: 'Discord Channel ID',
|
||||
required: true,
|
||||
default: 'Discord Channel ID',
|
||||
description: 'The ID of the channel to log admin broadcasts to.'
|
||||
},
|
||||
events: {
|
||||
type: 'Array',
|
||||
required: true,
|
||||
default: [],
|
||||
description: 'A list of events to dump.'
|
||||
}
|
||||
},
|
||||
|
||||
if (!channelID) throw new Error('DicordDebug must be provided with a channel ID.');
|
||||
init: async (server, options) => {
|
||||
const channel = await options.discordClient.channels.fetch(options.channelID);
|
||||
|
||||
const channel = await discordClient.channels.fetch(channelID);
|
||||
|
||||
for (const event of events) {
|
||||
server.on(event, info => {
|
||||
channel.send(`\`\`\`${JSON.stringify(info, null, 2)}\`\`\``);
|
||||
});
|
||||
for (const event of options.events) {
|
||||
server.on(event, (info) => {
|
||||
channel.send(`\`\`\`${JSON.stringify(info, null, 2)}\`\`\``);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,30 +0,0 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="../../assets/squadjs-logo.png" alt="Logo" width="500"/>
|
||||
|
||||
#### SquadJS - Discord RCON
|
||||
</div>
|
||||
|
||||
## About
|
||||
The Discord RCON plugin allows you to run RCON commands through a Discord channel.
|
||||
|
||||
## Installation
|
||||
```js
|
||||
// Place the following two lines at the top of your index.js file.
|
||||
import Discord from 'discord.js';
|
||||
import { discordRCON } 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 discordRCON(
|
||||
server,
|
||||
discordClient,
|
||||
'discordChannelID',
|
||||
{ // options - the options included below display the defaults and can be removed for simplicity.
|
||||
prependAdminNameInBroadcast: false // prepend admin names to broadcasts
|
||||
}
|
||||
);
|
||||
```
|
@ -1,37 +1,58 @@
|
||||
export default async function(server, discordClient, channelID, options = {}) {
|
||||
if (!server) throw new Error('DiscordRCON must be provided with a reference to the server.');
|
||||
if (!discordClient) throw new Error('DiscordRCON must be provided with a Discord.js client.');
|
||||
if (!channelID) throw new Error('DiscordRCON must be provided with a channel ID.');
|
||||
export default {
|
||||
name: 'discord-rcon',
|
||||
description: 'This plugin turns a Discord channel into a RCON console.',
|
||||
defaultDisabled: false,
|
||||
|
||||
options = {
|
||||
prependAdminNameInBroadcast: false,
|
||||
...options
|
||||
};
|
||||
optionsSpec: {
|
||||
discordClient: {
|
||||
type: 'DiscordConnector',
|
||||
required: true,
|
||||
default: 'discord',
|
||||
description: 'The name of the Discord Connector to use.'
|
||||
},
|
||||
channelID: {
|
||||
type: 'Discord Channel ID',
|
||||
required: true,
|
||||
default: 'Discord Channel ID',
|
||||
description: 'The ID of the channel you wish to turn into a RCON console.'
|
||||
},
|
||||
prependAdminNameInBroadcast: {
|
||||
type: 'Boolean',
|
||||
required: false,
|
||||
default: false,
|
||||
description: "Prepend the admin's name when he makes an announcement."
|
||||
}
|
||||
},
|
||||
|
||||
discordClient.on('message', async message => {
|
||||
if (message.author.bot || message.channel.id !== channelID) return;
|
||||
init: async (server, options) => {
|
||||
options.discordClient.on('message', async (message) => {
|
||||
if (message.author.bot || message.channel.id !== options.channelID) return;
|
||||
|
||||
let command = message.content;
|
||||
let command = message.content;
|
||||
|
||||
if(options.prependAdminNameInBroadcast && command.toLowerCase().startsWith('adminbroadcast'))
|
||||
command = command.replace(/^AdminBroadcast /i, `AdminBroadcast ${message.member.displayName}: `);
|
||||
if (options.prependAdminNameInBroadcast && command.toLowerCase().startsWith('adminbroadcast'))
|
||||
command = command.replace(
|
||||
/^AdminBroadcast /i,
|
||||
`AdminBroadcast ${message.member.displayName}: `
|
||||
);
|
||||
|
||||
const response = await server.rcon.execute(command);
|
||||
const response = await server.rcon.execute(command);
|
||||
|
||||
const responseMessages = [''];
|
||||
const responseMessages = [''];
|
||||
|
||||
for (const line of response.split('\n')) {
|
||||
if (responseMessages[responseMessages.length - 1].length + line.length > 1994) {
|
||||
responseMessages.push(line);
|
||||
} else {
|
||||
responseMessages[responseMessages.length - 1] = `${
|
||||
responseMessages[responseMessages.length - 1]
|
||||
}\n${line}`;
|
||||
for (const line of response.split('\n')) {
|
||||
if (responseMessages[responseMessages.length - 1].length + line.length > 1994) {
|
||||
responseMessages.push(line);
|
||||
} else {
|
||||
responseMessages[responseMessages.length - 1] = `${
|
||||
responseMessages[responseMessages.length - 1]
|
||||
}\n${line}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const responseMessage of responseMessages) {
|
||||
await message.channel.send(`\`\`\`${responseMessage}\`\`\``);
|
||||
}
|
||||
});
|
||||
}
|
||||
for (const responseMessage of responseMessages) {
|
||||
await message.channel.send(`\`\`\`${responseMessage}\`\`\``);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -1,33 +0,0 @@
|
||||
<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
|
||||
colorGradient: true, // gradient color based on player count
|
||||
connectLink: true, // show Steam connect link
|
||||
command: '!server', // command used to send message
|
||||
disableStatus: false // disable bot status as server status
|
||||
}
|
||||
);
|
||||
```
|
@ -1,6 +1,6 @@
|
||||
import tinygradient from 'tinygradient';
|
||||
|
||||
import { COPYRIGHT_MESSAGE } from 'core/config';
|
||||
import { COPYRIGHT_MESSAGE } from 'core/constants';
|
||||
import { SERVER_A2S_UPDATED } from 'squad-server/events/server';
|
||||
|
||||
const gradient = tinygradient([
|
||||
@ -54,52 +54,84 @@ function makeEmbed(server, options) {
|
||||
};
|
||||
}
|
||||
|
||||
export default async function(server, discordClient, options = {}) {
|
||||
if (!server) throw new Error('DiscordDebug must be provided with a reference to the server.');
|
||||
export default {
|
||||
name: 'discord-server-status',
|
||||
description: 'This plugin displays server status embeds in Discord.',
|
||||
defaultDisabled: false,
|
||||
|
||||
if (!discordClient) throw new Error('DiscordDebug must be provided with a Discord.js client.');
|
||||
optionsSpec: {
|
||||
discordClient: {
|
||||
type: 'DiscordConnector',
|
||||
required: true,
|
||||
default: 'discord',
|
||||
description: 'The name of the Discord Connector to use.'
|
||||
},
|
||||
color: {
|
||||
type: 'Discord Color Code',
|
||||
required: false,
|
||||
default: 16761867,
|
||||
description: 'The color code of the Discord embed.'
|
||||
},
|
||||
colorGradient: {
|
||||
type: 'Boolean',
|
||||
required: false,
|
||||
default: true,
|
||||
description: 'Apply gradient color to Discord embed depending on the player count.'
|
||||
},
|
||||
connectLink: {
|
||||
type: 'Boolean',
|
||||
required: false,
|
||||
default: true,
|
||||
description: 'Display a Steam server connection link.'
|
||||
},
|
||||
command: {
|
||||
type: 'String',
|
||||
required: false,
|
||||
default: '!server',
|
||||
description: 'The command that displays the embed.'
|
||||
},
|
||||
disableStatus: {
|
||||
type: 'Boolean',
|
||||
required: false,
|
||||
default: false,
|
||||
description: 'Disable the bot status.'
|
||||
}
|
||||
},
|
||||
|
||||
options = {
|
||||
color: 16761867,
|
||||
colorGradient: true,
|
||||
connectLink: true,
|
||||
command: '!server',
|
||||
disableStatus: false,
|
||||
...options
|
||||
};
|
||||
init: async (server, options) => {
|
||||
options.discordClient.on('message', async (message) => {
|
||||
if (message.content !== options.command) return;
|
||||
|
||||
discordClient.on('message', async message => {
|
||||
if (message.content !== options.command) return;
|
||||
const serverStatus = await message.channel.send(makeEmbed(server, options));
|
||||
|
||||
const serverStatus = await message.channel.send(makeEmbed(server, options));
|
||||
await serverStatus.react('🔄');
|
||||
});
|
||||
|
||||
await serverStatus.react('🔄');
|
||||
});
|
||||
options.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;
|
||||
|
||||
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;
|
||||
|
||||
// ignore bots reacting
|
||||
if (reaction.count === 1) return;
|
||||
// remove reaction and readd it
|
||||
await reaction.remove();
|
||||
await reaction.message.react('🔄');
|
||||
|
||||
// remove reaction and readd it
|
||||
await reaction.remove();
|
||||
await reaction.message.react('🔄');
|
||||
// update the message
|
||||
await reaction.message.edit(makeEmbed(server, options));
|
||||
});
|
||||
|
||||
// update the message
|
||||
await reaction.message.edit(makeEmbed(server, options));
|
||||
});
|
||||
|
||||
server.on(SERVER_A2S_UPDATED, () => {
|
||||
if (!options.disableStatus)
|
||||
discordClient.user.setActivity(
|
||||
`(${server.playerCount}/${server.publicSlots}) ${server.currentLayer}`,
|
||||
{ type: 'WATCHING' }
|
||||
);
|
||||
});
|
||||
}
|
||||
server.on(SERVER_A2S_UPDATED, () => {
|
||||
if (!options.disableStatus)
|
||||
options.discordClient.user.setActivity(
|
||||
`(${server.playerCount}/${server.publicSlots}) ${server.currentLayer}`,
|
||||
{ type: 'WATCHING' }
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -1,32 +0,0 @@
|
||||
<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.
|
||||
teamkillColor: 16761867, // colour of TK embed
|
||||
suicideColor: 16761867, // colour of suicide embed
|
||||
ignoreSuicides: false, // ignore suicide events
|
||||
}
|
||||
);
|
||||
```
|
@ -1,70 +1,101 @@
|
||||
import { COPYRIGHT_MESSAGE } from 'core/config';
|
||||
import { COPYRIGHT_MESSAGE } from 'core/constants';
|
||||
import { LOG_PARSER_TEAMKILL } from 'squad-server/events/log-parser';
|
||||
|
||||
export default async function(server, discordClient, channelID, options = {}) {
|
||||
if (!server) throw new Error('DiscordTeamKill must be provided with a reference to the server.');
|
||||
export default {
|
||||
name: 'discord-teamkill',
|
||||
description: 'Log teamkills to Discord.',
|
||||
defaultDisabled: false,
|
||||
|
||||
if (!discordClient) throw new Error('DiscordTeamkill must be provided with a Discord.js client.');
|
||||
optionsSpec: {
|
||||
discordClient: {
|
||||
type: 'DiscordConnector',
|
||||
required: true,
|
||||
default: 'discord',
|
||||
description: 'The name of the Discord Connector to use.'
|
||||
},
|
||||
channelID: {
|
||||
type: 'Discord Channel ID',
|
||||
required: true,
|
||||
default: 'Discord Channel ID',
|
||||
description: 'The ID of the channel to log admin broadcasts to.'
|
||||
},
|
||||
teamkillColor: {
|
||||
type: 'Discord Color Code',
|
||||
required: false,
|
||||
default: 16761867,
|
||||
description: 'The color of the embed for teamkills.'
|
||||
},
|
||||
suicideColor: {
|
||||
type: 'Discord Color Code',
|
||||
required: false,
|
||||
default: 16761867,
|
||||
description: 'The color of the embed for suicides.'
|
||||
},
|
||||
ignoreSuicides: {
|
||||
type: 'Boolean',
|
||||
required: false,
|
||||
default: false,
|
||||
description: 'Ignore suicides.'
|
||||
},
|
||||
disableSCBL: {
|
||||
type: 'Boolean',
|
||||
required: false,
|
||||
default: false,
|
||||
description: 'Disable Squad Community Ban List information.'
|
||||
}
|
||||
},
|
||||
|
||||
if (!channelID) throw new Error('DiscordTeamkill must be provided with a channel ID.');
|
||||
init: async (server, options) => {
|
||||
const channel = await options.discordClient.channels.fetch(options.channelID);
|
||||
|
||||
options = {
|
||||
teamkillColor: 16761867,
|
||||
suicideColor: 16761867,
|
||||
ignoreSuicides: false,
|
||||
disableSCBL: false,
|
||||
...options
|
||||
};
|
||||
server.on(LOG_PARSER_TEAMKILL, (info) => {
|
||||
if (!info.attacker) return;
|
||||
if (options.ignoreSuicides && info.suicide) return;
|
||||
|
||||
const channel = await discordClient.channels.fetch(channelID);
|
||||
|
||||
server.on(LOG_PARSER_TEAMKILL, info => {
|
||||
if (!info.attacker) return;
|
||||
if (options.ignoreSuicides && info.suicide) return;
|
||||
|
||||
const 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
|
||||
}
|
||||
];
|
||||
|
||||
if (!options.disableSCBL)
|
||||
fields.push({
|
||||
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})`
|
||||
});
|
||||
|
||||
channel.send({
|
||||
embed: {
|
||||
title: `${info.suicide ? 'Suicide' : 'Teamkill'}: ${info.attacker.name}`,
|
||||
color: info.suicide ? options.suicideColor : options.teamkillColor,
|
||||
fields: fields,
|
||||
timestamp: info.time.toISOString(),
|
||||
footer: {
|
||||
text: COPYRIGHT_MESSAGE
|
||||
const 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
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
if (!options.disableSCBL)
|
||||
fields.push({
|
||||
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})`
|
||||
});
|
||||
|
||||
channel.send({
|
||||
embed: {
|
||||
title: `${info.suicide ? 'Suicide' : 'Teamkill'}: ${info.attacker.name}`,
|
||||
color: info.suicide ? options.suicideColor : options.teamkillColor,
|
||||
fields: fields,
|
||||
timestamp: info.time.toISOString(),
|
||||
footer: {
|
||||
text: COPYRIGHT_MESSAGE
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -2,14 +2,13 @@ import autoTKWarn from './auto-tk-warn/index.js';
|
||||
import discordAdminBroadcast from './discord-admin-broadcast/index.js';
|
||||
import discordAdminCamLogs from './discord-admin-cam-logs/index.js';
|
||||
import discordChat from './discord-chat/index.js';
|
||||
import discordChatAdminRequest from './discord-chat-admin-request/index.js';
|
||||
import discordChatAdminRequest from './discord-admin-request/index.js';
|
||||
import discordDebug from './discord-debug/index.js';
|
||||
import discordRCON from './discord-rcon/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 mapvote123 from './mapvote/mapvote-123.js';
|
||||
import mapvoteDidYouMean from './mapvote/mapvote-did-you-mean.js';
|
||||
import mysqlLog from './mysql-log/index.js';
|
||||
import seedingMessage from './seeding-message/index.js';
|
||||
import teamRandomizer from './team-randomizer/index.js';
|
||||
@ -24,10 +23,32 @@ export {
|
||||
discordRCON,
|
||||
discordServerStatus,
|
||||
discordTeamkill,
|
||||
influxdbLog,
|
||||
influxdbLogDefaultSchema,
|
||||
mapvote,
|
||||
mapvote123,
|
||||
mapvoteDidYouMean,
|
||||
mysqlLog,
|
||||
seedingMessage,
|
||||
teamRandomizer
|
||||
};
|
||||
|
||||
const plugins = [
|
||||
autoTKWarn,
|
||||
discordAdminBroadcast,
|
||||
discordAdminCamLogs,
|
||||
discordChat,
|
||||
discordChatAdminRequest,
|
||||
discordDebug,
|
||||
discordRCON,
|
||||
discordServerStatus,
|
||||
discordTeamkill,
|
||||
mapvote123,
|
||||
mapvoteDidYouMean,
|
||||
mysqlLog,
|
||||
seedingMessage,
|
||||
teamRandomizer
|
||||
];
|
||||
|
||||
const namedPlugins = {};
|
||||
for (const plugin of plugins) {
|
||||
namedPlugins[plugin.name] = plugin;
|
||||
}
|
||||
export default namedPlugins;
|
||||
|
@ -1,30 +0,0 @@
|
||||
<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);
|
||||
```
|
@ -1,123 +0,0 @@
|
||||
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(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.');
|
||||
|
||||
const serverID = options.overrideServerID || server.id;
|
||||
|
||||
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: serverID },
|
||||
fields: { tick_rate: info.tickRate },
|
||||
timestamp: info.time
|
||||
});
|
||||
});
|
||||
|
||||
server.on(SERVER_PLAYERS_UPDATED, players => {
|
||||
points.push({
|
||||
measurement: 'PlayerCount',
|
||||
tags: { server: serverID },
|
||||
fields: { player_count: players.length },
|
||||
timestamp: new Date()
|
||||
});
|
||||
});
|
||||
|
||||
server.on(LOG_PARSER_NEW_GAME, info => {
|
||||
points.push({
|
||||
measurement: 'Match',
|
||||
tags: { server: serverID },
|
||||
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: serverID },
|
||||
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: serverID },
|
||||
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: 'PlayerRevived',
|
||||
tags: { server: serverID },
|
||||
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
|
||||
});
|
||||
});
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
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']
|
||||
}
|
||||
];
|
@ -1,134 +0,0 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="../../assets/squadjs-logo.png" alt="Logo" width="500"/>
|
||||
|
||||
#### SquadJS - Map Vote
|
||||
</div>
|
||||
|
||||
## Map Vote "Did you mean?"
|
||||
### About
|
||||
Map Vote "Did you mean?" is best suited for servers who wish to allow players to vote for any layer in a large pool of options as it allows players to vote by specifying the layer name in chat. It uses a "did you mean?" algorithm to correct misspelling in layer names making it easier for players to vote.
|
||||
|
||||
Commands:
|
||||
* `!mapvote help` - Shows other commands players can use.
|
||||
* `!mapvote results` - See the map vote results.
|
||||
* `!mapvote <layer name>` - Vote for a specific layer. Misspelling will be corrected where possible.
|
||||
|
||||
|
||||
* `!mapvote start` (Admin chat only) - Starts a new map vote.
|
||||
* `!mapvote restart` (Admin chat only) - Restarts a map vote.
|
||||
* `!mapvote end` (Admin chat only) - Ends a map vote and announces the winner.
|
||||
* `!mapvote destroy` (Admin chat only) - End a map vote without announcing the winner.
|
||||
|
||||
### Installation
|
||||
Add the following two lines at the top of your index.js file to import the required components:
|
||||
```js
|
||||
import SquadLayerFilter from 'connectors/squad-layer-filter';
|
||||
import { mapvote } from 'plugins';
|
||||
```
|
||||
|
||||
To control which constraints, e.g. map history and player count compliant, you need to create an active layer filter.
|
||||
```js
|
||||
const activeLayerFilter = {
|
||||
historyResetTime: 5 * 60 * 60 * 1000, // after 5 hours the layer history is ignored. null if off
|
||||
layerHistoryTolerance: 8, // a layer can be only played once every x layers. null if off
|
||||
mapHistoryTolerance: 4, // a map can only be played once every x layers. null if off
|
||||
gamemodeHistoryTolerance: {
|
||||
Invasion: 4 // invasion can only be played once every x layers
|
||||
// if not specified they will default to off
|
||||
},
|
||||
gamemodeRepetitiveTolerance: {
|
||||
Invasion: 4 // invasion can only be played up to x times in a row
|
||||
// if not specified they will default to off
|
||||
},
|
||||
playerCountComplianceEnabled: true, // filter layers based on suggested player counts if true
|
||||
factionComplianceEnabled: true, // a team cannot play the same faction twice in a row
|
||||
factionHistoryTolerance: {
|
||||
RUS: 4 // rus can only be played once every x layers
|
||||
// if not specified they will default to off
|
||||
},
|
||||
factionRepetitiveTolerance: {
|
||||
RUS: 4 // rus can only be played up to x times in a row
|
||||
// if not specified they will default to off
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
You can turn off all options with:
|
||||
```js
|
||||
const activeLayerFilter = null;
|
||||
```
|
||||
|
||||
Create a layer pool with one of the following options:
|
||||
```js
|
||||
// from a list
|
||||
const squadLayerFilter = SquadLayerFilter.buildFromFilter(['Layer name 1', 'layer name 2'], activeLayerFilter);
|
||||
|
||||
// from a file of layer anmes separated by new lines
|
||||
const squadLayerFilter = SquadLayerFilter.buildFromFile('filename', activeLayerFilter);
|
||||
|
||||
// from a filter
|
||||
const squadLayerFilter = SquadLayerFilter.buildFromFilter(
|
||||
{ // these options can also be turned off by replacing the value with null
|
||||
whitelistedLayers: null, // a list of layers that can be played
|
||||
blacklistedLayers: null, // a list of layers that cannot be played
|
||||
whitelistedMaps: null, // a list of maps that can be played
|
||||
blacklistedMaps: null, // a list of maps that cannot be played
|
||||
whitelistedGamemodes: null, // a list of gamemodes that can be played
|
||||
blacklistedGamemodes: ['Training'], // a list of gamemodes that cannot be played
|
||||
flagCountMin: null, // layers must have move than this number of flags
|
||||
flagCountMax: null, // layers must have less than this number of flags
|
||||
hasCommander: null, // layer must have a commander
|
||||
hasTanks: null, // layer must have tanks
|
||||
hasHelicopters: null // layer must have helicopters
|
||||
},
|
||||
activeLayerFilter
|
||||
);
|
||||
```
|
||||
|
||||
Setup the map vote plugin:
|
||||
```js
|
||||
mapvote(
|
||||
server,
|
||||
'didyoumean',
|
||||
squadLayerFilter,
|
||||
{
|
||||
alwaysOn: true, // map vote will start without admin interaction if true
|
||||
minPlayerCount: null, // this number of players must be online before they can vote. null is off
|
||||
minVoteCount: null, // this number of votes must be counted before a layer is selected. null is off
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## Map Vote "123"
|
||||
### About
|
||||
Map Vote "123" is best suited for servers who want to allow admins to create map votes that allow players to easily choose from a small selection of layers.
|
||||
|
||||
Commands:
|
||||
* `!mapvote help` - Shows other commands players can use.
|
||||
* `!mapvote results` - See the map vote results.
|
||||
* `<layer number>` - Vote for a specific layer via it's associated number.
|
||||
|
||||
|
||||
* `!mapvote start <layer name 1>, <layer name 2>` (Admin chat only) - Starts a new map vote.
|
||||
* `!mapvote restart` (Admin chat only) - Restarts a map vote with the same layers.
|
||||
* `!mapvote end` (Admin chat only) - Ends a map vote and announces the winner.
|
||||
* `!mapvote destroy` (Admin chat only) - End a map vote without announcing the winner.
|
||||
|
||||
### Installation
|
||||
Add the following two lines at the top of your index.js file to import the required components:
|
||||
```js
|
||||
import SquadLayerFilter from 'connectors/squad-layer-filter';
|
||||
import { mapvote } from 'plugins';
|
||||
```
|
||||
|
||||
Setup the map vote plugin:
|
||||
```js
|
||||
mapvote(
|
||||
server,
|
||||
'123',
|
||||
{
|
||||
minVoteCount: null, // this number of votes must be counted before a layer is selected. null is off
|
||||
}
|
||||
);
|
||||
```
|
@ -1,15 +0,0 @@
|
||||
import mapvoteDidYouMean from './mapvote-did-you-mean.js';
|
||||
import mapvote123 from './mapvote-123.js';
|
||||
|
||||
export default function(server, mode, ...args) {
|
||||
switch (mode) {
|
||||
case 'didyoumean':
|
||||
mapvoteDidYouMean(server, ...args);
|
||||
break;
|
||||
case '123':
|
||||
mapvote123(server, ...args);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid mode.');
|
||||
}
|
||||
}
|
@ -1,54 +1,94 @@
|
||||
import SquadLayerFilter from 'connectors/squad-layer-filter';
|
||||
import { COPYRIGHT_MESSAGE } from 'core/config';
|
||||
import { SquadLayerFilter } from 'core/squad-layers';
|
||||
import { COPYRIGHT_MESSAGE } from 'core/constants';
|
||||
import { LOG_PARSER_NEW_GAME } from 'squad-server/events/log-parser';
|
||||
import { RCON_CHAT_MESSAGE } from 'squad-server/events/rcon';
|
||||
|
||||
import MapVote from './mapvote.js';
|
||||
|
||||
export default function(server, options = {}) {
|
||||
let mapvote = null;
|
||||
export default {
|
||||
name: 'mapvote-123',
|
||||
description: 'A map voting system that uses numbers to allow players to vote on layers.',
|
||||
defaultDisabled: true,
|
||||
|
||||
options = {
|
||||
minVoteCount: null,
|
||||
...options
|
||||
};
|
||||
|
||||
server.on(LOG_PARSER_NEW_GAME, () => {
|
||||
mapvote = null;
|
||||
});
|
||||
|
||||
server.on(RCON_CHAT_MESSAGE, async info => {
|
||||
const voteMatch = info.message.match(/^([0-9])/);
|
||||
if (voteMatch) {
|
||||
if (!mapvote) return;
|
||||
try {
|
||||
const layerName = await mapvote.makeVoteByNumber(info.steamID, parseInt(voteMatch[1]));
|
||||
await server.rcon.warn(info.steamID, `You voted for ${layerName}.`);
|
||||
} catch (err) {
|
||||
await server.rcon.warn(info.steamID, err.message);
|
||||
}
|
||||
await server.rcon.warn(info.steamID, `Powered by: ${COPYRIGHT_MESSAGE}`);
|
||||
optionsSpec: {
|
||||
minVoteCount: {
|
||||
type: 'Integer',
|
||||
required: false,
|
||||
default: null,
|
||||
description: 'The minimum number of votes required for the vote to succeed.'
|
||||
}
|
||||
},
|
||||
|
||||
const commandMatch = info.message.match(/^!mapvote ?(.*)/);
|
||||
if (commandMatch) {
|
||||
if (commandMatch[1].startsWith('start')) {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
init: async (server, options) => {
|
||||
let mapvote = null;
|
||||
|
||||
if (mapvote) {
|
||||
await server.rcon.warn(info.steamID, 'A mapvote has already begun.');
|
||||
} else {
|
||||
mapvote = new MapVote(
|
||||
server,
|
||||
SquadLayerFilter.buildFromDidYouMeanList(
|
||||
commandMatch[1].replace('start ', '').split(', ')
|
||||
),
|
||||
{ minVoteCount: options.minVoteCount }
|
||||
);
|
||||
server.on(LOG_PARSER_NEW_GAME, () => {
|
||||
mapvote = null;
|
||||
});
|
||||
|
||||
server.on(RCON_CHAT_MESSAGE, async (info) => {
|
||||
const voteMatch = info.message.match(/^([0-9])/);
|
||||
if (voteMatch) {
|
||||
if (!mapvote) return;
|
||||
try {
|
||||
const layerName = await mapvote.makeVoteByNumber(info.steamID, parseInt(voteMatch[1]));
|
||||
await server.rcon.warn(info.steamID, `You voted for ${layerName}.`);
|
||||
} catch (err) {
|
||||
await server.rcon.warn(info.steamID, err.message);
|
||||
}
|
||||
await server.rcon.warn(info.steamID, `Powered by: ${COPYRIGHT_MESSAGE}`);
|
||||
}
|
||||
|
||||
const commandMatch = info.message.match(/^!mapvote ?(.*)/);
|
||||
if (commandMatch) {
|
||||
if (commandMatch[1].startsWith('start')) {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
|
||||
if (mapvote) {
|
||||
await server.rcon.warn(info.steamID, 'A mapvote has already begun.');
|
||||
} else {
|
||||
mapvote = new MapVote(
|
||||
server,
|
||||
SquadLayerFilter.buildFromDidYouMeanList(
|
||||
commandMatch[1].replace('start ', '').split(', ')
|
||||
),
|
||||
{ minVoteCount: options.minVoteCount }
|
||||
);
|
||||
|
||||
mapvote.on('NEW_WINNER', async (results) => {
|
||||
await server.rcon.broadcast(
|
||||
`New Map Vote Winner: ${results[0].layer.layer}. Participate in the map vote by typing "!mapvote help" in chat.`
|
||||
);
|
||||
});
|
||||
|
||||
mapvote.on('NEW_WINNER', async results => {
|
||||
await server.rcon.broadcast(
|
||||
`New Map Vote Winner: ${results[0].layer.layer}. Participate in the map vote by typing "!mapvote help" in chat.`
|
||||
`A new map vote has started. Participate in the map vote by typing "!mapvote help" in chat. Map options to follow...`
|
||||
);
|
||||
await server.rcon.broadcast(
|
||||
mapvote.squadLayerFilter
|
||||
.getLayerNames()
|
||||
.map((layerName, key) => `${key + 1} - ${layerName}`)
|
||||
.join(', ')
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mapvote) {
|
||||
await server.rcon.warn(info.steamID, 'A map vote has not begun.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (commandMatch[1] === 'restart') {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
|
||||
mapvote = new MapVote(server, mapvote.squadLayerFilter, {
|
||||
minVoteCount: options.minVoteCount
|
||||
});
|
||||
|
||||
mapvote.on('NEW_WINNER', async (results) => {
|
||||
await server.rcon.broadcast(
|
||||
`New Map Vote Winner: ${results[0].layer}. Participate in the map vote by typing "!mapvote help" in chat.`
|
||||
);
|
||||
});
|
||||
|
||||
@ -61,94 +101,65 @@ export default function(server, options = {}) {
|
||||
.map((layerName, key) => `${key + 1} - ${layerName}`)
|
||||
.join(', ')
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mapvote) {
|
||||
await server.rcon.warn(info.steamID, 'A map vote has not begun.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (commandMatch[1] === 'restart') {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
|
||||
mapvote = new MapVote(server, mapvote.squadLayerFilter, {
|
||||
minVoteCount: options.minVoteCount
|
||||
});
|
||||
|
||||
mapvote.on('NEW_WINNER', async results => {
|
||||
await server.rcon.broadcast(
|
||||
`New Map Vote Winner: ${results[0].layer}. Participate in the map vote by typing "!mapvote help" in chat.`
|
||||
);
|
||||
});
|
||||
|
||||
await server.rcon.broadcast(
|
||||
`A new map vote has started. Participate in the map vote by typing "!mapvote help" in chat. Map options to follow...`
|
||||
);
|
||||
await server.rcon.broadcast(
|
||||
mapvote.squadLayerFilter
|
||||
.getLayerNames()
|
||||
.map((layerName, key) => `${key + 1} - ${layerName}`)
|
||||
.join(', ')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (commandMatch[1] === 'end') {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
|
||||
const results = mapvote.getResults();
|
||||
|
||||
if (results.length === 0)
|
||||
await server.rcon.broadcast(`No layer gained enough votes to win.`);
|
||||
else await server.rcon.broadcast(`${mapvote.getResults()[0].layer.layer} won the mapvote!`);
|
||||
|
||||
mapvote = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (commandMatch[1] === 'destroy') {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
mapvote = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (commandMatch[1] === 'help') {
|
||||
await server.rcon.warn(info.steamID, 'To vote type the layer number into chat:');
|
||||
for (const layer of mapvote.squadLayerFilter.getLayers()) {
|
||||
await server.rcon.warn(info.steamID, `${layer.layerNumber} - ${layer.layer}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.minVoteCount !== null)
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
`${options.minVoteCount} votes need to be made for a winner to be selected.`
|
||||
);
|
||||
if (commandMatch[1] === 'end') {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
'To see current results type into chat: !mapvote results'
|
||||
);
|
||||
}
|
||||
const results = mapvote.getResults();
|
||||
|
||||
if (commandMatch[1] === 'results') {
|
||||
const results = mapvote.getResults();
|
||||
if (results.length === 0)
|
||||
await server.rcon.broadcast(`No layer gained enough votes to win.`);
|
||||
else
|
||||
await server.rcon.broadcast(`${mapvote.getResults()[0].layer.layer} won the mapvote!`);
|
||||
|
||||
if (results.length === 0) {
|
||||
await server.rcon.warn(info.steamID, 'No one has voted yet.');
|
||||
} else {
|
||||
await server.rcon.warn(info.steamID, 'The current vote counts are as follows:');
|
||||
for (const result of results) {
|
||||
mapvote = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (commandMatch[1] === 'destroy') {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
mapvote = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (commandMatch[1] === 'help') {
|
||||
await server.rcon.warn(info.steamID, 'To vote type the layer number into chat:');
|
||||
for (const layer of mapvote.squadLayerFilter.getLayers()) {
|
||||
await server.rcon.warn(info.steamID, `${layer.layerNumber} - ${layer.layer}`);
|
||||
}
|
||||
|
||||
if (options.minVoteCount !== null)
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
`${result.layer.layerNumber} - ${result.layer.layer} (${result.votes} vote${
|
||||
result.votes > 1 ? 's' : ''
|
||||
})`
|
||||
`${options.minVoteCount} votes need to be made for a winner to be selected.`
|
||||
);
|
||||
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
'To see current results type into chat: !mapvote results'
|
||||
);
|
||||
}
|
||||
|
||||
if (commandMatch[1] === 'results') {
|
||||
const results = mapvote.getResults();
|
||||
|
||||
if (results.length === 0) {
|
||||
await server.rcon.warn(info.steamID, 'No one has voted yet.');
|
||||
} else {
|
||||
await server.rcon.warn(info.steamID, 'The current vote counts are as follows:');
|
||||
for (const result of results) {
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
`${result.layer.layerNumber} - ${result.layer.layer} (${result.votes} vote${
|
||||
result.votes > 1 ? 's' : ''
|
||||
})`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -1,139 +1,170 @@
|
||||
import { COPYRIGHT_MESSAGE } from 'core/config';
|
||||
import { COPYRIGHT_MESSAGE } from 'core/constants';
|
||||
import { LOG_PARSER_NEW_GAME } from 'squad-server/events/log-parser';
|
||||
import { RCON_CHAT_MESSAGE } from 'squad-server/events/rcon';
|
||||
|
||||
import MapVote from './mapvote.js';
|
||||
|
||||
export default function(server, squadLayerFilter, options = {}) {
|
||||
options = {
|
||||
alwaysOn: true,
|
||||
minPlayerCount: null,
|
||||
minVoteCount: null,
|
||||
...options
|
||||
};
|
||||
export default {
|
||||
name: 'mapvote-did-you-mean',
|
||||
description:
|
||||
'A map voting system that uses a "Did you mean?" algorithm to allow players to vote on layers.',
|
||||
defaultDisabled: true,
|
||||
|
||||
let mapvote;
|
||||
let manuallyCreated;
|
||||
|
||||
async function newMapvote(manuallyCreatedOption = true) {
|
||||
mapvote = new MapVote(server, squadLayerFilter, {
|
||||
minVoteCount: options.minVoteCount
|
||||
});
|
||||
|
||||
manuallyCreated = manuallyCreatedOption;
|
||||
|
||||
mapvote.on('NEW_WINNER', async results => {
|
||||
await server.rcon.broadcast(
|
||||
`New Map Vote Winner: ${results[0].layer.layer}. Participate in the map vote by typing "!mapvote help" in chat.`
|
||||
);
|
||||
});
|
||||
|
||||
if (manuallyCreated)
|
||||
await server.rcon.broadcast(
|
||||
`A new map vote has started. Participate in the map vote by typing "!mapvote help" in chat.`
|
||||
);
|
||||
}
|
||||
|
||||
if (options.alwaysOn) newMapvote(false);
|
||||
|
||||
server.on(LOG_PARSER_NEW_GAME, () => {
|
||||
if (options.alwaysOn) {
|
||||
newMapvote(false);
|
||||
} else {
|
||||
mapvote = null;
|
||||
optionsSpec: {
|
||||
layerFilter: {
|
||||
type: 'SquadLayerFilterConnector',
|
||||
required: false,
|
||||
default: 'layerFilter',
|
||||
description: 'The layers players can choose from.'
|
||||
},
|
||||
alwaysOn: {
|
||||
type: 'Boolean',
|
||||
required: false,
|
||||
default: true,
|
||||
description: 'If true then the map voting system will always be live.'
|
||||
},
|
||||
minPlayerCount: {
|
||||
type: 'Integer',
|
||||
required: false,
|
||||
default: null,
|
||||
description: 'The minimum number of players required for the vote to succeed.'
|
||||
},
|
||||
minVoteCount: {
|
||||
type: 'Integer',
|
||||
required: false,
|
||||
default: null,
|
||||
description: 'The minimum number of votes required for the vote to succeed.'
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
server.on(RCON_CHAT_MESSAGE, async info => {
|
||||
const match = info.message.match(/^!mapvote ?(.*)/);
|
||||
if (!match) return;
|
||||
init: async (server, options) => {
|
||||
let mapvote;
|
||||
let manuallyCreated;
|
||||
|
||||
if (match[1] === 'help') {
|
||||
await server.rcon.warn(info.steamID, 'You may use any of the following commands in chat:');
|
||||
await server.rcon.warn(info.steamID, '!mapvote results - View the current vote counts.');
|
||||
await server.rcon.warn(info.steamID, '!mapvote <layer name> - Vote for the specified layer.');
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
'When inputting a layer name, we autocorrect any miss spelling.'
|
||||
);
|
||||
async function newMapvote(manuallyCreatedOption = true) {
|
||||
mapvote = new MapVote(server, options.squadLayerFilter, {
|
||||
minVoteCount: options.minVoteCount
|
||||
});
|
||||
|
||||
if (options.minVoteCount !== null)
|
||||
manuallyCreated = manuallyCreatedOption;
|
||||
|
||||
mapvote.on('NEW_WINNER', async (results) => {
|
||||
await server.rcon.broadcast(
|
||||
`New Map Vote Winner: ${results[0].layer.layer}. Participate in the map vote by typing "!mapvote help" in chat.`
|
||||
);
|
||||
});
|
||||
|
||||
if (manuallyCreated)
|
||||
await server.rcon.broadcast(
|
||||
`A new map vote has started. Participate in the map vote by typing "!mapvote help" in chat.`
|
||||
);
|
||||
}
|
||||
|
||||
if (options.alwaysOn) newMapvote(false);
|
||||
|
||||
server.on(LOG_PARSER_NEW_GAME, () => {
|
||||
if (options.alwaysOn) {
|
||||
newMapvote(false);
|
||||
} else {
|
||||
mapvote = null;
|
||||
}
|
||||
});
|
||||
|
||||
server.on(RCON_CHAT_MESSAGE, async (info) => {
|
||||
const match = info.message.match(/^!mapvote ?(.*)/);
|
||||
if (!match) return;
|
||||
|
||||
if (match[1] === 'help') {
|
||||
await server.rcon.warn(info.steamID, 'You may use any of the following commands in chat:');
|
||||
await server.rcon.warn(info.steamID, '!mapvote results - View the current vote counts.');
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
`${options.minVoteCount} votes need to be made for a winner to be selected.`
|
||||
'!mapvote <layer name> - Vote for the specified layer.'
|
||||
);
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
'When inputting a layer name, we autocorrect any miss spelling.'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (match[1] === 'start') {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
|
||||
if (mapvote) {
|
||||
await server.rcon.warn(info.steamID, 'A mapvote has already begun.');
|
||||
} else {
|
||||
await newMapvote();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mapvote) {
|
||||
await server.rcon.warn(info.steamID, 'A map vote has not begun.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (match[1] === 'restart') {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
await newMapvote();
|
||||
return;
|
||||
}
|
||||
|
||||
if (match[1] === 'end') {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
|
||||
const results = mapvote.getResults(true);
|
||||
|
||||
if (results.length === 0) await server.rcon.broadcast(`No layer gained enough votes to win.`);
|
||||
else await server.rcon.broadcast(`${mapvote.getResults()[0].layer.layer} won the mapvote!`);
|
||||
|
||||
mapvote = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (match[1] === 'destroy') {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
mapvote = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (match[1] === 'results') {
|
||||
const results = mapvote.getResults();
|
||||
|
||||
if (results.length === 0) {
|
||||
await server.rcon.warn(info.steamID, 'No one has voted yet.');
|
||||
} else {
|
||||
await server.rcon.warn(info.steamID, 'The current vote counts are as follows:');
|
||||
for (const result of results) {
|
||||
if (options.minVoteCount !== null)
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
`${result.layer.layer} - ${result.votes} vote${result.votes > 1 ? 's' : ''}`
|
||||
`${options.minVoteCount} votes need to be made for a winner to be selected.`
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (match[1] === 'start') {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
|
||||
if (mapvote) {
|
||||
await server.rcon.warn(info.steamID, 'A mapvote has already begun.');
|
||||
} else {
|
||||
await newMapvote();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!manuallyCreated && server.players.length < options.minPlayerCount) {
|
||||
await server.rcon.warn(info.steamID, 'Not enough players online to vote.');
|
||||
return;
|
||||
}
|
||||
if (!mapvote) {
|
||||
await server.rcon.warn(info.steamID, 'A map vote has not begun.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const layerName = await mapvote.makeVoteByDidYouMean(info.steamID, match[1]);
|
||||
await server.rcon.warn(info.steamID, `You voted for ${layerName}.`);
|
||||
} catch (err) {
|
||||
await server.rcon.warn(info.steamID, err.message);
|
||||
}
|
||||
await server.rcon.warn(info.steamID, `Powered by: ${COPYRIGHT_MESSAGE}`);
|
||||
});
|
||||
}
|
||||
if (match[1] === 'restart') {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
await newMapvote();
|
||||
return;
|
||||
}
|
||||
|
||||
if (match[1] === 'end') {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
|
||||
const results = mapvote.getResults(true);
|
||||
|
||||
if (results.length === 0)
|
||||
await server.rcon.broadcast(`No layer gained enough votes to win.`);
|
||||
else await server.rcon.broadcast(`${mapvote.getResults()[0].layer.layer} won the mapvote!`);
|
||||
|
||||
mapvote = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (match[1] === 'destroy') {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
mapvote = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (match[1] === 'results') {
|
||||
const results = mapvote.getResults();
|
||||
|
||||
if (results.length === 0) {
|
||||
await server.rcon.warn(info.steamID, 'No one has voted yet.');
|
||||
} else {
|
||||
await server.rcon.warn(info.steamID, 'The current vote counts are as follows:');
|
||||
for (const result of results) {
|
||||
await server.rcon.warn(
|
||||
info.steamID,
|
||||
`${result.layer.layer} - ${result.votes} vote${result.votes > 1 ? 's' : ''}`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!manuallyCreated && server.players.length < options.minPlayerCount) {
|
||||
await server.rcon.warn(info.steamID, 'Not enough players online to vote.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const layerName = await mapvote.makeVoteByDidYouMean(info.steamID, match[1]);
|
||||
await server.rcon.warn(info.steamID, `You voted for ${layerName}.`);
|
||||
} catch (err) {
|
||||
await server.rcon.warn(info.steamID, err.message);
|
||||
}
|
||||
await server.rcon.warn(info.steamID, `Powered by: ${COPYRIGHT_MESSAGE}`);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import EventEmitter from 'events';
|
||||
|
||||
import SquadLayers from 'connectors/squad-layers';
|
||||
import { SquadLayers } from 'core/squad-layers';
|
||||
|
||||
export default class MapVote extends EventEmitter {
|
||||
constructor(server, squadLayerFilter, options = {}) {
|
||||
@ -47,7 +47,7 @@ export default class MapVote extends EventEmitter {
|
||||
Object.keys(this.playerVotes).length >= this.minVoteCount
|
||||
) {
|
||||
return Object.keys(this.layerVotes)
|
||||
.map(layerName => ({
|
||||
.map((layerName) => ({
|
||||
layer: this.squadLayerFilter.getLayerByLayerName(layerName),
|
||||
votes: this.layerVotes[layerName]
|
||||
}))
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="../../assets/squadjs-logo.png" alt="Logo" width="500"/>
|
||||
<img src="../../core/assets/squadjs-logo.png" alt="Logo" width="500"/>
|
||||
|
||||
#### SquadJS - MySQL Log
|
||||
</div>
|
||||
|
@ -7,97 +7,113 @@ import {
|
||||
} from 'squad-server/events/log-parser';
|
||||
import { SERVER_PLAYERS_UPDATED } from 'squad-server/events/server';
|
||||
|
||||
export default function(server, mysqlPool, options = {}) {
|
||||
if (!server) throw new Error('MySQLLog must be provided with a reference to the server.');
|
||||
export default {
|
||||
name: 'mysql-log',
|
||||
description: 'Log server information and statistics to a MySQL DB.',
|
||||
defaultDisabled: true,
|
||||
|
||||
if (!mysqlPool) throw new Error('MySQLLog must be provided with a mysql Pool.');
|
||||
optionsSpec: {
|
||||
mysqlPool: {
|
||||
type: 'MySQLPoolConnector',
|
||||
required: true,
|
||||
default: 'mysql',
|
||||
description: 'The name of the MySQL Pool Connector to use.'
|
||||
},
|
||||
overrideServerID: {
|
||||
type: 'Int',
|
||||
required: false,
|
||||
default: null,
|
||||
description: 'A overridden server ID.'
|
||||
}
|
||||
},
|
||||
|
||||
const serverID = options.overrideServerID || server.id;
|
||||
init: async (server, options) => {
|
||||
const serverID = options.overrideServerID === null ? server.id : options.overrideServerID;
|
||||
|
||||
server.on(LOG_PARSER_SERVER_TICK_RATE, info => {
|
||||
mysqlPool.query('INSERT INTO ServerTickRate(time, server, tick_rate) VALUES (?,?,?)', [
|
||||
info.time,
|
||||
serverID,
|
||||
info.tickRate
|
||||
]);
|
||||
});
|
||||
server.on(LOG_PARSER_SERVER_TICK_RATE, (info) => {
|
||||
options.mysqlPool.query(
|
||||
'INSERT INTO ServerTickRate(time, server, tick_rate) VALUES (?,?,?)',
|
||||
[info.time, serverID, info.tickRate]
|
||||
);
|
||||
});
|
||||
|
||||
server.on(SERVER_PLAYERS_UPDATED, players => {
|
||||
mysqlPool.query('INSERT INTO PlayerCount(time, server, player_count) VALUES (NOW(),?,?)', [
|
||||
serverID,
|
||||
players.length
|
||||
]);
|
||||
});
|
||||
server.on(SERVER_PLAYERS_UPDATED, (players) => {
|
||||
options.mysqlPool.query(
|
||||
'INSERT INTO PlayerCount(time, server, player_count) VALUES (NOW(),?,?)',
|
||||
[serverID, players.length]
|
||||
);
|
||||
});
|
||||
|
||||
server.on(LOG_PARSER_NEW_GAME, info => {
|
||||
mysqlPool.query('call NewMatch(?,?,?,?,?,?,?)', [
|
||||
serverID,
|
||||
info.time,
|
||||
info.dlc,
|
||||
info.mapClassname,
|
||||
info.layerClassname,
|
||||
info.map,
|
||||
info.layer
|
||||
]);
|
||||
});
|
||||
server.on(LOG_PARSER_NEW_GAME, (info) => {
|
||||
options.mysqlPool.query('call NewMatch(?,?,?,?,?,?,?)', [
|
||||
serverID,
|
||||
info.time,
|
||||
info.dlc,
|
||||
info.mapClassname,
|
||||
info.layerClassname,
|
||||
info.map,
|
||||
info.layer
|
||||
]);
|
||||
});
|
||||
|
||||
server.on(LOG_PARSER_PLAYER_WOUNDED, info => {
|
||||
mysqlPool.query('call InsertPlayerWounded(?,?,?,?,?,?,?,?,?,?,?,?,?)', [
|
||||
serverID,
|
||||
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_WOUNDED, (info) => {
|
||||
options.mysqlPool.query('call InsertPlayerWounded(?,?,?,?,?,?,?,?,?,?,?,?,?)', [
|
||||
serverID,
|
||||
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(?,?,?,?,?,?,?,?,?,?,?,?,?,?)', [
|
||||
serverID,
|
||||
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_DIED, (info) => {
|
||||
options.mysqlPool.query('call InsertPlayerDied(?,?,?,?,?,?,?,?,?,?,?,?,?,?)', [
|
||||
serverID,
|
||||
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(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)', [
|
||||
serverID,
|
||||
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
|
||||
]);
|
||||
});
|
||||
}
|
||||
server.on(LOG_PARSER_PLAYER_REVIVED, (info) => {
|
||||
options.mysqlPool.query('call InsertPlayerRevived(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)', [
|
||||
serverID,
|
||||
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
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -6,7 +6,6 @@
|
||||
".": "./index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"connectors": "1.0.0",
|
||||
"didyoumean": "^1.2.1",
|
||||
"influx": "^5.5.1",
|
||||
"squad-server": "1.0.0",
|
||||
|
@ -1,27 +0,0 @@
|
||||
<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
|
||||
}
|
||||
);
|
||||
```
|
@ -1,59 +1,102 @@
|
||||
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.');
|
||||
export default {
|
||||
name: 'seeding-message',
|
||||
description: 'Display seeding messages in admin broadcasts.',
|
||||
defaultDisabled: false,
|
||||
|
||||
const mode = options.mode || 'interval';
|
||||
const interval = options.interval || 150 * 1000;
|
||||
const delay = options.delay || 45 * 1000;
|
||||
optionsSpec: {
|
||||
mode: {
|
||||
type: '`interval` or `onjoin`',
|
||||
required: false,
|
||||
default: 'interval',
|
||||
description: 'Display seeding messages at a set interval or after players join.'
|
||||
},
|
||||
interval: {
|
||||
type: 'Number',
|
||||
required: false,
|
||||
default: 150 * 1000,
|
||||
description: 'How frequently to display the seeding messages in seconds.'
|
||||
},
|
||||
delay: {
|
||||
type: 'Number',
|
||||
required: false,
|
||||
default: 45 * 1000,
|
||||
description: 'How long to wait after a player joins to display the announcement in seconds.'
|
||||
},
|
||||
seedingThreshold: {
|
||||
type: 'Number',
|
||||
required: false,
|
||||
default: 50,
|
||||
description: 'Number of players before the server is considered live.'
|
||||
},
|
||||
seedingMessage: {
|
||||
type: 'String',
|
||||
required: false,
|
||||
default: 'Seeding Rules Active! Fight only over the middle flags! No FOB Hunting!',
|
||||
description: 'The seeding message to display.'
|
||||
},
|
||||
liveEnabled: {
|
||||
type: 'String',
|
||||
required: false,
|
||||
default: true,
|
||||
description: 'Display a "Live" message when a certain player count is met.'
|
||||
},
|
||||
liveThreshold: {
|
||||
type: 'Number',
|
||||
required: false,
|
||||
default: 2,
|
||||
description:
|
||||
'When above the seeding threshold, but within this number "Live" messages are displayed.'
|
||||
},
|
||||
liveMessage: {
|
||||
type: 'String',
|
||||
required: false,
|
||||
default: 'Live',
|
||||
description: 'The "Live" message to display.'
|
||||
}
|
||||
},
|
||||
|
||||
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(() => {
|
||||
init: async (server, options) => {
|
||||
switch (options.mode) {
|
||||
case 'interval':
|
||||
setInterval(() => {
|
||||
const playerCount = server.players.length;
|
||||
|
||||
if (playerCount === 0) return;
|
||||
|
||||
if (playerCount < seedingThreshold) {
|
||||
server.rcon.execute(`AdminBroadcast ${seedingMessage}`);
|
||||
if (playerCount < options.seedingThreshold) {
|
||||
server.rcon.execute(`AdminBroadcast ${options.seedingMessage}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (liveEnabled && playerCount < liveThreshold) {
|
||||
server.rcon.execute(`AdminBroadcast ${liveMessage}`);
|
||||
if (options.liveEnabled && playerCount < options.liveThreshold) {
|
||||
server.rcon.execute(`AdminBroadcast ${options.liveMessage}`);
|
||||
}
|
||||
}, delay);
|
||||
});
|
||||
}, options.interval);
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid SeedingMessage mode.');
|
||||
break;
|
||||
case 'onjoin':
|
||||
server.on(LOG_PARSER_PLAYER_CONNECTED, () => {
|
||||
setTimeout(() => {
|
||||
const playerCount = server.players.length;
|
||||
|
||||
if (playerCount === 0) return;
|
||||
|
||||
if (playerCount < options.seedingThreshold) {
|
||||
server.rcon.execute(`AdminBroadcast ${options.seedingMessage}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.liveEnabled && playerCount < options.liveThreshold) {
|
||||
server.rcon.execute(`AdminBroadcast ${options.liveMessage}`);
|
||||
}
|
||||
}, options.delay);
|
||||
});
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid SeedingMessage mode.');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,20 +0,0 @@
|
||||
<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
|
||||
}
|
||||
);
|
||||
```
|
@ -1,48 +1,55 @@
|
||||
import { RCON_CHAT_MESSAGE } from 'squad-server/events/rcon';
|
||||
|
||||
function shuffle(array) {
|
||||
let currentIndex = array.length;
|
||||
let temporaryValue;
|
||||
let randomIndex;
|
||||
export default {
|
||||
name: 'team-randomizer',
|
||||
description: 'Randomize teams with an admin command.',
|
||||
defaultDisabled: false,
|
||||
|
||||
// While there remain elements to shuffle...
|
||||
while (currentIndex !== 0) {
|
||||
// Pick a remaining element...
|
||||
randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex -= 1;
|
||||
optionsSpec: {
|
||||
command: {
|
||||
type: 'String',
|
||||
required: false,
|
||||
default: '!randomize',
|
||||
description: 'The command used to randomize the teams.'
|
||||
}
|
||||
},
|
||||
|
||||
// And swap it with the current element.
|
||||
temporaryValue = array[currentIndex];
|
||||
array[currentIndex] = array[randomIndex];
|
||||
array[randomIndex] = temporaryValue;
|
||||
}
|
||||
init: async (server, options) => {
|
||||
const commandRegex = new RegExp(`^${options.command}`, 'i');
|
||||
|
||||
return array;
|
||||
}
|
||||
server.on(RCON_CHAT_MESSAGE, (info) => {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
|
||||
export default function(server, options = {}) {
|
||||
if (!server) throw new Error('TeamRandomizer must be provided with a reference to the server.');
|
||||
const match = info.message.match(commandRegex);
|
||||
if (!match) return;
|
||||
|
||||
const command = options.command || '!randomize';
|
||||
const commandRegex = new RegExp(`^${command}`, 'i');
|
||||
const players = server.players.slice(0);
|
||||
|
||||
server.on(RCON_CHAT_MESSAGE, info => {
|
||||
if (info.chat !== 'ChatAdmin') return;
|
||||
let currentIndex = players.length;
|
||||
let temporaryValue;
|
||||
let randomIndex;
|
||||
|
||||
const match = info.message.match(commandRegex);
|
||||
if (!match) return;
|
||||
// While there remain elements to shuffle...
|
||||
while (currentIndex !== 0) {
|
||||
// Pick a remaining element...
|
||||
randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex -= 1;
|
||||
|
||||
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}"`);
|
||||
// And swap it with the current element.
|
||||
temporaryValue = players[currentIndex];
|
||||
players[currentIndex] = players[randomIndex];
|
||||
players[randomIndex] = temporaryValue;
|
||||
}
|
||||
|
||||
team = team === '1' ? '2' : '1';
|
||||
}
|
||||
});
|
||||
}
|
||||
let team = '1';
|
||||
|
||||
for (const player of players) {
|
||||
if (player.teamID !== team) {
|
||||
server.rcon.execute(`AdminForceTeamChange "${player.steamID}"`);
|
||||
}
|
||||
|
||||
team = team === '1' ? '2' : '1';
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -2,8 +2,6 @@ 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';
|
||||
|
||||
@ -94,7 +92,6 @@ export default class Server extends EventEmitter {
|
||||
}
|
||||
|
||||
async watch() {
|
||||
printLogo();
|
||||
console.log(`Watching server ${this.id}...`);
|
||||
if (this.logParser) await this.logParser.watch();
|
||||
if (this.rcon) await this.rcon.watch();
|
||||
|
@ -1,4 +1,4 @@
|
||||
import SquadLayers from 'connectors/squad-layers';
|
||||
import { SquadLayers } from 'core/squad-layers';
|
||||
|
||||
import { LOG_PARSER_NEW_GAME } from '../../events/log-parser.js';
|
||||
|
||||
|
@ -1,74 +0,0 @@
|
||||
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);
|
||||
});
|
||||
}
|
@ -4,7 +4,6 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"async": "^3.2.0",
|
||||
"cli-progress": "^3.8.2",
|
||||
"core": "1.0.0",
|
||||
"ftp-tail": "^1.0.2",
|
||||
"gamedig": "^2.0.20",
|
||||
|
@ -105,12 +105,12 @@ export default class Rcon {
|
||||
|
||||
this.client.on('data', this.onData);
|
||||
|
||||
this.client.on('error', err => {
|
||||
this.client.on('error', (err) => {
|
||||
this.verbose(`Socket Error: ${err.message}`);
|
||||
this.emitter.emit(RCON_ERROR, err);
|
||||
});
|
||||
|
||||
this.client.on('close', async hadError => {
|
||||
this.client.on('close', async (hadError) => {
|
||||
this.verbose(`Socket Closed. AutoReconnect: ${this.autoReconnect}`);
|
||||
this.connected = false;
|
||||
this.client.removeListener('data', this.onData);
|
||||
@ -138,7 +138,7 @@ export default class Rcon {
|
||||
resolve();
|
||||
};
|
||||
|
||||
const onError = err => {
|
||||
const onError = (err) => {
|
||||
this.verbose(`Error Opening Socket: ${err.message}`);
|
||||
this.client.removeListener('connect', onConnect);
|
||||
reject(err);
|
||||
@ -162,7 +162,7 @@ export default class Rcon {
|
||||
resolve();
|
||||
};
|
||||
|
||||
const onError = err => {
|
||||
const onError = (err) => {
|
||||
this.verbose(`Error disconnecting: ${err.message}`);
|
||||
this.client.removeListener('close', onClose);
|
||||
reject(err);
|
||||
|
Loading…
Reference in New Issue
Block a user