const Core = require('./core'); class Unreal2 extends Core { constructor() { super(); this.encoding = 'latin1'; } async run(state) { let extraInfoReader; { const b = await this.sendPacket(0, true); const reader = this.reader(b); state.raw.serverid = reader.uint(4); state.raw.ip = this.readUnrealString(reader); state.gamePort = reader.uint(4); state.raw.queryport = reader.uint(4); state.name = this.readUnrealString(reader, true); state.map = this.readUnrealString(reader, true); state.raw.gametype = this.readUnrealString(reader, true); state.raw.numplayers = reader.uint(4); state.maxplayers = reader.uint(4); this.logger.debug(log => { log("UNREAL2 EXTRA INFO", reader.buffer.slice(reader.i)); }); extraInfoReader = reader; } { const b = await this.sendPacket(1,true); const reader = this.reader(b); state.raw.mutators = []; state.raw.rules = {}; while(!reader.done()) { const key = this.readUnrealString(reader,true); const value = this.readUnrealString(reader,true); this.logger.debug(key+'='+value); if(key === 'Mutator' || key === 'mutator') { state.raw.mutators.push(value); } else if (key || value) { if (state.raw.rules.hasOwnProperty(key)) { state.raw.rules[key] += ',' + value; } else { state.raw.rules[key] = value; } } } if('GamePassword' in state.raw.rules) state.password = state.raw.rules.GamePassword !== 'True'; } if (state.raw.mutators.includes('KillingFloorMut') || state.raw.rules['Num trader weapons'] || state.raw.rules['Server Version'] === '1065' ) { // Killing Floor state.raw.wavecurrent = extraInfoReader.uint(4); state.raw.wavetotal = extraInfoReader.uint(4); state.raw.ping = extraInfoReader.uint(4); state.raw.flags = extraInfoReader.uint(4); state.raw.skillLevel = this.readUnrealString(extraInfoReader, true); } else { state.raw.ping = extraInfoReader.uint(4); // These fields were added in later revisions of unreal engine if (extraInfoReader.remaining() >= 8) { state.raw.flags = extraInfoReader.uint(4); state.raw.skill = this.readUnrealString(extraInfoReader, true); } } { const b = await this.sendPacket(2,false); const reader = this.reader(b); state.raw.scoreboard = {}; while(!reader.done()) { const player = {}; player.id = reader.uint(4); player.name = this.readUnrealString(reader,true); player.ping = reader.uint(4); player.score = reader.int(4); player.statsId = reader.uint(4); this.logger.debug(player); if (!player.id) { state.raw.scoreboard[player.name] = player.score; } else if (!player.ping) { state.bots.push(player); } else { state.players.push(player); } } } } readUnrealString(reader, stripColor) { let length = reader.uint(1), ucs2 = false; if(length >= 0x80) { // This is flagged as a UCS-2 String length = (length&0x7f)*2; ucs2 = true; // For UCS-2 strings, some unreal 2 games randomly insert an extra 0x01 here, // not included in the length. Skip it if present (hopefully this never happens legitimately) const peek = reader.uint(1); if (peek !== 1) reader.skip(-1); this.debugLog(log => { log("UCS2 STRING"); log("UCS2 Length: " + length); log(reader.buffer.slice(reader.i,reader.i+length)); }); } let out = ''; if (ucs2) { out = reader.string({encoding:'ucs2',length:length}); this.debugLog("UCS2 String decoded: " + out); } else if (length > 0) { out = reader.string(); } // Sometimes the string has a null at the end (included with the length) // Strip it if present if(out.charCodeAt(out.length-1) === 0) { out = out.substring(0, out.length - 1); } if(stripColor) { out = out.replace(/\x1b...|[\x00-\x1a]/gus,''); } return out; } async sendPacket(type,required) { const outbuffer = Buffer.from([0x79,0,0,0,type]); const packets = []; return await this.udpSend(outbuffer,(buffer) => { const reader = this.reader(buffer); const header = reader.uint(4); const iType = reader.uint(1); if(iType !== type) return; packets.push(reader.rest()); }, () => { if(!packets.length && required) return; return Buffer.concat(packets); }); } } module.exports = Unreal2;