From 216945eb55aa020da21a1731e584b581229b8617 Mon Sep 17 00:00:00 2001 From: xpple Date: Wed, 11 Sep 2024 18:04:03 +0200 Subject: [PATCH 1/3] Add onChange attribute --- .../dev/xpple/betterconfig/api/Config.java | 20 ++++++ .../impl/BetterConfigInternals.java | 68 ++++++++++++++----- .../betterconfig/impl/ModConfigImpl.java | 32 ++++++--- .../betterconfig/util/CheckedRunnable.java | 6 ++ .../java/dev/xpple/betterconfig/Configs.java | 6 ++ .../java/dev/xpple/betterconfig/Configs.java | 6 ++ 6 files changed, 109 insertions(+), 29 deletions(-) create mode 100644 common/src/main/java/dev/xpple/betterconfig/util/CheckedRunnable.java diff --git a/common/src/main/java/dev/xpple/betterconfig/api/Config.java b/common/src/main/java/dev/xpple/betterconfig/api/Config.java index 7eb0b25..578dc84 100644 --- a/common/src/main/java/dev/xpple/betterconfig/api/Config.java +++ b/common/src/main/java/dev/xpple/betterconfig/api/Config.java @@ -53,6 +53,24 @@ *

* *

+ * To track changes to the configs, you can use the {@link Config#onChange()} attribute. + * Its value represents the name of a method where the changes can be tracked. The method + * should have two parameters; one for the old value and one for the new value. For + * example: + *

+ *     {@code
+ *     @Config(onChange = "exampleOnChange")
+ *     public static String exampleString = "defaultString";
+ *     public static void exampleOnChange(String oldValue, String newValue) {
+ *         System.out.println("Old: " + oldValue + ", new: " + newValue);
+ *     }
+ *     }
+ *     
+ * Both values are deep copies of the config that was changed, so they can be modified + * freely without care for the original object. + *

+ * + *

* To make a configuration unmodifiable by commands, mark it with {@code readOnly = true}. * Enabling this will ignore all update annotations. To make a configuration temporary, * that is, to disable loading and saving from a config file, set {@code temporary} to @@ -83,6 +101,8 @@ Putter putter() default @Putter; Remover remover() default @Remover; + String onChange() default ""; + boolean readOnly() default false; boolean temporary() default false; String condition() default ""; diff --git a/common/src/main/java/dev/xpple/betterconfig/impl/BetterConfigInternals.java b/common/src/main/java/dev/xpple/betterconfig/impl/BetterConfigInternals.java index 73cdf3b..feb0ec8 100644 --- a/common/src/main/java/dev/xpple/betterconfig/impl/BetterConfigInternals.java +++ b/common/src/main/java/dev/xpple/betterconfig/impl/BetterConfigInternals.java @@ -5,6 +5,7 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException; import dev.xpple.betterconfig.BetterConfigCommon; import dev.xpple.betterconfig.api.Config; +import dev.xpple.betterconfig.util.CheckedRunnable; import java.io.BufferedReader; import java.io.BufferedWriter; @@ -19,6 +20,7 @@ import java.util.Collection; import java.util.Map; import java.util.Objects; +import java.util.function.BiConsumer; public class BetterConfigInternals { @@ -47,12 +49,12 @@ public static void init(ModConfigImpl modConfig) { String fieldName = field.getName(); modConfig.getConfigs().put(fieldName, field); + modConfig.getAnnotations().put(fieldName, annotation); try { modConfig.getDefaults().put(fieldName, modConfig.getGson().fromJson(modConfig.getGson().toJsonTree(field.get(null)), field.getGenericType())); } catch (ReflectiveOperationException e) { throw new AssertionError(e); } - modConfig.getAnnotations().put(fieldName, annotation); if (!annotation.comment().isEmpty()) { modConfig.getComments().put(fieldName, annotation.comment()); @@ -122,13 +124,36 @@ public static void init(ModConfigImpl modConfig) { if (annotation.readOnly()) { continue; } + + BiConsumer onChange; + String onChangeMethodName = annotation.onChange(); + if (!onChangeMethodName.isEmpty()) { + Method onChangeMethod; + try { + onChangeMethod = modConfig.getConfigsClass().getDeclaredMethod(onChangeMethodName, field.getType(), field.getType()); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + onChangeMethod.setAccessible(true); + onChange = (oldValue, newValue) -> { + try { + onChangeMethod.invoke(null, oldValue, newValue); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + }; + } else { + onChange = (oldValue, newValue) -> {}; + } + modConfig.getOnChangeCallbacks().put(fieldName, onChange); + Class type = field.getType(); if (Collection.class.isAssignableFrom(type)) { - initCollection(modConfig, field, annotation); + initCollection(modConfig, field, annotation, onChange); } else if (Map.class.isAssignableFrom(type)) { - initMap(modConfig, field, annotation); + initMap(modConfig, field, annotation, onChange); } else { - initObject(modConfig, field, annotation); + initObject(modConfig, field, annotation, onChange); } } @@ -141,7 +166,7 @@ public static void init(ModConfigImpl modConfig) { } } - private static void initCollection(ModConfigImpl modConfig, Field field, Config annotation) { + private static void initCollection(ModConfigImpl modConfig, Field field, Config annotation, BiConsumer onChange) { String fieldName = field.getName(); Type[] types = ((ParameterizedType) field.getGenericType()).getActualTypeArguments(); Config.Adder adder = annotation.adder(); @@ -157,7 +182,7 @@ private static void initCollection(ModConfigImpl modConfig, Field field, C } modConfig.getAdders().put(fieldName, value -> { try { - add.invoke(field.get(null), value); + onChange(modConfig, field, () -> add.invoke(field.get(null), value), onChange); } catch (ReflectiveOperationException e) { throw new AssertionError(e); } @@ -173,7 +198,7 @@ private static void initCollection(ModConfigImpl modConfig, Field field, C adderMethod.setAccessible(true); modConfig.getAdders().put(fieldName, value -> { try { - adderMethod.invoke(null, value); + onChange(modConfig, field, () -> adderMethod.invoke(null, value), onChange); } catch (ReflectiveOperationException e) { if (e.getCause() instanceof CommandSyntaxException commandSyntaxException) { throw commandSyntaxException; @@ -195,7 +220,7 @@ private static void initCollection(ModConfigImpl modConfig, Field field, C } modConfig.getRemovers().put(fieldName, value -> { try { - remove.invoke(field.get(null), value); + onChange(modConfig, field, () -> remove.invoke(field.get(null), value), onChange); } catch (ReflectiveOperationException e) { throw new AssertionError(e); } @@ -211,7 +236,7 @@ private static void initCollection(ModConfigImpl modConfig, Field field, C removerMethod.setAccessible(true); modConfig.getRemovers().put(fieldName, value -> { try { - removerMethod.invoke(null, value); + onChange(modConfig, field, () -> removerMethod.invoke(null, value), onChange); } catch (ReflectiveOperationException e) { if (e.getCause() instanceof CommandSyntaxException commandSyntaxException) { throw commandSyntaxException; @@ -222,7 +247,7 @@ private static void initCollection(ModConfigImpl modConfig, Field field, C } } - private static void initMap(ModConfigImpl modConfig, Field field, Config annotation) { + private static void initMap(ModConfigImpl modConfig, Field field, Config annotation, BiConsumer onChange) { String fieldName = field.getName(); Type[] types = ((ParameterizedType) field.getGenericType()).getActualTypeArguments(); Config.Adder adder = annotation.adder(); @@ -240,7 +265,7 @@ private static void initMap(ModConfigImpl modConfig, Field field, Config a adderMethod.setAccessible(true); modConfig.getAdders().put(fieldName, key -> { try { - adderMethod.invoke(null, key); + onChange(modConfig, field, () -> adderMethod.invoke(null, key), onChange); } catch (ReflectiveOperationException e) { if (e.getCause() instanceof CommandSyntaxException commandSyntaxException) { throw commandSyntaxException; @@ -262,7 +287,7 @@ private static void initMap(ModConfigImpl modConfig, Field field, Config a } modConfig.getPutters().put(fieldName, (key, value) -> { try { - put.invoke(field.get(null), key, value); + onChange(modConfig, field, () -> put.invoke(field.get(null), key, value), onChange); } catch (ReflectiveOperationException e) { throw new AssertionError(e); } @@ -279,7 +304,7 @@ private static void initMap(ModConfigImpl modConfig, Field field, Config a putterMethod.setAccessible(true); modConfig.getPutters().put(fieldName, (key, value) -> { try { - putterMethod.invoke(null, key, value); + onChange(modConfig, field, () -> putterMethod.invoke(null, key, value), onChange); } catch (ReflectiveOperationException e) { if (e.getCause() instanceof CommandSyntaxException commandSyntaxException) { throw commandSyntaxException; @@ -301,7 +326,7 @@ private static void initMap(ModConfigImpl modConfig, Field field, Config a } modConfig.getRemovers().put(fieldName, key -> { try { - remove.invoke(field.get(null), key); + onChange(modConfig, field, () -> remove.invoke(field.get(null), key), onChange); } catch (ReflectiveOperationException e) { throw new AssertionError(e); } @@ -317,7 +342,7 @@ private static void initMap(ModConfigImpl modConfig, Field field, Config a removerMethod.setAccessible(true); modConfig.getRemovers().put(fieldName, key -> { try { - removerMethod.invoke(null, key); + onChange(modConfig, field, () -> removerMethod.invoke(null, key), onChange); } catch (ReflectiveOperationException e) { if (e.getCause() instanceof CommandSyntaxException commandSyntaxException) { throw commandSyntaxException; @@ -328,7 +353,7 @@ private static void initMap(ModConfigImpl modConfig, Field field, Config a } } - private static void initObject(ModConfigImpl modConfig, Field field, Config annotation) { + private static void initObject(ModConfigImpl modConfig, Field field, Config annotation, BiConsumer onChange) { String fieldName = field.getName(); Config.Setter setter = annotation.setter(); String setterMethodName = setter.value(); @@ -337,7 +362,7 @@ private static void initObject(ModConfigImpl modConfig, Field field, Confi } else if (setterMethodName.isEmpty()) { modConfig.getSetters().put(fieldName, value -> { try { - field.set(null, value); + onChange(modConfig, field, () -> field.set(null, value), onChange); } catch (ReflectiveOperationException e) { throw new AssertionError(e); } @@ -353,7 +378,7 @@ private static void initObject(ModConfigImpl modConfig, Field field, Confi setterMethod.setAccessible(true); modConfig.getSetters().put(fieldName, value -> { try { - setterMethod.invoke(null, value); + onChange(modConfig, field, () -> setterMethod.invoke(null, value), onChange); } catch (ReflectiveOperationException e) { if (e.getCause() instanceof CommandSyntaxException commandSyntaxException) { throw commandSyntaxException; @@ -363,4 +388,11 @@ private static void initObject(ModConfigImpl modConfig, Field field, Confi }); } } + + static void onChange(ModConfigImpl modConfig, Field field, CheckedRunnable updater, BiConsumer onChange) throws ReflectiveOperationException { + Object oldValue = modConfig.deepCopy(field.get(null), field.getGenericType()); + updater.run(); + Object newValue = modConfig.deepCopy(field.get(null), field.getGenericType()); + onChange.accept(oldValue, newValue); + } } diff --git a/common/src/main/java/dev/xpple/betterconfig/impl/ModConfigImpl.java b/common/src/main/java/dev/xpple/betterconfig/impl/ModConfigImpl.java index 03389fc..b7e4e4d 100644 --- a/common/src/main/java/dev/xpple/betterconfig/impl/ModConfigImpl.java +++ b/common/src/main/java/dev/xpple/betterconfig/impl/ModConfigImpl.java @@ -26,6 +26,7 @@ import java.nio.file.Path; import java.util.HashMap; import java.util.Map; +import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Predicate; @@ -46,14 +47,15 @@ public class ModConfigImpl implements ModConfig { .build(); private final Map configs = new HashMap<>(); + private final Map annotations = new HashMap<>(); private final Map defaults = new HashMap<>(); private final Map comments = new HashMap<>(); + private final Map> conditions = new HashMap<>(); private final Map> setters = new HashMap<>(); private final Map> adders = new HashMap<>(); private final Map> putters = new HashMap<>(); private final Map> removers = new HashMap<>(); - private final Map> conditions = new HashMap<>(); - private final Map annotations = new HashMap<>(); + private final Map> onChangeCallbacks = new HashMap<>(); private final String modId; private final Class configsClass; @@ -80,7 +82,7 @@ public Class getConfigsClass() { return this.configsClass; } - public Gson getGson() { + Gson getGson() { return this.gson; } @@ -88,7 +90,11 @@ public Map getConfigs() { return this.configs; } - public Map getDefaults() { + public Map getAnnotations() { + return this.annotations; + } + + Map getDefaults() { return this.defaults; } @@ -96,6 +102,10 @@ public Map getComments() { return this.comments; } + public Map> getConditions() { + return this.conditions; + } + public Map> getSetters() { return this.setters; } @@ -112,12 +122,8 @@ public Map> getRemovers( return this.removers; } - public Map> getConditions() { - return this.conditions; - } - - public Map getAnnotations() { - return this.annotations; + Map> getOnChangeCallbacks() { + return this.onChangeCallbacks; } @SuppressWarnings("unchecked") @@ -160,7 +166,7 @@ public void reset(String config) { throw new IllegalArgumentException(); } try { - field.set(null, this.gson.fromJson(this.gson.toJsonTree(this.defaults.get(config)), field.getGenericType())); + BetterConfigInternals.onChange(this, field, () -> field.set(null, this.deepCopy(this.defaults.get(config), field.getGenericType())), this.onChangeCallbacks.get(config)); } catch (ReflectiveOperationException e) { throw new AssertionError(e); } @@ -232,6 +238,10 @@ public Type[] getParameterTypes(String config) { return ((ParameterizedType) field.getGenericType()).getActualTypeArguments(); } + Object deepCopy(Object object, Type type) { + return this.gson.fromJson(this.gson.toJsonTree(object), type); + } + @Override public boolean save() { try (BufferedWriter writer = Files.newBufferedWriter(this.getConfigsPath())) { diff --git a/common/src/main/java/dev/xpple/betterconfig/util/CheckedRunnable.java b/common/src/main/java/dev/xpple/betterconfig/util/CheckedRunnable.java new file mode 100644 index 0000000..430621f --- /dev/null +++ b/common/src/main/java/dev/xpple/betterconfig/util/CheckedRunnable.java @@ -0,0 +1,6 @@ +package dev.xpple.betterconfig.util; + +@FunctionalInterface +public interface CheckedRunnable { + void run() throws E; +} diff --git a/fabric/src/testmod/java/dev/xpple/betterconfig/Configs.java b/fabric/src/testmod/java/dev/xpple/betterconfig/Configs.java index 6f8e7e1..1c843b6 100644 --- a/fabric/src/testmod/java/dev/xpple/betterconfig/Configs.java +++ b/fabric/src/testmod/java/dev/xpple/betterconfig/Configs.java @@ -86,4 +86,10 @@ private static void privateSetter(String string) { @Config public static StructureType exampleConvertedArgumentType = StructureType.WOODLAND_MANSION; + + @Config(onChange = "onChange") + public static List exampleOnChange = new ArrayList<>(List.of("xpple, earthcomputer")); + private static void onChange(List oldValue, List newValue) { + System.out.println("Old: " + oldValue + ", new: " + newValue); + } } diff --git a/paper/src/testplugin/java/dev/xpple/betterconfig/Configs.java b/paper/src/testplugin/java/dev/xpple/betterconfig/Configs.java index b860580..fc9c7f0 100644 --- a/paper/src/testplugin/java/dev/xpple/betterconfig/Configs.java +++ b/paper/src/testplugin/java/dev/xpple/betterconfig/Configs.java @@ -83,4 +83,10 @@ private static void privateSetter(String string) { @Config public static Structure exampleCustomArgumentType = Structure.MANSION; + + @Config(onChange = "onChange") + public static List exampleOnChange = new ArrayList<>(List.of("xpple, earthcomputer")); + private static void onChange(List oldValue, List newValue) { + System.out.println("Old: " + oldValue + ", new: " + newValue); + } } From ec35dcf1ff77505f184a9f6c23584875fb5c6eb1 Mon Sep 17 00:00:00 2001 From: xpple Date: Thu, 12 Sep 2024 16:52:27 +0200 Subject: [PATCH 2/3] Add fabric-resource-loader-v0 --- fabric/build.gradle | 1 + fabric/src/main/resources/fabric.mod.json | 1 + 2 files changed, 2 insertions(+) diff --git a/fabric/build.gradle b/fabric/build.gradle index 0d0cc69..2cc75ca 100644 --- a/fabric/build.gradle +++ b/fabric/build.gradle @@ -77,6 +77,7 @@ dependencies { } modImplementation "net.fabricmc:fabric-loader:${project.fabric_loader_version}" + modImplementation fabricApi.module("fabric-resource-loader-v0", project.fabric_api_version) modImplementation fabricApi.module("fabric-command-api-v2", project.fabric_api_version) } diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json index 60e9b8c..676e712 100644 --- a/fabric/src/main/resources/fabric.mod.json +++ b/fabric/src/main/resources/fabric.mod.json @@ -33,6 +33,7 @@ "fabricloader": ">=${fabric_loader_version}", "minecraft": "${minecraft_version}.x", "java": ">=21", + "fabric-resource-loader-v0": "*", "fabric-command-api-v2": "*" }, "custom": { From 984f6a656d2d2d3a5d444c0326a9b70e06f600bc Mon Sep 17 00:00:00 2001 From: xpple Date: Thu, 12 Sep 2024 17:36:41 +0200 Subject: [PATCH 3/3] Minor refactor --- .../impl/BetterConfigInternals.java | 137 +++++++++--------- 1 file changed, 72 insertions(+), 65 deletions(-) diff --git a/common/src/main/java/dev/xpple/betterconfig/impl/BetterConfigInternals.java b/common/src/main/java/dev/xpple/betterconfig/impl/BetterConfigInternals.java index feb0ec8..83ddd07 100644 --- a/common/src/main/java/dev/xpple/betterconfig/impl/BetterConfigInternals.java +++ b/common/src/main/java/dev/xpple/betterconfig/impl/BetterConfigInternals.java @@ -76,76 +76,13 @@ public static void init(ModConfigImpl modConfig) { } } - if (annotation.condition().isEmpty()) { - modConfig.getConditions().put(fieldName, source -> true); - } else { - Method predicateMethod; - boolean hasParameter = false; - try { - predicateMethod = modConfig.getConfigsClass().getDeclaredMethod(annotation.condition()); - } catch (ReflectiveOperationException e) { - hasParameter = true; - try { - Class commandSourceClass = Platform.current.getCommandSourceClass(); - predicateMethod = modConfig.getConfigsClass().getDeclaredMethod(annotation.condition(), commandSourceClass); - } catch (ReflectiveOperationException e1) { - throw new AssertionError(e1); - } - } - if (predicateMethod.getReturnType() != boolean.class) { - throw new AssertionError("Condition method '" + annotation.condition() + "' does not return boolean"); - } - if (!Modifier.isStatic(predicateMethod.getModifiers())) { - throw new AssertionError("Condition method '" + annotation.condition() + "' is not static"); - } - predicateMethod.setAccessible(true); - - Method predicateMethod_f = predicateMethod; - - if (hasParameter) { - modConfig.getConditions().put(fieldName, source -> { - try { - return (Boolean) predicateMethod_f.invoke(null, source); - } catch (ReflectiveOperationException e) { - throw new AssertionError(e); - } - }); - } else { - modConfig.getConditions().put(fieldName, source -> { - try { - return (Boolean) predicateMethod_f.invoke(null); - } catch (ReflectiveOperationException e) { - throw new AssertionError(e); - } - }); - } - } + initCondition(modConfig, annotation.condition(), fieldName); if (annotation.readOnly()) { continue; } - BiConsumer onChange; - String onChangeMethodName = annotation.onChange(); - if (!onChangeMethodName.isEmpty()) { - Method onChangeMethod; - try { - onChangeMethod = modConfig.getConfigsClass().getDeclaredMethod(onChangeMethodName, field.getType(), field.getType()); - } catch (ReflectiveOperationException e) { - throw new AssertionError(e); - } - onChangeMethod.setAccessible(true); - onChange = (oldValue, newValue) -> { - try { - onChangeMethod.invoke(null, oldValue, newValue); - } catch (ReflectiveOperationException e) { - throw new AssertionError(e); - } - }; - } else { - onChange = (oldValue, newValue) -> {}; - } - modConfig.getOnChangeCallbacks().put(fieldName, onChange); + BiConsumer onChange = initOnChange(modConfig, field, annotation.onChange()); Class type = field.getType(); if (Collection.class.isAssignableFrom(type)) { @@ -166,6 +103,76 @@ public static void init(ModConfigImpl modConfig) { } } + private static void initCondition(ModConfigImpl modConfig, String condition, String fieldName) { + if (condition.isEmpty()) { + modConfig.getConditions().put(fieldName, source -> true); + return; + } + Method predicateMethod; + boolean hasParameter = false; + try { + predicateMethod = modConfig.getConfigsClass().getDeclaredMethod(condition); + } catch (ReflectiveOperationException e) { + hasParameter = true; + try { + Class commandSourceClass = Platform.current.getCommandSourceClass(); + predicateMethod = modConfig.getConfigsClass().getDeclaredMethod(condition, commandSourceClass); + } catch (ReflectiveOperationException e1) { + throw new AssertionError(e1); + } + } + if (predicateMethod.getReturnType() != boolean.class) { + throw new AssertionError("Condition method '" + condition + "' does not return boolean"); + } + if (!Modifier.isStatic(predicateMethod.getModifiers())) { + throw new AssertionError("Condition method '" + condition + "' is not static"); + } + predicateMethod.setAccessible(true); + + Method predicateMethod_f = predicateMethod; + + if (hasParameter) { + modConfig.getConditions().put(fieldName, source -> { + try { + return (Boolean) predicateMethod_f.invoke(null, source); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + }); + } else { + modConfig.getConditions().put(fieldName, source -> { + try { + return (Boolean) predicateMethod_f.invoke(null); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + }); + } + } + + private static BiConsumer initOnChange(ModConfigImpl modConfig, Field field, String onChangeMethodName) { + if (onChangeMethodName.isEmpty()) { + return (oldValue, newValue) -> {}; + } + Method onChangeMethod; + try { + onChangeMethod = modConfig.getConfigsClass().getDeclaredMethod(onChangeMethodName, field.getType(), field.getType()); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + onChangeMethod.setAccessible(true); + BiConsumer onChange = (oldValue, newValue) -> { + try { + onChangeMethod.invoke(null, oldValue, newValue); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + }; + + modConfig.getOnChangeCallbacks().put(field.getName(), onChange); + return onChange; + } + private static void initCollection(ModConfigImpl modConfig, Field field, Config annotation, BiConsumer onChange) { String fieldName = field.getName(); Type[] types = ((ParameterizedType) field.getGenericType()).getActualTypeArguments();