1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* This file is part of GameQ. |
4
|
|
|
* |
5
|
|
|
* GameQ is free software; you can redistribute it and/or modify |
6
|
|
|
* it under the terms of the GNU Lesser General Public License as published by |
7
|
|
|
* the Free Software Foundation; either version 3 of the License, or |
8
|
|
|
* (at your option) any later version. |
9
|
|
|
* |
10
|
|
|
* GameQ is distributed in the hope that it will be useful, |
11
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
12
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13
|
|
|
* GNU Lesser General Public License for more details. |
14
|
|
|
* |
15
|
|
|
* You should have received a copy of the GNU Lesser General Public License |
16
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
17
|
|
|
*/ |
18
|
|
|
|
19
|
|
|
namespace GameQ\Protocols; |
20
|
|
|
|
21
|
|
|
use GameQ\Protocol; |
22
|
|
|
use GameQ\Buffer; |
23
|
|
|
use GameQ\Result; |
24
|
|
|
use GameQ\Exception\Protocol as Exception; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* Battlefield Bad Company 2 Protocol Class |
28
|
|
|
* |
29
|
|
|
* NOTE: There are no qualifiers to the response packets sent back from the server as to which response packet |
30
|
|
|
* belongs to which query request. For now this class assumes the responses are in the same order as the order in |
31
|
|
|
* which the packets were sent to the server. If this assumption turns out to be wrong there is easy way to tell which |
32
|
|
|
* response belongs to which query. Hopefully this assumption will hold true as it has in my testing. |
33
|
|
|
* |
34
|
|
|
* @package GameQ\Protocols |
35
|
|
|
* @author Austin Bischoff <[email protected]> |
36
|
|
|
*/ |
37
|
|
|
class Bfbc2 extends Protocol |
38
|
|
|
{ |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Array of packets we want to query. |
42
|
|
|
* |
43
|
|
|
* @type array |
44
|
|
|
*/ |
45
|
|
|
protected $packets = [ |
46
|
|
|
self::PACKET_VERSION => "\x00\x00\x00\x00\x18\x00\x00\x00\x01\x00\x00\x00\x07\x00\x00\x00version\x00", |
47
|
|
|
self::PACKET_STATUS => "\x00\x00\x00\x00\x1b\x00\x00\x00\x01\x00\x00\x00\x0a\x00\x00\x00serverInfo\x00", |
48
|
|
|
self::PACKET_PLAYERS => "\x00\x00\x00\x00\x24\x00\x00\x00\x02\x00\x00\x00\x0b\x00\x00\x00listPlayers\x00\x03\x00\x00\x00\x61ll\x00", |
49
|
|
|
]; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* Use the response flag to figure out what method to run |
53
|
|
|
* |
54
|
|
|
* @type array |
55
|
|
|
*/ |
56
|
|
|
protected $responses = [ |
57
|
|
|
"processVersion", |
58
|
|
|
"processDetails", |
59
|
|
|
"processPlayers", |
60
|
|
|
]; |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* The transport mode for this protocol is TCP |
64
|
|
|
* |
65
|
|
|
* @type string |
66
|
|
|
*/ |
67
|
|
|
protected $transport = self::TRANSPORT_TCP; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* The query protocol used to make the call |
71
|
|
|
* |
72
|
|
|
* @type string |
73
|
|
|
*/ |
74
|
|
|
protected $protocol = 'bfbc2'; |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* String name of this protocol class |
78
|
|
|
* |
79
|
|
|
* @type string |
80
|
|
|
*/ |
81
|
|
|
protected $name = 'bfbc2'; |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* Longer string name of this protocol class |
85
|
|
|
* |
86
|
|
|
* @type string |
87
|
|
|
*/ |
88
|
|
|
protected $name_long = "Battlefield Bad Company 2"; |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* The client join link |
92
|
|
|
* |
93
|
|
|
* @type string |
94
|
|
|
*/ |
95
|
|
|
protected $join_link = null; |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* query_port = client_port + 29321 |
99
|
|
|
* 48888 = 19567 + 29321 |
100
|
|
|
* |
101
|
|
|
* @type int |
102
|
|
|
*/ |
103
|
|
|
protected $port_diff = 29321; |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* Normalize settings for this protocol |
107
|
|
|
* |
108
|
|
|
* @type array |
109
|
|
|
*/ |
110
|
|
|
protected $normalize = [ |
111
|
|
|
// General |
112
|
|
|
'general' => [ |
113
|
|
|
// target => source |
114
|
|
|
'dedicated' => 'dedicated', |
115
|
|
|
'hostname' => 'hostname', |
116
|
|
|
'mapname' => 'map', |
117
|
|
|
'maxplayers' => 'max_players', |
118
|
|
|
'numplayers' => 'num_players', |
119
|
|
|
'password' => 'password', |
120
|
|
|
], |
121
|
|
|
'player' => [ |
122
|
|
|
'name' => 'name', |
123
|
|
|
'score' => 'score', |
124
|
|
|
'ping' => 'ping', |
125
|
|
|
], |
126
|
|
|
'team' => [ |
127
|
|
|
'score' => 'tickets', |
128
|
|
|
], |
129
|
|
|
]; |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* Process the response for the StarMade server |
133
|
|
|
* |
134
|
|
|
* @return array |
135
|
|
|
* @throws \GameQ\Exception\Protocol |
136
|
|
|
*/ |
137
|
4 |
|
public function processResponse() |
138
|
|
|
{ |
139
|
|
|
|
140
|
|
|
//print_r($this->packets_response); |
141
|
|
|
|
142
|
|
|
// Holds the results sent back |
143
|
4 |
|
$results = []; |
144
|
|
|
|
145
|
|
|
// Iterate over the response packets |
146
|
|
|
// @todo: This protocol has no packet ordering, ids or anyway to identify which packet coming back belongs to which initial call. |
147
|
4 |
|
foreach ($this->packets_response as $i => $packet) { |
148
|
|
|
// Create a new buffer |
149
|
4 |
|
$buffer = new Buffer($packet); |
150
|
|
|
|
151
|
|
|
// Burn first 4 bytes, same across all packets |
152
|
4 |
|
$buffer->skip(4); |
153
|
|
|
|
154
|
|
|
// Get the packet length |
155
|
4 |
|
$packetLength = $buffer->getLength(); |
156
|
|
|
|
157
|
|
|
// Check to make sure the expected length matches the real length |
158
|
|
|
// Subtract 4 for the header burn |
159
|
4 |
|
if ($packetLength != ($buffer->readInt32() - 4)) { |
160
|
1 |
|
throw new Exception(__METHOD__ . " packet length does not match expected length!"); |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
// We assume the packets are coming back in the same order as sent, this maybe incorrect... |
164
|
3 |
|
$results = array_merge( |
165
|
|
|
$results, |
166
|
3 |
|
call_user_func_array([$this, $this->responses[$i]], [$buffer]) |
167
|
|
|
); |
168
|
|
|
} |
169
|
|
|
|
170
|
3 |
|
unset($buffer, $packetLength); |
171
|
|
|
|
172
|
3 |
|
return $results; |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
/* |
176
|
|
|
* Internal Methods |
177
|
|
|
*/ |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* Decode the buffer into a usable format |
181
|
|
|
* |
182
|
|
|
* @param \GameQ\Buffer $buffer |
183
|
|
|
* |
184
|
|
|
* @return array |
185
|
|
|
*/ |
186
|
3 |
|
protected function decode(Buffer $buffer) |
187
|
|
|
{ |
188
|
|
|
|
189
|
3 |
|
$items = []; |
190
|
|
|
|
191
|
|
|
// Get the number of words in this buffer |
192
|
3 |
|
$itemCount = $buffer->readInt32(); |
193
|
|
|
|
194
|
|
|
// Loop over the number of items |
195
|
3 |
|
for ($i = 0; $i < $itemCount; $i++) { |
196
|
|
|
// Length of the string |
197
|
3 |
|
$buffer->readInt32(); |
198
|
|
|
|
199
|
|
|
// Just read the string |
200
|
3 |
|
$items[$i] = $buffer->readString(); |
201
|
|
|
} |
202
|
|
|
|
203
|
3 |
|
return $items; |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
/** |
207
|
|
|
* Process the server details |
208
|
|
|
* |
209
|
|
|
* @param \GameQ\Buffer $buffer |
210
|
|
|
* |
211
|
|
|
* @return array |
212
|
|
|
*/ |
213
|
3 |
|
protected function processDetails(Buffer $buffer) |
|
|
|
|
214
|
|
|
{ |
215
|
|
|
|
216
|
|
|
// Decode into items |
217
|
3 |
|
$items = $this->decode($buffer); |
218
|
|
|
|
219
|
|
|
// Set the result to a new result instance |
220
|
3 |
|
$result = new Result(); |
221
|
|
|
|
222
|
|
|
// Server is always dedicated |
223
|
3 |
|
$result->add('dedicated', 1); |
224
|
|
|
|
225
|
|
|
// These are the same no matter what mode the server is in |
226
|
3 |
|
$result->add('hostname', $items[1]); |
227
|
3 |
|
$result->add('num_players', (int)$items[2]); |
228
|
3 |
|
$result->add('max_players', (int)$items[3]); |
229
|
3 |
|
$result->add('gametype', $items[4]); |
230
|
3 |
|
$result->add('map', $items[5]); |
231
|
3 |
|
$result->add('roundsplayed', (int)$items[6]); |
232
|
3 |
|
$result->add('roundstotal', (int)$items[7]); |
233
|
3 |
|
$result->add('num_teams', (int)$items[8]); |
234
|
|
|
|
235
|
|
|
// Set the current index |
236
|
3 |
|
$index_current = 9; |
237
|
|
|
|
238
|
|
|
// Pull the team count |
239
|
3 |
|
$teamCount = $result->get('num_teams'); |
240
|
|
|
|
241
|
|
|
// Loop for the number of teams found, increment along the way |
242
|
3 |
|
for ($id = 1; $id <= $teamCount; $id++, $index_current++) { |
243
|
|
|
// Shows the tickets |
244
|
3 |
|
$result->addTeam('tickets', $items[$index_current]); |
245
|
|
|
// We add an id so we know which team this is |
246
|
3 |
|
$result->addTeam('id', $id); |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
// Get and set the rest of the data points. |
250
|
3 |
|
$result->add('targetscore', (int)$items[$index_current]); |
251
|
3 |
|
$result->add('online', 1); // Forced true, shows accepting players |
252
|
3 |
|
$result->add('ranked', (($items[$index_current + 2] == 'true') ? 1 : 0)); |
253
|
3 |
|
$result->add('punkbuster', (($items[$index_current + 3] == 'true') ? 1 : 0)); |
254
|
3 |
|
$result->add('password', (($items[$index_current + 4] == 'true') ? 1 : 0)); |
255
|
3 |
|
$result->add('uptime', (int)$items[$index_current + 5]); |
256
|
3 |
|
$result->add('roundtime', (int)$items[$index_current + 6]); |
257
|
3 |
|
$result->add('mod', $items[$index_current + 7]); |
258
|
|
|
|
259
|
3 |
|
$result->add('ip_port', $items[$index_current + 9]); |
260
|
3 |
|
$result->add('punkbuster_version', $items[$index_current + 10]); |
261
|
3 |
|
$result->add('join_queue', (($items[$index_current + 11] == 'true') ? 1 : 0)); |
262
|
3 |
|
$result->add('region', $items[$index_current + 12]); |
263
|
|
|
|
264
|
3 |
|
unset($items, $index_current, $teamCount, $buffer); |
265
|
|
|
|
266
|
3 |
|
return $result->fetch(); |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
/** |
270
|
|
|
* Process the server version |
271
|
|
|
* |
272
|
|
|
* @param \GameQ\Buffer $buffer |
273
|
|
|
* |
274
|
|
|
* @return array |
275
|
|
|
*/ |
276
|
3 |
|
protected function processVersion(Buffer $buffer) |
277
|
|
|
{ |
278
|
|
|
// Decode into items |
279
|
3 |
|
$items = $this->decode($buffer); |
280
|
|
|
|
281
|
|
|
// Set the result to a new result instance |
282
|
3 |
|
$result = new Result(); |
283
|
|
|
|
284
|
3 |
|
$result->add('version', $items[2]); |
285
|
|
|
|
286
|
3 |
|
unset($buffer, $items); |
287
|
|
|
|
288
|
3 |
|
return $result->fetch(); |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
/** |
292
|
|
|
* Process the players |
293
|
|
|
* |
294
|
|
|
* @param \GameQ\Buffer $buffer |
295
|
|
|
* |
296
|
|
|
* @return array |
297
|
|
|
*/ |
298
|
3 |
|
protected function processPlayers(Buffer $buffer) |
299
|
|
|
{ |
300
|
|
|
|
301
|
|
|
// Decode into items |
302
|
3 |
|
$items = $this->decode($buffer); |
303
|
|
|
|
304
|
|
|
// Set the result to a new result instance |
305
|
3 |
|
$result = new Result(); |
306
|
|
|
|
307
|
|
|
// Number of data points per player |
308
|
3 |
|
$numTags = $items[1]; |
309
|
|
|
|
310
|
|
|
// Grab the tags for each player |
311
|
3 |
|
$tags = array_slice($items, 2, $numTags); |
312
|
|
|
|
313
|
|
|
// Get the player count |
314
|
3 |
|
$playerCount = $items[$numTags + 2]; |
315
|
|
|
|
316
|
|
|
// Iterate over the index until we run out of players |
317
|
3 |
|
for ($i = 0, $x = $numTags + 3; $i < $playerCount; $i++, $x += $numTags) { |
318
|
|
|
// Loop over the player tags and extract the info for that tag |
319
|
2 |
|
foreach ($tags as $index => $tag) { |
320
|
2 |
|
$result->addPlayer($tag, $items[($x + $index)]); |
321
|
|
|
} |
322
|
|
|
} |
323
|
|
|
|
324
|
3 |
|
return $result->fetch(); |
325
|
|
|
} |
326
|
|
|
} |
327
|
|
|
|
A high number of execution paths generally suggests many nested conditional statements and make the code less readible. This can usually be fixed by splitting the method into several smaller methods.
You can also find more information in the “Code” section of your repository.