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