mirror of
https://github.com/gamedig/node-gamedig.git
synced 2024-11-18 17:50:37 +01:00
* Properly handle non-indexed team names in gamespy1 Fixes #213
* Rename raw.steamappid and raw.gameid to raw.appId in steam protocol * Don't query valve rules by default, unless requestRules option is set Fixes #176
This commit is contained in:
parent
7f08381b17
commit
f70112d092
9 changed files with 143 additions and 122 deletions
|
@ -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
|
stbc|Star Trek: Bridge Commander|gamespy1|port_query=22101
|
||||||
stvef|Star Trek: Voyager - Elite Force|quake3|port_query=27960
|
stvef|Star Trek: Voyager - Elite Force|quake3|port_query=27960
|
||||||
stvef2|Star Trek: Voyager - Elite Force 2|quake3|port_query=29253
|
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
|
swbf|Star Wars: Battlefront|gamespy2|port_query=3658
|
||||||
swbf2|Star Wars: Battlefront 2|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
|
swjk|Star Wars Jedi Knight: Jedi Academy (2003)|quake3|port_query=29070
|
||||||
|
|
51
lib/Results.js
Normal file
51
lib/Results.js
Normal file
|
@ -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;
|
|
@ -115,12 +115,12 @@ class Reader {
|
||||||
if(bytes === 1) r = this.buffer.readUInt8(this.i);
|
if(bytes === 1) r = this.buffer.readUInt8(this.i);
|
||||||
else if(bytes === 2) r = this.buffer.readUInt16BE(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 === 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 {
|
} else {
|
||||||
if(bytes === 1) r = this.buffer.readUInt8(this.i);
|
if(bytes === 1) r = this.buffer.readUInt8(this.i);
|
||||||
else if(bytes === 2) r = this.buffer.readUInt16LE(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 === 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;
|
this.i += bytes;
|
||||||
|
|
|
@ -5,7 +5,8 @@ const EventEmitter = require('events').EventEmitter,
|
||||||
got = require('got'),
|
got = require('got'),
|
||||||
Promises = require('../lib/Promises'),
|
Promises = require('../lib/Promises'),
|
||||||
Logger = require('../lib/Logger'),
|
Logger = require('../lib/Logger'),
|
||||||
DnsResolver = require('../lib/DnsResolver');
|
DnsResolver = require('../lib/DnsResolver'),
|
||||||
|
Results = require('../lib/Results');
|
||||||
|
|
||||||
let uid = 0;
|
let uid = 0;
|
||||||
|
|
||||||
|
@ -74,44 +75,13 @@ class Core extends EventEmitter {
|
||||||
if (resolved.port) options.port = resolved.port;
|
if (resolved.port) options.port = resolved.port;
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = {
|
const state = new Results();
|
||||||
name: '',
|
|
||||||
map: '',
|
|
||||||
password: false,
|
|
||||||
|
|
||||||
raw: {},
|
|
||||||
|
|
||||||
maxplayers: 0,
|
|
||||||
players: [],
|
|
||||||
bots: []
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.run(state);
|
await this.run(state);
|
||||||
|
|
||||||
// because lots of servers prefix with spaces to try to appear first
|
// because lots of servers prefix with spaces to try to appear first
|
||||||
state.name = (state.name || '').trim();
|
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)) {
|
if (!('connect' in state)) {
|
||||||
state.connect = ''
|
state.connect = ''
|
||||||
+ (state.gameHost || this.options.host || this.options.address)
|
+ (state.gameHost || this.options.host || this.options.address)
|
||||||
|
@ -130,7 +100,7 @@ class Core extends EventEmitter {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(state) {}
|
async run(/** Results */ state) {}
|
||||||
|
|
||||||
/** Param can be a time in ms, or a promise (which will be timed) */
|
/** Param can be a time in ms, or a promise (which will be timed) */
|
||||||
registerRtt(param) {
|
registerRtt(param) {
|
||||||
|
|
|
@ -35,9 +35,14 @@ class Gamespy1 extends Core {
|
||||||
teamNamesById[id] = value;
|
teamNamesById[id] = value;
|
||||||
} else {
|
} else {
|
||||||
if (!(id in playersById)) playersById[id] = {};
|
if (!(id in playersById)) playersById[id] = {};
|
||||||
if (key === 'playername') key = 'name';
|
if (key === 'playername') {
|
||||||
else if (key === 'team') value = parseInt(value);
|
key = 'name';
|
||||||
else if (key === 'score' || key === 'ping' || key === 'deaths' || key === 'kills') value = parseInt(value);
|
} 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;
|
playersById[id][key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,28 +50,6 @@ class Gamespy1 extends Core {
|
||||||
|
|
||||||
const players = Object.values(playersById);
|
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();
|
const seenHashes = new Set();
|
||||||
for (const player of players) {
|
for (const player of players) {
|
||||||
// Some servers (bf1942) report the same player multiple times (bug?)
|
// 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
|
// Convert player's team ID to team name if possible
|
||||||
if (player.team) {
|
if (player.hasOwnProperty('teamId')) {
|
||||||
if (teamNamesById[player.team]) {
|
if (Object.keys(teamNamesById).length) {
|
||||||
player.team = teamNamesById[player.team];
|
player.team = teamNamesById[player.teamId - 1] || '';
|
||||||
} else if (player.team === specTeamId) {
|
} else {
|
||||||
player.team = "spec";
|
player.team = player.teamId;
|
||||||
|
delete player.teamId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -60,9 +60,9 @@ class OpenTtd extends Core {
|
||||||
company.id = reader.uint(1);
|
company.id = reader.uint(1);
|
||||||
company.name = reader.string();
|
company.name = reader.string();
|
||||||
company.year_start = reader.uint(4);
|
company.year_start = reader.uint(4);
|
||||||
company.value = reader.uint(8);
|
company.value = reader.uint(8).toString();
|
||||||
company.money = reader.uint(8);
|
company.money = reader.uint(8).toString();
|
||||||
company.income = reader.uint(8);
|
company.income = reader.uint(8).toString();
|
||||||
company.performance = reader.uint(2);
|
company.performance = reader.uint(2);
|
||||||
company.password = !!reader.uint(1);
|
company.password = !!reader.uint(1);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
|
@ -16,7 +16,7 @@ class Starmade extends Core {
|
||||||
const reader = this.reader(buffer);
|
const reader = this.reader(buffer);
|
||||||
const packetLength = reader.uint(4);
|
const packetLength = reader.uint(4);
|
||||||
this.logger.debug("Received packet length: " + packetLength);
|
this.logger.debug("Received packet length: " + packetLength);
|
||||||
const timestamp = reader.uint(8);
|
const timestamp = reader.uint(8).toString();
|
||||||
this.logger.debug("Received timestamp: " + timestamp);
|
this.logger.debug("Received timestamp: " + timestamp);
|
||||||
if (reader.remaining() < packetLength || reader.remaining() < 5) return;
|
if (reader.remaining() < packetLength || reader.remaining() < 5) return;
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
const Bzip2 = require('compressjs').Bzip2,
|
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 {
|
class Valve extends Core {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -34,7 +41,7 @@ class Valve extends Core {
|
||||||
await this.cleanup(state);
|
await this.cleanup(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
async queryInfo(state) {
|
async queryInfo(/** Results */ state) {
|
||||||
this.debugLog("Requesting info ...");
|
this.debugLog("Requesting info ...");
|
||||||
const b = await this.sendPacket(
|
const b = await this.sendPacket(
|
||||||
0x54,
|
0x54,
|
||||||
|
@ -52,7 +59,7 @@ class Valve extends Core {
|
||||||
state.map = reader.string();
|
state.map = reader.string();
|
||||||
state.raw.folder = reader.string();
|
state.raw.folder = reader.string();
|
||||||
state.raw.game = 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.raw.numplayers = reader.uint(1);
|
||||||
state.maxplayers = reader.uint(1);
|
state.maxplayers = reader.uint(1);
|
||||||
|
|
||||||
|
@ -84,7 +91,7 @@ class Valve extends Core {
|
||||||
if(this.goldsrcInfo) {
|
if(this.goldsrcInfo) {
|
||||||
state.raw.numbots = reader.uint(1);
|
state.raw.numbots = reader.uint(1);
|
||||||
} else {
|
} else {
|
||||||
if(state.raw.folder === 'ship') {
|
if(state.raw.appId === AppId.Ship) {
|
||||||
state.raw.shipmode = reader.uint(1);
|
state.raw.shipmode = reader.uint(1);
|
||||||
state.raw.shipwitnesses = reader.uint(1);
|
state.raw.shipwitnesses = reader.uint(1);
|
||||||
state.raw.shipduration = reader.uint(1);
|
state.raw.shipduration = reader.uint(1);
|
||||||
|
@ -92,22 +99,28 @@ class Valve extends Core {
|
||||||
state.raw.version = reader.string();
|
state.raw.version = reader.string();
|
||||||
const extraFlag = reader.uint(1);
|
const extraFlag = reader.uint(1);
|
||||||
if(extraFlag & 0x80) state.gamePort = reader.uint(2);
|
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) {
|
if(extraFlag & 0x40) {
|
||||||
state.raw.sourcetvport = reader.uint(2);
|
state.raw.sourcetvport = reader.uint(2);
|
||||||
state.raw.sourcetvname = reader.string();
|
state.raw.sourcetvname = reader.string();
|
||||||
}
|
}
|
||||||
if(extraFlag & 0x20) state.raw.tags = 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
|
// from https://developer.valvesoftware.com/wiki/Server_queries
|
||||||
if(
|
if(
|
||||||
state.raw.protocol === 7 && (
|
state.raw.protocol === 7 && (
|
||||||
state.raw.steamappid === 215
|
state.raw.appId === 215
|
||||||
|| state.raw.steamappid === 17550
|
|| state.raw.appId === 17550
|
||||||
|| state.raw.steamappid === 17700
|
|| state.raw.appId === 17700
|
||||||
|| state.raw.steamappid === 240
|
|| state.raw.appId === 240
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
this._skipSizeInSplitHeader = true;
|
this._skipSizeInSplitHeader = true;
|
||||||
|
@ -133,7 +146,7 @@ class Valve extends Core {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async queryPlayers(state) {
|
async queryPlayers(/** Results */ state) {
|
||||||
state.raw.players = [];
|
state.raw.players = [];
|
||||||
|
|
||||||
this.debugLog("Requesting player list ...");
|
this.debugLog("Requesting player list ...");
|
||||||
|
@ -174,8 +187,18 @@ class Valve extends Core {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async queryRules(state) {
|
async queryRules(/** Results */ state) {
|
||||||
state.raw.rules = {};
|
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 ...");
|
this.debugLog("Requesting rules ...");
|
||||||
const b = await this.sendPacket(0x56,null,0x45,true);
|
const b = await this.sendPacket(0x56,null,0x45,true);
|
||||||
if (b === null) return; // timed out - the server probably has rules disabled
|
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++) {
|
for(let i = 0; i < num; i++) {
|
||||||
const key = reader.string();
|
const key = reader.string();
|
||||||
const value = reader.string();
|
const value = reader.string();
|
||||||
state.raw.rules[key] = value;
|
rules[key] = value;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanup(state) {
|
|
||||||
// Battalion 1944 puts its info into rules fields for some reason
|
// Battalion 1944 puts its info into rules fields for some reason
|
||||||
if ('bat_name_s' in state.raw.rules) {
|
if (appId === AppId.Bat1944) {
|
||||||
state.name = state.raw.rules.bat_name_s;
|
if ('bat_name_s' in rules) {
|
||||||
delete state.raw.rules.bat_name_s;
|
state.name = rules.bat_name_s;
|
||||||
if ('bat_player_count_s' in state.raw.rules) {
|
delete rules.bat_name_s;
|
||||||
state.raw.numplayers = parseInt(state.raw.rules.bat_player_count_s);
|
if ('bat_player_count_s' in rules) {
|
||||||
delete state.raw.rules.bat_player_count_s;
|
state.raw.numplayers = parseInt(rules.bat_player_count_s);
|
||||||
|
delete rules.bat_player_count_s;
|
||||||
}
|
}
|
||||||
if ('bat_max_players_i' in state.raw.rules) {
|
if ('bat_max_players_i' in rules) {
|
||||||
state.maxplayers = parseInt(state.raw.rules.bat_max_players_i);
|
state.maxplayers = parseInt(rules.bat_max_players_i);
|
||||||
delete state.raw.rules.bat_max_players_i;
|
delete rules.bat_max_players_i;
|
||||||
}
|
}
|
||||||
if ('bat_has_password_s' in state.raw.rules) {
|
if ('bat_has_password_s' in rules) {
|
||||||
state.password = state.raw.rules.bat_has_password_s === 'Y';
|
state.password = rules.bat_has_password_s === 'Y';
|
||||||
delete state.raw.rules.bat_has_password_s;
|
delete rules.bat_has_password_s;
|
||||||
}
|
}
|
||||||
// apparently map is already right, and this var is often wrong
|
// apparently map is already right, and this var is often wrong
|
||||||
delete state.raw.rules.bat_map_s;
|
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(/** Results */ state) {
|
||||||
// Organize players / hidden players into player / bot arrays
|
// Organize players / hidden players into player / bot arrays
|
||||||
const botProbability = (p) => {
|
const botProbability = (p) => {
|
||||||
if (p.time === -1) return Number.MAX_VALUE;
|
if (p.time === -1) return Number.MAX_VALUE;
|
||||||
|
|
Loading…
Reference in a new issue