server = $server; $this->dataPath = $dataPath; $this->providerManager = $providerManager; } public function getProviderManager() : WorldProviderManager{ return $this->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->cancel(); } $ev->call(); if(!$forceUnload and $ev->isCancelled()){ return false; } $this->server->getLogger()->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_unloading($world->getDisplayName()))); try{ $safeSpawn = $this->defaultWorld !== null ? $this->defaultWorld->getSafeSpawn() : null; }catch(WorldException $e){ $safeSpawn = null; } foreach($world->getPlayers() as $player){ if($world === $this->defaultWorld or $safeSpawn === null){ $player->disconnect("Forced default world unload"); }else{ $player->teleport($safeSpawn); } } if($world === $this->defaultWorld){ $this->defaultWorld = null; } unset($this->worlds[$world->getId()]); $world->onUnload(); 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()->translate(KnownTranslationFactory::pocketmine_level_loadError( $name, count($providers) === 0 ? KnownTranslationFactory::pocketmine_level_unknownFormat() : KnownTranslationFactory::pocketmine_level_ambiguousFormat(implode(", ", array_keys($providers))) ))); return false; } $providerClass = array_shift($providers); try{ $provider = $providerClass->fromPath($path); }catch(CorruptedWorldException $e){ $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_loadError($name, "Corruption detected: " . $e->getMessage()))); return false; }catch(UnsupportedWorldFormatException $e){ $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::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()->translate(KnownTranslationFactory::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(), Path::join($this->server->getDataPath(), "backups", "worlds"), $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->server->getAsyncPool()); $this->worlds[$world->getId()] = $world; $world->setAutoSave($this->autoSave); (new WorldLoadEvent($world))->call(); return true; } /** * Generates a new world if it does not exist * * @throws \InvalidArgumentException */ public function generateWorld(string $name, WorldCreationOptions $options, bool $backgroundGeneration = true) : bool{ if(trim($name) === "" or $this->isWorldGenerated($name)){ return false; } $providerEntry = $this->providerManager->getDefault(); $path = $this->getWorldPath($name); $providerEntry->generate($path, $name, $options); $world = new World($this->server, $name, $providerEntry->fromPath($path), $this->server->getAsyncPool()); $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()->translate(KnownTranslationFactory::pocketmine_level_backgroundGeneration($name))); $spawnLocation = $world->getSpawnLocation(); $centerX = $spawnLocation->getFloorX() >> 4; $centerZ = $spawnLocation->getFloorZ() >> 4; $selected = iterator_to_array((new ChunkSelector())->selectChunks(8, $centerX, $centerZ)); $done = 0; $total = count($selected); foreach($selected as $index){ World::getXZ($index, $chunkX, $chunkZ); $world->orderChunkPopulation($chunkX, $chunkZ, null)->onCompletion( static function() use ($world, &$done, $total) : void{ $oldProgress = (int) floor(($done / $total) * 100); $newProgress = (int) floor((++$done / $total) * 100); if(intdiv($oldProgress, 10) !== intdiv($newProgress, 10) || $done === $total || $done === 1){ $world->getLogger()->info("Generating spawn terrain chunks: $done / $total ($newProgress%)"); } }, static function() : void{ //NOOP: we don't care if the world was unloaded }); } } return true; } private function getWorldPath(string $name) : string{ return Path::join($this->dataPath, $name) . "/"; //TODO: check if we still need the trailing dirsep (I'm a little scared to remove it) } 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->isLoaded()); 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::$worldSave->startTiming(); foreach($this->worlds as $world){ foreach($world->getPlayers() as $player){ if($player->spawned){ $player->save(); } } $world->save(false); } Timings::$worldSave->stopTiming(); } }