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 fclose;
16: use function fread;
17: use function fsockopen;
18: use function fwrite;
19: use function is_array;
20: use function simplexml_load_string;
21: use function str_starts_with;
22: use Clansuite\Capture\Protocol\ProtocolInterface;
23: use Clansuite\Capture\ServerAddress;
24: use Clansuite\Capture\ServerInfo;
25: use Clansuite\ServerQuery\CSQuery;
26: use Override;
27:
28: /**
29: * Tibia protocol implementation.
30: *
31: * Uses TCP protocol with XML response.
32: */
33: class 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: }
176: