From 13df370a42673cd260bacc4bfd96222bcb817740 Mon Sep 17 00:00:00 2001 From: Moulberry Date: Tue, 26 Dec 2023 17:41:17 +0800 Subject: [PATCH] Add disallowed-blocks, log-large-block-buffer-changes and block-buffer-rate-limit config options --- build.gradle.kts | 6 +- .../java/com/moulberry/axiom/AxiomPaper.java | 40 +- .../com/moulberry/axiom/DisallowedBlocks.java | 92 ++++ .../moulberry/axiom/buffer/BiomeBuffer.java | 12 +- .../moulberry/axiom/buffer/BlockBuffer.java | 54 ++- .../axiom/buffer/CompressedBlockEntity.java | 3 +- .../axiom/buffer/Position2ByteMap.java | 16 +- .../axiom/packet/AxiomBigPayloadHandler.java | 1 + .../axiom/packet/HelloPacketListener.java | 11 +- .../packet/SetBlockBufferPacketListener.java | 421 ++++++++++-------- .../axiom/packet/SetBlockPacketListener.java | 2 +- src/main/resources/config.yml | 11 + 12 files changed, 437 insertions(+), 232 deletions(-) create mode 100644 src/main/java/com/moulberry/axiom/DisallowedBlocks.java diff --git a/build.gradle.kts b/build.gradle.kts index c361e1d..e771b98 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ plugins { `java-library` - id("io.papermc.paperweight.userdev") version "1.5.8" - id("xyz.jpenilla.run-paper") version "2.2.0" // Adds runServer and runMojangMappedServer tasks for testing + id("io.papermc.paperweight.userdev") version "1.5.11" + id("xyz.jpenilla.run-paper") version "2.2.2" // Adds runServer and runMojangMappedServer tasks for testing // Shades and relocates dependencies into our plugin jar. See https://imperceptiblethoughts.com/shadow/introduction/ id("com.github.johnrengelman.shadow") version "8.1.1" @@ -20,8 +20,8 @@ repositories { mavenCentral() maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") maven("https://jitpack.io") - maven("https://maven.enginehub.org/repo/") maven("https://repo.papermc.io/repository/maven-public/") + maven("https://maven.enginehub.org/repo/") } dependencies { diff --git a/src/main/java/com/moulberry/axiom/AxiomPaper.java b/src/main/java/com/moulberry/axiom/AxiomPaper.java index 138bb95..7e18301 100644 --- a/src/main/java/com/moulberry/axiom/AxiomPaper.java +++ b/src/main/java/com/moulberry/axiom/AxiomPaper.java @@ -1,5 +1,8 @@ package com.moulberry.axiom; +import com.google.common.util.concurrent.RateLimiter; +import com.mojang.brigadier.StringReader; +import com.moulberry.axiom.buffer.BlockBuffer; import com.moulberry.axiom.buffer.CompressedBlockEntity; import com.moulberry.axiom.event.AxiomCreateWorldPropertiesEvent; import com.moulberry.axiom.event.AxiomModifyWorldEvent; @@ -12,6 +15,11 @@ import io.papermc.paper.event.world.WorldGameRuleChangeEvent; import io.papermc.paper.network.ChannelInitializeListener; import io.papermc.paper.network.ChannelInitializeListenerHolder; import net.kyori.adventure.key.Key; +import net.minecraft.commands.arguments.blocks.BlockPredicateArgument; +import net.minecraft.commands.arguments.blocks.BlockStateArgument; +import net.minecraft.commands.arguments.blocks.BlockStateParser; +import net.minecraft.core.IdMapper; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.Connection; import net.minecraft.network.ConnectionProtocol; import net.minecraft.network.FriendlyByteBuf; @@ -19,6 +27,9 @@ import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.PacketFlow; import net.minecraft.network.protocol.common.ServerboundCustomPayloadPacket; import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; import org.bukkit.*; import org.bukkit.configuration.Configuration; import org.bukkit.entity.Player; @@ -32,14 +43,20 @@ import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; public class AxiomPaper extends JavaPlugin implements Listener { public static AxiomPaper PLUGIN; // tsk tsk tsk public final Set activeAxiomPlayers = Collections.newSetFromMap(new ConcurrentHashMap<>()); + public final Map playerBlockBufferRateLimiters = new ConcurrentHashMap<>(); public Configuration configuration; + public IdMapper allowedBlockRegistry = null; + private boolean logLargeBlockBufferChanges = false; + @Override public void onEnable() { PLUGIN = this; @@ -55,6 +72,11 @@ public class AxiomPaper extends JavaPlugin implements Listener { this.getLogger().warning("Invalid value for unsupported-axiom-version, expected 'kick', 'warn' or 'ignore'"); } + this.logLargeBlockBufferChanges = this.configuration.getBoolean("log-large-block-buffer-changes"); + + List disallowedBlocks = this.configuration.getStringList("disallowed-blocks"); + this.allowedBlockRegistry = DisallowedBlocks.createAllowedBlockRegistry(disallowedBlocks); + Bukkit.getPluginManager().registerEvents(this, this); // Bukkit.getPluginManager().registerEvents(new WorldPropertiesExample(), this); CompressedBlockEntity.initialize(this); @@ -70,7 +92,7 @@ public class AxiomPaper extends JavaPlugin implements Listener { msg.registerOutgoingPluginChannel(this, "axiom:ack_world_properties"); if (configuration.getBoolean("packet-handlers.hello")) { - msg.registerIncomingPluginChannel(this, "axiom:hello", new HelloPacketListener(this, activeAxiomPlayers)); + msg.registerIncomingPluginChannel(this, "axiom:hello", new HelloPacketListener(this)); } if (configuration.getBoolean("packet-handlers.set-gamemode")) { msg.registerIncomingPluginChannel(this, "axiom:set_gamemode", new SetGamemodePacketListener(this)); @@ -129,7 +151,7 @@ public class AxiomPaper extends JavaPlugin implements Listener { } Bukkit.getScheduler().scheduleSyncRepeatingTask(this, () -> { - HashSet newActiveAxiomPlayers = new HashSet<>(); + HashSet stillActiveAxiomPlayers = new HashSet<>(); for (Player player : Bukkit.getServer().getOnlinePlayers()) { if (activeAxiomPlayers.contains(player.getUniqueId())) { @@ -140,13 +162,13 @@ public class AxiomPaper extends JavaPlugin implements Listener { buf.getBytes(0, bytes); player.sendPluginMessage(this, "axiom:enable", bytes); } else { - newActiveAxiomPlayers.add(player.getUniqueId()); + stillActiveAxiomPlayers.add(player.getUniqueId()); } } } - activeAxiomPlayers.clear(); - activeAxiomPlayers.addAll(newActiveAxiomPlayers); + activeAxiomPlayers.retainAll(stillActiveAxiomPlayers); + playerBlockBufferRateLimiters.keySet().retainAll(stillActiveAxiomPlayers); }, 20, 20); int maxChunkRelightsPerTick = configuration.getInt("max-chunk-relights-per-tick"); @@ -157,10 +179,18 @@ public class AxiomPaper extends JavaPlugin implements Listener { }, 1, 1); } + public boolean logLargeBlockBufferChanges() { + return this.logLargeBlockBufferChanges; + } + public boolean canUseAxiom(Player player) { return player.hasPermission("axiom.*") && activeAxiomPlayers.contains(player.getUniqueId()); } + public @Nullable RateLimiter getBlockBufferRateLimiter(UUID uuid) { + return this.playerBlockBufferRateLimiters.get(uuid); + } + private final WeakHashMap worldProperties = new WeakHashMap<>(); public @Nullable ServerWorldPropertiesRegistry getWorldPropertiesIfPresent(World world) { diff --git a/src/main/java/com/moulberry/axiom/DisallowedBlocks.java b/src/main/java/com/moulberry/axiom/DisallowedBlocks.java new file mode 100644 index 0000000..f80d2b8 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/DisallowedBlocks.java @@ -0,0 +1,92 @@ +package com.moulberry.axiom; + +import com.mojang.brigadier.StringReader; +import com.mojang.datafixers.util.Either; +import com.moulberry.axiom.buffer.BlockBuffer; +import net.minecraft.commands.arguments.blocks.BlockPredicateArgument; +import net.minecraft.commands.arguments.blocks.BlockStateParser; +import net.minecraft.core.IdMapper; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.Property; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +public class DisallowedBlocks { + + public static IdMapper createAllowedBlockRegistry(List disallowedBlocks) { + List> disallowedPredicates = new ArrayList<>(); + + for (String disallowedBlock : disallowedBlocks) { + try { + var parsed = BlockStateParser.parseForTesting(BuiltInRegistries.BLOCK.asLookup(), new StringReader(disallowedBlock), false); + + parsed.left().ifPresent(result -> { + disallowedPredicates.add(blockState -> { + if (!blockState.is(result.blockState().getBlock())) { + return false; + } else { + for (Property property : result.properties().keySet()) { + if (blockState.getValue(property) != result.blockState().getValue(property)) { + return false; + } + } + return true; + } + }); + }); + + parsed.right().ifPresent(result -> { + disallowedPredicates.add(blockState -> { + if (!blockState.is(result.tag())) { + return false; + } else { + for(Map.Entry entry : result.vagueProperties().entrySet()) { + Property property = blockState.getBlock().getStateDefinition().getProperty(entry.getKey()); + if (property == null) { + return false; + } + + Comparable comparable = property.getValue(entry.getValue()).orElse(null); + if (comparable == null) { + return false; + } + + if (blockState.getValue(property) != comparable) { + return false; + } + } + + return true; + } + }); + }); + } catch (Exception ignored) {} + } + + IdMapper allowedBlockRegistry = new IdMapper<>(); + + // Create allowedBlockRegistry + blocks: + for (BlockState blockState : Block.BLOCK_STATE_REGISTRY) { + for (Predicate disallowedPredicate : disallowedPredicates) { + if (disallowedPredicate.test(blockState)) { + allowedBlockRegistry.add(BlockBuffer.EMPTY_STATE); + continue blocks; + } + } + + allowedBlockRegistry.add(blockState); + } + allowedBlockRegistry.addMapping(BlockBuffer.EMPTY_STATE, Block.BLOCK_STATE_REGISTRY.getId(BlockBuffer.EMPTY_STATE)); + return allowedBlockRegistry; + } + +} diff --git a/src/main/java/com/moulberry/axiom/buffer/BiomeBuffer.java b/src/main/java/com/moulberry/axiom/buffer/BiomeBuffer.java index bf14541..9f756d9 100644 --- a/src/main/java/com/moulberry/axiom/buffer/BiomeBuffer.java +++ b/src/main/java/com/moulberry/axiom/buffer/BiomeBuffer.java @@ -1,11 +1,15 @@ package com.moulberry.axiom.buffer; +import com.google.common.util.concurrent.RateLimiter; import it.unimi.dsi.fastutil.objects.Object2ByteMap; import it.unimi.dsi.fastutil.objects.Object2ByteOpenHashMap; import net.minecraft.core.registries.Registries; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.resources.ResourceKey; import net.minecraft.world.level.biome.Biome; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.atomic.AtomicBoolean; public class BiomeBuffer { @@ -27,6 +31,10 @@ public class BiomeBuffer { this.paletteSize = this.paletteReverse.size(); } + public int size() { + return this.map.size(); + } + public void save(FriendlyByteBuf friendlyByteBuf) { friendlyByteBuf.writeByte(this.paletteSize); for (int i = 0; i < this.paletteSize; i++) { @@ -35,7 +43,7 @@ public class BiomeBuffer { this.map.save(friendlyByteBuf); } - public static BiomeBuffer load(FriendlyByteBuf friendlyByteBuf) { + public static BiomeBuffer load(FriendlyByteBuf friendlyByteBuf, @Nullable RateLimiter rateLimiter, AtomicBoolean reachedRateLimit) { int paletteSize = friendlyByteBuf.readByte(); ResourceKey[] palette = new ResourceKey[255]; Object2ByteMap> paletteReverse = new Object2ByteOpenHashMap<>(); @@ -44,7 +52,7 @@ public class BiomeBuffer { palette[i] = key; paletteReverse.put(key, (byte)(i+1)); } - Position2ByteMap map = Position2ByteMap.load(friendlyByteBuf); + Position2ByteMap map = Position2ByteMap.load(friendlyByteBuf, rateLimiter, reachedRateLimit); return new BiomeBuffer(map, palette, paletteReverse); } diff --git a/src/main/java/com/moulberry/axiom/buffer/BlockBuffer.java b/src/main/java/com/moulberry/axiom/buffer/BlockBuffer.java index 6cc39bb..8094081 100644 --- a/src/main/java/com/moulberry/axiom/buffer/BlockBuffer.java +++ b/src/main/java/com/moulberry/axiom/buffer/BlockBuffer.java @@ -1,5 +1,6 @@ package com.moulberry.axiom.buffer; +import com.google.common.util.concurrent.RateLimiter; import com.moulberry.axiom.AxiomConstants; import com.moulberry.axiom.AxiomPaper; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; @@ -15,6 +16,8 @@ import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.chunk.PalettedContainer; import org.jetbrains.annotations.Nullable; +import java.util.concurrent.atomic.AtomicBoolean; + public class BlockBuffer { public static final BlockState EMPTY_STATE = Blocks.STRUCTURE_VOID.defaultBlockState(); @@ -24,6 +27,8 @@ public class BlockBuffer { private PalettedContainer last = null; private long lastId = AxiomConstants.MIN_POSITION_LONG; private final Long2ObjectMap> blockEntities = new Long2ObjectOpenHashMap<>(); + private long totalBlockEntities = 0; + private long totalBlockEntityBytes = 0; public BlockBuffer() { this.values = new Long2ObjectOpenHashMap<>(); @@ -53,54 +58,57 @@ public class BlockBuffer { friendlyByteBuf.writeLong(AxiomConstants.MIN_POSITION_LONG); } - public static BlockBuffer load(FriendlyByteBuf friendlyByteBuf) { + public static BlockBuffer load(FriendlyByteBuf friendlyByteBuf, @Nullable RateLimiter rateLimiter, AtomicBoolean reachedRateLimit) { BlockBuffer buffer = new BlockBuffer(); + long totalBlockEntities = 0; + long totalBlockEntityBytes = 0; + while (true) { long index = friendlyByteBuf.readLong(); if (index == AxiomConstants.MIN_POSITION_LONG) break; + if (rateLimiter != null) { + if (!rateLimiter.tryAcquire()) { + reachedRateLimit.set(true); + buffer.totalBlockEntities = totalBlockEntities; + buffer.totalBlockEntityBytes = totalBlockEntityBytes; + return buffer; + } + } + PalettedContainer palettedContainer = buffer.getOrCreateSection(index); palettedContainer.read(friendlyByteBuf); int blockEntitySize = Math.min(4096, friendlyByteBuf.readVarInt()); if (blockEntitySize > 0) { Short2ObjectMap map = new Short2ObjectOpenHashMap<>(blockEntitySize); + + int startIndex = friendlyByteBuf.readerIndex(); + for (int i = 0; i < blockEntitySize; i++) { short offset = friendlyByteBuf.readShort(); CompressedBlockEntity blockEntity = CompressedBlockEntity.read(friendlyByteBuf); map.put(offset, blockEntity); } + buffer.blockEntities.put(index, map); + totalBlockEntities += blockEntitySize; + totalBlockEntityBytes += friendlyByteBuf.readerIndex() - startIndex; } } + buffer.totalBlockEntities = totalBlockEntities; + buffer.totalBlockEntityBytes = totalBlockEntityBytes; return buffer; } - public void clear() { - this.last = null; - this.lastId = AxiomConstants.MIN_POSITION_LONG; - this.values.clear(); + public long getTotalBlockEntities() { + return this.totalBlockEntities; } - public void putBlockEntity(int x, int y, int z, CompressedBlockEntity blockEntity) { - long cpos = BlockPos.asLong(x >> 4, y >> 4, z >> 4); - Short2ObjectMap chunkMap = this.blockEntities.computeIfAbsent(cpos, k -> new Short2ObjectOpenHashMap<>()); - - int key = (x & 0xF) | ((y & 0xF) << 4) | ((z & 0xF) << 8); - chunkMap.put((short)key, blockEntity); - } - - @Nullable - public CompressedBlockEntity getBlockEntity(int x, int y, int z) { - long cpos = BlockPos.asLong(x >> 4, y >> 4, z >> 4); - Short2ObjectMap chunkMap = this.blockEntities.get(cpos); - - if (chunkMap == null) return null; - - int key = (x & 0xF) | ((y & 0xF) << 4) | ((z & 0xF) << 8); - return chunkMap.get((short)key); + public long getTotalBlockEntityBytes() { + return this.totalBlockEntityBytes; } @Nullable @@ -170,7 +178,7 @@ public class BlockBuffer { public PalettedContainer getOrCreateSection(long id) { if (this.last == null || id != this.lastId) { this.lastId = id; - this.last = this.values.computeIfAbsent(id, k -> new PalettedContainer<>(Block.BLOCK_STATE_REGISTRY, + this.last = this.values.computeIfAbsent(id, k -> new PalettedContainer<>(AxiomPaper.PLUGIN.allowedBlockRegistry, EMPTY_STATE, PalettedContainer.Strategy.SECTION_STATES)); } diff --git a/src/main/java/com/moulberry/axiom/buffer/CompressedBlockEntity.java b/src/main/java/com/moulberry/axiom/buffer/CompressedBlockEntity.java index 76f9bba..c1d09b0 100644 --- a/src/main/java/com/moulberry/axiom/buffer/CompressedBlockEntity.java +++ b/src/main/java/com/moulberry/axiom/buffer/CompressedBlockEntity.java @@ -5,6 +5,7 @@ import com.github.luben.zstd.ZstdDictCompress; import com.github.luben.zstd.ZstdDictDecompress; import com.moulberry.axiom.AxiomPaper; import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtAccounter; import net.minecraft.nbt.NbtIo; import net.minecraft.network.FriendlyByteBuf; @@ -44,7 +45,7 @@ public record CompressedBlockEntity(int originalSize, byte compressionDict, byte try { byte[] nbt = Zstd.decompress(this.compressed, zstdDictDecompress, this.originalSize); - return NbtIo.read(new DataInputStream(new ByteArrayInputStream(nbt))); + return NbtIo.read(new DataInputStream(new ByteArrayInputStream(nbt)), NbtAccounter.create(131072)); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/src/main/java/com/moulberry/axiom/buffer/Position2ByteMap.java b/src/main/java/com/moulberry/axiom/buffer/Position2ByteMap.java index 438872e..3080891 100644 --- a/src/main/java/com/moulberry/axiom/buffer/Position2ByteMap.java +++ b/src/main/java/com/moulberry/axiom/buffer/Position2ByteMap.java @@ -1,13 +1,16 @@ package com.moulberry.axiom.buffer; +import com.google.common.util.concurrent.RateLimiter; import com.moulberry.axiom.AxiomConstants; import com.moulberry.axiom.AxiomPaper; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import net.minecraft.core.BlockPos; import net.minecraft.network.FriendlyByteBuf; +import org.jetbrains.annotations.Nullable; import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.LongFunction; public class Position2ByteMap { @@ -42,6 +45,10 @@ public class Position2ByteMap { } } + public int size() { + return this.map.size(); + } + public void save(FriendlyByteBuf friendlyByteBuf) { friendlyByteBuf.writeByte(this.defaultValue); for (Long2ObjectMap.Entry entry : this.map.long2ObjectEntrySet()) { @@ -51,13 +58,20 @@ public class Position2ByteMap { friendlyByteBuf.writeLong(AxiomConstants.MIN_POSITION_LONG); } - public static Position2ByteMap load(FriendlyByteBuf friendlyByteBuf) { + public static Position2ByteMap load(FriendlyByteBuf friendlyByteBuf, @Nullable RateLimiter rateLimiter, AtomicBoolean reachedRateLimit) { Position2ByteMap map = new Position2ByteMap(friendlyByteBuf.readByte()); while (true) { long pos = friendlyByteBuf.readLong(); if (pos == AxiomConstants.MIN_POSITION_LONG) break; + if (rateLimiter != null) { + if (!rateLimiter.tryAcquire()) { + reachedRateLimit.set(true); + return map; + } + } + byte[] bytes = new byte[16*16*16]; friendlyByteBuf.readBytes(bytes); map.map.put(pos, bytes); diff --git a/src/main/java/com/moulberry/axiom/packet/AxiomBigPayloadHandler.java b/src/main/java/com/moulberry/axiom/packet/AxiomBigPayloadHandler.java index 60fa216..0b8761f 100644 --- a/src/main/java/com/moulberry/axiom/packet/AxiomBigPayloadHandler.java +++ b/src/main/java/com/moulberry/axiom/packet/AxiomBigPayloadHandler.java @@ -47,6 +47,7 @@ public class AxiomBigPayloadHandler extends ByteToMessageDecoder { if (player != null && player.getBukkitEntity().hasPermission("axiom.*")) { if (listener.onReceive(player, buf)) { success = true; + in.skipBytes(in.readableBytes()); return; } } diff --git a/src/main/java/com/moulberry/axiom/packet/HelloPacketListener.java b/src/main/java/com/moulberry/axiom/packet/HelloPacketListener.java index 62a2322..35084af 100644 --- a/src/main/java/com/moulberry/axiom/packet/HelloPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/HelloPacketListener.java @@ -1,5 +1,6 @@ package com.moulberry.axiom.packet; +import com.google.common.util.concurrent.RateLimiter; import com.moulberry.axiom.AxiomConstants; import com.moulberry.axiom.AxiomPaper; import com.moulberry.axiom.View; @@ -29,11 +30,9 @@ import java.util.UUID; public class HelloPacketListener implements PluginMessageListener { private final AxiomPaper plugin; - private final Set activeAxiomPlayers; - public HelloPacketListener(AxiomPaper plugin, Set activeAxiomPlayers) { + public HelloPacketListener(AxiomPaper plugin) { this.plugin = plugin; - this.activeAxiomPlayers = activeAxiomPlayers; } @Override @@ -86,7 +85,11 @@ public class HelloPacketListener implements PluginMessageListener { return; } - activeAxiomPlayers.add(player.getUniqueId()); + this.plugin.activeAxiomPlayers.add(player.getUniqueId()); + int rateLimit = this.plugin.configuration.getInt("block-buffer-rate-limit"); + if (rateLimit > 0) { + this.plugin.playerBlockBufferRateLimiters.putIfAbsent(player.getUniqueId(), RateLimiter.create(rateLimit)); + } // Enable FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); diff --git a/src/main/java/com/moulberry/axiom/packet/SetBlockBufferPacketListener.java b/src/main/java/com/moulberry/axiom/packet/SetBlockBufferPacketListener.java index a13eb1b..21d29aa 100644 --- a/src/main/java/com/moulberry/axiom/packet/SetBlockBufferPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/SetBlockBufferPacketListener.java @@ -1,5 +1,6 @@ package com.moulberry.axiom.packet; +import com.google.common.util.concurrent.RateLimiter; import com.moulberry.axiom.AxiomPaper; import com.moulberry.axiom.WorldExtension; import com.moulberry.axiom.buffer.BiomeBuffer; @@ -10,12 +11,14 @@ import com.moulberry.axiom.integration.SectionPermissionChecker; import com.moulberry.axiom.integration.plotsquared.PlotSquaredIntegration; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.shorts.Short2ObjectMap; +import net.minecraft.ChatFormatting; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; import net.minecraft.core.Registry; import net.minecraft.core.SectionPos; import net.minecraft.core.registries.Registries; import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; import net.minecraft.network.protocol.game.ClientboundChunksBiomesPacket; import net.minecraft.resources.ResourceKey; import net.minecraft.server.MinecraftServer; @@ -36,18 +39,19 @@ import net.minecraft.world.level.chunk.LevelChunkSection; import net.minecraft.world.level.chunk.PalettedContainer; import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.level.lighting.LightEngine; -import org.bukkit.Bukkit; import org.bukkit.Location; import xyz.jpenilla.reflectionremapper.ReflectionRemapper; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; public class SetBlockBufferPacketListener { private final AxiomPaper plugin; private final Method updateBlockEntityTicker; + private final WeakHashMap packetRateLimiter = new WeakHashMap<>(); public SetBlockBufferPacketListener(AxiomPaper plugin) { this.plugin = plugin; @@ -76,12 +80,38 @@ public class SetBlockBufferPacketListener { friendlyByteBuf.readNbt(); // Discard sourceInfo } + RateLimiter rateLimiter = this.plugin.getBlockBufferRateLimiter(player.getUUID()); + byte type = friendlyByteBuf.readByte(); if (type == 0) { - BlockBuffer buffer = BlockBuffer.load(friendlyByteBuf); + AtomicBoolean reachedRateLimit = new AtomicBoolean(false); + BlockBuffer buffer = BlockBuffer.load(friendlyByteBuf, rateLimiter, reachedRateLimit); + if (reachedRateLimit.get()) { + player.sendSystemMessage(Component.literal("[Axiom] Exceeded server rate-limit of " + (int)rateLimiter.getRate() + " sections per second") + .withStyle(ChatFormatting.RED)); + } + + if (this.plugin.logLargeBlockBufferChanges()) { + this.plugin.getLogger().info("Player " + player.getUUID() + " modified " + buffer.entrySet().size() + " chunk sections (blocks)"); + if (buffer.getTotalBlockEntities() > 0) { + this.plugin.getLogger().info("Player " + player.getUUID() + " modified " + buffer.getTotalBlockEntities() + " block entities, compressed bytes = " + + buffer.getTotalBlockEntityBytes()); + } + } + applyBlockBuffer(player, server, buffer, worldKey); } else if (type == 1) { - BiomeBuffer buffer = BiomeBuffer.load(friendlyByteBuf); + AtomicBoolean reachedRateLimit = new AtomicBoolean(false); + BiomeBuffer buffer = BiomeBuffer.load(friendlyByteBuf, rateLimiter, reachedRateLimit); + if (reachedRateLimit.get()) { + player.sendSystemMessage(Component.literal("[Axiom] Exceeded server rate-limit of " + (int)rateLimiter.getRate() + " sections per second") + .withStyle(ChatFormatting.RED)); + } + + if (this.plugin.logLargeBlockBufferChanges()) { + this.plugin.getLogger().info("Player " + player.getUUID() + " modified " + buffer.size() + " chunk sections (biomes)"); + } + applyBiomeBuffer(player, server, buffer, worldKey); } else { throw new RuntimeException("Unknown buffer type: " + type); @@ -92,245 +122,252 @@ public class SetBlockBufferPacketListener { private void applyBlockBuffer(ServerPlayer player, MinecraftServer server, BlockBuffer buffer, ResourceKey worldKey) { server.execute(() -> { - ServerLevel world = player.serverLevel(); - if (!world.dimension().equals(worldKey)) return; + try { + ServerLevel world = player.serverLevel(); + if (!world.dimension().equals(worldKey)) return; - if (!this.plugin.canUseAxiom(player.getBukkitEntity())) { - return; - } - - if (!this.plugin.canModifyWorld(player.getBukkitEntity(), world.getWorld())) { - return; - } - - // Allowed, apply buffer - BlockPos.MutableBlockPos blockPos = new BlockPos.MutableBlockPos(); - WorldExtension extension = WorldExtension.get(world); - - BlockState emptyState = BlockBuffer.EMPTY_STATE; - - for (Long2ObjectMap.Entry> entry : buffer.entrySet()) { - int cx = BlockPos.getX(entry.getLongKey()); - int cy = BlockPos.getY(entry.getLongKey()); - int cz = BlockPos.getZ(entry.getLongKey()); - PalettedContainer container = entry.getValue(); - - if (cy < world.getMinSection() || cy >= world.getMaxSection()) { - continue; + if (!this.plugin.canUseAxiom(player.getBukkitEntity())) { + return; } - SectionPermissionChecker checker = PlotSquaredIntegration.checkSection(player.getBukkitEntity(), world.getWorld(), cx, cy, cz); - if (checker != null && checker.noneAllowed()) { - continue; + if (!this.plugin.canModifyWorld(player.getBukkitEntity(), world.getWorld())) { + return; } - LevelChunk chunk = world.getChunk(cx, cz); + // Allowed, apply buffer + BlockPos.MutableBlockPos blockPos = new BlockPos.MutableBlockPos(); + WorldExtension extension = WorldExtension.get(world); - LevelChunkSection section = chunk.getSection(world.getSectionIndexFromSectionY(cy)); - PalettedContainer sectionStates = section.getStates(); - boolean hasOnlyAir = section.hasOnlyAir(); + BlockState emptyState = BlockBuffer.EMPTY_STATE; - Heightmap worldSurface = null; - Heightmap oceanFloor = null; - Heightmap motionBlocking = null; - Heightmap motionBlockingNoLeaves = null; - for (Map.Entry heightmap : chunk.getHeightmaps()) { - switch (heightmap.getKey()) { - case WORLD_SURFACE -> worldSurface = heightmap.getValue(); - case OCEAN_FLOOR -> oceanFloor = heightmap.getValue(); - case MOTION_BLOCKING -> motionBlocking = heightmap.getValue(); - case MOTION_BLOCKING_NO_LEAVES -> motionBlockingNoLeaves = heightmap.getValue(); - default -> {} + for (Long2ObjectMap.Entry> entry : buffer.entrySet()) { + int cx = BlockPos.getX(entry.getLongKey()); + int cy = BlockPos.getY(entry.getLongKey()); + int cz = BlockPos.getZ(entry.getLongKey()); + PalettedContainer container = entry.getValue(); + + if (cy < world.getMinSection() || cy >= world.getMaxSection()) { + continue; } - } - boolean sectionChanged = false; - boolean sectionLightChanged = false; - - boolean containerMaybeHasPoi = container.maybeHas(PoiTypes::hasPoi); - boolean sectionMaybeHasPoi = section.maybeHas(PoiTypes::hasPoi); - - Short2ObjectMap blockEntityChunkMap = buffer.getBlockEntityChunkMap(entry.getLongKey()); - - int minX = 0; - int minY = 0; - int minZ = 0; - int maxX = 15; - int maxY = 15; - int maxZ = 15; - - if (checker != null) { - minX = checker.bounds().minX(); - minY = checker.bounds().minY(); - minZ = checker.bounds().minZ(); - maxX = checker.bounds().maxX(); - maxY = checker.bounds().maxY(); - maxZ = checker.bounds().maxZ(); - if (checker.allAllowed()) { - checker = null; + SectionPermissionChecker checker = PlotSquaredIntegration.checkSection(player.getBukkitEntity(), world.getWorld(), cx, cy, cz); + if (checker != null && checker.noneAllowed()) { + continue; } - } - for (int x = minX; x <= maxX; x++) { - for (int y = minY; y <= maxY; y++) { - for (int z = minZ; z <= maxZ; z++) { - BlockState blockState = container.get(x, y, z); - if (blockState == emptyState) continue; + LevelChunk chunk = world.getChunk(cx, cz); - int bx = cx*16 + x; - int by = cy*16 + y; - int bz = cz*16 + z; + LevelChunkSection section = chunk.getSection(world.getSectionIndexFromSectionY(cy)); + PalettedContainer sectionStates = section.getStates(); + boolean hasOnlyAir = section.hasOnlyAir(); - if (hasOnlyAir && blockState.isAir()) { - continue; - } + Heightmap worldSurface = null; + Heightmap oceanFloor = null; + Heightmap motionBlocking = null; + Heightmap motionBlockingNoLeaves = null; + for (Map.Entry heightmap : chunk.getHeightmaps()) { + switch (heightmap.getKey()) { + case WORLD_SURFACE -> worldSurface = heightmap.getValue(); + case OCEAN_FLOOR -> oceanFloor = heightmap.getValue(); + case MOTION_BLOCKING -> motionBlocking = heightmap.getValue(); + case MOTION_BLOCKING_NO_LEAVES -> motionBlockingNoLeaves = heightmap.getValue(); + default -> {} + } + } - if (checker != null && !checker.allowed(x, y, z)) continue; + boolean sectionChanged = false; + boolean sectionLightChanged = false; - BlockState old = section.setBlockState(x, y, z, blockState, true); - if (blockState != old) { - sectionChanged = true; - blockPos.set(bx, by, bz); + boolean containerMaybeHasPoi = container.maybeHas(PoiTypes::hasPoi); + boolean sectionMaybeHasPoi = section.maybeHas(PoiTypes::hasPoi); - Block block = blockState.getBlock(); - motionBlocking.update(x, by, z, blockState); - motionBlockingNoLeaves.update(x, by, z, blockState); - oceanFloor.update(x, by, z, blockState); - worldSurface.update(x, by, z, blockState); + Short2ObjectMap blockEntityChunkMap = buffer.getBlockEntityChunkMap(entry.getLongKey()); - if (false) { // Full update - old.onRemove(world, blockPos, blockState, false); + int minX = 0; + int minY = 0; + int minZ = 0; + int maxX = 15; + int maxY = 15; + int maxZ = 15; - if (sectionStates.get(x, y, z).is(block)) { - blockState.onPlace(world, blockPos, old, false); - } + if (checker != null) { + minX = checker.bounds().minX(); + minY = checker.bounds().minY(); + minZ = checker.bounds().minZ(); + maxX = checker.bounds().maxX(); + maxY = checker.bounds().maxY(); + maxZ = checker.bounds().maxZ(); + if (checker.allAllowed()) { + checker = null; + } + } + + for (int x = minX; x <= maxX; x++) { + for (int y = minY; y <= maxY; y++) { + for (int z = minZ; z <= maxZ; z++) { + BlockState blockState = container.get(x, y, z); + if (blockState == emptyState) continue; + + int bx = cx*16 + x; + int by = cy*16 + y; + int bz = cz*16 + z; + + if (hasOnlyAir && blockState.isAir()) { + continue; } - if (blockState.hasBlockEntity()) { - BlockEntity blockEntity = chunk.getBlockEntity(blockPos, LevelChunk.EntityCreationType.CHECK); + if (checker != null && !checker.allowed(x, y, z)) continue; - if (blockEntity == null) { - // There isn't a block entity here, create it! - blockEntity = ((EntityBlock)block).newBlockEntity(blockPos, blockState); - if (blockEntity != null) { - chunk.addAndRegisterBlockEntity(blockEntity); - } - } else if (blockEntity.getType().isValid(blockState)) { - // Block entity is here and the type is correct - blockEntity.setBlockState(blockState); + BlockState old = section.setBlockState(x, y, z, blockState, true); + if (blockState != old) { + sectionChanged = true; + blockPos.set(bx, by, bz); - try { - this.updateBlockEntityTicker.invoke(chunk, blockEntity); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); + Block block = blockState.getBlock(); + motionBlocking.update(x, by, z, blockState); + motionBlockingNoLeaves.update(x, by, z, blockState); + oceanFloor.update(x, by, z, blockState); + worldSurface.update(x, by, z, blockState); + + if (false) { // Full update + old.onRemove(world, blockPos, blockState, false); + + if (sectionStates.get(x, y, z).is(block)) { + blockState.onPlace(world, blockPos, old, false); } - } else { - // Block entity type isn't correct, we need to recreate it + } + + if (blockState.hasBlockEntity()) { + BlockEntity blockEntity = chunk.getBlockEntity(blockPos, LevelChunk.EntityCreationType.CHECK); + + if (blockEntity == null) { + // There isn't a block entity here, create it! + blockEntity = ((EntityBlock)block).newBlockEntity(blockPos, blockState); + if (blockEntity != null) { + chunk.addAndRegisterBlockEntity(blockEntity); + } + } else if (blockEntity.getType().isValid(blockState)) { + // Block entity is here and the type is correct + blockEntity.setBlockState(blockState); + + try { + this.updateBlockEntityTicker.invoke(chunk, blockEntity); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } else { + // Block entity type isn't correct, we need to recreate it + chunk.removeBlockEntity(blockPos); + + blockEntity = ((EntityBlock)block).newBlockEntity(blockPos, blockState); + if (blockEntity != null) { + chunk.addAndRegisterBlockEntity(blockEntity); + } + } + if (blockEntity != null && blockEntityChunkMap != null) { + int key = x | (y << 4) | (z << 8); + CompressedBlockEntity savedBlockEntity = blockEntityChunkMap.get((short) key); + if (savedBlockEntity != null) { + blockEntity.load(savedBlockEntity.decompress()); + } + } + } else if (old.hasBlockEntity()) { chunk.removeBlockEntity(blockPos); - - blockEntity = ((EntityBlock)block).newBlockEntity(blockPos, blockState); - if (blockEntity != null) { - chunk.addAndRegisterBlockEntity(blockEntity); - } } - if (blockEntity != null && blockEntityChunkMap != null) { - int key = x | (y << 4) | (z << 8); - CompressedBlockEntity savedBlockEntity = blockEntityChunkMap.get((short) key); - if (savedBlockEntity != null) { - blockEntity.load(savedBlockEntity.decompress()); - } + + // Update Light + sectionLightChanged |= LightEngine.hasDifferentLightProperties(chunk, blockPos, old, blockState); + + // Update Poi + Optional> newPoi = containerMaybeHasPoi ? PoiTypes.forState(blockState) : Optional.empty(); + Optional> oldPoi = sectionMaybeHasPoi ? PoiTypes.forState(old) : Optional.empty(); + if (!Objects.equals(oldPoi, newPoi)) { + if (oldPoi.isPresent()) world.getPoiManager().remove(blockPos); + if (newPoi.isPresent()) world.getPoiManager().add(blockPos, newPoi.get()); } - } else if (old.hasBlockEntity()) { - chunk.removeBlockEntity(blockPos); - } - - // Update Light - sectionLightChanged |= LightEngine.hasDifferentLightProperties(chunk, blockPos, old, blockState); - - // Update Poi - Optional> newPoi = containerMaybeHasPoi ? PoiTypes.forState(blockState) : Optional.empty(); - Optional> oldPoi = sectionMaybeHasPoi ? PoiTypes.forState(old) : Optional.empty(); - if (!Objects.equals(oldPoi, newPoi)) { - if (oldPoi.isPresent()) world.getPoiManager().remove(blockPos); - if (newPoi.isPresent()) world.getPoiManager().add(blockPos, newPoi.get()); } } } } - } - boolean nowHasOnlyAir = section.hasOnlyAir(); - if (hasOnlyAir != nowHasOnlyAir) { - world.getChunkSource().getLightEngine().updateSectionStatus(SectionPos.of(cx, cy, cz), nowHasOnlyAir); - } + boolean nowHasOnlyAir = section.hasOnlyAir(); + if (hasOnlyAir != nowHasOnlyAir) { + world.getChunkSource().getLightEngine().updateSectionStatus(SectionPos.of(cx, cy, cz), nowHasOnlyAir); + } - if (sectionChanged) { - extension.sendChunk(cx, cz); - chunk.setUnsaved(true); - } - if (sectionLightChanged) { - extension.lightChunk(cx, cz); + if (sectionChanged) { + extension.sendChunk(cx, cz); + chunk.setUnsaved(true); + } + if (sectionLightChanged) { + extension.lightChunk(cx, cz); + } } + } catch (Throwable t) { + player.getBukkitEntity().kick(net.kyori.adventure.text.Component.text("An error occured while processing block change: " + t.getMessage())); } }); } - private void applyBiomeBuffer(ServerPlayer player, MinecraftServer server, BiomeBuffer biomeBuffer, ResourceKey worldKey) { server.execute(() -> { - ServerLevel world = player.serverLevel(); - if (!world.dimension().equals(worldKey)) return; + try { + ServerLevel world = player.serverLevel(); + if (!world.dimension().equals(worldKey)) return; - if (!this.plugin.canUseAxiom(player.getBukkitEntity())) { - return; - } - - if (!this.plugin.canModifyWorld(player.getBukkitEntity(), world.getWorld())) { - return; - } - - Set changedChunks = new HashSet<>(); - - int minSection = world.getMinSection(); - int maxSection = world.getMaxSection(); - - Optional> registryOptional = world.registryAccess().registry(Registries.BIOME); - if (registryOptional.isEmpty()) return; - - Registry registry = registryOptional.get(); - - biomeBuffer.forEachEntry((x, y, z, biome) -> { - int cy = y >> 2; - if (cy < minSection || cy >= maxSection) { + if (!this.plugin.canUseAxiom(player.getBukkitEntity())) { return; } - var chunk = (LevelChunk) world.getChunk(x >> 2, z >> 2, ChunkStatus.FULL, false); - if (chunk == null) return; + if (!this.plugin.canModifyWorld(player.getBukkitEntity(), world.getWorld())) { + return; + } - var section = chunk.getSection(cy - minSection); - PalettedContainer> container = (PalettedContainer>) section.getBiomes(); + Set changedChunks = new HashSet<>(); - var holder = registry.getHolder(biome); - if (holder.isPresent()) { - if (!PlotSquaredIntegration.canPlaceBlock(player.getBukkitEntity(), + int minSection = world.getMinSection(); + int maxSection = world.getMaxSection(); + + Optional> registryOptional = world.registryAccess().registry(Registries.BIOME); + if (registryOptional.isEmpty()) return; + + Registry registry = registryOptional.get(); + + biomeBuffer.forEachEntry((x, y, z, biome) -> { + int cy = y >> 2; + if (cy < minSection || cy >= maxSection) { + return; + } + + var chunk = (LevelChunk) world.getChunk(x >> 2, z >> 2, ChunkStatus.FULL, false); + if (chunk == null) return; + + var section = chunk.getSection(cy - minSection); + PalettedContainer> container = (PalettedContainer>) section.getBiomes(); + + var holder = registry.getHolder(biome); + if (holder.isPresent()) { + if (!PlotSquaredIntegration.canPlaceBlock(player.getBukkitEntity(), new Location(player.getBukkitEntity().getWorld(), x+1, y+1, z+1))) return; - container.set(x & 3, y & 3, z & 3, holder.get()); - changedChunks.add(chunk); - } - }); + container.set(x & 3, y & 3, z & 3, holder.get()); + changedChunks.add(chunk); + } + }); - var chunkMap = world.getChunkSource().chunkMap; - HashMap> map = new HashMap<>(); - for (LevelChunk chunk : changedChunks) { - chunk.setUnsaved(true); - ChunkPos chunkPos = chunk.getPos(); - for (ServerPlayer serverPlayer2 : chunkMap.getPlayers(chunkPos, false)) { - map.computeIfAbsent(serverPlayer2, serverPlayer -> new ArrayList<>()).add(chunk); + var chunkMap = world.getChunkSource().chunkMap; + HashMap> map = new HashMap<>(); + for (LevelChunk chunk : changedChunks) { + chunk.setUnsaved(true); + ChunkPos chunkPos = chunk.getPos(); + for (ServerPlayer serverPlayer2 : chunkMap.getPlayers(chunkPos, false)) { + map.computeIfAbsent(serverPlayer2, serverPlayer -> new ArrayList<>()).add(chunk); + } } + map.forEach((serverPlayer, list) -> serverPlayer.connection.send(ClientboundChunksBiomesPacket.forChunks(list))); + } catch (Throwable t) { + player.getBukkitEntity().kick(net.kyori.adventure.text.Component.text("An error occured while processing biome change: " + t.getMessage())); } - map.forEach((serverPlayer, list) -> serverPlayer.connection.send(ClientboundChunksBiomesPacket.forChunks(list))); }); } diff --git a/src/main/java/com/moulberry/axiom/packet/SetBlockPacketListener.java b/src/main/java/com/moulberry/axiom/packet/SetBlockPacketListener.java index ed1ae7d..a4ec05c 100644 --- a/src/main/java/com/moulberry/axiom/packet/SetBlockPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/SetBlockPacketListener.java @@ -80,7 +80,7 @@ public class SetBlockPacketListener implements PluginMessageListener { FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); IntFunction> mapFunction = FriendlyByteBuf.limitValue(Maps::newLinkedHashMapWithExpectedSize, 512); Map blocks = friendlyByteBuf.readMap(mapFunction, - FriendlyByteBuf::readBlockPos, buf -> buf.readById(Block.BLOCK_STATE_REGISTRY)); + FriendlyByteBuf::readBlockPos, buf -> buf.readById(this.plugin.allowedBlockRegistry)); boolean updateNeighbors = friendlyByteBuf.readBoolean(); int reason = friendlyByteBuf.readVarInt(); diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 094b641..edf4bc7 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -28,6 +28,17 @@ whitelist-world-regex: null # If the regex matches the world's name, the world can't be modified blacklist-world-regex: null +# Block buffer rate-limit (in chunk sections per second), 0 = no limit +block-buffer-rate-limit: 0 + +# Log large block buffer changes +log-large-block-buffer-changes: false + +# Disallowed blocks +disallowed-blocks: +# - "minecraft:wheat" +# - "minecraft:oak_stairs[waterlogged=true]" + # Toggles for individual packet handlers. May break certain features packet-handlers: hello: true