mirror of
https://github.com/NamelessMC/Nameless
synced 2026-08-18 06:29:04 -04:00
458 lines
18 KiB
PHP
458 lines
18 KiB
PHP
<?php
|
|
|
|
use xPaw\MinecraftPing;
|
|
use xPaw\MinecraftQuery;
|
|
|
|
/**
|
|
* Abstraction over xPaw\MinecraftQuery & xPaw\MinecraftPing to make them fit with our needs.
|
|
*
|
|
* @package NamelessMC\Minecraft
|
|
* @author Samerton
|
|
* @version 2.0.0-pr13
|
|
* @license MIT
|
|
*/
|
|
class MCQuery
|
|
{
|
|
private const COLOUR_CHAR = '§';
|
|
|
|
private const COLOURS = [
|
|
'AA0000' => '4',
|
|
'FF5555' => 'c',
|
|
'FFAA00' => '6',
|
|
'FFFF55' => 'e',
|
|
'00AA00' => '2',
|
|
'55FF55' => 'a',
|
|
'55FFFF' => 'b',
|
|
'00AAAA' => '3',
|
|
'0000AA' => '1',
|
|
'5555FF' => '9',
|
|
'FF55FF' => 'd',
|
|
'AA00AA' => '5',
|
|
'FFFFFF' => 'f',
|
|
'AAAAAA' => '7',
|
|
'555555' => '8',
|
|
'000000' => '0',
|
|
];
|
|
|
|
/**
|
|
* Query a single server.
|
|
*
|
|
* @param array $ip Array ['ip' => string, 'pre' => int] - 'ip' contains ip:port, 'pre' 1 for pre-Minecraft 1.7 otherwise 0
|
|
* @param string $type Type of query to use (`internal` or `external`).
|
|
* @param bool $bedrock Whether this is a Bedrock server or not.
|
|
* @param Language $language Query language object.
|
|
* @return array Array containing query result.
|
|
*/
|
|
public static function singleQuery(array $ip, string $type, bool $bedrock, Language $language): array
|
|
{
|
|
try {
|
|
$query_ip = explode(':', $ip['ip']);
|
|
if ($type == 'internal') {
|
|
// Internal query
|
|
if (count($query_ip) == 1 || (strlen($query_ip[1]) == 2 && empty($query_ip[1]))) {
|
|
$query_ip[1] = 25565;
|
|
}
|
|
|
|
if (count($query_ip) != 2) {
|
|
return [
|
|
'error' => true,
|
|
'value' => 'split IP by : must contain exactly two components',
|
|
];
|
|
}
|
|
|
|
if (!$bedrock) {
|
|
$ping = new MinecraftPing($query_ip[0], $query_ip[1], 5);
|
|
|
|
if ($ip['pre'] == 1) {
|
|
$query = $ping->QueryOldPre17();
|
|
} else {
|
|
$query = $ping->Query();
|
|
}
|
|
|
|
$ping->close();
|
|
|
|
if (isset($query['players'])) {
|
|
$player_list = $query['players']['sample'] ?? [];
|
|
|
|
return [
|
|
'status_value' => 1,
|
|
'status' => $language->get('general', 'online'),
|
|
'player_count' => Output::getClean($query['players']['online']),
|
|
'player_count_max' => Output::getClean($query['players']['max']),
|
|
'player_list' => $player_list,
|
|
'format_player_list' => self::formatPlayerList($player_list),
|
|
'x_players_online' => $language->get('general', 'currently_x_players_online', ['count' => Output::getClean($query['players']['online'])]),
|
|
'motd' => self::getMotd(
|
|
is_string($text = $query['description']) ? $text : $text['text'],
|
|
$query['description']['extra'] ?? [],
|
|
),
|
|
'version' => $query['version']['name'],
|
|
];
|
|
}
|
|
} else {
|
|
$querier = new MinecraftQuery();
|
|
$querier->ConnectBedrock($query_ip[0], $query_ip[1], 5);
|
|
$query = $querier->GetInfo();
|
|
|
|
return [
|
|
'status_value' => 1,
|
|
'status' => $language->get('general', 'online'),
|
|
'player_count' => Output::getClean($query['Players']),
|
|
'player_count_max' => Output::getClean($query['MaxPlayers']),
|
|
'x_players_online' => $language->get('general', 'currently_x_players_online', ['count' => Output::getClean($query['Players'])]),
|
|
'motd' => $query['HostName'],
|
|
'version' => $query['Version'],
|
|
];
|
|
}
|
|
|
|
return [
|
|
'status_value' => 0,
|
|
'status' => $language->get('general', 'offline'),
|
|
'server_offline' => $language->get('general', 'server_offline'),
|
|
];
|
|
}
|
|
|
|
// External query
|
|
if (count($query_ip) > 2) {
|
|
return [
|
|
'error' => true,
|
|
'value' => 'split IP by : contains more than two components',
|
|
];
|
|
}
|
|
|
|
$query = ExternalMCQuery::query($query_ip[0], $query_ip[1] ?? ($bedrock ? 19132 : 25565), $bedrock);
|
|
|
|
if ($query !== false && !$query->error && isset($query->response)) {
|
|
$player_list = $query->response->players->list ?? [];
|
|
|
|
return [
|
|
'status_value' => 1,
|
|
'status' => $language->get('general', 'online'),
|
|
'player_count' => Output::getClean($query->response->players->online),
|
|
'player_count_max' => Output::getClean($query->response->players->max),
|
|
'player_list' => $player_list,
|
|
'format_player_list' => self::formatPlayerList($player_list),
|
|
'x_players_online' => $language->get('general', 'currently_x_players_online', ['count' => Output::getClean($query->response->players->online)]),
|
|
// TODO: external query does not return bedrock MOTD at all
|
|
'motd' => self::getMotd(
|
|
json_decode(json_encode($query->response->description->text), true) ?? '',
|
|
json_decode(json_encode($query->response->description->extra), true) ?? []
|
|
),
|
|
];
|
|
}
|
|
|
|
return [
|
|
'status_value' => 0,
|
|
'status' => $language->get('general', 'offline'),
|
|
'server_offline' => $language->get('general', 'server_offline'),
|
|
];
|
|
} catch (Exception $e) {
|
|
$error = $e->getMessage();
|
|
|
|
$query_ip = explode(':', $ip['ip']);
|
|
|
|
DB::getInstance()->insert('query_errors', [
|
|
'date' => date('U'),
|
|
'error' => $error,
|
|
'ip' => $query_ip[0],
|
|
'port' => $query_ip[1] ?? 25565,
|
|
]);
|
|
|
|
return [
|
|
'error' => true,
|
|
'value' => $error,
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Formats a list of players into something useful for the frontend.
|
|
*
|
|
* @param array $player_list Unformatted array of players in format 'id' => string (UUID), 'name' => string (username)
|
|
* @return array Array of formatted players
|
|
**/
|
|
public static function formatPlayerList(array $player_list): array
|
|
{
|
|
$formatted = [];
|
|
|
|
$integration = Integrations::getInstance()->getIntegration('Minecraft');
|
|
foreach ($player_list as $player) {
|
|
$player = (array) $player;
|
|
|
|
$integration_user = new IntegrationUser($integration, str_replace('-', '', $player['id']), 'identifier');
|
|
if ($integration_user->exists()) {
|
|
$user = $integration_user->getUser();
|
|
if ($user->exists()) {
|
|
$avatar = $user->getAvatar();
|
|
$profile = $user->getProfileURL();
|
|
} else {
|
|
$avatar = AvatarSource::getAvatarFromUUID($player['id']);
|
|
$profile = '#';
|
|
}
|
|
} else {
|
|
$avatar = AvatarSource::getAvatarFromUUID($player['id']);
|
|
$profile = '#';
|
|
}
|
|
|
|
$formatted[] = [
|
|
'username' => Output::getClean($player['name']),
|
|
'uuid' => Output::getClean($player['id']),
|
|
'avatar' => $avatar,
|
|
'profile' => $profile,
|
|
];
|
|
}
|
|
|
|
return $formatted;
|
|
}
|
|
|
|
/**
|
|
* Query multiple servers.
|
|
*
|
|
* @param array $servers Servers
|
|
* @param string $type Type of query to use (internal or external)
|
|
* @param Language $language Query language object
|
|
* @param bool $accumulate Whether to return as one accumulated result or not
|
|
* @return array Array containing query result
|
|
*/
|
|
public static function multiQuery(array $servers, string $type, Language $language, bool $accumulate): array
|
|
{
|
|
$to_return = [];
|
|
$total_count = 0;
|
|
$status = 0;
|
|
|
|
if ($type === 'internal') {
|
|
foreach ($servers as $server) {
|
|
$query_ip = explode(':', $server['ip']);
|
|
if (count($query_ip) > 2) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
if ($server['bedrock']) {
|
|
$ping = new MinecraftQuery();
|
|
$ping->ConnectBedrock($query_ip[0], $query_ip[1] ?? 19132, 5);
|
|
$query = $ping->GetInfo();
|
|
} else {
|
|
$ping = new MinecraftPing($query_ip[0], $query_ip[1] ?? 25565, 5);
|
|
|
|
if ($server['pre'] == 1) {
|
|
$query = $ping->QueryOldPre17();
|
|
} else {
|
|
$query = $ping->Query();
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
$query = [];
|
|
|
|
DB::getInstance()->insert('query_errors', [
|
|
'date' => date('U'),
|
|
'error' => $e->getMessage(),
|
|
'ip' => $query_ip[0],
|
|
'port' => ($query_ip[1] ?? ($server['bedrock'] ? 19132 : 25565)),
|
|
]);
|
|
}
|
|
|
|
// bedrock
|
|
if ($server['bedrock']) {
|
|
if ($accumulate === false) {
|
|
$to_return[] = [
|
|
'name' => Output::getClean($server['name']),
|
|
'status_value' => 1,
|
|
'status' => $language->get('general', 'online'),
|
|
'player_count' => Output::getClean($query['Players']),
|
|
'player_count_max' => Output::getClean($query['MaxPlayers']),
|
|
'x_players_online' => $language->get('general', 'currently_x_players_online', ['count' => Output::getClean($query['Players'])]),
|
|
];
|
|
} else {
|
|
if ($status == 0) {
|
|
$status = 1;
|
|
}
|
|
$total_count += $query['Players'];
|
|
}
|
|
} elseif (isset($query['players'])) {
|
|
if ($accumulate === false) {
|
|
$to_return[] = [
|
|
'name' => Output::getClean($server['name']),
|
|
'status_value' => 1,
|
|
'status' => $language->get('general', 'online'),
|
|
'player_count' => Output::getClean($query['players']['online']),
|
|
'player_count_max' => Output::getClean($query['players']['max']),
|
|
'x_players_online' => $language->get('general', 'currently_x_players_online', ['count' => Output::getClean($query['players']['online'])]),
|
|
];
|
|
} else {
|
|
if ($status == 0) {
|
|
$status = 1;
|
|
}
|
|
$total_count += $query['players']['online'];
|
|
}
|
|
} elseif ($accumulate === true) {
|
|
$to_return[] = [
|
|
'name' => Output::getClean($server['name']),
|
|
'status_value' => 0,
|
|
'status' => $language->get('general', 'offline'),
|
|
'server_offline' => $language->get('general', 'server_offline'),
|
|
];
|
|
}
|
|
}
|
|
|
|
if (isset($ping) && $ping instanceof MinecraftPing) {
|
|
$ping->close();
|
|
}
|
|
} else {
|
|
// External query
|
|
foreach ($servers as $server) {
|
|
$query_ip = explode(':', $server['ip']);
|
|
if (count($query_ip) > 2) {
|
|
continue;
|
|
}
|
|
|
|
$is_bedrock = isset($server['bedrock']) && $server['bedrock'] === true;
|
|
|
|
$query = ExternalMCQuery::query($query_ip[0], $query_ip[1] ?? ($is_bedrock ? 19132 : 25565), $is_bedrock);
|
|
|
|
if ($query !== false && !$query->error && isset($query->response)) {
|
|
if ($accumulate === false) {
|
|
$to_return[] = [
|
|
'name' => Output::getClean($server['name']),
|
|
'status_value' => 1,
|
|
'status' => $language->get('general', 'online'),
|
|
'player_count' => Output::getClean($query->response->players->online),
|
|
'player_count_max' => Output::getClean($query->response->players->max),
|
|
'x_players_online' => $language->get('general', 'currently_x_players_online', ['count' => Output::getClean($query->response->players->online)]),
|
|
];
|
|
} else {
|
|
if ($status == 0) {
|
|
$status = 1;
|
|
}
|
|
$total_count += $query->response->players->online;
|
|
}
|
|
} elseif ($accumulate === true) {
|
|
$to_return[] = [
|
|
'name' => Output::getClean($server['name']),
|
|
'status_value' => 0,
|
|
'status' => $language->get('general', 'offline'),
|
|
'server_offline' => $language->get('general', 'server_offline'),
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($accumulate === true) {
|
|
$to_return = [
|
|
'status_value' => $status,
|
|
'status' => $status == 1
|
|
? $language->get('general', 'online')
|
|
: $language->get('general', 'offline'),
|
|
'status_full' => $status == 1
|
|
? $language->get('general', 'currently_x_players_online', ['count' => $total_count])
|
|
: $language->get('general', 'server_offline'),
|
|
'player_count' => $total_count,
|
|
];
|
|
}
|
|
|
|
return $to_return;
|
|
}
|
|
|
|
/**
|
|
* Convert a Minecraft MOTD to its legacy colour codes.
|
|
*
|
|
* @param string $text Legacy MOTD single-line text
|
|
* @param array $modern_format Array of modern MOTD format strings
|
|
* @return string MOTD as legacy MC colours
|
|
*/
|
|
private static function getMotd(string $text, array $modern_format): string
|
|
{
|
|
if ($text !== '') {
|
|
return $text;
|
|
}
|
|
|
|
// some servers (originrealms) return a weird MOTD
|
|
if (count($modern_format) === 1 && is_array($modern_format[0])) {
|
|
$modern_format = $modern_format[0]['extra'] ?? [];
|
|
}
|
|
// and sometimes it's doubly nested...
|
|
if (count($modern_format) === 1 && is_array($modern_format[0])) {
|
|
$modern_format = $modern_format[0]['extra'] ?? [];
|
|
}
|
|
|
|
$motd = '';
|
|
foreach ($modern_format as $word) {
|
|
if (!is_array($word)) {
|
|
continue;
|
|
}
|
|
|
|
$motd .= self::COLOUR_CHAR . 'r';
|
|
if (isset($word['color'])) {
|
|
$motd .= self::getColor($word['color']);
|
|
}
|
|
|
|
if (isset($word['bold']) && $word['bold'] === true) {
|
|
$motd .= self::COLOUR_CHAR . 'l';
|
|
}
|
|
|
|
$motd .= trim($word['text'], ' ');
|
|
}
|
|
|
|
return trim($motd);
|
|
}
|
|
|
|
/**
|
|
* Find the closest MC colour to a given hex colour.
|
|
*
|
|
* @param string $rgb RGB colour code
|
|
* @return string The closest Minecraft colour code to the given RGB value
|
|
*/
|
|
private static function getColor(string $rgb): string
|
|
{
|
|
if (strpos($rgb, '#') === 0) {
|
|
$rgb = substr($rgb, 1);
|
|
}
|
|
|
|
$smallestDiff = null;
|
|
$closestColor = '';
|
|
foreach (self::COLOURS as $hex => $char) {
|
|
$diff = self::colorDiff($hex, $rgb);
|
|
if ($smallestDiff === null || $diff < $smallestDiff) {
|
|
$smallestDiff = $diff;
|
|
$closestColor = $char;
|
|
}
|
|
}
|
|
|
|
return self::COLOUR_CHAR . $closestColor;
|
|
}
|
|
|
|
/**
|
|
* Find the numerical difference between two RGB colours.
|
|
*
|
|
* @param mixed $rgb1 RGB colour code
|
|
* @param mixed $rgb2 RGB colour code
|
|
* @return int The difference between two RGB colours
|
|
*/
|
|
private static function colorDiff($rgb1, $rgb2): int
|
|
{
|
|
$red1 = hexdec(substr($rgb1, 0, 2));
|
|
$green1 = hexdec(substr($rgb1, 2, 2));
|
|
$blue1 = hexdec(substr($rgb1, 4, 2));
|
|
|
|
$red2_substr = substr($rgb2, 0, 2);
|
|
$green2_substr = substr($rgb2, 2, 2);
|
|
$blue2_substr = substr($rgb2, 4, 2);
|
|
|
|
$red2 = 0;
|
|
if (ctype_xdigit($red2_substr)) {
|
|
$red2 = hexdec($red2_substr);
|
|
}
|
|
|
|
$green2 = 0;
|
|
if (ctype_xdigit($green2_substr)) {
|
|
$green2 = hexdec($green2_substr);
|
|
}
|
|
|
|
$blue2 = 0;
|
|
if (ctype_xdigit($blue2_substr)) {
|
|
$blue2 = hexdec($blue2_substr);
|
|
}
|
|
|
|
return abs($red1 - $red2) + abs($green1 - $green2) + abs($blue1 - $blue2);
|
|
}
|
|
}
|