mirror of
https://github.com/gamedig/node-gamedig.git
synced 2024-11-17 09:18:31 +01:00
Additional async rewrite
This commit is contained in:
parent
efe12a00aa
commit
29ce0b82d0
24 changed files with 654 additions and 470 deletions
|
@ -5,8 +5,8 @@ const argv = require('minimist')(process.argv.slice(2)),
|
||||||
|
|
||||||
const debug = argv.debug;
|
const debug = argv.debug;
|
||||||
delete argv.debug;
|
delete argv.debug;
|
||||||
const pretty = !!argv.pretty;
|
const pretty = !!argv.pretty || debug;
|
||||||
delete argv.output;
|
delete argv.pretty;
|
||||||
|
|
||||||
const options = {};
|
const options = {};
|
||||||
for(const key of Object.keys(argv)) {
|
for(const key of Object.keys(argv)) {
|
||||||
|
@ -14,14 +14,21 @@ for(const key of Object.keys(argv)) {
|
||||||
if(
|
if(
|
||||||
key === '_'
|
key === '_'
|
||||||
|| key.charAt(0) === '$'
|
|| key.charAt(0) === '$'
|
||||||
|| (typeof value !== 'string' && typeof value !== 'number')
|
|
||||||
)
|
)
|
||||||
continue;
|
continue;
|
||||||
options[key] = value;
|
options[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (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) Gamedig.debug = true;
|
if(debug) Gamedig.debug = true;
|
||||||
Gamedig.isCommandLine = true;
|
|
||||||
|
|
||||||
Gamedig.query(options)
|
Gamedig.query(options)
|
||||||
.then((state) => {
|
.then((state) => {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
const fs = require('fs'),
|
const fs = require('fs'),
|
||||||
TypeResolver = require('../lib/typeresolver');
|
TypeResolver = require('../lib/GameResolver');
|
||||||
|
|
||||||
const generated = TypeResolver.printReadme();
|
const generated = TypeResolver.printReadme();
|
||||||
|
|
||||||
|
|
28
games.txt
28
games.txt
|
@ -1,4 +1,4 @@
|
||||||
# id | pretty | protocol | options | parameters
|
# id | pretty name for readme | protocol | options | extra
|
||||||
|
|
||||||
#### TODO:
|
#### TODO:
|
||||||
# cube1|Cube 1|cube|port=28786,port_query_offset=1
|
# cube1|Cube 1|cube|port=28786,port_query_offset=1
|
||||||
|
@ -38,8 +38,8 @@ avp2|Aliens vs Predator 2|gamespy1|port=27888
|
||||||
# both port and port_query should be specified when used
|
# both port and port_query should be specified when used
|
||||||
avp2010|Aliens vs Predator 2010|valve
|
avp2010|Aliens vs Predator 2010|valve
|
||||||
|
|
||||||
americasarmy|America's Army|americasarmy|port=1716,port_query_offset=1
|
americasarmy|America's Army|gamespy2|port=1716,port_query_offset=1
|
||||||
americasarmy2|America's Army 2|americasarmy|port=1716,port_query_offset=1
|
americasarmy2|America's Army 2|gamespy2|port=1716,port_query_offset=1
|
||||||
americasarmy3|America's Army 3|valve|port=8777,port_query=27020
|
americasarmy3|America's Army 3|valve|port=8777,port_query=27020
|
||||||
americasarmypg|America's Army: Proving Grounds|valve|port=8777,port_query=27020
|
americasarmypg|America's Army: Proving Grounds|valve|port=8777,port_query=27020
|
||||||
|
|
||||||
|
@ -53,9 +53,9 @@ bat1944|Battalion 1944|valve|port=7777,port_query_offset=3
|
||||||
|
|
||||||
bf1942|Battlefield 1942|gamespy1|port=14567,port_query=23000
|
bf1942|Battlefield 1942|gamespy1|port=14567,port_query=23000
|
||||||
bfv|Battlefield Vietnam|gamespy2|port=15567,port_query=23000
|
bfv|Battlefield Vietnam|gamespy2|port=15567,port_query=23000
|
||||||
bf2|Battlefield 2|gamespy3|port=16567,port_query=29900|noChallenge
|
bf2|Battlefield 2|gamespy3|port=16567,port_query=29900
|
||||||
bf2142|Battlefield 2142|gamespy3|port=16567,port_query=29900
|
bf2142|Battlefield 2142|gamespy3|port=16567,port_query=29900
|
||||||
bfbc2|Battlefield: Bad Company 2|battlefield|port=19567,port_query=48888|isBadCompany2
|
bfbc2|Battlefield: Bad Company 2|battlefield|port=19567,port_query=48888
|
||||||
bf3|Battlefield 3|battlefield|port=25200,port_query_offset=22000
|
bf3|Battlefield 3|battlefield|port=25200,port_query_offset=22000
|
||||||
bf4|Battlefield 4|battlefield|port=25200,port_query_offset=22000
|
bf4|Battlefield 4|battlefield|port=25200,port_query_offset=22000
|
||||||
bfh|Battlefield Hardline|battlefield|port=25200,port_query_offset=22000
|
bfh|Battlefield Hardline|battlefield|port=25200,port_query_offset=22000
|
||||||
|
@ -113,7 +113,7 @@ doi|Day of Infamy|valve
|
||||||
doom3|Doom 3|doom3|port=27666
|
doom3|Doom 3|doom3|port=27666
|
||||||
dota2|DOTA 2|valve
|
dota2|DOTA 2|valve
|
||||||
drakan|Drakan|gamespy1|port=27045,port_query_offset=1
|
drakan|Drakan|gamespy1|port=27045,port_query_offset=1
|
||||||
etqw|Enemy Territory Quake Wars|doom3|port=3074,port_query=27733|isEtqw,hasSpaceBeforeClanTag,hasClanTag,hasTypeFlag
|
etqw|Enemy Territory Quake Wars|doom3|port=3074,port_query=27733
|
||||||
fear|F.E.A.R.|gamespy2|port_query=27888
|
fear|F.E.A.R.|gamespy2|port_query=27888
|
||||||
f12002|F1 2002|gamespy1|port_query=3297
|
f12002|F1 2002|gamespy1|port_query=3297
|
||||||
f1c9902|F1 Challenge 99-02|gamespy1|port_query=34397
|
f1c9902|F1 Challenge 99-02|gamespy1|port_query=34397
|
||||||
|
@ -168,9 +168,9 @@ mohbt|Medal of Honor: Breakthrough|gamespy1|port=12203,port_query_offset=97
|
||||||
moh2010|Medal of Honor 2010|battlefield|port=7673,port_query=48888
|
moh2010|Medal of Honor 2010|battlefield|port=7673,port_query=48888
|
||||||
mohwf|Medal of Honor: Warfighter|battlefield|port=25200,port_query_offset=22000
|
mohwf|Medal of Honor: Warfighter|battlefield|port=25200,port_query_offset=22000
|
||||||
|
|
||||||
minecraft|Minecraft|minecraft|port=25565|srvRecord=_minecraft._tcp,doc_notes=minecraft
|
minecraft|Minecraft|minecraft|port=25565|doc_notes=minecraft
|
||||||
# Legacy name
|
# Legacy name
|
||||||
minecraftping||minecraft|port=25565|srvRecord=_minecraft._tcp,doc_notes=minecraft
|
minecraftping||minecraft|port=25565|doc_notes=minecraft
|
||||||
|
|
||||||
minecraftpe|Minecraft: Pocket Edition|gamespy3|port=19132,maxAttempts=2
|
minecraftpe|Minecraft: Pocket Edition|gamespy3|port=19132,maxAttempts=2
|
||||||
mnc|Monday Night Combat|valve|port=7777,port_query=27016
|
mnc|Monday Night Combat|valve|port=7777,port_query=27016
|
||||||
|
@ -198,13 +198,13 @@ openttd|OpenTTD|openttd|port=3979
|
||||||
operationflashpoint|Operation Flashpoint|gamespy1|port=2234,port_query_offset=1
|
operationflashpoint|Operation Flashpoint|gamespy1|port=2234,port_query_offset=1
|
||||||
painkiller|Painkiller|ase|port=3455,port_query_offset=123
|
painkiller|Painkiller|ase|port=3455,port_query_offset=123
|
||||||
postal2|Postal 2|gamespy1|port=7777,port_query_offset=1
|
postal2|Postal 2|gamespy1|port=7777,port_query_offset=1
|
||||||
prey|Prey|doom3|port_query=27719
|
prey|Prey|doom3|port=27719
|
||||||
primalcarnage|Primal Carnage: Extinction|valve|port=7777,port_query=27015
|
primalcarnage|Primal Carnage: Extinction|valve|port=7777,port_query=27015
|
||||||
|
|
||||||
quake1|Quake 1: QuakeWorld|quake1|port=27500
|
quake1|Quake 1: QuakeWorld|quake1|port=27500
|
||||||
quake2|Quake 2|quake2|port=27910
|
quake2|Quake 2|quake2|port=27910
|
||||||
quake3|Quake 3: Arena|quake3|port=27960
|
quake3|Quake 3: Arena|quake3|port=27960
|
||||||
quake4|Quake 4|doom3|port=28004|hasClanTag
|
quake4|Quake 4|doom3|port=28004
|
||||||
|
|
||||||
ragdollkungfu|Rag Doll Kung Fu|valve
|
ragdollkungfu|Rag Doll Kung Fu|valve
|
||||||
|
|
||||||
|
@ -230,7 +230,7 @@ ss2|Serious Sam 2|gamespy2|port=25600
|
||||||
shatteredhorizon|Shattered Horizon|valve
|
shatteredhorizon|Shattered Horizon|valve
|
||||||
ship|The Ship|valve
|
ship|The Ship|valve
|
||||||
shogo|Shogo|gamespy1|port_query=27888
|
shogo|Shogo|gamespy1|port_query=27888
|
||||||
shootmania|Shootmania|nadeo||doc_notes=nadeo-shootmania--trackmania--etc
|
shootmania|Shootmania|nadeo|port=2350,port_query=5000|doc_notes=nadeo-shootmania--trackmania--etc
|
||||||
sin|SiN|gamespy1|port_query=22450
|
sin|SiN|gamespy1|port_query=22450
|
||||||
sinep|SiN Episodes|valve
|
sinep|SiN Episodes|valve
|
||||||
soldat|Soldat|ase|port=13073,port_query_offset=123
|
soldat|Soldat|ase|port=13073,port_query_offset=123
|
||||||
|
@ -266,8 +266,8 @@ thps3|Tony Hawk's Pro Skater 3|gamespy1|port_query=6500
|
||||||
thps4|Tony Hawk's Pro Skater 4|gamespy1|port_query=6500
|
thps4|Tony Hawk's Pro Skater 4|gamespy1|port_query=6500
|
||||||
thu2|Tony Hawk's Underground 2|gamespy1|port_query=5153
|
thu2|Tony Hawk's Underground 2|gamespy1|port_query=5153
|
||||||
towerunite|Tower Unite|valve
|
towerunite|Tower Unite|valve
|
||||||
trackmania2|Trackmania 2|nadeo||doc_notes=nadeo-shootmania--trackmania--etc
|
trackmania2|Trackmania 2|nadeo|port=2350,port_query=5000|doc_notes=nadeo-shootmania--trackmania--etc
|
||||||
trackmaniaforever|Trackmania Forever|nadeo||doc_notes=nadeo-shootmania--trackmania--etc
|
trackmaniaforever|Trackmania Forever|nadeo|port=2350,port_query=5000|doc_notes=nadeo-shootmania--trackmania--etc
|
||||||
tremulous|Tremulous|quake3|port_query=30720
|
tremulous|Tremulous|quake3|port_query=30720
|
||||||
tribes1|Tribes 1: Starsiege|tribes1|port=28001
|
tribes1|Tribes 1: Starsiege|tribes1|port=28001
|
||||||
tribesvengeance|Tribes: Vengeance|gamespy2|port=7777,port_query_offset=1
|
tribesvengeance|Tribes: Vengeance|gamespy2|port=7777,port_query_offset=1
|
||||||
|
@ -289,7 +289,7 @@ vietcong|Vietcong|gamespy1|port=5425,port_query=15425
|
||||||
vietcong2|Vietcong 2|gamespy2|port=5001,port_query=19967
|
vietcong2|Vietcong 2|gamespy2|port=5001,port_query=19967
|
||||||
warsow|Warsow|warsow|port=44400
|
warsow|Warsow|warsow|port=44400
|
||||||
wheeloftime|Wheel of Time|gamespy1|port=7777,port_query_offset=1
|
wheeloftime|Wheel of Time|gamespy1|port=7777,port_query_offset=1
|
||||||
wolfenstein2009|Wolfenstein 2009|doom3|port_query=27666|hasSpaceBeforeClanTag,hasClanTag,hasTypeFlag
|
wolfenstein2009|Wolfenstein 2009|doom3|port=27666
|
||||||
wolfensteinet|Wolfenstein: Enemy Territory|quake3|port_query=27960
|
wolfensteinet|Wolfenstein: Enemy Territory|quake3|port_query=27960
|
||||||
xpandrally|Xpand Rally|ase|port=28015,port_query_offset=123
|
xpandrally|Xpand Rally|ase|port=28015,port_query_offset=123
|
||||||
zombiemaster|Zombie Master|valve
|
zombiemaster|Zombie Master|valve
|
||||||
|
|
84
lib/GameResolver.js
Normal file
84
lib/GameResolver.js
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
const Path = require('path'),
|
||||||
|
fs = require('fs');
|
||||||
|
|
||||||
|
class GameResolver {
|
||||||
|
constructor() {
|
||||||
|
this.games = this._readGames();
|
||||||
|
}
|
||||||
|
|
||||||
|
lookup(type) {
|
||||||
|
if(!type) throw Error('No game specified');
|
||||||
|
|
||||||
|
if(type.substr(0,9) === 'protocol-') {
|
||||||
|
return {
|
||||||
|
protocol: type.substr(9)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const game = this.games.get(type);
|
||||||
|
if(!game) throw Error('Invalid game: '+type);
|
||||||
|
return game.options;
|
||||||
|
}
|
||||||
|
|
||||||
|
printReadme() {
|
||||||
|
let out = '';
|
||||||
|
for(const key of Object.keys(games)) {
|
||||||
|
const game = games[key];
|
||||||
|
if (!game.pretty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out += "* "+game.pretty+" ("+key+")";
|
||||||
|
if(game.options.port_query_offset || game.options.port_query)
|
||||||
|
out += " [[Separate Query Port](#separate-query-port)]";
|
||||||
|
if(game.extra.doc_notes)
|
||||||
|
out += " [[Additional Notes](#"+game.extra.doc_notes+")]";
|
||||||
|
out += "\n";
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
_readGames() {
|
||||||
|
const gamesFile = Path.normalize(__dirname+'/../games.txt');
|
||||||
|
const lines = fs.readFileSync(gamesFile,'utf8').split('\n');
|
||||||
|
const games = new Map();
|
||||||
|
|
||||||
|
for (let line of lines) {
|
||||||
|
// strip comments
|
||||||
|
const comment = line.indexOf('#');
|
||||||
|
if(comment !== -1) line = line.substr(0,comment);
|
||||||
|
line = line.trim();
|
||||||
|
if(!line) continue;
|
||||||
|
|
||||||
|
const split = line.split('|');
|
||||||
|
const gameId = split[0].trim();
|
||||||
|
const options = this._parseList(split[3]);
|
||||||
|
options.protocol = split[2].trim();
|
||||||
|
|
||||||
|
games.set(gameId, {
|
||||||
|
pretty: split[1].trim(),
|
||||||
|
options: options,
|
||||||
|
extra: this._parseList(split[4])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return games;
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseList(str) {
|
||||||
|
if(!str) return {};
|
||||||
|
const out = {};
|
||||||
|
for (const one of str.split(',')) {
|
||||||
|
const equals = one.indexOf('=');
|
||||||
|
const key = equals === -1 ? one : one.substr(0,equals);
|
||||||
|
let value = equals === -1 ? '' : one.substr(equals+1);
|
||||||
|
|
||||||
|
if(value === 'true' || value === '') value = true;
|
||||||
|
else if(value === 'false') value = false;
|
||||||
|
else if(!isNaN(parseInt(value))) value = parseInt(value);
|
||||||
|
|
||||||
|
out[key] = value;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = GameResolver;
|
42
lib/GlobalUdpSocket.js
Normal file
42
lib/GlobalUdpSocket.js
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
const dgram = require('dgram'),
|
||||||
|
HexUtil = require('./HexUtil');
|
||||||
|
|
||||||
|
class GlobalUdpSocket {
|
||||||
|
constructor() {
|
||||||
|
this.socket = null;
|
||||||
|
this.callbacks = new Set();
|
||||||
|
this.debug = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getSocket() {
|
||||||
|
if (!this.socket) {
|
||||||
|
const udpSocket = this.socket = dgram.createSocket('udp4');
|
||||||
|
udpSocket.unref();
|
||||||
|
udpSocket.bind();
|
||||||
|
udpSocket.on('message', (buffer, rinfo) => {
|
||||||
|
for (const cb of this.callbacks) {
|
||||||
|
cb(rinfo.address, rinfo.port, buffer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
udpSocket.on('error', (e) => {
|
||||||
|
if (this.debug) {
|
||||||
|
console.log("UDP ERROR: " + e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
send(buffer, address, port) {
|
||||||
|
this._getSocket().send(buffer,0,buffer.length,port,address);
|
||||||
|
}
|
||||||
|
|
||||||
|
addCallback(callback) {
|
||||||
|
this.callbacks.add(callback);
|
||||||
|
}
|
||||||
|
removeCallback(callback) {
|
||||||
|
this.callbacks.delete(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = GlobalUdpSocket;
|
20
lib/Promises.js
Normal file
20
lib/Promises.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
class Promises {
|
||||||
|
static createTimeout(timeoutMs, timeoutMsg) {
|
||||||
|
let cancel = null;
|
||||||
|
const wrapped = new Promise((res, rej) => {
|
||||||
|
const timeout = setTimeout(
|
||||||
|
() => {
|
||||||
|
rej(new Error(timeoutMsg + " - Timed out after " + timeoutMs + "ms"));
|
||||||
|
},
|
||||||
|
timeoutMs
|
||||||
|
);
|
||||||
|
cancel = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
wrapped.cancel = cancel;
|
||||||
|
return wrapped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Promises;
|
22
lib/ProtocolResolver.js
Normal file
22
lib/ProtocolResolver.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
const Path = require('path'),
|
||||||
|
fs = require('fs'),
|
||||||
|
Core = require('../protocols/core');
|
||||||
|
|
||||||
|
class ProtocolResolver {
|
||||||
|
constructor() {
|
||||||
|
this.protocolDir = Path.normalize(__dirname+'/../protocols');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns Core
|
||||||
|
*/
|
||||||
|
create(protocolId) {
|
||||||
|
protocolId = Path.basename(protocolId);
|
||||||
|
const path = this.protocolDir+'/'+protocolId;
|
||||||
|
if(!fs.existsSync(path+'.js')) throw Error('Protocol definition file missing: '+type);
|
||||||
|
const protocol = require(path);
|
||||||
|
return new protocol();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ProtocolResolver;
|
103
lib/QueryRunner.js
Normal file
103
lib/QueryRunner.js
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
const GameResolver = require('./GameResolver'),
|
||||||
|
ProtocolResolver = require('./ProtocolResolver');
|
||||||
|
|
||||||
|
const defaultOptions = {
|
||||||
|
socketTimeout: 2000,
|
||||||
|
attemptTimeout: 10000,
|
||||||
|
maxAttempts: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
class QueryRunner {
|
||||||
|
constructor(udpSocket, debug) {
|
||||||
|
this.debug = debug;
|
||||||
|
this.udpSocket = udpSocket;
|
||||||
|
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
|
||||||
|
});
|
||||||
|
} 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 (this.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;
|
89
lib/index.js
89
lib/index.js
|
@ -1,87 +1,48 @@
|
||||||
const dgram = require('dgram'),
|
const QueryRunner = require('./QueryRunner'),
|
||||||
TypeResolver = require('./typeresolver'),
|
GlobalUdpSocket = require('./GlobalUdpSocket');
|
||||||
HexUtil = require('./HexUtil');
|
|
||||||
|
|
||||||
const activeQueries = new Set();
|
let singleton = null;
|
||||||
|
|
||||||
const udpSocket = dgram.createSocket('udp4');
|
|
||||||
udpSocket.unref();
|
|
||||||
udpSocket.bind();
|
|
||||||
udpSocket.on('message', (buffer, rinfo) => {
|
|
||||||
if(Gamedig.debug) {
|
|
||||||
console.log(rinfo.address+':'+rinfo.port+" <--UDP");
|
|
||||||
console.log(HexUtil.debugDump(buffer));
|
|
||||||
}
|
|
||||||
for(const query of activeQueries) {
|
|
||||||
if(query.options.address !== rinfo.address) continue;
|
|
||||||
if(query.options.port_query !== rinfo.port) continue;
|
|
||||||
query._udpIncoming(buffer);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
udpSocket.on('error', (e) => {
|
|
||||||
if(Gamedig.debug) console.log("UDP ERROR: "+e);
|
|
||||||
});
|
|
||||||
|
|
||||||
class Gamedig {
|
class Gamedig {
|
||||||
|
constructor() {
|
||||||
static query(options,callback) {
|
this.udpSocket = new GlobalUdpSocket();
|
||||||
const promise = (async () => {
|
this.queryRunner = new QueryRunner(this.udpSocket);
|
||||||
for (const key of Object.keys(options)) {
|
this._debug = false;
|
||||||
if (['port_query', 'port'].includes(key)) {
|
|
||||||
options[key] = parseInt(options[key]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let query = TypeResolver.lookup(options.type);
|
setDebug(on) {
|
||||||
query.debug = Gamedig.debug;
|
this.udpSocket.debug = on;
|
||||||
query.udpSocket = udpSocket;
|
this._debug = on;
|
||||||
query.type = options.type;
|
this.queryRunner.debug = on;
|
||||||
|
|
||||||
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
|
async query(userOptions) {
|
||||||
for(const key of Object.keys(options)) {
|
userOptions.debug |= this._debug;
|
||||||
query.options[key] = options[key];
|
return await this.queryRunner.run(userOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
activeQueries.add(query);
|
static getInstance() {
|
||||||
try {
|
if (!singleton) {
|
||||||
return await query.runAll();
|
singleton = new Gamedig();
|
||||||
} finally {
|
|
||||||
activeQueries.delete(query);
|
|
||||||
}
|
}
|
||||||
})();
|
return singleton;
|
||||||
|
}
|
||||||
|
static query(userOptions, callback) {
|
||||||
|
const promise = Gamedig.getInstance().query(userOptions);
|
||||||
if (callback && callback instanceof Function) {
|
if (callback && callback instanceof Function) {
|
||||||
if(callback.length === 2) {
|
if (callback.length === 2) {
|
||||||
promise
|
promise
|
||||||
.then((state) => callback(null,state))
|
.then((state) => callback(null, state))
|
||||||
.catch((error) => callback(error));
|
.catch((error) => callback(error));
|
||||||
} else if (callback.length === 1) {
|
} else if (callback.length === 1) {
|
||||||
promise
|
promise
|
||||||
.then((state) => callback(state))
|
.then((state) => callback(state))
|
||||||
.catch((error) => callback({error:error}));
|
.catch((error) => callback({error: error}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Gamedig.debug = false;
|
Object.defineProperty(Gamedig, "debug", { set: on => Gamedig.getInstance().setDebug(on) });
|
||||||
Gamedig.isCommandLine = false;
|
|
||||||
|
|
||||||
module.exports = Gamedig;
|
module.exports = Gamedig;
|
||||||
|
|
|
@ -1,102 +0,0 @@
|
||||||
const Path = require('path'),
|
|
||||||
fs = require('fs'),
|
|
||||||
Core = require('../protocols/core');
|
|
||||||
|
|
||||||
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 {
|
|
||||||
/**
|
|
||||||
* @param {string} type
|
|
||||||
* @returns Core
|
|
||||||
*/
|
|
||||||
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,25 +0,0 @@
|
||||||
const Gamespy2 = require('./gamespy2');
|
|
||||||
|
|
||||||
class AmericasArmy extends Gamespy2 {
|
|
||||||
async run(state) {
|
|
||||||
await super.run(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;
|
|
|
@ -15,7 +15,7 @@ class Armagetron extends Core {
|
||||||
|
|
||||||
reader.skip(6);
|
reader.skip(6);
|
||||||
|
|
||||||
state.raw.port = this.readUInt(reader);
|
state.gamePort = this.readUInt(reader);
|
||||||
state.raw.hostname = this.readString(reader);
|
state.raw.hostname = this.readString(reader);
|
||||||
state.name = this.stripColorCodes(this.readString(reader));
|
state.name = this.stripColorCodes(this.readString(reader));
|
||||||
state.raw.numplayers = this.readUInt(reader);
|
state.raw.numplayers = this.readUInt(reader);
|
||||||
|
|
|
@ -5,12 +5,12 @@ class Ase extends Core {
|
||||||
const buffer = await this.udpSend('s',(buffer) => {
|
const buffer = await this.udpSend('s',(buffer) => {
|
||||||
const reader = this.reader(buffer);
|
const reader = this.reader(buffer);
|
||||||
const header = reader.string({length: 4});
|
const header = reader.string({length: 4});
|
||||||
if (header === 'EYE1') return buffer;
|
if (header === 'EYE1') return reader.rest();
|
||||||
});
|
});
|
||||||
|
|
||||||
const reader = this.reader(buffer);
|
const reader = this.reader(buffer);
|
||||||
state.raw.gamename = this.readString(reader);
|
state.raw.gamename = this.readString(reader);
|
||||||
state.raw.port = parseInt(this.readString(reader));
|
state.gamePort = parseInt(this.readString(reader));
|
||||||
state.name = this.readString(reader);
|
state.name = this.readString(reader);
|
||||||
state.raw.gametype = this.readString(reader);
|
state.raw.gametype = this.readString(reader);
|
||||||
state.map = this.readString(reader);
|
state.map = this.readString(reader);
|
||||||
|
|
|
@ -4,14 +4,13 @@ class Battlefield extends Core {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.encoding = 'latin1';
|
this.encoding = 'latin1';
|
||||||
this.isBadCompany2 = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(state) {
|
async run(state) {
|
||||||
await this.withTcp(async socket => {
|
await this.withTcp(async socket => {
|
||||||
{
|
{
|
||||||
const data = await this.query(socket, ['serverInfo']);
|
const data = await this.query(socket, ['serverInfo']);
|
||||||
state.raw.name = data.shift();
|
state.name = data.shift();
|
||||||
state.raw.numplayers = parseInt(data.shift());
|
state.raw.numplayers = parseInt(data.shift());
|
||||||
state.maxplayers = parseInt(data.shift());
|
state.maxplayers = parseInt(data.shift());
|
||||||
state.raw.gametype = data.shift();
|
state.raw.gametype = data.shift();
|
||||||
|
@ -29,25 +28,39 @@ class Battlefield extends Core {
|
||||||
}
|
}
|
||||||
|
|
||||||
state.raw.targetscore = parseInt(data.shift());
|
state.raw.targetscore = parseInt(data.shift());
|
||||||
data.shift();
|
state.raw.status = data.shift();
|
||||||
state.raw.ranked = (data.shift() === 'true');
|
|
||||||
state.raw.punkbuster = (data.shift() === 'true');
|
// Seems like the fields end at random places beyond this point
|
||||||
state.password = (data.shift() === 'true');
|
// depending on the server version
|
||||||
state.raw.uptime = parseInt(data.shift());
|
|
||||||
state.raw.roundtime = parseInt(data.shift());
|
if (data.length) state.raw.ranked = (data.shift() === 'true');
|
||||||
if (this.isBadCompany2) {
|
if (data.length) state.raw.punkbuster = (data.shift() === 'true');
|
||||||
data.shift();
|
if (data.length) state.password = (data.shift() === 'true');
|
||||||
data.shift();
|
if (data.length) state.raw.uptime = parseInt(data.shift());
|
||||||
|
if (data.length) state.raw.roundtime = parseInt(data.shift());
|
||||||
|
|
||||||
|
const isBadCompany2 = data[0] === 'BC2';
|
||||||
|
if (isBadCompany2) {
|
||||||
|
if (data.length) data.shift();
|
||||||
|
if (data.length) data.shift();
|
||||||
}
|
}
|
||||||
|
if (data.length) {
|
||||||
state.raw.ip = data.shift();
|
state.raw.ip = data.shift();
|
||||||
state.raw.punkbusterversion = data.shift();
|
const split = state.raw.ip.split(':');
|
||||||
state.raw.joinqueue = (data.shift() === 'true');
|
state.gameHost = split[0];
|
||||||
state.raw.region = data.shift();
|
state.gamePort = split[1];
|
||||||
if (!this.isBadCompany2) {
|
} else {
|
||||||
state.raw.pingsite = data.shift();
|
// best guess if the server doesn't tell us what the server port is
|
||||||
state.raw.country = data.shift();
|
// these are just the default game ports for different default query ports
|
||||||
state.raw.quickmatch = (data.shift() === 'true');
|
if (this.options.port === 48888) state.gamePort = 7673;
|
||||||
|
if (this.options.port === 22000) state.gamePort = 25200;
|
||||||
}
|
}
|
||||||
|
if (data.length) state.raw.punkbusterversion = data.shift();
|
||||||
|
if (data.length) state.raw.joinqueue = (data.shift() === 'true');
|
||||||
|
if (data.length) state.raw.region = data.shift();
|
||||||
|
if (data.length) state.raw.pingsite = data.shift();
|
||||||
|
if (data.length) state.raw.country = data.shift();
|
||||||
|
if (data.length) state.raw.quickmatch = (data.shift() === 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -135,6 +148,7 @@ class Battlefield extends Core {
|
||||||
const header = reader.uint(4);
|
const header = reader.uint(4);
|
||||||
const totalLength = reader.uint(4);
|
const totalLength = reader.uint(4);
|
||||||
if(buffer.length < totalLength) return false;
|
if(buffer.length < totalLength) return false;
|
||||||
|
this.debugLog("Expected " + totalLength + " bytes, have " + buffer.length);
|
||||||
|
|
||||||
const paramCount = reader.uint(4);
|
const paramCount = reader.uint(4);
|
||||||
const params = [];
|
const params = [];
|
||||||
|
|
|
@ -4,7 +4,7 @@ const Core = require('./core'),
|
||||||
class BuildAndShoot extends Core {
|
class BuildAndShoot extends Core {
|
||||||
async run(state) {
|
async run(state) {
|
||||||
const body = await this.request({
|
const body = await this.request({
|
||||||
uri: 'http://'+this.options.address+':'+this.options.port_query+'/',
|
uri: 'http://'+this.options.address+':'+this.options.port+'/',
|
||||||
});
|
});
|
||||||
|
|
||||||
let m;
|
let m;
|
||||||
|
|
|
@ -6,43 +6,24 @@ const EventEmitter = require('events').EventEmitter,
|
||||||
util = require('util'),
|
util = require('util'),
|
||||||
dnsLookupAsync = util.promisify(dns.lookup),
|
dnsLookupAsync = util.promisify(dns.lookup),
|
||||||
dnsResolveAsync = util.promisify(dns.resolve),
|
dnsResolveAsync = util.promisify(dns.resolve),
|
||||||
requestAsync = require('request-promise');
|
requestAsync = require('request-promise'),
|
||||||
|
Promises = require('../lib/Promises');
|
||||||
|
|
||||||
class Core extends EventEmitter {
|
class Core extends EventEmitter {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.options = {
|
|
||||||
socketTimeout: 2000,
|
|
||||||
attemptTimeout: 10000,
|
|
||||||
maxAttempts: 1
|
|
||||||
};
|
|
||||||
this.encoding = 'utf8';
|
this.encoding = 'utf8';
|
||||||
this.byteorder = 'le';
|
this.byteorder = 'le';
|
||||||
this.delimiter = '\0';
|
this.delimiter = '\0';
|
||||||
this.srvRecord = null;
|
this.srvRecord = null;
|
||||||
|
this.abortedPromise = null;
|
||||||
|
|
||||||
this.asyncLeaks = new Set();
|
// Sent to us by QueryRunner
|
||||||
this.udpCallback = null;
|
this.options = null;
|
||||||
this.udpLocked = false;
|
this.udpSocket = null;
|
||||||
this.lastAsyncLeakId = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initState() {
|
async runAllAttempts() {
|
||||||
return {
|
|
||||||
name: '',
|
|
||||||
map: '',
|
|
||||||
password: false,
|
|
||||||
|
|
||||||
raw: {},
|
|
||||||
|
|
||||||
maxplayers: 0,
|
|
||||||
players: [],
|
|
||||||
bots: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run all attempts
|
|
||||||
async runAll() {
|
|
||||||
let result = null;
|
let result = null;
|
||||||
let lastError = null;
|
let lastError = null;
|
||||||
for (let attempt = 1; attempt <= this.options.maxAttempts; attempt++) {
|
for (let attempt = 1; attempt <= this.options.maxAttempts; attempt++) {
|
||||||
|
@ -63,38 +44,27 @@ class Core extends EventEmitter {
|
||||||
|
|
||||||
// Runs a single attempt with a timeout and cleans up afterward
|
// Runs a single attempt with a timeout and cleans up afterward
|
||||||
async runOnceSafe() {
|
async runOnceSafe() {
|
||||||
try {
|
let abortCall = null;
|
||||||
const result = await this.timedPromise(this.runOnce(), this.options.attemptTimeout, "Attempt");
|
this.abortedPromise = new Promise((resolve,reject) => {
|
||||||
if (this.asyncLeaks.size) {
|
abortCall = () => reject("Query is finished -- cancelling outstanding promises");
|
||||||
let out = [];
|
|
||||||
for (const leak of this.asyncLeaks) {
|
|
||||||
out.push(leak.id + " " + leak.stack);
|
|
||||||
}
|
|
||||||
throw new Error('Query succeeded, but async leak was detected:\n' + out.join('\n---\n'));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} finally {
|
|
||||||
// Clean up any lingering long-running functions
|
|
||||||
for (const leak of this.asyncLeaks) {
|
|
||||||
try {
|
|
||||||
leak.cleanup();
|
|
||||||
} catch(e) {
|
|
||||||
this.debugLog("Error during async cleanup: " + e.stack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.asyncLeaks.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
timedPromise(promise, timeoutMs, timeoutMsg) {
|
|
||||||
return new Promise((resolve,reject) => {
|
|
||||||
const cancelTimeout = this.setTimeout(
|
|
||||||
() => reject(new Error(timeoutMsg + " - Timed out after " + timeoutMs + "ms")),
|
|
||||||
timeoutMs
|
|
||||||
);
|
|
||||||
promise = promise.finally(cancelTimeout);
|
|
||||||
promise.then(resolve,reject);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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() {
|
async runOnce() {
|
||||||
|
@ -103,25 +73,37 @@ class Core extends EventEmitter {
|
||||||
if (('host' in options) && !('address' in options)) {
|
if (('host' in options) && !('address' in options)) {
|
||||||
options.address = await this.parseDns(options.host);
|
options.address = await this.parseDns(options.host);
|
||||||
}
|
}
|
||||||
if(!('port_query' in options) && 'port' in options) {
|
|
||||||
const offset = options.port_query_offset || 0;
|
|
||||||
options.port_query = options.port + offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = this.initState();
|
const state = {
|
||||||
|
name: '',
|
||||||
|
map: '',
|
||||||
|
password: false,
|
||||||
|
|
||||||
|
raw: {},
|
||||||
|
|
||||||
|
maxplayers: 0,
|
||||||
|
players: [],
|
||||||
|
bots: []
|
||||||
|
};
|
||||||
|
|
||||||
await this.run(state);
|
await this.run(state);
|
||||||
|
|
||||||
if (this.options.notes)
|
if (this.options.notes)
|
||||||
state.notes = this.options.notes;
|
state.notes = this.options.notes;
|
||||||
|
|
||||||
state.query = {};
|
// because lots of servers prefix with spaces to try to appear first
|
||||||
if ('host' in this.options) state.query.host = this.options.host;
|
state.name = state.name.trim();
|
||||||
if ('address' in this.options) state.query.address = this.options.address;
|
|
||||||
if ('port' in this.options) state.query.port = this.options.port;
|
state.duration = Date.now() - startMillis;
|
||||||
if ('port_query' in this.options) state.query.port_query = this.options.port_query;
|
if (!('connect' in state)) {
|
||||||
state.query.type = this.type;
|
state.connect = ''
|
||||||
if ('pretty' in this) state.query.pretty = this.pretty;
|
+ (state.gameHost || this.options.host || this.options.address)
|
||||||
state.query.duration = Date.now() - startMillis;
|
+ ':'
|
||||||
|
+ (state.gamePort || this.options.port)
|
||||||
|
}
|
||||||
|
delete state.gameHost;
|
||||||
|
delete state.gamePort;
|
||||||
|
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -166,18 +148,6 @@ class Core extends EventEmitter {
|
||||||
else return await resolveStandard(host);
|
else return await resolveStandard(host);
|
||||||
}
|
}
|
||||||
|
|
||||||
addAsyncLeak(fn) {
|
|
||||||
const id = ++this.lastAsyncLeakId;
|
|
||||||
const stack = new Error().stack;
|
|
||||||
const entry = { id: id, cleanup: fn, stack: stack };
|
|
||||||
this.debugLog("Registering async leak: " + id);
|
|
||||||
this.asyncLeaks.add(entry);
|
|
||||||
return () => {
|
|
||||||
this.debugLog("Removing async leak: " + id);
|
|
||||||
this.asyncLeaks.delete(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// utils
|
// utils
|
||||||
/** @returns {Reader} */
|
/** @returns {Reader} */
|
||||||
reader(buffer) {
|
reader(buffer) {
|
||||||
|
@ -204,6 +174,12 @@ class Core extends EventEmitter {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assertValidPort(port) {
|
||||||
|
if (!port || port < 1 || port > 65535) {
|
||||||
|
throw new Error("Invalid tcp/ip port: " + port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @template T
|
* @template T
|
||||||
* @param {function(Socket):Promise<T>} fn
|
* @param {function(Socket):Promise<T>} fn
|
||||||
|
@ -211,11 +187,13 @@ class Core extends EventEmitter {
|
||||||
*/
|
*/
|
||||||
async withTcp(fn) {
|
async withTcp(fn) {
|
||||||
const address = this.options.address;
|
const address = this.options.address;
|
||||||
const port = this.options.port_query;
|
const port = this.options.port;
|
||||||
|
this.assertValidPort(port);
|
||||||
|
|
||||||
const socket = net.connect(port,address);
|
let socket, connectionTimeout;
|
||||||
|
try {
|
||||||
|
socket = net.connect(port,address);
|
||||||
socket.setNoDelay(true);
|
socket.setNoDelay(true);
|
||||||
const cancelAsyncLeak = this.addAsyncLeak(() => socket.destroy());
|
|
||||||
|
|
||||||
this.debugLog(log => {
|
this.debugLog(log => {
|
||||||
this.debugLog(address+':'+port+" TCP Connecting");
|
this.debugLog(address+':'+port+" TCP Connecting");
|
||||||
|
@ -234,33 +212,20 @@ class Core extends EventEmitter {
|
||||||
socket.on('ready', () => log(address+':'+port+" TCP Connected"));
|
socket.on('ready', () => log(address+':'+port+" TCP Connected"));
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
const connectionPromise = new Promise((resolve,reject) => {
|
||||||
await this.timedPromise(
|
|
||||||
new Promise((resolve,reject) => {
|
|
||||||
socket.on('ready', resolve);
|
socket.on('ready', resolve);
|
||||||
socket.on('close', () => reject(new Error('TCP Connection Refused')));
|
socket.on('close', () => reject(new Error('TCP Connection Refused')));
|
||||||
}),
|
});
|
||||||
this.options.socketTimeout,
|
connectionTimeout = Promises.createTimeout(this.options.socketTimeout, 'TCP Opening');
|
||||||
'TCP Opening'
|
await Promise.race([
|
||||||
);
|
connectionPromise,
|
||||||
|
connectionTimeout,
|
||||||
|
this.abortedPromise
|
||||||
|
]);
|
||||||
return await fn(socket);
|
return await fn(socket);
|
||||||
} finally {
|
} finally {
|
||||||
cancelAsyncLeak();
|
socket && socket.destroy();
|
||||||
socket.destroy();
|
connectionTimeout && connectionTimeout.cancel();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(callback, time) {
|
|
||||||
let cancelAsyncLeak;
|
|
||||||
const onTimeout = () => {
|
|
||||||
cancelAsyncLeak();
|
|
||||||
callback();
|
|
||||||
};
|
|
||||||
const timeout = setTimeout(onTimeout, time);
|
|
||||||
cancelAsyncLeak = this.addAsyncLeak(() => clearTimeout(timeout));
|
|
||||||
return () => {
|
|
||||||
cancelAsyncLeak();
|
|
||||||
clearTimeout(timeout);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -272,8 +237,9 @@ class Core extends EventEmitter {
|
||||||
* @returns Promise<T>
|
* @returns Promise<T>
|
||||||
*/
|
*/
|
||||||
async tcpSend(socket,buffer,ondata) {
|
async tcpSend(socket,buffer,ondata) {
|
||||||
return await this.timedPromise(
|
let timeout;
|
||||||
new Promise(async (resolve,reject) => {
|
try {
|
||||||
|
const promise = new Promise(async (resolve, reject) => {
|
||||||
let received = Buffer.from([]);
|
let received = Buffer.from([]);
|
||||||
const onData = (data) => {
|
const onData = (data) => {
|
||||||
received = Buffer.concat([received, data]);
|
received = Buffer.concat([received, data]);
|
||||||
|
@ -285,22 +251,11 @@ class Core extends EventEmitter {
|
||||||
};
|
};
|
||||||
socket.on('data', onData);
|
socket.on('data', onData);
|
||||||
socket.write(buffer);
|
socket.write(buffer);
|
||||||
}),
|
});
|
||||||
this.options.socketTimeout,
|
timeout = Promises.createTimeout(this.options.socketTimeout, 'TCP');
|
||||||
'TCP'
|
return await Promise.race([promise, timeout, this.abortedPromise]);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async withUdpLock(fn) {
|
|
||||||
if (this.udpLocked) {
|
|
||||||
throw new Error('Attempted to lock UDP when already locked');
|
|
||||||
}
|
|
||||||
this.udpLocked = true;
|
|
||||||
try {
|
|
||||||
return await fn();
|
|
||||||
} finally {
|
} finally {
|
||||||
this.udpLocked = false;
|
timeout && timeout.cancel();
|
||||||
this.udpCallback = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -312,72 +267,93 @@ class Core extends EventEmitter {
|
||||||
* @template T
|
* @template T
|
||||||
*/
|
*/
|
||||||
async udpSend(buffer,onPacket,onTimeout) {
|
async udpSend(buffer,onPacket,onTimeout) {
|
||||||
if(!('port_query' in this.options)) throw new Error('Attempted to send without setting a port');
|
const address = this.options.address;
|
||||||
if(!('address' in this.options)) throw new Error('Attempted to send without setting an address');
|
const port = this.options.port;
|
||||||
|
this.assertValidPort(port);
|
||||||
|
|
||||||
if(typeof buffer === 'string') buffer = Buffer.from(buffer,'binary');
|
if(typeof buffer === 'string') buffer = Buffer.from(buffer,'binary');
|
||||||
this.debugLog(log => {
|
this.debugLog(log => {
|
||||||
log(this.options.address+':'+this.options.port_query+" UDP-->");
|
log(address+':'+port+" UDP-->");
|
||||||
log(HexUtil.debugDump(buffer));
|
log(HexUtil.debugDump(buffer));
|
||||||
});
|
});
|
||||||
|
|
||||||
return await this.withUdpLock(async() => {
|
const socket = this.udpSocket;
|
||||||
this.udpSocket.send(buffer,0,buffer.length,this.options.port_query,this.options.address);
|
socket.send(buffer, address, port);
|
||||||
|
|
||||||
return await new Promise((resolve,reject) => {
|
let socketCallback;
|
||||||
const cancelTimeout = this.setTimeout(() => {
|
let timeout;
|
||||||
|
try {
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
socketCallback = (fromAddress, fromPort, buffer) => {
|
||||||
|
try {
|
||||||
|
if (fromAddress !== address) return;
|
||||||
|
if (fromPort !== port) return;
|
||||||
|
this.debugLog(log => {
|
||||||
|
log(fromAddress + ':' + fromPort + " <--UDP");
|
||||||
|
log(HexUtil.debugDump(buffer));
|
||||||
|
});
|
||||||
|
const result = onPacket(buffer);
|
||||||
|
if (result !== undefined) {
|
||||||
|
this.debugLog("UDP send finished by callback");
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
socket.addCallback(socketCallback);
|
||||||
|
});
|
||||||
|
timeout = Promises.createTimeout(this.options.socketTimeout, 'UDP');
|
||||||
|
const wrappedTimeout = new Promise((resolve, reject) => {
|
||||||
|
timeout.catch((e) => {
|
||||||
this.debugLog("UDP timeout detected");
|
this.debugLog("UDP timeout detected");
|
||||||
let success = false;
|
let success = false;
|
||||||
if (onTimeout) {
|
if (onTimeout) {
|
||||||
|
try {
|
||||||
const result = onTimeout();
|
const result = onTimeout();
|
||||||
if (result !== undefined) {
|
if (result !== undefined) {
|
||||||
this.debugLog("UDP timeout resolved by callback");
|
this.debugLog("UDP timeout resolved by callback");
|
||||||
resolve(result);
|
resolve(result);
|
||||||
success = true;
|
return;
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
reject(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!success) {
|
reject(e);
|
||||||
reject(new Error('UDP Watchdog Timeout'));
|
|
||||||
}
|
|
||||||
},this.options.socketTimeout);
|
|
||||||
|
|
||||||
this.udpCallback = (buffer) => {
|
|
||||||
const result = onPacket(buffer);
|
|
||||||
if(result !== undefined) {
|
|
||||||
this.debugLog("UDP send finished by callback");
|
|
||||||
cancelTimeout();
|
|
||||||
resolve(result);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
return await Promise.race([promise, wrappedTimeout, this.abortedPromise]);
|
||||||
|
} finally {
|
||||||
|
timeout && timeout.cancel();
|
||||||
|
socketCallback && socket.removeCallback(socketCallback);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_udpIncoming(buffer) {
|
async request(params) {
|
||||||
this.udpCallback && this.udpCallback(buffer);
|
let requestPromise;
|
||||||
}
|
try {
|
||||||
|
requestPromise = requestAsync({
|
||||||
request(params) {
|
|
||||||
let promise = requestAsync({
|
|
||||||
...params,
|
...params,
|
||||||
timeout: this.options.socketTimeout,
|
timeout: this.options.socketTimeout,
|
||||||
resolveWithFullResponse: true
|
resolveWithFullResponse: true
|
||||||
});
|
});
|
||||||
const cancelAsyncLeak = this.addAsyncLeak(() => {
|
|
||||||
promise.cancel();
|
|
||||||
});
|
|
||||||
this.debugLog(log => {
|
this.debugLog(log => {
|
||||||
log(() => params.uri+" HTTP-->");
|
log(() => params.uri + " HTTP-->");
|
||||||
promise
|
requestPromise
|
||||||
.then((response) => log(params.uri+" <--HTTP " + response.statusCode))
|
.then((response) => log(params.uri + " <--HTTP " + response.statusCode))
|
||||||
.catch(()=>{});
|
.catch(() => {
|
||||||
});
|
});
|
||||||
promise = promise.finally(cancelAsyncLeak);
|
});
|
||||||
promise = promise.then(response => response.body);
|
const wrappedPromise = promise.then(response => response.body);
|
||||||
return promise;
|
return await Promise.race([wrappedPromise, this.abortedPromise]);
|
||||||
|
} finally {
|
||||||
|
requestPromise && requestPromise.cancel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debugLog(...args) {
|
debugLog(...args) {
|
||||||
if (!this.debug) return;
|
if (!this.options.debug) return;
|
||||||
try {
|
try {
|
||||||
if(args[0] instanceof Buffer) {
|
if(args[0] instanceof Buffer) {
|
||||||
this.debugLog(HexUtil.debugDump(args[0]));
|
this.debugLog(HexUtil.debugDump(args[0]));
|
||||||
|
|
|
@ -3,7 +3,6 @@ const Core = require('./core');
|
||||||
class Doom3 extends Core {
|
class Doom3 extends Core {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.pretty = 'Doom 3';
|
|
||||||
this.encoding = 'latin1';
|
this.encoding = 'latin1';
|
||||||
this.isEtqw = false;
|
this.isEtqw = false;
|
||||||
this.hasSpaceBeforeClanTag = false;
|
this.hasSpaceBeforeClanTag = false;
|
||||||
|
@ -11,26 +10,33 @@ class Doom3 extends Core {
|
||||||
this.hasTypeFlag = false;
|
this.hasTypeFlag = false;
|
||||||
}
|
}
|
||||||
async run(state) {
|
async run(state) {
|
||||||
const body = await this.udpSend('\xff\xffgetInfo\x00PiNGPoNG\x00', packet => {
|
const body = await this.udpSend('\xff\xffgetInfo\x00PiNGPoNg\x00', packet => {
|
||||||
const reader = this.reader(packet);
|
const reader = this.reader(packet);
|
||||||
const header = reader.uint(2);
|
const header = reader.uint(2);
|
||||||
if(header !== 0xffff) return;
|
if(header !== 0xffff) return;
|
||||||
const header2 = reader.string();
|
const header2 = reader.string();
|
||||||
if(header2 !== 'infoResponse') return;
|
if(header2 !== 'infoResponse') return;
|
||||||
|
const challengePart1 = reader.string({length:4});
|
||||||
|
if (challengePart1 !== "PiNG") return;
|
||||||
|
// some doom3 implementations only return the first 4 bytes of the challenge
|
||||||
|
const challengePart2 = reader.string({length:4});
|
||||||
|
if (challengePart2 !== 'PoNg') reader.skip(-4);
|
||||||
return reader.rest();
|
return reader.rest();
|
||||||
});
|
});
|
||||||
|
|
||||||
const reader = this.reader(body);
|
let reader = this.reader(body);
|
||||||
if(this.isEtqw) {
|
|
||||||
const taskId = reader.uint(4);
|
|
||||||
}
|
|
||||||
|
|
||||||
const challenge = reader.uint(4);
|
|
||||||
const protoVersion = reader.uint(4);
|
const protoVersion = reader.uint(4);
|
||||||
state.raw.protocolVersion = (protoVersion>>16)+'.'+(protoVersion&0xffff);
|
state.raw.protocolVersion = (protoVersion>>16)+'.'+(protoVersion&0xffff);
|
||||||
|
|
||||||
if(this.isEtqw) {
|
// some doom implementations send us a packet size here, some don't (etqw does this)
|
||||||
|
// we can tell if this is a packet size, because the third and fourth byte will be 0 (no packets are that massive)
|
||||||
|
reader.skip(2);
|
||||||
|
const packetContainsSize = (reader.uint(2) === 0);
|
||||||
|
reader.skip(-4);
|
||||||
|
|
||||||
|
if (packetContainsSize) {
|
||||||
const size = reader.uint(4);
|
const size = reader.uint(4);
|
||||||
|
this.debugLog("Received packet size: " + size);
|
||||||
}
|
}
|
||||||
|
|
||||||
while(!reader.done()) {
|
while(!reader.done()) {
|
||||||
|
@ -42,23 +48,22 @@ class Doom3 extends Core {
|
||||||
}
|
}
|
||||||
if(!key) break;
|
if(!key) break;
|
||||||
state.raw[key] = value;
|
state.raw[key] = value;
|
||||||
|
this.debugLog(key + "=" + value);
|
||||||
}
|
}
|
||||||
|
|
||||||
let i = 0;
|
const isEtqw = state.raw.gamename && state.raw.gamename.toLowerCase().includes('etqw');
|
||||||
while(!reader.done()) {
|
|
||||||
i++;
|
|
||||||
const player = {};
|
|
||||||
player.id = reader.uint(1);
|
|
||||||
if(player.id === 32) break;
|
|
||||||
player.ping = reader.uint(2);
|
|
||||||
if(!this.isEtqw) player.rate = reader.uint(4);
|
|
||||||
player.name = this.stripColors(reader.string());
|
|
||||||
if(this.hasClanTag) {
|
|
||||||
if(this.hasSpaceBeforeClanTag) reader.uint(1);
|
|
||||||
player.clantag = this.stripColors(reader.string());
|
|
||||||
}
|
|
||||||
if(this.hasTypeFlag) player.typeflag = reader.uint(1);
|
|
||||||
|
|
||||||
|
const rest = reader.rest();
|
||||||
|
let playerResult = this.attemptPlayerParse(rest, isEtqw, false, false, false);
|
||||||
|
if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, false, false);
|
||||||
|
if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, true, true);
|
||||||
|
if (!playerResult) {
|
||||||
|
throw new Error("Unable to find a suitable parse strategy for player list");
|
||||||
|
}
|
||||||
|
let players;
|
||||||
|
[players,reader] = playerResult;
|
||||||
|
|
||||||
|
for (const player of players) {
|
||||||
if(!player.ping || player.typeflag)
|
if(!player.ping || player.typeflag)
|
||||||
state.bots.push(player);
|
state.bots.push(player);
|
||||||
else
|
else
|
||||||
|
@ -66,7 +71,7 @@ class Doom3 extends Core {
|
||||||
}
|
}
|
||||||
|
|
||||||
state.raw.osmask = reader.uint(4);
|
state.raw.osmask = reader.uint(4);
|
||||||
if(this.isEtqw) {
|
if(isEtqw) {
|
||||||
state.raw.ranked = reader.uint(1);
|
state.raw.ranked = reader.uint(1);
|
||||||
state.raw.timeleft = reader.uint(4);
|
state.raw.timeleft = reader.uint(4);
|
||||||
state.raw.gamestate = reader.uint(1);
|
state.raw.gamestate = reader.uint(1);
|
||||||
|
@ -84,6 +89,59 @@ class Doom3 extends Core {
|
||||||
if(state.raw.si_map) state.map = state.raw.si_map;
|
if(state.raw.si_map) state.map = state.raw.si_map;
|
||||||
if(state.raw.si_maxplayers) state.maxplayers = parseInt(state.raw.si_maxplayers);
|
if(state.raw.si_maxplayers) state.maxplayers = parseInt(state.raw.si_maxplayers);
|
||||||
if(state.raw.si_usepass === '1') state.password = true;
|
if(state.raw.si_usepass === '1') state.password = true;
|
||||||
|
if (this.options.port === 27733) state.gamePort = 3074; // etqw has a different query and game port
|
||||||
|
}
|
||||||
|
|
||||||
|
attemptPlayerParse(rest, isEtqw, hasClanTag, hasClanTagPos, hasTypeFlag) {
|
||||||
|
this.debugLog("starting player parse attempt:");
|
||||||
|
this.debugLog("isEtqw: " + isEtqw);
|
||||||
|
this.debugLog("hasClanTag: " + hasClanTag);
|
||||||
|
this.debugLog("hasClanTagPos: " + hasClanTagPos);
|
||||||
|
this.debugLog("hasTypeFlag: " + hasTypeFlag);
|
||||||
|
const reader = this.reader(rest);
|
||||||
|
let lastId = -1;
|
||||||
|
const players = [];
|
||||||
|
while(true) {
|
||||||
|
this.debugLog("---");
|
||||||
|
if (reader.done()) {
|
||||||
|
this.debugLog("* aborting attempt, overran buffer *");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const player = {};
|
||||||
|
player.id = reader.uint(1);
|
||||||
|
this.debugLog("id: " + player.id);
|
||||||
|
if (player.id <= lastId || player.id > 0x20) {
|
||||||
|
this.debugLog("* aborting attempt, invalid player id *");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
lastId = player.id;
|
||||||
|
if(player.id === 0x20) {
|
||||||
|
this.debugLog("* player parse successful *");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
player.ping = reader.uint(2);
|
||||||
|
this.debugLog("ping: " + player.ping);
|
||||||
|
if(!isEtqw) {
|
||||||
|
player.rate = reader.uint(4);
|
||||||
|
this.debugLog("rate: " + player.rate);
|
||||||
|
}
|
||||||
|
player.name = this.stripColors(reader.string());
|
||||||
|
this.debugLog("name: " + player.name);
|
||||||
|
if(hasClanTag) {
|
||||||
|
if(hasClanTagPos) {
|
||||||
|
const clanTagPos = reader.uint(1);
|
||||||
|
this.debugLog("clanTagPos: " + clanTagPos);
|
||||||
|
}
|
||||||
|
player.clantag = this.stripColors(reader.string());
|
||||||
|
this.debugLog("clan tag: " + player.clantag);
|
||||||
|
}
|
||||||
|
if(hasTypeFlag) {
|
||||||
|
player.typeflag = reader.uint(1);
|
||||||
|
this.debugLog("type flag: " + player.typeflag);
|
||||||
|
}
|
||||||
|
players.push(player);
|
||||||
|
}
|
||||||
|
return [players,reader];
|
||||||
}
|
}
|
||||||
|
|
||||||
stripColors(str) {
|
stripColors(str) {
|
||||||
|
|
|
@ -13,7 +13,7 @@ class FiveM extends Quake2 {
|
||||||
|
|
||||||
{
|
{
|
||||||
const raw = await this.request({
|
const raw = await this.request({
|
||||||
uri: 'http://' + this.options.address + ':' + this.options.port_query + '/info.json'
|
uri: 'http://' + this.options.address + ':' + this.options.port + '/info.json'
|
||||||
});
|
});
|
||||||
const json = JSON.parse(raw);
|
const json = JSON.parse(raw);
|
||||||
state.raw.info = json;
|
state.raw.info = json;
|
||||||
|
@ -21,7 +21,7 @@ class FiveM extends Quake2 {
|
||||||
|
|
||||||
{
|
{
|
||||||
const raw = await this.request({
|
const raw = await this.request({
|
||||||
uri: 'http://' + this.options.address + ':' + this.options.port_query + '/players.json'
|
uri: 'http://' + this.options.address + ':' + this.options.port + '/players.json'
|
||||||
});
|
});
|
||||||
const json = JSON.parse(raw);
|
const json = JSON.parse(raw);
|
||||||
state.raw.players = json;
|
state.raw.players = json;
|
||||||
|
|
|
@ -37,6 +37,30 @@ class Gamespy2 extends Core {
|
||||||
const reader = this.reader(body);
|
const reader = this.reader(body);
|
||||||
state.raw.teams = this.readFieldData(reader);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.raw.hostport) {
|
||||||
|
state.gamePort = parseInt(state.raw.hostport);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendPacket(type) {
|
async sendPacket(type) {
|
||||||
|
|
|
@ -7,17 +7,19 @@ class Gamespy3 extends Core {
|
||||||
this.sessionId = 1;
|
this.sessionId = 1;
|
||||||
this.encoding = 'latin1';
|
this.encoding = 'latin1';
|
||||||
this.byteorder = 'be';
|
this.byteorder = 'be';
|
||||||
this.noChallenge = false;
|
|
||||||
this.useOnlySingleSplit = false;
|
this.useOnlySingleSplit = false;
|
||||||
this.isJc2mp = false;
|
this.isJc2mp = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(state) {
|
async run(state) {
|
||||||
let challenge = null;
|
|
||||||
if (!this.noChallenge) {
|
|
||||||
const buffer = await this.sendPacket(9, false, false, false);
|
const buffer = await this.sendPacket(9, false, false, false);
|
||||||
const reader = this.reader(buffer);
|
const reader = this.reader(buffer);
|
||||||
challenge = parseInt(reader.string());
|
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;
|
let requestPayload;
|
||||||
|
|
|
@ -3,7 +3,7 @@ const Core = require('./core');
|
||||||
class Kspdmp extends Core {
|
class Kspdmp extends Core {
|
||||||
async run(state) {
|
async run(state) {
|
||||||
const body = await this.request({
|
const body = await this.request({
|
||||||
uri: 'http://'+this.options.address+':'+this.options.port_query
|
uri: 'http://'+this.options.address+':'+this.options.port
|
||||||
});
|
});
|
||||||
|
|
||||||
const json = JSON.parse(body);
|
const json = JSON.parse(body);
|
||||||
|
|
|
@ -2,9 +2,13 @@ const Core = require('./core'),
|
||||||
Varint = require('varint');
|
Varint = require('varint');
|
||||||
|
|
||||||
class Minecraft extends Core {
|
class Minecraft extends Core {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.srvRecord = "_minecraft._tcp";
|
||||||
|
}
|
||||||
async run(state) {
|
async run(state) {
|
||||||
const portBuf = Buffer.alloc(2);
|
const portBuf = Buffer.alloc(2);
|
||||||
portBuf.writeUInt16BE(this.options.port_query,0);
|
portBuf.writeUInt16BE(this.options.port,0);
|
||||||
|
|
||||||
const addressBuf = Buffer.from(this.options.host,'utf8');
|
const addressBuf = Buffer.from(this.options.host,'utf8');
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,6 @@ const gbxremote = require('gbxremote'),
|
||||||
Core = require('./core');
|
Core = require('./core');
|
||||||
|
|
||||||
class Nadeo extends Core {
|
class Nadeo extends Core {
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.options.port = 2350;
|
|
||||||
this.options.port_query = 5000;
|
|
||||||
}
|
|
||||||
|
|
||||||
async run(state) {
|
async run(state) {
|
||||||
await this.withClient(async client => {
|
await this.withClient(async client => {
|
||||||
await this.methodCall(client, 'Authenticate', this.options.login, this.options.password);
|
await this.methodCall(client, 'Authenticate', this.options.login, this.options.password);
|
||||||
|
@ -57,8 +51,8 @@ class Nadeo extends Core {
|
||||||
}
|
}
|
||||||
|
|
||||||
async withClient(fn) {
|
async withClient(fn) {
|
||||||
const socket = gbxremote.createClient(this.options.port_query, this.options.host);
|
const socket = gbxremote.createClient(this.options.port, this.options.host);
|
||||||
const cancelAsyncLeak = this.addAsyncLeak(() => socket.terminate());
|
const cancelAsyncLeak = this.addCleanup(() => socket.terminate());
|
||||||
try {
|
try {
|
||||||
await this.timedPromise(
|
await this.timedPromise(
|
||||||
new Promise((resolve,reject) => {
|
new Promise((resolve,reject) => {
|
||||||
|
|
|
@ -3,7 +3,7 @@ const Core = require('./core');
|
||||||
class Terraria extends Core {
|
class Terraria extends Core {
|
||||||
async run(state) {
|
async run(state) {
|
||||||
const body = await this.request({
|
const body = await this.request({
|
||||||
uri: 'http://'+this.options.address+':'+this.options.port_query+'/v2/server/status',
|
uri: 'http://'+this.options.address+':'+this.options.port+'/v2/server/status',
|
||||||
qs: {
|
qs: {
|
||||||
players: 'true',
|
players: 'true',
|
||||||
token: this.options.token
|
token: this.options.token
|
||||||
|
|
Loading…
Reference in a new issue