diff --git a/leaves-server/paper-patches/features/0003-Leaves-Server-Config-And-Command.patch b/leaves-server/paper-patches/features/0003-Leaves-Server-Config-And-Command.patch index 04b1d2d5..53e55f64 100644 --- a/leaves-server/paper-patches/features/0003-Leaves-Server-Config-And-Command.patch +++ b/leaves-server/paper-patches/features/0003-Leaves-Server-Config-And-Command.patch @@ -17,15 +17,34 @@ index 62e2d5704c348955bc8284dc2d54c933b7bcdd06..7ef20f0138fad39a1d23edd7b26ddc88 @Override public void executeAsync(final Runnable runnable) { MCUtil.scheduleAsyncTask(this.catching(runnable, "asynchronous")); +diff --git a/src/main/java/io/papermc/paper/command/brigadier/bukkit/BukkitCommandNode.java b/src/main/java/io/papermc/paper/command/brigadier/bukkit/BukkitCommandNode.java +index 1814cd072aaca3e72249f0509a9c3b3cb154eaba..11b394f479eb268b248fae8a72ea97886853d797 100644 +--- a/src/main/java/io/papermc/paper/command/brigadier/bukkit/BukkitCommandNode.java ++++ b/src/main/java/io/papermc/paper/command/brigadier/bukkit/BukkitCommandNode.java +@@ -105,6 +105,14 @@ public class BukkitCommandNode extends LiteralCommandNode { + List results = null; + Location pos = context.getSource().getLocation(); + try { ++ // Leaves start - custom suggestion ++ if (this.command instanceof org.leavesmc.leaves.command.LeavesSuggestionCommand suggestionCommand) { ++ CompletableFuture suggestions = suggestionCommand.tabSuggestion(sender, this.literal, args, pos.clone(), builder); ++ if (suggestions != null) { ++ return suggestions; ++ } ++ } ++ // Leaves end - custom suggestion + results = this.command.tabComplete(sender, this.literal, args, pos.clone()); + } catch (CommandException ex) { + sender.sendMessage(ChatColor.RED + "An internal error occurred while attempting to tab-complete this command"); diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -index cf5bcb0ebc79c318d106695e39ad2883a5734aa1..c6fbb091c513fb9e5527ac9cb08af607f14468b4 100644 +index 59eddee7e75e1de332346ffe73fb88eb1ae14db6..faa2d73c8a9664110d954b833da0dca9ca21dbef 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -1116,6 +1116,7 @@ public final class CraftServer implements Server { playerMetadata.removeAll(plugin); } // Paper end -+ org.leavesmc.leaves.LeavesConfig.init((File) console.options.valueOf("leaves-settings")); // Leaves - Server Config ++ org.leavesmc.leaves.LeavesConfig.reload(); // Leaves - Server Config this.reloadData(); org.spigotmc.SpigotConfig.registerCommands(); // Spigot io.papermc.paper.command.PaperCommands.registerCommands(this.console); // Paper diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java b/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java index f8c9c3c2..98fb0bca 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java @@ -8,9 +8,9 @@ import org.bukkit.command.Command; import org.bukkit.configuration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; import org.leavesmc.leaves.command.LeavesCommand; -import org.leavesmc.leaves.config.GlobalConfig; -import org.leavesmc.leaves.config.GlobalConfigCategory; -import org.leavesmc.leaves.config.RemovedConfig; +import org.leavesmc.leaves.config.annotations.GlobalConfig; +import org.leavesmc.leaves.config.annotations.GlobalConfigCategory; +import org.leavesmc.leaves.config.annotations.RemovedConfig; import org.leavesmc.leaves.config.GlobalConfigManager; import org.leavesmc.leaves.region.RegionFileFormat; import org.leavesmc.leaves.util.MathUtils; @@ -75,6 +75,22 @@ public final class LeavesConfig { registerCommand("leaves", new LeavesCommand("leaves")); } + public static void reload() { + if (!LeavesConfig.configFile.exists()) { + throw new RuntimeException("Leaves config file not found, please restart the server"); + } + + try { + config.load(LeavesConfig.configFile); + } catch (final Exception ex) { + LeavesLogger.LOGGER.severe("Failure to reload leaves config", ex); + SneakyThrow.sneaky(ex); + throw new RuntimeException(ex); + } + + GlobalConfigManager.reload(); + } + public static void save() { try { config.save(LeavesConfig.configFile); @@ -768,7 +784,7 @@ public final class LeavesConfig { private static class AlternativePlaceValidator extends EnumConfigValidator { @Override - public void runAfterLoader(AlternativePlaceType value, boolean firstLoad) { + public void runAfterLoader(AlternativePlaceType value, boolean reload) { if (value != AlternativePlaceType.NONE) { LeavesConfig.modify.disableDistanceCheckForUseItem = true; } @@ -805,8 +821,8 @@ public final class LeavesConfig { private static class AutoUpdateValidator extends BooleanConfigValidator { @Override - public void runAfterLoader(Boolean value, boolean firstLoad) { - if (firstLoad) { + public void runAfterLoader(Boolean value, boolean reload) { + if (reload) { org.leavesmc.leaves.util.LeavesUpdateHelper.init(); if (value) { LeavesLogger.LOGGER.warning("Auto-Update is not completely safe. Enabling it may cause data security problems!"); diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java index 8fcd3824..6c5d47b0 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java @@ -1,6 +1,9 @@ package org.leavesmc.leaves.command; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; import it.unimi.dsi.fastutil.Pair; +import net.kyori.adventure.text.Component; import net.minecraft.Util; import org.bukkit.Bukkit; import org.bukkit.Location; @@ -15,19 +18,23 @@ import org.leavesmc.leaves.command.subcommands.*; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import static net.kyori.adventure.text.Component.text; import static net.kyori.adventure.text.format.NamedTextColor.RED; -public final class LeavesCommand extends Command { - static final String BASE_PERM = "bukkit.command.leaves."; +public final class LeavesCommand extends Command implements LeavesSuggestionCommand { + + public static final String BASE_PERM = "bukkit.command.leaves."; + // subcommand label -> subcommand private static final Map SUBCOMMANDS = Util.make(() -> { final Map, LeavesSubcommand> commands = new HashMap<>(); @@ -41,7 +48,6 @@ public final class LeavesCommand extends Command { .flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue()))) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); }); - private static final Set COMPLETABLE_SUBCOMMANDS = SUBCOMMANDS.entrySet().stream().filter(entry -> entry.getValue().tabCompletes()).map(Map.Entry::getKey).collect(Collectors.toSet()); public LeavesCommand(final String name) { super(name); @@ -68,11 +74,10 @@ public final class LeavesCommand extends Command { } @NotNull - @Override public List tabComplete(final @NotNull CommandSender sender, final @NotNull String alias, final String[] args, final @Nullable Location location) throws IllegalArgumentException { if (args.length <= 1) { - return LeavesCommandUtil.getListMatchingLast(sender, args, COMPLETABLE_SUBCOMMANDS); + return LeavesCommandUtil.getListMatchingLast(sender, args, usableSubcommands()); } final @Nullable Pair subCommand = resolveCommand(args[0]); @@ -83,6 +88,18 @@ public final class LeavesCommand extends Command { return Collections.emptyList(); } + @Nullable + @Override + public CompletableFuture tabSuggestion(final @NotNull CommandSender sender, final @NotNull String alias, final @NotNull String @NotNull [] args, final @Nullable Location location, final @NotNull SuggestionsBuilder builder) throws IllegalArgumentException { + if (args.length > 1) { + final @Nullable Pair subCommand = resolveCommand(args[0]); + if (subCommand != null) { + return subCommand.second().tabSuggestion(sender, subCommand.first(), Arrays.copyOfRange(args, 1, args.length), location, builder); + } + } + return null; + } + @Override public boolean execute(final @NotNull CommandSender sender, final @NotNull String commandLabel, final String @NotNull [] args) { if (!testPermission(sender)) { @@ -90,13 +107,13 @@ public final class LeavesCommand extends Command { } if (args.length == 0) { - sender.sendMessage(text("Usage: " + this.usageMessage, RED)); + sender.sendMessage(unknownMessage()); return false; } final Pair subCommand = resolveCommand(args[0]); if (subCommand == null) { - sender.sendMessage(text("Usage: " + this.usageMessage, RED)); + sender.sendMessage(unknownMessage()); return false; } @@ -107,6 +124,20 @@ public final class LeavesCommand extends Command { return subCommand.second().execute(sender, subCommand.first(), choppedArgs); } + private Collection usableSubcommands() { + List subcommands = new ArrayList<>(); + for (var entry : SUBCOMMANDS.entrySet()) { + if (entry.getValue().tabCompletes()) { + subcommands.add(entry.getKey()); + } + } + return subcommands; + } + + public Component unknownMessage() { + return text("Usage: /bot [" + String.join(" | ", usableSubcommands()) + "]", RED); + } + @Nullable private static Pair resolveCommand(String label) { label = label.toLowerCase(Locale.ENGLISH); @@ -118,4 +149,5 @@ public final class LeavesCommand extends Command { return null; } + } diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesCommandUtil.java b/leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesCommandUtil.java index d110bf39..38bcf396 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesCommandUtil.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesCommandUtil.java @@ -7,12 +7,17 @@ import net.minecraft.resources.ResourceLocation; import org.bukkit.command.CommandSender; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; @DefaultQualifier(NonNull.class) public class LeavesCommandUtil { @@ -78,4 +83,106 @@ public class LeavesCommandUtil { return results; } // end copy stuff + + public static List getListClosestMatchingLast( + final CommandSender sender, + final String last, + final Collection collection, + final String overridePermission + ) { + ArrayList candidates = Lists.newArrayList(); + + if (collection.isEmpty() || !sender.hasPermission(overridePermission)) { + return Collections.emptyList(); + } + + String lastLower = last.toLowerCase(); + for (String item : Iterables.transform(collection, Functions.toStringFunction())) { + String itemLower = item.toLowerCase(); + if (itemLower.startsWith(lastLower)) { + candidates.add(Candidate.of(item, 0)); + } else if (itemLower.contains(lastLower)) { + candidates.add(Candidate.of(item, damerauLevenshteinDistance(lastLower, itemLower))); + } + } + candidates.sort(Comparator.comparingInt(c -> c.score)); + + List results = new ArrayList<>(candidates.size()); + for (Candidate candidate : candidates) { + results.add(candidate.item); + } + + return results; + } + + private record Candidate(String item, int score) { + private static Candidate of(String item, int score) { + return new Candidate(item, score); + } + } + + // Copy from org/bukkit/command/defaults/HelpCommand.java + /** + * Computes the Dameraur-Levenshtein Distance between two strings. Adapted + * from the algorithm at Wikipedia: Damerau–Levenshtein distance + * + * @param s1 The first string being compared. + * @param s2 The second string being compared. + * @return The number of substitutions, deletions, insertions, and + * transpositions required to get from s1 to s2. + */ + @SuppressWarnings("DuplicatedCode") + private static int damerauLevenshteinDistance(@Nullable String s1, @Nullable String s2) { + if (s1 == null && s2 == null) { + return 0; + } + if (s1 != null && s2 == null) { + return s1.length(); + } + if (s1 == null && s2 != null) { + return s2.length(); + } + + int s1Len = s1.length(); + int s2Len = s2.length(); + int[][] H = new int[s1Len + 2][s2Len + 2]; + + int INF = s1Len + s2Len; + H[0][0] = INF; + for (int i = 0; i <= s1Len; i++) { + H[i + 1][1] = i; + H[i + 1][0] = INF; + } + for (int j = 0; j <= s2Len; j++) { + H[1][j + 1] = j; + H[0][j + 1] = INF; + } + + Map sd = new HashMap<>(); + for (char Letter : (s1 + s2).toCharArray()) { + if (!sd.containsKey(Letter)) { + sd.put(Letter, 0); + } + } + + for (int i = 1; i <= s1Len; i++) { + int DB = 0; + for (int j = 1; j <= s2Len; j++) { + int i1 = sd.get(s2.charAt(j - 1)); + int j1 = DB; + + if (s1.charAt(i - 1) == s2.charAt(j - 1)) { + H[i + 1][j + 1] = H[i][j]; + DB = j; + } else { + H[i + 1][j + 1] = Math.min(H[i][j], Math.min(H[i + 1][j], H[i][j + 1])) + 1; + } + + H[i + 1][j + 1] = Math.min(H[i + 1][j + 1], H[i1][j1] + (i - i1 - 1) + 1 + (j - j1 - 1)); + } + sd.put(s1.charAt(i - 1), i); + } + + return H[s1Len + 1][s2Len + 1]; + } } \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesSubcommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesSubcommand.java index 5f5bf347..ff09291e 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesSubcommand.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesSubcommand.java @@ -1,10 +1,13 @@ package org.leavesmc.leaves.command; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; import org.bukkit.Location; import org.bukkit.command.CommandSender; import java.util.Collections; import java.util.List; +import java.util.concurrent.CompletableFuture; public interface LeavesSubcommand { boolean execute(CommandSender sender, String subCommand, String[] args); @@ -13,6 +16,10 @@ public interface LeavesSubcommand { return Collections.emptyList(); } + default CompletableFuture tabSuggestion(final CommandSender sender, final String subCommand, final String[] args, final Location location, final SuggestionsBuilder builder) { + return null; + } + default boolean tabCompletes() { return true; } diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesSuggestionCommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesSuggestionCommand.java new file mode 100644 index 00000000..42750461 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesSuggestionCommand.java @@ -0,0 +1,15 @@ +package org.leavesmc.leaves.command; + +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import org.bukkit.Location; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.CompletableFuture; + +public interface LeavesSuggestionCommand { + @Nullable + CompletableFuture tabSuggestion(@NotNull CommandSender sender, @NotNull String alias, @NotNull String @NotNull [] args, @Nullable Location location, @NotNull SuggestionsBuilder builder) throws IllegalArgumentException; +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/ConfigCommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/ConfigCommand.java index 6884245f..12195071 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/ConfigCommand.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/ConfigCommand.java @@ -1,17 +1,22 @@ package org.leavesmc.leaves.command.subcommands; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.JoinConfiguration; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Location; import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.leavesmc.leaves.command.LeavesCommandUtil; import org.leavesmc.leaves.command.LeavesSubcommand; import org.leavesmc.leaves.config.GlobalConfigManager; +import org.leavesmc.leaves.config.VerifiedConfig; -import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.CompletableFuture; public class ConfigCommand implements LeavesSubcommand { @@ -22,12 +27,12 @@ public class ConfigCommand implements LeavesSubcommand { return true; } - GlobalConfigManager.VerifiedConfig verifiedConfig = GlobalConfigManager.getVerifiedConfig(args[0]); + VerifiedConfig verifiedConfig = GlobalConfigManager.getVerifiedConfig(args[0]); if (verifiedConfig == null) { sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), - Component.text("Config ", NamedTextColor.GRAY), - Component.text(args[0], NamedTextColor.RED), - Component.text(" is Not Found.", NamedTextColor.GRAY) + Component.text("Config ", NamedTextColor.GRAY), + Component.text(args[0], NamedTextColor.RED), + Component.text(" is Not Found.", NamedTextColor.GRAY) )); return true; } @@ -36,25 +41,25 @@ public class ConfigCommand implements LeavesSubcommand { try { verifiedConfig.set(args[1]); sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), - Component.text("Config ", NamedTextColor.GRAY), - Component.text(args[0], NamedTextColor.AQUA), - Component.text(" changed to ", NamedTextColor.GRAY), - Component.text(verifiedConfig.getString(), NamedTextColor.AQUA) + Component.text("Config ", NamedTextColor.GRAY), + Component.text(args[0], NamedTextColor.AQUA), + Component.text(" changed to ", NamedTextColor.GRAY), + Component.text(verifiedConfig.getString(), NamedTextColor.AQUA) )); } catch (IllegalArgumentException exception) { sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), - Component.text("Config ", NamedTextColor.GRAY), - Component.text(args[0], NamedTextColor.RED), - Component.text(" modify error by ", NamedTextColor.GRAY), - Component.text(exception.getMessage(), NamedTextColor.RED) + Component.text("Config ", NamedTextColor.GRAY), + Component.text(args[0], NamedTextColor.RED), + Component.text(" modify error by ", NamedTextColor.GRAY), + Component.text(exception.getMessage(), NamedTextColor.RED) )); } } else { sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), - Component.text("Config ", NamedTextColor.GRAY), - Component.text(args[0], NamedTextColor.AQUA), - Component.text(" value is ", NamedTextColor.GRAY), - Component.text(verifiedConfig.getString(), NamedTextColor.AQUA) + Component.text("Config ", NamedTextColor.GRAY), + Component.text(args[0], NamedTextColor.AQUA), + Component.text(" value is ", NamedTextColor.GRAY), + Component.text(verifiedConfig.getString(), NamedTextColor.AQUA) )); } @@ -63,22 +68,27 @@ public class ConfigCommand implements LeavesSubcommand { @Override public List tabComplete(CommandSender sender, String subCommand, String[] args, Location location) { - switch (args.length) { - case 1 -> { - List list = new ArrayList<>(GlobalConfigManager.getVerifiedConfigPaths()); - return LeavesCommandUtil.getListMatchingLast(sender, args, list); - } - - case 2 -> { - GlobalConfigManager.VerifiedConfig verifiedConfig = GlobalConfigManager.getVerifiedConfig(args[0]); - if (verifiedConfig != null) { - return LeavesCommandUtil.getListMatchingLast(sender, args, verifiedConfig.validator().valueSuggest()); - } else { - return Collections.singletonList(""); - } + if (args.length == 2) { + VerifiedConfig verifiedConfig = GlobalConfigManager.getVerifiedConfig(args[0]); + if (verifiedConfig != null) { + return LeavesCommandUtil.getListMatchingLast(sender, args, verifiedConfig.validator().valueSuggest()); + } else { + return Collections.singletonList(""); } } - return Collections.emptyList(); } + + @Override + public CompletableFuture tabSuggestion(CommandSender sender, String subCommand, String @NotNull [] args, @Nullable Location location, @NotNull SuggestionsBuilder builder) { + if (args.length == 1) { + String arg = args[0]; + int dotIndex = arg.lastIndexOf("."); + builder = builder.createOffset(builder.getInput().lastIndexOf(' ') + dotIndex + 2); + LeavesCommandUtil.getListClosestMatchingLast(sender, arg.substring(dotIndex + 1), GlobalConfigManager.getVerifiedConfigSubPaths(arg), "bukkit.command.leaves.config") + .forEach(builder::suggest); + return builder.buildFuture(); + } + return null; + } } diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/ReloadCommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/ReloadCommand.java index 74971828..11af3e58 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/ReloadCommand.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/ReloadCommand.java @@ -1,12 +1,9 @@ package org.leavesmc.leaves.command.subcommands; -import net.minecraft.server.MinecraftServer; -import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.leavesmc.leaves.LeavesConfig; import org.leavesmc.leaves.command.LeavesSubcommand; -import java.io.File; import static net.kyori.adventure.text.Component.text; import static net.kyori.adventure.text.format.NamedTextColor.GREEN; @@ -14,9 +11,8 @@ import static net.kyori.adventure.text.format.NamedTextColor.GREEN; public class ReloadCommand implements LeavesSubcommand { @Override public boolean execute(CommandSender sender, String subCommand, String[] args) { - MinecraftServer server = MinecraftServer.getServer(); - LeavesConfig.init((File) server.options.valueOf("leaves-settings")); - Command.broadcastCommandMessage(sender, text("Leaves config reload complete.", GREEN)); + LeavesConfig.reload(); + sender.sendMessage(text("Leaves config reload complete.", GREEN)); return false; } } diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/UpdateCommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/UpdateCommand.java index 7f94df60..6406e3fa 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/UpdateCommand.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/UpdateCommand.java @@ -1,6 +1,7 @@ package org.leavesmc.leaves.command.subcommands; -import org.bukkit.ChatColor; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.command.CommandSender; import org.leavesmc.leaves.command.LeavesSubcommand; import org.leavesmc.leaves.util.LeavesUpdateHelper; @@ -9,7 +10,7 @@ public class UpdateCommand implements LeavesSubcommand { @Override public boolean execute(CommandSender sender, String subCommand, String[] args) { - sender.sendMessage(ChatColor.GRAY + "Trying to update Leaves, see the console for more info."); + sender.sendMessage(Component.text("Trying to update Leaves, see the console for more info.", NamedTextColor.GRAY)); LeavesUpdateHelper.tryUpdateLeaves(); return true; } diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/config/ConfigValidator.java b/leaves-server/src/main/java/org/leavesmc/leaves/config/ConfigValidator.java index fe892adb..660561a3 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/config/ConfigValidator.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/config/ConfigValidator.java @@ -10,6 +10,6 @@ public interface ConfigValidator extends ConfigConverter { return List.of(""); } - default void runAfterLoader(E value, boolean firstLoad) { + default void runAfterLoader(E value, boolean reload) { } } diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfigCreator.java b/leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfigCreator.java index dfc38bd1..c5ee2e12 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfigCreator.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfigCreator.java @@ -4,6 +4,8 @@ import org.bukkit.configuration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.config.annotations.GlobalConfig; +import org.leavesmc.leaves.config.annotations.GlobalConfigCategory; import java.io.File; import java.io.IOException; @@ -75,7 +77,7 @@ public class GlobalConfigCreator { private static void initConfig(@NotNull Field field, GlobalConfig globalConfig, @Nullable Object upstreamField, @NotNull String upstreamPath) { try { - GlobalConfigManager.VerifiedConfig verifiedConfig = GlobalConfigManager.VerifiedConfig.build(globalConfig, field, upstreamField, upstreamPath); + VerifiedConfig verifiedConfig = VerifiedConfig.build(globalConfig, field, upstreamField, upstreamPath); config.set(verifiedConfig.path(), verifiedConfig.validator().saveConvert(field.get(upstreamField))); } catch (Exception e) { e.printStackTrace(); diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfigManager.java b/leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfigManager.java index 4c9aab7d..968ee856 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfigManager.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfigManager.java @@ -1,13 +1,15 @@ package org.leavesmc.leaves.config; -import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.leavesmc.leaves.LeavesConfig; import org.leavesmc.leaves.LeavesLogger; +import org.leavesmc.leaves.config.annotations.GlobalConfig; +import org.leavesmc.leaves.config.annotations.GlobalConfigCategory; import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -16,19 +18,34 @@ public class GlobalConfigManager { public final static String CONFIG_START = "settings."; - private static boolean firstLoad = true; private static final Map verifiedConfigs = new HashMap<>(); + private static final ConfigNode rootNode = new ConfigNode(""); + + private static boolean loaded = false; public static void init() { - verifiedConfigs.clear(); + if (loaded) { + return; + } for (Field field : LeavesConfig.class.getDeclaredFields()) { initField(field, null, CONFIG_START); } + verifiedConfigs.forEach((path, config) -> config.validator().runAfterLoader(config.get(), false)); + LeavesConfig.save(); - verifiedConfigs.forEach((path, config) -> config.validator.runAfterLoader(config.get(), firstLoad)); + loaded = true; + } - firstLoad = false; + public static void reload() { + if (!loaded) { + return; + } + + for (Field field : LeavesConfig.class.getDeclaredFields()) { + initField(field, null, CONFIG_START); + } + verifiedConfigs.forEach((path, config) -> config.validator().runAfterLoader(config.get(), true)); LeavesConfig.save(); } @@ -39,6 +56,7 @@ public class GlobalConfigManager { for (Field field : categoryField.getType().getDeclaredFields()) { initField(field, category, categoryPath); } + traverseToNodeOrCreate(categoryPath.substring(CONFIG_START.length())); } catch (Exception e) { LeavesLogger.LOGGER.severe("Failure to load leaves config" + upstreamPath, e); } @@ -48,8 +66,8 @@ public class GlobalConfigManager { if (upstreamField != null || Modifier.isStatic(field.getModifiers())) { field.setAccessible(true); - for (RemovedConfig config : field.getAnnotationsByType(RemovedConfig.class)) { - RemovedVerifiedConfig.build(config, field, upstreamField).run(); + for (org.leavesmc.leaves.config.annotations.RemovedConfig config : field.getAnnotationsByType(org.leavesmc.leaves.config.annotations.RemovedConfig.class)) { + VerifiedRemovedConfig.build(config, field, upstreamField).run(); } GlobalConfig globalConfig = field.getAnnotation(GlobalConfig.class); @@ -67,19 +85,20 @@ public class GlobalConfigManager { private static void initConfig(@NotNull Field field, GlobalConfig globalConfig, @Nullable Object upstreamField, @NotNull String upstreamPath) { try { - VerifiedConfig verifiedConfig = VerifiedConfig.build(globalConfig, field, upstreamField, upstreamPath); - - if (globalConfig.lock() && !firstLoad) { - verifiedConfigs.put(verifiedConfig.path.substring(CONFIG_START.length()), verifiedConfig); + if (loaded && globalConfig.lock()) { + return; } - ConfigValidator validator = verifiedConfig.validator; + VerifiedConfig verifiedConfig = VerifiedConfig.build(globalConfig, field, upstreamField, upstreamPath); + + ConfigValidator validator = verifiedConfig.validator(); + String path = verifiedConfig.path(); Object defValue = validator.saveConvert(field.get(upstreamField)); - LeavesConfig.config.addDefault(verifiedConfig.path, defValue); + LeavesConfig.config.addDefault(path, defValue); try { - Object savedValue = LeavesConfig.config.get(verifiedConfig.path); + Object savedValue = LeavesConfig.config.get(path); if (savedValue == null) { throw new IllegalArgumentException("?"); } @@ -92,11 +111,12 @@ public class GlobalConfigManager { field.set(upstreamField, savedValue); } catch (IllegalArgumentException | ClassCastException e) { - LeavesConfig.config.set(verifiedConfig.path, defValue); + LeavesConfig.config.set(path, defValue); LeavesLogger.LOGGER.warning(e.getMessage() + ", reset to " + defValue); } - verifiedConfigs.put(verifiedConfig.path.substring(CONFIG_START.length()), verifiedConfig); + verifiedConfigs.put(path.substring(CONFIG_START.length()), verifiedConfig); + traverseToNodeOrCreate(path.substring(CONFIG_START.length())); } catch (Exception e) { LeavesLogger.LOGGER.severe("Failure to load leaves config", e); throw new RuntimeException(); @@ -107,143 +127,49 @@ public class GlobalConfigManager { return verifiedConfigs.get(path); } - @Contract(pure = true) - public static @NotNull Set getVerifiedConfigPaths() { - return verifiedConfigs.keySet(); - } + private static class ConfigNode { + String name; + Map children; - public record RemovedVerifiedConfig(ConfigTransformer transformer, boolean transform, Field field, Object upstreamField, String path) { - - public void run() { - if (transform) { - if (LeavesConfig.config.contains(path)) { - Object savedValue = LeavesConfig.config.get(path); - if (savedValue != null) { - try { - if (savedValue.getClass() != transformer.getFieldClass()) { - savedValue = transformer.loadConvert(savedValue); - } - savedValue = transformer.transform(savedValue); - field.set(upstreamField, savedValue); - } catch (IllegalAccessException | IllegalArgumentException e) { - LeavesLogger.LOGGER.warning("Failure to load leaves config" + path, e); - } - } else { - LeavesLogger.LOGGER.warning("Failed to convert saved value for " + path + ", reset to default"); - } - } - } - LeavesConfig.config.set(path, null); - } - - @Contract("_, _, _ -> new") - public static @NotNull RemovedVerifiedConfig build(@NotNull RemovedConfig config, @NotNull Field field, @Nullable Object upstreamField) { - StringBuilder path = new StringBuilder("settings."); - for (String category : config.category()) { - path.append(category).append("."); - } - path.append(config.name()); - - ConfigTransformer transformer = null; - try { - transformer = createTransformer(config.transformer(), field); - } catch (Exception e) { - LeavesLogger.LOGGER.warning("Failure to load leaves config" + path, e); - } - - return new RemovedVerifiedConfig(transformer, config.transform(), field, upstreamField, path.toString()); + public ConfigNode(String name) { + this.name = name; + this.children = new HashMap<>(); } } - public record VerifiedConfig(ConfigValidator validator, boolean lock, Field field, Object upstreamField, String path) { + private static void traverseToNodeOrCreate(@NotNull String path) { + String[] parts = path.split("\\."); + ConfigNode current = rootNode; - public void set(String stringValue) throws IllegalArgumentException { - Object value; - try { - value = validator.stringConvert(stringValue); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("value parse error: " + e.getMessage()); + for (String part : parts) { + if (!part.isEmpty()) { + current = current.children.computeIfAbsent(part, ConfigNode::new); } - - validator.verify(this.get(), value); - - try { - LeavesConfig.config.set(path, validator.saveConvert(value)); - LeavesConfig.save(); - if (lock) { - throw new IllegalArgumentException("locked, will load after restart"); - } - field.set(upstreamField, value); - } catch (IllegalAccessException e) { - throw new IllegalArgumentException(e.getMessage()); - } - } - - public Object get() { - try { - return field.get(upstreamField); - } catch (IllegalAccessException e) { - LeavesLogger.LOGGER.severe("Failure to get " + path + " value", e); - return ""; - } - } - - public String getString() { - Object value = this.get(); - - Object savedValue = LeavesConfig.config.get(path); - try { - if (savedValue != null) { - if (validator.getFieldClass() != savedValue.getClass()) { - savedValue = validator.loadConvert(savedValue); - } - - if (!savedValue.equals(value)) { - return value.toString() + "(" + savedValue + " after restart)"; - } - } - } catch (IllegalArgumentException e) { - LeavesLogger.LOGGER.severe("Failure to get " + path + " value", e); - } - return value.toString(); - } - - @NotNull - @Contract("_, _, _, _ -> new") - public static VerifiedConfig build(@NotNull GlobalConfig config, @NotNull Field field, @Nullable Object upstreamField, @NotNull String upstreamPath) { - String path = upstreamPath + config.value(); - - ConfigValidator validator; - try { - validator = createValidator(config.validator(), field); - } catch (Exception e) { - LeavesLogger.LOGGER.severe("Failure to load leaves config" + path, e); - throw new RuntimeException(); - } - - return new VerifiedConfig(validator, config.lock(), field, upstreamField, path); } } - @SuppressWarnings("unchecked") - private static ConfigValidator createValidator(@NotNull Class> clazz, Field field) throws Exception { - if (clazz.equals(AutoConfigValidator.class)) { - return (ConfigValidator) AutoConfigValidator.createValidator(field); - } else { - var constructor = clazz.getDeclaredConstructor(); - constructor.setAccessible(true); - return (ConfigValidator) constructor.newInstance(); + @Nullable + private static ConfigNode traverseToNode(@NotNull String path) { + int index = path.lastIndexOf('.'); + String[] parts = index == -1 ? new String[0] : path.substring(0, index).split("\\."); + + ConfigNode current = rootNode; + for (String part : parts) { + current = current.children.get(part); + if (current == null) { + return null; + } } + + return current; } - @SuppressWarnings("unchecked") - private static ConfigTransformer createTransformer(@NotNull Class> clazz, Field field) throws Exception { - if (clazz.equals(AutoConfigTransformer.class)) { - return (ConfigTransformer) AutoConfigTransformer.createValidator(field); - } else { - var constructor = clazz.getDeclaredConstructor(); - constructor.setAccessible(true); - return (ConfigTransformer) constructor.newInstance(); + @NotNull + public static Set getVerifiedConfigSubPaths(@NotNull String prefix) { + ConfigNode current = traverseToNode(prefix); + if (current == null) { + return Collections.emptySet(); } + return current.children.keySet(); } } diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/config/VerifiedConfig.java b/leaves-server/src/main/java/org/leavesmc/leaves/config/VerifiedConfig.java new file mode 100644 index 00000000..7af4b31c --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/config/VerifiedConfig.java @@ -0,0 +1,91 @@ +package org.leavesmc.leaves.config; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.LeavesLogger; +import org.leavesmc.leaves.config.annotations.GlobalConfig; + +import java.lang.reflect.Field; + +public record VerifiedConfig(ConfigValidator validator, boolean lock, Field field, Object upstreamField, String path) { + + public void set(String stringValue) throws IllegalArgumentException { + Object value; + try { + value = validator.stringConvert(stringValue); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("value parse error: " + e.getMessage()); + } + + validator.verify(this.get(), value); + + try { + LeavesConfig.config.set(path, validator.saveConvert(value)); + LeavesConfig.save(); + if (lock) { + throw new IllegalArgumentException("locked, will load after restart"); + } + field.set(upstreamField, value); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException(e.getMessage()); + } + } + + public Object get() { + try { + return field.get(upstreamField); + } catch (IllegalAccessException e) { + LeavesLogger.LOGGER.severe("Failure to get " + path + " value", e); + return ""; + } + } + + public String getString() { + Object value = this.get(); + + Object savedValue = LeavesConfig.config.get(path); + try { + if (savedValue != null) { + if (validator.getFieldClass() != savedValue.getClass()) { + savedValue = validator.loadConvert(savedValue); + } + + if (!savedValue.equals(value)) { + return value.toString() + "(" + savedValue + " after restart)"; + } + } + } catch (IllegalArgumentException e) { + LeavesLogger.LOGGER.severe("Failure to get " + path + " value", e); + } + return value.toString(); + } + + @NotNull + @Contract("_, _, _, _ -> new") + public static VerifiedConfig build(@NotNull GlobalConfig config, @NotNull Field field, @Nullable Object upstreamField, @NotNull String upstreamPath) { + String path = upstreamPath + config.value(); + + ConfigValidator validator; + try { + validator = createValidator(config.validator(), field); + } catch (Exception e) { + LeavesLogger.LOGGER.severe("Failure to load leaves config" + path, e); + throw new RuntimeException(); + } + + return new VerifiedConfig(validator, config.lock(), field, upstreamField, path); + } + + @SuppressWarnings("unchecked") + private static ConfigValidator createValidator(@NotNull Class> clazz, Field field) throws Exception { + if (clazz.equals(AutoConfigValidator.class)) { + return (ConfigValidator) AutoConfigValidator.createValidator(field); + } else { + var constructor = clazz.getDeclaredConstructor(); + constructor.setAccessible(true); + return (ConfigValidator) constructor.newInstance(); + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/config/VerifiedRemovedConfig.java b/leaves-server/src/main/java/org/leavesmc/leaves/config/VerifiedRemovedConfig.java new file mode 100644 index 00000000..bc264808 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/config/VerifiedRemovedConfig.java @@ -0,0 +1,63 @@ +package org.leavesmc.leaves.config; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.LeavesLogger; + +import java.lang.reflect.Field; + +public record VerifiedRemovedConfig(ConfigTransformer transformer, boolean transform, Field field, Object upstreamField, String path) { + + public void run() { + if (transform) { + if (LeavesConfig.config.contains(path)) { + Object savedValue = LeavesConfig.config.get(path); + if (savedValue != null) { + try { + if (savedValue.getClass() != transformer.getFieldClass()) { + savedValue = transformer.loadConvert(savedValue); + } + savedValue = transformer.transform(savedValue); + field.set(upstreamField, savedValue); + } catch (IllegalAccessException | IllegalArgumentException e) { + LeavesLogger.LOGGER.warning("Failure to load leaves config" + path, e); + } + } else { + LeavesLogger.LOGGER.warning("Failed to convert saved value for " + path + ", reset to default"); + } + } + } + LeavesConfig.config.set(path, null); + } + + @Contract("_, _, _ -> new") + public static @NotNull VerifiedRemovedConfig build(@NotNull org.leavesmc.leaves.config.annotations.RemovedConfig config, @NotNull Field field, @Nullable Object upstreamField) { + StringBuilder path = new StringBuilder("settings."); + for (String category : config.category()) { + path.append(category).append("."); + } + path.append(config.name()); + + ConfigTransformer transformer = null; + try { + transformer = createTransformer(config.transformer(), field); + } catch (Exception e) { + LeavesLogger.LOGGER.warning("Failure to load leaves config" + path, e); + } + + return new VerifiedRemovedConfig(transformer, config.transform(), field, upstreamField, path.toString()); + } + + @SuppressWarnings("unchecked") + private static ConfigTransformer createTransformer(@NotNull Class> clazz, Field field) throws Exception { + if (clazz.equals(AutoConfigTransformer.class)) { + return (ConfigTransformer) AutoConfigTransformer.createValidator(field); + } else { + var constructor = clazz.getDeclaredConstructor(); + constructor.setAccessible(true); + return (ConfigTransformer) constructor.newInstance(); + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfig.java b/leaves-server/src/main/java/org/leavesmc/leaves/config/annotations/GlobalConfig.java similarity index 72% rename from leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfig.java rename to leaves-server/src/main/java/org/leavesmc/leaves/config/annotations/GlobalConfig.java index ce752fe8..a566b3e4 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfig.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/config/annotations/GlobalConfig.java @@ -1,4 +1,7 @@ -package org.leavesmc.leaves.config; +package org.leavesmc.leaves.config.annotations; + +import org.leavesmc.leaves.config.AutoConfigValidator; +import org.leavesmc.leaves.config.ConfigValidator; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfigCategory.java b/leaves-server/src/main/java/org/leavesmc/leaves/config/annotations/GlobalConfigCategory.java similarity index 85% rename from leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfigCategory.java rename to leaves-server/src/main/java/org/leavesmc/leaves/config/annotations/GlobalConfigCategory.java index 1e109a8b..a3d6b933 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfigCategory.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/config/annotations/GlobalConfigCategory.java @@ -1,4 +1,4 @@ -package org.leavesmc.leaves.config; +package org.leavesmc.leaves.config.annotations; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/config/RemovedConfig.java b/leaves-server/src/main/java/org/leavesmc/leaves/config/annotations/RemovedConfig.java similarity index 80% rename from leaves-server/src/main/java/org/leavesmc/leaves/config/RemovedConfig.java rename to leaves-server/src/main/java/org/leavesmc/leaves/config/annotations/RemovedConfig.java index aeca5c34..251a163d 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/config/RemovedConfig.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/config/annotations/RemovedConfig.java @@ -1,4 +1,7 @@ -package org.leavesmc.leaves.config; +package org.leavesmc.leaves.config.annotations; + +import org.leavesmc.leaves.config.AutoConfigTransformer; +import org.leavesmc.leaves.config.ConfigTransformer; import java.lang.annotation.ElementType; import java.lang.annotation.Repeatable;