diff --git a/README.md b/README.md index 707ea93..6806e14 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,6 @@ Serverside component for Axiom (todo: better readme) + +## Download +https://modrinth.com/plugin/axiom-paper-plugin/ diff --git a/build.gradle.kts b/build.gradle.kts index 648ac87..24ecb7a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,14 +1,14 @@ 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" } group = "com.moulberry.axiom" -version = "1.5.1" +version = "1.5.8" description = "Serverside component for Axiom on Paper" java { @@ -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 1704367..2ea9e70 100644 --- a/src/main/java/com/moulberry/axiom/AxiomPaper.java +++ b/src/main/java/com/moulberry/axiom/AxiomPaper.java @@ -1,10 +1,12 @@ package com.moulberry.axiom; +import com.google.common.util.concurrent.RateLimiter; 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 com.moulberry.axiom.world_properties.server.ServerWorldProperty; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.papermc.paper.event.player.PlayerFailMoveEvent; @@ -12,14 +14,20 @@ 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.core.BlockPos; +import net.minecraft.core.IdMapper; import net.minecraft.network.Connection; import net.minecraft.network.ConnectionProtocol; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.PacketFlow; import net.minecraft.network.protocol.game.ServerboundCustomPayloadPacket; -import net.minecraft.resources.ResourceLocation; -import org.bukkit.*; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.block.state.BlockState; +import org.bukkit.Bukkit; +import org.bukkit.GameRule; +import org.bukkit.World; +import org.bukkit.configuration.Configuration; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; @@ -37,11 +45,33 @@ 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 final Map playerRestrictions = new ConcurrentHashMap<>(); + public Configuration configuration; + + public IdMapper allowedBlockRegistry = null; + private boolean logLargeBlockBufferChanges = false; @Override public void onEnable() { PLUGIN = this; + this.saveDefaultConfig(); + configuration = this.getConfig(); + + Set validResolutions = Set.of("kick", "warn", "ignore"); + if (!validResolutions.contains(configuration.getString("incompatible-data-version"))) { + this.getLogger().warning("Invalid value for incompatible-data-version, expected 'kick', 'warn' or 'ignore'"); + } + if (!validResolutions.contains(configuration.getString("unsupported-axiom-version"))) { + 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); @@ -55,44 +85,90 @@ 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"); + msg.registerOutgoingPluginChannel(this, "axiom:marker_data"); + msg.registerOutgoingPluginChannel(this, "axiom:marker_nbt_response"); - msg.registerIncomingPluginChannel(this, "axiom:hello", new HelloPacketListener(this, activeAxiomPlayers)); - msg.registerIncomingPluginChannel(this, "axiom:set_gamemode", new SetGamemodePacketListener()); - msg.registerIncomingPluginChannel(this, "axiom:set_fly_speed", new SetFlySpeedPacketListener()); - msg.registerIncomingPluginChannel(this, "axiom:set_world_time", new SetTimePacketListener()); - msg.registerIncomingPluginChannel(this, "axiom:set_world_property", new SetWorldPropertyListener()); - msg.registerIncomingPluginChannel(this, "axiom:set_block", new SetBlockPacketListener(this)); - msg.registerIncomingPluginChannel(this, "axiom:set_hotbar_slot", new SetHotbarSlotPacketListener()); - msg.registerIncomingPluginChannel(this, "axiom:switch_active_hotbar", new SwitchActiveHotbarPacketListener()); - msg.registerIncomingPluginChannel(this, "axiom:teleport", new TeleportPacketListener()); - msg.registerIncomingPluginChannel(this, "axiom:set_editor_views", new SetEditorViewsPacketListener()); - msg.registerIncomingPluginChannel(this, "axiom:request_chunk_data", new RequestChunkDataPacketListener(this)); + if (configuration.getBoolean("packet-handlers.hello")) { + msg.registerIncomingPluginChannel(this, "axiom:hello", new HelloPacketListener(this)); + } + if (configuration.getBoolean("packet-handlers.set-gamemode")) { + msg.registerIncomingPluginChannel(this, "axiom:set_gamemode", new SetGamemodePacketListener(this)); + } + if (configuration.getBoolean("packet-handlers.set-fly-speed")) { + msg.registerIncomingPluginChannel(this, "axiom:set_fly_speed", new SetFlySpeedPacketListener(this)); + } + if (configuration.getBoolean("packet-handlers.set-world-time")) { + msg.registerIncomingPluginChannel(this, "axiom:set_world_time", new SetTimePacketListener(this)); + } + if (configuration.getBoolean("packet-handlers.set-world-property")) { + msg.registerIncomingPluginChannel(this, "axiom:set_world_property", new SetWorldPropertyListener(this)); + } + if (configuration.getBoolean("packet-handlers.set-single-block")) { + msg.registerIncomingPluginChannel(this, "axiom:set_block", new SetBlockPacketListener(this)); + } + if (configuration.getBoolean("packet-handlers.set-hotbar-slot")) { + msg.registerIncomingPluginChannel(this, "axiom:set_hotbar_slot", new SetHotbarSlotPacketListener(this)); + } + if (configuration.getBoolean("packet-handlers.switch-active-hotbar")) { + msg.registerIncomingPluginChannel(this, "axiom:switch_active_hotbar", new SwitchActiveHotbarPacketListener(this)); + } + if (configuration.getBoolean("packet-handlers.teleport")) { + msg.registerIncomingPluginChannel(this, "axiom:teleport", new TeleportPacketListener(this)); + } + if (configuration.getBoolean("packet-handlers.set-editor-views")) { + msg.registerIncomingPluginChannel(this, "axiom:set_editor_views", new SetEditorViewsPacketListener(this)); + } + if (configuration.getBoolean("packet-handlers.request-chunk-data")) { + msg.registerIncomingPluginChannel(this, "axiom:request_chunk_data", new RequestChunkDataPacketListener(this)); + } + if (configuration.getBoolean("packet-handlers.spawn-entity")) { + msg.registerIncomingPluginChannel(this, "axiom:spawn_entity", new SpawnEntityPacketListener(this)); + } + if (configuration.getBoolean("packet-handlers.manipulate-entity")) { + msg.registerIncomingPluginChannel(this, "axiom:manipulate_entity", new ManipulateEntityPacketListener(this)); + } + if (configuration.getBoolean("packet-handlers.delete-entity")) { + msg.registerIncomingPluginChannel(this, "axiom:delete_entity", new DeleteEntityPacketListener(this)); + } + if (configuration.getBoolean("packet-handlers.marker-nbt-request")) { + msg.registerIncomingPluginChannel(this, "axiom:marker_nbt_request", new MarkerNbtRequestPacketListener(this)); + } - SetBlockBufferPacketListener setBlockBufferPacketListener = new SetBlockBufferPacketListener(this); + if (configuration.getBoolean("packet-handlers.set-buffer")) { + SetBlockBufferPacketListener setBlockBufferPacketListener = new SetBlockBufferPacketListener(this); - ChannelInitializeListenerHolder.addListener(Key.key("axiom:handle_big_payload"), new ChannelInitializeListener() { - @Override - public void afterInitChannel(@NonNull Channel channel) { - var packets = ConnectionProtocol.PLAY.getPacketsByIds(PacketFlow.SERVERBOUND); - int payloadId = -1; - for (Map.Entry>> entry : packets.entrySet()) { - if (entry.getValue() == ServerboundCustomPayloadPacket.class) { - payloadId = entry.getKey(); - break; + ChannelInitializeListenerHolder.addListener(Key.key("axiom:handle_big_payload"), new ChannelInitializeListener() { + @Override + public void afterInitChannel(@NonNull Channel channel) { + var packets = ConnectionProtocol.PLAY.getPacketsByIds(PacketFlow.SERVERBOUND); + int payloadId = -1; + for (Map.Entry>> entry : packets.entrySet()) { + if (entry.getValue() == ServerboundCustomPayloadPacket.class) { + payloadId = entry.getKey(); + break; + } + } + if (payloadId < 0) { + throw new RuntimeException("Failed to find ServerboundCustomPayloadPacket id"); } - } - if (payloadId < 0) { - throw new RuntimeException("Failed to find ServerboundCustomPayloadPacket id"); - } - Connection connection = (Connection) channel.pipeline().get("packet_handler"); - channel.pipeline().addBefore("decoder", "axiom-big-payload-handler", - new AxiomBigPayloadHandler(payloadId, connection, setBlockBufferPacketListener)); - } - }); + Connection connection = (Connection) channel.pipeline().get("packet_handler"); + channel.pipeline().addBefore("decoder", "axiom-big-payload-handler", + new AxiomBigPayloadHandler(payloadId, connection, setBlockBufferPacketListener)); + } + }); + } Bukkit.getScheduler().scheduleSyncRepeatingTask(this, () -> { - HashSet newActiveAxiomPlayers = new HashSet<>(); + 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())) { @@ -103,19 +179,100 @@ public class AxiomPaper extends JavaPlugin implements Listener { buf.getBytes(0, bytes); player.sendPluginMessage(this, "axiom:enable", bytes); } else { - newActiveAxiomPlayers.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.clear(); - activeAxiomPlayers.addAll(newActiveAxiomPlayers); + activeAxiomPlayers.retainAll(stillActiveAxiomPlayers); + playerBlockBufferRateLimiters.keySet().retainAll(stillActiveAxiomPlayers); + playerRestrictions.keySet().retainAll(stillActiveAxiomPlayers); }, 20, 20); + + boolean sendMarkers = configuration.getBoolean("send-markers"); + int maxChunkRelightsPerTick = configuration.getInt("max-chunk-relights-per-tick"); + int maxChunkSendsPerTick = configuration.getInt("max-chunk-sends-per-tick"); + + Bukkit.getScheduler().scheduleSyncRepeatingTask(this, () -> { + WorldExtension.tick(MinecraftServer.getServer(), sendMarkers, maxChunkRelightsPerTick, maxChunkSendsPerTick); + }, 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 getWorldProperties(World world) { + public @Nullable ServerWorldPropertiesRegistry getWorldPropertiesIfPresent(World world) { + return worldProperties.get(world); + } + + public @Nullable ServerWorldPropertiesRegistry getOrCreateWorldProperties(World world) { if (worldProperties.containsKey(world)) { return worldProperties.get(world); } else { @@ -125,40 +282,57 @@ public class AxiomPaper extends JavaPlugin implements Listener { } } + public boolean canModifyWorld(Player player, World world) { + String whitelist = this.configuration.getString("whitelist-world-regex"); + if (whitelist != null && !world.getName().matches(whitelist)) { + return false; + } + + String blacklist = this.configuration.getString("blacklist-world-regex"); + if (blacklist != null && world.getName().matches(blacklist)) { + return false; + } + + AxiomModifyWorldEvent modifyWorldEvent = new AxiomModifyWorldEvent(player, world); + Bukkit.getPluginManager().callEvent(modifyWorldEvent); + return !modifyWorldEvent.isCancelled(); + } + @EventHandler public void onFailMove(PlayerFailMoveEvent event) { - if (event.getPlayer().hasPermission("axiom.*")) { - if (event.getFailReason() == PlayerFailMoveEvent.FailReason.MOVED_TOO_QUICKLY) { - event.setAllowed(true); // Support for arcball camera - } else if (event.getPlayer().isFlying()) { - event.setAllowed(true); // Support for noclip - } + if (!this.activeAxiomPlayers.contains(event.getPlayer().getUniqueId())) { + return; + } + if (event.getFailReason() == PlayerFailMoveEvent.FailReason.MOVED_TOO_QUICKLY) { + event.setAllowed(true); // Support for arcball camera + } else if (event.getPlayer().isFlying()) { + event.setAllowed(true); // Support for noclip } } @EventHandler public void onChangedWorld(PlayerChangedWorldEvent event) { + if (!this.activeAxiomPlayers.contains(event.getPlayer().getUniqueId())) { + return; + } + World world = event.getPlayer().getWorld(); - ServerWorldPropertiesRegistry properties = getWorldProperties(world); + ServerWorldPropertiesRegistry properties = getOrCreateWorldProperties(world); if (properties == null) { event.getPlayer().sendPluginMessage(this, "axiom:register_world_properties", new byte[]{0}); } else { properties.registerFor(this, event.getPlayer()); } + + WorldExtension.onPlayerJoin(world, event.getPlayer()); } @EventHandler public void onGameRuleChanged(WorldGameRuleChangeEvent event) { if (event.getGameRule() == GameRule.DO_WEATHER_CYCLE) { - ServerWorldPropertiesRegistry properties = getWorldProperties(event.getWorld()); - if (properties != null) { - ServerWorldProperty property = properties.getById(new ResourceLocation("axiom:pause_weather")); - if (property != null) { - ((ServerWorldProperty)property).setValue(event.getWorld(), !Boolean.parseBoolean(event.getValue())); - } - } + ServerWorldPropertiesRegistry.PAUSE_WEATHER.setValue(event.getWorld(), !Boolean.parseBoolean(event.getValue())); } } 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..930bbe2 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/DisallowedBlocks.java @@ -0,0 +1,87 @@ +package com.moulberry.axiom; + +import com.mojang.brigadier.StringReader; +import com.moulberry.axiom.buffer.BlockBuffer; +import net.minecraft.commands.arguments.blocks.BlockStateParser; +import net.minecraft.core.IdMapper; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.level.block.Block; +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/NbtSanitization.java b/src/main/java/com/moulberry/axiom/NbtSanitization.java new file mode 100644 index 0000000..c24c4df --- /dev/null +++ b/src/main/java/com/moulberry/axiom/NbtSanitization.java @@ -0,0 +1,63 @@ +package com.moulberry.axiom; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; + +import java.util.Set; + +public class NbtSanitization { + + private static final Set ALLOWED_KEYS = Set.of( + "id", // entity id + // generic + "Pos", + "Rotation", + "Invulnerable", + "CustomName", + "CustomNameVisible", + "Silent", + "NoGravity", + "Glowing", + "Tags", + "Passengers", + // marker + "data", + // display entity + "transformation", + "interpolation_duration", + "start_interpolation", + "teleport_duration", + "billboard", + "view_range", + "shadow_radius", + "shadow_strength", + "width", + "height", + "glow_color_override", + "brightness", + "line_width", + "text_opacity", + "background", + "shadow", + "see_through", + "default_background", + "alignment", + "text", + "block_state", + "item", + "item_display" + ); + + public static void sanitizeEntity(CompoundTag entityRoot) { + entityRoot.getAllKeys().retainAll(ALLOWED_KEYS); + + if (entityRoot.contains("Passengers", Tag.TAG_LIST)) { + ListTag listTag = entityRoot.getList("Passengers", Tag.TAG_COMPOUND); + for (Tag tag : listTag) { + sanitizeEntity((CompoundTag) tag); + } + } + } + +} 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/View.java b/src/main/java/com/moulberry/axiom/View.java index 5dd2138..7d219dd 100644 --- a/src/main/java/com/moulberry/axiom/View.java +++ b/src/main/java/com/moulberry/axiom/View.java @@ -2,8 +2,6 @@ package com.moulberry.axiom; import com.moulberry.axiom.persistence.UUIDDataType; import net.minecraft.core.registries.Registries; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.Tag; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; diff --git a/src/main/java/com/moulberry/axiom/WorldExtension.java b/src/main/java/com/moulberry/axiom/WorldExtension.java new file mode 100644 index 0000000..4863c9a --- /dev/null +++ b/src/main/java/com/moulberry/axiom/WorldExtension.java @@ -0,0 +1,176 @@ +package com.moulberry.axiom; + +import com.moulberry.axiom.marker.MarkerData; +import io.netty.buffer.Unpooled; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongSet; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.Marker; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.LevelChunk; +import org.bukkit.World; +import org.bukkit.craftbukkit.v1_20_R1.CraftWorld; +import org.bukkit.entity.Player; + +import java.util.*; + +public class WorldExtension { + + private static final Map, WorldExtension> extensions = new HashMap<>(); + + public static WorldExtension get(ServerLevel serverLevel) { + WorldExtension extension = extensions.computeIfAbsent(serverLevel.dimension(), k -> new WorldExtension()); + extension.level = serverLevel; + return extension; + } + + public static void onPlayerJoin(World world, Player player) { + ServerLevel level = ((CraftWorld)world).getHandle(); + get(level).onPlayerJoin(player); + } + + public static void tick(MinecraftServer server, boolean sendMarkers, int maxChunkRelightsPerTick, int maxChunkSendsPerTick) { + extensions.keySet().retainAll(server.levelKeys()); + + for (ServerLevel level : server.getAllLevels()) { + get(level).tick(sendMarkers, maxChunkRelightsPerTick, maxChunkSendsPerTick); + } + } + + private ServerLevel level; + + private final LongSet pendingChunksToSend = new LongOpenHashSet(); + private final LongSet pendingChunksToLight = new LongOpenHashSet(); + private final Map previousMarkerData = new HashMap<>(); + + public void sendChunk(int cx, int cz) { + this.pendingChunksToSend.add(ChunkPos.asLong(cx, cz)); + } + + public void lightChunk(int cx, int cz) { + this.pendingChunksToLight.add(ChunkPos.asLong(cx, cz)); + } + + public void onPlayerJoin(Player player) { + if (!this.previousMarkerData.isEmpty()) { + List markerData = new ArrayList<>(this.previousMarkerData.values()); + + FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + buf.writeCollection(markerData, MarkerData::write); + buf.writeCollection(Set.of(), FriendlyByteBuf::writeUUID); + byte[] bytes = new byte[buf.writerIndex()]; + buf.getBytes(0, bytes); + + player.sendPluginMessage(AxiomPaper.PLUGIN, "axiom:marker_data", bytes); + } + } + + public void tick(boolean sendMarkers, int maxChunkRelightsPerTick, int maxChunkSendsPerTick) { + if (sendMarkers) { + this.tickMarkers(); + } + this.tickChunkRelight(maxChunkRelightsPerTick, maxChunkSendsPerTick); + } + + private void tickMarkers() { + List changedData = new ArrayList<>(); + + Set allMarkers = new HashSet<>(); + + for (Entity entity : this.level.getEntities().getAll()) { + if (entity instanceof Marker marker) { + MarkerData currentData = MarkerData.createFrom(marker); + + MarkerData previousData = this.previousMarkerData.get(marker.getUUID()); + if (!Objects.equals(currentData, previousData)) { + this.previousMarkerData.put(marker.getUUID(), currentData); + changedData.add(currentData); + } + + allMarkers.add(marker.getUUID()); + } + } + + Set oldUuids = new HashSet<>(this.previousMarkerData.keySet()); + oldUuids.removeAll(allMarkers); + this.previousMarkerData.keySet().removeAll(oldUuids); + + if (!changedData.isEmpty() || !oldUuids.isEmpty()) { + FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + buf.writeCollection(changedData, MarkerData::write); + buf.writeCollection(oldUuids, FriendlyByteBuf::writeUUID); + byte[] bytes = new byte[buf.writerIndex()]; + buf.getBytes(0, bytes); + + for (ServerPlayer player : this.level.players()) { + if (AxiomPaper.PLUGIN.activeAxiomPlayers.contains(player.getUUID())) { + player.getBukkitEntity().sendPluginMessage(AxiomPaper.PLUGIN, "axiom:marker_data", bytes); + } + } + } + } + + private void tickChunkRelight(int maxChunkRelightsPerTick, int maxChunkSendsPerTick) { + ChunkMap chunkMap = this.level.getChunkSource().chunkMap; + + boolean sendAll = maxChunkSendsPerTick <= 0; + + // Send chunks + LongIterator longIterator = this.pendingChunksToSend.longIterator(); + while (longIterator.hasNext()) { + ChunkPos chunkPos = new ChunkPos(longIterator.nextLong()); + List players = chunkMap.getPlayers(chunkPos, false); + if (players.isEmpty()) continue; + + LevelChunk chunk = this.level.getChunk(chunkPos.x, chunkPos.z); + var packet = new ClientboundLevelChunkWithLightPacket(chunk, this.level.getLightEngine(), null, null, false); + for (ServerPlayer player : players) { + player.connection.send(packet); + } + + if (!sendAll) { + longIterator.remove(); + + maxChunkSendsPerTick -= 1; + if (maxChunkSendsPerTick <= 0) { + break; + } + } + } + if (sendAll) { + this.pendingChunksToSend.clear(); + } + + // Relight chunks + Set chunkSet = new HashSet<>(); + longIterator = this.pendingChunksToLight.longIterator(); + if (maxChunkRelightsPerTick <= 0) { + while (longIterator.hasNext()) { + chunkSet.add(new ChunkPos(longIterator.nextLong())); + } + this.pendingChunksToLight.clear(); + } else { + while (longIterator.hasNext()) { + chunkSet.add(new ChunkPos(longIterator.nextLong())); + longIterator.remove(); + + maxChunkRelightsPerTick -= 1; + if (maxChunkRelightsPerTick <= 0) { + break; + } + } + } + + this.level.getChunkSource().getLightEngine().relight(chunkSet, pos -> {}, count -> {}); + } + +} diff --git a/src/main/java/com/moulberry/axiom/WorldPropertiesExample.java b/src/main/java/com/moulberry/axiom/WorldPropertiesExample.java index a9f5444..314d253 100644 --- a/src/main/java/com/moulberry/axiom/WorldPropertiesExample.java +++ b/src/main/java/com/moulberry/axiom/WorldPropertiesExample.java @@ -3,11 +3,10 @@ package com.moulberry.axiom; import com.moulberry.axiom.event.AxiomCreateWorldPropertiesEvent; import com.moulberry.axiom.world_properties.WorldPropertyCategory; import com.moulberry.axiom.world_properties.WorldPropertyWidgetType; +import com.moulberry.axiom.world_properties.server.PropertyUpdateResult; import com.moulberry.axiom.world_properties.server.ServerWorldProperty; import net.kyori.adventure.text.Component; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.util.Unit; -import org.bukkit.World; +import org.bukkit.NamespacedKey; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; @@ -15,41 +14,53 @@ import java.util.List; public class WorldPropertiesExample implements Listener { + private static final ServerWorldProperty CHECKBOX = new ServerWorldProperty<>( + new NamespacedKey("axiom", "checkbox"), + "Checkbox", + false, WorldPropertyWidgetType.CHECKBOX, world -> false, + (player, world, bool) -> { + world.sendMessage(Component.text("Checkbox: " + bool)); // Do something with input + return PropertyUpdateResult.UPDATE_AND_SYNC; // sync with client + } + ); + + private static final ServerWorldProperty SLIDER = new ServerWorldProperty<>( + new NamespacedKey("axiom", "slider"), + "Slider", + false, new WorldPropertyWidgetType.Slider(0, 8), + world -> 4, + (player, world, integer) -> { + world.sendMessage(Component.text("Slider: " + integer)); // Do something with input + return PropertyUpdateResult.UPDATE_AND_SYNC; // sync with client + } + ); + + private static final ServerWorldProperty TEXTBOX = new ServerWorldProperty<>( + new NamespacedKey("axiom", "textbox"), + "Textbox", + false, WorldPropertyWidgetType.TEXTBOX, + world -> "Hello", + (player, world, string) -> { + world.sendMessage(Component.text("Textbox: " + string)); // Do something with input + return PropertyUpdateResult.UPDATE_AND_SYNC; // sync with client + } + ); + + private static final ServerWorldProperty BUTTON = new ServerWorldProperty<>( + new NamespacedKey("axiom", "button"), + "Button", + false, WorldPropertyWidgetType.BUTTON, + world -> null, + (player, world, unit) -> { + world.sendMessage(Component.text("Button pressed")); // Do something with input + return PropertyUpdateResult.UPDATE_AND_SYNC; // sync with client + } + ); + @EventHandler public void onCreateWorldProperties(AxiomCreateWorldPropertiesEvent event) { WorldPropertyCategory category = new WorldPropertyCategory("Examples", false); - - World world = event.getWorld(); - - ServerWorldProperty checkbox = new ServerWorldProperty<>(new ResourceLocation("axiom:checkbox"), - "Checkbox", - false, WorldPropertyWidgetType.CHECKBOX, false, bool -> { - world.sendMessage(Component.text("Checkbox: " + bool)); // Do something with input - return true; // true to sync with client - }); - - ServerWorldProperty slider = new ServerWorldProperty<>(new ResourceLocation("axiom:slider"), - "Slider", - false, new WorldPropertyWidgetType.Slider(0, 8), 4, integer -> { - world.sendMessage(Component.text("Slider: " + integer)); // Do something with input - return true; // true to sync with client - }); - - ServerWorldProperty textbox = new ServerWorldProperty<>(new ResourceLocation("axiom:textbox"), - "Textbox", - false, WorldPropertyWidgetType.TEXTBOX, "Hello", string -> { - world.sendMessage(Component.text("Textbox: " + string)); // Do something with input - return true; // true to sync with client - }); - - ServerWorldProperty button = new ServerWorldProperty<>(new ResourceLocation("axiom:button"), - "Button", - false, WorldPropertyWidgetType.BUTTON, Unit.INSTANCE, unit -> { - world.sendMessage(Component.text("Button pressed")); // Do something with input - return true; // true to sync with client - }); - - event.addCategory(category, List.of(checkbox, slider, textbox, button)); + event.addCategory(category, List.of(CHECKBOX, SLIDER, TEXTBOX, BUTTON)); } } 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..aa24244 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; @@ -9,12 +10,13 @@ import it.unimi.dsi.fastutil.shorts.Short2ObjectMap; import it.unimi.dsi.fastutil.shorts.Short2ObjectOpenHashMap; import net.minecraft.core.BlockPos; import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; 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 +26,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 +57,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 +177,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..eee0923 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)), new NbtAccounter(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..b4b354b 100644 --- a/src/main/java/com/moulberry/axiom/buffer/Position2ByteMap.java +++ b/src/main/java/com/moulberry/axiom/buffer/Position2ByteMap.java @@ -1,13 +1,15 @@ 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 +44,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 +57,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/event/AxiomCreateWorldPropertiesEvent.java b/src/main/java/com/moulberry/axiom/event/AxiomCreateWorldPropertiesEvent.java index 36cf2d9..d88c077 100644 --- a/src/main/java/com/moulberry/axiom/event/AxiomCreateWorldPropertiesEvent.java +++ b/src/main/java/com/moulberry/axiom/event/AxiomCreateWorldPropertiesEvent.java @@ -2,14 +2,12 @@ package com.moulberry.axiom.event; import com.moulberry.axiom.world_properties.WorldPropertyCategory; import com.moulberry.axiom.world_properties.server.ServerWorldPropertiesRegistry; -import com.moulberry.axiom.world_properties.server.ServerWorldProperty; +import com.moulberry.axiom.world_properties.server.ServerWorldPropertyBase; import org.bukkit.World; -import org.bukkit.entity.Player; import org.bukkit.event.Cancellable; import org.bukkit.event.Event; import org.bukkit.event.HandlerList; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.util.List; @@ -30,7 +28,7 @@ public class AxiomCreateWorldPropertiesEvent extends Event implements Cancellabl return world; } - public void addCategory(WorldPropertyCategory category, List> properties) { + public void addCategory(WorldPropertyCategory category, List> properties) { this.registry.addCategory(category, properties); } diff --git a/src/main/java/com/moulberry/axiom/event/AxiomFlySpeedChangeEvent.java b/src/main/java/com/moulberry/axiom/event/AxiomFlySpeedChangeEvent.java index a914830..361ab89 100644 --- a/src/main/java/com/moulberry/axiom/event/AxiomFlySpeedChangeEvent.java +++ b/src/main/java/com/moulberry/axiom/event/AxiomFlySpeedChangeEvent.java @@ -1,6 +1,5 @@ package com.moulberry.axiom.event; -import org.bukkit.GameMode; import org.bukkit.entity.Player; import org.bukkit.event.Cancellable; import org.bukkit.event.Event; diff --git a/src/main/java/com/moulberry/axiom/event/AxiomGameModeChangeEvent.java b/src/main/java/com/moulberry/axiom/event/AxiomGameModeChangeEvent.java index 86da786..e3f110b 100644 --- a/src/main/java/com/moulberry/axiom/event/AxiomGameModeChangeEvent.java +++ b/src/main/java/com/moulberry/axiom/event/AxiomGameModeChangeEvent.java @@ -1,7 +1,6 @@ package com.moulberry.axiom.event; import org.bukkit.GameMode; -import org.bukkit.World; import org.bukkit.entity.Player; import org.bukkit.event.Cancellable; import org.bukkit.event.Event; diff --git a/src/main/java/com/moulberry/axiom/event/AxiomHandshakeEvent.java b/src/main/java/com/moulberry/axiom/event/AxiomHandshakeEvent.java index e9a75f3..749ec91 100644 --- a/src/main/java/com/moulberry/axiom/event/AxiomHandshakeEvent.java +++ b/src/main/java/com/moulberry/axiom/event/AxiomHandshakeEvent.java @@ -12,10 +12,11 @@ public class AxiomHandshakeEvent extends Event implements Cancellable { private final Player player; private boolean cancelled = false; - private int maxBufferSize = Short.MAX_VALUE; + private int maxBufferSize; - public AxiomHandshakeEvent(Player player) { + public AxiomHandshakeEvent(Player player, int maxBufferSize) { this.player = player; + this.maxBufferSize = maxBufferSize; } public Player getPlayer() { diff --git a/src/main/java/com/moulberry/axiom/event/AxiomTeleportEvent.java b/src/main/java/com/moulberry/axiom/event/AxiomTeleportEvent.java index 2b7f800..bed8a60 100644 --- a/src/main/java/com/moulberry/axiom/event/AxiomTeleportEvent.java +++ b/src/main/java/com/moulberry/axiom/event/AxiomTeleportEvent.java @@ -1,7 +1,6 @@ package com.moulberry.axiom.event; import org.bukkit.Location; -import org.bukkit.World; import org.bukkit.entity.Player; import org.bukkit.event.Cancellable; import org.bukkit.event.Event; diff --git a/src/main/java/com/moulberry/axiom/event/AxiomUnknownTeleportEvent.java b/src/main/java/com/moulberry/axiom/event/AxiomUnknownTeleportEvent.java new file mode 100644 index 0000000..21dc403 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/event/AxiomUnknownTeleportEvent.java @@ -0,0 +1,80 @@ +package com.moulberry.axiom.event; + +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +public class AxiomUnknownTeleportEvent extends Event implements Cancellable { + + private static final HandlerList HANDLERS = new HandlerList(); + + private final Player player; + private final NamespacedKey world; + private final double x; + private final double y; + private final double z; + private final float yaw; + private final float pitch; + private boolean cancelled = false; + + public AxiomUnknownTeleportEvent(Player player, NamespacedKey world, double x, double y, double z, float yaw, float pitch) { + this.player = player; + this.world = world; + this.x = x; + this.y = y; + this.z = z; + this.yaw = yaw; + this.pitch = pitch; + } + + public Player getPlayer() { + return this.player; + } + + public NamespacedKey getWorld() { + return this.world; + } + + public double getX() { + return this.x; + } + + public double getY() { + return this.y; + } + + public double getZ() { + return this.z; + } + + public float getYaw() { + return this.yaw; + } + + public float getPitch() { + return this.pitch; + } + + @Override + public boolean isCancelled() { + return this.cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + + public static HandlerList getHandlerList() { + return HANDLERS; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + +} diff --git a/src/main/java/com/moulberry/axiom/integration/Box.java b/src/main/java/com/moulberry/axiom/integration/Box.java new file mode 100644 index 0000000..28635bf --- /dev/null +++ b/src/main/java/com/moulberry/axiom/integration/Box.java @@ -0,0 +1,67 @@ +package com.moulberry.axiom.integration; + +import org.jetbrains.annotations.Nullable; + +public record Box(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) { + + @Nullable + public Box tryCombine(Box other) { + if (this.completelyOverlaps(other)) { + return this; + } + + if (other.completelyOverlaps(this)) { + return other; + } + + if (other.minX == this.minX && other.maxX == this.maxX) { + if (other.minY == this.minY && other.maxY == this.maxY) { + if (areLineSegmentsContinuous(other.minZ, other.maxZ, this.minZ, this.maxZ)) { + return new Box( + other.minX, other.minY, Math.min(other.minZ, this.minZ), + other.maxX, other.maxY, Math.max(other.maxZ, this.maxZ) + ); + } + } else if (other.minZ == this.minZ && other.maxZ == this.maxZ) { + if (areLineSegmentsContinuous(other.minY, other.maxY, this.minY, this.maxY)) { + return new Box( + other.minX, Math.min(other.minY, this.minY), other.minZ, + other.maxX, Math.max(other.maxY, this.maxY), other.maxZ + ); + } + } + } else if (other.minY == this.minY && other.maxY == this.maxY && + other.minZ == this.minZ && other.maxZ == this.maxZ) { + if (areLineSegmentsContinuous(other.minX, other.maxX, this.minX, this.maxX)) { + return new Box( + Math.min(other.minX, this.minX), other.minY, other.minZ, + Math.max(other.maxX, this.maxX), other.maxY, other.maxZ + ); + } + } + + return null; // Not able to combine + } + + public boolean completelyOverlaps(Box other) { + return this.minX() <= other.minX() && this.minY() <= other.minY() && this.minZ() <= other.minZ() && + this.maxX() >= other.maxX() && this.maxY() >= other.maxY() && this.maxZ() >= other.maxZ(); + } + + public boolean contains(int x, int y, int z) { + return this.minX() <= x && this.minY() <= y && this.minZ() <= z && + this.maxX() >= x && this.maxY() >= y && this.maxZ() >= z; + } + + private static boolean areLineSegmentsContinuous(int min1, int max1, int min2, int max2) { + int size1 = max1 - min1 + 1; + int size2 = max2 - min2 + 1; + + float mid1 = (min1 + max1); + float mid2 = (min2 + max2); + + float midDiff = Math.abs(mid1 - mid2); + return midDiff <= size1 + size2; + } + +} diff --git a/src/main/java/com/moulberry/axiom/integration/RegionProtection.java b/src/main/java/com/moulberry/axiom/integration/RegionProtection.java deleted file mode 100644 index cef48d9..0000000 --- a/src/main/java/com/moulberry/axiom/integration/RegionProtection.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.moulberry.axiom.integration; - -import org.bukkit.Bukkit; -import org.bukkit.World; -import org.bukkit.entity.Player; - -import java.util.ArrayList; -import java.util.List; - -public class RegionProtection { - - private final RegionProtectionWorldGuard worldGuard; - - public RegionProtection(Player player, World world) { - if (Bukkit.getPluginManager().isPluginEnabled("WorldGuard")) { - this.worldGuard = RegionProtectionWorldGuard.tryCreate(player, world); - } else { - this.worldGuard = null; - } - } - - public SectionProtection getSection(int cx, int cy, int cz) { - List protections = new ArrayList<>(); - if (this.worldGuard != null) { - return this.worldGuard.getSection(cx, cy, cz); - } - // todo: PlotSquared - return SectionProtection.ALLOW; - } - - public boolean canBuild(int x, int y, int z) { - if (this.worldGuard != null && !this.worldGuard.canBuild(x, y, z)) return false; - // todo: PlotSquared - return true; - } - - - -} diff --git a/src/main/java/com/moulberry/axiom/integration/RegionProtectionWorldGuard.java b/src/main/java/com/moulberry/axiom/integration/RegionProtectionWorldGuard.java deleted file mode 100644 index 5158140..0000000 --- a/src/main/java/com/moulberry/axiom/integration/RegionProtectionWorldGuard.java +++ /dev/null @@ -1,183 +0,0 @@ -package com.moulberry.axiom.integration; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Sets; -import com.sk89q.worldedit.bukkit.BukkitAdapter; -import com.sk89q.worldedit.math.BlockVector3; -import com.sk89q.worldguard.LocalPlayer; -import com.sk89q.worldguard.WorldGuard; -import com.sk89q.worldguard.bukkit.WorldGuardPlugin; -import com.sk89q.worldguard.internal.platform.WorldGuardPlatform; -import com.sk89q.worldguard.protection.ApplicableRegionSet; -import com.sk89q.worldguard.protection.FlagValueCalculator; -import com.sk89q.worldguard.protection.association.RegionAssociable; -import com.sk89q.worldguard.protection.flags.Flags; -import com.sk89q.worldguard.protection.flags.RegionGroup; -import com.sk89q.worldguard.protection.flags.StateFlag; -import com.sk89q.worldguard.protection.managers.RegionManager; -import com.sk89q.worldguard.protection.regions.*; -import org.bukkit.World; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.Nullable; - -import java.util.*; - -import static com.google.common.base.Preconditions.checkNotNull; - -public class RegionProtectionWorldGuard { - - private final LocalPlayer player; - private final RegionManager regionManager; - - public RegionProtectionWorldGuard(LocalPlayer player, RegionManager regionManager) { - this.player = player; - this.regionManager = regionManager; - } - - @Nullable - public static RegionProtectionWorldGuard tryCreate(Player player, World world) { - WorldGuardPlatform platform = WorldGuard.getInstance().getPlatform(); - - RegionContainer regionContainer = platform.getRegionContainer(); - if (regionContainer == null) return null; - - com.sk89q.worldedit.world.World worldEditWorld = BukkitAdapter.adapt(world); - LocalPlayer worldGuardPlayer = WorldGuardPlugin.inst().wrapPlayer(player); - - // Don't do any protection if player has bypass - if (platform.getSessionManager().hasBypass(worldGuardPlayer, worldEditWorld)) { - // todo: enable bypass - return null; - } - - RegionManager regionManager = regionContainer.get(worldEditWorld); - if (regionManager == null) return null; - - return new RegionProtectionWorldGuard(worldGuardPlayer, regionManager); - } - - public SectionProtection getSection(int cx, int cy, int cz) { - BlockVector3 min = BlockVector3.at(cx*16, cy*16, cz*16); - BlockVector3 max = BlockVector3.at(cx*16+15, cy*16+15, cz*16+15); - ProtectedRegion test = new ProtectedCuboidRegion("dummy", min, max); - ApplicableRegionSet regions = this.regionManager.getApplicableRegions(test, RegionQuery.QueryOption.COMPUTE_PARENTS); - - int minimumPriority = Integer.MIN_VALUE; - - Map consideredValues = new HashMap<>(); - Set ignoredParents = new HashSet<>(); - - for (ProtectedRegion region : regions) { - int priority = FlagValueCalculator.getPriorityOf(region); - - // todo: this logic doesn't work for us in determining ALLOW, DENY, CHECK - if (priority < minimumPriority) { - break; - } - - // todo: have to keep track of 2 booleans: partialAllow & partialDeny - - if (ignoredParents.contains(region)) { - continue; - } - - StateFlag.State value = FlagValueCalculator.getEffectiveFlagOf(region, Flags.BUILD, this.player); - if (value != null) { - minimumPriority = priority; - consideredValues.put(region, value); - } - - addParents(ignoredParents, region); - - // The BUILD flag is implicitly set on every region where - // PASSTHROUGH is not set to ALLOW - if (minimumPriority != priority && Flags.BUILD.implicitlySetWithMembership() && - FlagValueCalculator.getEffectiveFlagOf(region, Flags.PASSTHROUGH, this.player) != StateFlag.State.ALLOW) { - minimumPriority = priority; - } - } - - if (consideredValues.isEmpty()) { - if (Flags.BUILD.usesMembershipAsDefault()) { - // todo -// switch (getMembership(subject)) { -// case FAIL: -// return ImmutableList.of(); -// case SUCCESS: -// return (Collection) ImmutableList.of(StateFlag.State.ALLOW); -// } - } - - // System.out.println("returning default"); - StateFlag.State fallback = Flags.BUILD.getDefault(); - return fallback == StateFlag.State.DENY ? SectionProtection.DENY : SectionProtection.ALLOW; - } - - boolean hasPartialDeny = false; - for (Map.Entry entry : consideredValues.entrySet()) { - ProtectedRegion region = entry.getKey(); - if (entry.getValue() == StateFlag.State.DENY) { - // System.out.println("found region with deny!"); - if (region instanceof GlobalProtectedRegion) { - return SectionProtection.DENY; - } else if (region instanceof ProtectedCuboidRegion && doesRegionCompletelyContainSection(region, cx, cy, cz)) { - return SectionProtection.DENY; - } - hasPartialDeny = true; - } - } - - if (hasPartialDeny) { - // System.out.println("returning check!"); - return new SectionProtection() { - @Override - public SectionState getSectionState() { - return SectionState.CHECK; - } - - @Override - public boolean check(int wx, int wy, int wz) { - return true; - } - }; - // return complex thing - } - - // System.out.println("returning allow!"); - return SectionProtection.ALLOW; - } - - private boolean doesRegionCompletelyContainSection(ProtectedRegion region, int cx, int cy, int cz) { - BlockVector3 regionMin = region.getMinimumPoint(); - - if (regionMin.getBlockX() > cx*16) return false; - if (regionMin.getBlockY() > cy*16) return false; - if (regionMin.getBlockZ() > cz*16) return false; - - BlockVector3 regionMax = region.getMaximumPoint(); - - if (regionMax.getBlockX() < cx*16+15) return false; - if (regionMax.getBlockY() < cy*16+15) return false; - if (regionMax.getBlockZ() < cz*16+15) return false; - - return true; - } - - private void addParents(Set ignored, ProtectedRegion region) { - ProtectedRegion parent = region.getParent(); - - while (parent != null) { - ignored.add(parent); - parent = parent.getParent(); - } - } - - public boolean canBuild(int x, int y, int z) { - return this.regionManager.getApplicableRegions(BlockVector3.at(x, y, z)).testState(this.player, Flags.BUILD); - } - - public boolean isAllowed(LocalPlayer player, ProtectedRegion protectedRegion) { - return protectedRegion.isOwner(player) || protectedRegion.isMember(player); - } - -} diff --git a/src/main/java/com/moulberry/axiom/integration/SectionPermissionChecker.java b/src/main/java/com/moulberry/axiom/integration/SectionPermissionChecker.java new file mode 100644 index 0000000..cf7d685 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/integration/SectionPermissionChecker.java @@ -0,0 +1,137 @@ +package com.moulberry.axiom.integration; + +import java.util.List; + +public interface SectionPermissionChecker { + + boolean allAllowed(); + boolean noneAllowed(); + boolean allowed(int x, int y, int z); + Box bounds(); + + static SectionPermissionChecker fromAllowedBoxes(List allowed) { + if (allowed.isEmpty()) return NONE_ALLOWED; + + if (allowed.size() == 1) { + Box allowedBox = allowed.get(0); + if (allowedBox.completelyOverlaps(FULL_BOUNDS)) { + return ALL_ALLOWED; + } else { + return new AllAllowedInBox(allowedBox); + } + } + + int minBoundsX = 15; + int minBoundsY = 15; + int minBoundsZ = 15; + int maxBoundsX = 0; + int maxBoundsY = 0; + int maxBoundsZ = 0; + + for (Box box : allowed) { + minBoundsX = Math.min(box.minX(), minBoundsX); + minBoundsY = Math.min(box.minY(), minBoundsY); + minBoundsZ = Math.min(box.minZ(), minBoundsZ); + maxBoundsX = Math.max(box.maxX(), maxBoundsX); + maxBoundsY = Math.max(box.maxY(), maxBoundsY); + maxBoundsZ = Math.max(box.maxZ(), maxBoundsZ); + } + + return new AllAllowedBoxes(new Box(minBoundsX, minBoundsY, minBoundsZ, maxBoundsX, maxBoundsY, maxBoundsZ), allowed); + } + + record AllAllowedInBox(Box box) implements SectionPermissionChecker { + @Override + public boolean allAllowed() { + return true; + } + + @Override + public boolean noneAllowed() { + return false; + } + + @Override + public boolean allowed(int x, int y, int z) { + return true; + } + + @Override + public Box bounds() { + return box; + } + } + + record AllAllowedBoxes(Box bounds, List allowed) implements SectionPermissionChecker { + @Override + public boolean allAllowed() { + return false; + } + + @Override + public boolean noneAllowed() { + return false; + } + + @Override + public boolean allowed(int x, int y, int z) { + for (Box box : this.allowed) { + if (box.contains(x, y, z)) return true; + } + return false; + } + + @Override + public Box bounds() { + return this.bounds; + } + } + + Box FULL_BOUNDS = new Box(0, 0, 0, 15, 15, 15); + SectionPermissionChecker ALL_ALLOWED = new SectionPermissionChecker() { + @Override + public boolean allAllowed() { + return true; + } + + @Override + public boolean noneAllowed() { + return false; + } + + @Override + public boolean allowed(int x, int y, int z) { + return true; + } + + @Override + public Box bounds() { + return FULL_BOUNDS; + } + }; + + + Box EMPTY_BOUNDS = new Box(0, 0, 0, 0, 0, 0); + SectionPermissionChecker NONE_ALLOWED = new SectionPermissionChecker() { + @Override + public boolean allAllowed() { + return false; + } + + @Override + public boolean noneAllowed() { + return true; + } + + @Override + public boolean allowed(int x, int y, int z) { + return false; + } + + @Override + public Box bounds() { + return EMPTY_BOUNDS; + } + }; + +} diff --git a/src/main/java/com/moulberry/axiom/integration/SectionProtection.java b/src/main/java/com/moulberry/axiom/integration/SectionProtection.java deleted file mode 100644 index 07b8d60..0000000 --- a/src/main/java/com/moulberry/axiom/integration/SectionProtection.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.moulberry.axiom.integration; - -public interface SectionProtection { - - SectionProtection ALLOW = new SectionProtection() { - @Override - public SectionState getSectionState() { - return SectionState.ALLOW; - } - - @Override - public boolean check(int wx, int wy, int wz) { - return true; - } - }; - - SectionProtection DENY = new SectionProtection() { - @Override - public SectionState getSectionState() { - return SectionState.DENY; - } - - @Override - public boolean check(int wx, int wy, int wz) { - return false; - } - }; - - enum SectionState { - ALLOW, - DENY, - CHECK - } - - SectionState getSectionState(); - boolean check(int wx, int wy, int wz); - -} diff --git a/src/main/java/com/moulberry/axiom/integration/plotsquared/PlotSquaredIntegration.java b/src/main/java/com/moulberry/axiom/integration/plotsquared/PlotSquaredIntegration.java new file mode 100644 index 0000000..aefad2c --- /dev/null +++ b/src/main/java/com/moulberry/axiom/integration/plotsquared/PlotSquaredIntegration.java @@ -0,0 +1,68 @@ +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.World; +import org.bukkit.block.Block; +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; + } + return PlotSquaredIntegrationImpl.canBreakBlock(player, block); + } + + public static boolean canPlaceBlock(Player player, org.bukkit.Location loc) { + if (!Bukkit.getPluginManager().isPluginEnabled("PlotSquared")) { + return true; + } + return PlotSquaredIntegrationImpl.canPlaceBlock(player, loc); + } + + public static boolean isPlotWorld(World world) { + if (!Bukkit.getPluginManager().isPluginEnabled("PlotSquared")) { + return false; + } + 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; + } + return PlotSquaredIntegrationImpl.checkSection(player, world, sectionX, sectionY, sectionZ); + } + +} diff --git a/src/main/java/com/moulberry/axiom/integration/plotsquared/PlotSquaredIntegrationImpl.java b/src/main/java/com/moulberry/axiom/integration/plotsquared/PlotSquaredIntegrationImpl.java new file mode 100644 index 0000000..2dc36b6 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/integration/plotsquared/PlotSquaredIntegrationImpl.java @@ -0,0 +1,268 @@ +package com.moulberry.axiom.integration.plotsquared; + +import com.moulberry.axiom.integration.Box; +import com.moulberry.axiom.integration.SectionPermissionChecker; +import com.plotsquared.bukkit.player.BukkitPlayer; +import com.plotsquared.bukkit.util.BukkitUtil; +import com.plotsquared.core.PlotSquared; +import com.plotsquared.core.configuration.Settings; +import com.plotsquared.core.location.Location; +import com.plotsquared.core.permissions.Permission; +import com.plotsquared.core.plot.Plot; +import com.plotsquared.core.plot.PlotArea; +import com.plotsquared.core.plot.PlotId; +import com.plotsquared.core.plot.flag.implementations.BreakFlag; +import com.plotsquared.core.plot.flag.implementations.DoneFlag; +import com.plotsquared.core.plot.flag.types.BlockTypeWrapper; +import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.regions.CuboidRegion; +import com.sk89q.worldedit.world.block.BlockType; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; + +import java.util.*; + +/* + * PlotSquared, a land and world management plugin for Minecraft. + * Copyright (C) IntellectualSites + * Copyright (C) IntellectualSites team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +public class PlotSquaredIntegrationImpl { + + static boolean canBreakBlock(Player player, Block block) { + Location location = BukkitUtil.adapt(block.getLocation()); + PlotArea area = location.getPlotArea(); + if (area == null) { + return true; + } + + Plot plot = area.getPlot(location); + if (plot != null) { + BukkitPlayer plotPlayer = BukkitUtil.adapt(player); + // == rather than <= as we only care about the "ground level" not being destroyed + if (block.getY() == area.getMinGenHeight()) { + if (!plotPlayer.hasPermission(Permission.PERMISSION_ADMIN_DESTROY_GROUNDLEVEL, true)) { + return false; + } + } + if (area.notifyIfOutsideBuildArea(plotPlayer, location.getY())) { + return false; + } + // check unowned plots + if (!plot.hasOwner()) { + return plotPlayer.hasPermission(Permission.PERMISSION_ADMIN_DESTROY_UNOWNED, true); + } + // player is breaking another player's plot + if (!plot.isAdded(plotPlayer.getUUID())) { + List destroy = plot.getFlag(BreakFlag.class); + final BlockType blockType = BukkitAdapter.asBlockType(block.getType()); + for (final BlockTypeWrapper blockTypeWrapper : destroy) { + if (blockTypeWrapper.accepts(blockType)) { + return true; + } + } + return plotPlayer.hasPermission(Permission.PERMISSION_ADMIN_DESTROY_OTHER, true); + } + // plot is 'done' + if (Settings.Done.RESTRICT_BUILDING && DoneFlag.isDone(plot)) { + return plotPlayer.hasPermission(Permission.PERMISSION_ADMIN_BUILD_OTHER, true); + } + return true; + } + + BukkitPlayer pp = BukkitUtil.adapt(player); + return pp.hasPermission(Permission.PERMISSION_ADMIN_DESTROY_ROAD, true); + } + + static boolean canPlaceBlock(Player player, org.bukkit.Location loc) { + Location location = BukkitUtil.adapt(loc); + PlotArea area = location.getPlotArea(); + if (area == null) { + return true; + } + + BukkitPlayer pp = BukkitUtil.adapt(player); + Plot plot = area.getPlot(location); + if (plot != null) { + if (area.notifyIfOutsideBuildArea(pp, location.getY())) { + return false; + } + // check unowned plots + if (!plot.hasOwner()) { + return pp.hasPermission(Permission.PERMISSION_ADMIN_BUILD_UNOWNED, true); + } + // player is breaking another player's plot + if (!plot.isAdded(pp.getUUID())) { + return pp.hasPermission(Permission.PERMISSION_ADMIN_BUILD_OTHER, true); + } + // plot is 'done' + if (Settings.Done.RESTRICT_BUILDING && DoneFlag.isDone(plot)) { + return pp.hasPermission(Permission.PERMISSION_ADMIN_BUILD_OTHER, true); + } + return true; + } + + return pp.hasPermission(Permission.PERMISSION_ADMIN_BUILD_ROAD, true); + } + + private static final WeakHashMap plotWorldCache = new WeakHashMap<>(); + + static boolean isPlotWorld(World world) { + if (plotWorldCache.containsKey(world)) { + return plotWorldCache.get(world); + } + + String worldName = world.getName(); + PlotArea[] plotAreas = PlotSquared.get().getPlotAreaManager().getPlotAreas(worldName, null); + boolean isPlotWorld = plotAreas.length > 0; + plotWorldCache.put(world, isPlotWorld); + 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; + int minZ = sectionZ * 16; + int maxX = sectionX * 16 + 15; + int maxY = sectionY * 16 + 15; + int maxZ = sectionZ * 16 + 15; + + PlotArea[] plotAreas = PlotSquared.get().getPlotAreaManager().getPlotAreas(world.getName(), new CuboidRegion( + BlockVector3.at(minX, minY, minZ), + BlockVector3.at(maxX, maxY, maxZ) + )); + + if (plotAreas.length == 0) { + return SectionPermissionChecker.ALL_ALLOWED; + } + + Set checkedPlots = new HashSet<>(); + List allowed = new ArrayList<>(); + + for (PlotArea plotArea : plotAreas) { + for (int px = minX; px <= maxX; px += 15) { + for (int py = minY; py <= maxY; py += 15) { + for (int pz = minZ; pz <= maxZ; pz += 15) { + PlotId pid = plotArea.getPlotManager().getPlotId(px, py, pz); + if (pid == null) continue; + Plot plot = plotArea.getOwnedPlot(pid); + if (plot == null) continue; + + if (!checkedPlots.add(plot)) continue; + + if (!plot.hasOwner()) continue; + if (!plot.isAdded(player.getUniqueId())) continue; + if (Settings.Done.RESTRICT_BUILDING && DoneFlag.isDone(plot)) continue; + + for (CuboidRegion region : plot.getRegions()) { + BlockVector3 minPoint = region.getMinimumPoint(); + BlockVector3 maxPoint = region.getMaximumPoint(); + + int minPlotX = Math.max(minPoint.getX(), minX); + int minPlotY = Math.max(minPoint.getY(), minY); + int minPlotZ = Math.max(minPoint.getZ(), minZ); + int maxPlotX = Math.min(maxPoint.getX(), maxX); + int maxPlotY = Math.min(maxPoint.getY(), maxY); + int maxPlotZ = Math.min(maxPoint.getZ(), maxZ); + + if (minPlotX > maxPlotX) continue; + if (minPlotY > maxPlotY) continue; + if (minPlotZ > maxPlotZ) continue; + + if (minPlotX <= minX && minPlotY <= minY && minPlotZ <= minZ && + maxPlotX >= maxX && maxPlotY >= maxY && maxPlotZ >= maxZ) { + return SectionPermissionChecker.ALL_ALLOWED; + } + + allowed.add(new Box(minPlotX - minX, minPlotY - minY, minPlotZ - minZ, + maxPlotX - minX, maxPlotY - minY, maxPlotZ - minZ)); + } + } + } + } + } + + // Combine + main: + while (allowed.size() >= 2) { + for (int i = 0; i < allowed.size() - 1; i++) { + Box first = allowed.get(i); + for (int j = i + 1; j < allowed.size(); j++) { + Box second = allowed.get(j); + + Box combined = first.tryCombine(second); + if (combined != null) { + allowed.remove(j); + allowed.remove(i); + allowed.add(combined); + continue main; + } + } + } + break; + } + + return SectionPermissionChecker.fromAllowedBoxes(allowed); + } +} diff --git a/src/main/java/com/moulberry/axiom/marker/MarkerData.java b/src/main/java/com/moulberry/axiom/marker/MarkerData.java new file mode 100644 index 0000000..6f331af --- /dev/null +++ b/src/main/java/com/moulberry/axiom/marker/MarkerData.java @@ -0,0 +1,102 @@ +package com.moulberry.axiom.marker; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.Marker; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; +import xyz.jpenilla.reflectionremapper.ReflectionRemapper; + +import java.lang.reflect.Field; +import java.util.UUID; + +public record MarkerData(UUID uuid, Vec3 position, @Nullable String name, @Nullable Vec3 minRegion, @Nullable Vec3 maxRegion) { + public static MarkerData read(FriendlyByteBuf friendlyByteBuf) { + UUID uuid = friendlyByteBuf.readUUID(); + Vec3 position = new Vec3(friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble()); + String name = friendlyByteBuf.readNullable(FriendlyByteBuf::readUtf); + + Vec3 minRegion = null; + Vec3 maxRegion = null; + if (friendlyByteBuf.readBoolean()) { + minRegion = new Vec3(friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble()); + maxRegion = new Vec3(friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble()); + } + + return new MarkerData(uuid, position, name, minRegion, maxRegion); + } + + public static void write(FriendlyByteBuf friendlyByteBuf, MarkerData markerData) { + friendlyByteBuf.writeUUID(markerData.uuid); + friendlyByteBuf.writeDouble(markerData.position.x); + friendlyByteBuf.writeDouble(markerData.position.y); + friendlyByteBuf.writeDouble(markerData.position.z); + friendlyByteBuf.writeNullable(markerData.name, FriendlyByteBuf::writeUtf); + + if (markerData.minRegion != null && markerData.maxRegion != null) { + friendlyByteBuf.writeBoolean(true); + friendlyByteBuf.writeDouble(markerData.minRegion.x); + friendlyByteBuf.writeDouble(markerData.minRegion.y); + friendlyByteBuf.writeDouble(markerData.minRegion.z); + friendlyByteBuf.writeDouble(markerData.maxRegion.x); + friendlyByteBuf.writeDouble(markerData.maxRegion.y); + friendlyByteBuf.writeDouble(markerData.maxRegion.z); + } else { + friendlyByteBuf.writeBoolean(false); + } + } + + private static final Field dataField; + static { + ReflectionRemapper reflectionRemapper = ReflectionRemapper.forReobfMappingsInPaperJar(); + String fieldName = reflectionRemapper.remapFieldName(Marker.class, "data"); + + try { + dataField = Marker.class.getDeclaredField(fieldName); + dataField.setAccessible(true); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + public static CompoundTag getData(Marker marker) { + try { + return (CompoundTag) dataField.get(marker); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + public static MarkerData createFrom(Marker marker) { + Vec3 position = marker.position(); + CompoundTag data = getData(marker); + + String name = data.getString("name").trim(); + if (name.isEmpty()) name = null; + + Vec3 minRegion = null; + Vec3 maxRegion = null; + if (data.contains("min", Tag.TAG_LIST) && data.contains("max", Tag.TAG_LIST)) { + ListTag min = data.getList("min", Tag.TAG_DOUBLE); + ListTag max = data.getList("max", Tag.TAG_DOUBLE); + + if (min.size() == 3 && max.size() == 3) { + double minX = min.getDouble(0); + double minY = min.getDouble(1); + double minZ = min.getDouble(2); + double maxX = max.getDouble(0); + double maxY = max.getDouble(1); + double maxZ = max.getDouble(2); + minRegion = new Vec3(minX, minY, minZ); + maxRegion = new Vec3(maxX, maxY, maxZ); + } + + } + + return new MarkerData(marker.getUUID(), position, name, minRegion, maxRegion); + } +} diff --git a/src/main/java/com/moulberry/axiom/packet/AxiomBigPayloadHandler.java b/src/main/java/com/moulberry/axiom/packet/AxiomBigPayloadHandler.java index eb64174..0b8761f 100644 --- a/src/main/java/com/moulberry/axiom/packet/AxiomBigPayloadHandler.java +++ b/src/main/java/com/moulberry/axiom/packet/AxiomBigPayloadHandler.java @@ -26,10 +26,17 @@ public class AxiomBigPayloadHandler extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { - try { + // Don't process if channel isn't active + if (!ctx.channel().isActive()) { + in.skipBytes(in.readableBytes()); + return; + } + + int i = in.readableBytes(); + if (i != 0) { int readerIndex = in.readerIndex(); - int i = in.readableBytes(); - if (i != 0) { + boolean success = false; + try { FriendlyByteBuf buf = new FriendlyByteBuf(in); int packetId = buf.readVarInt(); @@ -39,18 +46,27 @@ public class AxiomBigPayloadHandler extends ByteToMessageDecoder { ServerPlayer player = connection.getPlayer(); if (player != null && player.getBukkitEntity().hasPermission("axiom.*")) { if (listener.onReceive(player, buf)) { + success = true; + in.skipBytes(in.readableBytes()); return; } } } } + } catch (Throwable ignored) { + } finally { + if (!success) { + in.readerIndex(readerIndex); + } } - in.readerIndex(readerIndex); - } catch (Exception e) { - e.printStackTrace(); } ctx.fireChannelRead(in.retain()); + + // Skip remaining bytes + if (in.readableBytes() > 0) { + in.skipBytes(in.readableBytes()); + } } @Override diff --git a/src/main/java/com/moulberry/axiom/packet/DeleteEntityPacketListener.java b/src/main/java/com/moulberry/axiom/packet/DeleteEntityPacketListener.java new file mode 100644 index 0000000..0045582 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/packet/DeleteEntityPacketListener.java @@ -0,0 +1,59 @@ +package com.moulberry.axiom.packet; + +import com.moulberry.axiom.AxiomPaper; +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import org.bukkit.craftbukkit.v1_20_R1.CraftWorld; +import org.bukkit.entity.Player; +import org.bukkit.plugin.messaging.PluginMessageListener; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.UUID; + +public class DeleteEntityPacketListener implements PluginMessageListener { + + private final AxiomPaper plugin; + public DeleteEntityPacketListener(AxiomPaper plugin) { + this.plugin = plugin; + } + + @Override + public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, @NotNull byte[] message) { + if (!this.plugin.canUseAxiom(player)) { + return; + } + + if (!player.hasPermission("axiom.entity.*") && !player.hasPermission("axiom.entity.delete")) { + return; + } + + if (!this.plugin.canModifyWorld(player, player.getWorld())) { + return; + } + + FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); + List delete = friendlyByteBuf.readList(FriendlyByteBuf::readUUID); + + ServerLevel serverLevel = ((CraftWorld)player.getWorld()).getHandle(); + + List whitelistedEntities = this.plugin.configuration.getStringList("whitelist-entities"); + List blacklistedEntities = this.plugin.configuration.getStringList("blacklist-entities"); + + for (UUID uuid : delete) { + Entity entity = serverLevel.getEntity(uuid); + if (entity == null || entity instanceof net.minecraft.world.entity.player.Player || entity.hasPassenger(e -> e instanceof net.minecraft.world.entity.player.Player)) continue; + + String type = EntityType.getKey(entity.getType()).toString(); + + if (!whitelistedEntities.isEmpty() && !whitelistedEntities.contains(type)) continue; + if (blacklistedEntities.contains(type)) continue; + + entity.remove(Entity.RemovalReason.DISCARDED); + } + } + +} diff --git a/src/main/java/com/moulberry/axiom/packet/HelloPacketListener.java b/src/main/java/com/moulberry/axiom/packet/HelloPacketListener.java index d20deed..3cb287a 100644 --- a/src/main/java/com/moulberry/axiom/packet/HelloPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/HelloPacketListener.java @@ -1,14 +1,17 @@ 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; +import com.moulberry.axiom.WorldExtension; import com.moulberry.axiom.event.AxiomHandshakeEvent; import com.moulberry.axiom.persistence.ItemStackDataType; import com.moulberry.axiom.persistence.UUIDDataType; import com.moulberry.axiom.world_properties.server.ServerWorldPropertiesRegistry; import io.netty.buffer.Unpooled; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import net.minecraft.SharedConstants; import net.minecraft.network.FriendlyByteBuf; import org.bukkit.Bukkit; @@ -22,17 +25,14 @@ import org.bukkit.persistence.PersistentDataType; import org.bukkit.plugin.messaging.PluginMessageListener; import org.jetbrains.annotations.NotNull; -import java.util.Set; 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 @@ -46,25 +46,64 @@ public class HelloPacketListener implements PluginMessageListener { int dataVersion = friendlyByteBuf.readVarInt(); friendlyByteBuf.readNbt(); // Discard - if (dataVersion != SharedConstants.getCurrentVersion().getDataVersion().getVersion()) { - player.kick(Component.text("Axiom: Incompatible data version detected, are you using ViaVersion?")); - return; + int serverDataVersion = SharedConstants.getCurrentVersion().getDataVersion().getVersion(); + if (dataVersion != serverDataVersion) { + Component text = Component.text("Axiom: Incompatible data version detected (client " + dataVersion + + ", server " + serverDataVersion + "), are you using ViaVersion?"); + + String incompatibleDataVersion = plugin.configuration.getString("incompatible-data-version"); + if (incompatibleDataVersion == null) incompatibleDataVersion = "kick"; + if (incompatibleDataVersion.equals("warn")) { + player.sendMessage(text.color(NamedTextColor.RED)); + return; + } else if (!incompatibleDataVersion.equals("ignore")) { + player.kick(text); + return; + } } if (apiVersion != AxiomConstants.API_VERSION) { - player.kick(Component.text("Unsupported Axiom API Version. Server supports " + AxiomConstants.API_VERSION + - ", while client is " + apiVersion)); - return; + Component text = Component.text("Unsupported Axiom API Version. Server supports " + AxiomConstants.API_VERSION + + ", while client is " + apiVersion); + + String unsupportedAxiomVersion = plugin.configuration.getString("unsupported-axiom-version"); + if (unsupportedAxiomVersion == null) unsupportedAxiomVersion = "kick"; + if (unsupportedAxiomVersion.equals("warn")) { + player.sendMessage(text.color(NamedTextColor.RED)); + return; + } else if (!unsupportedAxiomVersion.equals("ignore")) { + player.kick(text); + return; + } + } + + 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 - AxiomHandshakeEvent handshakeEvent = new AxiomHandshakeEvent(player); + int maxBufferSize = plugin.configuration.getInt("max-block-buffer-packet-size"); + AxiomHandshakeEvent handshakeEvent = new AxiomHandshakeEvent(player, maxBufferSize); Bukkit.getPluginManager().callEvent(handshakeEvent); if (handshakeEvent.isCancelled()) { 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()); @@ -121,13 +160,15 @@ public class HelloPacketListener implements PluginMessageListener { // Register world properties World world = player.getWorld(); - ServerWorldPropertiesRegistry properties = plugin.getWorldProperties(world); + ServerWorldPropertiesRegistry properties = plugin.getOrCreateWorldProperties(world); if (properties == null) { player.sendPluginMessage(plugin, "axiom:register_world_properties", new byte[]{0}); } else { properties.registerFor(plugin, player); } + + WorldExtension.onPlayerJoin(world, player); } } diff --git a/src/main/java/com/moulberry/axiom/packet/ManipulateEntityPacketListener.java b/src/main/java/com/moulberry/axiom/packet/ManipulateEntityPacketListener.java new file mode 100644 index 0000000..d802f7a --- /dev/null +++ b/src/main/java/com/moulberry/axiom/packet/ManipulateEntityPacketListener.java @@ -0,0 +1,212 @@ +package com.moulberry.axiom.packet; + +import com.moulberry.axiom.AxiomPaper; +import com.moulberry.axiom.NbtSanitization; +import io.netty.buffer.Unpooled; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.RelativeMovement; +import net.minecraft.world.entity.decoration.HangingEntity; +import net.minecraft.world.entity.decoration.ItemFrame; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.phys.Vec3; +import org.bukkit.craftbukkit.v1_20_R1.CraftWorld; +import org.bukkit.entity.Player; +import org.bukkit.plugin.messaging.PluginMessageListener; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Set; +import java.util.UUID; + +public class ManipulateEntityPacketListener implements PluginMessageListener { + + private final AxiomPaper plugin; + public ManipulateEntityPacketListener(AxiomPaper plugin) { + this.plugin = plugin; + } + + public enum PassengerManipulation { + NONE, + REMOVE_ALL, + ADD_LIST, + REMOVE_LIST + } + + public record ManipulateEntry(UUID uuid, @Nullable Set relativeMovementSet, @Nullable Vec3 position, + float yaw, float pitch, CompoundTag merge, PassengerManipulation passengerManipulation, List passengers) { + public static ManipulateEntry read(FriendlyByteBuf friendlyByteBuf) { + UUID uuid = friendlyByteBuf.readUUID(); + + int flags = friendlyByteBuf.readByte(); + Set relativeMovementSet = null; + Vec3 position = null; + float yaw = 0; + float pitch = 0; + if (flags >= 0) { + relativeMovementSet = RelativeMovement.unpack(flags); + position = new Vec3(friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble()); + yaw = friendlyByteBuf.readFloat(); + pitch = friendlyByteBuf.readFloat(); + } + + CompoundTag nbt = friendlyByteBuf.readNbt(); + + PassengerManipulation passengerManipulation = friendlyByteBuf.readEnum(PassengerManipulation.class); + List passengers = List.of(); + if (passengerManipulation == PassengerManipulation.ADD_LIST || passengerManipulation == PassengerManipulation.REMOVE_LIST) { + passengers = friendlyByteBuf.readList(FriendlyByteBuf::readUUID); + } + + return new ManipulateEntry(uuid, relativeMovementSet, position, yaw, pitch, nbt, + passengerManipulation, passengers); + } + } + + private static final Rotation[] ROTATION_VALUES = Rotation.values(); + + @Override + public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, @NotNull byte[] message) { + if (!this.plugin.canUseAxiom(player)) { + return; + } + + if (!player.hasPermission("axiom.entity.*") && !player.hasPermission("axiom.entity.manipulate")) { + return; + } + + if (!this.plugin.canModifyWorld(player, player.getWorld())) { + return; + } + + FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); + List entries = friendlyByteBuf.readList(ManipulateEntry::read); + + ServerLevel serverLevel = ((CraftWorld)player.getWorld()).getHandle(); + + List whitelistedEntities = this.plugin.configuration.getStringList("whitelist-entities"); + List blacklistedEntities = this.plugin.configuration.getStringList("blacklist-entities"); + + for (ManipulateEntry entry : entries) { + Entity entity = serverLevel.getEntity(entry.uuid); + if (entity == null || entity instanceof net.minecraft.world.entity.player.Player || entity.hasPassenger(ManipulateEntityPacketListener::isPlayer)) continue; + + String type = EntityType.getKey(entity.getType()).toString(); + + if (!whitelistedEntities.isEmpty() && !whitelistedEntities.contains(type)) continue; + if (blacklistedEntities.contains(type)) continue; + + if (entry.merge != null && !entry.merge.isEmpty()) { + NbtSanitization.sanitizeEntity(entry.merge); + + CompoundTag compoundTag = entity.saveWithoutId(new CompoundTag()); + compoundTag = merge(compoundTag, entry.merge); + entity.load(compoundTag); + } + + Vec3 entryPos = entry.position(); + if (entryPos != null && entry.relativeMovementSet != null) { + double newX = entry.relativeMovementSet.contains(RelativeMovement.X) ? entity.position().x + entryPos.x : entryPos.x; + double newY = entry.relativeMovementSet.contains(RelativeMovement.Y) ? entity.position().y + entryPos.y : entryPos.y; + double newZ = entry.relativeMovementSet.contains(RelativeMovement.Z) ? entity.position().z + entryPos.z : entryPos.z; + float newYaw = entry.relativeMovementSet.contains(RelativeMovement.Y_ROT) ? entity.getYRot() + entry.yaw : entry.yaw; + float newPitch = entry.relativeMovementSet.contains(RelativeMovement.X_ROT) ? entity.getXRot() + entry.pitch : entry.pitch; + + if (entity instanceof HangingEntity hangingEntity) { + float changedYaw = newYaw - entity.getYRot(); + int rotations = Math.round(changedYaw / 90); + hangingEntity.rotate(ROTATION_VALUES[rotations & 3]); + + if (entity instanceof ItemFrame itemFrame && itemFrame.getDirection().getAxis() == Direction.Axis.Y) { + itemFrame.setRotation(itemFrame.getRotation() - Math.round(changedYaw / 45)); + } + } + + entity.teleportTo(serverLevel, newX, newY, newZ, Set.of(), newYaw, newPitch); + entity.setYHeadRot(newYaw); + } + + switch (entry.passengerManipulation) { + case NONE -> {} + case REMOVE_ALL -> entity.ejectPassengers(); + case ADD_LIST -> { + for (UUID passengerUuid : entry.passengers) { + Entity passenger = serverLevel.getEntity(passengerUuid); + + if (passenger == null || passenger.isPassenger() || + passenger instanceof net.minecraft.world.entity.player.Player || passenger.hasPassenger(ManipulateEntityPacketListener::isPlayer)) continue; + + String passengerType = EntityType.getKey(passenger.getType()).toString(); + + if (!whitelistedEntities.isEmpty() && !whitelistedEntities.contains(passengerType)) continue; + if (blacklistedEntities.contains(passengerType)) continue; + + // Prevent mounting loop + if (passenger.getSelfAndPassengers().anyMatch(entity2 -> entity2 == entity)) { + continue; + } + + passenger.startRiding(entity, true); + } + } + case REMOVE_LIST -> { + for (UUID passengerUuid : entry.passengers) { + Entity passenger = serverLevel.getEntity(passengerUuid); + if (passenger == null || passenger == entity || passenger instanceof net.minecraft.world.entity.player.Player || + passenger.hasPassenger(ManipulateEntityPacketListener::isPlayer)) continue; + + String passengerType = EntityType.getKey(passenger.getType()).toString(); + + if (!whitelistedEntities.isEmpty() && !whitelistedEntities.contains(passengerType)) continue; + if (blacklistedEntities.contains(passengerType)) continue; + + Entity vehicle = passenger.getVehicle(); + if (vehicle == entity) { + passenger.stopRiding(); + } + } + } + } + } + } + + private static CompoundTag merge(CompoundTag left, CompoundTag right) { + if (right.contains("axiom:modify")) { + right.remove("axiom:modify"); + return right; + } + + for (String key : right.getAllKeys()) { + Tag tag = right.get(key); + if (tag instanceof CompoundTag compound) { + if (compound.isEmpty()) { + left.remove(key); + } else if (left.contains(key, Tag.TAG_COMPOUND)) { + CompoundTag child = left.getCompound(key); + child = merge(child, compound); + left.put(key, child); + } else { + CompoundTag copied = compound.copy(); + if (copied.contains("axiom:modify")) { + copied.remove("axiom:modify"); + } + left.put(key, copied); + } + } else { + left.put(key, tag.copy()); + } + } + return left; + } + + private static boolean isPlayer(Entity entity) { + return entity instanceof net.minecraft.world.entity.player.Player; + } + +} diff --git a/src/main/java/com/moulberry/axiom/packet/MarkerNbtRequestPacketListener.java b/src/main/java/com/moulberry/axiom/packet/MarkerNbtRequestPacketListener.java new file mode 100644 index 0000000..91acd37 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/packet/MarkerNbtRequestPacketListener.java @@ -0,0 +1,58 @@ +package com.moulberry.axiom.packet; + +import com.moulberry.axiom.AxiomPaper; +import com.moulberry.axiom.marker.MarkerData; +import io.netty.buffer.Unpooled; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.Marker; +import org.bukkit.craftbukkit.v1_20_R1.CraftWorld; +import org.bukkit.entity.Player; +import org.bukkit.plugin.messaging.PluginMessageListener; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public class MarkerNbtRequestPacketListener implements PluginMessageListener { + + private final AxiomPaper plugin; + public MarkerNbtRequestPacketListener(AxiomPaper plugin) { + this.plugin = plugin; + } + + @Override + public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, @NotNull byte[] message) { + if (!this.plugin.canUseAxiom(player)) { + return; + } + + if (!player.hasPermission("axiom.entity.*") && !player.hasPermission("axiom.entity.manipulate")) { + return; + } + + if (!this.plugin.canModifyWorld(player, player.getWorld())) { + return; + } + + FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); + UUID uuid = friendlyByteBuf.readUUID(); + + ServerLevel serverLevel = ((CraftWorld)player.getWorld()).getHandle(); + + Entity entity = serverLevel.getEntity(uuid); + if (entity instanceof Marker marker) { + CompoundTag data = MarkerData.getData(marker); + + FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + buf.writeUUID(uuid); + buf.writeNbt(data); + byte[] bytes = new byte[buf.writerIndex()]; + buf.getBytes(0, bytes); + + player.sendPluginMessage(AxiomPaper.PLUGIN, "axiom:marker_nbt_response", bytes); + } + } + +} diff --git a/src/main/java/com/moulberry/axiom/packet/RequestChunkDataPacketListener.java b/src/main/java/com/moulberry/axiom/packet/RequestChunkDataPacketListener.java index b848347..7baf46d 100644 --- a/src/main/java/com/moulberry/axiom/packet/RequestChunkDataPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/RequestChunkDataPacketListener.java @@ -3,8 +3,10 @@ package com.moulberry.axiom.packet; import com.moulberry.axiom.AxiomConstants; import com.moulberry.axiom.AxiomPaper; import com.moulberry.axiom.buffer.CompressedBlockEntity; +import com.moulberry.axiom.integration.plotsquared.PlotSquaredIntegration; import io.netty.buffer.Unpooled; -import it.unimi.dsi.fastutil.longs.*; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import net.minecraft.core.BlockPos; import net.minecraft.core.registries.Registries; import net.minecraft.nbt.CompoundTag; @@ -33,7 +35,6 @@ public class RequestChunkDataPacketListener implements PluginMessageListener { private static final ResourceLocation RESPONSE_ID = new ResourceLocation("axiom:response_chunk_data"); private final AxiomPaper plugin; - public RequestChunkDataPacketListener(AxiomPaper plugin) { this.plugin = plugin; } @@ -44,12 +45,17 @@ public class RequestChunkDataPacketListener implements PluginMessageListener { FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); long id = friendlyByteBuf.readLong(); - if (!bukkitPlayer.hasPermission("axiom.*")) { + if (!this.plugin.canUseAxiom(bukkitPlayer)) { // We always send an 'empty' response in order to make the client happy sendEmptyResponse(player, id); return; } + if (!this.plugin.canModifyWorld(bukkitPlayer, bukkitPlayer.getWorld())) { + sendEmptyResponse(player, id); + return; + } + MinecraftServer server = player.getServer(); if (server == null) { sendEmptyResponse(player, id); @@ -63,7 +69,7 @@ public class RequestChunkDataPacketListener implements PluginMessageListener { return; } - boolean sendBlockEntitiesInChunks= friendlyByteBuf.readBoolean(); + boolean sendBlockEntitiesInChunks = friendlyByteBuf.readBoolean(); Long2ObjectMap blockEntityMap = new Long2ObjectOpenHashMap<>(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -84,39 +90,49 @@ public class RequestChunkDataPacketListener implements PluginMessageListener { int playerSectionZ = player.getBlockZ() >> 4; Long2ObjectMap> sections = new Long2ObjectOpenHashMap<>(); - count = friendlyByteBuf.readVarInt(); - for (int i = 0; i < count; i++) { - long pos = friendlyByteBuf.readLong(); - int sx = BlockPos.getX(pos); - int sy = BlockPos.getY(pos); - int sz = BlockPos.getZ(pos); + int maxChunkLoadDistance = this.plugin.configuration.getInt("max-chunk-load-distance"); - int distance = Math.abs(playerSectionX - sx) + Math.abs(playerSectionZ - sz); - if (distance > 128) continue; + // Don't allow loading chunks outside render distance for plot worlds + if (PlotSquaredIntegration.isPlotWorld(level.getWorld())) { + maxChunkLoadDistance = 0; + } - LevelChunk chunk = level.getChunk(sx, sz); - int sectionIndex = chunk.getSectionIndexFromSectionY(sy); - if (sectionIndex < 0 || sectionIndex >= chunk.getSectionsCount()) continue; - LevelChunkSection section = chunk.getSection(sectionIndex); + if (maxChunkLoadDistance > 0) { + count = friendlyByteBuf.readVarInt(); + for (int i = 0; i < count; i++) { + long pos = friendlyByteBuf.readLong(); - if (section.hasOnlyAir()) { - sections.put(pos, null); - } else { - PalettedContainer container = section.getStates(); - sections.put(pos, container); + int sx = BlockPos.getX(pos); + int sy = BlockPos.getY(pos); + int sz = BlockPos.getZ(pos); - if (sendBlockEntitiesInChunks && section.maybeHas(BlockState::hasBlockEntity)) { - for (int x = 0; x < 16; x++) { - for (int y = 0; y < 16; y++) { - for (int z = 0; z < 16; z++) { - BlockState blockState = container.get(x, y, z); - if (blockState.hasBlockEntity()) { - mutableBlockPos.set(sx*16 + x, sy*16 + y, sz*16 + z); - BlockEntity blockEntity = chunk.getBlockEntity(mutableBlockPos, LevelChunk.EntityCreationType.CHECK); - if (blockEntity != null) { - CompoundTag tag = blockEntity.saveWithoutMetadata(); - blockEntityMap.put(mutableBlockPos.asLong(), CompressedBlockEntity.compress(tag, baos)); + int distance = Math.abs(playerSectionX - sx) + Math.abs(playerSectionZ - sz); + if (distance > maxChunkLoadDistance) continue; + + LevelChunk chunk = level.getChunk(sx, sz); + int sectionIndex = chunk.getSectionIndexFromSectionY(sy); + if (sectionIndex < 0 || sectionIndex >= chunk.getSectionsCount()) continue; + LevelChunkSection section = chunk.getSection(sectionIndex); + + if (section.hasOnlyAir()) { + sections.put(pos, null); + } else { + PalettedContainer container = section.getStates(); + sections.put(pos, container); + + if (sendBlockEntitiesInChunks && section.maybeHas(BlockState::hasBlockEntity)) { + for (int x = 0; x < 16; x++) { + for (int y = 0; y < 16; y++) { + for (int z = 0; z < 16; z++) { + BlockState blockState = container.get(x, y, z); + if (blockState.hasBlockEntity()) { + mutableBlockPos.set(sx*16 + x, sy*16 + y, sz*16 + z); + BlockEntity blockEntity = chunk.getBlockEntity(mutableBlockPos, LevelChunk.EntityCreationType.CHECK); + if (blockEntity != null) { + CompoundTag tag = blockEntity.saveWithoutMetadata(); + blockEntityMap.put(mutableBlockPos.asLong(), CompressedBlockEntity.compress(tag, baos)); + } } } } @@ -126,7 +142,6 @@ public class RequestChunkDataPacketListener implements PluginMessageListener { } } - // Send response packet boolean firstPart = true; diff --git a/src/main/java/com/moulberry/axiom/packet/SetBlockBufferPacketListener.java b/src/main/java/com/moulberry/axiom/packet/SetBlockBufferPacketListener.java index c7883ac..1db0581 100644 --- a/src/main/java/com/moulberry/axiom/packet/SetBlockBufferPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/SetBlockBufferPacketListener.java @@ -1,35 +1,23 @@ 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; import com.moulberry.axiom.buffer.BlockBuffer; import com.moulberry.axiom.buffer.CompressedBlockEntity; -import com.moulberry.axiom.event.AxiomModifyWorldEvent; -import com.moulberry.axiom.integration.RegionProtection; -import com.moulberry.axiom.integration.SectionProtection; -import com.sk89q.worldedit.bukkit.BukkitAdapter; -import com.sk89q.worldedit.math.BlockVector3; -import com.sk89q.worldedit.world.World; -import com.sk89q.worldguard.LocalPlayer; -import com.sk89q.worldguard.WorldGuard; -import com.sk89q.worldguard.bukkit.WorldGuardPlugin; -import com.sk89q.worldguard.protection.ApplicableRegionSet; -import com.sk89q.worldguard.protection.flags.Flags; -import com.sk89q.worldguard.protection.flags.StateFlag; -import com.sk89q.worldguard.protection.managers.RegionManager; -import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion; -import com.sk89q.worldguard.protection.regions.ProtectedRegion; -import com.sk89q.worldguard.protection.regions.RegionContainer; -import com.sk89q.worldguard.protection.regions.RegionQuery; -import io.netty.buffer.Unpooled; +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; @@ -41,7 +29,6 @@ import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; @@ -51,17 +38,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; @@ -90,13 +79,39 @@ 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); - applyBiomeBuffer(server, buffer, worldKey); + 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); } @@ -106,219 +121,252 @@ public class SetBlockBufferPacketListener { private void applyBlockBuffer(ServerPlayer player, MinecraftServer server, BlockBuffer buffer, ResourceKey worldKey) { server.execute(() -> { - ServerLevel world = server.getLevel(worldKey); - if (world == null) return; + try { + ServerLevel world = player.serverLevel(); + if (!world.dimension().equals(worldKey)) return; - // Call AxiomModifyWorldEvent event - AxiomModifyWorldEvent modifyWorldEvent = new AxiomModifyWorldEvent(player.getBukkitEntity(), world.getWorld()); - Bukkit.getPluginManager().callEvent(modifyWorldEvent); - if (modifyWorldEvent.isCancelled()) return; - - // RegionProtection regionProtection = new RegionProtection(player.getBukkitEntity(), world.getWorld()); - - // Allowed, apply buffer - BlockPos.MutableBlockPos blockPos = new BlockPos.MutableBlockPos(); - - var lightEngine = world.getChunkSource().getLightEngine(); - - 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; } -// SectionProtection sectionProtection = regionProtection.getSection(cx, cy, cz); -// switch (sectionProtection.getSectionState()) { -// case ALLOW -> sectionProtection = null; -// case DENY -> { -// continue; -// } -// case CHECK -> {} -// } + if (!this.plugin.canModifyWorld(player.getBukkitEntity(), world.getWorld())) { + return; + } - LevelChunk chunk = world.getChunk(cx, cz); - chunk.setUnsaved(true); + // 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; } - } - Short2ObjectMap blockEntityChunkMap = buffer.getBlockEntityChunkMap(entry.getLongKey()); + SectionPermissionChecker checker = PlotSquaredIntegration.checkSection(player.getBukkitEntity(), world.getWorld(), cx, cy, cz); + if (checker != null && checker.noneAllowed()) { + continue; + } - for (int x = 0; x < 16; x++) { - for (int y = 0; y < 16; y++) { - for (int z = 0; z < 16; z++) { - BlockState blockState = container.get(x, y, z); - if (blockState == emptyState) continue; + LevelChunk chunk = world.getChunk(cx, cz); -// switch (sectionProtection.getSectionState()) { -// case ALLOW -> {} -// case DENY -> blockState = Blocks.REDSTONE_BLOCK.defaultBlockState(); -// case CHECK -> blockState = Blocks.DIAMOND_BLOCK.defaultBlockState(); -// } + LevelChunkSection section = chunk.getSection(world.getSectionIndexFromSectionY(cy)); + PalettedContainer sectionStates = section.getStates(); + boolean hasOnlyAir = section.hasOnlyAir(); - int bx = cx*16 + x; - int by = cy*16 + y; - int bz = cz*16 + z; + 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 (!regionProtection.canBuild(bx, by, bz)) { -// continue; -// } + boolean sectionChanged = false; + boolean sectionLightChanged = false; - blockPos.set(bx, by, bz); + boolean containerMaybeHasPoi = container.maybeHas(PoiTypes::hasPoi); + boolean sectionMaybeHasPoi = section.maybeHas(PoiTypes::hasPoi); - if (hasOnlyAir && blockState.isAir()) { - continue; - } + Short2ObjectMap blockEntityChunkMap = buffer.getBlockEntityChunkMap(entry.getLongKey()); - BlockState old = section.setBlockState(x, y, z, blockState, true); - if (blockState != old) { - 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); + int minX = 0; + int minY = 0; + int minZ = 0; + int maxX = 15; + int maxY = 15; + int maxZ = 15; - if (false) { // Full update - old.onRemove(world, blockPos, blockState, 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; + } + } - if (sectionStates.get(x, y, z).is(block)) { - blockState.onPlace(world, blockPos, old, false); - } + 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); - } - - // Mark block changed - world.getChunkSource().blockChanged(blockPos); // todo: maybe simply resend chunk instead of this? - - // Update Light - if (LightEngine.hasDifferentLightProperties(chunk, blockPos, old, blockState)) { - chunk.getSkyLightSources().update(chunk, x, by, z); - lightEngine.checkBlock(blockPos); - } - - // Update Poi - Optional> newPoi = PoiTypes.forState(blockState); - Optional> oldPoi = PoiTypes.forState(old); - 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); + } } + } catch (Throwable t) { + player.getBukkitEntity().kick(net.kyori.adventure.text.Component.text("An error occured while processing block change: " + t.getMessage())); } }); } - - private void applyBiomeBuffer(MinecraftServer server, BiomeBuffer biomeBuffer, ResourceKey worldKey) { + private void applyBiomeBuffer(ServerPlayer player, MinecraftServer server, BiomeBuffer biomeBuffer, ResourceKey worldKey) { server.execute(() -> { - ServerLevel world = server.getLevel(worldKey); - if (world == null) return; + try { + ServerLevel world = player.serverLevel(); + if (!world.dimension().equals(worldKey)) 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; - - var section = chunk.getSection(cy - minSection); - PalettedContainer> container = (PalettedContainer>) section.getBiomes(); - - var holder = registry.getHolder(biome); - if (holder.isPresent()) { - container.set(x & 3, y & 3, z & 3, holder.get()); - changedChunks.add(chunk); + if (!this.plugin.canModifyWorld(player.getBukkitEntity(), world.getWorld())) { + return; } - }); - 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); + 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) { + 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); + } + }); + + 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 a0ff7da..5754b4f 100644 --- a/src/main/java/com/moulberry/axiom/packet/SetBlockPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/SetBlockPacketListener.java @@ -1,7 +1,8 @@ package com.moulberry.axiom.packet; +import com.google.common.collect.Maps; import com.moulberry.axiom.AxiomPaper; -import com.moulberry.axiom.event.AxiomModifyWorldEvent; +import com.moulberry.axiom.integration.plotsquared.PlotSquaredIntegration; import io.netty.buffer.Unpooled; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; @@ -12,7 +13,9 @@ import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.InteractionHand; import net.minecraft.world.entity.ai.village.poi.PoiType; import net.minecraft.world.entity.ai.village.poi.PoiTypes; +import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.BlockPlaceContext; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.entity.BlockEntity; @@ -22,8 +25,9 @@ import net.minecraft.world.level.chunk.LevelChunkSection; import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.level.lighting.LightEngine; import net.minecraft.world.phys.BlockHitResult; -import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.block.BlockFace; +import org.bukkit.craftbukkit.v1_20_R1.CraftWorld; import org.bukkit.craftbukkit.v1_20_R1.block.CraftBlock; import org.bukkit.craftbukkit.v1_20_R1.entity.CraftPlayer; import org.bukkit.entity.Player; @@ -38,6 +42,7 @@ import java.lang.reflect.Method; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.function.IntFunction; public class SetBlockPacketListener implements PluginMessageListener { @@ -61,18 +66,19 @@ public class SetBlockPacketListener implements PluginMessageListener { @Override public void onPluginMessageReceived(@NotNull String channel, @NotNull Player bukkitPlayer, @NotNull byte[] message) { - if (!bukkitPlayer.hasPermission("axiom.*")) { + if (!this.plugin.canUseAxiom(bukkitPlayer)) { return; } - // Check if player is allowed to modify this world - AxiomModifyWorldEvent modifyWorldEvent = new AxiomModifyWorldEvent(bukkitPlayer, bukkitPlayer.getWorld()); - Bukkit.getPluginManager().callEvent(modifyWorldEvent); - if (modifyWorldEvent.isCancelled()) return; + if (!this.plugin.canModifyWorld(bukkitPlayer, bukkitPlayer.getWorld())) { + return; + } // Read packet FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); - Map blocks = friendlyByteBuf.readMap(FriendlyByteBuf::readBlockPos, buf -> buf.readById(Block.BLOCK_STATE_REGISTRY)); + IntFunction> mapFunction = FriendlyByteBuf.limitValue(Maps::newLinkedHashMapWithExpectedSize, 512); + Map blocks = friendlyByteBuf.readMap(mapFunction, + FriendlyByteBuf::readBlockPos, buf -> buf.readById(this.plugin.allowedBlockRegistry)); boolean updateNeighbors = friendlyByteBuf.readBoolean(); int reason = friendlyByteBuf.readVarInt(); @@ -105,16 +111,49 @@ public class SetBlockPacketListener implements PluginMessageListener { return; } + CraftWorld world = player.level().getWorld(); + + BlockPlaceContext blockPlaceContext = new BlockPlaceContext(player, hand, player.getItemInHand(hand), blockHit); + // Update blocks if (updateNeighbors) { + int count = 0; for (Map.Entry entry : blocks.entrySet()) { - player.level().setBlock(entry.getKey(), entry.getValue(), 3); - } - } else { - for (Map.Entry entry : blocks.entrySet()) { + if (count++ > 64) break; + BlockPos blockPos = entry.getKey(); BlockState blockState = entry.getValue(); + // Check PlotSquared + if (blockState.isAir()) { + if (!PlotSquaredIntegration.canBreakBlock(bukkitPlayer, world.getBlockAt(blockPos.getX(), blockPos.getY(), blockPos.getZ()))) { + continue; + } + } else if (!PlotSquaredIntegration.canPlaceBlock(bukkitPlayer, new Location(world, blockPos.getX(), blockPos.getY(), blockPos.getZ()))) { + continue; + } + + // Place block + player.level().setBlock(blockPos, blockState, 3); + } + } else { + int count = 0; + for (Map.Entry entry : blocks.entrySet()) { + if (count++ > 64) break; + + BlockPos blockPos = entry.getKey(); + BlockState blockState = entry.getValue(); + + // Check PlotSquared + if (blockState.isAir()) { + if (!PlotSquaredIntegration.canBreakBlock(bukkitPlayer, world.getBlockAt(blockPos.getX(), blockPos.getY(), blockPos.getZ()))) { + continue; + } + } else if (!PlotSquaredIntegration.canPlaceBlock(bukkitPlayer, new Location(world, blockPos.getX(), blockPos.getY(), blockPos.getZ()))) { + continue; + } + + // Place block int bx = blockPos.getX(); int by = blockPos.getY(); int bz = blockPos.getZ(); @@ -191,7 +230,9 @@ public class SetBlockPacketListener implements PluginMessageListener { // Update Light if (LightEngine.hasDifferentLightProperties(chunk, blockPos, old, blockState)) { - chunk.getSkyLightSources().update(chunk, x, by, z); + // Note: Skylight Sources not currently needed on Paper due to Starlight + // This might change in the future, so be careful! + // chunk.getSkyLightSources().update(chunk, x, by, z); level.getChunkSource().getLightEngine().checkBlock(blockPos); } @@ -211,6 +252,15 @@ public class SetBlockPacketListener implements PluginMessageListener { } } + if (!breaking) { + BlockPos clickedPos = blockPlaceContext.getClickedPos(); + ItemStack inHand = player.getItemInHand(hand); + BlockState blockState = player.level().getBlockState(clickedPos); + + BlockItem.updateCustomBlockEntityTag(player.level(), player, clickedPos, inHand); + blockState.getBlock().setPlacedBy(player.level(), clickedPos, blockState, player, inHand); + } + if (sequenceId >= 0) { player.connection.ackBlockChangesUpTo(sequenceId); } diff --git a/src/main/java/com/moulberry/axiom/packet/SetEditorViewsPacketListener.java b/src/main/java/com/moulberry/axiom/packet/SetEditorViewsPacketListener.java index 1fd9bda..ce1465a 100644 --- a/src/main/java/com/moulberry/axiom/packet/SetEditorViewsPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/SetEditorViewsPacketListener.java @@ -1,6 +1,8 @@ package com.moulberry.axiom.packet; +import com.google.common.collect.Lists; import com.moulberry.axiom.AxiomConstants; +import com.moulberry.axiom.AxiomPaper; import com.moulberry.axiom.View; import com.moulberry.axiom.persistence.UUIDDataType; import io.netty.buffer.Unpooled; @@ -13,18 +15,25 @@ import org.jetbrains.annotations.NotNull; import java.util.List; import java.util.UUID; +import java.util.function.IntFunction; public class SetEditorViewsPacketListener implements PluginMessageListener { + private final AxiomPaper plugin; + public SetEditorViewsPacketListener(AxiomPaper plugin) { + this.plugin = plugin; + } + @Override public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, @NotNull byte[] message) { - if (!player.hasPermission("axiom.*")) { + if (!this.plugin.canUseAxiom(player)) { return; } FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); UUID uuid = friendlyByteBuf.readUUID(); - List views = friendlyByteBuf.readList(View::read); + IntFunction> listFunction = FriendlyByteBuf.limitValue(Lists::newArrayListWithCapacity, 64); + List views = friendlyByteBuf.readCollection(listFunction, View::read); PersistentDataContainer container = player.getPersistentDataContainer(); container.set(AxiomConstants.ACTIVE_VIEW, UUIDDataType.INSTANCE, uuid); diff --git a/src/main/java/com/moulberry/axiom/packet/SetFlySpeedPacketListener.java b/src/main/java/com/moulberry/axiom/packet/SetFlySpeedPacketListener.java index fe5fbe3..cbde259 100644 --- a/src/main/java/com/moulberry/axiom/packet/SetFlySpeedPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/SetFlySpeedPacketListener.java @@ -1,5 +1,6 @@ package com.moulberry.axiom.packet; +import com.moulberry.axiom.AxiomPaper; import com.moulberry.axiom.event.AxiomFlySpeedChangeEvent; import io.netty.buffer.Unpooled; import net.minecraft.network.FriendlyByteBuf; @@ -11,9 +12,14 @@ import org.jetbrains.annotations.NotNull; public class SetFlySpeedPacketListener implements PluginMessageListener { + private final AxiomPaper plugin; + public SetFlySpeedPacketListener(AxiomPaper plugin) { + this.plugin = plugin; + } + @Override public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, @NotNull byte[] message) { - if (!player.hasPermission("axiom.*")) { + if (!this.plugin.canUseAxiom(player)) { return; } diff --git a/src/main/java/com/moulberry/axiom/packet/SetGamemodePacketListener.java b/src/main/java/com/moulberry/axiom/packet/SetGamemodePacketListener.java index c9c18e3..692db6a 100644 --- a/src/main/java/com/moulberry/axiom/packet/SetGamemodePacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/SetGamemodePacketListener.java @@ -1,5 +1,6 @@ package com.moulberry.axiom.packet; +import com.moulberry.axiom.AxiomPaper; import com.moulberry.axiom.event.AxiomGameModeChangeEvent; import io.netty.buffer.Unpooled; import net.minecraft.network.FriendlyByteBuf; @@ -13,9 +14,14 @@ import org.jetbrains.annotations.NotNull; public class SetGamemodePacketListener implements PluginMessageListener { + private final AxiomPaper plugin; + public SetGamemodePacketListener(AxiomPaper plugin) { + this.plugin = plugin; + } + @Override public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, @NotNull byte[] message) { - if (!player.hasPermission("axiom.*")) { + if (!this.plugin.canUseAxiom(player)) { return; } diff --git a/src/main/java/com/moulberry/axiom/packet/SetHotbarSlotPacketListener.java b/src/main/java/com/moulberry/axiom/packet/SetHotbarSlotPacketListener.java index a91cd63..3ec626e 100644 --- a/src/main/java/com/moulberry/axiom/packet/SetHotbarSlotPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/SetHotbarSlotPacketListener.java @@ -1,11 +1,11 @@ package com.moulberry.axiom.packet; import com.moulberry.axiom.AxiomConstants; +import com.moulberry.axiom.AxiomPaper; import com.moulberry.axiom.persistence.ItemStackDataType; import io.netty.buffer.Unpooled; import net.minecraft.network.FriendlyByteBuf; import org.bukkit.NamespacedKey; -import org.bukkit.craftbukkit.v1_20_R1.entity.CraftPlayer; import org.bukkit.craftbukkit.v1_20_R1.inventory.CraftItemStack; import org.bukkit.entity.Player; import org.bukkit.persistence.PersistentDataContainer; @@ -15,9 +15,14 @@ import org.jetbrains.annotations.NotNull; public class SetHotbarSlotPacketListener implements PluginMessageListener { + private final AxiomPaper plugin; + public SetHotbarSlotPacketListener(AxiomPaper plugin) { + this.plugin = plugin; + } + @Override public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, @NotNull byte[] message) { - if (!player.hasPermission("axiom.*")) { + if (!this.plugin.canUseAxiom(player)) { return; } diff --git a/src/main/java/com/moulberry/axiom/packet/SetTimePacketListener.java b/src/main/java/com/moulberry/axiom/packet/SetTimePacketListener.java index 74ebb73..5cf48be 100644 --- a/src/main/java/com/moulberry/axiom/packet/SetTimePacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/SetTimePacketListener.java @@ -1,6 +1,8 @@ package com.moulberry.axiom.packet; +import com.moulberry.axiom.AxiomPaper; import com.moulberry.axiom.event.AxiomTimeChangeEvent; +import com.moulberry.axiom.integration.plotsquared.PlotSquaredIntegration; import io.netty.buffer.Unpooled; import net.minecraft.core.registries.Registries; import net.minecraft.network.FriendlyByteBuf; @@ -16,9 +18,14 @@ import org.jetbrains.annotations.NotNull; public class SetTimePacketListener implements PluginMessageListener { + private final AxiomPaper plugin; + public SetTimePacketListener(AxiomPaper plugin) { + this.plugin = plugin; + } + @Override public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, @NotNull byte[] message) { - if (!player.hasPermission("axiom.*")) { + if (!this.plugin.canUseAxiom(player)) { return; } @@ -32,7 +39,17 @@ public class SetTimePacketListener implements PluginMessageListener { ServerLevel level = ((CraftWorld)player.getWorld()).getHandle(); if (!level.dimension().equals(key)) return; - // Call event + // Call modify world + if (!this.plugin.canModifyWorld(player, player.getWorld())) { + return; + } + + // Don't allow on plot worlds + if (PlotSquaredIntegration.isPlotWorld(player.getWorld())) { + return; + } + + // Call time change event AxiomTimeChangeEvent timeChangeEvent = new AxiomTimeChangeEvent(player, time, freezeTime); Bukkit.getPluginManager().callEvent(timeChangeEvent); if (timeChangeEvent.isCancelled()) return; diff --git a/src/main/java/com/moulberry/axiom/packet/SetWorldPropertyListener.java b/src/main/java/com/moulberry/axiom/packet/SetWorldPropertyListener.java index c18a72f..a6b4691 100644 --- a/src/main/java/com/moulberry/axiom/packet/SetWorldPropertyListener.java +++ b/src/main/java/com/moulberry/axiom/packet/SetWorldPropertyListener.java @@ -1,8 +1,9 @@ package com.moulberry.axiom.packet; import com.moulberry.axiom.AxiomPaper; +import com.moulberry.axiom.integration.plotsquared.PlotSquaredIntegration; import com.moulberry.axiom.world_properties.server.ServerWorldPropertiesRegistry; -import com.moulberry.axiom.world_properties.server.ServerWorldProperty; +import com.moulberry.axiom.world_properties.server.ServerWorldPropertyHolder; import io.netty.buffer.Unpooled; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.resources.ResourceLocation; @@ -12,9 +13,14 @@ import org.jetbrains.annotations.NotNull; public class SetWorldPropertyListener implements PluginMessageListener { + private final AxiomPaper plugin; + public SetWorldPropertyListener(AxiomPaper plugin) { + this.plugin = plugin; + } + @Override public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, @NotNull byte[] message) { - if (!player.hasPermission("axiom.*")) { + if (!this.plugin.canUseAxiom(player)) { return; } @@ -24,14 +30,33 @@ public class SetWorldPropertyListener implements PluginMessageListener { byte[] data = friendlyByteBuf.readByteArray(); int updateId = friendlyByteBuf.readVarInt(); - ServerWorldPropertiesRegistry registry = AxiomPaper.PLUGIN.getWorldProperties(player.getWorld()); - if (registry == null) return; - - ServerWorldProperty property = registry.getById(id); - if (property != null && property.getType().getTypeId() == type) { - property.update(player.getWorld(), data); + // Call modify world + if (!this.plugin.canModifyWorld(player, player.getWorld())) { + sendAck(player, updateId); + return; } + // Don't allow on plot worlds + if (PlotSquaredIntegration.isPlotWorld(player.getWorld())) { + sendAck(player, updateId); + return; + } + + ServerWorldPropertiesRegistry registry = AxiomPaper.PLUGIN.getOrCreateWorldProperties(player.getWorld()); + if (registry == null) { + sendAck(player, updateId); + return; + } + + ServerWorldPropertyHolder property = registry.getById(id); + if (property != null && property.getType().getTypeId() == type) { + property.update(player, player.getWorld(), data); + } + + sendAck(player, updateId); + } + + private void sendAck(Player player, int updateId) { FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); buf.writeVarInt(updateId); diff --git a/src/main/java/com/moulberry/axiom/packet/SpawnEntityPacketListener.java b/src/main/java/com/moulberry/axiom/packet/SpawnEntityPacketListener.java new file mode 100644 index 0000000..53b708c --- /dev/null +++ b/src/main/java/com/moulberry/axiom/packet/SpawnEntityPacketListener.java @@ -0,0 +1,123 @@ +package com.moulberry.axiom.packet; + +import com.moulberry.axiom.AxiomPaper; +import com.moulberry.axiom.NbtSanitization; +import io.netty.buffer.Unpooled; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.decoration.HangingEntity; +import net.minecraft.world.entity.decoration.ItemFrame; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.phys.Vec3; +import org.bukkit.craftbukkit.v1_20_R1.CraftWorld; +import org.bukkit.entity.Player; +import org.bukkit.plugin.messaging.PluginMessageListener; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.UUID; + +public class SpawnEntityPacketListener implements PluginMessageListener { + + private final AxiomPaper plugin; + public SpawnEntityPacketListener(AxiomPaper plugin) { + this.plugin = plugin; + } + + private record SpawnEntry(UUID newUuid, double x, double y, double z, float yaw, float pitch, + @Nullable UUID copyFrom, CompoundTag tag) { + public SpawnEntry(FriendlyByteBuf friendlyByteBuf) { + this(friendlyByteBuf.readUUID(), friendlyByteBuf.readDouble(), friendlyByteBuf.readDouble(), + friendlyByteBuf.readDouble(), friendlyByteBuf.readFloat(), friendlyByteBuf.readFloat(), + friendlyByteBuf.readNullable(FriendlyByteBuf::readUUID), friendlyByteBuf.readNbt()); + } + } + + private static final Rotation[] ROTATION_VALUES = Rotation.values(); + + @Override + public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, @NotNull byte[] message) { + if (!this.plugin.canUseAxiom(player)) { + return; + } + + if (!player.hasPermission("axiom.entity.*") && !player.hasPermission("axiom.entity.spawn")) { + return; + } + + if (!this.plugin.canModifyWorld(player, player.getWorld())) { + return; + } + + FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(message)); + List entries = friendlyByteBuf.readList(SpawnEntry::new); + + ServerLevel serverLevel = ((CraftWorld)player.getWorld()).getHandle(); + + List whitelistedEntities = this.plugin.configuration.getStringList("whitelist-entities"); + List blacklistedEntities = this.plugin.configuration.getStringList("blacklist-entities"); + + for (SpawnEntry entry : entries) { + Vec3 position = new Vec3(entry.x, entry.y, entry.z); + + BlockPos blockPos = BlockPos.containing(position); + if (!Level.isInSpawnableBounds(blockPos)) { + continue; + } + + if (serverLevel.getEntity(entry.newUuid) != null) continue; + + CompoundTag tag = entry.tag == null ? new CompoundTag() : entry.tag; + + NbtSanitization.sanitizeEntity(tag); + + if (entry.copyFrom != null) { + Entity entityCopyFrom = serverLevel.getEntity(entry.copyFrom); + if (entityCopyFrom != null) { + CompoundTag compoundTag = new CompoundTag(); + if (entityCopyFrom.saveAsPassenger(compoundTag)) { + compoundTag.remove("Dimension"); + tag = tag.merge(compoundTag); + } + } + } + + if (!tag.contains("id")) continue; + + Entity spawned = EntityType.loadEntityRecursive(tag, serverLevel, entity -> { + String type = EntityType.getKey(entity.getType()).toString(); + if (!whitelistedEntities.isEmpty() && !whitelistedEntities.contains(type)) return null; + if (blacklistedEntities.contains(type)) return null; + + entity.setUUID(entry.newUuid); + + if (entity instanceof HangingEntity hangingEntity) { + float changedYaw = entry.yaw - entity.getYRot(); + int rotations = Math.round(changedYaw / 90); + hangingEntity.rotate(ROTATION_VALUES[rotations & 3]); + + if (entity instanceof ItemFrame itemFrame && itemFrame.getDirection().getAxis() == Direction.Axis.Y) { + itemFrame.setRotation(itemFrame.getRotation() - Math.round(changedYaw / 45)); + } + } + + entity.moveTo(position.x, position.y, position.z, entry.yaw, entry.pitch); + entity.setYHeadRot(entity.getYRot()); + + return entity; + }); + + if (spawned != null) { + serverLevel.tryAddFreshEntityWithPassengers(spawned); + } + } + } + +} diff --git a/src/main/java/com/moulberry/axiom/packet/SwitchActiveHotbarPacketListener.java b/src/main/java/com/moulberry/axiom/packet/SwitchActiveHotbarPacketListener.java index 38badf8..d28c14d 100644 --- a/src/main/java/com/moulberry/axiom/packet/SwitchActiveHotbarPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/SwitchActiveHotbarPacketListener.java @@ -1,6 +1,7 @@ package com.moulberry.axiom.packet; import com.moulberry.axiom.AxiomConstants; +import com.moulberry.axiom.AxiomPaper; import com.moulberry.axiom.persistence.ItemStackDataType; import io.netty.buffer.Unpooled; import net.minecraft.network.FriendlyByteBuf; @@ -17,9 +18,14 @@ import org.jetbrains.annotations.NotNull; public class SwitchActiveHotbarPacketListener implements PluginMessageListener { + private final AxiomPaper plugin; + public SwitchActiveHotbarPacketListener(AxiomPaper plugin) { + this.plugin = plugin; + } + @Override public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, @NotNull byte[] message) { - if (!player.hasPermission("axiom.*")) { + if (!this.plugin.canUseAxiom(player)) { return; } diff --git a/src/main/java/com/moulberry/axiom/packet/TeleportPacketListener.java b/src/main/java/com/moulberry/axiom/packet/TeleportPacketListener.java index 871cc2a..1b66209 100644 --- a/src/main/java/com/moulberry/axiom/packet/TeleportPacketListener.java +++ b/src/main/java/com/moulberry/axiom/packet/TeleportPacketListener.java @@ -1,22 +1,33 @@ package com.moulberry.axiom.packet; -import com.moulberry.axiom.event.AxiomGameModeChangeEvent; +import com.moulberry.axiom.AxiomPaper; import com.moulberry.axiom.event.AxiomTeleportEvent; +import com.moulberry.axiom.event.AxiomUnknownTeleportEvent; import io.netty.buffer.Unpooled; import net.minecraft.core.registries.Registries; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.resources.ResourceKey; import net.minecraft.world.level.Level; -import org.bukkit.*; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.craftbukkit.v1_20_R1.entity.CraftPlayer; +import org.bukkit.craftbukkit.v1_20_R1.util.CraftNamespacedKey; import org.bukkit.entity.Player; import org.bukkit.plugin.messaging.PluginMessageListener; import org.jetbrains.annotations.NotNull; public class TeleportPacketListener implements PluginMessageListener { + private final AxiomPaper plugin; + public TeleportPacketListener(AxiomPaper plugin) { + this.plugin = plugin; + } + @Override public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, @NotNull byte[] message) { - if (!player.hasPermission("axiom.*")) { + if (!this.plugin.canUseAxiom(player)) { return; } @@ -28,10 +39,28 @@ public class TeleportPacketListener implements PluginMessageListener { float yRot = friendlyByteBuf.readFloat(); float xRot = friendlyByteBuf.readFloat(); + // Prevent teleport based on config value + boolean allowTeleportBetweenWorlds = this.plugin.configuration.getBoolean("allow-teleport-between-worlds"); + if (!allowTeleportBetweenWorlds && !((CraftPlayer)player).getHandle().serverLevel().dimension().equals(resourceKey)) { + return; + } + + // Call unknown teleport event + AxiomUnknownTeleportEvent preTeleportEvent = new AxiomUnknownTeleportEvent(player, + CraftNamespacedKey.fromMinecraft(resourceKey.location()), x, y, z, yRot, xRot); + Bukkit.getPluginManager().callEvent(preTeleportEvent); + if (preTeleportEvent.isCancelled()) return; + + // Get bukkit world NamespacedKey namespacedKey = new NamespacedKey(resourceKey.location().getNamespace(), resourceKey.location().getPath()); World world = Bukkit.getWorld(namespacedKey); if (world == null) return; + // Prevent teleport based on config value + if (!allowTeleportBetweenWorlds && world != player.getWorld()) { + return; + } + // Call event AxiomTeleportEvent teleportEvent = new AxiomTeleportEvent(player, new Location(world, x, y, z, yRot, xRot)); Bukkit.getPluginManager().callEvent(teleportEvent); diff --git a/src/main/java/com/moulberry/axiom/world_properties/PropertyUpdateHandler.java b/src/main/java/com/moulberry/axiom/world_properties/PropertyUpdateHandler.java new file mode 100644 index 0000000..de081f3 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/world_properties/PropertyUpdateHandler.java @@ -0,0 +1,18 @@ +package com.moulberry.axiom.world_properties; + +import com.moulberry.axiom.world_properties.server.PropertyUpdateResult; +import org.bukkit.World; +import org.bukkit.entity.Player; + +@FunctionalInterface +public interface PropertyUpdateHandler { + + /** + * @param player the player that updated the property + * @param world the world for which the property has been updated + * @param value the new value of the property + * @return {@link PropertyUpdateResult} + */ + PropertyUpdateResult update(Player player, World world, T value); + +} diff --git a/src/main/java/com/moulberry/axiom/world_properties/WorldPropertyDataType.java b/src/main/java/com/moulberry/axiom/world_properties/WorldPropertyDataType.java index 21f8d85..23790f6 100644 --- a/src/main/java/com/moulberry/axiom/world_properties/WorldPropertyDataType.java +++ b/src/main/java/com/moulberry/axiom/world_properties/WorldPropertyDataType.java @@ -3,9 +3,10 @@ package com.moulberry.axiom.world_properties; import io.netty.buffer.Unpooled; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.util.Unit; import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; import java.nio.charset.StandardCharsets; @@ -23,7 +24,7 @@ public abstract class WorldPropertyDataType { @Override public byte[] serialize(Boolean value) { - return new byte[] { value ? (byte)1 : (byte)0 }; + return new byte[] { value != null && value ? (byte)1 : (byte)0 }; } @Override @@ -40,6 +41,8 @@ public abstract class WorldPropertyDataType { @Override public byte[] serialize(Integer value) { + if (value == null) value = 0; + FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer(8)); buf.writeVarInt(value); @@ -63,6 +66,7 @@ public abstract class WorldPropertyDataType { @Override public byte[] serialize(String value) { + if (value == null) value = ""; return value.getBytes(StandardCharsets.UTF_8); } @@ -80,6 +84,8 @@ public abstract class WorldPropertyDataType { @Override public byte[] serialize(Item value) { + if (value == null) value = Items.AIR; + FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer(8)); buf.writeId(BuiltInRegistries.ITEM, value); @@ -103,6 +109,8 @@ public abstract class WorldPropertyDataType { @Override public byte[] serialize(Block value) { + if (value == null) value = Blocks.AIR; + FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer(8)); buf.writeId(BuiltInRegistries.BLOCK, value); @@ -118,20 +126,20 @@ public abstract class WorldPropertyDataType { } }; - public static WorldPropertyDataType EMPTY = new WorldPropertyDataType<>() { + public static WorldPropertyDataType EMPTY = new WorldPropertyDataType<>() { @Override public int getTypeId() { return 5; } @Override - public byte[] serialize(Unit value) { + public byte[] serialize(Void value) { return new byte[0]; } @Override - public Unit deserialize(byte[] bytes) { - return Unit.INSTANCE; + public Void deserialize(byte[] bytes) { + return null; } }; diff --git a/src/main/java/com/moulberry/axiom/world_properties/WorldPropertyWidgetType.java b/src/main/java/com/moulberry/axiom/world_properties/WorldPropertyWidgetType.java index 547aefb..3dbf612 100644 --- a/src/main/java/com/moulberry/axiom/world_properties/WorldPropertyWidgetType.java +++ b/src/main/java/com/moulberry/axiom/world_properties/WorldPropertyWidgetType.java @@ -1,8 +1,6 @@ package com.moulberry.axiom.world_properties; import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.util.Unit; import java.util.List; @@ -61,10 +59,9 @@ public interface WorldPropertyWidgetType { } }; - - WorldPropertyWidgetType BUTTON = new WorldPropertyWidgetType<>() { + WorldPropertyWidgetType BUTTON = new WorldPropertyWidgetType<>() { @Override - public WorldPropertyDataType dataType() { + public WorldPropertyDataType dataType() { return WorldPropertyDataType.EMPTY; } diff --git a/src/main/java/com/moulberry/axiom/world_properties/server/PropertyUpdateResult.java b/src/main/java/com/moulberry/axiom/world_properties/server/PropertyUpdateResult.java new file mode 100644 index 0000000..01072ec --- /dev/null +++ b/src/main/java/com/moulberry/axiom/world_properties/server/PropertyUpdateResult.java @@ -0,0 +1,25 @@ +package com.moulberry.axiom.world_properties.server; + +public enum PropertyUpdateResult { + + UPDATE_AND_SYNC(true, true), + UPDATE_WITHOUT_SYNC(true, false), + CANCEL(false, false); + + private final boolean update; + private final boolean sync; + + PropertyUpdateResult(boolean update, boolean sync) { + this.update = update; + this.sync = sync; + } + + public boolean isUpdate() { + return this.update; + } + + public boolean isSync() { + return this.sync; + } + +} diff --git a/src/main/java/com/moulberry/axiom/world_properties/server/ServerWorldPropertiesRegistry.java b/src/main/java/com/moulberry/axiom/world_properties/server/ServerWorldPropertiesRegistry.java index 966c024..931c3db 100644 --- a/src/main/java/com/moulberry/axiom/world_properties/server/ServerWorldPropertiesRegistry.java +++ b/src/main/java/com/moulberry/axiom/world_properties/server/ServerWorldPropertiesRegistry.java @@ -6,40 +6,46 @@ import io.netty.buffer.Unpooled; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.level.GameRules; import org.bukkit.GameRule; +import org.bukkit.NamespacedKey; import org.bukkit.World; import org.bukkit.craftbukkit.v1_20_R1.CraftWorld; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; public class ServerWorldPropertiesRegistry { - private final LinkedHashMap>> propertyList = new LinkedHashMap<>(); - private final Map> propertyMap = new HashMap<>(); + private final LinkedHashMap>> propertyList = new LinkedHashMap<>(); + private final Map> propertyMap = new HashMap<>(); + private final World world; public ServerWorldPropertiesRegistry(World world) { - this.registerDefault(world); + this.world = world; + this.registerDefault(); } - public ServerWorldProperty getById(ResourceLocation resourceLocation) { + public ServerWorldPropertyHolder getById(ResourceLocation resourceLocation) { return propertyMap.get(resourceLocation); } - public void addCategory(WorldPropertyCategory category, List> properties) { - this.propertyList.put(category, properties); + @SuppressWarnings("unchecked") + public void addCategory(WorldPropertyCategory category, List> properties) { + List> holders = new ArrayList<>(); + for (ServerWorldPropertyBase property : properties) { + Object defaultValue = property.getDefaultValue(this.world); + holders.add(new ServerWorldPropertyHolder<>(defaultValue, (ServerWorldPropertyBase) property)); + } - for (ServerWorldProperty property : properties) { - ResourceLocation id = property.getId(); + this.propertyList.put(category, holders); + + for (ServerWorldPropertyHolder holder : holders) { + ResourceLocation id = holder.getId(); if (this.propertyMap.containsKey(id)) { throw new RuntimeException("Duplicate property: " + id); } - this.propertyMap.put(id, property); + this.propertyMap.put(id, holder); } } @@ -48,7 +54,7 @@ public class ServerWorldPropertiesRegistry { buf.writeVarInt(this.propertyList.size()); - for (Map.Entry>> entry : this.propertyList.entrySet()) { + for (Map.Entry>> entry : this.propertyList.entrySet()) { entry.getKey().write(buf); buf.writeCollection(entry.getValue(), (buffer, p) -> p.write(buffer)); } @@ -58,46 +64,51 @@ public class ServerWorldPropertiesRegistry { bukkitPlayer.sendPluginMessage(plugin, "axiom:register_world_properties", bytes); } - public void registerDefault(World world) { - ServerLevel serverLevel = ((CraftWorld)world).getHandle(); + private static final ServerWorldProperty TIME = new ServerWorldProperty<>( + new NamespacedKey("axiom", "time"), + "axiom.editorui.window.world_properties.time", + true, WorldPropertyWidgetType.TIME, world -> 0, + (player, w, integer) -> PropertyUpdateResult.UPDATE_WITHOUT_SYNC + ); + public static final ServerWorldProperty PAUSE_WEATHER = new ServerWorldProperty<>( + new NamespacedKey("axiom", "pause_weather"), + "axiom.editorui.window.world_properties.pause_weather", + true, WorldPropertyWidgetType.CHECKBOX, world -> !world.getGameRuleValue(GameRule.DO_WEATHER_CYCLE), + (player, world, bool) -> { + world.setGameRule(GameRule.DO_WEATHER_CYCLE, !bool); + return PropertyUpdateResult.UPDATE_WITHOUT_SYNC; + } + ); + + private static final ServerWorldProperty WEATHER_TYPE = new ServerWorldProperty<>( + new NamespacedKey("axiom", "weather_type"), + "axiom.editorui.window.world_properties.clear_weather", + true, new WorldPropertyWidgetType.ButtonArray( + List.of("axiom.editorui.window.world_properties.rain_weather", "axiom.editorui.window.world_properties.thunder_weather") + ), world -> 0, (player, world, index) -> { + ServerLevel serverLevel = ((CraftWorld)world).getHandle(); + if (index == 0) { + serverLevel.setWeatherParameters(ServerLevel.RAIN_DELAY.sample(serverLevel.random), 0, false, false); + } else if (index == 1) { + serverLevel.setWeatherParameters(0, ServerLevel.RAIN_DURATION.sample(serverLevel.random), true, false); + } else if (index == 2) { + serverLevel.setWeatherParameters(0, ServerLevel.THUNDER_DURATION.sample(serverLevel.random), true, true); + } + return PropertyUpdateResult.UPDATE_WITHOUT_SYNC; + }); + + public void registerDefault() { // Time WorldPropertyCategory timeCategory = new WorldPropertyCategory("axiom.editorui.window.world_properties.time", true); - ServerWorldProperty time = new ServerWorldProperty<>(new ResourceLocation("axiom:time"), - "axiom.editorui.window.world_properties.time", - true, WorldPropertyWidgetType.TIME, 0, integer -> false - ); - - this.addCategory(timeCategory, List.of(time)); + this.addCategory(timeCategory, List.of(TIME)); // Weather WorldPropertyCategory weatherCategory = new WorldPropertyCategory("axiom.editorui.window.world_properties.weather", true); - ServerWorldProperty pauseWeather = new ServerWorldProperty<>(new ResourceLocation("axiom:pause_weather"), - "axiom.editorui.window.world_properties.pause_weather", - true, WorldPropertyWidgetType.CHECKBOX, !world.getGameRuleValue(GameRule.DO_WEATHER_CYCLE), bool -> { - world.setGameRule(GameRule.DO_WEATHER_CYCLE, !bool); - return false; - }); - - ServerWorldProperty weatherType = new ServerWorldProperty<>(new ResourceLocation("axiom:weather_type"), - "axiom.editorui.window.world_properties.clear_weather", - true, new WorldPropertyWidgetType.ButtonArray( - List.of("axiom.editorui.window.world_properties.rain_weather", "axiom.editorui.window.world_properties.thunder_weather") - ), 0, index -> { - if (index == 0) { - serverLevel.setWeatherParameters(ServerLevel.RAIN_DELAY.sample(serverLevel.random), 0, false, false); - } else if (index == 1) { - serverLevel.setWeatherParameters(0, ServerLevel.RAIN_DURATION.sample(serverLevel.random), true, false); - } else if (index == 2) { - serverLevel.setWeatherParameters(0, ServerLevel.THUNDER_DURATION.sample(serverLevel.random), true, true); - } - return false; - }); - - this.addCategory(weatherCategory, List.of(pauseWeather, weatherType)); + this.addCategory(weatherCategory, List.of(PAUSE_WEATHER, WEATHER_TYPE)); } } diff --git a/src/main/java/com/moulberry/axiom/world_properties/server/ServerWorldProperty.java b/src/main/java/com/moulberry/axiom/world_properties/server/ServerWorldProperty.java index e5c8d5b..b38b2e6 100644 --- a/src/main/java/com/moulberry/axiom/world_properties/server/ServerWorldProperty.java +++ b/src/main/java/com/moulberry/axiom/world_properties/server/ServerWorldProperty.java @@ -1,81 +1,33 @@ package com.moulberry.axiom.world_properties.server; -import com.moulberry.axiom.AxiomPaper; -import com.moulberry.axiom.world_properties.WorldPropertyDataType; +import com.moulberry.axiom.world_properties.PropertyUpdateHandler; import com.moulberry.axiom.world_properties.WorldPropertyWidgetType; -import io.netty.buffer.Unpooled; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.resources.ResourceLocation; +import org.bukkit.NamespacedKey; import org.bukkit.World; import org.bukkit.entity.Player; -import java.util.function.Predicate; +import java.util.function.Function; -public class ServerWorldProperty { +public class ServerWorldProperty extends ServerWorldPropertyBase { - private final ResourceLocation id; - private final String name; - private final boolean localizeName; - private WorldPropertyWidgetType widget; - private T value; - private Predicate handler; + private final Function defaultValueFunction; + private final PropertyUpdateHandler handler; - public ServerWorldProperty(ResourceLocation id, String name, boolean localizeName, WorldPropertyWidgetType widget, - T value, Predicate handler) { - this.id = id; - this.name = name; - this.localizeName = localizeName; - this.widget = widget; - this.value = value; + public ServerWorldProperty(NamespacedKey id, String name, boolean localizeName, WorldPropertyWidgetType widget, + Function defaultValueFunction, PropertyUpdateHandler handler) { + super(id, name, localizeName, widget); + this.defaultValueFunction = defaultValueFunction; this.handler = handler; } - public ResourceLocation getId() { - return this.id; + @Override + public T getDefaultValue(World world) { + return this.defaultValueFunction.apply(world); } - public WorldPropertyDataType getType() { - return this.widget.dataType(); - } - - public void update(World world, byte[] data) { - this.value = this.widget.dataType().deserialize(data); - if (this.handler.test(this.value)) { - this.sync(world); - } - } - - public void setValueWithoutSyncing(T value) { - this.value = value; - } - - public void setValue(World world, T value) { - this.value = value; - this.sync(world); - } - - public void sync(World world) { - FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); - - buf.writeResourceLocation(this.id); - buf.writeVarInt(this.widget.dataType().getTypeId()); - buf.writeByteArray(this.widget.dataType().serialize(this.value)); - - byte[] message = new byte[buf.writerIndex()]; - buf.getBytes(0, message); - for (Player player : world.getPlayers()) { - if (AxiomPaper.PLUGIN.activeAxiomPlayers.contains(player.getUniqueId())) { - player.sendPluginMessage(AxiomPaper.PLUGIN, "axiom:set_world_property", message); - } - } - } - - public void write(FriendlyByteBuf friendlyByteBuf) { - friendlyByteBuf.writeResourceLocation(this.id); - friendlyByteBuf.writeUtf(this.name); - friendlyByteBuf.writeBoolean(this.localizeName); - this.widget.write(friendlyByteBuf); - friendlyByteBuf.writeByteArray(this.widget.dataType().serialize(this.value)); + @Override + public PropertyUpdateResult handleUpdateProperty(Player player, World world, T value) { + return this.handler.update(player, world, value); } } diff --git a/src/main/java/com/moulberry/axiom/world_properties/server/ServerWorldPropertyBase.java b/src/main/java/com/moulberry/axiom/world_properties/server/ServerWorldPropertyBase.java new file mode 100644 index 0000000..b14f373 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/world_properties/server/ServerWorldPropertyBase.java @@ -0,0 +1,64 @@ +package com.moulberry.axiom.world_properties.server; + +import com.moulberry.axiom.AxiomPaper; +import com.moulberry.axiom.world_properties.WorldPropertyDataType; +import com.moulberry.axiom.world_properties.WorldPropertyWidgetType; +import net.minecraft.resources.ResourceLocation; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.craftbukkit.v1_20_R1.util.CraftNamespacedKey; +import org.bukkit.entity.Player; + +public abstract class ServerWorldPropertyBase { + + private final ResourceLocation id; + /*package-private*/ final String name; + /*package-private*/ final boolean localizeName; + /*package-private*/ WorldPropertyWidgetType widget; + + public ServerWorldPropertyBase(NamespacedKey id, String name, boolean localizeName, WorldPropertyWidgetType widget) { + this.id = CraftNamespacedKey.toMinecraft(id); + this.name = name; + this.localizeName = localizeName; + this.widget = widget; + } + + public abstract T getDefaultValue(World world); + + public abstract PropertyUpdateResult handleUpdateProperty(Player player, World world, T value); + + public ResourceLocation getId() { + return this.id; + } + + public WorldPropertyDataType getType() { + return this.widget.dataType(); + } + + @SuppressWarnings("unchecked") + public boolean setValueWithoutSyncing(World world, T value) { + ServerWorldPropertiesRegistry properties = AxiomPaper.PLUGIN.getWorldPropertiesIfPresent(world); + if (properties != null) { + ServerWorldPropertyHolder property = properties.getById(this.id); + if (property != null && property.getProperty() == this) { + ((ServerWorldPropertyHolder)property).setValueWithoutSyncing(value); + return true; + } + } + return false; + } + + @SuppressWarnings("unchecked") + public boolean setValue(World world, T value) { + ServerWorldPropertiesRegistry properties = AxiomPaper.PLUGIN.getWorldPropertiesIfPresent(world); + if (properties != null) { + ServerWorldPropertyHolder property = properties.getById(this.id); + if (property != null && property.getProperty() == this) { + ((ServerWorldPropertyHolder)property).setValue(world, value); + return true; + } + } + return false; + } + +} diff --git a/src/main/java/com/moulberry/axiom/world_properties/server/ServerWorldPropertyHolder.java b/src/main/java/com/moulberry/axiom/world_properties/server/ServerWorldPropertyHolder.java new file mode 100644 index 0000000..d97dda5 --- /dev/null +++ b/src/main/java/com/moulberry/axiom/world_properties/server/ServerWorldPropertyHolder.java @@ -0,0 +1,90 @@ +package com.moulberry.axiom.world_properties.server; + +import com.moulberry.axiom.AxiomPaper; +import com.moulberry.axiom.world_properties.WorldPropertyDataType; +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import org.bukkit.World; +import org.bukkit.entity.Player; + +import java.util.Objects; + +public class ServerWorldPropertyHolder { + + private T value; + private ServerWorldPropertyBase property; + private boolean unsyncedValue = false; + + public ServerWorldPropertyHolder(T value, ServerWorldPropertyBase property) { + this.value = value; + this.property = property; + } + + public ResourceLocation getId() { + return this.property.getId(); + } + + public WorldPropertyDataType getType() { + return this.property.widget.dataType(); + } + + public ServerWorldPropertyBase getProperty() { + return property; + } + + public void update(Player player, World world, byte[] data) { + T newValue = this.property.widget.dataType().deserialize(data); + + PropertyUpdateResult result = this.property.handleUpdateProperty(player, world, newValue); + + if (result.isUpdate()) { + this.value = newValue; + + if (result.isSync()) { + this.sync(world); + } else { + this.unsyncedValue = true; + } + } + } + + public void setValueWithoutSyncing(T value) { + this.value = value; + } + + public void setValue(World world, T value) { + boolean sync = this.unsyncedValue || !Objects.equals(value, this.value); + this.value = value; + if (sync) { + this.sync(world); + } + } + + public void sync(World world) { + FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + + buf.writeResourceLocation(this.getId()); + buf.writeVarInt(this.property.widget.dataType().getTypeId()); + buf.writeByteArray(this.property.widget.dataType().serialize(this.value)); + + byte[] message = new byte[buf.writerIndex()]; + buf.getBytes(0, message); + for (Player player : world.getPlayers()) { + if (AxiomPaper.PLUGIN.activeAxiomPlayers.contains(player.getUniqueId())) { + player.sendPluginMessage(AxiomPaper.PLUGIN, "axiom:set_world_property", message); + } + } + + this.unsyncedValue = false; + } + + public void write(FriendlyByteBuf friendlyByteBuf) { + friendlyByteBuf.writeResourceLocation(this.getId()); + friendlyByteBuf.writeUtf(this.property.name); + friendlyByteBuf.writeBoolean(this.property.localizeName); + this.property.widget.write(friendlyByteBuf); + friendlyByteBuf.writeByteArray(this.property.widget.dataType().serialize(this.value)); + } + +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..f5c0b38 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,78 @@ +# Max chunk sends per tick (per-world), 0 = no limit +max-chunk-sends-per-tick: 0 + +# Max chunk relights per tick (per-world), 0 = no limit +max-chunk-relights-per-tick: 0 + +# Maximum distance from which players can load chunks +# Set to 0 to only allow editing within render distance +max-chunk-load-distance: 128 + +# Whether players are allowed to teleport between worlds using views +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: "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 + +# Regex for whitelisting worlds +# The world can only be modified if the regex matches the world's name +whitelist-world-regex: null + +# Regex for blacklisting worlds +# 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 + +# 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" + +# Blacklist entities that can be spawned/manipulated/deleted by the client +blacklist-entities: +# - "minecraft:ender_dragon" + +# True allows players to see/manipulate marker entities +send-markers: 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 + set-gamemode: true + set-fly-speed: true + set-world-time: true + set-world-property: true + set-single-block: true + set-hotbar-slot: true + switch-active-hotbar: true + teleport: true + set-editor-views: true + request-chunk-data: true + set-buffer: true + spawn-entity: true + manipulate-entity: true + delete-entity: true + marker-nbt-request: true diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 67339da..53267da 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -7,6 +7,22 @@ authors: api-version: "$apiVersion" permissions: axiom.*: - description: Allows use of all Axiom features + description: Allows use of all default Axiom features default: op + axiom.entity.*: + description: Allows use of all entity-related features (spawning, manipulating, deleting) + default: op + axiom.entity.spawn: + description: Allows entity spawning + axiom.entity.manipulate: + 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 diff --git a/steamwarci.yml b/steamwarci.yml index a42f256..7119112 100644 --- a/steamwarci.yml +++ b/steamwarci.yml @@ -6,7 +6,7 @@ build: - "JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 ./gradlew --stop" artifacts: - "/binarys/AxiomPaper.jar": "build/libs/AxiomPaper-1.5.1.jar" + "/binarys/AxiomPaper.jar": "build/libs/AxiomPaper-1.5.8.jar" release: - - "mvn deploy:deploy-file -DgroupId=de.steamwar -DartifactId=axiompaper -Dversion=RELEASE -Dpackaging=jar -Dfile=build/libs/AxiomPaper-1.5.1.jar -Durl=file:///var/www/html/maven/" \ No newline at end of file + - "mvn deploy:deploy-file -DgroupId=de.steamwar -DartifactId=axiompaper -Dversion=RELEASE -Dpackaging=jar -Dfile=build/libs/AxiomPaper-1.5.8.jar -Durl=file:///var/www/html/maven/" \ No newline at end of file