diff --git a/src/main/java/com/moulberry/axiom/AxiomPaper.java b/src/main/java/com/moulberry/axiom/AxiomPaper.java index 0e960c0..817a488 100644 --- a/src/main/java/com/moulberry/axiom/AxiomPaper.java +++ b/src/main/java/com/moulberry/axiom/AxiomPaper.java @@ -1,11 +1,10 @@ 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; +import com.moulberry.axiom.integration.plotsquared.PlotSquaredIntegration; import com.moulberry.axiom.packet.*; import com.moulberry.axiom.world_properties.server.ServerWorldPropertiesRegistry; import io.netty.buffer.Unpooled; @@ -15,11 +14,8 @@ 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.BlockPos; 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; @@ -27,8 +23,6 @@ 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; @@ -43,8 +37,6 @@ 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 { @@ -52,6 +44,7 @@ public class AxiomPaper extends JavaPlugin implements Listener { public final Set activeAxiomPlayers = Collections.newSetFromMap(new ConcurrentHashMap<>()); public final Map playerBlockBufferRateLimiters = new ConcurrentHashMap<>(); + public final Map playerRestrictions = new ConcurrentHashMap<>(); public Configuration configuration; public IdMapper allowedBlockRegistry = null; @@ -90,6 +83,7 @@ public class AxiomPaper extends JavaPlugin implements Listener { msg.registerOutgoingPluginChannel(this, "axiom:register_world_properties"); msg.registerOutgoingPluginChannel(this, "axiom:set_world_property"); msg.registerOutgoingPluginChannel(this, "axiom:ack_world_properties"); + msg.registerOutgoingPluginChannel(this, "axiom:restrictions"); if (configuration.getBoolean("packet-handlers.hello")) { msg.registerIncomingPluginChannel(this, "axiom:hello", new HelloPacketListener(this)); @@ -162,6 +156,13 @@ public class AxiomPaper extends JavaPlugin implements Listener { Bukkit.getScheduler().scheduleSyncRepeatingTask(this, () -> { HashSet stillActiveAxiomPlayers = new HashSet<>(); + int rateLimit = this.configuration.getInt("block-buffer-rate-limit"); + if (rateLimit > 0) { + // Reduce by 20% just to prevent synchronization/timing issues + rateLimit = rateLimit * 8/10; + if (rateLimit <= 0) rateLimit = 1; + } + for (Player player : Bukkit.getServer().getOnlinePlayers()) { if (activeAxiomPlayers.contains(player.getUniqueId())) { if (!player.hasPermission("axiom.*")) { @@ -171,13 +172,70 @@ public class AxiomPaper extends JavaPlugin implements Listener { buf.getBytes(0, bytes); player.sendPluginMessage(this, "axiom:enable", bytes); } else { - stillActiveAxiomPlayers.add(player.getUniqueId()); + UUID uuid = player.getUniqueId(); + stillActiveAxiomPlayers.add(uuid); + + boolean send = false; + + Restrictions restrictions = playerRestrictions.get(uuid); + if (restrictions == null) { + restrictions = new Restrictions(); + playerRestrictions.put(uuid, restrictions); + send = true; + } + + BlockPos boundsMin = null; + BlockPos boundsMax = null; + + if (!player.hasPermission("axiom.allow_copying_other_plots")) { + if (PlotSquaredIntegration.isPlotWorld(player.getWorld())) { + PlotSquaredIntegration.PlotBounds editable = PlotSquaredIntegration.getCurrentEditablePlot(player); + if (editable != null) { + restrictions.lastPlotBounds = editable; + boundsMin = editable.min(); + boundsMax = editable.max(); + } else if (restrictions.lastPlotBounds != null && restrictions.lastPlotBounds.worldName().equals(player.getWorld().getName())) { + boundsMin = restrictions.lastPlotBounds.min(); + boundsMax = restrictions.lastPlotBounds.max(); + } else { + boundsMin = BlockPos.ZERO; + boundsMax = BlockPos.ZERO; + } + } + + int min = Integer.MIN_VALUE; + int max = Integer.MAX_VALUE; + if (boundsMin != null && boundsMax != null && + boundsMin.getX() == min && boundsMin.getY() == min && boundsMin.getZ() == min && + boundsMax.getX() == max && boundsMax.getY() == max && boundsMax.getZ() == max) { + boundsMin = null; + boundsMax = null; + } + } + + boolean allowImportingBlocks = player.hasPermission("axiom.can_import_blocks"); + + if (restrictions.maxSectionsPerSecond != rateLimit || + restrictions.canImportBlocks != allowImportingBlocks || + !Objects.equals(restrictions.boundsMin, boundsMin) || + !Objects.equals(restrictions.boundsMax, boundsMax)) { + restrictions.maxSectionsPerSecond = rateLimit; + restrictions.canImportBlocks = allowImportingBlocks; + restrictions.boundsMin = boundsMin; + restrictions.boundsMax = boundsMax; + send = true; + } + + if (send) { + restrictions.send(this, player); + } } } } activeAxiomPlayers.retainAll(stillActiveAxiomPlayers); playerBlockBufferRateLimiters.keySet().retainAll(stillActiveAxiomPlayers); + playerRestrictions.keySet().retainAll(stillActiveAxiomPlayers); }, 20, 20); int maxChunkRelightsPerTick = configuration.getInt("max-chunk-relights-per-tick"); diff --git a/src/main/java/com/moulberry/axiom/Restrictions.java b/src/main/java/com/moulberry/axiom/Restrictions.java new file mode 100644 index 0000000..5dd0180 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/Restrictions.java @@ -0,0 +1,64 @@ +package com.moulberry.axiom; + +import com.moulberry.axiom.integration.plotsquared.PlotSquaredIntegration; +import io.netty.buffer.Unpooled; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import org.bukkit.entity.Player; + +public class Restrictions { + + public boolean canImportBlocks = true; + public boolean canUseEditor = true; + public boolean canEditDisplayEntities = true; + public int maxSectionsPerSecond = 0; + public BlockPos boundsMin = null; + public BlockPos boundsMax = null; + + public PlotSquaredIntegration.PlotBounds lastPlotBounds = null; + + public void send(AxiomPaper plugin, Player player) { + if (player.getListeningPluginChannels().contains("axiom:restrictions")) { + FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + buf.writeBoolean(this.canImportBlocks); + buf.writeBoolean(this.canUseEditor); + buf.writeBoolean(this.canEditDisplayEntities); + + buf.writeVarInt(this.maxSectionsPerSecond); + + if (this.boundsMin == null || this.boundsMax == null) { + buf.writeBoolean(false); + } else { + buf.writeBoolean(true); + int minX = this.boundsMin.getX(); + int minY = this.boundsMin.getY(); + int minZ = this.boundsMin.getZ(); + int maxX = this.boundsMax.getX(); + int maxY = this.boundsMax.getY(); + int maxZ = this.boundsMax.getZ(); + + if (minX < -33554431) minX = -33554431; + if (minX > 33554431) minX = 33554431; + if (minY < -2047) minY = -2047; + if (minY > 2047) minY = 2047; + if (minZ < -33554431) minZ = -33554431; + if (minZ > 33554431) minZ = 33554431; + + if (maxX < -33554431) maxX = -33554431; + if (maxX > 33554431) maxX = 33554431; + if (maxY < -2047) maxY = -2047; + if (maxY > 2047) maxY = 2047; + if (maxZ < -33554431) maxZ = -33554431; + if (maxZ > 33554431) maxZ = 33554431; + + buf.writeBlockPos(new BlockPos(minX, minY, minZ)); + buf.writeBlockPos(new BlockPos(maxX, maxY, maxZ)); + } + + byte[] bytes = new byte[buf.writerIndex()]; + buf.getBytes(0, bytes); + player.sendPluginMessage(plugin, "axiom:restrictions", bytes); + } + } + +} diff --git a/src/main/java/com/moulberry/axiom/integration/plotsquared/PlotSquaredIntegration.java b/src/main/java/com/moulberry/axiom/integration/plotsquared/PlotSquaredIntegration.java index e307a29..deab2cf 100644 --- a/src/main/java/com/moulberry/axiom/integration/plotsquared/PlotSquaredIntegration.java +++ b/src/main/java/com/moulberry/axiom/integration/plotsquared/PlotSquaredIntegration.java @@ -2,7 +2,10 @@ package com.moulberry.axiom.integration.plotsquared; import com.moulberry.axiom.integration.SectionPermissionChecker; +import com.sk89q.worldedit.regions.CuboidRegion; +import net.minecraft.core.BlockPos; import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.entity.Player; @@ -10,6 +13,24 @@ import org.bukkit.entity.Player; public class PlotSquaredIntegration { + public record PlotBounds(BlockPos min, BlockPos max, String worldName) { + public PlotBounds(CuboidRegion cuboidRegion, String worldName) { + this( + new BlockPos( + cuboidRegion.getMinimumPoint().getBlockX(), + cuboidRegion.getMinimumPoint().getBlockY(), + cuboidRegion.getMinimumPoint().getBlockZ() + ), + new BlockPos( + cuboidRegion.getMaximumPoint().getBlockX(), + cuboidRegion.getMaximumPoint().getBlockY(), + cuboidRegion.getMaximumPoint().getBlockZ() + ), + worldName + ); + } + } + public static boolean canBreakBlock(Player player, Block block) { if (!Bukkit.getPluginManager().isPluginEnabled("PlotSquared")) { return true; @@ -31,6 +52,13 @@ public class PlotSquaredIntegration { return PlotSquaredIntegrationImpl.isPlotWorld(world); } + public static PlotSquaredIntegration.PlotBounds getCurrentEditablePlot(Player player) { + if (!Bukkit.getPluginManager().isPluginEnabled("PlotSquared")) { + return null; + } + return PlotSquaredIntegrationImpl.getCurrentEditablePlot(player); + } + public static SectionPermissionChecker checkSection(Player player, World world, int sectionX, int sectionY, int sectionZ) { if (!Bukkit.getPluginManager().isPluginEnabled("PlotSquared")) { return SectionPermissionChecker.ALL_ALLOWED; diff --git a/src/main/java/com/moulberry/axiom/integration/plotsquared/PlotSquaredIntegrationImpl.java b/src/main/java/com/moulberry/axiom/integration/plotsquared/PlotSquaredIntegrationImpl.java index e530e0d..a9d6e3d 100644 --- a/src/main/java/com/moulberry/axiom/integration/plotsquared/PlotSquaredIntegrationImpl.java +++ b/src/main/java/com/moulberry/axiom/integration/plotsquared/PlotSquaredIntegrationImpl.java @@ -135,6 +135,52 @@ public class PlotSquaredIntegrationImpl { return isPlotWorld; } + static PlotSquaredIntegration.PlotBounds getCurrentEditablePlot(Player player) { + org.bukkit.Location loc = player.getLocation(); + + Location location = BukkitUtil.adapt(loc); + PlotArea area = location.getPlotArea(); + if (area == null) { + return null; + } + + BukkitPlayer pp = BukkitUtil.adapt(player); + Plot plot = area.getPlot(location); + if (plot != null) { + Location bottom = plot.getExtendedBottomAbs(); + Location top = plot.getExtendedTopAbs(); + CuboidRegion cuboidRegion = new CuboidRegion(bottom.getBlockVector3(), top.getBlockVector3()); + + // check unowned plots + if (!plot.hasOwner()) { + if (!pp.hasPermission(Permission.PERMISSION_ADMIN_BUILD_UNOWNED, false)) { + return null; + } else { + return new PlotSquaredIntegration.PlotBounds(cuboidRegion, player.getWorld().getName()); + } + } + // player is breaking another player's plot + if (!plot.isAdded(pp.getUUID())) { + if (!pp.hasPermission(Permission.PERMISSION_ADMIN_BUILD_OTHER, false)) { + return null; + } else { + return new PlotSquaredIntegration.PlotBounds(cuboidRegion, player.getWorld().getName()); + } + } + // plot is 'done' + if (Settings.Done.RESTRICT_BUILDING && DoneFlag.isDone(plot)) { + if (!pp.hasPermission(Permission.PERMISSION_ADMIN_BUILD_OTHER, false)) { + return null; + } else { + return new PlotSquaredIntegration.PlotBounds(cuboidRegion, player.getWorld().getName()); + } + } + return new PlotSquaredIntegration.PlotBounds(cuboidRegion, player.getWorld().getName()); + } + + return null; + } + static SectionPermissionChecker checkSection(Player player, World world, int sectionX, int sectionY, int sectionZ) { int minX = sectionX * 16; int minY = sectionY * 16; @@ -220,4 +266,4 @@ public class PlotSquaredIntegrationImpl { return SectionPermissionChecker.fromAllowedBoxes(allowed); } -} \ No newline at end of file +} diff --git a/src/main/java/com/moulberry/axiom/packet/HelloPacketListener.java b/src/main/java/com/moulberry/axiom/packet/HelloPacketListener.java index 5f60988..0344e6b 100644 --- a/src/main/java/com/moulberry/axiom/packet/HelloPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/HelloPacketListener.java @@ -77,6 +77,20 @@ public class HelloPacketListener implements PluginMessageListener { } } + if (!player.getListeningPluginChannels().contains("axiom:restrictions")) { + Component text = Component.text("This server requires the use of Axiom 2.3 or later. Contact the server administrator if you believe this is unintentional"); + + String unsupportedRestrictions = plugin.configuration.getString("client-doesnt-support-restrictions"); + if (unsupportedRestrictions == null) unsupportedRestrictions = "kick"; + if (unsupportedRestrictions.equals("warn")) { + player.sendMessage(text.color(NamedTextColor.RED)); + return; + } else if (!unsupportedRestrictions.equals("ignore")) { + player.kick(text); + return; + } + } + // Call handshake event int maxBufferSize = plugin.configuration.getInt("max-block-buffer-packet-size"); AxiomHandshakeEvent handshakeEvent = new AxiomHandshakeEvent(player, maxBufferSize); diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index bf42371..7e87583 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -13,9 +13,12 @@ allow-teleport-between-worlds: true # Action to take when a user with an incompatible Minecraft version or Axiom version joins # Valid actions are 'kick', 'warn' and 'ignore' +# 'warn' will give the player a warning and disable Axiom # Using 'ignore' may result in corruption and is only provided for debugging purposes -incompatible-data-version: "kick" -unsupported-axiom-version: "kick" +incompatible-data-version: "warn" +unsupported-axiom-version: "warn" +client-doesnt-support-restrictions: "ignore" + # Maximum packet size. Must not be less than 32767 max-block-buffer-packet-size: 0x100000 @@ -36,13 +39,13 @@ log-large-block-buffer-changes: false # Whitelist entities that can be spawned/manipulated/deleted by the client whitelist-entities: - - "minecraft:item_display" - - "minecraft:block_display" - - "minecraft:text_display" - - "minecraft:painting" - - "minecraft:armor_stand" - - "minecraft:item_frame" - - "minecraft:glow_item_frame" +# - "minecraft:item_display" +# - "minecraft:block_display" +# - "minecraft:text_display" +# - "minecraft:painting" +# - "minecraft:armor_stand" +# - "minecraft:item_frame" +# - "minecraft:glow_item_frame" # Blacklist entities that can be spawned/manipulated/deleted by the client blacklist-entities: diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index f9b3b63..53267da 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -19,3 +19,10 @@ permissions: description: Allows entity manipulation axiom.entity.delete: description: Allows entity deletion + + axiom.allow_copying_other_plots: + description: This permission allows users to copy other user's plots + default: true + axiom.can_import_blocks: + description: Allows players to import schematics/blueprints into Axiom + default: true