PocketMine-MP 5.21.2 git-b2aa6396c3cc2cafdd815eacc360e1ad89599899
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
60 private array $worlds = [];
61 private ?World $defaultWorld = null;
62
63 private bool $autoSave = true;
64 private int $autoSaveTicks = self::TICKS_PER_AUTOSAVE;
65 private int $autoSaveTicker = 0;
66
67 public function __construct(
68 private Server $server,
69 private string $dataPath,
70 private WorldProviderManager $providerManager
71 ){}
72
73 public function getProviderManager() : WorldProviderManager{
74 return $this->providerManager;
75 }
76
80 public function getWorlds() : array{
81 return $this->worlds;
82 }
83
84 public function getDefaultWorld() : ?World{
85 return $this->defaultWorld;
86 }
87
93 public function setDefaultWorld(?World $world) : void{
94 if($world === null || ($this->isWorldLoaded($world->getFolderName()) && $world !== $this->defaultWorld)){
95 $this->defaultWorld = $world;
96 }
97 }
98
99 public function isWorldLoaded(string $name) : bool{
100 return $this->getWorldByName($name) instanceof World;
101 }
102
103 public function getWorld(int $worldId) : ?World{
104 return $this->worlds[$worldId] ?? null;
105 }
106
110 public function getWorldByName(string $name) : ?World{
111 foreach($this->worlds as $world){
112 if($world->getFolderName() === $name){
113 return $world;
114 }
115 }
116
117 return null;
118 }
119
123 public function unloadWorld(World $world, bool $forceUnload = false) : bool{
124 if($world === $this->getDefaultWorld() && !$forceUnload){
125 throw new \InvalidArgumentException("The default world cannot be unloaded while running, please switch worlds.");
126 }
127 if($world->isDoingTick()){
128 throw new \InvalidArgumentException("Cannot unload a world during world tick");
129 }
130
131 $ev = new WorldUnloadEvent($world);
132 $ev->call();
133
134 if(!$forceUnload && $ev->isCancelled()){
135 return false;
136 }
137
138 $this->server->getLogger()->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_unloading($world->getDisplayName())));
139 if(count($world->getPlayers()) !== 0){
140 try{
141 $safeSpawn = $this->defaultWorld !== null && $this->defaultWorld !== $world ? $this->defaultWorld->getSafeSpawn() : null;
142 }catch(WorldException $e){
143 $safeSpawn = null;
144 }
145 foreach($world->getPlayers() as $player){
146 if($safeSpawn === null){
147 $player->disconnect("Forced default world unload");
148 }else{
149 $player->teleport($safeSpawn);
150 }
151 }
152 }
153
154 if($world === $this->defaultWorld){
155 $this->defaultWorld = null;
156 }
157 unset($this->worlds[$world->getId()]);
158
159 $world->onUnload();
160 return true;
161 }
162
170 public function loadWorld(string $name, bool $autoUpgrade = false) : bool{
171 if(trim($name) === ""){
172 throw new \InvalidArgumentException("Invalid empty world name");
173 }
174 if($this->isWorldLoaded($name)){
175 return true;
176 }elseif(!$this->isWorldGenerated($name)){
177 return false;
178 }
179
180 $path = $this->getWorldPath($name);
181
182 $providers = $this->providerManager->getMatchingProviders($path);
183 if(count($providers) !== 1){
184 $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_loadError(
185 $name,
186 count($providers) === 0 ?
187 KnownTranslationFactory::pocketmine_level_unknownFormat() :
188 KnownTranslationFactory::pocketmine_level_ambiguousFormat(implode(", ", array_keys($providers)))
189 )));
190 return false;
191 }
192 $providerClass = array_shift($providers);
193
194 try{
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(
198 $name,
199 KnownTranslationFactory::pocketmine_level_corrupted($e->getMessage())
200 )));
201 return false;
202 }catch(UnsupportedWorldFormatException $e){
203 $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_loadError(
204 $name,
205 KnownTranslationFactory::pocketmine_level_unsupportedFormat($e->getMessage())
206 )));
207 return false;
208 }
209
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(
213 $name,
214 KnownTranslationFactory::pocketmine_level_unknownGenerator($provider->getWorldData()->getGenerator())
215 )));
216 return false;
217 }
218 try{
219 $generatorEntry->validateGeneratorOptions($provider->getWorldData()->getGeneratorOptions());
220 }catch(InvalidGeneratorOptionsException $e){
221 $this->server->getLogger()->error($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_loadError(
222 $name,
223 KnownTranslationFactory::pocketmine_level_invalidGeneratorOptions(
224 $provider->getWorldData()->getGeneratorOptions(),
225 $provider->getWorldData()->getGenerator(),
226 $e->getMessage()
227 )
228 )));
229 return false;
230 }
231 if(!($provider instanceof WritableWorldProvider)){
232 if(!$autoUpgrade){
233 throw new UnsupportedWorldFormatException("World \"$name\" is in an unsupported format and needs to be upgraded");
234 }
235 $this->server->getLogger()->notice($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_conversion_start($name)));
236
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"));
241
242 $this->server->getLogger()->notice($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_conversion_finish($name, $converter->getBackupPath())));
243 }
244
245 $world = new World($this->server, $name, $provider, $this->server->getAsyncPool());
246
247 $this->worlds[$world->getId()] = $world;
248 $world->setAutoSave($this->autoSave);
249
250 (new WorldLoadEvent($world))->call();
251
252 return true;
253 }
254
260 public function generateWorld(string $name, WorldCreationOptions $options, bool $backgroundGeneration = true) : bool{
261 if(trim($name) === "" || $this->isWorldGenerated($name)){
262 return false;
263 }
264
265 $providerEntry = $this->providerManager->getDefault();
266
267 $path = $this->getWorldPath($name);
268 $providerEntry->generate($path, $name, $options);
269
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;
272
273 $world->setAutoSave($this->autoSave);
274
275 (new WorldInitEvent($world))->call();
276
277 (new WorldLoadEvent($world))->call();
278
279 if($backgroundGeneration){
280 $this->server->getLogger()->notice($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_level_backgroundGeneration($name)));
281
282 $spawnLocation = $world->getSpawnLocation();
283 $centerX = $spawnLocation->getFloorX() >> Chunk::COORD_BIT_SIZE;
284 $centerZ = $spawnLocation->getFloorZ() >> Chunk::COORD_BIT_SIZE;
285
286 $selected = iterator_to_array((new ChunkSelector())->selectChunks(8, $centerX, $centerZ), preserve_keys: false);
287 $done = 0;
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))));
297 }
298 },
299 static function() : void{
300 //NOOP: we don't care if the world was unloaded
301 });
302 }
303 }
304
305 return true;
306 }
307
308 private function getWorldPath(string $name) : string{
309 return Path::join($this->dataPath, $name) . "/"; //TODO: check if we still need the trailing dirsep (I'm a little scared to remove it)
310 }
311
312 public function isWorldGenerated(string $name) : bool{
313 if(trim($name) === ""){
314 return false;
315 }
316 $path = $this->getWorldPath($name);
317 if(!($this->getWorldByName($name) instanceof World)){
318 return count($this->providerManager->getMatchingProviders($path)) > 0;
319 }
320
321 return true;
322 }
323
328 public function findEntity(int $entityId) : ?Entity{
329 foreach($this->worlds as $world){
330 assert($world->isLoaded());
331 if(($entity = $world->getEntity($entityId)) instanceof Entity){
332 return $entity;
333 }
334 }
335
336 return null;
337 }
338
339 public function tick(int $currentTick) : void{
340 foreach($this->worlds as $k => $world){
341 if(!isset($this->worlds[$k])){
342 // World unloaded during the tick of a world earlier in this loop, perhaps by plugin
343 continue;
344 }
345
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)));
352 }
353 }
354
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);
359 $this->doAutoSave();
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"));
362 }
363 }
364
365 public function getAutoSave() : bool{
366 return $this->autoSave;
367 }
368
369 public function setAutoSave(bool $value) : void{
370 $this->autoSave = $value;
371 foreach($this->worlds as $world){
372 $world->setAutoSave($this->autoSave);
373 }
374 }
375
379 public function getAutoSaveInterval() : int{
380 return $this->autoSaveTicks;
381 }
382
383 public function setAutoSaveInterval(int $autoSaveTicks) : void{
384 if($autoSaveTicks <= 0){
385 throw new \InvalidArgumentException("Autosave ticks must be positive");
386 }
387 $this->autoSaveTicks = $autoSaveTicks;
388 }
389
390 private function doAutoSave() : void{
391 foreach($this->worlds as $world){
392 foreach($world->getPlayers() as $player){
393 if($player->spawned){
394 $player->save();
395 }
396 }
397 $world->save(false);
398 }
399 }
400}
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)