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
@@ -36,6 +36,7 @@ dependencies {
annotationProcessor(libs.classindex)
compileOnly(project(":SpigotCore", "default"))
compileOnly(project(":KotlinCore", "default"))
implementation(project(":HiderCore"))
compileOnly(libs.axiom)
compileOnly(libs.authlib)
@@ -0,0 +1,137 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.techhider;
import de.steamwar.bausystem.features.world.SpectatorListener;
import de.steamwar.bausystem.features.xray.XrayCommand;
import de.steamwar.linkage.Linked;
import de.steamwar.linkage.LinkedInstance;
import de.steamwar.techhider.AccessPrivilegeProvider;
import de.steamwar.techhider.TechHider;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
/**
* The single TechHider of the server. It combines the hiding rules of the spectator techhider, /techhider and /xray per player.
*/
@Linked
public class BauTechHider implements AccessPrivilegeProvider {
private static final HidingRule[] NO_RULES = new HidingRule[0];
// Injected after construction, until then nothing is hidden
@LinkedInstance
public XrayCommand xrayCommand;
@LinkedInstance
public TechHiderCommand techHiderCommand;
@LinkedInstance
public SpectatorListener spectatorListener;
public BauTechHider() {
new TechHider(this);
}
@Override
public ForPlayer forPlayer(Player p) {
if (xrayCommand == null || techHiderCommand == null || spectatorListener == null) {
return new BauPlayerAccess(p, NO_RULES);
}
// Region rules first: where rules overlap, the first one decides the obfuscation block
List<HidingRule> rules = new ArrayList<>();
xrayCommand.addHidingRules(p, rules);
techHiderCommand.addHidingRules(p, rules);
spectatorListener.addHidingRules(p, rules);
return new BauPlayerAccess(p, rules.toArray(NO_RULES));
}
@Override
public boolean isPlayerPrivilegedToPerformAction(Player p) {
return true;
}
/**
* Snapshot of the hiding rules of one player, taken once per outgoing packet on the netty thread.
*/
private record BauPlayerAccess(Player player, HidingRule[] rules) implements ForPlayer {
@Override
public Player getPlayer() {
return player;
}
@Override
public boolean canAccessAllDataWithinChunk(int chunkX, int chunkZ) {
for (HidingRule rule : rules) {
if (rule.coversChunk(chunkX, chunkZ)) return false;
}
return true;
}
@Override
public boolean canAccessPosition(int blockX, int blockY, int blockZ) {
return true;
}
@Override
public boolean canAccessBlock(int blockX, int blockY, int blockZ, Block block) {
for (HidingRule rule : rules) {
if (rule.covers(blockX, blockY, blockZ) && rule.obfuscation().hidesBlock(block)) return false;
}
return true;
}
@Override
public boolean canAccessBlockState(int blockX, int blockY, int blockZ, BlockState blockState) {
for (HidingRule rule : rules) {
if (rule.covers(blockX, blockY, blockZ) && rule.obfuscation().hidesBlockState(blockState)) return false;
}
return true;
}
@Override
public boolean canAccessEntity(int entityId) {
return true;
}
@Override
public boolean canAccessBlockEntity(int blockX, int blockY, int blockZ, BlockEntityType<?> type) {
for (HidingRule rule : rules) {
if (rule.covers(blockX, blockY, blockZ) && rule.obfuscation().hidesBlockEntity(type)) return false;
}
return true;
}
@Override
public BlockState getObfuscation(int blockX, int blockY, int blockZ) {
for (HidingRule rule : rules) {
if (rule.covers(blockX, blockY, blockZ)) return rule.obfuscation().getObfuscationState();
}
// Only asked for positions a rule hides
return Blocks.END_STONE.defaultBlockState();
}
}
}
@@ -0,0 +1,52 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.bausystem.features.techhider;
import de.steamwar.bausystem.region.Point;
import de.steamwar.bausystem.region.Region;
import de.steamwar.hullhider.HullRegion;
import de.steamwar.techhider.ObfuscationConfig;
/**
* Hides the blocks of an obfuscation config within an area, or everywhere if the area is null.
*/
public record HidingRule(HullRegion area, ObfuscationConfig obfuscation) {
public static HidingRule everywhere(ObfuscationConfig obfuscation) {
return new HidingRule(null, obfuscation);
}
/**
* Covers the area including its extension.
*/
public static HidingRule of(Region.Area area, ObfuscationConfig obfuscation) {
Point min = area.getMinPoint(true);
Point max = area.getMaxPoint(true);
return new HidingRule(HullRegion.fromInclusive(min.getX(), min.getY(), min.getZ(), max.getX(), max.getY(), max.getZ()), obfuscation);
}
public boolean covers(int x, int y, int z) {
return area == null || area.contains(x, y, z);
}
public boolean coversChunk(int chunkX, int chunkZ) {
return area == null || !area.chunkOutside(chunkX, chunkZ);
}
}
@@ -31,7 +31,7 @@ import de.steamwar.command.TypeMapper;
import de.steamwar.core.CraftbukkitWrapper;
import de.steamwar.linkage.Linked;
import de.steamwar.linkage.LinkedInstance;
import de.steamwar.techhider.legacy.TechHider;
import de.steamwar.techhider.ObfuscationConfig;
import net.md_5.bungee.api.ChatMessageType;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
@@ -41,6 +41,7 @@ import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@Linked
@@ -50,8 +51,9 @@ public class TechHiderCommand extends SWCommand implements Listener, ScoreboardE
super("techhider", "hider", "obfuscate", "hide", "hide-tech");
}
private Map<Region, Optional<TechHider>> techHiders = new HashMap<>();
private Map<Region, Set<Player>> hidden = new HashMap<>();
// Read on netty threads by the BauTechHider
private Map<Region, Optional<HidingRule>> hidingRules = new ConcurrentHashMap<>();
private Map<Region, Set<Player>> hidden = new ConcurrentHashMap<>();
@LinkedInstance
public XrayCommand xrayCommand;
@@ -64,22 +66,15 @@ public class TechHiderCommand extends SWCommand implements Listener, ScoreboardE
return;
}
Optional<TechHider> techHider = techHiders.computeIfAbsent(region, rg -> {
Optional<HidingRule> hidingRule = hidingRules.computeIfAbsent(region, rg -> {
if (!region.getGameModeConfig().Techhider.Active) {
return Optional.empty();
}
hidden.put(rg, new HashSet<>());
TechHider current = new TechHider((p, cX, cY) -> {
if (rg.getBuildArea().isChunkOutside(cX, cY)) return true;
return !hidden.get(rg).contains(p);
}, region.getGameModeConfig().Techhider.ObfuscateWith, region.getGameModeConfig().Techhider.HiddenBlocks, region.getGameModeConfig().Techhider.HiddenBlockEntities);
current.enable();
return Optional.of(current);
hidden.put(rg, ConcurrentHashMap.newKeySet());
return Optional.of(HidingRule.of(rg.getBuildArea(), ObfuscationConfig.of(rg.getGameModeConfig().Techhider)));
});
if (!techHider.isPresent()) {
if (!hidingRule.isPresent()) {
return;
}
@@ -128,6 +123,14 @@ public class TechHiderCommand extends SWCommand implements Listener, ScoreboardE
hidden.values().forEach(set -> set.remove(event.getPlayer()));
}
public void addHidingRules(Player player, List<HidingRule> rules) {
hidden.forEach((region, players) -> {
if (players.contains(player)) {
hidingRules.getOrDefault(region, Optional.empty()).ifPresent(rules::add);
}
});
}
public void disable(Region region, Player player) {
hidden.computeIfPresent(region, (rg, set) -> {
set.remove(player);
@@ -22,10 +22,11 @@ package de.steamwar.bausystem.features.world;
import de.steamwar.bausystem.BauSystem;
import de.steamwar.bausystem.Permission;
import de.steamwar.bausystem.config.BauServer;
import de.steamwar.bausystem.features.techhider.HidingRule;
import de.steamwar.bausystem.utils.BauMemberUpdateEvent;
import de.steamwar.linkage.Linked;
import de.steamwar.sql.BauweltMember;
import de.steamwar.techhider.legacy.TechHider;
import de.steamwar.techhider.ObfuscationConfig;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
@@ -41,14 +42,18 @@ import org.bukkit.event.entity.EntityPickupItemEvent;
import org.bukkit.event.player.*;
import org.bukkit.util.Vector;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@Linked
public class SpectatorListener implements Listener {
private static final TechHider techHider;
private static final Set<Player> NO_TECHHIDER = new HashSet<>();
private static final HidingRule SPECTATOR_RULE;
// Read on netty threads by the BauTechHider
private static final Set<Player> NO_TECHHIDER = ConcurrentHashMap.newKeySet();
static {
Set<Material> materials = new HashSet<>();
@@ -99,19 +104,12 @@ public class SpectatorListener implements Listener {
materials.add(Material.WATER);
materials.remove(Material.BARRIER);
materials.remove(Material.STONE);
techHider = new TechHider((TechHider.LocationEvaluator) (player, i, i1) -> {
return Permission.BUILD.hasPermission(player) || NO_TECHHIDER.contains(player);
}, Material.END_STONE, materials, new HashSet<>());
SPECTATOR_RULE = HidingRule.everywhere(new ObfuscationConfig(Material.END_STONE, materials, Collections.emptySet()));
}
private static void enableOrDisableTechhider() {
boolean enable = Bukkit.getOnlinePlayers()
.stream()
.anyMatch(player -> !(Permission.BUILD.hasPermission(player) || NO_TECHHIDER.contains(player)));
if (enable) {
techHider.enable();
} else {
techHider.disable();
public void addHidingRules(Player player, List<HidingRule> rules) {
if (!(Permission.BUILD.hasPermission(player) || NO_TECHHIDER.contains(player))) {
rules.add(SPECTATOR_RULE);
}
}
@@ -122,7 +120,6 @@ public class SpectatorListener implements Listener {
NO_TECHHIDER.add(player);
}
resendChunks(player);
enableOrDisableTechhider();
}
private static void resendChunks(Player player) {
@@ -141,7 +138,6 @@ public class SpectatorListener implements Listener {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
enableOrDisableTechhider();
if (BauSystem.DEV_SERVER) return;
if (event.getPlayer().getUniqueId().equals(BauServer.getInstance().getOwner())) {
return;
@@ -163,7 +159,6 @@ public class SpectatorListener implements Listener {
@EventHandler
public void onBauMemberUpdate(BauMemberUpdateEvent event) {
enableOrDisableTechhider();
if (!anySupervisorOnline(null) && !BauSystem.DEV_SERVER) {
Bukkit.getOnlinePlayers().forEach(player -> {
player.kickPlayer("");
@@ -180,7 +175,6 @@ public class SpectatorListener implements Listener {
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
NO_TECHHIDER.remove(event.getPlayer());
Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), SpectatorListener::enableOrDisableTechhider, 1);
if (BauSystem.DEV_SERVER) return;
if (!anySupervisorOnline(event.getPlayer())) {
Bukkit.getOnlinePlayers().forEach(player -> {
@@ -21,6 +21,7 @@ package de.steamwar.bausystem.features.xray;
import com.comphenix.tinyprotocol.TinyProtocol;
import de.steamwar.bausystem.BauSystem;
import de.steamwar.bausystem.features.techhider.HidingRule;
import de.steamwar.bausystem.features.techhider.TechHiderCommand;
import de.steamwar.bausystem.region.Region;
import de.steamwar.bausystem.utils.ScoreboardElement;
@@ -28,7 +29,7 @@ import de.steamwar.command.SWCommand;
import de.steamwar.core.CraftbukkitWrapper;
import de.steamwar.linkage.Linked;
import de.steamwar.linkage.LinkedInstance;
import de.steamwar.techhider.legacy.TechHider;
import de.steamwar.techhider.ObfuscationConfig;
import net.md_5.bungee.api.ChatMessageType;
import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket;
import net.minecraft.server.level.ServerPlayer;
@@ -43,6 +44,7 @@ import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;
@Linked
@@ -52,9 +54,10 @@ public class XrayCommand extends SWCommand implements Listener, ScoreboardElemen
super("xray");
}
private Map<Region, Optional<TechHider>> techHiders = new HashMap<>();
// Read on netty threads by the BauTechHider and the movement filter
private Map<Region, Optional<HidingRule>> hidingRules = new ConcurrentHashMap<>();
private Map<Region, Set<Material>> xrayedBlocks = new HashMap<>();
private Map<Region, Set<Player>> hidden = new HashMap<>();
private Map<Region, Set<Player>> hidden = new ConcurrentHashMap<>();
@LinkedInstance
public TechHiderCommand techHiderCommand;
@@ -67,19 +70,19 @@ public class XrayCommand extends SWCommand implements Listener, ScoreboardElemen
return;
}
Optional<TechHider> techHider = techHiders.computeIfAbsent(region, rg -> {
Optional<HidingRule> hidingRule = hidingRules.computeIfAbsent(region, rg -> {
if (!region.getGameModeConfig().Techhider.Active) {
return Optional.empty();
}
hidden.put(rg, new HashSet<>());
hidden.put(rg, ConcurrentHashMap.newKeySet());
Set<Material> blocks = new HashSet<>(Arrays.asList(region.getGameModeConfig().Techhider.ObfuscateWith));
if (blocks.contains(Material.END_STONE)) blocks.add(Material.END_STONE_BRICKS);
xrayedBlocks.put(region, blocks);
return Optional.of(createXray(rg, blocks));
return Optional.of(HidingRule.of(rg.getBuildArea(), new ObfuscationConfig(Material.STRUCTURE_VOID, blocks, Collections.emptySet())));
});
if (!techHider.isPresent()) {
if (!hidingRule.isPresent()) {
return;
}
@@ -139,13 +142,12 @@ public class XrayCommand extends SWCommand implements Listener, ScoreboardElemen
}
}
private TechHider createXray(Region rg, Set<Material> obfuscate) {
TechHider current = new TechHider((TechHider.LocationEvaluator) (p, cX, cY) -> {
if (rg.getBuildArea().isChunkOutside(cX, cY)) return true;
return !hidden.get(rg).contains(p);
}, Material.STRUCTURE_VOID, obfuscate, new HashSet<>());
current.enable();
return current;
public void addHidingRules(Player player, List<HidingRule> rules) {
hidden.forEach((region, players) -> {
if (players.contains(player)) {
hidingRules.getOrDefault(region, Optional.empty()).ifPresent(rules::add);
}
});
}
@EventHandler
@@ -70,7 +70,7 @@ public class TechHiderWrapper extends StateDependent implements Listener {
@Override
public void enable() {
ObfuscationConfig obfuscation = ObfuscationConfig.of(Config.GameModeConfig.Techhider);
new TechHider(obfuscation.getObfuscateWith(), newProvider(obfuscation));
new TechHider(newProvider(obfuscation));
}
private AccessPrivilegeProvider newProvider(ObfuscationConfig obfuscation) {
@@ -150,6 +150,11 @@ public class TechHiderWrapper extends StateDependent implements Listener {
public boolean canAccessBlockEntity(int blockX, int blockY, int blockZ, BlockEntityType<?> type) {
return !isHidden(blockX, blockY, blockZ) || !obfuscation.hidesBlockEntity(type);
}
@Override
public BlockState getObfuscation(int blockX, int blockY, int blockZ) {
return obfuscation.getObfuscationState();
}
}
@Override
@@ -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));
}
}
@@ -1,301 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.techhider.legacy;
import de.steamwar.core.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import lombok.Getter;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData;
import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket;
import net.minecraft.util.SimpleBitStorage;
import net.minecraft.world.level.block.entity.BlockEntityType;
import org.bukkit.entity.Player;
import java.util.Collections;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
@Deprecated
public class ChunkHider {
public static final ChunkHider impl = new ChunkHider();
private static final UnaryOperator<ClientboundLevelChunkWithLightPacket> chunkPacketCloner = ProtocolUtils.shallowCloneGenerator(ClientboundLevelChunkWithLightPacket.class, ClientboundLevelChunkWithLightPacket::new);
private static final UnaryOperator<ClientboundLevelChunkPacketData> chunkDataCloner = ProtocolUtils.shallowCloneGenerator(ClientboundLevelChunkPacketData.class, ClientboundLevelChunkPacketData::new);
public BiFunction<Player, ClientboundLevelChunkWithLightPacket, ClientboundLevelChunkWithLightPacket> chunkHiderGenerator(TechHider techHider) {
return (p, packet) -> {
int chunkX = packet.getX();
int chunkZ = packet.getZ();
if (techHider.getLocationEvaluator().skipChunk(p, chunkX, chunkZ)) {
return packet;
}
packet = chunkPacketCloner.apply(packet);
ClientboundLevelChunkPacketData dataWrapper = chunkDataCloner.apply(packet.getChunkData());
Set<String> hiddenBlockEntities = techHider.getHiddenBlockEntities();
dataWrapper.blockEntitiesData = dataWrapper.blockEntitiesData.stream()
.filter(te -> tileEntityVisible(hiddenBlockEntities, te))
.collect(Collectors.toList());
ByteBuf in = Unpooled.wrappedBuffer(dataWrapper.buffer);
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);
dataWrapper.buffer = data;
packet.chunkData = dataWrapper;
return packet;
};
}
protected boolean tileEntityVisible(Set<String> hiddenBlockEntities, ClientboundLevelChunkPacketData.BlockEntityInfo tile) {
BlockEntityType<?> type = tile.type;
String path = BuiltInRegistries.BLOCK_ENTITY_TYPE.getKey(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();
}
if (section.isSkipSection() || (!section.blockPrecise() && section.isPaletted())) {
section.skipNewDataArray(4096);
return;
}
SimpleBitStorage values = new SimpleBitStorage(section.getBitsPerBlock(), 4096, section.readNewDataArray(4096));
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;
TechHider.State test = section.test(x, y, z);
switch (test) {
case SKIP:
break;
case CHECK:
if (!section.getObfuscate().contains(values.get(pos))) {
break;
}
case HIDE:
values.set(pos, section.getTarget());
break;
case HIDE_AIR:
default:
values.set(pos, section.getAir());
}
}
}
}
section.writeDataArray(values.getRaw());
}
private void biomes(SectionHider section) {
section.copyBitsPerBlock();
if (section.getBitsPerBlock() == 0) {
section.copyVarInt();
} else if (section.getBitsPerBlock() < 6) {
section.skipPalette();
section.skipNewDataArray(64);
} else {
// Direct (global) biome IDs – no palette present
section.skipNewDataArray(64);
}
}
@Getter
class SectionHider {
private final Player player;
private final TechHider techHider;
private final ByteBuf in;
private final ByteBuf out;
private final int chunkX;
private final int chunkY;
private final int chunkZ;
private final int offsetX;
private final int offsetY;
private final int offsetZ;
private final boolean skipSection;
private boolean paletted;
private int bitsPerBlock;
private int blockCount;
private int air;
private int target;
private Set<Integer> obfuscate;
public SectionHider(Player player, TechHider techHider, ByteBuf in, ByteBuf out, int chunkX, int chunkY, int chunkZ) {
this.player = player;
this.techHider = techHider;
this.in = in;
this.out = out;
this.chunkX = chunkX;
this.chunkY = chunkY;
this.chunkZ = chunkZ;
this.offsetX = 16 * chunkX;
this.offsetY = 16 * chunkY;
this.offsetZ = 16 * chunkZ;
this.skipSection = techHider.getLocationEvaluator().skipChunkSection(player, chunkX, chunkY, chunkZ);
this.paletted = false;
this.bitsPerBlock = 0;
this.air = TechHider.AIR_ID;
this.target = techHider.getObfuscationTargetId();
this.obfuscate = techHider.getObfuscateIds();
}
public boolean blockPrecise() {
return techHider.getLocationEvaluator().blockPrecise(player, chunkX, chunkY, chunkZ);
}
public TechHider.State test(int x, int y, int z) {
return techHider.getLocationEvaluator().check(player, offsetX + x, offsetY + y, offsetZ + z);
}
public void copyBlockCount() {
this.blockCount = in.readShort();
out.writeShort(blockCount);
}
public void copyBitsPerBlock() {
bitsPerBlock = in.readByte();
out.writeByte(bitsPerBlock);
}
public int copyVarInt() {
int value = ProtocolUtils.readVarInt(in);
ProtocolUtils.writeVarInt(out, value);
return value;
}
public void skipPalette() {
int paletteLength = copyVarInt();
for (int i = 0; i < paletteLength; i++) {
copyVarInt();
}
}
public void processPalette() {
if (skipSection) {
skipPalette();
return;
}
int paletteLength = copyVarInt();
if (paletteLength == 0) return;
paletted = true;
air = 0;
target = 0;
for (int i = 0; i < paletteLength; i++) {
int entry = ProtocolUtils.readVarInt(in);
if (obfuscate.contains(entry)) {
entry = techHider.getObfuscationTargetId();
}
if (entry == TechHider.AIR_ID) {
air = i;
} else if (entry == techHider.getObfuscationTargetId()) {
target = i;
}
ProtocolUtils.writeVarInt(out, entry);
}
obfuscate = Collections.emptySet();
}
public void skipDataArray() {
int dataArrayLength = copyVarInt();
out.writeBytes(in, dataArrayLength * 8);
}
public void skipNewDataArray(int entries) {
if (bitsPerBlock == 0) {
return;
}
char valuesPerLong = (char) (64 / bitsPerBlock);
int i1 = (entries + valuesPerLong - 1) / valuesPerLong;
out.writeBytes(in, i1 * Long.BYTES);
}
public long[] readDataArray() {
long[] array = new long[copyVarInt()];
for (int i = 0; i < array.length; i++) {
array[i] = in.readLong();
}
return array;
}
public long[] readNewDataArray(int entries) {
if (bitsPerBlock == 0) {
return new long[entries];
}
char valuesPerLong = (char) (64 / bitsPerBlock);
int i1 = (entries + valuesPerLong - 1) / valuesPerLong;
long[] array = new long[i1];
for (int i = 0; i < i1; i++) {
array[i] = in.readLong();
}
return array;
}
public void writeDataArray(long[] array) {
for (long l : array) {
out.writeLong(l);
}
}
}
}
@@ -1,79 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.techhider.legacy;
import net.minecraft.core.SectionPos;
import net.minecraft.network.protocol.game.ClientboundSectionBlocksUpdatePacket;
import net.minecraft.world.level.block.state.BlockState;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.function.BiFunction;
@Deprecated
public class ProtocolWrapper {
public static final ProtocolWrapper impl = new ProtocolWrapper();
public BiFunction<Player, ClientboundSectionBlocksUpdatePacket, ClientboundSectionBlocksUpdatePacket> multiBlockChangeGenerator(TechHider techHider) {
return (p, packet) -> {
TechHider.LocationEvaluator locationEvaluator = techHider.getLocationEvaluator();
SectionPos chunkCoords = packet.sectionPos;
int chunkX = chunkCoords.getX();
int chunkY = chunkCoords.getY();
int chunkZ = chunkCoords.getZ();
if (locationEvaluator.skipChunkSection(p, chunkX, chunkY, chunkZ)) {
return packet;
}
packet = TechHider.multiBlockChangeCloner.apply(packet);
final short[] oldPos = packet.positions;
final BlockState[] oldBlocks = packet.states;
ArrayList<Short> poss = new ArrayList<>(oldPos.length);
ArrayList<BlockState> blocks = new ArrayList<>(oldPos.length);
for (int i = 0; i < oldPos.length; i++) {
short pos = oldPos[i];
BlockState block = oldBlocks[i];
switch (locationEvaluator.check(p, 16 * chunkX + (pos >> 8 & 0xF), 16 * chunkY + (pos & 0xF), 16 * chunkZ + (pos >> 4 & 0xF))) {
case SKIP:
poss.add(pos);
blocks.add(block);
break;
case CHECK:
poss.add(pos);
blocks.add(techHider.iBlockDataHidden(block) ? (BlockState) techHider.getObfuscationTarget() : block);
break;
default:
break;
}
}
if (blocks.isEmpty()) return null;
short[] newPos = new short[poss.size()];
for (int i = 0; i < newPos.length; i++) {
newPos[i] = poss.get(i);
}
packet.positions = newPos;
packet.states = blocks.toArray(BlockState[]::new);
return packet;
};
}
}
@@ -1,169 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.techhider.legacy;
import com.comphenix.tinyprotocol.TinyProtocol;
import de.steamwar.core.BlockIds;
import de.steamwar.core.ProtocolUtils;
import lombok.Getter;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Vec3i;
import net.minecraft.network.protocol.game.*;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import org.bukkit.Material;
import org.bukkit.craftbukkit.util.CraftMagicNumbers;
import org.bukkit.entity.Player;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
@Deprecated
public class TechHider {
public static final Class<?> blockPosition = BlockPos.class;
public static final Class<?> iBlockData = BlockState.class;
public static final Class<?> block = Block.class;
public boolean iBlockDataHidden(BlockState iBlockData) {
return obfuscateIds.contains(BlockIds.impl.getCombinedId(iBlockData));
}
public static final Object AIR = CraftMagicNumbers.getBlock(Material.AIR).defaultBlockState();
public static final int AIR_ID = BlockIds.impl.materialToId(Material.AIR);
private final Map<Class<?>, BiFunction<Player, Object, Object>> techhiders = new HashMap<>();
@Getter
private final LocationEvaluator locationEvaluator;
@Getter
private final Object obfuscationTarget;
@Getter
private final int obfuscationTargetId;
@Getter
private final Set<Integer> obfuscateIds;
@Getter
private final Set<String> hiddenBlockEntities;
public TechHider(LocationEvaluator locationEvaluator, Material obfuscationTarget, Set<Material> obfuscate, Set<String> hiddenBlockEntities) {
this.locationEvaluator = locationEvaluator;
this.obfuscateIds = obfuscate.stream().flatMap(m -> BlockIds.impl.materialToAllIds(m).stream()).collect(Collectors.toSet());
this.hiddenBlockEntities = hiddenBlockEntities;
this.obfuscationTarget = CraftMagicNumbers.getBlock(obfuscationTarget).defaultBlockState();
this.obfuscationTargetId = BlockIds.impl.materialToId(obfuscationTarget);
techhiders.put(ClientboundBlockEventPacket.class, (player, o) -> this.blockActionHider(player, (ClientboundBlockEventPacket) o));
techhiders.put(ClientboundBlockUpdatePacket.class, (player, o) -> this.blockChangeHider(player, (ClientboundBlockUpdatePacket) o));
techhiders.put(ClientboundBlockEntityDataPacket.class, (player, o) -> this.tileEntityDataHider(player, (ClientboundBlockEntityDataPacket) o));
techhiders.put(ClientboundSectionBlocksUpdatePacket.class, (BiFunction) ProtocolWrapper.impl.multiBlockChangeGenerator(this));
techhiders.put(ClientboundLevelChunkWithLightPacket.class, (BiFunction) ChunkHider.impl.chunkHiderGenerator(this));
techhiders.put(ServerboundUseItemOnPacket.class, (p, packet) -> locationEvaluator.suppressInteractions(p) ? null : packet);
techhiders.put(ServerboundInteractPacket.class, (p, packet) -> locationEvaluator.suppressInteractions(p) ? null : packet);
}
public void enable() {
techhiders.forEach((type, playerObjectBiFunction) -> TinyProtocol.instance.addFilter((Class) type, playerObjectBiFunction));
}
public void disable() {
techhiders.forEach(TinyProtocol.instance::removeFilter);
}
public static final UnaryOperator<ClientboundSectionBlocksUpdatePacket> multiBlockChangeCloner = ProtocolUtils.shallowCloneGenerator(ClientboundSectionBlocksUpdatePacket.class, ClientboundSectionBlocksUpdatePacket::new);
private static final UnaryOperator<ClientboundBlockUpdatePacket> blockChangeCloner = ProtocolUtils.shallowCloneGenerator(ClientboundBlockUpdatePacket.class, ClientboundBlockUpdatePacket::new);
private ClientboundBlockUpdatePacket blockChangeHider(Player p, ClientboundBlockUpdatePacket packet) {
switch (locationEvaluator.checkBlockPos(p, packet.getPos())) {
case SKIP:
return packet;
case CHECK:
if (!iBlockDataHidden(packet.blockState)) {
return packet;
}
case HIDE:
packet = blockChangeCloner.apply(packet);
packet.blockState = (BlockState) obfuscationTarget;
return packet;
case HIDE_AIR:
default:
packet = blockChangeCloner.apply(packet);
packet.blockState = (BlockState) AIR;
return packet;
}
}
private Object blockActionHider(Player p, ClientboundBlockEventPacket packet) {
if (locationEvaluator.checkBlockPos(p, packet.getPos()) == State.SKIP) {
return packet;
}
return null;
}
private ClientboundBlockEntityDataPacket tileEntityDataHider(Player p, ClientboundBlockEntityDataPacket packet) {
switch (locationEvaluator.checkBlockPos(p, packet.getPos())) {
case SKIP:
return packet;
case CHECK:
if (packet.getType() != BlockEntityType.SIGN) {
return packet;
}
default:
return null;
}
}
public enum State {
SKIP,
CHECK,
HIDE,
HIDE_AIR
}
public interface LocationEvaluator {
default boolean suppressInteractions(Player player) {
return false;
}
boolean skipChunk(Player player, int x, int z);
default boolean skipChunkSection(Player player, int x, int y, int z) {
return skipChunk(player, x, z);
}
default State check(Player player, int x, int y, int z) {
return skipChunkSection(player, ProtocolUtils.posToChunk(x), ProtocolUtils.posToChunk(y), ProtocolUtils.posToChunk(z)) ? State.SKIP : State.CHECK;
}
default State checkBlockPos(Player player, Vec3i pos) {
return check(player, pos.getX(), pos.getY(), pos.getZ());
}
default boolean blockPrecise(Player player, int x, int y, int z) {
return false;
}
}
}
@@ -35,34 +35,3 @@ accessible field net/minecraft/network/protocol/game/ClientboundRotateHeadPacket
mutable field net/minecraft/network/protocol/game/ClientboundRotateHeadPacket yHeadRot B
accessible field net/minecraft/network/protocol/game/ClientboundRotateHeadPacket entityId I
mutable field net/minecraft/network/protocol/game/ClientboundRotateHeadPacket entityId I
# For legacy/ChunkHider
## transitive-extendable means that a public no args constructor is added without any super initialization
transitive-extendable class net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket
accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket chunkData Lnet/minecraft/network/protocol/game/ClientboundLevelChunkPacketData;
mutable field net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket chunkData Lnet/minecraft/network/protocol/game/ClientboundLevelChunkPacketData;
## transitive-extendable means that a public no args constructor is added without any super initialization
transitive-extendable class net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData
accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData buffer [B
mutable field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData buffer [B
accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData blockEntitiesData Ljava/util/List;
mutable field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData blockEntitiesData Ljava/util/List;
accessible class net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData$BlockEntityInfo
accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData$BlockEntityInfo type Lnet/minecraft/world/level/block/entity/BlockEntityType;
accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData$BlockEntityInfo packedXZ I
accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData$BlockEntityInfo y I
# For legacy/TechHider
## transitive-extendable means that a public no args constructor is added without any super initialization
transitive-extendable class net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket
accessible field net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket sectionPos Lnet/minecraft/core/SectionPos;
accessible field net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket positions [S
mutable field net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket positions [S
accessible field net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket states [Lnet/minecraft/world/level/block/state/BlockState;
mutable field net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket states [Lnet/minecraft/world/level/block/state/BlockState;
## transitive-extendable means that a public no args constructor is added without any super initialization
transitive-extendable class net/minecraft/network/protocol/game/ClientboundBlockUpdatePacket
mutable field net/minecraft/network/protocol/game/ClientboundBlockUpdatePacket blockState Lnet/minecraft/world/level/block/state/BlockState;