gamedigz 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GAMES_LIST.md +453 -0
- package/LICENSE +21 -0
- package/README.md +144 -0
- package/bin/gamedig.js +79 -0
- package/lib/DnsResolver.js +76 -0
- package/lib/GlobalUdpSocket.js +69 -0
- package/lib/HexUtil.js +20 -0
- package/lib/Logger.js +45 -0
- package/lib/Promises.js +18 -0
- package/lib/ProtocolResolver.js +7 -0
- package/lib/QueryRunner.js +95 -0
- package/lib/Results.js +32 -0
- package/lib/game-resolver.js +17 -0
- package/lib/gamedig.js +23 -0
- package/lib/games.js +2747 -0
- package/lib/index.js +5 -0
- package/lib/reader.js +172 -0
- package/package.json +74 -0
- package/protocols/armagetron.js +65 -0
- package/protocols/asa.js +12 -0
- package/protocols/ase.js +45 -0
- package/protocols/assettocorsa.js +40 -0
- package/protocols/battlefield.js +162 -0
- package/protocols/beammp.js +32 -0
- package/protocols/beammpmaster.js +17 -0
- package/protocols/buildandshoot.js +55 -0
- package/protocols/core.js +349 -0
- package/protocols/cs2d.js +65 -0
- package/protocols/dayz.js +196 -0
- package/protocols/discord.js +29 -0
- package/protocols/doom3.js +148 -0
- package/protocols/eco.js +20 -0
- package/protocols/eldewrito.js +21 -0
- package/protocols/epic.js +95 -0
- package/protocols/ffow.js +38 -0
- package/protocols/fivem.js +33 -0
- package/protocols/gamespy1.js +181 -0
- package/protocols/gamespy2.js +144 -0
- package/protocols/gamespy3.js +197 -0
- package/protocols/geneshift.js +46 -0
- package/protocols/goldsrc.js +8 -0
- package/protocols/hexen2.js +14 -0
- package/protocols/index.js +61 -0
- package/protocols/jc2mp.js +16 -0
- package/protocols/kspdmp.js +28 -0
- package/protocols/mafia2mp.js +41 -0
- package/protocols/mafia2online.js +9 -0
- package/protocols/minecraft.js +102 -0
- package/protocols/minecraftbedrock.js +72 -0
- package/protocols/minecraftvanilla.js +87 -0
- package/protocols/mumble.js +39 -0
- package/protocols/mumbleping.js +24 -0
- package/protocols/nadeo.js +86 -0
- package/protocols/openttd.js +127 -0
- package/protocols/quake1.js +9 -0
- package/protocols/quake2.js +88 -0
- package/protocols/quake3.js +24 -0
- package/protocols/rfactor.js +69 -0
- package/protocols/samp.js +102 -0
- package/protocols/savage2.js +25 -0
- package/protocols/starmade.js +67 -0
- package/protocols/starsiege.js +10 -0
- package/protocols/teamspeak2.js +71 -0
- package/protocols/teamspeak3.js +69 -0
- package/protocols/terraria.js +24 -0
- package/protocols/tribes1.js +153 -0
- package/protocols/tribes1master.js +80 -0
- package/protocols/unreal2.js +150 -0
- package/protocols/ut3.js +45 -0
- package/protocols/valve.js +455 -0
- package/protocols/vcmp.js +10 -0
- package/protocols/ventrilo.js +237 -0
- package/protocols/warsow.js +13 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events'
|
|
2
|
+
import * as net from 'node:net'
|
|
3
|
+
import got from 'got'
|
|
4
|
+
import Reader from '../lib/reader.js'
|
|
5
|
+
import { debugDump } from '../lib/HexUtil.js'
|
|
6
|
+
import Logger from '../lib/Logger.js'
|
|
7
|
+
import DnsResolver from '../lib/DnsResolver.js'
|
|
8
|
+
import { Results } from '../lib/Results.js'
|
|
9
|
+
import Promises from '../lib/Promises.js'
|
|
10
|
+
|
|
11
|
+
let uid = 0
|
|
12
|
+
|
|
13
|
+
export default class Core extends EventEmitter {
|
|
14
|
+
constructor () {
|
|
15
|
+
super()
|
|
16
|
+
this.encoding = 'utf8'
|
|
17
|
+
this.byteorder = 'le'
|
|
18
|
+
this.delimiter = '\0'
|
|
19
|
+
this.srvRecord = null
|
|
20
|
+
this.abortedPromise = null
|
|
21
|
+
this.logger = new Logger()
|
|
22
|
+
this.dnsResolver = new DnsResolver(this.logger)
|
|
23
|
+
|
|
24
|
+
// Sent to us by QueryRunner
|
|
25
|
+
this.options = null
|
|
26
|
+
/** @type GlobalUdpSocket */
|
|
27
|
+
this.udpSocket = null
|
|
28
|
+
this.shortestRTT = 0
|
|
29
|
+
this.usedTcp = false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Runs a single attempt with a timeout and cleans up afterward
|
|
33
|
+
async runOnceSafe () {
|
|
34
|
+
if (this.options.debug) {
|
|
35
|
+
this.logger.debugEnabled = true
|
|
36
|
+
}
|
|
37
|
+
this.logger.prefix = 'Q#' + (uid++)
|
|
38
|
+
|
|
39
|
+
this.logger.debug('Starting')
|
|
40
|
+
this.logger.debug('Protocol: ' + this.constructor.name)
|
|
41
|
+
this.logger.debug('Options:', this.options)
|
|
42
|
+
|
|
43
|
+
let abortCall = null
|
|
44
|
+
this.abortedPromise = new Promise((resolve, reject) => {
|
|
45
|
+
abortCall = () => reject(new Error('Query is finished -- cancelling outstanding promises'))
|
|
46
|
+
}).catch(() => {
|
|
47
|
+
// Make sure that if this promise isn't attached to, it doesn't throw a unhandled promise rejection
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
let timeout
|
|
51
|
+
try {
|
|
52
|
+
const promise = this.runOnce()
|
|
53
|
+
timeout = Promises.createTimeout(this.options.attemptTimeout, 'Attempt')
|
|
54
|
+
const result = await Promise.race([promise, timeout])
|
|
55
|
+
this.logger.debug('Query was successful')
|
|
56
|
+
return result
|
|
57
|
+
} catch (e) {
|
|
58
|
+
this.logger.debug('Query failed with error', e)
|
|
59
|
+
throw e
|
|
60
|
+
} finally {
|
|
61
|
+
timeout && timeout.cancel()
|
|
62
|
+
try {
|
|
63
|
+
abortCall()
|
|
64
|
+
} catch (e) {
|
|
65
|
+
this.logger.debug('Error during abort cleanup: ' + e.stack)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async runOnce () {
|
|
71
|
+
const options = this.options
|
|
72
|
+
if (('host' in options) && !('address' in options)) {
|
|
73
|
+
const resolved = await this.dnsResolver.resolve(options.host, options.ipFamily, this.srvRecord)
|
|
74
|
+
options.address = resolved.address
|
|
75
|
+
if (resolved.port) options.port = resolved.port
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const state = new Results()
|
|
79
|
+
|
|
80
|
+
await this.run(state)
|
|
81
|
+
|
|
82
|
+
// because lots of servers prefix with spaces to try to appear first
|
|
83
|
+
state.name = (state.name || '').trim()
|
|
84
|
+
|
|
85
|
+
if (!('connect' in state)) {
|
|
86
|
+
state.connect = '' +
|
|
87
|
+
(state.gameHost || this.options.host || this.options.address) +
|
|
88
|
+
':' +
|
|
89
|
+
(state.gamePort || this.options.port)
|
|
90
|
+
}
|
|
91
|
+
state.ping = this.shortestRTT
|
|
92
|
+
delete state.gameHost
|
|
93
|
+
delete state.gamePort
|
|
94
|
+
|
|
95
|
+
this.logger.debug(log => {
|
|
96
|
+
log('Size of players array: ' + state.players.length)
|
|
97
|
+
log('Size of bots array: ' + state.bots.length)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
return state
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async run (/** Results */ state) {}
|
|
104
|
+
|
|
105
|
+
/** Param can be a time in ms, or a promise (which will be timed) */
|
|
106
|
+
registerRtt (param) {
|
|
107
|
+
if (param.then) {
|
|
108
|
+
const start = Date.now()
|
|
109
|
+
param.then(() => {
|
|
110
|
+
const end = Date.now()
|
|
111
|
+
const rtt = end - start
|
|
112
|
+
this.registerRtt(rtt)
|
|
113
|
+
}).catch(() => {})
|
|
114
|
+
} else {
|
|
115
|
+
this.logger.debug('Registered RTT: ' + param + 'ms')
|
|
116
|
+
if (this.shortestRTT === 0 || param < this.shortestRTT) {
|
|
117
|
+
this.shortestRTT = param
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// utils
|
|
123
|
+
/** @returns {Reader} */
|
|
124
|
+
reader (buffer) {
|
|
125
|
+
return new Reader(this, buffer)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
translate (obj, trans) {
|
|
129
|
+
for (const from of Object.keys(trans)) {
|
|
130
|
+
const to = trans[from]
|
|
131
|
+
if (from in obj) {
|
|
132
|
+
if (to) obj[to] = obj[from]
|
|
133
|
+
delete obj[from]
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
trueTest (str) {
|
|
139
|
+
if (typeof str === 'boolean') return str
|
|
140
|
+
if (typeof str === 'number') return str !== 0
|
|
141
|
+
if (typeof str === 'string') {
|
|
142
|
+
if (str.toLowerCase() === 'true') return true
|
|
143
|
+
if (str.toLowerCase() === 'yes') return true
|
|
144
|
+
if (str === '1') return true
|
|
145
|
+
}
|
|
146
|
+
return false
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
assertValidPort (port) {
|
|
150
|
+
if (!port) {
|
|
151
|
+
throw new Error('Could not determine port to query. Did you provide a port?')
|
|
152
|
+
}
|
|
153
|
+
if (port < 1 || port > 65535) {
|
|
154
|
+
throw new Error('Invalid tcp/ip port: ' + port)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @template T
|
|
160
|
+
* @param {function(NodeJS.Socket):Promise<T>} fn
|
|
161
|
+
* @param {number=} port
|
|
162
|
+
* @returns {Promise<T>}
|
|
163
|
+
*/
|
|
164
|
+
async withTcp (fn, port) {
|
|
165
|
+
this.usedTcp = true
|
|
166
|
+
const address = this.options.address
|
|
167
|
+
if (!port) port = this.options.port
|
|
168
|
+
this.assertValidPort(port)
|
|
169
|
+
|
|
170
|
+
let socket, connectionTimeout
|
|
171
|
+
try {
|
|
172
|
+
socket = net.connect(port, address)
|
|
173
|
+
socket.setNoDelay(true)
|
|
174
|
+
|
|
175
|
+
// Prevent unhandled 'error' events from dumping straight to console
|
|
176
|
+
socket.on('error', () => {})
|
|
177
|
+
|
|
178
|
+
this.logger.debug(log => {
|
|
179
|
+
this.logger.debug(address + ':' + port + ' TCP Connecting')
|
|
180
|
+
const writeHook = socket.write
|
|
181
|
+
socket.write = (...args) => {
|
|
182
|
+
log(address + ':' + port + ' TCP-->')
|
|
183
|
+
log(debugDump(args[0]))
|
|
184
|
+
writeHook.apply(socket, args)
|
|
185
|
+
}
|
|
186
|
+
socket.on('error', e => log('TCP Error:', e))
|
|
187
|
+
socket.on('close', () => log('TCP Closed'))
|
|
188
|
+
socket.on('data', (data) => {
|
|
189
|
+
log(address + ':' + port + ' <--TCP')
|
|
190
|
+
log(data)
|
|
191
|
+
})
|
|
192
|
+
socket.on('ready', () => log(address + ':' + port + ' TCP Connected'))
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
const connectionPromise = new Promise((resolve, reject) => {
|
|
196
|
+
socket.on('ready', resolve)
|
|
197
|
+
socket.on('close', () => reject(new Error('TCP Connection Refused')))
|
|
198
|
+
})
|
|
199
|
+
this.registerRtt(connectionPromise)
|
|
200
|
+
connectionTimeout = Promises.createTimeout(this.options.socketTimeout, 'TCP Opening')
|
|
201
|
+
await Promise.race([
|
|
202
|
+
connectionPromise,
|
|
203
|
+
connectionTimeout,
|
|
204
|
+
this.abortedPromise
|
|
205
|
+
])
|
|
206
|
+
return await fn(socket)
|
|
207
|
+
} finally {
|
|
208
|
+
socket && socket.destroy()
|
|
209
|
+
connectionTimeout && connectionTimeout.cancel()
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* @template T
|
|
215
|
+
* @param {NodeJS.Socket} socket
|
|
216
|
+
* @param {Buffer|string} buffer
|
|
217
|
+
* @param {function(Buffer):T} ondata
|
|
218
|
+
* @returns Promise<T>
|
|
219
|
+
*/
|
|
220
|
+
async tcpSend (socket, buffer, ondata) {
|
|
221
|
+
let timeout
|
|
222
|
+
try {
|
|
223
|
+
const promise = new Promise((resolve, reject) => {
|
|
224
|
+
let received = Buffer.from([])
|
|
225
|
+
const onData = (data) => {
|
|
226
|
+
received = Buffer.concat([received, data])
|
|
227
|
+
const result = ondata(received)
|
|
228
|
+
if (result !== undefined) {
|
|
229
|
+
socket.removeListener('data', onData)
|
|
230
|
+
resolve(result)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
socket.on('data', onData)
|
|
234
|
+
socket.write(buffer)
|
|
235
|
+
})
|
|
236
|
+
timeout = Promises.createTimeout(this.options.socketTimeout, 'TCP')
|
|
237
|
+
return await Promise.race([promise, timeout, this.abortedPromise])
|
|
238
|
+
} finally {
|
|
239
|
+
timeout && timeout.cancel()
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* @param {Buffer|string} buffer
|
|
245
|
+
* @param {function(Buffer):T=} onPacket
|
|
246
|
+
* @param {(function():T)=} onTimeout
|
|
247
|
+
* @returns Promise<T>
|
|
248
|
+
* @template T
|
|
249
|
+
*/
|
|
250
|
+
async udpSend (buffer, onPacket, onTimeout) {
|
|
251
|
+
const address = this.options.address
|
|
252
|
+
const port = this.options.port
|
|
253
|
+
this.assertValidPort(port)
|
|
254
|
+
|
|
255
|
+
if (typeof buffer === 'string') buffer = Buffer.from(buffer, 'binary')
|
|
256
|
+
|
|
257
|
+
const socket = this.udpSocket
|
|
258
|
+
await socket.send(buffer, address, port, this.options.debug)
|
|
259
|
+
|
|
260
|
+
if (!onPacket && !onTimeout) {
|
|
261
|
+
return null
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let socketCallback
|
|
265
|
+
let timeout
|
|
266
|
+
try {
|
|
267
|
+
const promise = new Promise((resolve, reject) => {
|
|
268
|
+
const start = Date.now()
|
|
269
|
+
let end = null
|
|
270
|
+
socketCallback = (fromAddress, fromPort, buffer) => {
|
|
271
|
+
try {
|
|
272
|
+
if (fromAddress !== address) return
|
|
273
|
+
if (fromPort !== port) return
|
|
274
|
+
if (end === null) {
|
|
275
|
+
end = Date.now()
|
|
276
|
+
const rtt = end - start
|
|
277
|
+
this.registerRtt(rtt)
|
|
278
|
+
}
|
|
279
|
+
const result = onPacket(buffer)
|
|
280
|
+
if (result !== undefined) {
|
|
281
|
+
this.logger.debug('UDP send finished by callback')
|
|
282
|
+
resolve(result)
|
|
283
|
+
}
|
|
284
|
+
} catch (e) {
|
|
285
|
+
reject(e)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
socket.addCallback(socketCallback, this.options.debug)
|
|
289
|
+
})
|
|
290
|
+
timeout = Promises.createTimeout(this.options.socketTimeout, 'UDP')
|
|
291
|
+
const wrappedTimeout = new Promise((resolve, reject) => {
|
|
292
|
+
timeout.catch((e) => {
|
|
293
|
+
this.logger.debug('UDP timeout detected')
|
|
294
|
+
if (onTimeout) {
|
|
295
|
+
try {
|
|
296
|
+
const result = onTimeout()
|
|
297
|
+
if (result !== undefined) {
|
|
298
|
+
this.logger.debug('UDP timeout resolved by callback')
|
|
299
|
+
resolve(result)
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
} catch (e) {
|
|
303
|
+
reject(e)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
reject(e)
|
|
307
|
+
})
|
|
308
|
+
})
|
|
309
|
+
return await Promise.race([promise, wrappedTimeout, this.abortedPromise])
|
|
310
|
+
} finally {
|
|
311
|
+
timeout && timeout.cancel()
|
|
312
|
+
socketCallback && socket.removeCallback(socketCallback)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async tcpPing () {
|
|
317
|
+
// This will give a much more accurate RTT than using the rtt of an http request.
|
|
318
|
+
if (!this.usedTcp) {
|
|
319
|
+
await this.withTcp(() => {})
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async request (params) {
|
|
324
|
+
await this.tcpPing()
|
|
325
|
+
|
|
326
|
+
let requestPromise
|
|
327
|
+
try {
|
|
328
|
+
requestPromise = got({
|
|
329
|
+
...params,
|
|
330
|
+
timeout: {
|
|
331
|
+
request: this.options.socketTimeout
|
|
332
|
+
}
|
|
333
|
+
})
|
|
334
|
+
this.logger.debug(log => {
|
|
335
|
+
log(() => params.url + ' HTTP-->')
|
|
336
|
+
requestPromise
|
|
337
|
+
.then((response) => log(params.url + ' <--HTTP ' + response.statusCode))
|
|
338
|
+
.catch(() => {})
|
|
339
|
+
})
|
|
340
|
+
const wrappedPromise = requestPromise.then(response => {
|
|
341
|
+
if (response.statusCode !== 200) throw new Error('Bad status code: ' + response.statusCode)
|
|
342
|
+
return response.body
|
|
343
|
+
})
|
|
344
|
+
return await Promise.race([wrappedPromise, this.abortedPromise])
|
|
345
|
+
} finally {
|
|
346
|
+
requestPromise && requestPromise.cancel()
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import Core from './core.js'
|
|
2
|
+
|
|
3
|
+
export default class cs2d extends Core {
|
|
4
|
+
async run (state) {
|
|
5
|
+
const reader = await this.sendQuery(
|
|
6
|
+
Buffer.from('\x01\x00\xFB\x01\xF5\x03\xFB\x05', 'binary'),
|
|
7
|
+
Buffer.from('\x01\x00\xFB\x01', 'binary')
|
|
8
|
+
)
|
|
9
|
+
const flags = reader.uint(1)
|
|
10
|
+
state.raw.flags = flags
|
|
11
|
+
state.password = this.readFlag(flags, 0)
|
|
12
|
+
state.raw.registeredOnly = this.readFlag(flags, 1)
|
|
13
|
+
state.raw.fogOfWar = this.readFlag(flags, 2)
|
|
14
|
+
state.raw.friendlyFire = this.readFlag(flags, 3)
|
|
15
|
+
state.raw.botsEnabled = this.readFlag(flags, 5)
|
|
16
|
+
state.raw.luaScripts = this.readFlag(flags, 6)
|
|
17
|
+
state.raw.forceLight = this.readFlag(flags, 7)
|
|
18
|
+
state.name = this.readString(reader)
|
|
19
|
+
state.map = this.readString(reader)
|
|
20
|
+
state.numplayers = reader.uint(1)
|
|
21
|
+
state.maxplayers = reader.uint(1)
|
|
22
|
+
if (flags & 32) {
|
|
23
|
+
state.raw.gamemode = reader.uint(1)
|
|
24
|
+
} else {
|
|
25
|
+
state.raw.gamemode = 0
|
|
26
|
+
}
|
|
27
|
+
state.raw.numbots = reader.uint(1)
|
|
28
|
+
const flags2 = reader.uint(1)
|
|
29
|
+
state.raw.flags2 = flags2
|
|
30
|
+
state.raw.recoil = this.readFlag(flags2, 0)
|
|
31
|
+
state.raw.offScreenDamage = this.readFlag(flags2, 1)
|
|
32
|
+
state.raw.hasDownloads = this.readFlag(flags2, 2)
|
|
33
|
+
reader.skip(2)
|
|
34
|
+
const players = reader.uint(1)
|
|
35
|
+
for (let i = 0; i < players; i++) {
|
|
36
|
+
const player = {}
|
|
37
|
+
player.id = reader.uint(1)
|
|
38
|
+
player.name = this.readString(reader)
|
|
39
|
+
player.team = reader.uint(1)
|
|
40
|
+
player.score = reader.uint(4)
|
|
41
|
+
player.deaths = reader.uint(4)
|
|
42
|
+
state.players.push(player)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async sendQuery (request, expectedHeader) {
|
|
47
|
+
// Send multiple copies of the request packet, because cs2d likes to just ignore them randomly
|
|
48
|
+
await this.udpSend(request)
|
|
49
|
+
await this.udpSend(request)
|
|
50
|
+
return await this.udpSend(request, (buffer) => {
|
|
51
|
+
const reader = this.reader(buffer)
|
|
52
|
+
const header = reader.part(4)
|
|
53
|
+
if (!header.equals(expectedHeader)) return
|
|
54
|
+
return reader
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
readFlag (flags, offset) {
|
|
59
|
+
return !!(flags & (1 << offset))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
readString (reader) {
|
|
63
|
+
return reader.pascalString(1)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import valve from './valve.js'
|
|
2
|
+
import { Buffer } from 'node:buffer'
|
|
3
|
+
|
|
4
|
+
export default class dayz extends valve {
|
|
5
|
+
async run (state) {
|
|
6
|
+
if (!this.options.port) this.options.port = 27016
|
|
7
|
+
await super.queryInfo(state)
|
|
8
|
+
await super.queryChallenge()
|
|
9
|
+
await super.queryPlayers(state)
|
|
10
|
+
await this.queryRules(state)
|
|
11
|
+
|
|
12
|
+
this.processQueryInfo(state)
|
|
13
|
+
await super.cleanup(state)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async queryRules (state) {
|
|
17
|
+
if (!this.options.requestRules) {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const rules = {}
|
|
22
|
+
state.raw.rules = rules
|
|
23
|
+
const dayZPayload = []
|
|
24
|
+
|
|
25
|
+
this.logger.debug('Requesting rules ...')
|
|
26
|
+
|
|
27
|
+
const b = await this.sendPacket(0x56, null, 0x45, true)
|
|
28
|
+
if (b === null && !this.options.requestRulesRequired) return // timed out - the server probably has rules disabled
|
|
29
|
+
|
|
30
|
+
let dayZPayloadEnded = false
|
|
31
|
+
|
|
32
|
+
const reader = this.reader(b)
|
|
33
|
+
const num = reader.uint(2)
|
|
34
|
+
for (let i = 0; i < num; i++) {
|
|
35
|
+
if (!dayZPayloadEnded) {
|
|
36
|
+
const one = reader.uint(1)
|
|
37
|
+
const two = reader.uint(1)
|
|
38
|
+
const three = reader.uint(1)
|
|
39
|
+
if (one !== 0 && two !== 0 && three === 0) {
|
|
40
|
+
while (true) {
|
|
41
|
+
const byte = reader.uint(1)
|
|
42
|
+
if (byte === 0) break
|
|
43
|
+
dayZPayload.push(byte)
|
|
44
|
+
}
|
|
45
|
+
continue
|
|
46
|
+
} else {
|
|
47
|
+
reader.skip(-3)
|
|
48
|
+
dayZPayloadEnded = true
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const key = reader.string()
|
|
53
|
+
rules[key] = reader.string()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
state.raw.dayzMods = this.readDayzMods(Buffer.from(dayZPayload))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
processQueryInfo (state) {
|
|
60
|
+
// DayZ embeds some of the server information inside the tags attribute
|
|
61
|
+
if (!state.raw.tags) { return }
|
|
62
|
+
|
|
63
|
+
state.raw.dlcEnabled = false
|
|
64
|
+
state.raw.firstPerson = false
|
|
65
|
+
state.raw.privateHive = false
|
|
66
|
+
state.raw.external = false
|
|
67
|
+
state.raw.official = false
|
|
68
|
+
|
|
69
|
+
for (const tag of state.raw.tags) {
|
|
70
|
+
if (tag.startsWith('lqs')) {
|
|
71
|
+
const value = parseInt(tag.replace('lqs', ''))
|
|
72
|
+
if (!isNaN(value)) {
|
|
73
|
+
state.raw.queue = value
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (tag.includes('no3rd')) {
|
|
77
|
+
state.raw.firstPerson = true
|
|
78
|
+
}
|
|
79
|
+
if (tag.includes('isDLC')) {
|
|
80
|
+
state.raw.dlcEnabled = true
|
|
81
|
+
}
|
|
82
|
+
if (tag.includes('privHive')) {
|
|
83
|
+
state.raw.privateHive = true;
|
|
84
|
+
}
|
|
85
|
+
if (tag.includes('external')) {
|
|
86
|
+
state.raw.external = true;
|
|
87
|
+
}
|
|
88
|
+
if (tag.includes(':')) {
|
|
89
|
+
state.raw.time = tag
|
|
90
|
+
}
|
|
91
|
+
if (tag.startsWith('etm')) {
|
|
92
|
+
const value = parseInt(tag.replace('etm', ''))
|
|
93
|
+
if (!isNaN(value)) {
|
|
94
|
+
state.raw.dayAcceleration = value
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (tag.startsWith('entm')) {
|
|
98
|
+
const value = parseInt(tag.replace('entm', ''))
|
|
99
|
+
if (!isNaN(value)) {
|
|
100
|
+
state.raw.nightAcceleration = value
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!state.raw.external && !state.raw.privateHive) {
|
|
106
|
+
state.raw.official = true
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
readDayzMods (/** Buffer */ buffer) {
|
|
111
|
+
if (!buffer.length) {
|
|
112
|
+
return {}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.logger.debug('DAYZ BUFFER')
|
|
116
|
+
this.logger.debug(buffer)
|
|
117
|
+
|
|
118
|
+
const reader = this.reader(buffer)
|
|
119
|
+
const version = this.readDayzByte(reader)
|
|
120
|
+
const overflow = this.readDayzByte(reader)
|
|
121
|
+
const dlc1 = this.readDayzByte(reader)
|
|
122
|
+
const dlc2 = this.readDayzByte(reader)
|
|
123
|
+
this.logger.debug('version ' + version)
|
|
124
|
+
this.logger.debug('overflow ' + overflow)
|
|
125
|
+
this.logger.debug('dlc1 ' + dlc1)
|
|
126
|
+
this.logger.debug('dlc2 ' + dlc2)
|
|
127
|
+
if (dlc1) {
|
|
128
|
+
const unknown = this.readDayzUint(reader, 4) // ?
|
|
129
|
+
this.logger.debug('unknown ' + unknown)
|
|
130
|
+
}
|
|
131
|
+
if (dlc2) {
|
|
132
|
+
const unknown = this.readDayzUint(reader, 4) // ?
|
|
133
|
+
this.logger.debug('unknown ' + unknown)
|
|
134
|
+
}
|
|
135
|
+
const mods = []
|
|
136
|
+
mods.push(...this.readDayzModsSection(reader, true))
|
|
137
|
+
mods.push(...this.readDayzModsSection(reader, false))
|
|
138
|
+
this.logger.debug('dayz buffer rest:', reader.rest())
|
|
139
|
+
return mods
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
readDayzModsSection (/** Reader */ reader, withHeader) {
|
|
143
|
+
const out = []
|
|
144
|
+
const count = this.readDayzByte(reader)
|
|
145
|
+
this.logger.debug('dayz mod section withHeader:' + withHeader + ' count:' + count)
|
|
146
|
+
for (let i = 0; i < count; i++) {
|
|
147
|
+
if (reader.done()) break
|
|
148
|
+
const mod = {}
|
|
149
|
+
if (withHeader) {
|
|
150
|
+
mod.unknown = this.readDayzUint(reader, 4) // ?
|
|
151
|
+
|
|
152
|
+
// For some reason this is 4 on all of them, but doesn't exist on the last one? but only sometimes?
|
|
153
|
+
const offset = reader.offset()
|
|
154
|
+
const flag = this.readDayzByte(reader)
|
|
155
|
+
if (flag !== 4) reader.setOffset(offset)
|
|
156
|
+
|
|
157
|
+
mod.workshopId = this.readDayzUint(reader, 4)
|
|
158
|
+
}
|
|
159
|
+
mod.title = this.readDayzString(reader)
|
|
160
|
+
this.logger.debug(mod)
|
|
161
|
+
out.push(mod)
|
|
162
|
+
}
|
|
163
|
+
return out
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
readDayzUint (reader, bytes) {
|
|
167
|
+
const out = []
|
|
168
|
+
for (let i = 0; i < bytes; i++) {
|
|
169
|
+
out.push(this.readDayzByte(reader))
|
|
170
|
+
}
|
|
171
|
+
const buf = Buffer.from(out)
|
|
172
|
+
const r2 = this.reader(buf)
|
|
173
|
+
return r2.uint(bytes)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
readDayzByte (reader) {
|
|
177
|
+
const byte = reader.uint(1)
|
|
178
|
+
if (byte === 1) {
|
|
179
|
+
const byte2 = reader.uint(1)
|
|
180
|
+
if (byte2 === 1) return 1
|
|
181
|
+
if (byte2 === 2) return 0
|
|
182
|
+
if (byte2 === 3) return 0xff
|
|
183
|
+
return 0 // ?
|
|
184
|
+
}
|
|
185
|
+
return byte
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
readDayzString (reader) {
|
|
189
|
+
const length = this.readDayzByte(reader)
|
|
190
|
+
const out = []
|
|
191
|
+
for (let i = 0; i < length; i++) {
|
|
192
|
+
out.push(this.readDayzByte(reader))
|
|
193
|
+
}
|
|
194
|
+
return Buffer.from(out).toString('utf8')
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import Core from './core.js'
|
|
2
|
+
|
|
3
|
+
export default class discord extends Core {
|
|
4
|
+
async run (state) {
|
|
5
|
+
const guildId = this.options.guildId
|
|
6
|
+
if (typeof guildId !== 'string') {
|
|
7
|
+
throw new Error('guildId option must be set when querying discord. Ensure the guildId is a string and not a number.' +
|
|
8
|
+
" (It's too large of a number for javascript to store without losing precision)")
|
|
9
|
+
}
|
|
10
|
+
this.usedTcp = true
|
|
11
|
+
const raw = await this.request({
|
|
12
|
+
url: 'https://discordapp.com/api/guilds/' + guildId + '/widget.json'
|
|
13
|
+
})
|
|
14
|
+
const json = JSON.parse(raw)
|
|
15
|
+
state.name = json.name
|
|
16
|
+
if (json.instant_invite) {
|
|
17
|
+
state.connect = json.instant_invite
|
|
18
|
+
} else {
|
|
19
|
+
state.connect = 'https://discordapp.com/channels/' + guildId
|
|
20
|
+
}
|
|
21
|
+
for (const member of json.members) {
|
|
22
|
+
const { username: name, ...rest } = member
|
|
23
|
+
state.players.push({ name, ...rest })
|
|
24
|
+
}
|
|
25
|
+
delete json.members
|
|
26
|
+
state.maxplayers = 500000
|
|
27
|
+
state.raw = json
|
|
28
|
+
}
|
|
29
|
+
}
|