Skip to content

Commit

Permalink
GH-393 Add implementation for command cooldowns; Fix typo in requirem…
Browse files Browse the repository at this point in the history
…ent package name (#393)

* Add implementation for command cooldowns; Fix typo in requirement package name

* Fix registration of global scoped validators.

* Fix overwriting of validator, in case of same scopes

---------

Co-authored-by: Rollczi <[email protected]>
  • Loading branch information
Rafał Chomczyk and Rollczi authored Apr 5, 2024
1 parent d0e69d5 commit 9be3a81
Show file tree
Hide file tree
Showing 20 changed files with 315 additions and 21 deletions.
17 changes: 9 additions & 8 deletions .github/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ TODO
- [x] @Join String
- [x] @Async @Arg
- [x] @Permission
- [ ] @Arg @Range(min = 1, max = 10) int
- [x] @Arg @Range(min = 1, max = 10) int
- [x] @Flag for booleans
- [ ] @Editor
- [x] @Arg Enum
- [ ] @Arg List<String> // for example text, text, text
- [ ] @Arg Set<T> // for example 1, 4, 5
- [x] @Arg List<String> // for example text, text, text
- [x] @Arg Set<T> // for example 1, 4, 5
- [ ] @Arg Map<K, V> // for example key1=value1, key2=value2 // do wyrzucenia chyba
- [ ] @Suggest
- [ ] @Literal
- [x] @Validate(validator = MyValidator.class)
- [ ] @Cooldown - Określa cza s, przez który użytkownik nie może ponownie użyć danej komendy po jej wykonaniu.
- [x] @Cooldown - Określa cza s, przez który użytkownik nie może ponownie użyć danej komendy po jej wykonaniu.
- [ ] @Cooldown(second = 10, bypass = "myplugin.bypass.cooldown", scope = CooldownScope.GLOBAL)
- [ ] @MaxCalls(second = 10, max = 5)
- [ ] @Delay - Adnotacja ta może być użyta przed metodą, aby określić czas opóźnienia (w milisekundach) przed wykonaniem
Expand All @@ -37,19 +37,20 @@ TODO
- [x] @Context
- [ ] .debugger(true)
- [x] .errorHandler()
- [ ] @Arg @Regex("") String
- [ ] @Arg @Regex("[a-zA-Z0-9_]") @Length(min = 3, max = 16) String playerName
- [x] @Arg @Regex("") String
- [x] @Arg @Regex("[a-zA-Z0-9_]") @Length(min = 3, max = 16) String playerName
- [x] Return result handler
- [x] Return result handler remapper
- [x] Support JDA
- [ ] Support Sponge
- [x] Support Sponge
- [x] Support BungeeCord
- [ ] Support Waterfall
- [x] Support Waterfall
- [x] Support Velocity
- [x] Support Bukkit
- [ ] Support Nukkit
- [x] Support Adventure
- [x] Support Paper
- [x] Support Fabric
- [x] Add support to provide other types of arguments by platforms

### This is not a command example. They're just concepts!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import dev.rollczi.litecommands.annotations.command.CommandAnnotationProcessor;
import dev.rollczi.litecommands.annotations.command.RootCommandAnnotationProcessor;
import dev.rollczi.litecommands.annotations.context.ContextRequirementProcessor;
import dev.rollczi.litecommands.annotations.cooldown.CooldownAnnotationResolver;
import dev.rollczi.litecommands.annotations.description.DescriptionAnnotationResolver;
import dev.rollczi.litecommands.annotations.execute.ExecuteAnnotationResolver;
import dev.rollczi.litecommands.annotations.flag.FlagArgumentProcessor;
Expand Down Expand Up @@ -55,6 +56,7 @@ public static <SENDER> AnnotationProcessorService<SENDER> defaultService() {
.register(new PermissionAnnotationResolver<>())
.register(new PermissionsAnnotationResolver<>())
.register(new ValidateAnnotationResolver<>())
.register(new CooldownAnnotationResolver<>())
// argument meta processors
.register(new KeyAnnotationResolver<>())
.register(new QuotedAnnotationProcessor<>())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dev.rollczi.litecommands.annotations.cooldown;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.time.temporal.ChronoUnit;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Cooldown {

String key();

long count();

ChronoUnit unit();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package dev.rollczi.litecommands.annotations.cooldown;

import dev.rollczi.litecommands.annotations.AnnotationInvoker;
import dev.rollczi.litecommands.annotations.AnnotationProcessor;
import dev.rollczi.litecommands.cooldown.CooldownContext;
import dev.rollczi.litecommands.meta.Meta;
import java.time.Duration;

public class CooldownAnnotationResolver<SENDER> implements AnnotationProcessor<SENDER> {

@Override
public AnnotationInvoker<SENDER> process(AnnotationInvoker<SENDER> invoker) {
return invoker
.on(Cooldown.class, ((annotation, metaHolder) -> metaHolder.meta()
.put(Meta.COOLDOWN, getCooldownContext(annotation))));
}

private CooldownContext getCooldownContext(Cooldown cooldown) {
return new CooldownContext(cooldown.key(), Duration.of(cooldown.count(), cooldown.unit()));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import dev.rollczi.litecommands.invocation.Invocation;
import dev.rollczi.litecommands.requirement.Requirement;
import dev.rollczi.litecommands.validator.ValidatorResult;
import dev.rollczi.litecommands.validator.requirment.RequirementValidator;
import dev.rollczi.litecommands.validator.requirement.RequirementValidator;

import java.lang.annotation.Annotation;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package dev.rollczi.litecommands.annotations.cooldown;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

import dev.rollczi.litecommands.annotations.LiteTestSpec;
import dev.rollczi.litecommands.annotations.command.Command;
import dev.rollczi.litecommands.annotations.execute.Execute;
import dev.rollczi.litecommands.cooldown.CooldownState;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.Test;

class CooldownAnnotationTest extends LiteTestSpec {

@Command(name = "test")
@Cooldown(key = "test-cooldown", count = 1, unit = ChronoUnit.SECONDS)
static class TestCommand {

@Execute
void execute() {}

}

@Test
void test() {
platform.execute("test");

CooldownState cooldownState = platform.execute("test")
.assertFailedAs(CooldownState.class);

assertEquals("test-cooldown", cooldownState.getCooldownContext().getKey());
assertEquals(Duration.ofSeconds(1), cooldownState.getCooldownContext().getDuration());
assertFalse(cooldownState.getRemainingDuration().isZero());

Awaitility.await()
.atMost(Duration.ofSeconds(3))
.until(() -> platform.execute("test").isSuccessful());
}

}
1 change: 1 addition & 0 deletions litecommands-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ plugins {
}

dependencies {
api("net.jodah:expiringmap:0.5.11")
api("org.panda-lang:expressible:1.3.6")
api("org.jetbrains:annotations:24.0.1")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import dev.rollczi.litecommands.scheduler.SchedulerPoll;
import dev.rollczi.litecommands.validator.ValidatorResult;
import dev.rollczi.litecommands.validator.ValidatorService;
import dev.rollczi.litecommands.validator.requirment.RequirementValidator;
import dev.rollczi.litecommands.validator.requirement.RequirementValidator;
import dev.rollczi.litecommands.wrapper.Wrap;
import dev.rollczi.litecommands.wrapper.WrapFormat;
import dev.rollczi.litecommands.wrapper.Wrapper;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package dev.rollczi.litecommands.cooldown;

import dev.rollczi.litecommands.identifier.Identifier;
import java.util.Objects;

class CooldownCompositeKey {

private final Identifier identifier;
private final String cooldownKey;

public CooldownCompositeKey(Identifier identifier, String cooldownKey) {
this.identifier = identifier;
this.cooldownKey = cooldownKey;
}

public Identifier getIdentifier() {
return identifier;
}

public String getCooldownKey() {
return cooldownKey;
}

@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}

if (object == null || getClass() != object.getClass()) {
return false;
}

CooldownCompositeKey that = (CooldownCompositeKey) object;
return Objects.equals(identifier, that.identifier) && Objects.equals(cooldownKey, that.cooldownKey);
}

@Override
public int hashCode() {
return Objects.hash(identifier, cooldownKey);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package dev.rollczi.litecommands.cooldown;

import java.time.Duration;

public class CooldownContext {

private final String key;
private final Duration duration;

public CooldownContext(String key, Duration duration) {
this.key = key;
this.duration = duration;
}

public String getKey() {
return key;
}

public Duration getDuration() {
return duration;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package dev.rollczi.litecommands.cooldown;

import java.time.Duration;
import java.time.Instant;

public class CooldownState {

private final CooldownContext cooldownContext;
private final Duration remainingDuration;
private final Instant expirationTime;

public CooldownState(CooldownContext cooldownContext, Duration remainingDuration, Instant expirationTime) {
this.cooldownContext = cooldownContext;
this.remainingDuration = remainingDuration;
this.expirationTime = expirationTime;
}

public CooldownContext getCooldownContext() {
return cooldownContext;
}

public Duration getRemainingDuration() {
return remainingDuration;
}

public Instant getExpirationTime() {
return expirationTime;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package dev.rollczi.litecommands.cooldown;

import static dev.rollczi.litecommands.message.LiteMessages.COMMAND_COOLDOWN;

import dev.rollczi.litecommands.handler.result.ResultHandler;
import dev.rollczi.litecommands.handler.result.ResultHandlerChain;
import dev.rollczi.litecommands.invocation.Invocation;
import dev.rollczi.litecommands.message.MessageRegistry;

public class CooldownStateResultHandler<SENDER> implements ResultHandler<SENDER, CooldownState> {

private final MessageRegistry<SENDER> messageRegistry;

public CooldownStateResultHandler(MessageRegistry<SENDER> messageRegistry) {
this.messageRegistry = messageRegistry;
}

@Override
public void handle(Invocation<SENDER> invocation, CooldownState cooldownState, ResultHandlerChain<SENDER> chain) {
this.messageRegistry.getInvoked(COMMAND_COOLDOWN, invocation, cooldownState)
.ifPresent(object -> chain.resolve(invocation, object));
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package dev.rollczi.litecommands.cooldown;

import dev.rollczi.litecommands.argument.suggester.input.SuggestionInput;
import dev.rollczi.litecommands.flow.Flow;
import dev.rollczi.litecommands.invocation.Invocation;
import dev.rollczi.litecommands.meta.Meta;
import dev.rollczi.litecommands.meta.MetaHolder;
import dev.rollczi.litecommands.validator.Validator;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.TimeUnit;
import net.jodah.expiringmap.ExpiringMap;

public class CooldownStateValidator<SENDER> implements Validator<SENDER> {

private final ExpiringMap<CooldownCompositeKey, Instant> cooldowns;

public CooldownStateValidator() {
this.cooldowns = ExpiringMap.builder()
.variableExpiration()
.build();
}

@Override
public Flow validate(Invocation<SENDER> invocation, MetaHolder metaHolder) {
if (invocation.arguments() instanceof SuggestionInput<?>) {
return Flow.continueFlow();
}

List<CooldownContext> cooldownContexts = metaHolder.metaCollector().collect(Meta.COOLDOWN);
if (cooldownContexts.isEmpty()) {
return Flow.continueFlow();
}

return validateCooldown(invocation, cooldownContexts.get(0));
}

private Flow validateCooldown(Invocation<SENDER> invocation, CooldownContext cooldownContext) {
CooldownCompositeKey compositeKey = new CooldownCompositeKey(invocation.platformSender().getIdentifier(), cooldownContext.getKey());

Instant now = Instant.now();
Instant expirationTime = cooldowns.get(compositeKey);
if (expirationTime != null && expirationTime.isAfter(now)) {
return Flow.terminateFlow(new CooldownState(cooldownContext, Duration.between(now, expirationTime), expirationTime));
}

cooldowns.put(compositeKey, now.plus(cooldownContext.getDuration()), cooldownContext.getDuration().toNanos(), TimeUnit.NANOSECONDS);
return Flow.continueFlow();
}

}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package dev.rollczi.litecommands.message;

import dev.rollczi.litecommands.argument.resolver.standard.InstantArgumentResolver;
import dev.rollczi.litecommands.cooldown.CooldownState;
import dev.rollczi.litecommands.invalidusage.InvalidUsage;
import dev.rollczi.litecommands.permission.MissingPermissions;
import dev.rollczi.litecommands.time.DurationParser;

public class LiteMessages {

Expand Down Expand Up @@ -44,6 +46,16 @@ public class LiteMessages {
input -> "Invalid date format '" + input + "'! Use: <yyyy-MM-dd> <HH:mm:ss> (INSTANT_INVALID_FORMAT)"
);

/**
* Command cooldown message.
* e. g. when user uses the command too frequently.
* @see dev.rollczi.litecommands.cooldown.CooldownStateResultHandler
*/
public static final MessageKey<CooldownState> COMMAND_COOLDOWN = MessageKey.of(
"command-cooldown",
state -> "You are on cooldown! Remaining time: " + DurationParser.DATE_TIME_UNITS.format(state.getRemainingDuration()) + " (COMMAND_COOLDOWN)"
);

protected LiteMessages() {
}

Expand Down
Loading

0 comments on commit 9be3a81

Please sign in to comment.