186 public const BROADCAST_CHANNEL_ADMINISTRATIVE =
"pocketmine.broadcast.admin";
187 public const BROADCAST_CHANNEL_USERS =
"pocketmine.broadcast.user";
189 public const DEFAULT_SERVER_NAME = VersionInfo::NAME .
" Server";
190 public const DEFAULT_MAX_PLAYERS = 20;
191 public const DEFAULT_PORT_IPV4 = 19132;
192 public const DEFAULT_PORT_IPV6 = 19133;
193 public const DEFAULT_MAX_VIEW_DISTANCE = 16;
200 public const TARGET_TICKS_PER_SECOND = 20;
204 public const TARGET_SECONDS_PER_TICK = 1 / self::TARGET_TICKS_PER_SECOND;
205 public const TARGET_NANOSECONDS_PER_TICK = 1_000_000_000 / self::TARGET_TICKS_PER_SECOND;
210 private const TPS_OVERLOAD_WARNING_THRESHOLD = self::TARGET_TICKS_PER_SECOND * 0.6;
212 private const TICKS_PER_WORLD_CACHE_CLEAR = 5 * self::TARGET_TICKS_PER_SECOND;
213 private const TICKS_PER_TPS_OVERLOAD_WARNING = 5 * self::TARGET_TICKS_PER_SECOND;
214 private const TICKS_PER_STATS_REPORT = 300 * self::TARGET_TICKS_PER_SECOND;
216 private const DEFAULT_ASYNC_COMPRESSION_THRESHOLD = 10_000;
218 private static ?
Server $instance =
null;
226 private Config $operators;
228 private Config $whitelist;
230 private bool $isRunning =
true;
232 private bool $hasStopped =
false;
236 private float $profilingTickRate = self::TARGET_TICKS_PER_SECOND;
243 private int $tickCounter = 0;
244 private float $nextTick = 0;
246 private array $tickAverage;
248 private array $useAverage;
249 private float $currentTPS = self::TARGET_TICKS_PER_SECOND;
250 private float $currentUse = 0;
251 private float $startTime;
253 private bool $doTitleTick =
true;
255 private int $sendUsageTicker = 0;
270 private int $maxPlayers;
272 private bool $onlineMode =
true;
275 private bool $networkCompressionAsync =
true;
276 private int $networkCompressionAsyncThreshold = self::DEFAULT_ASYNC_COMPRESSION_THRESHOLD;
279 private bool $forceLanguage =
false;
281 private UuidInterface $serverID;
283 private string $dataPath;
284 private string $pluginPath;
292 private array $uniquePlayers = [];
299 private array $playerList = [];
307 private array $broadcastSubscribers = [];
309 public function getName() : string{
313 public function isRunning() : bool{
314 return $this->isRunning;
317 public function getPocketMineVersion() : string{
318 return VersionInfo::VERSION()->getFullVersion(true);
321 public function getVersion() : string{
322 return ProtocolInfo::MINECRAFT_VERSION;
325 public function getApiVersion() : string{
326 return VersionInfo::BASE_VERSION;
329 public function getFilePath() : string{
333 public function getResourcePath() : string{
337 public function getDataPath() : string{
338 return $this->dataPath;
341 public function getPluginPath() : string{
342 return $this->pluginPath;
345 public function getMaxPlayers() : int{
346 return $this->maxPlayers;
354 return $this->onlineMode;
361 return $this->getOnlineMode();
364 public function getPort() : int{
365 return $this->configGroup->getConfigInt(
ServerProperties::SERVER_PORT_IPV4, self::DEFAULT_PORT_IPV4);
368 public function getPortV6() : int{
369 return $this->configGroup->getConfigInt(ServerProperties::SERVER_PORT_IPV6, self::DEFAULT_PORT_IPV6);
372 public function getViewDistance() : int{
373 return max(2, $this->configGroup->getConfigInt(ServerProperties::VIEW_DISTANCE, self::DEFAULT_MAX_VIEW_DISTANCE));
380 return max(2, min($distance, $this->memoryManager->getViewDistance($this->getViewDistance())));
383 public function getIp() : string{
385 return $str !==
"" ? $str :
"0.0.0.0";
388 public function getIpV6() : string{
389 $str = $this->configGroup->getConfigString(ServerProperties::SERVER_IPV6);
390 return $str !==
"" ? $str :
"::";
393 public function getServerUniqueId() : UuidInterface{
394 return $this->serverID;
397 public function getGamemode() : GameMode{
398 return GameMode::fromString($this->configGroup->getConfigString(ServerProperties::GAME_MODE)) ?? GameMode::SURVIVAL;
401 public function getForceGamemode() : bool{
402 return $this->configGroup->getConfigBool(ServerProperties::FORCE_GAME_MODE, false);
412 public function hasWhitelist() : bool{
413 return $this->configGroup->getConfigBool(
ServerProperties::WHITELIST, false);
416 public function isHardcore() : bool{
417 return $this->configGroup->getConfigBool(ServerProperties::HARDCORE, false);
420 public function getMotd() : string{
421 return $this->configGroup->getConfigString(ServerProperties::MOTD, self::DEFAULT_SERVER_NAME);
424 public function getLoader() : ThreadSafeClassLoader{
425 return $this->autoloader;
428 public function getLogger() : AttachableThreadSafeLogger{
429 return $this->logger;
432 public function getUpdater() : UpdateChecker{
433 return $this->updater;
436 public function getPluginManager() : PluginManager{
437 return $this->pluginManager;
440 public function getCraftingManager() : CraftingManager{
441 return $this->craftingManager;
444 public function getResourcePackManager() : ResourcePackManager{
445 return $this->resourceManager;
448 public function getWorldManager() : WorldManager{
449 return $this->worldManager;
452 public function getAsyncPool() : AsyncPool{
453 return $this->asyncPool;
456 public function getTick() : int{
457 return $this->tickCounter;
464 return round($this->currentTPS, 2);
471 return round(array_sum($this->tickAverage) / count($this->tickAverage), 2);
478 return round($this->currentUse * 100, 2);
485 return round((array_sum($this->useAverage) / count($this->useAverage)) * 100, 2);
488 public function getStartTime() : float{
489 return $this->startTime;
492 public function getCommandMap() : SimpleCommandMap{
493 return $this->commandMap;
500 return $this->playerList;
503 public function shouldSavePlayerData() : bool{
504 return $this->configGroup->getPropertyBool(Yml::PLAYER_SAVE_PLAYER_DATA, true);
507 public function getOfflinePlayer(
string $name) : Player|OfflinePlayer|null{
508 $name = strtolower($name);
509 $result = $this->getPlayerExact($name);
511 if($result ===
null){
512 $result =
new OfflinePlayer($name, $this->getOfflinePlayerData($name));
522 return $this->playerDataProvider->hasData($name);
525 public function getOfflinePlayerData(
string $name) : ?
CompoundTag{
528 return $this->playerDataProvider->loadData($name);
529 }
catch(PlayerDataLoadException $e){
530 $this->logger->debug(
"Failed to load player data for $name: " . $e->getMessage());
531 $this->logger->error($this->language->translate(KnownTranslationFactory::pocketmine_data_playerCorrupted($name)));
537 public function saveOfflinePlayerData(
string $name, CompoundTag $nbtTag) : void{
538 $ev = new PlayerDataSaveEvent($nbtTag, $name, $this->getPlayerExact($name));
539 if(!$this->shouldSavePlayerData()){
545 if(!$ev->isCancelled()){
546 Timings::$syncPlayerDataSave->time(function() use ($name, $ev) : void{
548 $this->playerDataProvider->saveData($name, $ev->getSaveData());
549 }catch(PlayerDataSaveException $e){
550 $this->logger->critical($this->language->translate(KnownTranslationFactory::pocketmine_data_saveError($name, $e->getMessage())));
551 $this->logger->logException($e);
563 $class = $ev->getPlayerClass();
565 if($offlinePlayerData !==
null && ($world = $this->worldManager->getWorldByName($offlinePlayerData->getString(Player::TAG_LEVEL,
""))) !==
null){
566 $playerPos = EntityDataHelper::parseLocation($offlinePlayerData, $world);
568 $world = $this->worldManager->getDefaultWorld();
570 throw new AssumptionFailedError(
"Default world should always be loaded");
577 $createPlayer =
function(
Location $location) use ($playerPromiseResolver, $class, $session, $playerInfo, $authenticated, $offlinePlayerData) :
void{
579 $player =
new $class($this, $session, $playerInfo, $authenticated, $location, $offlinePlayerData);
580 if(!$player->hasPlayedBefore()){
581 $player->onGround =
true;
583 $playerPromiseResolver->resolve($player);
586 if($playerPos ===
null){
587 $world->requestSafeSpawn()->onCompletion(
588 function(Position $spawn) use ($createPlayer, $playerPromiseResolver, $session, $world) :
void{
589 if(!$session->isConnected()){
590 $playerPromiseResolver->reject();
593 $createPlayer(Location::fromObject($spawn, $world));
595 function() use ($playerPromiseResolver, $session) : void{
596 if($session->isConnected()){
597 $session->disconnectWithError(KnownTranslationFactory::pocketmine_disconnect_error_respawn());
599 $playerPromiseResolver->reject();
603 $createPlayer($playerPos);
606 return $playerPromiseResolver->getPromise();
621 $name = strtolower($name);
622 $delta = PHP_INT_MAX;
623 foreach($this->getOnlinePlayers() as $player){
624 if(stripos($player->getName(), $name) === 0){
625 $curDelta = strlen($player->getName()) - strlen($name);
626 if($curDelta < $delta){
643 $name = strtolower($name);
644 foreach($this->getOnlinePlayers() as $player){
645 if(strtolower($player->getName()) === $name){
657 return $this->playerList[$rawUUID] ?? null;
664 return $this->getPlayerByRawUUID($uuid->getBytes());
668 return $this->configGroup;
676 if(($command = $this->commandMap->getCommand($name)) instanceof
PluginOwned){
683 public function getNameBans() :
BanList{
684 return $this->banByName;
687 public function getIPBans() : BanList{
688 return $this->banByIP;
691 public function addOp(
string $name) : void{
692 $this->operators->set(strtolower($name), true);
694 if(($player = $this->getPlayerExact($name)) !==
null){
695 $player->setBasePermission(DefaultPermissions::ROOT_OPERATOR,
true);
697 $this->operators->save();
700 public function removeOp(
string $name) : void{
701 $lowercaseName = strtolower($name);
702 foreach(Utils::promoteKeys($this->operators->getAll()) as $operatorName => $_){
703 $operatorName = (string) $operatorName;
704 if($lowercaseName === strtolower($operatorName)){
705 $this->operators->remove($operatorName);
709 if(($player = $this->getPlayerExact($name)) !==
null){
710 $player->unsetBasePermission(DefaultPermissions::ROOT_OPERATOR);
712 $this->operators->save();
715 public function addWhitelist(
string $name) : void{
716 $this->whitelist->set(strtolower($name), true);
717 $this->whitelist->save();
720 public function removeWhitelist(
string $name) : void{
721 $this->whitelist->remove(strtolower($name));
722 $this->whitelist->save();
725 public function isWhitelisted(
string $name) : bool{
726 return !$this->hasWhitelist() || $this->operators->exists($name, true) || $this->whitelist->exists($name, true);
729 public function isOp(
string $name) : bool{
730 return $this->operators->exists($name, true);
733 public function getWhitelisted() : Config{
734 return $this->whitelist;
737 public function getOps() : Config{
738 return $this->operators;
746 $section = $this->configGroup->getProperty(Yml::ALIASES);
748 if(is_array($section)){
749 foreach(Utils::promoteKeys($section) as $key => $value){
753 if(is_array($value)){
756 $commands[] = (string) $value;
759 $result[(string) $key] = $commands;
766 public static function getInstance() : Server{
767 if(self::$instance === null){
768 throw new \RuntimeException(
"Attempt to retrieve Server instance outside server thread");
770 return self::$instance;
773 public function __construct(
774 private ThreadSafeClassLoader $autoloader,
775 private AttachableThreadSafeLogger $logger,
779 if(self::$instance !==
null){
780 throw new \LogicException(
"Only one server instance can exist at once");
782 self::$instance = $this;
783 $this->startTime = microtime(
true);
784 $this->tickAverage = array_fill(0, self::TARGET_TICKS_PER_SECOND, self::TARGET_TICKS_PER_SECOND);
785 $this->useAverage = array_fill(0, self::TARGET_TICKS_PER_SECOND, 0);
788 $this->tickSleeper =
new TimeTrackingSleeperHandler(Timings::$serverInterrupts);
790 $this->signalHandler =
new SignalHandler(
function() :
void{
791 $this->logger->info(
"Received signal interrupt, stopping the server");
799 Path::join($dataPath,
"worlds"),
800 Path::join($dataPath,
"players")
802 if(!file_exists($neededPath)){
803 mkdir($neededPath, 0777);
807 $this->dataPath = realpath($dataPath) . DIRECTORY_SEPARATOR;
808 $this->pluginPath = realpath($pluginPath) . DIRECTORY_SEPARATOR;
810 $this->logger->info(
"Loading server configuration");
811 $pocketmineYmlPath = Path::join($this->dataPath,
"pocketmine.yml");
812 if(!file_exists($pocketmineYmlPath)){
813 $content = Filesystem::fileGetContents(Path::join(\
pocketmine\RESOURCE_PATH,
"pocketmine.yml"));
814 if(VersionInfo::IS_DEVELOPMENT_BUILD){
815 $content = str_replace(
"preferred-channel: stable",
"preferred-channel: beta", $content);
817 @file_put_contents($pocketmineYmlPath, $content);
820 $this->configGroup =
new ServerConfigGroup(
821 new Config($pocketmineYmlPath, Config::YAML, []),
822 new Config(Path::join($this->dataPath,
"server.properties"), Config::PROPERTIES, [
823 ServerProperties::MOTD => self::DEFAULT_SERVER_NAME,
824 ServerProperties::SERVER_PORT_IPV4 => self::DEFAULT_PORT_IPV4,
825 ServerProperties::SERVER_PORT_IPV6 => self::DEFAULT_PORT_IPV6,
826 ServerProperties::ENABLE_IPV6 =>
true,
827 ServerProperties::WHITELIST =>
false,
828 ServerProperties::MAX_PLAYERS => self::DEFAULT_MAX_PLAYERS,
829 ServerProperties::GAME_MODE => GameMode::SURVIVAL->name,
830 ServerProperties::FORCE_GAME_MODE =>
false,
831 ServerProperties::HARDCORE =>
false,
832 ServerProperties::PVP =>
true,
833 ServerProperties::DIFFICULTY => World::DIFFICULTY_NORMAL,
834 ServerProperties::DEFAULT_WORLD_GENERATOR_SETTINGS =>
"",
835 ServerProperties::DEFAULT_WORLD_NAME =>
"world",
836 ServerProperties::DEFAULT_WORLD_SEED =>
"",
837 ServerProperties::DEFAULT_WORLD_GENERATOR =>
"DEFAULT",
838 ServerProperties::ENABLE_QUERY =>
true,
839 ServerProperties::AUTO_SAVE =>
true,
840 ServerProperties::VIEW_DISTANCE => self::DEFAULT_MAX_VIEW_DISTANCE,
841 ServerProperties::XBOX_AUTH =>
true,
842 ServerProperties::LANGUAGE =>
"eng"
846 $debugLogLevel = $this->configGroup->getPropertyInt(Yml::DEBUG_LEVEL, 1);
847 if($this->logger instanceof MainLogger){
848 $this->logger->setLogDebug($debugLogLevel > 1);
851 $this->forceLanguage = $this->configGroup->getPropertyBool(Yml::SETTINGS_FORCE_LANGUAGE,
false);
852 $selectedLang = $this->configGroup->getConfigString(ServerProperties::LANGUAGE, $this->configGroup->getPropertyString(
"settings.language", Language::FALLBACK_LANGUAGE));
854 $this->language =
new Language($selectedLang);
855 }
catch(LanguageNotFoundException $e){
856 $this->logger->error($e->getMessage());
858 $this->language =
new Language(Language::FALLBACK_LANGUAGE);
859 }
catch(LanguageNotFoundException $e){
860 $this->logger->emergency(
"Fallback language \"" . Language::FALLBACK_LANGUAGE .
"\" not found");
865 $this->logger->info($this->language->translate(KnownTranslationFactory::language_selected($this->language->getName(), $this->language->getLang())));
867 if(VersionInfo::IS_DEVELOPMENT_BUILD){
868 if(!$this->configGroup->getPropertyBool(Yml::SETTINGS_ENABLE_DEV_BUILDS,
false)){
869 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error1(VersionInfo::NAME)));
870 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error2()));
871 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error3()));
872 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error4(Yml::SETTINGS_ENABLE_DEV_BUILDS)));
873 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error5(
"https://github.com/pmmp/PocketMine-MP/releases")));
874 $this->forceShutdownExit();
879 $this->logger->warning(str_repeat(
"-", 40));
880 $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_warning1(VersionInfo::NAME)));
881 $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_warning2()));
882 $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_warning3()));
883 $this->logger->warning(str_repeat(
"-", 40));
886 $this->memoryManager =
new MemoryManager($this);
888 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_start(TextFormat::AQUA . $this->getVersion() . TextFormat::RESET)));
890 if(($poolSize = $this->configGroup->getPropertyString(Yml::SETTINGS_ASYNC_WORKERS,
"auto")) ===
"auto"){
892 $processors = Utils::getCoreCount() - 2;
895 $poolSize = max(1, $processors);
898 $poolSize = max(1, (
int) $poolSize);
901 TimingsHandler::setEnabled($this->configGroup->getPropertyBool(Yml::SETTINGS_ENABLE_PROFILING,
false));
902 $this->profilingTickRate = $this->configGroup->getPropertyInt(Yml::SETTINGS_PROFILE_REPORT_TRIGGER, self::TARGET_TICKS_PER_SECOND);
904 $this->asyncPool =
new AsyncPool($poolSize, max(-1, $this->configGroup->getPropertyInt(Yml::MEMORY_ASYNC_WORKER_HARD_LIMIT, 256)), $this->autoloader, $this->logger, $this->tickSleeper);
905 $this->asyncPool->addWorkerStartHook(
function(
int $i) :
void{
906 if(TimingsHandler::isEnabled()){
907 $this->asyncPool->submitTaskToWorker(TimingsControlTask::setEnabled(
true), $i);
910 TimingsHandler::getToggleCallbacks()->add(
function(
bool $enable) :
void{
911 foreach($this->asyncPool->getRunningWorkers() as $workerId){
912 $this->asyncPool->submitTaskToWorker(TimingsControlTask::setEnabled($enable), $workerId);
915 TimingsHandler::getReloadCallbacks()->add(
function() :
void{
916 foreach($this->asyncPool->getRunningWorkers() as $workerId){
917 $this->asyncPool->submitTaskToWorker(TimingsControlTask::reload(), $workerId);
920 TimingsHandler::getCollectCallbacks()->add(
function() : array{
922 foreach($this->asyncPool->getRunningWorkers() as $workerId){
924 $resolver =
new PromiseResolver();
925 $this->asyncPool->submitTaskToWorker(
new TimingsCollectionTask($resolver), $workerId);
927 $promises[] = $resolver->getPromise();
933 $netCompressionThreshold = -1;
934 if($this->configGroup->getPropertyInt(Yml::NETWORK_BATCH_THRESHOLD, 256) >= 0){
935 $netCompressionThreshold = $this->configGroup->getPropertyInt(Yml::NETWORK_BATCH_THRESHOLD, 256);
937 if($netCompressionThreshold < 0){
938 $netCompressionThreshold =
null;
941 $netCompressionLevel = $this->configGroup->getPropertyInt(Yml::NETWORK_COMPRESSION_LEVEL, 6);
942 if($netCompressionLevel < 1 || $netCompressionLevel > 9){
943 $this->logger->warning(
"Invalid network compression level $netCompressionLevel set, setting to default 6");
944 $netCompressionLevel = 6;
946 ZlibCompressor::setInstance(
new ZlibCompressor($netCompressionLevel, $netCompressionThreshold, ZlibCompressor::DEFAULT_MAX_DECOMPRESSION_SIZE));
948 $this->networkCompressionAsync = $this->configGroup->getPropertyBool(Yml::NETWORK_ASYNC_COMPRESSION,
true);
949 $this->networkCompressionAsyncThreshold = max(
950 $this->configGroup->getPropertyInt(Yml::NETWORK_ASYNC_COMPRESSION_THRESHOLD, self::DEFAULT_ASYNC_COMPRESSION_THRESHOLD),
951 $netCompressionThreshold ?? self::DEFAULT_ASYNC_COMPRESSION_THRESHOLD
954 EncryptionContext::$ENABLED = $this->configGroup->getPropertyBool(Yml::NETWORK_ENABLE_ENCRYPTION,
true);
956 $this->doTitleTick = $this->configGroup->getPropertyBool(Yml::CONSOLE_TITLE_TICK,
true) && Terminal::hasFormattingCodes();
958 $this->operators =
new Config(Path::join($this->dataPath,
"ops.txt"), Config::ENUM);
959 $this->whitelist =
new Config(Path::join($this->dataPath,
"white-list.txt"), Config::ENUM);
961 $bannedTxt = Path::join($this->dataPath,
"banned.txt");
962 $bannedPlayersTxt = Path::join($this->dataPath,
"banned-players.txt");
963 if(file_exists($bannedTxt) && !file_exists($bannedPlayersTxt)){
964 @rename($bannedTxt, $bannedPlayersTxt);
966 @touch($bannedPlayersTxt);
967 $this->banByName =
new BanList($bannedPlayersTxt);
968 $this->banByName->load();
969 $bannedIpsTxt = Path::join($this->dataPath,
"banned-ips.txt");
970 @touch($bannedIpsTxt);
971 $this->banByIP =
new BanList($bannedIpsTxt);
972 $this->banByIP->load();
974 $this->maxPlayers = $this->configGroup->getConfigInt(ServerProperties::MAX_PLAYERS, self::DEFAULT_MAX_PLAYERS);
976 $this->onlineMode = $this->configGroup->getConfigBool(ServerProperties::XBOX_AUTH,
true);
977 if($this->onlineMode){
978 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_auth_enabled()));
980 $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_auth_disabled()));
981 $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_authWarning()));
982 $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_authProperty_disabled()));
985 if($this->configGroup->getConfigBool(ServerProperties::HARDCORE,
false) && $this->getDifficulty() < World::DIFFICULTY_HARD){
986 $this->configGroup->setConfigInt(ServerProperties::DIFFICULTY, World::DIFFICULTY_HARD);
989 @cli_set_process_title($this->getName() .
" " . $this->getPocketMineVersion());
991 $this->serverID = Utils::getMachineUniqueId($this->getIp() . $this->getPort());
993 $this->logger->debug(
"Server unique id: " . $this->getServerUniqueId());
994 $this->logger->debug(
"Machine unique id: " . Utils::getMachineUniqueId());
996 $this->network =
new Network($this->logger);
997 $this->network->setName($this->getMotd());
999 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_info(
1001 (VersionInfo::IS_DEVELOPMENT_BUILD ? TextFormat::YELLOW :
"") . $this->getPocketMineVersion() . TextFormat::RESET
1003 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_license($this->getName())));
1005 DefaultPermissions::registerCorePermissions();
1007 $this->commandMap =
new SimpleCommandMap($this);
1009 $this->craftingManager = CraftingManagerFromDataHelper::make(BedrockDataFiles::RECIPES);
1011 $this->resourceManager =
new ResourcePackManager(Path::join($this->dataPath,
"resource_packs"), $this->logger);
1013 $pluginGraylist =
null;
1014 $graylistFile = Path::join($this->dataPath,
"plugin_list.yml");
1015 if(!file_exists($graylistFile)){
1016 copy(Path::join(\
pocketmine\RESOURCE_PATH,
'plugin_list.yml'), $graylistFile);
1019 $array = yaml_parse(Filesystem::fileGetContents($graylistFile));
1020 if(!is_array($array)){
1021 throw new \InvalidArgumentException(
"Expected array for root, but have " . gettype($array));
1023 $pluginGraylist = PluginGraylist::fromArray($array);
1024 }
catch(\InvalidArgumentException $e){
1025 $this->logger->emergency(
"Failed to load $graylistFile: " . $e->getMessage());
1026 $this->forceShutdownExit();
1029 $this->pluginManager =
new PluginManager($this, $this->configGroup->getPropertyBool(Yml::PLUGINS_LEGACY_DATA_DIR,
true) ?
null : Path::join($this->dataPath,
"plugin_data"), $pluginGraylist);
1030 $this->pluginManager->registerInterface(
new PharPluginLoader($this->autoloader));
1031 $this->pluginManager->registerInterface(
new ScriptPluginLoader());
1033 $providerManager =
new WorldProviderManager();
1035 ($format = $providerManager->getProviderByName($formatName = $this->configGroup->getPropertyString(Yml::LEVEL_SETTINGS_DEFAULT_FORMAT,
""))) !==
null &&
1036 $format instanceof WritableWorldProviderManagerEntry
1038 $providerManager->setDefault($format);
1039 }elseif($formatName !==
""){
1040 $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_level_badDefaultFormat($formatName)));
1043 $this->worldManager =
new WorldManager($this, Path::join($this->dataPath,
"worlds"), $providerManager);
1044 $this->worldManager->setAutoSave($this->configGroup->getConfigBool(ServerProperties::AUTO_SAVE, $this->worldManager->getAutoSave()));
1045 $this->worldManager->setAutoSaveInterval($this->configGroup->getPropertyInt(Yml::TICKS_PER_AUTOSAVE, $this->worldManager->getAutoSaveInterval()));
1047 $this->updater =
new UpdateChecker($this, $this->configGroup->getPropertyString(Yml::AUTO_UPDATER_HOST,
"update.pmmp.io"));
1049 $this->queryInfo =
new QueryInfo($this);
1051 $this->playerDataProvider =
new DatFilePlayerDataProvider(Path::join($this->dataPath,
"players"));
1053 register_shutdown_function($this->crashDump(...));
1055 $loadErrorCount = 0;
1056 $this->pluginManager->loadPlugins($this->pluginPath, $loadErrorCount);
1057 if($loadErrorCount > 0){
1058 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_plugin_someLoadErrors()));
1059 $this->forceShutdownExit();
1062 if(!$this->enablePlugins(PluginEnableOrder::STARTUP)){
1063 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_plugin_someEnableErrors()));
1064 $this->forceShutdownExit();
1068 if(!$this->startupPrepareWorlds()){
1069 $this->forceShutdownExit();
1073 if(!$this->enablePlugins(PluginEnableOrder::POSTWORLD)){
1074 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_plugin_someEnableErrors()));
1075 $this->forceShutdownExit();
1079 if(!$this->startupPrepareNetworkInterfaces()){
1080 $this->forceShutdownExit();
1084 if($this->configGroup->getPropertyBool(Yml::ANONYMOUS_STATISTICS_ENABLED,
true)){
1085 $this->sendUsageTicker = self::TICKS_PER_STATS_REPORT;
1086 $this->sendUsage(SendUsageTask::TYPE_OPEN);
1089 $this->configGroup->save();
1091 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_defaultGameMode($this->getGamemode()->getTranslatableName())));
1092 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_donate(TextFormat::AQUA .
"https://patreon.com/pocketminemp" . TextFormat::RESET)));
1093 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_startFinished(strval(round(microtime(
true) - $this->startTime, 3)))));
1095 $forwarder =
new BroadcastLoggerForwarder($this, $this->logger, $this->language);
1096 $this->subscribeToBroadcastChannel(self::BROADCAST_CHANNEL_ADMINISTRATIVE, $forwarder);
1097 $this->subscribeToBroadcastChannel(self::BROADCAST_CHANNEL_USERS, $forwarder);
1100 if($this->configGroup->getPropertyBool(Yml::CONSOLE_ENABLE_INPUT,
true)){
1101 $this->console =
new ConsoleReaderChildProcessDaemon($this->logger);
1104 $this->tickProcessor();
1105 $this->forceShutdown();
1106 }
catch(\Throwable $e){
1107 $this->exceptionHandler($e);
1111 private function startupPrepareWorlds() : bool{
1112 $getGenerator = function(string $generatorName, string $generatorOptions, string $worldName) : ?string{
1113 $generatorEntry = GeneratorManager::getInstance()->getGenerator($generatorName);
1114 if($generatorEntry ===
null){
1115 $this->logger->error($this->language->translate(KnownTranslationFactory::pocketmine_level_generationError(
1117 KnownTranslationFactory::pocketmine_level_unknownGenerator($generatorName)
1122 $generatorEntry->validateGeneratorOptions($generatorOptions);
1123 }
catch(InvalidGeneratorOptionsException $e){
1124 $this->logger->error($this->language->translate(KnownTranslationFactory::pocketmine_level_generationError(
1126 KnownTranslationFactory::pocketmine_level_invalidGeneratorOptions($generatorOptions, $generatorName, $e->getMessage())
1130 return $generatorEntry->getGeneratorClass();
1133 $anyWorldFailedToLoad =
false;
1135 foreach(Utils::promoteKeys((array) $this->configGroup->getProperty(Yml::WORLDS, [])) as $name => $options){
1136 if(!is_string($name)){
1140 if($options ===
null){
1142 }elseif(!is_array($options)){
1146 if(!$this->worldManager->loadWorld($name,
true)){
1147 if($this->worldManager->isWorldGenerated($name)){
1149 $anyWorldFailedToLoad = true;
1152 $creationOptions = WorldCreationOptions::create();
1155 $generatorName = $options[
"generator"] ??
"default";
1156 $generatorOptions = isset($options[
"preset"]) && is_string($options[
"preset"]) ? $options[
"preset"] :
"";
1158 $generatorClass = $getGenerator($generatorName, $generatorOptions, $name);
1159 if($generatorClass ===
null){
1160 $anyWorldFailedToLoad =
true;
1163 $creationOptions->setGeneratorClass($generatorClass);
1164 $creationOptions->setGeneratorOptions($generatorOptions);
1166 $creationOptions->setDifficulty($this->getDifficulty());
1167 if(isset($options[
"difficulty"]) && is_string($options[
"difficulty"])){
1168 $creationOptions->setDifficulty(World::getDifficultyFromString($options[
"difficulty"]));
1171 if(isset($options[
"seed"])){
1172 $convertedSeed = Generator::convertSeed((
string) ($options[
"seed"] ??
""));
1173 if($convertedSeed !==
null){
1174 $creationOptions->setSeed($convertedSeed);
1178 $this->worldManager->generateWorld($name, $creationOptions);
1182 if($this->worldManager->getDefaultWorld() ===
null){
1183 $default = $this->configGroup->getConfigString(ServerProperties::DEFAULT_WORLD_NAME,
"world");
1184 if(trim($default) ===
""){
1185 $this->logger->warning(
"level-name cannot be null, using default");
1187 $this->configGroup->setConfigString(ServerProperties::DEFAULT_WORLD_NAME,
"world");
1189 if(!$this->worldManager->loadWorld($default,
true)){
1190 if($this->worldManager->isWorldGenerated($default)){
1191 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_level_defaultError()));
1195 $generatorName = $this->configGroup->getConfigString(ServerProperties::DEFAULT_WORLD_GENERATOR);
1196 $generatorOptions = $this->configGroup->getConfigString(ServerProperties::DEFAULT_WORLD_GENERATOR_SETTINGS);
1197 $generatorClass = $getGenerator($generatorName, $generatorOptions, $default);
1199 if($generatorClass ===
null){
1200 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_level_defaultError()));
1203 $creationOptions = WorldCreationOptions::create()
1204 ->setGeneratorClass($generatorClass)
1205 ->setGeneratorOptions($generatorOptions);
1206 $convertedSeed = Generator::convertSeed($this->configGroup->getConfigString(ServerProperties::DEFAULT_WORLD_SEED));
1207 if($convertedSeed !==
null){
1208 $creationOptions->setSeed($convertedSeed);
1210 $creationOptions->setDifficulty($this->getDifficulty());
1211 $this->worldManager->generateWorld($default, $creationOptions);
1214 $world = $this->worldManager->getWorldByName($default);
1215 if($world ===
null){
1216 throw new AssumptionFailedError(
"We just loaded/generated the default world, so it must exist");
1218 $this->worldManager->setDefaultWorld($world);
1221 return !$anyWorldFailedToLoad;
1224 private function startupPrepareConnectableNetworkInterfaces(
1229 PacketBroadcaster $packetBroadcaster,
1230 EntityEventBroadcaster $entityEventBroadcaster,
1231 TypeConverter $typeConverter
1233 $prettyIp = $ipV6 ?
"[$ip]" : $ip;
1235 $rakLibRegistered = $this->network->registerInterface(
new RakLibInterface($this, $ip, $port, $ipV6, $packetBroadcaster, $entityEventBroadcaster, $typeConverter));
1236 }
catch(NetworkInterfaceStartException $e){
1237 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_networkStartFailed(
1244 if($rakLibRegistered){
1245 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_networkStart($prettyIp, (
string) $port)));
1248 if(!$rakLibRegistered){
1251 $this->network->registerInterface(
new DedicatedQueryNetworkInterface($ip, $port, $ipV6,
new \
PrefixedLogger($this->logger,
"Dedicated Query Interface")));
1253 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_query_running($prettyIp, (
string) $port)));
1258 private function startupPrepareNetworkInterfaces() : bool{
1259 $useQuery = $this->configGroup->getConfigBool(ServerProperties::ENABLE_QUERY, true);
1261 $typeConverter = TypeConverter::getInstance();
1262 $packetBroadcaster =
new StandardPacketBroadcaster($this);
1263 $entityEventBroadcaster =
new StandardEntityEventBroadcaster($packetBroadcaster, $typeConverter);
1266 !$this->startupPrepareConnectableNetworkInterfaces($this->getIp(), $this->getPort(),
false, $useQuery, $packetBroadcaster, $entityEventBroadcaster, $typeConverter) ||
1268 $this->configGroup->getConfigBool(ServerProperties::ENABLE_IPV6,
true) &&
1269 !$this->startupPrepareConnectableNetworkInterfaces($this->getIpV6(), $this->getPortV6(),
true, $useQuery, $packetBroadcaster, $entityEventBroadcaster, $typeConverter)
1276 $this->network->registerRawPacketHandler(
new QueryHandler($this));
1279 foreach($this->getIPBans()->getEntries() as $entry){
1280 $this->network->blockAddress($entry->getName(), -1);
1283 if($this->configGroup->getPropertyBool(Yml::NETWORK_UPNP_FORWARDING,
false)){
1284 $this->network->registerInterface(
new UPnPNetworkInterface($this->logger, Internet::getInternalIP(), $this->getPort()));
1295 $this->broadcastSubscribers[$channelId][spl_object_id($subscriber)] = $subscriber;
1302 if(isset($this->broadcastSubscribers[$channelId][spl_object_id($subscriber)])){
1303 if(count($this->broadcastSubscribers[$channelId]) === 1){
1304 unset($this->broadcastSubscribers[$channelId]);
1306 unset($this->broadcastSubscribers[$channelId][spl_object_id($subscriber)]);
1315 foreach(
Utils::stringifyKeys($this->broadcastSubscribers) as $channelId => $recipients){
1316 $this->unsubscribeFromBroadcastChannel($channelId, $subscriber);
1327 return $this->broadcastSubscribers[$channelId] ?? [];
1334 $recipients = $recipients ?? $this->getBroadcastChannelSubscribers(self::BROADCAST_CHANNEL_USERS);
1336 foreach($recipients as $recipient){
1337 $recipient->sendMessage($message);
1340 return count($recipients);
1346 private function getPlayerBroadcastSubscribers(
string $channelId) : array{
1349 foreach($this->broadcastSubscribers[$channelId] as $subscriber){
1350 if($subscriber instanceof Player){
1351 $players[spl_object_id($subscriber)] = $subscriber;
1360 public function broadcastTip(
string $tip, ?array $recipients =
null) : int{
1361 $recipients = $recipients ?? $this->getPlayerBroadcastSubscribers(self::BROADCAST_CHANNEL_USERS);
1363 foreach($recipients as $recipient){
1364 $recipient->sendTip($tip);
1367 return count($recipients);
1374 $recipients = $recipients ?? $this->getPlayerBroadcastSubscribers(self::BROADCAST_CHANNEL_USERS);
1376 foreach($recipients as $recipient){
1377 $recipient->sendPopup($popup);
1380 return count($recipients);
1389 public function broadcastTitle(
string $title,
string $subtitle =
"",
int $fadeIn = -1,
int $stay = -1,
int $fadeOut = -1, ?array $recipients =
null) : int{
1390 $recipients = $recipients ?? $this->getPlayerBroadcastSubscribers(self::BROADCAST_CHANNEL_USERS);
1392 foreach($recipients as $recipient){
1393 $recipient->sendTitle($title, $subtitle, $fadeIn, $stay, $fadeOut);
1396 return count($recipients);
1412 public function prepareBatch(
string $buffer, Compressor $compressor, ?
bool $sync =
null, ?TimingsHandler $timings =
null) : CompressBatchPromise|string{
1413 $timings ??= Timings::$playerNetworkSendCompress;
1415 $timings->startTiming();
1417 $threshold = $compressor->getCompressionThreshold();
1418 if($threshold ===
null || strlen($buffer) < $compressor->getCompressionThreshold()){
1419 $compressionType = CompressionAlgorithm::NONE;
1420 $compressed = $buffer;
1423 $sync ??= !$this->networkCompressionAsync;
1425 if(!$sync && strlen($buffer) >= $this->networkCompressionAsyncThreshold){
1426 $promise =
new CompressBatchPromise();
1427 $task =
new CompressBatchTask($buffer, $promise, $compressor);
1428 $this->asyncPool->submitTask($task);
1432 $compressionType = $compressor->getNetworkId();
1433 $compressed = $compressor->compress($buffer);
1436 return chr($compressionType) . $compressed;
1438 $timings->stopTiming();
1442 public function enablePlugins(PluginEnableOrder $type) : bool{
1444 foreach($this->pluginManager->getPlugins() as $plugin){
1445 if(!$plugin->isEnabled() && $plugin->getDescription()->getOrder() === $type){
1446 if(!$this->pluginManager->enablePlugin($plugin)){
1447 $allSuccess =
false;
1452 if($type === PluginEnableOrder::POSTWORLD){
1453 $this->commandMap->registerServerAliases();
1466 if($ev->isCancelled()){
1470 $commandLine = $ev->getCommand();
1473 return $this->commandMap->dispatch($sender, $commandLine);
1480 if($this->isRunning){
1481 $this->isRunning =
false;
1482 $this->signalHandler->unregister();
1486 private function forceShutdownExit() : void{
1487 $this->forceShutdown();
1488 Process::kill(Process::pid());
1491 public function forceShutdown() : void{
1492 if($this->hasStopped){
1496 if($this->doTitleTick){
1500 if($this->isRunning){
1501 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_forcingShutdown()));
1504 if(!$this->isRunning()){
1505 $this->sendUsage(SendUsageTask::TYPE_CLOSE);
1508 $this->hasStopped =
true;
1512 if(isset($this->pluginManager)){
1513 $this->logger->debug(
"Disabling all plugins");
1514 $this->pluginManager->disablePlugins();
1517 if(isset($this->network)){
1518 $this->network->getSessionManager()->close($this->configGroup->getPropertyString(Yml::SETTINGS_SHUTDOWN_MESSAGE,
"Server closed"));
1521 if(isset($this->worldManager)){
1522 $this->logger->debug(
"Unloading all worlds");
1523 foreach($this->worldManager->getWorlds() as $world){
1524 $this->worldManager->unloadWorld($world,
true);
1528 $this->logger->debug(
"Removing event handlers");
1529 HandlerListManager::global()->unregisterAll();
1531 if(isset($this->asyncPool)){
1532 $this->logger->debug(
"Shutting down async task worker pool");
1533 $this->asyncPool->shutdown();
1536 if(isset($this->configGroup)){
1537 $this->logger->debug(
"Saving properties");
1538 $this->configGroup->save();
1541 if($this->console !==
null){
1542 $this->logger->debug(
"Closing console");
1543 $this->console->quit();
1546 if(isset($this->network)){
1547 $this->logger->debug(
"Stopping network interfaces");
1548 foreach($this->network->getInterfaces() as $interface){
1549 $this->logger->debug(
"Stopping network interface " . get_class($interface));
1550 $this->network->unregisterInterface($interface);
1553 }
catch(\Throwable $e){
1554 $this->logger->logException($e);
1555 $this->logger->emergency(
"Crashed while crashing, killing process");
1556 @Process::kill(Process::pid());
1561 public function getQueryInformation() : QueryInfo{
1562 return $this->queryInfo;
1570 while(@ob_end_flush()){}
1573 if($trace ===
null){
1574 $trace = $e->getTrace();
1580 $this->logger->logException($e, $trace);
1582 if($e instanceof ThreadCrashException){
1583 $info = $e->getCrashInfo();
1584 $type = $info->getType();
1585 $errstr = $info->getMessage();
1586 $errfile = $info->getFile();
1587 $errline = $info->getLine();
1588 $printableTrace = $info->getTrace();
1589 $thread = $info->getThreadName();
1591 $type = get_class($e);
1592 $errstr = $e->getMessage();
1593 $errfile = $e->getFile();
1594 $errline = $e->getLine();
1595 $printableTrace = Utils::printableTraceWithMetadata($trace);
1599 $errstr = preg_replace(
'/\s+/',
' ', trim($errstr));
1603 "message" => $errstr,
1604 "fullFile" => $errfile,
1605 "file" => Filesystem::cleanPath($errfile),
1607 "trace" => $printableTrace,
1611 global $lastExceptionError, $lastError;
1612 $lastExceptionError = $lastError;
1616 private function writeCrashDumpFile(CrashDump $dump) : string{
1617 $crashFolder = Path::join($this->dataPath,
"crashdumps");
1618 if(!is_dir($crashFolder)){
1619 mkdir($crashFolder);
1621 $crashDumpPath = Path::join($crashFolder, date(
"D_M_j-H.i.s-T_Y", (
int) $dump->getData()->time) .
".log");
1623 $fp = @fopen($crashDumpPath,
"wb");
1624 if(!is_resource($fp)){
1625 throw new \RuntimeException(
"Unable to open new file to generate crashdump");
1627 $writer =
new CrashDumpRenderer($fp, $dump->getData());
1628 $writer->renderHumanReadable();
1629 $dump->encodeData($writer);
1632 return $crashDumpPath;
1635 public function crashDump() : void{
1636 while(@ob_end_flush()){}
1637 if(!$this->isRunning){
1640 if($this->sendUsageTicker > 0){
1641 $this->sendUsage(SendUsageTask::TYPE_CLOSE);
1643 $this->hasStopped =
false;
1645 ini_set(
"error_reporting",
'0');
1646 ini_set(
"memory_limit",
'-1');
1648 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_crash_create()));
1649 $dump =
new CrashDump($this, $this->pluginManager ??
null);
1651 $crashDumpPath = $this->writeCrashDumpFile($dump);
1653 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_crash_submit($crashDumpPath)));
1655 if($this->configGroup->getPropertyBool(Yml::AUTO_REPORT_ENABLED,
true)){
1658 $stamp = Path::join($this->dataPath,
"crashdumps",
".last_crash");
1659 $crashInterval = 120;
1660 if(($lastReportTime = @filemtime($stamp)) !==
false && $lastReportTime + $crashInterval >= time()){
1662 $this->logger->debug(
"Not sending crashdump due to last crash less than $crashInterval seconds ago");
1666 if($dump->getData()->error[
"type"] === \ParseError::class){
1670 if(strrpos(VersionInfo::GIT_HASH(),
"-dirty") !==
false || VersionInfo::GIT_HASH() === str_repeat(
"00", 20)){
1671 $this->logger->debug(
"Not sending crashdump due to locally modified");
1676 $url = ($this->configGroup->getPropertyBool(Yml::AUTO_REPORT_USE_HTTPS,
true) ?
"https" :
"http") .
"://" . $this->configGroup->getPropertyString(Yml::AUTO_REPORT_HOST,
"crash.pmmp.io") .
"/submit/api";
1677 $postUrlError =
"Unknown error";
1678 $reply = Internet::postURL($url, [
1680 "name" => $this->getName() .
" " . $this->getPocketMineVersion(),
1682 "reportPaste" => base64_encode($dump->getEncodedData())
1683 ], 10, [], $postUrlError);
1685 if($reply !==
null && is_object($data = json_decode($reply->getBody()))){
1686 if(isset($data->crashId) && is_int($data->crashId) && isset($data->crashUrl) && is_string($data->crashUrl)){
1687 $reportId = $data->crashId;
1688 $reportUrl = $data->crashUrl;
1689 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_crash_archive($reportUrl, (
string) $reportId)));
1690 }elseif(isset($data->error) && is_string($data->error)){
1691 $this->logger->emergency(
"Automatic crash report submission failed: $data->error");
1693 $this->logger->emergency(
"Invalid JSON response received from crash archive: " . $reply->getBody());
1696 $this->logger->emergency(
"Failed to communicate with crash archive: $postUrlError");
1700 }
catch(\Throwable $e){
1701 $this->logger->logException($e);
1703 $this->logger->critical($this->language->translate(KnownTranslationFactory::pocketmine_crash_error($e->getMessage())));
1704 }
catch(\Throwable $e){}
1707 $this->forceShutdown();
1708 $this->isRunning =
false;
1711 $uptime = time() - ((int) $this->startTime);
1713 $spacing = $minUptime - $uptime;
1715 echo
"--- Uptime {$uptime}s - waiting {$spacing}s to throttle automatic restart (you can kill the process safely now) ---" . PHP_EOL;
1718 @Process::kill(Process::pid());
1730 return $this->tickSleeper;
1733 private function tickProcessor() : void{
1734 $this->nextTick = microtime(true);
1736 while($this->isRunning){
1740 $this->tickSleeper->sleepUntil($this->nextTick);
1744 public function addOnlinePlayer(Player $player) : bool{
1745 $ev = new PlayerLoginEvent($player,
"Plugin reason");
1747 if($ev->isCancelled() || !$player->isConnected()){
1748 $player->disconnect($ev->getKickMessage());
1753 $session = $player->getNetworkSession();
1754 $position = $player->getPosition();
1755 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_player_logIn(
1756 TextFormat::AQUA . $player->getName() . TextFormat::RESET,
1758 (
string) $session->getPort(),
1759 (
string) $player->getId(),
1760 $position->getWorld()->getDisplayName(),
1761 (
string) round($position->x, 4),
1762 (
string) round($position->y, 4),
1763 (
string) round($position->z, 4)
1766 foreach($this->playerList as $p){
1767 $p->getNetworkSession()->onPlayerAdded($player);
1769 $rawUUID = $player->getUniqueId()->getBytes();
1770 $this->playerList[$rawUUID] = $player;
1772 if($this->sendUsageTicker > 0){
1773 $this->uniquePlayers[$rawUUID] = $rawUUID;
1779 public function removeOnlinePlayer(Player $player) : void{
1780 if(isset($this->playerList[$rawUUID = $player->getUniqueId()->getBytes()])){
1781 unset($this->playerList[$rawUUID]);
1782 foreach($this->playerList as $p){
1783 $p->getNetworkSession()->onPlayerRemoved($player);
1788 public function sendUsage(
int $type = SendUsageTask::TYPE_STATUS) : void{
1789 if($this->configGroup->getPropertyBool(Yml::ANONYMOUS_STATISTICS_ENABLED, true)){
1790 $this->asyncPool->submitTask(
new SendUsageTask($this, $type, $this->uniquePlayers));
1792 $this->uniquePlayers = [];
1795 public function getLanguage() : Language{
1796 return $this->language;
1799 public function isLanguageForced() : bool{
1800 return $this->forceLanguage;
1803 public function getNetwork() : Network{
1804 return $this->network;
1807 public function getMemoryManager() : MemoryManager{
1808 return $this->memoryManager;
1811 private function titleTick() : void{
1812 Timings::$titleTick->startTiming();
1814 $u = Process::getAdvancedMemoryUsage();
1815 $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());
1817 $online = count($this->playerList);
1818 $connecting = $this->network->getConnectionCount() - $online;
1819 $bandwidthStats = $this->network->getBandwidthTracker();
1821 echo
"\x1b]0;" . $this->getName() .
" " .
1822 $this->getPocketMineVersion() .
1823 " | Online $online/" . $this->maxPlayers .
1824 ($connecting > 0 ?
" (+$connecting connecting)" :
"") .
1825 " | Memory " . $usage .
1826 " | U " . round($bandwidthStats->getSend()->getAverageBytes() / 1024, 2) .
1827 " D " . round($bandwidthStats->getReceive()->getAverageBytes() / 1024, 2) .
1828 " kB/s | TPS " . $this->getTicksPerSecondAverage() .
1829 " | Load " . $this->getTickUsageAverage() .
"%\x07";
1831 Timings::$titleTick->stopTiming();
1837 private function tick() : void{
1838 $tickTime = microtime(true);
1839 if(($tickTime - $this->nextTick) < -0.025){
1843 Timings::$serverTick->startTiming();
1845 ++$this->tickCounter;
1847 Timings::$scheduler->startTiming();
1848 $this->pluginManager->tickSchedulers($this->tickCounter);
1849 Timings::$scheduler->stopTiming();
1851 Timings::$schedulerAsync->startTiming();
1852 $this->asyncPool->collectTasks();
1853 Timings::$schedulerAsync->stopTiming();
1855 $this->worldManager->tick($this->tickCounter);
1857 Timings::$connection->startTiming();
1858 $this->network->tick();
1859 Timings::$connection->stopTiming();
1861 if(($this->tickCounter % self::TARGET_TICKS_PER_SECOND) === 0){
1862 if($this->doTitleTick){
1865 $this->currentTPS = self::TARGET_TICKS_PER_SECOND;
1866 $this->currentUse = 0;
1868 $queryRegenerateEvent =
new QueryRegenerateEvent(
new QueryInfo($this));
1869 $queryRegenerateEvent->call();
1870 $this->queryInfo = $queryRegenerateEvent->getQueryInfo();
1872 $this->network->updateName();
1873 $this->network->getBandwidthTracker()->rotateAverageHistory();
1876 if($this->sendUsageTicker > 0 && --$this->sendUsageTicker === 0){
1877 $this->sendUsageTicker = self::TICKS_PER_STATS_REPORT;
1878 $this->sendUsage(SendUsageTask::TYPE_STATUS);
1881 if(($this->tickCounter % self::TICKS_PER_WORLD_CACHE_CLEAR) === 0){
1882 foreach($this->worldManager->getWorlds() as $world){
1883 $world->clearCache();
1887 if(($this->tickCounter % self::TICKS_PER_TPS_OVERLOAD_WARNING) === 0 && $this->getTicksPerSecondAverage() < self::TPS_OVERLOAD_WARNING_THRESHOLD){
1888 $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_tickOverload()));
1891 $this->memoryManager->check();
1893 if($this->console !==
null){
1894 Timings::$serverCommand->startTiming();
1895 while(($line = $this->console->readLine()) !==
null){
1896 $this->consoleSender ??=
new ConsoleCommandSender($this, $this->language);
1897 $this->dispatchCommand($this->consoleSender, $line);
1899 Timings::$serverCommand->stopTiming();
1902 Timings::$serverTick->stopTiming();
1904 $now = microtime(
true);
1905 $totalTickTimeSeconds = $now - $tickTime + ($this->tickSleeper->getNotificationProcessingTime() / 1_000_000_000);
1906 $this->currentTPS = min(self::TARGET_TICKS_PER_SECOND, 1 / max(0.001, $totalTickTimeSeconds));
1907 $this->currentUse = min(1, $totalTickTimeSeconds / self::TARGET_SECONDS_PER_TICK);
1909 TimingsHandler::tick($this->currentTPS <= $this->profilingTickRate);
1911 $idx = $this->tickCounter % self::TARGET_TICKS_PER_SECOND;
1912 $this->tickAverage[$idx] = $this->currentTPS;
1913 $this->useAverage[$idx] = $this->currentUse;
1914 $this->tickSleeper->resetNotificationProcessingTime();
1916 if(($this->nextTick - $tickTime) < -1){
1917 $this->nextTick = $tickTime;
1919 $this->nextTick += self::TARGET_SECONDS_PER_TICK;