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 <austin@codebeard.com>
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: public function processResponse()
138: {
139:
140: //print_r($this->packets_response);
141:
142: // Holds the results sent back
143: $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: foreach ($this->packets_response as $i => $packet) {
148: // Create a new buffer
149: $buffer = new Buffer($packet);
150:
151: // Burn first 4 bytes, same across all packets
152: $buffer->skip(4);
153:
154: // Get the packet length
155: $packetLength = $buffer->getLength();
156:
157: // Check to make sure the expected length matches the real length
158: // Subtract 4 for the header burn
159: if ($packetLength != ($buffer->readInt32() - 4)) {
160: 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: $results = array_merge(
165: $results,
166: call_user_func_array([$this, $this->responses[$i]], [$buffer])
167: );
168: }
169:
170: unset($buffer, $packetLength);
171:
172: 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: protected function decode(Buffer $buffer)
187: {
188:
189: $items = [];
190:
191: // Get the number of words in this buffer
192: $itemCount = $buffer->readInt32();
193:
194: // Loop over the number of items
195: for ($i = 0; $i < $itemCount; $i++) {
196: // Length of the string
197: $buffer->readInt32();
198:
199: // Just read the string
200: $items[$i] = $buffer->readString();
201: }
202:
203: return $items;
204: }
205:
206: /**
207: * Process the server details
208: *
209: * @param \GameQ\Buffer $buffer
210: *
211: * @return array
212: */
213: protected function processDetails(Buffer $buffer)
214: {
215:
216: // Decode into items
217: $items = $this->decode($buffer);
218:
219: // Set the result to a new result instance
220: $result = new Result();
221:
222: // Server is always dedicated
223: $result->add('dedicated', 1);
224:
225: // These are the same no matter what mode the server is in
226: $result->add('hostname', $items[1]);
227: $result->add('num_players', (int)$items[2]);
228: $result->add('max_players', (int)$items[3]);
229: $result->add('gametype', $items[4]);
230: $result->add('map', $items[5]);
231: $result->add('roundsplayed', (int)$items[6]);
232: $result->add('roundstotal', (int)$items[7]);
233: $result->add('num_teams', (int)$items[8]);
234:
235: // Set the current index
236: $index_current = 9;
237:
238: // Pull the team count
239: $teamCount = $result->get('num_teams');
240:
241: // Loop for the number of teams found, increment along the way
242: for ($id = 1; $id <= $teamCount; $id++, $index_current++) {
243: // Shows the tickets
244: $result->addTeam('tickets', $items[$index_current]);
245: // We add an id so we know which team this is
246: $result->addTeam('id', $id);
247: }
248:
249: // Get and set the rest of the data points.
250: $result->add('targetscore', (int)$items[$index_current]);
251: $result->add('online', 1); // Forced true, shows accepting players
252: $result->add('ranked', (($items[$index_current + 2] == 'true') ? 1 : 0));
253: $result->add('punkbuster', (($items[$index_current + 3] == 'true') ? 1 : 0));
254: $result->add('password', (($items[$index_current + 4] == 'true') ? 1 : 0));
255: $result->add('uptime', (int)$items[$index_current + 5]);
256: $result->add('roundtime', (int)$items[$index_current + 6]);
257: $result->add('mod', $items[$index_current + 7]);
258:
259: $result->add('ip_port', $items[$index_current + 9]);
260: $result->add('punkbuster_version', $items[$index_current + 10]);
261: $result->add('join_queue', (($items[$index_current + 11] == 'true') ? 1 : 0));
262: $result->add('region', $items[$index_current + 12]);
263:
264: unset($items, $index_current, $teamCount, $buffer);
265:
266: return $result->fetch();
267: }
268:
269: /**
270: * Process the server version
271: *
272: * @param \GameQ\Buffer $buffer
273: *
274: * @return array
275: */
276: protected function processVersion(Buffer $buffer)
277: {
278: // Decode into items
279: $items = $this->decode($buffer);
280:
281: // Set the result to a new result instance
282: $result = new Result();
283:
284: $result->add('version', $items[2]);
285:
286: unset($buffer, $items);
287:
288: return $result->fetch();
289: }
290:
291: /**
292: * Process the players
293: *
294: * @param \GameQ\Buffer $buffer
295: *
296: * @return array
297: */
298: protected function processPlayers(Buffer $buffer)
299: {
300:
301: // Decode into items
302: $items = $this->decode($buffer);
303:
304: // Set the result to a new result instance
305: $result = new Result();
306:
307: // Number of data points per player
308: $numTags = $items[1];
309:
310: // Grab the tags for each player
311: $tags = array_slice($items, 2, $numTags);
312:
313: // Get the player count
314: $playerCount = $items[$numTags + 2];
315:
316: // Iterate over the index until we run out of players
317: 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: foreach ($tags as $index => $tag) {
320: $result->addPlayer($tag, $items[($x + $index)]);
321: }
322: }
323:
324: return $result->fetch();
325: }
326: }
327: