Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.04% covered (warning)
89.04%
65 / 73
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Bf1942
89.04% covered (warning)
89.04%
65 / 73
66.67% covered (warning)
66.67%
4 / 6
26.89
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 query_server
53.33% covered (warning)
53.33%
8 / 15
0.00% covered (danger)
0.00%
0 / 1
11.98
 query
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
1
 getProtocolName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getVersion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parseResponse
97.30% covered (success)
97.30%
36 / 37
0.00% covered (danger)
0.00%
0 / 1
15
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 array_chunk;
16use function array_shift;
17use function count;
18use function explode;
19use function trim;
20use Clansuite\Capture\Protocol\ProtocolInterface;
21use Clansuite\Capture\ServerAddress;
22use Clansuite\Capture\ServerInfo;
23use Clansuite\ServerQuery\CSQuery;
24use Override;
25
26/**
27 * Battlefield 1942 protocol implementation.
28 *
29 * Uses GameSpy protocol.
30 */
31class 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}