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.
Files changed (73) hide show
  1. package/GAMES_LIST.md +453 -0
  2. package/LICENSE +21 -0
  3. package/README.md +144 -0
  4. package/bin/gamedig.js +79 -0
  5. package/lib/DnsResolver.js +76 -0
  6. package/lib/GlobalUdpSocket.js +69 -0
  7. package/lib/HexUtil.js +20 -0
  8. package/lib/Logger.js +45 -0
  9. package/lib/Promises.js +18 -0
  10. package/lib/ProtocolResolver.js +7 -0
  11. package/lib/QueryRunner.js +95 -0
  12. package/lib/Results.js +32 -0
  13. package/lib/game-resolver.js +17 -0
  14. package/lib/gamedig.js +23 -0
  15. package/lib/games.js +2747 -0
  16. package/lib/index.js +5 -0
  17. package/lib/reader.js +172 -0
  18. package/package.json +74 -0
  19. package/protocols/armagetron.js +65 -0
  20. package/protocols/asa.js +12 -0
  21. package/protocols/ase.js +45 -0
  22. package/protocols/assettocorsa.js +40 -0
  23. package/protocols/battlefield.js +162 -0
  24. package/protocols/beammp.js +32 -0
  25. package/protocols/beammpmaster.js +17 -0
  26. package/protocols/buildandshoot.js +55 -0
  27. package/protocols/core.js +349 -0
  28. package/protocols/cs2d.js +65 -0
  29. package/protocols/dayz.js +196 -0
  30. package/protocols/discord.js +29 -0
  31. package/protocols/doom3.js +148 -0
  32. package/protocols/eco.js +20 -0
  33. package/protocols/eldewrito.js +21 -0
  34. package/protocols/epic.js +95 -0
  35. package/protocols/ffow.js +38 -0
  36. package/protocols/fivem.js +33 -0
  37. package/protocols/gamespy1.js +181 -0
  38. package/protocols/gamespy2.js +144 -0
  39. package/protocols/gamespy3.js +197 -0
  40. package/protocols/geneshift.js +46 -0
  41. package/protocols/goldsrc.js +8 -0
  42. package/protocols/hexen2.js +14 -0
  43. package/protocols/index.js +61 -0
  44. package/protocols/jc2mp.js +16 -0
  45. package/protocols/kspdmp.js +28 -0
  46. package/protocols/mafia2mp.js +41 -0
  47. package/protocols/mafia2online.js +9 -0
  48. package/protocols/minecraft.js +102 -0
  49. package/protocols/minecraftbedrock.js +72 -0
  50. package/protocols/minecraftvanilla.js +87 -0
  51. package/protocols/mumble.js +39 -0
  52. package/protocols/mumbleping.js +24 -0
  53. package/protocols/nadeo.js +86 -0
  54. package/protocols/openttd.js +127 -0
  55. package/protocols/quake1.js +9 -0
  56. package/protocols/quake2.js +88 -0
  57. package/protocols/quake3.js +24 -0
  58. package/protocols/rfactor.js +69 -0
  59. package/protocols/samp.js +102 -0
  60. package/protocols/savage2.js +25 -0
  61. package/protocols/starmade.js +67 -0
  62. package/protocols/starsiege.js +10 -0
  63. package/protocols/teamspeak2.js +71 -0
  64. package/protocols/teamspeak3.js +69 -0
  65. package/protocols/terraria.js +24 -0
  66. package/protocols/tribes1.js +153 -0
  67. package/protocols/tribes1master.js +80 -0
  68. package/protocols/unreal2.js +150 -0
  69. package/protocols/ut3.js +45 -0
  70. package/protocols/valve.js +455 -0
  71. package/protocols/vcmp.js +10 -0
  72. package/protocols/ventrilo.js +237 -0
  73. package/protocols/warsow.js +13 -0
@@ -0,0 +1,197 @@
1
+ import Core from './core.js'
2
+
3
+ export default class gamespy3 extends Core {
4
+ constructor () {
5
+ super()
6
+ this.sessionId = 1
7
+ this.encoding = 'latin1'
8
+ this.byteorder = 'be'
9
+ this.useOnlySingleSplit = false
10
+ this.isJc2mp = false
11
+ }
12
+
13
+ async run (state) {
14
+ const buffer = await this.sendPacket(9, false, false, false)
15
+ const reader = this.reader(buffer)
16
+ let challenge = parseInt(reader.string())
17
+ this.logger.debug('Received challenge key: ' + challenge)
18
+ if (challenge === 0) {
19
+ // Some servers send us a 0 if they don't want a challenge key used
20
+ // BF2 does this.
21
+ challenge = null
22
+ }
23
+
24
+ let requestPayload
25
+ if (this.isJc2mp) {
26
+ // they completely alter the protocol. because why not.
27
+ requestPayload = Buffer.from([0xff, 0xff, 0xff, 0x02])
28
+ } else {
29
+ requestPayload = Buffer.from([0xff, 0xff, 0xff, 0x01])
30
+ }
31
+ /** @type Buffer[] */
32
+ const packets = await this.sendPacket(0, challenge, requestPayload, true)
33
+
34
+ // iterate over the received packets
35
+ // the first packet will start off with k/v pairs, followed with data fields
36
+ // the following packets will only have data fields
37
+ state.raw.playerTeamInfo = {}
38
+
39
+ for (let iPacket = 0; iPacket < packets.length; iPacket++) {
40
+ const packet = packets[iPacket]
41
+ const reader = this.reader(packet)
42
+
43
+ this.logger.debug('Parsing packet #' + iPacket)
44
+ this.logger.debug(packet)
45
+
46
+ // Parse raw server key/values
47
+
48
+ if (iPacket === 0) {
49
+ while (!reader.done()) {
50
+ const key = reader.string()
51
+ if (!key) break
52
+
53
+ let value = reader.string()
54
+ while (value.match(/^p[0-9]+$/)) {
55
+ // fix a weird ut3 bug where some keys don't have values
56
+ value = reader.string()
57
+ }
58
+
59
+ state.raw[key] = value
60
+ this.logger.debug(key + ' = ' + value)
61
+ }
62
+ }
63
+
64
+ // Parse player, team, item array state
65
+
66
+ if (this.isJc2mp) {
67
+ state.raw.numPlayers2 = reader.uint(2)
68
+ while (!reader.done()) {
69
+ const player = {}
70
+ player.name = reader.string()
71
+ player.steamid = reader.string()
72
+ player.ping = reader.uint(2)
73
+ state.players.push(player)
74
+ }
75
+ } else {
76
+ while (!reader.done()) {
77
+ if (reader.uint(1) <= 2) continue
78
+ reader.skip(-1)
79
+ const fieldId = reader.string()
80
+ if (!fieldId) continue
81
+ const fieldIdSplit = fieldId.split('_')
82
+ const fieldName = fieldIdSplit[0]
83
+ const itemType = fieldIdSplit.length > 1 ? fieldIdSplit[1] : 'no_'
84
+
85
+ if (!(itemType in state.raw.playerTeamInfo)) {
86
+ state.raw.playerTeamInfo[itemType] = []
87
+ }
88
+ const items = state.raw.playerTeamInfo[itemType]
89
+
90
+ let offset = reader.uint(1)
91
+
92
+ this.logger.debug(() => 'Parsing new field: itemType=' + itemType + ' fieldName=' + fieldName + ' startOffset=' + offset)
93
+
94
+ while (!reader.done()) {
95
+ const item = reader.string()
96
+ if (!item) break
97
+
98
+ while (items.length <= offset) { items.push({}) }
99
+ items[offset][fieldName] = item
100
+ this.logger.debug('* ' + item)
101
+ offset++
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ // Turn all that raw state into something useful
108
+
109
+ if ('hostname' in state.raw) state.name = state.raw.hostname
110
+ else if ('servername' in state.raw) state.name = state.raw.servername
111
+ if ('mapname' in state.raw) state.map = state.raw.mapname
112
+ if (state.raw.password === '1') state.password = true
113
+ if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers)
114
+ if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport)
115
+
116
+ if ('' in state.raw.playerTeamInfo) {
117
+ for (const playerInfo of state.raw.playerTeamInfo['']) {
118
+ const player = {}
119
+ for (const from of Object.keys(playerInfo)) {
120
+ let key = from
121
+ let value = playerInfo[from]
122
+
123
+ if (key === 'player') key = 'name'
124
+ if (key === 'score' || key === 'ping' || key === 'team' || key === 'deaths' || key === 'pid') value = parseInt(value)
125
+ player[key] = value
126
+ }
127
+ state.players.push(player)
128
+ }
129
+ }
130
+
131
+ if ('numplayers' in state.raw) state.numplayers = parseInt(state.raw.numplayers)
132
+ else state.numplayers = state.players.length
133
+ }
134
+
135
+ async sendPacket (type, challenge, payload, assemble) {
136
+ const challengeLength = challenge === null ? 0 : 4
137
+ const payloadLength = payload ? payload.length : 0
138
+
139
+ const b = Buffer.alloc(7 + challengeLength + payloadLength)
140
+ b.writeUInt8(0xFE, 0)
141
+ b.writeUInt8(0xFD, 1)
142
+ b.writeUInt8(type, 2)
143
+ b.writeUInt32BE(this.sessionId, 3)
144
+ if (challengeLength) b.writeInt32BE(challenge, 7)
145
+ if (payloadLength) payload.copy(b, 7 + challengeLength)
146
+
147
+ let numPackets = 0
148
+ const packets = {}
149
+ return await this.udpSend(b, (buffer) => {
150
+ const reader = this.reader(buffer)
151
+ const iType = reader.uint(1)
152
+ if (iType !== type) {
153
+ this.logger.debug('Skipping packet, type mismatch')
154
+ return
155
+ }
156
+ const iSessionId = reader.uint(4)
157
+ if (iSessionId !== this.sessionId) {
158
+ this.logger.debug('Skipping packet, session id mismatch')
159
+ return
160
+ }
161
+
162
+ if (!assemble) {
163
+ return reader.rest()
164
+ }
165
+ if (this.useOnlySingleSplit) {
166
+ // has split headers, but they are worthless and only one packet is used
167
+ reader.skip(11)
168
+ return [reader.rest()]
169
+ }
170
+
171
+ reader.skip(9) // filler data -- usually set to 'splitnum\0'
172
+ let id = reader.uint(1)
173
+ const last = (id & 0x80)
174
+ id = id & 0x7f
175
+ if (last) numPackets = id + 1
176
+
177
+ reader.skip(1) // "another 'packet number' byte, but isn't understood."
178
+
179
+ packets[id] = reader.rest()
180
+ if (this.debug) {
181
+ this.logger.debug('Received packet #' + id + (last ? ' (last)' : ''))
182
+ }
183
+
184
+ if (!numPackets || Object.keys(packets).length !== numPackets) return
185
+
186
+ // assemble the parts
187
+ const list = []
188
+ for (let i = 0; i < numPackets; i++) {
189
+ if (!(i in packets)) {
190
+ throw new Error('Missing packet #' + i)
191
+ }
192
+ list.push(packets[i])
193
+ }
194
+ return list
195
+ })
196
+ }
197
+ }
@@ -0,0 +1,46 @@
1
+ import Core from './core.js'
2
+
3
+ export default class geneshift extends Core {
4
+ async run (state) {
5
+ await this.tcpPing()
6
+
7
+ const body = await this.request({
8
+ url: 'http://geneshift.net/game/receiveLobby.php'
9
+ })
10
+
11
+ const split = body.split('<br/>')
12
+ let found = null
13
+ for (const line of split) {
14
+ const fields = line.split('::')
15
+ const ip = fields[2]
16
+ const port = fields[3]
17
+ if (ip === this.options.address && parseInt(port) === this.options.port) {
18
+ found = fields
19
+ break
20
+ }
21
+ }
22
+
23
+ if (found === null) {
24
+ throw new Error('Server not found in list')
25
+ }
26
+
27
+ state.raw.countrycode = found[0]
28
+ state.raw.country = found[1]
29
+ state.name = found[4]
30
+ state.map = found[5]
31
+ state.numplayers = parseInt(found[6])
32
+ state.maxplayers = parseInt(found[7])
33
+ // fields[8] is unknown?
34
+ state.raw.rules = found[9]
35
+ state.raw.gamemode = parseInt(found[10])
36
+ state.raw.gangsters = parseInt(found[11])
37
+ state.raw.cashrate = parseInt(found[12])
38
+ state.raw.missions = !!parseInt(found[13])
39
+ state.raw.vehicles = !!parseInt(found[14])
40
+ state.raw.customweapons = !!parseInt(found[15])
41
+ state.raw.friendlyfire = !!parseInt(found[16])
42
+ state.raw.mercs = !!parseInt(found[17])
43
+ // fields[18] is unknown? listen server?
44
+ state.raw.version = found[19]
45
+ }
46
+ }
@@ -0,0 +1,8 @@
1
+ import valve from './valve.js'
2
+
3
+ export default class goldsrc extends valve {
4
+ constructor () {
5
+ super()
6
+ this.goldsrcInfo = true
7
+ }
8
+ }
@@ -0,0 +1,14 @@
1
+ import quake1 from './quake1.js'
2
+
3
+ export default class hexen2 extends quake1 {
4
+ constructor () {
5
+ super()
6
+ this.sendHeader = '\xFFstatus\x0a'
7
+ this.responseHeader = '\xffn'
8
+ }
9
+
10
+ async run (state) {
11
+ await super.run(state)
12
+ state.gamePort = this.options.port - 50
13
+ }
14
+ }
@@ -0,0 +1,61 @@
1
+ import armagetron from './armagetron.js'
2
+ import ase from './ase.js'
3
+ import asa from './asa.js'
4
+ import assettocorsa from './assettocorsa.js'
5
+ import battlefield from './battlefield.js'
6
+ import buildandshoot from './buildandshoot.js'
7
+ import cs2d from './cs2d.js'
8
+ import discord from './discord.js'
9
+ import doom3 from './doom3.js'
10
+ import eco from './eco.js'
11
+ import eldewrito from './eldewrito.js'
12
+ import epic from './epic.js'
13
+ import ffow from './ffow.js'
14
+ import fivem from './fivem.js'
15
+ import gamespy1 from './gamespy1.js'
16
+ import gamespy2 from './gamespy2.js'
17
+ import gamespy3 from './gamespy3.js'
18
+ import geneshift from './geneshift.js'
19
+ import goldsrc from './goldsrc.js'
20
+ import hexen2 from './hexen2.js'
21
+ import jc2mp from './jc2mp.js'
22
+ import kspdmp from './kspdmp.js'
23
+ import mafia2mp from './mafia2mp.js'
24
+ import mafia2online from './mafia2online.js'
25
+ import minecraft from './minecraft.js'
26
+ import minecraftbedrock from './minecraftbedrock.js'
27
+ import minecraftvanilla from './minecraftvanilla.js'
28
+ import mumble from './mumble.js'
29
+ import mumbleping from './mumbleping.js'
30
+ import nadeo from './nadeo.js'
31
+ import openttd from './openttd.js'
32
+ import quake1 from './quake1.js'
33
+ import quake2 from './quake2.js'
34
+ import quake3 from './quake3.js'
35
+ import rfactor from './rfactor.js'
36
+ import samp from './samp.js'
37
+ import savage2 from './savage2.js'
38
+ import starmade from './starmade.js'
39
+ import starsiege from './starsiege.js'
40
+ import teamspeak2 from './teamspeak2.js'
41
+ import teamspeak3 from './teamspeak3.js'
42
+ import terraria from './terraria.js'
43
+ import tribes1 from './tribes1.js'
44
+ import tribes1master from './tribes1master.js'
45
+ import unreal2 from './unreal2.js'
46
+ import ut3 from './ut3.js'
47
+ import valve from './valve.js'
48
+ import vcmp from './vcmp.js'
49
+ import ventrilo from './ventrilo.js'
50
+ import warsow from './warsow.js'
51
+ import beammpmaster from './beammpmaster.js'
52
+ import beammp from './beammp.js'
53
+ import dayz from './dayz.js'
54
+
55
+ export {
56
+ armagetron, ase, asa, assettocorsa, battlefield, buildandshoot, cs2d, discord, doom3, eco, epic, ffow, fivem, gamespy1,
57
+ gamespy2, gamespy3, geneshift, goldsrc, hexen2, jc2mp, kspdmp, mafia2mp, mafia2online, minecraft,
58
+ minecraftbedrock, minecraftvanilla, mumble, mumbleping, nadeo, openttd, quake1, quake2, quake3, rfactor, samp,
59
+ savage2, starmade, starsiege, teamspeak2, teamspeak3, terraria, tribes1, tribes1master, unreal2, ut3, valve,
60
+ vcmp, ventrilo, warsow, eldewrito, beammpmaster, beammp, dayz
61
+ }
@@ -0,0 +1,16 @@
1
+ import gamespy3 from './gamespy3.js'
2
+
3
+ // supposedly, gamespy3 is the "official" query protocol for jcmp,
4
+ // but it's broken (requires useOnlySingleSplit), and may not include some player names
5
+ export default class jc2mp extends gamespy3 {
6
+ constructor () {
7
+ super()
8
+ this.useOnlySingleSplit = true
9
+ this.isJc2mp = true
10
+ this.encoding = 'utf8'
11
+ }
12
+
13
+ async run (state) {
14
+ await super.run(state)
15
+ }
16
+ }
@@ -0,0 +1,28 @@
1
+ import Core from './core.js'
2
+
3
+ export default class kspdmp extends Core {
4
+ async run (state) {
5
+ const json = await this.request({
6
+ url: 'http://' + this.options.address + ':' + this.options.port,
7
+ responseType: 'json'
8
+ })
9
+
10
+ for (const one of json.players) {
11
+ state.players.push({ name: one.nickname, team: one.team })
12
+ }
13
+
14
+ for (const key of Object.keys(json)) {
15
+ state.raw[key] = json[key]
16
+ }
17
+ state.name = json.server_name
18
+ state.maxplayers = json.max_players
19
+ state.gamePort = json.port
20
+ if (json.players) {
21
+ const split = json.players.split(', ')
22
+ for (const name of split) {
23
+ state.players.push({ name })
24
+ }
25
+ }
26
+ state.numplayers = state.players.length
27
+ }
28
+ }
@@ -0,0 +1,41 @@
1
+ import Core from './core.js'
2
+
3
+ export default class mafia2mp extends Core {
4
+ constructor () {
5
+ super()
6
+ this.encoding = 'latin1'
7
+ this.header = 'M2MP'
8
+ this.isMafia2Online = false
9
+ }
10
+
11
+ async run (state) {
12
+ const body = await this.udpSend(this.header, (buffer) => {
13
+ const reader = this.reader(buffer)
14
+ const header = reader.string(this.header.length)
15
+ if (header !== this.header) return
16
+ return reader.rest()
17
+ })
18
+
19
+ const reader = this.reader(body)
20
+ state.name = this.readString(reader)
21
+ state.numplayers = parseInt(this.readString(reader))
22
+ state.maxplayers = parseInt(this.readString(reader))
23
+ state.raw.gamemode = this.readString(reader)
24
+ state.password = !!reader.uint(1)
25
+ state.gamePort = this.options.port - 1
26
+
27
+ while (!reader.done()) {
28
+ const player = {}
29
+ player.name = this.readString(reader)
30
+ if (!player.name) break
31
+ if (this.isMafia2Online) {
32
+ player.ping = parseInt(this.readString(reader))
33
+ }
34
+ state.players.push(player)
35
+ }
36
+ }
37
+
38
+ readString (reader) {
39
+ return reader.pascalString(1, -1)
40
+ }
41
+ }
@@ -0,0 +1,9 @@
1
+ import mafia2mp from './mafia2mp.js'
2
+
3
+ export default class mafia2online extends mafia2mp {
4
+ constructor () {
5
+ super()
6
+ this.header = 'M2Online'
7
+ this.isMafia2Online = true
8
+ }
9
+ }
@@ -0,0 +1,102 @@
1
+ import Core from './core.js'
2
+ import minecraftbedrock from './minecraftbedrock.js'
3
+ import minecraftvanilla from './minecraftvanilla.js'
4
+ import Gamespy3 from './gamespy3.js'
5
+
6
+ /*
7
+ Vanilla servers respond to minecraftvanilla only
8
+ Some modded vanilla servers respond to minecraftvanilla and gamespy3, or gamespy3 only
9
+ Some bedrock servers respond to gamespy3 only
10
+ Some bedrock servers respond to minecraftbedrock only
11
+ Unsure if any bedrock servers respond to gamespy3 and minecraftbedrock
12
+ */
13
+
14
+ export default class minecraft extends Core {
15
+ constructor () {
16
+ super()
17
+ this.srvRecord = '_minecraft._tcp'
18
+ }
19
+
20
+ async run (state) {
21
+ /** @type {Promise<Results>[]} */
22
+ const promises = []
23
+
24
+ const vanillaResolver = new minecraftvanilla()
25
+ vanillaResolver.options = this.options
26
+ vanillaResolver.udpSocket = this.udpSocket
27
+ promises.push((async () => {
28
+ try { return await vanillaResolver.runOnceSafe() } catch (e) {}
29
+ })())
30
+
31
+ const gamespyResolver = new Gamespy3()
32
+ gamespyResolver.options = {
33
+ ...this.options,
34
+ encoding: 'utf8'
35
+ }
36
+ gamespyResolver.udpSocket = this.udpSocket
37
+ promises.push((async () => {
38
+ try { return await gamespyResolver.runOnceSafe() } catch (e) {}
39
+ })())
40
+
41
+ const bedrockResolver = new minecraftbedrock()
42
+ bedrockResolver.options = this.options
43
+ bedrockResolver.udpSocket = this.udpSocket
44
+ promises.push((async () => {
45
+ try { return await bedrockResolver.runOnceSafe() } catch (e) {}
46
+ })())
47
+
48
+ const [vanillaState, gamespyState, bedrockState] = await Promise.all(promises)
49
+
50
+ state.raw.vanilla = vanillaState
51
+ state.raw.gamespy = gamespyState
52
+ state.raw.bedrock = bedrockState
53
+
54
+ if (!vanillaState && !gamespyState && !bedrockState) {
55
+ throw new Error('No protocols succeeded')
56
+ }
57
+
58
+ // Ordered from least worth to most worth (player names / etc)
59
+ if (bedrockState) {
60
+ if (bedrockState.players.length) state.players = bedrockState.players
61
+ }
62
+ if (vanillaState) {
63
+ try {
64
+ let name = ''
65
+ const description = vanillaState.raw.description
66
+ if (typeof description === 'string') {
67
+ name = description
68
+ }
69
+ if (!name && typeof description === 'object' && description.text) {
70
+ name = description.text
71
+ }
72
+ if (!name && typeof description === 'object' && description.extra) {
73
+ name = description.extra.map(part => part.text).join('')
74
+ }
75
+ state.name = name
76
+ } catch (e) {}
77
+ if (vanillaState.numplayers) state.numplayers = vanillaState.numplayers
78
+ if (vanillaState.maxplayers) state.maxplayers = vanillaState.maxplayers
79
+ if (vanillaState.players.length) state.players = vanillaState.players
80
+ if (vanillaState.ping) this.registerRtt(vanillaState.ping)
81
+ }
82
+ if (gamespyState) {
83
+ if (gamespyState.name) state.name = gamespyState.name
84
+ if (gamespyState.numplayers) state.numplayers = gamespyState.numplayers
85
+ if (gamespyState.maxplayers) state.maxplayers = gamespyState.maxplayers
86
+ if (gamespyState.players.length) state.players = gamespyState.players
87
+ else if (gamespyState.numplayers) state.numplayers = gamespyState.numplayers
88
+ if (gamespyState.ping) this.registerRtt(gamespyState.ping)
89
+ }
90
+ if (bedrockState) {
91
+ if (bedrockState.name) state.name = bedrockState.name
92
+ if (bedrockState.numplayers) state.numplayers = bedrockState.numplayers
93
+ if (bedrockState.maxplayers) state.maxplayers = bedrockState.maxplayers
94
+ if (bedrockState.map) state.map = bedrockState.map
95
+ if (bedrockState.ping) this.registerRtt(bedrockState.ping)
96
+ }
97
+ // remove dupe spaces from name
98
+ state.name = state.name.replace(/\s+/g, ' ')
99
+ // remove color codes from name
100
+ state.name = state.name.replace(/\u00A7./g, '')
101
+ }
102
+ }
@@ -0,0 +1,72 @@
1
+ import Core from './core.js'
2
+
3
+ export default class minecraftbedrock extends Core {
4
+ constructor () {
5
+ super()
6
+ this.byteorder = 'be'
7
+ }
8
+
9
+ async run (state) {
10
+ const bufs = [
11
+ Buffer.from([0x01]), // Message ID, ID_UNCONNECTED_PING
12
+ Buffer.from('1122334455667788', 'hex'), // Nonce / timestamp
13
+ Buffer.from('00ffff00fefefefefdfdfdfd12345678', 'hex'), // Magic
14
+ Buffer.from('0000000000000000', 'hex') // Cliend GUID
15
+ ]
16
+
17
+ return await this.udpSend(Buffer.concat(bufs), buffer => {
18
+ const reader = this.reader(buffer)
19
+
20
+ const messageId = reader.uint(1)
21
+ if (messageId !== 0x1c) {
22
+ this.logger.debug('Skipping packet, invalid message id')
23
+ return
24
+ }
25
+
26
+ const nonce = reader.part(8).toString('hex') // should match the nonce we sent
27
+ this.logger.debug('Nonce: ' + nonce)
28
+ if (nonce !== '1122334455667788') {
29
+ this.logger.debug('Skipping packet, invalid nonce')
30
+ return
31
+ }
32
+
33
+ // These 8 bytes are identical to the serverId string we receive in decimal below
34
+ reader.skip(8)
35
+
36
+ const magic = reader.part(16).toString('hex')
37
+ this.logger.debug('Magic value: ' + magic)
38
+ if (magic !== '00ffff00fefefefefdfdfdfd12345678') {
39
+ this.logger.debug('Skipping packet, invalid magic')
40
+ return
41
+ }
42
+
43
+ const statusLen = reader.uint(2)
44
+ if (reader.remaining() !== statusLen) {
45
+ throw new Error('Invalid status length: ' + reader.remaining() + ' vs ' + statusLen)
46
+ }
47
+
48
+ const statusStr = reader.rest().toString('utf8')
49
+ this.logger.debug('Raw status str: ' + statusStr)
50
+
51
+ const split = statusStr.split(';')
52
+ if (split.length < 6) {
53
+ throw new Error('Missing enough chunks in status str')
54
+ }
55
+
56
+ state.raw.edition = split.shift()
57
+ state.name = split.shift()
58
+ state.raw.protocolVersion = split.shift()
59
+ state.raw.mcVersion = split.shift()
60
+ state.numplayers = parseInt(split.shift())
61
+ state.maxplayers = parseInt(split.shift())
62
+ if (split.length) state.raw.serverId = split.shift()
63
+ if (split.length) state.map = split.shift()
64
+ if (split.length) state.raw.gameMode = split.shift()
65
+ if (split.length) state.raw.nintendoOnly = !!parseInt(split.shift())
66
+ if (split.length) state.raw.ipv4Port = split.shift()
67
+ if (split.length) state.raw.ipv6Port = split.shift()
68
+
69
+ return true
70
+ })
71
+ }
72
+ }
@@ -0,0 +1,87 @@
1
+ import Core from './core.js'
2
+ import Varint from 'varint'
3
+
4
+ export default class minecraftvanilla extends Core {
5
+ async run (state) {
6
+ const portBuf = Buffer.alloc(2)
7
+ portBuf.writeUInt16BE(this.options.port, 0)
8
+
9
+ const addressBuf = Buffer.from(this.options.host, 'utf8')
10
+
11
+ const bufs = [
12
+ this.varIntBuffer(47),
13
+ this.varIntBuffer(addressBuf.length),
14
+ addressBuf,
15
+ portBuf,
16
+ this.varIntBuffer(1)
17
+ ]
18
+
19
+ const outBuffer = Buffer.concat([
20
+ this.buildPacket(0, Buffer.concat(bufs)),
21
+ this.buildPacket(0)
22
+ ])
23
+
24
+ const data = await this.withTcp(async socket => {
25
+ return await this.tcpSend(socket, outBuffer, data => {
26
+ if (data.length < 10) return
27
+ const reader = this.reader(data)
28
+ const length = reader.varint()
29
+ if (data.length < length) return
30
+ return reader.rest()
31
+ })
32
+ })
33
+
34
+ const reader = this.reader(data)
35
+
36
+ const packetId = reader.varint()
37
+ this.logger.debug('Packet ID: ' + packetId)
38
+
39
+ const strLen = reader.varint()
40
+ this.logger.debug('String Length: ' + strLen)
41
+
42
+ const rest = reader.rest()
43
+
44
+ const str = rest.toString('utf8', 0, strLen)
45
+ this.logger.debug(str)
46
+
47
+ const json = JSON.parse(str.substring(0, strLen))
48
+ delete json.favicon
49
+
50
+ state.raw = json
51
+ state.maxplayers = json.players.max
52
+ state.numplayers = json.players.online
53
+
54
+ if (json.players.sample) {
55
+ for (const player of json.players.sample) {
56
+ state.players.push({
57
+ id: player.id,
58
+ name: player.name
59
+ })
60
+ }
61
+ }
62
+
63
+ // Better Compatibility Checker mod support
64
+ let bccJson = {}
65
+
66
+ if (rest.length > strLen) {
67
+ const bccStr = rest.toString('utf8', strLen + 1)
68
+ bccJson = JSON.parse(bccStr)
69
+ }
70
+
71
+ state.raw.bcc = bccJson
72
+ }
73
+
74
+ varIntBuffer (num) {
75
+ return Buffer.from(Varint.encode(num))
76
+ }
77
+
78
+ buildPacket (id, data) {
79
+ if (!data) data = Buffer.from([])
80
+ const idBuffer = this.varIntBuffer(id)
81
+ return Buffer.concat([
82
+ this.varIntBuffer(data.length + idBuffer.length),
83
+ idBuffer,
84
+ data
85
+ ])
86
+ }
87
+ }