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: use GameQ\Server;
27:
28: /**
29: * Teamspeak 2 Protocol Class
30: *
31: * All values are utf8 encoded upon processing
32: *
33: * This code ported from GameQ v1/v2. Credit to original author(s) as I just updated it to
34: * work within this new system.
35: *
36: * @author Austin Bischoff <austin@codebeard.com>
37: */
38: class Teamspeak2 extends Protocol
39: {
40: /**
41: * Array of packets we want to look up.
42: * Each key should correspond to a defined method in this or a parent class
43: *
44: * @var array
45: */
46: protected $packets = [
47: self::PACKET_DETAILS => "sel %d\x0asi\x0a",
48: self::PACKET_CHANNELS => "sel %d\x0acl\x0a",
49: self::PACKET_PLAYERS => "sel %d\x0apl\x0a",
50: ];
51:
52: /**
53: * The transport mode for this protocol is TCP
54: *
55: * @var string
56: */
57: protected $transport = self::TRANSPORT_TCP;
58:
59: /**
60: * The query protocol used to make the call
61: *
62: * @var string
63: */
64: protected $protocol = 'teamspeak2';
65:
66: /**
67: * String name of this protocol class
68: *
69: * @var string
70: */
71: protected $name = 'teamspeak2';
72:
73: /**
74: * Longer string name of this protocol class
75: *
76: * @var string
77: */
78: protected $name_long = "Teamspeak 2";
79:
80: /**
81: * The client join link
82: *
83: * @var string
84: */
85: protected $join_link = "teamspeak://%s:%d/";
86:
87: /**
88: * Normalize settings for this protocol
89: *
90: * @var array
91: */
92: protected $normalize = [
93: // General
94: 'general' => [
95: 'dedicated' => 'dedicated',
96: 'hostname' => 'server_name',
97: 'password' => 'server_password',
98: 'numplayers' => 'server_currentusers',
99: 'maxplayers' => 'server_maxusers',
100: ],
101: // Player
102: 'player' => [
103: 'id' => 'p_id',
104: 'team' => 'c_id',
105: 'name' => 'nick',
106: ],
107: // Team
108: 'team' => [
109: 'id' => 'id',
110: 'name' => 'name',
111: ],
112: ];
113:
114: /**
115: * Before we send off the queries we need to update the packets
116: *
117: * @param \GameQ\Server $server
118: *
119: * @throws \GameQ\Exception\Protocol
120: */
121: public function beforeSend(Server $server)
122: {
123: // Check to make sure we have a query_port because it is required
124: if (!isset($this->options[Server::SERVER_OPTIONS_QUERY_PORT])
125: || empty($this->options[Server::SERVER_OPTIONS_QUERY_PORT])
126: ) {
127: throw new Exception(__METHOD__ . " Missing required setting '" . Server::SERVER_OPTIONS_QUERY_PORT . "'.");
128: }
129:
130: // Let's loop the packets and set the proper pieces
131: foreach ($this->packets as $packet_type => $packet) {
132: // Update with the client port for the server
133: $this->packets[$packet_type] = sprintf($packet, $server->portClient());
134: }
135: }
136:
137: /**
138: * Process the response
139: *
140: * @return array
141: * @throws \GameQ\Exception\Protocol
142: */
143: public function processResponse()
144: {
145: // Make a new buffer out of all of the packets
146: $buffer = new Buffer(implode('', $this->packets_response));
147:
148: // Check the header [TS]
149: if (($header = trim($buffer->readString("\n"))) !== '[TS]') {
150: throw new Exception(__METHOD__ . " Expected header '{$header}' does not match expected '[TS]'.");
151: }
152:
153: // Split this buffer as the data blocks are bound by "OK" and drop any empty values
154: $sections = array_filter(explode("OK", $buffer->getBuffer()), function ($value) {
155: $value = trim($value);
156:
157: return !empty($value);
158: });
159:
160: // Trim up the values to remove extra whitespace
161: $sections = array_map('trim', $sections);
162:
163: // Set the result to a new result instance
164: $result = new Result();
165:
166: // Now we need to iterate over the sections and off load the processing
167: foreach ($sections as $section) {
168: // Grab a snip of the data so we can figure out what it is
169: $check = substr($section, 0, 7);
170:
171: // Offload to the proper method
172: if ($check == 'server_') {
173: // Server settings and info
174: $this->processDetails($section, $result);
175: } elseif ($check == "id\tcode") {
176: // Channel info
177: $this->processChannels($section, $result);
178: } elseif ($check == "p_id\tc_") {
179: // Player info
180: $this->processPlayers($section, $result);
181: }
182: }
183:
184: unset($buffer, $sections, $section, $check);
185:
186: return $result->fetch();
187: }
188:
189: // Internal methods
190:
191:
192: /**
193: * Handles processing the details data into a usable format
194: *
195: * @param string $data
196: * @param \GameQ\Result $result
197: * @return void
198: * @throws \GameQ\Exception\Protocol
199: */
200: protected function processDetails($data, Result &$result)
201: {
202: // Create a buffer
203: $buffer = new Buffer($data);
204:
205: // Always dedicated
206: $result->add('dedicated', 1);
207:
208: // Let's loop until we run out of data
209: while ($buffer->getLength()) {
210: // Grab the row, which is an item
211: $row = trim($buffer->readString("\n"));
212:
213: // Split out the information
214: list($key, $value) = explode('=', $row, 2);
215:
216: // Add this to the result
217: $result->add($key, Str::isoToUtf8($value));
218: }
219:
220: unset($data, $buffer, $row, $key, $value);
221: }
222:
223: /**
224: * Process the channel listing
225: *
226: * @param string $data
227: * @param \GameQ\Result $result
228: * @return void
229: * @throws \GameQ\Exception\Protocol
230: */
231: protected function processChannels($data, Result &$result)
232: {
233: // Create a buffer
234: $buffer = new Buffer($data);
235:
236: // The first line holds the column names, data returned is in column/row format
237: $columns = explode("\t", trim($buffer->readString("\n")), 9);
238:
239: // Loop through the rows until we run out of information
240: while ($buffer->getLength()) {
241: // Grab the row, which is a tabbed list of items
242: $row = trim($buffer->readString("\n"));
243:
244: // Explode and merge the data with the columns, then parse
245: $data = array_combine($columns, explode("\t", $row, 9));
246:
247: foreach ($data as $key => $value) {
248: // Now add the data to the result
249: $result->addTeam($key, Str::isoToUtf8($value));
250: }
251: }
252:
253: unset($data, $buffer, $row, $columns, $key, $value);
254: }
255:
256: /**
257: * Process the user listing
258: *
259: * @param string $data
260: * @param \GameQ\Result $result
261: * @return void
262: * @throws \GameQ\Exception\Protocol
263: */
264: protected function processPlayers($data, Result &$result)
265: {
266: // Create a buffer
267: $buffer = new Buffer($data);
268:
269: // The first line holds the column names, data returned is in column/row format
270: $columns = explode("\t", trim($buffer->readString("\n")), 16);
271:
272: // Loop through the rows until we run out of information
273: while ($buffer->getLength()) {
274: // Grab the row, which is a tabbed list of items
275: $row = trim($buffer->readString("\n"));
276:
277: // Explode and merge the data with the columns, then parse
278: $data = array_combine($columns, explode("\t", $row, 16));
279:
280: foreach ($data as $key => $value) {
281: // Now add the data to the result
282: $result->addPlayer($key, Str::isoToUtf8($value));
283: }
284: }
285:
286: unset($data, $buffer, $row, $columns, $key, $value);
287: }
288: }
289: