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.DataPalette;
import com.github.steveice10.mc.protocol.data.game.chunk.palette.GlobalPalette; 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.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.BlockEntityInfo;
import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityType; 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.mc.protocol.packet.ingame.clientbound.level.ClientboundLevelChunkWithLightPacket;
@ -47,6 +48,7 @@ import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.ByteBufOutputStream; import io.netty.buffer.ByteBufOutputStream;
import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList; 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.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator; 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.GeyserChunkSection;
import org.geysermc.connector.network.translators.world.chunk.bitarray.BitArray; 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.BitArrayVersion;
import org.geysermc.connector.network.translators.world.chunk.bitarray.SingletonBitArray;
import org.geysermc.connector.registry.BlockRegistries; import org.geysermc.connector.registry.BlockRegistries;
import org.geysermc.connector.utils.BlockEntityUtils; import org.geysermc.connector.utils.BlockEntityUtils;
import org.geysermc.connector.utils.ChunkUtils; import org.geysermc.connector.utils.ChunkUtils;
@ -145,6 +148,22 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
continue; 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()); IntList bedrockPalette = new IntArrayList(javaPalette.size());
waterloggedPaletteIds.clear(); waterloggedPaletteIds.clear();
pistonOrFlowerPaletteIds.clear(); pistonOrFlowerPaletteIds.clear();
@ -270,7 +289,11 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
int size = 0; int size = 0;
for (int i = 0; i < sectionCount; i++) { for (int i = 0; i < sectionCount; i++) {
GeyserChunkSection section = sections[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 += ChunkUtils.EMPTY_CHUNK_DATA.length; // Consists only of biome data
size += 1; // Border blocks size += 1; // Border blocks
@ -281,7 +304,11 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
byteBuf = ByteBufAllocator.DEFAULT.buffer(size); byteBuf = ByteBufAllocator.DEFAULT.buffer(size);
for (int i = 0; i < sectionCount; i++) { for (int i = 0; i < sectionCount; i++) {
GeyserChunkSection section = sections[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 // 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.mc.protocol.data.game.chunk.palette.SingletonPalette;
import com.github.steveice10.opennbt.tag.builtin.*; import com.github.steveice10.opennbt.tag.builtin.*;
import it.unimi.dsi.fastutil.ints.Int2IntMap; 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.session.GeyserSession;
import org.geysermc.connector.network.translators.world.chunk.BlockStorage; 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.GeyserChunkSection;
import org.geysermc.connector.network.translators.world.chunk.bitarray.SingletonBitArray;
import org.geysermc.connector.registry.Registries; import org.geysermc.connector.registry.Registries;
import java.util.Arrays; import java.util.Arrays;
@ -107,7 +109,7 @@ public class BiomeTranslator {
if (biomeData.getPalette() instanceof SingletonPalette palette) { if (biomeData.getPalette() instanceof SingletonPalette palette) {
int biomeId = biomeTranslations.get(palette.idToState(0)); int biomeId = biomeTranslations.get(palette.idToState(0));
return new BlockStorage(biomeId); return new BlockStorage(SingletonBitArray.INSTANCE, IntLists.singleton(biomeId));
} else { } else {
BlockStorage storage = new BlockStorage(0); BlockStorage storage = new BlockStorage(0);

View file

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

View file

@ -25,6 +25,9 @@
package org.geysermc.connector.network.translators.world.chunk.bitarray; package org.geysermc.connector.network.translators.world.chunk.bitarray;
import com.nukkitx.network.VarInts;
import io.netty.buffer.ByteBuf;
public interface BitArray { public interface BitArray {
void set(int index, int value); void set(int index, int value);
@ -33,6 +36,13 @@ public interface BitArray {
int size(); 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(); int[] getWords();
BitArrayVersion getVersion(); BitArrayVersion getVersion();

View file

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

View file

@ -23,36 +23,49 @@
* @link https://github.com/GeyserMC/Geyser * @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 io.netty.buffer.ByteBuf;
import com.nukkitx.nbt.NbtMap;
import com.nukkitx.nbt.NbtUtils;
import lombok.Getter;
import java.io.ByteArrayOutputStream; public class SingletonBitArray implements BitArray {
import java.io.IOException; public static final SingletonBitArray INSTANCE = new SingletonBitArray();
public class EmptyChunkProvider { private static final int[] EMPTY_ARRAY = new int[0];
@Getter
private final byte[] emptyLevelChunkData;
@Getter
private final GeyserChunkSection emptySection;
public EmptyChunkProvider(int airId) { public SingletonBitArray() {
BlockStorage emptyStorage = new BlockStorage(airId); }
emptySection = new GeyserChunkSection(new BlockStorage[]{emptyStorage});
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { @Override
outputStream.write(new byte[258]); // Biomes + Border Size + Extra Data Size public void set(int index, int value) {
}
try (NBTOutputStream stream = NbtUtils.createNetworkWriter(outputStream)) { @Override
stream.writeTag(NbtMap.EMPTY); public int get(int index) {
} return 0;
}
emptyLevelChunkData = outputStream.toByteArray(); @Override
} catch (IOException e) { public int size() {
throw new AssertionError("Unable to generate empty level chunk data"); 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 it.unimi.dsi.fastutil.objects.ObjectIntPair;
import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.network.translators.world.block.BlockStateValues; 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.BlockRegistries;
import org.geysermc.connector.registry.type.BlockMapping; import org.geysermc.connector.registry.type.BlockMapping;
import org.geysermc.connector.registry.type.BlockMappings; import org.geysermc.connector.registry.type.BlockMappings;
@ -200,7 +198,6 @@ public class BlockRegistryPopulator {
builder.bedrockBlockStates(blocksTag); builder.bedrockBlockStates(blocksTag);
BlockRegistries.BLOCKS.register(palette.getKey().valueInt(), builder.blockStateVersion(stateVersion) BlockRegistries.BLOCKS.register(palette.getKey().valueInt(), builder.blockStateVersion(stateVersion)
.emptyChunkSection(new GeyserChunkSection(new BlockStorage[]{new BlockStorage(airRuntimeId)}))
.javaToBedrockBlocks(javaToBedrockBlocks) .javaToBedrockBlocks(javaToBedrockBlocks)
.javaIdentifierToBedrockTag(javaIdentifierToBedrockTag) .javaIdentifierToBedrockTag(javaIdentifierToBedrockTag)
.itemFrames(itemFrames) .itemFrames(itemFrames)

View file

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

View file

@ -36,6 +36,7 @@ import com.nukkitx.protocol.bedrock.packet.NetworkChunkPublisherUpdatePacket;
import com.nukkitx.protocol.bedrock.packet.UpdateBlockPacket; import com.nukkitx.protocol.bedrock.packet.UpdateBlockPacket;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import it.unimi.dsi.fastutil.ints.IntLists;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
import org.geysermc.connector.entity.ItemFrameEntity; import org.geysermc.connector.entity.ItemFrameEntity;
import org.geysermc.connector.entity.player.SkullPlayerEntity; 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.BlockStateValues;
import org.geysermc.connector.network.translators.world.block.entity.BedrockOnlyBlockEntity; 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.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 org.geysermc.connector.registry.BlockRegistries;
import static org.geysermc.connector.network.translators.world.block.BlockStateValues.JAVA_AIR_ID; 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 = 256;
public static final int MAXIMUM_ACCEPTED_HEIGHT_OVERWORLD = 384; 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_CHUNK_DATA;
public static final byte[] EMPTY_BIOME_DATA; public static final byte[] EMPTY_BIOME_DATA;
static { static {
ByteBuf byteBuf = Unpooled.buffer(); ByteBuf byteBuf = Unpooled.buffer();
try { 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); blockStorage.writeToNetwork(byteBuf);
EMPTY_BIOME_DATA = new byte[byteBuf.readableBytes()]; EMPTY_BIOME_DATA = new byte[byteBuf.readableBytes()];