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