From 915af62c6d44438e199cf693f0d979ec4a7519a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maurice=20Eisenbl=C3=A4tter?= Date: Thu, 29 Aug 2024 00:42:50 +0200 Subject: [PATCH 1/3] feat: wip - add new obfuscation engine --- .../config/WorldConfigBundle.java | 2 +- .../imprex/orebfuscator/OrebfuscatorNms.java | 4 + .../orebfuscator/nms/v1_21_R1/NmsManager.java | 4 + .../net/imprex/orebfuscator/chunk/Chunk.java | 8 +- .../orebfuscator/chunk/ChunkCapabilities.java | 42 ++++ .../orebfuscator/chunk/ChunkSection.java | 4 +- .../orebfuscator/chunk/IndirectPalette.java | 8 +- .../chunk/SingleValuePalette.java | 4 +- .../chunk/{ByteBufUtil.java => VarInt.java} | 10 +- .../imprex/orebfuscator/chunk/next/Chunk.java | 94 +++++++++ .../orebfuscator/chunk/next/ChunkSection.java | 78 ++++++++ .../orebfuscator/chunk/next/ChunkSegment.java | 95 ++++++++++ .../orebfuscator/chunk/next/ChunkWriter.java | 97 ++++++++++ .../chunk/next/bitstorage/BitStorage.java | 58 ++++++ .../next/bitstorage/EmptyBitStorage.java | 68 +++++++ .../next/bitstorage/SimpleBitStorage.java | 129 +++++++++++++ .../chunk/next/palette/DirectPalette.java | 59 ++++++ .../chunk/next/palette/IndirectPalette.java | 126 ++++++++++++ .../chunk/next/palette/Palette.java | 34 ++++ .../next/palette/SingleValuedPalette.java | 65 +++++++ .../chunk/next/strategies/BiomeStrategy.java | 40 ++++ .../chunk/next/strategies/BlockStrategy.java | 40 ++++ .../chunk/next/strategies/PaletteBuilder.java | 36 ++++ .../strategies/PalettedContainerStrategy.java | 101 ++++++++++ .../config/OrebfuscatorConfig.java | 2 +- .../obfuscation/ObfuscationProcessor.java | 2 +- .../obfuscation/ObfuscationTaskProcessor.java | 179 ++++++++++++++++++ 27 files changed, 1369 insertions(+), 20 deletions(-) rename orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/{ByteBufUtil.java => VarInt.java} (76%) create mode 100644 orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/Chunk.java create mode 100644 orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/ChunkSection.java create mode 100644 orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/ChunkSegment.java create mode 100644 orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/ChunkWriter.java create mode 100644 orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/bitstorage/BitStorage.java create mode 100644 orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/bitstorage/EmptyBitStorage.java create mode 100644 orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/bitstorage/SimpleBitStorage.java create mode 100644 orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/palette/DirectPalette.java create mode 100644 orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/palette/IndirectPalette.java create mode 100644 orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/palette/Palette.java create mode 100644 orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/palette/SingleValuedPalette.java create mode 100644 orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/BiomeStrategy.java create mode 100644 orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/BlockStrategy.java create mode 100644 orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/PaletteBuilder.java create mode 100644 orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/PalettedContainerStrategy.java create mode 100644 orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationTaskProcessor.java diff --git a/orebfuscator-common/src/main/java/net/imprex/orebfuscator/config/WorldConfigBundle.java b/orebfuscator-common/src/main/java/net/imprex/orebfuscator/config/WorldConfigBundle.java index c13cad55..f5a82eef 100644 --- a/orebfuscator-common/src/main/java/net/imprex/orebfuscator/config/WorldConfigBundle.java +++ b/orebfuscator-common/src/main/java/net/imprex/orebfuscator/config/WorldConfigBundle.java @@ -18,7 +18,7 @@ public interface WorldConfigBundle { int maxSectionIndex(); - boolean shouldObfuscate(int y); + boolean shouldProcessBlock(int y); int nextRandomObfuscationBlock(int y); diff --git a/orebfuscator-nms/orebfuscator-nms-api/src/main/java/net/imprex/orebfuscator/OrebfuscatorNms.java b/orebfuscator-nms/orebfuscator-nms-api/src/main/java/net/imprex/orebfuscator/OrebfuscatorNms.java index 2e91161c..eeaadc81 100644 --- a/orebfuscator-nms/orebfuscator-nms-api/src/main/java/net/imprex/orebfuscator/OrebfuscatorNms.java +++ b/orebfuscator-nms/orebfuscator-nms-api/src/main/java/net/imprex/orebfuscator/OrebfuscatorNms.java @@ -58,6 +58,10 @@ public static int getMaxBitsPerBlockState() { return instance.getMaxBitsPerBlockState(); } + public static int getMaxBitsPerBiome() { + throw new UnsupportedOperationException("TODO implement this"); + } + public static BlockProperties getBlockByName(String key) { return instance.getBlockByName(NamespacedKey.fromString(key)); } diff --git a/orebfuscator-nms/orebfuscator-nms-v1_21_R1/src/main/java/net/imprex/orebfuscator/nms/v1_21_R1/NmsManager.java b/orebfuscator-nms/orebfuscator-nms-v1_21_R1/src/main/java/net/imprex/orebfuscator/nms/v1_21_R1/NmsManager.java index 27493057..1d48d1c8 100644 --- a/orebfuscator-nms/orebfuscator-nms-v1_21_R1/src/main/java/net/imprex/orebfuscator/nms/v1_21_R1/NmsManager.java +++ b/orebfuscator-nms/orebfuscator-nms-v1_21_R1/src/main/java/net/imprex/orebfuscator/nms/v1_21_R1/NmsManager.java @@ -99,6 +99,10 @@ public NmsManager(Config config) { public ReadOnlyChunk getReadOnlyChunk(World world, int chunkX, int chunkZ) { ServerChunkCache serverChunkCache = level(world).getChunkSource(); LevelChunk chunk = serverChunkCache.getChunk(chunkX, chunkZ, true); + /** + * LevelChunkSection + * PalettedContainer + */ return new ReadOnlyChunkWrapper(chunk); } diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/Chunk.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/Chunk.java index d9a8030a..c1711ec8 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/Chunk.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/Chunk.java @@ -88,14 +88,14 @@ private void skipBiomePalettedContainer() { int bitsPerValue = this.inputBuffer.readUnsignedByte(); if (bitsPerValue == 0) { - ByteBufUtil.readVarInt(this.inputBuffer); + VarInt.read(this.inputBuffer); } else if (bitsPerValue <= 3) { - for (int i = ByteBufUtil.readVarInt(this.inputBuffer); i > 0; i--) { - ByteBufUtil.readVarInt(this.inputBuffer); + for (int i = VarInt.read(this.inputBuffer); i > 0; i--) { + VarInt.read(this.inputBuffer); } } - int dataLength = ByteBufUtil.readVarInt(this.inputBuffer); + int dataLength = VarInt.read(this.inputBuffer); if (SimpleVarBitBuffer.calculateArraySize(bitsPerValue, 64) != dataLength) { throw new IndexOutOfBoundsException("data.length != VarBitBuffer::size " + dataLength + " " + SimpleVarBitBuffer.calculateArraySize(bitsPerValue, 64)); diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/ChunkCapabilities.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/ChunkCapabilities.java index 68c4e6a1..26000376 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/ChunkCapabilities.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/ChunkCapabilities.java @@ -1,5 +1,8 @@ package net.imprex.orebfuscator.chunk; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import net.imprex.orebfuscator.chunk.next.bitstorage.BitStorage; import net.imprex.orebfuscator.util.MinecraftVersion; public final class ChunkCapabilities { @@ -18,9 +21,48 @@ public final class ChunkCapabilities { private static final boolean hasHeightBitMask = MinecraftVersion.isBelow("1.18"); private static final boolean hasDynamicHeight = MinecraftVersion.isAtOrAbove("1.17"); + private static final byte[] emptyBlockPalettedContainer = createEmptyBlockPalettedContainer(); + + private static byte[] createEmptyBlockPalettedContainer() { + ByteBuf buffer = Unpooled.buffer(); + + // write block count + buffer.writeShort(0); + + if (ChunkCapabilities.hasSingleValuePalette()) { + // write bitsPerBlock + palette + buffer.writeByte(0); + VarInt.write(buffer, 0); + + // write empty block data + VarInt.write(buffer, 0); + } else { + // write min allowed bitsPerBlock + final int bitsPerBlock = 4; + buffer.writeByte(bitsPerBlock); + + // write palette with air entry + VarInt.write(buffer, 1); + VarInt.write(buffer, 0); + + // write empty block data + int packetSize = BitStorage.getSerializedSize(bitsPerBlock, 4096); + VarInt.write(buffer, packetSize); + buffer.skipBytes(packetSize); + } + + return buffer + .capacity(buffer.readableBytes()) + .array(); + } + private ChunkCapabilities() { } + public static byte[] emptyBlockPalettedContainer() { + return emptyBlockPalettedContainer; + } + public static boolean hasChunkPosFieldUnloadPacket() { return hasChunkPosFieldUnloadPacket; } diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/ChunkSection.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/ChunkSection.java index f6b2873b..7eb06424 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/ChunkSection.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/ChunkSection.java @@ -97,7 +97,7 @@ public void write(ByteBuf buffer) { this.palette.write(buffer); long[] data = this.data.toArray(); - ByteBufUtil.writeVarInt(buffer, data.length); + VarInt.write(buffer, data.length); for (long entry : data) { buffer.writeLong(entry); } @@ -111,7 +111,7 @@ public int[] read(ByteBuf buffer) { this.palette.read(buffer); long[] data = this.data.toArray(); - int length = ByteBufUtil.readVarInt(buffer); + int length = VarInt.read(buffer); if (data.length != length) { throw new IndexOutOfBoundsException("data.length != VarBitBuffer::size " + length + " " + this.data); } diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/IndirectPalette.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/IndirectPalette.java index be579c71..d187bee8 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/IndirectPalette.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/IndirectPalette.java @@ -52,9 +52,9 @@ public int valueFor(int id) { @Override public void read(ByteBuf buffer) { - this.size = ByteBufUtil.readVarInt(buffer); + this.size = VarInt.read(buffer); for (int id = 0; id < size; id++) { - int value = ByteBufUtil.readVarInt(buffer); + int value = VarInt.read(buffer); this.byId[id] = value; this.byValue[value] = (byte) id; } @@ -62,10 +62,10 @@ public void read(ByteBuf buffer) { @Override public void write(ByteBuf buffer) { - ByteBufUtil.writeVarInt(buffer, this.size); + VarInt.write(buffer, this.size); for (int id = 0; id < this.size; id++) { - ByteBufUtil.writeVarInt(buffer, this.valueFor(id)); + VarInt.write(buffer, this.valueFor(id)); } } } diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/SingleValuePalette.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/SingleValuePalette.java index 1e5036af..af75caa1 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/SingleValuePalette.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/SingleValuePalette.java @@ -34,7 +34,7 @@ public int valueFor(int id) { @Override public void read(ByteBuf buffer) { - this.value = ByteBufUtil.readVarInt(buffer); + this.value = VarInt.read(buffer); } @Override @@ -42,7 +42,7 @@ public void write(ByteBuf buffer) { if (this.value == -1) { throw new IllegalStateException("value isn't initialized"); } else { - ByteBufUtil.writeVarInt(buffer, this.value); + VarInt.write(buffer, this.value); } } } diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/ByteBufUtil.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/VarInt.java similarity index 76% rename from orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/ByteBufUtil.java rename to orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/VarInt.java index 8dbd842f..14c7710e 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/ByteBufUtil.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/VarInt.java @@ -2,9 +2,9 @@ import io.netty.buffer.ByteBuf; -public class ByteBufUtil { +public class VarInt { - public static int getVarIntSize(int value) { + public static int getSerializedSize(int value) { for (int bytes = 1; bytes < 5; bytes++) { if ((value & -1 << bytes * 7) == 0) { return bytes; @@ -13,7 +13,7 @@ public static int getVarIntSize(int value) { return 5; } - public static void skipVarInt(ByteBuf buffer) { + public static void skipBytes(ByteBuf buffer) { int bytes = 0; byte in; do { @@ -24,7 +24,7 @@ public static void skipVarInt(ByteBuf buffer) { } while ((in & 0x80) != 0); } - public static int readVarInt(ByteBuf buffer) { + public static int read(ByteBuf buffer) { int out = 0; int bytes = 0; byte in; @@ -38,7 +38,7 @@ public static int readVarInt(ByteBuf buffer) { return out; } - public static void writeVarInt(ByteBuf buffer, int value) { + public static void write(ByteBuf buffer, int value) { while ((value & -0x80) != 0) { buffer.writeByte(value & 0x7F | 0x80); value >>>= 7; diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/Chunk.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/Chunk.java new file mode 100644 index 00000000..072f06dc --- /dev/null +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/Chunk.java @@ -0,0 +1,94 @@ +package net.imprex.orebfuscator.chunk.next; + +import java.util.Arrays; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.PooledByteBufAllocator; +import io.netty.buffer.Unpooled; +import net.imprex.orebfuscator.chunk.ChunkStruct; +import net.imprex.orebfuscator.chunk.next.ChunkSegment.ProcessingChunkSegment; +import net.imprex.orebfuscator.chunk.next.ChunkSegment.ReadOnlyChunkSegment; +import net.imprex.orebfuscator.chunk.next.ChunkSegment.SkippedChunkSegment; +import net.imprex.orebfuscator.config.WorldConfigBundle; +import net.imprex.orebfuscator.util.HeightAccessor; + +public class Chunk implements AutoCloseable { + + public static Chunk fromChunkStruct(ChunkStruct chunkStruct, WorldConfigBundle bundle) { + return new Chunk(chunkStruct, bundle); + } + + private final int chunkX; + private final int chunkZ; + + private final HeightAccessor heightAccessor; + private final ChunkSegment[] sections; + + private final ByteBuf inputBuffer; + private final ByteBuf outputBuffer; + + private final ChunkWriter writer; + + private Chunk(ChunkStruct chunkStruct, WorldConfigBundle bundle) { + this.chunkX = chunkStruct.chunkX; + this.chunkZ = chunkStruct.chunkZ; + + this.heightAccessor = HeightAccessor.get(chunkStruct.world); + this.sections = new ChunkSegment[this.heightAccessor.getSectionCount()]; + + this.inputBuffer = Unpooled.wrappedBuffer(chunkStruct.data); + this.outputBuffer = PooledByteBufAllocator.DEFAULT.heapBuffer(chunkStruct.data.length); + + for (int sectionIndex = 0; sectionIndex < this.sections.length; sectionIndex++) { + if (chunkStruct.sectionMask.get(sectionIndex)) { + ChunkSegment sectionHolder; + + if (bundle.skipReadSectionIndex(sectionIndex)) { + sectionHolder = new SkippedChunkSegment(ChunkSection.skip(inputBuffer)); + } else { + ChunkSection chunkSection = new ChunkSection(inputBuffer); + if (bundle.skipProcessingSectionIndex(sectionIndex)) { + sectionHolder = new ReadOnlyChunkSegment(chunkSection); + } else { + sectionHolder = new ProcessingChunkSegment(chunkSection); + } + } + + this.sections[sectionIndex] = sectionHolder; + } + } + + this.writer = new ChunkWriter(this.outputBuffer, this.sections); + } + + public ChunkWriter getWriter() { + return writer; + } + + public HeightAccessor getHeightAccessor() { + return heightAccessor; + } + + public int getBlockState(int x, int y, int z) { + if (x >> 4 == this.chunkX && z >> 4 == this.chunkZ) { + ChunkSegment segment = this.sections[this.heightAccessor.getSectionIndex(y)]; + if (segment != null && segment.hasSection()) { + return segment.getSection().getBlockState(x, y, z); + } + return 0; + } + return -1; + } + + public byte[] finalizeOutput() { + this.outputBuffer.writeBytes(this.inputBuffer); + return Arrays.copyOfRange(this.outputBuffer.array(), this.outputBuffer.arrayOffset(), + this.outputBuffer.arrayOffset() + this.outputBuffer.readableBytes()); + } + + @Override + public void close() throws Exception { + this.inputBuffer.release(); + this.outputBuffer.release(); + } +} diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/ChunkSection.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/ChunkSection.java new file mode 100644 index 00000000..0da0ab18 --- /dev/null +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/ChunkSection.java @@ -0,0 +1,78 @@ +package net.imprex.orebfuscator.chunk.next; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import net.imprex.orebfuscator.chunk.ChunkCapabilities; +import net.imprex.orebfuscator.chunk.next.bitstorage.BitStorage; +import net.imprex.orebfuscator.chunk.next.palette.Palette; +import net.imprex.orebfuscator.chunk.next.strategies.BiomeStrategy; +import net.imprex.orebfuscator.chunk.next.strategies.BlockStrategy; + +public class ChunkSection { + + static ByteBuf skip(ByteBuf buffer) { + int readerIndex = buffer.readerIndex(); + + buffer.skipBytes(2); // skip block count + + int bitsPerBlock = buffer.readUnsignedByte(); + BlockStrategy.skipPalette(buffer, bitsPerBlock); + BlockStrategy.skipBitStorage(buffer, bitsPerBlock); + + if (ChunkCapabilities.hasBiomePalettedContainer()) { + int bitsPerBiome = buffer.readUnsignedByte(); + BiomeStrategy.skipPalette(buffer, bitsPerBiome); + BiomeStrategy.skipBitStorage(buffer, bitsPerBiome); + } + + int sectionLength = buffer.readerIndex() - readerIndex; + return buffer.slice(readerIndex, sectionLength); + } + + public final int blockCount; + public final int bitsPerBlock; + + public final Palette blockPalette; + public final BitStorage blockBitStorage; + + public final ByteBuf biomeSlice; + public final ByteBuf sectionSlice; + + public ChunkSection(ByteBuf buffer) { + int startIndex = buffer.readerIndex(); + + this.blockCount = buffer.readShort(); + + this.bitsPerBlock = buffer.readUnsignedByte(); + this.blockPalette = BlockStrategy.readPalette(buffer, this.bitsPerBlock); + this.blockBitStorage = BlockStrategy.readBitStorage(buffer, this.blockPalette); + + if (ChunkCapabilities.hasBiomePalettedContainer()) { + int suffixOffset = buffer.readerIndex(); + + int bitsPerBiome = buffer.readUnsignedByte(); + BiomeStrategy.skipPalette(buffer, bitsPerBiome); + BiomeStrategy.skipBitStorage(buffer, bitsPerBiome); + + int suffixLength = buffer.readerIndex() - suffixOffset; + this.biomeSlice = buffer.slice(suffixOffset, suffixLength); + } else { + this.biomeSlice = Unpooled.EMPTY_BUFFER; + } + + int sectionLength = buffer.readerIndex() - startIndex; + this.sectionSlice = buffer.slice(startIndex, sectionLength); + } + + public boolean isEmpty() { + return this.blockCount == 0; + } + + public int getBlockState(int x, int y, int z) { + return getBlockState(BlockStrategy.getIndex(x, y, z)); + } + + public int getBlockState(int index) { + return this.blockBitStorage.get(index); + } +} diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/ChunkSegment.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/ChunkSegment.java new file mode 100644 index 00000000..d08d5012 --- /dev/null +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/ChunkSegment.java @@ -0,0 +1,95 @@ +package net.imprex.orebfuscator.chunk.next; + +import io.netty.buffer.ByteBuf; + +public interface ChunkSegment { + + boolean needsProcessing(); + + boolean hasSection(); + + default ByteBuf getBuffer() { + throw new UnsupportedOperationException(); + } + + default ChunkSection getSection() { + throw new UnsupportedOperationException(); + } + + public static class SkippedChunkSegment implements ChunkSegment { + + private final ByteBuf buffer; + + public SkippedChunkSegment(ByteBuf buffer) { + this.buffer = buffer; + } + + @Override + public boolean needsProcessing() { + return false; + } + + @Override + public boolean hasSection() { + return false; + } + + @Override + public ByteBuf getBuffer() { + return this.buffer; + } + } + + public static class ReadOnlyChunkSegment implements ChunkSegment { + + private final ChunkSection section; + + public ReadOnlyChunkSegment(ChunkSection section) { + this.section = section; + } + + @Override + public boolean needsProcessing() { + return false; + } + + @Override + public boolean hasSection() { + return true; + } + + @Override + public ByteBuf getBuffer() { + return this.section.sectionSlice; + } + + @Override + public ChunkSection getSection() { + return this.section; + } + } + + public static class ProcessingChunkSegment implements ChunkSegment { + + private final ChunkSection section; + + public ProcessingChunkSegment(ChunkSection section) { + this.section = section; + } + + @Override + public boolean needsProcessing() { + return true; + } + + @Override + public boolean hasSection() { + return true; + } + + @Override + public ChunkSection getSection() { + return this.section; + } + } +} diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/ChunkWriter.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/ChunkWriter.java new file mode 100644 index 00000000..4849fa15 --- /dev/null +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/ChunkWriter.java @@ -0,0 +1,97 @@ +package net.imprex.orebfuscator.chunk.next; + +import io.netty.buffer.ByteBuf; +import net.imprex.orebfuscator.chunk.ChunkCapabilities; +import net.imprex.orebfuscator.chunk.next.bitstorage.BitStorage; +import net.imprex.orebfuscator.chunk.next.palette.Palette; +import net.imprex.orebfuscator.chunk.next.strategies.BlockStrategy; + +public class ChunkWriter { + + private static int getSerializedSize(Palette palette, int suffixLength) { + return 3 // blockCount + bitsPerBlock + + palette.getSerializedSize() + + BitStorage.getSerializedSize(palette.getBitsPerEntry(), BlockStrategy.size()) + + suffixLength; + } + + private final ByteBuf outputBuffer; + private final ChunkSegment[] sections; + + private int nextSectionIndex = 0; + + public ChunkWriter(ByteBuf outputBuffer, ChunkSegment[] sections) { + this.outputBuffer = outputBuffer; + this.sections = sections; + } + + public boolean hasNext() { + return this.nextSectionIndex < this.sections.length; + } + + public int getSectionIndex() { + return this.nextSectionIndex - 1; + } + + public ChunkSection getOrWriteNext() { + ChunkSegment sectionHolder = this.sections[this.nextSectionIndex++]; + // skip non-present chunks (e.g. heightMask) + if (sectionHolder == null) { + return null; + } + + // write section buffer if present and skip processing + if (!sectionHolder.needsProcessing()) { + this.outputBuffer.writeBytes(sectionHolder.getBuffer()); + return null; + } + + // always write empty sections without processing + ChunkSection chunkSection = sectionHolder.getSection(); + if (chunkSection != null && chunkSection.isEmpty()) { + this.writeEmpty(chunkSection.biomeSlice); + return null; + } + + return chunkSection; + } + + public void writeSection(Palette palette, int blockCount, int[] blockStates, ByteBuf suffix) { + // skip write if block count is zero + if (blockCount == 0) { + writeEmpty(suffix); + return; + } + + // make sure buffer has enough space + int serializedSize = getSerializedSize(palette, suffix.readableBytes()); + this.outputBuffer.ensureWritable(serializedSize); + + // write block count + this.outputBuffer.writeShort(blockCount); + + // write bitsPerBlock + palette + this.outputBuffer.writeByte(palette.getBitsPerEntry()); + palette.write(this.outputBuffer); + + // write block data + BitStorage.Writer blockWriter = BlockStrategy.createBitStorageWriter(this.outputBuffer, palette); + if (palette.getBitsPerEntry() > 0) { + for (int i = 0; i < blockStates.length; i++) { + blockWriter.write(blockStates[i]); + } + blockWriter.throwIfNotExhausted(); + } + + // append remaining suffix + this.outputBuffer.writeBytes(suffix); + } + + private void writeEmpty(ByteBuf suffix) { + // write default empty section + this.outputBuffer.writeBytes(ChunkCapabilities.emptyBlockPalettedContainer()); + + // append remaining suffix + this.outputBuffer.writeBytes(suffix); + } +} diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/bitstorage/BitStorage.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/bitstorage/BitStorage.java new file mode 100644 index 00000000..27f90da6 --- /dev/null +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/bitstorage/BitStorage.java @@ -0,0 +1,58 @@ +package net.imprex.orebfuscator.chunk.next.bitstorage; + +import io.netty.buffer.ByteBuf; +import net.imprex.orebfuscator.chunk.ChunkCapabilities; +import net.imprex.orebfuscator.chunk.next.palette.Palette; + +public interface BitStorage { + + static int getSerializedSize(int bitsPerEntry, int size) { + if (ChunkCapabilities.hasSingleValuePalette() && bitsPerEntry == 0) { + return EmptyBitStorage.getSerializedSize(); + } else { + return SimpleBitStorage.getSerializedSize(bitsPerEntry, size); + } + } + + static void skipBytes(ByteBuf buffer, int bitsPerEntry) { + if (ChunkCapabilities.hasSingleValuePalette() && bitsPerEntry == 0) { + EmptyBitStorage.skipBytes(buffer); + } else { + SimpleBitStorage.skipBytes(buffer); + } + } + + static BitStorage read(ByteBuf buffer, int size, Palette palette) { + if (ChunkCapabilities.hasSingleValuePalette() && palette.getBitsPerEntry() == 0) { + return EmptyBitStorage.read(buffer, size, palette); + } else { + return SimpleBitStorage.read(buffer, size, palette); + } + } + + static Writer createWriter(ByteBuf buffer, int size, Palette palette) { + if (ChunkCapabilities.hasSingleValuePalette() && palette.getBitsPerEntry() == 0) { + return EmptyBitStorage.createWriter(buffer); + } else { + return SimpleBitStorage.createWriter(buffer, size, palette); + } + } + + int get(int index); + + int size(); + + interface Writer { + + default void throwIfNotExhausted() { + if (!isExhausted()) { + throw new IllegalStateException("BitStorage.Writer is not exhausted but closed!"); + } + } + + boolean isExhausted(); + + void write(int value); + + } +} diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/bitstorage/EmptyBitStorage.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/bitstorage/EmptyBitStorage.java new file mode 100644 index 00000000..21732061 --- /dev/null +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/bitstorage/EmptyBitStorage.java @@ -0,0 +1,68 @@ +package net.imprex.orebfuscator.chunk.next.bitstorage; + +import io.netty.buffer.ByteBuf; +import net.imprex.orebfuscator.chunk.VarInt; +import net.imprex.orebfuscator.chunk.next.palette.Palette; + +public class EmptyBitStorage implements BitStorage { + + public static int getSerializedSize() { + return 1; + } + + public static void skipBytes(ByteBuf buffer) { + VarInt.skipBytes(buffer); + } + + public static BitStorage read(ByteBuf buffer, int size, Palette palette) { + if (palette.getBitsPerEntry() != 0) { + throw new IllegalArgumentException("invalid palette!"); + } + + // skip length field + VarInt.skipBytes(buffer); + + return new EmptyBitStorage(size, palette.valueAtIndex(0)); + } + + public static Writer createWriter(ByteBuf buffer) { + VarInt.write(buffer, 0); + return Writer.INSTANCE; + } + + private final int size; + private final int value; + + public EmptyBitStorage(int size, int value) { + this.size = size; + this.value = value; + } + + @Override + public int get(int index) { + return this.value; + } + + @Override + public int size() { + return this.size; + } + + private static class Writer implements BitStorage.Writer { + + private static final Writer INSTANCE = new Writer(); + + private Writer() { + } + + @Override + public boolean isExhausted() { + return true; + } + + @Override + public void write(int value) { + throw new UnsupportedOperationException("Can't write to EmptyBitStorage!"); + } + } +} diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/bitstorage/SimpleBitStorage.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/bitstorage/SimpleBitStorage.java new file mode 100644 index 00000000..d9a28394 --- /dev/null +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/bitstorage/SimpleBitStorage.java @@ -0,0 +1,129 @@ +package net.imprex.orebfuscator.chunk.next.bitstorage; + +import io.netty.buffer.ByteBuf; +import net.imprex.orebfuscator.chunk.VarInt; +import net.imprex.orebfuscator.chunk.next.palette.Palette; + +public class SimpleBitStorage implements BitStorage { + + public static int getSerializedSize(int bitsPerEntry, int size) { + int arrayLength = getArrayLength(size, bitsPerEntry); + return VarInt.getSerializedSize(arrayLength) + (arrayLength * Long.BYTES); + } + + public static void skipBytes(ByteBuf buffer) { + int length = VarInt.read(buffer); + buffer.skipBytes(length * Long.BYTES); + } + + public static BitStorage read(ByteBuf buffer, int size, Palette palette) { + int bitsPerEntry = palette.getBitsPerEntry(); + int expectedLength = getArrayLength(size, bitsPerEntry); + + int length = VarInt.read(buffer); + if (length != expectedLength) { + // fix: FAWE chunk format incompatibility with bitsPerEntry == 1 + // https://github.com/Imprex-Development/Orebfuscator/issues/36 + buffer.skipBytes(Long.BYTES * length); + return new EmptyBitStorage(size, palette.valueAtIndex(0)); + } + + int[] values = new int[size]; + int valueMask = (1 << palette.getBitsPerEntry()) - 1; + + int bitOffset = 0; + long longBuffer = buffer.readLong(); + + for (int index = 0; index < values.length; index++) { + if (bitOffset + bitsPerEntry > 64) { + longBuffer = buffer.readLong(); + bitOffset = 0; + } + + int paletteIndex = (int) (longBuffer >>> bitOffset) & valueMask; + values[index] = palette.valueAtIndex(paletteIndex); + + bitOffset += bitsPerEntry; + } + + return new SimpleBitStorage(values); + } + + public static Writer createWriter(ByteBuf buffer, int size, Palette palette) { + return new Writer(buffer, size, palette); + } + + private static int getArrayLength(int size, int bitsPerEntry) { + int valuesPerLong = 64 / bitsPerEntry; + return (size + valuesPerLong - 1) / valuesPerLong; + } + + private final int[] values; + + private SimpleBitStorage(int[] values) { + this.values = values; + } + + @Override + public int get(int index) { + return this.values[index]; + } + + @Override + public int size() { + return this.values.length; + } + + public static class Writer implements BitStorage.Writer { + + private final int bitsPerEntry; + private final long valueMask; + + private final ByteBuf buffer; + private final Palette palette; + + private int bitOffset = 0; + private long longBuffer = 0; + + private int capacity; + + public Writer(ByteBuf buffer, int size, Palette palette) { + this.bitsPerEntry = palette.getBitsPerEntry(); + this.valueMask = (1L << this.bitsPerEntry) - 1L; + + this.capacity = size; + + this.buffer = buffer; + this.palette = palette; + + VarInt.write(buffer, getArrayLength(size, this.bitsPerEntry)); + } + + @Override + public boolean isExhausted() { + return this.capacity == 0; + } + + @Override + public void write(int value) { + if (this.capacity <= 0) { + throw new IllegalStateException("BitStorage.Writer is already exhausted"); + } + + if (this.bitOffset + this.bitsPerEntry > 64) { + this.buffer.writeLong(this.longBuffer); + this.longBuffer = 0; + this.bitOffset = 0; + } + + value = this.palette.indexForValue(value); + longBuffer |= (value & this.valueMask) << this.bitOffset; + + this.bitOffset += this.bitsPerEntry; + + if (--this.capacity == 0) { + this.buffer.writeLong(this.longBuffer); + } + } + } +} diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/palette/DirectPalette.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/palette/DirectPalette.java new file mode 100644 index 00000000..98ec04f8 --- /dev/null +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/palette/DirectPalette.java @@ -0,0 +1,59 @@ +package net.imprex.orebfuscator.chunk.next.palette; + +import java.util.List; + +import io.netty.buffer.ByteBuf; + +public class DirectPalette implements Palette { + + public static final Factory createFactory(int maxBitsPerEntry) { + DirectPalette palette = new DirectPalette(maxBitsPerEntry); + + return new Factory() { + + @Override + public void skipBytes(ByteBuf buffer) { + } + + @Override + public Palette read(ByteBuf buffer, int bitsPerEntry) { + return palette; + } + + @Override + public Palette create(int bitsPerEntry, List values) { + return palette; + } + }; + } + + private final int bitsPerEntry; + + public DirectPalette(int maxBitsPerEntry) { + this.bitsPerEntry = maxBitsPerEntry; + } + + @Override + public int getBitsPerEntry() { + return this.bitsPerEntry; + } + + @Override + public int valueAtIndex(int index) { + return index; + } + + @Override + public int indexForValue(int value) { + return value; + } + + @Override + public void write(ByteBuf buffer) { + } + + @Override + public int getSerializedSize() { + return 0; + } +} diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/palette/IndirectPalette.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/palette/IndirectPalette.java new file mode 100644 index 00000000..cdf8ab59 --- /dev/null +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/palette/IndirectPalette.java @@ -0,0 +1,126 @@ +package net.imprex.orebfuscator.chunk.next.palette; + +import java.util.BitSet; +import java.util.List; + +import io.netty.buffer.ByteBuf; +import net.imprex.orebfuscator.chunk.VarInt; + +public class IndirectPalette implements Palette { + + public static final Factory FACTORY = new Factory() { + + @Override + public void skipBytes(ByteBuf buffer) { + int size = VarInt.read(buffer); + for (int i = 0; i < size; i++) { + VarInt.skipBytes(buffer); + } + } + + @Override + public Palette read(ByteBuf buffer, int bitsPerEntry) { + int size = VarInt.read(buffer); + if (size > (1 << bitsPerEntry)) { + throw new IllegalStateException( + String.format("indirect palette is too big (got=%d < expected=%d)", size, (1 << bitsPerEntry))); + } + + int[] byIndex = new int[size]; + int maxValue = 0; + + for (int index = 0; index < size; index++) { + int value = VarInt.read(buffer); + byIndex[index] = value; + + if (value > maxValue) { + maxValue = value; + } + } + + return new IndirectPalette(bitsPerEntry, byIndex, maxValue); + } + + @Override + public Palette create(int bitsPerEntry, List values) { + int[] byIndex = new int[values.size()]; + int maxValue = 0; + + for (int index = 0; index < values.size(); index++) { + int value = values.get(index); + byIndex[index] = value; + + if (value > maxValue) { + maxValue = value; + } + } + + return new IndirectPalette(bitsPerEntry, byIndex, maxValue); + } + }; + + private final int bitsPerEntry; + private final int size; + + private final BitSet byValuePresent; + private final byte[] byValue; + private final int[] byIndex; + + public IndirectPalette(int bitsPerEntry, int[] byIndex, int maxValue) { + this.size = byIndex.length; + this.bitsPerEntry = bitsPerEntry; + + this.byValuePresent = new BitSet(maxValue); + this.byValue = new byte[maxValue]; + + this.byIndex = byIndex; + + for (int index = 0; index < byIndex.length; index++) { + int value = byIndex[index]; + this.byValue[value] = (byte) index; + this.byValuePresent.set(value); + } + } + + @Override + public int getBitsPerEntry() { + return this.bitsPerEntry; + } + + @Override + public int getSerializedSize() { + int serializedSize = VarInt.getSerializedSize(this.size); + + for (int index = 0; index < this.size; index++) { + serializedSize += VarInt.getSerializedSize(this.byIndex[index]); + } + + return serializedSize; + } + + @Override + public int valueAtIndex(int index) { + if (index < 0 || index >= this.byIndex.length) { + throw new IndexOutOfBoundsException(); + } else { + return this.byIndex[index]; + } + } + + @Override + public int indexForValue(int value) { + if (!this.byValuePresent.get(value)) { + throw new IllegalArgumentException("value=" + value); + } + return this.byValue[value] & 0xFF; + } + + @Override + public void write(ByteBuf buffer) { + VarInt.write(buffer, this.size); + + for (int index = 0; index < this.size; index++) { + VarInt.write(buffer, this.byIndex[index]); + } + } +} diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/palette/Palette.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/palette/Palette.java new file mode 100644 index 00000000..d1e15e26 --- /dev/null +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/palette/Palette.java @@ -0,0 +1,34 @@ +package net.imprex.orebfuscator.chunk.next.palette; + +import java.util.List; + +import io.netty.buffer.ByteBuf; + +public interface Palette { + + int getBitsPerEntry(); + + int getSerializedSize(); + + int valueAtIndex(int index); + + int indexForValue(int value); + + void write(ByteBuf buffer); + + public interface Factory { + + void skipBytes(ByteBuf buffer); + + Palette read(ByteBuf buffer, int bitsPerEntry); + + Palette create(int bitsPerEntry, List values ); + } + + public interface Builder { + + Builder add(int value); + + Palette build(); + } +} diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/palette/SingleValuedPalette.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/palette/SingleValuedPalette.java new file mode 100644 index 00000000..a0915cde --- /dev/null +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/palette/SingleValuedPalette.java @@ -0,0 +1,65 @@ +package net.imprex.orebfuscator.chunk.next.palette; + +import java.util.List; + +import io.netty.buffer.ByteBuf; +import net.imprex.orebfuscator.chunk.VarInt; + +public class SingleValuedPalette implements Palette { + + public static final Factory FACTORY = new Factory() { + + @Override + public void skipBytes(ByteBuf buffer) { + VarInt.skipBytes(buffer); + } + + @Override + public Palette read(ByteBuf buffer, int bitsPerEntry) { + int value = VarInt.read(buffer); + return new SingleValuedPalette(value); + } + + @Override + public Palette create(int bitsPerEntry, List values) { + return new SingleValuedPalette(values.get(0)); + } + }; + + private final int value; + + public SingleValuedPalette(int value) { + this.value = value; + } + + @Override + public int getBitsPerEntry() { + return 0; + } + + @Override + public int getSerializedSize() { + return VarInt.getSerializedSize(this.value); + } + + @Override + public int valueAtIndex(int index) { + if (index != 0) { + throw new IllegalArgumentException("Invalid index, single valued palette only has one entry!"); + } + return this.value; + } + + @Override + public int indexForValue(int value) { + if (value != this.value) { + throw new IllegalArgumentException("Invalid value, single valued palette only has one entry!"); + } + return 0; + } + + @Override + public void write(ByteBuf buffer) { + VarInt.write(buffer, this.value); + } +} diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/BiomeStrategy.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/BiomeStrategy.java new file mode 100644 index 00000000..1f9b9c23 --- /dev/null +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/BiomeStrategy.java @@ -0,0 +1,40 @@ +package net.imprex.orebfuscator.chunk.next.strategies; + +import io.netty.buffer.ByteBuf; +import net.imprex.orebfuscator.chunk.next.bitstorage.BitStorage; +import net.imprex.orebfuscator.chunk.next.palette.Palette; + +public class BiomeStrategy { + + public static int size() { + return PalettedContainerStrategy.BIOMES.size(); + } + + public static int getIndex(int x, int y, int z) { + return PalettedContainerStrategy.BIOMES.getIndex(x, y, z); + } + + public static void skipPalette(ByteBuf buffer, int bits) { + PalettedContainerStrategy.BIOMES.getConfiguration(bits).skipBytes(buffer); + } + + public static Palette readPalette(ByteBuf buffer, int bits) { + return PalettedContainerStrategy.BIOMES.getConfiguration(bits).read(buffer); + } + + public static Palette.Builder createPaletteBuilder() { + return PalettedContainerStrategy.BIOMES.createBuilder(); + } + + public static void skipBitStorage(ByteBuf buffer, int bits) { + BitStorage.skipBytes(buffer, bits); + } + + public static BitStorage readBitStorage(ByteBuf buffer, Palette palette) { + return BitStorage.read(buffer, PalettedContainerStrategy.BIOMES.size(), palette); + } + + public static BitStorage.Writer createBitStorageWriter(ByteBuf buffer, Palette palette) { + return BitStorage.createWriter(buffer, PalettedContainerStrategy.BIOMES.size(), palette); + } +} diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/BlockStrategy.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/BlockStrategy.java new file mode 100644 index 00000000..87651c65 --- /dev/null +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/BlockStrategy.java @@ -0,0 +1,40 @@ +package net.imprex.orebfuscator.chunk.next.strategies; + +import io.netty.buffer.ByteBuf; +import net.imprex.orebfuscator.chunk.next.bitstorage.BitStorage; +import net.imprex.orebfuscator.chunk.next.palette.Palette; + +public class BlockStrategy { + + public static int size() { + return PalettedContainerStrategy.BLOCK_STATES.size(); + } + + public static int getIndex(int x, int y, int z) { + return PalettedContainerStrategy.BLOCK_STATES.getIndex(x, y, z); + } + + public static void skipPalette(ByteBuf buffer, int bits) { + PalettedContainerStrategy.BLOCK_STATES.getConfiguration(bits).skipBytes(buffer); + } + + public static Palette readPalette(ByteBuf buffer, int bits) { + return PalettedContainerStrategy.BLOCK_STATES.getConfiguration(bits).read(buffer); + } + + public static Palette.Builder createPaletteBuilder() { + return PalettedContainerStrategy.BLOCK_STATES.createBuilder(); + } + + public static void skipBitStorage(ByteBuf buffer, int bits) { + BitStorage.skipBytes(buffer, bits); + } + + public static BitStorage readBitStorage(ByteBuf buffer, Palette palette) { + return BitStorage.read(buffer, PalettedContainerStrategy.BIOMES.size(), palette); + } + + public static BitStorage.Writer createBitStorageWriter(ByteBuf buffer, Palette palette) { + return BitStorage.createWriter(buffer, PalettedContainerStrategy.BIOMES.size(), palette); + } +} diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/PaletteBuilder.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/PaletteBuilder.java new file mode 100644 index 00000000..8524e4f9 --- /dev/null +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/PaletteBuilder.java @@ -0,0 +1,36 @@ +package net.imprex.orebfuscator.chunk.next.strategies; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; + +import net.imprex.orebfuscator.chunk.next.palette.Palette; + +public class PaletteBuilder implements Palette.Builder { + + private final PalettedContainerStrategy strategy; + + private final BitSet byValue; + private final List byIndex; + + public PaletteBuilder(PalettedContainerStrategy strategy) { + this.strategy = strategy; + + this.byValue = new BitSet(); + this.byIndex = new ArrayList<>(); + } + + @Override + public PaletteBuilder add(int value) { + if (!this.byValue.get(value)) { + this.byValue.set(value); + this.byIndex.add(value); + } + return this; + } + + @Override + public Palette build() { + return this.strategy.createPalette(this.byIndex); + } +} diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/PalettedContainerStrategy.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/PalettedContainerStrategy.java new file mode 100644 index 00000000..4b3debb8 --- /dev/null +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/PalettedContainerStrategy.java @@ -0,0 +1,101 @@ +package net.imprex.orebfuscator.chunk.next.strategies; + +import java.util.List; +import java.util.function.IntFunction; + +import io.netty.buffer.ByteBuf; +import net.imprex.orebfuscator.OrebfuscatorNms; +import net.imprex.orebfuscator.chunk.ChunkCapabilities; +import net.imprex.orebfuscator.chunk.next.palette.DirectPalette; +import net.imprex.orebfuscator.chunk.next.palette.IndirectPalette; +import net.imprex.orebfuscator.chunk.next.palette.Palette; +import net.imprex.orebfuscator.chunk.next.palette.SingleValuedPalette; +import net.imprex.orebfuscator.util.MathUtil; + +public class PalettedContainerStrategy { + + public static final PalettedContainerStrategy BLOCK_STATES = new PalettedContainerStrategy(4, OrebfuscatorNms.getMaxBitsPerBlockState(), + bits -> { + if (ChunkCapabilities.hasSingleValuePalette() && bits == 0) { + return Configuration.singleValued(); + } else if (bits < 9) { + return Configuration.indirect(Math.max(4, bits)); + } else { + return Configuration.direct(OrebfuscatorNms.getMaxBitsPerBlockState()); + } + }); + + public static final PalettedContainerStrategy BIOMES = new PalettedContainerStrategy(2, OrebfuscatorNms.getMaxBitsPerBiome(), + bits -> { + if (ChunkCapabilities.hasSingleValuePalette() && bits == 0) { + return Configuration.singleValued(); + } else if (bits < 4) { + return Configuration.indirect(bits); + } else { + return Configuration.direct(OrebfuscatorNms.getMaxBitsPerBiome()); + } + }); + + private final int sizeBits; + private final Configuration[] configurations; + + private PalettedContainerStrategy(int sizeBits, int maxBitsPerEntry, IntFunction configure) { + this.sizeBits = sizeBits; + + this.configurations = new Configuration[maxBitsPerEntry + 1]; + for (int bits = 0; bits <= maxBitsPerEntry; bits++) { + this.configurations[bits] = configure.apply(bits); + } + } + + public int size() { + return 1 << (this.sizeBits * 3); + } + + public int getIndex(int x, int y, int z) { + int mask = (1 << this.sizeBits) - 1; + return ((y & mask) << this.sizeBits | (z & mask)) << this.sizeBits | (x & mask); + } + + public Palette.Builder createBuilder() { + return new PaletteBuilder(this); + } + + Palette createPalette(List values) { + int bits = MathUtil.ceilLog2(values.size()); + return getConfiguration(bits).createPalette(values); + } + + public Configuration getConfiguration(int bits) { + return bits > this.configurations.length + ? this.configurations[this.configurations.length - 1] + : this.configurations[bits]; + } + + public static record Configuration(Palette.Factory factory, int bits) { + + private static Configuration singleValued() { + return new Configuration(SingleValuedPalette.FACTORY, 0); + } + + private static Configuration indirect(int bits) { + return new Configuration(IndirectPalette.FACTORY, bits); + } + + private static Configuration direct(int maxBitsPerEntry) { + return new Configuration(DirectPalette.createFactory(maxBitsPerEntry), maxBitsPerEntry); + } + + public void skipBytes(ByteBuf buffer) { + this.factory.skipBytes(buffer); + } + + public Palette read(ByteBuf buffer) { + return this.factory.read(buffer, this.bits); + } + + private Palette createPalette(List values) { + return this.factory.create(this.bits, values); + } + } +} diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/config/OrebfuscatorConfig.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/config/OrebfuscatorConfig.java index 94b7fb14..3f0418e2 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/config/OrebfuscatorConfig.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/config/OrebfuscatorConfig.java @@ -363,7 +363,7 @@ public int maxSectionIndex() { } @Override - public boolean shouldObfuscate(int y) { + public boolean shouldProcessBlock(int y) { return y >= this.minY && y <= this.maxY; } diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationProcessor.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationProcessor.java index 2b761fd7..3b799137 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationProcessor.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationProcessor.java @@ -60,7 +60,7 @@ public void process(ObfuscationTask task) { final int baseY = heightAccessor.getMinBuildHeight() + (sectionIndex << 4); for (int index = 0; index < 4096; index++) { int y = baseY + (index >> 8 & 15); - if (!bundle.shouldObfuscate(y)) { + if (!bundle.shouldProcessBlock(y)) { continue; } diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationTaskProcessor.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationTaskProcessor.java new file mode 100644 index 00000000..eda475de --- /dev/null +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationTaskProcessor.java @@ -0,0 +1,179 @@ +package net.imprex.orebfuscator.obfuscation; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import org.bukkit.Bukkit; +import org.bukkit.World; + +import net.imprex.orebfuscator.Orebfuscator; +import net.imprex.orebfuscator.OrebfuscatorNms; +import net.imprex.orebfuscator.chunk.ChunkStruct; +import net.imprex.orebfuscator.chunk.next.Chunk; +import net.imprex.orebfuscator.chunk.next.ChunkSection; +import net.imprex.orebfuscator.chunk.next.ChunkWriter; +import net.imprex.orebfuscator.chunk.next.palette.Palette; +import net.imprex.orebfuscator.chunk.next.strategies.BlockStrategy; +import net.imprex.orebfuscator.config.BlockFlags; +import net.imprex.orebfuscator.config.ObfuscationConfig; +import net.imprex.orebfuscator.config.OrebfuscatorConfig; +import net.imprex.orebfuscator.config.ProximityConfig; +import net.imprex.orebfuscator.config.WorldConfigBundle; +import net.imprex.orebfuscator.util.BlockPos; +import net.imprex.orebfuscator.util.HeightAccessor; + +public class ObfuscationTaskProcessor { + + private static final AtomicLong TIME = new AtomicLong(); + private static final AtomicInteger COUNT = new AtomicInteger(-2500); + + private final OrebfuscatorConfig config; + + public ObfuscationTaskProcessor(Orebfuscator orebfuscator) { + this.config = orebfuscator.getOrebfuscatorConfig(); + } + + public void process(ObfuscationTask task) { + long time = System.currentTimeMillis(); + + ChunkStruct chunkStruct = task.getChunkStruct(); + + World world = chunkStruct.world; + HeightAccessor heightAccessor = HeightAccessor.get(world); + + WorldConfigBundle bundle = this.config.world(world); + BlockFlags blockFlags = bundle.blockFlags(); + ObfuscationConfig obfuscationConfig = bundle.obfuscation(); + ProximityConfig proximityConfig = bundle.proximity(); + + Set blockEntities = new HashSet<>(); + List proximityBlocks = new ArrayList<>(); + + int baseX = chunkStruct.chunkX << 4; + int baseZ = chunkStruct.chunkZ << 4; + + int[] blockStates = new int[4096]; + + try (Chunk chunk = Chunk.fromChunkStruct(chunkStruct, bundle)) { + ChunkWriter writer = chunk.getWriter(); + while (writer.hasNext()) { + try { + ChunkSection chunkSection = writer.getOrWriteNext(); + if (chunkSection == null) { + continue; + } + + int blockCount = chunkSection.blockCount; + Palette.Builder builder = BlockStrategy.createPaletteBuilder(); + + final int baseY = heightAccessor.getMinBuildHeight() + (writer.getSectionIndex() << 4); + for (int index = 0; index < 4096; index++) { + int blockState = chunkSection.getBlockState(index); + + int y = baseY + (index >> 8 & 15); + if (bundle.shouldProcessBlock(y)) { + + int obfuscateBits = blockFlags.flags(blockState, y); + if (!BlockFlags.isEmpty(obfuscateBits)) { + + int x = baseX + (index & 15); + int z = baseZ + (index >> 4 & 15); + boolean obfuscated = true; + + if (BlockFlags.isObfuscateBitSet(obfuscateBits) + && shouldObfuscate(task, chunk, x, y, z) + && obfuscationConfig.shouldObfuscate(y)) { + blockState = bundle.nextRandomObfuscationBlock(y); + } else if (BlockFlags.isProximityBitSet(obfuscateBits) + && proximityConfig.shouldObfuscate(y)) { + proximityBlocks.add(new BlockPos(x, y, z)); + + if (BlockFlags.isUseBlockBelowBitSet(obfuscateBits)) { + blockState = getBlockStateBelow(blockFlags, chunk, x, y, z); + if (blockState == -1) { + blockState = bundle.nextRandomProximityBlock(y); + } + } else { + blockState = bundle.nextRandomProximityBlock(y); + } + } else { + obfuscated = false; + } + + if (obfuscated) { + if (BlockFlags.isBlockEntityBitSet(obfuscateBits)) { + blockEntities.add(new BlockPos(x, y, z)); + } + if (OrebfuscatorNms.isAir(blockState)) { + blockCount--; + } + } + } + } + + builder.add(blockState); + blockStates[index] = blockState; + } + + writer.writeSection(builder.build(), blockCount, blockStates, chunkSection.biomeSlice); + } catch (Exception e) { + throw new RuntimeException("error in section: " + writer.getSectionIndex(), e); + } + } + + long duration = System.currentTimeMillis() - time; + int count = COUNT.incrementAndGet(); + if (count > 0) { + double totalTime = (double) TIME.addAndGet(duration); + if (count % 100 == 0) { + Bukkit.broadcastMessage("ms/chunk: " + (totalTime / count)); + } + } + + task.complete(chunk.finalizeOutput(), blockEntities, proximityBlocks); + } catch (Exception e) { + task.completeExceptionally(e); + } + } + + // returns first block below given position that wouldn't be obfuscated in any + // way at given position + private int getBlockStateBelow(BlockFlags blockFlags, Chunk chunk, int x, int y, int z) { + for (int targetY = y - 1; targetY > chunk.getHeightAccessor().getMinBuildHeight(); targetY--) { + int blockData = chunk.getBlockState(x, targetY, z); + if (blockData != -1) { + int mask = blockFlags.flags(blockData, y); + if (BlockFlags.isEmpty(mask) || BlockFlags.isAllowForUseBlockBelowBitSet(mask)) { + return blockData; + } + } + } + return -1; + } + + private boolean shouldObfuscate(ObfuscationTask task, Chunk chunk, int x, int y, int z) { + return isAdjacentBlockOccluding(task, chunk, x, y + 1, z) + && isAdjacentBlockOccluding(task, chunk, x, y - 1, z) + && isAdjacentBlockOccluding(task, chunk, x + 1, y, z) + && isAdjacentBlockOccluding(task, chunk, x - 1, y, z) + && isAdjacentBlockOccluding(task, chunk, x, y, z + 1) + && isAdjacentBlockOccluding(task, chunk, x, y, z - 1); + } + + private boolean isAdjacentBlockOccluding(ObfuscationTask task, Chunk chunk, int x, int y, int z) { + if (y >= chunk.getHeightAccessor().getMaxBuildHeight() || y < chunk.getHeightAccessor().getMinBuildHeight()) { + return false; + } + + int blockId = chunk.getBlockState(x, y, z); + if (blockId == -1) { + blockId = task.getBlockState(x, y, z); + } + + return blockId >= 0 && OrebfuscatorNms.isOccluding(blockId); + } +} From 78343ae66bec74c59483274e13b3ab063f770cf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maurice=20Eisenbl=C3=A4tter?= Date: Mon, 7 Oct 2024 21:50:39 +0200 Subject: [PATCH 2/3] fix: chunk BlockStrategy --- .../orebfuscator/chunk/next/strategies/BlockStrategy.java | 4 ++-- .../chunk/next/strategies/PalettedContainerStrategy.java | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/BlockStrategy.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/BlockStrategy.java index 87651c65..8fd396fe 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/BlockStrategy.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/BlockStrategy.java @@ -31,10 +31,10 @@ public static void skipBitStorage(ByteBuf buffer, int bits) { } public static BitStorage readBitStorage(ByteBuf buffer, Palette palette) { - return BitStorage.read(buffer, PalettedContainerStrategy.BIOMES.size(), palette); + return BitStorage.read(buffer, PalettedContainerStrategy.BLOCK_STATES.size(), palette); } public static BitStorage.Writer createBitStorageWriter(ByteBuf buffer, Palette palette) { - return BitStorage.createWriter(buffer, PalettedContainerStrategy.BIOMES.size(), palette); + return BitStorage.createWriter(buffer, PalettedContainerStrategy.BLOCK_STATES.size(), palette); } } diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/PalettedContainerStrategy.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/PalettedContainerStrategy.java index 4b3debb8..cb711116 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/PalettedContainerStrategy.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/PalettedContainerStrategy.java @@ -62,7 +62,9 @@ public Palette.Builder createBuilder() { } Palette createPalette(List values) { - int bits = MathUtil.ceilLog2(values.size()); + int bits = values.size() == 1 + ? 0 + : MathUtil.ceilLog2(values.size()); return getConfiguration(bits).createPalette(values); } From 8d394c8cc1623591a69c8b866059848cca3f77e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maurice=20Eisenbl=C3=A4tter?= Date: Mon, 14 Oct 2024 19:07:31 +0200 Subject: [PATCH 3/3] fix: plugin init --- .../imprex/orebfuscator/OrebfuscatorNms.java | 4 +- .../orebfuscator/nms/AbstractNmsManager.java | 11 +- .../imprex/orebfuscator/nms/NmsManager.java | 2 + .../orebfuscator/nms/v1_16_R1/NmsManager.java | 5 +- .../orebfuscator/nms/v1_16_R2/NmsManager.java | 5 +- .../orebfuscator/nms/v1_16_R3/NmsManager.java | 5 +- .../orebfuscator/nms/v1_17_R1/NmsManager.java | 5 +- .../orebfuscator/nms/v1_18_R1/NmsManager.java | 5 +- .../orebfuscator/nms/v1_18_R2/NmsManager.java | 5 +- .../orebfuscator/nms/v1_19_R1/NmsManager.java | 5 +- .../orebfuscator/nms/v1_19_R2/NmsManager.java | 6 +- .../orebfuscator/nms/v1_19_R3/NmsManager.java | 6 +- .../orebfuscator/nms/v1_20_R1/NmsManager.java | 6 +- .../orebfuscator/nms/v1_20_R2/NmsManager.java | 6 +- .../orebfuscator/nms/v1_20_R3/NmsManager.java | 6 +- .../orebfuscator/nms/v1_20_R4/NmsManager.java | 6 +- .../orebfuscator/nms/v1_21_R1/NmsManager.java | 7 +- orebfuscator-plugin/pom.xml | 4 +- .../net/imprex/orebfuscator/Orebfuscator.java | 1 + .../imprex/orebfuscator/chunk/next/Chunk.java | 28 +-- .../orebfuscator/chunk/next/ChunkSection.java | 17 +- .../next/bitstorage/EmptyBitStorage.java | 2 +- .../next/bitstorage/SimpleBitStorage.java | 5 + .../chunk/next/palette/IndirectPalette.java | 8 +- .../chunk/next/palette/Palette.java | 2 +- .../chunk/next/strategies/PaletteBuilder.java | 14 ++ .../obfuscation/ObfuscationSystem.java | 4 +- .../ObfuscationTaskDispatcher.java | 4 +- .../obfuscation/ObfuscationTaskProcessor.java | 183 +++--------------- .../ObfuscationTaskProcessorNew.java | 176 +++++++++++++++++ .../ObfuscationTaskProcessorOld.java | 164 ++++++++++++++++ .../obfuscation/ObfuscationTaskWorker.java | 4 +- .../chunk/bitstorage/EmptyBitStorageTest.java | 54 ++++++ .../bitstorage/SimpleBitStorageTest.java | 155 +++++++++++++++ .../src/test/resources/simple-bit-storage.bin | Bin 0 -> 131075 bytes 35 files changed, 719 insertions(+), 201 deletions(-) create mode 100644 orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationTaskProcessorNew.java create mode 100644 orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationTaskProcessorOld.java create mode 100644 orebfuscator-plugin/src/test/java/net/imprex/orebfuscator/chunk/bitstorage/EmptyBitStorageTest.java create mode 100644 orebfuscator-plugin/src/test/java/net/imprex/orebfuscator/chunk/bitstorage/SimpleBitStorageTest.java create mode 100644 orebfuscator-plugin/src/test/resources/simple-bit-storage.bin diff --git a/orebfuscator-nms/orebfuscator-nms-api/src/main/java/net/imprex/orebfuscator/OrebfuscatorNms.java b/orebfuscator-nms/orebfuscator-nms-api/src/main/java/net/imprex/orebfuscator/OrebfuscatorNms.java index eeaadc81..91ed454a 100644 --- a/orebfuscator-nms/orebfuscator-nms-api/src/main/java/net/imprex/orebfuscator/OrebfuscatorNms.java +++ b/orebfuscator-nms/orebfuscator-nms-api/src/main/java/net/imprex/orebfuscator/OrebfuscatorNms.java @@ -26,7 +26,7 @@ public static void initialize(Config config) { } String nmsVersion = MinecraftVersion.nmsVersion(); - if (ServerVersion.isMojangMapped()) { + if (ServerVersion.isMojangMapped() && !ServerVersion.isPaper()) { nmsVersion += "_mojang"; } @@ -59,7 +59,7 @@ public static int getMaxBitsPerBlockState() { } public static int getMaxBitsPerBiome() { - throw new UnsupportedOperationException("TODO implement this"); + return instance.getMaxBitsPerBiome(); } public static BlockProperties getBlockByName(String key) { diff --git a/orebfuscator-nms/orebfuscator-nms-api/src/main/java/net/imprex/orebfuscator/nms/AbstractNmsManager.java b/orebfuscator-nms/orebfuscator-nms-api/src/main/java/net/imprex/orebfuscator/nms/AbstractNmsManager.java index 3bc532fe..cde86965 100644 --- a/orebfuscator-nms/orebfuscator-nms-api/src/main/java/net/imprex/orebfuscator/nms/AbstractNmsManager.java +++ b/orebfuscator-nms/orebfuscator-nms-api/src/main/java/net/imprex/orebfuscator/nms/AbstractNmsManager.java @@ -15,15 +15,19 @@ public abstract class AbstractNmsManager implements NmsManager { private final int uniqueBlockStateCount; private final int maxBitsPerBlockState; + private final int maxBitsPerBiome; + private final BlockStateProperties[] blockStates; private final Map blocks = new HashMap<>(); - public AbstractNmsManager(int uniqueBlockStateCount, AbstractRegionFileCache regionFileCache) { + public AbstractNmsManager(int uniqueBlockStateCount, int uniqueBiomesCount, AbstractRegionFileCache regionFileCache) { this.regionFileCache = regionFileCache; this.uniqueBlockStateCount = uniqueBlockStateCount; this.maxBitsPerBlockState = MathUtil.ceilLog2(uniqueBlockStateCount); + this.maxBitsPerBiome = MathUtil.ceilLog2(uniqueBiomesCount); + this.blockStates = new BlockStateProperties[uniqueBlockStateCount]; } @@ -50,6 +54,11 @@ public final int getMaxBitsPerBlockState() { return this.maxBitsPerBlockState; } + @Override + public int getMaxBitsPerBiome() { + return this.maxBitsPerBiome; + } + @Override public final BlockProperties getBlockByName(NamespacedKey key) { return this.blocks.get(key); diff --git a/orebfuscator-nms/orebfuscator-nms-api/src/main/java/net/imprex/orebfuscator/nms/NmsManager.java b/orebfuscator-nms/orebfuscator-nms-api/src/main/java/net/imprex/orebfuscator/nms/NmsManager.java index 7df81e57..e6931e99 100644 --- a/orebfuscator-nms/orebfuscator-nms-api/src/main/java/net/imprex/orebfuscator/nms/NmsManager.java +++ b/orebfuscator-nms/orebfuscator-nms-api/src/main/java/net/imprex/orebfuscator/nms/NmsManager.java @@ -15,6 +15,8 @@ public interface NmsManager { int getMaxBitsPerBlockState(); + int getMaxBitsPerBiome(); + BlockProperties getBlockByName(NamespacedKey key); boolean isAir(int blockId); diff --git a/orebfuscator-nms/orebfuscator-nms-v1_16_R1/src/main/java/net/imprex/orebfuscator/nms/v1_16_R1/NmsManager.java b/orebfuscator-nms/orebfuscator-nms-v1_16_R1/src/main/java/net/imprex/orebfuscator/nms/v1_16_R1/NmsManager.java index a0e29308..8b50c77d 100644 --- a/orebfuscator-nms/orebfuscator-nms-v1_16_R1/src/main/java/net/imprex/orebfuscator/nms/v1_16_R1/NmsManager.java +++ b/orebfuscator-nms/orebfuscator-nms-v1_16_R1/src/main/java/net/imprex/orebfuscator/nms/v1_16_R1/NmsManager.java @@ -69,7 +69,10 @@ private static EntityPlayer player(Player player) { } public NmsManager(Config config) { - super(Block.REGISTRY_ID.a(), new RegionFileCache(config.cache())); + super( + Block.REGISTRY_ID.a(), + IRegistry.h.get(IRegistry.u.a()).keySet().size(), + new RegionFileCache(config.cache())); for (Map.Entry, Block> entry : IRegistry.BLOCK.c()) { NamespacedKey namespacedKey = NamespacedKey.fromString(entry.getKey().a().toString()); diff --git a/orebfuscator-nms/orebfuscator-nms-v1_16_R2/src/main/java/net/imprex/orebfuscator/nms/v1_16_R2/NmsManager.java b/orebfuscator-nms/orebfuscator-nms-v1_16_R2/src/main/java/net/imprex/orebfuscator/nms/v1_16_R2/NmsManager.java index 6de27bda..74a0d12b 100644 --- a/orebfuscator-nms/orebfuscator-nms-v1_16_R2/src/main/java/net/imprex/orebfuscator/nms/v1_16_R2/NmsManager.java +++ b/orebfuscator-nms/orebfuscator-nms-v1_16_R2/src/main/java/net/imprex/orebfuscator/nms/v1_16_R2/NmsManager.java @@ -69,7 +69,10 @@ private static EntityPlayer player(Player player) { } public NmsManager(Config config) { - super(Block.REGISTRY_ID.a(), new RegionFileCache(config.cache())); + super( + Block.REGISTRY_ID.a(), + IRegistry.f.get(IRegistry.ay.a()).keySet().size(), + new RegionFileCache(config.cache())); for (Map.Entry, Block> entry : IRegistry.BLOCK.d()) { NamespacedKey namespacedKey = NamespacedKey.fromString(entry.getKey().a().toString()); diff --git a/orebfuscator-nms/orebfuscator-nms-v1_16_R3/src/main/java/net/imprex/orebfuscator/nms/v1_16_R3/NmsManager.java b/orebfuscator-nms/orebfuscator-nms-v1_16_R3/src/main/java/net/imprex/orebfuscator/nms/v1_16_R3/NmsManager.java index 6b1a040e..c03f7a15 100644 --- a/orebfuscator-nms/orebfuscator-nms-v1_16_R3/src/main/java/net/imprex/orebfuscator/nms/v1_16_R3/NmsManager.java +++ b/orebfuscator-nms/orebfuscator-nms-v1_16_R3/src/main/java/net/imprex/orebfuscator/nms/v1_16_R3/NmsManager.java @@ -69,7 +69,10 @@ private static EntityPlayer player(Player player) { } public NmsManager(Config config) { - super(Block.REGISTRY_ID.a(), new RegionFileCache(config.cache())); + super( + Block.REGISTRY_ID.a(), + IRegistry.f.get(IRegistry.ay.a()).keySet().size(), + new RegionFileCache(config.cache())); for (Map.Entry, Block> entry : IRegistry.BLOCK.d()) { NamespacedKey namespacedKey = NamespacedKey.fromString(entry.getKey().a().toString()); diff --git a/orebfuscator-nms/orebfuscator-nms-v1_17_R1/src/main/java/net/imprex/orebfuscator/nms/v1_17_R1/NmsManager.java b/orebfuscator-nms/orebfuscator-nms-v1_17_R1/src/main/java/net/imprex/orebfuscator/nms/v1_17_R1/NmsManager.java index a4733ccf..8eb8dc45 100644 --- a/orebfuscator-nms/orebfuscator-nms-v1_17_R1/src/main/java/net/imprex/orebfuscator/nms/v1_17_R1/NmsManager.java +++ b/orebfuscator-nms/orebfuscator-nms-v1_17_R1/src/main/java/net/imprex/orebfuscator/nms/v1_17_R1/NmsManager.java @@ -68,7 +68,10 @@ private static ServerPlayer player(Player player) { } public NmsManager(Config config) { - super(Block.BLOCK_STATE_REGISTRY.size(), new RegionFileCache(config.cache())); + super( + Block.BLOCK_STATE_REGISTRY.size(), + Registry.REGISTRY.get(Registry.BIOME_REGISTRY.location()).keySet().size(), + new RegionFileCache(config.cache())); for (Map.Entry, Block> entry : Registry.BLOCK.entrySet()) { NamespacedKey namespacedKey = NamespacedKey.fromString(entry.getKey().location().toString()); diff --git a/orebfuscator-nms/orebfuscator-nms-v1_18_R1/src/main/java/net/imprex/orebfuscator/nms/v1_18_R1/NmsManager.java b/orebfuscator-nms/orebfuscator-nms-v1_18_R1/src/main/java/net/imprex/orebfuscator/nms/v1_18_R1/NmsManager.java index a5f59059..69c67dfc 100644 --- a/orebfuscator-nms/orebfuscator-nms-v1_18_R1/src/main/java/net/imprex/orebfuscator/nms/v1_18_R1/NmsManager.java +++ b/orebfuscator-nms/orebfuscator-nms-v1_18_R1/src/main/java/net/imprex/orebfuscator/nms/v1_18_R1/NmsManager.java @@ -69,7 +69,10 @@ private static ServerPlayer player(Player player) { } public NmsManager(Config config) { - super(Block.BLOCK_STATE_REGISTRY.size(), new RegionFileCache(config.cache())); + super( + Block.BLOCK_STATE_REGISTRY.size(), + Registry.REGISTRY.get(Registry.BIOME_REGISTRY.location()).size(), + new RegionFileCache(config.cache())); for (Map.Entry, Block> entry : Registry.BLOCK.entrySet()) { NamespacedKey namespacedKey = NamespacedKey.fromString(entry.getKey().location().toString()); diff --git a/orebfuscator-nms/orebfuscator-nms-v1_18_R2/src/main/java/net/imprex/orebfuscator/nms/v1_18_R2/NmsManager.java b/orebfuscator-nms/orebfuscator-nms-v1_18_R2/src/main/java/net/imprex/orebfuscator/nms/v1_18_R2/NmsManager.java index 9d2ef265..2296da67 100644 --- a/orebfuscator-nms/orebfuscator-nms-v1_18_R2/src/main/java/net/imprex/orebfuscator/nms/v1_18_R2/NmsManager.java +++ b/orebfuscator-nms/orebfuscator-nms-v1_18_R2/src/main/java/net/imprex/orebfuscator/nms/v1_18_R2/NmsManager.java @@ -69,7 +69,10 @@ private static ServerPlayer player(Player player) { } public NmsManager(Config config) { - super(Block.BLOCK_STATE_REGISTRY.size(), new RegionFileCache(config.cache())); + super( + Block.BLOCK_STATE_REGISTRY.size(), + Registry.REGISTRY.get(Registry.BIOME_REGISTRY.location()).size(), + new RegionFileCache(config.cache())); for (Map.Entry, Block> entry : Registry.BLOCK.entrySet()) { NamespacedKey namespacedKey = NamespacedKey.fromString(entry.getKey().location().toString()); diff --git a/orebfuscator-nms/orebfuscator-nms-v1_19_R1/src/main/java/net/imprex/orebfuscator/nms/v1_19_R1/NmsManager.java b/orebfuscator-nms/orebfuscator-nms-v1_19_R1/src/main/java/net/imprex/orebfuscator/nms/v1_19_R1/NmsManager.java index cb5fcb95..b79fa103 100644 --- a/orebfuscator-nms/orebfuscator-nms-v1_19_R1/src/main/java/net/imprex/orebfuscator/nms/v1_19_R1/NmsManager.java +++ b/orebfuscator-nms/orebfuscator-nms-v1_19_R1/src/main/java/net/imprex/orebfuscator/nms/v1_19_R1/NmsManager.java @@ -68,7 +68,10 @@ private static ServerPlayer player(Player player) { } public NmsManager(Config config) { - super(Block.BLOCK_STATE_REGISTRY.size(), new RegionFileCache(config.cache())); + super( + Block.BLOCK_STATE_REGISTRY.size(), + Registry.REGISTRY.get(Registry.BIOME_REGISTRY.location()).size(), + new RegionFileCache(config.cache())); for (Map.Entry, Block> entry : Registry.BLOCK.entrySet()) { NamespacedKey namespacedKey = NamespacedKey.fromString(entry.getKey().location().toString()); diff --git a/orebfuscator-nms/orebfuscator-nms-v1_19_R2/src/main/java/net/imprex/orebfuscator/nms/v1_19_R2/NmsManager.java b/orebfuscator-nms/orebfuscator-nms-v1_19_R2/src/main/java/net/imprex/orebfuscator/nms/v1_19_R2/NmsManager.java index 4a6a9479..0619fa1d 100644 --- a/orebfuscator-nms/orebfuscator-nms-v1_19_R2/src/main/java/net/imprex/orebfuscator/nms/v1_19_R2/NmsManager.java +++ b/orebfuscator-nms/orebfuscator-nms-v1_19_R2/src/main/java/net/imprex/orebfuscator/nms/v1_19_R2/NmsManager.java @@ -26,6 +26,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.SectionPos; import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.ClientGamePacketListener; import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket; @@ -68,7 +69,10 @@ private static ServerPlayer player(Player player) { } public NmsManager(Config config) { - super(Block.BLOCK_STATE_REGISTRY.size(), new RegionFileCache(config.cache())); + super( + Block.BLOCK_STATE_REGISTRY.size(), + BuiltInRegistries.REGISTRY.get(Registries.BIOME.location()).size(), + new RegionFileCache(config.cache())); for (Map.Entry, Block> entry : BuiltInRegistries.BLOCK.entrySet()) { NamespacedKey namespacedKey = NamespacedKey.fromString(entry.getKey().location().toString()); diff --git a/orebfuscator-nms/orebfuscator-nms-v1_19_R3/src/main/java/net/imprex/orebfuscator/nms/v1_19_R3/NmsManager.java b/orebfuscator-nms/orebfuscator-nms-v1_19_R3/src/main/java/net/imprex/orebfuscator/nms/v1_19_R3/NmsManager.java index 4f81a4cc..daf4d288 100644 --- a/orebfuscator-nms/orebfuscator-nms-v1_19_R3/src/main/java/net/imprex/orebfuscator/nms/v1_19_R3/NmsManager.java +++ b/orebfuscator-nms/orebfuscator-nms-v1_19_R3/src/main/java/net/imprex/orebfuscator/nms/v1_19_R3/NmsManager.java @@ -26,6 +26,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.SectionPos; import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.ClientGamePacketListener; import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket; @@ -68,7 +69,10 @@ private static ServerPlayer player(Player player) { } public NmsManager(Config config) { - super(Block.BLOCK_STATE_REGISTRY.size(), new RegionFileCache(config.cache())); + super( + Block.BLOCK_STATE_REGISTRY.size(), + BuiltInRegistries.REGISTRY.get(Registries.BIOME.location()).size(), + new RegionFileCache(config.cache())); for (Map.Entry, Block> entry : BuiltInRegistries.BLOCK.entrySet()) { NamespacedKey namespacedKey = NamespacedKey.fromString(entry.getKey().location().toString()); diff --git a/orebfuscator-nms/orebfuscator-nms-v1_20_R1/src/main/java/net/imprex/orebfuscator/nms/v1_20_R1/NmsManager.java b/orebfuscator-nms/orebfuscator-nms-v1_20_R1/src/main/java/net/imprex/orebfuscator/nms/v1_20_R1/NmsManager.java index cc1b5f3c..7f4373c3 100644 --- a/orebfuscator-nms/orebfuscator-nms-v1_20_R1/src/main/java/net/imprex/orebfuscator/nms/v1_20_R1/NmsManager.java +++ b/orebfuscator-nms/orebfuscator-nms-v1_20_R1/src/main/java/net/imprex/orebfuscator/nms/v1_20_R1/NmsManager.java @@ -26,6 +26,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.SectionPos; import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.ClientGamePacketListener; import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket; @@ -68,7 +69,10 @@ private static ServerPlayer player(Player player) { } public NmsManager(Config config) { - super(Block.BLOCK_STATE_REGISTRY.size(), new RegionFileCache(config.cache())); + super( + Block.BLOCK_STATE_REGISTRY.size(), + BuiltInRegistries.REGISTRY.get(Registries.BIOME.location()).size(), + new RegionFileCache(config.cache())); for (Map.Entry, Block> entry : BuiltInRegistries.BLOCK.entrySet()) { NamespacedKey namespacedKey = NamespacedKey.fromString(entry.getKey().location().toString()); diff --git a/orebfuscator-nms/orebfuscator-nms-v1_20_R2/src/main/java/net/imprex/orebfuscator/nms/v1_20_R2/NmsManager.java b/orebfuscator-nms/orebfuscator-nms-v1_20_R2/src/main/java/net/imprex/orebfuscator/nms/v1_20_R2/NmsManager.java index 1b026f68..eabb627a 100644 --- a/orebfuscator-nms/orebfuscator-nms-v1_20_R2/src/main/java/net/imprex/orebfuscator/nms/v1_20_R2/NmsManager.java +++ b/orebfuscator-nms/orebfuscator-nms-v1_20_R2/src/main/java/net/imprex/orebfuscator/nms/v1_20_R2/NmsManager.java @@ -26,6 +26,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.SectionPos; import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.ClientGamePacketListener; import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket; @@ -68,7 +69,10 @@ private static ServerPlayer player(Player player) { } public NmsManager(Config config) { - super(Block.BLOCK_STATE_REGISTRY.size(), new RegionFileCache(config.cache())); + super( + Block.BLOCK_STATE_REGISTRY.size(), + BuiltInRegistries.REGISTRY.get(Registries.BIOME.location()).size(), + new RegionFileCache(config.cache())); for (Map.Entry, Block> entry : BuiltInRegistries.BLOCK.entrySet()) { NamespacedKey namespacedKey = NamespacedKey.fromString(entry.getKey().location().toString()); diff --git a/orebfuscator-nms/orebfuscator-nms-v1_20_R3/src/main/java/net/imprex/orebfuscator/nms/v1_20_R3/NmsManager.java b/orebfuscator-nms/orebfuscator-nms-v1_20_R3/src/main/java/net/imprex/orebfuscator/nms/v1_20_R3/NmsManager.java index 8d73c036..b9342a8d 100644 --- a/orebfuscator-nms/orebfuscator-nms-v1_20_R3/src/main/java/net/imprex/orebfuscator/nms/v1_20_R3/NmsManager.java +++ b/orebfuscator-nms/orebfuscator-nms-v1_20_R3/src/main/java/net/imprex/orebfuscator/nms/v1_20_R3/NmsManager.java @@ -26,6 +26,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.SectionPos; import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.ClientGamePacketListener; import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket; @@ -68,7 +69,10 @@ private static ServerPlayer player(Player player) { } public NmsManager(Config config) { - super(Block.BLOCK_STATE_REGISTRY.size(), new RegionFileCache(config.cache())); + super( + Block.BLOCK_STATE_REGISTRY.size(), + BuiltInRegistries.REGISTRY.get(Registries.BIOME.location()).size(), + new RegionFileCache(config.cache())); for (Map.Entry, Block> entry : BuiltInRegistries.BLOCK.entrySet()) { NamespacedKey namespacedKey = NamespacedKey.fromString(entry.getKey().location().toString()); diff --git a/orebfuscator-nms/orebfuscator-nms-v1_20_R4/src/main/java/net/imprex/orebfuscator/nms/v1_20_R4/NmsManager.java b/orebfuscator-nms/orebfuscator-nms-v1_20_R4/src/main/java/net/imprex/orebfuscator/nms/v1_20_R4/NmsManager.java index f37d98f8..d2df19eb 100644 --- a/orebfuscator-nms/orebfuscator-nms-v1_20_R4/src/main/java/net/imprex/orebfuscator/nms/v1_20_R4/NmsManager.java +++ b/orebfuscator-nms/orebfuscator-nms-v1_20_R4/src/main/java/net/imprex/orebfuscator/nms/v1_20_R4/NmsManager.java @@ -26,6 +26,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.SectionPos; import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.ClientGamePacketListener; import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket; @@ -68,7 +69,10 @@ private static ServerPlayer player(Player player) { } public NmsManager(Config config) { - super(Block.BLOCK_STATE_REGISTRY.size(), new RegionFileCache(config.cache())); + super( + Block.BLOCK_STATE_REGISTRY.size(), + BuiltInRegistries.REGISTRY.get(Registries.BIOME.location()).size(), + new RegionFileCache(config.cache())); for (Map.Entry, Block> entry : BuiltInRegistries.BLOCK.entrySet()) { NamespacedKey namespacedKey = NamespacedKey.fromString(entry.getKey().location().toString()); diff --git a/orebfuscator-nms/orebfuscator-nms-v1_21_R1/src/main/java/net/imprex/orebfuscator/nms/v1_21_R1/NmsManager.java b/orebfuscator-nms/orebfuscator-nms-v1_21_R1/src/main/java/net/imprex/orebfuscator/nms/v1_21_R1/NmsManager.java index 1d48d1c8..61c05d6a 100644 --- a/orebfuscator-nms/orebfuscator-nms-v1_21_R1/src/main/java/net/imprex/orebfuscator/nms/v1_21_R1/NmsManager.java +++ b/orebfuscator-nms/orebfuscator-nms-v1_21_R1/src/main/java/net/imprex/orebfuscator/nms/v1_21_R1/NmsManager.java @@ -8,6 +8,7 @@ import org.bukkit.Material; import org.bukkit.World; +import org.bukkit.craftbukkit.v1_21_R1.CraftRegistry; import org.bukkit.craftbukkit.v1_21_R1.CraftWorld; import org.bukkit.craftbukkit.v1_21_R1.block.data.CraftBlockData; import org.bukkit.craftbukkit.v1_21_R1.entity.CraftPlayer; @@ -26,6 +27,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.SectionPos; import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.ClientGamePacketListener; import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket; @@ -68,7 +70,10 @@ private static ServerPlayer player(Player player) { } public NmsManager(Config config) { - super(Block.BLOCK_STATE_REGISTRY.size(), new RegionFileCache(config.cache())); + super( + Block.BLOCK_STATE_REGISTRY.size(), + CraftRegistry.getMinecraftRegistry(Registries.BIOME).size(), + new RegionFileCache(config.cache())); for (Map.Entry, Block> entry : BuiltInRegistries.BLOCK.entrySet()) { NamespacedKey namespacedKey = NamespacedKey.fromString(entry.getKey().location().toString()); diff --git a/orebfuscator-plugin/pom.xml b/orebfuscator-plugin/pom.xml index c5c13098..8254c8d3 100644 --- a/orebfuscator-plugin/pom.xml +++ b/orebfuscator-plugin/pom.xml @@ -149,7 +149,7 @@ ${revision} compile - + net.imprex orebfuscator-nms-v1_21_R1 diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/Orebfuscator.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/Orebfuscator.java index 9eecdaa9..666a6dee 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/Orebfuscator.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/Orebfuscator.java @@ -44,6 +44,7 @@ public void onLoad() { @Override public void onEnable() { try { + // GZIPOutputStream // Check for valid minecraft version if (MinecraftVersion.isBelow("1.16")) { throw new RuntimeException("Orebfuscator only supports minecraft 1.16 and above"); diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/Chunk.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/Chunk.java index 072f06dc..2fdc42f0 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/Chunk.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/Chunk.java @@ -40,21 +40,25 @@ private Chunk(ChunkStruct chunkStruct, WorldConfigBundle bundle) { this.outputBuffer = PooledByteBufAllocator.DEFAULT.heapBuffer(chunkStruct.data.length); for (int sectionIndex = 0; sectionIndex < this.sections.length; sectionIndex++) { - if (chunkStruct.sectionMask.get(sectionIndex)) { - ChunkSegment sectionHolder; - - if (bundle.skipReadSectionIndex(sectionIndex)) { - sectionHolder = new SkippedChunkSegment(ChunkSection.skip(inputBuffer)); - } else { - ChunkSection chunkSection = new ChunkSection(inputBuffer); - if (bundle.skipProcessingSectionIndex(sectionIndex)) { - sectionHolder = new ReadOnlyChunkSegment(chunkSection); + try { + if (chunkStruct.sectionMask.get(sectionIndex)) { + ChunkSegment sectionHolder; + + if (bundle.skipReadSectionIndex(sectionIndex)) { + sectionHolder = new SkippedChunkSegment(ChunkSection.skip(inputBuffer)); } else { - sectionHolder = new ProcessingChunkSegment(chunkSection); + ChunkSection chunkSection = new ChunkSection(inputBuffer); + if (bundle.skipProcessingSectionIndex(sectionIndex)) { + sectionHolder = new ReadOnlyChunkSegment(chunkSection); + } else { + sectionHolder = new ProcessingChunkSegment(chunkSection); + } } - } - this.sections[sectionIndex] = sectionHolder; + this.sections[sectionIndex] = sectionHolder; + } + } catch (Exception e) { + throw new RuntimeException("An error occurred while reading chunk section: " + sectionIndex, e); } } diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/ChunkSection.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/ChunkSection.java index 0da0ab18..a4eb4d9a 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/ChunkSection.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/ChunkSection.java @@ -11,7 +11,7 @@ public class ChunkSection { static ByteBuf skip(ByteBuf buffer) { - int readerIndex = buffer.readerIndex(); + int startIndex = buffer.readerIndex(); buffer.skipBytes(2); // skip block count @@ -25,12 +25,11 @@ static ByteBuf skip(ByteBuf buffer) { BiomeStrategy.skipBitStorage(buffer, bitsPerBiome); } - int sectionLength = buffer.readerIndex() - readerIndex; - return buffer.slice(readerIndex, sectionLength); + int sectionLength = buffer.readerIndex() - startIndex; + return buffer.slice(startIndex, sectionLength); } public final int blockCount; - public final int bitsPerBlock; public final Palette blockPalette; public final BitStorage blockBitStorage; @@ -43,19 +42,19 @@ public ChunkSection(ByteBuf buffer) { this.blockCount = buffer.readShort(); - this.bitsPerBlock = buffer.readUnsignedByte(); - this.blockPalette = BlockStrategy.readPalette(buffer, this.bitsPerBlock); + int bitsPerBlock = buffer.readUnsignedByte(); + this.blockPalette = BlockStrategy.readPalette(buffer, bitsPerBlock); this.blockBitStorage = BlockStrategy.readBitStorage(buffer, this.blockPalette); if (ChunkCapabilities.hasBiomePalettedContainer()) { - int suffixOffset = buffer.readerIndex(); + int suffixStart = buffer.readerIndex(); int bitsPerBiome = buffer.readUnsignedByte(); BiomeStrategy.skipPalette(buffer, bitsPerBiome); BiomeStrategy.skipBitStorage(buffer, bitsPerBiome); - int suffixLength = buffer.readerIndex() - suffixOffset; - this.biomeSlice = buffer.slice(suffixOffset, suffixLength); + int suffixLength = buffer.readerIndex() - suffixStart; + this.biomeSlice = buffer.slice(suffixStart, suffixLength); } else { this.biomeSlice = Unpooled.EMPTY_BUFFER; } diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/bitstorage/EmptyBitStorage.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/bitstorage/EmptyBitStorage.java index 21732061..8cb21d02 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/bitstorage/EmptyBitStorage.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/bitstorage/EmptyBitStorage.java @@ -16,7 +16,7 @@ public static void skipBytes(ByteBuf buffer) { public static BitStorage read(ByteBuf buffer, int size, Palette palette) { if (palette.getBitsPerEntry() != 0) { - throw new IllegalArgumentException("invalid palette!"); + throw new IllegalArgumentException("Invalid palette, expected single valued palette!"); } // skip length field diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/bitstorage/SimpleBitStorage.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/bitstorage/SimpleBitStorage.java index d9a28394..3f96ca07 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/bitstorage/SimpleBitStorage.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/bitstorage/SimpleBitStorage.java @@ -86,6 +86,11 @@ public static class Writer implements BitStorage.Writer { private long longBuffer = 0; private int capacity; + + /* + * TODO: Investigate if long[] + flush is faster than + * write once + */ public Writer(ByteBuf buffer, int size, Palette palette) { this.bitsPerEntry = palette.getBitsPerEntry(); diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/palette/IndirectPalette.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/palette/IndirectPalette.java index cdf8ab59..5e183efb 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/palette/IndirectPalette.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/palette/IndirectPalette.java @@ -38,7 +38,7 @@ public Palette read(ByteBuf buffer, int bitsPerEntry) { } } - return new IndirectPalette(bitsPerEntry, byIndex, maxValue); + return new IndirectPalette(bitsPerEntry, byIndex, maxValue + 1); } @Override @@ -55,7 +55,7 @@ public Palette create(int bitsPerEntry, List values) { } } - return new IndirectPalette(bitsPerEntry, byIndex, maxValue); + return new IndirectPalette(bitsPerEntry, byIndex, maxValue + 1); } }; @@ -65,6 +65,10 @@ public Palette create(int bitsPerEntry, List values) { private final BitSet byValuePresent; private final byte[] byValue; private final int[] byIndex; + + /* + * TODO: remove byValue if PaletteBuilder is reworked + */ public IndirectPalette(int bitsPerEntry, int[] byIndex, int maxValue) { this.size = byIndex.length; diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/palette/Palette.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/palette/Palette.java index d1e15e26..45d1861c 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/palette/Palette.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/palette/Palette.java @@ -22,7 +22,7 @@ public interface Factory { Palette read(ByteBuf buffer, int bitsPerEntry); - Palette create(int bitsPerEntry, List values ); + Palette create(int bitsPerEntry, List values); } public interface Builder { diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/PaletteBuilder.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/PaletteBuilder.java index 8524e4f9..57f9271a 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/PaletteBuilder.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/chunk/next/strategies/PaletteBuilder.java @@ -8,6 +8,15 @@ public class PaletteBuilder implements Palette.Builder { + /** + * TODO: rewrite to return index on value add to allow + * for fast write without remapping, discard index[] if + * size exceeds 8 bit; + * + * Use a hash bucket strategy with a power-2 growth until + * max size; + */ + private final PalettedContainerStrategy strategy; private final BitSet byValue; @@ -22,10 +31,15 @@ public PaletteBuilder(PalettedContainerStrategy strategy) { @Override public PaletteBuilder add(int value) { + if (this.byIndex.size() > 256) { + return this; + } + if (!this.byValue.get(value)) { this.byValue.set(value); this.byIndex.add(value); } + return this; } diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationSystem.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationSystem.java index 7238a468..c915e690 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationSystem.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationSystem.java @@ -16,7 +16,7 @@ public class ObfuscationSystem { private final OrebfuscatorConfig config; private final ObfuscationCache cache; - private final ObfuscationProcessor processor; + private final ObfuscationTaskProcessor processor; private final ObfuscationTaskDispatcher dispatcher; private ObfuscationListener listener; @@ -27,7 +27,7 @@ public ObfuscationSystem(Orebfuscator orebfuscator) { this.config = orebfuscator.getOrebfuscatorConfig(); this.cache = orebfuscator.getObfuscationCache(); - this.processor = new ObfuscationProcessor(orebfuscator); + this.processor = new ObfuscationTaskProcessor(orebfuscator); this.dispatcher = new ObfuscationTaskDispatcher(orebfuscator, this.processor); this.deobfuscationWorker = new DeobfuscationWorker(orebfuscator); diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationTaskDispatcher.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationTaskDispatcher.java index 0cf1aa1e..5d792b35 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationTaskDispatcher.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationTaskDispatcher.java @@ -11,10 +11,10 @@ class ObfuscationTaskDispatcher { private final Queue tasks = new ConcurrentLinkedQueue<>(); - private final ObfuscationProcessor processor; + private final ObfuscationTaskProcessor processor; private final ObfuscationTaskWorker[] worker; - public ObfuscationTaskDispatcher(Orebfuscator orebfuscator, ObfuscationProcessor processor) { + public ObfuscationTaskDispatcher(Orebfuscator orebfuscator, ObfuscationTaskProcessor processor) { this.processor = processor; AdvancedConfig config = orebfuscator.getOrebfuscatorConfig().advanced(); diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationTaskProcessor.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationTaskProcessor.java index eda475de..1040d982 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationTaskProcessor.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationTaskProcessor.java @@ -1,179 +1,60 @@ package net.imprex.orebfuscator.obfuscation; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import org.bukkit.Bukkit; -import org.bukkit.World; import net.imprex.orebfuscator.Orebfuscator; -import net.imprex.orebfuscator.OrebfuscatorNms; -import net.imprex.orebfuscator.chunk.ChunkStruct; -import net.imprex.orebfuscator.chunk.next.Chunk; -import net.imprex.orebfuscator.chunk.next.ChunkSection; -import net.imprex.orebfuscator.chunk.next.ChunkWriter; -import net.imprex.orebfuscator.chunk.next.palette.Palette; -import net.imprex.orebfuscator.chunk.next.strategies.BlockStrategy; -import net.imprex.orebfuscator.config.BlockFlags; -import net.imprex.orebfuscator.config.ObfuscationConfig; -import net.imprex.orebfuscator.config.OrebfuscatorConfig; -import net.imprex.orebfuscator.config.ProximityConfig; -import net.imprex.orebfuscator.config.WorldConfigBundle; -import net.imprex.orebfuscator.util.BlockPos; -import net.imprex.orebfuscator.util.HeightAccessor; public class ObfuscationTaskProcessor { - private static final AtomicLong TIME = new AtomicLong(); private static final AtomicInteger COUNT = new AtomicInteger(-2500); - private final OrebfuscatorConfig config; + private static final AtomicLong TIME_OLD = new AtomicLong(); + private static final AtomicLong TIME_NEW = new AtomicLong(); + + private static final AtomicLong SIZE_OLD = new AtomicLong(); + private static final AtomicLong SIZE_NEW = new AtomicLong(); + + private final ObfuscationTaskProcessorOld oldProcessor; + private final ObfuscationTaskProcessorNew newProcessor; public ObfuscationTaskProcessor(Orebfuscator orebfuscator) { - this.config = orebfuscator.getOrebfuscatorConfig(); + this.oldProcessor = new ObfuscationTaskProcessorOld(orebfuscator); + this.newProcessor = new ObfuscationTaskProcessorNew(orebfuscator); } public void process(ObfuscationTask task) { - long time = System.currentTimeMillis(); - - ChunkStruct chunkStruct = task.getChunkStruct(); - - World world = chunkStruct.world; - HeightAccessor heightAccessor = HeightAccessor.get(world); - - WorldConfigBundle bundle = this.config.world(world); - BlockFlags blockFlags = bundle.blockFlags(); - ObfuscationConfig obfuscationConfig = bundle.obfuscation(); - ProximityConfig proximityConfig = bundle.proximity(); + long processTimeNew, processTimeOld; + int sizeNew, sizeOld; - Set blockEntities = new HashSet<>(); - List proximityBlocks = new ArrayList<>(); - - int baseX = chunkStruct.chunkX << 4; - int baseZ = chunkStruct.chunkZ << 4; - - int[] blockStates = new int[4096]; - - try (Chunk chunk = Chunk.fromChunkStruct(chunkStruct, bundle)) { - ChunkWriter writer = chunk.getWriter(); - while (writer.hasNext()) { - try { - ChunkSection chunkSection = writer.getOrWriteNext(); - if (chunkSection == null) { - continue; - } - - int blockCount = chunkSection.blockCount; - Palette.Builder builder = BlockStrategy.createPaletteBuilder(); - - final int baseY = heightAccessor.getMinBuildHeight() + (writer.getSectionIndex() << 4); - for (int index = 0; index < 4096; index++) { - int blockState = chunkSection.getBlockState(index); - - int y = baseY + (index >> 8 & 15); - if (bundle.shouldProcessBlock(y)) { - - int obfuscateBits = blockFlags.flags(blockState, y); - if (!BlockFlags.isEmpty(obfuscateBits)) { - - int x = baseX + (index & 15); - int z = baseZ + (index >> 4 & 15); - boolean obfuscated = true; - - if (BlockFlags.isObfuscateBitSet(obfuscateBits) - && shouldObfuscate(task, chunk, x, y, z) - && obfuscationConfig.shouldObfuscate(y)) { - blockState = bundle.nextRandomObfuscationBlock(y); - } else if (BlockFlags.isProximityBitSet(obfuscateBits) - && proximityConfig.shouldObfuscate(y)) { - proximityBlocks.add(new BlockPos(x, y, z)); - - if (BlockFlags.isUseBlockBelowBitSet(obfuscateBits)) { - blockState = getBlockStateBelow(blockFlags, chunk, x, y, z); - if (blockState == -1) { - blockState = bundle.nextRandomProximityBlock(y); - } - } else { - blockState = bundle.nextRandomProximityBlock(y); - } - } else { - obfuscated = false; - } - - if (obfuscated) { - if (BlockFlags.isBlockEntityBitSet(obfuscateBits)) { - blockEntities.add(new BlockPos(x, y, z)); - } - if (OrebfuscatorNms.isAir(blockState)) { - blockCount--; - } - } - } - } - - builder.add(blockState); - blockStates[index] = blockState; - } - - writer.writeSection(builder.build(), blockCount, blockStates, chunkSection.biomeSlice); - } catch (Exception e) { - throw new RuntimeException("error in section: " + writer.getSectionIndex(), e); - } - } - - long duration = System.currentTimeMillis() - time; - int count = COUNT.incrementAndGet(); - if (count > 0) { - double totalTime = (double) TIME.addAndGet(duration); - if (count % 100 == 0) { - Bukkit.broadcastMessage("ms/chunk: " + (totalTime / count)); - } - } - - task.complete(chunk.finalizeOutput(), blockEntities, proximityBlocks); - } catch (Exception e) { - task.completeExceptionally(e); + long time = System.currentTimeMillis(); + try { + sizeNew = newProcessor.process(task); + } finally { + processTimeNew = System.currentTimeMillis() - time; } - } - // returns first block below given position that wouldn't be obfuscated in any - // way at given position - private int getBlockStateBelow(BlockFlags blockFlags, Chunk chunk, int x, int y, int z) { - for (int targetY = y - 1; targetY > chunk.getHeightAccessor().getMinBuildHeight(); targetY--) { - int blockData = chunk.getBlockState(x, targetY, z); - if (blockData != -1) { - int mask = blockFlags.flags(blockData, y); - if (BlockFlags.isEmpty(mask) || BlockFlags.isAllowForUseBlockBelowBitSet(mask)) { - return blockData; - } - } + time = System.currentTimeMillis(); + try { + sizeOld = oldProcessor.process(task); + } finally { + processTimeOld = System.currentTimeMillis() - time; } - return -1; - } - private boolean shouldObfuscate(ObfuscationTask task, Chunk chunk, int x, int y, int z) { - return isAdjacentBlockOccluding(task, chunk, x, y + 1, z) - && isAdjacentBlockOccluding(task, chunk, x, y - 1, z) - && isAdjacentBlockOccluding(task, chunk, x + 1, y, z) - && isAdjacentBlockOccluding(task, chunk, x - 1, y, z) - && isAdjacentBlockOccluding(task, chunk, x, y, z + 1) - && isAdjacentBlockOccluding(task, chunk, x, y, z - 1); - } + int count = COUNT.incrementAndGet(); + if (count > 0) { + double totalTimeNew = (double) TIME_NEW.addAndGet(processTimeNew); + double totalTimeOld = (double) TIME_OLD.addAndGet(processTimeOld); - private boolean isAdjacentBlockOccluding(ObfuscationTask task, Chunk chunk, int x, int y, int z) { - if (y >= chunk.getHeightAccessor().getMaxBuildHeight() || y < chunk.getHeightAccessor().getMinBuildHeight()) { - return false; - } + double totalSizeNew = (double) SIZE_NEW.addAndGet(sizeNew); + double totalSizeOld = (double) SIZE_OLD.addAndGet(sizeOld); - int blockId = chunk.getBlockState(x, y, z); - if (blockId == -1) { - blockId = task.getBlockState(x, y, z); + if (count % 100 == 0) { + Bukkit.broadcastMessage(String.format("ms/chunk: %.3f - %.3f", (totalTimeNew / count), (totalTimeOld / count))); + Bukkit.broadcastMessage(String.format("kb/chunk: %.3f - %.3f", (totalSizeNew / 1024 / count), (totalSizeOld / 1024 / count))); + } } - - return blockId >= 0 && OrebfuscatorNms.isOccluding(blockId); } } diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationTaskProcessorNew.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationTaskProcessorNew.java new file mode 100644 index 00000000..45cf4d95 --- /dev/null +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationTaskProcessorNew.java @@ -0,0 +1,176 @@ +package net.imprex.orebfuscator.obfuscation; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.bukkit.World; + +import net.imprex.orebfuscator.Orebfuscator; +import net.imprex.orebfuscator.OrebfuscatorNms; +import net.imprex.orebfuscator.chunk.ChunkStruct; +import net.imprex.orebfuscator.chunk.next.Chunk; +import net.imprex.orebfuscator.chunk.next.ChunkSection; +import net.imprex.orebfuscator.chunk.next.ChunkWriter; +import net.imprex.orebfuscator.chunk.next.palette.Palette; +import net.imprex.orebfuscator.chunk.next.strategies.BlockStrategy; +import net.imprex.orebfuscator.config.BlockFlags; +import net.imprex.orebfuscator.config.ObfuscationConfig; +import net.imprex.orebfuscator.config.OrebfuscatorConfig; +import net.imprex.orebfuscator.config.ProximityConfig; +import net.imprex.orebfuscator.config.WorldConfigBundle; +import net.imprex.orebfuscator.util.BlockPos; +import net.imprex.orebfuscator.util.HeightAccessor; + +public class ObfuscationTaskProcessorNew { + + private final OrebfuscatorConfig config; + + public ObfuscationTaskProcessorNew(Orebfuscator orebfuscator) { + this.config = orebfuscator.getOrebfuscatorConfig(); + } + + public int process(ObfuscationTask task) { + ChunkStruct chunkStruct = task.getChunkStruct(); + + World world = chunkStruct.world; + HeightAccessor heightAccessor = HeightAccessor.get(world); + + WorldConfigBundle bundle = this.config.world(world); + BlockFlags blockFlags = bundle.blockFlags(); + ObfuscationConfig obfuscationConfig = bundle.obfuscation(); + ProximityConfig proximityConfig = bundle.proximity(); + + Set blockEntities = new HashSet<>(); + List proximityBlocks = new ArrayList<>(); + + int baseX = chunkStruct.chunkX << 4; + int baseZ = chunkStruct.chunkZ << 4; + + int layerY = Integer.MIN_VALUE; + int layerYBlockState = -1; + + int[] blockStates = new int[4096]; + + try (Chunk chunk = Chunk.fromChunkStruct(chunkStruct, bundle)) { + ChunkWriter writer = chunk.getWriter(); + while (writer.hasNext()) { + try { + ChunkSection chunkSection = writer.getOrWriteNext(); + if (chunkSection == null) { + continue; + } + + int blockCount = chunkSection.blockCount; + Palette.Builder builder = BlockStrategy.createPaletteBuilder(); + + final int baseY = heightAccessor.getMinBuildHeight() + (writer.getSectionIndex() << 4); + for (int index = 0; index < 4096; index++) { + int blockState = chunkSection.getBlockState(index); + + int y = baseY + (index >> 8 & 15); + if (bundle.shouldProcessBlock(y)) { + + int obfuscateBits = blockFlags.flags(blockState, y); + if (!BlockFlags.isEmpty(obfuscateBits)) { + + int x = baseX + (index & 15); + int z = baseZ + (index >> 4 & 15); + boolean obfuscated = true; + + if (BlockFlags.isObfuscateBitSet(obfuscateBits) + && obfuscationConfig.shouldObfuscate(y) + && shouldObfuscate(task, chunk, x, y, z)) { + if (obfuscationConfig.layerObfuscation()) { + if (layerY != y) { + layerY = y; + layerYBlockState = bundle.nextRandomObfuscationBlock(y); + } + blockState = layerYBlockState; + } else { + blockState = bundle.nextRandomObfuscationBlock(y); + } + } else if (BlockFlags.isProximityBitSet(obfuscateBits) + && proximityConfig.shouldObfuscate(y)) { + proximityBlocks.add(new BlockPos(x, y, z)); + + if (BlockFlags.isUseBlockBelowBitSet(obfuscateBits)) { + blockState = getBlockStateBelow(blockFlags, chunk, x, y, z); + if (blockState == -1) { + blockState = bundle.nextRandomProximityBlock(y); + } + } else { + blockState = bundle.nextRandomProximityBlock(y); + } + } else { + obfuscated = false; + } + + if (obfuscated) { + if (BlockFlags.isBlockEntityBitSet(obfuscateBits)) { + blockEntities.add(new BlockPos(x, y, z)); + } + if (OrebfuscatorNms.isAir(blockState)) { + blockCount--; + } + } + } + } + + builder.add(blockState); + blockStates[index] = blockState; + } + + writer.writeSection(builder.build(), blockCount, blockStates, chunkSection.biomeSlice); + } catch (Exception e) { + throw new RuntimeException("error in section: " + writer.getSectionIndex(), e); + } + } + + byte[] output = chunk.finalizeOutput(); + task.complete(output, blockEntities, proximityBlocks); + return output.length; + } catch (Exception e) { + task.completeExceptionally(e); + return 0; + } + } + + // returns first block below given position that wouldn't be obfuscated in any + // way at given position + private int getBlockStateBelow(BlockFlags blockFlags, Chunk chunk, int x, int y, int z) { + for (int targetY = y - 1; targetY > chunk.getHeightAccessor().getMinBuildHeight(); targetY--) { + int blockData = chunk.getBlockState(x, targetY, z); + if (blockData != -1) { + int mask = blockFlags.flags(blockData, y); + if (BlockFlags.isEmpty(mask) || BlockFlags.isAllowForUseBlockBelowBitSet(mask)) { + return blockData; + } + } + } + return -1; + } + + private boolean shouldObfuscate(ObfuscationTask task, Chunk chunk, int x, int y, int z) { + return isAdjacentBlockOccluding(task, chunk, x, y + 1, z) + && isAdjacentBlockOccluding(task, chunk, x, y - 1, z) + && isAdjacentBlockOccluding(task, chunk, x + 1, y, z) + && isAdjacentBlockOccluding(task, chunk, x - 1, y, z) + && isAdjacentBlockOccluding(task, chunk, x, y, z + 1) + && isAdjacentBlockOccluding(task, chunk, x, y, z - 1); + } + + private boolean isAdjacentBlockOccluding(ObfuscationTask task, Chunk chunk, int x, int y, int z) { + if (y >= chunk.getHeightAccessor().getMaxBuildHeight() || y < chunk.getHeightAccessor().getMinBuildHeight()) { + return false; + } + + int blockId = chunk.getBlockState(x, y, z); + if (blockId == -1) { + blockId = task.getBlockState(x, y, z); + } + + return blockId >= 0 && OrebfuscatorNms.isOccluding(blockId); + } +} diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationTaskProcessorOld.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationTaskProcessorOld.java new file mode 100644 index 00000000..e489d7f7 --- /dev/null +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationTaskProcessorOld.java @@ -0,0 +1,164 @@ +package net.imprex.orebfuscator.obfuscation; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.bukkit.World; + +import net.imprex.orebfuscator.Orebfuscator; +import net.imprex.orebfuscator.OrebfuscatorNms; +import net.imprex.orebfuscator.chunk.Chunk; +import net.imprex.orebfuscator.chunk.ChunkSection; +import net.imprex.orebfuscator.chunk.ChunkStruct; +import net.imprex.orebfuscator.config.BlockFlags; +import net.imprex.orebfuscator.config.ObfuscationConfig; +import net.imprex.orebfuscator.config.OrebfuscatorConfig; +import net.imprex.orebfuscator.config.ProximityConfig; +import net.imprex.orebfuscator.config.ProximityHeightCondition; +import net.imprex.orebfuscator.config.WorldConfigBundle; +import net.imprex.orebfuscator.util.BlockPos; +import net.imprex.orebfuscator.util.HeightAccessor; + +public class ObfuscationTaskProcessorOld { + + private final OrebfuscatorConfig config; + + public ObfuscationTaskProcessorOld(Orebfuscator orebfuscator) { + this.config = orebfuscator.getOrebfuscatorConfig(); + } + + public int process(ObfuscationTask task) { + ChunkStruct chunkStruct = task.getChunkStruct(); + + World world = chunkStruct.world; + HeightAccessor heightAccessor = HeightAccessor.get(world); + + WorldConfigBundle bundle = this.config.world(world); + BlockFlags blockFlags = bundle.blockFlags(); + ObfuscationConfig obfuscationConfig = bundle.obfuscation(); + ProximityConfig proximityConfig = bundle.proximity(); + + Set blockEntities = new HashSet<>(); + List proximityBlocks = new ArrayList<>(); + + int baseX = chunkStruct.chunkX << 4; + int baseZ = chunkStruct.chunkZ << 4; + + int layerY = Integer.MIN_VALUE; + int layerYBlockState = -1; + + try (Chunk chunk = Chunk.fromChunkStruct(chunkStruct)) { + for (int sectionIndex = Math.max(0, bundle.minSectionIndex()); sectionIndex <= Math + .min(chunk.getSectionCount() - 1, bundle.maxSectionIndex()); sectionIndex++) { + ChunkSection chunkSection = chunk.getSection(sectionIndex); + if (chunkSection == null || chunkSection.isEmpty()) { + continue; + } + + final int baseY = heightAccessor.getMinBuildHeight() + (sectionIndex << 4); + for (int index = 0; index < 4096; index++) { + int y = baseY + (index >> 8 & 15); + if (!bundle.shouldProcessBlock(y)) { + continue; + } + + int blockState = chunkSection.getBlockState(index); + + int obfuscateBits = blockFlags.flags(blockState, y); + if (BlockFlags.isEmpty(obfuscateBits)) { + continue; + } + + int x = baseX + (index & 15); + int z = baseZ + (index >> 4 & 15); + + boolean isObfuscateBitSet = BlockFlags.isObfuscateBitSet(obfuscateBits); + boolean obfuscated = false; + + // should current block be obfuscated + if (isObfuscateBitSet && obfuscationConfig.shouldObfuscate(y) && shouldObfuscate(task, chunk, x, y, z)) { + if (obfuscationConfig.layerObfuscation()) { + if (layerY != y) { + layerY = y; + layerYBlockState = bundle.nextRandomObfuscationBlock(y); + } + blockState = layerYBlockState; + } else { + blockState = bundle.nextRandomObfuscationBlock(y); + } + obfuscated = true; + } + + // should current block be proximity hidden + if (!obfuscated && BlockFlags.isProximityBitSet(obfuscateBits) && proximityConfig.shouldObfuscate(y)) { + proximityBlocks.add(new BlockPos(x, y, z)); + if (BlockFlags.isUseBlockBelowBitSet(obfuscateBits)) { + boolean allowNonOcclude = !isObfuscateBitSet || !ProximityHeightCondition.isPresent(obfuscateBits); + blockState = getBlockStateBelow(bundle, chunk, x, y, z, allowNonOcclude); + } else { + blockState = bundle.nextRandomProximityBlock(y); + } + obfuscated = true; + } + + // update block state if needed + if (obfuscated) { + chunkSection.setBlockState(index, blockState); + if (BlockFlags.isBlockEntityBitSet(obfuscateBits)) { + blockEntities.add(new BlockPos(x, y, z)); + } + } + } + } + + byte[] output = chunk.finalizeOutput(); + task.complete(output, blockEntities, proximityBlocks); + return output.length; + } catch (Exception e) { + task.completeExceptionally(e); + return 0; + } + } + + // returns first block below given position that wouldn't be obfuscated in any + // way at given position + private int getBlockStateBelow(WorldConfigBundle bundle, Chunk chunk, int x, int y, int z, boolean allowNonOcclude) { + BlockFlags blockFlags = bundle.blockFlags(); + + for (int targetY = y - 1; targetY > chunk.getHeightAccessor().getMinBuildHeight(); targetY--) { + int blockData = chunk.getBlockState(x, targetY, z); + if (blockData != -1 && (allowNonOcclude || OrebfuscatorNms.isOccluding(blockData))) { + int mask = blockFlags.flags(blockData, y); + if (BlockFlags.isEmpty(mask) || BlockFlags.isAllowForUseBlockBelowBitSet(mask)) { + return blockData; + } + } + } + + return bundle.nextRandomProximityBlock(y); + } + + private boolean shouldObfuscate(ObfuscationTask task, Chunk chunk, int x, int y, int z) { + return isAdjacentBlockOccluding(task, chunk, x, y + 1, z) + && isAdjacentBlockOccluding(task, chunk, x, y - 1, z) + && isAdjacentBlockOccluding(task, chunk, x + 1, y, z) + && isAdjacentBlockOccluding(task, chunk, x - 1, y, z) + && isAdjacentBlockOccluding(task, chunk, x, y, z + 1) + && isAdjacentBlockOccluding(task, chunk, x, y, z - 1); + } + + private boolean isAdjacentBlockOccluding(ObfuscationTask task, Chunk chunk, int x, int y, int z) { + if (y >= chunk.getHeightAccessor().getMaxBuildHeight() || y < chunk.getHeightAccessor().getMinBuildHeight()) { + return false; + } + + int blockId = chunk.getBlockState(x, y, z); + if (blockId == -1) { + blockId = task.getBlockState(x, y, z); + } + + return blockId >= 0 && OrebfuscatorNms.isOccluding(blockId); + } +} diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationTaskWorker.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationTaskWorker.java index c4223394..cf0c1a2d 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationTaskWorker.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationTaskWorker.java @@ -10,12 +10,12 @@ class ObfuscationTaskWorker implements Runnable { private static final AtomicInteger WORKER_ID = new AtomicInteger(); private final ObfuscationTaskDispatcher dispatcher; - private final ObfuscationProcessor processor; + private final ObfuscationTaskProcessor processor; private final Thread thread; private volatile boolean running = true; - public ObfuscationTaskWorker(ObfuscationTaskDispatcher dispatcher, ObfuscationProcessor processor) { + public ObfuscationTaskWorker(ObfuscationTaskDispatcher dispatcher, ObfuscationTaskProcessor processor) { this.dispatcher = dispatcher; this.processor = processor; diff --git a/orebfuscator-plugin/src/test/java/net/imprex/orebfuscator/chunk/bitstorage/EmptyBitStorageTest.java b/orebfuscator-plugin/src/test/java/net/imprex/orebfuscator/chunk/bitstorage/EmptyBitStorageTest.java new file mode 100644 index 00000000..bff46371 --- /dev/null +++ b/orebfuscator-plugin/src/test/java/net/imprex/orebfuscator/chunk/bitstorage/EmptyBitStorageTest.java @@ -0,0 +1,54 @@ +package net.imprex.orebfuscator.chunk.bitstorage; + +import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import net.imprex.orebfuscator.chunk.next.bitstorage.BitStorage; +import net.imprex.orebfuscator.chunk.next.bitstorage.EmptyBitStorage; +import net.imprex.orebfuscator.chunk.next.palette.IndirectPalette; +import net.imprex.orebfuscator.chunk.next.palette.Palette; +import net.imprex.orebfuscator.chunk.next.palette.SingleValuedPalette; + +public class EmptyBitStorageTest { + + private static final int STORAGE_SIZE = 4095; + private static final int VALUE = 7; + + @Test + public void testReadWriteSkip() { + Palette validPalette = new SingleValuedPalette(VALUE); + Palette invalidPalette = new IndirectPalette(1, new int[0], 0); + + int serializedSize = EmptyBitStorage.getSerializedSize(); + ByteBuf buffer = Unpooled.buffer(serializedSize); + + BitStorage.Writer writer = EmptyBitStorage.createWriter(buffer); + + assertEquals(true, writer.isExhausted()); + assertDoesNotThrow(() -> writer.throwIfNotExhausted()); + + assertThrows(UnsupportedOperationException.class, () -> writer.write(0)); + assertEquals(serializedSize, buffer.readableBytes()); + + assertThrows(IllegalArgumentException.class, () -> EmptyBitStorage.read(buffer, STORAGE_SIZE, invalidPalette)); + BitStorage bitStorage = EmptyBitStorage.read(buffer, STORAGE_SIZE, validPalette); + + for (int i = 0; i < STORAGE_SIZE; i++) { + assertEquals(VALUE, bitStorage.get(i)); + } + + assertEquals(0, buffer.readableBytes()); + assertEquals(STORAGE_SIZE, bitStorage.size()); + + buffer.readerIndex(0); + + EmptyBitStorage.skipBytes(buffer); + + assertEquals(0, buffer.readableBytes()); + } +} \ No newline at end of file diff --git a/orebfuscator-plugin/src/test/java/net/imprex/orebfuscator/chunk/bitstorage/SimpleBitStorageTest.java b/orebfuscator-plugin/src/test/java/net/imprex/orebfuscator/chunk/bitstorage/SimpleBitStorageTest.java new file mode 100644 index 00000000..7725290d --- /dev/null +++ b/orebfuscator-plugin/src/test/java/net/imprex/orebfuscator/chunk/bitstorage/SimpleBitStorageTest.java @@ -0,0 +1,155 @@ +package net.imprex.orebfuscator.chunk.bitstorage; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +import org.junit.Test; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import net.imprex.orebfuscator.chunk.VarInt; +import net.imprex.orebfuscator.chunk.next.bitstorage.BitStorage; +import net.imprex.orebfuscator.chunk.next.bitstorage.SimpleBitStorage; +import net.imprex.orebfuscator.chunk.next.palette.Palette; + +public class SimpleBitStorageTest { + + private static final byte[] IDENTITY_BUFFER = readIdentityBuffer("src/test/resources/simple-bit-storage.bin"); + private static final int TEST_ENTRY_COUNT = 65535; + + private static byte[] readIdentityBuffer(String path) { + try { + return Files.readAllBytes(Paths.get(path)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Test + public void testReadWriteSkip() { + for (int bits = 1; bits < 32; bits++) { + Palette palette = new DummyIdentityPalette(bits); + int maxValue = (1 << bits) - 1; + + int serializedSize = SimpleBitStorage.getSerializedSize(bits, TEST_ENTRY_COUNT); + ByteBuf buffer = Unpooled.buffer(serializedSize); + + BitStorage.Writer writer = SimpleBitStorage.createWriter(buffer, TEST_ENTRY_COUNT, palette); + + // initial writer state check + assertEquals(false, writer.isExhausted()); + assertThrows(IllegalStateException.class, () -> writer.throwIfNotExhausted()); + + for (int i = 0; i < TEST_ENTRY_COUNT; i++) { + assertEquals(false, writer.isExhausted()); + writer.write(i % maxValue); + } + + // exhausted writer state check + assertEquals(true, writer.isExhausted()); + assertDoesNotThrow(() -> writer.throwIfNotExhausted()); + assertThrows(IllegalStateException.class, () -> writer.write(0)); + + // expected vs actual serialized size check + assertEquals(serializedSize, buffer.readableBytes()); + + BitStorage bitStorage = SimpleBitStorage.read(buffer, TEST_ENTRY_COUNT, palette); + for (int i = 0; i < TEST_ENTRY_COUNT; i++) { + assertEquals(i % maxValue, bitStorage.get(i)); + } + + assertEquals(0, buffer.readableBytes()); + assertEquals(TEST_ENTRY_COUNT, bitStorage.size()); + + buffer.readerIndex(0); + SimpleBitStorage.skipBytes(buffer); + + assertEquals(0, buffer.readableBytes()); + } + } + + @Test + public void testFaweFix() { + Palette palette = new DummyIdentityPalette(4); + + int faweLength = (int) Math.ceil(TEST_ENTRY_COUNT / 64f); + int fawePacketSize = faweLength * Long.BYTES; + ByteBuf buffer = Unpooled.buffer(VarInt.getSerializedSize(faweLength) + fawePacketSize); + + VarInt.write(buffer, faweLength); + buffer.writerIndex(buffer.writerIndex() + fawePacketSize); + + BitStorage bitStorage = SimpleBitStorage.read(buffer, TEST_ENTRY_COUNT, palette); + for (int i = 0; i < TEST_ENTRY_COUNT; i++) { + assertEquals(0, bitStorage.get(i)); + } + + assertEquals(0, buffer.readableBytes()); + } + + @Test + public void testReadIdentity() { + Palette palette = new DummyIdentityPalette(16); + ByteBuf buffer = Unpooled.wrappedBuffer(IDENTITY_BUFFER).asReadOnly(); + + BitStorage bitStorage = SimpleBitStorage.read(buffer, TEST_ENTRY_COUNT, palette); + for (int i = 0; i < TEST_ENTRY_COUNT; i++) { + assertEquals(i, bitStorage.get(i)); + } + } + + public void testWriteIdentity() { + Palette palette = new DummyIdentityPalette(16); + byte[] output = new byte[IDENTITY_BUFFER.length]; + + ByteBuf buffer = Unpooled.wrappedBuffer(output); + buffer.writerIndex(0); + + BitStorage.Writer writer = new SimpleBitStorage.Writer(buffer, TEST_ENTRY_COUNT, palette); + for (int i = 0; i < TEST_ENTRY_COUNT; i++) { + writer.write(i); + } + + assertArrayEquals(IDENTITY_BUFFER, output); + } + + public class DummyIdentityPalette implements Palette { + + private final int bitsPerEntry; + + public DummyIdentityPalette(int bitsPerEntry) { + this.bitsPerEntry = bitsPerEntry; + } + + @Override + public int getBitsPerEntry() { + return this.bitsPerEntry; + } + + @Override + public int getSerializedSize() { + throw new UnsupportedOperationException(); + } + + @Override + public int valueAtIndex(int index) { + return index; + } + + @Override + public int indexForValue(int value) { + return value; + } + + @Override + public void write(ByteBuf buffer) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/orebfuscator-plugin/src/test/resources/simple-bit-storage.bin b/orebfuscator-plugin/src/test/resources/simple-bit-storage.bin new file mode 100644 index 0000000000000000000000000000000000000000..29c590a6cac169c57ba3675721bf916d5d35bf4b GIT binary patch literal 131075 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

izE3DufvMYW__Q0=L9 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
axMsqNHqYC1KX`c6Hku2aWp>@;#3It`rWPBW*e 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 uI+kKF7GgeDV