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:
13: namespace Clansuite\ServerQuery\ServerProtocols;
14:
15: use function count;
16: use function file_get_contents;
17: use function is_array;
18: use function is_int;
19: use function is_numeric;
20: use function is_string;
21: use function json_decode;
22: use function random_int;
23: use function stream_context_create;
24: use Clansuite\Capture\Protocol\ProtocolInterface;
25: use Clansuite\Capture\ServerAddress;
26: use Clansuite\Capture\ServerInfo;
27: use Clansuite\ServerQuery\CSQuery;
28: use Override;
29:
30: /**
31: * Assetto Corsa protocol implementation.
32: *
33: * Uses HTTP requests to server endpoints.
34: */
35: class 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: }
256: