Skip to content

Commit

Permalink
feat: implement cooldowns (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
Citymonstret authored Jan 8, 2024
1 parent fb9be78 commit dbb9746
Show file tree
Hide file tree
Showing 29 changed files with 1,921 additions and 3 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ Command pre- & post-processors for [Cloud v2](https://github.com/incendo/cloud).

## postprocessors

- [cloud-processors-confirmation](./cloud-processors-confirmation)
- [cloud-processors-confirmation](./cloud-processors-confirmation)
- [cloud-processors-cooldown](./cloud-processors-cooldown)
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@
defaultAsDefault = true,
put = "*",
putAll = "*",
stagedBuilder = true
stagedBuilder = true,
depluralize = true,
depluralizeDictionary = "creationListeners:creationListener"
)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PACKAGE})
@Retention(RetentionPolicy.SOURCE)
Expand Down
90 changes: 90 additions & 0 deletions cloud-processors-cooldown/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# cloud-processors-cooldown

Postprocessor that adds command cooldowns.

## Installation

cloud-processors-cooldown is not yet available on Maven Central.

## Usage

The cooldown system is managed by a `CooldownManager`, so the first step is to create an instance of that:
```java
CooldownManager<YourSenderType> cooldownManager = CooldownManager.of(configuration);
```
The configuration is an instance of `CooldownConfiguration`. Refer to the JavaDocs for information about specific options,
but an example would be:
```java
CooldownConfiguration configuration = CooldownConfiguration.<YourSenderType>builder()
.repository(CooldownRepository.forMap(new HashMap<>()))
.addActiveCooldownListener(...)
.addCooldownCreationListener(...)
.cooldownNotifier(notifier)
.build();
```
The listeners are invoked when different events take place. The active cooldown listener in particular may be used to
inform the command sender that their command execution got blocked due to an active cooldown.

The repository stores active cooldowns for a command sender in the form of cooldown profiles.
The cooldowns are grouped by their `CooldownGroup`, by default a unique group will be created per command.
You may create a named group by using `CooldownGroup.named(name)`. Commands that use the same cooldown group
will have their cooldowns shared by the command sender.

You may create a repository from a map, `CloudCache` or even implement your own. If you want to persist the cooldowns
across multiple temporary sessions then you may use a mapping repository to store the cooldown profiles for a persistent key,
rather than the potentially temporary command sender objects:
```java
CooldownRepository repository = CooldownRepository.mapping(
sender -> sender.uuid(),
CooldownRepository.forMap(new HashMap<UUID, CooldownProfile>())
);
```

You may also customize how the cooldown profiles are created by passing a `CooldownProfileFactory` to the `CooldownConfiguration`.

If you want to have the cooldowns automatically removed from the repository to prevent unused profiles from taking up memory you
may register a `ScheduledCleanupCreationListener`:
```java
CooldownRepository repository = CooldownRepository.forMap(new HashMap<>());
ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
CooldownConfiguration configuration = CooldownConfiguration.<YourSenderType>builder()
// ...
.repository(repository)
.addCreationListener(new ScheduledCleanupCreationListener(executorService, repository))
.build();
```

You then need to register the postprocessor:
```java
commandManager.registerCommandPostProcessor(cooldownManager.createPostprocessor());
```

### Builders

The cooldowns are configured using a `Cooldown` instance:
```java
Cooldown cooldown = Cooldown.of(DurationFunction.constant(Duration.ofMinutes(5L)));
Cooldown cooldown = Cooldown.of(DurationFunction.constant(Duration.ofMinutes(5L)), CooldownGroup.named("group-name"));
```
which can then be applied to the command by either manually setting the meta value:
```java
commandBuilder.meta(CooldownManager.META_COOLDOWN_DURATION, cooldown);
```
or by applying the cooldown to the builder:
```java
commandBuilder.apply(cooldown);
```

### Annotations

Annotated commands may use the `@Cooldown` annotation:
```java
@Cooldown(duration = 5, timeUnit = ChronoUnit.MINUTES, group = "some-group")
public void yourCommand() {
// ...
}
```
You need to install the builder modifier for this to work:
```java
CooldownBuilderModifier.install(annotationParser);
```
16 changes: 16 additions & 0 deletions cloud-processors-cooldown/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
plugins {
id("cloud-processors.base-conventions")
id("cloud-processors.publishing-conventions")
}

dependencies {
api(projects.cloudProcessorsCommon)

compileOnly(libs.cloud.annotations)
}

// TODO(City): Disable this
// we're getting errors on generated files due to -Werror :(
tasks.withType<JavaCompile> {
options.compilerArgs.remove("-Werror")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//
// MIT License
//
// Copyright (c) 2024 Incendo
//
// 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 org.incendo.cloud.processors.cooldown;

import cloud.commandframework.Command;
import org.apiguardian.api.API;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.immutables.value.Value;
import org.incendo.cloud.processors.immutables.StagedImmutableBuilder;

/**
* An instance of a cooldown for a {@link CooldownGroup} belonging to a command sender.
*
* @param <C> command sender type
* @since 1.0.0
*/
@StagedImmutableBuilder
@Value.Immutable
@API(status = API.Status.STABLE, since = "1.0.0")
public interface Cooldown<C> extends Command.Builder.Applicable<C> {

/**
* Returns a new cooldown with the given {@code duration}
* using {@link CooldownConfiguration#fallbackGroup()} as the group.
*
* @param <C> command sender type
* @param duration the duration
* @return the cooldown
*/
static <C> @NonNull Cooldown<C> of(final @NonNull DurationFunction<C> duration) {
return Cooldown.<C>builder().duration(duration).build();
}

/**
* Returns a new cooldown with the given {@code duration} and {@code group}.
*
* @param <C> command sender type
* @param duration the duration
* @param group cooldown group
* @return the cooldown
*/
static <C> @NonNull Cooldown<C> of(
final @NonNull DurationFunction<C> duration,
final @NonNull CooldownGroup group
) {
return Cooldown.<C>builder().duration(duration).group(group).build();
}

/**
* Returns a new cooldown builder.
*
* @param <C> command sender type
* @return the builder
*/
static <C> ImmutableCooldown.@NonNull DurationBuildStage<C> builder() {
return ImmutableCooldown.builder();
}

/**
* Returns the cooldown duration.
*
* @return the duration
*/
@NonNull DurationFunction<C> duration();

/**
* Returns the group that this instance belongs to.
* If set to {@code null} then {@link CooldownConfiguration#fallbackGroup()} will be used.
*
* @return the cooldown group
*/
default @Nullable CooldownGroup group() {
return null;
}

@Override
default Command.@NonNull Builder<C> applyToCommandBuilder(Command.@NonNull Builder<C> builder) {
return builder.meta(CooldownManager.META_COOLDOWN_DURATION, this);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//
// MIT License
//
// Copyright (c) 2024 Incendo
//
// 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 org.incendo.cloud.processors.cooldown;

import cloud.commandframework.Command;
import cloud.commandframework.context.CommandContext;
import java.time.Clock;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import org.apiguardian.api.API;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.immutables.value.Value;
import org.incendo.cloud.processors.cooldown.listener.CooldownActiveListener;
import org.incendo.cloud.processors.cooldown.listener.CooldownCreationListener;
import org.incendo.cloud.processors.cooldown.profile.CooldownProfileFactory;
import org.incendo.cloud.processors.cooldown.profile.StandardCooldownProfileFactory;
import org.incendo.cloud.processors.immutables.StagedImmutableBuilder;

/**
* Configuration for a {@link CooldownManager}.
*
* @param <C> command sender type
* @since 1.0.0
*/
@Value.Immutable
@StagedImmutableBuilder
@API(status = API.Status.STABLE, since = "1.0.0")
public interface CooldownConfiguration<C> {

/**
* Returns a new configuration builder.
*
* @param <C> command sender type
* @return the builder
*/
static <C> ImmutableCooldownConfiguration.@NonNull RepositoryBuildStage<C> builder() {
return ImmutableCooldownConfiguration.builder();
}

/**
* Returns the repository used to store cooldowns.
*
* @return the repository
*/
@NonNull CooldownRepository<C> repository();

/**
* Returns the active cooldown listeners.
*
* <p>The active cooldown listeners are invoked when a cooldown is preventing a sender from executing a command.</p>
*
* @return active cooldown listeners
*/
@NonNull List<CooldownActiveListener<C>> activeCooldownListeners();

/**
* Returns the creation listeners.
*
* <p>The creation listeners are invoked when a new cooldown is created.</p>
*
* @return creation listeners
*/
@NonNull List<CooldownCreationListener<C>> creationListeners();

/**
* Returns a predicate that determines whether the {@link CommandContext}
* should bypass the cooldown requirement.
*
* <p>The default implementation always returns {@code false}</p>
*
* @return predicate that determines whether cooldowns should be skipped
*/
default @NonNull Predicate<@NonNull CommandContext<C>> bypassCooldown() {
return context -> false;
}

/**
* Returns the clock used to calculate the current time.
*
* @return the clock
*/
default @NonNull Clock clock() {
return Clock.systemUTC();
}

/**
* Returns the factory that produces cooldown profiles.
*
* @return cooldown profile factory
*/
default @NonNull CooldownProfileFactory profileFactory() {
return new StandardCooldownProfileFactory(this);
}

/**
* Returns the function that determines the fallback {@link CooldownGroup} in case no group has been provided
* to the {@link Cooldown}.
*
* @return the fallback group function
*/
default @NonNull Function<@NonNull Command<C>, @NonNull CooldownGroup> fallbackGroup() {
return CooldownGroup::command;
}
}
Loading

0 comments on commit dbb9746

Please sign in to comment.