diff --git a/Essentials/src/main/java/com/earth2me/essentials/AsyncTimedCommand.java b/Essentials/src/main/java/com/earth2me/essentials/AsyncTimedCommand.java new file mode 100644 index 00000000000..3c518b15b19 --- /dev/null +++ b/Essentials/src/main/java/com/earth2me/essentials/AsyncTimedCommand.java @@ -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; + } + } +} diff --git a/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java b/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java index f33efcf8a6f..fd5a24bedd7 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java +++ b/Essentials/src/main/java/com/earth2me/essentials/EssentialsPlayerListener.java @@ -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 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 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) diff --git a/Essentials/src/main/java/com/earth2me/essentials/ISettings.java b/Essentials/src/main/java/com/earth2me/essentials/ISettings.java index 69ba0f3fdc7..e93356135cf 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/ISettings.java +++ b/Essentials/src/main/java/com/earth2me/essentials/ISettings.java @@ -366,6 +366,14 @@ public interface ISettings extends IConf { boolean isCommandCooldownPersistent(String label); + boolean isCommandWarmupsEnabled(); + + long getCommandWarmupMs(String label); + + Entry getCommandWarmupEntry(String label); + + boolean isCommandWarmupPersistent(String label); + boolean isNpcsInBalanceRanking(); NumberFormat getCurrencyFormat(); diff --git a/Essentials/src/main/java/com/earth2me/essentials/IUser.java b/Essentials/src/main/java/com/earth2me/essentials/IUser.java index 8fce2df96d0..64ec8b134ab 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/IUser.java +++ b/Essentials/src/main/java/com/earth2me/essentials/IUser.java @@ -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; @@ -233,6 +234,16 @@ default boolean hasOutstandingTeleportRequest() { boolean clearCommandCooldown(Pattern pattern); + Map getCommandWarmups(); + + List getWarmupsList(); + + Date getCommandWarmupExpiry(String label); + + void addCommandWarmup(Pattern pattern, Date expiresAt, boolean save); + + boolean clearCommandWarmup(Pattern pattern); + /* * PlayerExtension */ diff --git a/Essentials/src/main/java/com/earth2me/essentials/Settings.java b/Essentials/src/main/java/com/earth2me/essentials/Settings.java index 72c47d74dc7..09be9192109 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/Settings.java +++ b/Essentials/src/main/java/com/earth2me/essentials/Settings.java @@ -134,6 +134,7 @@ public class Settings implements net.ess3.api.ISettings { private boolean isCustomNewUsernameMessage; private List spawnOnJoinGroups; private Map commandCooldowns; + private Map commandWarmups; private boolean npcsInBalanceRanking = false; private NumberFormat currencyFormat; private List unprotectedSigns = Collections.emptyList(); @@ -918,6 +919,7 @@ public void reloadConfig() { muteCommands = _getMuteCommands(); spawnOnJoinGroups = _getSpawnOnJoinGroups(); commandCooldowns = _getCommandCooldowns(); + commandWarmups = _getCommandWarmups(); npcsInBalanceRanking = _isNpcsInBalanceRanking(); currencyFormat = _getCurrencyFormat(); unprotectedSigns = _getUnprotectedSign(); @@ -1848,6 +1850,93 @@ public boolean isCommandCooldownPersistent(final String label) { return config.getBoolean("command-cooldown-persistence", true); } + private Map _getCommandWarmups() { + final CommentedConfigurationNode section = config.getSection("command-warmups"); + if (section == null) { + return null; + } + final Map result = new LinkedHashMap<>(); + for (Map.Entry 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 result = getCommandWarmupEntry(label); + return result != null ? result.getValue() : -1; + } + + @Override + public Entry getCommandWarmupEntry(final String label) { + if (isCommandWarmupsEnabled()) { + for (final Entry 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); } diff --git a/Essentials/src/main/java/com/earth2me/essentials/UserData.java b/Essentials/src/main/java/com/earth2me/essentials/UserData.java index 6e280b8942e..d05dbb2489f 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/UserData.java +++ b/Essentials/src/main/java/com/earth2me/essentials/UserData.java @@ -3,6 +3,7 @@ import com.earth2me.essentials.config.ConfigurateUtil; import com.earth2me.essentials.config.EssentialsUserConfiguration; import com.earth2me.essentials.config.entities.CommandCooldown; +import com.earth2me.essentials.config.entities.CommandWarmup; import com.earth2me.essentials.config.entities.LazyLocation; import com.earth2me.essentials.config.holders.UserConfigHolder; import com.earth2me.essentials.userstorage.ModernUserMap; @@ -695,6 +696,58 @@ public boolean clearCommandCooldown(final Pattern pattern) { return false; } + public List getWarmupsList() { + return holder.timestamps().commandWarmups(); + } + + public Map getCommandWarmups() { + final Map map = new HashMap<>(); + for (final CommandWarmup w : getWarmupsList()) { + if (w == null || w.isIncomplete()) { + continue; + } + map.put(w.pattern(), w.value()); + } + return map; + } + + public Date getCommandWarmupExpiry(final String label) { + for (CommandWarmup warmup : getWarmupsList()) { + if (warmup == null || warmup.isIncomplete()) { + continue; + } + if (warmup.pattern().matcher(label).matches()) { + return new Date(warmup.value()); + } + } + return null; + } + + public void addCommandWarmup(final Pattern pattern, final Date expiresAt, final boolean save) { + final CommandWarmup warmup = new CommandWarmup(); + warmup.pattern(pattern); + warmup.value(expiresAt.getTime()); + if (warmup.isIncomplete()) { + return; + } + holder.timestamps().commandWarmups().add(warmup); + if (save) { + save(); + } + } + + public boolean clearCommandWarmup(final Pattern pattern) { + if (holder.timestamps().commandWarmups().isEmpty()) { + return false; + } + + if (getWarmupsList().removeIf(warmup -> warmup != null && !warmup.isIncomplete() && warmup.pattern().equals(pattern))) { + save(); + return true; + } + return false; + } + public boolean isAcceptingPay() { return holder.acceptingPay(); } diff --git a/Essentials/src/main/java/com/earth2me/essentials/config/EssentialsConfiguration.java b/Essentials/src/main/java/com/earth2me/essentials/config/EssentialsConfiguration.java index 875683cb823..4be34dbf9e9 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/config/EssentialsConfiguration.java +++ b/Essentials/src/main/java/com/earth2me/essentials/config/EssentialsConfiguration.java @@ -4,11 +4,13 @@ import com.earth2me.essentials.config.annotations.DeleteIfIncomplete; import com.earth2me.essentials.config.annotations.DeleteOnEmpty; import com.earth2me.essentials.config.entities.CommandCooldown; +import com.earth2me.essentials.config.entities.CommandWarmup; import com.earth2me.essentials.config.entities.LazyLocation; import com.earth2me.essentials.config.processors.DeleteIfIncompleteProcessor; import com.earth2me.essentials.config.processors.DeleteOnEmptyProcessor; import com.earth2me.essentials.config.serializers.BigDecimalTypeSerializer; import com.earth2me.essentials.config.serializers.CommandCooldownSerializer; +import com.earth2me.essentials.config.serializers.CommandWarmupSerializer; import com.earth2me.essentials.config.serializers.LocationTypeSerializer; import com.earth2me.essentials.config.serializers.MailMessageSerializer; import com.earth2me.essentials.config.serializers.MaterialTypeSerializer; @@ -62,6 +64,7 @@ public class EssentialsConfiguration { .register(LazyLocation.class, new LocationTypeSerializer()) .register(Material.class, new MaterialTypeSerializer()) .register(CommandCooldown.class, new CommandCooldownSerializer()) + .register(CommandWarmup.class, new CommandWarmupSerializer()) .register(MailMessage.class, new MailMessageSerializer()) .build(); diff --git a/Essentials/src/main/java/com/earth2me/essentials/config/entities/CommandWarmup.java b/Essentials/src/main/java/com/earth2me/essentials/config/entities/CommandWarmup.java new file mode 100644 index 00000000000..2b1570527b2 --- /dev/null +++ b/Essentials/src/main/java/com/earth2me/essentials/config/entities/CommandWarmup.java @@ -0,0 +1,32 @@ +package com.earth2me.essentials.config.entities; + +import com.earth2me.essentials.config.processors.DeleteIfIncompleteProcessor; + +import java.util.regex.Pattern; + +public class CommandWarmup implements DeleteIfIncompleteProcessor.IncompleteEntity { + private Pattern pattern; + + public Pattern pattern() { + return this.pattern; + } + + public void pattern(final Pattern value) { + this.pattern = value; + } + + private Long value; + + public Long value() { + return this.value; + } + + public void value(final Long value) { + this.value = value; + } + + @Override + public boolean isIncomplete() { + return value == null || pattern == null; + } +} diff --git a/Essentials/src/main/java/com/earth2me/essentials/config/holders/UserConfigHolder.java b/Essentials/src/main/java/com/earth2me/essentials/config/holders/UserConfigHolder.java index 5455376675a..77e2fa6d3ea 100644 --- a/Essentials/src/main/java/com/earth2me/essentials/config/holders/UserConfigHolder.java +++ b/Essentials/src/main/java/com/earth2me/essentials/config/holders/UserConfigHolder.java @@ -3,6 +3,7 @@ import com.earth2me.essentials.config.annotations.DeleteIfIncomplete; import com.earth2me.essentials.config.annotations.DeleteOnEmpty; import com.earth2me.essentials.config.entities.CommandCooldown; +import com.earth2me.essentials.config.entities.CommandWarmup; import com.earth2me.essentials.config.entities.LazyLocation; import net.essentialsx.api.v2.services.mail.MailMessage; import org.bukkit.Location; @@ -462,5 +463,20 @@ public List commandCooldowns() { public void commandCooldowns(final List value) { this.commandCooldowns = value; } + + @DeleteOnEmpty + @DeleteIfIncomplete + private @MonotonicNonNull List commandWarmups; + + public List commandWarmups() { + if (this.commandWarmups == null) { + this.commandWarmups = new ArrayList<>(); + } + return this.commandWarmups; + } + + public void commandWarmups(final List value) { + this.commandWarmups = value; + } } } diff --git a/Essentials/src/main/java/com/earth2me/essentials/config/serializers/CommandWarmupSerializer.java b/Essentials/src/main/java/com/earth2me/essentials/config/serializers/CommandWarmupSerializer.java new file mode 100644 index 00000000000..2de2d182083 --- /dev/null +++ b/Essentials/src/main/java/com/earth2me/essentials/config/serializers/CommandWarmupSerializer.java @@ -0,0 +1,40 @@ +package com.earth2me.essentials.config.serializers; + +import com.earth2me.essentials.config.entities.CommandWarmup; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.serialize.SerializationException; +import org.spongepowered.configurate.serialize.TypeSerializer; + +import java.lang.reflect.Type; +import java.util.regex.Pattern; + +public class CommandWarmupSerializer implements TypeSerializer { + @Override + public CommandWarmup deserialize(Type type, ConfigurationNode node) throws SerializationException { + try { + final Pattern pattern = node.node("pattern").get(Pattern.class); + if (node.node("value").isNull()) { + return null; + } + final Long longValue = node.node("value").getLong(); + final CommandWarmup warmup = new CommandWarmup(); + warmup.pattern(pattern); + warmup.value(longValue); + return warmup; + } catch (final SerializationException ignored) { + } + return null; + } + + @Override + public void serialize(Type type, @Nullable CommandWarmup obj, ConfigurationNode node) throws SerializationException { + if (obj == null || obj.isIncomplete()) { + node.raw(null); + return; + } + + node.node("pattern").set(Pattern.class, obj.pattern()); + node.node("value").set(Long.class, obj.value()); + } +} diff --git a/Essentials/src/main/resources/config.yml b/Essentials/src/main/resources/config.yml index d4838238e23..3e949470a77 100644 --- a/Essentials/src/main/resources/config.yml +++ b/Essentials/src/main/resources/config.yml @@ -713,6 +713,28 @@ command-cooldowns: # Whether command cooldowns should persist across server shutdowns. command-cooldown-persistence: true +# Command warmups: Set a delay (in seconds) before a command is executed. +# During the warmup period, the player must stand still (unless they have essentials.commandwarmups.move permission). +# If the player moves or takes damage during this period, the command is cancelled. +# +# This is similar to the teleport warmup feature, but for any command. +# Bypass all warmups with the 'essentials.commandwarmups.bypass' permission, or +# bypass specific command warmups with 'essentials.commandwarmups.bypass.'. +# +# Wildcards and regex patterns are supported (same as command cooldowns). +# For example: +# - 'home': 5 # 5-second warmup before /home executes +# - 'tpa*': 3 # 3-second warmup for all commands starting with tpa +# +# Note: If you have a command that starts with ^, escape it using a backslash (\). +command-warmups: + #home: 5 # 5-second warmup on /home command + #back: 3 # 3-second warmup on /back command + #afk: 3 # 3-second warmup on /afk command + +# Whether command warmups should persist across server shutdowns. +command-warmup-persistence: true + # Whether NPC balances should be included in balance ranking features like /balancetop. # NPC balances can include features like factions from the FactionsUUID plugin. npcs-in-balance-ranking: false diff --git a/Essentials/src/main/resources/messages_en.properties b/Essentials/src/main/resources/messages_en.properties index 7aa38dabc01..bebb8355c52 100644 --- a/Essentials/src/main/resources/messages_en.properties +++ b/Essentials/src/main/resources/messages_en.properties @@ -145,6 +145,9 @@ commandArgumentOptional= commandArgumentOr= commandArgumentRequired= commandCooldown=You cannot type that command for {0}. +commandWarmup=Command will execute in {0}. Don''t move or take damage\! +commandWarmupComplete=Command executing now... +commandWarmupCancelled=Command warmup cancelled because you moved or took damage. commandDisabled=The command {0} is disabled. commandFailed=Command {0} failed\: commandHelpFailedForPlugin=Error getting help for plugin\: {0} diff --git a/Essentials/src/test/java/com/earth2me/essentials/CommandWarmupTest.java b/Essentials/src/test/java/com/earth2me/essentials/CommandWarmupTest.java new file mode 100644 index 00000000000..e1865821794 --- /dev/null +++ b/Essentials/src/test/java/com/earth2me/essentials/CommandWarmupTest.java @@ -0,0 +1,153 @@ +package com.earth2me.essentials; + +import com.earth2me.essentials.config.entities.CommandWarmup; +import org.junit.jupiter.api.Test; + +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class CommandWarmupTest { + + @Test + public void testCommandWarmupCreation() { + System.out.println("CommandWarmupTest: testCommandWarmupCreation"); + + // Create a command warmup with 5 second delay + final CommandWarmup warmup = new CommandWarmup(); + final Pattern pattern = Pattern.compile("home"); + warmup.pattern(pattern); + warmup.value(5000L); + + assertNotNull(warmup); + assertEquals(pattern.pattern(), warmup.pattern().pattern()); + assertEquals(5000L, warmup.value()); + assertFalse(warmup.isIncomplete()); + } + + @Test + public void testCommandWarmupIncomplete() { + System.out.println("CommandWarmupTest: testCommandWarmupIncomplete"); + + // Create an incomplete warmup (null pattern) + final CommandWarmup warmup = new CommandWarmup(); + warmup.value(5000L); + // Don't set pattern, leaving it null + + assertNotNull(warmup); + assertTrue(warmup.isIncomplete()); + } + + @Test + public void testCommandWarmupWithRegexPattern() { + System.out.println("CommandWarmupTest: testCommandWarmupWithRegexPattern"); + + // Test regex pattern like /^home-.*/ + final Pattern regexPattern = Pattern.compile("^home-.*"); + final CommandWarmup warmup = new CommandWarmup(); + warmup.pattern(regexPattern); + warmup.value(3000L); + + assertNotNull(warmup); + assertEquals(regexPattern.pattern(), warmup.pattern().pattern()); + assertEquals(3000L, warmup.value()); + + // Test that the pattern works as expected + assertTrue(regexPattern.matcher("home-test").matches()); + assertTrue(regexPattern.matcher("home-main").matches()); + assertFalse(regexPattern.matcher("back").matches()); + } + + @Test + public void testWarmupValueValidation() { + System.out.println("CommandWarmupTest: testWarmupValueValidation"); + + final Pattern pattern = Pattern.compile("home"); + + // Test various warmup durations + final CommandWarmup warmup1 = new CommandWarmup(); + warmup1.pattern(pattern); + warmup1.value(1000L); // 1 second + + final CommandWarmup warmup5 = new CommandWarmup(); + warmup5.pattern(pattern); + warmup5.value(5000L); // 5 seconds + + final CommandWarmup warmup10 = new CommandWarmup(); + warmup10.pattern(pattern); + warmup10.value(10000L); // 10 seconds + + assertEquals(1000L, warmup1.value()); + assertEquals(5000L, warmup5.value()); + assertEquals(10000L, warmup10.value()); + } + + @Test + public void testCommandWarmupEquality() { + System.out.println("CommandWarmupTest: testCommandWarmupEquality"); + + final Pattern pattern = Pattern.compile("home"); + + final CommandWarmup warmup1 = new CommandWarmup(); + warmup1.pattern(pattern); + warmup1.value(5000L); + + final CommandWarmup warmup2 = new CommandWarmup(); + warmup2.pattern(Pattern.compile("home")); + warmup2.value(5000L); + + // Pattern objects with same regex should have same pattern() string + assertEquals(warmup1.pattern().pattern(), warmup2.pattern().pattern()); + assertEquals(warmup1.value(), warmup2.value()); + } + + @Test + public void testWarmupPatternMatching() { + System.out.println("CommandWarmupTest: testWarmupPatternMatching"); + + // Test various pattern types + final Pattern exactPattern = Pattern.compile("home"); + final Pattern wildcardPattern = Pattern.compile("tp.*"); + final Pattern regexPattern = Pattern.compile("^warp-[a-z]+$"); + + // Test exact match + assertTrue(exactPattern.matcher("home").matches()); + assertFalse(exactPattern.matcher("home2").matches()); + + // Test wildcard pattern + assertTrue(wildcardPattern.matcher("tp").matches()); + assertTrue(wildcardPattern.matcher("tpa").matches()); + assertTrue(wildcardPattern.matcher("tpaccept").matches()); + assertFalse(wildcardPattern.matcher("home").matches()); + + // Test regex pattern + assertTrue(regexPattern.matcher("warp-spawn").matches()); + assertTrue(regexPattern.matcher("warp-pvp").matches()); + assertFalse(regexPattern.matcher("warp-").matches()); + assertFalse(regexPattern.matcher("warp-123").matches()); + } + + @Test + public void testWarmupIncompleteness() { + System.out.println("CommandWarmupTest: testWarmupIncompleteness"); + + // Test incomplete warmup with no pattern + final CommandWarmup noPattern = new CommandWarmup(); + noPattern.value(5000L); + assertTrue(noPattern.isIncomplete()); + + // Test incomplete warmup with no value + final CommandWarmup noValue = new CommandWarmup(); + noValue.pattern(Pattern.compile("home")); + assertTrue(noValue.isIncomplete()); + + // Test complete warmup + final CommandWarmup complete = new CommandWarmup(); + complete.pattern(Pattern.compile("home")); + complete.value(5000L); + assertFalse(complete.isIncomplete()); + } +}