Implement V0 bits-per-block for Bedrock

This also improves sending empty chunks by having an empty BlockStorage array.

The empty biome data has shrunk from 32k bytes to 65.

With thanks to @dktapps.
This commit is contained in:
Camotoy 2021-11-14 17:59:14 -05:00
parent 61f20217a9
commit badee15c46
No known key found for this signature in database
GPG key ID: 7EEFB66FE798081F
9 changed files with 105 additions and 36 deletions

View file

@ -30,6 +30,7 @@ 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;
@ -47,6 +48,7 @@ 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 it.unimi.dsi.fastutil.ints.IntLists;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator;
@ -59,6 +61,7 @@ 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.network.translators.world.chunk.bitarray.SingletonBitArray;
import org.geysermc.connector.registry.BlockRegistries;
import org.geysermc.connector.utils.BlockEntityUtils;
import org.geysermc.connector.utils.ChunkUtils;
@ -145,6 +148,22 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
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();
pistonOrFlowerPaletteIds.clear();
@ -270,7 +289,11 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
int size = 0;
for (int i = 0; i < sectionCount; i++) {
GeyserChunkSection section = sections[i];
size += (section != null ? section : session.getBlockMappings().getEmptyChunkSection()).estimateNetworkSize();
if (section != null) {
size += section.estimateNetworkSize();
} else {
size += SERIALIZED_CHUNK_DATA.length;
}
}
size += ChunkUtils.EMPTY_CHUNK_DATA.length; // Consists only of biome data
size += 1; // Border blocks
@ -281,7 +304,11 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
byteBuf = ByteBufAllocator.DEFAULT.buffer(size);
for (int i = 0; i < sectionCount; i++) {
GeyserChunkSection section = sections[i];
(section != null ? section : session.getBlockMappings().getEmptyChunkSection()).writeToNetwork(byteBuf);
if (section != null) {
section.writeToNetwork(byteBuf);
} else {
byteBuf.writeBytes(SERIALIZED_CHUNK_DATA);
}
}
// At this point we're dealing with Bedrock chunk sections

View file

@ -29,9 +29,11 @@ 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 it.unimi.dsi.fastutil.ints.IntLists;
import org.geysermc.connector.network.session.GeyserSession;
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.SingletonBitArray;
import org.geysermc.connector.registry.Registries;
import java.util.Arrays;
@ -107,7 +109,7 @@ public class BiomeTranslator {
if (biomeData.getPalette() instanceof SingletonPalette palette) {
int biomeId = biomeTranslations.get(palette.idToState(0));
return new BlockStorage(biomeId);
return new BlockStorage(SingletonBitArray.INSTANCE, IntLists.singleton(biomeId));
} else {
BlockStorage storage = new BlockStorage(0);

View file

@ -78,7 +78,7 @@ public class BlockStorage {
buffer.writeIntLE(word);
}
VarInts.writeInt(buffer, palette.size());
bitArray.writeSizeToNetwork(buffer, palette.size());
palette.forEach((IntConsumer) id -> VarInts.writeInt(buffer, id));
}

View file

@ -25,6 +25,9 @@
package org.geysermc.connector.network.translators.world.chunk.bitarray;
import com.nukkitx.network.VarInts;
import io.netty.buffer.ByteBuf;
public interface BitArray {
void set(int index, int value);
@ -33,6 +36,13 @@ public interface BitArray {
int size();
/**
* Overridden if the bit array implementation does not require size.
*/
default void writeSizeToNetwork(ByteBuf buffer, int size) {
VarInts.writeInt(buffer, size);
}
int[] getWords();
BitArrayVersion getVersion();

View file

@ -35,7 +35,8 @@ public enum BitArrayVersion {
V4(4, 8, V5),
V3(3, 10, V4), // 2 bit padding
V2(2, 16, V3),
V1(1, 32, V2);
V1(1, 32, V2),
V0(0, 0, V1);
private static final BitArrayVersion[] VALUES = values();
@ -94,6 +95,8 @@ public enum BitArrayVersion {
if (this == V3 || this == V5 || this == V6) {
// Padded palettes aren't able to use bitwise operations due to their padding.
return new PaddedBitArray(this, size, words);
} else if (this == V0) {
return new SingletonBitArray();
} else {
return new Pow2BitArray(this, size, words);
}

View file

@ -23,36 +23,49 @@
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.connector.network.translators.world.chunk;
package org.geysermc.connector.network.translators.world.chunk.bitarray;
import com.nukkitx.nbt.NBTOutputStream;
import com.nukkitx.nbt.NbtMap;
import com.nukkitx.nbt.NbtUtils;
import lombok.Getter;
import io.netty.buffer.ByteBuf;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class SingletonBitArray implements BitArray {
public static final SingletonBitArray INSTANCE = new SingletonBitArray();
public class EmptyChunkProvider {
@Getter
private final byte[] emptyLevelChunkData;
@Getter
private final GeyserChunkSection emptySection;
private static final int[] EMPTY_ARRAY = new int[0];
public EmptyChunkProvider(int airId) {
BlockStorage emptyStorage = new BlockStorage(airId);
emptySection = new GeyserChunkSection(new BlockStorage[]{emptyStorage});
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
outputStream.write(new byte[258]); // Biomes + Border Size + Extra Data Size
try (NBTOutputStream stream = NbtUtils.createNetworkWriter(outputStream)) {
stream.writeTag(NbtMap.EMPTY);
public SingletonBitArray() {
}
emptyLevelChunkData = outputStream.toByteArray();
} catch (IOException e) {
throw new AssertionError("Unable to generate empty level chunk data");
@Override
public void set(int index, int value) {
}
@Override
public int get(int index) {
return 0;
}
@Override
public int size() {
return 1;
}
@Override
public void writeSizeToNetwork(ByteBuf buffer, int size) {
// no-op - size is fixed
}
@Override
public int[] getWords() {
return EMPTY_ARRAY;
}
@Override
public BitArrayVersion getVersion() {
return BitArrayVersion.V0;
}
@Override
public SingletonBitArray copy() {
return new SingletonBitArray();
}
}

View file

@ -38,8 +38,6 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectIntPair;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.network.translators.world.block.BlockStateValues;
import org.geysermc.connector.network.translators.world.chunk.BlockStorage;
import org.geysermc.connector.network.translators.world.chunk.GeyserChunkSection;
import org.geysermc.connector.registry.BlockRegistries;
import org.geysermc.connector.registry.type.BlockMapping;
import org.geysermc.connector.registry.type.BlockMappings;
@ -200,7 +198,6 @@ public class BlockRegistryPopulator {
builder.bedrockBlockStates(blocksTag);
BlockRegistries.BLOCKS.register(palette.getKey().valueInt(), builder.blockStateVersion(stateVersion)
.emptyChunkSection(new GeyserChunkSection(new BlockStorage[]{new BlockStorage(airRuntimeId)}))
.javaToBedrockBlocks(javaToBedrockBlocks)
.javaIdentifierToBedrockTag(javaIdentifierToBedrockTag)
.itemFrames(itemFrames)

View file

@ -31,7 +31,6 @@ import it.unimi.dsi.fastutil.ints.IntSet;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import lombok.Builder;
import lombok.Value;
import org.geysermc.connector.network.translators.world.chunk.GeyserChunkSection;
import java.util.Map;
@ -44,8 +43,6 @@ public class BlockMappings {
int blockStateVersion;
GeyserChunkSection emptyChunkSection;
int[] javaToBedrockBlocks;
NbtList<NbtMap> bedrockBlockStates;

View file

@ -36,6 +36,7 @@ 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.IntLists;
import lombok.experimental.UtilityClass;
import org.geysermc.connector.entity.ItemFrameEntity;
import org.geysermc.connector.entity.player.SkullPlayerEntity;
@ -43,6 +44,8 @@ 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.chunk.BlockStorage;
import org.geysermc.connector.network.translators.world.chunk.GeyserChunkSection;
import org.geysermc.connector.network.translators.world.chunk.bitarray.SingletonBitArray;
import org.geysermc.connector.registry.BlockRegistries;
import static org.geysermc.connector.network.translators.world.block.BlockStateValues.JAVA_AIR_ID;
@ -60,13 +63,30 @@ public class ChunkUtils {
public static final int MAXIMUM_ACCEPTED_HEIGHT = 256;
public static final int MAXIMUM_ACCEPTED_HEIGHT_OVERWORLD = 384;
/**
* An empty subchunk.
*/
public static final byte[] SERIALIZED_CHUNK_DATA;
/**
* An empty chunk that can be safely passed on to a LevelChunkPacket with subcounts set to 0.
*/
public static final byte[] EMPTY_CHUNK_DATA;
public static final byte[] EMPTY_BIOME_DATA;
static {
ByteBuf byteBuf = Unpooled.buffer();
try {
BlockStorage blockStorage = new BlockStorage(0);
new GeyserChunkSection(new BlockStorage[0])
.writeToNetwork(byteBuf);
SERIALIZED_CHUNK_DATA = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(SERIALIZED_CHUNK_DATA);
} finally {
byteBuf.release();
}
byteBuf = Unpooled.buffer();
try {
BlockStorage blockStorage = new BlockStorage(SingletonBitArray.INSTANCE, IntLists.singleton(0));
blockStorage.writeToNetwork(byteBuf);
EMPTY_BIOME_DATA = new byte[byteBuf.readableBytes()];