mirror of
https://github.com/tribufu/node-gamedig
synced 2026-06-01 09:42:41 +00:00
Initial es6 async conversion work
This commit is contained in:
parent
a054557f10
commit
77b2cc1c7f
10 changed files with 773 additions and 748 deletions
|
|
@ -1,5 +1,4 @@
|
|||
const async = require('async'),
|
||||
Bzip2 = require('compressjs').Bzip2,
|
||||
const Bzip2 = require('compressjs').Bzip2,
|
||||
Core = require('./core');
|
||||
|
||||
class Valve extends Core {
|
||||
|
|
@ -28,173 +27,169 @@ 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) {
|
||||
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) {
|
||||
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.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;
|
||||
}
|
||||
}
|
||||
|
||||
queryPlayers(state,c) {
|
||||
async queryChallenge() {
|
||||
if(this.legacyChallenge) {
|
||||
// sendPacket will catch the response packet and
|
||||
// save the challenge for us
|
||||
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;
|
||||
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
|
||||
});
|
||||
}
|
||||
if(this.debug) console.log("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;
|
||||
});
|
||||
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 +229,158 @@ 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 retryQuery = false;
|
||||
const response = await this.sendPacketRaw(
|
||||
type, sendChallenge, payload,
|
||||
(payload) => {
|
||||
const reader = this.reader(payload);
|
||||
const type = reader.uint(1);
|
||||
if (type === 0x41) {
|
||||
const key = reader.uint(4);
|
||||
if (this._challenge !== key) {
|
||||
if (this.debug) console.log('Received new challenge key: ' + key);
|
||||
this._challenge = key;
|
||||
retryQuery = true;
|
||||
if (keyRetry === 0 && sendChallenge) {
|
||||
if (this.debug) console.log('Restarting query');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.debug) console.log("Received " + type.toString(16) + " expected " + expect.toString(16));
|
||||
if (type === expect) {
|
||||
return reader.rest();
|
||||
}
|
||||
},
|
||||
() => {
|
||||
if (allowTimeout) return null;
|
||||
}
|
||||
);
|
||||
if (!retryQuery) 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
|
||||
if(this.debug) console.log("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;
|
||||
};
|
||||
|
||||
const receivedOne = (buffer) => {
|
||||
const reader = this.reader(buffer);
|
||||
|
||||
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;
|
||||
if(this.debug) {
|
||||
console.log("Received partial packet uid:"+uid+" num:"+packetNum);
|
||||
console.log("Received "+Object.keys(packets).length+'/'+numPackets+" packets for this UID");
|
||||
}
|
||||
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;
|
||||
if(Object.keys(packets).length !== numPackets) return;
|
||||
|
||||
// assemble the parts
|
||||
const list = [];
|
||||
for(let i = 0; i < numPackets; i++) {
|
||||
if(!(i in packets)) {
|
||||
this.fatal('Missing packet #'+i);
|
||||
return true;
|
||||
}
|
||||
list.push(packets[i]);
|
||||
}
|
||||
|
||||
let assembled = Buffer.concat(list);
|
||||
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;
|
||||
}
|
||||
}
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue