Implement logic for hiding chunk data (not finished)

This commit is contained in:
D4rkr34lm
2026-05-19 22:17:41 +02:00
parent 270d82eb71
commit cf7a1ee086
3 changed files with 217 additions and 101 deletions
@@ -58,7 +58,6 @@ dependencies {
compileOnly(libs.netty)
compileOnly(libs.brigadier)
compileOnly(libs.fastutil)
compileOnly(libs.nms21)
implementation(libs.anvilgui)
}
@@ -22,134 +22,240 @@ package de.steamwar.techhider;
import de.steamwar.Reflection;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import it.unimi.dsi.fastutil.ints.Int2IntArrayMap;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import lombok.Getter;
import net.minecraft.core.Registry;
import net.minecraft.core.SectionPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData;
import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.SimpleBitStorage;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntityType;
import org.bukkit.craftbukkit.util.CraftMagicNumbers;
import org.bukkit.entity.Player;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
public class ChunkHider {
public static final ChunkHider impl = new ChunkHider();
public Class<?> mapChunkPacket() {
return ClientboundLevelChunkWithLightPacket.class;
}
public abstract class ChunkHider {
private static final UnaryOperator<Object> chunkPacketShallowCloner = ProtocolUtils.shallowCloneGenerator(ClientboundLevelChunkWithLightPacket.class);
private static final UnaryOperator<Object> chunkDataShallowCloner = ProtocolUtils.shallowCloneGenerator(ClientboundLevelChunkPacketData.class);
private static final UnaryOperator<Object> chunkPacketCloner = ProtocolUtils.shallowCloneGenerator(ClientboundLevelChunkWithLightPacket.class);
private static final UnaryOperator<Object> chunkDataCloner = ProtocolUtils.shallowCloneGenerator(ClientboundLevelChunkPacketData.class);
private static final Reflection.Field<ClientboundLevelChunkPacketData> levelChunkPacketDataField = Reflection.getField(ClientboundLevelChunkWithLightPacket.class, ClientboundLevelChunkPacketData.class, 0);
private static final Reflection.Field<Integer> chunkXField = Reflection.getField(ClientboundLevelChunkWithLightPacket.class, int.class, 0);
private static final Reflection.Field<Integer> chunkZField = Reflection.getField(ClientboundLevelChunkWithLightPacket.class, int.class, 1);
private static final Reflection.Field<ClientboundLevelChunkPacketData> chunkData = Reflection.getField(ClientboundLevelChunkWithLightPacket.class, ClientboundLevelChunkPacketData.class, 0);
private static final Reflection.Field<byte[]> chunkBlockDataField = Reflection.getField(ClientboundLevelChunkPacketData.class, byte[].class, 0);
private static final Reflection.Field<List> chunkBlockEntitiesDataField = Reflection.getField(ClientboundLevelChunkPacketData.class, List.class, 0);
private static final Reflection.Field<byte[]> dataField = Reflection.getField(ClientboundLevelChunkPacketData.class, byte[].class, 0);
private static final Reflection.Field<List> tileEntities = Reflection.getField(ClientboundLevelChunkPacketData.class, List.class, 0);
private final int SECTION_SPAN_SIZE = 16;
private final byte BIT_PER_BLOCK_INDIRECTION_LIMIT = 8;
private final int BLOCKS_PER_SECTION = 4096;
public BiFunction<Player, Object, Object> chunkHiderGenerator(TechHider techHider) {
return (p, packet) -> {
int chunkX = chunkXField.get(packet);
int chunkZ = chunkZField.get(packet);
if (techHider.getLocationEvaluator().skipChunk(p, chunkX, chunkZ)) {
return packet;
}
private final byte BITS_PER_LONG = 64;
packet = chunkPacketCloner.apply(packet);
Object dataWrapper = chunkDataCloner.apply(chunkData.get(packet));
private long[] readSectionDataFromBuffer(ByteBuf dataSource, byte bitsPerEntry, int entryCount) {
int entriesPerLong = BITS_PER_LONG / bitsPerEntry;
int dataLengthAsLongCount = (entryCount + entriesPerLong - 1) / entriesPerLong;
Set<String> hiddenBlockEntities = techHider.getHiddenBlockEntities();
tileEntities.set(dataWrapper, ((List<?>) tileEntities.get(dataWrapper)).stream().filter(te -> tileEntityVisible(hiddenBlockEntities, te)).collect(Collectors.toList()));
long[] dataArray = new long[dataLengthAsLongCount];
ByteBuf in = Unpooled.wrappedBuffer(dataField.get(dataWrapper));
ByteBuf out = Unpooled.buffer(in.readableBytes() + 64);
for (int yOffset = p.getWorld().getMinHeight(); yOffset < p.getWorld().getMaxHeight(); yOffset += 16) {
SectionHider section = new SectionHider(p, techHider, in, out, chunkX, yOffset / 16, chunkZ);
section.copyBlockCount();
blocks(section);
biomes(section);
}
if (in.readableBytes() != 0) {
throw new IllegalStateException("ChunkHider21: Incomplete chunk data, " + in.readableBytes() + " bytes left");
}
byte[] data = new byte[out.readableBytes()];
out.readBytes(data);
dataField.set(dataWrapper, data);
chunkData.set(packet, dataWrapper);
return packet;
};
}
private static final Registry<BlockEntityType<?>> registry = Reflection.getField(BuiltInRegistries.class, "BLOCK_ENTITY_TYPE", Registry.class).get(null);
private static final Reflection.Method getKey = Reflection.getTypedMethod(Reflection.getClass("net.minecraft.core.Registry"), "getKey", ResourceLocation.class, Object.class);
public static final Class<?> tileEntity = Reflection.getClass("net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData$BlockEntityInfo");
protected static final Reflection.Field<BlockEntityType> entityType = Reflection.getField(tileEntity, BlockEntityType.class, 0);
protected boolean tileEntityVisible(Set<String> hiddenBlockEntities, Object tile) {
BlockEntityType type = entityType.get(tile);
String path = ((ResourceLocation) getKey.invoke(registry, type)).getPath();
return !hiddenBlockEntities.contains(path);
}
private void blocks(SectionHider section) {
section.copyBitsPerBlock();
boolean singleValued = section.getBitsPerBlock() == 0;
if (singleValued) {
int value = ProtocolUtils.readVarInt(section.getIn());
ProtocolUtils.writeVarInt(section.getOut(), !section.isSkipSection() && section.getObfuscate().contains(value) ? section.getTarget() : value);
return;
} else if (section.getBitsPerBlock() < 9) {
// Indirect (paletted) storage only present when bitsPerBlock < 9 in 1.21+
section.processPalette();
for(int i = 0; i < dataLengthAsLongCount; i++){
dataArray[i] = dataSource.readLong();
}
if (section.isSkipSection() || (!section.blockPrecise() && section.isPaletted())) {
section.skipNewDataArray(4096);
return;
}
return dataArray;
}
SimpleBitStorage values = new SimpleBitStorage(section.getBitsPerBlock(), 4096, section.readNewDataArray(4096));
public ClientboundLevelChunkWithLightPacket chunkHiderGenerator(Player player, ClientboundLevelChunkWithLightPacket packet) {
int chunkX = packet.getX();
int chunkZ = packet.getZ();
ClientboundLevelChunkPacketData chunkData = packet.getChunkData();
for (int y = 0; y < 16; y++) {
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
int pos = (((y * 16) + z) * 16) + x;
ByteBuf in = Unpooled.wrappedBuffer(chunkData.getReadBuffer());
ByteBuf out = Unpooled.buffer(in.readableBytes() + 64);
TechHider.State test = section.test(x, y, z);
int worldMinHeight = player.getWorld().getMinHeight();
int worldMaxHeight = player.getWorld().getMaxHeight();
switch (test) {
case SKIP:
break;
case CHECK:
if (!section.getObfuscate().contains(values.get(pos))) {
break;
for (int yOffset = worldMinHeight; yOffset < worldMaxHeight; yOffset += SECTION_SPAN_SIZE) {
// TODO make configurable
int blockIdUsedForHiding = 121;
short blockCount = in.readShort();
byte bitsPerBlock = in.readByte();
if(bitsPerBlock == 0) {
int sectionBlockId = ProtocolUtils.readVarInt(in);
}
else if (bitsPerBlock <= BIT_PER_BLOCK_INDIRECTION_LIMIT) {
int palletLength = ProtocolUtils.readVarInt(in);
int[] pallet = ProtocolUtils.readVarIntArray(in, palletLength);
long[] rawData = readSectionDataFromBuffer(in, bitsPerBlock, BLOCKS_PER_SECTION);
SimpleBitStorage data = new SimpleBitStorage(bitsPerBlock, BLOCKS_PER_SECTION, rawData);
int[] resolvedData = new int[BLOCKS_PER_SECTION];
for(int i = 0; i < BLOCKS_PER_SECTION; i++){
int palletReference = data.get(i);
resolvedData[i] = pallet[palletReference];
}
int[] obfuscatedData = new int[BLOCKS_PER_SECTION];
for (int sectionY = 0; sectionY < 16; sectionY++) {
for (int sectionZ = 0; sectionZ < 16; sectionZ++) {
for (int sectionX = 0; sectionX < 16; sectionX++) {
int blockDataIndex = (((sectionY * 16) + sectionZ) * 16) + sectionX;
int worldX = sectionX * chunkX;
int worldY = sectionY * yOffset;
int worldZ = sectionZ * chunkZ;
int blockId = resolvedData[blockDataIndex];
Block block = BuiltInRegistries.BLOCK.get(blockId).get().value();
if(isPlayerPrivilegedToAccessPosition(player, worldX, worldY, worldZ) && isPlayerPrivilegedToAccessBlock(player, worldX, worldY, worldZ, block)) {
obfuscatedData[blockDataIndex] = blockId;
}
case HIDE:
values.set(pos, section.getTarget());
break;
case HIDE_AIR:
default:
values.set(pos, section.getAir());
else {
obfuscatedData[blockDataIndex] = blockIdUsedForHiding;
}
}
}
}
Int2IntMap blockIdToPalletIndex = new Int2IntOpenHashMap();
int[] newPallet = new int[palletLength];
int runningIndex = 0;
for(int blockId : obfuscatedData) {
if(!blockIdToPalletIndex.containsKey(blockId)) {
newPallet[runningIndex] = blockId;
blockIdToPalletIndex.put(blockId, runningIndex);
runningIndex++;
}
}
SimpleBitStorage reEncodedData = new SimpleBitStorage(bitsPerBlock, BLOCKS_PER_SECTION, new long[rawData.length]);
for(int i = 0; i < obfuscatedData.length; i++) {
int blockId = obfuscatedData[i];
int palletReference = blockIdToPalletIndex.get(blockId);
reEncodedData.set(i, palletReference);
}
out.writeByte(blockCount);
out.writeByte(bitsPerBlock);
ProtocolUtils.writeVarInt(out, palletLength);
ProtocolUtils.writeVarIntArray(out, pallet);
for(long rawDataSegment : reEncodedData.getRaw()) {
out.writeLong(rawDataSegment);
}
}
else {
long[] rawData = readSectionDataFromBuffer(in, bitsPerBlock, BLOCKS_PER_SECTION);
SimpleBitStorage data = new SimpleBitStorage(bitsPerBlock, BLOCKS_PER_SECTION, rawData);
int[] resolvedData = new int[BLOCKS_PER_SECTION];
for(int i = 0; i < BLOCKS_PER_SECTION; i++){
resolvedData[i] = data.get(i);
}
int[] obfuscatedData = new int[BLOCKS_PER_SECTION];
for (int sectionY = 0; sectionY < 16; sectionY++) {
for (int sectionZ = 0; sectionZ < 16; sectionZ++) {
for (int sectionX = 0; sectionX < 16; sectionX++) {
int blockDataIndex = (((sectionY * 16) + sectionZ) * 16) + sectionX;
int worldX = sectionX * chunkX;
int worldY = sectionY * yOffset;
int worldZ = sectionZ * chunkZ;
int blockId = resolvedData[blockDataIndex];
Block block = BuiltInRegistries.BLOCK.get(blockId).get().value();
if(isPlayerPrivilegedToAccessPosition(player, worldX, worldY, worldZ) && isPlayerPrivilegedToAccessBlock(player, worldX, worldY, worldZ, block)) {
obfuscatedData[blockDataIndex] = blockId;
}
else {
obfuscatedData[blockDataIndex] = blockIdUsedForHiding;
}
}
}
}
SimpleBitStorage reEncodedData = new SimpleBitStorage(bitsPerBlock, BLOCKS_PER_SECTION, new long[rawData.length]);
for(int i = 0; i < obfuscatedData.length; i++) {
int blockId = obfuscatedData[i];
reEncodedData.set(i, blockId);
}
out.writeByte(blockCount);
out.writeByte(bitsPerBlock);
for(long rawDataSegment : reEncodedData.getRaw()) {
out.writeLong(rawDataSegment);
}
}
}
section.writeDataArray(values.getRaw());
if (in.readableBytes() != 0) {
return null;
}
byte[] data = new byte[out.readableBytes()];
out.readBytes(data);
List<Object> blockEntities = chunkBlockEntitiesDataField.get(chunkData);
List<Object> filteredBlockEntities = filterBlockEntities(player, blockEntities);
return buildNewChunkPacket(packet, data, filteredBlockEntities);
}
public abstract boolean isPlayerPrivilegedToAccessPosition(Player p, int blockX, int blockY, int blockZ);
public abstract boolean isPlayerPrivilegedToAccessBlock(Player p, int blockX, int blockY, int blockZ, Block block);
public abstract boolean isPlayerPrivilegedToAccessBlockEntity(Player p, int blockX, int blockY, int blockZ, BlockEntityType<?> type);
private ClientboundLevelChunkWithLightPacket buildNewChunkPacket(ClientboundLevelChunkWithLightPacket originalPacket, byte[] newBlockDataBuffer, List<Object> newBlockEntities) {
ClientboundLevelChunkWithLightPacket clonedPacket = (ClientboundLevelChunkWithLightPacket) chunkPacketShallowCloner.apply(originalPacket);
ClientboundLevelChunkPacketData clonedPacketChunkData = (ClientboundLevelChunkPacketData) chunkDataShallowCloner.apply(originalPacket.getChunkData());
chunkBlockDataField.set(clonedPacketChunkData, newBlockDataBuffer);
chunkBlockEntitiesDataField.set(clonedPacketChunkData, newBlockEntities);
levelChunkPacketDataField.set(clonedPacket, clonedPacketChunkData);
return clonedPacket;
}
private static final Class<?> blockEntitiyInfoClass = Reflection.getClass("net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData$BlockEntityInfo");
private static final Reflection.Field<BlockEntityType> blockEntityInfoTypeField = Reflection.getField(blockEntitiyInfoClass, BlockEntityType.class, 0);
private static final Reflection.Field<Integer> packedXZField = Reflection.getField(blockEntitiyInfoClass, Integer.class, 0);
private static final Reflection.Field<Integer> yField = Reflection.getField(blockEntitiyInfoClass, Integer.class, 1);
private List<Object> filterBlockEntities(Player player, List<Object> blockEntities) {
return blockEntities.stream()
.filter((blockEntityInfo) -> {
BlockEntityType<?> type = blockEntityInfoTypeField.get(blockEntityInfo);
int packedXZ = packedXZField.get(blockEntityInfo);
int y = yField.get(blockEntityInfo);
int x = SectionPos.sectionRelativeX((short) packedXZ);
int z = SectionPos.sectionRelativeZ((short) packedXZ);
return isPlayerPrivilegedToAccessPosition(player, x, y, z) && isPlayerPrivilegedToAccessBlockEntity(player, x, y, z, type);
}).toList();
}
private void biomes(SectionHider section) {
@@ -187,7 +293,7 @@ public class ChunkHider {
private Set<Integer> blockIdsToObfuscate;
private final int blockIdToObfuscateTo;
public SectionHider(Player player, BlockState blockToObfuscateTo, Set<Integer> blockIdsToObfuscate, ByteBuf in, ByteBuf out, int chunkX, int chunkY, int chunkZ) {
public SectionHider(Player player, ByteBuf in, ByteBuf out, int chunkX, int chunkY, int chunkZ) {
this.player = player;
this.in = in;
this.out = out;
@@ -238,11 +344,6 @@ public class ChunkHider {
}
public void processPalette() {
if (skipSection) {
skipPalette();
return;
}
int paletteLength = copyVarInt();
if (paletteLength == 0) return;
@@ -140,6 +140,16 @@ public class ProtocolUtils {
return result;
}
public static int[] readVarIntArray(ByteBuf buffer, int length) {
int[] array = new int[length];
for(int i = 0; i < length; i++) {
array[i] = readVarInt(buffer);
}
return array;
}
public static void writeVarInt(ByteBuf buf, int value) {
do {
int temp = value & 0b01111111;
@@ -152,6 +162,12 @@ public class ProtocolUtils {
} while (value != 0);
}
public static void writeVarIntArray(ByteBuf buf, int[] values){
for(int varInt : values) {
writeVarInt(buf, varInt);
}
}
@Deprecated
public static int readVarIntLength(byte[] array, int startPos) {
int numRead = 0;