diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..64387f5 --- /dev/null +++ b/CHANGELOG.md @@ -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 [:]` +* 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 diff --git a/README.md b/README.md index bd58798..8407d27 100644 --- a/README.md +++ b/README.md @@ -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 -| 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`
`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`
`minecraftping` | Minecraft (2009) | `minecraftpe`
`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`
`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`
`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) @@ -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`). + +### 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 [:]` -* 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 diff --git a/bin/gamedig.js b/bin/gamedig.js old mode 100644 new mode 100755 index 720928e..4351b37 --- a/bin/gamedig.js +++ b/bin/gamedig.js @@ -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,' ')); diff --git a/games.txt b/games.txt index 2b55e19..aa0852d 100644 --- a/games.txt +++ b/games.txt @@ -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 diff --git a/lib/GameResolver.js b/lib/GameResolver.js index b75ad33..4856b9f 100644 --- a/lib/GameResolver.js +++ b/lib/GameResolver.js @@ -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('
'); 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; diff --git a/lib/GlobalUdpSocket.js b/lib/GlobalUdpSocket.js index 590ee19..99a6c13 100644 --- a/lib/GlobalUdpSocket.js +++ b/lib/GlobalUdpSocket.js @@ -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) { diff --git a/lib/QueryRunner.js b/lib/QueryRunner.js index 5919dee..47244b7 100644 --- a/lib/QueryRunner.js +++ b/lib/QueryRunner.js @@ -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; diff --git a/lib/Results.js b/lib/Results.js new file mode 100644 index 0000000..b2a9ae3 --- /dev/null +++ b/lib/Results.js @@ -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; diff --git a/lib/index.js b/lib/index.js index 4ded359..b1a1f3f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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) { diff --git a/lib/reader.js b/lib/reader.js index 5dbc2f2..0ab184e 100644 --- a/lib/reader.js +++ b/lib/reader.js @@ -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; diff --git a/package-lock.json b/package-lock.json index 4aae7ac..4a2b8dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 2b9915b..74ed642 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/protocols/assettocorsa.js b/protocols/assettocorsa.js index e3fe845..98d9fac 100644 --- a/protocols/assettocorsa.js +++ b/protocols/assettocorsa.js @@ -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,19 +23,18 @@ 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); + } } } -module.exports = AssettoCorsa; \ No newline at end of file +module.exports = AssettoCorsa; diff --git a/protocols/buildandshoot.js b/protocols/buildandshoot.js index 7598ae5..824eedc 100644 --- a/protocols/buildandshoot.js +++ b/protocols/buildandshoot.js @@ -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; diff --git a/protocols/core.js b/protocols/core.js index 9791970..a2c7465 100644 --- a/protocols/core.js +++ b/protocols/core.js @@ -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 => { diff --git a/protocols/discord.js b/protocols/discord.js new file mode 100644 index 0000000..739086e --- /dev/null +++ b/protocols/discord.js @@ -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; diff --git a/protocols/ffow.js b/protocols/ffow.js index ca82d55..5960beb 100644 --- a/protocols/ffow.js +++ b/protocols/ffow.js @@ -10,7 +10,6 @@ class Ffow extends Valve { this.debugLog("Requesting ffow info ..."); const b = await this.sendPacket( 0x46, - false, 'LSQ', 0x49 ); diff --git a/protocols/fivem.js b/protocols/fivem.js index b091ccc..8fc0288 100644 --- a/protocols/fivem.js +++ b/protocols/fivem.js @@ -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}); } diff --git a/protocols/gamespy1.js b/protocols/gamespy1.js index 3721152..0b04e9a 100644 --- a/protocols/gamespy1.js +++ b/protocols/gamespy1.js @@ -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('\\'); diff --git a/protocols/gamespy2.js b/protocols/gamespy2.js index 2936762..b913859 100644 --- a/protocols/gamespy2.js +++ b/protocols/gamespy2.js @@ -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 diff --git a/protocols/gamespy3.js b/protocols/gamespy3.js index 853c900..8372265 100644 --- a/protocols/gamespy3.js +++ b/protocols/gamespy3.js @@ -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(); diff --git a/protocols/geneshift.js b/protocols/geneshift.js index 891db5e..3c9c45c 100644 --- a/protocols/geneshift.js +++ b/protocols/geneshift.js @@ -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('
'); @@ -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]; diff --git a/protocols/jc2mp.js b/protocols/jc2mp.js index 996809c..05f060f 100644 --- a/protocols/jc2mp.js +++ b/protocols/jc2mp.js @@ -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)); } } } diff --git a/protocols/kspdmp.js b/protocols/kspdmp.js index 29e402b..9d107f8 100644 --- a/protocols/kspdmp.js +++ b/protocols/kspdmp.js @@ -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}); } diff --git a/protocols/minecraft.js b/protocols/minecraft.js index b21fefe..0ea1c1d 100644 --- a/protocols/minecraft.js +++ b/protocols/minecraft.js @@ -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[]} */ 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, ' '); diff --git a/protocols/minecraftbedrock.js b/protocols/minecraftbedrock.js new file mode 100644 index 0000000..3aa658e --- /dev/null +++ b/protocols/minecraftbedrock.js @@ -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; diff --git a/protocols/minecraftvanilla.js b/protocols/minecraftvanilla.js index c55294e..45dd6c5 100644 --- a/protocols/minecraftvanilla.js +++ b/protocols/minecraftvanilla.js @@ -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({}); } } diff --git a/protocols/mumbleping.js b/protocols/mumbleping.js index eca4a4c..b666178 100644 --- a/protocols/mumbleping.js +++ b/protocols/mumbleping.js @@ -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); } diff --git a/protocols/openttd.js b/protocols/openttd.js index 9bf336f..0da2d0d 100644 --- a/protocols/openttd.js +++ b/protocols/openttd.js @@ -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); diff --git a/protocols/rfactor.js b/protocols/rfactor.js index 1716a12..d48a559 100644 --- a/protocols/rfactor.js +++ b/protocols/rfactor.js @@ -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); diff --git a/protocols/samp.js b/protocols/samp.js index bc452aa..28ad970 100644 --- a/protocols/samp.js +++ b/protocols/samp.js @@ -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) { diff --git a/protocols/savage2.js b/protocols/savage2.js new file mode 100644 index 0000000..dfa7a5e --- /dev/null +++ b/protocols/savage2.js @@ -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; diff --git a/protocols/squad.js b/protocols/squad.js deleted file mode 100644 index 0be9e02..0000000 --- a/protocols/squad.js +++ /dev/null @@ -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; diff --git a/protocols/starmade.js b/protocols/starmade.js index 091fdf6..86567d3 100644 --- a/protocols/starmade.js +++ b/protocols/starmade.js @@ -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]; } } diff --git a/protocols/terraria.js b/protocols/terraria.js index 8daf47e..f9afaa0 100644 --- a/protocols/terraria.js +++ b/protocols/terraria.js @@ -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) { diff --git a/protocols/valve.js b/protocols/valve.js index d78ebfc..f82803c 100644 --- a/protocols/valve.js +++ b/protocols/valve.js @@ -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;