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\Helpers\Str;
24: use GameQ\Protocol;
25: use GameQ\Result;
26:
27: /**
28: * GameSpy2 Protocol class
29: *
30: * Given the ability for non utf-8 characters to be used as hostnames, player names, etc... this
31: * version returns all strings utf-8 encoded. To access the proper version of a
32: * string response you must use Str::utf8ToIso() on the specific response.
33: *
34: * @author Austin Bischoff <austin@codebeard.com>
35: */
36: class Gamespy2 extends Protocol
37: {
38: /**
39: * Define the state of this class
40: *
41: * @var int
42: */
43: protected $state = self::STATE_BETA;
44:
45: /**
46: * Array of packets we want to look up.
47: * Each key should correspond to a defined method in this or a parent class
48: *
49: * @var array
50: */
51: protected $packets = [
52: self::PACKET_DETAILS => "\xFE\xFD\x00\x43\x4F\x52\x59\xFF\x00\x00",
53: self::PACKET_PLAYERS => "\xFE\xFD\x00\x43\x4F\x52\x58\x00\xFF\xFF",
54: ];
55:
56: /**
57: * Use the response flag to figure out what method to run
58: *
59: * @var array
60: */
61: protected $responses = [
62: "\x00\x43\x4F\x52\x59" => "processDetails",
63: "\x00\x43\x4F\x52\x58" => "processPlayers",
64: ];
65:
66: /**
67: * The query protocol used to make the call
68: *
69: * @var string
70: */
71: protected $protocol = 'gamespy2';
72:
73: /**
74: * String name of this protocol class
75: *
76: * @var string
77: */
78: protected $name = 'gamespy2';
79:
80: /**
81: * Longer string name of this protocol class
82: *
83: * @var string
84: */
85: protected $name_long = "GameSpy2 Server";
86:
87: /**
88: * Normalize settings for this protocol
89: *
90: * @var array
91: */
92: protected $normalize = [
93: // General
94: 'general' => [
95: // target => source
96: 'dedicated' => 'dedicated',
97: 'gametype' => 'gametype',
98: 'hostname' => 'hostname',
99: 'mapname' => 'mapname',
100: 'maxplayers' => 'maxplayers',
101: 'mod' => 'mod',
102: 'numplayers' => 'numplayers',
103: 'password' => 'password',
104: ],
105: ];
106:
107:
108: /**
109: * Process the response
110: *
111: * @return array
112: * @throws Exception
113: */
114: public function processResponse()
115: {
116: // Will hold the packets after sorting
117: $packets = [];
118:
119: // We need to pre-sort these for split packets so we can do extra work where needed
120: foreach ($this->packets_response as $response) {
121: $buffer = new Buffer($response);
122:
123: // Pull out the header
124: $header = $buffer->read(5);
125:
126: // Add the packet to the proper section, we will combine later
127: $packets[$header][] = $buffer->getBuffer();
128: }
129:
130: unset($buffer);
131:
132: $results = [];
133:
134: // Now let's iterate and process
135: foreach ($packets as $header => $packetGroup) {
136: // Figure out which packet response this is
137: if (!array_key_exists($header, $this->responses)) {
138: throw new Exception(__METHOD__ . " response type '" . bin2hex($header) . "' is not valid");
139: }
140:
141: // Now we need to call the proper method
142: $results = array_merge(
143: $results,
144: call_user_func_array([$this, $this->responses[$header]], [new Buffer(implode($packetGroup))])
145: );
146: }
147:
148: unset($packets);
149:
150: return $results;
151: }
152:
153: // Internal methods
154:
155: /**
156: * Handles processing the details data into a usable format
157: *
158: * @param \GameQ\Buffer $buffer
159: *
160: * @return array
161: * @throws Exception
162: * @throws \GameQ\Exception\Protocol
163: */
164: protected function processDetails(Buffer $buffer)
165: {
166: // Set the result to a new result instance
167: $result = new Result();
168:
169: // We go until we hit an empty key
170: while ($buffer->getLength()) {
171: $key = $buffer->readString();
172: if (strlen($key) == 0) {
173: break;
174: }
175: $result->add($key, Str::isoToUtf8($buffer->readString()));
176: }
177:
178: unset($buffer);
179:
180: return $result->fetch();
181: }
182:
183: /**
184: * Handles processing the players data into a usable format
185: *
186: * @param \GameQ\Buffer $buffer
187: *
188: * @return array
189: * @throws Exception
190: * @throws \GameQ\Exception\Protocol
191: */
192: protected function processPlayers(Buffer $buffer)
193: {
194: // Set the result to a new result instance
195: $result = new Result();
196:
197: // Skip the header
198: $buffer->skip(1);
199:
200: // Players are first
201: $this->parsePlayerTeam('players', $buffer, $result);
202:
203: // Teams are next
204: $this->parsePlayerTeam('teams', $buffer, $result);
205:
206: unset($buffer);
207:
208: return $result->fetch();
209: }
210:
211: /**
212: * Parse the player/team info returned from the player call
213: *
214: * @param string $dataType
215: * @param \GameQ\Buffer $buffer
216: * @param \GameQ\Result $result
217: *
218: * @throws Exception
219: * @throws \GameQ\Exception\Protocol
220: */
221: protected function parsePlayerTeam($dataType, Buffer &$buffer, Result &$result)
222: {
223: // Do count
224: $result->add('num_' . $dataType, $buffer->readInt8());
225:
226: // Variable names
227: $varNames = [];
228:
229: // Loop until we run out of length
230: while ($buffer->getLength()) {
231: $varNames[] = str_replace('_', '', $buffer->readString());
232:
233: if ($buffer->lookAhead() === "\x00") {
234: $buffer->skip();
235: break;
236: }
237: }
238:
239: // Check if there are any value entries
240: if ($buffer->lookAhead() == "\x00") {
241: $buffer->skip();
242:
243: return;
244: }
245:
246: // Get the values
247: while ($buffer->getLength() > 4) {
248: foreach ($varNames as $varName) {
249: $result->addSub($dataType, Str::isoToUtf8($varName), Str::isoToUtf8($buffer->readString()));
250: }
251: if ($buffer->lookAhead() === "\x00") {
252: $buffer->skip();
253: break;
254: }
255: }
256:
257: return;
258: }
259: }
260: