LEVELDB_ZLIB_RAW_COMPRESSION ]); } public function __construct(string $path){ self::checkForLevelDBExtension(); parent::__construct($path); $this->db = self::createDB($path); } protected function loadLevelData() : LevelData{ return new BedrockLevelData($this->getPath() . "level.dat"); } public function getWorldHeight() : int{ return 256; } public static function isValid(string $path) : bool{ return file_exists($path . "/level.dat") and is_dir($path . "/db/"); } public static function generate(string $path, string $name, int $seed, string $generator, array $options = []) : void{ self::checkForLevelDBExtension(); if(!file_exists($path . "/db")){ mkdir($path . "/db", 0777, true); } BedrockLevelData::generate($path, $name, $seed, $generator, $options); } /** * @param int $chunkX * @param int $chunkZ * * @return Chunk|null * @throws UnsupportedChunkFormatException */ protected function readChunk(int $chunkX, int $chunkZ) : ?Chunk{ $index = LevelDB::chunkIndex($chunkX, $chunkZ); if(!$this->chunkExists($chunkX, $chunkZ)){ return null; } /** @var SubChunk[] $subChunks */ $subChunks = []; /** @var bool $lightPopulated */ $lightPopulated = true; $chunkVersion = ord($this->db->get($index . self::TAG_VERSION)); $binaryStream = new BinaryStream(); switch($chunkVersion){ case 7: //MCPE 1.2 (???) case 4: //MCPE 1.1 //TODO: check beds case 3: //MCPE 1.0 for($y = 0; $y < Chunk::MAX_SUBCHUNKS; ++$y){ if(($data = $this->db->get($index . self::TAG_SUBCHUNK_PREFIX . chr($y))) === false){ continue; } $binaryStream->setBuffer($data, 0); $subChunkVersion = $binaryStream->getByte(); switch($subChunkVersion){ case 0: $blocks = $binaryStream->get(4096); $blockData = $binaryStream->get(2048); if($chunkVersion < 4){ $blockSkyLight = $binaryStream->get(2048); $blockLight = $binaryStream->get(2048); }else{ //Mojang didn't bother changing the subchunk version when they stopped saving sky light -_- $blockSkyLight = ""; $blockLight = ""; $lightPopulated = false; } $subChunks[$y] = new SubChunk($blocks, $blockData, $blockSkyLight, $blockLight); break; default: //TODO: set chunks read-only so the version on disk doesn't get overwritten throw new UnsupportedChunkFormatException("don't know how to decode LevelDB subchunk format version $subChunkVersion"); } } $binaryStream->setBuffer($this->db->get($index . self::TAG_DATA_2D), 0); $heightMap = array_values(unpack("v*", $binaryStream->get(512))); $biomeIds = $binaryStream->get(256); break; case 2: // < MCPE 1.0 $binaryStream->setBuffer($this->db->get($index . self::TAG_LEGACY_TERRAIN)); $fullIds = $binaryStream->get(32768); $fullData = $binaryStream->get(16384); $fullSkyLight = $binaryStream->get(16384); $fullBlockLight = $binaryStream->get(16384); for($yy = 0; $yy < 8; ++$yy){ $subOffset = ($yy << 4); $ids = ""; for($i = 0; $i < 256; ++$i){ $ids .= substr($fullIds, $subOffset, 16); $subOffset += 128; } $data = ""; $subOffset = ($yy << 3); for($i = 0; $i < 256; ++$i){ $data .= substr($fullData, $subOffset, 8); $subOffset += 64; } $skyLight = ""; $subOffset = ($yy << 3); for($i = 0; $i < 256; ++$i){ $skyLight .= substr($fullSkyLight, $subOffset, 8); $subOffset += 64; } $blockLight = ""; $subOffset = ($yy << 3); for($i = 0; $i < 256; ++$i){ $blockLight .= substr($fullBlockLight, $subOffset, 8); $subOffset += 64; } $subChunks[$yy] = new SubChunk($ids, $data, $skyLight, $blockLight); } $heightMap = array_values(unpack("C*", $binaryStream->get(256))); $biomeIds = ChunkUtils::convertBiomeColors(array_values(unpack("N*", $binaryStream->get(1024)))); break; default: //TODO: set chunks read-only so the version on disk doesn't get overwritten throw new UnsupportedChunkFormatException("don't know how to decode chunk format version $chunkVersion"); } $nbt = new LittleEndianNBTStream(); /** @var CompoundTag[] $entities */ $entities = []; if(($entityData = $this->db->get($index . self::TAG_ENTITY)) !== false and $entityData !== ""){ $entities = $nbt->read($entityData, true); if(!is_array($entities)){ $entities = [$entities]; } } /** @var CompoundTag $entityNBT */ foreach($entities as $entityNBT){ if($entityNBT->hasTag("id", IntTag::class)){ $entityNBT->setInt("id", $entityNBT->getInt("id") & 0xff); //remove type flags - TODO: use these instead of removing them) } } $tiles = []; if(($tileData = $this->db->get($index . self::TAG_BLOCK_ENTITY)) !== false and $tileData !== ""){ $tiles = $nbt->read($tileData, true); if(!is_array($tiles)){ $tiles = [$tiles]; } } //TODO: extra data should be converted into blockstorage layers (first they need to be implemented!) /* $extraData = []; if(($extraRawData = $this->db->get($index . self::TAG_BLOCK_EXTRA_DATA)) !== false and $extraRawData !== ""){ $binaryStream->setBuffer($extraRawData, 0); $count = $binaryStream->getLInt(); for($i = 0; $i < $count; ++$i){ $key = $binaryStream->getLInt(); $value = $binaryStream->getLShort(); $extraData[$key] = $value; } }*/ $chunk = new Chunk( $chunkX, $chunkZ, $subChunks, $entities, $tiles, $biomeIds, $heightMap ); //TODO: tile ticks, biome states (?) $chunk->setGenerated(true); $chunk->setPopulated(true); $chunk->setLightPopulated($lightPopulated); return $chunk; } protected function writeChunk(Chunk $chunk) : void{ $index = LevelDB::chunkIndex($chunk->getX(), $chunk->getZ()); $this->db->put($index . self::TAG_VERSION, chr(self::CURRENT_LEVEL_CHUNK_VERSION)); $subChunks = $chunk->getSubChunks(); foreach($subChunks as $y => $subChunk){ $key = $index . self::TAG_SUBCHUNK_PREFIX . chr($y); if($subChunk->isEmpty(false)){ //MCPE doesn't save light anymore as of 1.1 $this->db->delete($key); }else{ $this->db->put($key, chr(self::CURRENT_LEVEL_SUBCHUNK_VERSION) . $subChunk->getBlockIdArray() . $subChunk->getBlockDataArray() ); } } $this->db->put($index . self::TAG_DATA_2D, pack("v*", ...$chunk->getHeightMapArray()) . $chunk->getBiomeIdArray()); //TODO: use this properly $this->db->put($index . self::TAG_STATE_FINALISATION, chr(self::FINALISATION_DONE)); /** @var CompoundTag[] $tiles */ $tiles = []; foreach($chunk->getTiles() as $tile){ $tiles[] = $tile->saveNBT(); } $this->writeTags($tiles, $index . self::TAG_BLOCK_ENTITY); /** @var CompoundTag[] $entities */ $entities = []; foreach($chunk->getSavableEntities() as $entity){ $entities[] = $entity->saveNBT(); } $this->writeTags($entities, $index . self::TAG_ENTITY); $this->db->delete($index . self::TAG_DATA_2D_LEGACY); $this->db->delete($index . self::TAG_LEGACY_TERRAIN); } /** * @param CompoundTag[] $targets * @param string $index */ private function writeTags(array $targets, string $index) : void{ if(!empty($targets)){ $nbt = new LittleEndianNBTStream(); $this->db->put($index, $nbt->write($targets)); }else{ $this->db->delete($index); } } /** * @return \LevelDB */ public function getDatabase() : \LevelDB{ return $this->db; } public static function chunkIndex(int $chunkX, int $chunkZ) : string{ return Binary::writeLInt($chunkX) . Binary::writeLInt($chunkZ); } private function chunkExists(int $chunkX, int $chunkZ) : bool{ return $this->db->get(LevelDB::chunkIndex($chunkX, $chunkZ) . self::TAG_VERSION) !== false; } public function doGarbageCollection() : void{ } public function close() : void{ $this->db->close(); } public function getAllChunks() : \Generator{ foreach($this->db->getIterator() as $key => $_){ if(strlen($key) === 9 and substr($key, -1) === self::TAG_VERSION){ $chunkX = Binary::readLInt(substr($key, 0, 4)); $chunkZ = Binary::readLInt(substr($key, 4, 4)); if(($chunk = $this->loadChunk($chunkX, $chunkZ)) !== null){ yield $chunk; } } } } }