diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/config/Config.java b/src/main/java/com/gtnewhorizon/gtnhlib/config/Config.java index 1419e8b..3827be3 100644 --- a/src/main/java/com/gtnewhorizon/gtnhlib/config/Config.java +++ b/src/main/java/com/gtnewhorizon/gtnhlib/config/Config.java @@ -39,6 +39,25 @@ String value(); } + /** + * Defines a pattern for generating lang keys for fields and categories in the annotated class. + *

+ * Placeholders:
+ * {@code %mod} - mod id
+ * {@code %file} - file name
+ * {@code %cat} - category name
+ * {@code %field} - field name (required)
+ *

+ * Default pattern: {@code %mod.%cat.%field}. Categories use the pattern without {@code %field}. Can be overridden + * for fields with {@link Config.LangKey}. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @interface LangKeyPattern { + + String pattern() default "%mod.%cat.%field"; + } + @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @interface Comment { diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/config/ConfigFieldParser.java b/src/main/java/com/gtnewhorizon/gtnhlib/config/ConfigFieldParser.java index 9b091ec..de66c28 100644 --- a/src/main/java/com/gtnewhorizon/gtnhlib/config/ConfigFieldParser.java +++ b/src/main/java/com/gtnewhorizon/gtnhlib/config/ConfigFieldParser.java @@ -41,18 +41,15 @@ public class ConfigFieldParser { PARSERS.put(Enum.class, new EnumParser()); } - public static void loadField(Object instance, Field field, Configuration config, String category) + public static void loadField(Object instance, Field field, Configuration config, String category, String key) 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); + parser.load(instance, defValueString, field, config, category, name, comment, key); } catch (Exception e) { throw new ConfigException( "Failed to load field " + field.getName() diff --git a/src/main/java/com/gtnewhorizon/gtnhlib/config/ConfigurationManager.java b/src/main/java/com/gtnewhorizon/gtnhlib/config/ConfigurationManager.java index e95f110..3f1158a 100644 --- a/src/main/java/com/gtnewhorizon/gtnhlib/config/ConfigurationManager.java +++ b/src/main/java/com/gtnewhorizon/gtnhlib/config/ConfigurationManager.java @@ -1,6 +1,7 @@ package com.gtnewhorizon.gtnhlib.config; import java.io.File; +import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; @@ -39,6 +40,7 @@ public class ConfigurationManager { static final Logger LOGGER = LogManager.getLogger("GTNHLibConfig"); private static final Map configs = new HashMap<>(); private static final Map>>> configToCategoryClassMap = new HashMap<>(); + private static final String[] langKeyPlaceholders = new String[] { "%mod", "%file", "%cat", "%field" }; private static final ConfigurationManager instance = new ConfigurationManager(); @@ -131,10 +133,13 @@ private static void processSubCategory(Object instance, Configuration config, Fi var comment = Optional.ofNullable(subCategoryField.getAnnotation(Config.Comment.class)) .map(Config.Comment::value).map((lines) -> String.join("\n", lines)).orElse(""); val name = ConfigFieldParser.getFieldName(subCategoryField); - val langKey = Optional.ofNullable(subCategoryField.getAnnotation(Config.LangKey.class)) - .map(Config.LangKey::value).orElse(name); val cat = (category.isEmpty() ? "" : category + Configuration.CATEGORY_SPLITTER) + name.toLowerCase(); ConfigCategory subCat = config.getCategory(cat); + val langKey = getLangKey( + subCategoryField.getType(), + subCategoryField.getAnnotation(Config.LangKey.class), + null, + subCat.getName()); subCat.setComment(comment); subCat.setLanguageKey(langKey); @@ -158,6 +163,12 @@ private static void processConfigInternal(Class configClass, String category, @Nullable Object instance) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException, ConfigException { boolean foundCategory = !category.isEmpty(); + ConfigCategory cat = foundCategory ? rawConfig.getCategory(category) : null; + boolean requiresMcRestart = getClassOrBaseAnnotation(configClass, Config.RequiresMcRestart.class) != null + || foundCategory && cat.requiresMcRestart(); + boolean requiresWorldRestart = getClassOrBaseAnnotation(configClass, Config.RequiresWorldRestart.class) != null + || foundCategory && cat.requiresWorldRestart(); + for (val field : configClass.getDeclaredFields()) { if (instance != null && Modifier.isStatic(field.getModifiers())) { throw new ConfigException( @@ -191,20 +202,31 @@ private static void processConfigInternal(Class configClass, String category, if (category.isEmpty()) continue; - ConfigFieldParser.loadField(instance, field, rawConfig, category); + val langKey = getLangKey( + configClass, + field.getAnnotation(Config.LangKey.class), + ConfigFieldParser.getFieldName(field), + category); + ConfigFieldParser.loadField(instance, field, rawConfig, category, langKey); - val cat = rawConfig.getCategory(category); - if (field.isAnnotationPresent(Config.RequiresMcRestart.class)) { - cat.setRequiresMcRestart(true); + if (!requiresMcRestart) { + requiresMcRestart = field.isAnnotationPresent(Config.RequiresMcRestart.class); } - if (field.isAnnotationPresent(Config.RequiresWorldRestart.class)) { - cat.setRequiresWorldRestart(true); + + if (!requiresWorldRestart) { + requiresWorldRestart = field.isAnnotationPresent(Config.RequiresWorldRestart.class); } } if (!foundCategory) { throw new ConfigException("No category found for config class " + configClass.getName() + "!"); } + + if (category.isEmpty()) return; + val langKey = getLangKey(configClass, configClass.getAnnotation(Config.LangKey.class), null, cat.getName()); + cat.setLanguageKey(langKey); + cat.setRequiresMcRestart(requiresMcRestart); + cat.setRequiresWorldRestart(requiresWorldRestart); } /** @@ -322,7 +344,65 @@ private static IConfigElementProxy getProxyElement(IConfigElement element, }); } - private static boolean isFieldSubCategory(Field field) { + private static String getLangKey(Class configClass, @Nullable Config.LangKey langKey, @Nullable String fieldName, + String categoryName) throws ConfigException { + if (langKey != null) return langKey.value(); + + Config.LangKeyPattern pattern = getClassOrBaseAnnotation(configClass, Config.LangKeyPattern.class); + String name = Optional.ofNullable(fieldName).orElse(categoryName); + if (pattern == null) return name; + String patternStr = pattern.pattern(); + + if (!patternStr.contains("%field") || !patternStr.contains(".")) { + throw new ConfigException("Invalid pattern for class " + configClass.getName() + ": " + patternStr); + } + + Config cfg = getClassOrBaseAnnotation(configClass, Config.class); + // Config annotation can't be null at this point + assert cfg != null; + + return buildKeyFromPattern(patternStr, cfg.modid(), cfg.filename(), categoryName, name); + } + + private static String buildKeyFromPattern(String pattern, String modId, String fileName, String categoryName, + String fieldName) { + StringBuilder s = new StringBuilder(pattern); + String[] replacements = new String[] { modId, fileName, categoryName, fieldName }; + boolean isCategory = categoryName.equals(fieldName); + for (int i = 0; i < langKeyPlaceholders.length; i++) { + String placeholder = langKeyPlaceholders[i]; + int index = s.indexOf(placeholder); + if (index == -1) continue; + int nextIndex = index + placeholder.length(); + if (isCategory && "%field".equals(placeholder)) { + if (nextIndex + 1 <= s.length() && s.charAt(nextIndex + 1) == '.') { + s.delete(index, nextIndex + 1); + } else { + s.delete(index - 1, nextIndex); + } + continue; + } + s.replace(index, nextIndex, replacements[i].toLowerCase()); + } + + return s.toString(); + } + + private static @Nullable A getClassOrBaseAnnotation(Class clazz, + Class annotationClass) { + A annotation = clazz.getAnnotation(annotationClass); + if (annotation != null || !clazz.isMemberClass()) return annotation; + + while (clazz.isMemberClass()) { + clazz = clazz.getDeclaringClass(); + } + + return clazz.getAnnotation(annotationClass); + } + + private static boolean isFieldSubCategory(@Nullable Field field) { + if (field == null) return false; + Class fieldClass = field.getType(); return !ConfigFieldParser.canParse(field) && fieldClass.getSuperclass() != null && fieldClass.getSuperclass().equals(Object.class);