diff --git a/connector/pom.xml b/connector/pom.xml index 0b9913ea2..1b6e9fc4e 100644 --- a/connector/pom.xml +++ b/connector/pom.xml @@ -149,7 +149,7 @@ com.github.steveice10 mcprotocollib - 1.17.1-3-SNAPSHOT + 1.18-pre-SNAPSHOT compile diff --git a/connector/src/main/java/org/geysermc/connector/network/session/cache/ChunkCache.java b/connector/src/main/java/org/geysermc/connector/network/session/cache/ChunkCache.java index 6154ff65f..b9f3b4db6 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/cache/ChunkCache.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/cache/ChunkCache.java @@ -25,8 +25,7 @@ package org.geysermc.connector.network.session.cache; -import com.github.steveice10.mc.protocol.data.game.chunk.Chunk; -import com.github.steveice10.mc.protocol.data.game.chunk.Column; +import com.github.steveice10.mc.protocol.data.game.chunk.DataPalette; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import lombok.Getter; @@ -57,14 +56,14 @@ public class ChunkCache { chunks = cache ? new Long2ObjectOpenHashMap<>() : null; } - public void addToCache(Column chunk) { + public void addToCache(int x, int z, DataPalette[] chunks) { if (!cache) { return; } - long chunkPosition = MathUtils.chunkPositionToLong(chunk.getX(), chunk.getZ()); - GeyserColumn geyserColumn = GeyserColumn.from(this, chunk); - chunks.put(chunkPosition, geyserColumn); + long chunkPosition = MathUtils.chunkPositionToLong(x, z); + GeyserColumn geyserColumn = GeyserColumn.from(chunks); + this.chunks.put(chunkPosition, geyserColumn); } /** @@ -90,11 +89,11 @@ public class ChunkCache { return; } - Chunk chunk = column.getChunks()[(y - minY) >> 4]; + DataPalette chunk = column.getChunks()[(y - minY) >> 4]; if (chunk == null) { if (block != BlockStateValues.JAVA_AIR_ID) { // A previously empty chunk, which is no longer empty as a block has been added to it - chunk = new Chunk(); + chunk = DataPalette.createForChunk(); // Fixes the chunk assuming that all blocks is the `block` variable we are updating. /shrug chunk.getPalette().stateToId(BlockStateValues.JAVA_AIR_ID); column.getChunks()[(y - minY) >> 4] = chunk; @@ -122,7 +121,7 @@ public class ChunkCache { return BlockStateValues.JAVA_AIR_ID; } - Chunk chunk = column.getChunks()[(y - minY) >> 4]; + DataPalette chunk = column.getChunks()[(y - minY) >> 4]; if (chunk != null) { return chunk.get(x & 0xF, y & 0xF, z & 0xF); } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaLoginTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaLoginTranslator.java index 9384a3515..cbcbe9f1d 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaLoginTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/JavaLoginTranslator.java @@ -106,7 +106,8 @@ public class JavaLoginTranslator extends PacketTranslator { - - @Override - public void translate(GeyserSession session, ClientboundLevelChunkPacket packet) { - if (session.isSpawned()) { - ChunkUtils.updateChunkPosition(session, session.getPlayerEntity().getPosition().toInt()); - } - - session.getChunkCache().addToCache(packet.getColumn()); - Column column = packet.getColumn(); - - // Ensure that, if the player is using lower world heights, the position is not offset - int yOffset = session.getChunkCache().getChunkMinY(); - - ChunkUtils.ChunkData chunkData = ChunkUtils.translateToBedrock(session, column, yOffset); - ChunkSection[] sections = chunkData.sections(); - - // Find highest section - int sectionCount = sections.length - 1; - while (sectionCount >= 0 && sections[sectionCount] == null) { - sectionCount--; - } - sectionCount++; - - // Estimate chunk size - int size = 0; - for (int i = 0; i < sectionCount; i++) { - ChunkSection section = sections[i]; - size += (section != null ? section : session.getBlockMappings().getEmptyChunkSection()).estimateNetworkSize(); - } - size += ChunkUtils.EMPTY_CHUNK_DATA.length; // Consists only of biome data - size += 1; // Border blocks - size += 1; // Extra data length (always 0) - size += chunkData.blockEntities().length * 64; // Conservative estimate of 64 bytes per tile entity - - // Allocate output buffer - ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(size); - byte[] payload; - try { - for (int i = 0; i < sectionCount; i++) { - ChunkSection section = sections[i]; - (section != null ? section : session.getBlockMappings().getEmptyChunkSection()).writeToNetwork(byteBuf); - } - - // At this point we're dealing with Bedrock chunk sections - boolean overworld = session.getChunkCache().isExtendedHeight(); - int dimensionOffset = (overworld ? MINIMUM_ACCEPTED_HEIGHT_OVERWORLD : MINIMUM_ACCEPTED_HEIGHT) >> 4; - for (int i = 0; i < sectionCount; 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; - } - BiomeTranslator.toNewBedrockBiome(session, column.getBiomeData(), i + (dimensionOffset - yOffset)).writeToNetwork(byteBuf); - } - - // As of 1.17.10, Bedrock hardcodes to always read 32 biome sections - int remainingEmptyBiomes = 32 - sectionCount; - for (int i = 0; i < remainingEmptyBiomes; i++) { - byteBuf.writeBytes(ChunkUtils.EMPTY_BIOME_DATA); - } - - byteBuf.writeByte(0); // Border blocks - Edu edition only - VarInts.writeUnsignedInt(byteBuf, 0); // extra data length, 0 for now - - // Encode tile entities into buffer - NBTOutputStream nbtStream = NbtUtils.createNetworkWriter(new ByteBufOutputStream(byteBuf)); - for (NbtMap blockEntity : chunkData.blockEntities()) { - nbtStream.writeTag(blockEntity); - } - - // Copy data into byte[], because the protocol lib really likes things that are s l o w - byteBuf.readBytes(payload = new byte[byteBuf.readableBytes()]); - } catch (IOException e) { - session.getConnector().getLogger().error("IO error while encoding chunk", e); - return; - } finally { - byteBuf.release(); // Release buffer to allow buffer pooling to be useful - } - - LevelChunkPacket levelChunkPacket = new LevelChunkPacket(); - levelChunkPacket.setSubChunksLength(sectionCount); - levelChunkPacket.setCachingEnabled(false); - levelChunkPacket.setChunkX(column.getX()); - levelChunkPacket.setChunkZ(column.getZ()); - levelChunkPacket.setData(payload); - session.sendUpstreamPacket(levelChunkPacket); - } -} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/java/level/JavaLevelChunkWithLightTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/java/level/JavaLevelChunkWithLightTranslator.java new file mode 100644 index 000000000..b6e7880f0 --- /dev/null +++ b/connector/src/main/java/org/geysermc/connector/network/translators/java/level/JavaLevelChunkWithLightTranslator.java @@ -0,0 +1,361 @@ +/* + * Copyright (c) 2019-2021 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.connector.network.translators.java.level; + +import com.github.steveice10.mc.protocol.data.game.chunk.BitStorage; +import com.github.steveice10.mc.protocol.data.game.chunk.Chunk; +import com.github.steveice10.mc.protocol.data.game.chunk.DataPalette; +import com.github.steveice10.mc.protocol.data.game.chunk.palette.GlobalPalette; +import com.github.steveice10.mc.protocol.data.game.chunk.palette.Palette; +import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityInfo; +import com.github.steveice10.mc.protocol.packet.ingame.clientbound.level.ClientboundLevelChunkWithLightPacket; +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.StringTag; +import com.github.steveice10.opennbt.tag.builtin.Tag; +import com.github.steveice10.packetlib.io.NetInput; +import com.github.steveice10.packetlib.io.stream.StreamNetInput; +import com.nukkitx.math.vector.Vector3i; +import com.nukkitx.nbt.NBTOutputStream; +import com.nukkitx.nbt.NbtMap; +import com.nukkitx.nbt.NbtUtils; +import com.nukkitx.network.VarInts; +import com.nukkitx.protocol.bedrock.packet.LevelChunkPacket; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufOutputStream; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import org.geysermc.connector.GeyserConnector; +import org.geysermc.connector.network.session.GeyserSession; +import org.geysermc.connector.network.translators.PacketTranslator; +import org.geysermc.connector.network.translators.Translator; +import org.geysermc.connector.network.translators.world.BiomeTranslator; +import org.geysermc.connector.network.translators.world.block.BlockStateValues; +import org.geysermc.connector.network.translators.world.block.entity.BedrockOnlyBlockEntity; +import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator; +import org.geysermc.connector.network.translators.world.block.entity.SkullBlockEntityTranslator; +import org.geysermc.connector.network.translators.world.chunk.BlockStorage; +import org.geysermc.connector.network.translators.world.chunk.ChunkSection; +import org.geysermc.connector.network.translators.world.chunk.bitarray.BitArray; +import org.geysermc.connector.network.translators.world.chunk.bitarray.BitArrayVersion; +import org.geysermc.connector.registry.BlockRegistries; +import org.geysermc.connector.utils.BlockEntityUtils; +import org.geysermc.connector.utils.ChunkUtils; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; + +import static org.geysermc.connector.utils.ChunkUtils.*; + +@Translator(packet = ClientboundLevelChunkWithLightPacket.class) +public class JavaLevelChunkWithLightTranslator extends PacketTranslator { + + @Override + public void translate(GeyserSession session, ClientboundLevelChunkWithLightPacket packet) { + if (session.isSpawned()) { + ChunkUtils.updateChunkPosition(session, session.getPlayerEntity().getPosition().toInt()); + } + + //todo session.getChunkCache().addToCache(packet.getColumn()); + + // Ensure that, if the player is using lower world heights, the position is not offset + int yOffset = session.getChunkCache().getChunkMinY(); + + // Temporarily stores compound tags of Bedrock-only block entities + List bedrockOnlyBlockEntities = new ArrayList<>(); + DataPalette[] javaChunks = new DataPalette[session.getChunkCache().getChunkHeightY()]; + DataPalette[] javaBiomes = new DataPalette[session.getChunkCache().getChunkHeightY()]; + + BitSet waterloggedPaletteIds = new BitSet(); + BitSet pistonOrFlowerPaletteIds = new BitSet(); + + boolean overworld = session.getChunkCache().isExtendedHeight(); + int maxBedrockSectionY = ((overworld ? MAXIMUM_ACCEPTED_HEIGHT_OVERWORLD : MAXIMUM_ACCEPTED_HEIGHT) >> 4) - 1; + + int sectionCount; + byte[] payload; + ByteBuf byteBuf = null; + ChunkSection[] sections = new ChunkSection[javaChunks.length - yOffset]; + + try { + NetInput in = new StreamNetInput(new ByteArrayInputStream(packet.getChunkData())); + for (int sectionY = 0; sectionY < session.getChunkCache().getChunkHeightY(); sectionY++) { + int bedrockSectionY = sectionY + (yOffset - ((overworld ? MINIMUM_ACCEPTED_HEIGHT_OVERWORLD : MINIMUM_ACCEPTED_HEIGHT) >> 4)); + if (bedrockSectionY < 0 || maxBedrockSectionY < bedrockSectionY) { + // Ignore this chunk section since it goes outside the bounds accepted by the Bedrock client + continue; + } + + Chunk javaSection = Chunk.read(in); + javaChunks[sectionY] = javaSection.getChunkData(); + javaBiomes[sectionY] = javaSection.getBiomeData(); + + // No need to encode an empty section... + if (javaSection.isBlockCountEmpty()) { + 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 + ChunkSection section = new ChunkSection(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)) { + bedrockOnlyBlockEntities.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; + } + + IntList bedrockPalette = new IntArrayList(javaPalette.size()); + waterloggedPaletteIds.clear(); + pistonOrFlowerPaletteIds.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 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)) { + pistonOrFlowerPaletteIds.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 (!pistonOrFlowerPaletteIds.isEmpty()) { + for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) { + int paletteId = javaData.get(yzx); + if (pistonOrFlowerPaletteIds.get(paletteId)) { + bedrockOnlyBlockEntities.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) + )); + } + } + } + + 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 = new IntArrayList(2); + layer1Palette.add(session.getBlockMappings().getBedrockAirId()); // Air - see BlockStorage's constructor for more information + layer1Palette.add(session.getBlockMappings().getBedrockWaterId()); + + layers = new BlockStorage[]{ layer0, new BlockStorage(BitArrayVersion.V1.createArray(BlockStorage.SIZE, layer1Data), layer1Palette) }; + } + + sections[bedrockSectionY] = new ChunkSection(layers); + } + + session.getChunkCache().addToCache(packet.getX(), packet.getZ(), javaChunks); + + BlockEntityInfo[] blockEntities = packet.getBlockEntities(); + NbtMap[] bedrockBlockEntities = new NbtMap[blockEntities.length + bedrockOnlyBlockEntities.size()]; + int blockEntityCount = 0; + while (blockEntityCount < blockEntities.length) { + BlockEntityInfo blockEntity = blockEntities[blockEntityCount]; + CompoundTag tag = blockEntity.getNbt(); + // TODO use the actual name + String tagName; + if (tag != null) { + Tag idTag = tag.get("id"); + if (idTag != null) { + tagName = (String) idTag.getValue(); + } else { + tagName = "Empty"; + // Sometimes legacy tags have their ID be a StringTag with empty value + for (Tag subTag : tag) { + if (subTag instanceof StringTag stringTag) { + if (stringTag.getValue().isEmpty()) { + tagName = stringTag.getName(); + break; + } + } + } + if (tagName.equals("Empty")) { + GeyserConnector.getInstance().getLogger().debug("Got tag with no id: " + tag.getValue()); + } + } + } else { + tagName = "Empty"; + } + + String id = BlockEntityUtils.getBedrockBlockEntityId(tagName); + int x = blockEntity.getX(); + int y = blockEntity.getY(); + int z = blockEntity.getZ(); + + // Get the Java block state ID from block entity position + DataPalette section = javaChunks[(blockEntity.getY() >> 4) - yOffset]; + int blockState = section.get(x & 0xF, y & 0xF, z & 0xF); + + if (tagName.equals("minecraft:lectern") && BlockStateValues.getLecternBookStates().get(blockState)) { + // If getLecternBookStates is false, let's just treat it like a normal block entity + bedrockBlockEntities[blockEntityCount] = session.getConnector().getWorldManager().getLecternDataAt( + session, blockEntity.getX(), blockEntity.getY(), blockEntity.getZ(), true); + blockEntityCount++; + continue; + } + + BlockEntityTranslator blockEntityTranslator = BlockEntityUtils.getBlockEntityTranslator(id); + bedrockBlockEntities[blockEntityCount] = blockEntityTranslator.getBlockEntityTag(tagName, x, y, z, tag, blockState); + + // Check for custom skulls + if (session.getPreferencesCache().showCustomSkulls() && tag != null && tag.contains("SkullOwner")) { + SkullBlockEntityTranslator.spawnPlayer(session, tag, blockState); + } + blockEntityCount++; + } + + // Append Bedrock-exclusive block entities to output array + for (NbtMap tag : bedrockOnlyBlockEntities) { + bedrockBlockEntities[blockEntityCount] = tag; + blockEntityCount++; + } + + // Find highest section + sectionCount = sections.length - 1; + while (sectionCount >= 0 && sections[sectionCount] == null) { + sectionCount--; + } + sectionCount++; + + // Estimate chunk size + int size = 0; + for (int i = 0; i < sectionCount; i++) { + ChunkSection section = sections[i]; + size += (section != null ? section : session.getBlockMappings().getEmptyChunkSection()).estimateNetworkSize(); + } + size += ChunkUtils.EMPTY_CHUNK_DATA.length; // Consists only of biome data + size += 1; // Border blocks + size += 1; // Extra data length (always 0) + size += bedrockBlockEntities.length * 64; // Conservative estimate of 64 bytes per tile entity + + // Allocate output buffer + byteBuf = ByteBufAllocator.DEFAULT.buffer(size); + for (int i = 0; i < sectionCount; i++) { + ChunkSection section = sections[i]; + (section != null ? section : session.getBlockMappings().getEmptyChunkSection()).writeToNetwork(byteBuf); + } + + // At this point we're dealing with Bedrock chunk sections + int dimensionOffset = (overworld ? MINIMUM_ACCEPTED_HEIGHT_OVERWORLD : MINIMUM_ACCEPTED_HEIGHT) >> 4; + for (int i = 0; i < sectionCount; 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; + } + BiomeTranslator.toNewBedrockBiome(session, javaBiomes[i]).writeToNetwork(byteBuf); + } + + // As of 1.17.10, Bedrock hardcodes to always read 32 biome sections + int remainingEmptyBiomes = 32 - sectionCount; + for (int i = 0; i < remainingEmptyBiomes; i++) { + byteBuf.writeBytes(ChunkUtils.EMPTY_BIOME_DATA); + } + + byteBuf.writeByte(0); // Border blocks - Edu edition only + VarInts.writeUnsignedInt(byteBuf, 0); // extra data length, 0 for now + + // Encode tile entities into buffer + 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 + byteBuf.readBytes(payload = new byte[byteBuf.readableBytes()]); + } catch (IOException e) { + session.getConnector().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(payload); + session.sendUpstreamPacket(levelChunkPacket); + } +} diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/BiomeTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/BiomeTranslator.java index baac1abc1..542b3002e 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/BiomeTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/BiomeTranslator.java @@ -25,6 +25,8 @@ package org.geysermc.connector.network.translators.world; +import com.github.steveice10.mc.protocol.data.game.chunk.DataPalette; +import com.github.steveice10.mc.protocol.data.game.chunk.palette.SingletonPalette; import com.github.steveice10.opennbt.tag.builtin.*; import it.unimi.dsi.fastutil.ints.Int2IntMap; import org.geysermc.connector.network.session.GeyserSession; @@ -98,33 +100,37 @@ public class BiomeTranslator { return bedrockData; } - public static BlockStorage toNewBedrockBiome(GeyserSession session, int[] biomeData, int ySection) { + public static BlockStorage toNewBedrockBiome(GeyserSession session, DataPalette biomeData) { Int2IntMap biomeTranslations = session.getBiomeTranslations(); // As of 1.17.10: the client expects the same format as a chunk but filled with biomes - BlockStorage storage = new BlockStorage(0); + // As of 1.18 this is the same as Java Edition - int biomeY = ySection << 2; - int javaOffsetY = biomeY << 4; - // Each section of biome corresponding to a chunk section contains 4 * 4 * 4 entries - for (int i = 0; i < 64; i++) { - int javaId = biomeData[javaOffsetY | i]; - int x = i & 3; - int y = (i >> 4) & 3; - int z = (i >> 2) & 3; - // Get the Bedrock biome ID override - int biomeId = biomeTranslations.get(javaId); - int idx = storage.idFor(biomeId); - // Convert biome coordinates into block coordinates - // Bedrock expects a full 4096 blocks - for (int blockX = x << 2; blockX < (x << 2) + 4; blockX++) { - for (int blockZ = z << 2; blockZ < (z << 2) + 4; blockZ++) { - for (int blockY = y << 2; blockY < (y << 2) + 4; blockY++) { - storage.getBitArray().set(ChunkSection.blockPosition(blockX, blockY, blockZ), idx); + if (biomeData.getPalette() instanceof SingletonPalette palette) { + int biomeId = biomeTranslations.get(palette.idToState(0)); + return new BlockStorage(biomeId); + } else { + BlockStorage storage = new BlockStorage(0); + + // Each section of biome corresponding to a chunk section contains 4 * 4 * 4 entries + for (int i = 0; i < 64; i++) { + int javaId = biomeData.getPalette().idToState(biomeData.getStorage().get(i)); + int x = i & 3; + int y = (i >> 4) & 3; + int z = (i >> 2) & 3; + // Get the Bedrock biome ID override + int biomeId = biomeTranslations.get(javaId); + int idx = storage.idFor(biomeId); + // Convert biome coordinates into block coordinates + // Bedrock expects a full 4096 blocks + for (int blockX = x << 2; blockX < (x << 2) + 4; blockX++) { + for (int blockZ = z << 2; blockZ < (z << 2) + 4; blockZ++) { + for (int blockY = y << 2; blockY < (y << 2) + 4; blockY++) { + storage.getBitArray().set(ChunkSection.blockPosition(blockX, blockY, blockZ), idx); + } } } } + return storage; } - - return storage; } } diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BlockEntityTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BlockEntityTranslator.java index 41843e96a..a9edb95be 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BlockEntityTranslator.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/block/entity/BlockEntityTranslator.java @@ -42,11 +42,7 @@ public abstract class BlockEntityTranslator { public abstract void translateTag(NbtMapBuilder builder, CompoundTag tag, int blockState); - public NbtMap getBlockEntityTag(String id, CompoundTag tag, int blockState) { - int x = ((IntTag) tag.getValue().get("x")).getValue(); - int y = ((IntTag) tag.getValue().get("y")).getValue(); - int z = ((IntTag) tag.getValue().get("z")).getValue(); - + public NbtMap getBlockEntityTag(String id, int x, int y, int z, CompoundTag tag, int blockState) { NbtMapBuilder tagBuilder = getConstantBedrockTag(BlockEntityUtils.getBedrockBlockEntityId(id), x, y, z); translateTag(tagBuilder, tag, blockState); return tagBuilder.build(); diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/GeyserColumn.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/GeyserColumn.java index 3da5787a5..785616f44 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/GeyserColumn.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/GeyserColumn.java @@ -25,33 +25,21 @@ package org.geysermc.connector.network.translators.world.chunk; -import com.github.steveice10.mc.protocol.data.game.chunk.Chunk; -import com.github.steveice10.mc.protocol.data.game.chunk.Column; +import com.github.steveice10.mc.protocol.data.game.chunk.DataPalette; import lombok.Getter; -import org.geysermc.connector.network.session.cache.ChunkCache; /** - * Acts as a lightweight version of {@link Column} that doesn't store - * biomes or heightmaps. + * Acts as a lightweight chunk class that doesn't store biomes, heightmaps or block entities. */ public class GeyserColumn { @Getter - private final Chunk[] chunks; + private final DataPalette[] chunks; - private GeyserColumn(Chunk[] chunks) { + private GeyserColumn(DataPalette[] chunks) { this.chunks = chunks; } - public static GeyserColumn from(ChunkCache chunkCache, Column column) { - int chunkHeightY = chunkCache.getChunkHeightY(); - Chunk[] chunks; - if (chunkHeightY < column.getChunks().length) { - chunks = new Chunk[chunkHeightY]; - // TODO addresses https://github.com/Steveice10/MCProtocolLib/pull/598#issuecomment-862782392 - System.arraycopy(column.getChunks(), 0, chunks, 0, chunks.length); - } else { - chunks = column.getChunks(); - } + public static GeyserColumn from(DataPalette[] chunks) { return new GeyserColumn(chunks); } } diff --git a/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java b/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java index a8fc5f7f8..1a6952453 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java @@ -25,13 +25,10 @@ package org.geysermc.connector.utils; -import com.github.steveice10.mc.protocol.data.game.chunk.BitStorage; -import com.github.steveice10.mc.protocol.data.game.chunk.Chunk; -import com.github.steveice10.mc.protocol.data.game.chunk.Column; -import com.github.steveice10.mc.protocol.data.game.chunk.palette.GlobalPalette; -import com.github.steveice10.mc.protocol.data.game.chunk.palette.Palette; import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position; -import com.github.steveice10.opennbt.tag.builtin.*; +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.DoubleTag; +import com.github.steveice10.opennbt.tag.builtin.IntTag; import com.nukkitx.math.vector.Vector2i; import com.nukkitx.math.vector.Vector3i; import com.nukkitx.nbt.NbtMap; @@ -40,27 +37,16 @@ import com.nukkitx.protocol.bedrock.packet.NetworkChunkPublisherUpdatePacket; import com.nukkitx.protocol.bedrock.packet.UpdateBlockPacket; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; -import it.unimi.dsi.fastutil.ints.IntArrayList; -import it.unimi.dsi.fastutil.ints.IntList; import lombok.experimental.UtilityClass; -import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.entity.ItemFrameEntity; import org.geysermc.connector.entity.player.SkullPlayerEntity; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.translators.world.block.BlockStateValues; import org.geysermc.connector.network.translators.world.block.entity.BedrockOnlyBlockEntity; -import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator; -import org.geysermc.connector.network.translators.world.block.entity.SkullBlockEntityTranslator; import org.geysermc.connector.network.translators.world.chunk.BlockStorage; import org.geysermc.connector.network.translators.world.chunk.ChunkSection; -import org.geysermc.connector.network.translators.world.chunk.bitarray.BitArray; -import org.geysermc.connector.network.translators.world.chunk.bitarray.BitArrayVersion; import org.geysermc.connector.registry.BlockRegistries; -import java.util.ArrayList; -import java.util.BitSet; -import java.util.List; - import static org.geysermc.connector.network.translators.world.block.BlockStateValues.JAVA_AIR_ID; @UtilityClass @@ -73,8 +59,8 @@ public class ChunkUtils { /** * The maximum chunk height Bedrock Edition will accept, from the lowest point to the highest. */ - private static final int MAXIMUM_ACCEPTED_HEIGHT = 256; - private static final int MAXIMUM_ACCEPTED_HEIGHT_OVERWORLD = 384; + public static final int MAXIMUM_ACCEPTED_HEIGHT = 256; + public static final int MAXIMUM_ACCEPTED_HEIGHT_OVERWORLD = 384; public static final byte[] EMPTY_CHUNK_DATA; public static final byte[] EMPTY_BIOME_DATA; @@ -106,200 +92,10 @@ public class ChunkUtils { } } - private static int indexYZXtoXZY(int yzx) { + public static int indexYZXtoXZY(int yzx) { return (yzx >> 8) | (yzx & 0x0F0) | ((yzx & 0x00F) << 8); } - public static ChunkData translateToBedrock(GeyserSession session, Column column, int yOffset) { - Chunk[] javaSections = column.getChunks(); - ChunkSection[] sections = new ChunkSection[javaSections.length - yOffset]; - - // Temporarily stores compound tags of Bedrock-only block entities - List bedrockOnlyBlockEntities = new ArrayList<>(); - - BitSet waterloggedPaletteIds = new BitSet(); - BitSet pistonOrFlowerPaletteIds = new BitSet(); - - boolean overworld = session.getChunkCache().isExtendedHeight(); - int maxBedrockSectionY = ((overworld ? MAXIMUM_ACCEPTED_HEIGHT_OVERWORLD : MAXIMUM_ACCEPTED_HEIGHT) >> 4) - 1; - - for (int sectionY = 0; sectionY < javaSections.length; sectionY++) { - int bedrockSectionY = sectionY + (yOffset - ((overworld ? MINIMUM_ACCEPTED_HEIGHT_OVERWORLD : MINIMUM_ACCEPTED_HEIGHT) >> 4)); - if (bedrockSectionY < 0 || maxBedrockSectionY < bedrockSectionY) { - // Ignore this chunk section since it goes outside the bounds accepted by the Bedrock client - continue; - } - - Chunk javaSection = javaSections[sectionY]; - - // No need to encode an empty section... - if (javaSection == null || javaSection.isEmpty()) { - continue; - } - - 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 - ChunkSection section = new ChunkSection(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)) { - bedrockOnlyBlockEntities.add(BedrockOnlyBlockEntity.getTag(session, - Vector3i.from((column.getX() << 4) + (yzx & 0xF), ((sectionY + yOffset) << 4) + ((yzx >> 8) & 0xF), (column.getZ() << 4) + ((yzx >> 4) & 0xF)), - javaId - )); - } - } - sections[bedrockSectionY] = section; - continue; - } - - IntList bedrockPalette = new IntArrayList(javaPalette.size()); - waterloggedPaletteIds.clear(); - pistonOrFlowerPaletteIds.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 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)) { - pistonOrFlowerPaletteIds.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 (!pistonOrFlowerPaletteIds.isEmpty()) { - for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) { - int paletteId = javaData.get(yzx); - if (pistonOrFlowerPaletteIds.get(paletteId)) { - bedrockOnlyBlockEntities.add(BedrockOnlyBlockEntity.getTag(session, - Vector3i.from((column.getX() << 4) + (yzx & 0xF), ((sectionY + yOffset) << 4) + ((yzx >> 8) & 0xF), (column.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 = new IntArrayList(2); - layer1Palette.add(session.getBlockMappings().getBedrockAirId()); // Air - see BlockStorage's constructor for more information - layer1Palette.add(session.getBlockMappings().getBedrockWaterId()); - - layers = new BlockStorage[]{ layer0, new BlockStorage(BitArrayVersion.V1.createArray(BlockStorage.SIZE, layer1Data), layer1Palette) }; - } - - sections[bedrockSectionY] = new ChunkSection(layers); - } - - CompoundTag[] blockEntities = column.getTileEntities(); - NbtMap[] bedrockBlockEntities = new NbtMap[blockEntities.length + bedrockOnlyBlockEntities.size()]; - int i = 0; - while (i < blockEntities.length) { - CompoundTag tag = blockEntities[i]; - String tagName; - Tag idTag = tag.get("id"); - if (idTag != null) { - tagName = (String) idTag.getValue(); - } else { - tagName = "Empty"; - // Sometimes legacy tags have their ID be a StringTag with empty value - for (Tag subTag : tag) { - if (subTag instanceof StringTag stringTag) { - if (stringTag.getValue().isEmpty()) { - tagName = stringTag.getName(); - break; - } - } - } - if (tagName.equals("Empty")) { - GeyserConnector.getInstance().getLogger().debug("Got tag with no id: " + tag.getValue()); - } - } - - String id = BlockEntityUtils.getBedrockBlockEntityId(tagName); - int x = (int) tag.get("x").getValue(); - int y = (int) tag.get("y").getValue(); - int z = (int) tag.get("z").getValue(); - - // Get Java blockstate ID from block entity position - int blockState = 0; - Chunk section = column.getChunks()[(y >> 4) - yOffset]; - if (section != null) { - blockState = section.get(x & 0xF, y & 0xF, z & 0xF); - } - - if (tagName.equals("minecraft:lectern") && BlockStateValues.getLecternBookStates().get(blockState)) { - // If getLecternBookStates is false, let's just treat it like a normal block entity - bedrockBlockEntities[i] = session.getConnector().getWorldManager().getLecternDataAt(session, x, y, z, true); - i++; - continue; - } - - BlockEntityTranslator blockEntityTranslator = BlockEntityUtils.getBlockEntityTranslator(id); - bedrockBlockEntities[i] = blockEntityTranslator.getBlockEntityTag(tagName, tag, blockState); - - // Check for custom skulls - if (session.getPreferencesCache().showCustomSkulls() && tag.contains("SkullOwner")) { - SkullBlockEntityTranslator.spawnPlayer(session, tag, blockState); - } - i++; - } - - // Append Bedrock-exclusive block entities to output array - for (NbtMap tag : bedrockOnlyBlockEntities) { - bedrockBlockEntities[i] = tag; - i++; - } - - return new ChunkData(sections, bedrockBlockEntities); - } - public static void updateChunkPosition(GeyserSession session, Vector3i position) { Vector2i chunkPos = session.getLastChunkPosition(); Vector2i newChunkPos = Vector2i.from(position.getX() >> 4, position.getZ() >> 4);