Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Tibia
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 6
210
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 query
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 getProtocolName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getVersion
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 query_server
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 parseResponse
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
30
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
13namespace Clansuite\ServerQuery\ServerProtocols;
14
15use function fclose;
16use function fread;
17use function fsockopen;
18use function fwrite;
19use function is_array;
20use function simplexml_load_string;
21use function str_starts_with;
22use Clansuite\Capture\Protocol\ProtocolInterface;
23use Clansuite\Capture\ServerAddress;
24use Clansuite\Capture\ServerInfo;
25use Clansuite\ServerQuery\CSQuery;
26use Override;
27
28/**
29 * Tibia protocol implementation.
30 *
31 * Uses TCP protocol with XML response.
32 */
33class Tibia extends CSQuery implements ProtocolInterface
34{
35    /**
36     * Protocol name.
37     */
38    public string $name = 'Tibia';
39
40    /**
41     * List of supported games.
42     *
43     * @var array<string>
44     */
45    public array $supportedGames = ['Tibia'];
46
47    /**
48     * Protocol identifier.
49     */
50    public string $protocol = 'tibia';
51
52    /**
53     * Constructor.
54     */
55    public function __construct(?string $address = null, ?int $queryport = null)
56    {
57        parent::__construct($address, $queryport);
58    }
59
60    /**
61     * query method.
62     */
63    #[Override]
64    public function query(ServerAddress $addr): ServerInfo
65    {
66        $info         = new ServerInfo;
67        $info->online = false;
68
69        $socket = @fsockopen($addr->ip, $addr->port, $errno, $errstr, 5);
70
71        if ($socket === false) {
72            return $info;
73        }
74
75        // Send info packet
76        $packet = "\x06\x00\xFF\xFF\x69\x6E\x66\x6F";
77        fwrite($socket, $packet);
78
79        // Read response
80        $response = fread($socket, 4096);
81        fclose($socket);
82
83        if ($response !== false) {
84            $this->parseResponse($response, $info);
85            $info->online = true;
86        }
87
88        return $info;
89    }
90
91    /**
92     * getProtocolName method.
93     */
94    #[Override]
95    public function getProtocolName(): string
96    {
97        return $this->protocol;
98    }
99
100    /**
101     * getVersion method.
102     */
103    #[Override]
104    public function getVersion(ServerInfo $info): string
105    {
106        return $info->version ?? 'unknown';
107    }
108
109    /**
110     * query_server method.
111     */
112    #[Override]
113    public function query_server(bool $getPlayers = true, bool $getRules = true): bool
114    {
115        $addr = new ServerAddress($this->address ?? '', $this->queryport ?? 0);
116        $info = $this->query($addr);
117
118        $this->online      = $info->online;
119        $this->servertitle = $info->name ?? '';
120        $this->mapname     = $info->map ?? '';
121        $this->numplayers  = $info->players_current ?? 0;
122        $this->maxplayers  = $info->players_max ?? 0;
123        $this->players     = [];
124
125        foreach ($info->players as $player) {
126            if (!is_array($player)) {
127                continue;
128            }
129            $this->players[] = [
130                'name'  => $player['name'] ?? '',
131                'score' => $player['score'] ?? 0,
132                'time'  => $player['time'] ?? 0,
133            ];
134        }
135
136        return $this->online;
137    }
138
139    private function parseResponse(string $response, ServerInfo $info): void
140    {
141        $xmlDoc = @simplexml_load_string($response);
142
143        if ($xmlDoc === false) {
144            return;
145        }
146
147        // Parse serverinfo
148        $attributes            = $xmlDoc->serverinfo->attributes();
149        $info->name            = (string) ($attributes['servername'] ?? '');
150        $info->map             = (string) ($attributes['map_name'] ?? '');
151        $info->players_current = (int) ($attributes['players_online'] ?? 0);
152        $info->players_max     = (int) ($attributes['players_max'] ?? 0);
153        $info->version         = (string) ($attributes['server'] ?? '');
154
155        // Parse MOTD
156        $info->motd = (string) $xmlDoc->motd;
157
158        // Parse players
159        $info->players = [];
160        $attributes    = $xmlDoc->players->attributes();
161
162        if ($attributes !== null) {
163            foreach ($attributes as $key => $value) {
164                // Players are stored as attributes, parse them
165                if (str_starts_with($key, 'player')) {
166                    $info->players[] = [
167                        'name'  => (string) $value,
168                        'score' => 0,
169                        'time'  => 0,
170                    ];
171                }
172            }
173        }
174    }
175}