(Tutorial) How to create cooldowns with Spigot

Before you start (prerequisites)

You don't need to have any extensive Java experience, but it helps to understand how some of the basic features work. The main point of this tutorial is to go over the method and it's implementation, not how Java features like Singletons work.
 

How we want it to work 

For these examples, we will use the command /cooldown as the feature. It can only be used once every 15 seconds.
 

(Pitfall) Method #1: Using Runnable Timer Tasks

Before we get into the most common and performance friendly cooldown system, I want to cover a commonly suggested method that you should avoid. It's suggested fairly often because of how simple it seems at first.

In this method, your plugin has a Singleton cooldown manager:

public class CooldownManager {

    private HashMap<UUID, Integer> cooldowns = new HashMap<>();

    public static final int DEFAULT_COOLDOWN = 15;

    public void setCooldown(UUID player, Integer time){
        if(time == null)
            cooldowns.remove(player);
        else
            cooldowns.put(player, time);
    }

    public int getCooldown(UUID player){
        return (cooldowns.get(player) == null ? 0 : cooldowns.get(player));
    }

    private CooldownManager(){}

    public static final CooldownManager INSTANCE = new CooldownManager();

}

It contains the basic methods for setting, getting, and removing from a cooldown map. Next we have the Command Executor:

public class CooldownCommand implements CommandExecutor {

    @Override
    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
        //Player only command
        if(sender instanceof Player){
            Player p = (Player) sender;
            int timeLeft = CooldownManager.INSTANCE.getCooldown(p.getUniqueId());
            //If the cooldown has expired
            if(timeLeft == 0){
                //Use the feature
                p.sendMessage(ChatColor.GREEN + "Feature used!");
                //Start the countdown task
                CooldownManager.INSTANCE.setCooldown(p.getUniqueId(), CooldownManager.DEFAULT_COOLDOWN);
                new BukkitRunnable() {
                    @Override
                    public void run() {
                        int timeLeft = CooldownManager.INSTANCE.getCooldown(p.getUniqueId());
                        if(timeLeft == 0){
                            CooldownManager.INSTANCE.setCooldown(p.getUniqueId(), null);
                            this.cancel();
                            return;
                        }
                        CooldownManager.INSTANCE.setCooldown(p.getUniqueId(), --timeLeft);
                    }
                }.runTaskTimer(SpaceRaiders.Companion.getPlugin(), 20, 20);

            }else{
                //Hasn't expired yet, shows how many seconds left until it does
                p.sendMessage(ChatColor.RED.toString() + timeLeft + " seconds before you can use this feature again.");
            }
        }else{
            sender.sendMessage("Player-only command");
        }

        return true;
    }

}

 

(Register the command executor)
As you can tell, it seems very simple. The main part of this section is that it decreases the time left before they can use the feature again every second, in the BukkitRunnable task timer. It will work just fine as it is now.

This method, however, has some major issues. For one, you should always avoid using runnables, as async tasks will create a thread for each task. Second, this means if you have several hundred players online, you will have the map updating many, many times a second (assuming the players are using it). For minigames with multiple cooldown features, there can be as many as a hundred tasks running at once, and it can become quite a mess internally.

One of the largest drawbacks, however, is that you can only track seconds with this method (at the smallest). You can't track anything smaller, and if you wanted to you would have to change the time unit to 1/10s of a second or less, and it would need to be updated 10 times more often.
 

Method #2: Using Timestamps

Rather than updating a variable every second or more, you can actually just save the last time a player used a feature, then subtract it from the current time to get how long ago the player used the feature. From there, it's simple to make sure it has been more than x time. It looks something like this:

if (current time) - (the time the featured was last used) > (delay) [do feature, update last used time]

For accurate time tracking, we can use a feature in programming known as a timestamp. Timestamps are just like what they sound like - a stamp of the time it was used at. The most accurate type is to use System time milliseconds. Basically, the timestamp is in milliseconds. More technically, how many milliseconds have passed since January 1 1960 (known as epoch). You can use the function System.currentTimeMillis() to get the current timestamp in milliseconds.

We'll use the CooldownManager from earlier, with two changes. int (or Integer) type can't store a variable of that size, millisecond timestamps are stored in Long type variables. Simply replace occurrences of "Integer" or "int" with "Long" in the manager class. Second, instead of returning "0" if the map doesn't contain the cooldown timestamp, we have to use Long.valueOf(0):

return (cooldowns.get(player) == null ? Long.valueOf(0) : cooldowns.get(player));

With this method, our CooldownCommand class should look like this:

Code (Text):

public class CooldownCommand implements CommandExecutor {

    @Override
    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
        //Player only command
        if(sender instanceof Player){
            Player p = (Player) sender;
            //Get the amount of milliseconds that have passed since the feature was last used.
            Long timeLeft = System.currentTimeMillis() - CooldownManager.INSTANCE.getCooldown(p.getUniqueId());
            if(TimeUnit.MILLISECONDS.toSeconds(timeLeft) >= CooldownManager.DEFAULT_COOLDOWN){
                p.sendMessage(ChatColor.GREEN + "Featured used!");
                CooldownManager.INSTANCE.setCooldown(p.getUniqueId(), System.currentTimeMillis());
            }else{
                p.sendMessage(ChatColor.RED.toString() + TimeUnit.MILLISECONDS.toSeconds(timeLeft) + " seconds before you can use this feature again.");
            }
        }else{
            sender.sendMessage("Player-only command");
        }

        return true;
    }

}


 

Already much shorter, and the variable is only updated when the feature is actually used!
You'll notice I used a Java util method you may not have seen before - TimeUnit. TimeUnit is extremely helpful, especially if you don't want to convert time units yourself. In the above example, we used TimeUnit.MILLISECONDS to tell TimeUnit the input variable will be in milliseconds, then used toSeconds() to get the output in seconds.

With a bit more math, you can convert to hours, subtract the hours from the initial variable, then convert the remainder to seconds. It does take some playing around with to get it down to perfection. There may be some available libraries or functions by other users that does this for you, however.