diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/command/GeyserBungeeCommandExecutor.java b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/command/GeyserBungeeCommandExecutor.java index 44edeb336..ad15ead91 100644 --- a/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/command/GeyserBungeeCommandExecutor.java +++ b/bootstrap/bungeecord/src/main/java/org/geysermc/platform/bungeecord/command/GeyserBungeeCommandExecutor.java @@ -35,20 +35,16 @@ import org.geysermc.connector.command.GeyserCommand; import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.utils.LanguageUtils; -import javax.annotation.Nullable; import java.util.Arrays; import java.util.Collections; public class GeyserBungeeCommandExecutor extends Command implements TabExecutor { - private final CommandExecutor commandExecutor; - private final GeyserConnector connector; public GeyserBungeeCommandExecutor(GeyserConnector connector) { super("geyser"); this.commandExecutor = new CommandExecutor(connector); - this.connector = connector; } @Override 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 1fd3ddd5e..4de049d1d 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.ChunkSection; +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(Chunk chunk) { + public void addToCache(int x, int z, DataPalette[] chunks) { if (!cache) { return; } - long chunkPosition = MathUtils.chunkPositionToLong(chunk.getX(), chunk.getZ()); - GeyserChunk geyserChunk = GeyserChunk.from(this, chunk); - chunks.put(chunkPosition, geyserChunk); + long chunkPosition = MathUtils.chunkPositionToLong(x, z); + GeyserChunk geyserChunk = GeyserChunk.from(chunks); + this.chunks.put(chunkPosition, geyserChunk); } /** @@ -85,26 +84,26 @@ public class ChunkCache { return; } - if (y < minY || ((y - minY) >> 4) > chunk.getSections().length - 1) { + if (y < minY || ((y - minY) >> 4) > chunk.sections().length - 1) { // Y likely goes above or below the height limit of this world return; } - ChunkSection section = chunk.getSections()[(y - minY) >> 4]; - if (section == null) { + DataPalette palette = chunk.sections()[(y - minY) >> 4]; + if (palette == null) { if (block != BlockStateValues.JAVA_AIR_ID) { // A previously empty chunk, which is no longer empty as a block has been added to it - section = new ChunkSection(); + palette = DataPalette.createForChunk(); // Fixes the chunk assuming that all blocks is the `block` variable we are updating. /shrug - section.getPalette().stateToId(BlockStateValues.JAVA_AIR_ID); - chunk.getSections()[(y - minY) >> 4] = section; + palette.getPalette().stateToId(BlockStateValues.JAVA_AIR_ID); + chunk.sections()[(y - minY) >> 4] = palette; } else { // Nothing to update return; } } - section.set(x & 0xF, y & 0xF, z & 0xF, block); + palette.set(x & 0xF, y & 0xF, z & 0xF, block); } public int getBlockAt(int x, int y, int z) { @@ -117,12 +116,12 @@ public class ChunkCache { return BlockStateValues.JAVA_AIR_ID; } - if (y < minY || ((y - minY) >> 4) > column.getSections().length - 1) { + if (y < minY || ((y - minY) >> 4) > column.sections().length - 1) { // Y likely goes above or below the height limit of this world return BlockStateValues.JAVA_AIR_ID; } - ChunkSection chunk = column.getSections()[(y - minY) >> 4]; + DataPalette chunk = column.sections()[(y - minY) >> 4]; if (chunk != null) { return chunk.get(x & 0xF, y & 0xF, z & 0xF); } @@ -142,7 +141,7 @@ public class ChunkCache { /** * Manually clears all entries in the chunk cache. * The server is responsible for clearing chunk entries if out of render distance (for example) or switching dimensions, - * but it is the client that must clear chunks in the event of proxy switches. + * but it is the client that must clear sections in the event of proxy switches. */ public void clear() { if (!cache) { 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 { @@ -56,44 +82,235 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator bedrockOnlyBlockEntities = new ArrayList<>(); + DataPalette[] javaChunks = new DataPalette[session.getChunkCache().getChunkHeightY()]; + DataPalette[] javaBiomes = new DataPalette[session.getChunkCache().getChunkHeightY()]; - // Find highest section - int sectionCount = sections.length - 1; - while (sectionCount >= 0 && sections[sectionCount] == null) { - sectionCount--; - } - sectionCount++; + BitSet waterloggedPaletteIds = new BitSet(); + BitSet pistonOrFlowerPaletteIds = new BitSet(); - // Estimate chunk size - int size = 0; - for (int i = 0; i < sectionCount; i++) { - GeyserChunkSection 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 + boolean overworld = session.getChunkCache().isExtendedHeight(); + int maxBedrockSectionY = ((overworld ? MAXIMUM_ACCEPTED_HEIGHT_OVERWORLD : MAXIMUM_ACCEPTED_HEIGHT) >> 4) - 1; - // Allocate output buffer - ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(size); + int sectionCount; byte[] payload; + ByteBuf byteBuf = null; + GeyserChunkSection[] sections = new GeyserChunkSection[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; + } + + ChunkSection javaSection = ChunkSection.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 + 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)) { + 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 GeyserChunkSection(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++) { + GeyserChunkSection 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++) { GeyserChunkSection 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; @@ -102,7 +319,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator> 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(GeyserChunkSection.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(GeyserChunkSection.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/GeyserChunk.java b/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/GeyserChunk.java index 47c0b4cf6..f833fd86f 100644 --- a/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/GeyserChunk.java +++ b/connector/src/main/java/org/geysermc/connector/network/translators/world/chunk/GeyserChunk.java @@ -25,33 +25,15 @@ 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.ChunkSection; +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 Chunk} that doesn't store - * biomes or heightmaps. + * Acts as a lightweight chunk class that doesn't store biomes, heightmaps or block entities. */ -public class GeyserChunk { - @Getter - private final ChunkSection[] sections; +public record GeyserChunk(@Getter DataPalette[] sections) { - private GeyserChunk(ChunkSection[] sections) { - this.sections = sections; - } - - public static GeyserChunk from(ChunkCache chunkCache, Chunk chunk) { - int chunkHeightY = chunkCache.getChunkHeightY(); - ChunkSection[] sections; - if (chunkHeightY < chunk.getSections().length) { - sections = new ChunkSection[chunkHeightY]; - // TODO addresses https://github.com/Steveice10/MCProtocolLib/pull/598#issuecomment-862782392 - System.arraycopy(chunk.getSections(), 0, sections, 0, sections.length); - } else { - sections = chunk.getSections(); - } + public static GeyserChunk from(DataPalette[] sections) { return new GeyserChunk(sections); } } diff --git a/connector/src/main/java/org/geysermc/connector/utils/BlockEntityUtils.java b/connector/src/main/java/org/geysermc/connector/utils/BlockEntityUtils.java index 22e31240d..500c0d723 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/BlockEntityUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/BlockEntityUtils.java @@ -43,7 +43,7 @@ import java.util.Map; public class BlockEntityUtils { /** * A list of all block entities that require the Java block state in order to fill out their block entity information. - * This list will be smaller with cache chunks on as we don't need to double-cache data + * This list will be smaller with cache sections on as we don't need to double-cache data */ public static final ObjectArrayList BEDROCK_ONLY_BLOCK_ENTITIES = new ObjectArrayList<>(); 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 6532d3452..8db82c0ae 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/ChunkUtils.java @@ -25,42 +25,26 @@ 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.ChunkSection; -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; import com.nukkitx.protocol.bedrock.packet.LevelChunkPacket; 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.GeyserChunkSection; -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 +57,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 +90,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, Chunk chunk, int yOffset) { - ChunkSection[] javaSections = chunk.getSections(); - GeyserChunkSection[] sections = new GeyserChunkSection[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; - } - - ChunkSection 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 - 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)) { - bedrockOnlyBlockEntities.add(BedrockOnlyBlockEntity.getTag(session, - Vector3i.from((chunk.getX() << 4) + (yzx & 0xF), ((sectionY + yOffset) << 4) + ((yzx >> 8) & 0xF), (chunk.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((chunk.getX() << 4) + (yzx & 0xF), ((sectionY + yOffset) << 4) + ((yzx >> 8) & 0xF), (chunk.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 GeyserChunkSection(layers); - } - - CompoundTag[] blockEntities = chunk.getBlockEntities(); - 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; - ChunkSection section = chunk.getSections()[(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); @@ -452,7 +246,4 @@ public class ChunkUtils { double coordinateScale = ((DoubleTag) dimensionTag.get("coordinate_scale")).getValue(); session.getWorldBorder().setWorldCoordinateScale(coordinateScale); } - - public record ChunkData(GeyserChunkSection[] sections, NbtMap[] blockEntities) { - } } diff --git a/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java b/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java index a1668acf0..b56e26246 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java +++ b/connector/src/main/java/org/geysermc/connector/utils/DimensionUtils.java @@ -99,8 +99,8 @@ public class DimensionUtils { stopSoundPacket.setSoundName(""); session.sendUpstreamPacket(stopSoundPacket); - // TODO - fix this hack of a fix by sending the final dimension switching logic after chunks have been sent. - // The client wants chunks sent to it before it can successfully respawn. + // TODO - fix this hack of a fix by sending the final dimension switching logic after sections have been sent. + // The client wants sections sent to it before it can successfully respawn. ChunkUtils.sendEmptyChunks(session, player.getPosition().toInt(), 3, true); }