diff --git a/src/main/java/net/minestom/server/ServerFlag.java b/src/main/java/net/minestom/server/ServerFlag.java index 5dcd1dcb2..653c0e09c 100644 --- a/src/main/java/net/minestom/server/ServerFlag.java +++ b/src/main/java/net/minestom/server/ServerFlag.java @@ -27,6 +27,7 @@ public final class ServerFlag { public static final int PLAYER_PACKET_QUEUE_SIZE = intProperty("minestom.packet-queue-size", 1000); public static final long KEEP_ALIVE_DELAY = longProperty("minestom.keep-alive-delay", 10_000); public static final long KEEP_ALIVE_KICK = longProperty("minestom.keep-alive-kick", 15_000); + public static final int PLAYER_CHUNK_UPDATE_LIMITER_HISTORY_SIZE = intProperty("minestom.player.chunk-update-limiter-history-size", 5, 0, Integer.MAX_VALUE); // Network buffers public static final int MAX_PACKET_SIZE = intProperty("minestom.max-packet-size", 2_097_151); // 3 bytes var-int @@ -98,8 +99,19 @@ public final class ServerFlag { return System.getProperty(name); } + private static int intProperty(String name, int defaultValue, int minValue, int maxValue) { + int value = Integer.getInteger(name, defaultValue); + if (value < minValue || value > maxValue) { + throw new IllegalArgumentException(String.format( + "Property '%s' value must be in range [%d..%d] but was %d", + name, minValue, maxValue, value + )); + } + return value; + } + private static int intProperty(String name, int defaultValue) { - return Integer.getInteger(name, defaultValue); + return intProperty(name, defaultValue, Integer.MIN_VALUE, Integer.MAX_VALUE); } private static long longProperty(String name, long defaultValue) { diff --git a/src/main/java/net/minestom/server/entity/Player.java b/src/main/java/net/minestom/server/entity/Player.java index eb2f9d3ed..c23c1ea8f 100644 --- a/src/main/java/net/minestom/server/entity/Player.java +++ b/src/main/java/net/minestom/server/entity/Player.java @@ -191,7 +191,7 @@ public class Player extends LivingEntity implements CommandSender, HoverEventSou // Game state (https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol#Game_Event) private boolean enableRespawnScreen; - private final ChunkUpdateLimitChecker chunkUpdateLimitChecker = new ChunkUpdateLimitChecker(6); + private final ChunkUpdateLimitChecker chunkUpdateLimitChecker = new ChunkUpdateLimitChecker(ServerFlag.PLAYER_CHUNK_UPDATE_LIMITER_HISTORY_SIZE); // Experience orb pickup protected Cooldown experiencePickupCooldown = new Cooldown(Duration.of(10, TimeUnit.SERVER_TICK)); diff --git a/src/main/java/net/minestom/server/utils/chunk/ChunkUpdateLimitChecker.java b/src/main/java/net/minestom/server/utils/chunk/ChunkUpdateLimitChecker.java index bff217aa6..e6eec0758 100644 --- a/src/main/java/net/minestom/server/utils/chunk/ChunkUpdateLimitChecker.java +++ b/src/main/java/net/minestom/server/utils/chunk/ChunkUpdateLimitChecker.java @@ -6,17 +6,28 @@ import org.jetbrains.annotations.ApiStatus; import java.util.Arrays; +/** + * Allows to limit operations with recently operated chunks + *

+ * {@link ChunkUpdateLimitChecker#historySize} defines how many last chunks will be remembered + * to skip operations with them via {@link ChunkUpdateLimitChecker#addToHistory(Chunk)} returning {@code false} + */ @ApiStatus.Internal public final class ChunkUpdateLimitChecker { + private final int historySize; private final long[] chunkHistory; public ChunkUpdateLimitChecker(int historySize) { - this.historySize = historySize; - this.chunkHistory = new long[historySize]; + this.historySize = Math.max(0, historySize); + this.chunkHistory = new long[this.historySize]; this.clearHistory(); } + public boolean isEnabled() { + return historySize > 0; + } + /** * Adds the chunk to the history * @@ -24,14 +35,19 @@ public final class ChunkUpdateLimitChecker { * @return {@code true} if it's a new chunk in the history */ public boolean addToHistory(Chunk chunk) { + if (!isEnabled()) { + return true; + } final long index = CoordConversion.chunkIndex(chunk.getChunkX(), chunk.getChunkZ()); boolean result = true; final int lastIndex = historySize - 1; - for (int i = 0; i < lastIndex; i++) { + for (int i = 0; i <= lastIndex; i++) { if (chunkHistory[i] == index) { result = false; } - chunkHistory[i] = chunkHistory[i + 1]; + if (i != lastIndex) { + chunkHistory[i] = chunkHistory[i + 1]; + } } chunkHistory[lastIndex] = index; return result; diff --git a/src/test/java/net/minestom/server/utils/chunk/ChunkUpdateLimitCheckerTest.java b/src/test/java/net/minestom/server/utils/chunk/ChunkUpdateLimitCheckerTest.java new file mode 100644 index 000000000..32266eea2 --- /dev/null +++ b/src/test/java/net/minestom/server/utils/chunk/ChunkUpdateLimitCheckerTest.java @@ -0,0 +1,54 @@ +package net.minestom.server.utils.chunk; + +import net.minestom.server.instance.DynamicChunk; +import net.minestom.testing.Env; +import net.minestom.testing.EnvTest; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@EnvTest +public class ChunkUpdateLimitCheckerTest { + + @Test + public void testHistory(Env env) { + var instance = env.createFlatInstance(); + var limiter = new ChunkUpdateLimitChecker(3); + + assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 0))); + assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 1))); + assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 2))); + // history : 0, 1, 2 + + assertFalse(limiter.addToHistory(new DynamicChunk(instance, 0, 0))); + // history : 1, 2, 0 + assertFalse(limiter.addToHistory(new DynamicChunk(instance, 0, 1))); + // history : 2, 0, 1 + assertFalse(limiter.addToHistory(new DynamicChunk(instance, 0, 2))); + // history : 0, 1, 2 + + assertFalse(limiter.addToHistory(new DynamicChunk(instance, 0, 2))); + // history : 1, 2, 2 + assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 0))); + } + + @Test + public void testOneSlotHistory(Env env) { + var instance = env.createFlatInstance(); + var limiter = new ChunkUpdateLimitChecker(1); + assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 0))); + assertFalse(limiter.addToHistory(new DynamicChunk(instance, 0, 0))); + assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 1))); + assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 0))); + } + + @Test + public void testDisabling(Env env) { + var instance = env.createFlatInstance(); + var limiter = new ChunkUpdateLimitChecker(0); + assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 0))); + assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 0))); + assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 1))); + } +}