Lamp Command Framework
repository·v4·Indexed 43 days ago
https://github.com/revxrsal/lampA 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.
What's inside Lamp
- 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.
Use EntitySelector for dynamic entity arguments
v4The Bukkit implementation of Lamp includes a specialized argument factory for
EntitySelector. When you useEntitySelector<T>as a parameter type in your commands, the framework uses the generic typeTto determine which entity type to look for and checks for the@Singleannotation to decide if the command expects a single entity or a collection.Example usage in a command parameter:
EntitySelector<Player>with@Singlewill resolve to a singlePlayer.EntitySelector<Entity>without@Singlewill resolve to multipleEntityobjects.EntitySelector<LivingEntity>will resolve to the specific subclass ofEntityprovided in the generic type.
// Conceptual usage in a command definition public void myCommand(@Single EntitySelector<Player> player) { // ... }Use @FallbackPrefix to define Bukkit command aliases
v4When using
BukkitCommandHooks, you can control how a Lamp command is registered in Bukkit by using the@FallbackPrefixannotation 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 thedefaultFallbackPrefixprovided during the construction ofBukkitCommandHooks.Manage command cooldowns with CooldownHandle
v4The
CooldownHandleinterface 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
@Cooldownannotation (Static)If you want a fixed cooldown duration, annotate the
CooldownHandleparameter (not the method) with@Cooldown. This allows you to use shortcut methods that automatically reference the annotated values.Note: You must place the
@Cooldownannotation 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
@Cooldownannotation. Instead, pass the duration andTimeUnitdirectly to the methods, or usewithCooldownto 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 }Use RuntimeExceptionAdapter for flexible exception handling
v4For a more modular and powerful way to handle exceptions, instead of a single monolithicCommandExceptionHandler, consider usingRuntimeExceptionAdapter. This allows you to define smaller, specialized functions to handle different types of exceptions, making your error-handling logic cleaner and easier to maintain.Understand the ExecutableCommand interface
v4The
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
CommandNodeobjects. You can navigate the command tree usingfirstNode(),lastNode(),childrenCommands(), orsiblingCommands(). - Execution: Commands can be tested for validity using
test(actor, input)(returning aPotential) or executed directly viaexecute(actor, input)orexecute(context). - Visibility: A command's visibility to an actor is determined by whether it is marked as a
SecretCommandand whether the actor has the required permissions. - Metadata: It provides access to command metadata like
path(),usage(),description(), andcommandPriority(). - Parameters: You can inspect the command's parameters (including required inputs and optional parameters) and access specific parameters by name using
parameter(name)orparameterOrNull(name).
- Hierarchy: Commands are structured as a sequence of
Handle asynchronous command responses with CompletionStage
v4Lamp supports asynchronous command execution by automatically handling return types that implement
CompletionStage<T>(such asCompletableFuture<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 registeredResponseHandlerfor the underlying typeT. If the stage completes exceptionally, the exception is passed tolamp.handleExceptionusing anErrorContexttied 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!"); }Apply command cooldowns using @Cooldown
v4The
@Cooldownannotation 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
@Cooldowndirectly 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.CooldownExceptionis thrown.
2. Manual Cooldown (Flexible)
Apply
@Cooldownto a parameter of typeCooldownHandle. 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 viaremainingTime(TimeUnit), or force a check usingrequireNotOnCooldown()(which throws aCooldownException). - Note: A
CooldownHandleparameter can be used even without the@Cooldownannotation.
// 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(); }Use @ParseWith to specify a ParameterType
v4Lamp automatically resolves several common types (Lists, Sets, Arrays, Enums, Primitives, and Strings). For custom types or specific parsing logic, use the@ParseWithannotation on your command parameter to tell Lamp whichParameterTypeto use.Use AnnotationList to manage annotations
v4The
AnnotationListinterface 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@DistributeOnMethodsandAnnotationReplacers.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(), orrequire(). - Functional Mapping: Transform annotation data using
map(),mapOr(), ormapOrGet(). - Transformation: Create new lists by adding annotations via
withAnnotations()or applying dynamic replacements viareplaceAnnotations().
// 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");- Creation: Create lists from
Handle command errors using ErrorContext
v4When a command fails in Lamp, an
ErrorContextis provided to describe why and where the error occurred.ErrorContextis a generic interface parameterized by theCommandActortype (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(), andisExecutingFunction()to determine the error type and then cast to the specific interface to access detailed information (like the problematicParameterNodeorLiteralNode).How to implement and register an OrphanCommand
v4An
OrphanCommandis 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 theOrphansutility.To use an
OrphanCommand:- Implement the
OrphanCommandinterface in your class. - Use
@Subcommandannotations on methods within that class to define subcommands. - 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()));- Implement the