Skip to content

Commit 129badc

Browse files
authored
Merge pull request #1 from MinceraftMC/feat/anti-xray
Implement lightweight anti-xray support
2 parents c90afa7 + 2b72e14 commit 129badc

File tree

41 files changed

+1643
-285
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1643
-285
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ caching of chunk data.
2121
- Configurable limitations for chunk sending and chunk generation
2222
- Per-dimension configuration options
2323
- Support for integrated or dedicated fabric servers
24+
- Lightweight anti-xray support for extended chunks
2425

2526
<details>
2627
<summary><strong>Example: Loading speed for a view distance of 127 chunks</strong></summary>
@@ -71,10 +72,18 @@ On the first start, this plugin will automatically create a configuration file.
7172
- `view-distance`: The maximum extended view distance for this dimension (default: `32`)
7273
- `cache-duration`: The cache duration for how long extended chunks should be kept in memory (default: `PT5M`,
7374
5 minutes)
75+
- `anti-xray`:
76+
- `enabled`: Whether anti-xray will be enabled or disabled in this world (default: `false`)
77+
- `engine-mode`: Engine modes of anti-xray, either `HIDE`, `OBFUSCATE`, or `OBFUSCATE_LAYER` (default: `HIDE`)
78+
- `hidden-blocks`: The list of blocks to hide/obfuscate (default: all ores and all base blocks of dimensions)
7479

7580
Feel free to play around with the chunk generations and chunk sending limits for
7681
an optimal experience on your server setup.
7782

83+
When using anti-xray, be aware that this plugin implements a lightweight version of anti-xray, which
84+
doesn't check if a block is exposed to air or not. This means that every engine-mode other than `HIDE`
85+
will probably not look very good.
86+
7887
Make sure to adjust the cache duration based on what you use your server for;
7988
for e.g. static lobby servers, you can use a longer cache duration than for dynamic SMP servers.
8089
If the cache duration is too high, chunks will display outdated content after e.g. a rejoin.

common/src/main/java/dev/booky/betterview/common/antixray/AntiXrayProcessor.java

Lines changed: 420 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package dev.booky.betterview.common.antixray;
2+
// Created by booky10 in BetterView (20:15 16.06.2025)
3+
4+
import org.jspecify.annotations.NullMarked;
5+
6+
import java.util.Arrays;
7+
8+
// https://github.com/PaperMC/Paper/blob/ba7fb23ddd2376079951d1e22f9204d1ed691585/paper-server/src/main/java/io/papermc/paper/antixray/ChunkPacketBlockControllerAntiXray.java#L150-L168
9+
// licensed under the terms of the MIT license
10+
// the code has been adapted to our own structure, additionally all
11+
// returned arrays should be sorted
12+
@NullMarked
13+
@FunctionalInterface
14+
public interface ReplacementPresets {
15+
16+
static ReplacementPresets createStatic(int... presets) {
17+
Arrays.sort(presets);
18+
return __ -> presets;
19+
}
20+
21+
static ReplacementPresets createStaticZeroSplit(int[] abovePresets, int[] belowPresets) {
22+
Arrays.sort(abovePresets);
23+
Arrays.sort(belowPresets);
24+
return sectionY -> sectionY < 0 ? belowPresets : abovePresets;
25+
}
26+
27+
/**
28+
* @return a sorted array of preset blockstate ids
29+
*/
30+
int[] getPresets(int sectionY);
31+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package dev.booky.betterview.common.antixray;
2+
// Created by booky10 in BetterView (19:50 16.06.2025)
3+
4+
import org.jspecify.annotations.NullMarked;
5+
6+
import java.util.concurrent.ThreadLocalRandom;
7+
8+
// see https://github.com/PaperMC/Paper/blob/ba7fb23ddd2376079951d1e22f9204d1ed691585/paper-server/src/main/java/io/papermc/paper/antixray/ChunkPacketBlockControllerAntiXray.java#L232-L272
9+
// licensed under the terms of the MIT license
10+
// other than adapting the code to our own structure, no modifications have been made to the actual logic
11+
@NullMarked
12+
@FunctionalInterface
13+
public interface ReplacementStrategy {
14+
15+
ReplacementStrategy STATIC_ZERO = () -> 0;
16+
17+
// Paper's engine-mode 1 (hide ores)
18+
static ReplacementStrategy replaceStaticZero(int ignoredBlockCount) {
19+
return STATIC_ZERO;
20+
}
21+
22+
// Paper's engine-mode 2 (obfuscate)
23+
static ReplacementStrategy replaceRandom(int blockCount) {
24+
return new ReplacementStrategy() {
25+
private int state;
26+
27+
{
28+
while ((this.state = ThreadLocalRandom.current().nextInt()) == 0) ;
29+
}
30+
31+
@Override
32+
public int get() {
33+
// https://en.wikipedia.org/wiki/Xorshift
34+
this.state ^= this.state << 13;
35+
this.state ^= this.state >>> 17;
36+
this.state ^= this.state << 5;
37+
// https://www.pcg-random.org/posts/bounded-rands.html
38+
return (int) ((Integer.toUnsignedLong(this.state) * blockCount) >>> 32);
39+
}
40+
};
41+
}
42+
43+
// Paper's engine-mode 3 (obfuscate layer)
44+
static ReplacementStrategy replaceRandomLayered(int blockCount) {
45+
return new ReplacementStrategy() {
46+
private int state;
47+
private int next;
48+
49+
{
50+
while ((this.state = ThreadLocalRandom.current().nextInt()) == 0) ;
51+
}
52+
53+
@Override
54+
public void advance() {
55+
// https://en.wikipedia.org/wiki/Xorshift
56+
this.state ^= this.state << 13;
57+
this.state ^= this.state >>> 17;
58+
this.state ^= this.state << 5;
59+
// https://www.pcg-random.org/posts/bounded-rands.html
60+
this.next = (int) ((Integer.toUnsignedLong(this.state) * blockCount) >>> 32);
61+
}
62+
63+
@Override
64+
public int get() {
65+
return this.next;
66+
}
67+
};
68+
}
69+
70+
default void advance() {
71+
// NO-OP
72+
}
73+
74+
int get();
75+
76+
@FunctionalInterface
77+
interface Ctor {
78+
79+
ReplacementStrategy construct(int blockCount);
80+
}
81+
}

common/src/main/java/dev/booky/betterview/common/config/BvLevelConfig.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package dev.booky.betterview.common.config;
22
// Created by booky10 in BetterView (14:49 03.06.2025)
33

4+
import dev.booky.betterview.common.antixray.ReplacementStrategy;
5+
import net.kyori.adventure.key.Key;
46
import org.jspecify.annotations.NullMarked;
57
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
68
import org.spongepowered.configurate.objectmapping.meta.Comment;
79

810
import java.time.Duration;
11+
import java.util.List;
912

1013
@NullMarked
1114
@ConfigSerializable
@@ -21,6 +24,8 @@ public class BvLevelConfig {
2124
private int viewDistance = 32;
2225
@Comment("The cache duration for all extended chunks, after which they will be re-build")
2326
private Duration cacheDuration = Duration.ofMinutes(5L);
27+
@Comment("Configuration options for a lightweight anti-xray applied to extended chunks")
28+
private AntiXrayConfig antiXray = new AntiXrayConfig();
2429

2530
public boolean isEnabled() {
2631
return this.enabled;
@@ -41,4 +46,64 @@ public int getViewDistance() {
4146
public Duration getCacheDuration() {
4247
return this.cacheDuration;
4348
}
49+
50+
public AntiXrayConfig getAntiXray() {
51+
return this.antiXray;
52+
}
53+
54+
@ConfigSerializable
55+
public static final class AntiXrayConfig {
56+
57+
@Comment("Whether or not anti-xray is enabled for extended chunks")
58+
private boolean enabled = false;
59+
@Comment("The anti-xray engine mode, recommended to be left at \"HIDE\" as this\n"
60+
+ "anti-xray doesn't check whether a block is exposed or not")
61+
private EngineMode engineMode = EngineMode.HIDE;
62+
@Comment("The blocks to hide or obfuscate")
63+
private List<Key> hiddenBlocks = List.of(
64+
Key.key("stone"), Key.key("deepslate"),
65+
Key.key("netherrack"), Key.key("end_stone"),
66+
Key.key("diamond_ore"), Key.key("deepslate_diamond_ore"),
67+
Key.key("iron_ore"), Key.key("deepslate_iron_ore"),
68+
Key.key("coal_ore"), Key.key("deepslate_coal_ore"),
69+
Key.key("emerald_ore"), Key.key("deepslate_emerald_ore"),
70+
Key.key("copper_ore"), Key.key("deepslate_copper_ore"),
71+
Key.key("redstone_ore"), Key.key("deepslate_redstone_ore"),
72+
Key.key("gold_ore"), Key.key("deepslate_gold_ore"),
73+
Key.key("lapis_ore"), Key.key("deepslate_lapis_ore"),
74+
Key.key("nether_gold_ore"), Key.key("nether_quartz_ore"),
75+
Key.key("ancient_debris"), Key.key("raw_copper_block"),
76+
Key.key("raw_iron_block")
77+
);
78+
79+
public boolean isEnabled() {
80+
return this.enabled;
81+
}
82+
83+
public EngineMode getEngineMode() {
84+
return this.engineMode;
85+
}
86+
87+
public List<Key> getHiddenBlocks() {
88+
return this.hiddenBlocks;
89+
}
90+
91+
public enum EngineMode {
92+
93+
HIDE(ReplacementStrategy::replaceStaticZero),
94+
OBFUSCATE(ReplacementStrategy::replaceRandom),
95+
OBFUSCATE_LAYER(ReplacementStrategy::replaceRandomLayered),
96+
;
97+
98+
private final ReplacementStrategy.Ctor strategy;
99+
100+
EngineMode(ReplacementStrategy.Ctor strategy) {
101+
this.strategy = strategy;
102+
}
103+
104+
public ReplacementStrategy.Ctor getStrategy() {
105+
return this.strategy;
106+
}
107+
}
108+
}
44109
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package dev.booky.betterview.common.util;
2+
// Created by booky10 in BetterView (21:10 16.06.2025)
3+
4+
import org.jspecify.annotations.NullMarked;
5+
6+
@NullMarked
7+
public final class MathUtil {
8+
9+
private MathUtil() {
10+
}
11+
12+
public static int log2(int i) {
13+
// https://stackoverflow.com/a/3305710
14+
return 31 - Integer.numberOfLeadingZeros(i);
15+
}
16+
17+
public static int ceilLog2(int i) {
18+
// https://stackoverflow.com/a/22027270
19+
boolean pow2 = i != 0 && (i & -i) == i;
20+
// if the number is a power of two, don't add one; otherwise,
21+
// we want to get to the ceiling!
22+
return log2(i) + (pow2 ? 0 : 1);
23+
}
24+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package dev.booky.betterview.common.util;
2+
// Created by booky10 in BetterView (20:39 16.06.2025)
3+
4+
import io.netty.buffer.ByteBuf;
5+
import org.jspecify.annotations.NullMarked;
6+
7+
// https://github.com/PaperMC/Velocity/blob/44bc15db409226d39ca0330ac01bb1295da34424/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java#L151-L234
8+
// licensed under the terms of the GPL
9+
// the only change made was to replace exception handling
10+
@NullMarked
11+
public final class VarIntUtil {
12+
13+
private static final int MAXIMUM_VARINT_SIZE = 5;
14+
15+
private VarIntUtil() {
16+
}
17+
18+
/**
19+
* Reads a Minecraft-style VarInt from the specified {@code buf}.
20+
*
21+
* @param buf the buffer to read from
22+
* @return the decoded VarInt
23+
*/
24+
public static int readVarInt(ByteBuf buf) {
25+
int readable = buf.readableBytes();
26+
if (readable == 0) {
27+
// special case for empty buffer
28+
throw new IllegalStateException("No bytes readable");
29+
}
30+
31+
// we can read at least one byte, and this should be a common case
32+
int k = buf.readByte();
33+
if ((k & 0x80) != 128) {
34+
return k;
35+
}
36+
37+
// in case decoding one byte was not enough, use a loop to decode up to the next 4 bytes
38+
int maxRead = Math.min(MAXIMUM_VARINT_SIZE, readable);
39+
int i = k & 0x7F;
40+
for (int j = 1; j < maxRead; j++) {
41+
k = buf.readByte();
42+
i |= (k & 0x7F) << j * 7;
43+
if ((k & 0x80) != 128) {
44+
return i;
45+
}
46+
}
47+
throw new IllegalStateException("Too many bytes readable");
48+
}
49+
50+
/**
51+
* Writes a Minecraft-style VarInt to the specified {@code buf}.
52+
*
53+
* @param buf the buffer to read from
54+
* @param value the integer to write
55+
*/
56+
public static void writeVarInt(ByteBuf buf, int value) {
57+
// Peel the one and two byte count cases explicitly as they are the most common VarInt sizes
58+
// that the proxy will write, to improve inlining.
59+
if ((value & (0xFFFFFFFF << 7)) == 0) {
60+
buf.writeByte(value);
61+
} else if ((value & (0xFFFFFFFF << 14)) == 0) {
62+
int w = (value & 0x7F | 0x80) << 8 | (value >>> 7);
63+
buf.writeShort(w);
64+
} else {
65+
writeVarIntFull(buf, value);
66+
}
67+
}
68+
69+
private static void writeVarIntFull(ByteBuf buf, int value) {
70+
// See https://steinborn.me/posts/performance/how-fast-can-you-write-a-varint/
71+
72+
// This essentially is an unrolled version of the "traditional" VarInt encoding.
73+
if ((value & (0xFFFFFFFF << 7)) == 0) {
74+
buf.writeByte(value);
75+
} else if ((value & (0xFFFFFFFF << 14)) == 0) {
76+
int w = (value & 0x7F | 0x80) << 8 | (value >>> 7);
77+
buf.writeShort(w);
78+
} else if ((value & (0xFFFFFFFF << 21)) == 0) {
79+
int w = (value & 0x7F | 0x80) << 16 | ((value >>> 7) & 0x7F | 0x80) << 8 | (value >>> 14);
80+
buf.writeMedium(w);
81+
} else if ((value & (0xFFFFFFFF << 28)) == 0) {
82+
int w = (value & 0x7F | 0x80) << 24 | (((value >>> 7) & 0x7F | 0x80) << 16)
83+
| ((value >>> 14) & 0x7F | 0x80) << 8 | (value >>> 21);
84+
buf.writeInt(w);
85+
} else {
86+
int w = (value & 0x7F | 0x80) << 24 | ((value >>> 7) & 0x7F | 0x80) << 16
87+
| ((value >>> 14) & 0x7F | 0x80) << 8 | ((value >>> 21) & 0x7F | 0x80);
88+
buf.writeInt(w);
89+
buf.writeByte(value >>> 28);
90+
}
91+
}
92+
}

0 commit comments

Comments
 (0)