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

View File

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

138
bin/gamedig.js Executable file → Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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