Skip to content

Commit

Permalink
add @Cooldown annotation because we're nearly stable!
Browse files Browse the repository at this point in the history
  • Loading branch information
Revxrsal committed Dec 14, 2024
1 parent 867bae9 commit 528b683
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 0 deletions.
2 changes: 2 additions & 0 deletions common/src/main/java/revxrsal/commands/Lamp.java
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,8 @@ public Builder() {
responseHandler(CompletionStageResponseHandler.INSTANCE);
responseHandler(OptionalResponseHandler.INSTANCE);
commandCondition(PermissionConditionChecker.INSTANCE);
commandCondition(CooldownCondition.INSTANCE);
hooks().onPostCommandExecuted(CooldownCondition.INSTANCE);
accept(KotlinFeatureRegistry.INSTANCE);
}

Expand Down
54 changes: 54 additions & 0 deletions common/src/main/java/revxrsal/commands/annotation/Cooldown.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* This file is part of sweeper, licensed under the MIT License.
*
* Copyright (c) Revxrsal <[email protected]>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package revxrsal.commands.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

/**
* Adds a cooldown to the command.
*/
@DistributeOnMethods
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Cooldown {

/**
* The cooldown value
*
* @return The command cooldown value
*/
long value();

/**
* The time unit in which the cooldown goes for.
*
* @return The time unit for the cooldown
*/
TimeUnit unit() default TimeUnit.SECONDS;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* This file is part of lamp, licensed under the MIT License.
*
* Copysecond (c) Revxrsal <[email protected]>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the seconds
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copysecond notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package revxrsal.commands.command;

import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import revxrsal.commands.annotation.Cooldown;
import revxrsal.commands.exception.CooldownException;
import revxrsal.commands.hook.PostCommandExecutedHook;
import revxrsal.commands.node.ExecutionContext;
import revxrsal.commands.process.CommandCondition;

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

@ApiStatus.Internal
public enum CooldownCondition implements CommandCondition<CommandActor>, PostCommandExecutedHook<CommandActor> {

INSTANCE;

private static final ScheduledExecutorService COOLDOWN_POOL = Executors.newSingleThreadScheduledExecutor();
private final Map<UUID, Map<Integer, Long>> cooldowns = new ConcurrentHashMap<>();

@Override
public void onPostExecuted(@NotNull ExecutableCommand<CommandActor> command, @NotNull ExecutionContext<CommandActor> context) {
Cooldown cooldown = command.annotations().get(Cooldown.class);
if (cooldown == null || cooldown.value() == 0) return;
Map<Integer, Long> spans = cooldowns.computeIfAbsent(context.actor().uniqueId(), u -> new ConcurrentHashMap<>());
spans.put(command.hashCode(), System.currentTimeMillis());
COOLDOWN_POOL.schedule(() -> spans.remove(command.hashCode()), cooldown.value(), cooldown.unit());
}

@Override public void test(@NotNull ExecutionContext<CommandActor> context) {
Cooldown cooldown = context.command().annotations().get(Cooldown.class);
if (cooldown == null || cooldown.value() == 0) return;
UUID uuid = context.actor().uniqueId();
Map<Integer, Long> spans = cooldowns.get(uuid);
if (spans == null) return;
long created = spans.get(context.command().hashCode());
long passed = System.currentTimeMillis() - created;
long left = cooldown.unit().toMillis(cooldown.value()) - passed;
if (left > 0 && left < 1000)
left = 1000L; // for formatting
throw new CooldownException(left);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* This file is part of lamp, licensed under the MIT License.
*
* Copysecond (c) Revxrsal <[email protected]>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the seconds
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copysecond notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package revxrsal.commands.exception;

import org.jetbrains.annotations.NotNull;
import revxrsal.commands.annotation.Cooldown;
import revxrsal.commands.command.CommandActor;

import java.util.concurrent.TimeUnit;

import static revxrsal.commands.util.Preconditions.notNull;

/**
* Thrown when the {@link CommandActor} has to wait before executing a
* command again. This is set by {@link Cooldown}.
*/
@ThrowableFromCommand
public class CooldownException extends RuntimeException {

/**
* The time left (in milliseconds)
*/
private final long timeLeft;

/**
* Creates a new {@link CooldownException} with the given timestamp
* in milliseconds
*
* @param timeLeft The time left in milliseconds
*/
public CooldownException(long timeLeft) {
this.timeLeft = timeLeft;
}

/**
* Creates a new {@link CooldownException} with the given timestamp
* in any unit
*
* @param unit The time unit in which the time left is given
* @param timeLeft The time left in the given unit
*/
public CooldownException(TimeUnit unit, long timeLeft) {
this.timeLeft = unit.toMillis(timeLeft);
}

/**
* Returns the time left before being able to execute again
*
* @return Time left in milliseconds
*/
public long getTimeLeftMillis() {
return timeLeft;
}

/**
* Returns the time left in the given unit
*
* @param unit Unit to convert to
* @return The time left
*/
public long getTimeLeft(@NotNull TimeUnit unit) {
notNull(unit, "unit");
return unit.convert(timeLeft, TimeUnit.MILLISECONDS);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
import revxrsal.commands.command.CommandActor;
import revxrsal.commands.node.ParameterNode;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.StringJoiner;

public class DefaultExceptionHandler<A extends CommandActor> extends RuntimeExceptionAdapter<A> {

@HandleException
Expand Down Expand Up @@ -135,9 +140,49 @@ public void onUnknownParameter(@NotNull UnknownParameterException e, @NotNull A
actor.error("Unknown flag: " + e.name());
}

@HandleException
public void onCooldown(@NotNull CooldownException e, @NotNull A actor) {
actor.error("You must wait " + formatTimeFancy(e.getTimeLeftMillis()) + " before using this command again.");
}

@HandleException
public void onSendable(@NotNull SendableException e, @NotNull A actor) {
e.sendTo(actor);
}

public static String formatTimeFancy(long time) {
Duration d = Duration.ofMillis(time);
long hours = d.toHours();
long minutes = d.minusHours(hours).getSeconds() / 60;
long seconds = d.minusMinutes(minutes).minusHours(hours).getSeconds();
List<String> words = new ArrayList<>();
if (hours != 0)
words.add(hours + plural(hours, " hour"));
if (minutes != 0)
words.add(minutes + plural(minutes, " minute"));
if (seconds != 0)
words.add(seconds + plural(seconds, " second"));
return toFancyString(words);
}

public static <T> String toFancyString(List<T> list) {
StringJoiner builder = new StringJoiner(", ");
if (list.isEmpty()) return "";
if (list.size() == 1) return list.get(0).toString();
for (int i = 0; i < list.size(); i++) {
T el = list.get(i);
if (i + 1 == list.size())
return builder + " and " + el.toString();
else
builder.add(el.toString());
}
return builder.toString();
}

public static String plural(Number count, String thing) {
if (count.intValue() == 1) return thing;
if (thing.endsWith("y"))
return thing.substring(0, thing.length() - 1) + "ies";
return thing + "s";
}
}

0 comments on commit 528b683

Please sign in to comment.