Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
2.86% covered (danger)
2.86%
3 / 105
12.50% covered (danger)
12.50%
1 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Unreal2
2.86% covered (danger)
2.86%
3 / 105
12.50% covered (danger)
12.50%
1 / 8
911.96
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 query_server
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
56
 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
 processDetails
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
90
 processRules
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 processPlayers
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
90
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_array;
16use function ord;
17use function substr;
18use function unpack;
19use Clansuite\Capture\Protocol\ProtocolInterface;
20use Clansuite\Capture\ServerAddress;
21use Clansuite\Capture\ServerInfo;
22use Clansuite\ServerQuery\CSQuery;
23use Override;
24
25/**
26 * Unreal 2 Protocol implementation.
27 *
28 * Base protocol for Unreal Engine 2 games like Killing Floor.
29 */
30class Unreal2 extends CSQuery implements ProtocolInterface
31{
32    /**
33     * Protocol name.
34     */
35    public string $name = 'Unreal2';
36
37    /**
38     * List of supported games.
39     *
40     * @var array<string>
41     */
42    public array $supportedGames = ['Unreal2', 'KillingFloor'];
43
44    /**
45     * Protocol identifier.
46     */
47    public string $protocol = 'Unreal2';
48
49    /**
50     * Game series.
51     *
52     * @var array<string>
53     */
54    public array $game_series_list = ['Unreal Tournament'];
55    public ?int $serverID          = null;
56
57    /**
58     * Constructor.
59     */
60    public function __construct(?string $address = null, ?int $queryport = null)
61    {
62        parent::__construct();
63        $this->address   = $address;
64        $this->queryport = $queryport;
65    }
66
67    /**
68     * query_server method.
69     */
70    #[Override]
71    public function query_server(bool $getPlayers = true, bool $getRules = true): bool
72    {
73        if ($this->online) {
74            $this->reset();
75        }
76
77        $address = (string) $this->address;
78        $port    = (int) $this->queryport;
79
80        // Send details query
81        $command = "\x79\x00\x00\x00\x00"; // Details packet
82
83        if (($result = $this->sendCommand($address, $port, $command)) === '' || ($result = $this->sendCommand($address, $port, $command)) === '0' || ($result = $this->sendCommand($address, $port, $command)) === false) {
84            return false;
85        }
86
87        $this->processDetails($result);
88        // Send rules query if requested
89        $command = "\x79\x00\x00\x00\x01";
90
91        // Rules packet
92        if (($result = $this->sendCommand($address, $port, $command)) !== false) {
93            $this->processRules($result);
94        }
95        // Send players query if requested
96        $command = "\x79\x00\x00\x00\x02";
97
98        // Players packet
99        if (($result = $this->sendCommand($address, $port, $command)) !== false) {
100            $this->processPlayers($result);
101        }
102
103        $this->online = true;
104
105        return true;
106    }
107
108    /**
109     * query method.
110     */
111    #[Override]
112    public function query(ServerAddress $addr): ServerInfo
113    {
114        $this->address   = $addr->ip;
115        $this->queryport = $addr->port;
116        $this->query_server(true, true);
117
118        return new ServerInfo(
119            address: $this->address,
120            queryport: $this->queryport,
121            online: $this->online,
122            gamename: $this->gamename,
123            gameversion: $this->gameversion,
124            servertitle: $this->servertitle,
125            mapname: $this->mapname,
126            gametype: $this->gametype,
127            numplayers: $this->numplayers,
128            maxplayers: $this->maxplayers,
129            rules: $this->rules,
130            players: $this->players,
131            errstr: $this->errstr,
132        );
133    }
134
135    /**
136     * getProtocolName method.
137     */
138    #[Override]
139    public function getProtocolName(): string
140    {
141        return $this->protocol;
142    }
143
144    /**
145     * getVersion method.
146     */
147    #[Override]
148    public function getVersion(ServerInfo $info): string
149    {
150        return $info->gameversion ?? 'unknown';
151    }
152
153    /**
154     * _processDetails method.
155     */
156    protected function processDetails(string $data): void
157    {
158        // Skip header (5 bytes)
159        $data = substr($data, 5);
160
161        // Server ID (4 bytes)
162        $tmp = @unpack('V', substr($data, 0, 4));
163
164        if (!is_array($tmp) || !isset($tmp[1])) {
165            // leave serverID null on malformed response
166            $this->serverID = null;
167        } else {
168            $this->serverID = $tmp[1];
169        }
170        $data = substr($data, 4);
171
172        // Server IP (pascal string, skip)
173        $len  = ord($data[0] ?? "\0");
174        $data = substr($data, 1 + $len);
175
176        // Game port (4 bytes)
177        $tmp = @unpack('V', substr($data, 0, 4));
178
179        if (!is_array($tmp) || !isset($tmp[1])) {
180            // malformed, default to 0
181            $this->hostport = 0;
182        } else {
183            $this->hostport = (int) $tmp[1];
184        }
185        $data = substr($data, 4);
186
187        // Query port (4 bytes, skip)
188        $data = substr($data, 4);
189
190        // Server name (pascal string)
191        $len               = ord($data[0] ?? "\0");
192        $this->servertitle = substr($data, 1, $len);
193        $data              = substr($data, 1 + $len);
194
195        // Map name (pascal string)
196        $len           = ord($data[0] ?? "\0");
197        $this->mapname = substr($data, 1, $len);
198        $data          = substr($data, 1 + $len);
199
200        // Game type (pascal string)
201        $len            = ord($data[0] ?? "\0");
202        $this->gametype = substr($data, 1, $len);
203        $data           = substr($data, 1 + $len);
204
205        // Num players (4 bytes)
206        $tmp = @unpack('V', substr($data, 0, 4));
207
208        if (!is_array($tmp) || !isset($tmp[1])) {
209            $this->numplayers = 0;
210        } else {
211            $this->numplayers = (int) $tmp[1];
212        }
213        $data = substr($data, 4);
214
215        // Max players (4 bytes)
216        $tmp = @unpack('V', substr($data, 0, 4));
217
218        if (!is_array($tmp) || !isset($tmp[1])) {
219            $this->maxplayers = 0;
220        } else {
221            $this->maxplayers = (int) $tmp[1];
222        }
223
224        // Ping (4 bytes, skip)
225        // $data = substr($data, 4);
226    }
227
228    /**
229     * _processRules method.
230     */
231    protected function processRules(string $data): void
232    {
233        // Skip header (5 bytes)
234        $data = substr($data, 5);
235
236        while ($data !== '') {
237            // Key (pascal string)
238            $len  = ord($data[0] ?? "\0");
239            $key  = substr($data, 1, $len);
240            $data = substr($data, 1 + $len);
241
242            // Value (pascal string)
243            $len   = ord($data[0] ?? "\0");
244            $value = substr($data, 1, $len);
245            $data  = substr($data, 1 + $len);
246
247            $this->rules[$key] = $value;
248        }
249    }
250
251    /**
252     * _processPlayers method.
253     */
254    protected function processPlayers(string $data): void
255    {
256        // Skip header (5 bytes)
257        $data = substr($data, 5);
258
259        $this->players = [];
260
261        while ($data !== '') {
262            // Player ID (4 bytes)
263            $tmp = @unpack('V', substr($data, 0, 4));
264
265            if (!is_array($tmp) || !isset($tmp[1])) {
266                // malformed, stop parsing players
267                break;
268            }
269            $id   = (int) $tmp[1];
270            $data = substr($data, 4);
271
272            if ($id === 0) {
273                break; // End of players
274            }
275
276            // Player name (pascal string)
277            $len  = ord($data[0] ?? "\0");
278            $name = substr($data, 1, $len);
279            $data = substr($data, 1 + $len);
280
281            // Ping (4 bytes)
282            $tmp  = @unpack('V', substr($data, 0, 4));
283            $ping = (is_array($tmp) && isset($tmp[1])) ? (int) $tmp[1] : 0;
284            $data = substr($data, 4);
285
286            // Score (4 bytes)
287            $tmp   = @unpack('V', substr($data, 0, 4));
288            $score = (is_array($tmp) && isset($tmp[1])) ? (int) $tmp[1] : 0;
289            $data  = substr($data, 4);
290
291            // Skip 4 unknown bytes
292            $data = substr($data, 4);
293
294            $this->players[] = [
295                'name'  => $name,
296                'ping'  => $ping,
297                'score' => $score,
298            ];
299        }
300    }
301}