mirror of
https://github.com/gamedig/node-gamedig.git
synced 2024-11-16 16:58:32 +01:00
198 lines
7.2 KiB
JavaScript
198 lines
7.2 KiB
JavaScript
const Core = require('./core');
|
|
|
|
class Gamespy3 extends Core {
|
|
constructor() {
|
|
super();
|
|
this.sessionId = 1;
|
|
this.encoding = 'latin1';
|
|
this.byteorder = 'be';
|
|
this.useOnlySingleSplit = false;
|
|
this.isJc2mp = false;
|
|
}
|
|
|
|
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[] */
|
|
const packets = await this.sendPacket(0,challenge,requestPayload,true);
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
b.writeUInt8(0xFE, 0);
|
|
b.writeUInt8(0xFD, 1);
|
|
b.writeUInt8(type, 2);
|
|
b.writeUInt32BE(this.sessionId, 3);
|
|
if(challengeLength) b.writeInt32BE(challenge, 7);
|
|
if(payloadLength) payload.copy(b, 7+challengeLength);
|
|
|
|
let numPackets = 0;
|
|
const packets = {};
|
|
return await this.udpSend(b,(buffer) => {
|
|
const reader = this.reader(buffer);
|
|
const iType = reader.uint(1);
|
|
if(iType !== type) {
|
|
this.logger.debug('Skipping packet, type mismatch');
|
|
return;
|
|
}
|
|
const iSessionId = reader.uint(4);
|
|
if(iSessionId !== this.sessionId) {
|
|
this.logger.debug('Skipping packet, session id mismatch');
|
|
return;
|
|
}
|
|
|
|
if(!assemble) {
|
|
return reader.rest();
|
|
}
|
|
if(this.useOnlySingleSplit) {
|
|
// has split headers, but they are worthless and only one packet is used
|
|
reader.skip(11);
|
|
return [reader.rest()];
|
|
}
|
|
|
|
reader.skip(9); // filler data -- usually set to 'splitnum\0'
|
|
let id = reader.uint(1);
|
|
const last = (id & 0x80);
|
|
id = id & 0x7f;
|
|
if(last) numPackets = id+1;
|
|
|
|
reader.skip(1); // "another 'packet number' byte, but isn't understood."
|
|
|
|
packets[id] = reader.rest();
|
|
if(this.debug) {
|
|
this.debugLog("Received packet #"+id + (last ? " (last)" : ""));
|
|
}
|
|
|
|
if(!numPackets || Object.keys(packets).length !== numPackets) return;
|
|
|
|
// 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]);
|
|
}
|
|
return list;
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports = Gamespy3;
|