From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: MrHua269 Date: Sun, 12 Jan 2025 10:15:53 +0800 Subject: [PATCH] Add luminol config framework diff --git a/src/main/java/me/earthme/luminol/commands/LuminolConfigCommand.java b/src/main/java/me/earthme/luminol/commands/LuminolConfigCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..5c8745dffa80cf47e856d04d283937bda86881f8 --- /dev/null +++ b/src/main/java/me/earthme/luminol/commands/LuminolConfigCommand.java @@ -0,0 +1,70 @@ +package me.earthme.luminol.commands; + +import me.earthme.luminol.config.LuminolConfig; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; +import org.bukkit.Location; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class LuminolConfigCommand extends Command { + public LuminolConfigCommand(){ + super("luminolconfig"); + this.setPermission("luminol.commands.luminolconfig"); + this.setDescription("Manage config file"); + this.setUsage("/luminolconfig"); + } + + @Override + public @NotNull List tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args, @Nullable Location location) throws IllegalArgumentException { + final List result = new ArrayList<>(); + + if (args.length == 1){ + result.add("reload"); + } + + return result; + } + + @Override + public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String[] args) { + if (!this.testPermission(sender)){ + sender.sendMessage(Component + .text("No permission to execute this command!") + .color(TextColor.color(255,0,0)) + ); + } + + if (args.length < 1){ + sender.sendMessage( + Component + .text("Wrong use!\n") + .color(TextColor.color(255,0,0)) + ); + return true; + } + + switch (args[0]){ + case "reload" -> { + LuminolConfig.reloadAsync().thenAccept(nullValue -> sender.sendMessage( + Component + .text("Reloaded config file!") + .color(TextColor.color(0,255,0)) + )); + } + + default -> sender.sendMessage( + Component + .text("Unknown action!\n") + .color(TextColor.color(255,0,0)) + ); + } + + return true; + } +} diff --git a/src/main/java/me/earthme/luminol/config/ConfigInfo.java b/src/main/java/me/earthme/luminol/config/ConfigInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..01b64c2cf6b437114337626c242e1da3fbdb8ead --- /dev/null +++ b/src/main/java/me/earthme/luminol/config/ConfigInfo.java @@ -0,0 +1,11 @@ +package me.earthme.luminol.config; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface ConfigInfo { + String baseName(); + + String comments() default ""; +} diff --git a/src/main/java/me/earthme/luminol/config/DoNotLoad.java b/src/main/java/me/earthme/luminol/config/DoNotLoad.java new file mode 100644 index 0000000000000000000000000000000000000000..fffc5eb4be4b78a886f3c340bd60f3a2b0108a7d --- /dev/null +++ b/src/main/java/me/earthme/luminol/config/DoNotLoad.java @@ -0,0 +1,8 @@ +package me.earthme.luminol.config; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface DoNotLoad { +} diff --git a/src/main/java/me/earthme/luminol/config/EnumConfigCategory.java b/src/main/java/me/earthme/luminol/config/EnumConfigCategory.java new file mode 100644 index 0000000000000000000000000000000000000000..7b75405c468d24ed8aea5aa54ae5ac339118bf60 --- /dev/null +++ b/src/main/java/me/earthme/luminol/config/EnumConfigCategory.java @@ -0,0 +1,19 @@ +package me.earthme.luminol.config; + +public enum EnumConfigCategory { + OPTIMIZATIONS("optimizations"), + FIXES("fixes"), + MISC("misc"), + GAMEPLAY("gameplay"), + EXPERIMENT("experiment"); + + private final String baseKeyName; + + EnumConfigCategory(String baseKeyName) { + this.baseKeyName = baseKeyName; + } + + public String getBaseKeyName() { + return this.baseKeyName; + } +} diff --git a/src/main/java/me/earthme/luminol/config/HotReloadUnsupported.java b/src/main/java/me/earthme/luminol/config/HotReloadUnsupported.java new file mode 100644 index 0000000000000000000000000000000000000000..559c11cb523c7cade34a0abfad15c988f5ad87fe --- /dev/null +++ b/src/main/java/me/earthme/luminol/config/HotReloadUnsupported.java @@ -0,0 +1,8 @@ +package me.earthme.luminol.config; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface HotReloadUnsupported { +} diff --git a/src/main/java/me/earthme/luminol/config/IConfigModule.java b/src/main/java/me/earthme/luminol/config/IConfigModule.java new file mode 100644 index 0000000000000000000000000000000000000000..9f6896711907ac30fe0c00130207b970007e4bb4 --- /dev/null +++ b/src/main/java/me/earthme/luminol/config/IConfigModule.java @@ -0,0 +1,22 @@ +package me.earthme.luminol.config; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import org.jetbrains.annotations.NotNull; + +public interface IConfigModule { + + EnumConfigCategory getCategory(); + + String getBaseName(); + + default void onLoaded(CommentedFileConfig configInstance) {} + + default T get(String keyName, T defaultValue, @NotNull CommentedFileConfig config){ + if (!config.contains(keyName)){ + config.set(keyName,defaultValue); + return defaultValue; + } + + return config.get(keyName); + } +} diff --git a/src/main/java/me/earthme/luminol/config/LuminolConfig.java b/src/main/java/me/earthme/luminol/config/LuminolConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..526b68e184a2f6f9e38cd02995b473a943404141 --- /dev/null +++ b/src/main/java/me/earthme/luminol/config/LuminolConfig.java @@ -0,0 +1,226 @@ +package me.earthme.luminol.config; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import io.papermc.paper.threadedregions.RegionizedServer; +import me.earthme.luminol.commands.LuminolConfigCommand; +import org.bukkit.Bukkit; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class LuminolConfig { + public static final Logger logger = LogManager.getLogger(); + private static final File baseConfigFolder = new File("luminol_config"); + private static final File baseConfigFile = new File(baseConfigFolder,"luminol_global_config.toml"); + private static final Set allInstanced = new HashSet<>(); + private static CommentedFileConfig configFileInstance; + public static boolean alreadyInited = false; + + public static void setupLatch(){ + Bukkit.getCommandMap().register("luminolconfig","luminol",new LuminolConfigCommand()); + alreadyInited = true; + } + + public static void reload(){ + RegionizedServer.ensureGlobalTickThread("Reload luminol config off global region thread!"); + + dropAllInstanced(); + try { + preLoadConfig(); + finalizeLoadConfig(); + }catch (Exception e){ + logger.error(e); + } + } + + @Contract(" -> new") + public static @NotNull CompletableFuture reloadAsync(){ + return CompletableFuture.runAsync(LuminolConfig::reload,task -> RegionizedServer.getInstance().addTask(() -> { + try{ + task.run(); + }catch (Exception e){ + logger.error(e); + } + })); + } + + public static void dropAllInstanced(){ + allInstanced.clear(); + } + + public static void finalizeLoadConfig() { + for (IConfigModule module : allInstanced) { + module.onLoaded(configFileInstance); + } + } + + public static void preLoadConfig() throws IOException { + baseConfigFolder.mkdirs(); + + if (!baseConfigFile.exists()){ + baseConfigFile.createNewFile(); + } + + configFileInstance = CommentedFileConfig.ofConcurrent(baseConfigFile); + + configFileInstance.load(); + + try { + instanceAllModule(); + loadAllModules(); + }catch (Exception e){ + logger.error("Failed to load config modules!",e); + throw new RuntimeException(e); + } + + configFileInstance.save(); + } + + private static void loadAllModules() throws IllegalAccessException { + for (IConfigModule instanced : allInstanced){ + loadForSingle(instanced); + } + } + + private static void instanceAllModule() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { + for (Class clazz : getClasses("me.earthme.luminol.config.modules")){ + if (IConfigModule.class.isAssignableFrom(clazz)){ + allInstanced.add((IConfigModule) clazz.getConstructor().newInstance()); + } + } + } + + private static void loadForSingle(@NotNull IConfigModule singleConfigModule) throws IllegalAccessException { + final EnumConfigCategory category = singleConfigModule.getCategory(); + + Field[] fields = singleConfigModule.getClass().getDeclaredFields(); + + for (Field field : fields) { + int modifiers = field.getModifiers(); + if (Modifier.isStatic(modifiers) && !Modifier.isFinal(modifiers)) { + boolean skipLoad = field.getAnnotation(DoNotLoad.class) != null || (alreadyInited && field.getAnnotation(HotReloadUnsupported.class) != null); + ConfigInfo configInfo = field.getAnnotation(ConfigInfo.class); + + if (skipLoad || configInfo == null){ + continue; + } + + final String fullConfigKeyName = category.getBaseKeyName() + "." + singleConfigModule.getBaseName() + "." + configInfo.baseName(); + + field.setAccessible(true); + final Object currentValue = field.get(null); + + if (!configFileInstance.contains(fullConfigKeyName)){ + if (currentValue == null){ + throw new UnsupportedOperationException("Config " + singleConfigModule.getBaseName() + "tried to add an null default value!"); + } + + final String comments = configInfo.comments(); + + if (!comments.isBlank()){ + configFileInstance.setComment(fullConfigKeyName,comments); + } + + configFileInstance.add(fullConfigKeyName,currentValue); + continue; + } + + final Object actuallyValue = configFileInstance.get(fullConfigKeyName); + field.set(null,actuallyValue); + } + } + } + + public static @NotNull Set> getClasses(String pack) { + Set> classes = new LinkedHashSet<>(); + String packageDirName = pack.replace('.', '/'); + Enumeration dirs; + + try { + dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName); + while (dirs.hasMoreElements()) { + URL url = dirs.nextElement(); + String protocol = url.getProtocol(); + if ("file".equals(protocol)) { + String filePath = URLDecoder.decode(url.getFile(), StandardCharsets.UTF_8); + findClassesInPackageByFile(pack, filePath, classes); + } else if ("jar".equals(protocol)) { + JarFile jar; + try { + jar = ((JarURLConnection) url.openConnection()).getJarFile(); + Enumeration entries = jar.entries(); + findClassesInPackageByJar(pack, entries, packageDirName, classes); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + return classes; + } + + private static void findClassesInPackageByFile(String packageName, String packagePath, Set> classes) { + File dir = new File(packagePath); + + if (!dir.exists() || !dir.isDirectory()) { + return; + } + + File[] dirfiles = dir.listFiles((file) -> file.isDirectory() || file.getName().endsWith(".class")); + if (dirfiles != null) { + for (File file : dirfiles) { + if (file.isDirectory()) { + findClassesInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), classes); + } else { + String className = file.getName().substring(0, file.getName().length() - 6); + try { + classes.add(Class.forName(packageName + '.' + className)); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + } + } + } + + private static void findClassesInPackageByJar(String packageName, Enumeration entries, String packageDirName, Set> classes) { + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String name = entry.getName(); + if (name.charAt(0) == '/') { + name = name.substring(1); + } + if (name.startsWith(packageDirName)) { + int idx = name.lastIndexOf('/'); + if (idx != -1) { + packageName = name.substring(0, idx).replace('/', '.'); + } + if (name.endsWith(".class") && !entry.isDirectory()) { + String className = name.substring(packageName.length() + 1, name.length() - 6); + try { + classes.add(Class.forName(packageName + '.' + className)); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + } + } + } +} diff --git a/src/main/java/me/earthme/luminol/utils/NullPlugin.java b/src/main/java/me/earthme/luminol/utils/NullPlugin.java new file mode 100644 index 0000000000000000000000000000000000000000..de94c8e39f0ae0da80d5a79af63413e287f5d190 --- /dev/null +++ b/src/main/java/me/earthme/luminol/utils/NullPlugin.java @@ -0,0 +1,152 @@ +package me.earthme.luminol.utils; + +import io.papermc.paper.plugin.configuration.PluginMeta; +import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager; +import org.bukkit.Server; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.generator.BiomeProvider; +import org.bukkit.generator.ChunkGenerator; +import org.bukkit.plugin.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.InputStream; +import java.util.List; +import java.util.logging.Logger; + +public class NullPlugin extends PluginBase { + private boolean enabled = true; + + private final String pluginName; + private PluginDescriptionFile pdf; + + public NullPlugin() { + this.pluginName = "Minecraft"; + pdf = new PluginDescriptionFile(pluginName, "1.0", "nms"); + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + public File getDataFolder() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public PluginDescriptionFile getDescription() { + return pdf; + } + // Paper start + @Override + public io.papermc.paper.plugin.configuration.PluginMeta getPluginMeta() { + return pdf; + } + // Paper end + + @Override + public FileConfiguration getConfig() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public InputStream getResource(String filename) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void saveConfig() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void saveDefaultConfig() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void saveResource(String resourcePath, boolean replace) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void reloadConfig() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public PluginLogger getLogger() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public PluginLoader getPluginLoader() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public Server getServer() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public void onDisable() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void onLoad() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void onEnable() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public boolean isNaggable() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void setNaggable(boolean canNag) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public @Nullable BiomeProvider getDefaultBiomeProvider(@NotNull String worldName, @Nullable String id) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + throw new UnsupportedOperationException("Not supported."); + } + + // Paper start - lifecycle events + @Override + public @NotNull io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager getLifecycleManager() { + throw new UnsupportedOperationException("Not supported."); + } + // Paper end - lifecycle events +} \ No newline at end of file