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