Merge remote-tracking branch 'origin/master' into proto-discord

# Conflicts:
#	README.md
#	bin/gamedig.js
This commit is contained in:
Michael Morrison 2021-05-18 22:08:37 -05:00
commit 576062e88b
28 changed files with 1014 additions and 808 deletions

40
protocols/assettocorsa.js Normal file
View file

@ -0,0 +1,40 @@
const Core = require('./core');
class AssettoCorsa extends Core {
async run(state) {
const serverInfo = await this.request({
url: `http://${this.options.address}:${this.options.port}/INFO`,
responseType: 'json'
});
const carInfo = await this.request({
url: `http://${this.options.address}:${this.options.port}/JSON|${parseInt(Math.random() * 999999999999999, 10)}`,
responseType: 'json'
});
if (!serverInfo || !carInfo || !carInfo.Cars) {
throw new Error('Query not successful');
}
state.maxplayers = serverInfo.maxclients;
state.name = serverInfo.name;
state.map = serverInfo.track;
state.password = serverInfo.pass;
state.gamePort = serverInfo.port;
state.raw.carInfo = carInfo.Cars;
state.raw.serverInfo = serverInfo;
for (const car of carInfo.Cars) {
if (car.IsConnected) {
state.players.push({
name: car.DriverName,
car: car.Model,
skin: car.Skin,
nation: car.DriverNation,
team: car.DriverTeam
});
}
}
}
}
module.exports = AssettoCorsa;

View file

@ -4,7 +4,7 @@ const Core = require('./core'),
class BuildAndShoot extends Core {
async run(state) {
const body = await this.request({
uri: 'http://'+this.options.address+':'+this.options.port+'/',
url: 'http://'+this.options.address+':'+this.options.port+'/',
});
let m;

View file

@ -2,10 +2,11 @@ const EventEmitter = require('events').EventEmitter,
net = require('net'),
Reader = require('../lib/reader'),
HexUtil = require('../lib/HexUtil'),
requestAsync = require('request-promise'),
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;
@ -35,16 +36,17 @@ class Core extends EventEmitter {
}
this.logger.prefix = 'Q#' + (uid++);
this.logger.debug("Query is running with options:", this.options);
this.logger.debug("Starting");
this.logger.debug("Protocol: " + this.constructor.name);
this.logger.debug("Options:", this.options);
let abortCall = null;
this.abortedPromise = new Promise((resolve,reject) => {
abortCall = () => reject(new Error("Query is finished -- cancelling outstanding promises"));
}).catch(() => {
// Make sure that if this promise isn't attached to, it doesn't throw a unhandled promise rejection
});
// Make sure that if this promise isn't attached to, it doesn't throw a unhandled promise rejection
this.abortedPromise.catch(() => {});
let timeout;
try {
const promise = this.runOnce();
@ -73,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)
@ -129,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) {
@ -342,24 +313,26 @@ class Core extends EventEmitter {
}
}
async request(params) {
// If we haven't opened a raw tcp socket yet during this query, just open one and then immediately close it.
// This will give us a much more accurate RTT than using the rtt of the http request.
async tcpPing() {
// This will give a much more accurate RTT than using the rtt of an http request.
if (!this.usedTcp) {
await this.withTcp(() => {});
}
}
async request(params) {
await this.tcpPing();
let requestPromise;
try {
requestPromise = requestAsync({
requestPromise = got({
...params,
timeout: this.options.socketTimeout,
resolveWithFullResponse: true
timeout: this.options.socketTimeout
});
this.debugLog(log => {
log(() => params.uri + " HTTP-->");
log(() => params.url + " HTTP-->");
requestPromise
.then((response) => log(params.uri + " <--HTTP " + response.statusCode))
.then((response) => log(params.url + " <--HTTP " + response.statusCode))
.catch(() => {});
});
const wrappedPromise = requestPromise.then(response => {

View file

@ -10,7 +10,6 @@ class Ffow extends Valve {
this.debugLog("Requesting ffow info ...");
const b = await this.sendPacket(
0x46,
false,
'LSQ',
0x49
);

View file

@ -12,20 +12,19 @@ class FiveM extends Quake2 {
await super.run(state);
{
const raw = await this.request({
uri: 'http://' + this.options.address + ':' + this.options.port + '/info.json'
const json = await this.request({
url: 'http://' + this.options.address + ':' + this.options.port + '/info.json',
responseType: 'json'
});
const json = JSON.parse(raw);
state.raw.info = json;
}
{
const raw = await this.request({
uri: 'http://' + this.options.address + ':' + this.options.port + '/players.json'
const json = await this.request({
url: 'http://' + this.options.address + ':' + this.options.port + '/players.json',
responseType: 'json'
});
const json = JSON.parse(raw);
state.raw.players = json;
state.players = [];
for (const player of json) {
state.players.push({name: player.name, ping: player.ping});
}

View file

@ -1,5 +1,33 @@
const Core = require('./core');
const stringKeys = new Set([
'website',
'gametype',
'gamemode',
'player'
]);
function normalizeEntry([key,value]) {
key = key.toLowerCase();
const split = key.split('_');
let keyType;
if (split.length === 2 && !isNaN(parseInt(split[1]))) {
keyType = split[0];
} else {
keyType = key;
}
if (!stringKeys.has(keyType) && !keyType.includes('name')) {
if (value.toLowerCase() === 'true') {
value = true;
} else if (value.toLowerCase() === 'false') {
value = false;
} else if (!isNaN(parseInt(value))) {
value = parseInt(value);
}
}
return [key,value];
}
class Gamespy1 extends Core {
constructor() {
super();
@ -8,89 +36,80 @@ class Gamespy1 extends Core {
}
async run(state) {
{
const data = await this.sendPacket('info');
state.raw = data;
if ('hostname' in state.raw) state.name = state.raw.hostname;
if ('mapname' in state.raw) state.map = state.raw.mapname;
if (this.trueTest(state.raw.password)) 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);
}
{
const data = await this.sendPacket('rules');
state.raw.rules = data;
}
{
const data = await this.sendPacket('players');
const playersById = {};
const teamNamesById = {};
for (const ident of Object.keys(data)) {
const split = ident.split('_');
let key = split[0];
const id = split[1];
let value = data[ident];
const raw = await this.sendPacket('\\status\\xserverquery');
// Convert all keys to lowercase and normalize value types
const data = Object.fromEntries(Object.entries(raw).map(entry => normalizeEntry(entry)));
state.raw = data;
if ('hostname' in data) state.name = data.hostname;
if ('mapname' in data) state.map = data.mapname;
if (this.trueTest(data.password)) state.password = true;
if ('maxplayers' in data) state.maxplayers = parseInt(data.maxplayers);
if ('hostport' in data) state.gamePort = parseInt(data.hostport);
const teamOffByOne = data.gameid === 'bf1942';
const playersById = {};
const teamNamesById = {};
for (const ident of Object.keys(data)) {
const split = ident.split('_');
if (split.length !== 2) continue;
let key = split[0].toLowerCase();
const id = parseInt(split[1]);
if (isNaN(id)) continue;
let value = data[ident];
delete data[ident];
if (key !== 'team' && key.startsWith('team')) {
// Info about a team
if (key === 'teamname') {
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);
playersById[id][key] = value;
// other team info which we don't track
}
} else {
// Info about a player
if (!(id in playersById)) playersById[id] = {};
if (key === 'playername' || key === 'player') {
key = 'name';
}
if (key === 'team' && !isNaN(parseInt(value))) {
key = 'teamId';
value = parseInt(value) + (teamOffByOne ? -1 : 0);
}
if (key !== 'name' && !isNaN(parseInt(value))) {
value = parseInt(value);
}
playersById[id][key] = value;
}
state.raw.teams = teamNamesById;
}
state.raw.teams = teamNamesById;
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) {
const seenHashes = new Set();
for (const player of players) {
// Some servers (bf1942) report the same player multiple times (bug?)
// Ignore these duplicates
if (player.keyhash) {
if (seenHashes.has(player.keyhash)) {
this.logger.debug("Rejected player with hash " + player.keyhash + " (Duplicate keyhash)");
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;
seenHashes.add(player.keyhash);
}
}
this.logger.debug(log => {
if (specTeamId === null) {
log("Could not detect a team ID for spectators");
// Convert player's team ID to team name if possible
if (player.hasOwnProperty('teamId')) {
if (Object.keys(teamNamesById).length) {
player.team = teamNamesById[player.teamId] || '';
} else {
log("Detected that team ID " + specTeamId + " is probably for spectators");
player.team = player.teamId;
delete player.teamId;
}
});
const seenHashes = new Set();
for (const player of players) {
// Some servers (bf1942) report the same player multiple times (bug?)
// Ignore these duplicates
if (player.keyhash) {
if (seenHashes.has(player.keyhash)) {
this.logger.debug("Rejected player with hash " + player.keyhash + " (Duplicate keyhash)");
continue;
} else {
seenHashes.add(player.keyhash);
}
}
// 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";
}
}
state.players.push(player);
}
state.players.push(player);
}
}
@ -100,7 +119,7 @@ class Gamespy1 extends Core {
const parts = new Set();
let maxPartNum = 0;
return await this.udpSend('\\'+type+'\\', buffer => {
return await this.udpSend(type, buffer => {
const reader = this.reader(buffer);
const str = reader.string(buffer.length);
const split = str.split('\\');

View file

@ -29,7 +29,9 @@ class Gamespy2 extends Core {
{
const body = await this.sendPacket([0, 0xff, 0]);
const reader = this.reader(body);
state.players = this.readFieldData(reader);
for (const rawPlayer of this.readFieldData(reader)) {
state.players.push(rawPlayer);
}
}
// Parse teams

View file

@ -148,9 +148,15 @@ class Gamespy3 extends Core {
return await this.udpSend(b,(buffer) => {
const reader = this.reader(buffer);
const iType = reader.uint(1);
if(iType !== type) return;
if(iType !== type) {
this.logger.debug('Skipping packet, type mismatch');
return;
}
const iSessionId = reader.uint(4);
if(iSessionId !== this.sessionId) return;
if(iSessionId !== this.sessionId) {
this.logger.debug('Skipping packet, session id mismatch');
return;
}
if(!assemble) {
return reader.rest();

View file

@ -2,8 +2,10 @@ const Core = require('./core');
class GeneShift extends Core {
async run(state) {
await this.tcpPing();
const body = await this.request({
uri: 'http://geneshift.net/game/receiveLobby.php'
url: 'http://geneshift.net/game/receiveLobby.php'
});
const split = body.split('<br/>');

View file

@ -2,11 +2,11 @@ const Core = require('./core');
class Kspdmp extends Core {
async run(state) {
const body = await this.request({
uri: 'http://'+this.options.address+':'+this.options.port
const json = await this.request({
url: 'http://'+this.options.address+':'+this.options.port,
responseType: 'json'
});
const json = JSON.parse(body);
for (const one of json.players) {
state.players.push({name:one.nickname,team:one.team});
}

View file

@ -1,7 +1,16 @@
const Core = require('./core'),
MinecraftVanilla = require('./minecraftvanilla'),
MinecraftBedrock = require('./minecraftbedrock'),
Gamespy3 = require('./gamespy3');
/*
Vanilla servers respond to minecraftvanilla only
Some modded vanilla servers respond to minecraftvanilla and gamespy3, or gamespy3 only
Some bedrock servers respond to gamespy3 only
Some bedrock servers respond to minecraftbedrock only
Unsure if any bedrock servers respond to gamespy3 and minecraftbedrock
*/
class Minecraft extends Core {
constructor() {
super();
@ -17,25 +26,40 @@ class Minecraft extends Core {
try { return await vanillaResolver.runOnceSafe(); } catch(e) {}
})());
const bedrockResolver = new Gamespy3();
bedrockResolver.options = {
const gamespyResolver = new Gamespy3();
gamespyResolver.options = {
...this.options,
encoding: 'utf8',
};
gamespyResolver.udpSocket = this.udpSocket;
promises.push((async () => {
try { return await gamespyResolver.runOnceSafe(); } catch(e) {}
})());
const bedrockResolver = new MinecraftBedrock();
bedrockResolver.options = this.options;
bedrockResolver.udpSocket = this.udpSocket;
promises.push((async () => {
try { return await bedrockResolver.runOnceSafe(); } catch(e) {}
})());
const [ vanillaState, bedrockState ] = await Promise.all(promises);
const [ vanillaState, gamespyState, bedrockState ] = await Promise.all(promises);
state.raw.vanilla = vanillaState;
state.raw.gamespy = gamespyState;
state.raw.bedrock = bedrockState;
if (!vanillaState && !bedrockState) {
if (!vanillaState && !gamespyState && !bedrockState) {
throw new Error('No protocols succeeded');
}
// Ordered from least worth to most worth (player names / etc)
if (bedrockState) {
if (bedrockState.name) state.name = bedrockState.name;
if (bedrockState.maxplayers) state.maxplayers = bedrockState.maxplayers;
if (bedrockState.players) state.players = bedrockState.players;
if (bedrockState.map) state.map = bedrockState.map;
}
if (vanillaState) {
try {
let name = '';
@ -54,10 +78,11 @@ class Minecraft extends Core {
if (vanillaState.maxplayers) state.maxplayers = vanillaState.maxplayers;
if (vanillaState.players) state.players = vanillaState.players;
}
if (bedrockState) {
if (bedrockState.name) state.name = bedrockState.name;
if (bedrockState.maxplayers) state.maxplayers = bedrockState.maxplayers;
if (bedrockState.players) state.players = bedrockState.players;
if (gamespyState) {
if (gamespyState.name) state.name = gamespyState.name;
if (gamespyState.maxplayers) state.maxplayers = gamespyState.maxplayers;
if (gamespyState.players.length) state.players = gamespyState.players;
else if (gamespyState.raw.numplayers) state.players = parseInt(gamespyState.raw.numplayers);
}
// remove dupe spaces from name
state.name = state.name.replace(/\s+/g, ' ');

View file

@ -0,0 +1,76 @@
const Core = require('./core');
class MinecraftBedrock extends Core {
constructor() {
super();
this.byteorder = 'be';
}
async run(state) {
const bufs = [
Buffer.from([0x01]), // Message ID, ID_UNCONNECTED_PING
Buffer.from('1122334455667788', 'hex'), // Nonce / timestamp
Buffer.from('00ffff00fefefefefdfdfdfd12345678', 'hex'), // Magic
Buffer.from('0000000000000000', 'hex') // Cliend GUID
];
return await this.udpSend(Buffer.concat(bufs), buffer => {
const reader = this.reader(buffer);
const messageId = reader.uint(1);
if (messageId !== 0x1c) {
this.logger.debug('Skipping packet, invalid message id');
return;
}
const nonce = reader.part(8).toString('hex'); // should match the nonce we sent
this.logger.debug('Nonce: ' + nonce);
if (nonce !== '1122334455667788') {
this.logger.debug('Skipping packet, invalid nonce');
return;
}
// These 8 bytes are identical to the serverId string we receive in decimal below
reader.skip(8);
const magic = reader.part(16).toString('hex');
this.logger.debug('Magic value: ' + magic);
if (magic !== '00ffff00fefefefefdfdfdfd12345678') {
this.logger.debug('Skipping packet, invalid magic');
return;
}
const statusLen = reader.uint(2);
if (reader.remaining() !== statusLen) {
throw new Error('Invalid status length: ' + reader.remaining() + ' vs ' + statusLen);
}
const statusStr = reader.rest().toString('utf8');
this.logger.debug('Raw status str: ' + statusStr);
const split = statusStr.split(';');
if (split.length < 6) {
throw new Error('Missing enough chunks in status str');
}
state.raw.edition = split.shift();
state.name = split.shift();
state.raw.protocolVersion = split.shift();
state.raw.mcVersion = split.shift();
state.players = parseInt(split.shift());
state.maxplayers = parseInt(split.shift());
if (split.length) state.raw.serverId = split.shift();
if (split.length) state.map = split.shift();
if (split.length) state.raw.gameMode = split.shift();
if (split.length) state.raw.nintendoOnly = !!parseInt(split.shift());
if (split.length) state.raw.ipv4Port = split.shift();
if (split.length) state.raw.ipv6Port = split.shift();
return true;
});
}
}
module.exports = MinecraftBedrock;

View file

@ -47,6 +47,7 @@ class MinecraftVanilla extends Core {
state.raw = json;
state.maxplayers = json.players.max;
if(json.players.sample) {
for(const player of json.players.sample) {
state.players.push({
@ -55,7 +56,11 @@ class MinecraftVanilla extends Core {
});
}
}
for (let i = 0; i < Math.min(json.players.online, 10000); i++) {
// players.sample may not contain all players or no players at all, depending on how many players are online.
// Insert a dummy player object for every online player that is not listed in players.sample.
// Limit player amount to 10.000 players for performance reasons.
for (let i = state.players.length; i < Math.min(json.players.online, 10000); i++) {
state.players.push({});
}
}

View file

@ -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);

31
protocols/savage2.js Normal file
View file

@ -0,0 +1,31 @@
const Core = require('./core');
class Savage2 extends Core {
constructor() {
super();
}
async run(state) {
const buffer = await this.udpSend('\x01',b => b);
const reader = this.reader(buffer);
reader.skip(12);
state.name = this.stripColorCodes(reader.string());
state.players = reader.uint(1);
state.maxplayers = reader.uint(1);
state.raw.time = reader.string();
state.map = reader.string();
state.raw.nextmap = reader.string();
state.raw.location = reader.string();
state.raw.minplayers = reader.uint(1);
state.raw.gametype = reader.string();
state.raw.version = reader.string();
state.raw.minlevel = reader.uint(1);
}
stripColorCodes(str) {
return str.replace(/\^./g,'');
}
}
module.exports = Savage2;

View file

@ -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;

View file

@ -2,15 +2,15 @@ const Core = require('./core');
class Terraria extends Core {
async run(state) {
const body = await this.request({
uri: 'http://'+this.options.address+':'+this.options.port+'/v2/server/status',
qs: {
const json = await this.request({
url: 'http://'+this.options.address+':'+this.options.port+'/v2/server/status',
searchParams: {
players: 'true',
token: this.options.token
}
},
responseType: 'json'
});
const json = JSON.parse(body);
if(json.status !== '200') throw new Error('Invalid status');
for (const one of json.players) {

View file

@ -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,11 +41,10 @@ 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,
false,
'Source Engine Query\0',
this.goldsrcInfo ? 0x6D : 0x49,
false
@ -53,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);
@ -85,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);
@ -93,30 +99,35 @@ 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;
}
this.debugLog("STEAM APPID: "+state.raw.steamappid);
this.debugLog("PROTOCOL: "+state.raw.protocol);
this.logger.debug("INFO: ", state.raw);
if(state.raw.protocol === 48) {
this.debugLog("GOLDSRC DETECTED - USING MODIFIED SPLIT FORMAT");
this.logger.debug("GOLDSRC DETECTED - USING MODIFIED SPLIT FORMAT");
this.goldsrcSplits = true;
}
}
@ -128,7 +139,6 @@ class Valve extends Core {
this.debugLog("Requesting legacy challenge key ...");
await this.sendPacket(
0x57,
false,
null,
0x41,
false
@ -136,22 +146,24 @@ class Valve extends Core {
}
}
async queryPlayers(state) {
async queryPlayers(/** Results */ state) {
state.raw.players = [];
// CSGO doesn't even respond sometimes if host_players_show is not 2
// Ignore timeouts in only this case
const allowTimeout = state.raw.steamappid === 730;
this.debugLog("Requesting player list ...");
const b = await this.sendPacket(
0x55,
true,
null,
0x44,
allowTimeout
true
);
if (b === null) return; // timed out
if (b === null) {
// Player query timed out
// CSGO doesn't respond to player query if host_players_show is not 2
// Conan Exiles never responds to player query
// Just skip it, and we'll fill with dummy objects in cleanup()
return;
}
const reader = this.reader(b);
const num = reader.uint(1);
@ -175,42 +187,61 @@ 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,true,null,0x45,true);
if (b === null) return; // timed out - the server probably just has rules disabled
const b = await this.sendPacket(0x56,null,0x45,true);
if (b === null) return; // timed out - the server probably has rules disabled
const reader = this.reader(b);
const num = reader.uint(2);
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;
@ -243,33 +274,29 @@ class Valve extends Core {
**/
async sendPacket(
type,
sendChallenge,
payload,
expect,
allowTimeout
) {
for (let keyRetry = 0; keyRetry < 3; keyRetry++) {
let requestKeyChanged = false;
let receivedNewChallengeKey = false;
const response = await this.sendPacketRaw(
type, sendChallenge, payload,
type, payload,
(payload) => {
const reader = this.reader(payload);
const type = reader.uint(1);
this.debugLog(() => "Received " + type.toString(16) + " expected " + expect.toString(16));
this.debugLog(() => "Received 0x" + type.toString(16) + " expected 0x" + expect.toString(16));
if (type === 0x41) {
const key = reader.uint(4);
if (this._challenge !== key) {
this.debugLog('Received new challenge key: ' + key);
this.debugLog('Received new challenge key: 0x' + key.toString(16));
this._challenge = key;
if (sendChallenge) {
this.debugLog('Challenge key changed -- allowing query retry if needed');
requestKeyChanged = true;
}
receivedNewChallengeKey = true;
}
}
if (type === expect) {
return reader.rest();
} else if (requestKeyChanged) {
} else if (receivedNewChallengeKey) {
return null;
}
},
@ -277,7 +304,7 @@ class Valve extends Core {
if (allowTimeout) return null;
}
);
if (!requestKeyChanged) {
if (!receivedNewChallengeKey) {
return response;
}
}
@ -294,26 +321,47 @@ class Valve extends Core {
**/
async sendPacketRaw(
type,
sendChallenge,
payload,
onResponse,
onTimeout
) {
const challengeAtBeginning = type === 0x55 || type === 0x56;
const challengeAtEnd = type === 0x54 && !!this._challenge;
if (typeof payload === 'string') payload = Buffer.from(payload, 'binary');
const challengeLength = sendChallenge ? 4 : 0;
const payloadLength = payload ? payload.length : 0;
const b = Buffer.alloc(5 + challengeLength + payloadLength);
b.writeInt32LE(-1, 0);
b.writeUInt8(type, 4);
const b = Buffer.alloc(5
+ (challengeAtBeginning ? 4 : 0)
+ (challengeAtEnd ? 4 : 0)
+ (payload ? payload.length : 0)
);
let offset = 0;
if (sendChallenge) {
let challenge = this._challenge;
if (!challenge) challenge = 0xffffffff;
if (this.byteorder === 'le') b.writeUInt32LE(challenge, 5);
else b.writeUInt32BE(challenge, 5);
let challenge = this._challenge;
if (!challenge) challenge = 0xffffffff;
b.writeInt32LE(-1, offset);
offset += 4;
b.writeUInt8(type, offset);
offset += 1;
if (challengeAtBeginning) {
if (this.byteorder === 'le') b.writeUInt32LE(challenge, offset);
else b.writeUInt32BE(challenge, offset);
offset += 4;
}
if (payload) {
payload.copy(b, offset);
offset += payload.length;
}
if (challengeAtEnd) {
if (this.byteorder === 'le') b.writeUInt32LE(challenge, offset);
else b.writeUInt32BE(challenge, offset);
offset += 4;
}
if (payloadLength) payload.copy(b, 5 + challengeLength);
const packetStorage = {};
return await this.udpSend(
@ -351,7 +399,7 @@ class Valve extends Core {
packets[packetNum] = payload;
this.debugLog(() => "Received partial packet uid:"+uid+" num:"+packetNum);
this.debugLog(() => "Received partial packet uid: 0x"+uid.toString(16)+" num: "+packetNum);
this.debugLog(() => "Received "+Object.keys(packets).length+'/'+numPackets+" packets for this UID");
if(Object.keys(packets).length !== numPackets) return;