forked from gamedig/node-gamedig
-
Notifications
You must be signed in to change notification settings - Fork 0
/
doom3.js
149 lines (136 loc) · 5.27 KB
/
doom3.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import Core from './core.js'
export default class doom3 extends Core {
constructor () {
super()
this.encoding = 'latin1'
}
async run (state) {
const body = await this.udpSend('\xff\xffgetInfo\x00PiNGPoNg\x00', packet => {
const reader = this.reader(packet)
const header = reader.uint(2)
if (header !== 0xffff) return
const header2 = reader.string()
if (header2 !== 'infoResponse') return
const challengePart1 = reader.string(4)
if (challengePart1 !== 'PiNG') return
// some doom3 implementations only return the first 4 bytes of the challenge
const challengePart2 = reader.string(4)
if (challengePart2 !== 'PoNg') reader.skip(-4)
return reader.rest()
})
let reader = this.reader(body)
const protoVersion = reader.uint(4)
state.raw.protocolVersion = (protoVersion >> 16) + '.' + (protoVersion & 0xffff)
state.version = state.raw.protocolVersion
// some doom implementations send us a packet size here, some don't (etqw does this)
// we can tell if this is a packet size, because the third and fourth byte will be 0 (no packets are that massive)
reader.skip(2)
const packetContainsSize = (reader.uint(2) === 0)
reader.skip(-4)
if (packetContainsSize) {
const size = reader.uint(4)
this.logger.debug('Received packet size: ' + size)
}
while (!reader.done()) {
const key = reader.string()
let value = this.stripColors(reader.string())
if (key === 'si_map') {
value = value.replace('maps/', '')
value = value.replace('.entities', '')
}
if (!key) break
state.raw[key] = value
this.logger.debug(key + '=' + value)
}
const isEtqw = state.raw.gamename && state.raw.gamename.toLowerCase().includes('etqw')
const rest = reader.rest()
let playerResult = this.attemptPlayerParse(rest, isEtqw, false, false, false)
if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, false, false)
if (!playerResult) playerResult = this.attemptPlayerParse(rest, isEtqw, true, true, true)
if (!playerResult) {
throw new Error('Unable to find a suitable parse strategy for player list')
}
let players;
[players, reader] = playerResult
state.numplayers = players.length
for (const player of players) {
if (!player.ping || player.typeflag) { state.bots.push(player) } else { state.players.push(player) }
}
state.raw.osmask = reader.uint(4)
if (isEtqw) {
state.raw.ranked = reader.uint(1)
state.raw.timeleft = reader.uint(4)
state.raw.gamestate = reader.uint(1)
state.raw.servertype = reader.uint(1)
// 0 = regular, 1 = tv
if (state.raw.servertype === 0) {
state.raw.interestedClients = reader.uint(1)
} else if (state.raw.servertype === 1) {
state.raw.connectedClients = reader.uint(4)
state.raw.maxClients = reader.uint(4)
}
}
if (state.raw.si_name) state.name = state.raw.si_name
if (state.raw.si_map) state.map = state.raw.si_map
if (state.raw.si_maxplayers) state.maxplayers = parseInt(state.raw.si_maxplayers)
if (state.raw.si_maxPlayers) state.maxplayers = parseInt(state.raw.si_maxPlayers)
if (state.raw.si_usepass === '1') state.password = true
if (state.raw.si_needPass === '1') state.password = true
if (this.options.port === 27733) state.gamePort = 3074 // etqw has a different query and game port
}
attemptPlayerParse (rest, isEtqw, hasClanTag, hasClanTagPos, hasTypeFlag) {
this.logger.debug('starting player parse attempt:')
this.logger.debug('isEtqw: ' + isEtqw)
this.logger.debug('hasClanTag: ' + hasClanTag)
this.logger.debug('hasClanTagPos: ' + hasClanTagPos)
this.logger.debug('hasTypeFlag: ' + hasTypeFlag)
const reader = this.reader(rest)
let lastId = -1
const players = []
while (true) {
this.logger.debug('---')
if (reader.done()) {
this.logger.debug('* aborting attempt, overran buffer *')
return null
}
const player = {}
player.id = reader.uint(1)
this.logger.debug('id: ' + player.id)
if (player.id <= lastId || player.id > 0x20) {
this.logger.debug('* aborting attempt, invalid player id *')
return null
}
lastId = player.id
if (player.id === 0x20) {
this.logger.debug('* player parse successful *')
break
}
player.ping = reader.uint(2)
this.logger.debug('ping: ' + player.ping)
if (!isEtqw) {
player.rate = reader.uint(4)
this.logger.debug('rate: ' + player.rate)
}
player.name = this.stripColors(reader.string())
this.logger.debug('name: ' + player.name)
if (hasClanTag) {
if (hasClanTagPos) {
const clanTagPos = reader.uint(1)
this.logger.debug('clanTagPos: ' + clanTagPos)
}
player.clantag = this.stripColors(reader.string())
this.logger.debug('clan tag: ' + player.clantag)
}
if (hasTypeFlag) {
player.typeflag = reader.uint(1)
this.logger.debug('type flag: ' + player.typeflag)
}
players.push(player)
}
return [players, reader]
}
stripColors (str) {
// uses quake 3 color codes
return this.options.stripColors ? str.replace(/\^(X.{6}|.)/g, '') : str
}
}