mirror of
https://github.com/gamedig/node-gamedig.git
synced 2024-11-13 07:21:11 +01:00
chore: Convert all files to LF endings (#400)
* Convert to LF? * Modify gitattributes * Force LF * Git --renormalize * Update .gitattributes to enforce eol=lf * Redo CRLF -> LF on remaining files
This commit is contained in:
parent
a8bc7521f6
commit
cee42e7a88
65 changed files with 5697 additions and 5697 deletions
2
.gitattributes
vendored
2
.gitattributes
vendored
|
@ -1 +1 @@
|
|||
text=auto
|
||||
* text=auto eol=lf
|
||||
|
|
596
CHANGELOG.md
596
CHANGELOG.md
|
@ -1,298 +1,298 @@
|
|||
|
||||
## To Be Released...
|
||||
### Breaking Changes
|
||||
#### Package
|
||||
* Node.js 16.20 is now required (from 14).
|
||||
* Made the library a `module`.
|
||||
|
||||
#### Games
|
||||
* Renamed `Counter Strike: 2D` to `CS2D` in [games.txt](games.txt) (why? see [this](https://cs2d.com/faq.php?show=misc_name#misc_name)).
|
||||
* Updated `CS2D` protocol (by @ernestpasnik).
|
||||
|
||||
### Other changes
|
||||
#### Package
|
||||
* Replaced usage of deprecated `substr` with `substring`.
|
||||
* Replaced deprecated internal `punycode` with the [punycode](https://www.npmjs.com/package/punycode) package.
|
||||
* Updated [got](https://github.com/sindresorhus/got) from 12.1 to 13.
|
||||
* Updated [minimist](https://github.com/minimistjs/minimist) from 1.2.6 to 1.2.8.
|
||||
* Updated [long](https://github.com/dcodeIO/long.js) from 5.2.0 to 5.2.3.
|
||||
* Updated @types/node from 14.18.13 to 16.18.58.
|
||||
* Updated [cheerio](https://github.com/cheeriojs/cheerio) from 1.0.0-rc.10 to 1.0.0-rc.12.
|
||||
* Added eslint which spotted some unused variables and other lints.
|
||||
* CLI: Resolved incorrect error message when querying with a non-existent protocol name.
|
||||
* Added Deno support: the library and CLI can now be experimentally used with the [Deno runtime](https://deno.com)
|
||||
* `deno run --allow-net --allow-read=. bin/gamedig.js --type tf2 127.0.0.1`
|
||||
* Added code examples.
|
||||
* Added Epic Online Services protocol.
|
||||
|
||||
#### Games
|
||||
* Added support by @dgibbs64: Eco (2018), Core Keeper (2022), ARMA: Reforger (2022),
|
||||
Action Half-Life, Action: Source (2019), Base Defense (2017), Blade Symphony (2014),
|
||||
Brainbread, Deathmatch Classic (2001), Double Action: Boogaloo (2014), Dystopia (2005),
|
||||
Empires Mod (2008), Fistful of Frags (2014), alf-Life: Opposing Force (1999),
|
||||
Pirates, Vikings, and Knights II (2007), Project Cars (2015), Project Cars 2 (2017),
|
||||
The Specialists, Vampire Slayer, Warfork (2018), Wurm Unlimited (2015).
|
||||
* Also added support: The Forest (2014), Operation: Harsh Doorstop (2023),
|
||||
Insurgency: Modern Infantry Combat (2007), Counter-Strike 2 (2023), The Front (2023),
|
||||
San Andreas OpenMP.
|
||||
* Capitalized 'Unturned' in game.txt
|
||||
* Removed the players::setNum method, the library will no longer add empty players as
|
||||
a placeholder in the `players` field.
|
||||
* Fixed wrong field being parsed for `maxplayers` on Doom3.
|
||||
* Stabilized field `numplayers`.
|
||||
* Added support by @GuilhermeWerner: ARK: Survival Ascended (2023).
|
||||
|
||||
### 4.1.0
|
||||
* Replace `compressjs` dependency by `seek-bzip` to solve some possible import issues.
|
||||
* Sons Of The Forest (2023) - Added support
|
||||
* Red Dead Redemption 2 - RedM (2018) - Added support
|
||||
* Creativerse (2017) - Added support
|
||||
* The Isle (2015) - Added support
|
||||
|
||||
### 4.0.7
|
||||
* Updated some dependencies to solve vulnerabilities
|
||||
* Fixed an issue regarding GameSpy 1 not correctly checking and parsing for numbers.
|
||||
* Risk of Rain 2 (2019) - Added support
|
||||
* Survive the Nights (2017) - Added support
|
||||
* V Rising (2022) - Added support
|
||||
* Day of Dragons (2019) - Added support
|
||||
* Onset (2019) - Added support
|
||||
* Don't Starve Together (2016) - Added support
|
||||
* Chivalry: Medieval Warfare (2012) - Added support
|
||||
* Avorion (2020) - Added support
|
||||
* Black Mesa (2020) - Added support
|
||||
* Ballistic Overkill (2017) - Added support
|
||||
* Codename CURE (2017) - Added support
|
||||
* Colony Survival (2017) - Added support
|
||||
* Rising World (2014) - Added support
|
||||
* BrainBread 2 (2016) - Added support
|
||||
|
||||
### 4.0.6
|
||||
* Fixed ping returned by minecraft queries
|
||||
* Added ipFamily option to query only ipv4 or only ipv6 dns records
|
||||
|
||||
### 4.0.5
|
||||
* Fixed filtering out fake "Max Players" player on CSGO
|
||||
* Removed moment dependency
|
||||
|
||||
### 4.0.4
|
||||
* Updated dependencies
|
||||
|
||||
### 4.0.3
|
||||
* Fixed nodejs version requirement in package.json (node 14 has been required since gamedig 4)
|
||||
* Ground Breach (2018) - Added support
|
||||
* Minecraft (All Versions) - Fixed character encoding for strings returned by servers using Geyser
|
||||
* Barotrauma (2019) - Added support
|
||||
|
||||
### 4.0.2
|
||||
* Counter-Strike 1.5 - Fixed support
|
||||
|
||||
### 4.0.1
|
||||
* Rust - Fixed maxplayers >255
|
||||
* dayZ - Fixed tag info not parsing when queryRules wasn't set
|
||||
|
||||
### 4.0.0
|
||||
|
||||
#### Breaking Changes
|
||||
* NodeJS 14 is now required
|
||||
|
||||
#### Other changes
|
||||
* Dependencies are updated
|
||||
* Node 14 is now required due to new requirement in `got` dependency
|
||||
|
||||
### 3.0.9
|
||||
* Fixes player info parsing issues on bf1942-based mods (Thanks cetteup)
|
||||
* Adds Project Zomboid support (Thanks xhip)
|
||||
* Adds Post Scriptum support (Thanks arkuar)
|
||||
* Adds some more DayZ info to state.raw (Thanks podrivo)
|
||||
* Updates to README regarding DayZ (Thanks podrivo)
|
||||
* Improvements to DayZ mod parsing from additional more recent reverse engineering (probably still buggy)
|
||||
* Fixes ping always being 0 for minecraft servers
|
||||
* Adds README documentation about teamspeakQueryPort
|
||||
|
||||
### 3.0.8
|
||||
* Fixes player array corruption on some protocols which only report player counts without names (Thanks to a-sync)
|
||||
* Fixes minecraft protocol not using player list from bedrock protocol in some cases
|
||||
|
||||
### 3.0.7
|
||||
* Fixes corrupted dayzMods when packet overflow is present
|
||||
|
||||
### 3.0.6
|
||||
* raw.tags for valve servers is now an array rather than a string
|
||||
* The special mod list for dayz servers is now parsed into raw.dayzMods is requestRules is set to true
|
||||
* DayZ queue length, day and night acceleration are now parsed into raw as well
|
||||
|
||||
### 3.0.5
|
||||
* Add support for `listenUdpPort` to specify a fixed bind port.
|
||||
* Improved udp bind failure detection.
|
||||
|
||||
### 3.0.4
|
||||
* Add support for Discord widget
|
||||
|
||||
### 3.0.3
|
||||
* Greatly improve gamespy1 protocol, with additional error handling and xserverquery support.
|
||||
|
||||
### 3.0.2
|
||||
* Fix player name extraction for Unreal Tournament (1999) and possibly
|
||||
other gamespy1 games.
|
||||
|
||||
### 3.0.1
|
||||
* Clarified that nodejs 12 is now required for gamedig 3
|
||||
* Fixed misc player fields not going into `raw` subobject in `assettocorsa`, `fivem`, and `gamespy2`
|
||||
|
||||
### 3.0.0
|
||||
Major Changes:
|
||||
* **NodeJS 12 is now required**
|
||||
* The `name` field is now guaranteed to exist on all player objects. If a player's name is unknown, the `name` will be an empty string.
|
||||
* All non-`name` player fields have been moved into a `raw` sub-field. This means that, like the `raw` subobject of the parent
|
||||
response, all non-`name` fields are now considered to be unstable and may be changed during minor releases of GameDig.
|
||||
* "Rules" are no longer queried for `valve` protocol games by default. Many games do not respond to this query anyways (meaning we have to wait
|
||||
for timeout), and its contents is often not even used since it only exists in the raw subfield. If you depend on rules,
|
||||
you may pass the `requestRules: true` option to re-enable them.
|
||||
* The `raw.steamappid` and `raw.gameid` fields for valve games have been consolidated into `raw.appId`.
|
||||
|
||||
### 2.0.28
|
||||
* Added Valheim (2021)
|
||||
|
||||
### 2.0.27
|
||||
* Reduced chance of protocol collisions between gamespy3 and minecraftbedrock
|
||||
|
||||
### 2.0.26
|
||||
* Added support for the native minecraft bedrock protocol, since some
|
||||
bedrock servers apparently do not respond to the gamespy3 protocol.
|
||||
|
||||
### 2.0.25
|
||||
* Support challenges in A2S_INFO (upcoming change to valve protocol)
|
||||
|
||||
### 2.0.24
|
||||
* Add Savage 2: A Tortured Soul (2008)
|
||||
|
||||
### 2.0.23
|
||||
* Fix Conan Exiles and other games which don't respond to the valve player query
|
||||
* Add givenPortOnly query option for users that require extreme optimization
|
||||
|
||||
### 2.0.22
|
||||
* Updated dependencies
|
||||
|
||||
### 2.0.21
|
||||
* Added Assetto Corsa (2014)
|
||||
* Fixed password flag for Squad
|
||||
* Added Mordhau (2019)
|
||||
* Fixed player count being incorrect in minecraftvanilla protocol in some cases
|
||||
* Updated dependencies
|
||||
* Replaced deprecated Request http library with Got
|
||||
|
||||
### 2.0.20
|
||||
* Fixed minecraft protocol never throwing exceptions
|
||||
|
||||
### 2.0.19
|
||||
* Added Days of War (2017)
|
||||
* Added The Forrest (2014)
|
||||
* Added Just Cause 3 Multiplayer (2017)
|
||||
* Added Project Reality: Battlefield 2 (2005)
|
||||
* Added Quake Live (2010)
|
||||
* Added Contagion (2011)
|
||||
* Added Empyrion: Galactic Survival (2015)
|
||||
* Added PixARK (2018)
|
||||
|
||||
### 2.0.16, 2.0.17, 2.0.18
|
||||
* Various improvements to killing floor / unreal2 protocol
|
||||
|
||||
### 2.0.15
|
||||
* Added Hell Let Loose
|
||||
* Added Rising Storm 2: Vietnam
|
||||
* Added Squad
|
||||
* Fixed DNS lookup not working in some situations when dns.lookup unexpectedly returns a string
|
||||
* Improved minecraft protocol for non-vanilla server implementations (bedrock, waterfall, bungeecord)
|
||||
* Updated dependencies
|
||||
|
||||
### 2.0.14
|
||||
* Node 8 compatibility fixes
|
||||
|
||||
### 2.0.13
|
||||
* Improved logging
|
||||
|
||||
### 2.0.12
|
||||
* Servers are now limited to 10000 players to prevent OOM
|
||||
* Improvements to Starmade (2012)
|
||||
* Added Atlas (2018)
|
||||
|
||||
### 2.0.11
|
||||
* Added Acra Sim Racing
|
||||
* Added Mafia 2: Online
|
||||
|
||||
### 2.0.10
|
||||
* Added rFactor
|
||||
|
||||
### 2.0.9
|
||||
* Added Vice City: Multiplayer
|
||||
|
||||
### 2.0.8
|
||||
* Improve out-of-order packet handling for gamespy1 protocol
|
||||
* Work-around for buggy duplicate player reporting from bf1942 servers
|
||||
* Report team names rather than IDs when possible for gamespy1 protocol
|
||||
|
||||
### 2.0.7
|
||||
* Prevent tcp socket errors from dumping straight to console
|
||||
|
||||
### 2.0.6
|
||||
* Added support for host domains requiring Punycode encoding (special characters)
|
||||
|
||||
### 2.0.5
|
||||
* Added support for Counter-Strike: 2D
|
||||
|
||||
### 2.0.4
|
||||
* Added details about new 2.0 reponse fields to the README.
|
||||
|
||||
### 2.0.3
|
||||
* Added support for Insurgency: Sandstorm
|
||||
|
||||
### 2.0.2
|
||||
* Added support for Starsiege 2009 (starsiege)
|
||||
|
||||
### 2.0.1
|
||||
* Updated readme games list for 2.0
|
||||
* Fixed csgo default port
|
||||
|
||||
### 2.0.0
|
||||
|
||||
##### Breaking API changes
|
||||
* **Node 8 is now required**
|
||||
* Removed the `port_query` option. You can now pass either the server's game port **or** query port in the `port` option, and
|
||||
GameDig will automatically discover the proper port to query. Passing the query port is more likely be successful in
|
||||
unusual cases, as otherwise it must be automatically derived from the game port.
|
||||
* Removed `callback` parameter from Gamedig.query. Only promises are now supported. If you would like to continue
|
||||
using callbacks, you can use node's `util.callbackify` function to convert the method to callback format.
|
||||
* Removed `query` field from response object, as it was poorly documented and unstable.
|
||||
* Removed `notes` field from options / response object. Data can be passed through a standard javascript context if needed.
|
||||
|
||||
##### Minor Changes
|
||||
* Rewrote core to use promises extensively for better error-handling. Async chains have been dramatically simplified
|
||||
by using async/await across the codebase, eliminating callback chains and the 'async' dependency.
|
||||
* Replaced `--output pretty` cli parameter with `--pretty`.
|
||||
* You can now query from CLI using shorthand syntax: `gamedig --type <gameid> <ip>[:<port>]`
|
||||
* UDP socket is only opened if needed by a query.
|
||||
* Automatic query port detection -- If provided with a non-standard port, gamedig will attempt to discover if it is a
|
||||
game port or query port by querying twice: once to the port provided, and once to the port including the game's query
|
||||
port offset (if available).
|
||||
* Added new `connect` field to the response object. This will typically include the game's `ip:port` (the port will reflect the server's
|
||||
game port, even if you passed in a query port in your request). For some games, this may be a server ID or connection url
|
||||
if an IP:Port is not appropriate.
|
||||
* Added new `ping` field (in milliseconds) to the response object. As icmp packets are often blocked by NATs, and node has poor support
|
||||
for raw sockets, this time is derived from the rtt of one of the UDP requests, or the time required to open a TCP socket
|
||||
during the query.
|
||||
* Improved debug logging across all parts of GameDig
|
||||
* Removed global `Gamedig.debug`. `debug` is now an option on each query.
|
||||
|
||||
##### Protocol Changes
|
||||
* Added support for games using older versions of battlefield protocol.
|
||||
* Simplified detection of BC2 when using battlefield protocol.
|
||||
* Fixed buildandshoot not reading player list
|
||||
* Standardized all doom3 games into a single protocol, which can discover protocol discrepancies automatically.
|
||||
* Standardized all gamespy2 games into a single protocol, which can discover protocol discrepancies automatically.
|
||||
* Standardized all gamespy3 games into a single protocol, which can discover protocol discrepancies automatically.
|
||||
* Improved valve protocol challenge key retry process
|
||||
|
||||
### 1.0.0
|
||||
* First official release
|
||||
* Node.js 6 is now required
|
||||
|
||||
## To Be Released...
|
||||
### Breaking Changes
|
||||
#### Package
|
||||
* Node.js 16.20 is now required (from 14).
|
||||
* Made the library a `module`.
|
||||
|
||||
#### Games
|
||||
* Renamed `Counter Strike: 2D` to `CS2D` in [games.txt](games.txt) (why? see [this](https://cs2d.com/faq.php?show=misc_name#misc_name)).
|
||||
* Updated `CS2D` protocol (by @ernestpasnik).
|
||||
|
||||
### Other changes
|
||||
#### Package
|
||||
* Replaced usage of deprecated `substr` with `substring`.
|
||||
* Replaced deprecated internal `punycode` with the [punycode](https://www.npmjs.com/package/punycode) package.
|
||||
* Updated [got](https://github.com/sindresorhus/got) from 12.1 to 13.
|
||||
* Updated [minimist](https://github.com/minimistjs/minimist) from 1.2.6 to 1.2.8.
|
||||
* Updated [long](https://github.com/dcodeIO/long.js) from 5.2.0 to 5.2.3.
|
||||
* Updated @types/node from 14.18.13 to 16.18.58.
|
||||
* Updated [cheerio](https://github.com/cheeriojs/cheerio) from 1.0.0-rc.10 to 1.0.0-rc.12.
|
||||
* Added eslint which spotted some unused variables and other lints.
|
||||
* CLI: Resolved incorrect error message when querying with a non-existent protocol name.
|
||||
* Added Deno support: the library and CLI can now be experimentally used with the [Deno runtime](https://deno.com)
|
||||
* `deno run --allow-net --allow-read=. bin/gamedig.js --type tf2 127.0.0.1`
|
||||
* Added code examples.
|
||||
* Added Epic Online Services protocol.
|
||||
|
||||
#### Games
|
||||
* Added support by @dgibbs64: Eco (2018), Core Keeper (2022), ARMA: Reforger (2022),
|
||||
Action Half-Life, Action: Source (2019), Base Defense (2017), Blade Symphony (2014),
|
||||
Brainbread, Deathmatch Classic (2001), Double Action: Boogaloo (2014), Dystopia (2005),
|
||||
Empires Mod (2008), Fistful of Frags (2014), alf-Life: Opposing Force (1999),
|
||||
Pirates, Vikings, and Knights II (2007), Project Cars (2015), Project Cars 2 (2017),
|
||||
The Specialists, Vampire Slayer, Warfork (2018), Wurm Unlimited (2015).
|
||||
* Also added support: The Forest (2014), Operation: Harsh Doorstop (2023),
|
||||
Insurgency: Modern Infantry Combat (2007), Counter-Strike 2 (2023), The Front (2023),
|
||||
San Andreas OpenMP.
|
||||
* Capitalized 'Unturned' in game.txt
|
||||
* Removed the players::setNum method, the library will no longer add empty players as
|
||||
a placeholder in the `players` field.
|
||||
* Fixed wrong field being parsed for `maxplayers` on Doom3.
|
||||
* Stabilized field `numplayers`.
|
||||
* Added support by @GuilhermeWerner: ARK: Survival Ascended (2023).
|
||||
|
||||
### 4.1.0
|
||||
* Replace `compressjs` dependency by `seek-bzip` to solve some possible import issues.
|
||||
* Sons Of The Forest (2023) - Added support
|
||||
* Red Dead Redemption 2 - RedM (2018) - Added support
|
||||
* Creativerse (2017) - Added support
|
||||
* The Isle (2015) - Added support
|
||||
|
||||
### 4.0.7
|
||||
* Updated some dependencies to solve vulnerabilities
|
||||
* Fixed an issue regarding GameSpy 1 not correctly checking and parsing for numbers.
|
||||
* Risk of Rain 2 (2019) - Added support
|
||||
* Survive the Nights (2017) - Added support
|
||||
* V Rising (2022) - Added support
|
||||
* Day of Dragons (2019) - Added support
|
||||
* Onset (2019) - Added support
|
||||
* Don't Starve Together (2016) - Added support
|
||||
* Chivalry: Medieval Warfare (2012) - Added support
|
||||
* Avorion (2020) - Added support
|
||||
* Black Mesa (2020) - Added support
|
||||
* Ballistic Overkill (2017) - Added support
|
||||
* Codename CURE (2017) - Added support
|
||||
* Colony Survival (2017) - Added support
|
||||
* Rising World (2014) - Added support
|
||||
* BrainBread 2 (2016) - Added support
|
||||
|
||||
### 4.0.6
|
||||
* Fixed ping returned by minecraft queries
|
||||
* Added ipFamily option to query only ipv4 or only ipv6 dns records
|
||||
|
||||
### 4.0.5
|
||||
* Fixed filtering out fake "Max Players" player on CSGO
|
||||
* Removed moment dependency
|
||||
|
||||
### 4.0.4
|
||||
* Updated dependencies
|
||||
|
||||
### 4.0.3
|
||||
* Fixed nodejs version requirement in package.json (node 14 has been required since gamedig 4)
|
||||
* Ground Breach (2018) - Added support
|
||||
* Minecraft (All Versions) - Fixed character encoding for strings returned by servers using Geyser
|
||||
* Barotrauma (2019) - Added support
|
||||
|
||||
### 4.0.2
|
||||
* Counter-Strike 1.5 - Fixed support
|
||||
|
||||
### 4.0.1
|
||||
* Rust - Fixed maxplayers >255
|
||||
* dayZ - Fixed tag info not parsing when queryRules wasn't set
|
||||
|
||||
### 4.0.0
|
||||
|
||||
#### Breaking Changes
|
||||
* NodeJS 14 is now required
|
||||
|
||||
#### Other changes
|
||||
* Dependencies are updated
|
||||
* Node 14 is now required due to new requirement in `got` dependency
|
||||
|
||||
### 3.0.9
|
||||
* Fixes player info parsing issues on bf1942-based mods (Thanks cetteup)
|
||||
* Adds Project Zomboid support (Thanks xhip)
|
||||
* Adds Post Scriptum support (Thanks arkuar)
|
||||
* Adds some more DayZ info to state.raw (Thanks podrivo)
|
||||
* Updates to README regarding DayZ (Thanks podrivo)
|
||||
* Improvements to DayZ mod parsing from additional more recent reverse engineering (probably still buggy)
|
||||
* Fixes ping always being 0 for minecraft servers
|
||||
* Adds README documentation about teamspeakQueryPort
|
||||
|
||||
### 3.0.8
|
||||
* Fixes player array corruption on some protocols which only report player counts without names (Thanks to a-sync)
|
||||
* Fixes minecraft protocol not using player list from bedrock protocol in some cases
|
||||
|
||||
### 3.0.7
|
||||
* Fixes corrupted dayzMods when packet overflow is present
|
||||
|
||||
### 3.0.6
|
||||
* raw.tags for valve servers is now an array rather than a string
|
||||
* The special mod list for dayz servers is now parsed into raw.dayzMods is requestRules is set to true
|
||||
* DayZ queue length, day and night acceleration are now parsed into raw as well
|
||||
|
||||
### 3.0.5
|
||||
* Add support for `listenUdpPort` to specify a fixed bind port.
|
||||
* Improved udp bind failure detection.
|
||||
|
||||
### 3.0.4
|
||||
* Add support for Discord widget
|
||||
|
||||
### 3.0.3
|
||||
* Greatly improve gamespy1 protocol, with additional error handling and xserverquery support.
|
||||
|
||||
### 3.0.2
|
||||
* Fix player name extraction for Unreal Tournament (1999) and possibly
|
||||
other gamespy1 games.
|
||||
|
||||
### 3.0.1
|
||||
* Clarified that nodejs 12 is now required for gamedig 3
|
||||
* Fixed misc player fields not going into `raw` subobject in `assettocorsa`, `fivem`, and `gamespy2`
|
||||
|
||||
### 3.0.0
|
||||
Major Changes:
|
||||
* **NodeJS 12 is now required**
|
||||
* The `name` field is now guaranteed to exist on all player objects. If a player's name is unknown, the `name` will be an empty string.
|
||||
* All non-`name` player fields have been moved into a `raw` sub-field. This means that, like the `raw` subobject of the parent
|
||||
response, all non-`name` fields are now considered to be unstable and may be changed during minor releases of GameDig.
|
||||
* "Rules" are no longer queried for `valve` protocol games by default. Many games do not respond to this query anyways (meaning we have to wait
|
||||
for timeout), and its contents is often not even used since it only exists in the raw subfield. If you depend on rules,
|
||||
you may pass the `requestRules: true` option to re-enable them.
|
||||
* The `raw.steamappid` and `raw.gameid` fields for valve games have been consolidated into `raw.appId`.
|
||||
|
||||
### 2.0.28
|
||||
* Added Valheim (2021)
|
||||
|
||||
### 2.0.27
|
||||
* Reduced chance of protocol collisions between gamespy3 and minecraftbedrock
|
||||
|
||||
### 2.0.26
|
||||
* Added support for the native minecraft bedrock protocol, since some
|
||||
bedrock servers apparently do not respond to the gamespy3 protocol.
|
||||
|
||||
### 2.0.25
|
||||
* Support challenges in A2S_INFO (upcoming change to valve protocol)
|
||||
|
||||
### 2.0.24
|
||||
* Add Savage 2: A Tortured Soul (2008)
|
||||
|
||||
### 2.0.23
|
||||
* Fix Conan Exiles and other games which don't respond to the valve player query
|
||||
* Add givenPortOnly query option for users that require extreme optimization
|
||||
|
||||
### 2.0.22
|
||||
* Updated dependencies
|
||||
|
||||
### 2.0.21
|
||||
* Added Assetto Corsa (2014)
|
||||
* Fixed password flag for Squad
|
||||
* Added Mordhau (2019)
|
||||
* Fixed player count being incorrect in minecraftvanilla protocol in some cases
|
||||
* Updated dependencies
|
||||
* Replaced deprecated Request http library with Got
|
||||
|
||||
### 2.0.20
|
||||
* Fixed minecraft protocol never throwing exceptions
|
||||
|
||||
### 2.0.19
|
||||
* Added Days of War (2017)
|
||||
* Added The Forrest (2014)
|
||||
* Added Just Cause 3 Multiplayer (2017)
|
||||
* Added Project Reality: Battlefield 2 (2005)
|
||||
* Added Quake Live (2010)
|
||||
* Added Contagion (2011)
|
||||
* Added Empyrion: Galactic Survival (2015)
|
||||
* Added PixARK (2018)
|
||||
|
||||
### 2.0.16, 2.0.17, 2.0.18
|
||||
* Various improvements to killing floor / unreal2 protocol
|
||||
|
||||
### 2.0.15
|
||||
* Added Hell Let Loose
|
||||
* Added Rising Storm 2: Vietnam
|
||||
* Added Squad
|
||||
* Fixed DNS lookup not working in some situations when dns.lookup unexpectedly returns a string
|
||||
* Improved minecraft protocol for non-vanilla server implementations (bedrock, waterfall, bungeecord)
|
||||
* Updated dependencies
|
||||
|
||||
### 2.0.14
|
||||
* Node 8 compatibility fixes
|
||||
|
||||
### 2.0.13
|
||||
* Improved logging
|
||||
|
||||
### 2.0.12
|
||||
* Servers are now limited to 10000 players to prevent OOM
|
||||
* Improvements to Starmade (2012)
|
||||
* Added Atlas (2018)
|
||||
|
||||
### 2.0.11
|
||||
* Added Acra Sim Racing
|
||||
* Added Mafia 2: Online
|
||||
|
||||
### 2.0.10
|
||||
* Added rFactor
|
||||
|
||||
### 2.0.9
|
||||
* Added Vice City: Multiplayer
|
||||
|
||||
### 2.0.8
|
||||
* Improve out-of-order packet handling for gamespy1 protocol
|
||||
* Work-around for buggy duplicate player reporting from bf1942 servers
|
||||
* Report team names rather than IDs when possible for gamespy1 protocol
|
||||
|
||||
### 2.0.7
|
||||
* Prevent tcp socket errors from dumping straight to console
|
||||
|
||||
### 2.0.6
|
||||
* Added support for host domains requiring Punycode encoding (special characters)
|
||||
|
||||
### 2.0.5
|
||||
* Added support for Counter-Strike: 2D
|
||||
|
||||
### 2.0.4
|
||||
* Added details about new 2.0 reponse fields to the README.
|
||||
|
||||
### 2.0.3
|
||||
* Added support for Insurgency: Sandstorm
|
||||
|
||||
### 2.0.2
|
||||
* Added support for Starsiege 2009 (starsiege)
|
||||
|
||||
### 2.0.1
|
||||
* Updated readme games list for 2.0
|
||||
* Fixed csgo default port
|
||||
|
||||
### 2.0.0
|
||||
|
||||
##### Breaking API changes
|
||||
* **Node 8 is now required**
|
||||
* Removed the `port_query` option. You can now pass either the server's game port **or** query port in the `port` option, and
|
||||
GameDig will automatically discover the proper port to query. Passing the query port is more likely be successful in
|
||||
unusual cases, as otherwise it must be automatically derived from the game port.
|
||||
* Removed `callback` parameter from Gamedig.query. Only promises are now supported. If you would like to continue
|
||||
using callbacks, you can use node's `util.callbackify` function to convert the method to callback format.
|
||||
* Removed `query` field from response object, as it was poorly documented and unstable.
|
||||
* Removed `notes` field from options / response object. Data can be passed through a standard javascript context if needed.
|
||||
|
||||
##### Minor Changes
|
||||
* Rewrote core to use promises extensively for better error-handling. Async chains have been dramatically simplified
|
||||
by using async/await across the codebase, eliminating callback chains and the 'async' dependency.
|
||||
* Replaced `--output pretty` cli parameter with `--pretty`.
|
||||
* You can now query from CLI using shorthand syntax: `gamedig --type <gameid> <ip>[:<port>]`
|
||||
* UDP socket is only opened if needed by a query.
|
||||
* Automatic query port detection -- If provided with a non-standard port, gamedig will attempt to discover if it is a
|
||||
game port or query port by querying twice: once to the port provided, and once to the port including the game's query
|
||||
port offset (if available).
|
||||
* Added new `connect` field to the response object. This will typically include the game's `ip:port` (the port will reflect the server's
|
||||
game port, even if you passed in a query port in your request). For some games, this may be a server ID or connection url
|
||||
if an IP:Port is not appropriate.
|
||||
* Added new `ping` field (in milliseconds) to the response object. As icmp packets are often blocked by NATs, and node has poor support
|
||||
for raw sockets, this time is derived from the rtt of one of the UDP requests, or the time required to open a TCP socket
|
||||
during the query.
|
||||
* Improved debug logging across all parts of GameDig
|
||||
* Removed global `Gamedig.debug`. `debug` is now an option on each query.
|
||||
|
||||
##### Protocol Changes
|
||||
* Added support for games using older versions of battlefield protocol.
|
||||
* Simplified detection of BC2 when using battlefield protocol.
|
||||
* Fixed buildandshoot not reading player list
|
||||
* Standardized all doom3 games into a single protocol, which can discover protocol discrepancies automatically.
|
||||
* Standardized all gamespy2 games into a single protocol, which can discover protocol discrepancies automatically.
|
||||
* Standardized all gamespy3 games into a single protocol, which can discover protocol discrepancies automatically.
|
||||
* Improved valve protocol challenge key retry process
|
||||
|
||||
### 1.0.0
|
||||
* First official release
|
||||
* Node.js 6 is now required
|
||||
|
|
890
GAMES_LIST.md
890
GAMES_LIST.md
|
@ -1,445 +1,445 @@
|
|||
### Supported
|
||||
| GameDig Type ID | Name | See Also |
|
||||
|---------------------------------------|---------------------------------------------------------|--------------------------------------------------|
|
||||
| `7d2d` | 7 Days to Die (2013) | [Valve Protocol](#valve) |
|
||||
| `as` | Action: Source | [Valve Protocol](#valve) |
|
||||
| `ahl` | Action Half-Life | [Valve Protocol](#valve) |
|
||||
| `ageofchivalry` | Age of Chivalry (2007) | [Valve Protocol](#valve) |
|
||||
| `aoe2` | Age of Empires 2 (1999) | |
|
||||
| `alienarena` | Alien Arena (2004) | |
|
||||
| `alienswarm` | Alien Swarm (2010) | [Valve Protocol](#valve) |
|
||||
| `avp2` | Aliens versus Predator 2 (2001) | |
|
||||
| `avp2010` | Aliens vs. Predator (2010) | [Valve Protocol](#valve) |
|
||||
| `americasarmy` | America's Army (2002) | |
|
||||
| `americasarmy2` | America's Army 2 (2003) | |
|
||||
| `americasarmy3` | America's Army 3 (2009) | [Valve Protocol](#valve) |
|
||||
| `americasarmypg` | America's Army: Proving Grounds (2015) | [Valve Protocol](#valve) |
|
||||
| `arcasimracing` | Arca Sim Racing (2008) | |
|
||||
| `arkse` | Ark: Survival Evolved (2017) | [Valve Protocol](#valve) |
|
||||
| `arma2` | ARMA 2 (2009) | [Valve Protocol](#valve) |
|
||||
| `arma2oa` | ARMA 2: Operation Arrowhead (2010) | [Valve Protocol](#valve) |
|
||||
| `arma3` | ARMA 3 (2013) | [Valve Protocol](#valve) |
|
||||
| `arma` | ARMA: Armed Assault (2007) | |
|
||||
| `armacwa` | ARMA: Cold War Assault (2011) | |
|
||||
| `armar` | ARMA: Resistance (2011) | |
|
||||
| `armare` | ARMA: Reforger (2022) | [Valve Protocol](#valve) |
|
||||
| `armagetron` | Armagetron Advanced (2001) | |
|
||||
| `assettocorsa` | Assetto Corsa (2014) | |
|
||||
| `atlas` | Atlas (2018) | [Valve Protocol](#valve) |
|
||||
| `avorion` | Avorion (2020) | [Valve Protocol](#valve) |
|
||||
| `baldursgate` | Baldur's Gate (1998) | |
|
||||
| `ballisticoverkill` | Ballistic Overkill (2017) | [Valve Protocol](#valve) |
|
||||
| `barotrauma` | Barotrauma (2019) | [Valve Protocol](#valve) |
|
||||
| `bat1944` | Battalion 1944 (2018) | [Valve Protocol](#valve) |
|
||||
| `bf1942` | Battlefield 1942 (2002) | |
|
||||
| `bf2` | Battlefield 2 (2005) | |
|
||||
| `bf2142` | Battlefield 2142 (2006) | |
|
||||
| `bf3` | Battlefield 3 (2011) | |
|
||||
| `bf4` | Battlefield 4 (2013) | |
|
||||
| `bfh` | Battlefield Hardline (2015) | |
|
||||
| `bfv` | Battlefield Vietnam (2004) | |
|
||||
| `bfbc2` | Battlefield: Bad Company 2 (2010) | |
|
||||
| `bd` | Base Defense (2017) | [Valve Protocol](#valve) |
|
||||
| `blackmesa` | Black Mesa (2020) | [Valve Protocol](#valve) |
|
||||
| `brainbread` | BrainBread | [Valve Protocol](#valve) |
|
||||
| `brainbread2` | BrainBread 2 (2022) | [Valve Protocol](#valve) |
|
||||
| `breach` | Breach (2011) | [Valve Protocol](#valve) |
|
||||
| `breed` | Breed (2004) | |
|
||||
| `brink` | Brink (2011) | [Valve Protocol](#valve) |
|
||||
| `bs` | Blade Symphony (2014) | [Valve Protocol](#valve) |
|
||||
| `buildandshoot` | Build and Shoot / Ace of Spades Classic (2012) | |
|
||||
| `cod` | Call of Duty (2003) | |
|
||||
| `cod2` | Call of Duty 2 (2005) | |
|
||||
| `cod3` | Call of Duty 3 (2006) | |
|
||||
| `cod4` | Call of Duty 4: Modern Warfare (2007) | |
|
||||
| `codmw2` | Call of Duty: Modern Warfare 2 (2009) | |
|
||||
| `codmw3` | Call of Duty: Modern Warfare 3 (2011) | [Valve Protocol](#valve) |
|
||||
| `coduo` | Call of Duty: United Offensive (2004) | |
|
||||
| `codwaw` | Call of Duty: World at War (2008) | |
|
||||
| `callofjuarez` | Call of Juarez (2006) | |
|
||||
| `chaser` | Chaser (2003) | |
|
||||
| `chivalry` | Chivalry: Medieval Warfare (2012) | [Valve Protocol](#valve) |
|
||||
| `chrome` | Chrome (2003) | |
|
||||
| `codenamecure` | Codename CURE (2017) | [Valve Protocol](#valve) |
|
||||
| `codenameeagle` | Codename Eagle (2000) | |
|
||||
| `colonysurvival` | Colony Survival (2017) | [Valve Protocol](#valve) |
|
||||
| `cacrenegade` | Command and Conquer: Renegade (2002) | |
|
||||
| `commandos3` | Commandos 3: Destination Berlin (2003) | |
|
||||
| `conanexiles` | Conan Exiles (2018) | [Valve Protocol](#valve) |
|
||||
| `contagion` | Contagion (2011) | [Valve Protocol](#valve) |
|
||||
| `contactjack` | Contract J.A.C.K. (2003) | |
|
||||
| `corekeeper` | Core Keeper (2022) | [Valve Protocol](#valve) |
|
||||
| `cs15` | Counter-Strike 1.5 (2002) | |
|
||||
| `cs16` | Counter-Strike 1.6 (2003) | [Valve Protocol](#valve) |
|
||||
| `cs2d` | CS2D (2004) | |
|
||||
| `cscz` | Counter-Strike: Condition Zero (2004) | [Valve Protocol](#valve) |
|
||||
| `csgo` | Counter-Strike: Global Offensive (2012) | [Notes](#csgo), [Valve Protocol](#valve) |
|
||||
| `css` | Counter-Strike: Source (2004) | [Valve Protocol](#valve) |
|
||||
| `cs2` | Counter-Strike 2 (2023) | [Valve Protocol](#valve) |
|
||||
| `creativerse` | Creativerse (2017) | [Valve Protocol](#valve) |
|
||||
| `crossracing` | Cross Racing Championship Extreme 2005 (2005) | |
|
||||
| `crysis` | Crysis (2007) | |
|
||||
| `crysis2` | Crysis 2 (2011) | |
|
||||
| `crysiswars` | Crysis Wars (2008) | |
|
||||
| `dab` | Double Action: Boogaloo (2014) | [Valve Protocol](#valve) |
|
||||
| `daikatana` | Daikatana (2000) | |
|
||||
| `dnl` | Dark and Light (2017) | [Valve Protocol](#valve) |
|
||||
| `dmomam` | Dark Messiah of Might and Magic (2006) | [Valve Protocol](#valve) |
|
||||
| `darkesthour` | Darkest Hour: Europe '44-'45 (2008) | |
|
||||
| `dod` | Day of Defeat (2003) | [Valve Protocol](#valve) |
|
||||
| `dods` | Day of Defeat: Source (2005) | [Valve Protocol](#valve) |
|
||||
| `dayofdragons` | Day of Dragons (2019) | [Valve Protocol](#valve) |
|
||||
| `doi` | Day of Infamy (2017) | [Valve Protocol](#valve) |
|
||||
| `daysofwar` | Days of War (2017) | [Valve Protocol](#valve) |
|
||||
| `dayz` | DayZ (2018) | [Valve Protocol](#valve) |
|
||||
| `dayzmod` | DayZ Mod (2013) | [Valve Protocol](#valve) |
|
||||
| `deadlydozenpt` | Deadly Dozen: Pacific Theater (2002) | |
|
||||
| `dh2005` | Deer Hunter 2005 (2004) | |
|
||||
| `descent3` | Descent 3 (1999) | |
|
||||
| `deusex` | Deus Ex (2000) | |
|
||||
| `devastation` | Devastation (2003) | |
|
||||
| `dinodday` | Dino D-Day (2011) | [Valve Protocol](#valve) |
|
||||
| `dirttrackracing2` | Dirt Track Racing 2 (2002) | |
|
||||
| `discord` | Discord | [Notes](#discord) |
|
||||
| `dmc` | Deathmatch Classic (2001) | [Valve Protocol](#valve) |
|
||||
| `dst` | Don't Starve Together (2016) | [Valve Protocol](#valve) |
|
||||
| `doom3` | Doom 3 (2004) | |
|
||||
| `dota2` | Dota 2 (2013) | [Valve Protocol](#valve) |
|
||||
| `drakan` | Drakan: Order of the Flame (1999) | |
|
||||
| `dystopia` | Dystopia (2005) | [Valve Protocol](#valve) |
|
||||
| `eco` | Eco (2018) | |
|
||||
| `empyrion` | Empyrion - Galactic Survival (2015) | [Valve Protocol](#valve) |
|
||||
| `empiresmod` | Empires Mod (2008) | [Valve Protocol](#valve) |
|
||||
| `etqw` | Enemy Territory: Quake Wars (2007) | |
|
||||
| `fear` | F.E.A.R. (2005) | |
|
||||
| `f1c9902` | F1 Challenge '99-'02 (2002) | |
|
||||
| `farcry` | Far Cry (2004) | |
|
||||
| `farcry2` | Far Cry 2 (2008) | |
|
||||
| `f12002` | Formula One 2002 (2002) | |
|
||||
| `fof` | Fistful of Frags (2014) | [Valve Protocol](#valve) |
|
||||
| `fortressforever` | Fortress Forever (2007) | [Valve Protocol](#valve) |
|
||||
| `ffow` | Frontlines: Fuel of War (2008) | |
|
||||
| `garrysmod` | Garry's Mod (2004) | [Valve Protocol](#valve) |
|
||||
| `geneshift`<br>`mutantfactions` | Geneshift (2017) | |
|
||||
| `giantscitizenkabuto` | Giants: Citizen Kabuto (2000) | |
|
||||
| `globaloperations` | Global Operations (2002) | |
|
||||
| `ges` | GoldenEye: Source (2010) | [Valve Protocol](#valve) |
|
||||
| `gore` | Gore: Ultimate Soldier (2002) | |
|
||||
| `fivem` | Grand Theft Auto V - FiveM (2013) | |
|
||||
| `mtasa` | Grand Theft Auto: San Andreas - Multi Theft Auto (2004) | |
|
||||
| `mtavc` | Grand Theft Auto: Vice City - Multi Theft Auto (2002) | |
|
||||
| `groundbreach` | Ground Breach (2018) | [Valve Protocol](#valve) |
|
||||
| `gunmanchronicles` | Gunman Chronicles (2000) | [Valve Protocol](#valve) |
|
||||
| `hl2dm` | Half-Life 2: Deathmatch (2004) | [Valve Protocol](#valve) |
|
||||
| `hldm` | Half-Life Deathmatch (1998) | [Valve Protocol](#valve) |
|
||||
| `hldms` | Half-Life Deathmatch: Source (2005) | [Valve Protocol](#valve) |
|
||||
| `hlopfor` | Half-Life: Opposing Force (1999) | [Valve Protocol](#valve) |
|
||||
| `halo` | Halo (2003) | |
|
||||
| `halo2` | Halo 2 (2007) | |
|
||||
| `hll` | Hell Let Loose | [Valve Protocol](#valve) |
|
||||
| `heretic2` | Heretic II (1998) | |
|
||||
| `hexen2` | Hexen II (1997) | |
|
||||
| `had2` | Hidden & Dangerous 2 (2003) | |
|
||||
| `homefront` | Homefront (2011) | [Valve Protocol](#valve) |
|
||||
| `homeworld2` | Homeworld 2 (2003) | |
|
||||
| `hurtworld` | Hurtworld (2015) | [Valve Protocol](#valve) |
|
||||
| `igi2` | I.G.I.-2: Covert Strike (2003) | |
|
||||
| `il2` | IL-2 Sturmovik (2001) | |
|
||||
| `insurgency` | Insurgency (2014) | [Valve Protocol](#valve) |
|
||||
| `insurgencymic` | Insurgency: Modern Infantry Combat (2007) | [Valve Protocol](#valve) |
|
||||
| `insurgencysandstorm` | Insurgency: Sandstorm (2018) | [Valve Protocol](#valve) |
|
||||
| `ironstorm` | Iron Storm (2002) | |
|
||||
| `isle` | The Isle (2015) | [Valve Protocol](#valve) |
|
||||
| `jamesbondnightfire` | James Bond 007: Nightfire (2002) | |
|
||||
| `jc2mp` | Just Cause 2 - Multiplayer (2010) | |
|
||||
| `jc3mp` | Just Cause 3 - Multiplayer (2017) | [Valve Protocol](#valve) |
|
||||
| `kspdmp` | Kerbal Space Program - DMP Multiplayer (2015) | |
|
||||
| `killingfloor` | Killing Floor (2009) | |
|
||||
| `killingfloor2` | Killing Floor 2 (2016) | [Valve Protocol](#valve) |
|
||||
| `kingpin` | Kingpin: Life of Crime (1999) | |
|
||||
| `kisspc` | Kiss: Psycho Circus: The Nightmare Child (2000) | |
|
||||
| `kzmod` | Kreedz Climbing (2017) | [Valve Protocol](#valve) |
|
||||
| `left4dead` | Left 4 Dead (2008) | [Valve Protocol](#valve) |
|
||||
| `left4dead2` | Left 4 Dead 2 (2009) | [Valve Protocol](#valve) |
|
||||
| `m2mp` | Mafia II - Multiplayer (2010) | |
|
||||
| `m2o` | Mafia II - Online (2010) | |
|
||||
| `moh2010` | Medal of Honor (2010) | |
|
||||
| `mohab` | Medal of Honor: Airborne (2007) | |
|
||||
| `mohaa` | Medal of Honor: Allied Assault (2002) | |
|
||||
| `mohbt` | Medal of Honor: Allied Assault Breakthrough (2003) | |
|
||||
| `mohsh` | Medal of Honor: Allied Assault Spearhead (2002) | |
|
||||
| `mohpa` | Medal of Honor: Pacific Assault (2004) | |
|
||||
| `mohwf` | Medal of Honor: Warfighter (2012) | |
|
||||
| `medievalengineers` | Medieval Engineers (2015) | [Valve Protocol](#valve) |
|
||||
| `minecraft`<br>`minecraftping` | Minecraft (2009) | |
|
||||
| `minecraftpe`<br>`minecraftbe` | Minecraft: Bedrock Edition (2011) | |
|
||||
| `mnc` | Monday Night Combat (2011) | [Valve Protocol](#valve) |
|
||||
| `mordhau` | Mordhau (2019) | [Valve Protocol](#valve) |
|
||||
| `mumble` | Mumble - GTmurmur Plugin (2005) | [Notes](#mumble) |
|
||||
| `mumbleping` | Mumble - Lightweight (2005) | [Notes](#mumble) |
|
||||
| `nascarthunder2004` | NASCAR Thunder 2004 (2003) | |
|
||||
| `ns` | Natural Selection (2002) | [Valve Protocol](#valve) |
|
||||
| `ns2` | Natural Selection 2 (2012) | [Valve Protocol](#valve) |
|
||||
| `nfshp2` | Need for Speed: Hot Pursuit 2 (2002) | |
|
||||
| `nab` | Nerf Arena Blast (1999) | |
|
||||
| `netpanzer` | netPanzer (2002) | |
|
||||
| `nwn` | Neverwinter Nights (2002) | |
|
||||
| `nwn2` | Neverwinter Nights 2 (2006) | |
|
||||
| `nexuiz` | Nexuiz (2005) | |
|
||||
| `nitrofamily` | Nitro Family (2004) | |
|
||||
| `nmrih` | No More Room in Hell (2011) | [Valve Protocol](#valve) |
|
||||
| `nolf2` | No One Lives Forever 2: A Spy in H.A.R.M.'s Way (2002) | |
|
||||
| `nucleardawn` | Nuclear Dawn (2011) | [Valve Protocol](#valve) |
|
||||
| `onset` | Onset (2019) | [Valve Protocol](#valve) |
|
||||
| `ohd` | Operation: Harsh Doorstop (2023) | [Valve Protocol](#valve) |
|
||||
| `openarena` | OpenArena (2005) | |
|
||||
| `openttd` | OpenTTD (2004) | |
|
||||
| `operationflashpoint`<br>`flashpoint` | Operation Flashpoint: Cold War Crisis (2001) | |
|
||||
| `flashpointresistance` | Operation Flashpoint: Resistance (2002) | |
|
||||
| `painkiller` | Painkiller | |
|
||||
| `pc` | Project Cars (2015) | [Valve Protocol](#valve) |
|
||||
| `pc2` | Project Cars 2 (2017) | [Valve Protocol](#valve) |
|
||||
| `pixark` | PixARK (2018) | [Valve Protocol](#valve) |
|
||||
| `pvkii` | Pirates, Vikings, and Knights II (2007) | [Valve Protocol](#valve) |
|
||||
| `ps` | Post Scriptum | |
|
||||
| `postal2` | Postal 2 | |
|
||||
| `prey` | Prey | |
|
||||
| `primalcarnage` | Primal Carnage: Extinction | [Valve Protocol](#valve) |
|
||||
| `prbf2` | Project Reality: Battlefield 2 (2005) | |
|
||||
| `przomboid` | Project Zomboid | [Valve Protocol](#valve) |
|
||||
| `quake1` | Quake 1: QuakeWorld (1996) | |
|
||||
| `quake2` | Quake 2 (1997) | |
|
||||
| `quake3` | Quake 3: Arena (1999) | |
|
||||
| `quake4` | Quake 4 (2005) | |
|
||||
| `quakelive` | Quake Live (2010) | [Valve Protocol](#valve) |
|
||||
| `ragdollkungfu` | Rag Doll Kung Fu | [Valve Protocol](#valve) |
|
||||
| `r6` | Rainbow Six | |
|
||||
| `r6roguespear` | Rainbow Six 2: Rogue Spear | |
|
||||
| `r6ravenshield` | Rainbow Six 3: Raven Shield | |
|
||||
| `rallisportchallenge` | RalliSport Challenge | |
|
||||
| `rallymasters` | Rally Masters | |
|
||||
| `redorchestra` | Red Orchestra | |
|
||||
| `redorchestra2` | Red Orchestra 2 | [Valve Protocol](#valve) |
|
||||
| `redorchestraost` | Red Orchestra: Ostfront 41-45 | |
|
||||
| `redline` | Redline | |
|
||||
| `redm` | Red Dead Redemption 2 - RedM (2018) | |
|
||||
| `rtcw` | Return to Castle Wolfenstein | |
|
||||
| `rfactor` | rFactor | |
|
||||
| `ricochet` | Ricochet | [Valve Protocol](#valve) |
|
||||
| `riseofnations` | Rise of Nations | |
|
||||
| `rs2` | Rising Storm 2: Vietnam | [Valve Protocol](#valve) |
|
||||
| `risingworld` | Rising World (2014) | [Valve Protocol](#valve) |
|
||||
| `ror2` | Risk of Rain 2 (2020) | [Valve Protocol](#valve) |
|
||||
| `rune` | Rune | |
|
||||
| `rust` | Rust | [Valve Protocol](#valve) |
|
||||
| `stalker` | S.T.A.L.K.E.R. | |
|
||||
| `samp` | San Andreas Multiplayer | |
|
||||
| `saomp` | San Andreas OpenMP | |
|
||||
| `savage2` | Savage 2: A Tortured Soul (2008) | |
|
||||
| `ss` | Serious Sam | |
|
||||
| `ss2` | Serious Sam 2 | |
|
||||
| `shatteredhorizon` | Shattered Horizon | [Valve Protocol](#valve) |
|
||||
| `shogo` | Shogo | |
|
||||
| `shootmania` | Shootmania | [Notes](#nadeo-shootmania--trackmania--etc) |
|
||||
| `sin` | SiN | |
|
||||
| `sinep` | SiN Episodes | [Valve Protocol](#valve) |
|
||||
| `soldat` | Soldat | |
|
||||
| `sof` | Soldier of Fortune | |
|
||||
| `sof2` | Soldier of Fortune 2 | |
|
||||
| `sonsoftheforest` | Sons Of The Forest | [Valve Protocol](#valve) |
|
||||
| `spaceengineers` | Space Engineers | [Valve Protocol](#valve) |
|
||||
| `squad` | Squad | [Valve Protocol](#valve) |
|
||||
| `stbc` | Star Trek: Bridge Commander | |
|
||||
| `stvef` | Star Trek: Voyager - Elite Force | |
|
||||
| `stvef2` | Star Trek: Voyager - Elite Force 2 | |
|
||||
| `swjk2` | Star Wars Jedi Knight II: Jedi Outcast (2002) | |
|
||||
| `swjk` | Star Wars Jedi Knight: Jedi Academy (2003) | |
|
||||
| `swbf` | Star Wars: Battlefront | |
|
||||
| `swbf2` | Star Wars: Battlefront 2 | |
|
||||
| `swrc` | Star Wars: Republic Commando | |
|
||||
| `starbound` | Starbound | [Valve Protocol](#valve) |
|
||||
| `starmade` | StarMade | |
|
||||
| `starsiege` | Starsiege (2009) | |
|
||||
| `suicidesurvival` | Suicide Survival | [Valve Protocol](#valve) |
|
||||
| `stn` | Survive the Nights (2017) | [Valve Protocol](#valve) |
|
||||
| `svencoop` | Sven Coop | [Valve Protocol](#valve) |
|
||||
| `swat4` | SWAT 4 | |
|
||||
| `synergy` | Synergy | [Valve Protocol](#valve) |
|
||||
| `tacticalops` | Tactical Ops | |
|
||||
| `takeonhelicopters` | Take On Helicopters (2011) | |
|
||||
| `teamfactor` | Team Factor | |
|
||||
| `tf2` | Team Fortress 2 | [Valve Protocol](#valve) |
|
||||
| `tfc` | Team Fortress Classic | [Valve Protocol](#valve) |
|
||||
| `teamspeak2` | Teamspeak 2 | |
|
||||
| `teamspeak3` | Teamspeak 3 | [Notes](#teamspeak3) |
|
||||
| `terminus` | Terminus | |
|
||||
| `terraria`<br>`tshock` | Terraria - TShock (2011) | [Notes](#terraria) |
|
||||
| `forrest` | The Forrest (2014) | [Valve Protocol](#valve) |
|
||||
| `thefront` | The Front (2023) | [The Front](#thefront), [Valve Protocol](#valve) |
|
||||
| `hidden` | The Hidden (2005) | [Valve Protocol](#valve) |
|
||||
| `nolf` | The Operative: No One Lives Forever (2000) | |
|
||||
| `ship` | The Ship | [Valve Protocol](#valve) |
|
||||
| `ts` | The Specialists | [Valve Protocol](#valve) |
|
||||
| `graw` | Tom Clancy's Ghost Recon Advanced Warfighter (2006) | |
|
||||
| `graw2` | Tom Clancy's Ghost Recon Advanced Warfighter 2 (2007) | |
|
||||
| `theforest` | The Forest (2014) | [Valve Protocol](#valve) |
|
||||
| `thps3` | Tony Hawk's Pro Skater 3 | |
|
||||
| `thps4` | Tony Hawk's Pro Skater 4 | |
|
||||
| `thu2` | Tony Hawk's Underground 2 | |
|
||||
| `towerunite` | Tower Unite | [Valve Protocol](#valve) |
|
||||
| `trackmania2` | Trackmania 2 | [Notes](#nadeo-shootmania--trackmania--etc) |
|
||||
| `trackmaniaforever` | Trackmania Forever | [Notes](#nadeo-shootmania--trackmania--etc) |
|
||||
| `tremulous` | Tremulous | |
|
||||
| `tribes1` | Tribes 1: Starsiege | |
|
||||
| `tribesvengeance` | Tribes: Vengeance | |
|
||||
| `tron20` | Tron 2.0 | |
|
||||
| `turok2` | Turok 2 | |
|
||||
| `universalcombat` | Universal Combat | |
|
||||
| `unreal` | Unreal | |
|
||||
| `ut` | Unreal Tournament | |
|
||||
| `ut2003` | Unreal Tournament 2003 | |
|
||||
| `ut2004` | Unreal Tournament 2004 | |
|
||||
| `ut3` | Unreal Tournament 3 | |
|
||||
| `unturned` | Unturned | [Valve Protocol](#valve) |
|
||||
| `urbanterror` | Urban Terror | |
|
||||
| `vrising` | V Rising (2022) | [Valve Protocol](#valve) |
|
||||
| `v8supercar` | V8 Supercar Challenge | |
|
||||
| `vs` | Vampire Slayer | [Valve Protocol](#valve) |
|
||||
| `valheim` | Valheim (2021) | [Notes](#valheim), [Valve Protocol](#valve) |
|
||||
| `ventrilo` | Ventrilo | |
|
||||
| `vcmp` | Vice City Multiplayer | |
|
||||
| `vietcong` | Vietcong | |
|
||||
| `vietcong2` | Vietcong 2 | |
|
||||
| `warfork` | Warfork | |
|
||||
| `warsow` | Warsow | |
|
||||
| `wheeloftime` | Wheel of Time | |
|
||||
| `wolfenstein2009` | Wolfenstein 2009 | |
|
||||
| `wolfensteinet` | Wolfenstein: Enemy Territory | |
|
||||
| `wurm` | Wurm: Unlimited | [Valve Protocol](#valve) |
|
||||
| `xpandrally` | Xpand Rally | |
|
||||
| `zombiemaster` | Zombie Master | [Valve Protocol](#valve) |
|
||||
| `zps` | Zombie Panic: Source | [Valve Protocol](#valve) |
|
||||
|
||||
### Not supported (yet)
|
||||
|
||||
* Cube Engine (cube):
|
||||
* Cube 1
|
||||
* Assault Cube
|
||||
* Cube 2: Sauerbraten
|
||||
* Blood Frontier
|
||||
* Alien vs Predator
|
||||
* Armed Assault 2: Operation Arrowhead
|
||||
* Battlefield Bad Company 2: Vietnam
|
||||
* BFRIS
|
||||
* Call of Duty: Black Ops 1 and 2 (no documentation, may require rcon)
|
||||
* Crysis Warhead
|
||||
* Days of War
|
||||
* DirtyBomb
|
||||
* Doom - Skulltag
|
||||
* Doom - ZDaemon
|
||||
* ECO Global Survival ([Ref](https://github.com/Austinb/GameQ/blob/v3/src/GameQ/Protocols/Eco.php))
|
||||
* Farming Simulator
|
||||
* Freelancer
|
||||
* Ghost Recon
|
||||
* GRAV Online
|
||||
* GTA Network ([Ref](https://github.com/Austinb/GameQ/blob/v3/src/GameQ/Protocols/Gtan.php))
|
||||
* GTR 2
|
||||
* Haze
|
||||
* Hexen World
|
||||
* Lost Heaven
|
||||
* Multi Theft Auto
|
||||
* Pariah
|
||||
* Plain Sight
|
||||
* Purge Jihad
|
||||
* Red Eclipse
|
||||
* Red Faction
|
||||
* S.T.A.L.K.E.R. Clear Sky
|
||||
* Savage: The Battle For Newerth
|
||||
* SiN 1 Multiplayer
|
||||
* South Park
|
||||
* Star Wars Jedi Knight: Dark Forces II
|
||||
* Star Wars: X-Wing Alliance
|
||||
* Sum of All Fears
|
||||
* Teeworlds
|
||||
* Tibia ([Ref](https://github.com/Austinb/GameQ/blob/v3/src/GameQ/Protocols/Tibia.php))
|
||||
* Titanfall
|
||||
* Tribes 2
|
||||
* Unreal 2 XMP
|
||||
* World in Conflict
|
||||
* World Opponent Network
|
||||
* Wurm Unlimited
|
||||
|
||||
> Want support for one of these games? Please open an issue to show your interest!
|
||||
> __Know how to code?__ Protocol details for many of the games above are documented
|
||||
> at https://github.com/gamedig/legacy-query-library-archive
|
||||
> , ready for you to develop into GameDig!
|
||||
|
||||
> Don't see your game listed here?
|
||||
>
|
||||
> First, let us know, so we can fix it. Then, you can try using some common query
|
||||
> protocols directly by using one of these server types:
|
||||
> * protocol-ase
|
||||
> * protocol-battlefield
|
||||
> * protocol-doom3
|
||||
> * protocol-gamespy1
|
||||
> * protocol-gamespy2
|
||||
> * protocol-gamespy3
|
||||
> * protocol-nadeo
|
||||
> * protocol-quake2
|
||||
> * protocol-quake3
|
||||
> * protocol-unreal2
|
||||
> * protocol-valve
|
||||
|
||||
Games with Additional Notes
|
||||
---
|
||||
|
||||
### <a name="csgo"></a>Counter-Strike: Global Offensive
|
||||
To receive a full player list response from CS:GO servers, the server must
|
||||
have set the cvar: host_players_show 2
|
||||
|
||||
### Discord
|
||||
You must set the `guildId` request field to the server's guild ID. Do not provide an IP.
|
||||
The Guild ID can be found in server widget settings (Server ID) or by enabling developer mode in client settings and right-clicking the server's icon.
|
||||
In order to retrieve information from discord server's they must have the `Enable server widget` option enabled.
|
||||
|
||||
### Mumble
|
||||
For full query results from Mumble, you must be running the
|
||||
[GTmurmur plugin](http://www.gametracker.com/downloads/gtmurmurplugin.php).
|
||||
If you do not wish to run the plugin, or do not require details such as channel and user lists,
|
||||
you can use the 'mumbleping' server type instead, which uses a less accurate but more reliable solution
|
||||
|
||||
### Nadeo (ShootMania / TrackMania / etc)
|
||||
The server must have xmlrpc enabled, and you must pass the xmlrpc port to GameDig, not the connection port.
|
||||
You must have a user account on the server with access level User or higher.
|
||||
Pass the login into to GameDig with the additional options: login, password
|
||||
|
||||
### <a name="teamspeak3"></a>TeamSpeak 3
|
||||
For teamspeak 3 queries to work correctly, the following permissions must be available for the guest server group:
|
||||
|
||||
* Virtual Server
|
||||
* b_virtualserver_info_view
|
||||
* b_virtualserver_channel_list
|
||||
* b_virtualserver_client_list
|
||||
* Group
|
||||
* b_virtualserver_servergroup_list
|
||||
* b_virtualserver_channelgroup_list
|
||||
|
||||
In the extremely unusual case that your server host responds to queries on a non-default port (the default is 10011),
|
||||
you can specify their host query port using the teamspeakQueryPort option.
|
||||
|
||||
### Terraria
|
||||
Requires tshock server mod, and a REST user token, which can be passed to GameDig with the
|
||||
additional option: `token`
|
||||
|
||||
### Valheim
|
||||
Valheim servers will only respond to queries if they are started in public mode (`-public 1`).
|
||||
|
||||
### DayZ
|
||||
DayZ stores some of it's servers information inside the `tags` attribute. Make sure to set `requestRules: true` to access it. Some data inside `dayzMods` attribute may be fuzzy, due to how mods are loaded into the servers. Alternatively, some servers may have a [third party tool](https://dayzsalauncher.com/#/tools) that you can use to get the mods information. If it's installed, you can access it via browser with the game servers IP:PORT, but add up 10 to the port. (eg. if game port is 2302 then use 2312).
|
||||
|
||||
### <a name="valve"></a>Valve Protocol
|
||||
For many valve games, additional 'rules' may be fetched into the unstable `raw` field by passing the additional
|
||||
option: `requestRules: true`. Beware that this may increase query time.
|
||||
|
||||
### <a name="thefront"></a>The Front
|
||||
Responses with wrong `name` (gives out a steamid instead of the server name) and `maxplayers` (always 200, whatever the config would be) field values.
|
||||
### Supported
|
||||
| GameDig Type ID | Name | See Also |
|
||||
|---------------------------------------|---------------------------------------------------------|--------------------------------------------------|
|
||||
| `7d2d` | 7 Days to Die (2013) | [Valve Protocol](#valve) |
|
||||
| `as` | Action: Source | [Valve Protocol](#valve) |
|
||||
| `ahl` | Action Half-Life | [Valve Protocol](#valve) |
|
||||
| `ageofchivalry` | Age of Chivalry (2007) | [Valve Protocol](#valve) |
|
||||
| `aoe2` | Age of Empires 2 (1999) | |
|
||||
| `alienarena` | Alien Arena (2004) | |
|
||||
| `alienswarm` | Alien Swarm (2010) | [Valve Protocol](#valve) |
|
||||
| `avp2` | Aliens versus Predator 2 (2001) | |
|
||||
| `avp2010` | Aliens vs. Predator (2010) | [Valve Protocol](#valve) |
|
||||
| `americasarmy` | America's Army (2002) | |
|
||||
| `americasarmy2` | America's Army 2 (2003) | |
|
||||
| `americasarmy3` | America's Army 3 (2009) | [Valve Protocol](#valve) |
|
||||
| `americasarmypg` | America's Army: Proving Grounds (2015) | [Valve Protocol](#valve) |
|
||||
| `arcasimracing` | Arca Sim Racing (2008) | |
|
||||
| `arkse` | Ark: Survival Evolved (2017) | [Valve Protocol](#valve) |
|
||||
| `arma2` | ARMA 2 (2009) | [Valve Protocol](#valve) |
|
||||
| `arma2oa` | ARMA 2: Operation Arrowhead (2010) | [Valve Protocol](#valve) |
|
||||
| `arma3` | ARMA 3 (2013) | [Valve Protocol](#valve) |
|
||||
| `arma` | ARMA: Armed Assault (2007) | |
|
||||
| `armacwa` | ARMA: Cold War Assault (2011) | |
|
||||
| `armar` | ARMA: Resistance (2011) | |
|
||||
| `armare` | ARMA: Reforger (2022) | [Valve Protocol](#valve) |
|
||||
| `armagetron` | Armagetron Advanced (2001) | |
|
||||
| `assettocorsa` | Assetto Corsa (2014) | |
|
||||
| `atlas` | Atlas (2018) | [Valve Protocol](#valve) |
|
||||
| `avorion` | Avorion (2020) | [Valve Protocol](#valve) |
|
||||
| `baldursgate` | Baldur's Gate (1998) | |
|
||||
| `ballisticoverkill` | Ballistic Overkill (2017) | [Valve Protocol](#valve) |
|
||||
| `barotrauma` | Barotrauma (2019) | [Valve Protocol](#valve) |
|
||||
| `bat1944` | Battalion 1944 (2018) | [Valve Protocol](#valve) |
|
||||
| `bf1942` | Battlefield 1942 (2002) | |
|
||||
| `bf2` | Battlefield 2 (2005) | |
|
||||
| `bf2142` | Battlefield 2142 (2006) | |
|
||||
| `bf3` | Battlefield 3 (2011) | |
|
||||
| `bf4` | Battlefield 4 (2013) | |
|
||||
| `bfh` | Battlefield Hardline (2015) | |
|
||||
| `bfv` | Battlefield Vietnam (2004) | |
|
||||
| `bfbc2` | Battlefield: Bad Company 2 (2010) | |
|
||||
| `bd` | Base Defense (2017) | [Valve Protocol](#valve) |
|
||||
| `blackmesa` | Black Mesa (2020) | [Valve Protocol](#valve) |
|
||||
| `brainbread` | BrainBread | [Valve Protocol](#valve) |
|
||||
| `brainbread2` | BrainBread 2 (2022) | [Valve Protocol](#valve) |
|
||||
| `breach` | Breach (2011) | [Valve Protocol](#valve) |
|
||||
| `breed` | Breed (2004) | |
|
||||
| `brink` | Brink (2011) | [Valve Protocol](#valve) |
|
||||
| `bs` | Blade Symphony (2014) | [Valve Protocol](#valve) |
|
||||
| `buildandshoot` | Build and Shoot / Ace of Spades Classic (2012) | |
|
||||
| `cod` | Call of Duty (2003) | |
|
||||
| `cod2` | Call of Duty 2 (2005) | |
|
||||
| `cod3` | Call of Duty 3 (2006) | |
|
||||
| `cod4` | Call of Duty 4: Modern Warfare (2007) | |
|
||||
| `codmw2` | Call of Duty: Modern Warfare 2 (2009) | |
|
||||
| `codmw3` | Call of Duty: Modern Warfare 3 (2011) | [Valve Protocol](#valve) |
|
||||
| `coduo` | Call of Duty: United Offensive (2004) | |
|
||||
| `codwaw` | Call of Duty: World at War (2008) | |
|
||||
| `callofjuarez` | Call of Juarez (2006) | |
|
||||
| `chaser` | Chaser (2003) | |
|
||||
| `chivalry` | Chivalry: Medieval Warfare (2012) | [Valve Protocol](#valve) |
|
||||
| `chrome` | Chrome (2003) | |
|
||||
| `codenamecure` | Codename CURE (2017) | [Valve Protocol](#valve) |
|
||||
| `codenameeagle` | Codename Eagle (2000) | |
|
||||
| `colonysurvival` | Colony Survival (2017) | [Valve Protocol](#valve) |
|
||||
| `cacrenegade` | Command and Conquer: Renegade (2002) | |
|
||||
| `commandos3` | Commandos 3: Destination Berlin (2003) | |
|
||||
| `conanexiles` | Conan Exiles (2018) | [Valve Protocol](#valve) |
|
||||
| `contagion` | Contagion (2011) | [Valve Protocol](#valve) |
|
||||
| `contactjack` | Contract J.A.C.K. (2003) | |
|
||||
| `corekeeper` | Core Keeper (2022) | [Valve Protocol](#valve) |
|
||||
| `cs15` | Counter-Strike 1.5 (2002) | |
|
||||
| `cs16` | Counter-Strike 1.6 (2003) | [Valve Protocol](#valve) |
|
||||
| `cs2d` | CS2D (2004) | |
|
||||
| `cscz` | Counter-Strike: Condition Zero (2004) | [Valve Protocol](#valve) |
|
||||
| `csgo` | Counter-Strike: Global Offensive (2012) | [Notes](#csgo), [Valve Protocol](#valve) |
|
||||
| `css` | Counter-Strike: Source (2004) | [Valve Protocol](#valve) |
|
||||
| `cs2` | Counter-Strike 2 (2023) | [Valve Protocol](#valve) |
|
||||
| `creativerse` | Creativerse (2017) | [Valve Protocol](#valve) |
|
||||
| `crossracing` | Cross Racing Championship Extreme 2005 (2005) | |
|
||||
| `crysis` | Crysis (2007) | |
|
||||
| `crysis2` | Crysis 2 (2011) | |
|
||||
| `crysiswars` | Crysis Wars (2008) | |
|
||||
| `dab` | Double Action: Boogaloo (2014) | [Valve Protocol](#valve) |
|
||||
| `daikatana` | Daikatana (2000) | |
|
||||
| `dnl` | Dark and Light (2017) | [Valve Protocol](#valve) |
|
||||
| `dmomam` | Dark Messiah of Might and Magic (2006) | [Valve Protocol](#valve) |
|
||||
| `darkesthour` | Darkest Hour: Europe '44-'45 (2008) | |
|
||||
| `dod` | Day of Defeat (2003) | [Valve Protocol](#valve) |
|
||||
| `dods` | Day of Defeat: Source (2005) | [Valve Protocol](#valve) |
|
||||
| `dayofdragons` | Day of Dragons (2019) | [Valve Protocol](#valve) |
|
||||
| `doi` | Day of Infamy (2017) | [Valve Protocol](#valve) |
|
||||
| `daysofwar` | Days of War (2017) | [Valve Protocol](#valve) |
|
||||
| `dayz` | DayZ (2018) | [Valve Protocol](#valve) |
|
||||
| `dayzmod` | DayZ Mod (2013) | [Valve Protocol](#valve) |
|
||||
| `deadlydozenpt` | Deadly Dozen: Pacific Theater (2002) | |
|
||||
| `dh2005` | Deer Hunter 2005 (2004) | |
|
||||
| `descent3` | Descent 3 (1999) | |
|
||||
| `deusex` | Deus Ex (2000) | |
|
||||
| `devastation` | Devastation (2003) | |
|
||||
| `dinodday` | Dino D-Day (2011) | [Valve Protocol](#valve) |
|
||||
| `dirttrackracing2` | Dirt Track Racing 2 (2002) | |
|
||||
| `discord` | Discord | [Notes](#discord) |
|
||||
| `dmc` | Deathmatch Classic (2001) | [Valve Protocol](#valve) |
|
||||
| `dst` | Don't Starve Together (2016) | [Valve Protocol](#valve) |
|
||||
| `doom3` | Doom 3 (2004) | |
|
||||
| `dota2` | Dota 2 (2013) | [Valve Protocol](#valve) |
|
||||
| `drakan` | Drakan: Order of the Flame (1999) | |
|
||||
| `dystopia` | Dystopia (2005) | [Valve Protocol](#valve) |
|
||||
| `eco` | Eco (2018) | |
|
||||
| `empyrion` | Empyrion - Galactic Survival (2015) | [Valve Protocol](#valve) |
|
||||
| `empiresmod` | Empires Mod (2008) | [Valve Protocol](#valve) |
|
||||
| `etqw` | Enemy Territory: Quake Wars (2007) | |
|
||||
| `fear` | F.E.A.R. (2005) | |
|
||||
| `f1c9902` | F1 Challenge '99-'02 (2002) | |
|
||||
| `farcry` | Far Cry (2004) | |
|
||||
| `farcry2` | Far Cry 2 (2008) | |
|
||||
| `f12002` | Formula One 2002 (2002) | |
|
||||
| `fof` | Fistful of Frags (2014) | [Valve Protocol](#valve) |
|
||||
| `fortressforever` | Fortress Forever (2007) | [Valve Protocol](#valve) |
|
||||
| `ffow` | Frontlines: Fuel of War (2008) | |
|
||||
| `garrysmod` | Garry's Mod (2004) | [Valve Protocol](#valve) |
|
||||
| `geneshift`<br>`mutantfactions` | Geneshift (2017) | |
|
||||
| `giantscitizenkabuto` | Giants: Citizen Kabuto (2000) | |
|
||||
| `globaloperations` | Global Operations (2002) | |
|
||||
| `ges` | GoldenEye: Source (2010) | [Valve Protocol](#valve) |
|
||||
| `gore` | Gore: Ultimate Soldier (2002) | |
|
||||
| `fivem` | Grand Theft Auto V - FiveM (2013) | |
|
||||
| `mtasa` | Grand Theft Auto: San Andreas - Multi Theft Auto (2004) | |
|
||||
| `mtavc` | Grand Theft Auto: Vice City - Multi Theft Auto (2002) | |
|
||||
| `groundbreach` | Ground Breach (2018) | [Valve Protocol](#valve) |
|
||||
| `gunmanchronicles` | Gunman Chronicles (2000) | [Valve Protocol](#valve) |
|
||||
| `hl2dm` | Half-Life 2: Deathmatch (2004) | [Valve Protocol](#valve) |
|
||||
| `hldm` | Half-Life Deathmatch (1998) | [Valve Protocol](#valve) |
|
||||
| `hldms` | Half-Life Deathmatch: Source (2005) | [Valve Protocol](#valve) |
|
||||
| `hlopfor` | Half-Life: Opposing Force (1999) | [Valve Protocol](#valve) |
|
||||
| `halo` | Halo (2003) | |
|
||||
| `halo2` | Halo 2 (2007) | |
|
||||
| `hll` | Hell Let Loose | [Valve Protocol](#valve) |
|
||||
| `heretic2` | Heretic II (1998) | |
|
||||
| `hexen2` | Hexen II (1997) | |
|
||||
| `had2` | Hidden & Dangerous 2 (2003) | |
|
||||
| `homefront` | Homefront (2011) | [Valve Protocol](#valve) |
|
||||
| `homeworld2` | Homeworld 2 (2003) | |
|
||||
| `hurtworld` | Hurtworld (2015) | [Valve Protocol](#valve) |
|
||||
| `igi2` | I.G.I.-2: Covert Strike (2003) | |
|
||||
| `il2` | IL-2 Sturmovik (2001) | |
|
||||
| `insurgency` | Insurgency (2014) | [Valve Protocol](#valve) |
|
||||
| `insurgencymic` | Insurgency: Modern Infantry Combat (2007) | [Valve Protocol](#valve) |
|
||||
| `insurgencysandstorm` | Insurgency: Sandstorm (2018) | [Valve Protocol](#valve) |
|
||||
| `ironstorm` | Iron Storm (2002) | |
|
||||
| `isle` | The Isle (2015) | [Valve Protocol](#valve) |
|
||||
| `jamesbondnightfire` | James Bond 007: Nightfire (2002) | |
|
||||
| `jc2mp` | Just Cause 2 - Multiplayer (2010) | |
|
||||
| `jc3mp` | Just Cause 3 - Multiplayer (2017) | [Valve Protocol](#valve) |
|
||||
| `kspdmp` | Kerbal Space Program - DMP Multiplayer (2015) | |
|
||||
| `killingfloor` | Killing Floor (2009) | |
|
||||
| `killingfloor2` | Killing Floor 2 (2016) | [Valve Protocol](#valve) |
|
||||
| `kingpin` | Kingpin: Life of Crime (1999) | |
|
||||
| `kisspc` | Kiss: Psycho Circus: The Nightmare Child (2000) | |
|
||||
| `kzmod` | Kreedz Climbing (2017) | [Valve Protocol](#valve) |
|
||||
| `left4dead` | Left 4 Dead (2008) | [Valve Protocol](#valve) |
|
||||
| `left4dead2` | Left 4 Dead 2 (2009) | [Valve Protocol](#valve) |
|
||||
| `m2mp` | Mafia II - Multiplayer (2010) | |
|
||||
| `m2o` | Mafia II - Online (2010) | |
|
||||
| `moh2010` | Medal of Honor (2010) | |
|
||||
| `mohab` | Medal of Honor: Airborne (2007) | |
|
||||
| `mohaa` | Medal of Honor: Allied Assault (2002) | |
|
||||
| `mohbt` | Medal of Honor: Allied Assault Breakthrough (2003) | |
|
||||
| `mohsh` | Medal of Honor: Allied Assault Spearhead (2002) | |
|
||||
| `mohpa` | Medal of Honor: Pacific Assault (2004) | |
|
||||
| `mohwf` | Medal of Honor: Warfighter (2012) | |
|
||||
| `medievalengineers` | Medieval Engineers (2015) | [Valve Protocol](#valve) |
|
||||
| `minecraft`<br>`minecraftping` | Minecraft (2009) | |
|
||||
| `minecraftpe`<br>`minecraftbe` | Minecraft: Bedrock Edition (2011) | |
|
||||
| `mnc` | Monday Night Combat (2011) | [Valve Protocol](#valve) |
|
||||
| `mordhau` | Mordhau (2019) | [Valve Protocol](#valve) |
|
||||
| `mumble` | Mumble - GTmurmur Plugin (2005) | [Notes](#mumble) |
|
||||
| `mumbleping` | Mumble - Lightweight (2005) | [Notes](#mumble) |
|
||||
| `nascarthunder2004` | NASCAR Thunder 2004 (2003) | |
|
||||
| `ns` | Natural Selection (2002) | [Valve Protocol](#valve) |
|
||||
| `ns2` | Natural Selection 2 (2012) | [Valve Protocol](#valve) |
|
||||
| `nfshp2` | Need for Speed: Hot Pursuit 2 (2002) | |
|
||||
| `nab` | Nerf Arena Blast (1999) | |
|
||||
| `netpanzer` | netPanzer (2002) | |
|
||||
| `nwn` | Neverwinter Nights (2002) | |
|
||||
| `nwn2` | Neverwinter Nights 2 (2006) | |
|
||||
| `nexuiz` | Nexuiz (2005) | |
|
||||
| `nitrofamily` | Nitro Family (2004) | |
|
||||
| `nmrih` | No More Room in Hell (2011) | [Valve Protocol](#valve) |
|
||||
| `nolf2` | No One Lives Forever 2: A Spy in H.A.R.M.'s Way (2002) | |
|
||||
| `nucleardawn` | Nuclear Dawn (2011) | [Valve Protocol](#valve) |
|
||||
| `onset` | Onset (2019) | [Valve Protocol](#valve) |
|
||||
| `ohd` | Operation: Harsh Doorstop (2023) | [Valve Protocol](#valve) |
|
||||
| `openarena` | OpenArena (2005) | |
|
||||
| `openttd` | OpenTTD (2004) | |
|
||||
| `operationflashpoint`<br>`flashpoint` | Operation Flashpoint: Cold War Crisis (2001) | |
|
||||
| `flashpointresistance` | Operation Flashpoint: Resistance (2002) | |
|
||||
| `painkiller` | Painkiller | |
|
||||
| `pc` | Project Cars (2015) | [Valve Protocol](#valve) |
|
||||
| `pc2` | Project Cars 2 (2017) | [Valve Protocol](#valve) |
|
||||
| `pixark` | PixARK (2018) | [Valve Protocol](#valve) |
|
||||
| `pvkii` | Pirates, Vikings, and Knights II (2007) | [Valve Protocol](#valve) |
|
||||
| `ps` | Post Scriptum | |
|
||||
| `postal2` | Postal 2 | |
|
||||
| `prey` | Prey | |
|
||||
| `primalcarnage` | Primal Carnage: Extinction | [Valve Protocol](#valve) |
|
||||
| `prbf2` | Project Reality: Battlefield 2 (2005) | |
|
||||
| `przomboid` | Project Zomboid | [Valve Protocol](#valve) |
|
||||
| `quake1` | Quake 1: QuakeWorld (1996) | |
|
||||
| `quake2` | Quake 2 (1997) | |
|
||||
| `quake3` | Quake 3: Arena (1999) | |
|
||||
| `quake4` | Quake 4 (2005) | |
|
||||
| `quakelive` | Quake Live (2010) | [Valve Protocol](#valve) |
|
||||
| `ragdollkungfu` | Rag Doll Kung Fu | [Valve Protocol](#valve) |
|
||||
| `r6` | Rainbow Six | |
|
||||
| `r6roguespear` | Rainbow Six 2: Rogue Spear | |
|
||||
| `r6ravenshield` | Rainbow Six 3: Raven Shield | |
|
||||
| `rallisportchallenge` | RalliSport Challenge | |
|
||||
| `rallymasters` | Rally Masters | |
|
||||
| `redorchestra` | Red Orchestra | |
|
||||
| `redorchestra2` | Red Orchestra 2 | [Valve Protocol](#valve) |
|
||||
| `redorchestraost` | Red Orchestra: Ostfront 41-45 | |
|
||||
| `redline` | Redline | |
|
||||
| `redm` | Red Dead Redemption 2 - RedM (2018) | |
|
||||
| `rtcw` | Return to Castle Wolfenstein | |
|
||||
| `rfactor` | rFactor | |
|
||||
| `ricochet` | Ricochet | [Valve Protocol](#valve) |
|
||||
| `riseofnations` | Rise of Nations | |
|
||||
| `rs2` | Rising Storm 2: Vietnam | [Valve Protocol](#valve) |
|
||||
| `risingworld` | Rising World (2014) | [Valve Protocol](#valve) |
|
||||
| `ror2` | Risk of Rain 2 (2020) | [Valve Protocol](#valve) |
|
||||
| `rune` | Rune | |
|
||||
| `rust` | Rust | [Valve Protocol](#valve) |
|
||||
| `stalker` | S.T.A.L.K.E.R. | |
|
||||
| `samp` | San Andreas Multiplayer | |
|
||||
| `saomp` | San Andreas OpenMP | |
|
||||
| `savage2` | Savage 2: A Tortured Soul (2008) | |
|
||||
| `ss` | Serious Sam | |
|
||||
| `ss2` | Serious Sam 2 | |
|
||||
| `shatteredhorizon` | Shattered Horizon | [Valve Protocol](#valve) |
|
||||
| `shogo` | Shogo | |
|
||||
| `shootmania` | Shootmania | [Notes](#nadeo-shootmania--trackmania--etc) |
|
||||
| `sin` | SiN | |
|
||||
| `sinep` | SiN Episodes | [Valve Protocol](#valve) |
|
||||
| `soldat` | Soldat | |
|
||||
| `sof` | Soldier of Fortune | |
|
||||
| `sof2` | Soldier of Fortune 2 | |
|
||||
| `sonsoftheforest` | Sons Of The Forest | [Valve Protocol](#valve) |
|
||||
| `spaceengineers` | Space Engineers | [Valve Protocol](#valve) |
|
||||
| `squad` | Squad | [Valve Protocol](#valve) |
|
||||
| `stbc` | Star Trek: Bridge Commander | |
|
||||
| `stvef` | Star Trek: Voyager - Elite Force | |
|
||||
| `stvef2` | Star Trek: Voyager - Elite Force 2 | |
|
||||
| `swjk2` | Star Wars Jedi Knight II: Jedi Outcast (2002) | |
|
||||
| `swjk` | Star Wars Jedi Knight: Jedi Academy (2003) | |
|
||||
| `swbf` | Star Wars: Battlefront | |
|
||||
| `swbf2` | Star Wars: Battlefront 2 | |
|
||||
| `swrc` | Star Wars: Republic Commando | |
|
||||
| `starbound` | Starbound | [Valve Protocol](#valve) |
|
||||
| `starmade` | StarMade | |
|
||||
| `starsiege` | Starsiege (2009) | |
|
||||
| `suicidesurvival` | Suicide Survival | [Valve Protocol](#valve) |
|
||||
| `stn` | Survive the Nights (2017) | [Valve Protocol](#valve) |
|
||||
| `svencoop` | Sven Coop | [Valve Protocol](#valve) |
|
||||
| `swat4` | SWAT 4 | |
|
||||
| `synergy` | Synergy | [Valve Protocol](#valve) |
|
||||
| `tacticalops` | Tactical Ops | |
|
||||
| `takeonhelicopters` | Take On Helicopters (2011) | |
|
||||
| `teamfactor` | Team Factor | |
|
||||
| `tf2` | Team Fortress 2 | [Valve Protocol](#valve) |
|
||||
| `tfc` | Team Fortress Classic | [Valve Protocol](#valve) |
|
||||
| `teamspeak2` | Teamspeak 2 | |
|
||||
| `teamspeak3` | Teamspeak 3 | [Notes](#teamspeak3) |
|
||||
| `terminus` | Terminus | |
|
||||
| `terraria`<br>`tshock` | Terraria - TShock (2011) | [Notes](#terraria) |
|
||||
| `forrest` | The Forrest (2014) | [Valve Protocol](#valve) |
|
||||
| `thefront` | The Front (2023) | [The Front](#thefront), [Valve Protocol](#valve) |
|
||||
| `hidden` | The Hidden (2005) | [Valve Protocol](#valve) |
|
||||
| `nolf` | The Operative: No One Lives Forever (2000) | |
|
||||
| `ship` | The Ship | [Valve Protocol](#valve) |
|
||||
| `ts` | The Specialists | [Valve Protocol](#valve) |
|
||||
| `graw` | Tom Clancy's Ghost Recon Advanced Warfighter (2006) | |
|
||||
| `graw2` | Tom Clancy's Ghost Recon Advanced Warfighter 2 (2007) | |
|
||||
| `theforest` | The Forest (2014) | [Valve Protocol](#valve) |
|
||||
| `thps3` | Tony Hawk's Pro Skater 3 | |
|
||||
| `thps4` | Tony Hawk's Pro Skater 4 | |
|
||||
| `thu2` | Tony Hawk's Underground 2 | |
|
||||
| `towerunite` | Tower Unite | [Valve Protocol](#valve) |
|
||||
| `trackmania2` | Trackmania 2 | [Notes](#nadeo-shootmania--trackmania--etc) |
|
||||
| `trackmaniaforever` | Trackmania Forever | [Notes](#nadeo-shootmania--trackmania--etc) |
|
||||
| `tremulous` | Tremulous | |
|
||||
| `tribes1` | Tribes 1: Starsiege | |
|
||||
| `tribesvengeance` | Tribes: Vengeance | |
|
||||
| `tron20` | Tron 2.0 | |
|
||||
| `turok2` | Turok 2 | |
|
||||
| `universalcombat` | Universal Combat | |
|
||||
| `unreal` | Unreal | |
|
||||
| `ut` | Unreal Tournament | |
|
||||
| `ut2003` | Unreal Tournament 2003 | |
|
||||
| `ut2004` | Unreal Tournament 2004 | |
|
||||
| `ut3` | Unreal Tournament 3 | |
|
||||
| `unturned` | Unturned | [Valve Protocol](#valve) |
|
||||
| `urbanterror` | Urban Terror | |
|
||||
| `vrising` | V Rising (2022) | [Valve Protocol](#valve) |
|
||||
| `v8supercar` | V8 Supercar Challenge | |
|
||||
| `vs` | Vampire Slayer | [Valve Protocol](#valve) |
|
||||
| `valheim` | Valheim (2021) | [Notes](#valheim), [Valve Protocol](#valve) |
|
||||
| `ventrilo` | Ventrilo | |
|
||||
| `vcmp` | Vice City Multiplayer | |
|
||||
| `vietcong` | Vietcong | |
|
||||
| `vietcong2` | Vietcong 2 | |
|
||||
| `warfork` | Warfork | |
|
||||
| `warsow` | Warsow | |
|
||||
| `wheeloftime` | Wheel of Time | |
|
||||
| `wolfenstein2009` | Wolfenstein 2009 | |
|
||||
| `wolfensteinet` | Wolfenstein: Enemy Territory | |
|
||||
| `wurm` | Wurm: Unlimited | [Valve Protocol](#valve) |
|
||||
| `xpandrally` | Xpand Rally | |
|
||||
| `zombiemaster` | Zombie Master | [Valve Protocol](#valve) |
|
||||
| `zps` | Zombie Panic: Source | [Valve Protocol](#valve) |
|
||||
|
||||
### Not supported (yet)
|
||||
|
||||
* Cube Engine (cube):
|
||||
* Cube 1
|
||||
* Assault Cube
|
||||
* Cube 2: Sauerbraten
|
||||
* Blood Frontier
|
||||
* Alien vs Predator
|
||||
* Armed Assault 2: Operation Arrowhead
|
||||
* Battlefield Bad Company 2: Vietnam
|
||||
* BFRIS
|
||||
* Call of Duty: Black Ops 1 and 2 (no documentation, may require rcon)
|
||||
* Crysis Warhead
|
||||
* Days of War
|
||||
* DirtyBomb
|
||||
* Doom - Skulltag
|
||||
* Doom - ZDaemon
|
||||
* ECO Global Survival ([Ref](https://github.com/Austinb/GameQ/blob/v3/src/GameQ/Protocols/Eco.php))
|
||||
* Farming Simulator
|
||||
* Freelancer
|
||||
* Ghost Recon
|
||||
* GRAV Online
|
||||
* GTA Network ([Ref](https://github.com/Austinb/GameQ/blob/v3/src/GameQ/Protocols/Gtan.php))
|
||||
* GTR 2
|
||||
* Haze
|
||||
* Hexen World
|
||||
* Lost Heaven
|
||||
* Multi Theft Auto
|
||||
* Pariah
|
||||
* Plain Sight
|
||||
* Purge Jihad
|
||||
* Red Eclipse
|
||||
* Red Faction
|
||||
* S.T.A.L.K.E.R. Clear Sky
|
||||
* Savage: The Battle For Newerth
|
||||
* SiN 1 Multiplayer
|
||||
* South Park
|
||||
* Star Wars Jedi Knight: Dark Forces II
|
||||
* Star Wars: X-Wing Alliance
|
||||
* Sum of All Fears
|
||||
* Teeworlds
|
||||
* Tibia ([Ref](https://github.com/Austinb/GameQ/blob/v3/src/GameQ/Protocols/Tibia.php))
|
||||
* Titanfall
|
||||
* Tribes 2
|
||||
* Unreal 2 XMP
|
||||
* World in Conflict
|
||||
* World Opponent Network
|
||||
* Wurm Unlimited
|
||||
|
||||
> Want support for one of these games? Please open an issue to show your interest!
|
||||
> __Know how to code?__ Protocol details for many of the games above are documented
|
||||
> at https://github.com/gamedig/legacy-query-library-archive
|
||||
> , ready for you to develop into GameDig!
|
||||
|
||||
> Don't see your game listed here?
|
||||
>
|
||||
> First, let us know, so we can fix it. Then, you can try using some common query
|
||||
> protocols directly by using one of these server types:
|
||||
> * protocol-ase
|
||||
> * protocol-battlefield
|
||||
> * protocol-doom3
|
||||
> * protocol-gamespy1
|
||||
> * protocol-gamespy2
|
||||
> * protocol-gamespy3
|
||||
> * protocol-nadeo
|
||||
> * protocol-quake2
|
||||
> * protocol-quake3
|
||||
> * protocol-unreal2
|
||||
> * protocol-valve
|
||||
|
||||
Games with Additional Notes
|
||||
---
|
||||
|
||||
### <a name="csgo"></a>Counter-Strike: Global Offensive
|
||||
To receive a full player list response from CS:GO servers, the server must
|
||||
have set the cvar: host_players_show 2
|
||||
|
||||
### Discord
|
||||
You must set the `guildId` request field to the server's guild ID. Do not provide an IP.
|
||||
The Guild ID can be found in server widget settings (Server ID) or by enabling developer mode in client settings and right-clicking the server's icon.
|
||||
In order to retrieve information from discord server's they must have the `Enable server widget` option enabled.
|
||||
|
||||
### Mumble
|
||||
For full query results from Mumble, you must be running the
|
||||
[GTmurmur plugin](http://www.gametracker.com/downloads/gtmurmurplugin.php).
|
||||
If you do not wish to run the plugin, or do not require details such as channel and user lists,
|
||||
you can use the 'mumbleping' server type instead, which uses a less accurate but more reliable solution
|
||||
|
||||
### Nadeo (ShootMania / TrackMania / etc)
|
||||
The server must have xmlrpc enabled, and you must pass the xmlrpc port to GameDig, not the connection port.
|
||||
You must have a user account on the server with access level User or higher.
|
||||
Pass the login into to GameDig with the additional options: login, password
|
||||
|
||||
### <a name="teamspeak3"></a>TeamSpeak 3
|
||||
For teamspeak 3 queries to work correctly, the following permissions must be available for the guest server group:
|
||||
|
||||
* Virtual Server
|
||||
* b_virtualserver_info_view
|
||||
* b_virtualserver_channel_list
|
||||
* b_virtualserver_client_list
|
||||
* Group
|
||||
* b_virtualserver_servergroup_list
|
||||
* b_virtualserver_channelgroup_list
|
||||
|
||||
In the extremely unusual case that your server host responds to queries on a non-default port (the default is 10011),
|
||||
you can specify their host query port using the teamspeakQueryPort option.
|
||||
|
||||
### Terraria
|
||||
Requires tshock server mod, and a REST user token, which can be passed to GameDig with the
|
||||
additional option: `token`
|
||||
|
||||
### Valheim
|
||||
Valheim servers will only respond to queries if they are started in public mode (`-public 1`).
|
||||
|
||||
### DayZ
|
||||
DayZ stores some of it's servers information inside the `tags` attribute. Make sure to set `requestRules: true` to access it. Some data inside `dayzMods` attribute may be fuzzy, due to how mods are loaded into the servers. Alternatively, some servers may have a [third party tool](https://dayzsalauncher.com/#/tools) that you can use to get the mods information. If it's installed, you can access it via browser with the game servers IP:PORT, but add up 10 to the port. (eg. if game port is 2302 then use 2312).
|
||||
|
||||
### <a name="valve"></a>Valve Protocol
|
||||
For many valve games, additional 'rules' may be fetched into the unstable `raw` field by passing the additional
|
||||
option: `requestRules: true`. Beware that this may increase query time.
|
||||
|
||||
### <a name="thefront"></a>The Front
|
||||
Responses with wrong `name` (gives out a steamid instead of the server name) and `maxplayers` (always 200, whatever the config would be) field values.
|
||||
|
|
138
bin/gamedig.js
Executable file → Normal file
138
bin/gamedig.js
Executable file → Normal file
|
@ -1,69 +1,69 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import * as process from "node:process";
|
||||
|
||||
import Minimist from 'minimist'
|
||||
import GameDig from './../lib/index.js'
|
||||
|
||||
const argv = Minimist(process.argv.slice(2), {
|
||||
boolean: ['pretty', 'debug', 'givenPortOnly', 'requestRules'],
|
||||
string: ['guildId', 'listenUdpPort', 'ipFamily']
|
||||
})
|
||||
|
||||
const debug = argv.debug
|
||||
delete argv.debug
|
||||
const pretty = !!argv.pretty || debug
|
||||
delete argv.pretty
|
||||
const givenPortOnly = argv.givenPortOnly
|
||||
delete argv.givenPortOnly
|
||||
|
||||
const options = {}
|
||||
for (const key of Object.keys(argv)) {
|
||||
const value = argv[key]
|
||||
|
||||
if (key === '_' || key.charAt(0) === '$') { continue }
|
||||
|
||||
options[key] = value
|
||||
}
|
||||
|
||||
if (argv._.length >= 1) {
|
||||
const target = argv._[0]
|
||||
const split = target.split(':')
|
||||
options.host = split[0]
|
||||
if (split.length >= 2) {
|
||||
options.port = split[1]
|
||||
}
|
||||
}
|
||||
if (debug) {
|
||||
options.debug = true
|
||||
}
|
||||
if (givenPortOnly) {
|
||||
options.givenPortOnly = true
|
||||
}
|
||||
|
||||
const printOnPretty = (object) => {
|
||||
if (pretty) {
|
||||
console.log(JSON.stringify(object, null, ' '))
|
||||
} else {
|
||||
console.log(JSON.stringify(object))
|
||||
}
|
||||
}
|
||||
|
||||
const gamedig = new GameDig(options)
|
||||
gamedig.query(options)
|
||||
.then(printOnPretty)
|
||||
.catch((error) => {
|
||||
if (debug) {
|
||||
if (error instanceof Error) {
|
||||
console.log(error.stack)
|
||||
} else {
|
||||
console.log(error)
|
||||
}
|
||||
} else {
|
||||
if (error instanceof Error) {
|
||||
error = error.message
|
||||
}
|
||||
|
||||
printOnPretty({ error })
|
||||
}
|
||||
})
|
||||
#!/usr/bin/env node
|
||||
|
||||
import * as process from "node:process";
|
||||
|
||||
import Minimist from 'minimist'
|
||||
import GameDig from './../lib/index.js'
|
||||
|
||||
const argv = Minimist(process.argv.slice(2), {
|
||||
boolean: ['pretty', 'debug', 'givenPortOnly', 'requestRules'],
|
||||
string: ['guildId', 'listenUdpPort', 'ipFamily']
|
||||
})
|
||||
|
||||
const debug = argv.debug
|
||||
delete argv.debug
|
||||
const pretty = !!argv.pretty || debug
|
||||
delete argv.pretty
|
||||
const givenPortOnly = argv.givenPortOnly
|
||||
delete argv.givenPortOnly
|
||||
|
||||
const options = {}
|
||||
for (const key of Object.keys(argv)) {
|
||||
const value = argv[key]
|
||||
|
||||
if (key === '_' || key.charAt(0) === '$') { continue }
|
||||
|
||||
options[key] = value
|
||||
}
|
||||
|
||||
if (argv._.length >= 1) {
|
||||
const target = argv._[0]
|
||||
const split = target.split(':')
|
||||
options.host = split[0]
|
||||
if (split.length >= 2) {
|
||||
options.port = split[1]
|
||||
}
|
||||
}
|
||||
if (debug) {
|
||||
options.debug = true
|
||||
}
|
||||
if (givenPortOnly) {
|
||||
options.givenPortOnly = true
|
||||
}
|
||||
|
||||
const printOnPretty = (object) => {
|
||||
if (pretty) {
|
||||
console.log(JSON.stringify(object, null, ' '))
|
||||
} else {
|
||||
console.log(JSON.stringify(object))
|
||||
}
|
||||
}
|
||||
|
||||
const gamedig = new GameDig(options)
|
||||
gamedig.query(options)
|
||||
.then(printOnPretty)
|
||||
.catch((error) => {
|
||||
if (debug) {
|
||||
if (error instanceof Error) {
|
||||
console.log(error.stack)
|
||||
} else {
|
||||
console.log(error)
|
||||
}
|
||||
} else {
|
||||
if (error instanceof Error) {
|
||||
error = error.message
|
||||
}
|
||||
|
||||
printOnPretty({ error })
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,74 +1,74 @@
|
|||
import dns from 'node:dns'
|
||||
import punycode from 'punycode/punycode.js'
|
||||
|
||||
export default class DnsResolver {
|
||||
/**
|
||||
* @param {Logger} logger
|
||||
*/
|
||||
constructor (logger) {
|
||||
this.logger = logger
|
||||
}
|
||||
|
||||
isIp (host) {
|
||||
return !!host.match(/\d+\.\d+\.\d+\.\d+/)
|
||||
}
|
||||
|
||||
/**
|
||||
* Response port will only be present if srv record was involved.
|
||||
* @param {string} host
|
||||
* @param {number} ipFamily
|
||||
* @param {string=} srvRecordPrefix
|
||||
* @returns {Promise<{address:string, port:number=}>}
|
||||
*/
|
||||
async resolve (host, ipFamily, srvRecordPrefix) {
|
||||
this.logger.debug('DNS Lookup: ' + host)
|
||||
|
||||
if (this.isIp(host)) {
|
||||
this.logger.debug('Raw IP Address: ' + host)
|
||||
return { address: host }
|
||||
}
|
||||
|
||||
const asciiForm = punycode.toASCII(host)
|
||||
if (asciiForm !== host) {
|
||||
this.logger.debug('Encoded punycode: ' + host + ' -> ' + asciiForm)
|
||||
host = asciiForm
|
||||
}
|
||||
|
||||
if (srvRecordPrefix) {
|
||||
this.logger.debug('SRV Resolve: ' + srvRecordPrefix + '.' + host)
|
||||
let records
|
||||
try {
|
||||
records = await dns.promises.resolve(srvRecordPrefix + '.' + host, 'SRV')
|
||||
if (records.length >= 1) {
|
||||
this.logger.debug('Found SRV Records: ', records)
|
||||
const record = records[0]
|
||||
const srvPort = record.port
|
||||
const srvHost = record.name
|
||||
if (srvHost === host) {
|
||||
throw new Error('Loop in DNS SRV records')
|
||||
}
|
||||
return {
|
||||
port: srvPort,
|
||||
...await this.resolve(srvHost, ipFamily, srvRecordPrefix)
|
||||
}
|
||||
}
|
||||
this.logger.debug('No SRV Record')
|
||||
} catch (e) {
|
||||
this.logger.debug(e)
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.debug('Standard Resolve: ' + host)
|
||||
const dnsResult = await dns.promises.lookup(host, ipFamily)
|
||||
// For some reason, this sometimes returns a string address rather than an object.
|
||||
// I haven't been able to reproduce, but it's been reported on the issue tracker.
|
||||
let address
|
||||
if (typeof dnsResult === 'string') {
|
||||
address = dnsResult
|
||||
} else {
|
||||
address = dnsResult.address
|
||||
}
|
||||
this.logger.debug('Found address: ' + address)
|
||||
return { address }
|
||||
}
|
||||
}
|
||||
import dns from 'node:dns'
|
||||
import punycode from 'punycode/punycode.js'
|
||||
|
||||
export default class DnsResolver {
|
||||
/**
|
||||
* @param {Logger} logger
|
||||
*/
|
||||
constructor (logger) {
|
||||
this.logger = logger
|
||||
}
|
||||
|
||||
isIp (host) {
|
||||
return !!host.match(/\d+\.\d+\.\d+\.\d+/)
|
||||
}
|
||||
|
||||
/**
|
||||
* Response port will only be present if srv record was involved.
|
||||
* @param {string} host
|
||||
* @param {number} ipFamily
|
||||
* @param {string=} srvRecordPrefix
|
||||
* @returns {Promise<{address:string, port:number=}>}
|
||||
*/
|
||||
async resolve (host, ipFamily, srvRecordPrefix) {
|
||||
this.logger.debug('DNS Lookup: ' + host)
|
||||
|
||||
if (this.isIp(host)) {
|
||||
this.logger.debug('Raw IP Address: ' + host)
|
||||
return { address: host }
|
||||
}
|
||||
|
||||
const asciiForm = punycode.toASCII(host)
|
||||
if (asciiForm !== host) {
|
||||
this.logger.debug('Encoded punycode: ' + host + ' -> ' + asciiForm)
|
||||
host = asciiForm
|
||||
}
|
||||
|
||||
if (srvRecordPrefix) {
|
||||
this.logger.debug('SRV Resolve: ' + srvRecordPrefix + '.' + host)
|
||||
let records
|
||||
try {
|
||||
records = await dns.promises.resolve(srvRecordPrefix + '.' + host, 'SRV')
|
||||
if (records.length >= 1) {
|
||||
this.logger.debug('Found SRV Records: ', records)
|
||||
const record = records[0]
|
||||
const srvPort = record.port
|
||||
const srvHost = record.name
|
||||
if (srvHost === host) {
|
||||
throw new Error('Loop in DNS SRV records')
|
||||
}
|
||||
return {
|
||||
port: srvPort,
|
||||
...await this.resolve(srvHost, ipFamily, srvRecordPrefix)
|
||||
}
|
||||
}
|
||||
this.logger.debug('No SRV Record')
|
||||
} catch (e) {
|
||||
this.logger.debug(e)
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.debug('Standard Resolve: ' + host)
|
||||
const dnsResult = await dns.promises.lookup(host, ipFamily)
|
||||
// For some reason, this sometimes returns a string address rather than an object.
|
||||
// I haven't been able to reproduce, but it's been reported on the issue tracker.
|
||||
let address
|
||||
if (typeof dnsResult === 'string') {
|
||||
address = dnsResult
|
||||
} else {
|
||||
address = dnsResult.address
|
||||
}
|
||||
this.logger.debug('Found address: ' + address)
|
||||
return { address }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,114 +1,114 @@
|
|||
import * as path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import * as fs from 'node:fs'
|
||||
|
||||
export default class GameResolver {
|
||||
constructor () {
|
||||
const loaded = this._readGames()
|
||||
this.gamesByKey = loaded.gamesByKey
|
||||
this.games = loaded.games
|
||||
}
|
||||
|
||||
lookup (type) {
|
||||
if (!type) { throw Error('No game specified') }
|
||||
|
||||
if (type.startsWith('protocol-')) {
|
||||
return {
|
||||
protocol: type.substring(9)
|
||||
}
|
||||
}
|
||||
|
||||
const game = this.gamesByKey.get(type)
|
||||
|
||||
if (!game) { throw Error('Invalid game: ' + type) }
|
||||
|
||||
return game.options
|
||||
}
|
||||
|
||||
printReadme () {
|
||||
let out = ''
|
||||
out += '| GameDig Type ID | Name | See Also\n'
|
||||
out += '|---|---|---\n'
|
||||
|
||||
const sorted = this.games
|
||||
.filter(game => game.pretty)
|
||||
.sort((a, b) => {
|
||||
return a.pretty.localeCompare(b.pretty)
|
||||
})
|
||||
for (const game of sorted) {
|
||||
const keysOut = game.keys.map(key => '`' + key + '`').join('<br>')
|
||||
out += '| ' + keysOut.padEnd(10, ' ') + ' ' +
|
||||
'| ' + game.pretty
|
||||
const notes = []
|
||||
if (game.extra.doc_notes) {
|
||||
notes.push('[Notes](#' + game.extra.doc_notes + ')')
|
||||
}
|
||||
if (game.options.protocol === 'valve') {
|
||||
notes.push('[Valve Protocol](#valve)')
|
||||
}
|
||||
if (notes.length) {
|
||||
out += ' | ' + notes.join(', ')
|
||||
}
|
||||
out += '\n'
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
_readGames () {
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const gamesFile = path.normalize(__dirname + '/../games.txt')
|
||||
const lines = fs.readFileSync(gamesFile, 'utf8').split('\n')
|
||||
|
||||
const gamesByKey = new Map()
|
||||
const games = []
|
||||
|
||||
for (let line of lines) {
|
||||
// strip comments
|
||||
const comment = line.indexOf('#')
|
||||
if (comment !== -1) line = line.substring(0, comment)
|
||||
line = line.trim()
|
||||
if (!line) continue
|
||||
|
||||
const split = line.split('|')
|
||||
const keys = split[0].trim().split(',')
|
||||
const name = split[1].trim()
|
||||
const options = this._parseList(split[3])
|
||||
options.protocol = split[2].trim()
|
||||
const extra = this._parseList(split[4])
|
||||
|
||||
const game = {
|
||||
keys,
|
||||
pretty: name,
|
||||
options,
|
||||
extra
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
gamesByKey.set(key, game)
|
||||
}
|
||||
|
||||
games.push(game)
|
||||
}
|
||||
return { gamesByKey, games }
|
||||
}
|
||||
|
||||
_parseList (str) {
|
||||
if (!str) { return {} }
|
||||
|
||||
const out = {}
|
||||
for (const one of str.split(',')) {
|
||||
const equals = one.indexOf('=')
|
||||
const key = equals === -1 ? one : one.substring(0, equals)
|
||||
|
||||
/** @type {string|number|boolean} */
|
||||
let value = equals === -1 ? '' : one.substring(equals + 1)
|
||||
|
||||
if (value === 'true' || value === '') { value = true } else if (value === 'false') { value = false } else if (!isNaN(parseInt(value))) { value = parseInt(value) }
|
||||
|
||||
out[key] = value
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
}
|
||||
import * as path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import * as fs from 'node:fs'
|
||||
|
||||
export default class GameResolver {
|
||||
constructor () {
|
||||
const loaded = this._readGames()
|
||||
this.gamesByKey = loaded.gamesByKey
|
||||
this.games = loaded.games
|
||||
}
|
||||
|
||||
lookup (type) {
|
||||
if (!type) { throw Error('No game specified') }
|
||||
|
||||
if (type.startsWith('protocol-')) {
|
||||
return {
|
||||
protocol: type.substring(9)
|
||||
}
|
||||
}
|
||||
|
||||
const game = this.gamesByKey.get(type)
|
||||
|
||||
if (!game) { throw Error('Invalid game: ' + type) }
|
||||
|
||||
return game.options
|
||||
}
|
||||
|
||||
printReadme () {
|
||||
let out = ''
|
||||
out += '| GameDig Type ID | Name | See Also\n'
|
||||
out += '|---|---|---\n'
|
||||
|
||||
const sorted = this.games
|
||||
.filter(game => game.pretty)
|
||||
.sort((a, b) => {
|
||||
return a.pretty.localeCompare(b.pretty)
|
||||
})
|
||||
for (const game of sorted) {
|
||||
const keysOut = game.keys.map(key => '`' + key + '`').join('<br>')
|
||||
out += '| ' + keysOut.padEnd(10, ' ') + ' ' +
|
||||
'| ' + game.pretty
|
||||
const notes = []
|
||||
if (game.extra.doc_notes) {
|
||||
notes.push('[Notes](#' + game.extra.doc_notes + ')')
|
||||
}
|
||||
if (game.options.protocol === 'valve') {
|
||||
notes.push('[Valve Protocol](#valve)')
|
||||
}
|
||||
if (notes.length) {
|
||||
out += ' | ' + notes.join(', ')
|
||||
}
|
||||
out += '\n'
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
_readGames () {
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const gamesFile = path.normalize(__dirname + '/../games.txt')
|
||||
const lines = fs.readFileSync(gamesFile, 'utf8').split('\n')
|
||||
|
||||
const gamesByKey = new Map()
|
||||
const games = []
|
||||
|
||||
for (let line of lines) {
|
||||
// strip comments
|
||||
const comment = line.indexOf('#')
|
||||
if (comment !== -1) line = line.substring(0, comment)
|
||||
line = line.trim()
|
||||
if (!line) continue
|
||||
|
||||
const split = line.split('|')
|
||||
const keys = split[0].trim().split(',')
|
||||
const name = split[1].trim()
|
||||
const options = this._parseList(split[3])
|
||||
options.protocol = split[2].trim()
|
||||
const extra = this._parseList(split[4])
|
||||
|
||||
const game = {
|
||||
keys,
|
||||
pretty: name,
|
||||
options,
|
||||
extra
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
gamesByKey.set(key, game)
|
||||
}
|
||||
|
||||
games.push(game)
|
||||
}
|
||||
return { gamesByKey, games }
|
||||
}
|
||||
|
||||
_parseList (str) {
|
||||
if (!str) { return {} }
|
||||
|
||||
const out = {}
|
||||
for (const one of str.split(',')) {
|
||||
const equals = one.indexOf('=')
|
||||
const key = equals === -1 ? one : one.substring(0, equals)
|
||||
|
||||
/** @type {string|number|boolean} */
|
||||
let value = equals === -1 ? '' : one.substring(equals + 1)
|
||||
|
||||
if (value === 'true' || value === '') { value = true } else if (value === 'false') { value = false } else if (!isNaN(parseInt(value))) { value = parseInt(value) }
|
||||
|
||||
out[key] = value
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,72 +1,72 @@
|
|||
import { createSocket } from 'node:dgram'
|
||||
import { debugDump } from './HexUtil.js'
|
||||
import { promisify } from 'node:util'
|
||||
import Logger from './Logger.js'
|
||||
|
||||
export default class GlobalUdpSocket {
|
||||
constructor ({ port }) {
|
||||
this.socket = null
|
||||
this.callbacks = new Set()
|
||||
this.debuggingCallbacks = new Set()
|
||||
this.logger = new Logger()
|
||||
this.port = port
|
||||
}
|
||||
|
||||
async _getSocket () {
|
||||
if (!this.socket) {
|
||||
const udpSocket = createSocket({
|
||||
type: 'udp4',
|
||||
reuseAddr: true
|
||||
})
|
||||
// https://github.com/denoland/deno/issues/20138
|
||||
if (typeof Deno === "undefined") {
|
||||
udpSocket.unref();
|
||||
}
|
||||
udpSocket.on('message', (buffer, rinfo) => {
|
||||
const fromAddress = rinfo.address
|
||||
const fromPort = rinfo.port
|
||||
this.logger.debug(log => {
|
||||
log(fromAddress + ':' + fromPort + ' <--UDP(' + this.port + ')')
|
||||
log(debugDump(buffer))
|
||||
})
|
||||
for (const callback of this.callbacks) {
|
||||
callback(fromAddress, fromPort, buffer)
|
||||
}
|
||||
})
|
||||
udpSocket.on('error', e => {
|
||||
this.logger.debug('UDP ERROR:', e)
|
||||
})
|
||||
await promisify(udpSocket.bind).bind(udpSocket)(this.port)
|
||||
this.port = udpSocket.address().port
|
||||
this.socket = udpSocket
|
||||
}
|
||||
return this.socket
|
||||
}
|
||||
|
||||
async send (buffer, address, port, debug) {
|
||||
const socket = await this._getSocket()
|
||||
|
||||
if (debug) {
|
||||
this.logger._print(log => {
|
||||
log(address + ':' + port + ' UDP(' + this.port + ')-->')
|
||||
log(debugDump(buffer))
|
||||
})
|
||||
}
|
||||
|
||||
await promisify(socket.send).bind(socket)(buffer, 0, buffer.length, port, address)
|
||||
}
|
||||
|
||||
addCallback (callback, debug) {
|
||||
this.callbacks.add(callback)
|
||||
if (debug) {
|
||||
this.debuggingCallbacks.add(callback)
|
||||
this.logger.debugEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
removeCallback (callback) {
|
||||
this.callbacks.delete(callback)
|
||||
this.debuggingCallbacks.delete(callback)
|
||||
this.logger.debugEnabled = this.debuggingCallbacks.size > 0
|
||||
}
|
||||
}
|
||||
import { createSocket } from 'node:dgram'
|
||||
import { debugDump } from './HexUtil.js'
|
||||
import { promisify } from 'node:util'
|
||||
import Logger from './Logger.js'
|
||||
|
||||
export default class GlobalUdpSocket {
|
||||
constructor ({ port }) {
|
||||
this.socket = null
|
||||
this.callbacks = new Set()
|
||||
this.debuggingCallbacks = new Set()
|
||||
this.logger = new Logger()
|
||||
this.port = port
|
||||
}
|
||||
|
||||
async _getSocket () {
|
||||
if (!this.socket) {
|
||||
const udpSocket = createSocket({
|
||||
type: 'udp4',
|
||||
reuseAddr: true
|
||||
})
|
||||
// https://github.com/denoland/deno/issues/20138
|
||||
if (typeof Deno === "undefined") {
|
||||
udpSocket.unref();
|
||||
}
|
||||
udpSocket.on('message', (buffer, rinfo) => {
|
||||
const fromAddress = rinfo.address
|
||||
const fromPort = rinfo.port
|
||||
this.logger.debug(log => {
|
||||
log(fromAddress + ':' + fromPort + ' <--UDP(' + this.port + ')')
|
||||
log(debugDump(buffer))
|
||||
})
|
||||
for (const callback of this.callbacks) {
|
||||
callback(fromAddress, fromPort, buffer)
|
||||
}
|
||||
})
|
||||
udpSocket.on('error', e => {
|
||||
this.logger.debug('UDP ERROR:', e)
|
||||
})
|
||||
await promisify(udpSocket.bind).bind(udpSocket)(this.port)
|
||||
this.port = udpSocket.address().port
|
||||
this.socket = udpSocket
|
||||
}
|
||||
return this.socket
|
||||
}
|
||||
|
||||
async send (buffer, address, port, debug) {
|
||||
const socket = await this._getSocket()
|
||||
|
||||
if (debug) {
|
||||
this.logger._print(log => {
|
||||
log(address + ':' + port + ' UDP(' + this.port + ')-->')
|
||||
log(debugDump(buffer))
|
||||
})
|
||||
}
|
||||
|
||||
await promisify(socket.send).bind(socket)(buffer, 0, buffer.length, port, address)
|
||||
}
|
||||
|
||||
addCallback (callback, debug) {
|
||||
this.callbacks.add(callback)
|
||||
if (debug) {
|
||||
this.debuggingCallbacks.add(callback)
|
||||
this.logger.debugEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
removeCallback (callback) {
|
||||
this.callbacks.delete(callback)
|
||||
this.debuggingCallbacks.delete(callback)
|
||||
this.logger.debugEnabled = this.debuggingCallbacks.size > 0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
/** @param {Buffer} buffer */
|
||||
export const debugDump = (buffer) => {
|
||||
let hexLine = ''
|
||||
let chrLine = ''
|
||||
let out = ''
|
||||
out += 'Buffer length: ' + buffer.length + ' bytes\n'
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
const sliced = buffer.slice(i, i + 1)
|
||||
hexLine += sliced.toString('hex') + ' '
|
||||
let chr = sliced.toString()
|
||||
if (chr < ' ' || chr > '~') chr = ' '
|
||||
chrLine += chr + ' '
|
||||
if (hexLine.length > 60 || i === buffer.length - 1) {
|
||||
out += hexLine + '\n'
|
||||
out += chrLine + '\n'
|
||||
hexLine = chrLine = ''
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
/** @param {Buffer} buffer */
|
||||
export const debugDump = (buffer) => {
|
||||
let hexLine = ''
|
||||
let chrLine = ''
|
||||
let out = ''
|
||||
out += 'Buffer length: ' + buffer.length + ' bytes\n'
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
const sliced = buffer.slice(i, i + 1)
|
||||
hexLine += sliced.toString('hex') + ' '
|
||||
let chr = sliced.toString()
|
||||
if (chr < ' ' || chr > '~') chr = ' '
|
||||
chrLine += chr + ' '
|
||||
if (hexLine.length > 60 || i === buffer.length - 1) {
|
||||
out += hexLine + '\n'
|
||||
out += chrLine + '\n'
|
||||
hexLine = chrLine = ''
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
|
|
@ -1,45 +1,45 @@
|
|||
import { debugDump } from './HexUtil.js'
|
||||
import { Buffer} from 'node:buffer'
|
||||
|
||||
export default class Logger {
|
||||
constructor () {
|
||||
this.debugEnabled = false
|
||||
this.prefix = ''
|
||||
}
|
||||
|
||||
debug (...args) {
|
||||
if (!this.debugEnabled) return
|
||||
this._print(...args)
|
||||
}
|
||||
|
||||
_print (...args) {
|
||||
try {
|
||||
const strings = this._convertArgsToStrings(...args)
|
||||
if (strings.length) {
|
||||
if (this.prefix) {
|
||||
strings.unshift(this.prefix)
|
||||
}
|
||||
console.log(...strings)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Error while logging: ' + e)
|
||||
}
|
||||
}
|
||||
|
||||
_convertArgsToStrings (...args) {
|
||||
const out = []
|
||||
for (const arg of args) {
|
||||
if (arg instanceof Error) {
|
||||
out.push(arg.stack)
|
||||
} else if (arg instanceof Buffer) {
|
||||
out.push(debugDump(arg))
|
||||
} else if (typeof arg === 'function') {
|
||||
const result = arg.call(undefined, (...args) => this._print(...args))
|
||||
if (result !== undefined) out.push(...this._convertArgsToStrings(result))
|
||||
} else {
|
||||
out.push(arg)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
import { debugDump } from './HexUtil.js'
|
||||
import { Buffer} from 'node:buffer'
|
||||
|
||||
export default class Logger {
|
||||
constructor () {
|
||||
this.debugEnabled = false
|
||||
this.prefix = ''
|
||||
}
|
||||
|
||||
debug (...args) {
|
||||
if (!this.debugEnabled) return
|
||||
this._print(...args)
|
||||
}
|
||||
|
||||
_print (...args) {
|
||||
try {
|
||||
const strings = this._convertArgsToStrings(...args)
|
||||
if (strings.length) {
|
||||
if (this.prefix) {
|
||||
strings.unshift(this.prefix)
|
||||
}
|
||||
console.log(...strings)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Error while logging: ' + e)
|
||||
}
|
||||
}
|
||||
|
||||
_convertArgsToStrings (...args) {
|
||||
const out = []
|
||||
for (const arg of args) {
|
||||
if (arg instanceof Error) {
|
||||
out.push(arg.stack)
|
||||
} else if (arg instanceof Buffer) {
|
||||
out.push(debugDump(arg))
|
||||
} else if (typeof arg === 'function') {
|
||||
const result = arg.call(undefined, (...args) => this._print(...args))
|
||||
if (result !== undefined) out.push(...this._convertArgsToStrings(result))
|
||||
} else {
|
||||
out.push(arg)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
export default class Promises {
|
||||
static createTimeout (timeoutMs, timeoutMsg) {
|
||||
let cancel = null
|
||||
const wrapped = new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(
|
||||
() => {
|
||||
reject(new Error(timeoutMsg + ' - Timed out after ' + timeoutMs + 'ms'))
|
||||
},
|
||||
timeoutMs
|
||||
)
|
||||
cancel = () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
})
|
||||
wrapped.cancel = cancel
|
||||
return wrapped
|
||||
}
|
||||
}
|
||||
export default class Promises {
|
||||
static createTimeout (timeoutMs, timeoutMsg) {
|
||||
let cancel = null
|
||||
const wrapped = new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(
|
||||
() => {
|
||||
reject(new Error(timeoutMsg + ' - Timed out after ' + timeoutMs + 'ms'))
|
||||
},
|
||||
timeoutMs
|
||||
)
|
||||
cancel = () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
})
|
||||
wrapped.cancel = cancel
|
||||
return wrapped
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as Protocols from '../protocols/index.js'
|
||||
|
||||
export const getProtocol = (protocolId) => {
|
||||
if (!(protocolId in Protocols)) { throw Error('Protocol definition file missing: ' + protocolId) }
|
||||
|
||||
return new Protocols[protocolId]()
|
||||
}
|
||||
import * as Protocols from '../protocols/index.js'
|
||||
|
||||
export const getProtocol = (protocolId) => {
|
||||
if (!(protocolId in Protocols)) { throw Error('Protocol definition file missing: ' + protocolId) }
|
||||
|
||||
return new Protocols[protocolId]()
|
||||
}
|
||||
|
|
|
@ -1,104 +1,104 @@
|
|||
import GameResolver from './GameResolver.js'
|
||||
import { getProtocol } from './ProtocolResolver.js'
|
||||
import GlobalUdpSocket from './GlobalUdpSocket.js'
|
||||
|
||||
const defaultOptions = {
|
||||
socketTimeout: 2000,
|
||||
attemptTimeout: 10000,
|
||||
maxAttempts: 1,
|
||||
ipFamily: 0
|
||||
}
|
||||
|
||||
export default class QueryRunner {
|
||||
constructor (runnerOpts = {}) {
|
||||
this.udpSocket = new GlobalUdpSocket({
|
||||
port: runnerOpts.listenUdpPort
|
||||
})
|
||||
this.gameResolver = new GameResolver()
|
||||
}
|
||||
|
||||
async run (userOptions) {
|
||||
for (const key of Object.keys(userOptions)) {
|
||||
const value = userOptions[key]
|
||||
if (['port', 'ipFamily'].includes(key)) {
|
||||
userOptions[key] = parseInt(value)
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
port_query: gameQueryPort,
|
||||
port_query_offset: gameQueryPortOffset,
|
||||
...gameOptions
|
||||
} = this.gameResolver.lookup(userOptions.type)
|
||||
const attempts = []
|
||||
|
||||
const optionsCollection = {
|
||||
...defaultOptions,
|
||||
...gameOptions,
|
||||
...userOptions
|
||||
}
|
||||
|
||||
const addAttemptWithPort = port => {
|
||||
attempts.push({
|
||||
...optionsCollection,
|
||||
port
|
||||
})
|
||||
}
|
||||
|
||||
if (userOptions.port) {
|
||||
if (!userOptions.givenPortOnly) {
|
||||
if (gameQueryPortOffset) { addAttemptWithPort(userOptions.port + gameQueryPortOffset) }
|
||||
|
||||
if (userOptions.port === gameOptions.port && gameQueryPort) { addAttemptWithPort(gameQueryPort) }
|
||||
}
|
||||
|
||||
attempts.push(optionsCollection)
|
||||
} else if (gameQueryPort) {
|
||||
addAttemptWithPort(gameQueryPort)
|
||||
} else if (gameOptions.port) {
|
||||
addAttemptWithPort(gameOptions.port + (gameQueryPortOffset || 0))
|
||||
} else {
|
||||
// Hopefully the request doesn't need a port. If it does, it'll fail when making the request.
|
||||
attempts.push(optionsCollection)
|
||||
}
|
||||
|
||||
const numRetries = userOptions.maxAttempts || gameOptions.maxAttempts || defaultOptions.maxAttempts
|
||||
|
||||
let attemptNum = 0
|
||||
const errors = []
|
||||
for (const attempt of attempts) {
|
||||
for (let retry = 0; retry < numRetries; retry++) {
|
||||
attemptNum++
|
||||
let result
|
||||
try {
|
||||
result = await this._attempt(attempt)
|
||||
} catch (e) {
|
||||
e.stack = 'Attempt #' + attemptNum + ' - Port=' + attempt.port + ' Retry=' + (retry) + ':\n' + e.stack
|
||||
errors.push(e)
|
||||
} finally {
|
||||
// Deno doesn't support unref, so we must close the socket after every connection
|
||||
// https://github.com/denoland/deno/issues/20138
|
||||
if (typeof Deno !== "undefined") {
|
||||
this.udpSocket?.socket?.close()
|
||||
delete this.udpSocket
|
||||
}
|
||||
}
|
||||
if (result) return result
|
||||
}
|
||||
}
|
||||
|
||||
const err = new Error('Failed all ' + errors.length + ' attempts')
|
||||
for (const e of errors) {
|
||||
err.stack += '\n' + e.stack
|
||||
}
|
||||
|
||||
throw err
|
||||
}
|
||||
|
||||
async _attempt (options) {
|
||||
const core = getProtocol(options.protocol)
|
||||
core.options = options
|
||||
core.udpSocket = this.udpSocket
|
||||
return await core.runOnceSafe()
|
||||
}
|
||||
}
|
||||
import GameResolver from './GameResolver.js'
|
||||
import { getProtocol } from './ProtocolResolver.js'
|
||||
import GlobalUdpSocket from './GlobalUdpSocket.js'
|
||||
|
||||
const defaultOptions = {
|
||||
socketTimeout: 2000,
|
||||
attemptTimeout: 10000,
|
||||
maxAttempts: 1,
|
||||
ipFamily: 0
|
||||
}
|
||||
|
||||
export default class QueryRunner {
|
||||
constructor (runnerOpts = {}) {
|
||||
this.udpSocket = new GlobalUdpSocket({
|
||||
port: runnerOpts.listenUdpPort
|
||||
})
|
||||
this.gameResolver = new GameResolver()
|
||||
}
|
||||
|
||||
async run (userOptions) {
|
||||
for (const key of Object.keys(userOptions)) {
|
||||
const value = userOptions[key]
|
||||
if (['port', 'ipFamily'].includes(key)) {
|
||||
userOptions[key] = parseInt(value)
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
port_query: gameQueryPort,
|
||||
port_query_offset: gameQueryPortOffset,
|
||||
...gameOptions
|
||||
} = this.gameResolver.lookup(userOptions.type)
|
||||
const attempts = []
|
||||
|
||||
const optionsCollection = {
|
||||
...defaultOptions,
|
||||
...gameOptions,
|
||||
...userOptions
|
||||
}
|
||||
|
||||
const addAttemptWithPort = port => {
|
||||
attempts.push({
|
||||
...optionsCollection,
|
||||
port
|
||||
})
|
||||
}
|
||||
|
||||
if (userOptions.port) {
|
||||
if (!userOptions.givenPortOnly) {
|
||||
if (gameQueryPortOffset) { addAttemptWithPort(userOptions.port + gameQueryPortOffset) }
|
||||
|
||||
if (userOptions.port === gameOptions.port && gameQueryPort) { addAttemptWithPort(gameQueryPort) }
|
||||
}
|
||||
|
||||
attempts.push(optionsCollection)
|
||||
} else if (gameQueryPort) {
|
||||
addAttemptWithPort(gameQueryPort)
|
||||
} else if (gameOptions.port) {
|
||||
addAttemptWithPort(gameOptions.port + (gameQueryPortOffset || 0))
|
||||
} else {
|
||||
// Hopefully the request doesn't need a port. If it does, it'll fail when making the request.
|
||||
attempts.push(optionsCollection)
|
||||
}
|
||||
|
||||
const numRetries = userOptions.maxAttempts || gameOptions.maxAttempts || defaultOptions.maxAttempts
|
||||
|
||||
let attemptNum = 0
|
||||
const errors = []
|
||||
for (const attempt of attempts) {
|
||||
for (let retry = 0; retry < numRetries; retry++) {
|
||||
attemptNum++
|
||||
let result
|
||||
try {
|
||||
result = await this._attempt(attempt)
|
||||
} catch (e) {
|
||||
e.stack = 'Attempt #' + attemptNum + ' - Port=' + attempt.port + ' Retry=' + (retry) + ':\n' + e.stack
|
||||
errors.push(e)
|
||||
} finally {
|
||||
// Deno doesn't support unref, so we must close the socket after every connection
|
||||
// https://github.com/denoland/deno/issues/20138
|
||||
if (typeof Deno !== "undefined") {
|
||||
this.udpSocket?.socket?.close()
|
||||
delete this.udpSocket
|
||||
}
|
||||
}
|
||||
if (result) return result
|
||||
}
|
||||
}
|
||||
|
||||
const err = new Error('Failed all ' + errors.length + ' attempts')
|
||||
for (const e of errors) {
|
||||
err.stack += '\n' + e.stack
|
||||
}
|
||||
|
||||
throw err
|
||||
}
|
||||
|
||||
async _attempt (options) {
|
||||
const core = getProtocol(options.protocol)
|
||||
core.options = options
|
||||
core.udpSocket = this.udpSocket
|
||||
return await core.runOnceSafe()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,32 +1,32 @@
|
|||
export class Player {
|
||||
name = ''
|
||||
raw = {}
|
||||
|
||||
constructor (data) {
|
||||
if (typeof data === 'string') {
|
||||
this.name = data
|
||||
} else {
|
||||
const { name, ...raw } = data
|
||||
if (name) this.name = name
|
||||
if (raw) this.raw = raw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Players extends Array {
|
||||
push (data) {
|
||||
super.push(new Player(data))
|
||||
}
|
||||
}
|
||||
|
||||
export class Results {
|
||||
name = ''
|
||||
map = ''
|
||||
password = false
|
||||
|
||||
raw = {}
|
||||
|
||||
maxplayers = 0
|
||||
players = new Players()
|
||||
bots = new Players()
|
||||
}
|
||||
export class Player {
|
||||
name = ''
|
||||
raw = {}
|
||||
|
||||
constructor (data) {
|
||||
if (typeof data === 'string') {
|
||||
this.name = data
|
||||
} else {
|
||||
const { name, ...raw } = data
|
||||
if (name) this.name = name
|
||||
if (raw) this.raw = raw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Players extends Array {
|
||||
push (data) {
|
||||
super.push(new Player(data))
|
||||
}
|
||||
}
|
||||
|
||||
export class Results {
|
||||
name = ''
|
||||
map = ''
|
||||
password = false
|
||||
|
||||
raw = {}
|
||||
|
||||
maxplayers = 0
|
||||
players = new Players()
|
||||
bots = new Players()
|
||||
}
|
||||
|
|
46
lib/index.js
46
lib/index.js
|
@ -1,23 +1,23 @@
|
|||
import QueryRunner from './QueryRunner.js'
|
||||
|
||||
let singleton = null
|
||||
|
||||
export default class Gamedig {
|
||||
constructor (runnerOpts) {
|
||||
this.queryRunner = new QueryRunner(runnerOpts)
|
||||
}
|
||||
|
||||
async query (userOptions) {
|
||||
return await this.queryRunner.run(userOptions)
|
||||
}
|
||||
|
||||
static getInstance () {
|
||||
if (!singleton) { singleton = new Gamedig() }
|
||||
|
||||
return singleton
|
||||
}
|
||||
|
||||
static async query (...args) {
|
||||
return await Gamedig.getInstance().query(...args)
|
||||
}
|
||||
}
|
||||
import QueryRunner from './QueryRunner.js'
|
||||
|
||||
let singleton = null
|
||||
|
||||
export default class Gamedig {
|
||||
constructor (runnerOpts) {
|
||||
this.queryRunner = new QueryRunner(runnerOpts)
|
||||
}
|
||||
|
||||
async query (userOptions) {
|
||||
return await this.queryRunner.run(userOptions)
|
||||
}
|
||||
|
||||
static getInstance () {
|
||||
if (!singleton) { singleton = new Gamedig() }
|
||||
|
||||
return singleton
|
||||
}
|
||||
|
||||
static async query (...args) {
|
||||
return await Gamedig.getInstance().query(...args)
|
||||
}
|
||||
}
|
||||
|
|
344
lib/reader.js
344
lib/reader.js
|
@ -1,172 +1,172 @@
|
|||
import Iconv from 'iconv-lite'
|
||||
import Long from 'long'
|
||||
import { Buffer } from 'node:buffer'
|
||||
import Varint from 'varint'
|
||||
|
||||
function readUInt64BE (buffer, offset) {
|
||||
const high = buffer.readUInt32BE(offset)
|
||||
const low = buffer.readUInt32BE(offset + 4)
|
||||
return new Long(low, high, true)
|
||||
}
|
||||
function readUInt64LE (buffer, offset) {
|
||||
const low = buffer.readUInt32LE(offset)
|
||||
const high = buffer.readUInt32LE(offset + 4)
|
||||
return new Long(low, high, true)
|
||||
}
|
||||
|
||||
export default class Reader {
|
||||
/**
|
||||
* @param {Core} query
|
||||
* @param {Buffer} buffer
|
||||
**/
|
||||
constructor (query, buffer) {
|
||||
this.defaultEncoding = query.options.encoding || query.encoding
|
||||
this.defaultDelimiter = query.delimiter
|
||||
this.defaultByteOrder = query.byteorder
|
||||
this.buffer = buffer
|
||||
this.i = 0
|
||||
}
|
||||
|
||||
setOffset (offset) {
|
||||
this.i = offset
|
||||
}
|
||||
|
||||
offset () {
|
||||
return this.i
|
||||
}
|
||||
|
||||
skip (i) {
|
||||
this.i += i
|
||||
}
|
||||
|
||||
pascalString (bytesForSize, adjustment = 0) {
|
||||
const length = this.uint(bytesForSize) + adjustment
|
||||
return this.string(length)
|
||||
}
|
||||
|
||||
string (arg) {
|
||||
let encoding = this.defaultEncoding
|
||||
let length = null
|
||||
let delimiter = this.defaultDelimiter
|
||||
|
||||
if (typeof arg === 'string') delimiter = arg
|
||||
else if (typeof arg === 'number') length = arg
|
||||
else if (typeof arg === 'object') {
|
||||
if ('encoding' in arg) encoding = arg.encoding
|
||||
if ('length' in arg) length = arg.length
|
||||
if ('delimiter' in arg) delimiter = arg.delimiter
|
||||
}
|
||||
|
||||
if (encoding === 'latin1') encoding = 'win1252'
|
||||
|
||||
const start = this.i
|
||||
let end = start
|
||||
if (length === null) {
|
||||
// terminated by the delimiter
|
||||
let delim = delimiter
|
||||
if (typeof delim === 'string') delim = delim.charCodeAt(0)
|
||||
while (true) {
|
||||
if (end >= this.buffer.length) {
|
||||
end = this.buffer.length
|
||||
break
|
||||
}
|
||||
if (this.buffer.readUInt8(end) === delim) break
|
||||
end++
|
||||
}
|
||||
this.i = end + 1
|
||||
} else if (length <= 0) {
|
||||
return ''
|
||||
} else {
|
||||
end = start + length
|
||||
if (end >= this.buffer.length) {
|
||||
end = this.buffer.length
|
||||
}
|
||||
this.i = end
|
||||
}
|
||||
|
||||
const slice = this.buffer.slice(start, end)
|
||||
const enc = encoding
|
||||
if (enc === 'utf8' || enc === 'ucs2' || enc === 'binary') {
|
||||
return slice.toString(enc)
|
||||
} else {
|
||||
return Iconv.decode(slice, enc)
|
||||
}
|
||||
}
|
||||
|
||||
int (bytes) {
|
||||
let r = 0
|
||||
if (this.remaining() >= bytes) {
|
||||
if (this.defaultByteOrder === 'be') {
|
||||
if (bytes === 1) r = this.buffer.readInt8(this.i)
|
||||
else if (bytes === 2) r = this.buffer.readInt16BE(this.i)
|
||||
else if (bytes === 4) r = this.buffer.readInt32BE(this.i)
|
||||
} else {
|
||||
if (bytes === 1) r = this.buffer.readInt8(this.i)
|
||||
else if (bytes === 2) r = this.buffer.readInt16LE(this.i)
|
||||
else if (bytes === 4) r = this.buffer.readInt32LE(this.i)
|
||||
}
|
||||
}
|
||||
this.i += bytes
|
||||
return r
|
||||
}
|
||||
|
||||
/** @returns {number} */
|
||||
uint (bytes) {
|
||||
let r = 0
|
||||
if (this.remaining() >= bytes) {
|
||||
if (this.defaultByteOrder === 'be') {
|
||||
if (bytes === 1) r = this.buffer.readUInt8(this.i)
|
||||
else if (bytes === 2) r = this.buffer.readUInt16BE(this.i)
|
||||
else if (bytes === 4) r = this.buffer.readUInt32BE(this.i)
|
||||
else if (bytes === 8) r = readUInt64BE(this.buffer, this.i)
|
||||
} else {
|
||||
if (bytes === 1) r = this.buffer.readUInt8(this.i)
|
||||
else if (bytes === 2) r = this.buffer.readUInt16LE(this.i)
|
||||
else if (bytes === 4) r = this.buffer.readUInt32LE(this.i)
|
||||
else if (bytes === 8) r = readUInt64LE(this.buffer, this.i)
|
||||
}
|
||||
}
|
||||
this.i += bytes
|
||||
return r
|
||||
}
|
||||
|
||||
float () {
|
||||
let r = 0
|
||||
if (this.remaining() >= 4) {
|
||||
if (this.defaultByteOrder === 'be') r = this.buffer.readFloatBE(this.i)
|
||||
else r = this.buffer.readFloatLE(this.i)
|
||||
}
|
||||
this.i += 4
|
||||
return r
|
||||
}
|
||||
|
||||
varint () {
|
||||
const out = Varint.decode(this.buffer, this.i)
|
||||
this.i += Varint.decode.bytes
|
||||
return out
|
||||
}
|
||||
|
||||
/** @returns Buffer */
|
||||
part (bytes) {
|
||||
let r
|
||||
if (this.remaining() >= bytes) {
|
||||
r = this.buffer.slice(this.i, this.i + bytes)
|
||||
} else {
|
||||
r = Buffer.from([])
|
||||
}
|
||||
this.i += bytes
|
||||
return r
|
||||
}
|
||||
|
||||
remaining () {
|
||||
return this.buffer.length - this.i
|
||||
}
|
||||
|
||||
rest () {
|
||||
return this.buffer.slice(this.i)
|
||||
}
|
||||
|
||||
done () {
|
||||
return this.i >= this.buffer.length
|
||||
}
|
||||
}
|
||||
import Iconv from 'iconv-lite'
|
||||
import Long from 'long'
|
||||
import { Buffer } from 'node:buffer'
|
||||
import Varint from 'varint'
|
||||
|
||||
function readUInt64BE (buffer, offset) {
|
||||
const high = buffer.readUInt32BE(offset)
|
||||
const low = buffer.readUInt32BE(offset + 4)
|
||||
return new Long(low, high, true)
|
||||
}
|
||||
function readUInt64LE (buffer, offset) {
|
||||
const low = buffer.readUInt32LE(offset)
|
||||
const high = buffer.readUInt32LE(offset + 4)
|
||||
return new Long(low, high, true)
|
||||
}
|
||||
|
||||
export default class Reader {
|
||||
/**
|
||||
* @param {Core} query
|
||||
* @param {Buffer} buffer
|
||||
**/
|
||||
constructor (query, buffer) {
|
||||
this.defaultEncoding = query.options.encoding || query.encoding
|
||||
this.defaultDelimiter = query.delimiter
|
||||
this.defaultByteOrder = query.byteorder
|
||||
this.buffer = buffer
|
||||
this.i = 0
|
||||
}
|
||||
|
||||
setOffset (offset) {
|
||||
this.i = offset
|
||||
}
|
||||
|
||||
offset () {
|
||||
return this.i
|
||||
}
|
||||
|
||||
skip (i) {
|
||||
this.i += i
|
||||
}
|
||||
|
||||
pascalString (bytesForSize, adjustment = 0) {
|
||||
const length = this.uint(bytesForSize) + adjustment
|
||||
return this.string(length)
|
||||
}
|
||||
|
||||
string (arg) {
|
||||
let encoding = this.defaultEncoding
|
||||
let length = null
|
||||
let delimiter = this.defaultDelimiter
|
||||
|
||||
if (typeof arg === 'string') delimiter = arg
|
||||
else if (typeof arg === 'number') length = arg
|
||||
else if (typeof arg === 'object') {
|
||||
if ('encoding' in arg) encoding = arg.encoding
|
||||
if ('length' in arg) length = arg.length
|
||||
if ('delimiter' in arg) delimiter = arg.delimiter
|
||||
}
|
||||
|
||||
if (encoding === 'latin1') encoding = 'win1252'
|
||||
|
||||
const start = this.i
|
||||
let end = start
|
||||
if (length === null) {
|
||||
// terminated by the delimiter
|
||||
let delim = delimiter
|
||||
if (typeof delim === 'string') delim = delim.charCodeAt(0)
|
||||
while (true) {
|
||||
if (end >= this.buffer.length) {
|
||||
end = this.buffer.length
|
||||
break
|
||||
}
|
||||
if (this.buffer.readUInt8(end) === delim) break
|
||||
end++
|
||||
}
|
||||
this.i = end + 1
|
||||
} else if (length <= 0) {
|
||||
return ''
|
||||
} else {
|
||||
end = start + length
|
||||
if (end >= this.buffer.length) {
|
||||
end = this.buffer.length
|
||||
}
|
||||
this.i = end
|
||||
}
|
||||
|
||||
const slice = this.buffer.slice(start, end)
|
||||
const enc = encoding
|
||||
if (enc === 'utf8' || enc === 'ucs2' || enc === 'binary') {
|
||||
return slice.toString(enc)
|
||||
} else {
|
||||
return Iconv.decode(slice, enc)
|
||||
}
|
||||
}
|
||||
|
||||
int (bytes) {
|
||||
let r = 0
|
||||
if (this.remaining() >= bytes) {
|
||||
if (this.defaultByteOrder === 'be') {
|
||||
if (bytes === 1) r = this.buffer.readInt8(this.i)
|
||||
else if (bytes === 2) r = this.buffer.readInt16BE(this.i)
|
||||
else if (bytes === 4) r = this.buffer.readInt32BE(this.i)
|
||||
} else {
|
||||
if (bytes === 1) r = this.buffer.readInt8(this.i)
|
||||
else if (bytes === 2) r = this.buffer.readInt16LE(this.i)
|
||||
else if (bytes === 4) r = this.buffer.readInt32LE(this.i)
|
||||
}
|
||||
}
|
||||
this.i += bytes
|
||||
return r
|
||||
}
|
||||
|
||||
/** @returns {number} */
|
||||
uint (bytes) {
|
||||
let r = 0
|
||||
if (this.remaining() >= bytes) {
|
||||
if (this.defaultByteOrder === 'be') {
|
||||
if (bytes === 1) r = this.buffer.readUInt8(this.i)
|
||||
else if (bytes === 2) r = this.buffer.readUInt16BE(this.i)
|
||||
else if (bytes === 4) r = this.buffer.readUInt32BE(this.i)
|
||||
else if (bytes === 8) r = readUInt64BE(this.buffer, this.i)
|
||||
} else {
|
||||
if (bytes === 1) r = this.buffer.readUInt8(this.i)
|
||||
else if (bytes === 2) r = this.buffer.readUInt16LE(this.i)
|
||||
else if (bytes === 4) r = this.buffer.readUInt32LE(this.i)
|
||||
else if (bytes === 8) r = readUInt64LE(this.buffer, this.i)
|
||||
}
|
||||
}
|
||||
this.i += bytes
|
||||
return r
|
||||
}
|
||||
|
||||
float () {
|
||||
let r = 0
|
||||
if (this.remaining() >= 4) {
|
||||
if (this.defaultByteOrder === 'be') r = this.buffer.readFloatBE(this.i)
|
||||
else r = this.buffer.readFloatLE(this.i)
|
||||
}
|
||||
this.i += 4
|
||||
return r
|
||||
}
|
||||
|
||||
varint () {
|
||||
const out = Varint.decode(this.buffer, this.i)
|
||||
this.i += Varint.decode.bytes
|
||||
return out
|
||||
}
|
||||
|
||||
/** @returns Buffer */
|
||||
part (bytes) {
|
||||
let r
|
||||
if (this.remaining() >= bytes) {
|
||||
r = this.buffer.slice(this.i, this.i + bytes)
|
||||
} else {
|
||||
r = Buffer.from([])
|
||||
}
|
||||
this.i += bytes
|
||||
return r
|
||||
}
|
||||
|
||||
remaining () {
|
||||
return this.buffer.length - this.i
|
||||
}
|
||||
|
||||
rest () {
|
||||
return this.buffer.slice(this.i)
|
||||
}
|
||||
|
||||
done () {
|
||||
return this.i >= this.buffer.length
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,65 +1,65 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class armagetron extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'latin1'
|
||||
this.byteorder = 'be'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
const b = Buffer.from([0, 0x35, 0, 0, 0, 0, 0, 0x11])
|
||||
|
||||
const buffer = await this.udpSend(b, b => b)
|
||||
const reader = this.reader(buffer)
|
||||
|
||||
reader.skip(6)
|
||||
|
||||
state.gamePort = this.readUInt(reader)
|
||||
state.raw.hostname = this.readString(reader)
|
||||
state.name = this.stripColorCodes(this.readString(reader))
|
||||
state.numplayers = this.readUInt(reader)
|
||||
state.raw.versionmin = this.readUInt(reader)
|
||||
state.raw.versionmax = this.readUInt(reader)
|
||||
state.raw.version = this.readString(reader)
|
||||
state.maxplayers = this.readUInt(reader)
|
||||
|
||||
const players = this.readString(reader)
|
||||
const list = players.split('\n')
|
||||
for (const name of list) {
|
||||
if (!name) continue
|
||||
state.players.push({
|
||||
name: this.stripColorCodes(name)
|
||||
})
|
||||
}
|
||||
|
||||
state.raw.options = this.stripColorCodes(this.readString(reader))
|
||||
state.raw.uri = this.readString(reader)
|
||||
state.raw.globalids = this.readString(reader)
|
||||
}
|
||||
|
||||
readUInt (reader) {
|
||||
const a = reader.uint(2)
|
||||
const b = reader.uint(2)
|
||||
return (b << 16) + a
|
||||
}
|
||||
|
||||
readString (reader) {
|
||||
const len = reader.uint(2)
|
||||
if (!len) return ''
|
||||
|
||||
let out = ''
|
||||
for (let i = 0; i < len; i += 2) {
|
||||
const hi = reader.uint(1)
|
||||
const lo = reader.uint(1)
|
||||
if (i + 1 < len) out += String.fromCharCode(lo)
|
||||
if (i + 2 < len) out += String.fromCharCode(hi)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
stripColorCodes (str) {
|
||||
return str.replace(/0x[0-9a-f]{6}/g, '')
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class armagetron extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'latin1'
|
||||
this.byteorder = 'be'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
const b = Buffer.from([0, 0x35, 0, 0, 0, 0, 0, 0x11])
|
||||
|
||||
const buffer = await this.udpSend(b, b => b)
|
||||
const reader = this.reader(buffer)
|
||||
|
||||
reader.skip(6)
|
||||
|
||||
state.gamePort = this.readUInt(reader)
|
||||
state.raw.hostname = this.readString(reader)
|
||||
state.name = this.stripColorCodes(this.readString(reader))
|
||||
state.numplayers = this.readUInt(reader)
|
||||
state.raw.versionmin = this.readUInt(reader)
|
||||
state.raw.versionmax = this.readUInt(reader)
|
||||
state.raw.version = this.readString(reader)
|
||||
state.maxplayers = this.readUInt(reader)
|
||||
|
||||
const players = this.readString(reader)
|
||||
const list = players.split('\n')
|
||||
for (const name of list) {
|
||||
if (!name) continue
|
||||
state.players.push({
|
||||
name: this.stripColorCodes(name)
|
||||
})
|
||||
}
|
||||
|
||||
state.raw.options = this.stripColorCodes(this.readString(reader))
|
||||
state.raw.uri = this.readString(reader)
|
||||
state.raw.globalids = this.readString(reader)
|
||||
}
|
||||
|
||||
readUInt (reader) {
|
||||
const a = reader.uint(2)
|
||||
const b = reader.uint(2)
|
||||
return (b << 16) + a
|
||||
}
|
||||
|
||||
readString (reader) {
|
||||
const len = reader.uint(2)
|
||||
if (!len) return ''
|
||||
|
||||
let out = ''
|
||||
for (let i = 0; i < len; i += 2) {
|
||||
const hi = reader.uint(1)
|
||||
const lo = reader.uint(1)
|
||||
if (i + 1 < len) out += String.fromCharCode(lo)
|
||||
if (i + 2 < len) out += String.fromCharCode(hi)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
stripColorCodes (str) {
|
||||
return str.replace(/0x[0-9a-f]{6}/g, '')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,45 +1,45 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class ase extends Core {
|
||||
async run (state) {
|
||||
const buffer = await this.udpSend('s', (buffer) => {
|
||||
const reader = this.reader(buffer)
|
||||
const header = reader.string(4)
|
||||
if (header === 'EYE1') return reader.rest()
|
||||
})
|
||||
|
||||
const reader = this.reader(buffer)
|
||||
state.raw.gamename = this.readString(reader)
|
||||
state.gamePort = parseInt(this.readString(reader))
|
||||
state.name = this.readString(reader)
|
||||
state.raw.gametype = this.readString(reader)
|
||||
state.map = this.readString(reader)
|
||||
state.raw.version = this.readString(reader)
|
||||
state.password = this.readString(reader) === '1'
|
||||
state.numplayers = parseInt(this.readString(reader))
|
||||
state.maxplayers = parseInt(this.readString(reader))
|
||||
|
||||
while (!reader.done()) {
|
||||
const key = this.readString(reader)
|
||||
if (!key) break
|
||||
const value = this.readString(reader)
|
||||
state.raw[key] = value
|
||||
}
|
||||
|
||||
while (!reader.done()) {
|
||||
const flags = reader.uint(1)
|
||||
const player = {}
|
||||
if (flags & 1) player.name = this.readString(reader)
|
||||
if (flags & 2) player.team = this.readString(reader)
|
||||
if (flags & 4) player.skin = this.readString(reader)
|
||||
if (flags & 8) player.score = parseInt(this.readString(reader))
|
||||
if (flags & 16) player.ping = parseInt(this.readString(reader))
|
||||
if (flags & 32) player.time = parseInt(this.readString(reader))
|
||||
state.players.push(player)
|
||||
}
|
||||
}
|
||||
|
||||
readString (reader) {
|
||||
return reader.pascalString(1, -1)
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class ase extends Core {
|
||||
async run (state) {
|
||||
const buffer = await this.udpSend('s', (buffer) => {
|
||||
const reader = this.reader(buffer)
|
||||
const header = reader.string(4)
|
||||
if (header === 'EYE1') return reader.rest()
|
||||
})
|
||||
|
||||
const reader = this.reader(buffer)
|
||||
state.raw.gamename = this.readString(reader)
|
||||
state.gamePort = parseInt(this.readString(reader))
|
||||
state.name = this.readString(reader)
|
||||
state.raw.gametype = this.readString(reader)
|
||||
state.map = this.readString(reader)
|
||||
state.raw.version = this.readString(reader)
|
||||
state.password = this.readString(reader) === '1'
|
||||
state.numplayers = parseInt(this.readString(reader))
|
||||
state.maxplayers = parseInt(this.readString(reader))
|
||||
|
||||
while (!reader.done()) {
|
||||
const key = this.readString(reader)
|
||||
if (!key) break
|
||||
const value = this.readString(reader)
|
||||
state.raw[key] = value
|
||||
}
|
||||
|
||||
while (!reader.done()) {
|
||||
const flags = reader.uint(1)
|
||||
const player = {}
|
||||
if (flags & 1) player.name = this.readString(reader)
|
||||
if (flags & 2) player.team = this.readString(reader)
|
||||
if (flags & 4) player.skin = this.readString(reader)
|
||||
if (flags & 8) player.score = parseInt(this.readString(reader))
|
||||
if (flags & 16) player.ping = parseInt(this.readString(reader))
|
||||
if (flags & 32) player.time = parseInt(this.readString(reader))
|
||||
state.players.push(player)
|
||||
}
|
||||
}
|
||||
|
||||
readString (reader) {
|
||||
return reader.pascalString(1, -1)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,40 +1,40 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class assettocorsa extends Core {
|
||||
async run (state) {
|
||||
const serverInfo = await this.request({
|
||||
url: `http://${this.options.address}:${this.options.port}/INFO`,
|
||||
responseType: 'json'
|
||||
})
|
||||
const carInfo = await this.request({
|
||||
url: `http://${this.options.address}:${this.options.port}/JSON|${parseInt(Math.random() * 999999999999999, 10)}`,
|
||||
responseType: 'json'
|
||||
})
|
||||
|
||||
if (!serverInfo || !carInfo || !carInfo.Cars) {
|
||||
throw new Error('Query not successful')
|
||||
}
|
||||
|
||||
state.maxplayers = serverInfo.maxclients
|
||||
state.name = serverInfo.name
|
||||
state.map = serverInfo.track
|
||||
state.password = serverInfo.pass
|
||||
state.gamePort = serverInfo.port
|
||||
state.raw.carInfo = carInfo.Cars
|
||||
state.raw.serverInfo = serverInfo
|
||||
|
||||
for (const car of carInfo.Cars) {
|
||||
if (car.IsConnected) {
|
||||
state.players.push({
|
||||
name: car.DriverName,
|
||||
car: car.Model,
|
||||
skin: car.Skin,
|
||||
nation: car.DriverNation,
|
||||
team: car.DriverTeam
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
state.numplayers = carInfo.Cars.length
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class assettocorsa extends Core {
|
||||
async run (state) {
|
||||
const serverInfo = await this.request({
|
||||
url: `http://${this.options.address}:${this.options.port}/INFO`,
|
||||
responseType: 'json'
|
||||
})
|
||||
const carInfo = await this.request({
|
||||
url: `http://${this.options.address}:${this.options.port}/JSON|${parseInt(Math.random() * 999999999999999, 10)}`,
|
||||
responseType: 'json'
|
||||
})
|
||||
|
||||
if (!serverInfo || !carInfo || !carInfo.Cars) {
|
||||
throw new Error('Query not successful')
|
||||
}
|
||||
|
||||
state.maxplayers = serverInfo.maxclients
|
||||
state.name = serverInfo.name
|
||||
state.map = serverInfo.track
|
||||
state.password = serverInfo.pass
|
||||
state.gamePort = serverInfo.port
|
||||
state.raw.carInfo = carInfo.Cars
|
||||
state.raw.serverInfo = serverInfo
|
||||
|
||||
for (const car of carInfo.Cars) {
|
||||
if (car.IsConnected) {
|
||||
state.players.push({
|
||||
name: car.DriverName,
|
||||
car: car.Model,
|
||||
skin: car.Skin,
|
||||
nation: car.DriverNation,
|
||||
team: car.DriverTeam
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
state.numplayers = carInfo.Cars.length
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,162 +1,162 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class battlefield extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'latin1'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
await this.withTcp(async socket => {
|
||||
{
|
||||
const data = await this.query(socket, ['serverInfo'])
|
||||
state.name = data.shift()
|
||||
state.numplayers = parseInt(data.shift())
|
||||
state.maxplayers = parseInt(data.shift())
|
||||
state.raw.gametype = data.shift()
|
||||
state.map = data.shift()
|
||||
state.raw.roundsplayed = parseInt(data.shift())
|
||||
state.raw.roundstotal = parseInt(data.shift())
|
||||
|
||||
const teamCount = data.shift()
|
||||
state.raw.teams = []
|
||||
for (let i = 0; i < teamCount; i++) {
|
||||
const tickets = parseFloat(data.shift())
|
||||
state.raw.teams.push({
|
||||
tickets
|
||||
})
|
||||
}
|
||||
|
||||
state.raw.targetscore = parseInt(data.shift())
|
||||
state.raw.status = data.shift()
|
||||
|
||||
// Seems like the fields end at random places beyond this point
|
||||
// depending on the server version
|
||||
|
||||
if (data.length) state.raw.ranked = (data.shift() === 'true')
|
||||
if (data.length) state.raw.punkbuster = (data.shift() === 'true')
|
||||
if (data.length) state.password = (data.shift() === 'true')
|
||||
if (data.length) state.raw.uptime = parseInt(data.shift())
|
||||
if (data.length) state.raw.roundtime = parseInt(data.shift())
|
||||
|
||||
const isBadCompany2 = data[0] === 'BC2'
|
||||
if (isBadCompany2) {
|
||||
if (data.length) data.shift()
|
||||
if (data.length) data.shift()
|
||||
}
|
||||
if (data.length) {
|
||||
state.raw.ip = data.shift()
|
||||
const split = state.raw.ip.split(':')
|
||||
state.gameHost = split[0]
|
||||
state.gamePort = split[1]
|
||||
} else {
|
||||
// best guess if the server doesn't tell us what the server port is
|
||||
// these are just the default game ports for different default query ports
|
||||
if (this.options.port === 48888) state.gamePort = 7673
|
||||
if (this.options.port === 22000) state.gamePort = 25200
|
||||
}
|
||||
if (data.length) state.raw.punkbusterversion = data.shift()
|
||||
if (data.length) state.raw.joinqueue = (data.shift() === 'true')
|
||||
if (data.length) state.raw.region = data.shift()
|
||||
if (data.length) state.raw.pingsite = data.shift()
|
||||
if (data.length) state.raw.country = data.shift()
|
||||
if (data.length) state.raw.quickmatch = (data.shift() === 'true')
|
||||
}
|
||||
|
||||
{
|
||||
const data = await this.query(socket, ['version'])
|
||||
data.shift()
|
||||
state.raw.version = data.shift()
|
||||
}
|
||||
|
||||
{
|
||||
const data = await this.query(socket, ['listPlayers', 'all'])
|
||||
const fieldCount = parseInt(data.shift())
|
||||
const fields = []
|
||||
for (let i = 0; i < fieldCount; i++) {
|
||||
fields.push(data.shift())
|
||||
}
|
||||
const numplayers = data.shift()
|
||||
for (let i = 0; i < numplayers; i++) {
|
||||
const player = {}
|
||||
for (let key of fields) {
|
||||
let value = data.shift()
|
||||
|
||||
if (key === 'teamId') key = 'team'
|
||||
else if (key === 'squadId') key = 'squad'
|
||||
|
||||
if (
|
||||
key === 'kills' ||
|
||||
key === 'deaths' ||
|
||||
key === 'score' ||
|
||||
key === 'rank' ||
|
||||
key === 'team' ||
|
||||
key === 'squad' ||
|
||||
key === 'ping' ||
|
||||
key === 'type'
|
||||
) {
|
||||
value = parseInt(value)
|
||||
}
|
||||
|
||||
player[key] = value
|
||||
}
|
||||
state.players.push(player)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async query (socket, params) {
|
||||
const outPacket = this.buildPacket(params)
|
||||
return await this.tcpSend(socket, outPacket, (data) => {
|
||||
const decoded = this.decodePacket(data)
|
||||
if (decoded) {
|
||||
this.logger.debug(decoded)
|
||||
if (decoded.shift() !== 'OK') throw new Error('Missing OK')
|
||||
return decoded
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
buildPacket (params) {
|
||||
const paramBuffers = []
|
||||
for (const param of params) {
|
||||
paramBuffers.push(Buffer.from(param, 'utf8'))
|
||||
}
|
||||
|
||||
let totalLength = 12
|
||||
for (const paramBuffer of paramBuffers) {
|
||||
totalLength += paramBuffer.length + 1 + 4
|
||||
}
|
||||
|
||||
const b = Buffer.alloc(totalLength)
|
||||
b.writeUInt32LE(0, 0)
|
||||
b.writeUInt32LE(totalLength, 4)
|
||||
b.writeUInt32LE(params.length, 8)
|
||||
let offset = 12
|
||||
for (const paramBuffer of paramBuffers) {
|
||||
b.writeUInt32LE(paramBuffer.length, offset); offset += 4
|
||||
paramBuffer.copy(b, offset); offset += paramBuffer.length
|
||||
b.writeUInt8(0, offset); offset += 1
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
decodePacket (buffer) {
|
||||
if (buffer.length < 8) return false
|
||||
const reader = this.reader(buffer)
|
||||
reader.uint(4) // header
|
||||
const totalLength = reader.uint(4)
|
||||
if (buffer.length < totalLength) return false
|
||||
this.logger.debug('Expected ' + totalLength + ' bytes, have ' + buffer.length)
|
||||
|
||||
const paramCount = reader.uint(4)
|
||||
const params = []
|
||||
for (let i = 0; i < paramCount; i++) {
|
||||
params.push(reader.pascalString(4))
|
||||
reader.uint(1) // strNull
|
||||
}
|
||||
return params
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class battlefield extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'latin1'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
await this.withTcp(async socket => {
|
||||
{
|
||||
const data = await this.query(socket, ['serverInfo'])
|
||||
state.name = data.shift()
|
||||
state.numplayers = parseInt(data.shift())
|
||||
state.maxplayers = parseInt(data.shift())
|
||||
state.raw.gametype = data.shift()
|
||||
state.map = data.shift()
|
||||
state.raw.roundsplayed = parseInt(data.shift())
|
||||
state.raw.roundstotal = parseInt(data.shift())
|
||||
|
||||
const teamCount = data.shift()
|
||||
state.raw.teams = []
|
||||
for (let i = 0; i < teamCount; i++) {
|
||||
const tickets = parseFloat(data.shift())
|
||||
state.raw.teams.push({
|
||||
tickets
|
||||
})
|
||||
}
|
||||
|
||||
state.raw.targetscore = parseInt(data.shift())
|
||||
state.raw.status = data.shift()
|
||||
|
||||
// Seems like the fields end at random places beyond this point
|
||||
// depending on the server version
|
||||
|
||||
if (data.length) state.raw.ranked = (data.shift() === 'true')
|
||||
if (data.length) state.raw.punkbuster = (data.shift() === 'true')
|
||||
if (data.length) state.password = (data.shift() === 'true')
|
||||
if (data.length) state.raw.uptime = parseInt(data.shift())
|
||||
if (data.length) state.raw.roundtime = parseInt(data.shift())
|
||||
|
||||
const isBadCompany2 = data[0] === 'BC2'
|
||||
if (isBadCompany2) {
|
||||
if (data.length) data.shift()
|
||||
if (data.length) data.shift()
|
||||
}
|
||||
if (data.length) {
|
||||
state.raw.ip = data.shift()
|
||||
const split = state.raw.ip.split(':')
|
||||
state.gameHost = split[0]
|
||||
state.gamePort = split[1]
|
||||
} else {
|
||||
// best guess if the server doesn't tell us what the server port is
|
||||
// these are just the default game ports for different default query ports
|
||||
if (this.options.port === 48888) state.gamePort = 7673
|
||||
if (this.options.port === 22000) state.gamePort = 25200
|
||||
}
|
||||
if (data.length) state.raw.punkbusterversion = data.shift()
|
||||
if (data.length) state.raw.joinqueue = (data.shift() === 'true')
|
||||
if (data.length) state.raw.region = data.shift()
|
||||
if (data.length) state.raw.pingsite = data.shift()
|
||||
if (data.length) state.raw.country = data.shift()
|
||||
if (data.length) state.raw.quickmatch = (data.shift() === 'true')
|
||||
}
|
||||
|
||||
{
|
||||
const data = await this.query(socket, ['version'])
|
||||
data.shift()
|
||||
state.raw.version = data.shift()
|
||||
}
|
||||
|
||||
{
|
||||
const data = await this.query(socket, ['listPlayers', 'all'])
|
||||
const fieldCount = parseInt(data.shift())
|
||||
const fields = []
|
||||
for (let i = 0; i < fieldCount; i++) {
|
||||
fields.push(data.shift())
|
||||
}
|
||||
const numplayers = data.shift()
|
||||
for (let i = 0; i < numplayers; i++) {
|
||||
const player = {}
|
||||
for (let key of fields) {
|
||||
let value = data.shift()
|
||||
|
||||
if (key === 'teamId') key = 'team'
|
||||
else if (key === 'squadId') key = 'squad'
|
||||
|
||||
if (
|
||||
key === 'kills' ||
|
||||
key === 'deaths' ||
|
||||
key === 'score' ||
|
||||
key === 'rank' ||
|
||||
key === 'team' ||
|
||||
key === 'squad' ||
|
||||
key === 'ping' ||
|
||||
key === 'type'
|
||||
) {
|
||||
value = parseInt(value)
|
||||
}
|
||||
|
||||
player[key] = value
|
||||
}
|
||||
state.players.push(player)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async query (socket, params) {
|
||||
const outPacket = this.buildPacket(params)
|
||||
return await this.tcpSend(socket, outPacket, (data) => {
|
||||
const decoded = this.decodePacket(data)
|
||||
if (decoded) {
|
||||
this.logger.debug(decoded)
|
||||
if (decoded.shift() !== 'OK') throw new Error('Missing OK')
|
||||
return decoded
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
buildPacket (params) {
|
||||
const paramBuffers = []
|
||||
for (const param of params) {
|
||||
paramBuffers.push(Buffer.from(param, 'utf8'))
|
||||
}
|
||||
|
||||
let totalLength = 12
|
||||
for (const paramBuffer of paramBuffers) {
|
||||
totalLength += paramBuffer.length + 1 + 4
|
||||
}
|
||||
|
||||
const b = Buffer.alloc(totalLength)
|
||||
b.writeUInt32LE(0, 0)
|
||||
b.writeUInt32LE(totalLength, 4)
|
||||
b.writeUInt32LE(params.length, 8)
|
||||
let offset = 12
|
||||
for (const paramBuffer of paramBuffers) {
|
||||
b.writeUInt32LE(paramBuffer.length, offset); offset += 4
|
||||
paramBuffer.copy(b, offset); offset += paramBuffer.length
|
||||
b.writeUInt8(0, offset); offset += 1
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
decodePacket (buffer) {
|
||||
if (buffer.length < 8) return false
|
||||
const reader = this.reader(buffer)
|
||||
reader.uint(4) // header
|
||||
const totalLength = reader.uint(4)
|
||||
if (buffer.length < totalLength) return false
|
||||
this.logger.debug('Expected ' + totalLength + ' bytes, have ' + buffer.length)
|
||||
|
||||
const paramCount = reader.uint(4)
|
||||
const params = []
|
||||
for (let i = 0; i < paramCount; i++) {
|
||||
params.push(reader.pascalString(4))
|
||||
reader.uint(1) // strNull
|
||||
}
|
||||
return params
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,55 +1,55 @@
|
|||
import Core from './core.js'
|
||||
import * as cheerio from 'cheerio'
|
||||
|
||||
export default class buildandshoot extends Core {
|
||||
async run (state) {
|
||||
const body = await this.request({
|
||||
url: 'http://' + this.options.address + ':' + this.options.port + '/'
|
||||
})
|
||||
|
||||
let m
|
||||
|
||||
m = body.match(/status server for (.*?)\.?[\r\n]/)
|
||||
if (m) state.name = m[1]
|
||||
|
||||
m = body.match(/Current uptime: (\d+)/)
|
||||
if (m) state.raw.uptime = m[1]
|
||||
|
||||
m = body.match(/currently running (.*?) by /)
|
||||
if (m) state.map = m[1]
|
||||
|
||||
m = body.match(/Current players: (\d+)\/(\d+)/)
|
||||
if (m) {
|
||||
state.numplayers = parseInt(m[1])
|
||||
state.maxplayers = m[2]
|
||||
}
|
||||
|
||||
m = body.match(/aos:\/\/[0-9]+:[0-9]+/)
|
||||
if (m) {
|
||||
state.connect = m[0]
|
||||
}
|
||||
|
||||
const $ = cheerio.load(body)
|
||||
$('#playerlist tbody tr').each((i, tr) => {
|
||||
if (!$(tr).find('td').first().attr('colspan')) {
|
||||
state.players.push({
|
||||
name: $(tr).find('td').eq(2).text(),
|
||||
ping: $(tr).find('td').eq(3).text().trim(),
|
||||
team: $(tr).find('td').eq(4).text().toLowerCase(),
|
||||
score: parseInt($(tr).find('td').eq(5).text())
|
||||
})
|
||||
}
|
||||
})
|
||||
/*
|
||||
var m = this.options.address.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/);
|
||||
if(m) {
|
||||
var o1 = parseInt(m[1]);
|
||||
var o2 = parseInt(m[2]);
|
||||
var o3 = parseInt(m[3]);
|
||||
var o4 = parseInt(m[4]);
|
||||
var addr = o1+(o2<<8)+(o3<<16)+(o4<<24);
|
||||
state.raw.url = 'aos://'+addr;
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
import * as cheerio from 'cheerio'
|
||||
|
||||
export default class buildandshoot extends Core {
|
||||
async run (state) {
|
||||
const body = await this.request({
|
||||
url: 'http://' + this.options.address + ':' + this.options.port + '/'
|
||||
})
|
||||
|
||||
let m
|
||||
|
||||
m = body.match(/status server for (.*?)\.?[\r\n]/)
|
||||
if (m) state.name = m[1]
|
||||
|
||||
m = body.match(/Current uptime: (\d+)/)
|
||||
if (m) state.raw.uptime = m[1]
|
||||
|
||||
m = body.match(/currently running (.*?) by /)
|
||||
if (m) state.map = m[1]
|
||||
|
||||
m = body.match(/Current players: (\d+)\/(\d+)/)
|
||||
if (m) {
|
||||
state.numplayers = parseInt(m[1])
|
||||
state.maxplayers = m[2]
|
||||
}
|
||||
|
||||
m = body.match(/aos:\/\/[0-9]+:[0-9]+/)
|
||||
if (m) {
|
||||
state.connect = m[0]
|
||||
}
|
||||
|
||||
const $ = cheerio.load(body)
|
||||
$('#playerlist tbody tr').each((i, tr) => {
|
||||
if (!$(tr).find('td').first().attr('colspan')) {
|
||||
state.players.push({
|
||||
name: $(tr).find('td').eq(2).text(),
|
||||
ping: $(tr).find('td').eq(3).text().trim(),
|
||||
team: $(tr).find('td').eq(4).text().toLowerCase(),
|
||||
score: parseInt($(tr).find('td').eq(5).text())
|
||||
})
|
||||
}
|
||||
})
|
||||
/*
|
||||
var m = this.options.address.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/);
|
||||
if(m) {
|
||||
var o1 = parseInt(m[1]);
|
||||
var o2 = parseInt(m[2]);
|
||||
var o3 = parseInt(m[3]);
|
||||
var o4 = parseInt(m[4]);
|
||||
var addr = o1+(o2<<8)+(o3<<16)+(o4<<24);
|
||||
state.raw.url = 'aos://'+addr;
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,349 +1,349 @@
|
|||
import { EventEmitter } from 'node:events'
|
||||
import * as net from 'node:net'
|
||||
import got from 'got'
|
||||
import Reader from '../lib/reader.js'
|
||||
import { debugDump } from '../lib/HexUtil.js'
|
||||
import Logger from '../lib/Logger.js'
|
||||
import DnsResolver from '../lib/DnsResolver.js'
|
||||
import { Results } from '../lib/Results.js'
|
||||
import Promises from '../lib/Promises.js'
|
||||
|
||||
let uid = 0
|
||||
|
||||
export default class Core extends EventEmitter {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'utf8'
|
||||
this.byteorder = 'le'
|
||||
this.delimiter = '\0'
|
||||
this.srvRecord = null
|
||||
this.abortedPromise = null
|
||||
this.logger = new Logger()
|
||||
this.dnsResolver = new DnsResolver(this.logger)
|
||||
|
||||
// Sent to us by QueryRunner
|
||||
this.options = null
|
||||
/** @type GlobalUdpSocket */
|
||||
this.udpSocket = null
|
||||
this.shortestRTT = 0
|
||||
this.usedTcp = false
|
||||
}
|
||||
|
||||
// Runs a single attempt with a timeout and cleans up afterward
|
||||
async runOnceSafe () {
|
||||
if (this.options.debug) {
|
||||
this.logger.debugEnabled = true
|
||||
}
|
||||
this.logger.prefix = 'Q#' + (uid++)
|
||||
|
||||
this.logger.debug('Starting')
|
||||
this.logger.debug('Protocol: ' + this.constructor.name)
|
||||
this.logger.debug('Options:', this.options)
|
||||
|
||||
let abortCall = null
|
||||
this.abortedPromise = new Promise((resolve, reject) => {
|
||||
abortCall = () => reject(new Error('Query is finished -- cancelling outstanding promises'))
|
||||
}).catch(() => {
|
||||
// Make sure that if this promise isn't attached to, it doesn't throw a unhandled promise rejection
|
||||
})
|
||||
|
||||
let timeout
|
||||
try {
|
||||
const promise = this.runOnce()
|
||||
timeout = Promises.createTimeout(this.options.attemptTimeout, 'Attempt')
|
||||
const result = await Promise.race([promise, timeout])
|
||||
this.logger.debug('Query was successful')
|
||||
return result
|
||||
} catch (e) {
|
||||
this.logger.debug('Query failed with error', e)
|
||||
throw e
|
||||
} finally {
|
||||
timeout && timeout.cancel()
|
||||
try {
|
||||
abortCall()
|
||||
} catch (e) {
|
||||
this.logger.debug('Error during abort cleanup: ' + e.stack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async runOnce () {
|
||||
const options = this.options
|
||||
if (('host' in options) && !('address' in options)) {
|
||||
const resolved = await this.dnsResolver.resolve(options.host, options.ipFamily, this.srvRecord)
|
||||
options.address = resolved.address
|
||||
if (resolved.port) options.port = resolved.port
|
||||
}
|
||||
|
||||
const state = new Results()
|
||||
|
||||
await this.run(state)
|
||||
|
||||
// because lots of servers prefix with spaces to try to appear first
|
||||
state.name = (state.name || '').trim()
|
||||
|
||||
if (!('connect' in state)) {
|
||||
state.connect = '' +
|
||||
(state.gameHost || this.options.host || this.options.address) +
|
||||
':' +
|
||||
(state.gamePort || this.options.port)
|
||||
}
|
||||
state.ping = this.shortestRTT
|
||||
delete state.gameHost
|
||||
delete state.gamePort
|
||||
|
||||
this.logger.debug(log => {
|
||||
log('Size of players array: ' + state.players.length)
|
||||
log('Size of bots array: ' + state.bots.length)
|
||||
})
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
async run (/** Results */ state) {}
|
||||
|
||||
/** Param can be a time in ms, or a promise (which will be timed) */
|
||||
registerRtt (param) {
|
||||
if (param.then) {
|
||||
const start = Date.now()
|
||||
param.then(() => {
|
||||
const end = Date.now()
|
||||
const rtt = end - start
|
||||
this.registerRtt(rtt)
|
||||
}).catch(() => {})
|
||||
} else {
|
||||
this.logger.debug('Registered RTT: ' + param + 'ms')
|
||||
if (this.shortestRTT === 0 || param < this.shortestRTT) {
|
||||
this.shortestRTT = param
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// utils
|
||||
/** @returns {Reader} */
|
||||
reader (buffer) {
|
||||
return new Reader(this, buffer)
|
||||
}
|
||||
|
||||
translate (obj, trans) {
|
||||
for (const from of Object.keys(trans)) {
|
||||
const to = trans[from]
|
||||
if (from in obj) {
|
||||
if (to) obj[to] = obj[from]
|
||||
delete obj[from]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trueTest (str) {
|
||||
if (typeof str === 'boolean') return str
|
||||
if (typeof str === 'number') return str !== 0
|
||||
if (typeof str === 'string') {
|
||||
if (str.toLowerCase() === 'true') return true
|
||||
if (str.toLowerCase() === 'yes') return true
|
||||
if (str === '1') return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
assertValidPort (port) {
|
||||
if (!port) {
|
||||
throw new Error('Could not determine port to query. Did you provide a port?')
|
||||
}
|
||||
if (port < 1 || port > 65535) {
|
||||
throw new Error('Invalid tcp/ip port: ' + port)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {function(NodeJS.Socket):Promise<T>} fn
|
||||
* @param {number=} port
|
||||
* @returns {Promise<T>}
|
||||
*/
|
||||
async withTcp (fn, port) {
|
||||
this.usedTcp = true
|
||||
const address = this.options.address
|
||||
if (!port) port = this.options.port
|
||||
this.assertValidPort(port)
|
||||
|
||||
let socket, connectionTimeout
|
||||
try {
|
||||
socket = net.connect(port, address)
|
||||
socket.setNoDelay(true)
|
||||
|
||||
// Prevent unhandled 'error' events from dumping straight to console
|
||||
socket.on('error', () => {})
|
||||
|
||||
this.logger.debug(log => {
|
||||
this.logger.debug(address + ':' + port + ' TCP Connecting')
|
||||
const writeHook = socket.write
|
||||
socket.write = (...args) => {
|
||||
log(address + ':' + port + ' TCP-->')
|
||||
log(debugDump(args[0]))
|
||||
writeHook.apply(socket, args)
|
||||
}
|
||||
socket.on('error', e => log('TCP Error:', e))
|
||||
socket.on('close', () => log('TCP Closed'))
|
||||
socket.on('data', (data) => {
|
||||
log(address + ':' + port + ' <--TCP')
|
||||
log(data)
|
||||
})
|
||||
socket.on('ready', () => log(address + ':' + port + ' TCP Connected'))
|
||||
})
|
||||
|
||||
const connectionPromise = new Promise((resolve, reject) => {
|
||||
socket.on('ready', resolve)
|
||||
socket.on('close', () => reject(new Error('TCP Connection Refused')))
|
||||
})
|
||||
this.registerRtt(connectionPromise)
|
||||
connectionTimeout = Promises.createTimeout(this.options.socketTimeout, 'TCP Opening')
|
||||
await Promise.race([
|
||||
connectionPromise,
|
||||
connectionTimeout,
|
||||
this.abortedPromise
|
||||
])
|
||||
return await fn(socket)
|
||||
} finally {
|
||||
socket && socket.destroy()
|
||||
connectionTimeout && connectionTimeout.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {NodeJS.Socket} socket
|
||||
* @param {Buffer|string} buffer
|
||||
* @param {function(Buffer):T} ondata
|
||||
* @returns Promise<T>
|
||||
*/
|
||||
async tcpSend (socket, buffer, ondata) {
|
||||
let timeout
|
||||
try {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
let received = Buffer.from([])
|
||||
const onData = (data) => {
|
||||
received = Buffer.concat([received, data])
|
||||
const result = ondata(received)
|
||||
if (result !== undefined) {
|
||||
socket.removeListener('data', onData)
|
||||
resolve(result)
|
||||
}
|
||||
}
|
||||
socket.on('data', onData)
|
||||
socket.write(buffer)
|
||||
})
|
||||
timeout = Promises.createTimeout(this.options.socketTimeout, 'TCP')
|
||||
return await Promise.race([promise, timeout, this.abortedPromise])
|
||||
} finally {
|
||||
timeout && timeout.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Buffer|string} buffer
|
||||
* @param {function(Buffer):T=} onPacket
|
||||
* @param {(function():T)=} onTimeout
|
||||
* @returns Promise<T>
|
||||
* @template T
|
||||
*/
|
||||
async udpSend (buffer, onPacket, onTimeout) {
|
||||
const address = this.options.address
|
||||
const port = this.options.port
|
||||
this.assertValidPort(port)
|
||||
|
||||
if (typeof buffer === 'string') buffer = Buffer.from(buffer, 'binary')
|
||||
|
||||
const socket = this.udpSocket
|
||||
await socket.send(buffer, address, port, this.options.debug)
|
||||
|
||||
if (!onPacket && !onTimeout) {
|
||||
return null
|
||||
}
|
||||
|
||||
let socketCallback
|
||||
let timeout
|
||||
try {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
const start = Date.now()
|
||||
let end = null
|
||||
socketCallback = (fromAddress, fromPort, buffer) => {
|
||||
try {
|
||||
if (fromAddress !== address) return
|
||||
if (fromPort !== port) return
|
||||
if (end === null) {
|
||||
end = Date.now()
|
||||
const rtt = end - start
|
||||
this.registerRtt(rtt)
|
||||
}
|
||||
const result = onPacket(buffer)
|
||||
if (result !== undefined) {
|
||||
this.logger.debug('UDP send finished by callback')
|
||||
resolve(result)
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
}
|
||||
socket.addCallback(socketCallback, this.options.debug)
|
||||
})
|
||||
timeout = Promises.createTimeout(this.options.socketTimeout, 'UDP')
|
||||
const wrappedTimeout = new Promise((resolve, reject) => {
|
||||
timeout.catch((e) => {
|
||||
this.logger.debug('UDP timeout detected')
|
||||
if (onTimeout) {
|
||||
try {
|
||||
const result = onTimeout()
|
||||
if (result !== undefined) {
|
||||
this.logger.debug('UDP timeout resolved by callback')
|
||||
resolve(result)
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
}
|
||||
reject(e)
|
||||
})
|
||||
})
|
||||
return await Promise.race([promise, wrappedTimeout, this.abortedPromise])
|
||||
} finally {
|
||||
timeout && timeout.cancel()
|
||||
socketCallback && socket.removeCallback(socketCallback)
|
||||
}
|
||||
}
|
||||
|
||||
async tcpPing () {
|
||||
// This will give a much more accurate RTT than using the rtt of an http request.
|
||||
if (!this.usedTcp) {
|
||||
await this.withTcp(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
async request (params) {
|
||||
await this.tcpPing()
|
||||
|
||||
let requestPromise
|
||||
try {
|
||||
requestPromise = got({
|
||||
...params,
|
||||
timeout: {
|
||||
request: this.options.socketTimeout
|
||||
}
|
||||
})
|
||||
this.logger.debug(log => {
|
||||
log(() => params.url + ' HTTP-->')
|
||||
requestPromise
|
||||
.then((response) => log(params.url + ' <--HTTP ' + response.statusCode))
|
||||
.catch(() => {})
|
||||
})
|
||||
const wrappedPromise = requestPromise.then(response => {
|
||||
if (response.statusCode !== 200) throw new Error('Bad status code: ' + response.statusCode)
|
||||
return response.body
|
||||
})
|
||||
return await Promise.race([wrappedPromise, this.abortedPromise])
|
||||
} finally {
|
||||
requestPromise && requestPromise.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
import { EventEmitter } from 'node:events'
|
||||
import * as net from 'node:net'
|
||||
import got from 'got'
|
||||
import Reader from '../lib/reader.js'
|
||||
import { debugDump } from '../lib/HexUtil.js'
|
||||
import Logger from '../lib/Logger.js'
|
||||
import DnsResolver from '../lib/DnsResolver.js'
|
||||
import { Results } from '../lib/Results.js'
|
||||
import Promises from '../lib/Promises.js'
|
||||
|
||||
let uid = 0
|
||||
|
||||
export default class Core extends EventEmitter {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'utf8'
|
||||
this.byteorder = 'le'
|
||||
this.delimiter = '\0'
|
||||
this.srvRecord = null
|
||||
this.abortedPromise = null
|
||||
this.logger = new Logger()
|
||||
this.dnsResolver = new DnsResolver(this.logger)
|
||||
|
||||
// Sent to us by QueryRunner
|
||||
this.options = null
|
||||
/** @type GlobalUdpSocket */
|
||||
this.udpSocket = null
|
||||
this.shortestRTT = 0
|
||||
this.usedTcp = false
|
||||
}
|
||||
|
||||
// Runs a single attempt with a timeout and cleans up afterward
|
||||
async runOnceSafe () {
|
||||
if (this.options.debug) {
|
||||
this.logger.debugEnabled = true
|
||||
}
|
||||
this.logger.prefix = 'Q#' + (uid++)
|
||||
|
||||
this.logger.debug('Starting')
|
||||
this.logger.debug('Protocol: ' + this.constructor.name)
|
||||
this.logger.debug('Options:', this.options)
|
||||
|
||||
let abortCall = null
|
||||
this.abortedPromise = new Promise((resolve, reject) => {
|
||||
abortCall = () => reject(new Error('Query is finished -- cancelling outstanding promises'))
|
||||
}).catch(() => {
|
||||
// Make sure that if this promise isn't attached to, it doesn't throw a unhandled promise rejection
|
||||
})
|
||||
|
||||
let timeout
|
||||
try {
|
||||
const promise = this.runOnce()
|
||||
timeout = Promises.createTimeout(this.options.attemptTimeout, 'Attempt')
|
||||
const result = await Promise.race([promise, timeout])
|
||||
this.logger.debug('Query was successful')
|
||||
return result
|
||||
} catch (e) {
|
||||
this.logger.debug('Query failed with error', e)
|
||||
throw e
|
||||
} finally {
|
||||
timeout && timeout.cancel()
|
||||
try {
|
||||
abortCall()
|
||||
} catch (e) {
|
||||
this.logger.debug('Error during abort cleanup: ' + e.stack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async runOnce () {
|
||||
const options = this.options
|
||||
if (('host' in options) && !('address' in options)) {
|
||||
const resolved = await this.dnsResolver.resolve(options.host, options.ipFamily, this.srvRecord)
|
||||
options.address = resolved.address
|
||||
if (resolved.port) options.port = resolved.port
|
||||
}
|
||||
|
||||
const state = new Results()
|
||||
|
||||
await this.run(state)
|
||||
|
||||
// because lots of servers prefix with spaces to try to appear first
|
||||
state.name = (state.name || '').trim()
|
||||
|
||||
if (!('connect' in state)) {
|
||||
state.connect = '' +
|
||||
(state.gameHost || this.options.host || this.options.address) +
|
||||
':' +
|
||||
(state.gamePort || this.options.port)
|
||||
}
|
||||
state.ping = this.shortestRTT
|
||||
delete state.gameHost
|
||||
delete state.gamePort
|
||||
|
||||
this.logger.debug(log => {
|
||||
log('Size of players array: ' + state.players.length)
|
||||
log('Size of bots array: ' + state.bots.length)
|
||||
})
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
async run (/** Results */ state) {}
|
||||
|
||||
/** Param can be a time in ms, or a promise (which will be timed) */
|
||||
registerRtt (param) {
|
||||
if (param.then) {
|
||||
const start = Date.now()
|
||||
param.then(() => {
|
||||
const end = Date.now()
|
||||
const rtt = end - start
|
||||
this.registerRtt(rtt)
|
||||
}).catch(() => {})
|
||||
} else {
|
||||
this.logger.debug('Registered RTT: ' + param + 'ms')
|
||||
if (this.shortestRTT === 0 || param < this.shortestRTT) {
|
||||
this.shortestRTT = param
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// utils
|
||||
/** @returns {Reader} */
|
||||
reader (buffer) {
|
||||
return new Reader(this, buffer)
|
||||
}
|
||||
|
||||
translate (obj, trans) {
|
||||
for (const from of Object.keys(trans)) {
|
||||
const to = trans[from]
|
||||
if (from in obj) {
|
||||
if (to) obj[to] = obj[from]
|
||||
delete obj[from]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trueTest (str) {
|
||||
if (typeof str === 'boolean') return str
|
||||
if (typeof str === 'number') return str !== 0
|
||||
if (typeof str === 'string') {
|
||||
if (str.toLowerCase() === 'true') return true
|
||||
if (str.toLowerCase() === 'yes') return true
|
||||
if (str === '1') return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
assertValidPort (port) {
|
||||
if (!port) {
|
||||
throw new Error('Could not determine port to query. Did you provide a port?')
|
||||
}
|
||||
if (port < 1 || port > 65535) {
|
||||
throw new Error('Invalid tcp/ip port: ' + port)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {function(NodeJS.Socket):Promise<T>} fn
|
||||
* @param {number=} port
|
||||
* @returns {Promise<T>}
|
||||
*/
|
||||
async withTcp (fn, port) {
|
||||
this.usedTcp = true
|
||||
const address = this.options.address
|
||||
if (!port) port = this.options.port
|
||||
this.assertValidPort(port)
|
||||
|
||||
let socket, connectionTimeout
|
||||
try {
|
||||
socket = net.connect(port, address)
|
||||
socket.setNoDelay(true)
|
||||
|
||||
// Prevent unhandled 'error' events from dumping straight to console
|
||||
socket.on('error', () => {})
|
||||
|
||||
this.logger.debug(log => {
|
||||
this.logger.debug(address + ':' + port + ' TCP Connecting')
|
||||
const writeHook = socket.write
|
||||
socket.write = (...args) => {
|
||||
log(address + ':' + port + ' TCP-->')
|
||||
log(debugDump(args[0]))
|
||||
writeHook.apply(socket, args)
|
||||
}
|
||||
socket.on('error', e => log('TCP Error:', e))
|
||||
socket.on('close', () => log('TCP Closed'))
|
||||
socket.on('data', (data) => {
|
||||
log(address + ':' + port + ' <--TCP')
|
||||
log(data)
|
||||
})
|
||||
socket.on('ready', () => log(address + ':' + port + ' TCP Connected'))
|
||||
})
|
||||
|
||||
const connectionPromise = new Promise((resolve, reject) => {
|
||||
socket.on('ready', resolve)
|
||||
socket.on('close', () => reject(new Error('TCP Connection Refused')))
|
||||
})
|
||||
this.registerRtt(connectionPromise)
|
||||
connectionTimeout = Promises.createTimeout(this.options.socketTimeout, 'TCP Opening')
|
||||
await Promise.race([
|
||||
connectionPromise,
|
||||
connectionTimeout,
|
||||
this.abortedPromise
|
||||
])
|
||||
return await fn(socket)
|
||||
} finally {
|
||||
socket && socket.destroy()
|
||||
connectionTimeout && connectionTimeout.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {NodeJS.Socket} socket
|
||||
* @param {Buffer|string} buffer
|
||||
* @param {function(Buffer):T} ondata
|
||||
* @returns Promise<T>
|
||||
*/
|
||||
async tcpSend (socket, buffer, ondata) {
|
||||
let timeout
|
||||
try {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
let received = Buffer.from([])
|
||||
const onData = (data) => {
|
||||
received = Buffer.concat([received, data])
|
||||
const result = ondata(received)
|
||||
if (result !== undefined) {
|
||||
socket.removeListener('data', onData)
|
||||
resolve(result)
|
||||
}
|
||||
}
|
||||
socket.on('data', onData)
|
||||
socket.write(buffer)
|
||||
})
|
||||
timeout = Promises.createTimeout(this.options.socketTimeout, 'TCP')
|
||||
return await Promise.race([promise, timeout, this.abortedPromise])
|
||||
} finally {
|
||||
timeout && timeout.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Buffer|string} buffer
|
||||
* @param {function(Buffer):T=} onPacket
|
||||
* @param {(function():T)=} onTimeout
|
||||
* @returns Promise<T>
|
||||
* @template T
|
||||
*/
|
||||
async udpSend (buffer, onPacket, onTimeout) {
|
||||
const address = this.options.address
|
||||
const port = this.options.port
|
||||
this.assertValidPort(port)
|
||||
|
||||
if (typeof buffer === 'string') buffer = Buffer.from(buffer, 'binary')
|
||||
|
||||
const socket = this.udpSocket
|
||||
await socket.send(buffer, address, port, this.options.debug)
|
||||
|
||||
if (!onPacket && !onTimeout) {
|
||||
return null
|
||||
}
|
||||
|
||||
let socketCallback
|
||||
let timeout
|
||||
try {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
const start = Date.now()
|
||||
let end = null
|
||||
socketCallback = (fromAddress, fromPort, buffer) => {
|
||||
try {
|
||||
if (fromAddress !== address) return
|
||||
if (fromPort !== port) return
|
||||
if (end === null) {
|
||||
end = Date.now()
|
||||
const rtt = end - start
|
||||
this.registerRtt(rtt)
|
||||
}
|
||||
const result = onPacket(buffer)
|
||||
if (result !== undefined) {
|
||||
this.logger.debug('UDP send finished by callback')
|
||||
resolve(result)
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
}
|
||||
socket.addCallback(socketCallback, this.options.debug)
|
||||
})
|
||||
timeout = Promises.createTimeout(this.options.socketTimeout, 'UDP')
|
||||
const wrappedTimeout = new Promise((resolve, reject) => {
|
||||
timeout.catch((e) => {
|
||||
this.logger.debug('UDP timeout detected')
|
||||
if (onTimeout) {
|
||||
try {
|
||||
const result = onTimeout()
|
||||
if (result !== undefined) {
|
||||
this.logger.debug('UDP timeout resolved by callback')
|
||||
resolve(result)
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
}
|
||||
reject(e)
|
||||
})
|
||||
})
|
||||
return await Promise.race([promise, wrappedTimeout, this.abortedPromise])
|
||||
} finally {
|
||||
timeout && timeout.cancel()
|
||||
socketCallback && socket.removeCallback(socketCallback)
|
||||
}
|
||||
}
|
||||
|
||||
async tcpPing () {
|
||||
// This will give a much more accurate RTT than using the rtt of an http request.
|
||||
if (!this.usedTcp) {
|
||||
await this.withTcp(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
async request (params) {
|
||||
await this.tcpPing()
|
||||
|
||||
let requestPromise
|
||||
try {
|
||||
requestPromise = got({
|
||||
...params,
|
||||
timeout: {
|
||||
request: this.options.socketTimeout
|
||||
}
|
||||
})
|
||||
this.logger.debug(log => {
|
||||
log(() => params.url + ' HTTP-->')
|
||||
requestPromise
|
||||
.then((response) => log(params.url + ' <--HTTP ' + response.statusCode))
|
||||
.catch(() => {})
|
||||
})
|
||||
const wrappedPromise = requestPromise.then(response => {
|
||||
if (response.statusCode !== 200) throw new Error('Bad status code: ' + response.statusCode)
|
||||
return response.body
|
||||
})
|
||||
return await Promise.race([wrappedPromise, this.abortedPromise])
|
||||
} finally {
|
||||
requestPromise && requestPromise.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,65 +1,65 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class cs2d extends Core {
|
||||
async run (state) {
|
||||
const reader = await this.sendQuery(
|
||||
Buffer.from('\x01\x00\xFB\x01\xF5\x03\xFB\x05', 'binary'),
|
||||
Buffer.from('\x01\x00\xFB\x01', 'binary')
|
||||
)
|
||||
const flags = reader.uint(1)
|
||||
state.raw.flags = flags
|
||||
state.password = this.readFlag(flags, 0)
|
||||
state.raw.registeredOnly = this.readFlag(flags, 1)
|
||||
state.raw.fogOfWar = this.readFlag(flags, 2)
|
||||
state.raw.friendlyFire = this.readFlag(flags, 3)
|
||||
state.raw.botsEnabled = this.readFlag(flags, 5)
|
||||
state.raw.luaScripts = this.readFlag(flags, 6)
|
||||
state.raw.forceLight = this.readFlag(flags, 7)
|
||||
state.name = this.readString(reader)
|
||||
state.map = this.readString(reader)
|
||||
state.numplayers = reader.uint(1)
|
||||
state.maxplayers = reader.uint(1)
|
||||
if (flags & 32) {
|
||||
state.raw.gamemode = reader.uint(1)
|
||||
} else {
|
||||
state.raw.gamemode = 0
|
||||
}
|
||||
state.raw.numbots = reader.uint(1)
|
||||
const flags2 = reader.uint(1)
|
||||
state.raw.flags2 = flags2
|
||||
state.raw.recoil = this.readFlag(flags2, 0)
|
||||
state.raw.offScreenDamage = this.readFlag(flags2, 1)
|
||||
state.raw.hasDownloads = this.readFlag(flags2, 2)
|
||||
reader.skip(2)
|
||||
const players = reader.uint(1)
|
||||
for (let i = 0; i < players; i++) {
|
||||
const player = {}
|
||||
player.id = reader.uint(1)
|
||||
player.name = this.readString(reader)
|
||||
player.team = reader.uint(1)
|
||||
player.score = reader.uint(4)
|
||||
player.deaths = reader.uint(4)
|
||||
state.players.push(player)
|
||||
}
|
||||
}
|
||||
|
||||
async sendQuery (request, expectedHeader) {
|
||||
// Send multiple copies of the request packet, because cs2d likes to just ignore them randomly
|
||||
await this.udpSend(request)
|
||||
await this.udpSend(request)
|
||||
return await this.udpSend(request, (buffer) => {
|
||||
const reader = this.reader(buffer)
|
||||
const header = reader.part(4)
|
||||
if (!header.equals(expectedHeader)) return
|
||||
return reader
|
||||
})
|
||||
}
|
||||
|
||||
readFlag (flags, offset) {
|
||||
return !!(flags & (1 << offset))
|
||||
}
|
||||
|
||||
readString (reader) {
|
||||
return reader.pascalString(1)
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class cs2d extends Core {
|
||||
async run (state) {
|
||||
const reader = await this.sendQuery(
|
||||
Buffer.from('\x01\x00\xFB\x01\xF5\x03\xFB\x05', 'binary'),
|
||||
Buffer.from('\x01\x00\xFB\x01', 'binary')
|
||||
)
|
||||
const flags = reader.uint(1)
|
||||
state.raw.flags = flags
|
||||
state.password = this.readFlag(flags, 0)
|
||||
state.raw.registeredOnly = this.readFlag(flags, 1)
|
||||
state.raw.fogOfWar = this.readFlag(flags, 2)
|
||||
state.raw.friendlyFire = this.readFlag(flags, 3)
|
||||
state.raw.botsEnabled = this.readFlag(flags, 5)
|
||||
state.raw.luaScripts = this.readFlag(flags, 6)
|
||||
state.raw.forceLight = this.readFlag(flags, 7)
|
||||
state.name = this.readString(reader)
|
||||
state.map = this.readString(reader)
|
||||
state.numplayers = reader.uint(1)
|
||||
state.maxplayers = reader.uint(1)
|
||||
if (flags & 32) {
|
||||
state.raw.gamemode = reader.uint(1)
|
||||
} else {
|
||||
state.raw.gamemode = 0
|
||||
}
|
||||
state.raw.numbots = reader.uint(1)
|
||||
const flags2 = reader.uint(1)
|
||||
state.raw.flags2 = flags2
|
||||
state.raw.recoil = this.readFlag(flags2, 0)
|
||||
state.raw.offScreenDamage = this.readFlag(flags2, 1)
|
||||
state.raw.hasDownloads = this.readFlag(flags2, 2)
|
||||
reader.skip(2)
|
||||
const players = reader.uint(1)
|
||||
for (let i = 0; i < players; i++) {
|
||||
const player = {}
|
||||
player.id = reader.uint(1)
|
||||
player.name = this.readString(reader)
|
||||
player.team = reader.uint(1)
|
||||
player.score = reader.uint(4)
|
||||
player.deaths = reader.uint(4)
|
||||
state.players.push(player)
|
||||
}
|
||||
}
|
||||
|
||||
async sendQuery (request, expectedHeader) {
|
||||
// Send multiple copies of the request packet, because cs2d likes to just ignore them randomly
|
||||
await this.udpSend(request)
|
||||
await this.udpSend(request)
|
||||
return await this.udpSend(request, (buffer) => {
|
||||
const reader = this.reader(buffer)
|
||||
const header = reader.part(4)
|
||||
if (!header.equals(expectedHeader)) return
|
||||
return reader
|
||||
})
|
||||
}
|
||||
|
||||
readFlag (flags, offset) {
|
||||
return !!(flags & (1 << offset))
|
||||
}
|
||||
|
||||
readString (reader) {
|
||||
return reader.pascalString(1)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +1,29 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class discord extends Core {
|
||||
async run (state) {
|
||||
const guildId = this.options.guildId
|
||||
if (typeof guildId !== 'string') {
|
||||
throw new Error('guildId option must be set when querying discord. Ensure the guildId is a string and not a number.' +
|
||||
" (It's too large of a number for javascript to store without losing precision)")
|
||||
}
|
||||
this.usedTcp = true
|
||||
const raw = await this.request({
|
||||
url: 'https://discordapp.com/api/guilds/' + guildId + '/widget.json'
|
||||
})
|
||||
const json = JSON.parse(raw)
|
||||
state.name = json.name
|
||||
if (json.instant_invite) {
|
||||
state.connect = json.instant_invite
|
||||
} else {
|
||||
state.connect = 'https://discordapp.com/channels/' + guildId
|
||||
}
|
||||
for (const member of json.members) {
|
||||
const { username: name, ...rest } = member
|
||||
state.players.push({ name, ...rest })
|
||||
}
|
||||
delete json.members
|
||||
state.maxplayers = 500000
|
||||
state.raw = json
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class discord extends Core {
|
||||
async run (state) {
|
||||
const guildId = this.options.guildId
|
||||
if (typeof guildId !== 'string') {
|
||||
throw new Error('guildId option must be set when querying discord. Ensure the guildId is a string and not a number.' +
|
||||
" (It's too large of a number for javascript to store without losing precision)")
|
||||
}
|
||||
this.usedTcp = true
|
||||
const raw = await this.request({
|
||||
url: 'https://discordapp.com/api/guilds/' + guildId + '/widget.json'
|
||||
})
|
||||
const json = JSON.parse(raw)
|
||||
state.name = json.name
|
||||
if (json.instant_invite) {
|
||||
state.connect = json.instant_invite
|
||||
} else {
|
||||
state.connect = 'https://discordapp.com/channels/' + guildId
|
||||
}
|
||||
for (const member of json.members) {
|
||||
const { username: name, ...rest } = member
|
||||
state.players.push({ name, ...rest })
|
||||
}
|
||||
delete json.members
|
||||
state.maxplayers = 500000
|
||||
state.raw = json
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,148 +1,148 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class doom3 extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'latin1'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
const body = await this.udpSend('\xff\xffgetInfo\x00PiNGPoNg\x00', packet => {
|
||||
const reader = this.reader(packet)
|
||||
const header = reader.uint(2)
|
||||
if (header !== 0xffff) return
|
||||
const header2 = reader.string()
|
||||
if (header2 !== 'infoResponse') return
|
||||
const challengePart1 = reader.string(4)
|
||||
if (challengePart1 !== 'PiNG') return
|
||||
// some doom3 implementations only return the first 4 bytes of the challenge
|
||||
const challengePart2 = reader.string(4)
|
||||
if (challengePart2 !== 'PoNg') reader.skip(-4)
|
||||
return reader.rest()
|
||||
})
|
||||
|
||||
let reader = this.reader(body)
|
||||
const protoVersion = reader.uint(4)
|
||||
state.raw.protocolVersion = (protoVersion >> 16) + '.' + (protoVersion & 0xffff)
|
||||
|
||||
// some doom implementations send us a packet size here, some don't (etqw does this)
|
||||
// we can tell if this is a packet size, because the third and fourth byte will be 0 (no packets are that massive)
|
||||
reader.skip(2)
|
||||
const packetContainsSize = (reader.uint(2) === 0)
|
||||
reader.skip(-4)
|
||||
|
||||
if (packetContainsSize) {
|
||||
const size = reader.uint(4)
|
||||
this.logger.debug('Received packet size: ' + size)
|
||||
}
|
||||
|
||||
while (!reader.done()) {
|
||||
const key = reader.string()
|
||||
let value = this.stripColors(reader.string())
|
||||
if (key === 'si_map') {
|
||||
value = value.replace('maps/', '')
|
||||
value = value.replace('.entities', '')
|
||||
}
|
||||
if (!key) break
|
||||
state.raw[key] = value
|
||||
this.logger.debug(key + '=' + value)
|
||||
}
|
||||
|
||||
const isEtqw = state.raw.gamename && state.raw.gamename.toLowerCase().includes('etqw')
|
||||
|
||||
const rest = reader.rest()
|
||||
let playerResult = this.attemptPlayerParse(rest, isEtqw, false, false, false)
|
||||
if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, false, false)
|
||||
if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, true, true)
|
||||
if (!playerResult) {
|
||||
throw new Error('Unable to find a suitable parse strategy for player list')
|
||||
}
|
||||
let players;
|
||||
[players, reader] = playerResult
|
||||
|
||||
state.numplayers = players.length
|
||||
for (const player of players) {
|
||||
if (!player.ping || player.typeflag) { state.bots.push(player) } else { state.players.push(player) }
|
||||
}
|
||||
|
||||
state.raw.osmask = reader.uint(4)
|
||||
if (isEtqw) {
|
||||
state.raw.ranked = reader.uint(1)
|
||||
state.raw.timeleft = reader.uint(4)
|
||||
state.raw.gamestate = reader.uint(1)
|
||||
state.raw.servertype = reader.uint(1)
|
||||
// 0 = regular, 1 = tv
|
||||
if (state.raw.servertype === 0) {
|
||||
state.raw.interestedClients = reader.uint(1)
|
||||
} else if (state.raw.servertype === 1) {
|
||||
state.raw.connectedClients = reader.uint(4)
|
||||
state.raw.maxClients = reader.uint(4)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.raw.si_name) state.name = state.raw.si_name
|
||||
if (state.raw.si_map) state.map = state.raw.si_map
|
||||
if (state.raw.si_maxplayers) state.maxplayers = parseInt(state.raw.si_maxplayers)
|
||||
if (state.raw.si_maxPlayers) state.maxplayers = parseInt(state.raw.si_maxPlayers)
|
||||
if (state.raw.si_usepass === '1') state.password = true
|
||||
if (state.raw.si_needPass === '1') state.password = true
|
||||
if (this.options.port === 27733) state.gamePort = 3074 // etqw has a different query and game port
|
||||
}
|
||||
|
||||
attemptPlayerParse (rest, isEtqw, hasClanTag, hasClanTagPos, hasTypeFlag) {
|
||||
this.logger.debug('starting player parse attempt:')
|
||||
this.logger.debug('isEtqw: ' + isEtqw)
|
||||
this.logger.debug('hasClanTag: ' + hasClanTag)
|
||||
this.logger.debug('hasClanTagPos: ' + hasClanTagPos)
|
||||
this.logger.debug('hasTypeFlag: ' + hasTypeFlag)
|
||||
const reader = this.reader(rest)
|
||||
let lastId = -1
|
||||
const players = []
|
||||
while (true) {
|
||||
this.logger.debug('---')
|
||||
if (reader.done()) {
|
||||
this.logger.debug('* aborting attempt, overran buffer *')
|
||||
return null
|
||||
}
|
||||
const player = {}
|
||||
player.id = reader.uint(1)
|
||||
this.logger.debug('id: ' + player.id)
|
||||
if (player.id <= lastId || player.id > 0x20) {
|
||||
this.logger.debug('* aborting attempt, invalid player id *')
|
||||
return null
|
||||
}
|
||||
lastId = player.id
|
||||
if (player.id === 0x20) {
|
||||
this.logger.debug('* player parse successful *')
|
||||
break
|
||||
}
|
||||
player.ping = reader.uint(2)
|
||||
this.logger.debug('ping: ' + player.ping)
|
||||
if (!isEtqw) {
|
||||
player.rate = reader.uint(4)
|
||||
this.logger.debug('rate: ' + player.rate)
|
||||
}
|
||||
player.name = this.stripColors(reader.string())
|
||||
this.logger.debug('name: ' + player.name)
|
||||
if (hasClanTag) {
|
||||
if (hasClanTagPos) {
|
||||
const clanTagPos = reader.uint(1)
|
||||
this.logger.debug('clanTagPos: ' + clanTagPos)
|
||||
}
|
||||
player.clantag = this.stripColors(reader.string())
|
||||
this.logger.debug('clan tag: ' + player.clantag)
|
||||
}
|
||||
if (hasTypeFlag) {
|
||||
player.typeflag = reader.uint(1)
|
||||
this.logger.debug('type flag: ' + player.typeflag)
|
||||
}
|
||||
players.push(player)
|
||||
}
|
||||
return [players, reader]
|
||||
}
|
||||
|
||||
stripColors (str) {
|
||||
// uses quake 3 color codes
|
||||
return str.replace(/\^(X.{6}|.)/g, '')
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class doom3 extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'latin1'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
const body = await this.udpSend('\xff\xffgetInfo\x00PiNGPoNg\x00', packet => {
|
||||
const reader = this.reader(packet)
|
||||
const header = reader.uint(2)
|
||||
if (header !== 0xffff) return
|
||||
const header2 = reader.string()
|
||||
if (header2 !== 'infoResponse') return
|
||||
const challengePart1 = reader.string(4)
|
||||
if (challengePart1 !== 'PiNG') return
|
||||
// some doom3 implementations only return the first 4 bytes of the challenge
|
||||
const challengePart2 = reader.string(4)
|
||||
if (challengePart2 !== 'PoNg') reader.skip(-4)
|
||||
return reader.rest()
|
||||
})
|
||||
|
||||
let reader = this.reader(body)
|
||||
const protoVersion = reader.uint(4)
|
||||
state.raw.protocolVersion = (protoVersion >> 16) + '.' + (protoVersion & 0xffff)
|
||||
|
||||
// some doom implementations send us a packet size here, some don't (etqw does this)
|
||||
// we can tell if this is a packet size, because the third and fourth byte will be 0 (no packets are that massive)
|
||||
reader.skip(2)
|
||||
const packetContainsSize = (reader.uint(2) === 0)
|
||||
reader.skip(-4)
|
||||
|
||||
if (packetContainsSize) {
|
||||
const size = reader.uint(4)
|
||||
this.logger.debug('Received packet size: ' + size)
|
||||
}
|
||||
|
||||
while (!reader.done()) {
|
||||
const key = reader.string()
|
||||
let value = this.stripColors(reader.string())
|
||||
if (key === 'si_map') {
|
||||
value = value.replace('maps/', '')
|
||||
value = value.replace('.entities', '')
|
||||
}
|
||||
if (!key) break
|
||||
state.raw[key] = value
|
||||
this.logger.debug(key + '=' + value)
|
||||
}
|
||||
|
||||
const isEtqw = state.raw.gamename && state.raw.gamename.toLowerCase().includes('etqw')
|
||||
|
||||
const rest = reader.rest()
|
||||
let playerResult = this.attemptPlayerParse(rest, isEtqw, false, false, false)
|
||||
if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, false, false)
|
||||
if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, true, true)
|
||||
if (!playerResult) {
|
||||
throw new Error('Unable to find a suitable parse strategy for player list')
|
||||
}
|
||||
let players;
|
||||
[players, reader] = playerResult
|
||||
|
||||
state.numplayers = players.length
|
||||
for (const player of players) {
|
||||
if (!player.ping || player.typeflag) { state.bots.push(player) } else { state.players.push(player) }
|
||||
}
|
||||
|
||||
state.raw.osmask = reader.uint(4)
|
||||
if (isEtqw) {
|
||||
state.raw.ranked = reader.uint(1)
|
||||
state.raw.timeleft = reader.uint(4)
|
||||
state.raw.gamestate = reader.uint(1)
|
||||
state.raw.servertype = reader.uint(1)
|
||||
// 0 = regular, 1 = tv
|
||||
if (state.raw.servertype === 0) {
|
||||
state.raw.interestedClients = reader.uint(1)
|
||||
} else if (state.raw.servertype === 1) {
|
||||
state.raw.connectedClients = reader.uint(4)
|
||||
state.raw.maxClients = reader.uint(4)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.raw.si_name) state.name = state.raw.si_name
|
||||
if (state.raw.si_map) state.map = state.raw.si_map
|
||||
if (state.raw.si_maxplayers) state.maxplayers = parseInt(state.raw.si_maxplayers)
|
||||
if (state.raw.si_maxPlayers) state.maxplayers = parseInt(state.raw.si_maxPlayers)
|
||||
if (state.raw.si_usepass === '1') state.password = true
|
||||
if (state.raw.si_needPass === '1') state.password = true
|
||||
if (this.options.port === 27733) state.gamePort = 3074 // etqw has a different query and game port
|
||||
}
|
||||
|
||||
attemptPlayerParse (rest, isEtqw, hasClanTag, hasClanTagPos, hasTypeFlag) {
|
||||
this.logger.debug('starting player parse attempt:')
|
||||
this.logger.debug('isEtqw: ' + isEtqw)
|
||||
this.logger.debug('hasClanTag: ' + hasClanTag)
|
||||
this.logger.debug('hasClanTagPos: ' + hasClanTagPos)
|
||||
this.logger.debug('hasTypeFlag: ' + hasTypeFlag)
|
||||
const reader = this.reader(rest)
|
||||
let lastId = -1
|
||||
const players = []
|
||||
while (true) {
|
||||
this.logger.debug('---')
|
||||
if (reader.done()) {
|
||||
this.logger.debug('* aborting attempt, overran buffer *')
|
||||
return null
|
||||
}
|
||||
const player = {}
|
||||
player.id = reader.uint(1)
|
||||
this.logger.debug('id: ' + player.id)
|
||||
if (player.id <= lastId || player.id > 0x20) {
|
||||
this.logger.debug('* aborting attempt, invalid player id *')
|
||||
return null
|
||||
}
|
||||
lastId = player.id
|
||||
if (player.id === 0x20) {
|
||||
this.logger.debug('* player parse successful *')
|
||||
break
|
||||
}
|
||||
player.ping = reader.uint(2)
|
||||
this.logger.debug('ping: ' + player.ping)
|
||||
if (!isEtqw) {
|
||||
player.rate = reader.uint(4)
|
||||
this.logger.debug('rate: ' + player.rate)
|
||||
}
|
||||
player.name = this.stripColors(reader.string())
|
||||
this.logger.debug('name: ' + player.name)
|
||||
if (hasClanTag) {
|
||||
if (hasClanTagPos) {
|
||||
const clanTagPos = reader.uint(1)
|
||||
this.logger.debug('clanTagPos: ' + clanTagPos)
|
||||
}
|
||||
player.clantag = this.stripColors(reader.string())
|
||||
this.logger.debug('clan tag: ' + player.clantag)
|
||||
}
|
||||
if (hasTypeFlag) {
|
||||
player.typeflag = reader.uint(1)
|
||||
this.logger.debug('type flag: ' + player.typeflag)
|
||||
}
|
||||
players.push(player)
|
||||
}
|
||||
return [players, reader]
|
||||
}
|
||||
|
||||
stripColors (str) {
|
||||
// uses quake 3 color codes
|
||||
return str.replace(/\^(X.{6}|.)/g, '')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class eco extends Core {
|
||||
async run (state) {
|
||||
if (!this.options.port) this.options.port = 3001
|
||||
|
||||
const request = await this.request({
|
||||
url: `http://${this.options.address}:${this.options.port}/frontpage`,
|
||||
responseType: 'json'
|
||||
})
|
||||
const serverInfo = request.Info
|
||||
|
||||
state.name = serverInfo.Description
|
||||
state.numplayers = serverInfo.OnlinePlayers;
|
||||
state.maxplayers = serverInfo.TotalPlayers
|
||||
state.password = serverInfo.HasPassword
|
||||
state.gamePort = serverInfo.GamePort
|
||||
state.raw = serverInfo
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class eco extends Core {
|
||||
async run (state) {
|
||||
if (!this.options.port) this.options.port = 3001
|
||||
|
||||
const request = await this.request({
|
||||
url: `http://${this.options.address}:${this.options.port}/frontpage`,
|
||||
responseType: 'json'
|
||||
})
|
||||
const serverInfo = request.Info
|
||||
|
||||
state.name = serverInfo.Description
|
||||
state.numplayers = serverInfo.OnlinePlayers;
|
||||
state.maxplayers = serverInfo.TotalPlayers
|
||||
state.password = serverInfo.HasPassword
|
||||
state.gamePort = serverInfo.GamePort
|
||||
state.raw = serverInfo
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,38 +1,38 @@
|
|||
import valve from './valve.js'
|
||||
|
||||
export default class ffow extends valve {
|
||||
constructor () {
|
||||
super()
|
||||
this.byteorder = 'be'
|
||||
this.legacyChallenge = true
|
||||
}
|
||||
|
||||
async queryInfo (state) {
|
||||
this.logger.debug('Requesting ffow info ...')
|
||||
const b = await this.sendPacket(
|
||||
0x46,
|
||||
'LSQ',
|
||||
0x49
|
||||
)
|
||||
|
||||
const reader = this.reader(b)
|
||||
state.raw.protocol = reader.uint(1)
|
||||
state.name = reader.string()
|
||||
state.map = reader.string()
|
||||
state.raw.mod = reader.string()
|
||||
state.raw.gamemode = reader.string()
|
||||
state.raw.description = reader.string()
|
||||
state.raw.version = reader.string()
|
||||
state.gamePort = reader.uint(2)
|
||||
state.numplayers = reader.uint(1)
|
||||
state.maxplayers = reader.uint(1)
|
||||
state.raw.listentype = String.fromCharCode(reader.uint(1))
|
||||
state.raw.environment = String.fromCharCode(reader.uint(1))
|
||||
state.password = !!reader.uint(1)
|
||||
state.raw.secure = reader.uint(1)
|
||||
state.raw.averagefps = reader.uint(1)
|
||||
state.raw.round = reader.uint(1)
|
||||
state.raw.maxrounds = reader.uint(1)
|
||||
state.raw.timeleft = reader.uint(2)
|
||||
}
|
||||
}
|
||||
import valve from './valve.js'
|
||||
|
||||
export default class ffow extends valve {
|
||||
constructor () {
|
||||
super()
|
||||
this.byteorder = 'be'
|
||||
this.legacyChallenge = true
|
||||
}
|
||||
|
||||
async queryInfo (state) {
|
||||
this.logger.debug('Requesting ffow info ...')
|
||||
const b = await this.sendPacket(
|
||||
0x46,
|
||||
'LSQ',
|
||||
0x49
|
||||
)
|
||||
|
||||
const reader = this.reader(b)
|
||||
state.raw.protocol = reader.uint(1)
|
||||
state.name = reader.string()
|
||||
state.map = reader.string()
|
||||
state.raw.mod = reader.string()
|
||||
state.raw.gamemode = reader.string()
|
||||
state.raw.description = reader.string()
|
||||
state.raw.version = reader.string()
|
||||
state.gamePort = reader.uint(2)
|
||||
state.numplayers = reader.uint(1)
|
||||
state.maxplayers = reader.uint(1)
|
||||
state.raw.listentype = String.fromCharCode(reader.uint(1))
|
||||
state.raw.environment = String.fromCharCode(reader.uint(1))
|
||||
state.password = !!reader.uint(1)
|
||||
state.raw.secure = reader.uint(1)
|
||||
state.raw.averagefps = reader.uint(1)
|
||||
state.raw.round = reader.uint(1)
|
||||
state.raw.maxrounds = reader.uint(1)
|
||||
state.raw.timeleft = reader.uint(2)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,33 +1,33 @@
|
|||
import quake2 from './quake2.js'
|
||||
|
||||
export default class fivem extends quake2 {
|
||||
constructor () {
|
||||
super()
|
||||
this.sendHeader = 'getinfo xxx'
|
||||
this.responseHeader = 'infoResponse'
|
||||
this.encoding = 'utf8'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
await super.run(state)
|
||||
|
||||
{
|
||||
const json = await this.request({
|
||||
url: 'http://' + this.options.address + ':' + this.options.port + '/info.json',
|
||||
responseType: 'json'
|
||||
})
|
||||
state.raw.info = json
|
||||
}
|
||||
|
||||
{
|
||||
const json = await this.request({
|
||||
url: 'http://' + this.options.address + ':' + this.options.port + '/players.json',
|
||||
responseType: 'json'
|
||||
})
|
||||
state.raw.players = json
|
||||
for (const player of json) {
|
||||
state.players.push({ name: player.name, ping: player.ping })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
import quake2 from './quake2.js'
|
||||
|
||||
export default class fivem extends quake2 {
|
||||
constructor () {
|
||||
super()
|
||||
this.sendHeader = 'getinfo xxx'
|
||||
this.responseHeader = 'infoResponse'
|
||||
this.encoding = 'utf8'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
await super.run(state)
|
||||
|
||||
{
|
||||
const json = await this.request({
|
||||
url: 'http://' + this.options.address + ':' + this.options.port + '/info.json',
|
||||
responseType: 'json'
|
||||
})
|
||||
state.raw.info = json
|
||||
}
|
||||
|
||||
{
|
||||
const json = await this.request({
|
||||
url: 'http://' + this.options.address + ':' + this.options.port + '/players.json',
|
||||
responseType: 'json'
|
||||
})
|
||||
state.raw.players = json
|
||||
for (const player of json) {
|
||||
state.players.push({ name: player.name, ping: player.ping })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,181 +1,181 @@
|
|||
import Core from './core.js'
|
||||
|
||||
const stringKeys = new Set([
|
||||
'website',
|
||||
'gametype',
|
||||
'gamemode',
|
||||
'player'
|
||||
])
|
||||
|
||||
function normalizeEntry ([key, value]) {
|
||||
key = key.toLowerCase()
|
||||
const split = key.split('_')
|
||||
let keyType = key
|
||||
|
||||
if (split.length === 2 && !isNaN(Number(split[1]))) {
|
||||
keyType = split[0]
|
||||
}
|
||||
|
||||
if (!stringKeys.has(keyType) && !keyType.includes('name')) { // todo! the latter check might be problematic, fails on key "name_tag_distance_scope"
|
||||
if (value.toLowerCase() === 'true') {
|
||||
value = true
|
||||
} else if (value.toLowerCase() === 'false') {
|
||||
value = false
|
||||
} else if (value.length && !isNaN(Number(value))) {
|
||||
value = Number(value)
|
||||
}
|
||||
}
|
||||
|
||||
return [key, value]
|
||||
}
|
||||
|
||||
export default class gamespy1 extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'latin1'
|
||||
this.byteorder = 'be'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
const raw = await this.sendPacket('\\status\\xserverquery')
|
||||
// Convert all keys to lowercase and normalize value types
|
||||
const data = Object.fromEntries(Object.entries(raw).map(entry => normalizeEntry(entry)))
|
||||
state.raw = data
|
||||
if ('hostname' in data) state.name = data.hostname
|
||||
if ('mapname' in data) state.map = data.mapname
|
||||
if (this.trueTest(data.password)) state.password = true
|
||||
if ('maxplayers' in data) state.maxplayers = Number(data.maxplayers)
|
||||
if ('hostport' in data) state.gamePort = Number(data.hostport)
|
||||
|
||||
const teamOffByOne = data.gamename === 'bfield1942'
|
||||
const playersById = {}
|
||||
const teamNamesById = {}
|
||||
for (const ident of Object.keys(data)) {
|
||||
const split = ident.split('_')
|
||||
if (split.length !== 2) continue
|
||||
let key = split[0].toLowerCase()
|
||||
const id = Number(split[1])
|
||||
if (isNaN(id)) continue
|
||||
let value = data[ident]
|
||||
|
||||
delete data[ident]
|
||||
|
||||
if (key !== 'team' && key.startsWith('team')) {
|
||||
// Info about a team
|
||||
if (key === 'teamname') {
|
||||
teamNamesById[id] = value
|
||||
} else {
|
||||
// other team info which we don't track
|
||||
}
|
||||
} else {
|
||||
// Info about a player
|
||||
if (!(id in playersById)) playersById[id] = {}
|
||||
|
||||
if (key === 'playername' || key === 'player') {
|
||||
key = 'name'
|
||||
}
|
||||
if (key === 'team' && !isNaN(value)) { // todo! technically, this NaN check isn't needed.
|
||||
key = 'teamId'
|
||||
value += teamOffByOne ? -1 : 0
|
||||
}
|
||||
|
||||
playersById[id][key] = value
|
||||
}
|
||||
}
|
||||
state.raw.teams = teamNamesById
|
||||
|
||||
const players = Object.values(playersById)
|
||||
|
||||
const seenHashes = new Set()
|
||||
for (const player of players) {
|
||||
// Some servers (bf1942) report the same player multiple times (bug?)
|
||||
// Ignore these duplicates
|
||||
if (player.keyhash) {
|
||||
if (seenHashes.has(player.keyhash)) {
|
||||
this.logger.debug('Rejected player with hash ' + player.keyhash + ' (Duplicate keyhash)')
|
||||
continue
|
||||
} else {
|
||||
seenHashes.add(player.keyhash)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert player's team ID to team name if possible
|
||||
if (Object.prototype.hasOwnProperty.call(player, 'teamId')) {
|
||||
if (Object.keys(teamNamesById).length) {
|
||||
player.team = teamNamesById[player.teamId] || ''
|
||||
} else {
|
||||
player.team = player.teamId
|
||||
delete player.teamId
|
||||
}
|
||||
}
|
||||
|
||||
state.players.push(player)
|
||||
}
|
||||
|
||||
state.numplayers = state.players.length
|
||||
}
|
||||
|
||||
async sendPacket (type) {
|
||||
let receivedQueryId
|
||||
const output = {}
|
||||
const parts = new Set()
|
||||
let maxPartNum = 0
|
||||
|
||||
return await this.udpSend(type, buffer => {
|
||||
const reader = this.reader(buffer)
|
||||
const str = reader.string(buffer.length)
|
||||
const split = str.split('\\')
|
||||
split.shift()
|
||||
const data = {}
|
||||
while (split.length) {
|
||||
const key = split.shift()
|
||||
const value = split.shift() || ''
|
||||
data[key] = value
|
||||
}
|
||||
|
||||
let queryId, partNum
|
||||
const partFinal = ('final' in data)
|
||||
if (data.queryid) {
|
||||
const split = data.queryid.split('.')
|
||||
if (split.length >= 2) {
|
||||
partNum = Number(split[1])
|
||||
}
|
||||
queryId = split[0]
|
||||
}
|
||||
delete data.final
|
||||
delete data.queryid
|
||||
this.logger.debug('Received part num=' + partNum + ' queryId=' + queryId + ' final=' + partFinal)
|
||||
|
||||
if (queryId) {
|
||||
if (receivedQueryId && receivedQueryId !== queryId) {
|
||||
this.logger.debug('Rejected packet (Wrong query ID)')
|
||||
return
|
||||
} else if (!receivedQueryId) {
|
||||
receivedQueryId = queryId
|
||||
}
|
||||
}
|
||||
if (!partNum) {
|
||||
partNum = parts.size
|
||||
this.logger.debug('No part number received (assigned #' + partNum + ')')
|
||||
}
|
||||
if (parts.has(partNum)) {
|
||||
this.logger.debug('Rejected packet (Duplicate part)')
|
||||
return
|
||||
}
|
||||
parts.add(partNum)
|
||||
if (partFinal) {
|
||||
maxPartNum = partNum
|
||||
}
|
||||
|
||||
this.logger.debug('Received part #' + partNum + ' of ' + (maxPartNum || '?'))
|
||||
for (const i of Object.keys(data)) {
|
||||
output[i] = data[i]
|
||||
}
|
||||
if (maxPartNum && parts.size === maxPartNum) {
|
||||
this.logger.debug('Received all parts')
|
||||
this.logger.debug(output)
|
||||
return output
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
const stringKeys = new Set([
|
||||
'website',
|
||||
'gametype',
|
||||
'gamemode',
|
||||
'player'
|
||||
])
|
||||
|
||||
function normalizeEntry ([key, value]) {
|
||||
key = key.toLowerCase()
|
||||
const split = key.split('_')
|
||||
let keyType = key
|
||||
|
||||
if (split.length === 2 && !isNaN(Number(split[1]))) {
|
||||
keyType = split[0]
|
||||
}
|
||||
|
||||
if (!stringKeys.has(keyType) && !keyType.includes('name')) { // todo! the latter check might be problematic, fails on key "name_tag_distance_scope"
|
||||
if (value.toLowerCase() === 'true') {
|
||||
value = true
|
||||
} else if (value.toLowerCase() === 'false') {
|
||||
value = false
|
||||
} else if (value.length && !isNaN(Number(value))) {
|
||||
value = Number(value)
|
||||
}
|
||||
}
|
||||
|
||||
return [key, value]
|
||||
}
|
||||
|
||||
export default class gamespy1 extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'latin1'
|
||||
this.byteorder = 'be'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
const raw = await this.sendPacket('\\status\\xserverquery')
|
||||
// Convert all keys to lowercase and normalize value types
|
||||
const data = Object.fromEntries(Object.entries(raw).map(entry => normalizeEntry(entry)))
|
||||
state.raw = data
|
||||
if ('hostname' in data) state.name = data.hostname
|
||||
if ('mapname' in data) state.map = data.mapname
|
||||
if (this.trueTest(data.password)) state.password = true
|
||||
if ('maxplayers' in data) state.maxplayers = Number(data.maxplayers)
|
||||
if ('hostport' in data) state.gamePort = Number(data.hostport)
|
||||
|
||||
const teamOffByOne = data.gamename === 'bfield1942'
|
||||
const playersById = {}
|
||||
const teamNamesById = {}
|
||||
for (const ident of Object.keys(data)) {
|
||||
const split = ident.split('_')
|
||||
if (split.length !== 2) continue
|
||||
let key = split[0].toLowerCase()
|
||||
const id = Number(split[1])
|
||||
if (isNaN(id)) continue
|
||||
let value = data[ident]
|
||||
|
||||
delete data[ident]
|
||||
|
||||
if (key !== 'team' && key.startsWith('team')) {
|
||||
// Info about a team
|
||||
if (key === 'teamname') {
|
||||
teamNamesById[id] = value
|
||||
} else {
|
||||
// other team info which we don't track
|
||||
}
|
||||
} else {
|
||||
// Info about a player
|
||||
if (!(id in playersById)) playersById[id] = {}
|
||||
|
||||
if (key === 'playername' || key === 'player') {
|
||||
key = 'name'
|
||||
}
|
||||
if (key === 'team' && !isNaN(value)) { // todo! technically, this NaN check isn't needed.
|
||||
key = 'teamId'
|
||||
value += teamOffByOne ? -1 : 0
|
||||
}
|
||||
|
||||
playersById[id][key] = value
|
||||
}
|
||||
}
|
||||
state.raw.teams = teamNamesById
|
||||
|
||||
const players = Object.values(playersById)
|
||||
|
||||
const seenHashes = new Set()
|
||||
for (const player of players) {
|
||||
// Some servers (bf1942) report the same player multiple times (bug?)
|
||||
// Ignore these duplicates
|
||||
if (player.keyhash) {
|
||||
if (seenHashes.has(player.keyhash)) {
|
||||
this.logger.debug('Rejected player with hash ' + player.keyhash + ' (Duplicate keyhash)')
|
||||
continue
|
||||
} else {
|
||||
seenHashes.add(player.keyhash)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert player's team ID to team name if possible
|
||||
if (Object.prototype.hasOwnProperty.call(player, 'teamId')) {
|
||||
if (Object.keys(teamNamesById).length) {
|
||||
player.team = teamNamesById[player.teamId] || ''
|
||||
} else {
|
||||
player.team = player.teamId
|
||||
delete player.teamId
|
||||
}
|
||||
}
|
||||
|
||||
state.players.push(player)
|
||||
}
|
||||
|
||||
state.numplayers = state.players.length
|
||||
}
|
||||
|
||||
async sendPacket (type) {
|
||||
let receivedQueryId
|
||||
const output = {}
|
||||
const parts = new Set()
|
||||
let maxPartNum = 0
|
||||
|
||||
return await this.udpSend(type, buffer => {
|
||||
const reader = this.reader(buffer)
|
||||
const str = reader.string(buffer.length)
|
||||
const split = str.split('\\')
|
||||
split.shift()
|
||||
const data = {}
|
||||
while (split.length) {
|
||||
const key = split.shift()
|
||||
const value = split.shift() || ''
|
||||
data[key] = value
|
||||
}
|
||||
|
||||
let queryId, partNum
|
||||
const partFinal = ('final' in data)
|
||||
if (data.queryid) {
|
||||
const split = data.queryid.split('.')
|
||||
if (split.length >= 2) {
|
||||
partNum = Number(split[1])
|
||||
}
|
||||
queryId = split[0]
|
||||
}
|
||||
delete data.final
|
||||
delete data.queryid
|
||||
this.logger.debug('Received part num=' + partNum + ' queryId=' + queryId + ' final=' + partFinal)
|
||||
|
||||
if (queryId) {
|
||||
if (receivedQueryId && receivedQueryId !== queryId) {
|
||||
this.logger.debug('Rejected packet (Wrong query ID)')
|
||||
return
|
||||
} else if (!receivedQueryId) {
|
||||
receivedQueryId = queryId
|
||||
}
|
||||
}
|
||||
if (!partNum) {
|
||||
partNum = parts.size
|
||||
this.logger.debug('No part number received (assigned #' + partNum + ')')
|
||||
}
|
||||
if (parts.has(partNum)) {
|
||||
this.logger.debug('Rejected packet (Duplicate part)')
|
||||
return
|
||||
}
|
||||
parts.add(partNum)
|
||||
if (partFinal) {
|
||||
maxPartNum = partNum
|
||||
}
|
||||
|
||||
this.logger.debug('Received part #' + partNum + ' of ' + (maxPartNum || '?'))
|
||||
for (const i of Object.keys(data)) {
|
||||
output[i] = data[i]
|
||||
}
|
||||
if (maxPartNum && parts.size === maxPartNum) {
|
||||
this.logger.debug('Received all parts')
|
||||
this.logger.debug(output)
|
||||
return output
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,144 +1,144 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class gamespy2 extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'latin1'
|
||||
this.byteorder = 'be'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
// Parse info
|
||||
{
|
||||
const body = await this.sendPacket([0xff, 0, 0])
|
||||
const reader = this.reader(body)
|
||||
while (!reader.done()) {
|
||||
const key = reader.string()
|
||||
const value = reader.string()
|
||||
if (!key) break
|
||||
state.raw[key] = value
|
||||
}
|
||||
if ('hostname' in state.raw) state.name = state.raw.hostname
|
||||
if ('mapname' in state.raw) state.map = state.raw.mapname
|
||||
if (this.trueTest(state.raw.password)) state.password = true
|
||||
if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers)
|
||||
if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport)
|
||||
}
|
||||
|
||||
// Parse players
|
||||
{
|
||||
const body = await this.sendPacket([0, 0xff, 0])
|
||||
const reader = this.reader(body)
|
||||
for (const rawPlayer of this.readFieldData(reader)) {
|
||||
state.players.push(rawPlayer)
|
||||
}
|
||||
|
||||
if ('numplayers' in state.raw) state.numplayers = parseInt(state.raw.numplayers)
|
||||
else state.numplayers = state.players.length
|
||||
}
|
||||
|
||||
// Parse teams
|
||||
{
|
||||
const body = await this.sendPacket([0, 0, 0xff])
|
||||
const reader = this.reader(body)
|
||||
state.raw.teams = this.readFieldData(reader)
|
||||
}
|
||||
|
||||
// Special case for america's army 1 and 2
|
||||
// both use gamename = "armygame"
|
||||
if (state.raw.gamename === 'armygame') {
|
||||
const stripColor = (str) => {
|
||||
// uses unreal 2 color codes
|
||||
return str.replace(/\x1b...|[\x00-\x1a]/g, '')
|
||||
}
|
||||
state.name = stripColor(state.name)
|
||||
state.map = stripColor(state.map)
|
||||
for (const key of Object.keys(state.raw)) {
|
||||
if (typeof state.raw[key] === 'string') {
|
||||
state.raw[key] = stripColor(state.raw[key])
|
||||
}
|
||||
}
|
||||
for (const player of state.players) {
|
||||
if (!('name' in player)) continue
|
||||
player.name = stripColor(player.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async sendPacket (type) {
|
||||
const request = Buffer.concat([
|
||||
Buffer.from([0xfe, 0xfd, 0x00]), // gamespy2
|
||||
Buffer.from([0x00, 0x00, 0x00, 0x01]), // ping ID
|
||||
Buffer.from(type)
|
||||
])
|
||||
return await this.udpSend(request, buffer => {
|
||||
const reader = this.reader(buffer)
|
||||
const header = reader.uint(1)
|
||||
if (header !== 0) return
|
||||
const pingId = reader.uint(4)
|
||||
if (pingId !== 1) return
|
||||
return reader.rest()
|
||||
})
|
||||
}
|
||||
|
||||
readFieldData (reader) {
|
||||
reader.uint(1) // always 0
|
||||
const count = reader.uint(1) // number of rows in this data
|
||||
|
||||
// some games omit the count byte entirely if it's 0 or at random (like americas army)
|
||||
// Luckily, count should always be <64, and ascii characters will typically be >64,
|
||||
// so we can detect this.
|
||||
if (count > 64) {
|
||||
reader.skip(-1)
|
||||
this.logger.debug('Detected missing count byte, rewinding by 1')
|
||||
} else {
|
||||
this.logger.debug('Detected row count: ' + count)
|
||||
}
|
||||
|
||||
this.logger.debug(() => 'Reading fields, starting at: ' + reader.rest())
|
||||
|
||||
const fields = []
|
||||
while (!reader.done()) {
|
||||
const field = reader.string()
|
||||
if (!field) break
|
||||
fields.push(field)
|
||||
this.logger.debug('field:' + field)
|
||||
}
|
||||
|
||||
if (!fields.length) return []
|
||||
|
||||
const units = []
|
||||
while (!reader.done()) {
|
||||
const unit = {}
|
||||
for (let iField = 0; iField < fields.length; iField++) {
|
||||
let key = fields[iField]
|
||||
let value = reader.string()
|
||||
if (!value && iField === 0) return units
|
||||
|
||||
this.logger.debug('value:' + value)
|
||||
if (key === 'player_') key = 'name'
|
||||
else if (key === 'score_') key = 'score'
|
||||
else if (key === 'deaths_') key = 'deaths'
|
||||
else if (key === 'ping_') key = 'ping'
|
||||
else if (key === 'team_') key = 'team'
|
||||
else if (key === 'kills_') key = 'kills'
|
||||
else if (key === 'team_t') key = 'name'
|
||||
else if (key === 'tickets_t') key = 'tickets'
|
||||
|
||||
if (
|
||||
key === 'score' || key === 'deaths' ||
|
||||
key === 'ping' || key === 'team' ||
|
||||
key === 'kills' || key === 'tickets'
|
||||
) {
|
||||
if (value === '') continue
|
||||
value = parseInt(value)
|
||||
}
|
||||
|
||||
unit[key] = value
|
||||
}
|
||||
units.push(unit)
|
||||
}
|
||||
|
||||
return units
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class gamespy2 extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'latin1'
|
||||
this.byteorder = 'be'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
// Parse info
|
||||
{
|
||||
const body = await this.sendPacket([0xff, 0, 0])
|
||||
const reader = this.reader(body)
|
||||
while (!reader.done()) {
|
||||
const key = reader.string()
|
||||
const value = reader.string()
|
||||
if (!key) break
|
||||
state.raw[key] = value
|
||||
}
|
||||
if ('hostname' in state.raw) state.name = state.raw.hostname
|
||||
if ('mapname' in state.raw) state.map = state.raw.mapname
|
||||
if (this.trueTest(state.raw.password)) state.password = true
|
||||
if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers)
|
||||
if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport)
|
||||
}
|
||||
|
||||
// Parse players
|
||||
{
|
||||
const body = await this.sendPacket([0, 0xff, 0])
|
||||
const reader = this.reader(body)
|
||||
for (const rawPlayer of this.readFieldData(reader)) {
|
||||
state.players.push(rawPlayer)
|
||||
}
|
||||
|
||||
if ('numplayers' in state.raw) state.numplayers = parseInt(state.raw.numplayers)
|
||||
else state.numplayers = state.players.length
|
||||
}
|
||||
|
||||
// Parse teams
|
||||
{
|
||||
const body = await this.sendPacket([0, 0, 0xff])
|
||||
const reader = this.reader(body)
|
||||
state.raw.teams = this.readFieldData(reader)
|
||||
}
|
||||
|
||||
// Special case for america's army 1 and 2
|
||||
// both use gamename = "armygame"
|
||||
if (state.raw.gamename === 'armygame') {
|
||||
const stripColor = (str) => {
|
||||
// uses unreal 2 color codes
|
||||
return str.replace(/\x1b...|[\x00-\x1a]/g, '')
|
||||
}
|
||||
state.name = stripColor(state.name)
|
||||
state.map = stripColor(state.map)
|
||||
for (const key of Object.keys(state.raw)) {
|
||||
if (typeof state.raw[key] === 'string') {
|
||||
state.raw[key] = stripColor(state.raw[key])
|
||||
}
|
||||
}
|
||||
for (const player of state.players) {
|
||||
if (!('name' in player)) continue
|
||||
player.name = stripColor(player.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async sendPacket (type) {
|
||||
const request = Buffer.concat([
|
||||
Buffer.from([0xfe, 0xfd, 0x00]), // gamespy2
|
||||
Buffer.from([0x00, 0x00, 0x00, 0x01]), // ping ID
|
||||
Buffer.from(type)
|
||||
])
|
||||
return await this.udpSend(request, buffer => {
|
||||
const reader = this.reader(buffer)
|
||||
const header = reader.uint(1)
|
||||
if (header !== 0) return
|
||||
const pingId = reader.uint(4)
|
||||
if (pingId !== 1) return
|
||||
return reader.rest()
|
||||
})
|
||||
}
|
||||
|
||||
readFieldData (reader) {
|
||||
reader.uint(1) // always 0
|
||||
const count = reader.uint(1) // number of rows in this data
|
||||
|
||||
// some games omit the count byte entirely if it's 0 or at random (like americas army)
|
||||
// Luckily, count should always be <64, and ascii characters will typically be >64,
|
||||
// so we can detect this.
|
||||
if (count > 64) {
|
||||
reader.skip(-1)
|
||||
this.logger.debug('Detected missing count byte, rewinding by 1')
|
||||
} else {
|
||||
this.logger.debug('Detected row count: ' + count)
|
||||
}
|
||||
|
||||
this.logger.debug(() => 'Reading fields, starting at: ' + reader.rest())
|
||||
|
||||
const fields = []
|
||||
while (!reader.done()) {
|
||||
const field = reader.string()
|
||||
if (!field) break
|
||||
fields.push(field)
|
||||
this.logger.debug('field:' + field)
|
||||
}
|
||||
|
||||
if (!fields.length) return []
|
||||
|
||||
const units = []
|
||||
while (!reader.done()) {
|
||||
const unit = {}
|
||||
for (let iField = 0; iField < fields.length; iField++) {
|
||||
let key = fields[iField]
|
||||
let value = reader.string()
|
||||
if (!value && iField === 0) return units
|
||||
|
||||
this.logger.debug('value:' + value)
|
||||
if (key === 'player_') key = 'name'
|
||||
else if (key === 'score_') key = 'score'
|
||||
else if (key === 'deaths_') key = 'deaths'
|
||||
else if (key === 'ping_') key = 'ping'
|
||||
else if (key === 'team_') key = 'team'
|
||||
else if (key === 'kills_') key = 'kills'
|
||||
else if (key === 'team_t') key = 'name'
|
||||
else if (key === 'tickets_t') key = 'tickets'
|
||||
|
||||
if (
|
||||
key === 'score' || key === 'deaths' ||
|
||||
key === 'ping' || key === 'team' ||
|
||||
key === 'kills' || key === 'tickets'
|
||||
) {
|
||||
if (value === '') continue
|
||||
value = parseInt(value)
|
||||
}
|
||||
|
||||
unit[key] = value
|
||||
}
|
||||
units.push(unit)
|
||||
}
|
||||
|
||||
return units
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,197 +1,197 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class gamespy3 extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.sessionId = 1
|
||||
this.encoding = 'latin1'
|
||||
this.byteorder = 'be'
|
||||
this.useOnlySingleSplit = false
|
||||
this.isJc2mp = false
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
const buffer = await this.sendPacket(9, false, false, false)
|
||||
const reader = this.reader(buffer)
|
||||
let challenge = parseInt(reader.string())
|
||||
this.logger.debug('Received challenge key: ' + challenge)
|
||||
if (challenge === 0) {
|
||||
// Some servers send us a 0 if they don't want a challenge key used
|
||||
// BF2 does this.
|
||||
challenge = null
|
||||
}
|
||||
|
||||
let requestPayload
|
||||
if (this.isJc2mp) {
|
||||
// they completely alter the protocol. because why not.
|
||||
requestPayload = Buffer.from([0xff, 0xff, 0xff, 0x02])
|
||||
} else {
|
||||
requestPayload = Buffer.from([0xff, 0xff, 0xff, 0x01])
|
||||
}
|
||||
/** @type Buffer[] */
|
||||
const packets = await this.sendPacket(0, challenge, requestPayload, true)
|
||||
|
||||
// iterate over the received packets
|
||||
// the first packet will start off with k/v pairs, followed with data fields
|
||||
// the following packets will only have data fields
|
||||
state.raw.playerTeamInfo = {}
|
||||
|
||||
for (let iPacket = 0; iPacket < packets.length; iPacket++) {
|
||||
const packet = packets[iPacket]
|
||||
const reader = this.reader(packet)
|
||||
|
||||
this.logger.debug('Parsing packet #' + iPacket)
|
||||
this.logger.debug(packet)
|
||||
|
||||
// Parse raw server key/values
|
||||
|
||||
if (iPacket === 0) {
|
||||
while (!reader.done()) {
|
||||
const key = reader.string()
|
||||
if (!key) break
|
||||
|
||||
let value = reader.string()
|
||||
while (value.match(/^p[0-9]+$/)) {
|
||||
// fix a weird ut3 bug where some keys don't have values
|
||||
value = reader.string()
|
||||
}
|
||||
|
||||
state.raw[key] = value
|
||||
this.logger.debug(key + ' = ' + value)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse player, team, item array state
|
||||
|
||||
if (this.isJc2mp) {
|
||||
state.raw.numPlayers2 = reader.uint(2)
|
||||
while (!reader.done()) {
|
||||
const player = {}
|
||||
player.name = reader.string()
|
||||
player.steamid = reader.string()
|
||||
player.ping = reader.uint(2)
|
||||
state.players.push(player)
|
||||
}
|
||||
} else {
|
||||
while (!reader.done()) {
|
||||
if (reader.uint(1) <= 2) continue
|
||||
reader.skip(-1)
|
||||
const fieldId = reader.string()
|
||||
if (!fieldId) continue
|
||||
const fieldIdSplit = fieldId.split('_')
|
||||
const fieldName = fieldIdSplit[0]
|
||||
const itemType = fieldIdSplit.length > 1 ? fieldIdSplit[1] : 'no_'
|
||||
|
||||
if (!(itemType in state.raw.playerTeamInfo)) {
|
||||
state.raw.playerTeamInfo[itemType] = []
|
||||
}
|
||||
const items = state.raw.playerTeamInfo[itemType]
|
||||
|
||||
let offset = reader.uint(1)
|
||||
|
||||
this.logger.debug(() => 'Parsing new field: itemType=' + itemType + ' fieldName=' + fieldName + ' startOffset=' + offset)
|
||||
|
||||
while (!reader.done()) {
|
||||
const item = reader.string()
|
||||
if (!item) break
|
||||
|
||||
while (items.length <= offset) { items.push({}) }
|
||||
items[offset][fieldName] = item
|
||||
this.logger.debug('* ' + item)
|
||||
offset++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Turn all that raw state into something useful
|
||||
|
||||
if ('hostname' in state.raw) state.name = state.raw.hostname
|
||||
else if ('servername' in state.raw) state.name = state.raw.servername
|
||||
if ('mapname' in state.raw) state.map = state.raw.mapname
|
||||
if (state.raw.password === '1') state.password = true
|
||||
if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers)
|
||||
if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport)
|
||||
|
||||
if ('' in state.raw.playerTeamInfo) {
|
||||
for (const playerInfo of state.raw.playerTeamInfo['']) {
|
||||
const player = {}
|
||||
for (const from of Object.keys(playerInfo)) {
|
||||
let key = from
|
||||
let value = playerInfo[from]
|
||||
|
||||
if (key === 'player') key = 'name'
|
||||
if (key === 'score' || key === 'ping' || key === 'team' || key === 'deaths' || key === 'pid') value = parseInt(value)
|
||||
player[key] = value
|
||||
}
|
||||
state.players.push(player)
|
||||
}
|
||||
}
|
||||
|
||||
if ('numplayers' in state.raw) state.numplayers = parseInt(state.raw.numplayers)
|
||||
else state.numplayers = state.players.length
|
||||
}
|
||||
|
||||
async sendPacket (type, challenge, payload, assemble) {
|
||||
const challengeLength = challenge === null ? 0 : 4
|
||||
const payloadLength = payload ? payload.length : 0
|
||||
|
||||
const b = Buffer.alloc(7 + challengeLength + payloadLength)
|
||||
b.writeUInt8(0xFE, 0)
|
||||
b.writeUInt8(0xFD, 1)
|
||||
b.writeUInt8(type, 2)
|
||||
b.writeUInt32BE(this.sessionId, 3)
|
||||
if (challengeLength) b.writeInt32BE(challenge, 7)
|
||||
if (payloadLength) payload.copy(b, 7 + challengeLength)
|
||||
|
||||
let numPackets = 0
|
||||
const packets = {}
|
||||
return await this.udpSend(b, (buffer) => {
|
||||
const reader = this.reader(buffer)
|
||||
const iType = reader.uint(1)
|
||||
if (iType !== type) {
|
||||
this.logger.debug('Skipping packet, type mismatch')
|
||||
return
|
||||
}
|
||||
const iSessionId = reader.uint(4)
|
||||
if (iSessionId !== this.sessionId) {
|
||||
this.logger.debug('Skipping packet, session id mismatch')
|
||||
return
|
||||
}
|
||||
|
||||
if (!assemble) {
|
||||
return reader.rest()
|
||||
}
|
||||
if (this.useOnlySingleSplit) {
|
||||
// has split headers, but they are worthless and only one packet is used
|
||||
reader.skip(11)
|
||||
return [reader.rest()]
|
||||
}
|
||||
|
||||
reader.skip(9) // filler data -- usually set to 'splitnum\0'
|
||||
let id = reader.uint(1)
|
||||
const last = (id & 0x80)
|
||||
id = id & 0x7f
|
||||
if (last) numPackets = id + 1
|
||||
|
||||
reader.skip(1) // "another 'packet number' byte, but isn't understood."
|
||||
|
||||
packets[id] = reader.rest()
|
||||
if (this.debug) {
|
||||
this.logger.debug('Received packet #' + id + (last ? ' (last)' : ''))
|
||||
}
|
||||
|
||||
if (!numPackets || Object.keys(packets).length !== numPackets) return
|
||||
|
||||
// assemble the parts
|
||||
const list = []
|
||||
for (let i = 0; i < numPackets; i++) {
|
||||
if (!(i in packets)) {
|
||||
throw new Error('Missing packet #' + i)
|
||||
}
|
||||
list.push(packets[i])
|
||||
}
|
||||
return list
|
||||
})
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class gamespy3 extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.sessionId = 1
|
||||
this.encoding = 'latin1'
|
||||
this.byteorder = 'be'
|
||||
this.useOnlySingleSplit = false
|
||||
this.isJc2mp = false
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
const buffer = await this.sendPacket(9, false, false, false)
|
||||
const reader = this.reader(buffer)
|
||||
let challenge = parseInt(reader.string())
|
||||
this.logger.debug('Received challenge key: ' + challenge)
|
||||
if (challenge === 0) {
|
||||
// Some servers send us a 0 if they don't want a challenge key used
|
||||
// BF2 does this.
|
||||
challenge = null
|
||||
}
|
||||
|
||||
let requestPayload
|
||||
if (this.isJc2mp) {
|
||||
// they completely alter the protocol. because why not.
|
||||
requestPayload = Buffer.from([0xff, 0xff, 0xff, 0x02])
|
||||
} else {
|
||||
requestPayload = Buffer.from([0xff, 0xff, 0xff, 0x01])
|
||||
}
|
||||
/** @type Buffer[] */
|
||||
const packets = await this.sendPacket(0, challenge, requestPayload, true)
|
||||
|
||||
// iterate over the received packets
|
||||
// the first packet will start off with k/v pairs, followed with data fields
|
||||
// the following packets will only have data fields
|
||||
state.raw.playerTeamInfo = {}
|
||||
|
||||
for (let iPacket = 0; iPacket < packets.length; iPacket++) {
|
||||
const packet = packets[iPacket]
|
||||
const reader = this.reader(packet)
|
||||
|
||||
this.logger.debug('Parsing packet #' + iPacket)
|
||||
this.logger.debug(packet)
|
||||
|
||||
// Parse raw server key/values
|
||||
|
||||
if (iPacket === 0) {
|
||||
while (!reader.done()) {
|
||||
const key = reader.string()
|
||||
if (!key) break
|
||||
|
||||
let value = reader.string()
|
||||
while (value.match(/^p[0-9]+$/)) {
|
||||
// fix a weird ut3 bug where some keys don't have values
|
||||
value = reader.string()
|
||||
}
|
||||
|
||||
state.raw[key] = value
|
||||
this.logger.debug(key + ' = ' + value)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse player, team, item array state
|
||||
|
||||
if (this.isJc2mp) {
|
||||
state.raw.numPlayers2 = reader.uint(2)
|
||||
while (!reader.done()) {
|
||||
const player = {}
|
||||
player.name = reader.string()
|
||||
player.steamid = reader.string()
|
||||
player.ping = reader.uint(2)
|
||||
state.players.push(player)
|
||||
}
|
||||
} else {
|
||||
while (!reader.done()) {
|
||||
if (reader.uint(1) <= 2) continue
|
||||
reader.skip(-1)
|
||||
const fieldId = reader.string()
|
||||
if (!fieldId) continue
|
||||
const fieldIdSplit = fieldId.split('_')
|
||||
const fieldName = fieldIdSplit[0]
|
||||
const itemType = fieldIdSplit.length > 1 ? fieldIdSplit[1] : 'no_'
|
||||
|
||||
if (!(itemType in state.raw.playerTeamInfo)) {
|
||||
state.raw.playerTeamInfo[itemType] = []
|
||||
}
|
||||
const items = state.raw.playerTeamInfo[itemType]
|
||||
|
||||
let offset = reader.uint(1)
|
||||
|
||||
this.logger.debug(() => 'Parsing new field: itemType=' + itemType + ' fieldName=' + fieldName + ' startOffset=' + offset)
|
||||
|
||||
while (!reader.done()) {
|
||||
const item = reader.string()
|
||||
if (!item) break
|
||||
|
||||
while (items.length <= offset) { items.push({}) }
|
||||
items[offset][fieldName] = item
|
||||
this.logger.debug('* ' + item)
|
||||
offset++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Turn all that raw state into something useful
|
||||
|
||||
if ('hostname' in state.raw) state.name = state.raw.hostname
|
||||
else if ('servername' in state.raw) state.name = state.raw.servername
|
||||
if ('mapname' in state.raw) state.map = state.raw.mapname
|
||||
if (state.raw.password === '1') state.password = true
|
||||
if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers)
|
||||
if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport)
|
||||
|
||||
if ('' in state.raw.playerTeamInfo) {
|
||||
for (const playerInfo of state.raw.playerTeamInfo['']) {
|
||||
const player = {}
|
||||
for (const from of Object.keys(playerInfo)) {
|
||||
let key = from
|
||||
let value = playerInfo[from]
|
||||
|
||||
if (key === 'player') key = 'name'
|
||||
if (key === 'score' || key === 'ping' || key === 'team' || key === 'deaths' || key === 'pid') value = parseInt(value)
|
||||
player[key] = value
|
||||
}
|
||||
state.players.push(player)
|
||||
}
|
||||
}
|
||||
|
||||
if ('numplayers' in state.raw) state.numplayers = parseInt(state.raw.numplayers)
|
||||
else state.numplayers = state.players.length
|
||||
}
|
||||
|
||||
async sendPacket (type, challenge, payload, assemble) {
|
||||
const challengeLength = challenge === null ? 0 : 4
|
||||
const payloadLength = payload ? payload.length : 0
|
||||
|
||||
const b = Buffer.alloc(7 + challengeLength + payloadLength)
|
||||
b.writeUInt8(0xFE, 0)
|
||||
b.writeUInt8(0xFD, 1)
|
||||
b.writeUInt8(type, 2)
|
||||
b.writeUInt32BE(this.sessionId, 3)
|
||||
if (challengeLength) b.writeInt32BE(challenge, 7)
|
||||
if (payloadLength) payload.copy(b, 7 + challengeLength)
|
||||
|
||||
let numPackets = 0
|
||||
const packets = {}
|
||||
return await this.udpSend(b, (buffer) => {
|
||||
const reader = this.reader(buffer)
|
||||
const iType = reader.uint(1)
|
||||
if (iType !== type) {
|
||||
this.logger.debug('Skipping packet, type mismatch')
|
||||
return
|
||||
}
|
||||
const iSessionId = reader.uint(4)
|
||||
if (iSessionId !== this.sessionId) {
|
||||
this.logger.debug('Skipping packet, session id mismatch')
|
||||
return
|
||||
}
|
||||
|
||||
if (!assemble) {
|
||||
return reader.rest()
|
||||
}
|
||||
if (this.useOnlySingleSplit) {
|
||||
// has split headers, but they are worthless and only one packet is used
|
||||
reader.skip(11)
|
||||
return [reader.rest()]
|
||||
}
|
||||
|
||||
reader.skip(9) // filler data -- usually set to 'splitnum\0'
|
||||
let id = reader.uint(1)
|
||||
const last = (id & 0x80)
|
||||
id = id & 0x7f
|
||||
if (last) numPackets = id + 1
|
||||
|
||||
reader.skip(1) // "another 'packet number' byte, but isn't understood."
|
||||
|
||||
packets[id] = reader.rest()
|
||||
if (this.debug) {
|
||||
this.logger.debug('Received packet #' + id + (last ? ' (last)' : ''))
|
||||
}
|
||||
|
||||
if (!numPackets || Object.keys(packets).length !== numPackets) return
|
||||
|
||||
// assemble the parts
|
||||
const list = []
|
||||
for (let i = 0; i < numPackets; i++) {
|
||||
if (!(i in packets)) {
|
||||
throw new Error('Missing packet #' + i)
|
||||
}
|
||||
list.push(packets[i])
|
||||
}
|
||||
return list
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,46 +1,46 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class geneshift extends Core {
|
||||
async run (state) {
|
||||
await this.tcpPing()
|
||||
|
||||
const body = await this.request({
|
||||
url: 'http://geneshift.net/game/receiveLobby.php'
|
||||
})
|
||||
|
||||
const split = body.split('<br/>')
|
||||
let found = null
|
||||
for (const line of split) {
|
||||
const fields = line.split('::')
|
||||
const ip = fields[2]
|
||||
const port = fields[3]
|
||||
if (ip === this.options.address && parseInt(port) === this.options.port) {
|
||||
found = fields
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (found === null) {
|
||||
throw new Error('Server not found in list')
|
||||
}
|
||||
|
||||
state.raw.countrycode = found[0]
|
||||
state.raw.country = found[1]
|
||||
state.name = found[4]
|
||||
state.map = found[5]
|
||||
state.numplayers = parseInt(found[6])
|
||||
state.maxplayers = parseInt(found[7])
|
||||
// fields[8] is unknown?
|
||||
state.raw.rules = found[9]
|
||||
state.raw.gamemode = parseInt(found[10])
|
||||
state.raw.gangsters = parseInt(found[11])
|
||||
state.raw.cashrate = parseInt(found[12])
|
||||
state.raw.missions = !!parseInt(found[13])
|
||||
state.raw.vehicles = !!parseInt(found[14])
|
||||
state.raw.customweapons = !!parseInt(found[15])
|
||||
state.raw.friendlyfire = !!parseInt(found[16])
|
||||
state.raw.mercs = !!parseInt(found[17])
|
||||
// fields[18] is unknown? listen server?
|
||||
state.raw.version = found[19]
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class geneshift extends Core {
|
||||
async run (state) {
|
||||
await this.tcpPing()
|
||||
|
||||
const body = await this.request({
|
||||
url: 'http://geneshift.net/game/receiveLobby.php'
|
||||
})
|
||||
|
||||
const split = body.split('<br/>')
|
||||
let found = null
|
||||
for (const line of split) {
|
||||
const fields = line.split('::')
|
||||
const ip = fields[2]
|
||||
const port = fields[3]
|
||||
if (ip === this.options.address && parseInt(port) === this.options.port) {
|
||||
found = fields
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (found === null) {
|
||||
throw new Error('Server not found in list')
|
||||
}
|
||||
|
||||
state.raw.countrycode = found[0]
|
||||
state.raw.country = found[1]
|
||||
state.name = found[4]
|
||||
state.map = found[5]
|
||||
state.numplayers = parseInt(found[6])
|
||||
state.maxplayers = parseInt(found[7])
|
||||
// fields[8] is unknown?
|
||||
state.raw.rules = found[9]
|
||||
state.raw.gamemode = parseInt(found[10])
|
||||
state.raw.gangsters = parseInt(found[11])
|
||||
state.raw.cashrate = parseInt(found[12])
|
||||
state.raw.missions = !!parseInt(found[13])
|
||||
state.raw.vehicles = !!parseInt(found[14])
|
||||
state.raw.customweapons = !!parseInt(found[15])
|
||||
state.raw.friendlyfire = !!parseInt(found[16])
|
||||
state.raw.mercs = !!parseInt(found[17])
|
||||
// fields[18] is unknown? listen server?
|
||||
state.raw.version = found[19]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import valve from './valve.js'
|
||||
|
||||
export default class goldsrc extends valve {
|
||||
constructor () {
|
||||
super()
|
||||
this.goldsrcInfo = true
|
||||
}
|
||||
}
|
||||
import valve from './valve.js'
|
||||
|
||||
export default class goldsrc extends valve {
|
||||
constructor () {
|
||||
super()
|
||||
this.goldsrcInfo = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import quake1 from './quake1.js'
|
||||
|
||||
export default class hexen2 extends quake1 {
|
||||
constructor () {
|
||||
super()
|
||||
this.sendHeader = '\xFFstatus\x0a'
|
||||
this.responseHeader = '\xffn'
|
||||
import quake1 from './quake1.js'
|
||||
|
||||
export default class hexen2 extends quake1 {
|
||||
constructor () {
|
||||
super()
|
||||
this.sendHeader = '\xFFstatus\x0a'
|
||||
this.responseHeader = '\xffn'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
await super.run(state)
|
||||
state.gamePort = this.options.port - 50
|
||||
}
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
await super.run(state)
|
||||
state.gamePort = this.options.port - 50
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,57 +1,57 @@
|
|||
import armagetron from './armagetron.js'
|
||||
import ase from './ase.js'
|
||||
import asa from './asa.js'
|
||||
import assettocorsa from './assettocorsa.js'
|
||||
import battlefield from './battlefield.js'
|
||||
import buildandshoot from './buildandshoot.js'
|
||||
import cs2d from './cs2d.js'
|
||||
import discord from './discord.js'
|
||||
import doom3 from './doom3.js'
|
||||
import eco from './eco.js'
|
||||
import epic from './epic.js'
|
||||
import ffow from './ffow.js'
|
||||
import fivem from './fivem.js'
|
||||
import gamespy1 from './gamespy1.js'
|
||||
import gamespy2 from './gamespy2.js'
|
||||
import gamespy3 from './gamespy3.js'
|
||||
import geneshift from './geneshift.js'
|
||||
import goldsrc from './goldsrc.js'
|
||||
import hexen2 from './hexen2.js'
|
||||
import jc2mp from './jc2mp.js'
|
||||
import kspdmp from './kspdmp.js'
|
||||
import mafia2mp from './mafia2mp.js'
|
||||
import mafia2online from './mafia2online.js'
|
||||
import minecraft from './minecraft.js'
|
||||
import minecraftbedrock from './minecraftbedrock.js'
|
||||
import minecraftvanilla from './minecraftvanilla.js'
|
||||
import mumble from './mumble.js'
|
||||
import mumbleping from './mumbleping.js'
|
||||
import nadeo from './nadeo.js'
|
||||
import openttd from './openttd.js'
|
||||
import quake1 from './quake1.js'
|
||||
import quake2 from './quake2.js'
|
||||
import quake3 from './quake3.js'
|
||||
import rfactor from './rfactor.js'
|
||||
import samp from './samp.js'
|
||||
import savage2 from './savage2.js'
|
||||
import starmade from './starmade.js'
|
||||
import starsiege from './starsiege.js'
|
||||
import teamspeak2 from './teamspeak2.js'
|
||||
import teamspeak3 from './teamspeak3.js'
|
||||
import terraria from './terraria.js'
|
||||
import tribes1 from './tribes1.js'
|
||||
import tribes1master from './tribes1master.js'
|
||||
import unreal2 from './unreal2.js'
|
||||
import ut3 from './ut3.js'
|
||||
import valve from './valve.js'
|
||||
import vcmp from './vcmp.js'
|
||||
import ventrilo from './ventrilo.js'
|
||||
import warsow from './warsow.js'
|
||||
|
||||
export {
|
||||
armagetron, ase, asa, assettocorsa, battlefield, buildandshoot, cs2d, discord, doom3, eco, epic, ffow, fivem, gamespy1,
|
||||
gamespy2, gamespy3, geneshift, goldsrc, hexen2, jc2mp, kspdmp, mafia2mp, mafia2online, minecraft,
|
||||
minecraftbedrock, minecraftvanilla, mumble, mumbleping, nadeo, openttd, quake1, quake2, quake3, rfactor, samp,
|
||||
savage2, starmade, starsiege, teamspeak2, teamspeak3, terraria, tribes1, tribes1master, unreal2, ut3, valve,
|
||||
vcmp, ventrilo, warsow
|
||||
}
|
||||
import armagetron from './armagetron.js'
|
||||
import ase from './ase.js'
|
||||
import asa from './asa.js'
|
||||
import assettocorsa from './assettocorsa.js'
|
||||
import battlefield from './battlefield.js'
|
||||
import buildandshoot from './buildandshoot.js'
|
||||
import cs2d from './cs2d.js'
|
||||
import discord from './discord.js'
|
||||
import doom3 from './doom3.js'
|
||||
import eco from './eco.js'
|
||||
import epic from './epic.js'
|
||||
import ffow from './ffow.js'
|
||||
import fivem from './fivem.js'
|
||||
import gamespy1 from './gamespy1.js'
|
||||
import gamespy2 from './gamespy2.js'
|
||||
import gamespy3 from './gamespy3.js'
|
||||
import geneshift from './geneshift.js'
|
||||
import goldsrc from './goldsrc.js'
|
||||
import hexen2 from './hexen2.js'
|
||||
import jc2mp from './jc2mp.js'
|
||||
import kspdmp from './kspdmp.js'
|
||||
import mafia2mp from './mafia2mp.js'
|
||||
import mafia2online from './mafia2online.js'
|
||||
import minecraft from './minecraft.js'
|
||||
import minecraftbedrock from './minecraftbedrock.js'
|
||||
import minecraftvanilla from './minecraftvanilla.js'
|
||||
import mumble from './mumble.js'
|
||||
import mumbleping from './mumbleping.js'
|
||||
import nadeo from './nadeo.js'
|
||||
import openttd from './openttd.js'
|
||||
import quake1 from './quake1.js'
|
||||
import quake2 from './quake2.js'
|
||||
import quake3 from './quake3.js'
|
||||
import rfactor from './rfactor.js'
|
||||
import samp from './samp.js'
|
||||
import savage2 from './savage2.js'
|
||||
import starmade from './starmade.js'
|
||||
import starsiege from './starsiege.js'
|
||||
import teamspeak2 from './teamspeak2.js'
|
||||
import teamspeak3 from './teamspeak3.js'
|
||||
import terraria from './terraria.js'
|
||||
import tribes1 from './tribes1.js'
|
||||
import tribes1master from './tribes1master.js'
|
||||
import unreal2 from './unreal2.js'
|
||||
import ut3 from './ut3.js'
|
||||
import valve from './valve.js'
|
||||
import vcmp from './vcmp.js'
|
||||
import ventrilo from './ventrilo.js'
|
||||
import warsow from './warsow.js'
|
||||
|
||||
export {
|
||||
armagetron, ase, asa, assettocorsa, battlefield, buildandshoot, cs2d, discord, doom3, eco, epic, ffow, fivem, gamespy1,
|
||||
gamespy2, gamespy3, geneshift, goldsrc, hexen2, jc2mp, kspdmp, mafia2mp, mafia2online, minecraft,
|
||||
minecraftbedrock, minecraftvanilla, mumble, mumbleping, nadeo, openttd, quake1, quake2, quake3, rfactor, samp,
|
||||
savage2, starmade, starsiege, teamspeak2, teamspeak3, terraria, tribes1, tribes1master, unreal2, ut3, valve,
|
||||
vcmp, ventrilo, warsow
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import gamespy3 from './gamespy3.js'
|
||||
|
||||
// supposedly, gamespy3 is the "official" query protocol for jcmp,
|
||||
// but it's broken (requires useOnlySingleSplit), and may not include some player names
|
||||
export default class jc2mp extends gamespy3 {
|
||||
constructor () {
|
||||
super()
|
||||
this.useOnlySingleSplit = true
|
||||
this.isJc2mp = true
|
||||
this.encoding = 'utf8'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
await super.run(state)
|
||||
}
|
||||
}
|
||||
import gamespy3 from './gamespy3.js'
|
||||
|
||||
// supposedly, gamespy3 is the "official" query protocol for jcmp,
|
||||
// but it's broken (requires useOnlySingleSplit), and may not include some player names
|
||||
export default class jc2mp extends gamespy3 {
|
||||
constructor () {
|
||||
super()
|
||||
this.useOnlySingleSplit = true
|
||||
this.isJc2mp = true
|
||||
this.encoding = 'utf8'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
await super.run(state)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +1,28 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class kspdmp extends Core {
|
||||
async run (state) {
|
||||
const json = await this.request({
|
||||
url: 'http://' + this.options.address + ':' + this.options.port,
|
||||
responseType: 'json'
|
||||
})
|
||||
|
||||
for (const one of json.players) {
|
||||
state.players.push({ name: one.nickname, team: one.team })
|
||||
}
|
||||
|
||||
for (const key of Object.keys(json)) {
|
||||
state.raw[key] = json[key]
|
||||
}
|
||||
state.name = json.server_name
|
||||
state.maxplayers = json.max_players
|
||||
state.gamePort = json.port
|
||||
if (json.players) {
|
||||
const split = json.players.split(', ')
|
||||
for (const name of split) {
|
||||
state.players.push({ name })
|
||||
}
|
||||
}
|
||||
state.numplayers = state.players.length
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class kspdmp extends Core {
|
||||
async run (state) {
|
||||
const json = await this.request({
|
||||
url: 'http://' + this.options.address + ':' + this.options.port,
|
||||
responseType: 'json'
|
||||
})
|
||||
|
||||
for (const one of json.players) {
|
||||
state.players.push({ name: one.nickname, team: one.team })
|
||||
}
|
||||
|
||||
for (const key of Object.keys(json)) {
|
||||
state.raw[key] = json[key]
|
||||
}
|
||||
state.name = json.server_name
|
||||
state.maxplayers = json.max_players
|
||||
state.gamePort = json.port
|
||||
if (json.players) {
|
||||
const split = json.players.split(', ')
|
||||
for (const name of split) {
|
||||
state.players.push({ name })
|
||||
}
|
||||
}
|
||||
state.numplayers = state.players.length
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,41 +1,41 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class mafia2mp extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'latin1'
|
||||
this.header = 'M2MP'
|
||||
this.isMafia2Online = false
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
const body = await this.udpSend(this.header, (buffer) => {
|
||||
const reader = this.reader(buffer)
|
||||
const header = reader.string(this.header.length)
|
||||
if (header !== this.header) return
|
||||
return reader.rest()
|
||||
})
|
||||
|
||||
const reader = this.reader(body)
|
||||
state.name = this.readString(reader)
|
||||
state.numplayers = parseInt(this.readString(reader))
|
||||
state.maxplayers = parseInt(this.readString(reader))
|
||||
state.raw.gamemode = this.readString(reader)
|
||||
state.password = !!reader.uint(1)
|
||||
state.gamePort = this.options.port - 1
|
||||
|
||||
while (!reader.done()) {
|
||||
const player = {}
|
||||
player.name = this.readString(reader)
|
||||
if (!player.name) break
|
||||
if (this.isMafia2Online) {
|
||||
player.ping = parseInt(this.readString(reader))
|
||||
}
|
||||
state.players.push(player)
|
||||
}
|
||||
}
|
||||
|
||||
readString (reader) {
|
||||
return reader.pascalString(1, -1)
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class mafia2mp extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'latin1'
|
||||
this.header = 'M2MP'
|
||||
this.isMafia2Online = false
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
const body = await this.udpSend(this.header, (buffer) => {
|
||||
const reader = this.reader(buffer)
|
||||
const header = reader.string(this.header.length)
|
||||
if (header !== this.header) return
|
||||
return reader.rest()
|
||||
})
|
||||
|
||||
const reader = this.reader(body)
|
||||
state.name = this.readString(reader)
|
||||
state.numplayers = parseInt(this.readString(reader))
|
||||
state.maxplayers = parseInt(this.readString(reader))
|
||||
state.raw.gamemode = this.readString(reader)
|
||||
state.password = !!reader.uint(1)
|
||||
state.gamePort = this.options.port - 1
|
||||
|
||||
while (!reader.done()) {
|
||||
const player = {}
|
||||
player.name = this.readString(reader)
|
||||
if (!player.name) break
|
||||
if (this.isMafia2Online) {
|
||||
player.ping = parseInt(this.readString(reader))
|
||||
}
|
||||
state.players.push(player)
|
||||
}
|
||||
}
|
||||
|
||||
readString (reader) {
|
||||
return reader.pascalString(1, -1)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import mafia2mp from './mafia2mp.js'
|
||||
|
||||
export default class mafia2online extends mafia2mp {
|
||||
constructor () {
|
||||
super()
|
||||
this.header = 'M2Online'
|
||||
this.isMafia2Online = true
|
||||
}
|
||||
}
|
||||
import mafia2mp from './mafia2mp.js'
|
||||
|
||||
export default class mafia2online extends mafia2mp {
|
||||
constructor () {
|
||||
super()
|
||||
this.header = 'M2Online'
|
||||
this.isMafia2Online = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,102 +1,102 @@
|
|||
import Core from './core.js'
|
||||
import minecraftbedrock from './minecraftbedrock.js'
|
||||
import minecraftvanilla from './minecraftvanilla.js'
|
||||
import Gamespy3 from './gamespy3.js'
|
||||
|
||||
/*
|
||||
Vanilla servers respond to minecraftvanilla only
|
||||
Some modded vanilla servers respond to minecraftvanilla and gamespy3, or gamespy3 only
|
||||
Some bedrock servers respond to gamespy3 only
|
||||
Some bedrock servers respond to minecraftbedrock only
|
||||
Unsure if any bedrock servers respond to gamespy3 and minecraftbedrock
|
||||
*/
|
||||
|
||||
export default class minecraft extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.srvRecord = '_minecraft._tcp'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
/** @type {Promise<Results>[]} */
|
||||
const promises = []
|
||||
|
||||
const vanillaResolver = new minecraftvanilla()
|
||||
vanillaResolver.options = this.options
|
||||
vanillaResolver.udpSocket = this.udpSocket
|
||||
promises.push((async () => {
|
||||
try { return await vanillaResolver.runOnceSafe() } catch (e) {}
|
||||
})())
|
||||
|
||||
const gamespyResolver = new Gamespy3()
|
||||
gamespyResolver.options = {
|
||||
...this.options,
|
||||
encoding: 'utf8'
|
||||
}
|
||||
gamespyResolver.udpSocket = this.udpSocket
|
||||
promises.push((async () => {
|
||||
try { return await gamespyResolver.runOnceSafe() } catch (e) {}
|
||||
})())
|
||||
|
||||
const bedrockResolver = new minecraftbedrock()
|
||||
bedrockResolver.options = this.options
|
||||
bedrockResolver.udpSocket = this.udpSocket
|
||||
promises.push((async () => {
|
||||
try { return await bedrockResolver.runOnceSafe() } catch (e) {}
|
||||
})())
|
||||
|
||||
const [vanillaState, gamespyState, bedrockState] = await Promise.all(promises)
|
||||
|
||||
state.raw.vanilla = vanillaState
|
||||
state.raw.gamespy = gamespyState
|
||||
state.raw.bedrock = bedrockState
|
||||
|
||||
if (!vanillaState && !gamespyState && !bedrockState) {
|
||||
throw new Error('No protocols succeeded')
|
||||
}
|
||||
|
||||
// Ordered from least worth to most worth (player names / etc)
|
||||
if (bedrockState) {
|
||||
if (bedrockState.players.length) state.players = bedrockState.players
|
||||
}
|
||||
if (vanillaState) {
|
||||
try {
|
||||
let name = ''
|
||||
const description = vanillaState.raw.description
|
||||
if (typeof description === 'string') {
|
||||
name = description
|
||||
}
|
||||
if (!name && typeof description === 'object' && description.text) {
|
||||
name = description.text
|
||||
}
|
||||
if (!name && typeof description === 'object' && description.extra) {
|
||||
name = description.extra.map(part => part.text).join('')
|
||||
}
|
||||
state.name = name
|
||||
} catch (e) {}
|
||||
if (vanillaState.numplayers) state.numplayers = vanillaState.numplayers
|
||||
if (vanillaState.maxplayers) state.maxplayers = vanillaState.maxplayers
|
||||
if (vanillaState.players.length) state.players = vanillaState.players
|
||||
if (vanillaState.ping) this.registerRtt(vanillaState.ping)
|
||||
}
|
||||
if (gamespyState) {
|
||||
if (gamespyState.name) state.name = gamespyState.name
|
||||
if (gamespyState.numplayers) state.numplayers = gamespyState.numplayers
|
||||
if (gamespyState.maxplayers) state.maxplayers = gamespyState.maxplayers
|
||||
if (gamespyState.players.length) state.players = gamespyState.players
|
||||
else if (gamespyState.numplayers) state.numplayers = gamespyState.numplayers
|
||||
if (gamespyState.ping) this.registerRtt(gamespyState.ping)
|
||||
}
|
||||
if (bedrockState) {
|
||||
if (bedrockState.name) state.name = bedrockState.name
|
||||
if (bedrockState.numplayers) state.numplayers = bedrockState.numplayers
|
||||
if (bedrockState.maxplayers) state.maxplayers = bedrockState.maxplayers
|
||||
if (bedrockState.map) state.map = bedrockState.map
|
||||
if (bedrockState.ping) this.registerRtt(bedrockState.ping)
|
||||
}
|
||||
// remove dupe spaces from name
|
||||
state.name = state.name.replace(/\s+/g, ' ')
|
||||
// remove color codes from name
|
||||
state.name = state.name.replace(/\u00A7./g, '')
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
import minecraftbedrock from './minecraftbedrock.js'
|
||||
import minecraftvanilla from './minecraftvanilla.js'
|
||||
import Gamespy3 from './gamespy3.js'
|
||||
|
||||
/*
|
||||
Vanilla servers respond to minecraftvanilla only
|
||||
Some modded vanilla servers respond to minecraftvanilla and gamespy3, or gamespy3 only
|
||||
Some bedrock servers respond to gamespy3 only
|
||||
Some bedrock servers respond to minecraftbedrock only
|
||||
Unsure if any bedrock servers respond to gamespy3 and minecraftbedrock
|
||||
*/
|
||||
|
||||
export default class minecraft extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.srvRecord = '_minecraft._tcp'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
/** @type {Promise<Results>[]} */
|
||||
const promises = []
|
||||
|
||||
const vanillaResolver = new minecraftvanilla()
|
||||
vanillaResolver.options = this.options
|
||||
vanillaResolver.udpSocket = this.udpSocket
|
||||
promises.push((async () => {
|
||||
try { return await vanillaResolver.runOnceSafe() } catch (e) {}
|
||||
})())
|
||||
|
||||
const gamespyResolver = new Gamespy3()
|
||||
gamespyResolver.options = {
|
||||
...this.options,
|
||||
encoding: 'utf8'
|
||||
}
|
||||
gamespyResolver.udpSocket = this.udpSocket
|
||||
promises.push((async () => {
|
||||
try { return await gamespyResolver.runOnceSafe() } catch (e) {}
|
||||
})())
|
||||
|
||||
const bedrockResolver = new minecraftbedrock()
|
||||
bedrockResolver.options = this.options
|
||||
bedrockResolver.udpSocket = this.udpSocket
|
||||
promises.push((async () => {
|
||||
try { return await bedrockResolver.runOnceSafe() } catch (e) {}
|
||||
})())
|
||||
|
||||
const [vanillaState, gamespyState, bedrockState] = await Promise.all(promises)
|
||||
|
||||
state.raw.vanilla = vanillaState
|
||||
state.raw.gamespy = gamespyState
|
||||
state.raw.bedrock = bedrockState
|
||||
|
||||
if (!vanillaState && !gamespyState && !bedrockState) {
|
||||
throw new Error('No protocols succeeded')
|
||||
}
|
||||
|
||||
// Ordered from least worth to most worth (player names / etc)
|
||||
if (bedrockState) {
|
||||
if (bedrockState.players.length) state.players = bedrockState.players
|
||||
}
|
||||
if (vanillaState) {
|
||||
try {
|
||||
let name = ''
|
||||
const description = vanillaState.raw.description
|
||||
if (typeof description === 'string') {
|
||||
name = description
|
||||
}
|
||||
if (!name && typeof description === 'object' && description.text) {
|
||||
name = description.text
|
||||
}
|
||||
if (!name && typeof description === 'object' && description.extra) {
|
||||
name = description.extra.map(part => part.text).join('')
|
||||
}
|
||||
state.name = name
|
||||
} catch (e) {}
|
||||
if (vanillaState.numplayers) state.numplayers = vanillaState.numplayers
|
||||
if (vanillaState.maxplayers) state.maxplayers = vanillaState.maxplayers
|
||||
if (vanillaState.players.length) state.players = vanillaState.players
|
||||
if (vanillaState.ping) this.registerRtt(vanillaState.ping)
|
||||
}
|
||||
if (gamespyState) {
|
||||
if (gamespyState.name) state.name = gamespyState.name
|
||||
if (gamespyState.numplayers) state.numplayers = gamespyState.numplayers
|
||||
if (gamespyState.maxplayers) state.maxplayers = gamespyState.maxplayers
|
||||
if (gamespyState.players.length) state.players = gamespyState.players
|
||||
else if (gamespyState.numplayers) state.numplayers = gamespyState.numplayers
|
||||
if (gamespyState.ping) this.registerRtt(gamespyState.ping)
|
||||
}
|
||||
if (bedrockState) {
|
||||
if (bedrockState.name) state.name = bedrockState.name
|
||||
if (bedrockState.numplayers) state.numplayers = bedrockState.numplayers
|
||||
if (bedrockState.maxplayers) state.maxplayers = bedrockState.maxplayers
|
||||
if (bedrockState.map) state.map = bedrockState.map
|
||||
if (bedrockState.ping) this.registerRtt(bedrockState.ping)
|
||||
}
|
||||
// remove dupe spaces from name
|
||||
state.name = state.name.replace(/\s+/g, ' ')
|
||||
// remove color codes from name
|
||||
state.name = state.name.replace(/\u00A7./g, '')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,72 +1,72 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class minecraftbedrock extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.byteorder = 'be'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
const bufs = [
|
||||
Buffer.from([0x01]), // Message ID, ID_UNCONNECTED_PING
|
||||
Buffer.from('1122334455667788', 'hex'), // Nonce / timestamp
|
||||
Buffer.from('00ffff00fefefefefdfdfdfd12345678', 'hex'), // Magic
|
||||
Buffer.from('0000000000000000', 'hex') // Cliend GUID
|
||||
]
|
||||
|
||||
return await this.udpSend(Buffer.concat(bufs), buffer => {
|
||||
const reader = this.reader(buffer)
|
||||
|
||||
const messageId = reader.uint(1)
|
||||
if (messageId !== 0x1c) {
|
||||
this.logger.debug('Skipping packet, invalid message id')
|
||||
return
|
||||
}
|
||||
|
||||
const nonce = reader.part(8).toString('hex') // should match the nonce we sent
|
||||
this.logger.debug('Nonce: ' + nonce)
|
||||
if (nonce !== '1122334455667788') {
|
||||
this.logger.debug('Skipping packet, invalid nonce')
|
||||
return
|
||||
}
|
||||
|
||||
// These 8 bytes are identical to the serverId string we receive in decimal below
|
||||
reader.skip(8)
|
||||
|
||||
const magic = reader.part(16).toString('hex')
|
||||
this.logger.debug('Magic value: ' + magic)
|
||||
if (magic !== '00ffff00fefefefefdfdfdfd12345678') {
|
||||
this.logger.debug('Skipping packet, invalid magic')
|
||||
return
|
||||
}
|
||||
|
||||
const statusLen = reader.uint(2)
|
||||
if (reader.remaining() !== statusLen) {
|
||||
throw new Error('Invalid status length: ' + reader.remaining() + ' vs ' + statusLen)
|
||||
}
|
||||
|
||||
const statusStr = reader.rest().toString('utf8')
|
||||
this.logger.debug('Raw status str: ' + statusStr)
|
||||
|
||||
const split = statusStr.split(';')
|
||||
if (split.length < 6) {
|
||||
throw new Error('Missing enough chunks in status str')
|
||||
}
|
||||
|
||||
state.raw.edition = split.shift()
|
||||
state.name = split.shift()
|
||||
state.raw.protocolVersion = split.shift()
|
||||
state.raw.mcVersion = split.shift()
|
||||
state.numplayers = parseInt(split.shift())
|
||||
state.maxplayers = parseInt(split.shift())
|
||||
if (split.length) state.raw.serverId = split.shift()
|
||||
if (split.length) state.map = split.shift()
|
||||
if (split.length) state.raw.gameMode = split.shift()
|
||||
if (split.length) state.raw.nintendoOnly = !!parseInt(split.shift())
|
||||
if (split.length) state.raw.ipv4Port = split.shift()
|
||||
if (split.length) state.raw.ipv6Port = split.shift()
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class minecraftbedrock extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.byteorder = 'be'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
const bufs = [
|
||||
Buffer.from([0x01]), // Message ID, ID_UNCONNECTED_PING
|
||||
Buffer.from('1122334455667788', 'hex'), // Nonce / timestamp
|
||||
Buffer.from('00ffff00fefefefefdfdfdfd12345678', 'hex'), // Magic
|
||||
Buffer.from('0000000000000000', 'hex') // Cliend GUID
|
||||
]
|
||||
|
||||
return await this.udpSend(Buffer.concat(bufs), buffer => {
|
||||
const reader = this.reader(buffer)
|
||||
|
||||
const messageId = reader.uint(1)
|
||||
if (messageId !== 0x1c) {
|
||||
this.logger.debug('Skipping packet, invalid message id')
|
||||
return
|
||||
}
|
||||
|
||||
const nonce = reader.part(8).toString('hex') // should match the nonce we sent
|
||||
this.logger.debug('Nonce: ' + nonce)
|
||||
if (nonce !== '1122334455667788') {
|
||||
this.logger.debug('Skipping packet, invalid nonce')
|
||||
return
|
||||
}
|
||||
|
||||
// These 8 bytes are identical to the serverId string we receive in decimal below
|
||||
reader.skip(8)
|
||||
|
||||
const magic = reader.part(16).toString('hex')
|
||||
this.logger.debug('Magic value: ' + magic)
|
||||
if (magic !== '00ffff00fefefefefdfdfdfd12345678') {
|
||||
this.logger.debug('Skipping packet, invalid magic')
|
||||
return
|
||||
}
|
||||
|
||||
const statusLen = reader.uint(2)
|
||||
if (reader.remaining() !== statusLen) {
|
||||
throw new Error('Invalid status length: ' + reader.remaining() + ' vs ' + statusLen)
|
||||
}
|
||||
|
||||
const statusStr = reader.rest().toString('utf8')
|
||||
this.logger.debug('Raw status str: ' + statusStr)
|
||||
|
||||
const split = statusStr.split(';')
|
||||
if (split.length < 6) {
|
||||
throw new Error('Missing enough chunks in status str')
|
||||
}
|
||||
|
||||
state.raw.edition = split.shift()
|
||||
state.name = split.shift()
|
||||
state.raw.protocolVersion = split.shift()
|
||||
state.raw.mcVersion = split.shift()
|
||||
state.numplayers = parseInt(split.shift())
|
||||
state.maxplayers = parseInt(split.shift())
|
||||
if (split.length) state.raw.serverId = split.shift()
|
||||
if (split.length) state.map = split.shift()
|
||||
if (split.length) state.raw.gameMode = split.shift()
|
||||
if (split.length) state.raw.nintendoOnly = !!parseInt(split.shift())
|
||||
if (split.length) state.raw.ipv4Port = split.shift()
|
||||
if (split.length) state.raw.ipv6Port = split.shift()
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,75 +1,75 @@
|
|||
import Core from './core.js'
|
||||
import Varint from 'varint'
|
||||
|
||||
export default class minecraftvanilla extends Core {
|
||||
async run (state) {
|
||||
const portBuf = Buffer.alloc(2)
|
||||
portBuf.writeUInt16BE(this.options.port, 0)
|
||||
|
||||
const addressBuf = Buffer.from(this.options.host, 'utf8')
|
||||
|
||||
const bufs = [
|
||||
this.varIntBuffer(47),
|
||||
this.varIntBuffer(addressBuf.length),
|
||||
addressBuf,
|
||||
portBuf,
|
||||
this.varIntBuffer(1)
|
||||
]
|
||||
|
||||
const outBuffer = Buffer.concat([
|
||||
this.buildPacket(0, Buffer.concat(bufs)),
|
||||
this.buildPacket(0)
|
||||
])
|
||||
|
||||
const data = await this.withTcp(async socket => {
|
||||
return await this.tcpSend(socket, outBuffer, data => {
|
||||
if (data.length < 10) return
|
||||
const reader = this.reader(data)
|
||||
const length = reader.varint()
|
||||
if (data.length < length) return
|
||||
return reader.rest()
|
||||
})
|
||||
})
|
||||
|
||||
const reader = this.reader(data)
|
||||
|
||||
const packetId = reader.varint()
|
||||
this.logger.debug('Packet ID: ' + packetId)
|
||||
|
||||
const strLen = reader.varint()
|
||||
this.logger.debug('String Length: ' + strLen)
|
||||
|
||||
const str = reader.rest().toString('utf8')
|
||||
this.logger.debug(str)
|
||||
|
||||
const json = JSON.parse(str)
|
||||
delete json.favicon
|
||||
|
||||
state.raw = json
|
||||
state.maxplayers = json.players.max
|
||||
state.numplayers = json.players.online
|
||||
|
||||
if (json.players.sample) {
|
||||
for (const player of json.players.sample) {
|
||||
state.players.push({
|
||||
id: player.id,
|
||||
name: player.name
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
varIntBuffer (num) {
|
||||
return Buffer.from(Varint.encode(num))
|
||||
}
|
||||
|
||||
buildPacket (id, data) {
|
||||
if (!data) data = Buffer.from([])
|
||||
const idBuffer = this.varIntBuffer(id)
|
||||
return Buffer.concat([
|
||||
this.varIntBuffer(data.length + idBuffer.length),
|
||||
idBuffer,
|
||||
data
|
||||
])
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
import Varint from 'varint'
|
||||
|
||||
export default class minecraftvanilla extends Core {
|
||||
async run (state) {
|
||||
const portBuf = Buffer.alloc(2)
|
||||
portBuf.writeUInt16BE(this.options.port, 0)
|
||||
|
||||
const addressBuf = Buffer.from(this.options.host, 'utf8')
|
||||
|
||||
const bufs = [
|
||||
this.varIntBuffer(47),
|
||||
this.varIntBuffer(addressBuf.length),
|
||||
addressBuf,
|
||||
portBuf,
|
||||
this.varIntBuffer(1)
|
||||
]
|
||||
|
||||
const outBuffer = Buffer.concat([
|
||||
this.buildPacket(0, Buffer.concat(bufs)),
|
||||
this.buildPacket(0)
|
||||
])
|
||||
|
||||
const data = await this.withTcp(async socket => {
|
||||
return await this.tcpSend(socket, outBuffer, data => {
|
||||
if (data.length < 10) return
|
||||
const reader = this.reader(data)
|
||||
const length = reader.varint()
|
||||
if (data.length < length) return
|
||||
return reader.rest()
|
||||
})
|
||||
})
|
||||
|
||||
const reader = this.reader(data)
|
||||
|
||||
const packetId = reader.varint()
|
||||
this.logger.debug('Packet ID: ' + packetId)
|
||||
|
||||
const strLen = reader.varint()
|
||||
this.logger.debug('String Length: ' + strLen)
|
||||
|
||||
const str = reader.rest().toString('utf8')
|
||||
this.logger.debug(str)
|
||||
|
||||
const json = JSON.parse(str)
|
||||
delete json.favicon
|
||||
|
||||
state.raw = json
|
||||
state.maxplayers = json.players.max
|
||||
state.numplayers = json.players.online
|
||||
|
||||
if (json.players.sample) {
|
||||
for (const player of json.players.sample) {
|
||||
state.players.push({
|
||||
id: player.id,
|
||||
name: player.name
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
varIntBuffer (num) {
|
||||
return Buffer.from(Varint.encode(num))
|
||||
}
|
||||
|
||||
buildPacket (id, data) {
|
||||
if (!data) data = Buffer.from([])
|
||||
const idBuffer = this.varIntBuffer(id)
|
||||
return Buffer.concat([
|
||||
this.varIntBuffer(data.length + idBuffer.length),
|
||||
idBuffer,
|
||||
data
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,39 +1,39 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class mumble extends Core {
|
||||
async run (state) {
|
||||
const json = await this.withTcp(async socket => {
|
||||
return await this.tcpSend(socket, 'json', (buffer) => {
|
||||
if (buffer.length < 10) return
|
||||
const str = buffer.toString()
|
||||
let json
|
||||
try {
|
||||
json = JSON.parse(str)
|
||||
} catch (e) {
|
||||
// probably not all here yet
|
||||
return
|
||||
}
|
||||
return json
|
||||
})
|
||||
})
|
||||
|
||||
state.raw = json
|
||||
state.name = json.name
|
||||
state.gamePort = json.x_gtmurmur_connectport || 64738
|
||||
|
||||
let channelStack = [state.raw.root]
|
||||
while (channelStack.length) {
|
||||
const channel = channelStack.shift()
|
||||
channel.description = this.cleanComment(channel.description)
|
||||
channelStack = channelStack.concat(channel.channels)
|
||||
for (const user of channel.users) {
|
||||
user.comment = this.cleanComment(user.comment)
|
||||
state.players.push(user)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cleanComment (str) {
|
||||
return str.replace(/<.*>/g, '')
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class mumble extends Core {
|
||||
async run (state) {
|
||||
const json = await this.withTcp(async socket => {
|
||||
return await this.tcpSend(socket, 'json', (buffer) => {
|
||||
if (buffer.length < 10) return
|
||||
const str = buffer.toString()
|
||||
let json
|
||||
try {
|
||||
json = JSON.parse(str)
|
||||
} catch (e) {
|
||||
// probably not all here yet
|
||||
return
|
||||
}
|
||||
return json
|
||||
})
|
||||
})
|
||||
|
||||
state.raw = json
|
||||
state.name = json.name
|
||||
state.gamePort = json.x_gtmurmur_connectport || 64738
|
||||
|
||||
let channelStack = [state.raw.root]
|
||||
while (channelStack.length) {
|
||||
const channel = channelStack.shift()
|
||||
channel.description = this.cleanComment(channel.description)
|
||||
channelStack = channelStack.concat(channel.channels)
|
||||
for (const user of channel.users) {
|
||||
user.comment = this.cleanComment(user.comment)
|
||||
state.players.push(user)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cleanComment (str) {
|
||||
return str.replace(/<.*>/g, '')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class mumbleping extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.byteorder = 'be'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
const data = await this.udpSend('\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08', (buffer) => {
|
||||
if (buffer.length >= 24) return buffer
|
||||
})
|
||||
|
||||
const reader = this.reader(data)
|
||||
reader.skip(1)
|
||||
state.raw.versionMajor = reader.uint(1)
|
||||
state.raw.versionMinor = reader.uint(1)
|
||||
state.raw.versionPatch = reader.uint(1)
|
||||
reader.skip(8)
|
||||
state.numplayers = reader.uint(4)
|
||||
state.maxplayers = reader.uint(4)
|
||||
state.raw.allowedbandwidth = reader.uint(4)
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class mumbleping extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.byteorder = 'be'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
const data = await this.udpSend('\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08', (buffer) => {
|
||||
if (buffer.length >= 24) return buffer
|
||||
})
|
||||
|
||||
const reader = this.reader(data)
|
||||
reader.skip(1)
|
||||
state.raw.versionMajor = reader.uint(1)
|
||||
state.raw.versionMinor = reader.uint(1)
|
||||
state.raw.versionPatch = reader.uint(1)
|
||||
reader.skip(8)
|
||||
state.numplayers = reader.uint(4)
|
||||
state.maxplayers = reader.uint(4)
|
||||
state.raw.allowedbandwidth = reader.uint(4)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,86 +1,86 @@
|
|||
import Core from './core.js'
|
||||
import Promises from '../lib/Promises.js'
|
||||
import * as gbxremote from 'gbxremote'
|
||||
|
||||
export default class nadeo extends Core {
|
||||
async run (state) {
|
||||
await this.withClient(async client => {
|
||||
const start = Date.now()
|
||||
await this.query(client, 'Authenticate', this.options.login, this.options.password)
|
||||
this.registerRtt(Date.now() - start)
|
||||
|
||||
// const data = this.methodCall(client, 'GetStatus');
|
||||
|
||||
{
|
||||
const results = await this.query(client, 'GetServerOptions')
|
||||
state.name = this.stripColors(results.Name)
|
||||
state.password = (results.Password !== 'No password')
|
||||
state.maxplayers = results.CurrentMaxPlayers
|
||||
state.raw.maxspectators = results.CurrentMaxSpectators
|
||||
}
|
||||
|
||||
{
|
||||
const results = await this.query(client, 'GetCurrentMapInfo')
|
||||
state.map = this.stripColors(results.Name)
|
||||
state.raw.mapUid = results.UId
|
||||
}
|
||||
|
||||
{
|
||||
const results = await this.query(client, 'GetCurrentGameInfo')
|
||||
let gamemode = ''
|
||||
const igm = results.GameMode
|
||||
if (igm === 0) gamemode = 'Rounds'
|
||||
if (igm === 1) gamemode = 'Time Attack'
|
||||
if (igm === 2) gamemode = 'Team'
|
||||
if (igm === 3) gamemode = 'Laps'
|
||||
if (igm === 4) gamemode = 'Stunts'
|
||||
if (igm === 5) gamemode = 'Cup'
|
||||
state.raw.gametype = gamemode
|
||||
state.raw.mapcount = results.NbChallenge
|
||||
}
|
||||
|
||||
{
|
||||
const results = await this.query(client, 'GetNextMapInfo')
|
||||
state.raw.nextmapName = this.stripColors(results.Name)
|
||||
state.raw.nextmapUid = results.UId
|
||||
}
|
||||
|
||||
if (this.options.port === 5000) {
|
||||
state.gamePort = 2350
|
||||
}
|
||||
|
||||
state.raw.players = await this.query(client, 'GetPlayerList', 10000, 0)
|
||||
for (const player of state.raw.players) {
|
||||
state.players.push({
|
||||
name: this.stripColors(player.Name || player.NickName)
|
||||
})
|
||||
}
|
||||
state.numplayers = state.players.length
|
||||
})
|
||||
}
|
||||
|
||||
async withClient (fn) {
|
||||
const socket = new gbxremote.Client(this.options.port, this.options.host)
|
||||
try {
|
||||
const connectPromise = socket.connect()
|
||||
const timeoutPromise = Promises.createTimeout(this.options.socketTimeout, 'GBX Remote Opening')
|
||||
await Promise.race([connectPromise, timeoutPromise, this.abortedPromise])
|
||||
return await fn(socket)
|
||||
} finally {
|
||||
socket.terminate()
|
||||
}
|
||||
}
|
||||
|
||||
async query (client, ...cmdset) {
|
||||
const cmd = cmdset[0]
|
||||
const params = cmdset.slice(1)
|
||||
|
||||
const sentPromise = client.query(cmd, params)
|
||||
const timeoutPromise = Promises.createTimeout(this.options.socketTimeout, 'GBX Method Call')
|
||||
return await Promise.race([sentPromise, timeoutPromise, this.abortedPromise])
|
||||
}
|
||||
|
||||
stripColors (str) {
|
||||
return str.replace(/\$([0-9a-f]{3}|[a-z])/gi, '')
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
import Promises from '../lib/Promises.js'
|
||||
import * as gbxremote from 'gbxremote'
|
||||
|
||||
export default class nadeo extends Core {
|
||||
async run (state) {
|
||||
await this.withClient(async client => {
|
||||
const start = Date.now()
|
||||
await this.query(client, 'Authenticate', this.options.login, this.options.password)
|
||||
this.registerRtt(Date.now() - start)
|
||||
|
||||
// const data = this.methodCall(client, 'GetStatus');
|
||||
|
||||
{
|
||||
const results = await this.query(client, 'GetServerOptions')
|
||||
state.name = this.stripColors(results.Name)
|
||||
state.password = (results.Password !== 'No password')
|
||||
state.maxplayers = results.CurrentMaxPlayers
|
||||
state.raw.maxspectators = results.CurrentMaxSpectators
|
||||
}
|
||||
|
||||
{
|
||||
const results = await this.query(client, 'GetCurrentMapInfo')
|
||||
state.map = this.stripColors(results.Name)
|
||||
state.raw.mapUid = results.UId
|
||||
}
|
||||
|
||||
{
|
||||
const results = await this.query(client, 'GetCurrentGameInfo')
|
||||
let gamemode = ''
|
||||
const igm = results.GameMode
|
||||
if (igm === 0) gamemode = 'Rounds'
|
||||
if (igm === 1) gamemode = 'Time Attack'
|
||||
if (igm === 2) gamemode = 'Team'
|
||||
if (igm === 3) gamemode = 'Laps'
|
||||
if (igm === 4) gamemode = 'Stunts'
|
||||
if (igm === 5) gamemode = 'Cup'
|
||||
state.raw.gametype = gamemode
|
||||
state.raw.mapcount = results.NbChallenge
|
||||
}
|
||||
|
||||
{
|
||||
const results = await this.query(client, 'GetNextMapInfo')
|
||||
state.raw.nextmapName = this.stripColors(results.Name)
|
||||
state.raw.nextmapUid = results.UId
|
||||
}
|
||||
|
||||
if (this.options.port === 5000) {
|
||||
state.gamePort = 2350
|
||||
}
|
||||
|
||||
state.raw.players = await this.query(client, 'GetPlayerList', 10000, 0)
|
||||
for (const player of state.raw.players) {
|
||||
state.players.push({
|
||||
name: this.stripColors(player.Name || player.NickName)
|
||||
})
|
||||
}
|
||||
state.numplayers = state.players.length
|
||||
})
|
||||
}
|
||||
|
||||
async withClient (fn) {
|
||||
const socket = new gbxremote.Client(this.options.port, this.options.host)
|
||||
try {
|
||||
const connectPromise = socket.connect()
|
||||
const timeoutPromise = Promises.createTimeout(this.options.socketTimeout, 'GBX Remote Opening')
|
||||
await Promise.race([connectPromise, timeoutPromise, this.abortedPromise])
|
||||
return await fn(socket)
|
||||
} finally {
|
||||
socket.terminate()
|
||||
}
|
||||
}
|
||||
|
||||
async query (client, ...cmdset) {
|
||||
const cmd = cmdset[0]
|
||||
const params = cmdset.slice(1)
|
||||
|
||||
const sentPromise = client.query(cmd, params)
|
||||
const timeoutPromise = Promises.createTimeout(this.options.socketTimeout, 'GBX Method Call')
|
||||
return await Promise.race([sentPromise, timeoutPromise, this.abortedPromise])
|
||||
}
|
||||
|
||||
stripColors (str) {
|
||||
return str.replace(/\$([0-9a-f]{3}|[a-z])/gi, '')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,127 +1,127 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class openttd extends Core {
|
||||
async run (state) {
|
||||
{
|
||||
const [reader, version] = await this.query(0, 1, 1, 4)
|
||||
if (version >= 4) {
|
||||
const numGrf = reader.uint(1)
|
||||
state.raw.grfs = []
|
||||
for (let i = 0; i < numGrf; i++) {
|
||||
const grf = {}
|
||||
grf.id = reader.part(4).toString('hex')
|
||||
grf.md5 = reader.part(16).toString('hex')
|
||||
state.raw.grfs.push(grf)
|
||||
}
|
||||
}
|
||||
if (version >= 3) {
|
||||
state.raw.date_current = this.readDate(reader)
|
||||
state.raw.date_start = this.readDate(reader)
|
||||
}
|
||||
if (version >= 2) {
|
||||
state.raw.maxcompanies = reader.uint(1)
|
||||
state.raw.numcompanies = reader.uint(1)
|
||||
state.raw.maxspectators = reader.uint(1)
|
||||
}
|
||||
|
||||
state.name = reader.string()
|
||||
state.raw.version = reader.string()
|
||||
|
||||
state.raw.language = this.decode(
|
||||
reader.uint(1),
|
||||
['any', 'en', 'de', 'fr']
|
||||
)
|
||||
|
||||
state.password = !!reader.uint(1)
|
||||
state.maxplayers = reader.uint(1)
|
||||
state.numplayers = reader.uint(1)
|
||||
state.raw.numspectators = reader.uint(1)
|
||||
state.map = reader.string()
|
||||
state.raw.map_width = reader.uint(2)
|
||||
state.raw.map_height = reader.uint(2)
|
||||
|
||||
state.raw.landscape = this.decode(
|
||||
reader.uint(1),
|
||||
['temperate', 'arctic', 'desert', 'toyland']
|
||||
)
|
||||
|
||||
state.raw.dedicated = !!reader.uint(1)
|
||||
}
|
||||
|
||||
{
|
||||
const [reader, version] = await this.query(2, 3, -1, -1)
|
||||
// we don't know how to deal with companies outside version 6
|
||||
if (version === 6) {
|
||||
state.raw.companies = []
|
||||
const numCompanies = reader.uint(1)
|
||||
for (let iCompany = 0; iCompany < numCompanies; iCompany++) {
|
||||
const company = {}
|
||||
company.id = reader.uint(1)
|
||||
company.name = reader.string()
|
||||
company.year_start = reader.uint(4)
|
||||
company.value = reader.uint(8).toString()
|
||||
company.money = reader.uint(8).toString()
|
||||
company.income = reader.uint(8).toString()
|
||||
company.performance = reader.uint(2)
|
||||
company.password = !!reader.uint(1)
|
||||
|
||||
const vehicleTypes = ['train', 'truck', 'bus', 'aircraft', 'ship']
|
||||
const stationTypes = ['station', 'truckbay', 'busstation', 'airport', 'dock']
|
||||
|
||||
company.vehicles = {}
|
||||
for (const type of vehicleTypes) {
|
||||
company.vehicles[type] = reader.uint(2)
|
||||
}
|
||||
company.stations = {}
|
||||
for (const type of stationTypes) {
|
||||
company.stations[type] = reader.uint(2)
|
||||
}
|
||||
|
||||
company.clients = reader.string()
|
||||
state.raw.companies.push(company)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async query (type, expected, minver, maxver) {
|
||||
const b = Buffer.from([0x03, 0x00, type])
|
||||
return await this.udpSend(b, (buffer) => {
|
||||
const reader = this.reader(buffer)
|
||||
|
||||
const packetLen = reader.uint(2)
|
||||
if (packetLen !== buffer.length) {
|
||||
this.logger.debug('Invalid reported packet length: ' + packetLen + ' ' + buffer.length)
|
||||
return
|
||||
}
|
||||
|
||||
const packetType = reader.uint(1)
|
||||
if (packetType !== expected) {
|
||||
this.logger.debug('Unexpected response packet type: ' + packetType)
|
||||
return
|
||||
}
|
||||
|
||||
const protocolVersion = reader.uint(1)
|
||||
if ((minver !== -1 && protocolVersion < minver) || (maxver !== -1 && protocolVersion > maxver)) {
|
||||
throw new Error('Unknown protocol version: ' + protocolVersion + ' Expected: ' + minver + '-' + maxver)
|
||||
}
|
||||
|
||||
return [reader, protocolVersion]
|
||||
})
|
||||
}
|
||||
|
||||
readDate (reader) {
|
||||
const daysSinceZero = reader.uint(4)
|
||||
const temp = new Date(0, 0, 1)
|
||||
temp.setFullYear(0)
|
||||
temp.setDate(daysSinceZero + 1)
|
||||
return temp.toISOString().split('T')[0]
|
||||
}
|
||||
|
||||
decode (num, arr) {
|
||||
if (num < 0 || num >= arr.length) {
|
||||
return num
|
||||
}
|
||||
return arr[num]
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class openttd extends Core {
|
||||
async run (state) {
|
||||
{
|
||||
const [reader, version] = await this.query(0, 1, 1, 4)
|
||||
if (version >= 4) {
|
||||
const numGrf = reader.uint(1)
|
||||
state.raw.grfs = []
|
||||
for (let i = 0; i < numGrf; i++) {
|
||||
const grf = {}
|
||||
grf.id = reader.part(4).toString('hex')
|
||||
grf.md5 = reader.part(16).toString('hex')
|
||||
state.raw.grfs.push(grf)
|
||||
}
|
||||
}
|
||||
if (version >= 3) {
|
||||
state.raw.date_current = this.readDate(reader)
|
||||
state.raw.date_start = this.readDate(reader)
|
||||
}
|
||||
if (version >= 2) {
|
||||
state.raw.maxcompanies = reader.uint(1)
|
||||
state.raw.numcompanies = reader.uint(1)
|
||||
state.raw.maxspectators = reader.uint(1)
|
||||
}
|
||||
|
||||
state.name = reader.string()
|
||||
state.raw.version = reader.string()
|
||||
|
||||
state.raw.language = this.decode(
|
||||
reader.uint(1),
|
||||
['any', 'en', 'de', 'fr']
|
||||
)
|
||||
|
||||
state.password = !!reader.uint(1)
|
||||
state.maxplayers = reader.uint(1)
|
||||
state.numplayers = reader.uint(1)
|
||||
state.raw.numspectators = reader.uint(1)
|
||||
state.map = reader.string()
|
||||
state.raw.map_width = reader.uint(2)
|
||||
state.raw.map_height = reader.uint(2)
|
||||
|
||||
state.raw.landscape = this.decode(
|
||||
reader.uint(1),
|
||||
['temperate', 'arctic', 'desert', 'toyland']
|
||||
)
|
||||
|
||||
state.raw.dedicated = !!reader.uint(1)
|
||||
}
|
||||
|
||||
{
|
||||
const [reader, version] = await this.query(2, 3, -1, -1)
|
||||
// we don't know how to deal with companies outside version 6
|
||||
if (version === 6) {
|
||||
state.raw.companies = []
|
||||
const numCompanies = reader.uint(1)
|
||||
for (let iCompany = 0; iCompany < numCompanies; iCompany++) {
|
||||
const company = {}
|
||||
company.id = reader.uint(1)
|
||||
company.name = reader.string()
|
||||
company.year_start = reader.uint(4)
|
||||
company.value = reader.uint(8).toString()
|
||||
company.money = reader.uint(8).toString()
|
||||
company.income = reader.uint(8).toString()
|
||||
company.performance = reader.uint(2)
|
||||
company.password = !!reader.uint(1)
|
||||
|
||||
const vehicleTypes = ['train', 'truck', 'bus', 'aircraft', 'ship']
|
||||
const stationTypes = ['station', 'truckbay', 'busstation', 'airport', 'dock']
|
||||
|
||||
company.vehicles = {}
|
||||
for (const type of vehicleTypes) {
|
||||
company.vehicles[type] = reader.uint(2)
|
||||
}
|
||||
company.stations = {}
|
||||
for (const type of stationTypes) {
|
||||
company.stations[type] = reader.uint(2)
|
||||
}
|
||||
|
||||
company.clients = reader.string()
|
||||
state.raw.companies.push(company)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async query (type, expected, minver, maxver) {
|
||||
const b = Buffer.from([0x03, 0x00, type])
|
||||
return await this.udpSend(b, (buffer) => {
|
||||
const reader = this.reader(buffer)
|
||||
|
||||
const packetLen = reader.uint(2)
|
||||
if (packetLen !== buffer.length) {
|
||||
this.logger.debug('Invalid reported packet length: ' + packetLen + ' ' + buffer.length)
|
||||
return
|
||||
}
|
||||
|
||||
const packetType = reader.uint(1)
|
||||
if (packetType !== expected) {
|
||||
this.logger.debug('Unexpected response packet type: ' + packetType)
|
||||
return
|
||||
}
|
||||
|
||||
const protocolVersion = reader.uint(1)
|
||||
if ((minver !== -1 && protocolVersion < minver) || (maxver !== -1 && protocolVersion > maxver)) {
|
||||
throw new Error('Unknown protocol version: ' + protocolVersion + ' Expected: ' + minver + '-' + maxver)
|
||||
}
|
||||
|
||||
return [reader, protocolVersion]
|
||||
})
|
||||
}
|
||||
|
||||
readDate (reader) {
|
||||
const daysSinceZero = reader.uint(4)
|
||||
const temp = new Date(0, 0, 1)
|
||||
temp.setFullYear(0)
|
||||
temp.setDate(daysSinceZero + 1)
|
||||
return temp.toISOString().split('T')[0]
|
||||
}
|
||||
|
||||
decode (num, arr) {
|
||||
if (num < 0 || num >= arr.length) {
|
||||
return num
|
||||
}
|
||||
return arr[num]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import quake2 from './quake2.js'
|
||||
|
||||
export default class quake1 extends quake2 {
|
||||
constructor () {
|
||||
super()
|
||||
this.responseHeader = 'n'
|
||||
this.isQuake1 = true
|
||||
}
|
||||
}
|
||||
import quake2 from './quake2.js'
|
||||
|
||||
export default class quake1 extends quake2 {
|
||||
constructor () {
|
||||
super()
|
||||
this.responseHeader = 'n'
|
||||
this.isQuake1 = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,88 +1,88 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class quake2 extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'latin1'
|
||||
this.delimiter = '\n'
|
||||
this.sendHeader = 'status'
|
||||
this.responseHeader = 'print'
|
||||
this.isQuake1 = false
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
const body = await this.udpSend('\xff\xff\xff\xff' + this.sendHeader + '\x00', packet => {
|
||||
const reader = this.reader(packet)
|
||||
const header = reader.string({ length: 4, encoding: 'latin1' })
|
||||
if (header !== '\xff\xff\xff\xff') return
|
||||
let type
|
||||
if (this.isQuake1) {
|
||||
type = reader.string(this.responseHeader.length)
|
||||
} else {
|
||||
type = reader.string({ encoding: 'latin1' })
|
||||
}
|
||||
if (type !== this.responseHeader) return
|
||||
return reader.rest()
|
||||
})
|
||||
|
||||
const reader = this.reader(body)
|
||||
const info = reader.string().split('\\')
|
||||
if (info[0] === '') info.shift()
|
||||
|
||||
while (true) {
|
||||
const key = info.shift()
|
||||
const value = info.shift()
|
||||
if (typeof value === 'undefined') break
|
||||
state.raw[key] = value
|
||||
}
|
||||
|
||||
while (!reader.done()) {
|
||||
const line = reader.string()
|
||||
if (!line || line.charAt(0) === '\0') break
|
||||
|
||||
const args = []
|
||||
const split = line.split('"')
|
||||
split.forEach((part, i) => {
|
||||
const inQuote = (i % 2 === 1)
|
||||
if (inQuote) {
|
||||
args.push(part)
|
||||
} else {
|
||||
const splitSpace = part.split(' ')
|
||||
for (const subpart of splitSpace) {
|
||||
if (subpart) args.push(subpart)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const player = {}
|
||||
if (this.isQuake1) {
|
||||
player.id = parseInt(args.shift())
|
||||
player.score = parseInt(args.shift())
|
||||
player.time = parseInt(args.shift())
|
||||
player.ping = parseInt(args.shift())
|
||||
player.name = args.shift()
|
||||
player.skin = args.shift()
|
||||
player.color1 = parseInt(args.shift())
|
||||
player.color2 = parseInt(args.shift())
|
||||
} else {
|
||||
player.frags = parseInt(args.shift())
|
||||
player.ping = parseInt(args.shift())
|
||||
player.name = args.shift() || ''
|
||||
if (!player.name) delete player.name
|
||||
player.address = args.shift() || ''
|
||||
if (!player.address) delete player.address
|
||||
}
|
||||
|
||||
(player.ping ? state.players : state.bots).push(player)
|
||||
}
|
||||
|
||||
if ('g_needpass' in state.raw) state.password = state.raw.g_needpass
|
||||
if ('mapname' in state.raw) state.map = state.raw.mapname
|
||||
if ('sv_maxclients' in state.raw) state.maxplayers = state.raw.sv_maxclients
|
||||
if ('maxclients' in state.raw) state.maxplayers = state.raw.maxclients
|
||||
if ('sv_hostname' in state.raw) state.name = state.raw.sv_hostname
|
||||
if ('hostname' in state.raw) state.name = state.raw.hostname
|
||||
if ('clients' in state.raw) state.numplayers = state.raw.clients
|
||||
else state.numplayers = state.players.length + state.bots.length
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class quake2 extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'latin1'
|
||||
this.delimiter = '\n'
|
||||
this.sendHeader = 'status'
|
||||
this.responseHeader = 'print'
|
||||
this.isQuake1 = false
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
const body = await this.udpSend('\xff\xff\xff\xff' + this.sendHeader + '\x00', packet => {
|
||||
const reader = this.reader(packet)
|
||||
const header = reader.string({ length: 4, encoding: 'latin1' })
|
||||
if (header !== '\xff\xff\xff\xff') return
|
||||
let type
|
||||
if (this.isQuake1) {
|
||||
type = reader.string(this.responseHeader.length)
|
||||
} else {
|
||||
type = reader.string({ encoding: 'latin1' })
|
||||
}
|
||||
if (type !== this.responseHeader) return
|
||||
return reader.rest()
|
||||
})
|
||||
|
||||
const reader = this.reader(body)
|
||||
const info = reader.string().split('\\')
|
||||
if (info[0] === '') info.shift()
|
||||
|
||||
while (true) {
|
||||
const key = info.shift()
|
||||
const value = info.shift()
|
||||
if (typeof value === 'undefined') break
|
||||
state.raw[key] = value
|
||||
}
|
||||
|
||||
while (!reader.done()) {
|
||||
const line = reader.string()
|
||||
if (!line || line.charAt(0) === '\0') break
|
||||
|
||||
const args = []
|
||||
const split = line.split('"')
|
||||
split.forEach((part, i) => {
|
||||
const inQuote = (i % 2 === 1)
|
||||
if (inQuote) {
|
||||
args.push(part)
|
||||
} else {
|
||||
const splitSpace = part.split(' ')
|
||||
for (const subpart of splitSpace) {
|
||||
if (subpart) args.push(subpart)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const player = {}
|
||||
if (this.isQuake1) {
|
||||
player.id = parseInt(args.shift())
|
||||
player.score = parseInt(args.shift())
|
||||
player.time = parseInt(args.shift())
|
||||
player.ping = parseInt(args.shift())
|
||||
player.name = args.shift()
|
||||
player.skin = args.shift()
|
||||
player.color1 = parseInt(args.shift())
|
||||
player.color2 = parseInt(args.shift())
|
||||
} else {
|
||||
player.frags = parseInt(args.shift())
|
||||
player.ping = parseInt(args.shift())
|
||||
player.name = args.shift() || ''
|
||||
if (!player.name) delete player.name
|
||||
player.address = args.shift() || ''
|
||||
if (!player.address) delete player.address
|
||||
}
|
||||
|
||||
(player.ping ? state.players : state.bots).push(player)
|
||||
}
|
||||
|
||||
if ('g_needpass' in state.raw) state.password = state.raw.g_needpass
|
||||
if ('mapname' in state.raw) state.map = state.raw.mapname
|
||||
if ('sv_maxclients' in state.raw) state.maxplayers = state.raw.sv_maxclients
|
||||
if ('maxclients' in state.raw) state.maxplayers = state.raw.maxclients
|
||||
if ('sv_hostname' in state.raw) state.name = state.raw.sv_hostname
|
||||
if ('hostname' in state.raw) state.name = state.raw.hostname
|
||||
if ('clients' in state.raw) state.numplayers = state.raw.clients
|
||||
else state.numplayers = state.players.length + state.bots.length
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
import quake2 from './quake2.js'
|
||||
|
||||
export default class quake3 extends quake2 {
|
||||
constructor () {
|
||||
super()
|
||||
this.sendHeader = 'getstatus'
|
||||
this.responseHeader = 'statusResponse'
|
||||
import quake2 from './quake2.js'
|
||||
|
||||
export default class quake3 extends quake2 {
|
||||
constructor () {
|
||||
super()
|
||||
this.sendHeader = 'getstatus'
|
||||
this.responseHeader = 'statusResponse'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
await super.run(state)
|
||||
state.name = this.stripColors(state.name)
|
||||
for (const key of Object.keys(state.raw)) {
|
||||
state.raw[key] = this.stripColors(state.raw[key])
|
||||
}
|
||||
for (const player of state.players) {
|
||||
player.name = this.stripColors(player.name)
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
await super.run(state)
|
||||
state.name = this.stripColors(state.name)
|
||||
for (const key of Object.keys(state.raw)) {
|
||||
state.raw[key] = this.stripColors(state.raw[key])
|
||||
}
|
||||
for (const player of state.players) {
|
||||
player.name = this.stripColors(player.name)
|
||||
}
|
||||
}
|
||||
|
||||
stripColors (str) {
|
||||
return str.replace(/\^(X.{6}|.)/g, '')
|
||||
}
|
||||
}
|
||||
|
||||
stripColors (str) {
|
||||
return str.replace(/\^(X.{6}|.)/g, '')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,69 +1,69 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class rfactor extends Core {
|
||||
async run (state) {
|
||||
const buffer = await this.udpSend('rF_S', b => b)
|
||||
const reader = this.reader(buffer)
|
||||
|
||||
state.raw.gamename = this.readString(reader, 8)
|
||||
state.raw.fullUpdate = reader.uint(1)
|
||||
state.raw.region = reader.uint(2)
|
||||
state.raw.ip = reader.part(4)
|
||||
state.raw.size = reader.uint(2)
|
||||
state.raw.version = reader.uint(2)
|
||||
state.raw.versionRaceCast = reader.uint(2)
|
||||
state.gamePort = reader.uint(2)
|
||||
state.raw.queryPort = reader.uint(2)
|
||||
state.raw.game = this.readString(reader, 20)
|
||||
state.name = this.readString(reader, 28)
|
||||
state.map = this.readString(reader, 32)
|
||||
state.raw.motd = this.readString(reader, 96)
|
||||
state.raw.packedAids = reader.uint(2)
|
||||
state.raw.ping = reader.uint(2)
|
||||
state.raw.packedFlags = reader.uint(1)
|
||||
state.raw.rate = reader.uint(1)
|
||||
state.numplayers = reader.uint(1)
|
||||
state.maxplayers = reader.uint(1)
|
||||
state.raw.bots = reader.uint(1)
|
||||
state.raw.packedSpecial = reader.uint(1)
|
||||
state.raw.damage = reader.uint(1)
|
||||
state.raw.packedRules = reader.uint(2)
|
||||
state.raw.credits1 = reader.uint(1)
|
||||
state.raw.credits2 = reader.uint(2)
|
||||
this.logger.debug(reader.offset())
|
||||
state.raw.time = reader.uint(2)
|
||||
state.raw.laps = reader.uint(2) / 16
|
||||
reader.skip(3)
|
||||
state.raw.vehicles = reader.string()
|
||||
|
||||
state.password = !!(state.raw.packedSpecial & 2)
|
||||
state.raw.raceCast = !!(state.raw.packedSpecial & 4)
|
||||
state.raw.fixedSetups = !!(state.raw.packedSpecial & 16)
|
||||
|
||||
const aids = [
|
||||
'TractionControl',
|
||||
'AntiLockBraking',
|
||||
'StabilityControl',
|
||||
'AutoShifting',
|
||||
'AutoClutch',
|
||||
'Invulnerability',
|
||||
'OppositeLock',
|
||||
'SteeringHelp',
|
||||
'BrakingHelp',
|
||||
'SpinRecovery',
|
||||
'AutoPitstop'
|
||||
]
|
||||
state.raw.aids = []
|
||||
for (let offset = 0; offset < aids.length; offset++) {
|
||||
if (state.packedAids && (1 << offset)) {
|
||||
state.raw.aids.push(aids[offset])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Consumes bytesToConsume, but only returns string up to the first null
|
||||
readString (reader, bytesToConsume) {
|
||||
const consumed = reader.part(bytesToConsume)
|
||||
return this.reader(consumed).string()
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class rfactor extends Core {
|
||||
async run (state) {
|
||||
const buffer = await this.udpSend('rF_S', b => b)
|
||||
const reader = this.reader(buffer)
|
||||
|
||||
state.raw.gamename = this.readString(reader, 8)
|
||||
state.raw.fullUpdate = reader.uint(1)
|
||||
state.raw.region = reader.uint(2)
|
||||
state.raw.ip = reader.part(4)
|
||||
state.raw.size = reader.uint(2)
|
||||
state.raw.version = reader.uint(2)
|
||||
state.raw.versionRaceCast = reader.uint(2)
|
||||
state.gamePort = reader.uint(2)
|
||||
state.raw.queryPort = reader.uint(2)
|
||||
state.raw.game = this.readString(reader, 20)
|
||||
state.name = this.readString(reader, 28)
|
||||
state.map = this.readString(reader, 32)
|
||||
state.raw.motd = this.readString(reader, 96)
|
||||
state.raw.packedAids = reader.uint(2)
|
||||
state.raw.ping = reader.uint(2)
|
||||
state.raw.packedFlags = reader.uint(1)
|
||||
state.raw.rate = reader.uint(1)
|
||||
state.numplayers = reader.uint(1)
|
||||
state.maxplayers = reader.uint(1)
|
||||
state.raw.bots = reader.uint(1)
|
||||
state.raw.packedSpecial = reader.uint(1)
|
||||
state.raw.damage = reader.uint(1)
|
||||
state.raw.packedRules = reader.uint(2)
|
||||
state.raw.credits1 = reader.uint(1)
|
||||
state.raw.credits2 = reader.uint(2)
|
||||
this.logger.debug(reader.offset())
|
||||
state.raw.time = reader.uint(2)
|
||||
state.raw.laps = reader.uint(2) / 16
|
||||
reader.skip(3)
|
||||
state.raw.vehicles = reader.string()
|
||||
|
||||
state.password = !!(state.raw.packedSpecial & 2)
|
||||
state.raw.raceCast = !!(state.raw.packedSpecial & 4)
|
||||
state.raw.fixedSetups = !!(state.raw.packedSpecial & 16)
|
||||
|
||||
const aids = [
|
||||
'TractionControl',
|
||||
'AntiLockBraking',
|
||||
'StabilityControl',
|
||||
'AutoShifting',
|
||||
'AutoClutch',
|
||||
'Invulnerability',
|
||||
'OppositeLock',
|
||||
'SteeringHelp',
|
||||
'BrakingHelp',
|
||||
'SpinRecovery',
|
||||
'AutoPitstop'
|
||||
]
|
||||
state.raw.aids = []
|
||||
for (let offset = 0; offset < aids.length; offset++) {
|
||||
if (state.packedAids && (1 << offset)) {
|
||||
state.raw.aids.push(aids[offset])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Consumes bytesToConsume, but only returns string up to the first null
|
||||
readString (reader, bytesToConsume) {
|
||||
const consumed = reader.part(bytesToConsume)
|
||||
return this.reader(consumed).string()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,102 +1,102 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class samp extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'win1252'
|
||||
this.magicHeader = 'SAMP'
|
||||
this.responseMagicHeader = null
|
||||
this.isVcmp = false
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
// read info
|
||||
{
|
||||
const reader = await this.sendPacket('i')
|
||||
if (this.isVcmp) {
|
||||
const consumed = reader.part(12)
|
||||
state.raw.version = this.reader(consumed).string()
|
||||
}
|
||||
state.password = !!reader.uint(1)
|
||||
state.numplayers = reader.uint(2)
|
||||
state.maxplayers = reader.uint(2)
|
||||
state.name = reader.pascalString(4)
|
||||
state.raw.gamemode = reader.pascalString(4)
|
||||
state.raw.map = reader.pascalString(4)
|
||||
}
|
||||
|
||||
// read rules
|
||||
if (!this.isVcmp) {
|
||||
const reader = await this.sendPacket('r')
|
||||
const ruleCount = reader.uint(2)
|
||||
state.raw.rules = {}
|
||||
for (let i = 0; i < ruleCount; i++) {
|
||||
const key = reader.pascalString(1)
|
||||
const value = reader.pascalString(1)
|
||||
state.raw.rules[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// read players
|
||||
// don't even bother if > 100 players, because the server won't respond
|
||||
if (state.numplayers < 100) {
|
||||
if (this.isVcmp) {
|
||||
const reader = await this.sendPacket('c', true)
|
||||
if (reader !== null) {
|
||||
const playerCount = reader.uint(2)
|
||||
for (let i = 0; i < playerCount; i++) {
|
||||
const player = {}
|
||||
player.name = reader.pascalString(1)
|
||||
state.players.push(player)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const reader = await this.sendPacket('d', true)
|
||||
if (reader !== null) {
|
||||
const playerCount = reader.uint(2)
|
||||
for (let i = 0; i < playerCount; i++) {
|
||||
const player = {}
|
||||
player.id = reader.uint(1)
|
||||
player.name = reader.pascalString(1)
|
||||
player.score = reader.int(4)
|
||||
player.ping = reader.uint(4)
|
||||
state.players.push(player)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async sendPacket (type, allowTimeout) {
|
||||
const outBuffer = Buffer.alloc(11)
|
||||
outBuffer.write(this.magicHeader, 0, 4)
|
||||
const ipSplit = this.options.address.split('.')
|
||||
outBuffer.writeUInt8(parseInt(ipSplit[0]), 4)
|
||||
outBuffer.writeUInt8(parseInt(ipSplit[1]), 5)
|
||||
outBuffer.writeUInt8(parseInt(ipSplit[2]), 6)
|
||||
outBuffer.writeUInt8(parseInt(ipSplit[3]), 7)
|
||||
outBuffer.writeUInt16LE(this.options.port, 8)
|
||||
outBuffer.writeUInt8(type.charCodeAt(0), 10)
|
||||
|
||||
const checkBuffer = Buffer.from(outBuffer)
|
||||
if (this.responseMagicHeader) {
|
||||
checkBuffer.write(this.responseMagicHeader, 0, 4)
|
||||
}
|
||||
|
||||
return await this.udpSend(
|
||||
outBuffer,
|
||||
(buffer) => {
|
||||
const reader = this.reader(buffer)
|
||||
for (let i = 0; i < checkBuffer.length; i++) {
|
||||
if (checkBuffer.readUInt8(i) !== reader.uint(1)) return
|
||||
}
|
||||
return reader
|
||||
},
|
||||
() => {
|
||||
if (allowTimeout) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class samp extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'win1252'
|
||||
this.magicHeader = 'SAMP'
|
||||
this.responseMagicHeader = null
|
||||
this.isVcmp = false
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
// read info
|
||||
{
|
||||
const reader = await this.sendPacket('i')
|
||||
if (this.isVcmp) {
|
||||
const consumed = reader.part(12)
|
||||
state.raw.version = this.reader(consumed).string()
|
||||
}
|
||||
state.password = !!reader.uint(1)
|
||||
state.numplayers = reader.uint(2)
|
||||
state.maxplayers = reader.uint(2)
|
||||
state.name = reader.pascalString(4)
|
||||
state.raw.gamemode = reader.pascalString(4)
|
||||
state.raw.map = reader.pascalString(4)
|
||||
}
|
||||
|
||||
// read rules
|
||||
if (!this.isVcmp) {
|
||||
const reader = await this.sendPacket('r')
|
||||
const ruleCount = reader.uint(2)
|
||||
state.raw.rules = {}
|
||||
for (let i = 0; i < ruleCount; i++) {
|
||||
const key = reader.pascalString(1)
|
||||
const value = reader.pascalString(1)
|
||||
state.raw.rules[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// read players
|
||||
// don't even bother if > 100 players, because the server won't respond
|
||||
if (state.numplayers < 100) {
|
||||
if (this.isVcmp) {
|
||||
const reader = await this.sendPacket('c', true)
|
||||
if (reader !== null) {
|
||||
const playerCount = reader.uint(2)
|
||||
for (let i = 0; i < playerCount; i++) {
|
||||
const player = {}
|
||||
player.name = reader.pascalString(1)
|
||||
state.players.push(player)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const reader = await this.sendPacket('d', true)
|
||||
if (reader !== null) {
|
||||
const playerCount = reader.uint(2)
|
||||
for (let i = 0; i < playerCount; i++) {
|
||||
const player = {}
|
||||
player.id = reader.uint(1)
|
||||
player.name = reader.pascalString(1)
|
||||
player.score = reader.int(4)
|
||||
player.ping = reader.uint(4)
|
||||
state.players.push(player)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async sendPacket (type, allowTimeout) {
|
||||
const outBuffer = Buffer.alloc(11)
|
||||
outBuffer.write(this.magicHeader, 0, 4)
|
||||
const ipSplit = this.options.address.split('.')
|
||||
outBuffer.writeUInt8(parseInt(ipSplit[0]), 4)
|
||||
outBuffer.writeUInt8(parseInt(ipSplit[1]), 5)
|
||||
outBuffer.writeUInt8(parseInt(ipSplit[2]), 6)
|
||||
outBuffer.writeUInt8(parseInt(ipSplit[3]), 7)
|
||||
outBuffer.writeUInt16LE(this.options.port, 8)
|
||||
outBuffer.writeUInt8(type.charCodeAt(0), 10)
|
||||
|
||||
const checkBuffer = Buffer.from(outBuffer)
|
||||
if (this.responseMagicHeader) {
|
||||
checkBuffer.write(this.responseMagicHeader, 0, 4)
|
||||
}
|
||||
|
||||
return await this.udpSend(
|
||||
outBuffer,
|
||||
(buffer) => {
|
||||
const reader = this.reader(buffer)
|
||||
for (let i = 0; i < checkBuffer.length; i++) {
|
||||
if (checkBuffer.readUInt8(i) !== reader.uint(1)) return
|
||||
}
|
||||
return reader
|
||||
},
|
||||
() => {
|
||||
if (allowTimeout) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class savage2 extends Core {
|
||||
async run (state) {
|
||||
const buffer = await this.udpSend('\x01', b => b)
|
||||
const reader = this.reader(buffer)
|
||||
|
||||
reader.skip(12)
|
||||
state.name = this.stripColorCodes(reader.string())
|
||||
state.numplayers = reader.uint(1)
|
||||
state.maxplayers = reader.uint(1)
|
||||
state.raw.time = reader.string()
|
||||
state.map = reader.string()
|
||||
state.raw.nextmap = reader.string()
|
||||
state.raw.location = reader.string()
|
||||
state.raw.minplayers = reader.uint(1)
|
||||
state.raw.gametype = reader.string()
|
||||
state.raw.version = reader.string()
|
||||
state.raw.minlevel = reader.uint(1)
|
||||
}
|
||||
|
||||
stripColorCodes (str) {
|
||||
return str.replace(/\^./g, '')
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class savage2 extends Core {
|
||||
async run (state) {
|
||||
const buffer = await this.udpSend('\x01', b => b)
|
||||
const reader = this.reader(buffer)
|
||||
|
||||
reader.skip(12)
|
||||
state.name = this.stripColorCodes(reader.string())
|
||||
state.numplayers = reader.uint(1)
|
||||
state.maxplayers = reader.uint(1)
|
||||
state.raw.time = reader.string()
|
||||
state.map = reader.string()
|
||||
state.raw.nextmap = reader.string()
|
||||
state.raw.location = reader.string()
|
||||
state.raw.minplayers = reader.uint(1)
|
||||
state.raw.gametype = reader.string()
|
||||
state.raw.version = reader.string()
|
||||
state.raw.minlevel = reader.uint(1)
|
||||
}
|
||||
|
||||
stripColorCodes (str) {
|
||||
return str.replace(/\^./g, '')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,67 +1,67 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class starmade extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'latin1'
|
||||
this.byteorder = 'be'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
const b = Buffer.from([0x00, 0x00, 0x00, 0x09, 0x2a, 0xff, 0xff, 0x01, 0x6f, 0x00, 0x00, 0x00, 0x00])
|
||||
|
||||
const payload = await this.withTcp(async socket => {
|
||||
return await this.tcpSend(socket, b, buffer => {
|
||||
if (buffer.length < 12) return
|
||||
const reader = this.reader(buffer)
|
||||
const packetLength = reader.uint(4)
|
||||
this.logger.debug('Received packet length: ' + packetLength)
|
||||
const timestamp = reader.uint(8).toString()
|
||||
this.logger.debug('Received timestamp: ' + timestamp)
|
||||
if (reader.remaining() < packetLength || reader.remaining() < 5) return
|
||||
|
||||
const checkId = reader.uint(1)
|
||||
const packetId = reader.uint(2)
|
||||
const commandId = reader.uint(1)
|
||||
const type = reader.uint(1)
|
||||
|
||||
this.logger.debug('checkId=' + checkId + ' packetId=' + packetId + ' commandId=' + commandId + ' type=' + type)
|
||||
if (checkId !== 0x2a) return
|
||||
|
||||
return reader.rest()
|
||||
})
|
||||
})
|
||||
|
||||
const reader = this.reader(payload)
|
||||
|
||||
const data = []
|
||||
state.raw.data = data
|
||||
|
||||
while (!reader.done()) {
|
||||
const mark = reader.uint(1)
|
||||
if (mark === 1) {
|
||||
// signed int
|
||||
data.push(reader.int(4))
|
||||
} else if (mark === 3) {
|
||||
// float
|
||||
data.push(reader.float())
|
||||
} else if (mark === 4) {
|
||||
// string
|
||||
data.push(reader.pascalString(2))
|
||||
} else if (mark === 6) {
|
||||
// byte
|
||||
data.push(reader.uint(1))
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.debug('Received raw data array', data)
|
||||
|
||||
if (typeof data[0] === 'number') state.raw.infoVersion = data[0]
|
||||
if (typeof data[1] === 'number') state.raw.version = data[1]
|
||||
if (typeof data[2] === 'string') state.name = data[2]
|
||||
if (typeof data[3] === 'string') state.raw.description = data[3]
|
||||
if (typeof data[4] === 'number') state.raw.startTime = data[4]
|
||||
if (typeof data[5] === 'number') state.numplayers = data[5]
|
||||
if (typeof data[6] === 'number') state.maxplayers = data[6]
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class starmade extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'latin1'
|
||||
this.byteorder = 'be'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
const b = Buffer.from([0x00, 0x00, 0x00, 0x09, 0x2a, 0xff, 0xff, 0x01, 0x6f, 0x00, 0x00, 0x00, 0x00])
|
||||
|
||||
const payload = await this.withTcp(async socket => {
|
||||
return await this.tcpSend(socket, b, buffer => {
|
||||
if (buffer.length < 12) return
|
||||
const reader = this.reader(buffer)
|
||||
const packetLength = reader.uint(4)
|
||||
this.logger.debug('Received packet length: ' + packetLength)
|
||||
const timestamp = reader.uint(8).toString()
|
||||
this.logger.debug('Received timestamp: ' + timestamp)
|
||||
if (reader.remaining() < packetLength || reader.remaining() < 5) return
|
||||
|
||||
const checkId = reader.uint(1)
|
||||
const packetId = reader.uint(2)
|
||||
const commandId = reader.uint(1)
|
||||
const type = reader.uint(1)
|
||||
|
||||
this.logger.debug('checkId=' + checkId + ' packetId=' + packetId + ' commandId=' + commandId + ' type=' + type)
|
||||
if (checkId !== 0x2a) return
|
||||
|
||||
return reader.rest()
|
||||
})
|
||||
})
|
||||
|
||||
const reader = this.reader(payload)
|
||||
|
||||
const data = []
|
||||
state.raw.data = data
|
||||
|
||||
while (!reader.done()) {
|
||||
const mark = reader.uint(1)
|
||||
if (mark === 1) {
|
||||
// signed int
|
||||
data.push(reader.int(4))
|
||||
} else if (mark === 3) {
|
||||
// float
|
||||
data.push(reader.float())
|
||||
} else if (mark === 4) {
|
||||
// string
|
||||
data.push(reader.pascalString(2))
|
||||
} else if (mark === 6) {
|
||||
// byte
|
||||
data.push(reader.uint(1))
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.debug('Received raw data array', data)
|
||||
|
||||
if (typeof data[0] === 'number') state.raw.infoVersion = data[0]
|
||||
if (typeof data[1] === 'number') state.raw.version = data[1]
|
||||
if (typeof data[2] === 'string') state.name = data[2]
|
||||
if (typeof data[3] === 'string') state.raw.description = data[3]
|
||||
if (typeof data[4] === 'number') state.raw.startTime = data[4]
|
||||
if (typeof data[5] === 'number') state.numplayers = data[5]
|
||||
if (typeof data[6] === 'number') state.maxplayers = data[6]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import tribes1 from './tribes1.js'
|
||||
|
||||
export default class starsiege extends tribes1 {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'latin1'
|
||||
this.requestByte = 0x72
|
||||
this.responseByte = 0x73
|
||||
}
|
||||
}
|
||||
import tribes1 from './tribes1.js'
|
||||
|
||||
export default class starsiege extends tribes1 {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'latin1'
|
||||
this.requestByte = 0x72
|
||||
this.responseByte = 0x73
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,71 +1,71 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class teamspeak2 extends Core {
|
||||
async run (state) {
|
||||
const queryPort = this.options.teamspeakQueryPort || 51234
|
||||
|
||||
await this.withTcp(async socket => {
|
||||
{
|
||||
const data = await this.sendCommand(socket, 'sel ' + this.options.port)
|
||||
if (data !== '[TS]') throw new Error('Invalid header')
|
||||
}
|
||||
|
||||
{
|
||||
const data = await this.sendCommand(socket, 'si')
|
||||
for (const line of data.split('\r\n')) {
|
||||
const equals = line.indexOf('=')
|
||||
const key = equals === -1 ? line : line.substring(0, equals)
|
||||
const value = equals === -1 ? '' : line.substring(equals + 1)
|
||||
state.raw[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const data = await this.sendCommand(socket, 'pl')
|
||||
const split = data.split('\r\n')
|
||||
const fields = split.shift().split('\t')
|
||||
for (const line of split) {
|
||||
const split2 = line.split('\t')
|
||||
const player = {}
|
||||
split2.forEach((value, i) => {
|
||||
let key = fields[i]
|
||||
if (!key) return
|
||||
if (key === 'nick') key = 'name'
|
||||
const m = value.match(/^"(.*)"$/)
|
||||
if (m) value = m[1]
|
||||
player[key] = value
|
||||
})
|
||||
state.players.push(player)
|
||||
}
|
||||
state.numplayers = state.players.length
|
||||
}
|
||||
|
||||
{
|
||||
const data = await this.sendCommand(socket, 'cl')
|
||||
const split = data.split('\r\n')
|
||||
const fields = split.shift().split('\t')
|
||||
state.raw.channels = []
|
||||
for (const line of split) {
|
||||
const split2 = line.split('\t')
|
||||
const channel = {}
|
||||
split2.forEach((value, i) => {
|
||||
const key = fields[i]
|
||||
if (!key) return
|
||||
const m = value.match(/^"(.*)"$/)
|
||||
if (m) value = m[1]
|
||||
channel[key] = value
|
||||
})
|
||||
state.raw.channels.push(channel)
|
||||
}
|
||||
}
|
||||
}, queryPort)
|
||||
}
|
||||
|
||||
async sendCommand (socket, cmd) {
|
||||
return await this.tcpSend(socket, cmd + '\x0A', buffer => {
|
||||
if (buffer.length < 6) return
|
||||
if (buffer.slice(-6).toString() !== '\r\nOK\r\n') return
|
||||
return buffer.slice(0, -6).toString()
|
||||
})
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class teamspeak2 extends Core {
|
||||
async run (state) {
|
||||
const queryPort = this.options.teamspeakQueryPort || 51234
|
||||
|
||||
await this.withTcp(async socket => {
|
||||
{
|
||||
const data = await this.sendCommand(socket, 'sel ' + this.options.port)
|
||||
if (data !== '[TS]') throw new Error('Invalid header')
|
||||
}
|
||||
|
||||
{
|
||||
const data = await this.sendCommand(socket, 'si')
|
||||
for (const line of data.split('\r\n')) {
|
||||
const equals = line.indexOf('=')
|
||||
const key = equals === -1 ? line : line.substring(0, equals)
|
||||
const value = equals === -1 ? '' : line.substring(equals + 1)
|
||||
state.raw[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const data = await this.sendCommand(socket, 'pl')
|
||||
const split = data.split('\r\n')
|
||||
const fields = split.shift().split('\t')
|
||||
for (const line of split) {
|
||||
const split2 = line.split('\t')
|
||||
const player = {}
|
||||
split2.forEach((value, i) => {
|
||||
let key = fields[i]
|
||||
if (!key) return
|
||||
if (key === 'nick') key = 'name'
|
||||
const m = value.match(/^"(.*)"$/)
|
||||
if (m) value = m[1]
|
||||
player[key] = value
|
||||
})
|
||||
state.players.push(player)
|
||||
}
|
||||
state.numplayers = state.players.length
|
||||
}
|
||||
|
||||
{
|
||||
const data = await this.sendCommand(socket, 'cl')
|
||||
const split = data.split('\r\n')
|
||||
const fields = split.shift().split('\t')
|
||||
state.raw.channels = []
|
||||
for (const line of split) {
|
||||
const split2 = line.split('\t')
|
||||
const channel = {}
|
||||
split2.forEach((value, i) => {
|
||||
const key = fields[i]
|
||||
if (!key) return
|
||||
const m = value.match(/^"(.*)"$/)
|
||||
if (m) value = m[1]
|
||||
channel[key] = value
|
||||
})
|
||||
state.raw.channels.push(channel)
|
||||
}
|
||||
}
|
||||
}, queryPort)
|
||||
}
|
||||
|
||||
async sendCommand (socket, cmd) {
|
||||
return await this.tcpSend(socket, cmd + '\x0A', buffer => {
|
||||
if (buffer.length < 6) return
|
||||
if (buffer.slice(-6).toString() !== '\r\nOK\r\n') return
|
||||
return buffer.slice(0, -6).toString()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,69 +1,69 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class teamspeak3 extends Core {
|
||||
async run (state) {
|
||||
const queryPort = this.options.teamspeakQueryPort || 10011
|
||||
|
||||
await this.withTcp(async socket => {
|
||||
{
|
||||
const data = await this.sendCommand(socket, 'use port=' + this.options.port, true)
|
||||
const split = data.split('\n\r')
|
||||
if (split[0] !== 'TS3') throw new Error('Invalid header')
|
||||
}
|
||||
|
||||
{
|
||||
const data = await this.sendCommand(socket, 'serverinfo')
|
||||
state.raw = data[0]
|
||||
if ('virtualserver_name' in state.raw) state.name = state.raw.virtualserver_name
|
||||
if ('virtualserver_maxclients' in state.raw) state.maxplayers = state.raw.virtualserver_maxclients
|
||||
if ('virtualserver_clientsonline' in state.raw) state.numplayers = state.raw.virtualserver_clientsonline
|
||||
}
|
||||
|
||||
{
|
||||
const list = await this.sendCommand(socket, 'clientlist')
|
||||
for (const client of list) {
|
||||
client.name = client.client_nickname
|
||||
delete client.client_nickname
|
||||
if (client.client_type === '0') {
|
||||
state.players.push(client)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const data = await this.sendCommand(socket, 'channellist -topic')
|
||||
state.raw.channels = data
|
||||
}
|
||||
}, queryPort)
|
||||
}
|
||||
|
||||
async sendCommand (socket, cmd, raw) {
|
||||
const body = await this.tcpSend(socket, cmd + '\x0A', (buffer) => {
|
||||
if (buffer.length < 21) return
|
||||
if (buffer.slice(-21).toString() !== '\n\rerror id=0 msg=ok\n\r') return
|
||||
return buffer.slice(0, -21).toString()
|
||||
})
|
||||
|
||||
if (raw) {
|
||||
return body
|
||||
} else {
|
||||
const segments = body.split('|')
|
||||
const out = []
|
||||
for (const line of segments) {
|
||||
const split = line.split(' ')
|
||||
const unit = {}
|
||||
for (const field of split) {
|
||||
const equals = field.indexOf('=')
|
||||
const key = equals === -1 ? field : field.substring(0, equals)
|
||||
const value = equals === -1
|
||||
? ''
|
||||
: field.substring(equals + 1)
|
||||
.replace(/\\s/g, ' ').replace(/\\\//g, '/')
|
||||
unit[key] = value
|
||||
}
|
||||
out.push(unit)
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class teamspeak3 extends Core {
|
||||
async run (state) {
|
||||
const queryPort = this.options.teamspeakQueryPort || 10011
|
||||
|
||||
await this.withTcp(async socket => {
|
||||
{
|
||||
const data = await this.sendCommand(socket, 'use port=' + this.options.port, true)
|
||||
const split = data.split('\n\r')
|
||||
if (split[0] !== 'TS3') throw new Error('Invalid header')
|
||||
}
|
||||
|
||||
{
|
||||
const data = await this.sendCommand(socket, 'serverinfo')
|
||||
state.raw = data[0]
|
||||
if ('virtualserver_name' in state.raw) state.name = state.raw.virtualserver_name
|
||||
if ('virtualserver_maxclients' in state.raw) state.maxplayers = state.raw.virtualserver_maxclients
|
||||
if ('virtualserver_clientsonline' in state.raw) state.numplayers = state.raw.virtualserver_clientsonline
|
||||
}
|
||||
|
||||
{
|
||||
const list = await this.sendCommand(socket, 'clientlist')
|
||||
for (const client of list) {
|
||||
client.name = client.client_nickname
|
||||
delete client.client_nickname
|
||||
if (client.client_type === '0') {
|
||||
state.players.push(client)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const data = await this.sendCommand(socket, 'channellist -topic')
|
||||
state.raw.channels = data
|
||||
}
|
||||
}, queryPort)
|
||||
}
|
||||
|
||||
async sendCommand (socket, cmd, raw) {
|
||||
const body = await this.tcpSend(socket, cmd + '\x0A', (buffer) => {
|
||||
if (buffer.length < 21) return
|
||||
if (buffer.slice(-21).toString() !== '\n\rerror id=0 msg=ok\n\r') return
|
||||
return buffer.slice(0, -21).toString()
|
||||
})
|
||||
|
||||
if (raw) {
|
||||
return body
|
||||
} else {
|
||||
const segments = body.split('|')
|
||||
const out = []
|
||||
for (const line of segments) {
|
||||
const split = line.split(' ')
|
||||
const unit = {}
|
||||
for (const field of split) {
|
||||
const equals = field.indexOf('=')
|
||||
const key = equals === -1 ? field : field.substring(0, equals)
|
||||
const value = equals === -1
|
||||
? ''
|
||||
: field.substring(equals + 1)
|
||||
.replace(/\\s/g, ' ').replace(/\\\//g, '/')
|
||||
unit[key] = value
|
||||
}
|
||||
out.push(unit)
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class terraria extends Core {
|
||||
async run (state) {
|
||||
const json = await this.request({
|
||||
url: 'http://' + this.options.address + ':' + this.options.port + '/v2/server/status',
|
||||
searchParams: {
|
||||
players: 'true',
|
||||
token: this.options.token
|
||||
},
|
||||
responseType: 'json'
|
||||
})
|
||||
|
||||
if (json.status !== '200') throw new Error('Invalid status')
|
||||
|
||||
for (const one of json.players) {
|
||||
state.players.push({ name: one.nickname, team: one.team })
|
||||
}
|
||||
|
||||
state.name = json.name
|
||||
state.gamePort = json.port
|
||||
state.numplayers = json.playercount
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class terraria extends Core {
|
||||
async run (state) {
|
||||
const json = await this.request({
|
||||
url: 'http://' + this.options.address + ':' + this.options.port + '/v2/server/status',
|
||||
searchParams: {
|
||||
players: 'true',
|
||||
token: this.options.token
|
||||
},
|
||||
responseType: 'json'
|
||||
})
|
||||
|
||||
if (json.status !== '200') throw new Error('Invalid status')
|
||||
|
||||
for (const one of json.players) {
|
||||
state.players.push({ name: one.nickname, team: one.team })
|
||||
}
|
||||
|
||||
state.name = json.name
|
||||
state.gamePort = json.port
|
||||
state.numplayers = json.playercount
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,153 +1,153 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class tribes1 extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'latin1'
|
||||
this.requestByte = 0x62
|
||||
this.responseByte = 0x63
|
||||
this.challenge = 0x01
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
const query = Buffer.alloc(3)
|
||||
query.writeUInt8(this.requestByte, 0)
|
||||
query.writeUInt16LE(this.challenge, 1)
|
||||
const reader = await this.udpSend(query, (buffer) => {
|
||||
const reader = this.reader(buffer)
|
||||
const responseByte = reader.uint(1)
|
||||
if (responseByte !== this.responseByte) {
|
||||
this.logger.debug('Unexpected response byte')
|
||||
return
|
||||
}
|
||||
const challenge = reader.uint(2)
|
||||
if (challenge !== this.challenge) {
|
||||
this.logger.debug('Unexpected challenge')
|
||||
return
|
||||
}
|
||||
const requestByte = reader.uint(1)
|
||||
if (requestByte !== this.requestByte) {
|
||||
this.logger.debug('Unexpected request byte')
|
||||
return
|
||||
}
|
||||
return reader
|
||||
})
|
||||
|
||||
state.raw.gametype = this.readString(reader)
|
||||
const isStarsiege2009 = state.raw.gametype === 'Starsiege'
|
||||
state.raw.version = this.readString(reader)
|
||||
state.name = this.readString(reader)
|
||||
|
||||
if (isStarsiege2009) {
|
||||
state.password = !!reader.uint(1)
|
||||
state.raw.dedicated = !!reader.uint(1)
|
||||
state.raw.dropInProgress = !!reader.uint(1)
|
||||
state.raw.gameInProgress = !!reader.uint(1)
|
||||
state.numplayers = reader.uint(4)
|
||||
state.maxplayers = reader.uint(4)
|
||||
state.raw.teamPlay = reader.uint(1)
|
||||
state.map = this.readString(reader)
|
||||
state.raw.cpuSpeed = reader.uint(2)
|
||||
state.raw.factoryVeh = reader.uint(1)
|
||||
state.raw.allowTecmix = reader.uint(1)
|
||||
state.raw.spawnLimit = reader.uint(4)
|
||||
state.raw.fragLimit = reader.uint(4)
|
||||
state.raw.timeLimit = reader.uint(4)
|
||||
state.raw.techLimit = reader.uint(4)
|
||||
state.raw.combatLimit = reader.uint(4)
|
||||
state.raw.massLimit = reader.uint(4)
|
||||
state.raw.playersSent = reader.uint(4)
|
||||
const teams = { 1: 'yellow', 2: 'blue', 4: 'red', 8: 'purple' }
|
||||
while (!reader.done()) {
|
||||
const player = {}
|
||||
player.name = this.readString(reader)
|
||||
const teamId = reader.uint(1)
|
||||
const team = teams[teamId]
|
||||
if (team) player.team = teams[teamId]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
state.raw.dedicated = !!reader.uint(1)
|
||||
state.password = !!reader.uint(1)
|
||||
state.raw.playerCount = reader.uint(1)
|
||||
state.maxplayers = reader.uint(1)
|
||||
state.raw.cpuSpeed = reader.uint(2)
|
||||
state.raw.mod = this.readString(reader)
|
||||
state.raw.type = this.readString(reader)
|
||||
state.map = this.readString(reader)
|
||||
state.raw.motd = this.readString(reader)
|
||||
state.raw.teamCount = reader.uint(1)
|
||||
|
||||
const teamFields = this.readFieldList(reader)
|
||||
const playerFields = this.readFieldList(reader)
|
||||
|
||||
state.raw.teams = []
|
||||
for (let i = 0; i < state.raw.teamCount; i++) {
|
||||
const teamName = this.readString(reader)
|
||||
const teamValues = this.readValues(reader)
|
||||
|
||||
const teamInfo = {}
|
||||
for (let i = 0; i < teamValues.length && i < teamFields.length; i++) {
|
||||
let key = teamFields[i]
|
||||
let value = teamValues[i]
|
||||
if (key === 'ultra_base') key = 'name'
|
||||
if (value === '%t') value = teamName
|
||||
if (['score', 'players'].includes(key)) value = parseInt(value)
|
||||
teamInfo[key] = value
|
||||
}
|
||||
state.raw.teams.push(teamInfo)
|
||||
}
|
||||
|
||||
for (let i = 0; i < state.raw.playerCount; i++) {
|
||||
const ping = reader.uint(1) * 4
|
||||
const packetLoss = reader.uint(1)
|
||||
const teamNum = reader.uint(1)
|
||||
const name = this.readString(reader)
|
||||
const playerValues = this.readValues(reader)
|
||||
|
||||
const playerInfo = {}
|
||||
for (let i = 0; i < playerValues.length && i < playerFields.length; i++) {
|
||||
const key = playerFields[i]
|
||||
let value = playerValues[i]
|
||||
if (value === '%p') value = ping
|
||||
if (value === '%l') value = packetLoss
|
||||
if (value === '%t') value = teamNum
|
||||
if (value === '%n') value = name
|
||||
if (['score', 'ping', 'pl', 'kills', 'lvl'].includes(key)) value = parseInt(value)
|
||||
if (key === 'team') {
|
||||
const teamId = parseInt(value)
|
||||
if (teamId >= 0 && teamId < state.raw.teams.length && state.raw.teams[teamId].name) {
|
||||
value = state.raw.teams[teamId].name
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
playerInfo[key] = value
|
||||
}
|
||||
state.players.push(playerInfo)
|
||||
}
|
||||
}
|
||||
|
||||
readFieldList (reader) {
|
||||
const str = this.readString(reader)
|
||||
if (!str) return []
|
||||
return ('?' + str)
|
||||
.split('\t')
|
||||
.map((a) => a.substring(1).trim().toLowerCase())
|
||||
.map((a) => a === 'team name' ? 'name' : a)
|
||||
.map((a) => a === 'player name' ? 'name' : a)
|
||||
}
|
||||
|
||||
readValues (reader) {
|
||||
const str = this.readString(reader)
|
||||
if (!str) return []
|
||||
return str
|
||||
.split('\t')
|
||||
.map((a) => a.trim())
|
||||
}
|
||||
|
||||
readString (reader) {
|
||||
return reader.pascalString(1)
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class tribes1 extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'latin1'
|
||||
this.requestByte = 0x62
|
||||
this.responseByte = 0x63
|
||||
this.challenge = 0x01
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
const query = Buffer.alloc(3)
|
||||
query.writeUInt8(this.requestByte, 0)
|
||||
query.writeUInt16LE(this.challenge, 1)
|
||||
const reader = await this.udpSend(query, (buffer) => {
|
||||
const reader = this.reader(buffer)
|
||||
const responseByte = reader.uint(1)
|
||||
if (responseByte !== this.responseByte) {
|
||||
this.logger.debug('Unexpected response byte')
|
||||
return
|
||||
}
|
||||
const challenge = reader.uint(2)
|
||||
if (challenge !== this.challenge) {
|
||||
this.logger.debug('Unexpected challenge')
|
||||
return
|
||||
}
|
||||
const requestByte = reader.uint(1)
|
||||
if (requestByte !== this.requestByte) {
|
||||
this.logger.debug('Unexpected request byte')
|
||||
return
|
||||
}
|
||||
return reader
|
||||
})
|
||||
|
||||
state.raw.gametype = this.readString(reader)
|
||||
const isStarsiege2009 = state.raw.gametype === 'Starsiege'
|
||||
state.raw.version = this.readString(reader)
|
||||
state.name = this.readString(reader)
|
||||
|
||||
if (isStarsiege2009) {
|
||||
state.password = !!reader.uint(1)
|
||||
state.raw.dedicated = !!reader.uint(1)
|
||||
state.raw.dropInProgress = !!reader.uint(1)
|
||||
state.raw.gameInProgress = !!reader.uint(1)
|
||||
state.numplayers = reader.uint(4)
|
||||
state.maxplayers = reader.uint(4)
|
||||
state.raw.teamPlay = reader.uint(1)
|
||||
state.map = this.readString(reader)
|
||||
state.raw.cpuSpeed = reader.uint(2)
|
||||
state.raw.factoryVeh = reader.uint(1)
|
||||
state.raw.allowTecmix = reader.uint(1)
|
||||
state.raw.spawnLimit = reader.uint(4)
|
||||
state.raw.fragLimit = reader.uint(4)
|
||||
state.raw.timeLimit = reader.uint(4)
|
||||
state.raw.techLimit = reader.uint(4)
|
||||
state.raw.combatLimit = reader.uint(4)
|
||||
state.raw.massLimit = reader.uint(4)
|
||||
state.raw.playersSent = reader.uint(4)
|
||||
const teams = { 1: 'yellow', 2: 'blue', 4: 'red', 8: 'purple' }
|
||||
while (!reader.done()) {
|
||||
const player = {}
|
||||
player.name = this.readString(reader)
|
||||
const teamId = reader.uint(1)
|
||||
const team = teams[teamId]
|
||||
if (team) player.team = teams[teamId]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
state.raw.dedicated = !!reader.uint(1)
|
||||
state.password = !!reader.uint(1)
|
||||
state.raw.playerCount = reader.uint(1)
|
||||
state.maxplayers = reader.uint(1)
|
||||
state.raw.cpuSpeed = reader.uint(2)
|
||||
state.raw.mod = this.readString(reader)
|
||||
state.raw.type = this.readString(reader)
|
||||
state.map = this.readString(reader)
|
||||
state.raw.motd = this.readString(reader)
|
||||
state.raw.teamCount = reader.uint(1)
|
||||
|
||||
const teamFields = this.readFieldList(reader)
|
||||
const playerFields = this.readFieldList(reader)
|
||||
|
||||
state.raw.teams = []
|
||||
for (let i = 0; i < state.raw.teamCount; i++) {
|
||||
const teamName = this.readString(reader)
|
||||
const teamValues = this.readValues(reader)
|
||||
|
||||
const teamInfo = {}
|
||||
for (let i = 0; i < teamValues.length && i < teamFields.length; i++) {
|
||||
let key = teamFields[i]
|
||||
let value = teamValues[i]
|
||||
if (key === 'ultra_base') key = 'name'
|
||||
if (value === '%t') value = teamName
|
||||
if (['score', 'players'].includes(key)) value = parseInt(value)
|
||||
teamInfo[key] = value
|
||||
}
|
||||
state.raw.teams.push(teamInfo)
|
||||
}
|
||||
|
||||
for (let i = 0; i < state.raw.playerCount; i++) {
|
||||
const ping = reader.uint(1) * 4
|
||||
const packetLoss = reader.uint(1)
|
||||
const teamNum = reader.uint(1)
|
||||
const name = this.readString(reader)
|
||||
const playerValues = this.readValues(reader)
|
||||
|
||||
const playerInfo = {}
|
||||
for (let i = 0; i < playerValues.length && i < playerFields.length; i++) {
|
||||
const key = playerFields[i]
|
||||
let value = playerValues[i]
|
||||
if (value === '%p') value = ping
|
||||
if (value === '%l') value = packetLoss
|
||||
if (value === '%t') value = teamNum
|
||||
if (value === '%n') value = name
|
||||
if (['score', 'ping', 'pl', 'kills', 'lvl'].includes(key)) value = parseInt(value)
|
||||
if (key === 'team') {
|
||||
const teamId = parseInt(value)
|
||||
if (teamId >= 0 && teamId < state.raw.teams.length && state.raw.teams[teamId].name) {
|
||||
value = state.raw.teams[teamId].name
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
playerInfo[key] = value
|
||||
}
|
||||
state.players.push(playerInfo)
|
||||
}
|
||||
}
|
||||
|
||||
readFieldList (reader) {
|
||||
const str = this.readString(reader)
|
||||
if (!str) return []
|
||||
return ('?' + str)
|
||||
.split('\t')
|
||||
.map((a) => a.substring(1).trim().toLowerCase())
|
||||
.map((a) => a === 'team name' ? 'name' : a)
|
||||
.map((a) => a === 'player name' ? 'name' : a)
|
||||
}
|
||||
|
||||
readValues (reader) {
|
||||
const str = this.readString(reader)
|
||||
if (!str) return []
|
||||
return str
|
||||
.split('\t')
|
||||
.map((a) => a.trim())
|
||||
}
|
||||
|
||||
readString (reader) {
|
||||
return reader.pascalString(1)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,80 +1,80 @@
|
|||
import Core from './core.js'
|
||||
|
||||
/** Unsupported -- use at your own risk!! */
|
||||
|
||||
export default class tribes1master extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'latin1'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
const queryBuffer = Buffer.from([
|
||||
0x10, // standard header
|
||||
0x03, // dump servers
|
||||
0xff, // ask for all packets
|
||||
0x00, // junk
|
||||
0x01, 0x02 // challenge
|
||||
])
|
||||
|
||||
const parts = new Map()
|
||||
let total = 0
|
||||
const full = await this.udpSend(queryBuffer, (buffer) => {
|
||||
const reader = this.reader(buffer)
|
||||
const header = reader.uint(2)
|
||||
if (header !== 0x0610) {
|
||||
this.logger.debug('Header response does not match: ' + header.toString(16))
|
||||
return
|
||||
}
|
||||
const num = reader.uint(1)
|
||||
const t = reader.uint(1)
|
||||
if (t <= 0 || (total > 0 && t !== total)) {
|
||||
throw new Error('Conflicting packet total: ' + t)
|
||||
}
|
||||
total = t
|
||||
|
||||
if (num < 1 || num > total) {
|
||||
this.logger.debug('Invalid packet number: ' + num + ' ' + total)
|
||||
return
|
||||
}
|
||||
if (parts.has(num)) {
|
||||
this.logger.debug('Duplicate part: ' + num)
|
||||
return
|
||||
}
|
||||
|
||||
reader.skip(2) // challenge (0x0201)
|
||||
reader.skip(2) // always 0x6600
|
||||
parts.set(num, reader.rest())
|
||||
|
||||
if (parts.size === total) {
|
||||
const ordered = []
|
||||
for (let i = 1; i <= total; i++) ordered.push(parts.get(i))
|
||||
return Buffer.concat(ordered)
|
||||
}
|
||||
})
|
||||
|
||||
const fullReader = this.reader(full)
|
||||
state.raw.name = this.readString(fullReader)
|
||||
state.raw.motd = this.readString(fullReader)
|
||||
|
||||
state.raw.servers = []
|
||||
while (!fullReader.done()) {
|
||||
fullReader.skip(1) // junk ?
|
||||
const count = fullReader.uint(1)
|
||||
for (let i = 0; i < count; i++) {
|
||||
const six = fullReader.uint(1)
|
||||
if (six !== 6) {
|
||||
throw new Error('Expecting 6')
|
||||
}
|
||||
const ip = fullReader.uint(4)
|
||||
const port = fullReader.uint(2)
|
||||
const ipStr = (ip & 255) + '.' + (ip >> 8 & 255) + '.' + (ip >> 16 & 255) + '.' + (ip >>> 24)
|
||||
state.raw.servers.push(ipStr + ':' + port)
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
/** Unsupported -- use at your own risk!! */
|
||||
|
||||
export default class tribes1master extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'latin1'
|
||||
}
|
||||
|
||||
readString (reader) {
|
||||
return reader.pascalString(1)
|
||||
}
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
const queryBuffer = Buffer.from([
|
||||
0x10, // standard header
|
||||
0x03, // dump servers
|
||||
0xff, // ask for all packets
|
||||
0x00, // junk
|
||||
0x01, 0x02 // challenge
|
||||
])
|
||||
|
||||
const parts = new Map()
|
||||
let total = 0
|
||||
const full = await this.udpSend(queryBuffer, (buffer) => {
|
||||
const reader = this.reader(buffer)
|
||||
const header = reader.uint(2)
|
||||
if (header !== 0x0610) {
|
||||
this.logger.debug('Header response does not match: ' + header.toString(16))
|
||||
return
|
||||
}
|
||||
const num = reader.uint(1)
|
||||
const t = reader.uint(1)
|
||||
if (t <= 0 || (total > 0 && t !== total)) {
|
||||
throw new Error('Conflicting packet total: ' + t)
|
||||
}
|
||||
total = t
|
||||
|
||||
if (num < 1 || num > total) {
|
||||
this.logger.debug('Invalid packet number: ' + num + ' ' + total)
|
||||
return
|
||||
}
|
||||
if (parts.has(num)) {
|
||||
this.logger.debug('Duplicate part: ' + num)
|
||||
return
|
||||
}
|
||||
|
||||
reader.skip(2) // challenge (0x0201)
|
||||
reader.skip(2) // always 0x6600
|
||||
parts.set(num, reader.rest())
|
||||
|
||||
if (parts.size === total) {
|
||||
const ordered = []
|
||||
for (let i = 1; i <= total; i++) ordered.push(parts.get(i))
|
||||
return Buffer.concat(ordered)
|
||||
}
|
||||
})
|
||||
|
||||
const fullReader = this.reader(full)
|
||||
state.raw.name = this.readString(fullReader)
|
||||
state.raw.motd = this.readString(fullReader)
|
||||
|
||||
state.raw.servers = []
|
||||
while (!fullReader.done()) {
|
||||
fullReader.skip(1) // junk ?
|
||||
const count = fullReader.uint(1)
|
||||
for (let i = 0; i < count; i++) {
|
||||
const six = fullReader.uint(1)
|
||||
if (six !== 6) {
|
||||
throw new Error('Expecting 6')
|
||||
}
|
||||
const ip = fullReader.uint(4)
|
||||
const port = fullReader.uint(2)
|
||||
const ipStr = (ip & 255) + '.' + (ip >> 8 & 255) + '.' + (ip >> 16 & 255) + '.' + (ip >>> 24)
|
||||
state.raw.servers.push(ipStr + ':' + port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readString (reader) {
|
||||
return reader.pascalString(1)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,150 +1,150 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class unreal2 extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'latin1'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
let extraInfoReader
|
||||
{
|
||||
const b = await this.sendPacket(0, true)
|
||||
const reader = this.reader(b)
|
||||
state.raw.serverid = reader.uint(4)
|
||||
state.raw.ip = this.readUnrealString(reader)
|
||||
state.gamePort = reader.uint(4)
|
||||
state.raw.queryport = reader.uint(4)
|
||||
state.name = this.readUnrealString(reader, true)
|
||||
state.map = this.readUnrealString(reader, true)
|
||||
state.raw.gametype = this.readUnrealString(reader, true)
|
||||
state.numplayers = reader.uint(4)
|
||||
state.maxplayers = reader.uint(4)
|
||||
this.logger.debug(log => {
|
||||
log('UNREAL2 EXTRA INFO', reader.buffer.slice(reader.i))
|
||||
})
|
||||
extraInfoReader = reader
|
||||
}
|
||||
|
||||
{
|
||||
const b = await this.sendPacket(1, true)
|
||||
const reader = this.reader(b)
|
||||
state.raw.mutators = []
|
||||
state.raw.rules = {}
|
||||
while (!reader.done()) {
|
||||
const key = this.readUnrealString(reader, true)
|
||||
const value = this.readUnrealString(reader, true)
|
||||
this.logger.debug(key + '=' + value)
|
||||
if (key === 'Mutator' || key === 'mutator') {
|
||||
state.raw.mutators.push(value)
|
||||
} else if (key || value) {
|
||||
if (Object.prototype.hasOwnProperty.call(state.raw.rules, key)) {
|
||||
state.raw.rules[key] += ',' + value
|
||||
} else {
|
||||
state.raw.rules[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
if ('GamePassword' in state.raw.rules) { state.password = state.raw.rules.GamePassword !== 'True' }
|
||||
}
|
||||
|
||||
if (state.raw.mutators.includes('KillingFloorMut') ||
|
||||
state.raw.rules['Num trader weapons'] ||
|
||||
state.raw.rules['Server Version'] === '1065'
|
||||
) {
|
||||
// Killing Floor
|
||||
state.raw.wavecurrent = extraInfoReader.uint(4)
|
||||
state.raw.wavetotal = extraInfoReader.uint(4)
|
||||
state.raw.ping = extraInfoReader.uint(4)
|
||||
state.raw.flags = extraInfoReader.uint(4)
|
||||
state.raw.skillLevel = this.readUnrealString(extraInfoReader, true)
|
||||
} else {
|
||||
state.raw.ping = extraInfoReader.uint(4)
|
||||
// These fields were added in later revisions of unreal engine
|
||||
if (extraInfoReader.remaining() >= 8) {
|
||||
state.raw.flags = extraInfoReader.uint(4)
|
||||
state.raw.skill = this.readUnrealString(extraInfoReader, true)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const b = await this.sendPacket(2, false)
|
||||
const reader = this.reader(b)
|
||||
|
||||
state.raw.scoreboard = {}
|
||||
while (!reader.done()) {
|
||||
const player = {}
|
||||
player.id = reader.uint(4)
|
||||
player.name = this.readUnrealString(reader, true)
|
||||
player.ping = reader.uint(4)
|
||||
player.score = reader.int(4)
|
||||
player.statsId = reader.uint(4)
|
||||
this.logger.debug(player)
|
||||
|
||||
if (!player.id) {
|
||||
state.raw.scoreboard[player.name] = player.score
|
||||
} else if (!player.ping) {
|
||||
state.bots.push(player)
|
||||
} else {
|
||||
state.players.push(player)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readUnrealString (reader, stripColor) {
|
||||
let length = reader.uint(1); let ucs2 = false
|
||||
if (length >= 0x80) {
|
||||
// This is flagged as a UCS-2 String
|
||||
length = (length & 0x7f) * 2
|
||||
ucs2 = true
|
||||
|
||||
// For UCS-2 strings, some unreal 2 games randomly insert an extra 0x01 here,
|
||||
// not included in the length. Skip it if present (hopefully this never happens legitimately)
|
||||
const peek = reader.uint(1)
|
||||
if (peek !== 1) reader.skip(-1)
|
||||
|
||||
this.logger.debug(log => {
|
||||
log('UCS2 STRING')
|
||||
log('UCS2 Length: ' + length)
|
||||
log(reader.buffer.slice(reader.i, reader.i + length))
|
||||
})
|
||||
}
|
||||
|
||||
let out = ''
|
||||
if (ucs2) {
|
||||
out = reader.string({ encoding: 'ucs2', length })
|
||||
this.logger.debug('UCS2 String decoded: ' + out)
|
||||
} else if (length > 0) {
|
||||
out = reader.string()
|
||||
}
|
||||
|
||||
// Sometimes the string has a null at the end (included with the length)
|
||||
// Strip it if present
|
||||
if (out.charCodeAt(out.length - 1) === 0) {
|
||||
out = out.substring(0, out.length - 1)
|
||||
}
|
||||
|
||||
if (stripColor) {
|
||||
out = out.replace(/\x1b...|[\x00-\x1a]/gus, '')
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
async sendPacket (type, required) {
|
||||
const outbuffer = Buffer.from([0x79, 0, 0, 0, type])
|
||||
|
||||
const packets = []
|
||||
return await this.udpSend(outbuffer, (buffer) => {
|
||||
const reader = this.reader(buffer)
|
||||
reader.uint(4) // header
|
||||
const iType = reader.uint(1)
|
||||
if (iType !== type) return
|
||||
packets.push(reader.rest())
|
||||
}, () => {
|
||||
if (!packets.length && required) return
|
||||
return Buffer.concat(packets)
|
||||
})
|
||||
}
|
||||
}
|
||||
import Core from './core.js'
|
||||
|
||||
export default class unreal2 extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.encoding = 'latin1'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
let extraInfoReader
|
||||
{
|
||||
const b = await this.sendPacket(0, true)
|
||||
const reader = this.reader(b)
|
||||
state.raw.serverid = reader.uint(4)
|
||||
state.raw.ip = this.readUnrealString(reader)
|
||||
state.gamePort = reader.uint(4)
|
||||
state.raw.queryport = reader.uint(4)
|
||||
state.name = this.readUnrealString(reader, true)
|
||||
state.map = this.readUnrealString(reader, true)
|
||||
state.raw.gametype = this.readUnrealString(reader, true)
|
||||
state.numplayers = reader.uint(4)
|
||||
state.maxplayers = reader.uint(4)
|
||||
this.logger.debug(log => {
|
||||
log('UNREAL2 EXTRA INFO', reader.buffer.slice(reader.i))
|
||||
})
|
||||
extraInfoReader = reader
|
||||
}
|
||||
|
||||
{
|
||||
const b = await this.sendPacket(1, true)
|
||||
const reader = this.reader(b)
|
||||
state.raw.mutators = []
|
||||
state.raw.rules = {}
|
||||
while (!reader.done()) {
|
||||
const key = this.readUnrealString(reader, true)
|
||||
const value = this.readUnrealString(reader, true)
|
||||
this.logger.debug(key + '=' + value)
|
||||
if (key === 'Mutator' || key === 'mutator') {
|
||||
state.raw.mutators.push(value)
|
||||
} else if (key || value) {
|
||||
if (Object.prototype.hasOwnProperty.call(state.raw.rules, key)) {
|
||||
state.raw.rules[key] += ',' + value
|
||||
} else {
|
||||
state.raw.rules[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
if ('GamePassword' in state.raw.rules) { state.password = state.raw.rules.GamePassword !== 'True' }
|
||||
}
|
||||
|
||||
if (state.raw.mutators.includes('KillingFloorMut') ||
|
||||
state.raw.rules['Num trader weapons'] ||
|
||||
state.raw.rules['Server Version'] === '1065'
|
||||
) {
|
||||
// Killing Floor
|
||||
state.raw.wavecurrent = extraInfoReader.uint(4)
|
||||
state.raw.wavetotal = extraInfoReader.uint(4)
|
||||
state.raw.ping = extraInfoReader.uint(4)
|
||||
state.raw.flags = extraInfoReader.uint(4)
|
||||
state.raw.skillLevel = this.readUnrealString(extraInfoReader, true)
|
||||
} else {
|
||||
state.raw.ping = extraInfoReader.uint(4)
|
||||
// These fields were added in later revisions of unreal engine
|
||||
if (extraInfoReader.remaining() >= 8) {
|
||||
state.raw.flags = extraInfoReader.uint(4)
|
||||
state.raw.skill = this.readUnrealString(extraInfoReader, true)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const b = await this.sendPacket(2, false)
|
||||
const reader = this.reader(b)
|
||||
|
||||
state.raw.scoreboard = {}
|
||||
while (!reader.done()) {
|
||||
const player = {}
|
||||
player.id = reader.uint(4)
|
||||
player.name = this.readUnrealString(reader, true)
|
||||
player.ping = reader.uint(4)
|
||||
player.score = reader.int(4)
|
||||
player.statsId = reader.uint(4)
|
||||
this.logger.debug(player)
|
||||
|
||||
if (!player.id) {
|
||||
state.raw.scoreboard[player.name] = player.score
|
||||
} else if (!player.ping) {
|
||||
state.bots.push(player)
|
||||
} else {
|
||||
state.players.push(player)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readUnrealString (reader, stripColor) {
|
||||
let length = reader.uint(1); let ucs2 = false
|
||||
if (length >= 0x80) {
|
||||
// This is flagged as a UCS-2 String
|
||||
length = (length & 0x7f) * 2
|
||||
ucs2 = true
|
||||
|
||||
// For UCS-2 strings, some unreal 2 games randomly insert an extra 0x01 here,
|
||||
// not included in the length. Skip it if present (hopefully this never happens legitimately)
|
||||
const peek = reader.uint(1)
|
||||
if (peek !== 1) reader.skip(-1)
|
||||
|
||||
this.logger.debug(log => {
|
||||
log('UCS2 STRING')
|
||||
log('UCS2 Length: ' + length)
|
||||
log(reader.buffer.slice(reader.i, reader.i + length))
|
||||
})
|
||||
}
|
||||
|
||||
let out = ''
|
||||
if (ucs2) {
|
||||
out = reader.string({ encoding: 'ucs2', length })
|
||||
this.logger.debug('UCS2 String decoded: ' + out)
|
||||
} else if (length > 0) {
|
||||
out = reader.string()
|
||||
}
|
||||
|
||||
// Sometimes the string has a null at the end (included with the length)
|
||||
// Strip it if present
|
||||
if (out.charCodeAt(out.length - 1) === 0) {
|
||||
out = out.substring(0, out.length - 1)
|
||||
}
|
||||
|
||||
if (stripColor) {
|
||||
out = out.replace(/\x1b...|[\x00-\x1a]/gus, '')
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
async sendPacket (type, required) {
|
||||
const outbuffer = Buffer.from([0x79, 0, 0, 0, type])
|
||||
|
||||
const packets = []
|
||||
return await this.udpSend(outbuffer, (buffer) => {
|
||||
const reader = this.reader(buffer)
|
||||
reader.uint(4) // header
|
||||
const iType = reader.uint(1)
|
||||
if (iType !== type) return
|
||||
packets.push(reader.rest())
|
||||
}, () => {
|
||||
if (!packets.length && required) return
|
||||
return Buffer.concat(packets)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,45 +1,45 @@
|
|||
import gamespy3 from './gamespy3.js'
|
||||
|
||||
export default class ut3 extends gamespy3 {
|
||||
async run (state) {
|
||||
await super.run(state)
|
||||
|
||||
this.translate(state.raw, {
|
||||
mapname: false,
|
||||
p1073741825: 'map',
|
||||
p1073741826: 'gametype',
|
||||
p1073741827: 'servername',
|
||||
p1073741828: 'custom_mutators',
|
||||
gamemode: 'joininprogress',
|
||||
s32779: 'gamemode',
|
||||
s0: 'bot_skill',
|
||||
s6: 'pure_server',
|
||||
s7: 'password',
|
||||
s8: 'vs_bots',
|
||||
s10: 'force_respawn',
|
||||
p268435704: 'frag_limit',
|
||||
p268435705: 'time_limit',
|
||||
p268435703: 'numbots',
|
||||
p268435717: 'stock_mutators',
|
||||
p1073741829: 'stock_mutators',
|
||||
s1: false,
|
||||
s9: false,
|
||||
s11: false,
|
||||
s12: false,
|
||||
s13: false,
|
||||
s14: false,
|
||||
p268435706: false,
|
||||
p268435968: false,
|
||||
p268435969: false
|
||||
})
|
||||
|
||||
const split = (a) => {
|
||||
let s = a.split('\x1c')
|
||||
s = s.filter((e) => { return e })
|
||||
return s
|
||||
}
|
||||
if ('custom_mutators' in state.raw) state.raw.custom_mutators = split(state.raw.custom_mutators)
|
||||
if ('stock_mutators' in state.raw) state.raw.stock_mutators = split(state.raw.stock_mutators)
|
||||
if ('map' in state.raw) state.map = state.raw.map
|
||||
}
|
||||
}
|
||||
import gamespy3 from './gamespy3.js'
|
||||
|
||||
export default class ut3 extends gamespy3 {
|
||||
async run (state) {
|
||||
await super.run(state)
|
||||
|
||||
this.translate(state.raw, {
|
||||
mapname: false,
|
||||
p1073741825: 'map',
|
||||
p1073741826: 'gametype',
|
||||
p1073741827: 'servername',
|
||||
p1073741828: 'custom_mutators',
|
||||
gamemode: 'joininprogress',
|
||||
s32779: 'gamemode',
|
||||
s0: 'bot_skill',
|
||||
s6: 'pure_server',
|
||||
s7: 'password',
|
||||
s8: 'vs_bots',
|
||||
s10: 'force_respawn',
|
||||
p268435704: 'frag_limit',
|
||||
p268435705: 'time_limit',
|
||||
p268435703: 'numbots',
|
||||
p268435717: 'stock_mutators',
|
||||
p1073741829: 'stock_mutators',
|
||||
s1: false,
|
||||
s9: false,
|
||||
s11: false,
|
||||
s12: false,
|
||||
s13: false,
|
||||
s14: false,
|
||||
p268435706: false,
|
||||
p268435968: false,
|
||||
p268435969: false
|
||||
})
|
||||
|
||||
const split = (a) => {
|
||||
let s = a.split('\x1c')
|
||||
s = s.filter((e) => { return e })
|
||||
return s
|
||||
}
|
||||
if ('custom_mutators' in state.raw) state.raw.custom_mutators = split(state.raw.custom_mutators)
|
||||
if ('stock_mutators' in state.raw) state.raw.stock_mutators = split(state.raw.stock_mutators)
|
||||
if ('map' in state.raw) state.map = state.raw.map
|
||||
}
|
||||
}
|
||||
|
|
1220
protocols/valve.js
1220
protocols/valve.js
File diff suppressed because it is too large
Load diff
|
@ -1,10 +1,10 @@
|
|||
import samp from './samp.js'
|
||||
|
||||
export default class vcmp extends samp {
|
||||
constructor () {
|
||||
super()
|
||||
this.magicHeader = 'VCMP'
|
||||
this.responseMagicHeader = 'MP04'
|
||||
this.isVcmp = true
|
||||
}
|
||||
}
|
||||
import samp from './samp.js'
|
||||
|
||||
export default class vcmp extends samp {
|
||||
constructor () {
|
||||
super()
|
||||
this.magicHeader = 'VCMP'
|
||||
this.responseMagicHeader = 'MP04'
|
||||
this.isVcmp = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,237 +1,237 @@
|
|||
import Core from './core.js'
|
||||
|
||||
export default class ventrilo extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.byteorder = 'be'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
const data = await this.sendCommand(2, '')
|
||||
state.raw = splitFields(data.toString())
|
||||
for (const client of state.raw.CLIENTS) {
|
||||
client.name = client.NAME
|
||||
delete client.NAME
|
||||
client.ping = parseInt(client.PING)
|
||||
delete client.PING
|
||||
state.players.push(client)
|
||||
}
|
||||
delete state.raw.CLIENTS
|
||||
state.numplayers = state.players.length
|
||||
|
||||
if ('NAME' in state.raw) state.name = state.raw.NAME
|
||||
if ('MAXCLIENTS' in state.raw) state.maxplayers = state.raw.MAXCLIENTS
|
||||
if (this.trueTest(state.raw.AUTH)) state.password = true
|
||||
}
|
||||
|
||||
async sendCommand (cmd, password) {
|
||||
const body = Buffer.alloc(16)
|
||||
body.write(password, 0, 15, 'utf8')
|
||||
const encrypted = encrypt(cmd, body)
|
||||
|
||||
const packets = {}
|
||||
return await this.udpSend(encrypted, (buffer) => {
|
||||
if (buffer.length < 20) return
|
||||
const data = decrypt(buffer)
|
||||
|
||||
if (data.zero !== 0) return
|
||||
packets[data.packetNum] = data.body
|
||||
if (Object.keys(packets).length !== data.packetTotal) return
|
||||
|
||||
const out = []
|
||||
for (let i = 0; i < data.packetTotal; i++) {
|
||||
if (!(i in packets)) throw new Error('Missing packet #' + i)
|
||||
out.push(packets[i])
|
||||
}
|
||||
return Buffer.concat(out)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function splitFields (str, subMode) {
|
||||
let splitter, delim
|
||||
if (subMode) {
|
||||
splitter = '='
|
||||
delim = ','
|
||||
} else {
|
||||
splitter = ': '
|
||||
delim = '\n'
|
||||
}
|
||||
|
||||
const split = str.split(delim)
|
||||
const out = {}
|
||||
if (!subMode) {
|
||||
out.CHANNELS = []
|
||||
out.CLIENTS = []
|
||||
}
|
||||
for (const one of split) {
|
||||
const equal = one.indexOf(splitter)
|
||||
const key = equal === -1 ? one : one.substring(0, equal)
|
||||
if (!key || key === '\0') continue
|
||||
const value = equal === -1 ? '' : one.substring(equal + splitter.length)
|
||||
if (!subMode && key === 'CHANNEL') out.CHANNELS.push(splitFields(value, true))
|
||||
else if (!subMode && key === 'CLIENT') out.CLIENTS.push(splitFields(value, true))
|
||||
else out[key] = value
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function randInt (min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1) + min)
|
||||
}
|
||||
|
||||
function crc (body) {
|
||||
let crc = 0
|
||||
for (let i = 0; i < body.length; i++) {
|
||||
crc = crcTable[crc >> 8] ^ body.readUInt8(i) ^ (crc << 8)
|
||||
crc &= 0xffff
|
||||
}
|
||||
return crc
|
||||
}
|
||||
|
||||
function encrypt (cmd, body) {
|
||||
const headerKeyStart = randInt(0, 0xff)
|
||||
const headerKeyAdd = randInt(1, 0xff)
|
||||
const bodyKeyStart = randInt(0, 0xff)
|
||||
const bodyKeyAdd = randInt(1, 0xff)
|
||||
|
||||
const header = Buffer.alloc(20)
|
||||
header.writeUInt8(headerKeyStart, 0)
|
||||
header.writeUInt8(headerKeyAdd, 1)
|
||||
header.writeUInt16BE(cmd, 4)
|
||||
header.writeUInt16BE(body.length, 8)
|
||||
header.writeUInt16BE(body.length, 10)
|
||||
header.writeUInt16BE(1, 12)
|
||||
header.writeUInt16BE(0, 14)
|
||||
header.writeUInt8(bodyKeyStart, 16)
|
||||
header.writeUInt8(bodyKeyAdd, 17)
|
||||
header.writeUInt16BE(crc(body), 18)
|
||||
|
||||
let offset = headerKeyStart
|
||||
for (let i = 2; i < header.length; i++) {
|
||||
let val = header.readUInt8(i)
|
||||
val += codeHead.charCodeAt(offset) + ((i - 2) % 5)
|
||||
val = val & 0xff
|
||||
header.writeUInt8(val, i)
|
||||
offset = (offset + headerKeyAdd) & 0xff
|
||||
}
|
||||
|
||||
offset = bodyKeyStart
|
||||
for (let i = 0; i < body.length; i++) {
|
||||
let val = body.readUInt8(i)
|
||||
val += codeBody.charCodeAt(offset) + (i % 72)
|
||||
val = val & 0xff
|
||||
body.writeUInt8(val, i)
|
||||
offset = (offset + bodyKeyAdd) & 0xff
|
||||
}
|
||||
|
||||
return Buffer.concat([header, body])
|
||||
}
|
||||
function decrypt (data) {
|
||||
const header = data.slice(0, 20)
|
||||
const body = data.slice(20)
|
||||
const headerKeyStart = header.readUInt8(0)
|
||||
const headerKeyAdd = header.readUInt8(1)
|
||||
|
||||
let offset = headerKeyStart
|
||||
for (let i = 2; i < header.length; i++) {
|
||||
let val = header.readUInt8(i)
|
||||
val -= codeHead.charCodeAt(offset) + ((i - 2) % 5)
|
||||
val = val & 0xff
|
||||
header.writeUInt8(val, i)
|
||||
offset = (offset + headerKeyAdd) & 0xff
|
||||
}
|
||||
|
||||
const bodyKeyStart = header.readUInt8(16)
|
||||
const bodyKeyAdd = header.readUInt8(17)
|
||||
offset = bodyKeyStart
|
||||
for (let i = 0; i < body.length; i++) {
|
||||
let val = body.readUInt8(i)
|
||||
val -= codeBody.charCodeAt(offset) + (i % 72)
|
||||
val = val & 0xff
|
||||
body.writeUInt8(val, i)
|
||||
offset = (offset + bodyKeyAdd) & 0xff
|
||||
}
|
||||
|
||||
// header format:
|
||||
// key, zero, cmd, echo, totallength, thislength
|
||||
// totalpacket, packetnum, body key, crc
|
||||
return {
|
||||
zero: header.readUInt16BE(2),
|
||||
cmd: header.readUInt16BE(4),
|
||||
packetTotal: header.readUInt16BE(12),
|
||||
packetNum: header.readUInt16BE(14),
|
||||
body
|
||||
}
|
||||
}
|
||||
|
||||
const codeHead =
|
||||
'\x80\xe5\x0e\x38\xba\x63\x4c\x99\x88\x63\x4c\xd6\x54\xb8\x65\x7e' +
|
||||
'\xbf\x8a\xf0\x17\x8a\xaa\x4d\x0f\xb7\x23\x27\xf6\xeb\x12\xf8\xea' +
|
||||
'\x17\xb7\xcf\x52\x57\xcb\x51\xcf\x1b\x14\xfd\x6f\x84\x38\xb5\x24' +
|
||||
'\x11\xcf\x7a\x75\x7a\xbb\x78\x74\xdc\xbc\x42\xf0\x17\x3f\x5e\xeb' +
|
||||
'\x74\x77\x04\x4e\x8c\xaf\x23\xdc\x65\xdf\xa5\x65\xdd\x7d\xf4\x3c' +
|
||||
'\x4c\x95\xbd\xeb\x65\x1c\xf4\x24\x5d\x82\x18\xfb\x50\x86\xb8\x53' +
|
||||
'\xe0\x4e\x36\x96\x1f\xb7\xcb\xaa\xaf\xea\xcb\x20\x27\x30\x2a\xae' +
|
||||
'\xb9\x07\x40\xdf\x12\x75\xc9\x09\x82\x9c\x30\x80\x5d\x8f\x0d\x09' +
|
||||
'\xa1\x64\xec\x91\xd8\x8a\x50\x1f\x40\x5d\xf7\x08\x2a\xf8\x60\x62' +
|
||||
'\xa0\x4a\x8b\xba\x4a\x6d\x00\x0a\x93\x32\x12\xe5\x07\x01\x65\xf5' +
|
||||
'\xff\xe0\xae\xa7\x81\xd1\xba\x25\x62\x61\xb2\x85\xad\x7e\x9d\x3f' +
|
||||
'\x49\x89\x26\xe5\xd5\xac\x9f\x0e\xd7\x6e\x47\x94\x16\x84\xc8\xff' +
|
||||
'\x44\xea\x04\x40\xe0\x33\x11\xa3\x5b\x1e\x82\xff\x7a\x69\xe9\x2f' +
|
||||
'\xfb\xea\x9a\xc6\x7b\xdb\xb1\xff\x97\x76\x56\xf3\x52\xc2\x3f\x0f' +
|
||||
'\xb6\xac\x77\xc4\xbf\x59\x5e\x80\x74\xbb\xf2\xde\x57\x62\x4c\x1a' +
|
||||
'\xff\x95\x6d\xc7\x04\xa2\x3b\xc4\x1b\x72\xc7\x6c\x82\x60\xd1\x0d'
|
||||
|
||||
const codeBody =
|
||||
'\x82\x8b\x7f\x68\x90\xe0\x44\x09\x19\x3b\x8e\x5f\xc2\x82\x38\x23' +
|
||||
'\x6d\xdb\x62\x49\x52\x6e\x21\xdf\x51\x6c\x76\x37\x86\x50\x7d\x48' +
|
||||
'\x1f\x65\xe7\x52\x6a\x88\xaa\xc1\x32\x2f\xf7\x54\x4c\xaa\x6d\x7e' +
|
||||
'\x6d\xa9\x8c\x0d\x3f\xff\x6c\x09\xb3\xa5\xaf\xdf\x98\x02\xb4\xbe' +
|
||||
'\x6d\x69\x0d\x42\x73\xe4\x34\x50\x07\x30\x79\x41\x2f\x08\x3f\x42' +
|
||||
'\x73\xa7\x68\xfa\xee\x88\x0e\x6e\xa4\x70\x74\x22\x16\xae\x3c\x81' +
|
||||
'\x14\xa1\xda\x7f\xd3\x7c\x48\x7d\x3f\x46\xfb\x6d\x92\x25\x17\x36' +
|
||||
'\x26\xdb\xdf\x5a\x87\x91\x6f\xd6\xcd\xd4\xad\x4a\x29\xdd\x7d\x59' +
|
||||
'\xbd\x15\x34\x53\xb1\xd8\x50\x11\x83\x79\x66\x21\x9e\x87\x5b\x24' +
|
||||
'\x2f\x4f\xd7\x73\x34\xa2\xf7\x09\xd5\xd9\x42\x9d\xf8\x15\xdf\x0e' +
|
||||
'\x10\xcc\x05\x04\x35\x81\xb2\xd5\x7a\xd2\xa0\xa5\x7b\xb8\x75\xd2' +
|
||||
'\x35\x0b\x39\x8f\x1b\x44\x0e\xce\x66\x87\x1b\x64\xac\xe1\xca\x67' +
|
||||
'\xb4\xce\x33\xdb\x89\xfe\xd8\x8e\xcd\x58\x92\x41\x50\x40\xcb\x08' +
|
||||
'\xe1\x15\xee\xf4\x64\xfe\x1c\xee\x25\xe7\x21\xe6\x6c\xc6\xa6\x2e' +
|
||||
'\x52\x23\xa7\x20\xd2\xd7\x28\x07\x23\x14\x24\x3d\x45\xa5\xc7\x90' +
|
||||
'\xdb\x77\xdd\xea\x38\x59\x89\x32\xbc\x00\x3a\x6d\x61\x4e\xdb\x29'
|
||||
|
||||
const crcTable = [
|
||||
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
|
||||
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
|
||||
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
|
||||
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
|
||||
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
|
||||
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
|
||||
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
|
||||
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
|
||||
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
|
||||
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
|
||||
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
|
||||
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
|
||||
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
|
||||
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
|
||||
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
|
||||
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
|
||||
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
|
||||
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
|
||||
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
|
||||
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
|
||||
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
|
||||
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
|
||||
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
|
||||
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
|
||||
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
|
||||
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
|
||||
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
|
||||
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
|
||||
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
|
||||
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
|
||||
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
|
||||
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
|
||||
]
|
||||
import Core from './core.js'
|
||||
|
||||
export default class ventrilo extends Core {
|
||||
constructor () {
|
||||
super()
|
||||
this.byteorder = 'be'
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
const data = await this.sendCommand(2, '')
|
||||
state.raw = splitFields(data.toString())
|
||||
for (const client of state.raw.CLIENTS) {
|
||||
client.name = client.NAME
|
||||
delete client.NAME
|
||||
client.ping = parseInt(client.PING)
|
||||
delete client.PING
|
||||
state.players.push(client)
|
||||
}
|
||||
delete state.raw.CLIENTS
|
||||
state.numplayers = state.players.length
|
||||
|
||||
if ('NAME' in state.raw) state.name = state.raw.NAME
|
||||
if ('MAXCLIENTS' in state.raw) state.maxplayers = state.raw.MAXCLIENTS
|
||||
if (this.trueTest(state.raw.AUTH)) state.password = true
|
||||
}
|
||||
|
||||
async sendCommand (cmd, password) {
|
||||
const body = Buffer.alloc(16)
|
||||
body.write(password, 0, 15, 'utf8')
|
||||
const encrypted = encrypt(cmd, body)
|
||||
|
||||
const packets = {}
|
||||
return await this.udpSend(encrypted, (buffer) => {
|
||||
if (buffer.length < 20) return
|
||||
const data = decrypt(buffer)
|
||||
|
||||
if (data.zero !== 0) return
|
||||
packets[data.packetNum] = data.body
|
||||
if (Object.keys(packets).length !== data.packetTotal) return
|
||||
|
||||
const out = []
|
||||
for (let i = 0; i < data.packetTotal; i++) {
|
||||
if (!(i in packets)) throw new Error('Missing packet #' + i)
|
||||
out.push(packets[i])
|
||||
}
|
||||
return Buffer.concat(out)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function splitFields (str, subMode) {
|
||||
let splitter, delim
|
||||
if (subMode) {
|
||||
splitter = '='
|
||||
delim = ','
|
||||
} else {
|
||||
splitter = ': '
|
||||
delim = '\n'
|
||||
}
|
||||
|
||||
const split = str.split(delim)
|
||||
const out = {}
|
||||
if (!subMode) {
|
||||
out.CHANNELS = []
|
||||
out.CLIENTS = []
|
||||
}
|
||||
for (const one of split) {
|
||||
const equal = one.indexOf(splitter)
|
||||
const key = equal === -1 ? one : one.substring(0, equal)
|
||||
if (!key || key === '\0') continue
|
||||
const value = equal === -1 ? '' : one.substring(equal + splitter.length)
|
||||
if (!subMode && key === 'CHANNEL') out.CHANNELS.push(splitFields(value, true))
|
||||
else if (!subMode && key === 'CLIENT') out.CLIENTS.push(splitFields(value, true))
|
||||
else out[key] = value
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function randInt (min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1) + min)
|
||||
}
|
||||
|
||||
function crc (body) {
|
||||
let crc = 0
|
||||
for (let i = 0; i < body.length; i++) {
|
||||
crc = crcTable[crc >> 8] ^ body.readUInt8(i) ^ (crc << 8)
|
||||
crc &= 0xffff
|
||||
}
|
||||
return crc
|
||||
}
|
||||
|
||||
function encrypt (cmd, body) {
|
||||
const headerKeyStart = randInt(0, 0xff)
|
||||
const headerKeyAdd = randInt(1, 0xff)
|
||||
const bodyKeyStart = randInt(0, 0xff)
|
||||
const bodyKeyAdd = randInt(1, 0xff)
|
||||
|
||||
const header = Buffer.alloc(20)
|
||||
header.writeUInt8(headerKeyStart, 0)
|
||||
header.writeUInt8(headerKeyAdd, 1)
|
||||
header.writeUInt16BE(cmd, 4)
|
||||
header.writeUInt16BE(body.length, 8)
|
||||
header.writeUInt16BE(body.length, 10)
|
||||
header.writeUInt16BE(1, 12)
|
||||
header.writeUInt16BE(0, 14)
|
||||
header.writeUInt8(bodyKeyStart, 16)
|
||||
header.writeUInt8(bodyKeyAdd, 17)
|
||||
header.writeUInt16BE(crc(body), 18)
|
||||
|
||||
let offset = headerKeyStart
|
||||
for (let i = 2; i < header.length; i++) {
|
||||
let val = header.readUInt8(i)
|
||||
val += codeHead.charCodeAt(offset) + ((i - 2) % 5)
|
||||
val = val & 0xff
|
||||
header.writeUInt8(val, i)
|
||||
offset = (offset + headerKeyAdd) & 0xff
|
||||
}
|
||||
|
||||
offset = bodyKeyStart
|
||||
for (let i = 0; i < body.length; i++) {
|
||||
let val = body.readUInt8(i)
|
||||
val += codeBody.charCodeAt(offset) + (i % 72)
|
||||
val = val & 0xff
|
||||
body.writeUInt8(val, i)
|
||||
offset = (offset + bodyKeyAdd) & 0xff
|
||||
}
|
||||
|
||||
return Buffer.concat([header, body])
|
||||
}
|
||||
function decrypt (data) {
|
||||
const header = data.slice(0, 20)
|
||||
const body = data.slice(20)
|
||||
const headerKeyStart = header.readUInt8(0)
|
||||
const headerKeyAdd = header.readUInt8(1)
|
||||
|
||||
let offset = headerKeyStart
|
||||
for (let i = 2; i < header.length; i++) {
|
||||
let val = header.readUInt8(i)
|
||||
val -= codeHead.charCodeAt(offset) + ((i - 2) % 5)
|
||||
val = val & 0xff
|
||||
header.writeUInt8(val, i)
|
||||
offset = (offset + headerKeyAdd) & 0xff
|
||||
}
|
||||
|
||||
const bodyKeyStart = header.readUInt8(16)
|
||||
const bodyKeyAdd = header.readUInt8(17)
|
||||
offset = bodyKeyStart
|
||||
for (let i = 0; i < body.length; i++) {
|
||||
let val = body.readUInt8(i)
|
||||
val -= codeBody.charCodeAt(offset) + (i % 72)
|
||||
val = val & 0xff
|
||||
body.writeUInt8(val, i)
|
||||
offset = (offset + bodyKeyAdd) & 0xff
|
||||
}
|
||||
|
||||
// header format:
|
||||
// key, zero, cmd, echo, totallength, thislength
|
||||
// totalpacket, packetnum, body key, crc
|
||||
return {
|
||||
zero: header.readUInt16BE(2),
|
||||
cmd: header.readUInt16BE(4),
|
||||
packetTotal: header.readUInt16BE(12),
|
||||
packetNum: header.readUInt16BE(14),
|
||||
body
|
||||
}
|
||||
}
|
||||
|
||||
const codeHead =
|
||||
'\x80\xe5\x0e\x38\xba\x63\x4c\x99\x88\x63\x4c\xd6\x54\xb8\x65\x7e' +
|
||||
'\xbf\x8a\xf0\x17\x8a\xaa\x4d\x0f\xb7\x23\x27\xf6\xeb\x12\xf8\xea' +
|
||||
'\x17\xb7\xcf\x52\x57\xcb\x51\xcf\x1b\x14\xfd\x6f\x84\x38\xb5\x24' +
|
||||
'\x11\xcf\x7a\x75\x7a\xbb\x78\x74\xdc\xbc\x42\xf0\x17\x3f\x5e\xeb' +
|
||||
'\x74\x77\x04\x4e\x8c\xaf\x23\xdc\x65\xdf\xa5\x65\xdd\x7d\xf4\x3c' +
|
||||
'\x4c\x95\xbd\xeb\x65\x1c\xf4\x24\x5d\x82\x18\xfb\x50\x86\xb8\x53' +
|
||||
'\xe0\x4e\x36\x96\x1f\xb7\xcb\xaa\xaf\xea\xcb\x20\x27\x30\x2a\xae' +
|
||||
'\xb9\x07\x40\xdf\x12\x75\xc9\x09\x82\x9c\x30\x80\x5d\x8f\x0d\x09' +
|
||||
'\xa1\x64\xec\x91\xd8\x8a\x50\x1f\x40\x5d\xf7\x08\x2a\xf8\x60\x62' +
|
||||
'\xa0\x4a\x8b\xba\x4a\x6d\x00\x0a\x93\x32\x12\xe5\x07\x01\x65\xf5' +
|
||||
'\xff\xe0\xae\xa7\x81\xd1\xba\x25\x62\x61\xb2\x85\xad\x7e\x9d\x3f' +
|
||||
'\x49\x89\x26\xe5\xd5\xac\x9f\x0e\xd7\x6e\x47\x94\x16\x84\xc8\xff' +
|
||||
'\x44\xea\x04\x40\xe0\x33\x11\xa3\x5b\x1e\x82\xff\x7a\x69\xe9\x2f' +
|
||||
'\xfb\xea\x9a\xc6\x7b\xdb\xb1\xff\x97\x76\x56\xf3\x52\xc2\x3f\x0f' +
|
||||
'\xb6\xac\x77\xc4\xbf\x59\x5e\x80\x74\xbb\xf2\xde\x57\x62\x4c\x1a' +
|
||||
'\xff\x95\x6d\xc7\x04\xa2\x3b\xc4\x1b\x72\xc7\x6c\x82\x60\xd1\x0d'
|
||||
|
||||
const codeBody =
|
||||
'\x82\x8b\x7f\x68\x90\xe0\x44\x09\x19\x3b\x8e\x5f\xc2\x82\x38\x23' +
|
||||
'\x6d\xdb\x62\x49\x52\x6e\x21\xdf\x51\x6c\x76\x37\x86\x50\x7d\x48' +
|
||||
'\x1f\x65\xe7\x52\x6a\x88\xaa\xc1\x32\x2f\xf7\x54\x4c\xaa\x6d\x7e' +
|
||||
'\x6d\xa9\x8c\x0d\x3f\xff\x6c\x09\xb3\xa5\xaf\xdf\x98\x02\xb4\xbe' +
|
||||
'\x6d\x69\x0d\x42\x73\xe4\x34\x50\x07\x30\x79\x41\x2f\x08\x3f\x42' +
|
||||
'\x73\xa7\x68\xfa\xee\x88\x0e\x6e\xa4\x70\x74\x22\x16\xae\x3c\x81' +
|
||||
'\x14\xa1\xda\x7f\xd3\x7c\x48\x7d\x3f\x46\xfb\x6d\x92\x25\x17\x36' +
|
||||
'\x26\xdb\xdf\x5a\x87\x91\x6f\xd6\xcd\xd4\xad\x4a\x29\xdd\x7d\x59' +
|
||||
'\xbd\x15\x34\x53\xb1\xd8\x50\x11\x83\x79\x66\x21\x9e\x87\x5b\x24' +
|
||||
'\x2f\x4f\xd7\x73\x34\xa2\xf7\x09\xd5\xd9\x42\x9d\xf8\x15\xdf\x0e' +
|
||||
'\x10\xcc\x05\x04\x35\x81\xb2\xd5\x7a\xd2\xa0\xa5\x7b\xb8\x75\xd2' +
|
||||
'\x35\x0b\x39\x8f\x1b\x44\x0e\xce\x66\x87\x1b\x64\xac\xe1\xca\x67' +
|
||||
'\xb4\xce\x33\xdb\x89\xfe\xd8\x8e\xcd\x58\x92\x41\x50\x40\xcb\x08' +
|
||||
'\xe1\x15\xee\xf4\x64\xfe\x1c\xee\x25\xe7\x21\xe6\x6c\xc6\xa6\x2e' +
|
||||
'\x52\x23\xa7\x20\xd2\xd7\x28\x07\x23\x14\x24\x3d\x45\xa5\xc7\x90' +
|
||||
'\xdb\x77\xdd\xea\x38\x59\x89\x32\xbc\x00\x3a\x6d\x61\x4e\xdb\x29'
|
||||
|
||||
const crcTable = [
|
||||
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
|
||||
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
|
||||
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
|
||||
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
|
||||
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
|
||||
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
|
||||
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
|
||||
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
|
||||
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
|
||||
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
|
||||
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
|
||||
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
|
||||
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
|
||||
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
|
||||
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
|
||||
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
|
||||
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
|
||||
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
|
||||
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
|
||||
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
|
||||
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
|
||||
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
|
||||
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
|
||||
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
|
||||
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
|
||||
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
|
||||
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
|
||||
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
|
||||
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
|
||||
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
|
||||
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
|
||||
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
|
||||
]
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import quake3 from './quake3.js'
|
||||
|
||||
export default class warsow extends quake3 {
|
||||
async run (state) {
|
||||
await super.run(state)
|
||||
if (state.players) {
|
||||
for (const player of state.players) {
|
||||
player.team = player.address
|
||||
delete player.address
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
import quake3 from './quake3.js'
|
||||
|
||||
export default class warsow extends quake3 {
|
||||
async run (state) {
|
||||
await super.run(state)
|
||||
if (state.players) {
|
||||
for (const player of state.players) {
|
||||
player.team = player.address
|
||||
delete player.address
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,36 +1,36 @@
|
|||
import Minimist from 'minimist'
|
||||
import GameDig from './../lib/index.js'
|
||||
|
||||
const argv = Minimist(process.argv.slice(2), {})
|
||||
|
||||
const options = {}
|
||||
if (argv._.length >= 1) {
|
||||
const target = argv._[0]
|
||||
const split = target.split(':')
|
||||
options.host = split[0]
|
||||
if (split.length >= 2) {
|
||||
options.port = split[1]
|
||||
}
|
||||
}
|
||||
|
||||
const gamedig = new GameDig(options)
|
||||
|
||||
const protocols = ['valve', 'gamespy1', 'gamespy2', 'gamespy3', 'goldsrc', 'minecraft', 'quake1', 'quake2', 'quake3', 'unreal2', 'valve']
|
||||
|
||||
const run = async () => {
|
||||
for (const protocol of protocols) {
|
||||
try {
|
||||
const response = await gamedig.query({
|
||||
...options,
|
||||
debug: true,
|
||||
type: `protocol-${protocol}`
|
||||
})
|
||||
console.log(response)
|
||||
process.exit()
|
||||
} catch (e) {
|
||||
console.log(`Error on '${protocol}': ${e}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run().then(() => {})
|
||||
import Minimist from 'minimist'
|
||||
import GameDig from './../lib/index.js'
|
||||
|
||||
const argv = Minimist(process.argv.slice(2), {})
|
||||
|
||||
const options = {}
|
||||
if (argv._.length >= 1) {
|
||||
const target = argv._[0]
|
||||
const split = target.split(':')
|
||||
options.host = split[0]
|
||||
if (split.length >= 2) {
|
||||
options.port = split[1]
|
||||
}
|
||||
}
|
||||
|
||||
const gamedig = new GameDig(options)
|
||||
|
||||
const protocols = ['valve', 'gamespy1', 'gamespy2', 'gamespy3', 'goldsrc', 'minecraft', 'quake1', 'quake2', 'quake3', 'unreal2', 'valve']
|
||||
|
||||
const run = async () => {
|
||||
for (const protocol of protocols) {
|
||||
try {
|
||||
const response = await gamedig.query({
|
||||
...options,
|
||||
debug: true,
|
||||
type: `protocol-${protocol}`
|
||||
})
|
||||
console.log(response)
|
||||
process.exit()
|
||||
} catch (e) {
|
||||
console.log(`Error on '${protocol}': ${e}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run().then(() => {})
|
||||
|
|
Loading…
Reference in a new issue