Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.69% covered (success)
96.69%
117 / 121
33.33% covered (danger)
33.33%
2 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Ase
96.69% covered (success)
96.69%
117 / 121
33.33% covered (danger)
33.33%
2 / 6
38
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 query_server
98.78% covered (success)
98.78%
81 / 82
0.00% covered (danger)
0.00%
0 / 1
28
 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
 readLengthPrefixedString
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
4.01
1<?php
2
3declare(strict_types=1);
4
5/**
6 * Clansuite Server Query
7 *
8 * SPDX-FileCopyrightText: 2003-2025 Jens A. Koch
9 * SPDX-License-Identifier: MIT
10 *
11 * For the full copyright and license information, please view
12 * the LICENSE file that was distributed with this source code.
13 */
14
15namespace Clansuite\ServerQuery\ServerProtocols;
16
17use function array_key_exists;
18use function max;
19use function ord;
20use function strlen;
21use function strpos;
22use function substr;
23use Clansuite\Capture\Protocol\ProtocolInterface;
24use Clansuite\Capture\ServerAddress;
25use Clansuite\Capture\ServerInfo;
26use Clansuite\ServerQuery\CSQuery;
27use Override;
28
29/**
30 * All-Seeing Eye protocol implementation.
31 *
32 * Used for Multi Theft Auto and other ASE-based games.
33 */
34class Ase extends CSQuery implements ProtocolInterface
35{
36    /**
37     * Protocol name.
38     */
39    public string $name = 'All-Seeing Eye';
40
41    /**
42     * List of supported games.
43     *
44     * @var array<string>
45     */
46    public array $supportedGames = ['Multi Theft Auto'];
47
48    /**
49     * Protocol identifier.
50     */
51    public string $protocol = 'ASE';
52
53    /**
54     * Game series.
55     *
56     * @var array<string>
57     */
58    public array $game_series_list = ['ASE'];
59
60    /**
61     * Constructor.
62     */
63    public function __construct(?string $address = null, ?int $queryport = null)
64    {
65        parent::__construct();
66
67        if ($address !== null) {
68            $this->address = $address;
69        }
70
71        if ($queryport !== null) {
72            $this->queryport = $queryport;
73        }
74    }
75
76    /**
77     * query_server method.
78     */
79    #[Override]
80    public function query_server(bool $getPlayers = true, bool $getRules = true): bool
81    {
82        if ($this->online) {
83            $this->reset();
84        }
85
86        $command = 's';
87
88        $address = $this->address ?? '';
89        $port    = $this->queryport ?? 0;
90
91        if (($result = $this->sendCommand($address, $port, $command)) === '' || ($result = $this->sendCommand($address, $port, $command)) === '0' || ($result = $this->sendCommand($address, $port, $command)) === false) {
92            $this->errstr = 'No reply received';
93
94            return false;
95        }
96
97        // Check for valid response
98        if (strlen($result) < 4) {
99            $this->errstr = 'Response too short';
100
101            return false;
102        }
103
104        // Read the header
105        $header = substr($result, 0, 4);
106
107        if ($header !== 'EYE1') {
108            $this->errstr = 'Invalid header';
109
110            return false;
111        }
112
113        $buffer = substr($result, 4);
114
115        // Read fixed header fields
116        $this->rules = [];
117        $gamename    = $this->readLengthPrefixedString($buffer);
118        $port        = $this->readLengthPrefixedString($buffer);
119        $servername  = $this->readLengthPrefixedString($buffer);
120        $gametype    = $this->readLengthPrefixedString($buffer);
121        $map         = $this->readLengthPrefixedString($buffer);
122        $version     = $this->readLengthPrefixedString($buffer);
123        $password    = $this->readLengthPrefixedString($buffer);
124        $num_players = $this->readLengthPrefixedString($buffer);
125        $max_players = $this->readLengthPrefixedString($buffer);
126
127        // Populate initial rule set from those fields so callers can access them via rules
128        if ($gamename !== '') {
129            $this->rules['gamename'] = $gamename;
130        }
131
132        if ($port !== '') {
133            $this->rules['port'] = $port;
134        }
135
136        if ($servername !== '') {
137            $this->rules['hostname'] = $servername;
138        }
139
140        if ($gametype !== '') {
141            $this->rules['gametype'] = $gametype;
142        }
143
144        if ($map !== '') {
145            $this->rules['map'] = $map;
146        }
147
148        if ($version !== '') {
149            $this->rules['version'] = $version;
150        }
151
152        if ($password !== '') {
153            $this->rules['password'] = $password;
154        }
155
156        if ($num_players !== '') {
157            $this->rules['num_players'] = $num_players;
158        }
159
160        if ($max_players !== '') {
161            $this->rules['max_players'] = $max_players;
162        }
163
164        // Default dedicated flag
165        if (!array_key_exists('dedicated', $this->rules)) {
166            $this->rules['dedicated'] = 1;
167        }
168
169        // Parse remaining key/value pairs
170        while ($buffer !== '') {
171            $key = $this->readLengthPrefixedString($buffer);
172
173            if ($key === '' || $key === '0') {
174                break;
175            }
176            $value             = $this->readLengthPrefixedString($buffer);
177            $this->rules[$key] = $value;
178        }
179
180        // Parse players
181        $this->players = [];
182
183        while ($buffer !== '') {
184            $flags  = ord($buffer[0]);
185            $buffer = substr($buffer, 1);
186
187            $player = [];
188
189            if (($flags & 1) !== 0) {
190                $player['name'] = $this->readLengthPrefixedString($buffer);
191            }
192
193            if (($flags & 2) !== 0) {
194                $player['team'] = $this->readLengthPrefixedString($buffer);
195            }
196
197            if (($flags & 4) !== 0) {
198                $player['skin'] = $this->readLengthPrefixedString($buffer);
199            }
200
201            if (($flags & 8) !== 0) {
202                $player['score'] = $this->readLengthPrefixedString($buffer);
203            }
204
205            if (($flags & 16) !== 0) {
206                $player['ping'] = $this->readLengthPrefixedString($buffer);
207            }
208
209            if (($flags & 32) !== 0) {
210                $player['time'] = $this->readLengthPrefixedString($buffer);
211            }
212
213            if ($player !== []) {
214                $this->players[] = $player;
215            }
216        }
217
218        // Set info from rules
219        $this->gamename    = (string) ($this->rules['gamename'] ?? '');
220        $this->gametype    = (string) ($this->rules['gametype'] ?? '');
221        $this->mapname     = (string) ($this->rules['map'] ?? '');
222        $this->gameversion = (string) ($this->rules['version'] ?? '');
223        $this->servertitle = (string) ($this->rules['hostname'] ?? '');
224        $this->numplayers  = (int) ($this->rules['num_players'] ?? 0);
225        $this->maxplayers  = (int) ($this->rules['max_players'] ?? 0);
226        $this->password    = (int) ($this->rules['password'] ?? 0);
227        $this->hostport    = (int) ($this->rules['port'] ?? 0);
228
229        $this->online = true;
230
231        return true;
232    }
233
234    /**
235     * query method.
236     */
237    #[Override]
238    public function query(ServerAddress $addr): ServerInfo
239    {
240        $this->address   = $addr->ip;
241        $this->queryport = $addr->port;
242
243        $this->query_server();
244
245        $info              = new ServerInfo;
246        $info->address     = $addr->ip;
247        $info->queryport   = $addr->port;
248        $info->online      = $this->online;
249        $info->gamename    = $this->gamename;
250        $info->gameversion = $this->gameversion;
251        $info->servertitle = $this->servertitle;
252        $info->mapname     = $this->mapname;
253        $info->gametype    = $this->gametype;
254        $info->numplayers  = $this->numplayers;
255        $info->maxplayers  = $this->maxplayers;
256        $info->players     = $this->players;
257        $info->rules       = $this->rules;
258        $info->errstr      = $this->errstr;
259
260        return $info;
261    }
262
263    /**
264     * getProtocolName method.
265     */
266    #[Override]
267    public function getProtocolName(): string
268    {
269        return $this->protocol;
270    }
271
272    /**
273     * getVersion method.
274     */
275    #[Override]
276    public function getVersion(ServerInfo $info): string
277    {
278        return $info->gameversion ?? 'unknown';
279    }
280
281    private function readLengthPrefixedString(string &$buffer): string
282    {
283        // If buffer is empty, return empty string
284        if (!isset($buffer[0])) {
285            return '';
286        }
287
288        $length = ord($buffer[0]);
289        // consume length byte
290        $buffer = substr($buffer, 1);
291
292        // the strings include an extra trailing byte; callers expect length-1 content
293        $readLen = max($length - 1, 0);
294
295        // if declared read length is larger than remaining buffer, clamp it
296        $remaining = strlen($buffer);
297
298        if ($readLen > $remaining) {
299            $readLen = $remaining;
300        }
301
302        $string  = substr($buffer, 0, $readLen);
303        $nullPos = strpos($string, "\0");
304
305        if ($nullPos !== false) {
306            $string = substr($string, 0, $nullPos);
307        }
308
309        // advance buffer to the next length-prefixed string
310        $buffer = substr($buffer, $readLen);
311
312        return $string;
313    }
314}