PocketMine-MP 5.23.3 git-f7687af337d001ddbcc47b8e773f014a33faa662
Loading...
Searching...
No Matches
WorldManager.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
24namespace pocketmine\world;
25
41use Symfony\Component\Filesystem\Path;
42use function array_keys;
43use function array_shift;
44use function assert;
45use function count;
46use function floor;
47use function implode;
48use function intdiv;
49use function iterator_to_array;
50use function microtime;
51use function round;
52use function sprintf;
53use function strval;
54use function trim;
55
57 public const TICKS_PER_AUTOSAVE = 300 * Server::TARGET_TICKS_PER_SECOND;
58
63 private array $worlds = [];
64 private ?World $defaultWorld = null;
65
66 private bool $autoSave = true;
67 private int $autoSaveTicks = self::TICKS_PER_AUTOSAVE;
68 private int $autoSaveTicker = 0;
69
70 public function __construct(
71 private Server $server,
72 private string $dataPath,
73 private WorldProviderManager $providerManager
74 ){}
75
76 public function getProviderManager() : WorldProviderManager{
77 return $this->providerManager;
78 }
79
84 public function getWorlds() : array{
85 return $this->worlds;
86 }
87
88 public function getDefaultWorld() : ?World{
89 return $this->defaultWorld;
90 }
91
97 public function setDefaultWorld(?World $world) : void{
98 if($world === null || ($this->isWorldLoaded($world->getFolderName()) && $world !== $this->defaultWorld)){
99 $this->defaultWorld = $world;
100 }
101 }
102
103 public function isWorldLoaded(string $name) : bool{
104 return $this->getWorldByName($name) instanceof World;
105 }
106
107 public function getWorld(int $worldId) : ?World{
108 return $this->worlds[$worldId] ?? null;
109 }
110
114 public function getWorldByName(string $name) : ?World{
115 foreach($this->worlds as $world){
116 if($world->getFolderName() === $name){
117 return $world;
118 }
119 }
120
121 return null;
122 }
123
127 public function unloadWorld(World $world, bool $forceUnload = false) : bool{
128 if($world === $this->getDefaultWorld() && !$forceUnload){
129 throw new \InvalidArgumentException("The default world cannot be unloaded while running, please switch worlds.");
130 }
131 if($world->isDoingTick()){
132 throw new \InvalidArgumentException("Cannot unload a world during world tick");
133 }
134
135 $ev = new WorldUnloadEvent($world);
136 $ev->call();
137
138 if(!$forceUnload && $ev->isCancelled()){
139 return false;
140 }
141
142 $this->server->getLogger()->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_unloading($world->getDisplayName())));
143 if(count($world->getPlayers()) !== 0){
144 try{
145 $safeSpawn = $this->defaultWorld !== null && $this->defaultWorld !== $world ? $this->defaultWorld->getSafeSpawn() : null;
146 }catch(WorldException $e){
147 $safeSpawn = null;
148 }
149 foreach($world->getPlayers() as $player){
150 if($safeSpawn === null){
151 $player->disconnect("Forced default world unload");
152 }else{
153 $player->teleport($safeSpawn);
154 }
155 }
156 }
157
158 if($world === $this->defaultWorld){
159 $this->defaultWorld = null;
160 }
161 unset($this->worlds[$world->getId()]);
162
163 $world->onUnload();
164 return true;
165 }
166
174 public function loadWorld(string $name, bool $autoUpgrade = false) : bool{
175 if(trim($name) === ""){
176 throw new \InvalidArgumentException("Invalid empty world name");
177 }
178 if($this->isWorldLoaded($name)){
179 return true;
180 }elseif(!$this->isWorldGenerated($name)){
181 return false;
182 }
183
184 $path = $this->getWorldPath($name);
185
186 $providers = $this->providerManager->getMatchingProviders($path);
187 if(count($providers) !== 1){
188 $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_loadError(
189 $name,
190 count($providers) === 0 ?
191 KnownTranslationFactory::pocketmine_level_unknownFormat() :
192 KnownTranslationFactory::pocketmine_level_ambiguousFormat(implode(", ", array_keys($providers)))
193 )));
194 return false;
195 }
196 $providerClass = array_shift($providers);
197
198 try{
199 $provider = $providerClass->fromPath($path, new \PrefixedLogger($this->server->getLogger(), "World Provider: $name"));
200 }catch(CorruptedWorldException $e){
201 $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_loadError(
202 $name,
203 KnownTranslationFactory::pocketmine_level_corrupted($e->getMessage())
204 )));
205 return false;
206 }catch(UnsupportedWorldFormatException $e){
207 $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_loadError(
208 $name,
209 KnownTranslationFactory::pocketmine_level_unsupportedFormat($e->getMessage())
210 )));
211 return false;
212 }
213
214 $generatorEntry = GeneratorManager::getInstance()->getGenerator($provider->getWorldData()->getGenerator());
215 if($generatorEntry === null){
216 $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_loadError(
217 $name,
218 KnownTranslationFactory::pocketmine_level_unknownGenerator($provider->getWorldData()->getGenerator())
219 )));
220 return false;
221 }
222 try{
223 $generatorEntry->validateGeneratorOptions($provider->getWorldData()->getGeneratorOptions());
224 }catch(InvalidGeneratorOptionsException $e){
225 $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_loadError(
226 $name,
227 KnownTranslationFactory::pocketmine_level_invalidGeneratorOptions(
228 $provider->getWorldData()->getGeneratorOptions(),
229 $provider->getWorldData()->getGenerator(),
230 $e->getMessage()
231 )
232 )));
233 return false;
234 }
235 if(!($provider instanceof WritableWorldProvider)){
236 if(!$autoUpgrade){
237 throw new UnsupportedWorldFormatException("World \"$name\" is in an unsupported format and needs to be upgraded");
238 }
239 $this->server->getLogger()->notice($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_conversion_start($name)));
240
241 $providerClass = $this->providerManager->getDefault();
242 $converter = new FormatConverter($provider, $providerClass, Path::join($this->server->getDataPath(), "backups", "worlds"), $this->server->getLogger());
243 $converter->execute();
244 $provider = $providerClass->fromPath($path, new \PrefixedLogger($this->server->getLogger(), "World Provider: $name"));
245
246 $this->server->getLogger()->notice($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_conversion_finish($name, $converter->getBackupPath())));
247 }
248
249 $world = new World($this->server, $name, $provider, $this->server->getAsyncPool());
250
251 $this->worlds[$world->getId()] = $world;
252 $world->setAutoSave($this->autoSave);
253
254 (new WorldLoadEvent($world))->call();
255
256 return true;
257 }
258
264 public function generateWorld(string $name, WorldCreationOptions $options, bool $backgroundGeneration = true) : bool{
265 if(trim($name) === "" || $this->isWorldGenerated($name)){
266 return false;
267 }
268
269 $providerEntry = $this->providerManager->getDefault();
270
271 $path = $this->getWorldPath($name);
272 $providerEntry->generate($path, $name, $options);
273
274 $world = new World($this->server, $name, $providerEntry->fromPath($path, new \PrefixedLogger($this->server->getLogger(), "World Provider: $name")), $this->server->getAsyncPool());
275 $this->worlds[$world->getId()] = $world;
276
277 $world->setAutoSave($this->autoSave);
278
279 (new WorldInitEvent($world))->call();
280
281 (new WorldLoadEvent($world))->call();
282
283 if($backgroundGeneration){
284 $this->server->getLogger()->notice($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_backgroundGeneration($name)));
285
286 $spawnLocation = $world->getSpawnLocation();
287 $centerX = $spawnLocation->getFloorX() >> Chunk::COORD_BIT_SIZE;
288 $centerZ = $spawnLocation->getFloorZ() >> Chunk::COORD_BIT_SIZE;
289
290 $selected = iterator_to_array((new ChunkSelector())->selectChunks(8, $centerX, $centerZ), preserve_keys: false);
291 $done = 0;
292 $total = count($selected);
293 foreach($selected as $index){
294 World::getXZ($index, $chunkX, $chunkZ);
295 $world->orderChunkPopulation($chunkX, $chunkZ, null)->onCompletion(
296 static function() use ($world, &$done, $total) : void{
297 $oldProgress = (int) floor(($done / $total) * 100);
298 $newProgress = (int) floor((++$done / $total) * 100);
299 if(intdiv($oldProgress, 10) !== intdiv($newProgress, 10) || $done === $total || $done === 1){
300 $world->getLogger()->info($world->getServer()->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_spawnTerrainGenerationProgress(strval($done), strval($total), strval($newProgress))));
301 }
302 },
303 static function() : void{
304 //NOOP: we don't care if the world was unloaded
305 });
306 }
307 }
308
309 return true;
310 }
311
312 private function getWorldPath(string $name) : string{
313 return Path::join($this->dataPath, $name) . "/"; //TODO: check if we still need the trailing dirsep (I'm a little scared to remove it)
314 }
315
316 public function isWorldGenerated(string $name) : bool{
317 if(trim($name) === ""){
318 return false;
319 }
320 $path = $this->getWorldPath($name);
321 if(!($this->getWorldByName($name) instanceof World)){
322 return count($this->providerManager->getMatchingProviders($path)) > 0;
323 }
324
325 return true;
326 }
327
332 public function findEntity(int $entityId) : ?Entity{
333 foreach($this->worlds as $world){
334 assert($world->isLoaded());
335 if(($entity = $world->getEntity($entityId)) instanceof Entity){
336 return $entity;
337 }
338 }
339
340 return null;
341 }
342
343 public function tick(int $currentTick) : void{
344 foreach($this->worlds as $k => $world){
345 if(!isset($this->worlds[$k])){
346 // World unloaded during the tick of a world earlier in this loop, perhaps by plugin
347 continue;
348 }
349
350 $worldTime = microtime(true);
351 $world->doTick($currentTick);
352 $tickMs = (microtime(true) - $worldTime) * 1000;
353 $world->tickRateTime = $tickMs;
354 if($tickMs >= Server::TARGET_SECONDS_PER_TICK * 1000){
355 $world->getLogger()->debug(sprintf("Tick took too long: %gms (%g ticks)", $tickMs, round($tickMs / (Server::TARGET_SECONDS_PER_TICK * 1000), 2)));
356 }
357 }
358
359 if($this->autoSave && ++$this->autoSaveTicker >= $this->autoSaveTicks){
360 $this->autoSaveTicker = 0;
361 $this->server->getLogger()->debug("[Auto Save] Saving worlds...");
362 $start = microtime(true);
363 $this->doAutoSave();
364 $time = microtime(true) - $start;
365 $this->server->getLogger()->debug("[Auto Save] Save completed in " . ($time >= 1 ? round($time, 3) . "s" : round($time * 1000) . "ms"));
366 }
367 }
368
369 public function getAutoSave() : bool{
370 return $this->autoSave;
371 }
372
373 public function setAutoSave(bool $value) : void{
374 $this->autoSave = $value;
375 foreach($this->worlds as $world){
376 $world->setAutoSave($this->autoSave);
377 }
378 }
379
383 public function getAutoSaveInterval() : int{
384 return $this->autoSaveTicks;
385 }
386
387 public function setAutoSaveInterval(int $autoSaveTicks) : void{
388 if($autoSaveTicks <= 0){
389 throw new \InvalidArgumentException("Autosave ticks must be positive");
390 }
391 $this->autoSaveTicks = $autoSaveTicks;
392 }
393
394 private function doAutoSave() : void{
395 foreach($this->worlds as $world){
396 foreach($world->getPlayers() as $player){
397 if($player->spawned){
398 $player->save();
399 }
400 }
401 $world->save(false);
402 }
403 }
404}
const TARGET_TICKS_PER_SECOND
generateWorld(string $name, WorldCreationOptions $options, bool $backgroundGeneration=true)
unloadWorld(World $world, bool $forceUnload=false)
loadWorld(string $name, bool $autoUpgrade=false)