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

129
README.md
View file

@ -14,75 +14,53 @@ 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({
type: 'minecraft', type: 'minecraft',
host: 'mc.example.com' host: 'mc.example.com'
}).then((state) => { }).then((state) => {
console.log(state); console.log(state);
}).catch((error) => { }).catch((error) => {
console.log("Server is offline"); console.log("Server is offline");
}); });
``` ```
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) => {
if (state.error) reject(state.error);
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
for(const key of Object.keys(options)) {
query.options[key] = options[key];
}
activeQueries.push(query);
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;
} }
async query(userOptions) {
return await this.queryRunner.run(userOptions);
}
static getInstance() {
if (!singleton) singleton = new Gamedig();
return singleton;
}
static async query(...args) {
return await Gamedig.getInstance().query(...args);
}
} }
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;

526
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",
"dependencies": { "domutils": "1.5.1",
"boom": { "nth-check": "~1.0.1"
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz",
"integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==",
"requires": {
"hoek": "4.2.1"
}
}
} }
}, },
"css-what": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.2.tgz",
"integrity": "sha512-wan8dMWQ0GUeF7DGEPVjhHemVW/vy6xUYmFzRY8RYqgA0JtXC9rJmbScBjqSu6dg9q0lwPQy6ZAmJVr3PPTvqQ=="
},
"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"
}
},
"string_decoder": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz",
"integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==",
"requires": {
"safe-buffer": "~5.1.0"
}
}
} }
}, },
"hoek": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz",
"integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA=="
},
"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,38 +7,35 @@ 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);
state.raw.versionmin = this.readUInt(reader); state.raw.versionmin = this.readUInt(reader);
state.raw.versionmax = this.readUInt(reader); state.raw.versionmax = this.readUInt(reader);
state.raw.version = this.readString(reader); state.raw.version = this.readString(reader);
state.maxplayers = this.readUInt(reader); state.maxplayers = this.readUInt(reader);
const players = this.readString(reader); const players = this.readString(reader);
const list = players.split('\n'); const list = players.split('\n');
for(const name of list) { for(const name of list) {
if(!name) continue; if(!name) continue;
state.players.push({ state.players.push({
name: this.stripColorCodes(name) name: this.stripColorCodes(name)
}); });
} }
state.raw.options = this.stripColorCodes(this.readString(reader)); state.raw.options = this.stripColorCodes(this.readString(reader));
state.raw.uri = this.readString(reader); state.raw.uri = this.readString(reader);
state.raw.globalids = this.readString(reader); state.raw.globalids = this.readString(reader);
this.finish(state);
return true;
});
} }
readUInt(reader) { readUInt(reader) {

View file

@ -1,44 +1,42 @@
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 reader.rest();
if(header !== 'EYE1') return;
state.raw.gamename = this.readString(reader);
state.raw.port = parseInt(this.readString(reader));
state.name = this.readString(reader);
state.raw.gametype = this.readString(reader);
state.map = this.readString(reader);
state.raw.version = this.readString(reader);
state.password = this.readString(reader) === '1';
state.raw.numplayers = parseInt(this.readString(reader));
state.maxplayers = parseInt(this.readString(reader));
while(!reader.done()) {
const key = this.readString(reader);
if(!key) break;
const value = this.readString(reader);
state.raw[key] = value;
}
while(!reader.done()) {
const flags = reader.uint(1);
const player = {};
if(flags & 1) player.name = this.readString(reader);
if(flags & 2) player.team = this.readString(reader);
if(flags & 4) player.skin = this.readString(reader);
if(flags & 8) player.score = parseInt(this.readString(reader));
if(flags & 16) player.ping = parseInt(this.readString(reader));
if(flags & 32) player.time = parseInt(this.readString(reader));
state.players.push(player);
}
this.finish(state);
}); });
const reader = this.reader(buffer);
state.raw.gamename = this.readString(reader);
state.gamePort = parseInt(this.readString(reader));
state.name = this.readString(reader);
state.raw.gametype = this.readString(reader);
state.map = this.readString(reader);
state.raw.version = this.readString(reader);
state.password = this.readString(reader) === '1';
state.raw.numplayers = parseInt(this.readString(reader));
state.maxplayers = parseInt(this.readString(reader));
while(!reader.done()) {
const key = this.readString(reader);
if(!key) break;
const value = this.readString(reader);
state.raw[key] = value;
}
while(!reader.done()) {
const flags = reader.uint(1);
const player = {};
if(flags & 1) player.name = this.readString(reader);
if(flags & 2) player.team = this.readString(reader);
if(flags & 4) player.skin = this.readString(reader);
if(flags & 8) player.score = parseInt(this.readString(reader));
if(flags & 16) player.ping = parseInt(this.readString(reader));
if(flags & 32) player.time = parseInt(this.readString(reader));
state.players.push(player);
}
} }
readString(reader) { readString(reader) {

View file

@ -1,127 +1,154 @@
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.numplayers = parseInt(data.shift());
state.maxplayers = parseInt(data.shift());
state.raw.gametype = data.shift();
state.map = data.shift();
state.raw.roundsplayed = parseInt(data.shift());
state.raw.roundstotal = parseInt(data.shift());
state.raw.name = data.shift(); const teamCount = data.shift();
state.raw.numplayers = parseInt(data.shift()); state.raw.teams = [];
state.maxplayers = parseInt(data.shift()); for (let i = 0; i < teamCount; i++) {
state.raw.gametype = data.shift(); const tickets = parseFloat(data.shift());
state.map = data.shift(); state.raw.teams.push({
state.raw.roundsplayed = parseInt(data.shift()); tickets: tickets
state.raw.roundstotal = parseInt(data.shift()); });
}
const teamCount = data.shift(); state.raw.targetscore = parseInt(data.shift());
state.raw.teams = []; state.raw.status = data.shift();
for(let i = 0; i < teamCount; i++) {
const tickets = parseFloat(data.shift());
state.raw.teams.push({
tickets:tickets
});
}
state.raw.targetscore = parseInt(data.shift()); // Seems like the fields end at random places beyond this point
data.shift(); // depending on the server version
state.raw.ranked = (data.shift() === 'true');
state.raw.punkbuster = (data.shift() === 'true'); if (data.length) state.raw.ranked = (data.shift() === 'true');
state.password = (data.shift() === 'true'); if (data.length) state.raw.punkbuster = (data.shift() === 'true');
state.raw.uptime = parseInt(data.shift()); if (data.length) state.password = (data.shift() === 'true');
state.raw.roundtime = parseInt(data.shift()); if (data.length) state.raw.uptime = parseInt(data.shift());
if(this.isBadCompany2) { if (data.length) state.raw.roundtime = parseInt(data.shift());
data.shift();
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;
}
c(); 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();
(c) => { if (data.length) state.raw.pingsite = data.shift();
this.query(['version'], (data) => { if (data.length) state.raw.country = data.shift();
if(this.debug) console.log(data); if (data.length) state.raw.quickmatch = (data.shift() === 'true');
if(data[0] !== 'OK') return this.fatal('Missing OK'); }
state.raw.version = data[2]; {
const data = await this.query(socket, ['version']);
c(); data.shift();
}); state.raw.version = data.shift();
}, }
(c) => {
this.query(['listPlayers','all'], (data) => { {
if(this.debug) console.log(data); const data = await this.query(socket, ['listPlayers', 'all']);
if(data.shift() !== 'OK') return this.fatal('Missing OK'); const fieldCount = parseInt(data.shift());
const fields = [];
const fieldCount = parseInt(data.shift()); for (let i = 0; i < fieldCount; i++) {
const fields = []; fields.push(data.shift());
for(let i = 0; i < fieldCount; i++) { }
fields.push(data.shift()); const numplayers = data.shift();
} for (let i = 0; i < numplayers; i++) {
const numplayers = data.shift(); const player = {};
for(let i = 0; i < numplayers; i++) { for (let key of fields) {
const player = {}; let value = data.shift();
for (let key of fields) {
let value = data.shift(); if (key === 'teamId') key = 'team';
else if (key === 'squadId') key = 'squad';
if(key === 'teamId') key = 'team';
else if(key === 'squadId') key = 'squad'; if (
key === 'kills'
if( || key === 'deaths'
key === 'kills' || key === 'score'
|| key === 'deaths' || key === 'rank'
|| key === 'score' || key === 'team'
|| key === 'rank' || key === 'squad'
|| key === 'team' || key === 'ping'
|| key === 'squad' || key === 'type'
|| key === 'ping' ) {
|| key === 'type' value = parseInt(value);
) { }
value = parseInt(value);
} player[key] = value;
}
player[key] = value; state.players.push(player);
} }
state.players.push(player);
}
this.finish(state);
});
} }
]);
}
query(params,c) {
this.tcpSend(buildPacket(params), (data) => {
const decoded = this.decodePacket(data);
if(!decoded) return false;
c(decoded);
return true;
}); });
} }
async query(socket, params) {
const outPacket = this.buildPacket(params);
return await this.tcpSend(socket, outPacket, (data) => {
const decoded = this.decodePacket(data);
if(decoded) {
this.debugLog(decoded);
if(decoded.shift() !== 'OK') throw new Error('Missing OK');
return decoded;
}
});
}
buildPacket(params) {
const paramBuffers = [];
for (const param of params) {
paramBuffers.push(Buffer.from(param,'utf8'));
}
let totalLength = 12;
for (const paramBuffer of paramBuffers) {
totalLength += paramBuffer.length+1+4;
}
const b = Buffer.alloc(totalLength);
b.writeUInt32LE(0,0);
b.writeUInt32LE(totalLength,4);
b.writeUInt32LE(params.length,8);
let offset = 12;
for (const paramBuffer of paramBuffers) {
b.writeUInt32LE(paramBuffer.length, offset); offset += 4;
paramBuffer.copy(b, offset); offset += paramBuffer.length;
b.writeUInt8(0, offset); offset += 1;
}
return b;
}
decodePacket(buffer) { decodePacket(buffer) {
if(buffer.length < 8) return false; if(buffer.length < 8) return false;
const reader = this.reader(buffer); const reader = this.reader(buffer);
const header = reader.uint(4); const header = reader.uint(4);
const totalLength = reader.uint(4); const totalLength = reader.uint(4);
if(buffer.length < totalLength) return false; if(buffer.length < totalLength) return false;
this.debugLog("Expected " + totalLength + " bytes, have " + buffer.length);
const paramCount = reader.uint(4); const paramCount = reader.uint(4);
const params = []; const params = [];
@ -134,29 +161,4 @@ class Battlefield extends Core {
} }
} }
function buildPacket(params) {
const paramBuffers = [];
for (const param of params) {
paramBuffers.push(Buffer.from(param,'utf8'));
}
let totalLength = 12;
for (const paramBuffer of paramBuffers) {
totalLength += paramBuffer.length+1+4;
}
const b = Buffer.alloc(totalLength);
b.writeUInt32LE(0,0);
b.writeUInt32LE(totalLength,4);
b.writeUInt32LE(params.length,8);
let offset = 12;
for (const paramBuffer of paramBuffers) {
b.writeUInt32LE(paramBuffer.length, offset); offset += 4;
paramBuffer.copy(b, offset); offset += paramBuffer.length;
b.writeUInt8(0, offset); offset += 1;
}
return b;
}
module.exports = Battlefield; module.exports = Battlefield;

View file

@ -1,59 +1,56 @@
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;
m = body.match(/status server for (.*?)\r|\n/);
if(m) state.name = m[1];
m = body.match(/Current uptime: (\d+)/);
if(m) state.raw.uptime = m[1];
m = body.match(/currently running (.*?) by /);
if(m) state.map = m[1];
m = body.match(/Current players: (\d+)\/(\d+)/);
if(m) {
state.raw.numplayers = m[1];
state.maxplayers = m[2];
}
m = body.match(/class="playerlist"([^]+?)\/table/);
if(m) {
const table = m[1];
const pre = /<tr>[^]*<td>([^]*)<\/td>[^]*<td>([^]*)<\/td>[^]*<td>([^]*)<\/td>[^]*<td>([^]*)<\/td>/g;
let pm;
while(pm = pre.exec(table)) {
if(pm[2] === 'Ping') continue;
state.players.push({
name: pm[1],
ping: pm[2],
team: pm[3],
score: pm[4]
});
}
}
/*
var m = this.options.address.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/);
if(m) {
var o1 = parseInt(m[1]);
var o2 = parseInt(m[2]);
var o3 = parseInt(m[3]);
var o4 = parseInt(m[4]);
var addr = o1+(o2<<8)+(o3<<16)+(o4<<24);
state.raw.url = 'aos://'+addr;
}
*/
this.finish(state);
}); });
let m;
m = body.match(/status server for (.*?)\.?(\r|\n)/);
if(m) state.name = m[1];
m = body.match(/Current uptime: (\d+)/);
if(m) state.raw.uptime = m[1];
m = body.match(/currently running (.*?) by /);
if(m) state.map = m[1];
m = body.match(/Current players: (\d+)\/(\d+)/);
if(m) {
state.raw.numplayers = m[1];
state.maxplayers = m[2];
}
m = body.match(/aos:\/\/[0-9]+:[0-9]+/);
if (m) {
state.connect = m[0];
}
const $ = cheerio.load(body);
$('#playerlist tbody tr').each((i,tr) => {
if (!$(tr).find('td').first().attr('colspan')) {
state.players.push({
name: $(tr).find('td').eq(2).text(),
ping: $(tr).find('td').eq(3).text().trim(),
team: $(tr).find('td').eq(4).text().toLowerCase(),
score: parseInt($(tr).find('td').eq(5).text())
});
}
});
/*
var m = this.options.address.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/);
if(m) {
var o1 = parseInt(m[1]);
var o2 = parseInt(m[2]);
var o3 = parseInt(m[3]);
var o4 = parseInt(m[4]);
var addr = o1+(o2<<8)+(o3<<16)+(o4<<24);
state.raw.url = 'aos://'+addr;
}
*/
} }
} }

View file

@ -1,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: []
}; };
}
finalizeState(state) {} await this.run(state);
finish(state) { // because lots of servers prefix with spaces to try to appear first
this.finalizeState(state); state.name = (state.name || '').trim();
this.done(state);
}
done(state) { if (!('connect' in state)) {
if(this.finished) return; state.connect = ''
+ (state.gameHost || this.options.host || this.options.address)
if(this.options.notes) + ':'
state.notes = this.options.notes; + (state.gamePort || this.options.port)
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);
}
reset() {
clearTimeout(this.attemptTimeoutTimer);
if(this.timers) {
for (const timer of this.timers) {
clearTimeout(timer);
}
} }
this.timers = []; state.ping = this.shortestRTT;
delete state.gameHost;
delete state.gamePort;
if(this.tcpSocket) {
this.tcpSocket.destroy();
delete this.tcpSocket;
}
this.udpTimeoutTimer = false; return state;
this.udpCallback = false;
} }
start() { async run(state) {}
const options = this.options;
this.reset();
this.startMillis = Date.now(); /**
* @param {string} host
this.attemptTimeoutTimer = setTimeout(() => { * @returns {Promise<string>}
this.fatal('timeout'); */
},this.options.attemptTimeout); async parseDns(host) {
const isIp = (host) => {
async.series([ return !!host.match(/\d+\.\d+\.\d+\.\d+/);
(c) => { };
// resolve host names const resolveStandard = async (host) => {
if(!('host' in options)) return c(); if(isIp(host)) return host;
if(options.host.match(/\d+\.\d+\.\d+\.\d+/)) { this.debugLog("Standard DNS Lookup: " + host);
options.address = options.host; const {address,family} = await dnsLookupAsync(host);
c(); this.debugLog(address);
} else { return address;
this.parseDns(options.host,c); };
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) {
(c) => { this.debugLog(e.toString());
// 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());
} }
return await resolveStandard(host);
]);
}
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) {
} else { const start = Date.now();
// resolve yet again param.then(() => {
resolveStandard(srvhost,c); const end = Date.now();
} const rtt = end - start;
return; this.registerRtt(rtt);
} }).catch(() => {});
return resolveStandard(host,c); } else {
}); this.debugLog("Registered RTT: " + param + "ms");
}; if (this.shortestRTT === 0 || param < this.shortestRTT) {
this.shortestRTT = param;
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([]);
const address = this.options.address;
const port = this.options.port_query;
const socket = this.tcpSocket = net.connect(port,address,() => {
if(this.debug) console.log(address+':'+port+" TCPCONNECTED");
connected = true;
c(socket);
});
socket.setNoDelay(true);
if(this.debug) console.log(address+':'+port+" TCPCONNECT");
const writeHook = socket.write;
socket.write = (...args) => {
if(this.debug) {
console.log(address+':'+port+" TCP-->");
console.log(HexUtil.debugDump(args[0]));
}
writeHook.apply(socket,args);
};
socket.on('error', () => {});
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) => {
if(!this.tcpCallback) return;
if(this.debug) {
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([]);
}
});
} }
tcpSend(buffer,ondata) {
process.nextTick(() => { /**
if(this.tcpCallback) return this.fatal('Attempted to send TCP packet while still waiting on a managed response'); * @template T
this._tcpConnect((socket) => { * @param {function(Socket):Promise<T>} fn
* @returns {Promise<T>}
*/
async withTcp(fn, port) {
this.usedTcp = true;
const address = this.options.address;
if (!port) port = this.options.port;
this.assertValidPort(port);
let socket, connectionTimeout;
try {
socket = net.connect(port,address);
socket.setNoDelay(true);
this.debugLog(log => {
this.debugLog(address+':'+port+" TCP Connecting");
const writeHook = socket.write;
socket.write = (...args) => {
log(address+':'+port+" TCP-->");
log(HexUtil.debugDump(args[0]));
writeHook.apply(socket,args);
};
socket.on('error', e => log('TCP Error: ' + e));
socket.on('close', () => log('TCP Closed'));
socket.on('data', (data) => {
log(address+':'+port+" <--TCP");
log(data);
});
socket.on('ready', () => log(address+':'+port+" TCP Connected"));
});
const connectionPromise = new Promise((resolve,reject) => {
socket.on('ready', resolve);
socket.on('close', () => reject(new Error('TCP Connection Refused')));
});
this.registerRtt(connectionPromise);
connectionTimeout = Promises.createTimeout(this.options.socketTimeout, 'TCP Opening');
await Promise.race([
connectionPromise,
connectionTimeout,
this.abortedPromise
]);
return await fn(socket);
} finally {
socket && socket.destroy();
connectionTimeout && connectionTimeout.cancel();
}
}
/**
* @template T
* @param {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);
}
const result = onPacket(buffer);
if (result !== undefined) {
this.debugLog("UDP send finished by callback");
resolve(result);
}
} catch(e) {
reject(e);
}
};
socket.addCallback(socketCallback, this.options.debug);
});
timeout = Promises.createTimeout(this.options.socketTimeout, 'UDP');
const wrappedTimeout = new Promise((resolve, reject) => {
timeout.catch((e) => {
this.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);
} }
this.udpSocket.send(buffer,0,buffer.length,this.options.port_query,this.options.address);
} }
_udpResponse(buffer) {
if(this.udpCallback) { async request(params) {
const result = this.udpCallback(buffer); // If we haven't opened a raw tcp socket yet during this query, just open one and then immediately close it.
if(result === true) { // This will give us a much more accurate RTT than using the rtt of the http request.
// we're done with this udp session if (!this.usedTcp) {
clearTimeout(this.udpTimeoutTimer); await this.withTcp(() => {});
this.udpCallback = false; }
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 {
console.log(...args);
} }
} else { } catch(e) {
this.udpResponse(buffer); console.log("Error while debug logging: " + e);
} }
} }
udpResponse() {}
} }
module.exports = Core; module.exports = Core;

View file

@ -3,89 +3,147 @@ 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(this.isEtqw) { if (challengePart1 !== "PiNG") return;
const taskId = reader.uint(4); // some doom3 implementations only return the first 4 bytes of the challenge
} const challengePart2 = reader.string({length:4});
if (challengePart2 !== 'PoNg') reader.skip(-4);
const challenge = reader.uint(4); return reader.rest();
const protoVersion = reader.uint(4);
state.raw.protocolVersion = (protoVersion>>16)+'.'+(protoVersion&0xffff);
if(this.isEtqw) {
const size = reader.uint(4);
}
while(!reader.done()) {
const key = reader.string();
let value = this.stripColors(reader.string());
if(key === 'si_map') {
value = value.replace('maps/','');
value = value.replace('.entities','');
}
if(!key) break;
state.raw[key] = value;
}
let i = 0;
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);
if(!player.ping || player.typeflag)
state.bots.push(player);
else
state.players.push(player);
}
state.raw.osmask = reader.uint(4);
if(this.isEtqw) {
state.raw.ranked = reader.uint(1);
state.raw.timeleft = reader.uint(4);
state.raw.gamestate = reader.uint(1);
state.raw.servertype = reader.uint(1);
// 0 = regular, 1 = tv
if(state.raw.servertype === 0) {
state.raw.interestedClients = reader.uint(1);
} else if(state.raw.servertype === 1) {
state.raw.connectedClients = reader.uint(4);
state.raw.maxClients = reader.uint(4);
}
}
if(state.raw.si_name) state.name = state.raw.si_name;
if(state.raw.si_map) state.map = state.raw.si_map;
if(state.raw.si_maxplayers) state.maxplayers = parseInt(state.raw.si_maxplayers);
if(state.raw.si_usepass === '1') state.password = true;
this.finish(state);
return true;
}); });
let reader = this.reader(body);
const protoVersion = reader.uint(4);
state.raw.protocolVersion = (protoVersion>>16)+'.'+(protoVersion&0xffff);
// some doom implementations send us a packet size here, some don't (etqw does this)
// we can tell if this is a packet size, because the third and fourth byte will be 0 (no packets are that massive)
reader.skip(2);
const packetContainsSize = (reader.uint(2) === 0);
reader.skip(-4);
if (packetContainsSize) {
const size = reader.uint(4);
this.debugLog("Received packet size: " + size);
}
while(!reader.done()) {
const key = reader.string();
let value = this.stripColors(reader.string());
if(key === 'si_map') {
value = value.replace('maps/','');
value = value.replace('.entities','');
}
if(!key) break;
state.raw[key] = value;
this.debugLog(key + "=" + value);
}
const isEtqw = state.raw.gamename && state.raw.gamename.toLowerCase().includes('etqw');
const rest = reader.rest();
let playerResult = this.attemptPlayerParse(rest, isEtqw, false, false, false);
if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, false, false);
if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, true, true);
if (!playerResult) {
throw new Error("Unable to find a suitable parse strategy for player list");
}
let players;
[players,reader] = playerResult;
for (const player of players) {
if(!player.ping || player.typeflag)
state.bots.push(player);
else
state.players.push(player);
}
state.raw.osmask = reader.uint(4);
if (isEtqw) {
state.raw.ranked = reader.uint(1);
state.raw.timeleft = reader.uint(4);
state.raw.gamestate = reader.uint(1);
state.raw.servertype = reader.uint(1);
// 0 = regular, 1 = tv
if(state.raw.servertype === 0) {
state.raw.interestedClients = reader.uint(1);
} else if(state.raw.servertype === 1) {
state.raw.connectedClients = reader.uint(4);
state.raw.maxClients = reader.uint(4);
}
}
if (state.raw.si_name) state.name = state.raw.si_name;
if (state.raw.si_map) state.map = state.raw.si_map;
if (state.raw.si_maxplayers) state.maxplayers = parseInt(state.raw.si_maxplayers);
if (state.raw.si_maxPlayers) state.maxplayers = parseInt(state.raw.si_maxplayers);
if (state.raw.si_usepass === '1') state.password = true;
if (state.raw.si_needPass === '1') state.password = true;
if (this.options.port === 27733) state.gamePort = 3074; // etqw has a different query and game port
}
attemptPlayerParse(rest, isEtqw, hasClanTag, hasClanTagPos, hasTypeFlag) {
this.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,29 +6,34 @@ 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 reader = this.reader(b); const b = await this.sendPacket(
state.raw.protocol = reader.uint(1); 0x46,
state.name = reader.string(); false,
state.map = reader.string(); 'LSQ',
state.raw.mod = reader.string(); 0x49
state.raw.gamemode = reader.string(); );
state.raw.description = reader.string();
state.raw.version = reader.string(); const reader = this.reader(b);
state.raw.port = reader.uint(2); state.raw.protocol = reader.uint(1);
state.raw.numplayers = reader.uint(1); state.name = reader.string();
state.maxplayers = reader.uint(1); state.map = reader.string();
state.raw.listentype = String.fromCharCode(reader.uint(1)); state.raw.mod = reader.string();
state.raw.environment = String.fromCharCode(reader.uint(1)); state.raw.gamemode = reader.string();
state.password = !!reader.uint(1); state.raw.description = reader.string();
state.raw.secure = reader.uint(1); state.raw.version = reader.string();
state.raw.averagefps = reader.uint(1); state.gamePort = reader.uint(2);
state.raw.round = reader.uint(1); state.raw.numplayers = reader.uint(1);
state.raw.maxrounds = reader.uint(1); state.maxplayers = reader.uint(1);
state.raw.timeleft = reader.uint(2); state.raw.listentype = String.fromCharCode(reader.uint(1));
c(); state.raw.environment = String.fromCharCode(reader.uint(1));
}); state.password = !!reader.uint(1);
state.raw.secure = reader.uint(1);
state.raw.averagefps = reader.uint(1);
state.raw.round = reader.uint(1);
state.raw.maxrounds = reader.uint(1);
state.raw.timeleft = reader.uint(2);
} }
} }

View file

@ -1,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');
}
state.raw.info = json; {
const raw = await this.request({
request({ uri: 'http://' + this.options.address + ':' + this.options.port + '/info.json'
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');
}
state.raw.players = json;
state.players = [];
for (const player of json) {
state.players.push({name:player.name, ping:player.ping});
}
super.finish(state);
}); });
}); const json = JSON.parse(raw);
state.raw.info = json;
}
{
const raw = await this.request({
uri: 'http://' + this.options.address + ':' + this.options.port + '/players.json'
});
const json = JSON.parse(raw);
state.raw.players = json;
state.players = [];
for (const player of json) {
state.players.push({name: player.name, ping: player.ping});
}
}
} }
} }

View file

@ -1,68 +1,58 @@
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); if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport);
c(); }
}); {
}, const data = await this.sendPacket('rules');
(c) => { state.raw.rules = data;
this.sendPacket('rules', (data) => { }
state.raw.rules = data; {
c(); const data = await this.sendPacket('players');
}); const players = {};
}, const teams = {};
(c) => { for (const ident of Object.keys(data)) {
this.sendPacket('players', (data) => { const split = ident.split('_');
const players = {}; let key = split[0];
const teams = {}; const id = split[1];
for(const ident of Object.keys(data)) { let value = data[ident];
const split = ident.split('_');
let key = split[0];
const id = split[1];
let value = data[ident];
if(key === 'teamname') { if (key === 'teamname') {
teams[id] = value; teams[id] = value;
} else { } else {
if(!(id in players)) players[id] = {}; if (!(id in players)) players[id] = {};
if(key === 'playername') key = 'name'; if (key === 'playername') key = 'name';
else if(key === 'team') value = parseInt(value); else if (key === 'team') value = parseInt(value);
else if(key === 'score' || key === 'ping' || key === 'deaths') value = parseInt(value); else if (key === 'score' || key === 'ping' || key === 'deaths') value = parseInt(value);
players[id][key] = value; players[id][key] = value;
} }
}
state.raw.teams = teams;
for(const id of Object.keys(players)) {
state.players.push(players[id]);
}
this.finish(state);
});
} }
]);
state.raw.teams = teams;
for (const id of Object.keys(players)) {
state.players.push(players[id]);
}
}
} }
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) while (!reader.done()) {
buffer = buffer.slice(1); const key = reader.string();
packets.push(buffer); const value = reader.string();
}, if (!key) break;
() => { state.raw[key] = value;
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()) {
const key = reader.string();
const value = reader.string();
if(!key) break;
state.raw[key] = value;
}
if('hostname' in state.raw) state.name = state.raw.hostname;
if('mapname' in state.raw) state.map = state.raw.mapname;
if(this.trueTest(state.raw.password)) state.password = true;
if('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers);
state.players = this.readFieldData(reader);
state.raw.teams = this.readFieldData(reader);
this.finish(state);
return true;
} }
); if ('hostname' in state.raw) state.name = state.raw.hostname;
if ('mapname' in state.raw) state.map = state.raw.mapname;
if (this.trueTest(state.raw.password)) state.password = true;
if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers);
if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport);
}
// Parse players
{
const body = await this.sendPacket([0, 0xff, 0]);
const reader = this.reader(body);
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,148 +7,133 @@ 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);
const reader = this.reader(buffer);
let challenge = parseInt(reader.string());
this.debugLog("Received challenge key: " + challenge);
if (challenge === 0) {
// Some servers send us a 0 if they don't want a challenge key used
// BF2 does this.
challenge = null;
}
let requestPayload;
if(this.isJc2mp) {
// they completely alter the protocol. because why not.
requestPayload = Buffer.from([0xff,0xff,0xff,0x02]);
} else {
requestPayload = Buffer.from([0xff,0xff,0xff,0x01]);
}
/** @type Buffer[] */ /** @type Buffer[] */
let packets; const packets = await this.sendPacket(0,challenge,requestPayload,true);
async.series([ // iterate over the received packets
(c) => { // the first packet will start off with k/v pairs, followed with data fields
if(this.noChallenge) return c(); // the following packets will only have data fields
this.sendPacket(9,false,false,false,(buffer) => { state.raw.playerTeamInfo = {};
const reader = this.reader(buffer);
challenge = parseInt(reader.string()); for(let iPacket = 0; iPacket < packets.length; iPacket++) {
c(); const packet = packets[iPacket];
}); const reader = this.reader(packet);
},
(c) => { this.debugLog("Parsing packet #" + iPacket);
let requestPayload; this.debugLog(packet);
if(this.isJc2mp) {
// they completely alter the protocol. because why not. // Parse raw server key/values
requestPayload = Buffer.from([0xff,0xff,0xff,0x02]);
} else { if(iPacket === 0) {
requestPayload = Buffer.from([0xff,0xff,0xff,0x01]); while(!reader.done()) {
const key = reader.string();
if(!key) break;
let value = reader.string();
while(value.match(/^p[0-9]+$/)) {
// fix a weird ut3 bug where some keys don't have values
value = reader.string();
}
state.raw[key] = value;
this.debugLog(key + " = " + value);
} }
this.sendPacket(0,challenge,requestPayload,true,(b) => {
packets = b;
c();
});
},
(c) => {
// iterate over the received packets
// the first packet will start off with k/v pairs, followed with data fields
// the following packets will only have data fields
state.raw.playerTeamInfo = {};
for(let iPacket = 0; iPacket < packets.length; iPacket++) {
const packet = packets[iPacket];
const reader = this.reader(packet);
if(this.debug) {
console.log("+++"+packet.toString('hex'));
console.log(":::"+packet.toString('ascii'));
}
// Parse raw server key/values
if(iPacket === 0) {
while(!reader.done()) {
const key = reader.string();
if(!key) break;
let value = reader.string();
// reread the next line if we hit the weird ut3 bug
if(value === 'p1073741829') value = reader.string();
state.raw[key] = value;
}
}
// Parse player, team, item array state
if(this.isJc2mp) {
state.raw.numPlayers2 = reader.uint(2);
while(!reader.done()) {
const player = {};
player.name = reader.string();
player.steamid = reader.string();
player.ping = reader.uint(2);
state.players.push(player);
}
} else {
let firstMode = true;
while(!reader.done()) {
let mode = reader.string();
if(mode.charCodeAt(0) <= 2) mode = mode.substring(1);
if(!mode) continue;
let offset = 0;
if(iPacket !== 0 && firstMode) offset = reader.uint(1);
reader.skip(1);
firstMode = false;
const modeSplit = mode.split('_');
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()) {
const item = reader.string();
if(!item) break;
while(store.length <= offset) { store.push({}); }
store[offset][modeName] = item;
offset++;
}
}
}
}
c();
},
(c) => {
// Turn all that raw state into something useful
if('hostname' in state.raw) state.name = state.raw.hostname;
else if('servername' in state.raw) state.name = state.raw.servername;
if('mapname' in state.raw) state.map = state.raw.mapname;
if(state.raw.password === '1') state.password = true;
if('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers);
if('' in state.raw.playerTeamInfo) {
for (const playerInfo of state.raw.playerTeamInfo['']) {
const player = {};
for(const from of Object.keys(playerInfo)) {
let key = from;
let value = playerInfo[from];
if(key === 'player') key = 'name';
if(key === 'score' || key === 'ping' || key === 'team' || key === 'deaths' || key === 'pid') value = parseInt(value);
player[key] = value;
}
state.players.push(player);
}
}
this.finish(state);
} }
]);
// Parse player, team, item array state
if(this.isJc2mp) {
state.raw.numPlayers2 = reader.uint(2);
while(!reader.done()) {
const player = {};
player.name = reader.string();
player.steamid = reader.string();
player.ping = reader.uint(2);
state.players.push(player);
}
} else {
let firstMode = true;
while(!reader.done()) {
if (reader.uint(1) <= 2) continue;
reader.skip(-1);
let fieldId = reader.string();
if(!fieldId) continue;
const fieldIdSplit = fieldId.split('_');
const fieldName = fieldIdSplit[0];
const itemType = fieldIdSplit.length > 1 ? fieldIdSplit[1] : 'no_';
if(!(itemType in state.raw.playerTeamInfo)) {
state.raw.playerTeamInfo[itemType] = [];
}
const items = state.raw.playerTeamInfo[itemType];
let offset = reader.uint(1);
firstMode = false;
this.debugLog(() => "Parsing new field: itemType=" + itemType + " fieldName=" + fieldName + " startOffset=" + offset);
while(!reader.done()) {
const item = reader.string();
if(!item) break;
while(items.length <= offset) { items.push({}); }
items[offset][fieldName] = item;
this.debugLog("* " + item);
offset++;
}
}
}
}
// Turn all that raw state into something useful
if ('hostname' in state.raw) state.name = state.raw.hostname;
else if('servername' in state.raw) state.name = state.raw.servername;
if ('mapname' in state.raw) state.map = state.raw.mapname;
if (state.raw.password === '1') state.password = true;
if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers);
if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport);
if('' in state.raw.playerTeamInfo) {
for (const playerInfo of state.raw.playerTeamInfo['']) {
const player = {};
for(const from of Object.keys(playerInfo)) {
let key = from;
let value = playerInfo[from];
if(key === 'player') key = 'name';
if(key === 'score' || key === 'ping' || key === 'team' || key === 'deaths' || key === 'pid') value = parseInt(value);
player[key] = value;
}
state.players.push(player);
}
}
} }
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,53 +1,49 @@
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/>');
let found = false;
for(const line of split) {
const fields = line.split('::');
const ip = fields[2];
const port = fields[3];
if(ip === this.options.address && parseInt(port) === this.options.port) {
found = fields;
break;
}
}
if(!found) return this.fatal('Server not found in list');
state.raw.countrycode = found[0];
state.raw.country = found[1];
state.name = found[4];
state.map = found[5];
state.raw.numplayers = parseInt(found[6]);
state.maxplayers = parseInt(found[7]);
// fields[8] is unknown?
state.raw.rules = found[9];
state.raw.gamemode = parseInt(found[10]);
state.raw.gangsters = parseInt(found[11]);
state.raw.cashrate = parseInt(found[12]);
state.raw.missions = !!parseInt(found[13]);
state.raw.vehicles = !!parseInt(found[14]);
state.raw.customweapons = !!parseInt(found[15]);
state.raw.friendlyfire = !!parseInt(found[16]);
state.raw.mercs = !!parseInt(found[17]);
// fields[18] is unknown? listen server?
state.raw.version = found[19];
for(let i = 0; i < state.raw.numplayers; i++) {
state.players.push({});
}
this.finish(state);
}); });
const split = body.split('<br/>');
let found = null;
for(const line of split) {
const fields = line.split('::');
const ip = fields[2];
const port = fields[3];
if(ip === this.options.address && parseInt(port) === this.options.port) {
found = fields;
break;
}
}
if(found === null) {
throw new Error('Server not found in list');
}
state.raw.countrycode = found[0];
state.raw.country = found[1];
state.name = found[4];
state.map = found[5];
state.raw.numplayers = parseInt(found[6]);
state.maxplayers = parseInt(found[7]);
// fields[8] is unknown?
state.raw.rules = found[9];
state.raw.gamemode = parseInt(found[10]);
state.raw.gangsters = parseInt(found[11]);
state.raw.cashrate = parseInt(found[12]);
state.raw.missions = !!parseInt(found[13]);
state.raw.vehicles = !!parseInt(found[14]);
state.raw.customweapons = !!parseInt(found[15]);
state.raw.friendlyfire = !!parseInt(found[16]);
state.raw.mercs = !!parseInt(found[17]);
// fields[18] is unknown? listen server?
state.raw.version = found[19];
for(let i = 0; i < state.raw.numplayers; i++) {
state.players.push({});
}
} }
} }

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,38 +1,28 @@
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');
}
for (const one of json.players) {
state.players.push({name:one.nickname,team:one.team});
}
for (const key of Object.keys(json)) {
state.raw[key] = json[key];
}
state.name = json.server_name;
state.maxplayers = json.max_players;
if (json.players) {
const split = json.players.split(', ');
for (const name of split) {
state.players.push({name:name});
}
}
this.finish(state);
}); });
const json = JSON.parse(body);
for (const one of json.players) {
state.players.push({name:one.nickname,team:one.team});
}
for (const key of Object.keys(json)) {
state.raw[key] = json[key];
}
state.name = json.server_name;
state.maxplayers = json.max_players;
state.gamePort = json.port;
if (json.players) {
const split = json.players.split(', ');
for (const name of split) {
state.players.push({name:name});
}
}
} }
} }

View file

@ -6,30 +6,29 @@ 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();
state.name = this.readString(reader);
state.raw.numplayers = this.readString(reader);
state.maxplayers = this.readString(reader);
state.raw.gamemode = this.readString(reader);
state.password = !!reader.uint(1);
while(!reader.done()) {
const name = this.readString(reader);
if(!name) break;
state.players.push({
name:name
});
}
this.finish(state);
return true;
}); });
const reader = this.reader(body);
state.name = this.readString(reader);
state.raw.numplayers = this.readString(reader);
state.maxplayers = this.readString(reader);
state.raw.gamemode = this.readString(reader);
state.password = !!reader.uint(1);
state.gamePort = this.options.port - 1;
while(!reader.done()) {
const name = this.readString(reader);
if(!name) break;
state.players.push({
name:name
});
}
} }
readString(reader) { readString(reader) {

View file

@ -1,98 +1,79 @@
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 run(state) {
const portBuf = Buffer.alloc(2);
portBuf.writeUInt16BE(this.options.port,0);
async.series([ const addressBuf = Buffer.from(this.options.host,'utf8');
(c) => {
// build and send handshake and status TCP packet
const portBuf = Buffer.alloc(2); const bufs = [
portBuf.writeUInt16BE(this.options.port_query,0); this.varIntBuffer(4),
this.varIntBuffer(addressBuf.length),
addressBuf,
portBuf,
this.varIntBuffer(1)
];
const addressBuf = Buffer.from(this.options.address,'utf8'); const outBuffer = Buffer.concat([
this.buildPacket(0,Buffer.concat(bufs)),
this.buildPacket(0)
]);
const bufs = [ const data = await this.withTcp(async socket => {
varIntBuffer(4), return await this.tcpSend(socket, outBuffer, data => {
varIntBuffer(addressBuf.length), if(data.length < 10) return;
addressBuf, const reader = this.reader(data);
portBuf, const length = reader.varint();
varIntBuffer(1) if(data.length < length) return;
]; return reader.rest();
});
});
const outBuffer = Buffer.concat([ const reader = this.reader(data);
buildPacket(0,Buffer.concat(bufs)),
buildPacket(0)
]);
this.tcpSend(outBuffer, (data) => { const packetId = reader.varint();
if(data.length < 10) return false; this.debugLog("Packet ID: "+packetId);
const expected = varint.decode(data);
data = data.slice(varint.decode.bytes); const strLen = reader.varint();
if(data.length < expected) return false; this.debugLog("String Length: "+strLen);
receivedData = data;
c(); const str = reader.rest().toString('utf8');
return true; this.debugLog(str);
const json = JSON.parse(str);
delete json.favicon;
state.raw = json;
state.maxplayers = json.players.max;
if(json.players.sample) {
for(const player of json.players.sample) {
state.players.push({
id: player.id,
name: player.name
}); });
},
(c) => {
// parse response
let data = receivedData;
const packetId = varint.decode(data);
if(this.debug) console.log("Packet ID: "+packetId);
data = data.slice(varint.decode.bytes);
const strLen = varint.decode(data);
if(this.debug) console.log("String Length: "+strLen);
data = data.slice(varint.decode.bytes);
const str = data.toString('utf8');
if(this.debug) {
console.log(str);
}
let json;
try {
json = JSON.parse(str);
delete json.favicon;
} catch(e) {
return this.fatal('Invalid JSON');
}
state.raw = json;
state.maxplayers = json.players.max;
if(json.players.sample) {
for(const player of json.players.sample) {
state.players.push({
id: player.id,
name: player.name
});
}
}
while(state.players.length < json.players.online) {
state.players.push({});
}
this.finish(state);
} }
}
while(state.players.length < json.players.online) {
state.players.push({});
}
}
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,40 +1,36 @@
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) => {
} if (buffer.length < 10) return;
const str = buffer.toString();
run(state) { let json;
this.tcpSend('json', (buffer) => { try {
if(buffer.length < 10) return; json = JSON.parse(str);
const str = buffer.toString(); } catch (e) {
let json; // probably not all here yet
try { return;
json = JSON.parse(str);
} catch(e) {
// probably not all here yet
return;
}
state.raw = json;
state.name = json.name;
let channelStack = [state.raw.root];
while(channelStack.length) {
const channel = channelStack.shift();
channel.description = this.cleanComment(channel.description);
channelStack = channelStack.concat(channel.channels);
for(const user of channel.users) {
user.comment = this.cleanComment(user.comment);
state.players.push(user);
} }
} return json;
});
this.finish(state);
return true;
}); });
state.raw = json;
state.name = json.name;
state.gamePort = json.x_gtmurmur_connectport || 64738;
let channelStack = [state.raw.root];
while(channelStack.length) {
const channel = channelStack.shift();
channel.description = this.cleanComment(channel.description);
channelStack = channelStack.concat(channel.channels);
for(const user of channel.users) {
user.comment = this.cleanComment(user.comment);
state.players.push(user);
}
}
} }
cleanComment(str) { cleanComment(str) {

View file

@ -6,24 +6,23 @@ 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);
reader.skip(1);
state.raw.versionMajor = reader.uint(1);
state.raw.versionMinor = reader.uint(1);
state.raw.versionPatch = reader.uint(1);
reader.skip(8);
state.raw.numplayers = reader.uint(4);
state.maxplayers = reader.uint(4);
state.raw.allowedbandwidth = reader.uint(4);
for(let i = 0; i < state.raw.numplayers; i++) {
state.players.push({});
}
this.finish(state);
return true;
}); });
const reader = this.reader(data);
reader.skip(1);
state.raw.versionMajor = reader.uint(1);
state.raw.versionMinor = reader.uint(1);
state.raw.versionPatch = reader.uint(1);
reader.skip(8);
state.raw.numplayers = reader.uint(4);
state.maxplayers = reader.uint(4);
state.raw.allowedbandwidth = reader.uint(4);
for(let i = 0; i < state.raw.numplayers; i++) {
state.players.push({});
}
} }
} }

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);
}
reset() { //const data = this.methodCall(client, 'GetStatus');
super.reset();
if(this.gbxclient) {
this.gbxclient.terminate();
this.gbxclient = false;
}
}
run(state) { {
const cmds = [ const results = await this.methodCall(client, 'GetServerOptions');
['Connect'], state.name = this.stripColors(results.Name);
['Authenticate', this.options.login,this.options.password], state.password = (results.Password !== 'No password');
['GetStatus'], // 1 state.maxplayers = results.CurrentMaxPlayers;
['GetPlayerList',10000,0], // 2 state.raw.maxspectators = results.CurrentMaxSpectators;
['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 = '';
const igm = results[5].GameMode;
if(igm === 0) gamemode="Rounds";
if(igm === 1) gamemode="Time Attack";
if(igm === 2) gamemode="Team";
if(igm === 3) gamemode="Laps";
if(igm === 4) gamemode="Stunts";
if(igm === 5) gamemode="Cup";
state.name = this.stripColors(results[3].Name); {
state.password = (results[3].Password !== 'No password'); const results = await this.methodCall(client, 'GetCurrentMapInfo');
state.maxplayers = results[3].CurrentMaxPlayers; state.map = this.stripColors(results.Name);
state.raw.maxspectators = results[3].CurrentMaxSpectators; state.raw.mapUid = results.UId;
state.map = this.stripColors(results[4].Name); }
state.raw.mapUid = results[4].UId;
state.raw.gametype = gamemode;
state.raw.players = results[2];
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, 'GetCurrentGameInfo');
let gamemode = '';
const igm = results.GameMode;
if(igm === 0) gamemode="Rounds";
if(igm === 1) gamemode="Time Attack";
if(igm === 2) gamemode="Team";
if(igm === 3) gamemode="Laps";
if(igm === 4) gamemode="Stunts";
if(igm === 5) gamemode="Cup";
state.raw.gametype = gamemode;
state.raw.mapcount = results.NbChallenge;
}
{
const results = await this.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,131 +1,116 @@
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 = []; for (let i = 0; i < numGrf; i++) {
for(let i = 0; i < numGrf; i++) { const grf = {};
const grf = {}; grf.id = reader.part(4).toString('hex');
grf.id = reader.part(4).toString('hex'); grf.md5 = reader.part(16).toString('hex');
grf.md5 = reader.part(16).toString('hex'); state.raw.grfs.push(grf);
state.raw.grfs.push(grf); }
}
}
if(version >= 3) {
state.raw.date_current = this.readDate(reader);
state.raw.date_start = this.readDate(reader);
}
if(version >= 2) {
state.raw.maxcompanies = reader.uint(1);
state.raw.numcompanies = reader.uint(1);
state.raw.maxspectators = reader.uint(1);
}
state.name = reader.string();
state.raw.version = reader.string();
state.raw.language = this.decode(
reader.uint(1),
['any','en','de','fr']
);
state.password = !!reader.uint(1);
state.maxplayers = reader.uint(1);
state.raw.numplayers = reader.uint(1);
for(let i = 0; i < state.raw.numplayers; i++) {
state.players.push({});
}
state.raw.numspectators = reader.uint(1);
state.map = reader.string();
state.raw.map_width = reader.uint(2);
state.raw.map_height = reader.uint(2);
state.raw.landscape = this.decode(
reader.uint(1),
['temperate','arctic','desert','toyland']
);
state.raw.dedicated = !!reader.uint(1);
c();
});
},
(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
if(version !== 6) return c();
state.raw.companies = [];
const numCompanies = reader.uint(1);
for(let iCompany = 0; iCompany < numCompanies; iCompany++) {
const company = {};
company.id = reader.uint(1);
company.name = reader.string();
company.year_start = reader.uint(4);
company.value = reader.uint(8);
company.money = reader.uint(8);
company.income = reader.uint(8);
company.performance = reader.uint(2);
company.password = !!reader.uint(1);
company.vehicles = {};
for(const type of vehicle_types) {
company.vehicles[type] = reader.uint(2);
}
company.stations = {};
for(const type of station_types) {
company.stations[type] = reader.uint(2);
}
company.clients = reader.string();
state.raw.companies.push(company);
}
c();
});
},
(c) => {
this.finish(state);
} }
]); if (version >= 3) {
state.raw.date_current = this.readDate(reader);
state.raw.date_start = this.readDate(reader);
}
if (version >= 2) {
state.raw.maxcompanies = reader.uint(1);
state.raw.numcompanies = reader.uint(1);
state.raw.maxspectators = reader.uint(1);
}
state.name = reader.string();
state.raw.version = reader.string();
state.raw.language = this.decode(
reader.uint(1),
['any', 'en', 'de', 'fr']
);
state.password = !!reader.uint(1);
state.maxplayers = reader.uint(1);
state.raw.numplayers = reader.uint(1);
for (let i = 0; i < state.raw.numplayers; i++) {
state.players.push({});
}
state.raw.numspectators = reader.uint(1);
state.map = reader.string();
state.raw.map_width = reader.uint(2);
state.raw.map_height = reader.uint(2);
state.raw.landscape = this.decode(
reader.uint(1),
['temperate', 'arctic', 'desert', 'toyland']
);
state.raw.dedicated = !!reader.uint(1);
}
{
const [reader,version] = await this.query(2,3,-1,-1);
// we don't know how to deal with companies outside version 6
if(version === 6) {
state.raw.companies = [];
const numCompanies = reader.uint(1);
for (let iCompany = 0; iCompany < numCompanies; iCompany++) {
const company = {};
company.id = reader.uint(1);
company.name = reader.string();
company.year_start = reader.uint(4);
company.value = reader.uint(8);
company.money = reader.uint(8);
company.income = reader.uint(8);
company.performance = reader.uint(2);
company.password = !!reader.uint(1);
const vehicle_types = ['train', 'truck', 'bus', 'aircraft', 'ship'];
const station_types = ['station', 'truckbay', 'busstation', 'airport', 'dock'];
company.vehicles = {};
for (const type of vehicle_types) {
company.vehicles[type] = reader.uint(2);
}
company.stations = {};
for (const type of station_types) {
company.stations[type] = reader.uint(2);
}
company.clients = reader.string();
state.raw.companies.push(company);
}
}
}
} }
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,79 +10,78 @@ 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;
if (this.isQuake1) {
let response; type = reader.string({length: this.responseHeader.length});
if(this.isQuake1) {
response = 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 info = reader.string().split('\\');
if(info[0] === '') info.shift();
while(true) {
const key = info.shift();
const value = info.shift();
if(typeof value === 'undefined') break;
state.raw[key] = value;
}
while(!reader.done()) {
const line = reader.string();
if(!line || line.charAt(0) === '\0') break;
const args = [];
const split = line.split('"');
split.forEach((part,i) => {
const inQuote = (i%2 === 1);
if(inQuote) {
args.push(part);
} else {
const splitSpace = part.split(' ');
for (const subpart of splitSpace) {
if(subpart) args.push(subpart);
}
}
});
const player = {};
if(this.isQuake1) {
player.id = parseInt(args.shift());
player.score = parseInt(args.shift());
player.time = parseInt(args.shift());
player.ping = parseInt(args.shift());
player.name = args.shift();
player.skin = args.shift();
player.color1 = parseInt(args.shift());
player.color2 = parseInt(args.shift());
} else {
player.frags = parseInt(args.shift());
player.ping = parseInt(args.shift());
player.name = args.shift() || '';
player.address = args.shift() || '';
}
(player.ping ? state.players : state.bots).push(player);
}
if('g_needpass' in state.raw) state.password = state.raw.g_needpass;
if('mapname' in state.raw) state.map = state.raw.mapname;
if('sv_maxclients' in state.raw) state.maxplayers = state.raw.sv_maxclients;
if('maxclients' in state.raw) state.maxplayers = state.raw.maxclients;
if('sv_hostname' in state.raw) state.name = state.raw.sv_hostname;
if('hostname' in state.raw) state.name = state.raw.hostname;
this.finish(state);
return true;
}); });
const reader = this.reader(body);
const info = reader.string().split('\\');
if(info[0] === '') info.shift();
while(true) {
const key = info.shift();
const value = info.shift();
if(typeof value === 'undefined') break;
state.raw[key] = value;
}
while(!reader.done()) {
const line = reader.string();
if(!line || line.charAt(0) === '\0') break;
const args = [];
const split = line.split('"');
split.forEach((part,i) => {
const inQuote = (i%2 === 1);
if(inQuote) {
args.push(part);
} else {
const splitSpace = part.split(' ');
for (const subpart of splitSpace) {
if(subpart) args.push(subpart);
}
}
});
const player = {};
if(this.isQuake1) {
player.id = parseInt(args.shift());
player.score = parseInt(args.shift());
player.time = parseInt(args.shift());
player.ping = parseInt(args.shift());
player.name = args.shift();
player.skin = args.shift();
player.color1 = parseInt(args.shift());
player.color2 = parseInt(args.shift());
} else {
player.frags = parseInt(args.shift());
player.ping = parseInt(args.shift());
player.name = args.shift() || '';
if (!player.name) delete player.name;
player.address = args.shift() || '';
if (!player.address) delete player.address;
}
(player.ping ? state.players : state.bots).push(player);
}
if('g_needpass' in state.raw) state.password = state.raw.g_needpass;
if('mapname' in state.raw) state.map = state.raw.mapname;
if('sv_maxclients' in state.raw) state.maxplayers = state.raw.sv_maxclients;
if('maxclients' in state.raw) state.maxplayers = state.raw.maxclients;
if('sv_hostname' in state.raw) state.name = state.raw.sv_hostname;
if('hostname' in state.raw) state.name = state.raw.hostname;
} }
} }

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,87 +6,83 @@ 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++) {
const key = this.readString(reader,1); const key = this.readString(reader,1);
const value = this.readString(reader,1); const value = this.readString(reader,1);
state.raw.rules[key] = value; state.raw.rules[key] = value;
}
if('mapname' in state.raw.rules)
state.map = state.raw.rules.mapname;
c();
});
},
(c) => {
this.sendPacket('d',(reader) => {
const playerCount = reader.uint(2);
for(let i = 0; i < playerCount; i++) {
const player = {};
player.id = reader.uint(1);
player.name = this.readString(reader,1);
player.score = reader.int(4);
player.ping = reader.uint(4);
state.players.push(player);
}
c();
},() => {
for(let i = 0; i < state.raw.numplayers; i++) {
state.players.push({});
}
c();
});
},
(c) => {
this.finish(state);
} }
]); if('mapname' in state.raw.rules)
state.map = state.raw.rules.mapname;
}
// read players
{
const reader = await this.sendPacket('d', true);
if (reader !== null) {
const playerCount = reader.uint(2);
for(let i = 0; i < playerCount; i++) {
const player = {};
player.id = reader.uint(1);
player.name = this.readString(reader,1);
player.score = reader.int(4);
player.ping = reader.uint(4);
state.players.push(player);
}
} else {
for(let i = 0; i < state.raw.numplayers; i++) {
state.players.push({});
}
}
}
} }
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(
const reader = this.reader(buffer); outBuffer,
for(let i = 0; i < outbuffer.length; i++) { (buffer) => {
if(outbuffer.readUInt8(i) !== reader.uint(1)) return; const reader = this.reader(buffer);
for(let i = 0; i < outBuffer.length; i++) {
if(outBuffer.readUInt8(i) !== reader.uint(1)) return;
}
return reader;
},
() => {
if(allowTimeout) {
return null;
}
} }
onresponse(reader); );
return true;
},() => {
if(ontimeout) {
ontimeout();
return true;
}
});
} }
} }

View file

@ -6,58 +6,59 @@ 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 => {
const reader = this.reader(buffer); return await this.tcpSend(socket, b, buffer => {
if (buffer.length < 4) return;
if(buffer.length < 4) return false; const reader = this.reader(buffer);
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 data = []; });
state.raw.data = data;
reader.skip(2);
while(!reader.done()) {
const mark = reader.uint(1);
if(mark === 1) {
// signed int
data.push(reader.int(4));
} else if(mark === 3) {
// float
data.push(reader.float());
} else if(mark === 4) {
// string
const length = reader.uint(2);
data.push(reader.string(length));
} else if(mark === 6) {
// byte
data.push(reader.uint(1));
}
}
if(data.length < 9) {
this.fatal("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[4] === 'string') state.name = data[4];
if(typeof data[5] === 'string') state.raw.description = data[5];
if(typeof data[7] === 'number') state.raw.numplayers = data[7];
if(typeof data[8] === 'number') state.maxplayers = data[8];
if('numplayers' in state.raw) {
for(let i = 0; i < state.raw.numplayers; i++) {
state.players.push({});
}
}
this.finish(state);
return true;
}); });
const reader = this.reader(payload);
const data = [];
state.raw.data = data;
reader.skip(2);
while(!reader.done()) {
const mark = reader.uint(1);
if(mark === 1) {
// signed int
data.push(reader.int(4));
} else if(mark === 3) {
// float
data.push(reader.float());
} else if(mark === 4) {
// string
const length = reader.uint(2);
data.push(reader.string(length));
} else if(mark === 6) {
// byte
data.push(reader.uint(1));
}
}
if(data.length < 9) {
throw new Error("Not enough units in data packet");
}
if(typeof data[3] === 'number') state.raw.version = data[3].toFixed(7).replace(/0+$/, '');
if(typeof data[4] === 'string') state.name = data[4];
if(typeof data[5] === 'string') state.raw.description = data[5];
if(typeof data[7] === 'number') state.raw.numplayers = data[7];
if(typeof data[8] === 'number') state.maxplayers = data[8];
if('numplayers' in state.raw) {
for(let i = 0; i < state.raw.numplayers; i++) {
state.players.push({});
}
}
} }
} }

View file

@ -1,77 +1,70 @@
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) => {
for (const line of data.split('\r\n')) {
const equals = line.indexOf('=');
const key = equals === -1 ? line : line.substr(0,equals);
const value = equals === -1 ? '' : line.substr(equals+1);
state.raw[key] = value;
}
c();
});
},
(c) => {
this.sendCommand('pl', (data) => {
const split = data.split('\r\n');
const fields = split.shift().split('\t');
for (const line of split) {
const split2 = line.split('\t');
const player = {};
split2.forEach((value,i) => {
let key = fields[i];
if(!key) return;
if(key === 'nick') key = 'name';
const m = value.match(/^"(.*)"$/);
if(m) value = m[1];
player[key] = value;
});
state.players.push(player);
}
c();
});
},
(c) => {
this.sendCommand('cl', (data) => {
const split = data.split('\r\n');
const fields = split.shift().split('\t');
state.raw.channels = [];
for (const line of split) {
const split2 = line.split('\t');
const channel = {};
split2.forEach((value,i) => {
const key = fields[i];
if(!key) return;
const m = value.match(/^"(.*)"$/);
if(m) value = m[1];
channel[key] = value;
});
state.raw.channels.push(channel);
}
c();
});
},
(c) => {
this.finish(state);
} }
]);
{
const data = await this.sendCommand(socket, 'si');
for (const line of data.split('\r\n')) {
const equals = line.indexOf('=');
const key = equals === -1 ? line : line.substr(0,equals);
const value = equals === -1 ? '' : line.substr(equals+1);
state.raw[key] = value;
}
}
{
const data = await this.sendCommand(socket, 'pl');
const split = data.split('\r\n');
const fields = split.shift().split('\t');
for (const line of split) {
const split2 = line.split('\t');
const player = {};
split2.forEach((value,i) => {
let key = fields[i];
if(!key) return;
if(key === 'nick') key = 'name';
const m = value.match(/^"(.*)"$/);
if(m) value = m[1];
player[key] = value;
});
state.players.push(player);
}
}
{
const data = await this.sendCommand(socket, 'cl');
const split = data.split('\r\n');
const fields = split.shift().split('\t');
state.raw.channels = [];
for (const line of split) {
const split2 = line.split('\t');
const channel = {};
split2.forEach((value,i) => {
const key = fields[i];
if(!key) return;
const m = value.match(/^"(.*)"$/);
if(m) value = m[1];
channel[key] = value;
});
state.raw.channels.push(channel);
}
}
}, queryPort);
} }
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,78 +1,67 @@
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 split = data.split('\n\r'); {
if(split[0] !== 'TS3') this.fatal('Invalid header'); const data = await this.sendCommand(socket, 'use port='+this.options.port, true);
c(); const split = data.split('\n\r');
}, true); if(split[0] !== 'TS3') throw new Error('Invalid header');
},
(c) => {
this.sendCommand('serverinfo', (data) => {
state.raw = data[0];
if('virtualserver_name' in state.raw) state.name = state.raw.virtualserver_name;
if('virtualserver_maxclients' in state.raw) state.maxplayers = state.raw.virtualserver_maxclients;
c();
});
},
(c) => {
this.sendCommand('clientlist', (list) => {
for (const client of list) {
client.name = client.client_nickname;
delete client.client_nickname;
if(client.client_type === '0') {
state.players.push(client);
}
}
c();
});
},
(c) => {
this.sendCommand('channellist -topic', (data) => {
state.raw.channels = data;
c();
});
},
(c) => {
this.finish(state);
} }
]);
}
sendCommand(cmd,c,raw) {
this.tcpSend(cmd+'\x0A', (buffer) => {
if(buffer.length < 21) return;
if(buffer.slice(-21).toString() !== '\n\rerror id=0 msg=ok\n\r') return;
const body = buffer.slice(0,-21).toString();
let out; {
const data = await this.sendCommand(socket, 'serverinfo');
state.raw = data[0];
if('virtualserver_name' in state.raw) state.name = state.raw.virtualserver_name;
if('virtualserver_maxclients' in state.raw) state.maxplayers = state.raw.virtualserver_maxclients;
}
if(raw) { {
out = body; const list = await this.sendCommand(socket, 'clientlist');
} else { for (const client of list) {
const segments = body.split('|'); client.name = client.client_nickname;
out = []; delete client.client_nickname;
for (const line of segments) { if(client.client_type === '0') {
const split = line.split(' '); state.players.push(client);
const unit = {};
for (const field of split) {
const equals = field.indexOf('=');
const key = equals === -1 ? field : field.substr(0,equals);
const value = equals === -1 ? '' : field.substr(equals+1)
.replace(/\\s/g,' ').replace(/\\\//g,'/');
unit[key] = value;
} }
out.push(unit);
} }
} }
c(out); {
const data = await this.sendCommand(socket, 'channellist -topic');
state.raw.channels = data;
}
}, queryPort);
}
return true; async sendCommand(socket,cmd,raw) {
const body = await this.tcpSend(socket, cmd+'\x0A', (buffer) => {
if (buffer.length < 21) return;
if (buffer.slice(-21).toString() !== '\n\rerror id=0 msg=ok\n\r') return;
return buffer.slice(0, -21).toString();
}); });
if(raw) {
return body;
} else {
const segments = body.split('|');
const out = [];
for (const line of segments) {
const split = line.split(' ');
const unit = {};
for (const field of split) {
const equals = field.indexOf('=');
const key = equals === -1 ? field : field.substr(0,equals);
const value = equals === -1 ? '' : field.substr(equals+1)
.replace(/\\s/g,' ').replace(/\\\//g,'/');
unit[key] = value;
}
out.push(unit);
}
return out;
}
} }
} }

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');
for (const one of json.players) {
state.players.push({name:one.nickname,team:one.team});
}
state.name = json.name;
state.raw.port = json.port;
state.raw.numplayers = json.playercount;
this.finish(state);
}); });
const json = JSON.parse(body);
if(json.status !== 200) throw new Error('Invalid status');
for (const one of json.players) {
state.players.push({name:one.nickname,team:one.team});
}
state.name = json.name;
state.gamePort = json.port;
state.raw.numplayers = json.playercount;
} }
} }

View file

@ -5,81 +5,81 @@ 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;
} }
state.raw.gametype = this.readString(reader); return reader;
state.raw.version = this.readString(reader);
state.name = this.readString(reader);
state.raw.dedicated = !!reader.uint(1);
state.password = !!reader.uint(1);
state.raw.playerCount = reader.uint(1);
state.maxplayers = reader.uint(1);
state.raw.cpu = reader.uint(2);
state.raw.mod = this.readString(reader);
state.raw.type = this.readString(reader);
state.map = this.readString(reader);
state.raw.motd = this.readString(reader);
state.raw.teamCount = reader.uint(1);
const teamFields = this.readFieldList(reader);
const playerFields = this.readFieldList(reader);
state.raw.teams = [];
for(let i = 0; i < state.raw.teamCount; i++) {
const teamName = this.readString(reader);
const teamValues = this.readValues(reader);
const teamInfo = {};
for (let i = 0; i < teamValues.length && i < teamFields.length; i++) {
let key = teamFields[i];
let value = teamValues[i];
if (key === 'ultra_base') key = 'name';
if (value === '%t') value = teamName;
if (['score','players'].includes(key)) value = parseInt(value);
teamInfo[key] = value;
}
state.raw.teams.push(teamInfo);
}
for(let i = 0; i < state.raw.playerCount; i++) {
const ping = reader.uint(1) * 4;
const packetLoss = reader.uint(1);
const teamNum = reader.uint(1);
const name = this.readString(reader);
const playerValues = this.readValues(reader);
const playerInfo = {};
for (let i = 0; i < playerValues.length && i < playerFields.length; i++) {
let key = playerFields[i];
let value = playerValues[i];
if (value === '%p') value = ping;
if (value === '%l') value = packetLoss;
if (value === '%t') value = teamNum;
if (value === '%n') value = name;
if (['score','ping','pl','kills','lvl'].includes(key)) value = parseInt(value);
if (key === 'team') {
const teamId = parseInt(value);
if (teamId >= 0 && teamId < state.raw.teams.length && state.raw.teams[teamId].name) {
value = state.raw.teams[teamId].name;
} else {
continue;
}
}
playerInfo[key] = value;
}
state.players.push(playerInfo);
}
this.finish(state);
return true;
}); });
state.raw.gametype = this.readString(reader);
state.raw.version = this.readString(reader);
state.name = this.readString(reader);
state.raw.dedicated = !!reader.uint(1);
state.password = !!reader.uint(1);
state.raw.playerCount = reader.uint(1);
state.maxplayers = reader.uint(1);
state.raw.cpu = reader.uint(2);
state.raw.mod = this.readString(reader);
state.raw.type = this.readString(reader);
state.map = this.readString(reader);
state.raw.motd = this.readString(reader);
state.raw.teamCount = reader.uint(1);
const teamFields = this.readFieldList(reader);
const playerFields = this.readFieldList(reader);
state.raw.teams = [];
for(let i = 0; i < state.raw.teamCount; i++) {
const teamName = this.readString(reader);
const teamValues = this.readValues(reader);
const teamInfo = {};
for (let i = 0; i < teamValues.length && i < teamFields.length; i++) {
let key = teamFields[i];
let value = teamValues[i];
if (key === 'ultra_base') key = 'name';
if (value === '%t') value = teamName;
if (['score','players'].includes(key)) value = parseInt(value);
teamInfo[key] = value;
}
state.raw.teams.push(teamInfo);
}
for(let i = 0; i < state.raw.playerCount; i++) {
const ping = reader.uint(1) * 4;
const packetLoss = reader.uint(1);
const teamNum = reader.uint(1);
const name = this.readString(reader);
const playerValues = this.readValues(reader);
const playerInfo = {};
for (let i = 0; i < playerValues.length && i < playerFields.length; i++) {
let key = playerFields[i];
let value = playerValues[i];
if (value === '%p') value = ping;
if (value === '%l') value = packetLoss;
if (value === '%t') value = teamNum;
if (value === '%n') value = name;
if (['score','ping','pl','kills','lvl'].includes(key)) value = parseInt(value);
if (key === 'team') {
const teamId = parseInt(value);
if (teamId >= 0 && teamId < state.raw.teams.length && state.raw.teams[teamId].name) {
value = state.raw.teams[teamId].name;
} else {
continue;
}
}
playerInfo[key] = value;
}
state.players.push(playerInfo);
}
} }
readFieldList(reader) { 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,32 +49,29 @@ 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);
state.raw.name = this.readString(fullReader);
state.raw.motd = this.readString(fullReader);
state.raw.servers = [];
while (!fullReader.done()) {
fullReader.skip(1); // junk ?
const count = fullReader.uint(1);
for (let i = 0; i < count; i++) {
const six = fullReader.uint(1);
if (six !== 6) {
this.fatal('Expecting 6');
return true;
}
const ip = fullReader.uint(4);
const port = fullReader.uint(2);
const ipStr = (ip & 255) + '.' + (ip >> 8 & 255) + '.' + (ip >> 16 & 255) + '.' + (ip >>> 24);
state.raw.servers.push(ipStr+":"+port);
}
}
this.finish(state);
return true;
} }
}); });
const fullReader = this.reader(full);
state.raw.name = this.readString(fullReader);
state.raw.motd = this.readString(fullReader);
state.raw.servers = [];
while (!fullReader.done()) {
fullReader.skip(1); // junk ?
const count = fullReader.uint(1);
for (let i = 0; i < count; i++) {
const six = fullReader.uint(1);
if (six !== 6) {
throw new Error('Expecting 6');
}
const ip = fullReader.uint(4);
const port = fullReader.uint(2);
const ipStr = (ip & 255) + '.' + (ip >> 8 & 255) + '.' + (ip >> 16 & 255) + '.' + (ip >>> 24);
state.raw.servers.push(ipStr+":"+port);
}
}
} }
readString(reader) { readString(reader) {
const length = reader.uint(1); const length = reader.uint(1);

View file

@ -1,101 +1,90 @@
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.gamePort = reader.uint(4);
state.raw.port = reader.uint(4); state.raw.queryport = reader.uint(4);
state.raw.queryport = reader.uint(4); state.name = this.readUnrealString(reader, true);
state.name = this.readUnrealString(reader,true); state.map = this.readUnrealString(reader, true);
state.map = this.readUnrealString(reader,true); state.raw.gametype = this.readUnrealString(reader, true);
state.raw.gametype = this.readUnrealString(reader,true); state.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);
}, const reader = this.reader(b);
(c) => { state.raw.mutators = [];
this.sendPacket(1,true,(b) => { state.raw.rules = {};
const reader = this.reader(b); while(!reader.done()) {
state.raw.mutators = []; const key = this.readUnrealString(reader,true);
state.raw.rules = {}; const value = this.readUnrealString(reader,true);
while(!reader.done()) { if(key === 'Mutator') state.raw.mutators.push(value);
else state.raw.rules[key] = value;
}
if('GamePassword' in state.raw.rules)
state.password = state.raw.rules.GamePassword !== 'True';
}
{
const b = await this.sendPacket(2,false);
const reader = this.reader(b);
while(!reader.done()) {
const player = {};
player.id = reader.uint(4);
if(!player.id) break;
if(player.id === 0) {
// Unreal2XMP Player (ID is always 0)
reader.skip(4);
}
player.name = this.readUnrealString(reader,true);
player.ping = reader.uint(4);
player.score = reader.int(4);
reader.skip(4); // stats ID
// Extra data for Unreal2XMP players
if(player.id === 0) {
const count = reader.uint(1);
for(let iField = 0; iField < count; iField++) {
const key = this.readUnrealString(reader,true); const key = this.readUnrealString(reader,true);
const value = this.readUnrealString(reader,true); const value = this.readUnrealString(reader,true);
if(key === 'Mutator') state.raw.mutators.push(value); player[key] = value;
else state.raw.rules[key] = value;
} }
}
if('GamePassword' in state.raw.rules) if(player.id === 0 && player.name === 'Player') {
state.password = state.raw.rules.GamePassword !== 'True'; // these show up in ut2004 queries, but aren't real
// not even really sure why they're there
continue;
}
c(); (player.ping ? state.players : state.bots).push(player);
});
},
(c) => {
this.sendPacket(2,false,(b) => {
const reader = this.reader(b);
while(!reader.done()) {
const player = {};
player.id = reader.uint(4);
if(!player.id) break;
if(player.id === 0) {
// Unreal2XMP Player (ID is always 0)
reader.skip(4);
}
player.name = this.readUnrealString(reader,true);
player.ping = reader.uint(4);
player.score = reader.int(4);
reader.skip(4); // stats ID
// Extra data for Unreal2XMP players
if(player.id === 0) {
const count = reader.uint(1);
for(let iField = 0; iField < count; iField++) {
const key = this.readUnrealString(reader,true);
const value = this.readUnrealString(reader,true);
player[key] = value;
}
}
if(player.id === 0 && player.name === 'Player') {
// these show up in ut2004 queries, but aren't real
// not even really sure why they're there
continue;
}
(player.ping ? state.players : state.bots).push(player);
}
c();
});
},
(c) => {
this.finish(state);
} }
]);
}
readExtraInfo(reader,state) {
if(this.debug) {
console.log("UNREAL2 EXTRA INFO:");
console.log(reader.uint(4));
console.log(reader.uint(4));
console.log(reader.uint(4));
console.log(reader.uint(4));
console.log(reader.buffer.slice(reader.i));
} }
} }
readExtraInfo(reader,state) {
this.debugLog(log => {
log("UNREAL2 EXTRA INFO:");
log(reader.uint(4));
log(reader.uint(4));
log(reader.uint(4));
log(reader.uint(4));
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,173 +25,172 @@ 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);
if(this.goldsrcInfo) state.raw.address = reader.string();
else state.raw.protocol = reader.uint(1);
state.name = reader.string();
state.map = reader.string();
state.raw.folder = reader.string();
state.raw.game = reader.string();
state.raw.steamappid = reader.uint(2);
state.raw.numplayers = reader.uint(1);
state.maxplayers = reader.uint(1);
if(this.goldsrcInfo) state.raw.protocol = reader.uint(1);
else state.raw.numbots = reader.uint(1);
state.raw.listentype = reader.uint(1);
state.raw.environment = reader.uint(1);
if(!this.goldsrcInfo) {
state.raw.listentype = String.fromCharCode(state.raw.listentype);
state.raw.environment = String.fromCharCode(state.raw.environment);
}
state.password = !!reader.uint(1);
if(this.goldsrcInfo) {
state.raw.ismod = reader.uint(1);
if(state.raw.ismod) {
state.raw.modlink = reader.string();
state.raw.moddownload = reader.string();
reader.skip(1);
state.raw.modversion = reader.uint(4);
state.raw.modsize = reader.uint(4);
state.raw.modtype = reader.uint(1);
state.raw.moddll = reader.uint(1);
}
}
state.raw.secure = reader.uint(1);
if(this.goldsrcInfo) {
state.raw.numbots = reader.uint(1);
} else {
if(state.raw.folder === 'ship') {
state.raw.shipmode = reader.uint(1);
state.raw.shipwitnesses = reader.uint(1);
state.raw.shipduration = reader.uint(1);
}
state.raw.version = reader.string();
const extraFlag = reader.uint(1);
if(extraFlag & 0x80) state.raw.port = reader.uint(2);
if(extraFlag & 0x10) state.raw.steamid = reader.uint(8);
if(extraFlag & 0x40) {
state.raw.sourcetvport = reader.uint(2);
state.raw.sourcetvname = reader.string();
}
if(extraFlag & 0x20) state.raw.tags = reader.string();
if(extraFlag & 0x01) state.raw.gameid = reader.uint(8);
}
// from https://developer.valvesoftware.com/wiki/Server_queries
if(
state.raw.protocol === 7 && (
state.raw.steamappid === 215
|| state.raw.steamappid === 17550
|| state.raw.steamappid === 17700
|| state.raw.steamappid === 240
)
) {
this._skipSizeInSplitHeader = true;
}
if(this.debug) {
console.log("STEAM APPID: "+state.raw.steamappid);
console.log("PROTOCOL: "+state.raw.protocol);
}
if(state.raw.protocol === 48) {
if(this.debug) console.log("GOLDSRC DETECTED - USING MODIFIED SPLIT FORMAT");
this.goldsrcSplits = true;
}
c();
}
); );
}
queryChallenge(state,c) { const reader = this.reader(b);
if(this.legacyChallenge) {
this.sendPacket(0x57,false,null,0x41,(b) => { if(this.goldsrcInfo) state.raw.address = reader.string();
// sendPacket will catch the response packet and else state.raw.protocol = reader.uint(1);
// save the challenge for us
c(); state.name = reader.string();
}); state.map = reader.string();
state.raw.folder = reader.string();
state.raw.game = reader.string();
state.raw.steamappid = reader.uint(2);
state.raw.numplayers = reader.uint(1);
state.maxplayers = reader.uint(1);
if(this.goldsrcInfo) state.raw.protocol = reader.uint(1);
else state.raw.numbots = reader.uint(1);
state.raw.listentype = reader.uint(1);
state.raw.environment = reader.uint(1);
if(!this.goldsrcInfo) {
state.raw.listentype = String.fromCharCode(state.raw.listentype);
state.raw.environment = String.fromCharCode(state.raw.environment);
}
state.password = !!reader.uint(1);
if(this.goldsrcInfo) {
state.raw.ismod = reader.uint(1);
if(state.raw.ismod) {
state.raw.modlink = reader.string();
state.raw.moddownload = reader.string();
reader.skip(1);
state.raw.modversion = reader.uint(4);
state.raw.modsize = reader.uint(4);
state.raw.modtype = reader.uint(1);
state.raw.moddll = reader.uint(1);
}
}
state.raw.secure = reader.uint(1);
if(this.goldsrcInfo) {
state.raw.numbots = reader.uint(1);
} else { } else {
c(); if(state.raw.folder === 'ship') {
state.raw.shipmode = reader.uint(1);
state.raw.shipwitnesses = reader.uint(1);
state.raw.shipduration = reader.uint(1);
}
state.raw.version = reader.string();
const extraFlag = reader.uint(1);
if(extraFlag & 0x80) state.gamePort = reader.uint(2);
if(extraFlag & 0x10) state.raw.steamid = reader.uint(8);
if(extraFlag & 0x40) {
state.raw.sourcetvport = reader.uint(2);
state.raw.sourcetvname = reader.string();
}
if(extraFlag & 0x20) state.raw.tags = reader.string();
if(extraFlag & 0x01) state.raw.gameid = reader.uint(8);
}
// from https://developer.valvesoftware.com/wiki/Server_queries
if(
state.raw.protocol === 7 && (
state.raw.steamappid === 215
|| state.raw.steamappid === 17550
|| state.raw.steamappid === 17700
|| state.raw.steamappid === 240
)
) {
this._skipSizeInSplitHeader = true;
}
this.debugLog("STEAM APPID: "+state.raw.steamappid);
this.debugLog("PROTOCOL: "+state.raw.protocol);
if(state.raw.protocol === 48) {
this.debugLog("GOLDSRC DETECTED - USING MODIFIED SPLIT FORMAT");
this.goldsrcSplits = true;
} }
} }
queryPlayers(state,c) { async queryChallenge() {
if(this.legacyChallenge) {
// sendPacket will catch the response packet and
// save the challenge for us
this.debugLog("Requesting legacy challenge key ...");
await this.sendPacket(
0x57,
false,
null,
0x41,
false
);
}
}
async queryPlayers(state) {
state.raw.players = []; state.raw.players = [];
this.sendPacket(0x55,true,null,0x44,(b) => {
const reader = this.reader(b);
const num = reader.uint(1);
for(let i = 0; i < num; i++) {
reader.skip(1);
const name = reader.string();
const score = reader.int(4);
const time = reader.float();
if(this.debug) console.log("Found player: "+name+" "+score+" "+time); // 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;
// connecting players don't count as players. this.debugLog("Requesting player list ...");
if(!name) continue; const b = await this.sendPacket(
0x55,
true,
null,
0x44,
allowTimeout
);
if (b === null) return; // timed out
// CSGO sometimes adds a bot named 'Max Players' if host_players_show is not 2 const reader = this.reader(b);
if (state.raw.steamappid === 730 && name === 'Max Players') continue; const num = reader.uint(1);
for(let i = 0; i < num; i++) {
reader.skip(1);
const name = reader.string();
const score = reader.int(4);
const time = reader.float();
state.raw.players.push({ this.debugLog("Found player: "+name+" "+score+" "+time);
name:name, score:score, time:time
});
}
c(); // connecting players don't count as players.
}, () => { if(!name) continue;
// CSGO doesn't even respond sometimes if host_players_show is not 2
// Ignore timeouts in only this case // CSGO sometimes adds a bot named 'Max Players' if host_players_show is not 2
if (state.raw.steamappid === 730) { if (state.raw.steamappid === 730 && name === 'Max Players') continue;
c();
return true; state.raw.players.push({
} name:name, score:score, time:time
}); });
}
} }
queryRules(state,c) { async queryRules(state) {
state.raw.rules = {}; state.raw.rules = {};
this.sendPacket(0x56,true,null,0x45,(b) => { this.debugLog("Requesting rules ...");
const reader = this.reader(b); const b = await this.sendPacket(0x56,true,null,0x45,true);
const num = reader.uint(2); if (b === null) return; // timed out - the server probably just has rules disabled
for(let i = 0; i < num; i++) {
const key = reader.string(); const reader = this.reader(b);
const value = reader.string(); const num = reader.uint(2);
state.raw.rules[key] = value; for(let i = 0; i < num; i++) {
} const key = reader.string();
c(); const value = reader.string();
}, () => { state.raw.rules[key] = value;
// 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,142 +230,157 @@ 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
) { ) {
for (let keyRetry = 0; keyRetry < 3; keyRetry++) {
let requestKeyChanged = false;
const response = await this.sendPacketRaw(
type, sendChallenge, payload,
(payload) => {
const reader = this.reader(payload);
const type = reader.uint(1);
this.debugLog(() => "Received " + type.toString(16) + " expected " + expect.toString(16));
if (type === 0x41) {
const key = reader.uint(4);
if (this._challenge !== key) {
this.debugLog('Received new challenge key: ' + key);
this._challenge = key;
if (sendChallenge) {
this.debugLog('Challenge key changed -- allowing query retry if needed');
requestKeyChanged = true;
}
}
}
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');
}
/**
* Sends a request packet and assembles partial responses
* @param {number} type
* @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 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 = {}; const packetStorage = {};
return await this.udpSend(
b,
(buffer) => {
const reader = this.reader(buffer);
const header = reader.int(4);
if(header === -1) {
// full package
this.debugLog("Received full packet");
return onResponse(reader.rest());
}
if(header === -2) {
// partial package
const uid = reader.uint(4);
if(!(uid in packetStorage)) packetStorage[uid] = {};
const packets = packetStorage[uid];
const receivedFull = (reader) => { let bzip = false;
const type = reader.uint(1); if(!this.goldsrcSplits && uid & 0x80000000) bzip = true;
if(type === 0x41) { let packetNum,payload,numPackets;
const key = reader.uint(4); if(this.goldsrcSplits) {
packetNum = reader.uint(1);
if(this.debug) console.log('Received challenge key: ' + key); numPackets = packetNum & 0x0f;
packetNum = (packetNum & 0xf0) >> 4;
if(this._challenge !== key) { payload = reader.rest();
this._challenge = key; } else {
if(sendChallenge) { numPackets = reader.uint(1);
if (this.debug) console.log('Restarting query'); packetNum = reader.uint(1);
send(); if(!this._skipSizeInSplitHeader) reader.skip(2);
return true; if(packetNum === 0 && bzip) reader.skip(8);
payload = reader.rest();
} }
}
return; packets[packetNum] = payload;
}
if(this.debug) console.log("Received "+type.toString(16)+" expected "+expect.toString(16)); this.debugLog(() => "Received partial packet uid:"+uid+" num:"+packetNum);
if(type !== expect) return; this.debugLog(() => "Received "+Object.keys(packets).length+'/'+numPackets+" packets for this UID");
callback(reader.rest());
return true;
};
const receivedOne = (buffer) => { if(Object.keys(packets).length !== numPackets) return;
const reader = this.reader(buffer);
const header = reader.int(4); // assemble the parts
if(header === -1) { const list = [];
// full package for(let i = 0; i < numPackets; i++) {
if(this.debug) console.log("Received full packet"); if(!(i in packets)) {
return receivedFull(reader); throw new Error('Missing packet #'+i);
} }
if(header === -2) { list.push(packets[i]);
// partial package
const uid = reader.uint(4);
if(!(uid in packetStorage)) packetStorage[uid] = {};
const packets = packetStorage[uid];
let bzip = false;
if(!this.goldsrcSplits && uid & 0x80000000) bzip = true;
let packetNum,payload,numPackets;
if(this.goldsrcSplits) {
packetNum = reader.uint(1);
numPackets = packetNum & 0x0f;
packetNum = (packetNum & 0xf0) >> 4;
payload = reader.rest();
} else {
numPackets = reader.uint(1);
packetNum = reader.uint(1);
if(!this._skipSizeInSplitHeader) reader.skip(2);
if(packetNum === 0 && bzip) reader.skip(8);
payload = reader.rest();
}
packets[packetNum] = payload;
if(this.debug) {
console.log("Received partial packet uid:"+uid+" num:"+packetNum);
console.log("Received "+Object.keys(packets).length+'/'+numPackets+" packets for this UID");
}
if(Object.keys(packets).length !== numPackets) return;
// assemble the parts
const list = [];
for(let i = 0; i < numPackets; i++) {
if(!(i in packets)) {
this.fatal('Missing packet #'+i);
return true;
} }
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);
assembledReader.skip(4); // header
return onResponse(assembledReader.rest());
} }
const assembledReader = this.reader(assembled); },
assembledReader.skip(4); // header onTimeout
return receivedFull(assembledReader); );
}
};
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,31 +5,30 @@ class Ventrilo extends Core {
super(); super();
this.byteorder = 'be'; this.byteorder = 'be';
} }
run(state) {
this.sendCommand(2,'',(data) => {
state.raw = splitFields(data.toString());
for (const client of state.raw.CLIENTS) {
client.name = client.NAME;
delete client.NAME;
client.ping = parseInt(client.PING);
delete client.PING;
state.players.push(client);
}
delete state.raw.CLIENTS;
if('NAME' in state.raw) state.name = state.raw.NAME; async run(state) {
if('MAXCLIENTS' in state.raw) state.maxplayers = state.raw.MAXCLIENTS; const data = await this.sendCommand(2,'');
if(this.trueTest(state.raw.AUTH)) state.password = true; state.raw = splitFields(data.toString());
this.finish(state); for (const client of state.raw.CLIENTS) {
}); client.name = client.NAME;
delete client.NAME;
client.ping = parseInt(client.PING);
delete client.PING;
state.players.push(client);
}
delete state.raw.CLIENTS;
if('NAME' in state.raw) state.name = state.raw.NAME;
if('MAXCLIENTS' in state.raw) state.maxplayers = state.raw.MAXCLIENTS;
if(this.trueTest(state.raw.AUTH)) state.password = true;
} }
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;