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:
CosminPerRam 2023-11-12 13:14:43 +02:00 committed by GitHub
parent a8bc7521f6
commit cee42e7a88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 5697 additions and 5697 deletions

2
.gitattributes vendored
View File

@ -1 +1 @@
text=auto * text=auto eol=lf

View File

@ -1,298 +1,298 @@
## To Be Released... ## To Be Released...
### Breaking Changes ### Breaking Changes
#### Package #### Package
* Node.js 16.20 is now required (from 14). * Node.js 16.20 is now required (from 14).
* Made the library a `module`. * Made the library a `module`.
#### Games #### 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)). * Renamed `Counter Strike: 2D` to `CS2D` in [games.txt](games.txt) (why? see [this](https://cs2d.com/faq.php?show=misc_name#misc_name)).
* Updated `CS2D` protocol (by @ernestpasnik). * Updated `CS2D` protocol (by @ernestpasnik).
### Other changes ### Other changes
#### Package #### Package
* Replaced usage of deprecated `substr` with `substring`. * Replaced usage of deprecated `substr` with `substring`.
* Replaced deprecated internal `punycode` with the [punycode](https://www.npmjs.com/package/punycode) package. * Replaced deprecated internal `punycode` with the [punycode](https://www.npmjs.com/package/punycode) package.
* Updated [got](https://github.com/sindresorhus/got) from 12.1 to 13. * 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 [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 [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 @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. * 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. * Added eslint which spotted some unused variables and other lints.
* CLI: Resolved incorrect error message when querying with a non-existent protocol name. * CLI: Resolved incorrect error message when querying with a non-existent protocol name.
* Added Deno support: the library and CLI can now be experimentally used with the [Deno runtime](https://deno.com) * 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` * `deno run --allow-net --allow-read=. bin/gamedig.js --type tf2 127.0.0.1`
* Added code examples. * Added code examples.
* Added Epic Online Services protocol. * Added Epic Online Services protocol.
#### Games #### Games
* Added support by @dgibbs64: Eco (2018), Core Keeper (2022), ARMA: Reforger (2022), * Added support by @dgibbs64: Eco (2018), Core Keeper (2022), ARMA: Reforger (2022),
Action Half-Life, Action: Source (2019), Base Defense (2017), Blade Symphony (2014), Action Half-Life, Action: Source (2019), Base Defense (2017), Blade Symphony (2014),
Brainbread, Deathmatch Classic (2001), Double Action: Boogaloo (2014), Dystopia (2005), Brainbread, Deathmatch Classic (2001), Double Action: Boogaloo (2014), Dystopia (2005),
Empires Mod (2008), Fistful of Frags (2014), alf-Life: Opposing Force (1999), 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), Pirates, Vikings, and Knights II (2007), Project Cars (2015), Project Cars 2 (2017),
The Specialists, Vampire Slayer, Warfork (2018), Wurm Unlimited (2015). The Specialists, Vampire Slayer, Warfork (2018), Wurm Unlimited (2015).
* Also added support: The Forest (2014), Operation: Harsh Doorstop (2023), * Also added support: The Forest (2014), Operation: Harsh Doorstop (2023),
Insurgency: Modern Infantry Combat (2007), Counter-Strike 2 (2023), The Front (2023), Insurgency: Modern Infantry Combat (2007), Counter-Strike 2 (2023), The Front (2023),
San Andreas OpenMP. San Andreas OpenMP.
* Capitalized 'Unturned' in game.txt * Capitalized 'Unturned' in game.txt
* Removed the players::setNum method, the library will no longer add empty players as * Removed the players::setNum method, the library will no longer add empty players as
a placeholder in the `players` field. a placeholder in the `players` field.
* Fixed wrong field being parsed for `maxplayers` on Doom3. * Fixed wrong field being parsed for `maxplayers` on Doom3.
* Stabilized field `numplayers`. * Stabilized field `numplayers`.
* Added support by @GuilhermeWerner: ARK: Survival Ascended (2023). * Added support by @GuilhermeWerner: ARK: Survival Ascended (2023).
### 4.1.0 ### 4.1.0
* Replace `compressjs` dependency by `seek-bzip` to solve some possible import issues. * Replace `compressjs` dependency by `seek-bzip` to solve some possible import issues.
* Sons Of The Forest (2023) - Added support * Sons Of The Forest (2023) - Added support
* Red Dead Redemption 2 - RedM (2018) - Added support * Red Dead Redemption 2 - RedM (2018) - Added support
* Creativerse (2017) - Added support * Creativerse (2017) - Added support
* The Isle (2015) - Added support * The Isle (2015) - Added support
### 4.0.7 ### 4.0.7
* Updated some dependencies to solve vulnerabilities * Updated some dependencies to solve vulnerabilities
* Fixed an issue regarding GameSpy 1 not correctly checking and parsing for numbers. * Fixed an issue regarding GameSpy 1 not correctly checking and parsing for numbers.
* Risk of Rain 2 (2019) - Added support * Risk of Rain 2 (2019) - Added support
* Survive the Nights (2017) - Added support * Survive the Nights (2017) - Added support
* V Rising (2022) - Added support * V Rising (2022) - Added support
* Day of Dragons (2019) - Added support * Day of Dragons (2019) - Added support
* Onset (2019) - Added support * Onset (2019) - Added support
* Don't Starve Together (2016) - Added support * Don't Starve Together (2016) - Added support
* Chivalry: Medieval Warfare (2012) - Added support * Chivalry: Medieval Warfare (2012) - Added support
* Avorion (2020) - Added support * Avorion (2020) - Added support
* Black Mesa (2020) - Added support * Black Mesa (2020) - Added support
* Ballistic Overkill (2017) - Added support * Ballistic Overkill (2017) - Added support
* Codename CURE (2017) - Added support * Codename CURE (2017) - Added support
* Colony Survival (2017) - Added support * Colony Survival (2017) - Added support
* Rising World (2014) - Added support * Rising World (2014) - Added support
* BrainBread 2 (2016) - Added support * BrainBread 2 (2016) - Added support
### 4.0.6 ### 4.0.6
* Fixed ping returned by minecraft queries * Fixed ping returned by minecraft queries
* Added ipFamily option to query only ipv4 or only ipv6 dns records * Added ipFamily option to query only ipv4 or only ipv6 dns records
### 4.0.5 ### 4.0.5
* Fixed filtering out fake "Max Players" player on CSGO * Fixed filtering out fake "Max Players" player on CSGO
* Removed moment dependency * Removed moment dependency
### 4.0.4 ### 4.0.4
* Updated dependencies * Updated dependencies
### 4.0.3 ### 4.0.3
* Fixed nodejs version requirement in package.json (node 14 has been required since gamedig 4) * Fixed nodejs version requirement in package.json (node 14 has been required since gamedig 4)
* Ground Breach (2018) - Added support * Ground Breach (2018) - Added support
* Minecraft (All Versions) - Fixed character encoding for strings returned by servers using Geyser * Minecraft (All Versions) - Fixed character encoding for strings returned by servers using Geyser
* Barotrauma (2019) - Added support * Barotrauma (2019) - Added support
### 4.0.2 ### 4.0.2
* Counter-Strike 1.5 - Fixed support * Counter-Strike 1.5 - Fixed support
### 4.0.1 ### 4.0.1
* Rust - Fixed maxplayers >255 * Rust - Fixed maxplayers >255
* dayZ - Fixed tag info not parsing when queryRules wasn't set * dayZ - Fixed tag info not parsing when queryRules wasn't set
### 4.0.0 ### 4.0.0
#### Breaking Changes #### Breaking Changes
* NodeJS 14 is now required * NodeJS 14 is now required
#### Other changes #### Other changes
* Dependencies are updated * Dependencies are updated
* Node 14 is now required due to new requirement in `got` dependency * Node 14 is now required due to new requirement in `got` dependency
### 3.0.9 ### 3.0.9
* Fixes player info parsing issues on bf1942-based mods (Thanks cetteup) * Fixes player info parsing issues on bf1942-based mods (Thanks cetteup)
* Adds Project Zomboid support (Thanks xhip) * Adds Project Zomboid support (Thanks xhip)
* Adds Post Scriptum support (Thanks arkuar) * Adds Post Scriptum support (Thanks arkuar)
* Adds some more DayZ info to state.raw (Thanks podrivo) * Adds some more DayZ info to state.raw (Thanks podrivo)
* Updates to README regarding DayZ (Thanks podrivo) * Updates to README regarding DayZ (Thanks podrivo)
* Improvements to DayZ mod parsing from additional more recent reverse engineering (probably still buggy) * Improvements to DayZ mod parsing from additional more recent reverse engineering (probably still buggy)
* Fixes ping always being 0 for minecraft servers * Fixes ping always being 0 for minecraft servers
* Adds README documentation about teamspeakQueryPort * Adds README documentation about teamspeakQueryPort
### 3.0.8 ### 3.0.8
* Fixes player array corruption on some protocols which only report player counts without names (Thanks to a-sync) * 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 * Fixes minecraft protocol not using player list from bedrock protocol in some cases
### 3.0.7 ### 3.0.7
* Fixes corrupted dayzMods when packet overflow is present * Fixes corrupted dayzMods when packet overflow is present
### 3.0.6 ### 3.0.6
* raw.tags for valve servers is now an array rather than a string * 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 * 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 * DayZ queue length, day and night acceleration are now parsed into raw as well
### 3.0.5 ### 3.0.5
* Add support for `listenUdpPort` to specify a fixed bind port. * Add support for `listenUdpPort` to specify a fixed bind port.
* Improved udp bind failure detection. * Improved udp bind failure detection.
### 3.0.4 ### 3.0.4
* Add support for Discord widget * Add support for Discord widget
### 3.0.3 ### 3.0.3
* Greatly improve gamespy1 protocol, with additional error handling and xserverquery support. * Greatly improve gamespy1 protocol, with additional error handling and xserverquery support.
### 3.0.2 ### 3.0.2
* Fix player name extraction for Unreal Tournament (1999) and possibly * Fix player name extraction for Unreal Tournament (1999) and possibly
other gamespy1 games. other gamespy1 games.
### 3.0.1 ### 3.0.1
* Clarified that nodejs 12 is now required for gamedig 3 * Clarified that nodejs 12 is now required for gamedig 3
* Fixed misc player fields not going into `raw` subobject in `assettocorsa`, `fivem`, and `gamespy2` * Fixed misc player fields not going into `raw` subobject in `assettocorsa`, `fivem`, and `gamespy2`
### 3.0.0 ### 3.0.0
Major Changes: Major Changes:
* **NodeJS 12 is now required** * **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. * 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 * 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. 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 * "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, 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. 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`. * The `raw.steamappid` and `raw.gameid` fields for valve games have been consolidated into `raw.appId`.
### 2.0.28 ### 2.0.28
* Added Valheim (2021) * Added Valheim (2021)
### 2.0.27 ### 2.0.27
* Reduced chance of protocol collisions between gamespy3 and minecraftbedrock * Reduced chance of protocol collisions between gamespy3 and minecraftbedrock
### 2.0.26 ### 2.0.26
* Added support for the native minecraft bedrock protocol, since some * Added support for the native minecraft bedrock protocol, since some
bedrock servers apparently do not respond to the gamespy3 protocol. bedrock servers apparently do not respond to the gamespy3 protocol.
### 2.0.25 ### 2.0.25
* Support challenges in A2S_INFO (upcoming change to valve protocol) * Support challenges in A2S_INFO (upcoming change to valve protocol)
### 2.0.24 ### 2.0.24
* Add Savage 2: A Tortured Soul (2008) * Add Savage 2: A Tortured Soul (2008)
### 2.0.23 ### 2.0.23
* Fix Conan Exiles and other games which don't respond to the valve player query * 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 * Add givenPortOnly query option for users that require extreme optimization
### 2.0.22 ### 2.0.22
* Updated dependencies * Updated dependencies
### 2.0.21 ### 2.0.21
* Added Assetto Corsa (2014) * Added Assetto Corsa (2014)
* Fixed password flag for Squad * Fixed password flag for Squad
* Added Mordhau (2019) * Added Mordhau (2019)
* Fixed player count being incorrect in minecraftvanilla protocol in some cases * Fixed player count being incorrect in minecraftvanilla protocol in some cases
* Updated dependencies * Updated dependencies
* Replaced deprecated Request http library with Got * Replaced deprecated Request http library with Got
### 2.0.20 ### 2.0.20
* Fixed minecraft protocol never throwing exceptions * Fixed minecraft protocol never throwing exceptions
### 2.0.19 ### 2.0.19
* Added Days of War (2017) * Added Days of War (2017)
* Added The Forrest (2014) * Added The Forrest (2014)
* Added Just Cause 3 Multiplayer (2017) * Added Just Cause 3 Multiplayer (2017)
* Added Project Reality: Battlefield 2 (2005) * Added Project Reality: Battlefield 2 (2005)
* Added Quake Live (2010) * Added Quake Live (2010)
* Added Contagion (2011) * Added Contagion (2011)
* Added Empyrion: Galactic Survival (2015) * Added Empyrion: Galactic Survival (2015)
* Added PixARK (2018) * Added PixARK (2018)
### 2.0.16, 2.0.17, 2.0.18 ### 2.0.16, 2.0.17, 2.0.18
* Various improvements to killing floor / unreal2 protocol * Various improvements to killing floor / unreal2 protocol
### 2.0.15 ### 2.0.15
* Added Hell Let Loose * Added Hell Let Loose
* Added Rising Storm 2: Vietnam * Added Rising Storm 2: Vietnam
* Added Squad * Added Squad
* Fixed DNS lookup not working in some situations when dns.lookup unexpectedly returns a string * 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) * Improved minecraft protocol for non-vanilla server implementations (bedrock, waterfall, bungeecord)
* Updated dependencies * Updated dependencies
### 2.0.14 ### 2.0.14
* Node 8 compatibility fixes * Node 8 compatibility fixes
### 2.0.13 ### 2.0.13
* Improved logging * Improved logging
### 2.0.12 ### 2.0.12
* Servers are now limited to 10000 players to prevent OOM * Servers are now limited to 10000 players to prevent OOM
* Improvements to Starmade (2012) * Improvements to Starmade (2012)
* Added Atlas (2018) * Added Atlas (2018)
### 2.0.11 ### 2.0.11
* Added Acra Sim Racing * Added Acra Sim Racing
* Added Mafia 2: Online * Added Mafia 2: Online
### 2.0.10 ### 2.0.10
* Added rFactor * Added rFactor
### 2.0.9 ### 2.0.9
* Added Vice City: Multiplayer * Added Vice City: Multiplayer
### 2.0.8 ### 2.0.8
* Improve out-of-order packet handling for gamespy1 protocol * Improve out-of-order packet handling for gamespy1 protocol
* Work-around for buggy duplicate player reporting from bf1942 servers * Work-around for buggy duplicate player reporting from bf1942 servers
* Report team names rather than IDs when possible for gamespy1 protocol * Report team names rather than IDs when possible for gamespy1 protocol
### 2.0.7 ### 2.0.7
* Prevent tcp socket errors from dumping straight to console * Prevent tcp socket errors from dumping straight to console
### 2.0.6 ### 2.0.6
* Added support for host domains requiring Punycode encoding (special characters) * Added support for host domains requiring Punycode encoding (special characters)
### 2.0.5 ### 2.0.5
* Added support for Counter-Strike: 2D * Added support for Counter-Strike: 2D
### 2.0.4 ### 2.0.4
* Added details about new 2.0 reponse fields to the README. * Added details about new 2.0 reponse fields to the README.
### 2.0.3 ### 2.0.3
* Added support for Insurgency: Sandstorm * Added support for Insurgency: Sandstorm
### 2.0.2 ### 2.0.2
* Added support for Starsiege 2009 (starsiege) * Added support for Starsiege 2009 (starsiege)
### 2.0.1 ### 2.0.1
* Updated readme games list for 2.0 * Updated readme games list for 2.0
* Fixed csgo default port * Fixed csgo default port
### 2.0.0 ### 2.0.0
##### Breaking API changes ##### Breaking API changes
* **Node 8 is now required** * **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 * 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 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. 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 * 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. 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 `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. * Removed `notes` field from options / response object. Data can be passed through a standard javascript context if needed.
##### Minor Changes ##### Minor Changes
* Rewrote core to use promises extensively for better error-handling. Async chains have been dramatically simplified * 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. by using async/await across the codebase, eliminating callback chains and the 'async' dependency.
* Replaced `--output pretty` cli parameter with `--pretty`. * Replaced `--output pretty` cli parameter with `--pretty`.
* You can now query from CLI using shorthand syntax: `gamedig --type <gameid> <ip>[:<port>]` * You can now query from CLI using shorthand syntax: `gamedig --type <gameid> <ip>[:<port>]`
* UDP socket is only opened if needed by a query. * 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 * 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 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). 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 * 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 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. 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 * 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 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. during the query.
* Improved debug logging across all parts of GameDig * Improved debug logging across all parts of GameDig
* Removed global `Gamedig.debug`. `debug` is now an option on each query. * Removed global `Gamedig.debug`. `debug` is now an option on each query.
##### Protocol Changes ##### Protocol Changes
* Added support for games using older versions of battlefield protocol. * Added support for games using older versions of battlefield protocol.
* Simplified detection of BC2 when using battlefield protocol. * Simplified detection of BC2 when using battlefield protocol.
* Fixed buildandshoot not reading player list * Fixed buildandshoot not reading player list
* Standardized all doom3 games into a single protocol, which can discover protocol discrepancies automatically. * Standardized all doom3 games into a single protocol, which can discover protocol discrepancies automatically.
* Standardized all gamespy2 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. * Standardized all gamespy3 games into a single protocol, which can discover protocol discrepancies automatically.
* Improved valve protocol challenge key retry process * Improved valve protocol challenge key retry process
### 1.0.0 ### 1.0.0
* First official release * First official release
* Node.js 6 is now required * Node.js 6 is now required

View File

@ -1,445 +1,445 @@
### Supported ### Supported
| GameDig Type ID | Name | See Also | | GameDig Type ID | Name | See Also |
|---------------------------------------|---------------------------------------------------------|--------------------------------------------------| |---------------------------------------|---------------------------------------------------------|--------------------------------------------------|
| `7d2d` | 7 Days to Die (2013) | [Valve Protocol](#valve) | | `7d2d` | 7 Days to Die (2013) | [Valve Protocol](#valve) |
| `as` | Action: Source | [Valve Protocol](#valve) | | `as` | Action: Source | [Valve Protocol](#valve) |
| `ahl` | Action Half-Life | [Valve Protocol](#valve) | | `ahl` | Action Half-Life | [Valve Protocol](#valve) |
| `ageofchivalry` | Age of Chivalry (2007) | [Valve Protocol](#valve) | | `ageofchivalry` | Age of Chivalry (2007) | [Valve Protocol](#valve) |
| `aoe2` | Age of Empires 2 (1999) | | | `aoe2` | Age of Empires 2 (1999) | |
| `alienarena` | Alien Arena (2004) | | | `alienarena` | Alien Arena (2004) | |
| `alienswarm` | Alien Swarm (2010) | [Valve Protocol](#valve) | | `alienswarm` | Alien Swarm (2010) | [Valve Protocol](#valve) |
| `avp2` | Aliens versus Predator 2 (2001) | | | `avp2` | Aliens versus Predator 2 (2001) | |
| `avp2010` | Aliens vs. Predator (2010) | [Valve Protocol](#valve) | | `avp2010` | Aliens vs. Predator (2010) | [Valve Protocol](#valve) |
| `americasarmy` | America's Army (2002) | | | `americasarmy` | America's Army (2002) | |
| `americasarmy2` | America's Army 2 (2003) | | | `americasarmy2` | America's Army 2 (2003) | |
| `americasarmy3` | America's Army 3 (2009) | [Valve Protocol](#valve) | | `americasarmy3` | America's Army 3 (2009) | [Valve Protocol](#valve) |
| `americasarmypg` | America's Army: Proving Grounds (2015) | [Valve Protocol](#valve) | | `americasarmypg` | America's Army: Proving Grounds (2015) | [Valve Protocol](#valve) |
| `arcasimracing` | Arca Sim Racing (2008) | | | `arcasimracing` | Arca Sim Racing (2008) | |
| `arkse` | Ark: Survival Evolved (2017) | [Valve Protocol](#valve) | | `arkse` | Ark: Survival Evolved (2017) | [Valve Protocol](#valve) |
| `arma2` | ARMA 2 (2009) | [Valve Protocol](#valve) | | `arma2` | ARMA 2 (2009) | [Valve Protocol](#valve) |
| `arma2oa` | ARMA 2: Operation Arrowhead (2010) | [Valve Protocol](#valve) | | `arma2oa` | ARMA 2: Operation Arrowhead (2010) | [Valve Protocol](#valve) |
| `arma3` | ARMA 3 (2013) | [Valve Protocol](#valve) | | `arma3` | ARMA 3 (2013) | [Valve Protocol](#valve) |
| `arma` | ARMA: Armed Assault (2007) | | | `arma` | ARMA: Armed Assault (2007) | |
| `armacwa` | ARMA: Cold War Assault (2011) | | | `armacwa` | ARMA: Cold War Assault (2011) | |
| `armar` | ARMA: Resistance (2011) | | | `armar` | ARMA: Resistance (2011) | |
| `armare` | ARMA: Reforger (2022) | [Valve Protocol](#valve) | | `armare` | ARMA: Reforger (2022) | [Valve Protocol](#valve) |
| `armagetron` | Armagetron Advanced (2001) | | | `armagetron` | Armagetron Advanced (2001) | |
| `assettocorsa` | Assetto Corsa (2014) | | | `assettocorsa` | Assetto Corsa (2014) | |
| `atlas` | Atlas (2018) | [Valve Protocol](#valve) | | `atlas` | Atlas (2018) | [Valve Protocol](#valve) |
| `avorion` | Avorion (2020) | [Valve Protocol](#valve) | | `avorion` | Avorion (2020) | [Valve Protocol](#valve) |
| `baldursgate` | Baldur's Gate (1998) | | | `baldursgate` | Baldur's Gate (1998) | |
| `ballisticoverkill` | Ballistic Overkill (2017) | [Valve Protocol](#valve) | | `ballisticoverkill` | Ballistic Overkill (2017) | [Valve Protocol](#valve) |
| `barotrauma` | Barotrauma (2019) | [Valve Protocol](#valve) | | `barotrauma` | Barotrauma (2019) | [Valve Protocol](#valve) |
| `bat1944` | Battalion 1944 (2018) | [Valve Protocol](#valve) | | `bat1944` | Battalion 1944 (2018) | [Valve Protocol](#valve) |
| `bf1942` | Battlefield 1942 (2002) | | | `bf1942` | Battlefield 1942 (2002) | |
| `bf2` | Battlefield 2 (2005) | | | `bf2` | Battlefield 2 (2005) | |
| `bf2142` | Battlefield 2142 (2006) | | | `bf2142` | Battlefield 2142 (2006) | |
| `bf3` | Battlefield 3 (2011) | | | `bf3` | Battlefield 3 (2011) | |
| `bf4` | Battlefield 4 (2013) | | | `bf4` | Battlefield 4 (2013) | |
| `bfh` | Battlefield Hardline (2015) | | | `bfh` | Battlefield Hardline (2015) | |
| `bfv` | Battlefield Vietnam (2004) | | | `bfv` | Battlefield Vietnam (2004) | |
| `bfbc2` | Battlefield: Bad Company 2 (2010) | | | `bfbc2` | Battlefield: Bad Company 2 (2010) | |
| `bd` | Base Defense (2017) | [Valve Protocol](#valve) | | `bd` | Base Defense (2017) | [Valve Protocol](#valve) |
| `blackmesa` | Black Mesa (2020) | [Valve Protocol](#valve) | | `blackmesa` | Black Mesa (2020) | [Valve Protocol](#valve) |
| `brainbread` | BrainBread | [Valve Protocol](#valve) | | `brainbread` | BrainBread | [Valve Protocol](#valve) |
| `brainbread2` | BrainBread 2 (2022) | [Valve Protocol](#valve) | | `brainbread2` | BrainBread 2 (2022) | [Valve Protocol](#valve) |
| `breach` | Breach (2011) | [Valve Protocol](#valve) | | `breach` | Breach (2011) | [Valve Protocol](#valve) |
| `breed` | Breed (2004) | | | `breed` | Breed (2004) | |
| `brink` | Brink (2011) | [Valve Protocol](#valve) | | `brink` | Brink (2011) | [Valve Protocol](#valve) |
| `bs` | Blade Symphony (2014) | [Valve Protocol](#valve) | | `bs` | Blade Symphony (2014) | [Valve Protocol](#valve) |
| `buildandshoot` | Build and Shoot / Ace of Spades Classic (2012) | | | `buildandshoot` | Build and Shoot / Ace of Spades Classic (2012) | |
| `cod` | Call of Duty (2003) | | | `cod` | Call of Duty (2003) | |
| `cod2` | Call of Duty 2 (2005) | | | `cod2` | Call of Duty 2 (2005) | |
| `cod3` | Call of Duty 3 (2006) | | | `cod3` | Call of Duty 3 (2006) | |
| `cod4` | Call of Duty 4: Modern Warfare (2007) | | | `cod4` | Call of Duty 4: Modern Warfare (2007) | |
| `codmw2` | Call of Duty: Modern Warfare 2 (2009) | | | `codmw2` | Call of Duty: Modern Warfare 2 (2009) | |
| `codmw3` | Call of Duty: Modern Warfare 3 (2011) | [Valve Protocol](#valve) | | `codmw3` | Call of Duty: Modern Warfare 3 (2011) | [Valve Protocol](#valve) |
| `coduo` | Call of Duty: United Offensive (2004) | | | `coduo` | Call of Duty: United Offensive (2004) | |
| `codwaw` | Call of Duty: World at War (2008) | | | `codwaw` | Call of Duty: World at War (2008) | |
| `callofjuarez` | Call of Juarez (2006) | | | `callofjuarez` | Call of Juarez (2006) | |
| `chaser` | Chaser (2003) | | | `chaser` | Chaser (2003) | |
| `chivalry` | Chivalry: Medieval Warfare (2012) | [Valve Protocol](#valve) | | `chivalry` | Chivalry: Medieval Warfare (2012) | [Valve Protocol](#valve) |
| `chrome` | Chrome (2003) | | | `chrome` | Chrome (2003) | |
| `codenamecure` | Codename CURE (2017) | [Valve Protocol](#valve) | | `codenamecure` | Codename CURE (2017) | [Valve Protocol](#valve) |
| `codenameeagle` | Codename Eagle (2000) | | | `codenameeagle` | Codename Eagle (2000) | |
| `colonysurvival` | Colony Survival (2017) | [Valve Protocol](#valve) | | `colonysurvival` | Colony Survival (2017) | [Valve Protocol](#valve) |
| `cacrenegade` | Command and Conquer: Renegade (2002) | | | `cacrenegade` | Command and Conquer: Renegade (2002) | |
| `commandos3` | Commandos 3: Destination Berlin (2003) | | | `commandos3` | Commandos 3: Destination Berlin (2003) | |
| `conanexiles` | Conan Exiles (2018) | [Valve Protocol](#valve) | | `conanexiles` | Conan Exiles (2018) | [Valve Protocol](#valve) |
| `contagion` | Contagion (2011) | [Valve Protocol](#valve) | | `contagion` | Contagion (2011) | [Valve Protocol](#valve) |
| `contactjack` | Contract J.A.C.K. (2003) | | | `contactjack` | Contract J.A.C.K. (2003) | |
| `corekeeper` | Core Keeper (2022) | [Valve Protocol](#valve) | | `corekeeper` | Core Keeper (2022) | [Valve Protocol](#valve) |
| `cs15` | Counter-Strike 1.5 (2002) | | | `cs15` | Counter-Strike 1.5 (2002) | |
| `cs16` | Counter-Strike 1.6 (2003) | [Valve Protocol](#valve) | | `cs16` | Counter-Strike 1.6 (2003) | [Valve Protocol](#valve) |
| `cs2d` | CS2D (2004) | | | `cs2d` | CS2D (2004) | |
| `cscz` | Counter-Strike: Condition Zero (2004) | [Valve Protocol](#valve) | | `cscz` | Counter-Strike: Condition Zero (2004) | [Valve Protocol](#valve) |
| `csgo` | Counter-Strike: Global Offensive (2012) | [Notes](#csgo), [Valve Protocol](#valve) | | `csgo` | Counter-Strike: Global Offensive (2012) | [Notes](#csgo), [Valve Protocol](#valve) |
| `css` | Counter-Strike: Source (2004) | [Valve Protocol](#valve) | | `css` | Counter-Strike: Source (2004) | [Valve Protocol](#valve) |
| `cs2` | Counter-Strike 2 (2023) | [Valve Protocol](#valve) | | `cs2` | Counter-Strike 2 (2023) | [Valve Protocol](#valve) |
| `creativerse` | Creativerse (2017) | [Valve Protocol](#valve) | | `creativerse` | Creativerse (2017) | [Valve Protocol](#valve) |
| `crossracing` | Cross Racing Championship Extreme 2005 (2005) | | | `crossracing` | Cross Racing Championship Extreme 2005 (2005) | |
| `crysis` | Crysis (2007) | | | `crysis` | Crysis (2007) | |
| `crysis2` | Crysis 2 (2011) | | | `crysis2` | Crysis 2 (2011) | |
| `crysiswars` | Crysis Wars (2008) | | | `crysiswars` | Crysis Wars (2008) | |
| `dab` | Double Action: Boogaloo (2014) | [Valve Protocol](#valve) | | `dab` | Double Action: Boogaloo (2014) | [Valve Protocol](#valve) |
| `daikatana` | Daikatana (2000) | | | `daikatana` | Daikatana (2000) | |
| `dnl` | Dark and Light (2017) | [Valve Protocol](#valve) | | `dnl` | Dark and Light (2017) | [Valve Protocol](#valve) |
| `dmomam` | Dark Messiah of Might and Magic (2006) | [Valve Protocol](#valve) | | `dmomam` | Dark Messiah of Might and Magic (2006) | [Valve Protocol](#valve) |
| `darkesthour` | Darkest Hour: Europe '44-'45 (2008) | | | `darkesthour` | Darkest Hour: Europe '44-'45 (2008) | |
| `dod` | Day of Defeat (2003) | [Valve Protocol](#valve) | | `dod` | Day of Defeat (2003) | [Valve Protocol](#valve) |
| `dods` | Day of Defeat: Source (2005) | [Valve Protocol](#valve) | | `dods` | Day of Defeat: Source (2005) | [Valve Protocol](#valve) |
| `dayofdragons` | Day of Dragons (2019) | [Valve Protocol](#valve) | | `dayofdragons` | Day of Dragons (2019) | [Valve Protocol](#valve) |
| `doi` | Day of Infamy (2017) | [Valve Protocol](#valve) | | `doi` | Day of Infamy (2017) | [Valve Protocol](#valve) |
| `daysofwar` | Days of War (2017) | [Valve Protocol](#valve) | | `daysofwar` | Days of War (2017) | [Valve Protocol](#valve) |
| `dayz` | DayZ (2018) | [Valve Protocol](#valve) | | `dayz` | DayZ (2018) | [Valve Protocol](#valve) |
| `dayzmod` | DayZ Mod (2013) | [Valve Protocol](#valve) | | `dayzmod` | DayZ Mod (2013) | [Valve Protocol](#valve) |
| `deadlydozenpt` | Deadly Dozen: Pacific Theater (2002) | | | `deadlydozenpt` | Deadly Dozen: Pacific Theater (2002) | |
| `dh2005` | Deer Hunter 2005 (2004) | | | `dh2005` | Deer Hunter 2005 (2004) | |
| `descent3` | Descent 3 (1999) | | | `descent3` | Descent 3 (1999) | |
| `deusex` | Deus Ex (2000) | | | `deusex` | Deus Ex (2000) | |
| `devastation` | Devastation (2003) | | | `devastation` | Devastation (2003) | |
| `dinodday` | Dino D-Day (2011) | [Valve Protocol](#valve) | | `dinodday` | Dino D-Day (2011) | [Valve Protocol](#valve) |
| `dirttrackracing2` | Dirt Track Racing 2 (2002) | | | `dirttrackracing2` | Dirt Track Racing 2 (2002) | |
| `discord` | Discord | [Notes](#discord) | | `discord` | Discord | [Notes](#discord) |
| `dmc` | Deathmatch Classic (2001) | [Valve Protocol](#valve) | | `dmc` | Deathmatch Classic (2001) | [Valve Protocol](#valve) |
| `dst` | Don't Starve Together (2016) | [Valve Protocol](#valve) | | `dst` | Don't Starve Together (2016) | [Valve Protocol](#valve) |
| `doom3` | Doom 3 (2004) | | | `doom3` | Doom 3 (2004) | |
| `dota2` | Dota 2 (2013) | [Valve Protocol](#valve) | | `dota2` | Dota 2 (2013) | [Valve Protocol](#valve) |
| `drakan` | Drakan: Order of the Flame (1999) | | | `drakan` | Drakan: Order of the Flame (1999) | |
| `dystopia` | Dystopia (2005) | [Valve Protocol](#valve) | | `dystopia` | Dystopia (2005) | [Valve Protocol](#valve) |
| `eco` | Eco (2018) | | | `eco` | Eco (2018) | |
| `empyrion` | Empyrion - Galactic Survival (2015) | [Valve Protocol](#valve) | | `empyrion` | Empyrion - Galactic Survival (2015) | [Valve Protocol](#valve) |
| `empiresmod` | Empires Mod (2008) | [Valve Protocol](#valve) | | `empiresmod` | Empires Mod (2008) | [Valve Protocol](#valve) |
| `etqw` | Enemy Territory: Quake Wars (2007) | | | `etqw` | Enemy Territory: Quake Wars (2007) | |
| `fear` | F.E.A.R. (2005) | | | `fear` | F.E.A.R. (2005) | |
| `f1c9902` | F1 Challenge '99-'02 (2002) | | | `f1c9902` | F1 Challenge '99-'02 (2002) | |
| `farcry` | Far Cry (2004) | | | `farcry` | Far Cry (2004) | |
| `farcry2` | Far Cry 2 (2008) | | | `farcry2` | Far Cry 2 (2008) | |
| `f12002` | Formula One 2002 (2002) | | | `f12002` | Formula One 2002 (2002) | |
| `fof` | Fistful of Frags (2014) | [Valve Protocol](#valve) | | `fof` | Fistful of Frags (2014) | [Valve Protocol](#valve) |
| `fortressforever` | Fortress Forever (2007) | [Valve Protocol](#valve) | | `fortressforever` | Fortress Forever (2007) | [Valve Protocol](#valve) |
| `ffow` | Frontlines: Fuel of War (2008) | | | `ffow` | Frontlines: Fuel of War (2008) | |
| `garrysmod` | Garry's Mod (2004) | [Valve Protocol](#valve) | | `garrysmod` | Garry's Mod (2004) | [Valve Protocol](#valve) |
| `geneshift`<br>`mutantfactions` | Geneshift (2017) | | | `geneshift`<br>`mutantfactions` | Geneshift (2017) | |
| `giantscitizenkabuto` | Giants: Citizen Kabuto (2000) | | | `giantscitizenkabuto` | Giants: Citizen Kabuto (2000) | |
| `globaloperations` | Global Operations (2002) | | | `globaloperations` | Global Operations (2002) | |
| `ges` | GoldenEye: Source (2010) | [Valve Protocol](#valve) | | `ges` | GoldenEye: Source (2010) | [Valve Protocol](#valve) |
| `gore` | Gore: Ultimate Soldier (2002) | | | `gore` | Gore: Ultimate Soldier (2002) | |
| `fivem` | Grand Theft Auto V - FiveM (2013) | | | `fivem` | Grand Theft Auto V - FiveM (2013) | |
| `mtasa` | Grand Theft Auto: San Andreas - Multi Theft Auto (2004) | | | `mtasa` | Grand Theft Auto: San Andreas - Multi Theft Auto (2004) | |
| `mtavc` | Grand Theft Auto: Vice City - Multi Theft Auto (2002) | | | `mtavc` | Grand Theft Auto: Vice City - Multi Theft Auto (2002) | |
| `groundbreach` | Ground Breach (2018) | [Valve Protocol](#valve) | | `groundbreach` | Ground Breach (2018) | [Valve Protocol](#valve) |
| `gunmanchronicles` | Gunman Chronicles (2000) | [Valve Protocol](#valve) | | `gunmanchronicles` | Gunman Chronicles (2000) | [Valve Protocol](#valve) |
| `hl2dm` | Half-Life 2: Deathmatch (2004) | [Valve Protocol](#valve) | | `hl2dm` | Half-Life 2: Deathmatch (2004) | [Valve Protocol](#valve) |
| `hldm` | Half-Life Deathmatch (1998) | [Valve Protocol](#valve) | | `hldm` | Half-Life Deathmatch (1998) | [Valve Protocol](#valve) |
| `hldms` | Half-Life Deathmatch: Source (2005) | [Valve Protocol](#valve) | | `hldms` | Half-Life Deathmatch: Source (2005) | [Valve Protocol](#valve) |
| `hlopfor` | Half-Life: Opposing Force (1999) | [Valve Protocol](#valve) | | `hlopfor` | Half-Life: Opposing Force (1999) | [Valve Protocol](#valve) |
| `halo` | Halo (2003) | | | `halo` | Halo (2003) | |
| `halo2` | Halo 2 (2007) | | | `halo2` | Halo 2 (2007) | |
| `hll` | Hell Let Loose | [Valve Protocol](#valve) | | `hll` | Hell Let Loose | [Valve Protocol](#valve) |
| `heretic2` | Heretic II (1998) | | | `heretic2` | Heretic II (1998) | |
| `hexen2` | Hexen II (1997) | | | `hexen2` | Hexen II (1997) | |
| `had2` | Hidden & Dangerous 2 (2003) | | | `had2` | Hidden & Dangerous 2 (2003) | |
| `homefront` | Homefront (2011) | [Valve Protocol](#valve) | | `homefront` | Homefront (2011) | [Valve Protocol](#valve) |
| `homeworld2` | Homeworld 2 (2003) | | | `homeworld2` | Homeworld 2 (2003) | |
| `hurtworld` | Hurtworld (2015) | [Valve Protocol](#valve) | | `hurtworld` | Hurtworld (2015) | [Valve Protocol](#valve) |
| `igi2` | I.G.I.-2: Covert Strike (2003) | | | `igi2` | I.G.I.-2: Covert Strike (2003) | |
| `il2` | IL-2 Sturmovik (2001) | | | `il2` | IL-2 Sturmovik (2001) | |
| `insurgency` | Insurgency (2014) | [Valve Protocol](#valve) | | `insurgency` | Insurgency (2014) | [Valve Protocol](#valve) |
| `insurgencymic` | Insurgency: Modern Infantry Combat (2007) | [Valve Protocol](#valve) | | `insurgencymic` | Insurgency: Modern Infantry Combat (2007) | [Valve Protocol](#valve) |
| `insurgencysandstorm` | Insurgency: Sandstorm (2018) | [Valve Protocol](#valve) | | `insurgencysandstorm` | Insurgency: Sandstorm (2018) | [Valve Protocol](#valve) |
| `ironstorm` | Iron Storm (2002) | | | `ironstorm` | Iron Storm (2002) | |
| `isle` | The Isle (2015) | [Valve Protocol](#valve) | | `isle` | The Isle (2015) | [Valve Protocol](#valve) |
| `jamesbondnightfire` | James Bond 007: Nightfire (2002) | | | `jamesbondnightfire` | James Bond 007: Nightfire (2002) | |
| `jc2mp` | Just Cause 2 - Multiplayer (2010) | | | `jc2mp` | Just Cause 2 - Multiplayer (2010) | |
| `jc3mp` | Just Cause 3 - Multiplayer (2017) | [Valve Protocol](#valve) | | `jc3mp` | Just Cause 3 - Multiplayer (2017) | [Valve Protocol](#valve) |
| `kspdmp` | Kerbal Space Program - DMP Multiplayer (2015) | | | `kspdmp` | Kerbal Space Program - DMP Multiplayer (2015) | |
| `killingfloor` | Killing Floor (2009) | | | `killingfloor` | Killing Floor (2009) | |
| `killingfloor2` | Killing Floor 2 (2016) | [Valve Protocol](#valve) | | `killingfloor2` | Killing Floor 2 (2016) | [Valve Protocol](#valve) |
| `kingpin` | Kingpin: Life of Crime (1999) | | | `kingpin` | Kingpin: Life of Crime (1999) | |
| `kisspc` | Kiss: Psycho Circus: The Nightmare Child (2000) | | | `kisspc` | Kiss: Psycho Circus: The Nightmare Child (2000) | |
| `kzmod` | Kreedz Climbing (2017) | [Valve Protocol](#valve) | | `kzmod` | Kreedz Climbing (2017) | [Valve Protocol](#valve) |
| `left4dead` | Left 4 Dead (2008) | [Valve Protocol](#valve) | | `left4dead` | Left 4 Dead (2008) | [Valve Protocol](#valve) |
| `left4dead2` | Left 4 Dead 2 (2009) | [Valve Protocol](#valve) | | `left4dead2` | Left 4 Dead 2 (2009) | [Valve Protocol](#valve) |
| `m2mp` | Mafia II - Multiplayer (2010) | | | `m2mp` | Mafia II - Multiplayer (2010) | |
| `m2o` | Mafia II - Online (2010) | | | `m2o` | Mafia II - Online (2010) | |
| `moh2010` | Medal of Honor (2010) | | | `moh2010` | Medal of Honor (2010) | |
| `mohab` | Medal of Honor: Airborne (2007) | | | `mohab` | Medal of Honor: Airborne (2007) | |
| `mohaa` | Medal of Honor: Allied Assault (2002) | | | `mohaa` | Medal of Honor: Allied Assault (2002) | |
| `mohbt` | Medal of Honor: Allied Assault Breakthrough (2003) | | | `mohbt` | Medal of Honor: Allied Assault Breakthrough (2003) | |
| `mohsh` | Medal of Honor: Allied Assault Spearhead (2002) | | | `mohsh` | Medal of Honor: Allied Assault Spearhead (2002) | |
| `mohpa` | Medal of Honor: Pacific Assault (2004) | | | `mohpa` | Medal of Honor: Pacific Assault (2004) | |
| `mohwf` | Medal of Honor: Warfighter (2012) | | | `mohwf` | Medal of Honor: Warfighter (2012) | |
| `medievalengineers` | Medieval Engineers (2015) | [Valve Protocol](#valve) | | `medievalengineers` | Medieval Engineers (2015) | [Valve Protocol](#valve) |
| `minecraft`<br>`minecraftping` | Minecraft (2009) | | | `minecraft`<br>`minecraftping` | Minecraft (2009) | |
| `minecraftpe`<br>`minecraftbe` | Minecraft: Bedrock Edition (2011) | | | `minecraftpe`<br>`minecraftbe` | Minecraft: Bedrock Edition (2011) | |
| `mnc` | Monday Night Combat (2011) | [Valve Protocol](#valve) | | `mnc` | Monday Night Combat (2011) | [Valve Protocol](#valve) |
| `mordhau` | Mordhau (2019) | [Valve Protocol](#valve) | | `mordhau` | Mordhau (2019) | [Valve Protocol](#valve) |
| `mumble` | Mumble - GTmurmur Plugin (2005) | [Notes](#mumble) | | `mumble` | Mumble - GTmurmur Plugin (2005) | [Notes](#mumble) |
| `mumbleping` | Mumble - Lightweight (2005) | [Notes](#mumble) | | `mumbleping` | Mumble - Lightweight (2005) | [Notes](#mumble) |
| `nascarthunder2004` | NASCAR Thunder 2004 (2003) | | | `nascarthunder2004` | NASCAR Thunder 2004 (2003) | |
| `ns` | Natural Selection (2002) | [Valve Protocol](#valve) | | `ns` | Natural Selection (2002) | [Valve Protocol](#valve) |
| `ns2` | Natural Selection 2 (2012) | [Valve Protocol](#valve) | | `ns2` | Natural Selection 2 (2012) | [Valve Protocol](#valve) |
| `nfshp2` | Need for Speed: Hot Pursuit 2 (2002) | | | `nfshp2` | Need for Speed: Hot Pursuit 2 (2002) | |
| `nab` | Nerf Arena Blast (1999) | | | `nab` | Nerf Arena Blast (1999) | |
| `netpanzer` | netPanzer (2002) | | | `netpanzer` | netPanzer (2002) | |
| `nwn` | Neverwinter Nights (2002) | | | `nwn` | Neverwinter Nights (2002) | |
| `nwn2` | Neverwinter Nights 2 (2006) | | | `nwn2` | Neverwinter Nights 2 (2006) | |
| `nexuiz` | Nexuiz (2005) | | | `nexuiz` | Nexuiz (2005) | |
| `nitrofamily` | Nitro Family (2004) | | | `nitrofamily` | Nitro Family (2004) | |
| `nmrih` | No More Room in Hell (2011) | [Valve Protocol](#valve) | | `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) | | | `nolf2` | No One Lives Forever 2: A Spy in H.A.R.M.'s Way (2002) | |
| `nucleardawn` | Nuclear Dawn (2011) | [Valve Protocol](#valve) | | `nucleardawn` | Nuclear Dawn (2011) | [Valve Protocol](#valve) |
| `onset` | Onset (2019) | [Valve Protocol](#valve) | | `onset` | Onset (2019) | [Valve Protocol](#valve) |
| `ohd` | Operation: Harsh Doorstop (2023) | [Valve Protocol](#valve) | | `ohd` | Operation: Harsh Doorstop (2023) | [Valve Protocol](#valve) |
| `openarena` | OpenArena (2005) | | | `openarena` | OpenArena (2005) | |
| `openttd` | OpenTTD (2004) | | | `openttd` | OpenTTD (2004) | |
| `operationflashpoint`<br>`flashpoint` | Operation Flashpoint: Cold War Crisis (2001) | | | `operationflashpoint`<br>`flashpoint` | Operation Flashpoint: Cold War Crisis (2001) | |
| `flashpointresistance` | Operation Flashpoint: Resistance (2002) | | | `flashpointresistance` | Operation Flashpoint: Resistance (2002) | |
| `painkiller` | Painkiller | | | `painkiller` | Painkiller | |
| `pc` | Project Cars (2015) | [Valve Protocol](#valve) | | `pc` | Project Cars (2015) | [Valve Protocol](#valve) |
| `pc2` | Project Cars 2 (2017) | [Valve Protocol](#valve) | | `pc2` | Project Cars 2 (2017) | [Valve Protocol](#valve) |
| `pixark` | PixARK (2018) | [Valve Protocol](#valve) | | `pixark` | PixARK (2018) | [Valve Protocol](#valve) |
| `pvkii` | Pirates, Vikings, and Knights II (2007) | [Valve Protocol](#valve) | | `pvkii` | Pirates, Vikings, and Knights II (2007) | [Valve Protocol](#valve) |
| `ps` | Post Scriptum | | | `ps` | Post Scriptum | |
| `postal2` | Postal 2 | | | `postal2` | Postal 2 | |
| `prey` | Prey | | | `prey` | Prey | |
| `primalcarnage` | Primal Carnage: Extinction | [Valve Protocol](#valve) | | `primalcarnage` | Primal Carnage: Extinction | [Valve Protocol](#valve) |
| `prbf2` | Project Reality: Battlefield 2 (2005) | | | `prbf2` | Project Reality: Battlefield 2 (2005) | |
| `przomboid` | Project Zomboid | [Valve Protocol](#valve) | | `przomboid` | Project Zomboid | [Valve Protocol](#valve) |
| `quake1` | Quake 1: QuakeWorld (1996) | | | `quake1` | Quake 1: QuakeWorld (1996) | |
| `quake2` | Quake 2 (1997) | | | `quake2` | Quake 2 (1997) | |
| `quake3` | Quake 3: Arena (1999) | | | `quake3` | Quake 3: Arena (1999) | |
| `quake4` | Quake 4 (2005) | | | `quake4` | Quake 4 (2005) | |
| `quakelive` | Quake Live (2010) | [Valve Protocol](#valve) | | `quakelive` | Quake Live (2010) | [Valve Protocol](#valve) |
| `ragdollkungfu` | Rag Doll Kung Fu | [Valve Protocol](#valve) | | `ragdollkungfu` | Rag Doll Kung Fu | [Valve Protocol](#valve) |
| `r6` | Rainbow Six | | | `r6` | Rainbow Six | |
| `r6roguespear` | Rainbow Six 2: Rogue Spear | | | `r6roguespear` | Rainbow Six 2: Rogue Spear | |
| `r6ravenshield` | Rainbow Six 3: Raven Shield | | | `r6ravenshield` | Rainbow Six 3: Raven Shield | |
| `rallisportchallenge` | RalliSport Challenge | | | `rallisportchallenge` | RalliSport Challenge | |
| `rallymasters` | Rally Masters | | | `rallymasters` | Rally Masters | |
| `redorchestra` | Red Orchestra | | | `redorchestra` | Red Orchestra | |
| `redorchestra2` | Red Orchestra 2 | [Valve Protocol](#valve) | | `redorchestra2` | Red Orchestra 2 | [Valve Protocol](#valve) |
| `redorchestraost` | Red Orchestra: Ostfront 41-45 | | | `redorchestraost` | Red Orchestra: Ostfront 41-45 | |
| `redline` | Redline | | | `redline` | Redline | |
| `redm` | Red Dead Redemption 2 - RedM (2018) | | | `redm` | Red Dead Redemption 2 - RedM (2018) | |
| `rtcw` | Return to Castle Wolfenstein | | | `rtcw` | Return to Castle Wolfenstein | |
| `rfactor` | rFactor | | | `rfactor` | rFactor | |
| `ricochet` | Ricochet | [Valve Protocol](#valve) | | `ricochet` | Ricochet | [Valve Protocol](#valve) |
| `riseofnations` | Rise of Nations | | | `riseofnations` | Rise of Nations | |
| `rs2` | Rising Storm 2: Vietnam | [Valve Protocol](#valve) | | `rs2` | Rising Storm 2: Vietnam | [Valve Protocol](#valve) |
| `risingworld` | Rising World (2014) | [Valve Protocol](#valve) | | `risingworld` | Rising World (2014) | [Valve Protocol](#valve) |
| `ror2` | Risk of Rain 2 (2020) | [Valve Protocol](#valve) | | `ror2` | Risk of Rain 2 (2020) | [Valve Protocol](#valve) |
| `rune` | Rune | | | `rune` | Rune | |
| `rust` | Rust | [Valve Protocol](#valve) | | `rust` | Rust | [Valve Protocol](#valve) |
| `stalker` | S.T.A.L.K.E.R. | | | `stalker` | S.T.A.L.K.E.R. | |
| `samp` | San Andreas Multiplayer | | | `samp` | San Andreas Multiplayer | |
| `saomp` | San Andreas OpenMP | | | `saomp` | San Andreas OpenMP | |
| `savage2` | Savage 2: A Tortured Soul (2008) | | | `savage2` | Savage 2: A Tortured Soul (2008) | |
| `ss` | Serious Sam | | | `ss` | Serious Sam | |
| `ss2` | Serious Sam 2 | | | `ss2` | Serious Sam 2 | |
| `shatteredhorizon` | Shattered Horizon | [Valve Protocol](#valve) | | `shatteredhorizon` | Shattered Horizon | [Valve Protocol](#valve) |
| `shogo` | Shogo | | | `shogo` | Shogo | |
| `shootmania` | Shootmania | [Notes](#nadeo-shootmania--trackmania--etc) | | `shootmania` | Shootmania | [Notes](#nadeo-shootmania--trackmania--etc) |
| `sin` | SiN | | | `sin` | SiN | |
| `sinep` | SiN Episodes | [Valve Protocol](#valve) | | `sinep` | SiN Episodes | [Valve Protocol](#valve) |
| `soldat` | Soldat | | | `soldat` | Soldat | |
| `sof` | Soldier of Fortune | | | `sof` | Soldier of Fortune | |
| `sof2` | Soldier of Fortune 2 | | | `sof2` | Soldier of Fortune 2 | |
| `sonsoftheforest` | Sons Of The Forest | [Valve Protocol](#valve) | | `sonsoftheforest` | Sons Of The Forest | [Valve Protocol](#valve) |
| `spaceengineers` | Space Engineers | [Valve Protocol](#valve) | | `spaceengineers` | Space Engineers | [Valve Protocol](#valve) |
| `squad` | Squad | [Valve Protocol](#valve) | | `squad` | Squad | [Valve Protocol](#valve) |
| `stbc` | Star Trek: Bridge Commander | | | `stbc` | Star Trek: Bridge Commander | |
| `stvef` | Star Trek: Voyager - Elite Force | | | `stvef` | Star Trek: Voyager - Elite Force | |
| `stvef2` | Star Trek: Voyager - Elite Force 2 | | | `stvef2` | Star Trek: Voyager - Elite Force 2 | |
| `swjk2` | Star Wars Jedi Knight II: Jedi Outcast (2002) | | | `swjk2` | Star Wars Jedi Knight II: Jedi Outcast (2002) | |
| `swjk` | Star Wars Jedi Knight: Jedi Academy (2003) | | | `swjk` | Star Wars Jedi Knight: Jedi Academy (2003) | |
| `swbf` | Star Wars: Battlefront | | | `swbf` | Star Wars: Battlefront | |
| `swbf2` | Star Wars: Battlefront 2 | | | `swbf2` | Star Wars: Battlefront 2 | |
| `swrc` | Star Wars: Republic Commando | | | `swrc` | Star Wars: Republic Commando | |
| `starbound` | Starbound | [Valve Protocol](#valve) | | `starbound` | Starbound | [Valve Protocol](#valve) |
| `starmade` | StarMade | | | `starmade` | StarMade | |
| `starsiege` | Starsiege (2009) | | | `starsiege` | Starsiege (2009) | |
| `suicidesurvival` | Suicide Survival | [Valve Protocol](#valve) | | `suicidesurvival` | Suicide Survival | [Valve Protocol](#valve) |
| `stn` | Survive the Nights (2017) | [Valve Protocol](#valve) | | `stn` | Survive the Nights (2017) | [Valve Protocol](#valve) |
| `svencoop` | Sven Coop | [Valve Protocol](#valve) | | `svencoop` | Sven Coop | [Valve Protocol](#valve) |
| `swat4` | SWAT 4 | | | `swat4` | SWAT 4 | |
| `synergy` | Synergy | [Valve Protocol](#valve) | | `synergy` | Synergy | [Valve Protocol](#valve) |
| `tacticalops` | Tactical Ops | | | `tacticalops` | Tactical Ops | |
| `takeonhelicopters` | Take On Helicopters (2011) | | | `takeonhelicopters` | Take On Helicopters (2011) | |
| `teamfactor` | Team Factor | | | `teamfactor` | Team Factor | |
| `tf2` | Team Fortress 2 | [Valve Protocol](#valve) | | `tf2` | Team Fortress 2 | [Valve Protocol](#valve) |
| `tfc` | Team Fortress Classic | [Valve Protocol](#valve) | | `tfc` | Team Fortress Classic | [Valve Protocol](#valve) |
| `teamspeak2` | Teamspeak 2 | | | `teamspeak2` | Teamspeak 2 | |
| `teamspeak3` | Teamspeak 3 | [Notes](#teamspeak3) | | `teamspeak3` | Teamspeak 3 | [Notes](#teamspeak3) |
| `terminus` | Terminus | | | `terminus` | Terminus | |
| `terraria`<br>`tshock` | Terraria - TShock (2011) | [Notes](#terraria) | | `terraria`<br>`tshock` | Terraria - TShock (2011) | [Notes](#terraria) |
| `forrest` | The Forrest (2014) | [Valve Protocol](#valve) | | `forrest` | The Forrest (2014) | [Valve Protocol](#valve) |
| `thefront` | The Front (2023) | [The Front](#thefront), [Valve Protocol](#valve) | | `thefront` | The Front (2023) | [The Front](#thefront), [Valve Protocol](#valve) |
| `hidden` | The Hidden (2005) | [Valve Protocol](#valve) | | `hidden` | The Hidden (2005) | [Valve Protocol](#valve) |
| `nolf` | The Operative: No One Lives Forever (2000) | | | `nolf` | The Operative: No One Lives Forever (2000) | |
| `ship` | The Ship | [Valve Protocol](#valve) | | `ship` | The Ship | [Valve Protocol](#valve) |
| `ts` | The Specialists | [Valve Protocol](#valve) | | `ts` | The Specialists | [Valve Protocol](#valve) |
| `graw` | Tom Clancy's Ghost Recon Advanced Warfighter (2006) | | | `graw` | Tom Clancy's Ghost Recon Advanced Warfighter (2006) | |
| `graw2` | Tom Clancy's Ghost Recon Advanced Warfighter 2 (2007) | | | `graw2` | Tom Clancy's Ghost Recon Advanced Warfighter 2 (2007) | |
| `theforest` | The Forest (2014) | [Valve Protocol](#valve) | | `theforest` | The Forest (2014) | [Valve Protocol](#valve) |
| `thps3` | Tony Hawk's Pro Skater 3 | | | `thps3` | Tony Hawk's Pro Skater 3 | |
| `thps4` | Tony Hawk's Pro Skater 4 | | | `thps4` | Tony Hawk's Pro Skater 4 | |
| `thu2` | Tony Hawk's Underground 2 | | | `thu2` | Tony Hawk's Underground 2 | |
| `towerunite` | Tower Unite | [Valve Protocol](#valve) | | `towerunite` | Tower Unite | [Valve Protocol](#valve) |
| `trackmania2` | Trackmania 2 | [Notes](#nadeo-shootmania--trackmania--etc) | | `trackmania2` | Trackmania 2 | [Notes](#nadeo-shootmania--trackmania--etc) |
| `trackmaniaforever` | Trackmania Forever | [Notes](#nadeo-shootmania--trackmania--etc) | | `trackmaniaforever` | Trackmania Forever | [Notes](#nadeo-shootmania--trackmania--etc) |
| `tremulous` | Tremulous | | | `tremulous` | Tremulous | |
| `tribes1` | Tribes 1: Starsiege | | | `tribes1` | Tribes 1: Starsiege | |
| `tribesvengeance` | Tribes: Vengeance | | | `tribesvengeance` | Tribes: Vengeance | |
| `tron20` | Tron 2.0 | | | `tron20` | Tron 2.0 | |
| `turok2` | Turok 2 | | | `turok2` | Turok 2 | |
| `universalcombat` | Universal Combat | | | `universalcombat` | Universal Combat | |
| `unreal` | Unreal | | | `unreal` | Unreal | |
| `ut` | Unreal Tournament | | | `ut` | Unreal Tournament | |
| `ut2003` | Unreal Tournament 2003 | | | `ut2003` | Unreal Tournament 2003 | |
| `ut2004` | Unreal Tournament 2004 | | | `ut2004` | Unreal Tournament 2004 | |
| `ut3` | Unreal Tournament 3 | | | `ut3` | Unreal Tournament 3 | |
| `unturned` | Unturned | [Valve Protocol](#valve) | | `unturned` | Unturned | [Valve Protocol](#valve) |
| `urbanterror` | Urban Terror | | | `urbanterror` | Urban Terror | |
| `vrising` | V Rising (2022) | [Valve Protocol](#valve) | | `vrising` | V Rising (2022) | [Valve Protocol](#valve) |
| `v8supercar` | V8 Supercar Challenge | | | `v8supercar` | V8 Supercar Challenge | |
| `vs` | Vampire Slayer | [Valve Protocol](#valve) | | `vs` | Vampire Slayer | [Valve Protocol](#valve) |
| `valheim` | Valheim (2021) | [Notes](#valheim), [Valve Protocol](#valve) | | `valheim` | Valheim (2021) | [Notes](#valheim), [Valve Protocol](#valve) |
| `ventrilo` | Ventrilo | | | `ventrilo` | Ventrilo | |
| `vcmp` | Vice City Multiplayer | | | `vcmp` | Vice City Multiplayer | |
| `vietcong` | Vietcong | | | `vietcong` | Vietcong | |
| `vietcong2` | Vietcong 2 | | | `vietcong2` | Vietcong 2 | |
| `warfork` | Warfork | | | `warfork` | Warfork | |
| `warsow` | Warsow | | | `warsow` | Warsow | |
| `wheeloftime` | Wheel of Time | | | `wheeloftime` | Wheel of Time | |
| `wolfenstein2009` | Wolfenstein 2009 | | | `wolfenstein2009` | Wolfenstein 2009 | |
| `wolfensteinet` | Wolfenstein: Enemy Territory | | | `wolfensteinet` | Wolfenstein: Enemy Territory | |
| `wurm` | Wurm: Unlimited | [Valve Protocol](#valve) | | `wurm` | Wurm: Unlimited | [Valve Protocol](#valve) |
| `xpandrally` | Xpand Rally | | | `xpandrally` | Xpand Rally | |
| `zombiemaster` | Zombie Master | [Valve Protocol](#valve) | | `zombiemaster` | Zombie Master | [Valve Protocol](#valve) |
| `zps` | Zombie Panic: Source | [Valve Protocol](#valve) | | `zps` | Zombie Panic: Source | [Valve Protocol](#valve) |
### Not supported (yet) ### Not supported (yet)
* Cube Engine (cube): * Cube Engine (cube):
* Cube 1 * Cube 1
* Assault Cube * Assault Cube
* Cube 2: Sauerbraten * Cube 2: Sauerbraten
* Blood Frontier * Blood Frontier
* Alien vs Predator * Alien vs Predator
* Armed Assault 2: Operation Arrowhead * Armed Assault 2: Operation Arrowhead
* Battlefield Bad Company 2: Vietnam * Battlefield Bad Company 2: Vietnam
* BFRIS * BFRIS
* Call of Duty: Black Ops 1 and 2 (no documentation, may require rcon) * Call of Duty: Black Ops 1 and 2 (no documentation, may require rcon)
* Crysis Warhead * Crysis Warhead
* Days of War * Days of War
* DirtyBomb * DirtyBomb
* Doom - Skulltag * Doom - Skulltag
* Doom - ZDaemon * Doom - ZDaemon
* ECO Global Survival ([Ref](https://github.com/Austinb/GameQ/blob/v3/src/GameQ/Protocols/Eco.php)) * ECO Global Survival ([Ref](https://github.com/Austinb/GameQ/blob/v3/src/GameQ/Protocols/Eco.php))
* Farming Simulator * Farming Simulator
* Freelancer * Freelancer
* Ghost Recon * Ghost Recon
* GRAV Online * GRAV Online
* GTA Network ([Ref](https://github.com/Austinb/GameQ/blob/v3/src/GameQ/Protocols/Gtan.php)) * GTA Network ([Ref](https://github.com/Austinb/GameQ/blob/v3/src/GameQ/Protocols/Gtan.php))
* GTR 2 * GTR 2
* Haze * Haze
* Hexen World * Hexen World
* Lost Heaven * Lost Heaven
* Multi Theft Auto * Multi Theft Auto
* Pariah * Pariah
* Plain Sight * Plain Sight
* Purge Jihad * Purge Jihad
* Red Eclipse * Red Eclipse
* Red Faction * Red Faction
* S.T.A.L.K.E.R. Clear Sky * S.T.A.L.K.E.R. Clear Sky
* Savage: The Battle For Newerth * Savage: The Battle For Newerth
* SiN 1 Multiplayer * SiN 1 Multiplayer
* South Park * South Park
* Star Wars Jedi Knight: Dark Forces II * Star Wars Jedi Knight: Dark Forces II
* Star Wars: X-Wing Alliance * Star Wars: X-Wing Alliance
* Sum of All Fears * Sum of All Fears
* Teeworlds * Teeworlds
* Tibia ([Ref](https://github.com/Austinb/GameQ/blob/v3/src/GameQ/Protocols/Tibia.php)) * Tibia ([Ref](https://github.com/Austinb/GameQ/blob/v3/src/GameQ/Protocols/Tibia.php))
* Titanfall * Titanfall
* Tribes 2 * Tribes 2
* Unreal 2 XMP * Unreal 2 XMP
* World in Conflict * World in Conflict
* World Opponent Network * World Opponent Network
* Wurm Unlimited * Wurm Unlimited
> Want support for one of these games? Please open an issue to show your interest! > 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 > __Know how to code?__ Protocol details for many of the games above are documented
> at https://github.com/gamedig/legacy-query-library-archive > at https://github.com/gamedig/legacy-query-library-archive
> , ready for you to develop into GameDig! > , ready for you to develop into GameDig!
> Don't see your game listed here? > Don't see your game listed here?
> >
> First, let us know, so we can fix it. Then, you can try using some common query > 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: > protocols directly by using one of these server types:
> * protocol-ase > * protocol-ase
> * protocol-battlefield > * protocol-battlefield
> * protocol-doom3 > * protocol-doom3
> * protocol-gamespy1 > * protocol-gamespy1
> * protocol-gamespy2 > * protocol-gamespy2
> * protocol-gamespy3 > * protocol-gamespy3
> * protocol-nadeo > * protocol-nadeo
> * protocol-quake2 > * protocol-quake2
> * protocol-quake3 > * protocol-quake3
> * protocol-unreal2 > * protocol-unreal2
> * protocol-valve > * protocol-valve
Games with Additional Notes Games with Additional Notes
--- ---
### <a name="csgo"></a>Counter-Strike: Global Offensive ### <a name="csgo"></a>Counter-Strike: Global Offensive
To receive a full player list response from CS:GO servers, the server must To receive a full player list response from CS:GO servers, the server must
have set the cvar: host_players_show 2 have set the cvar: host_players_show 2
### Discord ### Discord
You must set the `guildId` request field to the server's guild ID. Do not provide an IP. 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. 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. In order to retrieve information from discord server's they must have the `Enable server widget` option enabled.
### Mumble ### Mumble
For full query results from Mumble, you must be running the For full query results from Mumble, you must be running the
[GTmurmur plugin](http://www.gametracker.com/downloads/gtmurmurplugin.php). [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, 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 you can use the 'mumbleping' server type instead, which uses a less accurate but more reliable solution
### Nadeo (ShootMania / TrackMania / etc) ### Nadeo (ShootMania / TrackMania / etc)
The server must have xmlrpc enabled, and you must pass the xmlrpc port to GameDig, not the connection port. 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. 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 Pass the login into to GameDig with the additional options: login, password
### <a name="teamspeak3"></a>TeamSpeak 3 ### <a name="teamspeak3"></a>TeamSpeak 3
For teamspeak 3 queries to work correctly, the following permissions must be available for the guest server group: For teamspeak 3 queries to work correctly, the following permissions must be available for the guest server group:
* Virtual Server * Virtual Server
* b_virtualserver_info_view * b_virtualserver_info_view
* b_virtualserver_channel_list * b_virtualserver_channel_list
* b_virtualserver_client_list * b_virtualserver_client_list
* Group * Group
* b_virtualserver_servergroup_list * b_virtualserver_servergroup_list
* b_virtualserver_channelgroup_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), 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. you can specify their host query port using the teamspeakQueryPort option.
### Terraria ### Terraria
Requires tshock server mod, and a REST user token, which can be passed to GameDig with the Requires tshock server mod, and a REST user token, which can be passed to GameDig with the
additional option: `token` additional option: `token`
### Valheim ### Valheim
Valheim servers will only respond to queries if they are started in public mode (`-public 1`). Valheim servers will only respond to queries if they are started in public mode (`-public 1`).
### DayZ ### 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). 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 ### <a name="valve"></a>Valve Protocol
For many valve games, additional 'rules' may be fetched into the unstable `raw` field by passing the additional 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. option: `requestRules: true`. Beware that this may increase query time.
### <a name="thefront"></a>The Front ### <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. 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
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,32 +1,32 @@
export class Player { export class Player {
name = '' name = ''
raw = {} raw = {}
constructor (data) { constructor (data) {
if (typeof data === 'string') { if (typeof data === 'string') {
this.name = data this.name = data
} else { } else {
const { name, ...raw } = data const { name, ...raw } = data
if (name) this.name = name if (name) this.name = name
if (raw) this.raw = raw if (raw) this.raw = raw
} }
} }
} }
export class Players extends Array { export class Players extends Array {
push (data) { push (data) {
super.push(new Player(data)) super.push(new Player(data))
} }
} }
export class Results { export class Results {
name = '' name = ''
map = '' map = ''
password = false password = false
raw = {} raw = {}
maxplayers = 0 maxplayers = 0
players = new Players() players = new Players()
bots = new Players() bots = new Players()
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,75 +1,75 @@
import Core from './core.js' import Core from './core.js'
import Varint from 'varint' import Varint from 'varint'
export default class minecraftvanilla extends Core { export default class minecraftvanilla extends Core {
async run (state) { async run (state) {
const portBuf = Buffer.alloc(2) const portBuf = Buffer.alloc(2)
portBuf.writeUInt16BE(this.options.port, 0) portBuf.writeUInt16BE(this.options.port, 0)
const addressBuf = Buffer.from(this.options.host, 'utf8') const addressBuf = Buffer.from(this.options.host, 'utf8')
const bufs = [ const bufs = [
this.varIntBuffer(47), this.varIntBuffer(47),
this.varIntBuffer(addressBuf.length), this.varIntBuffer(addressBuf.length),
addressBuf, addressBuf,
portBuf, portBuf,
this.varIntBuffer(1) this.varIntBuffer(1)
] ]
const outBuffer = Buffer.concat([ const outBuffer = Buffer.concat([
this.buildPacket(0, Buffer.concat(bufs)), this.buildPacket(0, Buffer.concat(bufs)),
this.buildPacket(0) this.buildPacket(0)
]) ])
const data = await this.withTcp(async socket => { const data = await this.withTcp(async socket => {
return await this.tcpSend(socket, outBuffer, data => { return await this.tcpSend(socket, outBuffer, data => {
if (data.length < 10) return if (data.length < 10) return
const reader = this.reader(data) const reader = this.reader(data)
const length = reader.varint() const length = reader.varint()
if (data.length < length) return if (data.length < length) return
return reader.rest() return reader.rest()
}) })
}) })
const reader = this.reader(data) const reader = this.reader(data)
const packetId = reader.varint() const packetId = reader.varint()
this.logger.debug('Packet ID: ' + packetId) this.logger.debug('Packet ID: ' + packetId)
const strLen = reader.varint() const strLen = reader.varint()
this.logger.debug('String Length: ' + strLen) this.logger.debug('String Length: ' + strLen)
const str = reader.rest().toString('utf8') const str = reader.rest().toString('utf8')
this.logger.debug(str) this.logger.debug(str)
const json = JSON.parse(str) const json = JSON.parse(str)
delete json.favicon delete json.favicon
state.raw = json state.raw = json
state.maxplayers = json.players.max state.maxplayers = json.players.max
state.numplayers = json.players.online state.numplayers = json.players.online
if (json.players.sample) { if (json.players.sample) {
for (const player of json.players.sample) { for (const player of json.players.sample) {
state.players.push({ state.players.push({
id: player.id, id: player.id,
name: player.name name: player.name
}) })
} }
} }
} }
varIntBuffer (num) { varIntBuffer (num) {
return Buffer.from(Varint.encode(num)) return Buffer.from(Varint.encode(num))
} }
buildPacket (id, data) { buildPacket (id, data) {
if (!data) data = Buffer.from([]) if (!data) data = Buffer.from([])
const idBuffer = this.varIntBuffer(id) const idBuffer = this.varIntBuffer(id)
return Buffer.concat([ return Buffer.concat([
this.varIntBuffer(data.length + idBuffer.length), this.varIntBuffer(data.length + idBuffer.length),
idBuffer, idBuffer,
data data
]) ])
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,237 +1,237 @@
import Core from './core.js' import Core from './core.js'
export default class ventrilo extends Core { export default class ventrilo extends Core {
constructor () { constructor () {
super() super()
this.byteorder = 'be' this.byteorder = 'be'
} }
async run (state) { async run (state) {
const data = await this.sendCommand(2, '') const data = await this.sendCommand(2, '')
state.raw = splitFields(data.toString()) state.raw = splitFields(data.toString())
for (const client of state.raw.CLIENTS) { for (const client of state.raw.CLIENTS) {
client.name = client.NAME client.name = client.NAME
delete client.NAME delete client.NAME
client.ping = parseInt(client.PING) client.ping = parseInt(client.PING)
delete client.PING delete client.PING
state.players.push(client) state.players.push(client)
} }
delete state.raw.CLIENTS delete state.raw.CLIENTS
state.numplayers = state.players.length state.numplayers = state.players.length
if ('NAME' in state.raw) state.name = state.raw.NAME if ('NAME' in state.raw) state.name = state.raw.NAME
if ('MAXCLIENTS' in state.raw) state.maxplayers = state.raw.MAXCLIENTS if ('MAXCLIENTS' in state.raw) state.maxplayers = state.raw.MAXCLIENTS
if (this.trueTest(state.raw.AUTH)) state.password = true if (this.trueTest(state.raw.AUTH)) state.password = true
} }
async sendCommand (cmd, password) { async sendCommand (cmd, password) {
const body = Buffer.alloc(16) const body = Buffer.alloc(16)
body.write(password, 0, 15, 'utf8') body.write(password, 0, 15, 'utf8')
const encrypted = encrypt(cmd, body) const encrypted = encrypt(cmd, body)
const packets = {} const packets = {}
return await this.udpSend(encrypted, (buffer) => { return await this.udpSend(encrypted, (buffer) => {
if (buffer.length < 20) return if (buffer.length < 20) return
const data = decrypt(buffer) const data = decrypt(buffer)
if (data.zero !== 0) return if (data.zero !== 0) return
packets[data.packetNum] = data.body packets[data.packetNum] = data.body
if (Object.keys(packets).length !== data.packetTotal) return if (Object.keys(packets).length !== data.packetTotal) return
const out = [] const out = []
for (let i = 0; i < data.packetTotal; i++) { for (let i = 0; i < data.packetTotal; i++) {
if (!(i in packets)) throw new Error('Missing packet #' + i) if (!(i in packets)) throw new Error('Missing packet #' + i)
out.push(packets[i]) out.push(packets[i])
} }
return Buffer.concat(out) return Buffer.concat(out)
}) })
} }
} }
function splitFields (str, subMode) { function splitFields (str, subMode) {
let splitter, delim let splitter, delim
if (subMode) { if (subMode) {
splitter = '=' splitter = '='
delim = ',' delim = ','
} else { } else {
splitter = ': ' splitter = ': '
delim = '\n' delim = '\n'
} }
const split = str.split(delim) const split = str.split(delim)
const out = {} const out = {}
if (!subMode) { if (!subMode) {
out.CHANNELS = [] out.CHANNELS = []
out.CLIENTS = [] out.CLIENTS = []
} }
for (const one of split) { for (const one of split) {
const equal = one.indexOf(splitter) const equal = one.indexOf(splitter)
const key = equal === -1 ? one : one.substring(0, equal) const key = equal === -1 ? one : one.substring(0, equal)
if (!key || key === '\0') continue if (!key || key === '\0') continue
const value = equal === -1 ? '' : one.substring(equal + splitter.length) const value = equal === -1 ? '' : one.substring(equal + splitter.length)
if (!subMode && key === 'CHANNEL') out.CHANNELS.push(splitFields(value, true)) if (!subMode && key === 'CHANNEL') out.CHANNELS.push(splitFields(value, true))
else if (!subMode && key === 'CLIENT') out.CLIENTS.push(splitFields(value, true)) else if (!subMode && key === 'CLIENT') out.CLIENTS.push(splitFields(value, true))
else out[key] = value else out[key] = value
} }
return out return out
} }
function randInt (min, max) { function randInt (min, max) {
return Math.floor(Math.random() * (max - min + 1) + min) return Math.floor(Math.random() * (max - min + 1) + min)
} }
function crc (body) { function crc (body) {
let crc = 0 let crc = 0
for (let i = 0; i < body.length; i++) { for (let i = 0; i < body.length; i++) {
crc = crcTable[crc >> 8] ^ body.readUInt8(i) ^ (crc << 8) crc = crcTable[crc >> 8] ^ body.readUInt8(i) ^ (crc << 8)
crc &= 0xffff crc &= 0xffff
} }
return crc return crc
} }
function encrypt (cmd, body) { function encrypt (cmd, body) {
const headerKeyStart = randInt(0, 0xff) const headerKeyStart = randInt(0, 0xff)
const headerKeyAdd = randInt(1, 0xff) const headerKeyAdd = randInt(1, 0xff)
const bodyKeyStart = randInt(0, 0xff) const bodyKeyStart = randInt(0, 0xff)
const bodyKeyAdd = randInt(1, 0xff) const bodyKeyAdd = randInt(1, 0xff)
const header = Buffer.alloc(20) const header = Buffer.alloc(20)
header.writeUInt8(headerKeyStart, 0) header.writeUInt8(headerKeyStart, 0)
header.writeUInt8(headerKeyAdd, 1) header.writeUInt8(headerKeyAdd, 1)
header.writeUInt16BE(cmd, 4) header.writeUInt16BE(cmd, 4)
header.writeUInt16BE(body.length, 8) header.writeUInt16BE(body.length, 8)
header.writeUInt16BE(body.length, 10) header.writeUInt16BE(body.length, 10)
header.writeUInt16BE(1, 12) header.writeUInt16BE(1, 12)
header.writeUInt16BE(0, 14) header.writeUInt16BE(0, 14)
header.writeUInt8(bodyKeyStart, 16) header.writeUInt8(bodyKeyStart, 16)
header.writeUInt8(bodyKeyAdd, 17) header.writeUInt8(bodyKeyAdd, 17)
header.writeUInt16BE(crc(body), 18) header.writeUInt16BE(crc(body), 18)
let offset = headerKeyStart let offset = headerKeyStart
for (let i = 2; i < header.length; i++) { for (let i = 2; i < header.length; i++) {
let val = header.readUInt8(i) let val = header.readUInt8(i)
val += codeHead.charCodeAt(offset) + ((i - 2) % 5) val += codeHead.charCodeAt(offset) + ((i - 2) % 5)
val = val & 0xff val = val & 0xff
header.writeUInt8(val, i) header.writeUInt8(val, i)
offset = (offset + headerKeyAdd) & 0xff offset = (offset + headerKeyAdd) & 0xff
} }
offset = bodyKeyStart offset = bodyKeyStart
for (let i = 0; i < body.length; i++) { for (let i = 0; i < body.length; i++) {
let val = body.readUInt8(i) let val = body.readUInt8(i)
val += codeBody.charCodeAt(offset) + (i % 72) val += codeBody.charCodeAt(offset) + (i % 72)
val = val & 0xff val = val & 0xff
body.writeUInt8(val, i) body.writeUInt8(val, i)
offset = (offset + bodyKeyAdd) & 0xff offset = (offset + bodyKeyAdd) & 0xff
} }
return Buffer.concat([header, body]) return Buffer.concat([header, body])
} }
function decrypt (data) { function decrypt (data) {
const header = data.slice(0, 20) const header = data.slice(0, 20)
const body = data.slice(20) const body = data.slice(20)
const headerKeyStart = header.readUInt8(0) const headerKeyStart = header.readUInt8(0)
const headerKeyAdd = header.readUInt8(1) const headerKeyAdd = header.readUInt8(1)
let offset = headerKeyStart let offset = headerKeyStart
for (let i = 2; i < header.length; i++) { for (let i = 2; i < header.length; i++) {
let val = header.readUInt8(i) let val = header.readUInt8(i)
val -= codeHead.charCodeAt(offset) + ((i - 2) % 5) val -= codeHead.charCodeAt(offset) + ((i - 2) % 5)
val = val & 0xff val = val & 0xff
header.writeUInt8(val, i) header.writeUInt8(val, i)
offset = (offset + headerKeyAdd) & 0xff offset = (offset + headerKeyAdd) & 0xff
} }
const bodyKeyStart = header.readUInt8(16) const bodyKeyStart = header.readUInt8(16)
const bodyKeyAdd = header.readUInt8(17) const bodyKeyAdd = header.readUInt8(17)
offset = bodyKeyStart offset = bodyKeyStart
for (let i = 0; i < body.length; i++) { for (let i = 0; i < body.length; i++) {
let val = body.readUInt8(i) let val = body.readUInt8(i)
val -= codeBody.charCodeAt(offset) + (i % 72) val -= codeBody.charCodeAt(offset) + (i % 72)
val = val & 0xff val = val & 0xff
body.writeUInt8(val, i) body.writeUInt8(val, i)
offset = (offset + bodyKeyAdd) & 0xff offset = (offset + bodyKeyAdd) & 0xff
} }
// header format: // header format:
// key, zero, cmd, echo, totallength, thislength // key, zero, cmd, echo, totallength, thislength
// totalpacket, packetnum, body key, crc // totalpacket, packetnum, body key, crc
return { return {
zero: header.readUInt16BE(2), zero: header.readUInt16BE(2),
cmd: header.readUInt16BE(4), cmd: header.readUInt16BE(4),
packetTotal: header.readUInt16BE(12), packetTotal: header.readUInt16BE(12),
packetNum: header.readUInt16BE(14), packetNum: header.readUInt16BE(14),
body body
} }
} }
const codeHead = const codeHead =
'\x80\xe5\x0e\x38\xba\x63\x4c\x99\x88\x63\x4c\xd6\x54\xb8\x65\x7e' + '\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' + '\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' + '\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' + '\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' + '\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' + '\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' + '\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' + '\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' + '\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' + '\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' + '\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' + '\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' + '\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' + '\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' + '\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' '\xff\x95\x6d\xc7\x04\xa2\x3b\xc4\x1b\x72\xc7\x6c\x82\x60\xd1\x0d'
const codeBody = const codeBody =
'\x82\x8b\x7f\x68\x90\xe0\x44\x09\x19\x3b\x8e\x5f\xc2\x82\x38\x23' + '\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' + '\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' + '\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\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' + '\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' + '\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' + '\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' + '\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' + '\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' + '\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' + '\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' + '\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' + '\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' + '\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' + '\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' '\xdb\x77\xdd\xea\x38\x59\x89\x32\xbc\x00\x3a\x6d\x61\x4e\xdb\x29'
const crcTable = [ const crcTable = [
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
] ]

View File

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

View File

@ -1,36 +1,36 @@
import Minimist from 'minimist' import Minimist from 'minimist'
import GameDig from './../lib/index.js' import GameDig from './../lib/index.js'
const argv = Minimist(process.argv.slice(2), {}) const argv = Minimist(process.argv.slice(2), {})
const options = {} const options = {}
if (argv._.length >= 1) { if (argv._.length >= 1) {
const target = argv._[0] const target = argv._[0]
const split = target.split(':') const split = target.split(':')
options.host = split[0] options.host = split[0]
if (split.length >= 2) { if (split.length >= 2) {
options.port = split[1] options.port = split[1]
} }
} }
const gamedig = new GameDig(options) const gamedig = new GameDig(options)
const protocols = ['valve', 'gamespy1', 'gamespy2', 'gamespy3', 'goldsrc', 'minecraft', 'quake1', 'quake2', 'quake3', 'unreal2', 'valve'] const protocols = ['valve', 'gamespy1', 'gamespy2', 'gamespy3', 'goldsrc', 'minecraft', 'quake1', 'quake2', 'quake3', 'unreal2', 'valve']
const run = async () => { const run = async () => {
for (const protocol of protocols) { for (const protocol of protocols) {
try { try {
const response = await gamedig.query({ const response = await gamedig.query({
...options, ...options,
debug: true, debug: true,
type: `protocol-${protocol}` type: `protocol-${protocol}`
}) })
console.log(response) console.log(response)
process.exit() process.exit()
} catch (e) { } catch (e) {
console.log(`Error on '${protocol}': ${e}`) console.log(`Error on '${protocol}': ${e}`)
} }
} }
} }
run().then(() => {}) run().then(() => {})