annotationClass, @Nullable Method method, Object parent) {
+ A ann = method == null ? null : method.getAnnotation(annotationClass);
+ if (ann == null) {
+ if (parent instanceof Class>) {
+ ann = ((Class>) parent).getAnnotation(annotationClass);
+ } else {
+ ann = parent.getClass().getAnnotation(annotationClass);
+ }
+ }
+ return ann;
+ }
+
+}
\ No newline at end of file
diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/CommandContext.java b/common/src/main/java/com/georgev22/skinoverlay/command/CommandContext.java
new file mode 100644
index 00000000..da9ec2e9
--- /dev/null
+++ b/common/src/main/java/com/georgev22/skinoverlay/command/CommandContext.java
@@ -0,0 +1,26 @@
+package com.georgev22.skinoverlay.command;
+
+import com.georgev22.skinoverlay.utilities.CustomData;
+
+/**
+ * Represents the context of a command execution.
+ *
+ * Provides a place to store custom data related to the current command execution,
+ * which can be used by preprocessors, postprocessors, or the command itself.
+ */
+public class CommandContext {
+
+ /**
+ * Stores custom data associated with this command execution.
+ */
+ private final CustomData customData = new CustomData();
+
+ /**
+ * Returns the custom data container for this command execution.
+ *
+ * @return the {@link CustomData} object
+ */
+ public CustomData getData() {
+ return customData;
+ }
+}
diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/CommandIssuer.java b/common/src/main/java/com/georgev22/skinoverlay/command/CommandIssuer.java
new file mode 100644
index 00000000..0a0114ef
--- /dev/null
+++ b/common/src/main/java/com/georgev22/skinoverlay/command/CommandIssuer.java
@@ -0,0 +1,96 @@
+package com.georgev22.skinoverlay.command;
+
+import net.kyori.adventure.text.Component;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+/**
+ * Represents an entity that can issue commands.
+ *
+ * Can be a player, console, or any custom command sender implementation.
+ */
+public interface CommandIssuer {
+
+ /**
+ * Checks if the issuer is a player.
+ *
+ * @return true if the issuer is a player, false otherwise
+ */
+ boolean isPlayer();
+
+ /**
+ * Sends a plain text message to the issuer.
+ *
+ * @param message the message to send
+ */
+ void sendMessage(@NotNull String message);
+
+ /**
+ * Sends multiple plain text messages to the issuer.
+ *
+ * @param messages the messages to send
+ */
+ default void sendMessage(String @NotNull ... messages) {
+ for (String message : messages) {
+ sendMessage(message);
+ }
+ }
+
+ /**
+ * Sends a rich text component to the issuer.
+ *
+ * @param component the component to send
+ */
+ void sendMessage(@NotNull Component component);
+
+ /**
+ * Sends multiple rich text components to the issuer.
+ *
+ * @param components the components to send
+ */
+ default void sendMessage(Component @NotNull ... components) {
+ for (Component component : components) {
+ sendMessage(component);
+ }
+ }
+
+ /**
+ * Returns the underlying issuer object.
+ *
+ * For example, a Bukkit {@code CommandSender} instance.
+ *
+ * @param the type of the underlying issuer
+ * @return the underlying issuer object
+ */
+ @NotNull T getIssuer();
+
+ /**
+ * Checks if the issuer has a specific permission.
+ *
+ * @param permission the permission string
+ * @return true if the issuer has the permission, false otherwise
+ */
+ boolean hasPermission(String permission);
+
+ /**
+ * Checks if the issuer is an operator.
+ *
+ * @return true if the issuer is an op, false otherwise
+ */
+ boolean isOp();
+
+ /**
+ * Gets the unique identifier (UUID) of the issuer.
+ *
+ * @return the UUID of the issuer
+ */
+ UUID getUniqueId();
+
+ /**
+ * Gets the display name of the issuer.
+ *
+ * @return the name of the issuer
+ */
+ String getName();
+}
diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/CommandManager.java b/common/src/main/java/com/georgev22/skinoverlay/command/CommandManager.java
new file mode 100644
index 00000000..e432ea0f
--- /dev/null
+++ b/common/src/main/java/com/georgev22/skinoverlay/command/CommandManager.java
@@ -0,0 +1,99 @@
+package com.georgev22.skinoverlay.command;
+
+import com.georgev22.skinoverlay.SkinOverlay;
+import com.georgev22.skinoverlay.command.annotation.CommandAlias;
+import com.georgev22.skinoverlay.command.annotation.Subcommand;
+import com.georgev22.skinoverlay.command.processors.PostProcessor;
+import com.georgev22.skinoverlay.command.processors.PreProcessor;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+
+/**
+ * Central manager for registering and handling commands in the Core plugin.
+ *
+ * Supports registration of BaseCommand instances, nested subcommands, global preprocessors
+ * and postprocessors, and argument resolvers for tab completion and value parsing.
+ */
+public abstract class CommandManager {
+
+ private final List globalPreprocessors = new ArrayList<>();
+ private final List globalPostprocessors = new ArrayList<>();
+ private final SkinOverlay plugin = SkinOverlay.getInstance();
+
+ protected CommandManager() {
+ // private constructor for singleton pattern
+ }
+
+ /**
+ * Registers a {@link BaseCommand} and all its nested subcommands.
+ *
+ * @param command the command to register
+ */
+ public void registerCommand(@NotNull BaseCommand command) {
+ try {
+ registerCommand0(command);
+
+ for (Class> innerClass : command.getClass().getDeclaredClasses()) {
+ if (!BaseCommand.class.isAssignableFrom(innerClass)) continue;
+ if (!innerClass.isAnnotationPresent(Subcommand.class)) continue;
+
+ BaseCommand subcommand = (BaseCommand) innerClass.getDeclaredConstructor().newInstance();
+ command.addSubcommand(subcommand);
+
+ if (innerClass.isAnnotationPresent(CommandAlias.class)) {
+ registerCommand0(subcommand);
+ }
+
+ registerCommand(subcommand);
+ }
+ } catch (Exception e) {
+ plugin.getLogger().log(Level.SEVERE, "Failed to register command: " + command.getClass().getName(), e);
+ }
+ }
+
+ /**
+ * Registers a single command.
+ *
+ * @param command the command to register
+ */
+ protected abstract void registerCommand0(@NotNull BaseCommand command);
+
+ /**
+ * Adds a global preprocessor that runs before any command execution.
+ *
+ * @param preprocessor the preprocessor to add
+ */
+ public void addGlobalPreprocessor(PreProcessor preprocessor) {
+ globalPreprocessors.add(preprocessor);
+ }
+
+ /**
+ * Adds a global postprocessor that runs after any command execution.
+ *
+ * @param postprocessor the postprocessor to add
+ */
+ public void addGlobalPostprocessor(PostProcessor postprocessor) {
+ globalPostprocessors.add(postprocessor);
+ }
+
+ /**
+ * Returns a list of all global postprocessors.
+ *
+ * @return the global postprocessors
+ */
+ public List getGlobalPostprocessors() {
+ return globalPostprocessors;
+ }
+
+ /**
+ * Returns a list of all global preprocessors.
+ *
+ * @return the global preprocessors
+ */
+ public List getGlobalPreprocessors() {
+ return globalPreprocessors;
+ }
+}
diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/CompletionEngine.java b/common/src/main/java/com/georgev22/skinoverlay/command/CompletionEngine.java
new file mode 100644
index 00000000..99f99852
--- /dev/null
+++ b/common/src/main/java/com/georgev22/skinoverlay/command/CompletionEngine.java
@@ -0,0 +1,212 @@
+package com.georgev22.skinoverlay.command;
+
+import com.georgev22.skinoverlay.command.annotation.Argument;
+import com.georgev22.skinoverlay.command.annotation.CommandCompletion;
+import com.georgev22.skinoverlay.command.resolvers.ArgumentResolver;
+import com.georgev22.skinoverlay.command.resolvers.PlayersResolver;
+import com.georgev22.skinoverlay.command.resolvers.RangeResolver;
+import com.georgev22.skinoverlay.maps.ConcurrentObjectMap;
+import com.georgev22.skinoverlay.maps.ObjectMap;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.Unmodifiable;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.util.*;
+
+/**
+ * The {@code CompletionEngine} class provides a unified tab-completion system
+ * for the command framework.
+ *
+ * It supports both static completions (literal strings) and dynamic completions
+ * via registered {@link ArgumentResolver}s. Command classes can declare their
+ * completion pattern using the {@link CommandCompletion} annotation, while
+ * custom resolvers can be registered globally.
+ *
+ * Default resolvers include:
+ *
+ * {@code @players} — provides online player names.
+ * {@code @range:min-max} — provides numeric ranges.
+ *
+ *
+ * @see ArgumentResolver
+ * @see CommandCompletion
+ */
+public class CompletionEngine {
+
+ /**
+ * Holds all registered argument resolvers, mapped by lowercase key.
+ */
+ private static final ObjectMap resolvers = new ConcurrentObjectMap<>();
+
+ static {
+ registerDefaultResolvers();
+ }
+
+ /**
+ * Resolves possible tab completions for a command class and current user input.
+ *
+ * @param clazz the command class annotated with {@link CommandCompletion}
+ * @param commandIssuer the command issuer executing the command
+ * @param args the current arguments typed by the user
+ * @return a collection of completions filtered and sorted to match the current input;
+ * returns an empty collection if no matches are found or the class lacks {@link CommandCompletion}
+ */
+ public static @NotNull Collection resolveCompletions(
+ @NotNull Class> clazz,
+ CommandIssuer commandIssuer,
+ String[] args
+ ) {
+ if (!clazz.isAnnotationPresent(CommandCompletion.class)) {
+ return new ArrayList<>();
+ }
+
+ String pattern = clazz.getAnnotation(CommandCompletion.class).value();
+ String[] parts = pattern.split(" ");
+
+ if (args.length > parts.length) {
+ return new ArrayList<>();
+ }
+
+ String segment = parts[args.length - 1];
+ return getStrings(commandIssuer, args, segment);
+ }
+
+ /**
+ * Resolves possible tab completions for a method's arguments.
+ *
+ * This method checks parameters annotated with {@link Argument} and uses
+ * the {@code completion} property to provide tab completions.
+ *
+ * @param method the method whose parameters are being completed
+ * @param commandIssuer the command issuer executing the command
+ * @param args the current arguments typed by the user
+ * @return a collection of completions filtered and sorted to match the current input;
+ * returns an empty collection if no matches are found or if no completion is defined
+ */
+ public static @NotNull Collection resolveCompletions(
+ @NotNull Method method,
+ CommandIssuer commandIssuer,
+ String @NotNull [] args
+ ) {
+ Parameter[] parameters = Arrays.stream(method.getParameters())
+ .filter(p -> p.isAnnotationPresent(Argument.class))
+ .toArray(Parameter[]::new);
+
+ if (args.length > parameters.length) {
+ return List.of();
+ }
+
+ Parameter param = parameters[args.length - 1];
+ Argument argAnno = param.getAnnotation(Argument.class);
+ if (argAnno == null) {
+ return List.of();
+ }
+
+ String completion = argAnno.completion();
+ if (completion.isEmpty()) {
+ return List.of();
+ }
+
+ return getStrings(commandIssuer, args, completion);
+ }
+
+ /**
+ * Resolves an {@link ArgumentResolver} by its registered key.
+ *
+ * @param key the resolver key to look up — must start with {@code '@'} (case-insensitive)
+ * @return the corresponding {@link ArgumentResolver}, or {@code null} if none is found
+ */
+ public static @Nullable ArgumentResolver resolve(@NotNull String key) {
+ if (!key.startsWith("@")) {
+ return null;
+ }
+
+ String baseKey = key.toLowerCase(Locale.ROOT);
+ int colonIndex = baseKey.indexOf(':');
+ if (colonIndex != -1) {
+ baseKey = baseKey.substring(0, colonIndex);
+ }
+
+ return resolvers.get(baseKey);
+ }
+
+ /**
+ * Registers a new {@link ArgumentResolver} for the given key.
+ *
+ * @param key the resolver key to register — must start with {@code '@'}
+ * @param resolver the {@link ArgumentResolver} instance to associate with the key
+ * @throws IllegalArgumentException if {@code key} does not start with {@code '@'}
+ * @throws NullPointerException if {@code resolver} is {@code null}
+ */
+ public static void registerResolver(@NotNull String key, @NotNull ArgumentResolver resolver) {
+ if (!key.startsWith("@")) {
+ throw new IllegalArgumentException("Resolver key must start with '@' to avoid colliding with subcommands: " + key);
+ }
+ resolvers.put(key.toLowerCase(Locale.ROOT), Objects.requireNonNull(resolver, "resolver"));
+ }
+
+ /**
+ * Unregisters an existing {@link ArgumentResolver} by key.
+ *
+ * @param key the resolver key to remove — must start with {@code '@'}
+ */
+ public static void unregisterResolver(@NotNull String key) {
+ resolvers.remove(key.toLowerCase(Locale.ROOT));
+ }
+
+ /**
+ * Registers the built-in default argument resolvers.
+ *
+ * Currently includes:
+ *
+ * {@code @players} — resolves online player names.
+ * {@code @range} — resolves integer ranges (e.g., 1–10).
+ *
+ */
+ public static void registerDefaultResolvers() {
+ registerResolver("@players", new PlayersResolver());
+ registerResolver("@range", new RangeResolver());
+ }
+
+ /**
+ * Processes a completion segment and returns matching suggestions.
+ *
+ * The segment may contain literal options separated by {@code |} and/or
+ * dynamic resolver keys prefixed with {@code @}. If a resolver key contains
+ * a colon, the part after the colon is passed as an argument to the resolver.
+ *
+ * Matches are filtered based on the current input (last element of {@code args})
+ * and sorted with exact matches first, then by the index of the input in the option.
+ *
+ * @param commandIssuer the issuer of the command
+ * @param args the current arguments typed by the user
+ * @param segment the completion segment (literal options and/or resolver keys)
+ * @return an unmodifiable collection of matching completion strings
+ */
+ private static @NotNull @Unmodifiable Collection getStrings(CommandIssuer commandIssuer, String[] args, @NotNull String segment) {
+ Collection completions = new ArrayList<>();
+
+ for (String option : segment.split("\\|")) {
+ if (option.startsWith("@")) {
+ String[] resolverParts = option.split(":", 2);
+ String resolverName = resolverParts[0];
+ String[] resolverArgs = resolverParts.length > 1 ? new String[]{resolverParts[1]} : new String[0];
+
+ ArgumentResolver resolver = resolve(resolverName);
+ if (resolver != null) {
+ completions.addAll(resolver.resolve(commandIssuer, resolverArgs));
+ }
+ } else {
+ completions.add(option);
+ }
+ }
+
+ String currentInput = args[args.length - 1];
+ return completions.stream()
+ .filter(s -> s.toLowerCase().startsWith(currentInput.toLowerCase()))
+ .sorted(Comparator.comparingInt(s -> s.equalsIgnoreCase(currentInput) ? 0 : s.toLowerCase().indexOf(currentInput.toLowerCase())))
+ .toList();
+ }
+}
diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/ExecutionType.java b/common/src/main/java/com/georgev22/skinoverlay/command/ExecutionType.java
new file mode 100644
index 00000000..0ce6b160
--- /dev/null
+++ b/common/src/main/java/com/georgev22/skinoverlay/command/ExecutionType.java
@@ -0,0 +1,20 @@
+package com.georgev22.skinoverlay.command;
+
+/**
+ * Represents how a command should be executed.
+ *
+ * {@link #SYNC} — the command runs on the main server thread immediately.
+ * {@link #ASYNC} — the command runs asynchronously, off the main server thread.
+ *
+ */
+public enum ExecutionType {
+ /**
+ * Execute the command on the main server thread (synchronously).
+ */
+ SYNC,
+
+ /**
+ * Execute the command asynchronously on a separate thread.
+ */
+ ASYNC
+}
diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/MethodCommand.java b/common/src/main/java/com/georgev22/skinoverlay/command/MethodCommand.java
new file mode 100644
index 00000000..a411e484
--- /dev/null
+++ b/common/src/main/java/com/georgev22/skinoverlay/command/MethodCommand.java
@@ -0,0 +1,164 @@
+package com.georgev22.skinoverlay.command;
+
+import com.georgev22.skinoverlay.SkinOverlay;
+import com.georgev22.skinoverlay.command.annotation.*;
+import com.georgev22.skinoverlay.maps.HashObjectMap;
+import com.georgev22.skinoverlay.message.messages.CommandMessages;
+import com.georgev22.skinoverlay.player.SPlayer;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.logging.Level;
+
+public class MethodCommand extends BaseCommand {
+
+ private final BaseCommand parent;
+ protected final Method method;
+ private final boolean isDefault;
+
+ public MethodCommand(@NotNull BaseCommand parent, @NotNull Method method) {
+ this.parent = parent;
+ this.method = method;
+ this.isDefault = method.isAnnotationPresent(Default.class);
+
+ this.subCommandAliases = Optional.ofNullable(getAnnotation(Subcommand.class, method, parent))
+ .map(Subcommand::value)
+ .orElse(new String[0]);
+
+ this.permission = Optional.ofNullable(getAnnotation(Permission.class, method, parent))
+ .map(Permission::value)
+ .orElse("");
+
+ this.description = Optional.ofNullable(getAnnotation(Description.class, method, parent))
+ .map(Description::value)
+ .orElse("");
+
+ this.usage = Optional.ofNullable(getAnnotation(Usage.class, method, parent))
+ .map(Usage::value)
+ .orElse("");
+
+ this.targets = Optional.ofNullable(getAnnotation(CommandTarget.class, method, parent))
+ .map(CommandTarget::value)
+ .orElse(new String[]{"any"});
+ }
+
+ @Override
+ public void execute(CommandIssuer sender, String @NotNull [] args, CommandContext context) {
+ try {
+ method.setAccessible(true);
+ invokeHandler(sender, args, context);
+ } catch (Exception e) {
+ sender.sendMessage("§cError executing command.");
+ SkinOverlay.getInstance().getLogger()
+ .log(Level.SEVERE, "Error while executing command", e);
+ }
+ }
+
+ public BaseCommand getParent() {
+ return parent;
+ }
+
+ public boolean isDefault() {
+ return isDefault;
+ }
+
+ @Override
+ public Collection tabComplete(CommandIssuer sender, String @NotNull [] args) {
+ Collection completions = CompletionEngine.resolveCompletions(method, sender, args);
+ if (!completions.isEmpty()) {
+ return completions;
+ }
+
+ return CompletionEngine.resolveCompletions(method.getDeclaringClass(), sender, args);
+ }
+
+ private void invokeHandler(@NotNull CommandIssuer sender, String[] args, CommandContext context) throws Exception {
+ List resolved = new ArrayList<>();
+ int argIndex = 0;
+
+ for (Parameter param : method.getParameters()) {
+ Class> type = param.getType();
+
+ if (type.equals(CommandIssuer.class)) {
+ resolved.add(sender);
+ } else if (type.equals(CommandContext.class)) {
+ resolved.add(context);
+ } else if (type.equals(String[].class)) {
+ resolved.add(args);
+ } else if (param.isAnnotationPresent(Argument.class)) {
+ Argument argAnno = param.getAnnotation(Argument.class);
+ String argName = !argAnno.name().isEmpty() ? argAnno.name() : param.getName();
+ String raw = argIndex < args.length ? args[argIndex] : null;
+
+ if (raw == null || raw.isEmpty()) {
+ if (!argAnno.optional() && argAnno.defaultValue().isEmpty()) {
+ CommandMessages.COMMAND_MISSING_ARGUMENT.msg(sender,
+ new HashObjectMap()
+ .append("%command%", context.getData().getOr("command", ""))
+ .append("%arg%", argName), true);
+ return;
+ }
+ raw = argAnno.defaultValue();
+ }
+
+ Object value = convertArgument(raw, type);
+
+ if (value == null) {
+ CommandMessages.COMMAND_INVALID_ARGUMENT.msg(sender,
+ new HashObjectMap()
+ .append("%command%", context.getData().getOr("command", ""))
+ .append("%arg%", argName), true);
+ return;
+ }
+
+ // Resolve with completion key if present
+// if (!argAnno.completion().isEmpty()) {
+// var resolver = CompletionEngine.resolve(argAnno.completion());
+// if (resolver != null) value = resolver.resolveValue(sender, raw);
+// }
+
+ resolved.add(value);
+ argIndex++;
+ } else {
+ // fallback: raw string
+ resolved.add(argIndex < args.length ? args[argIndex++] : null);
+ }
+ }
+
+ method.invoke(parent, resolved.toArray());
+ }
+
+ /**
+ * Converts a raw string argument into a typed object.
+ *
+ * @param raw the raw argument
+ * @param type the target type
+ * @return the converted object or null if conversion failed
+ */
+ protected @Nullable Object convertArgument(@NotNull String raw, @NotNull Class> type) {
+ try {
+ // primitives & String
+ if (type.equals(String.class)) return raw;
+ if (type.equals(int.class) || type.equals(Integer.class)) return Integer.parseInt(raw);
+ if (type.equals(double.class) || type.equals(Double.class)) return Double.parseDouble(raw);
+ if (type.equals(boolean.class) || type.equals(Boolean.class)) return Boolean.parseBoolean(raw);
+ if (type.equals(BigInteger.class)) return new BigInteger(raw);
+ if (type.equals(BigDecimal.class)) return new BigDecimal(raw);
+
+ if (type.equals(SPlayer.class)) return SkinOverlay.getInstance().getPlayerProvider().getSPlayer(raw);
+
+ // Fallback: raw string
+ return raw;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+}
diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Argument.java b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Argument.java
new file mode 100644
index 00000000..448c55a4
--- /dev/null
+++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Argument.java
@@ -0,0 +1,67 @@
+package com.georgev22.skinoverlay.command.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * Marks a method parameter as a command argument with optional metadata.
+ *
+ * This annotation is placed on parameters of methods inside a class that extends
+ * {@link com.georgev22.skinoverlay.command.BaseCommand}. It allows specifying
+ * tab-completion hints, optional arguments, default values, and an explicit
+ * argument name.
+ *
+ *
+ * Example usage:
+ * {@code
+ * @Subcommand("teleport")
+ * public void onTeleport(
+ * CommandIssuer sender,
+ * @Argument(name = "target", completion = "@players") String target,
+ * @Argument(name = "world", completion = "@worlds", optional = true, defaultValue = "world") String worldName
+ * ) {
+ * sender.sendMessage("Teleporting " + target + " to " + worldName + "!");
+ * }
+ * }
+ *
+ * Notes:
+ *
+ * {@code name}: the display name for this argument (used in error messages, usage, etc).
+ * {@code completion}: the key used by the {@code CompletionEngine} to suggest values.
+ * {@code optional}: whether this argument can be omitted.
+ * {@code defaultValue}: the value to use if the argument is optional and not provided.
+ *
+ */
+@Target(ElementType.PARAMETER)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Argument {
+
+ /**
+ * The explicit name of the argument, used for display (e.g., in usage messages).
+ * If left empty, frameworks may fall back to reflection (param.getName()).
+ *
+ * @return the argument name
+ */
+ String name() default "";
+
+ /**
+ * The completion key for this argument (e.g., "@players", "@worlds").
+ *
+ * @return the completion key string
+ */
+ String completion() default "";
+
+ /**
+ * Whether this argument is optional.
+ *
+ * @return true if the argument is optional, false otherwise
+ */
+ boolean optional() default false;
+
+ /**
+ * The default value to use if the argument is optional and not provided.
+ *
+ * @return the default value string
+ */
+ String defaultValue() default "";
+}
diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandAlias.java b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandAlias.java
new file mode 100644
index 00000000..961abaec
--- /dev/null
+++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandAlias.java
@@ -0,0 +1,36 @@
+package com.georgev22.skinoverlay.command.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Declares one or more aliases for a command class.
+ *
+ * This annotation is placed on a class that extends {@link com.georgev22.skinoverlay.command.BaseCommand}
+ * to define the main command label(s) that can be used to invoke it.
+ *
+ *
+ * Example usage:
+ * {@code
+ * @CommandAlias({"home", "homes"})
+ * public class HomeCommand extends BaseCommand {
+ * // Command implementation here
+ * }
+ * }
+ *
+ * In the above example, both {@code /home} and {@code /homes} would
+ * execute the {@code HomeCommand}.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface CommandAlias {
+
+ /**
+ * One or more aliases that can be used to invoke the command.
+ *
+ * @return an array of command aliases
+ */
+ String[] value();
+}
diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandCompletion.java b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandCompletion.java
new file mode 100644
index 00000000..13c5addc
--- /dev/null
+++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandCompletion.java
@@ -0,0 +1,51 @@
+package com.georgev22.skinoverlay.command.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Declares the tab-completion pattern for a command or subcommand.
+ *
+ * This annotation is placed on a class that extends
+ * {@link com.georgev22.skinoverlay.command.BaseCommand} to define
+ * the default completion values shown when typing the command.
+ *
+ *
+ * Example usage:
+ * {@code
+ * @CommandAlias("teleport")
+ * @CommandCompletion("@players @worlds")
+ * public class TeleportCommand extends BaseCommand {
+ * // Command implementation
+ * }
+ * }
+ *
+ * In the above example:
+ *
+ * The first argument will suggest online players.
+ * The second argument will suggest available worlds.
+ *
+ *
+ *
+ * Completion values typically reference keys understood by your
+ * {@code CompletionEngine}, e.g. {@code @players}, {@code @worlds}, or
+ * custom registered completions.
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface CommandCompletion {
+
+ /**
+ * The completion pattern to apply to this command.
+ *
+ * Each argument is separated by a space, and may reference a completion key
+ * (e.g. {@code @players}) or a literal suggestion.
+ *
+ *
+ * @return the completion pattern string
+ */
+ String value();
+}
diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandTarget.java b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandTarget.java
new file mode 100644
index 00000000..c6a0a99f
--- /dev/null
+++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/CommandTarget.java
@@ -0,0 +1,23 @@
+package com.georgev22.skinoverlay.command.annotation;
+
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Specifies which executor type(s) can execute this command.
+ *
+ * The value refers to a target name registered in the
+ * {@link com.georgev22.skinoverlay.registry.CommandTargetRegistry}.
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD})
+public @interface CommandTarget {
+ /**
+ * The name of the target (e.g. "player", "console", "any", etc.).
+ */
+ String[] value() default {"any"};
+}
diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Default.java b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Default.java
new file mode 100644
index 00000000..f161ddb2
--- /dev/null
+++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Default.java
@@ -0,0 +1,43 @@
+package com.georgev22.skinoverlay.command.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * Marks a command method as the default entry point for the command
+ * when no subcommand or arguments are specified.
+ *
+ * This annotation is placed on a method inside a class that extends
+ * {@link com.georgev22.skinoverlay.command.BaseCommand}.
+ * The annotated method will be executed if the command is run
+ * without any subcommand or extra arguments.
+ *
+ *
+ * Example usage:
+ * {@code
+ * @CommandAlias("home")
+ * public class HomeCommand extends BaseCommand {
+ *
+ * @Default
+ * public void onDefault(CommandIssuer sender) {
+ * sender.sendMessage("Usage: /home ");
+ * }
+ *
+ * @Subcommand("set")
+ * public void onSet(CommandIssuer sender, String name) {
+ * // set home logic
+ * }
+ * }
+ * }
+ *
+ * Notes:
+ *
+ * There should typically be only one method annotated with {@code @Default} per command class.
+ * The method can accept a {@link com.georgev22.skinoverlay.command.CommandIssuer} parameter
+ * and/or a {@code String[]} parameter for raw arguments.
+ *
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Default {
+}
diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Description.java b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Description.java
new file mode 100644
index 00000000..e571695d
--- /dev/null
+++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Description.java
@@ -0,0 +1,35 @@
+package com.georgev22.skinoverlay.command.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Provides a human-readable description for a command class.
+ *
+ * This annotation is placed on a class that extends
+ * {@link com.georgev22.skinoverlay.command.BaseCommand} to define
+ * a description that can be used in help menus, logs, or usage messages.
+ *
+ *
+ * Example usage:
+ * {@code
+ * @CommandAlias("home")
+ * @Description("Allows players to manage and teleport to their homes")
+ * public class HomeCommand extends BaseCommand {
+ * // Command implementation
+ * }
+ * }
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD})
+public @interface Description {
+
+ /**
+ * The description text for the command.
+ *
+ * @return the command description
+ */
+ String value();
+}
diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Execution.java b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Execution.java
new file mode 100644
index 00000000..17a03aa8
--- /dev/null
+++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Execution.java
@@ -0,0 +1,39 @@
+package com.georgev22.skinoverlay.command.annotation;
+
+import com.georgev22.skinoverlay.command.ExecutionType;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Specifies the execution type of the command.
+ *
+ * This annotation is placed on a class that extends
+ * {@link com.georgev22.skinoverlay.command.BaseCommand} to indicate
+ * whether the command should run synchronously (SYNC) or asynchronously (ASYNC).
+ *
+ *
+ * Example usage:
+ * {@code
+ * @CommandAlias("backup")
+ * @Execution(ExecutionType.ASYNC)
+ * public class BackupCommand extends BaseCommand {
+ * // Command implementation
+ * }
+ * }
+ *
+ * By default, if this annotation is not present, the command runs synchronously.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Execution {
+
+ /**
+ * The execution type of the command.
+ *
+ * @return {@link ExecutionType#SYNC} or {@link ExecutionType#ASYNC}
+ */
+ ExecutionType value() default ExecutionType.SYNC;
+}
diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Permission.java b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Permission.java
new file mode 100644
index 00000000..2e6e71c7
--- /dev/null
+++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Permission.java
@@ -0,0 +1,38 @@
+package com.georgev22.skinoverlay.command.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Defines the permission node required to execute a command.
+ *
+ * This annotation is placed on a class that extends
+ * {@link com.georgev22.skinoverlay.command.BaseCommand} to specify
+ * which permission a user must have to run the command.
+ *
+ *
+ * Example usage:
+ * {@code
+ * @CommandAlias("fly")
+ * @Permission("core.fly")
+ * public class FlyCommand extends BaseCommand {
+ * // Command implementation
+ * }
+ * }
+ *
+ * If the permission is not set or is an empty string, the command
+ * will be available to all users.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD})
+public @interface Permission {
+
+ /**
+ * The permission node required to execute the command.
+ *
+ * @return the permission string
+ */
+ String value();
+}
diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Subcommand.java b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Subcommand.java
new file mode 100644
index 00000000..335bf345
--- /dev/null
+++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Subcommand.java
@@ -0,0 +1,43 @@
+package com.georgev22.skinoverlay.command.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a command class or method as a subcommand of a parent command.
+ *
+ * This annotation is placed on a class that extends {@link com.georgev22.skinoverlay.command.BaseCommand}
+ * or on a method inside a command class to indicate that it is a subcommand.
+ *
+ *
+ * Example usage on a class:
+ * {@code
+ * @Subcommand("list")
+ * public class ListSubCommand extends BaseCommand {
+ * // Subcommand implementation
+ * }
+ * }
+ *
+ * Example usage on a method:
+ * {@code
+ * @Subcommand("set")
+ * public void onSet(CommandIssuer sender, String key, String value) {
+ * // Subcommand logic
+ * }
+ * }
+ *
+ * The {@code value()} array defines one or more aliases for the subcommand.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD})
+public @interface Subcommand {
+
+ /**
+ * One or more aliases for the subcommand.
+ *
+ * @return an array of subcommand names
+ */
+ String[] value();
+}
diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Usage.java b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Usage.java
new file mode 100644
index 00000000..b40639b5
--- /dev/null
+++ b/common/src/main/java/com/georgev22/skinoverlay/command/annotation/Usage.java
@@ -0,0 +1,38 @@
+package com.georgev22.skinoverlay.command.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Provides a usage string for a command, showing the correct syntax.
+ *
+ * This annotation is placed on a class that extends
+ * {@link com.georgev22.skinoverlay.command.BaseCommand} to indicate
+ * how the command should be executed. It is typically displayed
+ * when a user runs the command incorrectly or requests help.
+ *
+ *
+ * Example usage:
+ * {@code
+ * @CommandAlias("home")
+ * @Usage("/home [name]")
+ * public class HomeCommand extends BaseCommand {
+ * // Command implementation
+ * }
+ * }
+ *
+ * The usage string is purely informational and does not enforce argument validation.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD})
+public @interface Usage {
+
+ /**
+ * The usage string describing the command syntax.
+ *
+ * @return the command usage
+ */
+ String value();
+}
diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/commands/SkinOverlayBaseCommand.java b/common/src/main/java/com/georgev22/skinoverlay/command/commands/SkinOverlayBaseCommand.java
new file mode 100644
index 00000000..ff3c310b
--- /dev/null
+++ b/common/src/main/java/com/georgev22/skinoverlay/command/commands/SkinOverlayBaseCommand.java
@@ -0,0 +1,21 @@
+package com.georgev22.skinoverlay.command.commands;
+
+import com.georgev22.skinoverlay.SkinOverlay;
+import com.georgev22.skinoverlay.command.BaseCommand;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Optional;
+import java.util.logging.Level;
+
+public abstract class SkinOverlayBaseCommand extends BaseCommand {
+ protected final SkinOverlay mainPlugin = SkinOverlay.getInstance();
+
+ @Override
+ public void addSubcommand(@NotNull BaseCommand subcommand) {
+ try {
+ super.addSubcommand(subcommand);
+ } catch (Exception e) {
+ mainPlugin.getLogger().log(Level.SEVERE, "Failed to register subcommand " + subcommand.getClass().getName(), e);
+ }
+ }
+}
diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/commands/SkinOverlayMain.java b/common/src/main/java/com/georgev22/skinoverlay/command/commands/SkinOverlayMain.java
new file mode 100644
index 00000000..382e18bd
--- /dev/null
+++ b/common/src/main/java/com/georgev22/skinoverlay/command/commands/SkinOverlayMain.java
@@ -0,0 +1,29 @@
+package com.georgev22.skinoverlay.command.commands;
+
+import com.georgev22.skinoverlay.command.CommandContext;
+import com.georgev22.skinoverlay.command.CommandIssuer;
+import com.georgev22.skinoverlay.command.annotation.*;
+import com.georgev22.skinoverlay.command.commands.sub.*;
+import com.georgev22.skinoverlay.message.messages.CommandMessages;
+import org.jetbrains.annotations.NotNull;
+
+@CommandAlias({"skinoverlay", "soverlay", "skino"})
+@Permission("skinoverlay.help")
+@CommandCompletion("help|reload|wear|reset|url")
+@Description("SkinOverlay commands")
+@Usage("/skinoverlay [arguments]")
+public class SkinOverlayMain extends SkinOverlayBaseCommand {
+
+ public SkinOverlayMain() {
+ this.addSubcommand(new WearSubCommand());
+ this.addSubcommand(new ClearSubCommand());
+ this.addSubcommand(new WearUrlSubCommand());
+ this.addSubcommand(new ReloadSubCommand());
+ this.addSubcommand(new InfoSubCommand());
+ }
+
+ @Default
+ protected void handle(@NotNull CommandIssuer commandIssuer) {
+ CommandMessages.COMMAND_HELP.msg(commandIssuer);
+ }
+}
diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/ClearSubCommand.java b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/ClearSubCommand.java
new file mode 100644
index 00000000..20556920
--- /dev/null
+++ b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/ClearSubCommand.java
@@ -0,0 +1,74 @@
+package com.georgev22.skinoverlay.command.commands.sub;
+
+import com.georgev22.skinoverlay.SkinOverlay;
+import com.georgev22.skinoverlay.command.CommandIssuer;
+import com.georgev22.skinoverlay.command.annotation.*;
+import com.georgev22.skinoverlay.command.commands.SkinOverlayBaseCommand;
+import com.georgev22.skinoverlay.maps.HashObjectMap;
+import com.georgev22.skinoverlay.message.messages.CommandMessages;
+import com.georgev22.skinoverlay.player.SPlayer;
+import com.georgev22.skinoverlay.storage.data.Skin;
+import com.georgev22.skinoverlay.utilities.skin.SkinParts;
+import org.jetbrains.annotations.NotNull;
+
+@Subcommand({"clear", "reset"})
+@CommandAlias("soclear")
+@CommandCompletion("@players")
+@Description("Removes the skin overlay")
+@Permission("skinoverlay.wear.clear")
+public class ClearSubCommand extends SkinOverlayBaseCommand {
+
+ @Default
+ protected void handle(@NotNull CommandIssuer commandIssuer, @Argument(name = "target", completion = "@players", optional = true) SPlayer target) {
+ if (target == null) {
+ if (!commandIssuer.isPlayer()) {
+ CommandMessages.COMMAND_MISSING_ARGUMENT.msg(commandIssuer,
+ new HashObjectMap().append("%command%", "clear ").append("%arg%", "target"), true);
+ return;
+ }
+ SPlayer player = mainPlugin.getPlayerProvider().getSPlayer(commandIssuer.getUniqueId());
+ if (player == null) {
+ return;
+ }
+ SkinParts skinParts = new SkinParts(null, "default");
+ mainPlugin.getSkinProvider().retrieveOrGenerateSkin(
+ player,
+ skinParts)
+ .thenAcceptAsync(optionalSkin -> {
+ if (optionalSkin.isEmpty()) {
+ return;
+ }
+ Skin skin = optionalSkin.get();
+ mainPlugin.getSkinApplier().setSkin(player, skin);
+ CommandMessages.COMMAND_OVERLAY_RESET.msg(
+ commandIssuer,
+ new HashObjectMap().append("%player%", player.getName()),
+ true
+ );
+
+ }, runnable -> SkinOverlay.getInstance().getScheduler().runTask(SkinOverlay.getInstance().getPlugin(), runnable));
+ } else {
+ if (!target.isOnline()) {
+ CommandMessages.COMMAND_OFFLINE_PLAYER.msg(commandIssuer, new HashObjectMap().append("%player%", target.getName()), true);
+ return;
+ }
+ SkinParts skinParts = new SkinParts(null, "default");
+ mainPlugin.getSkinProvider().retrieveOrGenerateSkin(
+ target,
+ skinParts)
+ .thenAcceptAsync(skinOptional -> {
+ if (skinOptional.isEmpty()) {
+ return;
+ }
+ Skin skin = skinOptional.get();
+ mainPlugin.getSkinApplier().setSkin(target, skin);
+ CommandMessages.COMMAND_OVERLAY_RESET.msg(
+ commandIssuer,
+ new HashObjectMap().append("%player%", target.getName()),
+ true
+ );
+
+ }, runnable -> SkinOverlay.getInstance().getScheduler().runTask(SkinOverlay.getInstance().getPlugin(), runnable));
+ }
+ }
+}
diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/InfoSubCommand.java b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/InfoSubCommand.java
new file mode 100644
index 00000000..59840c19
--- /dev/null
+++ b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/InfoSubCommand.java
@@ -0,0 +1,41 @@
+package com.georgev22.skinoverlay.command.commands.sub;
+
+import com.georgev22.skinoverlay.BuildParameters;
+import com.georgev22.skinoverlay.command.CommandIssuer;
+import com.georgev22.skinoverlay.command.annotation.CommandAlias;
+import com.georgev22.skinoverlay.command.annotation.Default;
+import com.georgev22.skinoverlay.command.annotation.Permission;
+import com.georgev22.skinoverlay.command.annotation.Subcommand;
+import com.georgev22.skinoverlay.command.commands.SkinOverlayBaseCommand;
+import org.jetbrains.annotations.NotNull;
+
+@Subcommand("info")
+@CommandAlias("soinfo")
+@Permission("skinoverlay.info")
+public class InfoSubCommand extends SkinOverlayBaseCommand {
+
+ @Default
+ protected void handle(@NotNull CommandIssuer commandIssuer) {
+ String pluginName = BuildParameters.PLUGIN_NAME;
+ String pluginVersion = BuildParameters.VERSION;
+ String pluginAuthor = BuildParameters.AUTHOR;
+ String pluginDescription = BuildParameters.DESCRIPTION;
+ String pluginWebsite = BuildParameters.URL;
+ String CIName = BuildParameters.CI_NAME;
+ String CIBuildNumber = BuildParameters.CI_BUILD_NUMBER;
+ String commit = BuildParameters.COMMIT;
+ String branch = BuildParameters.BRANCH;
+ String buildTime = BuildParameters.BUILD_TIME;
+
+ commandIssuer.sendMessage("Name: " + pluginName);
+ commandIssuer.sendMessage("Version: " + pluginVersion);
+ commandIssuer.sendMessage("Author: " + pluginAuthor);
+ commandIssuer.sendMessage("Description: " + pluginDescription);
+ commandIssuer.sendMessage("Website: " + pluginWebsite);
+ commandIssuer.sendMessage("CI Name: " + CIName);
+ commandIssuer.sendMessage("CI Build Number: " + CIBuildNumber);
+ commandIssuer.sendMessage("Commit: " + commit);
+ commandIssuer.sendMessage("Branch: " + branch);
+ commandIssuer.sendMessage("Build Time: " + buildTime);
+ }
+}
diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/ReloadSubCommand.java b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/ReloadSubCommand.java
new file mode 100644
index 00000000..26846995
--- /dev/null
+++ b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/ReloadSubCommand.java
@@ -0,0 +1,35 @@
+package com.georgev22.skinoverlay.command.commands.sub;
+
+import com.georgev22.skinoverlay.command.CommandIssuer;
+import com.georgev22.skinoverlay.command.annotation.CommandAlias;
+import com.georgev22.skinoverlay.command.annotation.Default;
+import com.georgev22.skinoverlay.command.annotation.Permission;
+import com.georgev22.skinoverlay.command.annotation.Subcommand;
+import com.georgev22.skinoverlay.command.commands.SkinOverlayBaseCommand;
+import com.georgev22.skinoverlay.message.MessageEntry;
+import com.georgev22.skinoverlay.message.MessagesRegistry;
+import com.georgev22.skinoverlay.message.messages.CommandMessages;
+import com.georgev22.skinoverlay.message.messages.CoreMessages;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.logging.Level;
+
+@Subcommand("reload")
+@CommandAlias("soreload")
+@Permission("skinoverlay.reload")
+public class ReloadSubCommand extends SkinOverlayBaseCommand {
+ @Default
+ protected void handle(@NotNull CommandIssuer commandIssuer) {
+ //TODO RELOAD SKIN HANDLER
+ mainPlugin.getFileManager().getConfig().reloadFile();
+ try {
+ MessagesRegistry.registerAll(new MessageEntry[][]{
+ CommandMessages.values(),
+ CoreMessages.values(),
+ });
+ } catch (Exception e) {
+ mainPlugin.getLogger().log(Level.SEVERE, "Error loading the language file: ", e);
+ }
+ CoreMessages.PLUGIN_RELOAD.msg(commandIssuer);
+ }
+}
diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/WearSubCommand.java b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/WearSubCommand.java
new file mode 100644
index 00000000..9c636816
--- /dev/null
+++ b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/WearSubCommand.java
@@ -0,0 +1,81 @@
+package com.georgev22.skinoverlay.command.commands.sub;
+
+import com.georgev22.skinoverlay.SkinOverlay;
+import com.georgev22.skinoverlay.command.CommandIssuer;
+import com.georgev22.skinoverlay.command.annotation.*;
+import com.georgev22.skinoverlay.command.commands.SkinOverlayBaseCommand;
+import com.georgev22.skinoverlay.maps.HashObjectMap;
+import com.georgev22.skinoverlay.message.messages.CommandMessages;
+import com.georgev22.skinoverlay.player.SPlayer;
+import com.georgev22.skinoverlay.storage.data.Skin;
+import com.georgev22.skinoverlay.utilities.SerializableBufferedImage;
+import com.georgev22.skinoverlay.utilities.skin.SkinParts;
+import org.jetbrains.annotations.NotNull;
+
+import javax.imageio.ImageIO;
+import java.io.File;
+import java.io.IOException;
+import java.util.logging.Level;
+
+@Subcommand({"wear", "overlay"})
+@CommandAlias("sowear")
+@Permission("skinoverlay.wear.overlay")
+@Description("Wear an overlay on a player's skin")
+@CommandCompletion("@overlays @players")
+public class WearSubCommand extends SkinOverlayBaseCommand {
+
+ @Default
+ protected void handle(@NotNull CommandIssuer commandIssuer,
+ @Argument(name = "overlay", completion = "@overlays") String overlay,
+ @Argument(name = "target", completion = "@players", optional = true) SPlayer target) {
+ if (overlay == null || overlay.isEmpty()) {
+ CommandMessages.COMMAND_MISSING_ARGUMENT.msg(commandIssuer, new HashObjectMap()
+ .append("%command%", "wear ").append("%arg%", "overlay"), true);
+ return;
+ }
+
+ if (target == null) {
+ if (!commandIssuer.isPlayer()) {
+ CommandMessages.COMMAND_MISSING_ARGUMENT.msg(commandIssuer, new HashObjectMap()
+ .append("%command%", "wear ").append("%arg%", "target"), true);
+ return;
+ }
+ target = mainPlugin.getPlayerProvider().getSPlayer(commandIssuer.getUniqueId());
+ }
+ SkinParts skinParts;
+ try {
+ File overlayFile = new File(mainPlugin.getSkinsDataFolder(), overlay + ".png");
+ if (!overlayFile.exists()) {
+ CommandMessages.COMMAND_OVERLAY_NOT_FOUND.msg(commandIssuer, new HashObjectMap().append("%overlay%", overlay), true);
+ return;
+ }
+ skinParts = new SkinParts(new SerializableBufferedImage(ImageIO.read(overlayFile)), overlay);
+ } catch (IOException e) {
+ mainPlugin.getLogger().log(Level.SEVERE, "Error while trying to load the skin: ", e);
+ return;
+ }
+ SPlayer finalTarget = target;
+ mainPlugin.getSkinProvider()
+ .retrieveOrGenerateSkin(
+ target,
+ skinParts
+ ).thenAcceptAsync(optionalSkin -> {
+ if (optionalSkin.isEmpty()) {
+ mainPlugin.getLogger().info("Skin is null");
+ return;
+ }
+ Skin skin = optionalSkin.get();
+ mainPlugin.getSkinApplier()
+ .setSkin(finalTarget, skin);
+ CommandMessages.COMMAND_OVERLAY_DONE.msg(
+ commandIssuer,
+ new HashObjectMap()
+ .append("%player%", finalTarget.getName())
+ .append("%url%", skin.skinURL())
+ .append("%name%", skin.getSkinParts().getSkinName())
+ .append("%skinParts%", skin.getSkinParts().toString()),
+ true
+ );
+ }, runnable -> SkinOverlay.getInstance().getScheduler().runTask(SkinOverlay.getInstance().getPlugin(), runnable));
+ }
+}
diff --git a/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/WearUrlSubCommand.java b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/WearUrlSubCommand.java
new file mode 100644
index 00000000..a7784c09
--- /dev/null
+++ b/common/src/main/java/com/georgev22/skinoverlay/command/commands/sub/WearUrlSubCommand.java
@@ -0,0 +1,100 @@
+package com.georgev22.skinoverlay.command.commands.sub;
+
+import com.georgev22.skinoverlay.SkinOverlay;
+import com.georgev22.skinoverlay.command.CommandIssuer;
+import com.georgev22.skinoverlay.command.annotation.*;
+import com.georgev22.skinoverlay.command.commands.SkinOverlayBaseCommand;
+import com.georgev22.skinoverlay.maps.HashObjectMap;
+import com.georgev22.skinoverlay.message.messages.CommandMessages;
+import com.georgev22.skinoverlay.player.SPlayer;
+import com.georgev22.skinoverlay.storage.data.Skin;
+import com.georgev22.skinoverlay.utilities.SerializableBufferedImage;
+import com.georgev22.skinoverlay.utilities.skin.SkinParts;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.imageio.ImageIO;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.logging.Level;
+
+@Subcommand("url")
+@CommandAlias("sowearurl")
+@Permission("skinoverlay.wear.url")
+@Description("Wear an overlay on a player's skin")
+@CommandCompletion(" @players")
+public class WearUrlSubCommand extends SkinOverlayBaseCommand {
+
+ @Default
+ protected void handle(@NotNull CommandIssuer commandIssuer,
+ @Argument(name = "url") String urlStr,
+ @Argument(name = "target", completion = "@players", optional = true) SPlayer target) {
+
+ try {
+ URL url = new URL(urlStr);
+
+ byte[] imageBytes = downloadImageBytes(url);
+ if (imageBytes == null) {
+ CommandMessages.COMMAND_INVALID_URL.msg(commandIssuer, new HashObjectMap().append("%url%", url.toString()), true);
+ return;
+ }
+
+ if (target == null && !commandIssuer.isPlayer()) {
+ CommandMessages.COMMAND_MISSING_ARGUMENT.msg(commandIssuer, new HashObjectMap().append("%command%", "url