diff --git a/src/pocketmine/MemoryManager.php b/src/pocketmine/MemoryManager.php index d6e03a393c..fcf29504af 100644 --- a/src/pocketmine/MemoryManager.php +++ b/src/pocketmine/MemoryManager.php @@ -206,13 +206,13 @@ class MemoryManager{ $this->server->getLogger()->debug(sprintf("[Memory Manager] %sLow memory triggered, limit %gMB, using %gMB", $global ? "Global " : "", round(($limit / 1024) / 1024, 2), round(($memory / 1024) / 1024, 2))); if($this->lowMemClearWorldCache){ - foreach($this->server->getLevels() as $level){ + foreach($this->server->getLevelManager()->getLevels() as $level){ $level->clearCache(true); } } if($this->lowMemChunkGC){ - foreach($this->server->getLevels() as $level){ + foreach($this->server->getLevelManager()->getLevels() as $level){ $level->doChunkGarbageCollection(); } } diff --git a/src/pocketmine/Player.php b/src/pocketmine/Player.php index 37604a4135..1ddd2db4b0 100644 --- a/src/pocketmine/Player.php +++ b/src/pocketmine/Player.php @@ -1137,7 +1137,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ if($this->hasValidSpawnPosition()){ return $this->spawnPosition; }else{ - $level = $this->server->getDefaultLevel(); + $level = $this->server->getLevelManager()->getDefaultLevel(); return $level->getSafeSpawn(); } @@ -1837,9 +1837,9 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ public function _actuallyConstruct(){ $namedtag = $this->server->getOfflinePlayerData($this->username); //TODO: make this async - if(($level = $this->server->getLevelByName($namedtag->getString("Level", "", true))) === null){ + if(($level = $this->server->getLevelManager()->getLevelByName($namedtag->getString("Level", "", true))) === null){ /** @var Level $level */ - $level = $this->server->getDefaultLevel(); //TODO: default level may be null + $level = $this->server->getLevelManager()->getDefaultLevel(); //TODO: default level may be null $spawnLocation = $level->getSafeSpawn(); $namedtag->setTag(new ListTag("Pos", [ @@ -1910,7 +1910,7 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{ } if(!$this->hasValidSpawnPosition()){ - if(($level = $this->server->getLevelByName($nbt->getString("SpawnLevel", ""))) instanceof Level){ + if(($level = $this->server->getLevelManager()->getLevelByName($nbt->getString("SpawnLevel", ""))) instanceof Level){ $this->spawnPosition = new Position($nbt->getInt("SpawnX"), $nbt->getInt("SpawnY"), $nbt->getInt("SpawnZ"), $level); }else{ $this->spawnPosition = $this->level->getSafeSpawn(); diff --git a/src/pocketmine/Server.php b/src/pocketmine/Server.php index 059d243bd6..f8709ca67c 100644 --- a/src/pocketmine/Server.php +++ b/src/pocketmine/Server.php @@ -33,12 +33,9 @@ use pocketmine\command\CommandSender; use pocketmine\command\ConsoleCommandSender; use pocketmine\command\PluginIdentifiableCommand; use pocketmine\command\SimpleCommandMap; -use pocketmine\entity\Entity; use pocketmine\entity\EntityFactory; use pocketmine\entity\Skin; use pocketmine\event\HandlerList; -use pocketmine\event\level\LevelInitEvent; -use pocketmine\event\level\LevelLoadEvent; use pocketmine\event\player\PlayerDataSaveEvent; use pocketmine\event\server\CommandEvent; use pocketmine\event\server\DataPacketBroadcastEvent; @@ -51,13 +48,12 @@ use pocketmine\lang\Language; use pocketmine\lang\LanguageNotFoundException; use pocketmine\lang\TextContainer; use pocketmine\level\biome\Biome; -use pocketmine\level\format\io\exception\UnsupportedLevelFormatException; -use pocketmine\level\format\io\LevelProvider; use pocketmine\level\format\io\LevelProviderManager; use pocketmine\level\generator\Generator; use pocketmine\level\generator\GeneratorManager; +use pocketmine\level\generator\normal\Normal; use pocketmine\level\Level; -use pocketmine\level\LevelException; +use pocketmine\level\LevelManager; use pocketmine\metadata\EntityMetadataStore; use pocketmine\metadata\LevelMetadataStore; use pocketmine\metadata\PlayerMetadataStore; @@ -113,14 +109,10 @@ use pocketmine\utils\TextFormat; use pocketmine\utils\Utils; use pocketmine\utils\UUID; use function array_key_exists; -use function array_keys; use function array_shift; use function array_sum; -use function asort; -use function assert; use function base64_encode; use function bin2hex; -use function class_exists; use function count; use function define; use function explode; @@ -129,7 +121,6 @@ use function file_exists; use function file_get_contents; use function file_put_contents; use function filemtime; -use function floor; use function function_exists; use function gc_collect_cycles; use function get_class; @@ -141,7 +132,6 @@ use function ini_set; use function is_array; use function is_bool; use function is_string; -use function is_subclass_of; use function json_decode; use function max; use function microtime; @@ -151,7 +141,6 @@ use function pcntl_signal; use function pcntl_signal_dispatch; use function preg_replace; use function random_bytes; -use function random_int; use function realpath; use function register_shutdown_function; use function rename; @@ -170,8 +159,6 @@ use function time; use function touch; use function trim; use const DIRECTORY_SEPARATOR; -use const INT32_MAX; -use const INT32_MIN; use const PHP_EOL; use const PHP_INT_MAX; use const PTHREADS_INHERIT_NONE; @@ -266,15 +253,15 @@ class Server{ /** @var ResourcePackManager */ private $resourceManager; + /** @var LevelManager */ + private $levelManager; + /** @var int */ private $maxPlayers; /** @var bool */ private $onlineMode = true; - /** @var bool */ - private $autoSave; - /** @var RCON */ private $rcon; @@ -292,20 +279,6 @@ class Server{ /** @var bool */ private $networkCompressionAsync = true; - /** @var bool */ - private $autoTickRate = true; - /** @var int */ - private $autoTickRateLimit = 20; - /** @var bool */ - private $alwaysTickPlayers = false; - /** @var int */ - private $baseTickRate = 1; - - /** @var int */ - private $autoSaveTicker = 0; - /** @var int */ - private $autoSaveTicks = 6000; - /** @var Language */ private $language; /** @var bool */ @@ -347,12 +320,6 @@ class Server{ /** @var Player[] */ private $playerList = []; - /** @var Level[] */ - private $levels = []; - - /** @var Level */ - private $levelDefault = null; - /** * @return string */ @@ -481,37 +448,6 @@ class Server{ return $this->serverID; } - /** - * @return bool - */ - public function getAutoSave() : bool{ - return $this->autoSave; - } - - /** - * @param bool $value - */ - public function setAutoSave(bool $value){ - $this->autoSave = $value; - foreach($this->getLevels() as $level){ - $level->setAutoSave($this->autoSave); - } - } - - /** - * @return string - */ - public function getLevelType() : string{ - return $this->getConfigString("level-type", "DEFAULT"); - } - - /** - * @return bool - */ - public function getGenerateStructures() : bool{ - return $this->getConfigBool("generate-structures", true); - } - /** * @return int */ @@ -632,6 +568,13 @@ class Server{ return $this->resourceManager; } + /** + * @return LevelManager + */ + public function getLevelManager() : LevelManager{ + return $this->levelManager; + } + public function getAsyncPool() : AsyncPool{ return $this->asyncPool; } @@ -752,7 +695,7 @@ class Server{ $this->logger->notice($this->getLanguage()->translateString("pocketmine.data.playerNotFound", [$name])); } } - $spawn = $this->getDefaultLevel()->getSafeSpawn(); + $spawn = $this->levelManager->getDefaultLevel()->getSafeSpawn(); $currentTimeMillis = (int) (microtime(true) * 1000); $nbt = new CompoundTag("", [ @@ -763,7 +706,7 @@ class Server{ new DoubleTag("", $spawn->y), new DoubleTag("", $spawn->z) ], NBT::TAG_Double), - new StringTag("Level", $this->getDefaultLevel()->getFolderName()), + new StringTag("Level", $this->levelManager->getDefaultLevel()->getFolderName()), //new StringTag("SpawnLevel", $this->getDefaultLevel()->getFolderName()), //new IntTag("SpawnX", $spawn->getFloorX()), //new IntTag("SpawnY", $spawn->getFloorY()), @@ -898,255 +841,6 @@ class Server{ return $this->getPlayerByRawUUID($uuid->toBinary()); } - /** - * @return Level[] - */ - public function getLevels() : array{ - return $this->levels; - } - - /** - * @return Level|null - */ - public function getDefaultLevel() : ?Level{ - return $this->levelDefault; - } - - /** - * Sets the default level to a different level - * This won't change the level-name property, - * it only affects the server on runtime - * - * @param Level|null $level - */ - public function setDefaultLevel(?Level $level) : void{ - if($level === null or ($this->isLevelLoaded($level->getFolderName()) and $level !== $this->levelDefault)){ - $this->levelDefault = $level; - } - } - - /** - * @param string $name - * - * @return bool - */ - public function isLevelLoaded(string $name) : bool{ - return $this->getLevelByName($name) instanceof Level; - } - - /** - * @param int $levelId - * - * @return Level|null - */ - public function getLevel(int $levelId) : ?Level{ - return $this->levels[$levelId] ?? null; - } - - /** - * NOTE: This matches levels based on the FOLDER name, NOT the display name. - * - * @param string $name - * - * @return Level|null - */ - public function getLevelByName(string $name) : ?Level{ - foreach($this->getLevels() as $level){ - if($level->getFolderName() === $name){ - return $level; - } - } - - return null; - } - - /** - * @param Level $level - * @param bool $forceUnload - * - * @return bool - * - * @throws \InvalidStateException - */ - public function unloadLevel(Level $level, bool $forceUnload = false) : bool{ - if($level === $this->getDefaultLevel() and !$forceUnload){ - throw new \InvalidStateException("The default level cannot be unloaded while running, please switch levels."); - } - - return $level->onUnload($forceUnload); - } - - /** - * @internal - * - * @param Level $level - */ - public function removeLevel(Level $level) : void{ - unset($this->levels[$level->getId()]); - } - - /** - * Loads a level from the data directory - * - * @param string $name - * - * @return bool - * - * @throws LevelException - */ - public function loadLevel(string $name) : bool{ - if(trim($name) === ""){ - throw new LevelException("Invalid empty level name"); - } - if($this->isLevelLoaded($name)){ - return true; - }elseif(!$this->isLevelGenerated($name)){ - $this->logger->notice($this->getLanguage()->translateString("pocketmine.level.notFound", [$name])); - - return false; - } - - $path = $this->getDataPath() . "worlds/" . $name . "/"; - - $providers = LevelProviderManager::getMatchingProviders($path); - if(count($providers) !== 1){ - $this->logger->error($this->language->translateString("pocketmine.level.loadError", [ - $name, - empty($providers) ? - $this->language->translateString("pocketmine.level.unknownFormat") : - $this->language->translateString("pocketmine.level.ambiguousFormat", [implode(", ", array_keys($providers))]) - ])); - return false; - } - $providerClass = array_shift($providers); - - try{ - /** @see LevelProvider::__construct() */ - $level = new Level($this, $name, new $providerClass($path)); - }catch(UnsupportedLevelFormatException $e){ - $this->logger->error($this->language->translateString("pocketmine.level.loadError", [$name, $e->getMessage()])); - return false; - } - - $this->levels[$level->getId()] = $level; - - (new LevelLoadEvent($level))->call(); - - $level->setTickRate($this->baseTickRate); - - return true; - } - - /** - * Generates a new level if it does not exist - * - * @param string $name - * @param int|null $seed - * @param string|null $generator Class name that extends pocketmine\level\generator\Generator - * @param array $options - * @param bool $backgroundGeneration - * - * @return bool - */ - public function generateLevel(string $name, int $seed = null, $generator = null, array $options = [], bool $backgroundGeneration = true) : bool{ - if(trim($name) === "" or $this->isLevelGenerated($name)){ - return false; - } - - $seed = $seed ?? random_int(INT32_MIN, INT32_MAX); - - if(!isset($options["preset"])){ - $options["preset"] = $this->getConfigString("generator-settings", ""); - } - - if(!($generator !== null and class_exists($generator, true) and is_subclass_of($generator, Generator::class))){ - $generator = GeneratorManager::getGenerator($this->getLevelType()); - } - - $providerClass = LevelProviderManager::getDefault(); - - $path = $this->getDataPath() . "worlds/" . $name . "/"; - /** @var LevelProvider $providerClass */ - $providerClass::generate($path, $name, $seed, $generator, $options); - - /** @see LevelProvider::__construct() */ - $level = new Level($this, $name, new $providerClass($path)); - $this->levels[$level->getId()] = $level; - - $level->setTickRate($this->baseTickRate); - - (new LevelInitEvent($level))->call(); - - (new LevelLoadEvent($level))->call(); - - if(!$backgroundGeneration){ - return true; - } - - $this->getLogger()->notice($this->getLanguage()->translateString("pocketmine.level.backgroundGeneration", [$name])); - - $spawnLocation = $level->getSpawnLocation(); - $centerX = $spawnLocation->getFloorX() >> 4; - $centerZ = $spawnLocation->getFloorZ() >> 4; - - $order = []; - - for($X = -3; $X <= 3; ++$X){ - for($Z = -3; $Z <= 3; ++$Z){ - $distance = $X ** 2 + $Z ** 2; - $chunkX = $X + $centerX; - $chunkZ = $Z + $centerZ; - $index = Level::chunkHash($chunkX, $chunkZ); - $order[$index] = $distance; - } - } - - asort($order); - - foreach($order as $index => $distance){ - Level::getXZ($index, $chunkX, $chunkZ); - $level->populateChunk($chunkX, $chunkZ, true); - } - - return true; - } - - /** - * @param string $name - * - * @return bool - */ - public function isLevelGenerated(string $name) : bool{ - if(trim($name) === ""){ - return false; - } - $path = $this->getDataPath() . "worlds/" . $name . "/"; - if(!($this->getLevelByName($name) instanceof Level)){ - return !empty(LevelProviderManager::getMatchingProviders($path)); - } - - return true; - } - - /** - * Searches all levels for the entity with the specified ID. - * Useful for tracking entities across multiple worlds without needing strong references. - * - * @param int $entityId - * - * @return Entity|null - */ - public function findEntity(int $entityId){ - foreach($this->levels as $level){ - assert(!$level->isClosed()); - if(($entity = $level->getEntity($entityId)) instanceof Entity){ - return $entity; - } - } - - return null; - } - /** * @param string $variable * @param mixed $defaultValue @@ -1530,11 +1224,6 @@ class Server{ NetworkCipher::$ENABLED = (bool) $this->getProperty("network.enable-encryption", true); - $this->autoTickRate = (bool) $this->getProperty("level-settings.auto-tick-rate", true); - $this->autoTickRateLimit = (int) $this->getProperty("level-settings.auto-tick-rate-limit", 20); - $this->alwaysTickPlayers = (bool) $this->getProperty("level-settings.always-tick-players", false); - $this->baseTickRate = (int) $this->getProperty("level-settings.base-tick-rate", 1); - $this->doTitleTick = ((bool) $this->getProperty("console.title-tick", true)) && Terminal::hasFormattingCodes(); @@ -1583,7 +1272,6 @@ class Server{ $this->banByIP->load(); $this->maxPlayers = $this->getConfigInt("max-players", 20); - $this->setAutoSave($this->getConfigBool("auto-save", true)); $this->onlineMode = $this->getConfigBool("xbox-auth", true); if($this->onlineMode){ @@ -1644,6 +1332,20 @@ class Server{ $this->pluginManager->registerInterface(new PharPluginLoader($this->autoloader)); $this->pluginManager->registerInterface(new ScriptPluginLoader()); + LevelProviderManager::init(); + if(($format = LevelProviderManager::getProviderByName($formatName = (string) $this->getProperty("level-settings.default-format"))) !== null){ + LevelProviderManager::setDefault($format); + }elseif($formatName !== ""){ + $this->logger->warning($this->language->translateString("pocketmine.level.badDefaultFormat", [$formatName])); + } + if(extension_loaded("leveldb")){ + $this->logger->debug($this->getLanguage()->translateString("pocketmine.debug.enable")); + } + + $this->levelManager = new LevelManager($this); + + GeneratorManager::registerDefaultGenerators(); + register_shutdown_function([$this, "crashDump"]); $this->queryRegenerateTask = new QueryRegenerateEvent($this, 5); @@ -1656,26 +1358,13 @@ class Server{ $this->network->registerInterface(new RakLibInterface($this)); - LevelProviderManager::init(); - if(($format = LevelProviderManager::getProviderByName($formatName = (string) $this->getProperty("level-settings.default-format"))) !== null){ - LevelProviderManager::setDefault($format); - }elseif($formatName !== ""){ - $this->logger->warning($this->language->translateString("pocketmine.level.badDefaultFormat", [$formatName])); - } - - if(extension_loaded("leveldb")){ - $this->logger->debug($this->getLanguage()->translateString("pocketmine.debug.enable")); - } - - GeneratorManager::registerDefaultGenerators(); - foreach((array) $this->getProperty("worlds", []) as $name => $options){ if($options === null){ $options = []; }elseif(!is_array($options)){ continue; } - if(!$this->loadLevel($name)){ + if(!$this->levelManager->loadLevel($name)){ if(isset($options["generator"])){ $generatorOptions = explode(":", $options["generator"]); $generator = GeneratorManager::getGenerator(array_shift($generatorOptions)); @@ -1683,42 +1372,43 @@ class Server{ $options["preset"] = implode(":", $generatorOptions); } }else{ - $generator = GeneratorManager::getGenerator("default"); + $generator = Normal::class; } - $this->generateLevel($name, Generator::convertSeed((string) ($options["seed"] ?? "")), $generator, $options); + $this->levelManager->generateLevel($name, Generator::convertSeed((string) ($options["seed"] ?? "")), $generator, $options); } } - if($this->getDefaultLevel() === null){ + if($this->levelManager->getDefaultLevel() === null){ $default = $this->getConfigString("level-name", "world"); if(trim($default) == ""){ $this->getLogger()->warning("level-name cannot be null, using default"); $default = "world"; $this->setConfigString("level-name", "world"); } - if(!$this->loadLevel($default)){ - $this->generateLevel($default, Generator::convertSeed($this->getConfigString("level-seed"))); + if(!$this->levelManager->loadLevel($default)){ + $this->levelManager->generateLevel( + $default, + Generator::convertSeed($this->getConfigString("level-seed")), + GeneratorManager::getGenerator($this->getConfigString("level-type")), + ["preset" => $this->getConfigString("generator-settings")] + ); } - $this->setDefaultLevel($this->getLevelByName($default)); + $level = $this->levelManager->getLevelByName($default); + if($level === null){ + $this->getLogger()->emergency($this->getLanguage()->translateString("pocketmine.level.defaultError")); + $this->forceShutdown(); + + return; + } + $this->levelManager->setDefaultLevel($level); } if($this->properties->hasChanged()){ $this->properties->save(); } - if(!($this->getDefaultLevel() instanceof Level)){ - $this->getLogger()->emergency($this->getLanguage()->translateString("pocketmine.level.defaultError")); - $this->forceShutdown(); - - return; - } - - if($this->getProperty("ticks-per.autosave", 6000) > 0){ - $this->autoSaveTicks = (int) $this->getProperty("ticks-per.autosave", 6000); - } - $this->enablePlugins(PluginLoadOrder::POSTWORLD); $this->start(); @@ -2005,7 +1695,7 @@ class Server{ public function reload(){ $this->logger->info("Saving levels..."); - foreach($this->levels as $level){ + foreach($this->levelManager->getLevels() as $level){ $level->save(); } @@ -2082,8 +1772,8 @@ class Server{ } $this->getLogger()->debug("Unloading all levels"); - foreach($this->getLevels() as $level){ - $this->unloadLevel($level, true); + foreach($this->levelManager->getLevels() as $level){ + $this->levelManager->unloadLevel($level, true); } $this->getLogger()->debug("Removing event handlers"); @@ -2393,69 +2083,6 @@ class Server{ $p->sendDataPacket($pk); } - private function checkTickUpdates(int $currentTick) : void{ - if($this->alwaysTickPlayers){ - foreach($this->players as $p){ - if($p->spawned){ - $p->onUpdate($currentTick); - } - } - } - - //Do level ticks - foreach($this->levels as $k => $level){ - if(!isset($this->levels[$k])){ - // Level unloaded during the tick of a level earlier in this loop, perhaps by plugin - continue; - } - if($level->getTickRate() > $this->baseTickRate and --$level->tickRateCounter > 0){ - continue; - } - - $levelTime = microtime(true); - $level->doTick($currentTick); - $tickMs = (microtime(true) - $levelTime) * 1000; - $level->tickRateTime = $tickMs; - - if($this->autoTickRate){ - if($tickMs < 50 and $level->getTickRate() > $this->baseTickRate){ - $level->setTickRate($r = $level->getTickRate() - 1); - if($r > $this->baseTickRate){ - $level->tickRateCounter = $level->getTickRate(); - } - $this->getLogger()->debug("Raising level \"{$level->getName()}\" tick rate to {$level->getTickRate()} ticks"); - }elseif($tickMs >= 50){ - if($level->getTickRate() === $this->baseTickRate){ - $level->setTickRate(max($this->baseTickRate + 1, min($this->autoTickRateLimit, (int) floor($tickMs / 50)))); - $this->getLogger()->debug(sprintf("Level \"%s\" took %gms, setting tick rate to %d ticks", $level->getName(), (int) round($tickMs, 2), $level->getTickRate())); - }elseif(($tickMs / $level->getTickRate()) >= 50 and $level->getTickRate() < $this->autoTickRateLimit){ - $level->setTickRate($level->getTickRate() + 1); - $this->getLogger()->debug(sprintf("Level \"%s\" took %gms, setting tick rate to %d ticks", $level->getName(), (int) round($tickMs, 2), $level->getTickRate())); - } - $level->tickRateCounter = $level->getTickRate(); - } - } - } - } - - public function doAutoSave(){ - if($this->getAutoSave()){ - Timings::$worldSaveTimer->startTiming(); - foreach($this->players as $index => $player){ - if($player->spawned){ - $player->save(); - }elseif(!$player->isConnected()){ - $this->removePlayer($player); - } - } - - foreach($this->getLevels() as $level){ - $level->save(false); - } - Timings::$worldSaveTimer->stopTiming(); - } - } - public function sendUsage($type = SendUsageTask::TYPE_STATUS){ if((bool) $this->getProperty("anonymous-statistics.enabled", true)){ $this->asyncPool->submitTask(new SendUsageTask($this, $type, $this->uniquePlayers)); @@ -2556,7 +2183,7 @@ class Server{ $this->asyncPool->collectTasks(); Timings::$schedulerAsyncTimer->stopTiming(); - $this->checkTickUpdates($this->tickCounter); + $this->levelManager->tick($this->tickCounter); Timings::$connectionTimer->startTiming(); $this->network->tick(); @@ -2580,18 +2207,13 @@ class Server{ } } - if($this->autoSave and ++$this->autoSaveTicker >= $this->autoSaveTicks){ - $this->autoSaveTicker = 0; - $this->doAutoSave(); - } - if($this->sendUsageTicker > 0 and --$this->sendUsageTicker === 0){ $this->sendUsageTicker = 6000; $this->sendUsage(SendUsageTask::TYPE_STATUS); } if(($this->tickCounter % 100) === 0){ - foreach($this->levels as $level){ + foreach($this->levelManager->getLevels() as $level){ $level->clearCache(); } diff --git a/src/pocketmine/command/defaults/DifficultyCommand.php b/src/pocketmine/command/defaults/DifficultyCommand.php index a03f828ae6..8addfb0a87 100644 --- a/src/pocketmine/command/defaults/DifficultyCommand.php +++ b/src/pocketmine/command/defaults/DifficultyCommand.php @@ -60,7 +60,7 @@ class DifficultyCommand extends VanillaCommand{ $sender->getServer()->setConfigInt("difficulty", $difficulty); //TODO: add per-world support - foreach($sender->getServer()->getLevels() as $level){ + foreach($sender->getServer()->getLevelManager()->getLevels() as $level){ $level->setDifficulty($difficulty); } diff --git a/src/pocketmine/command/defaults/GarbageCollectorCommand.php b/src/pocketmine/command/defaults/GarbageCollectorCommand.php index f6cbc9af7e..7df2d16ed8 100644 --- a/src/pocketmine/command/defaults/GarbageCollectorCommand.php +++ b/src/pocketmine/command/defaults/GarbageCollectorCommand.php @@ -52,7 +52,7 @@ class GarbageCollectorCommand extends VanillaCommand{ $memory = memory_get_usage(); - foreach($sender->getServer()->getLevels() as $level){ + foreach($sender->getServer()->getLevelManager()->getLevels() as $level){ $diff = [count($level->getChunks()), count($level->getEntities()), count($level->getTiles())]; $level->doChunkGarbageCollection(); $level->unloadChunks(true); diff --git a/src/pocketmine/command/defaults/ParticleCommand.php b/src/pocketmine/command/defaults/ParticleCommand.php index 78c586943e..12b939eb39 100644 --- a/src/pocketmine/command/defaults/ParticleCommand.php +++ b/src/pocketmine/command/defaults/ParticleCommand.php @@ -98,7 +98,7 @@ class ParticleCommand extends VanillaCommand{ $this->getRelativeDouble($sender->getZ(), $sender, $args[3]) ); }else{ - $level = $sender->getServer()->getDefaultLevel(); + $level = $sender->getServer()->getLevelManager()->getDefaultLevel(); $pos = new Vector3((float) $args[1], (float) $args[2], (float) $args[3]); } diff --git a/src/pocketmine/command/defaults/SaveCommand.php b/src/pocketmine/command/defaults/SaveCommand.php index a52eabcfc8..d1f4882fbf 100644 --- a/src/pocketmine/command/defaults/SaveCommand.php +++ b/src/pocketmine/command/defaults/SaveCommand.php @@ -49,7 +49,7 @@ class SaveCommand extends VanillaCommand{ $player->save(); } - foreach($sender->getServer()->getLevels() as $level){ + foreach($sender->getServer()->getLevelManager()->getLevels() as $level){ $level->save(true); } diff --git a/src/pocketmine/command/defaults/SaveOffCommand.php b/src/pocketmine/command/defaults/SaveOffCommand.php index c81d169056..e5b8d79e31 100644 --- a/src/pocketmine/command/defaults/SaveOffCommand.php +++ b/src/pocketmine/command/defaults/SaveOffCommand.php @@ -43,7 +43,7 @@ class SaveOffCommand extends VanillaCommand{ return true; } - $sender->getServer()->setAutoSave(false); + $sender->getServer()->getLevelManager()->setAutoSave(false); Command::broadcastCommandMessage($sender, new TranslationContainer("commands.save.disabled")); diff --git a/src/pocketmine/command/defaults/SaveOnCommand.php b/src/pocketmine/command/defaults/SaveOnCommand.php index 8962baa2bb..d79f03be50 100644 --- a/src/pocketmine/command/defaults/SaveOnCommand.php +++ b/src/pocketmine/command/defaults/SaveOnCommand.php @@ -43,7 +43,7 @@ class SaveOnCommand extends VanillaCommand{ return true; } - $sender->getServer()->setAutoSave(true); + $sender->getServer()->getLevelManager()->setAutoSave(true); Command::broadcastCommandMessage($sender, new TranslationContainer("commands.save.enabled")); diff --git a/src/pocketmine/command/defaults/SeedCommand.php b/src/pocketmine/command/defaults/SeedCommand.php index 431c04d48a..5fdaa50b9e 100644 --- a/src/pocketmine/command/defaults/SeedCommand.php +++ b/src/pocketmine/command/defaults/SeedCommand.php @@ -46,7 +46,7 @@ class SeedCommand extends VanillaCommand{ if($sender instanceof Player){ $seed = $sender->getLevel()->getSeed(); }else{ - $seed = $sender->getServer()->getDefaultLevel()->getSeed(); + $seed = $sender->getServer()->getLevelManager()->getDefaultLevel()->getSeed(); } $sender->sendMessage(new TranslationContainer("commands.seed.success", [$seed])); diff --git a/src/pocketmine/command/defaults/SetWorldSpawnCommand.php b/src/pocketmine/command/defaults/SetWorldSpawnCommand.php index 054adc059f..fe50ec205e 100644 --- a/src/pocketmine/command/defaults/SetWorldSpawnCommand.php +++ b/src/pocketmine/command/defaults/SetWorldSpawnCommand.php @@ -59,7 +59,7 @@ class SetWorldSpawnCommand extends VanillaCommand{ return true; } }elseif(count($args) === 3){ - $level = $sender->getServer()->getDefaultLevel(); + $level = $sender->getServer()->getLevelManager()->getDefaultLevel(); $pos = new Vector3($this->getInteger($sender, $args[0]), $this->getInteger($sender, $args[1]), $this->getInteger($sender, $args[2])); }else{ throw new InvalidCommandSyntaxException(); diff --git a/src/pocketmine/command/defaults/StatusCommand.php b/src/pocketmine/command/defaults/StatusCommand.php index 9c258c3cc6..c580fda328 100644 --- a/src/pocketmine/command/defaults/StatusCommand.php +++ b/src/pocketmine/command/defaults/StatusCommand.php @@ -106,7 +106,7 @@ class StatusCommand extends VanillaCommand{ $sender->sendMessage(TextFormat::GOLD . "Maximum memory (manager): " . TextFormat::RED . number_format(round($server->getProperty("memory.global-limit"), 2), 2) . " MB."); } - foreach($server->getLevels() as $level){ + foreach($server->getLevelManager()->getLevels() as $level){ $levelName = $level->getFolderName() !== $level->getName() ? " (" . $level->getName() . ")" : ""; $timeColor = ($level->getTickRate() > 1 or $level->getTickRateTime() > 40) ? TextFormat::RED : TextFormat::YELLOW; $tickRate = $level->getTickRate() > 1 ? " (tick rate " . $level->getTickRate() . ")" : ""; diff --git a/src/pocketmine/command/defaults/TimeCommand.php b/src/pocketmine/command/defaults/TimeCommand.php index 61a9967e14..2289e3f716 100644 --- a/src/pocketmine/command/defaults/TimeCommand.php +++ b/src/pocketmine/command/defaults/TimeCommand.php @@ -54,7 +54,7 @@ class TimeCommand extends VanillaCommand{ return true; } - foreach($sender->getServer()->getLevels() as $level){ + foreach($sender->getServer()->getLevelManager()->getLevels() as $level){ $level->startTime(); } Command::broadcastCommandMessage($sender, "Restarted the time"); @@ -65,7 +65,7 @@ class TimeCommand extends VanillaCommand{ return true; } - foreach($sender->getServer()->getLevels() as $level){ + foreach($sender->getServer()->getLevelManager()->getLevels() as $level){ $level->stopTime(); } Command::broadcastCommandMessage($sender, "Stopped the time"); @@ -79,7 +79,7 @@ class TimeCommand extends VanillaCommand{ if($sender instanceof Player){ $level = $sender->getLevel(); }else{ - $level = $sender->getServer()->getDefaultLevel(); + $level = $sender->getServer()->getLevelManager()->getDefaultLevel(); } $sender->sendMessage(new TranslationContainer("commands.time.query", [$level->getTime()])); return true; @@ -105,7 +105,7 @@ class TimeCommand extends VanillaCommand{ $value = $this->getInteger($sender, $args[1], 0); } - foreach($sender->getServer()->getLevels() as $level){ + foreach($sender->getServer()->getLevelManager()->getLevels() as $level){ $level->setTime($value); } Command::broadcastCommandMessage($sender, new TranslationContainer("commands.time.set", [$value])); @@ -117,7 +117,7 @@ class TimeCommand extends VanillaCommand{ } $value = $this->getInteger($sender, $args[1], 0); - foreach($sender->getServer()->getLevels() as $level){ + foreach($sender->getServer()->getLevelManager()->getLevels() as $level){ $level->setTime($level->getTime() + $value); } Command::broadcastCommandMessage($sender, new TranslationContainer("commands.time.added", [$value])); diff --git a/src/pocketmine/entity/Entity.php b/src/pocketmine/entity/Entity.php index 617f9f4f2c..56846fc478 100644 --- a/src/pocketmine/entity/Entity.php +++ b/src/pocketmine/entity/Entity.php @@ -625,7 +625,7 @@ abstract class Entity extends Location implements Metadatable, EntityIds{ public function getOwningEntity() : ?Entity{ $eid = $this->getOwningEntityId(); if($eid !== null){ - return $this->server->findEntity($eid); + return $this->server->getLevelManager()->findEntity($eid); } return null; @@ -665,7 +665,7 @@ abstract class Entity extends Location implements Metadatable, EntityIds{ public function getTargetEntity() : ?Entity{ $eid = $this->getTargetEntityId(); if($eid !== null){ - return $this->server->findEntity($eid); + return $this->server->getLevelManager()->findEntity($eid); } return null; diff --git a/src/pocketmine/entity/object/ExperienceOrb.php b/src/pocketmine/entity/object/ExperienceOrb.php index 96578acd0f..d962de77fc 100644 --- a/src/pocketmine/entity/object/ExperienceOrb.php +++ b/src/pocketmine/entity/object/ExperienceOrb.php @@ -150,7 +150,7 @@ class ExperienceOrb extends Entity{ return null; } - $entity = $this->server->findEntity($this->targetPlayerRuntimeId); + $entity = $this->server->getLevelManager()->findEntity($this->targetPlayerRuntimeId); if($entity instanceof Human){ return $entity; } diff --git a/src/pocketmine/event/entity/EntityDamageByChildEntityEvent.php b/src/pocketmine/event/entity/EntityDamageByChildEntityEvent.php index 879af9e684..e10e062ba9 100644 --- a/src/pocketmine/event/entity/EntityDamageByChildEntityEvent.php +++ b/src/pocketmine/event/entity/EntityDamageByChildEntityEvent.php @@ -51,6 +51,6 @@ class EntityDamageByChildEntityEvent extends EntityDamageByEntityEvent{ * @return Entity|null */ public function getChild() : ?Entity{ - return $this->getEntity()->getLevel()->getServer()->findEntity($this->childEntityEid); + return $this->getEntity()->getLevel()->getServer()->getLevelManager()->findEntity($this->childEntityEid); } } diff --git a/src/pocketmine/event/entity/EntityDamageByEntityEvent.php b/src/pocketmine/event/entity/EntityDamageByEntityEvent.php index 970818aa5b..a89a0dea71 100644 --- a/src/pocketmine/event/entity/EntityDamageByEntityEvent.php +++ b/src/pocketmine/event/entity/EntityDamageByEntityEvent.php @@ -69,7 +69,7 @@ class EntityDamageByEntityEvent extends EntityDamageEvent{ * @return Entity|null */ public function getDamager() : ?Entity{ - return $this->getEntity()->getLevel()->getServer()->findEntity($this->damagerEntityId); + return $this->getEntity()->getLevel()->getServer()->getLevelManager()->findEntity($this->damagerEntityId); } /** diff --git a/src/pocketmine/event/server/QueryRegenerateEvent.php b/src/pocketmine/event/server/QueryRegenerateEvent.php index beef6b987b..c54d51166b 100644 --- a/src/pocketmine/event/server/QueryRegenerateEvent.php +++ b/src/pocketmine/event/server/QueryRegenerateEvent.php @@ -88,7 +88,8 @@ class QueryRegenerateEvent extends ServerEvent{ $this->gametype = ($server->getGamemode() & 0x01) === 0 ? "SMP" : "CMP"; $this->version = $server->getVersion(); $this->server_engine = $server->getName() . " " . $server->getPocketMineVersion(); - $this->map = $server->getDefaultLevel() === null ? "unknown" : $server->getDefaultLevel()->getName(); + $level = $server->getLevelManager()->getDefaultLevel(); + $this->map = $level === null ? "unknown" : $level->getName(); $this->numPlayers = count($this->players); $this->maxPlayers = $server->getMaxPlayers(); $this->whitelist = $server->hasWhitelist() ? "on" : "off"; diff --git a/src/pocketmine/level/Level.php b/src/pocketmine/level/Level.php index 374ece7d84..664bacca41 100644 --- a/src/pocketmine/level/Level.php +++ b/src/pocketmine/level/Level.php @@ -40,7 +40,6 @@ use pocketmine\event\level\ChunkLoadEvent; use pocketmine\event\level\ChunkPopulateEvent; use pocketmine\event\level\ChunkUnloadEvent; use pocketmine\event\level\LevelSaveEvent; -use pocketmine\event\level\LevelUnloadEvent; use pocketmine\event\level\SpawnChangeEvent; use pocketmine\event\player\PlayerInteractEvent; use pocketmine\item\Item; @@ -359,7 +358,6 @@ class Level implements ChunkManager, Metadatable{ $this->levelId = static::$levelIdCounter++; $this->blockMetadata = new BlockMetadataStore($this); $this->server = $server; - $this->autoSave = $server->getAutoSave(); $this->provider = $provider; @@ -455,6 +453,9 @@ class Level implements ChunkManager, Metadatable{ return $this->closed; } + /** + * @internal + */ public function close(){ if($this->closed){ throw new \InvalidStateException("Tried to close a level which is already closed"); @@ -558,52 +559,6 @@ class Level implements ChunkManager, Metadatable{ $this->autoSave = $value; } - /** - * @internal DO NOT use this from plugins, it's for internal use only. Use Server->unloadLevel() instead. - * - * @param bool $force default false, force unload of default level - * - * @return bool - * @throws \InvalidStateException if trying to unload a level during level tick - */ - public function onUnload(bool $force = false) : bool{ - if($this->doingTick and !$force){ - throw new \InvalidStateException("Cannot unload a level during level tick"); - } - - $ev = new LevelUnloadEvent($this); - - if($this === $this->server->getDefaultLevel() and !$force){ - $ev->setCancelled(true); - } - - $ev->call(); - - if(!$force and $ev->isCancelled()){ - return false; - } - - $this->server->getLogger()->info($this->server->getLanguage()->translateString("pocketmine.level.unloading", [$this->getName()])); - $defaultLevel = $this->server->getDefaultLevel(); - foreach($this->getPlayers() as $player){ - if($this === $defaultLevel or $defaultLevel === null){ - $player->close($player->getLeaveMessage(), "Forced default level unload"); - }elseif($defaultLevel instanceof Level){ - $player->teleport($this->server->getDefaultLevel()->getSafeSpawn()); - } - } - - if($this === $defaultLevel){ - $this->server->setDefaultLevel(null); - } - - $this->server->removeLevel($this); - - $this->close(); - - return true; - } - /** * Gets the players being used in a specific chunk * @@ -738,6 +693,10 @@ class Level implements ChunkManager, Metadatable{ } } + public function isDoingTick() : bool{ + return $this->doingTick; + } + /** * WARNING: Do not use this, it's only for internal use. * Changes to this function won't be recorded on the version. diff --git a/src/pocketmine/level/LevelManager.php b/src/pocketmine/level/LevelManager.php new file mode 100644 index 0000000000..1631175a24 --- /dev/null +++ b/src/pocketmine/level/LevelManager.php @@ -0,0 +1,439 @@ +server = $server; + + $this->autoTickRate = (bool) $this->server->getProperty("level-settings.auto-tick-rate", $this->autoTickRate); + $this->autoTickRateLimit = (int) $this->server->getProperty("level-settings.auto-tick-rate-limit", $this->autoTickRateLimit); + $this->alwaysTickPlayers = (bool) $this->server->getProperty("level-settings.always-tick-players", $this->alwaysTickPlayers); + $this->baseTickRate = (int) $this->server->getProperty("level-settings.base-tick-rate", $this->baseTickRate); + + $this->autoSave = $this->server->getConfigBool("auto-save", $this->autoSave); + $this->autoSaveTicks = (int) $this->server->getProperty("ticks-per.autosave", 6000); + } + + /** + * @return Level[] + */ + public function getLevels() : array{ + return $this->levels; + } + + /** + * @return Level|null + */ + public function getDefaultLevel() : ?Level{ + return $this->levelDefault; + } + + /** + * Sets the default level to a different level + * This won't change the level-name property, + * it only affects the server on runtime + * + * @param Level|null $level + */ + public function setDefaultLevel(?Level $level) : void{ + if($level === null or ($this->isLevelLoaded($level->getFolderName()) and $level !== $this->levelDefault)){ + $this->levelDefault = $level; + } + } + + /** + * @param string $name + * + * @return bool + */ + public function isLevelLoaded(string $name) : bool{ + return $this->getLevelByName($name) instanceof Level; + } + + /** + * @param int $levelId + * + * @return Level|null + */ + public function getLevel(int $levelId) : ?Level{ + return $this->levels[$levelId] ?? null; + } + + /** + * NOTE: This matches levels based on the FOLDER name, NOT the display name. + * + * @param string $name + * + * @return Level|null + */ + public function getLevelByName(string $name) : ?Level{ + foreach($this->levels as $level){ + if($level->getFolderName() === $name){ + return $level; + } + } + + return null; + } + + /** + * @param Level $level + * @param bool $forceUnload + * + * @return bool + * + * @throws \InvalidArgumentException + */ + public function unloadLevel(Level $level, bool $forceUnload = false) : bool{ + if($level === $this->getDefaultLevel() and !$forceUnload){ + throw new \InvalidArgumentException("The default level cannot be unloaded while running, please switch levels."); + } + if($level->isDoingTick()){ + throw new \InvalidArgumentException("Cannot unload a level during level tick"); + } + + $ev = new LevelUnloadEvent($level); + if($level === $this->levelDefault and !$forceUnload){ + $ev->setCancelled(true); + } + + $ev->call(); + + if(!$forceUnload and $ev->isCancelled()){ + return false; + } + + $this->server->getLogger()->info($this->server->getLanguage()->translateString("pocketmine.level.unloading", [$level->getName()])); + foreach($level->getPlayers() as $player){ + if($level === $this->levelDefault or $this->levelDefault === null){ + $player->close($player->getLeaveMessage(), "Forced default level unload"); + }elseif($this->levelDefault instanceof Level){ + $player->teleport($this->levelDefault->getSafeSpawn()); + } + } + + if($level === $this->levelDefault){ + $this->levelDefault = null; + } + unset($this->levels[$level->getId()]); + + $level->close(); + return true; + } + + /** + * Loads a level from the data directory + * + * @param string $name + * + * @return bool + * + * @throws LevelException + */ + public function loadLevel(string $name) : bool{ + if(trim($name) === ""){ + throw new LevelException("Invalid empty level name"); + } + if($this->isLevelLoaded($name)){ + return true; + }elseif(!$this->isLevelGenerated($name)){ + $this->server->getLogger()->notice($this->server->getLanguage()->translateString("pocketmine.level.notFound", [$name])); + + return false; + } + + $path = $this->server->getDataPath() . "worlds/" . $name . "/"; + + $providers = LevelProviderManager::getMatchingProviders($path); + if(count($providers) !== 1){ + $this->server->getLogger()->error($this->server->getLanguage()->translateString("pocketmine.level.loadError", [ + $name, + empty($providers) ? + $this->server->getLanguage()->translateString("pocketmine.level.unknownFormat") : + $this->server->getLanguage()->translateString("pocketmine.level.ambiguousFormat", [implode(", ", array_keys($providers))]) + ])); + return false; + } + $providerClass = array_shift($providers); + + try{ + /** @see LevelProvider::__construct() */ + $level = new Level($this->server, $name, new $providerClass($path)); + }catch(UnsupportedLevelFormatException $e){ + $this->server->getLogger()->error($this->server->getLanguage()->translateString("pocketmine.level.loadError", [$name, $e->getMessage()])); + return false; + } + + $this->levels[$level->getId()] = $level; + $level->setTickRate($this->baseTickRate); + $level->setAutoSave($this->autoSave); + + (new LevelLoadEvent($level))->call(); + + return true; + } + + /** + * Generates a new level if it does not exist + * + * @param string $name + * @param int|null $seed + * @param string $generator Class name that extends pocketmine\level\generator\Generator + * @param array $options + * @param bool $backgroundGeneration + * + * @return bool + * @throws \InvalidArgumentException + */ + public function generateLevel(string $name, int $seed = null, string $generator = Normal::class, array $options = [], bool $backgroundGeneration = true) : bool{ + if(trim($name) === "" or $this->isLevelGenerated($name)){ + return false; + } + + $seed = $seed ?? random_int(INT32_MIN, INT32_MAX); + + Utils::testValidInstance($generator, Generator::class); + + $providerClass = LevelProviderManager::getDefault(); + + $path = $this->server->getDataPath() . "worlds/" . $name . "/"; + /** @var LevelProvider $providerClass */ + $providerClass::generate($path, $name, $seed, $generator, $options); + + /** @see LevelProvider::__construct() */ + $level = new Level($this->server, $name, new $providerClass($path)); + $this->levels[$level->getId()] = $level; + + $level->setTickRate($this->baseTickRate); + $level->setAutoSave($this->autoSave); + + (new LevelInitEvent($level))->call(); + + (new LevelLoadEvent($level))->call(); + + if(!$backgroundGeneration){ + return true; + } + + $this->server->getLogger()->notice($this->server->getLanguage()->translateString("pocketmine.level.backgroundGeneration", [$name])); + + $spawnLocation = $level->getSpawnLocation(); + $centerX = $spawnLocation->getFloorX() >> 4; + $centerZ = $spawnLocation->getFloorZ() >> 4; + + $order = []; + + for($X = -3; $X <= 3; ++$X){ + for($Z = -3; $Z <= 3; ++$Z){ + $distance = $X ** 2 + $Z ** 2; + $chunkX = $X + $centerX; + $chunkZ = $Z + $centerZ; + $index = Level::chunkHash($chunkX, $chunkZ); + $order[$index] = $distance; + } + } + + asort($order); + + foreach($order as $index => $distance){ + Level::getXZ($index, $chunkX, $chunkZ); + $level->populateChunk($chunkX, $chunkZ, true); + } + + return true; + } + + /** + * @param string $name + * + * @return bool + */ + public function isLevelGenerated(string $name) : bool{ + if(trim($name) === ""){ + return false; + } + $path = $this->server->getDataPath() . "worlds/" . $name . "/"; + if(!($this->getLevelByName($name) instanceof Level)){ + return !empty(LevelProviderManager::getMatchingProviders($path)); + } + + return true; + } + + /** + * Searches all levels for the entity with the specified ID. + * Useful for tracking entities across multiple worlds without needing strong references. + * + * @param int $entityId + * + * @return Entity|null + */ + public function findEntity(int $entityId){ + foreach($this->levels as $level){ + assert(!$level->isClosed()); + if(($entity = $level->getEntity($entityId)) instanceof Entity){ + return $entity; + } + } + + return null; + } + + + public function tick(int $currentTick) : void{ + foreach($this->levels as $k => $level){ + if(!isset($this->levels[$k])){ + // Level unloaded during the tick of a level earlier in this loop, perhaps by plugin + continue; + } + if($level->getTickRate() > $this->baseTickRate and --$level->tickRateCounter > 0){ + if($this->alwaysTickPlayers){ + foreach($level->getPlayers() as $p){ + if($p->spawned){ + $p->onUpdate($currentTick); + } + } + } + continue; + } + + $levelTime = microtime(true); + $level->doTick($currentTick); + $tickMs = (microtime(true) - $levelTime) * 1000; + $level->tickRateTime = $tickMs; + + if($this->autoTickRate){ + if($tickMs < 50 and $level->getTickRate() > $this->baseTickRate){ + $level->setTickRate($r = $level->getTickRate() - 1); + if($r > $this->baseTickRate){ + $level->tickRateCounter = $level->getTickRate(); + } + $this->server->getLogger()->debug("Raising level \"{$level->getName()}\" tick rate to {$level->getTickRate()} ticks"); + }elseif($tickMs >= 50){ + if($level->getTickRate() === $this->baseTickRate){ + $level->setTickRate(max($this->baseTickRate + 1, min($this->autoTickRateLimit, (int) floor($tickMs / 50)))); + $this->server->getLogger()->debug(sprintf("Level \"%s\" took %gms, setting tick rate to %d ticks", $level->getName(), (int) round($tickMs, 2), $level->getTickRate())); + }elseif(($tickMs / $level->getTickRate()) >= 50 and $level->getTickRate() < $this->autoTickRateLimit){ + $level->setTickRate($level->getTickRate() + 1); + $this->server->getLogger()->debug(sprintf("Level \"%s\" took %gms, setting tick rate to %d ticks", $level->getName(), (int) round($tickMs, 2), $level->getTickRate())); + } + $level->tickRateCounter = $level->getTickRate(); + } + } + } + + if($this->autoSave and ++$this->autoSaveTicker >= $this->autoSaveTicks){ + $this->autoSaveTicker = 0; + $this->doAutoSave(); + } + } + + + /** + * @return bool + */ + public function getAutoSave() : bool{ + return $this->autoSave; + } + + /** + * @param bool $value + */ + public function setAutoSave(bool $value){ + $this->autoSave = $value; + foreach($this->levels as $level){ + $level->setAutoSave($this->autoSave); + } + } + + private function doAutoSave() : void{ + Timings::$worldSaveTimer->startTiming(); + foreach($this->levels as $level){ + foreach($level->getPlayers() as $player){ + if($player->spawned){ + $player->save(); + }elseif(!$player->isConnected()){ //TODO: check if this is ever possible + $this->server->removePlayer($player); + } + } + $level->save(false); + } + Timings::$worldSaveTimer->stopTiming(); + } +} diff --git a/src/pocketmine/level/generator/GeneratorManager.php b/src/pocketmine/level/generator/GeneratorManager.php index 5e24b1faaa..fbf8fb5327 100644 --- a/src/pocketmine/level/generator/GeneratorManager.php +++ b/src/pocketmine/level/generator/GeneratorManager.php @@ -25,8 +25,8 @@ namespace pocketmine\level\generator; use pocketmine\level\generator\hell\Nether; use pocketmine\level\generator\normal\Normal; +use pocketmine\utils\Utils; use function array_keys; -use function is_subclass_of; use function strtolower; final class GeneratorManager{ @@ -48,11 +48,11 @@ final class GeneratorManager{ * @param string $class Fully qualified name of class that extends \pocketmine\level\generator\Generator * @param string $name Alias for this generator type that can be written in configs * @param bool $overwrite Whether to force overwriting any existing registered generator with the same name + * + * @throws \InvalidArgumentException */ public static function addGenerator(string $class, string $name, bool $overwrite = false) : void{ - if(!is_subclass_of($class, Generator::class)){ - throw new \InvalidArgumentException("Class $class does not extend " . Generator::class); - } + Utils::testValidInstance($class, Generator::class); if(!$overwrite and isset(self::$list[$name = strtolower($name)])){ throw new \InvalidArgumentException("Alias \"$name\" is already assigned"); diff --git a/src/pocketmine/timings/TimingsHandler.php b/src/pocketmine/timings/TimingsHandler.php index 4018ee7302..4667cb2311 100644 --- a/src/pocketmine/timings/TimingsHandler.php +++ b/src/pocketmine/timings/TimingsHandler.php @@ -64,7 +64,7 @@ class TimingsHandler{ $entities = 0; $livingEntities = 0; - foreach(Server::getInstance()->getLevels() as $level){ + foreach(Server::getInstance()->getLevelManager()->getLevels() as $level){ $entities += count($level->getEntities()); foreach($level->getEntities() as $e){ if($e instanceof Living){