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