| 1: | <?php declare(strict_types=1); |
| 2: | |
| 3: | |
| 4: | |
| 5: | |
| 6: | |
| 7: | |
| 8: | |
| 9: | |
| 10: | |
| 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: | |
| 36: | |
| 37: | class DocumentProtocols |
| 38: | { |
| 39: | |
| 40: | private array $protocols = []; |
| 41: | |
| 42: | |
| 43: | private array $gameList = []; |
| 44: | |
| 45: | |
| 46: | private array $series = []; |
| 47: | private int $totalProtocols = 0; |
| 48: | private int $totalGames = 0; |
| 49: | |
| 50: | |
| 51: | |
| 52: | |
| 53: | |
| 54: | |
| 55: | public function __construct(private readonly string $protocolsDir = __DIR__ . '/ServerProtocols') |
| 56: | { |
| 57: | } |
| 58: | |
| 59: | |
| 60: | |
| 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: | |
| 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: | |
| 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: | |
| 129: | foreach ($this->protocols as $base => &$games) { |
| 130: | $games = array_unique($games); |
| 131: | sort($games); |
| 132: | } |
| 133: | |
| 134: | |
| 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: | |
| 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: | |
| 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: | $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: | |
| 187: | $this->totalProtocols = count($this->protocols); |
| 188: | $this->totalGames = count($this->gameList); |
| 189: | } |
| 190: | |
| 191: | |
| 192: | |
| 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: | |
| 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: | |
| 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: | |
| 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: | |
| 342: | |
| 343: | public function run(): void |
| 344: | { |
| 345: | $this->parseProtocols(); |
| 346: | $this->writeFiles(); |
| 347: | } |
| 348: | |
| 349: | |
| 350: | |
| 351: | |
| 352: | |
| 353: | |
| 354: | public function getSupportedGames(): array |
| 355: | { |
| 356: | return array_merge(...array_values($this->series)); |
| 357: | } |
| 358: | |
| 359: | |
| 360: | |
| 361: | |
| 362: | |
| 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: | |
| 445: | if (isset($argv) && __FILE__ === $argv[0]) { |
| 446: | $doc = new DocumentProtocols; |
| 447: | $doc->run(); |
| 448: | } |
| 449: | |