Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package com.earth2me.essentials;

import net.ess3.api.IEssentials;
import net.ess3.api.IUser;
import org.bukkit.Bukkit;
import org.bukkit.Location;

import java.util.UUID;
import java.util.regex.Pattern;

public class AsyncTimedCommand implements Runnable {
private static final double MOVE_CONSTANT = 0.3;
private final IUser commandUser;
private final IEssentials ess;
private final UUID timer_userId;
private final long timer_started;
private final long timer_delay;
private final long timer_initX;
private final long timer_initY;
private final long timer_initZ;
private final String timer_command;
private final Pattern timer_pattern;
private final boolean timer_canMove;
private int timer_task;
private double timer_health;

AsyncTimedCommand(final IUser user, final IEssentials ess, final long delay, final String command, final Pattern pattern) {
this.commandUser = user;
this.ess = ess;
this.timer_started = System.currentTimeMillis();
this.timer_delay = delay;
this.timer_health = user.getBase().getHealth();
this.timer_initX = Math.round(user.getBase().getLocation().getX() * MOVE_CONSTANT);
this.timer_initY = Math.round(user.getBase().getLocation().getY() * MOVE_CONSTANT);
this.timer_initZ = Math.round(user.getBase().getLocation().getZ() * MOVE_CONSTANT);
this.timer_userId = user.getBase().getUniqueId();
this.timer_command = command;
this.timer_pattern = pattern;
this.timer_canMove = user.isAuthorized("essentials.commandwarmups.move");

timer_task = ess.runTaskTimerAsynchronously(this, 20, 20).getTaskId();
}

@Override
public void run() {
if (commandUser == null || !commandUser.getBase().isOnline() || commandUser.getBase().getLocation() == null) {
cancelTimer(false);
return;
}

final IUser user = ess.getUser(this.timer_userId);

if (user == null || !user.getBase().isOnline()) {
cancelTimer(false);
return;
}

final Location currLocation = user.getBase().getLocation();
if (currLocation == null) {
cancelTimer(false);
return;
}

if (!timer_canMove && (Math.round(currLocation.getX() * MOVE_CONSTANT) != timer_initX
|| Math.round(currLocation.getY() * MOVE_CONSTANT) != timer_initY
|| Math.round(currLocation.getZ() * MOVE_CONSTANT) != timer_initZ
|| user.getBase().getHealth() < timer_health)) {
// user moved or took damage, cancel command warmup
cancelTimer(true);
return;
}

class DelayedCommandTask implements Runnable {
@Override
public void run() {
timer_health = user.getBase().getHealth();
final long now = System.currentTimeMillis();
if (now > timer_started + timer_delay) {
try {
cancelTimer(false);

// Clear the warmup from the user's data BEFORE executing the command
// This prevents the warmup check from triggering again
user.clearCommandWarmup(timer_pattern);

// Execute the command by dispatching it to the server
Bukkit.getScheduler().runTask(ess, () -> {
// Execute as server command to bypass the warmup check
Bukkit.dispatchCommand(user.getBase(), timer_command.substring(1)); // Remove the leading '/'
});

user.sendTl("commandWarmupComplete");

} catch (final Exception ex) {
ess.showError(user.getSource(), ex, "\\ command warmup");
}
}
}
}

ess.scheduleSyncDelayedTask(new DelayedCommandTask());
}

void cancelTimer(final boolean notifyUser) {
if (timer_task == -1) {
return;
}
try {
ess.getServer().getScheduler().cancelTask(timer_task);
if (notifyUser) {
commandUser.sendTl("commandWarmupCancelled");
}
// Clear the warmup from the user's data
commandUser.clearCommandWarmup(timer_pattern);
} finally {
timer_task = -1;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,66 @@ public void handlePlayerCommandPreprocess(final PlayerCommandPreprocessEvent eve
}
}
}

// Command warmups
if (ess.getSettings().isDebug()) {
ess.getLogger().info("Warmup check - enabled: " + ess.getSettings().isCommandWarmupsEnabled()
+ ", bypass: " + user.isAuthorized("essentials.commandwarmups.bypass")
+ ", command: " + effectiveCommand);
}

if (ess.getSettings().isCommandWarmupsEnabled()
&& !user.isAuthorized("essentials.commandwarmups.bypass")
&& (pluginCommand == null || !user.isAuthorized("essentials.commandwarmups.bypass." + pluginCommand.getName()))) {
final int argStartIndex = effectiveCommand.indexOf(" ");
final String args = argStartIndex == -1 ? ""
: " " + effectiveCommand.substring(argStartIndex);
final String fullCommand = pluginCommand == null ? effectiveCommand : pluginCommand.getName() + args;

// Check if user already has an active warmup for this command
boolean warmupFound = false;

for (final Entry<Pattern, Long> entry : user.getCommandWarmups().entrySet()) {
// Remove any expired warmups
if (entry.getValue() <= System.currentTimeMillis()) {
user.clearCommandWarmup(entry.getKey());
} else if (entry.getKey().matcher(fullCommand).matches()) {
// User's current warmup hasn't expired, inform them
final String commandWarmupTime = DateUtil.formatDateDiff(entry.getValue());
user.sendTl("commandWarmup", commandWarmupTime);
warmupFound = true;
event.setCancelled(true);
}
}

if (!warmupFound) {
final Entry<Pattern, Long> warmupEntry = ess.getSettings().getCommandWarmupEntry(fullCommand);

if (warmupEntry != null) {
// Check if the player has permission to use this command before starting warmup
if (pluginCommand != null && !pluginCommand.testPermissionSilent(user.getBase())) {
// Player doesn't have permission, let the command fail naturally
return;
}

if (ess.getSettings().isDebug()) {
ess.getLogger().info("Applying " + warmupEntry.getValue() + "ms warmup on /" + fullCommand + " for " + user.getName() + ".");
}
event.setCancelled(true);

// Store the warmup expiry
final Date expiry = new Date(System.currentTimeMillis() + warmupEntry.getValue());
user.addCommandWarmup(warmupEntry.getKey(), expiry, ess.getSettings().isCommandWarmupPersistent(fullCommand));

// Notify user about warmup
final String warmupTime = DateUtil.formatDateDiff(expiry.getTime());
user.sendTl("commandWarmup", warmupTime);

// Start the async timed command
new AsyncTimedCommand(user, ess, warmupEntry.getValue(), event.getMessage(), warmupEntry.getKey());
}
}
}
}

@EventHandler(priority = EventPriority.NORMAL)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,14 @@ public interface ISettings extends IConf {

boolean isCommandCooldownPersistent(String label);

boolean isCommandWarmupsEnabled();

long getCommandWarmupMs(String label);

Entry<Pattern, Long> getCommandWarmupEntry(String label);

boolean isCommandWarmupPersistent(String label);

boolean isNpcsInBalanceRanking();

NumberFormat getCurrencyFormat();
Expand Down
11 changes: 11 additions & 0 deletions Essentials/src/main/java/com/earth2me/essentials/IUser.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.earth2me.essentials.api.IAsyncTeleport;
import com.earth2me.essentials.commands.IEssentialsCommand;
import com.earth2me.essentials.config.entities.CommandCooldown;
import com.earth2me.essentials.config.entities.CommandWarmup;
import net.ess3.api.MaxMoneyException;
import net.ess3.api.events.AfkStatusChangeEvent;
import net.essentialsx.api.v2.services.mail.MailMessage;
Expand Down Expand Up @@ -233,6 +234,16 @@ default boolean hasOutstandingTeleportRequest() {

boolean clearCommandCooldown(Pattern pattern);

Map<Pattern, Long> getCommandWarmups();

List<CommandWarmup> getWarmupsList();

Date getCommandWarmupExpiry(String label);

void addCommandWarmup(Pattern pattern, Date expiresAt, boolean save);

boolean clearCommandWarmup(Pattern pattern);

/*
* PlayerExtension
*/
Expand Down
89 changes: 89 additions & 0 deletions Essentials/src/main/java/com/earth2me/essentials/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ public class Settings implements net.ess3.api.ISettings {
private boolean isCustomNewUsernameMessage;
private List<String> spawnOnJoinGroups;
private Map<Pattern, Long> commandCooldowns;
private Map<Pattern, Long> commandWarmups;
private boolean npcsInBalanceRanking = false;
private NumberFormat currencyFormat;
private List<EssentialsSign> unprotectedSigns = Collections.emptyList();
Expand Down Expand Up @@ -918,6 +919,7 @@ public void reloadConfig() {
muteCommands = _getMuteCommands();
spawnOnJoinGroups = _getSpawnOnJoinGroups();
commandCooldowns = _getCommandCooldowns();
commandWarmups = _getCommandWarmups();
npcsInBalanceRanking = _isNpcsInBalanceRanking();
currencyFormat = _getCurrencyFormat();
unprotectedSigns = _getUnprotectedSign();
Expand Down Expand Up @@ -1848,6 +1850,93 @@ public boolean isCommandCooldownPersistent(final String label) {
return config.getBoolean("command-cooldown-persistence", true);
}

private Map<Pattern, Long> _getCommandWarmups() {
final CommentedConfigurationNode section = config.getSection("command-warmups");
if (section == null) {
return null;
}
final Map<Pattern, Long> result = new LinkedHashMap<>();
for (Map.Entry<String, Object> entry : ConfigurateUtil.getRawMap(section).entrySet()) {
String cmdEntry = entry.getKey();
Object value = entry.getValue();
Pattern pattern = null;

/* ================================
* >> Regex
* ================================ */
if (cmdEntry.startsWith("^")) {
try {
pattern = Pattern.compile(cmdEntry.substring(1));
} catch (final PatternSyntaxException e) {
ess.getLogger().warning("Command warmup error: " + e.getMessage());
}
} else {
// Escape above Regex
if (cmdEntry.startsWith("\\^")) {
cmdEntry = cmdEntry.substring(1);
}
final String cmd = cmdEntry
.replaceAll("\\*", ".*"); // Wildcards are accepted as asterisk * as known universally.
pattern = Pattern.compile(cmd + "( .*)?"); // This matches arguments, if present, to "ignore" them from the feature.
}

/* ================================
* >> Process warmup value
* ================================ */
if (value instanceof String) {
try {
value = Double.parseDouble(value.toString());
} catch (final NumberFormatException ignored) {
}
}
if (!(value instanceof Number)) {
ess.getLogger().warning("Command warmup error: '" + value + "' is not a valid warmup");
continue;
}
final double warmup = ((Number) value).doubleValue();
if (warmup < 1) {
ess.getLogger().warning("Command warmup with very short " + warmup + " warmup.");
}

result.put(pattern, (long) warmup * 1000); // convert to milliseconds
}
return result;
}

@Override
public boolean isCommandWarmupsEnabled() {
return commandWarmups != null;
}

@Override
public long getCommandWarmupMs(final String label) {
final Entry<Pattern, Long> result = getCommandWarmupEntry(label);
return result != null ? result.getValue() : -1;
}

@Override
public Entry<Pattern, Long> getCommandWarmupEntry(final String label) {
if (isCommandWarmupsEnabled()) {
for (final Entry<Pattern, Long> entry : this.commandWarmups.entrySet()) {
final boolean matches = entry.getKey().matcher(label).matches();
if (isDebug()) {
ess.getLogger().info(String.format("Checking command '%s' against warmup '%s': %s", label, entry.getKey(), matches));
}

if (matches) {
return entry;
}
}
}
return null;
}

@Override
public boolean isCommandWarmupPersistent(final String label) {
// TODO: enable per command warmup specification for persistence.
return config.getBoolean("command-warmup-persistence", true);
}

private boolean _isNpcsInBalanceRanking() {
return config.getBoolean("npcs-in-balance-ranking", false);
}
Expand Down
Loading