*/ protected $subChunks; /** @var Tile[] */ protected $tiles = []; /** @var Entity[] */ protected $entities = []; /** @var HeightArray */ protected $heightMap; /** @var BiomeArray */ protected $biomeIds; /** @var CompoundTag[]|null */ protected $NBTtiles; /** @var CompoundTag[]|null */ protected $NBTentities; /** * @param SubChunk[] $subChunks * @param CompoundTag[] $entities * @param CompoundTag[] $tiles */ public function __construct(int $chunkX, int $chunkZ, array $subChunks = [], ?array $entities = null, ?array $tiles = null, ?BiomeArray $biomeIds = null, ?HeightArray $heightMap = null){ $this->x = $chunkX; $this->z = $chunkZ; $this->subChunks = new \SplFixedArray(Chunk::MAX_SUBCHUNKS); foreach($this->subChunks as $y => $null){ $this->subChunks[$y] = $subChunks[$y] ?? new SubChunk(BlockLegacyIds::AIR << 4, []); } $val = ($this->subChunks->getSize() * 16); $this->heightMap = $heightMap ?? new HeightArray(array_fill(0, 256, $val)); $this->biomeIds = $biomeIds ?? new BiomeArray(str_repeat("\x00", 256)); $this->NBTtiles = $tiles; $this->NBTentities = $entities; } public function getX() : int{ return $this->x; } public function getZ() : int{ return $this->z; } public function setX(int $x) : void{ $this->x = $x; } public function setZ(int $z) : void{ $this->z = $z; } /** * Returns the chunk height in count of subchunks. */ public function getHeight() : int{ return $this->subChunks->getSize(); } /** * Returns the internal ID of the blockstate at the given coordinates. * * @param int $x 0-15 * @param int $y 0-255 * @param int $z 0-15 * * @return int bitmap, (id << 4) | meta */ public function getFullBlock(int $x, int $y, int $z) : int{ return $this->getSubChunk($y >> 4)->getFullBlock($x, $y & 0x0f, $z); } /** * Sets the blockstate at the given coordinate by internal ID. */ public function setFullBlock(int $x, int $y, int $z, int $block) : void{ $this->getSubChunk($y >> 4)->setFullBlock($x, $y & 0xf, $z, $block); $this->dirtyFlags |= self::DIRTY_FLAG_TERRAIN; } /** * Returns the sky light level at the specified chunk block coordinates * * @param int $x 0-15 * @param int $y 0-255 * @param int $z 0-15 * * @return int 0-15 */ public function getBlockSkyLight(int $x, int $y, int $z) : int{ return $this->getSubChunk($y >> 4)->getBlockSkyLightArray()->get($x & 0xf, $y & 0x0f, $z & 0xf); } /** * Sets the sky light level at the specified chunk block coordinates * * @param int $x 0-15 * @param int $y 0-255 * @param int $z 0-15 * @param int $level 0-15 */ public function setBlockSkyLight(int $x, int $y, int $z, int $level) : void{ $this->getSubChunk($y >> 4)->getBlockSkyLightArray()->set($x & 0xf, $y & 0x0f, $z & 0xf, $level); } public function setAllBlockSkyLight(int $level) : void{ for($y = $this->subChunks->count() - 1; $y >= 0; --$y){ $this->getSubChunk($y)->setBlockSkyLightArray(LightArray::fill($level)); } } /** * Returns the block light level at the specified chunk block coordinates * * @param int $x 0-15 * @param int $y 0-255 * @param int $z 0-15 * * @return int 0-15 */ public function getBlockLight(int $x, int $y, int $z) : int{ return $this->getSubChunk($y >> 4)->getBlockLightArray()->get($x & 0xf, $y & 0x0f, $z & 0xf); } /** * Sets the block light level at the specified chunk block coordinates * * @param int $x 0-15 * @param int $y 0-255 * @param int $z 0-15 * @param int $level 0-15 */ public function setBlockLight(int $x, int $y, int $z, int $level) : void{ $this->getSubChunk($y >> 4)->getBlockLightArray()->set($x & 0xf, $y & 0x0f, $z & 0xf, $level); } public function setAllBlockLight(int $level) : void{ for($y = $this->subChunks->count() - 1; $y >= 0; --$y){ $this->getSubChunk($y)->setBlockLightArray(LightArray::fill($level)); } } /** * Returns the Y coordinate of the highest non-air block at the specified X/Z chunk block coordinates * * @param int $x 0-15 * @param int $z 0-15 * * @return int 0-255, or -1 if there are no blocks in the column */ public function getHighestBlockAt(int $x, int $z) : int{ for($y = $this->subChunks->count() - 1; $y >= 0; --$y){ $height = $this->getSubChunk($y)->getHighestBlockAt($x, $z) | ($y << 4); if($height !== -1){ return $height; } } return -1; } /** * Returns the heightmap value at the specified X/Z chunk block coordinates * * @param int $x 0-15 * @param int $z 0-15 */ public function getHeightMap(int $x, int $z) : int{ return $this->heightMap->get($x, $z); } /** * Returns the heightmap value at the specified X/Z chunk block coordinates * * @param int $x 0-15 * @param int $z 0-15 */ public function setHeightMap(int $x, int $z, int $value) : void{ $this->heightMap->set($x, $z, $value); } /** * Recalculates the heightmap for the whole chunk. * * @param \SplFixedArray|int[] $lightFilters * @param \SplFixedArray|bool[] $lightDiffusers * @phpstan-param \SplFixedArray $lightFilters * @phpstan-param \SplFixedArray $lightDiffusers */ public function recalculateHeightMap(\SplFixedArray $lightFilters, \SplFixedArray $lightDiffusers) : void{ for($z = 0; $z < 16; ++$z){ for($x = 0; $x < 16; ++$x){ $this->recalculateHeightMapColumn($x, $z, $lightFilters, $lightDiffusers); } } } /** * Recalculates the heightmap for the block column at the specified X/Z chunk coordinates * * @param int $x 0-15 * @param int $z 0-15 * @param \SplFixedArray|int[] $lightFilters * @param \SplFixedArray|bool[] $lightDiffusers * @phpstan-param \SplFixedArray $lightFilters * @phpstan-param \SplFixedArray $lightDiffusers * * @return int New calculated heightmap value (0-256 inclusive) */ public function recalculateHeightMapColumn(int $x, int $z, \SplFixedArray $lightFilters, \SplFixedArray $lightDiffusers) : int{ $y = $this->getHighestBlockAt($x, $z); for(; $y >= 0; --$y){ if($lightFilters[$state = $this->getFullBlock($x, $y, $z)] > 1 or $lightDiffusers[$state]){ break; } } $this->setHeightMap($x, $z, $y + 1); return $y + 1; } /** * Performs basic sky light population on the chunk. * This does not cater for adjacent sky light, this performs direct sky light population only. This may cause some strange visual artifacts * if the chunk is light-populated after being terrain-populated. * * @param \SplFixedArray|int[] $lightFilters * @phpstan-param \SplFixedArray $lightFilters * * TODO: fast adjacent light spread */ public function populateSkyLight(\SplFixedArray $lightFilters) : void{ $this->setAllBlockSkyLight(0); for($x = 0; $x < 16; ++$x){ for($z = 0; $z < 16; ++$z){ $y = ($this->subChunks->count() * 16) - 1; $heightMap = $this->getHeightMap($x, $z); for(; $y >= $heightMap; --$y){ $this->setBlockSkyLight($x, $y, $z, 15); } $light = 15; for(; $y >= 0; --$y){ $light -= $lightFilters[$this->getFullBlock($x, $y, $z)]; if($light <= 0){ break; } $this->setBlockSkyLight($x, $y, $z, $light); } } } } /** * Returns the biome ID at the specified X/Z chunk block coordinates * * @param int $x 0-15 * @param int $z 0-15 * * @return int 0-255 */ public function getBiomeId(int $x, int $z) : int{ return $this->biomeIds->get($x, $z); } /** * Sets the biome ID at the specified X/Z chunk block coordinates * * @param int $x 0-15 * @param int $z 0-15 * @param int $biomeId 0-255 */ public function setBiomeId(int $x, int $z, int $biomeId) : void{ $this->biomeIds->set($x, $z, $biomeId); $this->dirtyFlags |= self::DIRTY_FLAG_BIOMES; } public function isLightPopulated() : bool{ return $this->lightPopulated; } public function setLightPopulated(bool $value = true) : void{ $this->lightPopulated = $value; } public function isPopulated() : bool{ return $this->terrainPopulated; } public function setPopulated(bool $value = true) : void{ $this->terrainPopulated = $value; $this->dirtyFlags |= self::DIRTY_FLAG_TERRAIN; } public function isGenerated() : bool{ return $this->terrainGenerated; } public function setGenerated(bool $value = true) : void{ $this->terrainGenerated = $value; $this->dirtyFlags |= self::DIRTY_FLAG_TERRAIN; } public function addEntity(Entity $entity) : void{ if($entity->isClosed()){ throw new \InvalidArgumentException("Attempted to add a garbage closed Entity to a chunk"); } $this->entities[$entity->getId()] = $entity; if(!($entity instanceof Player)){ $this->dirtyFlags |= self::DIRTY_FLAG_ENTITIES; } } public function removeEntity(Entity $entity) : void{ unset($this->entities[$entity->getId()]); if(!($entity instanceof Player)){ $this->dirtyFlags |= self::DIRTY_FLAG_ENTITIES; } } public function addTile(Tile $tile) : void{ if($tile->isClosed()){ throw new \InvalidArgumentException("Attempted to add a garbage closed Tile to a chunk"); } $pos = $tile->getPos(); if(isset($this->tiles[$index = Chunk::blockHash($pos->x, $pos->y, $pos->z)]) and $this->tiles[$index] !== $tile){ $this->tiles[$index]->close(); } $this->tiles[$index] = $tile; $this->dirtyFlags |= self::DIRTY_FLAG_TILES; } public function removeTile(Tile $tile) : void{ $pos = $tile->getPos(); unset($this->tiles[Chunk::blockHash($pos->x, $pos->y, $pos->z)]); $this->dirtyFlags |= self::DIRTY_FLAG_TILES; } /** * Returns an array of entities currently using this chunk. * * @return Entity[] */ public function getEntities() : array{ return $this->entities; } /** * @return Entity[] */ public function getSavableEntities() : array{ return array_filter($this->entities, function(Entity $entity) : bool{ return $entity->canSaveWithChunk(); }); } /** * @return Tile[] */ public function getTiles() : array{ return $this->tiles; } /** * Returns the tile at the specified chunk block coordinates, or null if no tile exists. * * @param int $x 0-15 * @param int $y 0-255 * @param int $z 0-15 */ public function getTile(int $x, int $y, int $z) : ?Tile{ return $this->tiles[Chunk::blockHash($x, $y, $z)] ?? null; } /** * Called when the chunk is unloaded, closing entities and tiles. */ public function onUnload() : void{ foreach($this->getEntities() as $entity){ if($entity instanceof Player){ continue; } $entity->close(); } foreach($this->getTiles() as $tile){ $tile->close(); } } /** * @return CompoundTag[] */ public function getNBTtiles() : array{ return $this->NBTtiles ?? array_map(function(Tile $tile) : CompoundTag{ return $tile->saveNBT(); }, $this->tiles); } /** * @return CompoundTag[] */ public function getNBTentities() : array{ return $this->NBTentities ?? array_map(function(Entity $entity) : CompoundTag{ return $entity->saveNBT(); }, $this->getSavableEntities()); } /** * Deserializes tiles and entities from NBT */ public function initChunk(World $world) : void{ if($this->NBTentities !== null){ $this->dirtyFlags |= self::DIRTY_FLAG_ENTITIES; $world->timings->syncChunkLoadEntitiesTimer->startTiming(); $entityFactory = EntityFactory::getInstance(); foreach($this->NBTentities as $nbt){ try{ $entity = $entityFactory->createFromData($world, $nbt); if(!($entity instanceof Entity)){ $saveIdTag = $nbt->getTag("id") ?? $nbt->getTag("identifier"); $saveId = ""; if($saveIdTag instanceof StringTag){ $saveId = $saveIdTag->getValue(); }elseif($saveIdTag instanceof IntTag){ //legacy MCPE format $saveId = "legacy(" . $saveIdTag->getValue() . ")"; } $world->getLogger()->warning("Chunk $this->x $this->z: Deleted unknown entity type $saveId"); continue; } }catch(\Exception $t){ //TODO: this shouldn't be here $world->getLogger()->logException($t); continue; } } $this->NBTentities = null; $world->timings->syncChunkLoadEntitiesTimer->stopTiming(); } if($this->NBTtiles !== null){ $this->dirtyFlags |= self::DIRTY_FLAG_TILES; $world->timings->syncChunkLoadTileEntitiesTimer->startTiming(); $tileFactory = TileFactory::getInstance(); foreach($this->NBTtiles as $nbt){ if(($tile = $tileFactory->createFromData($world, $nbt)) !== null){ $world->addTile($tile); }else{ $world->getLogger()->warning("Chunk $this->x $this->z: Deleted unknown tile entity type " . $nbt->getString("id", "")); continue; } } $this->NBTtiles = null; $world->timings->syncChunkLoadTileEntitiesTimer->stopTiming(); } } public function getBiomeIdArray() : string{ return $this->biomeIds->getData(); } /** * @return int[] */ public function getHeightMapArray() : array{ return $this->heightMap->getValues(); } /** * @param int[] $values */ public function setHeightMapArray(array $values) : void{ $this->heightMap = new HeightArray($values); } public function isDirty() : bool{ return $this->dirtyFlags !== 0 or count($this->tiles) > 0 or count($this->getSavableEntities()) > 0; } public function getDirtyFlag(int $flag) : bool{ return ($this->dirtyFlags & $flag) !== 0; } public function getDirtyFlags() : int{ return $this->dirtyFlags; } public function setDirtyFlag(int $flag, bool $value) : void{ if($value){ $this->dirtyFlags |= $flag; }else{ $this->dirtyFlags &= ~$flag; } } public function setDirty() : void{ $this->dirtyFlags = ~0; } public function clearDirtyFlags() : void{ $this->dirtyFlags = 0; } /** * Returns the subchunk at the specified subchunk Y coordinate, or an empty, unmodifiable stub if it does not exist or the coordinate is out of range. */ public function getSubChunk(int $y) : SubChunkInterface{ if($y < 0 or $y >= $this->subChunks->getSize()){ return EmptySubChunk::getInstance(); //TODO: drop this and throw an exception here } return $this->subChunks[$y]; } /** * Sets a subchunk in the chunk index */ public function setSubChunk(int $y, ?SubChunk $subChunk) : void{ if($y < 0 or $y >= $this->subChunks->getSize()){ throw new \InvalidArgumentException("Invalid subchunk Y coordinate $y"); } $this->subChunks[$y] = $subChunk ?? new SubChunk(BlockLegacyIds::AIR << 4, []); $this->setDirtyFlag(self::DIRTY_FLAG_TERRAIN, true); } /** * @return \SplFixedArray|SubChunk[] * @phpstan-return \SplFixedArray */ public function getSubChunks() : \SplFixedArray{ return $this->subChunks; } /** * Disposes of empty subchunks and frees data where possible */ public function collectGarbage() : void{ foreach($this->subChunks as $y => $subChunk){ $subChunk->collectGarbage(); } } /** * Hashes the given chunk block coordinates into a single integer. * * @param int $x 0-15 * @param int $y 0-255 * @param int $z 0-15 */ public static function blockHash(int $x, int $y, int $z) : int{ return ($y << 8) | (($z & 0x0f) << 4) | ($x & 0x0f); } }