Merge remote-tracking branch 'origin/async'
This commit is contained in:
commit
075106c190
129
README.md
129
README.md
|
@ -14,75 +14,53 @@ Usage from Node.js
|
|||
npm install gamedig
|
||||
```
|
||||
|
||||
Promise:
|
||||
```javascript
|
||||
const Gamedig = require('gamedig');
|
||||
Gamedig.query({
|
||||
type: 'minecraft',
|
||||
host: 'mc.example.com'
|
||||
type: 'minecraft',
|
||||
host: 'mc.example.com'
|
||||
}).then((state) => {
|
||||
console.log(state);
|
||||
console.log(state);
|
||||
}).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
|
||||
|
||||
**Typical**
|
||||
|
||||
* **type**: One of the game IDs listed in the game list below
|
||||
* **host**: Hostname or IP of the game server
|
||||
* **port**: (optional) Uses the protocol default if not set
|
||||
* **type**: string - One of the game IDs listed in the game list below
|
||||
* **host**: string - Hostname or IP of the game server
|
||||
* **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**
|
||||
|
||||
* **notes**: (optional) An object passed through in the return value.
|
||||
* **maxAttempts**: (optional) Number of attempts to query server in case of failure. (default 1)
|
||||
* **socketTimeout**: (optional) Milliseconds to wait for a single packet. Beware that increasing this
|
||||
* **maxAttempts**: number - 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
|
||||
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)
|
||||
* **debug**: boolean - Enables massive amounts of debug logging to stdout. (default false)
|
||||
|
||||
### Return Value
|
||||
|
||||
The returned state object will contain the following keys:
|
||||
|
||||
**Stable, always present:**
|
||||
|
||||
* **name**
|
||||
* **map**
|
||||
* **password**: Boolean
|
||||
* **maxplayers**
|
||||
* **players**: (array of objects) Each object **may** contain name, ping, score, team, address
|
||||
* **bots**: Same schema as players
|
||||
* **notes**: Passed through from the input
|
||||
|
||||
**Unstable, not guaranteed:**
|
||||
|
||||
* **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.
|
||||
* **name**: string - Server name
|
||||
* **map**: string - Current server game map
|
||||
* **password**: boolean - If a password is required
|
||||
* **maxplayers**: number
|
||||
* **players**: array of objects
|
||||
* Each object **may or may not** contain name, ping, score, team, address.
|
||||
* The number of players online can be determined by `players.length`.
|
||||
* For servers which do not provide player names, this may be an array
|
||||
of empty objects (ex. `[{},{},{}]`), one for each player without a name.
|
||||
* **bots**: array of objects - Same schema as players
|
||||
* **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).
|
||||
|
||||
Games List
|
||||
---
|
||||
|
@ -365,8 +343,6 @@ Games List
|
|||
> __Know how to code?__ Protocols for most of the games above are documented
|
||||
> in the /reference folder, ready for you to develop into GameDig!
|
||||
|
||||
<!-- -->
|
||||
|
||||
> Don't see your game listed here?
|
||||
>
|
||||
> First, let us know so we can fix it. Then, you can try using some common query
|
||||
|
@ -392,7 +368,7 @@ have set the cvar: host_players_show 2
|
|||
|
||||
### DayZ
|
||||
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
|
||||
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.
|
||||
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,
|
||||
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
|
||||
---
|
||||
|
@ -435,17 +411,56 @@ You'll still need npm to install gamedig:
|
|||
npm install gamedig -g
|
||||
```
|
||||
|
||||
After installing gamedig globally, you can call gamedig via the command line
|
||||
using the same parameters mentioned in the API above:
|
||||
After installing gamedig globally, you can call gamedig via the command line:
|
||||
```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
|
||||
* First official release
|
||||
* Node.js 6.0 is now required
|
||||
* Node.js 6 is now required
|
||||
|
|
|
@ -5,8 +5,8 @@ const argv = require('minimist')(process.argv.slice(2)),
|
|||
|
||||
const debug = argv.debug;
|
||||
delete argv.debug;
|
||||
const outputFormat = argv.output;
|
||||
delete argv.output;
|
||||
const pretty = !!argv.pretty || debug;
|
||||
delete argv.pretty;
|
||||
|
||||
const options = {};
|
||||
for(const key of Object.keys(argv)) {
|
||||
|
@ -14,18 +14,26 @@ for(const key of Object.keys(argv)) {
|
|||
if(
|
||||
key === '_'
|
||||
|| key.charAt(0) === '$'
|
||||
|| (typeof value !== 'string' && typeof value !== 'number')
|
||||
)
|
||||
continue;
|
||||
options[key] = value;
|
||||
}
|
||||
|
||||
if(debug) Gamedig.debug = true;
|
||||
Gamedig.isCommandLine = true;
|
||||
if (argv._.length >= 1) {
|
||||
const target = argv._[0];
|
||||
const split = target.split(':');
|
||||
options.host = split[0];
|
||||
if (split.length >= 2) {
|
||||
options.port = split[1];
|
||||
}
|
||||
}
|
||||
if (debug) {
|
||||
options.debug = true;
|
||||
}
|
||||
|
||||
Gamedig.query(options)
|
||||
.then((state) => {
|
||||
if(outputFormat === 'pretty') {
|
||||
if(pretty) {
|
||||
console.log(JSON.stringify(state,null,' '));
|
||||
} else {
|
||||
console.log(JSON.stringify(state));
|
||||
|
@ -42,7 +50,7 @@ Gamedig.query(options)
|
|||
if (error instanceof Error) {
|
||||
error = error.message;
|
||||
}
|
||||
if (outputFormat === 'pretty') {
|
||||
if (pretty) {
|
||||
console.log(JSON.stringify({error: error}, null, ' '));
|
||||
} else {
|
||||
console.log(JSON.stringify({error: error}));
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs'),
|
||||
TypeResolver = require('../lib/typeresolver');
|
||||
TypeResolver = require('../lib/GameResolver');
|
||||
|
||||
const generated = TypeResolver.printReadme();
|
||||
|
||||
|
|
125
games.txt
125
games.txt
|
@ -1,4 +1,4 @@
|
|||
# id | pretty | protocol | options | parameters
|
||||
# id | pretty name for readme | protocol | options | extra
|
||||
|
||||
#### TODO:
|
||||
# cube1|Cube 1|cube|port=28786,port_query_offset=1
|
||||
|
@ -15,7 +15,6 @@
|
|||
# gr|Ghost Recon|ghostrecon|port=2346,port_query_offset=2
|
||||
# gtr2|GTR2|gtr2|port=34297,port_query_offset=1
|
||||
# haze|Haze|haze
|
||||
# openttd|OpenTTD|openttd|port=3979
|
||||
# plainsight|Plain Sight|plainsight
|
||||
# redfaction|Red Faction|redfaction|port_query=7755
|
||||
# savage|Savage|savage|port_query=11235
|
||||
|
@ -28,18 +27,18 @@
|
|||
|
||||
|
||||
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
|
||||
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
|
||||
avp2|Aliens vs Predator 2|gamespy1|port=27888
|
||||
# avp2010 doesn't really... have a default port or query port
|
||||
# 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
|
||||
americasarmy2|America's Army 2|americasarmy|port=1716,port_query_offset=1
|
||||
americasarmy|America's Army|gamespy2|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
|
||||
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
|
||||
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
|
||||
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
|
||||
bf4|Battlefield 4|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
|
||||
breed|Breed|gamespy2|port=7649
|
||||
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
|
||||
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
|
||||
contactjack|Contact J.A.C.K.|gamespy1|port_query=27888
|
||||
|
||||
cs16|Counter-Strike 1.6|valve
|
||||
cscz|Counter-Strike: Condition Zero|valve
|
||||
css|Counter-Strike: Source|valve
|
||||
csgo|Counter-Strike: Global Offensive|valve||doc_notes=csgo
|
||||
cs16|Counter-Strike 1.6|valve|port=27015
|
||||
cscz|Counter-Strike: Condition Zero|valve|port=27015
|
||||
css|Counter-Strike: Source|valve|port=27015
|
||||
csgo|Counter-Strike: Global Offensive|valve||port=27015|doc_notes=csgo
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
dayz|DayZ|valve|port=2302,port_query_offset=24714|doc_notes=dayz
|
||||
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
|
||||
deusex|Deus Ex|gamespy2|port=7791,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
|
||||
dnl|Dark and Light|valve|port=7777,port_query=27015
|
||||
dod|Day of Defeat|valve
|
||||
dods|Day of Defeat: Source|valve
|
||||
doi|Day of Infamy|valve
|
||||
dod|Day of Defeat|valve|port=27015
|
||||
dods|Day of Defeat: Source|valve|port=27015
|
||||
doi|Day of Infamy|valve|port=27015
|
||||
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
|
||||
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
|
||||
f12002|F1 2002|gamespy1|port_query=3297
|
||||
f1c9902|F1 Challenge 99-02|gamespy1|port_query=34397
|
||||
farcry|Far Cry|ase|port=49001,port_query_offset=123
|
||||
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
|
||||
ffow|Frontlines: Fuel of War|ffow|port=5476,port_query_offset=2
|
||||
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
|
||||
graw2|Ghost Recon: Advanced Warfighter 2|gamespy2|port_query=16250
|
||||
giantscitizenkabuto|Giants: Citizen Kabuto|gamespy1|port_query=8911
|
||||
globaloperations|Global Operations|gamespy1|port_query=28672
|
||||
geneshift|Geneshift|geneshift|port=11235
|
||||
ges|GoldenEye: Source|valve
|
||||
ges|GoldenEye: Source|valve|port=27015
|
||||
gore|Gore|gamespy1|port=27777,port_query_offset=1
|
||||
gunmanchronicles|Gunman Chronicles|valve
|
||||
hldm|Half-Life 1 Deathmatch|valve
|
||||
hl2dm|Half-Life 2 Deathmatch|valve
|
||||
gunmanchronicles|Gunman Chronicles|valve|port=27015
|
||||
hldm|Half-Life 1 Deathmatch|valve|port=27015
|
||||
hl2dm|Half-Life 2 Deathmatch|valve|port=27015
|
||||
halo|Halo|gamespy2|port=2302
|
||||
halo2|Halo 2|gamespy2|port=2302
|
||||
heretic2|Heretic 2|gamespy1|port=27900,port_query_offset=1
|
||||
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
|
||||
homefront|Homefront|valve
|
||||
homefront|Homefront|valve|port=27015
|
||||
homeworld2|Homeworld 2|gamespy1|port_query=6500
|
||||
hurtworld|Hurtworld|valve|port=12871,port_query=12881
|
||||
igi2|IGI-2: Covert Strike|gamespy1|port_query=26001
|
||||
il2|IL-2 Sturmovik|gamespy1|port_query=21000
|
||||
insurgency|Insurgency|valve
|
||||
insurgency|Insurgency|valve|port=27015
|
||||
ironstorm|Iron Storm|gamespy1|port_query=3505
|
||||
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
|
||||
killingfloor2|Killing Floor 2|valve|port=7777,port_query=27015
|
||||
kingpin|Kingpin: Life of Crime|gamespy1|port=31510,port_query_offset=-10
|
||||
kisspc|KISS Psycho Circus|gamespy1|port=7777,port_query_offset=1
|
||||
kspdmp|DMP - KSP Multiplayer|kspdmp|port=6702,port_query_offset=1
|
||||
kzmod|KzMod|valve
|
||||
left4dead|Left 4 Dead|valve
|
||||
left4dead2|Left 4 Dead 2|valve
|
||||
kzmod|KzMod|valve|port=27015
|
||||
left4dead|Left 4 Dead|valve|port=27015
|
||||
left4dead2|Left 4 Dead 2|valve|port=27015
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
mutantfactions|Mutant Factions|geneshift|port=11235
|
||||
nascarthunder2004|Nascar Thunder 2004|gamespy2|port_query=13333
|
||||
netpanzer|netPanzer|gamespy1|3030
|
||||
nmrih|No More Room in Hell|valve
|
||||
ns|Natural Selection|valve
|
||||
netpanzer|netPanzer|gamespy1|port=3030
|
||||
nmrih|No More Room in Hell|valve|port=27015
|
||||
ns|Natural Selection|valve|port=27015
|
||||
ns2|Natural Selection 2|valve|port_query_offset=1
|
||||
nfshp2|Need for Speed: Hot Pursuit 2|gamespy1|port_query=61220
|
||||
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
|
||||
nolf|No One Lives Forever|gamespy1|port_query=27888
|
||||
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
|
||||
openttd|OpenTTD|openttd|port=3979
|
||||
operationflashpoint|Operation Flashpoint|gamespy1|port=2234,port_query_offset=1
|
||||
painkiller|Painkiller|ase|port=3455,port_query_offset=123
|
||||
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
|
||||
|
||||
quake1|Quake 1: QuakeWorld|quake1|port=27500
|
||||
quake2|Quake 2|quake2|port=27910
|
||||
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
|
||||
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
|
||||
redline|Redline|gamespy1|port_query=25252
|
||||
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
|
||||
rune|Rune|gamespy1|port=7777,port_query_offset=1
|
||||
rust|Rust|valve|port=28015
|
||||
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
|
||||
ss2|Serious Sam 2|gamespy2|port=25600
|
||||
shatteredhorizon|Shattered Horizon|valve
|
||||
ship|The Ship|valve
|
||||
shatteredhorizon|Shattered Horizon|valve|port=27015
|
||||
ship|The Ship|valve|port=27015
|
||||
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
|
||||
sinep|SiN Episodes|valve
|
||||
sinep|SiN Episodes|valve|port=27015
|
||||
soldat|Soldat|ase|port=13073,port_query_offset=123
|
||||
sof|Soldier of Fortune|quake1|port_query=28910
|
||||
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
|
||||
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
|
||||
svencoop|Sven Coop|valve
|
||||
synergy|Synergy|valve
|
||||
svencoop|Sven Coop|valve|port=27015
|
||||
synergy|Synergy|valve|port=27015
|
||||
tacticalops|Tactical Ops|gamespy1|port=7777,port_query_offset=1
|
||||
teamfactor|Team Factor|gamespy1|port_query=57778
|
||||
tfc|Team Fortress Classic|valve
|
||||
tf2|Team Fortress 2|valve
|
||||
teamspeak2|Teamspeak 2|teamspeak2|port=8767,port_query=51234
|
||||
teamspeak3|Teamspeak 3|teamspeak3|port=9987,port_query=10011|doc_notes=teamspeak3
|
||||
tfc|Team Fortress Classic|valve|port=27015
|
||||
tf2|Team Fortress 2|valve|port=27015
|
||||
teamspeak2|Teamspeak 2|teamspeak2|port=8767
|
||||
teamspeak3|Teamspeak 3|teamspeak3|port=9987|doc_notes=teamspeak3
|
||||
terminus|Terminus|gamespy1|port_query=12286
|
||||
terraria|Terraria|terraria|port=7777,port_query_offset=101|doc_notes=terraria
|
||||
thps3|Tony Hawk's Pro Skater 3|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
|
||||
towerunite|Tower Unite|valve
|
||||
trackmania2|Trackmania 2|nadeo||doc_notes=nadeo-shootmania--trackmania--etc
|
||||
trackmaniaforever|Trackmania Forever|nadeo||doc_notes=nadeo-shootmania--trackmania--etc
|
||||
towerunite|Tower Unite|valve|port=27015
|
||||
trackmania2|Trackmania 2|nadeo|port=2350,port_query=5000|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
|
||||
tribes1|Tribes 1: Starsiege|tribes1|port=28001
|
||||
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
|
||||
warsow|Warsow|warsow|port=44400
|
||||
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
|
||||
xpandrally|Xpand Rally|ase|port=28015,port_query_offset=123
|
||||
zombiemaster|Zombie Master|valve
|
||||
zps|Zombie Panic: Source|valve
|
||||
zombiemaster|Zombie Master|valve|port=27015
|
||||
zps|Zombie Panic: Source|valve|port=27015
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
114
lib/index.js
114
lib/index.js
|
@ -1,107 +1,23 @@
|
|||
const dgram = require('dgram'),
|
||||
TypeResolver = require('./typeresolver'),
|
||||
HexUtil = require('./HexUtil');
|
||||
const QueryRunner = require('./QueryRunner');
|
||||
|
||||
const activeQueries = [];
|
||||
|
||||
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);
|
||||
});
|
||||
let singleton = null;
|
||||
|
||||
class Gamedig {
|
||||
|
||||
static query(options,callback) {
|
||||
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;
|
||||
constructor() {
|
||||
this.queryRunner = new QueryRunner();
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
const Iconv = require('iconv-lite'),
|
||||
Long = require('long'),
|
||||
Core = require('../protocols/core'),
|
||||
Buffer = require('buffer');
|
||||
Buffer = require('buffer'),
|
||||
Varint = require('varint');
|
||||
|
||||
function readUInt64BE(buffer,offset) {
|
||||
const high = buffer.readUInt32BE(offset);
|
||||
|
@ -126,6 +127,12 @@ class Reader {
|
|||
return r;
|
||||
}
|
||||
|
||||
varint() {
|
||||
const out = Varint.decode(this.buffer, this.i);
|
||||
this.i += Varint.decode.bytes;
|
||||
return out;
|
||||
}
|
||||
|
||||
/** @returns Buffer */
|
||||
part(bytes) {
|
||||
let r;
|
||||
|
|
|
@ -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;
|
|
@ -1,18 +1,23 @@
|
|||
{
|
||||
"name": "gamedig",
|
||||
"version": "1.0.41",
|
||||
"version": "1.0.49",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"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": {
|
||||
"version": "5.5.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
|
||||
"integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.2.tgz",
|
||||
"integrity": "sha512-FBHEW6Jf5TB9MGBgUUA9XHkTbjXYfAUjY43ACMfmdMRHniyoMHjHjzD50OK8LGDWQwp4rWEsIq5kEqq7rvIM1g==",
|
||||
"requires": {
|
||||
"co": "4.6.0",
|
||||
"fast-deep-equal": "1.1.0",
|
||||
"fast-json-stable-stringify": "2.0.0",
|
||||
"json-schema-traverse": "0.3.1"
|
||||
"fast-deep-equal": "^2.0.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
}
|
||||
},
|
||||
"amdefine": {
|
||||
|
@ -21,20 +26,18 @@
|
|||
"integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU="
|
||||
},
|
||||
"asn1": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
|
||||
"integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y="
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
|
||||
"integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
|
||||
"requires": {
|
||||
"safer-buffer": "~2.1.0"
|
||||
}
|
||||
},
|
||||
"assert-plus": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
|
||||
},
|
||||
"async": {
|
||||
"version": "0.9.2",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz",
|
||||
"integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0="
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
|
@ -46,51 +49,60 @@
|
|||
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
|
||||
},
|
||||
"aws4": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz",
|
||||
"integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4="
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
|
||||
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
|
||||
},
|
||||
"barse": {
|
||||
"version": "0.4.3",
|
||||
"resolved": "https://registry.npmjs.org/barse/-/barse-0.4.3.tgz",
|
||||
"integrity": "sha1-KJhk15XQECu7sYHmbs0IxUobwMs=",
|
||||
"requires": {
|
||||
"readable-stream": "1.0.34"
|
||||
"readable-stream": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"bcrypt-pbkdf": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz",
|
||||
"integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=",
|
||||
"optional": true,
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
"integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
|
||||
"requires": {
|
||||
"tweetnacl": "0.14.5"
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"boom": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz",
|
||||
"integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=",
|
||||
"requires": {
|
||||
"hoek": "4.2.1"
|
||||
}
|
||||
"bluebird": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz",
|
||||
"integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw=="
|
||||
},
|
||||
"boolbase": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
|
||||
},
|
||||
"caseless": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
|
||||
"integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
|
||||
},
|
||||
"co": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||
"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ="
|
||||
"cheerio": {
|
||||
"version": "1.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.2.tgz",
|
||||
"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": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz",
|
||||
"integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=",
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
|
||||
"integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
|
||||
"requires": {
|
||||
"delayed-stream": "1.0.0"
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"commander": {
|
||||
|
@ -98,7 +110,7 @@
|
|||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz",
|
||||
"integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=",
|
||||
"requires": {
|
||||
"graceful-readlink": "1.0.1"
|
||||
"graceful-readlink": ">= 1.0.0"
|
||||
}
|
||||
},
|
||||
"compressjs": {
|
||||
|
@ -106,8 +118,8 @@
|
|||
"resolved": "https://registry.npmjs.org/compressjs/-/compressjs-1.0.3.tgz",
|
||||
"integrity": "sha1-ldt03VuQOM+AvKMhqw7eJxtJWbY=",
|
||||
"requires": {
|
||||
"amdefine": "1.0.1",
|
||||
"commander": "2.8.1"
|
||||
"amdefine": "~1.0.0",
|
||||
"commander": "~2.8.1"
|
||||
}
|
||||
},
|
||||
"core-util-is": {
|
||||
|
@ -115,30 +127,28 @@
|
|||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
},
|
||||
"cryptiles": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz",
|
||||
"integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=",
|
||||
"css-select": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
|
||||
"integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
|
||||
"requires": {
|
||||
"boom": "5.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"boom": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz",
|
||||
"integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==",
|
||||
"requires": {
|
||||
"hoek": "4.2.1"
|
||||
}
|
||||
}
|
||||
"boolbase": "~1.0.0",
|
||||
"css-what": "2.1",
|
||||
"domutils": "1.5.1",
|
||||
"nth-check": "~1.0.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": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
|
||||
"requires": {
|
||||
"assert-plus": "1.0.0"
|
||||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"delayed-stream": {
|
||||
|
@ -146,19 +156,62 @@
|
|||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
|
||||
},
|
||||
"ecc-jsbn": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz",
|
||||
"integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=",
|
||||
"optional": true,
|
||||
"dom-serializer": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
|
||||
"integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=",
|
||||
"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": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
|
||||
"integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ="
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
|
||||
},
|
||||
"extsprintf": {
|
||||
"version": "1.3.0",
|
||||
|
@ -166,9 +219,9 @@
|
|||
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
|
||||
},
|
||||
"fast-deep-equal": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
|
||||
"integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ="
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
|
||||
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
|
||||
},
|
||||
"fast-json-stable-stringify": {
|
||||
"version": "2.0.0",
|
||||
|
@ -181,13 +234,13 @@
|
|||
"integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz",
|
||||
"integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=",
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
|
||||
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
|
||||
"requires": {
|
||||
"asynckit": "0.4.0",
|
||||
"combined-stream": "1.0.6",
|
||||
"mime-types": "2.1.18"
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.6",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"gbxremote": {
|
||||
|
@ -195,8 +248,8 @@
|
|||
"resolved": "https://registry.npmjs.org/gbxremote/-/gbxremote-0.1.4.tgz",
|
||||
"integrity": "sha1-x+0iWC5WBRtOF2AbPdWjAE7u/UM=",
|
||||
"requires": {
|
||||
"barse": "0.4.3",
|
||||
"sax": "0.4.3",
|
||||
"barse": "~0.4.2",
|
||||
"sax": "0.4.x",
|
||||
"xmlbuilder": "0.3.1"
|
||||
}
|
||||
},
|
||||
|
@ -205,7 +258,7 @@
|
|||
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
|
||||
"integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
|
||||
"requires": {
|
||||
"assert-plus": "1.0.0"
|
||||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"graceful-readlink": {
|
||||
|
@ -219,38 +272,55 @@
|
|||
"integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
|
||||
},
|
||||
"har-validator": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz",
|
||||
"integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=",
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
|
||||
"integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
|
||||
"requires": {
|
||||
"ajv": "5.5.2",
|
||||
"har-schema": "2.0.0"
|
||||
"ajv": "^6.5.5",
|
||||
"har-schema": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"hawk": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz",
|
||||
"integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==",
|
||||
"htmlparser2": {
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.0.tgz",
|
||||
"integrity": "sha512-J1nEUGv+MkXS0weHNWVKJJ+UrLfePxRWpN3C9bEi9fLxL2+ggW94DQvgYVXsaT30PGwYRIZKNZXuyMhp3Di4bQ==",
|
||||
"requires": {
|
||||
"boom": "4.3.1",
|
||||
"cryptiles": "3.1.2",
|
||||
"hoek": "4.2.1",
|
||||
"sntp": "2.1.0"
|
||||
"domelementtype": "^1.3.0",
|
||||
"domhandler": "^2.3.0",
|
||||
"domutils": "^1.5.1",
|
||||
"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": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
|
||||
"integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
|
||||
"requires": {
|
||||
"assert-plus": "1.0.0",
|
||||
"jsprim": "1.4.1",
|
||||
"sshpk": "1.14.1"
|
||||
"assert-plus": "^1.0.0",
|
||||
"jsprim": "^1.2.2",
|
||||
"sshpk": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"iconv-lite": {
|
||||
|
@ -263,6 +333,11 @@
|
|||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||
|
@ -281,8 +356,7 @@
|
|||
"jsbn": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
||||
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
|
||||
"optional": true
|
||||
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
|
||||
},
|
||||
"json-schema": {
|
||||
"version": "0.2.3",
|
||||
|
@ -290,9 +364,9 @@
|
|||
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
|
||||
},
|
||||
"json-schema-traverse": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
|
||||
"integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A="
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
|
||||
},
|
||||
"json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
|
@ -310,22 +384,27 @@
|
|||
"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": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-2.4.0.tgz",
|
||||
"integrity": "sha1-n6GAux2VAM3CnEFWdmoZleH0Uk8="
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
|
||||
"integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ=="
|
||||
"version": "1.37.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz",
|
||||
"integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.18",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
|
||||
"integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
|
||||
"version": "2.1.21",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz",
|
||||
"integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==",
|
||||
"requires": {
|
||||
"mime-db": "1.33.0"
|
||||
"mime-db": "~1.37.0"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
|
@ -338,115 +417,171 @@
|
|||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.21.0.tgz",
|
||||
"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": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz",
|
||||
"integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM="
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
|
||||
"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": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"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": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.5.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",
|
||||
"integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A=="
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
||||
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "1.0.34",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
|
||||
"integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
|
||||
"requires": {
|
||||
"core-util-is": "1.0.2",
|
||||
"inherits": "2.0.3",
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.1",
|
||||
"isarray": "0.0.1",
|
||||
"string_decoder": "0.10.31"
|
||||
"string_decoder": "~0.10.x"
|
||||
}
|
||||
},
|
||||
"request": {
|
||||
"version": "2.85.0",
|
||||
"resolved": "https://registry.npmjs.org/request/-/request-2.85.0.tgz",
|
||||
"integrity": "sha512-8H7Ehijd4js+s6wuVPLjwORxD4zeuyjYugprdOXlPSqaApmL/QOy+EB/beICHVCHkGMKNh5rvihb5ov+IDw4mg==",
|
||||
"version": "2.88.0",
|
||||
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
|
||||
"integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
|
||||
"requires": {
|
||||
"aws-sign2": "0.7.0",
|
||||
"aws4": "1.6.0",
|
||||
"caseless": "0.12.0",
|
||||
"combined-stream": "1.0.6",
|
||||
"extend": "3.0.1",
|
||||
"forever-agent": "0.6.1",
|
||||
"form-data": "2.3.2",
|
||||
"har-validator": "5.0.3",
|
||||
"hawk": "6.0.2",
|
||||
"http-signature": "1.2.0",
|
||||
"is-typedarray": "1.0.0",
|
||||
"isstream": "0.1.2",
|
||||
"json-stringify-safe": "5.0.1",
|
||||
"mime-types": "2.1.18",
|
||||
"oauth-sign": "0.8.2",
|
||||
"performance-now": "2.1.0",
|
||||
"qs": "6.5.1",
|
||||
"safe-buffer": "5.1.1",
|
||||
"stringstream": "0.0.5",
|
||||
"tough-cookie": "2.3.4",
|
||||
"tunnel-agent": "0.6.0",
|
||||
"uuid": "3.2.1"
|
||||
"aws-sign2": "~0.7.0",
|
||||
"aws4": "^1.8.0",
|
||||
"caseless": "~0.12.0",
|
||||
"combined-stream": "~1.0.6",
|
||||
"extend": "~3.0.2",
|
||||
"forever-agent": "~0.6.1",
|
||||
"form-data": "~2.3.2",
|
||||
"har-validator": "~5.1.0",
|
||||
"http-signature": "~1.2.0",
|
||||
"is-typedarray": "~1.0.0",
|
||||
"isstream": "~0.1.2",
|
||||
"json-stringify-safe": "~5.0.1",
|
||||
"mime-types": "~2.1.19",
|
||||
"oauth-sign": "~0.9.0",
|
||||
"performance-now": "^2.1.0",
|
||||
"qs": "~6.5.2",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"tough-cookie": "~2.4.3",
|
||||
"tunnel-agent": "^0.6.0",
|
||||
"uuid": "^3.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"tough-cookie": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
|
||||
"integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
|
||||
"requires": {
|
||||
"psl": "^1.1.24",
|
||||
"punycode": "^1.4.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
|
||||
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"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": {
|
||||
"version": "0.4.3",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-0.4.3.tgz",
|
||||
"integrity": "sha1-cA46NOsueSzjgHkccSgPNzGWXdw="
|
||||
},
|
||||
"sntp": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz",
|
||||
"integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==",
|
||||
"sshpk": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.0.tgz",
|
||||
"integrity": "sha512-Zhev35/y7hRMcID/upReIvRse+I9SVhyVre/KTJSJQWMz3C3+G+HpO7m1wK/yckEtujKZ7dS4hkVxAnmHaIGVQ==",
|
||||
"requires": {
|
||||
"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": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz",
|
||||
"integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=",
|
||||
"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"
|
||||
}
|
||||
"stealthy-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
|
||||
"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks="
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "0.10.31",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
||||
"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": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz",
|
||||
"integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.0.tgz",
|
||||
"integrity": "sha512-LHMvg+RBP/mAVNqVbOX8t+iJ+tqhBA/t49DuI7+IDAWHrASnesqSu1vWbKB7UrE2yk+HMFUBMadRGMkB4VCfog==",
|
||||
"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": {
|
||||
|
@ -454,19 +589,38 @@
|
|||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
"integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
|
||||
"requires": {
|
||||
"safe-buffer": "5.1.1"
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"tweetnacl": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
|
||||
"optional": true
|
||||
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
|
||||
},
|
||||
"uri-js": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
|
||||
"integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
|
||||
"requires": {
|
||||
"punycode": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz",
|
||||
"integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA=="
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
|
||||
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
|
||||
},
|
||||
"varint": {
|
||||
"version": "4.0.1",
|
||||
|
@ -478,9 +632,9 @@
|
|||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
||||
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
|
||||
"requires": {
|
||||
"assert-plus": "1.0.0",
|
||||
"assert-plus": "^1.0.0",
|
||||
"core-util-is": "1.0.2",
|
||||
"extsprintf": "1.3.0"
|
||||
"extsprintf": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"xmlbuilder": {
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
],
|
||||
"main": "lib/index.js",
|
||||
"author": "Michael Morrison",
|
||||
"version": "1.0.49",
|
||||
"version": "2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sonicsnes/node-gamedig.git"
|
||||
|
@ -21,17 +21,18 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"async": "^0.9.2",
|
||||
"cheerio": "^1.0.0-rc.2",
|
||||
"compressjs": "^1.0.2",
|
||||
"gbxremote": "^0.1.4",
|
||||
"iconv-lite": "^0.4.18",
|
||||
"long": "^2.4.0",
|
||||
"minimist": "^1.2.0",
|
||||
"moment": "^2.21.0",
|
||||
"request": "^2.85.0",
|
||||
"request": "^2.88.0",
|
||||
"request-promise": "^4.2.2",
|
||||
"varint": "^4.0.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
|
@ -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;
|
|
@ -7,38 +7,35 @@ class Armagetron extends Core {
|
|||
this.byteorder = 'be';
|
||||
}
|
||||
|
||||
run(state) {
|
||||
async run(state) {
|
||||
const b = Buffer.from([0,0x35,0,0,0,0,0,0x11]);
|
||||
|
||||
this.udpSend(b,(buffer) => {
|
||||
const reader = this.reader(buffer);
|
||||
const buffer = await this.udpSend(b,b => b);
|
||||
const reader = this.reader(buffer);
|
||||
|
||||
reader.skip(6);
|
||||
reader.skip(6);
|
||||
|
||||
state.raw.port = this.readUInt(reader);
|
||||
state.raw.hostname = this.readString(reader);
|
||||
state.name = this.stripColorCodes(this.readString(reader));
|
||||
state.raw.numplayers = this.readUInt(reader);
|
||||
state.raw.versionmin = this.readUInt(reader);
|
||||
state.raw.versionmax = this.readUInt(reader);
|
||||
state.raw.version = this.readString(reader);
|
||||
state.maxplayers = this.readUInt(reader);
|
||||
state.gamePort = this.readUInt(reader);
|
||||
state.raw.hostname = this.readString(reader);
|
||||
state.name = this.stripColorCodes(this.readString(reader));
|
||||
state.raw.numplayers = this.readUInt(reader);
|
||||
state.raw.versionmin = this.readUInt(reader);
|
||||
state.raw.versionmax = this.readUInt(reader);
|
||||
state.raw.version = this.readString(reader);
|
||||
state.maxplayers = this.readUInt(reader);
|
||||
|
||||
const players = this.readString(reader);
|
||||
const list = players.split('\n');
|
||||
for(const name of list) {
|
||||
if(!name) continue;
|
||||
state.players.push({
|
||||
name: this.stripColorCodes(name)
|
||||
});
|
||||
}
|
||||
const players = this.readString(reader);
|
||||
const list = players.split('\n');
|
||||
for(const name of list) {
|
||||
if(!name) continue;
|
||||
state.players.push({
|
||||
name: this.stripColorCodes(name)
|
||||
});
|
||||
}
|
||||
|
||||
state.raw.options = this.stripColorCodes(this.readString(reader));
|
||||
state.raw.uri = this.readString(reader);
|
||||
state.raw.globalids = this.readString(reader);
|
||||
this.finish(state);
|
||||
return true;
|
||||
});
|
||||
state.raw.options = this.stripColorCodes(this.readString(reader));
|
||||
state.raw.uri = this.readString(reader);
|
||||
state.raw.globalids = this.readString(reader);
|
||||
}
|
||||
|
||||
readUInt(reader) {
|
||||
|
|
|
@ -1,44 +1,42 @@
|
|||
const Core = require('./core');
|
||||
|
||||
class Ase extends Core {
|
||||
run(state) {
|
||||
this.udpSend('s',(buffer) => {
|
||||
async run(state) {
|
||||
const buffer = await this.udpSend('s',(buffer) => {
|
||||
const reader = this.reader(buffer);
|
||||
|
||||
const header = reader.string({length:4});
|
||||
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 header = reader.string({length: 4});
|
||||
if (header === 'EYE1') return reader.rest();
|
||||
});
|
||||
|
||||
const reader = this.reader(buffer);
|
||||
state.raw.gamename = this.readString(reader);
|
||||
state.gamePort = parseInt(this.readString(reader));
|
||||
state.name = this.readString(reader);
|
||||
state.raw.gametype = this.readString(reader);
|
||||
state.map = this.readString(reader);
|
||||
state.raw.version = this.readString(reader);
|
||||
state.password = this.readString(reader) === '1';
|
||||
state.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) {
|
||||
|
|
|
@ -1,127 +1,154 @@
|
|||
const async = require('async'),
|
||||
Core = require('./core');
|
||||
const Core = require('./core');
|
||||
|
||||
class Battlefield extends Core {
|
||||
constructor() {
|
||||
super();
|
||||
this.encoding = 'latin1';
|
||||
this.isBadCompany2 = false;
|
||||
}
|
||||
|
||||
run(state) {
|
||||
async.series([
|
||||
(c) => {
|
||||
this.query(['serverInfo'], (data) => {
|
||||
if(this.debug) console.log(data);
|
||||
if(data.shift() !== 'OK') return this.fatal('Missing OK');
|
||||
async run(state) {
|
||||
await this.withTcp(async socket => {
|
||||
{
|
||||
const data = await this.query(socket, ['serverInfo']);
|
||||
state.name = data.shift();
|
||||
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();
|
||||
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());
|
||||
const teamCount = data.shift();
|
||||
state.raw.teams = [];
|
||||
for (let i = 0; i < teamCount; i++) {
|
||||
const tickets = parseFloat(data.shift());
|
||||
state.raw.teams.push({
|
||||
tickets: tickets
|
||||
});
|
||||
}
|
||||
|
||||
const teamCount = data.shift();
|
||||
state.raw.teams = [];
|
||||
for(let i = 0; i < teamCount; i++) {
|
||||
const tickets = parseFloat(data.shift());
|
||||
state.raw.teams.push({
|
||||
tickets:tickets
|
||||
});
|
||||
}
|
||||
state.raw.targetscore = parseInt(data.shift());
|
||||
state.raw.status = data.shift();
|
||||
|
||||
state.raw.targetscore = parseInt(data.shift());
|
||||
data.shift();
|
||||
state.raw.ranked = (data.shift() === 'true');
|
||||
state.raw.punkbuster = (data.shift() === 'true');
|
||||
state.password = (data.shift() === 'true');
|
||||
state.raw.uptime = parseInt(data.shift());
|
||||
state.raw.roundtime = parseInt(data.shift());
|
||||
if(this.isBadCompany2) {
|
||||
data.shift();
|
||||
data.shift();
|
||||
}
|
||||
// Seems like the fields end at random places beyond this point
|
||||
// depending on the server version
|
||||
|
||||
if (data.length) state.raw.ranked = (data.shift() === 'true');
|
||||
if (data.length) state.raw.punkbuster = (data.shift() === 'true');
|
||||
if (data.length) state.password = (data.shift() === 'true');
|
||||
if (data.length) state.raw.uptime = parseInt(data.shift());
|
||||
if (data.length) state.raw.roundtime = parseInt(data.shift());
|
||||
|
||||
const isBadCompany2 = data[0] === 'BC2';
|
||||
if (isBadCompany2) {
|
||||
if (data.length) data.shift();
|
||||
if (data.length) data.shift();
|
||||
}
|
||||
if (data.length) {
|
||||
state.raw.ip = data.shift();
|
||||
state.raw.punkbusterversion = data.shift();
|
||||
state.raw.joinqueue = (data.shift() === 'true');
|
||||
state.raw.region = data.shift();
|
||||
if(!this.isBadCompany2) {
|
||||
state.raw.pingsite = data.shift();
|
||||
state.raw.country = data.shift();
|
||||
state.raw.quickmatch = (data.shift() === 'true');
|
||||
}
|
||||
|
||||
c();
|
||||
});
|
||||
},
|
||||
(c) => {
|
||||
this.query(['version'], (data) => {
|
||||
if(this.debug) console.log(data);
|
||||
if(data[0] !== 'OK') return this.fatal('Missing OK');
|
||||
|
||||
state.raw.version = data[2];
|
||||
|
||||
c();
|
||||
});
|
||||
},
|
||||
(c) => {
|
||||
this.query(['listPlayers','all'], (data) => {
|
||||
if(this.debug) console.log(data);
|
||||
if(data.shift() !== 'OK') return this.fatal('Missing OK');
|
||||
|
||||
const fieldCount = parseInt(data.shift());
|
||||
const fields = [];
|
||||
for(let i = 0; i < fieldCount; i++) {
|
||||
fields.push(data.shift());
|
||||
}
|
||||
const numplayers = data.shift();
|
||||
for(let i = 0; i < numplayers; i++) {
|
||||
const player = {};
|
||||
for (let key of fields) {
|
||||
let value = data.shift();
|
||||
|
||||
if(key === 'teamId') key = 'team';
|
||||
else if(key === 'squadId') key = 'squad';
|
||||
|
||||
if(
|
||||
key === 'kills'
|
||||
|| key === 'deaths'
|
||||
|| key === 'score'
|
||||
|| key === 'rank'
|
||||
|| key === 'team'
|
||||
|| key === 'squad'
|
||||
|| key === 'ping'
|
||||
|| key === 'type'
|
||||
) {
|
||||
value = parseInt(value);
|
||||
}
|
||||
|
||||
player[key] = value;
|
||||
}
|
||||
state.players.push(player);
|
||||
}
|
||||
|
||||
this.finish(state);
|
||||
});
|
||||
const split = state.raw.ip.split(':');
|
||||
state.gameHost = split[0];
|
||||
state.gamePort = split[1];
|
||||
} else {
|
||||
// best guess if the server doesn't tell us what the server port is
|
||||
// these are just the default game ports for different default query ports
|
||||
if (this.options.port === 48888) state.gamePort = 7673;
|
||||
if (this.options.port === 22000) state.gamePort = 25200;
|
||||
}
|
||||
if (data.length) state.raw.punkbusterversion = data.shift();
|
||||
if (data.length) state.raw.joinqueue = (data.shift() === 'true');
|
||||
if (data.length) state.raw.region = data.shift();
|
||||
if (data.length) state.raw.pingsite = data.shift();
|
||||
if (data.length) state.raw.country = data.shift();
|
||||
if (data.length) state.raw.quickmatch = (data.shift() === 'true');
|
||||
}
|
||||
|
||||
{
|
||||
const data = await this.query(socket, ['version']);
|
||||
data.shift();
|
||||
state.raw.version = data.shift();
|
||||
}
|
||||
|
||||
{
|
||||
const data = await this.query(socket, ['listPlayers', 'all']);
|
||||
const fieldCount = parseInt(data.shift());
|
||||
const fields = [];
|
||||
for (let i = 0; i < fieldCount; i++) {
|
||||
fields.push(data.shift());
|
||||
}
|
||||
const numplayers = data.shift();
|
||||
for (let i = 0; i < numplayers; i++) {
|
||||
const player = {};
|
||||
for (let key of fields) {
|
||||
let value = data.shift();
|
||||
|
||||
if (key === 'teamId') key = 'team';
|
||||
else if (key === 'squadId') key = 'squad';
|
||||
|
||||
if (
|
||||
key === 'kills'
|
||||
|| key === 'deaths'
|
||||
|| key === 'score'
|
||||
|| key === 'rank'
|
||||
|| key === 'team'
|
||||
|| key === 'squad'
|
||||
|| key === 'ping'
|
||||
|| key === 'type'
|
||||
) {
|
||||
value = parseInt(value);
|
||||
}
|
||||
|
||||
player[key] = value;
|
||||
}
|
||||
state.players.push(player);
|
||||
}
|
||||
}
|
||||
]);
|
||||
}
|
||||
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) {
|
||||
if(buffer.length < 8) return false;
|
||||
const reader = this.reader(buffer);
|
||||
const header = reader.uint(4);
|
||||
const totalLength = reader.uint(4);
|
||||
if(buffer.length < totalLength) return false;
|
||||
this.debugLog("Expected " + totalLength + " bytes, have " + buffer.length);
|
||||
|
||||
const paramCount = reader.uint(4);
|
||||
const params = [];
|
||||
|
@ -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;
|
|
@ -1,59 +1,56 @@
|
|||
const request = require('request'),
|
||||
Core = require('./core');
|
||||
const Core = require('./core'),
|
||||
cheerio = require('cheerio');
|
||||
|
||||
class BuildAndShoot extends Core {
|
||||
run(state) {
|
||||
request({
|
||||
uri: 'http://'+this.options.address+':'+this.options.port_query+'/',
|
||||
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);
|
||||
async run(state) {
|
||||
const body = await this.request({
|
||||
uri: 'http://'+this.options.address+':'+this.options.port+'/',
|
||||
});
|
||||
|
||||
let m;
|
||||
|
||||
m = body.match(/status server for (.*?)\.?(\r|\n)/);
|
||||
if(m) state.name = m[1];
|
||||
|
||||
m = body.match(/Current uptime: (\d+)/);
|
||||
if(m) state.raw.uptime = m[1];
|
||||
|
||||
m = body.match(/currently running (.*?) by /);
|
||||
if(m) state.map = m[1];
|
||||
|
||||
m = body.match(/Current players: (\d+)\/(\d+)/);
|
||||
if(m) {
|
||||
state.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;
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,39 +1,81 @@
|
|||
const EventEmitter = require('events').EventEmitter,
|
||||
dns = require('dns'),
|
||||
net = require('net'),
|
||||
async = require('async'),
|
||||
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 {
|
||||
constructor() {
|
||||
super();
|
||||
this.options = {
|
||||
socketTimeout: 2000,
|
||||
attemptTimeout: 10000,
|
||||
maxAttempts: 1
|
||||
};
|
||||
this.attempt = 1;
|
||||
this.finished = false;
|
||||
this.encoding = 'utf8';
|
||||
this.byteorder = 'le';
|
||||
this.delimiter = '\0';
|
||||
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) {
|
||||
if(!noretry && this.attempt < this.options.maxAttempts) {
|
||||
this.attempt++;
|
||||
this.start();
|
||||
return;
|
||||
async runAllAttempts() {
|
||||
let result = null;
|
||||
let lastError = null;
|
||||
for (let attempt = 1; attempt <= this.options.maxAttempts; attempt++) {
|
||||
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() {
|
||||
return {
|
||||
// Runs a single attempt with a timeout and cleans up afterward
|
||||
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: '',
|
||||
map: '',
|
||||
password: false,
|
||||
|
@ -44,130 +86,81 @@ class Core extends EventEmitter {
|
|||
players: [],
|
||||
bots: []
|
||||
};
|
||||
}
|
||||
|
||||
finalizeState(state) {}
|
||||
await this.run(state);
|
||||
|
||||
finish(state) {
|
||||
this.finalizeState(state);
|
||||
this.done(state);
|
||||
}
|
||||
// because lots of servers prefix with spaces to try to appear first
|
||||
state.name = (state.name || '').trim();
|
||||
|
||||
done(state) {
|
||||
if(this.finished) return;
|
||||
|
||||
if(this.options.notes)
|
||||
state.notes = this.options.notes;
|
||||
|
||||
state.query = {};
|
||||
if('host' in this.options) state.query.host = this.options.host;
|
||||
if('address' in this.options) state.query.address = this.options.address;
|
||||
if('port' in this.options) state.query.port = this.options.port;
|
||||
if('port_query' in this.options) state.query.port_query = this.options.port_query;
|
||||
state.query.type = this.type;
|
||||
if('pretty' in this) state.query.pretty = this.pretty;
|
||||
state.query.duration = Date.now() - this.startMillis;
|
||||
state.query.attempts = this.attempt;
|
||||
|
||||
this.reset();
|
||||
this.finished = true;
|
||||
this.emit('finished',state);
|
||||
if(this.options.callback) this.options.callback(state);
|
||||
}
|
||||
|
||||
reset() {
|
||||
clearTimeout(this.attemptTimeoutTimer);
|
||||
if(this.timers) {
|
||||
for (const timer of this.timers) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
if (!('connect' in state)) {
|
||||
state.connect = ''
|
||||
+ (state.gameHost || this.options.host || this.options.address)
|
||||
+ ':'
|
||||
+ (state.gamePort || this.options.port)
|
||||
}
|
||||
this.timers = [];
|
||||
state.ping = this.shortestRTT;
|
||||
delete state.gameHost;
|
||||
delete state.gamePort;
|
||||
|
||||
if(this.tcpSocket) {
|
||||
this.tcpSocket.destroy();
|
||||
delete this.tcpSocket;
|
||||
}
|
||||
|
||||
this.udpTimeoutTimer = false;
|
||||
this.udpCallback = false;
|
||||
return state;
|
||||
}
|
||||
|
||||
start() {
|
||||
const options = this.options;
|
||||
this.reset();
|
||||
async run(state) {}
|
||||
|
||||
this.startMillis = Date.now();
|
||||
|
||||
this.attemptTimeoutTimer = setTimeout(() => {
|
||||
this.fatal('timeout');
|
||||
},this.options.attemptTimeout);
|
||||
|
||||
async.series([
|
||||
(c) => {
|
||||
// resolve host names
|
||||
if(!('host' in options)) return c();
|
||||
if(options.host.match(/\d+\.\d+\.\d+\.\d+/)) {
|
||||
options.address = options.host;
|
||||
c();
|
||||
} else {
|
||||
this.parseDns(options.host,c);
|
||||
/**
|
||||
* @param {string} host
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async parseDns(host) {
|
||||
const isIp = (host) => {
|
||||
return !!host.match(/\d+\.\d+\.\d+\.\d+/);
|
||||
};
|
||||
const resolveStandard = async (host) => {
|
||||
if(isIp(host)) return host;
|
||||
this.debugLog("Standard DNS Lookup: " + host);
|
||||
const {address,family} = await dnsLookupAsync(host);
|
||||
this.debugLog(address);
|
||||
return address;
|
||||
};
|
||||
const resolveSrv = async (srv,host) => {
|
||||
if(isIp(host)) return host;
|
||||
this.debugLog("SRV DNS Lookup: " + srv+'.'+host);
|
||||
let records;
|
||||
try {
|
||||
records = await dnsResolveAsync(srv + '.' + host, 'SRV');
|
||||
this.debugLog(records);
|
||||
if(records.length >= 1) {
|
||||
const record = records[0];
|
||||
this.options.port = record.port;
|
||||
const srvhost = record.name;
|
||||
return await resolveStandard(srvhost);
|
||||
}
|
||||
},
|
||||
(c) => {
|
||||
// calculate query port if needed
|
||||
if(!('port_query' in options) && 'port' in options) {
|
||||
const offset = options.port_query_offset || 0;
|
||||
options.port_query = options.port + offset;
|
||||
}
|
||||
c();
|
||||
},
|
||||
(c) => {
|
||||
// run
|
||||
this.run(this.initState());
|
||||
} catch(e) {
|
||||
this.debugLog(e.toString());
|
||||
}
|
||||
|
||||
]);
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
return await resolveStandard(host);
|
||||
};
|
||||
|
||||
const resolveSrv = (srv,host,c) => {
|
||||
if(this.debug) console.log("SRV DNS Lookup: " + srv+'.'+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(this.srvRecord) return await resolveSrv(this.srvRecord, host);
|
||||
else return await resolveStandard(host);
|
||||
}
|
||||
|
||||
if(srvhost.match(/\d+\.\d+\.\d+\.\d+/)) {
|
||||
this.options.address = srvhost;
|
||||
c();
|
||||
} else {
|
||||
// resolve yet again
|
||||
resolveStandard(srvhost,c);
|
||||
}
|
||||
return;
|
||||
}
|
||||
return resolveStandard(host,c);
|
||||
});
|
||||
};
|
||||
|
||||
if(this.srvRecord) resolveSrv(this.srvRecord,host,c);
|
||||
else resolveStandard(host,c);
|
||||
/** Param can be a time in ms, or a promise (which will be timed) */
|
||||
registerRtt(param) {
|
||||
if (param.then) {
|
||||
const start = Date.now();
|
||||
param.then(() => {
|
||||
const end = Date.now();
|
||||
const rtt = end - start;
|
||||
this.registerRtt(rtt);
|
||||
}).catch(() => {});
|
||||
} else {
|
||||
this.debugLog("Registered RTT: " + param + "ms");
|
||||
if (this.shortestRTT === 0 || param < this.shortestRTT) {
|
||||
this.shortestRTT = param;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if(typeof str === 'boolean') return str;
|
||||
if(typeof str === 'number') return str !== 0;
|
||||
if(typeof str === 'string') {
|
||||
if(str.toLowerCase() === 'true') return true;
|
||||
if(str === 'yes') return true;
|
||||
if(str.toLowerCase() === 'yes') return true;
|
||||
if(str === '1') return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
_tcpConnect(c) {
|
||||
if(this.tcpSocket) return c(this.tcpSocket);
|
||||
|
||||
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([]);
|
||||
}
|
||||
});
|
||||
assertValidPort(port) {
|
||||
if (!port || port < 1 || port > 65535) {
|
||||
throw new Error("Invalid tcp/ip port: " + port);
|
||||
}
|
||||
}
|
||||
tcpSend(buffer,ondata) {
|
||||
process.nextTick(() => {
|
||||
if(this.tcpCallback) return this.fatal('Attempted to send TCP packet while still waiting on a managed response');
|
||||
this._tcpConnect((socket) => {
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {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);
|
||||
});
|
||||
if(!ondata) return;
|
||||
|
||||
this.tcpTimeoutTimer = this.setTimeout(() => {
|
||||
this.tcpCallback = false;
|
||||
this.fatal('TCP Watchdog Timeout');
|
||||
},this.options.socketTimeout);
|
||||
this.tcpCallback = ondata;
|
||||
});
|
||||
timeout = Promises.createTimeout(this.options.socketTimeout, 'TCP');
|
||||
return await Promise.race([promise, timeout, this.abortedPromise]);
|
||||
} finally {
|
||||
timeout && timeout.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
udpSend(buffer,onpacket,ontimeout) {
|
||||
process.nextTick(() => {
|
||||
if(this.udpCallback) return this.fatal('Attempted to send UDP packet while still waiting on a managed response');
|
||||
this._udpSendNow(buffer);
|
||||
if(!onpacket) return;
|
||||
|
||||
this.udpTimeoutTimer = this.setTimeout(() => {
|
||||
this.udpCallback = false;
|
||||
let timeout = false;
|
||||
if(!ontimeout || ontimeout() !== true) timeout = true;
|
||||
if(timeout) this.fatal('UDP Watchdog Timeout');
|
||||
},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');
|
||||
/**
|
||||
* @param {Buffer|string} buffer
|
||||
* @param {function(Buffer):T} onPacket
|
||||
* @param {(function():T)=} onTimeout
|
||||
* @returns Promise<T>
|
||||
* @template T
|
||||
*/
|
||||
async udpSend(buffer,onPacket,onTimeout) {
|
||||
const address = this.options.address;
|
||||
const port = this.options.port;
|
||||
this.assertValidPort(port);
|
||||
|
||||
if(typeof buffer === 'string') buffer = Buffer.from(buffer,'binary');
|
||||
this.debugLog(log => {
|
||||
log(address+':'+port+" UDP-->");
|
||||
log(HexUtil.debugDump(buffer));
|
||||
});
|
||||
|
||||
if(this.debug) {
|
||||
console.log(this.options.address+':'+this.options.port_query+" UDP-->");
|
||||
console.log(HexUtil.debugDump(buffer));
|
||||
const socket = this.udpSocket;
|
||||
socket.send(buffer, address, port);
|
||||
|
||||
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) {
|
||||
const result = this.udpCallback(buffer);
|
||||
if(result === true) {
|
||||
// we're done with this udp session
|
||||
clearTimeout(this.udpTimeoutTimer);
|
||||
this.udpCallback = false;
|
||||
|
||||
async request(params) {
|
||||
// If we haven't opened a raw tcp socket yet during this query, just open one and then immediately close it.
|
||||
// This will give us a much more accurate RTT than using the rtt of the http request.
|
||||
if (!this.usedTcp) {
|
||||
await this.withTcp(() => {});
|
||||
}
|
||||
|
||||
let requestPromise;
|
||||
try {
|
||||
requestPromise = requestAsync({
|
||||
...params,
|
||||
timeout: this.options.socketTimeout,
|
||||
resolveWithFullResponse: true
|
||||
});
|
||||
this.debugLog(log => {
|
||||
log(() => params.uri + " HTTP-->");
|
||||
requestPromise
|
||||
.then((response) => log(params.uri + " <--HTTP " + response.statusCode))
|
||||
.catch(() => {});
|
||||
});
|
||||
const wrappedPromise = requestPromise.then(response => {
|
||||
if (response.statusCode !== 200) throw new Error("Bad status code: " + response.statusCode);
|
||||
return response.body;
|
||||
});
|
||||
return await Promise.race([wrappedPromise, this.abortedPromise]);
|
||||
} finally {
|
||||
requestPromise && requestPromise.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
debugLog(...args) {
|
||||
if (!this.options.debug) return;
|
||||
try {
|
||||
if(args[0] instanceof Buffer) {
|
||||
this.debugLog(HexUtil.debugDump(args[0]));
|
||||
} else if (typeof args[0] == 'function') {
|
||||
const result = args[0].call(undefined, this.debugLog.bind(this));
|
||||
if (result !== undefined) {
|
||||
this.debugLog(result);
|
||||
}
|
||||
} else {
|
||||
console.log(...args);
|
||||
}
|
||||
} else {
|
||||
this.udpResponse(buffer);
|
||||
} catch(e) {
|
||||
console.log("Error while debug logging: " + e);
|
||||
}
|
||||
}
|
||||
udpResponse() {}
|
||||
}
|
||||
|
||||
module.exports = Core;
|
||||
|
|
|
@ -3,89 +3,147 @@ const Core = require('./core');
|
|||
class Doom3 extends Core {
|
||||
constructor() {
|
||||
super();
|
||||
this.pretty = 'Doom 3';
|
||||
this.encoding = 'latin1';
|
||||
this.isEtqw = false;
|
||||
this.hasSpaceBeforeClanTag = false;
|
||||
this.hasClanTag = false;
|
||||
this.hasTypeFlag = false;
|
||||
}
|
||||
run(state) {
|
||||
this.udpSend('\xff\xffgetInfo\x00PiNGPoNG\x00', (buffer) => {
|
||||
const reader = this.reader(buffer);
|
||||
|
||||
async run(state) {
|
||||
const body = await this.udpSend('\xff\xffgetInfo\x00PiNGPoNg\x00', packet => {
|
||||
const reader = this.reader(packet);
|
||||
const header = reader.uint(2);
|
||||
if(header !== 0xffff) return;
|
||||
const header2 = reader.string();
|
||||
if(header2 !== 'infoResponse') return;
|
||||
|
||||
if(this.isEtqw) {
|
||||
const taskId = reader.uint(4);
|
||||
}
|
||||
|
||||
const challenge = reader.uint(4);
|
||||
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;
|
||||
const challengePart1 = reader.string({length:4});
|
||||
if (challengePart1 !== "PiNG") return;
|
||||
// some doom3 implementations only return the first 4 bytes of the challenge
|
||||
const challengePart2 = reader.string({length:4});
|
||||
if (challengePart2 !== 'PoNg') reader.skip(-4);
|
||||
return reader.rest();
|
||||
});
|
||||
|
||||
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) {
|
||||
|
|
|
@ -6,29 +6,34 @@ class Ffow extends Valve {
|
|||
this.byteorder = 'be';
|
||||
this.legacyChallenge = true;
|
||||
}
|
||||
queryInfo(state,c) {
|
||||
this.sendPacket(0x46,false,'LSQ',0x49, (b) => {
|
||||
const reader = this.reader(b);
|
||||
state.raw.protocol = reader.uint(1);
|
||||
state.name = reader.string();
|
||||
state.map = reader.string();
|
||||
state.raw.mod = reader.string();
|
||||
state.raw.gamemode = reader.string();
|
||||
state.raw.description = reader.string();
|
||||
state.raw.version = reader.string();
|
||||
state.raw.port = reader.uint(2);
|
||||
state.raw.numplayers = reader.uint(1);
|
||||
state.maxplayers = reader.uint(1);
|
||||
state.raw.listentype = String.fromCharCode(reader.uint(1));
|
||||
state.raw.environment = String.fromCharCode(reader.uint(1));
|
||||
state.password = !!reader.uint(1);
|
||||
state.raw.secure = reader.uint(1);
|
||||
state.raw.averagefps = reader.uint(1);
|
||||
state.raw.round = reader.uint(1);
|
||||
state.raw.maxrounds = reader.uint(1);
|
||||
state.raw.timeleft = reader.uint(2);
|
||||
c();
|
||||
});
|
||||
async queryInfo(state) {
|
||||
this.debugLog("Requesting ffow info ...");
|
||||
const b = await this.sendPacket(
|
||||
0x46,
|
||||
false,
|
||||
'LSQ',
|
||||
0x49
|
||||
);
|
||||
|
||||
const reader = this.reader(b);
|
||||
state.raw.protocol = reader.uint(1);
|
||||
state.name = reader.string();
|
||||
state.map = reader.string();
|
||||
state.raw.mod = reader.string();
|
||||
state.raw.gamemode = reader.string();
|
||||
state.raw.description = reader.string();
|
||||
state.raw.version = reader.string();
|
||||
state.gamePort = reader.uint(2);
|
||||
state.raw.numplayers = reader.uint(1);
|
||||
state.maxplayers = reader.uint(1);
|
||||
state.raw.listentype = String.fromCharCode(reader.uint(1));
|
||||
state.raw.environment = String.fromCharCode(reader.uint(1));
|
||||
state.password = !!reader.uint(1);
|
||||
state.raw.secure = reader.uint(1);
|
||||
state.raw.averagefps = reader.uint(1);
|
||||
state.raw.round = reader.uint(1);
|
||||
state.raw.maxrounds = reader.uint(1);
|
||||
state.raw.timeleft = reader.uint(2);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
const request = require('request'),
|
||||
Quake2 = require('./quake2');
|
||||
const Quake2 = require('./quake2');
|
||||
|
||||
class FiveM extends Quake2 {
|
||||
constructor() {
|
||||
|
@ -9,43 +8,28 @@ class FiveM extends Quake2 {
|
|||
this.encoding = 'utf8';
|
||||
}
|
||||
|
||||
finish(state) {
|
||||
request({
|
||||
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');
|
||||
}
|
||||
async run(state) {
|
||||
await super.run(state);
|
||||
|
||||
state.raw.info = json;
|
||||
|
||||
request({
|
||||
uri: 'http://'+this.options.address+':'+this.options.port_query+'/players.json',
|
||||
timeout: this.options.socketTimeout
|
||||
}, (e,r,body) => {
|
||||
if(e) return this.fatal('HTTP error');
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(body);
|
||||
} catch(e) {
|
||||
return this.fatal('Invalid JSON');
|
||||
}
|
||||
|
||||
state.raw.players = json;
|
||||
|
||||
state.players = [];
|
||||
for (const player of json) {
|
||||
state.players.push({name:player.name, ping:player.ping});
|
||||
}
|
||||
|
||||
super.finish(state);
|
||||
{
|
||||
const raw = await this.request({
|
||||
uri: 'http://' + this.options.address + ':' + this.options.port + '/info.json'
|
||||
});
|
||||
});
|
||||
const json = JSON.parse(raw);
|
||||
state.raw.info = json;
|
||||
}
|
||||
|
||||
{
|
||||
const raw = await this.request({
|
||||
uri: 'http://' + this.options.address + ':' + this.options.port + '/players.json'
|
||||
});
|
||||
const json = JSON.parse(raw);
|
||||
state.raw.players = json;
|
||||
state.players = [];
|
||||
for (const player of json) {
|
||||
state.players.push({name: player.name, ping: player.ping});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,68 +1,58 @@
|
|||
const async = require('async'),
|
||||
Core = require('./core');
|
||||
const Core = require('./core');
|
||||
|
||||
class Gamespy1 extends Core {
|
||||
constructor() {
|
||||
super();
|
||||
this.sessionId = 1;
|
||||
this.encoding = 'latin1';
|
||||
this.byteorder = 'be';
|
||||
}
|
||||
|
||||
run(state) {
|
||||
async.series([
|
||||
(c) => {
|
||||
this.sendPacket('info', (data) => {
|
||||
state.raw = data;
|
||||
if('hostname' in state.raw) state.name = state.raw.hostname;
|
||||
if('mapname' in state.raw) state.map = state.raw.mapname;
|
||||
if(this.trueTest(state.raw.password)) state.password = true;
|
||||
if('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers);
|
||||
c();
|
||||
});
|
||||
},
|
||||
(c) => {
|
||||
this.sendPacket('rules', (data) => {
|
||||
state.raw.rules = data;
|
||||
c();
|
||||
});
|
||||
},
|
||||
(c) => {
|
||||
this.sendPacket('players', (data) => {
|
||||
const players = {};
|
||||
const teams = {};
|
||||
for(const ident of Object.keys(data)) {
|
||||
const split = ident.split('_');
|
||||
let key = split[0];
|
||||
const id = split[1];
|
||||
let value = data[ident];
|
||||
async run(state) {
|
||||
{
|
||||
const data = await this.sendPacket('info');
|
||||
state.raw = data;
|
||||
if ('hostname' in state.raw) state.name = state.raw.hostname;
|
||||
if ('mapname' in state.raw) state.map = state.raw.mapname;
|
||||
if (this.trueTest(state.raw.password)) state.password = true;
|
||||
if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers);
|
||||
if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport);
|
||||
}
|
||||
{
|
||||
const data = await this.sendPacket('rules');
|
||||
state.raw.rules = data;
|
||||
}
|
||||
{
|
||||
const data = await this.sendPacket('players');
|
||||
const players = {};
|
||||
const teams = {};
|
||||
for (const ident of Object.keys(data)) {
|
||||
const split = ident.split('_');
|
||||
let key = split[0];
|
||||
const id = split[1];
|
||||
let value = data[ident];
|
||||
|
||||
if(key === 'teamname') {
|
||||
teams[id] = value;
|
||||
} else {
|
||||
if(!(id in players)) players[id] = {};
|
||||
if(key === 'playername') key = 'name';
|
||||
else if(key === 'team') value = parseInt(value);
|
||||
else if(key === 'score' || key === 'ping' || key === 'deaths') value = parseInt(value);
|
||||
players[id][key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
state.raw.teams = teams;
|
||||
for(const id of Object.keys(players)) {
|
||||
state.players.push(players[id]);
|
||||
}
|
||||
this.finish(state);
|
||||
});
|
||||
if (key === 'teamname') {
|
||||
teams[id] = value;
|
||||
} else {
|
||||
if (!(id in players)) players[id] = {};
|
||||
if (key === 'playername') key = 'name';
|
||||
else if (key === 'team') value = parseInt(value);
|
||||
else if (key === 'score' || key === 'ping' || key === 'deaths') value = parseInt(value);
|
||||
players[id][key] = value;
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
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 output = {};
|
||||
this.udpSend('\\'+type+'\\', (buffer) => {
|
||||
return await this.udpSend('\\'+type+'\\', buffer => {
|
||||
const reader = this.reader(buffer);
|
||||
const str = reader.string({length:buffer.length});
|
||||
const split = str.split('\\');
|
||||
|
@ -79,8 +69,7 @@ class Gamespy1 extends Core {
|
|||
if('final' in output) {
|
||||
delete output.final;
|
||||
delete output.queryid;
|
||||
callback(output);
|
||||
return true;
|
||||
return output;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3,65 +3,105 @@ const Core = require('./core');
|
|||
class Gamespy2 extends Core {
|
||||
constructor() {
|
||||
super();
|
||||
this.sessionId = 1;
|
||||
this.encoding = 'latin1';
|
||||
this.byteorder = 'be';
|
||||
}
|
||||
|
||||
run(state) {
|
||||
const request = Buffer.from([0xfe,0xfd,0x00,0x00,0x00,0x00,0x01,0xff,0xff,0xff]);
|
||||
const packets = [];
|
||||
this.udpSend(request,
|
||||
(buffer) => {
|
||||
if(packets.length && buffer.readUInt8(0) === 0)
|
||||
buffer = buffer.slice(1);
|
||||
packets.push(buffer);
|
||||
},
|
||||
() => {
|
||||
const buffer = Buffer.concat(packets);
|
||||
const reader = this.reader(buffer);
|
||||
const header = reader.uint(1);
|
||||
if(header !== 0) return;
|
||||
const pingId = reader.uint(4);
|
||||
if(pingId !== 1) return;
|
||||
|
||||
while(!reader.done()) {
|
||||
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;
|
||||
async run(state) {
|
||||
// Parse info
|
||||
{
|
||||
const body = await this.sendPacket([0xff, 0, 0]);
|
||||
const reader = this.reader(body);
|
||||
while (!reader.done()) {
|
||||
const key = reader.string();
|
||||
const value = reader.string();
|
||||
if (!key) break;
|
||||
state.raw[key] = value;
|
||||
}
|
||||
);
|
||||
if ('hostname' in state.raw) state.name = state.raw.hostname;
|
||||
if ('mapname' in state.raw) state.map = state.raw.mapname;
|
||||
if (this.trueTest(state.raw.password)) state.password = true;
|
||||
if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers);
|
||||
if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport);
|
||||
}
|
||||
|
||||
// Parse players
|
||||
{
|
||||
const body = await this.sendPacket([0, 0xff, 0]);
|
||||
const reader = this.reader(body);
|
||||
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) {
|
||||
const count = reader.uint(1);
|
||||
// count is unreliable (often it's wrong), so we don't use it.
|
||||
// read until we hit an empty first field string
|
||||
const zero = reader.uint(1); // always 0
|
||||
const count = reader.uint(1); // number of rows in this data
|
||||
|
||||
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 = [];
|
||||
while(!reader.done()) {
|
||||
let field = reader.string();
|
||||
if(!field) break;
|
||||
if(field.charCodeAt(0) <= 2) field = field.substring(1);
|
||||
fields.push(field);
|
||||
if(this.debug) console.log("field:"+field);
|
||||
this.debugLog("field:"+field);
|
||||
}
|
||||
|
||||
if (!fields.length) return [];
|
||||
|
||||
const units = [];
|
||||
outer: while(!reader.done()) {
|
||||
const unit = {};
|
||||
|
@ -69,7 +109,7 @@ class Gamespy2 extends Core {
|
|||
let key = fields[iField];
|
||||
let value = reader.string();
|
||||
if(!value && iField === 0) break outer;
|
||||
if(this.debug) console.log("value:"+value);
|
||||
this.debugLog("value:"+value);
|
||||
if(key === 'player_') key = 'name';
|
||||
else if(key === 'score_') key = 'score';
|
||||
else if(key === 'deaths_') key = 'deaths';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const async = require('async'),
|
||||
Core = require('./core');
|
||||
const Core = require('./core'),
|
||||
HexUtil = require('../lib/HexUtil');
|
||||
|
||||
class Gamespy3 extends Core {
|
||||
constructor() {
|
||||
|
@ -7,148 +7,133 @@ class Gamespy3 extends Core {
|
|||
this.sessionId = 1;
|
||||
this.encoding = 'latin1';
|
||||
this.byteorder = 'be';
|
||||
this.noChallenge = false;
|
||||
this.useOnlySingleSplit = false;
|
||||
this.isJc2mp = false;
|
||||
}
|
||||
|
||||
run(state) {
|
||||
let challenge;
|
||||
async run(state) {
|
||||
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[] */
|
||||
let packets;
|
||||
const packets = await this.sendPacket(0,challenge,requestPayload,true);
|
||||
|
||||
async.series([
|
||||
(c) => {
|
||||
if(this.noChallenge) return c();
|
||||
this.sendPacket(9,false,false,false,(buffer) => {
|
||||
const reader = this.reader(buffer);
|
||||
challenge = parseInt(reader.string());
|
||||
c();
|
||||
});
|
||||
},
|
||||
(c) => {
|
||||
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]);
|
||||
// iterate over the received packets
|
||||
// the first packet will start off with k/v pairs, followed with data fields
|
||||
// the following packets will only have data fields
|
||||
state.raw.playerTeamInfo = {};
|
||||
|
||||
for(let iPacket = 0; iPacket < packets.length; iPacket++) {
|
||||
const packet = packets[iPacket];
|
||||
const reader = this.reader(packet);
|
||||
|
||||
this.debugLog("Parsing packet #" + iPacket);
|
||||
this.debugLog(packet);
|
||||
|
||||
// Parse raw server key/values
|
||||
|
||||
if(iPacket === 0) {
|
||||
while(!reader.done()) {
|
||||
const key = reader.string();
|
||||
if(!key) break;
|
||||
|
||||
let value = reader.string();
|
||||
while(value.match(/^p[0-9]+$/)) {
|
||||
// fix a weird ut3 bug where some keys don't have values
|
||||
value = reader.string();
|
||||
}
|
||||
|
||||
state.raw[key] = value;
|
||||
this.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) {
|
||||
const challengeLength = (this.noChallenge || challenge === false) ? 0 : 4;
|
||||
async sendPacket(type,challenge,payload,assemble) {
|
||||
const challengeLength = challenge === null ? 0 : 4;
|
||||
const payloadLength = payload ? payload.length : 0;
|
||||
|
||||
const b = Buffer.alloc(7 + challengeLength + payloadLength);
|
||||
|
@ -161,7 +146,7 @@ class Gamespy3 extends Core {
|
|||
|
||||
let numPackets = 0;
|
||||
const packets = {};
|
||||
this.udpSend(b,(buffer) => {
|
||||
return this.udpSend(b,(buffer) => {
|
||||
const reader = this.reader(buffer);
|
||||
const iType = reader.uint(1);
|
||||
if(iType !== type) return;
|
||||
|
@ -169,14 +154,12 @@ class Gamespy3 extends Core {
|
|||
if(iSessionId !== this.sessionId) return;
|
||||
|
||||
if(!assemble) {
|
||||
c(reader.rest());
|
||||
return true;
|
||||
return reader.rest();
|
||||
}
|
||||
if(this.useOnlySingleSplit) {
|
||||
// has split headers, but they are worthless and only one packet is used
|
||||
reader.skip(11);
|
||||
c([reader.rest()]);
|
||||
return true;
|
||||
return [reader.rest()];
|
||||
}
|
||||
|
||||
reader.skip(9); // filler data -- usually set to 'splitnum\0'
|
||||
|
@ -189,8 +172,7 @@ class Gamespy3 extends Core {
|
|||
|
||||
packets[id] = reader.rest();
|
||||
if(this.debug) {
|
||||
console.log("Received packet #"+id);
|
||||
if(last) console.log("(last)");
|
||||
this.debugLog("Received packet #"+id + (last ? " (last)" : ""));
|
||||
}
|
||||
|
||||
if(!numPackets || Object.keys(packets).length !== numPackets) return;
|
||||
|
@ -199,13 +181,11 @@ class Gamespy3 extends Core {
|
|||
const list = [];
|
||||
for(let i = 0; i < numPackets; i++) {
|
||||
if(!(i in packets)) {
|
||||
this.fatal('Missing packet #'+i);
|
||||
return true;
|
||||
throw new Error('Missing packet #'+i);
|
||||
}
|
||||
list.push(packets[i]);
|
||||
}
|
||||
c(list);
|
||||
return true;
|
||||
return list;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,53 +1,49 @@
|
|||
const request = require('request'),
|
||||
Core = require('./core');
|
||||
const Core = require('./core');
|
||||
|
||||
class GeneShift extends Core {
|
||||
run(state) {
|
||||
request({
|
||||
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);
|
||||
async run(state) {
|
||||
const body = await this.request({
|
||||
uri: 'http://geneshift.net/game/receiveLobby.php'
|
||||
});
|
||||
|
||||
const split = body.split('<br/>');
|
||||
let found = null;
|
||||
for(const line of split) {
|
||||
const fields = line.split('::');
|
||||
const ip = fields[2];
|
||||
const port = fields[3];
|
||||
if(ip === this.options.address && parseInt(port) === this.options.port) {
|
||||
found = fields;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(found === null) {
|
||||
throw new Error('Server not found in list');
|
||||
}
|
||||
|
||||
state.raw.countrycode = found[0];
|
||||
state.raw.country = found[1];
|
||||
state.name = found[4];
|
||||
state.map = found[5];
|
||||
state.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({});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,10 @@ class Hexen2 extends Quake1 {
|
|||
this.sendHeader = '\xFFstatus\x0a';
|
||||
this.responseHeader = '\xffn';
|
||||
}
|
||||
async run(state) {
|
||||
await super.run(state);
|
||||
state.gamePort = this.options.port - 50;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Hexen2;
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
const Gamespy3 = require('./gamespy3');
|
||||
|
||||
// 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 {
|
||||
constructor() {
|
||||
super();
|
||||
this.useOnlySingleSplit = true;
|
||||
this.isJc2mp = true;
|
||||
this.encoding = 'utf8';
|
||||
}
|
||||
finalizeState(state) {
|
||||
super.finalizeState(state);
|
||||
async run(state) {
|
||||
await super.run(state);
|
||||
if(!state.players.length && parseInt(state.raw.numplayers)) {
|
||||
for(let i = 0; i < parseInt(state.raw.numplayers); i++) {
|
||||
state.players.push({});
|
||||
|
|
|
@ -1,38 +1,28 @@
|
|||
const request = require('request'),
|
||||
Core = require('./core');
|
||||
const Core = require('./core');
|
||||
|
||||
class Kspdmp extends Core {
|
||||
run(state) {
|
||||
request({
|
||||
uri: 'http://'+this.options.address+':'+this.options.port_query,
|
||||
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);
|
||||
async run(state) {
|
||||
const body = await this.request({
|
||||
uri: 'http://'+this.options.address+':'+this.options.port
|
||||
});
|
||||
|
||||
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});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,30 +6,29 @@ class M2mp extends Core {
|
|||
this.encoding = 'latin1';
|
||||
}
|
||||
|
||||
run(state) {
|
||||
this.udpSend('M2MP',(buffer) => {
|
||||
async run(state) {
|
||||
const body = await this.udpSend('M2MP',(buffer) => {
|
||||
const reader = this.reader(buffer);
|
||||
|
||||
const header = reader.string({length:4});
|
||||
if(header !== 'M2MP') return;
|
||||
|
||||
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 header = reader.string({length: 4});
|
||||
if (header !== 'M2MP') return;
|
||||
return reader.rest();
|
||||
});
|
||||
|
||||
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) {
|
||||
|
|
|
@ -1,98 +1,79 @@
|
|||
const varint = require('varint'),
|
||||
async = require('async'),
|
||||
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
|
||||
]);
|
||||
}
|
||||
const Core = require('./core'),
|
||||
Varint = require('varint');
|
||||
|
||||
class Minecraft extends Core {
|
||||
run(state) {
|
||||
/** @type Buffer */
|
||||
let receivedData;
|
||||
constructor() {
|
||||
super();
|
||||
this.srvRecord = "_minecraft._tcp";
|
||||
}
|
||||
async run(state) {
|
||||
const portBuf = Buffer.alloc(2);
|
||||
portBuf.writeUInt16BE(this.options.port,0);
|
||||
|
||||
async.series([
|
||||
(c) => {
|
||||
// build and send handshake and status TCP packet
|
||||
const addressBuf = Buffer.from(this.options.host,'utf8');
|
||||
|
||||
const portBuf = Buffer.alloc(2);
|
||||
portBuf.writeUInt16BE(this.options.port_query,0);
|
||||
const bufs = [
|
||||
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 = [
|
||||
varIntBuffer(4),
|
||||
varIntBuffer(addressBuf.length),
|
||||
addressBuf,
|
||||
portBuf,
|
||||
varIntBuffer(1)
|
||||
];
|
||||
const data = await this.withTcp(async socket => {
|
||||
return await this.tcpSend(socket, outBuffer, data => {
|
||||
if(data.length < 10) return;
|
||||
const reader = this.reader(data);
|
||||
const length = reader.varint();
|
||||
if(data.length < length) return;
|
||||
return reader.rest();
|
||||
});
|
||||
});
|
||||
|
||||
const outBuffer = Buffer.concat([
|
||||
buildPacket(0,Buffer.concat(bufs)),
|
||||
buildPacket(0)
|
||||
]);
|
||||
const reader = this.reader(data);
|
||||
|
||||
this.tcpSend(outBuffer, (data) => {
|
||||
if(data.length < 10) return false;
|
||||
const expected = varint.decode(data);
|
||||
data = data.slice(varint.decode.bytes);
|
||||
if(data.length < expected) return false;
|
||||
receivedData = data;
|
||||
c();
|
||||
return true;
|
||||
const packetId = reader.varint();
|
||||
this.debugLog("Packet ID: "+packetId);
|
||||
|
||||
const strLen = reader.varint();
|
||||
this.debugLog("String Length: "+strLen);
|
||||
|
||||
const str = reader.rest().toString('utf8');
|
||||
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
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,40 +1,36 @@
|
|||
const Core = require('./core');
|
||||
|
||||
class Mumble extends Core {
|
||||
constructor() {
|
||||
super();
|
||||
this.options.socketTimeout = 5000;
|
||||
}
|
||||
|
||||
run(state) {
|
||||
this.tcpSend('json', (buffer) => {
|
||||
if(buffer.length < 10) return;
|
||||
const str = buffer.toString();
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(str);
|
||||
} catch(e) {
|
||||
// probably not all here yet
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
async run(state) {
|
||||
const json = await this.withTcp(async socket => {
|
||||
return await this.tcpSend(socket, 'json', (buffer) => {
|
||||
if (buffer.length < 10) return;
|
||||
const str = buffer.toString();
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(str);
|
||||
} catch (e) {
|
||||
// probably not all here yet
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.finish(state);
|
||||
return true;
|
||||
return json;
|
||||
});
|
||||
});
|
||||
|
||||
state.raw = json;
|
||||
state.name = json.name;
|
||||
state.gamePort = json.x_gtmurmur_connectport || 64738;
|
||||
|
||||
let channelStack = [state.raw.root];
|
||||
while(channelStack.length) {
|
||||
const channel = channelStack.shift();
|
||||
channel.description = this.cleanComment(channel.description);
|
||||
channelStack = channelStack.concat(channel.channels);
|
||||
for(const user of channel.users) {
|
||||
user.comment = this.cleanComment(user.comment);
|
||||
state.players.push(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cleanComment(str) {
|
||||
|
|
|
@ -6,24 +6,23 @@ class MumblePing extends Core {
|
|||
this.byteorder = 'be';
|
||||
}
|
||||
|
||||
run(state) {
|
||||
this.udpSend('\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08', (buffer) => {
|
||||
if(buffer.length < 24) return;
|
||||
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;
|
||||
async run(state) {
|
||||
const data = await this.udpSend('\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08', (buffer) => {
|
||||
if (buffer.length >= 24) return buffer;
|
||||
});
|
||||
|
||||
const reader = this.reader(data);
|
||||
reader.skip(1);
|
||||
state.raw.versionMajor = reader.uint(1);
|
||||
state.raw.versionMinor = reader.uint(1);
|
||||
state.raw.versionPatch = reader.uint(1);
|
||||
reader.skip(8);
|
||||
state.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({});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,85 +1,97 @@
|
|||
const gbxremote = require('gbxremote'),
|
||||
async = require('async'),
|
||||
Core = require('./core');
|
||||
|
||||
class Nadeo extends Core {
|
||||
constructor() {
|
||||
super();
|
||||
this.options.port = 2350;
|
||||
this.options.port_query = 5000;
|
||||
this.gbxclient = false;
|
||||
}
|
||||
async run(state) {
|
||||
await this.withClient(async client => {
|
||||
const start = Date.now();
|
||||
await this.methodCall(client, 'Authenticate', this.options.login, this.options.password);
|
||||
this.registerRtt(Date.now()-start);
|
||||
|
||||
reset() {
|
||||
super.reset();
|
||||
if(this.gbxclient) {
|
||||
this.gbxclient.terminate();
|
||||
this.gbxclient = false;
|
||||
}
|
||||
}
|
||||
//const data = this.methodCall(client, 'GetStatus');
|
||||
|
||||
run(state) {
|
||||
const cmds = [
|
||||
['Connect'],
|
||||
['Authenticate', this.options.login,this.options.password],
|
||||
['GetStatus'], // 1
|
||||
['GetPlayerList',10000,0], // 2
|
||||
['GetServerOptions'], // 3
|
||||
['GetCurrentMapInfo'], // 4
|
||||
['GetCurrentGameInfo'], // 5
|
||||
['GetNextMapInfo'] // 6
|
||||
];
|
||||
const results = [];
|
||||
|
||||
async.eachSeries(cmds, (cmdset,c) => {
|
||||
const cmd = cmdset[0];
|
||||
const params = cmdset.slice(1);
|
||||
|
||||
if(cmd === 'Connect') {
|
||||
const client = this.gbxclient = gbxremote.createClient(this.options.port_query,this.options.host, (err) => {
|
||||
if(err) return this.fatal('GBX error '+JSON.stringify(err));
|
||||
c();
|
||||
});
|
||||
client.on('error',() => {});
|
||||
} else {
|
||||
this.gbxclient.methodCall(cmd, params, (err, value) => {
|
||||
if(err) return this.fatal('XMLRPC error '+JSON.stringify(err));
|
||||
results.push(value);
|
||||
c();
|
||||
});
|
||||
{
|
||||
const results = await this.methodCall(client, 'GetServerOptions');
|
||||
state.name = this.stripColors(results.Name);
|
||||
state.password = (results.Password !== 'No password');
|
||||
state.maxplayers = results.CurrentMaxPlayers;
|
||||
state.raw.maxspectators = results.CurrentMaxSpectators;
|
||||
}
|
||||
}, () => {
|
||||
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');
|
||||
state.maxplayers = results[3].CurrentMaxPlayers;
|
||||
state.raw.maxspectators = results[3].CurrentMaxSpectators;
|
||||
state.map = this.stripColors(results[4].Name);
|
||||
state.raw.mapUid = results[4].UId;
|
||||
state.raw.gametype = gamemode;
|
||||
state.raw.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, 'GetCurrentMapInfo');
|
||||
state.map = this.stripColors(results.Name);
|
||||
state.raw.mapUid = results.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) {
|
||||
state.players.push({
|
||||
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) {
|
||||
return str.replace(/\$([0-9a-f]{3}|[a-z])/gi,'');
|
||||
}
|
||||
|
|
|
@ -1,131 +1,116 @@
|
|||
const async = require('async'),
|
||||
moment = require('moment'),
|
||||
const moment = require('moment'),
|
||||
Core = require('./core');
|
||||
|
||||
class OpenTtd extends Core {
|
||||
run(state) {
|
||||
async.series([
|
||||
(c) => {
|
||||
this.query(0,1,1,4,(reader, version) => {
|
||||
if(version >= 4) {
|
||||
const numGrf = reader.uint(1);
|
||||
state.raw.grfs = [];
|
||||
for(let i = 0; i < numGrf; i++) {
|
||||
const grf = {};
|
||||
grf.id = reader.part(4).toString('hex');
|
||||
grf.md5 = reader.part(16).toString('hex');
|
||||
state.raw.grfs.push(grf);
|
||||
}
|
||||
}
|
||||
if(version >= 3) {
|
||||
state.raw.date_current = this.readDate(reader);
|
||||
state.raw.date_start = this.readDate(reader);
|
||||
}
|
||||
if(version >= 2) {
|
||||
state.raw.maxcompanies = reader.uint(1);
|
||||
state.raw.numcompanies = reader.uint(1);
|
||||
state.raw.maxspectators = reader.uint(1);
|
||||
}
|
||||
|
||||
state.name = reader.string();
|
||||
state.raw.version = reader.string();
|
||||
|
||||
state.raw.language = this.decode(
|
||||
reader.uint(1),
|
||||
['any','en','de','fr']
|
||||
);
|
||||
|
||||
state.password = !!reader.uint(1);
|
||||
state.maxplayers = reader.uint(1);
|
||||
state.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);
|
||||
async run(state) {
|
||||
{
|
||||
const [reader, version] = await this.query(0, 1, 1, 4);
|
||||
if (version >= 4) {
|
||||
const numGrf = reader.uint(1);
|
||||
state.raw.grfs = [];
|
||||
for (let i = 0; i < numGrf; i++) {
|
||||
const grf = {};
|
||||
grf.id = reader.part(4).toString('hex');
|
||||
grf.md5 = reader.part(16).toString('hex');
|
||||
state.raw.grfs.push(grf);
|
||||
}
|
||||
}
|
||||
]);
|
||||
if (version >= 3) {
|
||||
state.raw.date_current = this.readDate(reader);
|
||||
state.raw.date_start = this.readDate(reader);
|
||||
}
|
||||
if (version >= 2) {
|
||||
state.raw.maxcompanies = reader.uint(1);
|
||||
state.raw.numcompanies = reader.uint(1);
|
||||
state.raw.maxspectators = reader.uint(1);
|
||||
}
|
||||
|
||||
state.name = reader.string();
|
||||
state.raw.version = reader.string();
|
||||
|
||||
state.raw.language = this.decode(
|
||||
reader.uint(1),
|
||||
['any', 'en', 'de', 'fr']
|
||||
);
|
||||
|
||||
state.password = !!reader.uint(1);
|
||||
state.maxplayers = reader.uint(1);
|
||||
state.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]);
|
||||
this.udpSend(b,(buffer) => {
|
||||
return await this.udpSend(b,(buffer) => {
|
||||
const reader = this.reader(buffer);
|
||||
|
||||
const packetLen = reader.uint(2);
|
||||
if(packetLen !== buffer.length) {
|
||||
this.fatal('Invalid reported packet length: '+packetLen+' '+buffer.length);
|
||||
return true;
|
||||
this.debugLog('Invalid reported packet length: '+packetLen+' '+buffer.length);
|
||||
return;
|
||||
}
|
||||
|
||||
const packetType = reader.uint(1);
|
||||
if(packetType !== expected) {
|
||||
this.fatal('Unexpected response packet type: '+packetType);
|
||||
return true;
|
||||
this.debugLog('Unexpected response packet type: '+packetType);
|
||||
return;
|
||||
}
|
||||
|
||||
const protocolVersion = reader.uint(1);
|
||||
if((minver !== -1 && protocolVersion < minver) || (maxver !== -1 && protocolVersion > maxver)) {
|
||||
this.fatal('Unknown protocol version: '+protocolVersion+' Expected: '+minver+'-'+maxver);
|
||||
return true;
|
||||
throw new Error('Unknown protocol version: '+protocolVersion+' Expected: '+minver+'-'+maxver);
|
||||
}
|
||||
|
||||
done(reader,protocolVersion);
|
||||
return true;
|
||||
return [reader,protocolVersion];
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -10,79 +10,78 @@ class Quake2 extends Core {
|
|||
this.isQuake1 = false;
|
||||
}
|
||||
|
||||
run(state) {
|
||||
this.udpSend('\xff\xff\xff\xff'+this.sendHeader+'\x00', (buffer) => {
|
||||
const reader = this.reader(buffer);
|
||||
|
||||
const header = reader.string({length:4,encoding:'latin1'});
|
||||
if(header !== '\xff\xff\xff\xff') return;
|
||||
|
||||
let response;
|
||||
if(this.isQuake1) {
|
||||
response = reader.string({length:this.responseHeader.length});
|
||||
async run(state) {
|
||||
const body = await this.udpSend('\xff\xff\xff\xff'+this.sendHeader+'\x00', packet => {
|
||||
const reader = this.reader(packet);
|
||||
const header = reader.string({length: 4, encoding: 'latin1'});
|
||||
if (header !== '\xff\xff\xff\xff') return;
|
||||
let type;
|
||||
if (this.isQuake1) {
|
||||
type = reader.string({length: this.responseHeader.length});
|
||||
} else {
|
||||
response = reader.string({encoding:'latin1'});
|
||||
type = reader.string({encoding: 'latin1'});
|
||||
}
|
||||
if(response !== this.responseHeader) return;
|
||||
|
||||
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;
|
||||
if (type !== this.responseHeader) return;
|
||||
return reader.rest();
|
||||
});
|
||||
|
||||
const reader = this.reader(body);
|
||||
const info = reader.string().split('\\');
|
||||
if(info[0] === '') info.shift();
|
||||
|
||||
while(true) {
|
||||
const key = info.shift();
|
||||
const value = info.shift();
|
||||
if(typeof value === 'undefined') break;
|
||||
state.raw[key] = value;
|
||||
}
|
||||
|
||||
while(!reader.done()) {
|
||||
const line = reader.string();
|
||||
if(!line || line.charAt(0) === '\0') break;
|
||||
|
||||
const args = [];
|
||||
const split = line.split('"');
|
||||
split.forEach((part,i) => {
|
||||
const inQuote = (i%2 === 1);
|
||||
if(inQuote) {
|
||||
args.push(part);
|
||||
} else {
|
||||
const splitSpace = part.split(' ');
|
||||
for (const subpart of splitSpace) {
|
||||
if(subpart) args.push(subpart);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const player = {};
|
||||
if(this.isQuake1) {
|
||||
player.id = parseInt(args.shift());
|
||||
player.score = parseInt(args.shift());
|
||||
player.time = parseInt(args.shift());
|
||||
player.ping = parseInt(args.shift());
|
||||
player.name = args.shift();
|
||||
player.skin = args.shift();
|
||||
player.color1 = parseInt(args.shift());
|
||||
player.color2 = parseInt(args.shift());
|
||||
} else {
|
||||
player.frags = parseInt(args.shift());
|
||||
player.ping = parseInt(args.shift());
|
||||
player.name = args.shift() || '';
|
||||
if (!player.name) delete player.name;
|
||||
player.address = args.shift() || '';
|
||||
if (!player.address) delete player.address;
|
||||
}
|
||||
|
||||
(player.ping ? state.players : state.bots).push(player);
|
||||
}
|
||||
|
||||
if('g_needpass' in state.raw) state.password = state.raw.g_needpass;
|
||||
if('mapname' in state.raw) state.map = state.raw.mapname;
|
||||
if('sv_maxclients' in state.raw) state.maxplayers = state.raw.sv_maxclients;
|
||||
if('maxclients' in state.raw) state.maxplayers = state.raw.maxclients;
|
||||
if('sv_hostname' in state.raw) state.name = state.raw.sv_hostname;
|
||||
if('hostname' in state.raw) state.name = state.raw.hostname;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,8 @@ class Quake3 extends Quake2 {
|
|||
this.sendHeader = 'getstatus';
|
||||
this.responseHeader = 'statusResponse';
|
||||
}
|
||||
finalizeState(state) {
|
||||
async run(state) {
|
||||
await super.run(state);
|
||||
state.name = this.stripColors(state.name);
|
||||
for(const key of Object.keys(state.raw)) {
|
||||
state.raw[key] = this.stripColors(state.raw[key]);
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
const async = require('async'),
|
||||
Core = require('./core');
|
||||
const Core = require('./core');
|
||||
|
||||
class Samp extends Core {
|
||||
constructor() {
|
||||
|
@ -7,87 +6,83 @@ class Samp extends Core {
|
|||
this.encoding = 'win1252';
|
||||
}
|
||||
|
||||
run(state) {
|
||||
async.series([
|
||||
(c) => {
|
||||
this.sendPacket('i',(reader) => {
|
||||
state.password = !!reader.uint(1);
|
||||
state.raw.numplayers = reader.uint(2);
|
||||
state.maxplayers = reader.uint(2);
|
||||
state.name = this.readString(reader,4);
|
||||
state.raw.gamemode = this.readString(reader,4);
|
||||
this.map = this.readString(reader,4);
|
||||
c();
|
||||
});
|
||||
},
|
||||
(c) => {
|
||||
this.sendPacket('r',(reader) => {
|
||||
const ruleCount = reader.uint(2);
|
||||
state.raw.rules = {};
|
||||
for(let i = 0; i < ruleCount; i++) {
|
||||
const key = this.readString(reader,1);
|
||||
const value = this.readString(reader,1);
|
||||
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);
|
||||
async run(state) {
|
||||
// read info
|
||||
{
|
||||
const reader = await this.sendPacket('i');
|
||||
state.password = !!reader.uint(1);
|
||||
state.raw.numplayers = reader.uint(2);
|
||||
state.maxplayers = reader.uint(2);
|
||||
state.name = this.readString(reader,4);
|
||||
state.raw.gamemode = this.readString(reader,4);
|
||||
this.map = this.readString(reader,4);
|
||||
}
|
||||
|
||||
// read rules
|
||||
{
|
||||
const reader = await this.sendPacket('r');
|
||||
const ruleCount = reader.uint(2);
|
||||
state.raw.rules = {};
|
||||
for(let i = 0; i < ruleCount; i++) {
|
||||
const key = this.readString(reader,1);
|
||||
const value = this.readString(reader,1);
|
||||
state.raw.rules[key] = value;
|
||||
}
|
||||
]);
|
||||
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) {
|
||||
const length = reader.uint(lenBytes);
|
||||
if(!length) return '';
|
||||
const string = reader.string({length:length});
|
||||
return string;
|
||||
return reader.string({length:length});
|
||||
}
|
||||
sendPacket(type,onresponse,ontimeout) {
|
||||
const outbuffer = Buffer.alloc(11);
|
||||
outbuffer.writeUInt32BE(0x53414D50,0);
|
||||
async sendPacket(type,allowTimeout) {
|
||||
const outBuffer = Buffer.alloc(11);
|
||||
outBuffer.writeUInt32BE(0x53414D50,0);
|
||||
const ipSplit = this.options.address.split('.');
|
||||
outbuffer.writeUInt8(parseInt(ipSplit[0]),4);
|
||||
outbuffer.writeUInt8(parseInt(ipSplit[1]),5);
|
||||
outbuffer.writeUInt8(parseInt(ipSplit[2]),6);
|
||||
outbuffer.writeUInt8(parseInt(ipSplit[3]),7);
|
||||
outbuffer.writeUInt16LE(this.options.port,8);
|
||||
outbuffer.writeUInt8(type.charCodeAt(0),10);
|
||||
outBuffer.writeUInt8(parseInt(ipSplit[0]),4);
|
||||
outBuffer.writeUInt8(parseInt(ipSplit[1]),5);
|
||||
outBuffer.writeUInt8(parseInt(ipSplit[2]),6);
|
||||
outBuffer.writeUInt8(parseInt(ipSplit[3]),7);
|
||||
outBuffer.writeUInt16LE(this.options.port,8);
|
||||
outBuffer.writeUInt8(type.charCodeAt(0),10);
|
||||
|
||||
this.udpSend(outbuffer,(buffer) => {
|
||||
const reader = this.reader(buffer);
|
||||
for(let i = 0; i < outbuffer.length; i++) {
|
||||
if(outbuffer.readUInt8(i) !== reader.uint(1)) return;
|
||||
return await this.udpSend(
|
||||
outBuffer,
|
||||
(buffer) => {
|
||||
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;
|
||||
}
|
||||
});
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,58 +6,59 @@ class Starmade extends Core {
|
|||
this.encoding = 'latin1';
|
||||
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]);
|
||||
|
||||
this.tcpSend(b,(buffer) => {
|
||||
const reader = this.reader(buffer);
|
||||
|
||||
if(buffer.length < 4) return false;
|
||||
const packetLength = reader.uint(4);
|
||||
if(buffer.length < packetLength+12) return false;
|
||||
|
||||
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 payload = await this.withTcp(async socket => {
|
||||
return await this.tcpSend(socket, b, buffer => {
|
||||
if (buffer.length < 4) return;
|
||||
const reader = this.reader(buffer);
|
||||
const packetLength = reader.uint(4);
|
||||
if (buffer.length < packetLength + 12) return;
|
||||
return reader.rest();
|
||||
});
|
||||
});
|
||||
|
||||
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({});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,77 +1,70 @@
|
|||
const async = require('async'),
|
||||
Core = require('./core');
|
||||
const Core = require('./core');
|
||||
|
||||
class Teamspeak2 extends Core {
|
||||
run(state) {
|
||||
async.series([
|
||||
(c) => {
|
||||
this.sendCommand('sel '+this.options.port, (data) => {
|
||||
if(data !== '[TS]') this.fatal('Invalid header');
|
||||
c();
|
||||
});
|
||||
},
|
||||
(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);
|
||||
async run(state) {
|
||||
const queryPort = this.options.teamspeakQueryPort || 51234;
|
||||
|
||||
await this.withTcp(async socket => {
|
||||
{
|
||||
const data = await this.sendCommand(socket, 'sel '+this.options.port);
|
||||
if(data !== '[TS]') throw new Error('Invalid header');
|
||||
}
|
||||
]);
|
||||
|
||||
{
|
||||
const data = await this.sendCommand(socket, 'si');
|
||||
for (const line of data.split('\r\n')) {
|
||||
const equals = line.indexOf('=');
|
||||
const key = equals === -1 ? line : line.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.slice(-6).toString() !== '\r\nOK\r\n') return;
|
||||
c(buffer.slice(0,-6).toString());
|
||||
return true;
|
||||
return buffer.slice(0,-6).toString();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,78 +1,67 @@
|
|||
const async = require('async'),
|
||||
Core = require('./core');
|
||||
const Core = require('./core');
|
||||
|
||||
class Teamspeak3 extends Core {
|
||||
run(state) {
|
||||
async.series([
|
||||
(c) => {
|
||||
this.sendCommand('use port='+this.options.port, (data) => {
|
||||
const split = data.split('\n\r');
|
||||
if(split[0] !== 'TS3') this.fatal('Invalid header');
|
||||
c();
|
||||
}, true);
|
||||
},
|
||||
(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);
|
||||
async run(state) {
|
||||
const queryPort = this.options.teamspeakQueryPort || 10011;
|
||||
|
||||
await this.withTcp(async socket => {
|
||||
{
|
||||
const data = await this.sendCommand(socket, 'use port='+this.options.port, true);
|
||||
const split = data.split('\n\r');
|
||||
if(split[0] !== 'TS3') throw new Error('Invalid header');
|
||||
}
|
||||
]);
|
||||
}
|
||||
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;
|
||||
} else {
|
||||
const segments = body.split('|');
|
||||
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;
|
||||
{
|
||||
const list = await this.sendCommand(socket, 'clientlist');
|
||||
for (const client of list) {
|
||||
client.name = client.client_nickname;
|
||||
delete client.client_nickname;
|
||||
if(client.client_type === '0') {
|
||||
state.players.push(client);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,36 +1,25 @@
|
|||
const request = require('request'),
|
||||
Core = require('./core');
|
||||
const Core = require('./core');
|
||||
|
||||
class Terraria extends Core {
|
||||
run(state) {
|
||||
request({
|
||||
uri: 'http://'+this.options.address+':'+this.options.port_query+'/v2/server/status',
|
||||
timeout: this.options.socketTimeout,
|
||||
async run(state) {
|
||||
const body = await this.request({
|
||||
uri: 'http://'+this.options.address+':'+this.options.port+'/v2/server/status',
|
||||
qs: {
|
||||
players: 'true',
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,81 +5,81 @@ class Tribes1 extends Core {
|
|||
super();
|
||||
this.encoding = 'latin1';
|
||||
}
|
||||
run(state) {
|
||||
|
||||
async run(state) {
|
||||
const queryBuffer = Buffer.from('b++');
|
||||
this.udpSend(queryBuffer,(buffer) => {
|
||||
const reader = await this.udpSend(queryBuffer,(buffer) => {
|
||||
const reader = this.reader(buffer);
|
||||
const header = reader.string({length:4});
|
||||
const header = reader.string({length: 4});
|
||||
if (header !== 'c++b') {
|
||||
this.fatal('Header response does not match: ' + header);
|
||||
return true;
|
||||
this.debugLog('Header response does not match: ' + header);
|
||||
return;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
this.finish(state);
|
||||
return true;
|
||||
return reader;
|
||||
});
|
||||
|
||||
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) {
|
||||
const str = this.readString(reader);
|
||||
|
|
|
@ -7,7 +7,8 @@ class Tribes1Master extends Core {
|
|||
super();
|
||||
this.encoding = 'latin1';
|
||||
}
|
||||
run(state) {
|
||||
|
||||
async run(state) {
|
||||
const queryBuffer = Buffer.from([
|
||||
0x10, // standard header
|
||||
0x03, // dump servers
|
||||
|
@ -18,28 +19,27 @@ class Tribes1Master extends Core {
|
|||
|
||||
let parts = new Map();
|
||||
let total = 0;
|
||||
this.udpSend(queryBuffer,(buffer) => {
|
||||
const full = await this.udpSend(queryBuffer,(buffer) => {
|
||||
const reader = this.reader(buffer);
|
||||
const header = reader.uint(2);
|
||||
if (header !== 0x0610) {
|
||||
this.fatal('Header response does not match: ' + header.toString(16));
|
||||
return true;
|
||||
this.debugLog('Header response does not match: ' + header.toString(16));
|
||||
return;
|
||||
}
|
||||
const num = reader.uint(1);
|
||||
const t = reader.uint(1);
|
||||
if (t <= 0 || (total > 0 && t !== total)) {
|
||||
this.fatal('Conflicting total: ' + t);
|
||||
return true;
|
||||
throw new Error('Conflicting packet total: ' + t);
|
||||
}
|
||||
total = t;
|
||||
|
||||
if (num < 1 || num > total) {
|
||||
this.fatal('Invalid packet number: ' + num + ' ' + total);
|
||||
return true;
|
||||
this.debugLog('Invalid packet number: ' + num + ' ' + total);
|
||||
return;
|
||||
}
|
||||
if (parts.has(num)) {
|
||||
this.fatal('Duplicate part: ' + num);
|
||||
return true;
|
||||
this.debugLog('Duplicate part: ' + num);
|
||||
return;
|
||||
}
|
||||
|
||||
reader.skip(2); // challenge (0x0201)
|
||||
|
@ -49,32 +49,29 @@ class Tribes1Master extends Core {
|
|||
if (parts.size === total) {
|
||||
const ordered = [];
|
||||
for (let i = 1; i <= total; i++) ordered.push(parts.get(i));
|
||||
const full = 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;
|
||||
return Buffer.concat(ordered);
|
||||
}
|
||||
});
|
||||
|
||||
const fullReader = this.reader(full);
|
||||
state.raw.name = this.readString(fullReader);
|
||||
state.raw.motd = this.readString(fullReader);
|
||||
|
||||
state.raw.servers = [];
|
||||
while (!fullReader.done()) {
|
||||
fullReader.skip(1); // junk ?
|
||||
const count = fullReader.uint(1);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const six = fullReader.uint(1);
|
||||
if (six !== 6) {
|
||||
throw new Error('Expecting 6');
|
||||
}
|
||||
const ip = fullReader.uint(4);
|
||||
const port = fullReader.uint(2);
|
||||
const ipStr = (ip & 255) + '.' + (ip >> 8 & 255) + '.' + (ip >> 16 & 255) + '.' + (ip >>> 24);
|
||||
state.raw.servers.push(ipStr+":"+port);
|
||||
}
|
||||
}
|
||||
}
|
||||
readString(reader) {
|
||||
const length = reader.uint(1);
|
||||
|
|
|
@ -1,101 +1,90 @@
|
|||
const async = require('async'),
|
||||
Core = require('./core');
|
||||
const Core = require('./core');
|
||||
|
||||
class Unreal2 extends Core {
|
||||
constructor() {
|
||||
super();
|
||||
this.encoding = 'latin1';
|
||||
}
|
||||
run(state) {
|
||||
async.series([
|
||||
(c) => {
|
||||
this.sendPacket(0,true,(b) => {
|
||||
const reader = this.reader(b);
|
||||
state.raw.serverid = reader.uint(4);
|
||||
state.raw.ip = this.readUnrealString(reader);
|
||||
state.raw.port = reader.uint(4);
|
||||
state.raw.queryport = reader.uint(4);
|
||||
state.name = this.readUnrealString(reader,true);
|
||||
state.map = this.readUnrealString(reader,true);
|
||||
state.raw.gametype = this.readUnrealString(reader,true);
|
||||
state.raw.numplayers = reader.uint(4);
|
||||
state.maxplayers = reader.uint(4);
|
||||
this.readExtraInfo(reader,state);
|
||||
async run(state) {
|
||||
{
|
||||
const b = await this.sendPacket(0, true);
|
||||
const reader = this.reader(b);
|
||||
state.raw.serverid = reader.uint(4);
|
||||
state.raw.ip = this.readUnrealString(reader);
|
||||
state.gamePort = reader.uint(4);
|
||||
state.raw.queryport = reader.uint(4);
|
||||
state.name = this.readUnrealString(reader, true);
|
||||
state.map = this.readUnrealString(reader, true);
|
||||
state.raw.gametype = this.readUnrealString(reader, true);
|
||||
state.raw.numplayers = reader.uint(4);
|
||||
state.maxplayers = reader.uint(4);
|
||||
this.readExtraInfo(reader, state);
|
||||
}
|
||||
|
||||
c();
|
||||
});
|
||||
},
|
||||
(c) => {
|
||||
this.sendPacket(1,true,(b) => {
|
||||
const reader = this.reader(b);
|
||||
state.raw.mutators = [];
|
||||
state.raw.rules = {};
|
||||
while(!reader.done()) {
|
||||
{
|
||||
const b = await this.sendPacket(1,true);
|
||||
const reader = this.reader(b);
|
||||
state.raw.mutators = [];
|
||||
state.raw.rules = {};
|
||||
while(!reader.done()) {
|
||||
const key = this.readUnrealString(reader,true);
|
||||
const value = this.readUnrealString(reader,true);
|
||||
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 value = this.readUnrealString(reader,true);
|
||||
if(key === 'Mutator') state.raw.mutators.push(value);
|
||||
else state.raw.rules[key] = value;
|
||||
player[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if('GamePassword' in state.raw.rules)
|
||||
state.password = state.raw.rules.GamePassword !== 'True';
|
||||
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;
|
||||
}
|
||||
|
||||
c();
|
||||
});
|
||||
},
|
||||
(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);
|
||||
(player.ping ? state.players : state.bots).push(player);
|
||||
}
|
||||
]);
|
||||
}
|
||||
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) {
|
||||
let length = reader.uint(1);
|
||||
let out;
|
||||
|
@ -105,10 +94,10 @@ class Unreal2 extends Core {
|
|||
if(length > 0) out = reader.string();
|
||||
} else {
|
||||
length = (length&0x7f)*2;
|
||||
if(this.debug) {
|
||||
console.log("UCS2 STRING");
|
||||
console.log(length,reader.buffer.slice(reader.i,reader.i+length));
|
||||
}
|
||||
this.debugLog(log => {
|
||||
log("UCS2 STRING");
|
||||
log(length,reader.buffer.slice(reader.i,reader.i+length));
|
||||
});
|
||||
out = reader.string({encoding:'ucs2',length:length});
|
||||
}
|
||||
|
||||
|
@ -120,11 +109,12 @@ class Unreal2 extends Core {
|
|||
|
||||
return out;
|
||||
}
|
||||
sendPacket(type,required,callback) {
|
||||
|
||||
async sendPacket(type,required) {
|
||||
const outbuffer = Buffer.from([0x79,0,0,0,type]);
|
||||
|
||||
const packets = [];
|
||||
this.udpSend(outbuffer,(buffer) => {
|
||||
return await this.udpSend(outbuffer,(buffer) => {
|
||||
const reader = this.reader(buffer);
|
||||
const header = reader.uint(4);
|
||||
const iType = reader.uint(1);
|
||||
|
@ -132,8 +122,7 @@ class Unreal2 extends Core {
|
|||
packets.push(reader.rest());
|
||||
}, () => {
|
||||
if(!packets.length && required) return;
|
||||
callback(Buffer.concat(packets));
|
||||
return true;
|
||||
return Buffer.concat(packets);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
const Gamespy3 = require('./gamespy3');
|
||||
|
||||
class Ut3 extends Gamespy3 {
|
||||
finalizeState(state) {
|
||||
super.finalizeState(state);
|
||||
async run(state) {
|
||||
await super.run(state);
|
||||
|
||||
this.translate(state.raw,{
|
||||
'mapname': false,
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
const async = require('async'),
|
||||
Bzip2 = require('compressjs').Bzip2,
|
||||
const Bzip2 = require('compressjs').Bzip2,
|
||||
Core = require('./core');
|
||||
|
||||
class Valve extends Core {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.options.port = 27015;
|
||||
|
||||
// legacy goldsrc info response -- basically not used by ANYTHING now,
|
||||
// as most (all?) goldsrc servers respond with the source info reponse
|
||||
// delete in a few years if nothing ends up using it anymore
|
||||
|
@ -28,173 +25,172 @@ class Valve extends Core {
|
|||
this._challenge = '';
|
||||
}
|
||||
|
||||
run(state) {
|
||||
async.series([
|
||||
(c) => { this.queryInfo(state,c); },
|
||||
(c) => { this.queryChallenge(state,c); },
|
||||
(c) => { this.queryPlayers(state,c); },
|
||||
(c) => { this.queryRules(state,c); },
|
||||
(c) => { this.cleanup(state,c); },
|
||||
(c) => { this.finish(state); }
|
||||
]);
|
||||
async run(state) {
|
||||
if (!this.options.port) this.options.port = 27015;
|
||||
await this.queryInfo(state);
|
||||
await this.queryChallenge();
|
||||
await this.queryPlayers(state);
|
||||
await this.queryRules(state);
|
||||
await this.cleanup(state);
|
||||
}
|
||||
|
||||
queryInfo(state,c) {
|
||||
this.sendPacket(
|
||||
0x54,false,'Source Engine Query\0',
|
||||
async queryInfo(state) {
|
||||
this.debugLog("Requesting info ...");
|
||||
const b = await this.sendPacket(
|
||||
0x54,
|
||||
false,
|
||||
'Source Engine Query\0',
|
||||
this.goldsrcInfo ? 0x6D : 0x49,
|
||||
(b) => {
|
||||
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();
|
||||
}
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
queryChallenge(state,c) {
|
||||
if(this.legacyChallenge) {
|
||||
this.sendPacket(0x57,false,null,0x41,(b) => {
|
||||
// sendPacket will catch the response packet and
|
||||
// save the challenge for us
|
||||
c();
|
||||
});
|
||||
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 {
|
||||
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 = [];
|
||||
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.
|
||||
if(!name) continue;
|
||||
this.debugLog("Requesting player list ...");
|
||||
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
|
||||
if (state.raw.steamappid === 730 && name === 'Max Players') continue;
|
||||
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();
|
||||
|
||||
state.raw.players.push({
|
||||
name:name, score:score, time:time
|
||||
});
|
||||
}
|
||||
this.debugLog("Found player: "+name+" "+score+" "+time);
|
||||
|
||||
c();
|
||||
}, () => {
|
||||
// CSGO doesn't even respond sometimes if host_players_show is not 2
|
||||
// Ignore timeouts in only this case
|
||||
if (state.raw.steamappid === 730) {
|
||||
c();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
// connecting players don't count as players.
|
||||
if(!name) continue;
|
||||
|
||||
// CSGO sometimes adds a bot named 'Max Players' if host_players_show is not 2
|
||||
if (state.raw.steamappid === 730 && name === 'Max Players') continue;
|
||||
|
||||
state.raw.players.push({
|
||||
name:name, score:score, time:time
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
queryRules(state,c) {
|
||||
async queryRules(state) {
|
||||
state.raw.rules = {};
|
||||
this.sendPacket(0x56,true,null,0x45,(b) => {
|
||||
const reader = this.reader(b);
|
||||
const num = reader.uint(2);
|
||||
for(let i = 0; i < num; i++) {
|
||||
const key = reader.string();
|
||||
const value = reader.string();
|
||||
state.raw.rules[key] = value;
|
||||
}
|
||||
c();
|
||||
}, () => {
|
||||
// no rules were returned after timeout --
|
||||
// the server probably has them disabled
|
||||
// ignore the timeout
|
||||
c();
|
||||
return true;
|
||||
});
|
||||
this.debugLog("Requesting rules ...");
|
||||
const b = await this.sendPacket(0x56,true,null,0x45,true);
|
||||
if (b === null) return; // timed out - the server probably just has rules disabled
|
||||
|
||||
const reader = this.reader(b);
|
||||
const num = reader.uint(2);
|
||||
for(let i = 0; i < num; i++) {
|
||||
const key = reader.string();
|
||||
const value = reader.string();
|
||||
state.raw.rules[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
cleanup(state,c) {
|
||||
async cleanup(state) {
|
||||
// Battalion 1944 puts its info into rules fields for some reason
|
||||
if ('bat_name_s' in state.raw.rules) {
|
||||
state.name = state.raw.rules.bat_name_s;
|
||||
|
@ -234,142 +230,157 @@ class Valve extends Core {
|
|||
if (sortedPlayers.length) state.players.push(sortedPlayers.pop());
|
||||
else state.players.push({});
|
||||
}
|
||||
|
||||
c();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request packet and returns only the response type expected
|
||||
* @param {number} type
|
||||
* @param {boolean} sendChallenge
|
||||
* @param {?string|Buffer} payload
|
||||
* @param {number} expect
|
||||
* @param {function(Buffer)} callback
|
||||
* @param {(function():boolean)=} ontimeout
|
||||
* @param {boolean=} allowTimeout
|
||||
* @returns Buffer|null
|
||||
**/
|
||||
sendPacket(
|
||||
async sendPacket(
|
||||
type,
|
||||
sendChallenge,
|
||||
payload,
|
||||
expect,
|
||||
callback,
|
||||
ontimeout
|
||||
allowTimeout
|
||||
) {
|
||||
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 = {};
|
||||
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) => {
|
||||
const type = reader.uint(1);
|
||||
let bzip = false;
|
||||
if(!this.goldsrcSplits && uid & 0x80000000) bzip = true;
|
||||
|
||||
if(type === 0x41) {
|
||||
const key = reader.uint(4);
|
||||
|
||||
if(this.debug) console.log('Received challenge key: ' + key);
|
||||
|
||||
if(this._challenge !== key) {
|
||||
this._challenge = key;
|
||||
if(sendChallenge) {
|
||||
if (this.debug) console.log('Restarting query');
|
||||
send();
|
||||
return 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();
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
packets[packetNum] = payload;
|
||||
|
||||
if(this.debug) console.log("Received "+type.toString(16)+" expected "+expect.toString(16));
|
||||
if(type !== expect) return;
|
||||
callback(reader.rest());
|
||||
return true;
|
||||
};
|
||||
this.debugLog(() => "Received partial packet uid:"+uid+" num:"+packetNum);
|
||||
this.debugLog(() => "Received "+Object.keys(packets).length+'/'+numPackets+" packets for this UID");
|
||||
|
||||
const receivedOne = (buffer) => {
|
||||
const reader = this.reader(buffer);
|
||||
if(Object.keys(packets).length !== numPackets) return;
|
||||
|
||||
const header = reader.int(4);
|
||||
if(header === -1) {
|
||||
// full package
|
||||
if(this.debug) console.log("Received full packet");
|
||||
return receivedFull(reader);
|
||||
}
|
||||
if(header === -2) {
|
||||
// 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;
|
||||
// assemble the parts
|
||||
const list = [];
|
||||
for(let i = 0; i < numPackets; i++) {
|
||||
if(!(i in packets)) {
|
||||
throw new Error('Missing packet #'+i);
|
||||
}
|
||||
list.push(packets[i]);
|
||||
}
|
||||
list.push(packets[i]);
|
||||
}
|
||||
|
||||
let assembled = Buffer.concat(list);
|
||||
if(bzip) {
|
||||
if(this.debug) console.log("BZIP DETECTED - Extracing packet...");
|
||||
try {
|
||||
assembled = Buffer.from(Bzip2.decompressFile(assembled));
|
||||
} catch(e) {
|
||||
this.fatal('Invalid bzip packet');
|
||||
return true;
|
||||
let assembled = Buffer.concat(list);
|
||||
if(bzip) {
|
||||
this.debugLog("BZIP DETECTED - Extracing packet...");
|
||||
try {
|
||||
assembled = Buffer.from(Bzip2.decompressFile(assembled));
|
||||
} catch(e) {
|
||||
throw new Error('Invalid bzip packet');
|
||||
}
|
||||
}
|
||||
const assembledReader = this.reader(assembled);
|
||||
assembledReader.skip(4); // header
|
||||
return onResponse(assembledReader.rest());
|
||||
}
|
||||
const assembledReader = this.reader(assembled);
|
||||
assembledReader.skip(4); // header
|
||||
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();
|
||||
},
|
||||
onTimeout
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,31 +5,30 @@ class Ventrilo extends Core {
|
|||
super();
|
||||
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;
|
||||
if('MAXCLIENTS' in state.raw) state.maxplayers = state.raw.MAXCLIENTS;
|
||||
if(this.trueTest(state.raw.AUTH)) state.password = true;
|
||||
this.finish(state);
|
||||
});
|
||||
async run(state) {
|
||||
const data = await this.sendCommand(2,'');
|
||||
state.raw = splitFields(data.toString());
|
||||
for (const client of state.raw.CLIENTS) {
|
||||
client.name = client.NAME;
|
||||
delete client.NAME;
|
||||
client.ping = parseInt(client.PING);
|
||||
delete client.PING;
|
||||
state.players.push(client);
|
||||
}
|
||||
delete state.raw.CLIENTS;
|
||||
|
||||
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);
|
||||
body.write(password,0,15,'utf8');
|
||||
const encrypted = encrypt(cmd,body);
|
||||
|
||||
const packets = {};
|
||||
this.udpSend(encrypted, (buffer) => {
|
||||
return await this.udpSend(encrypted, (buffer) => {
|
||||
if(buffer.length < 20) return;
|
||||
const data = decrypt(buffer);
|
||||
|
||||
|
@ -39,11 +38,10 @@ class Ventrilo extends Core {
|
|||
|
||||
const out = [];
|
||||
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]);
|
||||
}
|
||||
c(Buffer.concat(out));
|
||||
return true;
|
||||
return Buffer.concat(out);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
const Quake3 = require('./quake3');
|
||||
|
||||
class Warsow extends Quake3 {
|
||||
finalizeState(state) {
|
||||
super.finalizeState(state);
|
||||
async run(state) {
|
||||
await super.run(state);
|
||||
if(state.players) {
|
||||
for(const player of state.players) {
|
||||
player.team = player.address;
|
||||
|
|
Loading…
Reference in New Issue