Migrate BauSystem to the new TechHider and remove the legacy TechHider

The new TechHider registers a permanent global filter, so BauSystem now uses
a single BauTechHider that combines the hiding rules of the spectator
techhider, /techhider and /xray per player. To support the different
obfuscation blocks of these rules, ForPlayer now provides the obfuscation
block per position.

The ChunkHider now also checks single valued chunk sections, which were sent
unfiltered before, and recalculates their block count if they change.
Unhandled packet types are logged once; statistics, debug samples and ping
responses are passed through.

The legacy TechHider and its access widener entries are removed.
This commit is contained in:
2026-10-31 15:00:37 +01:00
parent 7d4252325a
commit ce2a23f4b8
16 changed files with 295 additions and 644 deletions
@@ -69,6 +69,10 @@ public final class HullRegion {
return contains(location.getBlockX(), location.getBlockY(), location.getBlockZ());
}
public boolean chunkOutside(int chunkX, int chunkZ) {
return chunkX < (minX >> 4) || chunkX > ((maxX - 1) >> 4) || chunkZ < (minZ >> 4) || chunkZ > ((maxZ - 1) >> 4);
}
/**
* @return the index of the block within a bitset of {@link #volume()} bits, only valid for contained blocks
*/
@@ -53,5 +53,10 @@ public interface AccessPrivilegeProvider {
boolean canAccessEntity(int entityId);
boolean canAccessBlockEntity(int blockX, int blockY, int blockZ, BlockEntityType<?> type);
/**
* @return the block state sent instead of a block this player may not access at this position
*/
BlockState getObfuscation(int blockX, int blockY, int blockZ);
}
}
@@ -31,6 +31,7 @@ import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import org.bukkit.entity.Player;
import java.util.Arrays;
import java.util.List;
import java.util.function.UnaryOperator;
@@ -45,12 +46,6 @@ public class ChunkHider {
private final int BLOCKS_PER_SECTION = 4096;
private final int BIOMES_PER_SECTION = 64;
private final int blockIdUsedForHiding;
public ChunkHider(Block blockUsedForObfuscation) {
blockIdUsedForHiding = Block.BLOCK_STATE_REGISTRY.getId(blockUsedForObfuscation.defaultBlockState());
}
private int getLongsRequiredToEncodeEntries(int bitsPerEntry, int entryCount) {
int entriesPerLong = Long.SIZE / bitsPerEntry;
int dataLengthAsLongCount = (entryCount + entriesPerLong - 1) / entriesPerLong;
@@ -89,9 +84,22 @@ public class ChunkHider {
if (bitsPerBlock == 0) {
int sectionBlockId = ProtocolUtils.readVarInt(in);
out.writeShort(blockCount);
out.writeByte(bitsPerBlock);
ProtocolUtils.writeVarInt(out, sectionBlockId);
int[] blockData = new int[BLOCKS_PER_SECTION];
Arrays.fill(blockData, sectionBlockId);
int[] obfuscatedBlockData = obfuscateBlockDataArray(access, chunkX, chunkZ, yOffset, blockData);
if (Arrays.equals(blockData, obfuscatedBlockData)) {
out.writeShort(blockCount);
out.writeByte(bitsPerBlock);
ProtocolUtils.writeVarInt(out, sectionBlockId);
} else {
// The client skips sections without non-air blocks, so the count has to include the obfuscation blocks
out.writeShort(countNonAirBlocks(obfuscatedBlockData));
out.writeByte(15);
for (long rawDataSegment : encodeDirectBlockDataArray(obfuscatedBlockData)) {
out.writeLong(rawDataSegment);
}
}
} else if (bitsPerBlock <= BIT_PER_BLOCK_INDIRECTION_LIMIT) {
int palletLength = ProtocolUtils.readVarInt(in);
int[] pallet = ProtocolUtils.readVarIntArray(in, palletLength);
@@ -198,7 +206,7 @@ public class ChunkHider {
if (access.canAccessPosition(worldX, worldY, worldZ) && access.canAccessBlock(worldX, worldY, worldZ, block) && access.canAccessBlockState(worldX, worldY, worldZ, blockState)) {
obfuscatedData[blockDataIndex] = blockId;
} else {
obfuscatedData[blockDataIndex] = blockIdUsedForHiding;
obfuscatedData[blockDataIndex] = Block.getId(access.getObfuscation(worldX, worldY, worldZ));
}
}
}
@@ -207,6 +215,16 @@ public class ChunkHider {
return obfuscatedData;
}
private int countNonAirBlocks(int[] blockDataArray) {
int count = 0;
for (int blockId : blockDataArray) {
if (!Block.BLOCK_STATE_REGISTRY.byId(blockId).isAir()) {
count++;
}
}
return count;
}
private int[] resolveDirectBlockDataArray(long[] rawDataArray, byte bitsPerBlock) {
SimpleBitStorage data = new SimpleBitStorage(bitsPerBlock, BLOCKS_PER_SECTION, rawDataArray);
@@ -49,6 +49,7 @@ public final class ObfuscationConfig {
}
private final Block obfuscateWith;
private final BlockState obfuscationState;
private final Set<Block> blocks;
private final Set<BlockState> blockStates;
private final Set<BlockEntityType<?>> blockEntityTypes;
@@ -59,6 +60,7 @@ public final class ObfuscationConfig {
*/
public ObfuscationConfig(Material obfuscateWith, Set<Material> hiddenBlocks, Set<String> hiddenBlockEntities) {
this.obfuscateWith = CraftMagicNumbers.getBlock(obfuscateWith);
this.obfuscationState = this.obfuscateWith.defaultBlockState();
this.blocks = hiddenBlocks.stream()
.map(CraftMagicNumbers::getBlock)
@@ -36,16 +36,20 @@ import net.minecraft.network.protocol.configuration.ClientboundUpdateEnabledFeat
import net.minecraft.network.protocol.cookie.ClientboundCookieRequestPacket;
import net.minecraft.network.protocol.game.*;
import net.minecraft.network.protocol.login.*;
import net.minecraft.network.protocol.ping.ClientboundPongResponsePacket;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec3;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.ToIntFunction;
import java.util.logging.Level;
/**
* The TechHider follows the default-deny security principle,
@@ -56,16 +60,14 @@ public class TechHider {
private final Set<Class<?>> bypassingPackets;
private final Map<Class<? extends Packet<? extends PacketListener>>, BiFunction<ForPlayer, Packet<? extends PacketListener>, Packet<? extends PacketListener>>> packetProcessors;
private final BlockState blockStateUsedForObfuscation;
private final Set<Class<?>> loggedUnhandledPackets = ConcurrentHashMap.newKeySet();
private final ChunkHider chunkHider;
private final AccessPrivilegeProvider privilegeProvider;
// TODO handle packet bundle
public TechHider(Block blockUsedForObfuscation, AccessPrivilegeProvider privilegeProvider) {
this.blockStateUsedForObfuscation = blockUsedForObfuscation.defaultBlockState();
this.chunkHider = new ChunkHider(blockUsedForObfuscation);
public TechHider(AccessPrivilegeProvider privilegeProvider) {
this.chunkHider = new ChunkHider();
this.privilegeProvider = privilegeProvider;
this.bypassingPackets = new HashSet<>(List.of(
@@ -176,6 +178,9 @@ public class TechHider {
ClientboundBlockChangedAckPacket.class, // 7.1.5 Acknowledge Block Change
ClientboundChunkBatchFinishedPacket.class, // 7.1.12 Chunk Batch Finished (Delimiter)
ClientboundChunkBatchStartPacket.class, // 7.1.13 Chunk Batch Start (Delimiter)
ClientboundAwardStatsPacket.class, // 7.1.4 Award Statistics: the player's own statistics
ClientboundDebugSamplePacket.class, // 7.1.28 Debug Sample: server tick timings
ClientboundPongResponsePacket.class, // Ping Response: answer to the client's own ping
ClientboundChunksBiomesPacket.class, // 7.1.14 Chunk Biomes
ClientboundContainerClosePacket.class, // 7.1.18 Close Container
ClientboundSetChunkCacheCenterPacket.class, // 7.1.93 Set Center Chunk
@@ -359,6 +364,9 @@ public class TechHider {
if (processor != null) {
return processor.apply(access.get(), packet);
} else {
if (loggedUnhandledPackets.add(packet.getClass())) {
Bukkit.getLogger().log(Level.WARNING, "[TechHider] Dropping unhandled packet type " + packet.getClass().getName());
}
return null;
}
}
@@ -438,7 +446,7 @@ public class TechHider {
if (access.canAccessPosition(blockX, blockY, blockZ) && access.canAccessBlock(blockX, blockY, blockZ, block) && access.canAccessBlockState(blockX, blockY, blockZ, blockState)) {
return packet;
} else if (access.canAccessPosition(blockX, blockY, blockZ)) {
return new ClientboundBlockUpdatePacket(blockPos, blockStateUsedForObfuscation);
return new ClientboundBlockUpdatePacket(blockPos, access.getObfuscation(blockX, blockY, blockZ));
} else {
return null;
}
@@ -491,7 +499,7 @@ public class TechHider {
filteredStates.add(state);
} else if (access.canAccessPosition(worldX, worldY, worldZ)) {
filteredPos.add(posShort);
filteredStates.add(blockStateUsedForObfuscation);
filteredStates.add(access.getObfuscation(worldX, worldY, worldZ));
}
}