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 array_values;
16: use function count;
17: use function explode;
18: use function is_int;
19: use function is_numeric;
20: use function is_string;
21: use function max;
22: use function preg_match;
23: use function substr;
24: use Clansuite\Capture\Protocol\ProtocolInterface;
25: use Clansuite\Capture\ServerAddress;
26: use Clansuite\Capture\ServerInfo;
27: use Clansuite\ServerQuery\CSQuery;
28: use Override;
29:
30: /**
31: * GameSpy protocol implementation (version 1).
32: *
33: * Used by older games like Unreal Tournament.
34: */
35: class Gamespy extends CSQuery implements ProtocolInterface
36: {
37: /**
38: * Protocol name.
39: */
40: public string $name = 'Gamespy';
41:
42: /**
43: * List of supported games.
44: *
45: * @var array<string>
46: */
47: public array $supportedGames = ['Gamespy'];
48:
49: /**
50: * Protocol identifier.
51: */
52: public string $protocol = 'Gamespy';
53:
54: /**
55: * Constructor.
56: */
57: public function __construct(mixed $address = null, mixed $queryport = null)
58: {
59: parent::__construct(is_string($address) ? $address : null, is_int($queryport) ? $queryport : null);
60: }
61:
62: /**
63: * query_server method.
64: */
65: #[Override]
66: public function query_server(bool $getPlayers = true, bool $getRules = true): bool
67: {
68: if ($this->online) {
69: $this->reset();
70: }
71: $address = $this->address ?? '';
72: $queryport = $this->queryport ?? 0;
73:
74: // Send status query
75: $command = "\x5C\x73\x74\x61\x74\x75\x73\x5C";
76:
77: if (($result = $this->sendCommand($address, $queryport, $command)) === '' || ($result = $this->sendCommand($address, $queryport, $command)) === '0' || ($result = $this->sendCommand($address, $queryport, $command)) === false) {
78: return false;
79: }
80:
81: $this->online = true;
82:
83: // Process status response
84: $this->processStatus($result);
85:
86: return true;
87: }
88:
89: /**
90: * query method.
91: */
92: #[Override]
93: public function query(ServerAddress $addr): ServerInfo
94: {
95: $this->address = $addr->ip;
96: $this->queryport = $addr->port;
97: $this->query_server(true, true);
98:
99: return new ServerInfo(
100: address: $this->address,
101: queryport: $this->queryport,
102: online: $this->online,
103: gamename: $this->gamename,
104: gameversion: $this->gameversion,
105: servertitle: $this->servertitle,
106: mapname: $this->mapname,
107: gametype: $this->gametype,
108: numplayers: $this->numplayers,
109: maxplayers: $this->maxplayers,
110: rules: $this->rules,
111: players: $this->players,
112: errstr: $this->errstr,
113: );
114: }
115:
116: /**
117: * getProtocolName method.
118: */
119: #[Override]
120: public function getProtocolName(): string
121: {
122: return $this->protocol;
123: }
124:
125: /**
126: * getVersion method.
127: */
128: #[Override]
129: public function getVersion(ServerInfo $info): string
130: {
131: return $info->gameversion ?? 'unknown';
132: }
133:
134: /**
135: * _processStatus method.
136: */
137: protected function processStatus(string $buffer): void
138: {
139: // Skip the first \ if present
140: if ($buffer !== '' && $buffer[0] === '\\') {
141: $buffer = substr($buffer, 1);
142: }
143:
144: // Explode on \
145: $data = explode('\\', $buffer);
146:
147: $itemCount = count($data);
148:
149: // Process key-value pairs
150: $this->rules = [];
151: $this->players = [];
152: $numPlayers = 0;
153:
154: $x = 0;
155:
156: while ($x + 1 < $itemCount) {
157: $key = $data[$x] ?? '';
158: $val = $data[$x + 1] ?? '';
159: $x += 2;
160:
161: // Check for player data
162: if (preg_match('/^player_(\d+)$/', $key, $matches) === 1) {
163: $playerIndex = (int) $matches[1];
164:
165: if (!isset($this->players[$playerIndex])) {
166: $this->players[$playerIndex] = [];
167: }
168: $this->players[$playerIndex]['name'] = $val;
169: $numPlayers = max($numPlayers, $playerIndex + 1);
170: } elseif (preg_match('/^frags_(\d+)$/', $key, $matches) === 1) {
171: $playerIndex = (int) $matches[1];
172:
173: if (!isset($this->players[$playerIndex])) {
174: $this->players[$playerIndex] = [];
175: }
176: $this->players[$playerIndex]['score'] = is_numeric($val) ? (int) $val : 0;
177: } elseif (preg_match('/^ping_(\d+)$/', $key, $matches) === 1) {
178: $playerIndex = (int) $matches[1];
179:
180: if (!isset($this->players[$playerIndex])) {
181: $this->players[$playerIndex] = [];
182: }
183: $this->players[$playerIndex]['ping'] = is_numeric($val) ? (int) $val : 0;
184: } elseif ($key === 'hostname') {
185: $this->servertitle = $val;
186: } elseif ($key === 'mapname') {
187: $this->mapname = $val;
188: } elseif ($key === 'gametype') {
189: $this->gametype = $val;
190: } elseif ($key === 'numplayers') {
191: $this->numplayers = is_numeric($val) ? (int) $val : 0;
192: } elseif ($key === 'maxplayers') {
193: $this->maxplayers = is_numeric($val) ? (int) $val : 0;
194: } elseif ($key === 'gamever') {
195: $this->gameversion = $val;
196: } else {
197: $this->rules[$key] = $val;
198: }
199: }
200:
201: // Reindex players
202: $this->players = array_values($this->players);
203: }
204: }
205: