Lamp Command Framework

repository·v4·Indexed 43 days ago

https://github.com/revxrsal/lamp

A multi-platform command framework for Java and Kotlin supporting Minecraft servers (Bukkit, BungeeCord, Sponge, Velocity, Minestom, Fabric), Discord (via JDA slash commands), and CLI applications. It features an annotation-based API for defining commands, subcommands, and permissions, and includes deep integration with Mojang Brigadier via the BNode builder and BrigadierParser.

Tokens
45K
Snippets
150
Records
190
Agent score
56%

What's inside Lamp

  1. Overview of Lamp command framework

    v4
    Lamp is a versatile command framework for Java and Kotlin applications. It is designed to support multiple platforms including Minecraft server implementations (Bukkit, BungeeCord, Sponge, Velocity, Minestom, Fabric), Discord (via JDA slash commands), and standard command-line applications. It uses an annotation-based approach to define commands, subcommands, parameters, and permissions, providing a clean and idiomatic API for modern development.
  2. Use EntitySelector for dynamic entity arguments

    v4

    The Bukkit implementation of Lamp includes a specialized argument factory for EntitySelector. When you use EntitySelector<T> as a parameter type in your commands, the framework uses the generic type T to determine which entity type to look for and checks for the @Single annotation to decide if the command expects a single entity or a collection.

    Example usage in a command parameter:

    • EntitySelector<Player> with @Single will resolve to a single Player.
    • EntitySelector<Entity> without @Single will resolve to multiple Entity objects.
    • EntitySelector<LivingEntity> will resolve to the specific subclass of Entity provided in the generic type.
    // Conceptual usage in a command definition
    public void myCommand(@Single EntitySelector<Player> player) {
        // ...
    }
  3. Use @FallbackPrefix to define Bukkit command aliases

    v4

    When using BukkitCommandHooks, you can control how a Lamp command is registered in Bukkit by using the @FallbackPrefix annotation on your command.

    If the annotation is present, the hook uses the specified value as the prefix for the Bukkit command (e.g., /prefix:command). If the annotation is absent, the hook falls back to the defaultFallbackPrefix provided during the construction of BukkitCommandHooks.

  4. Manage command cooldowns with CooldownHandle

    v4

    The CooldownHandle interface provides fine-grained control over command cooldowns. It can be used in two primary ways depending on whether you want static or dynamic cooldown durations.

    1. Using the @Cooldown annotation (Static)

    If you want a fixed cooldown duration, annotate the CooldownHandle parameter (not the method) with @Cooldown. This allows you to use shortcut methods that automatically reference the annotated values.

    Note: You must place the @Cooldown annotation on the parameter itself. Placing it on the method will result in an error.

    2. Manual/Dynamic Cooldowns (Dynamic)

    If you need to specify durations at runtime (e.g., from a configuration file), do not use the @Cooldown annotation. Instead, pass the duration and TimeUnit directly to the methods, or use withCooldown to create a specialized handle that supports shortcut methods.

    // Static approach using annotation on the parameter
    @Command("foo")
    public void foo(
         ...,
         @Cooldown(value = 10, unit = TimeUnit.SECONDS) CooldownHandle cooldownHandle
    ) {
         cooldownHandle.requireNotOnCooldown(); // Uses 10s from annotation
         // ... action
         cooldownHandle.cooldown(); // Uses 10s from annotation
    }
    
    // Dynamic approach using withCooldown
    @Command("foo")
    public void foo(
         ...,
         CooldownHandle cooldownHandle
    ) {
         // Create a handle with a specific duration
         cooldownHandle = cooldownHandle.withCooldown(10, TimeUnit.SECONDS);
    
         cooldownHandle.requireNotOnCooldown(); // Uses the 10s set above
         // ... action
         cooldownHandle.cooldown(); // Uses the 10s set above
    }
  5. Understand the ExecutableCommand interface

    v4

    The ExecutableCommand<A> interface represents a complete, immutable command path that can be executed within the Lamp framework. It acts as the primary abstraction for a command, encompassing its path, usage, description, and the underlying function that performs the action.

    Key characteristics include:

    • Hierarchy: Commands are structured as a sequence of CommandNode objects. You can navigate the command tree using firstNode(), lastNode(), childrenCommands(), or siblingCommands().
    • Execution: Commands can be tested for validity using test(actor, input) (returning a Potential) or executed directly via execute(actor, input) or execute(context).
    • Visibility: A command's visibility to an actor is determined by whether it is marked as a SecretCommand and whether the actor has the required permissions.
    • Metadata: It provides access to command metadata like path(), usage(), description(), and commandPriority().
    • Parameters: You can inspect the command's parameters (including required inputs and optional parameters) and access specific parameters by name using parameter(name) or parameterOrNull(name).
  6. Handle asynchronous command responses with CompletionStage

    v4

    Lamp supports asynchronous command execution by automatically handling return types that implement CompletionStage<T> (such as CompletableFuture<T>).

    When a command returns a CompletionStage, Lamp will wait for the stage to complete. If the stage completes successfully, the result is passed to the registered ResponseHandler for the underlying type T. If the stage completes exceptionally, the exception is passed to lamp.handleException using an ErrorContext tied to the executing function.

    // Example of a command returning a CompletableFuture
    @Command(name = "async")
    public CompletableFuture<String> asyncCommand() {
        return CompletableFuture.supplyAsync(() -> "Hello from the future!");
    }
  7. Apply command cooldowns using @Cooldown

    v4

    The @Cooldown annotation allows you to restrict how frequently a command can be executed. It can be applied in two distinct ways depending on the level of control you need:

    1. Automatic Cooldown (Simple)

    Apply @Cooldown directly to a class or a method.

    • Behavior: If the command executes successfully, the user is automatically placed on cooldown.
    • Error Handling: If a user attempts to execute the command while on cooldown, a revxrsal.commands.exception.CooldownException is thrown.

    2. Manual Cooldown (Flexible)

    Apply @Cooldown to a parameter of type CooldownHandle. This gives you manual control over when the cooldown starts and how to handle users currently on cooldown.

    • Behavior: The cooldown is not applied automatically. You must explicitly call cooldownHandle.cooldown() within your method logic to trigger the cooldown.
    • Capabilities: You can check if a user is on cooldown using isOnCooldown(), retrieve the remaining time via remainingTime(TimeUnit), or force a check using requireNotOnCooldown() (which throws a CooldownException).
    • Note: A CooldownHandle parameter can be used even without the @Cooldown annotation.
    // Example 1: Automatic Cooldown
    @Command("foo")
    @Cooldown(value = 10, unit = TimeUnit.SECONDS)
    public void foo() {
        // Command action here. User is automatically put on cooldown after success.
    }
    
    // Example 2: Manual Cooldown with custom logic
    @Command("foo")
    public void foo(@Cooldown(value = 10, unit = TimeUnit.SECONDS) CooldownHandle cooldownHandle) {
        if (cooldownHandle.isOnCooldown()) {
            long secondsLeft = cooldownHandle.remainingTime(TimeUnit.SECONDS);
            actor.reply("You must wait " + secondsLeft + " second(s) before using this command again!");
            return;
        }
        
        // ... command action here
    
        // Manually trigger the cooldown
        cooldownHandle.cooldown();
    }
  8. Use AnnotationList to manage annotations

    v4

    The AnnotationList interface represents an immutable collection of annotations. It provides a high-level API for querying, mapping, and transforming annotations while respecting Lamp's specialized annotation behaviors like @DistributeOnMethods and AnnotationReplacers.

    Key Capabilities

    • Creation: Create lists from AnnotatedElement (which accounts for distributed annotations), or from existing collections/maps (which do not).
    • Querying: Retrieve specific annotations by type using get(), contains(), or require().
    • Functional Mapping: Transform annotation data using map(), mapOr(), or mapOrGet().
    • Transformation: Create new lists by adding annotations via withAnnotations() or applying dynamic replacements via replaceAnnotations().
    // Creating a list from an element (respects @DistributeOnMethods)
    AnnotationList list = AnnotationList.create(someAnnotatedElement);
    
    // Querying an annotation
    MyAnnotation annotation = list.get(MyAnnotation.class);
    
    // Mapping an annotation value safely
    String value = list.mapOr(MyAnnotation.class, ann -> ann.value(), "default");
  9. Handle command errors using ErrorContext

    v4

    When a command fails in Lamp, an ErrorContext is provided to describe why and where the error occurred. ErrorContext is a generic interface parameterized by the CommandActor type (A).

    Common error types include:

    • ParsingLiteral: An error occurred while parsing a specific literal node.
    • ParsingParameter: An error occurred while parsing a parameter (e.g., invalid input for a type).
    • ExecutingFunction: An error occurred during the actual execution of the command's logic.
    • UnknownCommand: The user attempted to call a command that does not exist.
    • UnknownParameter: An unrecognized flag or switch was provided.

    You can use helper methods like isParsingLiteral(), isParsingParameter(), and isExecutingFunction() to determine the error type and then cast to the specific interface to access detailed information (like the problematic ParameterNode or LiteralNode).

  10. How to implement and register an OrphanCommand

    v4

    An OrphanCommand is used for commands that do not have a fixed parent command known at compile-time. Instead of being part of a static command tree, you define the command path at runtime using the Orphans utility.

    To use an OrphanCommand:

    1. Implement the OrphanCommand interface in your class.
    2. Use @Subcommand annotations on methods within that class to define subcommands.
    3. Register the command using lamp.register(Orphans.path("your_path").handler(new YourClass())).
    // 1. Implementation
    public class Foo implements OrphanCommand {
    
        @Subcommand("bar")
        public void bar(CommandActor actor) {
            actor.reply("Executed as 'foo bar'!");
        }
    }
    
    // 2. Registration
    Lamp lamp = ...;
    lamp.register(Orphans.path("foo").handler(new Foo()));