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 count;
16: use function explode;
17: use function is_numeric;
18: use function preg_split;
19: use function strlen;
20: use function substr;
21: use Override;
22:
23: /**
24: * Battlefield 2 protocol implementation.
25: *
26: * Based on GameSpy 3 protocol.
27: */
28: class Bf2 extends Gamespy3
29: {
30: /**
31: * Protocol name.
32: */
33: public string $name = 'Battlefield 2';
34:
35: /**
36: * Protocol identifier.
37: */
38: public string $protocol = 'Bf2';
39:
40: /**
41: * Game series.
42: *
43: * @var array<string>
44: */
45: public array $game_series_list = ['Battlefield'];
46:
47: /**
48: * List of supported games.
49: *
50: * @var array<string>
51: */
52: public array $supportedGames = ['Battlefield 2'];
53: protected int $port_diff = 13333;
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: $queryPort = ($this->queryport ?? 0) + $this->port_diff;
66:
67: // BF2 uses GameSpy 3 protocol with specific packet (no challenge needed)
68: $packet = "\xFE\xFD\x00\x10\x20\x30\x40\xFF\xFF\xFF\x01";
69:
70: $result = $this->udpClient->query($this->address ?? '', $queryPort, $packet);
71:
72: if ($result === null || $result === '' || $result === '0') {
73: $this->errstr = 'No response from server';
74:
75: return false;
76: }
77:
78: // Add to debug for capture tool
79: $this->debug[] = [$packet, $result];
80:
81: // Parse the response using the inherited method
82: $this->processResponse($result);
83:
84: $this->online = true;
85:
86: return true;
87: }
88:
89: /**
90: * Parse the GameSpy 3 response.
91: */
92: protected function processResponse(string $response): void
93: {
94: // Skip header: packet type (1), session id (4), splitnum\0 (9)
95: $offset = 1 + 4 + 9;
96:
97: if (strlen($response) < $offset) {
98: $this->errstr = 'Response too short';
99:
100: return;
101: }
102: $offset++;
103:
104: // Skip next byte
105: $offset++;
106:
107: // The rest is the data
108: $data = substr($response, $offset);
109:
110: // Split into server info and players/teams
111: $split = preg_split('/\\x00\\x00\\x01/', $data, 2);
112:
113: if ($split === [] || $split === false) {
114: $this->errstr = 'Failed to split response';
115:
116: return;
117: }
118:
119: // Parse server details
120: $this->parseDetails($split[0]);
121:
122: // Parse players and teams if present
123: if (isset($split[1])) {
124: $this->parsePlayersAndTeams($split[1]);
125: }
126: }
127:
128: /**
129: * Parse server details.
130: */
131: private function parseDetails(string $data): void
132: {
133: $parts = explode("\x00", $data);
134:
135: if (count($parts) < 2) {
136: return;
137: }
138:
139: $partsCount = count($parts);
140:
141: for ($i = 0; $i + 1 < $partsCount; $i += 2) {
142: if (!isset($parts[$i]) || !isset($parts[$i + 1])) {
143: continue;
144: }
145:
146: $key = $parts[$i];
147: $value = $parts[$i + 1];
148:
149: switch ($key) {
150: case 'hostname':
151: $this->servertitle = $value;
152:
153: break;
154:
155: case 'mapname':
156: $this->mapname = $value;
157:
158: break;
159:
160: case 'gametype':
161: $this->gametype = $value;
162:
163: break;
164:
165: case 'maxplayers':
166: $this->maxplayers = (int) $value;
167:
168: break;
169:
170: case 'numplayers':
171: $this->numplayers = (int) $value;
172:
173: break;
174:
175: case 'password':
176: $this->password = $value === '1' ? 1 : 0;
177:
178: break;
179:
180: default:
181: $this->rules[$key] = $value;
182:
183: break;
184: }
185: }
186: }
187:
188: /**
189: * Parse players and teams.
190: */
191: private function parsePlayersAndTeams(string $data): void
192: {
193: $parts = explode("\x00", $data);
194:
195: $this->players = [];
196: $i = 0;
197: $partsCount = count($parts);
198:
199: // Skip 'player_' and empty
200: if (isset($parts[$i]) && $parts[$i] === 'player_') {
201: $i++;
202: }
203:
204: if (isset($parts[$i]) && $parts[$i] === '') {
205: $i++;
206: }
207:
208: // Parse name\team pairs
209: while ($i + 1 < $partsCount) {
210: $name = $parts[$i++] ?? '';
211: $team = $parts[$i++] ?? '';
212:
213: if (!is_numeric($name) && $name !== '') {
214: $this->players[] = ['name' => $name, 'team' => $team];
215: } else {
216: break;
217: }
218: }
219: }
220: }
221: