Merge across 1.18 protocol changes

This commit is contained in:
RednedEpic 2021-11-13 23:36:43 -06:00
commit 4bbea1de68
13 changed files with 316 additions and 323 deletions

View file

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

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.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) {

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

@ -56,7 +56,7 @@ public class JavaAnimateTranslator extends PacketTranslator<ClientboundAnimatePa
case SWING_ARM:
animatePacket.setAction(AnimatePacket.Action.SWING_ARM);
break;
case EAT_FOOD:
case SWING_OFFHAND:
// Use the OptionalPack to trigger the animation
AnimateEntityPacket offHandPacket = new AnimateEntityPacket();
offHandPacket.setAnimation("animation.player.attack.rotations.offhand");

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.BlockEntityType;
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

@ -25,8 +25,19 @@
package org.geysermc.connector.network.translators.java.level;
import com.github.steveice10.mc.protocol.data.game.chunk.Chunk;
import com.github.steveice10.mc.protocol.data.game.chunk.BitStorage;
import com.github.steveice10.mc.protocol.data.game.chunk.ChunkSection;
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;
@ -35,17 +46,32 @@ 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.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 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.MINIMUM_ACCEPTED_HEIGHT;
import static org.geysermc.connector.utils.ChunkUtils.MINIMUM_ACCEPTED_HEIGHT_OVERWORLD;
import static org.geysermc.connector.utils.ChunkUtils.*;
@Translator(packet = ClientboundLevelChunkWithLightPacket.class)
public class JavaLevelChunkWithLightTranslator extends PacketTranslator<ClientboundLevelChunkWithLightPacket> {
@ -56,44 +82,235 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
ChunkUtils.updateChunkPosition(session, session.getPlayerEntity().getPosition().toInt());
}
session.getChunkCache().addToCache(packet.getChunk());
Chunk chunk = packet.getChunk();
// 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, chunk, yOffset);
GeyserChunkSection[] sections = chunkData.sections();
// 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()];
// 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<Clientbo
byteBuf.writeBytes(ChunkUtils.EMPTY_BIOME_DATA);
continue;
}
BiomeTranslator.toNewBedrockBiome(session, chunk.getBiomeData(), i + (dimensionOffset - yOffset)).writeToNetwork(byteBuf);
BiomeTranslator.toNewBedrockBiome(session, javaBiomes[i]).writeToNetwork(byteBuf);
}
// As of 1.17.10, Bedrock hardcodes to always read 32 biome sections
@ -116,7 +333,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
// Encode tile entities into buffer
NBTOutputStream nbtStream = NbtUtils.createNetworkWriter(new ByteBufOutputStream(byteBuf));
for (NbtMap blockEntity : chunkData.blockEntities()) {
for (NbtMap blockEntity : bedrockBlockEntities) {
nbtStream.writeTag(blockEntity);
}
@ -126,15 +343,17 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
session.getConnector().getLogger().error("IO error while encoding chunk", e);
return;
} finally {
byteBuf.release(); // Release buffer to allow buffer pooling to be useful
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(chunk.getX());
levelChunkPacket.setChunkZ(chunk.getZ());
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(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;
}
}

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

View file

@ -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<BedrockOnlyBlockEntity> BEDROCK_ONLY_BLOCK_ENTITIES = new ObjectArrayList<>();

View file

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

View file

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