From 5b4322e53a848b8f07bc0196690187889dc4c65b Mon Sep 17 00:00:00 2001 From: Yeregorix Date: Thu, 14 Sep 2023 11:09:27 +0200 Subject: [PATCH] Fix left click on air being ignored and right click on block being handled twice (#2153) * Fix left click on air being ignored and right click on block being handled twice * Factorize debouncing and cache event result * Fix ForgePlatform#server being always null --- .../bukkit/BukkitServerInterface.java | 9 ++ .../worldedit/bukkit/WorldEditListener.java | 84 +++++------ .../extension/platform/AbstractPlatform.java | 4 + .../extension/platform/Platform.java | 8 + .../internal/event/InteractionDebouncer.java | 69 +++++++++ .../worldedit/fabric/FabricPlatform.java | 5 + .../worldedit/fabric/FabricWorldEdit.java | 67 +++++---- .../mixin/AccessorServerPlayerGameMode.java | 31 ++++ .../MixinServerGamePacketListenerImpl.java | 59 ++++++++ .../resources/worldedit-fabric.mixins.json | 2 + .../sk89q/worldedit/forge/ForgePlatform.java | 26 ++-- .../sk89q/worldedit/forge/ForgeWorldEdit.java | 125 ++++++++++------ .../mixin/AccessorServerPlayerGameMode.java | 31 ++++ .../MixinServerGamePacketListenerImpl.java | 59 ++++++++ .../net/handler/InternalPacketHandler.java | 43 ------ .../net/packet/LeftClickAirEventMessage.java | 51 ------- .../resources/worldedit-forge.mixins.json | 4 +- .../worldedit/sponge/SpongePlatform.java | 5 + .../worldedit/sponge/SpongeWorldEdit.java | 137 +++++++++++------- 19 files changed, 557 insertions(+), 262 deletions(-) create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/internal/event/InteractionDebouncer.java create mode 100644 worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/mixin/AccessorServerPlayerGameMode.java create mode 100644 worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/mixin/MixinServerGamePacketListenerImpl.java create mode 100644 worldedit-forge/src/main/java/com/sk89q/worldedit/forge/mixin/AccessorServerPlayerGameMode.java create mode 100644 worldedit-forge/src/main/java/com/sk89q/worldedit/forge/mixin/MixinServerGamePacketListenerImpl.java delete mode 100644 worldedit-forge/src/main/java/com/sk89q/worldedit/forge/net/handler/InternalPacketHandler.java delete mode 100644 worldedit-forge/src/main/java/com/sk89q/worldedit/forge/net/packet/LeftClickAirEventMessage.java diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitServerInterface.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitServerInterface.java index 6d74f5d55..f72e0b58e 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitServerInterface.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitServerInterface.java @@ -37,6 +37,7 @@ import com.sk89q.worldedit.util.lifecycle.Lifecycled; import com.sk89q.worldedit.world.DataFixer; import com.sk89q.worldedit.world.registry.Registries; +import io.papermc.lib.PaperLib; import org.bukkit.Bukkit; import org.bukkit.Server; import org.bukkit.World; @@ -234,6 +235,14 @@ public Set getSupportedSideEffects() { return SUPPORTED_SIDE_EFFECTS; } + @Override + public long getTickCount() { + if (PaperLib.isPaper()) { + return Bukkit.getCurrentTick(); + } + return super.getTickCount(); + } + public void unregisterCommands() { dynamicCommands.unregisterCommands(); } diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/WorldEditListener.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/WorldEditListener.java index fd72b13e5..89a038315 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/WorldEditListener.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/WorldEditListener.java @@ -25,6 +25,7 @@ import com.sk89q.worldedit.entity.Player; import com.sk89q.worldedit.event.platform.SessionIdleEvent; import com.sk89q.worldedit.extension.platform.Actor; +import com.sk89q.worldedit.internal.event.InteractionDebouncer; import com.sk89q.worldedit.util.Direction; import com.sk89q.worldedit.util.Location; import com.sk89q.worldedit.world.World; @@ -52,6 +53,7 @@ public class WorldEditListener implements Listener { private final WorldEditPlugin plugin; + private final InteractionDebouncer debouncer; /** * Construct the object. @@ -60,6 +62,7 @@ public class WorldEditListener implements Listener { */ public WorldEditListener(WorldEditPlugin plugin) { this.plugin = plugin; + debouncer = new InteractionDebouncer(plugin.getInternalPlatform()); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) @@ -93,62 +96,59 @@ public void onPlayerCommandSend(PlayerCommandSendEvent event) { */ @EventHandler public void onPlayerInteract(PlayerInteractEvent event) { - if (!plugin.getInternalPlatform().isHookingEvents()) { - return; - } - - if (event.useItemInHand() == Result.DENY) { - return; - } - - if (event.getHand() == EquipmentSlot.OFF_HAND) { + if (!plugin.getInternalPlatform().isHookingEvents() + || event.useItemInHand() == Result.DENY + || event.getHand() == EquipmentSlot.OFF_HAND + || event.getAction() == Action.PHYSICAL) { return; } final Player player = plugin.wrapPlayer(event.getPlayer()); + + if (event.getAction() != Action.LEFT_CLICK_BLOCK) { + Optional previousResult = debouncer.getDuplicateInteractionResult(player); + if (previousResult.isPresent()) { + if (previousResult.get()) { + event.setCancelled(true); + } + return; + } + } + final World world = player.getWorld(); final WorldEdit we = plugin.getWorldEdit(); final Direction direction = BukkitAdapter.adapt(event.getBlockFace()); + final Block clickedBlock = event.getClickedBlock(); + final Location pos = clickedBlock == null ? null : new Location(world, clickedBlock.getX(), clickedBlock.getY(), clickedBlock.getZ()); - Action action = event.getAction(); - if (action == Action.LEFT_CLICK_BLOCK) { - final Block clickedBlock = event.getClickedBlock(); - final Location pos = new Location(world, clickedBlock.getX(), clickedBlock.getY(), clickedBlock.getZ()); + boolean result = false; + switch (event.getAction()) { + case LEFT_CLICK_BLOCK: + result = we.handleBlockLeftClick(player, pos, direction) || we.handleArmSwing(player); + break; + case LEFT_CLICK_AIR: + result = we.handleArmSwing(player); + break; + case RIGHT_CLICK_BLOCK: + result = we.handleBlockRightClick(player, pos, direction) || we.handleRightClick(player); + break; + case RIGHT_CLICK_AIR: + result = we.handleRightClick(player); + break; + default: + break; + } - if (we.handleBlockLeftClick(player, pos, direction)) { - event.setCancelled(true); - } - - if (we.handleArmSwing(player)) { - event.setCancelled(true); - } - - } else if (action == Action.LEFT_CLICK_AIR) { - - if (we.handleArmSwing(player)) { - event.setCancelled(true); - } - - } else if (action == Action.RIGHT_CLICK_BLOCK) { - final Block clickedBlock = event.getClickedBlock(); - final Location pos = new Location(world, clickedBlock.getX(), clickedBlock.getY(), clickedBlock.getZ()); - - if (we.handleBlockRightClick(player, pos, direction)) { - event.setCancelled(true); - } - - if (we.handleRightClick(player)) { - event.setCancelled(true); - } - } else if (action == Action.RIGHT_CLICK_AIR) { - if (we.handleRightClick(player)) { - event.setCancelled(true); - } + debouncer.setLastInteraction(player, result); + if (result) { + event.setCancelled(true); } } @EventHandler public void onPlayerQuit(PlayerQuitEvent event) { + debouncer.clearInteraction(plugin.wrapPlayer(event.getPlayer())); + plugin.getWorldEdit().getEventBus().post(new SessionIdleEvent(new BukkitPlayer.SessionKeyImpl(event.getPlayer()))); } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/AbstractPlatform.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/AbstractPlatform.java index 6f93277b8..3bf41a1ef 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/AbstractPlatform.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/AbstractPlatform.java @@ -84,4 +84,8 @@ public void reload() { getTranslationManager().reload(); } + @Override + public long getTickCount() { + return System.nanoTime() / 50_000_000; + } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/Platform.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/Platform.java index b4deb56f7..7edc9b942 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/Platform.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/Platform.java @@ -207,4 +207,12 @@ default void registerGameHooks() { * @return A set of supported side effects */ Set getSupportedSideEffects(); + + /** + * Get the number of ticks since the server started. + * On some platforms this value may be an approximation based on the JVM run time. + * + * @return The number of ticks since the server started. + */ + long getTickCount(); } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/event/InteractionDebouncer.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/event/InteractionDebouncer.java new file mode 100644 index 000000000..b33570cd9 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/event/InteractionDebouncer.java @@ -0,0 +1,69 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit 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 . + */ + +package com.sk89q.worldedit.internal.event; + +import com.sk89q.worldedit.extension.platform.Platform; +import com.sk89q.worldedit.util.Identifiable; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +public class InteractionDebouncer { + private final Platform platform; + private final Map lastInteractions = new HashMap<>(); + + public InteractionDebouncer(Platform platform) { + this.platform = platform; + } + + public void clearInteraction(Identifiable player) { + lastInteractions.remove(player.getUniqueId()); + } + + public void setLastInteraction(Identifiable player, boolean result) { + lastInteractions.put(player.getUniqueId(), new Interaction(platform.getTickCount(), result)); + } + + public Optional getDuplicateInteractionResult(Identifiable player) { + Interaction last = lastInteractions.get(player.getUniqueId()); + if (last == null) { + return Optional.empty(); + } + + long now = platform.getTickCount(); + if (now - last.tick <= 1) { + return Optional.of(last.result); + } + + return Optional.empty(); + } + + private static class Interaction { + public final long tick; + public final boolean result; + + public Interaction(long tick, boolean result) { + this.tick = tick; + this.result = result; + } + } +} diff --git a/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/FabricPlatform.java b/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/FabricPlatform.java index 7896f2055..4d4436623 100644 --- a/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/FabricPlatform.java +++ b/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/FabricPlatform.java @@ -213,6 +213,11 @@ public Set getSupportedSideEffects() { : SUPPORTED_SIDE_EFFECTS_NO_MIXIN; } + @Override + public long getTickCount() { + return FabricWorldEdit.LIFECYCLED_SERVER.valueOrThrow().getTickCount(); + } + @Override public Collection getConnectedUsers() { List users = new ArrayList<>(); diff --git a/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/FabricWorldEdit.java b/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/FabricWorldEdit.java index a4045c0cf..2ff84cfa5 100644 --- a/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/FabricWorldEdit.java +++ b/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/FabricWorldEdit.java @@ -32,6 +32,7 @@ import com.sk89q.worldedit.extension.platform.PlatformManager; import com.sk89q.worldedit.fabric.net.handler.WECUIPacketHandler; import com.sk89q.worldedit.internal.anvil.ChunkDeleter; +import com.sk89q.worldedit.internal.event.InteractionDebouncer; import com.sk89q.worldedit.internal.util.LogManagerCompat; import com.sk89q.worldedit.util.Location; import com.sk89q.worldedit.util.lifecycle.Lifecycled; @@ -81,6 +82,7 @@ import java.nio.file.Path; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.Set; import static com.google.common.base.Preconditions.checkNotNull; @@ -113,6 +115,7 @@ public static Registry getRegistry(ResourceKey> key) { public static FabricWorldEdit inst; + private InteractionDebouncer debouncer; private FabricPlatform platform; private FabricConfiguration config; private Path workingDir; @@ -139,6 +142,7 @@ public void onInitialize() { } } this.platform = new FabricPlatform(this); + debouncer = new InteractionDebouncer(platform); WorldEdit.getInstance().getPlatformManager().register(platform); @@ -250,16 +254,16 @@ private void onStopServer(MinecraftServer minecraftServer) { WorldEdit.getInstance().getEventBus().post(new PlatformUnreadyEvent(platform)); } - private boolean shouldSkip() { - if (platform == null) { - return true; - } + private boolean skipEvents() { + return platform == null || !platform.isHookingEvents(); + } - return !platform.isHookingEvents(); // We have to be told to catch these events + private boolean skipInteractionEvent(Player player, InteractionHand hand) { + return skipEvents() || hand != InteractionHand.MAIN_HAND || player.level().isClientSide || !(player instanceof ServerPlayer); } private InteractionResult onLeftClickBlock(Player playerEntity, Level world, InteractionHand hand, BlockPos blockPos, Direction direction) { - if (shouldSkip() || hand == InteractionHand.OFF_HAND || world.isClientSide) { + if (skipInteractionEvent(playerEntity, hand)) { return InteractionResult.PASS; } @@ -273,19 +277,14 @@ private InteractionResult onLeftClickBlock(Player playerEntity, Level world, Int ); com.sk89q.worldedit.util.Direction weDirection = FabricAdapter.adaptEnumFacing(direction); - if (we.handleBlockLeftClick(player, pos, weDirection)) { - return InteractionResult.SUCCESS; - } + boolean result = we.handleBlockLeftClick(player, pos, weDirection) || we.handleArmSwing(player); + debouncer.setLastInteraction(player, result); - if (we.handleArmSwing(player)) { - return InteractionResult.SUCCESS; - } - - return InteractionResult.PASS; + return result ? InteractionResult.SUCCESS : InteractionResult.PASS; } private InteractionResult onRightClickBlock(Player playerEntity, Level world, InteractionHand hand, BlockHitResult blockHitResult) { - if (shouldSkip() || hand == InteractionHand.OFF_HAND || world.isClientSide) { + if (skipInteractionEvent(playerEntity, hand)) { return InteractionResult.PASS; } @@ -299,36 +298,52 @@ private InteractionResult onRightClickBlock(Player playerEntity, Level world, In ); com.sk89q.worldedit.util.Direction direction = FabricAdapter.adaptEnumFacing(blockHitResult.getDirection()); - if (we.handleBlockRightClick(player, pos, direction)) { - return InteractionResult.SUCCESS; + boolean result = we.handleBlockRightClick(player, pos, direction) || we.handleRightClick(player); + debouncer.setLastInteraction(player, result); + + return result ? InteractionResult.SUCCESS : InteractionResult.PASS; + } + + public void onLeftClickAir(ServerPlayer playerEntity, InteractionHand hand) { + if (skipInteractionEvent(playerEntity, hand)) { + return; } - if (we.handleRightClick(player)) { - return InteractionResult.SUCCESS; + WorldEdit we = WorldEdit.getInstance(); + FabricPlayer player = adaptPlayer(playerEntity); + + Optional previousResult = debouncer.getDuplicateInteractionResult(player); + if (previousResult.isPresent()) { + return; } - return InteractionResult.PASS; + boolean result = we.handleArmSwing(player); + debouncer.setLastInteraction(player, result); } private InteractionResultHolder onRightClickAir(Player playerEntity, Level world, InteractionHand hand) { ItemStack stackInHand = playerEntity.getItemInHand(hand); - if (shouldSkip() || hand == InteractionHand.OFF_HAND || world.isClientSide) { + if (skipInteractionEvent(playerEntity, hand)) { return InteractionResultHolder.pass(stackInHand); } WorldEdit we = WorldEdit.getInstance(); FabricPlayer player = adaptPlayer((ServerPlayer) playerEntity); - if (we.handleRightClick(player)) { - return InteractionResultHolder.success(stackInHand); + Optional previousResult = debouncer.getDuplicateInteractionResult(player); + if (previousResult.isPresent()) { + return previousResult.get() ? InteractionResultHolder.success(stackInHand) : InteractionResultHolder.pass(stackInHand); } - return InteractionResultHolder.pass(stackInHand); + boolean result = we.handleRightClick(player); + debouncer.setLastInteraction(player, result); + + return result ? InteractionResultHolder.success(stackInHand) : InteractionResultHolder.pass(stackInHand); } - // TODO Pass empty left click to server - private void onPlayerDisconnect(ServerGamePacketListenerImpl handler, MinecraftServer server) { + debouncer.clearInteraction(adaptPlayer(handler.player)); + WorldEdit.getInstance().getEventBus() .post(new SessionIdleEvent(new FabricPlayer.SessionKeyImpl(handler.player))); } diff --git a/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/mixin/AccessorServerPlayerGameMode.java b/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/mixin/AccessorServerPlayerGameMode.java new file mode 100644 index 000000000..c9aaaa62d --- /dev/null +++ b/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/mixin/AccessorServerPlayerGameMode.java @@ -0,0 +1,31 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit 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 . + */ + +package com.sk89q.worldedit.fabric.mixin; + +import net.minecraft.server.level.ServerPlayerGameMode; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(ServerPlayerGameMode.class) +public interface AccessorServerPlayerGameMode { + + @Accessor("isDestroyingBlock") + boolean isDestroyingBlock(); +} diff --git a/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/mixin/MixinServerGamePacketListenerImpl.java b/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/mixin/MixinServerGamePacketListenerImpl.java new file mode 100644 index 000000000..ed238fda8 --- /dev/null +++ b/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/mixin/MixinServerGamePacketListenerImpl.java @@ -0,0 +1,59 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit 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 . + */ + +package com.sk89q.worldedit.fabric.mixin; + +import com.sk89q.worldedit.fabric.FabricWorldEdit; +import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket; +import net.minecraft.network.protocol.game.ServerboundSwingPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ServerGamePacketListenerImpl.class) +public class MixinServerGamePacketListenerImpl { + @Shadow + public ServerPlayer player; + + private int ignoreSwingPackets; + + @Inject(method = "handleAnimate", at = @At("HEAD")) + private void onAnimate(ServerboundSwingPacket packet, CallbackInfo ci) { + if (!((AccessorServerPlayerGameMode) this.player.gameMode).isDestroyingBlock()) { + if (this.ignoreSwingPackets > 0) { + this.ignoreSwingPackets--; + } else if (FabricWorldEdit.inst != null) { + FabricWorldEdit.inst.onLeftClickAir(this.player, packet.getHand()); + } + } + } + + @Inject(method = "handlePlayerAction", at = @At("HEAD")) + private void onAction(ServerboundPlayerActionPacket packet, CallbackInfo ci) { + switch (packet.getAction()) { + case DROP_ITEM, DROP_ALL_ITEMS, START_DESTROY_BLOCK -> this.ignoreSwingPackets++; + default -> { + } + } + } +} diff --git a/worldedit-fabric/src/main/resources/worldedit-fabric.mixins.json b/worldedit-fabric/src/main/resources/worldedit-fabric.mixins.json index 076670106..357780004 100644 --- a/worldedit-fabric/src/main/resources/worldedit-fabric.mixins.json +++ b/worldedit-fabric/src/main/resources/worldedit-fabric.mixins.json @@ -7,8 +7,10 @@ "AccessorPrimaryLevelData", "AccessorDerivedLevelData", "AccessorServerChunkCache", + "AccessorServerPlayerGameMode", "MixinLevelChunkSetBlockHook", "MixinMinecraftServer", + "MixinServerGamePacketListenerImpl", "MixinServerPlayer" ], "plugin": "com.sk89q.worldedit.fabric.internal.MixinConfigPlugin", diff --git a/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/ForgePlatform.java b/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/ForgePlatform.java index 2c66dced7..6cd07d0e8 100644 --- a/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/ForgePlatform.java +++ b/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/ForgePlatform.java @@ -63,18 +63,14 @@ class ForgePlatform extends AbstractPlatform implements MultiUserPlatform { private final ForgeWorldEdit mod; - private final MinecraftServer server; private final ForgeDataFixer dataFixer; - private final @Nullable ForgeWatchdog watchdog; + private @Nullable ForgeWatchdog watchdog; private boolean hookingEvents = false; private final ResourceLoader resourceLoader = new ForgeResourceLoader(WorldEdit.getInstance()); ForgePlatform(ForgeWorldEdit mod) { this.mod = mod; - this.server = ServerLifecycleHooks.getCurrentServer(); this.dataFixer = new ForgeDataFixer(getDataVersion()); - this.watchdog = server instanceof DedicatedServer - ? new ForgeWatchdog((DedicatedServer) server) : null; } boolean isHookingEvents() { @@ -120,12 +116,18 @@ public int schedule(long delay, long period, Runnable task) { @Override @Nullable public ForgeWatchdog getWatchdog() { + if (watchdog == null) { + MinecraftServer server = ServerLifecycleHooks.getCurrentServer(); + if (server instanceof DedicatedServer) { + watchdog = new ForgeWatchdog((DedicatedServer) server); + } + } return watchdog; } @Override public List getWorlds() { - Iterable worlds = server.getAllLevels(); + Iterable worlds = ServerLifecycleHooks.getCurrentServer().getAllLevels(); List ret = new ArrayList<>(); for (ServerLevel world : worlds) { ret.add(new ForgeWorld(world)); @@ -139,7 +141,7 @@ public Player matchPlayer(Player player) { if (player instanceof ForgePlayer) { return player; } else { - ServerPlayer entity = server.getPlayerList().getPlayerByName(player.getName()); + ServerPlayer entity = ServerLifecycleHooks.getCurrentServer().getPlayerList().getPlayerByName(player.getName()); return entity != null ? new ForgePlayer(entity) : null; } } @@ -150,7 +152,7 @@ public World matchWorld(World world) { if (world instanceof ForgeWorld) { return world; } else { - for (ServerLevel ws : server.getAllLevels()) { + for (ServerLevel ws : ServerLifecycleHooks.getCurrentServer().getAllLevels()) { if (((ServerLevelData) ws.getLevelData()).getLevelName().equals(world.getName())) { return new ForgeWorld(ws); } @@ -162,6 +164,7 @@ public World matchWorld(World world) { @Override public void registerCommands(CommandManager manager) { + MinecraftServer server = ServerLifecycleHooks.getCurrentServer(); if (server == null) { return; } @@ -234,10 +237,15 @@ public Set getSupportedSideEffects() { : SUPPORTED_SIDE_EFFECTS_NO_MIXIN; } + @Override + public long getTickCount() { + return ServerLifecycleHooks.getCurrentServer().getTickCount(); + } + @Override public Collection getConnectedUsers() { List users = new ArrayList<>(); - PlayerList scm = server.getPlayerList(); + PlayerList scm = ServerLifecycleHooks.getCurrentServer().getPlayerList(); for (ServerPlayer entity : scm.getPlayers()) { if (entity != null) { users.add(new ForgePlayer(entity)); diff --git a/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/ForgeWorldEdit.java b/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/ForgeWorldEdit.java index a23605f90..f05dba0cb 100644 --- a/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/ForgeWorldEdit.java +++ b/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/ForgeWorldEdit.java @@ -31,10 +31,9 @@ import com.sk89q.worldedit.extension.platform.Capability; import com.sk89q.worldedit.extension.platform.Platform; import com.sk89q.worldedit.extension.platform.PlatformManager; -import com.sk89q.worldedit.forge.net.handler.InternalPacketHandler; import com.sk89q.worldedit.forge.net.handler.WECUIPacketHandler; -import com.sk89q.worldedit.forge.net.packet.LeftClickAirEventMessage; import com.sk89q.worldedit.internal.anvil.ChunkDeleter; +import com.sk89q.worldedit.internal.event.InteractionDebouncer; import com.sk89q.worldedit.internal.util.LogManagerCompat; import com.sk89q.worldedit.util.Direction; import com.sk89q.worldedit.util.Location; @@ -45,6 +44,7 @@ import com.sk89q.worldedit.world.item.ItemCategory; import com.sk89q.worldedit.world.item.ItemType; import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.BlockPos; import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; @@ -52,12 +52,12 @@ import net.minecraft.server.level.ServerPlayer; import net.minecraft.tags.TagKey; import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.player.Player; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.CommandEvent; import net.minecraftforge.event.RegisterCommandsEvent; import net.minecraftforge.event.entity.player.PlayerEvent; import net.minecraftforge.event.entity.player.PlayerInteractEvent; -import net.minecraftforge.event.entity.player.PlayerInteractEvent.LeftClickEmpty; import net.minecraftforge.event.server.ServerAboutToStartEvent; import net.minecraftforge.event.server.ServerStartedEvent; import net.minecraftforge.event.server.ServerStoppingEvent; @@ -83,6 +83,7 @@ import java.nio.file.Path; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.function.Supplier; @@ -105,6 +106,7 @@ public class ForgeWorldEdit { public static ForgeWorldEdit inst; + private InteractionDebouncer debouncer; private ForgePlatform platform; private ForgeConfiguration config; private Path workingDir; @@ -153,13 +155,13 @@ private void init(FMLCommonSetupEvent event) { setupPlatform(); WECUIPacketHandler.init(); - InternalPacketHandler.init(); LOGGER.info("WorldEdit for Forge (version " + getInternalVersion() + ") is loaded"); } private void setupPlatform() { this.platform = new ForgePlatform(this); + debouncer = new InteractionDebouncer(platform); WorldEdit.getInstance().getPlatformManager().register(platform); @@ -260,61 +262,100 @@ public void serverStarted(ServerStartedEvent event) { WorldEdit.getInstance().getEventBus().post(new PlatformReadyEvent(platform)); } + private boolean skipEvents() { + return platform == null || !platform.isHookingEvents(); + } + + private boolean skipInteractionEvent(Player player, InteractionHand hand) { + return skipEvents() || hand != InteractionHand.MAIN_HAND || player.level().isClientSide || !(player instanceof ServerPlayer); + } + @SubscribeEvent - public void onPlayerInteract(PlayerInteractEvent event) { - if (platform == null) { + public void onLeftClickBlock(PlayerInteractEvent.LeftClickBlock event) { + if (skipInteractionEvent(event.getEntity(), event.getHand()) || event.getUseItem() == Event.Result.DENY) { return; } - if (!platform.isHookingEvents()) { - return; // We have to be told to catch these events - } + ServerPlayer playerEntity = (ServerPlayer) event.getEntity(); + WorldEdit we = WorldEdit.getInstance(); + ForgePlayer player = adaptPlayer(playerEntity); + ForgeWorld world = getWorld((ServerLevel) playerEntity.level()); + Direction direction = ForgeAdapter.adaptEnumFacing(event.getFace()); - if (event.getLevel().isClientSide && event instanceof LeftClickEmpty) { - // catch LCE, pass it to server - InternalPacketHandler.getHandler().sendToServer(LeftClickAirEventMessage.INSTANCE); + BlockPos blockPos = event.getPos(); + Location pos = new Location(world, blockPos.getX(), blockPos.getY(), blockPos.getZ()); + + boolean result = we.handleBlockLeftClick(player, pos, direction) || we.handleArmSwing(player); + debouncer.setLastInteraction(player, result); + + if (result) { + event.setCanceled(true); + } + } + + @SubscribeEvent + public void onRightClickBlock(PlayerInteractEvent.RightClickBlock event) { + if (skipInteractionEvent(event.getEntity(), event.getHand()) || event.getUseItem() == Event.Result.DENY) { return; } - boolean isLeftDeny = event instanceof PlayerInteractEvent.LeftClickBlock lcb - && lcb.getUseItem() == Event.Result.DENY; - boolean isRightDeny = event instanceof PlayerInteractEvent.RightClickBlock rcb - && rcb.getUseItem() == Event.Result.DENY; - if (isLeftDeny || isRightDeny || event.getEntity().level().isClientSide || event.getHand() == InteractionHand.OFF_HAND) { + ServerPlayer playerEntity = (ServerPlayer) event.getEntity(); + WorldEdit we = WorldEdit.getInstance(); + ForgePlayer player = adaptPlayer(playerEntity); + ForgeWorld world = getWorld((ServerLevel) playerEntity.level()); + Direction direction = ForgeAdapter.adaptEnumFacing(event.getFace()); + + BlockPos blockPos = event.getPos(); + Location pos = new Location(world, blockPos.getX(), blockPos.getY(), blockPos.getZ()); + + boolean result = we.handleBlockRightClick(player, pos, direction) || we.handleRightClick(player); + debouncer.setLastInteraction(player, result); + + if (result) { + event.setCanceled(true); + } + } + + public void onLeftClickAir(ServerPlayer playerEntity, InteractionHand hand) { + if (skipInteractionEvent(playerEntity, hand)) { return; } WorldEdit we = WorldEdit.getInstance(); - ForgePlayer player = adaptPlayer((ServerPlayer) event.getEntity()); - ForgeWorld world = getWorld((ServerLevel) event.getEntity().level()); - Direction direction = ForgeAdapter.adaptEnumFacing(event.getFace()); + ForgePlayer player = adaptPlayer(playerEntity); - if (event instanceof PlayerInteractEvent.LeftClickEmpty) { - we.handleArmSwing(player); // this event cannot be canceled - } else if (event instanceof PlayerInteractEvent.LeftClickBlock) { - Location pos = new Location(world, event.getPos().getX(), event.getPos().getY(), event.getPos().getZ()); + Optional previousResult = debouncer.getDuplicateInteractionResult(player); + if (previousResult.isPresent()) { + return; + } - if (we.handleBlockLeftClick(player, pos, direction)) { + boolean result = we.handleArmSwing(player); + debouncer.setLastInteraction(player, result); + } + + @SubscribeEvent + public void onRightClickItem(PlayerInteractEvent.RightClickItem event) { + if (skipInteractionEvent(event.getEntity(), event.getHand())) { + return; + } + + ServerPlayer playerEntity = (ServerPlayer) event.getEntity(); + WorldEdit we = WorldEdit.getInstance(); + ForgePlayer player = adaptPlayer(playerEntity); + + Optional previousResult = debouncer.getDuplicateInteractionResult(player); + if (previousResult.isPresent()) { + if (previousResult.get()) { event.setCanceled(true); } + return; + } - if (we.handleArmSwing(player)) { - event.setCanceled(true); - } - } else if (event instanceof PlayerInteractEvent.RightClickBlock) { - Location pos = new Location(world, event.getPos().getX(), event.getPos().getY(), event.getPos().getZ()); + boolean result = we.handleRightClick(player); + debouncer.setLastInteraction(player, result); - if (we.handleBlockRightClick(player, pos, direction)) { - event.setCanceled(true); - } - - if (we.handleRightClick(player)) { - event.setCanceled(true); - } - } else if (event instanceof PlayerInteractEvent.RightClickItem) { - if (we.handleRightClick(player)) { - event.setCanceled(true); - } + if (result) { + event.setCanceled(true); } } @@ -337,6 +378,8 @@ public void onCommandEvent(CommandEvent event) throws CommandSyntaxException { @SubscribeEvent public void onPlayerLogOut(PlayerEvent.PlayerLoggedOutEvent event) { if (event.getEntity() instanceof ServerPlayer player) { + debouncer.clearInteraction(adaptPlayer(player)); + WorldEdit.getInstance().getEventBus() .post(new SessionIdleEvent(new ForgePlayer.SessionKeyImpl(player))); } diff --git a/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/mixin/AccessorServerPlayerGameMode.java b/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/mixin/AccessorServerPlayerGameMode.java new file mode 100644 index 000000000..5bb01abe2 --- /dev/null +++ b/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/mixin/AccessorServerPlayerGameMode.java @@ -0,0 +1,31 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit 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 . + */ + +package com.sk89q.worldedit.forge.mixin; + +import net.minecraft.server.level.ServerPlayerGameMode; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(ServerPlayerGameMode.class) +public interface AccessorServerPlayerGameMode { + + @Accessor("isDestroyingBlock") + boolean isDestroyingBlock(); +} diff --git a/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/mixin/MixinServerGamePacketListenerImpl.java b/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/mixin/MixinServerGamePacketListenerImpl.java new file mode 100644 index 000000000..de559a823 --- /dev/null +++ b/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/mixin/MixinServerGamePacketListenerImpl.java @@ -0,0 +1,59 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit 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 . + */ + +package com.sk89q.worldedit.forge.mixin; + +import com.sk89q.worldedit.forge.ForgeWorldEdit; +import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket; +import net.minecraft.network.protocol.game.ServerboundSwingPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ServerGamePacketListenerImpl.class) +public class MixinServerGamePacketListenerImpl { + @Shadow + public ServerPlayer player; + + private int ignoreSwingPackets; + + @Inject(method = "handleAnimate", at = @At("HEAD")) + private void onAnimate(ServerboundSwingPacket packet, CallbackInfo ci) { + if (!((AccessorServerPlayerGameMode) this.player.gameMode).isDestroyingBlock()) { + if (this.ignoreSwingPackets > 0) { + this.ignoreSwingPackets--; + } else if (ForgeWorldEdit.inst != null) { + ForgeWorldEdit.inst.onLeftClickAir(this.player, packet.getHand()); + } + } + } + + @Inject(method = "handlePlayerAction", at = @At("HEAD")) + private void onAction(ServerboundPlayerActionPacket packet, CallbackInfo ci) { + switch (packet.getAction()) { + case DROP_ITEM, DROP_ALL_ITEMS, START_DESTROY_BLOCK -> this.ignoreSwingPackets++; + default -> { + } + } + } +} diff --git a/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/net/handler/InternalPacketHandler.java b/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/net/handler/InternalPacketHandler.java deleted file mode 100644 index 9264f1978..000000000 --- a/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/net/handler/InternalPacketHandler.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit 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 . - */ - -package com.sk89q.worldedit.forge.net.handler; - -import com.sk89q.worldedit.forge.net.packet.LeftClickAirEventMessage; -import com.sk89q.worldedit.forge.net.packet.LeftClickAirEventMessage.Handler; -import net.minecraftforge.network.simple.SimpleChannel; - -public final class InternalPacketHandler { - private static final int PROTOCOL_VERSION = 1; - private static final SimpleChannel HANDLER = PacketHandlerUtil - .buildLenientHandler("internal", PROTOCOL_VERSION) - .simpleChannel(); - - private InternalPacketHandler() { - } - - public static void init() { - HANDLER.registerMessage(0, LeftClickAirEventMessage.class, - LeftClickAirEventMessage::encode, LeftClickAirEventMessage::decode, Handler::handle); - } - - public static SimpleChannel getHandler() { - return HANDLER; - } -} diff --git a/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/net/packet/LeftClickAirEventMessage.java b/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/net/packet/LeftClickAirEventMessage.java deleted file mode 100644 index 31ab4340a..000000000 --- a/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/net/packet/LeftClickAirEventMessage.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit 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 . - */ - -package com.sk89q.worldedit.forge.net.packet; - -import com.sk89q.worldedit.forge.ForgeWorldEdit; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraftforge.event.entity.player.PlayerInteractEvent.LeftClickEmpty; -import net.minecraftforge.network.NetworkEvent; - -import java.util.Objects; -import java.util.function.Supplier; - -public class LeftClickAirEventMessage { - - public static final LeftClickAirEventMessage INSTANCE = new LeftClickAirEventMessage(); - - public static final class Handler { - public static void handle(final LeftClickAirEventMessage message, Supplier ctx) { - NetworkEvent.Context context = ctx.get(); - context.enqueueWork(() -> ForgeWorldEdit.inst.onPlayerInteract(new LeftClickEmpty(Objects.requireNonNull(context.getSender())))); - } - } - - public static LeftClickAirEventMessage decode(FriendlyByteBuf buf) { - return INSTANCE; - } - - public static void encode(LeftClickAirEventMessage msg, FriendlyByteBuf buf) { - } - - private LeftClickAirEventMessage() { - } - -} diff --git a/worldedit-forge/src/main/resources/worldedit-forge.mixins.json b/worldedit-forge/src/main/resources/worldedit-forge.mixins.json index 930b41b6d..d7de4e1d3 100644 --- a/worldedit-forge/src/main/resources/worldedit-forge.mixins.json +++ b/worldedit-forge/src/main/resources/worldedit-forge.mixins.json @@ -3,7 +3,9 @@ "package": "com.sk89q.worldedit.forge.mixin", "compatibilityLevel": "JAVA_17", "mixins": [ - "MixinLevelChunkSetBlockHook" + "AccessorServerPlayerGameMode", + "MixinLevelChunkSetBlockHook", + "MixinServerGamePacketListenerImpl" ], "server": [ ], diff --git a/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/SpongePlatform.java b/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/SpongePlatform.java index f3cc74add..41296a032 100644 --- a/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/SpongePlatform.java +++ b/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/SpongePlatform.java @@ -183,6 +183,11 @@ public Set getSupportedSideEffects() { ); } + @Override + public long getTickCount() { + return Sponge.server().runningTimeTicks().ticks(); + } + @Override public Collection getConnectedUsers() { return Sponge.server().onlinePlayers().stream().map(SpongePlayer::new).collect(toList()); diff --git a/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/SpongeWorldEdit.java b/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/SpongeWorldEdit.java index 2666bd795..2654d16da 100644 --- a/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/SpongeWorldEdit.java +++ b/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/SpongeWorldEdit.java @@ -35,6 +35,7 @@ import com.sk89q.worldedit.extension.platform.PlatformManager; import com.sk89q.worldedit.internal.anvil.ChunkDeleter; import com.sk89q.worldedit.internal.command.CommandUtil; +import com.sk89q.worldedit.internal.event.InteractionDebouncer; import com.sk89q.worldedit.sponge.config.SpongeConfiguration; import com.sk89q.worldedit.world.biome.BiomeType; import com.sk89q.worldedit.world.block.BlockCategory; @@ -47,7 +48,6 @@ import org.spongepowered.api.Sponge; import org.spongepowered.api.block.BlockSnapshot; import org.spongepowered.api.block.BlockType; -import org.spongepowered.api.block.BlockTypes; import org.spongepowered.api.block.entity.BlockEntity; import org.spongepowered.api.block.entity.CommandBlock; import org.spongepowered.api.command.Command; @@ -56,8 +56,11 @@ import org.spongepowered.api.command.CommandResult; import org.spongepowered.api.command.parameter.ArgumentReader; import org.spongepowered.api.config.ConfigDir; +import org.spongepowered.api.data.type.HandTypes; import org.spongepowered.api.entity.living.player.server.ServerPlayer; +import org.spongepowered.api.event.EventContextKeys; import org.spongepowered.api.event.Listener; +import org.spongepowered.api.event.action.InteractEvent; import org.spongepowered.api.event.block.InteractBlockEvent; import org.spongepowered.api.event.filter.cause.Root; import org.spongepowered.api.event.item.inventory.InteractItemEvent; @@ -107,6 +110,7 @@ public static SpongeWorldEdit inst() { private final SpongeConfiguration config; private final Path workingDir; + private InteractionDebouncer debouncer; private SpongePermissionsProvider provider; private SpongePlatform platform; @@ -128,6 +132,8 @@ public SpongeWorldEdit(Logger logger, @Listener public void onPluginConstruction(ConstructPluginEvent event) { this.platform = new SpongePlatform(this); + debouncer = new InteractionDebouncer(platform); + WorldEdit.getInstance().getPlatformManager().register(platform); this.provider = new SpongePermissionsProvider(); @@ -279,84 +285,117 @@ public List complete(CommandCause cause, ArgumentReader.Mutab ); } - private boolean isHookingEvents() { - return platform != null && platform.isHookingEvents(); + private boolean skipEvents() { + return platform == null || !platform.isHookingEvents(); + } + + private boolean skipInteractionEvent(InteractEvent event) { + return skipEvents() || event.context().get(EventContextKeys.USED_HAND).orElse(null) != HandTypes.MAIN_HAND.get(); } @Listener - public void onPlayerItemInteract(InteractItemEvent.Secondary event, @Root ServerPlayer spongePlayer) { - if (!isHookingEvents()) { + public void onPlayerInteractItemPrimary(InteractItemEvent.Primary event, @Root ServerPlayer spongePlayer) { + if (skipInteractionEvent(event)) { return; } WorldEdit we = WorldEdit.getInstance(); - SpongePlayer player = SpongeAdapter.adapt(spongePlayer); - if (we.handleRightClick(player)) { + + Optional previousResult = debouncer.getDuplicateInteractionResult(player); + if (previousResult.isPresent()) { + return; + } + + boolean result = we.handleArmSwing(player); + debouncer.setLastInteraction(player, result); + } + + @Listener + public void onPlayerInteractItemSecondary(InteractItemEvent.Secondary event, @Root ServerPlayer spongePlayer) { + if (skipInteractionEvent(event)) { + return; + } + + WorldEdit we = WorldEdit.getInstance(); + SpongePlayer player = SpongeAdapter.adapt(spongePlayer); + + Optional previousResult = debouncer.getDuplicateInteractionResult(player); + if (previousResult.isPresent()) { + if (previousResult.get()) { + event.setCancelled(true); + } + return; + } + + boolean result = we.handleRightClick(player); + debouncer.setLastInteraction(player, result); + + if (result) { event.setCancelled(true); } } @Listener - public void onPlayerInteract(InteractBlockEvent event, @Root ServerPlayer spongePlayer) { - if (platform == null) { + public void onPlayerInteractBlockPrimary(InteractBlockEvent.Primary.Start event, @Root ServerPlayer spongePlayer) { + if (skipInteractionEvent(event)) { return; } - if (!platform.isHookingEvents()) { - return; // We have to be told to catch these events - } - WorldEdit we = WorldEdit.getInstance(); - SpongePlayer player = SpongeAdapter.adapt(spongePlayer); BlockSnapshot targetBlock = event.block(); Optional optLoc = targetBlock.location(); - BlockType interactedType = targetBlock.state().type(); - if (event instanceof InteractBlockEvent.Primary.Start) { - InteractBlockEvent.Primary.Start eventCast = ((InteractBlockEvent.Primary.Start) event); - if (interactedType != BlockTypes.AIR.get()) { - if (!optLoc.isPresent()) { - return; - } - - ServerLocation loc = optLoc.get(); - com.sk89q.worldedit.util.Location pos = SpongeAdapter.adapt( - loc, Vector3d.ZERO - ); - - if (we.handleBlockLeftClick(player, pos, SpongeAdapter.adapt(eventCast.targetSide()))) { - eventCast.setCancelled(true); - } - } - if (we.handleArmSwing(player)) { - eventCast.setCancelled(true); - } - } else if (event instanceof InteractBlockEvent.Secondary) { - if (!optLoc.isPresent()) { - return; - } - InteractBlockEvent.Secondary eventCast = ((InteractBlockEvent.Secondary) event); - + boolean result = false; + if (optLoc.isPresent()) { ServerLocation loc = optLoc.get(); - com.sk89q.worldedit.util.Location pos = SpongeAdapter.adapt( - loc, Vector3d.ZERO - ); + com.sk89q.worldedit.util.Location pos = SpongeAdapter.adapt(loc, Vector3d.ZERO); - if (we.handleBlockRightClick(player, pos, SpongeAdapter.adapt(eventCast.targetSide()))) { - eventCast.setCancelled(true); - } + result = we.handleBlockLeftClick(player, pos, SpongeAdapter.adapt(event.targetSide())); + } - if (we.handleRightClick(player)) { - eventCast.setCancelled(true); - } + result = we.handleArmSwing(player) || result; + debouncer.setLastInteraction(player, result); + + if (result) { + event.setCancelled(true); + } + } + + @Listener + public void onPlayerInteractBlockSecondary(InteractBlockEvent.Secondary event, @Root ServerPlayer spongePlayer) { + if (skipInteractionEvent(event)) { + return; + } + + WorldEdit we = WorldEdit.getInstance(); + SpongePlayer player = SpongeAdapter.adapt(spongePlayer); + + BlockSnapshot targetBlock = event.block(); + Optional optLoc = targetBlock.location(); + + boolean result = false; + if (optLoc.isPresent()) { + ServerLocation loc = optLoc.get(); + com.sk89q.worldedit.util.Location pos = SpongeAdapter.adapt(loc, Vector3d.ZERO); + + result = we.handleBlockRightClick(player, pos, SpongeAdapter.adapt(event.targetSide())); + } + + result = we.handleRightClick(player) || result; + debouncer.setLastInteraction(player, result); + + if (result) { + event.setCancelled(true); } } @Listener public void onPlayerQuit(ServerSideConnectionEvent.Disconnect event) { + debouncer.clearInteraction(SpongeAdapter.adapt(event.player())); + WorldEdit.getInstance().getEventBus() .post(new SessionIdleEvent(new SpongePlayer.SessionKeyImpl(event.player()))); }