Initial update to 1.18 protocol

This commit is contained in:
Camotoy 2021-11-13 23:49:31 -05:00
parent 393c2b0f91
commit 6b012778d8
No known key found for this signature in database
GPG key ID: 7EEFB66FE798081F
10 changed files with 415 additions and 406 deletions

View file

@ -149,7 +149,7 @@
<dependency>
<groupId>com.github.steveice10</groupId>
<artifactId>mcprotocollib</artifactId>
<version>1.17.1-3-SNAPSHOT</version>
<version>1.18-pre-SNAPSHOT</version>
<scope>compile</scope>
<exclusions>
<exclusion>

View file

@ -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);
}

View file

@ -106,7 +106,8 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
// We need to send our skin parts to the server otherwise java sees us with no hat, jacket etc
String locale = session.getLocale();
ServerboundClientInformationPacket infoPacket = new ServerboundClientInformationPacket(locale, (byte) session.getRenderDistance(), ChatVisibility.FULL, true, SKIN_PART_VALUES, HandPreference.RIGHT_HAND, false);
// TODO customize
ServerboundClientInformationPacket infoPacket = new ServerboundClientInformationPacket(locale, (byte) session.getRenderDistance(), ChatVisibility.FULL, true, SKIN_PART_VALUES, HandPreference.RIGHT_HAND, false, true);
session.sendDownstreamPacket(infoPacket);
session.sendDownstreamPacket(new ServerboundCustomPayloadPacket("minecraft:brand", PluginMessageUtils.getGeyserBrandData()));

View file

@ -25,6 +25,7 @@
package org.geysermc.connector.network.translators.java.level;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import com.github.steveice10.mc.protocol.data.game.level.block.UpdatedTileType;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.level.ClientboundBlockEntityDataPacket;
@ -61,7 +62,9 @@ public class JavaBlockEntityDataTranslator extends PacketTranslator<ClientboundB
} else {
blockState = BlockStateValues.JAVA_AIR_ID;
}
BlockEntityUtils.updateBlockEntity(session, translator.getBlockEntityTag(id, packet.getNbt(), blockState), packet.getPosition());
Position position = packet.getPosition();
BlockEntityUtils.updateBlockEntity(session, translator.getBlockEntityTag(id, position.getX(), position.getY(), position.getZ(),
packet.getNbt(), blockState), packet.getPosition());
// Check for custom skulls.
if (session.getPreferencesCache().showCustomSkulls() && packet.getNbt().contains("SkullOwner")) {
SkullBlockEntityTranslator.spawnPlayer(session, packet.getNbt(), blockState);

View file

@ -1,141 +0,0 @@
/*
* 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.Column;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.level.ClientboundLevelChunkPacket;
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 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.chunk.ChunkSection;
import org.geysermc.connector.utils.ChunkUtils;
import java.io.IOException;
import static org.geysermc.connector.utils.ChunkUtils.MINIMUM_ACCEPTED_HEIGHT;
import static org.geysermc.connector.utils.ChunkUtils.MINIMUM_ACCEPTED_HEIGHT_OVERWORLD;
@Translator(packet = ClientboundLevelChunkPacket.class)
public class JavaLevelChunkTranslator extends PacketTranslator<ClientboundLevelChunkPacket> {
@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);
}
}

View file

@ -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<ClientboundLevelChunkWithLightPacket> {
@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<NbtMap> 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);
}
}

View file

@ -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;
}
}

View file

@ -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();

View file

@ -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);
}
}

View file

@ -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<NbtMap> 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);