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,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,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,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
|
+
}
|