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 fd72b13e56..a4ac8f9f35 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 @@ -28,6 +28,10 @@ import com.sk89q.worldedit.util.Direction; import com.sk89q.worldedit.util.Location; import com.sk89q.worldedit.world.World; +import io.papermc.lib.PaperLib; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import org.bukkit.Bukkit; import org.bukkit.block.Block; import org.bukkit.event.Event.Result; import org.bukkit.event.EventHandler; @@ -45,12 +49,15 @@ import org.enginehub.piston.inject.MapBackedValueStore; import java.util.Optional; +import java.util.UUID; /** * Handles all events thrown in relation to a Player. */ public class WorldEditListener implements Listener { + private final Object2IntMap lastInteractionTicks = new Object2IntOpenHashMap<>(); + private final WorldEditPlugin plugin; /** @@ -86,6 +93,19 @@ public void onPlayerCommandSend(PlayerCommandSendEvent event) { ); } + private static int getCurrentTick() { + if (PaperLib.isPaper()) { + return Bukkit.getCurrentTick(); + } + return (int) (System.currentTimeMillis() / 50); + } + + private boolean isDuplicateInteraction(Player player) { + int now = getCurrentTick(); + int last = lastInteractionTicks.getInt(player.getUniqueId()); + return now - last <= 1; + } + /** * Called when a player interacts. * @@ -124,31 +144,36 @@ public void onPlayerInteract(PlayerInteractEvent event) { } } else if (action == Action.LEFT_CLICK_AIR) { - - if (we.handleArmSwing(player)) { + if (!isDuplicateInteraction(player) && 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 (!isDuplicateInteraction(player)) { + 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.handleBlockRightClick(player, pos, direction)) { + event.setCancelled(true); + } - if (we.handleRightClick(player)) { - event.setCancelled(true); + if (we.handleRightClick(player)) { + event.setCancelled(true); + } } } else if (action == Action.RIGHT_CLICK_AIR) { - if (we.handleRightClick(player)) { + if (!isDuplicateInteraction(player) && we.handleRightClick(player)) { event.setCancelled(true); } } + + lastInteractionTicks.put(player.getUniqueId(), getCurrentTick()); } @EventHandler public void onPlayerQuit(PlayerQuitEvent event) { + lastInteractionTicks.removeInt(event.getPlayer().getUniqueId()); + plugin.getWorldEdit().getEventBus().post(new SessionIdleEvent(new BukkitPlayer.SessionKeyImpl(event.getPlayer()))); } } 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 8fc531ccb8..3df786d41b 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 @@ -42,6 +42,8 @@ import com.sk89q.worldedit.world.entity.EntityType; import com.sk89q.worldedit.world.item.ItemCategory; import com.sk89q.worldedit.world.item.ItemType; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; @@ -80,6 +82,7 @@ import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.UUID; import static com.google.common.base.Preconditions.checkNotNull; import static com.sk89q.worldedit.fabric.FabricAdapter.adaptPlayer; @@ -107,6 +110,8 @@ public class FabricWorldEdit implements ModInitializer { public static FabricWorldEdit inst; + private final Object2IntMap lastInteractionTicks = new Object2IntOpenHashMap<>(); + private FabricPlatform platform; private FabricConfiguration config; private Path workingDir; @@ -244,19 +249,36 @@ 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 isDuplicateInteraction(ServerPlayer player) { + int now = player.server.getTickCount(); + int last = lastInteractionTicks.getInt(player.getUUID()); + return now - last <= 1; + } + + private void saveInteractionTick(ServerPlayer player) { + int now = player.server.getTickCount(); + lastInteractionTicks.put(player.getUUID(), now); + } + + private void removeInteractionTick(ServerPlayer player) { + lastInteractionTicks.removeInt(player.getUUID()); + } + + 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; } + saveInteractionTick((ServerPlayer) playerEntity); + WorldEdit we = WorldEdit.getInstance(); FabricPlayer player = adaptPlayer((ServerPlayer) playerEntity); FabricWorld localWorld = getWorld(world); @@ -279,10 +301,12 @@ private InteractionResult onLeftClickBlock(Player playerEntity, Level world, Int } 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; } + saveInteractionTick((ServerPlayer) playerEntity); + WorldEdit we = WorldEdit.getInstance(); FabricPlayer player = adaptPlayer((ServerPlayer) playerEntity); FabricWorld localWorld = getWorld(world); @@ -304,12 +328,27 @@ private InteractionResult onRightClickBlock(Player playerEntity, Level world, In return InteractionResult.PASS; } + public void onLeftClickAir(ServerPlayer playerEntity, InteractionHand hand) { + if (skipInteractionEvent(playerEntity, hand) || isDuplicateInteraction(playerEntity)) { + return; + } + + saveInteractionTick(playerEntity); + + WorldEdit we = WorldEdit.getInstance(); + FabricPlayer player = adaptPlayer(playerEntity); + + we.handleArmSwing(player); + } + 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) || isDuplicateInteraction((ServerPlayer) playerEntity)) { return InteractionResultHolder.pass(stackInHand); } + saveInteractionTick((ServerPlayer) playerEntity); + WorldEdit we = WorldEdit.getInstance(); FabricPlayer player = adaptPlayer((ServerPlayer) playerEntity); @@ -320,9 +359,9 @@ private InteractionResultHolder onRightClickAir(Player playerEntity, return InteractionResultHolder.pass(stackInHand); } - // TODO Pass empty left click to server - private void onPlayerDisconnect(ServerGamePacketListenerImpl handler, MinecraftServer server) { + removeInteractionTick(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 0000000000..c9aaaa62d3 --- /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 0000000000..d062fc2c84 --- /dev/null +++ b/worldedit-fabric/src/main/java/com/sk89q/worldedit/fabric/mixin/MixinServerGamePacketListenerImpl.java @@ -0,0 +1,63 @@ +/* + * 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: + case DROP_ALL_ITEMS: + case START_DESTROY_BLOCK: + this.ignoreSwingPackets++; + break; + default: + break; + } + } +} diff --git a/worldedit-fabric/src/main/resources/worldedit-fabric.mixins.json b/worldedit-fabric/src/main/resources/worldedit-fabric.mixins.json index 076670106b..357780004f 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/ForgeWorldEdit.java b/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/ForgeWorldEdit.java index a9a7eef090..8710aebb43 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,9 +31,7 @@ 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.util.LogManagerCompat; import com.sk89q.worldedit.util.Direction; @@ -44,7 +42,10 @@ import com.sk89q.worldedit.world.entity.EntityType; import com.sk89q.worldedit.world.item.ItemCategory; import com.sk89q.worldedit.world.item.ItemType; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.BlockPos; import net.minecraft.core.Registry; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; @@ -52,12 +53,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; @@ -84,6 +85,7 @@ import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.UUID; import java.util.function.Supplier; import static com.google.common.base.Preconditions.checkNotNull; @@ -104,6 +106,8 @@ public class ForgeWorldEdit { public static ForgeWorldEdit inst; + private final Object2IntMap lastInteractionTicks = new Object2IntOpenHashMap<>(); + private ForgePlatform platform; private ForgeConfiguration config; private Path workingDir; @@ -152,7 +156,6 @@ private void init(FMLCommonSetupEvent event) { setupPlatform(); WECUIPacketHandler.init(); - InternalPacketHandler.init(); LOGGER.info("WorldEdit for Forge (version " + getInternalVersion() + ") is loaded"); } @@ -259,61 +262,108 @@ public void serverStarted(ServerStartedEvent event) { WorldEdit.getInstance().getEventBus().post(new PlatformReadyEvent(platform)); } + private boolean skipEvents() { + return platform == null || !platform.isHookingEvents(); + } + + private boolean isDuplicateInteraction(ServerPlayer player) { + int now = player.server.getTickCount(); + int last = lastInteractionTicks.getInt(player.getUUID()); + return now - last <= 1; + } + + private void saveInteractionTick(ServerPlayer player) { + int now = player.server.getTickCount(); + lastInteractionTicks.put(player.getUUID(), now); + } + + private void removeInteractionTick(ServerPlayer player) { + lastInteractionTicks.removeInt(player.getUUID()); + } + + 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(); + saveInteractionTick(playerEntity); + + 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()); + + if (we.handleBlockLeftClick(player, pos, direction)) { + event.setCanceled(true); } - if (event.getLevel().isClientSide && event instanceof LeftClickEmpty) { - // catch LCE, pass it to server - InternalPacketHandler.getHandler().sendToServer(LeftClickAirEventMessage.INSTANCE); - return; + if (we.handleArmSwing(player)) { + event.setCanceled(true); } + } - 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) { + @SubscribeEvent + public void onRightClickBlock(PlayerInteractEvent.RightClickBlock event) { + if (skipInteractionEvent(event.getEntity(), event.getHand()) || event.getUseItem() == Event.Result.DENY) { return; } + ServerPlayer playerEntity = (ServerPlayer) event.getEntity(); + saveInteractionTick(playerEntity); + WorldEdit we = WorldEdit.getInstance(); - ForgePlayer player = adaptPlayer((ServerPlayer) event.getEntity()); - ForgeWorld world = getWorld((ServerLevel) event.getEntity().level); + ForgePlayer player = adaptPlayer(playerEntity); + ForgeWorld world = getWorld((ServerLevel) playerEntity.level); Direction direction = ForgeAdapter.adaptEnumFacing(event.getFace()); - 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()); + BlockPos blockPos = event.getPos(); + Location pos = new Location(world, blockPos.getX(), blockPos.getY(), blockPos.getZ()); - if (we.handleBlockLeftClick(player, pos, direction)) { - event.setCanceled(true); - } + if (we.handleBlockRightClick(player, pos, direction)) { + event.setCanceled(true); + } - 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()); + if (we.handleRightClick(player)) { + event.setCanceled(true); + } + } - if (we.handleBlockRightClick(player, pos, direction)) { - event.setCanceled(true); - } + public void onLeftClickAir(ServerPlayer playerEntity, InteractionHand hand) { + if (skipInteractionEvent(playerEntity, hand) || isDuplicateInteraction(playerEntity)) { + return; + } - if (we.handleRightClick(player)) { - event.setCanceled(true); - } - } else if (event instanceof PlayerInteractEvent.RightClickItem) { - if (we.handleRightClick(player)) { - event.setCanceled(true); - } + saveInteractionTick(playerEntity); + + WorldEdit we = WorldEdit.getInstance(); + ForgePlayer player = adaptPlayer(playerEntity); + + we.handleArmSwing(player); + } + + @SubscribeEvent + public void onRightClickItem(PlayerInteractEvent.RightClickItem event) { + if (skipInteractionEvent(event.getEntity(), event.getHand()) || isDuplicateInteraction((ServerPlayer) event.getEntity())) { + return; + } + + ServerPlayer playerEntity = (ServerPlayer) event.getEntity(); + saveInteractionTick(playerEntity); + + WorldEdit we = WorldEdit.getInstance(); + ForgePlayer player = adaptPlayer(playerEntity); + + if (we.handleRightClick(player)) { + event.setCanceled(true); } } @@ -339,6 +389,8 @@ public void onCommandEvent(CommandEvent event) throws CommandSyntaxException { @SubscribeEvent public void onPlayerLogOut(PlayerEvent.PlayerLoggedOutEvent event) { if (event.getEntity() instanceof ServerPlayer player) { + removeInteractionTick(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 0000000000..5bb01abe28 --- /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 0000000000..3d6ed980fb --- /dev/null +++ b/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/mixin/MixinServerGamePacketListenerImpl.java @@ -0,0 +1,63 @@ +/* + * 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: + case DROP_ALL_ITEMS: + case START_DESTROY_BLOCK: + this.ignoreSwingPackets++; + break; + default: + break; + } + } +} 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 9264f19781..0000000000 --- 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 31ab4340ad..0000000000 --- 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 930b41b6db..d7de4e1d3f 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/SpongeWorldEdit.java b/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/SpongeWorldEdit.java index 81157d9a94..31dcb00055 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 @@ -39,6 +39,8 @@ import com.sk89q.worldedit.world.biome.BiomeType; import com.sk89q.worldedit.world.block.BlockCategory; import com.sk89q.worldedit.world.item.ItemCategory; +import it.unimi.dsi.fastutil.objects.Object2LongMap; +import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; import net.kyori.adventure.audience.Audience; import org.apache.logging.log4j.Logger; import org.bstats.sponge.Metrics; @@ -54,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; @@ -79,6 +84,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.UUID; import java.util.concurrent.TimeUnit; import static com.sk89q.worldedit.internal.anvil.ChunkDeleter.DELCHUNKS_FILE_NAME; @@ -104,6 +110,8 @@ public static SpongeWorldEdit inst() { private final SpongeConfiguration config; private final Path workingDir; + private final Object2LongMap lastInteractionTicks = new Object2LongOpenHashMap<>(); + private SpongePermissionsProvider provider; private SpongePlatform platform; @@ -276,84 +284,125 @@ 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 isDuplicateInteraction(ServerPlayer player) { + long now = Sponge.server().runningTimeTicks().ticks(); + long last = lastInteractionTicks.getLong(player.uniqueId()); + return now - last <= 1; + } + + private void saveInteractionTick(ServerPlayer player) { + long now = Sponge.server().runningTimeTicks().ticks(); + lastInteractionTicks.put(player.uniqueId(), now); + } + + private void removeInteractionTick(ServerPlayer player) { + lastInteractionTicks.removeLong(player.uniqueId()); + } + + 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) || isDuplicateInteraction(spongePlayer)) { return; } + saveInteractionTick(spongePlayer); + WorldEdit we = WorldEdit.getInstance(); + SpongePlayer player = SpongeAdapter.adapt(spongePlayer); + + we.handleArmSwing(player); + } + + @Listener + public void onPlayerInteractItemSecondary(InteractItemEvent.Secondary event, @Root ServerPlayer spongePlayer) { + if (skipInteractionEvent(event) || isDuplicateInteraction(spongePlayer)) { + return; + } + + saveInteractionTick(spongePlayer); + WorldEdit we = WorldEdit.getInstance(); SpongePlayer player = SpongeAdapter.adapt(spongePlayer); + if (we.handleRightClick(player)) { 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 - } + saveInteractionTick(spongePlayer); 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 (interactedType != BlockTypes.AIR.get()) { + Optional optLoc = targetBlock.location(); if (!optLoc.isPresent()) { return; } - InteractBlockEvent.Secondary eventCast = ((InteractBlockEvent.Secondary) event); 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); + if (we.handleBlockLeftClick(player, pos, SpongeAdapter.adapt(event.targetSide()))) { + event.setCancelled(true); } + } - if (we.handleRightClick(player)) { - eventCast.setCancelled(true); - } + if (we.handleArmSwing(player)) { + event.setCancelled(true); + } + } + + @Listener + public void onPlayerInteractBlockSecondary(InteractBlockEvent.Secondary event, @Root ServerPlayer spongePlayer) { + if (skipInteractionEvent(event)) { + return; + } + + saveInteractionTick(spongePlayer); + + WorldEdit we = WorldEdit.getInstance(); + SpongePlayer player = SpongeAdapter.adapt(spongePlayer); + + BlockSnapshot targetBlock = event.block(); + Optional optLoc = targetBlock.location(); + if (!optLoc.isPresent()) { + return; + } + + ServerLocation loc = optLoc.get(); + com.sk89q.worldedit.util.Location pos = SpongeAdapter.adapt(loc, Vector3d.ZERO); + + if (we.handleBlockRightClick(player, pos, SpongeAdapter.adapt(event.targetSide()))) { + event.setCancelled(true); + } + + if (we.handleRightClick(player)) { + event.setCancelled(true); } } @Listener public void onPlayerQuit(ServerSideConnectionEvent.Disconnect event) { + removeInteractionTick(event.player()); + WorldEdit.getInstance().getEventBus() .post(new SessionIdleEvent(new SpongePlayer.SessionKeyImpl(event.player()))); }