Add eslint (#364)

* Add initial prettier and eslint configs

* Modify prettierrc

* Run eslint on everything

* Actually remove prettier

* Fix some eslints

* Remove label in gs2

* Update CHANGELOG

* Update eslintrc to specify es2021
This commit is contained in:
CosminPerRam 2023-09-19 19:52:35 +03:00 committed by GitHub
parent bff9507189
commit 93a9095d99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
67 changed files with 6960 additions and 5211 deletions

13
.eslintrc.json Normal file
View file

@ -0,0 +1,13 @@
{
"env": {
"browser": false,
"es2021": true
},
"extends": "standard",
"parserOptions": {
"ecmaVersion": 2021,
"sourceType": "module"
},
"rules": {
}
}

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
text=auto

View file

@ -1,269 +1,270 @@
### To Be Released...
#### Breaking Changes
* NodeJS 14.17 is now required (from 14).
* Renamed `Counter Strike: 2D` to `CS2D` in [games.txt](games.txt) (why? see [this](https://cs2d.com/faq.php?show=misc_name#misc_name)).
* Updated `CS2D` protocol (by @ernestpasnik)
#### Other changes
* Replaced usage of deprecated `substr` with `substring`.
* Moved the library a `module`.
* CLI: Resolved incorrect error message when querying with a non-existent protocol name.
* Replaced deprecated internal `punycode` with the [punycode](https://www.npmjs.com/package/punycode) package.
* Eco (2018) - Added support (requested by @dgibbs64).
* Core Keeper (2022) - Added support (by @dgibbs64).
### 4.1.0
* Replace `compressjs` dependency by `seek-bzip` to solve some possible import issues.
* Sons Of The Forest (2023) - Added support
* Red Dead Redemption 2 - RedM (2018) - Added support
* Creativerse (2017) - Added support
* The Isle (2015) - Added support
### 4.0.7
* Updated some dependencies to solve vulnerabilities
* Fixed an issue regarding GameSpy 1 not correctly checking and parsing for numbers.
* Risk of Rain 2 (2019) - Added support
* Survive the Nights (2017) - Added support
* V Rising (2022) - Added support
* Day of Dragons (2019) - Added support
* Onset (2019) - Added support
* Don't Starve Together (2016) - Added support
* Chivalry: Medieval Warfare (2012) - Added support
* Avorion (2020) - Added support
* Black Mesa (2020) - Added support
* Ballistic Overkill (2017) - Added support
* Codename CURE (2017) - Added support
* Colony Survival (2017) - Added support
* Rising World (2014) - Added support
* BrainBread 2 (2016) - Added support
### 4.0.6
* Fixed ping returned by minecraft queries
* Added ipFamily option to query only ipv4 or only ipv6 dns records
### 4.0.5
* Fixed filtering out fake "Max Players" player on CSGO
* Removed moment dependency
### 4.0.4
* Updated dependencies
### 4.0.3
* Fixed nodejs version requirement in package.json (node 14 has been required since gamedig 4)
* Ground Breach (2018) - Added support
* Minecraft (All Versions) - Fixed character encoding for strings returned by servers using Geyser
* Barotrauma (2019) - Added support
### 4.0.2
* Counter-Strike 1.5 - Fixed support
### 4.0.1
* Rust - Fixed maxplayers >255
* dayZ - Fixed tag info not parsing when queryRules wasn't set
### 4.0.0
#### Breaking Changes
* NodeJS 14 is now required
#### Other changes
* Dependencies are updated
* Node 14 is now required due to new requirement in `got` dependency
### 3.0.9
* Fixes player info parsing issues on bf1942-based mods (Thanks cetteup)
* Adds Project Zomboid support (Thanks xhip)
* Adds Post Scriptum support (Thanks arkuar)
* Adds some more DayZ info to state.raw (Thanks podrivo)
* Updates to README regarding DayZ (Thanks podrivo)
* Improvements to DayZ mod parsing from additional more recent reverse engineering (probably still buggy)
* Fixes ping always being 0 for minecraft servers
* Adds README documentation about teamspeakQueryPort
### 3.0.8
* Fixes player array corruption on some protocols which only report player counts without names (Thanks to a-sync)
* Fixes minecraft protocol not using player list from bedrock protocol in some cases
### 3.0.7
* Fixes corrupted dayzMods when packet overflow is present
### 3.0.6
* raw.tags for valve servers is now an array rather than a string
* The special mod list for dayz servers is now parsed into raw.dayzMods is requestRules is set to true
* DayZ queue length, day and night acceleration are now parsed into raw as well
### 3.0.5
* Add support for `listenUdpPort` to specify a fixed bind port.
* Improved udp bind failure detection.
### 3.0.4
* Add support for Discord widget
### 3.0.3
* Greatly improve gamespy1 protocol, with additional error handling and xserverquery support.
### 3.0.2
* Fix player name extraction for Unreal Tournament (1999) and possibly
other gamespy1 games.
### 3.0.1
* Clarified that nodejs 12 is now required for gamedig 3
* Fixed misc player fields not going into `raw` subobject in `assettocorsa`, `fivem`, and `gamespy2`
### 3.0.0
Major Changes:
* **NodeJS 12 is now required**
* The `name` field is now guaranteed to exist on all player objects. If a player's name is unknown, the `name` will be an empty string.
* All non-`name` player fields have been moved into a `raw` sub-field. This means that, like the `raw` subobject of the parent
response, all non-`name` fields are now considered to be unstable and may be changed during minor releases of GameDig.
* "Rules" are no longer queried for `valve` protocol games by default. Many games do not respond to this query anyways (meaning we have to wait
for timeout), and its contents is often not even used since it only exists in the raw subfield. If you depend on rules,
you may pass the `requestRules: true` option to re-enable them.
* The `raw.steamappid` and `raw.gameid` fields for valve games have been consolidated into `raw.appId`.
### 2.0.28
* Added Valheim (2021)
### 2.0.27
* Reduced chance of protocol collisions between gamespy3 and minecraftbedrock
### 2.0.26
* Added support for the native minecraft bedrock protocol, since some
bedrock servers apparently do not respond to the gamespy3 protocol.
### 2.0.25
* Support challenges in A2S_INFO (upcoming change to valve protocol)
### 2.0.24
* Add Savage 2: A Tortured Soul (2008)
### 2.0.23
* Fix Conan Exiles and other games which don't respond to the valve player query
* Add givenPortOnly query option for users that require extreme optimization
### 2.0.22
* Updated dependencies
### 2.0.21
* Added Assetto Corsa (2014)
* Fixed password flag for Squad
* Added Mordhau (2019)
* Fixed player count being incorrect in minecraftvanilla protocol in some cases
* Updated dependencies
* Replaced deprecated Request http library with Got
### 2.0.20
* Fixed minecraft protocol never throwing exceptions
### 2.0.19
* Added Days of War (2017)
* Added The Forrest (2014)
* Added Just Cause 3 Multiplayer (2017)
* Added Project Reality: Battlefield 2 (2005)
* Added Quake Live (2010)
* Added Contagion (2011)
* Added Empyrion: Galactic Survival (2015)
* Added PixARK (2018)
### 2.0.16, 2.0.17, 2.0.18
* Various improvements to killing floor / unreal2 protocol
### 2.0.15
* Added Hell Let Loose
* Added Rising Storm 2: Vietnam
* Added Squad
* Fixed DNS lookup not working in some situations when dns.lookup unexpectedly returns a string
* Improved minecraft protocol for non-vanilla server implementations (bedrock, waterfall, bungeecord)
* Updated dependencies
### 2.0.14
* Node 8 compatibility fixes
### 2.0.13
* Improved logging
### 2.0.12
* Servers are now limited to 10000 players to prevent OOM
* Improvements to Starmade (2012)
* Added Atlas (2018)
### 2.0.11
* Added Acra Sim Racing
* Added Mafia 2: Online
### 2.0.10
* Added rFactor
### 2.0.9
* Added Vice City: Multiplayer
### 2.0.8
* Improve out-of-order packet handling for gamespy1 protocol
* Work-around for buggy duplicate player reporting from bf1942 servers
* Report team names rather than IDs when possible for gamespy1 protocol
### 2.0.7
* Prevent tcp socket errors from dumping straight to console
### 2.0.6
* Added support for host domains requiring Punycode encoding (special characters)
### 2.0.5
* Added support for Counter-Strike: 2D
### 2.0.4
* Added details about new 2.0 reponse fields to the README.
### 2.0.3
* Added support for Insurgency: Sandstorm
### 2.0.2
* Added support for Starsiege 2009 (starsiege)
### 2.0.1
* Updated readme games list for 2.0
* Fixed csgo default port
### 2.0.0
##### Breaking API changes
* **Node 8 is now required**
* Removed the `port_query` option. You can now pass either the server's game port **or** query port in the `port` option, and
GameDig will automatically discover the proper port to query. Passing the query port is more likely be successful in
unusual cases, as otherwise it must be automatically derived from the game port.
* Removed `callback` parameter from Gamedig.query. Only promises are now supported. If you would like to continue
using callbacks, you can use node's `util.callbackify` function to convert the method to callback format.
* Removed `query` field from response object, as it was poorly documented and unstable.
* Removed `notes` field from options / response object. Data can be passed through a standard javascript context if needed.
##### Minor Changes
* Rewrote core to use promises extensively for better error-handling. Async chains have been dramatically simplified
by using async/await across the codebase, eliminating callback chains and the 'async' dependency.
* Replaced `--output pretty` cli parameter with `--pretty`.
* You can now query from CLI using shorthand syntax: `gamedig --type <gameid> <ip>[:<port>]`
* UDP socket is only opened if needed by a query.
* Automatic query port detection -- If provided with a non-standard port, gamedig will attempt to discover if it is a
game port or query port by querying twice: once to the port provided, and once to the port including the game's query
port offset (if available).
* Added new `connect` field to the response object. This will typically include the game's `ip:port` (the port will reflect the server's
game port, even if you passed in a query port in your request). For some games, this may be a server ID or connection url
if an IP:Port is not appropriate.
* Added new `ping` field (in milliseconds) to the response object. As icmp packets are often blocked by NATs, and node has poor support
for raw sockets, this time is derived from the rtt of one of the UDP requests, or the time required to open a TCP socket
during the query.
* Improved debug logging across all parts of GameDig
* Removed global `Gamedig.debug`. `debug` is now an option on each query.
##### Protocol Changes
* Added support for games using older versions of battlefield protocol.
* Simplified detection of BC2 when using battlefield protocol.
* Fixed buildandshoot not reading player list
* Standardized all doom3 games into a single protocol, which can discover protocol discrepancies automatically.
* Standardized all gamespy2 games into a single protocol, which can discover protocol discrepancies automatically.
* Standardized all gamespy3 games into a single protocol, which can discover protocol discrepancies automatically.
* Improved valve protocol challenge key retry process
### 1.0.0
* First official release
* Node.js 6 is now required
### To Be Released...
#### Breaking Changes
* NodeJS 14.17 is now required (from 14).
* Renamed `Counter Strike: 2D` to `CS2D` in [games.txt](games.txt) (why? see [this](https://cs2d.com/faq.php?show=misc_name#misc_name)).
* Updated `CS2D` protocol (by @ernestpasnik)
#### Other changes
* Replaced usage of deprecated `substr` with `substring`.
* Moved the library a `module`.
* CLI: Resolved incorrect error message when querying with a non-existent protocol name.
* Replaced deprecated internal `punycode` with the [punycode](https://www.npmjs.com/package/punycode) package.
* Eco (2018) - Added support (requested by @dgibbs64).
* Core Keeper (2022) - Added support (by @dgibbs64).
* Added eslint which spotted some unused variables and other lints.
### 4.1.0
* Replace `compressjs` dependency by `seek-bzip` to solve some possible import issues.
* Sons Of The Forest (2023) - Added support
* Red Dead Redemption 2 - RedM (2018) - Added support
* Creativerse (2017) - Added support
* The Isle (2015) - Added support
### 4.0.7
* Updated some dependencies to solve vulnerabilities
* Fixed an issue regarding GameSpy 1 not correctly checking and parsing for numbers.
* Risk of Rain 2 (2019) - Added support
* Survive the Nights (2017) - Added support
* V Rising (2022) - Added support
* Day of Dragons (2019) - Added support
* Onset (2019) - Added support
* Don't Starve Together (2016) - Added support
* Chivalry: Medieval Warfare (2012) - Added support
* Avorion (2020) - Added support
* Black Mesa (2020) - Added support
* Ballistic Overkill (2017) - Added support
* Codename CURE (2017) - Added support
* Colony Survival (2017) - Added support
* Rising World (2014) - Added support
* BrainBread 2 (2016) - Added support
### 4.0.6
* Fixed ping returned by minecraft queries
* Added ipFamily option to query only ipv4 or only ipv6 dns records
### 4.0.5
* Fixed filtering out fake "Max Players" player on CSGO
* Removed moment dependency
### 4.0.4
* Updated dependencies
### 4.0.3
* Fixed nodejs version requirement in package.json (node 14 has been required since gamedig 4)
* Ground Breach (2018) - Added support
* Minecraft (All Versions) - Fixed character encoding for strings returned by servers using Geyser
* Barotrauma (2019) - Added support
### 4.0.2
* Counter-Strike 1.5 - Fixed support
### 4.0.1
* Rust - Fixed maxplayers >255
* dayZ - Fixed tag info not parsing when queryRules wasn't set
### 4.0.0
#### Breaking Changes
* NodeJS 14 is now required
#### Other changes
* Dependencies are updated
* Node 14 is now required due to new requirement in `got` dependency
### 3.0.9
* Fixes player info parsing issues on bf1942-based mods (Thanks cetteup)
* Adds Project Zomboid support (Thanks xhip)
* Adds Post Scriptum support (Thanks arkuar)
* Adds some more DayZ info to state.raw (Thanks podrivo)
* Updates to README regarding DayZ (Thanks podrivo)
* Improvements to DayZ mod parsing from additional more recent reverse engineering (probably still buggy)
* Fixes ping always being 0 for minecraft servers
* Adds README documentation about teamspeakQueryPort
### 3.0.8
* Fixes player array corruption on some protocols which only report player counts without names (Thanks to a-sync)
* Fixes minecraft protocol not using player list from bedrock protocol in some cases
### 3.0.7
* Fixes corrupted dayzMods when packet overflow is present
### 3.0.6
* raw.tags for valve servers is now an array rather than a string
* The special mod list for dayz servers is now parsed into raw.dayzMods is requestRules is set to true
* DayZ queue length, day and night acceleration are now parsed into raw as well
### 3.0.5
* Add support for `listenUdpPort` to specify a fixed bind port.
* Improved udp bind failure detection.
### 3.0.4
* Add support for Discord widget
### 3.0.3
* Greatly improve gamespy1 protocol, with additional error handling and xserverquery support.
### 3.0.2
* Fix player name extraction for Unreal Tournament (1999) and possibly
other gamespy1 games.
### 3.0.1
* Clarified that nodejs 12 is now required for gamedig 3
* Fixed misc player fields not going into `raw` subobject in `assettocorsa`, `fivem`, and `gamespy2`
### 3.0.0
Major Changes:
* **NodeJS 12 is now required**
* The `name` field is now guaranteed to exist on all player objects. If a player's name is unknown, the `name` will be an empty string.
* All non-`name` player fields have been moved into a `raw` sub-field. This means that, like the `raw` subobject of the parent
response, all non-`name` fields are now considered to be unstable and may be changed during minor releases of GameDig.
* "Rules" are no longer queried for `valve` protocol games by default. Many games do not respond to this query anyways (meaning we have to wait
for timeout), and its contents is often not even used since it only exists in the raw subfield. If you depend on rules,
you may pass the `requestRules: true` option to re-enable them.
* The `raw.steamappid` and `raw.gameid` fields for valve games have been consolidated into `raw.appId`.
### 2.0.28
* Added Valheim (2021)
### 2.0.27
* Reduced chance of protocol collisions between gamespy3 and minecraftbedrock
### 2.0.26
* Added support for the native minecraft bedrock protocol, since some
bedrock servers apparently do not respond to the gamespy3 protocol.
### 2.0.25
* Support challenges in A2S_INFO (upcoming change to valve protocol)
### 2.0.24
* Add Savage 2: A Tortured Soul (2008)
### 2.0.23
* Fix Conan Exiles and other games which don't respond to the valve player query
* Add givenPortOnly query option for users that require extreme optimization
### 2.0.22
* Updated dependencies
### 2.0.21
* Added Assetto Corsa (2014)
* Fixed password flag for Squad
* Added Mordhau (2019)
* Fixed player count being incorrect in minecraftvanilla protocol in some cases
* Updated dependencies
* Replaced deprecated Request http library with Got
### 2.0.20
* Fixed minecraft protocol never throwing exceptions
### 2.0.19
* Added Days of War (2017)
* Added The Forrest (2014)
* Added Just Cause 3 Multiplayer (2017)
* Added Project Reality: Battlefield 2 (2005)
* Added Quake Live (2010)
* Added Contagion (2011)
* Added Empyrion: Galactic Survival (2015)
* Added PixARK (2018)
### 2.0.16, 2.0.17, 2.0.18
* Various improvements to killing floor / unreal2 protocol
### 2.0.15
* Added Hell Let Loose
* Added Rising Storm 2: Vietnam
* Added Squad
* Fixed DNS lookup not working in some situations when dns.lookup unexpectedly returns a string
* Improved minecraft protocol for non-vanilla server implementations (bedrock, waterfall, bungeecord)
* Updated dependencies
### 2.0.14
* Node 8 compatibility fixes
### 2.0.13
* Improved logging
### 2.0.12
* Servers are now limited to 10000 players to prevent OOM
* Improvements to Starmade (2012)
* Added Atlas (2018)
### 2.0.11
* Added Acra Sim Racing
* Added Mafia 2: Online
### 2.0.10
* Added rFactor
### 2.0.9
* Added Vice City: Multiplayer
### 2.0.8
* Improve out-of-order packet handling for gamespy1 protocol
* Work-around for buggy duplicate player reporting from bf1942 servers
* Report team names rather than IDs when possible for gamespy1 protocol
### 2.0.7
* Prevent tcp socket errors from dumping straight to console
### 2.0.6
* Added support for host domains requiring Punycode encoding (special characters)
### 2.0.5
* Added support for Counter-Strike: 2D
### 2.0.4
* Added details about new 2.0 reponse fields to the README.
### 2.0.3
* Added support for Insurgency: Sandstorm
### 2.0.2
* Added support for Starsiege 2009 (starsiege)
### 2.0.1
* Updated readme games list for 2.0
* Fixed csgo default port
### 2.0.0
##### Breaking API changes
* **Node 8 is now required**
* Removed the `port_query` option. You can now pass either the server's game port **or** query port in the `port` option, and
GameDig will automatically discover the proper port to query. Passing the query port is more likely be successful in
unusual cases, as otherwise it must be automatically derived from the game port.
* Removed `callback` parameter from Gamedig.query. Only promises are now supported. If you would like to continue
using callbacks, you can use node's `util.callbackify` function to convert the method to callback format.
* Removed `query` field from response object, as it was poorly documented and unstable.
* Removed `notes` field from options / response object. Data can be passed through a standard javascript context if needed.
##### Minor Changes
* Rewrote core to use promises extensively for better error-handling. Async chains have been dramatically simplified
by using async/await across the codebase, eliminating callback chains and the 'async' dependency.
* Replaced `--output pretty` cli parameter with `--pretty`.
* You can now query from CLI using shorthand syntax: `gamedig --type <gameid> <ip>[:<port>]`
* UDP socket is only opened if needed by a query.
* Automatic query port detection -- If provided with a non-standard port, gamedig will attempt to discover if it is a
game port or query port by querying twice: once to the port provided, and once to the port including the game's query
port offset (if available).
* Added new `connect` field to the response object. This will typically include the game's `ip:port` (the port will reflect the server's
game port, even if you passed in a query port in your request). For some games, this may be a server ID or connection url
if an IP:Port is not appropriate.
* Added new `ping` field (in milliseconds) to the response object. As icmp packets are often blocked by NATs, and node has poor support
for raw sockets, this time is derived from the rtt of one of the UDP requests, or the time required to open a TCP socket
during the query.
* Improved debug logging across all parts of GameDig
* Removed global `Gamedig.debug`. `debug` is now an option on each query.
##### Protocol Changes
* Added support for games using older versions of battlefield protocol.
* Simplified detection of BC2 when using battlefield protocol.
* Fixed buildandshoot not reading player list
* Standardized all doom3 games into a single protocol, which can discover protocol discrepancies automatically.
* Standardized all gamespy2 games into a single protocol, which can discover protocol discrepancies automatically.
* Standardized all gamespy3 games into a single protocol, which can discover protocol discrepancies automatically.
* Improved valve protocol challenge key retry process
### 1.0.0
* First official release
* Node.js 6 is now required

View file

@ -1,68 +1,67 @@
#!/usr/bin/env node
import Minimist from 'minimist';
import GameDig from './../lib/index.js';
const argv = Minimist(process.argv.slice(2), {
boolean: ['pretty','debug','givenPortOnly','requestRules'],
string: ['guildId','listenUdpPort','ipFamily']
});
const debug = argv.debug;
delete argv.debug;
const pretty = !!argv.pretty || debug;
delete argv.pretty;
const givenPortOnly = argv.givenPortOnly;
delete argv.givenPortOnly;
let options = {};
for(const key of Object.keys(argv)) {
const value = argv[key];
if(key === '_' || key.charAt(0) === '$')
continue;
options[key] = value;
}
if (argv._.length >= 1) {
const target = argv._[0];
const split = target.split(':');
options.host = split[0];
if (split.length >= 2) {
options.port = split[1];
}
}
if (debug) {
options.debug = true;
}
if (givenPortOnly) {
options.givenPortOnly = true;
}
const printOnPretty = (object) => {
if(pretty) {
console.log(JSON.stringify(object,null,' '));
} else {
console.log(JSON.stringify(object));
}
}
const gamedig = new GameDig(options);
gamedig.query(options)
.then(printOnPretty)
.catch((error) => {
if (debug) {
if (error instanceof Error) {
console.log(error.stack);
} else {
console.log(error);
}
} else {
if (error instanceof Error) {
error = error.message;
}
printOnPretty({error: error});
}
});
#!/usr/bin/env node
import Minimist from 'minimist'
import GameDig from './../lib/index.js'
const argv = Minimist(process.argv.slice(2), {
boolean: ['pretty', 'debug', 'givenPortOnly', 'requestRules'],
string: ['guildId', 'listenUdpPort', 'ipFamily']
})
const debug = argv.debug
delete argv.debug
const pretty = !!argv.pretty || debug
delete argv.pretty
const givenPortOnly = argv.givenPortOnly
delete argv.givenPortOnly
const options = {}
for (const key of Object.keys(argv)) {
const value = argv[key]
if (key === '_' || key.charAt(0) === '$') { continue }
options[key] = value
}
if (argv._.length >= 1) {
const target = argv._[0]
const split = target.split(':')
options.host = split[0]
if (split.length >= 2) {
options.port = split[1]
}
}
if (debug) {
options.debug = true
}
if (givenPortOnly) {
options.givenPortOnly = true
}
const printOnPretty = (object) => {
if (pretty) {
console.log(JSON.stringify(object, null, ' '))
} else {
console.log(JSON.stringify(object))
}
}
const gamedig = new GameDig(options)
gamedig.query(options)
.then(printOnPretty)
.catch((error) => {
if (debug) {
if (error instanceof Error) {
console.log(error.stack)
} else {
console.log(error)
}
} else {
if (error instanceof Error) {
error = error.message
}
printOnPretty({ error })
}
})

View file

@ -1,21 +1,21 @@
#!/usr/bin/env node
import * as fs from 'fs';
import GameResolver from "../lib/GameResolver";
import * as fs from 'fs'
import GameResolver from '../lib/GameResolver'
const gameResolver = new GameResolver();
const gameResolver = new GameResolver()
const generated = gameResolver.printReadme();
const generated = gameResolver.printReadme()
const readmeFilename = __dirname+'/../README.md';
const readme = fs.readFileSync(readmeFilename, {encoding:'utf8'});
const readmeFilename = __dirname + '/../README.md'
const readme = fs.readFileSync(readmeFilename, { encoding: 'utf8' })
const marker_top = '<!--- BEGIN GENERATED GAMES -->';
const marker_bottom = '<!--- END GENERATED GAMES -->';
const marker_top = '<!--- BEGIN GENERATED GAMES -->'
const marker_bottom = '<!--- END GENERATED GAMES -->'
let start = readme.indexOf(marker_top);
start += marker_top.length;
const end = readme.indexOf(marker_bottom);
let start = readme.indexOf(marker_top)
start += marker_top.length
const end = readme.indexOf(marker_bottom)
const updated = readme.substring(0,start)+"\n\n"+generated+"\n"+readme.substring(end);
fs.writeFileSync(readmeFilename, updated);
const updated = readme.substring(0, start) + '\n\n' + generated + '\n' + readme.substring(end)
fs.writeFileSync(readmeFilename, updated)

View file

@ -1,78 +1,78 @@
import * as dns from 'dns';
import punycode from "punycode/punycode.js";
import { promisify } from "util";
const dnsLookupAsync = promisify(dns.lookup);
const dnsResolveAsync = promisify(dns.resolve);
export default class DnsResolver {
/**
* @param {Logger} logger
*/
constructor(logger) {
this.logger = logger;
}
isIp(host) {
return !!host.match(/\d+\.\d+\.\d+\.\d+/);
}
/**
* Response port will only be present if srv record was involved.
* @param {string} host
* @param {number} ipFamily
* @param {string=} srvRecordPrefix
* @returns {Promise<{address:string, port:number=}>}
*/
async resolve(host, ipFamily, srvRecordPrefix) {
this.logger.debug("DNS Lookup: " + host);
if(this.isIp(host)) {
this.logger.debug("Raw IP Address: " + host);
return {address: host};
}
const asciiForm = punycode.toASCII(host);
if (asciiForm !== host) {
this.logger.debug("Encoded punycode: " + host + " -> " + asciiForm);
host = asciiForm;
}
if (srvRecordPrefix) {
this.logger.debug("SRV Resolve: " + srvRecordPrefix + '.' + host);
let records;
try {
records = await dnsResolveAsync(srvRecordPrefix + '.' + host, 'SRV');
if (records.length >= 1) {
this.logger.debug("Found SRV Records: ", records);
const record = records[0];
const srvPort = record.port;
const srvHost = record.name;
if (srvHost === host) {
throw new Error('Loop in DNS SRV records');
}
return {
port: srvPort,
...await this.resolve(srvHost, ipFamily, srvRecordPrefix)
};
}
this.logger.debug("No SRV Record");
} catch (e) {
this.logger.debug(e);
}
}
this.logger.debug("Standard Resolve: " + host);
const dnsResult = await dnsLookupAsync(host, ipFamily);
// For some reason, this sometimes returns a string address rather than an object.
// I haven't been able to reproduce, but it's been reported on the issue tracker.
let address;
if (typeof dnsResult === 'string') {
address = dnsResult;
} else {
address = dnsResult.address;
}
this.logger.debug("Found address: " + address);
return {address: address};
}
}
import * as dns from 'dns'
import punycode from 'punycode/punycode.js'
import { promisify } from 'util'
const dnsLookupAsync = promisify(dns.lookup)
const dnsResolveAsync = promisify(dns.resolve)
export default class DnsResolver {
/**
* @param {Logger} logger
*/
constructor (logger) {
this.logger = logger
}
isIp (host) {
return !!host.match(/\d+\.\d+\.\d+\.\d+/)
}
/**
* Response port will only be present if srv record was involved.
* @param {string} host
* @param {number} ipFamily
* @param {string=} srvRecordPrefix
* @returns {Promise<{address:string, port:number=}>}
*/
async resolve (host, ipFamily, srvRecordPrefix) {
this.logger.debug('DNS Lookup: ' + host)
if (this.isIp(host)) {
this.logger.debug('Raw IP Address: ' + host)
return { address: host }
}
const asciiForm = punycode.toASCII(host)
if (asciiForm !== host) {
this.logger.debug('Encoded punycode: ' + host + ' -> ' + asciiForm)
host = asciiForm
}
if (srvRecordPrefix) {
this.logger.debug('SRV Resolve: ' + srvRecordPrefix + '.' + host)
let records
try {
records = await dnsResolveAsync(srvRecordPrefix + '.' + host, 'SRV')
if (records.length >= 1) {
this.logger.debug('Found SRV Records: ', records)
const record = records[0]
const srvPort = record.port
const srvHost = record.name
if (srvHost === host) {
throw new Error('Loop in DNS SRV records')
}
return {
port: srvPort,
...await this.resolve(srvHost, ipFamily, srvRecordPrefix)
}
}
this.logger.debug('No SRV Record')
} catch (e) {
this.logger.debug(e)
}
}
this.logger.debug('Standard Resolve: ' + host)
const dnsResult = await dnsLookupAsync(host, ipFamily)
// For some reason, this sometimes returns a string address rather than an object.
// I haven't been able to reproduce, but it's been reported on the issue tracker.
let address
if (typeof dnsResult === 'string') {
address = dnsResult
} else {
address = dnsResult.address
}
this.logger.debug('Found address: ' + address)
return { address }
}
}

View file

@ -1,122 +1,114 @@
import * as path from 'path';
import { fileURLToPath } from "url";
import * as fs from 'fs';
export default class GameResolver {
constructor() {
const loaded = this._readGames();
this.gamesByKey = loaded.gamesByKey;
this.games = loaded.games;
}
lookup(type) {
if(!type)
throw Error('No game specified');
if(type.substring(0,9) === 'protocol-') {
return {
protocol: type.substring(9)
};
}
const game = this.gamesByKey.get(type);
if(!game)
throw Error('Invalid game: '+type);
return game.options;
}
printReadme() {
let out = '';
out += '| GameDig Type ID | Name | See Also\n';
out += '|---|---|---\n';
const sorted = this.games
.filter(game => game.pretty)
.sort((a,b) => {
return a.pretty.localeCompare(b.pretty);
});
for(const game of sorted) {
let keysOut = game.keys.map(key => '`'+key+'`').join('<br>');
out += "| " + keysOut.padEnd(10, " ") + " "
+ "| " + game.pretty;
let notes = [];
if(game.extra.doc_notes) {
notes.push("[Notes](#" + game.extra.doc_notes + ")");
}
if(game.options.protocol === 'valve') {
notes.push('[Valve Protocol](#valve)');
}
if(notes.length) {
out += " | " + notes.join(', ');
}
out += "\n";
}
return out;
}
_readGames() {
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const gamesFile = path.normalize(__dirname+'/../games.txt');
const lines = fs.readFileSync(gamesFile,'utf8').split('\n');
const gamesByKey = new Map();
const games = [];
for (let line of lines) {
// strip comments
const comment = line.indexOf('#');
if(comment !== -1) line = line.substring(0,comment);
line = line.trim();
if(!line) continue;
const split = line.split('|');
const keys = split[0].trim().split(',');
const name = split[1].trim();
const options = this._parseList(split[3]);
options.protocol = split[2].trim();
const extra = this._parseList(split[4]);
const game = {
keys: keys,
pretty: name,
options: options,
extra: extra
};
for (const key of keys) {
gamesByKey.set(key, game);
}
games.push(game);
}
return { gamesByKey, games };
}
_parseList(str) {
if(!str)
return {};
let out = {};
for (const one of str.split(',')) {
const equals = one.indexOf('=');
const key = equals === -1 ? one : one.substring(0, equals);
/** @type {string|number|boolean} */
let value = equals === -1 ? '' : one.substring(equals + 1);
if(value === 'true' || value === '')
value = true;
else if(value === 'false')
value = false;
else if(!isNaN(parseInt(value)))
value = parseInt(value);
out[key] = value;
}
return out;
}
}
import * as path from 'path'
import { fileURLToPath } from 'url'
import * as fs from 'fs'
export default class GameResolver {
constructor () {
const loaded = this._readGames()
this.gamesByKey = loaded.gamesByKey
this.games = loaded.games
}
lookup (type) {
if (!type) { throw Error('No game specified') }
if (type.substring(0, 9) === 'protocol-') {
return {
protocol: type.substring(9)
}
}
const game = this.gamesByKey.get(type)
if (!game) { throw Error('Invalid game: ' + type) }
return game.options
}
printReadme () {
let out = ''
out += '| GameDig Type ID | Name | See Also\n'
out += '|---|---|---\n'
const sorted = this.games
.filter(game => game.pretty)
.sort((a, b) => {
return a.pretty.localeCompare(b.pretty)
})
for (const game of sorted) {
const keysOut = game.keys.map(key => '`' + key + '`').join('<br>')
out += '| ' + keysOut.padEnd(10, ' ') + ' ' +
'| ' + game.pretty
const notes = []
if (game.extra.doc_notes) {
notes.push('[Notes](#' + game.extra.doc_notes + ')')
}
if (game.options.protocol === 'valve') {
notes.push('[Valve Protocol](#valve)')
}
if (notes.length) {
out += ' | ' + notes.join(', ')
}
out += '\n'
}
return out
}
_readGames () {
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const gamesFile = path.normalize(__dirname + '/../games.txt')
const lines = fs.readFileSync(gamesFile, 'utf8').split('\n')
const gamesByKey = new Map()
const games = []
for (let line of lines) {
// strip comments
const comment = line.indexOf('#')
if (comment !== -1) line = line.substring(0, comment)
line = line.trim()
if (!line) continue
const split = line.split('|')
const keys = split[0].trim().split(',')
const name = split[1].trim()
const options = this._parseList(split[3])
options.protocol = split[2].trim()
const extra = this._parseList(split[4])
const game = {
keys,
pretty: name,
options,
extra
}
for (const key of keys) {
gamesByKey.set(key, game)
}
games.push(game)
}
return { gamesByKey, games }
}
_parseList (str) {
if (!str) { return {} }
const out = {}
for (const one of str.split(',')) {
const equals = one.indexOf('=')
const key = equals === -1 ? one : one.substring(0, equals)
/** @type {string|number|boolean} */
let value = equals === -1 ? '' : one.substring(equals + 1)
if (value === 'true' || value === '') { value = true } else if (value === 'false') { value = false } else if (!isNaN(parseInt(value))) { value = parseInt(value) }
out[key] = value
}
return out
}
}

View file

@ -1,69 +1,69 @@
import { createSocket } from "dgram";
import { debugDump } from "./HexUtil.js";
import { promisify } from "util";
import Logger from "./Logger.js";
export default class GlobalUdpSocket {
constructor({port}) {
this.socket = null;
this.callbacks = new Set();
this.debuggingCallbacks = new Set();
this.logger = new Logger();
this.port = port;
}
async _getSocket() {
if (!this.socket) {
const udpSocket = createSocket({
type: 'udp4',
reuseAddr: true
});
udpSocket.unref();
udpSocket.on('message', (buffer, rinfo) => {
const fromAddress = rinfo.address;
const fromPort = rinfo.port;
this.logger.debug(log => {
log(fromAddress + ':' + fromPort + " <--UDP(" + this.port + ")");
log(debugDump(buffer));
});
for (const callback of this.callbacks) {
callback(fromAddress, fromPort, buffer);
}
});
udpSocket.on('error', e => {
this.logger.debug("UDP ERROR:", e);
});
await promisify(udpSocket.bind).bind(udpSocket)(this.port);
this.port = udpSocket.address().port;
this.socket = udpSocket;
}
return this.socket;
}
async send(buffer, address, port, debug) {
const socket = await this._getSocket();
if (debug) {
this.logger._print(log => {
log(address + ':' + port + " UDP(" + this.port + ")-->");
log(debugDump(buffer));
});
}
await promisify(socket.send).bind(socket)(buffer,0,buffer.length,port,address);
}
addCallback(callback, debug) {
this.callbacks.add(callback);
if (debug) {
this.debuggingCallbacks.add(callback);
this.logger.debugEnabled = true;
}
}
removeCallback(callback) {
this.callbacks.delete(callback);
this.debuggingCallbacks.delete(callback);
this.logger.debugEnabled = this.debuggingCallbacks.size > 0;
}
}
import { createSocket } from 'dgram'
import { debugDump } from './HexUtil.js'
import { promisify } from 'util'
import Logger from './Logger.js'
export default class GlobalUdpSocket {
constructor ({ port }) {
this.socket = null
this.callbacks = new Set()
this.debuggingCallbacks = new Set()
this.logger = new Logger()
this.port = port
}
async _getSocket () {
if (!this.socket) {
const udpSocket = createSocket({
type: 'udp4',
reuseAddr: true
})
udpSocket.unref()
udpSocket.on('message', (buffer, rinfo) => {
const fromAddress = rinfo.address
const fromPort = rinfo.port
this.logger.debug(log => {
log(fromAddress + ':' + fromPort + ' <--UDP(' + this.port + ')')
log(debugDump(buffer))
})
for (const callback of this.callbacks) {
callback(fromAddress, fromPort, buffer)
}
})
udpSocket.on('error', e => {
this.logger.debug('UDP ERROR:', e)
})
await promisify(udpSocket.bind).bind(udpSocket)(this.port)
this.port = udpSocket.address().port
this.socket = udpSocket
}
return this.socket
}
async send (buffer, address, port, debug) {
const socket = await this._getSocket()
if (debug) {
this.logger._print(log => {
log(address + ':' + port + ' UDP(' + this.port + ')-->')
log(debugDump(buffer))
})
}
await promisify(socket.send).bind(socket)(buffer, 0, buffer.length, port, address)
}
addCallback (callback, debug) {
this.callbacks.add(callback)
if (debug) {
this.debuggingCallbacks.add(callback)
this.logger.debugEnabled = true
}
}
removeCallback (callback) {
this.callbacks.delete(callback)
this.debuggingCallbacks.delete(callback)
this.logger.debugEnabled = this.debuggingCallbacks.size > 0
}
}

View file

@ -1,21 +1,20 @@
/** @param {Buffer} buffer */
export const debugDump = (buffer) => {
let hexLine = '';
let chrLine = '';
let out = '';
out += "Buffer length: " + buffer.length + " bytes\n";
for(let i = 0; i < buffer.length; i++) {
const sliced = buffer.slice(i,i+1);
hexLine += sliced.toString('hex')+' ';
let chr = sliced.toString();
if(chr < ' ' || chr > '~') chr = ' ';
chrLine += chr+' ';
if(hexLine.length > 60 || i === buffer.length - 1) {
out += hexLine + '\n';
out += chrLine + '\n';
hexLine = chrLine = '';
}
}
return out;
}
/** @param {Buffer} buffer */
export const debugDump = (buffer) => {
let hexLine = ''
let chrLine = ''
let out = ''
out += 'Buffer length: ' + buffer.length + ' bytes\n'
for (let i = 0; i < buffer.length; i++) {
const sliced = buffer.slice(i, i + 1)
hexLine += sliced.toString('hex') + ' '
let chr = sliced.toString()
if (chr < ' ' || chr > '~') chr = ' '
chrLine += chr + ' '
if (hexLine.length > 60 || i === buffer.length - 1) {
out += hexLine + '\n'
out += chrLine + '\n'
hexLine = chrLine = ''
}
}
return out
}

View file

@ -1,44 +1,44 @@
import {debugDump} from './HexUtil.js';
export default class Logger {
constructor() {
this.debugEnabled = false;
this.prefix = '';
}
debug(...args) {
if (!this.debugEnabled) return;
this._print(...args);
}
_print(...args) {
try {
const strings = this._convertArgsToStrings(...args);
if (strings.length) {
if (this.prefix) {
strings.unshift(this.prefix);
}
console.log(...strings);
}
} catch(e) {
console.log("Error while logging: " + e);
}
}
_convertArgsToStrings(...args) {
const out = [];
for (const arg of args) {
if (arg instanceof Error) {
out.push(arg.stack);
} else if (arg instanceof Buffer) {
out.push(debugDump(arg));
} else if (typeof arg == 'function') {
const result = arg.call(undefined, (...args) => this._print(...args));
if (result !== undefined) out.push(...this._convertArgsToStrings(result));
} else {
out.push(arg);
}
}
return out;
}
}
import { debugDump } from './HexUtil.js'
export default class Logger {
constructor () {
this.debugEnabled = false
this.prefix = ''
}
debug (...args) {
if (!this.debugEnabled) return
this._print(...args)
}
_print (...args) {
try {
const strings = this._convertArgsToStrings(...args)
if (strings.length) {
if (this.prefix) {
strings.unshift(this.prefix)
}
console.log(...strings)
}
} catch (e) {
console.log('Error while logging: ' + e)
}
}
_convertArgsToStrings (...args) {
const out = []
for (const arg of args) {
if (arg instanceof Error) {
out.push(arg.stack)
} else if (arg instanceof Buffer) {
out.push(debugDump(arg))
} else if (typeof arg === 'function') {
const result = arg.call(undefined, (...args) => this._print(...args))
if (result !== undefined) out.push(...this._convertArgsToStrings(result))
} else {
out.push(arg)
}
}
return out
}
}

View file

@ -1,18 +1,18 @@
export default class Promises {
static createTimeout(timeoutMs, timeoutMsg) {
let cancel = null;
const wrapped = new Promise((res, rej) => {
const timeout = setTimeout(
() => {
rej(new Error(timeoutMsg + " - Timed out after " + timeoutMs + "ms"));
},
timeoutMs
);
cancel = () => {
clearTimeout(timeout);
};
});
wrapped.cancel = cancel;
return wrapped;
}
}
export default class Promises {
static createTimeout (timeoutMs, timeoutMsg) {
let cancel = null
const wrapped = new Promise((resolve, reject) => {
const timeout = setTimeout(
() => {
reject(new Error(timeoutMsg + ' - Timed out after ' + timeoutMs + 'ms'))
},
timeoutMs
)
cancel = () => {
clearTimeout(timeout)
}
})
wrapped.cancel = cancel
return wrapped
}
}

View file

@ -1,8 +1,7 @@
import * as Protocols from '../protocols/index.js'
export const getProtocol = (protocolId) => {
if(!(protocolId in Protocols))
throw Error('Protocol definition file missing: ' + protocolId);
return new Protocols[protocolId];
}
import * as Protocols from '../protocols/index.js'
export const getProtocol = (protocolId) => {
if (!(protocolId in Protocols)) { throw Error('Protocol definition file missing: ' + protocolId) }
return new Protocols[protocolId]()
}

View file

@ -1,97 +1,95 @@
import GameResolver from "./GameResolver.js";
import {getProtocol} from './ProtocolResolver.js';
import GlobalUdpSocket from "./GlobalUdpSocket.js";
const defaultOptions = {
socketTimeout: 2000,
attemptTimeout: 10000,
maxAttempts: 1,
ipFamily: 0
};
export default class QueryRunner {
constructor(runnerOpts = {}) {
this.udpSocket = new GlobalUdpSocket({
port: runnerOpts.listenUdpPort
});
this.gameResolver = new GameResolver();
}
async run(userOptions) {
for (const key of Object.keys(userOptions)) {
const value = userOptions[key];
if (['port', 'ipFamily'].includes(key)) {
userOptions[key] = parseInt(value);
}
}
const {
port_query: gameQueryPort,
port_query_offset: gameQueryPortOffset,
...gameOptions
} = this.gameResolver.lookup(userOptions.type);
let attempts = [];
const optionsCollection = {
...defaultOptions,
...gameOptions,
...userOptions
};
const addAttemptWithPort = port => {
attempts.push({
...optionsCollection,
port
});
}
if (userOptions.port) {
if(!userOptions.givenPortOnly) {
if (gameQueryPortOffset)
addAttemptWithPort(userOptions.port + gameQueryPortOffset);
if (userOptions.port === gameOptions.port && gameQueryPort)
addAttemptWithPort(gameQueryPort);
}
attempts.push(optionsCollection);
} else if (gameQueryPort) {
addAttemptWithPort(gameQueryPort);
} else if (gameOptions.port) {
addAttemptWithPort(gameOptions.port + (gameQueryPortOffset || 0));
} else {
// Hopefully the request doesn't need a port. If it does, it'll fail when making the request.
attempts.push(optionsCollection);
}
const numRetries = userOptions.maxAttempts || gameOptions.maxAttempts || defaultOptions.maxAttempts;
let attemptNum = 0;
const errors = [];
for (const attempt of attempts) {
for (let retry = 0; retry < numRetries; retry++) {
attemptNum++;
try {
return await this._attempt(attempt);
} catch (e) {
e.stack = 'Attempt #' + attemptNum + ' - Port=' + attempt.port + ' Retry=' + (retry) + ':\n' + e.stack;
errors.push(e);
}
}
}
const err = new Error('Failed all ' + errors.length + ' attempts');
for (const e of errors) {
err.stack += '\n' + e.stack;
}
throw err;
}
async _attempt(options) {
const core = getProtocol(options.protocol);
core.options = options;
core.udpSocket = this.udpSocket;
return await core.runOnceSafe();
}
}
import GameResolver from './GameResolver.js'
import { getProtocol } from './ProtocolResolver.js'
import GlobalUdpSocket from './GlobalUdpSocket.js'
const defaultOptions = {
socketTimeout: 2000,
attemptTimeout: 10000,
maxAttempts: 1,
ipFamily: 0
}
export default class QueryRunner {
constructor (runnerOpts = {}) {
this.udpSocket = new GlobalUdpSocket({
port: runnerOpts.listenUdpPort
})
this.gameResolver = new GameResolver()
}
async run (userOptions) {
for (const key of Object.keys(userOptions)) {
const value = userOptions[key]
if (['port', 'ipFamily'].includes(key)) {
userOptions[key] = parseInt(value)
}
}
const {
port_query: gameQueryPort,
port_query_offset: gameQueryPortOffset,
...gameOptions
} = this.gameResolver.lookup(userOptions.type)
const attempts = []
const optionsCollection = {
...defaultOptions,
...gameOptions,
...userOptions
}
const addAttemptWithPort = port => {
attempts.push({
...optionsCollection,
port
})
}
if (userOptions.port) {
if (!userOptions.givenPortOnly) {
if (gameQueryPortOffset) { addAttemptWithPort(userOptions.port + gameQueryPortOffset) }
if (userOptions.port === gameOptions.port && gameQueryPort) { addAttemptWithPort(gameQueryPort) }
}
attempts.push(optionsCollection)
} else if (gameQueryPort) {
addAttemptWithPort(gameQueryPort)
} else if (gameOptions.port) {
addAttemptWithPort(gameOptions.port + (gameQueryPortOffset || 0))
} else {
// Hopefully the request doesn't need a port. If it does, it'll fail when making the request.
attempts.push(optionsCollection)
}
const numRetries = userOptions.maxAttempts || gameOptions.maxAttempts || defaultOptions.maxAttempts
let attemptNum = 0
const errors = []
for (const attempt of attempts) {
for (let retry = 0; retry < numRetries; retry++) {
attemptNum++
try {
return await this._attempt(attempt)
} catch (e) {
e.stack = 'Attempt #' + attemptNum + ' - Port=' + attempt.port + ' Retry=' + (retry) + ':\n' + e.stack
errors.push(e)
}
}
}
const err = new Error('Failed all ' + errors.length + ' attempts')
for (const e of errors) {
err.stack += '\n' + e.stack
}
throw err
}
async _attempt (options) {
const core = getProtocol(options.protocol)
core.options = options
core.udpSocket = this.udpSocket
return await core.runOnceSafe()
}
}

View file

@ -1,43 +1,42 @@
export class Player {
name = '';
raw = {};
constructor(data) {
if (typeof data === 'string') {
this.name = data;
} else {
const {name, ...raw} = data;
if (name) this.name = name;
if (raw) this.raw = raw;
}
}
}
export class Players extends Array {
setNum(num) {
// If the server specified some ridiculous number of players (billions), we don't want to
// run out of ram allocating these objects.
num = Math.min(num, 10000);
while(this.length < num) {
this.push({});
}
}
push(data) {
super.push(new Player(data));
}
}
export class Results {
name = '';
map = '';
password = false;
raw = {};
maxplayers = 0;
players = new Players();
bots = new Players();
}
export class Player {
name = ''
raw = {}
constructor (data) {
if (typeof data === 'string') {
this.name = data
} else {
const { name, ...raw } = data
if (name) this.name = name
if (raw) this.raw = raw
}
}
}
export class Players extends Array {
setNum (num) {
// If the server specified some ridiculous number of players (billions), we don't want to
// run out of ram allocating these objects.
num = Math.min(num, 10000)
while (this.length < num) {
this.push({})
}
}
push (data) {
super.push(new Player(data))
}
}
export class Results {
name = ''
map = ''
password = false
raw = {}
maxplayers = 0
players = new Players()
bots = new Players()
}

View file

@ -1,24 +1,23 @@
import QueryRunner from './QueryRunner.js';
let singleton = null;
export default class Gamedig {
constructor(runnerOpts) {
this.queryRunner = new QueryRunner(runnerOpts);
}
async query(userOptions) {
return await this.queryRunner.run(userOptions);
}
static getInstance() {
if (!singleton)
singleton = new Gamedig();
return singleton;
}
static async query(...args) {
return await Gamedig.getInstance().query(...args);
}
}
import QueryRunner from './QueryRunner.js'
let singleton = null
export default class Gamedig {
constructor (runnerOpts) {
this.queryRunner = new QueryRunner(runnerOpts)
}
async query (userOptions) {
return await this.queryRunner.run(userOptions)
}
static getInstance () {
if (!singleton) { singleton = new Gamedig() }
return singleton
}
static async query (...args) {
return await Gamedig.getInstance().query(...args)
}
}

View file

@ -1,172 +1,172 @@
import Iconv from "iconv-lite";
import Long from 'long';
import {Buffer} from "buffer";
import Varint from 'varint';
function readUInt64BE(buffer,offset) {
const high = buffer.readUInt32BE(offset);
const low = buffer.readUInt32BE(offset+4);
return new Long(low,high,true);
}
function readUInt64LE(buffer,offset) {
const low = buffer.readUInt32LE(offset);
const high = buffer.readUInt32LE(offset+4);
return new Long(low,high,true);
}
export default class Reader {
/**
* @param {Core} query
* @param {Buffer} buffer
**/
constructor(query,buffer) {
this.defaultEncoding = query.options.encoding || query.encoding;
this.defaultDelimiter = query.delimiter;
this.defaultByteOrder = query.byteorder;
this.buffer = buffer;
this.i = 0;
}
setOffset(offset) {
this.i = offset;
}
offset() {
return this.i;
}
skip(i) {
this.i += i;
}
pascalString(bytesForSize, adjustment=0) {
const length = this.uint(bytesForSize) + adjustment;
return this.string(length);
}
string(arg) {
let encoding = this.defaultEncoding;
let length = null;
let delimiter = this.defaultDelimiter;
if(typeof arg === 'string') delimiter = arg;
else if(typeof arg === 'number') length = arg;
else if(typeof arg === 'object') {
if ('encoding' in arg) encoding = arg.encoding;
if ('length' in arg) length = arg.length;
if ('delimiter' in arg) delimiter = arg.delimiter;
}
if(encoding === 'latin1') encoding = 'win1252';
const start = this.i;
let end = start;
if(length === null) {
// terminated by the delimiter
let delim = delimiter;
if (typeof delim === 'string') delim = delim.charCodeAt(0);
while (true) {
if (end >= this.buffer.length) {
end = this.buffer.length;
break;
}
if (this.buffer.readUInt8(end) === delim) break;
end++;
}
this.i = end + 1;
} else if (length <= 0) {
return '';
} else {
end = start+length;
if(end >= this.buffer.length) {
end = this.buffer.length;
}
this.i = end;
}
const slice = this.buffer.slice(start, end);
const enc = encoding;
if(enc === 'utf8' || enc === 'ucs2' || enc === 'binary') {
return slice.toString(enc);
} else {
return Iconv.decode(slice,enc);
}
}
int(bytes) {
let r = 0;
if(this.remaining() >= bytes) {
if(this.defaultByteOrder === 'be') {
if(bytes === 1) r = this.buffer.readInt8(this.i);
else if(bytes === 2) r = this.buffer.readInt16BE(this.i);
else if(bytes === 4) r = this.buffer.readInt32BE(this.i);
} else {
if(bytes === 1) r = this.buffer.readInt8(this.i);
else if(bytes === 2) r = this.buffer.readInt16LE(this.i);
else if(bytes === 4) r = this.buffer.readInt32LE(this.i);
}
}
this.i += bytes;
return r;
}
/** @returns {number} */
uint(bytes) {
let r = 0;
if(this.remaining() >= bytes) {
if(this.defaultByteOrder === 'be') {
if(bytes === 1) r = this.buffer.readUInt8(this.i);
else if(bytes === 2) r = this.buffer.readUInt16BE(this.i);
else if(bytes === 4) r = this.buffer.readUInt32BE(this.i);
else if(bytes === 8) r = readUInt64BE(this.buffer,this.i);
} else {
if(bytes === 1) r = this.buffer.readUInt8(this.i);
else if(bytes === 2) r = this.buffer.readUInt16LE(this.i);
else if(bytes === 4) r = this.buffer.readUInt32LE(this.i);
else if(bytes === 8) r = readUInt64LE(this.buffer,this.i);
}
}
this.i += bytes;
return r;
}
float() {
let r = 0;
if(this.remaining() >= 4) {
if(this.defaultByteOrder === 'be') r = this.buffer.readFloatBE(this.i);
else r = this.buffer.readFloatLE(this.i);
}
this.i += 4;
return r;
}
varint() {
const out = Varint.decode(this.buffer, this.i);
this.i += Varint.decode.bytes;
return out;
}
/** @returns Buffer */
part(bytes) {
let r;
if(this.remaining() >= bytes) {
r = this.buffer.slice(this.i,this.i+bytes);
} else {
r = Buffer.from([]);
}
this.i += bytes;
return r;
}
remaining() {
return this.buffer.length-this.i;
}
rest() {
return this.buffer.slice(this.i);
}
done() {
return this.i >= this.buffer.length;
}
}
import Iconv from 'iconv-lite'
import Long from 'long'
import { Buffer } from 'buffer'
import Varint from 'varint'
function readUInt64BE (buffer, offset) {
const high = buffer.readUInt32BE(offset)
const low = buffer.readUInt32BE(offset + 4)
return new Long(low, high, true)
}
function readUInt64LE (buffer, offset) {
const low = buffer.readUInt32LE(offset)
const high = buffer.readUInt32LE(offset + 4)
return new Long(low, high, true)
}
export default class Reader {
/**
* @param {Core} query
* @param {Buffer} buffer
**/
constructor (query, buffer) {
this.defaultEncoding = query.options.encoding || query.encoding
this.defaultDelimiter = query.delimiter
this.defaultByteOrder = query.byteorder
this.buffer = buffer
this.i = 0
}
setOffset (offset) {
this.i = offset
}
offset () {
return this.i
}
skip (i) {
this.i += i
}
pascalString (bytesForSize, adjustment = 0) {
const length = this.uint(bytesForSize) + adjustment
return this.string(length)
}
string (arg) {
let encoding = this.defaultEncoding
let length = null
let delimiter = this.defaultDelimiter
if (typeof arg === 'string') delimiter = arg
else if (typeof arg === 'number') length = arg
else if (typeof arg === 'object') {
if ('encoding' in arg) encoding = arg.encoding
if ('length' in arg) length = arg.length
if ('delimiter' in arg) delimiter = arg.delimiter
}
if (encoding === 'latin1') encoding = 'win1252'
const start = this.i
let end = start
if (length === null) {
// terminated by the delimiter
let delim = delimiter
if (typeof delim === 'string') delim = delim.charCodeAt(0)
while (true) {
if (end >= this.buffer.length) {
end = this.buffer.length
break
}
if (this.buffer.readUInt8(end) === delim) break
end++
}
this.i = end + 1
} else if (length <= 0) {
return ''
} else {
end = start + length
if (end >= this.buffer.length) {
end = this.buffer.length
}
this.i = end
}
const slice = this.buffer.slice(start, end)
const enc = encoding
if (enc === 'utf8' || enc === 'ucs2' || enc === 'binary') {
return slice.toString(enc)
} else {
return Iconv.decode(slice, enc)
}
}
int (bytes) {
let r = 0
if (this.remaining() >= bytes) {
if (this.defaultByteOrder === 'be') {
if (bytes === 1) r = this.buffer.readInt8(this.i)
else if (bytes === 2) r = this.buffer.readInt16BE(this.i)
else if (bytes === 4) r = this.buffer.readInt32BE(this.i)
} else {
if (bytes === 1) r = this.buffer.readInt8(this.i)
else if (bytes === 2) r = this.buffer.readInt16LE(this.i)
else if (bytes === 4) r = this.buffer.readInt32LE(this.i)
}
}
this.i += bytes
return r
}
/** @returns {number} */
uint (bytes) {
let r = 0
if (this.remaining() >= bytes) {
if (this.defaultByteOrder === 'be') {
if (bytes === 1) r = this.buffer.readUInt8(this.i)
else if (bytes === 2) r = this.buffer.readUInt16BE(this.i)
else if (bytes === 4) r = this.buffer.readUInt32BE(this.i)
else if (bytes === 8) r = readUInt64BE(this.buffer, this.i)
} else {
if (bytes === 1) r = this.buffer.readUInt8(this.i)
else if (bytes === 2) r = this.buffer.readUInt16LE(this.i)
else if (bytes === 4) r = this.buffer.readUInt32LE(this.i)
else if (bytes === 8) r = readUInt64LE(this.buffer, this.i)
}
}
this.i += bytes
return r
}
float () {
let r = 0
if (this.remaining() >= 4) {
if (this.defaultByteOrder === 'be') r = this.buffer.readFloatBE(this.i)
else r = this.buffer.readFloatLE(this.i)
}
this.i += 4
return r
}
varint () {
const out = Varint.decode(this.buffer, this.i)
this.i += Varint.decode.bytes
return out
}
/** @returns Buffer */
part (bytes) {
let r
if (this.remaining() >= bytes) {
r = this.buffer.slice(this.i, this.i + bytes)
} else {
r = Buffer.from([])
}
this.i += bytes
return r
}
remaining () {
return this.buffer.length - this.i
}
rest () {
return this.buffer.slice(this.i)
}
done () {
return this.i >= this.buffer.length
}
}

1730
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,10 @@
{
"name": "gamedig",
"description": "Query for the status of any game server in Node.JS",
"scripts": {
"lint:check": "eslint .",
"lint:fix": "eslint --fix ."
},
"keywords": [
"srcds",
"query",
@ -35,17 +39,6 @@
"engines": {
"node": ">=14.17.0"
},
"dependencies": {
"cheerio": "^1.0.0-rc.10",
"gbxremote": "^0.2.1",
"got": "^12.1.0",
"iconv-lite": "^0.6.3",
"long": "^5.2.0",
"minimist": "^1.2.6",
"punycode": "^2.3.0",
"seek-bzip": "^2.0.0",
"varint": "^6.0.0"
},
"bin": {
"gamedig": "bin/gamedig.js"
},
@ -58,8 +51,24 @@
"GAMES_LIST.md",
"README.md"
],
"dependencies": {
"cheerio": "^1.0.0-rc.10",
"gbxremote": "^0.2.1",
"got": "^12.1.0",
"iconv-lite": "^0.6.3",
"long": "^5.2.0",
"minimist": "^1.2.6",
"punycode": "^2.3.0",
"seek-bzip": "^2.0.0",
"varint": "^6.0.0"
},
"devDependencies": {
"@types/cheerio": "^0.22.31",
"@types/node": "^14.18.13"
"@types/node": "^14.18.13",
"eslint": "^8.49.0",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-n": "15.7.0",
"eslint-plugin-promise": "^6.1.1"
}
}

View file

@ -1,63 +1,65 @@
import Core from './core.js';
export default class armagetron extends Core {
constructor() {
super();
this.encoding = 'latin1';
this.byteorder = 'be';
}
async run(state) {
const b = Buffer.from([0,0x35,0,0,0,0,0,0x11]);
const buffer = await this.udpSend(b,b => b);
const reader = this.reader(buffer);
reader.skip(6);
state.gamePort = this.readUInt(reader);
state.raw.hostname = this.readString(reader);
state.name = this.stripColorCodes(this.readString(reader));
state.raw.numplayers = this.readUInt(reader);
state.raw.versionmin = this.readUInt(reader);
state.raw.versionmax = this.readUInt(reader);
state.raw.version = this.readString(reader);
state.maxplayers = this.readUInt(reader);
const players = this.readString(reader);
const list = players.split('\n');
for(const name of list) {
if(!name) continue;
state.players.push({
name: this.stripColorCodes(name)
});
}
state.raw.options = this.stripColorCodes(this.readString(reader));
state.raw.uri = this.readString(reader);
state.raw.globalids = this.readString(reader);
}
readUInt(reader) {
const a = reader.uint(2);
const b = reader.uint(2);
return (b<<16) + a;
}
readString(reader) {
const len = reader.uint(2);
if(!len) return '';
let out = '';
for(let i = 0; i < len; i += 2) {
const hi = reader.uint(1);
const lo = reader.uint(1);
if(i+1<len) out += String.fromCharCode(lo);
if(i+2<len) out += String.fromCharCode(hi);
}
return out;
}
stripColorCodes(str) {
return str.replace(/0x[0-9a-f]{6}/g,'');
}
}
import Core from './core.js'
export default class armagetron extends Core {
constructor () {
super()
this.encoding = 'latin1'
this.byteorder = 'be'
}
async run (state) {
const b = Buffer.from([0, 0x35, 0, 0, 0, 0, 0, 0x11])
const buffer = await this.udpSend(b, b => b)
const reader = this.reader(buffer)
reader.skip(6)
state.gamePort = this.readUInt(reader)
state.raw.hostname = this.readString(reader)
state.name = this.stripColorCodes(this.readString(reader))
state.raw.numplayers = this.readUInt(reader)
state.raw.versionmin = this.readUInt(reader)
state.raw.versionmax = this.readUInt(reader)
state.raw.version = this.readString(reader)
state.maxplayers = this.readUInt(reader)
const players = this.readString(reader)
const list = players.split('\n')
for (const name of list) {
if (!name) continue
state.players.push({
name: this.stripColorCodes(name)
})
}
state.raw.options = this.stripColorCodes(this.readString(reader))
state.raw.uri = this.readString(reader)
state.raw.globalids = this.readString(reader)
}
readUInt (reader) {
const a = reader.uint(2)
const b = reader.uint(2)
return (b << 16) + a
}
readString (reader) {
const len = reader.uint(2)
if (!len) return ''
let out = ''
for (let i = 0; i < len; i += 2) {
const hi = reader.uint(1)
const lo = reader.uint(1)
if (i + 1 < len) out += String.fromCharCode(lo)
if (i + 2 < len) out += String.fromCharCode(hi)
}
return out
}
stripColorCodes (str) {
return str.replace(/0x[0-9a-f]{6}/g, '')
}
}

View file

@ -1,45 +1,45 @@
import Core from './core.js';
export default class ase extends Core {
async run(state) {
const buffer = await this.udpSend('s',(buffer) => {
const reader = this.reader(buffer);
const header = reader.string(4);
if (header === 'EYE1') return reader.rest();
});
const reader = this.reader(buffer);
state.raw.gamename = this.readString(reader);
state.gamePort = parseInt(this.readString(reader));
state.name = this.readString(reader);
state.raw.gametype = this.readString(reader);
state.map = this.readString(reader);
state.raw.version = this.readString(reader);
state.password = this.readString(reader) === '1';
state.raw.numplayers = parseInt(this.readString(reader));
state.maxplayers = parseInt(this.readString(reader));
while(!reader.done()) {
const key = this.readString(reader);
if(!key) break;
const value = this.readString(reader);
state.raw[key] = value;
}
while(!reader.done()) {
const flags = reader.uint(1);
const player = {};
if(flags & 1) player.name = this.readString(reader);
if(flags & 2) player.team = this.readString(reader);
if(flags & 4) player.skin = this.readString(reader);
if(flags & 8) player.score = parseInt(this.readString(reader));
if(flags & 16) player.ping = parseInt(this.readString(reader));
if(flags & 32) player.time = parseInt(this.readString(reader));
state.players.push(player);
}
}
readString(reader) {
return reader.pascalString(1, -1);
}
}
import Core from './core.js'
export default class ase extends Core {
async run (state) {
const buffer = await this.udpSend('s', (buffer) => {
const reader = this.reader(buffer)
const header = reader.string(4)
if (header === 'EYE1') return reader.rest()
})
const reader = this.reader(buffer)
state.raw.gamename = this.readString(reader)
state.gamePort = parseInt(this.readString(reader))
state.name = this.readString(reader)
state.raw.gametype = this.readString(reader)
state.map = this.readString(reader)
state.raw.version = this.readString(reader)
state.password = this.readString(reader) === '1'
state.raw.numplayers = parseInt(this.readString(reader))
state.maxplayers = parseInt(this.readString(reader))
while (!reader.done()) {
const key = this.readString(reader)
if (!key) break
const value = this.readString(reader)
state.raw[key] = value
}
while (!reader.done()) {
const flags = reader.uint(1)
const player = {}
if (flags & 1) player.name = this.readString(reader)
if (flags & 2) player.team = this.readString(reader)
if (flags & 4) player.skin = this.readString(reader)
if (flags & 8) player.score = parseInt(this.readString(reader))
if (flags & 16) player.ping = parseInt(this.readString(reader))
if (flags & 32) player.time = parseInt(this.readString(reader))
state.players.push(player)
}
}
readString (reader) {
return reader.pascalString(1, -1)
}
}

View file

@ -1,38 +1,38 @@
import Core from './core.js';
export default class assettocorsa extends Core {
async run(state) {
const serverInfo = await this.request({
url: `http://${this.options.address}:${this.options.port}/INFO`,
responseType: 'json'
});
const carInfo = await this.request({
url: `http://${this.options.address}:${this.options.port}/JSON|${parseInt(Math.random() * 999999999999999, 10)}`,
responseType: 'json'
});
if (!serverInfo || !carInfo || !carInfo.Cars) {
throw new Error('Query not successful');
}
state.maxplayers = serverInfo.maxclients;
state.name = serverInfo.name;
state.map = serverInfo.track;
state.password = serverInfo.pass;
state.gamePort = serverInfo.port;
state.raw.carInfo = carInfo.Cars;
state.raw.serverInfo = serverInfo;
for (const car of carInfo.Cars) {
if (car.IsConnected) {
state.players.push({
name: car.DriverName,
car: car.Model,
skin: car.Skin,
nation: car.DriverNation,
team: car.DriverTeam
});
}
}
}
}
import Core from './core.js'
export default class assettocorsa extends Core {
async run (state) {
const serverInfo = await this.request({
url: `http://${this.options.address}:${this.options.port}/INFO`,
responseType: 'json'
})
const carInfo = await this.request({
url: `http://${this.options.address}:${this.options.port}/JSON|${parseInt(Math.random() * 999999999999999, 10)}`,
responseType: 'json'
})
if (!serverInfo || !carInfo || !carInfo.Cars) {
throw new Error('Query not successful')
}
state.maxplayers = serverInfo.maxclients
state.name = serverInfo.name
state.map = serverInfo.track
state.password = serverInfo.pass
state.gamePort = serverInfo.port
state.raw.carInfo = carInfo.Cars
state.raw.serverInfo = serverInfo
for (const car of carInfo.Cars) {
if (car.IsConnected) {
state.players.push({
name: car.DriverName,
car: car.Model,
skin: car.Skin,
nation: car.DriverNation,
team: car.DriverTeam
})
}
}
}
}

View file

@ -1,161 +1,162 @@
import Core from './core.js';
export default class battlefield extends Core {
constructor() {
super();
this.encoding = 'latin1';
}
async run(state) {
await this.withTcp(async socket => {
{
const data = await this.query(socket, ['serverInfo']);
state.name = data.shift();
state.raw.numplayers = parseInt(data.shift());
state.maxplayers = parseInt(data.shift());
state.raw.gametype = data.shift();
state.map = data.shift();
state.raw.roundsplayed = parseInt(data.shift());
state.raw.roundstotal = parseInt(data.shift());
const teamCount = data.shift();
state.raw.teams = [];
for (let i = 0; i < teamCount; i++) {
const tickets = parseFloat(data.shift());
state.raw.teams.push({
tickets: tickets
});
}
state.raw.targetscore = parseInt(data.shift());
state.raw.status = data.shift();
// Seems like the fields end at random places beyond this point
// depending on the server version
if (data.length) state.raw.ranked = (data.shift() === 'true');
if (data.length) state.raw.punkbuster = (data.shift() === 'true');
if (data.length) state.password = (data.shift() === 'true');
if (data.length) state.raw.uptime = parseInt(data.shift());
if (data.length) state.raw.roundtime = parseInt(data.shift());
const isBadCompany2 = data[0] === 'BC2';
if (isBadCompany2) {
if (data.length) data.shift();
if (data.length) data.shift();
}
if (data.length) {
state.raw.ip = data.shift();
const split = state.raw.ip.split(':');
state.gameHost = split[0];
state.gamePort = split[1];
} else {
// best guess if the server doesn't tell us what the server port is
// these are just the default game ports for different default query ports
if (this.options.port === 48888) state.gamePort = 7673;
if (this.options.port === 22000) state.gamePort = 25200;
}
if (data.length) state.raw.punkbusterversion = data.shift();
if (data.length) state.raw.joinqueue = (data.shift() === 'true');
if (data.length) state.raw.region = data.shift();
if (data.length) state.raw.pingsite = data.shift();
if (data.length) state.raw.country = data.shift();
if (data.length) state.raw.quickmatch = (data.shift() === 'true');
}
{
const data = await this.query(socket, ['version']);
data.shift();
state.raw.version = data.shift();
}
{
const data = await this.query(socket, ['listPlayers', 'all']);
const fieldCount = parseInt(data.shift());
const fields = [];
for (let i = 0; i < fieldCount; i++) {
fields.push(data.shift());
}
const numplayers = data.shift();
for (let i = 0; i < numplayers; i++) {
const player = {};
for (let key of fields) {
let value = data.shift();
if (key === 'teamId') key = 'team';
else if (key === 'squadId') key = 'squad';
if (
key === 'kills'
|| key === 'deaths'
|| key === 'score'
|| key === 'rank'
|| key === 'team'
|| key === 'squad'
|| key === 'ping'
|| key === 'type'
) {
value = parseInt(value);
}
player[key] = value;
}
state.players.push(player);
}
}
});
}
async query(socket, params) {
const outPacket = this.buildPacket(params);
return await this.tcpSend(socket, outPacket, (data) => {
const decoded = this.decodePacket(data);
if(decoded) {
this.logger.debug(decoded);
if(decoded.shift() !== 'OK') throw new Error('Missing OK');
return decoded;
}
});
}
buildPacket(params) {
const paramBuffers = [];
for (const param of params) {
paramBuffers.push(Buffer.from(param,'utf8'));
}
let totalLength = 12;
for (const paramBuffer of paramBuffers) {
totalLength += paramBuffer.length+1+4;
}
const b = Buffer.alloc(totalLength);
b.writeUInt32LE(0,0);
b.writeUInt32LE(totalLength,4);
b.writeUInt32LE(params.length,8);
let offset = 12;
for (const paramBuffer of paramBuffers) {
b.writeUInt32LE(paramBuffer.length, offset); offset += 4;
paramBuffer.copy(b, offset); offset += paramBuffer.length;
b.writeUInt8(0, offset); offset += 1;
}
return b;
}
decodePacket(buffer) {
if(buffer.length < 8) return false;
const reader = this.reader(buffer);
const header = reader.uint(4);
const totalLength = reader.uint(4);
if(buffer.length < totalLength) return false;
this.logger.debug("Expected " + totalLength + " bytes, have " + buffer.length);
const paramCount = reader.uint(4);
const params = [];
for(let i = 0; i < paramCount; i++) {
params.push(reader.pascalString(4));
const strNull = reader.uint(1);
}
return params;
}
}
import Core from './core.js'
export default class battlefield extends Core {
constructor () {
super()
this.encoding = 'latin1'
}
async run (state) {
await this.withTcp(async socket => {
{
const data = await this.query(socket, ['serverInfo'])
state.name = data.shift()
state.raw.numplayers = parseInt(data.shift())
state.maxplayers = parseInt(data.shift())
state.raw.gametype = data.shift()
state.map = data.shift()
state.raw.roundsplayed = parseInt(data.shift())
state.raw.roundstotal = parseInt(data.shift())
const teamCount = data.shift()
state.raw.teams = []
for (let i = 0; i < teamCount; i++) {
const tickets = parseFloat(data.shift())
state.raw.teams.push({
tickets
})
}
state.raw.targetscore = parseInt(data.shift())
state.raw.status = data.shift()
// Seems like the fields end at random places beyond this point
// depending on the server version
if (data.length) state.raw.ranked = (data.shift() === 'true')
if (data.length) state.raw.punkbuster = (data.shift() === 'true')
if (data.length) state.password = (data.shift() === 'true')
if (data.length) state.raw.uptime = parseInt(data.shift())
if (data.length) state.raw.roundtime = parseInt(data.shift())
const isBadCompany2 = data[0] === 'BC2'
if (isBadCompany2) {
if (data.length) data.shift()
if (data.length) data.shift()
}
if (data.length) {
state.raw.ip = data.shift()
const split = state.raw.ip.split(':')
state.gameHost = split[0]
state.gamePort = split[1]
} else {
// best guess if the server doesn't tell us what the server port is
// these are just the default game ports for different default query ports
if (this.options.port === 48888) state.gamePort = 7673
if (this.options.port === 22000) state.gamePort = 25200
}
if (data.length) state.raw.punkbusterversion = data.shift()
if (data.length) state.raw.joinqueue = (data.shift() === 'true')
if (data.length) state.raw.region = data.shift()
if (data.length) state.raw.pingsite = data.shift()
if (data.length) state.raw.country = data.shift()
if (data.length) state.raw.quickmatch = (data.shift() === 'true')
}
{
const data = await this.query(socket, ['version'])
data.shift()
state.raw.version = data.shift()
}
{
const data = await this.query(socket, ['listPlayers', 'all'])
const fieldCount = parseInt(data.shift())
const fields = []
for (let i = 0; i < fieldCount; i++) {
fields.push(data.shift())
}
const numplayers = data.shift()
for (let i = 0; i < numplayers; i++) {
const player = {}
for (let key of fields) {
let value = data.shift()
if (key === 'teamId') key = 'team'
else if (key === 'squadId') key = 'squad'
if (
key === 'kills' ||
key === 'deaths' ||
key === 'score' ||
key === 'rank' ||
key === 'team' ||
key === 'squad' ||
key === 'ping' ||
key === 'type'
) {
value = parseInt(value)
}
player[key] = value
}
state.players.push(player)
}
}
})
}
async query (socket, params) {
const outPacket = this.buildPacket(params)
return await this.tcpSend(socket, outPacket, (data) => {
const decoded = this.decodePacket(data)
if (decoded) {
this.logger.debug(decoded)
if (decoded.shift() !== 'OK') throw new Error('Missing OK')
return decoded
}
})
}
buildPacket (params) {
const paramBuffers = []
for (const param of params) {
paramBuffers.push(Buffer.from(param, 'utf8'))
}
let totalLength = 12
for (const paramBuffer of paramBuffers) {
totalLength += paramBuffer.length + 1 + 4
}
const b = Buffer.alloc(totalLength)
b.writeUInt32LE(0, 0)
b.writeUInt32LE(totalLength, 4)
b.writeUInt32LE(params.length, 8)
let offset = 12
for (const paramBuffer of paramBuffers) {
b.writeUInt32LE(paramBuffer.length, offset); offset += 4
paramBuffer.copy(b, offset); offset += paramBuffer.length
b.writeUInt8(0, offset); offset += 1
}
return b
}
decodePacket (buffer) {
if (buffer.length < 8) return false
const reader = this.reader(buffer)
reader.uint(4) // header
const totalLength = reader.uint(4)
if (buffer.length < totalLength) return false
this.logger.debug('Expected ' + totalLength + ' bytes, have ' + buffer.length)
const paramCount = reader.uint(4)
const params = []
for (let i = 0; i < paramCount; i++) {
params.push(reader.pascalString(4))
reader.uint(1) // strNull
}
return params
}
}

View file

@ -1,55 +1,55 @@
import Core from './core.js';
import * as cheerio from "cheerio";
export default class buildandshoot extends Core {
async run(state) {
const body = await this.request({
url: 'http://'+this.options.address+':'+this.options.port+'/',
});
let m;
m = body.match(/status server for (.*?)\.?[\r\n]/);
if(m) state.name = m[1];
m = body.match(/Current uptime: (\d+)/);
if(m) state.raw.uptime = m[1];
m = body.match(/currently running (.*?) by /);
if(m) state.map = m[1];
m = body.match(/Current players: (\d+)\/(\d+)/);
if(m) {
state.raw.numplayers = m[1];
state.maxplayers = m[2];
}
m = body.match(/aos:\/\/[0-9]+:[0-9]+/);
if (m) {
state.connect = m[0];
}
const $ = cheerio.load(body);
$('#playerlist tbody tr').each((i,tr) => {
if (!$(tr).find('td').first().attr('colspan')) {
state.players.push({
name: $(tr).find('td').eq(2).text(),
ping: $(tr).find('td').eq(3).text().trim(),
team: $(tr).find('td').eq(4).text().toLowerCase(),
score: parseInt($(tr).find('td').eq(5).text())
});
}
});
/*
var m = this.options.address.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/);
if(m) {
var o1 = parseInt(m[1]);
var o2 = parseInt(m[2]);
var o3 = parseInt(m[3]);
var o4 = parseInt(m[4]);
var addr = o1+(o2<<8)+(o3<<16)+(o4<<24);
state.raw.url = 'aos://'+addr;
}
*/
}
}
import Core from './core.js'
import * as cheerio from 'cheerio'
export default class buildandshoot extends Core {
async run (state) {
const body = await this.request({
url: 'http://' + this.options.address + ':' + this.options.port + '/'
})
let m
m = body.match(/status server for (.*?)\.?[\r\n]/)
if (m) state.name = m[1]
m = body.match(/Current uptime: (\d+)/)
if (m) state.raw.uptime = m[1]
m = body.match(/currently running (.*?) by /)
if (m) state.map = m[1]
m = body.match(/Current players: (\d+)\/(\d+)/)
if (m) {
state.raw.numplayers = m[1]
state.maxplayers = m[2]
}
m = body.match(/aos:\/\/[0-9]+:[0-9]+/)
if (m) {
state.connect = m[0]
}
const $ = cheerio.load(body)
$('#playerlist tbody tr').each((i, tr) => {
if (!$(tr).find('td').first().attr('colspan')) {
state.players.push({
name: $(tr).find('td').eq(2).text(),
ping: $(tr).find('td').eq(3).text().trim(),
team: $(tr).find('td').eq(4).text().toLowerCase(),
score: parseInt($(tr).find('td').eq(5).text())
})
}
})
/*
var m = this.options.address.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/);
if(m) {
var o1 = parseInt(m[1]);
var o2 = parseInt(m[2]);
var o3 = parseInt(m[3]);
var o4 = parseInt(m[4]);
var addr = o1+(o2<<8)+(o3<<16)+(o4<<24);
state.raw.url = 'aos://'+addr;
}
*/
}
}

View file

@ -1,349 +1,350 @@
import {EventEmitter} from "events";
import * as net from "net";
import Reader from "../lib/reader.js";
import {debugDump} from '../lib/HexUtil.js';
import Logger from "../lib/Logger.js";
import DnsResolver from "../lib/DnsResolver.js";
import {Results} from "../lib/Results.js";
import Promises from "../lib/Promises.js";
let uid = 0;
export default class Core extends EventEmitter {
constructor() {
super();
this.encoding = 'utf8';
this.byteorder = 'le';
this.delimiter = '\0';
this.srvRecord = null;
this.abortedPromise = null;
this.logger = new Logger();
this.dnsResolver = new DnsResolver(this.logger);
// Sent to us by QueryRunner
this.options = null;
/** @type GlobalUdpSocket */
this.udpSocket = null;
this.shortestRTT = 0;
this.usedTcp = false;
}
// Runs a single attempt with a timeout and cleans up afterward
async runOnceSafe() {
if (this.options.debug) {
this.logger.debugEnabled = true;
}
this.logger.prefix = 'Q#' + (uid++);
this.logger.debug("Starting");
this.logger.debug("Protocol: " + this.constructor.name);
this.logger.debug("Options:", this.options);
let abortCall = null;
this.abortedPromise = new Promise((resolve,reject) => {
abortCall = () => reject(new Error("Query is finished -- cancelling outstanding promises"));
}).catch(() => {
// Make sure that if this promise isn't attached to, it doesn't throw a unhandled promise rejection
});
let timeout;
try {
const promise = this.runOnce();
timeout = Promises.createTimeout(this.options.attemptTimeout, "Attempt");
const result = await Promise.race([promise, timeout]);
this.logger.debug("Query was successful");
return result;
} catch(e) {
this.logger.debug("Query failed with error", e);
throw e;
} finally {
timeout && timeout.cancel();
try {
abortCall();
} catch(e) {
this.logger.debug("Error during abort cleanup: " + e.stack);
}
}
}
async runOnce() {
const options = this.options;
if (('host' in options) && !('address' in options)) {
const resolved = await this.dnsResolver.resolve(options.host, options.ipFamily, this.srvRecord);
options.address = resolved.address;
if (resolved.port) options.port = resolved.port;
}
const state = new Results();
await this.run(state);
// because lots of servers prefix with spaces to try to appear first
state.name = (state.name || '').trim();
if (!('connect' in state)) {
state.connect = ''
+ (state.gameHost || this.options.host || this.options.address)
+ ':'
+ (state.gamePort || this.options.port)
}
state.ping = this.shortestRTT;
delete state.gameHost;
delete state.gamePort;
this.logger.debug(log => {
log("Size of players array: " + state.players.length);
log("Size of bots array: " + state.bots.length);
});
return state;
}
async run(/** Results */ state) {}
/** Param can be a time in ms, or a promise (which will be timed) */
registerRtt(param) {
if (param.then) {
const start = Date.now();
param.then(() => {
const end = Date.now();
const rtt = end - start;
this.registerRtt(rtt);
}).catch(() => {});
} else {
this.logger.debug("Registered RTT: " + param + "ms");
if (this.shortestRTT === 0 || param < this.shortestRTT) {
this.shortestRTT = param;
}
}
}
// utils
/** @returns {Reader} */
reader(buffer) {
return new Reader(this,buffer);
}
translate(obj,trans) {
for(const from of Object.keys(trans)) {
const to = trans[from];
if(from in obj) {
if(to) obj[to] = obj[from];
delete obj[from];
}
}
}
trueTest(str) {
if(typeof str === 'boolean') return str;
if(typeof str === 'number') return str !== 0;
if(typeof str === 'string') {
if(str.toLowerCase() === 'true') return true;
if(str.toLowerCase() === 'yes') return true;
if(str === '1') return true;
}
return false;
}
assertValidPort(port) {
if (!port) {
throw new Error("Could not determine port to query. Did you provide a port?");
}
if (port < 1 || port > 65535) {
throw new Error("Invalid tcp/ip port: " + port);
}
}
/**
* @template T
* @param {function(NodeJS.Socket):Promise<T>} fn
* @param {number=} port
* @returns {Promise<T>}
*/
async withTcp(fn, port) {
this.usedTcp = true;
const address = this.options.address;
if (!port) port = this.options.port;
this.assertValidPort(port);
let socket, connectionTimeout;
try {
socket = net.connect(port,address);
socket.setNoDelay(true);
// Prevent unhandled 'error' events from dumping straight to console
socket.on('error', () => {});
this.logger.debug(log => {
this.logger.debug(address+':'+port+" TCP Connecting");
const writeHook = socket.write;
socket.write = (...args) => {
log(address+':'+port+" TCP-->");
log(debugDump(args[0]));
writeHook.apply(socket,args);
};
socket.on('error', e => log('TCP Error:', e));
socket.on('close', () => log('TCP Closed'));
socket.on('data', (data) => {
log(address+':'+port+" <--TCP");
log(data);
});
socket.on('ready', () => log(address+':'+port+" TCP Connected"));
});
const connectionPromise = new Promise((resolve,reject) => {
socket.on('ready', resolve);
socket.on('close', () => reject(new Error('TCP Connection Refused')));
});
this.registerRtt(connectionPromise);
connectionTimeout = Promises.createTimeout(this.options.socketTimeout, 'TCP Opening');
await Promise.race([
connectionPromise,
connectionTimeout,
this.abortedPromise
]);
return await fn(socket);
} finally {
socket && socket.destroy();
connectionTimeout && connectionTimeout.cancel();
}
}
/**
* @template T
* @param {NodeJS.Socket} socket
* @param {Buffer|string} buffer
* @param {function(Buffer):T} ondata
* @returns Promise<T>
*/
async tcpSend(socket,buffer,ondata) {
let timeout;
try {
const promise = new Promise(async (resolve, reject) => {
let received = Buffer.from([]);
const onData = (data) => {
received = Buffer.concat([received, data]);
const result = ondata(received);
if (result !== undefined) {
socket.removeListener('data', onData);
resolve(result);
}
};
socket.on('data', onData);
socket.write(buffer);
});
timeout = Promises.createTimeout(this.options.socketTimeout, 'TCP');
return await Promise.race([promise, timeout, this.abortedPromise]);
} finally {
timeout && timeout.cancel();
}
}
/**
* @param {Buffer|string} buffer
* @param {function(Buffer):T=} onPacket
* @param {(function():T)=} onTimeout
* @returns Promise<T>
* @template T
*/
async udpSend(buffer,onPacket,onTimeout) {
const address = this.options.address;
const port = this.options.port;
this.assertValidPort(port);
if(typeof buffer === 'string') buffer = Buffer.from(buffer,'binary');
const socket = this.udpSocket;
await socket.send(buffer, address, port, this.options.debug);
if (!onPacket && !onTimeout) {
return null;
}
let socketCallback;
let timeout;
try {
const promise = new Promise((resolve, reject) => {
const start = Date.now();
let end = null;
socketCallback = (fromAddress, fromPort, buffer) => {
try {
if (fromAddress !== address) return;
if (fromPort !== port) return;
if (end === null) {
end = Date.now();
const rtt = end-start;
this.registerRtt(rtt);
}
const result = onPacket(buffer);
if (result !== undefined) {
this.logger.debug("UDP send finished by callback");
resolve(result);
}
} catch(e) {
reject(e);
}
};
socket.addCallback(socketCallback, this.options.debug);
});
timeout = Promises.createTimeout(this.options.socketTimeout, 'UDP');
const wrappedTimeout = new Promise((resolve, reject) => {
timeout.catch((e) => {
this.logger.debug("UDP timeout detected");
if (onTimeout) {
try {
const result = onTimeout();
if (result !== undefined) {
this.logger.debug("UDP timeout resolved by callback");
resolve(result);
return;
}
} catch(e) {
reject(e);
}
}
reject(e);
});
});
return await Promise.race([promise, wrappedTimeout, this.abortedPromise]);
} finally {
timeout && timeout.cancel();
socketCallback && socket.removeCallback(socketCallback);
}
}
async tcpPing() {
// This will give a much more accurate RTT than using the rtt of an http request.
if (!this.usedTcp) {
await this.withTcp(() => {});
}
}
async request(params) {
await this.tcpPing();
const got = (await import('got')).got;
let requestPromise;
try {
requestPromise = got({
...params,
timeout: {
request: this.options.socketTimeout
}
});
this.logger.debug(log => {
log(() => params.url + " HTTP-->");
requestPromise
.then((response) => log(params.url + " <--HTTP " + response.statusCode))
.catch(() => {});
});
const wrappedPromise = requestPromise.then(response => {
if (response.statusCode !== 200) throw new Error("Bad status code: " + response.statusCode);
return response.body;
});
return await Promise.race([wrappedPromise, this.abortedPromise]);
} finally {
requestPromise && requestPromise.cancel();
}
}
}
import { EventEmitter } from 'events'
import * as net from 'net'
import Reader from '../lib/reader.js'
import { debugDump } from '../lib/HexUtil.js'
import Logger from '../lib/Logger.js'
import DnsResolver from '../lib/DnsResolver.js'
import { Results } from '../lib/Results.js'
import Promises from '../lib/Promises.js'
let uid = 0
export default class Core extends EventEmitter {
constructor () {
super()
this.encoding = 'utf8'
this.byteorder = 'le'
this.delimiter = '\0'
this.srvRecord = null
this.abortedPromise = null
this.logger = new Logger()
this.dnsResolver = new DnsResolver(this.logger)
// Sent to us by QueryRunner
this.options = null
/** @type GlobalUdpSocket */
this.udpSocket = null
this.shortestRTT = 0
this.usedTcp = false
}
// Runs a single attempt with a timeout and cleans up afterward
async runOnceSafe () {
if (this.options.debug) {
this.logger.debugEnabled = true
}
this.logger.prefix = 'Q#' + (uid++)
this.logger.debug('Starting')
this.logger.debug('Protocol: ' + this.constructor.name)
this.logger.debug('Options:', this.options)
let abortCall = null
this.abortedPromise = new Promise((resolve, reject) => {
abortCall = () => reject(new Error('Query is finished -- cancelling outstanding promises'))
}).catch(() => {
// Make sure that if this promise isn't attached to, it doesn't throw a unhandled promise rejection
})
let timeout
try {
const promise = this.runOnce()
timeout = Promises.createTimeout(this.options.attemptTimeout, 'Attempt')
const result = await Promise.race([promise, timeout])
this.logger.debug('Query was successful')
return result
} catch (e) {
this.logger.debug('Query failed with error', e)
throw e
} finally {
timeout && timeout.cancel()
try {
abortCall()
} catch (e) {
this.logger.debug('Error during abort cleanup: ' + e.stack)
}
}
}
async runOnce () {
const options = this.options
if (('host' in options) && !('address' in options)) {
const resolved = await this.dnsResolver.resolve(options.host, options.ipFamily, this.srvRecord)
options.address = resolved.address
if (resolved.port) options.port = resolved.port
}
const state = new Results()
await this.run(state)
// because lots of servers prefix with spaces to try to appear first
state.name = (state.name || '').trim()
if (!('connect' in state)) {
state.connect = '' +
(state.gameHost || this.options.host || this.options.address) +
':' +
(state.gamePort || this.options.port)
}
state.ping = this.shortestRTT
delete state.gameHost
delete state.gamePort
this.logger.debug(log => {
log('Size of players array: ' + state.players.length)
log('Size of bots array: ' + state.bots.length)
})
return state
}
async run (/** Results */ state) {}
/** Param can be a time in ms, or a promise (which will be timed) */
registerRtt (param) {
if (param.then) {
const start = Date.now()
param.then(() => {
const end = Date.now()
const rtt = end - start
this.registerRtt(rtt)
}).catch(() => {})
} else {
this.logger.debug('Registered RTT: ' + param + 'ms')
if (this.shortestRTT === 0 || param < this.shortestRTT) {
this.shortestRTT = param
}
}
}
// utils
/** @returns {Reader} */
reader (buffer) {
return new Reader(this, buffer)
}
translate (obj, trans) {
for (const from of Object.keys(trans)) {
const to = trans[from]
if (from in obj) {
if (to) obj[to] = obj[from]
delete obj[from]
}
}
}
trueTest (str) {
if (typeof str === 'boolean') return str
if (typeof str === 'number') return str !== 0
if (typeof str === 'string') {
if (str.toLowerCase() === 'true') return true
if (str.toLowerCase() === 'yes') return true
if (str === '1') return true
}
return false
}
assertValidPort (port) {
if (!port) {
throw new Error('Could not determine port to query. Did you provide a port?')
}
if (port < 1 || port > 65535) {
throw new Error('Invalid tcp/ip port: ' + port)
}
}
/**
* @template T
* @param {function(NodeJS.Socket):Promise<T>} fn
* @param {number=} port
* @returns {Promise<T>}
*/
async withTcp (fn, port) {
this.usedTcp = true
const address = this.options.address
if (!port) port = this.options.port
this.assertValidPort(port)
let socket, connectionTimeout
try {
socket = net.connect(port, address)
socket.setNoDelay(true)
// Prevent unhandled 'error' events from dumping straight to console
socket.on('error', () => {})
this.logger.debug(log => {
this.logger.debug(address + ':' + port + ' TCP Connecting')
const writeHook = socket.write
socket.write = (...args) => {
log(address + ':' + port + ' TCP-->')
log(debugDump(args[0]))
writeHook.apply(socket, args)
}
socket.on('error', e => log('TCP Error:', e))
socket.on('close', () => log('TCP Closed'))
socket.on('data', (data) => {
log(address + ':' + port + ' <--TCP')
log(data)
})
socket.on('ready', () => log(address + ':' + port + ' TCP Connected'))
})
const connectionPromise = new Promise((resolve, reject) => {
socket.on('ready', resolve)
socket.on('close', () => reject(new Error('TCP Connection Refused')))
})
this.registerRtt(connectionPromise)
connectionTimeout = Promises.createTimeout(this.options.socketTimeout, 'TCP Opening')
await Promise.race([
connectionPromise,
connectionTimeout,
this.abortedPromise
])
return await fn(socket)
} finally {
socket && socket.destroy()
connectionTimeout && connectionTimeout.cancel()
}
}
/**
* @template T
* @param {NodeJS.Socket} socket
* @param {Buffer|string} buffer
* @param {function(Buffer):T} ondata
* @returns Promise<T>
*/
async tcpSend (socket, buffer, ondata) {
let timeout
try {
const promise = new Promise((resolve, reject) => {
let received = Buffer.from([])
const onData = (data) => {
received = Buffer.concat([received, data])
const result = ondata(received)
if (result !== undefined) {
socket.removeListener('data', onData)
resolve(result)
}
}
socket.on('data', onData)
socket.write(buffer)
})
timeout = Promises.createTimeout(this.options.socketTimeout, 'TCP')
return await Promise.race([promise, timeout, this.abortedPromise])
} finally {
timeout && timeout.cancel()
}
}
/**
* @param {Buffer|string} buffer
* @param {function(Buffer):T=} onPacket
* @param {(function():T)=} onTimeout
* @returns Promise<T>
* @template T
*/
async udpSend (buffer, onPacket, onTimeout) {
const address = this.options.address
const port = this.options.port
this.assertValidPort(port)
if (typeof buffer === 'string') buffer = Buffer.from(buffer, 'binary')
const socket = this.udpSocket
await socket.send(buffer, address, port, this.options.debug)
if (!onPacket && !onTimeout) {
return null
}
let socketCallback
let timeout
try {
const promise = new Promise((resolve, reject) => {
const start = Date.now()
let end = null
socketCallback = (fromAddress, fromPort, buffer) => {
try {
if (fromAddress !== address) return
if (fromPort !== port) return
if (end === null) {
end = Date.now()
const rtt = end - start
this.registerRtt(rtt)
}
const result = onPacket(buffer)
if (result !== undefined) {
this.logger.debug('UDP send finished by callback')
resolve(result)
}
} catch (e) {
reject(e)
}
}
socket.addCallback(socketCallback, this.options.debug)
})
timeout = Promises.createTimeout(this.options.socketTimeout, 'UDP')
const wrappedTimeout = new Promise((resolve, reject) => {
timeout.catch((e) => {
this.logger.debug('UDP timeout detected')
if (onTimeout) {
try {
const result = onTimeout()
if (result !== undefined) {
this.logger.debug('UDP timeout resolved by callback')
resolve(result)
return
}
} catch (e) {
reject(e)
}
}
reject(e)
})
})
return await Promise.race([promise, wrappedTimeout, this.abortedPromise])
} finally {
timeout && timeout.cancel()
socketCallback && socket.removeCallback(socketCallback)
}
}
async tcpPing () {
// This will give a much more accurate RTT than using the rtt of an http request.
if (!this.usedTcp) {
await this.withTcp(() => {})
}
}
async request (params) {
await this.tcpPing()
const got = (await import('got')).got
let requestPromise
try {
requestPromise = got({
...params,
timeout: {
request: this.options.socketTimeout
}
})
this.logger.debug(log => {
log(() => params.url + ' HTTP-->')
requestPromise
.then((response) => log(params.url + ' <--HTTP ' + response.statusCode))
.catch(() => {})
})
const wrappedPromise = requestPromise.then(response => {
if (response.statusCode !== 200) throw new Error('Bad status code: ' + response.statusCode)
return response.body
})
return await Promise.race([wrappedPromise, this.abortedPromise])
} finally {
requestPromise && requestPromise.cancel()
}
}
}

View file

@ -1,67 +1,65 @@
import Core from './core.js';
export default class cs2d extends Core {
async run(state) {
{
const reader = await this.sendQuery(
Buffer.from('\x01\x00\xFB\x01\xF5\x03\xFB\x05', 'binary'),
Buffer.from('\x01\x00\xFB\x01', 'binary')
);
const flags = reader.uint(1);
state.raw.flags = flags;
state.password = this.readFlag(flags, 0);
state.raw.registeredOnly = this.readFlag(flags, 1);
state.raw.fogOfWar = this.readFlag(flags, 2);
state.raw.friendlyFire = this.readFlag(flags, 3);
state.raw.botsEnabled = this.readFlag(flags, 5);
state.raw.luaScripts = this.readFlag(flags, 6);
state.raw.forceLight = this.readFlag(flags, 7);
state.name = this.readString(reader);
state.map = this.readString(reader);
state.raw.numplayers = reader.uint(1);
state.maxplayers = reader.uint(1);
if (flags & 32) {
state.raw.gamemode = reader.uint(1);
} else {
state.raw.gamemode = 0;
}
state.raw.numbots = reader.uint(1);
const flags2 = reader.uint(1);
state.raw.flags2 = flags2;
state.raw.recoil = this.readFlag(flags2, 0);
state.raw.offScreenDamage = this.readFlag(flags2, 1);
state.raw.hasDownloads = this.readFlag(flags2, 2);
reader.skip(2);
const players = reader.uint(1);
for (let i = 0; i < players; i++) {
const player = {}
player.id = reader.uint(1);
player.name = this.readString(reader);
player.team = reader.uint(1);
player.score = reader.uint(4);
player.deaths = reader.uint(4);
state.players.push(player);
}
}
}
async sendQuery(request, expectedHeader) {
// Send multiple copies of the request packet, because cs2d likes to just ignore them randomly
await this.udpSend(request);
await this.udpSend(request);
return await this.udpSend(request, (buffer) => {
const reader = this.reader(buffer);
const header = reader.part(4);
if (!header.equals(expectedHeader)) return;
return reader;
});
}
readFlag(flags, offset) {
return !!(flags & (1 << offset));
}
readString(reader) {
return reader.pascalString(1);
}
}
import Core from './core.js'
export default class cs2d extends Core {
async run (state) {
const reader = await this.sendQuery(
Buffer.from('\x01\x00\xFB\x01\xF5\x03\xFB\x05', 'binary'),
Buffer.from('\x01\x00\xFB\x01', 'binary')
)
const flags = reader.uint(1)
state.raw.flags = flags
state.password = this.readFlag(flags, 0)
state.raw.registeredOnly = this.readFlag(flags, 1)
state.raw.fogOfWar = this.readFlag(flags, 2)
state.raw.friendlyFire = this.readFlag(flags, 3)
state.raw.botsEnabled = this.readFlag(flags, 5)
state.raw.luaScripts = this.readFlag(flags, 6)
state.raw.forceLight = this.readFlag(flags, 7)
state.name = this.readString(reader)
state.map = this.readString(reader)
state.raw.numplayers = reader.uint(1)
state.maxplayers = reader.uint(1)
if (flags & 32) {
state.raw.gamemode = reader.uint(1)
} else {
state.raw.gamemode = 0
}
state.raw.numbots = reader.uint(1)
const flags2 = reader.uint(1)
state.raw.flags2 = flags2
state.raw.recoil = this.readFlag(flags2, 0)
state.raw.offScreenDamage = this.readFlag(flags2, 1)
state.raw.hasDownloads = this.readFlag(flags2, 2)
reader.skip(2)
const players = reader.uint(1)
for (let i = 0; i < players; i++) {
const player = {}
player.id = reader.uint(1)
player.name = this.readString(reader)
player.team = reader.uint(1)
player.score = reader.uint(4)
player.deaths = reader.uint(4)
state.players.push(player)
}
}
async sendQuery (request, expectedHeader) {
// Send multiple copies of the request packet, because cs2d likes to just ignore them randomly
await this.udpSend(request)
await this.udpSend(request)
return await this.udpSend(request, (buffer) => {
const reader = this.reader(buffer)
const header = reader.part(4)
if (!header.equals(expectedHeader)) return
return reader
})
}
readFlag (flags, offset) {
return !!(flags & (1 << offset))
}
readString (reader) {
return reader.pascalString(1)
}
}

View file

@ -1,29 +1,29 @@
import Core from './core.js';
export default class discord extends Core {
async run(state) {
const guildId = this.options.guildId;
if (typeof guildId !== 'string') {
throw new Error('guildId option must be set when querying discord. Ensure the guildId is a string and not a number.'
+ " (It's too large of a number for javascript to store without losing precision)");
}
this.usedTcp = true;
const raw = await this.request({
url: 'https://discordapp.com/api/guilds/' + guildId + '/widget.json',
});
const json = JSON.parse(raw);
state.name = json.name;
if (json.instant_invite) {
state.connect = json.instant_invite;
} else {
state.connect = 'https://discordapp.com/channels/' + guildId;
}
for (const member of json.members) {
const {username: name, ...rest} = member;
state.players.push({ name, ...rest });
}
delete json.members;
state.maxplayers = 500000;
state.raw = json;
}
}
import Core from './core.js'
export default class discord extends Core {
async run (state) {
const guildId = this.options.guildId
if (typeof guildId !== 'string') {
throw new Error('guildId option must be set when querying discord. Ensure the guildId is a string and not a number.' +
" (It's too large of a number for javascript to store without losing precision)")
}
this.usedTcp = true
const raw = await this.request({
url: 'https://discordapp.com/api/guilds/' + guildId + '/widget.json'
})
const json = JSON.parse(raw)
state.name = json.name
if (json.instant_invite) {
state.connect = json.instant_invite
} else {
state.connect = 'https://discordapp.com/channels/' + guildId
}
for (const member of json.members) {
const { username: name, ...rest } = member
state.players.push({ name, ...rest })
}
delete json.members
state.maxplayers = 500000
state.raw = json
}
}

View file

@ -1,149 +1,147 @@
import Core from './core.js';
export default class doom3 extends Core {
constructor() {
super();
this.encoding = 'latin1';
}
async run(state) {
const body = await this.udpSend('\xff\xffgetInfo\x00PiNGPoNg\x00', packet => {
const reader = this.reader(packet);
const header = reader.uint(2);
if(header !== 0xffff) return;
const header2 = reader.string();
if(header2 !== 'infoResponse') return;
const challengePart1 = reader.string(4);
if (challengePart1 !== "PiNG") return;
// some doom3 implementations only return the first 4 bytes of the challenge
const challengePart2 = reader.string(4);
if (challengePart2 !== 'PoNg') reader.skip(-4);
return reader.rest();
});
let reader = this.reader(body);
const protoVersion = reader.uint(4);
state.raw.protocolVersion = (protoVersion>>16)+'.'+(protoVersion&0xffff);
// some doom implementations send us a packet size here, some don't (etqw does this)
// we can tell if this is a packet size, because the third and fourth byte will be 0 (no packets are that massive)
reader.skip(2);
const packetContainsSize = (reader.uint(2) === 0);
reader.skip(-4);
if (packetContainsSize) {
const size = reader.uint(4);
this.logger.debug("Received packet size: " + size);
}
while(!reader.done()) {
const key = reader.string();
let value = this.stripColors(reader.string());
if(key === 'si_map') {
value = value.replace('maps/','');
value = value.replace('.entities','');
}
if(!key) break;
state.raw[key] = value;
this.logger.debug(key + "=" + value);
}
const isEtqw = state.raw.gamename && state.raw.gamename.toLowerCase().includes('etqw');
const rest = reader.rest();
let playerResult = this.attemptPlayerParse(rest, isEtqw, false, false, false);
if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, false, false);
if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, true, true);
if (!playerResult) {
throw new Error("Unable to find a suitable parse strategy for player list");
}
let players;
[players,reader] = playerResult;
for (const player of players) {
if(!player.ping || player.typeflag)
state.bots.push(player);
else
state.players.push(player);
}
state.raw.osmask = reader.uint(4);
if (isEtqw) {
state.raw.ranked = reader.uint(1);
state.raw.timeleft = reader.uint(4);
state.raw.gamestate = reader.uint(1);
state.raw.servertype = reader.uint(1);
// 0 = regular, 1 = tv
if(state.raw.servertype === 0) {
state.raw.interestedClients = reader.uint(1);
} else if(state.raw.servertype === 1) {
state.raw.connectedClients = reader.uint(4);
state.raw.maxClients = reader.uint(4);
}
}
if (state.raw.si_name) state.name = state.raw.si_name;
if (state.raw.si_map) state.map = state.raw.si_map;
if (state.raw.si_maxplayers) state.maxplayers = parseInt(state.raw.si_maxplayers);
if (state.raw.si_maxPlayers) state.maxplayers = parseInt(state.raw.si_maxplayers);
if (state.raw.si_usepass === '1') state.password = true;
if (state.raw.si_needPass === '1') state.password = true;
if (this.options.port === 27733) state.gamePort = 3074; // etqw has a different query and game port
}
attemptPlayerParse(rest, isEtqw, hasClanTag, hasClanTagPos, hasTypeFlag) {
this.logger.debug("starting player parse attempt:");
this.logger.debug("isEtqw: " + isEtqw);
this.logger.debug("hasClanTag: " + hasClanTag);
this.logger.debug("hasClanTagPos: " + hasClanTagPos);
this.logger.debug("hasTypeFlag: " + hasTypeFlag);
const reader = this.reader(rest);
let lastId = -1;
const players = [];
while(true) {
this.logger.debug("---");
if (reader.done()) {
this.logger.debug("* aborting attempt, overran buffer *");
return null;
}
const player = {};
player.id = reader.uint(1);
this.logger.debug("id: " + player.id);
if (player.id <= lastId || player.id > 0x20) {
this.logger.debug("* aborting attempt, invalid player id *");
return null;
}
lastId = player.id;
if(player.id === 0x20) {
this.logger.debug("* player parse successful *");
break;
}
player.ping = reader.uint(2);
this.logger.debug("ping: " + player.ping);
if(!isEtqw) {
player.rate = reader.uint(4);
this.logger.debug("rate: " + player.rate);
}
player.name = this.stripColors(reader.string());
this.logger.debug("name: " + player.name);
if(hasClanTag) {
if(hasClanTagPos) {
const clanTagPos = reader.uint(1);
this.logger.debug("clanTagPos: " + clanTagPos);
}
player.clantag = this.stripColors(reader.string());
this.logger.debug("clan tag: " + player.clantag);
}
if(hasTypeFlag) {
player.typeflag = reader.uint(1);
this.logger.debug("type flag: " + player.typeflag);
}
players.push(player);
}
return [players,reader];
}
stripColors(str) {
// uses quake 3 color codes
return str.replace(/\^(X.{6}|.)/g,'');
}
}
import Core from './core.js'
export default class doom3 extends Core {
constructor () {
super()
this.encoding = 'latin1'
}
async run (state) {
const body = await this.udpSend('\xff\xffgetInfo\x00PiNGPoNg\x00', packet => {
const reader = this.reader(packet)
const header = reader.uint(2)
if (header !== 0xffff) return
const header2 = reader.string()
if (header2 !== 'infoResponse') return
const challengePart1 = reader.string(4)
if (challengePart1 !== 'PiNG') return
// some doom3 implementations only return the first 4 bytes of the challenge
const challengePart2 = reader.string(4)
if (challengePart2 !== 'PoNg') reader.skip(-4)
return reader.rest()
})
let reader = this.reader(body)
const protoVersion = reader.uint(4)
state.raw.protocolVersion = (protoVersion >> 16) + '.' + (protoVersion & 0xffff)
// some doom implementations send us a packet size here, some don't (etqw does this)
// we can tell if this is a packet size, because the third and fourth byte will be 0 (no packets are that massive)
reader.skip(2)
const packetContainsSize = (reader.uint(2) === 0)
reader.skip(-4)
if (packetContainsSize) {
const size = reader.uint(4)
this.logger.debug('Received packet size: ' + size)
}
while (!reader.done()) {
const key = reader.string()
let value = this.stripColors(reader.string())
if (key === 'si_map') {
value = value.replace('maps/', '')
value = value.replace('.entities', '')
}
if (!key) break
state.raw[key] = value
this.logger.debug(key + '=' + value)
}
const isEtqw = state.raw.gamename && state.raw.gamename.toLowerCase().includes('etqw')
const rest = reader.rest()
let playerResult = this.attemptPlayerParse(rest, isEtqw, false, false, false)
if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, false, false)
if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, true, true)
if (!playerResult) {
throw new Error('Unable to find a suitable parse strategy for player list')
}
let players;
[players, reader] = playerResult
for (const player of players) {
if (!player.ping || player.typeflag) { state.bots.push(player) } else { state.players.push(player) }
}
state.raw.osmask = reader.uint(4)
if (isEtqw) {
state.raw.ranked = reader.uint(1)
state.raw.timeleft = reader.uint(4)
state.raw.gamestate = reader.uint(1)
state.raw.servertype = reader.uint(1)
// 0 = regular, 1 = tv
if (state.raw.servertype === 0) {
state.raw.interestedClients = reader.uint(1)
} else if (state.raw.servertype === 1) {
state.raw.connectedClients = reader.uint(4)
state.raw.maxClients = reader.uint(4)
}
}
if (state.raw.si_name) state.name = state.raw.si_name
if (state.raw.si_map) state.map = state.raw.si_map
if (state.raw.si_maxplayers) state.maxplayers = parseInt(state.raw.si_maxplayers)
if (state.raw.si_maxPlayers) state.maxplayers = parseInt(state.raw.si_maxplayers)
if (state.raw.si_usepass === '1') state.password = true
if (state.raw.si_needPass === '1') state.password = true
if (this.options.port === 27733) state.gamePort = 3074 // etqw has a different query and game port
}
attemptPlayerParse (rest, isEtqw, hasClanTag, hasClanTagPos, hasTypeFlag) {
this.logger.debug('starting player parse attempt:')
this.logger.debug('isEtqw: ' + isEtqw)
this.logger.debug('hasClanTag: ' + hasClanTag)
this.logger.debug('hasClanTagPos: ' + hasClanTagPos)
this.logger.debug('hasTypeFlag: ' + hasTypeFlag)
const reader = this.reader(rest)
let lastId = -1
const players = []
while (true) {
this.logger.debug('---')
if (reader.done()) {
this.logger.debug('* aborting attempt, overran buffer *')
return null
}
const player = {}
player.id = reader.uint(1)
this.logger.debug('id: ' + player.id)
if (player.id <= lastId || player.id > 0x20) {
this.logger.debug('* aborting attempt, invalid player id *')
return null
}
lastId = player.id
if (player.id === 0x20) {
this.logger.debug('* player parse successful *')
break
}
player.ping = reader.uint(2)
this.logger.debug('ping: ' + player.ping)
if (!isEtqw) {
player.rate = reader.uint(4)
this.logger.debug('rate: ' + player.rate)
}
player.name = this.stripColors(reader.string())
this.logger.debug('name: ' + player.name)
if (hasClanTag) {
if (hasClanTagPos) {
const clanTagPos = reader.uint(1)
this.logger.debug('clanTagPos: ' + clanTagPos)
}
player.clantag = this.stripColors(reader.string())
this.logger.debug('clan tag: ' + player.clantag)
}
if (hasTypeFlag) {
player.typeflag = reader.uint(1)
this.logger.debug('type flag: ' + player.typeflag)
}
players.push(player)
}
return [players, reader]
}
stripColors (str) {
// uses quake 3 color codes
return str.replace(/\^(X.{6}|.)/g, '')
}
}

View file

@ -1,19 +1,19 @@
import Core from './core.js';
export default class eco extends Core {
async run(state) {
if (!this.options.port) this.options.port = 3001;
const request = await this.request({
url: `http://${this.options.address}:${this.options.port}/frontpage`,
responseType: 'json'
});
const serverInfo = request.Info;
state.name = serverInfo.Description;
state.maxplayers = serverInfo.TotalPlayers;
state.password = serverInfo.HasPassword;
state.gamePort = serverInfo.GamePort;
state.raw = serverInfo;
}
}
import Core from './core.js'
export default class eco extends Core {
async run (state) {
if (!this.options.port) this.options.port = 3001
const request = await this.request({
url: `http://${this.options.address}:${this.options.port}/frontpage`,
responseType: 'json'
})
const serverInfo = request.Info
state.name = serverInfo.Description
state.maxplayers = serverInfo.TotalPlayers
state.password = serverInfo.HasPassword
state.gamePort = serverInfo.GamePort
state.raw = serverInfo
}
}

View file

@ -1,37 +1,38 @@
import valve from './valve.js';
export default class ffow extends valve {
constructor() {
super();
this.byteorder = 'be';
this.legacyChallenge = true;
}
async queryInfo(state) {
this.logger.debug("Requesting ffow info ...");
const b = await this.sendPacket(
0x46,
'LSQ',
0x49
);
const reader = this.reader(b);
state.raw.protocol = reader.uint(1);
state.name = reader.string();
state.map = reader.string();
state.raw.mod = reader.string();
state.raw.gamemode = reader.string();
state.raw.description = reader.string();
state.raw.version = reader.string();
state.gamePort = reader.uint(2);
state.raw.numplayers = reader.uint(1);
state.maxplayers = reader.uint(1);
state.raw.listentype = String.fromCharCode(reader.uint(1));
state.raw.environment = String.fromCharCode(reader.uint(1));
state.password = !!reader.uint(1);
state.raw.secure = reader.uint(1);
state.raw.averagefps = reader.uint(1);
state.raw.round = reader.uint(1);
state.raw.maxrounds = reader.uint(1);
state.raw.timeleft = reader.uint(2);
}
}
import valve from './valve.js'
export default class ffow extends valve {
constructor () {
super()
this.byteorder = 'be'
this.legacyChallenge = true
}
async queryInfo (state) {
this.logger.debug('Requesting ffow info ...')
const b = await this.sendPacket(
0x46,
'LSQ',
0x49
)
const reader = this.reader(b)
state.raw.protocol = reader.uint(1)
state.name = reader.string()
state.map = reader.string()
state.raw.mod = reader.string()
state.raw.gamemode = reader.string()
state.raw.description = reader.string()
state.raw.version = reader.string()
state.gamePort = reader.uint(2)
state.raw.numplayers = reader.uint(1)
state.maxplayers = reader.uint(1)
state.raw.listentype = String.fromCharCode(reader.uint(1))
state.raw.environment = String.fromCharCode(reader.uint(1))
state.password = !!reader.uint(1)
state.raw.secure = reader.uint(1)
state.raw.averagefps = reader.uint(1)
state.raw.round = reader.uint(1)
state.raw.maxrounds = reader.uint(1)
state.raw.timeleft = reader.uint(2)
}
}

View file

@ -1,33 +1,33 @@
import quake2 from './quake2.js';
export default class fivem extends quake2 {
constructor() {
super();
this.sendHeader = 'getinfo xxx';
this.responseHeader = 'infoResponse';
this.encoding = 'utf8';
}
async run(state) {
await super.run(state);
{
const json = await this.request({
url: 'http://' + this.options.address + ':' + this.options.port + '/info.json',
responseType: 'json'
});
state.raw.info = json;
}
{
const json = await this.request({
url: 'http://' + this.options.address + ':' + this.options.port + '/players.json',
responseType: 'json'
});
state.raw.players = json;
for (const player of json) {
state.players.push({name: player.name, ping: player.ping});
}
}
}
}
import quake2 from './quake2.js'
export default class fivem extends quake2 {
constructor () {
super()
this.sendHeader = 'getinfo xxx'
this.responseHeader = 'infoResponse'
this.encoding = 'utf8'
}
async run (state) {
await super.run(state)
{
const json = await this.request({
url: 'http://' + this.options.address + ':' + this.options.port + '/info.json',
responseType: 'json'
})
state.raw.info = json
}
{
const json = await this.request({
url: 'http://' + this.options.address + ':' + this.options.port + '/players.json',
responseType: 'json'
})
state.raw.players = json
for (const player of json) {
state.players.push({ name: player.name, ping: player.ping })
}
}
}
}

View file

@ -1,179 +1,179 @@
import Core from './core.js';
const stringKeys = new Set([
'website',
'gametype',
'gamemode',
'player'
]);
function normalizeEntry([key,value]) {
key = key.toLowerCase();
const split = key.split('_');
let keyType = key;
if (split.length === 2 && !isNaN(Number(split[1]))) {
keyType = split[0];
}
if (!stringKeys.has(keyType) && !keyType.includes('name')) { // todo! the latter check might be problematic, fails on key "name_tag_distance_scope"
if (value.toLowerCase() === 'true') {
value = true;
} else if (value.toLowerCase() === 'false') {
value = false;
} else if (value.length && !isNaN(Number(value))) {
value = Number(value);
}
}
return [key,value];
}
export default class gamespy1 extends Core {
constructor() {
super();
this.encoding = 'latin1';
this.byteorder = 'be';
}
async run(state) {
const raw = await this.sendPacket('\\status\\xserverquery');
// Convert all keys to lowercase and normalize value types
const data = Object.fromEntries(Object.entries(raw).map(entry => normalizeEntry(entry)));
state.raw = data;
if ('hostname' in data) state.name = data.hostname;
if ('mapname' in data) state.map = data.mapname;
if (this.trueTest(data.password)) state.password = true;
if ('maxplayers' in data) state.maxplayers = Number(data.maxplayers);
if ('hostport' in data) state.gamePort = Number(data.hostport);
const teamOffByOne = data.gamename === 'bfield1942';
const playersById = {};
const teamNamesById = {};
for (const ident of Object.keys(data)) {
const split = ident.split('_');
if (split.length !== 2) continue;
let key = split[0].toLowerCase();
const id = Number(split[1]);
if (isNaN(id)) continue;
let value = data[ident];
delete data[ident];
if (key !== 'team' && key.startsWith('team')) {
// Info about a team
if (key === 'teamname') {
teamNamesById[id] = value;
} else {
// other team info which we don't track
}
} else {
// Info about a player
if (!(id in playersById)) playersById[id] = {};
if (key === 'playername' || key === 'player') {
key = 'name';
}
if (key === 'team' && !isNaN(value)) { // todo! technically, this NaN check isn't needed.
key = 'teamId';
value += teamOffByOne ? -1 : 0;
}
playersById[id][key] = value;
}
}
state.raw.teams = teamNamesById;
const players = Object.values(playersById);
const seenHashes = new Set();
for (const player of players) {
// Some servers (bf1942) report the same player multiple times (bug?)
// Ignore these duplicates
if (player.keyhash) {
if (seenHashes.has(player.keyhash)) {
this.logger.debug("Rejected player with hash " + player.keyhash + " (Duplicate keyhash)");
continue;
} else {
seenHashes.add(player.keyhash);
}
}
// Convert player's team ID to team name if possible
if (player.hasOwnProperty('teamId')) {
if (Object.keys(teamNamesById).length) {
player.team = teamNamesById[player.teamId] || '';
} else {
player.team = player.teamId;
delete player.teamId;
}
}
state.players.push(player);
}
}
async sendPacket(type) {
let receivedQueryId;
const output = {};
const parts = new Set();
let maxPartNum = 0;
return await this.udpSend(type, buffer => {
const reader = this.reader(buffer);
const str = reader.string(buffer.length);
const split = str.split('\\');
split.shift();
const data = {};
while(split.length) {
const key = split.shift();
const value = split.shift() || '';
data[key] = value;
}
let queryId, partNum;
const partFinal = ('final' in data);
if (data.queryid) {
const split = data.queryid.split('.');
if (split.length >= 2) {
partNum = Number(split[1]);
}
queryId = split[0];
}
delete data.final;
delete data.queryid;
this.logger.debug("Received part num=" + partNum + " queryId=" + queryId + " final=" + partFinal);
if (queryId) {
if (receivedQueryId && receivedQueryId !== queryId) {
this.logger.debug("Rejected packet (Wrong query ID)");
return;
} else if (!receivedQueryId) {
receivedQueryId = queryId;
}
}
if (!partNum) {
partNum = parts.size;
this.logger.debug("No part number received (assigned #" + partNum + ")");
}
if (parts.has(partNum)) {
this.logger.debug("Rejected packet (Duplicate part)");
return;
}
parts.add(partNum);
if (partFinal) {
maxPartNum = partNum;
}
this.logger.debug("Received part #" + partNum + " of " + (maxPartNum ? maxPartNum : "?"));
for(const i of Object.keys(data)) {
output[i] = data[i];
}
if (maxPartNum && parts.size === maxPartNum) {
this.logger.debug("Received all parts");
this.logger.debug(output);
return output;
}
});
}
}
import Core from './core.js'
const stringKeys = new Set([
'website',
'gametype',
'gamemode',
'player'
])
function normalizeEntry ([key, value]) {
key = key.toLowerCase()
const split = key.split('_')
let keyType = key
if (split.length === 2 && !isNaN(Number(split[1]))) {
keyType = split[0]
}
if (!stringKeys.has(keyType) && !keyType.includes('name')) { // todo! the latter check might be problematic, fails on key "name_tag_distance_scope"
if (value.toLowerCase() === 'true') {
value = true
} else if (value.toLowerCase() === 'false') {
value = false
} else if (value.length && !isNaN(Number(value))) {
value = Number(value)
}
}
return [key, value]
}
export default class gamespy1 extends Core {
constructor () {
super()
this.encoding = 'latin1'
this.byteorder = 'be'
}
async run (state) {
const raw = await this.sendPacket('\\status\\xserverquery')
// Convert all keys to lowercase and normalize value types
const data = Object.fromEntries(Object.entries(raw).map(entry => normalizeEntry(entry)))
state.raw = data
if ('hostname' in data) state.name = data.hostname
if ('mapname' in data) state.map = data.mapname
if (this.trueTest(data.password)) state.password = true
if ('maxplayers' in data) state.maxplayers = Number(data.maxplayers)
if ('hostport' in data) state.gamePort = Number(data.hostport)
const teamOffByOne = data.gamename === 'bfield1942'
const playersById = {}
const teamNamesById = {}
for (const ident of Object.keys(data)) {
const split = ident.split('_')
if (split.length !== 2) continue
let key = split[0].toLowerCase()
const id = Number(split[1])
if (isNaN(id)) continue
let value = data[ident]
delete data[ident]
if (key !== 'team' && key.startsWith('team')) {
// Info about a team
if (key === 'teamname') {
teamNamesById[id] = value
} else {
// other team info which we don't track
}
} else {
// Info about a player
if (!(id in playersById)) playersById[id] = {}
if (key === 'playername' || key === 'player') {
key = 'name'
}
if (key === 'team' && !isNaN(value)) { // todo! technically, this NaN check isn't needed.
key = 'teamId'
value += teamOffByOne ? -1 : 0
}
playersById[id][key] = value
}
}
state.raw.teams = teamNamesById
const players = Object.values(playersById)
const seenHashes = new Set()
for (const player of players) {
// Some servers (bf1942) report the same player multiple times (bug?)
// Ignore these duplicates
if (player.keyhash) {
if (seenHashes.has(player.keyhash)) {
this.logger.debug('Rejected player with hash ' + player.keyhash + ' (Duplicate keyhash)')
continue
} else {
seenHashes.add(player.keyhash)
}
}
// Convert player's team ID to team name if possible
if (Object.prototype.hasOwnProperty.call(player, 'teamId')) {
if (Object.keys(teamNamesById).length) {
player.team = teamNamesById[player.teamId] || ''
} else {
player.team = player.teamId
delete player.teamId
}
}
state.players.push(player)
}
}
async sendPacket (type) {
let receivedQueryId
const output = {}
const parts = new Set()
let maxPartNum = 0
return await this.udpSend(type, buffer => {
const reader = this.reader(buffer)
const str = reader.string(buffer.length)
const split = str.split('\\')
split.shift()
const data = {}
while (split.length) {
const key = split.shift()
const value = split.shift() || ''
data[key] = value
}
let queryId, partNum
const partFinal = ('final' in data)
if (data.queryid) {
const split = data.queryid.split('.')
if (split.length >= 2) {
partNum = Number(split[1])
}
queryId = split[0]
}
delete data.final
delete data.queryid
this.logger.debug('Received part num=' + partNum + ' queryId=' + queryId + ' final=' + partFinal)
if (queryId) {
if (receivedQueryId && receivedQueryId !== queryId) {
this.logger.debug('Rejected packet (Wrong query ID)')
return
} else if (!receivedQueryId) {
receivedQueryId = queryId
}
}
if (!partNum) {
partNum = parts.size
this.logger.debug('No part number received (assigned #' + partNum + ')')
}
if (parts.has(partNum)) {
this.logger.debug('Rejected packet (Duplicate part)')
return
}
parts.add(partNum)
if (partFinal) {
maxPartNum = partNum
}
this.logger.debug('Received part #' + partNum + ' of ' + (maxPartNum || '?'))
for (const i of Object.keys(data)) {
output[i] = data[i]
}
if (maxPartNum && parts.size === maxPartNum) {
this.logger.debug('Received all parts')
this.logger.debug(output)
return output
}
})
}
}

View file

@ -1,140 +1,141 @@
import Core from './core.js';
export default class gamespy2 extends Core {
constructor() {
super();
this.encoding = 'latin1';
this.byteorder = 'be';
}
async run(state) {
// Parse info
{
const body = await this.sendPacket([0xff, 0, 0]);
const reader = this.reader(body);
while (!reader.done()) {
const key = reader.string();
const value = reader.string();
if (!key) break;
state.raw[key] = value;
}
if ('hostname' in state.raw) state.name = state.raw.hostname;
if ('mapname' in state.raw) state.map = state.raw.mapname;
if (this.trueTest(state.raw.password)) state.password = true;
if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers);
if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport);
}
// Parse players
{
const body = await this.sendPacket([0, 0xff, 0]);
const reader = this.reader(body);
for (const rawPlayer of this.readFieldData(reader)) {
state.players.push(rawPlayer);
}
}
// Parse teams
{
const body = await this.sendPacket([0, 0, 0xff]);
const reader = this.reader(body);
state.raw.teams = this.readFieldData(reader);
}
// Special case for america's army 1 and 2
// both use gamename = "armygame"
if (state.raw.gamename === 'armygame') {
const stripColor = (str) => {
// uses unreal 2 color codes
return str.replace(/\x1b...|[\x00-\x1a]/g,'');
};
state.name = stripColor(state.name);
state.map = stripColor(state.map);
for(const key of Object.keys(state.raw)) {
if(typeof state.raw[key] === 'string') {
state.raw[key] = stripColor(state.raw[key]);
}
}
for(const player of state.players) {
if(!('name' in player)) continue;
player.name = stripColor(player.name);
}
}
}
async sendPacket(type) {
const request = Buffer.concat([
Buffer.from([0xfe,0xfd,0x00]), // gamespy2
Buffer.from([0x00,0x00,0x00,0x01]), // ping ID
Buffer.from(type)
]);
return await this.udpSend(request, buffer => {
const reader = this.reader(buffer);
const header = reader.uint(1);
if (header !== 0) return;
const pingId = reader.uint(4);
if (pingId !== 1) return;
return reader.rest();
});
}
readFieldData(reader) {
const zero = reader.uint(1); // always 0
const count = reader.uint(1); // number of rows in this data
// some games omit the count byte entirely if it's 0 or at random (like americas army)
// Luckily, count should always be <64, and ascii characters will typically be >64,
// so we can detect this.
if (count > 64) {
reader.skip(-1);
this.logger.debug("Detected missing count byte, rewinding by 1");
} else {
this.logger.debug("Detected row count: " + count);
}
this.logger.debug(() => "Reading fields, starting at: "+reader.rest());
const fields = [];
while(!reader.done()) {
let field = reader.string();
if(!field) break;
fields.push(field);
this.logger.debug("field:"+field);
}
if (!fields.length) return [];
const units = [];
outer: while(!reader.done()) {
const unit = {};
for(let iField = 0; iField < fields.length; iField++) {
let key = fields[iField];
let value = reader.string();
if(!value && iField === 0) break outer;
this.logger.debug("value:"+value);
if(key === 'player_') key = 'name';
else if(key === 'score_') key = 'score';
else if(key === 'deaths_') key = 'deaths';
else if(key === 'ping_') key = 'ping';
else if(key === 'team_') key = 'team';
else if(key === 'kills_') key = 'kills';
else if(key === 'team_t') key = 'name';
else if(key === 'tickets_t') key = 'tickets';
if(
key === 'score' || key === 'deaths'
|| key === 'ping' || key === 'team'
|| key === 'kills' || key === 'tickets'
) {
if(value === '') continue;
value = parseInt(value);
}
unit[key] = value;
}
units.push(unit);
}
return units;
}
}
import Core from './core.js'
export default class gamespy2 extends Core {
constructor () {
super()
this.encoding = 'latin1'
this.byteorder = 'be'
}
async run (state) {
// Parse info
{
const body = await this.sendPacket([0xff, 0, 0])
const reader = this.reader(body)
while (!reader.done()) {
const key = reader.string()
const value = reader.string()
if (!key) break
state.raw[key] = value
}
if ('hostname' in state.raw) state.name = state.raw.hostname
if ('mapname' in state.raw) state.map = state.raw.mapname
if (this.trueTest(state.raw.password)) state.password = true
if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers)
if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport)
}
// Parse players
{
const body = await this.sendPacket([0, 0xff, 0])
const reader = this.reader(body)
for (const rawPlayer of this.readFieldData(reader)) {
state.players.push(rawPlayer)
}
}
// Parse teams
{
const body = await this.sendPacket([0, 0, 0xff])
const reader = this.reader(body)
state.raw.teams = this.readFieldData(reader)
}
// Special case for america's army 1 and 2
// both use gamename = "armygame"
if (state.raw.gamename === 'armygame') {
const stripColor = (str) => {
// uses unreal 2 color codes
return str.replace(/\x1b...|[\x00-\x1a]/g, '')
}
state.name = stripColor(state.name)
state.map = stripColor(state.map)
for (const key of Object.keys(state.raw)) {
if (typeof state.raw[key] === 'string') {
state.raw[key] = stripColor(state.raw[key])
}
}
for (const player of state.players) {
if (!('name' in player)) continue
player.name = stripColor(player.name)
}
}
}
async sendPacket (type) {
const request = Buffer.concat([
Buffer.from([0xfe, 0xfd, 0x00]), // gamespy2
Buffer.from([0x00, 0x00, 0x00, 0x01]), // ping ID
Buffer.from(type)
])
return await this.udpSend(request, buffer => {
const reader = this.reader(buffer)
const header = reader.uint(1)
if (header !== 0) return
const pingId = reader.uint(4)
if (pingId !== 1) return
return reader.rest()
})
}
readFieldData (reader) {
reader.uint(1) // always 0
const count = reader.uint(1) // number of rows in this data
// some games omit the count byte entirely if it's 0 or at random (like americas army)
// Luckily, count should always be <64, and ascii characters will typically be >64,
// so we can detect this.
if (count > 64) {
reader.skip(-1)
this.logger.debug('Detected missing count byte, rewinding by 1')
} else {
this.logger.debug('Detected row count: ' + count)
}
this.logger.debug(() => 'Reading fields, starting at: ' + reader.rest())
const fields = []
while (!reader.done()) {
const field = reader.string()
if (!field) break
fields.push(field)
this.logger.debug('field:' + field)
}
if (!fields.length) return []
const units = []
while (!reader.done()) {
const unit = {}
for (let iField = 0; iField < fields.length; iField++) {
let key = fields[iField]
let value = reader.string()
if (!value && iField === 0) return units
this.logger.debug('value:' + value)
if (key === 'player_') key = 'name'
else if (key === 'score_') key = 'score'
else if (key === 'deaths_') key = 'deaths'
else if (key === 'ping_') key = 'ping'
else if (key === 'team_') key = 'team'
else if (key === 'kills_') key = 'kills'
else if (key === 'team_t') key = 'name'
else if (key === 'tickets_t') key = 'tickets'
if (
key === 'score' || key === 'deaths' ||
key === 'ping' || key === 'team' ||
key === 'kills' || key === 'tickets'
) {
if (value === '') continue
value = parseInt(value)
}
unit[key] = value
}
units.push(unit)
}
return units
}
}

View file

@ -1,196 +1,194 @@
import Core from './core.js';
export default class gamespy3 extends Core {
constructor() {
super();
this.sessionId = 1;
this.encoding = 'latin1';
this.byteorder = 'be';
this.useOnlySingleSplit = false;
this.isJc2mp = false;
}
async run(state) {
const buffer = await this.sendPacket(9, false, false, false);
const reader = this.reader(buffer);
let challenge = parseInt(reader.string());
this.logger.debug("Received challenge key: " + challenge);
if (challenge === 0) {
// Some servers send us a 0 if they don't want a challenge key used
// BF2 does this.
challenge = null;
}
let requestPayload;
if(this.isJc2mp) {
// they completely alter the protocol. because why not.
requestPayload = Buffer.from([0xff,0xff,0xff,0x02]);
} else {
requestPayload = Buffer.from([0xff,0xff,0xff,0x01]);
}
/** @type Buffer[] */
const packets = await this.sendPacket(0,challenge,requestPayload,true);
// iterate over the received packets
// the first packet will start off with k/v pairs, followed with data fields
// the following packets will only have data fields
state.raw.playerTeamInfo = {};
for(let iPacket = 0; iPacket < packets.length; iPacket++) {
const packet = packets[iPacket];
const reader = this.reader(packet);
this.logger.debug("Parsing packet #" + iPacket);
this.logger.debug(packet);
// Parse raw server key/values
if(iPacket === 0) {
while(!reader.done()) {
const key = reader.string();
if(!key) break;
let value = reader.string();
while(value.match(/^p[0-9]+$/)) {
// fix a weird ut3 bug where some keys don't have values
value = reader.string();
}
state.raw[key] = value;
this.logger.debug(key + " = " + value);
}
}
// Parse player, team, item array state
if(this.isJc2mp) {
state.raw.numPlayers2 = reader.uint(2);
while(!reader.done()) {
const player = {};
player.name = reader.string();
player.steamid = reader.string();
player.ping = reader.uint(2);
state.players.push(player);
}
} else {
let firstMode = true;
while(!reader.done()) {
if (reader.uint(1) <= 2) continue;
reader.skip(-1);
let fieldId = reader.string();
if(!fieldId) continue;
const fieldIdSplit = fieldId.split('_');
const fieldName = fieldIdSplit[0];
const itemType = fieldIdSplit.length > 1 ? fieldIdSplit[1] : 'no_';
if(!(itemType in state.raw.playerTeamInfo)) {
state.raw.playerTeamInfo[itemType] = [];
}
const items = state.raw.playerTeamInfo[itemType];
let offset = reader.uint(1);
firstMode = false;
this.logger.debug(() => "Parsing new field: itemType=" + itemType + " fieldName=" + fieldName + " startOffset=" + offset);
while(!reader.done()) {
const item = reader.string();
if(!item) break;
while(items.length <= offset) { items.push({}); }
items[offset][fieldName] = item;
this.logger.debug("* " + item);
offset++;
}
}
}
}
// Turn all that raw state into something useful
if ('hostname' in state.raw) state.name = state.raw.hostname;
else if('servername' in state.raw) state.name = state.raw.servername;
if ('mapname' in state.raw) state.map = state.raw.mapname;
if (state.raw.password === '1') state.password = true;
if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers);
if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport);
if('' in state.raw.playerTeamInfo) {
for (const playerInfo of state.raw.playerTeamInfo['']) {
const player = {};
for(const from of Object.keys(playerInfo)) {
let key = from;
let value = playerInfo[from];
if(key === 'player') key = 'name';
if(key === 'score' || key === 'ping' || key === 'team' || key === 'deaths' || key === 'pid') value = parseInt(value);
player[key] = value;
}
state.players.push(player);
}
}
}
async sendPacket(type,challenge,payload,assemble) {
const challengeLength = challenge === null ? 0 : 4;
const payloadLength = payload ? payload.length : 0;
const b = Buffer.alloc(7 + challengeLength + payloadLength);
b.writeUInt8(0xFE, 0);
b.writeUInt8(0xFD, 1);
b.writeUInt8(type, 2);
b.writeUInt32BE(this.sessionId, 3);
if(challengeLength) b.writeInt32BE(challenge, 7);
if(payloadLength) payload.copy(b, 7+challengeLength);
let numPackets = 0;
const packets = {};
return await this.udpSend(b,(buffer) => {
const reader = this.reader(buffer);
const iType = reader.uint(1);
if(iType !== type) {
this.logger.debug('Skipping packet, type mismatch');
return;
}
const iSessionId = reader.uint(4);
if(iSessionId !== this.sessionId) {
this.logger.debug('Skipping packet, session id mismatch');
return;
}
if(!assemble) {
return reader.rest();
}
if(this.useOnlySingleSplit) {
// has split headers, but they are worthless and only one packet is used
reader.skip(11);
return [reader.rest()];
}
reader.skip(9); // filler data -- usually set to 'splitnum\0'
let id = reader.uint(1);
const last = (id & 0x80);
id = id & 0x7f;
if(last) numPackets = id+1;
reader.skip(1); // "another 'packet number' byte, but isn't understood."
packets[id] = reader.rest();
if(this.debug) {
this.logger.debug("Received packet #"+id + (last ? " (last)" : ""));
}
if(!numPackets || Object.keys(packets).length !== numPackets) return;
// assemble the parts
const list = [];
for(let i = 0; i < numPackets; i++) {
if(!(i in packets)) {
throw new Error('Missing packet #'+i);
}
list.push(packets[i]);
}
return list;
});
}
}
import Core from './core.js'
export default class gamespy3 extends Core {
constructor () {
super()
this.sessionId = 1
this.encoding = 'latin1'
this.byteorder = 'be'
this.useOnlySingleSplit = false
this.isJc2mp = false
}
async run (state) {
const buffer = await this.sendPacket(9, false, false, false)
const reader = this.reader(buffer)
let challenge = parseInt(reader.string())
this.logger.debug('Received challenge key: ' + challenge)
if (challenge === 0) {
// Some servers send us a 0 if they don't want a challenge key used
// BF2 does this.
challenge = null
}
let requestPayload
if (this.isJc2mp) {
// they completely alter the protocol. because why not.
requestPayload = Buffer.from([0xff, 0xff, 0xff, 0x02])
} else {
requestPayload = Buffer.from([0xff, 0xff, 0xff, 0x01])
}
/** @type Buffer[] */
const packets = await this.sendPacket(0, challenge, requestPayload, true)
// iterate over the received packets
// the first packet will start off with k/v pairs, followed with data fields
// the following packets will only have data fields
state.raw.playerTeamInfo = {}
for (let iPacket = 0; iPacket < packets.length; iPacket++) {
const packet = packets[iPacket]
const reader = this.reader(packet)
this.logger.debug('Parsing packet #' + iPacket)
this.logger.debug(packet)
// Parse raw server key/values
if (iPacket === 0) {
while (!reader.done()) {
const key = reader.string()
if (!key) break
let value = reader.string()
while (value.match(/^p[0-9]+$/)) {
// fix a weird ut3 bug where some keys don't have values
value = reader.string()
}
state.raw[key] = value
this.logger.debug(key + ' = ' + value)
}
}
// Parse player, team, item array state
if (this.isJc2mp) {
state.raw.numPlayers2 = reader.uint(2)
while (!reader.done()) {
const player = {}
player.name = reader.string()
player.steamid = reader.string()
player.ping = reader.uint(2)
state.players.push(player)
}
} else {
while (!reader.done()) {
if (reader.uint(1) <= 2) continue
reader.skip(-1)
const fieldId = reader.string()
if (!fieldId) continue
const fieldIdSplit = fieldId.split('_')
const fieldName = fieldIdSplit[0]
const itemType = fieldIdSplit.length > 1 ? fieldIdSplit[1] : 'no_'
if (!(itemType in state.raw.playerTeamInfo)) {
state.raw.playerTeamInfo[itemType] = []
}
const items = state.raw.playerTeamInfo[itemType]
let offset = reader.uint(1)
this.logger.debug(() => 'Parsing new field: itemType=' + itemType + ' fieldName=' + fieldName + ' startOffset=' + offset)
while (!reader.done()) {
const item = reader.string()
if (!item) break
while (items.length <= offset) { items.push({}) }
items[offset][fieldName] = item
this.logger.debug('* ' + item)
offset++
}
}
}
}
// Turn all that raw state into something useful
if ('hostname' in state.raw) state.name = state.raw.hostname
else if ('servername' in state.raw) state.name = state.raw.servername
if ('mapname' in state.raw) state.map = state.raw.mapname
if (state.raw.password === '1') state.password = true
if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers)
if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport)
if ('' in state.raw.playerTeamInfo) {
for (const playerInfo of state.raw.playerTeamInfo['']) {
const player = {}
for (const from of Object.keys(playerInfo)) {
let key = from
let value = playerInfo[from]
if (key === 'player') key = 'name'
if (key === 'score' || key === 'ping' || key === 'team' || key === 'deaths' || key === 'pid') value = parseInt(value)
player[key] = value
}
state.players.push(player)
}
}
}
async sendPacket (type, challenge, payload, assemble) {
const challengeLength = challenge === null ? 0 : 4
const payloadLength = payload ? payload.length : 0
const b = Buffer.alloc(7 + challengeLength + payloadLength)
b.writeUInt8(0xFE, 0)
b.writeUInt8(0xFD, 1)
b.writeUInt8(type, 2)
b.writeUInt32BE(this.sessionId, 3)
if (challengeLength) b.writeInt32BE(challenge, 7)
if (payloadLength) payload.copy(b, 7 + challengeLength)
let numPackets = 0
const packets = {}
return await this.udpSend(b, (buffer) => {
const reader = this.reader(buffer)
const iType = reader.uint(1)
if (iType !== type) {
this.logger.debug('Skipping packet, type mismatch')
return
}
const iSessionId = reader.uint(4)
if (iSessionId !== this.sessionId) {
this.logger.debug('Skipping packet, session id mismatch')
return
}
if (!assemble) {
return reader.rest()
}
if (this.useOnlySingleSplit) {
// has split headers, but they are worthless and only one packet is used
reader.skip(11)
return [reader.rest()]
}
reader.skip(9) // filler data -- usually set to 'splitnum\0'
let id = reader.uint(1)
const last = (id & 0x80)
id = id & 0x7f
if (last) numPackets = id + 1
reader.skip(1) // "another 'packet number' byte, but isn't understood."
packets[id] = reader.rest()
if (this.debug) {
this.logger.debug('Received packet #' + id + (last ? ' (last)' : ''))
}
if (!numPackets || Object.keys(packets).length !== numPackets) return
// assemble the parts
const list = []
for (let i = 0; i < numPackets; i++) {
if (!(i in packets)) {
throw new Error('Missing packet #' + i)
}
list.push(packets[i])
}
return list
})
}
}

View file

@ -1,46 +1,46 @@
import Core from './core.js';
export default class geneshift extends Core {
async run(state) {
await this.tcpPing();
const body = await this.request({
url: 'http://geneshift.net/game/receiveLobby.php'
});
const split = body.split('<br/>');
let found = null;
for(const line of split) {
const fields = line.split('::');
const ip = fields[2];
const port = fields[3];
if(ip === this.options.address && parseInt(port) === this.options.port) {
found = fields;
break;
}
}
if(found === null) {
throw new Error('Server not found in list');
}
state.raw.countrycode = found[0];
state.raw.country = found[1];
state.name = found[4];
state.map = found[5];
state.players.setNum(parseInt(found[6]));
state.maxplayers = parseInt(found[7]);
// fields[8] is unknown?
state.raw.rules = found[9];
state.raw.gamemode = parseInt(found[10]);
state.raw.gangsters = parseInt(found[11]);
state.raw.cashrate = parseInt(found[12]);
state.raw.missions = !!parseInt(found[13]);
state.raw.vehicles = !!parseInt(found[14]);
state.raw.customweapons = !!parseInt(found[15]);
state.raw.friendlyfire = !!parseInt(found[16]);
state.raw.mercs = !!parseInt(found[17]);
// fields[18] is unknown? listen server?
state.raw.version = found[19];
}
}
import Core from './core.js'
export default class geneshift extends Core {
async run (state) {
await this.tcpPing()
const body = await this.request({
url: 'http://geneshift.net/game/receiveLobby.php'
})
const split = body.split('<br/>')
let found = null
for (const line of split) {
const fields = line.split('::')
const ip = fields[2]
const port = fields[3]
if (ip === this.options.address && parseInt(port) === this.options.port) {
found = fields
break
}
}
if (found === null) {
throw new Error('Server not found in list')
}
state.raw.countrycode = found[0]
state.raw.country = found[1]
state.name = found[4]
state.map = found[5]
state.players.setNum(parseInt(found[6]))
state.maxplayers = parseInt(found[7])
// fields[8] is unknown?
state.raw.rules = found[9]
state.raw.gamemode = parseInt(found[10])
state.raw.gangsters = parseInt(found[11])
state.raw.cashrate = parseInt(found[12])
state.raw.missions = !!parseInt(found[13])
state.raw.vehicles = !!parseInt(found[14])
state.raw.customweapons = !!parseInt(found[15])
state.raw.friendlyfire = !!parseInt(found[16])
state.raw.mercs = !!parseInt(found[17])
// fields[18] is unknown? listen server?
state.raw.version = found[19]
}
}

View file

@ -1,8 +1,8 @@
import valve from './valve.js';
export default class goldsrc extends valve {
constructor() {
super();
this.goldsrcInfo = true;
}
}
import valve from './valve.js'
export default class goldsrc extends valve {
constructor () {
super()
this.goldsrcInfo = true
}
}

View file

@ -1,13 +1,14 @@
import quake1 from './quake1.js';
export default class hexen2 extends quake1 {
constructor() {
super();
this.sendHeader = '\xFFstatus\x0a';
this.responseHeader = '\xffn';
}
async run(state) {
await super.run(state);
state.gamePort = this.options.port - 50;
}
}
import quake1 from './quake1.js'
export default class hexen2 extends quake1 {
constructor () {
super()
this.sendHeader = '\xFFstatus\x0a'
this.responseHeader = '\xffn'
}
async run (state) {
await super.run(state)
state.gamePort = this.options.port - 50
}
}

View file

@ -1,53 +1,55 @@
import armagetron from "./armagetron.js";
import ase from "./ase.js";
import assettocorsa from "./assettocorsa.js";
import battlefield from "./battlefield.js";
import buildandshoot from "./buildandshoot.js";
import cs2d from "./cs2d.js";
import discord from "./discord.js";
import doom3 from "./doom3.js";
import eco from "./eco.js";
import ffow from "./ffow.js";
import fivem from "./fivem.js";
import gamespy1 from "./gamespy1.js";
import gamespy2 from "./gamespy2.js";
import gamespy3 from "./gamespy3.js";
import geneshift from "./geneshift.js";
import goldsrc from "./goldsrc.js";
import hexen2 from "./hexen2.js";
import jc2mp from "./jc2mp.js";
import kspdmp from "./kspdmp.js";
import mafia2mp from "./mafia2mp.js";
import mafia2online from "./mafia2online.js";
import minecraft from "./minecraft.js";
import minecraftbedrock from "./minecraftbedrock.js";
import minecraftvanilla from "./minecraftvanilla.js";
import mumble from "./mumble.js";
import mumbleping from "./mumbleping.js";
import nadeo from "./nadeo.js";
import openttd from "./openttd.js";
import quake1 from "./quake1.js";
import quake2 from "./quake2.js";
import quake3 from "./quake3.js";
import rfactor from "./rfactor.js";
import samp from "./samp.js";
import savage2 from "./savage2.js";
import starmade from "./starmade.js";
import starsiege from "./starsiege.js";
import teamspeak2 from "./teamspeak2.js";
import teamspeak3 from "./teamspeak3.js";
import terraria from "./terraria.js";
import tribes1 from "./tribes1.js";
import tribes1master from "./tribes1master.js";
import unreal2 from "./unreal2.js";
import ut3 from "./ut3.js";
import valve from "./valve.js";
import vcmp from "./vcmp.js";
import ventrilo from "./ventrilo.js";
import warsow from "./warsow.js";
export { armagetron, ase, assettocorsa, battlefield, buildandshoot, cs2d, discord, doom3, eco, ffow, fivem, gamespy1,
gamespy2, gamespy3, geneshift, goldsrc, hexen2, jc2mp, kspdmp, mafia2mp, mafia2online, minecraft,
minecraftbedrock, minecraftvanilla, mumble, mumbleping, nadeo, openttd, quake1, quake2, quake3, rfactor, samp,
savage2, starmade, starsiege, teamspeak2, teamspeak3, terraria, tribes1, tribes1master, unreal2, ut3, valve,
vcmp, ventrilo, warsow }
import armagetron from './armagetron.js'
import ase from './ase.js'
import assettocorsa from './assettocorsa.js'
import battlefield from './battlefield.js'
import buildandshoot from './buildandshoot.js'
import cs2d from './cs2d.js'
import discord from './discord.js'
import doom3 from './doom3.js'
import eco from './eco.js'
import ffow from './ffow.js'
import fivem from './fivem.js'
import gamespy1 from './gamespy1.js'
import gamespy2 from './gamespy2.js'
import gamespy3 from './gamespy3.js'
import geneshift from './geneshift.js'
import goldsrc from './goldsrc.js'
import hexen2 from './hexen2.js'
import jc2mp from './jc2mp.js'
import kspdmp from './kspdmp.js'
import mafia2mp from './mafia2mp.js'
import mafia2online from './mafia2online.js'
import minecraft from './minecraft.js'
import minecraftbedrock from './minecraftbedrock.js'
import minecraftvanilla from './minecraftvanilla.js'
import mumble from './mumble.js'
import mumbleping from './mumbleping.js'
import nadeo from './nadeo.js'
import openttd from './openttd.js'
import quake1 from './quake1.js'
import quake2 from './quake2.js'
import quake3 from './quake3.js'
import rfactor from './rfactor.js'
import samp from './samp.js'
import savage2 from './savage2.js'
import starmade from './starmade.js'
import starsiege from './starsiege.js'
import teamspeak2 from './teamspeak2.js'
import teamspeak3 from './teamspeak3.js'
import terraria from './terraria.js'
import tribes1 from './tribes1.js'
import tribes1master from './tribes1master.js'
import unreal2 from './unreal2.js'
import ut3 from './ut3.js'
import valve from './valve.js'
import vcmp from './vcmp.js'
import ventrilo from './ventrilo.js'
import warsow from './warsow.js'
export {
armagetron, ase, assettocorsa, battlefield, buildandshoot, cs2d, discord, doom3, eco, ffow, fivem, gamespy1,
gamespy2, gamespy3, geneshift, goldsrc, hexen2, jc2mp, kspdmp, mafia2mp, mafia2online, minecraft,
minecraftbedrock, minecraftvanilla, mumble, mumbleping, nadeo, openttd, quake1, quake2, quake3, rfactor, samp,
savage2, starmade, starsiege, teamspeak2, teamspeak3, terraria, tribes1, tribes1master, unreal2, ut3, valve,
vcmp, ventrilo, warsow
}

View file

@ -1,18 +1,19 @@
import gamespy3 from './gamespy3.js';
// supposedly, gamespy3 is the "official" query protocol for jcmp,
// but it's broken (requires useOnlySingleSplit), and may not include some player names
export default class jc2mp extends gamespy3 {
constructor() {
super();
this.useOnlySingleSplit = true;
this.isJc2mp = true;
this.encoding = 'utf8';
}
async run(state) {
await super.run(state);
if(!state.players.length && parseInt(state.raw.numplayers)) {
state.players.setNum(parseInt(state.raw.numplayers));
}
}
}
import gamespy3 from './gamespy3.js'
// supposedly, gamespy3 is the "official" query protocol for jcmp,
// but it's broken (requires useOnlySingleSplit), and may not include some player names
export default class jc2mp extends gamespy3 {
constructor () {
super()
this.useOnlySingleSplit = true
this.isJc2mp = true
this.encoding = 'utf8'
}
async run (state) {
await super.run(state)
if (!state.players.length && parseInt(state.raw.numplayers)) {
state.players.setNum(parseInt(state.raw.numplayers))
}
}
}

View file

@ -1,27 +1,27 @@
import Core from './core.js';
export default class kspdmp extends Core {
async run(state) {
const json = await this.request({
url: 'http://'+this.options.address+':'+this.options.port,
responseType: 'json'
});
for (const one of json.players) {
state.players.push({name:one.nickname,team:one.team});
}
for (const key of Object.keys(json)) {
state.raw[key] = json[key];
}
state.name = json.server_name;
state.maxplayers = json.max_players;
state.gamePort = json.port;
if (json.players) {
const split = json.players.split(', ');
for (const name of split) {
state.players.push({name:name});
}
}
}
}
import Core from './core.js'
export default class kspdmp extends Core {
async run (state) {
const json = await this.request({
url: 'http://' + this.options.address + ':' + this.options.port,
responseType: 'json'
})
for (const one of json.players) {
state.players.push({ name: one.nickname, team: one.team })
}
for (const key of Object.keys(json)) {
state.raw[key] = json[key]
}
state.name = json.server_name
state.maxplayers = json.max_players
state.gamePort = json.port
if (json.players) {
const split = json.players.split(', ')
for (const name of split) {
state.players.push({ name })
}
}
}
}

View file

@ -1,41 +1,41 @@
import Core from './core.js';
export default class mafia2mp extends Core {
constructor() {
super();
this.encoding = 'latin1';
this.header = 'M2MP';
this.isMafia2Online = false;
}
async run(state) {
const body = await this.udpSend(this.header,(buffer) => {
const reader = this.reader(buffer);
const header = reader.string(this.header.length);
if (header !== this.header) return;
return reader.rest();
});
const reader = this.reader(body);
state.name = this.readString(reader);
state.raw.numplayers = this.readString(reader);
state.maxplayers = parseInt(this.readString(reader));
state.raw.gamemode = this.readString(reader);
state.password = !!reader.uint(1);
state.gamePort = this.options.port - 1;
while(!reader.done()) {
const player = {};
player.name = this.readString(reader);
if(!player.name) break;
if (this.isMafia2Online) {
player.ping = parseInt(this.readString(reader));
}
state.players.push(player);
}
}
readString(reader) {
return reader.pascalString(1,-1);
}
}
import Core from './core.js'
export default class mafia2mp extends Core {
constructor () {
super()
this.encoding = 'latin1'
this.header = 'M2MP'
this.isMafia2Online = false
}
async run (state) {
const body = await this.udpSend(this.header, (buffer) => {
const reader = this.reader(buffer)
const header = reader.string(this.header.length)
if (header !== this.header) return
return reader.rest()
})
const reader = this.reader(body)
state.name = this.readString(reader)
state.raw.numplayers = this.readString(reader)
state.maxplayers = parseInt(this.readString(reader))
state.raw.gamemode = this.readString(reader)
state.password = !!reader.uint(1)
state.gamePort = this.options.port - 1
while (!reader.done()) {
const player = {}
player.name = this.readString(reader)
if (!player.name) break
if (this.isMafia2Online) {
player.ping = parseInt(this.readString(reader))
}
state.players.push(player)
}
}
readString (reader) {
return reader.pascalString(1, -1)
}
}

View file

@ -1,9 +1,9 @@
import mafia2mp from './mafia2mp.js';
export default class mafia2online extends mafia2mp {
constructor() {
super();
this.header = 'M2Online';
this.isMafia2Online = true;
}
}
import mafia2mp from './mafia2mp.js'
export default class mafia2online extends mafia2mp {
constructor () {
super()
this.header = 'M2Online'
this.isMafia2Online = true
}
}

View file

@ -1,99 +1,99 @@
import Core from './core.js';
import minecraftbedrock from "./minecraftbedrock.js";
import minecraftvanilla from "./minecraftvanilla.js";
import Gamespy3 from "./gamespy3.js";
import {Results} from "../lib/Results.js";
/*
Vanilla servers respond to minecraftvanilla only
Some modded vanilla servers respond to minecraftvanilla and gamespy3, or gamespy3 only
Some bedrock servers respond to gamespy3 only
Some bedrock servers respond to minecraftbedrock only
Unsure if any bedrock servers respond to gamespy3 and minecraftbedrock
*/
export default class minecraft extends Core {
constructor() {
super();
this.srvRecord = "_minecraft._tcp";
}
async run(state) {
/** @type {Promise<Results>[]} */
const promises = [];
const vanillaResolver = new minecraftvanilla();
vanillaResolver.options = this.options;
vanillaResolver.udpSocket = this.udpSocket;
promises.push((async () => {
try { return await vanillaResolver.runOnceSafe(); } catch(e) {}
})());
const gamespyResolver = new Gamespy3();
gamespyResolver.options = {
...this.options,
encoding: 'utf8',
};
gamespyResolver.udpSocket = this.udpSocket;
promises.push((async () => {
try { return await gamespyResolver.runOnceSafe(); } catch(e) {}
})());
const bedrockResolver = new minecraftbedrock();
bedrockResolver.options = this.options;
bedrockResolver.udpSocket = this.udpSocket;
promises.push((async () => {
try { return await bedrockResolver.runOnceSafe(); } catch(e) {}
})());
const [ vanillaState, gamespyState, bedrockState ] = await Promise.all(promises);
state.raw.vanilla = vanillaState;
state.raw.gamespy = gamespyState;
state.raw.bedrock = bedrockState;
if (!vanillaState && !gamespyState && !bedrockState) {
throw new Error('No protocols succeeded');
}
// Ordered from least worth to most worth (player names / etc)
if (bedrockState) {
if (bedrockState.players.length) state.players = bedrockState.players;
}
if (vanillaState) {
try {
let name = '';
const description = vanillaState.raw.description;
if (typeof description === 'string') {
name = description;
}
if (!name && typeof description === 'object' && description.text) {
name = description.text;
}
if (!name && typeof description === 'object' && description.extra) {
name = description.extra.map(part => part.text).join('');
}
state.name = name;
} catch(e) {}
if (vanillaState.maxplayers) state.maxplayers = vanillaState.maxplayers;
if (vanillaState.players.length) state.players = vanillaState.players;
if (vanillaState.ping) this.registerRtt(vanillaState.ping);
}
if (gamespyState) {
if (gamespyState.name) state.name = gamespyState.name;
if (gamespyState.maxplayers) state.maxplayers = gamespyState.maxplayers;
if (gamespyState.players.length) state.players = gamespyState.players;
else if (gamespyState.raw.numplayers) state.players.setNum(parseInt(gamespyState.raw.numplayers));
if (gamespyState.ping) this.registerRtt(gamespyState.ping);
}
if (bedrockState) {
if (bedrockState.name) state.name = bedrockState.name;
if (bedrockState.maxplayers) state.maxplayers = bedrockState.maxplayers;
if (bedrockState.map) state.map = bedrockState.map;
if (bedrockState.ping) this.registerRtt(bedrockState.ping);
}
// remove dupe spaces from name
state.name = state.name.replace(/\s+/g, ' ');
// remove color codes from name
state.name = state.name.replace(/\u00A7./g, '');
}
}
import Core from './core.js'
import minecraftbedrock from './minecraftbedrock.js'
import minecraftvanilla from './minecraftvanilla.js'
import Gamespy3 from './gamespy3.js'
/*
Vanilla servers respond to minecraftvanilla only
Some modded vanilla servers respond to minecraftvanilla and gamespy3, or gamespy3 only
Some bedrock servers respond to gamespy3 only
Some bedrock servers respond to minecraftbedrock only
Unsure if any bedrock servers respond to gamespy3 and minecraftbedrock
*/
export default class minecraft extends Core {
constructor () {
super()
this.srvRecord = '_minecraft._tcp'
}
async run (state) {
/** @type {Promise<Results>[]} */
const promises = []
const vanillaResolver = new minecraftvanilla()
vanillaResolver.options = this.options
vanillaResolver.udpSocket = this.udpSocket
promises.push((async () => {
try { return await vanillaResolver.runOnceSafe() } catch (e) {}
})())
const gamespyResolver = new Gamespy3()
gamespyResolver.options = {
...this.options,
encoding: 'utf8'
}
gamespyResolver.udpSocket = this.udpSocket
promises.push((async () => {
try { return await gamespyResolver.runOnceSafe() } catch (e) {}
})())
const bedrockResolver = new minecraftbedrock()
bedrockResolver.options = this.options
bedrockResolver.udpSocket = this.udpSocket
promises.push((async () => {
try { return await bedrockResolver.runOnceSafe() } catch (e) {}
})())
const [vanillaState, gamespyState, bedrockState] = await Promise.all(promises)
state.raw.vanilla = vanillaState
state.raw.gamespy = gamespyState
state.raw.bedrock = bedrockState
if (!vanillaState && !gamespyState && !bedrockState) {
throw new Error('No protocols succeeded')
}
// Ordered from least worth to most worth (player names / etc)
if (bedrockState) {
if (bedrockState.players.length) state.players = bedrockState.players
}
if (vanillaState) {
try {
let name = ''
const description = vanillaState.raw.description
if (typeof description === 'string') {
name = description
}
if (!name && typeof description === 'object' && description.text) {
name = description.text
}
if (!name && typeof description === 'object' && description.extra) {
name = description.extra.map(part => part.text).join('')
}
state.name = name
} catch (e) {}
if (vanillaState.maxplayers) state.maxplayers = vanillaState.maxplayers
if (vanillaState.players.length) state.players = vanillaState.players
if (vanillaState.ping) this.registerRtt(vanillaState.ping)
}
if (gamespyState) {
if (gamespyState.name) state.name = gamespyState.name
if (gamespyState.maxplayers) state.maxplayers = gamespyState.maxplayers
if (gamespyState.players.length) state.players = gamespyState.players
else if (gamespyState.raw.numplayers) state.players.setNum(parseInt(gamespyState.raw.numplayers))
if (gamespyState.ping) this.registerRtt(gamespyState.ping)
}
if (bedrockState) {
if (bedrockState.name) state.name = bedrockState.name
if (bedrockState.maxplayers) state.maxplayers = bedrockState.maxplayers
if (bedrockState.map) state.map = bedrockState.map
if (bedrockState.ping) this.registerRtt(bedrockState.ping)
}
// remove dupe spaces from name
state.name = state.name.replace(/\s+/g, ' ')
// remove color codes from name
state.name = state.name.replace(/\u00A7./g, '')
}
}

View file

@ -1,72 +1,72 @@
import Core from './core.js';
export default class minecraftbedrock extends Core {
constructor() {
super();
this.byteorder = 'be';
}
async run(state) {
const bufs = [
Buffer.from([0x01]), // Message ID, ID_UNCONNECTED_PING
Buffer.from('1122334455667788', 'hex'), // Nonce / timestamp
Buffer.from('00ffff00fefefefefdfdfdfd12345678', 'hex'), // Magic
Buffer.from('0000000000000000', 'hex') // Cliend GUID
];
return await this.udpSend(Buffer.concat(bufs), buffer => {
const reader = this.reader(buffer);
const messageId = reader.uint(1);
if (messageId !== 0x1c) {
this.logger.debug('Skipping packet, invalid message id');
return;
}
const nonce = reader.part(8).toString('hex'); // should match the nonce we sent
this.logger.debug('Nonce: ' + nonce);
if (nonce !== '1122334455667788') {
this.logger.debug('Skipping packet, invalid nonce');
return;
}
// These 8 bytes are identical to the serverId string we receive in decimal below
reader.skip(8);
const magic = reader.part(16).toString('hex');
this.logger.debug('Magic value: ' + magic);
if (magic !== '00ffff00fefefefefdfdfdfd12345678') {
this.logger.debug('Skipping packet, invalid magic');
return;
}
const statusLen = reader.uint(2);
if (reader.remaining() !== statusLen) {
throw new Error('Invalid status length: ' + reader.remaining() + ' vs ' + statusLen);
}
const statusStr = reader.rest().toString('utf8');
this.logger.debug('Raw status str: ' + statusStr);
const split = statusStr.split(';');
if (split.length < 6) {
throw new Error('Missing enough chunks in status str');
}
state.raw.edition = split.shift();
state.name = split.shift();
state.raw.protocolVersion = split.shift();
state.raw.mcVersion = split.shift();
state.players.setNum(parseInt(split.shift()));
state.maxplayers = parseInt(split.shift());
if (split.length) state.raw.serverId = split.shift();
if (split.length) state.map = split.shift();
if (split.length) state.raw.gameMode = split.shift();
if (split.length) state.raw.nintendoOnly = !!parseInt(split.shift());
if (split.length) state.raw.ipv4Port = split.shift();
if (split.length) state.raw.ipv6Port = split.shift();
return true;
});
}
}
import Core from './core.js'
export default class minecraftbedrock extends Core {
constructor () {
super()
this.byteorder = 'be'
}
async run (state) {
const bufs = [
Buffer.from([0x01]), // Message ID, ID_UNCONNECTED_PING
Buffer.from('1122334455667788', 'hex'), // Nonce / timestamp
Buffer.from('00ffff00fefefefefdfdfdfd12345678', 'hex'), // Magic
Buffer.from('0000000000000000', 'hex') // Cliend GUID
]
return await this.udpSend(Buffer.concat(bufs), buffer => {
const reader = this.reader(buffer)
const messageId = reader.uint(1)
if (messageId !== 0x1c) {
this.logger.debug('Skipping packet, invalid message id')
return
}
const nonce = reader.part(8).toString('hex') // should match the nonce we sent
this.logger.debug('Nonce: ' + nonce)
if (nonce !== '1122334455667788') {
this.logger.debug('Skipping packet, invalid nonce')
return
}
// These 8 bytes are identical to the serverId string we receive in decimal below
reader.skip(8)
const magic = reader.part(16).toString('hex')
this.logger.debug('Magic value: ' + magic)
if (magic !== '00ffff00fefefefefdfdfdfd12345678') {
this.logger.debug('Skipping packet, invalid magic')
return
}
const statusLen = reader.uint(2)
if (reader.remaining() !== statusLen) {
throw new Error('Invalid status length: ' + reader.remaining() + ' vs ' + statusLen)
}
const statusStr = reader.rest().toString('utf8')
this.logger.debug('Raw status str: ' + statusStr)
const split = statusStr.split(';')
if (split.length < 6) {
throw new Error('Missing enough chunks in status str')
}
state.raw.edition = split.shift()
state.name = split.shift()
state.raw.protocolVersion = split.shift()
state.raw.mcVersion = split.shift()
state.players.setNum(parseInt(split.shift()))
state.maxplayers = parseInt(split.shift())
if (split.length) state.raw.serverId = split.shift()
if (split.length) state.map = split.shift()
if (split.length) state.raw.gameMode = split.shift()
if (split.length) state.raw.nintendoOnly = !!parseInt(split.shift())
if (split.length) state.raw.ipv4Port = split.shift()
if (split.length) state.raw.ipv6Port = split.shift()
return true
})
}
}

View file

@ -1,80 +1,81 @@
import Core from './core.js';
import Varint from "varint";
export default class minecraftvanilla extends Core {
async run(state) {
const portBuf = Buffer.alloc(2);
portBuf.writeUInt16BE(this.options.port,0);
const addressBuf = Buffer.from(this.options.host,'utf8');
const bufs = [
this.varIntBuffer(47),
this.varIntBuffer(addressBuf.length),
addressBuf,
portBuf,
this.varIntBuffer(1)
];
const outBuffer = Buffer.concat([
this.buildPacket(0,Buffer.concat(bufs)),
this.buildPacket(0)
]);
const data = await this.withTcp(async socket => {
return await this.tcpSend(socket, outBuffer, data => {
if(data.length < 10) return;
const reader = this.reader(data);
const length = reader.varint();
if(data.length < length) return;
return reader.rest();
});
});
const reader = this.reader(data);
const packetId = reader.varint();
this.logger.debug("Packet ID: "+packetId);
const strLen = reader.varint();
this.logger.debug("String Length: "+strLen);
const str = reader.rest().toString('utf8');
this.logger.debug(str);
const json = JSON.parse(str);
delete json.favicon;
state.raw = json;
state.maxplayers = json.players.max;
if(json.players.sample) {
for(const player of json.players.sample) {
state.players.push({
id: player.id,
name: player.name
});
}
}
// players.sample may not contain all players or no players at all, depending on how many players are online.
// Insert a dummy player object for every online player that is not listed in players.sample.
// Limit player amount to 10.000 players for performance reasons.
for (let i = state.players.length; i < Math.min(json.players.online, 10000); i++) {
state.players.push({});
}
}
varIntBuffer(num) {
return Buffer.from(Varint.encode(num));
}
buildPacket(id,data) {
if(!data) data = Buffer.from([]);
const idBuffer = this.varIntBuffer(id);
return Buffer.concat([
this.varIntBuffer(data.length+idBuffer.length),
idBuffer,
data
]);
}
}
import Core from './core.js'
import Varint from 'varint'
export default class minecraftvanilla extends Core {
async run (state) {
const portBuf = Buffer.alloc(2)
portBuf.writeUInt16BE(this.options.port, 0)
const addressBuf = Buffer.from(this.options.host, 'utf8')
const bufs = [
this.varIntBuffer(47),
this.varIntBuffer(addressBuf.length),
addressBuf,
portBuf,
this.varIntBuffer(1)
]
const outBuffer = Buffer.concat([
this.buildPacket(0, Buffer.concat(bufs)),
this.buildPacket(0)
])
const data = await this.withTcp(async socket => {
return await this.tcpSend(socket, outBuffer, data => {
if (data.length < 10) return
const reader = this.reader(data)
const length = reader.varint()
if (data.length < length) return
return reader.rest()
})
})
const reader = this.reader(data)
const packetId = reader.varint()
this.logger.debug('Packet ID: ' + packetId)
const strLen = reader.varint()
this.logger.debug('String Length: ' + strLen)
const str = reader.rest().toString('utf8')
this.logger.debug(str)
const json = JSON.parse(str)
delete json.favicon
state.raw = json
state.maxplayers = json.players.max
if (json.players.sample) {
for (const player of json.players.sample) {
state.players.push({
id: player.id,
name: player.name
})
}
}
// players.sample may not contain all players or no players at all, depending on how many players are online.
// Insert a dummy player object for every online player that is not listed in players.sample.
// Limit player amount to 10.000 players for performance reasons.
for (let i = state.players.length; i < Math.min(json.players.online, 10000); i++) {
state.players.push({})
}
}
varIntBuffer (num) {
return Buffer.from(Varint.encode(num))
}
buildPacket (id, data) {
if (!data) data = Buffer.from([])
const idBuffer = this.varIntBuffer(id)
return Buffer.concat([
this.varIntBuffer(data.length + idBuffer.length),
idBuffer,
data
])
}
}

View file

@ -1,39 +1,39 @@
import Core from './core.js';
export default class mumble extends Core {
async run(state) {
const json = await this.withTcp(async socket => {
return await this.tcpSend(socket, 'json', (buffer) => {
if (buffer.length < 10) return;
const str = buffer.toString();
let json;
try {
json = JSON.parse(str);
} catch (e) {
// probably not all here yet
return;
}
return json;
});
});
state.raw = json;
state.name = json.name;
state.gamePort = json.x_gtmurmur_connectport || 64738;
let channelStack = [state.raw.root];
while(channelStack.length) {
const channel = channelStack.shift();
channel.description = this.cleanComment(channel.description);
channelStack = channelStack.concat(channel.channels);
for(const user of channel.users) {
user.comment = this.cleanComment(user.comment);
state.players.push(user);
}
}
}
cleanComment(str) {
return str.replace(/<.*>/g,'');
}
}
import Core from './core.js'
export default class mumble extends Core {
async run (state) {
const json = await this.withTcp(async socket => {
return await this.tcpSend(socket, 'json', (buffer) => {
if (buffer.length < 10) return
const str = buffer.toString()
let json
try {
json = JSON.parse(str)
} catch (e) {
// probably not all here yet
return
}
return json
})
})
state.raw = json
state.name = json.name
state.gamePort = json.x_gtmurmur_connectport || 64738
let channelStack = [state.raw.root]
while (channelStack.length) {
const channel = channelStack.shift()
channel.description = this.cleanComment(channel.description)
channelStack = channelStack.concat(channel.channels)
for (const user of channel.users) {
user.comment = this.cleanComment(user.comment)
state.players.push(user)
}
}
}
cleanComment (str) {
return str.replace(/<.*>/g, '')
}
}

View file

@ -1,24 +1,24 @@
import Core from './core.js';
export default class mumbleping extends Core {
constructor() {
super();
this.byteorder = 'be';
}
async run(state) {
const data = await this.udpSend('\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08', (buffer) => {
if (buffer.length >= 24) return buffer;
});
const reader = this.reader(data);
reader.skip(1);
state.raw.versionMajor = reader.uint(1);
state.raw.versionMinor = reader.uint(1);
state.raw.versionPatch = reader.uint(1);
reader.skip(8);
state.players.setNum(reader.uint(4));
state.maxplayers = reader.uint(4);
state.raw.allowedbandwidth = reader.uint(4);
}
}
import Core from './core.js'
export default class mumbleping extends Core {
constructor () {
super()
this.byteorder = 'be'
}
async run (state) {
const data = await this.udpSend('\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08', (buffer) => {
if (buffer.length >= 24) return buffer
})
const reader = this.reader(data)
reader.skip(1)
state.raw.versionMajor = reader.uint(1)
state.raw.versionMinor = reader.uint(1)
state.raw.versionPatch = reader.uint(1)
reader.skip(8)
state.players.setNum(reader.uint(4))
state.maxplayers = reader.uint(4)
state.raw.allowedbandwidth = reader.uint(4)
}
}

View file

@ -1,85 +1,85 @@
import Core from './core.js';
import Promises from "../lib/Promises.js";
import * as gbxremote from 'gbxremote';
export default class nadeo extends Core {
async run(state) {
await this.withClient(async client => {
const start = Date.now();
await this.query(client, 'Authenticate', this.options.login, this.options.password);
this.registerRtt(Date.now()-start);
//const data = this.methodCall(client, 'GetStatus');
{
const results = await this.query(client, 'GetServerOptions');
state.name = this.stripColors(results.Name);
state.password = (results.Password !== 'No password');
state.maxplayers = results.CurrentMaxPlayers;
state.raw.maxspectators = results.CurrentMaxSpectators;
}
{
const results = await this.query(client, 'GetCurrentMapInfo');
state.map = this.stripColors(results.Name);
state.raw.mapUid = results.UId;
}
{
const results = await this.query(client, 'GetCurrentGameInfo');
let gamemode = '';
const igm = results.GameMode;
if(igm === 0) gamemode="Rounds";
if(igm === 1) gamemode="Time Attack";
if(igm === 2) gamemode="Team";
if(igm === 3) gamemode="Laps";
if(igm === 4) gamemode="Stunts";
if(igm === 5) gamemode="Cup";
state.raw.gametype = gamemode;
state.raw.mapcount = results.NbChallenge;
}
{
const results = await this.query(client, 'GetNextMapInfo');
state.raw.nextmapName = this.stripColors(results.Name);
state.raw.nextmapUid = results.UId;
}
if (this.options.port === 5000) {
state.gamePort = 2350;
}
state.raw.players = await this.query(client, 'GetPlayerList', 10000, 0);
for (const player of state.raw.players) {
state.players.push({
name:this.stripColors(player.Name || player.NickName)
});
}
});
}
async withClient(fn) {
const socket = new gbxremote.Client(this.options.port, this.options.host);
try {
const connectPromise = socket.connect();
const timeoutPromise = Promises.createTimeout(this.options.socketTimeout, 'GBX Remote Opening');
await Promise.race([connectPromise, timeoutPromise, this.abortedPromise]);
return await fn(socket);
} finally {
socket.terminate();
}
}
async query(client, ...cmdset) {
const cmd = cmdset[0];
const params = cmdset.slice(1);
const sentPromise = client.query(cmd, params);
const timeoutPromise = Promises.createTimeout(this.options.socketTimeout, 'GBX Method Call');
return await Promise.race([sentPromise, timeoutPromise, this.abortedPromise]);
}
stripColors(str) {
return str.replace(/\$([0-9a-f]{3}|[a-z])/gi,'');
}
}
import Core from './core.js'
import Promises from '../lib/Promises.js'
import * as gbxremote from 'gbxremote'
export default class nadeo extends Core {
async run (state) {
await this.withClient(async client => {
const start = Date.now()
await this.query(client, 'Authenticate', this.options.login, this.options.password)
this.registerRtt(Date.now() - start)
// const data = this.methodCall(client, 'GetStatus');
{
const results = await this.query(client, 'GetServerOptions')
state.name = this.stripColors(results.Name)
state.password = (results.Password !== 'No password')
state.maxplayers = results.CurrentMaxPlayers
state.raw.maxspectators = results.CurrentMaxSpectators
}
{
const results = await this.query(client, 'GetCurrentMapInfo')
state.map = this.stripColors(results.Name)
state.raw.mapUid = results.UId
}
{
const results = await this.query(client, 'GetCurrentGameInfo')
let gamemode = ''
const igm = results.GameMode
if (igm === 0) gamemode = 'Rounds'
if (igm === 1) gamemode = 'Time Attack'
if (igm === 2) gamemode = 'Team'
if (igm === 3) gamemode = 'Laps'
if (igm === 4) gamemode = 'Stunts'
if (igm === 5) gamemode = 'Cup'
state.raw.gametype = gamemode
state.raw.mapcount = results.NbChallenge
}
{
const results = await this.query(client, 'GetNextMapInfo')
state.raw.nextmapName = this.stripColors(results.Name)
state.raw.nextmapUid = results.UId
}
if (this.options.port === 5000) {
state.gamePort = 2350
}
state.raw.players = await this.query(client, 'GetPlayerList', 10000, 0)
for (const player of state.raw.players) {
state.players.push({
name: this.stripColors(player.Name || player.NickName)
})
}
})
}
async withClient (fn) {
const socket = new gbxremote.Client(this.options.port, this.options.host)
try {
const connectPromise = socket.connect()
const timeoutPromise = Promises.createTimeout(this.options.socketTimeout, 'GBX Remote Opening')
await Promise.race([connectPromise, timeoutPromise, this.abortedPromise])
return await fn(socket)
} finally {
socket.terminate()
}
}
async query (client, ...cmdset) {
const cmd = cmdset[0]
const params = cmdset.slice(1)
const sentPromise = client.query(cmd, params)
const timeoutPromise = Promises.createTimeout(this.options.socketTimeout, 'GBX Method Call')
return await Promise.race([sentPromise, timeoutPromise, this.abortedPromise])
}
stripColors (str) {
return str.replace(/\$([0-9a-f]{3}|[a-z])/gi, '')
}
}

View file

@ -1,127 +1,127 @@
import Core from './core.js';
export default class openttd extends Core {
async run(state) {
{
const [reader, version] = await this.query(0, 1, 1, 4);
if (version >= 4) {
const numGrf = reader.uint(1);
state.raw.grfs = [];
for (let i = 0; i < numGrf; i++) {
const grf = {};
grf.id = reader.part(4).toString('hex');
grf.md5 = reader.part(16).toString('hex');
state.raw.grfs.push(grf);
}
}
if (version >= 3) {
state.raw.date_current = this.readDate(reader);
state.raw.date_start = this.readDate(reader);
}
if (version >= 2) {
state.raw.maxcompanies = reader.uint(1);
state.raw.numcompanies = reader.uint(1);
state.raw.maxspectators = reader.uint(1);
}
state.name = reader.string();
state.raw.version = reader.string();
state.raw.language = this.decode(
reader.uint(1),
['any', 'en', 'de', 'fr']
);
state.password = !!reader.uint(1);
state.maxplayers = reader.uint(1);
state.players.setNum(reader.uint(1));
state.raw.numspectators = reader.uint(1);
state.map = reader.string();
state.raw.map_width = reader.uint(2);
state.raw.map_height = reader.uint(2);
state.raw.landscape = this.decode(
reader.uint(1),
['temperate', 'arctic', 'desert', 'toyland']
);
state.raw.dedicated = !!reader.uint(1);
}
{
const [reader,version] = await this.query(2,3,-1,-1);
// we don't know how to deal with companies outside version 6
if(version === 6) {
state.raw.companies = [];
const numCompanies = reader.uint(1);
for (let iCompany = 0; iCompany < numCompanies; iCompany++) {
const company = {};
company.id = reader.uint(1);
company.name = reader.string();
company.year_start = reader.uint(4);
company.value = reader.uint(8).toString();
company.money = reader.uint(8).toString();
company.income = reader.uint(8).toString();
company.performance = reader.uint(2);
company.password = !!reader.uint(1);
const vehicle_types = ['train', 'truck', 'bus', 'aircraft', 'ship'];
const station_types = ['station', 'truckbay', 'busstation', 'airport', 'dock'];
company.vehicles = {};
for (const type of vehicle_types) {
company.vehicles[type] = reader.uint(2);
}
company.stations = {};
for (const type of station_types) {
company.stations[type] = reader.uint(2);
}
company.clients = reader.string();
state.raw.companies.push(company);
}
}
}
}
async query(type,expected,minver,maxver) {
const b = Buffer.from([0x03,0x00,type]);
return await this.udpSend(b,(buffer) => {
const reader = this.reader(buffer);
const packetLen = reader.uint(2);
if(packetLen !== buffer.length) {
this.logger.debug('Invalid reported packet length: '+packetLen+' '+buffer.length);
return;
}
const packetType = reader.uint(1);
if(packetType !== expected) {
this.logger.debug('Unexpected response packet type: '+packetType);
return;
}
const protocolVersion = reader.uint(1);
if((minver !== -1 && protocolVersion < minver) || (maxver !== -1 && protocolVersion > maxver)) {
throw new Error('Unknown protocol version: '+protocolVersion+' Expected: '+minver+'-'+maxver);
}
return [reader,protocolVersion];
});
}
readDate(reader) {
const daysSinceZero = reader.uint(4);
const temp = new Date(0,0,1);
temp.setFullYear(0);
temp.setDate(daysSinceZero+1);
return temp.toISOString().split('T')[0];
}
decode(num,arr) {
if(num < 0 || num >= arr.length) {
return num;
}
return arr[num];
}
}
import Core from './core.js'
export default class openttd extends Core {
async run (state) {
{
const [reader, version] = await this.query(0, 1, 1, 4)
if (version >= 4) {
const numGrf = reader.uint(1)
state.raw.grfs = []
for (let i = 0; i < numGrf; i++) {
const grf = {}
grf.id = reader.part(4).toString('hex')
grf.md5 = reader.part(16).toString('hex')
state.raw.grfs.push(grf)
}
}
if (version >= 3) {
state.raw.date_current = this.readDate(reader)
state.raw.date_start = this.readDate(reader)
}
if (version >= 2) {
state.raw.maxcompanies = reader.uint(1)
state.raw.numcompanies = reader.uint(1)
state.raw.maxspectators = reader.uint(1)
}
state.name = reader.string()
state.raw.version = reader.string()
state.raw.language = this.decode(
reader.uint(1),
['any', 'en', 'de', 'fr']
)
state.password = !!reader.uint(1)
state.maxplayers = reader.uint(1)
state.players.setNum(reader.uint(1))
state.raw.numspectators = reader.uint(1)
state.map = reader.string()
state.raw.map_width = reader.uint(2)
state.raw.map_height = reader.uint(2)
state.raw.landscape = this.decode(
reader.uint(1),
['temperate', 'arctic', 'desert', 'toyland']
)
state.raw.dedicated = !!reader.uint(1)
}
{
const [reader, version] = await this.query(2, 3, -1, -1)
// we don't know how to deal with companies outside version 6
if (version === 6) {
state.raw.companies = []
const numCompanies = reader.uint(1)
for (let iCompany = 0; iCompany < numCompanies; iCompany++) {
const company = {}
company.id = reader.uint(1)
company.name = reader.string()
company.year_start = reader.uint(4)
company.value = reader.uint(8).toString()
company.money = reader.uint(8).toString()
company.income = reader.uint(8).toString()
company.performance = reader.uint(2)
company.password = !!reader.uint(1)
const vehicleTypes = ['train', 'truck', 'bus', 'aircraft', 'ship']
const stationTypes = ['station', 'truckbay', 'busstation', 'airport', 'dock']
company.vehicles = {}
for (const type of vehicleTypes) {
company.vehicles[type] = reader.uint(2)
}
company.stations = {}
for (const type of stationTypes) {
company.stations[type] = reader.uint(2)
}
company.clients = reader.string()
state.raw.companies.push(company)
}
}
}
}
async query (type, expected, minver, maxver) {
const b = Buffer.from([0x03, 0x00, type])
return await this.udpSend(b, (buffer) => {
const reader = this.reader(buffer)
const packetLen = reader.uint(2)
if (packetLen !== buffer.length) {
this.logger.debug('Invalid reported packet length: ' + packetLen + ' ' + buffer.length)
return
}
const packetType = reader.uint(1)
if (packetType !== expected) {
this.logger.debug('Unexpected response packet type: ' + packetType)
return
}
const protocolVersion = reader.uint(1)
if ((minver !== -1 && protocolVersion < minver) || (maxver !== -1 && protocolVersion > maxver)) {
throw new Error('Unknown protocol version: ' + protocolVersion + ' Expected: ' + minver + '-' + maxver)
}
return [reader, protocolVersion]
})
}
readDate (reader) {
const daysSinceZero = reader.uint(4)
const temp = new Date(0, 0, 1)
temp.setFullYear(0)
temp.setDate(daysSinceZero + 1)
return temp.toISOString().split('T')[0]
}
decode (num, arr) {
if (num < 0 || num >= arr.length) {
return num
}
return arr[num]
}
}

View file

@ -1,9 +1,9 @@
import quake2 from './quake2.js';
export default class quake1 extends quake2 {
constructor() {
super();
this.responseHeader = 'n';
this.isQuake1 = true;
}
}
import quake2 from './quake2.js'
export default class quake1 extends quake2 {
constructor () {
super()
this.responseHeader = 'n'
this.isQuake1 = true
}
}

View file

@ -1,86 +1,86 @@
import Core from './core.js';
export default class quake2 extends Core {
constructor() {
super();
this.encoding = 'latin1';
this.delimiter = '\n';
this.sendHeader = 'status';
this.responseHeader = 'print';
this.isQuake1 = false;
}
async run(state) {
const body = await this.udpSend('\xff\xff\xff\xff'+this.sendHeader+'\x00', packet => {
const reader = this.reader(packet);
const header = reader.string({length: 4, encoding: 'latin1'});
if (header !== '\xff\xff\xff\xff') return;
let type;
if (this.isQuake1) {
type = reader.string(this.responseHeader.length);
} else {
type = reader.string({encoding: 'latin1'});
}
if (type !== this.responseHeader) return;
return reader.rest();
});
const reader = this.reader(body);
const info = reader.string().split('\\');
if(info[0] === '') info.shift();
while(true) {
const key = info.shift();
const value = info.shift();
if(typeof value === 'undefined') break;
state.raw[key] = value;
}
while(!reader.done()) {
const line = reader.string();
if(!line || line.charAt(0) === '\0') break;
const args = [];
const split = line.split('"');
split.forEach((part,i) => {
const inQuote = (i%2 === 1);
if(inQuote) {
args.push(part);
} else {
const splitSpace = part.split(' ');
for (const subpart of splitSpace) {
if(subpart) args.push(subpart);
}
}
});
const player = {};
if(this.isQuake1) {
player.id = parseInt(args.shift());
player.score = parseInt(args.shift());
player.time = parseInt(args.shift());
player.ping = parseInt(args.shift());
player.name = args.shift();
player.skin = args.shift();
player.color1 = parseInt(args.shift());
player.color2 = parseInt(args.shift());
} else {
player.frags = parseInt(args.shift());
player.ping = parseInt(args.shift());
player.name = args.shift() || '';
if (!player.name) delete player.name;
player.address = args.shift() || '';
if (!player.address) delete player.address;
}
(player.ping ? state.players : state.bots).push(player);
}
if('g_needpass' in state.raw) state.password = state.raw.g_needpass;
if('mapname' in state.raw) state.map = state.raw.mapname;
if('sv_maxclients' in state.raw) state.maxplayers = state.raw.sv_maxclients;
if('maxclients' in state.raw) state.maxplayers = state.raw.maxclients;
if('sv_hostname' in state.raw) state.name = state.raw.sv_hostname;
if('hostname' in state.raw) state.name = state.raw.hostname;
}
}
import Core from './core.js'
export default class quake2 extends Core {
constructor () {
super()
this.encoding = 'latin1'
this.delimiter = '\n'
this.sendHeader = 'status'
this.responseHeader = 'print'
this.isQuake1 = false
}
async run (state) {
const body = await this.udpSend('\xff\xff\xff\xff' + this.sendHeader + '\x00', packet => {
const reader = this.reader(packet)
const header = reader.string({ length: 4, encoding: 'latin1' })
if (header !== '\xff\xff\xff\xff') return
let type
if (this.isQuake1) {
type = reader.string(this.responseHeader.length)
} else {
type = reader.string({ encoding: 'latin1' })
}
if (type !== this.responseHeader) return
return reader.rest()
})
const reader = this.reader(body)
const info = reader.string().split('\\')
if (info[0] === '') info.shift()
while (true) {
const key = info.shift()
const value = info.shift()
if (typeof value === 'undefined') break
state.raw[key] = value
}
while (!reader.done()) {
const line = reader.string()
if (!line || line.charAt(0) === '\0') break
const args = []
const split = line.split('"')
split.forEach((part, i) => {
const inQuote = (i % 2 === 1)
if (inQuote) {
args.push(part)
} else {
const splitSpace = part.split(' ')
for (const subpart of splitSpace) {
if (subpart) args.push(subpart)
}
}
})
const player = {}
if (this.isQuake1) {
player.id = parseInt(args.shift())
player.score = parseInt(args.shift())
player.time = parseInt(args.shift())
player.ping = parseInt(args.shift())
player.name = args.shift()
player.skin = args.shift()
player.color1 = parseInt(args.shift())
player.color2 = parseInt(args.shift())
} else {
player.frags = parseInt(args.shift())
player.ping = parseInt(args.shift())
player.name = args.shift() || ''
if (!player.name) delete player.name
player.address = args.shift() || ''
if (!player.address) delete player.address
}
(player.ping ? state.players : state.bots).push(player)
}
if ('g_needpass' in state.raw) state.password = state.raw.g_needpass
if ('mapname' in state.raw) state.map = state.raw.mapname
if ('sv_maxclients' in state.raw) state.maxplayers = state.raw.sv_maxclients
if ('maxclients' in state.raw) state.maxplayers = state.raw.maxclients
if ('sv_hostname' in state.raw) state.name = state.raw.sv_hostname
if ('hostname' in state.raw) state.name = state.raw.hostname
}
}

View file

@ -1,22 +1,24 @@
import quake2 from './quake2.js';
export default class quake3 extends quake2 {
constructor() {
super();
this.sendHeader = 'getstatus';
this.responseHeader = 'statusResponse';
}
async run(state) {
await super.run(state);
state.name = this.stripColors(state.name);
for(const key of Object.keys(state.raw)) {
state.raw[key] = this.stripColors(state.raw[key]);
}
for(const player of state.players) {
player.name = this.stripColors(player.name);
}
}
stripColors(str) {
return str.replace(/\^(X.{6}|.)/g,'');
}
}
import quake2 from './quake2.js'
export default class quake3 extends quake2 {
constructor () {
super()
this.sendHeader = 'getstatus'
this.responseHeader = 'statusResponse'
}
async run (state) {
await super.run(state)
state.name = this.stripColors(state.name)
for (const key of Object.keys(state.raw)) {
state.raw[key] = this.stripColors(state.raw[key])
}
for (const player of state.players) {
player.name = this.stripColors(player.name)
}
}
stripColors (str) {
return str.replace(/\^(X.{6}|.)/g, '')
}
}

View file

@ -1,74 +1,69 @@
import Core from './core.js';
export default class rfactor extends Core {
constructor() {
super();
//this.byteorder = 'be';
}
async run(state) {
const buffer = await this.udpSend('rF_S',b => b);
const reader = this.reader(buffer);
state.raw.gamename = this.readString(reader, 8);
state.raw.fullUpdate = reader.uint(1);
state.raw.region = reader.uint(2);
state.raw.ip = reader.part(4);
state.raw.size = reader.uint(2);
state.raw.version = reader.uint(2);
state.raw.versionRaceCast = reader.uint(2);
state.gamePort = reader.uint(2);
state.raw.queryPort = reader.uint(2);
state.raw.game = this.readString(reader, 20);
state.name = this.readString(reader, 28);
state.map = this.readString(reader, 32);
state.raw.motd = this.readString(reader, 96);
state.raw.packedAids = reader.uint(2);
state.raw.ping = reader.uint(2);
state.raw.packedFlags = reader.uint(1);
state.raw.rate = reader.uint(1);
state.players.setNum(reader.uint(1));
state.maxplayers = reader.uint(1);
state.raw.bots = reader.uint(1);
state.raw.packedSpecial = reader.uint(1);
state.raw.damage = reader.uint(1);
state.raw.packedRules = reader.uint(2);
state.raw.credits1 = reader.uint(1);
state.raw.credits2 = reader.uint(2);
this.logger.debug(reader.offset());
state.raw.time = reader.uint(2);
state.raw.laps = reader.uint(2) / 16;
reader.skip(3);
state.raw.vehicles = reader.string();
state.password = !!(state.raw.packedSpecial & 2);
state.raw.raceCast = !!(state.raw.packedSpecial & 4);
state.raw.fixedSetups = !!(state.raw.packedSpecial & 16);
const aids = [
'TractionControl',
'AntiLockBraking',
'StabilityControl',
'AutoShifting',
'AutoClutch',
'Invulnerability',
'OppositeLock',
'SteeringHelp',
'BrakingHelp',
'SpinRecovery',
'AutoPitstop'
];
state.raw.aids = [];
for (let offset = 0; offset < aids.length; offset++) {
if (state.packedAids && (1 << offset)) {
state.raw.aids.push(aids[offset]);
}
}
}
// Consumes bytesToConsume, but only returns string up to the first null
readString(reader, bytesToConsume) {
const consumed = reader.part(bytesToConsume);
return this.reader(consumed).string();
}
}
import Core from './core.js'
export default class rfactor extends Core {
async run (state) {
const buffer = await this.udpSend('rF_S', b => b)
const reader = this.reader(buffer)
state.raw.gamename = this.readString(reader, 8)
state.raw.fullUpdate = reader.uint(1)
state.raw.region = reader.uint(2)
state.raw.ip = reader.part(4)
state.raw.size = reader.uint(2)
state.raw.version = reader.uint(2)
state.raw.versionRaceCast = reader.uint(2)
state.gamePort = reader.uint(2)
state.raw.queryPort = reader.uint(2)
state.raw.game = this.readString(reader, 20)
state.name = this.readString(reader, 28)
state.map = this.readString(reader, 32)
state.raw.motd = this.readString(reader, 96)
state.raw.packedAids = reader.uint(2)
state.raw.ping = reader.uint(2)
state.raw.packedFlags = reader.uint(1)
state.raw.rate = reader.uint(1)
state.players.setNum(reader.uint(1))
state.maxplayers = reader.uint(1)
state.raw.bots = reader.uint(1)
state.raw.packedSpecial = reader.uint(1)
state.raw.damage = reader.uint(1)
state.raw.packedRules = reader.uint(2)
state.raw.credits1 = reader.uint(1)
state.raw.credits2 = reader.uint(2)
this.logger.debug(reader.offset())
state.raw.time = reader.uint(2)
state.raw.laps = reader.uint(2) / 16
reader.skip(3)
state.raw.vehicles = reader.string()
state.password = !!(state.raw.packedSpecial & 2)
state.raw.raceCast = !!(state.raw.packedSpecial & 4)
state.raw.fixedSetups = !!(state.raw.packedSpecial & 16)
const aids = [
'TractionControl',
'AntiLockBraking',
'StabilityControl',
'AutoShifting',
'AutoClutch',
'Invulnerability',
'OppositeLock',
'SteeringHelp',
'BrakingHelp',
'SpinRecovery',
'AutoPitstop'
]
state.raw.aids = []
for (let offset = 0; offset < aids.length; offset++) {
if (state.packedAids && (1 << offset)) {
state.raw.aids.push(aids[offset])
}
}
}
// Consumes bytesToConsume, but only returns string up to the first null
readString (reader, bytesToConsume) {
const consumed = reader.part(bytesToConsume)
return this.reader(consumed).string()
}
}

View file

@ -1,107 +1,108 @@
import Core from './core.js';
export default class samp extends Core {
constructor() {
super();
this.encoding = 'win1252';
this.magicHeader = 'SAMP';
this.responseMagicHeader = null;
this.isVcmp = false;
}
async run(state) {
// read info
{
const reader = await this.sendPacket('i');
if (this.isVcmp) {
const consumed = reader.part(12);
state.raw.version = this.reader(consumed).string();
}
state.password = !!reader.uint(1);
state.raw.numplayers = reader.uint(2);
state.maxplayers = reader.uint(2);
state.name = reader.pascalString(4);
state.raw.gamemode = reader.pascalString(4);
state.raw.map = reader.pascalString(4);
}
// read rules
if (!this.isVcmp) {
const reader = await this.sendPacket('r');
const ruleCount = reader.uint(2);
state.raw.rules = {};
for(let i = 0; i < ruleCount; i++) {
const key = reader.pascalString(1);
const value = reader.pascalString(1);
state.raw.rules[key] = value;
}
}
// read players
// don't even bother if > 100 players, because the server won't respond
let gotPlayerData = false;
if (state.raw.numplayers < 100) {
if (this.isVcmp) {
const reader = await this.sendPacket('c', true);
if (reader !== null) {
gotPlayerData = true;
const playerCount = reader.uint(2);
for(let i = 0; i < playerCount; i++) {
const player = {};
player.name = reader.pascalString(1);
state.players.push(player);
}
}
} else {
const reader = await this.sendPacket('d', true);
if (reader !== null) {
gotPlayerData = true;
const playerCount = reader.uint(2);
for(let i = 0; i < playerCount; i++) {
const player = {};
player.id = reader.uint(1);
player.name = reader.pascalString(1);
player.score = reader.int(4);
player.ping = reader.uint(4);
state.players.push(player);
}
}
}
}
if (!gotPlayerData) {
state.players.setNum(state.raw.numplayers);
}
}
async sendPacket(type,allowTimeout) {
const outBuffer = Buffer.alloc(11);
outBuffer.write(this.magicHeader,0, 4);
const ipSplit = this.options.address.split('.');
outBuffer.writeUInt8(parseInt(ipSplit[0]),4);
outBuffer.writeUInt8(parseInt(ipSplit[1]),5);
outBuffer.writeUInt8(parseInt(ipSplit[2]),6);
outBuffer.writeUInt8(parseInt(ipSplit[3]),7);
outBuffer.writeUInt16LE(this.options.port,8);
outBuffer.writeUInt8(type.charCodeAt(0),10);
const checkBuffer = Buffer.from(outBuffer);
if (this.responseMagicHeader) {
checkBuffer.write(this.responseMagicHeader, 0, 4);
}
return await this.udpSend(
outBuffer,
(buffer) => {
const reader = this.reader(buffer);
for(let i = 0; i < checkBuffer.length; i++) {
if(checkBuffer.readUInt8(i) !== reader.uint(1)) return;
}
return reader;
},
() => {
if(allowTimeout) {
return null;
}
}
);
}
}
import Core from './core.js'
export default class samp extends Core {
constructor () {
super()
this.encoding = 'win1252'
this.magicHeader = 'SAMP'
this.responseMagicHeader = null
this.isVcmp = false
}
async run (state) {
// read info
{
const reader = await this.sendPacket('i')
if (this.isVcmp) {
const consumed = reader.part(12)
state.raw.version = this.reader(consumed).string()
}
state.password = !!reader.uint(1)
state.raw.numplayers = reader.uint(2)
state.maxplayers = reader.uint(2)
state.name = reader.pascalString(4)
state.raw.gamemode = reader.pascalString(4)
state.raw.map = reader.pascalString(4)
}
// read rules
if (!this.isVcmp) {
const reader = await this.sendPacket('r')
const ruleCount = reader.uint(2)
state.raw.rules = {}
for (let i = 0; i < ruleCount; i++) {
const key = reader.pascalString(1)
const value = reader.pascalString(1)
state.raw.rules[key] = value
}
}
// read players
// don't even bother if > 100 players, because the server won't respond
let gotPlayerData = false
if (state.raw.numplayers < 100) {
if (this.isVcmp) {
const reader = await this.sendPacket('c', true)
if (reader !== null) {
gotPlayerData = true
const playerCount = reader.uint(2)
for (let i = 0; i < playerCount; i++) {
const player = {}
player.name = reader.pascalString(1)
state.players.push(player)
}
}
} else {
const reader = await this.sendPacket('d', true)
if (reader !== null) {
gotPlayerData = true
const playerCount = reader.uint(2)
for (let i = 0; i < playerCount; i++) {
const player = {}
player.id = reader.uint(1)
player.name = reader.pascalString(1)
player.score = reader.int(4)
player.ping = reader.uint(4)
state.players.push(player)
}
}
}
}
if (!gotPlayerData) {
state.players.setNum(state.raw.numplayers)
}
}
async sendPacket (type, allowTimeout) {
const outBuffer = Buffer.alloc(11)
outBuffer.write(this.magicHeader, 0, 4)
const ipSplit = this.options.address.split('.')
outBuffer.writeUInt8(parseInt(ipSplit[0]), 4)
outBuffer.writeUInt8(parseInt(ipSplit[1]), 5)
outBuffer.writeUInt8(parseInt(ipSplit[2]), 6)
outBuffer.writeUInt8(parseInt(ipSplit[3]), 7)
outBuffer.writeUInt16LE(this.options.port, 8)
outBuffer.writeUInt8(type.charCodeAt(0), 10)
const checkBuffer = Buffer.from(outBuffer)
if (this.responseMagicHeader) {
checkBuffer.write(this.responseMagicHeader, 0, 4)
}
return await this.udpSend(
outBuffer,
(buffer) => {
const reader = this.reader(buffer)
for (let i = 0; i < checkBuffer.length; i++) {
if (checkBuffer.readUInt8(i) !== reader.uint(1)) return
}
return reader
},
() => {
if (allowTimeout) {
return null
}
}
)
}
}

View file

@ -1,29 +1,25 @@
import Core from './core.js';
export default class savage2 extends Core {
constructor() {
super();
}
async run(state) {
const buffer = await this.udpSend('\x01',b => b);
const reader = this.reader(buffer);
reader.skip(12);
state.name = this.stripColorCodes(reader.string());
state.players.setNum(reader.uint(1));
state.maxplayers = reader.uint(1);
state.raw.time = reader.string();
state.map = reader.string();
state.raw.nextmap = reader.string();
state.raw.location = reader.string();
state.raw.minplayers = reader.uint(1);
state.raw.gametype = reader.string();
state.raw.version = reader.string();
state.raw.minlevel = reader.uint(1);
}
stripColorCodes(str) {
return str.replace(/\^./g,'');
}
}
import Core from './core.js'
export default class savage2 extends Core {
async run (state) {
const buffer = await this.udpSend('\x01', b => b)
const reader = this.reader(buffer)
reader.skip(12)
state.name = this.stripColorCodes(reader.string())
state.players.setNum(reader.uint(1))
state.maxplayers = reader.uint(1)
state.raw.time = reader.string()
state.map = reader.string()
state.raw.nextmap = reader.string()
state.raw.location = reader.string()
state.raw.minplayers = reader.uint(1)
state.raw.gametype = reader.string()
state.raw.version = reader.string()
state.raw.minlevel = reader.uint(1)
}
stripColorCodes (str) {
return str.replace(/\^./g, '')
}
}

View file

@ -1,67 +1,67 @@
import Core from './core.js';
export default class starmade extends Core {
constructor() {
super();
this.encoding = 'latin1';
this.byteorder = 'be';
}
async run(state) {
const b = Buffer.from([0x00,0x00,0x00,0x09,0x2a,0xff,0xff,0x01,0x6f,0x00,0x00,0x00,0x00]);
const payload = await this.withTcp(async socket => {
return await this.tcpSend(socket, b, buffer => {
if (buffer.length < 12) return;
const reader = this.reader(buffer);
const packetLength = reader.uint(4);
this.logger.debug("Received packet length: " + packetLength);
const timestamp = reader.uint(8).toString();
this.logger.debug("Received timestamp: " + timestamp);
if (reader.remaining() < packetLength || reader.remaining() < 5) return;
const checkId = reader.uint(1);
const packetId = reader.uint(2);
const commandId = reader.uint(1);
const type = reader.uint(1);
this.logger.debug("checkId=" + checkId + " packetId=" + packetId + " commandId=" + commandId + " type=" + type);
if (checkId !== 0x2a) return;
return reader.rest();
});
});
const reader = this.reader(payload);
const data = [];
state.raw.data = data;
while(!reader.done()) {
const mark = reader.uint(1);
if(mark === 1) {
// signed int
data.push(reader.int(4));
} else if(mark === 3) {
// float
data.push(reader.float());
} else if(mark === 4) {
// string
data.push(reader.pascalString(2));
} else if(mark === 6) {
// byte
data.push(reader.uint(1));
}
}
this.logger.debug("Received raw data array", data);
if(typeof data[0] === 'number') state.raw.infoVersion = data[0];
if(typeof data[1] === 'number') state.raw.version = data[1];
if(typeof data[2] === 'string') state.name = data[2];
if(typeof data[3] === 'string') state.raw.description = data[3];
if(typeof data[4] === 'number') state.raw.startTime = data[4];
if(typeof data[5] === 'number') state.players.setNum(data[5]);
if(typeof data[6] === 'number') state.maxplayers = data[6];
}
}
import Core from './core.js'
export default class starmade extends Core {
constructor () {
super()
this.encoding = 'latin1'
this.byteorder = 'be'
}
async run (state) {
const b = Buffer.from([0x00, 0x00, 0x00, 0x09, 0x2a, 0xff, 0xff, 0x01, 0x6f, 0x00, 0x00, 0x00, 0x00])
const payload = await this.withTcp(async socket => {
return await this.tcpSend(socket, b, buffer => {
if (buffer.length < 12) return
const reader = this.reader(buffer)
const packetLength = reader.uint(4)
this.logger.debug('Received packet length: ' + packetLength)
const timestamp = reader.uint(8).toString()
this.logger.debug('Received timestamp: ' + timestamp)
if (reader.remaining() < packetLength || reader.remaining() < 5) return
const checkId = reader.uint(1)
const packetId = reader.uint(2)
const commandId = reader.uint(1)
const type = reader.uint(1)
this.logger.debug('checkId=' + checkId + ' packetId=' + packetId + ' commandId=' + commandId + ' type=' + type)
if (checkId !== 0x2a) return
return reader.rest()
})
})
const reader = this.reader(payload)
const data = []
state.raw.data = data
while (!reader.done()) {
const mark = reader.uint(1)
if (mark === 1) {
// signed int
data.push(reader.int(4))
} else if (mark === 3) {
// float
data.push(reader.float())
} else if (mark === 4) {
// string
data.push(reader.pascalString(2))
} else if (mark === 6) {
// byte
data.push(reader.uint(1))
}
}
this.logger.debug('Received raw data array', data)
if (typeof data[0] === 'number') state.raw.infoVersion = data[0]
if (typeof data[1] === 'number') state.raw.version = data[1]
if (typeof data[2] === 'string') state.name = data[2]
if (typeof data[3] === 'string') state.raw.description = data[3]
if (typeof data[4] === 'number') state.raw.startTime = data[4]
if (typeof data[5] === 'number') state.players.setNum(data[5])
if (typeof data[6] === 'number') state.maxplayers = data[6]
}
}

View file

@ -1,10 +1,10 @@
import tribes1 from "./tribes1.js";
export default class starsiege extends tribes1 {
constructor() {
super();
this.encoding = 'latin1';
this.requestByte = 0x72;
this.responseByte = 0x73;
}
}
import tribes1 from './tribes1.js'
export default class starsiege extends tribes1 {
constructor () {
super()
this.encoding = 'latin1'
this.requestByte = 0x72
this.responseByte = 0x73
}
}

View file

@ -1,70 +1,70 @@
import Core from './core.js';
export default class teamspeak2 extends Core {
async run(state) {
const queryPort = this.options.teamspeakQueryPort || 51234;
await this.withTcp(async socket => {
{
const data = await this.sendCommand(socket, 'sel '+this.options.port);
if(data !== '[TS]') throw new Error('Invalid header');
}
{
const data = await this.sendCommand(socket, 'si');
for (const line of data.split('\r\n')) {
const equals = line.indexOf('=');
const key = equals === -1 ? line : line.substring(0,equals);
const value = equals === -1 ? '' : line.substring(equals+1);
state.raw[key] = value;
}
}
{
const data = await this.sendCommand(socket, 'pl');
const split = data.split('\r\n');
const fields = split.shift().split('\t');
for (const line of split) {
const split2 = line.split('\t');
const player = {};
split2.forEach((value,i) => {
let key = fields[i];
if(!key) return;
if(key === 'nick') key = 'name';
const m = value.match(/^"(.*)"$/);
if(m) value = m[1];
player[key] = value;
});
state.players.push(player);
}
}
{
const data = await this.sendCommand(socket, 'cl');
const split = data.split('\r\n');
const fields = split.shift().split('\t');
state.raw.channels = [];
for (const line of split) {
const split2 = line.split('\t');
const channel = {};
split2.forEach((value,i) => {
const key = fields[i];
if(!key) return;
const m = value.match(/^"(.*)"$/);
if(m) value = m[1];
channel[key] = value;
});
state.raw.channels.push(channel);
}
}
}, queryPort);
}
async sendCommand(socket,cmd) {
return await this.tcpSend(socket, cmd+'\x0A', buffer => {
if(buffer.length < 6) return;
if(buffer.slice(-6).toString() !== '\r\nOK\r\n') return;
return buffer.slice(0,-6).toString();
});
}
}
import Core from './core.js'
export default class teamspeak2 extends Core {
async run (state) {
const queryPort = this.options.teamspeakQueryPort || 51234
await this.withTcp(async socket => {
{
const data = await this.sendCommand(socket, 'sel ' + this.options.port)
if (data !== '[TS]') throw new Error('Invalid header')
}
{
const data = await this.sendCommand(socket, 'si')
for (const line of data.split('\r\n')) {
const equals = line.indexOf('=')
const key = equals === -1 ? line : line.substring(0, equals)
const value = equals === -1 ? '' : line.substring(equals + 1)
state.raw[key] = value
}
}
{
const data = await this.sendCommand(socket, 'pl')
const split = data.split('\r\n')
const fields = split.shift().split('\t')
for (const line of split) {
const split2 = line.split('\t')
const player = {}
split2.forEach((value, i) => {
let key = fields[i]
if (!key) return
if (key === 'nick') key = 'name'
const m = value.match(/^"(.*)"$/)
if (m) value = m[1]
player[key] = value
})
state.players.push(player)
}
}
{
const data = await this.sendCommand(socket, 'cl')
const split = data.split('\r\n')
const fields = split.shift().split('\t')
state.raw.channels = []
for (const line of split) {
const split2 = line.split('\t')
const channel = {}
split2.forEach((value, i) => {
const key = fields[i]
if (!key) return
const m = value.match(/^"(.*)"$/)
if (m) value = m[1]
channel[key] = value
})
state.raw.channels.push(channel)
}
}
}, queryPort)
}
async sendCommand (socket, cmd) {
return await this.tcpSend(socket, cmd + '\x0A', buffer => {
if (buffer.length < 6) return
if (buffer.slice(-6).toString() !== '\r\nOK\r\n') return
return buffer.slice(0, -6).toString()
})
}
}

View file

@ -1,66 +1,68 @@
import Core from './core.js';
export default class teamspeak3 extends Core {
async run(state) {
const queryPort = this.options.teamspeakQueryPort || 10011;
await this.withTcp(async socket => {
{
const data = await this.sendCommand(socket, 'use port='+this.options.port, true);
const split = data.split('\n\r');
if(split[0] !== 'TS3') throw new Error('Invalid header');
}
{
const data = await this.sendCommand(socket, 'serverinfo');
state.raw = data[0];
if('virtualserver_name' in state.raw) state.name = state.raw.virtualserver_name;
if('virtualserver_maxclients' in state.raw) state.maxplayers = state.raw.virtualserver_maxclients;
}
{
const list = await this.sendCommand(socket, 'clientlist');
for (const client of list) {
client.name = client.client_nickname;
delete client.client_nickname;
if(client.client_type === '0') {
state.players.push(client);
}
}
}
{
const data = await this.sendCommand(socket, 'channellist -topic');
state.raw.channels = data;
}
}, queryPort);
}
async sendCommand(socket,cmd,raw) {
const body = await this.tcpSend(socket, cmd+'\x0A', (buffer) => {
if (buffer.length < 21) return;
if (buffer.slice(-21).toString() !== '\n\rerror id=0 msg=ok\n\r') return;
return buffer.slice(0, -21).toString();
});
if(raw) {
return body;
} else {
const segments = body.split('|');
const out = [];
for (const line of segments) {
const split = line.split(' ');
const unit = {};
for (const field of split) {
const equals = field.indexOf('=');
const key = equals === -1 ? field : field.substring(0, equals);
const value = equals === -1 ? '' : field.substring(equals + 1)
.replace(/\\s/g,' ').replace(/\\\//g,'/');
unit[key] = value;
}
out.push(unit);
}
return out;
}
}
}
import Core from './core.js'
export default class teamspeak3 extends Core {
async run (state) {
const queryPort = this.options.teamspeakQueryPort || 10011
await this.withTcp(async socket => {
{
const data = await this.sendCommand(socket, 'use port=' + this.options.port, true)
const split = data.split('\n\r')
if (split[0] !== 'TS3') throw new Error('Invalid header')
}
{
const data = await this.sendCommand(socket, 'serverinfo')
state.raw = data[0]
if ('virtualserver_name' in state.raw) state.name = state.raw.virtualserver_name
if ('virtualserver_maxclients' in state.raw) state.maxplayers = state.raw.virtualserver_maxclients
}
{
const list = await this.sendCommand(socket, 'clientlist')
for (const client of list) {
client.name = client.client_nickname
delete client.client_nickname
if (client.client_type === '0') {
state.players.push(client)
}
}
}
{
const data = await this.sendCommand(socket, 'channellist -topic')
state.raw.channels = data
}
}, queryPort)
}
async sendCommand (socket, cmd, raw) {
const body = await this.tcpSend(socket, cmd + '\x0A', (buffer) => {
if (buffer.length < 21) return
if (buffer.slice(-21).toString() !== '\n\rerror id=0 msg=ok\n\r') return
return buffer.slice(0, -21).toString()
})
if (raw) {
return body
} else {
const segments = body.split('|')
const out = []
for (const line of segments) {
const split = line.split(' ')
const unit = {}
for (const field of split) {
const equals = field.indexOf('=')
const key = equals === -1 ? field : field.substring(0, equals)
const value = equals === -1
? ''
: field.substring(equals + 1)
.replace(/\\s/g, ' ').replace(/\\\//g, '/')
unit[key] = value
}
out.push(unit)
}
return out
}
}
}

View file

@ -1,24 +1,24 @@
import Core from './core.js';
export default class terraria extends Core {
async run(state) {
const json = await this.request({
url: 'http://'+this.options.address+':'+this.options.port+'/v2/server/status',
searchParams: {
players: 'true',
token: this.options.token
},
responseType: 'json'
});
if(json.status !== '200') throw new Error('Invalid status');
for (const one of json.players) {
state.players.push({name:one.nickname,team:one.team});
}
state.name = json.name;
state.gamePort = json.port;
state.raw.numplayers = json.playercount;
}
}
import Core from './core.js'
export default class terraria extends Core {
async run (state) {
const json = await this.request({
url: 'http://' + this.options.address + ':' + this.options.port + '/v2/server/status',
searchParams: {
players: 'true',
token: this.options.token
},
responseType: 'json'
})
if (json.status !== '200') throw new Error('Invalid status')
for (const one of json.players) {
state.players.push({ name: one.nickname, team: one.team })
}
state.name = json.name
state.gamePort = json.port
state.raw.numplayers = json.playercount
}
}

View file

@ -1,150 +1,153 @@
import Core from './core.js';
export default class tribes1 extends Core {
constructor() {
super();
this.encoding = 'latin1';
this.requestByte = 0x62;
this.responseByte = 0x63;
this.challenge = 0x01;
}
async run(state) {
const query = Buffer.alloc(3);
query.writeUInt8(this.requestByte, 0);
query.writeUInt16LE(this.challenge, 1);
const reader = await this.udpSend(query,(buffer) => {
const reader = this.reader(buffer);
const responseByte = reader.uint(1);
if (responseByte !== this.responseByte) {
this.logger.debug('Unexpected response byte');
return;
}
const challenge = reader.uint(2);
if (challenge !== this.challenge) {
this.logger.debug('Unexpected challenge');
return;
}
const requestByte = reader.uint(1);
if (requestByte !== this.requestByte) {
this.logger.debug('Unexpected request byte');
return;
}
return reader;
});
state.raw.gametype = this.readString(reader);
const isStarsiege2009 = state.raw.gametype === 'Starsiege';
state.raw.version = this.readString(reader);
state.name = this.readString(reader);
if (isStarsiege2009) {
state.password = !!reader.uint(1);
state.raw.dedicated = !!reader.uint(1);
state.raw.dropInProgress = !!reader.uint(1);
state.raw.gameInProgress = !!reader.uint(1);
state.raw.playerCount = reader.uint(4);
state.maxplayers = reader.uint(4);
state.raw.teamPlay = reader.uint(1);
state.map = this.readString(reader);
state.raw.cpuSpeed = reader.uint(2);
state.raw.factoryVeh = reader.uint(1);
state.raw.allowTecmix = reader.uint(1);
state.raw.spawnLimit = reader.uint(4);
state.raw.fragLimit = reader.uint(4);
state.raw.timeLimit = reader.uint(4);
state.raw.techLimit = reader.uint(4);
state.raw.combatLimit = reader.uint(4);
state.raw.massLimit = reader.uint(4);
state.raw.playersSent = reader.uint(4);
const teams = {1:'yellow', 2:'blue', 4:'red', 8:'purple'};
while (!reader.done()) {
const player = {};
player.name = this.readString(reader);
const teamId = reader.uint(1);
const team = teams[teamId];
if (team) player.team = teams[teamId];
}
return;
}
state.raw.dedicated = !!reader.uint(1);
state.password = !!reader.uint(1);
state.raw.playerCount = reader.uint(1);
state.maxplayers = reader.uint(1);
state.raw.cpuSpeed = reader.uint(2);
state.raw.mod = this.readString(reader);
state.raw.type = this.readString(reader);
state.map = this.readString(reader);
state.raw.motd = this.readString(reader);
state.raw.teamCount = reader.uint(1);
const teamFields = this.readFieldList(reader);
const playerFields = this.readFieldList(reader);
state.raw.teams = [];
for(let i = 0; i < state.raw.teamCount; i++) {
const teamName = this.readString(reader);
const teamValues = this.readValues(reader);
const teamInfo = {};
for (let i = 0; i < teamValues.length && i < teamFields.length; i++) {
let key = teamFields[i];
let value = teamValues[i];
if (key === 'ultra_base') key = 'name';
if (value === '%t') value = teamName;
if (['score','players'].includes(key)) value = parseInt(value);
teamInfo[key] = value;
}
state.raw.teams.push(teamInfo);
}
for(let i = 0; i < state.raw.playerCount; i++) {
const ping = reader.uint(1) * 4;
const packetLoss = reader.uint(1);
const teamNum = reader.uint(1);
const name = this.readString(reader);
const playerValues = this.readValues(reader);
const playerInfo = {};
for (let i = 0; i < playerValues.length && i < playerFields.length; i++) {
let key = playerFields[i];
let value = playerValues[i];
if (value === '%p') value = ping;
if (value === '%l') value = packetLoss;
if (value === '%t') value = teamNum;
if (value === '%n') value = name;
if (['score','ping','pl','kills','lvl'].includes(key)) value = parseInt(value);
if (key === 'team') {
const teamId = parseInt(value);
if (teamId >= 0 && teamId < state.raw.teams.length && state.raw.teams[teamId].name) {
value = state.raw.teams[teamId].name;
} else {
continue;
}
}
playerInfo[key] = value;
}
state.players.push(playerInfo);
}
}
readFieldList(reader) {
const str = this.readString(reader);
if (!str) return [];
return ('?'+str)
.split('\t')
.map((a) => a.substring(1).trim().toLowerCase())
.map((a) => a === 'team name' ? 'name' : a)
.map((a) => a === 'player name' ? 'name' : a);
}
readValues(reader) {
const str = this.readString(reader);
if (!str) return [];
return str
.split('\t')
.map((a) => a.trim());
}
readString(reader) {
return reader.pascalString(1);
}
}
import Core from './core.js'
export default class tribes1 extends Core {
constructor () {
super()
this.encoding = 'latin1'
this.requestByte = 0x62
this.responseByte = 0x63
this.challenge = 0x01
}
async run (state) {
const query = Buffer.alloc(3)
query.writeUInt8(this.requestByte, 0)
query.writeUInt16LE(this.challenge, 1)
const reader = await this.udpSend(query, (buffer) => {
const reader = this.reader(buffer)
const responseByte = reader.uint(1)
if (responseByte !== this.responseByte) {
this.logger.debug('Unexpected response byte')
return
}
const challenge = reader.uint(2)
if (challenge !== this.challenge) {
this.logger.debug('Unexpected challenge')
return
}
const requestByte = reader.uint(1)
if (requestByte !== this.requestByte) {
this.logger.debug('Unexpected request byte')
return
}
return reader
})
state.raw.gametype = this.readString(reader)
const isStarsiege2009 = state.raw.gametype === 'Starsiege'
state.raw.version = this.readString(reader)
state.name = this.readString(reader)
if (isStarsiege2009) {
state.password = !!reader.uint(1)
state.raw.dedicated = !!reader.uint(1)
state.raw.dropInProgress = !!reader.uint(1)
state.raw.gameInProgress = !!reader.uint(1)
state.raw.playerCount = reader.uint(4)
state.maxplayers = reader.uint(4)
state.raw.teamPlay = reader.uint(1)
state.map = this.readString(reader)
state.raw.cpuSpeed = reader.uint(2)
state.raw.factoryVeh = reader.uint(1)
state.raw.allowTecmix = reader.uint(1)
state.raw.spawnLimit = reader.uint(4)
state.raw.fragLimit = reader.uint(4)
state.raw.timeLimit = reader.uint(4)
state.raw.techLimit = reader.uint(4)
state.raw.combatLimit = reader.uint(4)
state.raw.massLimit = reader.uint(4)
state.raw.playersSent = reader.uint(4)
const teams = { 1: 'yellow', 2: 'blue', 4: 'red', 8: 'purple' }
while (!reader.done()) {
const player = {}
player.name = this.readString(reader)
const teamId = reader.uint(1)
const team = teams[teamId]
if (team) player.team = teams[teamId]
}
return
}
state.raw.dedicated = !!reader.uint(1)
state.password = !!reader.uint(1)
state.raw.playerCount = reader.uint(1)
state.maxplayers = reader.uint(1)
state.raw.cpuSpeed = reader.uint(2)
state.raw.mod = this.readString(reader)
state.raw.type = this.readString(reader)
state.map = this.readString(reader)
state.raw.motd = this.readString(reader)
state.raw.teamCount = reader.uint(1)
const teamFields = this.readFieldList(reader)
const playerFields = this.readFieldList(reader)
state.raw.teams = []
for (let i = 0; i < state.raw.teamCount; i++) {
const teamName = this.readString(reader)
const teamValues = this.readValues(reader)
const teamInfo = {}
for (let i = 0; i < teamValues.length && i < teamFields.length; i++) {
let key = teamFields[i]
let value = teamValues[i]
if (key === 'ultra_base') key = 'name'
if (value === '%t') value = teamName
if (['score', 'players'].includes(key)) value = parseInt(value)
teamInfo[key] = value
}
state.raw.teams.push(teamInfo)
}
for (let i = 0; i < state.raw.playerCount; i++) {
const ping = reader.uint(1) * 4
const packetLoss = reader.uint(1)
const teamNum = reader.uint(1)
const name = this.readString(reader)
const playerValues = this.readValues(reader)
const playerInfo = {}
for (let i = 0; i < playerValues.length && i < playerFields.length; i++) {
const key = playerFields[i]
let value = playerValues[i]
if (value === '%p') value = ping
if (value === '%l') value = packetLoss
if (value === '%t') value = teamNum
if (value === '%n') value = name
if (['score', 'ping', 'pl', 'kills', 'lvl'].includes(key)) value = parseInt(value)
if (key === 'team') {
const teamId = parseInt(value)
if (teamId >= 0 && teamId < state.raw.teams.length && state.raw.teams[teamId].name) {
value = state.raw.teams[teamId].name
} else {
continue
}
}
playerInfo[key] = value
}
state.players.push(playerInfo)
}
}
readFieldList (reader) {
const str = this.readString(reader)
if (!str) return []
return ('?' + str)
.split('\t')
.map((a) => a.substring(1).trim().toLowerCase())
.map((a) => a === 'team name' ? 'name' : a)
.map((a) => a === 'player name' ? 'name' : a)
}
readValues (reader) {
const str = this.readString(reader)
if (!str) return []
return str
.split('\t')
.map((a) => a.trim())
}
readString (reader) {
return reader.pascalString(1)
}
}

View file

@ -1,79 +1,80 @@
import Core from './core.js';
/** Unsupported -- use at your own risk!! */
export default class tribes1master extends Core {
constructor() {
super();
this.encoding = 'latin1';
}
async run(state) {
const queryBuffer = Buffer.from([
0x10, // standard header
0x03, // dump servers
0xff, // ask for all packets
0x00, // junk
0x01, 0x02, // challenge
]);
let parts = new Map();
let total = 0;
const full = await this.udpSend(queryBuffer,(buffer) => {
const reader = this.reader(buffer);
const header = reader.uint(2);
if (header !== 0x0610) {
this.logger.debug('Header response does not match: ' + header.toString(16));
return;
}
const num = reader.uint(1);
const t = reader.uint(1);
if (t <= 0 || (total > 0 && t !== total)) {
throw new Error('Conflicting packet total: ' + t);
}
total = t;
if (num < 1 || num > total) {
this.logger.debug('Invalid packet number: ' + num + ' ' + total);
return;
}
if (parts.has(num)) {
this.logger.debug('Duplicate part: ' + num);
return;
}
reader.skip(2); // challenge (0x0201)
reader.skip(2); // always 0x6600
parts.set(num, reader.rest());
if (parts.size === total) {
const ordered = [];
for (let i = 1; i <= total; i++) ordered.push(parts.get(i));
return Buffer.concat(ordered);
}
});
const fullReader = this.reader(full);
state.raw.name = this.readString(fullReader);
state.raw.motd = this.readString(fullReader);
state.raw.servers = [];
while (!fullReader.done()) {
fullReader.skip(1); // junk ?
const count = fullReader.uint(1);
for (let i = 0; i < count; i++) {
const six = fullReader.uint(1);
if (six !== 6) {
throw new Error('Expecting 6');
}
const ip = fullReader.uint(4);
const port = fullReader.uint(2);
const ipStr = (ip & 255) + '.' + (ip >> 8 & 255) + '.' + (ip >> 16 & 255) + '.' + (ip >>> 24);
state.raw.servers.push(ipStr+":"+port);
}
}
}
readString(reader) {
return reader.pascalString(1);
}
}
import Core from './core.js'
/** Unsupported -- use at your own risk!! */
export default class tribes1master extends Core {
constructor () {
super()
this.encoding = 'latin1'
}
async run (state) {
const queryBuffer = Buffer.from([
0x10, // standard header
0x03, // dump servers
0xff, // ask for all packets
0x00, // junk
0x01, 0x02 // challenge
])
const parts = new Map()
let total = 0
const full = await this.udpSend(queryBuffer, (buffer) => {
const reader = this.reader(buffer)
const header = reader.uint(2)
if (header !== 0x0610) {
this.logger.debug('Header response does not match: ' + header.toString(16))
return
}
const num = reader.uint(1)
const t = reader.uint(1)
if (t <= 0 || (total > 0 && t !== total)) {
throw new Error('Conflicting packet total: ' + t)
}
total = t
if (num < 1 || num > total) {
this.logger.debug('Invalid packet number: ' + num + ' ' + total)
return
}
if (parts.has(num)) {
this.logger.debug('Duplicate part: ' + num)
return
}
reader.skip(2) // challenge (0x0201)
reader.skip(2) // always 0x6600
parts.set(num, reader.rest())
if (parts.size === total) {
const ordered = []
for (let i = 1; i <= total; i++) ordered.push(parts.get(i))
return Buffer.concat(ordered)
}
})
const fullReader = this.reader(full)
state.raw.name = this.readString(fullReader)
state.raw.motd = this.readString(fullReader)
state.raw.servers = []
while (!fullReader.done()) {
fullReader.skip(1) // junk ?
const count = fullReader.uint(1)
for (let i = 0; i < count; i++) {
const six = fullReader.uint(1)
if (six !== 6) {
throw new Error('Expecting 6')
}
const ip = fullReader.uint(4)
const port = fullReader.uint(2)
const ipStr = (ip & 255) + '.' + (ip >> 8 & 255) + '.' + (ip >> 16 & 255) + '.' + (ip >>> 24)
state.raw.servers.push(ipStr + ':' + port)
}
}
}
readString (reader) {
return reader.pascalString(1)
}
}

View file

@ -1,150 +1,150 @@
import Core from './core.js';
export default class unreal2 extends Core {
constructor() {
super();
this.encoding = 'latin1';
}
async run(state) {
let extraInfoReader;
{
const b = await this.sendPacket(0, true);
const reader = this.reader(b);
state.raw.serverid = reader.uint(4);
state.raw.ip = this.readUnrealString(reader);
state.gamePort = reader.uint(4);
state.raw.queryport = reader.uint(4);
state.name = this.readUnrealString(reader, true);
state.map = this.readUnrealString(reader, true);
state.raw.gametype = this.readUnrealString(reader, true);
state.raw.numplayers = reader.uint(4);
state.maxplayers = reader.uint(4);
this.logger.debug(log => {
log("UNREAL2 EXTRA INFO", reader.buffer.slice(reader.i));
});
extraInfoReader = reader;
}
{
const b = await this.sendPacket(1,true);
const reader = this.reader(b);
state.raw.mutators = [];
state.raw.rules = {};
while(!reader.done()) {
const key = this.readUnrealString(reader,true);
const value = this.readUnrealString(reader,true);
this.logger.debug(key+'='+value);
if(key === 'Mutator' || key === 'mutator') {
state.raw.mutators.push(value);
} else if (key || value) {
if (state.raw.rules.hasOwnProperty(key)) {
state.raw.rules[key] += ',' + value;
} else {
state.raw.rules[key] = value;
}
}
}
if('GamePassword' in state.raw.rules)
state.password = state.raw.rules.GamePassword !== 'True';
}
if (state.raw.mutators.includes('KillingFloorMut')
|| state.raw.rules['Num trader weapons']
|| state.raw.rules['Server Version'] === '1065'
) {
// Killing Floor
state.raw.wavecurrent = extraInfoReader.uint(4);
state.raw.wavetotal = extraInfoReader.uint(4);
state.raw.ping = extraInfoReader.uint(4);
state.raw.flags = extraInfoReader.uint(4);
state.raw.skillLevel = this.readUnrealString(extraInfoReader, true);
} else {
state.raw.ping = extraInfoReader.uint(4);
// These fields were added in later revisions of unreal engine
if (extraInfoReader.remaining() >= 8) {
state.raw.flags = extraInfoReader.uint(4);
state.raw.skill = this.readUnrealString(extraInfoReader, true);
}
}
{
const b = await this.sendPacket(2,false);
const reader = this.reader(b);
state.raw.scoreboard = {};
while(!reader.done()) {
const player = {};
player.id = reader.uint(4);
player.name = this.readUnrealString(reader,true);
player.ping = reader.uint(4);
player.score = reader.int(4);
player.statsId = reader.uint(4);
this.logger.debug(player);
if (!player.id) {
state.raw.scoreboard[player.name] = player.score;
} else if (!player.ping) {
state.bots.push(player);
} else {
state.players.push(player);
}
}
}
}
readUnrealString(reader, stripColor) {
let length = reader.uint(1), ucs2 = false;
if(length >= 0x80) {
// This is flagged as a UCS-2 String
length = (length&0x7f)*2;
ucs2 = true;
// For UCS-2 strings, some unreal 2 games randomly insert an extra 0x01 here,
// not included in the length. Skip it if present (hopefully this never happens legitimately)
const peek = reader.uint(1);
if (peek !== 1) reader.skip(-1);
this.logger.debug(log => {
log("UCS2 STRING");
log("UCS2 Length: " + length);
log(reader.buffer.slice(reader.i,reader.i+length));
});
}
let out = '';
if (ucs2) {
out = reader.string({encoding:'ucs2',length:length});
this.logger.debug("UCS2 String decoded: " + out);
} else if (length > 0) {
out = reader.string();
}
// Sometimes the string has a null at the end (included with the length)
// Strip it if present
if(out.charCodeAt(out.length-1) === 0) {
out = out.substring(0, out.length - 1);
}
if(stripColor) {
out = out.replace(/\x1b...|[\x00-\x1a]/gus,'');
}
return out;
}
async sendPacket(type,required) {
const outbuffer = Buffer.from([0x79,0,0,0,type]);
const packets = [];
return await this.udpSend(outbuffer,(buffer) => {
const reader = this.reader(buffer);
const header = reader.uint(4);
const iType = reader.uint(1);
if(iType !== type) return;
packets.push(reader.rest());
}, () => {
if(!packets.length && required) return;
return Buffer.concat(packets);
});
}
}
import Core from './core.js'
export default class unreal2 extends Core {
constructor () {
super()
this.encoding = 'latin1'
}
async run (state) {
let extraInfoReader
{
const b = await this.sendPacket(0, true)
const reader = this.reader(b)
state.raw.serverid = reader.uint(4)
state.raw.ip = this.readUnrealString(reader)
state.gamePort = reader.uint(4)
state.raw.queryport = reader.uint(4)
state.name = this.readUnrealString(reader, true)
state.map = this.readUnrealString(reader, true)
state.raw.gametype = this.readUnrealString(reader, true)
state.raw.numplayers = reader.uint(4)
state.maxplayers = reader.uint(4)
this.logger.debug(log => {
log('UNREAL2 EXTRA INFO', reader.buffer.slice(reader.i))
})
extraInfoReader = reader
}
{
const b = await this.sendPacket(1, true)
const reader = this.reader(b)
state.raw.mutators = []
state.raw.rules = {}
while (!reader.done()) {
const key = this.readUnrealString(reader, true)
const value = this.readUnrealString(reader, true)
this.logger.debug(key + '=' + value)
if (key === 'Mutator' || key === 'mutator') {
state.raw.mutators.push(value)
} else if (key || value) {
if (Object.prototype.hasOwnProperty.call(state.raw.rules, key)) {
state.raw.rules[key] += ',' + value
} else {
state.raw.rules[key] = value
}
}
}
if ('GamePassword' in state.raw.rules) { state.password = state.raw.rules.GamePassword !== 'True' }
}
if (state.raw.mutators.includes('KillingFloorMut') ||
state.raw.rules['Num trader weapons'] ||
state.raw.rules['Server Version'] === '1065'
) {
// Killing Floor
state.raw.wavecurrent = extraInfoReader.uint(4)
state.raw.wavetotal = extraInfoReader.uint(4)
state.raw.ping = extraInfoReader.uint(4)
state.raw.flags = extraInfoReader.uint(4)
state.raw.skillLevel = this.readUnrealString(extraInfoReader, true)
} else {
state.raw.ping = extraInfoReader.uint(4)
// These fields were added in later revisions of unreal engine
if (extraInfoReader.remaining() >= 8) {
state.raw.flags = extraInfoReader.uint(4)
state.raw.skill = this.readUnrealString(extraInfoReader, true)
}
}
{
const b = await this.sendPacket(2, false)
const reader = this.reader(b)
state.raw.scoreboard = {}
while (!reader.done()) {
const player = {}
player.id = reader.uint(4)
player.name = this.readUnrealString(reader, true)
player.ping = reader.uint(4)
player.score = reader.int(4)
player.statsId = reader.uint(4)
this.logger.debug(player)
if (!player.id) {
state.raw.scoreboard[player.name] = player.score
} else if (!player.ping) {
state.bots.push(player)
} else {
state.players.push(player)
}
}
}
}
readUnrealString (reader, stripColor) {
let length = reader.uint(1); let ucs2 = false
if (length >= 0x80) {
// This is flagged as a UCS-2 String
length = (length & 0x7f) * 2
ucs2 = true
// For UCS-2 strings, some unreal 2 games randomly insert an extra 0x01 here,
// not included in the length. Skip it if present (hopefully this never happens legitimately)
const peek = reader.uint(1)
if (peek !== 1) reader.skip(-1)
this.logger.debug(log => {
log('UCS2 STRING')
log('UCS2 Length: ' + length)
log(reader.buffer.slice(reader.i, reader.i + length))
})
}
let out = ''
if (ucs2) {
out = reader.string({ encoding: 'ucs2', length })
this.logger.debug('UCS2 String decoded: ' + out)
} else if (length > 0) {
out = reader.string()
}
// Sometimes the string has a null at the end (included with the length)
// Strip it if present
if (out.charCodeAt(out.length - 1) === 0) {
out = out.substring(0, out.length - 1)
}
if (stripColor) {
out = out.replace(/\x1b...|[\x00-\x1a]/gus, '')
}
return out
}
async sendPacket (type, required) {
const outbuffer = Buffer.from([0x79, 0, 0, 0, type])
const packets = []
return await this.udpSend(outbuffer, (buffer) => {
const reader = this.reader(buffer)
reader.uint(4) // header
const iType = reader.uint(1)
if (iType !== type) return
packets.push(reader.rest())
}, () => {
if (!packets.length && required) return
return Buffer.concat(packets)
})
}
}

View file

@ -1,45 +1,45 @@
import gamespy3 from './gamespy3.js';
export default class ut3 extends gamespy3 {
async run(state) {
await super.run(state);
this.translate(state.raw,{
'mapname': false,
'p1073741825': 'map',
'p1073741826': 'gametype',
'p1073741827': 'servername',
'p1073741828': 'custom_mutators',
'gamemode': 'joininprogress',
's32779': 'gamemode',
's0': 'bot_skill',
's6': 'pure_server',
's7': 'password',
's8': 'vs_bots',
's10': 'force_respawn',
'p268435704': 'frag_limit',
'p268435705': 'time_limit',
'p268435703': 'numbots',
'p268435717': 'stock_mutators',
'p1073741829': 'stock_mutators',
's1': false,
's9': false,
's11': false,
's12': false,
's13': false,
's14': false,
'p268435706': false,
'p268435968': false,
'p268435969': false
});
const split = (a) => {
let s = a.split('\x1c');
s = s.filter((e) => { return e });
return s;
};
if('custom_mutators' in state.raw) state.raw['custom_mutators'] = split(state.raw['custom_mutators']);
if('stock_mutators' in state.raw) state.raw['stock_mutators'] = split(state.raw['stock_mutators']);
if('map' in state.raw) state.map = state.raw.map;
}
}
import gamespy3 from './gamespy3.js'
export default class ut3 extends gamespy3 {
async run (state) {
await super.run(state)
this.translate(state.raw, {
mapname: false,
p1073741825: 'map',
p1073741826: 'gametype',
p1073741827: 'servername',
p1073741828: 'custom_mutators',
gamemode: 'joininprogress',
s32779: 'gamemode',
s0: 'bot_skill',
s6: 'pure_server',
s7: 'password',
s8: 'vs_bots',
s10: 'force_respawn',
p268435704: 'frag_limit',
p268435705: 'time_limit',
p268435703: 'numbots',
p268435717: 'stock_mutators',
p1073741829: 'stock_mutators',
s1: false,
s9: false,
s11: false,
s12: false,
s13: false,
s14: false,
p268435706: false,
p268435968: false,
p268435969: false
})
const split = (a) => {
let s = a.split('\x1c')
s = s.filter((e) => { return e })
return s
}
if ('custom_mutators' in state.raw) state.raw.custom_mutators = split(state.raw.custom_mutators)
if ('stock_mutators' in state.raw) state.raw.stock_mutators = split(state.raw.stock_mutators)
if ('map' in state.raw) state.map = state.raw.map
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,10 @@
import samp from './samp.js';
export default class vcmp extends samp {
constructor() {
super();
this.magicHeader = 'VCMP';
this.responseMagicHeader = 'MP04';
this.isVcmp = true;
}
}
import samp from './samp.js'
export default class vcmp extends samp {
constructor () {
super()
this.magicHeader = 'VCMP'
this.responseMagicHeader = 'MP04'
this.isVcmp = true
}
}

View file

@ -1,235 +1,236 @@
import Core from './core.js';
export default class ventrilo extends Core {
constructor() {
super();
this.byteorder = 'be';
}
async run(state) {
const data = await this.sendCommand(2,'');
state.raw = splitFields(data.toString());
for (const client of state.raw.CLIENTS) {
client.name = client.NAME;
delete client.NAME;
client.ping = parseInt(client.PING);
delete client.PING;
state.players.push(client);
}
delete state.raw.CLIENTS;
if('NAME' in state.raw) state.name = state.raw.NAME;
if('MAXCLIENTS' in state.raw) state.maxplayers = state.raw.MAXCLIENTS;
if(this.trueTest(state.raw.AUTH)) state.password = true;
}
async sendCommand(cmd,password) {
const body = Buffer.alloc(16);
body.write(password,0,15,'utf8');
const encrypted = encrypt(cmd,body);
const packets = {};
return await this.udpSend(encrypted, (buffer) => {
if(buffer.length < 20) return;
const data = decrypt(buffer);
if(data.zero !== 0) return;
packets[data.packetNum] = data.body;
if(Object.keys(packets).length !== data.packetTotal) return;
const out = [];
for(let i = 0; i < data.packetTotal; i++) {
if(!(i in packets)) throw new Error('Missing packet #'+i);
out.push(packets[i]);
}
return Buffer.concat(out);
});
}
}
function splitFields(str,subMode) {
let splitter,delim;
if(subMode) {
splitter = '=';
delim = ',';
} else {
splitter = ': ';
delim = '\n';
}
const split = str.split(delim);
const out = {};
if(!subMode) {
out.CHANNELS = [];
out.CLIENTS = [];
}
for (const one of split) {
const equal = one.indexOf(splitter);
const key = equal === -1 ? one : one.substring(0,equal);
if(!key || key === '\0') continue;
const value = equal === -1 ? '' : one.substring(equal+splitter.length);
if(!subMode && key === 'CHANNEL') out.CHANNELS.push(splitFields(value,true));
else if(!subMode && key === 'CLIENT') out.CLIENTS.push(splitFields(value,true));
else out[key] = value;
}
return out;
}
function randInt(min,max) {
return Math.floor(Math.random()*(max-min+1)+min)
}
function crc(body) {
let crc = 0;
for(let i = 0; i < body.length; i++) {
crc = crc_table[crc>>8] ^ body.readUInt8(i) ^ (crc<<8);
crc &= 0xffff;
}
return crc;
}
function encrypt(cmd,body) {
const headerKeyStart = randInt(0,0xff);
const headerKeyAdd = randInt(1,0xff);
const bodyKeyStart = randInt(0,0xff);
const bodyKeyAdd = randInt(1,0xff);
const header = Buffer.alloc(20);
header.writeUInt8(headerKeyStart,0);
header.writeUInt8(headerKeyAdd,1);
header.writeUInt16BE(cmd,4);
header.writeUInt16BE(body.length,8);
header.writeUInt16BE(body.length,10);
header.writeUInt16BE(1,12);
header.writeUInt16BE(0,14);
header.writeUInt8(bodyKeyStart,16);
header.writeUInt8(bodyKeyAdd,17);
header.writeUInt16BE(crc(body),18);
let offset = headerKeyStart;
for(let i = 2; i < header.length; i++) {
let val = header.readUInt8(i);
val += code_head.charCodeAt(offset) + ((i-2) % 5);
val = val & 0xff;
header.writeUInt8(val,i);
offset = (offset+headerKeyAdd) & 0xff;
}
offset = bodyKeyStart;
for(let i = 0; i < body.length; i++) {
let val = body.readUInt8(i);
val += code_body.charCodeAt(offset) + (i % 72);
val = val & 0xff;
body.writeUInt8(val,i);
offset = (offset+bodyKeyAdd) & 0xff;
}
return Buffer.concat([header,body]);
}
function decrypt(data) {
const header = data.slice(0,20);
const body = data.slice(20);
const headerKeyStart = header.readUInt8(0);
const headerKeyAdd = header.readUInt8(1);
let offset = headerKeyStart;
for(let i = 2; i < header.length; i++) {
let val = header.readUInt8(i);
val -= code_head.charCodeAt(offset) + ((i-2) % 5);
val = val & 0xff;
header.writeUInt8(val,i);
offset = (offset+headerKeyAdd) & 0xff;
}
const bodyKeyStart = header.readUInt8(16);
const bodyKeyAdd = header.readUInt8(17);
offset = bodyKeyStart;
for(let i = 0; i < body.length; i++) {
let val = body.readUInt8(i);
val -= code_body.charCodeAt(offset) + (i % 72);
val = val & 0xff;
body.writeUInt8(val,i);
offset = (offset+bodyKeyAdd) & 0xff;
}
// header format:
// key, zero, cmd, echo, totallength, thislength
// totalpacket, packetnum, body key, crc
return {
zero: header.readUInt16BE(2),
cmd: header.readUInt16BE(4),
packetTotal: header.readUInt16BE(12),
packetNum: header.readUInt16BE(14),
body: body
};
}
const code_head =
'\x80\xe5\x0e\x38\xba\x63\x4c\x99\x88\x63\x4c\xd6\x54\xb8\x65\x7e'+
'\xbf\x8a\xf0\x17\x8a\xaa\x4d\x0f\xb7\x23\x27\xf6\xeb\x12\xf8\xea'+
'\x17\xb7\xcf\x52\x57\xcb\x51\xcf\x1b\x14\xfd\x6f\x84\x38\xb5\x24'+
'\x11\xcf\x7a\x75\x7a\xbb\x78\x74\xdc\xbc\x42\xf0\x17\x3f\x5e\xeb'+
'\x74\x77\x04\x4e\x8c\xaf\x23\xdc\x65\xdf\xa5\x65\xdd\x7d\xf4\x3c'+
'\x4c\x95\xbd\xeb\x65\x1c\xf4\x24\x5d\x82\x18\xfb\x50\x86\xb8\x53'+
'\xe0\x4e\x36\x96\x1f\xb7\xcb\xaa\xaf\xea\xcb\x20\x27\x30\x2a\xae'+
'\xb9\x07\x40\xdf\x12\x75\xc9\x09\x82\x9c\x30\x80\x5d\x8f\x0d\x09'+
'\xa1\x64\xec\x91\xd8\x8a\x50\x1f\x40\x5d\xf7\x08\x2a\xf8\x60\x62'+
'\xa0\x4a\x8b\xba\x4a\x6d\x00\x0a\x93\x32\x12\xe5\x07\x01\x65\xf5'+
'\xff\xe0\xae\xa7\x81\xd1\xba\x25\x62\x61\xb2\x85\xad\x7e\x9d\x3f'+
'\x49\x89\x26\xe5\xd5\xac\x9f\x0e\xd7\x6e\x47\x94\x16\x84\xc8\xff'+
'\x44\xea\x04\x40\xe0\x33\x11\xa3\x5b\x1e\x82\xff\x7a\x69\xe9\x2f'+
'\xfb\xea\x9a\xc6\x7b\xdb\xb1\xff\x97\x76\x56\xf3\x52\xc2\x3f\x0f'+
'\xb6\xac\x77\xc4\xbf\x59\x5e\x80\x74\xbb\xf2\xde\x57\x62\x4c\x1a'+
'\xff\x95\x6d\xc7\x04\xa2\x3b\xc4\x1b\x72\xc7\x6c\x82\x60\xd1\x0d';
const code_body =
'\x82\x8b\x7f\x68\x90\xe0\x44\x09\x19\x3b\x8e\x5f\xc2\x82\x38\x23'+
'\x6d\xdb\x62\x49\x52\x6e\x21\xdf\x51\x6c\x76\x37\x86\x50\x7d\x48'+
'\x1f\x65\xe7\x52\x6a\x88\xaa\xc1\x32\x2f\xf7\x54\x4c\xaa\x6d\x7e'+
'\x6d\xa9\x8c\x0d\x3f\xff\x6c\x09\xb3\xa5\xaf\xdf\x98\x02\xb4\xbe'+
'\x6d\x69\x0d\x42\x73\xe4\x34\x50\x07\x30\x79\x41\x2f\x08\x3f\x42'+
'\x73\xa7\x68\xfa\xee\x88\x0e\x6e\xa4\x70\x74\x22\x16\xae\x3c\x81'+
'\x14\xa1\xda\x7f\xd3\x7c\x48\x7d\x3f\x46\xfb\x6d\x92\x25\x17\x36'+
'\x26\xdb\xdf\x5a\x87\x91\x6f\xd6\xcd\xd4\xad\x4a\x29\xdd\x7d\x59'+
'\xbd\x15\x34\x53\xb1\xd8\x50\x11\x83\x79\x66\x21\x9e\x87\x5b\x24'+
'\x2f\x4f\xd7\x73\x34\xa2\xf7\x09\xd5\xd9\x42\x9d\xf8\x15\xdf\x0e'+
'\x10\xcc\x05\x04\x35\x81\xb2\xd5\x7a\xd2\xa0\xa5\x7b\xb8\x75\xd2'+
'\x35\x0b\x39\x8f\x1b\x44\x0e\xce\x66\x87\x1b\x64\xac\xe1\xca\x67'+
'\xb4\xce\x33\xdb\x89\xfe\xd8\x8e\xcd\x58\x92\x41\x50\x40\xcb\x08'+
'\xe1\x15\xee\xf4\x64\xfe\x1c\xee\x25\xe7\x21\xe6\x6c\xc6\xa6\x2e'+
'\x52\x23\xa7\x20\xd2\xd7\x28\x07\x23\x14\x24\x3d\x45\xa5\xc7\x90'+
'\xdb\x77\xdd\xea\x38\x59\x89\x32\xbc\x00\x3a\x6d\x61\x4e\xdb\x29';
const crc_table = [
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
];
import Core from './core.js'
export default class ventrilo extends Core {
constructor () {
super()
this.byteorder = 'be'
}
async run (state) {
const data = await this.sendCommand(2, '')
state.raw = splitFields(data.toString())
for (const client of state.raw.CLIENTS) {
client.name = client.NAME
delete client.NAME
client.ping = parseInt(client.PING)
delete client.PING
state.players.push(client)
}
delete state.raw.CLIENTS
if ('NAME' in state.raw) state.name = state.raw.NAME
if ('MAXCLIENTS' in state.raw) state.maxplayers = state.raw.MAXCLIENTS
if (this.trueTest(state.raw.AUTH)) state.password = true
}
async sendCommand (cmd, password) {
const body = Buffer.alloc(16)
body.write(password, 0, 15, 'utf8')
const encrypted = encrypt(cmd, body)
const packets = {}
return await this.udpSend(encrypted, (buffer) => {
if (buffer.length < 20) return
const data = decrypt(buffer)
if (data.zero !== 0) return
packets[data.packetNum] = data.body
if (Object.keys(packets).length !== data.packetTotal) return
const out = []
for (let i = 0; i < data.packetTotal; i++) {
if (!(i in packets)) throw new Error('Missing packet #' + i)
out.push(packets[i])
}
return Buffer.concat(out)
})
}
}
function splitFields (str, subMode) {
let splitter, delim
if (subMode) {
splitter = '='
delim = ','
} else {
splitter = ': '
delim = '\n'
}
const split = str.split(delim)
const out = {}
if (!subMode) {
out.CHANNELS = []
out.CLIENTS = []
}
for (const one of split) {
const equal = one.indexOf(splitter)
const key = equal === -1 ? one : one.substring(0, equal)
if (!key || key === '\0') continue
const value = equal === -1 ? '' : one.substring(equal + splitter.length)
if (!subMode && key === 'CHANNEL') out.CHANNELS.push(splitFields(value, true))
else if (!subMode && key === 'CLIENT') out.CLIENTS.push(splitFields(value, true))
else out[key] = value
}
return out
}
function randInt (min, max) {
return Math.floor(Math.random() * (max - min + 1) + min)
}
function crc (body) {
let crc = 0
for (let i = 0; i < body.length; i++) {
crc = crcTable[crc >> 8] ^ body.readUInt8(i) ^ (crc << 8)
crc &= 0xffff
}
return crc
}
function encrypt (cmd, body) {
const headerKeyStart = randInt(0, 0xff)
const headerKeyAdd = randInt(1, 0xff)
const bodyKeyStart = randInt(0, 0xff)
const bodyKeyAdd = randInt(1, 0xff)
const header = Buffer.alloc(20)
header.writeUInt8(headerKeyStart, 0)
header.writeUInt8(headerKeyAdd, 1)
header.writeUInt16BE(cmd, 4)
header.writeUInt16BE(body.length, 8)
header.writeUInt16BE(body.length, 10)
header.writeUInt16BE(1, 12)
header.writeUInt16BE(0, 14)
header.writeUInt8(bodyKeyStart, 16)
header.writeUInt8(bodyKeyAdd, 17)
header.writeUInt16BE(crc(body), 18)
let offset = headerKeyStart
for (let i = 2; i < header.length; i++) {
let val = header.readUInt8(i)
val += codeHead.charCodeAt(offset) + ((i - 2) % 5)
val = val & 0xff
header.writeUInt8(val, i)
offset = (offset + headerKeyAdd) & 0xff
}
offset = bodyKeyStart
for (let i = 0; i < body.length; i++) {
let val = body.readUInt8(i)
val += codeBody.charCodeAt(offset) + (i % 72)
val = val & 0xff
body.writeUInt8(val, i)
offset = (offset + bodyKeyAdd) & 0xff
}
return Buffer.concat([header, body])
}
function decrypt (data) {
const header = data.slice(0, 20)
const body = data.slice(20)
const headerKeyStart = header.readUInt8(0)
const headerKeyAdd = header.readUInt8(1)
let offset = headerKeyStart
for (let i = 2; i < header.length; i++) {
let val = header.readUInt8(i)
val -= codeHead.charCodeAt(offset) + ((i - 2) % 5)
val = val & 0xff
header.writeUInt8(val, i)
offset = (offset + headerKeyAdd) & 0xff
}
const bodyKeyStart = header.readUInt8(16)
const bodyKeyAdd = header.readUInt8(17)
offset = bodyKeyStart
for (let i = 0; i < body.length; i++) {
let val = body.readUInt8(i)
val -= codeBody.charCodeAt(offset) + (i % 72)
val = val & 0xff
body.writeUInt8(val, i)
offset = (offset + bodyKeyAdd) & 0xff
}
// header format:
// key, zero, cmd, echo, totallength, thislength
// totalpacket, packetnum, body key, crc
return {
zero: header.readUInt16BE(2),
cmd: header.readUInt16BE(4),
packetTotal: header.readUInt16BE(12),
packetNum: header.readUInt16BE(14),
body
}
}
const codeHead =
'\x80\xe5\x0e\x38\xba\x63\x4c\x99\x88\x63\x4c\xd6\x54\xb8\x65\x7e' +
'\xbf\x8a\xf0\x17\x8a\xaa\x4d\x0f\xb7\x23\x27\xf6\xeb\x12\xf8\xea' +
'\x17\xb7\xcf\x52\x57\xcb\x51\xcf\x1b\x14\xfd\x6f\x84\x38\xb5\x24' +
'\x11\xcf\x7a\x75\x7a\xbb\x78\x74\xdc\xbc\x42\xf0\x17\x3f\x5e\xeb' +
'\x74\x77\x04\x4e\x8c\xaf\x23\xdc\x65\xdf\xa5\x65\xdd\x7d\xf4\x3c' +
'\x4c\x95\xbd\xeb\x65\x1c\xf4\x24\x5d\x82\x18\xfb\x50\x86\xb8\x53' +
'\xe0\x4e\x36\x96\x1f\xb7\xcb\xaa\xaf\xea\xcb\x20\x27\x30\x2a\xae' +
'\xb9\x07\x40\xdf\x12\x75\xc9\x09\x82\x9c\x30\x80\x5d\x8f\x0d\x09' +
'\xa1\x64\xec\x91\xd8\x8a\x50\x1f\x40\x5d\xf7\x08\x2a\xf8\x60\x62' +
'\xa0\x4a\x8b\xba\x4a\x6d\x00\x0a\x93\x32\x12\xe5\x07\x01\x65\xf5' +
'\xff\xe0\xae\xa7\x81\xd1\xba\x25\x62\x61\xb2\x85\xad\x7e\x9d\x3f' +
'\x49\x89\x26\xe5\xd5\xac\x9f\x0e\xd7\x6e\x47\x94\x16\x84\xc8\xff' +
'\x44\xea\x04\x40\xe0\x33\x11\xa3\x5b\x1e\x82\xff\x7a\x69\xe9\x2f' +
'\xfb\xea\x9a\xc6\x7b\xdb\xb1\xff\x97\x76\x56\xf3\x52\xc2\x3f\x0f' +
'\xb6\xac\x77\xc4\xbf\x59\x5e\x80\x74\xbb\xf2\xde\x57\x62\x4c\x1a' +
'\xff\x95\x6d\xc7\x04\xa2\x3b\xc4\x1b\x72\xc7\x6c\x82\x60\xd1\x0d'
const codeBody =
'\x82\x8b\x7f\x68\x90\xe0\x44\x09\x19\x3b\x8e\x5f\xc2\x82\x38\x23' +
'\x6d\xdb\x62\x49\x52\x6e\x21\xdf\x51\x6c\x76\x37\x86\x50\x7d\x48' +
'\x1f\x65\xe7\x52\x6a\x88\xaa\xc1\x32\x2f\xf7\x54\x4c\xaa\x6d\x7e' +
'\x6d\xa9\x8c\x0d\x3f\xff\x6c\x09\xb3\xa5\xaf\xdf\x98\x02\xb4\xbe' +
'\x6d\x69\x0d\x42\x73\xe4\x34\x50\x07\x30\x79\x41\x2f\x08\x3f\x42' +
'\x73\xa7\x68\xfa\xee\x88\x0e\x6e\xa4\x70\x74\x22\x16\xae\x3c\x81' +
'\x14\xa1\xda\x7f\xd3\x7c\x48\x7d\x3f\x46\xfb\x6d\x92\x25\x17\x36' +
'\x26\xdb\xdf\x5a\x87\x91\x6f\xd6\xcd\xd4\xad\x4a\x29\xdd\x7d\x59' +
'\xbd\x15\x34\x53\xb1\xd8\x50\x11\x83\x79\x66\x21\x9e\x87\x5b\x24' +
'\x2f\x4f\xd7\x73\x34\xa2\xf7\x09\xd5\xd9\x42\x9d\xf8\x15\xdf\x0e' +
'\x10\xcc\x05\x04\x35\x81\xb2\xd5\x7a\xd2\xa0\xa5\x7b\xb8\x75\xd2' +
'\x35\x0b\x39\x8f\x1b\x44\x0e\xce\x66\x87\x1b\x64\xac\xe1\xca\x67' +
'\xb4\xce\x33\xdb\x89\xfe\xd8\x8e\xcd\x58\x92\x41\x50\x40\xcb\x08' +
'\xe1\x15\xee\xf4\x64\xfe\x1c\xee\x25\xe7\x21\xe6\x6c\xc6\xa6\x2e' +
'\x52\x23\xa7\x20\xd2\xd7\x28\x07\x23\x14\x24\x3d\x45\xa5\xc7\x90' +
'\xdb\x77\xdd\xea\x38\x59\x89\x32\xbc\x00\x3a\x6d\x61\x4e\xdb\x29'
const crcTable = [
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
]

View file

@ -1,13 +1,13 @@
import quake3 from './quake3.js';
export default class warsow extends quake3 {
async run(state) {
await super.run(state);
if(state.players) {
for(const player of state.players) {
player.team = player.address;
delete player.address;
}
}
}
}
import quake3 from './quake3.js'
export default class warsow extends quake3 {
async run (state) {
await super.run(state)
if (state.players) {
for (const player of state.players) {
player.team = player.address
delete player.address
}
}
}
}