Search code examples
javahashmapminecraftbukkit

Minecraft (Bukkit) : Issue with falldamage after "Doublejump"


I've recently written a "Double Jump" code for my plugin, which also cancels the falldamage after a successful jump.

The current problem is, that falldamage is removed completely, and I couldn't find out why. The issue seems to be the onFall event, and the .setAllowFlight(true); from the onMove.

package at.skyblock.events;

import java.util.ArrayList;
import java.util.HashMap;

import org.bukkit.ChatColor;
import org.bukkit.GameMode;
import org.bukkit.Material;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityDamageEvent.DamageCause;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerToggleFlightEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.util.Vector;

import at.skyblok.SnowflakeUtil;

public class MovementHandler implements Listener {

    private SnowflakeUtil pl;
    private ArrayList<Player> jumpers = new ArrayList<Player>();
    private HashMap<Player, Integer> cooldownTime = new HashMap<Player, Integer>();
    private HashMap<Player, BukkitRunnable> cooldownTask = new HashMap<Player, BukkitRunnable>();

    public MovementHandler(SnowflakeUtil pl) {

        this.pl = pl;

    }

    @EventHandler
    public void onFall(EntityDamageEvent e) {

        if (e.getEntity() instanceof Player) {
            if (e.getCause().equals(DamageCause.FALL)) {
                Player p = (Player) e.getEntity();

                if (jumpers.contains(p)) {

                    e.setCancelled(true);
                    jumpers.remove(p);

                }
            }

        }

    }

    @EventHandler
    public void onMove(final PlayerMoveEvent event) {

        if (cooldownTime.containsKey(event.getPlayer()))
            return;

        if (event.getPlayer().getGameMode() != GameMode.CREATIVE
                && event.getPlayer().getLocation().getBlock().getRelative(BlockFace.DOWN).getType() != Material.AIR) {

            event.getPlayer().sendMessage("ready");

            event.getPlayer().setAllowFlight(true);

            cooldownTime.put(event.getPlayer(), 5);

            cooldownTask.put(event.getPlayer(), new BukkitRunnable() {
                public void run() {
                    cooldownTime.put(event.getPlayer(), cooldownTime.get(event.getPlayer()) - 1);

                    if (cooldownTime.get(event.getPlayer()) == 0) {

                        cooldownTime.remove(event.getPlayer());
                        cooldownTask.remove(event.getPlayer());
                        jumpers.remove(event.getPlayer());
                        cancel();
                    }
                }
            });

            cooldownTask.get(event.getPlayer()).runTaskTimer(pl, 20, 20);

        }

    }

    @EventHandler(priority = EventPriority.HIGH)
    public void onDoubleJump(PlayerToggleFlightEvent e) {

        Player p = e.getPlayer();
        if (!p.getGameMode().equals(GameMode.CREATIVE)) {
            e.setCancelled(true);
            p.setFlying(false);
            p.setAllowFlight(false);

            String type = "";

            if (p.getInventory().getArmorContents() != null) {
                for (ItemStack is : p.getInventory().getArmorContents()) {

                    if (is.hasItemMeta()) {
                        if (is.getItemMeta().hasLore()) {
                            for (int i = 0; i < is.getItemMeta().getLore().size(); i++) {

                                if (ChatColor.stripColor(is.getItemMeta().getLore().get(i).toLowerCase())
                                        .contains(ChatColor.stripColor("movement"))) {

                                    String part = ChatColor.stripColor(is.getItemMeta().getLore().get(i));

                                    type = SnowflakeUtil
                                            .capitalize(part.replaceAll("Movement:", "").replaceAll("\\s", ""));

                                }
                            }
                        }

                    }
                }

            }
            jumpers.add(p);
            switch (type) {
            case "Rocketjump":

                p.setVelocity(p.getLocation().getDirection().multiply(1));
                p.setVelocity(new Vector(p.getVelocity().getX(), 0.75D, p.getVelocity().getZ()));

                break;

            }

        }
    }

}

Solution

  • The obstacle here is that player.setAllowFlight(true) makes players immune to fall damage, but unfortunately this is done by the client and not the server. The client doesn't let the server know they fell at all, so no EntityDamageEvent is triggered and we can't un-cancel any damage that might've been prevented by the server (the server doesn't check whether a player has fallen and instead relies on the client to tell the the server when they fell).

    Yet since players need to be able to toggle flight mode during their entire fall so that the PlayerFlightToggleEvent is triggered (if they haven't used their double jump yet), we need a way to detect the moment right before a player is about to land, at which point, if the player still hasn't used their double jump ability, we can safely assume that they have decided not to use it at all and are falling "regularly" and should therefore take fall damage. If we disabled a player's ability to use their double jump earlier than that, we would create an (unnecessary) trade-off where a player could potentially not double jump a split second before landing (or even earlier).

    By checking the block right underneath the location that a player is about to move to during a PlayerMoveEvent, we can "predict" whether a player is in the act of landing on a non-air block. Before the PlayerMoveEvent has actually happened yet, we disable flight mode so that the player takes regular fall damage after the move is completed. The code would look something like this:

    // Inside your "PlayerMoveEvent" method
    
    // If a player is not on the ground and has fallen more than two blocks
    if (!((CraftPlayer) player).isOnGround() && player.getFallDistance() > 2) {
        Location to = event.getTo().clone().subtract(0, 0.0001, 0); // Get the location they will be at next tick
        if (to.getBlock().getType() != Material.AIR) { // If that block is not air
            player.setAllowFlight(false); // Cancel their ability to fly so that they take regular fall damage
        }
    }
    

    Note that the fall distance and the onGround value of player entities are supplied by the client and can be spoofed with custom clients, so it might be smarter to use the block below their feet to check whether they are on the ground or not (although this also for various other reasons sometimes doesn't return the right result, whether a player is truly on the ground or not is more complicated than it seems).

    One more tip for your code: If you want to create some kind of cool down period for some ability, instead of creating a relatively resource expensive task that counts down every tick, you can just put the player and the time when they can use the ability again inside a map. If they try to use the ability again, you can check whether the current time is past the time at which point they were allowed to use the ability again. Example:

    private HashMap<UUID, Long> abilityCooldown = new HashMap<>();
    
    public void onEvent(SomePlayerEvent event) {
        // Player is trying to use some ability...
    
        // It's usually safer to store IDs rather than names or player objects for various reasons
        UUID id = event.getPlayer().getUniqueId();
        if (!abilityCooldown.containsKey(id)) { // They are not in the map, so have never tried to use the ability yet
            // Let them use the ability here...
            int seconds = 5; // The amount of time to cool down for
            // Put the player's ID and the time when they will be allowed to use the ability again (future)
            abilityCooldown.put(id, System.currentTimeMillis() + 1000 * seconds);
        } else {
            // The time when they are allowed to use the ability again (we put this in the map when they used it last)
            long time = abilityCooldown.get(id);
            if (time > System.currentTimeMillis()) { // If that time is still in the future
                // Do not allow them to use the ability (maybe send them a message)
            } else {
                // Let them use the ability here...
                int seconds = 5; // The amount of time to cool down for
                abilityCooldown.put(id, System.currentTimeMillis() + 1000 * seconds); // Update the time when they can use the ablity again
            }
        }
    }