mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-06-13 06:55:29 +00:00
I thought I did this already in eff856d8e513a1f01eca16ab55bacf6e83399527, but it looks like my brain slipped a gear. Without this change, it's possible to crash the server by specifying an invalid generator for the default world if it doesn't yet exist.
1848 lines
60 KiB
PHP
1848 lines
60 KiB
PHP
<?php
|
|
|
|
/*
|
|
*
|
|
* ____ _ _ __ __ _ __ __ ____
|
|
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
|
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
|
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
|
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* @author PocketMine Team
|
|
* @link http://www.pocketmine.net/
|
|
*
|
|
*
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* PocketMine-MP is the Minecraft: PE multiplayer server software
|
|
* Homepage: http://www.pocketmine.net/
|
|
*/
|
|
namespace pocketmine;
|
|
|
|
use pocketmine\command\Command;
|
|
use pocketmine\command\CommandSender;
|
|
use pocketmine\command\SimpleCommandMap;
|
|
use pocketmine\console\ConsoleCommandSender;
|
|
use pocketmine\console\ConsoleReaderThread;
|
|
use pocketmine\crafting\CraftingManager;
|
|
use pocketmine\crafting\CraftingManagerFromDataHelper;
|
|
use pocketmine\crash\CrashDump;
|
|
use pocketmine\crash\CrashDumpRenderer;
|
|
use pocketmine\entity\EntityDataHelper;
|
|
use pocketmine\entity\Location;
|
|
use pocketmine\event\HandlerListManager;
|
|
use pocketmine\event\player\PlayerCreationEvent;
|
|
use pocketmine\event\player\PlayerDataSaveEvent;
|
|
use pocketmine\event\player\PlayerLoginEvent;
|
|
use pocketmine\event\server\CommandEvent;
|
|
use pocketmine\event\server\DataPacketSendEvent;
|
|
use pocketmine\event\server\QueryRegenerateEvent;
|
|
use pocketmine\lang\KnownTranslationFactory;
|
|
use pocketmine\lang\Language;
|
|
use pocketmine\lang\LanguageNotFoundException;
|
|
use pocketmine\lang\Translatable;
|
|
use pocketmine\nbt\BigEndianNbtSerializer;
|
|
use pocketmine\nbt\NbtDataException;
|
|
use pocketmine\nbt\tag\CompoundTag;
|
|
use pocketmine\nbt\TreeRoot;
|
|
use pocketmine\network\mcpe\compression\CompressBatchPromise;
|
|
use pocketmine\network\mcpe\compression\CompressBatchTask;
|
|
use pocketmine\network\mcpe\compression\Compressor;
|
|
use pocketmine\network\mcpe\compression\ZlibCompressor;
|
|
use pocketmine\network\mcpe\encryption\EncryptionContext;
|
|
use pocketmine\network\mcpe\NetworkSession;
|
|
use pocketmine\network\mcpe\PacketBroadcaster;
|
|
use pocketmine\network\mcpe\protocol\ClientboundPacket;
|
|
use pocketmine\network\mcpe\protocol\ProtocolInfo;
|
|
use pocketmine\network\mcpe\protocol\serializer\PacketBatch;
|
|
use pocketmine\network\mcpe\raklib\RakLibInterface;
|
|
use pocketmine\network\Network;
|
|
use pocketmine\network\NetworkInterfaceStartException;
|
|
use pocketmine\network\query\DedicatedQueryNetworkInterface;
|
|
use pocketmine\network\query\QueryHandler;
|
|
use pocketmine\network\query\QueryInfo;
|
|
use pocketmine\network\upnp\UPnPNetworkInterface;
|
|
use pocketmine\permission\BanList;
|
|
use pocketmine\permission\DefaultPermissions;
|
|
use pocketmine\player\GameMode;
|
|
use pocketmine\player\OfflinePlayer;
|
|
use pocketmine\player\Player;
|
|
use pocketmine\player\PlayerInfo;
|
|
use pocketmine\plugin\PharPluginLoader;
|
|
use pocketmine\plugin\Plugin;
|
|
use pocketmine\plugin\PluginEnableOrder;
|
|
use pocketmine\plugin\PluginGraylist;
|
|
use pocketmine\plugin\PluginManager;
|
|
use pocketmine\plugin\PluginOwned;
|
|
use pocketmine\plugin\ScriptPluginLoader;
|
|
use pocketmine\promise\Promise;
|
|
use pocketmine\promise\PromiseResolver;
|
|
use pocketmine\resourcepacks\ResourcePackManager;
|
|
use pocketmine\scheduler\AsyncPool;
|
|
use pocketmine\snooze\SleeperHandler;
|
|
use pocketmine\snooze\SleeperNotifier;
|
|
use pocketmine\stats\SendUsageTask;
|
|
use pocketmine\timings\Timings;
|
|
use pocketmine\timings\TimingsHandler;
|
|
use pocketmine\updater\UpdateChecker;
|
|
use pocketmine\utils\AssumptionFailedError;
|
|
use pocketmine\utils\Config;
|
|
use pocketmine\utils\Filesystem;
|
|
use pocketmine\utils\Internet;
|
|
use pocketmine\utils\MainLogger;
|
|
use pocketmine\utils\NotCloneable;
|
|
use pocketmine\utils\NotSerializable;
|
|
use pocketmine\utils\Process;
|
|
use pocketmine\utils\SignalHandler;
|
|
use pocketmine\utils\Terminal;
|
|
use pocketmine\utils\TextFormat;
|
|
use pocketmine\utils\Utils;
|
|
use pocketmine\world\format\Chunk;
|
|
use pocketmine\world\format\io\WorldProviderManager;
|
|
use pocketmine\world\format\io\WritableWorldProviderManagerEntry;
|
|
use pocketmine\world\generator\Generator;
|
|
use pocketmine\world\generator\GeneratorManager;
|
|
use pocketmine\world\generator\InvalidGeneratorOptionsException;
|
|
use pocketmine\world\World;
|
|
use pocketmine\world\WorldCreationOptions;
|
|
use pocketmine\world\WorldManager;
|
|
use Ramsey\Uuid\UuidInterface;
|
|
use Webmozart\PathUtil\Path;
|
|
use function array_sum;
|
|
use function base64_encode;
|
|
use function cli_set_process_title;
|
|
use function copy;
|
|
use function count;
|
|
use function date;
|
|
use function fclose;
|
|
use function file_exists;
|
|
use function file_get_contents;
|
|
use function file_put_contents;
|
|
use function filemtime;
|
|
use function fopen;
|
|
use function get_class;
|
|
use function ini_set;
|
|
use function is_array;
|
|
use function is_dir;
|
|
use function is_object;
|
|
use function is_resource;
|
|
use function is_string;
|
|
use function json_decode;
|
|
use function max;
|
|
use function microtime;
|
|
use function min;
|
|
use function mkdir;
|
|
use function ob_end_flush;
|
|
use function preg_replace;
|
|
use function realpath;
|
|
use function register_shutdown_function;
|
|
use function rename;
|
|
use function round;
|
|
use function sleep;
|
|
use function spl_object_id;
|
|
use function sprintf;
|
|
use function str_repeat;
|
|
use function str_replace;
|
|
use function stripos;
|
|
use function strlen;
|
|
use function strrpos;
|
|
use function strtolower;
|
|
use function strval;
|
|
use function time;
|
|
use function touch;
|
|
use function trim;
|
|
use function yaml_parse;
|
|
use function zlib_decode;
|
|
use function zlib_encode;
|
|
use const DIRECTORY_SEPARATOR;
|
|
use const PHP_EOL;
|
|
use const PHP_INT_MAX;
|
|
use const PTHREADS_INHERIT_NONE;
|
|
use const ZLIB_ENCODING_GZIP;
|
|
|
|
/**
|
|
* The class that manages everything
|
|
*/
|
|
class Server{
|
|
use NotCloneable;
|
|
use NotSerializable;
|
|
|
|
public const BROADCAST_CHANNEL_ADMINISTRATIVE = "pocketmine.broadcast.admin";
|
|
public const BROADCAST_CHANNEL_USERS = "pocketmine.broadcast.user";
|
|
|
|
public const DEFAULT_SERVER_NAME = VersionInfo::NAME . " Server";
|
|
public const DEFAULT_MAX_PLAYERS = 20;
|
|
public const DEFAULT_PORT_IPV4 = 19132;
|
|
public const DEFAULT_PORT_IPV6 = 19133;
|
|
public const DEFAULT_MAX_VIEW_DISTANCE = 16;
|
|
|
|
private static ?Server $instance = null;
|
|
|
|
private SleeperHandler $tickSleeper;
|
|
|
|
private BanList $banByName;
|
|
|
|
private BanList $banByIP;
|
|
|
|
private Config $operators;
|
|
|
|
private Config $whitelist;
|
|
|
|
private bool $isRunning = true;
|
|
|
|
private bool $hasStopped = false;
|
|
|
|
private PluginManager $pluginManager;
|
|
|
|
private float $profilingTickRate = 20;
|
|
|
|
private UpdateChecker $updater;
|
|
|
|
private AsyncPool $asyncPool;
|
|
|
|
/** Counts the ticks since the server start */
|
|
private int $tickCounter = 0;
|
|
private float $nextTick = 0;
|
|
/** @var float[] */
|
|
private array $tickAverage = [20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20];
|
|
/** @var float[] */
|
|
private array $useAverage = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
|
private float $currentTPS = 20;
|
|
private float $currentUse = 0;
|
|
private float $startTime;
|
|
|
|
private bool $doTitleTick = true;
|
|
|
|
private int $sendUsageTicker = 0;
|
|
|
|
private \AttachableThreadedLogger $logger;
|
|
|
|
private MemoryManager $memoryManager;
|
|
|
|
private ConsoleReaderThread $console;
|
|
|
|
private SimpleCommandMap $commandMap;
|
|
|
|
private CraftingManager $craftingManager;
|
|
|
|
private ResourcePackManager $resourceManager;
|
|
|
|
private WorldManager $worldManager;
|
|
|
|
private int $maxPlayers;
|
|
|
|
private bool $onlineMode = true;
|
|
|
|
private Network $network;
|
|
private bool $networkCompressionAsync = true;
|
|
|
|
private Language $language;
|
|
private bool $forceLanguage = false;
|
|
|
|
private UuidInterface $serverID;
|
|
|
|
private \DynamicClassLoader $autoloader;
|
|
private string $dataPath;
|
|
private string $pluginPath;
|
|
|
|
/**
|
|
* @var string[]
|
|
* @phpstan-var array<string, string>
|
|
*/
|
|
private array $uniquePlayers = [];
|
|
|
|
private QueryInfo $queryInfo;
|
|
|
|
private ServerConfigGroup $configGroup;
|
|
|
|
/** @var Player[] */
|
|
private array $playerList = [];
|
|
|
|
private SignalHandler $signalHandler;
|
|
|
|
/**
|
|
* @var CommandSender[][]
|
|
* @phpstan-var array<string, array<int, CommandSender>>
|
|
*/
|
|
private array $broadcastSubscribers = [];
|
|
|
|
public function getName() : string{
|
|
return VersionInfo::NAME;
|
|
}
|
|
|
|
public function isRunning() : bool{
|
|
return $this->isRunning;
|
|
}
|
|
|
|
public function getPocketMineVersion() : string{
|
|
return VersionInfo::VERSION()->getFullVersion(true);
|
|
}
|
|
|
|
public function getVersion() : string{
|
|
return ProtocolInfo::MINECRAFT_VERSION;
|
|
}
|
|
|
|
public function getApiVersion() : string{
|
|
return VersionInfo::BASE_VERSION;
|
|
}
|
|
|
|
public function getFilePath() : string{
|
|
return \pocketmine\PATH;
|
|
}
|
|
|
|
public function getResourcePath() : string{
|
|
return \pocketmine\RESOURCE_PATH;
|
|
}
|
|
|
|
public function getDataPath() : string{
|
|
return $this->dataPath;
|
|
}
|
|
|
|
public function getPluginPath() : string{
|
|
return $this->pluginPath;
|
|
}
|
|
|
|
public function getMaxPlayers() : int{
|
|
return $this->maxPlayers;
|
|
}
|
|
|
|
/**
|
|
* Returns whether the server requires that players be authenticated to Xbox Live. If true, connecting players who
|
|
* are not logged into Xbox Live will be disconnected.
|
|
*/
|
|
public function getOnlineMode() : bool{
|
|
return $this->onlineMode;
|
|
}
|
|
|
|
/**
|
|
* Alias of {@link #getOnlineMode()}.
|
|
*/
|
|
public function requiresAuthentication() : bool{
|
|
return $this->getOnlineMode();
|
|
}
|
|
|
|
public function getPort() : int{
|
|
return $this->configGroup->getConfigInt("server-port", self::DEFAULT_PORT_IPV4);
|
|
}
|
|
|
|
public function getPortV6() : int{
|
|
return $this->configGroup->getConfigInt("server-portv6", self::DEFAULT_PORT_IPV6);
|
|
}
|
|
|
|
public function getViewDistance() : int{
|
|
return max(2, $this->configGroup->getConfigInt("view-distance", self::DEFAULT_MAX_VIEW_DISTANCE));
|
|
}
|
|
|
|
/**
|
|
* Returns a view distance up to the currently-allowed limit.
|
|
*/
|
|
public function getAllowedViewDistance(int $distance) : int{
|
|
return max(2, min($distance, $this->memoryManager->getViewDistance($this->getViewDistance())));
|
|
}
|
|
|
|
public function getIp() : string{
|
|
$str = $this->configGroup->getConfigString("server-ip");
|
|
return $str !== "" ? $str : "0.0.0.0";
|
|
}
|
|
|
|
public function getIpV6() : string{
|
|
$str = $this->configGroup->getConfigString("server-ipv6");
|
|
return $str !== "" ? $str : "::";
|
|
}
|
|
|
|
public function getServerUniqueId() : UuidInterface{
|
|
return $this->serverID;
|
|
}
|
|
|
|
public function getGamemode() : GameMode{
|
|
return GameMode::fromString($this->configGroup->getConfigString("gamemode", GameMode::SURVIVAL()->name())) ?? GameMode::SURVIVAL();
|
|
}
|
|
|
|
public function getForceGamemode() : bool{
|
|
return $this->configGroup->getConfigBool("force-gamemode", false);
|
|
}
|
|
|
|
/**
|
|
* Returns Server global difficulty. Note that this may be overridden in individual worlds.
|
|
*/
|
|
public function getDifficulty() : int{
|
|
return $this->configGroup->getConfigInt("difficulty", World::DIFFICULTY_NORMAL);
|
|
}
|
|
|
|
public function hasWhitelist() : bool{
|
|
return $this->configGroup->getConfigBool("white-list", false);
|
|
}
|
|
|
|
public function isHardcore() : bool{
|
|
return $this->configGroup->getConfigBool("hardcore", false);
|
|
}
|
|
|
|
public function getMotd() : string{
|
|
return $this->configGroup->getConfigString("motd", self::DEFAULT_SERVER_NAME);
|
|
}
|
|
|
|
public function getLoader() : \DynamicClassLoader{
|
|
return $this->autoloader;
|
|
}
|
|
|
|
public function getLogger() : \AttachableThreadedLogger{
|
|
return $this->logger;
|
|
}
|
|
|
|
public function getUpdater() : UpdateChecker{
|
|
return $this->updater;
|
|
}
|
|
|
|
public function getPluginManager() : PluginManager{
|
|
return $this->pluginManager;
|
|
}
|
|
|
|
public function getCraftingManager() : CraftingManager{
|
|
return $this->craftingManager;
|
|
}
|
|
|
|
public function getResourcePackManager() : ResourcePackManager{
|
|
return $this->resourceManager;
|
|
}
|
|
|
|
public function getWorldManager() : WorldManager{
|
|
return $this->worldManager;
|
|
}
|
|
|
|
public function getAsyncPool() : AsyncPool{
|
|
return $this->asyncPool;
|
|
}
|
|
|
|
public function getTick() : int{
|
|
return $this->tickCounter;
|
|
}
|
|
|
|
/**
|
|
* Returns the last server TPS measure
|
|
*/
|
|
public function getTicksPerSecond() : float{
|
|
return round($this->currentTPS, 2);
|
|
}
|
|
|
|
/**
|
|
* Returns the last server TPS average measure
|
|
*/
|
|
public function getTicksPerSecondAverage() : float{
|
|
return round(array_sum($this->tickAverage) / count($this->tickAverage), 2);
|
|
}
|
|
|
|
/**
|
|
* Returns the TPS usage/load in %
|
|
*/
|
|
public function getTickUsage() : float{
|
|
return round($this->currentUse * 100, 2);
|
|
}
|
|
|
|
/**
|
|
* Returns the TPS usage/load average in %
|
|
*/
|
|
public function getTickUsageAverage() : float{
|
|
return round((array_sum($this->useAverage) / count($this->useAverage)) * 100, 2);
|
|
}
|
|
|
|
public function getStartTime() : float{
|
|
return $this->startTime;
|
|
}
|
|
|
|
public function getCommandMap() : SimpleCommandMap{
|
|
return $this->commandMap;
|
|
}
|
|
|
|
/**
|
|
* @return Player[]
|
|
*/
|
|
public function getOnlinePlayers() : array{
|
|
return $this->playerList;
|
|
}
|
|
|
|
public function shouldSavePlayerData() : bool{
|
|
return $this->configGroup->getPropertyBool("player.save-player-data", true);
|
|
}
|
|
|
|
/**
|
|
* @return OfflinePlayer|Player
|
|
*/
|
|
public function getOfflinePlayer(string $name){
|
|
$name = strtolower($name);
|
|
$result = $this->getPlayerExact($name);
|
|
|
|
if($result === null){
|
|
$result = new OfflinePlayer($name, $this->getOfflinePlayerData($name));
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
private function getPlayerDataPath(string $username) : string{
|
|
return Path::join($this->getDataPath(), 'players', strtolower($username) . '.dat');
|
|
}
|
|
|
|
/**
|
|
* Returns whether the server has stored any saved data for this player.
|
|
*/
|
|
public function hasOfflinePlayerData(string $name) : bool{
|
|
return file_exists($this->getPlayerDataPath($name));
|
|
}
|
|
|
|
private function handleCorruptedPlayerData(string $name) : void{
|
|
$path = $this->getPlayerDataPath($name);
|
|
rename($path, $path . '.bak');
|
|
$this->logger->error($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_data_playerCorrupted($name)));
|
|
}
|
|
|
|
public function getOfflinePlayerData(string $name) : ?CompoundTag{
|
|
return Timings::$syncPlayerDataLoad->time(function() use ($name) : ?CompoundTag{
|
|
$name = strtolower($name);
|
|
$path = $this->getPlayerDataPath($name);
|
|
|
|
if(file_exists($path)){
|
|
$contents = @file_get_contents($path);
|
|
if($contents === false){
|
|
throw new \RuntimeException("Failed to read player data file \"$path\" (permission denied?)");
|
|
}
|
|
$decompressed = @zlib_decode($contents);
|
|
if($decompressed === false){
|
|
$this->logger->debug("Failed to decompress raw player data for \"$name\"");
|
|
$this->handleCorruptedPlayerData($name);
|
|
return null;
|
|
}
|
|
|
|
try{
|
|
return (new BigEndianNbtSerializer())->read($decompressed)->mustGetCompoundTag();
|
|
}catch(NbtDataException $e){ //corrupt data
|
|
$this->logger->debug("Failed to decode NBT data for \"$name\": " . $e->getMessage());
|
|
$this->handleCorruptedPlayerData($name);
|
|
return null;
|
|
}
|
|
}
|
|
return null;
|
|
});
|
|
}
|
|
|
|
public function saveOfflinePlayerData(string $name, CompoundTag $nbtTag) : void{
|
|
$ev = new PlayerDataSaveEvent($nbtTag, $name, $this->getPlayerExact($name));
|
|
if(!$this->shouldSavePlayerData()){
|
|
$ev->cancel();
|
|
}
|
|
|
|
$ev->call();
|
|
|
|
if(!$ev->isCancelled()){
|
|
Timings::$syncPlayerDataSave->time(function() use ($name, $ev) : void{
|
|
$nbt = new BigEndianNbtSerializer();
|
|
$contents = Utils::assumeNotFalse(zlib_encode($nbt->write(new TreeRoot($ev->getSaveData())), ZLIB_ENCODING_GZIP), "zlib_encode() failed unexpectedly");
|
|
try{
|
|
Filesystem::safeFilePutContents($this->getPlayerDataPath($name), $contents);
|
|
}catch(\RuntimeException $e){
|
|
$this->logger->critical($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_data_saveError($name, $e->getMessage())));
|
|
$this->logger->logException($e);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @phpstan-return Promise<Player>
|
|
*/
|
|
public function createPlayer(NetworkSession $session, PlayerInfo $playerInfo, bool $authenticated, ?CompoundTag $offlinePlayerData) : Promise{
|
|
$ev = new PlayerCreationEvent($session);
|
|
$ev->call();
|
|
$class = $ev->getPlayerClass();
|
|
|
|
if($offlinePlayerData !== null && ($world = $this->worldManager->getWorldByName($offlinePlayerData->getString("Level", ""))) !== null){
|
|
$playerPos = EntityDataHelper::parseLocation($offlinePlayerData, $world);
|
|
$spawn = $playerPos->asVector3();
|
|
}else{
|
|
$world = $this->worldManager->getDefaultWorld();
|
|
if($world === null){
|
|
throw new AssumptionFailedError("Default world should always be loaded");
|
|
}
|
|
$playerPos = null;
|
|
$spawn = $world->getSpawnLocation();
|
|
}
|
|
$playerPromiseResolver = new PromiseResolver();
|
|
$world->requestChunkPopulation($spawn->getFloorX() >> Chunk::COORD_BIT_SIZE, $spawn->getFloorZ() >> Chunk::COORD_BIT_SIZE, null)->onCompletion(
|
|
function() use ($playerPromiseResolver, $class, $session, $playerInfo, $authenticated, $world, $playerPos, $spawn, $offlinePlayerData) : void{
|
|
if(!$session->isConnected()){
|
|
$playerPromiseResolver->reject();
|
|
return;
|
|
}
|
|
|
|
/* Stick with the original spawn at the time of generation request, even if it changed since then.
|
|
* This is because we know for sure that that chunk will be generated, but the one at the new location
|
|
* might not be, and it would be much more complex to go back and redo the whole thing.
|
|
*
|
|
* TODO: this relies on the assumption that getSafeSpawn() will only alter the Y coordinate of the
|
|
* provided position. If this assumption is broken, we'll start seeing crashes in here.
|
|
*/
|
|
|
|
/**
|
|
* @see Player::__construct()
|
|
* @var Player $player
|
|
*/
|
|
$player = new $class($this, $session, $playerInfo, $authenticated, $playerPos ?? Location::fromObject($world->getSafeSpawn($spawn), $world), $offlinePlayerData);
|
|
if(!$player->hasPlayedBefore()){
|
|
$player->onGround = true; //TODO: this hack is needed for new players in-air ticks - they don't get detected as on-ground until they move
|
|
}
|
|
$playerPromiseResolver->resolve($player);
|
|
},
|
|
static function() use ($playerPromiseResolver, $session) : void{
|
|
if($session->isConnected()){
|
|
$session->disconnect("Spawn terrain generation failed");
|
|
}
|
|
$playerPromiseResolver->reject();
|
|
}
|
|
);
|
|
return $playerPromiseResolver->getPromise();
|
|
}
|
|
|
|
/**
|
|
* Returns an online player whose name begins with or equals the given string (case insensitive).
|
|
* The closest match will be returned, or null if there are no online matches.
|
|
*
|
|
* @see Server::getPlayerExact()
|
|
*/
|
|
public function getPlayerByPrefix(string $name) : ?Player{
|
|
$found = null;
|
|
$name = strtolower($name);
|
|
$delta = PHP_INT_MAX;
|
|
foreach($this->getOnlinePlayers() as $player){
|
|
if(stripos($player->getName(), $name) === 0){
|
|
$curDelta = strlen($player->getName()) - strlen($name);
|
|
if($curDelta < $delta){
|
|
$found = $player;
|
|
$delta = $curDelta;
|
|
}
|
|
if($curDelta === 0){
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $found;
|
|
}
|
|
|
|
/**
|
|
* Returns an online player with the given name (case insensitive), or null if not found.
|
|
*/
|
|
public function getPlayerExact(string $name) : ?Player{
|
|
$name = strtolower($name);
|
|
foreach($this->getOnlinePlayers() as $player){
|
|
if(strtolower($player->getName()) === $name){
|
|
return $player;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns the player online with the specified raw UUID, or null if not found
|
|
*/
|
|
public function getPlayerByRawUUID(string $rawUUID) : ?Player{
|
|
return $this->playerList[$rawUUID] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Returns the player online with a UUID equivalent to the specified UuidInterface object, or null if not found
|
|
*/
|
|
public function getPlayerByUUID(UuidInterface $uuid) : ?Player{
|
|
return $this->getPlayerByRawUUID($uuid->getBytes());
|
|
}
|
|
|
|
public function getConfigGroup() : ServerConfigGroup{
|
|
return $this->configGroup;
|
|
}
|
|
|
|
/**
|
|
* @return Command|PluginOwned|null
|
|
* @phpstan-return (Command&PluginOwned)|null
|
|
*/
|
|
public function getPluginCommand(string $name){
|
|
if(($command = $this->commandMap->getCommand($name)) instanceof PluginOwned){
|
|
return $command;
|
|
}else{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public function getNameBans() : BanList{
|
|
return $this->banByName;
|
|
}
|
|
|
|
public function getIPBans() : BanList{
|
|
return $this->banByIP;
|
|
}
|
|
|
|
public function addOp(string $name) : void{
|
|
$this->operators->set(strtolower($name), true);
|
|
|
|
if(($player = $this->getPlayerExact($name)) !== null){
|
|
$player->setBasePermission(DefaultPermissions::ROOT_OPERATOR, true);
|
|
}
|
|
$this->operators->save();
|
|
}
|
|
|
|
public function removeOp(string $name) : void{
|
|
$lowercaseName = strtolower($name);
|
|
foreach($this->operators->getAll() as $operatorName => $_){
|
|
$operatorName = (string) $operatorName;
|
|
if($lowercaseName === strtolower($operatorName)){
|
|
$this->operators->remove($operatorName);
|
|
}
|
|
}
|
|
|
|
if(($player = $this->getPlayerExact($name)) !== null){
|
|
$player->unsetBasePermission(DefaultPermissions::ROOT_OPERATOR);
|
|
}
|
|
$this->operators->save();
|
|
}
|
|
|
|
public function addWhitelist(string $name) : void{
|
|
$this->whitelist->set(strtolower($name), true);
|
|
$this->whitelist->save();
|
|
}
|
|
|
|
public function removeWhitelist(string $name) : void{
|
|
$this->whitelist->remove(strtolower($name));
|
|
$this->whitelist->save();
|
|
}
|
|
|
|
public function isWhitelisted(string $name) : bool{
|
|
return !$this->hasWhitelist() || $this->operators->exists($name, true) || $this->whitelist->exists($name, true);
|
|
}
|
|
|
|
public function isOp(string $name) : bool{
|
|
return $this->operators->exists($name, true);
|
|
}
|
|
|
|
public function getWhitelisted() : Config{
|
|
return $this->whitelist;
|
|
}
|
|
|
|
public function getOps() : Config{
|
|
return $this->operators;
|
|
}
|
|
|
|
/**
|
|
* @return string[][]
|
|
*/
|
|
public function getCommandAliases() : array{
|
|
$section = $this->configGroup->getProperty("aliases");
|
|
$result = [];
|
|
if(is_array($section)){
|
|
foreach($section as $key => $value){
|
|
$commands = [];
|
|
if(is_array($value)){
|
|
$commands = $value;
|
|
}else{
|
|
$commands[] = (string) $value;
|
|
}
|
|
|
|
$result[$key] = $commands;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
public static function getInstance() : Server{
|
|
if(self::$instance === null){
|
|
throw new \RuntimeException("Attempt to retrieve Server instance outside server thread");
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
public function __construct(\DynamicClassLoader $autoloader, \AttachableThreadedLogger $logger, string $dataPath, string $pluginPath){
|
|
if(self::$instance !== null){
|
|
throw new \LogicException("Only one server instance can exist at once");
|
|
}
|
|
self::$instance = $this;
|
|
$this->startTime = microtime(true);
|
|
|
|
$this->tickSleeper = new SleeperHandler();
|
|
$this->autoloader = $autoloader;
|
|
$this->logger = $logger;
|
|
|
|
$this->signalHandler = new SignalHandler(function() : void{
|
|
$this->logger->info("Received signal interrupt, stopping the server");
|
|
$this->shutdown();
|
|
});
|
|
|
|
try{
|
|
foreach([
|
|
$dataPath,
|
|
$pluginPath,
|
|
Path::join($dataPath, "worlds"),
|
|
Path::join($dataPath, "players")
|
|
] as $neededPath){
|
|
if(!file_exists($neededPath)){
|
|
mkdir($neededPath, 0777);
|
|
}
|
|
}
|
|
|
|
$this->dataPath = realpath($dataPath) . DIRECTORY_SEPARATOR;
|
|
$this->pluginPath = realpath($pluginPath) . DIRECTORY_SEPARATOR;
|
|
|
|
$this->logger->info("Loading server configuration");
|
|
$pocketmineYmlPath = Path::join($this->dataPath, "pocketmine.yml");
|
|
if(!file_exists($pocketmineYmlPath)){
|
|
$content = Utils::assumeNotFalse(file_get_contents(Path::join(\pocketmine\RESOURCE_PATH, "pocketmine.yml")), "Missing required resource file");
|
|
if(VersionInfo::IS_DEVELOPMENT_BUILD){
|
|
$content = str_replace("preferred-channel: stable", "preferred-channel: beta", $content);
|
|
}
|
|
@file_put_contents($pocketmineYmlPath, $content);
|
|
}
|
|
|
|
$this->configGroup = new ServerConfigGroup(
|
|
new Config($pocketmineYmlPath, Config::YAML, []),
|
|
new Config(Path::join($this->dataPath, "server.properties"), Config::PROPERTIES, [
|
|
"motd" => self::DEFAULT_SERVER_NAME,
|
|
"server-port" => self::DEFAULT_PORT_IPV4,
|
|
"server-portv6" => self::DEFAULT_PORT_IPV6,
|
|
"enable-ipv6" => true,
|
|
"white-list" => false,
|
|
"max-players" => self::DEFAULT_MAX_PLAYERS,
|
|
"gamemode" => GameMode::SURVIVAL()->name(),
|
|
"force-gamemode" => false,
|
|
"hardcore" => false,
|
|
"pvp" => true,
|
|
"difficulty" => World::DIFFICULTY_NORMAL,
|
|
"generator-settings" => "",
|
|
"level-name" => "world",
|
|
"level-seed" => "",
|
|
"level-type" => "DEFAULT",
|
|
"enable-query" => true,
|
|
"auto-save" => true,
|
|
"view-distance" => self::DEFAULT_MAX_VIEW_DISTANCE,
|
|
"xbox-auth" => true,
|
|
"language" => "eng"
|
|
])
|
|
);
|
|
|
|
$debugLogLevel = $this->configGroup->getPropertyInt("debug.level", 1);
|
|
if($this->logger instanceof MainLogger){
|
|
$this->logger->setLogDebug($debugLogLevel > 1);
|
|
}
|
|
|
|
$this->forceLanguage = $this->configGroup->getPropertyBool("settings.force-language", false);
|
|
$selectedLang = $this->configGroup->getConfigString("language", $this->configGroup->getPropertyString("settings.language", Language::FALLBACK_LANGUAGE));
|
|
try{
|
|
$this->language = new Language($selectedLang);
|
|
}catch(LanguageNotFoundException $e){
|
|
$this->logger->error($e->getMessage());
|
|
try{
|
|
$this->language = new Language(Language::FALLBACK_LANGUAGE);
|
|
}catch(LanguageNotFoundException $e){
|
|
$this->logger->emergency("Fallback language \"" . Language::FALLBACK_LANGUAGE . "\" not found");
|
|
return;
|
|
}
|
|
}
|
|
|
|
$this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::language_selected($this->getLanguage()->getName(), $this->getLanguage()->getLang())));
|
|
|
|
if(VersionInfo::IS_DEVELOPMENT_BUILD){
|
|
if(!$this->configGroup->getPropertyBool("settings.enable-dev-builds", false)){
|
|
$this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error1(VersionInfo::NAME)));
|
|
$this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error2()));
|
|
$this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error3()));
|
|
$this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error4("settings.enable-dev-builds")));
|
|
$this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error5("https://github.com/pmmp/PocketMine-MP/releases")));
|
|
$this->forceShutdown();
|
|
|
|
return;
|
|
}
|
|
|
|
$this->logger->warning(str_repeat("-", 40));
|
|
$this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_warning1(VersionInfo::NAME)));
|
|
$this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_warning2()));
|
|
$this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_warning3()));
|
|
$this->logger->warning(str_repeat("-", 40));
|
|
}
|
|
|
|
$this->memoryManager = new MemoryManager($this);
|
|
|
|
$this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_start(TextFormat::AQUA . $this->getVersion() . TextFormat::RESET)));
|
|
|
|
if(($poolSize = $this->configGroup->getPropertyString("settings.async-workers", "auto")) === "auto"){
|
|
$poolSize = 2;
|
|
$processors = Utils::getCoreCount() - 2;
|
|
|
|
if($processors > 0){
|
|
$poolSize = max(1, $processors);
|
|
}
|
|
}else{
|
|
$poolSize = max(1, (int) $poolSize);
|
|
}
|
|
|
|
$this->asyncPool = new AsyncPool($poolSize, max(-1, $this->configGroup->getPropertyInt("memory.async-worker-hard-limit", 256)), $this->autoloader, $this->logger, $this->tickSleeper);
|
|
|
|
$netCompressionThreshold = -1;
|
|
if($this->configGroup->getPropertyInt("network.batch-threshold", 256) >= 0){
|
|
$netCompressionThreshold = $this->configGroup->getPropertyInt("network.batch-threshold", 256);
|
|
}
|
|
|
|
$netCompressionLevel = $this->configGroup->getPropertyInt("network.compression-level", 6);
|
|
if($netCompressionLevel < 1 || $netCompressionLevel > 9){
|
|
$this->logger->warning("Invalid network compression level $netCompressionLevel set, setting to default 6");
|
|
$netCompressionLevel = 6;
|
|
}
|
|
ZlibCompressor::setInstance(new ZlibCompressor($netCompressionLevel, $netCompressionThreshold, ZlibCompressor::DEFAULT_MAX_DECOMPRESSION_SIZE));
|
|
|
|
$this->networkCompressionAsync = $this->configGroup->getPropertyBool("network.async-compression", true);
|
|
|
|
EncryptionContext::$ENABLED = $this->configGroup->getPropertyBool("network.enable-encryption", true);
|
|
|
|
$this->doTitleTick = $this->configGroup->getPropertyBool("console.title-tick", true) && Terminal::hasFormattingCodes();
|
|
|
|
$this->operators = new Config(Path::join($this->dataPath, "ops.txt"), Config::ENUM);
|
|
$this->whitelist = new Config(Path::join($this->dataPath, "white-list.txt"), Config::ENUM);
|
|
|
|
$bannedTxt = Path::join($this->dataPath, "banned.txt");
|
|
$bannedPlayersTxt = Path::join($this->dataPath, "banned-players.txt");
|
|
if(file_exists($bannedTxt) && !file_exists($bannedPlayersTxt)){
|
|
@rename($bannedTxt, $bannedPlayersTxt);
|
|
}
|
|
@touch($bannedPlayersTxt);
|
|
$this->banByName = new BanList($bannedPlayersTxt);
|
|
$this->banByName->load();
|
|
$bannedIpsTxt = Path::join($this->dataPath, "banned-ips.txt");
|
|
@touch($bannedIpsTxt);
|
|
$this->banByIP = new BanList($bannedIpsTxt);
|
|
$this->banByIP->load();
|
|
|
|
$this->maxPlayers = $this->configGroup->getConfigInt("max-players", self::DEFAULT_MAX_PLAYERS);
|
|
|
|
$this->onlineMode = $this->configGroup->getConfigBool("xbox-auth", true);
|
|
if($this->onlineMode){
|
|
$this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_auth_enabled()));
|
|
}else{
|
|
$this->logger->warning($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_auth_disabled()));
|
|
$this->logger->warning($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_authWarning()));
|
|
$this->logger->warning($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_authProperty_disabled()));
|
|
}
|
|
|
|
if($this->configGroup->getConfigBool("hardcore", false) && $this->getDifficulty() < World::DIFFICULTY_HARD){
|
|
$this->configGroup->setConfigInt("difficulty", World::DIFFICULTY_HARD);
|
|
}
|
|
|
|
@cli_set_process_title($this->getName() . " " . $this->getPocketMineVersion());
|
|
|
|
$this->serverID = Utils::getMachineUniqueId($this->getIp() . $this->getPort());
|
|
|
|
$this->getLogger()->debug("Server unique id: " . $this->getServerUniqueId());
|
|
$this->getLogger()->debug("Machine unique id: " . Utils::getMachineUniqueId());
|
|
|
|
$this->network = new Network($this->logger);
|
|
$this->network->setName($this->getMotd());
|
|
|
|
$this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_info(
|
|
$this->getName(),
|
|
(VersionInfo::IS_DEVELOPMENT_BUILD ? TextFormat::YELLOW : "") . $this->getPocketMineVersion() . TextFormat::RESET
|
|
)));
|
|
$this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_license($this->getName())));
|
|
|
|
Timings::init();
|
|
TimingsHandler::setEnabled($this->configGroup->getPropertyBool("settings.enable-profiling", false));
|
|
$this->profilingTickRate = $this->configGroup->getPropertyInt("settings.profile-report-trigger", 20);
|
|
|
|
DefaultPermissions::registerCorePermissions();
|
|
|
|
$this->commandMap = new SimpleCommandMap($this);
|
|
|
|
$this->craftingManager = CraftingManagerFromDataHelper::make(Path::join(\pocketmine\BEDROCK_DATA_PATH, "recipes.json"));
|
|
|
|
$this->resourceManager = new ResourcePackManager(Path::join($this->getDataPath(), "resource_packs"), $this->logger);
|
|
|
|
$pluginGraylist = null;
|
|
$graylistFile = Path::join($this->dataPath, "plugin_list.yml");
|
|
if(!file_exists($graylistFile)){
|
|
copy(Path::join(\pocketmine\RESOURCE_PATH, 'plugin_list.yml'), $graylistFile);
|
|
}
|
|
try{
|
|
$pluginGraylist = PluginGraylist::fromArray(yaml_parse(file_get_contents($graylistFile)));
|
|
}catch(\InvalidArgumentException $e){
|
|
$this->logger->emergency("Failed to load $graylistFile: " . $e->getMessage());
|
|
$this->forceShutdown();
|
|
return;
|
|
}
|
|
$this->pluginManager = new PluginManager($this, $this->configGroup->getPropertyBool("plugins.legacy-data-dir", true) ? null : Path::join($this->getDataPath(), "plugin_data"), $pluginGraylist);
|
|
$this->pluginManager->registerInterface(new PharPluginLoader($this->autoloader));
|
|
$this->pluginManager->registerInterface(new ScriptPluginLoader());
|
|
|
|
$providerManager = new WorldProviderManager();
|
|
if(
|
|
($format = $providerManager->getProviderByName($formatName = $this->configGroup->getPropertyString("level-settings.default-format", ""))) !== null &&
|
|
$format instanceof WritableWorldProviderManagerEntry
|
|
){
|
|
$providerManager->setDefault($format);
|
|
}elseif($formatName !== ""){
|
|
$this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_level_badDefaultFormat($formatName)));
|
|
}
|
|
|
|
$this->worldManager = new WorldManager($this, Path::join($this->dataPath, "worlds"), $providerManager);
|
|
$this->worldManager->setAutoSave($this->configGroup->getConfigBool("auto-save", $this->worldManager->getAutoSave()));
|
|
$this->worldManager->setAutoSaveInterval($this->configGroup->getPropertyInt("ticks-per.autosave", 6000));
|
|
|
|
$this->updater = new UpdateChecker($this, $this->configGroup->getPropertyString("auto-updater.host", "update.pmmp.io"));
|
|
|
|
$this->queryInfo = new QueryInfo($this);
|
|
|
|
register_shutdown_function([$this, "crashDump"]);
|
|
|
|
$this->pluginManager->loadPlugins($this->pluginPath);
|
|
$this->enablePlugins(PluginEnableOrder::STARTUP());
|
|
|
|
if(!$this->startupPrepareWorlds()){
|
|
$this->forceShutdown();
|
|
return;
|
|
}
|
|
$this->enablePlugins(PluginEnableOrder::POSTWORLD());
|
|
|
|
if(!$this->startupPrepareNetworkInterfaces()){
|
|
$this->forceShutdown();
|
|
return;
|
|
}
|
|
|
|
if($this->configGroup->getPropertyBool("anonymous-statistics.enabled", true)){
|
|
$this->sendUsageTicker = 6000;
|
|
$this->sendUsage(SendUsageTask::TYPE_OPEN);
|
|
}
|
|
|
|
$this->configGroup->save();
|
|
|
|
$this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_defaultGameMode($this->getGamemode()->getTranslatableName())));
|
|
$this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_donate(TextFormat::AQUA . "https://patreon.com/pocketminemp" . TextFormat::RESET)));
|
|
$this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_startFinished(strval(round(microtime(true) - $this->startTime, 3)))));
|
|
|
|
//TODO: move console parts to a separate component
|
|
$consoleSender = new ConsoleCommandSender($this, $this->language);
|
|
$this->subscribeToBroadcastChannel(self::BROADCAST_CHANNEL_ADMINISTRATIVE, $consoleSender);
|
|
$this->subscribeToBroadcastChannel(self::BROADCAST_CHANNEL_USERS, $consoleSender);
|
|
|
|
$consoleNotifier = new SleeperNotifier();
|
|
$commandBuffer = new \Threaded();
|
|
$this->console = new ConsoleReaderThread($commandBuffer, $consoleNotifier);
|
|
$this->tickSleeper->addNotifier($consoleNotifier, function() use ($commandBuffer, $consoleSender) : void{
|
|
Timings::$serverCommand->startTiming();
|
|
while(($line = $commandBuffer->shift()) !== null){
|
|
$this->dispatchCommand($consoleSender, (string) $line);
|
|
}
|
|
Timings::$serverCommand->stopTiming();
|
|
});
|
|
$this->console->start(PTHREADS_INHERIT_NONE);
|
|
|
|
$this->tickProcessor();
|
|
$this->forceShutdown();
|
|
}catch(\Throwable $e){
|
|
$this->exceptionHandler($e);
|
|
}
|
|
}
|
|
|
|
private function startupPrepareWorlds() : bool{
|
|
$getGenerator = function(string $generatorName, string $generatorOptions, string $worldName) : ?string{
|
|
$generatorEntry = GeneratorManager::getInstance()->getGenerator($generatorName);
|
|
if($generatorEntry === null){
|
|
$this->logger->error($this->language->translate(KnownTranslationFactory::pocketmine_level_generationError(
|
|
$worldName,
|
|
KnownTranslationFactory::pocketmine_level_unknownGenerator($generatorName)
|
|
)));
|
|
return null;
|
|
}
|
|
try{
|
|
$generatorEntry->validateGeneratorOptions($generatorOptions);
|
|
}catch(InvalidGeneratorOptionsException $e){
|
|
$this->logger->error($this->language->translate(KnownTranslationFactory::pocketmine_level_generationError(
|
|
$worldName,
|
|
KnownTranslationFactory::pocketmine_level_invalidGeneratorOptions($generatorOptions, $generatorName, $e->getMessage())
|
|
)));
|
|
return null;
|
|
}
|
|
return $generatorEntry->getGeneratorClass();
|
|
};
|
|
|
|
$anyWorldFailedToLoad = false;
|
|
|
|
foreach((array) $this->configGroup->getProperty("worlds", []) as $name => $options){
|
|
if($options === null){
|
|
$options = [];
|
|
}elseif(!is_array($options)){
|
|
//TODO: this probably should be an error
|
|
continue;
|
|
}
|
|
if(!$this->worldManager->loadWorld($name, true)){
|
|
if($this->worldManager->isWorldGenerated($name)){
|
|
//allow checking if other worlds are loadable, so the user gets all the errors in one go
|
|
$anyWorldFailedToLoad = true;
|
|
continue;
|
|
}
|
|
$creationOptions = WorldCreationOptions::create();
|
|
//TODO: error checking
|
|
|
|
$generatorName = $options["generator"] ?? "default";
|
|
$generatorOptions = isset($options["preset"]) && is_string($options["preset"]) ? $options["preset"] : "";
|
|
|
|
$generatorClass = $getGenerator($generatorName, $generatorOptions, $name);
|
|
if($generatorClass === null){
|
|
$anyWorldFailedToLoad = true;
|
|
continue;
|
|
}
|
|
$creationOptions->setGeneratorClass($generatorClass);
|
|
$creationOptions->setGeneratorOptions($generatorOptions);
|
|
|
|
if(isset($options["difficulty"]) && is_string($options["difficulty"])){
|
|
$creationOptions->setDifficulty(World::getDifficultyFromString($options["difficulty"]));
|
|
}
|
|
|
|
if(isset($options["seed"])){
|
|
$convertedSeed = Generator::convertSeed((string) ($options["seed"] ?? ""));
|
|
if($convertedSeed !== null){
|
|
$creationOptions->setSeed($convertedSeed);
|
|
}
|
|
}
|
|
|
|
$this->worldManager->generateWorld($name, $creationOptions);
|
|
}
|
|
}
|
|
|
|
if($this->worldManager->getDefaultWorld() === null){
|
|
$default = $this->configGroup->getConfigString("level-name", "world");
|
|
if(trim($default) == ""){
|
|
$this->getLogger()->warning("level-name cannot be null, using default");
|
|
$default = "world";
|
|
$this->configGroup->setConfigString("level-name", "world");
|
|
}
|
|
if(!$this->worldManager->loadWorld($default, true)){
|
|
if($this->worldManager->isWorldGenerated($default)){
|
|
$this->getLogger()->emergency($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_defaultError()));
|
|
|
|
return false;
|
|
}
|
|
$generatorName = $this->configGroup->getConfigString("level-type");
|
|
$generatorOptions = $this->configGroup->getConfigString("generator-settings");
|
|
$generatorClass = $getGenerator($generatorName, $generatorOptions, $default);
|
|
|
|
if($generatorClass === null){
|
|
$this->getLogger()->emergency($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_defaultError()));
|
|
return false;
|
|
}
|
|
$creationOptions = WorldCreationOptions::create()
|
|
->setGeneratorClass($generatorClass)
|
|
->setGeneratorOptions($generatorOptions);
|
|
$convertedSeed = Generator::convertSeed($this->configGroup->getConfigString("level-seed"));
|
|
if($convertedSeed !== null){
|
|
$creationOptions->setSeed($convertedSeed);
|
|
}
|
|
$this->worldManager->generateWorld($default, $creationOptions);
|
|
}
|
|
|
|
$world = $this->worldManager->getWorldByName($default);
|
|
if($world === null){
|
|
throw new AssumptionFailedError("We just loaded/generated the default world, so it must exist");
|
|
}
|
|
$this->worldManager->setDefaultWorld($world);
|
|
}
|
|
|
|
return !$anyWorldFailedToLoad;
|
|
}
|
|
|
|
private function startupPrepareConnectableNetworkInterfaces(string $ip, int $port, bool $ipV6, bool $useQuery) : bool{
|
|
$prettyIp = $ipV6 ? "[$ip]" : $ip;
|
|
try{
|
|
$rakLibRegistered = $this->network->registerInterface(new RakLibInterface($this, $ip, $port, $ipV6));
|
|
}catch(NetworkInterfaceStartException $e){
|
|
$this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_networkStartFailed(
|
|
$ip,
|
|
(string) $port,
|
|
$e->getMessage()
|
|
)));
|
|
return false;
|
|
}
|
|
if($rakLibRegistered){
|
|
$this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_networkStart($prettyIp, (string) $port)));
|
|
}
|
|
if($useQuery){
|
|
if(!$rakLibRegistered){
|
|
//RakLib would normally handle the transport for Query packets
|
|
//if it's not registered we need to make sure Query still works
|
|
$this->network->registerInterface(new DedicatedQueryNetworkInterface($ip, $port, $ipV6, new \PrefixedLogger($this->logger, "Dedicated Query Interface")));
|
|
}
|
|
$this->logger->info($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_query_running($prettyIp, (string) $port)));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private function startupPrepareNetworkInterfaces() : bool{
|
|
$useQuery = $this->configGroup->getConfigBool("enable-query", true);
|
|
|
|
if(
|
|
!$this->startupPrepareConnectableNetworkInterfaces($this->getIp(), $this->getPort(), false, $useQuery) ||
|
|
(
|
|
$this->configGroup->getConfigBool("enable-ipv6", true) &&
|
|
!$this->startupPrepareConnectableNetworkInterfaces($this->getIpV6(), $this->getPortV6(), true, $useQuery)
|
|
)
|
|
){
|
|
return false;
|
|
}
|
|
|
|
if($useQuery){
|
|
$this->network->registerRawPacketHandler(new QueryHandler($this));
|
|
}
|
|
|
|
foreach($this->getIPBans()->getEntries() as $entry){
|
|
$this->network->blockAddress($entry->getName(), -1);
|
|
}
|
|
|
|
if($this->configGroup->getPropertyBool("network.upnp-forwarding", false)){
|
|
$this->network->registerInterface(new UPnPNetworkInterface($this->logger, Internet::getInternalIP(), $this->getPort()));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Subscribes to a particular message broadcast channel.
|
|
* The channel ID can be any arbitrary string.
|
|
*/
|
|
public function subscribeToBroadcastChannel(string $channelId, CommandSender $subscriber) : void{
|
|
$this->broadcastSubscribers[$channelId][spl_object_id($subscriber)] = $subscriber;
|
|
}
|
|
|
|
/**
|
|
* Unsubscribes from a particular message broadcast channel.
|
|
*/
|
|
public function unsubscribeFromBroadcastChannel(string $channelId, CommandSender $subscriber) : void{
|
|
if(isset($this->broadcastSubscribers[$channelId][spl_object_id($subscriber)])){
|
|
unset($this->broadcastSubscribers[$channelId][spl_object_id($subscriber)]);
|
|
if(count($this->broadcastSubscribers[$channelId]) === 0){
|
|
unset($this->broadcastSubscribers[$channelId]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unsubscribes from all broadcast channels.
|
|
*/
|
|
public function unsubscribeFromAllBroadcastChannels(CommandSender $subscriber) : void{
|
|
foreach(Utils::stringifyKeys($this->broadcastSubscribers) as $channelId => $recipients){
|
|
$this->unsubscribeFromBroadcastChannel($channelId, $subscriber);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a list of all the CommandSenders subscribed to the given broadcast channel.
|
|
*
|
|
* @return CommandSender[]
|
|
* @phpstan-return array<int, CommandSender>
|
|
*/
|
|
public function getBroadcastChannelSubscribers(string $channelId) : array{
|
|
return $this->broadcastSubscribers[$channelId] ?? [];
|
|
}
|
|
|
|
/**
|
|
* @param CommandSender[]|null $recipients
|
|
*/
|
|
public function broadcastMessage(Translatable|string $message, ?array $recipients = null) : int{
|
|
$recipients = $recipients ?? $this->getBroadcastChannelSubscribers(self::BROADCAST_CHANNEL_USERS);
|
|
|
|
foreach($recipients as $recipient){
|
|
$recipient->sendMessage($message);
|
|
}
|
|
|
|
return count($recipients);
|
|
}
|
|
|
|
/**
|
|
* @return Player[]
|
|
*/
|
|
private function getPlayerBroadcastSubscribers(string $channelId) : array{
|
|
/** @var Player[] $players */
|
|
$players = [];
|
|
foreach($this->broadcastSubscribers[$channelId] as $subscriber){
|
|
if($subscriber instanceof Player){
|
|
$players[spl_object_id($subscriber)] = $subscriber;
|
|
}
|
|
}
|
|
return $players;
|
|
}
|
|
|
|
/**
|
|
* @param Player[]|null $recipients
|
|
*/
|
|
public function broadcastTip(string $tip, ?array $recipients = null) : int{
|
|
$recipients = $recipients ?? $this->getPlayerBroadcastSubscribers(self::BROADCAST_CHANNEL_USERS);
|
|
|
|
foreach($recipients as $recipient){
|
|
$recipient->sendTip($tip);
|
|
}
|
|
|
|
return count($recipients);
|
|
}
|
|
|
|
/**
|
|
* @param Player[]|null $recipients
|
|
*/
|
|
public function broadcastPopup(string $popup, ?array $recipients = null) : int{
|
|
$recipients = $recipients ?? $this->getPlayerBroadcastSubscribers(self::BROADCAST_CHANNEL_USERS);
|
|
|
|
foreach($recipients as $recipient){
|
|
$recipient->sendPopup($popup);
|
|
}
|
|
|
|
return count($recipients);
|
|
}
|
|
|
|
/**
|
|
* @param int $fadeIn Duration in ticks for fade-in. If -1 is given, client-sided defaults will be used.
|
|
* @param int $stay Duration in ticks to stay on screen for
|
|
* @param int $fadeOut Duration in ticks for fade-out.
|
|
* @param Player[]|null $recipients
|
|
*/
|
|
public function broadcastTitle(string $title, string $subtitle = "", int $fadeIn = -1, int $stay = -1, int $fadeOut = -1, ?array $recipients = null) : int{
|
|
$recipients = $recipients ?? $this->getPlayerBroadcastSubscribers(self::BROADCAST_CHANNEL_USERS);
|
|
|
|
foreach($recipients as $recipient){
|
|
$recipient->sendTitle($title, $subtitle, $fadeIn, $stay, $fadeOut);
|
|
}
|
|
|
|
return count($recipients);
|
|
}
|
|
|
|
/**
|
|
* @param Player[] $players
|
|
* @param ClientboundPacket[] $packets
|
|
*/
|
|
public function broadcastPackets(array $players, array $packets) : bool{
|
|
if(count($packets) === 0){
|
|
throw new \InvalidArgumentException("Cannot broadcast empty list of packets");
|
|
}
|
|
|
|
return Timings::$broadcastPackets->time(function() use ($players, $packets) : bool{
|
|
/** @var NetworkSession[] $recipients */
|
|
$recipients = [];
|
|
foreach($players as $player){
|
|
if($player->isConnected()){
|
|
$recipients[] = $player->getNetworkSession();
|
|
}
|
|
}
|
|
if(count($recipients) === 0){
|
|
return false;
|
|
}
|
|
|
|
$ev = new DataPacketSendEvent($recipients, $packets);
|
|
$ev->call();
|
|
if($ev->isCancelled()){
|
|
return false;
|
|
}
|
|
$recipients = $ev->getTargets();
|
|
|
|
/** @var PacketBroadcaster[] $broadcasters */
|
|
$broadcasters = [];
|
|
/** @var NetworkSession[][] $broadcasterTargets */
|
|
$broadcasterTargets = [];
|
|
foreach($recipients as $recipient){
|
|
$broadcaster = $recipient->getBroadcaster();
|
|
$broadcasters[spl_object_id($broadcaster)] = $broadcaster;
|
|
$broadcasterTargets[spl_object_id($broadcaster)][] = $recipient;
|
|
}
|
|
foreach($broadcasters as $broadcaster){
|
|
$broadcaster->broadcastPackets($broadcasterTargets[spl_object_id($broadcaster)], $packets);
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Broadcasts a list of packets in a batch to a list of players
|
|
*
|
|
* @param bool|null $sync Compression on the main thread (true) or workers (false). Default is automatic (null).
|
|
*/
|
|
public function prepareBatch(PacketBatch $stream, Compressor $compressor, ?bool $sync = null) : CompressBatchPromise{
|
|
try{
|
|
Timings::$playerNetworkSendCompress->startTiming();
|
|
|
|
$buffer = $stream->getBuffer();
|
|
|
|
if($sync === null){
|
|
$sync = !($this->networkCompressionAsync && $compressor->willCompress($buffer));
|
|
}
|
|
|
|
$promise = new CompressBatchPromise();
|
|
if(!$sync){
|
|
$task = new CompressBatchTask($buffer, $promise, $compressor);
|
|
$this->asyncPool->submitTask($task);
|
|
}else{
|
|
$promise->resolve($compressor->compress($buffer));
|
|
}
|
|
|
|
return $promise;
|
|
}finally{
|
|
Timings::$playerNetworkSendCompress->stopTiming();
|
|
}
|
|
}
|
|
|
|
public function enablePlugins(PluginEnableOrder $type) : void{
|
|
foreach($this->pluginManager->getPlugins() as $plugin){
|
|
if(!$plugin->isEnabled() && $plugin->getDescription()->getOrder()->equals($type)){
|
|
$this->pluginManager->enablePlugin($plugin);
|
|
}
|
|
}
|
|
|
|
if($type->equals(PluginEnableOrder::POSTWORLD())){
|
|
$this->commandMap->registerServerAliases();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Executes a command from a CommandSender
|
|
*/
|
|
public function dispatchCommand(CommandSender $sender, string $commandLine, bool $internal = false) : bool{
|
|
if(!$internal){
|
|
$ev = new CommandEvent($sender, $commandLine);
|
|
$ev->call();
|
|
if($ev->isCancelled()){
|
|
return false;
|
|
}
|
|
|
|
$commandLine = $ev->getCommand();
|
|
}
|
|
|
|
return $this->commandMap->dispatch($sender, $commandLine);
|
|
}
|
|
|
|
/**
|
|
* Shuts the server down correctly
|
|
*/
|
|
public function shutdown() : void{
|
|
if($this->isRunning){
|
|
$this->isRunning = false;
|
|
$this->signalHandler->unregister();
|
|
}
|
|
}
|
|
|
|
public function forceShutdown() : void{
|
|
if($this->hasStopped){
|
|
return;
|
|
}
|
|
|
|
if($this->doTitleTick){
|
|
echo "\x1b]0;\x07";
|
|
}
|
|
|
|
if($this->isRunning){
|
|
$this->logger->emergency("Forcing server shutdown");
|
|
}
|
|
try{
|
|
if(!$this->isRunning()){
|
|
$this->sendUsage(SendUsageTask::TYPE_CLOSE);
|
|
}
|
|
|
|
$this->hasStopped = true;
|
|
|
|
$this->shutdown();
|
|
|
|
if(isset($this->pluginManager)){
|
|
$this->getLogger()->debug("Disabling all plugins");
|
|
$this->pluginManager->disablePlugins();
|
|
}
|
|
|
|
if(isset($this->network)){
|
|
$this->network->getSessionManager()->close($this->configGroup->getPropertyString("settings.shutdown-message", "Server closed"));
|
|
}
|
|
|
|
if(isset($this->worldManager)){
|
|
$this->getLogger()->debug("Unloading all worlds");
|
|
foreach($this->worldManager->getWorlds() as $world){
|
|
$this->worldManager->unloadWorld($world, true);
|
|
}
|
|
}
|
|
|
|
$this->getLogger()->debug("Removing event handlers");
|
|
HandlerListManager::global()->unregisterAll();
|
|
|
|
if(isset($this->asyncPool)){
|
|
$this->getLogger()->debug("Shutting down async task worker pool");
|
|
$this->asyncPool->shutdown();
|
|
}
|
|
|
|
if(isset($this->configGroup)){
|
|
$this->getLogger()->debug("Saving properties");
|
|
$this->configGroup->save();
|
|
}
|
|
|
|
if(isset($this->console)){
|
|
$this->getLogger()->debug("Closing console");
|
|
$this->console->quit();
|
|
}
|
|
|
|
if(isset($this->network)){
|
|
$this->getLogger()->debug("Stopping network interfaces");
|
|
foreach($this->network->getInterfaces() as $interface){
|
|
$this->getLogger()->debug("Stopping network interface " . get_class($interface));
|
|
$this->network->unregisterInterface($interface);
|
|
}
|
|
}
|
|
}catch(\Throwable $e){
|
|
$this->logger->logException($e);
|
|
$this->logger->emergency("Crashed while crashing, killing process");
|
|
@Process::kill(Process::pid(), true);
|
|
}
|
|
|
|
}
|
|
|
|
public function getQueryInformation() : QueryInfo{
|
|
return $this->queryInfo;
|
|
}
|
|
|
|
/**
|
|
* @param mixed[][]|null $trace
|
|
* @phpstan-param list<array<string, mixed>>|null $trace
|
|
*/
|
|
public function exceptionHandler(\Throwable $e, $trace = null) : void{
|
|
while(@ob_end_flush()){}
|
|
global $lastError;
|
|
|
|
if($trace === null){
|
|
$trace = $e->getTrace();
|
|
}
|
|
|
|
$errstr = $e->getMessage();
|
|
$errfile = $e->getFile();
|
|
$errline = $e->getLine();
|
|
|
|
$errstr = preg_replace('/\s+/', ' ', trim($errstr));
|
|
|
|
$errfile = Filesystem::cleanPath($errfile);
|
|
|
|
$this->logger->logException($e, $trace);
|
|
|
|
$lastError = [
|
|
"type" => get_class($e),
|
|
"message" => $errstr,
|
|
"fullFile" => $e->getFile(),
|
|
"file" => $errfile,
|
|
"line" => $errline,
|
|
"trace" => $trace
|
|
];
|
|
|
|
global $lastExceptionError, $lastError;
|
|
$lastExceptionError = $lastError;
|
|
$this->crashDump();
|
|
}
|
|
|
|
private function writeCrashDumpFile(CrashDump $dump) : string{
|
|
$crashFolder = Path::join($this->getDataPath(), "crashdumps");
|
|
if(!is_dir($crashFolder)){
|
|
mkdir($crashFolder);
|
|
}
|
|
$crashDumpPath = Path::join($crashFolder, date("D_M_j-H.i.s-T_Y", (int) $dump->getData()->time) . ".log");
|
|
|
|
$fp = @fopen($crashDumpPath, "wb");
|
|
if(!is_resource($fp)){
|
|
throw new \RuntimeException("Unable to open new file to generate crashdump");
|
|
}
|
|
$writer = new CrashDumpRenderer($fp, $dump->getData());
|
|
$writer->renderHumanReadable();
|
|
$dump->encodeData($writer);
|
|
|
|
fclose($fp);
|
|
return $crashDumpPath;
|
|
}
|
|
|
|
public function crashDump() : void{
|
|
while(@ob_end_flush()){}
|
|
if(!$this->isRunning){
|
|
return;
|
|
}
|
|
if($this->sendUsageTicker > 0){
|
|
$this->sendUsage(SendUsageTask::TYPE_CLOSE);
|
|
}
|
|
$this->hasStopped = false;
|
|
|
|
ini_set("error_reporting", '0');
|
|
ini_set("memory_limit", '-1'); //Fix error dump not dumped on memory problems
|
|
try{
|
|
$this->logger->emergency($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_crash_create()));
|
|
$dump = new CrashDump($this, $this->pluginManager ?? null);
|
|
|
|
$crashDumpPath = $this->writeCrashDumpFile($dump);
|
|
|
|
$this->logger->emergency($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_crash_submit($crashDumpPath)));
|
|
|
|
if($this->configGroup->getPropertyBool("auto-report.enabled", true)){
|
|
$report = true;
|
|
|
|
$stamp = Path::join($this->getDataPath(), "crashdumps", ".last_crash");
|
|
$crashInterval = 120; //2 minutes
|
|
if(($lastReportTime = @filemtime($stamp)) !== false && $lastReportTime + $crashInterval >= time()){
|
|
$report = false;
|
|
$this->logger->debug("Not sending crashdump due to last crash less than $crashInterval seconds ago");
|
|
}
|
|
@touch($stamp); //update file timestamp
|
|
|
|
$plugin = $dump->getData()->plugin;
|
|
if($plugin !== ""){
|
|
$p = $this->pluginManager->getPlugin($plugin);
|
|
if($p instanceof Plugin && !($p->getPluginLoader() instanceof PharPluginLoader)){
|
|
$this->logger->debug("Not sending crashdump due to caused by non-phar plugin");
|
|
$report = false;
|
|
}
|
|
}
|
|
|
|
if($dump->getData()->error["type"] === \ParseError::class){
|
|
$report = false;
|
|
}
|
|
|
|
if(strrpos(VersionInfo::GIT_HASH(), "-dirty") !== false || VersionInfo::GIT_HASH() === str_repeat("00", 20)){
|
|
$this->logger->debug("Not sending crashdump due to locally modified");
|
|
$report = false; //Don't send crashdumps for locally modified builds
|
|
}
|
|
|
|
if($report){
|
|
$url = ($this->configGroup->getPropertyBool("auto-report.use-https", true) ? "https" : "http") . "://" . $this->configGroup->getPropertyString("auto-report.host", "crash.pmmp.io") . "/submit/api";
|
|
$postUrlError = "Unknown error";
|
|
$reply = Internet::postURL($url, [
|
|
"report" => "yes",
|
|
"name" => $this->getName() . " " . $this->getPocketMineVersion(),
|
|
"email" => "crash@pocketmine.net",
|
|
"reportPaste" => base64_encode($dump->getEncodedData())
|
|
], 10, [], $postUrlError);
|
|
|
|
if($reply !== null && is_object($data = json_decode($reply->getBody()))){
|
|
if(isset($data->crashId) && isset($data->crashUrl)){
|
|
$reportId = $data->crashId;
|
|
$reportUrl = $data->crashUrl;
|
|
$this->logger->emergency($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_crash_archive($reportUrl, (string) $reportId)));
|
|
}elseif(isset($data->error)){
|
|
$this->logger->emergency("Automatic crash report submission failed: $data->error");
|
|
}
|
|
}else{
|
|
$this->logger->emergency("Failed to communicate with crash archive: $postUrlError");
|
|
}
|
|
}
|
|
}
|
|
}catch(\Throwable $e){
|
|
$this->logger->logException($e);
|
|
try{
|
|
$this->logger->critical($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_crash_error($e->getMessage())));
|
|
}catch(\Throwable $e){}
|
|
}
|
|
|
|
$this->forceShutdown();
|
|
$this->isRunning = false;
|
|
|
|
//Force minimum uptime to be >= 120 seconds, to reduce the impact of spammy crash loops
|
|
$spacing = ((int) $this->startTime) - time() + 120;
|
|
if($spacing > 0){
|
|
echo "--- Waiting $spacing seconds to throttle automatic restart (you can kill the process safely now) ---" . PHP_EOL;
|
|
sleep($spacing);
|
|
}
|
|
@Process::kill(Process::pid(), true);
|
|
exit(1);
|
|
}
|
|
|
|
/**
|
|
* @return mixed[]
|
|
*/
|
|
public function __debugInfo() : array{
|
|
return [];
|
|
}
|
|
|
|
public function getTickSleeper() : SleeperHandler{
|
|
return $this->tickSleeper;
|
|
}
|
|
|
|
private function tickProcessor() : void{
|
|
$this->nextTick = microtime(true);
|
|
|
|
while($this->isRunning){
|
|
$this->tick();
|
|
|
|
//sleeps are self-correcting - if we undersleep 1ms on this tick, we'll sleep an extra ms on the next tick
|
|
$this->tickSleeper->sleepUntil($this->nextTick);
|
|
}
|
|
}
|
|
|
|
public function addOnlinePlayer(Player $player) : bool{
|
|
$ev = new PlayerLoginEvent($player, "Plugin reason");
|
|
$ev->call();
|
|
if($ev->isCancelled() || !$player->isConnected()){
|
|
$player->disconnect($ev->getKickMessage());
|
|
|
|
return false;
|
|
}
|
|
|
|
$session = $player->getNetworkSession();
|
|
$position = $player->getPosition();
|
|
$this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_player_logIn(
|
|
TextFormat::AQUA . $player->getName() . TextFormat::WHITE,
|
|
$session->getIp(),
|
|
(string) $session->getPort(),
|
|
(string) $player->getId(),
|
|
$position->getWorld()->getDisplayName(),
|
|
(string) round($position->x, 4),
|
|
(string) round($position->y, 4),
|
|
(string) round($position->z, 4)
|
|
)));
|
|
|
|
foreach($this->playerList as $p){
|
|
$p->getNetworkSession()->onPlayerAdded($player);
|
|
}
|
|
$rawUUID = $player->getUniqueId()->getBytes();
|
|
$this->playerList[$rawUUID] = $player;
|
|
|
|
if($this->sendUsageTicker > 0){
|
|
$this->uniquePlayers[$rawUUID] = $rawUUID;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public function removeOnlinePlayer(Player $player) : void{
|
|
if(isset($this->playerList[$rawUUID = $player->getUniqueId()->getBytes()])){
|
|
unset($this->playerList[$rawUUID]);
|
|
foreach($this->playerList as $p){
|
|
$p->getNetworkSession()->onPlayerRemoved($player);
|
|
}
|
|
}
|
|
}
|
|
|
|
public function sendUsage(int $type = SendUsageTask::TYPE_STATUS) : void{
|
|
if($this->configGroup->getPropertyBool("anonymous-statistics.enabled", true)){
|
|
$this->asyncPool->submitTask(new SendUsageTask($this, $type, $this->uniquePlayers));
|
|
}
|
|
$this->uniquePlayers = [];
|
|
}
|
|
|
|
public function getLanguage() : Language{
|
|
return $this->language;
|
|
}
|
|
|
|
public function isLanguageForced() : bool{
|
|
return $this->forceLanguage;
|
|
}
|
|
|
|
public function getNetwork() : Network{
|
|
return $this->network;
|
|
}
|
|
|
|
public function getMemoryManager() : MemoryManager{
|
|
return $this->memoryManager;
|
|
}
|
|
|
|
private function titleTick() : void{
|
|
Timings::$titleTick->startTiming();
|
|
|
|
$u = Process::getAdvancedMemoryUsage();
|
|
$usage = sprintf("%g/%g/%g MB @ %d threads", round(($u[0] / 1024) / 1024, 2), round(($u[1] / 1024) / 1024, 2), round(($u[2] / 1024) / 1024, 2), Process::getThreadCount());
|
|
|
|
$online = count($this->playerList);
|
|
$connecting = $this->network->getConnectionCount() - $online;
|
|
$bandwidthStats = $this->network->getBandwidthTracker();
|
|
|
|
echo "\x1b]0;" . $this->getName() . " " .
|
|
$this->getPocketMineVersion() .
|
|
" | Online $online/" . $this->getMaxPlayers() .
|
|
($connecting > 0 ? " (+$connecting connecting)" : "") .
|
|
" | Memory " . $usage .
|
|
" | U " . round($bandwidthStats->getSend()->getAverageBytes() / 1024, 2) .
|
|
" D " . round($bandwidthStats->getReceive()->getAverageBytes() / 1024, 2) .
|
|
" kB/s | TPS " . $this->getTicksPerSecondAverage() .
|
|
" | Load " . $this->getTickUsageAverage() . "%\x07";
|
|
|
|
Timings::$titleTick->stopTiming();
|
|
}
|
|
|
|
/**
|
|
* Tries to execute a server tick
|
|
*/
|
|
private function tick() : void{
|
|
$tickTime = microtime(true);
|
|
if(($tickTime - $this->nextTick) < -0.025){ //Allow half a tick of diff
|
|
return;
|
|
}
|
|
|
|
Timings::$serverTick->startTiming();
|
|
|
|
++$this->tickCounter;
|
|
|
|
Timings::$scheduler->startTiming();
|
|
$this->pluginManager->tickSchedulers($this->tickCounter);
|
|
Timings::$scheduler->stopTiming();
|
|
|
|
Timings::$schedulerAsync->startTiming();
|
|
$this->asyncPool->collectTasks();
|
|
Timings::$schedulerAsync->stopTiming();
|
|
|
|
$this->worldManager->tick($this->tickCounter);
|
|
|
|
Timings::$connection->startTiming();
|
|
$this->network->tick();
|
|
Timings::$connection->stopTiming();
|
|
|
|
if(($this->tickCounter % 20) === 0){
|
|
if($this->doTitleTick){
|
|
$this->titleTick();
|
|
}
|
|
$this->currentTPS = 20;
|
|
$this->currentUse = 0;
|
|
|
|
$queryRegenerateEvent = new QueryRegenerateEvent(new QueryInfo($this));
|
|
$queryRegenerateEvent->call();
|
|
$this->queryInfo = $queryRegenerateEvent->getQueryInfo();
|
|
|
|
$this->network->updateName();
|
|
$this->network->getBandwidthTracker()->rotateAverageHistory();
|
|
}
|
|
|
|
if($this->sendUsageTicker > 0 && --$this->sendUsageTicker === 0){
|
|
$this->sendUsageTicker = 6000;
|
|
$this->sendUsage(SendUsageTask::TYPE_STATUS);
|
|
}
|
|
|
|
if(($this->tickCounter % 100) === 0){
|
|
foreach($this->worldManager->getWorlds() as $world){
|
|
$world->clearCache();
|
|
}
|
|
|
|
if($this->getTicksPerSecondAverage() < 12){
|
|
$this->logger->warning($this->getLanguage()->translate(KnownTranslationFactory::pocketmine_server_tickOverload()));
|
|
}
|
|
}
|
|
|
|
$this->getMemoryManager()->check();
|
|
|
|
Timings::$serverTick->stopTiming();
|
|
|
|
$now = microtime(true);
|
|
$this->currentTPS = min(20, 1 / max(0.001, $now - $tickTime));
|
|
$this->currentUse = min(1, ($now - $tickTime) / 0.05);
|
|
|
|
TimingsHandler::tick($this->currentTPS <= $this->profilingTickRate);
|
|
|
|
$idx = $this->tickCounter % 20;
|
|
$this->tickAverage[$idx] = $this->currentTPS;
|
|
$this->useAverage[$idx] = $this->currentUse;
|
|
|
|
if(($this->nextTick - $tickTime) < -1){
|
|
$this->nextTick = $tickTime;
|
|
}else{
|
|
$this->nextTick += 0.05;
|
|
}
|
|
}
|
|
}
|