Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 131
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Satisfactory
0.00% covered (danger)
0.00%
0 / 131
0.00% covered (danger)
0.00%
0 / 6
1640
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 query_server
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
42
 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
 parseResponse
0.00% covered (danger)
0.00%
0 / 90
0.00% covered (danger)
0.00%
0 / 1
930
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 pack;
17use function strlen;
18use function substr;
19use function time;
20use function unpack;
21use Clansuite\Capture\Protocol\ProtocolInterface;
22use Clansuite\Capture\ServerAddress;
23use Clansuite\Capture\ServerInfo;
24use Clansuite\ServerQuery\CSQuery;
25use Override;
26
27/**
28 * Queries Satisfactory game servers.
29 *
30 * Retrieves server information for the game Satisfactory, including player count,
31 * server settings, and game state by communicating with the game's query protocol.
32 * Enables monitoring of Satisfactory multiplayer servers.
33 */
34class Satisfactory extends CSQuery implements ProtocolInterface
35{
36    /**
37     * Protocol name.
38     */
39    public string $name = 'Satisfactory';
40
41    /**
42     * List of supported games.
43     *
44     * @var array<string>
45     */
46    public array $supportedGames = ['Satisfactory'];
47
48    /**
49     * Protocol identifier.
50     */
51    public string $protocol = 'Satisfactory';
52
53    /**
54     * Game series.
55     *
56     * @var array<string>
57     */
58    public array $game_series_list = ['Satisfactory'];
59
60    /**
61     * Default query port.
62     */
63    protected int $port_diff = 0; // Query port is 15777, game port 7777
64
65    /**
66     * Constructor.
67     *
68     * Initializes the Satisfactory query instance.
69     *
70     * @param null|string $address   The server address to query
71     * @param null|int    $queryport The query port for the Satisfactory server (default 15777)
72     */
73    public function __construct(?string $address = null, ?int $queryport = null)
74    {
75        parent::__construct();
76        $this->address   = $address;
77        $this->queryport = $queryport ?? 15777; // Default query port
78    }
79
80    /**
81     * Queries the Satisfactory server and populates server information.
82     *
83     * Sends a query request and processes the response to extract server details,
84     * player information, and game settings.
85     *
86     * @param bool $getPlayers Whether to retrieve player information
87     * @param bool $getRules   Whether to retrieve server rules/settings
88     *
89     * @return bool True on successful query, false on failure
90     */
91    #[Override]
92    public function query_server(bool $getPlayers = true, bool $getRules = true): bool
93    {
94        if ($this->online) {
95            $this->reset();
96        }
97
98        // Create Poll Server State message
99        // ProtocolMagic: 0xF6D5 (little endian)
100        // MessageType: 0 (Poll Server State)
101        // ProtocolVersion: 1
102        // Payload: uint64 LE Cookie (use current time or random)
103        // Terminator: 0x1
104
105        $cookie  = time(); // Use current timestamp as cookie
106        $payload = pack('VV', $cookie & 0xFFFFFFFF, ($cookie >> 32) & 0xFFFFFFFF);
107
108        $message = pack('v', 0xF6D5) . // ProtocolMagic LE
109                   pack('C', 0) .     // MessageType
110                   pack('C', 1) .     // ProtocolVersion
111                   $payload .         // Cookie
112                   pack('C', 0x1);    // Terminator
113
114        $address = (string) $this->address;
115        $port    = (int) $this->queryport;
116
117        if (($result = $this->sendCommand($address, $port, $message)) === '' || ($result = $this->sendCommand($address, $port, $message)) === '0' || ($result = $this->sendCommand($address, $port, $message)) === false) {
118            $this->errstr = 'No reply received';
119
120            return false;
121        }
122
123        // Parse the response
124        if (!$this->parseResponse($result)) {
125            return false;
126        }
127
128        $this->online = true;
129
130        return true;
131    }
132
133    // ProtocolInterface methods
134    /**
135     * query method.
136     *
137     * Performs a query on the specified Satisfactory server address.
138     *
139     * Updates internal state with server information and returns a ServerInfo object
140     * containing the query results.
141     *
142     * @param ServerAddress $addr The server address and port to query
143     *
144     * @return ServerInfo Server information including status, players, and settings
145     */
146    #[Override]
147    public function query(ServerAddress $addr): ServerInfo
148    {
149        $this->address   = $addr->ip;
150        $this->queryport = $addr->port;
151        $this->query_server();
152
153        return new ServerInfo(
154            address: $this->address,
155            queryport: $this->queryport,
156            online: $this->online,
157            gamename: $this->gamename,
158            gameversion: $this->gameversion,
159            servertitle: $this->servertitle,
160            mapname: $this->mapname,
161            gametype: $this->gametype,
162            numplayers: $this->numplayers,
163            maxplayers: $this->maxplayers,
164            rules: $this->rules,
165            players: $this->players,
166            errstr: $this->errstr,
167        );
168    }
169
170    /**
171     * Returns the protocol name for Satisfactory.
172     *
173     * @return string The protocol identifier 'Satisfactory'
174     */
175    #[Override]
176    public function getProtocolName(): string
177    {
178        return $this->protocol;
179    }
180
181    /**
182     * Extracts the Satisfactory server version from server information.
183     *
184     * @param ServerInfo $info The server information object
185     *
186     * @return string The server version string, or 'unknown' if not available
187     */
188    #[Override]
189    public function getVersion(ServerInfo $info): string
190    {
191        return $info->gameversion ?? 'unknown';
192    }
193
194    /**
195     * Parses the raw response data from the Satisfactory server.
196     *
197     * Extracts server information, player data, and settings from the binary response.
198     *
199     * @param string $data The raw binary data received from the server
200     *
201     * @return bool True if parsing was successful, false otherwise
202     */
203    protected function parseResponse(string $data): bool
204    {
205        if (strlen($data) < 5) {
206            $this->errstr = 'Response too short';
207
208            return false;
209        }
210
211        // Check ProtocolMagic
212        $tmp = @unpack('v', substr($data, 0, 2));
213
214        if (!is_array($tmp) || !isset($tmp[1])) {
215            $this->errstr = 'Invalid response unpack';
216
217            return false;
218        }
219        $magic = $tmp[1];
220
221        if ($magic !== 0xF6D5) {
222            $this->errstr = 'Invalid protocol magic';
223
224            return false;
225        }
226
227        $tmp = @unpack('C', substr($data, 2, 1));
228
229        if (!is_array($tmp) || !isset($tmp[1])) {
230            $this->errstr = 'Invalid response unpack';
231
232            return false;
233        }
234        $messageType = $tmp[1];
235
236        if ($messageType !== 1) { // Server State Response
237            $this->errstr = 'Unexpected message type';
238
239            return false;
240        }
241
242        $tmp = @unpack('C', substr($data, 3, 1));
243
244        if (!is_array($tmp) || !isset($tmp[1])) {
245            $this->errstr = 'Invalid response unpack';
246
247            return false;
248        }
249        $protocolVersion = $tmp[1];
250
251        if ($protocolVersion !== 1) {
252            $this->errstr = 'Unsupported protocol version';
253
254            return false;
255        }
256
257        $offset = 4;
258        $offset += 8;
259
260        // ServerState (uint8)
261        $tmp = @unpack('C', substr($data, $offset, 1));
262
263        if (!is_array($tmp) || !isset($tmp[1])) {
264            $this->errstr = 'Invalid response unpack';
265
266            return false;
267        }
268        $serverState = $tmp[1];
269        $offset++;
270
271        // ServerNetCL (uint32 LE)
272        $tmp = @unpack('V', substr($data, $offset, 4));
273
274        if (!is_array($tmp) || !isset($tmp[1])) {
275            $this->errstr = 'Invalid response unpack';
276
277            return false;
278        }
279        $serverNetCL = $tmp[1];
280        $offset += 4;
281        $offset += 8;
282
283        // NumSubStates (uint8)
284        $tmp = @unpack('C', substr($data, $offset, 1));
285
286        if (!is_array($tmp) || !isset($tmp[1])) {
287            $this->errstr = 'Invalid response unpack';
288
289            return false;
290        }
291        $numSubStates = $tmp[1];
292        $offset++;
293
294        // SubStates (array of ServerSubState)
295        for ($i = 0; $i < $numSubStates; $i++) {
296            // SubStateId (uint8)
297            $tmp = @unpack('C', substr($data, $offset, 1));
298
299            if (!is_array($tmp) || !isset($tmp[1])) {
300                $this->errstr = 'Invalid response unpack (substate id)';
301
302                return false;
303            }
304            $subStateId = $tmp[1];
305            $offset++;
306
307            // SubStateVersion (uint16 LE)
308            $tmp = @unpack('v', substr($data, $offset, 2));
309
310            if (!is_array($tmp) || !isset($tmp[1])) {
311                $this->errstr = 'Invalid response unpack (substate version)';
312
313                return false;
314            }
315            $subStateVersion = $tmp[1];
316            $offset += 2;
317        }
318
319        // ServerNameLength (uint16 LE)
320        $tmp = @unpack('v', substr($data, $offset, 2));
321
322        if (!is_array($tmp) || !isset($tmp[1])) {
323            $this->errstr = 'Invalid response unpack';
324
325            return false;
326        }
327        $serverNameLength = $tmp[1];
328        $offset += 2;
329
330        // ServerName (UTF-8)
331        $serverName = substr($data, $offset, $serverNameLength);
332        $offset += $serverNameLength;
333
334        // Terminator
335        if ($offset < strlen($data)) {
336            $tmp = @unpack('C', substr($data, $offset, 1));
337
338            if (!is_array($tmp) || !isset($tmp[1])) {
339                $this->errstr = 'Invalid response unpack (terminator)';
340
341                return false;
342            }
343            $terminator = $tmp[1];
344
345            if ($terminator !== 0x1) {
346                $this->errstr = 'Invalid terminator';
347
348                return false;
349            }
350        }
351
352        // Now set the properties
353        $this->servertitle = $serverName;
354        $this->gamename    = 'Satisfactory';
355        $this->gameversion = (string) $serverNetCL; // Use changelist as version
356
357        // Map server state to online status
358        // 0: Offline, 1: Idle, 2: Loading, 3: Playing
359        if ($serverState === 3 || $serverState === 1) {
360            $this->online = true;
361        } else {
362            $this->online = false;
363        }
364
365        // For now, set some defaults since the lightweight API doesn't provide player count etc.
366        $this->numplayers = 0; // Not provided in lightweight API
367        $this->maxplayers = 0; // Not provided
368        $this->mapname    = ''; // Not provided
369        $this->gametype   = ''; // Not provided
370        $this->password   = -1; // Unknown
371
372        return true;
373    }
374}