Merge branch 'master' into feat/version-top-level
This commit is contained in:
commit
6639536209
|
@ -7,6 +7,11 @@
|
|||
* Factorio (2016) - Added support (By @Vito0912 #527)
|
||||
* Farming Simulator 22 (2021) - Added support (By @Vito0912 #531)
|
||||
* Farming Simulator 19 (2018) - Added support (By @Vito0912 #531)
|
||||
* Assetto Corsa - Fixed how `state.numplayers` is set (By @podrivo #538)
|
||||
* TeamSpeak 2 - Fixed how `state.name` is set (By @podrivo #544)
|
||||
* Grand Theft Auto: San Andreas OpenMP - Fixed `state.players` returning an empty array (By @Focus04 #547)
|
||||
* Perf: Re-write of the `core` class.
|
||||
* Perf: Remove many if statements from `GameSpy2`.
|
||||
|
||||
## 5.0.0-beta.2
|
||||
* Fixed support for projects using `require`.
|
||||
|
|
|
@ -356,6 +356,7 @@
|
|||
* GTR 2
|
||||
* Haze
|
||||
* Hexen World
|
||||
* Last Oasis (#248, #446, #352)
|
||||
* Lost Heaven
|
||||
* Multi Theft Auto
|
||||
* Pariah
|
||||
|
|
|
@ -2446,7 +2446,7 @@ export const games = {
|
|||
release_year: 2019,
|
||||
options: {
|
||||
port: 7777,
|
||||
protocol: 'samp'
|
||||
protocol: 'gtasao'
|
||||
},
|
||||
extra: {
|
||||
old_id: 'saomp'
|
||||
|
|
|
@ -36,6 +36,6 @@ export default class assettocorsa extends Core {
|
|||
}
|
||||
}
|
||||
|
||||
state.numplayers = carInfo.Cars.length
|
||||
state.numplayers = serverInfo.clients
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,9 @@ export default class Core extends EventEmitter {
|
|||
|
||||
// Runs a single attempt with a timeout and cleans up afterward
|
||||
async runOnceSafe () {
|
||||
if (this.options.debug) {
|
||||
const { debug, attemptTimeout } = this.options
|
||||
|
||||
if (debug) {
|
||||
this.logger.debugEnabled = true
|
||||
}
|
||||
this.logger.prefix = 'Q#' + (uid++)
|
||||
|
@ -41,16 +43,16 @@ export default class Core extends EventEmitter {
|
|||
this.logger.debug('Options:', this.options)
|
||||
|
||||
let abortCall = null
|
||||
this.abortedPromise = new Promise((resolve, reject) => {
|
||||
this.abortedPromise = new Promise((_resolve, reject) => {
|
||||
abortCall = () => reject(new Error('Query is finished -- cancelling outstanding promises'))
|
||||
}).catch(() => {
|
||||
// Make sure that if this promise isn't attached to, it doesn't throw a unhandled promise rejection
|
||||
// Make sure that if this promise isn't attached to, it doesn't throw an unhandled promise rejection
|
||||
})
|
||||
|
||||
let timeout
|
||||
try {
|
||||
const promise = this.runOnce()
|
||||
timeout = Promises.createTimeout(this.options.attemptTimeout, 'Attempt')
|
||||
timeout = Promises.createTimeout(attemptTimeout, 'Attempt')
|
||||
const result = await Promise.race([promise, timeout])
|
||||
this.logger.debug('Query was successful')
|
||||
return result
|
||||
|
@ -58,9 +60,9 @@ export default class Core extends EventEmitter {
|
|||
this.logger.debug('Query failed with error', e)
|
||||
throw e
|
||||
} finally {
|
||||
timeout && timeout.cancel()
|
||||
timeout?.cancel()
|
||||
try {
|
||||
abortCall()
|
||||
abortCall?.()
|
||||
} catch (e) {
|
||||
this.logger.debug('Error during abort cleanup: ' + e.stack)
|
||||
}
|
||||
|
@ -68,34 +70,29 @@ export default class Core extends EventEmitter {
|
|||
}
|
||||
|
||||
async runOnce () {
|
||||
const options = this.options
|
||||
const { options, dnsResolver } = this
|
||||
|
||||
if (('host' in options) && !('address' in options)) {
|
||||
const resolved = await this.dnsResolver.resolve(options.host, options.ipFamily, this.srvRecord)
|
||||
const resolved = await dnsResolver.resolve(options.host, options.ipFamily, this.srvRecord)
|
||||
options.address = resolved.address
|
||||
if (resolved.port) options.port = resolved.port
|
||||
options.port ||= resolved.port
|
||||
}
|
||||
|
||||
const state = new Results()
|
||||
|
||||
await this.run(state)
|
||||
state.queryPort = options.port
|
||||
|
||||
state.queryPort = options.port
|
||||
// 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.connect = `${state.gameHost || options.host || options.address}:${state.gamePort || 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)
|
||||
log('Size of players array:', state.players.length)
|
||||
log('Size of bots array:', state.bots.length)
|
||||
})
|
||||
|
||||
return state
|
||||
|
@ -104,19 +101,20 @@ export default class Core extends EventEmitter {
|
|||
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
|
||||
async registerRtt (param) {
|
||||
try {
|
||||
if (param instanceof Promise) {
|
||||
const start = Date.now()
|
||||
await param
|
||||
await this.registerRtt(Date.now() - start)
|
||||
} else {
|
||||
this.logger.debug(`Registered RTT: ${param}ms`)
|
||||
if (this.shortestRTT === 0 || param < this.shortestRTT) {
|
||||
this.shortestRTT = param
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.debug(`Error in promise: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -164,8 +162,10 @@ export default class Core extends EventEmitter {
|
|||
*/
|
||||
async withTcp (fn, port) {
|
||||
this.usedTcp = true
|
||||
const { options, logger } = this
|
||||
const address = this.options.address
|
||||
if (!port) port = this.options.port
|
||||
port ??= options.port
|
||||
|
||||
this.assertValidPort(port)
|
||||
|
||||
let socket, connectionTimeout
|
||||
|
@ -176,28 +176,29 @@ export default class Core extends EventEmitter {
|
|||
// Prevent unhandled 'error' events from dumping straight to console
|
||||
socket.on('error', () => {})
|
||||
|
||||
this.logger.debug(log => {
|
||||
this.logger.debug(address + ':' + port + ' TCP Connecting')
|
||||
logger.debug(log => {
|
||||
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(`${address}:${port} <--TCP`)
|
||||
log(data)
|
||||
})
|
||||
socket.on('ready', () => log(address + ':' + port + ' TCP Connected'))
|
||||
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)
|
||||
await this.registerRtt(connectionPromise)
|
||||
connectionTimeout = Promises.createTimeout(this.options.socketTimeout, 'TCP Opening')
|
||||
await Promise.race([
|
||||
connectionPromise,
|
||||
|
@ -206,8 +207,8 @@ export default class Core extends EventEmitter {
|
|||
])
|
||||
return await fn(socket)
|
||||
} finally {
|
||||
socket && socket.destroy()
|
||||
connectionTimeout && connectionTimeout.cancel()
|
||||
socket?.destroy()
|
||||
connectionTimeout?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -221,7 +222,7 @@ export default class Core extends EventEmitter {
|
|||
async tcpSend (socket, buffer, ondata) {
|
||||
let timeout
|
||||
try {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
const promise = new Promise((resolve, _reject) => {
|
||||
let received = Buffer.from([])
|
||||
const onData = (data) => {
|
||||
received = Buffer.concat([received, data])
|
||||
|
@ -237,7 +238,7 @@ export default class Core extends EventEmitter {
|
|||
timeout = Promises.createTimeout(this.options.socketTimeout, 'TCP')
|
||||
return await Promise.race([promise, timeout, this.abortedPromise])
|
||||
} finally {
|
||||
timeout && timeout.cancel()
|
||||
timeout?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -249,8 +250,7 @@ export default class Core extends EventEmitter {
|
|||
* @template T
|
||||
*/
|
||||
async udpSend (buffer, onPacket, onTimeout) {
|
||||
const address = this.options.address
|
||||
const port = this.options.port
|
||||
const { address, port, debug, socketTimeout } = this.options
|
||||
this.assertValidPort(port)
|
||||
|
||||
if (typeof buffer === 'string') buffer = Buffer.from(buffer, 'binary')
|
||||
|
@ -264,19 +264,14 @@ export default class Core extends EventEmitter {
|
|||
|
||||
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)
|
||||
}
|
||||
if (fromAddress !== address || fromPort !== port) return
|
||||
this.registerRtt(Date.now() - start)
|
||||
const result = onPacket(buffer)
|
||||
if (result !== undefined) {
|
||||
this.logger.debug('UDP send finished by callback')
|
||||
|
@ -286,30 +281,24 @@ export default class Core extends EventEmitter {
|
|||
reject(e)
|
||||
}
|
||||
}
|
||||
socket.addCallback(socketCallback, this.options.debug)
|
||||
socket.addCallback(socketCallback, 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)
|
||||
}
|
||||
timeout = Promises.createTimeout(socketTimeout, 'UDP')
|
||||
const wrappedTimeout = Promise.resolve(timeout).catch((e) => {
|
||||
this.logger.debug('UDP timeout detected')
|
||||
if (onTimeout) {
|
||||
const result = onTimeout()
|
||||
if (result !== undefined) {
|
||||
this.logger.debug('UDP timeout resolved by callback')
|
||||
return result
|
||||
}
|
||||
reject(e)
|
||||
})
|
||||
}
|
||||
throw e
|
||||
})
|
||||
|
||||
return await Promise.race([promise, wrappedTimeout, this.abortedPromise])
|
||||
} finally {
|
||||
timeout && timeout.cancel()
|
||||
timeout?.cancel()
|
||||
socketCallback && socket.removeCallback(socketCallback)
|
||||
}
|
||||
}
|
||||
|
@ -344,7 +333,7 @@ export default class Core extends EventEmitter {
|
|||
})
|
||||
return await Promise.race([wrappedPromise, this.abortedPromise])
|
||||
} finally {
|
||||
requestPromise && requestPromise.cancel()
|
||||
requestPromise?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -111,26 +111,23 @@ export default class gamespy2 extends Core {
|
|||
const units = []
|
||||
while (!reader.done()) {
|
||||
const unit = {}
|
||||
for (let iField = 0; iField < fields.length; iField++) {
|
||||
let key = fields[iField]
|
||||
for (let index = 0; index < fields.length; index++) {
|
||||
let key = fields[index]
|
||||
let value = reader.string()
|
||||
if (!value && iField === 0) return units
|
||||
if (!value && index === 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'
|
||||
|
||||
// many fields end with "_"
|
||||
if (key.endsWith('_')) {
|
||||
key = key.slice(0, -1)
|
||||
}
|
||||
|
||||
if (key === 'player') key = 'name'
|
||||
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 (['score', 'deaths', 'ping', 'team', 'kills', 'tickets'].includes(key)) {
|
||||
if (value === '') continue
|
||||
value = parseInt(value)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import samp from './samp.js'
|
||||
|
||||
export default class gtasao extends samp {
|
||||
constructor() {
|
||||
super()
|
||||
this.isOmp = true
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ import gamespy2 from './gamespy2.js'
|
|||
import gamespy3 from './gamespy3.js'
|
||||
import geneshift from './geneshift.js'
|
||||
import goldsrc from './goldsrc.js'
|
||||
import gtasao from './gtasao.js'
|
||||
import hexen2 from './hexen2.js'
|
||||
import jc2mp from './jc2mp.js'
|
||||
import kspdmp from './kspdmp.js'
|
||||
|
@ -58,7 +59,7 @@ import theisleevrima from './theisleevrima.js'
|
|||
|
||||
export {
|
||||
armagetron, ase, asa, assettocorsa, battlefield, buildandshoot, cs2d, discord, doom3, eco, epic, factorio, farmingsimulator, ffow,
|
||||
fivem, gamespy1, gamespy2, gamespy3, geneshift, goldsrc, hexen2, jc2mp, kspdmp, mafia2mp, mafia2online, minecraft,
|
||||
fivem, gamespy1, gamespy2, gamespy3, geneshift, goldsrc, gtasao, hexen2, jc2mp, kspdmp, mafia2mp, mafia2online, minecraft,
|
||||
minecraftbedrock, minecraftvanilla, mumble, mumbleping, nadeo, openttd, palworld, quake1, quake2, quake3, rfactor, samp,
|
||||
savage2, starmade, starsiege, teamspeak2, teamspeak3, terraria, tribes1, tribes1master, unreal2, ut3, valve,
|
||||
vcmp, ventrilo, warsow, eldewrito, beammpmaster, beammp, dayz, theisleevrima
|
||||
|
|
|
@ -7,6 +7,7 @@ export default class samp extends Core {
|
|||
this.magicHeader = 'SAMP'
|
||||
this.responseMagicHeader = null
|
||||
this.isVcmp = false
|
||||
this.isOmp = false
|
||||
}
|
||||
|
||||
async run (state) {
|
||||
|
@ -40,13 +41,14 @@ export default class samp extends Core {
|
|||
// read players
|
||||
// don't even bother if > 100 players, because the server won't respond
|
||||
if (state.numplayers < 100) {
|
||||
if (this.isVcmp) {
|
||||
if (this.isVcmp || this.isOmp) {
|
||||
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)
|
||||
player.score = reader.int(4)
|
||||
state.players.push(player)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ export default class teamspeak2 extends Core {
|
|||
const value = equals === -1 ? '' : line.substring(equals + 1)
|
||||
state.raw[key] = value
|
||||
}
|
||||
if ('server_name' in state.raw) state.name = state.raw.server_name
|
||||
}
|
||||
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue