1: <?php
2:
3:
4: namespace GameQ\Protocols;
5:
6: use GameQ\Protocol;
7: use GameQ\Buffer;
8: use GameQ\Result;
9: use GameQ\Exception\Protocol as Exception;
10:
11: /**
12: * Quake3 Protocol Class
13: *
14: * Handles processing Quake 3 servers
15: *
16: * @package GameQ\Protocols
17: */
18: class Quake3 extends Protocol
19: {
20: /**
21: * Array of packets we want to look up.
22: * Each key should correspond to a defined method in this or a parent class
23: *
24: * @type array
25: */
26: protected $packets = [
27: self::PACKET_STATUS => "\xFF\xFF\xFF\xFF\x67\x65\x74\x73\x74\x61\x74\x75\x73\x0A",
28: ];
29:
30: /**
31: * Use the response flag to figure out what method to run
32: *
33: * @type array
34: */
35: protected $responses = [
36: "\xFF\xFF\xFF\xFFstatusResponse" => 'processStatus',
37: ];
38:
39: /**
40: * The query protocol used to make the call
41: *
42: * @type string
43: */
44: protected $protocol = 'quake3';
45:
46: /**
47: * String name of this protocol class
48: *
49: * @type string
50: */
51: protected $name = 'quake3';
52:
53: /**
54: * Longer string name of this protocol class
55: *
56: * @type string
57: */
58: protected $name_long = "Quake 3 Server";
59:
60: /**
61: * The client join link
62: *
63: * @type string
64: */
65: protected $join_link = null;
66:
67: /**
68: * Normalize settings for this protocol
69: *
70: * @type array
71: */
72: protected $normalize = [
73: // General
74: 'general' => [
75: // target => source
76: 'gametype' => 'gamename',
77: 'hostname' => 'sv_hostname',
78: 'mapname' => 'mapname',
79: 'maxplayers' => 'sv_maxclients',
80: 'mod' => 'g_gametype',
81: 'numplayers' => 'clients',
82: 'password' => ['g_needpass', 'pswrd'],
83: ],
84: // Individual
85: 'player' => [
86: 'name' => 'name',
87: 'ping' => 'ping',
88: 'score' => 'frags',
89: ],
90: ];
91:
92: /**
93: * Handle response from the server
94: *
95: * @return mixed
96: * @throws Exception
97: */
98: public function processResponse()
99: {
100: // Make a buffer
101: $buffer = new Buffer(implode('', $this->packets_response));
102:
103: // Grab the header
104: $header = $buffer->readString("\x0A");
105:
106: // Figure out which packet response this is
107: if (empty($header) || !array_key_exists($header, $this->responses)) {
108: throw new Exception(__METHOD__ . " response type '" . bin2hex($header) . "' is not valid");
109: }
110:
111: return call_user_func_array([$this, $this->responses[$header]], [$buffer]);
112: }
113:
114: protected function processStatus(Buffer $buffer)
115: {
116: // We need to split the data and offload
117: $results = $this->processServerInfo(new Buffer($buffer->readString("\x0A")));
118:
119: $results = array_merge_recursive(
120: $results,
121: $this->processPlayers(new Buffer($buffer->getBuffer()))
122: );
123:
124: unset($buffer);
125:
126: // Return results
127: return $results;
128: }
129:
130: /**
131: * Handle processing the server information
132: *
133: * @param Buffer $buffer
134: *
135: * @return array
136: */
137: protected function processServerInfo(Buffer $buffer)
138: {
139: // Set the result to a new result instance
140: $result = new Result();
141:
142: // Burn leading \ if one exists
143: $buffer->readString('\\');
144:
145: // Key / value pairs
146: while ($buffer->getLength()) {
147: // Add result
148: $result->add(
149: trim($buffer->readString('\\')),
150: utf8_encode(trim($buffer->readStringMulti(['\\', "\x0a"])))
151: );
152: }
153:
154: unset($buffer);
155:
156: return $result->fetch();
157: }
158:
159: /**
160: * Handle processing of player data
161: *
162: * @param Buffer $buffer
163: *
164: * @return array
165: * @throws Exception
166: */
167: protected function processPlayers(Buffer $buffer)
168: {
169: // Some games do not have a number of current players
170: $playerCount = 0;
171:
172: // Set the result to a new result instance
173: $result = new Result();
174:
175: // Loop until we are out of data
176: while ($buffer->getLength()) {
177: // Add player info
178: $result->addPlayer('frags', $buffer->readString("\x20"));
179: $result->addPlayer('ping', $buffer->readString("\x20"));
180:
181: // Look ahead to see if we have a name or team
182: $checkTeam = $buffer->lookAhead(1);
183:
184: // We have team info
185: if ($checkTeam != '' and $checkTeam != '"') {
186: $result->addPlayer('team', $buffer->readString("\x20"));
187: }
188:
189: // Check to make sure we have player name
190: $checkPlayerName = $buffer->read();
191:
192: // Bad response
193: if ($checkPlayerName !== '"') {
194: throw new Exception('Expected " but got ' . $checkPlayerName . ' for beginning of player name string!');
195: }
196:
197: // Add player name, encoded
198: $result->addPlayer('name', utf8_encode(trim($buffer->readString('"'))));
199:
200: // Burn ending delimiter
201: $buffer->read();
202:
203: // Increment
204: $playerCount++;
205: }
206:
207: $result->add('clients', $playerCount);
208:
209: // Clear
210: unset($buffer, $playerCount);
211:
212: return $result->fetch();
213: }
214: }
215: