Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
91.97% |
229 / 249 |
|
77.78% |
7 / 9 |
CRAP | |
0.00% |
0 / 1 |
| DocumentProtocols | |
92.68% |
228 / 246 |
|
77.78% |
7 / 9 |
42.69 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| parseProtocols | |
77.46% |
55 / 71 |
|
0.00% |
0 / 1 |
23.13 | |||
| renderMarkdown | |
100.00% |
48 / 48 |
|
100.00% |
1 / 1 |
9 | |||
| renderHtml | |
100.00% |
48 / 48 |
|
100.00% |
1 / 1 |
8 | |||
| writeFiles | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
| run | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| getSupportedGames | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getGameSeries | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getBaseProtocol | |
100.00% |
69 / 69 |
|
100.00% |
1 / 1 |
1 | |||
| 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; |
| 14 | |
| 15 | use function array_keys; |
| 16 | use function array_merge; |
| 17 | use function array_search; |
| 18 | use function array_unique; |
| 19 | use function array_values; |
| 20 | use function basename; |
| 21 | use function class_exists; |
| 22 | use function count; |
| 23 | use function explode; |
| 24 | use function file_put_contents; |
| 25 | use function glob; |
| 26 | use function is_array; |
| 27 | use function sort; |
| 28 | use function strcasecmp; |
| 29 | use function uksort; |
| 30 | use function usort; |
| 31 | use function var_export; |
| 32 | use ReflectionClass; |
| 33 | |
| 34 | /** |
| 35 | * Utility class to generate markdown and HTML documentation of server protocols. |
| 36 | */ |
| 37 | class DocumentProtocols |
| 38 | { |
| 39 | /** @var array<string, array<string>> */ |
| 40 | private array $protocols = []; |
| 41 | |
| 42 | /** @var array<string> */ |
| 43 | private array $gameList = []; |
| 44 | |
| 45 | /** @var array<string, array<string>> */ |
| 46 | private array $series = []; |
| 47 | private int $totalProtocols = 0; |
| 48 | private int $totalGames = 0; |
| 49 | |
| 50 | /** |
| 51 | * Initializes the documentation generator with the protocols directory. |
| 52 | * |
| 53 | * @param string $protocolsDir Path to the server protocols directory |
| 54 | */ |
| 55 | public function __construct(private readonly string $protocolsDir = __DIR__ . '/ServerProtocols') |
| 56 | { |
| 57 | } |
| 58 | |
| 59 | /** |
| 60 | * parseProtocols method. |
| 61 | */ |
| 62 | public function parseProtocols(): void |
| 63 | { |
| 64 | $files = glob($this->protocolsDir . '/*.php'); |
| 65 | |
| 66 | if ($files === false) { |
| 67 | $files = []; |
| 68 | } |
| 69 | |
| 70 | $this->protocols = []; |
| 71 | $this->gameList = []; |
| 72 | |
| 73 | foreach ($files as $file) { |
| 74 | $className = basename($file, '.php'); |
| 75 | $fullClassName = 'Clansuite\\ServerQuery\\ServerProtocols\\' . $className; |
| 76 | |
| 77 | if (!class_exists($fullClassName)) { |
| 78 | continue; |
| 79 | } |
| 80 | |
| 81 | $reflection = new ReflectionClass($fullClassName); |
| 82 | |
| 83 | if (!$reflection->isInstantiable()) { |
| 84 | continue; |
| 85 | } |
| 86 | |
| 87 | /** @var CSQuery $instance */ |
| 88 | $instance = $reflection->newInstanceWithoutConstructor(); |
| 89 | |
| 90 | $parent = $reflection->getParentClass(); |
| 91 | $baseProtocol = $parent !== false ? $parent->getName() : 'CSQuery'; |
| 92 | |
| 93 | $name = $instance->name; |
| 94 | |
| 95 | if ($name === 'Unknown') { |
| 96 | var_export($className); |
| 97 | } |
| 98 | $supportedGames = $instance->supportedGames; |
| 99 | $protocol = $instance->protocol; |
| 100 | $game_series_list = $instance->game_series_list; |
| 101 | |
| 102 | $base = $this->getBaseProtocol($protocol); |
| 103 | |
| 104 | if (!isset($this->protocols[$base])) { |
| 105 | $this->protocols[$base] = []; |
| 106 | } |
| 107 | $this->protocols[$base] = array_merge($this->protocols[$base], $supportedGames); |
| 108 | } |
| 109 | |
| 110 | // Sort bases in custom order |
| 111 | $order = ['Steam', 'Half-Life', 'Gamespy', 'Gamespy 2', 'Gamespy 3', 'Quake', 'Quake 3', 'Unreal 2', 'Minecraft', 'Mumble', 'Teamspeak 3', 'SAMP', 'Factorio', 'ASE', 'Assetto Corsa', 'Battlefield', 'Cube Engine', 'DDnet', 'Eco', 'Farming Simulator', 'Palworld', 'SQP', 'Satisfactory', 'Starbound', 'Terraria', 'Tibia', 'Torque', 'Ventrilo', 'Launcher Protocol', 'Tribes 2', 'BeamMP']; |
| 112 | uksort($this->protocols, static function (string $a, string $b) use ($order): int |
| 113 | { |
| 114 | $posA = array_search($a, $order, true); |
| 115 | $posB = array_search($b, $order, true); |
| 116 | |
| 117 | if ($posA === false) { |
| 118 | $posA = 999; |
| 119 | } |
| 120 | |
| 121 | if ($posB === false) { |
| 122 | $posB = 999; |
| 123 | } |
| 124 | |
| 125 | return $posA <=> $posB; |
| 126 | }); |
| 127 | |
| 128 | // Deduplicate and sort games under each base |
| 129 | foreach ($this->protocols as $base => &$games) { |
| 130 | $games = array_unique($games); |
| 131 | sort($games); |
| 132 | } |
| 133 | |
| 134 | // Build gameList |
| 135 | $this->gameList = []; |
| 136 | |
| 137 | foreach ($this->protocols as $base => $games) { |
| 138 | foreach ($games as $game) { |
| 139 | $this->gameList[] = $game . ' - ' . $base; |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | // Sort gameList alphabetically |
| 144 | usort($this->gameList, static function (string $a, string $b): int |
| 145 | { |
| 146 | $gameA = explode(' - ', $a)[0]; |
| 147 | $gameB = explode(' - ', $b)[0]; |
| 148 | |
| 149 | return strcasecmp($gameA, $gameB); |
| 150 | }); |
| 151 | |
| 152 | // Build series |
| 153 | $this->series = []; |
| 154 | |
| 155 | foreach ($files as $file) { |
| 156 | $className = basename($file, '.php'); |
| 157 | $fullClassName = 'Clansuite\\ServerQuery\\ServerProtocols\\' . $className; |
| 158 | |
| 159 | if (!class_exists($fullClassName)) { |
| 160 | continue; |
| 161 | } |
| 162 | |
| 163 | $reflection = new ReflectionClass($fullClassName); |
| 164 | |
| 165 | if (!$reflection->isInstantiable()) { |
| 166 | continue; |
| 167 | } |
| 168 | |
| 169 | /** @var CSQuery $instance */ $instance = $reflection->newInstanceWithoutConstructor(); |
| 170 | $game_series_list = $instance->game_series_list ?? [$instance->name ?? 'Unknown']; |
| 171 | $supportedGames = $instance->supportedGames ?? []; |
| 172 | |
| 173 | foreach ($game_series_list as $series) { |
| 174 | if (!isset($this->series[$series])) { |
| 175 | $this->series[$series] = []; |
| 176 | } |
| 177 | $this->series[$series] = array_merge($this->series[$series], $supportedGames); |
| 178 | } |
| 179 | } |
| 180 | |
| 181 | foreach ($this->series as &$games) { |
| 182 | $games = array_unique($games); |
| 183 | sort($games); |
| 184 | } |
| 185 | |
| 186 | // Set totals |
| 187 | $this->totalProtocols = count($this->protocols); |
| 188 | $this->totalGames = count($this->gameList); |
| 189 | } |
| 190 | |
| 191 | /** |
| 192 | * renderMarkdown method. |
| 193 | */ |
| 194 | public function renderMarkdown(): string |
| 195 | { |
| 196 | $markdown = "## Clansuite Server Query\n"; |
| 197 | $markdown .= "### Table of Contents\n\n"; |
| 198 | $markdown .= "1. [Supported Server Protocols](#supported-server-protocols)\n"; |
| 199 | $markdown .= "2. [Supported Game and Voice Servers](#supported-game-and-voice-servers)\n"; |
| 200 | $markdown .= "3. [Game Series](#game-series)\n\n"; |
| 201 | $markdown .= "\n"; |
| 202 | $markdown .= "---\n"; |
| 203 | $markdown .= "\n"; |
| 204 | $markdown .= "### Supported Server Protocols\n\n"; |
| 205 | $markdown .= "Total Protocols: {$this->totalProtocols}\n"; |
| 206 | $markdown .= "Total Games: {$this->totalGames}\n\n"; |
| 207 | $markdown .= "This section lists the server protocols supported, organized in a hierarchical tree structure.\n"; |
| 208 | $markdown .= "Each protocol serves as a top-level category, with the games they support listed underneath.\n\n"; |
| 209 | $markdown .= "```\n"; |
| 210 | $markdown .= "Server Query Protocols\n"; |
| 211 | $totalBases = count($this->protocols); |
| 212 | $i = 0; |
| 213 | |
| 214 | foreach ($this->protocols as $base => $games) { |
| 215 | $isLastBase = $i === $totalBases - 1; |
| 216 | $baseConnector = $isLastBase ? '└── ' : '├── '; |
| 217 | $markdown .= $baseConnector . $base . "\n"; |
| 218 | |
| 219 | foreach ($games as $gIndex => $game) { |
| 220 | $isLastGame = $gIndex === count($games) - 1; |
| 221 | $gameConnector = $isLastBase ? ' ' : '│ '; |
| 222 | $markdown .= $gameConnector . $game . "\n"; |
| 223 | } |
| 224 | $i++; |
| 225 | } |
| 226 | $markdown .= "```\n\n"; |
| 227 | $markdown .= "### Supported Game and Voice Servers\n\n"; |
| 228 | $markdown .= "This is a table of all supported games with their corresponding protocols, including voice servers.\n\n"; |
| 229 | $markdown .= "| Number | Game Name | Server Protocol |\n"; |
| 230 | $markdown .= "|--------|-----------|-----------------|\n"; |
| 231 | |
| 232 | foreach ($this->gameList as $index => $game) { |
| 233 | $parts = explode(' - ', (string) $game, 2); |
| 234 | $gameName = $parts[0]; |
| 235 | $protocol = $parts[1] ?? ''; |
| 236 | $number = $index + 1; |
| 237 | $markdown .= "| {$number} | {$gameName} | {$protocol} |\n"; |
| 238 | } |
| 239 | $markdown .= "\n"; |
| 240 | $markdown .= "### Game Series\n\n"; |
| 241 | $markdown .= "This section lists games grouped by their series, regardless of protocol differences.\n\n"; |
| 242 | |
| 243 | foreach ($this->series as $series => $games) { |
| 244 | $markdown .= "#### {$series}\n\n"; |
| 245 | |
| 246 | if (is_array($games)) { |
| 247 | foreach ($games as $game) { |
| 248 | $markdown .= '- ' . (string) $game . "\n"; |
| 249 | } |
| 250 | } |
| 251 | $markdown .= "\n"; |
| 252 | } |
| 253 | $markdown .= "\n"; |
| 254 | |
| 255 | return $markdown; |
| 256 | } |
| 257 | |
| 258 | /** |
| 259 | * renderHtml method. |
| 260 | */ |
| 261 | public function renderHtml(): string |
| 262 | { |
| 263 | $html = "<!DOCTYPE html>\n<html>\n"; |
| 264 | $html .= "<head>\n<title>Supported Server Protocols by Clansuite Server Query</title>\n</head>\n"; |
| 265 | $html .= "<body>\n"; |
| 266 | $html .= "<h2>Clansuite Server Query</h2>\n"; |
| 267 | $html .= "Clansuite Server Query currently supports {$this->totalProtocols} server protocols and is compatible with {$this->totalGames} game and voice servers.\n"; |
| 268 | $html .= "<p>This document provides a complete list of supported protocols and games.</p>\n"; |
| 269 | $html .= "<p>If the server you are looking for is not included, you can either contribute by forking the project on <a href='https://github.com/Clansuite/ServerQuery'>GitHub</a> and add it yourself, or <a href='https://github.com/Clansuite/ServerQuery/issues'>submit a request</a> to have it added.</p>\n"; |
| 270 | $html .= "<p>Best regards,<br>Jens A. Koch</p>\n"; |
| 271 | |
| 272 | $html .= "<h3>Supported Server Protocols</h3>\n"; |
| 273 | $html .= "Clansuite Server Query currently supports {$this->totalProtocols} server protocols.\n"; |
| 274 | $html .= '<p>The protocols are displayed in a hierarchical tree structure.</p>'; |
| 275 | $html .= "<p>Each protocol appears as a top-level category, with the supported games organized beneath it.</p>\n"; |
| 276 | $html .= "<pre>\n"; |
| 277 | $html .= "Server Query Protocols\n"; |
| 278 | $totalBases = count($this->protocols); |
| 279 | $i = 0; |
| 280 | |
| 281 | foreach ($this->protocols as $base => $games) { |
| 282 | $isLastBase = $i === $totalBases - 1; |
| 283 | $baseConnector = $isLastBase ? '└── ' : '├── '; |
| 284 | $html .= $baseConnector . $base . "\n"; |
| 285 | |
| 286 | foreach ($games as $gIndex => $game) { |
| 287 | $isLastGame = $gIndex === count($games) - 1; |
| 288 | $gameConnector = $isLastBase ? ' ' : '│ '; |
| 289 | $html .= $gameConnector . $game . "\n"; |
| 290 | } |
| 291 | $i++; |
| 292 | } |
| 293 | $html .= "</pre>\n\n"; |
| 294 | $html .= "<h3>Supported Game Servers</h3>\n"; |
| 295 | $html .= "<p>The table below lists all supported games along with their corresponding server protocols.</p>\n"; |
| 296 | $html .= "<p>Each entry includes the game name and the protocol it uses for server communication.</p>\n"; |
| 297 | $html .= "<table>\n"; |
| 298 | $html .= "<thead>\n<tr>\n<th>#</th>\n<th>Game Name</th>\n<th>Server Protocol</th>\n</tr>\n</thead>\n"; |
| 299 | $html .= "<tbody>\n"; |
| 300 | |
| 301 | foreach ($this->gameList as $index => $game) { |
| 302 | $parts = explode(' - ', (string) $game, 2); |
| 303 | $gameName = $parts[0] ?? ''; |
| 304 | $protocol = $parts[1] ?? ''; |
| 305 | $number = $index + 1; |
| 306 | $html .= "<tr>\n<td>{$number}</td>\n<td>{$gameName}</td>\n<td>{$protocol}</td>\n</tr>\n"; |
| 307 | } |
| 308 | $html .= "</tbody>\n</table>\n\n"; |
| 309 | $html .= "<h3>Game Series</h3>\n"; |
| 310 | $html .= "<p>This section lists games organized by series, independent of their server protocols.</p>\n"; |
| 311 | |
| 312 | foreach ($this->series as $series => $games) { |
| 313 | /** @var array<string> $games */ |
| 314 | $html .= "<h4>{$series}</h4>\n<ul>\n"; |
| 315 | |
| 316 | foreach ($games as $game) { |
| 317 | $html .= "<li>{$game}</li>\n"; |
| 318 | } |
| 319 | $html .= "</ul>\n"; |
| 320 | } |
| 321 | $html .= "</body>\n</html>\n"; |
| 322 | |
| 323 | return $html; |
| 324 | } |
| 325 | |
| 326 | /** |
| 327 | * writeFiles method. |
| 328 | */ |
| 329 | public function writeFiles(string $outputDir = __DIR__ . '/../../'): void |
| 330 | { |
| 331 | $markdown = $this->renderMarkdown(); |
| 332 | $html = $this->renderHtml(); |
| 333 | |
| 334 | file_put_contents($outputDir . '/protocols.md', $markdown); |
| 335 | file_put_contents($outputDir . '/protocols.html', $html); |
| 336 | |
| 337 | print "Generated protocols.md and protocols.html\n"; |
| 338 | } |
| 339 | |
| 340 | /** |
| 341 | * run method. |
| 342 | */ |
| 343 | public function run(): void |
| 344 | { |
| 345 | $this->parseProtocols(); |
| 346 | $this->writeFiles(); |
| 347 | } |
| 348 | |
| 349 | /** |
| 350 | * getSupportedGames method. |
| 351 | * |
| 352 | * @return array<string> |
| 353 | */ |
| 354 | public function getSupportedGames(): array |
| 355 | { |
| 356 | return array_merge(...array_values($this->series)); |
| 357 | } |
| 358 | |
| 359 | /** |
| 360 | * getGameSeries method. |
| 361 | * |
| 362 | * @return array<string> |
| 363 | */ |
| 364 | public function getGameSeries(): array |
| 365 | { |
| 366 | return array_keys($this->series); |
| 367 | } |
| 368 | |
| 369 | private function getBaseProtocol(string $protocol): string |
| 370 | { |
| 371 | $map = [ |
| 372 | 'A2S' => 'Steam', |
| 373 | 'Halflife' => 'Half-Life', |
| 374 | 'Halflife2' => 'Half-Life', |
| 375 | 'Gamespy' => 'Gamespy', |
| 376 | 'Gamespy2' => 'Gamespy 2', |
| 377 | 'Gamespy3' => 'Gamespy 3', |
| 378 | 'Quake' => 'Quake', |
| 379 | 'Quake3' => 'Quake 3', |
| 380 | 'Quake4' => 'Quake', |
| 381 | 'doom3' => 'Quake 3', |
| 382 | 'etqw' => 'Quake 3', |
| 383 | 'fear' => 'Quake 3', |
| 384 | 'Unreal2' => 'Unreal 2', |
| 385 | 'minecraft' => 'Minecraft', |
| 386 | 'mumble' => 'Mumble', |
| 387 | 'teamspeak3' => 'Teamspeak 3', |
| 388 | 'samp' => 'SAMP', |
| 389 | 'Satisfactory' => 'Satisfactory', |
| 390 | 'starbound' => 'Starbound', |
| 391 | 'terraria' => 'Terraria', |
| 392 | 'tibia' => 'Tibia', |
| 393 | 'Torque' => 'Torque', |
| 394 | 'Tribes2' => 'Tribes 2', |
| 395 | 'ventrilo' => 'Ventrilo', |
| 396 | 'Skulltag' => 'Launcher Protocol', |
| 397 | 'Zandronum' => 'Launcher Protocol', |
| 398 | 'AgeOfTime' => 'Tribes 2', |
| 399 | 'Blockland' => 'Tribes 2', |
| 400 | 'beammp' => 'BeamMP', |
| 401 | 'CounterStrike16' => 'Half-Life', |
| 402 | 'assettocorsa' => 'Assetto Corsa', |
| 403 | 'Battlefield4' => 'Battlefield', |
| 404 | 'bc2' => 'Battlefield', |
| 405 | 'Frostbite' => 'Battlefield', |
| 406 | 'BF4' => 'Battlefield', |
| 407 | 'Cube' => 'Cube Engine', |
| 408 | 'ddnet' => 'DDnet', |
| 409 | 'arma' => 'Gamespy 2', |
| 410 | 'halo' => 'Gamespy 2', |
| 411 | 'swat4' => 'Gamespy 2', |
| 412 | 'Bf2' => 'Gamespy 3', |
| 413 | 'ut3' => 'Gamespy 3', |
| 414 | 'gamespy3' => 'Gamespy 3', |
| 415 | 'et' => 'Quake 3', |
| 416 | 'ql' => 'Quake 3', |
| 417 | 'StarWarsJK' => 'Quake 3', |
| 418 | 'urbanterror' => 'Quake 3', |
| 419 | 'wolf' => 'Quake 3', |
| 420 | 'killingfloor' => 'Unreal 2', |
| 421 | 'ravaged' => 'Unreal 2', |
| 422 | 'ro2' => 'Unreal 2', |
| 423 | 'ror' => 'Unreal 2', |
| 424 | 'sniperelite2' => 'Unreal 2', |
| 425 | 'conan' => 'Steam', |
| 426 | 'dayz' => 'Steam', |
| 427 | 'ffow' => 'Steam', |
| 428 | 'source' => 'Steam', |
| 429 | 'dods' => 'Steam', |
| 430 | 'scum' => 'Steam', |
| 431 | 'ASE' => 'ASE', |
| 432 | 'gta-san-andreas-mta' => 'ASE', |
| 433 | 'SQP' => 'SQP', |
| 434 | 'Factorio' => 'Factorio', |
| 435 | 'Eco' => 'Eco', |
| 436 | 'FarmingSimulator' => 'Farming Simulator', |
| 437 | 'Palworld' => 'Palworld', |
| 438 | ]; |
| 439 | |
| 440 | return $map[$protocol] ?? $protocol; |
| 441 | } |
| 442 | } |
| 443 | |
| 444 | // Run if called directly |
| 445 | if (isset($argv) && __FILE__ === $argv[0]) { |
| 446 | $doc = new DocumentProtocols; |
| 447 | $doc->run(); |
| 448 | } |