diff --git a/eternalcore-core/src/main/java/com/eternalcode/core/litecommand/configurator/CommandConfigurator.java b/eternalcore-core/src/main/java/com/eternalcode/core/litecommand/configurator/CommandConfigurator.java index 8796e7a55..a0c7378ba 100644 --- a/eternalcore-core/src/main/java/com/eternalcode/core/litecommand/configurator/CommandConfigurator.java +++ b/eternalcore-core/src/main/java/com/eternalcode/core/litecommand/configurator/CommandConfigurator.java @@ -1,54 +1,224 @@ package com.eternalcode.core.litecommand.configurator; +import com.eternalcode.core.configuration.ConfigurationManager; import com.eternalcode.core.litecommand.configurator.config.Command; import com.eternalcode.core.litecommand.configurator.config.CommandConfiguration; import com.eternalcode.core.litecommand.configurator.config.SubCommand; +import com.eternalcode.commons.scheduler.Scheduler; import com.eternalcode.core.injector.annotations.Inject; import com.eternalcode.core.injector.annotations.lite.LiteCommandEditor; import dev.rollczi.litecommands.command.builder.CommandBuilder; import dev.rollczi.litecommands.editor.Editor; import dev.rollczi.litecommands.meta.Meta; import dev.rollczi.litecommands.permission.PermissionSet; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.UnaryOperator; +import java.util.logging.Logger; import org.bukkit.command.CommandSender; @LiteCommandEditor class CommandConfigurator implements Editor { private final CommandConfiguration commandConfiguration; + private final ConfigurationManager configurationManager; + private final Scheduler scheduler; + private final Logger logger; + private final Set discoveredRuntimeCommands = ConcurrentHashMap.newKeySet(); + private volatile boolean staleCleanupScheduled; @Inject - CommandConfigurator(CommandConfiguration commandConfiguration) { + CommandConfigurator( + CommandConfiguration commandConfiguration, + ConfigurationManager configurationManager, + Scheduler scheduler, + Logger logger + ) { this.commandConfiguration = commandConfiguration; + this.configurationManager = configurationManager; + this.scheduler = scheduler; + this.logger = logger; } @Override public CommandBuilder edit(CommandBuilder context) { - Command command = this.commandConfiguration.commands.get(context.name()); + if (context.isRoot()) { + this.scheduleStaleCleanupOnce(); + return context; + } + + if (context.name() == null || context.name().isBlank()) { + return context; + } + + String commandName = context.name(); + this.discoveredRuntimeCommands.add(commandName); + + boolean changed = this.synchronizeDefaults(context); + if (changed) { + this.configurationManager.save(this.commandConfiguration); + } + + Command command = this.commandConfiguration.commands.get(commandName); if (command == null) { return context; } - for (String child : command.subCommands().keySet()) { - SubCommand subCommand = command.subCommands().get(child); + for (Map.Entry childEntry : command.subCommands().entrySet()) { + String child = childEntry.getKey(); + SubCommand subCommand = childEntry.getValue(); + + if (context.getChild(child).isEmpty()) { + continue; + } context = context.editChild(child, editor -> editor.name(subCommand.name()) - .aliases(subCommand.aliases()) - .applyMeta(editPermissions(command.permissions())) + .aliases(safeList(subCommand.aliases())) + .applyMeta(editPermissions(safeList(subCommand.permissions()))) .enabled(subCommand.isEnabled()) ); } return context .name(command.name()) - .aliases(command.aliases()) - .applyMeta(editPermissions(command.permissions())) + .aliases(safeList(command.aliases())) + .applyMeta(editPermissions(safeList(command.permissions()))) .enabled(command.isEnabled()); } + private void scheduleStaleCleanupOnce() { + if (this.staleCleanupScheduled) { + return; + } + + this.staleCleanupScheduled = true; + this.scheduler.runLater(this::cleanupStaleCommands, Duration.ofSeconds(1)); + } + + private void cleanupStaleCommands() { + this.commandConfiguration.commands = mutableMap(this.commandConfiguration.commands); + boolean changed = this.commandConfiguration.commands.remove("") != null; + + if (changed) { + this.logger.warning("[CommandConfigurator] Removed invalid empty command key ('') from commands.yml"); + } + + Set runtimeNames = new HashSet<>(this.discoveredRuntimeCommands); + int configuredCount = this.commandConfiguration.commands.size(); + int discoveredCount = runtimeNames.size(); + + if (configuredCount > 0 && discoveredCount == 0) { + this.logger.warning("[CommandConfigurator] Skipped stale cleanup because no runtime commands were discovered."); + if (changed) { + this.configurationManager.save(this.commandConfiguration); + } + return; + } + + List staleKeys = this.commandConfiguration.commands.entrySet().stream() + .filter(entry -> entry.getValue() == null || !runtimeNames.contains(entry.getKey())) + .map(Map.Entry::getKey) + .toList(); + + for (String staleKey : staleKeys) { + this.commandConfiguration.commands.remove(staleKey); + this.logger.warning("[CommandConfigurator] Removed stale command from commands.yml: '" + staleKey + "'"); + changed = true; + } + + if (changed) { + this.configurationManager.save(this.commandConfiguration); + } + } + + private boolean synchronizeDefaults(CommandBuilder context) { + this.commandConfiguration.commands = mutableMap(this.commandConfiguration.commands); + boolean changed = false; + + if (this.commandConfiguration.commands.remove("") != null) { + this.logger.warning("[CommandConfigurator] Removed invalid empty command key ('') from commands.yml"); + changed = true; + } + + String runtimeName = context.name(); + Command current = this.commandConfiguration.commands.get(runtimeName); + + if (current == null) { + current = new Command( + runtimeName, + new ArrayList<>(context.aliases()), + extractPermissions(context.meta()), + context.isEnabled() + ); + current.subCommands = new LinkedHashMap<>(); + + this.commandConfiguration.commands.put(runtimeName, current); + this.logger.info("[CommandConfigurator] Added new command defaults to commands.yml: '" + runtimeName + "'"); + changed = true; + } + + current.subCommands = mutableMap(current.subCommands); + + for (CommandBuilder child : context.children()) { + if (current.subCommands.containsKey(child.name())) { + continue; + } + + current.subCommands.put( + child.name(), + new SubCommand( + child.name(), + child.isEnabled(), + new ArrayList<>(child.aliases()), + extractPermissions(child.meta()) + ) + ); + this.logger.info( + "[CommandConfigurator] Added missing subcommand defaults for '" + runtimeName + "': '" + child.name() + "'" + ); + changed = true; + } + + return changed; + } + + private static List extractPermissions(Meta meta) { + Collection permissionSets = meta.get(Meta.PERMISSIONS); + LinkedHashSet permissions = new LinkedHashSet<>(); + + for (PermissionSet permissionSet : permissionSets) { + permissions.addAll(permissionSet.getPermissions()); + } + + return new ArrayList<>(permissions); + } + + private static Map mutableMap(Map map) { + if (map == null) { + return new LinkedHashMap<>(); + } + + return new LinkedHashMap<>(map); + } + + private static List safeList(List value) { + if (value == null) { + return List.of(); + } + + return value; + } + private static UnaryOperator editPermissions(List permissions) { return meta -> meta.listEditor(Meta.PERMISSIONS) .clear()