Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
1.37% covered (danger)
1.37%
1 / 73
16.67% covered (danger)
16.67%
1 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Gamespy
1.37% covered (danger)
1.37%
1 / 73
16.67% covered (danger)
16.67%
1 / 6
953.05
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 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 query
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
2
 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
 processStatus
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
420
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 array_values;
16use function count;
17use function explode;
18use function is_int;
19use function is_numeric;
20use function is_string;
21use function max;
22use function preg_match;
23use function substr;
24use Clansuite\Capture\Protocol\ProtocolInterface;
25use Clansuite\Capture\ServerAddress;
26use Clansuite\Capture\ServerInfo;
27use Clansuite\ServerQuery\CSQuery;
28use Override;
29
30/**
31 * GameSpy protocol implementation (version 1).
32 *
33 * Used by older games like Unreal Tournament.
34 */
35class Gamespy extends CSQuery implements ProtocolInterface
36{
37    /**
38     * Protocol name.
39     */
40    public string $name = 'Gamespy';
41
42    /**
43     * List of supported games.
44     *
45     * @var array<string>
46     */
47    public array $supportedGames = ['Gamespy'];
48
49    /**
50     * Protocol identifier.
51     */
52    public string $protocol = 'Gamespy';
53
54    /**
55     * Constructor.
56     */
57    public function __construct(mixed $address = null, mixed $queryport = null)
58    {
59        parent::__construct(is_string($address) ? $address : null, is_int($queryport) ? $queryport : null);
60    }
61
62    /**
63     * query_server method.
64     */
65    #[Override]
66    public function query_server(bool $getPlayers = true, bool $getRules = true): bool
67    {
68        if ($this->online) {
69            $this->reset();
70        }
71        $address   = $this->address ?? '';
72        $queryport = $this->queryport ?? 0;
73
74        // Send status query
75        $command = "\x5C\x73\x74\x61\x74\x75\x73\x5C";
76
77        if (($result = $this->sendCommand($address, $queryport, $command)) === '' || ($result = $this->sendCommand($address, $queryport, $command)) === '0' || ($result = $this->sendCommand($address, $queryport, $command)) === false) {
78            return false;
79        }
80
81        $this->online = true;
82
83        // Process status response
84        $this->processStatus($result);
85
86        return true;
87    }
88
89    /**
90     * query method.
91     */
92    #[Override]
93    public function query(ServerAddress $addr): ServerInfo
94    {
95        $this->address   = $addr->ip;
96        $this->queryport = $addr->port;
97        $this->query_server(true, true);
98
99        return new ServerInfo(
100            address: $this->address,
101            queryport: $this->queryport,
102            online: $this->online,
103            gamename: $this->gamename,
104            gameversion: $this->gameversion,
105            servertitle: $this->servertitle,
106            mapname: $this->mapname,
107            gametype: $this->gametype,
108            numplayers: $this->numplayers,
109            maxplayers: $this->maxplayers,
110            rules: $this->rules,
111            players: $this->players,
112            errstr: $this->errstr,
113        );
114    }
115
116    /**
117     * getProtocolName method.
118     */
119    #[Override]
120    public function getProtocolName(): string
121    {
122        return $this->protocol;
123    }
124
125    /**
126     * getVersion method.
127     */
128    #[Override]
129    public function getVersion(ServerInfo $info): string
130    {
131        return $info->gameversion ?? 'unknown';
132    }
133
134    /**
135     * _processStatus method.
136     */
137    protected function processStatus(string $buffer): void
138    {
139        // Skip the first \ if present
140        if ($buffer !== '' && $buffer[0] === '\\') {
141            $buffer = substr($buffer, 1);
142        }
143
144        // Explode on \
145        $data = explode('\\', $buffer);
146
147        $itemCount = count($data);
148
149        // Process key-value pairs
150        $this->rules   = [];
151        $this->players = [];
152        $numPlayers    = 0;
153
154        $x = 0;
155
156        while ($x + 1 < $itemCount) {
157            $key = $data[$x] ?? '';
158            $val = $data[$x + 1] ?? '';
159            $x += 2;
160
161            // Check for player data
162            if (preg_match('/^player_(\d+)$/', $key, $matches) === 1) {
163                $playerIndex = (int) $matches[1];
164
165                if (!isset($this->players[$playerIndex])) {
166                    $this->players[$playerIndex] = [];
167                }
168                $this->players[$playerIndex]['name'] = $val;
169                $numPlayers                          = max($numPlayers, $playerIndex + 1);
170            } elseif (preg_match('/^frags_(\d+)$/', $key, $matches) === 1) {
171                $playerIndex = (int) $matches[1];
172
173                if (!isset($this->players[$playerIndex])) {
174                    $this->players[$playerIndex] = [];
175                }
176                $this->players[$playerIndex]['score'] = is_numeric($val) ? (int) $val : 0;
177            } elseif (preg_match('/^ping_(\d+)$/', $key, $matches) === 1) {
178                $playerIndex = (int) $matches[1];
179
180                if (!isset($this->players[$playerIndex])) {
181                    $this->players[$playerIndex] = [];
182                }
183                $this->players[$playerIndex]['ping'] = is_numeric($val) ? (int) $val : 0;
184            } elseif ($key === 'hostname') {
185                $this->servertitle = $val;
186            } elseif ($key === 'mapname') {
187                $this->mapname = $val;
188            } elseif ($key === 'gametype') {
189                $this->gametype = $val;
190            } elseif ($key === 'numplayers') {
191                $this->numplayers = is_numeric($val) ? (int) $val : 0;
192            } elseif ($key === 'maxplayers') {
193                $this->maxplayers = is_numeric($val) ? (int) $val : 0;
194            } elseif ($key === 'gamever') {
195                $this->gameversion = $val;
196            } else {
197                $this->rules[$key] = $val;
198            }
199        }
200
201        // Reindex players
202        $this->players = array_values($this->players);
203    }
204}