Skip to content

Commit 6ee9e30

Browse files
authored
feat: add processor give commands and some fixs (#55)
1 parent 55e8710 commit 6ee9e30

25 files changed

Lines changed: 528 additions & 363 deletions

File tree

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,43 @@ dependencies {
8080
8181
---
8282

83+
## 🏷️ Annotations Addon
84+
85+
The `annotations-addon` module provides annotation-based command registration. When using `@Arg` implicitly (without annotation), parameter names are used as argument names. This requires the `-parameters` compiler flag.
86+
87+
### Maven
88+
89+
```xml
90+
<plugin>
91+
<groupId>org.apache.maven.plugins</groupId>
92+
<artifactId>maven-compiler-plugin</artifactId>
93+
<version>3.13.0</version>
94+
<configuration>
95+
<parameters>true</parameters>
96+
</configuration>
97+
</plugin>
98+
```
99+
100+
### Gradle (Groovy)
101+
102+
```groovy
103+
tasks.withType(JavaCompile).configureEach {
104+
options.compilerArgs.add('-parameters')
105+
}
106+
```
107+
108+
### Gradle (Kotlin DSL)
109+
110+
```kotlin
111+
tasks.withType<JavaCompile>().configureEach {
112+
options.compilerArgs.add("-parameters")
113+
}
114+
```
115+
116+
> 💡 Without this flag, parameter names default to `arg0`, `arg1`, etc. You can always use `@Arg("name")` explicitly to avoid this requirement.
117+
118+
---
119+
83120
## 💡 Example (Spigot)
84121

85122
Be sure to extends all the classes from the platform you are using (Spigot, Velocity, etc.):

annotations-addon/src/main/java/fr/traqueur/commands/annotations/AnnotationCommandProcessor.java

Lines changed: 125 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import fr.traqueur.commands.api.models.Command;
77
import fr.traqueur.commands.api.models.CommandBuilder;
88
import fr.traqueur.commands.api.resolver.SenderResolver;
9+
import fr.traqueur.commands.api.utils.Patterns;
910

1011
import java.lang.reflect.Method;
1112
import java.lang.reflect.Parameter;
@@ -31,30 +32,47 @@ public AnnotationCommandProcessor(CommandManager<T, S> manager) {
3132
this.senderResolver = manager.getPlatform().getSenderResolver();
3233
}
3334

34-
public void register(Object... handlers) {
35+
public List<Command<T, S>> register(Object... handlers) {
36+
List<Command<T, S>> allCommands = new ArrayList<>();
3537
for (Object handler : handlers) {
36-
processHandler(handler);
38+
allCommands.addAll(processHandler(handler));
3739
}
40+
return allCommands;
3841
}
3942

40-
private void processHandler(Object handler) {
43+
private List<Command<T, S>> processHandler(Object handler) {
4144
Class<?> clazz = handler.getClass();
45+
validateCommandContainer(clazz);
4246

47+
collectTabCompleters(handler, clazz);
48+
49+
List<CommandMethodInfo> commandMethods = collectCommandMethods(handler, clazz);
50+
Set<String> allPaths = extractAllPaths(commandMethods);
51+
52+
Map<String, Command<T, S>> builtCommands = buildAllCommands(commandMethods, allPaths);
53+
Set<String> rootCommands = organizeHierarchy(commandMethods, allPaths, builtCommands);
54+
55+
return registerRootCommands(rootCommands, builtCommands);
56+
}
57+
58+
private void validateCommandContainer(Class<?> clazz) {
4359
if (!clazz.isAnnotationPresent(CommandContainer.class)) {
4460
throw new IllegalArgumentException(
4561
"Class must be annotated with @CommandContainer: " + clazz.getName()
4662
);
4763
}
64+
}
4865

49-
// First pass: collect all @TabComplete methods
66+
private void collectTabCompleters(Object handler, Class<?> clazz) {
5067
tabCompleters.clear();
5168
for (Method method : clazz.getDeclaredMethods()) {
5269
if (method.isAnnotationPresent(TabComplete.class)) {
5370
processTabCompleter(handler, method);
5471
}
5572
}
73+
}
5674

57-
// Second pass: collect all @Command methods and sort by depth
75+
private List<CommandMethodInfo> collectCommandMethods(Object handler, Class<?> clazz) {
5876
List<CommandMethodInfo> commandMethods = new ArrayList<>();
5977
for (Method method : clazz.getDeclaredMethods()) {
6078
if (method.isAnnotationPresent(fr.traqueur.commands.annotations.Command.class)) {
@@ -63,46 +81,51 @@ private void processHandler(Object handler) {
6381
commandMethods.add(new CommandMethodInfo(handler, method, annotation.name()));
6482
}
6583
}
84+
commandMethods.sort(Comparator.comparingInt(info -> Patterns.DOT.split(info.name).length));
85+
return commandMethods;
86+
}
6687

67-
// Sort by depth (parents first)
68-
commandMethods.sort(Comparator.comparingInt(info -> info.name.split("\\.").length));
69-
70-
// Collect all command paths to determine which have parents defined
88+
private Set<String> extractAllPaths(List<CommandMethodInfo> commandMethods) {
7189
Set<String> allPaths = new HashSet<>();
7290
for (CommandMethodInfo info : commandMethods) {
7391
allPaths.add(info.name);
7492
}
93+
return allPaths;
94+
}
7595

76-
// Third pass: build ALL commands first
96+
private Map<String, Command<T, S>> buildAllCommands(List<CommandMethodInfo> commandMethods, Set<String> allPaths) {
7797
Map<String, Command<T, S>> builtCommands = new LinkedHashMap<>();
78-
Set<String> rootCommands = new LinkedHashSet<>();
79-
8098
for (CommandMethodInfo info : commandMethods) {
8199
String parentPath = getParentPath(info.name);
82100
boolean hasParentInBatch = parentPath != null && allPaths.contains(parentPath);
83-
84101
Command<T, S> command = buildCommand(info.handler, info.method, info.name, hasParentInBatch);
85102
builtCommands.put(info.name, command);
86103
}
104+
return builtCommands;
105+
}
87106

88-
// Fourth pass: organize hierarchy (add subcommands to parents)
107+
private Set<String> organizeHierarchy(List<CommandMethodInfo> commandMethods, Set<String> allPaths,
108+
Map<String, Command<T, S>> builtCommands) {
109+
Set<String> rootCommands = new LinkedHashSet<>();
89110
for (CommandMethodInfo info : commandMethods) {
90111
String parentPath = getParentPath(info.name);
91-
92112
if (parentPath != null && allPaths.contains(parentPath)) {
93-
Command<T, S> parent = builtCommands.get(parentPath);
94-
Command<T, S> child = builtCommands.get(info.name);
95-
parent.addSubCommand(child);
113+
builtCommands.get(parentPath).addSubCommand(builtCommands.get(info.name));
96114
} else {
97115
rootCommands.add(info.name);
98116
}
99117
}
118+
return rootCommands;
119+
}
100120

101-
// Fifth pass: register only root commands
121+
private List<Command<T, S>> registerRootCommands(Set<String> rootCommands, Map<String, Command<T, S>> builtCommands) {
122+
List<Command<T, S>> registeredCommands = new ArrayList<>();
102123
for (String rootPath : rootCommands) {
103-
Command<T, S> rootCommand = builtCommands.get(rootPath);
104-
manager.registerCommand(rootCommand);
124+
Command<T, S> command = builtCommands.get(rootPath);
125+
manager.registerCommand(command);
126+
registeredCommands.add(command);
105127
}
128+
return registeredCommands;
106129
}
107130

108131
private String getParentPath(String path) {
@@ -167,67 +190,48 @@ private void processTabCompleter(Object handler, Method method) {
167190

168191
private void processParameters(CommandBuilder<T, S> builder, Method method, String commandPath) {
169192
Parameter[] params = method.getParameters();
170-
Type[] genericTypes = method.getGenericParameterTypes();
171193

172194
for (int i = 0; i < params.length; i++) {
173195
Parameter param = params[i];
174-
Class<?> paramType = param.getType();
175-
176-
// First parameter is sender (skip it for args)
177-
if (i == 0) {
178-
Class<?> senderType = paramType;
179-
if (paramType == Optional.class) {
180-
senderType = extractOptionalType(param);
181-
}
182-
if (senderResolver.canResolve(senderType)) {
183-
continue;
184-
}
185-
}
186196

187-
// Must have @Arg annotation
188-
Arg argAnnotation = param.getAnnotation(Arg.class);
189-
if (argAnnotation == null) {
190-
throw new IllegalArgumentException(
191-
"Parameter '" + param.getName() + "' in method '" + method.getName() +
192-
"' must be annotated with @Arg or be the sender type"
193-
);
197+
if (i == 0 && isSenderParameter(param)) {
198+
continue;
194199
}
195200

196-
String argName = argAnnotation.value();
197-
boolean isOptional = paramType == Optional.class;
198-
boolean isInfinite = param.isAnnotationPresent(Infinite.class);
201+
registerArgument(builder, param, commandPath);
202+
}
203+
}
199204

200-
// Determine the actual argument type
201-
Class<?> argType;
202-
if (isOptional) {
203-
argType = extractOptionalType(param);
204-
} else {
205-
argType = paramType;
206-
}
205+
private boolean isSenderParameter(Parameter param) {
206+
Class<?> paramType = param.getType();
207+
Class<?> senderType = (paramType == Optional.class) ? extractOptionalType(param) : paramType;
208+
return senderResolver.canResolve(senderType);
209+
}
207210

208-
// If @Infinite, use Infinite.class as the type
209-
if (isInfinite) {
210-
argType = fr.traqueur.commands.api.arguments.Infinite.class;
211-
}
211+
private void registerArgument(CommandBuilder<T, S> builder, Parameter param, String commandPath) {
212+
String argName = getArgumentName(param);
213+
Class<?> argType = resolveArgumentType(param);
214+
boolean isOptional = param.getType() == Optional.class;
215+
TabCompleter<S> completer = getTabCompleter(commandPath, argName);
212216

213-
// Get tab completer if exists
214-
TabCompleter<S> completer = getTabCompleter(commandPath, argName);
217+
if (isOptional) {
218+
builder.optionalArg(argName, argType, completer);
219+
} else {
220+
builder.arg(argName, argType, completer);
221+
}
222+
}
215223

216-
// Add argument to builder
217-
if (isOptional) {
218-
if (completer != null) {
219-
builder.optionalArg(argName, argType, completer);
220-
} else {
221-
builder.optionalArg(argName, argType);
222-
}
223-
} else {
224-
if (completer != null) {
225-
builder.arg(argName, argType, completer);
226-
} else {
227-
builder.arg(argName, argType);
228-
}
229-
}
224+
private String getArgumentName(Parameter param) {
225+
Arg argAnnotation = param.getAnnotation(Arg.class);
226+
return (argAnnotation != null) ? argAnnotation.value() : param.getName();
227+
}
228+
229+
private Class<?> resolveArgumentType(Parameter param) {
230+
if (param.isAnnotationPresent(Infinite.class)) {
231+
return fr.traqueur.commands.api.arguments.Infinite.class;
230232
}
233+
Class<?> paramType = param.getType();
234+
return (paramType == Optional.class) ? extractOptionalType(param) : paramType;
231235
}
232236

233237
/**
@@ -257,67 +261,71 @@ private TabCompleter<S> getTabCompleter(String commandPath, String argName) {
257261

258262
return (sender, args) -> {
259263
try {
260-
Object result;
261-
Parameter[] params = tcMethod.method.getParameters();
262-
263-
if (params.length == 0) {
264-
result = tcMethod.method.invoke(tcMethod.handler);
265-
} else if (params.length == 1) {
266-
Object resolvedSender = senderResolver.resolve(sender, params[0].getType());
267-
result = tcMethod.method.invoke(tcMethod.handler, resolvedSender);
268-
} else {
269-
Object resolvedSender = senderResolver.resolve(sender, params[0].getType());
270-
String current = !args.isEmpty() ? args.getLast() : "";
271-
result = tcMethod.method.invoke(tcMethod.handler, resolvedSender, current);
272-
}
273-
264+
Object result = invokeTabCompleter(tcMethod, sender, args);
274265
return (List<String>) result;
275266
} catch (Exception e) {
276-
throw new RuntimeException("Failed to invoke tab completer", e);
267+
throw new RuntimeException(
268+
"Failed to invoke tab completer for command '" + commandPath +
269+
"', argument '" + argName + "', method '" + tcMethod.method.getName() + "'", e);
277270
}
278271
};
279272
}
280273

274+
private Object invokeTabCompleter(TabCompleterMethod tcMethod, S sender, List<String> args) throws Exception {
275+
Parameter[] params = tcMethod.method.getParameters();
276+
277+
if (params.length == 0) {
278+
return tcMethod.method.invoke(tcMethod.handler);
279+
}
280+
281+
Object resolvedSender = senderResolver.resolve(sender, params[0].getType());
282+
if (params.length == 1) {
283+
return tcMethod.method.invoke(tcMethod.handler, resolvedSender);
284+
}
285+
286+
String current = !args.isEmpty() ? args.getLast() : "";
287+
return tcMethod.method.invoke(tcMethod.handler, resolvedSender, current);
288+
}
289+
281290
private void invokeMethod(Object handler, Method method, S sender, Arguments args) {
282291
try {
283292
Parameter[] params = method.getParameters();
284-
Object[] invokeArgs = new Object[params.length];
285-
286-
for (int i = 0; i < params.length; i++) {
287-
Parameter param = params[i];
288-
Class<?> paramType = param.getType();
289-
boolean isOptional = paramType == Optional.class;
290-
291-
// First param: sender
292-
if (i == 0) {
293-
Class<?> senderType = isOptional ? extractOptionalType(param) : paramType;
294-
if (senderResolver.canResolve(senderType)) {
295-
Object resolved = senderResolver.resolve(sender, senderType);
296-
invokeArgs[i] = isOptional ? Optional.ofNullable(resolved) : resolved;
297-
continue;
298-
}
299-
}
300-
301-
// Other params: @Arg
302-
Arg argAnnotation = param.getAnnotation(Arg.class);
303-
if (argAnnotation != null) {
304-
String argName = argAnnotation.value();
305-
306-
if (isOptional) {
307-
invokeArgs[i] = args.getOptional(argName);
308-
} else {
309-
invokeArgs[i] = args.get(argName);
310-
}
311-
}
312-
}
313-
293+
Object[] invokeArgs = buildInvokeArgs(params, sender, args);
314294
method.invoke(handler, invokeArgs);
315-
316295
} catch (Exception e) {
317296
throw new RuntimeException("Failed to invoke command method: " + method.getName(), e);
318297
}
319298
}
320299

300+
private Object[] buildInvokeArgs(Parameter[] params, S sender, Arguments args) {
301+
Object[] invokeArgs = new Object[params.length];
302+
303+
for (int i = 0; i < params.length; i++) {
304+
Parameter param = params[i];
305+
306+
if (i == 0 && isSenderParameter(param)) {
307+
invokeArgs[i] = resolveSender(param, sender);
308+
} else {
309+
invokeArgs[i] = resolveArgument(param, args);
310+
}
311+
}
312+
return invokeArgs;
313+
}
314+
315+
private Object resolveSender(Parameter param, S sender) {
316+
Class<?> paramType = param.getType();
317+
boolean isOptional = paramType == Optional.class;
318+
Class<?> senderType = isOptional ? extractOptionalType(param) : paramType;
319+
Object resolved = senderResolver.resolve(sender, senderType);
320+
return isOptional ? Optional.ofNullable(resolved) : resolved;
321+
}
322+
323+
private Object resolveArgument(Parameter param, Arguments args) {
324+
String argName = getArgumentName(param);
325+
boolean isOptional = param.getType() == Optional.class;
326+
return isOptional ? args.getOptional(argName) : args.get(argName);
327+
}
328+
321329
private record CommandMethodInfo(Object handler, Method method, String name) {}
322330
private record TabCompleterMethod(Object handler, Method method) {}
323331
}

0 commit comments

Comments
 (0)