From 3d11a4c641334f9fa5b0a93ca6b195e3b2e7ee30 Mon Sep 17 00:00:00 2001 From: Valaphee Date: Sat, 17 Dec 2022 00:10:13 +0100 Subject: [PATCH] Use sub chunk request system Use sub chunk request system --- .../geyser/level/chunk/GeyserChunk.java | 10 +- .../geyser/network/LoggingPacketHandler.java | 5 + .../geyser/session/cache/ChunkCache.java | 9 +- .../BedrockSubChunkRequestTranslator.java | 282 ++++++++++++ .../JavaLevelChunkWithLightTranslator.java | 401 ++++++++++-------- 5 files changed, 514 insertions(+), 193 deletions(-) create mode 100644 core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockSubChunkRequestTranslator.java diff --git a/core/src/main/java/org/geysermc/geyser/level/chunk/GeyserChunk.java b/core/src/main/java/org/geysermc/geyser/level/chunk/GeyserChunk.java index ca8c4db1d1e..74e03619367 100644 --- a/core/src/main/java/org/geysermc/geyser/level/chunk/GeyserChunk.java +++ b/core/src/main/java/org/geysermc/geyser/level/chunk/GeyserChunk.java @@ -26,13 +26,15 @@ package org.geysermc.geyser.level.chunk; import com.github.steveice10.mc.protocol.data.game.chunk.DataPalette; +import com.github.steveice10.mc.protocol.data.game.level.LightUpdateData; +import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityInfo; /** - * Acts as a lightweight chunk class that doesn't store biomes, heightmaps or block entities. + * Acts as a lightweight chunk class that doesn't store biomes. */ -public record GeyserChunk(DataPalette[] sections) { +public record GeyserChunk(DataPalette[] sections, BlockEntityInfo[][] blockEntities, LightUpdateData lightData) { - public static GeyserChunk from(DataPalette[] sections) { - return new GeyserChunk(sections); + public static GeyserChunk from(DataPalette[] sections, BlockEntityInfo[][] blockEntities, LightUpdateData lightData) { + return new GeyserChunk(sections, blockEntities, lightData); } } diff --git a/core/src/main/java/org/geysermc/geyser/network/LoggingPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/LoggingPacketHandler.java index 8d2db081aca..fcec20b7a5f 100644 --- a/core/src/main/java/org/geysermc/geyser/network/LoggingPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/LoggingPacketHandler.java @@ -870,4 +870,9 @@ public boolean handle(RequestAbilityPacket packet) { public boolean handle(RequestNetworkSettingsPacket packet) { return defaultHandler(packet); } + + @Override + public boolean handle(SubChunkRequestPacket packet) { + return defaultHandler(packet); + } } \ No newline at end of file diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/ChunkCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/ChunkCache.java index d2c1415a386..142d6946a68 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/ChunkCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/ChunkCache.java @@ -26,6 +26,8 @@ package org.geysermc.geyser.session.cache; import com.github.steveice10.mc.protocol.data.game.chunk.DataPalette; +import com.github.steveice10.mc.protocol.data.game.level.LightUpdateData; +import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityInfo; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import lombok.Getter; @@ -37,6 +39,7 @@ import org.geysermc.geyser.util.MathUtils; public class ChunkCache { + @Getter private final boolean cache; private final Long2ObjectMap chunks; @@ -57,20 +60,20 @@ public ChunkCache(GeyserSession session) { chunks = cache ? new Long2ObjectOpenHashMap<>() : null; } - public void addToCache(int x, int z, DataPalette[] chunks) { + public void addToCache(int x, int z, DataPalette[] chunks, BlockEntityInfo[][] blockEntities, LightUpdateData lightData) { if (!cache) { return; } long chunkPosition = MathUtils.chunkPositionToLong(x, z); - GeyserChunk geyserChunk = GeyserChunk.from(chunks); + GeyserChunk geyserChunk = GeyserChunk.from(chunks, blockEntities, lightData); this.chunks.put(chunkPosition, geyserChunk); } /** * Doesn't check for cache enabled, so don't use this without checking that first! */ - private GeyserChunk getChunk(int chunkX, int chunkZ) { + public GeyserChunk getChunk(int chunkX, int chunkZ) { long chunkPosition = MathUtils.chunkPositionToLong(chunkX, chunkZ); return chunks.getOrDefault(chunkPosition, null); } diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockSubChunkRequestTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockSubChunkRequestTranslator.java new file mode 100644 index 00000000000..fbb40d40134 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockSubChunkRequestTranslator.java @@ -0,0 +1,282 @@ +/* + * 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.bedrock; + +import com.github.steveice10.mc.protocol.data.game.chunk.DataPalette; +import com.github.steveice10.mc.protocol.data.game.level.LightUpdateData; +import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityInfo; +import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityType; +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.nukkitx.math.vector.Vector3i; +import com.nukkitx.nbt.NBTOutputStream; +import com.nukkitx.nbt.NbtMap; +import com.nukkitx.nbt.NbtUtils; +import com.nukkitx.protocol.bedrock.data.HeightMapDataType; +import com.nukkitx.protocol.bedrock.data.SubChunkData; +import com.nukkitx.protocol.bedrock.data.SubChunkRequestResult; +import com.nukkitx.protocol.bedrock.packet.SubChunkPacket; +import com.nukkitx.protocol.bedrock.packet.SubChunkRequestPacket; +import org.geysermc.geyser.level.BedrockDimension; +import org.geysermc.geyser.level.block.BlockStateValues; +import org.geysermc.geyser.level.chunk.GeyserChunk; +import org.geysermc.geyser.level.chunk.GeyserChunkSection; +import org.geysermc.geyser.session.GeyserSession; +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.translator.protocol.java.level.JavaLevelChunkWithLightTranslator; +import org.geysermc.geyser.util.BlockEntityUtils; +import org.geysermc.geyser.util.DimensionUtils; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufOutputStream; +import io.netty.buffer.ByteBufUtil; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; + +import java.io.IOException; +import java.util.BitSet; +import java.util.List; + +@Translator(packet = SubChunkRequestPacket.class) +public class BedrockSubChunkRequestTranslator extends PacketTranslator { + @Override + public void translate(GeyserSession session, SubChunkRequestPacket packet) { + Vector3i centerPosition = packet.getSubChunkPosition(); + + SubChunkPacket subChunkPacket = new SubChunkPacket(); + subChunkPacket.setDimension(packet.getDimension()); + subChunkPacket.setCenterPosition(centerPosition); + + int javaSubChunkOffset = session.getChunkCache().getChunkMinY(); + + BedrockDimension bedrockDimension = session.getChunkCache().getBedrockDimension(); + int bedrockSubChunkMinY = bedrockDimension.minY() >> 4; + int bedrockSubChunkMaxY = bedrockSubChunkMinY + (bedrockDimension.height() >> 4); + + ByteBuf byteBuf = null; + + try { + for (Vector3i positionOffset : packet.getPositionOffsets()) { + SubChunkData subChunkData = new SubChunkData(); + subChunkData.setPosition(positionOffset); + subChunkPacket.getSubChunks().add(subChunkData); + + if (!session.getChunkCache().isCache()) { + subChunkData.setResult(SubChunkRequestResult.UNDEFINED); + subChunkData.setData(new byte[0]); + subChunkData.setHeightMapType(HeightMapDataType.NO_DATA); + continue; + } + + if (packet.getDimension() != DimensionUtils.javaToBedrock(session.getDimension())) { + subChunkData.setResult(SubChunkRequestResult.INVALID_DIMENSION); + subChunkData.setData(new byte[0]); + subChunkData.setHeightMapType(HeightMapDataType.NO_DATA); + continue; + } + + Vector3i position = centerPosition.add(positionOffset); + GeyserChunk chunk = session.getChunkCache().getChunk(position.getX(), position.getZ()); + if (chunk == null) { + subChunkData.setResult(SubChunkRequestResult.CHUNK_NOT_FOUND); + subChunkData.setData(new byte[0]); + subChunkData.setHeightMapType(HeightMapDataType.NO_DATA); + continue; + } + + int sectionY = position.getY() - javaSubChunkOffset; + if (position.getY() < bedrockSubChunkMinY || position.getY() >= bedrockSubChunkMaxY) { + subChunkData.setResult(SubChunkRequestResult.INDEX_OUT_OF_BOUNDS); + subChunkData.setData(new byte[0]); + subChunkData.setHeightMapType(HeightMapDataType.NO_DATA); + continue; + } + + if (sectionY < 0) { + subChunkData.setHeightMapType(HeightMapDataType.NO_DATA); + } else { + LightUpdateData lightData = chunk.lightData(); + BitSet emptyLightMask = lightData.getEmptySkyYMask(); + BitSet lightMask = lightData.getSkyYMask(); + List lightData_ = lightData.getSkyUpdates(); + if (emptyLightMask.get(sectionY + 1)) { + subChunkData.setHeightMapType(HeightMapDataType.TOO_HIGH); + } else if (lightMask.get(sectionY + 1)) { + byte[] belowLight; + if (lightMask.get(sectionY)) { + int belowSection = 0; + for (int i = 0; i < sectionY; i++) { + if (lightMask.get(i)) { + belowSection++; + } + } + belowLight = lightData_.get(belowSection); + } else { + belowLight = null; + } + int lightIndex = 0; + for (int i = 0; i < sectionY + 1; i++) { + if (lightMask.get(i)) { + lightIndex++; + } + } + byte[] light = lightData_.get(lightIndex); + byte[] aboveLight; + if (lightMask.get(sectionY + 2)) { + int aboveSection = 0; + for (int i = 0; i < sectionY + 2; i++) { + if (lightMask.get(i)) { + aboveSection++; + } + } + aboveLight = lightData_.get(aboveSection); + } else { + aboveLight = null; + } + + byte[] heightMapData = new byte[16 * 16]; + boolean lower = true, higher = true; +xyLoop: for (int i = 0; i < heightMapData.length; i++) { + if (aboveLight != null) { + int key = i; + int index = key >> 1; + int part = key & 1; + int value = part == 0 ? aboveLight[index] & 15 : aboveLight[index] >> 4 & 15; + if (value != 0xF) { + heightMapData[i] = 16; + lower = false; + continue; + } + } + for (int y = 15; y != -1; y--) { + int key = i | y << 8; + int index = key >> 1; + int part = key & 1; + int value = part == 0 ? light[index] & 15 : light[index] >> 4 & 15; + if (value != 0xF) { + heightMapData[i] = (byte) y; + lower = false; + higher = false; + continue xyLoop; + } + } + if (belowLight != null) { + int key = i | 15 << 8; + int index = key >> 1; + int part = key & 1; + int value = part == 0 ? belowLight[index] & 15 : belowLight[index] >> 4 & 15; + if (value != 0xF) { + heightMapData[i] = -1; + higher = false; + } + } + } + if (lower) { + subChunkData.setHeightMapType(HeightMapDataType.TOO_LOW); + } else if (higher) { + subChunkData.setHeightMapType(HeightMapDataType.TOO_HIGH); + } else { + subChunkData.setHeightMapType(HeightMapDataType.HAS_DATA); + subChunkData.setHeightMapData(heightMapData); + } + } else { + subChunkData.setHeightMapType(HeightMapDataType.TOO_LOW); + } + } + + DataPalette javaSection = sectionY < 0 || sectionY >= chunk.sections().length ? null : chunk.sections()[sectionY]; + if (javaSection == null) { + subChunkData.setResult(SubChunkRequestResult.SUCCESS_ALL_AIR); + subChunkData.setData(new byte[0]); + continue; + } + + final BlockEntityInfo[] blockEntities = chunk.blockEntities()[sectionY]; + final List bedrockBlockEntities = new ObjectArrayList<>(); + + GeyserChunkSection section = JavaLevelChunkWithLightTranslator.translateSubChunk(session, position, javaSection, bedrockBlockEntities); + + final int chunkBlockX = position.getX() << 4; + final int chunkBlockZ = position.getZ() << 4; + for (BlockEntityInfo blockEntity : blockEntities) { + BlockEntityType type = blockEntity.getType(); + if (type == null) { + // As an example: ViaVersion will send -1 if it cannot find the block entity type + // Vanilla Minecraft gracefully handles this + continue; + } + CompoundTag tag = blockEntity.getNbt(); + 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 + int blockState = javaSection.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 + bedrockBlockEntities.add(session.getGeyser().getWorldManager().getLecternDataAt( + session, x + chunkBlockX, y, z + chunkBlockZ, true)); + continue; + } + + BlockEntityTranslator blockEntityTranslator = BlockEntityUtils.getBlockEntityTranslator(type); + bedrockBlockEntities.add(blockEntityTranslator.getBlockEntityTag(type, x + chunkBlockX, y, z + chunkBlockZ, tag, blockState)); + + // Check for custom skulls + if (session.getPreferencesCache().showCustomSkulls() && type == BlockEntityType.SKULL && tag != null && tag.contains("SkullOwner")) { + SkullBlockEntityTranslator.translateSkull(session, tag, x + chunkBlockX, y, z + chunkBlockZ, blockState); + } + } + + if (byteBuf == null) { + byteBuf = ByteBufAllocator.DEFAULT.buffer(section.estimateNetworkSize() + bedrockBlockEntities.size() * 64); + } else { + byteBuf.clear(); + } + + section.writeToNetwork(byteBuf); + NBTOutputStream nbtStream = NbtUtils.createNetworkWriter(new ByteBufOutputStream(byteBuf)); + for (NbtMap blockEntity : bedrockBlockEntities) { + nbtStream.writeTag(blockEntity); + } + + subChunkData.setResult(SubChunkRequestResult.SUCCESS); + subChunkData.setData(ByteBufUtil.getBytes(byteBuf)); + subChunkPacket.getSubChunks().add(subChunkData); + } + + session.sendUpstreamPacket(subChunkPacket); + } catch (IOException ex) { + session.getGeyser().getLogger().error("IO error while encoding chunk", ex); + } finally { + if (byteBuf != null) { + byteBuf.release(); + } + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelChunkWithLightTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelChunkWithLightTranslator.java index 08125144772..f02984bf278 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelChunkWithLightTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelChunkWithLightTranslator.java @@ -68,9 +68,12 @@ import org.geysermc.geyser.util.ChunkUtils; import java.io.IOException; +import java.util.Arrays; import java.util.BitSet; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import static org.geysermc.geyser.util.ChunkUtils.SERIALIZED_CHUNK_DATA; import static org.geysermc.geyser.util.ChunkUtils.indexYZXtoXZY; @@ -85,36 +88,48 @@ public void translate(GeyserSession session, ClientboundLevelChunkWithLightPacke } // 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(); + int javaSubChunkOffset = session.getChunkCache().getChunkMinY(); + int javaSubChunkCount = session.getChunkCache().getChunkHeightY(); int biomeGlobalPalette = session.getBiomeGlobalPalette(); - DataPalette[] javaChunks = new DataPalette[chunkSize]; - DataPalette[] javaBiomes = new DataPalette[chunkSize]; - - final BlockEntityInfo[] blockEntities = packet.getBlockEntities(); - final List bedrockBlockEntities = new ObjectArrayList<>(blockEntities.length); - - BitSet waterloggedPaletteIds = new BitSet(); - BitSet bedrockOnlyBlockEntityIds = new BitSet(); + DataPalette[] javaChunks = new DataPalette[javaSubChunkCount]; + DataPalette[] javaBiomes = new DataPalette[javaSubChunkCount]; BedrockDimension bedrockDimension = session.getChunkCache().getBedrockDimension(); - int maxBedrockSectionY = (bedrockDimension.height() >> 4) - 1; + int bedrockSubChunkOffset = bedrockDimension.minY() >> 4; + int bedrockSubChunkCount = bedrockDimension.height() >> 4; + int subChunkDiff = javaSubChunkOffset + bedrockSubChunkOffset; + + final boolean requestSubChunks = session.getChunkCache().isCache(); + final BlockEntityInfo[] blockEntities = packet.getBlockEntities(); + final Map> _blockEntitiesBySection; + final BlockEntityInfo[][] blockEntitiesBySection; + final List bedrockBlockEntities; + final GeyserChunkSection[] sections; + if (requestSubChunks) { + _blockEntitiesBySection = Arrays.stream(blockEntities).collect(Collectors.groupingBy(blockEntity -> (blockEntity.getY() >> 4) - javaSubChunkOffset)); + blockEntitiesBySection = new BlockEntityInfo[javaSubChunkCount][]; + bedrockBlockEntities = null; + sections = new GeyserChunkSection[javaChunks.length - subChunkDiff]; + } else { + _blockEntitiesBySection = null; + blockEntitiesBySection = null; + bedrockBlockEntities = new ObjectArrayList<>(blockEntities.length); + sections = new GeyserChunkSection[javaChunks.length - subChunkDiff]; + } int sectionCount; byte[] payload; ByteBuf byteBuf = null; - GeyserChunkSection[] sections = new GeyserChunkSection[javaChunks.length - (yOffset + (bedrockDimension.minY() >> 4))]; try { ByteBuf in = Unpooled.wrappedBuffer(packet.getChunkData()); - for (int sectionY = 0; sectionY < chunkSize; sectionY++) { + for (int sectionY = 0; sectionY < javaSubChunkCount; sectionY++) { ChunkSection javaSection = session.getCodecHelper().readChunkSection(in, biomeGlobalPalette); - javaChunks[sectionY] = javaSection.getChunkData(); javaBiomes[sectionY] = javaSection.getBiomeData(); - int bedrockSectionY = sectionY + (yOffset - (bedrockDimension.minY() >> 4)); - if (bedrockSectionY < 0 || maxBedrockSectionY < bedrockSectionY) { + int bedrockSectionY = sectionY + (javaSubChunkOffset - (bedrockDimension.minY() >> 4)); + if (bedrockSectionY < 0 || bedrockSectionY > bedrockSubChunkCount - 1) { // Ignore this chunk section since it goes outside the bounds accepted by the Bedrock client continue; } @@ -124,155 +139,51 @@ public void translate(GeyserSession session, ClientboundLevelChunkWithLightPacke 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().getBedrockAirId()); - 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().contains(javaId)) { - section.getBlockStorageArray()[1].setFullBlock(xzy, session.getBlockMappings().getBedrockWaterId()); - } - - // 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.isNonWaterCauldron(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; - 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().contains(javaId)) { - BlockStorage waterlogged = new BlockStorage(SingletonBitArray.INSTANCE, IntLists.singleton(session.getBlockMappings().getBedrockWaterId())); - sections[bedrockSectionY] = new GeyserChunkSection(new BlockStorage[] {blockStorage, waterlogged}); - } else { - sections[bedrockSectionY] = new GeyserChunkSection(new BlockStorage[] {blockStorage}); - } - // If a chunk contains all of the same piston or flower pot then god help us - continue; + javaChunks[sectionY] = javaSection.getChunkData(); + if (requestSubChunks) { + blockEntitiesBySection[sectionY] = _blockEntitiesBySection.getOrDefault(sectionY, Collections.emptyList()).toArray(new BlockEntityInfo[0]); + sections[bedrockSectionY] = new GeyserChunkSection(null); + } else { + sections[bedrockSectionY] = translateSubChunk(session, Vector3i.from(packet.getX(), sectionY + javaSubChunkOffset, packet.getZ()), javaSection.getChunkData(), bedrockBlockEntities); } - - IntList bedrockPalette = new IntArrayList(javaPalette.size()); - waterloggedPaletteIds.clear(); - bedrockOnlyBlockEntityIds.clear(); - - // Iterate through palette and convert state IDs to Bedrock, doing some additional checks as we go - for (int i = 0; i < javaPalette.size(); i++) { - int javaId = javaPalette.idToState(i); - bedrockPalette.add(session.getBlockMappings().getBedrockBlockId(javaId)); - - if (BlockRegistries.WATERLOGGED.get().contains(javaId)) { - waterloggedPaletteIds.set(i); - } - - // 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.isNonWaterCauldron(javaId)) { - bedrockOnlyBlockEntityIds.set(i); + } + session.getChunkCache().addToCache(packet.getX(), packet.getZ(), javaChunks, blockEntitiesBySection, requestSubChunks ? packet.getLightData() : null); + + if (!requestSubChunks) { + final int chunkBlockX = packet.getX() << 4; + final int chunkBlockZ = packet.getZ() << 4; + for (BlockEntityInfo blockEntity : blockEntities) { + BlockEntityType type = blockEntity.getType(); + if (type == null) { + // As an example: ViaVersion will send -1 if it cannot find the block entity type + // Vanilla Minecraft gracefully handles this + continue; } - } - - // 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) - )); + CompoundTag tag = blockEntity.getNbt(); + 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) - javaSubChunkOffset]; + if (section != null) { + 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 + bedrockBlockEntities.add(session.getGeyser().getWorldManager().getLecternDataAt( + session, x + chunkBlockX, y, z + chunkBlockZ, true)); + continue; } - } - } - - BitArray bedrockData = BitArrayVersion.forBitsCeil(javaData.getBitsPerEntry()).createArray(BlockStorage.SIZE); - BlockStorage layer0 = new BlockStorage(bedrockData, bedrockPalette); - BlockStorage[] layers; - // Convert data array from YZX to XZY coordinate order - if (waterloggedPaletteIds.isEmpty()) { - // No blocks are waterlogged, simply convert coordinate order - // This could probably be optimized further... - for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) { - bedrockData.set(indexYZXtoXZY(yzx), javaData.get(yzx)); - } + BlockEntityTranslator blockEntityTranslator = BlockEntityUtils.getBlockEntityTranslator(type); + bedrockBlockEntities.add(blockEntityTranslator.getBlockEntityTag(type, x + chunkBlockX, y, z + chunkBlockZ, tag, blockState)); - layers = new BlockStorage[]{ layer0 }; - } else { - // 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); + // Check for custom skulls + if (session.getPreferencesCache().showCustomSkulls() && type == BlockEntityType.SKULL && tag != null && tag.contains("SkullOwner")) { + SkullBlockEntityTranslator.translateSkull(session, tag, x + chunkBlockX, y, z + chunkBlockZ, blockState); } } - - // V1 palette - IntList layer1Palette = IntList.of( - session.getBlockMappings().getBedrockAirId(), // Air - see BlockStorage's constructor for more information - session.getBlockMappings().getBedrockWaterId()); - - layers = new BlockStorage[]{ layer0, new BlockStorage(BitArrayVersion.V1.createArray(BlockStorage.SIZE, layer1Data), layer1Palette) }; - } - - sections[bedrockSectionY] = new GeyserChunkSection(layers); - } - - 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(); - if (type == null) { - // As an example: ViaVersion will send -1 if it cannot find the block entity type - // Vanilla Minecraft gracefully handles this - continue; - } - CompoundTag tag = blockEntity.getNbt(); - 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 - bedrockBlockEntities.add(session.getGeyser().getWorldManager().getLecternDataAt( - session, x + chunkBlockX, y, z + chunkBlockZ, true)); - continue; - } - - BlockEntityTranslator blockEntityTranslator = BlockEntityUtils.getBlockEntityTranslator(type); - bedrockBlockEntities.add(blockEntityTranslator.getBlockEntityTag(type, x + chunkBlockX, y, z + chunkBlockZ, tag, blockState)); - - // Check for custom skulls - if (session.getPreferencesCache().showCustomSkulls() && type == BlockEntityType.SKULL && tag != null && tag.contains("SkullOwner")) { - SkullBlockEntityTranslator.translateSkull(session, tag, x + chunkBlockX, y, z + chunkBlockZ, blockState); } } @@ -283,58 +194,61 @@ public void translate(GeyserSession session, ClientboundLevelChunkWithLightPacke } 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 += SERIALIZED_CHUNK_DATA.length; + if (!requestSubChunks) { + for (int i = 0; i < sectionCount; i++) { + GeyserChunkSection section = sections[i]; + if (section != null) { + size += section.estimateNetworkSize(); + } else { + size += SERIALIZED_CHUNK_DATA.length; + } } } - size += ChunkUtils.EMPTY_BIOME_DATA.length * biomeCount; - size += 1; // Border blocks - size += bedrockBlockEntities.size() * 64; // Conservative estimate of 64 bytes per tile entity + size += ChunkUtils.EMPTY_BIOME_DATA.length * bedrockSubChunkCount + 1; // Consists only of biome data and border blocks // Allocate output buffer byteBuf = ByteBufAllocator.DEFAULT.buffer(size); - for (int i = 0; i < sectionCount; i++) { - GeyserChunkSection section = sections[i]; - if (section != null) { - section.writeToNetwork(byteBuf); - } else { - byteBuf.writeBytes(SERIALIZED_CHUNK_DATA); + + if (!requestSubChunks) { + for (int i = 0; i < sectionCount; i++) { + GeyserChunkSection section = sections[i]; + + if (section != null) { + section.writeToNetwork(byteBuf); + } else { + byteBuf.writeBytes(SERIALIZED_CHUNK_DATA); + } } } - int dimensionOffset = bedrockDimension.minY() >> 4; - for (int i = 0; i < biomeCount; i++) { - int biomeYOffset = dimensionOffset + i; - if (biomeYOffset < yOffset) { + // As of 1.18.30, the amount of biomes read is dependent on how high Bedrock thinks the dimension is + for (int i = 0; i < bedrockSubChunkCount; i++) { + int biomeYOffset = bedrockSubChunkOffset + i; + if (biomeYOffset < javaSubChunkOffset) { // 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)) { + if (biomeYOffset >= (javaSubChunkCount + javaSubChunkOffset)) { // 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); + BiomeTranslator.toNewBedrockBiome(session, javaBiomes[i + (bedrockSubChunkOffset - javaSubChunkOffset)]).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); + if (!requestSubChunks) { + NBTOutputStream nbtStream = NbtUtils.createNetworkWriter(new ByteBufOutputStream(byteBuf)); + for (NbtMap blockEntity : bedrockBlockEntities) { + nbtStream.writeTag(blockEntity); + } } // Copy data into byte[], because the protocol lib really likes things that are s l o w @@ -350,9 +264,10 @@ public void translate(GeyserSession session, ClientboundLevelChunkWithLightPacke LevelChunkPacket levelChunkPacket = new LevelChunkPacket(); levelChunkPacket.setSubChunksLength(sectionCount); - levelChunkPacket.setCachingEnabled(false); levelChunkPacket.setChunkX(packet.getX()); levelChunkPacket.setChunkZ(packet.getZ()); + levelChunkPacket.setRequestSubChunks(requestSubChunks); + levelChunkPacket.setSubChunkLimit(sectionCount - 1); levelChunkPacket.setData(payload); session.sendUpstreamPacket(levelChunkPacket); @@ -365,4 +280,118 @@ public void translate(GeyserSession session, ClientboundLevelChunkWithLightPacke } } } + + public static GeyserChunkSection translateSubChunk(GeyserSession session, Vector3i position, DataPalette javaSection, List bedrockBlockEntities) { + Palette javaPalette = javaSection.getPalette(); + BitStorage javaData = javaSection.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().getBedrockAirId()); + 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().contains(javaId)) { + section.getBlockStorageArray()[1].setFullBlock(xzy, session.getBlockMappings().getBedrockWaterId()); + } + + // 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.isNonWaterCauldron(javaId)) { + bedrockBlockEntities.add(BedrockOnlyBlockEntity.getTag(session, + Vector3i.from((position.getX() << 4) + (yzx & 0xF), (position.getY() << 4) + ((yzx >> 8) & 0xF), (position.getZ() << 4) + ((yzx >> 4) & 0xF)), + javaId + )); + } + } + return section; + } + + 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().contains(javaId)) { + BlockStorage waterlogged = new BlockStorage(SingletonBitArray.INSTANCE, IntLists.singleton(session.getBlockMappings().getBedrockWaterId())); + return new GeyserChunkSection(new BlockStorage[] {blockStorage, waterlogged}); + } else { + return new GeyserChunkSection(new BlockStorage[] {blockStorage}); + } + } + + IntList bedrockPalette = new IntArrayList(javaPalette.size()); + BitSet waterloggedPaletteIds = new BitSet(); + BitSet bedrockOnlyBlockEntityIds = new BitSet(); + + // Iterate through palette and convert state IDs to Bedrock, doing some additional checks as we go + for (int i = 0; i < javaPalette.size(); i++) { + int javaId = javaPalette.idToState(i); + bedrockPalette.add(session.getBlockMappings().getBedrockBlockId(javaId)); + + if (BlockRegistries.WATERLOGGED.get().contains(javaId)) { + waterloggedPaletteIds.set(i); + } + + // 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.isNonWaterCauldron(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((position.getX() << 4) + (yzx & 0xF), (position.getY() << 4) + ((yzx >> 8) & 0xF), (position.getZ() << 4) + ((yzx >> 4) & 0xF)), + javaPalette.idToState(paletteId) + )); + } + } + } + + BitArray bedrockData = BitArrayVersion.forBitsCeil(javaData.getBitsPerEntry()).createArray(BlockStorage.SIZE); + BlockStorage layer0 = new BlockStorage(bedrockData, bedrockPalette); + BlockStorage[] layers; + + // Convert data array from YZX to XZY coordinate order + if (waterloggedPaletteIds.isEmpty()) { + // No blocks are waterlogged, simply convert coordinate order + // This could probably be optimized further... + for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) { + bedrockData.set(indexYZXtoXZY(yzx), javaData.get(yzx)); + } + + layers = new BlockStorage[]{ layer0 }; + } else { + // 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().getBedrockAirId(), // Air - see BlockStorage's constructor for more information + session.getBlockMappings().getBedrockWaterId()); + + layers = new BlockStorage[]{ layer0, new BlockStorage(BitArrayVersion.V1.createArray(BlockStorage.SIZE, layer1Data), layer1Palette) }; + } + + return new GeyserChunkSection(layers); + } } \ No newline at end of file