Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 1
CRAP
0.00% covered (danger)
0.00%
0 / 1
Halflife2
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 1
812
0.00% covered (danger)
0.00%
0 / 1
 query_server
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 1
812
1<?php
2
3declare(strict_types=1);
4
5/**
6 * Clansuite Server Query
7 *
8 * SPDX-FileCopyrightText: 2003-2025 Jens A. Koch
9 * SPDX-License-Identifier: MIT
10 *
11 * For the full copyright and license information, please view
12 * the LICENSE file that was distributed with this source code.
13 */
14
15namespace Clansuite\ServerQuery\ServerProtocols;
16
17use function count;
18use function explode;
19use function is_numeric;
20use function is_scalar;
21use function ord;
22use function preg_replace;
23use function strtolower;
24use function substr;
25use Override;
26
27/**
28 * This class implements the protocol used by Half-Life 2.
29 */
30class Halflife2 extends Halflife
31{
32    /**
33     * Protocol name.
34     */
35    public string $name = 'Half-Life 2';
36
37    /**
38     * Protocol identifier.
39     */
40    public string $protocol = 'Halflife2';
41
42    /**
43     * Game series.
44     *
45     * @var array<string>
46     */
47    public array $game_series_list = ['Half-Life'];
48
49    /**
50     * List of supported games.
51     *
52     * @var array<string>
53     */
54    public array $supportedGames = ['Half-Life 2'];
55
56    /**
57     * query_server method.
58     */
59    #[Override]
60    public function query_server(bool $getPlayers = true, bool $getRules = true): bool
61    {
62        if ($this->online) {
63            $this->reset();
64        }
65
66        $address = is_scalar($this->address) ? $this->address : '';
67        $port    = is_numeric($this->queryport) ? $this->queryport : 0;
68
69        $command = "\xFF\xFF\xFF\xFFT";
70
71        if (($result = $this->sendCommand($address, $port, $command)) === '' || ($result = $this->sendCommand($address, $port, $command)) === '0' || ($result = $this->sendCommand($address, $port, $command)) === false) {
72            return false;
73        }
74
75        $i = 5; // start after header
76
77        $this->gameversion = (string) ord($result[5]);
78        $this->hostport    = $this->queryport ?? 0;
79
80        $basic = explode("\x00", substr($result, 6));
81
82        // XXX: Replace old code
83        $this->rules['gamedir'] = '';
84
85        if (isset($basic[0])) {
86            $this->servertitle = $basic[0];
87        }
88
89        if (isset($basic[1])) {
90            $this->mapname = $basic[1];
91        }
92
93        if (isset($basic[2])) {
94            $this->rules['gamedir'] = $basic[2];
95        }
96
97        $this->gamename         = isset($basic[3]) ? preg_replace('/[ :]/', '_', strtolower($basic[3])) ?? '' : '';
98        $this->rules['steamid'] = ord($result[$i]) | (ord($result[$i + 1]) << 8);
99        $i += 2;
100        $this->numplayers          = ord(substr($result, $i++, 1));
101        $this->maxplayers          = ord(substr($result, $i++, 1));
102        $this->rules['botplayers'] = ord(substr($result, $i++, 1));
103        $this->rules['dedicated']  = ($result[$i++] === 'd' ? 'Yes' : 'No');
104        $this->rules['server_os']  = ($result[$i++] === 'l' ? 'Linux' : 'Windows');
105        $this->password            = ord(substr($result, $i++, 1));
106        $this->rules['secure']     = ($result[$i++] === '1' ? 'Yes' : 'No');
107
108        // Already normalized above
109
110        // do rules
111        $command = "\xFF\xFF\xFF\xFFV";
112
113        if (($result = $this->sendCommand($address, $port, $command)) === '' || ($result = $this->sendCommand($address, $port, $command)) === '0' || ($result = $this->sendCommand($address, $port, $command)) === false) {
114            return false;
115        }
116
117        $exploded_data = explode("\x00", $result);
118
119        $z = count($exploded_data);
120
121        $i = 1;
122
123        while ($i < $z) {
124            $key   = $exploded_data[$i++] ?? '';
125            $value = $exploded_data[$i++] ?? '';
126
127            switch ($key) {
128                case 'sv_password':
129                    $this->password = (int) $value;
130
131                    break;
132
133                case 'deathmatch':
134                    if ($value === '1') {
135                        $this->gametype = 'Deathmatch';
136                    }
137
138                    break;
139
140                case 'coop':
141                    if ($value === '1') {
142                        $this->gametype = 'Cooperative';
143                    }
144
145                    break;
146
147                default:
148                    $this->rules[$key] = $value;
149            }
150        }
151
152        if ($getPlayers) {
153            // do players
154            $command = "\xFF\xFF\xFF\xFFU";
155
156            if (($result = $this->sendCommand($address, $port, $command)) === '' || ($result = $this->sendCommand($address, $port, $command)) === '0' || ($result = $this->sendCommand($address, $port, $command)) === false) {
157                return false;
158            }
159
160            $this->processPlayers($result, $this->playerFormat, 8);
161
162            $this->playerkeys['name']  = true;
163            $this->playerkeys['score'] = true;
164            $this->playerkeys['time']  = true;
165        }
166
167        $this->online = true;
168
169        return true;
170    }
171}