*/ private array $uniquePlayers = []; private QueryInfo $queryInfo; private ServerConfigGroup $configGroup; /** @var Player[] */ private array $playerList = []; private SignalHandler $signalHandler; /** * @var CommandSender[][] * @phpstan-var array> */ 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 */ 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( private \DynamicClassLoader $autoloader, private \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->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->forceShutdownExit(); 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->forceShutdownExit(); 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"]); $loadErrorCount = 0; $this->pluginManager->loadPlugins($this->pluginPath, $loadErrorCount); if($loadErrorCount > 0){ $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_plugin_someLoadErrors())); $this->forceShutdownExit(); return; } if(!$this->enablePlugins(PluginEnableOrder::STARTUP())){ $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_plugin_someEnableErrors())); $this->forceShutdownExit(); return; } if(!$this->startupPrepareWorlds()){ $this->forceShutdownExit(); return; } if(!$this->enablePlugins(PluginEnableOrder::POSTWORLD())){ $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_plugin_someEnableErrors())); $this->forceShutdownExit(); return; } if(!$this->startupPrepareNetworkInterfaces()){ $this->forceShutdownExit(); 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); $creationOptions->setDifficulty($this->getDifficulty()); 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); } $creationOptions->setDifficulty($this->getDifficulty()); $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 */ 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) : bool{ $allSuccess = true; foreach($this->pluginManager->getPlugins() as $plugin){ if(!$plugin->isEnabled() && $plugin->getDescription()->getOrder()->equals($type)){ if(!$this->pluginManager->enablePlugin($plugin)){ $allSuccess = false; } } } if($type->equals(PluginEnableOrder::POSTWORLD())){ $this->commandMap->registerServerAliases(); } return $allSuccess; } /** * 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(); } } private function forceShutdownExit() : void{ $this->forceShutdown(); Process::kill(Process::pid(), true); } public function forceShutdown() : void{ if($this->hasStopped){ return; } if($this->doTitleTick){ echo "\x1b]0;\x07"; } if($this->isRunning){ $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_forcingShutdown())); } 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>|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; } } }