From 1ed012331bb4a8c215148f1991cb5cd879eea216 Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 15 Feb 2026 23:15:50 -0500 Subject: [PATCH] Enhance ability input handling: block movement during ability locks, ensure proper input suppression, and cancel abilities on logout, dimension change, death, and respawn --- .../noppes/npcs/ScriptPlayerEventHandler.java | 22 +++++ .../npcs/client/ClientEventHandler.java | 13 ++- .../noppes/npcs/client/ClientTickHandler.java | 85 +++++++++++++++++-- .../controllers/data/PlayerAbilityData.java | 1 + .../npcs/controllers/data/PlayerData.java | 2 + 5 files changed, 116 insertions(+), 7 deletions(-) diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index 909634a09..8c9bde6ac 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -106,6 +106,7 @@ public class ScriptPlayerEventHandler { Ability current = playerData.abilityData.getCurrentAbility(); if (current == null || !current.hasAbilityMovement()) { player.motionX = 0; + player.motionY = Math.min(player.motionY, 0); // Prevent jumping, allow gravity player.motionZ = 0; player.velocityChanged = true; } @@ -328,6 +329,12 @@ public class ScriptPlayerEventHandler { return; if (event.player.worldObj instanceof WorldServer && event.player instanceof EntityPlayerMP) { + // Cancel any executing ability on dimension change + PlayerData playerData = PlayerData.get(event.player); + if (playerData != null) { + playerData.abilityData.interruptCurrentAbility(); + } + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(event.player); IPlayer scriptPlayer = (IPlayer) NpcAPI.Instance().getIEntity(event.player); EventHooks.onPlayerChangeDim(handler, scriptPlayer, event.fromDim, event.toDim); @@ -478,6 +485,13 @@ public class ScriptPlayerEventHandler { if (event.entityLiving.worldObj instanceof WorldServer) { if (event.entityLiving instanceof EntityPlayerMP) { + // Cancel jump if movement is locked by an ability + PlayerData playerData = PlayerData.get((EntityPlayer) event.entityLiving); + if (playerData != null && playerData.abilityData.isMovementLocked()) { + event.entityLiving.motionY = 0; + event.entityLiving.velocityChanged = true; + } + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts((EntityPlayer) event.entityLiving); IPlayer scriptPlayer = (IPlayer) NpcAPI.Instance().getIEntity(event.entityLiving); EventHooks.onPlayerJump(handler, scriptPlayer); @@ -532,6 +546,8 @@ public class ScriptPlayerEventHandler { EntityPlayer player = (EntityPlayer) event.entityLiving; PlayerData playerData = PlayerData.get(player); + // Cancel any executing ability on death + playerData.abilityData.interruptCurrentAbility(); if (ConfigScript.ClearActionsOnDeath) playerData.actionManager.clear(); } @@ -631,6 +647,12 @@ public class ScriptPlayerEventHandler { return; if (event.player.worldObj instanceof WorldServer && event.player instanceof EntityPlayerMP) { + // Cancel any executing ability on respawn + PlayerData playerData = PlayerData.get(event.player); + if (playerData != null) { + playerData.abilityData.interruptCurrentAbility(); + } + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(event.player); IPlayer scriptPlayer = (IPlayer) NpcAPI.Instance().getIEntity(event.player); EventHooks.onPlayerRespawn(handler, scriptPlayer); diff --git a/src/main/java/noppes/npcs/client/ClientEventHandler.java b/src/main/java/noppes/npcs/client/ClientEventHandler.java index a7a0f2b39..6fc690b5a 100644 --- a/src/main/java/noppes/npcs/client/ClientEventHandler.java +++ b/src/main/java/noppes/npcs/client/ClientEventHandler.java @@ -84,9 +84,20 @@ public class ClientEventHandler { @SubscribeEvent public void onMouse(MouseEvent event) { - if (Minecraft.getMinecraft().currentScreen != null) + Minecraft mc = Minecraft.getMinecraft(); + if (mc.currentScreen != null) return; + // Block mouse movement during ability rotation lock. + // Allow clicks (button >= 0) and scroll (dwheel != 0) to pass through. + if (mc.theWorld != null && mc.thePlayer != null + && ClientAbilityState.shouldLockRotation() + && (event.dx != 0 || event.dy != 0) + && event.button == -1 && event.dwheel == 0) { + event.setCanceled(true); + return; + } + ArrayList removeList = new ArrayList<>(); for (Map.Entry entry : disabledButtonTimes.entrySet()) { if (entry.getValue() > 0) { diff --git a/src/main/java/noppes/npcs/client/ClientTickHandler.java b/src/main/java/noppes/npcs/client/ClientTickHandler.java index 7d82ca0f1..d12921756 100644 --- a/src/main/java/noppes/npcs/client/ClientTickHandler.java +++ b/src/main/java/noppes/npcs/client/ClientTickHandler.java @@ -13,6 +13,7 @@ import kamkeel.npcs.network.packets.player.InputDevicePacket; import kamkeel.npcs.network.packets.player.ScreenSizePacket; import kamkeel.npcs.network.packets.player.SpecialKeyStatePacket; import net.minecraft.client.Minecraft; +import net.minecraft.client.settings.KeyBinding; import net.minecraft.entity.Entity; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.inventory.ContainerPlayer; @@ -75,13 +76,27 @@ public class ClientTickHandler { lastSpecialKeyDown = specialKeyDown; } - // Suppress player input during ability-controlled phases - if (ClientAbilityState.shouldSuppressMovementInput()) { + // Suppress player input during ability-controlled phases. + // Only suppress when no GUI screen is open (screens already capture input). + if (mc.currentScreen == null && ClientAbilityState.shouldSuppressMovementInput()) { + // Unpress movement keybinds at the source BEFORE updatePlayerMoveState() reads them. + // This is more robust than just zeroing movementInput fields, because + // updatePlayerMoveState() reads directly from keybind pressed state. + KeyBinding.setKeyBindState(mc.gameSettings.keyBindForward.getKeyCode(), false); + KeyBinding.setKeyBindState(mc.gameSettings.keyBindBack.getKeyCode(), false); + KeyBinding.setKeyBindState(mc.gameSettings.keyBindLeft.getKeyCode(), false); + KeyBinding.setKeyBindState(mc.gameSettings.keyBindRight.getKeyCode(), false); + KeyBinding.setKeyBindState(mc.gameSettings.keyBindJump.getKeyCode(), false); + KeyBinding.setKeyBindState(mc.gameSettings.keyBindSneak.getKeyCode(), false); + KeyBinding.setKeyBindState(mc.gameSettings.keyBindSprint.getKeyCode(), false); + + // Also zero the movementInput values directly as a safety net mc.thePlayer.movementInput.moveForward = 0; mc.thePlayer.movementInput.moveStrafe = 0; mc.thePlayer.movementInput.jump = false; + mc.thePlayer.movementInput.sneak = false; } - if (ClientAbilityState.shouldLockRotation()) { + if (mc.currentScreen == null && ClientAbilityState.shouldLockRotation()) { mc.thePlayer.rotationYaw = ClientAbilityState.lockedYaw; mc.thePlayer.rotationPitch = ClientAbilityState.lockedPitch; mc.thePlayer.prevRotationYaw = ClientAbilityState.lockedYaw; @@ -100,8 +115,42 @@ public class ClientTickHandler { } } if (event.phase == Phase.END) { - if (mc.thePlayer != null && mc.theWorld != null && !mc.isGamePaused() && ClientEventHandler.hasOverlays(mc.thePlayer)) { - renderCNPCPlayer.itemRenderer.updateEquippedItem(); + if (mc.thePlayer != null) { + // Re-enforce ability locks AFTER entity update. + // EntityPlayerSP.onLivingUpdate() calls updatePlayerMoveState() which + // overwrites Phase.START suppression with actual keyboard state, causing + // client-predicted movement that fights the server lock. Undo it here. + // Skip when a GUI screen is open (screens already capture all input). + if (mc.currentScreen == null && ClientAbilityState.shouldSuppressMovementInput()) { + mc.thePlayer.movementInput.moveForward = 0; + mc.thePlayer.movementInput.moveStrafe = 0; + mc.thePlayer.movementInput.jump = false; + mc.thePlayer.movementInput.sneak = false; + + // Zero horizontal motion (unless ability provides its own movement) + if (!ClientAbilityState.hasAbilityMovement) { + mc.thePlayer.motionX = 0; + mc.thePlayer.motionZ = 0; + } + + // Prevent jumping: clamp upward motion when locked (allow gravity) + if ((ClientAbilityState.movementLocked || ClientAbilityState.positionLocked) + && !ClientAbilityState.hasAbilityMovement) { + mc.thePlayer.motionY = Math.min(mc.thePlayer.motionY, 0); + } + } + + if (mc.currentScreen == null && ClientAbilityState.shouldLockRotation()) { + mc.thePlayer.rotationYaw = ClientAbilityState.lockedYaw; + mc.thePlayer.rotationPitch = ClientAbilityState.lockedPitch; + mc.thePlayer.prevRotationYaw = ClientAbilityState.lockedYaw; + mc.thePlayer.prevRotationPitch = ClientAbilityState.lockedPitch; + mc.thePlayer.rotationYawHead = ClientAbilityState.lockedYaw; + } + + if (mc.theWorld != null && !mc.isGamePaused() && ClientEventHandler.hasOverlays(mc.thePlayer)) { + renderCNPCPlayer.itemRenderer.updateEquippedItem(); + } } return; } @@ -159,8 +208,9 @@ public class ClientTickHandler { @SubscribeEvent public void onKey(InputEvent.KeyInputEvent event) { + Minecraft mc = Minecraft.getMinecraft(); + if (ClientProxy.NPCButton.isPressed()) { - Minecraft mc = Minecraft.getMinecraft(); if (mc.currentScreen == null) { InventoryTabCustomNpc.tabHelper(); } else if (mc.currentScreen instanceof GuiCNPCInventory) @@ -171,10 +221,33 @@ public class ClientTickHandler { int key = Keyboard.getEventKey(); boolean keyDown = Keyboard.isKeyDown(key); + // Block movement keys during ability lock (only when no GUI is open and world exists). + // Non-movement keys (ESC, chat, inventory, etc.) always pass through. + if (mc.theWorld != null && mc.currentScreen == null && keyDown + && ClientAbilityState.shouldSuppressMovementInput() && isMovementKey(key)) { + // Unpress the keybind so vanilla doesn't process it + KeyBinding.setKeyBindState(key, false); + return; + } + InputDevicePacket.sendKeyboard(key, keyDown); } } + /** + * Check if a key code corresponds to a movement keybind (WASD, jump, sneak, sprint). + */ + private boolean isMovementKey(int keyCode) { + Minecraft mc = Minecraft.getMinecraft(); + return keyCode == mc.gameSettings.keyBindForward.getKeyCode() + || keyCode == mc.gameSettings.keyBindBack.getKeyCode() + || keyCode == mc.gameSettings.keyBindLeft.getKeyCode() + || keyCode == mc.gameSettings.keyBindRight.getKeyCode() + || keyCode == mc.gameSettings.keyBindJump.getKeyCode() + || keyCode == mc.gameSettings.keyBindSneak.getKeyCode() + || keyCode == mc.gameSettings.keyBindSprint.getKeyCode(); + } + private boolean isIgnoredKey(int key) { int[] var2 = this.ignoreKeys; int var3 = var2.length; diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index 22ec80c4b..6ced6f68a 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -699,6 +699,7 @@ public class PlayerAbilityData implements IPlayerAbilityData { player.prevPosY = lockedPosY; player.prevPosZ = lockedPosZ; player.motionX = 0; + player.motionY = 0; // Full vertical freeze for position lock player.motionZ = 0; } diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerData.java b/src/main/java/noppes/npcs/controllers/data/PlayerData.java index 02fd67563..7f326a344 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerData.java @@ -124,6 +124,8 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { } public void onLogout() { + // Cancel any executing ability and release all locks + abilityData.interruptCurrentAbility(); this.partyInvites.clear(); this.actionManager.clear(); }