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