PocketMine-MP 5.23.3 git-f7687af337d001ddbcc47b8e773f014a33faa662
Loading...
Searching...
No Matches
src/Server.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
28namespace pocketmine;
29
75use pocketmine\player\GameMode;
108use pocketmine\utils\NotCloneable;
109use pocketmine\utils\NotSerializable;
125use Ramsey\Uuid\UuidInterface;
126use Symfony\Component\Filesystem\Path;
127use function array_fill;
128use function array_sum;
129use function base64_encode;
130use function chr;
131use function cli_set_process_title;
132use function copy;
133use function count;
134use function date;
135use function fclose;
136use function file_exists;
137use function file_put_contents;
138use function filemtime;
139use function fopen;
140use function get_class;
141use function ini_set;
142use function is_array;
143use function is_dir;
144use function is_int;
145use function is_object;
146use function is_resource;
147use function is_string;
148use function json_decode;
149use function max;
150use function microtime;
151use function min;
152use function mkdir;
153use function ob_end_flush;
154use function preg_replace;
155use function realpath;
156use function register_shutdown_function;
157use function rename;
158use function round;
159use function sleep;
160use function spl_object_id;
161use function sprintf;
162use function str_repeat;
163use function str_replace;
164use function stripos;
165use function strlen;
166use function strrpos;
167use function strtolower;
168use function strval;
169use function time;
170use function touch;
171use function trim;
172use function yaml_parse;
173use const DIRECTORY_SEPARATOR;
174use const PHP_EOL;
175use const PHP_INT_MAX;
176
180class Server{
181 use NotCloneable;
182 use NotSerializable;
183
184 public const BROADCAST_CHANNEL_ADMINISTRATIVE = "pocketmine.broadcast.admin";
185 public const BROADCAST_CHANNEL_USERS = "pocketmine.broadcast.user";
186
187 public const DEFAULT_SERVER_NAME = VersionInfo::NAME . " Server";
188 public const DEFAULT_MAX_PLAYERS = 20;
189 public const DEFAULT_PORT_IPV4 = 19132;
190 public const DEFAULT_PORT_IPV6 = 19133;
191 public const DEFAULT_MAX_VIEW_DISTANCE = 16;
192
198 public const TARGET_TICKS_PER_SECOND = 20;
202 public const TARGET_SECONDS_PER_TICK = 1 / self::TARGET_TICKS_PER_SECOND;
203 public const TARGET_NANOSECONDS_PER_TICK = 1_000_000_000 / self::TARGET_TICKS_PER_SECOND;
204
208 private const TPS_OVERLOAD_WARNING_THRESHOLD = self::TARGET_TICKS_PER_SECOND * 0.6;
209
210 private const TICKS_PER_WORLD_CACHE_CLEAR = 5 * self::TARGET_TICKS_PER_SECOND;
211 private const TICKS_PER_TPS_OVERLOAD_WARNING = 5 * self::TARGET_TICKS_PER_SECOND;
212 private const TICKS_PER_STATS_REPORT = 300 * self::TARGET_TICKS_PER_SECOND;
213
214 private const DEFAULT_ASYNC_COMPRESSION_THRESHOLD = 10_000;
215
216 private static ?Server $instance = null;
217
218 private TimeTrackingSleeperHandler $tickSleeper;
219
220 private BanList $banByName;
221
222 private BanList $banByIP;
223
224 private Config $operators;
225
226 private Config $whitelist;
227
228 private bool $isRunning = true;
229
230 private bool $hasStopped = false;
231
232 private PluginManager $pluginManager;
233
234 private float $profilingTickRate = self::TARGET_TICKS_PER_SECOND;
235
236 private UpdateChecker $updater;
237
238 private AsyncPool $asyncPool;
239
241 private int $tickCounter = 0;
242 private float $nextTick = 0;
244 private array $tickAverage;
246 private array $useAverage;
247 private float $currentTPS = self::TARGET_TICKS_PER_SECOND;
248 private float $currentUse = 0;
249 private float $startTime;
250
251 private bool $doTitleTick = true;
252
253 private int $sendUsageTicker = 0;
254
255 private MemoryManager $memoryManager;
256
257 private ?ConsoleReaderChildProcessDaemon $console = null;
258 private ?ConsoleCommandSender $consoleSender = null;
259
260 private SimpleCommandMap $commandMap;
261
262 private CraftingManager $craftingManager;
263
264 private ResourcePackManager $resourceManager;
265
266 private WorldManager $worldManager;
267
268 private int $maxPlayers;
269
270 private bool $onlineMode = true;
271
272 private Network $network;
273 private bool $networkCompressionAsync = true;
274 private int $networkCompressionAsyncThreshold = self::DEFAULT_ASYNC_COMPRESSION_THRESHOLD;
275
276 private Language $language;
277 private bool $forceLanguage = false;
278
279 private UuidInterface $serverID;
280
281 private string $dataPath;
282 private string $pluginPath;
283
284 private PlayerDataProvider $playerDataProvider;
285
290 private array $uniquePlayers = [];
291
292 private QueryInfo $queryInfo;
293
294 private ServerConfigGroup $configGroup;
295
297 private array $playerList = [];
298
299 private SignalHandler $signalHandler;
300
305 private array $broadcastSubscribers = [];
306
307 public function getName() : string{
308 return VersionInfo::NAME;
309 }
310
311 public function isRunning() : bool{
312 return $this->isRunning;
313 }
314
315 public function getPocketMineVersion() : string{
316 return VersionInfo::VERSION()->getFullVersion(true);
317 }
318
319 public function getVersion() : string{
320 return ProtocolInfo::MINECRAFT_VERSION;
321 }
322
323 public function getApiVersion() : string{
324 return VersionInfo::BASE_VERSION;
325 }
326
327 public function getFilePath() : string{
328 return \pocketmine\PATH;
329 }
330
331 public function getResourcePath() : string{
332 return \pocketmine\RESOURCE_PATH;
333 }
334
335 public function getDataPath() : string{
336 return $this->dataPath;
337 }
338
339 public function getPluginPath() : string{
340 return $this->pluginPath;
341 }
342
343 public function getMaxPlayers() : int{
344 return $this->maxPlayers;
345 }
346
351 public function getOnlineMode() : bool{
352 return $this->onlineMode;
353 }
354
358 public function requiresAuthentication() : bool{
359 return $this->getOnlineMode();
360 }
361
362 public function getPort() : int{
363 return $this->configGroup->getConfigInt(ServerProperties::SERVER_PORT_IPV4, self::DEFAULT_PORT_IPV4);
364 }
365
366 public function getPortV6() : int{
367 return $this->configGroup->getConfigInt(ServerProperties::SERVER_PORT_IPV6, self::DEFAULT_PORT_IPV6);
368 }
369
370 public function getViewDistance() : int{
371 return max(2, $this->configGroup->getConfigInt(ServerProperties::VIEW_DISTANCE, self::DEFAULT_MAX_VIEW_DISTANCE));
372 }
373
377 public function getAllowedViewDistance(int $distance) : int{
378 return max(2, min($distance, $this->memoryManager->getViewDistance($this->getViewDistance())));
379 }
380
381 public function getIp() : string{
382 $str = $this->configGroup->getConfigString(ServerProperties::SERVER_IPV4);
383 return $str !== "" ? $str : "0.0.0.0";
384 }
385
386 public function getIpV6() : string{
387 $str = $this->configGroup->getConfigString(ServerProperties::SERVER_IPV6);
388 return $str !== "" ? $str : "::";
389 }
390
391 public function getServerUniqueId() : UuidInterface{
392 return $this->serverID;
393 }
394
395 public function getGamemode() : GameMode{
396 return GameMode::fromString($this->configGroup->getConfigString(ServerProperties::GAME_MODE)) ?? GameMode::SURVIVAL;
397 }
398
399 public function getForceGamemode() : bool{
400 return $this->configGroup->getConfigBool(ServerProperties::FORCE_GAME_MODE, false);
401 }
402
406 public function getDifficulty() : int{
407 return $this->configGroup->getConfigInt(ServerProperties::DIFFICULTY, World::DIFFICULTY_NORMAL);
408 }
409
410 public function hasWhitelist() : bool{
411 return $this->configGroup->getConfigBool(ServerProperties::WHITELIST, false);
412 }
413
414 public function isHardcore() : bool{
415 return $this->configGroup->getConfigBool(ServerProperties::HARDCORE, false);
416 }
417
418 public function getMotd() : string{
419 return $this->configGroup->getConfigString(ServerProperties::MOTD, self::DEFAULT_SERVER_NAME);
420 }
421
422 public function getLoader() : ThreadSafeClassLoader{
423 return $this->autoloader;
424 }
425
426 public function getLogger() : AttachableThreadSafeLogger{
427 return $this->logger;
428 }
429
430 public function getUpdater() : UpdateChecker{
431 return $this->updater;
432 }
433
434 public function getPluginManager() : PluginManager{
435 return $this->pluginManager;
436 }
437
438 public function getCraftingManager() : CraftingManager{
439 return $this->craftingManager;
440 }
441
442 public function getResourcePackManager() : ResourcePackManager{
443 return $this->resourceManager;
444 }
445
446 public function getWorldManager() : WorldManager{
447 return $this->worldManager;
448 }
449
450 public function getAsyncPool() : AsyncPool{
451 return $this->asyncPool;
452 }
453
454 public function getTick() : int{
455 return $this->tickCounter;
456 }
457
461 public function getTicksPerSecond() : float{
462 return round($this->currentTPS, 2);
463 }
464
468 public function getTicksPerSecondAverage() : float{
469 return round(array_sum($this->tickAverage) / count($this->tickAverage), 2);
470 }
471
475 public function getTickUsage() : float{
476 return round($this->currentUse * 100, 2);
477 }
478
482 public function getTickUsageAverage() : float{
483 return round((array_sum($this->useAverage) / count($this->useAverage)) * 100, 2);
484 }
485
486 public function getStartTime() : float{
487 return $this->startTime;
488 }
489
490 public function getCommandMap() : SimpleCommandMap{
491 return $this->commandMap;
492 }
493
497 public function getOnlinePlayers() : array{
498 return $this->playerList;
499 }
500
501 public function shouldSavePlayerData() : bool{
502 return $this->configGroup->getPropertyBool(Yml::PLAYER_SAVE_PLAYER_DATA, true);
503 }
504
505 public function getOfflinePlayer(string $name) : Player|OfflinePlayer|null{
506 $name = strtolower($name);
507 $result = $this->getPlayerExact($name);
508
509 if($result === null){
510 $result = new OfflinePlayer($name, $this->getOfflinePlayerData($name));
511 }
512
513 return $result;
514 }
515
519 public function hasOfflinePlayerData(string $name) : bool{
520 return $this->playerDataProvider->hasData($name);
521 }
522
523 public function getOfflinePlayerData(string $name) : ?CompoundTag{
524 return Timings::$syncPlayerDataLoad->time(function() use ($name) : ?CompoundTag{
525 try{
526 return $this->playerDataProvider->loadData($name);
527 }catch(PlayerDataLoadException $e){
528 $this->logger->debug("Failed to load player data for $name: " . $e->getMessage());
529 $this->logger->error($this->language->translate(KnownTranslationFactory::pocketmine_data_playerCorrupted($name)));
530 return null;
531 }
532 });
533 }
534
535 public function saveOfflinePlayerData(string $name, CompoundTag $nbtTag) : void{
536 $ev = new PlayerDataSaveEvent($nbtTag, $name, $this->getPlayerExact($name));
537 if(!$this->shouldSavePlayerData()){
538 $ev->cancel();
539 }
540
541 $ev->call();
542
543 if(!$ev->isCancelled()){
544 Timings::$syncPlayerDataSave->time(function() use ($name, $ev) : void{
545 try{
546 $this->playerDataProvider->saveData($name, $ev->getSaveData());
547 }catch(PlayerDataSaveException $e){
548 $this->logger->critical($this->language->translate(KnownTranslationFactory::pocketmine_data_saveError($name, $e->getMessage())));
549 $this->logger->logException($e);
550 }
551 });
552 }
553 }
554
558 public function createPlayer(NetworkSession $session, PlayerInfo $playerInfo, bool $authenticated, ?CompoundTag $offlinePlayerData) : Promise{
559 $ev = new PlayerCreationEvent($session);
560 $ev->call();
561 $class = $ev->getPlayerClass();
562
563 if($offlinePlayerData !== null && ($world = $this->worldManager->getWorldByName($offlinePlayerData->getString(Player::TAG_LEVEL, ""))) !== null){
564 $playerPos = EntityDataHelper::parseLocation($offlinePlayerData, $world);
565 }else{
566 $world = $this->worldManager->getDefaultWorld();
567 if($world === null){
568 throw new AssumptionFailedError("Default world should always be loaded");
569 }
570 $playerPos = null;
571 }
573 $playerPromiseResolver = new PromiseResolver();
574
575 $createPlayer = function(Location $location) use ($playerPromiseResolver, $class, $session, $playerInfo, $authenticated, $offlinePlayerData) : void{
577 $player = new $class($this, $session, $playerInfo, $authenticated, $location, $offlinePlayerData);
578 if(!$player->hasPlayedBefore()){
579 $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
580 }
581 $playerPromiseResolver->resolve($player);
582 };
583
584 if($playerPos === null){ //new player or no valid position due to world not being loaded
585 $world->requestSafeSpawn()->onCompletion(
586 function(Position $spawn) use ($createPlayer, $playerPromiseResolver, $session, $world) : void{
587 if(!$session->isConnected()){
588 $playerPromiseResolver->reject();
589 return;
590 }
591 $createPlayer(Location::fromObject($spawn, $world));
592 },
593 function() use ($playerPromiseResolver, $session) : void{
594 if($session->isConnected()){
595 $session->disconnectWithError(KnownTranslationFactory::pocketmine_disconnect_error_respawn());
596 }
597 $playerPromiseResolver->reject();
598 }
599 );
600 }else{ //returning player with a valid position - safe spawn not required
601 $createPlayer($playerPos);
602 }
603
604 return $playerPromiseResolver->getPromise();
605 }
606
617 public function getPlayerByPrefix(string $name) : ?Player{
618 $found = null;
619 $name = strtolower($name);
620 $delta = PHP_INT_MAX;
621 foreach($this->getOnlinePlayers() as $player){
622 if(stripos($player->getName(), $name) === 0){
623 $curDelta = strlen($player->getName()) - strlen($name);
624 if($curDelta < $delta){
625 $found = $player;
626 $delta = $curDelta;
627 }
628 if($curDelta === 0){
629 break;
630 }
631 }
632 }
633
634 return $found;
635 }
636
640 public function getPlayerExact(string $name) : ?Player{
641 $name = strtolower($name);
642 foreach($this->getOnlinePlayers() as $player){
643 if(strtolower($player->getName()) === $name){
644 return $player;
645 }
646 }
647
648 return null;
649 }
650
654 public function getPlayerByRawUUID(string $rawUUID) : ?Player{
655 return $this->playerList[$rawUUID] ?? null;
656 }
657
661 public function getPlayerByUUID(UuidInterface $uuid) : ?Player{
662 return $this->getPlayerByRawUUID($uuid->getBytes());
663 }
664
665 public function getConfigGroup() : ServerConfigGroup{
666 return $this->configGroup;
667 }
668
673 public function getPluginCommand(string $name){
674 if(($command = $this->commandMap->getCommand($name)) instanceof PluginOwned){
675 return $command;
676 }else{
677 return null;
678 }
679 }
680
681 public function getNameBans() : BanList{
682 return $this->banByName;
683 }
684
685 public function getIPBans() : BanList{
686 return $this->banByIP;
687 }
688
689 public function addOp(string $name) : void{
690 $this->operators->set(strtolower($name), true);
691
692 if(($player = $this->getPlayerExact($name)) !== null){
693 $player->setBasePermission(DefaultPermissions::ROOT_OPERATOR, true);
694 }
695 $this->operators->save();
696 }
697
698 public function removeOp(string $name) : void{
699 $lowercaseName = strtolower($name);
700 foreach($this->operators->getAll() as $operatorName => $_){
701 $operatorName = (string) $operatorName;
702 if($lowercaseName === strtolower($operatorName)){
703 $this->operators->remove($operatorName);
704 }
705 }
706
707 if(($player = $this->getPlayerExact($name)) !== null){
708 $player->unsetBasePermission(DefaultPermissions::ROOT_OPERATOR);
709 }
710 $this->operators->save();
711 }
712
713 public function addWhitelist(string $name) : void{
714 $this->whitelist->set(strtolower($name), true);
715 $this->whitelist->save();
716 }
717
718 public function removeWhitelist(string $name) : void{
719 $this->whitelist->remove(strtolower($name));
720 $this->whitelist->save();
721 }
722
723 public function isWhitelisted(string $name) : bool{
724 return !$this->hasWhitelist() || $this->operators->exists($name, true) || $this->whitelist->exists($name, true);
725 }
726
727 public function isOp(string $name) : bool{
728 return $this->operators->exists($name, true);
729 }
730
731 public function getWhitelisted() : Config{
732 return $this->whitelist;
733 }
734
735 public function getOps() : Config{
736 return $this->operators;
737 }
738
743 public function getCommandAliases() : array{
744 $section = $this->configGroup->getProperty(Yml::ALIASES);
745 $result = [];
746 if(is_array($section)){
747 foreach(Utils::promoteKeys($section) as $key => $value){
748 //TODO: more validation needed here
749 //key might not be a string, value might not be list<string>
750 $commands = [];
751 if(is_array($value)){
752 $commands = $value;
753 }else{
754 $commands[] = (string) $value;
755 }
756
757 $result[(string) $key] = $commands;
758 }
759 }
760
761 return $result;
762 }
763
764 public static function getInstance() : Server{
765 if(self::$instance === null){
766 throw new \RuntimeException("Attempt to retrieve Server instance outside server thread");
767 }
768 return self::$instance;
769 }
770
771 public function __construct(
772 private ThreadSafeClassLoader $autoloader,
773 private AttachableThreadSafeLogger $logger,
774 string $dataPath,
775 string $pluginPath
776 ){
777 if(self::$instance !== null){
778 throw new \LogicException("Only one server instance can exist at once");
779 }
780 self::$instance = $this;
781 $this->startTime = microtime(true);
782 $this->tickAverage = array_fill(0, self::TARGET_TICKS_PER_SECOND, self::TARGET_TICKS_PER_SECOND);
783 $this->useAverage = array_fill(0, self::TARGET_TICKS_PER_SECOND, 0);
784
785 Timings::init();
786 $this->tickSleeper = new TimeTrackingSleeperHandler(Timings::$serverInterrupts);
787
788 $this->signalHandler = new SignalHandler(function() : void{
789 $this->logger->info("Received signal interrupt, stopping the server");
790 $this->shutdown();
791 });
792
793 try{
794 foreach([
795 $dataPath,
796 $pluginPath,
797 Path::join($dataPath, "worlds"),
798 Path::join($dataPath, "players")
799 ] as $neededPath){
800 if(!file_exists($neededPath)){
801 mkdir($neededPath, 0777);
802 }
803 }
804
805 $this->dataPath = realpath($dataPath) . DIRECTORY_SEPARATOR;
806 $this->pluginPath = realpath($pluginPath) . DIRECTORY_SEPARATOR;
807
808 $this->logger->info("Loading server configuration");
809 $pocketmineYmlPath = Path::join($this->dataPath, "pocketmine.yml");
810 if(!file_exists($pocketmineYmlPath)){
811 $content = Filesystem::fileGetContents(Path::join(\pocketmine\RESOURCE_PATH, "pocketmine.yml"));
812 if(VersionInfo::IS_DEVELOPMENT_BUILD){
813 $content = str_replace("preferred-channel: stable", "preferred-channel: beta", $content);
814 }
815 @file_put_contents($pocketmineYmlPath, $content);
816 }
817
818 $this->configGroup = new ServerConfigGroup(
819 new Config($pocketmineYmlPath, Config::YAML, []),
820 new Config(Path::join($this->dataPath, "server.properties"), Config::PROPERTIES, [
821 ServerProperties::MOTD => self::DEFAULT_SERVER_NAME,
822 ServerProperties::SERVER_PORT_IPV4 => self::DEFAULT_PORT_IPV4,
823 ServerProperties::SERVER_PORT_IPV6 => self::DEFAULT_PORT_IPV6,
824 ServerProperties::ENABLE_IPV6 => true,
825 ServerProperties::WHITELIST => false,
826 ServerProperties::MAX_PLAYERS => self::DEFAULT_MAX_PLAYERS,
827 ServerProperties::GAME_MODE => GameMode::SURVIVAL->name, //TODO: this probably shouldn't use the enum name directly
828 ServerProperties::FORCE_GAME_MODE => false,
829 ServerProperties::HARDCORE => false,
830 ServerProperties::PVP => true,
831 ServerProperties::DIFFICULTY => World::DIFFICULTY_NORMAL,
832 ServerProperties::DEFAULT_WORLD_GENERATOR_SETTINGS => "",
833 ServerProperties::DEFAULT_WORLD_NAME => "world",
834 ServerProperties::DEFAULT_WORLD_SEED => "",
835 ServerProperties::DEFAULT_WORLD_GENERATOR => "DEFAULT",
836 ServerProperties::ENABLE_QUERY => true,
837 ServerProperties::AUTO_SAVE => true,
838 ServerProperties::VIEW_DISTANCE => self::DEFAULT_MAX_VIEW_DISTANCE,
839 ServerProperties::XBOX_AUTH => true,
840 ServerProperties::LANGUAGE => "eng"
841 ])
842 );
843
844 $debugLogLevel = $this->configGroup->getPropertyInt(Yml::DEBUG_LEVEL, 1);
845 if($this->logger instanceof MainLogger){
846 $this->logger->setLogDebug($debugLogLevel > 1);
847 }
848
849 $this->forceLanguage = $this->configGroup->getPropertyBool(Yml::SETTINGS_FORCE_LANGUAGE, false);
850 $selectedLang = $this->configGroup->getConfigString(ServerProperties::LANGUAGE, $this->configGroup->getPropertyString("settings.language", Language::FALLBACK_LANGUAGE));
851 try{
852 $this->language = new Language($selectedLang);
853 }catch(LanguageNotFoundException $e){
854 $this->logger->error($e->getMessage());
855 try{
856 $this->language = new Language(Language::FALLBACK_LANGUAGE);
857 }catch(LanguageNotFoundException $e){
858 $this->logger->emergency("Fallback language \"" . Language::FALLBACK_LANGUAGE . "\" not found");
859 return;
860 }
861 }
862
863 $this->logger->info($this->language->translate(KnownTranslationFactory::language_selected($this->language->getName(), $this->language->getLang())));
864
865 if(VersionInfo::IS_DEVELOPMENT_BUILD){
866 if(!$this->configGroup->getPropertyBool(Yml::SETTINGS_ENABLE_DEV_BUILDS, false)){
867 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error1(VersionInfo::NAME)));
868 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error2()));
869 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error3()));
870 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error4(Yml::SETTINGS_ENABLE_DEV_BUILDS)));
871 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error5("https://github.com/pmmp/PocketMine-MP/releases")));
872 $this->forceShutdownExit();
873
874 return;
875 }
876
877 $this->logger->warning(str_repeat("-", 40));
878 $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_warning1(VersionInfo::NAME)));
879 $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_warning2()));
880 $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_warning3()));
881 $this->logger->warning(str_repeat("-", 40));
882 }
883
884 $this->memoryManager = new MemoryManager($this);
885
886 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_start(TextFormat::AQUA . $this->getVersion() . TextFormat::RESET)));
887
888 if(($poolSize = $this->configGroup->getPropertyString(Yml::SETTINGS_ASYNC_WORKERS, "auto")) === "auto"){
889 $poolSize = 2;
890 $processors = Utils::getCoreCount() - 2;
891
892 if($processors > 0){
893 $poolSize = max(1, $processors);
894 }
895 }else{
896 $poolSize = max(1, (int) $poolSize);
897 }
898
899 TimingsHandler::setEnabled($this->configGroup->getPropertyBool(Yml::SETTINGS_ENABLE_PROFILING, false));
900 $this->profilingTickRate = $this->configGroup->getPropertyInt(Yml::SETTINGS_PROFILE_REPORT_TRIGGER, self::TARGET_TICKS_PER_SECOND);
901
902 $this->asyncPool = new AsyncPool($poolSize, max(-1, $this->configGroup->getPropertyInt(Yml::MEMORY_ASYNC_WORKER_HARD_LIMIT, 256)), $this->autoloader, $this->logger, $this->tickSleeper);
903 $this->asyncPool->addWorkerStartHook(function(int $i) : void{
904 if(TimingsHandler::isEnabled()){
905 $this->asyncPool->submitTaskToWorker(TimingsControlTask::setEnabled(true), $i);
906 }
907 });
908 TimingsHandler::getToggleCallbacks()->add(function(bool $enable) : void{
909 foreach($this->asyncPool->getRunningWorkers() as $workerId){
910 $this->asyncPool->submitTaskToWorker(TimingsControlTask::setEnabled($enable), $workerId);
911 }
912 });
913 TimingsHandler::getReloadCallbacks()->add(function() : void{
914 foreach($this->asyncPool->getRunningWorkers() as $workerId){
915 $this->asyncPool->submitTaskToWorker(TimingsControlTask::reload(), $workerId);
916 }
917 });
918 TimingsHandler::getCollectCallbacks()->add(function() : array{
919 $promises = [];
920 foreach($this->asyncPool->getRunningWorkers() as $workerId){
921 $resolver = new PromiseResolver();
922 $this->asyncPool->submitTaskToWorker(new TimingsCollectionTask($resolver), $workerId);
923
924 $promises[] = $resolver->getPromise();
925 }
926
927 return $promises;
928 });
929
930 $netCompressionThreshold = -1;
931 if($this->configGroup->getPropertyInt(Yml::NETWORK_BATCH_THRESHOLD, 256) >= 0){
932 $netCompressionThreshold = $this->configGroup->getPropertyInt(Yml::NETWORK_BATCH_THRESHOLD, 256);
933 }
934 if($netCompressionThreshold < 0){
935 $netCompressionThreshold = null;
936 }
937
938 $netCompressionLevel = $this->configGroup->getPropertyInt(Yml::NETWORK_COMPRESSION_LEVEL, 6);
939 if($netCompressionLevel < 1 || $netCompressionLevel > 9){
940 $this->logger->warning("Invalid network compression level $netCompressionLevel set, setting to default 6");
941 $netCompressionLevel = 6;
942 }
943 ZlibCompressor::setInstance(new ZlibCompressor($netCompressionLevel, $netCompressionThreshold, ZlibCompressor::DEFAULT_MAX_DECOMPRESSION_SIZE));
944
945 $this->networkCompressionAsync = $this->configGroup->getPropertyBool(Yml::NETWORK_ASYNC_COMPRESSION, true);
946 $this->networkCompressionAsyncThreshold = max(
947 $this->configGroup->getPropertyInt(Yml::NETWORK_ASYNC_COMPRESSION_THRESHOLD, self::DEFAULT_ASYNC_COMPRESSION_THRESHOLD),
948 $netCompressionThreshold ?? self::DEFAULT_ASYNC_COMPRESSION_THRESHOLD
949 );
950
951 EncryptionContext::$ENABLED = $this->configGroup->getPropertyBool(Yml::NETWORK_ENABLE_ENCRYPTION, true);
952
953 $this->doTitleTick = $this->configGroup->getPropertyBool(Yml::CONSOLE_TITLE_TICK, true) && Terminal::hasFormattingCodes();
954
955 $this->operators = new Config(Path::join($this->dataPath, "ops.txt"), Config::ENUM);
956 $this->whitelist = new Config(Path::join($this->dataPath, "white-list.txt"), Config::ENUM);
957
958 $bannedTxt = Path::join($this->dataPath, "banned.txt");
959 $bannedPlayersTxt = Path::join($this->dataPath, "banned-players.txt");
960 if(file_exists($bannedTxt) && !file_exists($bannedPlayersTxt)){
961 @rename($bannedTxt, $bannedPlayersTxt);
962 }
963 @touch($bannedPlayersTxt);
964 $this->banByName = new BanList($bannedPlayersTxt);
965 $this->banByName->load();
966 $bannedIpsTxt = Path::join($this->dataPath, "banned-ips.txt");
967 @touch($bannedIpsTxt);
968 $this->banByIP = new BanList($bannedIpsTxt);
969 $this->banByIP->load();
970
971 $this->maxPlayers = $this->configGroup->getConfigInt(ServerProperties::MAX_PLAYERS, self::DEFAULT_MAX_PLAYERS);
972
973 $this->onlineMode = $this->configGroup->getConfigBool(ServerProperties::XBOX_AUTH, true);
974 if($this->onlineMode){
975 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_auth_enabled()));
976 }else{
977 $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_auth_disabled()));
978 $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_authWarning()));
979 $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_authProperty_disabled()));
980 }
981
982 if($this->configGroup->getConfigBool(ServerProperties::HARDCORE, false) && $this->getDifficulty() < World::DIFFICULTY_HARD){
983 $this->configGroup->setConfigInt(ServerProperties::DIFFICULTY, World::DIFFICULTY_HARD);
984 }
985
986 @cli_set_process_title($this->getName() . " " . $this->getPocketMineVersion());
987
988 $this->serverID = Utils::getMachineUniqueId($this->getIp() . $this->getPort());
989
990 $this->logger->debug("Server unique id: " . $this->getServerUniqueId());
991 $this->logger->debug("Machine unique id: " . Utils::getMachineUniqueId());
992
993 $this->network = new Network($this->logger);
994 $this->network->setName($this->getMotd());
995
996 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_info(
997 $this->getName(),
998 (VersionInfo::IS_DEVELOPMENT_BUILD ? TextFormat::YELLOW : "") . $this->getPocketMineVersion() . TextFormat::RESET
999 )));
1000 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_license($this->getName())));
1001
1002 DefaultPermissions::registerCorePermissions();
1003
1004 $this->commandMap = new SimpleCommandMap($this);
1005
1006 $this->craftingManager = CraftingManagerFromDataHelper::make(Path::join(\pocketmine\BEDROCK_DATA_PATH, "recipes"));
1007
1008 $this->resourceManager = new ResourcePackManager(Path::join($this->dataPath, "resource_packs"), $this->logger);
1009
1010 $pluginGraylist = null;
1011 $graylistFile = Path::join($this->dataPath, "plugin_list.yml");
1012 if(!file_exists($graylistFile)){
1013 copy(Path::join(\pocketmine\RESOURCE_PATH, 'plugin_list.yml'), $graylistFile);
1014 }
1015 try{
1016 $pluginGraylist = PluginGraylist::fromArray(yaml_parse(Filesystem::fileGetContents($graylistFile)));
1017 }catch(\InvalidArgumentException $e){
1018 $this->logger->emergency("Failed to load $graylistFile: " . $e->getMessage());
1019 $this->forceShutdownExit();
1020 return;
1021 }
1022 $this->pluginManager = new PluginManager($this, $this->configGroup->getPropertyBool(Yml::PLUGINS_LEGACY_DATA_DIR, true) ? null : Path::join($this->dataPath, "plugin_data"), $pluginGraylist);
1023 $this->pluginManager->registerInterface(new PharPluginLoader($this->autoloader));
1024 $this->pluginManager->registerInterface(new ScriptPluginLoader());
1025
1026 $providerManager = new WorldProviderManager();
1027 if(
1028 ($format = $providerManager->getProviderByName($formatName = $this->configGroup->getPropertyString(Yml::LEVEL_SETTINGS_DEFAULT_FORMAT, ""))) !== null &&
1029 $format instanceof WritableWorldProviderManagerEntry
1030 ){
1031 $providerManager->setDefault($format);
1032 }elseif($formatName !== ""){
1033 $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_level_badDefaultFormat($formatName)));
1034 }
1035
1036 $this->worldManager = new WorldManager($this, Path::join($this->dataPath, "worlds"), $providerManager);
1037 $this->worldManager->setAutoSave($this->configGroup->getConfigBool(ServerProperties::AUTO_SAVE, $this->worldManager->getAutoSave()));
1038 $this->worldManager->setAutoSaveInterval($this->configGroup->getPropertyInt(Yml::TICKS_PER_AUTOSAVE, $this->worldManager->getAutoSaveInterval()));
1039
1040 $this->updater = new UpdateChecker($this, $this->configGroup->getPropertyString(Yml::AUTO_UPDATER_HOST, "update.pmmp.io"));
1041
1042 $this->queryInfo = new QueryInfo($this);
1043
1044 $this->playerDataProvider = new DatFilePlayerDataProvider(Path::join($this->dataPath, "players"));
1045
1046 register_shutdown_function($this->crashDump(...));
1047
1048 $loadErrorCount = 0;
1049 $this->pluginManager->loadPlugins($this->pluginPath, $loadErrorCount);
1050 if($loadErrorCount > 0){
1051 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_plugin_someLoadErrors()));
1052 $this->forceShutdownExit();
1053 return;
1054 }
1055 if(!$this->enablePlugins(PluginEnableOrder::STARTUP)){
1056 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_plugin_someEnableErrors()));
1057 $this->forceShutdownExit();
1058 return;
1059 }
1060
1061 if(!$this->startupPrepareWorlds()){
1062 $this->forceShutdownExit();
1063 return;
1064 }
1065
1066 if(!$this->enablePlugins(PluginEnableOrder::POSTWORLD)){
1067 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_plugin_someEnableErrors()));
1068 $this->forceShutdownExit();
1069 return;
1070 }
1071
1072 if(!$this->startupPrepareNetworkInterfaces()){
1073 $this->forceShutdownExit();
1074 return;
1075 }
1076
1077 if($this->configGroup->getPropertyBool(Yml::ANONYMOUS_STATISTICS_ENABLED, true)){
1078 $this->sendUsageTicker = self::TICKS_PER_STATS_REPORT;
1079 $this->sendUsage(SendUsageTask::TYPE_OPEN);
1080 }
1081
1082 $this->configGroup->save();
1083
1084 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_defaultGameMode($this->getGamemode()->getTranslatableName())));
1085 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_donate(TextFormat::AQUA . "https://patreon.com/pocketminemp" . TextFormat::RESET)));
1086 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_startFinished(strval(round(microtime(true) - $this->startTime, 3)))));
1087
1088 $forwarder = new BroadcastLoggerForwarder($this, $this->logger, $this->language);
1089 $this->subscribeToBroadcastChannel(self::BROADCAST_CHANNEL_ADMINISTRATIVE, $forwarder);
1090 $this->subscribeToBroadcastChannel(self::BROADCAST_CHANNEL_USERS, $forwarder);
1091
1092 //TODO: move console parts to a separate component
1093 if($this->configGroup->getPropertyBool(Yml::CONSOLE_ENABLE_INPUT, true)){
1094 $this->console = new ConsoleReaderChildProcessDaemon($this->logger);
1095 }
1096
1097 $this->tickProcessor();
1098 $this->forceShutdown();
1099 }catch(\Throwable $e){
1100 $this->exceptionHandler($e);
1101 }
1102 }
1103
1104 private function startupPrepareWorlds() : bool{
1105 $getGenerator = function(string $generatorName, string $generatorOptions, string $worldName) : ?string{
1106 $generatorEntry = GeneratorManager::getInstance()->getGenerator($generatorName);
1107 if($generatorEntry === null){
1108 $this->logger->error($this->language->translate(KnownTranslationFactory::pocketmine_level_generationError(
1109 $worldName,
1110 KnownTranslationFactory::pocketmine_level_unknownGenerator($generatorName)
1111 )));
1112 return null;
1113 }
1114 try{
1115 $generatorEntry->validateGeneratorOptions($generatorOptions);
1116 }catch(InvalidGeneratorOptionsException $e){
1117 $this->logger->error($this->language->translate(KnownTranslationFactory::pocketmine_level_generationError(
1118 $worldName,
1119 KnownTranslationFactory::pocketmine_level_invalidGeneratorOptions($generatorOptions, $generatorName, $e->getMessage())
1120 )));
1121 return null;
1122 }
1123 return $generatorEntry->getGeneratorClass();
1124 };
1125
1126 $anyWorldFailedToLoad = false;
1127
1128 foreach(Utils::promoteKeys((array) $this->configGroup->getProperty(Yml::WORLDS, [])) as $name => $options){
1129 if(!is_string($name)){
1130 //TODO: this probably should be an error
1131 continue;
1132 }
1133 if($options === null){
1134 $options = [];
1135 }elseif(!is_array($options)){
1136 //TODO: this probably should be an error
1137 continue;
1138 }
1139 if(!$this->worldManager->loadWorld($name, true)){
1140 if($this->worldManager->isWorldGenerated($name)){
1141 //allow checking if other worlds are loadable, so the user gets all the errors in one go
1142 $anyWorldFailedToLoad = true;
1143 continue;
1144 }
1145 $creationOptions = WorldCreationOptions::create();
1146 //TODO: error checking
1147
1148 $generatorName = $options["generator"] ?? "default";
1149 $generatorOptions = isset($options["preset"]) && is_string($options["preset"]) ? $options["preset"] : "";
1150
1151 $generatorClass = $getGenerator($generatorName, $generatorOptions, $name);
1152 if($generatorClass === null){
1153 $anyWorldFailedToLoad = true;
1154 continue;
1155 }
1156 $creationOptions->setGeneratorClass($generatorClass);
1157 $creationOptions->setGeneratorOptions($generatorOptions);
1158
1159 $creationOptions->setDifficulty($this->getDifficulty());
1160 if(isset($options["difficulty"]) && is_string($options["difficulty"])){
1161 $creationOptions->setDifficulty(World::getDifficultyFromString($options["difficulty"]));
1162 }
1163
1164 if(isset($options["seed"])){
1165 $convertedSeed = Generator::convertSeed((string) ($options["seed"] ?? ""));
1166 if($convertedSeed !== null){
1167 $creationOptions->setSeed($convertedSeed);
1168 }
1169 }
1170
1171 $this->worldManager->generateWorld($name, $creationOptions);
1172 }
1173 }
1174
1175 if($this->worldManager->getDefaultWorld() === null){
1176 $default = $this->configGroup->getConfigString(ServerProperties::DEFAULT_WORLD_NAME, "world");
1177 if(trim($default) == ""){
1178 $this->logger->warning("level-name cannot be null, using default");
1179 $default = "world";
1180 $this->configGroup->setConfigString(ServerProperties::DEFAULT_WORLD_NAME, "world");
1181 }
1182 if(!$this->worldManager->loadWorld($default, true)){
1183 if($this->worldManager->isWorldGenerated($default)){
1184 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_level_defaultError()));
1185
1186 return false;
1187 }
1188 $generatorName = $this->configGroup->getConfigString(ServerProperties::DEFAULT_WORLD_GENERATOR);
1189 $generatorOptions = $this->configGroup->getConfigString(ServerProperties::DEFAULT_WORLD_GENERATOR_SETTINGS);
1190 $generatorClass = $getGenerator($generatorName, $generatorOptions, $default);
1191
1192 if($generatorClass === null){
1193 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_level_defaultError()));
1194 return false;
1195 }
1196 $creationOptions = WorldCreationOptions::create()
1197 ->setGeneratorClass($generatorClass)
1198 ->setGeneratorOptions($generatorOptions);
1199 $convertedSeed = Generator::convertSeed($this->configGroup->getConfigString(ServerProperties::DEFAULT_WORLD_SEED));
1200 if($convertedSeed !== null){
1201 $creationOptions->setSeed($convertedSeed);
1202 }
1203 $creationOptions->setDifficulty($this->getDifficulty());
1204 $this->worldManager->generateWorld($default, $creationOptions);
1205 }
1206
1207 $world = $this->worldManager->getWorldByName($default);
1208 if($world === null){
1209 throw new AssumptionFailedError("We just loaded/generated the default world, so it must exist");
1210 }
1211 $this->worldManager->setDefaultWorld($world);
1212 }
1213
1214 return !$anyWorldFailedToLoad;
1215 }
1216
1217 private function startupPrepareConnectableNetworkInterfaces(
1218 string $ip,
1219 int $port,
1220 bool $ipV6,
1221 bool $useQuery,
1222 PacketBroadcaster $packetBroadcaster,
1223 EntityEventBroadcaster $entityEventBroadcaster,
1224 TypeConverter $typeConverter
1225 ) : bool{
1226 $prettyIp = $ipV6 ? "[$ip]" : $ip;
1227 try{
1228 $rakLibRegistered = $this->network->registerInterface(new RakLibInterface($this, $ip, $port, $ipV6, $packetBroadcaster, $entityEventBroadcaster, $typeConverter));
1229 }catch(NetworkInterfaceStartException $e){
1230 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_networkStartFailed(
1231 $ip,
1232 (string) $port,
1233 $e->getMessage()
1234 )));
1235 return false;
1236 }
1237 if($rakLibRegistered){
1238 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_networkStart($prettyIp, (string) $port)));
1239 }
1240 if($useQuery){
1241 if(!$rakLibRegistered){
1242 //RakLib would normally handle the transport for Query packets
1243 //if it's not registered we need to make sure Query still works
1244 $this->network->registerInterface(new DedicatedQueryNetworkInterface($ip, $port, $ipV6, new \PrefixedLogger($this->logger, "Dedicated Query Interface")));
1245 }
1246 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_query_running($prettyIp, (string) $port)));
1247 }
1248 return true;
1249 }
1250
1251 private function startupPrepareNetworkInterfaces() : bool{
1252 $useQuery = $this->configGroup->getConfigBool(ServerProperties::ENABLE_QUERY, true);
1253
1254 $typeConverter = TypeConverter::getInstance();
1255 $packetBroadcaster = new StandardPacketBroadcaster($this);
1256 $entityEventBroadcaster = new StandardEntityEventBroadcaster($packetBroadcaster, $typeConverter);
1257
1258 if(
1259 !$this->startupPrepareConnectableNetworkInterfaces($this->getIp(), $this->getPort(), false, $useQuery, $packetBroadcaster, $entityEventBroadcaster, $typeConverter) ||
1260 (
1261 $this->configGroup->getConfigBool(ServerProperties::ENABLE_IPV6, true) &&
1262 !$this->startupPrepareConnectableNetworkInterfaces($this->getIpV6(), $this->getPortV6(), true, $useQuery, $packetBroadcaster, $entityEventBroadcaster, $typeConverter)
1263 )
1264 ){
1265 return false;
1266 }
1267
1268 if($useQuery){
1269 $this->network->registerRawPacketHandler(new QueryHandler($this));
1270 }
1271
1272 foreach($this->getIPBans()->getEntries() as $entry){
1273 $this->network->blockAddress($entry->getName(), -1);
1274 }
1275
1276 if($this->configGroup->getPropertyBool(Yml::NETWORK_UPNP_FORWARDING, false)){
1277 $this->network->registerInterface(new UPnPNetworkInterface($this->logger, Internet::getInternalIP(), $this->getPort()));
1278 }
1279
1280 return true;
1281 }
1282
1287 public function subscribeToBroadcastChannel(string $channelId, CommandSender $subscriber) : void{
1288 $this->broadcastSubscribers[$channelId][spl_object_id($subscriber)] = $subscriber;
1289 }
1290
1294 public function unsubscribeFromBroadcastChannel(string $channelId, CommandSender $subscriber) : void{
1295 if(isset($this->broadcastSubscribers[$channelId][spl_object_id($subscriber)])){
1296 if(count($this->broadcastSubscribers[$channelId]) === 1){
1297 unset($this->broadcastSubscribers[$channelId]);
1298 }else{
1299 unset($this->broadcastSubscribers[$channelId][spl_object_id($subscriber)]);
1300 }
1301 }
1302 }
1303
1307 public function unsubscribeFromAllBroadcastChannels(CommandSender $subscriber) : void{
1308 foreach(Utils::stringifyKeys($this->broadcastSubscribers) as $channelId => $recipients){
1309 $this->unsubscribeFromBroadcastChannel($channelId, $subscriber);
1310 }
1311 }
1312
1319 public function getBroadcastChannelSubscribers(string $channelId) : array{
1320 return $this->broadcastSubscribers[$channelId] ?? [];
1321 }
1322
1326 public function broadcastMessage(Translatable|string $message, ?array $recipients = null) : int{
1327 $recipients = $recipients ?? $this->getBroadcastChannelSubscribers(self::BROADCAST_CHANNEL_USERS);
1328
1329 foreach($recipients as $recipient){
1330 $recipient->sendMessage($message);
1331 }
1332
1333 return count($recipients);
1334 }
1335
1339 private function getPlayerBroadcastSubscribers(string $channelId) : array{
1341 $players = [];
1342 foreach($this->broadcastSubscribers[$channelId] as $subscriber){
1343 if($subscriber instanceof Player){
1344 $players[spl_object_id($subscriber)] = $subscriber;
1345 }
1346 }
1347 return $players;
1348 }
1349
1353 public function broadcastTip(string $tip, ?array $recipients = null) : int{
1354 $recipients = $recipients ?? $this->getPlayerBroadcastSubscribers(self::BROADCAST_CHANNEL_USERS);
1355
1356 foreach($recipients as $recipient){
1357 $recipient->sendTip($tip);
1358 }
1359
1360 return count($recipients);
1361 }
1362
1366 public function broadcastPopup(string $popup, ?array $recipients = null) : int{
1367 $recipients = $recipients ?? $this->getPlayerBroadcastSubscribers(self::BROADCAST_CHANNEL_USERS);
1368
1369 foreach($recipients as $recipient){
1370 $recipient->sendPopup($popup);
1371 }
1372
1373 return count($recipients);
1374 }
1375
1382 public function broadcastTitle(string $title, string $subtitle = "", int $fadeIn = -1, int $stay = -1, int $fadeOut = -1, ?array $recipients = null) : int{
1383 $recipients = $recipients ?? $this->getPlayerBroadcastSubscribers(self::BROADCAST_CHANNEL_USERS);
1384
1385 foreach($recipients as $recipient){
1386 $recipient->sendTitle($title, $subtitle, $fadeIn, $stay, $fadeOut);
1387 }
1388
1389 return count($recipients);
1390 }
1391
1405 public function prepareBatch(string $buffer, Compressor $compressor, ?bool $sync = null, ?TimingsHandler $timings = null) : CompressBatchPromise|string{
1406 $timings ??= Timings::$playerNetworkSendCompress;
1407 try{
1408 $timings->startTiming();
1409
1410 $threshold = $compressor->getCompressionThreshold();
1411 if($threshold === null || strlen($buffer) < $compressor->getCompressionThreshold()){
1412 $compressionType = CompressionAlgorithm::NONE;
1413 $compressed = $buffer;
1414
1415 }else{
1416 $sync ??= !$this->networkCompressionAsync;
1417
1418 if(!$sync && strlen($buffer) >= $this->networkCompressionAsyncThreshold){
1419 $promise = new CompressBatchPromise();
1420 $task = new CompressBatchTask($buffer, $promise, $compressor);
1421 $this->asyncPool->submitTask($task);
1422 return $promise;
1423 }
1424
1425 $compressionType = $compressor->getNetworkId();
1426 $compressed = $compressor->compress($buffer);
1427 }
1428
1429 return chr($compressionType) . $compressed;
1430 }finally{
1431 $timings->stopTiming();
1432 }
1433 }
1434
1435 public function enablePlugins(PluginEnableOrder $type) : bool{
1436 $allSuccess = true;
1437 foreach($this->pluginManager->getPlugins() as $plugin){
1438 if(!$plugin->isEnabled() && $plugin->getDescription()->getOrder() === $type){
1439 if(!$this->pluginManager->enablePlugin($plugin)){
1440 $allSuccess = false;
1441 }
1442 }
1443 }
1444
1445 if($type === PluginEnableOrder::POSTWORLD){
1446 $this->commandMap->registerServerAliases();
1447 }
1448
1449 return $allSuccess;
1450 }
1451
1455 public function dispatchCommand(CommandSender $sender, string $commandLine, bool $internal = false) : bool{
1456 if(!$internal){
1457 $ev = new CommandEvent($sender, $commandLine);
1458 $ev->call();
1459 if($ev->isCancelled()){
1460 return false;
1461 }
1462
1463 $commandLine = $ev->getCommand();
1464 }
1465
1466 return $this->commandMap->dispatch($sender, $commandLine);
1467 }
1468
1472 public function shutdown() : void{
1473 if($this->isRunning){
1474 $this->isRunning = false;
1475 $this->signalHandler->unregister();
1476 }
1477 }
1478
1479 private function forceShutdownExit() : void{
1480 $this->forceShutdown();
1481 Process::kill(Process::pid());
1482 }
1483
1484 public function forceShutdown() : void{
1485 if($this->hasStopped){
1486 return;
1487 }
1488
1489 if($this->doTitleTick){
1490 echo "\x1b]0;\x07";
1491 }
1492
1493 if($this->isRunning){
1494 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_forcingShutdown()));
1495 }
1496 try{
1497 if(!$this->isRunning()){
1498 $this->sendUsage(SendUsageTask::TYPE_CLOSE);
1499 }
1500
1501 $this->hasStopped = true;
1502
1503 $this->shutdown();
1504
1505 if(isset($this->pluginManager)){
1506 $this->logger->debug("Disabling all plugins");
1507 $this->pluginManager->disablePlugins();
1508 }
1509
1510 if(isset($this->network)){
1511 $this->network->getSessionManager()->close($this->configGroup->getPropertyString(Yml::SETTINGS_SHUTDOWN_MESSAGE, "Server closed"));
1512 }
1513
1514 if(isset($this->worldManager)){
1515 $this->logger->debug("Unloading all worlds");
1516 foreach($this->worldManager->getWorlds() as $world){
1517 $this->worldManager->unloadWorld($world, true);
1518 }
1519 }
1520
1521 $this->logger->debug("Removing event handlers");
1522 HandlerListManager::global()->unregisterAll();
1523
1524 if(isset($this->asyncPool)){
1525 $this->logger->debug("Shutting down async task worker pool");
1526 $this->asyncPool->shutdown();
1527 }
1528
1529 if(isset($this->configGroup)){
1530 $this->logger->debug("Saving properties");
1531 $this->configGroup->save();
1532 }
1533
1534 if($this->console !== null){
1535 $this->logger->debug("Closing console");
1536 $this->console->quit();
1537 }
1538
1539 if(isset($this->network)){
1540 $this->logger->debug("Stopping network interfaces");
1541 foreach($this->network->getInterfaces() as $interface){
1542 $this->logger->debug("Stopping network interface " . get_class($interface));
1543 $this->network->unregisterInterface($interface);
1544 }
1545 }
1546 }catch(\Throwable $e){
1547 $this->logger->logException($e);
1548 $this->logger->emergency("Crashed while crashing, killing process");
1549 @Process::kill(Process::pid());
1550 }
1551
1552 }
1553
1554 public function getQueryInformation() : QueryInfo{
1555 return $this->queryInfo;
1556 }
1557
1562 public function exceptionHandler(\Throwable $e, ?array $trace = null) : void{
1563 while(@ob_end_flush()){}
1564 global $lastError;
1565
1566 if($trace === null){
1567 $trace = $e->getTrace();
1568 }
1569
1570 //If this is a thread crash, this logs where the exception came from on the main thread, as opposed to the
1571 //crashed thread. This is intentional, and might be useful for debugging
1572 //Assume that the thread already logged the original exception with the correct stack trace
1573 $this->logger->logException($e, $trace);
1574
1575 if($e instanceof ThreadCrashException){
1576 $info = $e->getCrashInfo();
1577 $type = $info->getType();
1578 $errstr = $info->getMessage();
1579 $errfile = $info->getFile();
1580 $errline = $info->getLine();
1581 $printableTrace = $info->getTrace();
1582 $thread = $info->getThreadName();
1583 }else{
1584 $type = get_class($e);
1585 $errstr = $e->getMessage();
1586 $errfile = $e->getFile();
1587 $errline = $e->getLine();
1588 $printableTrace = Utils::printableTraceWithMetadata($trace);
1589 $thread = "Main";
1590 }
1591
1592 $errstr = preg_replace('/\s+/', ' ', trim($errstr));
1593
1594 $lastError = [
1595 "type" => $type,
1596 "message" => $errstr,
1597 "fullFile" => $errfile,
1598 "file" => Filesystem::cleanPath($errfile),
1599 "line" => $errline,
1600 "trace" => $printableTrace,
1601 "thread" => $thread
1602 ];
1603
1604 global $lastExceptionError, $lastError;
1605 $lastExceptionError = $lastError;
1606 $this->crashDump();
1607 }
1608
1609 private function writeCrashDumpFile(CrashDump $dump) : string{
1610 $crashFolder = Path::join($this->dataPath, "crashdumps");
1611 if(!is_dir($crashFolder)){
1612 mkdir($crashFolder);
1613 }
1614 $crashDumpPath = Path::join($crashFolder, date("D_M_j-H.i.s-T_Y", (int) $dump->getData()->time) . ".log");
1615
1616 $fp = @fopen($crashDumpPath, "wb");
1617 if(!is_resource($fp)){
1618 throw new \RuntimeException("Unable to open new file to generate crashdump");
1619 }
1620 $writer = new CrashDumpRenderer($fp, $dump->getData());
1621 $writer->renderHumanReadable();
1622 $dump->encodeData($writer);
1623
1624 fclose($fp);
1625 return $crashDumpPath;
1626 }
1627
1628 public function crashDump() : void{
1629 while(@ob_end_flush()){}
1630 if(!$this->isRunning){
1631 return;
1632 }
1633 if($this->sendUsageTicker > 0){
1634 $this->sendUsage(SendUsageTask::TYPE_CLOSE);
1635 }
1636 $this->hasStopped = false;
1637
1638 ini_set("error_reporting", '0');
1639 ini_set("memory_limit", '-1'); //Fix error dump not dumped on memory problems
1640 try{
1641 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_crash_create()));
1642 $dump = new CrashDump($this, $this->pluginManager ?? null);
1643
1644 $crashDumpPath = $this->writeCrashDumpFile($dump);
1645
1646 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_crash_submit($crashDumpPath)));
1647
1648 if($this->configGroup->getPropertyBool(Yml::AUTO_REPORT_ENABLED, true)){
1649 $report = true;
1650
1651 $stamp = Path::join($this->dataPath, "crashdumps", ".last_crash");
1652 $crashInterval = 120; //2 minutes
1653 if(($lastReportTime = @filemtime($stamp)) !== false && $lastReportTime + $crashInterval >= time()){
1654 $report = false;
1655 $this->logger->debug("Not sending crashdump due to last crash less than $crashInterval seconds ago");
1656 }
1657 @touch($stamp); //update file timestamp
1658
1659 if($dump->getData()->error["type"] === \ParseError::class){
1660 $report = false;
1661 }
1662
1663 if(strrpos(VersionInfo::GIT_HASH(), "-dirty") !== false || VersionInfo::GIT_HASH() === str_repeat("00", 20)){
1664 $this->logger->debug("Not sending crashdump due to locally modified");
1665 $report = false; //Don't send crashdumps for locally modified builds
1666 }
1667
1668 if($report){
1669 $url = ($this->configGroup->getPropertyBool(Yml::AUTO_REPORT_USE_HTTPS, true) ? "https" : "http") . "://" . $this->configGroup->getPropertyString(Yml::AUTO_REPORT_HOST, "crash.pmmp.io") . "/submit/api";
1670 $postUrlError = "Unknown error";
1671 $reply = Internet::postURL($url, [
1672 "report" => "yes",
1673 "name" => $this->getName() . " " . $this->getPocketMineVersion(),
1674 "email" => "[email protected]",
1675 "reportPaste" => base64_encode($dump->getEncodedData())
1676 ], 10, [], $postUrlError);
1677
1678 if($reply !== null && is_object($data = json_decode($reply->getBody()))){
1679 if(isset($data->crashId) && is_int($data->crashId) && isset($data->crashUrl) && is_string($data->crashUrl)){
1680 $reportId = $data->crashId;
1681 $reportUrl = $data->crashUrl;
1682 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_crash_archive($reportUrl, (string) $reportId)));
1683 }elseif(isset($data->error) && is_string($data->error)){
1684 $this->logger->emergency("Automatic crash report submission failed: $data->error");
1685 }else{
1686 $this->logger->emergency("Invalid JSON response received from crash archive: " . $reply->getBody());
1687 }
1688 }else{
1689 $this->logger->emergency("Failed to communicate with crash archive: $postUrlError");
1690 }
1691 }
1692 }
1693 }catch(\Throwable $e){
1694 $this->logger->logException($e);
1695 try{
1696 $this->logger->critical($this->language->translate(KnownTranslationFactory::pocketmine_crash_error($e->getMessage())));
1697 }catch(\Throwable $e){}
1698 }
1699
1700 $this->forceShutdown();
1701 $this->isRunning = false;
1702
1703 //Force minimum uptime to be >= 120 seconds, to reduce the impact of spammy crash loops
1704 $uptime = time() - ((int) $this->startTime);
1705 $minUptime = 120;
1706 $spacing = $minUptime - $uptime;
1707 if($spacing > 0){
1708 echo "--- Uptime {$uptime}s - waiting {$spacing}s to throttle automatic restart (you can kill the process safely now) ---" . PHP_EOL;
1709 sleep($spacing);
1710 }
1711 @Process::kill(Process::pid());
1712 exit(1);
1713 }
1714
1718 public function __debugInfo() : array{
1719 return [];
1720 }
1721
1722 public function getTickSleeper() : SleeperHandler{
1723 return $this->tickSleeper;
1724 }
1725
1726 private function tickProcessor() : void{
1727 $this->nextTick = microtime(true);
1728
1729 while($this->isRunning){
1730 $this->tick();
1731
1732 //sleeps are self-correcting - if we undersleep 1ms on this tick, we'll sleep an extra ms on the next tick
1733 $this->tickSleeper->sleepUntil($this->nextTick);
1734 }
1735 }
1736
1737 public function addOnlinePlayer(Player $player) : bool{
1738 $ev = new PlayerLoginEvent($player, "Plugin reason");
1739 $ev->call();
1740 if($ev->isCancelled() || !$player->isConnected()){
1741 $player->disconnect($ev->getKickMessage());
1742
1743 return false;
1744 }
1745
1746 $session = $player->getNetworkSession();
1747 $position = $player->getPosition();
1748 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_player_logIn(
1749 TextFormat::AQUA . $player->getName() . TextFormat::RESET,
1750 $session->getIp(),
1751 (string) $session->getPort(),
1752 (string) $player->getId(),
1753 $position->getWorld()->getDisplayName(),
1754 (string) round($position->x, 4),
1755 (string) round($position->y, 4),
1756 (string) round($position->z, 4)
1757 )));
1758
1759 foreach($this->playerList as $p){
1760 $p->getNetworkSession()->onPlayerAdded($player);
1761 }
1762 $rawUUID = $player->getUniqueId()->getBytes();
1763 $this->playerList[$rawUUID] = $player;
1764
1765 if($this->sendUsageTicker > 0){
1766 $this->uniquePlayers[$rawUUID] = $rawUUID;
1767 }
1768
1769 return true;
1770 }
1771
1772 public function removeOnlinePlayer(Player $player) : void{
1773 if(isset($this->playerList[$rawUUID = $player->getUniqueId()->getBytes()])){
1774 unset($this->playerList[$rawUUID]);
1775 foreach($this->playerList as $p){
1776 $p->getNetworkSession()->onPlayerRemoved($player);
1777 }
1778 }
1779 }
1780
1781 public function sendUsage(int $type = SendUsageTask::TYPE_STATUS) : void{
1782 if($this->configGroup->getPropertyBool(Yml::ANONYMOUS_STATISTICS_ENABLED, true)){
1783 $this->asyncPool->submitTask(new SendUsageTask($this, $type, $this->uniquePlayers));
1784 }
1785 $this->uniquePlayers = [];
1786 }
1787
1788 public function getLanguage() : Language{
1789 return $this->language;
1790 }
1791
1792 public function isLanguageForced() : bool{
1793 return $this->forceLanguage;
1794 }
1795
1796 public function getNetwork() : Network{
1797 return $this->network;
1798 }
1799
1800 public function getMemoryManager() : MemoryManager{
1801 return $this->memoryManager;
1802 }
1803
1804 private function titleTick() : void{
1805 Timings::$titleTick->startTiming();
1806
1807 $u = Process::getAdvancedMemoryUsage();
1808 $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());
1809
1810 $online = count($this->playerList);
1811 $connecting = $this->network->getConnectionCount() - $online;
1812 $bandwidthStats = $this->network->getBandwidthTracker();
1813
1814 echo "\x1b]0;" . $this->getName() . " " .
1815 $this->getPocketMineVersion() .
1816 " | Online $online/" . $this->maxPlayers .
1817 ($connecting > 0 ? " (+$connecting connecting)" : "") .
1818 " | Memory " . $usage .
1819 " | U " . round($bandwidthStats->getSend()->getAverageBytes() / 1024, 2) .
1820 " D " . round($bandwidthStats->getReceive()->getAverageBytes() / 1024, 2) .
1821 " kB/s | TPS " . $this->getTicksPerSecondAverage() .
1822 " | Load " . $this->getTickUsageAverage() . "%\x07";
1823
1824 Timings::$titleTick->stopTiming();
1825 }
1826
1830 private function tick() : void{
1831 $tickTime = microtime(true);
1832 if(($tickTime - $this->nextTick) < -0.025){ //Allow half a tick of diff
1833 return;
1834 }
1835
1836 Timings::$serverTick->startTiming();
1837
1838 ++$this->tickCounter;
1839
1840 Timings::$scheduler->startTiming();
1841 $this->pluginManager->tickSchedulers($this->tickCounter);
1842 Timings::$scheduler->stopTiming();
1843
1844 Timings::$schedulerAsync->startTiming();
1845 $this->asyncPool->collectTasks();
1846 Timings::$schedulerAsync->stopTiming();
1847
1848 $this->worldManager->tick($this->tickCounter);
1849
1850 Timings::$connection->startTiming();
1851 $this->network->tick();
1852 Timings::$connection->stopTiming();
1853
1854 if(($this->tickCounter % self::TARGET_TICKS_PER_SECOND) === 0){
1855 if($this->doTitleTick){
1856 $this->titleTick();
1857 }
1858 $this->currentTPS = self::TARGET_TICKS_PER_SECOND;
1859 $this->currentUse = 0;
1860
1861 $queryRegenerateEvent = new QueryRegenerateEvent(new QueryInfo($this));
1862 $queryRegenerateEvent->call();
1863 $this->queryInfo = $queryRegenerateEvent->getQueryInfo();
1864
1865 $this->network->updateName();
1866 $this->network->getBandwidthTracker()->rotateAverageHistory();
1867 }
1868
1869 if($this->sendUsageTicker > 0 && --$this->sendUsageTicker === 0){
1870 $this->sendUsageTicker = self::TICKS_PER_STATS_REPORT;
1871 $this->sendUsage(SendUsageTask::TYPE_STATUS);
1872 }
1873
1874 if(($this->tickCounter % self::TICKS_PER_WORLD_CACHE_CLEAR) === 0){
1875 foreach($this->worldManager->getWorlds() as $world){
1876 $world->clearCache();
1877 }
1878 }
1879
1880 if(($this->tickCounter % self::TICKS_PER_TPS_OVERLOAD_WARNING) === 0 && $this->getTicksPerSecondAverage() < self::TPS_OVERLOAD_WARNING_THRESHOLD){
1881 $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_tickOverload()));
1882 }
1883
1884 $this->memoryManager->check();
1885
1886 if($this->console !== null){
1887 Timings::$serverCommand->startTiming();
1888 while(($line = $this->console->readLine()) !== null){
1889 $this->consoleSender ??= new ConsoleCommandSender($this, $this->language);
1890 $this->dispatchCommand($this->consoleSender, $line);
1891 }
1892 Timings::$serverCommand->stopTiming();
1893 }
1894
1895 Timings::$serverTick->stopTiming();
1896
1897 $now = microtime(true);
1898 $totalTickTimeSeconds = $now - $tickTime + ($this->tickSleeper->getNotificationProcessingTime() / 1_000_000_000);
1899 $this->currentTPS = min(self::TARGET_TICKS_PER_SECOND, 1 / max(0.001, $totalTickTimeSeconds));
1900 $this->currentUse = min(1, $totalTickTimeSeconds / self::TARGET_SECONDS_PER_TICK);
1901
1902 TimingsHandler::tick($this->currentTPS <= $this->profilingTickRate);
1903
1904 $idx = $this->tickCounter % self::TARGET_TICKS_PER_SECOND;
1905 $this->tickAverage[$idx] = $this->currentTPS;
1906 $this->useAverage[$idx] = $this->currentUse;
1907 $this->tickSleeper->resetNotificationProcessingTime();
1908
1909 if(($this->nextTick - $tickTime) < -1){
1910 $this->nextTick = $tickTime;
1911 }else{
1912 $this->nextTick += self::TARGET_SECONDS_PER_TICK;
1913 }
1914 }
1915}
getBroadcastChannelSubscribers(string $channelId)
unsubscribeFromBroadcastChannel(string $channelId, CommandSender $subscriber)
getPlayerExact(string $name)
exceptionHandler(\Throwable $e, ?array $trace=null)
broadcastTitle(string $title, string $subtitle="", int $fadeIn=-1, int $stay=-1, int $fadeOut=-1, ?array $recipients=null)
getPlayerByUUID(UuidInterface $uuid)
getPlayerByPrefix(string $name)
unsubscribeFromAllBroadcastChannels(CommandSender $subscriber)
hasOfflinePlayerData(string $name)
getPluginCommand(string $name)
dispatchCommand(CommandSender $sender, string $commandLine, bool $internal=false)
getAllowedViewDistance(int $distance)
broadcastMessage(Translatable|string $message, ?array $recipients=null)
createPlayer(NetworkSession $session, PlayerInfo $playerInfo, bool $authenticated, ?CompoundTag $offlinePlayerData)
broadcastPopup(string $popup, ?array $recipients=null)
subscribeToBroadcastChannel(string $channelId, CommandSender $subscriber)
getPlayerByRawUUID(string $rawUUID)
broadcastTip(string $tip, ?array $recipients=null)