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