chore: Convert all files to LF endings (#400)

* Convert to LF?

* Modify gitattributes

* Force LF

* Git --renormalize

* Update .gitattributes to enforce eol=lf

* Redo CRLF -> LF on remaining files
This commit is contained in:
CosminPerRam 2023-11-12 13:14:43 +02:00 committed by GitHub
parent a8bc7521f6
commit cee42e7a88
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 5697 additions and 5697 deletions

View file

@ -1,65 +1,65 @@
import Core from './core.js'
export default class armagetron extends Core {
constructor () {
super()
this.encoding = 'latin1'
this.byteorder = 'be'
}
async run (state) {
const b = Buffer.from([0, 0x35, 0, 0, 0, 0, 0, 0x11])
const buffer = await this.udpSend(b, b => b)
const reader = this.reader(buffer)
reader.skip(6)
state.gamePort = this.readUInt(reader)
state.raw.hostname = this.readString(reader)
state.name = this.stripColorCodes(this.readString(reader))
state.numplayers = this.readUInt(reader)
state.raw.versionmin = this.readUInt(reader)
state.raw.versionmax = this.readUInt(reader)
state.raw.version = this.readString(reader)
state.maxplayers = this.readUInt(reader)
const players = this.readString(reader)
const list = players.split('\n')
for (const name of list) {
if (!name) continue
state.players.push({
name: this.stripColorCodes(name)
})
}
state.raw.options = this.stripColorCodes(this.readString(reader))
state.raw.uri = this.readString(reader)
state.raw.globalids = this.readString(reader)
}
readUInt (reader) {
const a = reader.uint(2)
const b = reader.uint(2)
return (b << 16) + a
}
readString (reader) {
const len = reader.uint(2)
if (!len) return ''
let out = ''
for (let i = 0; i < len; i += 2) {
const hi = reader.uint(1)
const lo = reader.uint(1)
if (i + 1 < len) out += String.fromCharCode(lo)
if (i + 2 < len) out += String.fromCharCode(hi)
}
return out
}
stripColorCodes (str) {
return str.replace(/0x[0-9a-f]{6}/g, '')
}
}
import Core from './core.js'
export default class armagetron extends Core {
constructor () {
super()
this.encoding = 'latin1'
this.byteorder = 'be'
}
async run (state) {
const b = Buffer.from([0, 0x35, 0, 0, 0, 0, 0, 0x11])
const buffer = await this.udpSend(b, b => b)
const reader = this.reader(buffer)
reader.skip(6)
state.gamePort = this.readUInt(reader)
state.raw.hostname = this.readString(reader)
state.name = this.stripColorCodes(this.readString(reader))
state.numplayers = this.readUInt(reader)
state.raw.versionmin = this.readUInt(reader)
state.raw.versionmax = this.readUInt(reader)
state.raw.version = this.readString(reader)
state.maxplayers = this.readUInt(reader)
const players = this.readString(reader)
const list = players.split('\n')
for (const name of list) {
if (!name) continue
state.players.push({
name: this.stripColorCodes(name)
})
}
state.raw.options = this.stripColorCodes(this.readString(reader))
state.raw.uri = this.readString(reader)
state.raw.globalids = this.readString(reader)
}
readUInt (reader) {
const a = reader.uint(2)
const b = reader.uint(2)
return (b << 16) + a
}
readString (reader) {
const len = reader.uint(2)
if (!len) return ''
let out = ''
for (let i = 0; i < len; i += 2) {
const hi = reader.uint(1)
const lo = reader.uint(1)
if (i + 1 < len) out += String.fromCharCode(lo)
if (i + 2 < len) out += String.fromCharCode(hi)
}
return out
}
stripColorCodes (str) {
return str.replace(/0x[0-9a-f]{6}/g, '')
}
}

View file

@ -1,45 +1,45 @@
import Core from './core.js'
export default class ase extends Core {
async run (state) {
const buffer = await this.udpSend('s', (buffer) => {
const reader = this.reader(buffer)
const header = reader.string(4)
if (header === 'EYE1') return reader.rest()
})
const reader = this.reader(buffer)
state.raw.gamename = this.readString(reader)
state.gamePort = parseInt(this.readString(reader))
state.name = this.readString(reader)
state.raw.gametype = this.readString(reader)
state.map = this.readString(reader)
state.raw.version = this.readString(reader)
state.password = this.readString(reader) === '1'
state.numplayers = parseInt(this.readString(reader))
state.maxplayers = parseInt(this.readString(reader))
while (!reader.done()) {
const key = this.readString(reader)
if (!key) break
const value = this.readString(reader)
state.raw[key] = value
}
while (!reader.done()) {
const flags = reader.uint(1)
const player = {}
if (flags & 1) player.name = this.readString(reader)
if (flags & 2) player.team = this.readString(reader)
if (flags & 4) player.skin = this.readString(reader)
if (flags & 8) player.score = parseInt(this.readString(reader))
if (flags & 16) player.ping = parseInt(this.readString(reader))
if (flags & 32) player.time = parseInt(this.readString(reader))
state.players.push(player)
}
}
readString (reader) {
return reader.pascalString(1, -1)
}
}
import Core from './core.js'
export default class ase extends Core {
async run (state) {
const buffer = await this.udpSend('s', (buffer) => {
const reader = this.reader(buffer)
const header = reader.string(4)
if (header === 'EYE1') return reader.rest()
})
const reader = this.reader(buffer)
state.raw.gamename = this.readString(reader)
state.gamePort = parseInt(this.readString(reader))
state.name = this.readString(reader)
state.raw.gametype = this.readString(reader)
state.map = this.readString(reader)
state.raw.version = this.readString(reader)
state.password = this.readString(reader) === '1'
state.numplayers = parseInt(this.readString(reader))
state.maxplayers = parseInt(this.readString(reader))
while (!reader.done()) {
const key = this.readString(reader)
if (!key) break
const value = this.readString(reader)
state.raw[key] = value
}
while (!reader.done()) {
const flags = reader.uint(1)
const player = {}
if (flags & 1) player.name = this.readString(reader)
if (flags & 2) player.team = this.readString(reader)
if (flags & 4) player.skin = this.readString(reader)
if (flags & 8) player.score = parseInt(this.readString(reader))
if (flags & 16) player.ping = parseInt(this.readString(reader))
if (flags & 32) player.time = parseInt(this.readString(reader))
state.players.push(player)
}
}
readString (reader) {
return reader.pascalString(1, -1)
}
}

View file

@ -1,40 +1,40 @@
import Core from './core.js'
export default 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
})
}
}
state.numplayers = carInfo.Cars.length
}
}
import Core from './core.js'
export default 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
})
}
}
state.numplayers = carInfo.Cars.length
}
}

View file

@ -1,162 +1,162 @@
import Core from './core.js'
export default class battlefield extends Core {
constructor () {
super()
this.encoding = 'latin1'
}
async run (state) {
await this.withTcp(async socket => {
{
const data = await this.query(socket, ['serverInfo'])
state.name = data.shift()
state.numplayers = parseInt(data.shift())
state.maxplayers = parseInt(data.shift())
state.raw.gametype = data.shift()
state.map = data.shift()
state.raw.roundsplayed = parseInt(data.shift())
state.raw.roundstotal = parseInt(data.shift())
const teamCount = data.shift()
state.raw.teams = []
for (let i = 0; i < teamCount; i++) {
const tickets = parseFloat(data.shift())
state.raw.teams.push({
tickets
})
}
state.raw.targetscore = parseInt(data.shift())
state.raw.status = data.shift()
// Seems like the fields end at random places beyond this point
// depending on the server version
if (data.length) state.raw.ranked = (data.shift() === 'true')
if (data.length) state.raw.punkbuster = (data.shift() === 'true')
if (data.length) state.password = (data.shift() === 'true')
if (data.length) state.raw.uptime = parseInt(data.shift())
if (data.length) state.raw.roundtime = parseInt(data.shift())
const isBadCompany2 = data[0] === 'BC2'
if (isBadCompany2) {
if (data.length) data.shift()
if (data.length) data.shift()
}
if (data.length) {
state.raw.ip = data.shift()
const split = state.raw.ip.split(':')
state.gameHost = split[0]
state.gamePort = split[1]
} else {
// best guess if the server doesn't tell us what the server port is
// these are just the default game ports for different default query ports
if (this.options.port === 48888) state.gamePort = 7673
if (this.options.port === 22000) state.gamePort = 25200
}
if (data.length) state.raw.punkbusterversion = data.shift()
if (data.length) state.raw.joinqueue = (data.shift() === 'true')
if (data.length) state.raw.region = data.shift()
if (data.length) state.raw.pingsite = data.shift()
if (data.length) state.raw.country = data.shift()
if (data.length) state.raw.quickmatch = (data.shift() === 'true')
}
{
const data = await this.query(socket, ['version'])
data.shift()
state.raw.version = data.shift()
}
{
const data = await this.query(socket, ['listPlayers', 'all'])
const fieldCount = parseInt(data.shift())
const fields = []
for (let i = 0; i < fieldCount; i++) {
fields.push(data.shift())
}
const numplayers = data.shift()
for (let i = 0; i < numplayers; i++) {
const player = {}
for (let key of fields) {
let value = data.shift()
if (key === 'teamId') key = 'team'
else if (key === 'squadId') key = 'squad'
if (
key === 'kills' ||
key === 'deaths' ||
key === 'score' ||
key === 'rank' ||
key === 'team' ||
key === 'squad' ||
key === 'ping' ||
key === 'type'
) {
value = parseInt(value)
}
player[key] = value
}
state.players.push(player)
}
}
})
}
async query (socket, params) {
const outPacket = this.buildPacket(params)
return await this.tcpSend(socket, outPacket, (data) => {
const decoded = this.decodePacket(data)
if (decoded) {
this.logger.debug(decoded)
if (decoded.shift() !== 'OK') throw new Error('Missing OK')
return decoded
}
})
}
buildPacket (params) {
const paramBuffers = []
for (const param of params) {
paramBuffers.push(Buffer.from(param, 'utf8'))
}
let totalLength = 12
for (const paramBuffer of paramBuffers) {
totalLength += paramBuffer.length + 1 + 4
}
const b = Buffer.alloc(totalLength)
b.writeUInt32LE(0, 0)
b.writeUInt32LE(totalLength, 4)
b.writeUInt32LE(params.length, 8)
let offset = 12
for (const paramBuffer of paramBuffers) {
b.writeUInt32LE(paramBuffer.length, offset); offset += 4
paramBuffer.copy(b, offset); offset += paramBuffer.length
b.writeUInt8(0, offset); offset += 1
}
return b
}
decodePacket (buffer) {
if (buffer.length < 8) return false
const reader = this.reader(buffer)
reader.uint(4) // header
const totalLength = reader.uint(4)
if (buffer.length < totalLength) return false
this.logger.debug('Expected ' + totalLength + ' bytes, have ' + buffer.length)
const paramCount = reader.uint(4)
const params = []
for (let i = 0; i < paramCount; i++) {
params.push(reader.pascalString(4))
reader.uint(1) // strNull
}
return params
}
}
import Core from './core.js'
export default class battlefield extends Core {
constructor () {
super()
this.encoding = 'latin1'
}
async run (state) {
await this.withTcp(async socket => {
{
const data = await this.query(socket, ['serverInfo'])
state.name = data.shift()
state.numplayers = parseInt(data.shift())
state.maxplayers = parseInt(data.shift())
state.raw.gametype = data.shift()
state.map = data.shift()
state.raw.roundsplayed = parseInt(data.shift())
state.raw.roundstotal = parseInt(data.shift())
const teamCount = data.shift()
state.raw.teams = []
for (let i = 0; i < teamCount; i++) {
const tickets = parseFloat(data.shift())
state.raw.teams.push({
tickets
})
}
state.raw.targetscore = parseInt(data.shift())
state.raw.status = data.shift()
// Seems like the fields end at random places beyond this point
// depending on the server version
if (data.length) state.raw.ranked = (data.shift() === 'true')
if (data.length) state.raw.punkbuster = (data.shift() === 'true')
if (data.length) state.password = (data.shift() === 'true')
if (data.length) state.raw.uptime = parseInt(data.shift())
if (data.length) state.raw.roundtime = parseInt(data.shift())
const isBadCompany2 = data[0] === 'BC2'
if (isBadCompany2) {
if (data.length) data.shift()
if (data.length) data.shift()
}
if (data.length) {
state.raw.ip = data.shift()
const split = state.raw.ip.split(':')
state.gameHost = split[0]
state.gamePort = split[1]
} else {
// best guess if the server doesn't tell us what the server port is
// these are just the default game ports for different default query ports
if (this.options.port === 48888) state.gamePort = 7673
if (this.options.port === 22000) state.gamePort = 25200
}
if (data.length) state.raw.punkbusterversion = data.shift()
if (data.length) state.raw.joinqueue = (data.shift() === 'true')
if (data.length) state.raw.region = data.shift()
if (data.length) state.raw.pingsite = data.shift()
if (data.length) state.raw.country = data.shift()
if (data.length) state.raw.quickmatch = (data.shift() === 'true')
}
{
const data = await this.query(socket, ['version'])
data.shift()
state.raw.version = data.shift()
}
{
const data = await this.query(socket, ['listPlayers', 'all'])
const fieldCount = parseInt(data.shift())
const fields = []
for (let i = 0; i < fieldCount; i++) {
fields.push(data.shift())
}
const numplayers = data.shift()
for (let i = 0; i < numplayers; i++) {
const player = {}
for (let key of fields) {
let value = data.shift()
if (key === 'teamId') key = 'team'
else if (key === 'squadId') key = 'squad'
if (
key === 'kills' ||
key === 'deaths' ||
key === 'score' ||
key === 'rank' ||
key === 'team' ||
key === 'squad' ||
key === 'ping' ||
key === 'type'
) {
value = parseInt(value)
}
player[key] = value
}
state.players.push(player)
}
}
})
}
async query (socket, params) {
const outPacket = this.buildPacket(params)
return await this.tcpSend(socket, outPacket, (data) => {
const decoded = this.decodePacket(data)
if (decoded) {
this.logger.debug(decoded)
if (decoded.shift() !== 'OK') throw new Error('Missing OK')
return decoded
}
})
}
buildPacket (params) {
const paramBuffers = []
for (const param of params) {
paramBuffers.push(Buffer.from(param, 'utf8'))
}
let totalLength = 12
for (const paramBuffer of paramBuffers) {
totalLength += paramBuffer.length + 1 + 4
}
const b = Buffer.alloc(totalLength)
b.writeUInt32LE(0, 0)
b.writeUInt32LE(totalLength, 4)
b.writeUInt32LE(params.length, 8)
let offset = 12
for (const paramBuffer of paramBuffers) {
b.writeUInt32LE(paramBuffer.length, offset); offset += 4
paramBuffer.copy(b, offset); offset += paramBuffer.length
b.writeUInt8(0, offset); offset += 1
}
return b
}
decodePacket (buffer) {
if (buffer.length < 8) return false
const reader = this.reader(buffer)
reader.uint(4) // header
const totalLength = reader.uint(4)
if (buffer.length < totalLength) return false
this.logger.debug('Expected ' + totalLength + ' bytes, have ' + buffer.length)
const paramCount = reader.uint(4)
const params = []
for (let i = 0; i < paramCount; i++) {
params.push(reader.pascalString(4))
reader.uint(1) // strNull
}
return params
}
}

View file

@ -1,55 +1,55 @@
import Core from './core.js'
import * as cheerio from 'cheerio'
export default class buildandshoot extends Core {
async run (state) {
const body = await this.request({
url: 'http://' + this.options.address + ':' + this.options.port + '/'
})
let m
m = body.match(/status server for (.*?)\.?[\r\n]/)
if (m) state.name = m[1]
m = body.match(/Current uptime: (\d+)/)
if (m) state.raw.uptime = m[1]
m = body.match(/currently running (.*?) by /)
if (m) state.map = m[1]
m = body.match(/Current players: (\d+)\/(\d+)/)
if (m) {
state.numplayers = parseInt(m[1])
state.maxplayers = m[2]
}
m = body.match(/aos:\/\/[0-9]+:[0-9]+/)
if (m) {
state.connect = m[0]
}
const $ = cheerio.load(body)
$('#playerlist tbody tr').each((i, tr) => {
if (!$(tr).find('td').first().attr('colspan')) {
state.players.push({
name: $(tr).find('td').eq(2).text(),
ping: $(tr).find('td').eq(3).text().trim(),
team: $(tr).find('td').eq(4).text().toLowerCase(),
score: parseInt($(tr).find('td').eq(5).text())
})
}
})
/*
var m = this.options.address.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/);
if(m) {
var o1 = parseInt(m[1]);
var o2 = parseInt(m[2]);
var o3 = parseInt(m[3]);
var o4 = parseInt(m[4]);
var addr = o1+(o2<<8)+(o3<<16)+(o4<<24);
state.raw.url = 'aos://'+addr;
}
*/
}
}
import Core from './core.js'
import * as cheerio from 'cheerio'
export default class buildandshoot extends Core {
async run (state) {
const body = await this.request({
url: 'http://' + this.options.address + ':' + this.options.port + '/'
})
let m
m = body.match(/status server for (.*?)\.?[\r\n]/)
if (m) state.name = m[1]
m = body.match(/Current uptime: (\d+)/)
if (m) state.raw.uptime = m[1]
m = body.match(/currently running (.*?) by /)
if (m) state.map = m[1]
m = body.match(/Current players: (\d+)\/(\d+)/)
if (m) {
state.numplayers = parseInt(m[1])
state.maxplayers = m[2]
}
m = body.match(/aos:\/\/[0-9]+:[0-9]+/)
if (m) {
state.connect = m[0]
}
const $ = cheerio.load(body)
$('#playerlist tbody tr').each((i, tr) => {
if (!$(tr).find('td').first().attr('colspan')) {
state.players.push({
name: $(tr).find('td').eq(2).text(),
ping: $(tr).find('td').eq(3).text().trim(),
team: $(tr).find('td').eq(4).text().toLowerCase(),
score: parseInt($(tr).find('td').eq(5).text())
})
}
})
/*
var m = this.options.address.match(/(\d+)\.(\d+)\.(\d+)\.(\d+)/);
if(m) {
var o1 = parseInt(m[1]);
var o2 = parseInt(m[2]);
var o3 = parseInt(m[3]);
var o4 = parseInt(m[4]);
var addr = o1+(o2<<8)+(o3<<16)+(o4<<24);
state.raw.url = 'aos://'+addr;
}
*/
}
}

View file

@ -1,349 +1,349 @@
import { EventEmitter } from 'node:events'
import * as net from 'node:net'
import got from 'got'
import Reader from '../lib/reader.js'
import { debugDump } from '../lib/HexUtil.js'
import Logger from '../lib/Logger.js'
import DnsResolver from '../lib/DnsResolver.js'
import { Results } from '../lib/Results.js'
import Promises from '../lib/Promises.js'
let uid = 0
export default class Core extends EventEmitter {
constructor () {
super()
this.encoding = 'utf8'
this.byteorder = 'le'
this.delimiter = '\0'
this.srvRecord = null
this.abortedPromise = null
this.logger = new Logger()
this.dnsResolver = new DnsResolver(this.logger)
// Sent to us by QueryRunner
this.options = null
/** @type GlobalUdpSocket */
this.udpSocket = null
this.shortestRTT = 0
this.usedTcp = false
}
// Runs a single attempt with a timeout and cleans up afterward
async runOnceSafe () {
if (this.options.debug) {
this.logger.debugEnabled = true
}
this.logger.prefix = 'Q#' + (uid++)
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
})
let timeout
try {
const promise = this.runOnce()
timeout = Promises.createTimeout(this.options.attemptTimeout, 'Attempt')
const result = await Promise.race([promise, timeout])
this.logger.debug('Query was successful')
return result
} catch (e) {
this.logger.debug('Query failed with error', e)
throw e
} finally {
timeout && timeout.cancel()
try {
abortCall()
} catch (e) {
this.logger.debug('Error during abort cleanup: ' + e.stack)
}
}
}
async runOnce () {
const options = this.options
if (('host' in options) && !('address' in options)) {
const resolved = await this.dnsResolver.resolve(options.host, options.ipFamily, this.srvRecord)
options.address = resolved.address
if (resolved.port) options.port = resolved.port
}
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 (!('connect' in state)) {
state.connect = '' +
(state.gameHost || this.options.host || this.options.address) +
':' +
(state.gamePort || this.options.port)
}
state.ping = this.shortestRTT
delete state.gameHost
delete state.gamePort
this.logger.debug(log => {
log('Size of players array: ' + state.players.length)
log('Size of bots array: ' + state.bots.length)
})
return state
}
async run (/** Results */ state) {}
/** Param can be a time in ms, or a promise (which will be timed) */
registerRtt (param) {
if (param.then) {
const start = Date.now()
param.then(() => {
const end = Date.now()
const rtt = end - start
this.registerRtt(rtt)
}).catch(() => {})
} else {
this.logger.debug('Registered RTT: ' + param + 'ms')
if (this.shortestRTT === 0 || param < this.shortestRTT) {
this.shortestRTT = param
}
}
}
// utils
/** @returns {Reader} */
reader (buffer) {
return new Reader(this, buffer)
}
translate (obj, trans) {
for (const from of Object.keys(trans)) {
const to = trans[from]
if (from in obj) {
if (to) obj[to] = obj[from]
delete obj[from]
}
}
}
trueTest (str) {
if (typeof str === 'boolean') return str
if (typeof str === 'number') return str !== 0
if (typeof str === 'string') {
if (str.toLowerCase() === 'true') return true
if (str.toLowerCase() === 'yes') return true
if (str === '1') return true
}
return false
}
assertValidPort (port) {
if (!port) {
throw new Error('Could not determine port to query. Did you provide a port?')
}
if (port < 1 || port > 65535) {
throw new Error('Invalid tcp/ip port: ' + port)
}
}
/**
* @template T
* @param {function(NodeJS.Socket):Promise<T>} fn
* @param {number=} port
* @returns {Promise<T>}
*/
async withTcp (fn, port) {
this.usedTcp = true
const address = this.options.address
if (!port) port = this.options.port
this.assertValidPort(port)
let socket, connectionTimeout
try {
socket = net.connect(port, address)
socket.setNoDelay(true)
// Prevent unhandled 'error' events from dumping straight to console
socket.on('error', () => {})
this.logger.debug(log => {
this.logger.debug(address + ':' + port + ' TCP Connecting')
const writeHook = socket.write
socket.write = (...args) => {
log(address + ':' + port + ' TCP-->')
log(debugDump(args[0]))
writeHook.apply(socket, args)
}
socket.on('error', e => log('TCP Error:', e))
socket.on('close', () => log('TCP Closed'))
socket.on('data', (data) => {
log(address + ':' + port + ' <--TCP')
log(data)
})
socket.on('ready', () => log(address + ':' + port + ' TCP Connected'))
})
const connectionPromise = new Promise((resolve, reject) => {
socket.on('ready', resolve)
socket.on('close', () => reject(new Error('TCP Connection Refused')))
})
this.registerRtt(connectionPromise)
connectionTimeout = Promises.createTimeout(this.options.socketTimeout, 'TCP Opening')
await Promise.race([
connectionPromise,
connectionTimeout,
this.abortedPromise
])
return await fn(socket)
} finally {
socket && socket.destroy()
connectionTimeout && connectionTimeout.cancel()
}
}
/**
* @template T
* @param {NodeJS.Socket} socket
* @param {Buffer|string} buffer
* @param {function(Buffer):T} ondata
* @returns Promise<T>
*/
async tcpSend (socket, buffer, ondata) {
let timeout
try {
const promise = new Promise((resolve, reject) => {
let received = Buffer.from([])
const onData = (data) => {
received = Buffer.concat([received, data])
const result = ondata(received)
if (result !== undefined) {
socket.removeListener('data', onData)
resolve(result)
}
}
socket.on('data', onData)
socket.write(buffer)
})
timeout = Promises.createTimeout(this.options.socketTimeout, 'TCP')
return await Promise.race([promise, timeout, this.abortedPromise])
} finally {
timeout && timeout.cancel()
}
}
/**
* @param {Buffer|string} buffer
* @param {function(Buffer):T=} onPacket
* @param {(function():T)=} onTimeout
* @returns Promise<T>
* @template T
*/
async udpSend (buffer, onPacket, onTimeout) {
const address = this.options.address
const port = this.options.port
this.assertValidPort(port)
if (typeof buffer === 'string') buffer = Buffer.from(buffer, 'binary')
const socket = this.udpSocket
await socket.send(buffer, address, port, this.options.debug)
if (!onPacket && !onTimeout) {
return null
}
let socketCallback
let timeout
try {
const promise = new Promise((resolve, reject) => {
const start = Date.now()
let end = null
socketCallback = (fromAddress, fromPort, buffer) => {
try {
if (fromAddress !== address) return
if (fromPort !== port) return
if (end === null) {
end = Date.now()
const rtt = end - start
this.registerRtt(rtt)
}
const result = onPacket(buffer)
if (result !== undefined) {
this.logger.debug('UDP send finished by callback')
resolve(result)
}
} catch (e) {
reject(e)
}
}
socket.addCallback(socketCallback, this.options.debug)
})
timeout = Promises.createTimeout(this.options.socketTimeout, 'UDP')
const wrappedTimeout = new Promise((resolve, reject) => {
timeout.catch((e) => {
this.logger.debug('UDP timeout detected')
if (onTimeout) {
try {
const result = onTimeout()
if (result !== undefined) {
this.logger.debug('UDP timeout resolved by callback')
resolve(result)
return
}
} catch (e) {
reject(e)
}
}
reject(e)
})
})
return await Promise.race([promise, wrappedTimeout, this.abortedPromise])
} finally {
timeout && timeout.cancel()
socketCallback && socket.removeCallback(socketCallback)
}
}
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 = got({
...params,
timeout: {
request: this.options.socketTimeout
}
})
this.logger.debug(log => {
log(() => params.url + ' HTTP-->')
requestPromise
.then((response) => log(params.url + ' <--HTTP ' + response.statusCode))
.catch(() => {})
})
const wrappedPromise = requestPromise.then(response => {
if (response.statusCode !== 200) throw new Error('Bad status code: ' + response.statusCode)
return response.body
})
return await Promise.race([wrappedPromise, this.abortedPromise])
} finally {
requestPromise && requestPromise.cancel()
}
}
}
import { EventEmitter } from 'node:events'
import * as net from 'node:net'
import got from 'got'
import Reader from '../lib/reader.js'
import { debugDump } from '../lib/HexUtil.js'
import Logger from '../lib/Logger.js'
import DnsResolver from '../lib/DnsResolver.js'
import { Results } from '../lib/Results.js'
import Promises from '../lib/Promises.js'
let uid = 0
export default class Core extends EventEmitter {
constructor () {
super()
this.encoding = 'utf8'
this.byteorder = 'le'
this.delimiter = '\0'
this.srvRecord = null
this.abortedPromise = null
this.logger = new Logger()
this.dnsResolver = new DnsResolver(this.logger)
// Sent to us by QueryRunner
this.options = null
/** @type GlobalUdpSocket */
this.udpSocket = null
this.shortestRTT = 0
this.usedTcp = false
}
// Runs a single attempt with a timeout and cleans up afterward
async runOnceSafe () {
if (this.options.debug) {
this.logger.debugEnabled = true
}
this.logger.prefix = 'Q#' + (uid++)
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
})
let timeout
try {
const promise = this.runOnce()
timeout = Promises.createTimeout(this.options.attemptTimeout, 'Attempt')
const result = await Promise.race([promise, timeout])
this.logger.debug('Query was successful')
return result
} catch (e) {
this.logger.debug('Query failed with error', e)
throw e
} finally {
timeout && timeout.cancel()
try {
abortCall()
} catch (e) {
this.logger.debug('Error during abort cleanup: ' + e.stack)
}
}
}
async runOnce () {
const options = this.options
if (('host' in options) && !('address' in options)) {
const resolved = await this.dnsResolver.resolve(options.host, options.ipFamily, this.srvRecord)
options.address = resolved.address
if (resolved.port) options.port = resolved.port
}
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 (!('connect' in state)) {
state.connect = '' +
(state.gameHost || this.options.host || this.options.address) +
':' +
(state.gamePort || this.options.port)
}
state.ping = this.shortestRTT
delete state.gameHost
delete state.gamePort
this.logger.debug(log => {
log('Size of players array: ' + state.players.length)
log('Size of bots array: ' + state.bots.length)
})
return state
}
async run (/** Results */ state) {}
/** Param can be a time in ms, or a promise (which will be timed) */
registerRtt (param) {
if (param.then) {
const start = Date.now()
param.then(() => {
const end = Date.now()
const rtt = end - start
this.registerRtt(rtt)
}).catch(() => {})
} else {
this.logger.debug('Registered RTT: ' + param + 'ms')
if (this.shortestRTT === 0 || param < this.shortestRTT) {
this.shortestRTT = param
}
}
}
// utils
/** @returns {Reader} */
reader (buffer) {
return new Reader(this, buffer)
}
translate (obj, trans) {
for (const from of Object.keys(trans)) {
const to = trans[from]
if (from in obj) {
if (to) obj[to] = obj[from]
delete obj[from]
}
}
}
trueTest (str) {
if (typeof str === 'boolean') return str
if (typeof str === 'number') return str !== 0
if (typeof str === 'string') {
if (str.toLowerCase() === 'true') return true
if (str.toLowerCase() === 'yes') return true
if (str === '1') return true
}
return false
}
assertValidPort (port) {
if (!port) {
throw new Error('Could not determine port to query. Did you provide a port?')
}
if (port < 1 || port > 65535) {
throw new Error('Invalid tcp/ip port: ' + port)
}
}
/**
* @template T
* @param {function(NodeJS.Socket):Promise<T>} fn
* @param {number=} port
* @returns {Promise<T>}
*/
async withTcp (fn, port) {
this.usedTcp = true
const address = this.options.address
if (!port) port = this.options.port
this.assertValidPort(port)
let socket, connectionTimeout
try {
socket = net.connect(port, address)
socket.setNoDelay(true)
// Prevent unhandled 'error' events from dumping straight to console
socket.on('error', () => {})
this.logger.debug(log => {
this.logger.debug(address + ':' + port + ' TCP Connecting')
const writeHook = socket.write
socket.write = (...args) => {
log(address + ':' + port + ' TCP-->')
log(debugDump(args[0]))
writeHook.apply(socket, args)
}
socket.on('error', e => log('TCP Error:', e))
socket.on('close', () => log('TCP Closed'))
socket.on('data', (data) => {
log(address + ':' + port + ' <--TCP')
log(data)
})
socket.on('ready', () => log(address + ':' + port + ' TCP Connected'))
})
const connectionPromise = new Promise((resolve, reject) => {
socket.on('ready', resolve)
socket.on('close', () => reject(new Error('TCP Connection Refused')))
})
this.registerRtt(connectionPromise)
connectionTimeout = Promises.createTimeout(this.options.socketTimeout, 'TCP Opening')
await Promise.race([
connectionPromise,
connectionTimeout,
this.abortedPromise
])
return await fn(socket)
} finally {
socket && socket.destroy()
connectionTimeout && connectionTimeout.cancel()
}
}
/**
* @template T
* @param {NodeJS.Socket} socket
* @param {Buffer|string} buffer
* @param {function(Buffer):T} ondata
* @returns Promise<T>
*/
async tcpSend (socket, buffer, ondata) {
let timeout
try {
const promise = new Promise((resolve, reject) => {
let received = Buffer.from([])
const onData = (data) => {
received = Buffer.concat([received, data])
const result = ondata(received)
if (result !== undefined) {
socket.removeListener('data', onData)
resolve(result)
}
}
socket.on('data', onData)
socket.write(buffer)
})
timeout = Promises.createTimeout(this.options.socketTimeout, 'TCP')
return await Promise.race([promise, timeout, this.abortedPromise])
} finally {
timeout && timeout.cancel()
}
}
/**
* @param {Buffer|string} buffer
* @param {function(Buffer):T=} onPacket
* @param {(function():T)=} onTimeout
* @returns Promise<T>
* @template T
*/
async udpSend (buffer, onPacket, onTimeout) {
const address = this.options.address
const port = this.options.port
this.assertValidPort(port)
if (typeof buffer === 'string') buffer = Buffer.from(buffer, 'binary')
const socket = this.udpSocket
await socket.send(buffer, address, port, this.options.debug)
if (!onPacket && !onTimeout) {
return null
}
let socketCallback
let timeout
try {
const promise = new Promise((resolve, reject) => {
const start = Date.now()
let end = null
socketCallback = (fromAddress, fromPort, buffer) => {
try {
if (fromAddress !== address) return
if (fromPort !== port) return
if (end === null) {
end = Date.now()
const rtt = end - start
this.registerRtt(rtt)
}
const result = onPacket(buffer)
if (result !== undefined) {
this.logger.debug('UDP send finished by callback')
resolve(result)
}
} catch (e) {
reject(e)
}
}
socket.addCallback(socketCallback, this.options.debug)
})
timeout = Promises.createTimeout(this.options.socketTimeout, 'UDP')
const wrappedTimeout = new Promise((resolve, reject) => {
timeout.catch((e) => {
this.logger.debug('UDP timeout detected')
if (onTimeout) {
try {
const result = onTimeout()
if (result !== undefined) {
this.logger.debug('UDP timeout resolved by callback')
resolve(result)
return
}
} catch (e) {
reject(e)
}
}
reject(e)
})
})
return await Promise.race([promise, wrappedTimeout, this.abortedPromise])
} finally {
timeout && timeout.cancel()
socketCallback && socket.removeCallback(socketCallback)
}
}
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 = got({
...params,
timeout: {
request: this.options.socketTimeout
}
})
this.logger.debug(log => {
log(() => params.url + ' HTTP-->')
requestPromise
.then((response) => log(params.url + ' <--HTTP ' + response.statusCode))
.catch(() => {})
})
const wrappedPromise = requestPromise.then(response => {
if (response.statusCode !== 200) throw new Error('Bad status code: ' + response.statusCode)
return response.body
})
return await Promise.race([wrappedPromise, this.abortedPromise])
} finally {
requestPromise && requestPromise.cancel()
}
}
}

View file

@ -1,65 +1,65 @@
import Core from './core.js'
export default class cs2d extends Core {
async run (state) {
const reader = await this.sendQuery(
Buffer.from('\x01\x00\xFB\x01\xF5\x03\xFB\x05', 'binary'),
Buffer.from('\x01\x00\xFB\x01', 'binary')
)
const flags = reader.uint(1)
state.raw.flags = flags
state.password = this.readFlag(flags, 0)
state.raw.registeredOnly = this.readFlag(flags, 1)
state.raw.fogOfWar = this.readFlag(flags, 2)
state.raw.friendlyFire = this.readFlag(flags, 3)
state.raw.botsEnabled = this.readFlag(flags, 5)
state.raw.luaScripts = this.readFlag(flags, 6)
state.raw.forceLight = this.readFlag(flags, 7)
state.name = this.readString(reader)
state.map = this.readString(reader)
state.numplayers = reader.uint(1)
state.maxplayers = reader.uint(1)
if (flags & 32) {
state.raw.gamemode = reader.uint(1)
} else {
state.raw.gamemode = 0
}
state.raw.numbots = reader.uint(1)
const flags2 = reader.uint(1)
state.raw.flags2 = flags2
state.raw.recoil = this.readFlag(flags2, 0)
state.raw.offScreenDamage = this.readFlag(flags2, 1)
state.raw.hasDownloads = this.readFlag(flags2, 2)
reader.skip(2)
const players = reader.uint(1)
for (let i = 0; i < players; i++) {
const player = {}
player.id = reader.uint(1)
player.name = this.readString(reader)
player.team = reader.uint(1)
player.score = reader.uint(4)
player.deaths = reader.uint(4)
state.players.push(player)
}
}
async sendQuery (request, expectedHeader) {
// Send multiple copies of the request packet, because cs2d likes to just ignore them randomly
await this.udpSend(request)
await this.udpSend(request)
return await this.udpSend(request, (buffer) => {
const reader = this.reader(buffer)
const header = reader.part(4)
if (!header.equals(expectedHeader)) return
return reader
})
}
readFlag (flags, offset) {
return !!(flags & (1 << offset))
}
readString (reader) {
return reader.pascalString(1)
}
}
import Core from './core.js'
export default class cs2d extends Core {
async run (state) {
const reader = await this.sendQuery(
Buffer.from('\x01\x00\xFB\x01\xF5\x03\xFB\x05', 'binary'),
Buffer.from('\x01\x00\xFB\x01', 'binary')
)
const flags = reader.uint(1)
state.raw.flags = flags
state.password = this.readFlag(flags, 0)
state.raw.registeredOnly = this.readFlag(flags, 1)
state.raw.fogOfWar = this.readFlag(flags, 2)
state.raw.friendlyFire = this.readFlag(flags, 3)
state.raw.botsEnabled = this.readFlag(flags, 5)
state.raw.luaScripts = this.readFlag(flags, 6)
state.raw.forceLight = this.readFlag(flags, 7)
state.name = this.readString(reader)
state.map = this.readString(reader)
state.numplayers = reader.uint(1)
state.maxplayers = reader.uint(1)
if (flags & 32) {
state.raw.gamemode = reader.uint(1)
} else {
state.raw.gamemode = 0
}
state.raw.numbots = reader.uint(1)
const flags2 = reader.uint(1)
state.raw.flags2 = flags2
state.raw.recoil = this.readFlag(flags2, 0)
state.raw.offScreenDamage = this.readFlag(flags2, 1)
state.raw.hasDownloads = this.readFlag(flags2, 2)
reader.skip(2)
const players = reader.uint(1)
for (let i = 0; i < players; i++) {
const player = {}
player.id = reader.uint(1)
player.name = this.readString(reader)
player.team = reader.uint(1)
player.score = reader.uint(4)
player.deaths = reader.uint(4)
state.players.push(player)
}
}
async sendQuery (request, expectedHeader) {
// Send multiple copies of the request packet, because cs2d likes to just ignore them randomly
await this.udpSend(request)
await this.udpSend(request)
return await this.udpSend(request, (buffer) => {
const reader = this.reader(buffer)
const header = reader.part(4)
if (!header.equals(expectedHeader)) return
return reader
})
}
readFlag (flags, offset) {
return !!(flags & (1 << offset))
}
readString (reader) {
return reader.pascalString(1)
}
}

View file

@ -1,29 +1,29 @@
import Core from './core.js'
export default class discord extends Core {
async run (state) {
const guildId = this.options.guildId
if (typeof guildId !== 'string') {
throw new Error('guildId option must be set when querying discord. Ensure the guildId is a string and not a number.' +
" (It's too large of a number for javascript to store without losing precision)")
}
this.usedTcp = true
const raw = await this.request({
url: 'https://discordapp.com/api/guilds/' + guildId + '/widget.json'
})
const json = JSON.parse(raw)
state.name = json.name
if (json.instant_invite) {
state.connect = json.instant_invite
} else {
state.connect = 'https://discordapp.com/channels/' + guildId
}
for (const member of json.members) {
const { username: name, ...rest } = member
state.players.push({ name, ...rest })
}
delete json.members
state.maxplayers = 500000
state.raw = json
}
}
import Core from './core.js'
export default class discord extends Core {
async run (state) {
const guildId = this.options.guildId
if (typeof guildId !== 'string') {
throw new Error('guildId option must be set when querying discord. Ensure the guildId is a string and not a number.' +
" (It's too large of a number for javascript to store without losing precision)")
}
this.usedTcp = true
const raw = await this.request({
url: 'https://discordapp.com/api/guilds/' + guildId + '/widget.json'
})
const json = JSON.parse(raw)
state.name = json.name
if (json.instant_invite) {
state.connect = json.instant_invite
} else {
state.connect = 'https://discordapp.com/channels/' + guildId
}
for (const member of json.members) {
const { username: name, ...rest } = member
state.players.push({ name, ...rest })
}
delete json.members
state.maxplayers = 500000
state.raw = json
}
}

View file

@ -1,148 +1,148 @@
import Core from './core.js'
export default class doom3 extends Core {
constructor () {
super()
this.encoding = 'latin1'
}
async run (state) {
const body = await this.udpSend('\xff\xffgetInfo\x00PiNGPoNg\x00', packet => {
const reader = this.reader(packet)
const header = reader.uint(2)
if (header !== 0xffff) return
const header2 = reader.string()
if (header2 !== 'infoResponse') return
const challengePart1 = reader.string(4)
if (challengePart1 !== 'PiNG') return
// some doom3 implementations only return the first 4 bytes of the challenge
const challengePart2 = reader.string(4)
if (challengePart2 !== 'PoNg') reader.skip(-4)
return reader.rest()
})
let reader = this.reader(body)
const protoVersion = reader.uint(4)
state.raw.protocolVersion = (protoVersion >> 16) + '.' + (protoVersion & 0xffff)
// some doom implementations send us a packet size here, some don't (etqw does this)
// we can tell if this is a packet size, because the third and fourth byte will be 0 (no packets are that massive)
reader.skip(2)
const packetContainsSize = (reader.uint(2) === 0)
reader.skip(-4)
if (packetContainsSize) {
const size = reader.uint(4)
this.logger.debug('Received packet size: ' + size)
}
while (!reader.done()) {
const key = reader.string()
let value = this.stripColors(reader.string())
if (key === 'si_map') {
value = value.replace('maps/', '')
value = value.replace('.entities', '')
}
if (!key) break
state.raw[key] = value
this.logger.debug(key + '=' + value)
}
const isEtqw = state.raw.gamename && state.raw.gamename.toLowerCase().includes('etqw')
const rest = reader.rest()
let playerResult = this.attemptPlayerParse(rest, isEtqw, false, false, false)
if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, false, false)
if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, true, true)
if (!playerResult) {
throw new Error('Unable to find a suitable parse strategy for player list')
}
let players;
[players, reader] = playerResult
state.numplayers = players.length
for (const player of players) {
if (!player.ping || player.typeflag) { state.bots.push(player) } else { state.players.push(player) }
}
state.raw.osmask = reader.uint(4)
if (isEtqw) {
state.raw.ranked = reader.uint(1)
state.raw.timeleft = reader.uint(4)
state.raw.gamestate = reader.uint(1)
state.raw.servertype = reader.uint(1)
// 0 = regular, 1 = tv
if (state.raw.servertype === 0) {
state.raw.interestedClients = reader.uint(1)
} else if (state.raw.servertype === 1) {
state.raw.connectedClients = reader.uint(4)
state.raw.maxClients = reader.uint(4)
}
}
if (state.raw.si_name) state.name = state.raw.si_name
if (state.raw.si_map) state.map = state.raw.si_map
if (state.raw.si_maxplayers) state.maxplayers = parseInt(state.raw.si_maxplayers)
if (state.raw.si_maxPlayers) state.maxplayers = parseInt(state.raw.si_maxPlayers)
if (state.raw.si_usepass === '1') state.password = true
if (state.raw.si_needPass === '1') state.password = true
if (this.options.port === 27733) state.gamePort = 3074 // etqw has a different query and game port
}
attemptPlayerParse (rest, isEtqw, hasClanTag, hasClanTagPos, hasTypeFlag) {
this.logger.debug('starting player parse attempt:')
this.logger.debug('isEtqw: ' + isEtqw)
this.logger.debug('hasClanTag: ' + hasClanTag)
this.logger.debug('hasClanTagPos: ' + hasClanTagPos)
this.logger.debug('hasTypeFlag: ' + hasTypeFlag)
const reader = this.reader(rest)
let lastId = -1
const players = []
while (true) {
this.logger.debug('---')
if (reader.done()) {
this.logger.debug('* aborting attempt, overran buffer *')
return null
}
const player = {}
player.id = reader.uint(1)
this.logger.debug('id: ' + player.id)
if (player.id <= lastId || player.id > 0x20) {
this.logger.debug('* aborting attempt, invalid player id *')
return null
}
lastId = player.id
if (player.id === 0x20) {
this.logger.debug('* player parse successful *')
break
}
player.ping = reader.uint(2)
this.logger.debug('ping: ' + player.ping)
if (!isEtqw) {
player.rate = reader.uint(4)
this.logger.debug('rate: ' + player.rate)
}
player.name = this.stripColors(reader.string())
this.logger.debug('name: ' + player.name)
if (hasClanTag) {
if (hasClanTagPos) {
const clanTagPos = reader.uint(1)
this.logger.debug('clanTagPos: ' + clanTagPos)
}
player.clantag = this.stripColors(reader.string())
this.logger.debug('clan tag: ' + player.clantag)
}
if (hasTypeFlag) {
player.typeflag = reader.uint(1)
this.logger.debug('type flag: ' + player.typeflag)
}
players.push(player)
}
return [players, reader]
}
stripColors (str) {
// uses quake 3 color codes
return str.replace(/\^(X.{6}|.)/g, '')
}
}
import Core from './core.js'
export default class doom3 extends Core {
constructor () {
super()
this.encoding = 'latin1'
}
async run (state) {
const body = await this.udpSend('\xff\xffgetInfo\x00PiNGPoNg\x00', packet => {
const reader = this.reader(packet)
const header = reader.uint(2)
if (header !== 0xffff) return
const header2 = reader.string()
if (header2 !== 'infoResponse') return
const challengePart1 = reader.string(4)
if (challengePart1 !== 'PiNG') return
// some doom3 implementations only return the first 4 bytes of the challenge
const challengePart2 = reader.string(4)
if (challengePart2 !== 'PoNg') reader.skip(-4)
return reader.rest()
})
let reader = this.reader(body)
const protoVersion = reader.uint(4)
state.raw.protocolVersion = (protoVersion >> 16) + '.' + (protoVersion & 0xffff)
// some doom implementations send us a packet size here, some don't (etqw does this)
// we can tell if this is a packet size, because the third and fourth byte will be 0 (no packets are that massive)
reader.skip(2)
const packetContainsSize = (reader.uint(2) === 0)
reader.skip(-4)
if (packetContainsSize) {
const size = reader.uint(4)
this.logger.debug('Received packet size: ' + size)
}
while (!reader.done()) {
const key = reader.string()
let value = this.stripColors(reader.string())
if (key === 'si_map') {
value = value.replace('maps/', '')
value = value.replace('.entities', '')
}
if (!key) break
state.raw[key] = value
this.logger.debug(key + '=' + value)
}
const isEtqw = state.raw.gamename && state.raw.gamename.toLowerCase().includes('etqw')
const rest = reader.rest()
let playerResult = this.attemptPlayerParse(rest, isEtqw, false, false, false)
if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, false, false)
if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, true, true)
if (!playerResult) {
throw new Error('Unable to find a suitable parse strategy for player list')
}
let players;
[players, reader] = playerResult
state.numplayers = players.length
for (const player of players) {
if (!player.ping || player.typeflag) { state.bots.push(player) } else { state.players.push(player) }
}
state.raw.osmask = reader.uint(4)
if (isEtqw) {
state.raw.ranked = reader.uint(1)
state.raw.timeleft = reader.uint(4)
state.raw.gamestate = reader.uint(1)
state.raw.servertype = reader.uint(1)
// 0 = regular, 1 = tv
if (state.raw.servertype === 0) {
state.raw.interestedClients = reader.uint(1)
} else if (state.raw.servertype === 1) {
state.raw.connectedClients = reader.uint(4)
state.raw.maxClients = reader.uint(4)
}
}
if (state.raw.si_name) state.name = state.raw.si_name
if (state.raw.si_map) state.map = state.raw.si_map
if (state.raw.si_maxplayers) state.maxplayers = parseInt(state.raw.si_maxplayers)
if (state.raw.si_maxPlayers) state.maxplayers = parseInt(state.raw.si_maxPlayers)
if (state.raw.si_usepass === '1') state.password = true
if (state.raw.si_needPass === '1') state.password = true
if (this.options.port === 27733) state.gamePort = 3074 // etqw has a different query and game port
}
attemptPlayerParse (rest, isEtqw, hasClanTag, hasClanTagPos, hasTypeFlag) {
this.logger.debug('starting player parse attempt:')
this.logger.debug('isEtqw: ' + isEtqw)
this.logger.debug('hasClanTag: ' + hasClanTag)
this.logger.debug('hasClanTagPos: ' + hasClanTagPos)
this.logger.debug('hasTypeFlag: ' + hasTypeFlag)
const reader = this.reader(rest)
let lastId = -1
const players = []
while (true) {
this.logger.debug('---')
if (reader.done()) {
this.logger.debug('* aborting attempt, overran buffer *')
return null
}
const player = {}
player.id = reader.uint(1)
this.logger.debug('id: ' + player.id)
if (player.id <= lastId || player.id > 0x20) {
this.logger.debug('* aborting attempt, invalid player id *')
return null
}
lastId = player.id
if (player.id === 0x20) {
this.logger.debug('* player parse successful *')
break
}
player.ping = reader.uint(2)
this.logger.debug('ping: ' + player.ping)
if (!isEtqw) {
player.rate = reader.uint(4)
this.logger.debug('rate: ' + player.rate)
}
player.name = this.stripColors(reader.string())
this.logger.debug('name: ' + player.name)
if (hasClanTag) {
if (hasClanTagPos) {
const clanTagPos = reader.uint(1)
this.logger.debug('clanTagPos: ' + clanTagPos)
}
player.clantag = this.stripColors(reader.string())
this.logger.debug('clan tag: ' + player.clantag)
}
if (hasTypeFlag) {
player.typeflag = reader.uint(1)
this.logger.debug('type flag: ' + player.typeflag)
}
players.push(player)
}
return [players, reader]
}
stripColors (str) {
// uses quake 3 color codes
return str.replace(/\^(X.{6}|.)/g, '')
}
}

View file

@ -1,20 +1,20 @@
import Core from './core.js'
export default class eco extends Core {
async run (state) {
if (!this.options.port) this.options.port = 3001
const request = await this.request({
url: `http://${this.options.address}:${this.options.port}/frontpage`,
responseType: 'json'
})
const serverInfo = request.Info
state.name = serverInfo.Description
state.numplayers = serverInfo.OnlinePlayers;
state.maxplayers = serverInfo.TotalPlayers
state.password = serverInfo.HasPassword
state.gamePort = serverInfo.GamePort
state.raw = serverInfo
}
}
import Core from './core.js'
export default class eco extends Core {
async run (state) {
if (!this.options.port) this.options.port = 3001
const request = await this.request({
url: `http://${this.options.address}:${this.options.port}/frontpage`,
responseType: 'json'
})
const serverInfo = request.Info
state.name = serverInfo.Description
state.numplayers = serverInfo.OnlinePlayers;
state.maxplayers = serverInfo.TotalPlayers
state.password = serverInfo.HasPassword
state.gamePort = serverInfo.GamePort
state.raw = serverInfo
}
}

View file

@ -1,38 +1,38 @@
import valve from './valve.js'
export default class ffow extends valve {
constructor () {
super()
this.byteorder = 'be'
this.legacyChallenge = true
}
async queryInfo (state) {
this.logger.debug('Requesting ffow info ...')
const b = await this.sendPacket(
0x46,
'LSQ',
0x49
)
const reader = this.reader(b)
state.raw.protocol = reader.uint(1)
state.name = reader.string()
state.map = reader.string()
state.raw.mod = reader.string()
state.raw.gamemode = reader.string()
state.raw.description = reader.string()
state.raw.version = reader.string()
state.gamePort = reader.uint(2)
state.numplayers = reader.uint(1)
state.maxplayers = reader.uint(1)
state.raw.listentype = String.fromCharCode(reader.uint(1))
state.raw.environment = String.fromCharCode(reader.uint(1))
state.password = !!reader.uint(1)
state.raw.secure = reader.uint(1)
state.raw.averagefps = reader.uint(1)
state.raw.round = reader.uint(1)
state.raw.maxrounds = reader.uint(1)
state.raw.timeleft = reader.uint(2)
}
}
import valve from './valve.js'
export default class ffow extends valve {
constructor () {
super()
this.byteorder = 'be'
this.legacyChallenge = true
}
async queryInfo (state) {
this.logger.debug('Requesting ffow info ...')
const b = await this.sendPacket(
0x46,
'LSQ',
0x49
)
const reader = this.reader(b)
state.raw.protocol = reader.uint(1)
state.name = reader.string()
state.map = reader.string()
state.raw.mod = reader.string()
state.raw.gamemode = reader.string()
state.raw.description = reader.string()
state.raw.version = reader.string()
state.gamePort = reader.uint(2)
state.numplayers = reader.uint(1)
state.maxplayers = reader.uint(1)
state.raw.listentype = String.fromCharCode(reader.uint(1))
state.raw.environment = String.fromCharCode(reader.uint(1))
state.password = !!reader.uint(1)
state.raw.secure = reader.uint(1)
state.raw.averagefps = reader.uint(1)
state.raw.round = reader.uint(1)
state.raw.maxrounds = reader.uint(1)
state.raw.timeleft = reader.uint(2)
}
}

View file

@ -1,33 +1,33 @@
import quake2 from './quake2.js'
export default class fivem extends quake2 {
constructor () {
super()
this.sendHeader = 'getinfo xxx'
this.responseHeader = 'infoResponse'
this.encoding = 'utf8'
}
async run (state) {
await super.run(state)
{
const json = await this.request({
url: 'http://' + this.options.address + ':' + this.options.port + '/info.json',
responseType: 'json'
})
state.raw.info = json
}
{
const json = await this.request({
url: 'http://' + this.options.address + ':' + this.options.port + '/players.json',
responseType: 'json'
})
state.raw.players = json
for (const player of json) {
state.players.push({ name: player.name, ping: player.ping })
}
}
}
}
import quake2 from './quake2.js'
export default class fivem extends quake2 {
constructor () {
super()
this.sendHeader = 'getinfo xxx'
this.responseHeader = 'infoResponse'
this.encoding = 'utf8'
}
async run (state) {
await super.run(state)
{
const json = await this.request({
url: 'http://' + this.options.address + ':' + this.options.port + '/info.json',
responseType: 'json'
})
state.raw.info = json
}
{
const json = await this.request({
url: 'http://' + this.options.address + ':' + this.options.port + '/players.json',
responseType: 'json'
})
state.raw.players = json
for (const player of json) {
state.players.push({ name: player.name, ping: player.ping })
}
}
}
}

View file

@ -1,181 +1,181 @@
import Core from './core.js'
const stringKeys = new Set([
'website',
'gametype',
'gamemode',
'player'
])
function normalizeEntry ([key, value]) {
key = key.toLowerCase()
const split = key.split('_')
let keyType = key
if (split.length === 2 && !isNaN(Number(split[1]))) {
keyType = split[0]
}
if (!stringKeys.has(keyType) && !keyType.includes('name')) { // todo! the latter check might be problematic, fails on key "name_tag_distance_scope"
if (value.toLowerCase() === 'true') {
value = true
} else if (value.toLowerCase() === 'false') {
value = false
} else if (value.length && !isNaN(Number(value))) {
value = Number(value)
}
}
return [key, value]
}
export default class gamespy1 extends Core {
constructor () {
super()
this.encoding = 'latin1'
this.byteorder = 'be'
}
async run (state) {
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 = Number(data.maxplayers)
if ('hostport' in data) state.gamePort = Number(data.hostport)
const teamOffByOne = data.gamename === 'bfield1942'
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 = Number(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 {
// 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(value)) { // todo! technically, this NaN check isn't needed.
key = 'teamId'
value += teamOffByOne ? -1 : 0
}
playersById[id][key] = value
}
}
state.raw.teams = teamNamesById
const players = Object.values(playersById)
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 (Object.prototype.hasOwnProperty.call(player, 'teamId')) {
if (Object.keys(teamNamesById).length) {
player.team = teamNamesById[player.teamId] || ''
} else {
player.team = player.teamId
delete player.teamId
}
}
state.players.push(player)
}
state.numplayers = state.players.length
}
async sendPacket (type) {
let receivedQueryId
const output = {}
const parts = new Set()
let maxPartNum = 0
return await this.udpSend(type, buffer => {
const reader = this.reader(buffer)
const str = reader.string(buffer.length)
const split = str.split('\\')
split.shift()
const data = {}
while (split.length) {
const key = split.shift()
const value = split.shift() || ''
data[key] = value
}
let queryId, partNum
const partFinal = ('final' in data)
if (data.queryid) {
const split = data.queryid.split('.')
if (split.length >= 2) {
partNum = Number(split[1])
}
queryId = split[0]
}
delete data.final
delete data.queryid
this.logger.debug('Received part num=' + partNum + ' queryId=' + queryId + ' final=' + partFinal)
if (queryId) {
if (receivedQueryId && receivedQueryId !== queryId) {
this.logger.debug('Rejected packet (Wrong query ID)')
return
} else if (!receivedQueryId) {
receivedQueryId = queryId
}
}
if (!partNum) {
partNum = parts.size
this.logger.debug('No part number received (assigned #' + partNum + ')')
}
if (parts.has(partNum)) {
this.logger.debug('Rejected packet (Duplicate part)')
return
}
parts.add(partNum)
if (partFinal) {
maxPartNum = partNum
}
this.logger.debug('Received part #' + partNum + ' of ' + (maxPartNum || '?'))
for (const i of Object.keys(data)) {
output[i] = data[i]
}
if (maxPartNum && parts.size === maxPartNum) {
this.logger.debug('Received all parts')
this.logger.debug(output)
return output
}
})
}
}
import Core from './core.js'
const stringKeys = new Set([
'website',
'gametype',
'gamemode',
'player'
])
function normalizeEntry ([key, value]) {
key = key.toLowerCase()
const split = key.split('_')
let keyType = key
if (split.length === 2 && !isNaN(Number(split[1]))) {
keyType = split[0]
}
if (!stringKeys.has(keyType) && !keyType.includes('name')) { // todo! the latter check might be problematic, fails on key "name_tag_distance_scope"
if (value.toLowerCase() === 'true') {
value = true
} else if (value.toLowerCase() === 'false') {
value = false
} else if (value.length && !isNaN(Number(value))) {
value = Number(value)
}
}
return [key, value]
}
export default class gamespy1 extends Core {
constructor () {
super()
this.encoding = 'latin1'
this.byteorder = 'be'
}
async run (state) {
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 = Number(data.maxplayers)
if ('hostport' in data) state.gamePort = Number(data.hostport)
const teamOffByOne = data.gamename === 'bfield1942'
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 = Number(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 {
// 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(value)) { // todo! technically, this NaN check isn't needed.
key = 'teamId'
value += teamOffByOne ? -1 : 0
}
playersById[id][key] = value
}
}
state.raw.teams = teamNamesById
const players = Object.values(playersById)
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 (Object.prototype.hasOwnProperty.call(player, 'teamId')) {
if (Object.keys(teamNamesById).length) {
player.team = teamNamesById[player.teamId] || ''
} else {
player.team = player.teamId
delete player.teamId
}
}
state.players.push(player)
}
state.numplayers = state.players.length
}
async sendPacket (type) {
let receivedQueryId
const output = {}
const parts = new Set()
let maxPartNum = 0
return await this.udpSend(type, buffer => {
const reader = this.reader(buffer)
const str = reader.string(buffer.length)
const split = str.split('\\')
split.shift()
const data = {}
while (split.length) {
const key = split.shift()
const value = split.shift() || ''
data[key] = value
}
let queryId, partNum
const partFinal = ('final' in data)
if (data.queryid) {
const split = data.queryid.split('.')
if (split.length >= 2) {
partNum = Number(split[1])
}
queryId = split[0]
}
delete data.final
delete data.queryid
this.logger.debug('Received part num=' + partNum + ' queryId=' + queryId + ' final=' + partFinal)
if (queryId) {
if (receivedQueryId && receivedQueryId !== queryId) {
this.logger.debug('Rejected packet (Wrong query ID)')
return
} else if (!receivedQueryId) {
receivedQueryId = queryId
}
}
if (!partNum) {
partNum = parts.size
this.logger.debug('No part number received (assigned #' + partNum + ')')
}
if (parts.has(partNum)) {
this.logger.debug('Rejected packet (Duplicate part)')
return
}
parts.add(partNum)
if (partFinal) {
maxPartNum = partNum
}
this.logger.debug('Received part #' + partNum + ' of ' + (maxPartNum || '?'))
for (const i of Object.keys(data)) {
output[i] = data[i]
}
if (maxPartNum && parts.size === maxPartNum) {
this.logger.debug('Received all parts')
this.logger.debug(output)
return output
}
})
}
}

View file

@ -1,144 +1,144 @@
import Core from './core.js'
export default class gamespy2 extends Core {
constructor () {
super()
this.encoding = 'latin1'
this.byteorder = 'be'
}
async run (state) {
// Parse info
{
const body = await this.sendPacket([0xff, 0, 0])
const reader = this.reader(body)
while (!reader.done()) {
const key = reader.string()
const value = reader.string()
if (!key) break
state.raw[key] = value
}
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)
}
// Parse players
{
const body = await this.sendPacket([0, 0xff, 0])
const reader = this.reader(body)
for (const rawPlayer of this.readFieldData(reader)) {
state.players.push(rawPlayer)
}
if ('numplayers' in state.raw) state.numplayers = parseInt(state.raw.numplayers)
else state.numplayers = state.players.length
}
// Parse teams
{
const body = await this.sendPacket([0, 0, 0xff])
const reader = this.reader(body)
state.raw.teams = this.readFieldData(reader)
}
// Special case for america's army 1 and 2
// both use gamename = "armygame"
if (state.raw.gamename === 'armygame') {
const stripColor = (str) => {
// uses unreal 2 color codes
return str.replace(/\x1b...|[\x00-\x1a]/g, '')
}
state.name = stripColor(state.name)
state.map = stripColor(state.map)
for (const key of Object.keys(state.raw)) {
if (typeof state.raw[key] === 'string') {
state.raw[key] = stripColor(state.raw[key])
}
}
for (const player of state.players) {
if (!('name' in player)) continue
player.name = stripColor(player.name)
}
}
}
async sendPacket (type) {
const request = Buffer.concat([
Buffer.from([0xfe, 0xfd, 0x00]), // gamespy2
Buffer.from([0x00, 0x00, 0x00, 0x01]), // ping ID
Buffer.from(type)
])
return await this.udpSend(request, buffer => {
const reader = this.reader(buffer)
const header = reader.uint(1)
if (header !== 0) return
const pingId = reader.uint(4)
if (pingId !== 1) return
return reader.rest()
})
}
readFieldData (reader) {
reader.uint(1) // always 0
const count = reader.uint(1) // number of rows in this data
// some games omit the count byte entirely if it's 0 or at random (like americas army)
// Luckily, count should always be <64, and ascii characters will typically be >64,
// so we can detect this.
if (count > 64) {
reader.skip(-1)
this.logger.debug('Detected missing count byte, rewinding by 1')
} else {
this.logger.debug('Detected row count: ' + count)
}
this.logger.debug(() => 'Reading fields, starting at: ' + reader.rest())
const fields = []
while (!reader.done()) {
const field = reader.string()
if (!field) break
fields.push(field)
this.logger.debug('field:' + field)
}
if (!fields.length) return []
const units = []
while (!reader.done()) {
const unit = {}
for (let iField = 0; iField < fields.length; iField++) {
let key = fields[iField]
let value = reader.string()
if (!value && iField === 0) return units
this.logger.debug('value:' + value)
if (key === 'player_') key = 'name'
else if (key === 'score_') key = 'score'
else if (key === 'deaths_') key = 'deaths'
else if (key === 'ping_') key = 'ping'
else if (key === 'team_') key = 'team'
else if (key === 'kills_') key = 'kills'
else if (key === 'team_t') key = 'name'
else if (key === 'tickets_t') key = 'tickets'
if (
key === 'score' || key === 'deaths' ||
key === 'ping' || key === 'team' ||
key === 'kills' || key === 'tickets'
) {
if (value === '') continue
value = parseInt(value)
}
unit[key] = value
}
units.push(unit)
}
return units
}
}
import Core from './core.js'
export default class gamespy2 extends Core {
constructor () {
super()
this.encoding = 'latin1'
this.byteorder = 'be'
}
async run (state) {
// Parse info
{
const body = await this.sendPacket([0xff, 0, 0])
const reader = this.reader(body)
while (!reader.done()) {
const key = reader.string()
const value = reader.string()
if (!key) break
state.raw[key] = value
}
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)
}
// Parse players
{
const body = await this.sendPacket([0, 0xff, 0])
const reader = this.reader(body)
for (const rawPlayer of this.readFieldData(reader)) {
state.players.push(rawPlayer)
}
if ('numplayers' in state.raw) state.numplayers = parseInt(state.raw.numplayers)
else state.numplayers = state.players.length
}
// Parse teams
{
const body = await this.sendPacket([0, 0, 0xff])
const reader = this.reader(body)
state.raw.teams = this.readFieldData(reader)
}
// Special case for america's army 1 and 2
// both use gamename = "armygame"
if (state.raw.gamename === 'armygame') {
const stripColor = (str) => {
// uses unreal 2 color codes
return str.replace(/\x1b...|[\x00-\x1a]/g, '')
}
state.name = stripColor(state.name)
state.map = stripColor(state.map)
for (const key of Object.keys(state.raw)) {
if (typeof state.raw[key] === 'string') {
state.raw[key] = stripColor(state.raw[key])
}
}
for (const player of state.players) {
if (!('name' in player)) continue
player.name = stripColor(player.name)
}
}
}
async sendPacket (type) {
const request = Buffer.concat([
Buffer.from([0xfe, 0xfd, 0x00]), // gamespy2
Buffer.from([0x00, 0x00, 0x00, 0x01]), // ping ID
Buffer.from(type)
])
return await this.udpSend(request, buffer => {
const reader = this.reader(buffer)
const header = reader.uint(1)
if (header !== 0) return
const pingId = reader.uint(4)
if (pingId !== 1) return
return reader.rest()
})
}
readFieldData (reader) {
reader.uint(1) // always 0
const count = reader.uint(1) // number of rows in this data
// some games omit the count byte entirely if it's 0 or at random (like americas army)
// Luckily, count should always be <64, and ascii characters will typically be >64,
// so we can detect this.
if (count > 64) {
reader.skip(-1)
this.logger.debug('Detected missing count byte, rewinding by 1')
} else {
this.logger.debug('Detected row count: ' + count)
}
this.logger.debug(() => 'Reading fields, starting at: ' + reader.rest())
const fields = []
while (!reader.done()) {
const field = reader.string()
if (!field) break
fields.push(field)
this.logger.debug('field:' + field)
}
if (!fields.length) return []
const units = []
while (!reader.done()) {
const unit = {}
for (let iField = 0; iField < fields.length; iField++) {
let key = fields[iField]
let value = reader.string()
if (!value && iField === 0) return units
this.logger.debug('value:' + value)
if (key === 'player_') key = 'name'
else if (key === 'score_') key = 'score'
else if (key === 'deaths_') key = 'deaths'
else if (key === 'ping_') key = 'ping'
else if (key === 'team_') key = 'team'
else if (key === 'kills_') key = 'kills'
else if (key === 'team_t') key = 'name'
else if (key === 'tickets_t') key = 'tickets'
if (
key === 'score' || key === 'deaths' ||
key === 'ping' || key === 'team' ||
key === 'kills' || key === 'tickets'
) {
if (value === '') continue
value = parseInt(value)
}
unit[key] = value
}
units.push(unit)
}
return units
}
}

View file

@ -1,197 +1,197 @@
import Core from './core.js'
export default class gamespy3 extends Core {
constructor () {
super()
this.sessionId = 1
this.encoding = 'latin1'
this.byteorder = 'be'
this.useOnlySingleSplit = false
this.isJc2mp = false
}
async run (state) {
const buffer = await this.sendPacket(9, false, false, false)
const reader = this.reader(buffer)
let challenge = parseInt(reader.string())
this.logger.debug('Received challenge key: ' + challenge)
if (challenge === 0) {
// Some servers send us a 0 if they don't want a challenge key used
// BF2 does this.
challenge = null
}
let requestPayload
if (this.isJc2mp) {
// they completely alter the protocol. because why not.
requestPayload = Buffer.from([0xff, 0xff, 0xff, 0x02])
} else {
requestPayload = Buffer.from([0xff, 0xff, 0xff, 0x01])
}
/** @type Buffer[] */
const packets = await this.sendPacket(0, challenge, requestPayload, true)
// iterate over the received packets
// the first packet will start off with k/v pairs, followed with data fields
// the following packets will only have data fields
state.raw.playerTeamInfo = {}
for (let iPacket = 0; iPacket < packets.length; iPacket++) {
const packet = packets[iPacket]
const reader = this.reader(packet)
this.logger.debug('Parsing packet #' + iPacket)
this.logger.debug(packet)
// Parse raw server key/values
if (iPacket === 0) {
while (!reader.done()) {
const key = reader.string()
if (!key) break
let value = reader.string()
while (value.match(/^p[0-9]+$/)) {
// fix a weird ut3 bug where some keys don't have values
value = reader.string()
}
state.raw[key] = value
this.logger.debug(key + ' = ' + value)
}
}
// Parse player, team, item array state
if (this.isJc2mp) {
state.raw.numPlayers2 = reader.uint(2)
while (!reader.done()) {
const player = {}
player.name = reader.string()
player.steamid = reader.string()
player.ping = reader.uint(2)
state.players.push(player)
}
} else {
while (!reader.done()) {
if (reader.uint(1) <= 2) continue
reader.skip(-1)
const fieldId = reader.string()
if (!fieldId) continue
const fieldIdSplit = fieldId.split('_')
const fieldName = fieldIdSplit[0]
const itemType = fieldIdSplit.length > 1 ? fieldIdSplit[1] : 'no_'
if (!(itemType in state.raw.playerTeamInfo)) {
state.raw.playerTeamInfo[itemType] = []
}
const items = state.raw.playerTeamInfo[itemType]
let offset = reader.uint(1)
this.logger.debug(() => 'Parsing new field: itemType=' + itemType + ' fieldName=' + fieldName + ' startOffset=' + offset)
while (!reader.done()) {
const item = reader.string()
if (!item) break
while (items.length <= offset) { items.push({}) }
items[offset][fieldName] = item
this.logger.debug('* ' + item)
offset++
}
}
}
}
// Turn all that raw state into something useful
if ('hostname' in state.raw) state.name = state.raw.hostname
else if ('servername' in state.raw) state.name = state.raw.servername
if ('mapname' in state.raw) state.map = state.raw.mapname
if (state.raw.password === '1') 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)
if ('' in state.raw.playerTeamInfo) {
for (const playerInfo of state.raw.playerTeamInfo['']) {
const player = {}
for (const from of Object.keys(playerInfo)) {
let key = from
let value = playerInfo[from]
if (key === 'player') key = 'name'
if (key === 'score' || key === 'ping' || key === 'team' || key === 'deaths' || key === 'pid') value = parseInt(value)
player[key] = value
}
state.players.push(player)
}
}
if ('numplayers' in state.raw) state.numplayers = parseInt(state.raw.numplayers)
else state.numplayers = state.players.length
}
async sendPacket (type, challenge, payload, assemble) {
const challengeLength = challenge === null ? 0 : 4
const payloadLength = payload ? payload.length : 0
const b = Buffer.alloc(7 + challengeLength + payloadLength)
b.writeUInt8(0xFE, 0)
b.writeUInt8(0xFD, 1)
b.writeUInt8(type, 2)
b.writeUInt32BE(this.sessionId, 3)
if (challengeLength) b.writeInt32BE(challenge, 7)
if (payloadLength) payload.copy(b, 7 + challengeLength)
let numPackets = 0
const packets = {}
return await this.udpSend(b, (buffer) => {
const reader = this.reader(buffer)
const iType = reader.uint(1)
if (iType !== type) {
this.logger.debug('Skipping packet, type mismatch')
return
}
const iSessionId = reader.uint(4)
if (iSessionId !== this.sessionId) {
this.logger.debug('Skipping packet, session id mismatch')
return
}
if (!assemble) {
return reader.rest()
}
if (this.useOnlySingleSplit) {
// has split headers, but they are worthless and only one packet is used
reader.skip(11)
return [reader.rest()]
}
reader.skip(9) // filler data -- usually set to 'splitnum\0'
let id = reader.uint(1)
const last = (id & 0x80)
id = id & 0x7f
if (last) numPackets = id + 1
reader.skip(1) // "another 'packet number' byte, but isn't understood."
packets[id] = reader.rest()
if (this.debug) {
this.logger.debug('Received packet #' + id + (last ? ' (last)' : ''))
}
if (!numPackets || Object.keys(packets).length !== numPackets) return
// assemble the parts
const list = []
for (let i = 0; i < numPackets; i++) {
if (!(i in packets)) {
throw new Error('Missing packet #' + i)
}
list.push(packets[i])
}
return list
})
}
}
import Core from './core.js'
export default class gamespy3 extends Core {
constructor () {
super()
this.sessionId = 1
this.encoding = 'latin1'
this.byteorder = 'be'
this.useOnlySingleSplit = false
this.isJc2mp = false
}
async run (state) {
const buffer = await this.sendPacket(9, false, false, false)
const reader = this.reader(buffer)
let challenge = parseInt(reader.string())
this.logger.debug('Received challenge key: ' + challenge)
if (challenge === 0) {
// Some servers send us a 0 if they don't want a challenge key used
// BF2 does this.
challenge = null
}
let requestPayload
if (this.isJc2mp) {
// they completely alter the protocol. because why not.
requestPayload = Buffer.from([0xff, 0xff, 0xff, 0x02])
} else {
requestPayload = Buffer.from([0xff, 0xff, 0xff, 0x01])
}
/** @type Buffer[] */
const packets = await this.sendPacket(0, challenge, requestPayload, true)
// iterate over the received packets
// the first packet will start off with k/v pairs, followed with data fields
// the following packets will only have data fields
state.raw.playerTeamInfo = {}
for (let iPacket = 0; iPacket < packets.length; iPacket++) {
const packet = packets[iPacket]
const reader = this.reader(packet)
this.logger.debug('Parsing packet #' + iPacket)
this.logger.debug(packet)
// Parse raw server key/values
if (iPacket === 0) {
while (!reader.done()) {
const key = reader.string()
if (!key) break
let value = reader.string()
while (value.match(/^p[0-9]+$/)) {
// fix a weird ut3 bug where some keys don't have values
value = reader.string()
}
state.raw[key] = value
this.logger.debug(key + ' = ' + value)
}
}
// Parse player, team, item array state
if (this.isJc2mp) {
state.raw.numPlayers2 = reader.uint(2)
while (!reader.done()) {
const player = {}
player.name = reader.string()
player.steamid = reader.string()
player.ping = reader.uint(2)
state.players.push(player)
}
} else {
while (!reader.done()) {
if (reader.uint(1) <= 2) continue
reader.skip(-1)
const fieldId = reader.string()
if (!fieldId) continue
const fieldIdSplit = fieldId.split('_')
const fieldName = fieldIdSplit[0]
const itemType = fieldIdSplit.length > 1 ? fieldIdSplit[1] : 'no_'
if (!(itemType in state.raw.playerTeamInfo)) {
state.raw.playerTeamInfo[itemType] = []
}
const items = state.raw.playerTeamInfo[itemType]
let offset = reader.uint(1)
this.logger.debug(() => 'Parsing new field: itemType=' + itemType + ' fieldName=' + fieldName + ' startOffset=' + offset)
while (!reader.done()) {
const item = reader.string()
if (!item) break
while (items.length <= offset) { items.push({}) }
items[offset][fieldName] = item
this.logger.debug('* ' + item)
offset++
}
}
}
}
// Turn all that raw state into something useful
if ('hostname' in state.raw) state.name = state.raw.hostname
else if ('servername' in state.raw) state.name = state.raw.servername
if ('mapname' in state.raw) state.map = state.raw.mapname
if (state.raw.password === '1') 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)
if ('' in state.raw.playerTeamInfo) {
for (const playerInfo of state.raw.playerTeamInfo['']) {
const player = {}
for (const from of Object.keys(playerInfo)) {
let key = from
let value = playerInfo[from]
if (key === 'player') key = 'name'
if (key === 'score' || key === 'ping' || key === 'team' || key === 'deaths' || key === 'pid') value = parseInt(value)
player[key] = value
}
state.players.push(player)
}
}
if ('numplayers' in state.raw) state.numplayers = parseInt(state.raw.numplayers)
else state.numplayers = state.players.length
}
async sendPacket (type, challenge, payload, assemble) {
const challengeLength = challenge === null ? 0 : 4
const payloadLength = payload ? payload.length : 0
const b = Buffer.alloc(7 + challengeLength + payloadLength)
b.writeUInt8(0xFE, 0)
b.writeUInt8(0xFD, 1)
b.writeUInt8(type, 2)
b.writeUInt32BE(this.sessionId, 3)
if (challengeLength) b.writeInt32BE(challenge, 7)
if (payloadLength) payload.copy(b, 7 + challengeLength)
let numPackets = 0
const packets = {}
return await this.udpSend(b, (buffer) => {
const reader = this.reader(buffer)
const iType = reader.uint(1)
if (iType !== type) {
this.logger.debug('Skipping packet, type mismatch')
return
}
const iSessionId = reader.uint(4)
if (iSessionId !== this.sessionId) {
this.logger.debug('Skipping packet, session id mismatch')
return
}
if (!assemble) {
return reader.rest()
}
if (this.useOnlySingleSplit) {
// has split headers, but they are worthless and only one packet is used
reader.skip(11)
return [reader.rest()]
}
reader.skip(9) // filler data -- usually set to 'splitnum\0'
let id = reader.uint(1)
const last = (id & 0x80)
id = id & 0x7f
if (last) numPackets = id + 1
reader.skip(1) // "another 'packet number' byte, but isn't understood."
packets[id] = reader.rest()
if (this.debug) {
this.logger.debug('Received packet #' + id + (last ? ' (last)' : ''))
}
if (!numPackets || Object.keys(packets).length !== numPackets) return
// assemble the parts
const list = []
for (let i = 0; i < numPackets; i++) {
if (!(i in packets)) {
throw new Error('Missing packet #' + i)
}
list.push(packets[i])
}
return list
})
}
}

View file

@ -1,46 +1,46 @@
import Core from './core.js'
export default class geneshift extends Core {
async run (state) {
await this.tcpPing()
const body = await this.request({
url: 'http://geneshift.net/game/receiveLobby.php'
})
const split = body.split('<br/>')
let found = null
for (const line of split) {
const fields = line.split('::')
const ip = fields[2]
const port = fields[3]
if (ip === this.options.address && parseInt(port) === this.options.port) {
found = fields
break
}
}
if (found === null) {
throw new Error('Server not found in list')
}
state.raw.countrycode = found[0]
state.raw.country = found[1]
state.name = found[4]
state.map = found[5]
state.numplayers = parseInt(found[6])
state.maxplayers = parseInt(found[7])
// fields[8] is unknown?
state.raw.rules = found[9]
state.raw.gamemode = parseInt(found[10])
state.raw.gangsters = parseInt(found[11])
state.raw.cashrate = parseInt(found[12])
state.raw.missions = !!parseInt(found[13])
state.raw.vehicles = !!parseInt(found[14])
state.raw.customweapons = !!parseInt(found[15])
state.raw.friendlyfire = !!parseInt(found[16])
state.raw.mercs = !!parseInt(found[17])
// fields[18] is unknown? listen server?
state.raw.version = found[19]
}
}
import Core from './core.js'
export default class geneshift extends Core {
async run (state) {
await this.tcpPing()
const body = await this.request({
url: 'http://geneshift.net/game/receiveLobby.php'
})
const split = body.split('<br/>')
let found = null
for (const line of split) {
const fields = line.split('::')
const ip = fields[2]
const port = fields[3]
if (ip === this.options.address && parseInt(port) === this.options.port) {
found = fields
break
}
}
if (found === null) {
throw new Error('Server not found in list')
}
state.raw.countrycode = found[0]
state.raw.country = found[1]
state.name = found[4]
state.map = found[5]
state.numplayers = parseInt(found[6])
state.maxplayers = parseInt(found[7])
// fields[8] is unknown?
state.raw.rules = found[9]
state.raw.gamemode = parseInt(found[10])
state.raw.gangsters = parseInt(found[11])
state.raw.cashrate = parseInt(found[12])
state.raw.missions = !!parseInt(found[13])
state.raw.vehicles = !!parseInt(found[14])
state.raw.customweapons = !!parseInt(found[15])
state.raw.friendlyfire = !!parseInt(found[16])
state.raw.mercs = !!parseInt(found[17])
// fields[18] is unknown? listen server?
state.raw.version = found[19]
}
}

View file

@ -1,8 +1,8 @@
import valve from './valve.js'
export default class goldsrc extends valve {
constructor () {
super()
this.goldsrcInfo = true
}
}
import valve from './valve.js'
export default class goldsrc extends valve {
constructor () {
super()
this.goldsrcInfo = true
}
}

View file

@ -1,14 +1,14 @@
import quake1 from './quake1.js'
export default class hexen2 extends quake1 {
constructor () {
super()
this.sendHeader = '\xFFstatus\x0a'
this.responseHeader = '\xffn'
import quake1 from './quake1.js'
export default class hexen2 extends quake1 {
constructor () {
super()
this.sendHeader = '\xFFstatus\x0a'
this.responseHeader = '\xffn'
}
async run (state) {
await super.run(state)
state.gamePort = this.options.port - 50
}
}
async run (state) {
await super.run(state)
state.gamePort = this.options.port - 50
}
}

View file

@ -1,57 +1,57 @@
import armagetron from './armagetron.js'
import ase from './ase.js'
import asa from './asa.js'
import assettocorsa from './assettocorsa.js'
import battlefield from './battlefield.js'
import buildandshoot from './buildandshoot.js'
import cs2d from './cs2d.js'
import discord from './discord.js'
import doom3 from './doom3.js'
import eco from './eco.js'
import epic from './epic.js'
import ffow from './ffow.js'
import fivem from './fivem.js'
import gamespy1 from './gamespy1.js'
import gamespy2 from './gamespy2.js'
import gamespy3 from './gamespy3.js'
import geneshift from './geneshift.js'
import goldsrc from './goldsrc.js'
import hexen2 from './hexen2.js'
import jc2mp from './jc2mp.js'
import kspdmp from './kspdmp.js'
import mafia2mp from './mafia2mp.js'
import mafia2online from './mafia2online.js'
import minecraft from './minecraft.js'
import minecraftbedrock from './minecraftbedrock.js'
import minecraftvanilla from './minecraftvanilla.js'
import mumble from './mumble.js'
import mumbleping from './mumbleping.js'
import nadeo from './nadeo.js'
import openttd from './openttd.js'
import quake1 from './quake1.js'
import quake2 from './quake2.js'
import quake3 from './quake3.js'
import rfactor from './rfactor.js'
import samp from './samp.js'
import savage2 from './savage2.js'
import starmade from './starmade.js'
import starsiege from './starsiege.js'
import teamspeak2 from './teamspeak2.js'
import teamspeak3 from './teamspeak3.js'
import terraria from './terraria.js'
import tribes1 from './tribes1.js'
import tribes1master from './tribes1master.js'
import unreal2 from './unreal2.js'
import ut3 from './ut3.js'
import valve from './valve.js'
import vcmp from './vcmp.js'
import ventrilo from './ventrilo.js'
import warsow from './warsow.js'
export {
armagetron, ase, asa, assettocorsa, battlefield, buildandshoot, cs2d, discord, doom3, eco, epic, ffow, fivem, gamespy1,
gamespy2, gamespy3, geneshift, goldsrc, hexen2, jc2mp, kspdmp, mafia2mp, mafia2online, minecraft,
minecraftbedrock, minecraftvanilla, mumble, mumbleping, nadeo, openttd, quake1, quake2, quake3, rfactor, samp,
savage2, starmade, starsiege, teamspeak2, teamspeak3, terraria, tribes1, tribes1master, unreal2, ut3, valve,
vcmp, ventrilo, warsow
}
import armagetron from './armagetron.js'
import ase from './ase.js'
import asa from './asa.js'
import assettocorsa from './assettocorsa.js'
import battlefield from './battlefield.js'
import buildandshoot from './buildandshoot.js'
import cs2d from './cs2d.js'
import discord from './discord.js'
import doom3 from './doom3.js'
import eco from './eco.js'
import epic from './epic.js'
import ffow from './ffow.js'
import fivem from './fivem.js'
import gamespy1 from './gamespy1.js'
import gamespy2 from './gamespy2.js'
import gamespy3 from './gamespy3.js'
import geneshift from './geneshift.js'
import goldsrc from './goldsrc.js'
import hexen2 from './hexen2.js'
import jc2mp from './jc2mp.js'
import kspdmp from './kspdmp.js'
import mafia2mp from './mafia2mp.js'
import mafia2online from './mafia2online.js'
import minecraft from './minecraft.js'
import minecraftbedrock from './minecraftbedrock.js'
import minecraftvanilla from './minecraftvanilla.js'
import mumble from './mumble.js'
import mumbleping from './mumbleping.js'
import nadeo from './nadeo.js'
import openttd from './openttd.js'
import quake1 from './quake1.js'
import quake2 from './quake2.js'
import quake3 from './quake3.js'
import rfactor from './rfactor.js'
import samp from './samp.js'
import savage2 from './savage2.js'
import starmade from './starmade.js'
import starsiege from './starsiege.js'
import teamspeak2 from './teamspeak2.js'
import teamspeak3 from './teamspeak3.js'
import terraria from './terraria.js'
import tribes1 from './tribes1.js'
import tribes1master from './tribes1master.js'
import unreal2 from './unreal2.js'
import ut3 from './ut3.js'
import valve from './valve.js'
import vcmp from './vcmp.js'
import ventrilo from './ventrilo.js'
import warsow from './warsow.js'
export {
armagetron, ase, asa, assettocorsa, battlefield, buildandshoot, cs2d, discord, doom3, eco, epic, ffow, fivem, gamespy1,
gamespy2, gamespy3, geneshift, goldsrc, hexen2, jc2mp, kspdmp, mafia2mp, mafia2online, minecraft,
minecraftbedrock, minecraftvanilla, mumble, mumbleping, nadeo, openttd, quake1, quake2, quake3, rfactor, samp,
savage2, starmade, starsiege, teamspeak2, teamspeak3, terraria, tribes1, tribes1master, unreal2, ut3, valve,
vcmp, ventrilo, warsow
}

View file

@ -1,16 +1,16 @@
import gamespy3 from './gamespy3.js'
// supposedly, gamespy3 is the "official" query protocol for jcmp,
// but it's broken (requires useOnlySingleSplit), and may not include some player names
export default class jc2mp extends gamespy3 {
constructor () {
super()
this.useOnlySingleSplit = true
this.isJc2mp = true
this.encoding = 'utf8'
}
async run (state) {
await super.run(state)
}
}
import gamespy3 from './gamespy3.js'
// supposedly, gamespy3 is the "official" query protocol for jcmp,
// but it's broken (requires useOnlySingleSplit), and may not include some player names
export default class jc2mp extends gamespy3 {
constructor () {
super()
this.useOnlySingleSplit = true
this.isJc2mp = true
this.encoding = 'utf8'
}
async run (state) {
await super.run(state)
}
}

View file

@ -1,28 +1,28 @@
import Core from './core.js'
export default class kspdmp extends Core {
async run (state) {
const json = await this.request({
url: 'http://' + this.options.address + ':' + this.options.port,
responseType: 'json'
})
for (const one of json.players) {
state.players.push({ name: one.nickname, team: one.team })
}
for (const key of Object.keys(json)) {
state.raw[key] = json[key]
}
state.name = json.server_name
state.maxplayers = json.max_players
state.gamePort = json.port
if (json.players) {
const split = json.players.split(', ')
for (const name of split) {
state.players.push({ name })
}
}
state.numplayers = state.players.length
}
}
import Core from './core.js'
export default class kspdmp extends Core {
async run (state) {
const json = await this.request({
url: 'http://' + this.options.address + ':' + this.options.port,
responseType: 'json'
})
for (const one of json.players) {
state.players.push({ name: one.nickname, team: one.team })
}
for (const key of Object.keys(json)) {
state.raw[key] = json[key]
}
state.name = json.server_name
state.maxplayers = json.max_players
state.gamePort = json.port
if (json.players) {
const split = json.players.split(', ')
for (const name of split) {
state.players.push({ name })
}
}
state.numplayers = state.players.length
}
}

View file

@ -1,41 +1,41 @@
import Core from './core.js'
export default class mafia2mp extends Core {
constructor () {
super()
this.encoding = 'latin1'
this.header = 'M2MP'
this.isMafia2Online = false
}
async run (state) {
const body = await this.udpSend(this.header, (buffer) => {
const reader = this.reader(buffer)
const header = reader.string(this.header.length)
if (header !== this.header) return
return reader.rest()
})
const reader = this.reader(body)
state.name = this.readString(reader)
state.numplayers = parseInt(this.readString(reader))
state.maxplayers = parseInt(this.readString(reader))
state.raw.gamemode = this.readString(reader)
state.password = !!reader.uint(1)
state.gamePort = this.options.port - 1
while (!reader.done()) {
const player = {}
player.name = this.readString(reader)
if (!player.name) break
if (this.isMafia2Online) {
player.ping = parseInt(this.readString(reader))
}
state.players.push(player)
}
}
readString (reader) {
return reader.pascalString(1, -1)
}
}
import Core from './core.js'
export default class mafia2mp extends Core {
constructor () {
super()
this.encoding = 'latin1'
this.header = 'M2MP'
this.isMafia2Online = false
}
async run (state) {
const body = await this.udpSend(this.header, (buffer) => {
const reader = this.reader(buffer)
const header = reader.string(this.header.length)
if (header !== this.header) return
return reader.rest()
})
const reader = this.reader(body)
state.name = this.readString(reader)
state.numplayers = parseInt(this.readString(reader))
state.maxplayers = parseInt(this.readString(reader))
state.raw.gamemode = this.readString(reader)
state.password = !!reader.uint(1)
state.gamePort = this.options.port - 1
while (!reader.done()) {
const player = {}
player.name = this.readString(reader)
if (!player.name) break
if (this.isMafia2Online) {
player.ping = parseInt(this.readString(reader))
}
state.players.push(player)
}
}
readString (reader) {
return reader.pascalString(1, -1)
}
}

View file

@ -1,9 +1,9 @@
import mafia2mp from './mafia2mp.js'
export default class mafia2online extends mafia2mp {
constructor () {
super()
this.header = 'M2Online'
this.isMafia2Online = true
}
}
import mafia2mp from './mafia2mp.js'
export default class mafia2online extends mafia2mp {
constructor () {
super()
this.header = 'M2Online'
this.isMafia2Online = true
}
}

View file

@ -1,102 +1,102 @@
import Core from './core.js'
import minecraftbedrock from './minecraftbedrock.js'
import minecraftvanilla from './minecraftvanilla.js'
import Gamespy3 from './gamespy3.js'
/*
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
*/
export default class minecraft extends Core {
constructor () {
super()
this.srvRecord = '_minecraft._tcp'
}
async run (state) {
/** @type {Promise<Results>[]} */
const promises = []
const vanillaResolver = new minecraftvanilla()
vanillaResolver.options = this.options
vanillaResolver.udpSocket = this.udpSocket
promises.push((async () => {
try { return await vanillaResolver.runOnceSafe() } catch (e) {}
})())
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, gamespyState, bedrockState] = await Promise.all(promises)
state.raw.vanilla = vanillaState
state.raw.gamespy = gamespyState
state.raw.bedrock = 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.players.length) state.players = bedrockState.players
}
if (vanillaState) {
try {
let name = ''
const description = vanillaState.raw.description
if (typeof description === 'string') {
name = description
}
if (!name && typeof description === 'object' && description.text) {
name = description.text
}
if (!name && typeof description === 'object' && description.extra) {
name = description.extra.map(part => part.text).join('')
}
state.name = name
} catch (e) {}
if (vanillaState.numplayers) state.numplayers = vanillaState.numplayers
if (vanillaState.maxplayers) state.maxplayers = vanillaState.maxplayers
if (vanillaState.players.length) state.players = vanillaState.players
if (vanillaState.ping) this.registerRtt(vanillaState.ping)
}
if (gamespyState) {
if (gamespyState.name) state.name = gamespyState.name
if (gamespyState.numplayers) state.numplayers = gamespyState.numplayers
if (gamespyState.maxplayers) state.maxplayers = gamespyState.maxplayers
if (gamespyState.players.length) state.players = gamespyState.players
else if (gamespyState.numplayers) state.numplayers = gamespyState.numplayers
if (gamespyState.ping) this.registerRtt(gamespyState.ping)
}
if (bedrockState) {
if (bedrockState.name) state.name = bedrockState.name
if (bedrockState.numplayers) state.numplayers = bedrockState.numplayers
if (bedrockState.maxplayers) state.maxplayers = bedrockState.maxplayers
if (bedrockState.map) state.map = bedrockState.map
if (bedrockState.ping) this.registerRtt(bedrockState.ping)
}
// remove dupe spaces from name
state.name = state.name.replace(/\s+/g, ' ')
// remove color codes from name
state.name = state.name.replace(/\u00A7./g, '')
}
}
import Core from './core.js'
import minecraftbedrock from './minecraftbedrock.js'
import minecraftvanilla from './minecraftvanilla.js'
import Gamespy3 from './gamespy3.js'
/*
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
*/
export default class minecraft extends Core {
constructor () {
super()
this.srvRecord = '_minecraft._tcp'
}
async run (state) {
/** @type {Promise<Results>[]} */
const promises = []
const vanillaResolver = new minecraftvanilla()
vanillaResolver.options = this.options
vanillaResolver.udpSocket = this.udpSocket
promises.push((async () => {
try { return await vanillaResolver.runOnceSafe() } catch (e) {}
})())
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, gamespyState, bedrockState] = await Promise.all(promises)
state.raw.vanilla = vanillaState
state.raw.gamespy = gamespyState
state.raw.bedrock = 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.players.length) state.players = bedrockState.players
}
if (vanillaState) {
try {
let name = ''
const description = vanillaState.raw.description
if (typeof description === 'string') {
name = description
}
if (!name && typeof description === 'object' && description.text) {
name = description.text
}
if (!name && typeof description === 'object' && description.extra) {
name = description.extra.map(part => part.text).join('')
}
state.name = name
} catch (e) {}
if (vanillaState.numplayers) state.numplayers = vanillaState.numplayers
if (vanillaState.maxplayers) state.maxplayers = vanillaState.maxplayers
if (vanillaState.players.length) state.players = vanillaState.players
if (vanillaState.ping) this.registerRtt(vanillaState.ping)
}
if (gamespyState) {
if (gamespyState.name) state.name = gamespyState.name
if (gamespyState.numplayers) state.numplayers = gamespyState.numplayers
if (gamespyState.maxplayers) state.maxplayers = gamespyState.maxplayers
if (gamespyState.players.length) state.players = gamespyState.players
else if (gamespyState.numplayers) state.numplayers = gamespyState.numplayers
if (gamespyState.ping) this.registerRtt(gamespyState.ping)
}
if (bedrockState) {
if (bedrockState.name) state.name = bedrockState.name
if (bedrockState.numplayers) state.numplayers = bedrockState.numplayers
if (bedrockState.maxplayers) state.maxplayers = bedrockState.maxplayers
if (bedrockState.map) state.map = bedrockState.map
if (bedrockState.ping) this.registerRtt(bedrockState.ping)
}
// remove dupe spaces from name
state.name = state.name.replace(/\s+/g, ' ')
// remove color codes from name
state.name = state.name.replace(/\u00A7./g, '')
}
}

View file

@ -1,72 +1,72 @@
import Core from './core.js'
export default 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.numplayers = 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
})
}
}
import Core from './core.js'
export default 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.numplayers = 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
})
}
}

View file

@ -1,75 +1,75 @@
import Core from './core.js'
import Varint from 'varint'
export default class minecraftvanilla extends Core {
async run (state) {
const portBuf = Buffer.alloc(2)
portBuf.writeUInt16BE(this.options.port, 0)
const addressBuf = Buffer.from(this.options.host, 'utf8')
const bufs = [
this.varIntBuffer(47),
this.varIntBuffer(addressBuf.length),
addressBuf,
portBuf,
this.varIntBuffer(1)
]
const outBuffer = Buffer.concat([
this.buildPacket(0, Buffer.concat(bufs)),
this.buildPacket(0)
])
const data = await this.withTcp(async socket => {
return await this.tcpSend(socket, outBuffer, data => {
if (data.length < 10) return
const reader = this.reader(data)
const length = reader.varint()
if (data.length < length) return
return reader.rest()
})
})
const reader = this.reader(data)
const packetId = reader.varint()
this.logger.debug('Packet ID: ' + packetId)
const strLen = reader.varint()
this.logger.debug('String Length: ' + strLen)
const str = reader.rest().toString('utf8')
this.logger.debug(str)
const json = JSON.parse(str)
delete json.favicon
state.raw = json
state.maxplayers = json.players.max
state.numplayers = json.players.online
if (json.players.sample) {
for (const player of json.players.sample) {
state.players.push({
id: player.id,
name: player.name
})
}
}
}
varIntBuffer (num) {
return Buffer.from(Varint.encode(num))
}
buildPacket (id, data) {
if (!data) data = Buffer.from([])
const idBuffer = this.varIntBuffer(id)
return Buffer.concat([
this.varIntBuffer(data.length + idBuffer.length),
idBuffer,
data
])
}
}
import Core from './core.js'
import Varint from 'varint'
export default class minecraftvanilla extends Core {
async run (state) {
const portBuf = Buffer.alloc(2)
portBuf.writeUInt16BE(this.options.port, 0)
const addressBuf = Buffer.from(this.options.host, 'utf8')
const bufs = [
this.varIntBuffer(47),
this.varIntBuffer(addressBuf.length),
addressBuf,
portBuf,
this.varIntBuffer(1)
]
const outBuffer = Buffer.concat([
this.buildPacket(0, Buffer.concat(bufs)),
this.buildPacket(0)
])
const data = await this.withTcp(async socket => {
return await this.tcpSend(socket, outBuffer, data => {
if (data.length < 10) return
const reader = this.reader(data)
const length = reader.varint()
if (data.length < length) return
return reader.rest()
})
})
const reader = this.reader(data)
const packetId = reader.varint()
this.logger.debug('Packet ID: ' + packetId)
const strLen = reader.varint()
this.logger.debug('String Length: ' + strLen)
const str = reader.rest().toString('utf8')
this.logger.debug(str)
const json = JSON.parse(str)
delete json.favicon
state.raw = json
state.maxplayers = json.players.max
state.numplayers = json.players.online
if (json.players.sample) {
for (const player of json.players.sample) {
state.players.push({
id: player.id,
name: player.name
})
}
}
}
varIntBuffer (num) {
return Buffer.from(Varint.encode(num))
}
buildPacket (id, data) {
if (!data) data = Buffer.from([])
const idBuffer = this.varIntBuffer(id)
return Buffer.concat([
this.varIntBuffer(data.length + idBuffer.length),
idBuffer,
data
])
}
}

View file

@ -1,39 +1,39 @@
import Core from './core.js'
export default class mumble extends Core {
async run (state) {
const json = await this.withTcp(async socket => {
return await this.tcpSend(socket, 'json', (buffer) => {
if (buffer.length < 10) return
const str = buffer.toString()
let json
try {
json = JSON.parse(str)
} catch (e) {
// probably not all here yet
return
}
return json
})
})
state.raw = json
state.name = json.name
state.gamePort = json.x_gtmurmur_connectport || 64738
let channelStack = [state.raw.root]
while (channelStack.length) {
const channel = channelStack.shift()
channel.description = this.cleanComment(channel.description)
channelStack = channelStack.concat(channel.channels)
for (const user of channel.users) {
user.comment = this.cleanComment(user.comment)
state.players.push(user)
}
}
}
cleanComment (str) {
return str.replace(/<.*>/g, '')
}
}
import Core from './core.js'
export default class mumble extends Core {
async run (state) {
const json = await this.withTcp(async socket => {
return await this.tcpSend(socket, 'json', (buffer) => {
if (buffer.length < 10) return
const str = buffer.toString()
let json
try {
json = JSON.parse(str)
} catch (e) {
// probably not all here yet
return
}
return json
})
})
state.raw = json
state.name = json.name
state.gamePort = json.x_gtmurmur_connectport || 64738
let channelStack = [state.raw.root]
while (channelStack.length) {
const channel = channelStack.shift()
channel.description = this.cleanComment(channel.description)
channelStack = channelStack.concat(channel.channels)
for (const user of channel.users) {
user.comment = this.cleanComment(user.comment)
state.players.push(user)
}
}
}
cleanComment (str) {
return str.replace(/<.*>/g, '')
}
}

View file

@ -1,24 +1,24 @@
import Core from './core.js'
export default class mumbleping extends Core {
constructor () {
super()
this.byteorder = 'be'
}
async run (state) {
const data = await this.udpSend('\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08', (buffer) => {
if (buffer.length >= 24) return buffer
})
const reader = this.reader(data)
reader.skip(1)
state.raw.versionMajor = reader.uint(1)
state.raw.versionMinor = reader.uint(1)
state.raw.versionPatch = reader.uint(1)
reader.skip(8)
state.numplayers = reader.uint(4)
state.maxplayers = reader.uint(4)
state.raw.allowedbandwidth = reader.uint(4)
}
}
import Core from './core.js'
export default class mumbleping extends Core {
constructor () {
super()
this.byteorder = 'be'
}
async run (state) {
const data = await this.udpSend('\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08', (buffer) => {
if (buffer.length >= 24) return buffer
})
const reader = this.reader(data)
reader.skip(1)
state.raw.versionMajor = reader.uint(1)
state.raw.versionMinor = reader.uint(1)
state.raw.versionPatch = reader.uint(1)
reader.skip(8)
state.numplayers = reader.uint(4)
state.maxplayers = reader.uint(4)
state.raw.allowedbandwidth = reader.uint(4)
}
}

View file

@ -1,86 +1,86 @@
import Core from './core.js'
import Promises from '../lib/Promises.js'
import * as gbxremote from 'gbxremote'
export default class nadeo extends Core {
async run (state) {
await this.withClient(async client => {
const start = Date.now()
await this.query(client, 'Authenticate', this.options.login, this.options.password)
this.registerRtt(Date.now() - start)
// const data = this.methodCall(client, 'GetStatus');
{
const results = await this.query(client, 'GetServerOptions')
state.name = this.stripColors(results.Name)
state.password = (results.Password !== 'No password')
state.maxplayers = results.CurrentMaxPlayers
state.raw.maxspectators = results.CurrentMaxSpectators
}
{
const results = await this.query(client, 'GetCurrentMapInfo')
state.map = this.stripColors(results.Name)
state.raw.mapUid = results.UId
}
{
const results = await this.query(client, 'GetCurrentGameInfo')
let gamemode = ''
const igm = results.GameMode
if (igm === 0) gamemode = 'Rounds'
if (igm === 1) gamemode = 'Time Attack'
if (igm === 2) gamemode = 'Team'
if (igm === 3) gamemode = 'Laps'
if (igm === 4) gamemode = 'Stunts'
if (igm === 5) gamemode = 'Cup'
state.raw.gametype = gamemode
state.raw.mapcount = results.NbChallenge
}
{
const results = await this.query(client, 'GetNextMapInfo')
state.raw.nextmapName = this.stripColors(results.Name)
state.raw.nextmapUid = results.UId
}
if (this.options.port === 5000) {
state.gamePort = 2350
}
state.raw.players = await this.query(client, 'GetPlayerList', 10000, 0)
for (const player of state.raw.players) {
state.players.push({
name: this.stripColors(player.Name || player.NickName)
})
}
state.numplayers = state.players.length
})
}
async withClient (fn) {
const socket = new gbxremote.Client(this.options.port, this.options.host)
try {
const connectPromise = socket.connect()
const timeoutPromise = Promises.createTimeout(this.options.socketTimeout, 'GBX Remote Opening')
await Promise.race([connectPromise, timeoutPromise, this.abortedPromise])
return await fn(socket)
} finally {
socket.terminate()
}
}
async query (client, ...cmdset) {
const cmd = cmdset[0]
const params = cmdset.slice(1)
const sentPromise = client.query(cmd, params)
const timeoutPromise = Promises.createTimeout(this.options.socketTimeout, 'GBX Method Call')
return await Promise.race([sentPromise, timeoutPromise, this.abortedPromise])
}
stripColors (str) {
return str.replace(/\$([0-9a-f]{3}|[a-z])/gi, '')
}
}
import Core from './core.js'
import Promises from '../lib/Promises.js'
import * as gbxremote from 'gbxremote'
export default class nadeo extends Core {
async run (state) {
await this.withClient(async client => {
const start = Date.now()
await this.query(client, 'Authenticate', this.options.login, this.options.password)
this.registerRtt(Date.now() - start)
// const data = this.methodCall(client, 'GetStatus');
{
const results = await this.query(client, 'GetServerOptions')
state.name = this.stripColors(results.Name)
state.password = (results.Password !== 'No password')
state.maxplayers = results.CurrentMaxPlayers
state.raw.maxspectators = results.CurrentMaxSpectators
}
{
const results = await this.query(client, 'GetCurrentMapInfo')
state.map = this.stripColors(results.Name)
state.raw.mapUid = results.UId
}
{
const results = await this.query(client, 'GetCurrentGameInfo')
let gamemode = ''
const igm = results.GameMode
if (igm === 0) gamemode = 'Rounds'
if (igm === 1) gamemode = 'Time Attack'
if (igm === 2) gamemode = 'Team'
if (igm === 3) gamemode = 'Laps'
if (igm === 4) gamemode = 'Stunts'
if (igm === 5) gamemode = 'Cup'
state.raw.gametype = gamemode
state.raw.mapcount = results.NbChallenge
}
{
const results = await this.query(client, 'GetNextMapInfo')
state.raw.nextmapName = this.stripColors(results.Name)
state.raw.nextmapUid = results.UId
}
if (this.options.port === 5000) {
state.gamePort = 2350
}
state.raw.players = await this.query(client, 'GetPlayerList', 10000, 0)
for (const player of state.raw.players) {
state.players.push({
name: this.stripColors(player.Name || player.NickName)
})
}
state.numplayers = state.players.length
})
}
async withClient (fn) {
const socket = new gbxremote.Client(this.options.port, this.options.host)
try {
const connectPromise = socket.connect()
const timeoutPromise = Promises.createTimeout(this.options.socketTimeout, 'GBX Remote Opening')
await Promise.race([connectPromise, timeoutPromise, this.abortedPromise])
return await fn(socket)
} finally {
socket.terminate()
}
}
async query (client, ...cmdset) {
const cmd = cmdset[0]
const params = cmdset.slice(1)
const sentPromise = client.query(cmd, params)
const timeoutPromise = Promises.createTimeout(this.options.socketTimeout, 'GBX Method Call')
return await Promise.race([sentPromise, timeoutPromise, this.abortedPromise])
}
stripColors (str) {
return str.replace(/\$([0-9a-f]{3}|[a-z])/gi, '')
}
}

View file

@ -1,127 +1,127 @@
import Core from './core.js'
export default class openttd extends Core {
async run (state) {
{
const [reader, version] = await this.query(0, 1, 1, 4)
if (version >= 4) {
const numGrf = reader.uint(1)
state.raw.grfs = []
for (let i = 0; i < numGrf; i++) {
const grf = {}
grf.id = reader.part(4).toString('hex')
grf.md5 = reader.part(16).toString('hex')
state.raw.grfs.push(grf)
}
}
if (version >= 3) {
state.raw.date_current = this.readDate(reader)
state.raw.date_start = this.readDate(reader)
}
if (version >= 2) {
state.raw.maxcompanies = reader.uint(1)
state.raw.numcompanies = reader.uint(1)
state.raw.maxspectators = reader.uint(1)
}
state.name = reader.string()
state.raw.version = reader.string()
state.raw.language = this.decode(
reader.uint(1),
['any', 'en', 'de', 'fr']
)
state.password = !!reader.uint(1)
state.maxplayers = reader.uint(1)
state.numplayers = reader.uint(1)
state.raw.numspectators = reader.uint(1)
state.map = reader.string()
state.raw.map_width = reader.uint(2)
state.raw.map_height = reader.uint(2)
state.raw.landscape = this.decode(
reader.uint(1),
['temperate', 'arctic', 'desert', 'toyland']
)
state.raw.dedicated = !!reader.uint(1)
}
{
const [reader, version] = await this.query(2, 3, -1, -1)
// we don't know how to deal with companies outside version 6
if (version === 6) {
state.raw.companies = []
const numCompanies = reader.uint(1)
for (let iCompany = 0; iCompany < numCompanies; iCompany++) {
const company = {}
company.id = reader.uint(1)
company.name = reader.string()
company.year_start = reader.uint(4)
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)
const vehicleTypes = ['train', 'truck', 'bus', 'aircraft', 'ship']
const stationTypes = ['station', 'truckbay', 'busstation', 'airport', 'dock']
company.vehicles = {}
for (const type of vehicleTypes) {
company.vehicles[type] = reader.uint(2)
}
company.stations = {}
for (const type of stationTypes) {
company.stations[type] = reader.uint(2)
}
company.clients = reader.string()
state.raw.companies.push(company)
}
}
}
}
async query (type, expected, minver, maxver) {
const b = Buffer.from([0x03, 0x00, type])
return await this.udpSend(b, (buffer) => {
const reader = this.reader(buffer)
const packetLen = reader.uint(2)
if (packetLen !== buffer.length) {
this.logger.debug('Invalid reported packet length: ' + packetLen + ' ' + buffer.length)
return
}
const packetType = reader.uint(1)
if (packetType !== expected) {
this.logger.debug('Unexpected response packet type: ' + packetType)
return
}
const protocolVersion = reader.uint(1)
if ((minver !== -1 && protocolVersion < minver) || (maxver !== -1 && protocolVersion > maxver)) {
throw new Error('Unknown protocol version: ' + protocolVersion + ' Expected: ' + minver + '-' + maxver)
}
return [reader, protocolVersion]
})
}
readDate (reader) {
const daysSinceZero = reader.uint(4)
const temp = new Date(0, 0, 1)
temp.setFullYear(0)
temp.setDate(daysSinceZero + 1)
return temp.toISOString().split('T')[0]
}
decode (num, arr) {
if (num < 0 || num >= arr.length) {
return num
}
return arr[num]
}
}
import Core from './core.js'
export default class openttd extends Core {
async run (state) {
{
const [reader, version] = await this.query(0, 1, 1, 4)
if (version >= 4) {
const numGrf = reader.uint(1)
state.raw.grfs = []
for (let i = 0; i < numGrf; i++) {
const grf = {}
grf.id = reader.part(4).toString('hex')
grf.md5 = reader.part(16).toString('hex')
state.raw.grfs.push(grf)
}
}
if (version >= 3) {
state.raw.date_current = this.readDate(reader)
state.raw.date_start = this.readDate(reader)
}
if (version >= 2) {
state.raw.maxcompanies = reader.uint(1)
state.raw.numcompanies = reader.uint(1)
state.raw.maxspectators = reader.uint(1)
}
state.name = reader.string()
state.raw.version = reader.string()
state.raw.language = this.decode(
reader.uint(1),
['any', 'en', 'de', 'fr']
)
state.password = !!reader.uint(1)
state.maxplayers = reader.uint(1)
state.numplayers = reader.uint(1)
state.raw.numspectators = reader.uint(1)
state.map = reader.string()
state.raw.map_width = reader.uint(2)
state.raw.map_height = reader.uint(2)
state.raw.landscape = this.decode(
reader.uint(1),
['temperate', 'arctic', 'desert', 'toyland']
)
state.raw.dedicated = !!reader.uint(1)
}
{
const [reader, version] = await this.query(2, 3, -1, -1)
// we don't know how to deal with companies outside version 6
if (version === 6) {
state.raw.companies = []
const numCompanies = reader.uint(1)
for (let iCompany = 0; iCompany < numCompanies; iCompany++) {
const company = {}
company.id = reader.uint(1)
company.name = reader.string()
company.year_start = reader.uint(4)
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)
const vehicleTypes = ['train', 'truck', 'bus', 'aircraft', 'ship']
const stationTypes = ['station', 'truckbay', 'busstation', 'airport', 'dock']
company.vehicles = {}
for (const type of vehicleTypes) {
company.vehicles[type] = reader.uint(2)
}
company.stations = {}
for (const type of stationTypes) {
company.stations[type] = reader.uint(2)
}
company.clients = reader.string()
state.raw.companies.push(company)
}
}
}
}
async query (type, expected, minver, maxver) {
const b = Buffer.from([0x03, 0x00, type])
return await this.udpSend(b, (buffer) => {
const reader = this.reader(buffer)
const packetLen = reader.uint(2)
if (packetLen !== buffer.length) {
this.logger.debug('Invalid reported packet length: ' + packetLen + ' ' + buffer.length)
return
}
const packetType = reader.uint(1)
if (packetType !== expected) {
this.logger.debug('Unexpected response packet type: ' + packetType)
return
}
const protocolVersion = reader.uint(1)
if ((minver !== -1 && protocolVersion < minver) || (maxver !== -1 && protocolVersion > maxver)) {
throw new Error('Unknown protocol version: ' + protocolVersion + ' Expected: ' + minver + '-' + maxver)
}
return [reader, protocolVersion]
})
}
readDate (reader) {
const daysSinceZero = reader.uint(4)
const temp = new Date(0, 0, 1)
temp.setFullYear(0)
temp.setDate(daysSinceZero + 1)
return temp.toISOString().split('T')[0]
}
decode (num, arr) {
if (num < 0 || num >= arr.length) {
return num
}
return arr[num]
}
}

View file

@ -1,9 +1,9 @@
import quake2 from './quake2.js'
export default class quake1 extends quake2 {
constructor () {
super()
this.responseHeader = 'n'
this.isQuake1 = true
}
}
import quake2 from './quake2.js'
export default class quake1 extends quake2 {
constructor () {
super()
this.responseHeader = 'n'
this.isQuake1 = true
}
}

View file

@ -1,88 +1,88 @@
import Core from './core.js'
export default class quake2 extends Core {
constructor () {
super()
this.encoding = 'latin1'
this.delimiter = '\n'
this.sendHeader = 'status'
this.responseHeader = 'print'
this.isQuake1 = false
}
async run (state) {
const body = await this.udpSend('\xff\xff\xff\xff' + this.sendHeader + '\x00', packet => {
const reader = this.reader(packet)
const header = reader.string({ length: 4, encoding: 'latin1' })
if (header !== '\xff\xff\xff\xff') return
let type
if (this.isQuake1) {
type = reader.string(this.responseHeader.length)
} else {
type = reader.string({ encoding: 'latin1' })
}
if (type !== this.responseHeader) return
return reader.rest()
})
const reader = this.reader(body)
const info = reader.string().split('\\')
if (info[0] === '') info.shift()
while (true) {
const key = info.shift()
const value = info.shift()
if (typeof value === 'undefined') break
state.raw[key] = value
}
while (!reader.done()) {
const line = reader.string()
if (!line || line.charAt(0) === '\0') break
const args = []
const split = line.split('"')
split.forEach((part, i) => {
const inQuote = (i % 2 === 1)
if (inQuote) {
args.push(part)
} else {
const splitSpace = part.split(' ')
for (const subpart of splitSpace) {
if (subpart) args.push(subpart)
}
}
})
const player = {}
if (this.isQuake1) {
player.id = parseInt(args.shift())
player.score = parseInt(args.shift())
player.time = parseInt(args.shift())
player.ping = parseInt(args.shift())
player.name = args.shift()
player.skin = args.shift()
player.color1 = parseInt(args.shift())
player.color2 = parseInt(args.shift())
} else {
player.frags = parseInt(args.shift())
player.ping = parseInt(args.shift())
player.name = args.shift() || ''
if (!player.name) delete player.name
player.address = args.shift() || ''
if (!player.address) delete player.address
}
(player.ping ? state.players : state.bots).push(player)
}
if ('g_needpass' in state.raw) state.password = state.raw.g_needpass
if ('mapname' in state.raw) state.map = state.raw.mapname
if ('sv_maxclients' in state.raw) state.maxplayers = state.raw.sv_maxclients
if ('maxclients' in state.raw) state.maxplayers = state.raw.maxclients
if ('sv_hostname' in state.raw) state.name = state.raw.sv_hostname
if ('hostname' in state.raw) state.name = state.raw.hostname
if ('clients' in state.raw) state.numplayers = state.raw.clients
else state.numplayers = state.players.length + state.bots.length
}
}
import Core from './core.js'
export default class quake2 extends Core {
constructor () {
super()
this.encoding = 'latin1'
this.delimiter = '\n'
this.sendHeader = 'status'
this.responseHeader = 'print'
this.isQuake1 = false
}
async run (state) {
const body = await this.udpSend('\xff\xff\xff\xff' + this.sendHeader + '\x00', packet => {
const reader = this.reader(packet)
const header = reader.string({ length: 4, encoding: 'latin1' })
if (header !== '\xff\xff\xff\xff') return
let type
if (this.isQuake1) {
type = reader.string(this.responseHeader.length)
} else {
type = reader.string({ encoding: 'latin1' })
}
if (type !== this.responseHeader) return
return reader.rest()
})
const reader = this.reader(body)
const info = reader.string().split('\\')
if (info[0] === '') info.shift()
while (true) {
const key = info.shift()
const value = info.shift()
if (typeof value === 'undefined') break
state.raw[key] = value
}
while (!reader.done()) {
const line = reader.string()
if (!line || line.charAt(0) === '\0') break
const args = []
const split = line.split('"')
split.forEach((part, i) => {
const inQuote = (i % 2 === 1)
if (inQuote) {
args.push(part)
} else {
const splitSpace = part.split(' ')
for (const subpart of splitSpace) {
if (subpart) args.push(subpart)
}
}
})
const player = {}
if (this.isQuake1) {
player.id = parseInt(args.shift())
player.score = parseInt(args.shift())
player.time = parseInt(args.shift())
player.ping = parseInt(args.shift())
player.name = args.shift()
player.skin = args.shift()
player.color1 = parseInt(args.shift())
player.color2 = parseInt(args.shift())
} else {
player.frags = parseInt(args.shift())
player.ping = parseInt(args.shift())
player.name = args.shift() || ''
if (!player.name) delete player.name
player.address = args.shift() || ''
if (!player.address) delete player.address
}
(player.ping ? state.players : state.bots).push(player)
}
if ('g_needpass' in state.raw) state.password = state.raw.g_needpass
if ('mapname' in state.raw) state.map = state.raw.mapname
if ('sv_maxclients' in state.raw) state.maxplayers = state.raw.sv_maxclients
if ('maxclients' in state.raw) state.maxplayers = state.raw.maxclients
if ('sv_hostname' in state.raw) state.name = state.raw.sv_hostname
if ('hostname' in state.raw) state.name = state.raw.hostname
if ('clients' in state.raw) state.numplayers = state.raw.clients
else state.numplayers = state.players.length + state.bots.length
}
}

View file

@ -1,24 +1,24 @@
import quake2 from './quake2.js'
export default class quake3 extends quake2 {
constructor () {
super()
this.sendHeader = 'getstatus'
this.responseHeader = 'statusResponse'
import quake2 from './quake2.js'
export default class quake3 extends quake2 {
constructor () {
super()
this.sendHeader = 'getstatus'
this.responseHeader = 'statusResponse'
}
async run (state) {
await super.run(state)
state.name = this.stripColors(state.name)
for (const key of Object.keys(state.raw)) {
state.raw[key] = this.stripColors(state.raw[key])
}
for (const player of state.players) {
player.name = this.stripColors(player.name)
}
async run (state) {
await super.run(state)
state.name = this.stripColors(state.name)
for (const key of Object.keys(state.raw)) {
state.raw[key] = this.stripColors(state.raw[key])
}
for (const player of state.players) {
player.name = this.stripColors(player.name)
}
}
stripColors (str) {
return str.replace(/\^(X.{6}|.)/g, '')
}
}
stripColors (str) {
return str.replace(/\^(X.{6}|.)/g, '')
}
}

View file

@ -1,69 +1,69 @@
import Core from './core.js'
export default class rfactor extends Core {
async run (state) {
const buffer = await this.udpSend('rF_S', b => b)
const reader = this.reader(buffer)
state.raw.gamename = this.readString(reader, 8)
state.raw.fullUpdate = reader.uint(1)
state.raw.region = reader.uint(2)
state.raw.ip = reader.part(4)
state.raw.size = reader.uint(2)
state.raw.version = reader.uint(2)
state.raw.versionRaceCast = reader.uint(2)
state.gamePort = reader.uint(2)
state.raw.queryPort = reader.uint(2)
state.raw.game = this.readString(reader, 20)
state.name = this.readString(reader, 28)
state.map = this.readString(reader, 32)
state.raw.motd = this.readString(reader, 96)
state.raw.packedAids = reader.uint(2)
state.raw.ping = reader.uint(2)
state.raw.packedFlags = reader.uint(1)
state.raw.rate = reader.uint(1)
state.numplayers = reader.uint(1)
state.maxplayers = reader.uint(1)
state.raw.bots = reader.uint(1)
state.raw.packedSpecial = reader.uint(1)
state.raw.damage = reader.uint(1)
state.raw.packedRules = reader.uint(2)
state.raw.credits1 = reader.uint(1)
state.raw.credits2 = reader.uint(2)
this.logger.debug(reader.offset())
state.raw.time = reader.uint(2)
state.raw.laps = reader.uint(2) / 16
reader.skip(3)
state.raw.vehicles = reader.string()
state.password = !!(state.raw.packedSpecial & 2)
state.raw.raceCast = !!(state.raw.packedSpecial & 4)
state.raw.fixedSetups = !!(state.raw.packedSpecial & 16)
const aids = [
'TractionControl',
'AntiLockBraking',
'StabilityControl',
'AutoShifting',
'AutoClutch',
'Invulnerability',
'OppositeLock',
'SteeringHelp',
'BrakingHelp',
'SpinRecovery',
'AutoPitstop'
]
state.raw.aids = []
for (let offset = 0; offset < aids.length; offset++) {
if (state.packedAids && (1 << offset)) {
state.raw.aids.push(aids[offset])
}
}
}
// Consumes bytesToConsume, but only returns string up to the first null
readString (reader, bytesToConsume) {
const consumed = reader.part(bytesToConsume)
return this.reader(consumed).string()
}
}
import Core from './core.js'
export default class rfactor extends Core {
async run (state) {
const buffer = await this.udpSend('rF_S', b => b)
const reader = this.reader(buffer)
state.raw.gamename = this.readString(reader, 8)
state.raw.fullUpdate = reader.uint(1)
state.raw.region = reader.uint(2)
state.raw.ip = reader.part(4)
state.raw.size = reader.uint(2)
state.raw.version = reader.uint(2)
state.raw.versionRaceCast = reader.uint(2)
state.gamePort = reader.uint(2)
state.raw.queryPort = reader.uint(2)
state.raw.game = this.readString(reader, 20)
state.name = this.readString(reader, 28)
state.map = this.readString(reader, 32)
state.raw.motd = this.readString(reader, 96)
state.raw.packedAids = reader.uint(2)
state.raw.ping = reader.uint(2)
state.raw.packedFlags = reader.uint(1)
state.raw.rate = reader.uint(1)
state.numplayers = reader.uint(1)
state.maxplayers = reader.uint(1)
state.raw.bots = reader.uint(1)
state.raw.packedSpecial = reader.uint(1)
state.raw.damage = reader.uint(1)
state.raw.packedRules = reader.uint(2)
state.raw.credits1 = reader.uint(1)
state.raw.credits2 = reader.uint(2)
this.logger.debug(reader.offset())
state.raw.time = reader.uint(2)
state.raw.laps = reader.uint(2) / 16
reader.skip(3)
state.raw.vehicles = reader.string()
state.password = !!(state.raw.packedSpecial & 2)
state.raw.raceCast = !!(state.raw.packedSpecial & 4)
state.raw.fixedSetups = !!(state.raw.packedSpecial & 16)
const aids = [
'TractionControl',
'AntiLockBraking',
'StabilityControl',
'AutoShifting',
'AutoClutch',
'Invulnerability',
'OppositeLock',
'SteeringHelp',
'BrakingHelp',
'SpinRecovery',
'AutoPitstop'
]
state.raw.aids = []
for (let offset = 0; offset < aids.length; offset++) {
if (state.packedAids && (1 << offset)) {
state.raw.aids.push(aids[offset])
}
}
}
// Consumes bytesToConsume, but only returns string up to the first null
readString (reader, bytesToConsume) {
const consumed = reader.part(bytesToConsume)
return this.reader(consumed).string()
}
}

View file

@ -1,102 +1,102 @@
import Core from './core.js'
export default class samp extends Core {
constructor () {
super()
this.encoding = 'win1252'
this.magicHeader = 'SAMP'
this.responseMagicHeader = null
this.isVcmp = false
}
async run (state) {
// read info
{
const reader = await this.sendPacket('i')
if (this.isVcmp) {
const consumed = reader.part(12)
state.raw.version = this.reader(consumed).string()
}
state.password = !!reader.uint(1)
state.numplayers = reader.uint(2)
state.maxplayers = reader.uint(2)
state.name = reader.pascalString(4)
state.raw.gamemode = reader.pascalString(4)
state.raw.map = reader.pascalString(4)
}
// read rules
if (!this.isVcmp) {
const reader = await this.sendPacket('r')
const ruleCount = reader.uint(2)
state.raw.rules = {}
for (let i = 0; i < ruleCount; i++) {
const key = reader.pascalString(1)
const value = reader.pascalString(1)
state.raw.rules[key] = value
}
}
// read players
// don't even bother if > 100 players, because the server won't respond
if (state.numplayers < 100) {
if (this.isVcmp) {
const reader = await this.sendPacket('c', true)
if (reader !== null) {
const playerCount = reader.uint(2)
for (let i = 0; i < playerCount; i++) {
const player = {}
player.name = reader.pascalString(1)
state.players.push(player)
}
}
} else {
const reader = await this.sendPacket('d', true)
if (reader !== null) {
const playerCount = reader.uint(2)
for (let i = 0; i < playerCount; i++) {
const player = {}
player.id = reader.uint(1)
player.name = reader.pascalString(1)
player.score = reader.int(4)
player.ping = reader.uint(4)
state.players.push(player)
}
}
}
}
}
async sendPacket (type, allowTimeout) {
const outBuffer = Buffer.alloc(11)
outBuffer.write(this.magicHeader, 0, 4)
const ipSplit = this.options.address.split('.')
outBuffer.writeUInt8(parseInt(ipSplit[0]), 4)
outBuffer.writeUInt8(parseInt(ipSplit[1]), 5)
outBuffer.writeUInt8(parseInt(ipSplit[2]), 6)
outBuffer.writeUInt8(parseInt(ipSplit[3]), 7)
outBuffer.writeUInt16LE(this.options.port, 8)
outBuffer.writeUInt8(type.charCodeAt(0), 10)
const checkBuffer = Buffer.from(outBuffer)
if (this.responseMagicHeader) {
checkBuffer.write(this.responseMagicHeader, 0, 4)
}
return await this.udpSend(
outBuffer,
(buffer) => {
const reader = this.reader(buffer)
for (let i = 0; i < checkBuffer.length; i++) {
if (checkBuffer.readUInt8(i) !== reader.uint(1)) return
}
return reader
},
() => {
if (allowTimeout) {
return null
}
}
)
}
}
import Core from './core.js'
export default class samp extends Core {
constructor () {
super()
this.encoding = 'win1252'
this.magicHeader = 'SAMP'
this.responseMagicHeader = null
this.isVcmp = false
}
async run (state) {
// read info
{
const reader = await this.sendPacket('i')
if (this.isVcmp) {
const consumed = reader.part(12)
state.raw.version = this.reader(consumed).string()
}
state.password = !!reader.uint(1)
state.numplayers = reader.uint(2)
state.maxplayers = reader.uint(2)
state.name = reader.pascalString(4)
state.raw.gamemode = reader.pascalString(4)
state.raw.map = reader.pascalString(4)
}
// read rules
if (!this.isVcmp) {
const reader = await this.sendPacket('r')
const ruleCount = reader.uint(2)
state.raw.rules = {}
for (let i = 0; i < ruleCount; i++) {
const key = reader.pascalString(1)
const value = reader.pascalString(1)
state.raw.rules[key] = value
}
}
// read players
// don't even bother if > 100 players, because the server won't respond
if (state.numplayers < 100) {
if (this.isVcmp) {
const reader = await this.sendPacket('c', true)
if (reader !== null) {
const playerCount = reader.uint(2)
for (let i = 0; i < playerCount; i++) {
const player = {}
player.name = reader.pascalString(1)
state.players.push(player)
}
}
} else {
const reader = await this.sendPacket('d', true)
if (reader !== null) {
const playerCount = reader.uint(2)
for (let i = 0; i < playerCount; i++) {
const player = {}
player.id = reader.uint(1)
player.name = reader.pascalString(1)
player.score = reader.int(4)
player.ping = reader.uint(4)
state.players.push(player)
}
}
}
}
}
async sendPacket (type, allowTimeout) {
const outBuffer = Buffer.alloc(11)
outBuffer.write(this.magicHeader, 0, 4)
const ipSplit = this.options.address.split('.')
outBuffer.writeUInt8(parseInt(ipSplit[0]), 4)
outBuffer.writeUInt8(parseInt(ipSplit[1]), 5)
outBuffer.writeUInt8(parseInt(ipSplit[2]), 6)
outBuffer.writeUInt8(parseInt(ipSplit[3]), 7)
outBuffer.writeUInt16LE(this.options.port, 8)
outBuffer.writeUInt8(type.charCodeAt(0), 10)
const checkBuffer = Buffer.from(outBuffer)
if (this.responseMagicHeader) {
checkBuffer.write(this.responseMagicHeader, 0, 4)
}
return await this.udpSend(
outBuffer,
(buffer) => {
const reader = this.reader(buffer)
for (let i = 0; i < checkBuffer.length; i++) {
if (checkBuffer.readUInt8(i) !== reader.uint(1)) return
}
return reader
},
() => {
if (allowTimeout) {
return null
}
}
)
}
}

View file

@ -1,25 +1,25 @@
import Core from './core.js'
export default class savage2 extends Core {
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.numplayers = 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, '')
}
}
import Core from './core.js'
export default class savage2 extends Core {
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.numplayers = 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, '')
}
}

View file

@ -1,67 +1,67 @@
import Core from './core.js'
export default class starmade extends Core {
constructor () {
super()
this.encoding = 'latin1'
this.byteorder = 'be'
}
async run (state) {
const b = Buffer.from([0x00, 0x00, 0x00, 0x09, 0x2a, 0xff, 0xff, 0x01, 0x6f, 0x00, 0x00, 0x00, 0x00])
const payload = await this.withTcp(async socket => {
return await this.tcpSend(socket, b, buffer => {
if (buffer.length < 12) return
const reader = this.reader(buffer)
const packetLength = reader.uint(4)
this.logger.debug('Received packet length: ' + packetLength)
const timestamp = reader.uint(8).toString()
this.logger.debug('Received timestamp: ' + timestamp)
if (reader.remaining() < packetLength || reader.remaining() < 5) return
const checkId = reader.uint(1)
const packetId = reader.uint(2)
const commandId = reader.uint(1)
const type = reader.uint(1)
this.logger.debug('checkId=' + checkId + ' packetId=' + packetId + ' commandId=' + commandId + ' type=' + type)
if (checkId !== 0x2a) return
return reader.rest()
})
})
const reader = this.reader(payload)
const data = []
state.raw.data = data
while (!reader.done()) {
const mark = reader.uint(1)
if (mark === 1) {
// signed int
data.push(reader.int(4))
} else if (mark === 3) {
// float
data.push(reader.float())
} else if (mark === 4) {
// string
data.push(reader.pascalString(2))
} else if (mark === 6) {
// byte
data.push(reader.uint(1))
}
}
this.logger.debug('Received raw data array', data)
if (typeof data[0] === 'number') state.raw.infoVersion = data[0]
if (typeof data[1] === 'number') state.raw.version = data[1]
if (typeof data[2] === 'string') state.name = data[2]
if (typeof data[3] === 'string') state.raw.description = data[3]
if (typeof data[4] === 'number') state.raw.startTime = data[4]
if (typeof data[5] === 'number') state.numplayers = data[5]
if (typeof data[6] === 'number') state.maxplayers = data[6]
}
}
import Core from './core.js'
export default class starmade extends Core {
constructor () {
super()
this.encoding = 'latin1'
this.byteorder = 'be'
}
async run (state) {
const b = Buffer.from([0x00, 0x00, 0x00, 0x09, 0x2a, 0xff, 0xff, 0x01, 0x6f, 0x00, 0x00, 0x00, 0x00])
const payload = await this.withTcp(async socket => {
return await this.tcpSend(socket, b, buffer => {
if (buffer.length < 12) return
const reader = this.reader(buffer)
const packetLength = reader.uint(4)
this.logger.debug('Received packet length: ' + packetLength)
const timestamp = reader.uint(8).toString()
this.logger.debug('Received timestamp: ' + timestamp)
if (reader.remaining() < packetLength || reader.remaining() < 5) return
const checkId = reader.uint(1)
const packetId = reader.uint(2)
const commandId = reader.uint(1)
const type = reader.uint(1)
this.logger.debug('checkId=' + checkId + ' packetId=' + packetId + ' commandId=' + commandId + ' type=' + type)
if (checkId !== 0x2a) return
return reader.rest()
})
})
const reader = this.reader(payload)
const data = []
state.raw.data = data
while (!reader.done()) {
const mark = reader.uint(1)
if (mark === 1) {
// signed int
data.push(reader.int(4))
} else if (mark === 3) {
// float
data.push(reader.float())
} else if (mark === 4) {
// string
data.push(reader.pascalString(2))
} else if (mark === 6) {
// byte
data.push(reader.uint(1))
}
}
this.logger.debug('Received raw data array', data)
if (typeof data[0] === 'number') state.raw.infoVersion = data[0]
if (typeof data[1] === 'number') state.raw.version = data[1]
if (typeof data[2] === 'string') state.name = data[2]
if (typeof data[3] === 'string') state.raw.description = data[3]
if (typeof data[4] === 'number') state.raw.startTime = data[4]
if (typeof data[5] === 'number') state.numplayers = data[5]
if (typeof data[6] === 'number') state.maxplayers = data[6]
}
}

View file

@ -1,10 +1,10 @@
import tribes1 from './tribes1.js'
export default class starsiege extends tribes1 {
constructor () {
super()
this.encoding = 'latin1'
this.requestByte = 0x72
this.responseByte = 0x73
}
}
import tribes1 from './tribes1.js'
export default class starsiege extends tribes1 {
constructor () {
super()
this.encoding = 'latin1'
this.requestByte = 0x72
this.responseByte = 0x73
}
}

View file

@ -1,71 +1,71 @@
import Core from './core.js'
export default class teamspeak2 extends Core {
async run (state) {
const queryPort = this.options.teamspeakQueryPort || 51234
await this.withTcp(async socket => {
{
const data = await this.sendCommand(socket, 'sel ' + this.options.port)
if (data !== '[TS]') throw new Error('Invalid header')
}
{
const data = await this.sendCommand(socket, 'si')
for (const line of data.split('\r\n')) {
const equals = line.indexOf('=')
const key = equals === -1 ? line : line.substring(0, equals)
const value = equals === -1 ? '' : line.substring(equals + 1)
state.raw[key] = value
}
}
{
const data = await this.sendCommand(socket, 'pl')
const split = data.split('\r\n')
const fields = split.shift().split('\t')
for (const line of split) {
const split2 = line.split('\t')
const player = {}
split2.forEach((value, i) => {
let key = fields[i]
if (!key) return
if (key === 'nick') key = 'name'
const m = value.match(/^"(.*)"$/)
if (m) value = m[1]
player[key] = value
})
state.players.push(player)
}
state.numplayers = state.players.length
}
{
const data = await this.sendCommand(socket, 'cl')
const split = data.split('\r\n')
const fields = split.shift().split('\t')
state.raw.channels = []
for (const line of split) {
const split2 = line.split('\t')
const channel = {}
split2.forEach((value, i) => {
const key = fields[i]
if (!key) return
const m = value.match(/^"(.*)"$/)
if (m) value = m[1]
channel[key] = value
})
state.raw.channels.push(channel)
}
}
}, queryPort)
}
async sendCommand (socket, cmd) {
return await this.tcpSend(socket, cmd + '\x0A', buffer => {
if (buffer.length < 6) return
if (buffer.slice(-6).toString() !== '\r\nOK\r\n') return
return buffer.slice(0, -6).toString()
})
}
}
import Core from './core.js'
export default class teamspeak2 extends Core {
async run (state) {
const queryPort = this.options.teamspeakQueryPort || 51234
await this.withTcp(async socket => {
{
const data = await this.sendCommand(socket, 'sel ' + this.options.port)
if (data !== '[TS]') throw new Error('Invalid header')
}
{
const data = await this.sendCommand(socket, 'si')
for (const line of data.split('\r\n')) {
const equals = line.indexOf('=')
const key = equals === -1 ? line : line.substring(0, equals)
const value = equals === -1 ? '' : line.substring(equals + 1)
state.raw[key] = value
}
}
{
const data = await this.sendCommand(socket, 'pl')
const split = data.split('\r\n')
const fields = split.shift().split('\t')
for (const line of split) {
const split2 = line.split('\t')
const player = {}
split2.forEach((value, i) => {
let key = fields[i]
if (!key) return
if (key === 'nick') key = 'name'
const m = value.match(/^"(.*)"$/)
if (m) value = m[1]
player[key] = value
})
state.players.push(player)
}
state.numplayers = state.players.length
}
{
const data = await this.sendCommand(socket, 'cl')
const split = data.split('\r\n')
const fields = split.shift().split('\t')
state.raw.channels = []
for (const line of split) {
const split2 = line.split('\t')
const channel = {}
split2.forEach((value, i) => {
const key = fields[i]
if (!key) return
const m = value.match(/^"(.*)"$/)
if (m) value = m[1]
channel[key] = value
})
state.raw.channels.push(channel)
}
}
}, queryPort)
}
async sendCommand (socket, cmd) {
return await this.tcpSend(socket, cmd + '\x0A', buffer => {
if (buffer.length < 6) return
if (buffer.slice(-6).toString() !== '\r\nOK\r\n') return
return buffer.slice(0, -6).toString()
})
}
}

View file

@ -1,69 +1,69 @@
import Core from './core.js'
export default class teamspeak3 extends Core {
async run (state) {
const queryPort = this.options.teamspeakQueryPort || 10011
await this.withTcp(async socket => {
{
const data = await this.sendCommand(socket, 'use port=' + this.options.port, true)
const split = data.split('\n\r')
if (split[0] !== 'TS3') throw new Error('Invalid header')
}
{
const data = await this.sendCommand(socket, 'serverinfo')
state.raw = data[0]
if ('virtualserver_name' in state.raw) state.name = state.raw.virtualserver_name
if ('virtualserver_maxclients' in state.raw) state.maxplayers = state.raw.virtualserver_maxclients
if ('virtualserver_clientsonline' in state.raw) state.numplayers = state.raw.virtualserver_clientsonline
}
{
const list = await this.sendCommand(socket, 'clientlist')
for (const client of list) {
client.name = client.client_nickname
delete client.client_nickname
if (client.client_type === '0') {
state.players.push(client)
}
}
}
{
const data = await this.sendCommand(socket, 'channellist -topic')
state.raw.channels = data
}
}, queryPort)
}
async sendCommand (socket, cmd, raw) {
const body = await this.tcpSend(socket, cmd + '\x0A', (buffer) => {
if (buffer.length < 21) return
if (buffer.slice(-21).toString() !== '\n\rerror id=0 msg=ok\n\r') return
return buffer.slice(0, -21).toString()
})
if (raw) {
return body
} else {
const segments = body.split('|')
const out = []
for (const line of segments) {
const split = line.split(' ')
const unit = {}
for (const field of split) {
const equals = field.indexOf('=')
const key = equals === -1 ? field : field.substring(0, equals)
const value = equals === -1
? ''
: field.substring(equals + 1)
.replace(/\\s/g, ' ').replace(/\\\//g, '/')
unit[key] = value
}
out.push(unit)
}
return out
}
}
}
import Core from './core.js'
export default class teamspeak3 extends Core {
async run (state) {
const queryPort = this.options.teamspeakQueryPort || 10011
await this.withTcp(async socket => {
{
const data = await this.sendCommand(socket, 'use port=' + this.options.port, true)
const split = data.split('\n\r')
if (split[0] !== 'TS3') throw new Error('Invalid header')
}
{
const data = await this.sendCommand(socket, 'serverinfo')
state.raw = data[0]
if ('virtualserver_name' in state.raw) state.name = state.raw.virtualserver_name
if ('virtualserver_maxclients' in state.raw) state.maxplayers = state.raw.virtualserver_maxclients
if ('virtualserver_clientsonline' in state.raw) state.numplayers = state.raw.virtualserver_clientsonline
}
{
const list = await this.sendCommand(socket, 'clientlist')
for (const client of list) {
client.name = client.client_nickname
delete client.client_nickname
if (client.client_type === '0') {
state.players.push(client)
}
}
}
{
const data = await this.sendCommand(socket, 'channellist -topic')
state.raw.channels = data
}
}, queryPort)
}
async sendCommand (socket, cmd, raw) {
const body = await this.tcpSend(socket, cmd + '\x0A', (buffer) => {
if (buffer.length < 21) return
if (buffer.slice(-21).toString() !== '\n\rerror id=0 msg=ok\n\r') return
return buffer.slice(0, -21).toString()
})
if (raw) {
return body
} else {
const segments = body.split('|')
const out = []
for (const line of segments) {
const split = line.split(' ')
const unit = {}
for (const field of split) {
const equals = field.indexOf('=')
const key = equals === -1 ? field : field.substring(0, equals)
const value = equals === -1
? ''
: field.substring(equals + 1)
.replace(/\\s/g, ' ').replace(/\\\//g, '/')
unit[key] = value
}
out.push(unit)
}
return out
}
}
}

View file

@ -1,24 +1,24 @@
import Core from './core.js'
export default class terraria extends Core {
async run (state) {
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'
})
if (json.status !== '200') throw new Error('Invalid status')
for (const one of json.players) {
state.players.push({ name: one.nickname, team: one.team })
}
state.name = json.name
state.gamePort = json.port
state.numplayers = json.playercount
}
}
import Core from './core.js'
export default class terraria extends Core {
async run (state) {
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'
})
if (json.status !== '200') throw new Error('Invalid status')
for (const one of json.players) {
state.players.push({ name: one.nickname, team: one.team })
}
state.name = json.name
state.gamePort = json.port
state.numplayers = json.playercount
}
}

View file

@ -1,153 +1,153 @@
import Core from './core.js'
export default class tribes1 extends Core {
constructor () {
super()
this.encoding = 'latin1'
this.requestByte = 0x62
this.responseByte = 0x63
this.challenge = 0x01
}
async run (state) {
const query = Buffer.alloc(3)
query.writeUInt8(this.requestByte, 0)
query.writeUInt16LE(this.challenge, 1)
const reader = await this.udpSend(query, (buffer) => {
const reader = this.reader(buffer)
const responseByte = reader.uint(1)
if (responseByte !== this.responseByte) {
this.logger.debug('Unexpected response byte')
return
}
const challenge = reader.uint(2)
if (challenge !== this.challenge) {
this.logger.debug('Unexpected challenge')
return
}
const requestByte = reader.uint(1)
if (requestByte !== this.requestByte) {
this.logger.debug('Unexpected request byte')
return
}
return reader
})
state.raw.gametype = this.readString(reader)
const isStarsiege2009 = state.raw.gametype === 'Starsiege'
state.raw.version = this.readString(reader)
state.name = this.readString(reader)
if (isStarsiege2009) {
state.password = !!reader.uint(1)
state.raw.dedicated = !!reader.uint(1)
state.raw.dropInProgress = !!reader.uint(1)
state.raw.gameInProgress = !!reader.uint(1)
state.numplayers = reader.uint(4)
state.maxplayers = reader.uint(4)
state.raw.teamPlay = reader.uint(1)
state.map = this.readString(reader)
state.raw.cpuSpeed = reader.uint(2)
state.raw.factoryVeh = reader.uint(1)
state.raw.allowTecmix = reader.uint(1)
state.raw.spawnLimit = reader.uint(4)
state.raw.fragLimit = reader.uint(4)
state.raw.timeLimit = reader.uint(4)
state.raw.techLimit = reader.uint(4)
state.raw.combatLimit = reader.uint(4)
state.raw.massLimit = reader.uint(4)
state.raw.playersSent = reader.uint(4)
const teams = { 1: 'yellow', 2: 'blue', 4: 'red', 8: 'purple' }
while (!reader.done()) {
const player = {}
player.name = this.readString(reader)
const teamId = reader.uint(1)
const team = teams[teamId]
if (team) player.team = teams[teamId]
}
return
}
state.raw.dedicated = !!reader.uint(1)
state.password = !!reader.uint(1)
state.raw.playerCount = reader.uint(1)
state.maxplayers = reader.uint(1)
state.raw.cpuSpeed = reader.uint(2)
state.raw.mod = this.readString(reader)
state.raw.type = this.readString(reader)
state.map = this.readString(reader)
state.raw.motd = this.readString(reader)
state.raw.teamCount = reader.uint(1)
const teamFields = this.readFieldList(reader)
const playerFields = this.readFieldList(reader)
state.raw.teams = []
for (let i = 0; i < state.raw.teamCount; i++) {
const teamName = this.readString(reader)
const teamValues = this.readValues(reader)
const teamInfo = {}
for (let i = 0; i < teamValues.length && i < teamFields.length; i++) {
let key = teamFields[i]
let value = teamValues[i]
if (key === 'ultra_base') key = 'name'
if (value === '%t') value = teamName
if (['score', 'players'].includes(key)) value = parseInt(value)
teamInfo[key] = value
}
state.raw.teams.push(teamInfo)
}
for (let i = 0; i < state.raw.playerCount; i++) {
const ping = reader.uint(1) * 4
const packetLoss = reader.uint(1)
const teamNum = reader.uint(1)
const name = this.readString(reader)
const playerValues = this.readValues(reader)
const playerInfo = {}
for (let i = 0; i < playerValues.length && i < playerFields.length; i++) {
const key = playerFields[i]
let value = playerValues[i]
if (value === '%p') value = ping
if (value === '%l') value = packetLoss
if (value === '%t') value = teamNum
if (value === '%n') value = name
if (['score', 'ping', 'pl', 'kills', 'lvl'].includes(key)) value = parseInt(value)
if (key === 'team') {
const teamId = parseInt(value)
if (teamId >= 0 && teamId < state.raw.teams.length && state.raw.teams[teamId].name) {
value = state.raw.teams[teamId].name
} else {
continue
}
}
playerInfo[key] = value
}
state.players.push(playerInfo)
}
}
readFieldList (reader) {
const str = this.readString(reader)
if (!str) return []
return ('?' + str)
.split('\t')
.map((a) => a.substring(1).trim().toLowerCase())
.map((a) => a === 'team name' ? 'name' : a)
.map((a) => a === 'player name' ? 'name' : a)
}
readValues (reader) {
const str = this.readString(reader)
if (!str) return []
return str
.split('\t')
.map((a) => a.trim())
}
readString (reader) {
return reader.pascalString(1)
}
}
import Core from './core.js'
export default class tribes1 extends Core {
constructor () {
super()
this.encoding = 'latin1'
this.requestByte = 0x62
this.responseByte = 0x63
this.challenge = 0x01
}
async run (state) {
const query = Buffer.alloc(3)
query.writeUInt8(this.requestByte, 0)
query.writeUInt16LE(this.challenge, 1)
const reader = await this.udpSend(query, (buffer) => {
const reader = this.reader(buffer)
const responseByte = reader.uint(1)
if (responseByte !== this.responseByte) {
this.logger.debug('Unexpected response byte')
return
}
const challenge = reader.uint(2)
if (challenge !== this.challenge) {
this.logger.debug('Unexpected challenge')
return
}
const requestByte = reader.uint(1)
if (requestByte !== this.requestByte) {
this.logger.debug('Unexpected request byte')
return
}
return reader
})
state.raw.gametype = this.readString(reader)
const isStarsiege2009 = state.raw.gametype === 'Starsiege'
state.raw.version = this.readString(reader)
state.name = this.readString(reader)
if (isStarsiege2009) {
state.password = !!reader.uint(1)
state.raw.dedicated = !!reader.uint(1)
state.raw.dropInProgress = !!reader.uint(1)
state.raw.gameInProgress = !!reader.uint(1)
state.numplayers = reader.uint(4)
state.maxplayers = reader.uint(4)
state.raw.teamPlay = reader.uint(1)
state.map = this.readString(reader)
state.raw.cpuSpeed = reader.uint(2)
state.raw.factoryVeh = reader.uint(1)
state.raw.allowTecmix = reader.uint(1)
state.raw.spawnLimit = reader.uint(4)
state.raw.fragLimit = reader.uint(4)
state.raw.timeLimit = reader.uint(4)
state.raw.techLimit = reader.uint(4)
state.raw.combatLimit = reader.uint(4)
state.raw.massLimit = reader.uint(4)
state.raw.playersSent = reader.uint(4)
const teams = { 1: 'yellow', 2: 'blue', 4: 'red', 8: 'purple' }
while (!reader.done()) {
const player = {}
player.name = this.readString(reader)
const teamId = reader.uint(1)
const team = teams[teamId]
if (team) player.team = teams[teamId]
}
return
}
state.raw.dedicated = !!reader.uint(1)
state.password = !!reader.uint(1)
state.raw.playerCount = reader.uint(1)
state.maxplayers = reader.uint(1)
state.raw.cpuSpeed = reader.uint(2)
state.raw.mod = this.readString(reader)
state.raw.type = this.readString(reader)
state.map = this.readString(reader)
state.raw.motd = this.readString(reader)
state.raw.teamCount = reader.uint(1)
const teamFields = this.readFieldList(reader)
const playerFields = this.readFieldList(reader)
state.raw.teams = []
for (let i = 0; i < state.raw.teamCount; i++) {
const teamName = this.readString(reader)
const teamValues = this.readValues(reader)
const teamInfo = {}
for (let i = 0; i < teamValues.length && i < teamFields.length; i++) {
let key = teamFields[i]
let value = teamValues[i]
if (key === 'ultra_base') key = 'name'
if (value === '%t') value = teamName
if (['score', 'players'].includes(key)) value = parseInt(value)
teamInfo[key] = value
}
state.raw.teams.push(teamInfo)
}
for (let i = 0; i < state.raw.playerCount; i++) {
const ping = reader.uint(1) * 4
const packetLoss = reader.uint(1)
const teamNum = reader.uint(1)
const name = this.readString(reader)
const playerValues = this.readValues(reader)
const playerInfo = {}
for (let i = 0; i < playerValues.length && i < playerFields.length; i++) {
const key = playerFields[i]
let value = playerValues[i]
if (value === '%p') value = ping
if (value === '%l') value = packetLoss
if (value === '%t') value = teamNum
if (value === '%n') value = name
if (['score', 'ping', 'pl', 'kills', 'lvl'].includes(key)) value = parseInt(value)
if (key === 'team') {
const teamId = parseInt(value)
if (teamId >= 0 && teamId < state.raw.teams.length && state.raw.teams[teamId].name) {
value = state.raw.teams[teamId].name
} else {
continue
}
}
playerInfo[key] = value
}
state.players.push(playerInfo)
}
}
readFieldList (reader) {
const str = this.readString(reader)
if (!str) return []
return ('?' + str)
.split('\t')
.map((a) => a.substring(1).trim().toLowerCase())
.map((a) => a === 'team name' ? 'name' : a)
.map((a) => a === 'player name' ? 'name' : a)
}
readValues (reader) {
const str = this.readString(reader)
if (!str) return []
return str
.split('\t')
.map((a) => a.trim())
}
readString (reader) {
return reader.pascalString(1)
}
}

View file

@ -1,80 +1,80 @@
import Core from './core.js'
/** Unsupported -- use at your own risk!! */
export default class tribes1master extends Core {
constructor () {
super()
this.encoding = 'latin1'
}
async run (state) {
const queryBuffer = Buffer.from([
0x10, // standard header
0x03, // dump servers
0xff, // ask for all packets
0x00, // junk
0x01, 0x02 // challenge
])
const parts = new Map()
let total = 0
const full = await this.udpSend(queryBuffer, (buffer) => {
const reader = this.reader(buffer)
const header = reader.uint(2)
if (header !== 0x0610) {
this.logger.debug('Header response does not match: ' + header.toString(16))
return
}
const num = reader.uint(1)
const t = reader.uint(1)
if (t <= 0 || (total > 0 && t !== total)) {
throw new Error('Conflicting packet total: ' + t)
}
total = t
if (num < 1 || num > total) {
this.logger.debug('Invalid packet number: ' + num + ' ' + total)
return
}
if (parts.has(num)) {
this.logger.debug('Duplicate part: ' + num)
return
}
reader.skip(2) // challenge (0x0201)
reader.skip(2) // always 0x6600
parts.set(num, reader.rest())
if (parts.size === total) {
const ordered = []
for (let i = 1; i <= total; i++) ordered.push(parts.get(i))
return Buffer.concat(ordered)
}
})
const fullReader = this.reader(full)
state.raw.name = this.readString(fullReader)
state.raw.motd = this.readString(fullReader)
state.raw.servers = []
while (!fullReader.done()) {
fullReader.skip(1) // junk ?
const count = fullReader.uint(1)
for (let i = 0; i < count; i++) {
const six = fullReader.uint(1)
if (six !== 6) {
throw new Error('Expecting 6')
}
const ip = fullReader.uint(4)
const port = fullReader.uint(2)
const ipStr = (ip & 255) + '.' + (ip >> 8 & 255) + '.' + (ip >> 16 & 255) + '.' + (ip >>> 24)
state.raw.servers.push(ipStr + ':' + port)
}
}
import Core from './core.js'
/** Unsupported -- use at your own risk!! */
export default class tribes1master extends Core {
constructor () {
super()
this.encoding = 'latin1'
}
readString (reader) {
return reader.pascalString(1)
}
}
async run (state) {
const queryBuffer = Buffer.from([
0x10, // standard header
0x03, // dump servers
0xff, // ask for all packets
0x00, // junk
0x01, 0x02 // challenge
])
const parts = new Map()
let total = 0
const full = await this.udpSend(queryBuffer, (buffer) => {
const reader = this.reader(buffer)
const header = reader.uint(2)
if (header !== 0x0610) {
this.logger.debug('Header response does not match: ' + header.toString(16))
return
}
const num = reader.uint(1)
const t = reader.uint(1)
if (t <= 0 || (total > 0 && t !== total)) {
throw new Error('Conflicting packet total: ' + t)
}
total = t
if (num < 1 || num > total) {
this.logger.debug('Invalid packet number: ' + num + ' ' + total)
return
}
if (parts.has(num)) {
this.logger.debug('Duplicate part: ' + num)
return
}
reader.skip(2) // challenge (0x0201)
reader.skip(2) // always 0x6600
parts.set(num, reader.rest())
if (parts.size === total) {
const ordered = []
for (let i = 1; i <= total; i++) ordered.push(parts.get(i))
return Buffer.concat(ordered)
}
})
const fullReader = this.reader(full)
state.raw.name = this.readString(fullReader)
state.raw.motd = this.readString(fullReader)
state.raw.servers = []
while (!fullReader.done()) {
fullReader.skip(1) // junk ?
const count = fullReader.uint(1)
for (let i = 0; i < count; i++) {
const six = fullReader.uint(1)
if (six !== 6) {
throw new Error('Expecting 6')
}
const ip = fullReader.uint(4)
const port = fullReader.uint(2)
const ipStr = (ip & 255) + '.' + (ip >> 8 & 255) + '.' + (ip >> 16 & 255) + '.' + (ip >>> 24)
state.raw.servers.push(ipStr + ':' + port)
}
}
}
readString (reader) {
return reader.pascalString(1)
}
}

View file

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

View file

@ -1,45 +1,45 @@
import gamespy3 from './gamespy3.js'
export default class ut3 extends gamespy3 {
async run (state) {
await super.run(state)
this.translate(state.raw, {
mapname: false,
p1073741825: 'map',
p1073741826: 'gametype',
p1073741827: 'servername',
p1073741828: 'custom_mutators',
gamemode: 'joininprogress',
s32779: 'gamemode',
s0: 'bot_skill',
s6: 'pure_server',
s7: 'password',
s8: 'vs_bots',
s10: 'force_respawn',
p268435704: 'frag_limit',
p268435705: 'time_limit',
p268435703: 'numbots',
p268435717: 'stock_mutators',
p1073741829: 'stock_mutators',
s1: false,
s9: false,
s11: false,
s12: false,
s13: false,
s14: false,
p268435706: false,
p268435968: false,
p268435969: false
})
const split = (a) => {
let s = a.split('\x1c')
s = s.filter((e) => { return e })
return s
}
if ('custom_mutators' in state.raw) state.raw.custom_mutators = split(state.raw.custom_mutators)
if ('stock_mutators' in state.raw) state.raw.stock_mutators = split(state.raw.stock_mutators)
if ('map' in state.raw) state.map = state.raw.map
}
}
import gamespy3 from './gamespy3.js'
export default class ut3 extends gamespy3 {
async run (state) {
await super.run(state)
this.translate(state.raw, {
mapname: false,
p1073741825: 'map',
p1073741826: 'gametype',
p1073741827: 'servername',
p1073741828: 'custom_mutators',
gamemode: 'joininprogress',
s32779: 'gamemode',
s0: 'bot_skill',
s6: 'pure_server',
s7: 'password',
s8: 'vs_bots',
s10: 'force_respawn',
p268435704: 'frag_limit',
p268435705: 'time_limit',
p268435703: 'numbots',
p268435717: 'stock_mutators',
p1073741829: 'stock_mutators',
s1: false,
s9: false,
s11: false,
s12: false,
s13: false,
s14: false,
p268435706: false,
p268435968: false,
p268435969: false
})
const split = (a) => {
let s = a.split('\x1c')
s = s.filter((e) => { return e })
return s
}
if ('custom_mutators' in state.raw) state.raw.custom_mutators = split(state.raw.custom_mutators)
if ('stock_mutators' in state.raw) state.raw.stock_mutators = split(state.raw.stock_mutators)
if ('map' in state.raw) state.map = state.raw.map
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,10 @@
import samp from './samp.js'
export default class vcmp extends samp {
constructor () {
super()
this.magicHeader = 'VCMP'
this.responseMagicHeader = 'MP04'
this.isVcmp = true
}
}
import samp from './samp.js'
export default class vcmp extends samp {
constructor () {
super()
this.magicHeader = 'VCMP'
this.responseMagicHeader = 'MP04'
this.isVcmp = true
}
}

View file

@ -1,237 +1,237 @@
import Core from './core.js'
export default class ventrilo extends Core {
constructor () {
super()
this.byteorder = 'be'
}
async run (state) {
const data = await this.sendCommand(2, '')
state.raw = splitFields(data.toString())
for (const client of state.raw.CLIENTS) {
client.name = client.NAME
delete client.NAME
client.ping = parseInt(client.PING)
delete client.PING
state.players.push(client)
}
delete state.raw.CLIENTS
state.numplayers = state.players.length
if ('NAME' in state.raw) state.name = state.raw.NAME
if ('MAXCLIENTS' in state.raw) state.maxplayers = state.raw.MAXCLIENTS
if (this.trueTest(state.raw.AUTH)) state.password = true
}
async sendCommand (cmd, password) {
const body = Buffer.alloc(16)
body.write(password, 0, 15, 'utf8')
const encrypted = encrypt(cmd, body)
const packets = {}
return await this.udpSend(encrypted, (buffer) => {
if (buffer.length < 20) return
const data = decrypt(buffer)
if (data.zero !== 0) return
packets[data.packetNum] = data.body
if (Object.keys(packets).length !== data.packetTotal) return
const out = []
for (let i = 0; i < data.packetTotal; i++) {
if (!(i in packets)) throw new Error('Missing packet #' + i)
out.push(packets[i])
}
return Buffer.concat(out)
})
}
}
function splitFields (str, subMode) {
let splitter, delim
if (subMode) {
splitter = '='
delim = ','
} else {
splitter = ': '
delim = '\n'
}
const split = str.split(delim)
const out = {}
if (!subMode) {
out.CHANNELS = []
out.CLIENTS = []
}
for (const one of split) {
const equal = one.indexOf(splitter)
const key = equal === -1 ? one : one.substring(0, equal)
if (!key || key === '\0') continue
const value = equal === -1 ? '' : one.substring(equal + splitter.length)
if (!subMode && key === 'CHANNEL') out.CHANNELS.push(splitFields(value, true))
else if (!subMode && key === 'CLIENT') out.CLIENTS.push(splitFields(value, true))
else out[key] = value
}
return out
}
function randInt (min, max) {
return Math.floor(Math.random() * (max - min + 1) + min)
}
function crc (body) {
let crc = 0
for (let i = 0; i < body.length; i++) {
crc = crcTable[crc >> 8] ^ body.readUInt8(i) ^ (crc << 8)
crc &= 0xffff
}
return crc
}
function encrypt (cmd, body) {
const headerKeyStart = randInt(0, 0xff)
const headerKeyAdd = randInt(1, 0xff)
const bodyKeyStart = randInt(0, 0xff)
const bodyKeyAdd = randInt(1, 0xff)
const header = Buffer.alloc(20)
header.writeUInt8(headerKeyStart, 0)
header.writeUInt8(headerKeyAdd, 1)
header.writeUInt16BE(cmd, 4)
header.writeUInt16BE(body.length, 8)
header.writeUInt16BE(body.length, 10)
header.writeUInt16BE(1, 12)
header.writeUInt16BE(0, 14)
header.writeUInt8(bodyKeyStart, 16)
header.writeUInt8(bodyKeyAdd, 17)
header.writeUInt16BE(crc(body), 18)
let offset = headerKeyStart
for (let i = 2; i < header.length; i++) {
let val = header.readUInt8(i)
val += codeHead.charCodeAt(offset) + ((i - 2) % 5)
val = val & 0xff
header.writeUInt8(val, i)
offset = (offset + headerKeyAdd) & 0xff
}
offset = bodyKeyStart
for (let i = 0; i < body.length; i++) {
let val = body.readUInt8(i)
val += codeBody.charCodeAt(offset) + (i % 72)
val = val & 0xff
body.writeUInt8(val, i)
offset = (offset + bodyKeyAdd) & 0xff
}
return Buffer.concat([header, body])
}
function decrypt (data) {
const header = data.slice(0, 20)
const body = data.slice(20)
const headerKeyStart = header.readUInt8(0)
const headerKeyAdd = header.readUInt8(1)
let offset = headerKeyStart
for (let i = 2; i < header.length; i++) {
let val = header.readUInt8(i)
val -= codeHead.charCodeAt(offset) + ((i - 2) % 5)
val = val & 0xff
header.writeUInt8(val, i)
offset = (offset + headerKeyAdd) & 0xff
}
const bodyKeyStart = header.readUInt8(16)
const bodyKeyAdd = header.readUInt8(17)
offset = bodyKeyStart
for (let i = 0; i < body.length; i++) {
let val = body.readUInt8(i)
val -= codeBody.charCodeAt(offset) + (i % 72)
val = val & 0xff
body.writeUInt8(val, i)
offset = (offset + bodyKeyAdd) & 0xff
}
// header format:
// key, zero, cmd, echo, totallength, thislength
// totalpacket, packetnum, body key, crc
return {
zero: header.readUInt16BE(2),
cmd: header.readUInt16BE(4),
packetTotal: header.readUInt16BE(12),
packetNum: header.readUInt16BE(14),
body
}
}
const codeHead =
'\x80\xe5\x0e\x38\xba\x63\x4c\x99\x88\x63\x4c\xd6\x54\xb8\x65\x7e' +
'\xbf\x8a\xf0\x17\x8a\xaa\x4d\x0f\xb7\x23\x27\xf6\xeb\x12\xf8\xea' +
'\x17\xb7\xcf\x52\x57\xcb\x51\xcf\x1b\x14\xfd\x6f\x84\x38\xb5\x24' +
'\x11\xcf\x7a\x75\x7a\xbb\x78\x74\xdc\xbc\x42\xf0\x17\x3f\x5e\xeb' +
'\x74\x77\x04\x4e\x8c\xaf\x23\xdc\x65\xdf\xa5\x65\xdd\x7d\xf4\x3c' +
'\x4c\x95\xbd\xeb\x65\x1c\xf4\x24\x5d\x82\x18\xfb\x50\x86\xb8\x53' +
'\xe0\x4e\x36\x96\x1f\xb7\xcb\xaa\xaf\xea\xcb\x20\x27\x30\x2a\xae' +
'\xb9\x07\x40\xdf\x12\x75\xc9\x09\x82\x9c\x30\x80\x5d\x8f\x0d\x09' +
'\xa1\x64\xec\x91\xd8\x8a\x50\x1f\x40\x5d\xf7\x08\x2a\xf8\x60\x62' +
'\xa0\x4a\x8b\xba\x4a\x6d\x00\x0a\x93\x32\x12\xe5\x07\x01\x65\xf5' +
'\xff\xe0\xae\xa7\x81\xd1\xba\x25\x62\x61\xb2\x85\xad\x7e\x9d\x3f' +
'\x49\x89\x26\xe5\xd5\xac\x9f\x0e\xd7\x6e\x47\x94\x16\x84\xc8\xff' +
'\x44\xea\x04\x40\xe0\x33\x11\xa3\x5b\x1e\x82\xff\x7a\x69\xe9\x2f' +
'\xfb\xea\x9a\xc6\x7b\xdb\xb1\xff\x97\x76\x56\xf3\x52\xc2\x3f\x0f' +
'\xb6\xac\x77\xc4\xbf\x59\x5e\x80\x74\xbb\xf2\xde\x57\x62\x4c\x1a' +
'\xff\x95\x6d\xc7\x04\xa2\x3b\xc4\x1b\x72\xc7\x6c\x82\x60\xd1\x0d'
const codeBody =
'\x82\x8b\x7f\x68\x90\xe0\x44\x09\x19\x3b\x8e\x5f\xc2\x82\x38\x23' +
'\x6d\xdb\x62\x49\x52\x6e\x21\xdf\x51\x6c\x76\x37\x86\x50\x7d\x48' +
'\x1f\x65\xe7\x52\x6a\x88\xaa\xc1\x32\x2f\xf7\x54\x4c\xaa\x6d\x7e' +
'\x6d\xa9\x8c\x0d\x3f\xff\x6c\x09\xb3\xa5\xaf\xdf\x98\x02\xb4\xbe' +
'\x6d\x69\x0d\x42\x73\xe4\x34\x50\x07\x30\x79\x41\x2f\x08\x3f\x42' +
'\x73\xa7\x68\xfa\xee\x88\x0e\x6e\xa4\x70\x74\x22\x16\xae\x3c\x81' +
'\x14\xa1\xda\x7f\xd3\x7c\x48\x7d\x3f\x46\xfb\x6d\x92\x25\x17\x36' +
'\x26\xdb\xdf\x5a\x87\x91\x6f\xd6\xcd\xd4\xad\x4a\x29\xdd\x7d\x59' +
'\xbd\x15\x34\x53\xb1\xd8\x50\x11\x83\x79\x66\x21\x9e\x87\x5b\x24' +
'\x2f\x4f\xd7\x73\x34\xa2\xf7\x09\xd5\xd9\x42\x9d\xf8\x15\xdf\x0e' +
'\x10\xcc\x05\x04\x35\x81\xb2\xd5\x7a\xd2\xa0\xa5\x7b\xb8\x75\xd2' +
'\x35\x0b\x39\x8f\x1b\x44\x0e\xce\x66\x87\x1b\x64\xac\xe1\xca\x67' +
'\xb4\xce\x33\xdb\x89\xfe\xd8\x8e\xcd\x58\x92\x41\x50\x40\xcb\x08' +
'\xe1\x15\xee\xf4\x64\xfe\x1c\xee\x25\xe7\x21\xe6\x6c\xc6\xa6\x2e' +
'\x52\x23\xa7\x20\xd2\xd7\x28\x07\x23\x14\x24\x3d\x45\xa5\xc7\x90' +
'\xdb\x77\xdd\xea\x38\x59\x89\x32\xbc\x00\x3a\x6d\x61\x4e\xdb\x29'
const crcTable = [
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
]
import Core from './core.js'
export default class ventrilo extends Core {
constructor () {
super()
this.byteorder = 'be'
}
async run (state) {
const data = await this.sendCommand(2, '')
state.raw = splitFields(data.toString())
for (const client of state.raw.CLIENTS) {
client.name = client.NAME
delete client.NAME
client.ping = parseInt(client.PING)
delete client.PING
state.players.push(client)
}
delete state.raw.CLIENTS
state.numplayers = state.players.length
if ('NAME' in state.raw) state.name = state.raw.NAME
if ('MAXCLIENTS' in state.raw) state.maxplayers = state.raw.MAXCLIENTS
if (this.trueTest(state.raw.AUTH)) state.password = true
}
async sendCommand (cmd, password) {
const body = Buffer.alloc(16)
body.write(password, 0, 15, 'utf8')
const encrypted = encrypt(cmd, body)
const packets = {}
return await this.udpSend(encrypted, (buffer) => {
if (buffer.length < 20) return
const data = decrypt(buffer)
if (data.zero !== 0) return
packets[data.packetNum] = data.body
if (Object.keys(packets).length !== data.packetTotal) return
const out = []
for (let i = 0; i < data.packetTotal; i++) {
if (!(i in packets)) throw new Error('Missing packet #' + i)
out.push(packets[i])
}
return Buffer.concat(out)
})
}
}
function splitFields (str, subMode) {
let splitter, delim
if (subMode) {
splitter = '='
delim = ','
} else {
splitter = ': '
delim = '\n'
}
const split = str.split(delim)
const out = {}
if (!subMode) {
out.CHANNELS = []
out.CLIENTS = []
}
for (const one of split) {
const equal = one.indexOf(splitter)
const key = equal === -1 ? one : one.substring(0, equal)
if (!key || key === '\0') continue
const value = equal === -1 ? '' : one.substring(equal + splitter.length)
if (!subMode && key === 'CHANNEL') out.CHANNELS.push(splitFields(value, true))
else if (!subMode && key === 'CLIENT') out.CLIENTS.push(splitFields(value, true))
else out[key] = value
}
return out
}
function randInt (min, max) {
return Math.floor(Math.random() * (max - min + 1) + min)
}
function crc (body) {
let crc = 0
for (let i = 0; i < body.length; i++) {
crc = crcTable[crc >> 8] ^ body.readUInt8(i) ^ (crc << 8)
crc &= 0xffff
}
return crc
}
function encrypt (cmd, body) {
const headerKeyStart = randInt(0, 0xff)
const headerKeyAdd = randInt(1, 0xff)
const bodyKeyStart = randInt(0, 0xff)
const bodyKeyAdd = randInt(1, 0xff)
const header = Buffer.alloc(20)
header.writeUInt8(headerKeyStart, 0)
header.writeUInt8(headerKeyAdd, 1)
header.writeUInt16BE(cmd, 4)
header.writeUInt16BE(body.length, 8)
header.writeUInt16BE(body.length, 10)
header.writeUInt16BE(1, 12)
header.writeUInt16BE(0, 14)
header.writeUInt8(bodyKeyStart, 16)
header.writeUInt8(bodyKeyAdd, 17)
header.writeUInt16BE(crc(body), 18)
let offset = headerKeyStart
for (let i = 2; i < header.length; i++) {
let val = header.readUInt8(i)
val += codeHead.charCodeAt(offset) + ((i - 2) % 5)
val = val & 0xff
header.writeUInt8(val, i)
offset = (offset + headerKeyAdd) & 0xff
}
offset = bodyKeyStart
for (let i = 0; i < body.length; i++) {
let val = body.readUInt8(i)
val += codeBody.charCodeAt(offset) + (i % 72)
val = val & 0xff
body.writeUInt8(val, i)
offset = (offset + bodyKeyAdd) & 0xff
}
return Buffer.concat([header, body])
}
function decrypt (data) {
const header = data.slice(0, 20)
const body = data.slice(20)
const headerKeyStart = header.readUInt8(0)
const headerKeyAdd = header.readUInt8(1)
let offset = headerKeyStart
for (let i = 2; i < header.length; i++) {
let val = header.readUInt8(i)
val -= codeHead.charCodeAt(offset) + ((i - 2) % 5)
val = val & 0xff
header.writeUInt8(val, i)
offset = (offset + headerKeyAdd) & 0xff
}
const bodyKeyStart = header.readUInt8(16)
const bodyKeyAdd = header.readUInt8(17)
offset = bodyKeyStart
for (let i = 0; i < body.length; i++) {
let val = body.readUInt8(i)
val -= codeBody.charCodeAt(offset) + (i % 72)
val = val & 0xff
body.writeUInt8(val, i)
offset = (offset + bodyKeyAdd) & 0xff
}
// header format:
// key, zero, cmd, echo, totallength, thislength
// totalpacket, packetnum, body key, crc
return {
zero: header.readUInt16BE(2),
cmd: header.readUInt16BE(4),
packetTotal: header.readUInt16BE(12),
packetNum: header.readUInt16BE(14),
body
}
}
const codeHead =
'\x80\xe5\x0e\x38\xba\x63\x4c\x99\x88\x63\x4c\xd6\x54\xb8\x65\x7e' +
'\xbf\x8a\xf0\x17\x8a\xaa\x4d\x0f\xb7\x23\x27\xf6\xeb\x12\xf8\xea' +
'\x17\xb7\xcf\x52\x57\xcb\x51\xcf\x1b\x14\xfd\x6f\x84\x38\xb5\x24' +
'\x11\xcf\x7a\x75\x7a\xbb\x78\x74\xdc\xbc\x42\xf0\x17\x3f\x5e\xeb' +
'\x74\x77\x04\x4e\x8c\xaf\x23\xdc\x65\xdf\xa5\x65\xdd\x7d\xf4\x3c' +
'\x4c\x95\xbd\xeb\x65\x1c\xf4\x24\x5d\x82\x18\xfb\x50\x86\xb8\x53' +
'\xe0\x4e\x36\x96\x1f\xb7\xcb\xaa\xaf\xea\xcb\x20\x27\x30\x2a\xae' +
'\xb9\x07\x40\xdf\x12\x75\xc9\x09\x82\x9c\x30\x80\x5d\x8f\x0d\x09' +
'\xa1\x64\xec\x91\xd8\x8a\x50\x1f\x40\x5d\xf7\x08\x2a\xf8\x60\x62' +
'\xa0\x4a\x8b\xba\x4a\x6d\x00\x0a\x93\x32\x12\xe5\x07\x01\x65\xf5' +
'\xff\xe0\xae\xa7\x81\xd1\xba\x25\x62\x61\xb2\x85\xad\x7e\x9d\x3f' +
'\x49\x89\x26\xe5\xd5\xac\x9f\x0e\xd7\x6e\x47\x94\x16\x84\xc8\xff' +
'\x44\xea\x04\x40\xe0\x33\x11\xa3\x5b\x1e\x82\xff\x7a\x69\xe9\x2f' +
'\xfb\xea\x9a\xc6\x7b\xdb\xb1\xff\x97\x76\x56\xf3\x52\xc2\x3f\x0f' +
'\xb6\xac\x77\xc4\xbf\x59\x5e\x80\x74\xbb\xf2\xde\x57\x62\x4c\x1a' +
'\xff\x95\x6d\xc7\x04\xa2\x3b\xc4\x1b\x72\xc7\x6c\x82\x60\xd1\x0d'
const codeBody =
'\x82\x8b\x7f\x68\x90\xe0\x44\x09\x19\x3b\x8e\x5f\xc2\x82\x38\x23' +
'\x6d\xdb\x62\x49\x52\x6e\x21\xdf\x51\x6c\x76\x37\x86\x50\x7d\x48' +
'\x1f\x65\xe7\x52\x6a\x88\xaa\xc1\x32\x2f\xf7\x54\x4c\xaa\x6d\x7e' +
'\x6d\xa9\x8c\x0d\x3f\xff\x6c\x09\xb3\xa5\xaf\xdf\x98\x02\xb4\xbe' +
'\x6d\x69\x0d\x42\x73\xe4\x34\x50\x07\x30\x79\x41\x2f\x08\x3f\x42' +
'\x73\xa7\x68\xfa\xee\x88\x0e\x6e\xa4\x70\x74\x22\x16\xae\x3c\x81' +
'\x14\xa1\xda\x7f\xd3\x7c\x48\x7d\x3f\x46\xfb\x6d\x92\x25\x17\x36' +
'\x26\xdb\xdf\x5a\x87\x91\x6f\xd6\xcd\xd4\xad\x4a\x29\xdd\x7d\x59' +
'\xbd\x15\x34\x53\xb1\xd8\x50\x11\x83\x79\x66\x21\x9e\x87\x5b\x24' +
'\x2f\x4f\xd7\x73\x34\xa2\xf7\x09\xd5\xd9\x42\x9d\xf8\x15\xdf\x0e' +
'\x10\xcc\x05\x04\x35\x81\xb2\xd5\x7a\xd2\xa0\xa5\x7b\xb8\x75\xd2' +
'\x35\x0b\x39\x8f\x1b\x44\x0e\xce\x66\x87\x1b\x64\xac\xe1\xca\x67' +
'\xb4\xce\x33\xdb\x89\xfe\xd8\x8e\xcd\x58\x92\x41\x50\x40\xcb\x08' +
'\xe1\x15\xee\xf4\x64\xfe\x1c\xee\x25\xe7\x21\xe6\x6c\xc6\xa6\x2e' +
'\x52\x23\xa7\x20\xd2\xd7\x28\x07\x23\x14\x24\x3d\x45\xa5\xc7\x90' +
'\xdb\x77\xdd\xea\x38\x59\x89\x32\xbc\x00\x3a\x6d\x61\x4e\xdb\x29'
const crcTable = [
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
]

View file

@ -1,13 +1,13 @@
import quake3 from './quake3.js'
export default class warsow extends quake3 {
async run (state) {
await super.run(state)
if (state.players) {
for (const player of state.players) {
player.team = player.address
delete player.address
}
}
}
}
import quake3 from './quake3.js'
export default class warsow extends quake3 {
async run (state) {
await super.run(state)
if (state.players) {
for (const player of state.players) {
player.team = player.address
delete player.address
}
}
}
}