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