mirror of https://github.com/GeyserMC/Geyser.git
368 lines
19 KiB
Java
368 lines
19 KiB
Java
/*
|
|
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
*
|
|
* @author GeyserMC
|
|
* @link https://github.com/GeyserMC/Geyser
|
|
*/
|
|
|
|
package org.geysermc.geyser.translator.protocol.java.level;
|
|
|
|
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.chunk.palette.SingletonPalette;
|
|
import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityInfo;
|
|
import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityType;
|
|
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.level.ClientboundLevelChunkWithLightPacket;
|
|
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
|
import com.nukkitx.math.vector.Vector3i;
|
|
import com.nukkitx.nbt.NBTOutputStream;
|
|
import com.nukkitx.nbt.NbtMap;
|
|
import com.nukkitx.nbt.NbtUtils;
|
|
import com.nukkitx.protocol.bedrock.packet.LevelChunkPacket;
|
|
import io.netty.buffer.ByteBuf;
|
|
import io.netty.buffer.ByteBufAllocator;
|
|
import io.netty.buffer.ByteBufOutputStream;
|
|
import io.netty.buffer.Unpooled;
|
|
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
|
import it.unimi.dsi.fastutil.ints.IntList;
|
|
import it.unimi.dsi.fastutil.ints.IntLists;
|
|
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
|
import org.geysermc.geyser.entity.type.ItemFrameEntity;
|
|
import org.geysermc.geyser.level.BedrockDimension;
|
|
import org.geysermc.geyser.level.block.BlockStateValues;
|
|
import org.geysermc.geyser.level.chunk.BlockStorage;
|
|
import org.geysermc.geyser.level.chunk.GeyserChunkSection;
|
|
import org.geysermc.geyser.level.chunk.bitarray.BitArray;
|
|
import org.geysermc.geyser.level.chunk.bitarray.BitArrayVersion;
|
|
import org.geysermc.geyser.level.chunk.bitarray.SingletonBitArray;
|
|
import org.geysermc.geyser.registry.BlockRegistries;
|
|
import org.geysermc.geyser.session.GeyserSession;
|
|
import org.geysermc.geyser.translator.level.BiomeTranslator;
|
|
import org.geysermc.geyser.translator.level.block.entity.BedrockOnlyBlockEntity;
|
|
import org.geysermc.geyser.translator.level.block.entity.BlockEntityTranslator;
|
|
import org.geysermc.geyser.translator.level.block.entity.SkullBlockEntityTranslator;
|
|
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
|
import org.geysermc.geyser.translator.protocol.Translator;
|
|
import org.geysermc.geyser.util.BlockEntityUtils;
|
|
import org.geysermc.geyser.util.ChunkUtils;
|
|
|
|
import java.io.IOException;
|
|
import java.util.BitSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
import static org.geysermc.geyser.util.ChunkUtils.SERIALIZED_CHUNK_DATA;
|
|
import static org.geysermc.geyser.util.ChunkUtils.indexYZXtoXZY;
|
|
|
|
@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());
|
|
}
|
|
|
|
// Ensure that, if the player is using lower world heights, the position is not offset
|
|
int yOffset = session.getChunkCache().getChunkMinY();
|
|
int chunkSize = session.getChunkCache().getChunkHeightY();
|
|
int biomeGlobalPalette = session.getBiomeGlobalPalette();
|
|
|
|
DataPalette[] javaChunks = new DataPalette[chunkSize];
|
|
DataPalette[] javaBiomes = new DataPalette[chunkSize];
|
|
|
|
final BlockEntityInfo[] blockEntities = packet.getBlockEntities();
|
|
final List<NbtMap> bedrockBlockEntities = new ObjectArrayList<>(blockEntities.length);
|
|
|
|
BitSet waterloggedPaletteIds = new BitSet();
|
|
BitSet bedrockOnlyBlockEntityIds = new BitSet();
|
|
|
|
BedrockDimension bedrockDimension = session.getChunkCache().getBedrockDimension();
|
|
int maxBedrockSectionY = (bedrockDimension.height() >> 4) - 1;
|
|
|
|
int sectionCount;
|
|
byte[] payload;
|
|
ByteBuf byteBuf = null;
|
|
GeyserChunkSection[] sections = new GeyserChunkSection[javaChunks.length - (yOffset + (bedrockDimension.minY() >> 4))];
|
|
|
|
try {
|
|
ByteBuf in = Unpooled.wrappedBuffer(packet.getChunkData());
|
|
for (int sectionY = 0; sectionY < chunkSize; sectionY++) {
|
|
ChunkSection javaSection = session.getCodecHelper().readChunkSection(in, biomeGlobalPalette);
|
|
javaChunks[sectionY] = javaSection.getChunkData();
|
|
javaBiomes[sectionY] = javaSection.getBiomeData();
|
|
|
|
int bedrockSectionY = sectionY + (yOffset - (bedrockDimension.minY() >> 4));
|
|
if (bedrockSectionY < 0 || maxBedrockSectionY < bedrockSectionY) {
|
|
// Ignore this chunk section since it goes outside the bounds accepted by the Bedrock client
|
|
continue;
|
|
}
|
|
|
|
// 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) || BlockStateValues.isNonWaterCauldron(javaId)) {
|
|
bedrockBlockEntities.add(BedrockOnlyBlockEntity.getTag(session,
|
|
Vector3i.from((packet.getX() << 4) + (yzx & 0xF), ((sectionY + yOffset) << 4) + ((yzx >> 8) & 0xF), (packet.getZ() << 4) + ((yzx >> 4) & 0xF)),
|
|
javaId
|
|
));
|
|
}
|
|
}
|
|
sections[bedrockSectionY] = section;
|
|
continue;
|
|
}
|
|
|
|
if (javaPalette instanceof SingletonPalette) {
|
|
// There's only one block here. Very easy!
|
|
int javaId = javaPalette.idToState(0);
|
|
int bedrockId = session.getBlockMappings().getBedrockBlockId(javaId);
|
|
BlockStorage blockStorage = new BlockStorage(SingletonBitArray.INSTANCE, IntLists.singleton(bedrockId));
|
|
|
|
if (BlockRegistries.WATERLOGGED.get().contains(javaId)) {
|
|
BlockStorage waterlogged = new BlockStorage(SingletonBitArray.INSTANCE, IntLists.singleton(session.getBlockMappings().getBedrockWaterId()));
|
|
sections[bedrockSectionY] = new GeyserChunkSection(new BlockStorage[] {blockStorage, waterlogged});
|
|
} else {
|
|
sections[bedrockSectionY] = new GeyserChunkSection(new BlockStorage[] {blockStorage});
|
|
}
|
|
// If a chunk contains all of the same piston or flower pot then god help us
|
|
continue;
|
|
}
|
|
|
|
IntList bedrockPalette = new IntArrayList(javaPalette.size());
|
|
waterloggedPaletteIds.clear();
|
|
bedrockOnlyBlockEntityIds.clear();
|
|
|
|
// Iterate through palette and convert state IDs to Bedrock, doing some additional checks as we go
|
|
for (int i = 0; i < javaPalette.size(); i++) {
|
|
int javaId = javaPalette.idToState(i);
|
|
bedrockPalette.add(session.getBlockMappings().getBedrockBlockId(javaId));
|
|
|
|
if (BlockRegistries.WATERLOGGED.get().contains(javaId)) {
|
|
waterloggedPaletteIds.set(i);
|
|
}
|
|
|
|
// Check if block is piston, flower or cauldron to see if we'll need to create additional block entities, as they're only block entities in Bedrock
|
|
if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId) || BlockStateValues.isNonWaterCauldron(javaId)) {
|
|
bedrockOnlyBlockEntityIds.set(i);
|
|
}
|
|
}
|
|
|
|
// Add Bedrock-exclusive block entities
|
|
// We only if the palette contained any blocks that are Bedrock-exclusive block entities to avoid iterating through the whole block data
|
|
// for no reason, as most sections will not contain any pistons or flower pots
|
|
if (!bedrockOnlyBlockEntityIds.isEmpty()) {
|
|
for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
|
|
int paletteId = javaData.get(yzx);
|
|
if (bedrockOnlyBlockEntityIds.get(paletteId)) {
|
|
bedrockBlockEntities.add(BedrockOnlyBlockEntity.getTag(session,
|
|
Vector3i.from((packet.getX() << 4) + (yzx & 0xF), ((sectionY + yOffset) << 4) + ((yzx >> 8) & 0xF), (packet.getZ() << 4) + ((yzx >> 4) & 0xF)),
|
|
javaPalette.idToState(paletteId)
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
BitArray bedrockData = BitArrayVersion.forBitsCeil(javaData.getBitsPerEntry()).createArray(BlockStorage.SIZE);
|
|
BlockStorage layer0 = new BlockStorage(bedrockData, bedrockPalette);
|
|
BlockStorage[] layers;
|
|
|
|
// Convert data array from YZX to XZY coordinate order
|
|
if (waterloggedPaletteIds.isEmpty()) {
|
|
// No blocks are waterlogged, simply convert coordinate order
|
|
// This could probably be optimized further...
|
|
for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
|
|
bedrockData.set(indexYZXtoXZY(yzx), javaData.get(yzx));
|
|
}
|
|
|
|
layers = new BlockStorage[]{ layer0 };
|
|
} else {
|
|
// The section contains waterlogged blocks, we need to convert coordinate order AND generate a V1 block storage for
|
|
// layer 1 with palette ID 1 indicating water
|
|
int[] layer1Data = new int[BlockStorage.SIZE >> 5];
|
|
for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
|
|
int paletteId = javaData.get(yzx);
|
|
int xzy = indexYZXtoXZY(yzx);
|
|
bedrockData.set(xzy, paletteId);
|
|
|
|
if (waterloggedPaletteIds.get(paletteId)) {
|
|
layer1Data[xzy >> 5] |= 1 << (xzy & 0x1F);
|
|
}
|
|
}
|
|
|
|
// V1 palette
|
|
IntList layer1Palette = IntList.of(
|
|
session.getBlockMappings().getBedrockAirId(), // Air - see BlockStorage's constructor for more information
|
|
session.getBlockMappings().getBedrockWaterId());
|
|
|
|
layers = new BlockStorage[]{ layer0, new BlockStorage(BitArrayVersion.V1.createArray(BlockStorage.SIZE, layer1Data), layer1Palette) };
|
|
}
|
|
|
|
sections[bedrockSectionY] = new GeyserChunkSection(layers);
|
|
}
|
|
|
|
session.getChunkCache().addToCache(packet.getX(), packet.getZ(), javaChunks);
|
|
|
|
final int chunkBlockX = packet.getX() << 4;
|
|
final int chunkBlockZ = packet.getZ() << 4;
|
|
for (BlockEntityInfo blockEntity : blockEntities) {
|
|
BlockEntityType type = blockEntity.getType();
|
|
if (type == null) {
|
|
// As an example: ViaVersion will send -1 if it cannot find the block entity type
|
|
// Vanilla Minecraft gracefully handles this
|
|
continue;
|
|
}
|
|
CompoundTag tag = blockEntity.getNbt();
|
|
int x = blockEntity.getX(); // Relative to chunk
|
|
int y = blockEntity.getY();
|
|
int z = blockEntity.getZ(); // Relative to chunk
|
|
|
|
// Get the Java block state ID from block entity position
|
|
DataPalette section = javaChunks[(y >> 4) - yOffset];
|
|
int blockState = section.get(x, y & 0xF, z);
|
|
|
|
if (type == BlockEntityType.LECTERN && BlockStateValues.getLecternBookStates().get(blockState)) {
|
|
// If getLecternBookStates is false, let's just treat it like a normal block entity
|
|
bedrockBlockEntities.add(session.getGeyser().getWorldManager().getLecternDataAt(
|
|
session, x + chunkBlockX, y, z + chunkBlockZ, true));
|
|
continue;
|
|
}
|
|
|
|
BlockEntityTranslator blockEntityTranslator = BlockEntityUtils.getBlockEntityTranslator(type);
|
|
bedrockBlockEntities.add(blockEntityTranslator.getBlockEntityTag(type, x + chunkBlockX, y, z + chunkBlockZ, tag, blockState));
|
|
|
|
// Check for custom skulls
|
|
if (session.getPreferencesCache().showCustomSkulls() && type == BlockEntityType.SKULL && tag != null && tag.contains("SkullOwner")) {
|
|
SkullBlockEntityTranslator.translateSkull(session, tag, x + chunkBlockX, y, z + chunkBlockZ, blockState);
|
|
}
|
|
}
|
|
|
|
// Find highest section
|
|
sectionCount = sections.length - 1;
|
|
while (sectionCount >= 0 && sections[sectionCount] == null) {
|
|
sectionCount--;
|
|
}
|
|
sectionCount++;
|
|
|
|
// As of 1.18.30, the amount of biomes read is dependent on how high Bedrock thinks the dimension is
|
|
int biomeCount = bedrockDimension.height() >> 4;
|
|
|
|
// Estimate chunk size
|
|
int size = 0;
|
|
for (int i = 0; i < sectionCount; i++) {
|
|
GeyserChunkSection section = sections[i];
|
|
if (section != null) {
|
|
size += section.estimateNetworkSize();
|
|
} else {
|
|
size += SERIALIZED_CHUNK_DATA.length;
|
|
}
|
|
}
|
|
size += ChunkUtils.EMPTY_BIOME_DATA.length * biomeCount;
|
|
size += 1; // Border blocks
|
|
size += bedrockBlockEntities.size() * 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];
|
|
if (section != null) {
|
|
section.writeToNetwork(byteBuf);
|
|
} else {
|
|
byteBuf.writeBytes(SERIALIZED_CHUNK_DATA);
|
|
}
|
|
}
|
|
|
|
int dimensionOffset = bedrockDimension.minY() >> 4;
|
|
for (int i = 0; i < biomeCount; i++) {
|
|
int biomeYOffset = dimensionOffset + i;
|
|
if (biomeYOffset < yOffset) {
|
|
// Ignore this biome section since it goes below the height of the Java world
|
|
byteBuf.writeBytes(ChunkUtils.EMPTY_BIOME_DATA);
|
|
continue;
|
|
}
|
|
if (biomeYOffset >= (chunkSize + yOffset)) {
|
|
// This biome section goes above the height of the Java world
|
|
// The byte written here is a header that says to carry on the biome data from the previous chunk
|
|
byteBuf.writeByte((127 << 1) | 1);
|
|
continue;
|
|
}
|
|
|
|
BiomeTranslator.toNewBedrockBiome(session, javaBiomes[i + (dimensionOffset - yOffset)]).writeToNetwork(byteBuf);
|
|
}
|
|
|
|
byteBuf.writeByte(0); // Border blocks - Edu edition only
|
|
|
|
// Encode tile entities into buffer
|
|
NBTOutputStream nbtStream = NbtUtils.createNetworkWriter(new ByteBufOutputStream(byteBuf));
|
|
for (NbtMap blockEntity : bedrockBlockEntities) {
|
|
nbtStream.writeTag(blockEntity);
|
|
}
|
|
|
|
// 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.getGeyser().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);
|
|
|
|
for (Map.Entry<Vector3i, ItemFrameEntity> entry : session.getItemFrameCache().entrySet()) {
|
|
Vector3i position = entry.getKey();
|
|
if ((position.getX() >> 4) == packet.getX() && (position.getZ() >> 4) == packet.getZ()) {
|
|
// Update this item frame so it doesn't get lost in the abyss
|
|
//TODO optimize
|
|
entry.getValue().updateBlock(true);
|
|
}
|
|
}
|
|
}
|
|
} |