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 is_array;
16: use function ord;
17: use function pack;
18: use function str_starts_with;
19: use function strlen;
20: use function substr;
21: use function unpack;
22: use Clansuite\Capture\Protocol\ProtocolInterface;
23: use Clansuite\Capture\ServerAddress;
24: use Clansuite\Capture\ServerInfo;
25: use Clansuite\ServerQuery\CSQuery;
26: use 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: */
35: class 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: }
191: