22declare(strict_types=1);
41use Symfony\Component\Filesystem\Path;
42use
function array_keys;
43use
function array_shift;
49use
function iterator_to_array;
50use
function microtime;
60 private array $worlds = [];
61 private ?
World $defaultWorld =
null;
63 private bool $autoSave =
true;
64 private int $autoSaveTicks = self::TICKS_PER_AUTOSAVE;
65 private int $autoSaveTicker = 0;
67 public function __construct(
69 private string $dataPath,
74 return $this->providerManager;
84 public function getDefaultWorld() : ?
World{
85 return $this->defaultWorld;
94 if($world === null || ($this->isWorldLoaded($world->getFolderName()) && $world !== $this->defaultWorld)){
95 $this->defaultWorld = $world;
99 public function isWorldLoaded(
string $name) : bool{
100 return $this->getWorldByName($name) instanceof
World;
103 public function getWorld(
int $worldId) : ?World{
104 return $this->worlds[$worldId] ?? null;
111 foreach($this->worlds as $world){
112 if($world->getFolderName() === $name){
124 if($world === $this->getDefaultWorld() && !$forceUnload){
125 throw new \InvalidArgumentException(
"The default world cannot be unloaded while running, please switch worlds.");
127 if($world->isDoingTick()){
128 throw new \InvalidArgumentException(
"Cannot unload a world during world tick");
131 $ev =
new WorldUnloadEvent($world);
134 if(!$forceUnload && $ev->isCancelled()){
138 $this->server->getLogger()->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_unloading($world->getDisplayName())));
139 if(count($world->getPlayers()) !== 0){
141 $safeSpawn = $this->defaultWorld !==
null && $this->defaultWorld !== $world ? $this->defaultWorld->getSafeSpawn() :
null;
142 }
catch(WorldException $e){
145 foreach($world->getPlayers() as $player){
146 if($safeSpawn ===
null){
147 $player->disconnect(
"Forced default world unload");
149 $player->teleport($safeSpawn);
154 if($world === $this->defaultWorld){
155 $this->defaultWorld =
null;
157 unset($this->worlds[$world->getId()]);
170 public function loadWorld(
string $name,
bool $autoUpgrade =
false) : bool{
171 if(trim($name) ===
""){
172 throw new \InvalidArgumentException(
"Invalid empty world name");
174 if($this->isWorldLoaded($name)){
176 }elseif(!$this->isWorldGenerated($name)){
180 $path = $this->getWorldPath($name);
182 $providers = $this->providerManager->getMatchingProviders($path);
183 if(count($providers) !== 1){
184 $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_loadError(
186 count($providers) === 0 ?
187 KnownTranslationFactory::pocketmine_level_unknownFormat() :
188 KnownTranslationFactory::pocketmine_level_ambiguousFormat(implode(
", ", array_keys($providers)))
192 $providerClass = array_shift($providers);
195 $provider = $providerClass->fromPath($path,
new \
PrefixedLogger($this->
server->getLogger(),
"World Provider: $name"));
196 }
catch(CorruptedWorldException $e){
197 $this->
server->getLogger()->error($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_loadError(
199 KnownTranslationFactory::pocketmine_level_corrupted($e->getMessage())
202 }
catch(UnsupportedWorldFormatException $e){
203 $this->
server->getLogger()->error($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_loadError(
205 KnownTranslationFactory::pocketmine_level_unsupportedFormat($e->getMessage())
210 $generatorEntry = GeneratorManager::getInstance()->getGenerator($provider->getWorldData()->getGenerator());
211 if($generatorEntry ===
null){
212 $this->
server->getLogger()->error($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_loadError(
214 KnownTranslationFactory::pocketmine_level_unknownGenerator($provider->getWorldData()->getGenerator())
219 $generatorEntry->validateGeneratorOptions($provider->getWorldData()->getGeneratorOptions());
220 }
catch(InvalidGeneratorOptionsException $e){
221 $this->
server->getLogger()->error($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_loadError(
223 KnownTranslationFactory::pocketmine_level_invalidGeneratorOptions(
224 $provider->getWorldData()->getGeneratorOptions(),
225 $provider->getWorldData()->getGenerator(),
231 if(!($provider instanceof WritableWorldProvider)){
233 throw new UnsupportedWorldFormatException(
"World \"$name\" is in an unsupported format and needs to be upgraded");
235 $this->
server->getLogger()->notice($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_conversion_start($name)));
237 $providerClass = $this->providerManager->getDefault();
238 $converter =
new FormatConverter($provider, $providerClass, Path::join($this->
server->getDataPath(),
"backups",
"worlds"), $this->server->getLogger());
239 $converter->execute();
240 $provider = $providerClass->fromPath($path,
new \
PrefixedLogger($this->
server->getLogger(),
"World Provider: $name"));
242 $this->
server->getLogger()->notice($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_conversion_finish($name, $converter->getBackupPath())));
245 $world =
new World($this->server, $name, $provider, $this->
server->getAsyncPool());
247 $this->worlds[$world->getId()] = $world;
248 $world->setAutoSave($this->autoSave);
250 (
new WorldLoadEvent($world))->call();
261 if(trim($name) ===
"" || $this->isWorldGenerated($name)){
265 $providerEntry = $this->providerManager->getDefault();
267 $path = $this->getWorldPath($name);
268 $providerEntry->generate($path, $name, $options);
270 $world =
new World($this->server, $name, $providerEntry->fromPath($path,
new \
PrefixedLogger($this->
server->getLogger(),
"World Provider: $name")), $this->server->getAsyncPool());
271 $this->worlds[$world->getId()] = $world;
273 $world->setAutoSave($this->autoSave);
279 if($backgroundGeneration){
280 $this->
server->getLogger()->notice($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_backgroundGeneration($name)));
282 $spawnLocation = $world->getSpawnLocation();
283 $centerX = $spawnLocation->getFloorX() >> Chunk::COORD_BIT_SIZE;
284 $centerZ = $spawnLocation->getFloorZ() >> Chunk::COORD_BIT_SIZE;
286 $selected = iterator_to_array((
new ChunkSelector())->selectChunks(8, $centerX, $centerZ), preserve_keys:
false);
288 $total = count($selected);
289 foreach($selected as $index){
290 World::getXZ($index, $chunkX, $chunkZ);
291 $world->orderChunkPopulation($chunkX, $chunkZ,
null)->onCompletion(
292 static function() use ($world, &$done, $total) :
void{
293 $oldProgress = (int) floor(($done / $total) * 100);
294 $newProgress = (int) floor((++$done / $total) * 100);
295 if(intdiv($oldProgress, 10) !== intdiv($newProgress, 10) || $done === $total || $done === 1){
296 $world->getLogger()->info($world->getServer()->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_spawnTerrainGenerationProgress(strval($done), strval($total), strval($newProgress))));
299 static function() :
void{
308 private function getWorldPath(
string $name) : string{
309 return Path::join($this->dataPath, $name) .
"/";
312 public function isWorldGenerated(
string $name) : bool{
313 if(trim($name) ===
""){
316 $path = $this->getWorldPath($name);
317 if(!($this->getWorldByName($name) instanceof World)){
318 return count($this->providerManager->getMatchingProviders($path)) > 0;
329 foreach($this->worlds as $world){
330 assert($world->isLoaded());
331 if(($entity = $world->getEntity($entityId)) instanceof
Entity){
339 public function tick(
int $currentTick) : void{
340 foreach($this->worlds as $k => $world){
341 if(!isset($this->worlds[$k])){
346 $worldTime = microtime(
true);
347 $world->doTick($currentTick);
348 $tickMs = (microtime(
true) - $worldTime) * 1000;
349 $world->tickRateTime = $tickMs;
350 if($tickMs >= Server::TARGET_SECONDS_PER_TICK * 1000){
351 $world->getLogger()->debug(sprintf(
"Tick took too long: %gms (%g ticks)", $tickMs, round($tickMs / (Server::TARGET_SECONDS_PER_TICK * 1000), 2)));
355 if($this->autoSave && ++$this->autoSaveTicker >= $this->autoSaveTicks){
356 $this->autoSaveTicker = 0;
357 $this->
server->getLogger()->debug(
"[Auto Save] Saving worlds...");
358 $start = microtime(
true);
360 $time = microtime(
true) - $start;
361 $this->
server->getLogger()->debug(
"[Auto Save] Save completed in " . ($time >= 1 ? round($time, 3) .
"s" : round($time * 1000) .
"ms"));
365 public function getAutoSave() : bool{
366 return $this->autoSave;
369 public function setAutoSave(
bool $value) : void{
370 $this->autoSave = $value;
371 foreach($this->worlds as $world){
372 $world->setAutoSave($this->autoSave);
380 return $this->autoSaveTicks;
383 public function setAutoSaveInterval(
int $autoSaveTicks) : void{
384 if($autoSaveTicks <= 0){
385 throw new \InvalidArgumentException(
"Autosave ticks must be positive");
387 $this->autoSaveTicks = $autoSaveTicks;
390 private function doAutoSave() : void{
391 foreach($this->worlds as $world){
392 foreach($world->getPlayers() as $player){
393 if($player->spawned){
const TARGET_TICKS_PER_SECOND
generateWorld(string $name, WorldCreationOptions $options, bool $backgroundGeneration=true)
getWorldByName(string $name)
unloadWorld(World $world, bool $forceUnload=false)
findEntity(int $entityId)
setDefaultWorld(?World $world)
loadWorld(string $name, bool $autoUpgrade=false)