Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
24.42% covered (danger)
24.42%
21 / 86
44.44% covered (danger)
44.44%
4 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Gamespy3
24.42% covered (danger)
24.42%
21 / 86
44.44% covered (danger)
44.44%
4 / 9
595.56
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 / 21
0.00% covered (danger)
0.00%
0 / 1
90
 query
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
1
 getProtocolName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getVersion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 processResponse
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 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 / 1
0.00% covered (danger)
0.00%
0 / 1
2
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 const PREG_SPLIT_NO_EMPTY;
16use function count;
17use function is_int;
18use function is_string;
19use function ord;
20use function preg_replace;
21use function preg_split;
22use function sprintf;
23use function strlen;
24use function substr;
25use Clansuite\Capture\Protocol\ProtocolInterface;
26use Clansuite\Capture\ServerAddress;
27use Clansuite\Capture\ServerInfo;
28use Clansuite\ServerQuery\CSQuery;
29use Override;
30
31/**
32 * GameSpy3 protocol implementation.
33 */
34class Gamespy3 extends CSQuery implements ProtocolInterface
35{
36    /**
37     * Protocol name.
38     */
39    public string $name = 'Gamespy3';
40
41    /**
42     * List of supported games.
43     *
44     * @var array<string>
45     */
46    public array $supportedGames = ['Gamespy3', 'Just Cause 2 Multiplayer'];
47
48    /**
49     * Protocol identifier.
50     */
51    public string $protocol = 'Gamespy3';
52
53    /**
54     * Constructor.
55     */
56    public function __construct(mixed $address = null, mixed $queryport = null)
57    {
58        parent::__construct(is_string($address) ? $address : null, is_int($queryport) ? $queryport : null);
59    }
60
61    /**
62     * query_server method.
63     */
64    #[Override]
65    public function query_server(bool $getPlayers = true, bool $getRules = true): bool
66    {
67        if ($this->online) {
68            $this->reset();
69        }
70
71        // Send challenge packet
72        $challengePacket = "\xFE\xFD\x09\x10\x20\x30\x40";
73
74        if (($challenge = $this->sendCommand($this->address ?? '', $this->queryport ?? 0, $challengePacket)) === '' || ($challenge = $this->sendCommand($this->address ?? '', $this->queryport ?? 0, $challengePacket)) === '0' || ($challenge = $this->sendCommand($this->address ?? '', $this->queryport ?? 0, $challengePacket)) === false) {
75            return false;
76        }
77
78        // Parse challenge
79        $challenge       = (int) substr((string) preg_replace("/[^0-9\-]/si", '', $challenge), 1);
80        $challengeResult = '';
81
82        if ($challenge !== 0) {
83            $challengeResult = sprintf(
84                '%c%c%c%c',
85                ($challenge >> 24),
86                ($challenge >> 16),
87                ($challenge >> 8),
88                ($challenge >> 0),
89            );
90        }
91
92        // Send main query packet
93        $queryPacket = "\xFE\xFD\x00\x10\x20\x30\x40" . $challengeResult . "\xFF\xFF\xFF\x01";
94
95        if (($result = $this->sendCommand($this->address ?? '', $this->queryport ?? 0, $queryPacket)) === '' || ($result = $this->sendCommand($this->address ?? '', $this->queryport ?? 0, $queryPacket)) === '0' || ($result = $this->sendCommand($this->address ?? '', $this->queryport ?? 0, $queryPacket)) === false) {
96            return false;
97        }
98
99        $this->online = true;
100
101        // Process response
102        $this->processResponse($result);
103
104        return true;
105    }
106
107    /**
108     * query method.
109     */
110    #[Override]
111    public function query(ServerAddress $addr): ServerInfo
112    {
113        $this->address   = $addr->ip;
114        $this->queryport = $addr->port;
115        $this->query_server(true, true);
116
117        return new ServerInfo(
118            address: $this->address,
119            queryport: $this->queryport,
120            online: $this->online,
121            gamename: $this->gamename,
122            gameversion: $this->gameversion,
123            servertitle: $this->servertitle,
124            mapname: $this->mapname,
125            gametype: $this->gametype,
126            numplayers: $this->numplayers,
127            maxplayers: $this->maxplayers,
128            rules: $this->rules,
129            players: $this->players,
130            errstr: $this->errstr,
131        );
132    }
133
134    /**
135     * getProtocolName method.
136     */
137    #[Override]
138    public function getProtocolName(): string
139    {
140        return $this->protocol;
141    }
142
143    /**
144     * getVersion method.
145     */
146    #[Override]
147    public function getVersion(ServerInfo $info): string
148    {
149        return $info->gameversion ?? 'unknown';
150    }
151
152    private function processResponse(string $buffer): void
153    {
154        // Split the response at the player/team delimiter
155        $parts = preg_split('/\\x00\\x00\\x01/', $buffer, -1, PREG_SPLIT_NO_EMPTY);
156
157        if ($parts === false) {
158            return;
159        }
160
161        if (count($parts) >= 1) {
162            $this->processDetails($parts[0]);
163        }
164
165        if (count($parts) >= 2) {
166            $this->processPlayers();
167        }
168    }
169
170    private function processDetails(string $buffer): void
171    {
172        $i   = 0;
173        $len = strlen($buffer);
174
175        while ($i < $len) {
176            $key = '';
177
178            while ($i < $len && ord($buffer[$i]) !== 0) {
179                $key .= $buffer[$i];
180                $i++;
181            }
182            $i++; // Skip null
183
184            if ($key === '' || $key === '0') {
185                break;
186            }
187
188            $value = '';
189
190            while ($i < $len && ord($buffer[$i]) !== 0) {
191                $value .= $buffer[$i];
192                $i++;
193            }
194            $i++; // Skip null
195
196            $this->setDetail($key, $value);
197        }
198    }
199
200    private function setDetail(string $key, string $value): void
201    {
202        switch ($key) {
203            case 'hostname':
204                $this->servertitle = $value;
205
206                break;
207
208            case 'mapname':
209                $this->mapname = $value;
210
211                break;
212
213            case 'gametype':
214                $this->gametype = $value;
215
216                break;
217
218            case 'maxplayers':
219                $this->maxplayers = (int) $value;
220
221                break;
222
223            case 'numplayers':
224                $this->numplayers = (int) $value;
225
226                break;
227
228            case 'gamever':
229                $this->gameversion = $value;
230
231                break;
232
233            default:
234                $this->rules[$key] = $value;
235
236                break;
237        }
238    }
239
240    private function processPlayers(): void
241    {
242        // This is a simplified implementation
243        // In a full implementation, we'd parse the complex GameSpy3 player format
244        $this->players = [];
245    }
246}