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 is_int;
16: use function is_string;
17: use function preg_match;
18: use function strlen;
19: use function substr;
20: use Override;
21:
22: /**
23: * Implements the query protocol for Enemy Territory: Quake Wars servers.
24: * Extends the Quake 4 protocol with game-specific query handling and data parsing.
25: */
26: class Etqw extends Quake4
27: {
28: /**
29: * Protocol name.
30: */
31: public string $name = 'Enemy Territory Quake Wars';
32:
33: /**
34: * List of supported games.
35: *
36: * @var array<string>
37: */
38: public array $supportedGames = ['Enemy Territory Quake Wars'];
39:
40: /**
41: * Protocol identifier.
42: */
43: public string $protocol = 'etqw';
44:
45: /**
46: * getProtocolName method.
47: */
48: #[Override]
49: public function getProtocolName(): string
50: {
51: return $this->protocol;
52: }
53:
54: /**
55: * query_server method.
56: */
57: #[Override]
58: public function query_server(mixed $getPlayers = true, mixed $getRules = true): bool
59: {
60: if ($this->online) {
61: $this->reset();
62: }
63:
64: // ETQW uses the same packet format as Doom3/Quake4
65: $command = "\xFF\xFFgetInfo\x00\x01\x00\x00\x00";
66:
67: if (($result = $this->sendCommand((string) $this->address, (int) $this->queryport, $command)) === '' || ($result = $this->sendCommand((string) $this->address, (int) $this->queryport, $command)) === '0' || ($result = $this->sendCommand((string) $this->address, (int) $this->queryport, $command)) === false) {
68: $this->errstr = 'No reply received';
69:
70: return false;
71: }
72:
73: // Parse the Doom3 packet format
74: if (strlen($result) < 16) {
75: $this->errstr = 'Invalid packet received';
76:
77: return false;
78: }
79:
80: // Skip the packet header (16 bytes: short check, 12 bytes check2, int challengeId, int protocol, char pack)
81: $data = substr($result, 16);
82:
83: // Parse server info
84: $info = $this->parseDoom3Info($data);
85:
86: // Extract basic server information
87: $this->gamename = isset($info['gamename']) && is_string($info['gamename']) ? $info['gamename'] : 'baseETQW-1';
88: $this->gameversion = $this->translateProtocolVersion(isset($info['protocol']) && is_string($info['protocol']) ? $info['protocol'] : (isset($info['si_version']) && is_string($info['si_version']) ? $info['si_version'] : ''));
89: $this->servertitle = isset($info['si_name']) && is_string($info['si_name']) ? $info['si_name'] : '';
90: $this->mapname = isset($info['si_map']) && is_string($info['si_map']) ? $info['si_map'] : '';
91: $this->gametype = isset($info['si_rules']) && is_string($info['si_rules']) ? $info['si_rules'] : '';
92: $this->maxplayers = isset($info['si_maxPlayers']) && is_int($info['si_maxPlayers']) ? $info['si_maxPlayers'] : 0;
93: $this->numplayers = 0; // Will be calculated from players
94:
95: // Store all rules
96: $this->rules = $info;
97:
98: // Get players if requested
99: if ($getPlayers && isset($info['si_players']) && is_string($info['si_players'])) {
100: $this->parsePlayers($info['si_players']);
101: }
102:
103: $this->online = true;
104:
105: return true;
106: }
107:
108: /**
109: * translateProtocolVersion method.
110: */
111: #[Override]
112: protected function translateProtocolVersion(string $protocol): string
113: {
114: // For ETQW, extract version like 1.5.12663.12663 from the string
115: if (preg_match('/(\d+\.\d+\.\d+\.\d+)/', $protocol, $matches) !== false && isset($matches[1])) {
116: return 'v' . $matches[1];
117: }
118:
119: return $protocol;
120: }
121: }
122: