PocketMine-MP 5.23.3 git-f7687af337d001ddbcc47b8e773f014a33faa662
Loading...
Searching...
No Matches
RegionWorldProvider.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\region;
25
35use Symfony\Component\Filesystem\Path;
36use function assert;
37use function file_exists;
38use function is_dir;
39use function is_int;
40use function morton2d_encode;
41use function rename;
42use function scandir;
43use function strlen;
44use function strrpos;
45use function substr;
46use function time;
47use const SCANDIR_SORT_NONE;
48
50
54 abstract protected static function getRegionFileExtension() : string;
55
59 abstract protected static function getPcWorldFormatVersion() : int;
60
61 public static function isValid(string $path) : bool{
62 if(file_exists(Path::join($path, "level.dat")) && is_dir($regionPath = Path::join($path, "region"))){
63 foreach(scandir($regionPath, SCANDIR_SORT_NONE) as $file){
64 $extPos = strrpos($file, ".");
65 if($extPos !== false && substr($file, $extPos + 1) === static::getRegionFileExtension()){
66 //we don't care if other region types exist, we only care if this format is possible
67 return true;
68 }
69 }
70 }
71
72 return false;
73 }
74
79 protected array $regions = [];
80
81 protected function loadLevelData() : WorldData{
82 return new JavaWorldData(Path::join($this->getPath(), "level.dat"));
83 }
84
85 public function doGarbageCollection() : void{
86 $limit = time() - 300;
87 foreach($this->regions as $index => $region){
88 if($region->lastUsed <= $limit){
89 $region->close();
90 unset($this->regions[$index]);
91 }
92 }
93 }
94
101 public static function getRegionIndex(int $chunkX, int $chunkZ, &$regionX, &$regionZ) : void{
102 $regionX = $chunkX >> 5;
103 $regionZ = $chunkZ >> 5;
104 }
105
106 protected function getRegion(int $regionX, int $regionZ) : ?RegionLoader{
107 return $this->regions[morton2d_encode($regionX, $regionZ)] ?? null;
108 }
109
113 protected function pathToRegion(int $regionX, int $regionZ) : string{
114 return Path::join($this->path, "region", "r.$regionX.$regionZ." . static::getRegionFileExtension());
115 }
116
117 protected function loadRegion(int $regionX, int $regionZ) : RegionLoader{
118 if(!isset($this->regions[$index = morton2d_encode($regionX, $regionZ)])){
119 $path = $this->pathToRegion($regionX, $regionZ);
120
121 try{
122 $this->regions[$index] = RegionLoader::loadExisting($path);
123 }catch(CorruptedRegionException $e){
124 $this->logger->error("Corrupted region file detected: " . $e->getMessage());
125
126 $backupPath = $path . ".bak." . time();
127 rename($path, $backupPath);
128 $this->logger->error("Corrupted region file has been backed up to " . $backupPath);
129
130 $this->regions[$index] = RegionLoader::createNew($path);
131 }
132 }
133 return $this->regions[$index];
134 }
135
136 protected function unloadRegion(int $regionX, int $regionZ) : void{
137 if(isset($this->regions[$hash = morton2d_encode($regionX, $regionZ)])){
138 $this->regions[$hash]->close();
139 unset($this->regions[$hash]);
140 }
141 }
142
143 public function close() : void{
144 foreach($this->regions as $index => $region){
145 $region->close();
146 unset($this->regions[$index]);
147 }
148 }
149
153 abstract protected function deserializeChunk(string $data, \Logger $logger) : ?LoadedChunkData;
154
159 protected static function getCompoundList(string $context, ListTag $list) : array{
160 if($list->count() === 0){ //empty lists might have wrong types, we don't care
161 return [];
162 }
163 if($list->getTagType() !== NBT::TAG_Compound){
164 throw new CorruptedChunkException("Expected TAG_List<TAG_Compound> for '$context'");
165 }
166 $result = [];
167 foreach($list as $tag){
168 if(!($tag instanceof CompoundTag)){
169 //this should never happen, but it's still possible due to lack of native type safety
170 throw new CorruptedChunkException("Expected TAG_List<TAG_Compound> for '$context'");
171 }
172 $result[] = $tag;
173 }
174 return $result;
175 }
176
177 protected static function readFixedSizeByteArray(CompoundTag $chunk, string $tagName, int $length) : string{
178 $tag = $chunk->getTag($tagName);
179 if(!($tag instanceof ByteArrayTag)){
180 if($tag === null){
181 throw new CorruptedChunkException("'$tagName' key is missing from chunk NBT");
182 }
183 throw new CorruptedChunkException("Expected TAG_ByteArray for '$tagName'");
184 }
185 $data = $tag->getValue();
186 if(strlen($data) !== $length){
187 throw new CorruptedChunkException("Expected '$tagName' payload to have exactly $length bytes, but have " . strlen($data));
188 }
189 return $data;
190 }
191
195 public function loadChunk(int $chunkX, int $chunkZ) : ?LoadedChunkData{
196 $regionX = $regionZ = null;
197 self::getRegionIndex($chunkX, $chunkZ, $regionX, $regionZ);
198 assert(is_int($regionX) && is_int($regionZ));
199
200 if(!file_exists($this->pathToRegion($regionX, $regionZ))){
201 return null;
202 }
203
204 $chunkData = $this->loadRegion($regionX, $regionZ)->readChunk($chunkX & 0x1f, $chunkZ & 0x1f);
205 if($chunkData !== null){
206 return $this->deserializeChunk($chunkData, new \PrefixedLogger($this->logger, "Loading chunk x=$chunkX z=$chunkZ"));
207 }
208
209 return null;
210 }
211
212 private function createRegionIterator() : \RegexIterator{
213 return new \RegexIterator(
214 new \FilesystemIterator(
215 Path::join($this->path, 'region'),
216 \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS
217 ),
218 '/\/r\.(-?\d+)\.(-?\d+)\.' . static::getRegionFileExtension() . '$/',
219 \RegexIterator::GET_MATCH
220 );
221 }
222
223 public function getAllChunks(bool $skipCorrupted = false, ?\Logger $logger = null) : \Generator{
224 $iterator = $this->createRegionIterator();
225
226 foreach($iterator as $region){
227 $regionX = ((int) $region[1]);
228 $regionZ = ((int) $region[2]);
229 $rX = $regionX << 5;
230 $rZ = $regionZ << 5;
231
232 for($chunkX = $rX; $chunkX < $rX + 32; ++$chunkX){
233 for($chunkZ = $rZ; $chunkZ < $rZ + 32; ++$chunkZ){
234 try{
235 $chunk = $this->loadChunk($chunkX, $chunkZ);
236 if($chunk !== null){
237 yield [$chunkX, $chunkZ] => $chunk;
238 }
239 }catch(CorruptedChunkException $e){
240 if(!$skipCorrupted){
241 throw $e;
242 }
243 if($logger !== null){
244 $logger->error("Skipped corrupted chunk $chunkX $chunkZ (" . $e->getMessage() . ")");
245 }
246 }
247 }
248 }
249
250 $this->unloadRegion($regionX, $regionZ);
251 }
252 }
253
254 public function calculateChunkCount() : int{
255 $count = 0;
256 foreach($this->createRegionIterator() as $region){
257 $regionX = ((int) $region[1]);
258 $regionZ = ((int) $region[2]);
259 $count += $this->loadRegion($regionX, $regionZ)->calculateChunkCount();
260 $this->unloadRegion($regionX, $regionZ);
261 }
262 return $count;
263 }
264}
getAllChunks(bool $skipCorrupted=false, ?\Logger $logger=null)
deserializeChunk(string $data, \Logger $logger)
static getCompoundList(string $context, ListTag $list)
static getRegionIndex(int $chunkX, int $chunkZ, &$regionX, &$regionZ)