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\Buffer;
22: use GameQ\Exception\Protocol as Exception;
23: use GameQ\Protocol;
24: use GameQ\Result;
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 <austin@codebeard.com>
36: */
37: class Bfbc2 extends Protocol
38: {
39: /**
40: * Array of packets we want to query.
41: *
42: * @var array
43: */
44: protected $packets = [
45: self::PACKET_VERSION => "\x00\x00\x00\x00\x18\x00\x00\x00\x01\x00\x00\x00\x07\x00\x00\x00version\x00",
46: self::PACKET_STATUS => "\x00\x00\x00\x00\x1b\x00\x00\x00\x01\x00\x00\x00\x0a\x00\x00\x00serverInfo\x00",
47: 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",
48: ];
49:
50: /**
51: * Use the response flag to figure out what method to run
52: *
53: * @var array
54: */
55: protected $responses = [
56: "processVersion",
57: "processDetails",
58: "processPlayers",
59: ];
60:
61: /**
62: * The transport mode for this protocol is TCP
63: *
64: * @var string
65: */
66: protected $transport = self::TRANSPORT_TCP;
67:
68: /**
69: * The query protocol used to make the call
70: *
71: * @var string
72: */
73: protected $protocol = 'bfbc2';
74:
75: /**
76: * String name of this protocol class
77: *
78: * @var string
79: */
80: protected $name = 'bfbc2';
81:
82: /**
83: * Longer string name of this protocol class
84: *
85: * @var string
86: */
87: protected $name_long = "Battlefield Bad Company 2";
88:
89: /**
90: * query_port = client_port + 29321
91: * 48888 = 19567 + 29321
92: *
93: * @var int
94: */
95: protected $port_diff = 29321;
96:
97: /**
98: * Normalize settings for this protocol
99: *
100: * @var array
101: */
102: protected $normalize = [
103: // General
104: 'general' => [
105: // target => source
106: 'dedicated' => 'dedicated',
107: 'hostname' => 'hostname',
108: 'mapname' => 'map',
109: 'maxplayers' => 'max_players',
110: 'numplayers' => 'num_players',
111: 'password' => 'password',
112: ],
113: 'player' => [
114: 'name' => 'name',
115: 'score' => 'score',
116: 'ping' => 'ping',
117: ],
118: 'team' => [
119: 'score' => 'tickets',
120: ],
121: ];
122:
123: /**
124: * Process the response for the StarMade server
125: *
126: * @return array
127: * @throws \GameQ\Exception\Protocol
128: */
129: public function processResponse()
130: {
131: //print_r($this->packets_response);
132:
133: // Holds the results sent back
134: $results = [];
135:
136: // Iterate over the response packets
137: // @todo: This protocol has no packet ordering, ids or anyway to identify which packet coming back belongs to which initial call.
138: foreach ($this->packets_response as $i => $packet) {
139: // Create a new buffer
140: $buffer = new Buffer($packet);
141:
142: // Burn first 4 bytes, same across all packets
143: $buffer->skip(4);
144:
145: // Get the packet length
146: $packetLength = $buffer->getLength();
147:
148: // Check to make sure the expected length matches the real length
149: // Subtract 4 for the header burn
150: if ($packetLength != ($buffer->readInt32() - 4)) {
151: throw new Exception(__METHOD__ . " packet length does not match expected length!");
152: }
153:
154: // We assume the packets are coming back in the same order as sent, this maybe incorrect...
155: $results = array_merge(
156: $results,
157: call_user_func_array([$this, $this->responses[$i]], [$buffer])
158: );
159: }
160:
161: unset($buffer, $packetLength);
162:
163: return $results;
164: }
165:
166: // Internal Methods
167:
168: /**
169: * Decode the buffer into a usable format
170: *
171: * @param \GameQ\Buffer $buffer
172: *
173: * @return array
174: * @throws \GameQ\Exception\Protocol
175: */
176: protected function decode(Buffer $buffer)
177: {
178: $items = [];
179:
180: // Get the number of words in this buffer
181: $itemCount = $buffer->readInt32();
182:
183: // Loop over the number of items
184: for ($i = 0; $i < $itemCount; $i++) {
185: // Length of the string
186: $buffer->readInt32();
187:
188: // Just read the string
189: $items[$i] = $buffer->readString();
190: }
191:
192: return $items;
193: }
194:
195: /**
196: * Process the server details
197: *
198: * @param \GameQ\Buffer $buffer
199: *
200: * @return array
201: * @throws \GameQ\Exception\Protocol
202: */
203: protected function processDetails(Buffer $buffer)
204: {
205: // Decode into items
206: $items = $this->decode($buffer);
207:
208: // Set the result to a new result instance
209: $result = new Result();
210:
211: // Server is always dedicated
212: $result->add('dedicated', 1);
213:
214: // These are the same no matter what mode the server is in
215: $result->add('hostname', $items[1]);
216: $result->add('num_players', (int)$items[2]);
217: $result->add('max_players', (int)$items[3]);
218: $result->add('gametype', $items[4]);
219: $result->add('map', $items[5]);
220: $result->add('roundsplayed', (int)$items[6]);
221: $result->add('roundstotal', (int)$items[7]);
222: $result->add('num_teams', (int)$items[8]);
223:
224: // Set the current index
225: $index_current = 9;
226:
227: // Pull the team count
228: $teamCount = $result->get('num_teams');
229:
230: // Loop for the number of teams found, increment along the way
231: for ($id = 1; $id <= $teamCount; $id++, $index_current++) {
232: // Shows the tickets
233: $result->addTeam('tickets', $items[$index_current]);
234: // We add an id so we know which team this is
235: $result->addTeam('id', $id);
236: }
237:
238: // Get and set the rest of the data points.
239: $result->add('targetscore', (int)$items[$index_current]);
240: $result->add('online', 1); // Forced true, shows accepting players
241: $result->add('ranked', (($items[$index_current + 2] == 'true') ? 1 : 0));
242: $result->add('punkbuster', (($items[$index_current + 3] == 'true') ? 1 : 0));
243: $result->add('password', (($items[$index_current + 4] == 'true') ? 1 : 0));
244: $result->add('uptime', (int)$items[$index_current + 5]);
245: $result->add('roundtime', (int)$items[$index_current + 6]);
246: $result->add('mod', $items[$index_current + 7]);
247:
248: $result->add('ip_port', $items[$index_current + 9]);
249: $result->add('punkbuster_version', $items[$index_current + 10]);
250: $result->add('join_queue', (($items[$index_current + 11] == 'true') ? 1 : 0));
251: $result->add('region', $items[$index_current + 12]);
252:
253: unset($items, $index_current, $teamCount, $buffer);
254:
255: return $result->fetch();
256: }
257:
258: /**
259: * Process the server version
260: *
261: * @param \GameQ\Buffer $buffer
262: *
263: * @return array
264: * @throws \GameQ\Exception\Protocol
265: */
266: protected function processVersion(Buffer $buffer)
267: {
268: // Decode into items
269: $items = $this->decode($buffer);
270:
271: // Set the result to a new result instance
272: $result = new Result();
273:
274: $result->add('version', $items[2]);
275:
276: unset($buffer, $items);
277:
278: return $result->fetch();
279: }
280:
281: /**
282: * Process the players
283: *
284: * @param \GameQ\Buffer $buffer
285: *
286: * @return array
287: * @throws \GameQ\Exception\Protocol
288: */
289: protected function processPlayers(Buffer $buffer)
290: {
291: // Decode into items
292: $items = $this->decode($buffer);
293:
294: // Set the result to a new result instance
295: $result = new Result();
296:
297: // Number of data points per player
298: $numTags = $items[1];
299:
300: // Grab the tags for each player
301: $tags = array_slice($items, 2, $numTags);
302:
303: // Get the player count
304: $playerCount = $items[$numTags + 2];
305:
306: // Iterate over the index until we run out of players
307: for ($i = 0, $x = $numTags + 3; $i < $playerCount; $i++, $x += $numTags) {
308: // Loop over the player tags and extract the info for that tag
309: foreach ($tags as $index => $tag) {
310: $result->addPlayer($tag, $items[($x + $index)]);
311: }
312: }
313:
314: return $result->fetch();
315: }
316: }
317: