• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Austinb / GameQ / 25304434950

04 May 2026 06:27AM UTC coverage: 91.979% (-1.1%) from 93.121%
25304434950

Pull #771

travis-ci

web-flow
Merge d7505df88 into 347cf58c5
Pull Request #771: Update squizlabs/php_codesniffer requirement from 3.*@stable to 4.*@stable

2328 of 2531 relevant lines covered (91.98%)

194.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.25
/src/GameQ/GameQ.php
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;
20

21
use GameQ\Exception\Protocol as ProtocolException;
22
use GameQ\Exception\Query as QueryException;
23

24
/**
25
 * Base GameQ Class
26
 *
27
 * This class should be the only one that is included when you use GameQ to query
28
 * any games servers.
29
 *
30
 * Requirements: See wiki or README for more information on the requirements
31
 *  - PHP 5.4.14+
32
 *    * Bzip2 - http://www.php.net/manual/en/book.bzip2.php
33
 *
34
 * @author Austin Bischoff <austin@codebeard.com>
35
 *
36
 * @property bool   $debug
37
 * @property string $capture_packets_file
38
 * @property int    $stream_timeout
39
 * @property int    $timeout
40
 * @property int    $write_wait
41
 */
42
class GameQ
43
{
44
    // Constants
45
    const PROTOCOLS_DIRECTORY = __DIR__ . '/Protocols';
46

47
    // Static Section
48

49
    /**
50
     * Holds the instance of itself
51
     *
52
     * @var self
53
     */
54
    protected static $instance = null;
55

56
    /**
57
     * Create a new instance of this class
58
     *
59
     * @return \GameQ\GameQ
60
     */
61
    public static function factory()
8✔
62
    {
63
        // Create a new instance
64
        self::$instance = new self();
8✔
65

66
        // Return this new instance
67
        return self::$instance;
8✔
68
    }
69

70
    // Dynamic Section
71

72
    /**
73
     * Default options
74
     *
75
     * @var array
76
     */
77
    protected $options = [
78
        'debug'                => false,
79
        'timeout'              => 3, // Seconds
80
        'filters'              => [
81
            // Default normalize
82
            'normalize_d751713988987e9331980363e24189ce' => [
83
                'filter'  => 'normalize',
84
                'options' => [],
85
            ],
86
        ],
87
        // Advanced settings
88
        'stream_timeout'       => 200000, // See http://www.php.net/manual/en/function.stream-select.php for more info
89
        'write_wait'           => 500,
90
        // How long (in micro-seconds) to pause between writing to server sockets, helps cpu usage
91

92
        // Used for generating protocol test data
93
        'capture_packets_file' => null,
94
    ];
95

96
    /**
97
     * Array of servers being queried
98
     *
99
     * @var array
100
     */
101
    protected $servers = [];
102

103
    /**
104
     * The query library to use.  Default is Native
105
     *
106
     * @var string
107
     */
108
    protected $queryLibrary = 'GameQ\\Query\\Native';
109

110
    /**
111
     * Holds the instance of the queryLibrary
112
     *
113
     * @var \GameQ\Query\Core|null
114
     */
115
    protected $query = null;
116

117
    /**
118
     * Get an option's value
119
     *
120
     * @param mixed $option
121
     *
122
     * @return mixed|null
123
     */
124
    public function __get($option)
2,552✔
125
    {
126
        return isset($this->options[$option]) ? $this->options[$option] : null;
2,552✔
127
    }
128

129
    /**
130
     * Set an option's value
131
     *
132
     * @param mixed $option
133
     * @param mixed $value
134
     *
135
     * @return bool
136
     */
137
    public function __set($option, $value)
2,568✔
138
    {
139
        $this->options[$option] = $value;
2,568✔
140

141
        return true;
2,568✔
142
    }
143

144
    public function getServers()
24✔
145
    {
146
        return $this->servers;
24✔
147
    }
148

149
    public function getOptions()
×
150
    {
151
        return $this->options;
×
152
    }
153

154
    /**
155
     * Chainable call to __set, uses set as the actual setter
156
     *
157
     * @param mixed $var
158
     * @param mixed $value
159
     *
160
     * @return $this
161
     */
162
    public function setOption($var, $value)
2,568✔
163
    {
164
        // Use magic
165
        $this->{$var} = $value;
2,568✔
166

167
        return $this; // Make chainable
2,568✔
168
    }
169

170
    /**
171
     * Add a single server
172
     *
173
     * @param array $server_info
174
     *
175
     * @return $this
176
     */
177
    public function addServer(array $server_info = [])
64✔
178
    {
179
        // Add and validate the server
180
        $this->servers[uniqid()] = new Server($server_info);
64✔
181

182
        return $this; // Make calls chainable
64✔
183
    }
184

185
    /**
186
     * Add multiple servers in a single call
187
     *
188
     * @param array $servers
189
     *
190
     * @return $this
191
     */
192
    public function addServers(array $servers = [])
16✔
193
    {
194
        // Loop through all the servers and add them
195
        foreach ($servers as $server_info) {
16✔
196
            $this->addServer($server_info);
16✔
197
        }
198

199
        return $this; // Make calls chainable
16✔
200
    }
201

202
    /**
203
     * Add a set of servers from a file or an array of files.
204
     * Supported formats:
205
     * JSON
206
     *
207
     * @param array $files
208
     *
209
     * @return $this
210
     * @throws \Exception
211
     */
212
    public function addServersFromFiles($files = [])
8✔
213
    {
214
        // Since we expect an array let us turn a string (i.e. single file) into an array
215
        if (!is_array($files)) {
8✔
216
            $files = [$files];
8✔
217
        }
218

219
        // Iterate over the file(s) and add them
220
        foreach ($files as $file) {
8✔
221
            // Check to make sure the file exists and we can read it
222
            if (!file_exists($file) || !is_readable($file)) {
8✔
223
                continue;
8✔
224
            }
225

226
            // See if this file is JSON
227
            if (($servers = json_decode(file_get_contents($file), true)) === null
8✔
228
                && json_last_error() !== JSON_ERROR_NONE
8✔
229
            ) {
230
                // Type not supported
231
                continue;
8✔
232
            }
233

234
            // Add this list of servers
235
            $this->addServers($servers);
8✔
236
        }
237

238
        return $this;
8✔
239
    }
240

241
    /**
242
     * Clear all of the defined servers
243
     *
244
     * @return $this
245
     */
246
    public function clearServers()
16✔
247
    {
248
        // Reset all the servers
249
        $this->servers = [];
16✔
250

251
        return $this; // Make Chainable
16✔
252
    }
253

254
    /**
255
     * Add a filter to the processing list
256
     *
257
     * @param string $filterName
258
     * @param array  $options
259
     *
260
     * @return $this
261
     */
262
    public function addFilter($filterName, $options = [])
24✔
263
    {
264
        // Create the filter hash so we can run multiple versions of the same filter
265
        $filterHash = sprintf('%s_%s', strtolower($filterName), md5(json_encode($options)));
24✔
266

267
        // Add the filter
268
        $this->options['filters'][$filterHash] = [
24✔
269
            'filter'  => strtolower($filterName),
24✔
270
            'options' => $options,
24✔
271
        ];
21✔
272

273
        unset($filterHash);
24✔
274

275
        return $this;
24✔
276
    }
277

278
    /**
279
     * Remove an added filter
280
     *
281
     * @param string $filterHash
282
     *
283
     * @return $this
284
     */
285
    public function removeFilter($filterHash)
2,568✔
286
    {
287
        // Make lower case
288
        $filterHash = strtolower($filterHash);
2,568✔
289

290
        // Remove this filter if it has been defined
291
        if (array_key_exists($filterHash, $this->options['filters'])) {
2,568✔
292
            unset($this->options['filters'][$filterHash]);
24✔
293
        }
294

295
        unset($filterHash);
2,568✔
296

297
        return $this;
2,568✔
298
    }
299

300
    /**
301
     * Return the list of applied filters
302
     *
303
     * @return array
304
     */
305
    public function listFilters()
8✔
306
    {
307
        return $this->options['filters'];
8✔
308
    }
309

310
    /**
311
     * Main method used to actually process all of the added servers and return the information
312
     *
313
     * @return array
314
     * @throws \Exception
315
     */
316
    public function process()
40✔
317
    {
318
        // Initialize the query library we are using
319
        $class = new \ReflectionClass($this->queryLibrary);
40✔
320

321
        // Set the query pointer to the new instance of the library
322
        $this->query = $class->newInstance();
40✔
323

324
        unset($class);
40✔
325

326
        // Define the return
327
        $results = [];
40✔
328

329
        // @todo: Add break up into loop to split large arrays into smaller chunks
330

331
        // Do server challenge(s) first, if any
332
        $this->doChallenges();
40✔
333

334
        // Do packets for server(s) and get query responses
335
        $this->doQueries();
40✔
336

337
        // Now we should have some information to process for each server
338
        foreach ($this->servers as $server) {
40✔
339
            // @var $server \GameQ\Server
340

341
            // Parse the responses for this server
342
            $result = $this->doParseResponse($server);
40✔
343

344
            // Apply the filters
345
            $result = array_merge($result, $this->doApplyFilters($result, $server));
40✔
346

347
            // Sort the keys so they are alphabetical and nicer to look at
348
            ksort($result);
40✔
349

350
            // Add the result to the results array
351
            $results[$server->id()] = $result;
40✔
352
        }
353

354
        return $results;
40✔
355
    }
356

357
    /**
358
     * Do server challenges, where required
359
     */
360
    protected function doChallenges()
40✔
361
    {
362
        // Initialize the sockets for reading
363
        $sockets = [];
40✔
364

365
        // By default we don't have any challenges to process
366
        $server_challenge = false;
40✔
367

368
        // Do challenge packets
369
        foreach ($this->servers as $server_id => $server) {
40✔
370
            // @var $server \GameQ\Server
371

372
            // This protocol has a challenge packet that needs to be sent
373
            if ($server->protocol()->hasChallenge()) {
40✔
374
                // We have a challenge, set the flag
375
                $server_challenge = true;
×
376

377
                // Let's make a clone of the query class
378
                $socket = clone $this->query;
×
379

380
                // Set the information for this query socket
381
                $socket->set(
×
382
                    $server->protocol()->transport(),
×
383
                    $server->ip,
×
384
                    $server->port_query,
×
385
                    $this->timeout
×
386
                );
387

388
                try {
389
                    // Now write the challenge packet to the socket.
390
                    $socket->write($server->protocol()->getPacket(Protocol::PACKET_CHALLENGE));
×
391

392
                    // Add the socket information so we can reference it easily
393
                    $sockets[(int)$socket->get()] = [
×
394
                        'server_id' => $server_id,
×
395
                        'socket'    => $socket,
×
396
                    ];
397
                } catch (QueryException $exception) {
×
398
                    // Check to see if we are in debug, if so bubble up the exception
399
                    if ($this->debug) {
×
400
                        throw new \Exception($exception->getMessage(), $exception->getCode(), $exception);
×
401
                    }
402
                }
403

404
                unset($socket);
×
405

406
                // Let's sleep shortly so we are not hammering out calls rapid fire style hogging cpu
407
                usleep($this->write_wait);
×
408
            }
409
        }
410

411
        // We have at least one server with a challenge, we need to listen for responses
412
        if ($server_challenge) {
40✔
413
            // Now we need to listen for and grab challenge response(s)
414
            $responses = call_user_func_array(
×
415
                [$this->query, 'getResponses'],
×
416
                [$sockets, $this->timeout, $this->stream_timeout]
×
417
            );
418

419
            // Iterate over the challenge responses
420
            foreach ($responses as $socket_id => $response) {
×
421
                // Back out the server_id we need to update the challenge response for
422
                $server_id = $sockets[$socket_id]['server_id'];
×
423

424
                // Make this into a buffer so it is easier to manipulate
425
                $challenge = new Buffer(implode('', $response));
×
426

427
                // Grab the server instance
428
                // @var $server \GameQ\Server
429
                $server = $this->servers[$server_id];
×
430

431
                // Apply the challenge
432
                $server->protocol()->challengeParseAndApply($challenge);
×
433

434
                // Add this socket to be reused, has to be reused in GameSpy3 for example
435
                $server->socketAdd($sockets[$socket_id]['socket']);
×
436

437
                // Clear
438
                unset($server);
×
439
            }
440
        }
441
    }
5✔
442

443
    /**
444
     * Run the actual queries and get the response(s)
445
     */
446
    protected function doQueries()
40✔
447
    {
448
        // Initialize the array of sockets
449
        $sockets = [];
40✔
450

451
        // Iterate over the server list
452
        foreach ($this->servers as $server_id => $server) {
40✔
453
            // @var $server \GameQ\Server
454

455
            // Invoke the beforeSend method
456
            $server->protocol()->beforeSend($server);
40✔
457

458
            // Get all the non-challenge packets we need to send
459
            $packets = $server->protocol()->getPacket('!' . Protocol::PACKET_CHALLENGE);
40✔
460

461
            if (count($packets) == 0) {
40✔
462
                // Skip nothing else to do for some reason.
463
                continue;
×
464
            }
465

466
            // Try to use an existing socket
467
            if (($socket = $server->socketGet()) === null) {
40✔
468
                // Let's make a clone of the query class
469
                $socket = clone $this->query;
40✔
470

471
                // Set the information for this query socket
472
                $socket->set(
40✔
473
                    $server->protocol()->transport(),
40✔
474
                    $server->ip,
40✔
475
                    $server->port_query,
40✔
476
                    $this->timeout
40✔
477
                );
35✔
478
            }
479

480
            try {
481
                // Iterate over all the packets we need to send
482
                foreach ($packets as $packet_data) {
40✔
483
                    // Now write the packet to the socket.
484
                    $socket->write($packet_data);
40✔
485

486
                    // Let's sleep shortly so we are not hammering out calls rapid fire style
487
                    usleep($this->write_wait);
×
488
                }
489

490
                unset($packets);
×
491

492
                // Add the socket information so we can reference it easily
493
                $sockets[(int)$socket->get()] = [
×
494
                    'server_id' => $server_id,
×
495
                    'socket'    => $socket,
×
496
                ];
497
            } catch (QueryException $exception) {
40✔
498
                // Check to see if we are in debug, if so bubble up the exception
499
                if ($this->debug) {
40✔
500
                    throw new \Exception($exception->getMessage(), $exception->getCode(), $exception);
×
501
                }
502

503
                continue;
40✔
504
            }
505

506
            // Clean up the sockets, if any left over
507
            $server->socketCleanse();
×
508
        }
509

510
        // Now we need to listen for and grab response(s)
511
        $responses = call_user_func_array(
40✔
512
            [$this->query, 'getResponses'],
40✔
513
            [$sockets, $this->timeout, $this->stream_timeout]
40✔
514
        );
35✔
515

516
        // Iterate over the responses
517
        foreach ($responses as $socket_id => $response) {
40✔
518
            // Back out the server_id
519
            $server_id = $sockets[$socket_id]['server_id'];
×
520

521
            // Grab the server instance
522
            // @var $server \GameQ\Server
523
            $server = $this->servers[$server_id];
×
524

525
            // Save the response from this packet
526
            $server->protocol()->packetResponse($response);
×
527

528
            unset($server);
×
529
        }
530

531
        // Now we need to close all of the sockets
532
        foreach ($sockets as $socketInfo) {
40✔
533
            // @var $socket \GameQ\Query\Core
534
            $socket = $socketInfo['socket'];
×
535

536
            // Close the socket
537
            $socket->close();
×
538

539
            unset($socket);
×
540
        }
541

542
        unset($sockets);
40✔
543
    }
5✔
544

545
    /**
546
     * Parse the response for a specific server
547
     *
548
     * @param \GameQ\Server $server
549
     *
550
     * @return array
551
     * @throws \Exception
552
     */
553
    protected function doParseResponse(Server $server)
2,544✔
554
    {
555
        try {
556
            // @codeCoverageIgnoreStart
557
            // We want to save this server's response to a file (useful for unit testing)
558
            if (!is_null($this->capture_packets_file)) {
559
                file_put_contents(
560
                    $this->capture_packets_file,
561
                    implode(PHP_EOL . '||' . PHP_EOL, $server->protocol()->packetResponse())
562
                );
563
            }
564
            // @codeCoverageIgnoreEnd
565

566
            // Get the server response
567
            $results = $server->protocol()->processResponse();
2,544✔
568

569
            // Check for online before we do anything else
570
            $results['gq_online'] = (count($results) > 0);
2,208✔
571
        } catch (ProtocolException $e) {
352✔
572
            // Check to see if we are in debug, if so bubble up the exception
573
            if ($this->debug) {
352✔
574
                throw new \Exception($e->getMessage(), $e->getCode(), $e);
192✔
575
            }
576

577
            // We ignore this server
578
            $results = [
140✔
579
                'gq_online' => false,
160✔
580
            ];
140✔
581
        }
582

583
        // Now add some default stuff
584
        $results['gq_address'] = (isset($results['gq_address'])) ? $results['gq_address'] : $server->ip();
2,360✔
585
        $results['gq_port_client'] = $server->portClient();
2,360✔
586
        $results['gq_port_query'] = (isset($results['gq_port_query'])) ? $results['gq_port_query'] : $server->portQuery();
2,360✔
587
        $results['gq_protocol'] = $server->protocol()->getProtocol();
2,360✔
588
        $results['gq_type'] = (string)$server->protocol();
2,360✔
589
        $results['gq_name'] = $server->protocol()->nameLong();
2,360✔
590
        $results['gq_transport'] = $server->protocol()->transport();
2,360✔
591

592
        // Process the join link
593
        if (!isset($results['gq_joinlink']) || empty($results['gq_joinlink'])) {
2,360✔
594
            $results['gq_joinlink'] = $server->getJoinLink();
2,360✔
595
        }
596

597
        return $results;
2,360✔
598
    }
599

600
    /**
601
     * Apply any filters to the results
602
     *
603
     * @param array         $results
604
     * @param \GameQ\Server $server
605
     *
606
     * @return array
607
     */
608
    protected function doApplyFilters(array $results, Server $server)
56✔
609
    {
610
        // Loop over the filters
611
        foreach ($this->options['filters'] as $filterOptions) {
56✔
612
            // Try to do this filter
613
            try {
614
                // Make a new reflection class
615
                $class = new \ReflectionClass(sprintf('GameQ\\Filters\\%s', ucfirst($filterOptions['filter'])));
56✔
616

617
                // Create a new instance of the filter class specified
618
                $filter = $class->newInstanceArgs([$filterOptions['options']]);
48✔
619

620
                // Apply the filter to the data
621
                $results = $filter->apply($results, $server);
48✔
622
            } catch (\ReflectionException $exception) {
8✔
623
                // Invalid, skip it
624
                continue;
8✔
625
            }
626
        }
627

628
        return $results;
56✔
629
    }
630
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc