diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/config/Config.java b/src/main/java/com/gtnewhorizon/gtnhlib/config/Config.java index ad49b95..1419e8b 100644 --- a/src/main/java/com/gtnewhorizon/gtnhlib/config/Config.java +++ b/src/main/java/com/gtnewhorizon/gtnhlib/config/Config.java @@ -161,4 +161,31 @@ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD, ElementType.TYPE }) @interface RequiresWorldRestart {} + + /** + * Set a default value if the listed coremod or modID is found. Coremod will take precedence value will be parsed to + * target field's type + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @interface ModDetectedDefault { + + String coremod() default ""; + + String modID() default ""; + + String value() default ""; + + /** + * Can be used instead of value() for array fields + */ + String[] values() default {}; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @interface ModDetectedDefaultList { + + ModDetectedDefault[] values() default {}; + } } diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/config/ConfigFieldParser.java b/src/main/java/com/gtnewhorizon/gtnhlib/config/ConfigFieldParser.java index 307e3fc..9b091ec 100644 --- a/src/main/java/com/gtnewhorizon/gtnhlib/config/ConfigFieldParser.java +++ b/src/main/java/com/gtnewhorizon/gtnhlib/config/ConfigFieldParser.java @@ -1,9 +1,9 @@ package com.gtnewhorizon.gtnhlib.config; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.regex.Pattern; @@ -14,12 +14,16 @@ import net.minecraftforge.common.config.Configuration; import net.minecraftforge.common.config.Property; +import cpw.mods.fml.common.Loader; +import it.unimi.dsi.fastutil.objects.Object2BooleanMap; +import it.unimi.dsi.fastutil.objects.Object2BooleanOpenHashMap; import lombok.SneakyThrows; import lombok.val; public class ConfigFieldParser { private static final Map, Parser> PARSERS = new HashMap<>(); + private static final Object2BooleanMap detectedMods = new Object2BooleanOpenHashMap<>(); static { PARSERS.put(boolean.class, new BooleanParser()); @@ -38,22 +42,45 @@ public class ConfigFieldParser { } public static void loadField(Object instance, Field field, Configuration config, String category) - throws NoSuchFieldException, InvocationTargetException, IllegalAccessException, NoSuchMethodException, - ConfigException { - Parser parser = getParser(field); - var comment = Optional.ofNullable(field.getAnnotation(Config.Comment.class)).map(Config.Comment::value) - .map((lines) -> String.join("\n", lines)).orElse(""); - val name = getFieldName(field); - val langKey = Optional.ofNullable(field.getAnnotation(Config.LangKey.class)).map(Config.LangKey::value) - .orElse(name); - parser.load(instance, field, config, category, name, comment, langKey); + throws ConfigException { + try { + Parser parser = getParser(field); + var comment = Optional.ofNullable(field.getAnnotation(Config.Comment.class)).map(Config.Comment::value) + .map((lines) -> String.join("\n", lines)).orElse(""); + val name = getFieldName(field); + val langKey = Optional.ofNullable(field.getAnnotation(Config.LangKey.class)).map(Config.LangKey::value) + .orElse(name); + val defValueString = getModDefault(field); + + parser.load(instance, defValueString, field, config, category, name, comment, langKey); + } catch (Exception e) { + throw new ConfigException( + "Failed to load field " + field.getName() + + " of type " + + field.getType().getSimpleName() + + " in class " + + field.getDeclaringClass().getName() + + ". Caused by: " + + e); + } } public static void saveField(Object instance, Field field, Configuration config, String category) - throws IllegalAccessException, ConfigException { - val name = getFieldName(field); - Parser parser = getParser(field); - parser.save(instance, field, config, category, name); + throws ConfigException { + try { + val name = getFieldName(field); + Parser parser = getParser(field); + parser.save(instance, field, config, category, name); + } catch (Exception e) { + throw new ConfigException( + "Failed to save field " + field.getName() + + " of type " + + field.getType().getSimpleName() + + " in class " + + field.getDeclaringClass().getName() + + ". Caused by: " + + e); + } } private static Parser getParser(Field field) throws ConfigException { @@ -64,8 +91,7 @@ private static Parser getParser(Field field) throws ConfigException { } if (parser == null) { - throw new ConfigException( - "No parser found for field " + field.getName() + " of type " + fieldClass.getName() + "!"); + throw new ConfigException("No parser found for field"); } return parser; @@ -84,6 +110,41 @@ public static String getFieldName(Field field) { return field.getName(); } + private static @Nullable String getModDefault(Field field) { + val modDefaultList = field.getAnnotation(Config.ModDetectedDefaultList.class); + if (modDefaultList != null) { + return Arrays.stream(modDefaultList.values()).filter(ConfigFieldParser::isModDetected).findFirst() + .map( + modDefault -> modDefault.values().length != 0 ? String.join(",", modDefault.values()) + : modDefault.value().trim()) + .orElse(null); + } + + val modDefault = field.getAnnotation(Config.ModDetectedDefault.class); + if (isModDetected(modDefault)) { + return modDefault.values().length != 0 ? String.join(",", modDefault.values()) : modDefault.value().trim(); + } + + return null; + } + + private static boolean isModDetected(Config.ModDetectedDefault modDefault) { + if (modDefault == null) return false; + val modID = modDefault.modID(); + val coremod = modDefault.coremod(); + if (modID.isEmpty() && coremod.isEmpty()) return false; + return detectedMods.computeIfAbsent(modID.isEmpty() ? coremod : modID, id -> { + if (!coremod.isEmpty()) { + try { + Class.forName(coremod); + return true; + } catch (ClassNotFoundException ignored) {} + } + + return !modID.isEmpty() && Loader.isModLoaded(modID); + }); + } + @SneakyThrows private static Field extractField(Class clazz, String field) { return clazz.getDeclaredField(field); @@ -96,95 +157,127 @@ private static Object extractValue(Field field) { public interface Parser { - void load(@Nullable Object instance, Field field, Configuration config, String category, String name, - String comment, String langKey) throws IllegalAccessException, NoSuchMethodException, - InvocationTargetException, NoSuchFieldException, ConfigException; + void load(@Nullable Object instance, @Nullable String defValueString, Field field, Configuration config, + String category, String name, String comment, String langKey); - void save(@Nullable Object instance, Field field, Configuration config, String category, String name) - throws IllegalAccessException; + void save(@Nullable Object instance, Field field, Configuration config, String category, String name); } private static class BooleanParser implements Parser { @Override - public void load(@Nullable Object instance, Field field, Configuration config, String category, String name, - String comment, String langKey) throws IllegalAccessException { - boolean boxed = field.getType().equals(Boolean.class); - val defaultValue = Optional.ofNullable(field.getAnnotation(Config.DefaultBoolean.class)) - .map(Config.DefaultBoolean::value) - .orElse(boxed ? (Boolean) field.get(instance) : field.getBoolean(instance)); + @SneakyThrows + public void load(@Nullable Object instance, @Nullable String defValueString, Field field, Configuration config, + String category, String name, String comment, String langKey) { + val defaultValue = fromStringOrDefault(instance, defValueString, field); field.setBoolean(instance, config.getBoolean(name, category, defaultValue, comment, langKey)); } @Override - public void save(@Nullable Object instance, Field field, Configuration config, String category, String name) - throws IllegalAccessException { + @SneakyThrows + public void save(@Nullable Object instance, Field field, Configuration config, String category, String name) { boolean boxed = field.getType().equals(Boolean.class); Property prop = config.getCategory(category).get(name); prop.set(boxed ? (Boolean) field.get(instance) : field.getBoolean(instance)); } + + @SneakyThrows + private boolean fromStringOrDefault(@Nullable Object instance, @Nullable String defValueString, Field field) { + val boxed = field.getType().equals(Boolean.class); + if (defValueString == null) { + return Optional.ofNullable(field.getAnnotation(Config.DefaultBoolean.class)) + .map(Config.DefaultBoolean::value) + .orElse(boxed ? (Boolean) field.get(instance) : field.getBoolean(instance)); + } + + // Boolean.parseBoolean returns false for any string that is not "true" which is probably not desired here + if (!"true".equalsIgnoreCase(defValueString) && !"false".equalsIgnoreCase(defValueString)) { + throw new ConfigException("Invalid boolean value: " + defValueString); + } + + return Boolean.parseBoolean(defValueString); + } } private static class IntParser implements Parser { @Override - public void load(@Nullable Object instance, Field field, Configuration config, String category, String name, - String comment, String langKey) throws IllegalAccessException { - boolean boxed = field.getType().equals(Integer.class); + @SneakyThrows + public void load(@Nullable Object instance, @Nullable String defValueString, Field field, Configuration config, + String category, String name, String comment, String langKey) { val range = Optional.ofNullable(field.getAnnotation(Config.RangeInt.class)); val min = range.map(Config.RangeInt::min).orElse(Integer.MIN_VALUE); val max = range.map(Config.RangeInt::max).orElse(Integer.MAX_VALUE); - val defaultValue = Optional.ofNullable(field.getAnnotation(Config.DefaultInt.class)) - .map(Config.DefaultInt::value) - .orElse(boxed ? (Integer) field.get(instance) : field.getInt(instance)); + val defaultValue = fromStringOrDefault(instance, defValueString, field); + field.setInt(instance, config.getInt(name, category, defaultValue, min, max, comment, langKey)); } @Override - public void save(@Nullable Object instance, Field field, Configuration config, String category, String name) - throws IllegalAccessException { + @SneakyThrows + public void save(@Nullable Object instance, Field field, Configuration config, String category, String name) { boolean boxed = field.getType().equals(Integer.class); Property prop = config.getCategory(category).get(name); prop.set(boxed ? (Integer) field.get(instance) : field.getInt(instance)); } + + @SneakyThrows + private int fromStringOrDefault(@Nullable Object instance, @Nullable String defValueString, Field field) { + val boxed = field.getType().equals(Integer.class); + if (defValueString == null) { + return Optional.ofNullable(field.getAnnotation(Config.DefaultInt.class)).map(Config.DefaultInt::value) + .orElse(boxed ? (Integer) field.get(instance) : field.getInt(instance)); + } + + return Integer.parseInt(defValueString); + } } private static class FloatParser implements Parser { @Override - public void load(@Nullable Object instance, Field field, Configuration config, String category, String name, - String comment, String langKey) throws IllegalAccessException { - boolean boxed = field.getType().equals(Float.class); + @SneakyThrows + public void load(@Nullable Object instance, @Nullable String defValueString, Field field, Configuration config, + String category, String name, String comment, String langKey) { val range = Optional.ofNullable(field.getAnnotation(Config.RangeFloat.class)); val min = range.map(Config.RangeFloat::min).orElse(Float.MIN_VALUE); val max = range.map(Config.RangeFloat::max).orElse(Float.MAX_VALUE); - val defaultValue = Optional.ofNullable(field.getAnnotation(Config.DefaultFloat.class)) - .map(Config.DefaultFloat::value) - .orElse(boxed ? (Float) field.get(instance) : field.getFloat(instance)); + val defaultValue = fromStringOrDefault(instance, defValueString, field); field.setFloat(instance, config.getFloat(name, category, defaultValue, min, max, comment, langKey)); } @Override - public void save(@Nullable Object instance, Field field, Configuration config, String category, String name) - throws IllegalAccessException { + @SneakyThrows + public void save(@Nullable Object instance, Field field, Configuration config, String category, String name) { boolean boxed = field.getType().equals(Float.class); Property prop = config.getCategory(category).get(name); prop.set(boxed ? (Float) field.get(instance) : field.getFloat(instance)); } + + @SneakyThrows + private float fromStringOrDefault(@Nullable Object instance, @Nullable String defValueString, Field field) { + val boxed = field.getType().equals(Float.class); + if (defValueString == null) { + return Optional.ofNullable(field.getAnnotation(Config.DefaultFloat.class)) + .map(Config.DefaultFloat::value) + .orElse(boxed ? (Float) field.get(instance) : field.getFloat(instance)); + } + + return Float.parseFloat(defValueString); + } } private static class DoubleParser implements Parser { @Override - public void load(@Nullable Object instance, Field field, Configuration config, String category, String name, - String comment, String langKey) throws IllegalAccessException { - boolean boxed = field.getType().equals(Double.class); + @SneakyThrows + public void load(@Nullable Object instance, @Nullable String defValueString, Field field, Configuration config, + String category, String name, String comment, String langKey) { val range = Optional.ofNullable(field.getAnnotation(Config.RangeDouble.class)); val min = range.map(Config.RangeDouble::min).orElse(Double.MIN_VALUE); val max = range.map(Config.RangeDouble::max).orElse(Double.MAX_VALUE); - val defaultValue = Optional.ofNullable(field.getAnnotation(Config.DefaultDouble.class)) - .map(Config.DefaultDouble::value) - .orElse(boxed ? (Double) field.get(instance) : field.getDouble(instance)); + val defaultValue = fromStringOrDefault(instance, defValueString, field); + val defaultValueComment = comment + " [range: " + min + " ~ " + max + ", default: " + defaultValue + "]"; field.setDouble( instance, @@ -193,58 +286,66 @@ public void load(@Nullable Object instance, Field field, Configuration config, S } @Override - public void save(@Nullable Object instance, Field field, Configuration config, String category, String name) - throws IllegalAccessException { + @SneakyThrows + public void save(@Nullable Object instance, Field field, Configuration config, String category, String name) { boolean boxed = field.getType().equals(Double.class); Property prop = config.getCategory(category).get(name); prop.set(boxed ? (Double) field.get(instance) : field.getDouble(instance)); } + + @SneakyThrows + private double fromStringOrDefault(@Nullable Object instance, @Nullable String defValueString, Field field) { + val boxed = field.getType().equals(Double.class); + if (defValueString == null) { + return Optional.ofNullable(field.getAnnotation(Config.DefaultDouble.class)) + .map(Config.DefaultDouble::value) + .orElse(boxed ? (Double) field.get(instance) : field.getDouble(instance)); + } + + return Double.parseDouble(defValueString); + } } private static class StringParser implements Parser { @Override - public void load(@Nullable Object instance, Field field, Configuration config, String category, String name, - String comment, String langKey) throws IllegalAccessException { - val defaultValue = Optional.ofNullable(field.getAnnotation(Config.DefaultString.class)) - .map(Config.DefaultString::value).orElse((String) field.get(instance)); + @SneakyThrows + public void load(@Nullable Object instance, @Nullable String defValueString, Field field, Configuration config, + String category, String name, String comment, String langKey) { + val defaultValue = fromStringOrDefault(instance, defValueString, field); val pattern = Optional.ofNullable(field.getAnnotation(Config.Pattern.class)).map(Config.Pattern::value) .map(Pattern::compile).orElse(null); field.set(instance, config.getString(name, category, defaultValue, comment, langKey, pattern)); } @Override - public void save(@Nullable Object instance, Field field, Configuration config, String category, String name) - throws IllegalAccessException { + @SneakyThrows + public void save(@Nullable Object instance, Field field, Configuration config, String category, String name) { Property prop = config.getCategory(category).get(name); prop.set((String) field.get(instance)); } + + @SneakyThrows + private String fromStringOrDefault(@Nullable Object instance, @Nullable String defValueString, Field field) { + if (defValueString == null) { + return Optional.ofNullable(field.getAnnotation(Config.DefaultString.class)) + .map(Config.DefaultString::value).orElse((String) field.get(instance)); + } + + return defValueString; + } } private static class EnumParser implements Parser { @Override - public void load(@Nullable Object instance, Field field, Configuration config, String category, String name, - String comment, String langKey) - throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, ConfigException { + @SneakyThrows + public void load(@Nullable Object instance, @Nullable String defValueString, Field field, Configuration config, + String category, String name, String comment, String langKey) { Class fieldClass = field.getType(); val enumValues = Arrays.stream((Object[]) fieldClass.getDeclaredMethod("values").invoke(instance)) .map((obj) -> (Enum) obj).collect(Collectors.toList()); - val defaultValue = (Enum) Optional.ofNullable(field.getAnnotation(Config.DefaultEnum.class)) - .map(Config.DefaultEnum::value).map((defName) -> extractField(fieldClass, defName)) - .map(ConfigFieldParser::extractValue).orElse(field.get(instance)); - - if (defaultValue == null) { - throw new ConfigException( - "Invalid default value for enum field " + field.getName() - + " of type " - + fieldClass.getName() - + " in config class " - + field.getDeclaringClass().getName() - + " Valid values are: " - + enumValues); - } - + val defaultValue = fromStringOrDefault(instance, defValueString, field, enumValues); val possibleValues = enumValues.stream().map(Enum::name).toArray(String[]::new); String value = config.getString( name, @@ -265,58 +366,82 @@ public void load(@Nullable Object instance, Field field, Configuration config, S field.set(instance, enumField.get(instance)); } catch (NoSuchFieldException e) { ConfigurationManager.LOGGER.warn( - "Invalid value " + value - + " for enum configuration field " - + field.getName() - + " of type " - + fieldClass.getName() - + " in config class " - + field.getDeclaringClass().getName() - + "! Using default value of " - + defaultValue - + "!"); + "Invalid value {} for enum configuration field {} of type {} in config class {}! Using default value of {}!", + value, + field.getName(), + fieldClass.getName(), + field.getDeclaringClass().getName(), + defaultValue); field.set(instance, defaultValue); } } @Override - public void save(@Nullable Object instance, Field field, Configuration config, String category, String name) - throws IllegalAccessException { + @SneakyThrows + public void save(@Nullable Object instance, Field field, Configuration config, String category, String name) { Property prop = config.getCategory(category).get(name); prop.set(((Enum) field.get(instance)).name()); } + + @SneakyThrows + private Enum fromStringOrDefault(@Nullable Object instance, @Nullable String defValueString, Field field, + List> validValues) { + Enum value; + if (defValueString == null) { + value = (Enum) Optional.ofNullable(field.getAnnotation(Config.DefaultEnum.class)) + .map(Config.DefaultEnum::value).map((defName) -> extractField(field.getType(), defName)) + .map(ConfigFieldParser::extractValue).orElse(field.get(instance)); + } else { + val modDefaultField = extractField(field.getType(), defValueString); + value = (Enum) extractValue(modDefaultField); + } + + if (value == null) { + throw new ConfigException("Invalid default value for enum field! Valid values are " + validValues); + } + + return value; + } } private static class StringArrayParser implements Parser { @Override - public void load(@Nullable Object instance, Field field, Configuration config, String category, String name, - String comment, String langKey) throws IllegalAccessException { - - String[] defaultValue = Optional.ofNullable(field.getAnnotation(Config.DefaultStringList.class)) - .map(Config.DefaultStringList::value).orElse((String[]) field.get(instance)); - if (defaultValue == null) defaultValue = new String[0]; - String[] value = config.getStringList(name, category, defaultValue, comment, null, langKey); + @SneakyThrows + public void load(@Nullable Object instance, @Nullable String defValueString, Field field, Configuration config, + String category, String name, String comment, String langKey) { + val defaultValue = fromStringOrDefault(instance, defValueString, field); + val value = config.getStringList(name, category, defaultValue, comment, null, langKey); field.set(instance, value); } @Override - public void save(@Nullable Object instance, Field field, Configuration config, String category, String name) - throws IllegalAccessException { - Property prop = config.getCategory(category).get(name); + @SneakyThrows + public void save(@Nullable Object instance, Field field, Configuration config, String category, String name) { + val prop = config.getCategory(category).get(name); prop.set((String[]) field.get(instance)); } + + @SneakyThrows + private String[] fromStringOrDefault(@Nullable Object instance, @Nullable String defValueString, Field field) { + String[] value; + if (defValueString == null) { + value = Optional.ofNullable(field.getAnnotation(Config.DefaultStringList.class)) + .map(Config.DefaultStringList::value).orElse((String[]) field.get(instance)); + } else { + value = defValueString.split(","); + } + return value == null ? new String[0] : value; + } } private static class DoubleArrayParser implements Parser { @Override - public void load(@Nullable Object instance, Field field, Configuration config, String category, String name, - String comment, String langKey) throws IllegalAccessException { - double[] defaultValue = Optional.ofNullable(field.getAnnotation(Config.DefaultDoubleList.class)) - .map(Config.DefaultDoubleList::value).orElse((double[]) field.get(instance)); - - if (defaultValue == null) defaultValue = new double[0]; + @SneakyThrows + public void load(@Nullable Object instance, @Nullable String defValueString, Field field, Configuration config, + String category, String name, String comment, String langKey) { + val defaultValue = fromStringOrDefault(instance, defValueString, field); String[] stringValues = new String[defaultValue.length]; for (int i = 0; i < defaultValue.length; i++) { @@ -329,23 +454,34 @@ public void load(@Nullable Object instance, Field field, Configuration config, S } @Override - public void save(@Nullable Object instance, Field field, Configuration config, String category, String name) - throws IllegalAccessException { + @SneakyThrows + public void save(@Nullable Object instance, Field field, Configuration config, String category, String name) { Property prop = config.getCategory(category).get(name); prop.set((double[]) field.get(instance)); } + + @SneakyThrows + private double[] fromStringOrDefault(@Nullable Object instance, @Nullable String defValueString, Field field) { + double[] value; + if (defValueString == null) { + value = Optional.ofNullable(field.getAnnotation(Config.DefaultDoubleList.class)) + .map(Config.DefaultDoubleList::value).orElse((double[]) field.get(instance)); + } else { + value = Arrays.stream(defValueString.split(",")).mapToDouble(s -> Double.parseDouble(s.trim())) + .toArray(); + } + + return value == null ? new double[0] : value; + } } private static class IntArrayParser implements Parser { @Override - public void load(@Nullable Object instance, Field field, Configuration config, String category, String name, - String comment, String langKey) throws IllegalAccessException { - int[] defaultValue = Optional.ofNullable(field.getAnnotation(Config.DefaultIntList.class)) - .map(Config.DefaultIntList::value).orElse((int[]) field.get(instance)); - - if (defaultValue == null) defaultValue = new int[0]; - + @SneakyThrows + public void load(@Nullable Object instance, @Nullable String defValueString, Field field, Configuration config, + String category, String name, String comment, String langKey) { + val defaultValue = fromStringOrDefault(instance, defValueString, field); String[] stringValues = new String[defaultValue.length]; for (int i = 0; i < defaultValue.length; i++) { stringValues[i] = Integer.toString(defaultValue[i]); @@ -357,10 +493,23 @@ public void load(@Nullable Object instance, Field field, Configuration config, S } @Override - public void save(@Nullable Object instance, Field field, Configuration config, String category, String name) - throws IllegalAccessException { + @SneakyThrows + public void save(@Nullable Object instance, Field field, Configuration config, String category, String name) { Property prop = config.getCategory(category).get(name); prop.set((int[]) field.get(instance)); } + + @SneakyThrows + private int[] fromStringOrDefault(@Nullable Object instance, @Nullable String defValueString, Field field) { + int[] value; + if (defValueString == null) { + value = Optional.ofNullable(field.getAnnotation(Config.DefaultIntList.class)) + .map(Config.DefaultIntList::value).orElse((int[]) field.get(instance)); + } else { + value = Arrays.stream(defValueString.split(",")).mapToInt(s -> Integer.parseInt(s.trim())).toArray(); + } + + return value == null ? new int[0] : value; + } } }