Merge remote-tracking branch 'origin/async'

This commit is contained in:
mmorrison 2019-01-12 22:43:54 -06:00
commit 075106c190
50 changed files with 2951 additions and 2622 deletions

121
README.md
View file

@ -14,7 +14,6 @@ Usage from Node.js
npm install gamedig npm install gamedig
``` ```
Promise:
```javascript ```javascript
const Gamedig = require('gamedig'); const Gamedig = require('gamedig');
Gamedig.query({ Gamedig.query({
@ -27,62 +26,41 @@ Gamedig.query({
}); });
``` ```
or Node.JS Callback:
```javascript
const Gamedig = require('gamedig');
Gamedig.query({
type: 'minecraft',
host: 'mc.example.com'
},
function(e,state) {
if(e) console.log("Server is offline");
else console.log(state);
});
```
> Is NPM out of date? If you're feeling lucky, you can install the latest code with
> ```shell
> npm install sonicsnes/node-gamedig
> ```
### Query Options ### Query Options
**Typical** **Typical**
* **type**: One of the game IDs listed in the game list below * **type**: string - One of the game IDs listed in the game list below
* **host**: Hostname or IP of the game server * **host**: string - Hostname or IP of the game server
* **port**: (optional) Uses the protocol default if not set * **port**: number (optional) - Connection port or query port for the game server. Some
games utilize a separate "query" port. If specifying the game port does not seem to work as expected, passing in
this query port may work instead. (defaults to protocol default port)
**Advanced** **Advanced**
* **notes**: (optional) An object passed through in the return value. * **maxAttempts**: number - Number of attempts to query server in case of failure. (default 1)
* **maxAttempts**: (optional) Number of attempts to query server in case of failure. (default 1) * **socketTimeout**: number - Milliseconds to wait for a single packet. Beware that increasing this
* **socketTimeout**: (optional) Milliseconds to wait for a single packet. Beware that increasing this
will cause many queries to take longer even if the server is online. (default 2000) will cause many queries to take longer even if the server is online. (default 2000)
* **attemptTimeout**: (optional) Milliseconds allowed for an entire query attempt. This timeout is not commonly hit, * **attemptTimeout**: number - Milliseconds allowed for an entire query attempt. This timeout is not commonly hit,
as the socketTimeout typically fires first. (default 10000) as the socketTimeout typically fires first. (default 10000)
* **debug**: boolean - Enables massive amounts of debug logging to stdout. (default false)
### Return Value ### Return Value
The returned state object will contain the following keys: The returned state object will contain the following keys:
**Stable, always present:** * **name**: string - Server name
* **map**: string - Current server game map
* **name** * **password**: boolean - If a password is required
* **map** * **maxplayers**: number
* **password**: Boolean * **players**: array of objects
* **maxplayers** * Each object **may or may not** contain name, ping, score, team, address.
* **players**: (array of objects) Each object **may** contain name, ping, score, team, address * The number of players online can be determined by `players.length`.
* **bots**: Same schema as players * For servers which do not provide player names, this may be an array
* **notes**: Passed through from the input of empty objects (ex. `[{},{},{}]`), one for each player without a name.
* **bots**: array of objects - Same schema as players
**Unstable, not guaranteed:** * **raw**: freeform object - Contains all information received from the server in a disorganized format. The content of this
field is unstable, and may change on a per-protocol basis between GameDig patch releases (although not typical).
* **raw**: Contains all information received from the server
* **query**: Details about the query performed
It can usually be assumed that the number of players online is equal to the length of the players array.
Some servers may return an additional player count number, which may be present in the unstable raw object.
Games List Games List
--- ---
@ -365,8 +343,6 @@ Games List
> __Know how to code?__ Protocols for most of the games above are documented > __Know how to code?__ Protocols for most of the games above are documented
> in the /reference folder, ready for you to develop into GameDig! > in the /reference folder, ready for you to develop into GameDig!
<!-- -->
> Don't see your game listed here? > Don't see your game listed here?
> >
> First, let us know so we can fix it. Then, you can try using some common query > First, let us know so we can fix it. Then, you can try using some common query
@ -392,7 +368,7 @@ have set the cvar: host_players_show 2
### DayZ ### DayZ
DayZ uses a query port that is separate from its main game port. The query port is usually DayZ uses a query port that is separate from its main game port. The query port is usually
the game port PLUS 24714 or 24715. You may need to pass this port in as the 'port_query' request option. the game port PLUS 24714 or 24715. You may need to pass this query port into GameDig instead.
### Mumble ### Mumble
For full query results from Mumble, you must be running the For full query results from Mumble, you must be running the
@ -424,7 +400,7 @@ additional option: token
Games with this note use a query port which is usually not the same as the game's connection port. Games with this note use a query port which is usually not the same as the game's connection port.
Usually, no action will be required from you. The 'port' option you pass GameDig should be the game's Usually, no action will be required from you. The 'port' option you pass GameDig should be the game's
connection port. GameDig will attempt to calculate the query port automatically. If the query still fails, connection port. GameDig will attempt to calculate the query port automatically. If the query still fails,
you may need to pass the 'port_query' option to GameDig as well, indicating the separate query port. you may need to find your server's query port, and pass that to GameDig instead.
Usage from Command Line Usage from Command Line
--- ---
@ -435,17 +411,56 @@ You'll still need npm to install gamedig:
npm install gamedig -g npm install gamedig -g
``` ```
After installing gamedig globally, you can call gamedig via the command line After installing gamedig globally, you can call gamedig via the command line:
using the same parameters mentioned in the API above:
```shell ```shell
gamedig --type minecraft --host mc.example.com --port 11234 gamedig --type minecraft mc.example.com:11234
``` ```
The output of the command will be in JSON format. The output of the command will be in JSON format. Additional advanced parameters can be passed in
as well: `--debug`, `--pretty`, `--socketTimeout 5000`, etc.
Major Version Changes Changelog
--- ---
### 2.0
##### Breaking API changes
* **Node 8 is now required**
* Removed the `port_query` option. You can now pass either the server's game port **or** query port in the `port` option, and
GameDig will automatically discover the proper port to query. Passing the query port is more likely be successful in
unusual cases, as otherwise it must be automatically derived from the game port.
* Removed `callback` parameter from Gamedig.query. Only promises are now supported. If you would like to continue
using callbacks, you can use node's `util.callbackify` function to convert the method to callback format.
* Removed `query` field from response object, as it was poorly documented and unstable.
* Removed `notes` field from options / response object. Data can be passed through a standard javascript context if needed.
##### Minor Changes
* Rewrote core to use promises extensively for better error-handling. Async chains have been dramatically simplified
by using async/await across the codebase, eliminating callback chains and the 'async' dependency.
* Replaced `--output pretty` cli parameter with `--pretty`.
* You can now query from CLI using shorthand syntax: `gamedig --type <gameid> <ip>[:<port>]`
* UDP socket is only opened if needed by a query.
* Automatic query port detection -- If provided with a non-standard port, gamedig will attempt to discover if it is a
game port or query port by querying twice: once to the port provided, and once to the port including the game's query
port offset (if available).
* Added new `connect` field to the response object. This will typically include the game's `ip:port` (the port will reflect the server's
game port, even if you passed in a query port in your request). For some games, this may be a server ID or connection url
if an IP:Port is not appropriate.
* Added new `ping` field (in milliseconds) to the response object. As icmp packets are often blocked by NATs, and node has poor support
for raw sockets, this time is derived from the rtt of one of the UDP requests, or the time required to open a TCP socket
during the query.
* Improved debug logging across all parts of GameDig
* Removed global `Gamedig.debug`. `debug` is now an option on each query.
##### Protocol Changes
* Added support for games using older versions of battlefield protocol.
* Simplified detection of BC2 when using battlefield protocol.
* Fixed buildandshoot not reading player list
* Standardized all doom3 games into a single protocol, which can discover protocol discrepancies automatically.
* Standardized all gamespy2 games into a single protocol, which can discover protocol discrepancies automatically.
* Standardized all gamespy3 games into a single protocol, which can discover protocol discrepancies automatically.
* Improved valve protocol challenge key retry process
### 1.0 ### 1.0
* First official release * First official release
* Node.js 6.0 is now required * Node.js 6 is now required

View file

@ -5,8 +5,8 @@ const argv = require('minimist')(process.argv.slice(2)),
const debug = argv.debug; const debug = argv.debug;
delete argv.debug; delete argv.debug;
const outputFormat = argv.output; const pretty = !!argv.pretty || debug;
delete argv.output; delete argv.pretty;
const options = {}; const options = {};
for(const key of Object.keys(argv)) { for(const key of Object.keys(argv)) {
@ -14,18 +14,26 @@ for(const key of Object.keys(argv)) {
if( if(
key === '_' key === '_'
|| key.charAt(0) === '$' || key.charAt(0) === '$'
|| (typeof value !== 'string' && typeof value !== 'number')
) )
continue; continue;
options[key] = value; options[key] = value;
} }
if(debug) Gamedig.debug = true; if (argv._.length >= 1) {
Gamedig.isCommandLine = true; const target = argv._[0];
const split = target.split(':');
options.host = split[0];
if (split.length >= 2) {
options.port = split[1];
}
}
if (debug) {
options.debug = true;
}
Gamedig.query(options) Gamedig.query(options)
.then((state) => { .then((state) => {
if(outputFormat === 'pretty') { if(pretty) {
console.log(JSON.stringify(state,null,' ')); console.log(JSON.stringify(state,null,' '));
} else { } else {
console.log(JSON.stringify(state)); console.log(JSON.stringify(state));
@ -42,7 +50,7 @@ Gamedig.query(options)
if (error instanceof Error) { if (error instanceof Error) {
error = error.message; error = error.message;
} }
if (outputFormat === 'pretty') { if (pretty) {
console.log(JSON.stringify({error: error}, null, ' ')); console.log(JSON.stringify({error: error}, null, ' '));
} else { } else {
console.log(JSON.stringify({error: error})); console.log(JSON.stringify({error: error}));

View file

@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
const fs = require('fs'), const fs = require('fs'),
TypeResolver = require('../lib/typeresolver'); TypeResolver = require('../lib/GameResolver');
const generated = TypeResolver.printReadme(); const generated = TypeResolver.printReadme();

125
games.txt
View file

@ -1,4 +1,4 @@
# id | pretty | protocol | options | parameters # id | pretty name for readme | protocol | options | extra
#### TODO: #### TODO:
# cube1|Cube 1|cube|port=28786,port_query_offset=1 # cube1|Cube 1|cube|port=28786,port_query_offset=1
@ -15,7 +15,6 @@
# gr|Ghost Recon|ghostrecon|port=2346,port_query_offset=2 # gr|Ghost Recon|ghostrecon|port=2346,port_query_offset=2
# gtr2|GTR2|gtr2|port=34297,port_query_offset=1 # gtr2|GTR2|gtr2|port=34297,port_query_offset=1
# haze|Haze|haze # haze|Haze|haze
# openttd|OpenTTD|openttd|port=3979
# plainsight|Plain Sight|plainsight # plainsight|Plain Sight|plainsight
# redfaction|Red Faction|redfaction|port_query=7755 # redfaction|Red Faction|redfaction|port_query=7755
# savage|Savage|savage|port_query=11235 # savage|Savage|savage|port_query=11235
@ -28,18 +27,18 @@
7d2d|7 Days to Die|valve|port=26900,port_query_offset=1 7d2d|7 Days to Die|valve|port=26900,port_query_offset=1
ageofchivalry|Age of Chivalry|valve ageofchivalry|Age of Chivalry|valve|port=27015
aoe2|Age of Empires 2|ase|port_query=27224 aoe2|Age of Empires 2|ase|port_query=27224
alienarena|Alien Arena|quake2|port_query=27910 alienarena|Alien Arena|quake2|port_query=27910
alienswarm|Alien Swarm|valve alienswarm|Alien Swarm|valve|port=27015
arkse|ARK: Survival Evolved|valve|port=7777,port_query=27015 arkse|ARK: Survival Evolved|valve|port=7777,port_query=27015
avp2|Aliens vs Predator 2|gamespy1|port=27888 avp2|Aliens vs Predator 2|gamespy1|port=27888
# avp2010 doesn't really... have a default port or query port # avp2010 doesn't really... have a default port or query port
# both port and port_query should be specified when used # both port and port_query should be specified when used
avp2010|Aliens vs Predator 2010|valve avp2010|Aliens vs Predator 2010|valve|port=27015
americasarmy|America's Army|americasarmy|port=1716,port_query_offset=1 americasarmy|America's Army|gamespy2|port=1716,port_query_offset=1
americasarmy2|America's Army 2|americasarmy|port=1716,port_query_offset=1 americasarmy2|America's Army 2|gamespy2|port=1716,port_query_offset=1
americasarmy3|America's Army 3|valve|port=8777,port_query=27020 americasarmy3|America's Army 3|valve|port=8777,port_query=27020
americasarmypg|America's Army: Proving Grounds|valve|port=8777,port_query=27020 americasarmypg|America's Army: Proving Grounds|valve|port=8777,port_query=27020
@ -53,9 +52,9 @@ bat1944|Battalion 1944|valve|port=7777,port_query_offset=3
bf1942|Battlefield 1942|gamespy1|port=14567,port_query=23000 bf1942|Battlefield 1942|gamespy1|port=14567,port_query=23000
bfv|Battlefield Vietnam|gamespy2|port=15567,port_query=23000 bfv|Battlefield Vietnam|gamespy2|port=15567,port_query=23000
bf2|Battlefield 2|gamespy3|port=16567,port_query=29900|noChallenge bf2|Battlefield 2|gamespy3|port=16567,port_query=29900
bf2142|Battlefield 2142|gamespy3|port=16567,port_query=29900 bf2142|Battlefield 2142|gamespy3|port=16567,port_query=29900
bfbc2|Battlefield: Bad Company 2|battlefield|port=19567,port_query=48888|isBadCompany2 bfbc2|Battlefield: Bad Company 2|battlefield|port=19567,port_query=48888
bf3|Battlefield 3|battlefield|port=25200,port_query_offset=22000 bf3|Battlefield 3|battlefield|port=25200,port_query_offset=22000
bf4|Battlefield 4|battlefield|port=25200,port_query_offset=22000 bf4|Battlefield 4|battlefield|port=25200,port_query_offset=22000
bfh|Battlefield Hardline|battlefield|port=25200,port_query_offset=22000 bfh|Battlefield Hardline|battlefield|port=25200,port_query_offset=22000
@ -63,7 +62,7 @@ bfh|Battlefield Hardline|battlefield|port=25200,port_query_offset=22000
breach|Breach|valve|port=27016 breach|Breach|valve|port=27016
breed|Breed|gamespy2|port=7649 breed|Breed|gamespy2|port=7649
brink|Brink|valve|port_query_offset=1 brink|Brink|valve|port_query_offset=1
buildandshoot|Build and Shoot|buildandshoot|port=32887,port_query=32886 buildandshoot|Build and Shoot|buildandshoot|port=32887,port_query_offset=-1
cod|Call of Duty|quake3|port=28960 cod|Call of Duty|quake3|port=28960
coduo|Call of Duty: United Offensive|quake3|port=28960 coduo|Call of Duty: United Offensive|quake3|port=28960
@ -83,10 +82,10 @@ cacrenegade|Command and Conquer: Renegade|gamespy1|port=4848,port_query=25300
conanexiles|Conan Exiles|valve|port=7777,port_query=27015 conanexiles|Conan Exiles|valve|port=7777,port_query=27015
contactjack|Contact J.A.C.K.|gamespy1|port_query=27888 contactjack|Contact J.A.C.K.|gamespy1|port_query=27888
cs16|Counter-Strike 1.6|valve cs16|Counter-Strike 1.6|valve|port=27015
cscz|Counter-Strike: Condition Zero|valve cscz|Counter-Strike: Condition Zero|valve|port=27015
css|Counter-Strike: Source|valve css|Counter-Strike: Source|valve|port=27015
csgo|Counter-Strike: Global Offensive|valve||doc_notes=csgo csgo|Counter-Strike: Global Offensive|valve||port=27015|doc_notes=csgo
crossracing|Cross Racing Championship|ase|port=12321,port_query_offset=123 crossracing|Cross Racing Championship|ase|port=12321,port_query_offset=123
@ -95,7 +94,7 @@ crysiswars|Crysis Wars|gamespy3|port=64100
crysis2|Crysis 2|gamespy3|port=64000 crysis2|Crysis 2|gamespy3|port=64000
daikatana|Daikatana|quake2|port=27982,port_query_offset=10 daikatana|Daikatana|quake2|port=27982,port_query_offset=10
dmomam|Dark Messiah of Might and Magic|valve dmomam|Dark Messiah of Might and Magic|valve|port=27015
darkesthour|Darkest Hour|unreal2|port=7757,port_query_offset=1 darkesthour|Darkest Hour|unreal2|port=7757,port_query_offset=1
dayz|DayZ|valve|port=2302,port_query_offset=24714|doc_notes=dayz dayz|DayZ|valve|port=2302,port_query_offset=24714|doc_notes=dayz
dayzmod|DayZ Mod|valve|port=2302,port_query_offset=1 dayzmod|DayZ Mod|valve|port=2302,port_query_offset=1
@ -104,61 +103,61 @@ dh2005|Deer Hunter 2005|gamespy2|port=23459,port_query=34567
descent3|Descent 3|gamespy1|port=2092,port_query=20142 descent3|Descent 3|gamespy1|port=2092,port_query=20142
deusex|Deus Ex|gamespy2|port=7791,port_query_offset=1 deusex|Deus Ex|gamespy2|port=7791,port_query_offset=1
devastation|Devastation|unreal2|port=7777,port_query_offset=1 devastation|Devastation|unreal2|port=7777,port_query_offset=1
dinodday|Dino D-Day|valve dinodday|Dino D-Day|valve|port=27015
dirttrackracing2|Dirt Track Racing 2|gamespy1|port=32240,port_query_offset=-100 dirttrackracing2|Dirt Track Racing 2|gamespy1|port=32240,port_query_offset=-100
dnl|Dark and Light|valve|port=7777,port_query=27015 dnl|Dark and Light|valve|port=7777,port_query=27015
dod|Day of Defeat|valve dod|Day of Defeat|valve|port=27015
dods|Day of Defeat: Source|valve dods|Day of Defeat: Source|valve|port=27015
doi|Day of Infamy|valve doi|Day of Infamy|valve|port=27015
doom3|Doom 3|doom3|port=27666 doom3|Doom 3|doom3|port=27666
dota2|DOTA 2|valve dota2|DOTA 2|valve|port=27015
drakan|Drakan|gamespy1|port=27045,port_query_offset=1 drakan|Drakan|gamespy1|port=27045,port_query_offset=1
etqw|Enemy Territory Quake Wars|doom3|port=3074,port_query=27733|isEtqw,hasSpaceBeforeClanTag,hasClanTag,hasTypeFlag etqw|Enemy Territory Quake Wars|doom3|port=3074,port_query=27733
fear|F.E.A.R.|gamespy2|port_query=27888 fear|F.E.A.R.|gamespy2|port_query=27888
f12002|F1 2002|gamespy1|port_query=3297 f12002|F1 2002|gamespy1|port_query=3297
f1c9902|F1 Challenge 99-02|gamespy1|port_query=34397 f1c9902|F1 Challenge 99-02|gamespy1|port_query=34397
farcry|Far Cry|ase|port=49001,port_query_offset=123 farcry|Far Cry|ase|port=49001,port_query_offset=123
farcry2|Far Cry|ase|port_query=14001 farcry2|Far Cry|ase|port_query=14001
fortressforever|Fortress Forever|valve fortressforever|Fortress Forever|valve|port=27015
flashpoint|Flashpoint|gamespy1|port=2302,port_query_offset=1 flashpoint|Flashpoint|gamespy1|port=2302,port_query_offset=1
ffow|Frontlines: Fuel of War|ffow|port=5476,port_query_offset=2 ffow|Frontlines: Fuel of War|ffow|port=5476,port_query_offset=2
fivem|FiveM|fivem|port=30120 fivem|FiveM|fivem|port=30120
garrysmod|Garry's Mod|valve garrysmod|Garry's Mod|valve|port=27015
graw|Ghost Recon: Advanced Warfighter|gamespy2|port_query=15250 graw|Ghost Recon: Advanced Warfighter|gamespy2|port_query=15250
graw2|Ghost Recon: Advanced Warfighter 2|gamespy2|port_query=16250 graw2|Ghost Recon: Advanced Warfighter 2|gamespy2|port_query=16250
giantscitizenkabuto|Giants: Citizen Kabuto|gamespy1|port_query=8911 giantscitizenkabuto|Giants: Citizen Kabuto|gamespy1|port_query=8911
globaloperations|Global Operations|gamespy1|port_query=28672 globaloperations|Global Operations|gamespy1|port_query=28672
geneshift|Geneshift|geneshift|port=11235 geneshift|Geneshift|geneshift|port=11235
ges|GoldenEye: Source|valve ges|GoldenEye: Source|valve|port=27015
gore|Gore|gamespy1|port=27777,port_query_offset=1 gore|Gore|gamespy1|port=27777,port_query_offset=1
gunmanchronicles|Gunman Chronicles|valve gunmanchronicles|Gunman Chronicles|valve|port=27015
hldm|Half-Life 1 Deathmatch|valve hldm|Half-Life 1 Deathmatch|valve|port=27015
hl2dm|Half-Life 2 Deathmatch|valve hl2dm|Half-Life 2 Deathmatch|valve|port=27015
halo|Halo|gamespy2|port=2302 halo|Halo|gamespy2|port=2302
halo2|Halo 2|gamespy2|port=2302 halo2|Halo 2|gamespy2|port=2302
heretic2|Heretic 2|gamespy1|port=27900,port_query_offset=1 heretic2|Heretic 2|gamespy1|port=27900,port_query_offset=1
hexen2|Hexen 2|hexen2|port=26900,port_query_offset=50 hexen2|Hexen 2|hexen2|port=26900,port_query_offset=50
hidden|The Hidden: Source|valve hidden|The Hidden: Source|valve|port=27015
had2|Hidden and Dangerous 2|gamespy1|port=11001,port_query_offset=3 had2|Hidden and Dangerous 2|gamespy1|port=11001,port_query_offset=3
homefront|Homefront|valve homefront|Homefront|valve|port=27015
homeworld2|Homeworld 2|gamespy1|port_query=6500 homeworld2|Homeworld 2|gamespy1|port_query=6500
hurtworld|Hurtworld|valve|port=12871,port_query=12881 hurtworld|Hurtworld|valve|port=12871,port_query=12881
igi2|IGI-2: Covert Strike|gamespy1|port_query=26001 igi2|IGI-2: Covert Strike|gamespy1|port_query=26001
il2|IL-2 Sturmovik|gamespy1|port_query=21000 il2|IL-2 Sturmovik|gamespy1|port_query=21000
insurgency|Insurgency|valve insurgency|Insurgency|valve|port=27015
ironstorm|Iron Storm|gamespy1|port_query=3505 ironstorm|Iron Storm|gamespy1|port_query=3505
jamesbondnightfire|James Bond: Nightfire|gamespy1|port_query=6550 jamesbondnightfire|James Bond: Nightfire|gamespy1|port_query=6550
jc2mp|Just Cause 2 Multiplayer|jc2mp|port=7777|isJc2mp jc2mp|Just Cause 2 Multiplayer|jc2mp|port=7777
killingfloor|Killing Floor|killingfloor|port=7707,port_query_offset=1 killingfloor|Killing Floor|killingfloor|port=7707,port_query_offset=1
killingfloor2|Killing Floor 2|valve|port=7777,port_query=27015 killingfloor2|Killing Floor 2|valve|port=7777,port_query=27015
kingpin|Kingpin: Life of Crime|gamespy1|port=31510,port_query_offset=-10 kingpin|Kingpin: Life of Crime|gamespy1|port=31510,port_query_offset=-10
kisspc|KISS Psycho Circus|gamespy1|port=7777,port_query_offset=1 kisspc|KISS Psycho Circus|gamespy1|port=7777,port_query_offset=1
kspdmp|DMP - KSP Multiplayer|kspdmp|port=6702,port_query_offset=1 kspdmp|DMP - KSP Multiplayer|kspdmp|port=6702,port_query_offset=1
kzmod|KzMod|valve kzmod|KzMod|valve|port=27015
left4dead|Left 4 Dead|valve left4dead|Left 4 Dead|valve|port=27015
left4dead2|Left 4 Dead 2|valve left4dead2|Left 4 Dead 2|valve|port=27015
m2mp|Mafia 2 Multiplayer|m2mp|port=27016,port_query_offset=1 m2mp|Mafia 2 Multiplayer|m2mp|port=27016,port_query_offset=1
medievalengineers|Medieval Engineers|valve medievalengineers|Medieval Engineers|valve|port=27015
mohaa|Medal of Honor: Allied Assault|gamespy1|port=12203,port_query_offset=97 mohaa|Medal of Honor: Allied Assault|gamespy1|port=12203,port_query_offset=97
mohpa|Medal of Honor: Pacific Assault|gamespy1|port=13203,port_query_offset=97 mohpa|Medal of Honor: Pacific Assault|gamespy1|port=13203,port_query_offset=97
@ -168,9 +167,9 @@ mohbt|Medal of Honor: Breakthrough|gamespy1|port=12203,port_query_offset=97
moh2010|Medal of Honor 2010|battlefield|port=7673,port_query=48888 moh2010|Medal of Honor 2010|battlefield|port=7673,port_query=48888
mohwf|Medal of Honor: Warfighter|battlefield|port=25200,port_query_offset=22000 mohwf|Medal of Honor: Warfighter|battlefield|port=25200,port_query_offset=22000
minecraft|Minecraft|minecraft|port=25565|srvRecord=_minecraft._tcp,doc_notes=minecraft minecraft|Minecraft|minecraft|port=25565|doc_notes=minecraft
# Legacy name # Legacy name
minecraftping||minecraft|port=25565|srvRecord=_minecraft._tcp,doc_notes=minecraft minecraftping||minecraft|port=25565|doc_notes=minecraft
minecraftpe|Minecraft: Pocket Edition|gamespy3|port=19132,maxAttempts=2 minecraftpe|Minecraft: Pocket Edition|gamespy3|port=19132,maxAttempts=2
mnc|Monday Night Combat|valve|port=7777,port_query=27016 mnc|Monday Night Combat|valve|port=7777,port_query=27016
@ -180,9 +179,9 @@ mumble|Mumble|mumble|port=64738,port_query=27800|doc_notes=mumble
mumbleping|Mumble|mumbleping|port=64738|doc_notes=mumble mumbleping|Mumble|mumbleping|port=64738|doc_notes=mumble
mutantfactions|Mutant Factions|geneshift|port=11235 mutantfactions|Mutant Factions|geneshift|port=11235
nascarthunder2004|Nascar Thunder 2004|gamespy2|port_query=13333 nascarthunder2004|Nascar Thunder 2004|gamespy2|port_query=13333
netpanzer|netPanzer|gamespy1|3030 netpanzer|netPanzer|gamespy1|port=3030
nmrih|No More Room in Hell|valve nmrih|No More Room in Hell|valve|port=27015
ns|Natural Selection|valve ns|Natural Selection|valve|port=27015
ns2|Natural Selection 2|valve|port_query_offset=1 ns2|Natural Selection 2|valve|port_query_offset=1
nfshp2|Need for Speed: Hot Pursuit 2|gamespy1|port_query=61220 nfshp2|Need for Speed: Hot Pursuit 2|gamespy1|port_query=61220
nab|Nerf Arena Blast|gamespy1|port=4444,port_query_offset=1 nab|Nerf Arena Blast|gamespy1|port=4444,port_query_offset=1
@ -192,21 +191,21 @@ nexuiz|Nexuiz|quake3|port_query=26000
nitrofamily|Nitro Family|gamespy1|port_query=25601 nitrofamily|Nitro Family|gamespy1|port_query=25601
nolf|No One Lives Forever|gamespy1|port_query=27888 nolf|No One Lives Forever|gamespy1|port_query=27888
nolf2|No One Lives Forever 2|gamespy1|port_query=27890 nolf2|No One Lives Forever 2|gamespy1|port_query=27890
nucleardawn|Nuclear Dawn|valve nucleardawn|Nuclear Dawn|valve|port=27015
openarena|OpenArena|quake3|port_query=27960 openarena|OpenArena|quake3|port_query=27960
openttd|OpenTTD|openttd|port=3979 openttd|OpenTTD|openttd|port=3979
operationflashpoint|Operation Flashpoint|gamespy1|port=2234,port_query_offset=1 operationflashpoint|Operation Flashpoint|gamespy1|port=2234,port_query_offset=1
painkiller|Painkiller|ase|port=3455,port_query_offset=123 painkiller|Painkiller|ase|port=3455,port_query_offset=123
postal2|Postal 2|gamespy1|port=7777,port_query_offset=1 postal2|Postal 2|gamespy1|port=7777,port_query_offset=1
prey|Prey|doom3|port_query=27719 prey|Prey|doom3|port=27719
primalcarnage|Primal Carnage: Extinction|valve|port=7777,port_query=27015 primalcarnage|Primal Carnage: Extinction|valve|port=7777,port_query=27015
quake1|Quake 1: QuakeWorld|quake1|port=27500 quake1|Quake 1: QuakeWorld|quake1|port=27500
quake2|Quake 2|quake2|port=27910 quake2|Quake 2|quake2|port=27910
quake3|Quake 3: Arena|quake3|port=27960 quake3|Quake 3: Arena|quake3|port=27960
quake4|Quake 4|doom3|port=28004|hasClanTag quake4|Quake 4|doom3|port=28004
ragdollkungfu|Rag Doll Kung Fu|valve ragdollkungfu|Rag Doll Kung Fu|valve|port=27015
r6|Rainbow Six|gamespy1|port_query=2348 r6|Rainbow Six|gamespy1|port_query=2348
r6roguespear|Rainbow Six 2: Rogue Spear|gamespy1|port_query=2346 r6roguespear|Rainbow Six 2: Rogue Spear|gamespy1|port_query=2346
@ -219,20 +218,20 @@ redorchestraost|Red Orchestra: Ostfront 41-45|gamespy1|port=7757,port_query_offs
redorchestra2|Red Orchestra 2|valve|port=7777,port_query=27015 redorchestra2|Red Orchestra 2|valve|port=7777,port_query=27015
redline|Redline|gamespy1|port_query=25252 redline|Redline|gamespy1|port_query=25252
rtcw|Return to Castle Wolfenstein|quake3|port_query=27960 rtcw|Return to Castle Wolfenstein|quake3|port_query=27960
ricochet|Ricochet|valve ricochet|Ricochet|valve|port=27015
riseofnations|Rise of Nations|gamespy1|port_query=6501 riseofnations|Rise of Nations|gamespy1|port_query=6501
rune|Rune|gamespy1|port=7777,port_query_offset=1 rune|Rune|gamespy1|port=7777,port_query_offset=1
rust|Rust|valve|port=28015 rust|Rust|valve|port=28015
samp|San Andreas Multiplayer|samp|port=7777 samp|San Andreas Multiplayer|samp|port=7777
spaceengineers|Space Engineers|valve spaceengineers|Space Engineers|valve|port=27015
ss|Serious Sam|gamespy1|port=25600,port_query_offset=1 ss|Serious Sam|gamespy1|port=25600,port_query_offset=1
ss2|Serious Sam 2|gamespy2|port=25600 ss2|Serious Sam 2|gamespy2|port=25600
shatteredhorizon|Shattered Horizon|valve shatteredhorizon|Shattered Horizon|valve|port=27015
ship|The Ship|valve ship|The Ship|valve|port=27015
shogo|Shogo|gamespy1|port_query=27888 shogo|Shogo|gamespy1|port_query=27888
shootmania|Shootmania|nadeo||doc_notes=nadeo-shootmania--trackmania--etc shootmania|Shootmania|nadeo|port=2350,port_query=5000|doc_notes=nadeo-shootmania--trackmania--etc
sin|SiN|gamespy1|port_query=22450 sin|SiN|gamespy1|port_query=22450
sinep|SiN Episodes|valve sinep|SiN Episodes|valve|port=27015
soldat|Soldat|ase|port=13073,port_query_offset=123 soldat|Soldat|ase|port=13073,port_query_offset=123
sof|Soldier of Fortune|quake1|port_query=28910 sof|Soldier of Fortune|quake1|port_query=28910
sof2|Soldier of Fortune 2|quake3|port_query=20100 sof2|Soldier of Fortune 2|quake3|port_query=20100
@ -250,24 +249,24 @@ swrc|Star Wars: Republic Commando|gamespy2|port=7777,port_query=11138
starbound|Starbound|valve|port=21025 starbound|Starbound|valve|port=21025
starmade|StarMade|starmade|port=4242 starmade|StarMade|starmade|port=4242
suicidesurvival|Suicide Survival|valve suicidesurvival|Suicide Survival|valve|port=27015
swat4|SWAT 4|gamespy2|port=10480,port_query_offset=2 swat4|SWAT 4|gamespy2|port=10480,port_query_offset=2
svencoop|Sven Coop|valve svencoop|Sven Coop|valve|port=27015
synergy|Synergy|valve synergy|Synergy|valve|port=27015
tacticalops|Tactical Ops|gamespy1|port=7777,port_query_offset=1 tacticalops|Tactical Ops|gamespy1|port=7777,port_query_offset=1
teamfactor|Team Factor|gamespy1|port_query=57778 teamfactor|Team Factor|gamespy1|port_query=57778
tfc|Team Fortress Classic|valve tfc|Team Fortress Classic|valve|port=27015
tf2|Team Fortress 2|valve tf2|Team Fortress 2|valve|port=27015
teamspeak2|Teamspeak 2|teamspeak2|port=8767,port_query=51234 teamspeak2|Teamspeak 2|teamspeak2|port=8767
teamspeak3|Teamspeak 3|teamspeak3|port=9987,port_query=10011|doc_notes=teamspeak3 teamspeak3|Teamspeak 3|teamspeak3|port=9987|doc_notes=teamspeak3
terminus|Terminus|gamespy1|port_query=12286 terminus|Terminus|gamespy1|port_query=12286
terraria|Terraria|terraria|port=7777,port_query_offset=101|doc_notes=terraria terraria|Terraria|terraria|port=7777,port_query_offset=101|doc_notes=terraria
thps3|Tony Hawk's Pro Skater 3|gamespy1|port_query=6500 thps3|Tony Hawk's Pro Skater 3|gamespy1|port_query=6500
thps4|Tony Hawk's Pro Skater 4|gamespy1|port_query=6500 thps4|Tony Hawk's Pro Skater 4|gamespy1|port_query=6500
thu2|Tony Hawk's Underground 2|gamespy1|port_query=5153 thu2|Tony Hawk's Underground 2|gamespy1|port_query=5153
towerunite|Tower Unite|valve towerunite|Tower Unite|valve|port=27015
trackmania2|Trackmania 2|nadeo||doc_notes=nadeo-shootmania--trackmania--etc trackmania2|Trackmania 2|nadeo|port=2350,port_query=5000|doc_notes=nadeo-shootmania--trackmania--etc
trackmaniaforever|Trackmania Forever|nadeo||doc_notes=nadeo-shootmania--trackmania--etc trackmaniaforever|Trackmania Forever|nadeo|port=2350,port_query=5000|doc_notes=nadeo-shootmania--trackmania--etc
tremulous|Tremulous|quake3|port_query=30720 tremulous|Tremulous|quake3|port_query=30720
tribes1|Tribes 1: Starsiege|tribes1|port=28001 tribes1|Tribes 1: Starsiege|tribes1|port=28001
tribesvengeance|Tribes: Vengeance|gamespy2|port=7777,port_query_offset=1 tribesvengeance|Tribes: Vengeance|gamespy2|port=7777,port_query_offset=1
@ -289,8 +288,8 @@ vietcong|Vietcong|gamespy1|port=5425,port_query=15425
vietcong2|Vietcong 2|gamespy2|port=5001,port_query=19967 vietcong2|Vietcong 2|gamespy2|port=5001,port_query=19967
warsow|Warsow|warsow|port=44400 warsow|Warsow|warsow|port=44400
wheeloftime|Wheel of Time|gamespy1|port=7777,port_query_offset=1 wheeloftime|Wheel of Time|gamespy1|port=7777,port_query_offset=1
wolfenstein2009|Wolfenstein 2009|doom3|port_query=27666|hasSpaceBeforeClanTag,hasClanTag,hasTypeFlag wolfenstein2009|Wolfenstein 2009|doom3|port=27666
wolfensteinet|Wolfenstein: Enemy Territory|quake3|port_query=27960 wolfensteinet|Wolfenstein: Enemy Territory|quake3|port_query=27960
xpandrally|Xpand Rally|ase|port=28015,port_query_offset=123 xpandrally|Xpand Rally|ase|port=28015,port_query_offset=123
zombiemaster|Zombie Master|valve zombiemaster|Zombie Master|valve|port=27015
zps|Zombie Panic: Source|valve zps|Zombie Panic: Source|valve|port=27015

84
lib/GameResolver.js Normal file
View file

@ -0,0 +1,84 @@
const Path = require('path'),
fs = require('fs');
class GameResolver {
constructor() {
this.games = this._readGames();
}
lookup(type) {
if(!type) throw Error('No game specified');
if(type.substr(0,9) === 'protocol-') {
return {
protocol: type.substr(9)
};
}
const game = this.games.get(type);
if(!game) throw Error('Invalid game: '+type);
return game.options;
}
printReadme() {
let out = '';
for(const key of Object.keys(games)) {
const game = games[key];
if (!game.pretty) {
continue;
}
out += "* "+game.pretty+" ("+key+")";
if(game.options.port_query_offset || game.options.port_query)
out += " [[Separate Query Port](#separate-query-port)]";
if(game.extra.doc_notes)
out += " [[Additional Notes](#"+game.extra.doc_notes+")]";
out += "\n";
}
return out;
}
_readGames() {
const gamesFile = Path.normalize(__dirname+'/../games.txt');
const lines = fs.readFileSync(gamesFile,'utf8').split('\n');
const games = new Map();
for (let line of lines) {
// strip comments
const comment = line.indexOf('#');
if(comment !== -1) line = line.substr(0,comment);
line = line.trim();
if(!line) continue;
const split = line.split('|');
const gameId = split[0].trim();
const options = this._parseList(split[3]);
options.protocol = split[2].trim();
games.set(gameId, {
pretty: split[1].trim(),
options: options,
extra: this._parseList(split[4])
});
}
return games;
}
_parseList(str) {
if(!str) return {};
const out = {};
for (const one of str.split(',')) {
const equals = one.indexOf('=');
const key = equals === -1 ? one : one.substr(0,equals);
let value = equals === -1 ? '' : one.substr(equals+1);
if(value === 'true' || value === '') value = true;
else if(value === 'false') value = false;
else if(!isNaN(parseInt(value))) value = parseInt(value);
out[key] = value;
}
return out;
}
}
module.exports = GameResolver;

52
lib/GlobalUdpSocket.js Normal file
View file

@ -0,0 +1,52 @@
const dgram = require('dgram'),
HexUtil = require('./HexUtil');
class GlobalUdpSocket {
constructor() {
this.socket = null;
this.callbacks = new Set();
this.debuggingCallbacks = new Set();
}
_getSocket() {
if (!this.socket) {
const udpSocket = this.socket = dgram.createSocket('udp4');
udpSocket.unref();
udpSocket.bind();
udpSocket.on('message', (buffer, rinfo) => {
const fromAddress = rinfo.address;
const fromPort = rinfo.port;
if (this.debuggingCallbacks.size) {
console.log(fromAddress + ':' + fromPort + " <--UDP");
console.log(HexUtil.debugDump(buffer));
}
for (const cb of this.callbacks) {
cb(fromAddress, fromPort, buffer);
}
});
udpSocket.on('error', (e) => {
if (this.debuggingCallbacks.size) {
console.log("UDP ERROR: " + e);
}
});
}
return this.socket;
}
send(buffer, address, port) {
this._getSocket().send(buffer,0,buffer.length,port,address);
}
addCallback(callback, debug) {
this.callbacks.add(callback);
if (debug) {
this.debuggingCallbacks.add(callback);
}
}
removeCallback(callback) {
this.callbacks.delete(callback);
this.debuggingCallbacks.delete(callback);
}
}
module.exports = GlobalUdpSocket;

20
lib/Promises.js Normal file
View file

@ -0,0 +1,20 @@
class Promises {
static createTimeout(timeoutMs, timeoutMsg) {
let cancel = null;
const wrapped = new Promise((res, rej) => {
const timeout = setTimeout(
() => {
rej(new Error(timeoutMsg + " - Timed out after " + timeoutMs + "ms"));
},
timeoutMs
);
cancel = () => {
clearTimeout(timeout);
};
});
wrapped.cancel = cancel;
return wrapped;
}
}
module.exports = Promises;

22
lib/ProtocolResolver.js Normal file
View file

@ -0,0 +1,22 @@
const Path = require('path'),
fs = require('fs'),
Core = require('../protocols/core');
class ProtocolResolver {
constructor() {
this.protocolDir = Path.normalize(__dirname+'/../protocols');
}
/**
* @returns Core
*/
create(protocolId) {
protocolId = Path.basename(protocolId);
const path = this.protocolDir+'/'+protocolId;
if(!fs.existsSync(path+'.js')) throw Error('Protocol definition file missing: '+type);
const protocol = require(path);
return new protocol();
}
}
module.exports = ProtocolResolver;

104
lib/QueryRunner.js Normal file
View file

@ -0,0 +1,104 @@
const GameResolver = require('./GameResolver'),
ProtocolResolver = require('./ProtocolResolver'),
GlobalUdpSocket = require('./GlobalUdpSocket');
const defaultOptions = {
socketTimeout: 2000,
attemptTimeout: 10000,
maxAttempts: 1
};
class QueryRunner {
constructor() {
this.udpSocket = new GlobalUdpSocket();
this.gameResolver = new GameResolver();
this.protocolResolver = new ProtocolResolver();
}
async run(userOptions) {
for (const key of Object.keys(userOptions)) {
const value = userOptions[key];
if (['port'].includes(key)) {
userOptions[key] = parseInt(value);
}
}
const {
port_query: gameQueryPort,
port_query_offset: gameQueryPortOffset,
...gameOptions
} = this.gameResolver.lookup(userOptions.type);
const attempts = [];
if (userOptions.port) {
if (gameQueryPortOffset) {
attempts.push({
...defaultOptions,
...gameOptions,
...userOptions,
port: userOptions.port + gameQueryPortOffset
});
}
if (userOptions.port === gameOptions.port && gameQueryPort) {
attempts.push({
...defaultOptions,
...gameOptions,
...userOptions,
port: gameQueryPort
});
}
attempts.push({
...defaultOptions,
...gameOptions,
...userOptions
});
} else if (gameQueryPort) {
attempts.push({
...defaultOptions,
...gameOptions,
...userOptions,
port: gameQueryPort
});
} else if (gameOptions.port) {
attempts.push({
...defaultOptions,
...gameOptions,
...userOptions,
port: gameOptions.port + (gameQueryPortOffset || 0)
});
} else {
throw new Error("Could not determine port to query. Did you provide a port or gameid?");
}
if (attempts.length === 1) {
return await this._attempt(attempts[0]);
} else {
const errors = [];
for (const attempt of attempts) {
try {
return await this._attempt(attempt);
} catch(e) {
const e2 = new Error('Failed to query port ' + attempt.port);
e2.stack += "\nCaused by:\n" + e.stack;
errors.push(e2);
}
}
const err = new Error('Failed all port attempts');
err.stack = errors.map(e => e.stack).join('\n');
throw err;
}
}
async _attempt(options) {
if (options.debug) {
console.log("Running attempt with options:");
console.log(options);
}
const core = this.protocolResolver.create(options.protocol);
core.options = options;
core.udpSocket = this.udpSocket;
return await core.runAllAttempts();
}
}
module.exports = QueryRunner;

View file

@ -1,107 +1,23 @@
const dgram = require('dgram'), const QueryRunner = require('./QueryRunner');
TypeResolver = require('./typeresolver'),
HexUtil = require('./HexUtil');
const activeQueries = []; let singleton = null;
const udpSocket = dgram.createSocket('udp4');
udpSocket.unref();
udpSocket.bind();
udpSocket.on('message', (buffer, rinfo) => {
if(Gamedig.debug) {
console.log(rinfo.address+':'+rinfo.port+" <--UDP");
console.log(HexUtil.debugDump(buffer));
}
for(const query of activeQueries) {
if(
query.options.address !== rinfo.address
&& query.options.altaddress !== rinfo.address
) continue;
if(query.options.port_query !== rinfo.port) continue;
query._udpResponse(buffer);
break;
}
});
udpSocket.on('error', (e) => {
if(Gamedig.debug) console.log("UDP ERROR: "+e);
});
class Gamedig { class Gamedig {
constructor() {
static query(options,callback) { this.queryRunner = new QueryRunner();
const promise = new Promise((resolve,reject) => {
for (const key of Object.keys(options)) {
if (['port_query', 'port'].includes(key)) {
options[key] = parseInt(options[key]);
}
} }
options.callback = (state) => { async query(userOptions) {
if (state.error) reject(state.error); return await this.queryRunner.run(userOptions);
else resolve(state);
};
let query;
try {
query = TypeResolver.lookup(options.type);
} catch(e) {
process.nextTick(() => {
options.callback({error:e});
});
return;
}
query.debug = Gamedig.debug;
query.udpSocket = udpSocket;
query.type = options.type;
if(!('port' in query.options) && ('port_query' in query.options)) {
if(Gamedig.isCommandLine) {
process.stderr.write(
"Warning! This game is so old, that we don't know"
+" what the server's connection port is. We've guessed that"
+" the query port for "+query.type+" is "+query.options.port_query+"."
+" If you know the connection port for this type of server, please let"
+" us know on the GameDig issue tracker, thanks!\n"
);
}
query.options.port = query.options.port_query;
delete query.options.port_query;
} }
// copy over options static getInstance() {
for(const key of Object.keys(options)) { if (!singleton) singleton = new Gamedig();
query.options[key] = options[key]; return singleton;
} }
static async query(...args) {
activeQueries.push(query); return await Gamedig.getInstance().query(...args);
query.on('finished',() => {
const i = activeQueries.indexOf(query);
if(i >= 0) activeQueries.splice(i, 1);
});
process.nextTick(() => {
query.start();
});
});
if (callback && callback instanceof Function) {
if(callback.length === 2) {
promise
.then((state) => callback(null,state))
.catch((error) => callback(error));
} else if (callback.length === 1) {
promise
.then((state) => callback(state))
.catch((error) => callback({error:error}));
} }
} }
return promise;
}
}
Gamedig.debug = false;
Gamedig.isCommandLine = false;
module.exports = Gamedig; module.exports = Gamedig;

View file

@ -1,7 +1,8 @@
const Iconv = require('iconv-lite'), const Iconv = require('iconv-lite'),
Long = require('long'), Long = require('long'),
Core = require('../protocols/core'), Core = require('../protocols/core'),
Buffer = require('buffer'); Buffer = require('buffer'),
Varint = require('varint');
function readUInt64BE(buffer,offset) { function readUInt64BE(buffer,offset) {
const high = buffer.readUInt32BE(offset); const high = buffer.readUInt32BE(offset);
@ -126,6 +127,12 @@ class Reader {
return r; return r;
} }
varint() {
const out = Varint.decode(this.buffer, this.i);
this.i += Varint.decode.bytes;
return out;
}
/** @returns Buffer */ /** @returns Buffer */
part(bytes) { part(bytes) {
let r; let r;

View file

@ -1,97 +0,0 @@
const Path = require('path'),
fs = require('fs');
const protocolDir = Path.normalize(__dirname+'/../protocols');
const gamesFile = Path.normalize(__dirname+'/../games.txt');
function parseList(str) {
if(!str) return {};
const out = {};
for (const one of str.split(',')) {
const equals = one.indexOf('=');
const key = equals === -1 ? one : one.substr(0,equals);
let value = equals === -1 ? '' : one.substr(equals+1);
if(value === 'true' || value === '') value = true;
else if(value === 'false') value = false;
else if(!isNaN(parseInt(value))) value = parseInt(value);
out[key] = value;
}
return out;
}
function readGames() {
const lines = fs.readFileSync(gamesFile,'utf8').split('\n');
const games = {};
for (let line of lines) {
// strip comments
const comment = line.indexOf('#');
if(comment !== -1) line = line.substr(0,comment);
line = line.trim();
if(!line) continue;
const split = line.split('|');
games[split[0].trim()] = {
pretty: split[1].trim(),
protocol: split[2].trim(),
options: parseList(split[3]),
params: parseList(split[4])
};
}
return games;
}
const games = readGames();
function createProtocolInstance(type) {
type = Path.basename(type);
const path = protocolDir+'/'+type;
if(!fs.existsSync(path+'.js')) throw Error('Protocol definition file missing: '+type);
const protocol = require(path);
return new protocol();
}
class TypeResolver {
static lookup(type) {
if(!type) throw Error('No game specified');
if(type.substr(0,9) === 'protocol-') {
return createProtocolInstance(type.substr(9));
}
const game = games[type];
if(!game) throw Error('Invalid game: '+type);
const query = createProtocolInstance(game.protocol);
query.pretty = game.pretty;
for(const key of Object.keys(game.options)) {
query.options[key] = game.options[key];
}
for(const key of Object.keys(game.params)) {
query[key] = game.params[key];
}
return query;
}
static printReadme() {
let out = '';
for(const key of Object.keys(games)) {
const game = games[key];
if (!game.pretty) {
continue;
}
out += "* "+game.pretty+" ("+key+")";
if(game.options.port_query_offset || game.options.port_query)
out += " [[Separate Query Port](#separate-query-port)]";
if(game.params.doc_notes)
out += " [[Additional Notes](#"+game.params.doc_notes+")]";
out += "\n";
}
return out;
}
}
module.exports = TypeResolver;

524
package-lock.json generated
View file

@ -1,18 +1,23 @@
{ {
"name": "gamedig", "name": "gamedig",
"version": "1.0.41", "version": "1.0.49",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@types/node": {
"version": "10.12.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.18.tgz",
"integrity": "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ=="
},
"ajv": { "ajv": {
"version": "5.5.2", "version": "6.6.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.2.tgz",
"integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", "integrity": "sha512-FBHEW6Jf5TB9MGBgUUA9XHkTbjXYfAUjY43ACMfmdMRHniyoMHjHjzD50OK8LGDWQwp4rWEsIq5kEqq7rvIM1g==",
"requires": { "requires": {
"co": "4.6.0", "fast-deep-equal": "^2.0.1",
"fast-deep-equal": "1.1.0", "fast-json-stable-stringify": "^2.0.0",
"fast-json-stable-stringify": "2.0.0", "json-schema-traverse": "^0.4.1",
"json-schema-traverse": "0.3.1" "uri-js": "^4.2.2"
} }
}, },
"amdefine": { "amdefine": {
@ -21,20 +26,18 @@
"integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU="
}, },
"asn1": { "asn1": {
"version": "0.2.3", "version": "0.2.4",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
"integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
"requires": {
"safer-buffer": "~2.1.0"
}
}, },
"assert-plus": { "assert-plus": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
}, },
"async": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz",
"integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0="
},
"asynckit": { "asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@ -46,51 +49,60 @@
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
}, },
"aws4": { "aws4": {
"version": "1.6.0", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
"integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
}, },
"barse": { "barse": {
"version": "0.4.3", "version": "0.4.3",
"resolved": "https://registry.npmjs.org/barse/-/barse-0.4.3.tgz", "resolved": "https://registry.npmjs.org/barse/-/barse-0.4.3.tgz",
"integrity": "sha1-KJhk15XQECu7sYHmbs0IxUobwMs=", "integrity": "sha1-KJhk15XQECu7sYHmbs0IxUobwMs=",
"requires": { "requires": {
"readable-stream": "1.0.34" "readable-stream": "~1.0.2"
} }
}, },
"bcrypt-pbkdf": { "bcrypt-pbkdf": {
"version": "1.0.1", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
"optional": true,
"requires": { "requires": {
"tweetnacl": "0.14.5" "tweetnacl": "^0.14.3"
} }
}, },
"boom": { "bluebird": {
"version": "4.3.1", "version": "3.5.3",
"resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz",
"integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw=="
"requires": { },
"hoek": "4.2.1" "boolbase": {
} "version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
}, },
"caseless": { "caseless": {
"version": "0.12.0", "version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
}, },
"co": { "cheerio": {
"version": "4.6.0", "version": "1.0.0-rc.2",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.2.tgz",
"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" "integrity": "sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs=",
"requires": {
"css-select": "~1.2.0",
"dom-serializer": "~0.1.0",
"entities": "~1.1.1",
"htmlparser2": "^3.9.1",
"lodash": "^4.15.0",
"parse5": "^3.0.1"
}
}, },
"combined-stream": { "combined-stream": {
"version": "1.0.6", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
"integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
"requires": { "requires": {
"delayed-stream": "1.0.0" "delayed-stream": "~1.0.0"
} }
}, },
"commander": { "commander": {
@ -98,7 +110,7 @@
"resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz",
"integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=",
"requires": { "requires": {
"graceful-readlink": "1.0.1" "graceful-readlink": ">= 1.0.0"
} }
}, },
"compressjs": { "compressjs": {
@ -106,8 +118,8 @@
"resolved": "https://registry.npmjs.org/compressjs/-/compressjs-1.0.3.tgz", "resolved": "https://registry.npmjs.org/compressjs/-/compressjs-1.0.3.tgz",
"integrity": "sha1-ldt03VuQOM+AvKMhqw7eJxtJWbY=", "integrity": "sha1-ldt03VuQOM+AvKMhqw7eJxtJWbY=",
"requires": { "requires": {
"amdefine": "1.0.1", "amdefine": "~1.0.0",
"commander": "2.8.1" "commander": "~2.8.1"
} }
}, },
"core-util-is": { "core-util-is": {
@ -115,30 +127,28 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
}, },
"cryptiles": { "css-select": {
"version": "3.1.2", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
"integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
"requires": { "requires": {
"boom": "5.2.0" "boolbase": "~1.0.0",
"css-what": "2.1",
"domutils": "1.5.1",
"nth-check": "~1.0.1"
}
}, },
"dependencies": { "css-what": {
"boom": { "version": "2.1.2",
"version": "5.2.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.2.tgz",
"resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", "integrity": "sha512-wan8dMWQ0GUeF7DGEPVjhHemVW/vy6xUYmFzRY8RYqgA0JtXC9rJmbScBjqSu6dg9q0lwPQy6ZAmJVr3PPTvqQ=="
"integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==",
"requires": {
"hoek": "4.2.1"
}
}
}
}, },
"dashdash": { "dashdash": {
"version": "1.14.1", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
"requires": { "requires": {
"assert-plus": "1.0.0" "assert-plus": "^1.0.0"
} }
}, },
"delayed-stream": { "delayed-stream": {
@ -146,19 +156,62 @@
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
}, },
"ecc-jsbn": { "dom-serializer": {
"version": "0.1.1", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
"integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=",
"optional": true,
"requires": { "requires": {
"jsbn": "0.1.1" "domelementtype": "~1.1.1",
"entities": "~1.1.1"
},
"dependencies": {
"domelementtype": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz",
"integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs="
}
} }
}, },
"domelementtype": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
"integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w=="
},
"domhandler": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
"integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
"requires": {
"domelementtype": "1"
}
},
"domutils": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
"integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=",
"requires": {
"dom-serializer": "0",
"domelementtype": "1"
}
},
"ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
"integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
"requires": {
"jsbn": "~0.1.0",
"safer-buffer": "^2.1.0"
}
},
"entities": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w=="
},
"extend": { "extend": {
"version": "3.0.1", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
}, },
"extsprintf": { "extsprintf": {
"version": "1.3.0", "version": "1.3.0",
@ -166,9 +219,9 @@
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
}, },
"fast-deep-equal": { "fast-deep-equal": {
"version": "1.1.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
"integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
}, },
"fast-json-stable-stringify": { "fast-json-stable-stringify": {
"version": "2.0.0", "version": "2.0.0",
@ -181,13 +234,13 @@
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
}, },
"form-data": { "form-data": {
"version": "2.3.2", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
"integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
"requires": { "requires": {
"asynckit": "0.4.0", "asynckit": "^0.4.0",
"combined-stream": "1.0.6", "combined-stream": "^1.0.6",
"mime-types": "2.1.18" "mime-types": "^2.1.12"
} }
}, },
"gbxremote": { "gbxremote": {
@ -195,8 +248,8 @@
"resolved": "https://registry.npmjs.org/gbxremote/-/gbxremote-0.1.4.tgz", "resolved": "https://registry.npmjs.org/gbxremote/-/gbxremote-0.1.4.tgz",
"integrity": "sha1-x+0iWC5WBRtOF2AbPdWjAE7u/UM=", "integrity": "sha1-x+0iWC5WBRtOF2AbPdWjAE7u/UM=",
"requires": { "requires": {
"barse": "0.4.3", "barse": "~0.4.2",
"sax": "0.4.3", "sax": "0.4.x",
"xmlbuilder": "0.3.1" "xmlbuilder": "0.3.1"
} }
}, },
@ -205,7 +258,7 @@
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
"integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
"requires": { "requires": {
"assert-plus": "1.0.0" "assert-plus": "^1.0.0"
} }
}, },
"graceful-readlink": { "graceful-readlink": {
@ -219,38 +272,55 @@
"integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
}, },
"har-validator": { "har-validator": {
"version": "5.0.3", "version": "5.1.3",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
"integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
"requires": { "requires": {
"ajv": "5.5.2", "ajv": "^6.5.5",
"har-schema": "2.0.0" "har-schema": "^2.0.0"
} }
}, },
"hawk": { "htmlparser2": {
"version": "6.0.2", "version": "3.10.0",
"resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.0.tgz",
"integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", "integrity": "sha512-J1nEUGv+MkXS0weHNWVKJJ+UrLfePxRWpN3C9bEi9fLxL2+ggW94DQvgYVXsaT30PGwYRIZKNZXuyMhp3Di4bQ==",
"requires": { "requires": {
"boom": "4.3.1", "domelementtype": "^1.3.0",
"cryptiles": "3.1.2", "domhandler": "^2.3.0",
"hoek": "4.2.1", "domutils": "^1.5.1",
"sntp": "2.1.0" "entities": "^1.1.1",
"inherits": "^2.0.1",
"readable-stream": "^3.0.6"
},
"dependencies": {
"readable-stream": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.1.1.tgz",
"integrity": "sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==",
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
} }
}, },
"hoek": { "string_decoder": {
"version": "4.2.1", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz",
"integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==",
"requires": {
"safe-buffer": "~5.1.0"
}
}
}
}, },
"http-signature": { "http-signature": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
"integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
"requires": { "requires": {
"assert-plus": "1.0.0", "assert-plus": "^1.0.0",
"jsprim": "1.4.1", "jsprim": "^1.2.2",
"sshpk": "1.14.1" "sshpk": "^1.7.0"
} }
}, },
"iconv-lite": { "iconv-lite": {
@ -263,6 +333,11 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
}, },
"ip-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-3.0.0.tgz",
"integrity": "sha512-T8wDtjy+Qf2TAPDQmBp0eGKJ8GavlWlUnamr3wRn6vvdZlKVuJXXMlSncYFRYgVHOM3If5NR1H4+OvVQU9Idvg=="
},
"is-typedarray": { "is-typedarray": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
@ -281,8 +356,7 @@
"jsbn": { "jsbn": {
"version": "0.1.1", "version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
"optional": true
}, },
"json-schema": { "json-schema": {
"version": "0.2.3", "version": "0.2.3",
@ -290,9 +364,9 @@
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
}, },
"json-schema-traverse": { "json-schema-traverse": {
"version": "0.3.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
}, },
"json-stringify-safe": { "json-stringify-safe": {
"version": "5.0.1", "version": "5.0.1",
@ -310,22 +384,27 @@
"verror": "1.10.0" "verror": "1.10.0"
} }
}, },
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
},
"long": { "long": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/long/-/long-2.4.0.tgz", "resolved": "https://registry.npmjs.org/long/-/long-2.4.0.tgz",
"integrity": "sha1-n6GAux2VAM3CnEFWdmoZleH0Uk8=" "integrity": "sha1-n6GAux2VAM3CnEFWdmoZleH0Uk8="
}, },
"mime-db": { "mime-db": {
"version": "1.33.0", "version": "1.37.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz",
"integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg=="
}, },
"mime-types": { "mime-types": {
"version": "2.1.18", "version": "2.1.21",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz",
"integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==",
"requires": { "requires": {
"mime-db": "1.33.0" "mime-db": "~1.37.0"
} }
}, },
"minimist": { "minimist": {
@ -338,115 +417,171 @@
"resolved": "https://registry.npmjs.org/moment/-/moment-2.21.0.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.21.0.tgz",
"integrity": "sha512-TCZ36BjURTeFTM/CwRcViQlfkMvL1/vFISuNLO5GkcVm1+QHfbSiNqZuWeMFjj1/3+uAjXswgRk30j1kkLYJBQ==" "integrity": "sha512-TCZ36BjURTeFTM/CwRcViQlfkMvL1/vFISuNLO5GkcVm1+QHfbSiNqZuWeMFjj1/3+uAjXswgRk30j1kkLYJBQ=="
}, },
"nth-check": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz",
"integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==",
"requires": {
"boolbase": "~1.0.0"
}
},
"oauth-sign": { "oauth-sign": {
"version": "0.8.2", "version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
"integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
},
"parse5": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz",
"integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==",
"requires": {
"@types/node": "*"
}
}, },
"performance-now": { "performance-now": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
}, },
"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=="
},
"punycode": { "punycode": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
}, },
"qs": { "qs": {
"version": "6.5.1", "version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
"integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
}, },
"readable-stream": { "readable-stream": {
"version": "1.0.34", "version": "1.0.34",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
"integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
"requires": { "requires": {
"core-util-is": "1.0.2", "core-util-is": "~1.0.0",
"inherits": "2.0.3", "inherits": "~2.0.1",
"isarray": "0.0.1", "isarray": "0.0.1",
"string_decoder": "0.10.31" "string_decoder": "~0.10.x"
} }
}, },
"request": { "request": {
"version": "2.85.0", "version": "2.88.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.85.0.tgz", "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
"integrity": "sha512-8H7Ehijd4js+s6wuVPLjwORxD4zeuyjYugprdOXlPSqaApmL/QOy+EB/beICHVCHkGMKNh5rvihb5ov+IDw4mg==", "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
"requires": { "requires": {
"aws-sign2": "0.7.0", "aws-sign2": "~0.7.0",
"aws4": "1.6.0", "aws4": "^1.8.0",
"caseless": "0.12.0", "caseless": "~0.12.0",
"combined-stream": "1.0.6", "combined-stream": "~1.0.6",
"extend": "3.0.1", "extend": "~3.0.2",
"forever-agent": "0.6.1", "forever-agent": "~0.6.1",
"form-data": "2.3.2", "form-data": "~2.3.2",
"har-validator": "5.0.3", "har-validator": "~5.1.0",
"hawk": "6.0.2", "http-signature": "~1.2.0",
"http-signature": "1.2.0", "is-typedarray": "~1.0.0",
"is-typedarray": "1.0.0", "isstream": "~0.1.2",
"isstream": "0.1.2", "json-stringify-safe": "~5.0.1",
"json-stringify-safe": "5.0.1", "mime-types": "~2.1.19",
"mime-types": "2.1.18", "oauth-sign": "~0.9.0",
"oauth-sign": "0.8.2", "performance-now": "^2.1.0",
"performance-now": "2.1.0", "qs": "~6.5.2",
"qs": "6.5.1", "safe-buffer": "^5.1.2",
"safe-buffer": "5.1.1", "tough-cookie": "~2.4.3",
"stringstream": "0.0.5", "tunnel-agent": "^0.6.0",
"tough-cookie": "2.3.4", "uuid": "^3.3.2"
"tunnel-agent": "0.6.0", },
"uuid": "3.2.1" "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"
}
}
}
},
"request-promise": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.2.tgz",
"integrity": "sha1-0epG1lSm7k+O5qT+oQGMIpEZBLQ=",
"requires": {
"bluebird": "^3.5.0",
"request-promise-core": "1.1.1",
"stealthy-require": "^1.1.0",
"tough-cookie": ">=2.3.3"
}
},
"request-promise-core": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz",
"integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=",
"requires": {
"lodash": "^4.13.1"
} }
}, },
"safe-buffer": { "safe-buffer": {
"version": "5.1.1", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
}, },
"sax": { "sax": {
"version": "0.4.3", "version": "0.4.3",
"resolved": "https://registry.npmjs.org/sax/-/sax-0.4.3.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-0.4.3.tgz",
"integrity": "sha1-cA46NOsueSzjgHkccSgPNzGWXdw=" "integrity": "sha1-cA46NOsueSzjgHkccSgPNzGWXdw="
}, },
"sntp": { "sshpk": {
"version": "2.1.0", "version": "1.16.0",
"resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.0.tgz",
"integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", "integrity": "sha512-Zhev35/y7hRMcID/upReIvRse+I9SVhyVre/KTJSJQWMz3C3+G+HpO7m1wK/yckEtujKZ7dS4hkVxAnmHaIGVQ==",
"requires": { "requires": {
"hoek": "4.2.1" "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"
} }
}, },
"sshpk": { "stealthy-require": {
"version": "1.14.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
"integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks="
"requires": {
"asn1": "0.2.3",
"assert-plus": "1.0.0",
"bcrypt-pbkdf": "1.0.1",
"dashdash": "1.14.1",
"ecc-jsbn": "0.1.1",
"getpass": "0.1.7",
"jsbn": "0.1.1",
"tweetnacl": "0.14.5"
}
}, },
"string_decoder": { "string_decoder": {
"version": "0.10.31", "version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
}, },
"stringstream": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",
"integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg="
},
"tough-cookie": { "tough-cookie": {
"version": "2.3.4", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.0.tgz",
"integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", "integrity": "sha512-LHMvg+RBP/mAVNqVbOX8t+iJ+tqhBA/t49DuI7+IDAWHrASnesqSu1vWbKB7UrE2yk+HMFUBMadRGMkB4VCfog==",
"requires": { "requires": {
"punycode": "1.4.1" "ip-regex": "^3.0.0",
"psl": "^1.1.28",
"punycode": "^2.1.1"
},
"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=="
}
} }
}, },
"tunnel-agent": { "tunnel-agent": {
@ -454,19 +589,38 @@
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
"requires": { "requires": {
"safe-buffer": "5.1.1" "safe-buffer": "^5.0.1"
} }
}, },
"tweetnacl": { "tweetnacl": {
"version": "0.14.5", "version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
"optional": true },
"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": { "uuid": {
"version": "3.2.1", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
"integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
}, },
"varint": { "varint": {
"version": "4.0.1", "version": "4.0.1",
@ -478,9 +632,9 @@
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
"requires": { "requires": {
"assert-plus": "1.0.0", "assert-plus": "^1.0.0",
"core-util-is": "1.0.2", "core-util-is": "1.0.2",
"extsprintf": "1.3.0" "extsprintf": "^1.2.0"
} }
}, },
"xmlbuilder": { "xmlbuilder": {

View file

@ -11,7 +11,7 @@
], ],
"main": "lib/index.js", "main": "lib/index.js",
"author": "Michael Morrison", "author": "Michael Morrison",
"version": "1.0.49", "version": "2.0",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/sonicsnes/node-gamedig.git" "url": "https://github.com/sonicsnes/node-gamedig.git"
@ -21,17 +21,18 @@
}, },
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=8.0.0"
}, },
"dependencies": { "dependencies": {
"async": "^0.9.2", "cheerio": "^1.0.0-rc.2",
"compressjs": "^1.0.2", "compressjs": "^1.0.2",
"gbxremote": "^0.1.4", "gbxremote": "^0.1.4",
"iconv-lite": "^0.4.18", "iconv-lite": "^0.4.18",
"long": "^2.4.0", "long": "^2.4.0",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"moment": "^2.21.0", "moment": "^2.21.0",
"request": "^2.85.0", "request": "^2.88.0",
"request-promise": "^4.2.2",
"varint": "^4.0.1" "varint": "^4.0.1"
}, },
"bin": { "bin": {

View file

@ -1,25 +0,0 @@
const Gamespy2 = require('./gamespy2');
class AmericasArmy extends Gamespy2 {
finalizeState(state) {
super.finalizeState(state);
state.name = this.stripColor(state.name);
state.map = this.stripColor(state.map);
for(const key of Object.keys(state.raw)) {
if(typeof state.raw[key] === 'string') {
state.raw[key] = this.stripColor(state.raw[key]);
}
}
for(const player of state.players) {
if(!('name' in player)) continue;
player.name = this.stripColor(player.name);
}
}
stripColor(str) {
// uses unreal 2 color codes
return str.replace(/\x1b...|[\x00-\x1a]/g,'');
}
}
module.exports = AmericasArmy;

View file

@ -7,15 +7,15 @@ class Armagetron extends Core {
this.byteorder = 'be'; this.byteorder = 'be';
} }
run(state) { async run(state) {
const b = Buffer.from([0,0x35,0,0,0,0,0,0x11]); const b = Buffer.from([0,0x35,0,0,0,0,0,0x11]);
this.udpSend(b,(buffer) => { const buffer = await this.udpSend(b,b => b);
const reader = this.reader(buffer); const reader = this.reader(buffer);
reader.skip(6); reader.skip(6);
state.raw.port = this.readUInt(reader); state.gamePort = this.readUInt(reader);
state.raw.hostname = this.readString(reader); state.raw.hostname = this.readString(reader);
state.name = this.stripColorCodes(this.readString(reader)); state.name = this.stripColorCodes(this.readString(reader));
state.raw.numplayers = this.readUInt(reader); state.raw.numplayers = this.readUInt(reader);
@ -36,9 +36,6 @@ class Armagetron extends Core {
state.raw.options = this.stripColorCodes(this.readString(reader)); state.raw.options = this.stripColorCodes(this.readString(reader));
state.raw.uri = this.readString(reader); state.raw.uri = this.readString(reader);
state.raw.globalids = this.readString(reader); state.raw.globalids = this.readString(reader);
this.finish(state);
return true;
});
} }
readUInt(reader) { readUInt(reader) {

View file

@ -1,15 +1,16 @@
const Core = require('./core'); const Core = require('./core');
class Ase extends Core { class Ase extends Core {
run(state) { async run(state) {
this.udpSend('s',(buffer) => { const buffer = await this.udpSend('s',(buffer) => {
const reader = this.reader(buffer); const reader = this.reader(buffer);
const header = reader.string({length: 4}); const header = reader.string({length: 4});
if(header !== 'EYE1') return; if (header === 'EYE1') return reader.rest();
});
const reader = this.reader(buffer);
state.raw.gamename = this.readString(reader); state.raw.gamename = this.readString(reader);
state.raw.port = parseInt(this.readString(reader)); state.gamePort = parseInt(this.readString(reader));
state.name = this.readString(reader); state.name = this.readString(reader);
state.raw.gametype = this.readString(reader); state.raw.gametype = this.readString(reader);
state.map = this.readString(reader); state.map = this.readString(reader);
@ -36,9 +37,6 @@ class Ase extends Core {
if(flags & 32) player.time = parseInt(this.readString(reader)); if(flags & 32) player.time = parseInt(this.readString(reader));
state.players.push(player); state.players.push(player);
} }
this.finish(state);
});
} }
readString(reader) { readString(reader) {

View file

@ -1,21 +1,16 @@
const async = require('async'), const Core = require('./core');
Core = require('./core');
class Battlefield extends Core { class Battlefield extends Core {
constructor() { constructor() {
super(); super();
this.encoding = 'latin1'; this.encoding = 'latin1';
this.isBadCompany2 = false;
} }
run(state) { async run(state) {
async.series([ await this.withTcp(async socket => {
(c) => { {
this.query(['serverInfo'], (data) => { const data = await this.query(socket, ['serverInfo']);
if(this.debug) console.log(data); state.name = data.shift();
if(data.shift() !== 'OK') return this.fatal('Missing OK');
state.raw.name = data.shift();
state.raw.numplayers = parseInt(data.shift()); state.raw.numplayers = parseInt(data.shift());
state.maxplayers = parseInt(data.shift()); state.maxplayers = parseInt(data.shift());
state.raw.gametype = data.shift(); state.raw.gametype = data.shift();
@ -33,44 +28,49 @@ class Battlefield extends Core {
} }
state.raw.targetscore = parseInt(data.shift()); state.raw.targetscore = parseInt(data.shift());
data.shift(); state.raw.status = data.shift();
state.raw.ranked = (data.shift() === 'true');
state.raw.punkbuster = (data.shift() === 'true'); // Seems like the fields end at random places beyond this point
state.password = (data.shift() === 'true'); // depending on the server version
state.raw.uptime = parseInt(data.shift());
state.raw.roundtime = parseInt(data.shift()); if (data.length) state.raw.ranked = (data.shift() === 'true');
if(this.isBadCompany2) { if (data.length) state.raw.punkbuster = (data.shift() === 'true');
data.shift(); if (data.length) state.password = (data.shift() === 'true');
data.shift(); if (data.length) state.raw.uptime = parseInt(data.shift());
if (data.length) state.raw.roundtime = parseInt(data.shift());
const isBadCompany2 = data[0] === 'BC2';
if (isBadCompany2) {
if (data.length) data.shift();
if (data.length) data.shift();
} }
if (data.length) {
state.raw.ip = data.shift(); state.raw.ip = data.shift();
state.raw.punkbusterversion = data.shift(); const split = state.raw.ip.split(':');
state.raw.joinqueue = (data.shift() === 'true'); state.gameHost = split[0];
state.raw.region = data.shift(); state.gamePort = split[1];
if(!this.isBadCompany2) { } else {
state.raw.pingsite = data.shift(); // best guess if the server doesn't tell us what the server port is
state.raw.country = data.shift(); // these are just the default game ports for different default query ports
state.raw.quickmatch = (data.shift() === 'true'); if (this.options.port === 48888) state.gamePort = 7673;
if (this.options.port === 22000) state.gamePort = 25200;
}
if (data.length) state.raw.punkbusterversion = data.shift();
if (data.length) state.raw.joinqueue = (data.shift() === 'true');
if (data.length) state.raw.region = data.shift();
if (data.length) state.raw.pingsite = data.shift();
if (data.length) state.raw.country = data.shift();
if (data.length) state.raw.quickmatch = (data.shift() === 'true');
} }
c(); {
}); const data = await this.query(socket, ['version']);
}, data.shift();
(c) => { state.raw.version = data.shift();
this.query(['version'], (data) => { }
if(this.debug) console.log(data);
if(data[0] !== 'OK') return this.fatal('Missing OK');
state.raw.version = data[2];
c();
});
},
(c) => {
this.query(['listPlayers','all'], (data) => {
if(this.debug) console.log(data);
if(data.shift() !== 'OK') return this.fatal('Missing OK');
{
const data = await this.query(socket, ['listPlayers', 'all']);
const fieldCount = parseInt(data.shift()); const fieldCount = parseInt(data.shift());
const fields = []; const fields = [];
for (let i = 0; i < fieldCount; i++) { for (let i = 0; i < fieldCount; i++) {
@ -102,39 +102,23 @@ class Battlefield extends Core {
} }
state.players.push(player); state.players.push(player);
} }
}
this.finish(state);
}); });
} }
]);
} async query(socket, params) {
query(params,c) { const outPacket = this.buildPacket(params);
this.tcpSend(buildPacket(params), (data) => { return await this.tcpSend(socket, outPacket, (data) => {
const decoded = this.decodePacket(data); const decoded = this.decodePacket(data);
if(!decoded) return false; if(decoded) {
c(decoded); this.debugLog(decoded);
return true; if(decoded.shift() !== 'OK') throw new Error('Missing OK');
return decoded;
}
}); });
} }
decodePacket(buffer) {
if(buffer.length < 8) return false;
const reader = this.reader(buffer);
const header = reader.uint(4);
const totalLength = reader.uint(4);
if(buffer.length < totalLength) return false;
const paramCount = reader.uint(4); buildPacket(params) {
const params = [];
for(let i = 0; i < paramCount; i++) {
const len = reader.uint(4);
params.push(reader.string({length:len}));
const strNull = reader.uint(1);
}
return params;
}
}
function buildPacket(params) {
const paramBuffers = []; const paramBuffers = [];
for (const param of params) { for (const param of params) {
paramBuffers.push(Buffer.from(param,'utf8')); paramBuffers.push(Buffer.from(param,'utf8'));
@ -158,5 +142,23 @@ function buildPacket(params) {
return b; return b;
} }
decodePacket(buffer) {
if(buffer.length < 8) return false;
const reader = this.reader(buffer);
const header = reader.uint(4);
const totalLength = reader.uint(4);
if(buffer.length < totalLength) return false;
this.debugLog("Expected " + totalLength + " bytes, have " + buffer.length);
const paramCount = reader.uint(4);
const params = [];
for(let i = 0; i < paramCount; i++) {
const len = reader.uint(4);
params.push(reader.string({length:len}));
const strNull = reader.uint(1);
}
return params;
}
}
module.exports = Battlefield; module.exports = Battlefield;

View file

@ -1,17 +1,15 @@
const request = require('request'), const Core = require('./core'),
Core = require('./core'); cheerio = require('cheerio');
class BuildAndShoot extends Core { class BuildAndShoot extends Core {
run(state) { async run(state) {
request({ const body = await this.request({
uri: 'http://'+this.options.address+':'+this.options.port_query+'/', uri: 'http://'+this.options.address+':'+this.options.port+'/',
timeout: 3000, });
}, (e,r,body) => {
if(e) return this.fatal('HTTP error');
let m; let m;
m = body.match(/status server for (.*?)\r|\n/); m = body.match(/status server for (.*?)\.?(\r|\n)/);
if(m) state.name = m[1]; if(m) state.name = m[1];
m = body.match(/Current uptime: (\d+)/); m = body.match(/Current uptime: (\d+)/);
@ -26,21 +24,22 @@ class BuildAndShoot extends Core {
state.maxplayers = m[2]; state.maxplayers = m[2];
} }
m = body.match(/class="playerlist"([^]+?)\/table/); m = body.match(/aos:\/\/[0-9]+:[0-9]+/);
if (m) { if (m) {
const table = m[1]; state.connect = m[0];
const pre = /<tr>[^]*<td>([^]*)<\/td>[^]*<td>([^]*)<\/td>[^]*<td>([^]*)<\/td>[^]*<td>([^]*)<\/td>/g; }
let pm;
while(pm = pre.exec(table)) { const $ = cheerio.load(body);
if(pm[2] === 'Ping') continue; $('#playerlist tbody tr').each((i,tr) => {
if (!$(tr).find('td').first().attr('colspan')) {
state.players.push({ state.players.push({
name: pm[1], name: $(tr).find('td').eq(2).text(),
ping: pm[2], ping: $(tr).find('td').eq(3).text().trim(),
team: pm[3], team: $(tr).find('td').eq(4).text().toLowerCase(),
score: pm[4] score: parseInt($(tr).find('td').eq(5).text())
}); });
} }
} });
/* /*
var m = this.options.address.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/); var m = this.options.address.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/);
if(m) { if(m) {
@ -52,8 +51,6 @@ class BuildAndShoot extends Core {
state.raw.url = 'aos://'+addr; state.raw.url = 'aos://'+addr;
} }
*/ */
this.finish(state);
});
} }
} }

View file

@ -1,39 +1,81 @@
const EventEmitter = require('events').EventEmitter, const EventEmitter = require('events').EventEmitter,
dns = require('dns'), dns = require('dns'),
net = require('net'), net = require('net'),
async = require('async'),
Reader = require('../lib/reader'), Reader = require('../lib/reader'),
HexUtil = require('../lib/HexUtil'); HexUtil = require('../lib/HexUtil'),
util = require('util'),
dnsLookupAsync = util.promisify(dns.lookup),
dnsResolveAsync = util.promisify(dns.resolve),
requestAsync = require('request-promise'),
Promises = require('../lib/Promises');
class Core extends EventEmitter { class Core extends EventEmitter {
constructor() { constructor() {
super(); super();
this.options = {
socketTimeout: 2000,
attemptTimeout: 10000,
maxAttempts: 1
};
this.attempt = 1;
this.finished = false;
this.encoding = 'utf8'; this.encoding = 'utf8';
this.byteorder = 'le'; this.byteorder = 'le';
this.delimiter = '\0'; this.delimiter = '\0';
this.srvRecord = null; this.srvRecord = null;
this.attemptTimeoutTimer = null; this.abortedPromise = null;
// Sent to us by QueryRunner
this.options = null;
this.udpSocket = null;
this.shortestRTT = 0;
this.usedTcp = false;
} }
fatal(err,noretry) { async runAllAttempts() {
if(!noretry && this.attempt < this.options.maxAttempts) { let result = null;
this.attempt++; let lastError = null;
this.start(); for (let attempt = 1; attempt <= this.options.maxAttempts; attempt++) {
return; try {
result = await this.runOnceSafe();
result.query.attempts = attempt;
break;
} catch (e) {
lastError = e;
}
} }
this.done({error: err.toString()}); if (result === null) {
throw lastError;
}
return result;
} }
initState() { // Runs a single attempt with a timeout and cleans up afterward
return { async runOnceSafe() {
let abortCall = null;
this.abortedPromise = new Promise((resolve,reject) => {
abortCall = () => reject("Query is finished -- cancelling outstanding promises");
});
// 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();
timeout = Promises.createTimeout(this.options.attemptTimeout, "Attempt");
return await Promise.race([promise,timeout]);
} finally {
timeout && timeout.cancel();
try {
abortCall();
} catch(e) {
this.debugLog("Error during abort cleanup: " + e.stack);
}
}
}
async runOnce() {
const options = this.options;
if (('host' in options) && !('address' in options)) {
options.address = await this.parseDns(options.host);
}
const state = {
name: '', name: '',
map: '', map: '',
password: false, password: false,
@ -44,130 +86,81 @@ class Core extends EventEmitter {
players: [], players: [],
bots: [] bots: []
}; };
await this.run(state);
// because lots of servers prefix with spaces to try to appear first
state.name = (state.name || '').trim();
if (!('connect' in state)) {
state.connect = ''
+ (state.gameHost || this.options.host || this.options.address)
+ ':'
+ (state.gamePort || this.options.port)
}
state.ping = this.shortestRTT;
delete state.gameHost;
delete state.gamePort;
return state;
} }
finalizeState(state) {} async run(state) {}
finish(state) { /**
this.finalizeState(state); * @param {string} host
this.done(state); * @returns {Promise<string>}
*/
async parseDns(host) {
const isIp = (host) => {
return !!host.match(/\d+\.\d+\.\d+\.\d+/);
};
const resolveStandard = async (host) => {
if(isIp(host)) return host;
this.debugLog("Standard DNS Lookup: " + host);
const {address,family} = await dnsLookupAsync(host);
this.debugLog(address);
return address;
};
const resolveSrv = async (srv,host) => {
if(isIp(host)) return host;
this.debugLog("SRV DNS Lookup: " + srv+'.'+host);
let records;
try {
records = await dnsResolveAsync(srv + '.' + host, 'SRV');
this.debugLog(records);
if(records.length >= 1) {
const record = records[0];
this.options.port = record.port;
const srvhost = record.name;
return await resolveStandard(srvhost);
} }
} catch(e) {
done(state) { this.debugLog(e.toString());
if(this.finished) return;
if(this.options.notes)
state.notes = this.options.notes;
state.query = {};
if('host' in this.options) state.query.host = this.options.host;
if('address' in this.options) state.query.address = this.options.address;
if('port' in this.options) state.query.port = this.options.port;
if('port_query' in this.options) state.query.port_query = this.options.port_query;
state.query.type = this.type;
if('pretty' in this) state.query.pretty = this.pretty;
state.query.duration = Date.now() - this.startMillis;
state.query.attempts = this.attempt;
this.reset();
this.finished = true;
this.emit('finished',state);
if(this.options.callback) this.options.callback(state);
} }
return await resolveStandard(host);
reset() {
clearTimeout(this.attemptTimeoutTimer);
if(this.timers) {
for (const timer of this.timers) {
clearTimeout(timer);
}
}
this.timers = [];
if(this.tcpSocket) {
this.tcpSocket.destroy();
delete this.tcpSocket;
}
this.udpTimeoutTimer = false;
this.udpCallback = false;
}
start() {
const options = this.options;
this.reset();
this.startMillis = Date.now();
this.attemptTimeoutTimer = setTimeout(() => {
this.fatal('timeout');
},this.options.attemptTimeout);
async.series([
(c) => {
// resolve host names
if(!('host' in options)) return c();
if(options.host.match(/\d+\.\d+\.\d+\.\d+/)) {
options.address = options.host;
c();
} else {
this.parseDns(options.host,c);
}
},
(c) => {
// calculate query port if needed
if(!('port_query' in options) && 'port' in options) {
const offset = options.port_query_offset || 0;
options.port_query = options.port + offset;
}
c();
},
(c) => {
// run
this.run(this.initState());
}
]);
}
run() {}
parseDns(host,c) {
const resolveStandard = (host,c) => {
if(this.debug) console.log("Standard DNS Lookup: " + host);
dns.lookup(host, (err,address,family) => {
if(err) return this.fatal(err);
if(this.debug) console.log(address);
this.options.address = address;
c();
});
}; };
const resolveSrv = (srv,host,c) => { if(this.srvRecord) return await resolveSrv(this.srvRecord, host);
if(this.debug) console.log("SRV DNS Lookup: " + srv+'.'+host); else return await resolveStandard(host);
dns.resolve(srv+'.'+host, 'SRV', (err,addresses) => { }
if(this.debug) console.log(err, addresses);
if(err) return resolveStandard(host,c);
if(addresses.length >= 1) {
const line = addresses[0];
this.options.port = line.port;
const srvhost = line.name;
if(srvhost.match(/\d+\.\d+\.\d+\.\d+/)) { /** Param can be a time in ms, or a promise (which will be timed) */
this.options.address = srvhost; registerRtt(param) {
c(); if (param.then) {
const start = Date.now();
param.then(() => {
const end = Date.now();
const rtt = end - start;
this.registerRtt(rtt);
}).catch(() => {});
} else { } else {
// resolve yet again this.debugLog("Registered RTT: " + param + "ms");
resolveStandard(srvhost,c); if (this.shortestRTT === 0 || param < this.shortestRTT) {
this.shortestRTT = param;
} }
return;
} }
return resolveStandard(host,c);
});
};
if(this.srvRecord) resolveSrv(this.srvRecord,host,c);
else resolveStandard(host,c);
} }
// utils // utils
@ -184,125 +177,225 @@ class Core extends EventEmitter {
} }
} }
} }
setTimeout(c,t) {
if(this.finished) return 0;
const id = setTimeout(c,t);
this.timers.push(id);
return id;
}
trueTest(str) { trueTest(str) {
if(typeof str === 'boolean') return str; if(typeof str === 'boolean') return str;
if(typeof str === 'number') return str !== 0; if(typeof str === 'number') return str !== 0;
if(typeof str === 'string') { if(typeof str === 'string') {
if(str.toLowerCase() === 'true') return true; if(str.toLowerCase() === 'true') return true;
if(str === 'yes') return true; if(str.toLowerCase() === 'yes') return true;
if(str === '1') return true; if(str === '1') return true;
} }
return false; return false;
} }
_tcpConnect(c) { assertValidPort(port) {
if(this.tcpSocket) return c(this.tcpSocket); if (!port || port < 1 || port > 65535) {
throw new Error("Invalid tcp/ip port: " + port);
}
}
let connected = false; /**
let received = Buffer.from([]); * @template T
* @param {function(Socket):Promise<T>} fn
* @returns {Promise<T>}
*/
async withTcp(fn, port) {
this.usedTcp = true;
const address = this.options.address; const address = this.options.address;
const port = this.options.port_query; if (!port) port = this.options.port;
this.assertValidPort(port);
const socket = this.tcpSocket = net.connect(port,address,() => { let socket, connectionTimeout;
if(this.debug) console.log(address+':'+port+" TCPCONNECTED"); try {
connected = true; socket = net.connect(port,address);
c(socket);
});
socket.setNoDelay(true); socket.setNoDelay(true);
if(this.debug) console.log(address+':'+port+" TCPCONNECT");
this.debugLog(log => {
this.debugLog(address+':'+port+" TCP Connecting");
const writeHook = socket.write; const writeHook = socket.write;
socket.write = (...args) => { socket.write = (...args) => {
if(this.debug) { log(address+':'+port+" TCP-->");
console.log(address+':'+port+" TCP-->"); log(HexUtil.debugDump(args[0]));
console.log(HexUtil.debugDump(args[0]));
}
writeHook.apply(socket,args); writeHook.apply(socket,args);
}; };
socket.on('error', e => log('TCP Error: ' + e));
socket.on('error', () => {}); socket.on('close', () => log('TCP Closed'));
socket.on('close', () => {
if(!this.tcpCallback) return;
if(connected) return this.fatal('Socket closed while waiting on TCP');
else return this.fatal('TCP Connection Refused');
});
socket.on('data', (data) => { socket.on('data', (data) => {
if(!this.tcpCallback) return; log(address+':'+port+" <--TCP");
if(this.debug) { log(data);
console.log(address+':'+port+" <--TCP");
console.log(HexUtil.debugDump(data));
}
received = Buffer.concat([received,data]);
if(this.tcpCallback(received)) {
clearTimeout(this.tcpTimeoutTimer);
this.tcpCallback = false;
received = Buffer.from([]);
}
}); });
socket.on('ready', () => log(address+':'+port+" TCP Connected"));
});
const connectionPromise = new Promise((resolve,reject) => {
socket.on('ready', resolve);
socket.on('close', () => reject(new Error('TCP Connection Refused')));
});
this.registerRtt(connectionPromise);
connectionTimeout = Promises.createTimeout(this.options.socketTimeout, 'TCP Opening');
await Promise.race([
connectionPromise,
connectionTimeout,
this.abortedPromise
]);
return await fn(socket);
} finally {
socket && socket.destroy();
connectionTimeout && connectionTimeout.cancel();
} }
tcpSend(buffer,ondata) { }
process.nextTick(() => {
if(this.tcpCallback) return this.fatal('Attempted to send TCP packet while still waiting on a managed response'); /**
this._tcpConnect((socket) => { * @template T
* @param {Socket} socket
* @param {Buffer|string} buffer
* @param {function(Buffer):T} ondata
* @returns Promise<T>
*/
async tcpSend(socket,buffer,ondata) {
let timeout;
try {
const promise = new Promise(async (resolve, reject) => {
let received = Buffer.from([]);
const onData = (data) => {
received = Buffer.concat([received, data]);
const result = ondata(received);
if (result !== undefined) {
socket.off('data', onData);
resolve(result);
}
};
socket.on('data', onData);
socket.write(buffer); socket.write(buffer);
}); });
if(!ondata) return; timeout = Promises.createTimeout(this.options.socketTimeout, 'TCP');
return await Promise.race([promise, timeout, this.abortedPromise]);
this.tcpTimeoutTimer = this.setTimeout(() => { } finally {
this.tcpCallback = false; timeout && timeout.cancel();
this.fatal('TCP Watchdog Timeout'); }
},this.options.socketTimeout);
this.tcpCallback = ondata;
});
} }
udpSend(buffer,onpacket,ontimeout) { /**
process.nextTick(() => { * @param {Buffer|string} buffer
if(this.udpCallback) return this.fatal('Attempted to send UDP packet while still waiting on a managed response'); * @param {function(Buffer):T} onPacket
this._udpSendNow(buffer); * @param {(function():T)=} onTimeout
if(!onpacket) return; * @returns Promise<T>
* @template T
this.udpTimeoutTimer = this.setTimeout(() => { */
this.udpCallback = false; async udpSend(buffer,onPacket,onTimeout) {
let timeout = false; const address = this.options.address;
if(!ontimeout || ontimeout() !== true) timeout = true; const port = this.options.port;
if(timeout) this.fatal('UDP Watchdog Timeout'); this.assertValidPort(port);
},this.options.socketTimeout);
this.udpCallback = onpacket;
});
}
_udpSendNow(buffer) {
if(!('port_query' in this.options)) return this.fatal('Attempted to send without setting a port');
if(!('address' in this.options)) return this.fatal('Attempted to send without setting an address');
if(typeof buffer === 'string') buffer = Buffer.from(buffer,'binary'); if(typeof buffer === 'string') buffer = Buffer.from(buffer,'binary');
this.debugLog(log => {
log(address+':'+port+" UDP-->");
log(HexUtil.debugDump(buffer));
});
if(this.debug) { const socket = this.udpSocket;
console.log(this.options.address+':'+this.options.port_query+" UDP-->"); socket.send(buffer, address, port);
console.log(HexUtil.debugDump(buffer));
let socketCallback;
let timeout;
try {
const promise = new Promise((resolve, reject) => {
const start = Date.now();
let end = null;
socketCallback = (fromAddress, fromPort, buffer) => {
try {
if (fromAddress !== address) return;
if (fromPort !== port) return;
if (end === null) {
end = Date.now();
const rtt = end-start;
this.registerRtt(rtt);
} }
this.udpSocket.send(buffer,0,buffer.length,this.options.port_query,this.options.address); const result = onPacket(buffer);
if (result !== undefined) {
this.debugLog("UDP send finished by callback");
resolve(result);
} }
_udpResponse(buffer) { } catch(e) {
if(this.udpCallback) { reject(e);
const result = this.udpCallback(buffer); }
if(result === true) { };
// we're done with this udp session socket.addCallback(socketCallback, this.options.debug);
clearTimeout(this.udpTimeoutTimer); });
this.udpCallback = false; timeout = Promises.createTimeout(this.options.socketTimeout, 'UDP');
const wrappedTimeout = new Promise((resolve, reject) => {
timeout.catch((e) => {
this.debugLog("UDP timeout detected");
if (onTimeout) {
try {
const result = onTimeout();
if (result !== undefined) {
this.debugLog("UDP timeout resolved by callback");
resolve(result);
return;
}
} catch(e) {
reject(e);
}
}
reject(e);
});
});
return await Promise.race([promise, wrappedTimeout, this.abortedPromise]);
} finally {
timeout && timeout.cancel();
socketCallback && socket.removeCallback(socketCallback);
}
}
async 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.
if (!this.usedTcp) {
await this.withTcp(() => {});
}
let requestPromise;
try {
requestPromise = requestAsync({
...params,
timeout: this.options.socketTimeout,
resolveWithFullResponse: true
});
this.debugLog(log => {
log(() => params.uri + " HTTP-->");
requestPromise
.then((response) => log(params.uri + " <--HTTP " + response.statusCode))
.catch(() => {});
});
const wrappedPromise = requestPromise.then(response => {
if (response.statusCode !== 200) throw new Error("Bad status code: " + response.statusCode);
return response.body;
});
return await Promise.race([wrappedPromise, this.abortedPromise]);
} finally {
requestPromise && requestPromise.cancel();
}
}
debugLog(...args) {
if (!this.options.debug) return;
try {
if(args[0] instanceof Buffer) {
this.debugLog(HexUtil.debugDump(args[0]));
} else if (typeof args[0] == 'function') {
const result = args[0].call(undefined, this.debugLog.bind(this));
if (result !== undefined) {
this.debugLog(result);
} }
} else { } else {
this.udpResponse(buffer); console.log(...args);
}
} catch(e) {
console.log("Error while debug logging: " + e);
} }
} }
udpResponse() {}
} }
module.exports = Core; module.exports = Core;

View file

@ -3,32 +3,40 @@ const Core = require('./core');
class Doom3 extends Core { class Doom3 extends Core {
constructor() { constructor() {
super(); super();
this.pretty = 'Doom 3';
this.encoding = 'latin1'; this.encoding = 'latin1';
this.isEtqw = false; this.isEtqw = false;
this.hasSpaceBeforeClanTag = false; this.hasSpaceBeforeClanTag = false;
this.hasClanTag = false; this.hasClanTag = false;
this.hasTypeFlag = false; this.hasTypeFlag = false;
} }
run(state) { async run(state) {
this.udpSend('\xff\xffgetInfo\x00PiNGPoNG\x00', (buffer) => { const body = await this.udpSend('\xff\xffgetInfo\x00PiNGPoNg\x00', packet => {
const reader = this.reader(buffer); const reader = this.reader(packet);
const header = reader.uint(2); const header = reader.uint(2);
if(header !== 0xffff) return; if(header !== 0xffff) return;
const header2 = reader.string(); const header2 = reader.string();
if(header2 !== 'infoResponse') return; if(header2 !== 'infoResponse') return;
const challengePart1 = reader.string({length:4});
if (challengePart1 !== "PiNG") return;
// some doom3 implementations only return the first 4 bytes of the challenge
const challengePart2 = reader.string({length:4});
if (challengePart2 !== 'PoNg') reader.skip(-4);
return reader.rest();
});
if(this.isEtqw) { let reader = this.reader(body);
const taskId = reader.uint(4);
}
const challenge = reader.uint(4);
const protoVersion = reader.uint(4); const protoVersion = reader.uint(4);
state.raw.protocolVersion = (protoVersion>>16)+'.'+(protoVersion&0xffff); state.raw.protocolVersion = (protoVersion>>16)+'.'+(protoVersion&0xffff);
if(this.isEtqw) { // some doom implementations send us a packet size here, some don't (etqw does this)
// we can tell if this is a packet size, because the third and fourth byte will be 0 (no packets are that massive)
reader.skip(2);
const packetContainsSize = (reader.uint(2) === 0);
reader.skip(-4);
if (packetContainsSize) {
const size = reader.uint(4); const size = reader.uint(4);
this.debugLog("Received packet size: " + size);
} }
while(!reader.done()) { while(!reader.done()) {
@ -40,23 +48,22 @@ class Doom3 extends Core {
} }
if(!key) break; if(!key) break;
state.raw[key] = value; state.raw[key] = value;
this.debugLog(key + "=" + value);
} }
let i = 0; const isEtqw = state.raw.gamename && state.raw.gamename.toLowerCase().includes('etqw');
while(!reader.done()) {
i++;
const player = {};
player.id = reader.uint(1);
if(player.id === 32) break;
player.ping = reader.uint(2);
if(!this.isEtqw) player.rate = reader.uint(4);
player.name = this.stripColors(reader.string());
if(this.hasClanTag) {
if(this.hasSpaceBeforeClanTag) reader.uint(1);
player.clantag = this.stripColors(reader.string());
}
if(this.hasTypeFlag) player.typeflag = reader.uint(1);
const rest = reader.rest();
let playerResult = this.attemptPlayerParse(rest, isEtqw, false, false, false);
if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, false, false);
if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, true, true);
if (!playerResult) {
throw new Error("Unable to find a suitable parse strategy for player list");
}
let players;
[players,reader] = playerResult;
for (const player of players) {
if(!player.ping || player.typeflag) if(!player.ping || player.typeflag)
state.bots.push(player); state.bots.push(player);
else else
@ -64,7 +71,7 @@ class Doom3 extends Core {
} }
state.raw.osmask = reader.uint(4); state.raw.osmask = reader.uint(4);
if(this.isEtqw) { if (isEtqw) {
state.raw.ranked = reader.uint(1); state.raw.ranked = reader.uint(1);
state.raw.timeleft = reader.uint(4); state.raw.timeleft = reader.uint(4);
state.raw.gamestate = reader.uint(1); state.raw.gamestate = reader.uint(1);
@ -81,11 +88,62 @@ class Doom3 extends Core {
if (state.raw.si_name) state.name = state.raw.si_name; if (state.raw.si_name) state.name = state.raw.si_name;
if (state.raw.si_map) state.map = state.raw.si_map; if (state.raw.si_map) state.map = state.raw.si_map;
if (state.raw.si_maxplayers) state.maxplayers = parseInt(state.raw.si_maxplayers); if (state.raw.si_maxplayers) state.maxplayers = parseInt(state.raw.si_maxplayers);
if (state.raw.si_maxPlayers) state.maxplayers = parseInt(state.raw.si_maxplayers);
if (state.raw.si_usepass === '1') state.password = true; if (state.raw.si_usepass === '1') state.password = true;
if (state.raw.si_needPass === '1') state.password = true;
if (this.options.port === 27733) state.gamePort = 3074; // etqw has a different query and game port
}
this.finish(state); attemptPlayerParse(rest, isEtqw, hasClanTag, hasClanTagPos, hasTypeFlag) {
return true; this.debugLog("starting player parse attempt:");
}); this.debugLog("isEtqw: " + isEtqw);
this.debugLog("hasClanTag: " + hasClanTag);
this.debugLog("hasClanTagPos: " + hasClanTagPos);
this.debugLog("hasTypeFlag: " + hasTypeFlag);
const reader = this.reader(rest);
let lastId = -1;
const players = [];
while(true) {
this.debugLog("---");
if (reader.done()) {
this.debugLog("* aborting attempt, overran buffer *");
return null;
}
const player = {};
player.id = reader.uint(1);
this.debugLog("id: " + player.id);
if (player.id <= lastId || player.id > 0x20) {
this.debugLog("* aborting attempt, invalid player id *");
return null;
}
lastId = player.id;
if(player.id === 0x20) {
this.debugLog("* player parse successful *");
break;
}
player.ping = reader.uint(2);
this.debugLog("ping: " + player.ping);
if(!isEtqw) {
player.rate = reader.uint(4);
this.debugLog("rate: " + player.rate);
}
player.name = this.stripColors(reader.string());
this.debugLog("name: " + player.name);
if(hasClanTag) {
if(hasClanTagPos) {
const clanTagPos = reader.uint(1);
this.debugLog("clanTagPos: " + clanTagPos);
}
player.clantag = this.stripColors(reader.string());
this.debugLog("clan tag: " + player.clantag);
}
if(hasTypeFlag) {
player.typeflag = reader.uint(1);
this.debugLog("type flag: " + player.typeflag);
}
players.push(player);
}
return [players,reader];
} }
stripColors(str) { stripColors(str) {

View file

@ -6,8 +6,15 @@ class Ffow extends Valve {
this.byteorder = 'be'; this.byteorder = 'be';
this.legacyChallenge = true; this.legacyChallenge = true;
} }
queryInfo(state,c) { async queryInfo(state) {
this.sendPacket(0x46,false,'LSQ',0x49, (b) => { this.debugLog("Requesting ffow info ...");
const b = await this.sendPacket(
0x46,
false,
'LSQ',
0x49
);
const reader = this.reader(b); const reader = this.reader(b);
state.raw.protocol = reader.uint(1); state.raw.protocol = reader.uint(1);
state.name = reader.string(); state.name = reader.string();
@ -16,7 +23,7 @@ class Ffow extends Valve {
state.raw.gamemode = reader.string(); state.raw.gamemode = reader.string();
state.raw.description = reader.string(); state.raw.description = reader.string();
state.raw.version = reader.string(); state.raw.version = reader.string();
state.raw.port = reader.uint(2); state.gamePort = reader.uint(2);
state.raw.numplayers = reader.uint(1); state.raw.numplayers = reader.uint(1);
state.maxplayers = reader.uint(1); state.maxplayers = reader.uint(1);
state.raw.listentype = String.fromCharCode(reader.uint(1)); state.raw.listentype = String.fromCharCode(reader.uint(1));
@ -27,8 +34,6 @@ class Ffow extends Valve {
state.raw.round = reader.uint(1); state.raw.round = reader.uint(1);
state.raw.maxrounds = reader.uint(1); state.raw.maxrounds = reader.uint(1);
state.raw.timeleft = reader.uint(2); state.raw.timeleft = reader.uint(2);
c();
});
} }
} }

View file

@ -1,5 +1,4 @@
const request = require('request'), const Quake2 = require('./quake2');
Quake2 = require('./quake2');
class FiveM extends Quake2 { class FiveM extends Quake2 {
constructor() { constructor() {
@ -9,43 +8,28 @@ class FiveM extends Quake2 {
this.encoding = 'utf8'; this.encoding = 'utf8';
} }
finish(state) { async run(state) {
request({ await super.run(state);
uri: 'http://'+this.options.address+':'+this.options.port_query+'/info.json',
timeout: this.options.socketTimeout
}, (e,r,body) => {
if(e) return this.fatal('HTTP error');
let json;
try {
json = JSON.parse(body);
} catch(e) {
return this.fatal('Invalid JSON');
}
{
const raw = await this.request({
uri: 'http://' + this.options.address + ':' + this.options.port + '/info.json'
});
const json = JSON.parse(raw);
state.raw.info = json; state.raw.info = json;
request({
uri: 'http://'+this.options.address+':'+this.options.port_query+'/players.json',
timeout: this.options.socketTimeout
}, (e,r,body) => {
if(e) return this.fatal('HTTP error');
let json;
try {
json = JSON.parse(body);
} catch(e) {
return this.fatal('Invalid JSON');
} }
{
const raw = await this.request({
uri: 'http://' + this.options.address + ':' + this.options.port + '/players.json'
});
const json = JSON.parse(raw);
state.raw.players = json; state.raw.players = json;
state.players = []; state.players = [];
for (const player of json) { for (const player of json) {
state.players.push({name: player.name, ping: player.ping}); state.players.push({name: player.name, ping: player.ping});
} }
}
super.finish(state);
});
});
} }
} }

View file

@ -1,34 +1,28 @@
const async = require('async'), const Core = require('./core');
Core = require('./core');
class Gamespy1 extends Core { class Gamespy1 extends Core {
constructor() { constructor() {
super(); super();
this.sessionId = 1;
this.encoding = 'latin1'; this.encoding = 'latin1';
this.byteorder = 'be'; this.byteorder = 'be';
} }
run(state) { async run(state) {
async.series([ {
(c) => { const data = await this.sendPacket('info');
this.sendPacket('info', (data) => {
state.raw = data; state.raw = data;
if ('hostname' in state.raw) state.name = state.raw.hostname; if ('hostname' in state.raw) state.name = state.raw.hostname;
if ('mapname' in state.raw) state.map = state.raw.mapname; if ('mapname' in state.raw) state.map = state.raw.mapname;
if (this.trueTest(state.raw.password)) state.password = true; if (this.trueTest(state.raw.password)) state.password = true;
if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers); if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers);
c(); if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport);
}); }
}, {
(c) => { const data = await this.sendPacket('rules');
this.sendPacket('rules', (data) => {
state.raw.rules = data; state.raw.rules = data;
c(); }
}); {
}, const data = await this.sendPacket('players');
(c) => {
this.sendPacket('players', (data) => {
const players = {}; const players = {};
const teams = {}; const teams = {};
for (const ident of Object.keys(data)) { for (const ident of Object.keys(data)) {
@ -52,17 +46,13 @@ class Gamespy1 extends Core {
for (const id of Object.keys(players)) { for (const id of Object.keys(players)) {
state.players.push(players[id]); state.players.push(players[id]);
} }
this.finish(state);
});
} }
]);
} }
sendPacket(type,callback) { async sendPacket(type) {
const queryId = ''; const queryId = '';
const output = {}; const output = {};
this.udpSend('\\'+type+'\\', (buffer) => { return await this.udpSend('\\'+type+'\\', buffer => {
const reader = this.reader(buffer); const reader = this.reader(buffer);
const str = reader.string({length:buffer.length}); const str = reader.string({length:buffer.length});
const split = str.split('\\'); const split = str.split('\\');
@ -79,8 +69,7 @@ class Gamespy1 extends Core {
if('final' in output) { if('final' in output) {
delete output.final; delete output.final;
delete output.queryid; delete output.queryid;
callback(output); return output;
return true;
} }
}); });
} }

View file

@ -3,65 +3,105 @@ const Core = require('./core');
class Gamespy2 extends Core { class Gamespy2 extends Core {
constructor() { constructor() {
super(); super();
this.sessionId = 1;
this.encoding = 'latin1'; this.encoding = 'latin1';
this.byteorder = 'be'; this.byteorder = 'be';
} }
run(state) { async run(state) {
const request = Buffer.from([0xfe,0xfd,0x00,0x00,0x00,0x00,0x01,0xff,0xff,0xff]); // Parse info
const packets = []; {
this.udpSend(request, const body = await this.sendPacket([0xff, 0, 0]);
(buffer) => { const reader = this.reader(body);
if(packets.length && buffer.readUInt8(0) === 0)
buffer = buffer.slice(1);
packets.push(buffer);
},
() => {
const buffer = Buffer.concat(packets);
const reader = this.reader(buffer);
const header = reader.uint(1);
if(header !== 0) return;
const pingId = reader.uint(4);
if(pingId !== 1) return;
while (!reader.done()) { while (!reader.done()) {
const key = reader.string(); const key = reader.string();
const value = reader.string(); const value = reader.string();
if (!key) break; if (!key) break;
state.raw[key] = value; state.raw[key] = value;
} }
if ('hostname' in state.raw) state.name = state.raw.hostname; if ('hostname' in state.raw) state.name = state.raw.hostname;
if ('mapname' in state.raw) state.map = state.raw.mapname; if ('mapname' in state.raw) state.map = state.raw.mapname;
if (this.trueTest(state.raw.password)) state.password = true; if (this.trueTest(state.raw.password)) state.password = true;
if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers); if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers);
if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport);
state.players = this.readFieldData(reader);
state.raw.teams = this.readFieldData(reader);
this.finish(state);
return true;
} }
);
// Parse players
{
const body = await this.sendPacket([0, 0xff, 0]);
const reader = this.reader(body);
state.players = this.readFieldData(reader);
}
// Parse teams
{
const body = await this.sendPacket([0, 0, 0xff]);
const reader = this.reader(body);
state.raw.teams = this.readFieldData(reader);
}
// Special case for america's army 1 and 2
// both use gamename = "armygame"
if (state.raw.gamename === 'armygame') {
const stripColor = (str) => {
// uses unreal 2 color codes
return str.replace(/\x1b...|[\x00-\x1a]/g,'');
};
state.name = stripColor(state.name);
state.map = stripColor(state.map);
for(const key of Object.keys(state.raw)) {
if(typeof state.raw[key] === 'string') {
state.raw[key] = stripColor(state.raw[key]);
}
}
for(const player of state.players) {
if(!('name' in player)) continue;
player.name = stripColor(player.name);
}
}
}
async sendPacket(type) {
const request = Buffer.concat([
Buffer.from([0xfe,0xfd,0x00]), // gamespy2
Buffer.from([0x00,0x00,0x00,0x01]), // ping ID
Buffer.from(type)
]);
return await this.udpSend(request, buffer => {
const reader = this.reader(buffer);
const header = reader.uint(1);
if (header !== 0) return;
const pingId = reader.uint(4);
if (pingId !== 1) return;
return reader.rest();
});
} }
readFieldData(reader) { readFieldData(reader) {
const count = reader.uint(1); const zero = reader.uint(1); // always 0
// count is unreliable (often it's wrong), so we don't use it. const count = reader.uint(1); // number of rows in this data
// read until we hit an empty first field string
if(this.debug) console.log("Reading fields, starting at: "+reader.rest()); // some games omit the count byte entirely if it's 0 or at random (like americas army)
// Luckily, count should always be <64, and ascii characters will typically be >64,
// so we can detect this.
if (count > 64) {
reader.skip(-1);
this.debugLog("Detected missing count byte, rewinding by 1");
} else {
this.debugLog("Detected row count: " + count);
}
this.debugLog(() => "Reading fields, starting at: "+reader.rest());
const fields = []; const fields = [];
while(!reader.done()) { while(!reader.done()) {
let field = reader.string(); let field = reader.string();
if(!field) break; if(!field) break;
if(field.charCodeAt(0) <= 2) field = field.substring(1);
fields.push(field); fields.push(field);
if(this.debug) console.log("field:"+field); this.debugLog("field:"+field);
} }
if (!fields.length) return [];
const units = []; const units = [];
outer: while(!reader.done()) { outer: while(!reader.done()) {
const unit = {}; const unit = {};
@ -69,7 +109,7 @@ class Gamespy2 extends Core {
let key = fields[iField]; let key = fields[iField];
let value = reader.string(); let value = reader.string();
if(!value && iField === 0) break outer; if(!value && iField === 0) break outer;
if(this.debug) console.log("value:"+value); this.debugLog("value:"+value);
if(key === 'player_') key = 'name'; if(key === 'player_') key = 'name';
else if(key === 'score_') key = 'score'; else if(key === 'score_') key = 'score';
else if(key === 'deaths_') key = 'deaths'; else if(key === 'deaths_') key = 'deaths';

View file

@ -1,5 +1,5 @@
const async = require('async'), const Core = require('./core'),
Core = require('./core'); HexUtil = require('../lib/HexUtil');
class Gamespy3 extends Core { class Gamespy3 extends Core {
constructor() { constructor() {
@ -7,27 +7,21 @@ class Gamespy3 extends Core {
this.sessionId = 1; this.sessionId = 1;
this.encoding = 'latin1'; this.encoding = 'latin1';
this.byteorder = 'be'; this.byteorder = 'be';
this.noChallenge = false;
this.useOnlySingleSplit = false; this.useOnlySingleSplit = false;
this.isJc2mp = false; this.isJc2mp = false;
} }
run(state) { async run(state) {
let challenge; const buffer = await this.sendPacket(9, false, false, false);
/** @type Buffer[] */
let packets;
async.series([
(c) => {
if(this.noChallenge) return c();
this.sendPacket(9,false,false,false,(buffer) => {
const reader = this.reader(buffer); const reader = this.reader(buffer);
challenge = parseInt(reader.string()); let challenge = parseInt(reader.string());
c(); this.debugLog("Received challenge key: " + challenge);
}); if (challenge === 0) {
}, // Some servers send us a 0 if they don't want a challenge key used
(c) => { // BF2 does this.
challenge = null;
}
let requestPayload; let requestPayload;
if(this.isJc2mp) { if(this.isJc2mp) {
// they completely alter the protocol. because why not. // they completely alter the protocol. because why not.
@ -35,27 +29,20 @@ class Gamespy3 extends Core {
} else { } else {
requestPayload = Buffer.from([0xff,0xff,0xff,0x01]); requestPayload = Buffer.from([0xff,0xff,0xff,0x01]);
} }
/** @type Buffer[] */
const packets = await this.sendPacket(0,challenge,requestPayload,true);
this.sendPacket(0,challenge,requestPayload,true,(b) => {
packets = b;
c();
});
},
(c) => {
// iterate over the received packets // iterate over the received packets
// the first packet will start off with k/v pairs, followed with data fields // the first packet will start off with k/v pairs, followed with data fields
// the following packets will only have data fields // the following packets will only have data fields
state.raw.playerTeamInfo = {}; state.raw.playerTeamInfo = {};
for(let iPacket = 0; iPacket < packets.length; iPacket++) { for(let iPacket = 0; iPacket < packets.length; iPacket++) {
const packet = packets[iPacket]; const packet = packets[iPacket];
const reader = this.reader(packet); const reader = this.reader(packet);
if(this.debug) { this.debugLog("Parsing packet #" + iPacket);
console.log("+++"+packet.toString('hex')); this.debugLog(packet);
console.log(":::"+packet.toString('ascii'));
}
// Parse raw server key/values // Parse raw server key/values
@ -63,12 +50,15 @@ class Gamespy3 extends Core {
while(!reader.done()) { while(!reader.done()) {
const key = reader.string(); const key = reader.string();
if(!key) break; if(!key) break;
let value = reader.string();
// reread the next line if we hit the weird ut3 bug let value = reader.string();
if(value === 'p1073741829') value = reader.string(); while(value.match(/^p[0-9]+$/)) {
// fix a weird ut3 bug where some keys don't have values
value = reader.string();
}
state.raw[key] = value; state.raw[key] = value;
this.debugLog(key + " = " + value);
} }
} }
@ -86,39 +76,37 @@ class Gamespy3 extends Core {
} else { } else {
let firstMode = true; let firstMode = true;
while(!reader.done()) { while(!reader.done()) {
let mode = reader.string(); if (reader.uint(1) <= 2) continue;
if(mode.charCodeAt(0) <= 2) mode = mode.substring(1); reader.skip(-1);
if(!mode) continue; let fieldId = reader.string();
let offset = 0; if(!fieldId) continue;
if(iPacket !== 0 && firstMode) offset = reader.uint(1); const fieldIdSplit = fieldId.split('_');
reader.skip(1); const fieldName = fieldIdSplit[0];
const itemType = fieldIdSplit.length > 1 ? fieldIdSplit[1] : 'no_';
if(!(itemType in state.raw.playerTeamInfo)) {
state.raw.playerTeamInfo[itemType] = [];
}
const items = state.raw.playerTeamInfo[itemType];
let offset = reader.uint(1);
firstMode = false; firstMode = false;
const modeSplit = mode.split('_'); this.debugLog(() => "Parsing new field: itemType=" + itemType + " fieldName=" + fieldName + " startOffset=" + offset);
const modeName = modeSplit[0];
const modeType = modeSplit.length > 1 ? modeSplit[1] : 'no_';
if(!(modeType in state.raw.playerTeamInfo)) {
state.raw.playerTeamInfo[modeType] = [];
}
const store = state.raw.playerTeamInfo[modeType];
while(!reader.done()) { while(!reader.done()) {
const item = reader.string(); const item = reader.string();
if(!item) break; if(!item) break;
while(store.length <= offset) { store.push({}); } while(items.length <= offset) { items.push({}); }
store[offset][modeName] = item; items[offset][fieldName] = item;
this.debugLog("* " + item);
offset++; offset++;
} }
} }
} }
} }
c();
},
(c) => {
// Turn all that raw state into something useful // Turn all that raw state into something useful
if ('hostname' in state.raw) state.name = state.raw.hostname; if ('hostname' in state.raw) state.name = state.raw.hostname;
@ -126,6 +114,7 @@ class Gamespy3 extends Core {
if ('mapname' in state.raw) state.map = state.raw.mapname; if ('mapname' in state.raw) state.map = state.raw.mapname;
if (state.raw.password === '1') state.password = true; if (state.raw.password === '1') state.password = true;
if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers); if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers);
if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport);
if('' in state.raw.playerTeamInfo) { if('' in state.raw.playerTeamInfo) {
for (const playerInfo of state.raw.playerTeamInfo['']) { for (const playerInfo of state.raw.playerTeamInfo['']) {
@ -141,14 +130,10 @@ class Gamespy3 extends Core {
state.players.push(player); state.players.push(player);
} }
} }
this.finish(state);
}
]);
} }
sendPacket(type,challenge,payload,assemble,c) { async sendPacket(type,challenge,payload,assemble) {
const challengeLength = (this.noChallenge || challenge === false) ? 0 : 4; const challengeLength = challenge === null ? 0 : 4;
const payloadLength = payload ? payload.length : 0; const payloadLength = payload ? payload.length : 0;
const b = Buffer.alloc(7 + challengeLength + payloadLength); const b = Buffer.alloc(7 + challengeLength + payloadLength);
@ -161,7 +146,7 @@ class Gamespy3 extends Core {
let numPackets = 0; let numPackets = 0;
const packets = {}; const packets = {};
this.udpSend(b,(buffer) => { return this.udpSend(b,(buffer) => {
const reader = this.reader(buffer); const reader = this.reader(buffer);
const iType = reader.uint(1); const iType = reader.uint(1);
if(iType !== type) return; if(iType !== type) return;
@ -169,14 +154,12 @@ class Gamespy3 extends Core {
if(iSessionId !== this.sessionId) return; if(iSessionId !== this.sessionId) return;
if(!assemble) { if(!assemble) {
c(reader.rest()); return reader.rest();
return true;
} }
if(this.useOnlySingleSplit) { if(this.useOnlySingleSplit) {
// has split headers, but they are worthless and only one packet is used // has split headers, but they are worthless and only one packet is used
reader.skip(11); reader.skip(11);
c([reader.rest()]); return [reader.rest()];
return true;
} }
reader.skip(9); // filler data -- usually set to 'splitnum\0' reader.skip(9); // filler data -- usually set to 'splitnum\0'
@ -189,8 +172,7 @@ class Gamespy3 extends Core {
packets[id] = reader.rest(); packets[id] = reader.rest();
if(this.debug) { if(this.debug) {
console.log("Received packet #"+id); this.debugLog("Received packet #"+id + (last ? " (last)" : ""));
if(last) console.log("(last)");
} }
if(!numPackets || Object.keys(packets).length !== numPackets) return; if(!numPackets || Object.keys(packets).length !== numPackets) return;
@ -199,13 +181,11 @@ class Gamespy3 extends Core {
const list = []; const list = [];
for(let i = 0; i < numPackets; i++) { for(let i = 0; i < numPackets; i++) {
if(!(i in packets)) { if(!(i in packets)) {
this.fatal('Missing packet #'+i); throw new Error('Missing packet #'+i);
return true;
} }
list.push(packets[i]); list.push(packets[i]);
} }
c(list); return list;
return true;
}); });
} }
} }

View file

@ -1,16 +1,13 @@
const request = require('request'), const Core = require('./core');
Core = require('./core');
class GeneShift extends Core { class GeneShift extends Core {
run(state) { async run(state) {
request({ const body = await this.request({
uri: 'http://geneshift.net/game/receiveLobby.php', uri: 'http://geneshift.net/game/receiveLobby.php'
timeout: 3000, });
}, (e,r,body) => {
if(e) return this.fatal('Lobby request error');
const split = body.split('<br/>'); const split = body.split('<br/>');
let found = false; let found = null;
for(const line of split) { for(const line of split) {
const fields = line.split('::'); const fields = line.split('::');
const ip = fields[2]; const ip = fields[2];
@ -21,7 +18,9 @@ class GeneShift extends Core {
} }
} }
if(!found) return this.fatal('Server not found in list'); if(found === null) {
throw new Error('Server not found in list');
}
state.raw.countrycode = found[0]; state.raw.countrycode = found[0];
state.raw.country = found[1]; state.raw.country = found[1];
@ -45,9 +44,6 @@ class GeneShift extends Core {
for(let i = 0; i < state.raw.numplayers; i++) { for(let i = 0; i < state.raw.numplayers; i++) {
state.players.push({}); state.players.push({});
} }
this.finish(state);
});
} }
} }

View file

@ -6,6 +6,10 @@ class Hexen2 extends Quake1 {
this.sendHeader = '\xFFstatus\x0a'; this.sendHeader = '\xFFstatus\x0a';
this.responseHeader = '\xffn'; this.responseHeader = '\xffn';
} }
async run(state) {
await super.run(state);
state.gamePort = this.options.port - 50;
}
} }
module.exports = Hexen2; module.exports = Hexen2;

View file

@ -1,14 +1,16 @@
const Gamespy3 = require('./gamespy3'); const Gamespy3 = require('./gamespy3');
// supposedly, gamespy3 is the "official" query protocol for jcmp, // supposedly, gamespy3 is the "official" query protocol for jcmp,
// but it's broken (requires useOnlySingleSplit), and doesn't include player names // but it's broken (requires useOnlySingleSplit), and may not include some player names
class Jc2mp extends Gamespy3 { class Jc2mp extends Gamespy3 {
constructor() { constructor() {
super(); super();
this.useOnlySingleSplit = true; this.useOnlySingleSplit = true;
this.isJc2mp = true;
this.encoding = 'utf8';
} }
finalizeState(state) { async run(state) {
super.finalizeState(state); await super.run(state);
if(!state.players.length && parseInt(state.raw.numplayers)) { if(!state.players.length && parseInt(state.raw.numplayers)) {
for(let i = 0; i < parseInt(state.raw.numplayers); i++) { for(let i = 0; i < parseInt(state.raw.numplayers); i++) {
state.players.push({}); state.players.push({});

View file

@ -1,20 +1,12 @@
const request = require('request'), const Core = require('./core');
Core = require('./core');
class Kspdmp extends Core { class Kspdmp extends Core {
run(state) { async run(state) {
request({ const body = await this.request({
uri: 'http://'+this.options.address+':'+this.options.port_query, uri: 'http://'+this.options.address+':'+this.options.port
timeout: this.options.socketTimeout });
}, (e,r,body) => {
if(e) return this.fatal('HTTP error');
let json;
try {
json = JSON.parse(body);
} catch(e) {
return this.fatal('Invalid JSON');
}
const json = JSON.parse(body);
for (const one of json.players) { for (const one of json.players) {
state.players.push({name:one.nickname,team:one.team}); state.players.push({name:one.nickname,team:one.team});
} }
@ -24,15 +16,13 @@ class Kspdmp extends Core {
} }
state.name = json.server_name; state.name = json.server_name;
state.maxplayers = json.max_players; state.maxplayers = json.max_players;
state.gamePort = json.port;
if (json.players) { if (json.players) {
const split = json.players.split(', '); const split = json.players.split(', ');
for (const name of split) { for (const name of split) {
state.players.push({name:name}); state.players.push({name:name});
} }
} }
this.finish(state);
});
} }
} }

View file

@ -6,18 +6,21 @@ class M2mp extends Core {
this.encoding = 'latin1'; this.encoding = 'latin1';
} }
run(state) { async run(state) {
this.udpSend('M2MP',(buffer) => { const body = await this.udpSend('M2MP',(buffer) => {
const reader = this.reader(buffer); const reader = this.reader(buffer);
const header = reader.string({length: 4}); const header = reader.string({length: 4});
if (header !== 'M2MP') return; if (header !== 'M2MP') return;
return reader.rest();
});
const reader = this.reader(body);
state.name = this.readString(reader); state.name = this.readString(reader);
state.raw.numplayers = this.readString(reader); state.raw.numplayers = this.readString(reader);
state.maxplayers = this.readString(reader); state.maxplayers = this.readString(reader);
state.raw.gamemode = this.readString(reader); state.raw.gamemode = this.readString(reader);
state.password = !!reader.uint(1); state.password = !!reader.uint(1);
state.gamePort = this.options.port - 1;
while(!reader.done()) { while(!reader.done()) {
const name = this.readString(reader); const name = this.readString(reader);
@ -26,10 +29,6 @@ class M2mp extends Core {
name:name name:name
}); });
} }
this.finish(state);
return true;
});
} }
readString(reader) { readString(reader) {

View file

@ -1,81 +1,53 @@
const varint = require('varint'), const Core = require('./core'),
async = require('async'), Varint = require('varint');
Core = require('./core');
function varIntBuffer(num) {
return Buffer.from(varint.encode(num));
}
function buildPacket(id,data) {
if(!data) data = Buffer.from([]);
const idBuffer = varIntBuffer(id);
return Buffer.concat([
varIntBuffer(data.length+idBuffer.length),
idBuffer,
data
]);
}
class Minecraft extends Core { class Minecraft extends Core {
run(state) { constructor() {
/** @type Buffer */ super();
let receivedData; this.srvRecord = "_minecraft._tcp";
}
async.series([ async run(state) {
(c) => {
// build and send handshake and status TCP packet
const portBuf = Buffer.alloc(2); const portBuf = Buffer.alloc(2);
portBuf.writeUInt16BE(this.options.port_query,0); portBuf.writeUInt16BE(this.options.port,0);
const addressBuf = Buffer.from(this.options.address,'utf8'); const addressBuf = Buffer.from(this.options.host,'utf8');
const bufs = [ const bufs = [
varIntBuffer(4), this.varIntBuffer(4),
varIntBuffer(addressBuf.length), this.varIntBuffer(addressBuf.length),
addressBuf, addressBuf,
portBuf, portBuf,
varIntBuffer(1) this.varIntBuffer(1)
]; ];
const outBuffer = Buffer.concat([ const outBuffer = Buffer.concat([
buildPacket(0,Buffer.concat(bufs)), this.buildPacket(0,Buffer.concat(bufs)),
buildPacket(0) this.buildPacket(0)
]); ]);
this.tcpSend(outBuffer, (data) => { const data = await this.withTcp(async socket => {
if(data.length < 10) return false; return await this.tcpSend(socket, outBuffer, data => {
const expected = varint.decode(data); if(data.length < 10) return;
data = data.slice(varint.decode.bytes); const reader = this.reader(data);
if(data.length < expected) return false; const length = reader.varint();
receivedData = data; if(data.length < length) return;
c(); return reader.rest();
return true; });
}); });
},
(c) => {
// parse response
let data = receivedData; const reader = this.reader(data);
const packetId = varint.decode(data);
if(this.debug) console.log("Packet ID: "+packetId);
data = data.slice(varint.decode.bytes);
const strLen = varint.decode(data); const packetId = reader.varint();
if(this.debug) console.log("String Length: "+strLen); this.debugLog("Packet ID: "+packetId);
data = data.slice(varint.decode.bytes);
const str = data.toString('utf8'); const strLen = reader.varint();
if(this.debug) { this.debugLog("String Length: "+strLen);
console.log(str);
}
let json; const str = reader.rest().toString('utf8');
try { this.debugLog(str);
json = JSON.parse(str);
const json = JSON.parse(str);
delete json.favicon; delete json.favicon;
} catch(e) {
return this.fatal('Invalid JSON');
}
state.raw = json; state.raw = json;
state.maxplayers = json.players.max; state.maxplayers = json.players.max;
@ -90,9 +62,18 @@ class Minecraft extends Core {
while(state.players.length < json.players.online) { while(state.players.length < json.players.online) {
state.players.push({}); state.players.push({});
} }
this.finish(state);
} }
varIntBuffer(num) {
return Buffer.from(Varint.encode(num));
}
buildPacket(id,data) {
if(!data) data = Buffer.from([]);
const idBuffer = this.varIntBuffer(id);
return Buffer.concat([
this.varIntBuffer(data.length+idBuffer.length),
idBuffer,
data
]); ]);
} }
} }

View file

@ -1,13 +1,9 @@
const Core = require('./core'); const Core = require('./core');
class Mumble extends Core { class Mumble extends Core {
constructor() { async run(state) {
super(); const json = await this.withTcp(async socket => {
this.options.socketTimeout = 5000; return await this.tcpSend(socket, 'json', (buffer) => {
}
run(state) {
this.tcpSend('json', (buffer) => {
if (buffer.length < 10) return; if (buffer.length < 10) return;
const str = buffer.toString(); const str = buffer.toString();
let json; let json;
@ -17,9 +13,13 @@ class Mumble extends Core {
// probably not all here yet // probably not all here yet
return; return;
} }
return json;
});
});
state.raw = json; state.raw = json;
state.name = json.name; state.name = json.name;
state.gamePort = json.x_gtmurmur_connectport || 64738;
let channelStack = [state.raw.root]; let channelStack = [state.raw.root];
while(channelStack.length) { while(channelStack.length) {
@ -31,10 +31,6 @@ class Mumble extends Core {
state.players.push(user); state.players.push(user);
} }
} }
this.finish(state);
return true;
});
} }
cleanComment(str) { cleanComment(str) {

View file

@ -6,10 +6,12 @@ class MumblePing extends Core {
this.byteorder = 'be'; this.byteorder = 'be';
} }
run(state) { async run(state) {
this.udpSend('\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08', (buffer) => { const data = await this.udpSend('\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08', (buffer) => {
if(buffer.length < 24) return; if (buffer.length >= 24) return buffer;
const reader = this.reader(buffer); });
const reader = this.reader(data);
reader.skip(1); reader.skip(1);
state.raw.versionMajor = reader.uint(1); state.raw.versionMajor = reader.uint(1);
state.raw.versionMinor = reader.uint(1); state.raw.versionMinor = reader.uint(1);
@ -21,9 +23,6 @@ class MumblePing extends Core {
for(let i = 0; i < state.raw.numplayers; i++) { for(let i = 0; i < state.raw.numplayers; i++) {
state.players.push({}); state.players.push({});
} }
this.finish(state);
return true;
});
} }
} }

View file

@ -1,85 +1,97 @@
const gbxremote = require('gbxremote'), const gbxremote = require('gbxremote'),
async = require('async'),
Core = require('./core'); Core = require('./core');
class Nadeo extends Core { class Nadeo extends Core {
constructor() { async run(state) {
super(); await this.withClient(async client => {
this.options.port = 2350; const start = Date.now();
this.options.port_query = 5000; await this.methodCall(client, 'Authenticate', this.options.login, this.options.password);
this.gbxclient = false; this.registerRtt(Date.now()-start);
//const data = this.methodCall(client, 'GetStatus');
{
const results = await this.methodCall(client, 'GetServerOptions');
state.name = this.stripColors(results.Name);
state.password = (results.Password !== 'No password');
state.maxplayers = results.CurrentMaxPlayers;
state.raw.maxspectators = results.CurrentMaxSpectators;
} }
reset() { {
super.reset(); const results = await this.methodCall(client, 'GetCurrentMapInfo');
if(this.gbxclient) { state.map = this.stripColors(results.Name);
this.gbxclient.terminate(); state.raw.mapUid = results.UId;
this.gbxclient = false;
}
} }
run(state) { {
const cmds = [ const results = await this.methodCall(client, 'GetCurrentGameInfo');
['Connect'],
['Authenticate', this.options.login,this.options.password],
['GetStatus'], // 1
['GetPlayerList',10000,0], // 2
['GetServerOptions'], // 3
['GetCurrentMapInfo'], // 4
['GetCurrentGameInfo'], // 5
['GetNextMapInfo'] // 6
];
const results = [];
async.eachSeries(cmds, (cmdset,c) => {
const cmd = cmdset[0];
const params = cmdset.slice(1);
if(cmd === 'Connect') {
const client = this.gbxclient = gbxremote.createClient(this.options.port_query,this.options.host, (err) => {
if(err) return this.fatal('GBX error '+JSON.stringify(err));
c();
});
client.on('error',() => {});
} else {
this.gbxclient.methodCall(cmd, params, (err, value) => {
if(err) return this.fatal('XMLRPC error '+JSON.stringify(err));
results.push(value);
c();
});
}
}, () => {
let gamemode = ''; let gamemode = '';
const igm = results[5].GameMode; const igm = results.GameMode;
if(igm === 0) gamemode="Rounds"; if(igm === 0) gamemode="Rounds";
if(igm === 1) gamemode="Time Attack"; if(igm === 1) gamemode="Time Attack";
if(igm === 2) gamemode="Team"; if(igm === 2) gamemode="Team";
if(igm === 3) gamemode="Laps"; if(igm === 3) gamemode="Laps";
if(igm === 4) gamemode="Stunts"; if(igm === 4) gamemode="Stunts";
if(igm === 5) gamemode="Cup"; if(igm === 5) gamemode="Cup";
state.name = this.stripColors(results[3].Name);
state.password = (results[3].Password !== 'No password');
state.maxplayers = results[3].CurrentMaxPlayers;
state.raw.maxspectators = results[3].CurrentMaxSpectators;
state.map = this.stripColors(results[4].Name);
state.raw.mapUid = results[4].UId;
state.raw.gametype = gamemode; state.raw.gametype = gamemode;
state.raw.players = results[2]; state.raw.mapcount = results.NbChallenge;
state.raw.mapcount = results[5].NbChallenge; }
state.raw.nextmapName = this.stripColors(results[6].Name);
state.raw.nextmapUid = results[6].UId;
{
const results = await this.methodCall(client, 'GetNextMapInfo');
state.raw.nextmapName = this.stripColors(results.Name);
state.raw.nextmapUid = results.UId;
}
if (this.options.port === 5000) {
state.gamePort = 2350;
}
state.raw.players = await this.methodCall(client, 'GetPlayerList', 10000, 0);
for (const player of state.raw.players) { for (const player of state.raw.players) {
state.players.push({ state.players.push({
name:this.stripColors(player.Name || player.NickName) name:this.stripColors(player.Name || player.NickName)
}); });
} }
this.finish(state);
}); });
} }
async withClient(fn) {
const socket = gbxremote.createClient(this.options.port, this.options.host);
const cancelAsyncLeak = this.addCleanup(() => socket.terminate());
try {
await this.timedPromise(
new Promise((resolve,reject) => {
socket.on('connect', resolve);
socket.on('error', e => reject(new Error('GBX Remote Connection Error: ' + e)));
socket.on('close', () => reject(new Error('GBX Remote Connection Refused')));
}),
this.options.socketTimeout,
'GBX Remote Opening'
);
return await fn(socket);
} finally {
cancelAsyncLeak();
socket.terminate();
}
}
async methodCall(client, ...cmdset) {
const cmd = cmdset[0];
const params = cmdset.slice(1);
return await this.timedPromise(
new Promise(async (resolve,reject) => {
client.methodCall(cmd, params, (err, value) => {
if (err) reject('XMLRPC error ' + JSON.stringify(err));
resolve(value);
});
}),
this.options.socketTimeout,
'GBX Method Call'
);
}
stripColors(str) { stripColors(str) {
return str.replace(/\$([0-9a-f]{3}|[a-z])/gi,''); return str.replace(/\$([0-9a-f]{3}|[a-z])/gi,'');
} }

View file

@ -1,12 +1,10 @@
const async = require('async'), const moment = require('moment'),
moment = require('moment'),
Core = require('./core'); Core = require('./core');
class OpenTtd extends Core { class OpenTtd extends Core {
run(state) { async run(state) {
async.series([ {
(c) => { const [reader, version] = await this.query(0, 1, 1, 4);
this.query(0,1,1,4,(reader, version) => {
if (version >= 4) { if (version >= 4) {
const numGrf = reader.uint(1); const numGrf = reader.uint(1);
state.raw.grfs = []; state.raw.grfs = [];
@ -52,19 +50,12 @@ class OpenTtd extends Core {
); );
state.raw.dedicated = !!reader.uint(1); state.raw.dedicated = !!reader.uint(1);
}
c(); {
}); const [reader,version] = await this.query(2,3,-1,-1);
},
(c) => {
const vehicle_types = ['train','truck','bus','aircraft','ship'];
const station_types = ['station','truckbay','busstation','airport','dock'];
this.query(2,3,-1,-1, (reader,version) => {
// we don't know how to deal with companies outside version 6 // we don't know how to deal with companies outside version 6
if(version !== 6) return c(); if(version === 6) {
state.raw.companies = []; state.raw.companies = [];
const numCompanies = reader.uint(1); const numCompanies = reader.uint(1);
for (let iCompany = 0; iCompany < numCompanies; iCompany++) { for (let iCompany = 0; iCompany < numCompanies; iCompany++) {
@ -78,6 +69,9 @@ class OpenTtd extends Core {
company.performance = reader.uint(2); company.performance = reader.uint(2);
company.password = !!reader.uint(1); company.password = !!reader.uint(1);
const vehicle_types = ['train', 'truck', 'bus', 'aircraft', 'ship'];
const station_types = ['station', 'truckbay', 'busstation', 'airport', 'dock'];
company.vehicles = {}; company.vehicles = {};
for (const type of vehicle_types) { for (const type of vehicle_types) {
company.vehicles[type] = reader.uint(2); company.vehicles[type] = reader.uint(2);
@ -90,42 +84,33 @@ class OpenTtd extends Core {
company.clients = reader.string(); company.clients = reader.string();
state.raw.companies.push(company); state.raw.companies.push(company);
} }
c();
});
},
(c) => {
this.finish(state);
} }
]); }
} }
query(type,expected,minver,maxver,done) { async query(type,expected,minver,maxver) {
const b = Buffer.from([0x03,0x00,type]); const b = Buffer.from([0x03,0x00,type]);
this.udpSend(b,(buffer) => { return await this.udpSend(b,(buffer) => {
const reader = this.reader(buffer); const reader = this.reader(buffer);
const packetLen = reader.uint(2); const packetLen = reader.uint(2);
if(packetLen !== buffer.length) { if(packetLen !== buffer.length) {
this.fatal('Invalid reported packet length: '+packetLen+' '+buffer.length); this.debugLog('Invalid reported packet length: '+packetLen+' '+buffer.length);
return true; return;
} }
const packetType = reader.uint(1); const packetType = reader.uint(1);
if(packetType !== expected) { if(packetType !== expected) {
this.fatal('Unexpected response packet type: '+packetType); this.debugLog('Unexpected response packet type: '+packetType);
return true; return;
} }
const protocolVersion = reader.uint(1); const protocolVersion = reader.uint(1);
if((minver !== -1 && protocolVersion < minver) || (maxver !== -1 && protocolVersion > maxver)) { if((minver !== -1 && protocolVersion < minver) || (maxver !== -1 && protocolVersion > maxver)) {
this.fatal('Unknown protocol version: '+protocolVersion+' Expected: '+minver+'-'+maxver); throw new Error('Unknown protocol version: '+protocolVersion+' Expected: '+minver+'-'+maxver);
return true;
} }
done(reader,protocolVersion); return [reader,protocolVersion];
return true;
}); });
} }

View file

@ -10,21 +10,22 @@ class Quake2 extends Core {
this.isQuake1 = false; this.isQuake1 = false;
} }
run(state) { async run(state) {
this.udpSend('\xff\xff\xff\xff'+this.sendHeader+'\x00', (buffer) => { const body = await this.udpSend('\xff\xff\xff\xff'+this.sendHeader+'\x00', packet => {
const reader = this.reader(buffer); const reader = this.reader(packet);
const header = reader.string({length: 4, encoding: 'latin1'}); const header = reader.string({length: 4, encoding: 'latin1'});
if (header !== '\xff\xff\xff\xff') return; if (header !== '\xff\xff\xff\xff') return;
let type;
let response;
if (this.isQuake1) { if (this.isQuake1) {
response = reader.string({length:this.responseHeader.length}); type = reader.string({length: this.responseHeader.length});
} else { } else {
response = reader.string({encoding:'latin1'}); type = reader.string({encoding: 'latin1'});
} }
if(response !== this.responseHeader) return; if (type !== this.responseHeader) return;
return reader.rest();
});
const reader = this.reader(body);
const info = reader.string().split('\\'); const info = reader.string().split('\\');
if(info[0] === '') info.shift(); if(info[0] === '') info.shift();
@ -67,7 +68,9 @@ class Quake2 extends Core {
player.frags = parseInt(args.shift()); player.frags = parseInt(args.shift());
player.ping = parseInt(args.shift()); player.ping = parseInt(args.shift());
player.name = args.shift() || ''; player.name = args.shift() || '';
if (!player.name) delete player.name;
player.address = args.shift() || ''; player.address = args.shift() || '';
if (!player.address) delete player.address;
} }
(player.ping ? state.players : state.bots).push(player); (player.ping ? state.players : state.bots).push(player);
@ -79,10 +82,6 @@ class Quake2 extends Core {
if('maxclients' in state.raw) state.maxplayers = state.raw.maxclients; if('maxclients' in state.raw) state.maxplayers = state.raw.maxclients;
if('sv_hostname' in state.raw) state.name = state.raw.sv_hostname; if('sv_hostname' in state.raw) state.name = state.raw.sv_hostname;
if('hostname' in state.raw) state.name = state.raw.hostname; if('hostname' in state.raw) state.name = state.raw.hostname;
this.finish(state);
return true;
});
} }
} }

View file

@ -6,7 +6,8 @@ class Quake3 extends Quake2 {
this.sendHeader = 'getstatus'; this.sendHeader = 'getstatus';
this.responseHeader = 'statusResponse'; this.responseHeader = 'statusResponse';
} }
finalizeState(state) { async run(state) {
await super.run(state);
state.name = this.stripColors(state.name); state.name = this.stripColors(state.name);
for(const key of Object.keys(state.raw)) { for(const key of Object.keys(state.raw)) {
state.raw[key] = this.stripColors(state.raw[key]); state.raw[key] = this.stripColors(state.raw[key]);

View file

@ -1,5 +1,4 @@
const async = require('async'), const Core = require('./core');
Core = require('./core');
class Samp extends Core { class Samp extends Core {
constructor() { constructor() {
@ -7,21 +6,21 @@ class Samp extends Core {
this.encoding = 'win1252'; this.encoding = 'win1252';
} }
run(state) { async run(state) {
async.series([ // read info
(c) => { {
this.sendPacket('i',(reader) => { const reader = await this.sendPacket('i');
state.password = !!reader.uint(1); state.password = !!reader.uint(1);
state.raw.numplayers = reader.uint(2); state.raw.numplayers = reader.uint(2);
state.maxplayers = reader.uint(2); state.maxplayers = reader.uint(2);
state.name = this.readString(reader,4); state.name = this.readString(reader,4);
state.raw.gamemode = this.readString(reader,4); state.raw.gamemode = this.readString(reader,4);
this.map = this.readString(reader,4); this.map = this.readString(reader,4);
c(); }
});
}, // read rules
(c) => { {
this.sendPacket('r',(reader) => { const reader = await this.sendPacket('r');
const ruleCount = reader.uint(2); const ruleCount = reader.uint(2);
state.raw.rules = {}; state.raw.rules = {};
for(let i = 0; i < ruleCount; i++) { for(let i = 0; i < ruleCount; i++) {
@ -31,11 +30,12 @@ class Samp extends Core {
} }
if('mapname' in state.raw.rules) if('mapname' in state.raw.rules)
state.map = state.raw.rules.mapname; state.map = state.raw.rules.mapname;
c(); }
});
}, // read players
(c) => { {
this.sendPacket('d',(reader) => { const reader = await this.sendPacket('d', true);
if (reader !== null) {
const playerCount = reader.uint(2); const playerCount = reader.uint(2);
for(let i = 0; i < playerCount; i++) { for(let i = 0; i < playerCount; i++) {
const player = {}; const player = {};
@ -45,49 +45,44 @@ class Samp extends Core {
player.ping = reader.uint(4); player.ping = reader.uint(4);
state.players.push(player); state.players.push(player);
} }
c(); } else {
},() => {
for(let i = 0; i < state.raw.numplayers; i++) { for(let i = 0; i < state.raw.numplayers; i++) {
state.players.push({}); state.players.push({});
} }
c();
});
},
(c) => {
this.finish(state);
} }
]); }
} }
readString(reader,lenBytes) { readString(reader,lenBytes) {
const length = reader.uint(lenBytes); const length = reader.uint(lenBytes);
if(!length) return ''; if(!length) return '';
const string = reader.string({length:length}); return reader.string({length:length});
return string;
} }
sendPacket(type,onresponse,ontimeout) { async sendPacket(type,allowTimeout) {
const outbuffer = Buffer.alloc(11); const outBuffer = Buffer.alloc(11);
outbuffer.writeUInt32BE(0x53414D50,0); outBuffer.writeUInt32BE(0x53414D50,0);
const ipSplit = this.options.address.split('.'); const ipSplit = this.options.address.split('.');
outbuffer.writeUInt8(parseInt(ipSplit[0]),4); outBuffer.writeUInt8(parseInt(ipSplit[0]),4);
outbuffer.writeUInt8(parseInt(ipSplit[1]),5); outBuffer.writeUInt8(parseInt(ipSplit[1]),5);
outbuffer.writeUInt8(parseInt(ipSplit[2]),6); outBuffer.writeUInt8(parseInt(ipSplit[2]),6);
outbuffer.writeUInt8(parseInt(ipSplit[3]),7); outBuffer.writeUInt8(parseInt(ipSplit[3]),7);
outbuffer.writeUInt16LE(this.options.port,8); outBuffer.writeUInt16LE(this.options.port,8);
outbuffer.writeUInt8(type.charCodeAt(0),10); outBuffer.writeUInt8(type.charCodeAt(0),10);
this.udpSend(outbuffer,(buffer) => { return await this.udpSend(
outBuffer,
(buffer) => {
const reader = this.reader(buffer); const reader = this.reader(buffer);
for(let i = 0; i < outbuffer.length; i++) { for(let i = 0; i < outBuffer.length; i++) {
if(outbuffer.readUInt8(i) !== reader.uint(1)) return; if(outBuffer.readUInt8(i) !== reader.uint(1)) return;
} }
onresponse(reader); return reader;
return true; },
},() => { () => {
if(ontimeout) { if(allowTimeout) {
ontimeout(); return null;
return true;
} }
}); }
);
} }
} }

View file

@ -6,15 +6,21 @@ class Starmade extends Core {
this.encoding = 'latin1'; this.encoding = 'latin1';
this.byteorder = 'be'; this.byteorder = 'be';
} }
run(state) {
async run(state) {
const b = Buffer.from([0x00,0x00,0x00,0x09,0x2a,0xff,0xff,0x01,0x6f,0x00,0x00,0x00,0x00]); const b = Buffer.from([0x00,0x00,0x00,0x09,0x2a,0xff,0xff,0x01,0x6f,0x00,0x00,0x00,0x00]);
this.tcpSend(b,(buffer) => { const payload = await this.withTcp(async socket => {
return await this.tcpSend(socket, b, buffer => {
if (buffer.length < 4) return;
const reader = this.reader(buffer); const reader = this.reader(buffer);
if(buffer.length < 4) return false;
const packetLength = reader.uint(4); const packetLength = reader.uint(4);
if(buffer.length < packetLength+12) return false; if (buffer.length < packetLength + 12) return;
return reader.rest();
});
});
const reader = this.reader(payload);
const data = []; const data = [];
state.raw.data = data; state.raw.data = data;
@ -39,8 +45,7 @@ class Starmade extends Core {
} }
if(data.length < 9) { if(data.length < 9) {
this.fatal("Not enough units in data packet"); throw new Error("Not enough units in data packet");
return true;
} }
if(typeof data[3] === 'number') state.raw.version = data[3].toFixed(7).replace(/0+$/, ''); if(typeof data[3] === 'number') state.raw.version = data[3].toFixed(7).replace(/0+$/, '');
@ -54,10 +59,6 @@ class Starmade extends Core {
state.players.push({}); state.players.push({});
} }
} }
this.finish(state);
return true;
});
} }
} }

View file

@ -1,28 +1,27 @@
const async = require('async'), const Core = require('./core');
Core = require('./core');
class Teamspeak2 extends Core { class Teamspeak2 extends Core {
run(state) { async run(state) {
async.series([ const queryPort = this.options.teamspeakQueryPort || 51234;
(c) => {
this.sendCommand('sel '+this.options.port, (data) => { await this.withTcp(async socket => {
if(data !== '[TS]') this.fatal('Invalid header'); {
c(); const data = await this.sendCommand(socket, 'sel '+this.options.port);
}); if(data !== '[TS]') throw new Error('Invalid header');
}, }
(c) => {
this.sendCommand('si', (data) => { {
const data = await this.sendCommand(socket, 'si');
for (const line of data.split('\r\n')) { for (const line of data.split('\r\n')) {
const equals = line.indexOf('='); const equals = line.indexOf('=');
const key = equals === -1 ? line : line.substr(0,equals); const key = equals === -1 ? line : line.substr(0,equals);
const value = equals === -1 ? '' : line.substr(equals+1); const value = equals === -1 ? '' : line.substr(equals+1);
state.raw[key] = value; state.raw[key] = value;
} }
c(); }
});
}, {
(c) => { const data = await this.sendCommand(socket, 'pl');
this.sendCommand('pl', (data) => {
const split = data.split('\r\n'); const split = data.split('\r\n');
const fields = split.shift().split('\t'); const fields = split.shift().split('\t');
for (const line of split) { for (const line of split) {
@ -38,11 +37,10 @@ class Teamspeak2 extends Core {
}); });
state.players.push(player); state.players.push(player);
} }
c(); }
});
}, {
(c) => { const data = await this.sendCommand(socket, 'cl');
this.sendCommand('cl', (data) => {
const split = data.split('\r\n'); const split = data.split('\r\n');
const fields = split.shift().split('\t'); const fields = split.shift().split('\t');
state.raw.channels = []; state.raw.channels = [];
@ -58,20 +56,15 @@ class Teamspeak2 extends Core {
}); });
state.raw.channels.push(channel); state.raw.channels.push(channel);
} }
c();
});
},
(c) => {
this.finish(state);
} }
]); }, queryPort);
} }
sendCommand(cmd,c) {
this.tcpSend(cmd+'\x0A', (buffer) => { async sendCommand(socket,cmd) {
return await this.tcpSend(socket, cmd+'\x0A', buffer => {
if(buffer.length < 6) return; if(buffer.length < 6) return;
if(buffer.slice(-6).toString() !== '\r\nOK\r\n') return; if(buffer.slice(-6).toString() !== '\r\nOK\r\n') return;
c(buffer.slice(0,-6).toString()); return buffer.slice(0,-6).toString();
return true;
}); });
} }
} }

View file

@ -1,26 +1,25 @@
const async = require('async'), const Core = require('./core');
Core = require('./core');
class Teamspeak3 extends Core { class Teamspeak3 extends Core {
run(state) { async run(state) {
async.series([ const queryPort = this.options.teamspeakQueryPort || 10011;
(c) => {
this.sendCommand('use port='+this.options.port, (data) => { await this.withTcp(async socket => {
{
const data = await this.sendCommand(socket, 'use port='+this.options.port, true);
const split = data.split('\n\r'); const split = data.split('\n\r');
if(split[0] !== 'TS3') this.fatal('Invalid header'); if(split[0] !== 'TS3') throw new Error('Invalid header');
c(); }
}, true);
}, {
(c) => { const data = await this.sendCommand(socket, 'serverinfo');
this.sendCommand('serverinfo', (data) => {
state.raw = data[0]; state.raw = data[0];
if('virtualserver_name' in state.raw) state.name = state.raw.virtualserver_name; if('virtualserver_name' in state.raw) state.name = state.raw.virtualserver_name;
if('virtualserver_maxclients' in state.raw) state.maxplayers = state.raw.virtualserver_maxclients; if('virtualserver_maxclients' in state.raw) state.maxplayers = state.raw.virtualserver_maxclients;
c(); }
});
}, {
(c) => { const list = await this.sendCommand(socket, 'clientlist');
this.sendCommand('clientlist', (list) => {
for (const client of list) { for (const client of list) {
client.name = client.client_nickname; client.name = client.client_nickname;
delete client.client_nickname; delete client.client_nickname;
@ -28,33 +27,27 @@ class Teamspeak3 extends Core {
state.players.push(client); state.players.push(client);
} }
} }
c(); }
});
}, {
(c) => { const data = await this.sendCommand(socket, 'channellist -topic');
this.sendCommand('channellist -topic', (data) => {
state.raw.channels = data; state.raw.channels = data;
c();
});
},
(c) => {
this.finish(state);
} }
]); }, queryPort);
} }
sendCommand(cmd,c,raw) {
this.tcpSend(cmd+'\x0A', (buffer) => { async sendCommand(socket,cmd,raw) {
const body = await this.tcpSend(socket, cmd+'\x0A', (buffer) => {
if (buffer.length < 21) return; if (buffer.length < 21) return;
if (buffer.slice(-21).toString() !== '\n\rerror id=0 msg=ok\n\r') return; if (buffer.slice(-21).toString() !== '\n\rerror id=0 msg=ok\n\r') return;
const body = buffer.slice(0,-21).toString(); return buffer.slice(0, -21).toString();
});
let out;
if(raw) { if(raw) {
out = body; return body;
} else { } else {
const segments = body.split('|'); const segments = body.split('|');
out = []; const out = [];
for (const line of segments) { for (const line of segments) {
const split = line.split(' '); const split = line.split(' ');
const unit = {}; const unit = {};
@ -67,12 +60,8 @@ class Teamspeak3 extends Core {
} }
out.push(unit); out.push(unit);
} }
return out;
} }
c(out);
return true;
});
} }
} }

View file

@ -1,36 +1,25 @@
const request = require('request'), const Core = require('./core');
Core = require('./core');
class Terraria extends Core { class Terraria extends Core {
run(state) { async run(state) {
request({ const body = await this.request({
uri: 'http://'+this.options.address+':'+this.options.port_query+'/v2/server/status', uri: 'http://'+this.options.address+':'+this.options.port+'/v2/server/status',
timeout: this.options.socketTimeout,
qs: { qs: {
players: 'true', players: 'true',
token: this.options.token token: this.options.token
} }
}, (e,r,body) => { });
if(e) return this.fatal('HTTP error');
let json;
try {
json = JSON.parse(body);
} catch(e) {
return this.fatal('Invalid JSON');
}
if(json.status !== 200) return this.fatal('Invalid status'); const json = JSON.parse(body);
if(json.status !== 200) throw new Error('Invalid status');
for (const one of json.players) { for (const one of json.players) {
state.players.push({name:one.nickname,team:one.team}); state.players.push({name:one.nickname,team:one.team});
} }
state.name = json.name; state.name = json.name;
state.raw.port = json.port; state.gamePort = json.port;
state.raw.numplayers = json.playercount; state.raw.numplayers = json.playercount;
this.finish(state);
});
} }
} }

View file

@ -5,15 +5,19 @@ class Tribes1 extends Core {
super(); super();
this.encoding = 'latin1'; this.encoding = 'latin1';
} }
run(state) {
async run(state) {
const queryBuffer = Buffer.from('b++'); const queryBuffer = Buffer.from('b++');
this.udpSend(queryBuffer,(buffer) => { const reader = await this.udpSend(queryBuffer,(buffer) => {
const reader = this.reader(buffer); const reader = this.reader(buffer);
const header = reader.string({length: 4}); const header = reader.string({length: 4});
if (header !== 'c++b') { if (header !== 'c++b') {
this.fatal('Header response does not match: ' + header); this.debugLog('Header response does not match: ' + header);
return true; return;
} }
return reader;
});
state.raw.gametype = this.readString(reader); state.raw.gametype = this.readString(reader);
state.raw.version = this.readString(reader); state.raw.version = this.readString(reader);
state.name = this.readString(reader); state.name = this.readString(reader);
@ -76,10 +80,6 @@ class Tribes1 extends Core {
} }
state.players.push(playerInfo); state.players.push(playerInfo);
} }
this.finish(state);
return true;
});
} }
readFieldList(reader) { readFieldList(reader) {
const str = this.readString(reader); const str = this.readString(reader);

View file

@ -7,7 +7,8 @@ class Tribes1Master extends Core {
super(); super();
this.encoding = 'latin1'; this.encoding = 'latin1';
} }
run(state) {
async run(state) {
const queryBuffer = Buffer.from([ const queryBuffer = Buffer.from([
0x10, // standard header 0x10, // standard header
0x03, // dump servers 0x03, // dump servers
@ -18,28 +19,27 @@ class Tribes1Master extends Core {
let parts = new Map(); let parts = new Map();
let total = 0; let total = 0;
this.udpSend(queryBuffer,(buffer) => { const full = await this.udpSend(queryBuffer,(buffer) => {
const reader = this.reader(buffer); const reader = this.reader(buffer);
const header = reader.uint(2); const header = reader.uint(2);
if (header !== 0x0610) { if (header !== 0x0610) {
this.fatal('Header response does not match: ' + header.toString(16)); this.debugLog('Header response does not match: ' + header.toString(16));
return true; return;
} }
const num = reader.uint(1); const num = reader.uint(1);
const t = reader.uint(1); const t = reader.uint(1);
if (t <= 0 || (total > 0 && t !== total)) { if (t <= 0 || (total > 0 && t !== total)) {
this.fatal('Conflicting total: ' + t); throw new Error('Conflicting packet total: ' + t);
return true;
} }
total = t; total = t;
if (num < 1 || num > total) { if (num < 1 || num > total) {
this.fatal('Invalid packet number: ' + num + ' ' + total); this.debugLog('Invalid packet number: ' + num + ' ' + total);
return true; return;
} }
if (parts.has(num)) { if (parts.has(num)) {
this.fatal('Duplicate part: ' + num); this.debugLog('Duplicate part: ' + num);
return true; return;
} }
reader.skip(2); // challenge (0x0201) reader.skip(2); // challenge (0x0201)
@ -49,9 +49,11 @@ class Tribes1Master extends Core {
if (parts.size === total) { if (parts.size === total) {
const ordered = []; const ordered = [];
for (let i = 1; i <= total; i++) ordered.push(parts.get(i)); for (let i = 1; i <= total; i++) ordered.push(parts.get(i));
const full = Buffer.concat(ordered); return Buffer.concat(ordered);
const fullReader = this.reader(full); }
});
const fullReader = this.reader(full);
state.raw.name = this.readString(fullReader); state.raw.name = this.readString(fullReader);
state.raw.motd = this.readString(fullReader); state.raw.motd = this.readString(fullReader);
@ -62,8 +64,7 @@ class Tribes1Master extends Core {
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const six = fullReader.uint(1); const six = fullReader.uint(1);
if (six !== 6) { if (six !== 6) {
this.fatal('Expecting 6'); throw new Error('Expecting 6');
return true;
} }
const ip = fullReader.uint(4); const ip = fullReader.uint(4);
const port = fullReader.uint(2); const port = fullReader.uint(2);
@ -71,10 +72,6 @@ class Tribes1Master extends Core {
state.raw.servers.push(ipStr+":"+port); state.raw.servers.push(ipStr+":"+port);
} }
} }
this.finish(state);
return true;
}
});
} }
readString(reader) { readString(reader) {
const length = reader.uint(1); const length = reader.uint(1);

View file

@ -1,19 +1,17 @@
const async = require('async'), const Core = require('./core');
Core = require('./core');
class Unreal2 extends Core { class Unreal2 extends Core {
constructor() { constructor() {
super(); super();
this.encoding = 'latin1'; this.encoding = 'latin1';
} }
run(state) { async run(state) {
async.series([ {
(c) => { const b = await this.sendPacket(0, true);
this.sendPacket(0,true,(b) => {
const reader = this.reader(b); const reader = this.reader(b);
state.raw.serverid = reader.uint(4); state.raw.serverid = reader.uint(4);
state.raw.ip = this.readUnrealString(reader); state.raw.ip = this.readUnrealString(reader);
state.raw.port = reader.uint(4); state.gamePort = reader.uint(4);
state.raw.queryport = reader.uint(4); state.raw.queryport = reader.uint(4);
state.name = this.readUnrealString(reader, true); state.name = this.readUnrealString(reader, true);
state.map = this.readUnrealString(reader, true); state.map = this.readUnrealString(reader, true);
@ -21,12 +19,10 @@ class Unreal2 extends Core {
state.raw.numplayers = reader.uint(4); state.raw.numplayers = reader.uint(4);
state.maxplayers = reader.uint(4); state.maxplayers = reader.uint(4);
this.readExtraInfo(reader, state); this.readExtraInfo(reader, state);
}
c(); {
}); const b = await this.sendPacket(1,true);
},
(c) => {
this.sendPacket(1,true,(b) => {
const reader = this.reader(b); const reader = this.reader(b);
state.raw.mutators = []; state.raw.mutators = [];
state.raw.rules = {}; state.raw.rules = {};
@ -36,15 +32,12 @@ class Unreal2 extends Core {
if(key === 'Mutator') state.raw.mutators.push(value); if(key === 'Mutator') state.raw.mutators.push(value);
else state.raw.rules[key] = value; else state.raw.rules[key] = value;
} }
if('GamePassword' in state.raw.rules) if('GamePassword' in state.raw.rules)
state.password = state.raw.rules.GamePassword !== 'True'; state.password = state.raw.rules.GamePassword !== 'True';
}
c(); {
}); const b = await this.sendPacket(2,false);
},
(c) => {
this.sendPacket(2,false,(b) => {
const reader = this.reader(b); const reader = this.reader(b);
while(!reader.done()) { while(!reader.done()) {
@ -78,24 +71,20 @@ class Unreal2 extends Core {
(player.ping ? state.players : state.bots).push(player); (player.ping ? state.players : state.bots).push(player);
} }
c();
});
},
(c) => {
this.finish(state);
} }
]);
} }
readExtraInfo(reader,state) { readExtraInfo(reader,state) {
if(this.debug) { this.debugLog(log => {
console.log("UNREAL2 EXTRA INFO:"); log("UNREAL2 EXTRA INFO:");
console.log(reader.uint(4)); log(reader.uint(4));
console.log(reader.uint(4)); log(reader.uint(4));
console.log(reader.uint(4)); log(reader.uint(4));
console.log(reader.uint(4)); log(reader.uint(4));
console.log(reader.buffer.slice(reader.i)); log(reader.buffer.slice(reader.i));
} });
} }
readUnrealString(reader, stripColor) { readUnrealString(reader, stripColor) {
let length = reader.uint(1); let length = reader.uint(1);
let out; let out;
@ -105,10 +94,10 @@ class Unreal2 extends Core {
if(length > 0) out = reader.string(); if(length > 0) out = reader.string();
} else { } else {
length = (length&0x7f)*2; length = (length&0x7f)*2;
if(this.debug) { this.debugLog(log => {
console.log("UCS2 STRING"); log("UCS2 STRING");
console.log(length,reader.buffer.slice(reader.i,reader.i+length)); log(length,reader.buffer.slice(reader.i,reader.i+length));
} });
out = reader.string({encoding:'ucs2',length:length}); out = reader.string({encoding:'ucs2',length:length});
} }
@ -120,11 +109,12 @@ class Unreal2 extends Core {
return out; return out;
} }
sendPacket(type,required,callback) {
async sendPacket(type,required) {
const outbuffer = Buffer.from([0x79,0,0,0,type]); const outbuffer = Buffer.from([0x79,0,0,0,type]);
const packets = []; const packets = [];
this.udpSend(outbuffer,(buffer) => { return await this.udpSend(outbuffer,(buffer) => {
const reader = this.reader(buffer); const reader = this.reader(buffer);
const header = reader.uint(4); const header = reader.uint(4);
const iType = reader.uint(1); const iType = reader.uint(1);
@ -132,8 +122,7 @@ class Unreal2 extends Core {
packets.push(reader.rest()); packets.push(reader.rest());
}, () => { }, () => {
if(!packets.length && required) return; if(!packets.length && required) return;
callback(Buffer.concat(packets)); return Buffer.concat(packets);
return true;
}); });
} }
} }

View file

@ -1,8 +1,8 @@
const Gamespy3 = require('./gamespy3'); const Gamespy3 = require('./gamespy3');
class Ut3 extends Gamespy3 { class Ut3 extends Gamespy3 {
finalizeState(state) { async run(state) {
super.finalizeState(state); await super.run(state);
this.translate(state.raw,{ this.translate(state.raw,{
'mapname': false, 'mapname': false,

View file

@ -1,13 +1,10 @@
const async = require('async'), const Bzip2 = require('compressjs').Bzip2,
Bzip2 = require('compressjs').Bzip2,
Core = require('./core'); Core = require('./core');
class Valve extends Core { class Valve extends Core {
constructor() { constructor() {
super(); super();
this.options.port = 27015;
// legacy goldsrc info response -- basically not used by ANYTHING now, // legacy goldsrc info response -- basically not used by ANYTHING now,
// as most (all?) goldsrc servers respond with the source info reponse // as most (all?) goldsrc servers respond with the source info reponse
// delete in a few years if nothing ends up using it anymore // delete in a few years if nothing ends up using it anymore
@ -28,22 +25,25 @@ class Valve extends Core {
this._challenge = ''; this._challenge = '';
} }
run(state) { async run(state) {
async.series([ if (!this.options.port) this.options.port = 27015;
(c) => { this.queryInfo(state,c); }, await this.queryInfo(state);
(c) => { this.queryChallenge(state,c); }, await this.queryChallenge();
(c) => { this.queryPlayers(state,c); }, await this.queryPlayers(state);
(c) => { this.queryRules(state,c); }, await this.queryRules(state);
(c) => { this.cleanup(state,c); }, await this.cleanup(state);
(c) => { this.finish(state); }
]);
} }
queryInfo(state,c) { async queryInfo(state) {
this.sendPacket( this.debugLog("Requesting info ...");
0x54,false,'Source Engine Query\0', const b = await this.sendPacket(
0x54,
false,
'Source Engine Query\0',
this.goldsrcInfo ? 0x6D : 0x49, this.goldsrcInfo ? 0x6D : 0x49,
(b) => { false
);
const reader = this.reader(b); const reader = this.reader(b);
if(this.goldsrcInfo) state.raw.address = reader.string(); if(this.goldsrcInfo) state.raw.address = reader.string();
@ -92,7 +92,7 @@ class Valve extends Core {
} }
state.raw.version = reader.string(); state.raw.version = reader.string();
const extraFlag = reader.uint(1); const extraFlag = reader.uint(1);
if(extraFlag & 0x80) state.raw.port = reader.uint(2); 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);
if(extraFlag & 0x40) { if(extraFlag & 0x40) {
state.raw.sourcetvport = reader.uint(2); state.raw.sourcetvport = reader.uint(2);
@ -113,35 +113,46 @@ class Valve extends Core {
) { ) {
this._skipSizeInSplitHeader = true; this._skipSizeInSplitHeader = true;
} }
if(this.debug) { this.debugLog("STEAM APPID: "+state.raw.steamappid);
console.log("STEAM APPID: "+state.raw.steamappid); this.debugLog("PROTOCOL: "+state.raw.protocol);
console.log("PROTOCOL: "+state.raw.protocol);
}
if(state.raw.protocol === 48) { if(state.raw.protocol === 48) {
if(this.debug) console.log("GOLDSRC DETECTED - USING MODIFIED SPLIT FORMAT"); this.debugLog("GOLDSRC DETECTED - USING MODIFIED SPLIT FORMAT");
this.goldsrcSplits = true; this.goldsrcSplits = true;
} }
c();
}
);
} }
queryChallenge(state,c) { async queryChallenge() {
if(this.legacyChallenge) { if(this.legacyChallenge) {
this.sendPacket(0x57,false,null,0x41,(b) => {
// sendPacket will catch the response packet and // sendPacket will catch the response packet and
// save the challenge for us // save the challenge for us
c(); this.debugLog("Requesting legacy challenge key ...");
}); await this.sendPacket(
} else { 0x57,
c(); false,
null,
0x41,
false
);
} }
} }
queryPlayers(state,c) { async queryPlayers(state) {
state.raw.players = []; state.raw.players = [];
this.sendPacket(0x55,true,null,0x44,(b) => {
// 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
);
if (b === null) return; // timed out
const reader = this.reader(b); const reader = this.reader(b);
const num = reader.uint(1); const num = reader.uint(1);
for(let i = 0; i < num; i++) { for(let i = 0; i < num; i++) {
@ -150,7 +161,7 @@ class Valve extends Core {
const score = reader.int(4); const score = reader.int(4);
const time = reader.float(); const time = reader.float();
if(this.debug) console.log("Found player: "+name+" "+score+" "+time); this.debugLog("Found player: "+name+" "+score+" "+time);
// connecting players don't count as players. // connecting players don't count as players.
if(!name) continue; if(!name) continue;
@ -162,21 +173,14 @@ class Valve extends Core {
name:name, score:score, time:time name:name, score:score, time:time
}); });
} }
c();
}, () => {
// CSGO doesn't even respond sometimes if host_players_show is not 2
// Ignore timeouts in only this case
if (state.raw.steamappid === 730) {
c();
return true;
}
});
} }
queryRules(state,c) { async queryRules(state) {
state.raw.rules = {}; state.raw.rules = {};
this.sendPacket(0x56,true,null,0x45,(b) => { 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 reader = this.reader(b); const reader = this.reader(b);
const num = reader.uint(2); const num = reader.uint(2);
for(let i = 0; i < num; i++) { for(let i = 0; i < num; i++) {
@ -184,17 +188,9 @@ class Valve extends Core {
const value = reader.string(); const value = reader.string();
state.raw.rules[key] = value; state.raw.rules[key] = value;
} }
c();
}, () => {
// no rules were returned after timeout --
// the server probably has them disabled
// ignore the timeout
c();
return true;
});
} }
cleanup(state,c) { async cleanup(state) {
// Battalion 1944 puts its info into rules fields for some reason // Battalion 1944 puts its info into rules fields for some reason
if ('bat_name_s' in state.raw.rules) { if ('bat_name_s' in state.raw.rules) {
state.name = state.raw.rules.bat_name_s; state.name = state.raw.rules.bat_name_s;
@ -234,62 +230,101 @@ class Valve extends Core {
if (sortedPlayers.length) state.players.push(sortedPlayers.pop()); if (sortedPlayers.length) state.players.push(sortedPlayers.pop());
else state.players.push({}); else state.players.push({});
} }
c();
} }
/** /**
* Sends a request packet and returns only the response type expected
* @param {number} type * @param {number} type
* @param {boolean} sendChallenge * @param {boolean} sendChallenge
* @param {?string|Buffer} payload * @param {?string|Buffer} payload
* @param {number} expect * @param {number} expect
* @param {function(Buffer)} callback * @param {boolean=} allowTimeout
* @param {(function():boolean)=} ontimeout * @returns Buffer|null
**/ **/
sendPacket( async sendPacket(
type, type,
sendChallenge, sendChallenge,
payload, payload,
expect, expect,
callback, allowTimeout
ontimeout
) { ) {
const packetStorage = {}; for (let keyRetry = 0; keyRetry < 3; keyRetry++) {
let requestKeyChanged = false;
const receivedFull = (reader) => { const response = await this.sendPacketRaw(
type, sendChallenge, payload,
(payload) => {
const reader = this.reader(payload);
const type = reader.uint(1); const type = reader.uint(1);
this.debugLog(() => "Received " + type.toString(16) + " expected " + expect.toString(16));
if (type === 0x41) { if (type === 0x41) {
const key = reader.uint(4); const key = reader.uint(4);
if(this.debug) console.log('Received challenge key: ' + key);
if (this._challenge !== key) { if (this._challenge !== key) {
this.debugLog('Received new challenge key: ' + key);
this._challenge = key; this._challenge = key;
if (sendChallenge) { if (sendChallenge) {
if (this.debug) console.log('Restarting query'); this.debugLog('Challenge key changed -- allowing query retry if needed');
send(); requestKeyChanged = true;
return true;
} }
} }
}
return; if (type === expect) {
return reader.rest();
} else if (requestKeyChanged) {
return null;
}
},
() => {
if (allowTimeout) return null;
}
);
if (!requestKeyChanged) {
return response;
}
}
throw new Error('Received too many challenge key responses');
} }
if(this.debug) console.log("Received "+type.toString(16)+" expected "+expect.toString(16)); /**
if(type !== expect) return; * Sends a request packet and assembles partial responses
callback(reader.rest()); * @param {number} type
return true; * @param {boolean} sendChallenge
}; * @param {?string|Buffer} payload
* @param {function(Buffer)} onResponse
* @param {function()} onTimeout
**/
async sendPacketRaw(
type,
sendChallenge,
payload,
onResponse,
onTimeout
) {
if (typeof payload === 'string') payload = Buffer.from(payload, 'binary');
const challengeLength = sendChallenge ? 4 : 0;
const payloadLength = payload ? payload.length : 0;
const receivedOne = (buffer) => { const b = Buffer.alloc(5 + challengeLength + payloadLength);
b.writeInt32LE(-1, 0);
b.writeUInt8(type, 4);
if (sendChallenge) {
let challenge = this._challenge;
if (!challenge) challenge = 0xffffffff;
if (this.byteorder === 'le') b.writeUInt32LE(challenge, 5);
else b.writeUInt32BE(challenge, 5);
}
if (payloadLength) payload.copy(b, 5 + challengeLength);
const packetStorage = {};
return await this.udpSend(
b,
(buffer) => {
const reader = this.reader(buffer); const reader = this.reader(buffer);
const header = reader.int(4); const header = reader.int(4);
if(header === -1) { if(header === -1) {
// full package // full package
if(this.debug) console.log("Received full packet"); this.debugLog("Received full packet");
return receivedFull(reader); return onResponse(reader.rest());
} }
if(header === -2) { if(header === -2) {
// partial package // partial package
@ -316,10 +351,8 @@ class Valve extends Core {
packets[packetNum] = payload; packets[packetNum] = payload;
if(this.debug) { this.debugLog(() => "Received partial packet uid:"+uid+" num:"+packetNum);
console.log("Received partial packet uid:"+uid+" num:"+packetNum); this.debugLog(() => "Received "+Object.keys(packets).length+'/'+numPackets+" packets for this UID");
console.log("Received "+Object.keys(packets).length+'/'+numPackets+" packets for this UID");
}
if(Object.keys(packets).length !== numPackets) return; if(Object.keys(packets).length !== numPackets) return;
@ -327,49 +360,27 @@ class Valve extends Core {
const list = []; const list = [];
for(let i = 0; i < numPackets; i++) { for(let i = 0; i < numPackets; i++) {
if(!(i in packets)) { if(!(i in packets)) {
this.fatal('Missing packet #'+i); throw new Error('Missing packet #'+i);
return true;
} }
list.push(packets[i]); list.push(packets[i]);
} }
let assembled = Buffer.concat(list); let assembled = Buffer.concat(list);
if(bzip) { if(bzip) {
if(this.debug) console.log("BZIP DETECTED - Extracing packet..."); this.debugLog("BZIP DETECTED - Extracing packet...");
try { try {
assembled = Buffer.from(Bzip2.decompressFile(assembled)); assembled = Buffer.from(Bzip2.decompressFile(assembled));
} catch(e) { } catch(e) {
this.fatal('Invalid bzip packet'); throw new Error('Invalid bzip packet');
return true;
} }
} }
const assembledReader = this.reader(assembled); const assembledReader = this.reader(assembled);
assembledReader.skip(4); // header assembledReader.skip(4); // header
return receivedFull(assembledReader); return onResponse(assembledReader.rest());
} }
}; },
onTimeout
const send = (c) => { );
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);
if(sendChallenge) {
let challenge = this._challenge;
if(!challenge) challenge = 0xffffffff;
if(this.byteorder === 'le') b.writeUInt32LE(challenge, 5);
else b.writeUInt32BE(challenge, 5);
}
if(payloadLength) payload.copy(b, 5+challengeLength);
this.udpSend(b,receivedOne,ontimeout);
};
send();
} }
} }

View file

@ -5,8 +5,9 @@ class Ventrilo extends Core {
super(); super();
this.byteorder = 'be'; this.byteorder = 'be';
} }
run(state) {
this.sendCommand(2,'',(data) => { async run(state) {
const data = await this.sendCommand(2,'');
state.raw = splitFields(data.toString()); state.raw = splitFields(data.toString());
for (const client of state.raw.CLIENTS) { for (const client of state.raw.CLIENTS) {
client.name = client.NAME; client.name = client.NAME;
@ -20,16 +21,14 @@ class Ventrilo extends Core {
if('NAME' in state.raw) state.name = state.raw.NAME; if('NAME' in state.raw) state.name = state.raw.NAME;
if('MAXCLIENTS' in state.raw) state.maxplayers = state.raw.MAXCLIENTS; if('MAXCLIENTS' in state.raw) state.maxplayers = state.raw.MAXCLIENTS;
if(this.trueTest(state.raw.AUTH)) state.password = true; if(this.trueTest(state.raw.AUTH)) state.password = true;
this.finish(state);
});
} }
sendCommand(cmd,password,c) { async sendCommand(cmd,password) {
const body = Buffer.alloc(16); const body = Buffer.alloc(16);
body.write(password,0,15,'utf8'); body.write(password,0,15,'utf8');
const encrypted = encrypt(cmd,body); const encrypted = encrypt(cmd,body);
const packets = {}; const packets = {};
this.udpSend(encrypted, (buffer) => { return await this.udpSend(encrypted, (buffer) => {
if(buffer.length < 20) return; if(buffer.length < 20) return;
const data = decrypt(buffer); const data = decrypt(buffer);
@ -39,11 +38,10 @@ class Ventrilo extends Core {
const out = []; const out = [];
for(let i = 0; i < data.packetTotal; i++) { for(let i = 0; i < data.packetTotal; i++) {
if(!(i in packets)) return this.fatal('Missing packet #'+i); if(!(i in packets)) throw new Error('Missing packet #'+i);
out.push(packets[i]); out.push(packets[i]);
} }
c(Buffer.concat(out)); return Buffer.concat(out);
return true;
}); });
} }
} }

View file

@ -1,8 +1,8 @@
const Quake3 = require('./quake3'); const Quake3 = require('./quake3');
class Warsow extends Quake3 { class Warsow extends Quake3 {
finalizeState(state) { async run(state) {
super.finalizeState(state); await super.run(state);
if(state.players) { if(state.players) {
for(const player of state.players) { for(const player of state.players) {
player.team = player.address; player.team = player.address;