Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
Quake2
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 3
600
0.00% covered (danger)
0.00%
0 / 1
 query_server
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
90
 parseServerInfo
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
90
 parsePlayers
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
42
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_shift;
16use function array_slice;
17use function count;
18use function explode;
19use function is_string;
20use function preg_match;
21use function trim;
22use Override;
23
24/**
25 * Implements the query protocol for Quake 2 servers.
26 * Extends the base Quake protocol with Quake 2 specific query handling and data parsing.
27 */
28class Quake2 extends Quake
29{
30    /**
31     * Protocol name.
32     */
33    public string $name = 'Quake 2';
34
35    /**
36     * List of supported games.
37     */
38    public array $supportedGames = ['Quake 2'];
39
40    /**
41     * Protocol identifier.
42     */
43    public string $protocol = 'Quake';
44
45    /**
46     * Game series.
47     */
48    public string $game_series = 'Quake';
49
50    /**
51     * query_server method.
52     */
53    #[Override]
54    public function query_server(bool $getPlayers = true, bool $getRules = true): bool
55    {
56        if ($this->online) {
57            $this->reset();
58        }
59
60        // Quake2 uses simple text commands - try different ones
61        $commands = ["status\n", "info\n", "ping\n"];
62
63        $result = false;
64
65        foreach ($commands as $command) {
66            if (($result = $this->sendCommand((string) $this->address, (int) $this->queryport, $command)) !== false) {
67                break;
68            }
69        }
70
71        if ($result === '' || $result === '0' || $result === false) {
72            $this->errstr = 'No reply received';
73
74            return false;
75        }
76
77        // Parse the Quake2 response
78        // Format: first line is server info, subsequent lines are players
79        $lines = explode("\n", trim($result));
80
81        // First line contains server info in key\value format
82        $serverInfo = $lines[0];
83        $this->parseServerInfo($serverInfo);
84
85        // Remaining lines are players (if any)
86        if ($getPlayers && count($lines) > 1) {
87            $this->parsePlayers(array_slice($lines, 1));
88        }
89
90        $this->online = true;
91
92        return true;
93    }
94
95    /**
96     * Parse server info line.
97     */
98    private function parseServerInfo(string $info): void
99    {
100        // Format: \key\value\key\value...
101        $parts = explode('\\', $info);
102
103        // Skip empty first part if it starts with \
104        if (isset($parts[0]) && $parts[0] === '') {
105            array_shift($parts);
106        }
107
108        $rules = [];
109
110        for ($i = 0; $i < count($parts) - 1; $i += 2) {
111            $key   = $parts[$i] ?? '';
112            $value = $parts[$i + 1] ?? '';
113
114            // Map common keys to properties
115            switch ($key) {
116                case 'hostname':
117                    $this->servertitle = $value;
118
119                    break;
120
121                case 'mapname':
122                    $this->mapname = $value;
123
124                    break;
125
126                case 'maxclients':
127                    $this->maxplayers = (int) $value;
128
129                    break;
130
131                case 'version':
132                    $this->gameversion = $value;
133
134                    break;
135
136                case 'gamename':
137                    $this->gamename = $value;
138
139                    break;
140            }
141
142            $rules[$key] = $value;
143        }
144
145        $this->rules    = $rules;
146        $this->gametype = $rules['gametype'] ?? '';
147    }
148
149    /**
150     * Parse player lines.
151     *
152     * @param array<mixed> $playerLines
153     */
154    private function parsePlayers(array $playerLines): void
155    {
156        $players = [];
157
158        foreach ($playerLines as $line) {
159            if (!is_string($line)) {
160                continue;
161            }
162            $line = trim($line);
163
164            if ($line === '') {
165                continue;
166            }
167
168            if ($line === '0') {
169                continue;
170            }
171
172            // Quake2 player format: score ping "name"
173            // Example:  5  50 "PlayerName"
174            if (preg_match('/^\s*(\d+)\s+(\d+)\s+"([^"]*)"/', $line, $matches) === 1) {
175                $players[] = [
176                    'score' => (int) $matches[1],
177                    'ping'  => (int) $matches[2],
178                    'name'  => $matches[3],
179                ];
180            }
181        }
182
183        $this->numplayers = count($players);
184        $this->playerkeys = ['name' => true, 'score' => true, 'ping' => true];
185        $this->players    = $players;
186    }
187}