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..83ddd07 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()); @@ -74,61 +76,21 @@ 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 = initOnChange(modConfig, field, annotation.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 +103,77 @@ public static void init(ModConfigImpl modConfig) { } } - private static void initCollection(ModConfigImpl modConfig, Field field, Config annotation) { + 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(); Config.Adder adder = annotation.adder(); @@ -157,7 +189,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 +205,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 +227,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 +243,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 +254,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 +272,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 +294,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 +311,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 +333,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 +349,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 +360,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 +369,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 +385,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 +395,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); + } }