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
This commit is contained in:
Yeregorix 2023-09-14 11:09:27 +02:00 committed by GitHub
parent 18cc29a26f
commit 5b4322e53a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 557 additions and 262 deletions

View File

@ -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<SideEffect> getSupportedSideEffects() {
return SUPPORTED_SIDE_EFFECTS;
}
@Override
public long getTickCount() {
if (PaperLib.isPaper()) {
return Bukkit.getCurrentTick();
}
return super.getTickCount();
}
public void unregisterCommands() {
dynamicCommands.unregisterCommands();
}

View File

@ -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<Boolean> 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())));
}
}

View File

@ -84,4 +84,8 @@ public void reload() {
getTranslationManager().reload();
}
@Override
public long getTickCount() {
return System.nanoTime() / 50_000_000;
}
}

View File

@ -207,4 +207,12 @@ default void registerGameHooks() {
* @return A set of supported side effects
*/
Set<SideEffect> 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();
}

View File

@ -0,0 +1,69 @@
/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* 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 <https://www.gnu.org/licenses/>.
*/
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<UUID, Interaction> 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<Boolean> 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;
}
}
}

View File

@ -213,6 +213,11 @@ public Set<SideEffect> getSupportedSideEffects() {
: SUPPORTED_SIDE_EFFECTS_NO_MIXIN;
}
@Override
public long getTickCount() {
return FabricWorldEdit.LIFECYCLED_SERVER.valueOrThrow().getTickCount();
}
@Override
public Collection<Actor> getConnectedUsers() {
List<Actor> users = new ArrayList<>();

View File

@ -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 <T> Registry<T> getRegistry(ResourceKey<Registry<T>> 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<Boolean> previousResult = debouncer.getDuplicateInteractionResult(player);
if (previousResult.isPresent()) {
return;
}
return InteractionResult.PASS;
boolean result = we.handleArmSwing(player);
debouncer.setLastInteraction(player, result);
}
private InteractionResultHolder<ItemStack> 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<Boolean> 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)));
}

View File

@ -0,0 +1,31 @@
/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* 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 <https://www.gnu.org/licenses/>.
*/
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();
}

View File

@ -0,0 +1,59 @@
/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* 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 <https://www.gnu.org/licenses/>.
*/
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 -> {
}
}
}
}

View File

@ -7,8 +7,10 @@
"AccessorPrimaryLevelData",
"AccessorDerivedLevelData",
"AccessorServerChunkCache",
"AccessorServerPlayerGameMode",
"MixinLevelChunkSetBlockHook",
"MixinMinecraftServer",
"MixinServerGamePacketListenerImpl",
"MixinServerPlayer"
],
"plugin": "com.sk89q.worldedit.fabric.internal.MixinConfigPlugin",

View File

@ -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<? extends World> getWorlds() {
Iterable<ServerLevel> worlds = server.getAllLevels();
Iterable<ServerLevel> worlds = ServerLifecycleHooks.getCurrentServer().getAllLevels();
List<World> 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<SideEffect> getSupportedSideEffects() {
: SUPPORTED_SIDE_EFFECTS_NO_MIXIN;
}
@Override
public long getTickCount() {
return ServerLifecycleHooks.getCurrentServer().getTickCount();
}
@Override
public Collection<Actor> getConnectedUsers() {
List<Actor> 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));

View File

@ -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<Boolean> 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<Boolean> 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)));
}

View File

@ -0,0 +1,31 @@
/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* 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 <https://www.gnu.org/licenses/>.
*/
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();
}

View File

@ -0,0 +1,59 @@
/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* 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 <https://www.gnu.org/licenses/>.
*/
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 -> {
}
}
}
}

View File

@ -1,43 +0,0 @@
/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* 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 <https://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -1,51 +0,0 @@
/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* 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 <https://www.gnu.org/licenses/>.
*/
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<NetworkEvent.Context> 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() {
}
}

View File

@ -3,7 +3,9 @@
"package": "com.sk89q.worldedit.forge.mixin",
"compatibilityLevel": "JAVA_17",
"mixins": [
"MixinLevelChunkSetBlockHook"
"AccessorServerPlayerGameMode",
"MixinLevelChunkSetBlockHook",
"MixinServerGamePacketListenerImpl"
],
"server": [
],

View File

@ -183,6 +183,11 @@ public Set<SideEffect> getSupportedSideEffects() {
);
}
@Override
public long getTickCount() {
return Sponge.server().runningTimeTicks().ticks();
}
@Override
public Collection<Actor> getConnectedUsers() {
return Sponge.server().onlinePlayers().stream().map(SpongePlayer::new).collect(toList());

View File

@ -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<CommandCompletion> 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<Boolean> 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<Boolean> 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<ServerLocation> 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<ServerLocation> 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())));
}