Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Palworld
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 6
380
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 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 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
 queryHTTP
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
110
 makeAPIRequest
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
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 const JSON_ERROR_NONE;
16use function base64_encode;
17use function file_get_contents;
18use function is_array;
19use function json_decode;
20use function json_last_error;
21use function stream_context_create;
22use Clansuite\Capture\Protocol\ProtocolInterface;
23use Clansuite\Capture\ServerAddress;
24use Clansuite\Capture\ServerInfo;
25use Clansuite\ServerQuery\CSQuery;
26use 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 */
36class 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}