Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.96% covered (warning)
86.96%
60 / 69
25.00% covered (danger)
25.00%
1 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Bf2
86.96% covered (warning)
86.96%
60 / 69
25.00% covered (danger)
25.00%
1 / 4
33.13
0.00% covered (danger)
0.00%
0 / 1
 query_server
58.33% covered (warning)
58.33%
7 / 12
0.00% covered (danger)
0.00%
0 / 1
6.81
 processResponse
85.71% covered (warning)
85.71%
12 / 14
0.00% covered (danger)
0.00%
0 / 1
5.07
 parseDetails
93.10% covered (success)
93.10%
27 / 29
0.00% covered (danger)
0.00%
0 / 1
13.06
 parsePlayersAndTeams
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
8
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
13namespace Clansuite\ServerQuery\ServerProtocols;
14
15use function count;
16use function explode;
17use function is_numeric;
18use function preg_split;
19use function strlen;
20use function substr;
21use Override;
22
23/**
24 * Battlefield 2 protocol implementation.
25 *
26 * Based on GameSpy 3 protocol.
27 */
28class 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}