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 3 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 Teamspeak3 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 => "use port=%d\x0Aserverinfo\x0A",
48: self::PACKET_PLAYERS => "use port=%d\x0Aclientlist\x0A",
49: self::PACKET_CHANNELS => "use port=%d\x0Achannellist -topic\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 = 'teamspeak3';
65:
66: /**
67: * String name of this protocol class
68: *
69: * @var string
70: */
71: protected $name = 'teamspeak3';
72:
73: /**
74: * Longer string name of this protocol class
75: *
76: * @var string
77: */
78: protected $name_long = "Teamspeak 3";
79:
80: /**
81: * The client join link
82: *
83: * @var string
84: */
85: protected $join_link = "ts3server://%s?port=%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' => 'virtualserver_name',
97: 'password' => 'virtualserver_flag_password',
98: 'numplayers' => 'numplayers',
99: 'maxplayers' => 'virtualserver_maxclients',
100: ],
101: // Player
102: 'player' => [
103: 'id' => 'clid',
104: 'team' => 'cid',
105: 'name' => 'client_nickname',
106: ],
107: // Team
108: 'team' => [
109: 'id' => 'cid',
110: 'name' => 'channel_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: * @return void
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 TS3
149: if (($header = trim($buffer->readString("\n"))) !== 'TS3') {
150: throw new Exception(__METHOD__ . " Expected header '{$header}' does not match expected 'TS3'.");
151: }
152:
153: // Convert all the escaped characters
154: $raw = str_replace(
155: [
156: '\\\\', // Translate escaped \
157: '\\/', // Translate escaped /
158: ],
159: [
160: '\\',
161: '/',
162: ],
163: $buffer->getBuffer()
164: );
165:
166: // Explode the sections and filter to remove empty, junk ones
167: $sections = array_filter(explode("\n", $raw), function ($value) {
168: $value = trim($value);
169:
170: // Not empty string or a message response for "error id=\d"
171: return !empty($value) && substr($value, 0, 5) !== 'error';
172: });
173:
174: // Trim up the values to remove extra whitespace
175: $sections = array_map('trim', $sections);
176:
177: // Set the result to a new result instance
178: $result = new Result();
179:
180: // Iterate over the sections and offload the parsing
181: foreach ($sections as $section) {
182: // Grab a snip of the data so we can figure out what it is
183: $check = substr(trim($section), 0, 4);
184:
185: // Use the first part of the response to figure out where we need to go
186: if ($check == 'virt') {
187: // Server info
188: $this->processDetails($section, $result);
189: } elseif ($check == 'cid=') {
190: // Channels
191: $this->processChannels($section, $result);
192: } elseif ($check == 'clid') {
193: // Clients (players)
194: $this->processPlayers($section, $result);
195: }
196: }
197:
198: unset($buffer, $sections, $section, $check);
199:
200: return $result->fetch();
201: }
202:
203: // Internal methods
204:
205: /**
206: * Process the properties of the data.
207: *
208: * Takes data in "key1=value1 key2=value2 ..." and processes it into a usable format
209: *
210: * @param $data
211: * @return array
212: */
213: protected function processProperties($data)
214: {
215: // Will hold the properties we are sending back
216: $properties = [];
217:
218: // All of these are split on space
219: $items = explode(' ', $data);
220:
221: // Iterate over the items
222: foreach ($items as $item) {
223: // Explode and make sure we always have 2 items in the array
224: list($key, $value) = array_pad(explode('=', $item, 2), 2, '');
225:
226: // Convert spaces and other character changes
227: $properties[$key] = Str::isoToUtf8(str_replace(
228: [
229: '\\s', // Translate spaces
230: ],
231: [
232: ' ',
233: ],
234: $value
235: ));
236: }
237:
238: return $properties;
239: }
240:
241: /**
242: * Handles processing the details data into a usable format
243: *
244: * @param string $data
245: * @param \GameQ\Result $result
246: * @return void
247: */
248: protected function processDetails($data, Result &$result)
249: {
250: // Offload the parsing for these values
251: $properties = $this->processProperties($data);
252:
253: // Always dedicated
254: $result->add('dedicated', 1);
255:
256: // Iterate over the properties
257: foreach ($properties as $key => $value) {
258: $result->add($key, $value);
259: }
260:
261: // We need to manually figure out the number of players
262: $result->add(
263: 'numplayers',
264: ($properties['virtualserver_clientsonline'] - $properties['virtualserver_queryclientsonline'])
265: );
266:
267: unset($data, $properties, $key, $value);
268: }
269:
270: /**
271: * Process the channel listing
272: *
273: * @param string $data
274: * @param \GameQ\Result $result
275: * @return void
276: */
277: protected function processChannels($data, Result &$result)
278: {
279: // We need to split the data at the pipe
280: $channels = explode('|', $data);
281:
282: // Iterate over the channels
283: foreach ($channels as $channel) {
284: // Offload the parsing for these values
285: $properties = $this->processProperties($channel);
286:
287: // Iterate over the properties
288: foreach ($properties as $key => $value) {
289: $result->addTeam($key, $value);
290: }
291: }
292:
293: unset($data, $channel, $channels, $properties, $key, $value);
294: }
295:
296: /**
297: * Process the user listing
298: *
299: * @param string $data
300: * @param \GameQ\Result $result
301: * @return void
302: */
303: protected function processPlayers($data, Result &$result)
304: {
305: // We need to split the data at the pipe
306: $players = explode('|', $data);
307:
308: // Iterate over the channels
309: foreach ($players as $player) {
310: // Offload the parsing for these values
311: $properties = $this->processProperties($player);
312:
313: // Iterate over the properties
314: foreach ($properties as $key => $value) {
315: $result->addPlayer($key, $value);
316: }
317: }
318:
319: unset($data, $player, $players, $properties, $key, $value);
320: }
321: }
322: