diff --git a/games.txt b/games.txt index a5c19ba..2978c46 100644 --- a/games.txt +++ b/games.txt @@ -234,7 +234,7 @@ stalker|S.T.A.L.K.E.R.|gamespy3|port=5445,port_query_offset=2 stbc|Star Trek: Bridge Commander|gamespy1|port_query=22101 stvef|Star Trek: Voyager - Elite Force|quake3|port_query=27960 stvef2|Star Trek: Voyager - Elite Force 2|quake3|port_query=29253 -squad|Squad|squad|port=7787,port_query=27165 +squad|Squad|valve|port=7787,port_query=27165 swbf|Star Wars: Battlefront|gamespy2|port_query=3658 swbf2|Star Wars: Battlefront 2|gamespy2|port_query=3658 swjk|Star Wars Jedi Knight: Jedi Academy (2003)|quake3|port_query=29070 diff --git a/lib/Results.js b/lib/Results.js new file mode 100644 index 0000000..2ca54ba --- /dev/null +++ b/lib/Results.js @@ -0,0 +1,51 @@ +class Player { + name = ''; + raw = {}; + + constructor(data) { + if (typeof data === 'string') { + this.name = data; + } else { + const {name, ...raw} = data; + if (name) this.name = name; + if (raw) this.raw = raw; + } + } +} + +class Players extends Array { + setNum(num) { + // If the server specified some ridiculous number of players (billions), we don't want to + // run out of ram allocating these objects. + num = Math.min(num, 10000); + + while(this.players.length < num) { + this.push({}); + } + } + + push(data) { + super.push(new Player(data)); + } +} + +class Results { + name = ''; + map = ''; + password = false; + + raw = {}; + + maxplayers = 0; + players = new Players(); + bots = new Players(); + + set players(num) { + this.players.setNum(num); + } + set bots(num) { + this.bots.setNum(num); + } +} + +module.exports = Results; diff --git a/lib/reader.js b/lib/reader.js index 5dbc2f2..0ab184e 100644 --- a/lib/reader.js +++ b/lib/reader.js @@ -115,12 +115,12 @@ class Reader { if(bytes === 1) r = this.buffer.readUInt8(this.i); else if(bytes === 2) r = this.buffer.readUInt16BE(this.i); else if(bytes === 4) r = this.buffer.readUInt32BE(this.i); - else if(bytes === 8) r = readUInt64BE(this.buffer,this.i).toString(); + else if(bytes === 8) r = readUInt64BE(this.buffer,this.i); } else { if(bytes === 1) r = this.buffer.readUInt8(this.i); else if(bytes === 2) r = this.buffer.readUInt16LE(this.i); else if(bytes === 4) r = this.buffer.readUInt32LE(this.i); - else if(bytes === 8) r = readUInt64LE(this.buffer,this.i).toString(); + else if(bytes === 8) r = readUInt64LE(this.buffer,this.i); } } this.i += bytes; diff --git a/protocols/core.js b/protocols/core.js index 0989495..d2535ff 100644 --- a/protocols/core.js +++ b/protocols/core.js @@ -5,7 +5,8 @@ const EventEmitter = require('events').EventEmitter, got = require('got'), Promises = require('../lib/Promises'), Logger = require('../lib/Logger'), - DnsResolver = require('../lib/DnsResolver'); + DnsResolver = require('../lib/DnsResolver'), + Results = require('../lib/Results'); let uid = 0; @@ -74,44 +75,13 @@ class Core extends EventEmitter { if (resolved.port) options.port = resolved.port; } - const state = { - name: '', - map: '', - password: false, - - raw: {}, - - maxplayers: 0, - players: [], - bots: [] - }; + const state = new Results(); await this.run(state); // because lots of servers prefix with spaces to try to appear first state.name = (state.name || '').trim(); - if (typeof state.players === 'number') { - const num = state.players; - state.players = []; - state.raw.rcvNumPlayers = num; - if (num < 10000) { - for (let i = 0; i < num; i++) { - state.players.push({}); - } - } - } - if (typeof state.bots === 'number') { - const num = state.bots; - state.bots = []; - state.raw.rcvNumBots = num; - if (num < 10000) { - for (let i = 0; i < num; i++) { - state.bots.push({}); - } - } - } - if (!('connect' in state)) { state.connect = '' + (state.gameHost || this.options.host || this.options.address) @@ -130,7 +100,7 @@ class Core extends EventEmitter { return state; } - async run(state) {} + async run(/** Results */ state) {} /** Param can be a time in ms, or a promise (which will be timed) */ registerRtt(param) { diff --git a/protocols/gamespy1.js b/protocols/gamespy1.js index 3721152..206bf54 100644 --- a/protocols/gamespy1.js +++ b/protocols/gamespy1.js @@ -35,9 +35,14 @@ class Gamespy1 extends Core { teamNamesById[id] = value; } else { if (!(id in playersById)) playersById[id] = {}; - if (key === 'playername') key = 'name'; - else if (key === 'team') value = parseInt(value); - else if (key === 'score' || key === 'ping' || key === 'deaths' || key === 'kills') value = parseInt(value); + if (key === 'playername') { + key = 'name'; + } else if (key === 'team' && !isNaN(parseInt(value))) { + key = 'teamId'; + value = parseInt(value); + } else if (key === 'score' || key === 'ping' || key === 'deaths' || key === 'kills') { + value = parseInt(value); + } playersById[id][key] = value; } } @@ -45,28 +50,6 @@ class Gamespy1 extends Core { const players = Object.values(playersById); - // Determine which team id might be for spectators - let specTeamId = null; - for (const player of players) { - if (!player.team) { - continue; - } else if (teamNamesById[player.team]) { - continue; - } else if (teamNamesById[player.team-1] && (specTeamId === null || specTeamId === player.team)) { - specTeamId = player.team; - } else { - specTeamId = null; - break; - } - } - this.logger.debug(log => { - if (specTeamId === null) { - log("Could not detect a team ID for spectators"); - } else { - log("Detected that team ID " + specTeamId + " is probably for spectators"); - } - }); - const seenHashes = new Set(); for (const player of players) { // Some servers (bf1942) report the same player multiple times (bug?) @@ -81,11 +64,12 @@ class Gamespy1 extends Core { } // Convert player's team ID to team name if possible - if (player.team) { - if (teamNamesById[player.team]) { - player.team = teamNamesById[player.team]; - } else if (player.team === specTeamId) { - player.team = "spec"; + if (player.hasOwnProperty('teamId')) { + if (Object.keys(teamNamesById).length) { + player.team = teamNamesById[player.teamId - 1] || ''; + } else { + player.team = player.teamId; + delete player.teamId; } } diff --git a/protocols/openttd.js b/protocols/openttd.js index 9bf336f..5fc2e11 100644 --- a/protocols/openttd.js +++ b/protocols/openttd.js @@ -60,9 +60,9 @@ class OpenTtd extends Core { company.id = reader.uint(1); company.name = reader.string(); company.year_start = reader.uint(4); - company.value = reader.uint(8); - company.money = reader.uint(8); - company.income = reader.uint(8); + company.value = reader.uint(8).toString(); + company.money = reader.uint(8).toString(); + company.income = reader.uint(8).toString(); company.performance = reader.uint(2); company.password = !!reader.uint(1); diff --git a/protocols/squad.js b/protocols/squad.js deleted file mode 100644 index 0be9e02..0000000 --- a/protocols/squad.js +++ /dev/null @@ -1,16 +0,0 @@ -const Valve = require('./valve'); - -class Squad extends Valve { - constructor() { - super(); - } - - async cleanup(state) { - await super.cleanup(state); - if (state.raw.rules != null && state.raw.rules.Password_b === "true") { - state.password = true; - } - } -} - -module.exports = Squad; diff --git a/protocols/starmade.js b/protocols/starmade.js index 091fdf6..d9e4f10 100644 --- a/protocols/starmade.js +++ b/protocols/starmade.js @@ -16,7 +16,7 @@ class Starmade extends Core { const reader = this.reader(buffer); const packetLength = reader.uint(4); this.logger.debug("Received packet length: " + packetLength); - const timestamp = reader.uint(8); + const timestamp = reader.uint(8).toString(); this.logger.debug("Received timestamp: " + timestamp); if (reader.remaining() < packetLength || reader.remaining() < 5) return; diff --git a/protocols/valve.js b/protocols/valve.js index 3cc72ba..3d2fea0 100644 --- a/protocols/valve.js +++ b/protocols/valve.js @@ -1,5 +1,12 @@ const Bzip2 = require('compressjs').Bzip2, - Core = require('./core'); + Core = require('./core'), + Results = require('../lib/Results'); + +const AppId = { + Squad: 393380, + Bat1944: 489940, + Ship: 2400 +}; class Valve extends Core { constructor() { @@ -34,7 +41,7 @@ class Valve extends Core { await this.cleanup(state); } - async queryInfo(state) { + async queryInfo(/** Results */ state) { this.debugLog("Requesting info ..."); const b = await this.sendPacket( 0x54, @@ -52,7 +59,7 @@ class Valve extends Core { state.map = reader.string(); state.raw.folder = reader.string(); state.raw.game = reader.string(); - state.raw.steamappid = reader.uint(2); + state.raw.appId = reader.uint(2); state.raw.numplayers = reader.uint(1); state.maxplayers = reader.uint(1); @@ -84,7 +91,7 @@ class Valve extends Core { if(this.goldsrcInfo) { state.raw.numbots = reader.uint(1); } else { - if(state.raw.folder === 'ship') { + if(state.raw.appId === AppId.Ship) { state.raw.shipmode = reader.uint(1); state.raw.shipwitnesses = reader.uint(1); state.raw.shipduration = reader.uint(1); @@ -92,22 +99,28 @@ class Valve extends Core { state.raw.version = reader.string(); const extraFlag = reader.uint(1); if(extraFlag & 0x80) state.gamePort = reader.uint(2); - if(extraFlag & 0x10) state.raw.steamid = reader.uint(8); + if(extraFlag & 0x10) state.raw.steamid = reader.uint(8).toString(); 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); + if(extraFlag & 0x01) { + const gameId = reader.uint(8); + const betterAppId = gameId.getLowBitsUnsigned() & 0xffffff; + if (betterAppId) { + state.raw.appId = betterAppId; + } + } } // 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 + state.raw.appId === 215 + || state.raw.appId === 17550 + || state.raw.appId === 17700 + || state.raw.appId === 240 ) ) { this._skipSizeInSplitHeader = true; @@ -133,7 +146,7 @@ class Valve extends Core { } } - async queryPlayers(state) { + async queryPlayers(/** Results */ state) { state.raw.players = []; this.debugLog("Requesting player list ..."); @@ -174,8 +187,18 @@ class Valve extends Core { } } - async queryRules(state) { - state.raw.rules = {}; + async queryRules(/** Results */ state) { + const appId = state.raw.appId; + if (appId === AppId.Squad + || appId === AppId.Bat1944 + || this.options.requestRules) { + // let's get 'em + } else { + return; + } + + const rules = {}; + state.raw.rules = rules; this.debugLog("Requesting rules ..."); const b = await this.sendPacket(0x56,null,0x45,true); if (b === null) return; // timed out - the server probably has rules disabled @@ -185,31 +208,40 @@ class Valve extends Core { for(let i = 0; i < num; i++) { const key = reader.string(); const value = reader.string(); - state.raw.rules[key] = value; + rules[key] = value; + } + + // Battalion 1944 puts its info into rules fields for some reason + if (appId === AppId.Bat1944) { + if ('bat_name_s' in rules) { + state.name = rules.bat_name_s; + delete rules.bat_name_s; + if ('bat_player_count_s' in rules) { + state.raw.numplayers = parseInt(rules.bat_player_count_s); + delete rules.bat_player_count_s; + } + if ('bat_max_players_i' in rules) { + state.maxplayers = parseInt(rules.bat_max_players_i); + delete rules.bat_max_players_i; + } + if ('bat_has_password_s' in rules) { + state.password = rules.bat_has_password_s === 'Y'; + delete rules.bat_has_password_s; + } + // apparently map is already right, and this var is often wrong + delete rules.bat_map_s; + } + } + + // Squad keeps its password in a separate field + if (appId === AppId.Squad) { + if (rules.Password_b === "true") { + state.password = true; + } } } - 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; - delete state.raw.rules.bat_name_s; - if ('bat_player_count_s' in state.raw.rules) { - state.raw.numplayers = parseInt(state.raw.rules.bat_player_count_s); - delete state.raw.rules.bat_player_count_s; - } - if ('bat_max_players_i' in state.raw.rules) { - state.maxplayers = parseInt(state.raw.rules.bat_max_players_i); - delete state.raw.rules.bat_max_players_i; - } - if ('bat_has_password_s' in state.raw.rules) { - state.password = state.raw.rules.bat_has_password_s === 'Y'; - delete state.raw.rules.bat_has_password_s; - } - // apparently map is already right, and this var is often wrong - delete state.raw.rules.bat_map_s; - } - + async cleanup(/** Results */ state) { // Organize players / hidden players into player / bot arrays const botProbability = (p) => { if (p.time === -1) return Number.MAX_VALUE;