mirror of
https://github.com/tribufu/node-gamedig
synced 2026-05-18 09:35:50 +00:00
Merge remote-tracking branch 'origin/master' into proto-discord
# Conflicts: # README.md # bin/gamedig.js
This commit is contained in:
commit
576062e88b
28 changed files with 1014 additions and 808 deletions
40
protocols/assettocorsa.js
Normal file
40
protocols/assettocorsa.js
Normal 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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ class Ffow extends Valve {
|
|||
this.debugLog("Requesting ffow info ...");
|
||||
const b = await this.sendPacket(
|
||||
0x46,
|
||||
false,
|
||||
'LSQ',
|
||||
0x49
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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('\\');
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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/>');
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, ' ');
|
||||
|
|
|
|||
76
protocols/minecraftbedrock.js
Normal file
76
protocols/minecraftbedrock.js
Normal 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;
|
||||
|
|
@ -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({});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
31
protocols/savage2.js
Normal 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;
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue