Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
19.79% covered (danger)
19.79%
19 / 96
25.00% covered (danger)
25.00%
2 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Gamespy2
19.79% covered (danger)
19.79%
19 / 96
25.00% covered (danger)
25.00%
2 / 8
952.24
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 query_server
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
 query
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
1
 getProtocolName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getVersion
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 processDetails
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
72
 setDetail
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
72
 processPlayers
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
210
1<?php declare(strict_types=1);
2
3/**
4 * Clansuite Server Query
5 *
6 * SPDX-FileCopyrightText: 2003-2025 Jens A. Koch
7 * SPDX-License-Identifier: MIT
8 *
9 * For the full copyright and license information, please view
10 * the LICENSE file that was distributed with this source code.
11 */
12
13namespace Clansuite\ServerQuery\ServerProtocols;
14
15use function is_int;
16use function is_string;
17use function ord;
18use function strlen;
19use Clansuite\Capture\Protocol\ProtocolInterface;
20use Clansuite\Capture\ServerAddress;
21use Clansuite\Capture\ServerInfo;
22use Clansuite\ServerQuery\CSQuery;
23use Override;
24
25/**
26 * GameSpy2 protocol implementation.
27 */
28class Gamespy2 extends CSQuery implements ProtocolInterface
29{
30    /**
31     * Protocol name.
32     */
33    public string $name = 'Gamespy2';
34
35    /**
36     * List of supported games.
37     *
38     * @var array<string>
39     */
40    public array $supportedGames = ['Gamespy2', 'Halo'];
41
42    /**
43     * Protocol identifier.
44     */
45    public string $protocol = 'Gamespy2';
46
47    /**
48     * Constructor.
49     */
50    public function __construct(mixed $address = null, mixed $queryport = null)
51    {
52        parent::__construct(is_string($address) ? $address : null, is_int($queryport) ? $queryport : null);
53    }
54
55    /**
56     * query_server method.
57     */
58    #[Override]
59    public function query_server(bool $getPlayers = true, bool $getRules = true): bool
60    {
61        if ($this->online) {
62            $this->reset();
63        }
64
65        // Send details query
66        $command = "\xFE\xFD\x00\x43\x4F\x52\x59\xFF\x00\x00";
67
68        if (($result = $this->sendCommand($this->address ?? '', $this->queryport ?? 0, $command)) === '' || ($result = $this->sendCommand($this->address ?? '', $this->queryport ?? 0, $command)) === '0' || ($result = $this->sendCommand($this->address ?? '', $this->queryport ?? 0, $command)) === false) {
69            return false;
70        }
71
72        $this->online = true;
73
74        // Process details response
75        $this->processDetails($result);
76        // Send players query
77        $command = "\xFE\xFD\x00\x43\x4F\x52\x58\x00\xFF\xFF";
78
79        if (($result = $this->sendCommand($this->address ?? '', $this->queryport ?? 0, $command)) !== false) {
80            $this->processPlayers($result);
81        }
82
83        return true;
84    }
85
86    /**
87     * query method.
88     */
89    #[Override]
90    public function query(ServerAddress $addr): ServerInfo
91    {
92        $this->address   = $addr->ip;
93        $this->queryport = $addr->port;
94        $this->query_server(true, true);
95
96        return new ServerInfo(
97            address: $this->address,
98            queryport: $this->queryport,
99            online: $this->online,
100            gamename: $this->gamename,
101            gameversion: $this->gameversion,
102            servertitle: $this->servertitle,
103            mapname: $this->mapname,
104            gametype: $this->gametype,
105            numplayers: $this->numplayers,
106            maxplayers: $this->maxplayers,
107            rules: $this->rules,
108            players: $this->players,
109            errstr: $this->errstr,
110        );
111    }
112
113    /**
114     * getProtocolName method.
115     */
116    #[Override]
117    public function getProtocolName(): string
118    {
119        return $this->protocol;
120    }
121
122    /**
123     * getVersion method.
124     */
125    #[Override]
126    public function getVersion(ServerInfo $info): string
127    {
128        return $info->gameversion ?? 'unknown';
129    }
130
131    private function processDetails(string $buffer): void
132    {
133        $i   = 5; // Skip header
134        $len = strlen($buffer);
135
136        while ($i < $len) {
137            $key = '';
138
139            while ($i < $len && ord($buffer[$i]) !== 0) {
140                $key .= $buffer[$i];
141                $i++;
142            }
143            $i++; // Skip null
144
145            if ($key === '' || $key === '0') {
146                break;
147            }
148
149            $value = '';
150
151            while ($i < $len && ord($buffer[$i]) !== 0) {
152                $value .= $buffer[$i];
153                $i++;
154            }
155            $i++; // Skip null
156
157            $this->setDetail($key, $value);
158        }
159    }
160
161    private function setDetail(string $key, string $value): void
162    {
163        switch ($key) {
164            case 'hostname':
165                $this->servertitle = $value;
166
167                break;
168
169            case 'mapname':
170                $this->mapname = $value;
171
172                break;
173
174            case 'gametype':
175                $this->gametype = $value;
176
177                break;
178
179            case 'maxplayers':
180                $this->maxplayers = (int) $value;
181
182                break;
183
184            case 'numplayers':
185                $this->numplayers = (int) $value;
186
187                break;
188
189            case 'gamever':
190                $this->gameversion = $value;
191
192                break;
193
194            default:
195                $this->rules[$key] = $value;
196
197                break;
198        }
199    }
200
201    private function processPlayers(string $buffer): void
202    {
203        $i   = 6; // Skip header and count byte
204        $len = strlen($buffer);
205
206        // Skip player count
207        if ($i < $len) {
208            $i++;
209        }
210
211        // Read variable names
212        $varNames = [];
213
214        while ($i < $len) {
215            $var = '';
216
217            while ($i < $len && ord($buffer[$i]) !== 0) {
218                $var .= $buffer[$i];
219                $i++;
220            }
221            $i++; // Skip null
222
223            if ($var === '' || $var === '0') {
224                break;
225            }
226
227            $varNames[] = $var;
228        }
229
230        // Read player data
231        $this->players = [];
232
233        while ($i < $len - 4) {
234            $player = [];
235
236            foreach ($varNames as $varName) {
237                $value = '';
238
239                while ($i < $len && ord($buffer[$i]) !== 0) {
240                    $value .= $buffer[$i];
241                    $i++;
242                }
243                $i++; // Skip null
244
245                $player[$varName] = $value;
246            }
247
248            if ($player !== []) {
249                $this->players[] = $player;
250            }
251
252            if ($i >= $len || ord($buffer[$i]) === 0) {
253                break;
254            }
255        }
256    }
257}