server = $server; $this->dataPath = $dataPath; $this->providerManager = $providerManager; } /** * @return World[] */ public function getWorlds() : array{ return $this->worlds; } public function getDefaultWorld() : ?World{ return $this->defaultWorld; } /** * Sets the default world to a different world * This won't change the level-name property, * it only affects the server on runtime */ public function setDefaultWorld(?World $world) : void{ if($world === null or ($this->isWorldLoaded($world->getFolderName()) and $world !== $this->defaultWorld)){ $this->defaultWorld = $world; } } public function isWorldLoaded(string $name) : bool{ return $this->getWorldByName($name) instanceof World; } public function getWorld(int $worldId) : ?World{ return $this->worlds[$worldId] ?? null; } /** * NOTE: This matches worlds based on the FOLDER name, NOT the display name. */ public function getWorldByName(string $name) : ?World{ foreach($this->worlds as $world){ if($world->getFolderName() === $name){ return $world; } } return null; } /** * @throws \InvalidArgumentException */ public function unloadWorld(World $world, bool $forceUnload = false) : bool{ if($world === $this->getDefaultWorld() and !$forceUnload){ throw new \InvalidArgumentException("The default world cannot be unloaded while running, please switch worlds."); } if($world->isDoingTick()){ throw new \InvalidArgumentException("Cannot unload a world during world tick"); } $ev = new WorldUnloadEvent($world); if($world === $this->defaultWorld 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", [$world->getDisplayName()])); foreach($world->getPlayers() as $player){ if($world === $this->defaultWorld or $this->defaultWorld === null){ $player->disconnect("Forced default world unload"); }else{ $player->teleport($this->defaultWorld->getSafeSpawn()); } } if($world === $this->defaultWorld){ $this->defaultWorld = null; } unset($this->worlds[$world->getId()]); $world->close(); return true; } /** * Loads a world from the data directory * * @param bool $autoUpgrade Converts worlds to the default format if the world's format is not writable / deprecated * * @throws WorldException */ public function loadWorld(string $name, bool $autoUpgrade = false) : bool{ if(trim($name) === ""){ throw new \InvalidArgumentException("Invalid empty world name"); } if($this->isWorldLoaded($name)){ return true; }elseif(!$this->isWorldGenerated($name)){ return false; } $path = $this->getWorldPath($name); $providers = $this->providerManager->getMatchingProviders($path); if(count($providers) !== 1){ $this->server->getLogger()->error($this->server->getLanguage()->translateString("pocketmine.level.loadError", [ $name, count($providers) === 0 ? $this->server->getLanguage()->translateString("pocketmine.level.unknownFormat") : $this->server->getLanguage()->translateString("pocketmine.level.ambiguousFormat", [implode(", ", array_keys($providers))]) ])); return false; } $providerClass = array_shift($providers); /** * @var WorldProvider $provider * @see WorldProvider::__construct() */ try{ $provider = new $providerClass($path); }catch(CorruptedWorldException $e){ $this->server->getLogger()->error($this->server->getLanguage()->translateString("pocketmine.level.loadError", [$name, "Corruption detected: " . $e->getMessage()])); return false; }catch(UnsupportedWorldFormatException $e){ $this->server->getLogger()->error($this->server->getLanguage()->translateString("pocketmine.level.loadError", [$name, "Unsupported format: " . $e->getMessage()])); return false; } try{ GeneratorManager::getInstance()->getGenerator($provider->getWorldData()->getGenerator(), true); }catch(\InvalidArgumentException $e){ $this->server->getLogger()->error($this->server->getLanguage()->translateString("pocketmine.level.loadError", [$name, "Unknown generator \"" . $provider->getWorldData()->getGenerator() . "\""])); return false; } if(!($provider instanceof WritableWorldProvider)){ if(!$autoUpgrade){ throw new UnsupportedWorldFormatException("World \"$name\" is in an unsupported format and needs to be upgraded"); } $this->server->getLogger()->notice("Upgrading world \"$name\" to new format. This may take a while."); $converter = new FormatConverter($provider, $this->providerManager->getDefault(), $this->server->getDataPath() . "world_conversion_backups", $this->server->getLogger()); $provider = $converter->execute(); $this->server->getLogger()->notice("Upgraded world \"$name\" to new format successfully. Backed up pre-conversion world at " . $converter->getBackupPath()); } $world = new World($this->server, $name, $provider); $this->worlds[$world->getId()] = $world; $world->setAutoSave($this->autoSave); (new WorldLoadEvent($world))->call(); return true; } /** * Generates a new world if it does not exist * * @param string $generator Class name that extends pocketmine\world\generator\Generator * @param mixed[] $options * @phpstan-param class-string $generator * @phpstan-param array $options * * @throws \InvalidArgumentException */ public function generateWorld(string $name, ?int $seed = null, string $generator = Normal::class, array $options = [], bool $backgroundGeneration = true) : bool{ if(trim($name) === "" or $this->isWorldGenerated($name)){ return false; } $seed = $seed ?? random_int(Limits::INT32_MIN, Limits::INT32_MAX); Utils::testValidInstance($generator, Generator::class); $providerClass = $this->providerManager->getDefault(); $path = $this->getWorldPath($name); /** @var WritableWorldProvider $providerClass */ $providerClass::generate($path, $name, $seed, $generator, $options); /** @see WritableWorldProvider::__construct() */ $world = new World($this->server, $name, new $providerClass($path)); $this->worlds[$world->getId()] = $world; $world->setAutoSave($this->autoSave); (new WorldInitEvent($world))->call(); (new WorldLoadEvent($world))->call(); if($backgroundGeneration){ $this->server->getLogger()->notice($this->server->getLanguage()->translateString("pocketmine.level.backgroundGeneration", [$name])); $spawnLocation = $world->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 = World::chunkHash($chunkX, $chunkZ); $order[$index] = $distance; } } asort($order); foreach($order as $index => $distance){ World::getXZ($index, $chunkX, $chunkZ); $world->populateChunk($chunkX, $chunkZ, true); } } return true; } private function getWorldPath(string $name) : string{ return $this->dataPath . "/" . $name . "/"; } public function isWorldGenerated(string $name) : bool{ if(trim($name) === ""){ return false; } $path = $this->getWorldPath($name); if(!($this->getWorldByName($name) instanceof World)){ return count($this->providerManager->getMatchingProviders($path)) > 0; } return true; } /** * Searches all worlds for the entity with the specified ID. * Useful for tracking entities across multiple worlds without needing strong references. */ public function findEntity(int $entityId) : ?Entity{ foreach($this->worlds as $world){ assert(!$world->isClosed()); if(($entity = $world->getEntity($entityId)) instanceof Entity){ return $entity; } } return null; } public function tick(int $currentTick) : void{ foreach($this->worlds as $k => $world){ if(!isset($this->worlds[$k])){ // World unloaded during the tick of a world earlier in this loop, perhaps by plugin continue; } $worldTime = microtime(true); $world->doTick($currentTick); $tickMs = (microtime(true) - $worldTime) * 1000; $world->tickRateTime = $tickMs; if($tickMs >= 50){ $world->getLogger()->debug(sprintf("Tick took too long: %gms (%g ticks)", $tickMs, round($tickMs / 50, 2))); } } if($this->autoSave and ++$this->autoSaveTicker >= $this->autoSaveTicks){ $this->autoSaveTicker = 0; $this->server->getLogger()->debug("[Auto Save] Saving worlds..."); $start = microtime(true); $this->doAutoSave(); $time = microtime(true) - $start; $this->server->getLogger()->debug("[Auto Save] Save completed in " . ($time >= 1 ? round($time, 3) . "s" : round($time * 1000) . "ms")); } } public function getAutoSave() : bool{ return $this->autoSave; } public function setAutoSave(bool $value) : void{ $this->autoSave = $value; foreach($this->worlds as $world){ $world->setAutoSave($this->autoSave); } } /** * Returns the period in ticks after which loaded worlds will be automatically saved to disk. */ public function getAutoSaveInterval() : int{ return $this->autoSaveTicks; } public function setAutoSaveInterval(int $autoSaveTicks) : void{ if($autoSaveTicks <= 0){ throw new \InvalidArgumentException("Autosave ticks must be positive"); } $this->autoSaveTicks = $autoSaveTicks; } private function doAutoSave() : void{ Timings::$worldSaveTimer->startTiming(); foreach($this->worlds as $world){ foreach($world->getPlayers() as $player){ if($player->spawned){ $player->save(); } } $world->save(false); } Timings::$worldSaveTimer->stopTiming(); } }