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 const JSON_ERROR_NONE;
16: use function base64_encode;
17: use function file_get_contents;
18: use function is_array;
19: use function json_decode;
20: use function json_last_error;
21: use function stream_context_create;
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: * Palworld server protocol implementation.
30: *
31: * Palworld uses REST API with basic authentication.
32: * Default port is 8212.
33: *
34: * @see https://docs.palworldgame.com/
35: */
36: class Palworld extends CSQuery implements ProtocolInterface
37: {
38: /**
39: * Protocol name.
40: */
41: public string $name = 'Palworld';
42:
43: /**
44: * Protocol identifier.
45: */
46: public string $protocol = 'palworld';
47:
48: /**
49: * Constructor.
50: */
51: public function __construct(?string $address = null, ?int $queryport = null)
52: {
53: parent::__construct($address, $queryport);
54: }
55:
56: /**
57: * query method.
58: */
59: #[Override]
60: public function query(ServerAddress $addr): ServerInfo
61: {
62: $info = new ServerInfo;
63: $info->online = false;
64:
65: if ($this->queryHTTP($addr, $info)) {
66: $info->online = true;
67: }
68:
69: return $info;
70: }
71:
72: /**
73: * getProtocolName method.
74: */
75: #[Override]
76: public function getProtocolName(): string
77: {
78: return $this->protocol;
79: }
80:
81: /**
82: * getVersion method.
83: */
84: #[Override]
85: public function getVersion(ServerInfo $info): string
86: {
87: return $info->gameversion ?? 'unknown';
88: }
89:
90: private function queryHTTP(ServerAddress $addr, ServerInfo $info): bool
91: {
92: $host = $addr->ip;
93: $port = $addr->port;
94:
95: // Try to get server info
96: $infoData = $this->makeAPIRequest($host, $port, 'info');
97:
98: if ($infoData === null || $infoData === []) {
99: return false;
100: }
101:
102: // Try to get players
103: $playersData = $this->makeAPIRequest($host, $port, 'players');
104:
105: // Try to get metrics
106: $metricsData = $this->makeAPIRequest($host, $port, 'metrics');
107:
108: // Parse the data
109: $info->address = $host . ':' . $port;
110: $info->queryport = $port;
111: $info->gamename = 'Palworld';
112: $info->servertitle = (string) ($infoData['servername'] ?? '');
113: $info->mapname = ''; // Palworld doesn't have traditional maps
114: $info->gametype = '';
115: $info->gameversion = (string) ($infoData['version'] ?? '');
116:
117: // Get player count from metrics if available
118: if ($metricsData !== null && $metricsData !== []) {
119: $info->numplayers = (int) ($metricsData['currentplayernum'] ?? 0);
120: $info->maxplayers = (int) ($metricsData['maxplayernum'] ?? 0);
121: } else {
122: $info->numplayers = 0;
123: $info->maxplayers = 0;
124: }
125:
126: $info->password = false; // Assume no password for now
127:
128: // Parse players
129: if ($playersData !== null && isset($playersData['players']) && is_array($playersData['players'])) {
130: $info->players = [];
131:
132: foreach ($playersData['players'] as $player) {
133: if (is_array($player)) {
134: $info->players[] = [
135: 'name' => (string) ($player['name'] ?? ''),
136: 'score' => 0, // Palworld doesn't have scores
137: 'time' => 0, // Palworld doesn't track play time in API
138: ];
139: }
140: }
141: }
142:
143: // Add server rules/settings
144: $info->rules = [];
145:
146: $info->rules['version'] = $infoData['version'] ?? '';
147: $info->rules['servername'] = $infoData['servername'] ?? '';
148: $info->rules['description'] = $infoData['description'] ?? '';
149:
150: return true;
151: }
152:
153: /**
154: * @return ?array<mixed>
155: */
156: private function makeAPIRequest(string $host, int $port, string $endpoint): ?array
157: {
158: $url = "http://{$host}:{$port}/v1/api/{$endpoint}";
159:
160: $context = stream_context_create([
161: 'http' => [
162: 'timeout' => 5,
163: 'header' => 'Authorization: Basic ' . base64_encode('admin:admin'), // Default credentials
164: ],
165: ]);
166:
167: $response = @file_get_contents($url, false, $context);
168:
169: if ($response === false) {
170: return null;
171: }
172:
173: $data = json_decode($response, true);
174:
175: if (json_last_error() !== JSON_ERROR_NONE) {
176: return null;
177: }
178:
179: return is_array($data) ? $data : null;
180: }
181: }
182: