PocketMine-MP 5.23.3 git-976fc63567edab7a6fb6aeae739f43cf9fe57de4
Loading...
Searching...
No Matches
src/Server.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
28namespace pocketmine;
29
75use pocketmine\player\GameMode;
84use pocketmine\plugin\PluginEnableOrder;
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
349 public function setMaxPlayers(int $maxPlayers) : void{
350 $this->maxPlayers = $maxPlayers;
351 }
352
357 public function getOnlineMode() : bool{
358 return $this->onlineMode;
359 }
360
364 public function requiresAuthentication() : bool{
365 return $this->getOnlineMode();
366 }
367
368 public function getPort() : int{
369 return $this->configGroup->getConfigInt(ServerProperties::SERVER_PORT_IPV4, self::DEFAULT_PORT_IPV4);
370 }
371
372 public function getPortV6() : int{
373 return $this->configGroup->getConfigInt(ServerProperties::SERVER_PORT_IPV6, self::DEFAULT_PORT_IPV6);
374 }
375
376 public function getViewDistance() : int{
377 return max(2, $this->configGroup->getConfigInt(ServerProperties::VIEW_DISTANCE, self::DEFAULT_MAX_VIEW_DISTANCE));
378 }
379
383 public function getAllowedViewDistance(int $distance) : int{
384 return max(2, min($distance, $this->memoryManager->getViewDistance($this->getViewDistance())));
385 }
386
387 public function getIp() : string{
388 $str = $this->configGroup->getConfigString(ServerProperties::SERVER_IPV4);
389 return $str !== "" ? $str : "0.0.0.0";
390 }
391
392 public function getIpV6() : string{
393 $str = $this->configGroup->getConfigString(ServerProperties::SERVER_IPV6);
394 return $str !== "" ? $str : "::";
395 }
396
397 public function getServerUniqueId() : UuidInterface{
398 return $this->serverID;
399 }
400
401 public function getGamemode() : GameMode{
402 return GameMode::fromString($this->configGroup->getConfigString(ServerProperties::GAME_MODE)) ?? GameMode::SURVIVAL;
403 }
404
405 public function getForceGamemode() : bool{
406 return $this->configGroup->getConfigBool(ServerProperties::FORCE_GAME_MODE, false);
407 }
408
412 public function getDifficulty() : int{
413 return $this->configGroup->getConfigInt(ServerProperties::DIFFICULTY, World::DIFFICULTY_NORMAL);
414 }
415
416 public function hasWhitelist() : bool{
417 return $this->configGroup->getConfigBool(ServerProperties::WHITELIST, false);
418 }
419
420 public function isHardcore() : bool{
421 return $this->configGroup->getConfigBool(ServerProperties::HARDCORE, false);
422 }
423
424 public function getMotd() : string{
425 return $this->configGroup->getConfigString(ServerProperties::MOTD, self::DEFAULT_SERVER_NAME);
426 }
427
428 public function getLoader() : ThreadSafeClassLoader{
429 return $this->autoloader;
430 }
431
432 public function getLogger() : AttachableThreadSafeLogger{
433 return $this->logger;
434 }
435
436 public function getUpdater() : UpdateChecker{
437 return $this->updater;
438 }
439
440 public function getPluginManager() : PluginManager{
441 return $this->pluginManager;
442 }
443
444 public function getCraftingManager() : CraftingManager{
445 return $this->craftingManager;
446 }
447
448 public function getResourcePackManager() : ResourcePackManager{
449 return $this->resourceManager;
450 }
451
452 public function getWorldManager() : WorldManager{
453 return $this->worldManager;
454 }
455
456 public function getAsyncPool() : AsyncPool{
457 return $this->asyncPool;
458 }
459
460 public function getTick() : int{
461 return $this->tickCounter;
462 }
463
467 public function getTicksPerSecond() : float{
468 return round($this->currentTPS, 2);
469 }
470
474 public function getTicksPerSecondAverage() : float{
475 return round(array_sum($this->tickAverage) / count($this->tickAverage), 2);
476 }
477
481 public function getTickUsage() : float{
482 return round($this->currentUse * 100, 2);
483 }
484
488 public function getTickUsageAverage() : float{
489 return round((array_sum($this->useAverage) / count($this->useAverage)) * 100, 2);
490 }
491
492 public function getStartTime() : float{
493 return $this->startTime;
494 }
495
496 public function getCommandMap() : SimpleCommandMap{
497 return $this->commandMap;
498 }
499
503 public function getOnlinePlayers() : array{
504 return $this->playerList;
505 }
506
507 public function shouldSavePlayerData() : bool{
508 return $this->configGroup->getPropertyBool(Yml::PLAYER_SAVE_PLAYER_DATA, true);
509 }
510
511 public function getOfflinePlayer(string $name) : Player|OfflinePlayer|null{
512 $name = strtolower($name);
513 $result = $this->getPlayerExact($name);
514
515 if($result === null){
516 $result = new OfflinePlayer($name, $this->getOfflinePlayerData($name));
517 }
518
519 return $result;
520 }
521
525 public function hasOfflinePlayerData(string $name) : bool{
526 return $this->playerDataProvider->hasData($name);
527 }
528
529 public function getOfflinePlayerData(string $name) : ?CompoundTag{
530 return Timings::$syncPlayerDataLoad->time(function() use ($name) : ?CompoundTag{
531 try{
532 return $this->playerDataProvider->loadData($name);
533 }catch(PlayerDataLoadException $e){
534 $this->logger->debug("Failed to load player data for $name: " . $e->getMessage());
535 $this->logger->error($this->language->translate(KnownTranslationFactory::pocketmine_data_playerCorrupted($name)));
536 return null;
537 }
538 });
539 }
540
541 public function saveOfflinePlayerData(string $name, CompoundTag $nbtTag) : void{
542 $ev = new PlayerDataSaveEvent($nbtTag, $name, $this->getPlayerExact($name));
543 if(!$this->shouldSavePlayerData()){
544 $ev->cancel();
545 }
546
547 $ev->call();
548
549 if(!$ev->isCancelled()){
550 Timings::$syncPlayerDataSave->time(function() use ($name, $ev) : void{
551 try{
552 $this->playerDataProvider->saveData($name, $ev->getSaveData());
553 }catch(PlayerDataSaveException $e){
554 $this->logger->critical($this->language->translate(KnownTranslationFactory::pocketmine_data_saveError($name, $e->getMessage())));
555 $this->logger->logException($e);
556 }
557 });
558 }
559 }
560
564 public function createPlayer(NetworkSession $session, PlayerInfo $playerInfo, bool $authenticated, ?CompoundTag $offlinePlayerData) : Promise{
565 $ev = new PlayerCreationEvent($session);
566 $ev->call();
567 $class = $ev->getPlayerClass();
568
569 if($offlinePlayerData !== null && ($world = $this->worldManager->getWorldByName($offlinePlayerData->getString(Player::TAG_LEVEL, ""))) !== null){
570 $playerPos = EntityDataHelper::parseLocation($offlinePlayerData, $world);
571 }else{
572 $world = $this->worldManager->getDefaultWorld();
573 if($world === null){
574 throw new AssumptionFailedError("Default world should always be loaded");
575 }
576 $playerPos = null;
577 }
579 $playerPromiseResolver = new PromiseResolver();
580
581 $createPlayer = function(Location $location) use ($playerPromiseResolver, $class, $session, $playerInfo, $authenticated, $offlinePlayerData) : void{
583 $player = new $class($this, $session, $playerInfo, $authenticated, $location, $offlinePlayerData);
584 if(!$player->hasPlayedBefore()){
585 $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
586 }
587 $playerPromiseResolver->resolve($player);
588 };
589
590 if($playerPos === null){ //new player or no valid position due to world not being loaded
591 $world->requestSafeSpawn()->onCompletion(
592 function(Position $spawn) use ($createPlayer, $playerPromiseResolver, $session, $world) : void{
593 if(!$session->isConnected()){
594 $playerPromiseResolver->reject();
595 return;
596 }
597 $createPlayer(Location::fromObject($spawn, $world));
598 },
599 function() use ($playerPromiseResolver, $session) : void{
600 if($session->isConnected()){
601 $session->disconnectWithError(KnownTranslationFactory::pocketmine_disconnect_error_respawn());
602 }
603 $playerPromiseResolver->reject();
604 }
605 );
606 }else{ //returning player with a valid position - safe spawn not required
607 $createPlayer($playerPos);
608 }
609
610 return $playerPromiseResolver->getPromise();
611 }
612
623 public function getPlayerByPrefix(string $name) : ?Player{
624 $found = null;
625 $name = strtolower($name);
626 $delta = PHP_INT_MAX;
627 foreach($this->getOnlinePlayers() as $player){
628 if(stripos($player->getName(), $name) === 0){
629 $curDelta = strlen($player->getName()) - strlen($name);
630 if($curDelta < $delta){
631 $found = $player;
632 $delta = $curDelta;
633 }
634 if($curDelta === 0){
635 break;
636 }
637 }
638 }
639
640 return $found;
641 }
642
646 public function getPlayerExact(string $name) : ?Player{
647 $name = strtolower($name);
648 foreach($this->getOnlinePlayers() as $player){
649 if(strtolower($player->getName()) === $name){
650 return $player;
651 }
652 }
653
654 return null;
655 }
656
660 public function getPlayerByRawUUID(string $rawUUID) : ?Player{
661 return $this->playerList[$rawUUID] ?? null;
662 }
663
667 public function getPlayerByUUID(UuidInterface $uuid) : ?Player{
668 return $this->getPlayerByRawUUID($uuid->getBytes());
669 }
670
671 public function getConfigGroup() : ServerConfigGroup{
672 return $this->configGroup;
673 }
674
679 public function getPluginCommand(string $name){
680 if(($command = $this->commandMap->getCommand($name)) instanceof PluginOwned){
681 return $command;
682 }else{
683 return null;
684 }
685 }
686
687 public function getNameBans() : BanList{
688 return $this->banByName;
689 }
690
691 public function getIPBans() : BanList{
692 return $this->banByIP;
693 }
694
695 public function addOp(string $name) : void{
696 $this->operators->set(strtolower($name), true);
697
698 if(($player = $this->getPlayerExact($name)) !== null){
699 $player->setBasePermission(DefaultPermissions::ROOT_OPERATOR, true);
700 }
701 $this->operators->save();
702 }
703
704 public function removeOp(string $name) : void{
705 $lowercaseName = strtolower($name);
706 foreach($this->operators->getAll() as $operatorName => $_){
707 $operatorName = (string) $operatorName;
708 if($lowercaseName === strtolower($operatorName)){
709 $this->operators->remove($operatorName);
710 }
711 }
712
713 if(($player = $this->getPlayerExact($name)) !== null){
714 $player->unsetBasePermission(DefaultPermissions::ROOT_OPERATOR);
715 }
716 $this->operators->save();
717 }
718
719 public function addWhitelist(string $name) : void{
720 $this->whitelist->set(strtolower($name), true);
721 $this->whitelist->save();
722 }
723
724 public function removeWhitelist(string $name) : void{
725 $this->whitelist->remove(strtolower($name));
726 $this->whitelist->save();
727 }
728
729 public function isWhitelisted(string $name) : bool{
730 return !$this->hasWhitelist() || $this->operators->exists($name, true) || $this->whitelist->exists($name, true);
731 }
732
733 public function isOp(string $name) : bool{
734 return $this->operators->exists($name, true);
735 }
736
737 public function getWhitelisted() : Config{
738 return $this->whitelist;
739 }
740
741 public function getOps() : Config{
742 return $this->operators;
743 }
744
749 public function getCommandAliases() : array{
750 $section = $this->configGroup->getProperty(Yml::ALIASES);
751 $result = [];
752 if(is_array($section)){
753 foreach(Utils::promoteKeys($section) as $key => $value){
754 //TODO: more validation needed here
755 //key might not be a string, value might not be list<string>
756 $commands = [];
757 if(is_array($value)){
758 $commands = $value;
759 }else{
760 $commands[] = (string) $value;
761 }
762
763 $result[(string) $key] = $commands;
764 }
765 }
766
767 return $result;
768 }
769
770 public static function getInstance() : Server{
771 if(self::$instance === null){
772 throw new \RuntimeException("Attempt to retrieve Server instance outside server thread");
773 }
774 return self::$instance;
775 }
776
777 public function __construct(
778 private ThreadSafeClassLoader $autoloader,
779 private AttachableThreadSafeLogger $logger,
780 string $dataPath,
781 string $pluginPath
782 ){
783 if(self::$instance !== null){
784 throw new \LogicException("Only one server instance can exist at once");
785 }
786 self::$instance = $this;
787 $this->startTime = microtime(true);
788 $this->tickAverage = array_fill(0, self::TARGET_TICKS_PER_SECOND, self::TARGET_TICKS_PER_SECOND);
789 $this->useAverage = array_fill(0, self::TARGET_TICKS_PER_SECOND, 0);
790
791 Timings::init();
792 $this->tickSleeper = new TimeTrackingSleeperHandler(Timings::$serverInterrupts);
793
794 $this->signalHandler = new SignalHandler(function() : void{
795 $this->logger->info("Received signal interrupt, stopping the server");
796 $this->shutdown();
797 });
798
799 try{
800 foreach([
801 $dataPath,
802 $pluginPath,
803 Path::join($dataPath, "worlds"),
804 Path::join($dataPath, "players")
805 ] as $neededPath){
806 if(!file_exists($neededPath)){
807 mkdir($neededPath, 0777);
808 }
809 }
810
811 $this->dataPath = realpath($dataPath) . DIRECTORY_SEPARATOR;
812 $this->pluginPath = realpath($pluginPath) . DIRECTORY_SEPARATOR;
813
814 $this->logger->info("Loading server configuration");
815 $pocketmineYmlPath = Path::join($this->dataPath, "pocketmine.yml");
816 if(!file_exists($pocketmineYmlPath)){
817 $content = Filesystem::fileGetContents(Path::join(\pocketmine\RESOURCE_PATH, "pocketmine.yml"));
818 if(VersionInfo::IS_DEVELOPMENT_BUILD){
819 $content = str_replace("preferred-channel: stable", "preferred-channel: beta", $content);
820 }
821 @file_put_contents($pocketmineYmlPath, $content);
822 }
823
824 $this->configGroup = new ServerConfigGroup(
825 new Config($pocketmineYmlPath, Config::YAML, []),
826 new Config(Path::join($this->dataPath, "server.properties"), Config::PROPERTIES, [
827 ServerProperties::MOTD => self::DEFAULT_SERVER_NAME,
828 ServerProperties::SERVER_PORT_IPV4 => self::DEFAULT_PORT_IPV4,
829 ServerProperties::SERVER_PORT_IPV6 => self::DEFAULT_PORT_IPV6,
830 ServerProperties::ENABLE_IPV6 => true,
831 ServerProperties::WHITELIST => false,
832 ServerProperties::MAX_PLAYERS => self::DEFAULT_MAX_PLAYERS,
833 ServerProperties::GAME_MODE => GameMode::SURVIVAL->name, //TODO: this probably shouldn't use the enum name directly
834 ServerProperties::FORCE_GAME_MODE => false,
835 ServerProperties::HARDCORE => false,
836 ServerProperties::PVP => true,
837 ServerProperties::DIFFICULTY => World::DIFFICULTY_NORMAL,
838 ServerProperties::DEFAULT_WORLD_GENERATOR_SETTINGS => "",
839 ServerProperties::DEFAULT_WORLD_NAME => "world",
840 ServerProperties::DEFAULT_WORLD_SEED => "",
841 ServerProperties::DEFAULT_WORLD_GENERATOR => "DEFAULT",
842 ServerProperties::ENABLE_QUERY => true,
843 ServerProperties::AUTO_SAVE => true,
844 ServerProperties::VIEW_DISTANCE => self::DEFAULT_MAX_VIEW_DISTANCE,
845 ServerProperties::XBOX_AUTH => true,
846 ServerProperties::LANGUAGE => "eng"
847 ])
848 );
849
850 $debugLogLevel = $this->configGroup->getPropertyInt(Yml::DEBUG_LEVEL, 1);
851 if($this->logger instanceof MainLogger){
852 $this->logger->setLogDebug($debugLogLevel > 1);
853 }
854
855 $this->forceLanguage = $this->configGroup->getPropertyBool(Yml::SETTINGS_FORCE_LANGUAGE, false);
856 $selectedLang = $this->configGroup->getConfigString(ServerProperties::LANGUAGE, $this->configGroup->getPropertyString("settings.language", Language::FALLBACK_LANGUAGE));
857 try{
858 $this->language = new Language($selectedLang);
859 }catch(LanguageNotFoundException $e){
860 $this->logger->error($e->getMessage());
861 try{
862 $this->language = new Language(Language::FALLBACK_LANGUAGE);
863 }catch(LanguageNotFoundException $e){
864 $this->logger->emergency("Fallback language \"" . Language::FALLBACK_LANGUAGE . "\" not found");
865 return;
866 }
867 }
868
869 $this->logger->info($this->language->translate(KnownTranslationFactory::language_selected($this->language->getName(), $this->language->getLang())));
870
871 if(VersionInfo::IS_DEVELOPMENT_BUILD){
872 if(!$this->configGroup->getPropertyBool(Yml::SETTINGS_ENABLE_DEV_BUILDS, false)){
873 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error1(VersionInfo::NAME)));
874 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error2()));
875 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error3()));
876 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error4(Yml::SETTINGS_ENABLE_DEV_BUILDS)));
877 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_error5("https://github.com/pmmp/PocketMine-MP/releases")));
878 $this->forceShutdownExit();
879
880 return;
881 }
882
883 $this->logger->warning(str_repeat("-", 40));
884 $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_warning1(VersionInfo::NAME)));
885 $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_warning2()));
886 $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_devBuild_warning3()));
887 $this->logger->warning(str_repeat("-", 40));
888 }
889
890 $this->memoryManager = new MemoryManager($this);
891
892 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_start(TextFormat::AQUA . $this->getVersion() . TextFormat::RESET)));
893
894 if(($poolSize = $this->configGroup->getPropertyString(Yml::SETTINGS_ASYNC_WORKERS, "auto")) === "auto"){
895 $poolSize = 2;
896 $processors = Utils::getCoreCount() - 2;
897
898 if($processors > 0){
899 $poolSize = max(1, $processors);
900 }
901 }else{
902 $poolSize = max(1, (int) $poolSize);
903 }
904
905 TimingsHandler::setEnabled($this->configGroup->getPropertyBool(Yml::SETTINGS_ENABLE_PROFILING, false));
906 $this->profilingTickRate = $this->configGroup->getPropertyInt(Yml::SETTINGS_PROFILE_REPORT_TRIGGER, self::TARGET_TICKS_PER_SECOND);
907
908 $this->asyncPool = new AsyncPool($poolSize, max(-1, $this->configGroup->getPropertyInt(Yml::MEMORY_ASYNC_WORKER_HARD_LIMIT, 256)), $this->autoloader, $this->logger, $this->tickSleeper);
909 $this->asyncPool->addWorkerStartHook(function(int $i) : void{
910 if(TimingsHandler::isEnabled()){
911 $this->asyncPool->submitTaskToWorker(TimingsControlTask::setEnabled(true), $i);
912 }
913 });
914 TimingsHandler::getToggleCallbacks()->add(function(bool $enable) : void{
915 foreach($this->asyncPool->getRunningWorkers() as $workerId){
916 $this->asyncPool->submitTaskToWorker(TimingsControlTask::setEnabled($enable), $workerId);
917 }
918 });
919 TimingsHandler::getReloadCallbacks()->add(function() : void{
920 foreach($this->asyncPool->getRunningWorkers() as $workerId){
921 $this->asyncPool->submitTaskToWorker(TimingsControlTask::reload(), $workerId);
922 }
923 });
924 TimingsHandler::getCollectCallbacks()->add(function() : array{
925 $promises = [];
926 foreach($this->asyncPool->getRunningWorkers() as $workerId){
928 $resolver = new PromiseResolver();
929 $this->asyncPool->submitTaskToWorker(new TimingsCollectionTask($resolver), $workerId);
930
931 $promises[] = $resolver->getPromise();
932 }
933
934 return $promises;
935 });
936
937 $netCompressionThreshold = -1;
938 if($this->configGroup->getPropertyInt(Yml::NETWORK_BATCH_THRESHOLD, 256) >= 0){
939 $netCompressionThreshold = $this->configGroup->getPropertyInt(Yml::NETWORK_BATCH_THRESHOLD, 256);
940 }
941 if($netCompressionThreshold < 0){
942 $netCompressionThreshold = null;
943 }
944
945 $netCompressionLevel = $this->configGroup->getPropertyInt(Yml::NETWORK_COMPRESSION_LEVEL, 6);
946 if($netCompressionLevel < 1 || $netCompressionLevel > 9){
947 $this->logger->warning("Invalid network compression level $netCompressionLevel set, setting to default 6");
948 $netCompressionLevel = 6;
949 }
950 ZlibCompressor::setInstance(new ZlibCompressor($netCompressionLevel, $netCompressionThreshold, ZlibCompressor::DEFAULT_MAX_DECOMPRESSION_SIZE));
951
952 $this->networkCompressionAsync = $this->configGroup->getPropertyBool(Yml::NETWORK_ASYNC_COMPRESSION, true);
953 $this->networkCompressionAsyncThreshold = max(
954 $this->configGroup->getPropertyInt(Yml::NETWORK_ASYNC_COMPRESSION_THRESHOLD, self::DEFAULT_ASYNC_COMPRESSION_THRESHOLD),
955 $netCompressionThreshold ?? self::DEFAULT_ASYNC_COMPRESSION_THRESHOLD
956 );
957
958 EncryptionContext::$ENABLED = $this->configGroup->getPropertyBool(Yml::NETWORK_ENABLE_ENCRYPTION, true);
959
960 $this->doTitleTick = $this->configGroup->getPropertyBool(Yml::CONSOLE_TITLE_TICK, true) && Terminal::hasFormattingCodes();
961
962 $this->operators = new Config(Path::join($this->dataPath, "ops.txt"), Config::ENUM);
963 $this->whitelist = new Config(Path::join($this->dataPath, "white-list.txt"), Config::ENUM);
964
965 $bannedTxt = Path::join($this->dataPath, "banned.txt");
966 $bannedPlayersTxt = Path::join($this->dataPath, "banned-players.txt");
967 if(file_exists($bannedTxt) && !file_exists($bannedPlayersTxt)){
968 @rename($bannedTxt, $bannedPlayersTxt);
969 }
970 @touch($bannedPlayersTxt);
971 $this->banByName = new BanList($bannedPlayersTxt);
972 $this->banByName->load();
973 $bannedIpsTxt = Path::join($this->dataPath, "banned-ips.txt");
974 @touch($bannedIpsTxt);
975 $this->banByIP = new BanList($bannedIpsTxt);
976 $this->banByIP->load();
977
978 $this->maxPlayers = $this->configGroup->getConfigInt(ServerProperties::MAX_PLAYERS, self::DEFAULT_MAX_PLAYERS);
979
980 $this->onlineMode = $this->configGroup->getConfigBool(ServerProperties::XBOX_AUTH, true);
981 if($this->onlineMode){
982 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_auth_enabled()));
983 }else{
984 $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_auth_disabled()));
985 $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_authWarning()));
986 $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_authProperty_disabled()));
987 }
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(Path::join(\pocketmine\BEDROCK_DATA_PATH, "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 $this->pluginManager->registerInterface(new FolderPluginLoader($this->autoloader));
1037
1038 $providerManager = new WorldProviderManager();
1039 if(
1040 ($format = $providerManager->getProviderByName($formatName = $this->configGroup->getPropertyString(Yml::LEVEL_SETTINGS_DEFAULT_FORMAT, ""))) !== null &&
1041 $format instanceof WritableWorldProviderManagerEntry
1042 ){
1043 $providerManager->setDefault($format);
1044 }elseif($formatName !== ""){
1045 $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_level_badDefaultFormat($formatName)));
1046 }
1047
1048 $this->worldManager = new WorldManager($this, Path::join($this->dataPath, "worlds"), $providerManager);
1049 $this->worldManager->setAutoSave($this->configGroup->getConfigBool(ServerProperties::AUTO_SAVE, $this->worldManager->getAutoSave()));
1050 $this->worldManager->setAutoSaveInterval($this->configGroup->getPropertyInt(Yml::TICKS_PER_AUTOSAVE, $this->worldManager->getAutoSaveInterval()));
1051
1052 $this->updater = new UpdateChecker($this, $this->configGroup->getPropertyString(Yml::AUTO_UPDATER_HOST, "update.pmmp.io"));
1053
1054 $this->queryInfo = new QueryInfo($this);
1055
1056 $this->playerDataProvider = new DatFilePlayerDataProvider(Path::join($this->dataPath, "players"));
1057
1058 register_shutdown_function($this->crashDump(...));
1059
1060 $loadErrorCount = 0;
1061 $this->pluginManager->loadPlugins($this->pluginPath, $loadErrorCount);
1062 if($loadErrorCount > 0){
1063 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_plugin_someLoadErrors()));
1064 $this->forceShutdownExit();
1065 return;
1066 }
1067 if(!$this->enablePlugins(PluginEnableOrder::STARTUP)){
1068 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_plugin_someEnableErrors()));
1069 $this->forceShutdownExit();
1070 return;
1071 }
1072
1073 if(!$this->startupPrepareWorlds()){
1074 $this->forceShutdownExit();
1075 return;
1076 }
1077
1078 if(!$this->enablePlugins(PluginEnableOrder::POSTWORLD)){
1079 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_plugin_someEnableErrors()));
1080 $this->forceShutdownExit();
1081 return;
1082 }
1083
1084 if(!$this->startupPrepareNetworkInterfaces()){
1085 $this->forceShutdownExit();
1086 return;
1087 }
1088
1089 if($this->configGroup->getPropertyBool(Yml::ANONYMOUS_STATISTICS_ENABLED, true)){
1090 $this->sendUsageTicker = self::TICKS_PER_STATS_REPORT;
1091 $this->sendUsage(SendUsageTask::TYPE_OPEN);
1092 }
1093
1094 $this->configGroup->save();
1095
1096 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_defaultGameMode($this->getGamemode()->getTranslatableName())));
1097 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_donate(TextFormat::AQUA . "https://patreon.com/pocketminemp" . TextFormat::RESET)));
1098 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_startFinished(strval(round(microtime(true) - $this->startTime, 3)))));
1099
1100 $forwarder = new BroadcastLoggerForwarder($this, $this->logger, $this->language);
1101 $this->subscribeToBroadcastChannel(self::BROADCAST_CHANNEL_ADMINISTRATIVE, $forwarder);
1102 $this->subscribeToBroadcastChannel(self::BROADCAST_CHANNEL_USERS, $forwarder);
1103
1104 //TODO: move console parts to a separate component
1105 if($this->configGroup->getPropertyBool(Yml::CONSOLE_ENABLE_INPUT, true)){
1106 $this->console = new ConsoleReaderChildProcessDaemon($this->logger);
1107 }
1108
1109 $this->tickProcessor();
1110 $this->forceShutdown();
1111 }catch(\Throwable $e){
1112 $this->exceptionHandler($e);
1113 }
1114 }
1115
1116 private function startupPrepareWorlds() : bool{
1117 $getGenerator = function(string $generatorName, string $generatorOptions, string $worldName) : ?string{
1118 $generatorEntry = GeneratorManager::getInstance()->getGenerator($generatorName);
1119 if($generatorEntry === null){
1120 $this->logger->error($this->language->translate(KnownTranslationFactory::pocketmine_level_generationError(
1121 $worldName,
1122 KnownTranslationFactory::pocketmine_level_unknownGenerator($generatorName)
1123 )));
1124 return null;
1125 }
1126 try{
1127 $generatorEntry->validateGeneratorOptions($generatorOptions);
1128 }catch(InvalidGeneratorOptionsException $e){
1129 $this->logger->error($this->language->translate(KnownTranslationFactory::pocketmine_level_generationError(
1130 $worldName,
1131 KnownTranslationFactory::pocketmine_level_invalidGeneratorOptions($generatorOptions, $generatorName, $e->getMessage())
1132 )));
1133 return null;
1134 }
1135 return $generatorEntry->getGeneratorClass();
1136 };
1137
1138 $anyWorldFailedToLoad = false;
1139
1140 foreach(Utils::promoteKeys((array) $this->configGroup->getProperty(Yml::WORLDS, [])) as $name => $options){
1141 if(!is_string($name)){
1142 //TODO: this probably should be an error
1143 continue;
1144 }
1145 if($options === null){
1146 $options = [];
1147 }elseif(!is_array($options)){
1148 //TODO: this probably should be an error
1149 continue;
1150 }
1151 if(!$this->worldManager->loadWorld($name, true)){
1152 if($this->worldManager->isWorldGenerated($name)){
1153 //allow checking if other worlds are loadable, so the user gets all the errors in one go
1154 $anyWorldFailedToLoad = true;
1155 continue;
1156 }
1157 $creationOptions = WorldCreationOptions::create();
1158 //TODO: error checking
1159
1160 $generatorName = $options["generator"] ?? "default";
1161 $generatorOptions = isset($options["preset"]) && is_string($options["preset"]) ? $options["preset"] : "";
1162
1163 $generatorClass = $getGenerator($generatorName, $generatorOptions, $name);
1164 if($generatorClass === null){
1165 $anyWorldFailedToLoad = true;
1166 continue;
1167 }
1168 $creationOptions->setGeneratorClass($generatorClass);
1169 $creationOptions->setGeneratorOptions($generatorOptions);
1170
1171 $creationOptions->setDifficulty($this->getDifficulty());
1172 if(isset($options["difficulty"]) && is_string($options["difficulty"])){
1173 $creationOptions->setDifficulty(World::getDifficultyFromString($options["difficulty"]));
1174 }
1175
1176 if(isset($options["seed"])){
1177 $convertedSeed = Generator::convertSeed((string) ($options["seed"] ?? ""));
1178 if($convertedSeed !== null){
1179 $creationOptions->setSeed($convertedSeed);
1180 }
1181 }
1182
1183 $this->worldManager->generateWorld($name, $creationOptions);
1184 }
1185 }
1186
1187 if($this->worldManager->getDefaultWorld() === null){
1188 $default = $this->configGroup->getConfigString(ServerProperties::DEFAULT_WORLD_NAME, "world");
1189 if(trim($default) === ""){
1190 $this->logger->warning("level-name cannot be null, using default");
1191 $default = "world";
1192 $this->configGroup->setConfigString(ServerProperties::DEFAULT_WORLD_NAME, "world");
1193 }
1194 if(!$this->worldManager->loadWorld($default, true)){
1195 if($this->worldManager->isWorldGenerated($default)){
1196 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_level_defaultError()));
1197
1198 return false;
1199 }
1200 $generatorName = $this->configGroup->getConfigString(ServerProperties::DEFAULT_WORLD_GENERATOR);
1201 $generatorOptions = $this->configGroup->getConfigString(ServerProperties::DEFAULT_WORLD_GENERATOR_SETTINGS);
1202 $generatorClass = $getGenerator($generatorName, $generatorOptions, $default);
1203
1204 if($generatorClass === null){
1205 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_level_defaultError()));
1206 return false;
1207 }
1208 $creationOptions = WorldCreationOptions::create()
1209 ->setGeneratorClass($generatorClass)
1210 ->setGeneratorOptions($generatorOptions);
1211 $convertedSeed = Generator::convertSeed($this->configGroup->getConfigString(ServerProperties::DEFAULT_WORLD_SEED));
1212 if($convertedSeed !== null){
1213 $creationOptions->setSeed($convertedSeed);
1214 }
1215 $creationOptions->setDifficulty($this->getDifficulty());
1216 $this->worldManager->generateWorld($default, $creationOptions);
1217 }
1218
1219 $world = $this->worldManager->getWorldByName($default);
1220 if($world === null){
1221 throw new AssumptionFailedError("We just loaded/generated the default world, so it must exist");
1222 }
1223 $this->worldManager->setDefaultWorld($world);
1224 }
1225
1226 return !$anyWorldFailedToLoad;
1227 }
1228
1229 private function startupPrepareConnectableNetworkInterfaces(
1230 string $ip,
1231 int $port,
1232 bool $ipV6,
1233 bool $useQuery,
1234 PacketBroadcaster $packetBroadcaster,
1235 EntityEventBroadcaster $entityEventBroadcaster,
1236 TypeConverter $typeConverter
1237 ) : bool{
1238 $prettyIp = $ipV6 ? "[$ip]" : $ip;
1239 try{
1240 $rakLibRegistered = $this->network->registerInterface(new RakLibInterface($this, $ip, $port, $ipV6, $packetBroadcaster, $entityEventBroadcaster, $typeConverter));
1241 }catch(NetworkInterfaceStartException $e){
1242 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_networkStartFailed(
1243 $ip,
1244 (string) $port,
1245 $e->getMessage()
1246 )));
1247 return false;
1248 }
1249 if($rakLibRegistered){
1250 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_networkStart($prettyIp, (string) $port)));
1251 }
1252 if($useQuery){
1253 if(!$rakLibRegistered){
1254 //RakLib would normally handle the transport for Query packets
1255 //if it's not registered we need to make sure Query still works
1256 $this->network->registerInterface(new DedicatedQueryNetworkInterface($ip, $port, $ipV6, new \PrefixedLogger($this->logger, "Dedicated Query Interface")));
1257 }
1258 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_server_query_running($prettyIp, (string) $port)));
1259 }
1260 return true;
1261 }
1262
1263 private function startupPrepareNetworkInterfaces() : bool{
1264 $useQuery = $this->configGroup->getConfigBool(ServerProperties::ENABLE_QUERY, true);
1265
1266 $typeConverter = TypeConverter::getInstance();
1267 $packetBroadcaster = new StandardPacketBroadcaster($this);
1268 $entityEventBroadcaster = new StandardEntityEventBroadcaster($packetBroadcaster, $typeConverter);
1269
1270 if(
1271 !$this->startupPrepareConnectableNetworkInterfaces($this->getIp(), $this->getPort(), false, $useQuery, $packetBroadcaster, $entityEventBroadcaster, $typeConverter) ||
1272 (
1273 $this->configGroup->getConfigBool(ServerProperties::ENABLE_IPV6, true) &&
1274 !$this->startupPrepareConnectableNetworkInterfaces($this->getIpV6(), $this->getPortV6(), true, $useQuery, $packetBroadcaster, $entityEventBroadcaster, $typeConverter)
1275 )
1276 ){
1277 return false;
1278 }
1279
1280 if($useQuery){
1281 $this->network->registerRawPacketHandler(new QueryHandler($this));
1282 }
1283
1284 foreach($this->getIPBans()->getEntries() as $entry){
1285 $this->network->blockAddress($entry->getName(), -1);
1286 }
1287
1288 if($this->configGroup->getPropertyBool(Yml::NETWORK_UPNP_FORWARDING, false)){
1289 $this->network->registerInterface(new UPnPNetworkInterface($this->logger, Internet::getInternalIP(), $this->getPort()));
1290 }
1291
1292 return true;
1293 }
1294
1299 public function subscribeToBroadcastChannel(string $channelId, CommandSender $subscriber) : void{
1300 $this->broadcastSubscribers[$channelId][spl_object_id($subscriber)] = $subscriber;
1301 }
1302
1306 public function unsubscribeFromBroadcastChannel(string $channelId, CommandSender $subscriber) : void{
1307 if(isset($this->broadcastSubscribers[$channelId][spl_object_id($subscriber)])){
1308 if(count($this->broadcastSubscribers[$channelId]) === 1){
1309 unset($this->broadcastSubscribers[$channelId]);
1310 }else{
1311 unset($this->broadcastSubscribers[$channelId][spl_object_id($subscriber)]);
1312 }
1313 }
1314 }
1315
1319 public function unsubscribeFromAllBroadcastChannels(CommandSender $subscriber) : void{
1320 foreach(Utils::stringifyKeys($this->broadcastSubscribers) as $channelId => $recipients){
1321 $this->unsubscribeFromBroadcastChannel($channelId, $subscriber);
1322 }
1323 }
1324
1331 public function getBroadcastChannelSubscribers(string $channelId) : array{
1332 return $this->broadcastSubscribers[$channelId] ?? [];
1333 }
1334
1338 public function broadcastMessage(Translatable|string $message, ?array $recipients = null) : int{
1339 $recipients = $recipients ?? $this->getBroadcastChannelSubscribers(self::BROADCAST_CHANNEL_USERS);
1340
1341 foreach($recipients as $recipient){
1342 $recipient->sendMessage($message);
1343 }
1344
1345 return count($recipients);
1346 }
1347
1351 private function getPlayerBroadcastSubscribers(string $channelId) : array{
1353 $players = [];
1354 foreach($this->broadcastSubscribers[$channelId] as $subscriber){
1355 if($subscriber instanceof Player){
1356 $players[spl_object_id($subscriber)] = $subscriber;
1357 }
1358 }
1359 return $players;
1360 }
1361
1365 public function broadcastTip(string $tip, ?array $recipients = null) : int{
1366 $recipients = $recipients ?? $this->getPlayerBroadcastSubscribers(self::BROADCAST_CHANNEL_USERS);
1367
1368 foreach($recipients as $recipient){
1369 $recipient->sendTip($tip);
1370 }
1371
1372 return count($recipients);
1373 }
1374
1378 public function broadcastPopup(string $popup, ?array $recipients = null) : int{
1379 $recipients = $recipients ?? $this->getPlayerBroadcastSubscribers(self::BROADCAST_CHANNEL_USERS);
1380
1381 foreach($recipients as $recipient){
1382 $recipient->sendPopup($popup);
1383 }
1384
1385 return count($recipients);
1386 }
1387
1394 public function broadcastTitle(string $title, string $subtitle = "", int $fadeIn = -1, int $stay = -1, int $fadeOut = -1, ?array $recipients = null) : int{
1395 $recipients = $recipients ?? $this->getPlayerBroadcastSubscribers(self::BROADCAST_CHANNEL_USERS);
1396
1397 foreach($recipients as $recipient){
1398 $recipient->sendTitle($title, $subtitle, $fadeIn, $stay, $fadeOut);
1399 }
1400
1401 return count($recipients);
1402 }
1403
1417 public function prepareBatch(string $buffer, Compressor $compressor, ?bool $sync = null, ?TimingsHandler $timings = null) : CompressBatchPromise|string{
1418 $timings ??= Timings::$playerNetworkSendCompress;
1419 try{
1420 $timings->startTiming();
1421
1422 $threshold = $compressor->getCompressionThreshold();
1423 if($threshold === null || strlen($buffer) < $compressor->getCompressionThreshold()){
1424 $compressionType = CompressionAlgorithm::NONE;
1425 $compressed = $buffer;
1426
1427 }else{
1428 $sync ??= !$this->networkCompressionAsync;
1429
1430 if(!$sync && strlen($buffer) >= $this->networkCompressionAsyncThreshold){
1431 $promise = new CompressBatchPromise();
1432 $task = new CompressBatchTask($buffer, $promise, $compressor);
1433 $this->asyncPool->submitTask($task);
1434 return $promise;
1435 }
1436
1437 $compressionType = $compressor->getNetworkId();
1438 $compressed = $compressor->compress($buffer);
1439 }
1440
1441 return chr($compressionType) . $compressed;
1442 }finally{
1443 $timings->stopTiming();
1444 }
1445 }
1446
1447 public function enablePlugins(PluginEnableOrder $type) : bool{
1448 $allSuccess = true;
1449 foreach($this->pluginManager->getPlugins() as $plugin){
1450 if(!$plugin->isEnabled() && $plugin->getDescription()->getOrder() === $type){
1451 if(!$this->pluginManager->enablePlugin($plugin)){
1452 $allSuccess = false;
1453 }
1454 }
1455 }
1456
1457 if($type === PluginEnableOrder::POSTWORLD){
1458 $this->commandMap->registerServerAliases();
1459 }
1460
1461 return $allSuccess;
1462 }
1463
1467 public function dispatchCommand(CommandSender $sender, string $commandLine, bool $internal = false) : bool{
1468 if(!$internal){
1469 $ev = new CommandEvent($sender, $commandLine);
1470 $ev->call();
1471 if($ev->isCancelled()){
1472 return false;
1473 }
1474
1475 $commandLine = $ev->getCommand();
1476 }
1477
1478 return $this->commandMap->dispatch($sender, $commandLine);
1479 }
1480
1484 public function shutdown() : void{
1485 if($this->isRunning){
1486 $this->isRunning = false;
1487 $this->signalHandler->unregister();
1488 }
1489 }
1490
1491 private function forceShutdownExit() : void{
1492 $this->forceShutdown();
1493 Process::kill(Process::pid());
1494 }
1495
1496 public function forceShutdown() : void{
1497 if($this->hasStopped){
1498 return;
1499 }
1500
1501 if($this->doTitleTick){
1502 echo "\x1b]0;\x07";
1503 }
1504
1505 if($this->isRunning){
1506 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_server_forcingShutdown()));
1507 }
1508 try{
1509 if(!$this->isRunning()){
1510 $this->sendUsage(SendUsageTask::TYPE_CLOSE);
1511 }
1512
1513 $this->hasStopped = true;
1514
1515 $this->shutdown();
1516
1517 if(isset($this->pluginManager)){
1518 $this->logger->debug("Disabling all plugins");
1519 $this->pluginManager->disablePlugins();
1520 }
1521
1522 if(isset($this->network)){
1523 $this->network->getSessionManager()->close($this->configGroup->getPropertyString(Yml::SETTINGS_SHUTDOWN_MESSAGE, "Server closed"));
1524 }
1525
1526 if(isset($this->worldManager)){
1527 $this->logger->debug("Unloading all worlds");
1528 foreach($this->worldManager->getWorlds() as $world){
1529 $this->worldManager->unloadWorld($world, true);
1530 }
1531 }
1532
1533 $this->logger->debug("Removing event handlers");
1534 HandlerListManager::global()->unregisterAll();
1535
1536 if(isset($this->asyncPool)){
1537 $this->logger->debug("Shutting down async task worker pool");
1538 $this->asyncPool->shutdown();
1539 }
1540
1541 if(isset($this->configGroup)){
1542 $this->logger->debug("Saving properties");
1543 $this->configGroup->save();
1544 }
1545
1546 if($this->console !== null){
1547 $this->logger->debug("Closing console");
1548 $this->console->quit();
1549 }
1550
1551 if(isset($this->network)){
1552 $this->logger->debug("Stopping network interfaces");
1553 foreach($this->network->getInterfaces() as $interface){
1554 $this->logger->debug("Stopping network interface " . get_class($interface));
1555 $this->network->unregisterInterface($interface);
1556 }
1557 }
1558 }catch(\Throwable $e){
1559 $this->logger->logException($e);
1560 $this->logger->emergency("Crashed while crashing, killing process");
1561 @Process::kill(Process::pid());
1562 }
1563
1564 }
1565
1566 public function getQueryInformation() : QueryInfo{
1567 return $this->queryInfo;
1568 }
1569
1574 public function exceptionHandler(\Throwable $e, ?array $trace = null) : void{
1575 while(@ob_end_flush()){}
1576 global $lastError;
1577
1578 if($trace === null){
1579 $trace = $e->getTrace();
1580 }
1581
1582 //If this is a thread crash, this logs where the exception came from on the main thread, as opposed to the
1583 //crashed thread. This is intentional, and might be useful for debugging
1584 //Assume that the thread already logged the original exception with the correct stack trace
1585 $this->logger->logException($e, $trace);
1586
1587 if($e instanceof ThreadCrashException){
1588 $info = $e->getCrashInfo();
1589 $type = $info->getType();
1590 $errstr = $info->getMessage();
1591 $errfile = $info->getFile();
1592 $errline = $info->getLine();
1593 $printableTrace = $info->getTrace();
1594 $thread = $info->getThreadName();
1595 }else{
1596 $type = get_class($e);
1597 $errstr = $e->getMessage();
1598 $errfile = $e->getFile();
1599 $errline = $e->getLine();
1600 $printableTrace = Utils::printableTraceWithMetadata($trace);
1601 $thread = "Main";
1602 }
1603
1604 $errstr = preg_replace('/\s+/', ' ', trim($errstr));
1605
1606 $lastError = [
1607 "type" => $type,
1608 "message" => $errstr,
1609 "fullFile" => $errfile,
1610 "file" => Filesystem::cleanPath($errfile),
1611 "line" => $errline,
1612 "trace" => $printableTrace,
1613 "thread" => $thread
1614 ];
1615
1616 global $lastExceptionError, $lastError;
1617 $lastExceptionError = $lastError;
1618 $this->crashDump();
1619 }
1620
1621 private function writeCrashDumpFile(CrashDump $dump) : string{
1622 $crashFolder = Path::join($this->dataPath, "crashdumps");
1623 if(!is_dir($crashFolder)){
1624 mkdir($crashFolder);
1625 }
1626 $crashDumpPath = Path::join($crashFolder, date("D_M_j-H.i.s-T_Y", (int) $dump->getData()->time) . ".log");
1627
1628 $fp = @fopen($crashDumpPath, "wb");
1629 if(!is_resource($fp)){
1630 throw new \RuntimeException("Unable to open new file to generate crashdump");
1631 }
1632 $writer = new CrashDumpRenderer($fp, $dump->getData());
1633 $writer->renderHumanReadable();
1634 $dump->encodeData($writer);
1635
1636 fclose($fp);
1637 return $crashDumpPath;
1638 }
1639
1640 public function crashDump() : void{
1641 while(@ob_end_flush()){}
1642 if(!$this->isRunning){
1643 return;
1644 }
1645 if($this->sendUsageTicker > 0){
1646 $this->sendUsage(SendUsageTask::TYPE_CLOSE);
1647 }
1648 $this->hasStopped = false;
1649
1650 ini_set("error_reporting", '0');
1651 ini_set("memory_limit", '-1'); //Fix error dump not dumped on memory problems
1652 try{
1653 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_crash_create()));
1654 $dump = new CrashDump($this, $this->pluginManager ?? null);
1655
1656 $crashDumpPath = $this->writeCrashDumpFile($dump);
1657
1658 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_crash_submit($crashDumpPath)));
1659
1660 if($this->configGroup->getPropertyBool(Yml::AUTO_REPORT_ENABLED, true)){
1661 $report = true;
1662
1663 $stamp = Path::join($this->dataPath, "crashdumps", ".last_crash");
1664 $crashInterval = 120; //2 minutes
1665 if(($lastReportTime = @filemtime($stamp)) !== false && $lastReportTime + $crashInterval >= time()){
1666 $report = false;
1667 $this->logger->debug("Not sending crashdump due to last crash less than $crashInterval seconds ago");
1668 }
1669 @touch($stamp); //update file timestamp
1670
1671 if($dump->getData()->error["type"] === \ParseError::class){
1672 $report = false;
1673 }
1674
1675 if(strrpos(VersionInfo::GIT_HASH(), "-dirty") !== false || VersionInfo::GIT_HASH() === str_repeat("00", 20)){
1676 $this->logger->debug("Not sending crashdump due to locally modified");
1677 $report = false; //Don't send crashdumps for locally modified builds
1678 }
1679
1680 if($report){
1681 $url = ($this->configGroup->getPropertyBool(Yml::AUTO_REPORT_USE_HTTPS, true) ? "https" : "http") . "://" . $this->configGroup->getPropertyString(Yml::AUTO_REPORT_HOST, "crash.pmmp.io") . "/submit/api";
1682 $postUrlError = "Unknown error";
1683 $reply = Internet::postURL($url, [
1684 "report" => "yes",
1685 "name" => $this->getName() . " " . $this->getPocketMineVersion(),
1686 "email" => "[email protected]",
1687 "reportPaste" => base64_encode($dump->getEncodedData())
1688 ], 10, [], $postUrlError);
1689
1690 if($reply !== null && is_object($data = json_decode($reply->getBody()))){
1691 if(isset($data->crashId) && is_int($data->crashId) && isset($data->crashUrl) && is_string($data->crashUrl)){
1692 $reportId = $data->crashId;
1693 $reportUrl = $data->crashUrl;
1694 $this->logger->emergency($this->language->translate(KnownTranslationFactory::pocketmine_crash_archive($reportUrl, (string) $reportId)));
1695 }elseif(isset($data->error) && is_string($data->error)){
1696 $this->logger->emergency("Automatic crash report submission failed: $data->error");
1697 }else{
1698 $this->logger->emergency("Invalid JSON response received from crash archive: " . $reply->getBody());
1699 }
1700 }else{
1701 $this->logger->emergency("Failed to communicate with crash archive: $postUrlError");
1702 }
1703 }
1704 }
1705 }catch(\Throwable $e){
1706 $this->logger->logException($e);
1707 try{
1708 $this->logger->critical($this->language->translate(KnownTranslationFactory::pocketmine_crash_error($e->getMessage())));
1709 }catch(\Throwable $e){}
1710 }
1711
1712 $this->forceShutdown();
1713 $this->isRunning = false;
1714
1715 //Force minimum uptime to be >= 120 seconds, to reduce the impact of spammy crash loops
1716 $uptime = time() - ((int) $this->startTime);
1717 $minUptime = 120;
1718 $spacing = $minUptime - $uptime;
1719 if($spacing > 0){
1720 echo "--- Uptime {$uptime}s - waiting {$spacing}s to throttle automatic restart (you can kill the process safely now) ---" . PHP_EOL;
1721 sleep($spacing);
1722 }
1723 @Process::kill(Process::pid());
1724 exit(1);
1725 }
1726
1730 public function __debugInfo() : array{
1731 return [];
1732 }
1733
1734 public function getTickSleeper() : SleeperHandler{
1735 return $this->tickSleeper;
1736 }
1737
1738 private function tickProcessor() : void{
1739 $this->nextTick = microtime(true);
1740
1741 while($this->isRunning){
1742 $this->tick();
1743
1744 //sleeps are self-correcting - if we undersleep 1ms on this tick, we'll sleep an extra ms on the next tick
1745 $this->tickSleeper->sleepUntil($this->nextTick);
1746 }
1747 }
1748
1749 public function addOnlinePlayer(Player $player) : bool{
1750 $ev = new PlayerLoginEvent($player, "Plugin reason");
1751 $ev->call();
1752 if($ev->isCancelled() || !$player->isConnected()){
1753 $player->disconnect($ev->getKickMessage());
1754
1755 return false;
1756 }
1757
1758 $session = $player->getNetworkSession();
1759 $position = $player->getPosition();
1760 $this->logger->info($this->language->translate(KnownTranslationFactory::pocketmine_player_logIn(
1761 TextFormat::AQUA . $player->getName() . TextFormat::RESET,
1762 $session->getIp(),
1763 (string) $session->getPort(),
1764 (string) $player->getId(),
1765 $position->getWorld()->getDisplayName(),
1766 (string) round($position->x, 4),
1767 (string) round($position->y, 4),
1768 (string) round($position->z, 4)
1769 )));
1770
1771 foreach($this->playerList as $p){
1772 $p->getNetworkSession()->onPlayerAdded($player);
1773 }
1774 $rawUUID = $player->getUniqueId()->getBytes();
1775 $this->playerList[$rawUUID] = $player;
1776
1777 if($this->sendUsageTicker > 0){
1778 $this->uniquePlayers[$rawUUID] = $rawUUID;
1779 }
1780
1781 return true;
1782 }
1783
1784 public function removeOnlinePlayer(Player $player) : void{
1785 if(isset($this->playerList[$rawUUID = $player->getUniqueId()->getBytes()])){
1786 unset($this->playerList[$rawUUID]);
1787 foreach($this->playerList as $p){
1788 $p->getNetworkSession()->onPlayerRemoved($player);
1789 }
1790 }
1791 }
1792
1793 public function sendUsage(int $type = SendUsageTask::TYPE_STATUS) : void{
1794 if($this->configGroup->getPropertyBool(Yml::ANONYMOUS_STATISTICS_ENABLED, true)){
1795 $this->asyncPool->submitTask(new SendUsageTask($this, $type, $this->uniquePlayers));
1796 }
1797 $this->uniquePlayers = [];
1798 }
1799
1800 public function getLanguage() : Language{
1801 return $this->language;
1802 }
1803
1804 public function isLanguageForced() : bool{
1805 return $this->forceLanguage;
1806 }
1807
1808 public function getNetwork() : Network{
1809 return $this->network;
1810 }
1811
1812 public function getMemoryManager() : MemoryManager{
1813 return $this->memoryManager;
1814 }
1815
1816 private function titleTick() : void{
1817 Timings::$titleTick->startTiming();
1818
1819 $u = Process::getAdvancedMemoryUsage();
1820 $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());
1821
1822 $online = count($this->playerList);
1823 $connecting = $this->network->getConnectionCount() - $online;
1824 $bandwidthStats = $this->network->getBandwidthTracker();
1825
1826 echo "\x1b]0;" . $this->getName() . " " .
1827 $this->getPocketMineVersion() .
1828 " | Online $online/" . $this->maxPlayers .
1829 ($connecting > 0 ? " (+$connecting connecting)" : "") .
1830 " | Memory " . $usage .
1831 " | U " . round($bandwidthStats->getSend()->getAverageBytes() / 1024, 2) .
1832 " D " . round($bandwidthStats->getReceive()->getAverageBytes() / 1024, 2) .
1833 " kB/s | TPS " . $this->getTicksPerSecondAverage() .
1834 " | Load " . $this->getTickUsageAverage() . "%\x07";
1835
1836 Timings::$titleTick->stopTiming();
1837 }
1838
1842 private function tick() : void{
1843 $tickTime = microtime(true);
1844 if(($tickTime - $this->nextTick) < -0.025){ //Allow half a tick of diff
1845 return;
1846 }
1847
1848 Timings::$serverTick->startTiming();
1849
1850 ++$this->tickCounter;
1851
1852 Timings::$scheduler->startTiming();
1853 $this->pluginManager->tickSchedulers($this->tickCounter);
1854 Timings::$scheduler->stopTiming();
1855
1856 Timings::$schedulerAsync->startTiming();
1857 $this->asyncPool->collectTasks();
1858 Timings::$schedulerAsync->stopTiming();
1859
1860 $this->worldManager->tick($this->tickCounter);
1861
1862 Timings::$connection->startTiming();
1863 $this->network->tick();
1864 Timings::$connection->stopTiming();
1865
1866 if(($this->tickCounter % self::TARGET_TICKS_PER_SECOND) === 0){
1867 if($this->doTitleTick){
1868 $this->titleTick();
1869 }
1870 $this->currentTPS = self::TARGET_TICKS_PER_SECOND;
1871 $this->currentUse = 0;
1872
1873 $queryRegenerateEvent = new QueryRegenerateEvent(new QueryInfo($this));
1874 $queryRegenerateEvent->call();
1875 $this->queryInfo = $queryRegenerateEvent->getQueryInfo();
1876
1877 $this->network->updateName();
1878 $this->network->getBandwidthTracker()->rotateAverageHistory();
1879 }
1880
1881 if($this->sendUsageTicker > 0 && --$this->sendUsageTicker === 0){
1882 $this->sendUsageTicker = self::TICKS_PER_STATS_REPORT;
1883 $this->sendUsage(SendUsageTask::TYPE_STATUS);
1884 }
1885
1886 if(($this->tickCounter % self::TICKS_PER_WORLD_CACHE_CLEAR) === 0){
1887 foreach($this->worldManager->getWorlds() as $world){
1888 $world->clearCache();
1889 }
1890 }
1891
1892 if(($this->tickCounter % self::TICKS_PER_TPS_OVERLOAD_WARNING) === 0 && $this->getTicksPerSecondAverage() < self::TPS_OVERLOAD_WARNING_THRESHOLD){
1893 $this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_tickOverload()));
1894 }
1895
1896 $this->memoryManager->check();
1897
1898 if($this->console !== null){
1899 Timings::$serverCommand->startTiming();
1900 while(($line = $this->console->readLine()) !== null){
1901 $this->consoleSender ??= new ConsoleCommandSender($this, $this->language);
1902 $this->dispatchCommand($this->consoleSender, $line);
1903 }
1904 Timings::$serverCommand->stopTiming();
1905 }
1906
1907 Timings::$serverTick->stopTiming();
1908
1909 $now = microtime(true);
1910 $totalTickTimeSeconds = $now - $tickTime + ($this->tickSleeper->getNotificationProcessingTime() / 1_000_000_000);
1911 $this->currentTPS = min(self::TARGET_TICKS_PER_SECOND, 1 / max(0.001, $totalTickTimeSeconds));
1912 $this->currentUse = min(1, $totalTickTimeSeconds / self::TARGET_SECONDS_PER_TICK);
1913
1914 TimingsHandler::tick($this->currentTPS <= $this->profilingTickRate);
1915
1916 $idx = $this->tickCounter % self::TARGET_TICKS_PER_SECOND;
1917 $this->tickAverage[$idx] = $this->currentTPS;
1918 $this->useAverage[$idx] = $this->currentUse;
1919 $this->tickSleeper->resetNotificationProcessingTime();
1920
1921 if(($this->nextTick - $tickTime) < -1){
1922 $this->nextTick = $tickTime;
1923 }else{
1924 $this->nextTick += self::TARGET_SECONDS_PER_TICK;
1925 }
1926 }
1927}
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)