Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.94% covered (success)
92.94%
79 / 85
37.50% covered (danger)
37.50%
3 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
AssettoCorsa
92.94% covered (success)
92.94%
79 / 85
37.50% covered (danger)
37.50%
3 / 8
42.62
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
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
6.02
 query_server
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
3
 getProtocolName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getVersion
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 queryEndpoint
89.47% covered (warning)
89.47%
17 / 19
0.00% covered (danger)
0.00%
0 / 1
6.04
 parseServerInfo
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
12.07
 parseCarInfo
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
10.02
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 count;
16use function file_get_contents;
17use function is_array;
18use function is_int;
19use function is_numeric;
20use function is_string;
21use function json_decode;
22use function random_int;
23use function stream_context_create;
24use Clansuite\Capture\Protocol\ProtocolInterface;
25use Clansuite\Capture\ServerAddress;
26use Clansuite\Capture\ServerInfo;
27use Clansuite\ServerQuery\CSQuery;
28use Override;
29
30/**
31 * Assetto Corsa protocol implementation.
32 *
33 * Uses HTTP requests to server endpoints.
34 */
35class AssettoCorsa extends CSQuery implements ProtocolInterface
36{
37    /**
38     * Protocol name.
39     */
40    public string $name = 'Assetto Corsa';
41
42    /**
43     * List of supported games.
44     *
45     * @var array<string>
46     */
47    public array $supportedGames = ['Assetto Corsa'];
48
49    /**
50     * Protocol identifier.
51     */
52    public string $protocol = 'assettocorsa';
53
54    /**
55     * Constructor.
56     */
57    public function __construct(?string $address = null, ?int $queryport = null)
58    {
59        parent::__construct();
60
61        if ($address !== null) {
62            $this->address = $address;
63        }
64
65        if ($queryport !== null) {
66            $this->queryport = $queryport;
67        }
68    }
69
70    /**
71     * query method.
72     */
73    #[Override]
74    public function query(ServerAddress $addr): ServerInfo
75    {
76        $info         = new ServerInfo;
77        $info->online = false;
78
79        // Query server info
80        $serverInfo = $this->queryEndpoint($addr, '/info');
81
82        if ($serverInfo === null || $serverInfo === []) {
83            return $info;
84        }
85
86        // Query car info
87        $carInfo = $this->queryEndpoint($addr, '/JSON|' . random_int(0, 999999999999999));
88
89        if ($carInfo === null || $carInfo === [] || !isset($carInfo['Cars'])) {
90            return $info;
91        }
92
93        $this->parseServerInfo($serverInfo, $info);
94        $this->parseCarInfo($carInfo, $info);
95        $info->online = true;
96
97        return $info;
98    }
99
100    /**
101     * query_server method.
102     */
103    #[Override]
104    public function query_server(bool $getPlayers = true, bool $getRules = true): bool
105    {
106        $addr = new ServerAddress($this->address ?? '', $this->queryport ?? 0);
107        $info = $this->query($addr);
108
109        $this->online      = $info->online;
110        $this->servertitle = $info->servertitle ?? '';
111        $this->mapname     = $info->mapname ?? '';
112        $this->numplayers  = $info->numplayers;
113        $this->maxplayers  = $info->maxplayers;
114        $this->players     = [];
115
116        foreach ($info->players as $player) {
117            if (is_array($player)) {
118                $this->players[] = [
119                    'name'  => $player['name'] ?? '',
120                    'score' => 0,
121                    'time'  => 0,
122                ];
123            }
124        }
125        $this->numplayers = count($this->players);
126
127        return $this->online;
128    }
129
130    /**
131     * getProtocolName method.
132     */
133    #[Override]
134    public function getProtocolName(): string
135    {
136        return $this->protocol;
137    }
138
139    /**
140     * getVersion method.
141     */
142    #[Override]
143    public function getVersion(ServerInfo $info): string
144    {
145        return $info->version ?? 'unknown';
146    }
147
148    /**
149     * Query a HTTP endpoint and return a decoded JSON array.
150     *
151     * @return null|array<string,mixed>
152     */
153    private function queryEndpoint(ServerAddress $addr, string $path): ?array
154    {
155        $url     = "http://{$addr->ip}:{$addr->port}{$path}";
156        $context = stream_context_create([
157            'http' => [
158                'timeout'    => 5,
159                'user_agent' => 'Clansuite-GameServer-Query/1.0',
160            ],
161        ]);
162
163        $response = @file_get_contents($url, false, $context);
164
165        if ($response === false) {
166            return null;
167        }
168
169        $result = json_decode($response, true);
170
171        // Ensure we have an associative array with string keys
172        if ($result === null || !is_array($result)) {
173            return null;
174        }
175
176        // Normalize keys: if array has non-string keys, try to cast to string-indexed array
177        $assoc = [];
178
179        foreach ($result as $k => $v) {
180            if (!is_string($k)) {
181                // cast numeric keys to string to match signature
182                $k = (string) $k;
183            }
184            $assoc[$k] = $v;
185        }
186
187        return $assoc;
188    }
189
190    /**
191     * @param array<string, mixed> $serverInfo
192     */
193    private function parseServerInfo(array $serverInfo, ServerInfo $info): void
194    {
195        // Parse server info with type checks
196        $name              = $serverInfo['name'] ?? null;
197        $info->servertitle = is_string($name) ? $name : '';
198
199        $track         = $serverInfo['track'] ?? null;
200        $info->mapname = is_string($track) ? $track : '';
201
202        $clients          = $serverInfo['clients'] ?? null;
203        $info->numplayers = is_int($clients) ? $clients : (is_numeric($clients) ? (int) $clients : 0);
204
205        $maxclients       = $serverInfo['maxclients'] ?? null;
206        $info->maxplayers = is_int($maxclients) ? $maxclients : (is_numeric($maxclients) ? (int) $maxclients : 0);
207
208        $poweredBy         = $serverInfo['poweredBy'] ?? null;
209        $info->gameversion = is_string($poweredBy) ? $poweredBy : '';
210
211        // Password not directly available, perhaps set in rules
212        $pass = $serverInfo['pass'] ?? null;
213
214        if ($pass === true || $pass === 1 || $pass === '1' || $pass === 'true') {
215            $info->rules['password'] = true;
216        }
217    }
218
219    /**
220     * @param array<string, mixed> $carInfo
221     */
222    private function parseCarInfo(array $carInfo, ServerInfo $info): void
223    {
224        // Parse car info
225        $info->players = [];
226
227        $cars = $carInfo['Cars'] ?? null;
228
229        if (is_array($cars)) {
230            foreach ($cars as $car) {
231                if (!is_array($car)) {
232                    continue;
233                }
234
235                $isConnected = $car['IsConnected'] ?? null;
236
237                if ($isConnected === true || $isConnected === 1 || $isConnected === '1' || $isConnected === 'true') {
238                    $driver = $car['DriverName'] ?? null;
239                    $name   = is_string($driver) ? $driver : '';
240
241                    $info->players[] = [
242                        'name'  => $name,
243                        'score' => 0,
244                        'time'  => 0,
245                    ];
246                }
247            }
248        }
249
250        // If server info has clients, use that, else count
251        if ($info->numplayers === 0) {
252            $info->numplayers = count($info->players);
253        }
254    }
255}