Geyser/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelChunkWithLightTran...

585 lines
31 KiB
Java

/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.translator.protocol.java.level;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.Unpooled;
import it.unimi.dsi.fastutil.ints.*;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.nbt.NBTOutputStream;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder;
import org.cloudburstmc.nbt.NbtUtils;
import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition;
import org.cloudburstmc.protocol.bedrock.packet.LevelChunkPacket;
import org.geysermc.erosion.util.LecternUtils;
import org.geysermc.geyser.entity.type.ItemFrameEntity;
import org.geysermc.geyser.level.BedrockDimension;
import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.level.chunk.BlockStorage;
import org.geysermc.geyser.level.chunk.GeyserChunkSection;
import org.geysermc.geyser.level.chunk.bitarray.BitArray;
import org.geysermc.geyser.level.chunk.bitarray.BitArrayVersion;
import org.geysermc.geyser.level.chunk.bitarray.SingletonBitArray;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.level.BiomeTranslator;
import org.geysermc.geyser.translator.level.block.entity.BedrockOnlyBlockEntity;
import org.geysermc.geyser.translator.level.block.entity.BlockEntityTranslator;
import org.geysermc.geyser.translator.level.block.entity.SkullBlockEntityTranslator;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.util.BlockEntityUtils;
import org.geysermc.geyser.util.ChunkUtils;
import org.geysermc.geyser.util.DimensionUtils;
import org.geysermc.mcprotocollib.protocol.data.game.chunk.BitStorage;
import org.geysermc.mcprotocollib.protocol.data.game.chunk.ChunkSection;
import org.geysermc.mcprotocollib.protocol.data.game.chunk.DataPalette;
import org.geysermc.mcprotocollib.protocol.data.game.chunk.palette.GlobalPalette;
import org.geysermc.mcprotocollib.protocol.data.game.chunk.palette.Palette;
import org.geysermc.mcprotocollib.protocol.data.game.chunk.palette.SingletonPalette;
import org.geysermc.mcprotocollib.protocol.data.game.level.block.BlockEntityInfo;
import org.geysermc.mcprotocollib.protocol.data.game.level.block.BlockEntityType;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.level.ClientboundLevelChunkWithLightPacket;
import java.io.IOException;
import java.util.BitSet;
import java.util.List;
import java.util.Map;
import static org.geysermc.geyser.util.ChunkUtils.*;
@Translator(packet = ClientboundLevelChunkWithLightPacket.class)
public class JavaLevelChunkWithLightTranslator extends PacketTranslator<ClientboundLevelChunkWithLightPacket> {
private static final ThreadLocal<ExtendedCollisionsStorage> EXTENDED_COLLISIONS_STORAGE = ThreadLocal.withInitial(ExtendedCollisionsStorage::new);
@Override
public void translate(GeyserSession session, ClientboundLevelChunkWithLightPacket packet) {
final boolean useExtendedCollisions = !session.getBlockMappings().getExtendedCollisionBoxes().isEmpty();
if (session.isSpawned()) {
ChunkUtils.updateChunkPosition(session, session.getPlayerEntity().getPosition().toInt());
}
// Ensure that, if the player is using lower world heights, the position is not offset
int yOffset = session.getChunkCache().getChunkMinY();
int chunkSize = session.getChunkCache().getChunkHeightY();
DataPalette[] javaChunks = new DataPalette[chunkSize];
DataPalette[] javaBiomes = new DataPalette[chunkSize];
final BlockEntityInfo[] blockEntities = packet.getBlockEntities();
final List<NbtMap> bedrockBlockEntities = new ObjectArrayList<>(blockEntities.length);
final List<BlockEntityInfo> lecterns = new ObjectArrayList<>();
BitSet waterloggedPaletteIds = new BitSet();
BitSet bedrockOnlyBlockEntityIds = new BitSet();
BedrockDimension bedrockDimension = session.getChunkCache().getBedrockDimension();
int maxBedrockSectionY = (bedrockDimension.height() >> 4) - 1;
int sectionCount;
byte[] payload;
ByteBuf byteBuf = null;
GeyserChunkSection[] sections = new GeyserChunkSection[javaChunks.length - (yOffset + (bedrockDimension.minY() >> 4))];
try {
ByteBuf in = Unpooled.wrappedBuffer(packet.getChunkData());
boolean extendedCollisionNextSection = false;
for (int sectionY = 0; sectionY < chunkSize; sectionY++) {
ChunkSection javaSection = session.getDownstream().getCodecHelper().readChunkSection(in);
javaChunks[sectionY] = javaSection.getChunkData();
javaBiomes[sectionY] = javaSection.getBiomeData();
boolean extendedCollision = extendedCollisionNextSection;
boolean thisExtendedCollisionNextSection = false;
int bedrockSectionY = sectionY + (yOffset - (bedrockDimension.minY() >> 4));
int subChunkIndex = sectionY + yOffset;
if (bedrockSectionY < 0 || maxBedrockSectionY < bedrockSectionY) {
// Ignore this chunk section since it goes outside the bounds accepted by the Bedrock client
if (useExtendedCollisions) {
EXTENDED_COLLISIONS_STORAGE.get().clear();
}
extendedCollisionNextSection = false;
continue;
}
// No need to encode an empty section...
if (javaSection.isBlockCountEmpty()) {
// Unless we need to send extended collisions
if (useExtendedCollisions) {
if (extendedCollision) {
int blocks = EXTENDED_COLLISIONS_STORAGE.get().bottomLayerCollisions() + 1;
BitArray bedrockData = BitArrayVersion.forBitsCeil(Integer.SIZE - Integer.numberOfLeadingZeros(blocks)).createArray(BlockStorage.SIZE);
BlockStorage layer0 = new BlockStorage(bedrockData, new IntArrayList(blocks));
layer0.idFor(session.getBlockMappings().getBedrockAir().getRuntimeId());
for (int yzx = 0; yzx < BlockStorage.SIZE / 16; yzx++) {
if (EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY) != 0) {
bedrockData.set(indexYZXtoXZY(yzx), layer0.idFor(EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY)));
EXTENDED_COLLISIONS_STORAGE.get().set(yzx, 0, sectionY);
}
}
BlockStorage[] layers = new BlockStorage[]{ layer0 };
sections[bedrockSectionY] = new GeyserChunkSection(layers, subChunkIndex);
}
EXTENDED_COLLISIONS_STORAGE.get().clear();
extendedCollisionNextSection = false;
}
continue;
}
Palette javaPalette = javaSection.getChunkData().getPalette();
BitStorage javaData = javaSection.getChunkData().getStorage();
if (javaPalette instanceof GlobalPalette) {
// As this is the global palette, simply iterate through the whole chunk section once
GeyserChunkSection section = new GeyserChunkSection(session.getBlockMappings().getBedrockAir().getRuntimeId(), subChunkIndex);
for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
int javaId = javaData.get(yzx);
int bedrockId = session.getBlockMappings().getBedrockBlockId(javaId);
int xzy = indexYZXtoXZY(yzx);
section.getBlockStorageArray()[0].setFullBlock(xzy, bedrockId);
if (BlockRegistries.WATERLOGGED.get().get(javaId)) {
section.getBlockStorageArray()[1].setFullBlock(xzy, session.getBlockMappings().getBedrockWater().getRuntimeId());
}
// Extended collision blocks
if (useExtendedCollisions) {
if (EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY) != 0) {
if (javaId == BlockStateValues.JAVA_AIR_ID) {
section.getBlockStorageArray()[0].setFullBlock(xzy, EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY));
}
EXTENDED_COLLISIONS_STORAGE.get().set(yzx, 0, sectionY);
continue;
}
BlockDefinition aboveBedrockExtendedCollisionDefinition = session.getBlockMappings().getExtendedCollisionBoxes().get(javaId);
if (aboveBedrockExtendedCollisionDefinition != null) {
EXTENDED_COLLISIONS_STORAGE.get().set((yzx + 0x100) & 0xFFF, aboveBedrockExtendedCollisionDefinition.getRuntimeId(), sectionY);
if ((xzy & 0xF) == 15) {
thisExtendedCollisionNextSection = true;
}
}
}
// Check if block is piston or flower to see if we'll need to create additional block entities, as they're only block entities in Bedrock
if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId) || BlockStateValues.isCauldron(javaId)) {
bedrockBlockEntities.add(BedrockOnlyBlockEntity.getTag(session,
Vector3i.from((packet.getX() << 4) + (yzx & 0xF), ((sectionY + yOffset) << 4) + ((yzx >> 8) & 0xF), (packet.getZ() << 4) + ((yzx >> 4) & 0xF)),
javaId
));
}
}
sections[bedrockSectionY] = section;
extendedCollisionNextSection = thisExtendedCollisionNextSection;
continue;
}
if (javaPalette instanceof SingletonPalette) {
// There's only one block here. Very easy!
int javaId = javaPalette.idToState(0);
int bedrockId = session.getBlockMappings().getBedrockBlockId(javaId);
BlockStorage blockStorage = new BlockStorage(SingletonBitArray.INSTANCE, IntLists.singleton(bedrockId));
if (BlockRegistries.WATERLOGGED.get().get(javaId)) {
BlockStorage waterlogged = new BlockStorage(SingletonBitArray.INSTANCE, IntLists.singleton(session.getBlockMappings().getBedrockWater().getRuntimeId()));
sections[bedrockSectionY] = new GeyserChunkSection(new BlockStorage[] {blockStorage, waterlogged}, subChunkIndex);
} else {
sections[bedrockSectionY] = new GeyserChunkSection(new BlockStorage[] {blockStorage}, subChunkIndex);
}
if (useExtendedCollisions) {
EXTENDED_COLLISIONS_STORAGE.get().clear();
extendedCollisionNextSection = false;
}
// If a chunk contains all of the same piston or flower pot then god help us
continue;
}
IntList bedrockPalette = new IntArrayList(javaPalette.size());
int airPaletteId = -1;
waterloggedPaletteIds.clear();
bedrockOnlyBlockEntityIds.clear();
// Iterate through palette and convert state IDs to Bedrock, doing some additional checks as we go
int extendedCollisionsInPalette = 0;
for (int i = 0; i < javaPalette.size(); i++) {
int javaId = javaPalette.idToState(i);
bedrockPalette.add(session.getBlockMappings().getBedrockBlockId(javaId));
if (BlockRegistries.WATERLOGGED.get().get(javaId)) {
waterloggedPaletteIds.set(i);
}
if (javaId == BlockStateValues.JAVA_AIR_ID) {
airPaletteId = i;
}
if (useExtendedCollisions) {
if (session.getBlockMappings().getExtendedCollisionBoxes().get(javaId) != null) {
extendedCollision = true;
extendedCollisionsInPalette++;
}
}
// Check if block is piston, flower or cauldron to see if we'll need to create additional block entities, as they're only block entities in Bedrock
if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId) || BlockStateValues.isCauldron(javaId)) {
bedrockOnlyBlockEntityIds.set(i);
}
}
// Add Bedrock-exclusive block entities
// We only if the palette contained any blocks that are Bedrock-exclusive block entities to avoid iterating through the whole block data
// for no reason, as most sections will not contain any pistons or flower pots
if (!bedrockOnlyBlockEntityIds.isEmpty()) {
for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
int paletteId = javaData.get(yzx);
if (bedrockOnlyBlockEntityIds.get(paletteId)) {
bedrockBlockEntities.add(BedrockOnlyBlockEntity.getTag(session,
Vector3i.from((packet.getX() << 4) + (yzx & 0xF), ((sectionY + yOffset) << 4) + ((yzx >> 8) & 0xF), (packet.getZ() << 4) + ((yzx >> 4) & 0xF)),
javaPalette.idToState(paletteId)
));
}
}
}
// We need to ensure we use enough bits to represent extended collision blocks in the chunk section
int sectionCollisionBlocks = 0;
if (useExtendedCollisions) {
int bottomLayerCollisions = extendedCollision ? EXTENDED_COLLISIONS_STORAGE.get().bottomLayerCollisions() : 0;
sectionCollisionBlocks = bottomLayerCollisions + extendedCollisionsInPalette;
}
int bedrockDataBits = Integer.SIZE - Integer.numberOfLeadingZeros(javaPalette.size() + sectionCollisionBlocks);
BitArray bedrockData = BitArrayVersion.forBitsCeil(bedrockDataBits).createArray(BlockStorage.SIZE);
BlockStorage layer0 = new BlockStorage(bedrockData, bedrockPalette);
BlockStorage[] layers;
// Convert data array from YZX to XZY coordinate order
if (waterloggedPaletteIds.isEmpty() && !extendedCollision) {
// No blocks are waterlogged, simply convert coordinate order
// This could probably be optimized further...
for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
int paletteId = javaData.get(yzx);
int xzy = indexYZXtoXZY(yzx);
bedrockData.set(xzy, paletteId);
}
layers = new BlockStorage[]{ layer0 };
} else if (!waterloggedPaletteIds.isEmpty() && !extendedCollision) {
// The section contains waterlogged blocks, we need to convert coordinate order AND generate a V1 block storage for
// layer 1 with palette ID 1 indicating water
int[] layer1Data = new int[BlockStorage.SIZE >> 5];
for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
int paletteId = javaData.get(yzx);
int xzy = indexYZXtoXZY(yzx);
bedrockData.set(xzy, paletteId);
if (waterloggedPaletteIds.get(paletteId)) {
layer1Data[xzy >> 5] |= 1 << (xzy & 0x1F);
}
}
// V1 palette
IntList layer1Palette = IntList.of(
session.getBlockMappings().getBedrockAir().getRuntimeId(), // Air - see BlockStorage's constructor for more information
session.getBlockMappings().getBedrockWater().getRuntimeId());
layers = new BlockStorage[]{ layer0, new BlockStorage(BitArrayVersion.V1.createArray(BlockStorage.SIZE, layer1Data), layer1Palette) };
} else if (waterloggedPaletteIds.isEmpty() && extendedCollision) {
for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
int paletteId = javaData.get(yzx);
int xzy = indexYZXtoXZY(yzx);
bedrockData.set(xzy, paletteId);
if (EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY) != 0) {
if (paletteId == airPaletteId) {
bedrockData.set(xzy, layer0.idFor(EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY)));
}
EXTENDED_COLLISIONS_STORAGE.get().set(yzx, 0, sectionY);
continue;
}
BlockDefinition aboveBedrockExtendedCollisionDefinition = session.getBlockMappings()
.getExtendedCollisionBoxes().get(javaPalette.idToState(paletteId));
if (aboveBedrockExtendedCollisionDefinition != null) {
EXTENDED_COLLISIONS_STORAGE.get().set((yzx + 0x100) & 0xFFF, aboveBedrockExtendedCollisionDefinition.getRuntimeId(), sectionY);
if ((xzy & 0xF) == 15) {
thisExtendedCollisionNextSection = true;
}
}
}
layers = new BlockStorage[]{ layer0 };
} else {
int[] layer1Data = new int[BlockStorage.SIZE >> 5];
for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
int paletteId = javaData.get(yzx);
int xzy = indexYZXtoXZY(yzx);
bedrockData.set(xzy, paletteId);
if (waterloggedPaletteIds.get(paletteId)) {
layer1Data[xzy >> 5] |= 1 << (xzy & 0x1F);
}
if (EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY) != 0) {
if (paletteId == airPaletteId) {
bedrockData.set(xzy, layer0.idFor(EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY)));
}
EXTENDED_COLLISIONS_STORAGE.get().set(yzx, 0, sectionY);
continue;
}
BlockDefinition aboveBedrockExtendedCollisionDefinition = session.getBlockMappings().getExtendedCollisionBoxes()
.get(javaPalette.idToState(paletteId));
if (aboveBedrockExtendedCollisionDefinition != null) {
EXTENDED_COLLISIONS_STORAGE.get().set((yzx + 0x100) & 0xFFF, aboveBedrockExtendedCollisionDefinition.getRuntimeId(), sectionY);
if ((xzy & 0xF) == 15) {
thisExtendedCollisionNextSection = true;
}
}
}
// V1 palette
IntList layer1Palette = IntList.of(
session.getBlockMappings().getBedrockAir().getRuntimeId(), // Air - see BlockStorage's constructor for more information
session.getBlockMappings().getBedrockWater().getRuntimeId());
layers = new BlockStorage[]{ layer0, new BlockStorage(BitArrayVersion.V1.createArray(BlockStorage.SIZE, layer1Data), layer1Palette) };
}
sections[bedrockSectionY] = new GeyserChunkSection(layers, subChunkIndex);
extendedCollisionNextSection = thisExtendedCollisionNextSection;
}
if (!session.getErosionHandler().isActive()) {
session.getChunkCache().addToCache(packet.getX(), packet.getZ(), javaChunks);
}
final int chunkBlockX = packet.getX() << 4;
final int chunkBlockZ = packet.getZ() << 4;
for (BlockEntityInfo blockEntity : blockEntities) {
BlockEntityType type = blockEntity.getType();
NbtMap tag = blockEntity.getNbt();
if (type == null) {
// As an example: ViaVersion will send -1 if it cannot find the block entity type
// Vanilla Minecraft gracefully handles this
continue;
}
int x = blockEntity.getX(); // Relative to chunk
int y = blockEntity.getY();
int z = blockEntity.getZ(); // Relative to chunk
// Get the Java block state ID from block entity position
DataPalette section = javaChunks[(y >> 4) - yOffset];
int blockState = section.get(x, y & 0xF, z);
if (type == BlockEntityType.LECTERN && BlockStateValues.getLecternBookStates().get(blockState)) {
// If getLecternBookStates is false, let's just treat it like a normal block entity
// Fill in tag with a default value
NbtMapBuilder lecternTag = LecternUtils.getBaseLecternTag(x + chunkBlockX, y, z + chunkBlockZ, 1);
lecternTag.putCompound("book", NbtMap.builder()
.putByte("Count", (byte) 1)
.putShort("Damage", (short) 0)
.putString("Name", "minecraft:written_book").build());
lecternTag.putInt("page", -1);
bedrockBlockEntities.add(lecternTag.build());
lecterns.add(blockEntity);
continue;
}
if (tag != null) {
BlockEntityTranslator blockEntityTranslator = BlockEntityUtils.getBlockEntityTranslator(type);
bedrockBlockEntities.add(blockEntityTranslator.getBlockEntityTag(session, type, x + chunkBlockX, y, z + chunkBlockZ, tag, blockState));
} else {
// Since 1.20.5, tags can be null, but Bedrock still needs a default tag to render the item
bedrockBlockEntities.add(BlockEntityTranslator.getConstantBedrockTag(type, x + chunkBlockX, y, z + chunkBlockZ).build());
}
// Check for custom skulls
if (session.getPreferencesCache().showCustomSkulls() && type == BlockEntityType.SKULL && tag != null && tag.containsKey("profile")) {
BlockDefinition blockDefinition = SkullBlockEntityTranslator.translateSkull(session, tag, Vector3i.from(x + chunkBlockX, y, z + chunkBlockZ), blockState);
if (blockDefinition != null) {
int bedrockSectionY = (y >> 4) - (bedrockDimension.minY() >> 4);
int subChunkIndex = (y >> 4) + (bedrockDimension.minY() >> 4);
if (0 <= bedrockSectionY && bedrockSectionY < maxBedrockSectionY) {
// Custom skull is in a section accepted by Bedrock
GeyserChunkSection bedrockSection = sections[bedrockSectionY];
IntList palette = bedrockSection.getBlockStorageArray()[0].getPalette();
if (palette instanceof IntImmutableList || palette instanceof IntLists.Singleton) {
// TODO there has to be a better way to expand the palette .-.
bedrockSection = bedrockSection.copy(subChunkIndex);
sections[bedrockSectionY] = bedrockSection;
}
bedrockSection.setFullBlock(x, y & 0xF, z, 0, blockDefinition.getRuntimeId());
}
}
}
}
// Find highest section
sectionCount = sections.length - 1;
while (sectionCount >= 0 && sections[sectionCount] == null) {
sectionCount--;
}
sectionCount++;
// As of 1.18.30, the amount of biomes read is dependent on how high Bedrock thinks the dimension is
int biomeCount = bedrockDimension.height() >> 4;
// Estimate chunk size
int size = 0;
for (int i = 0; i < sectionCount; i++) {
GeyserChunkSection section = sections[i];
if (section != null) {
size += section.estimateNetworkSize();
} else {
size += EMPTY_CHUNK_SECTION_SIZE;
}
}
size += ChunkUtils.EMPTY_BIOME_DATA.length * biomeCount;
size += 1; // Border blocks
size += bedrockBlockEntities.size() * 64; // Conservative estimate of 64 bytes per tile entity
// Allocate output buffer
byteBuf = ByteBufAllocator.DEFAULT.ioBuffer(size);
for (int i = 0; i < sectionCount; i++) {
GeyserChunkSection section = sections[i];
if (section != null) {
section.writeToNetwork(byteBuf);
} else {
int subChunkIndex = (i + (bedrockDimension.minY() >> 4));
new GeyserChunkSection(EMPTY_BLOCK_STORAGE, subChunkIndex).writeToNetwork(byteBuf);
}
}
int dimensionOffset = bedrockDimension.minY() >> 4;
for (int i = 0; i < biomeCount; i++) {
int biomeYOffset = dimensionOffset + i;
if (biomeYOffset < yOffset) {
// Ignore this biome section since it goes below the height of the Java world
byteBuf.writeBytes(ChunkUtils.EMPTY_BIOME_DATA);
continue;
}
if (biomeYOffset >= (chunkSize + yOffset)) {
// This biome section goes above the height of the Java world
// The byte written here is a header that says to carry on the biome data from the previous chunk
byteBuf.writeByte((127 << 1) | 1);
continue;
}
BiomeTranslator.toNewBedrockBiome(session, javaBiomes[i + (dimensionOffset - yOffset)]).writeToNetwork(byteBuf);
}
byteBuf.writeByte(0); // Border blocks - Edu edition only
// Encode tile entities into buffer
NBTOutputStream nbtStream = NbtUtils.createNetworkWriter(new ByteBufOutputStream(byteBuf));
for (NbtMap blockEntity : bedrockBlockEntities) {
nbtStream.writeTag(blockEntity);
}
payload = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(payload);
} catch (IOException e) {
session.getGeyser().getLogger().error("IO error while encoding chunk", e);
return;
} finally {
if (byteBuf != null) {
byteBuf.release(); // Release buffer to allow buffer pooling to be useful
}
}
LevelChunkPacket levelChunkPacket = new LevelChunkPacket();
levelChunkPacket.setSubChunksLength(sectionCount);
levelChunkPacket.setCachingEnabled(false);
levelChunkPacket.setChunkX(packet.getX());
levelChunkPacket.setChunkZ(packet.getZ());
levelChunkPacket.setData(Unpooled.wrappedBuffer(payload));
levelChunkPacket.setDimension(DimensionUtils.javaToBedrock(session.getChunkCache().getBedrockDimension()));
session.sendUpstreamPacket(levelChunkPacket);
if (!lecterns.isEmpty()) {
session.getGeyser().getWorldManager().sendLecternData(session, packet.getX(), packet.getZ(), lecterns);
}
for (Map.Entry<Vector3i, ItemFrameEntity> entry : session.getItemFrameCache().entrySet()) {
Vector3i position = entry.getKey();
if ((position.getX() >> 4) == packet.getX() && (position.getZ() >> 4) == packet.getZ()) {
// Update this item frame so it doesn't get lost in the abyss
//TODO optimize
entry.getValue().updateBlock(true);
}
}
}
static final class ExtendedCollisionsStorage {
private int[] data;
private int sectionY;
int get(int index, int sY) {
if (data == null) {
return 0;
}
if (!(sY == sectionY || sY == sectionY + 1)) {
data = null;
return 0;
}
return data[index];
}
void set(int index, int value, int sY) {
ensureDataExists();
data[index] = value;
sectionY = sY;
}
void clear() {
data = null;
}
int bottomLayerCollisions() {
if (data == null) {
return 0;
}
IntSet uniqueNonZeroSet = new IntOpenHashSet();
for (int i = 0; i < BlockStorage.SIZE / 16; i++) {
if (data[i] != 0) {
uniqueNonZeroSet.add(data[i]);
}
}
return uniqueNonZeroSet.size();
}
private void ensureDataExists() {
if (data == null) {
data = new int[BlockStorage.SIZE];
}
}
}
}