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