mirror of
https://github.com/LeavesMC/Leaves.git
synced 2025-12-28 19:39:22 +00:00
Better Leaves config command
This commit is contained in:
@@ -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<AlternativePlaceType> {
|
||||
@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!");
|
||||
|
||||
@@ -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<String, LeavesSubcommand> SUBCOMMANDS = Util.make(() -> {
|
||||
final Map<Set<String>, 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<String> 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<String> 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<String, LeavesSubcommand> subCommand = resolveCommand(args[0]);
|
||||
@@ -83,6 +88,18 @@ public final class LeavesCommand extends Command {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public CompletableFuture<Suggestions> 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<String, LeavesSubcommand> 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<String, LeavesSubcommand> 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<String> usableSubcommands() {
|
||||
List<String> 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<String, LeavesSubcommand> resolveCommand(String label) {
|
||||
label = label.toLowerCase(Locale.ENGLISH);
|
||||
@@ -118,4 +149,5 @@ public final class LeavesCommand extends Command {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<String> getListClosestMatchingLast(
|
||||
final CommandSender sender,
|
||||
final String last,
|
||||
final Collection<?> collection,
|
||||
final String overridePermission
|
||||
) {
|
||||
ArrayList<Candidate> 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<String> 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 <a href="http://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance">Wikipedia: Damerau–Levenshtein distance</a>
|
||||
*
|
||||
* @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<Character, Integer> 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];
|
||||
}
|
||||
}
|
||||
@@ -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<Suggestions> tabSuggestion(final CommandSender sender, final String subCommand, final String[] args, final Location location, final SuggestionsBuilder builder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
default boolean tabCompletes() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -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<Suggestions> tabSuggestion(@NotNull CommandSender sender, @NotNull String alias, @NotNull String @NotNull [] args, @Nullable Location location, @NotNull SuggestionsBuilder builder) throws IllegalArgumentException;
|
||||
}
|
||||
@@ -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<String> tabComplete(CommandSender sender, String subCommand, String[] args, Location location) {
|
||||
switch (args.length) {
|
||||
case 1 -> {
|
||||
List<String> 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("<ERROR CONFIG>");
|
||||
}
|
||||
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("<ERROR CONFIG>");
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Suggestions> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,6 @@ public interface ConfigValidator<E> extends ConfigConverter<E> {
|
||||
return List.of("<value>");
|
||||
}
|
||||
|
||||
default void runAfterLoader(E value, boolean firstLoad) {
|
||||
default void runAfterLoader(E value, boolean reload) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<String, VerifiedConfig> 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<? super Object> validator = verifiedConfig.validator;
|
||||
VerifiedConfig verifiedConfig = VerifiedConfig.build(globalConfig, field, upstreamField, upstreamPath);
|
||||
|
||||
ConfigValidator<? super Object> 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<String> getVerifiedConfigPaths() {
|
||||
return verifiedConfigs.keySet();
|
||||
}
|
||||
private static class ConfigNode {
|
||||
String name;
|
||||
Map<String, ConfigNode> children;
|
||||
|
||||
public record RemovedVerifiedConfig(ConfigTransformer<? super Object, ? super Object> 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<? super Object, ? super Object> 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<? super Object> 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 "<VALUE ERROR>";
|
||||
}
|
||||
}
|
||||
|
||||
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<? super Object> 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<? super Object> createValidator(@NotNull Class<? extends ConfigValidator<?>> clazz, Field field) throws Exception {
|
||||
if (clazz.equals(AutoConfigValidator.class)) {
|
||||
return (ConfigValidator<? super Object>) AutoConfigValidator.createValidator(field);
|
||||
} else {
|
||||
var constructor = clazz.getDeclaredConstructor();
|
||||
constructor.setAccessible(true);
|
||||
return (ConfigValidator<? super Object>) 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<? super Object, ? super Object> createTransformer(@NotNull Class<? extends ConfigTransformer<?, ?>> clazz, Field field) throws Exception {
|
||||
if (clazz.equals(AutoConfigTransformer.class)) {
|
||||
return (ConfigTransformer<? super Object, ? super Object>) AutoConfigTransformer.createValidator(field);
|
||||
} else {
|
||||
var constructor = clazz.getDeclaredConstructor();
|
||||
constructor.setAccessible(true);
|
||||
return (ConfigTransformer<? super Object, ? super Object>) constructor.newInstance();
|
||||
@NotNull
|
||||
public static Set<String> getVerifiedConfigSubPaths(@NotNull String prefix) {
|
||||
ConfigNode current = traverseToNode(prefix);
|
||||
if (current == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
return current.children.keySet();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<? super Object> 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 "<VALUE ERROR>";
|
||||
}
|
||||
}
|
||||
|
||||
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<? super Object> 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<? super Object> createValidator(@NotNull Class<? extends ConfigValidator<?>> clazz, Field field) throws Exception {
|
||||
if (clazz.equals(AutoConfigValidator.class)) {
|
||||
return (ConfigValidator<? super Object>) AutoConfigValidator.createValidator(field);
|
||||
} else {
|
||||
var constructor = clazz.getDeclaredConstructor();
|
||||
constructor.setAccessible(true);
|
||||
return (ConfigValidator<? super Object>) constructor.newInstance();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<? super Object, ? super Object> 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<? super Object, ? super Object> 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<? super Object, ? super Object> createTransformer(@NotNull Class<? extends ConfigTransformer<?, ?>> clazz, Field field) throws Exception {
|
||||
if (clazz.equals(AutoConfigTransformer.class)) {
|
||||
return (ConfigTransformer<? super Object, ? super Object>) AutoConfigTransformer.createValidator(field);
|
||||
} else {
|
||||
var constructor = clazz.getDeclaredConstructor();
|
||||
constructor.setAccessible(true);
|
||||
return (ConfigTransformer<? super Object, ? super Object>) constructor.newInstance();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user