Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
2.86% covered (danger)
2.86%
3 / 105
11.11% covered (danger)
11.11%
1 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Torque
2.86% covered (danger)
2.86%
3 / 105
11.11% covered (danger)
11.11%
1 / 9
1659.08
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
110
 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
 createPingPacket
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 createInfoPacket
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 processPingResponse
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
210
 processInfoResponse
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
156
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 is_array;
17use function min;
18use function ord;
19use function pack;
20use function strlen;
21use function substr;
22use function unpack;
23use Clansuite\Capture\Protocol\ProtocolInterface;
24use Clansuite\Capture\ServerAddress;
25use Clansuite\Capture\ServerInfo;
26use Clansuite\ServerQuery\CSQuery;
27use Override;
28
29/**
30 * Torque Game Engine protocol implementation.
31 *
32 * Based on Torque Game Engine server query protocol from serverQuery.cc
33 * Used by Tribes 2, Blockland, Age of Time, and other Torque-based games.
34 */
35class Torque extends CSQuery implements ProtocolInterface
36{
37    /**
38     * Protocol name.
39     */
40    public string $name = 'Torque';
41
42    /**
43     * List of supported games.
44     *
45     * @var array<string>
46     */
47    public array $supportedGames = ['Torque'];
48
49    /**
50     * Protocol identifier.
51     */
52    public string $protocol = 'Torque';
53
54    /**
55     * Game series.
56     *
57     * @var array<string>
58     */
59    public array $game_series_list = ['Torque'];
60
61    /**
62     * Constructor.
63     */
64    public function __construct(?string $address = null, ?int $queryport = null)
65    {
66        parent::__construct();
67        $this->address   = $address;
68        $this->queryport = $queryport;
69    }
70
71    /**
72     * query_server method.
73     */
74    #[Override]
75    public function query_server(bool $getPlayers = true, bool $getRules = true): bool
76    {
77        if ($this->online) {
78            $this->reset();
79        }
80
81        $address = (string) $this->address;
82        $port    = (int) $this->queryport;
83
84        // Send ping request first
85        $pingPacket = $this->createPingPacket();
86
87        if (($pingResponse = $this->sendCommand($address, $port, $pingPacket)) === '' || ($pingResponse = $this->sendCommand($address, $port, $pingPacket)) === '0' || ($pingResponse = $this->sendCommand($address, $port, $pingPacket)) === false) {
88            return false;
89        }
90
91        // Parse ping response to get server info
92        if (!$this->processPingResponse($pingResponse)) {
93            return false;
94        }
95
96        // Send info request
97        $infoPacket = $this->createInfoPacket();
98
99        if (($infoResponse = $this->sendCommand($address, $port, $infoPacket)) === '' || ($infoResponse = $this->sendCommand($address, $port, $infoPacket)) === '0' || ($infoResponse = $this->sendCommand($address, $port, $infoPacket)) === false) {
100            return false;
101        }
102
103        // Parse info response
104        if (!$this->processInfoResponse($infoResponse)) {
105            return false;
106        }
107
108        $this->online = true;
109
110        return true;
111    }
112
113    /**
114     * query method.
115     */
116    #[Override]
117    public function query(ServerAddress $addr): ServerInfo
118    {
119        $this->address   = $addr->ip;
120        $this->queryport = $addr->port;
121        $this->query_server(true, true);
122
123        return new ServerInfo(
124            address: $this->address,
125            queryport: $this->queryport,
126            online: $this->online,
127            gamename: $this->gamename,
128            gameversion: $this->gameversion,
129            servertitle: $this->servertitle,
130            mapname: $this->mapname,
131            gametype: $this->gametype,
132            numplayers: $this->numplayers,
133            maxplayers: $this->maxplayers,
134            rules: $this->rules,
135            players: $this->players,
136            errstr: $this->errstr,
137        );
138    }
139
140    /**
141     * getProtocolName method.
142     */
143    #[Override]
144    public function getProtocolName(): string
145    {
146        return $this->protocol;
147    }
148
149    /**
150     * getVersion method.
151     */
152    #[Override]
153    public function getVersion(ServerInfo $info): string
154    {
155        return $info->gameversion ?? 'unknown';
156    }
157
158    private function createPingPacket(): string
159    {
160        // Based on Torque protocol: GamePingRequest packet
161        // Format: packet_type(1) + flags(1) + key(4)
162        $packetType = "\x02"; // GamePingRequest
163        $flags      = "\x00";      // No special flags
164        $key        = pack('N', 0); // Key (will be set by session)
165
166        return $packetType . $flags . $key;
167    }
168
169    private function createInfoPacket(): string
170    {
171        // Based on Torque protocol: GameInfoRequest packet
172        // Format: packet_type(1) + flags(1) + key(4)
173        $packetType = "\x04"; // GameInfoRequest
174        $flags      = "\x00";      // No special flags
175        $key        = pack('N', 0); // Key (will be set by session)
176
177        return $packetType . $flags . $key;
178    }
179
180    private function processPingResponse(string $buffer): bool
181    {
182        if (strlen($buffer) < 14) {
183            return false;
184        }
185
186        // Parse ping response based on Torque protocol
187        // Format: packet_type(1) + flags(1) + key(4) + version_string + protocol_version(4) + min_protocol(4) + build_version(4) + server_name(24)
188
189        $offset = 6; // Skip packet_type, flags, key
190
191        // Read version string (null terminated)
192        $versionString = '';
193
194        while ($offset < strlen($buffer) && ord($buffer[$offset]) !== 0) {
195            $versionString .= $buffer[$offset];
196            $offset++;
197        }
198        $offset++; // Skip null
199
200        $this->gameversion = $versionString;
201
202        // Read protocol versions
203        if ($offset + 8 <= strlen($buffer)) {
204            $tmp                = @unpack('N', substr($buffer, $offset, 4));
205            $protocolVersion    = is_array($tmp) && isset($tmp[1]) ? (int) $tmp[1] : 0;
206            $tmp                = @unpack('N', substr($buffer, $offset + 4, 4));
207            $minProtocolVersion = is_array($tmp) && isset($tmp[1]) ? (int) $tmp[1] : 0;
208            $offset += 8;
209
210            // Read build version
211            if ($offset + 4 <= strlen($buffer)) {
212                $tmp          = @unpack('N', substr($buffer, $offset, 4));
213                $buildVersion = is_array($tmp) && isset($tmp[1]) ? (int) $tmp[1] : 0;
214                $offset += 4;
215
216                // Read server name (up to 24 chars, null terminated)
217                $serverName = '';
218                $maxLen     = min(24, strlen($buffer) - $offset);
219
220                for ($i = 0; $i < $maxLen && ord($buffer[$offset + $i]) !== 0; $i++) {
221                    $serverName .= $buffer[$offset + $i];
222                }
223
224                $this->servertitle = $serverName;
225
226                return true;
227            }
228        }
229
230        return false;
231    }
232
233    private function processInfoResponse(string $buffer): bool
234    {
235        if (strlen($buffer) < 10) {
236            return false;
237        }
238
239        // Parse info response based on Torque protocol
240        // Format: packet_type(1) + flags(1) + key(4) + game_type + mission_type + mission_name + status(1) + num_players(1) + max_players(1) + num_bots(1) + cpu_speed(2) + ...
241
242        $offset = 6; // Skip packet_type, flags, key
243
244        // Read strings (null terminated)
245        $strings = [];
246
247        for ($i = 0; $i < 3; $i++) { // game_type, mission_type, mission_name
248            $str = '';
249
250            while ($offset < strlen($buffer) && ord($buffer[$offset]) !== 0) {
251                $str .= $buffer[$offset];
252                $offset++;
253            }
254            $offset++; // Skip null
255            $strings[] = $str;
256        }
257
258        if (count($strings) >= 3) {
259            $this->gametype = $strings[0];
260
261            /** @phpstan-ignore offsetAccess.notFound */
262            $this->rules['mission_type'] = $strings[1];
263
264            /** @phpstan-ignore offsetAccess.notFound */
265            $this->mapname = $strings[2];
266        }
267
268        // Read status byte
269        if ($offset < strlen($buffer)) {
270            $status = ord($buffer[$offset]);
271            $offset++;
272            $this->rules['status'] = $status;
273        }
274
275        // Read player counts
276        if ($offset + 2 <= strlen($buffer)) {
277            $this->numplayers = ord($buffer[$offset]);
278            $this->maxplayers = ord($buffer[$offset + 1]);
279            $offset += 2;
280        }
281
282        // Read bot count
283        if ($offset < strlen($buffer)) {
284            $this->rules['num_bots'] = ord($buffer[$offset]);
285            $offset++;
286        }
287
288        // Read CPU speed
289        if ($offset + 2 <= strlen($buffer)) {
290            $tmp                      = @unpack('n', substr($buffer, $offset, 2));
291            $cpuSpeed                 = is_array($tmp) && isset($tmp[1]) ? (int) $tmp[1] : 0;
292            $this->rules['cpu_speed'] = $cpuSpeed;
293            $offset += 2;
294        }
295
296        return true;
297    }
298}