PocketMine-MP 5.23.3 git-976fc63567edab7a6fb6aeae739f43cf9fe57de4
Loading...
Searching...
No Matches
FormatConverter.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\format\io;
25
31use Symfony\Component\Filesystem\Path;
32use function basename;
33use function crc32;
34use function file_exists;
35use function floor;
36use function flush;
37use function microtime;
38use function mkdir;
39use function random_bytes;
40use function rename;
41use function round;
42use function rtrim;
43use const DIRECTORY_SEPARATOR;
44
46 private string $backupPath;
47 private \Logger $logger;
48
49 public function __construct(
50 private WorldProvider $oldProvider,
51 private WritableWorldProviderManagerEntry $newProvider,
52 string $backupPath,
53 \Logger $logger,
54 private int $chunksPerProgressUpdate = 256
55 ){
56 $this->logger = new \PrefixedLogger($logger, "World Converter: " . $this->oldProvider->getWorldData()->getName());
57
58 if(!file_exists($backupPath)){
59 @mkdir($backupPath, 0777, true);
60 }
61 $nextSuffix = "";
62 do{
63 $this->backupPath = Path::join($backupPath, basename($this->oldProvider->getPath()) . $nextSuffix);
64 $nextSuffix = "_" . crc32(random_bytes(4));
65 }while(file_exists($this->backupPath));
66 }
67
68 public function getBackupPath() : string{
69 return $this->backupPath;
70 }
71
72 public function execute() : void{
73 $new = $this->generateNew();
74
75 $this->populateLevelData($new->getWorldData());
76 $this->convertTerrain($new);
77
78 $path = $this->oldProvider->getPath();
79 $this->oldProvider->close();
80 $new->close();
81
82 $this->logger->info("Backing up pre-conversion world to " . $this->backupPath);
83 if(!@rename($path, $this->backupPath)){
84 $this->logger->warning("Moving old world files for backup failed, attempting copy instead. This might take a long time.");
85 Filesystem::recursiveCopy($path, $this->backupPath);
86 Filesystem::recursiveUnlink($path);
87 }
88 if(!@rename($new->getPath(), $path)){
89 //we don't expect this to happen because worlds/ should most likely be all on the same FS, but just in case...
90 $this->logger->debug("Relocation of new world files to location failed, attempting copy and delete instead");
91 Filesystem::recursiveCopy($new->getPath(), $path);
92 Filesystem::recursiveUnlink($new->getPath());
93 }
94
95 $this->logger->info("Conversion completed");
96 }
97
98 private function generateNew() : WritableWorldProvider{
99 $this->logger->info("Generating new world");
100 $data = $this->oldProvider->getWorldData();
101
102 $convertedOutput = rtrim($this->oldProvider->getPath(), "/" . DIRECTORY_SEPARATOR) . "_converted" . DIRECTORY_SEPARATOR;
103 if(file_exists($convertedOutput)){
104 $this->logger->info("Found previous conversion attempt, deleting...");
105 Filesystem::recursiveUnlink($convertedOutput);
106 }
107 $this->newProvider->generate($convertedOutput, $data->getName(), WorldCreationOptions::create()
108 //TODO: defaulting to NORMAL here really isn't very good behaviour, but it's consistent with what we already
109 //did previously; besides, WorldManager checks for unknown generators before this is reached anyway.
110 ->setGeneratorClass(GeneratorManager::getInstance()->getGenerator($data->getGenerator())?->getGeneratorClass() ?? Normal::class)
111 ->setGeneratorOptions($data->getGeneratorOptions())
112 ->setSeed($data->getSeed())
113 ->setSpawnPosition($data->getSpawn())
114 ->setDifficulty($data->getDifficulty())
115 );
116
117 return $this->newProvider->fromPath($convertedOutput, $this->logger);
118 }
119
120 private function populateLevelData(WorldData $data) : void{
121 $this->logger->info("Converting world manifest");
122 $oldData = $this->oldProvider->getWorldData();
123 $data->setDifficulty($oldData->getDifficulty());
124 $data->setLightningLevel($oldData->getLightningLevel());
125 $data->setLightningTime($oldData->getLightningTime());
126 $data->setRainLevel($oldData->getRainLevel());
127 $data->setRainTime($oldData->getRainTime());
128 $data->setSpawn($oldData->getSpawn());
129 $data->setTime($oldData->getTime());
130
131 $data->save();
132 $this->logger->info("Finished converting manifest");
133 //TODO: add more properties as-needed
134 }
135
136 private function convertTerrain(WritableWorldProvider $new) : void{
137 $this->logger->info("Calculating chunk count");
138 $count = $this->oldProvider->calculateChunkCount();
139 $this->logger->info("Discovered $count chunks");
140
141 $counter = 0;
142
143 $start = microtime(true);
144 $thisRound = $start;
145 foreach($this->oldProvider->getAllChunks(true, $this->logger) as $coords => $loadedChunkData){
146 [$chunkX, $chunkZ] = $coords;
147 $new->saveChunk($chunkX, $chunkZ, $loadedChunkData->getData(), Chunk::DIRTY_FLAGS_ALL);
148 $counter++;
149 if(($counter % $this->chunksPerProgressUpdate) === 0){
150 $time = microtime(true);
151 $diff = $time - $thisRound;
152 $thisRound = $time;
153 $this->logger->info("Converted $counter / $count chunks (" . floor($this->chunksPerProgressUpdate / $diff) . " chunks/sec)");
154 flush();
155 }
156 if(($counter % (2 ** 16)) === 0){
157 $new->doGarbageCollection();
158 }
159 }
160 $total = microtime(true) - $start;
161 $this->logger->info("Converted $counter / $counter chunks in " . round($total, 3) . " seconds (" . floor($counter / $total) . " chunks/sec)");
162 }
163}
saveChunk(int $chunkX, int $chunkZ, ChunkData $chunkData, int $dirtyFlags)