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_shift;
16: use function array_slice;
17: use function count;
18: use function explode;
19: use function is_string;
20: use function preg_match;
21: use function trim;
22: use 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: */
28: class 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: }
188: