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_chunk;
16: use function array_shift;
17: use function count;
18: use function explode;
19: use function trim;
20: use Clansuite\Capture\Protocol\ProtocolInterface;
21: use Clansuite\Capture\ServerAddress;
22: use Clansuite\Capture\ServerInfo;
23: use Clansuite\ServerQuery\CSQuery;
24: use Override;
25:
26: /**
27: * Battlefield 1942 protocol implementation.
28: *
29: * Uses GameSpy protocol.
30: */
31: class Bf1942 extends CSQuery implements ProtocolInterface
32: {
33: /**
34: * Protocol name.
35: */
36: public string $name = 'Battlefield 1942';
37:
38: /**
39: * List of supported games.
40: *
41: * @var array<string>
42: */
43: public array $supportedGames = ['Battlefield 1942'];
44:
45: /**
46: * Protocol identifier.
47: */
48: public string $protocol = 'bf1942';
49:
50: /**
51: * Game series.
52: *
53: * @var array<string>
54: */
55: public array $game_series_list = ['Battlefield'];
56: protected int $port_diff = 8433;
57:
58: /**
59: * Constructor.
60: */
61: public function __construct(?string $address = null, ?int $queryport = null)
62: {
63: parent::__construct($address, $queryport);
64: }
65:
66: /**
67: * query_server method.
68: */
69: #[Override]
70: public function query_server(bool $getPlayers = true, bool $getRules = true): bool
71: {
72: if ($this->online) {
73: $this->reset();
74: }
75:
76: if ($this->queryport <= 0 || $this->address === '') {
77: $this->errstr = 'Query port or address not set';
78:
79: return false;
80: }
81:
82: $queryPort = $this->queryport + $this->port_diff;
83:
84: // BF1942 uses GameSpy protocol
85: $command = "\x5C\x73\x74\x61\x74\x75\x73\x5C";
86:
87: $result = $this->udpClient->query($this->address ?? '', $queryPort, $command);
88:
89: if ($result === null || $result === '' || $result === '0') {
90: $this->errstr = 'No reply received';
91:
92: return false;
93: }
94:
95: // Add to debug for capture tool
96: $this->debug[] = [$command, $result];
97:
98: // Parse the GameSpy response
99: // Format: \key\value\key\value...
100: $this->parseResponse($result);
101:
102: $this->online = true;
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: /**
153: * Parse the GameSpy response.
154: */
155: private function parseResponse(string $response): void
156: {
157: // Remove leading/trailing backslashes
158: $response = trim($response, '\\');
159:
160: // Split by \
161: $parts = explode('\\', $response);
162:
163: // Remove empty first element if present
164: if (($parts[0] ?? '') === '') {
165: array_shift($parts);
166: }
167:
168: $this->players = [];
169: $parsingPlayers = false;
170:
171: foreach (array_chunk($parts, 2) as $chunk) {
172: if (!isset($chunk[0], $chunk[1])) {
173: continue;
174: }
175:
176: [$key, $value] = $chunk;
177:
178: if ($key === 'playername') {
179: $parsingPlayers = true;
180: $this->players[] = ['name' => $value];
181: } elseif ($parsingPlayers) {
182: // Additional player fields
183: if ($this->players !== []) {
184: $lastPlayer = &$this->players[count($this->players) - 1];
185: $lastPlayer[$key] = $value;
186: }
187: } else {
188: // Server info
189: switch ($key) {
190: case 'hostname':
191: $this->servertitle = $value;
192:
193: break;
194:
195: case 'mapname':
196: $this->mapname = $value;
197:
198: break;
199:
200: case 'gametype':
201: $this->gametype = $value;
202:
203: break;
204:
205: case 'maxplayers':
206: $this->maxplayers = (int) $value;
207:
208: break;
209:
210: case 'numplayers':
211: $this->numplayers = (int) $value;
212:
213: break;
214:
215: case 'password':
216: $this->password = $value === '1' ? 1 : 0;
217:
218: break;
219:
220: default:
221: $this->rules[$key] = $value;
222:
223: break;
224: }
225: }
226: }
227: }
228: }
229: