Merge branch 'master' into master

This commit is contained in:
Michael Morrison 2022-02-05 19:26:26 -06:00 committed by GitHub
commit 10ac17fa01
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 1246 additions and 868 deletions

186
CHANGELOG.md Normal file
View file

@ -0,0 +1,186 @@
### 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

347
README.md
View file

@ -43,6 +43,7 @@ this query port may work instead. (defaults to protocol default port)
will cause many queries to take longer even if the server is online. (default 2000)
* **attemptTimeout**: number - Milliseconds allowed for an entire query attempt. This timeout is not commonly hit,
as the socketTimeout typically fires first. (default 10000)
* **givenPortOnly**: boolean - Only attempt to query server on given port. (default false)
* **debug**: boolean - Enables massive amounts of debug logging to stdout. (default false)
### Return Value
@ -54,10 +55,9 @@ The returned state object will contain the following keys:
* **password**: boolean - If a password is required
* **maxplayers**: number
* **players**: array of objects
* Each object **may or may not** contain name, ping, score, team, address.
* The number of players online can be determined by `players.length`.
* For servers which do not provide player names, this may be an array
of empty objects (ex. `[{},{},{}]`), one for each player without a name.
* **name**: string - If the player's name is unknown, the string will be empty.
* **raw**: object - Additional information about the player if available (unstable)
* The content of this field MAY change on a per-protocol basis between GameDig patch releases (although not typical).
* **bots**: array of objects - Same schema as `players`
* **connect**: string
* This will typically include the game's `ip:port`
@ -78,32 +78,32 @@ Games List
### Supported
<!--- BEGIN GENERATED GAMES -->
| GameDig Type ID | Name | Notes
| GameDig Type ID | Name | See Also
|---|---|---
| `7d2d` | 7 Days to Die (2013)
| `ageofchivalry` | Age of Chivalry (2007)
| `7d2d` | 7 Days to Die (2013) | [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)
| `alienswarm` | Alien Swarm (2010) | [Valve Protocol](#valve)
| `avp2` | Aliens versus Predator 2 (2001)
| `avp2010` | Aliens vs. Predator (2010)
| `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)
| `americasarmypg` | America's Army: Proving Grounds (2015)
| `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)
| `arma2` | ARMA 2 (2009)
| `arma2oa` | ARMA 2: Operation Arrowhead (2010)
| `arma3` | ARMA 3 (2013)
| `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)
| `armagetron` | Armagetron Advanced (2001)
| `assettocorsa` | Assetto Corsa (2014)
| `atlas` | Atlas (2018)
| `atlas` | Atlas (2018) | [Valve Protocol](#valve)
| `baldursgate` | Baldur's Gate (1998)
| `bat1944` | Battalion 1944 (2018)
| `bat1944` | Battalion 1944 (2018) | [Valve Protocol](#valve)
| `bf1942` | Battlefield 1942 (2002)
| `bf2` | Battlefield 2 (2005)
| `bf2142` | Battlefield 2142 (2006)
@ -112,16 +112,16 @@ Games List
| `bfh` | Battlefield Hardline (2015)
| `bfv` | Battlefield Vietnam (2004)
| `bfbc2` | Battlefield: Bad Company 2 (2010)
| `breach` | Breach (2011)
| `breach` | Breach (2011) | [Valve Protocol](#valve)
| `breed` | Breed (2004)
| `brink` | Brink (2011)
| `brink` | Brink (2011) | [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)
| `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)
@ -130,86 +130,87 @@ Games List
| `codenameeagle` | Codename Eagle (2000)
| `cacrenegade` | Command and Conquer: Renegade (2002)
| `commandos3` | Commandos 3: Destination Berlin (2003)
| `conanexiles` | Conan Exiles (2018)
| `contagion` | Contagion (2011)
| `conanexiles` | Conan Exiles (2018) | [Valve Protocol](#valve)
| `contagion` | Contagion (2011) | [Valve Protocol](#valve)
| `contactjack` | Contract J.A.C.K. (2003)
| `cs15` | Counter-Strike 1.5 (2002)
| `cs16` | Counter-Strike 1.6 (2003)
| `cs15` | Counter-Strike 1.5 (2002) | [Valve Protocol](#valve)
| `cs16` | Counter-Strike 1.6 (2003) | [Valve Protocol](#valve)
| `cs2d` | Counter-Strike: 2D (2004)
| `cscz` | Counter-Strike: Condition Zero (2004)
| `csgo` | Counter-Strike: Global Offensive (2012) | [Notes](#csgo)
| `css` | Counter-Strike: Source (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)
| `crossracing` | Cross Racing Championship Extreme 2005 (2005)
| `crysis` | Crysis (2007)
| `crysis2` | Crysis 2 (2011)
| `crysiswars` | Crysis Wars (2008)
| `daikatana` | Daikatana (2000)
| `dnl` | Dark and Light (2017)
| `dmomam` | Dark Messiah of Might and Magic (2006)
| `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)
| `dods` | Day of Defeat: Source (2005)
| `doi` | Day of Infamy (2017)
| `daysofwar` | Days of War (2017)
| `dayz` | DayZ (2018)
| `dayzmod` | DayZ Mod (2013)
| `dod` | Day of Defeat (2003) | [Valve Protocol](#valve)
| `dods` | Day of Defeat: Source (2005) | [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)
| `dinodday` | Dino D-Day (2011) | [Valve Protocol](#valve)
| `dirttrackracing2` | Dirt Track Racing 2 (2002)
| `discord` | Discord | [Notes](#discord)
| `doom3` | Doom 3 (2004)
| `dota2` | Dota 2 (2013)
| `dota2` | Dota 2 (2013) | [Valve Protocol](#valve)
| `drakan` | Drakan: Order of the Flame (1999)
| `empyrion` | Empyrion - Galactic Survival (2015)
| `empyrion` | Empyrion - Galactic Survival (2015) | [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)
| `fortressforever` | Fortress Forever (2007)
| `fortressforever` | Fortress Forever (2007) | [Valve Protocol](#valve)
| `ffow` | Frontlines: Fuel of War (2008)
| `garrysmod` | Garry's Mod (2004)
| `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)
| `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)
| `gunmanchronicles` | Gunman Chronicles (2000)
| `hl2dm` | Half-Life 2: Deathmatch (2004)
| `hldm` | Half-Life Deathmatch (1998)
| `hldms` | Half-Life Deathmatch: Source (2005)
| `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)
| `halo` | Halo (2003)
| `halo2` | Halo 2 (2007)
| `hll` | Hell Let Loose
| `hll` | Hell Let Loose | [Valve Protocol](#valve)
| `heretic2` | Heretic II (1998)
| `hexen2` | Hexen II (1997)
| `had2` | Hidden & Dangerous 2 (2003)
| `homefront` | Homefront (2011)
| `homefront` | Homefront (2011) | [Valve Protocol](#valve)
| `homeworld2` | Homeworld 2 (2003)
| `hurtworld` | Hurtworld (2015)
| `hurtworld` | Hurtworld (2015) | [Valve Protocol](#valve)
| `igi2` | I.G.I.-2: Covert Strike (2003)
| `il2` | IL-2 Sturmovik (2001)
| `insurgency` | Insurgency (2014)
| `insurgencysandstorm` | Insurgency: Sandstorm (2018)
| `insurgency` | Insurgency (2014) | [Valve Protocol](#valve)
| `insurgencysandstorm` | Insurgency: Sandstorm (2018) | [Valve Protocol](#valve)
| `ironstorm` | Iron Storm (2002)
| `jamesbondnightfire` | James Bond 007: Nightfire (2002)
| `jc2mp` | Just Cause 2 - Multiplayer (2010)
| `jc3mp` | Just Cause 3 - Multiplayer (2017)
| `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)
| `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)
| `left4dead` | Left 4 Dead (2008)
| `left4dead2` | Left 4 Dead 2 (2009)
| `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)
@ -219,16 +220,16 @@ Games List
| `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)
| `medievalengineers` | Medieval Engineers (2015) | [Valve Protocol](#valve)
| `minecraft`<br>`minecraftping` | Minecraft (2009)
| `minecraftpe`<br>`minecraftbe` | Minecraft: Bedrock Edition (2011)
| `mnc` | Monday Night Combat (2011)
| `mordhau` | Mordhau (2019)
| `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)
| `ns2` | Natural Selection 2 (2012)
| `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)
@ -236,56 +237,57 @@ Games List
| `nwn2` | Neverwinter Nights 2 (2006)
| `nexuiz` | Nexuiz (2005)
| `nitrofamily` | Nitro Family (2004)
| `nmrih` | No More Room in Hell (2011)
| `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)
| `nucleardawn` | Nuclear Dawn (2011) | [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
| `pixark` | PixARK (2018)
| `pixark` | PixARK (2018) | [Valve Protocol](#valve)
| `ps` | Post Scriptum
| `postal2` | Postal 2
| `prey` | Prey
| `primalcarnage` | Primal Carnage: Extinction
| `primalcarnage` | Primal Carnage: Extinction | [Valve Protocol](#valve)
| `prbf2` | Project Reality: Battlefield 2 (2005)
| `quake1` | Quake 1: QuakeWorld (1996)
| `quake2` | Quake 2 (1997)
| `quake3` | Quake 3: Arena (1999)
| `quake4` | Quake 4 (2005)
| `quakelive` | Quake Live (2010)
| `ragdollkungfu` | Rag Doll Kung Fu
| `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
| `redorchestra2` | Red Orchestra 2 | [Valve Protocol](#valve)
| `redorchestraost` | Red Orchestra: Ostfront 41-45
| `redline` | Redline
| `rtcw` | Return to Castle Wolfenstein
| `rfactor` | rFactor
| `ricochet` | Ricochet
| `ricochet` | Ricochet | [Valve Protocol](#valve)
| `riseofnations` | Rise of Nations
| `rs2` | Rising Storm 2: Vietnam
| `rs2` | Rising Storm 2: Vietnam | [Valve Protocol](#valve)
| `rune` | Rune
| `rust` | Rust
| `rust` | Rust | [Valve Protocol](#valve)
| `stalker` | S.T.A.L.K.E.R.
| `samp` | San Andreas Multiplayer
| `savage2` | Savage 2: A Tortured Soul (2008)
| `ss` | Serious Sam
| `ss2` | Serious Sam 2
| `shatteredhorizon` | Shattered Horizon
| `shatteredhorizon` | Shattered Horizon | [Valve Protocol](#valve)
| `shogo` | Shogo
| `shootmania` | Shootmania | [Notes](#nadeo-shootmania--trackmania--etc)
| `sin` | SiN
| `sinep` | SiN Episodes
| `sinep` | SiN Episodes | [Valve Protocol](#valve)
| `soldat` | Soldat
| `sof` | Soldier of Fortune
| `sof2` | Soldier of Fortune 2
| `spaceengineers` | Space Engineers
| `squad` | Squad
| `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
@ -294,32 +296,32 @@ Games List
| `swbf` | Star Wars: Battlefront
| `swbf2` | Star Wars: Battlefront 2
| `swrc` | Star Wars: Republic Commando
| `starbound` | Starbound
| `starbound` | Starbound | [Valve Protocol](#valve)
| `starmade` | StarMade
| `starsiege` | Starsiege (2009)
| `suicidesurvival` | Suicide Survival
| `svencoop` | Sven Coop
| `suicidesurvival` | Suicide Survival | [Valve Protocol](#valve)
| `svencoop` | Sven Coop | [Valve Protocol](#valve)
| `swat4` | SWAT 4
| `synergy` | Synergy
| `synergy` | Synergy | [Valve Protocol](#valve)
| `tacticalops` | Tactical Ops
| `takeonhelicopters` | Take On Helicopters (2011)
| `teamfactor` | Team Factor
| `tf2` | Team Fortress 2
| `tfc` | Team Fortress Classic
| `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)
| `hidden` | The Hidden (2005)
| `forrest` | The Forrest (2014) | [Valve Protocol](#valve)
| `hidden` | The Hidden (2005) | [Valve Protocol](#valve)
| `nolf` | The Operative: No One Lives Forever (2000)
| `ship` | The Ship
| `ship` | The Ship | [Valve Protocol](#valve)
| `graw` | Tom Clancy's Ghost Recon Advanced Warfighter (2006)
| `graw2` | Tom Clancy's Ghost Recon Advanced Warfighter 2 (2007)
| `thps3` | Tony Hawk's Pro Skater 3
| `thps4` | Tony Hawk's Pro Skater 4
| `thu2` | Tony Hawk's Underground 2
| `towerunite` | Tower Unite
| `towerunite` | Tower Unite | [Valve Protocol](#valve)
| `trackmania2` | Trackmania 2 | [Notes](#nadeo-shootmania--trackmania--etc)
| `trackmaniaforever` | Trackmania Forever | [Notes](#nadeo-shootmania--trackmania--etc)
| `tremulous` | Tremulous
@ -333,9 +335,10 @@ Games List
| `ut2003` | Unreal Tournament 2003
| `ut2004` | Unreal Tournament 2004
| `ut3` | Unreal Tournament 3
| `unturned` | unturned
| `unturned` | unturned | [Valve Protocol](#valve)
| `urbanterror` | Urban Terror
| `v8supercar` | V8 Supercar Challenge
| `valheim` | Valheim (2021) | [Notes](#valheim), [Valve Protocol](#valve)
| `ventrilo` | Ventrilo
| `vcmp` | Vice City Multiplayer
| `vietcong` | Vietcong
@ -345,8 +348,8 @@ Games List
| `wolfenstein2009` | Wolfenstein 2009
| `wolfensteinet` | Wolfenstein: Enemy Territory
| `xpandrally` | Xpand Rally
| `zombiemaster` | Zombie Master
| `zps` | Zombie Panic: Source
| `zombiemaster` | Zombie Master | [Valve Protocol](#valve)
| `zps` | Zombie Panic: Source | [Valve Protocol](#valve)
<!--- END GENERATED GAMES -->
@ -385,7 +388,6 @@ Games List
* Red Faction
* S.T.A.L.K.E.R. Clear Sky
* Savage: The Battle For Newerth
* Savage 2: A Tortured Soul
* SiN 1 Multiplayer
* South Park
* Star Wars Jedi Knight: Dark Forces II
@ -402,7 +404,7 @@ Games List
> 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/sonicsnes/legacy-query-library-archive
> at https://github.com/gamedig/legacy-query-library-archive
> , ready for you to develop into GameDig!
> Don't see your game listed here?
@ -428,6 +430,11 @@ Games with Additional Notes
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).
@ -452,7 +459,49 @@ For teamspeak 3 queries to work correctly, the following permissions must be ava
### Terraria
Requires tshock server mod, and a REST user token, which can be passed to GameDig with the
additional option: token
additional option: `token`
### Valheim
Valheim servers will only respond to queries if they are started in public mode (`-public 1`).
### <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.
Common Issues
---
### Firewalls block incoming UDP
*(replit / docker / some VPS providers)*
Most game query protocols require a UDP request and response. This means that in some environments, gamedig may not be able to receive the reponse required due to environmental restrictions.
Some examples include:
* Docker containers
* You may need to run the container in `--network host` mode so that gamedig can bind a UDP listen port.
* Alternatively, you can forward a single UDP port to your container, and force gamedig to listen on that port using the
instructions in the section down below.
* replit
* Most online IDEs run in an isolated container, which will never receive UDP responses from outside networks.
* Various VPS / server providers
* Even if your server provider doesn't explicitly block incoming UDP packets, some server hosts block other server hosts from connecting to them for DDOS-mitigation and anti-botting purposes.
### Gamedig doesn't work in the browser
Gamedig cannot operate within a browser. This means you cannot package it as part of your webpack / browserify / rollup / parcel package.
Even if you were able to get it packaged into a bundle, unfortunately no browsers support the UDP protocols required to query server status
from most game servers. As an alternative, we'd recommend using gamedig on your server-side, then expose your own API to your webapp's frontend
displaying the status information. If your application is thin (with no constant server component), you may wish to investigate a server-less lambda provider.
### Specifying a listen UDP port override
In some very rare scenarios, you may need to bind / listen on a fixed local UDP port. The is usually not needed except behind
some extremely strict firewalls, or within a docker container (where you only wish to forward a single UDP port).
To use a fixed listen udp port, construct a new Gamedig object like this:
```
const gamedig = new Gamedig({
listenUdpPort: 13337
});
gamedig.query(...)
```
Usage from Command Line
---
@ -470,121 +519,3 @@ gamedig --type minecraft mc.example.com:11234
The output of the command will be in JSON format. Additional advanced parameters can be passed in
as well: `--debug`, `--pretty`, `--socketTimeout 5000`, etc.
Changelog
---
### 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

11
bin/gamedig.js Normal file → Executable file
View file

@ -4,13 +4,16 @@ const Minimist = require('minimist'),
Gamedig = require('..');
const argv = Minimist(process.argv.slice(2), {
boolean: ['pretty','debug']
boolean: ['pretty','debug','givenPortOnly','requestRules'],
string: ['guildId','listenUdpPort']
});
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)) {
@ -34,8 +37,12 @@ if (argv._.length >= 1) {
if (debug) {
options.debug = true;
}
if (givenPortOnly) {
options.givenPortOnly = true;
}
Gamedig.query(options)
const gamedig = new Gamedig(options);
gamedig.query(options)
.then((state) => {
if(pretty) {
console.log(JSON.stringify(state,null,' '));

View file

@ -88,6 +88,7 @@ deusex|Deus Ex (2000)|gamespy2|port=7791,port_query_offset=1
devastation|Devastation (2003)|unreal2|port=7777,port_query_offset=1
dinodday|Dino D-Day (2011)|valve|port=27015
dirttrackracing2|Dirt Track Racing 2 (2002)|gamespy1|port=32240,port_query_offset=-100
discord|Discord|discord||doc_notes=discord
dnl|Dark and Light (2017)|valve|port=7777,port_query=27015
dod|Day of Defeat (2003)|valve|port=27015
dods|Day of Defeat: Source (2005)|valve|port=27015
@ -217,6 +218,7 @@ rs2|Rising Storm 2: Vietnam|valve|port=27015
rune|Rune|gamespy1|port=7777,port_query_offset=1
rust|Rust|valve|port=28015
samp|San Andreas Multiplayer|samp|port=7777
savage2|Savage 2: A Tortured Soul (2008)|savage2|port_query=11235
spaceengineers|Space Engineers|valve|port=27015
ss|Serious Sam|gamespy1|port=25600,port_query_offset=1
ss2|Serious Sam 2|gamespy2|port=25600
@ -234,7 +236,7 @@ stalker|S.T.A.L.K.E.R.|gamespy3|port=5445,port_query_offset=2
stbc|Star Trek: Bridge Commander|gamespy1|port_query=22101
stvef|Star Trek: Voyager - Elite Force|quake3|port_query=27960
stvef2|Star Trek: Voyager - Elite Force 2|quake3|port_query=29253
squad|Squad|squad|port=7787,port_query=27165
squad|Squad|valve|port=7787,port_query=27165
swbf|Star Wars: Battlefront|gamespy2|port_query=3658
swbf2|Star Wars: Battlefront 2|gamespy2|port_query=3658
swjk|Star Wars Jedi Knight: Jedi Academy (2003)|quake3|port_query=29070
@ -279,6 +281,7 @@ ut3|Unreal Tournament 3|ut3|port=7777,port_query_offset=-1277
urbanterror|Urban Terror|quake3|port_query=27960
v8supercar|V8 Supercar Challenge|gamespy1|port_query=16700
valheim|Valheim (2021)|valve|port=2456,port_query_offset=1|doc_notes=valheim
vcmp|Vice City Multiplayer|vcmp|port=8192
ventrilo|Ventrilo|ventrilo|port=3784
vietcong|Vietcong|gamespy1|port=5425,port_query=15425

View file

@ -24,7 +24,7 @@ class GameResolver {
printReadme() {
let out = '';
out += '| GameDig Type ID | Name | Notes\n';
out += '| GameDig Type ID | Name | See Also\n';
out += '|---|---|---\n';
const sorted = this.games
@ -36,8 +36,16 @@ class GameResolver {
let keysOut = game.keys.map(key => '`'+key+'`').join('<br>');
out += "| " + keysOut.padEnd(10, " ") + " "
+ "| " + game.pretty;
if(game.extra.doc_notes)
out += " | [Notes](#"+game.extra.doc_notes+")";
let notes = [];
if(game.extra.doc_notes) {
notes.push("[Notes](#" + game.extra.doc_notes + ")");
}
if(game.options.protocol === 'valve') {
notes.push('[Valve Protocol](#valve)');
}
if(notes.length) {
out += " | " + notes.join(', ');
}
out += "\n";
}
return out;

View file

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

View file

@ -9,8 +9,10 @@ const defaultOptions = {
};
class QueryRunner {
constructor() {
this.udpSocket = new GlobalUdpSocket();
constructor(runnerOpts = {}) {
this.udpSocket = new GlobalUdpSocket({
port: runnerOpts.listenUdpPort
});
this.gameResolver = new GameResolver();
this.protocolResolver = new ProtocolResolver();
}
@ -30,7 +32,7 @@ class QueryRunner {
const attempts = [];
if (userOptions.port) {
if (gameQueryPortOffset) {
if (gameQueryPortOffset && !userOptions.givenPortOnly) {
attempts.push({
...defaultOptions,
...gameOptions,
@ -38,7 +40,7 @@ class QueryRunner {
port: userOptions.port + gameQueryPortOffset
});
}
if (userOptions.port === gameOptions.port && gameQueryPort) {
if (userOptions.port === gameOptions.port && gameQueryPort && !userOptions.givenPortOnly) {
attempts.push({
...defaultOptions,
...gameOptions,
@ -66,7 +68,12 @@ class QueryRunner {
port: gameOptions.port + (gameQueryPortOffset || 0)
});
} else {
throw new Error("Could not determine port to query. Did you provide a port or gameid?");
// Hopefully the request doesn't need a port. If it does, it'll fail when making the request.
attempts.push({
...defaultOptions,
...gameOptions,
...userOptions
});
}
const numRetries = userOptions.maxAttempts || gameOptions.maxAttempts || defaultOptions.maxAttempts;

44
lib/Results.js Normal file
View file

@ -0,0 +1,44 @@
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;
}
}
}
class Players extends Array {
setNum(num) {
// If the server specified some ridiculous number of players (billions), we don't want to
// run out of ram allocating these objects.
num = Math.min(num, 10000);
while(this.length < num) {
this.push({});
}
}
push(data) {
super.push(new Player(data));
}
}
class Results {
name = '';
map = '';
password = false;
raw = {};
maxplayers = 0;
players = new Players();
bots = new Players();
}
module.exports = Results;

View file

@ -3,8 +3,8 @@ const QueryRunner = require('./QueryRunner');
let singleton = null;
class Gamedig {
constructor() {
this.queryRunner = new QueryRunner();
constructor(runnerOpts) {
this.queryRunner = new QueryRunner(runnerOpts);
}
async query(userOptions) {

View file

@ -115,12 +115,12 @@ class Reader {
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).toString();
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).toString();
else if(bytes === 8) r = readUInt64LE(this.buffer,this.i);
}
}
this.i += bytes;

552
package-lock.json generated
View file

@ -1,32 +1,66 @@
{
"name": "gamedig",
"version": "2.0.14",
"version": "3.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@sindresorhus/is": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-3.1.0.tgz",
"integrity": "sha512-n4J+zu52VdY43kdi/XdI9DzuMr1Mur8zFL5ZRG2opCans9aiFwkPxHYFEb5Xgy7n1Z4K6WfI4FpqUqsh3E8BPQ=="
},
"@szmarczak/http-timer": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.5.tgz",
"integrity": "sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ==",
"requires": {
"defer-to-connect": "^2.0.0"
}
},
"@types/cacheable-request": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.1.tgz",
"integrity": "sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ==",
"requires": {
"@types/http-cache-semantics": "*",
"@types/keyv": "*",
"@types/node": "*",
"@types/responselike": "*"
}
},
"@types/cheerio": {
"version": "0.22.13",
"resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.13.tgz",
"integrity": "sha512-OZd7dCUOUkiTorf97vJKwZnSja/DmHfuBAroe1kREZZTCf/tlFecwHhsOos3uVHxeKGZDwzolIrCUApClkdLuA==",
"version": "0.22.21",
"resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.21.tgz",
"integrity": "sha512-aGI3DfswwqgKPiEOTaiHV2ZPC9KEhprpgEbJnv0fZl3SGX0cGgEva1126dGrMC6AJM6v/aihlUgJn9M5DbDZ/Q==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/node": {
"version": "8.10.54",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.54.tgz",
"integrity": "sha512-kaYyLYf6ICn6/isAyD4K1MyWWd5Q3JgH6bnMN089LUx88+s4W8GvK9Q6JMBVu5vsFFp7pMdSxdKmlBXwH/VFRg=="
"@types/http-cache-semantics": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz",
"integrity": "sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A=="
},
"ajv": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.2.tgz",
"integrity": "sha512-FBHEW6Jf5TB9MGBgUUA9XHkTbjXYfAUjY43ACMfmdMRHniyoMHjHjzD50OK8LGDWQwp4rWEsIq5kEqq7rvIM1g==",
"@types/keyv": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.1.tgz",
"integrity": "sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw==",
"requires": {
"fast-deep-equal": "^2.0.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
"@types/node": "*"
}
},
"@types/node": {
"version": "12.20.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.7.tgz",
"integrity": "sha512-gWL8VUkg8VRaCAUgG9WmhefMqHmMblxe2rVpMF86nZY/+ZysU+BkAp+3cz03AixWDSSz0ks5WX59yAhv/cDwFA=="
},
"@types/responselike": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
"integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==",
"requires": {
"@types/node": "*"
}
},
"amdefine": {
@ -39,34 +73,6 @@
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8="
},
"asn1": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
"integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
"requires": {
"safer-buffer": "~2.1.0"
}
},
"assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
},
"aws4": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
},
"barse": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/barse/-/barse-0.4.3.tgz",
@ -75,28 +81,29 @@
"readable-stream": "~1.0.2"
}
},
"bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
"requires": {
"tweetnacl": "^0.14.3"
}
},
"bluebird": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.1.tgz",
"integrity": "sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg=="
},
"boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
},
"caseless": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
"cacheable-lookup": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz",
"integrity": "sha512-W+JBqF9SWe18A72XFzN/V/CULFzPm7sBXzzR6ekkE+3tLG72wFZrBiBZhrZuDoYexop4PHJVdFAKb/Nj9+tm9w=="
},
"cacheable-request": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.1.tgz",
"integrity": "sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw==",
"requires": {
"clone-response": "^1.0.2",
"get-stream": "^5.1.0",
"http-cache-semantics": "^4.0.0",
"keyv": "^4.0.0",
"lowercase-keys": "^2.0.0",
"normalize-url": "^4.1.0",
"responselike": "^2.0.0"
}
},
"cheerio": {
"version": "1.0.0-rc.3",
@ -111,12 +118,12 @@
"parse5": "^3.0.1"
}
},
"combined-stream": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
"integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
"clone-response": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
"integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=",
"requires": {
"delayed-stream": "~1.0.0"
"mimic-response": "^1.0.0"
}
},
"commander": {
@ -157,18 +164,25 @@
"resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz",
"integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg=="
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
"decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"requires": {
"assert-plus": "^1.0.0"
"mimic-response": "^3.1.0"
},
"dependencies": {
"mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="
}
}
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
"defer-to-connect": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.0.tgz",
"integrity": "sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg=="
},
"dom-serializer": {
"version": "0.1.1",
@ -201,13 +215,12 @@
"domelementtype": "1"
}
},
"ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
"integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
"end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"requires": {
"jsbn": "~0.1.0",
"safer-buffer": "^2.1.0"
"once": "^1.4.0"
}
},
"entities": {
@ -220,41 +233,6 @@
"resolved": "https://registry.npmjs.org/event-to-promise/-/event-to-promise-0.7.0.tgz",
"integrity": "sha1-ywffzUGNoiIdkPd+q3E7wjXiCQ8="
},
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"extsprintf": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
},
"fast-deep-equal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
},
"fast-json-stable-stringify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
"integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
},
"forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
},
"form-data": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
}
},
"gbxremote": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/gbxremote/-/gbxremote-0.2.1.tgz",
@ -267,12 +245,30 @@
"xmlrpc": "^1.3.1"
}
},
"getpass": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
"integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
"get-stream": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz",
"integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==",
"requires": {
"assert-plus": "^1.0.0"
"pump": "^3.0.0"
}
},
"got": {
"version": "11.5.1",
"resolved": "https://registry.npmjs.org/got/-/got-11.5.1.tgz",
"integrity": "sha512-reQEZcEBMTGnujmQ+Wm97mJs/OK6INtO6HmLI+xt3+9CvnRwWjXutUvb2mqr+Ao4Lu05Rx6+udx9sOQAmExMxA==",
"requires": {
"@sindresorhus/is": "^3.0.0",
"@szmarczak/http-timer": "^4.0.5",
"@types/cacheable-request": "^6.0.1",
"@types/responselike": "^1.0.0",
"cacheable-lookup": "^5.0.3",
"cacheable-request": "^7.0.1",
"decompress-response": "^6.0.0",
"http2-wrapper": "^1.0.0-beta.5.0",
"lowercase-keys": "^2.0.0",
"p-cancelable": "^2.0.0",
"responselike": "^2.0.0"
}
},
"graceful-readlink": {
@ -280,20 +276,6 @@
"resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz",
"integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU="
},
"har-schema": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
"integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
},
"har-validator": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
"integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
"requires": {
"ajv": "^6.5.5",
"har-schema": "^2.0.0"
}
},
"htmlparser2": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
@ -332,22 +314,26 @@
}
}
},
"http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
"integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
"http-cache-semantics": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
"integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ=="
},
"http2-wrapper": {
"version": "1.0.0-beta.5.2",
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.0-beta.5.2.tgz",
"integrity": "sha512-xYz9goEyBnC8XwXDTuC/MZ6t+MrKVQZOk4s7+PaDkwIsQd8IwqvM+0M6bA/2lvG8GHXcPdf+MejTUeO2LCPCeQ==",
"requires": {
"assert-plus": "^1.0.0",
"jsprim": "^1.2.2",
"sshpk": "^1.7.0"
"quick-lru": "^5.1.1",
"resolve-alpn": "^1.0.0"
}
},
"iconv-lite": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.0.tgz",
"integrity": "sha512-NnEhI9hIEKHOzJ4f697DMz9IQEXr/MMJ5w64vN2/4Ai+wRnvV7SBrL0KLoRlwaKVghOc7LQ5YkPLuX146b6Ydw==",
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz",
"integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
"safer-buffer": ">= 2.1.2 < 3.0.0"
}
},
"inherits": {
@ -355,84 +341,58 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
},
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
"json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
},
"jsbn": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
},
"json-schema": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
},
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
},
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
"integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
"keyv": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.1.tgz",
"integrity": "sha512-xz6Jv6oNkbhrFCvCP7HQa8AaII8y8LRpoSm661NOKLr4uHuBwhX4epXrPQgF3+xdJnN4Esm5X0xwY4bOlALOtw==",
"requires": {
"assert-plus": "1.0.0",
"extsprintf": "1.3.0",
"json-schema": "0.2.3",
"verror": "1.10.0"
"json-buffer": "3.0.1"
}
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
},
"long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
},
"mime-db": {
"version": "1.37.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz",
"integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg=="
"lowercase-keys": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="
},
"mime-types": {
"version": "2.1.21",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz",
"integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==",
"requires": {
"mime-db": "~1.37.0"
}
"mimic-response": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="
},
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
},
"moment": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz",
"integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ=="
},
"normalize-url": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz",
"integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ=="
},
"nth-check": {
"version": "1.0.2",
@ -442,10 +402,18 @@
"boolbase": "~1.0.0"
}
},
"oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"requires": {
"wrappy": "1"
}
},
"p-cancelable": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz",
"integrity": "sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg=="
},
"parse5": {
"version": "3.0.3",
@ -455,30 +423,29 @@
"@types/node": "*"
}
},
"performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
},
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"psl": {
"version": "1.1.31",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz",
"integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw=="
"pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"requires": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
},
"qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
"quick-lru": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="
},
"readable-stream": {
"version": "1.0.34",
@ -491,68 +458,17 @@
"string_decoder": "~0.10.x"
}
},
"request": {
"version": "2.88.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
"integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
"requires": {
"aws-sign2": "~0.7.0",
"aws4": "^1.8.0",
"caseless": "~0.12.0",
"combined-stream": "~1.0.6",
"extend": "~3.0.2",
"forever-agent": "~0.6.1",
"form-data": "~2.3.2",
"har-validator": "~5.1.0",
"http-signature": "~1.2.0",
"is-typedarray": "~1.0.0",
"isstream": "~0.1.2",
"json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19",
"oauth-sign": "~0.9.0",
"performance-now": "^2.1.0",
"qs": "~6.5.2",
"safe-buffer": "^5.1.2",
"tough-cookie": "~2.4.3",
"tunnel-agent": "^0.6.0",
"uuid": "^3.3.2"
},
"dependencies": {
"tough-cookie": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
"integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
"requires": {
"psl": "^1.1.24",
"punycode": "^1.4.1"
},
"dependencies": {
"punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
}
}
}
}
"resolve-alpn": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.0.0.tgz",
"integrity": "sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA=="
},
"request-promise": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.4.tgz",
"integrity": "sha512-8wgMrvE546PzbR5WbYxUQogUnUDfM0S7QIFZMID+J73vdFARkFy+HElj4T+MWYhpXwlLp0EQ8Zoj8xUA0he4Vg==",
"responselike": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz",
"integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==",
"requires": {
"bluebird": "^3.5.0",
"request-promise-core": "1.1.2",
"stealthy-require": "^1.1.1",
"tough-cookie": "^2.3.3"
}
},
"request-promise-core": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz",
"integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==",
"requires": {
"lodash": "^4.17.11"
"lowercase-keys": "^2.0.0"
}
},
"safe-buffer": {
@ -570,27 +486,6 @@
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"sshpk": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.0.tgz",
"integrity": "sha512-Zhev35/y7hRMcID/upReIvRse+I9SVhyVre/KTJSJQWMz3C3+G+HpO7m1wK/yckEtujKZ7dS4hkVxAnmHaIGVQ==",
"requires": {
"asn1": "~0.2.3",
"assert-plus": "^1.0.0",
"bcrypt-pbkdf": "^1.0.0",
"dashdash": "^1.12.0",
"ecc-jsbn": "~0.1.1",
"getpass": "^0.1.1",
"jsbn": "~0.1.0",
"safer-buffer": "^2.0.2",
"tweetnacl": "~0.14.0"
}
},
"stealthy-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks="
},
"string-to-stream": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string-to-stream/-/string-to-stream-1.1.1.tgz",
@ -634,67 +529,20 @@
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
},
"tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
"requires": {
"psl": "^1.1.28",
"punycode": "^2.1.1"
}
},
"tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
"requires": {
"safe-buffer": "^5.0.1"
}
},
"tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
},
"uri-js": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
"requires": {
"punycode": "^2.1.0"
},
"dependencies": {
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
}
}
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"uuid": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
},
"varint": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/varint/-/varint-5.0.0.tgz",
"integrity": "sha1-2Ca4n3SQcy+rwMDtaT7Uddyynr8="
},
"verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
"requires": {
"assert-plus": "^1.0.0",
"core-util-is": "1.0.2",
"extsprintf": "^1.2.0"
}
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"xmlbuilder": {
"version": "8.2.2",

View file

@ -7,33 +7,45 @@
"game",
"utility",
"util",
"server"
"server",
"gameserver",
"node",
"nodejs",
"game-server-query",
"game server query",
"server query",
"game server",
"gameserverquery",
"serverquery",
"terraria",
"counter strike",
"csgo",
"minecraft"
],
"main": "lib/index.js",
"author": "Michael Morrison",
"version": "2.0.20",
"author": "GameDig Contributors",
"version": "3.0.8",
"repository": {
"type": "git",
"url": "https://github.com/sonicsnes/node-gamedig.git"
"url": "https://github.com/gamedig/node-gamedig.git"
},
"bugs": {
"url": "https://github.com/sonicsnes/node-gamedig/issues"
"url": "https://github.com/gamedig/node-gamedig/issues"
},
"license": "MIT",
"engines": {
"node": ">=8.0.0"
"node": ">=12.0.0"
},
"dependencies": {
"cheerio": "^1.0.0-rc.3",
"compressjs": "^1.0.2",
"gbxremote": "^0.2.1",
"iconv-lite": "^0.5.0",
"got": "^11.5.1",
"iconv-lite": "^0.6.2",
"long": "^4.0.0",
"minimist": "^1.2.0",
"moment": "^2.24.0",
"minimist": "^1.2.5",
"moment": "^2.27.0",
"punycode": "^2.1.1",
"request": "^2.88.0",
"request-promise": "^4.2.4",
"varint": "^5.0.0"
},
"bin": {
@ -48,7 +60,7 @@
"README.md"
],
"devDependencies": {
"@types/cheerio": "^0.22.13",
"@types/node": "^8.10.54"
"@types/cheerio": "^0.22.21",
"@types/node": "^12.20.7"
}
}

View file

@ -3,12 +3,12 @@ const Core = require('./core');
class AssettoCorsa extends Core {
async run(state) {
const serverInfo = await this.request({
json: true,
uri: `http://${this.options.address}:${this.options.port}/INFO`
url: `http://${this.options.address}:${this.options.port}/INFO`,
responseType: 'json'
});
const carInfo = await this.request({
json: true,
uri: `http://${this.options.address}:${this.options.port}/JSON|${parseInt(Math.random() * 999999999999999, 10)}`
url: `http://${this.options.address}:${this.options.port}/JSON|${parseInt(Math.random() * 999999999999999, 10)}`,
responseType: 'json'
});
if (!serverInfo || !carInfo || !carInfo.Cars) {
@ -23,18 +23,17 @@ class AssettoCorsa extends Core {
state.raw.carInfo = carInfo.Cars;
state.raw.serverInfo = serverInfo;
state.players = carInfo.Cars.reduce((r, e) => {
if (e.IsConnected) {
r.push({
name: e.DriverName,
car: e.Model,
skin: e.Skin,
nation: e.DriverNation,
team: e.DriverTeam
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
});
}
return r;
}, state.players);
}
}
}

View file

@ -4,7 +4,7 @@ const Core = require('./core'),
class BuildAndShoot extends Core {
async run(state) {
const body = await this.request({
uri: 'http://'+this.options.address+':'+this.options.port+'/',
url: 'http://'+this.options.address+':'+this.options.port+'/',
});
let m;

View file

@ -2,10 +2,11 @@ const EventEmitter = require('events').EventEmitter,
net = require('net'),
Reader = require('../lib/reader'),
HexUtil = require('../lib/HexUtil'),
requestAsync = require('request-promise'),
got = require('got'),
Promises = require('../lib/Promises'),
Logger = require('../lib/Logger'),
DnsResolver = require('../lib/DnsResolver');
DnsResolver = require('../lib/DnsResolver'),
Results = require('../lib/Results');
let uid = 0;
@ -35,16 +36,17 @@ class Core extends EventEmitter {
}
this.logger.prefix = 'Q#' + (uid++);
this.logger.debug("Query is running with options:", this.options);
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
});
// Make sure that if this promise isn't attached to, it doesn't throw a unhandled promise rejection
this.abortedPromise.catch(() => {});
let timeout;
try {
const promise = this.runOnce();
@ -73,44 +75,13 @@ class Core extends EventEmitter {
if (resolved.port) options.port = resolved.port;
}
const state = {
name: '',
map: '',
password: false,
raw: {},
maxplayers: 0,
players: [],
bots: []
};
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 (typeof state.players === 'number') {
const num = state.players;
state.players = [];
state.raw.rcvNumPlayers = num;
if (num < 10000) {
for (let i = 0; i < num; i++) {
state.players.push({});
}
}
}
if (typeof state.bots === 'number') {
const num = state.bots;
state.bots = [];
state.raw.rcvNumBots = num;
if (num < 10000) {
for (let i = 0; i < num; i++) {
state.bots.push({});
}
}
}
if (!('connect' in state)) {
state.connect = ''
+ (state.gameHost || this.options.host || this.options.address)
@ -129,7 +100,7 @@ class Core extends EventEmitter {
return state;
}
async run(state) {}
async run(/** Results */ state) {}
/** Param can be a time in ms, or a promise (which will be timed) */
registerRtt(param) {
@ -175,7 +146,10 @@ class Core extends EventEmitter {
}
assertValidPort(port) {
if (!port || port < 1 || port > 65535) {
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);
}
}
@ -278,13 +252,9 @@ class Core extends EventEmitter {
this.assertValidPort(port);
if(typeof buffer === 'string') buffer = Buffer.from(buffer,'binary');
this.debugLog(log => {
log(address+':'+port+" UDP-->");
log(HexUtil.debugDump(buffer));
});
const socket = this.udpSocket;
socket.send(buffer, address, port);
await socket.send(buffer, address, port, this.options.debug);
if (!onPacket && !onTimeout) {
return null;
@ -342,24 +312,26 @@ class Core extends EventEmitter {
}
}
async request(params) {
// If we haven't opened a raw tcp socket yet during this query, just open one and then immediately close it.
// This will give us a much more accurate RTT than using the rtt of the http request.
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 = requestAsync({
requestPromise = got({
...params,
timeout: this.options.socketTimeout,
resolveWithFullResponse: true
timeout: this.options.socketTimeout
});
this.debugLog(log => {
log(() => params.uri + " HTTP-->");
log(() => params.url + " HTTP-->");
requestPromise
.then((response) => log(params.uri + " <--HTTP " + response.statusCode))
.then((response) => log(params.url + " <--HTTP " + response.statusCode))
.catch(() => {});
});
const wrappedPromise = requestPromise.then(response => {

31
protocols/discord.js Normal file
View file

@ -0,0 +1,31 @@
const Core = require('./core');
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;
}
}
module.exports = Discord;

View file

@ -10,7 +10,6 @@ class Ffow extends Valve {
this.debugLog("Requesting ffow info ...");
const b = await this.sendPacket(
0x46,
false,
'LSQ',
0x49
);

View file

@ -12,20 +12,19 @@ class FiveM extends Quake2 {
await super.run(state);
{
const raw = await this.request({
uri: 'http://' + this.options.address + ':' + this.options.port + '/info.json'
const json = await this.request({
url: 'http://' + this.options.address + ':' + this.options.port + '/info.json',
responseType: 'json'
});
const json = JSON.parse(raw);
state.raw.info = json;
}
{
const raw = await this.request({
uri: 'http://' + this.options.address + ':' + this.options.port + '/players.json'
const json = await this.request({
url: 'http://' + this.options.address + ':' + this.options.port + '/players.json',
responseType: 'json'
});
const json = JSON.parse(raw);
state.raw.players = json;
state.players = [];
for (const player of json) {
state.players.push({name: player.name, ping: player.ping});
}

View file

@ -1,5 +1,33 @@
const Core = require('./core');
const stringKeys = new Set([
'website',
'gametype',
'gamemode',
'player'
]);
function normalizeEntry([key,value]) {
key = key.toLowerCase();
const split = key.split('_');
let keyType;
if (split.length === 2 && !isNaN(parseInt(split[1]))) {
keyType = split[0];
} else {
keyType = key;
}
if (!stringKeys.has(keyType) && !keyType.includes('name')) {
if (value.toLowerCase() === 'true') {
value = true;
} else if (value.toLowerCase() === 'false') {
value = false;
} else if (!isNaN(parseInt(value))) {
value = parseInt(value);
}
}
return [key,value];
}
class Gamespy1 extends Core {
constructor() {
super();
@ -8,89 +36,80 @@ class Gamespy1 extends Core {
}
async run(state) {
{
const data = await this.sendPacket('info');
state.raw = data;
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);
}
{
const data = await this.sendPacket('rules');
state.raw.rules = data;
}
{
const data = await this.sendPacket('players');
const playersById = {};
const teamNamesById = {};
for (const ident of Object.keys(data)) {
const split = ident.split('_');
let key = split[0];
const id = split[1];
let value = data[ident];
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 = parseInt(data.maxplayers);
if ('hostport' in data) state.gamePort = parseInt(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 = parseInt(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 {
if (!(id in playersById)) playersById[id] = {};
if (key === 'playername') key = 'name';
else if (key === 'team') value = parseInt(value);
else if (key === 'score' || key === 'ping' || key === 'deaths' || key === 'kills') value = parseInt(value);
playersById[id][key] = value;
// 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(parseInt(value))) {
key = 'teamId';
value = parseInt(value) + (teamOffByOne ? -1 : 0);
}
if (key !== 'name' && !isNaN(parseInt(value))) {
value = parseInt(value);
}
playersById[id][key] = value;
}
state.raw.teams = teamNamesById;
}
state.raw.teams = teamNamesById;
const players = Object.values(playersById);
const players = Object.values(playersById);
// Determine which team id might be for spectators
let specTeamId = null;
for (const player of players) {
if (!player.team) {
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 if (teamNamesById[player.team]) {
continue;
} else if (teamNamesById[player.team-1] && (specTeamId === null || specTeamId === player.team)) {
specTeamId = player.team;
} else {
specTeamId = null;
break;
seenHashes.add(player.keyhash);
}
}
this.logger.debug(log => {
if (specTeamId === null) {
log("Could not detect a team ID for spectators");
// Convert player's team ID to team name if possible
if (player.hasOwnProperty('teamId')) {
if (Object.keys(teamNamesById).length) {
player.team = teamNamesById[player.teamId] || '';
} else {
log("Detected that team ID " + specTeamId + " is probably for spectators");
player.team = player.teamId;
delete player.teamId;
}
});
const seenHashes = new Set();
for (const player of players) {
// Some servers (bf1942) report the same player multiple times (bug?)
// Ignore these duplicates
if (player.keyhash) {
if (seenHashes.has(player.keyhash)) {
this.logger.debug("Rejected player with hash " + player.keyhash + " (Duplicate keyhash)");
continue;
} else {
seenHashes.add(player.keyhash);
}
}
// Convert player's team ID to team name if possible
if (player.team) {
if (teamNamesById[player.team]) {
player.team = teamNamesById[player.team];
} else if (player.team === specTeamId) {
player.team = "spec";
}
}
state.players.push(player);
}
state.players.push(player);
}
}
@ -100,7 +119,7 @@ class Gamespy1 extends Core {
const parts = new Set();
let maxPartNum = 0;
return await this.udpSend('\\'+type+'\\', buffer => {
return await this.udpSend(type, buffer => {
const reader = this.reader(buffer);
const str = reader.string(buffer.length);
const split = str.split('\\');

View file

@ -29,7 +29,9 @@ class Gamespy2 extends Core {
{
const body = await this.sendPacket([0, 0xff, 0]);
const reader = this.reader(body);
state.players = this.readFieldData(reader);
for (const rawPlayer of this.readFieldData(reader)) {
state.players.push(rawPlayer);
}
}
// Parse teams

View file

@ -148,9 +148,15 @@ class Gamespy3 extends Core {
return await this.udpSend(b,(buffer) => {
const reader = this.reader(buffer);
const iType = reader.uint(1);
if(iType !== type) return;
if(iType !== type) {
this.logger.debug('Skipping packet, type mismatch');
return;
}
const iSessionId = reader.uint(4);
if(iSessionId !== this.sessionId) return;
if(iSessionId !== this.sessionId) {
this.logger.debug('Skipping packet, session id mismatch');
return;
}
if(!assemble) {
return reader.rest();

View file

@ -2,8 +2,10 @@ const Core = require('./core');
class GeneShift extends Core {
async run(state) {
await this.tcpPing();
const body = await this.request({
uri: 'http://geneshift.net/game/receiveLobby.php'
url: 'http://geneshift.net/game/receiveLobby.php'
});
const split = body.split('<br/>');
@ -26,7 +28,7 @@ class GeneShift extends Core {
state.raw.country = found[1];
state.name = found[4];
state.map = found[5];
state.players = parseInt(found[6]);
state.players.setNum(parseInt(found[6]));
state.maxplayers = parseInt(found[7]);
// fields[8] is unknown?
state.raw.rules = found[9];

View file

@ -12,7 +12,7 @@ class Jc2mp extends Gamespy3 {
async run(state) {
await super.run(state);
if(!state.players.length && parseInt(state.raw.numplayers)) {
state.players = parseInt(state.raw.numplayers);
state.players.setNum(parseInt(state.raw.numplayers));
}
}
}

View file

@ -2,11 +2,11 @@ const Core = require('./core');
class Kspdmp extends Core {
async run(state) {
const body = await this.request({
uri: 'http://'+this.options.address+':'+this.options.port
const json = await this.request({
url: 'http://'+this.options.address+':'+this.options.port,
responseType: 'json'
});
const json = JSON.parse(body);
for (const one of json.players) {
state.players.push({name:one.nickname,team:one.team});
}

View file

@ -1,6 +1,16 @@
const Core = require('./core'),
MinecraftVanilla = require('./minecraftvanilla'),
Gamespy3 = require('./gamespy3');
const Core = require('./core');
const MinecraftVanilla = require('./minecraftvanilla');
const MinecraftBedrock = require('./minecraftbedrock');
const Gamespy3 = require('./gamespy3');
const Results = require('../lib/Results');
/*
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
*/
class Minecraft extends Core {
constructor() {
@ -8,6 +18,7 @@ class Minecraft extends Core {
this.srvRecord = "_minecraft._tcp";
}
async run(state) {
/** @type {Promise<Results>[]} */
const promises = [];
const vanillaResolver = new MinecraftVanilla();
@ -17,25 +28,40 @@ class Minecraft extends Core {
try { return await vanillaResolver.runOnceSafe(); } catch(e) {}
})());
const bedrockResolver = new Gamespy3();
bedrockResolver.options = {
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, bedrockState ] = await Promise.all(promises);
const [ vanillaState, gamespyState, bedrockState ] = await Promise.all(promises);
state.raw.vanilla = vanillaState;
state.raw.gamespy = gamespyState;
state.raw.bedrock = bedrockState;
if (!vanillaState && !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.name) state.name = bedrockState.name;
if (bedrockState.maxplayers) state.maxplayers = bedrockState.maxplayers;
if (bedrockState.players.length) state.players = bedrockState.players;
if (bedrockState.map) state.map = bedrockState.map;
}
if (vanillaState) {
try {
let name = '';
@ -52,12 +78,13 @@ class Minecraft extends Core {
state.name = name;
} catch(e) {}
if (vanillaState.maxplayers) state.maxplayers = vanillaState.maxplayers;
if (vanillaState.players) state.players = vanillaState.players;
if (vanillaState.players.length) state.players = vanillaState.players;
}
if (bedrockState) {
if (bedrockState.name) state.name = bedrockState.name;
if (bedrockState.maxplayers) state.maxplayers = bedrockState.maxplayers;
if (bedrockState.players) state.players = bedrockState.players;
if (gamespyState) {
if (gamespyState.name) state.name = gamespyState.name;
if (gamespyState.maxplayers) state.maxplayers = gamespyState.maxplayers;
if (gamespyState.players.length) state.players = gamespyState.players;
else if (gamespyState.raw.numplayers) state.players.setNum(parseInt(gamespyState.raw.numplayers));
}
// remove dupe spaces from name
state.name = state.name.replace(/\s+/g, ' ');

View file

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

View file

@ -47,6 +47,7 @@ class MinecraftVanilla extends Core {
state.raw = json;
state.maxplayers = json.players.max;
if(json.players.sample) {
for(const player of json.players.sample) {
state.players.push({
@ -55,7 +56,11 @@ class MinecraftVanilla extends Core {
});
}
}
for (let i = 0; i < Math.min(json.players.online, 10000); i++) {
// players.sample may not contain all players or no players at all, depending on how many players are online.
// Insert a dummy player object for every online player that is not listed in players.sample.
// Limit player amount to 10.000 players for performance reasons.
for (let i = state.players.length; i < Math.min(json.players.online, 10000); i++) {
state.players.push({});
}
}

View file

@ -17,7 +17,7 @@ class MumblePing extends Core {
state.raw.versionMinor = reader.uint(1);
state.raw.versionPatch = reader.uint(1);
reader.skip(8);
state.players = reader.uint(4);
state.players.setNum(reader.uint(4));
state.maxplayers = reader.uint(4);
state.raw.allowedbandwidth = reader.uint(4);
}

View file

@ -35,7 +35,7 @@ class OpenTtd extends Core {
state.password = !!reader.uint(1);
state.maxplayers = reader.uint(1);
state.players = reader.uint(1);
state.players.setNum(reader.uint(1));
state.raw.numspectators = reader.uint(1);
state.map = reader.string();
state.raw.map_width = reader.uint(2);
@ -60,9 +60,9 @@ class OpenTtd extends Core {
company.id = reader.uint(1);
company.name = reader.string();
company.year_start = reader.uint(4);
company.value = reader.uint(8);
company.money = reader.uint(8);
company.income = reader.uint(8);
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);

View file

@ -27,7 +27,7 @@ class Rfactor extends Core {
state.raw.ping = reader.uint(2);
state.raw.packedFlags = reader.uint(1);
state.raw.rate = reader.uint(1);
state.players = reader.uint(1);
state.players.setNum(reader.uint(1));
state.maxplayers = reader.uint(1);
state.raw.bots = reader.uint(1);
state.raw.packedSpecial = reader.uint(1);

View file

@ -69,7 +69,7 @@ class Samp extends Core {
}
}
if (!gotPlayerData) {
state.players = state.raw.numplayers;
state.players.setNum(state.raw.numplayers);
}
}
async sendPacket(type,allowTimeout) {

31
protocols/savage2.js Normal file
View file

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

View file

@ -1,16 +0,0 @@
const Valve = require('./valve');
class Squad extends Valve {
constructor() {
super();
}
async cleanup(state) {
await super.cleanup(state);
if (state.raw.rules != null && state.raw.rules.Password_b === "true") {
state.password = true;
}
}
}
module.exports = Squad;

View file

@ -16,7 +16,7 @@ class Starmade extends Core {
const reader = this.reader(buffer);
const packetLength = reader.uint(4);
this.logger.debug("Received packet length: " + packetLength);
const timestamp = reader.uint(8);
const timestamp = reader.uint(8).toString();
this.logger.debug("Received timestamp: " + timestamp);
if (reader.remaining() < packetLength || reader.remaining() < 5) return;
@ -61,7 +61,7 @@ class Starmade extends Core {
if(typeof data[2] === 'string') state.name = data[2];
if(typeof data[3] === 'string') state.raw.description = data[3];
if(typeof data[4] === 'number') state.raw.startTime = data[4];
if(typeof data[5] === 'number') state.players = data[5];
if(typeof data[5] === 'number') state.players.setNum(data[5]);
if(typeof data[6] === 'number') state.maxplayers = data[6];
}
}

View file

@ -2,15 +2,15 @@ const Core = require('./core');
class Terraria extends Core {
async run(state) {
const body = await this.request({
uri: 'http://'+this.options.address+':'+this.options.port+'/v2/server/status',
qs: {
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'
});
const json = JSON.parse(body);
if(json.status !== '200') throw new Error('Invalid status');
for (const one of json.players) {

View file

@ -1,5 +1,13 @@
const Bzip2 = require('compressjs').Bzip2,
Core = require('./core');
Core = require('./core'),
Results = require('../lib/Results');
const AppId = {
Squad: 393380,
Bat1944: 489940,
Ship: 2400,
DayZ: 221100
};
class Valve extends Core {
constructor() {
@ -34,11 +42,10 @@ class Valve extends Core {
await this.cleanup(state);
}
async queryInfo(state) {
async queryInfo(/** Results */ state) {
this.debugLog("Requesting info ...");
const b = await this.sendPacket(
0x54,
false,
'Source Engine Query\0',
this.goldsrcInfo ? 0x6D : 0x49,
false
@ -53,7 +60,7 @@ class Valve extends Core {
state.map = reader.string();
state.raw.folder = reader.string();
state.raw.game = reader.string();
state.raw.steamappid = reader.uint(2);
state.raw.appId = reader.uint(2);
state.raw.numplayers = reader.uint(1);
state.maxplayers = reader.uint(1);
@ -85,7 +92,7 @@ class Valve extends Core {
if(this.goldsrcInfo) {
state.raw.numbots = reader.uint(1);
} else {
if(state.raw.folder === 'ship') {
if(state.raw.appId === AppId.Ship) {
state.raw.shipmode = reader.uint(1);
state.raw.shipwitnesses = reader.uint(1);
state.raw.shipduration = reader.uint(1);
@ -93,30 +100,35 @@ class Valve extends Core {
state.raw.version = reader.string();
const extraFlag = reader.uint(1);
if(extraFlag & 0x80) state.gamePort = reader.uint(2);
if(extraFlag & 0x10) state.raw.steamid = reader.uint(8);
if(extraFlag & 0x10) state.raw.steamid = reader.uint(8).toString();
if(extraFlag & 0x40) {
state.raw.sourcetvport = reader.uint(2);
state.raw.sourcetvname = reader.string();
}
if(extraFlag & 0x20) state.raw.tags = reader.string();
if(extraFlag & 0x01) state.raw.gameid = reader.uint(8);
if(extraFlag & 0x20) state.raw.tags = reader.string().split(',');
if(extraFlag & 0x01) {
const gameId = reader.uint(8);
const betterAppId = gameId.getLowBitsUnsigned() & 0xffffff;
if (betterAppId) {
state.raw.appId = betterAppId;
}
}
}
// from https://developer.valvesoftware.com/wiki/Server_queries
if(
state.raw.protocol === 7 && (
state.raw.steamappid === 215
|| state.raw.steamappid === 17550
|| state.raw.steamappid === 17700
|| state.raw.steamappid === 240
state.raw.appId === 215
|| state.raw.appId === 17550
|| state.raw.appId === 17700
|| state.raw.appId === 240
)
) {
this._skipSizeInSplitHeader = true;
}
this.debugLog("STEAM APPID: "+state.raw.steamappid);
this.debugLog("PROTOCOL: "+state.raw.protocol);
this.logger.debug("INFO: ", state.raw);
if(state.raw.protocol === 48) {
this.debugLog("GOLDSRC DETECTED - USING MODIFIED SPLIT FORMAT");
this.logger.debug("GOLDSRC DETECTED - USING MODIFIED SPLIT FORMAT");
this.goldsrcSplits = true;
}
}
@ -128,7 +140,6 @@ class Valve extends Core {
this.debugLog("Requesting legacy challenge key ...");
await this.sendPacket(
0x57,
false,
null,
0x41,
false
@ -136,22 +147,24 @@ class Valve extends Core {
}
}
async queryPlayers(state) {
async queryPlayers(/** Results */ state) {
state.raw.players = [];
// CSGO doesn't even respond sometimes if host_players_show is not 2
// Ignore timeouts in only this case
const allowTimeout = state.raw.steamappid === 730;
this.debugLog("Requesting player list ...");
const b = await this.sendPacket(
0x55,
true,
null,
0x44,
allowTimeout
true
);
if (b === null) return; // timed out
if (b === null) {
// Player query timed out
// CSGO doesn't respond to player query if host_players_show is not 2
// Conan Exiles never responds to player query
// Just skip it, and we'll fill with dummy objects in cleanup()
return;
}
const reader = this.reader(b);
const num = reader.uint(1);
@ -175,42 +188,178 @@ class Valve extends Core {
}
}
async queryRules(state) {
state.raw.rules = {};
async queryRules(/** Results */ state) {
const appId = state.raw.appId;
if (appId === AppId.Squad
|| appId === AppId.Bat1944
|| this.options.requestRules) {
// let's get 'em
} else {
return;
}
const rules = {};
state.raw.rules = rules;
this.debugLog("Requesting rules ...");
const b = await this.sendPacket(0x56,true,null,0x45,true);
if (b === null) return; // timed out - the server probably just has rules disabled
const b = await this.sendPacket(0x56,null,0x45,true);
if (b === null) return; // timed out - the server probably has rules disabled
const dayZPayload = [];
let dayZPayloadEnded = false;
const reader = this.reader(b);
const num = reader.uint(2);
for(let i = 0; i < num; i++) {
if (appId === AppId.DayZ && !dayZPayloadEnded) {
const one = reader.uint(1);
const two = reader.uint(1);
const three = reader.uint(1);
if (one !== 0 && two !== 0 && three === 0) {
while (true) {
const byte = reader.uint(1);
if (byte === 0) break;
dayZPayload.push(byte);
}
continue;
} else {
reader.skip(-3);
dayZPayloadEnded = true;
}
}
const key = reader.string();
const value = reader.string();
state.raw.rules[key] = value;
rules[key] = value;
}
// Battalion 1944 puts its info into rules fields for some reason
if (appId === AppId.Bat1944) {
if ('bat_name_s' in rules) {
state.name = rules.bat_name_s;
delete rules.bat_name_s;
if ('bat_player_count_s' in rules) {
state.raw.numplayers = parseInt(rules.bat_player_count_s);
delete rules.bat_player_count_s;
}
if ('bat_max_players_i' in rules) {
state.maxplayers = parseInt(rules.bat_max_players_i);
delete rules.bat_max_players_i;
}
if ('bat_has_password_s' in rules) {
state.password = rules.bat_has_password_s === 'Y';
delete rules.bat_has_password_s;
}
// apparently map is already right, and this var is often wrong
delete rules.bat_map_s;
}
}
// Squad keeps its password in a separate field
if (appId === AppId.Squad) {
if (rules.Password_b === "true") {
state.password = true;
}
}
if (appId === AppId.DayZ) {
state.raw.dayzMods = this.readDayzMods(Buffer.from(dayZPayload));
if (state.raw.tags) {
for (const tag of state.raw.tags) {
if (tag.startsWith('lqs')) {
const value = parseInt(tag.replace('lqs', ''));
if (!isNaN(value)) {
state.raw.queue = value;
}
}
if (tag.startsWith('etm')) {
const value = parseInt(tag.replace('etm', ''));
if (!isNaN(value)) {
state.raw.dayAcceleration = value;
}
}
if (tag.startsWith('entm')) {
const value = parseInt(tag.replace('entm', ''));
if (!isNaN(value)) {
state.raw.nightAcceleration = value;
}
}
}
}
}
}
async cleanup(state) {
// Battalion 1944 puts its info into rules fields for some reason
if ('bat_name_s' in state.raw.rules) {
state.name = state.raw.rules.bat_name_s;
delete state.raw.rules.bat_name_s;
if ('bat_player_count_s' in state.raw.rules) {
state.raw.numplayers = parseInt(state.raw.rules.bat_player_count_s);
delete state.raw.rules.bat_player_count_s;
}
if ('bat_max_players_i' in state.raw.rules) {
state.maxplayers = parseInt(state.raw.rules.bat_max_players_i);
delete state.raw.rules.bat_max_players_i;
}
if ('bat_has_password_s' in state.raw.rules) {
state.password = state.raw.rules.bat_has_password_s === 'Y';
delete state.raw.rules.bat_has_password_s;
}
// apparently map is already right, and this var is often wrong
delete state.raw.rules.bat_map_s;
readDayzMods(/** Buffer */ buffer) {
if (!buffer.length) {
return {};
}
this.logger.debug("DAYZ BUFFER");
this.logger.debug(buffer);
const reader = this.reader(buffer);
const version = this.readDayzByte(reader);
const overflow = this.readDayzByte(reader);
const dlc1 = this.readDayzByte(reader);
const dlc2 = this.readDayzByte(reader);
this.logger.debug("version " + version);
this.logger.debug("overflow " + overflow);
this.logger.debug("dlc1 " + dlc1);
this.logger.debug("dlc2 " + dlc2);
const mods = [];
mods.push(...this.readDayzModsSection(reader, true));
mods.push(...this.readDayzModsSection(reader, false));
return mods;
}
readDayzModsSection(reader, withHeader) {
const out = [];
const count = this.readDayzByte(reader);
for(let i = 0; i < count; i++) {
const mod = {};
if (withHeader) {
const unknown = this.readDayzUint(reader, 4); // mod hash?
if (i !== count - 1) {
// For some reason this is 4 on all of them, but doesn't exist on the last one?
const flag = this.readDayzByte(reader);
//mod.flag = flag;
}
mod.workshopId = this.readDayzUint(reader, 4);
}
mod.title = this.readDayzString(reader);
out.push(mod);
}
return out;
}
readDayzUint(reader, bytes) {
const out = [];
for (let i = 0; i < bytes; i++) {
out.push(this.readDayzByte(reader));
}
const buf = Buffer.from(out);
const r2 = this.reader(buf);
return r2.uint(bytes);
}
readDayzByte(reader) {
const byte = reader.uint(1);
if (byte === 1) {
const byte2 = reader.uint(1);
if (byte2 === 1) return 1;
if (byte2 === 2) return 0;
if (byte2 === 3) return 0xff;
return 0; // ?
}
return byte;
}
readDayzString(reader) {
const length = this.readDayzByte(reader);
const out = [];
for (let i = 0; i < length; i++) {
out.push(this.readDayzByte(reader));
}
return Buffer.from(out).toString('utf8');
}
async cleanup(/** Results */ state) {
// Organize players / hidden players into player / bot arrays
const botProbability = (p) => {
if (p.time === -1) return Number.MAX_VALUE;
@ -243,33 +392,29 @@ class Valve extends Core {
**/
async sendPacket(
type,
sendChallenge,
payload,
expect,
allowTimeout
) {
for (let keyRetry = 0; keyRetry < 3; keyRetry++) {
let requestKeyChanged = false;
let receivedNewChallengeKey = false;
const response = await this.sendPacketRaw(
type, sendChallenge, payload,
type, payload,
(payload) => {
const reader = this.reader(payload);
const type = reader.uint(1);
this.debugLog(() => "Received " + type.toString(16) + " expected " + expect.toString(16));
this.debugLog(() => "Received 0x" + type.toString(16) + " expected 0x" + expect.toString(16));
if (type === 0x41) {
const key = reader.uint(4);
if (this._challenge !== key) {
this.debugLog('Received new challenge key: ' + key);
this.debugLog('Received new challenge key: 0x' + key.toString(16));
this._challenge = key;
if (sendChallenge) {
this.debugLog('Challenge key changed -- allowing query retry if needed');
requestKeyChanged = true;
}
receivedNewChallengeKey = true;
}
}
if (type === expect) {
return reader.rest();
} else if (requestKeyChanged) {
} else if (receivedNewChallengeKey) {
return null;
}
},
@ -277,7 +422,7 @@ class Valve extends Core {
if (allowTimeout) return null;
}
);
if (!requestKeyChanged) {
if (!receivedNewChallengeKey) {
return response;
}
}
@ -294,26 +439,47 @@ class Valve extends Core {
**/
async sendPacketRaw(
type,
sendChallenge,
payload,
onResponse,
onTimeout
) {
const challengeAtBeginning = type === 0x55 || type === 0x56;
const challengeAtEnd = type === 0x54 && !!this._challenge;
if (typeof payload === 'string') payload = Buffer.from(payload, 'binary');
const challengeLength = sendChallenge ? 4 : 0;
const payloadLength = payload ? payload.length : 0;
const b = Buffer.alloc(5 + challengeLength + payloadLength);
b.writeInt32LE(-1, 0);
b.writeUInt8(type, 4);
const b = Buffer.alloc(5
+ (challengeAtBeginning ? 4 : 0)
+ (challengeAtEnd ? 4 : 0)
+ (payload ? payload.length : 0)
);
let offset = 0;
if (sendChallenge) {
let challenge = this._challenge;
if (!challenge) challenge = 0xffffffff;
if (this.byteorder === 'le') b.writeUInt32LE(challenge, 5);
else b.writeUInt32BE(challenge, 5);
let challenge = this._challenge;
if (!challenge) challenge = 0xffffffff;
b.writeInt32LE(-1, offset);
offset += 4;
b.writeUInt8(type, offset);
offset += 1;
if (challengeAtBeginning) {
if (this.byteorder === 'le') b.writeUInt32LE(challenge, offset);
else b.writeUInt32BE(challenge, offset);
offset += 4;
}
if (payload) {
payload.copy(b, offset);
offset += payload.length;
}
if (challengeAtEnd) {
if (this.byteorder === 'le') b.writeUInt32LE(challenge, offset);
else b.writeUInt32BE(challenge, offset);
offset += 4;
}
if (payloadLength) payload.copy(b, 5 + challengeLength);
const packetStorage = {};
return await this.udpSend(
@ -351,7 +517,7 @@ class Valve extends Core {
packets[packetNum] = payload;
this.debugLog(() => "Received partial packet uid:"+uid+" num:"+packetNum);
this.debugLog(() => "Received partial packet uid: 0x"+uid.toString(16)+" num: "+packetNum);
this.debugLog(() => "Received "+Object.keys(packets).length+'/'+numPackets+" packets for this UID");
if(Object.keys(packets).length !== numPackets) return;