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