Adding config GUIs to the @Config based configuration System (#3735)

Lots of internal API that modders should not touch. See test mods for example usages.
This commit is contained in:
sinus 2017-04-01 23:24:50 +02:00 committed by LexManos
parent 72937c90be
commit 25497d310b
20 changed files with 1665 additions and 439 deletions

View File

@ -102,9 +102,16 @@ public class ForgeGuiFactory implements IModGuiFactory
{
@Override
public void initialize(Minecraft minecraftInstance) {}
@Override
public Class<? extends GuiScreen> mainConfigGuiClass() { return ForgeConfigGui.class; }
public boolean hasConfigGui() { return true; }
@Override
public GuiScreen createConfigGui(GuiScreen parent) { return new ForgeConfigGui(parent); }
@Override
public Class<? extends GuiScreen> mainConfigGuiClass() { return null; }
@Override
public Set<RuntimeOptionCategoryElement> runtimeGuiCategories() { return null; }

View File

@ -428,7 +428,7 @@ public class ForgeModContainer extends DummyModContainer implements WorldAccessC
NetworkRegistry.INSTANCE.register(this, this.getClass(), "*", evt.getASMHarvestedData());
ForgeNetworkHandler.registerChannel(this, evt.getSide());
ConfigManager.load(this.getModId(), Config.Type.INSTANCE);
ConfigManager.sync(this.getModId(), Config.Type.INSTANCE);
}
@Subscribe

View File

@ -66,7 +66,7 @@ public @interface Config
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface LangKey
{
String value();
@ -101,4 +101,14 @@ public @interface Config
{
String value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface RequiresMcRestart
{}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface RequiresWorldRestart
{}
}

View File

@ -348,6 +348,8 @@ public class ConfigCategory implements Map<String, Property>
char type = prop.getType().getID();
write(out, pad1, String.valueOf(type), ":", propName, "=", prop.getString());
}
prop.resetChangedState();
}
if (children.size() > 0)

View File

@ -27,9 +27,14 @@ package net.minecraftforge.common.config;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import net.minecraftforge.fml.client.config.ConfigGuiType;
import net.minecraftforge.fml.client.config.DummyConfigElement.DummyCategoryElement;
import net.minecraftforge.fml.client.config.GuiConfigEntries.IConfigEntry;
import net.minecraftforge.fml.client.config.GuiEditArrayEntries.IArrayEntry;
import net.minecraftforge.fml.client.config.IConfigElement;
@ -360,4 +365,57 @@ public class ConfigElement implements IConfigElement
{
return isProperty ? prop.getMaxValue() : null;
}
/**
* Provides a ConfigElement derived from the annotation-based config system
* @param configClass the class which contains the configuration
* @return A ConfigElement based on the described category.
*/
public static IConfigElement from(Class<?> configClass)
{
Config annotation = configClass.getAnnotation(Config.class);
if (annotation == null)
throw new RuntimeException(String.format("The class '%s' has no @Config annotation!", configClass.getName()));
Configuration config = ConfigManager.getConfiguration(annotation.modid(), annotation.name());
if (config == null)
{
String error = String.format("The configuration '%s' of mod '%s' isn't loaded with the ConfigManager!", annotation.name(), annotation.modid());
throw new RuntimeException(error);
}
String name = Strings.isNullOrEmpty(annotation.name()) ? annotation.modid() : annotation.name();
String langKey = name;
Config.LangKey langKeyAnnotation = configClass.getAnnotation(Config.LangKey.class);
if (langKeyAnnotation != null)
{
langKey = langKeyAnnotation.value();
}
if (annotation.category().isEmpty())
{
List<IConfigElement> elements = Lists.newArrayList();
Set<String> catNames = config.getCategoryNames();
for (String catName : catNames)
{
if (catName.isEmpty())
continue;
ConfigCategory category = config.getCategory(catName);
DummyCategoryElement element = new DummyCategoryElement(category.getName(), category.getLanguagekey(), new ConfigElement(category).getChildElements());
element.setRequiresMcRestart(category.requiresMcRestart());
element.setRequiresWorldRestart(category.requiresWorldRestart());
elements.add(element);
}
return new DummyCategoryElement(name, langKey, elements);
}
else
{
ConfigCategory category = config.getCategory(annotation.category());
DummyCategoryElement element = new DummyCategoryElement(name, langKey, new ConfigElement(category).getChildElements());
element.setRequiresMcRestart(category.requiresMcRestart());
element.setRequiresWorldRestart(category.requiresWorldRestart());
return element;
}
}
}

View File

@ -22,27 +22,23 @@ package net.minecraftforge.common.config;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;
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.Lists;
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.common.config.Config.RangeDouble;
import net.minecraftforge.common.config.Config.RangeInt;
import net.minecraftforge.fml.common.FMLLog;
import net.minecraftforge.fml.common.Loader;
import net.minecraftforge.fml.common.LoaderException;
@ -53,9 +49,9 @@ import net.minecraftforge.fml.common.discovery.asm.ModAnnotation.EnumHolder;
public class ConfigManager
{
private static Map<String, Multimap<Config.Type, ASMData>> asm_data = Maps.newHashMap();
private static Map<Class<?>, ITypeAdapter> ADAPTERS = Maps.newHashMap();
private static Map<Class<?>, ITypeAdapter.Map> MAP_ADAPTERS = Maps.newHashMap();
static Map<Class<?>, ITypeAdapter> ADAPTERS = Maps.newHashMap();
private static Map<String, Configuration> CONFIGS = Maps.newHashMap();
private static Map<String, Set<Class<?>>> MOD_CONFIG_CLASSES = Maps.newHashMap();
static
{
@ -93,8 +89,6 @@ public class ConfigManager
private static void register(Class<?> cls, ITypeAdapter adpt)
{
ADAPTERS.put(cls, adpt);
if (adpt instanceof ITypeAdapter.Map)
MAP_ADAPTERS.put(cls, (ITypeAdapter.Map)adpt);
}
public static void loadData(ASMDataTable data)
@ -116,8 +110,34 @@ public class ConfigManager
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();
@ -132,6 +152,11 @@ public class ConfigManager
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;
@ -141,15 +166,17 @@ public class ConfigManager
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;
}
createConfig(cfg, cls, modid, type == Config.Type.INSTANCE, category);
sync(cfg, cls, modid, category, loading, null);
cfg.save();
@ -161,160 +188,171 @@ public class ConfigManager
}
}
}
public static Class<?>[] getModConfigClasses(String modid)
{
return MOD_CONFIG_CLASSES.get(modid).toArray(new Class<?>[0]);
}
// =======================================================
// INTERNAL
// =======================================================
private static void createConfig(Configuration cfg, Class<?> cls, String modid, boolean isStatic, String category)
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()) != isStatic)
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());
createConfig(modid, category, cfg, f.getType(), f, null);
}
}
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);
private static final Joiner NEW_LINE = Joiner.on('\n');
private static final Joiner PIPE = Joiner.on('|');
@SuppressWarnings({ "unchecked", "rawtypes" })
private static void createConfig(String modid, String category, Configuration cfg, Class<?> ftype, Field f, Object instance)
{
Property prop = null;
String comment = null;
Comment ca = f.getAnnotation(Comment.class);
if (ca != null)
comment = NEW_LINE.join(ca.value());
String langKey = modid + "." + category + "." + f.getName().toLowerCase(Locale.ENGLISH);
LangKey la = f.getAnnotation(LangKey.class);
if (la != null)
langKey = la.value();
ITypeAdapter adapter = ADAPTERS.get(ftype);
if (adapter != null)
{
if (category.isEmpty())
throw new RuntimeException("Can not specify a primitive field when the category is empty: " + f.getDeclaringClass() +"/" + f.getName());
prop = adapter.getProp(cfg, category, f, instance, comment);
set(instance, f, adapter.getValue(prop));
}
else if (ftype.getSuperclass() == Enum.class)
{
if (category.isEmpty())
throw new RuntimeException("Can not specify a primitive field when the category is empty: " + f.getDeclaringClass() +"/" + f.getName());
Enum enu = (Enum)get(instance, f);
prop = cfg.get(category, getName(f), enu.name(), comment);
prop.setValidationPattern(makePattern((Class<? extends Enum>)ftype));
set(instance, f, Enum.valueOf((Class<? extends Enum>)ftype, prop.getString()));
}
else if (ftype == Map.class)
{
if (category.isEmpty())
throw new RuntimeException("Can not specify a primitive field when the category is empty: " + f.getDeclaringClass() +"/" + f.getName());
String sub = category + "." + getName(f).toLowerCase(Locale.ENGLISH);
Map<String, Object> m = (Map<String, Object>)get(instance, f);
ParameterizedType type = (ParameterizedType)f.getGenericType();
Type mtype = type.getActualTypeArguments()[1];
cfg.getCategory(sub).setComment(comment);
for (Entry<String, Object> e : m.entrySet())
if (FieldWrapper.hasWrapperFor(f)) //Access the field
{
ITypeAdapter.Map adpt = MAP_ADAPTERS.get(mtype);
if (adpt != null)
if (Strings.isNullOrEmpty(category))
throw new RuntimeException("An empty category may not contain anything but objects representing categories!");
try
{
prop = adpt.getProp(cfg, sub, e.getKey(), e.getValue());
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);
}
else if (mtype instanceof Class && ((Class<?>)mtype).getSuperclass() == Enum.class)
catch (Exception e) //If anything goes wrong, add the errored field and class.
{
prop = TypeAdapters.Str.getProp(cfg, sub, e.getKey(), ((Enum)e.getValue()).name());
prop.setValidationPattern(makePattern((Class<? extends Enum>)mtype));
String format = "Error syncing field '%s' of class '%s'!";
String error = String.format(format, f.getName(), cls.getName());
throw new RuntimeException(error, e);
}
else
throw new RuntimeException("Unknown type in map! " + f.getDeclaringClass() + "/" + f.getName() + " " + mtype);
prop.setLanguageKey(langKey + "." + e.getKey().toLowerCase(Locale.ENGLISH));
}
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);
}
prop = null;
}
else if (ftype.getSuperclass() == Object.class) //Only support classes that are one level below Object.
}
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)
{
String sub = (category.isEmpty() ? "" : category + ".") + getName(f).toLowerCase(Locale.ENGLISH);
cfg.getCategory(sub).setComment(comment);
Object sinst = get(instance, f);
for (Field sf : ftype.getDeclaredFields())
{
if (!Modifier.isPublic(sf.getModifiers()))
continue;
createConfig(modid, sub, cfg, sf.getType(), sf, sinst);
}
if (isList)
prop = new Property(property, new String[0], type);
else
prop = new Property(property, (String)null, type);
cfg.getCategory(category).put(property, prop);
}
// TODO Lists ? other stuff
else
throw new RuntimeException("Unknown type in config! " + f.getDeclaringClass() + "/" + f.getName() + " " + ftype);
if (prop != null)
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))
{
prop.setLanguageKey(langKey);
RangeInt ia = f.getAnnotation(RangeInt.class);
if (ia != null)
{
prop.setMinValue(ia.min());
prop.setMaxValue(ia.max());
if (comment != null)
prop.setComment(NEW_LINE.join(new String[]{comment, "Min: " + ia.min(), "Max: " + ia.max()}));
else
prop.setComment(NEW_LINE.join(new String[]{"Min: " + ia.min(), "Max: " + ia.max()}));
}
RangeDouble da = f.getAnnotation(RangeDouble.class);
if (da != null)
{
prop.setMinValue(da.min());
prop.setMaxValue(da.max());
if (comment != null)
prop.setComment(NEW_LINE.join(new String[]{comment, "Min: " + da.min(), "Max: " + da.max()}));
else
prop.setComment(NEW_LINE.join(new String[]{"Min: " + da.min(), "Max: " + da.max()}));
}
//TODO List length values
if (property.hasChanged())
return false;
else
return true;
}
}
private static void set(Object instance, Field f, Object v)
{
try {
f.set(instance, v);
} catch (Exception e) {
e.printStackTrace();
}
}
private static Object get(Object instance, Field f)
{
try {
return f.get(instance);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@SuppressWarnings("rawtypes")
private static Pattern makePattern(Class<? extends Enum> cls)
{
List<String> lst = Lists.newArrayList();
for (Enum e : cls.getEnumConstants())
lst.add(e.name());
return Pattern.compile(PIPE.join(lst));
return false;
}
private static String getName(Field f)

View File

@ -0,0 +1,396 @@
package net.minecraftforge.common.config;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import java.util.Set;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import net.minecraftforge.common.config.Config.RangeDouble;
import net.minecraftforge.common.config.Config.RangeInt;
import static net.minecraftforge.common.config.ConfigManager.*;
public abstract class FieldWrapper implements IFieldWrapper
{
protected String category, name;
protected Field field;
protected Object instance;
public FieldWrapper(String category, Field field, Object instance)
{
this.instance = instance;
this.field = field;
this.category = category;
this.name = field.getName();
if (field.isAnnotationPresent(Config.Name.class))
this.name = field.getAnnotation(Config.Name.class).value();
this.field.setAccessible(true); // Just in case
}
public static IFieldWrapper get(Object instance, Field field, String category)
{
if (ADAPTERS.get(field.getType()) != null)
return new PrimitiveWrapper(category, field, instance);
else if (Enum.class.isAssignableFrom(field.getType()))
return new EnumWrapper(category, field, instance);
else if (Map.class.isAssignableFrom(field.getType()))
return new MapWrapper(category, field, instance);
else if (field.getType().getSuperclass().equals(Object.class))
throw new RuntimeException("Objects should not be handled by field wrappers");
else
throw new IllegalArgumentException(String.format("Fields of type '%s' are not supported!", field.getType().getCanonicalName()));
}
public static boolean hasWrapperFor(Field field)
{
if (ADAPTERS.get(field.getType()) != null)
return true;
else if (Enum.class.isAssignableFrom(field.getType()))
return true;
else if (Map.class.isAssignableFrom(field.getType()))
return true;
return false;
}
private static class MapWrapper extends FieldWrapper
{
private Map<String, Object> theMap = null;
private Type mType;
@SuppressWarnings("unchecked")
private MapWrapper(String category, Field field, Object instance)
{
super(category, field, instance);
try
{
theMap = (Map<String, Object>) field.get(instance);
}
catch (ClassCastException cce)
{
throw new IllegalArgumentException(String.format("The map '%s' of class '%s' must have the key type String!", field.getName(),
field.getDeclaringClass().getCanonicalName()), cce);
}
catch (Exception e)
{
Throwables.propagate(e);
}
ParameterizedType type = (ParameterizedType) field.getGenericType();
mType = type.getActualTypeArguments()[1];
if (ADAPTERS.get(mType) == null && !Enum.class.isAssignableFrom((Class<?>) mType))
throw new IllegalArgumentException(String.format("The map '%s' of class '%s' has target values which are neither primitive nor an enum!",
field.getName(), field.getDeclaringClass().getCanonicalName()));
}
@Override
public ITypeAdapter getTypeAdapter()
{
ITypeAdapter adapter = ADAPTERS.get(mType);
if (adapter == null && Enum.class.isAssignableFrom((Class<?>) mType))
adapter = TypeAdapters.Str;
return adapter;
}
@Override
public String[] getKeys()
{
Set<String> keys = theMap.keySet();
String[] keyArray = new String[keys.size()];
Iterator<String> it = keys.iterator();
for (int i = 0; i < keyArray.length; i++)
{
keyArray[i] = category + "." + name + "." + it.next();
}
return keyArray;
}
@Override
public Object getValue(String key)
{
return theMap.get(key.replaceFirst(category + "." + name + ".", ""));
}
@Override
public void setValue(String key, Object value)
{
String suffix = key.replaceFirst(category + "." + name + ".", "");
theMap.put(suffix, value);
}
@Override
public boolean hasKey(String name)
{
return theMap.containsKey(name);
}
@Override
public boolean handlesKey(String name)
{
if (name == null)
return false;
return name.startsWith(category + "." + name + ".");
}
@Override
public void setupConfiguration(Configuration cfg, String desc, String langKey, boolean reqMCRestart, boolean reqWorldRestart)
{
ConfigCategory confCat = cfg.getCategory(category);
confCat.setComment(desc);
confCat.setLanguageKey(langKey);
confCat.setRequiresMcRestart(reqMCRestart);
confCat.setRequiresWorldRestart(reqWorldRestart);
}
@Override
public String getCategory()
{
return category + "." + name;
}
}
private static class EnumWrapper extends SingleValueFieldWrapper
{
private EnumWrapper(String category, Field field, Object instance)
{
super(category, field, instance);
}
@Override
public ITypeAdapter getTypeAdapter()
{
return TypeAdapters.Str;
}
@Override
public Object getValue(String key)
{
if (!hasKey(key))
throw new IllegalArgumentException("Unsupported Key!");
try
{
@SuppressWarnings("rawtypes")
Enum enu = (Enum) field.get(instance);
return enu.name();
}
catch (Exception e)
{
Throwables.propagate(e);
}
return null;
}
@Override
public void setValue(String key, Object value)
{
if (!hasKey(key))
throw new IllegalArgumentException("Unsupported Key!");
@SuppressWarnings({ "unchecked", "rawtypes" })
Enum enu = Enum.valueOf((Class<? extends Enum>) field.getType(), (String) value);
try
{
field.set(instance, enu);
}
catch (Exception e)
{
Throwables.propagate(e);
}
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public void setupConfiguration(Configuration cfg, String desc, String langKey, boolean reqMCRestart, boolean reqWorldRestart)
{
super.setupConfiguration(cfg, desc, langKey, reqMCRestart, reqWorldRestart);
Property prop = cfg.getCategory(this.category).get(this.name); // Will be setup in general by ConfigManager
List<String> lst = Lists.newArrayList();
for (Enum e : ((Class<? extends Enum>) field.getType()).getEnumConstants())
lst.add(e.name());
prop.setValidationPattern(Pattern.compile(PIPE.join(lst)));
prop.setValidValues(lst.toArray(new String[0]));
String validValues = NEW_LINE.join(lst);
if (desc != null)
prop.setComment(NEW_LINE.join(new String[] { desc, "Valid values:" }) + "\n" + validValues);
else
prop.setComment("Valid values:" + "\n" + validValues);
}
}
private static class PrimitiveWrapper extends SingleValueFieldWrapper
{
private PrimitiveWrapper(String category, Field field, Object instance)
{
super(category, field, instance);
}
@Override
public ITypeAdapter getTypeAdapter()
{
return ADAPTERS.get(field.getType());
}
@Override
public Object getValue(String key)
{
if (!hasKey(key))
throw new IllegalArgumentException("Unknown key!");
try
{
return field.get(instance);
}
catch (Exception e)
{
Throwables.propagate(e);
}
return null;
}
@Override
public void setValue(String key, Object value)
{
if (!hasKey(key))
throw new IllegalArgumentException("Unknown key: " + key);
try
{
field.set(instance, value);
}
catch (Exception e)
{
Throwables.propagate(e);
}
}
public void setupConfiguration(Configuration cfg, String desc, String langKey, boolean reqMCRestart, boolean reqWorldRestart)
{
super.setupConfiguration(cfg, desc, langKey, reqMCRestart, reqWorldRestart);
Property prop = cfg.getCategory(this.category).get(this.name);
RangeInt ia = field.getAnnotation(RangeInt.class);
if (ia != null)
{
prop.setMinValue(ia.min());
prop.setMaxValue(ia.max());
if (desc != null)
prop.setComment(NEW_LINE.join(new String[] { desc, "Min: " + ia.min(), "Max: " + ia.max() }));
else
prop.setComment(NEW_LINE.join(new String[] { "Min: " + ia.min(), "Max: " + ia.max() }));
}
RangeDouble da = field.getAnnotation(RangeDouble.class);
if (da != null)
{
prop.setMinValue(da.min());
prop.setMaxValue(da.max());
if (desc != null)
prop.setComment(NEW_LINE.join(new String[] { desc, "Min: " + da.min(), "Max: " + da.max() }));
else
prop.setComment(NEW_LINE.join(new String[] { "Min: " + da.min(), "Max: " + da.max() }));
}
}
}
private static abstract class SingleValueFieldWrapper extends FieldWrapper
{
private SingleValueFieldWrapper(String category, Field field, Object instance)
{
super(category, field, instance);
}
@Override
public String[] getKeys()
{
return asArray(this.category + "." + this.name);
}
@Override
public boolean hasKey(String name)
{
return (this.category + "." + this.name).equals(name);
}
@Override
public boolean handlesKey(String name)
{
return hasKey(name);
}
@Override
public void setupConfiguration(Configuration cfg, String desc, String langKey, boolean reqMCRestart, boolean reqWorldRestart)
{
Property prop = cfg.getCategory(this.category).get(this.name); // Will be setup in general by ConfigManager
prop.setComment(desc);
prop.setLanguageKey(langKey);
prop.setRequiresMcRestart(reqMCRestart);
prop.setRequiresWorldRestart(reqWorldRestart);
}
@Override
public String getCategory()
{
return category;
}
}
private static <T> T[] asArray(T... in)
{
return in;
}
public static class BeanEntry<K, V> implements Entry<K, V>
{
private K key;
private V value;
public BeanEntry(K key, V value)
{
this.key = key;
this.value = value;
}
@Override
public K getKey()
{
return key;
}
@Override
public V getValue()
{
return value;
}
@Override
public V setValue(V value)
{
throw new UnsupportedOperationException("This is a static bean.");
}
}
}

View File

@ -0,0 +1,35 @@
package net.minecraftforge.common.config;
/**
* The objects are expected to get their wrapped field, the owning class, instance and category string on initialization.
*/
public interface IFieldWrapper
{
/**
* @return The type adapter to serialize the values returned by getValue. Null if non-primitive.
*/
public ITypeAdapter getTypeAdapter();
/**
* @param field the field about which to retrieve information
* @param instance The instance whose field shall be queried.
* @return a list of keys handled by this field
*/
public String[] getKeys();
public Object getValue(String key);
public void setValue(String key, Object value);
public boolean hasKey(String name);
public boolean handlesKey(String name);
public void setupConfiguration(Configuration cfg, String desc, String langKey, boolean reqMCRestart, boolean reqWorldRestart);
/**
* @return the category name in which the entries should be saved.
*/
public String getCategory();
}

View File

@ -18,16 +18,17 @@
*/
package net.minecraftforge.common.config;
import java.lang.reflect.Field;
import net.minecraftforge.common.config.Property.Type;
interface ITypeAdapter
{
Property getProp(Configuration cfg, String category, Field field, Object instance, String comment);
void setDefaultValue(Property property, Object value);
void setValue(Property property, Object value);
Object getValue(Property prop);
public interface Map extends ITypeAdapter
{
Property getProp(Configuration cfg, String category, String name, Object value);
}
Type getType();
boolean isArrayAdapter();
}

View File

@ -0,0 +1,63 @@
package net.minecraftforge.fml.client;
import java.util.Set;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiScreen;
import net.minecraftforge.fml.client.config.GuiConfig;
import net.minecraftforge.fml.common.ModContainer;
public class DefaultGuiFactory implements IModGuiFactory
{
protected String modid, title;
protected Minecraft minecraft;
protected DefaultGuiFactory(String modid, String title)
{
this.modid = modid;
this.title = title;
}
@Override
public boolean hasConfigGui()
{
return true;
}
@Override
public void initialize(Minecraft minecraftInstance)
{
this.minecraft = minecraftInstance;
}
@Override
public GuiScreen createConfigGui(GuiScreen parentScreen)
{
return new GuiConfig(parentScreen, modid, title);
}
@Deprecated
@Override
public Class<? extends GuiScreen> mainConfigGuiClass()
{
return null;
}
@Override
public Set<RuntimeOptionCategoryElement> runtimeGuiCategories()
{
return null;
}
@Override
public RuntimeOptionGuiHandler getHandlerFor(RuntimeOptionCategoryElement element)
{
return null;
}
public static IModGuiFactory forMod(ModContainer mod)
{
return new DefaultGuiFactory(mod.getModId(), mod.getName());
}
}

View File

@ -371,6 +371,7 @@ public class FMLClientHandler implements IFMLSidedHandler
String className = mc.getGuiClassName();
if (Strings.isNullOrEmpty(className))
{
guiFactories.put(mc, DefaultGuiFactory.forMod(mc));
continue;
}
try

View File

@ -104,16 +104,29 @@ public class FMLConfigGuiFactory implements IModGuiFactory
return list;
}
}
@Override
public boolean hasConfigGui()
{
return true;
}
@Override
public void initialize(Minecraft minecraftInstance)
{
}
@Override
public GuiScreen createConfigGui(GuiScreen parentScreen)
{
return new FMLConfigGuiScreen(parentScreen);
}
@Override
public Class<? extends GuiScreen> mainConfigGuiClass()
{
return FMLConfigGuiScreen.class;
return null;
}
private static final Set<RuntimeOptionCategoryElement> fmlCategories = ImmutableSet.of(new RuntimeOptionCategoryElement("HELP", "FML"));

View File

@ -282,7 +282,15 @@ public class GuiModList extends GuiScreen
try
{
IModGuiFactory guiFactory = FMLClientHandler.instance().getGuiFactoryFor(selectedMod);
GuiScreen newScreen = guiFactory.mainConfigGuiClass().getConstructor(GuiScreen.class).newInstance(this);
GuiScreen newScreen = null;
try
{
newScreen = guiFactory.createConfigGui(this);
}
catch (AbstractMethodError error)
{
newScreen = guiFactory.mainConfigGuiClass().getConstructor(GuiScreen.class).newInstance(this);
}
this.mc.displayGuiScreen(newScreen);
}
catch (Exception e)
@ -415,8 +423,18 @@ public class GuiModList extends GuiScreen
IModGuiFactory guiFactory = FMLClientHandler.instance().getGuiFactoryFor(selectedMod);
configModButton.visible = true;
configModButton.enabled = guiFactory != null && guiFactory.mainConfigGuiClass() != null;
configModButton.enabled = false;
if (guiFactory != null)
{
try
{
configModButton.enabled = guiFactory.hasConfigGui();
}
catch(AbstractMethodError error)
{
configModButton.enabled = guiFactory.mainConfigGuiClass() != null;
}
}
lines.add(selectedMod.getMetadata().name);
lines.add(String.format("Version: %s (%s)", selectedMod.getDisplayVersion(), selectedMod.getVersion()));
lines.add(String.format("Mod ID: '%s' Mod State: %s", selectedMod.getModId(), Loader.instance().getModState(selectedMod)));

View File

@ -27,6 +27,11 @@ import net.minecraft.client.gui.GuiScreen;
import javax.annotation.Nullable;
/**
* This is the interface you need to implement if you want to provide a customized config screen.
* {@link DefaultGuiFactory} provides a default implementation of this interface and will be used
* if the mod does not specify anything else.
*/
public interface IModGuiFactory {
/**
* Called when instantiated to initialize with the active minecraft instance.
@ -34,6 +39,35 @@ public interface IModGuiFactory {
* @param minecraftInstance the instance
*/
public void initialize(Minecraft minecraftInstance);
/**
* If this method returns false, the config button in the mod list will be disabled
* @return true if this object provides a config gui screen, false otherwise
*/
public boolean hasConfigGui();
/**
* Return an initialized {@link GuiScreen}. This screen will be displayed
* when the "config" button is pressed in the mod list. It will
* have a single argument constructor - the "parent" screen, the same as all
* Minecraft GUIs. The expected behaviour is that this screen will replace the
* "mod list" screen completely, and will return to the mod list screen through
* the parent link, once the appropriate action is taken from the config screen.
*
* This config GUI is anticipated to provide configuration to the mod in a friendly
* visual way. It should not be abused to set internals such as IDs (they're gonna
* keep disappearing anyway), but rather, interesting behaviours. This config GUI
* is never run when a server game is running, and should be used to configure
* desired behaviours that affect server state. Costs, mod game modes, stuff like that
* can be changed here.
*
* @param parentScreen The screen to which must be returned when closing the
* returned screen.
* @return A class that will be instantiated on clicks on the config button
* or null if no GUI is desired.
*/
public GuiScreen createConfigGui(GuiScreen parentScreen);
/**
* Return the name of a class extending {@link GuiScreen}. This class will
* be instantiated when the "config" button is pressed in the mod list. It will
@ -52,12 +86,13 @@ public interface IModGuiFactory {
* desired behaviours that affect server state. Costs, mod game modes, stuff like that
* can be changed here.
*
* @deprecated The method {@link IModGuiFactory.maingConfigGui(GuiScreen} is the recommended method.
* @return A class that will be instantiated on clicks on the config button
* or null if no GUI is desired.
*/
@Deprecated
public Class<? extends GuiScreen> mainConfigGuiClass();
/**
* Return a list of the "runtime" categories this mod wishes to populate with
* GUI elements.

View File

@ -25,6 +25,7 @@ import static net.minecraftforge.fml.client.config.GuiUtils.UNDO_CHAR;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import net.minecraft.client.Minecraft;
@ -33,6 +34,8 @@ import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.resources.I18n;
import net.minecraft.util.text.TextComponentString;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.config.ConfigElement;
import net.minecraftforge.common.config.ConfigManager;
import net.minecraftforge.fml.client.config.GuiConfigEntries.IConfigEntry;
import net.minecraftforge.fml.client.event.ConfigChangedEvent;
import net.minecraftforge.fml.client.event.ConfigChangedEvent.OnConfigChangedEvent;
@ -80,6 +83,59 @@ public class GuiConfig extends GuiScreen
protected HoverChecker undoHoverChecker;
protected HoverChecker resetHoverChecker;
protected HoverChecker checkBoxHoverChecker;
/**
* This constructor handles the {@code @Config} configuration classes
* @param parentScreen the parent GuiScreen object
* @param mod the mod for which to create a screen
*/
public GuiConfig(GuiScreen parentScreen, String modid, String title)
{
this(parentScreen, modid, false, false, title, ConfigManager.getModConfigClasses(modid));
}
/**
*
* @param parentScreen the parrent GuiScreen object
* @param modID the mod ID for the mod whose config settings will be editted
* @param allRequireWorldRestart whether all config elements on this screen require a world restart
* @param allRequireMcRestart whether all config elements on this screen require a game restart
* @param title the desired title for this screen. For consistency it is recommended that you pass the path of the config file being
* edited.
* @param configClasses an array of classes annotated with {@code @Config} providing the configuration
*/
public GuiConfig(GuiScreen parentScreen, String modID, boolean allRequireWorldRestart, boolean allRequireMcRestart, String title,
Class<?>... configClasses)
{
this(parentScreen, collectConfigElements(configClasses), modID, null, allRequireWorldRestart, allRequireMcRestart, title, null);
}
private static List<IConfigElement> collectConfigElements(Class<?>[] configClasses)
{
List<IConfigElement> toReturn;
if(configClasses.length == 1)
{
toReturn = ConfigElement.from(configClasses[0]).getChildElements();
}
else
{
toReturn = new ArrayList<IConfigElement>();
for(Class<?> clazz : configClasses)
{
toReturn.add(ConfigElement.from(clazz));
}
}
toReturn.sort(new Comparator<IConfigElement>(){
@Override
public int compare(IConfigElement e1, IConfigElement e2)
{
return I18n.format(e1.getLanguageKey()).compareTo(I18n.format(e2.getLanguageKey()));
}
});
return toReturn;
}
/**
* GuiConfig constructor that will use ConfigChangedEvent when editing is concluded. If a non-null value is passed for configID,
@ -167,7 +223,25 @@ public class GuiConfig extends GuiScreen
this.entryList = new GuiConfigEntries(this, mc);
this.initEntries = new ArrayList<IConfigEntry>(entryList.listEntries);
this.allRequireWorldRestart = allRequireWorldRestart;
IF:if (!allRequireWorldRestart)
{
for (IConfigElement element : configElements)
{
if (!element.requiresWorldRestart());
break IF;
}
allRequireWorldRestart = true;
}
this.allRequireMcRestart = allRequireMcRestart;
IF:if (!allRequireMcRestart)
{
for (IConfigElement element : configElements)
{
if (!element.requiresMcRestart());
break IF;
}
allRequireMcRestart = true;
}
this.modID = modID;
this.configID = configID;
this.isWorldRunning = mc.world != null;

View File

@ -617,7 +617,7 @@ public class FMLModContainer implements ModContainer
}
ProxyInjector.inject(this, event.getASMHarvestedData(), FMLCommonHandler.instance().getSide(), getLanguageAdapter());
AutomaticEventSubscriber.inject(this, event.getASMHarvestedData(), FMLCommonHandler.instance().getSide());
ConfigManager.load(this.getModId(), Config.Type.INSTANCE);
ConfigManager.sync(this.getModId(), Config.Type.INSTANCE);
processFieldAnnotations(event.getASMHarvestedData());
}

View File

@ -1,21 +1,54 @@
package net.minecraftforge.debug;
import java.util.Map;
import java.util.Set;
import com.google.common.base.Function;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiScreen;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.config.Config;
import net.minecraftforge.common.config.ConfigManager;
import net.minecraftforge.common.config.Config.*;
import net.minecraftforge.fml.client.IModGuiFactory;
import net.minecraftforge.fml.client.config.GuiConfig;
import net.minecraftforge.fml.client.event.ConfigChangedEvent.OnConfigChangedEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.event.FMLInitializationEvent;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
@Mod(modid = ConfigTest.MODID, name = "ConfigTest", version = "1.0", acceptableRemoteVersions = "*")
public class ConfigTest
{
public static final String MODID = "config_test";
@Mod.EventHandler
public void preInit(FMLPreInitializationEvent event) {
MinecraftForge.EVENT_BUS.register(this);
}
@Mod.EventHandler
public void init(FMLInitializationEvent event) {
System.out.println("Old: " + CONFIG_TYPES.bool);
CONFIG_TYPES.bool = !CONFIG_TYPES.bool;
System.out.println("New: " + CONFIG_TYPES.bool);
ConfigManager.sync(MODID, Type.INSTANCE);
System.out.println("After sync: " + CONFIG_TYPES.bool);
}
@SubscribeEvent
public void onConfigChangedEvent(OnConfigChangedEvent event) {
if (event.getModID().equals(MODID))
{
ConfigManager.sync(MODID, Type.INSTANCE);
}
}
@LangKey("config_test.config.types")
@Config(modid = MODID, type = Type.INSTANCE, name = MODID + "_types")
public static class CONFIG_TYPES
{
@ -58,6 +91,7 @@ public class ConfigTest
public String HeyLook = "I'm Inside!";
}
}
@LangKey("config_test.config.annotations")
@Config(modid = MODID)
public static class CONFIG_ANNOTATIONS
{
@ -79,6 +113,7 @@ public class ConfigTest
public String HeyLook = "Go in!";
}
}
@LangKey("config_test.config.subcats")
@Config(modid = MODID, name = MODID + "_subcats", category = "")
public static class CONFIG_SUBCATS
{
@ -92,11 +127,34 @@ public class ConfigTest
public static class SubCat
{
@Name("i_say")
public static String value;
public String value;
public SubCat(String value)
{
this.value = value;
}
}
}
@LangKey("config_test.config.maps")
@Config(modid = MODID, name = MODID + "_map")
public static class CONFIG_MAP
{
@Name("map")
@RequiresMcRestart
public static Map<String, Integer[]> theMap;
static
{
theMap = Maps.newHashMap();
for (int i = 0; i < 7; i++)
{
Integer[] array = new Integer[6];
for (int x = 0; x < array.length; x++)
{
array[x] = i + x;
}
theMap.put("" + i, array);
}
}
}
}

View File

@ -0,0 +1,4 @@
config_test.config.types=Field Types
config_test.config.annotations=Annotations
config_test.config.subcats=Subcategories
config_test.config.maps=Maps

View File

@ -0,0 +1,16 @@
[
{
"modid": "config_test",
"name": "ConfigTest",
"description": "Tests config",
"version": "1.0",
"mcversion": "",
"url": "",
"updateUrl": "",
"authorList": [""],
"credits": "",
"logoFile": "",
"screenshots": [],
"dependencies": []
}
]