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

View File

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

View File

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

View File

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

View File

@ -1,122 +1,114 @@
import * as path from 'path'; import * as path from 'path'
import { fileURLToPath } from "url"; import { fileURLToPath } from 'url'
import * as fs from 'fs'; import * as fs from 'fs'
export default class GameResolver { export default class GameResolver {
constructor() { constructor () {
const loaded = this._readGames(); const loaded = this._readGames()
this.gamesByKey = loaded.gamesByKey; this.gamesByKey = loaded.gamesByKey
this.games = loaded.games; this.games = loaded.games
} }
lookup(type) { lookup (type) {
if(!type) if (!type) { throw Error('No game specified') }
throw Error('No game specified');
if (type.substring(0, 9) === 'protocol-') {
if(type.substring(0,9) === 'protocol-') { return {
return { protocol: type.substring(9)
protocol: type.substring(9) }
}; }
}
const game = this.gamesByKey.get(type)
const game = this.gamesByKey.get(type);
if (!game) { throw Error('Invalid game: ' + type) }
if(!game)
throw Error('Invalid game: '+type); return game.options
}
return game.options;
} printReadme () {
let out = ''
printReadme() { out += '| GameDig Type ID | Name | See Also\n'
let out = ''; out += '|---|---|---\n'
out += '| GameDig Type ID | Name | See Also\n';
out += '|---|---|---\n'; const sorted = this.games
.filter(game => game.pretty)
const sorted = this.games .sort((a, b) => {
.filter(game => game.pretty) return a.pretty.localeCompare(b.pretty)
.sort((a,b) => { })
return a.pretty.localeCompare(b.pretty); for (const game of sorted) {
}); const keysOut = game.keys.map(key => '`' + key + '`').join('<br>')
for(const game of sorted) { out += '| ' + keysOut.padEnd(10, ' ') + ' ' +
let keysOut = game.keys.map(key => '`'+key+'`').join('<br>'); '| ' + game.pretty
out += "| " + keysOut.padEnd(10, " ") + " " const notes = []
+ "| " + game.pretty; if (game.extra.doc_notes) {
let notes = []; notes.push('[Notes](#' + game.extra.doc_notes + ')')
if(game.extra.doc_notes) { }
notes.push("[Notes](#" + game.extra.doc_notes + ")"); if (game.options.protocol === 'valve') {
} notes.push('[Valve Protocol](#valve)')
if(game.options.protocol === 'valve') { }
notes.push('[Valve Protocol](#valve)'); if (notes.length) {
} out += ' | ' + notes.join(', ')
if(notes.length) { }
out += " | " + notes.join(', '); out += '\n'
} }
out += "\n"; return out
} }
return out;
} _readGames () {
const __filename = fileURLToPath(import.meta.url)
_readGames() { const __dirname = path.dirname(__filename)
const __filename = fileURLToPath(import.meta.url); const gamesFile = path.normalize(__dirname + '/../games.txt')
const __dirname = path.dirname(__filename); const lines = fs.readFileSync(gamesFile, 'utf8').split('\n')
const gamesFile = path.normalize(__dirname+'/../games.txt');
const lines = fs.readFileSync(gamesFile,'utf8').split('\n'); const gamesByKey = new Map()
const games = []
const gamesByKey = new Map();
const games = []; for (let line of lines) {
// strip comments
for (let line of lines) { const comment = line.indexOf('#')
// strip comments if (comment !== -1) line = line.substring(0, comment)
const comment = line.indexOf('#'); line = line.trim()
if(comment !== -1) line = line.substring(0,comment); if (!line) continue
line = line.trim();
if(!line) continue; const split = line.split('|')
const keys = split[0].trim().split(',')
const split = line.split('|'); const name = split[1].trim()
const keys = split[0].trim().split(','); const options = this._parseList(split[3])
const name = split[1].trim(); options.protocol = split[2].trim()
const options = this._parseList(split[3]); const extra = this._parseList(split[4])
options.protocol = split[2].trim();
const extra = this._parseList(split[4]); const game = {
keys,
const game = { pretty: name,
keys: keys, options,
pretty: name, extra
options: options, }
extra: extra
}; for (const key of keys) {
gamesByKey.set(key, game)
for (const key of keys) { }
gamesByKey.set(key, game);
} games.push(game)
}
games.push(game); return { gamesByKey, games }
} }
return { gamesByKey, games };
} _parseList (str) {
if (!str) { return {} }
_parseList(str) {
if(!str) const out = {}
return {}; for (const one of str.split(',')) {
const equals = one.indexOf('=')
let out = {}; const key = equals === -1 ? one : one.substring(0, equals)
for (const one of str.split(',')) {
const equals = one.indexOf('='); /** @type {string|number|boolean} */
const key = equals === -1 ? one : one.substring(0, equals); let value = equals === -1 ? '' : one.substring(equals + 1)
/** @type {string|number|boolean} */ if (value === 'true' || value === '') { value = true } else if (value === 'false') { value = false } else if (!isNaN(parseInt(value))) { value = parseInt(value) }
let value = equals === -1 ? '' : one.substring(equals + 1);
out[key] = value
if(value === 'true' || value === '') }
value = true;
else if(value === 'false') return out
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 { createSocket } from "dgram"; import { debugDump } from './HexUtil.js'
import { debugDump } from "./HexUtil.js"; import { promisify } from 'util'
import { promisify } from "util"; import Logger from './Logger.js'
import Logger from "./Logger.js";
export default class GlobalUdpSocket {
export default class GlobalUdpSocket { constructor ({ port }) {
constructor({port}) { this.socket = null
this.socket = null; this.callbacks = new Set()
this.callbacks = new Set(); this.debuggingCallbacks = new Set()
this.debuggingCallbacks = new Set(); this.logger = new Logger()
this.logger = new Logger(); this.port = port
this.port = port; }
}
async _getSocket () {
async _getSocket() { if (!this.socket) {
if (!this.socket) { const udpSocket = createSocket({
const udpSocket = createSocket({ type: 'udp4',
type: 'udp4', reuseAddr: true
reuseAddr: true })
}); udpSocket.unref()
udpSocket.unref(); udpSocket.on('message', (buffer, rinfo) => {
udpSocket.on('message', (buffer, rinfo) => { const fromAddress = rinfo.address
const fromAddress = rinfo.address; const fromPort = rinfo.port
const fromPort = rinfo.port; this.logger.debug(log => {
this.logger.debug(log => { log(fromAddress + ':' + fromPort + ' <--UDP(' + this.port + ')')
log(fromAddress + ':' + fromPort + " <--UDP(" + this.port + ")"); log(debugDump(buffer))
log(debugDump(buffer)); })
}); for (const callback of this.callbacks) {
for (const callback of this.callbacks) { callback(fromAddress, fromPort, buffer)
callback(fromAddress, fromPort, buffer); }
} })
}); udpSocket.on('error', e => {
udpSocket.on('error', e => { this.logger.debug('UDP ERROR:', e)
this.logger.debug("UDP ERROR:", e); })
}); await promisify(udpSocket.bind).bind(udpSocket)(this.port)
await promisify(udpSocket.bind).bind(udpSocket)(this.port); this.port = udpSocket.address().port
this.port = udpSocket.address().port; this.socket = udpSocket
this.socket = udpSocket; }
} return this.socket
return this.socket; }
}
async send (buffer, address, port, debug) {
async send(buffer, address, port, debug) { const socket = await this._getSocket()
const socket = await this._getSocket();
if (debug) {
if (debug) { this.logger._print(log => {
this.logger._print(log => { log(address + ':' + port + ' UDP(' + this.port + ')-->')
log(address + ':' + port + " UDP(" + this.port + ")-->"); log(debugDump(buffer))
log(debugDump(buffer)); })
}); }
}
await promisify(socket.send).bind(socket)(buffer, 0, buffer.length, port, address)
await promisify(socket.send).bind(socket)(buffer,0,buffer.length,port,address); }
}
addCallback (callback, debug) {
addCallback(callback, debug) { this.callbacks.add(callback)
this.callbacks.add(callback); if (debug) {
if (debug) { this.debuggingCallbacks.add(callback)
this.debuggingCallbacks.add(callback); this.logger.debugEnabled = true
this.logger.debugEnabled = true; }
} }
}
removeCallback(callback) { removeCallback (callback) {
this.callbacks.delete(callback); this.callbacks.delete(callback)
this.debuggingCallbacks.delete(callback); this.debuggingCallbacks.delete(callback)
this.logger.debugEnabled = this.debuggingCallbacks.size > 0; this.logger.debugEnabled = this.debuggingCallbacks.size > 0
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,172 +1,172 @@
import Iconv from "iconv-lite"; import Iconv from 'iconv-lite'
import Long from 'long'; import Long from 'long'
import {Buffer} from "buffer"; import { Buffer } from 'buffer'
import Varint from 'varint'; import Varint from 'varint'
function readUInt64BE(buffer,offset) { function readUInt64BE (buffer, offset) {
const high = buffer.readUInt32BE(offset); const high = buffer.readUInt32BE(offset)
const low = buffer.readUInt32BE(offset+4); const low = buffer.readUInt32BE(offset + 4)
return new Long(low,high,true); return new Long(low, high, true)
} }
function readUInt64LE(buffer,offset) { function readUInt64LE (buffer, offset) {
const low = buffer.readUInt32LE(offset); const low = buffer.readUInt32LE(offset)
const high = buffer.readUInt32LE(offset+4); const high = buffer.readUInt32LE(offset + 4)
return new Long(low,high,true); return new Long(low, high, true)
} }
export default class Reader { export default class Reader {
/** /**
* @param {Core} query * @param {Core} query
* @param {Buffer} buffer * @param {Buffer} buffer
**/ **/
constructor(query,buffer) { constructor (query, buffer) {
this.defaultEncoding = query.options.encoding || query.encoding; this.defaultEncoding = query.options.encoding || query.encoding
this.defaultDelimiter = query.delimiter; this.defaultDelimiter = query.delimiter
this.defaultByteOrder = query.byteorder; this.defaultByteOrder = query.byteorder
this.buffer = buffer; this.buffer = buffer
this.i = 0; this.i = 0
} }
setOffset(offset) { setOffset (offset) {
this.i = offset; this.i = offset
} }
offset() { offset () {
return this.i; return this.i
} }
skip(i) { skip (i) {
this.i += i; this.i += i
} }
pascalString(bytesForSize, adjustment=0) { pascalString (bytesForSize, adjustment = 0) {
const length = this.uint(bytesForSize) + adjustment; const length = this.uint(bytesForSize) + adjustment
return this.string(length); return this.string(length)
} }
string(arg) { string (arg) {
let encoding = this.defaultEncoding; let encoding = this.defaultEncoding
let length = null; let length = null
let delimiter = this.defaultDelimiter; let delimiter = this.defaultDelimiter
if(typeof arg === 'string') delimiter = arg; if (typeof arg === 'string') delimiter = arg
else if(typeof arg === 'number') length = arg; else if (typeof arg === 'number') length = arg
else if(typeof arg === 'object') { else if (typeof arg === 'object') {
if ('encoding' in arg) encoding = arg.encoding; if ('encoding' in arg) encoding = arg.encoding
if ('length' in arg) length = arg.length; if ('length' in arg) length = arg.length
if ('delimiter' in arg) delimiter = arg.delimiter; if ('delimiter' in arg) delimiter = arg.delimiter
} }
if(encoding === 'latin1') encoding = 'win1252'; if (encoding === 'latin1') encoding = 'win1252'
const start = this.i; const start = this.i
let end = start; let end = start
if(length === null) { if (length === null) {
// terminated by the delimiter // terminated by the delimiter
let delim = delimiter; let delim = delimiter
if (typeof delim === 'string') delim = delim.charCodeAt(0); if (typeof delim === 'string') delim = delim.charCodeAt(0)
while (true) { while (true) {
if (end >= this.buffer.length) { if (end >= this.buffer.length) {
end = this.buffer.length; end = this.buffer.length
break; break
} }
if (this.buffer.readUInt8(end) === delim) break; if (this.buffer.readUInt8(end) === delim) break
end++; end++
} }
this.i = end + 1; this.i = end + 1
} else if (length <= 0) { } else if (length <= 0) {
return ''; return ''
} else { } else {
end = start+length; end = start + length
if(end >= this.buffer.length) { if (end >= this.buffer.length) {
end = this.buffer.length; end = this.buffer.length
} }
this.i = end; this.i = end
} }
const slice = this.buffer.slice(start, end); const slice = this.buffer.slice(start, end)
const enc = encoding; const enc = encoding
if(enc === 'utf8' || enc === 'ucs2' || enc === 'binary') { if (enc === 'utf8' || enc === 'ucs2' || enc === 'binary') {
return slice.toString(enc); return slice.toString(enc)
} else { } else {
return Iconv.decode(slice,enc); return Iconv.decode(slice, enc)
} }
} }
int(bytes) { int (bytes) {
let r = 0; let r = 0
if(this.remaining() >= bytes) { if (this.remaining() >= bytes) {
if(this.defaultByteOrder === 'be') { if (this.defaultByteOrder === 'be') {
if(bytes === 1) r = this.buffer.readInt8(this.i); if (bytes === 1) r = this.buffer.readInt8(this.i)
else if(bytes === 2) r = this.buffer.readInt16BE(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 === 4) r = this.buffer.readInt32BE(this.i)
} else { } else {
if(bytes === 1) r = this.buffer.readInt8(this.i); if (bytes === 1) r = this.buffer.readInt8(this.i)
else if(bytes === 2) r = this.buffer.readInt16LE(this.i); else if (bytes === 2) r = this.buffer.readInt16LE(this.i)
else if(bytes === 4) r = this.buffer.readInt32LE(this.i); else if (bytes === 4) r = this.buffer.readInt32LE(this.i)
} }
} }
this.i += bytes; this.i += bytes
return r; return r
} }
/** @returns {number} */ /** @returns {number} */
uint(bytes) { uint (bytes) {
let r = 0; let r = 0
if(this.remaining() >= bytes) { if (this.remaining() >= bytes) {
if(this.defaultByteOrder === 'be') { if (this.defaultByteOrder === 'be') {
if(bytes === 1) r = this.buffer.readUInt8(this.i); if (bytes === 1) r = this.buffer.readUInt8(this.i)
else if(bytes === 2) r = this.buffer.readUInt16BE(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 === 4) r = this.buffer.readUInt32BE(this.i)
else if(bytes === 8) r = readUInt64BE(this.buffer,this.i); else if (bytes === 8) r = readUInt64BE(this.buffer, this.i)
} else { } else {
if(bytes === 1) r = this.buffer.readUInt8(this.i); if (bytes === 1) r = this.buffer.readUInt8(this.i)
else if(bytes === 2) r = this.buffer.readUInt16LE(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 === 4) r = this.buffer.readUInt32LE(this.i)
else if(bytes === 8) r = readUInt64LE(this.buffer,this.i); else if (bytes === 8) r = readUInt64LE(this.buffer, this.i)
} }
} }
this.i += bytes; this.i += bytes
return r; return r
} }
float() { float () {
let r = 0; let r = 0
if(this.remaining() >= 4) { if (this.remaining() >= 4) {
if(this.defaultByteOrder === 'be') r = this.buffer.readFloatBE(this.i); if (this.defaultByteOrder === 'be') r = this.buffer.readFloatBE(this.i)
else r = this.buffer.readFloatLE(this.i); else r = this.buffer.readFloatLE(this.i)
} }
this.i += 4; this.i += 4
return r; return r
} }
varint() { varint () {
const out = Varint.decode(this.buffer, this.i); const out = Varint.decode(this.buffer, this.i)
this.i += Varint.decode.bytes; this.i += Varint.decode.bytes
return out; return out
} }
/** @returns Buffer */ /** @returns Buffer */
part(bytes) { part (bytes) {
let r; let r
if(this.remaining() >= bytes) { if (this.remaining() >= bytes) {
r = this.buffer.slice(this.i,this.i+bytes); r = this.buffer.slice(this.i, this.i + bytes)
} else { } else {
r = Buffer.from([]); r = Buffer.from([])
} }
this.i += bytes; this.i += bytes
return r; return r
} }
remaining() { remaining () {
return this.buffer.length-this.i; return this.buffer.length - this.i
} }
rest() { rest () {
return this.buffer.slice(this.i); return this.buffer.slice(this.i)
} }
done() { done () {
return this.i >= this.buffer.length; 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", "name": "gamedig",
"description": "Query for the status of any game server in Node.JS", "description": "Query for the status of any game server in Node.JS",
"scripts": {
"lint:check": "eslint .",
"lint:fix": "eslint --fix ."
},
"keywords": [ "keywords": [
"srcds", "srcds",
"query", "query",
@ -35,17 +39,6 @@
"engines": { "engines": {
"node": ">=14.17.0" "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": { "bin": {
"gamedig": "bin/gamedig.js" "gamedig": "bin/gamedig.js"
}, },
@ -58,8 +51,24 @@
"GAMES_LIST.md", "GAMES_LIST.md",
"README.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": { "devDependencies": {
"@types/cheerio": "^0.22.31", "@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'; import Core from './core.js'
export default class armagetron extends Core { export default class armagetron extends Core {
constructor() { constructor () {
super(); super()
this.encoding = 'latin1'; this.encoding = 'latin1'
this.byteorder = 'be'; this.byteorder = 'be'
} }
async run(state) { async run (state) {
const b = Buffer.from([0,0x35,0,0,0,0,0,0x11]); const b = Buffer.from([0, 0x35, 0, 0, 0, 0, 0, 0x11])
const buffer = await this.udpSend(b,b => b); const buffer = await this.udpSend(b, b => b)
const reader = this.reader(buffer); const reader = this.reader(buffer)
reader.skip(6); reader.skip(6)
state.gamePort = this.readUInt(reader); state.gamePort = this.readUInt(reader)
state.raw.hostname = this.readString(reader); state.raw.hostname = this.readString(reader)
state.name = this.stripColorCodes(this.readString(reader)); state.name = this.stripColorCodes(this.readString(reader))
state.raw.numplayers = this.readUInt(reader); state.raw.numplayers = this.readUInt(reader)
state.raw.versionmin = this.readUInt(reader); state.raw.versionmin = this.readUInt(reader)
state.raw.versionmax = this.readUInt(reader); state.raw.versionmax = this.readUInt(reader)
state.raw.version = this.readString(reader); state.raw.version = this.readString(reader)
state.maxplayers = this.readUInt(reader); state.maxplayers = this.readUInt(reader)
const players = this.readString(reader); const players = this.readString(reader)
const list = players.split('\n'); const list = players.split('\n')
for(const name of list) { for (const name of list) {
if(!name) continue; if (!name) continue
state.players.push({ state.players.push({
name: this.stripColorCodes(name) name: this.stripColorCodes(name)
}); })
} }
state.raw.options = this.stripColorCodes(this.readString(reader)); state.raw.options = this.stripColorCodes(this.readString(reader))
state.raw.uri = this.readString(reader); state.raw.uri = this.readString(reader)
state.raw.globalids = this.readString(reader); state.raw.globalids = this.readString(reader)
} }
readUInt(reader) { readUInt (reader) {
const a = reader.uint(2); const a = reader.uint(2)
const b = reader.uint(2); const b = reader.uint(2)
return (b<<16) + a; return (b << 16) + a
} }
readString(reader) {
const len = reader.uint(2); readString (reader) {
if(!len) return ''; const len = reader.uint(2)
if (!len) return ''
let out = '';
for(let i = 0; i < len; i += 2) { let out = ''
const hi = reader.uint(1); for (let i = 0; i < len; i += 2) {
const lo = reader.uint(1); const hi = reader.uint(1)
if(i+1<len) out += String.fromCharCode(lo); const lo = reader.uint(1)
if(i+2<len) out += String.fromCharCode(hi); if (i + 1 < len) out += String.fromCharCode(lo)
} if (i + 2 < len) out += String.fromCharCode(hi)
}
return out;
} return out
stripColorCodes(str) { }
return str.replace(/0x[0-9a-f]{6}/g,'');
} stripColorCodes (str) {
} return str.replace(/0x[0-9a-f]{6}/g, '')
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,29 +1,29 @@
import Core from './core.js'; import Core from './core.js'
export default class discord extends Core { export default class discord extends Core {
async run(state) { async run (state) {
const guildId = this.options.guildId; const guildId = this.options.guildId
if (typeof guildId !== 'string') { 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.' 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)"); " (It's too large of a number for javascript to store without losing precision)")
} }
this.usedTcp = true; this.usedTcp = true
const raw = await this.request({ const raw = await this.request({
url: 'https://discordapp.com/api/guilds/' + guildId + '/widget.json', url: 'https://discordapp.com/api/guilds/' + guildId + '/widget.json'
}); })
const json = JSON.parse(raw); const json = JSON.parse(raw)
state.name = json.name; state.name = json.name
if (json.instant_invite) { if (json.instant_invite) {
state.connect = json.instant_invite; state.connect = json.instant_invite
} else { } else {
state.connect = 'https://discordapp.com/channels/' + guildId; state.connect = 'https://discordapp.com/channels/' + guildId
} }
for (const member of json.members) { for (const member of json.members) {
const {username: name, ...rest} = member; const { username: name, ...rest } = member
state.players.push({ name, ...rest }); state.players.push({ name, ...rest })
} }
delete json.members; delete json.members
state.maxplayers = 500000; state.maxplayers = 500000
state.raw = json; state.raw = json
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,53 +1,55 @@
import armagetron from "./armagetron.js"; import armagetron from './armagetron.js'
import ase from "./ase.js"; import ase from './ase.js'
import assettocorsa from "./assettocorsa.js"; import assettocorsa from './assettocorsa.js'
import battlefield from "./battlefield.js"; import battlefield from './battlefield.js'
import buildandshoot from "./buildandshoot.js"; import buildandshoot from './buildandshoot.js'
import cs2d from "./cs2d.js"; import cs2d from './cs2d.js'
import discord from "./discord.js"; import discord from './discord.js'
import doom3 from "./doom3.js"; import doom3 from './doom3.js'
import eco from "./eco.js"; import eco from './eco.js'
import ffow from "./ffow.js"; import ffow from './ffow.js'
import fivem from "./fivem.js"; import fivem from './fivem.js'
import gamespy1 from "./gamespy1.js"; import gamespy1 from './gamespy1.js'
import gamespy2 from "./gamespy2.js"; import gamespy2 from './gamespy2.js'
import gamespy3 from "./gamespy3.js"; import gamespy3 from './gamespy3.js'
import geneshift from "./geneshift.js"; import geneshift from './geneshift.js'
import goldsrc from "./goldsrc.js"; import goldsrc from './goldsrc.js'
import hexen2 from "./hexen2.js"; import hexen2 from './hexen2.js'
import jc2mp from "./jc2mp.js"; import jc2mp from './jc2mp.js'
import kspdmp from "./kspdmp.js"; import kspdmp from './kspdmp.js'
import mafia2mp from "./mafia2mp.js"; import mafia2mp from './mafia2mp.js'
import mafia2online from "./mafia2online.js"; import mafia2online from './mafia2online.js'
import minecraft from "./minecraft.js"; import minecraft from './minecraft.js'
import minecraftbedrock from "./minecraftbedrock.js"; import minecraftbedrock from './minecraftbedrock.js'
import minecraftvanilla from "./minecraftvanilla.js"; import minecraftvanilla from './minecraftvanilla.js'
import mumble from "./mumble.js"; import mumble from './mumble.js'
import mumbleping from "./mumbleping.js"; import mumbleping from './mumbleping.js'
import nadeo from "./nadeo.js"; import nadeo from './nadeo.js'
import openttd from "./openttd.js"; import openttd from './openttd.js'
import quake1 from "./quake1.js"; import quake1 from './quake1.js'
import quake2 from "./quake2.js"; import quake2 from './quake2.js'
import quake3 from "./quake3.js"; import quake3 from './quake3.js'
import rfactor from "./rfactor.js"; import rfactor from './rfactor.js'
import samp from "./samp.js"; import samp from './samp.js'
import savage2 from "./savage2.js"; import savage2 from './savage2.js'
import starmade from "./starmade.js"; import starmade from './starmade.js'
import starsiege from "./starsiege.js"; import starsiege from './starsiege.js'
import teamspeak2 from "./teamspeak2.js"; import teamspeak2 from './teamspeak2.js'
import teamspeak3 from "./teamspeak3.js"; import teamspeak3 from './teamspeak3.js'
import terraria from "./terraria.js"; import terraria from './terraria.js'
import tribes1 from "./tribes1.js"; import tribes1 from './tribes1.js'
import tribes1master from "./tribes1master.js"; import tribes1master from './tribes1master.js'
import unreal2 from "./unreal2.js"; import unreal2 from './unreal2.js'
import ut3 from "./ut3.js"; import ut3 from './ut3.js'
import valve from "./valve.js"; import valve from './valve.js'
import vcmp from "./vcmp.js"; import vcmp from './vcmp.js'
import ventrilo from "./ventrilo.js"; import ventrilo from './ventrilo.js'
import warsow from "./warsow.js"; import warsow from './warsow.js'
export { armagetron, ase, assettocorsa, battlefield, buildandshoot, cs2d, discord, doom3, eco, ffow, fivem, gamespy1, export {
gamespy2, gamespy3, geneshift, goldsrc, hexen2, jc2mp, kspdmp, mafia2mp, mafia2online, minecraft, armagetron, ase, assettocorsa, battlefield, buildandshoot, cs2d, discord, doom3, eco, ffow, fivem, gamespy1,
minecraftbedrock, minecraftvanilla, mumble, mumbleping, nadeo, openttd, quake1, quake2, quake3, rfactor, samp, gamespy2, gamespy3, geneshift, goldsrc, hexen2, jc2mp, kspdmp, mafia2mp, mafia2online, minecraft,
savage2, starmade, starsiege, teamspeak2, teamspeak3, terraria, tribes1, tribes1master, unreal2, ut3, valve, minecraftbedrock, minecraftvanilla, mumble, mumbleping, nadeo, openttd, quake1, quake2, quake3, rfactor, samp,
vcmp, ventrilo, warsow } 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'; import gamespy3 from './gamespy3.js'
// supposedly, gamespy3 is the "official" query protocol for jcmp, // supposedly, gamespy3 is the "official" query protocol for jcmp,
// but it's broken (requires useOnlySingleSplit), and may not include some player names // but it's broken (requires useOnlySingleSplit), and may not include some player names
export default class jc2mp extends gamespy3 { export default class jc2mp extends gamespy3 {
constructor() { constructor () {
super(); super()
this.useOnlySingleSplit = true; this.useOnlySingleSplit = true
this.isJc2mp = true; this.isJc2mp = true
this.encoding = 'utf8'; this.encoding = 'utf8'
} }
async run(state) {
await super.run(state); async run (state) {
if(!state.players.length && parseInt(state.raw.numplayers)) { await super.run(state)
state.players.setNum(parseInt(state.raw.numplayers)); 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'; import Core from './core.js'
export default class kspdmp extends Core { export default class kspdmp extends Core {
async run(state) { async run (state) {
const json = await this.request({ const json = await this.request({
url: 'http://'+this.options.address+':'+this.options.port, url: 'http://' + this.options.address + ':' + this.options.port,
responseType: 'json' responseType: 'json'
}); })
for (const one of json.players) { for (const one of json.players) {
state.players.push({name:one.nickname,team:one.team}); state.players.push({ name: one.nickname, team: one.team })
} }
for (const key of Object.keys(json)) { for (const key of Object.keys(json)) {
state.raw[key] = json[key]; state.raw[key] = json[key]
} }
state.name = json.server_name; state.name = json.server_name
state.maxplayers = json.max_players; state.maxplayers = json.max_players
state.gamePort = json.port; state.gamePort = json.port
if (json.players) { if (json.players) {
const split = json.players.split(', '); const split = json.players.split(', ')
for (const name of split) { for (const name of split) {
state.players.push({name:name}); state.players.push({ name })
} }
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,74 +1,69 @@
import Core from './core.js'; import Core from './core.js'
export default class rfactor extends Core { export default class rfactor extends Core {
constructor() { async run (state) {
super(); const buffer = await this.udpSend('rF_S', b => b)
//this.byteorder = 'be'; const reader = this.reader(buffer)
}
state.raw.gamename = this.readString(reader, 8)
async run(state) { state.raw.fullUpdate = reader.uint(1)
const buffer = await this.udpSend('rF_S',b => b); state.raw.region = reader.uint(2)
const reader = this.reader(buffer); state.raw.ip = reader.part(4)
state.raw.size = reader.uint(2)
state.raw.gamename = this.readString(reader, 8); state.raw.version = reader.uint(2)
state.raw.fullUpdate = reader.uint(1); state.raw.versionRaceCast = reader.uint(2)
state.raw.region = reader.uint(2); state.gamePort = reader.uint(2)
state.raw.ip = reader.part(4); state.raw.queryPort = reader.uint(2)
state.raw.size = reader.uint(2); state.raw.game = this.readString(reader, 20)
state.raw.version = reader.uint(2); state.name = this.readString(reader, 28)
state.raw.versionRaceCast = reader.uint(2); state.map = this.readString(reader, 32)
state.gamePort = reader.uint(2); state.raw.motd = this.readString(reader, 96)
state.raw.queryPort = reader.uint(2); state.raw.packedAids = reader.uint(2)
state.raw.game = this.readString(reader, 20); state.raw.ping = reader.uint(2)
state.name = this.readString(reader, 28); state.raw.packedFlags = reader.uint(1)
state.map = this.readString(reader, 32); state.raw.rate = reader.uint(1)
state.raw.motd = this.readString(reader, 96); state.players.setNum(reader.uint(1))
state.raw.packedAids = reader.uint(2); state.maxplayers = reader.uint(1)
state.raw.ping = reader.uint(2); state.raw.bots = reader.uint(1)
state.raw.packedFlags = reader.uint(1); state.raw.packedSpecial = reader.uint(1)
state.raw.rate = reader.uint(1); state.raw.damage = reader.uint(1)
state.players.setNum(reader.uint(1)); state.raw.packedRules = reader.uint(2)
state.maxplayers = reader.uint(1); state.raw.credits1 = reader.uint(1)
state.raw.bots = reader.uint(1); state.raw.credits2 = reader.uint(2)
state.raw.packedSpecial = reader.uint(1); this.logger.debug(reader.offset())
state.raw.damage = reader.uint(1); state.raw.time = reader.uint(2)
state.raw.packedRules = reader.uint(2); state.raw.laps = reader.uint(2) / 16
state.raw.credits1 = reader.uint(1); reader.skip(3)
state.raw.credits2 = reader.uint(2); state.raw.vehicles = reader.string()
this.logger.debug(reader.offset());
state.raw.time = reader.uint(2); state.password = !!(state.raw.packedSpecial & 2)
state.raw.laps = reader.uint(2) / 16; state.raw.raceCast = !!(state.raw.packedSpecial & 4)
reader.skip(3); state.raw.fixedSetups = !!(state.raw.packedSpecial & 16)
state.raw.vehicles = reader.string();
const aids = [
state.password = !!(state.raw.packedSpecial & 2); 'TractionControl',
state.raw.raceCast = !!(state.raw.packedSpecial & 4); 'AntiLockBraking',
state.raw.fixedSetups = !!(state.raw.packedSpecial & 16); 'StabilityControl',
'AutoShifting',
const aids = [ 'AutoClutch',
'TractionControl', 'Invulnerability',
'AntiLockBraking', 'OppositeLock',
'StabilityControl', 'SteeringHelp',
'AutoShifting', 'BrakingHelp',
'AutoClutch', 'SpinRecovery',
'Invulnerability', 'AutoPitstop'
'OppositeLock', ]
'SteeringHelp', state.raw.aids = []
'BrakingHelp', for (let offset = 0; offset < aids.length; offset++) {
'SpinRecovery', if (state.packedAids && (1 << offset)) {
'AutoPitstop' state.raw.aids.push(aids[offset])
]; }
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()
}
// 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'; import Core from './core.js'
export default class samp extends Core { export default class samp extends Core {
constructor() { constructor () {
super(); super()
this.encoding = 'win1252'; this.encoding = 'win1252'
this.magicHeader = 'SAMP'; this.magicHeader = 'SAMP'
this.responseMagicHeader = null; this.responseMagicHeader = null
this.isVcmp = false; this.isVcmp = false
} }
async run(state) { async run (state) {
// read info // read info
{ {
const reader = await this.sendPacket('i'); const reader = await this.sendPacket('i')
if (this.isVcmp) { if (this.isVcmp) {
const consumed = reader.part(12); const consumed = reader.part(12)
state.raw.version = this.reader(consumed).string(); state.raw.version = this.reader(consumed).string()
} }
state.password = !!reader.uint(1); state.password = !!reader.uint(1)
state.raw.numplayers = reader.uint(2); state.raw.numplayers = reader.uint(2)
state.maxplayers = reader.uint(2); state.maxplayers = reader.uint(2)
state.name = reader.pascalString(4); state.name = reader.pascalString(4)
state.raw.gamemode = reader.pascalString(4); state.raw.gamemode = reader.pascalString(4)
state.raw.map = reader.pascalString(4); state.raw.map = reader.pascalString(4)
} }
// read rules // read rules
if (!this.isVcmp) { if (!this.isVcmp) {
const reader = await this.sendPacket('r'); const reader = await this.sendPacket('r')
const ruleCount = reader.uint(2); const ruleCount = reader.uint(2)
state.raw.rules = {}; state.raw.rules = {}
for(let i = 0; i < ruleCount; i++) { for (let i = 0; i < ruleCount; i++) {
const key = reader.pascalString(1); const key = reader.pascalString(1)
const value = reader.pascalString(1); const value = reader.pascalString(1)
state.raw.rules[key] = value; state.raw.rules[key] = value
} }
} }
// read players // read players
// don't even bother if > 100 players, because the server won't respond // don't even bother if > 100 players, because the server won't respond
let gotPlayerData = false; let gotPlayerData = false
if (state.raw.numplayers < 100) { if (state.raw.numplayers < 100) {
if (this.isVcmp) { if (this.isVcmp) {
const reader = await this.sendPacket('c', true); const reader = await this.sendPacket('c', true)
if (reader !== null) { if (reader !== null) {
gotPlayerData = true; gotPlayerData = true
const playerCount = reader.uint(2); const playerCount = reader.uint(2)
for(let i = 0; i < playerCount; i++) { for (let i = 0; i < playerCount; i++) {
const player = {}; const player = {}
player.name = reader.pascalString(1); player.name = reader.pascalString(1)
state.players.push(player); state.players.push(player)
} }
} }
} else { } else {
const reader = await this.sendPacket('d', true); const reader = await this.sendPacket('d', true)
if (reader !== null) { if (reader !== null) {
gotPlayerData = true; gotPlayerData = true
const playerCount = reader.uint(2); const playerCount = reader.uint(2)
for(let i = 0; i < playerCount; i++) { for (let i = 0; i < playerCount; i++) {
const player = {}; const player = {}
player.id = reader.uint(1); player.id = reader.uint(1)
player.name = reader.pascalString(1); player.name = reader.pascalString(1)
player.score = reader.int(4); player.score = reader.int(4)
player.ping = reader.uint(4); player.ping = reader.uint(4)
state.players.push(player); state.players.push(player)
} }
} }
} }
} }
if (!gotPlayerData) { if (!gotPlayerData) {
state.players.setNum(state.raw.numplayers); state.players.setNum(state.raw.numplayers)
} }
} }
async sendPacket(type,allowTimeout) {
const outBuffer = Buffer.alloc(11); async sendPacket (type, allowTimeout) {
outBuffer.write(this.magicHeader,0, 4); const outBuffer = Buffer.alloc(11)
const ipSplit = this.options.address.split('.'); outBuffer.write(this.magicHeader, 0, 4)
outBuffer.writeUInt8(parseInt(ipSplit[0]),4); const ipSplit = this.options.address.split('.')
outBuffer.writeUInt8(parseInt(ipSplit[1]),5); outBuffer.writeUInt8(parseInt(ipSplit[0]), 4)
outBuffer.writeUInt8(parseInt(ipSplit[2]),6); outBuffer.writeUInt8(parseInt(ipSplit[1]), 5)
outBuffer.writeUInt8(parseInt(ipSplit[3]),7); outBuffer.writeUInt8(parseInt(ipSplit[2]), 6)
outBuffer.writeUInt16LE(this.options.port,8); outBuffer.writeUInt8(parseInt(ipSplit[3]), 7)
outBuffer.writeUInt8(type.charCodeAt(0),10); outBuffer.writeUInt16LE(this.options.port, 8)
outBuffer.writeUInt8(type.charCodeAt(0), 10)
const checkBuffer = Buffer.from(outBuffer);
if (this.responseMagicHeader) { const checkBuffer = Buffer.from(outBuffer)
checkBuffer.write(this.responseMagicHeader, 0, 4); if (this.responseMagicHeader) {
} checkBuffer.write(this.responseMagicHeader, 0, 4)
}
return await this.udpSend(
outBuffer, return await this.udpSend(
(buffer) => { outBuffer,
const reader = this.reader(buffer); (buffer) => {
for(let i = 0; i < checkBuffer.length; i++) { const reader = this.reader(buffer)
if(checkBuffer.readUInt8(i) !== reader.uint(1)) return; for (let i = 0; i < checkBuffer.length; i++) {
} if (checkBuffer.readUInt8(i) !== reader.uint(1)) return
return reader; }
}, return reader
() => { },
if(allowTimeout) { () => {
return null; if (allowTimeout) {
} return null
} }
); }
} )
} }
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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