Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 78
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Samp
0.00% covered (danger)
0.00%
0 / 78
0.00% covered (danger)
0.00%
0 / 5
462
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 / 41
0.00% covered (danger)
0.00%
0 / 1
306
 query
0.00% covered (danger)
0.00%
0 / 32
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
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 ord;
17use function pack;
18use function str_starts_with;
19use function strlen;
20use function substr;
21use function unpack;
22use Clansuite\Capture\Protocol\ProtocolInterface;
23use Clansuite\Capture\ServerAddress;
24use Clansuite\Capture\ServerInfo;
25use Clansuite\ServerQuery\CSQuery;
26use Override;
27
28/**
29 * GTA: San Andreas Multiplayer (SAMP) server protocol implementation.
30 *
31 * SAMP uses a custom UDP query protocol.
32 *
33 * @see https://sampwiki.blast.hk/wiki/Query_Mechanism
34 */
35class Samp extends CSQuery implements ProtocolInterface
36{
37    /**
38     * Protocol name.
39     */
40    public string $name = 'SAMP';
41
42    /**
43     * List of supported games.
44     *
45     * @var array<string>
46     */
47    public array $supportedGames = ['GTA: San Andreas Multiplayer'];
48
49    /**
50     * Protocol identifier.
51     */
52    public string $protocol = 'samp';
53
54    /**
55     * Constructor.
56     */
57    public function __construct(?string $address = null, ?int $queryport = null)
58    {
59        parent::__construct();
60        $this->address   = $address;
61        $this->queryport = $queryport;
62    }
63
64    /**
65     * Query server information.
66     */
67    #[Override]
68    public function query_server(bool $getPlayers = true, bool $getRules = true): bool
69    {
70        if ($this->online) {
71            $this->reset();
72        }
73
74        // SAMP query packet: 'SAMP' + 4 bytes IP + 2 bytes port + 'i'
75        $packet = 'SAMP' . pack('C*', 127, 0, 0, 1) . pack('n', 7777) . 'i';
76
77        $address = (string) $this->address;
78        $port    = (int) $this->queryport;
79
80        $response = $this->sendCommand($address, $port, $packet);
81
82        if ($response === '' || $response === '0' || $response === false) {
83            $this->errstr = 'No reply received';
84
85            return false;
86        }
87
88        if (strlen($response) < 11 || !str_starts_with($response, 'SAMP')) {
89            $this->errstr = 'Invalid response';
90
91            return false;
92        }
93
94        $offset           = 4;
95        $this->password   = ord($response[$offset++]);
96        $tmp              = @unpack('n', substr($response, $offset, 2));
97        $this->numplayers = is_array($tmp) && isset($tmp[1]) ? (int) $tmp[1] : 0;
98        $offset += 2;
99        $tmp              = @unpack('n', substr($response, $offset, 2));
100        $this->maxplayers = is_array($tmp) && isset($tmp[1]) ? (int) $tmp[1] : 0;
101        $offset += 2;
102
103        $tmp         = @unpack('N', substr($response, $offset, 4));
104        $hostnameLen = is_array($tmp) && isset($tmp[1]) ? (int) $tmp[1] : 0;
105        $offset += 4;
106        $this->servertitle = substr($response, $offset, $hostnameLen);
107        $offset += $hostnameLen;
108
109        $tmp         = @unpack('N', substr($response, $offset, 4));
110        $gamemodeLen = is_array($tmp) && isset($tmp[1]) ? (int) $tmp[1] : 0;
111        $offset += 4;
112        $gamemode = substr($response, $offset, $gamemodeLen);
113        $offset += $gamemodeLen;
114
115        $tmp         = @unpack('N', substr($response, $offset, 4));
116        $languageLen = is_array($tmp) && isset($tmp[1]) ? (int) $tmp[1] : 0;
117        $offset += 4;
118        $language = substr($response, $offset, $languageLen);
119
120        $this->mapname = $gamemode; // Use gamemode as map
121        $this->rules   = [
122            'gamemode' => $gamemode,
123            'language' => $language,
124        ];
125
126        $this->online = true;
127
128        return true;
129    }
130
131    /**
132     * query method.
133     */
134    #[Override]
135    public function query(ServerAddress $addr): ServerInfo
136    {
137        $this->address   = $addr->ip;
138        $this->queryport = $addr->port;
139
140        $this->query_server(true, true);
141
142        return new ServerInfo(
143            address: $addr->ip,
144            queryport: $addr->port,
145            online: $this->online,
146            gamename: $this->name,
147            gameversion: $this->getVersion(new ServerInfo(
148                address: $addr->ip,
149                queryport: $addr->port,
150                online: $this->online,
151                gamename: $this->name,
152                gameversion: '',
153                servertitle: $this->servertitle,
154                mapname: $this->mapname,
155                gametype: '',
156                numplayers: $this->numplayers,
157                maxplayers: $this->maxplayers,
158                rules: $this->rules,
159                players: $this->players,
160                errstr: $this->errstr,
161            )),
162            servertitle: $this->servertitle,
163            mapname: $this->mapname,
164            gametype: '',
165            numplayers: $this->numplayers,
166            maxplayers: $this->maxplayers,
167            rules: $this->rules,
168            players: $this->players,
169            errstr: $this->errstr,
170        );
171    }
172
173    /**
174     * getProtocolName method.
175     */
176    #[Override]
177    public function getProtocolName(): string
178    {
179        return $this->protocol;
180    }
181
182    /**
183     * getVersion method.
184     */
185    #[Override]
186    public function getVersion(ServerInfo $info): string
187    {
188        return '';
189    }
190}