Compile-time annotation processing generates platform-specific command wrappers. No runtime reflection is used for command execution, and the core runtime has no external dependencies.
The processor is split into two independently usable halves:
@Command class
│ CommandParserFactory (parser/) reads annotations → immutable models
▼
CommandModel tree
│ CommandGeneratorFactory (generator/) models → JavaPoet → source file
▼
*$StandaloneCommand / *$BukkitCommand / *$PaperCommand
BaseCommandProcessor only wires the two halves: it parses annotated classes and passes each model to the
platform's generator. It contains no generation logic.
craftcommand/
├── annotations/ @Command, @Default, @Resolve, @Greedy, @Suggest, @Name (no dependencies)
├── runtime/ CommandManager, ArgumentResolver, CommandInfo (JDK only)
├── bukkit/
│ ├── annotations/ @Permission
│ ├── runtime/ BukkitCommandManager
│ └── processor/ BukkitCommandProcessor → BukkitCommandGenerator
├── paper/
│ ├── runtime/ PaperCommandManager, PaperSuggestions
│ └── processor/ PaperCommandProcessor → PaperCommandGenerator, PaperExecutionSource
├── standalone/
│ ├── runtime/ StandaloneCommandManager
│ └── processor/ StandaloneCommandProcessor → StandaloneCommandGenerator
├── validation/
│ ├── annotations/ @Min, @Max, @ValidateWith
│ └── processor/ MinHandler, MaxHandler, ValidateWithHandler (SPI)
├── processor/
│ ├── BaseCommandProcessor
│ ├── parser/ CommandParserFactory, CommandParser, ResolverLookup
│ ├── generator/ CommandGenerator(Factor), BaseCommandGenerator, Execution/SuggestionGenerator,
│ │ TypeSupport, SenderTypeRegistry, Naming, ...
│ ├── model/ CommandModel, MethodModel, ParameterModel
│ └── extension/ ParameterAnnotationHandler, MethodAnnotationHandler
└── docs/ This documentation
BaseCommandProcessor.processfinds classes annotated with@Command(nested command classes are handled by their enclosing tree).CommandParserFactory.parseproduces aCommandModel. This is the only phase that reads the core command annotations; it links each parameter's@Resolve/@Suggesttargets, including on nested resolver parameters.CommandGenerator.generatewrites one wrapper class per root command. Generators consume models; the only remaining annotation reads are SPI handler dispatch (third-party annotations) and platform annotations such as@Permission. Elements are still used for types and names.- Platform processor registration is generated by AutoService (
@AutoService(Processor.class)), so a platform processor only declares its supported annotation and its generator factory.
Core and platform annotations are read through the standard Element#getAnnotation(Class) API, so no prism
generation step or extra annotation-processing dependency is required.
BaseCommandGenerator owns the shared six-step template. A platform generator overrides the hooks it needs:
| Step | Hook | Purpose |
|---|---|---|
| 1 | configureClass |
Superclass / interfaces / extra fields |
| 2 | addConstructorStatements |
Constructor setup |
| 3 | generateEntryMethods |
execute / tabComplete / getCommandNode |
| 4 | generateHelpers |
Routing, suggestions, sender casts, platform helpers (overridable) |
| 5 | generatePlatformHelpers |
Platform helper methods, called from step 4 |
| 6 | (inherited) | Command info, routing, parameter resolution |
onBeforeExecute and getSenderExpression are optional hooks used by the Bukkit/Paper generators.
Generated wrappers never send text directly. They call CommandManager.sendMessage, which formats the key/fallback
and delegates delivery to the platform's sendMessage implementation; handleException uses the same path for caught
exceptions. Override formatMessage to translate keys.
| Platform | Delivery |
|---|---|
| Standalone | System.out.println |
| Bukkit | CommandSender#sendMessage |
| Paper | Adventure Component |
Keys include permission, unknown-subcommand, usage, missing-argument, validation.*, and the literals passed
to @Permission(message = ...) (or their i18n: suffix).
manager.register(new MyCommand())
→ Class.forName(wrapper name, commandClass.getClassLoader())
→ MethodHandle constructor lookup
→ Platform registration (CommandMap / LifecycleEvents / HashMap)
The wrapper is resolved with the command class's own class loader, so addon classes and their generated wrappers stay
in the same class-loader scope. The runtime modules depend on the JDK only. bukkit-runtime and paper-runtime also need the server API, which
is provided; there are no third-party runtime libraries.
| Interface | Module | Purpose |
|---|---|---|
ParameterAnnotationHandler |
processor SPI | Custom parameter annotations |
MethodAnnotationHandler |
processor SPI | Custom method annotations |
ArgumentResolver |
runtime | Custom type resolution & suggestions |
CommandGeneratorFactory |
processor | New platform generator |
| Library | Used for | Decision |
|---|---|---|
| Palantir JavaPoet 0.22 | Code generation | Kept: maintained fork, source-model output. Requires JDK 17 |
Brigadier (via paper-api) |
Paper command tree and suggestions | Kept: Paper's native API, no replacement |
| AutoService 1.1.1 | META-INF/services processor entries |
Kept: removes three hand-written service files |
| Avaje Prisms | Annotation access | Removed: standard Element#getAnnotation is less build setup |
| JUnit, Mockito, MockBukkit | Tests only | Kept: no production footprint |
No annotation-processing library is required beyond JavaPoet and AutoService. The generated wrappers use the JDK only.
- Parse once, generate from models — core annotations are read in
parser/only - Generate, not reflect — wrappers are plain Java
- Classloader-aware wrapper lookup — addon class loaders work; wrapper construction uses a MethodHandle
- Java 8 generated code, JDK 17 processor — generated wrappers avoid records and var; the processor modules compile at Java 17 because Palantir JavaPoet 0.22 is compiled for Java 17
- Minimal generated code — named locals, no unused helpers, no generated narration
- SPI for extensions — third-party annotations and suggestions
- Standard annotation reads —
Element#getAnnotation(Class)for core and platform annotations; no generated prism classes or prism processor dependency