Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delayed Commands example project #460

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions examples/bukkit/delayed-commands/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Delayed Commands

A simple example showcasing various ways to implement "delayed commands" with the CommandAPI. This was inspired by [#451](https://github.com/JorelAli/CommandAPI/issues/451). While delayed commands are not directly implemented in the CommandAPI, this example aims to show various ways to add them yourself. Developers are encouraged to find solutions that fit their own needs, perhaps adding them to this project for others to use.

### What is a "delayed command"?

Per the [original issue](https://github.com/JorelAli/CommandAPI/issues/451), a delayed command is limited to only run once within a certain time period. This can prevent users from spamming commands that might perform CPU-intensive tasks. This creates a delay between executions of a command, hence, a "delayed command".

### TODO: Write some examples and explain them
47 changes: 47 additions & 0 deletions examples/bukkit/delayed-commands/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dev.jorel</groupId>
<artifactId>delayed-commands</artifactId>
<version>0.0.1-SNAPSHOT</version>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<repositories>
<!-- This adds the Spigot Maven repository to the build -->
<repository>
<id>spigot-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
</repositories>

<dependencies>
<dependency>
<groupId>dev.jorel</groupId>
<artifactId>commandapi-bukkit-core</artifactId>
<version>9.0.2</version>
</dependency>

<!--This adds the Spigot API artifact to the build -->
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.19.4-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>

<build>
<defaultGoal>clean package</defaultGoal>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<release>17</release>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.github.jorelali;

import io.github.jorelali.delayedapicommand.DelayedAPICommands;
import io.github.jorelali.delayhandler.DelayHandlerCommands;
import org.bukkit.plugin.java.JavaPlugin;

public class Main extends JavaPlugin {
@Override
public void onEnable() {
DelayHandlerCommands.registerCommands();
DelayedAPICommands.registerCommands();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.github.jorelali.delayedapicommand;

import java.util.concurrent.TimeUnit;

public class DelayedAPICommands {
public static void registerCommands() {
// PlayerDelayedCommandAPICommand has all the functions of a CommandAPICommand, but also adds delay to any
// executors given by the `executesPlayer` methods

// PerPlayerDelayedCommandAPICommand keeps track of the delay for each player that uses it
new PerPlayerDelayedCommandAPICommand("delayedAPICommandPerPlayer", 10, TimeUnit.SECONDS)
.executesPlayer(info -> {
info.sender().sendMessage("You ran delayedAPICommandPerPlayer");
})
.register();

// GlobalPlayerDelayedCommandAPICommand shares its delay for all players
new GlobalPlayerDelayedCommandAPICommand("delayedAPICommandGlobal", 10, TimeUnit.SECONDS)
.executesPlayer((player, args) -> {
player.sendMessage("You ran delayedAPICommandGlobal");
})
.register();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.github.jorelali.delayedapicommand;

import dev.jorel.commandapi.CommandAPI;
import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException;
import org.bukkit.entity.Player;

import java.util.concurrent.TimeUnit;

// This shares its delay for all players
public class GlobalPlayerDelayedCommandAPICommand extends PlayerDelayedCommandAPICommand {
// The next time when this command will be allowed to run
// The default time is 0, which is always in the past, so the command will always be run the first time
private long nextTime = 0;

public GlobalPlayerDelayedCommandAPICommand(String commandName, long time, TimeUnit timeUnit) {
super(commandName, time, timeUnit);
}

@Override
void throwExceptionIfCannotRun(Player player) throws WrapperCommandSyntaxException {
// We don't have to worry about this overflowing for about 290 million years
// https://stackoverflow.com/questions/2978452/when-will-system-currenttimemillis-overflow
// This code will reward your patience by letting you run the command without waiting
long currentTime = System.currentTimeMillis();

// If it isn't time to run the command yet, throw the exception
if(currentTime < nextTime) {
throw CommandAPI.failWithString(
"This command cannot be run for another "
+ getDurationString(nextTime - currentTime)
);
}

// If the command is run, set the next possible time
nextTime = currentTime + delay;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.github.jorelali.delayedapicommand;

import dev.jorel.commandapi.CommandAPI;
import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException;
import org.bukkit.entity.Player;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

// This keeps track of the delay for each player that uses it
public class PerPlayerDelayedCommandAPICommand extends PlayerDelayedCommandAPICommand {
// Use UUID here in case player leaves and rejoins server to get around delay
private final Map<UUID, Long> nextTimesPerPlayer = new HashMap<>();

public PerPlayerDelayedCommandAPICommand(String commandName, long time, TimeUnit timeUnit) {
super(commandName, time, timeUnit);
}

@Override
void throwExceptionIfCannotRun(Player player) throws WrapperCommandSyntaxException {
// Get the next time when this player is allowed to run the command
// The default time is 0, which is always in the past, so the command will always be run the first time
long nextTime = nextTimesPerPlayer.getOrDefault(player.getUniqueId(), 0L);
long currentTime = System.currentTimeMillis();

// If it isn't time to run the command yet, throw the exception
if(currentTime < nextTime) {
throw CommandAPI.failWithString(
"You must wait "
+ getDurationString(nextTime - currentTime)
+ " before running this command again"
);
}

// If the command is run, set the next possible time
nextTimesPerPlayer.put(player.getUniqueId(), currentTime + delay);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package io.github.jorelali.delayedapicommand;

import dev.jorel.commandapi.CommandAPICommand;
import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException;
import dev.jorel.commandapi.executors.*;
import org.bukkit.entity.Player;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

// This class extends CommandAPICommand to implement delayed commands
public abstract class PlayerDelayedCommandAPICommand extends CommandAPICommand {
protected final long delay;

public PlayerDelayedCommandAPICommand(String commandName, long time, TimeUnit timeUnit) {
super(commandName);
// Delay is the value in milliseconds
delay = timeUnit.toMillis(time);
}

// This method is implemented by the child classes
// If the command is currently delayed for the player, an exception should be thrown
abstract void throwExceptionIfCannotRun(Player player) throws WrapperCommandSyntaxException;

// This helper method formats a millisecond duration into a String representing how long is left in the delay
static String getDurationString(long millis) {
Duration duration = Duration.ofMillis(millis);

long days = duration.toDays();
long hours = duration.toHours() % 24;
long minutes = duration.toMinutes() % 60;
long seconds = duration.getSeconds() % 60;

String durationString;
if(days != 0) durationString = days + ":" + hours + ":" + minutes + " days";
else if (hours != 0) durationString = hours + ":" + minutes + ":" + seconds + "hours";
else if (minutes != 0) durationString = minutes + ":" + seconds + "minutes";
else if (seconds != 1) durationString = seconds + " seconds";
else durationString = "1 second";

return durationString;
}

// Override the usual executes methods to replace the executor with a delayed executor
// They take one CommandAPI executor and uses it inside a new executor
// The new executor also uses the throwExceptionIfCannotRun method to stop
// command execution if the command's delay is currently in effect
@Override
public CommandAPICommand executesPlayer(PlayerExecutionInfo executor) {
super.executesPlayer(info -> {
throwExceptionIfCannotRun(info.sender());
executor.run(info);
});

return this;
}

@Override
public CommandAPICommand executesPlayer(PlayerCommandExecutor executor) {
super.executesPlayer((player, args) -> {
throwExceptionIfCannotRun(player);
executor.run(player, args);
});

return this;
}

@Override
public CommandAPICommand executesPlayer(PlayerResultingExecutionInfo executor) {
super.executesPlayer(info -> {
throwExceptionIfCannotRun(info.sender());
return executor.run(info);
});

return this;
}

@Override
public CommandAPICommand executesPlayer(PlayerResultingCommandExecutor executor) {
super.executesPlayer((player, args) -> {
throwExceptionIfCannotRun(player);
executor.run(player, args);
});

return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.github.jorelali.delayhandler;

import dev.jorel.commandapi.CommandAPICommand;

import java.util.concurrent.TimeUnit;

public class DelayHandlerCommands {
public static void registerCommands() {
// PlayerDelayHandler is a general interface for turning a normal player executor into a delayed executor
// PerPlayerDelayHandler keeps track of the delay for each player that uses it
PlayerDelayHandler perPlayerDelay = new PerPlayerDelayHandler(10, TimeUnit.SECONDS);

new CommandAPICommand("delayHandlerPerPlayer1")
// PlayerDelayHandler works directly on the Executors of the method, so it goes inside the `executes` methods
// This delay only acts on this method
// This also works for the executes methods of CommandTree and ArgumentTree
.executesPlayer(perPlayerDelay.delayMethod(info -> info.sender().sendMessage("You ran delayHandlerPerPlayer1")))
.register();

new CommandAPICommand("delayHandlerPerPlayer2")
// While the PlayerDelayHandler only works on one Executor at a time, it can be reused for multiple methods,
// making them to share a delay
.executesPlayer(perPlayerDelay.delayMethod(info -> info.sender().sendMessage("You ran delayHandlerPerPlayer2")))
.register();

// GlobalPlayerDelayHandler shares its delay for all players
PlayerDelayHandler globalDelay = new GlobalPlayerDelayHandler(10, TimeUnit.SECONDS);

new CommandAPICommand("delayHandlerGlobal1")
// This new PlayerDelayHandler is independent of the first two commands, since it is a different object
.executesPlayer(globalDelay.delayMethod(info -> info.sender().sendMessage("You ran delayHandlerGlobal1")))
.register();

new CommandAPICommand("delayHandlerGlobal2")
.executesPlayer(globalDelay.delayMethod(info -> info.sender().sendMessage("You ran delayHandlerGlobal2")))
.register();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.github.jorelali.delayhandler;

import dev.jorel.commandapi.CommandAPI;
import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException;
import dev.jorel.commandapi.executors.ExecutionInfo;
import org.bukkit.entity.Player;

import java.util.concurrent.TimeUnit;

// This shares its delay for all players
public class GlobalPlayerDelayHandler implements PlayerDelayHandler {
private final long delay;
// The next time when this command will be allowed to run
// The default time is 0, which is always in the past, so the command will always be run the first time
private long nextTime = 0;

public GlobalPlayerDelayHandler(long time, TimeUnit unit) {
// Delay is the value in milliseconds
delay = unit.toMillis(time);
}

@Override
public void throwExceptionIfCannotRun(ExecutionInfo<Player, ?> info) throws WrapperCommandSyntaxException {
// We don't have to worry about this overflowing for about 290 million years
// https://stackoverflow.com/questions/2978452/when-will-system-currenttimemillis-overflow
// This code will reward your patience by letting you run the command without waiting
long currentTime = System.currentTimeMillis();

// If it isn't time to run the command yet, throw the exception
if(currentTime < nextTime) {
throw CommandAPI.failWithString(
"This command cannot be run for another "
+ PlayerDelayHandler.getDurationString(nextTime - currentTime)
);
}

// If the command is run, set the next possible time
nextTime = currentTime + delay;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.github.jorelali.delayhandler;

import dev.jorel.commandapi.CommandAPI;
import dev.jorel.commandapi.exceptions.WrapperCommandSyntaxException;
import dev.jorel.commandapi.executors.ExecutionInfo;
import org.bukkit.entity.Player;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

// This keeps track of the delay for each player that uses it
public class PerPlayerDelayHandler implements PlayerDelayHandler {
private final long delay;
// Use UUID here in case player leaves and rejoins server to get around delay
private final Map<UUID, Long> nextTimesPerPlayer = new HashMap<>();

public PerPlayerDelayHandler(long time, TimeUnit timeUnit) {
// Delay is the value in milliseconds
delay = timeUnit.toMillis(time);
}

@Override
public void throwExceptionIfCannotRun(ExecutionInfo<Player, ?> info) throws WrapperCommandSyntaxException {
// Get the next time when this player is allowed to run the command
// The default time is 0, which is always in the past, so the command will always be run the first time
long nextTime = nextTimesPerPlayer.getOrDefault(info.sender().getUniqueId(), 0L);
long currentTime = System.currentTimeMillis();

// If it isn't time to run the command yet, throw the exception
if(currentTime < nextTime) {
throw CommandAPI.failWithString(
"You must wait "
+ PlayerDelayHandler.getDurationString(nextTime - currentTime)
+ " before running this command again"
);
}

// If the command is run, set the next possible time
nextTimesPerPlayer.put(info.sender().getUniqueId(), currentTime + delay);
}
}
Loading