diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/TechHiderCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/TechHiderCommand.java
index 4820b072..4744e41d 100644
--- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/TechHiderCommand.java
+++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/TechHiderCommand.java
@@ -38,6 +38,7 @@ import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
+import de.steamwar.techhider.legacy.TechHider;
import java.util.*;
import java.util.stream.Collectors;
diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/SpectatorListener.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/SpectatorListener.java
index 3b02bffc..92a5c59e 100644
--- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/SpectatorListener.java
+++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/SpectatorListener.java
@@ -39,6 +39,7 @@ import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.EntityPickupItemEvent;
import org.bukkit.event.player.*;
import org.bukkit.util.Vector;
+import de.steamwar.techhider.legacy.TechHider;
import java.util.HashSet;
import java.util.Set;
diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/xray/XrayCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/xray/XrayCommand.java
index 1ddfe6f7..55bcbeeb 100644
--- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/xray/XrayCommand.java
+++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/xray/XrayCommand.java
@@ -40,6 +40,7 @@ import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerQuitEvent;
+import de.steamwar.techhider.legacy.TechHider;
import java.util.*;
import java.util.function.BiFunction;
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/BlockIds.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/BlockIds.java
new file mode 100644
index 00000000..0d10088f
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/BlockIds.java
@@ -0,0 +1,68 @@
+/*
+ * 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 .
+ */
+
+package de.steamwar.techhider.legacy;
+
+import de.steamwar.Reflection;
+import net.minecraft.core.IdMapper;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.material.FluidState;
+import net.minecraft.world.level.material.Fluids;
+import org.bukkit.Material;
+import org.bukkit.craftbukkit.util.CraftMagicNumbers;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class BlockIds {
+ public static final BlockIds impl = new BlockIds();
+
+ public int materialToId(Material material) {
+ return getCombinedId(getBlock(material).defaultBlockState());
+ }
+
+ private static final FluidState water = Fluids.WATER.getSource(false);
+ private static final Iterable registryBlockId = (Iterable) Reflection.getField(TechHider.block, IdMapper.class, 0).get(null);
+
+ public Set materialToAllIds(Material material) {
+ Set ids = new HashSet<>();
+ for (BlockState data : getBlock(material).getStateDefinition().getPossibleStates()) {
+ ids.add(getCombinedId(data));
+ }
+
+ if (material == Material.WATER) {
+ for (BlockState data : registryBlockId) {
+ if (data.getFluidState() == water) {
+ ids.add(getCombinedId(data));
+ }
+ }
+ }
+
+ return ids;
+ }
+
+ private Block getBlock(Material material) {
+ return CraftMagicNumbers.getBlock(material);
+ }
+
+ public int getCombinedId(BlockState blockData) {
+ return Block.getId(blockData);
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/ChunkHider.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/ChunkHider.java
new file mode 100644
index 00000000..9394d1b0
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/ChunkHider.java
@@ -0,0 +1,317 @@
+/*
+ * 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 .
+ */
+
+package de.steamwar.techhider.legacy;
+
+import de.steamwar.Reflection;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import lombok.Getter;
+import net.minecraft.core.Registry;
+import net.minecraft.core.registries.BuiltInRegistries;
+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.entity.BlockEntityType;
+import org.bukkit.entity.Player;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+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;
+ }
+
+ private static final UnaryOperator chunkPacketCloner = ProtocolUtils.shallowCloneGenerator(ClientboundLevelChunkWithLightPacket.class);
+ private static final UnaryOperator chunkDataCloner = ProtocolUtils.shallowCloneGenerator(ClientboundLevelChunkPacketData.class);
+
+ private static final Reflection.Field chunkXField = Reflection.getField(ClientboundLevelChunkWithLightPacket.class, int.class, 0);
+ private static final Reflection.Field chunkZField = Reflection.getField(ClientboundLevelChunkWithLightPacket.class, int.class, 1);
+ private static final Reflection.Field chunkData = Reflection.getField(ClientboundLevelChunkWithLightPacket.class, ClientboundLevelChunkPacketData.class, 0);
+
+ private static final Reflection.Field dataField = Reflection.getField(ClientboundLevelChunkPacketData.class, byte[].class, 0);
+ private static final Reflection.Field tileEntities = Reflection.getField(ClientboundLevelChunkPacketData.class, List.class, 0);
+
+ public BiFunction 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;
+ }
+
+ packet = chunkPacketCloner.apply(packet);
+ Object dataWrapper = chunkDataCloner.apply(chunkData.get(packet));
+
+ Set hiddenBlockEntities = techHider.getHiddenBlockEntities();
+ tileEntities.set(dataWrapper, ((List>) tileEntities.get(dataWrapper)).stream().filter(te -> tileEntityVisible(hiddenBlockEntities, te)).collect(Collectors.toList()));
+
+ 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> 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 entityType = Reflection.getField(tileEntity, BlockEntityType.class, 0);
+
+ protected boolean tileEntityVisible(Set 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();
+ }
+
+ 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 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);
+ }
+ }
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/ProtocolUtils.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/ProtocolUtils.java
new file mode 100644
index 00000000..5e9a7c78
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/ProtocolUtils.java
@@ -0,0 +1,203 @@
+/*
+ * 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 .
+ */
+
+package de.steamwar.techhider.legacy;
+
+import com.google.common.primitives.Bytes;
+import de.steamwar.Reflection;
+import io.netty.buffer.ByteBuf;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.UnaryOperator;
+
+public class ProtocolUtils {
+ private ProtocolUtils() {
+ }
+
+ @Deprecated
+ public static BiFunction, Object> arrayCloneGenerator(Class> elementClass) {
+ return (array, worker) -> {
+ int length = Array.getLength(array);
+ Object result = Array.newInstance(elementClass, length);
+
+ for (int i = 0; i < length; i++) {
+ Array.set(result, i, worker.apply(Array.get(array, i)));
+ }
+
+ return result;
+ };
+ }
+
+ public static UnaryOperator shallowCloneGenerator(Class> clazz) {
+ BiConsumer filler = shallowFill(clazz);
+
+ return source -> {
+ Object clone = Reflection.newInstance(clazz);
+ filler.accept(source, clone);
+ return clone;
+ };
+ }
+
+ public static UnaryOperator shallowTypedCloneGenerator(Class clazz) {
+ BiConsumer filler = shallowFill(clazz);
+
+ return source -> {
+ Object clone = Reflection.newInstance(clazz);
+ filler.accept(source, clone);
+ return (T) clone;
+ };
+ }
+
+ private static BiConsumer shallowFill(Class> clazz) {
+ if (clazz == null) {
+ return (source, clone) -> {
+ };
+ }
+
+ BiConsumer superFiller = shallowFill(clazz.getSuperclass());
+
+ Field[] fds = clazz.getDeclaredFields();
+ List fields = new ArrayList<>();
+ for (Field field : fds) {
+ if (Modifier.isStatic(field.getModifiers())) continue;
+
+ field.setAccessible(true);
+ fields.add(field);
+ }
+
+ return (source, clone) -> {
+ superFiller.accept(source, clone);
+ try {
+ for (Field field : fields) {
+ field.set(clone, field.get(source));
+ }
+ } catch (IllegalAccessException e) {
+ throw new IllegalStateException("Could not set field", e);
+ }
+
+ };
+ }
+
+ public static int posToChunk(int c) {
+ int chunk = c / 16;
+ if (c < 0) chunk--;
+ return chunk;
+ }
+
+ @Deprecated
+ public static int readVarInt(byte[] array, int startPos) {
+ int numRead = 0;
+ int result = 0;
+ byte read;
+ do {
+ read = array[startPos + numRead];
+ int value = (read & 0b01111111);
+ result |= (value << (7 * numRead));
+
+ numRead++;
+ if (numRead > 5) {
+ break;
+ }
+ } while ((read & 0b10000000) != 0);
+
+ return result;
+ }
+
+ public static int readVarInt(ByteBuf buf) {
+ int numRead = 0;
+ int result = 0;
+ byte read;
+ do {
+ read = buf.readByte();
+ int value = (read & 0b01111111);
+ result |= (value << (7 * numRead));
+
+ if (++numRead > 5) throw new SecurityException("VarInt too long");
+ } while ((read & 0b10000000) != 0);
+
+ return result;
+ }
+
+ public static void writeVarInt(ByteBuf buf, int value) {
+ do {
+ int temp = value & 0b01111111;
+ // Note: >>> means that the sign bit is shifted with the rest of the number rather than being left alone
+ value >>>= 7;
+ if (value != 0) {
+ temp |= 0b10000000;
+ }
+ buf.writeByte(temp);
+ } while (value != 0);
+ }
+
+ @Deprecated
+ public static int readVarIntLength(byte[] array, int startPos) {
+ int numRead = 0;
+ byte read;
+ do {
+ read = array[startPos + numRead];
+ numRead++;
+ if (numRead > 5) {
+ break;
+ }
+ } while ((read & 0b10000000) != 0);
+
+ return numRead;
+ }
+
+ @Deprecated
+ public static byte[] writeVarInt(int value) {
+ List buffer = new ArrayList<>(5);
+ do {
+ byte temp = (byte) (value & 0b01111111);
+ // Note: >>> means that the sign bit is shifted with the rest of the number rather than being left alone
+ value >>>= 7;
+ if (value != 0) {
+ temp |= 0b10000000;
+ }
+ buffer.add(temp);
+ } while (value != 0);
+ return Bytes.toArray(buffer);
+ }
+
+ @Deprecated
+ public static class ChunkPos {
+ final int x;
+ final int z;
+
+ public ChunkPos(int x, int z) {
+ this.x = x;
+ this.z = z;
+ }
+
+ public final int x() {
+ return x;
+ }
+
+ public final int z() {
+ return z;
+ }
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/ProtocolWrapper.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/ProtocolWrapper.java
new file mode 100644
index 00000000..9b6ac216
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/ProtocolWrapper.java
@@ -0,0 +1,95 @@
+/*
+ * 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 .
+ */
+
+package de.steamwar.techhider.legacy;
+
+import de.steamwar.Reflection;
+import net.minecraft.core.SectionPos;
+import net.minecraft.world.level.block.entity.BlockEntityType;
+import net.minecraft.world.level.block.entity.SignBlockEntity;
+import net.minecraft.world.level.block.state.BlockState;
+import org.bukkit.entity.Player;
+
+import java.util.ArrayList;
+import java.util.function.BiFunction;
+
+public class ProtocolWrapper {
+ public static final ProtocolWrapper impl = new ProtocolWrapper();
+
+ private static final Reflection.Field multiBlockChangeChunk = Reflection.getField(TechHider.multiBlockChangePacket, SectionPos.class, 0);
+ private static final Reflection.Field multiBlockChangePos = Reflection.getField(TechHider.multiBlockChangePacket, short[].class, 0);
+ private static final Reflection.Field multiBlockChangeBlocks = Reflection.getField(TechHider.multiBlockChangePacket, BlockState[].class, 0);
+
+ public BiFunction multiBlockChangeGenerator(TechHider techHider) {
+ return (p, packet) -> {
+ TechHider.LocationEvaluator locationEvaluator = techHider.getLocationEvaluator();
+ Object chunkCoords = multiBlockChangeChunk.get(packet);
+ int chunkX = TechHider.blockPositionX.get(chunkCoords);
+ int chunkY = TechHider.blockPositionY.get(chunkCoords);
+ int chunkZ = TechHider.blockPositionZ.get(chunkCoords);
+ if (locationEvaluator.skipChunkSection(p, chunkX, chunkY, chunkZ)) {
+ return packet;
+ }
+
+ packet = TechHider.multiBlockChangeCloner.apply(packet);
+ final short[] oldPos = multiBlockChangePos.get(packet);
+ final BlockState[] oldBlocks = multiBlockChangeBlocks.get(packet);
+ ArrayList poss = new ArrayList<>(oldPos.length);
+ ArrayList 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);
+ }
+
+ multiBlockChangePos.set(packet, newPos);
+ multiBlockChangeBlocks.set(packet, blocks.toArray(new BlockState[0]));
+ return packet;
+ };
+ }
+
+ private static final Reflection.Field tileEntityType = Reflection.getField(TechHider.tileEntityDataPacket, BlockEntityType.class, 0);
+ private static final BlockEntityType> signType = Reflection.getField(BlockEntityType.class, BlockEntityType.class, 0, SignBlockEntity.class).get(null);
+
+ public boolean unfilteredTileEntityDataAction(Object packet) {
+ return tileEntityType.get(packet) != signType;
+ }
+
+ public BiFunction blockBreakHiderGenerator(Class> blockBreakPacket, TechHider techHider) {
+ return null;
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/TechHider.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/TechHider.java
new file mode 100644
index 00000000..006b50d0
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/TechHider.java
@@ -0,0 +1,181 @@
+/*
+ * 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 .
+ */
+
+package de.steamwar.techhider.legacy;
+
+import com.comphenix.tinyprotocol.TinyProtocol;
+import de.steamwar.Reflection;
+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.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.Function;
+import java.util.function.UnaryOperator;
+import java.util.stream.Collectors;
+
+public class TechHider {
+
+ public static final Class> blockPosition = BlockPos.class;
+ private static final Class> baseBlockPosition = Vec3i.class;
+ public static final Reflection.Field blockPositionX = Reflection.getField(baseBlockPosition, int.class, 0);
+ public static final Reflection.Field blockPositionY = Reflection.getField(baseBlockPosition, int.class, 1);
+ public static final Reflection.Field blockPositionZ = Reflection.getField(baseBlockPosition, int.class, 2);
+
+ 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, BiFunction> techhiders = new HashMap<>();
+ @Getter
+ private final LocationEvaluator locationEvaluator;
+ @Getter
+ private final Object obfuscationTarget;
+ @Getter
+ private final int obfuscationTargetId;
+ @Getter
+ private final Set obfuscateIds;
+ @Getter
+ private final Set hiddenBlockEntities;
+
+ public TechHider(LocationEvaluator locationEvaluator, Material obfuscationTarget, Set obfuscate, Set 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(blockActionPacket, this::blockActionHider);
+ techhiders.put(blockChangePacket, this::blockChangeHider);
+ techhiders.put(tileEntityDataPacket, this::tileEntityDataHider);
+ techhiders.put(multiBlockChangePacket, ProtocolWrapper.impl.multiBlockChangeGenerator(this));
+ techhiders.put(ChunkHider.impl.mapChunkPacket(), 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(TinyProtocol.instance::addFilter);
+ }
+
+ public void disable() {
+ techhiders.forEach(TinyProtocol.instance::removeFilter);
+ }
+
+ public static final Class> multiBlockChangePacket = ClientboundSectionBlocksUpdatePacket.class;
+ public static final UnaryOperator multiBlockChangeCloner = ProtocolUtils.shallowCloneGenerator(TechHider.multiBlockChangePacket);
+
+ private static final Class> blockChangePacket = ClientboundBlockUpdatePacket.class;
+ private static final Function blockChangeCloner = ProtocolUtils.shallowCloneGenerator(blockChangePacket);
+ private static final Reflection.Field> blockChangePosition = Reflection.getField(blockChangePacket, blockPosition, 0);
+ private static final Reflection.Field> blockChangeBlockData = Reflection.getField(blockChangePacket, iBlockData, 0);
+
+ private Object blockChangeHider(Player p, Object packet) {
+ switch (locationEvaluator.checkBlockPos(p, blockChangePosition.get(packet))) {
+ case SKIP:
+ return packet;
+ case CHECK:
+ if (!iBlockDataHidden((BlockState) blockChangeBlockData.get(packet))) {
+ return packet;
+ }
+ case HIDE:
+ packet = blockChangeCloner.apply(packet);
+ blockChangeBlockData.set(packet, obfuscationTarget);
+ return packet;
+ case HIDE_AIR:
+ default:
+ packet = blockChangeCloner.apply(packet);
+ blockChangeBlockData.set(packet, AIR);
+ return packet;
+ }
+ }
+
+ private static final Class> blockActionPacket = ClientboundBlockEventPacket.class;
+ private static final Reflection.Field> blockActionPosition = Reflection.getField(blockActionPacket, blockPosition, 0);
+
+ private Object blockActionHider(Player p, Object packet) {
+ if (locationEvaluator.checkBlockPos(p, blockActionPosition.get(packet)) == State.SKIP) {
+ return packet;
+ }
+ return null;
+ }
+
+ public static final Class> tileEntityDataPacket = ClientboundBlockEntityDataPacket.class;
+ private static final Reflection.Field> tileEntityDataPosition = Reflection.getField(tileEntityDataPacket, blockPosition, 0);
+
+ private Object tileEntityDataHider(Player p, Object packet) {
+ switch (locationEvaluator.checkBlockPos(p, tileEntityDataPosition.get(packet))) {
+ case SKIP:
+ return packet;
+ case CHECK:
+ if (ProtocolWrapper.impl.unfilteredTileEntityDataAction(packet)) {
+ 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, Object pos) {
+ return check(player, blockPositionX.get(pos), blockPositionY.get(pos), blockPositionZ.get(pos));
+ }
+
+ default boolean blockPrecise(Player player, int x, int y, int z) {
+ return false;
+ }
+ }
+}