2019-01-07 01:52:03 +01:00
|
|
|
const Core = require('./core');
|
|
|
|
|
|
|
|
class Doom3 extends Core {
|
2017-08-09 12:32:09 +02:00
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
this.encoding = 'latin1';
|
|
|
|
this.isEtqw = false;
|
|
|
|
this.hasSpaceBeforeClanTag = false;
|
|
|
|
this.hasClanTag = false;
|
|
|
|
this.hasTypeFlag = false;
|
|
|
|
}
|
2019-01-09 12:35:11 +01:00
|
|
|
async run(state) {
|
2019-01-12 11:43:36 +01:00
|
|
|
const body = await this.udpSend('\xff\xffgetInfo\x00PiNGPoNg\x00', packet => {
|
2019-01-09 12:35:11 +01:00
|
|
|
const reader = this.reader(packet);
|
2017-08-09 11:05:55 +02:00
|
|
|
const header = reader.uint(2);
|
2017-08-09 12:32:09 +02:00
|
|
|
if(header !== 0xffff) return;
|
2017-08-09 11:05:55 +02:00
|
|
|
const header2 = reader.string();
|
2017-08-09 12:32:09 +02:00
|
|
|
if(header2 !== 'infoResponse') return;
|
2019-01-12 11:43:36 +01:00
|
|
|
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);
|
2019-01-09 12:35:11 +01:00
|
|
|
return reader.rest();
|
|
|
|
});
|
2014-10-29 08:02:03 +01:00
|
|
|
|
2019-01-12 11:43:36 +01:00
|
|
|
let reader = this.reader(body);
|
2019-01-09 12:35:11 +01:00
|
|
|
const protoVersion = reader.uint(4);
|
|
|
|
state.raw.protocolVersion = (protoVersion>>16)+'.'+(protoVersion&0xffff);
|
2017-08-09 12:32:09 +02:00
|
|
|
|
2019-01-12 11:43:36 +01:00
|
|
|
// 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) {
|
2019-01-09 12:35:11 +01:00
|
|
|
const size = reader.uint(4);
|
2019-01-12 11:43:36 +01:00
|
|
|
this.debugLog("Received packet size: " + size);
|
2019-01-09 12:35:11 +01:00
|
|
|
}
|
2014-10-29 08:02:03 +01:00
|
|
|
|
2019-01-09 12:35:11 +01:00
|
|
|
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','');
|
2017-08-09 12:32:09 +02:00
|
|
|
}
|
2019-01-09 12:35:11 +01:00
|
|
|
if(!key) break;
|
|
|
|
state.raw[key] = value;
|
2019-01-12 11:43:36 +01:00
|
|
|
this.debugLog(key + "=" + value);
|
2019-01-09 12:35:11 +01:00
|
|
|
}
|
2014-10-29 08:02:03 +01:00
|
|
|
|
2019-01-12 11:43:36 +01:00
|
|
|
const isEtqw = state.raw.gamename && state.raw.gamename.toLowerCase().includes('etqw');
|
2017-08-09 12:32:09 +02:00
|
|
|
|
2019-01-12 11:43:36 +01:00
|
|
|
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) {
|
2019-01-09 12:35:11 +01:00
|
|
|
if(!player.ping || player.typeflag)
|
|
|
|
state.bots.push(player);
|
|
|
|
else
|
|
|
|
state.players.push(player);
|
|
|
|
}
|
2017-08-09 12:32:09 +02:00
|
|
|
|
2019-01-09 12:35:11 +01:00
|
|
|
state.raw.osmask = reader.uint(4);
|
2019-01-12 12:45:09 +01:00
|
|
|
if (isEtqw) {
|
2019-01-09 12:35:11 +01:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
2014-10-29 08:02:03 +01:00
|
|
|
|
2019-01-12 12:45:09 +01:00
|
|
|
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;
|
2019-01-12 11:43:36 +01:00
|
|
|
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];
|
2017-08-09 12:32:09 +02:00
|
|
|
}
|
2017-08-09 11:05:55 +02:00
|
|
|
|
2017-08-09 12:32:09 +02:00
|
|
|
stripColors(str) {
|
|
|
|
// uses quake 3 color codes
|
|
|
|
return str.replace(/\^(X.{6}|.)/g,'');
|
|
|
|
}
|
2017-08-09 11:05:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = Doom3;
|