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_int;
16: use function is_string;
17: use function ord;
18: use function strlen;
19: use Clansuite\Capture\Protocol\ProtocolInterface;
20: use Clansuite\Capture\ServerAddress;
21: use Clansuite\Capture\ServerInfo;
22: use Clansuite\ServerQuery\CSQuery;
23: use Override;
24:
25: /**
26: * GameSpy2 protocol implementation.
27: */
28: class Gamespy2 extends CSQuery implements ProtocolInterface
29: {
30: /**
31: * Protocol name.
32: */
33: public string $name = 'Gamespy2';
34:
35: /**
36: * List of supported games.
37: *
38: * @var array<string>
39: */
40: public array $supportedGames = ['Gamespy2', 'Halo'];
41:
42: /**
43: * Protocol identifier.
44: */
45: public string $protocol = 'Gamespy2';
46:
47: /**
48: * Constructor.
49: */
50: public function __construct(mixed $address = null, mixed $queryport = null)
51: {
52: parent::__construct(is_string($address) ? $address : null, is_int($queryport) ? $queryport : null);
53: }
54:
55: /**
56: * query_server method.
57: */
58: #[Override]
59: public function query_server(bool $getPlayers = true, bool $getRules = true): bool
60: {
61: if ($this->online) {
62: $this->reset();
63: }
64:
65: // Send details query
66: $command = "\xFE\xFD\x00\x43\x4F\x52\x59\xFF\x00\x00";
67:
68: if (($result = $this->sendCommand($this->address ?? '', $this->queryport ?? 0, $command)) === '' || ($result = $this->sendCommand($this->address ?? '', $this->queryport ?? 0, $command)) === '0' || ($result = $this->sendCommand($this->address ?? '', $this->queryport ?? 0, $command)) === false) {
69: return false;
70: }
71:
72: $this->online = true;
73:
74: // Process details response
75: $this->processDetails($result);
76: // Send players query
77: $command = "\xFE\xFD\x00\x43\x4F\x52\x58\x00\xFF\xFF";
78:
79: if (($result = $this->sendCommand($this->address ?? '', $this->queryport ?? 0, $command)) !== false) {
80: $this->processPlayers($result);
81: }
82:
83: return true;
84: }
85:
86: /**
87: * query method.
88: */
89: #[Override]
90: public function query(ServerAddress $addr): ServerInfo
91: {
92: $this->address = $addr->ip;
93: $this->queryport = $addr->port;
94: $this->query_server(true, true);
95:
96: return new ServerInfo(
97: address: $this->address,
98: queryport: $this->queryport,
99: online: $this->online,
100: gamename: $this->gamename,
101: gameversion: $this->gameversion,
102: servertitle: $this->servertitle,
103: mapname: $this->mapname,
104: gametype: $this->gametype,
105: numplayers: $this->numplayers,
106: maxplayers: $this->maxplayers,
107: rules: $this->rules,
108: players: $this->players,
109: errstr: $this->errstr,
110: );
111: }
112:
113: /**
114: * getProtocolName method.
115: */
116: #[Override]
117: public function getProtocolName(): string
118: {
119: return $this->protocol;
120: }
121:
122: /**
123: * getVersion method.
124: */
125: #[Override]
126: public function getVersion(ServerInfo $info): string
127: {
128: return $info->gameversion ?? 'unknown';
129: }
130:
131: private function processDetails(string $buffer): void
132: {
133: $i = 5; // Skip header
134: $len = strlen($buffer);
135:
136: while ($i < $len) {
137: $key = '';
138:
139: while ($i < $len && ord($buffer[$i]) !== 0) {
140: $key .= $buffer[$i];
141: $i++;
142: }
143: $i++; // Skip null
144:
145: if ($key === '' || $key === '0') {
146: break;
147: }
148:
149: $value = '';
150:
151: while ($i < $len && ord($buffer[$i]) !== 0) {
152: $value .= $buffer[$i];
153: $i++;
154: }
155: $i++; // Skip null
156:
157: $this->setDetail($key, $value);
158: }
159: }
160:
161: private function setDetail(string $key, string $value): void
162: {
163: switch ($key) {
164: case 'hostname':
165: $this->servertitle = $value;
166:
167: break;
168:
169: case 'mapname':
170: $this->mapname = $value;
171:
172: break;
173:
174: case 'gametype':
175: $this->gametype = $value;
176:
177: break;
178:
179: case 'maxplayers':
180: $this->maxplayers = (int) $value;
181:
182: break;
183:
184: case 'numplayers':
185: $this->numplayers = (int) $value;
186:
187: break;
188:
189: case 'gamever':
190: $this->gameversion = $value;
191:
192: break;
193:
194: default:
195: $this->rules[$key] = $value;
196:
197: break;
198: }
199: }
200:
201: private function processPlayers(string $buffer): void
202: {
203: $i = 6; // Skip header and count byte
204: $len = strlen($buffer);
205:
206: // Skip player count
207: if ($i < $len) {
208: $i++;
209: }
210:
211: // Read variable names
212: $varNames = [];
213:
214: while ($i < $len) {
215: $var = '';
216:
217: while ($i < $len && ord($buffer[$i]) !== 0) {
218: $var .= $buffer[$i];
219: $i++;
220: }
221: $i++; // Skip null
222:
223: if ($var === '' || $var === '0') {
224: break;
225: }
226:
227: $varNames[] = $var;
228: }
229:
230: // Read player data
231: $this->players = [];
232:
233: while ($i < $len - 4) {
234: $player = [];
235:
236: foreach ($varNames as $varName) {
237: $value = '';
238:
239: while ($i < $len && ord($buffer[$i]) !== 0) {
240: $value .= $buffer[$i];
241: $i++;
242: }
243: $i++; // Skip null
244:
245: $player[$varName] = $value;
246: }
247:
248: if ($player !== []) {
249: $this->players[] = $player;
250: }
251:
252: if ($i >= $len || ord($buffer[$i]) === 0) {
253: break;
254: }
255: }
256: }
257: }
258: