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