/* * Minecraft Forge * Copyright (c) 2016. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation version 2.1 * of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ package net.minecraftforge.common.config; import java.io.File; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.Locale; import java.util.Map; import java.util.Set; import org.apache.logging.log4j.Level; import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import net.minecraftforge.common.config.Config.Comment; import net.minecraftforge.common.config.Config.LangKey; import net.minecraftforge.common.config.Config.Name; import net.minecraftforge.fml.common.FMLLog; import net.minecraftforge.fml.common.Loader; import net.minecraftforge.fml.common.LoaderException; import net.minecraftforge.fml.common.discovery.ASMDataTable; import net.minecraftforge.fml.common.discovery.ASMDataTable.ASMData; import net.minecraftforge.fml.common.discovery.asm.ModAnnotation.EnumHolder; public class ConfigManager { private static Map<String, Multimap<Config.Type, ASMData>> asm_data = Maps.newHashMap(); static Map<Class<?>, ITypeAdapter> ADAPTERS = Maps.newHashMap(); static Map<Class<?>, Class<?>> ARRAY_REMAP = Maps.newHashMap(); private static Map<String, Configuration> CONFIGS = Maps.newHashMap(); private static Map<String, Set<Class<?>>> MOD_CONFIG_CLASSES = Maps.newHashMap(); static { register(boolean.class, TypeAdapters.bool); register(boolean[].class, TypeAdapters.boolA); register(Boolean.class, TypeAdapters.Bool); register(Boolean[].class, TypeAdapters.BoolA); register(float.class, TypeAdapters.flt); register(float[].class, TypeAdapters.fltA); register(Float.class, TypeAdapters.Flt); register(Float[].class, TypeAdapters.FltA); register(double.class, TypeAdapters.dbl); register(double[].class, TypeAdapters.dblA); register(Double.class, TypeAdapters.Dbl); register(Double[].class, TypeAdapters.DblA); register(byte.class, TypeAdapters.byt); register(byte[].class, TypeAdapters.bytA); register(Byte.class, TypeAdapters.Byt); register(Byte[].class, TypeAdapters.BytA); register(char.class, TypeAdapters.chr); register(char[].class, TypeAdapters.chrA); register(Character.class, TypeAdapters.Chr); register(Character[].class, TypeAdapters.ChrA); register(short.class, TypeAdapters.shrt); register(short[].class, TypeAdapters.shrtA); register(Short.class, TypeAdapters.Shrt); register(Short[].class, TypeAdapters.ShrtA); register(int.class, TypeAdapters.int_); register(int[].class, TypeAdapters.intA); register(Integer.class, TypeAdapters.Int); register(Integer[].class, TypeAdapters.IntA); register(String.class, TypeAdapters.Str); register(String[].class, TypeAdapters.StrA); ARRAY_REMAP.put(Boolean.class, Boolean[].class ); ARRAY_REMAP.put(Float.class, Float[].class ); ARRAY_REMAP.put(Double.class, Double[].class ); ARRAY_REMAP.put(Byte.class, Byte[].class ); ARRAY_REMAP.put(Character.class, Character[].class); ARRAY_REMAP.put(Short.class, Short[].class ); ARRAY_REMAP.put(Integer.class, Integer[].class ); ARRAY_REMAP.put(String.class, String[].class ); } private static void register(Class<?> cls, ITypeAdapter adpt) { ADAPTERS.put(cls, adpt); } public static void loadData(ASMDataTable data) { FMLLog.fine("Loading @Config anotation data"); for (ASMData target : data.getAll(Config.class.getName())) { String modid = (String)target.getAnnotationInfo().get("modid"); Multimap<Config.Type, ASMData> map = asm_data.get(modid); if (map == null) { map = ArrayListMultimap.create(); asm_data.put(modid, map); } EnumHolder tholder = (EnumHolder)target.getAnnotationInfo().get("type"); Config.Type type = tholder == null ? Config.Type.INSTANCE : Config.Type.valueOf(tholder.getValue()); map.put(type, target); } } /** * Bounces to sync(). * TODO: remove */ public static void load(String modid, Config.Type type) { sync(modid, type); } /** * Synchronizes configuration data between the file on disk, the {@code Configuration} object and the annotated * mod classes containing the configuration variables. * * When first called, this method will try to load the configuration from disk. If this fails, because the file * does not exist, it will be created with default values derived from the mods config classes variable default values * and comments and ranges, as well as configuration names based on the appropriate annotations found in {@code @Config}. * * Note, that this method is being called by the {@link FMLModContaier}, so the mod needn't call it in init(). * * If this method is called after the initial load, it will check whether the values in the Configuration object differ * from the values in the corresponding variables. If they differ, it will either overwrite the variables if the Configuration * object is marked as changed (e.g. if it was changed with the ConfigGui) or otherwise overwrite the Configuration object's values. * It then proceeds to saving the changes to disk. * @param modid the mod's ID for which the configuration shall be loaded * @param type the configuration type, currently always {@code Config.Type.INSTANCE} */ public static void sync(String modid, Config.Type type) { FMLLog.fine("Attempting to inject @Config classes into %s for type %s", modid, type); ClassLoader mcl = Loader.instance().getModClassLoader(); File configDir = Loader.instance().getConfigDir(); Multimap<Config.Type, ASMData> map = asm_data.get(modid); if (map == null) return; for (ASMData targ : map.get(type)) { try { Class<?> cls = Class.forName(targ.getClassName(), true, mcl); if (MOD_CONFIG_CLASSES.get(modid) == null) MOD_CONFIG_CLASSES.put(modid, Sets.<Class<?>>newHashSet()); MOD_CONFIG_CLASSES.get(modid).add(cls); String name = (String)targ.getAnnotationInfo().get("name"); if (name == null) name = modid; String category = (String)targ.getAnnotationInfo().get("category"); if (category == null) category = "general"; File file = new File(configDir, name + ".cfg"); boolean loading = false; Configuration cfg = CONFIGS.get(file.getAbsolutePath()); if (cfg == null) { cfg = new Configuration(file); cfg.load(); CONFIGS.put(file.getAbsolutePath(), cfg); loading = true; } sync(cfg, cls, modid, category, loading, null); cfg.save(); } catch (Exception e) { FMLLog.log(Level.ERROR, e, "An error occurred trying to load a config for %s into %s", modid, targ.getClassName()); throw new LoaderException(e); } } } public static Class<?>[] getModConfigClasses(String modid) { return (MOD_CONFIG_CLASSES.containsKey(modid) ? MOD_CONFIG_CLASSES.get(modid).toArray(new Class<?>[0]) : new Class<?>[0]); } public static boolean hasConfigForMod(String modid) { return asm_data.containsKey(modid); } // ======================================================= // INTERNAL // ======================================================= static Configuration getConfiguration(String modid, String name) { if (Strings.isNullOrEmpty(name)) name = modid; File configDir = Loader.instance().getConfigDir(); File configFile = new File(configDir, name + ".cfg"); return CONFIGS.get(configFile.getAbsolutePath()); } private static void sync(Configuration cfg, Class<?> cls, String modid, String category, boolean loading, Object instance) { for (Field f : cls.getDeclaredFields()) { if (!Modifier.isPublic(f.getModifiers())) continue; if (Modifier.isStatic(f.getModifiers()) != (instance == null)) continue; String comment = null; Comment ca = f.getAnnotation(Comment.class); if (ca != null) comment = NEW_LINE.join(ca.value()); String langKey = modid + "." + (category.isEmpty() ? "" : category + ".") + f.getName().toLowerCase(Locale.ENGLISH); LangKey la = f.getAnnotation(LangKey.class); if (la != null) langKey = la.value(); boolean requiresMcRestart = f.isAnnotationPresent(Config.RequiresMcRestart.class); boolean requiresWorldRestart = f.isAnnotationPresent(Config.RequiresWorldRestart.class); if (FieldWrapper.hasWrapperFor(f)) //Access the field { if (Strings.isNullOrEmpty(category)) throw new RuntimeException("An empty category may not contain anything but objects representing categories!"); try { IFieldWrapper wrapper = FieldWrapper.get(instance, f, category); ITypeAdapter adapt = wrapper.getTypeAdapter(); Property.Type propType = adapt.getType(); for (String key : wrapper.getKeys()) { String suffix = key.replaceFirst(wrapper.getCategory() + ".", ""); boolean existed = exists(cfg, wrapper.getCategory(), suffix); if (!existed || loading) //Creates keys in category specified by the wrapper if new ones are programaticaly added { Property property = property(cfg, wrapper.getCategory(), suffix, propType, adapt.isArrayAdapter()); adapt.setDefaultValue(property, wrapper.getValue(key)); if (!existed) adapt.setValue(property, wrapper.getValue(key)); else wrapper.setValue(key, adapt.getValue(property)); } else //If the key is not new, sync according to shoudlReadFromVar() { Property property = property(cfg, wrapper.getCategory(), suffix, propType, adapt.isArrayAdapter()); Object propVal = adapt.getValue(property); Object mapVal = wrapper.getValue(key); if (shouldReadFromVar(property, propVal, mapVal)) adapt.setValue(property, mapVal); else wrapper.setValue(key, propVal); } } ConfigCategory confCat = cfg.getCategory(wrapper.getCategory()); for (Property property : confCat.getOrderedValues())//Are new keys in the Configuration object? { if (!wrapper.handlesKey(property.getName())) continue; if (loading || !wrapper.hasKey(property.getName())) { Object value = wrapper.getTypeAdapter().getValue(property); wrapper.setValue(confCat.getName() + "." + property.getName(), value); } } if (loading) //Doing this after the loops. The wrapper should set cosmetic stuff. wrapper.setupConfiguration(cfg, comment, langKey, requiresMcRestart, requiresWorldRestart); } catch (Exception e) //If anything goes wrong, add the errored field and class. { String format = "Error syncing field '%s' of class '%s'!"; String error = String.format(format, f.getName(), cls.getName()); throw new RuntimeException(error, e); } } else if (f.getType().getSuperclass() != null && f.getType().getSuperclass().equals(Object.class)) //Descend the object tree { Object newInstance = null; try { newInstance = f.get(instance); } catch (Exception e) { //This should never happen. Previous checks should eliminate this. Throwables.propagate(e); } String sub = (category.isEmpty() ? "" : category + ".") + getName(f).toLowerCase(Locale.ENGLISH); ConfigCategory confCat = cfg.getCategory(sub); confCat.setComment(comment); confCat.setLanguageKey(langKey); confCat.setRequiresMcRestart(requiresMcRestart); confCat.setRequiresWorldRestart(requiresWorldRestart); sync(cfg, f.getType(), modid, sub, loading, newInstance); } else { String format = "Can't handle field '%s' of class '%s': Unknown type."; String error = String.format(format, f.getName(), cls.getCanonicalName()); throw new RuntimeException(error); } } } static final Joiner NEW_LINE = Joiner.on('\n'); static final Joiner PIPE = Joiner.on('|'); private static Property property(Configuration cfg, String category, String property, Property.Type type, boolean isList) { Property prop = cfg.getCategory(category).get(property); if (prop == null) { if (isList) prop = new Property(property, new String[0], type); else prop = new Property(property, (String)null, type); cfg.getCategory(category).put(property, prop); } return prop; } private static boolean exists(Configuration cfg, String category, String property) { return cfg.hasCategory(category) && cfg.getCategory(category).containsKey(property); } private static boolean shouldReadFromVar(Property property, Object propValue, Object fieldValue) { if (!propValue.equals(fieldValue)) { if (property.hasChanged()) return false; else return true; } return false; } private static String getName(Field f) { if (f.isAnnotationPresent(Name.class)) return f.getAnnotation(Name.class).value(); return f.getName(); } }