package openmods.config.properties;
import com.google.common.base.CharMatcher;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Map;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.config.Configuration;
import net.minecraftforge.common.config.Property;
import net.minecraftforge.common.config.Property.Type;
import openmods.Log;
import openmods.utils.io.IStringSerializer;
import openmods.utils.io.StringConversionException;
import openmods.utils.io.TypeRW;
import org.apache.commons.lang3.StringUtils;
public abstract class ConfigPropertyMeta {
public enum Result {
CANCELLED,
ONLINE,
OFFLINE
}
public final String name;
public final String category;
public final String comment;
public final Property.Type type;
protected final Field field;
private final boolean onLine;
private final Object defaultValue;
private final String[] defaultText;
// shadowed, since properties can change independently
protected String[] propertyText;
protected final IStringSerializer<?> converter;
protected final Property wrappedProperty;
public static final Map<Class<?>, Property.Type> CONFIG_TYPES = ImmutableMap.<Class<?>, Property.Type> builder()
.put(Integer.class, Property.Type.INTEGER)
.put(int.class, Property.Type.INTEGER)
.put(Boolean.class, Property.Type.BOOLEAN)
.put(boolean.class, Property.Type.BOOLEAN)
.put(Byte.class, Property.Type.INTEGER)
.put(byte.class, Property.Type.INTEGER)
.put(Double.class, Property.Type.DOUBLE)
.put(double.class, Property.Type.DOUBLE)
.put(Float.class, Property.Type.DOUBLE)
.put(float.class, Property.Type.DOUBLE)
.put(Long.class, Property.Type.INTEGER)
.put(long.class, Property.Type.INTEGER)
.put(Short.class, Property.Type.INTEGER)
.put(short.class, Property.Type.INTEGER)
.put(String.class, Property.Type.STRING)
.build();
protected ConfigPropertyMeta(Configuration config, Field field, ConfigProperty annotation) {
this.comment = annotation.comment();
this.category = annotation.category();
OnLineModifiable mod = field.getAnnotation(OnLineModifiable.class);
this.onLine = mod != null;
String name = annotation.name();
String category = annotation.category();
if (Strings.isNullOrEmpty(name)) name = field.getName();
if (Strings.isNullOrEmpty(category)) category = null;
this.name = name;
this.field = field;
defaultValue = getFieldValue();
Preconditions.checkNotNull(defaultValue, "Config field %s has no default value", name);
defaultText = convertToStringArray(defaultValue);
final Class<?> fieldType = getFieldType();
type = ConfigPropertyMeta.CONFIG_TYPES.get(fieldType);
Preconditions.checkNotNull(type, "Config field %s has no property type mapping", name);
converter = TypeRW.STRING_SERIALIZERS.get(fieldType);
Preconditions.checkNotNull(converter, "Config field %s has no known conversion from string", name);
wrappedProperty = getProperty(config, type, defaultValue);
}
void updateValueFromConfig(boolean force) {
// return on newly created value. Due to forge bug list properties
// don't set this value properly
if (!force && !wrappedProperty.wasRead() && !wrappedProperty.isList()) return;
final Type actualType = wrappedProperty.getType();
Preconditions.checkState(type == actualType, "Invalid config property type '%s', expected '%s'", actualType, type);
String[] currentValue = getActualPropertyValue();
try {
Object converted = convertValue(currentValue);
setFieldValue(converted);
} catch (StringConversionException e) {
Log.warn(e, "Invalid config property value %s, using default value", Arrays.toString(currentValue));
}
}
protected void setFieldValue(Object value) {
try {
field.set(null, value);
} catch (Throwable e) {
throw Throwables.propagate(e);
}
}
protected Object getFieldValue() {
try {
return field.get(null);
} catch (Throwable t) {
throw Throwables.propagate(t);
}
}
protected abstract Class<? extends Object> getFieldType();
protected abstract Property getProperty(Configuration configFile, Type expectedType, Object defaultValue);
public String[] getPropertyValue() {
return propertyText;
}
public abstract String[] getActualPropertyValue();
protected abstract void setPropertyValue(String... values);
protected abstract Object convertValue(String... values);
public abstract boolean acceptsMultipleValues();
public abstract String valueDescription();
protected abstract String[] convertToStringArray(Object value);
public Result tryChangeValue(String... proposedValues) {
ConfigurationChange.Pre evt = new ConfigurationChange.Pre(name, category, proposedValues);
if (MinecraftForge.EVENT_BUS.post(evt)) return Result.CANCELLED;
Object converted = convertValue(evt.proposedValues);
if (onLine) setFieldValue(converted);
MinecraftForge.EVENT_BUS.post(new ConfigurationChange.Post(name, category));
setPropertyValue(evt.proposedValues);
this.propertyText = evt.proposedValues;
return onLine? Result.ONLINE : Result.OFFLINE;
}
public String[] getDefaultValues() {
return defaultText.clone();
}
public Property getProperty() {
return wrappedProperty;
}
private static class SingleValue extends ConfigPropertyMeta {
protected SingleValue(Configuration config, Field field, ConfigProperty annotation) {
super(config, field, annotation);
this.propertyText = new String[] { wrappedProperty.getString() };
}
@Override
protected Class<? extends Object> getFieldType() {
return field.getType();
}
@Override
protected Property getProperty(Configuration configFile, Type expectedType, Object defaultValue) {
final String defaultString = defaultValue.toString();
return configFile.get(category, name, defaultString, comment, expectedType);
}
@Override
protected Object convertValue(String... values) {
Preconditions.checkArgument(values.length == 1, "This parameter has only one value");
final String value = values[0];
return converter.readFromString(value);
}
@Override
public String[] getActualPropertyValue() {
return new String[] { wrappedProperty.getString() };
}
@Override
protected void setPropertyValue(String... values) {
Preconditions.checkArgument(values.length == 1, "This parameter has only one value");
wrappedProperty.set(values[0]);
}
@Override
public boolean acceptsMultipleValues() {
return false;
}
@Override
public String valueDescription() {
return propertyText[0];
}
@Override
protected String[] convertToStringArray(Object value) {
return new String[] { value.toString() };
}
}
private static class MultipleValues extends ConfigPropertyMeta {
protected MultipleValues(Configuration config, Field field, ConfigProperty annotation) {
super(config, field, annotation);
this.propertyText = wrappedProperty.getStringList();
}
@Override
protected Class<? extends Object> getFieldType() {
return field.getType().getComponentType();
}
@Override
protected Property getProperty(Configuration configFile, Type expectedType, Object defaultValue) {
final String[] defaultStrings = convertToStringArray(defaultValue);
return configFile.get(category, name, defaultStrings, comment, expectedType);
}
@Override
protected Object convertValue(String... values) {
final Object result = Array.newInstance(field.getType().getComponentType(), values.length);
final CharMatcher matcher = CharMatcher.is('"');
for (int i = 0; i < values.length; i++) {
final String value = matcher.trimFrom(StringUtils.strip(values[i]));
final Object converted = converter.readFromString(value);
Array.set(result, i, converted);
}
return result;
}
@Override
public String[] getActualPropertyValue() {
return wrappedProperty.getStringList();
}
@Override
protected void setPropertyValue(String... values) {
wrappedProperty.set(values);
}
@Override
public boolean acceptsMultipleValues() {
return true;
}
@Override
public String valueDescription() {
return Arrays.toString(propertyText);
}
@Override
protected String[] convertToStringArray(Object value) {
Preconditions.checkArgument(value.getClass().isArray(), "Type %s is not an array", value.getClass());
int length = Array.getLength(value);
String[] result = new String[length];
for (int i = 0; i < length; i++)
result[i] = String.format("\"%s\"", Array.get(value, i).toString());
return result;
}
}
public static ConfigPropertyMeta createMetaForField(Configuration config, Field field) {
ConfigProperty annotation = field.getAnnotation(ConfigProperty.class);
if (annotation == null) return null;
Class<?> fieldType = field.getType();
return fieldType.isArray()? new MultipleValues(config, field, annotation) : new SingleValue(config, field, annotation);
}
}