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 world cannot be unloaded while running, please switch worlds."); } if($level->isDoingTick()){ throw new \InvalidArgumentException("Cannot unload a world during world 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->getDisplayName()])); foreach($level->getPlayers() as $player){ if($level === $this->levelDefault or $this->levelDefault === null){ $player->close($player->getLeaveMessage(), "Forced default world 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 world name"); } if($this->isLevelLoaded($name)){ return true; }elseif(!$this->isLevelGenerated($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 world \"{$level->getDisplayName()}\" 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("World \"%s\" took %gms, setting tick rate to %d ticks", $level->getDisplayName(), (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("World \"%s\" took %gms, setting tick rate to %d ticks", $level->getDisplayName(), (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(); } }