ForgePatch/src/main/java/net/minecraftforge/common/ForgeConfigSpec.java

645 lines
28 KiB
Java

/*
* Minecraft Forge
* Copyright (c) 2016-2018.
*
* 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;
import static com.electronwill.nightconfig.core.ConfigSpec.CorrectionAction.ADD;
import static com.electronwill.nightconfig.core.ConfigSpec.CorrectionAction.REMOVE;
import static com.electronwill.nightconfig.core.ConfigSpec.CorrectionAction.REPLACE;
import static net.minecraftforge.fml.Logging.CORE;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager;
import com.electronwill.nightconfig.core.CommentedConfig;
import com.electronwill.nightconfig.core.Config;
import com.electronwill.nightconfig.core.InMemoryFormat;
import com.electronwill.nightconfig.core.utils.UnmodifiableConfigWrapper;
import com.electronwill.nightconfig.core.ConfigSpec.CorrectionAction;
import com.electronwill.nightconfig.core.ConfigSpec.CorrectionListener;
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import com.electronwill.nightconfig.core.file.FileConfig;
import com.electronwill.nightconfig.core.io.WritingMode;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import io.netty.util.BooleanSupplier;
/*
* Like {@link com.electronwill.nightconfig.core.ConfigSpec} except in builder format, and extended to acept comments, language keys,
* and other things Forge configs would find useful.
*/
public class ForgeConfigSpec extends UnmodifiableConfigWrapper<Config>
{
private Map<List<String>, String> levelComments = new HashMap<>();
private Config childConfig;
private ForgeConfigSpec(Config storage, Map<List<String>, String> levelComments) {
super(storage);
this.levelComments = levelComments;
}
public void setConfig(CommentedConfig config) {
this.childConfig = config;
if (!isCorrect(config)) {
String configName = config instanceof FileConfig ? ((FileConfig) config).getNioPath().toString() : config.toString();
LogManager.getLogger().warn(CORE, "Configuration file {} is not correct. Correcting", configName);
correct(config, (action, path, incorrectValue, correctedValue) ->
LogManager.getLogger().warn(CORE, "Incorrect key {} was corrected from {} to {}", DOT_JOINER.join( path ), incorrectValue, correctedValue));
if (config instanceof FileConfig) {
((FileConfig) config).save();
}
}
}
public boolean isCorrect(CommentedConfig config) {
LinkedList<String> parentPath = new LinkedList<>();
return correct(this.config, config, parentPath, Collections.unmodifiableList( parentPath ), null, true) == 0;
}
public int correct(CommentedConfig config) {
return correct(config, (action, path, incorrectValue, correctedValue) -> {});
}
public int correct(CommentedConfig config, CorrectionListener listener) {
LinkedList<String> parentPath = new LinkedList<>(); //Linked list for fast add/removes
return correct(this.config, config, parentPath, Collections.unmodifiableList(parentPath), listener, false);
}
private int correct(Config spec, CommentedConfig config, LinkedList<String> parentPath, List<String> parentPathUnmodifiable, CorrectionListener listener, boolean dryRun)
{
int count = 0;
Map<String, Object> specMap = spec.valueMap();
Map<String, Object> configMap = config.valueMap();
for (Map.Entry<String, Object> specEntry : specMap.entrySet())
{
final String key = specEntry.getKey();
final Object specValue = specEntry.getValue();
final Object configValue = configMap.get(key);
final CorrectionAction action = configValue == null ? ADD : REPLACE;
parentPath.addLast(key);
if (specValue instanceof Config)
{
if (configValue instanceof CommentedConfig)
{
count += correct((Config)specValue, (CommentedConfig)configValue, parentPath, parentPathUnmodifiable, listener, dryRun);
if (count > 0 && dryRun)
return count;
}
else if (dryRun)
{
return 1;
}
else
{
CommentedConfig newValue = config.createSubConfig();
configMap.put(key, newValue);
listener.onCorrect(action, parentPathUnmodifiable, configValue, newValue);
count++;
count += correct((Config)specValue, newValue, parentPath, parentPathUnmodifiable, listener, dryRun);
}
String newComment = levelComments.get(parentPath);
String oldComment = config.getComment(key);
if (!Objects.equals(oldComment, newComment))
{
if (dryRun)
return 1;
//TODO: Comment correction listener?
config.setComment(key, newComment);
}
}
else
{
ValueSpec valueSpec = (ValueSpec)specValue;
if (!valueSpec.test(configValue))
{
if (dryRun)
return 1;
Object newValue = valueSpec.correct(configValue);
configMap.put(key, newValue);
listener.onCorrect(action, parentPathUnmodifiable, configValue, newValue);
count++;
}
String oldComment = config.getComment(key);
if (!Objects.equals(oldComment, valueSpec.getComment()))
{
if (dryRun)
return 1;
//TODO: Comment correction listener?
config.setComment(key, valueSpec.getComment());
}
}
parentPath.removeLast();
}
// Second step: removes the unspecified values
for (Iterator<Map.Entry<String, Object>> ittr = configMap.entrySet().iterator(); ittr.hasNext();)
{
Map.Entry<String, Object> entry = ittr.next();
if (!specMap.containsKey(entry.getKey()))
{
if (dryRun)
return 1;
ittr.remove();
parentPath.addLast(entry.getKey());
listener.onCorrect(REMOVE, parentPathUnmodifiable, entry.getValue(), null);
parentPath.removeLast();
count++;
}
}
return count;
}
public static class Builder
{
private final Config storage = InMemoryFormat.withUniversalSupport().createConfig();
private BuilderContext context = new BuilderContext();
private Map<List<String>, String> levelComments = new HashMap<>();
private List<String> currentPath = new ArrayList<>();
private List<ConfigValue<?>> values = new ArrayList<>();
//Object
public <T> ConfigValue<T> define(String path, T defaultValue) {
return define(split(path), defaultValue);
}
public <T> ConfigValue<T> define(List<String> path, T defaultValue) {
return define(path, defaultValue, o -> o != null && defaultValue.getClass().isAssignableFrom(o.getClass()));
}
public <T> ConfigValue<T> define(String path, T defaultValue, Predicate<Object> validator) {
return define(split(path), defaultValue, validator);
}
public <T> ConfigValue<T> define(List<String> path, T defaultValue, Predicate<Object> validator) {
Objects.requireNonNull(defaultValue, "Default value can not be null");
return define(path, () -> defaultValue, validator);
}
public <T> ConfigValue<T> define(String path, Supplier<T> defaultSupplier, Predicate<Object> validator) {
return define(split(path), defaultSupplier, validator);
}
public <T> ConfigValue<T> define(List<String> path, Supplier<T> defaultSupplier, Predicate<Object> validator) {
return define(path, defaultSupplier, validator, Object.class);
}
public <T> ConfigValue<T> define(List<String> path, Supplier<T> defaultSupplier, Predicate<Object> validator, Class<?> clazz) {
context.setClazz(clazz);
return define(path, new ValueSpec(defaultSupplier, validator, context), defaultSupplier);
}
public <T> ConfigValue<T> define(List<String> path, ValueSpec value, Supplier<T> defaultSupplier) { // This is the root where everything at the end of the day ends up.
if (!currentPath.isEmpty()) {
List<String> tmp = new ArrayList<>(currentPath.size() + path.size());
tmp.addAll(currentPath);
tmp.addAll(path);
path = tmp;
}
storage.set(path, value);
context = new BuilderContext();
return new ConfigValue<>(this, path, defaultSupplier);
}
public <V extends Comparable<? super V>> ConfigValue<V> defineInRange(String path, V defaultValue, V min, V max, Class<V> clazz) {
return defineInRange(split(path), defaultValue, min, max, clazz);
}
public <V extends Comparable<? super V>> ConfigValue<V> defineInRange(List<String> path, V defaultValue, V min, V max, Class<V> clazz) {
return defineInRange(path, (Supplier<V>)() -> defaultValue, min, max, clazz);
}
public <V extends Comparable<? super V>> ConfigValue<V> defineInRange(String path, Supplier<V> defaultSupplier, V min, V max, Class<V> clazz) {
return defineInRange(split(path), defaultSupplier, min, max, clazz);
}
public <V extends Comparable<? super V>> ConfigValue<V> defineInRange(List<String> path, Supplier<V> defaultSupplier, V min, V max, Class<V> clazz) {
Range<V> range = new Range<>(clazz, min, max);
context.setRange(range);
if (min.compareTo(max) > 0)
throw new IllegalArgumentException("Range min most be less then max.");
return define(path, defaultSupplier, range);
}
public <T> ConfigValue<T> defineInList(String path, T defaultValue, Collection<? extends T> acceptableValues) {
return defineInList(split(path), defaultValue, acceptableValues);
}
public <T> ConfigValue<T> defineInList(String path, Supplier<T> defaultSupplier, Collection<? extends T> acceptableValues) {
return defineInList(split(path), defaultSupplier, acceptableValues);
}
public <T> ConfigValue<T> defineInList(List<String> path, T defaultValue, Collection<? extends T> acceptableValues) {
return defineInList(path, () -> defaultValue, acceptableValues);
}
public <T> ConfigValue<T> defineInList(List<String> path, Supplier<T> defaultSupplier, Collection<? extends T> acceptableValues) {
return define(path, defaultSupplier, acceptableValues::contains);
}
public <T> ConfigValue<List<? extends T>> defineList(String path, List<? extends T> defaultValue, Predicate<Object> elementValidator) {
return defineList(split(path), defaultValue, elementValidator);
}
public <T> ConfigValue<List<? extends T>> defineList(String path, Supplier<List<? extends T>> defaultSupplier, Predicate<Object> elementValidator) {
return defineList(split(path), defaultSupplier, elementValidator);
}
public <T> ConfigValue<List<? extends T>> defineList(List<String> path, List<? extends T> defaultValue, Predicate<Object> elementValidator) {
return defineList(path, () -> defaultValue, elementValidator);
}
public <T> ConfigValue<List<? extends T>> defineList(List<String> path, Supplier<List<? extends T>> defaultSupplier, Predicate<Object> elementValidator) {
context.setClazz(List.class);
return define(path, new ValueSpec(defaultSupplier, x -> x instanceof List && ((List<?>) x).stream().allMatch( elementValidator ), context) {
@Override
public Object correct(Object value) {
if (value == null || !(value instanceof List) || ((List<?>)value).isEmpty()) {
return getDefault();
}
List<?> list = Lists.newArrayList((List<?>) value);
list.removeIf(elementValidator.negate());
if (list.isEmpty()) {
return getDefault();
}
return list;
}
}, defaultSupplier);
}
//Enum
public <V extends Enum<V>> ConfigValue<V> defineEnum(String path, V defaultValue) {
return defineEnum(split(path), defaultValue);
}
public <V extends Enum<V>> ConfigValue<V> defineEnum(List<String> path, V defaultValue) {
return defineEnum(path, defaultValue, defaultValue.getDeclaringClass().getEnumConstants());
}
public <V extends Enum<V>> ConfigValue<V> defineEnum(String path, V defaultValue, @SuppressWarnings("unchecked") V... acceptableValues) {
return defineEnum(split(path), defaultValue, acceptableValues);
}
public <V extends Enum<V>> ConfigValue<V> defineEnum(List<String> path, V defaultValue, @SuppressWarnings("unchecked") V... acceptableValues) {
return defineEnum(path, defaultValue, Arrays.asList(acceptableValues));
}
public <V extends Enum<V>> ConfigValue<V> defineEnum(String path, V defaultValue, Collection<V> acceptableValues) {
return defineEnum(split(path), defaultValue, acceptableValues);
}
public <V extends Enum<V>> ConfigValue<V> defineEnum(List<String> path, V defaultValue, Collection<V> acceptableValues) {
return defineEnum(path, defaultValue, acceptableValues::contains);
}
public <V extends Enum<V>> ConfigValue<V> defineEnum(String path, V defaultValue, Predicate<Object> validator) {
return defineEnum(split(path), defaultValue, validator);
}
public <V extends Enum<V>> ConfigValue<V> defineEnum(List<String> path, V defaultValue, Predicate<Object> validator) {
return defineEnum(path, () -> defaultValue, validator, defaultValue.getDeclaringClass());
}
public <V extends Enum<V>> ConfigValue<V> defineEnum(String path, Supplier<V> defaultSupplier, Predicate<Object> validator, Class<V> clazz) {
return defineEnum(split(path), defaultSupplier, validator, clazz);
}
public <V extends Enum<V>> ConfigValue<V> defineEnum(List<String> path, Supplier<V> defaultSupplier, Predicate<Object> validator, Class<V> clazz) {
return define(path, defaultSupplier, validator, clazz);
}
//boolean
public BooleanValue define(String path, boolean defaultValue) {
return define(split(path), defaultValue);
}
public BooleanValue define(List<String> path, boolean defaultValue) {
return define(path, (Supplier<Boolean>)() -> defaultValue);
}
public BooleanValue define(String path, Supplier<Boolean> defaultSupplier) {
return define(split(path), defaultSupplier);
}
public BooleanValue define(List<String> path, Supplier<Boolean> defaultSupplier) {
return new BooleanValue(this, define(path, defaultSupplier, o -> {
if (o instanceof String) return ((String)o).equalsIgnoreCase("true") || ((String)o).equalsIgnoreCase("false");
return o instanceof Boolean;
}, Boolean.class).getPath(), defaultSupplier);
}
//Double
public DoubleValue defineInRange(String path, double defaultValue, double min, double max) {
return defineInRange(split(path), defaultValue, min, max);
}
public DoubleValue defineInRange(List<String> path, double defaultValue, double min, double max) {
return defineInRange(path, (Supplier<Double>)() -> defaultValue, min, max);
}
public DoubleValue defineInRange(String path, Supplier<Double> defaultSupplier, double min, double max) {
return defineInRange(split(path), defaultSupplier, min, max);
}
public DoubleValue defineInRange(List<String> path, Supplier<Double> defaultSupplier, double min, double max) {
return new DoubleValue(this, defineInRange(path, defaultSupplier, min, max, Double.class).getPath(), defaultSupplier);
}
//Ints
public IntValue defineInRange(String path, int defaultValue, int min, int max) {
return defineInRange(split(path), defaultValue, min, max);
}
public IntValue defineInRange(List<String> path, int defaultValue, int min, int max) {
return defineInRange(path, (Supplier<Integer>)() -> defaultValue, min, max);
}
public IntValue defineInRange(String path, Supplier<Integer> defaultSupplier, int min, int max) {
return defineInRange(split(path), defaultSupplier, min, max);
}
public IntValue defineInRange(List<String> path, Supplier<Integer> defaultSupplier, int min, int max) {
return new IntValue(this, defineInRange(path, defaultSupplier, min, max, Integer.class).getPath(), defaultSupplier);
}
//Longs
public LongValue defineInRange(String path, long defaultValue, long min, long max) {
return defineInRange(split(path), defaultValue, min, max);
}
public LongValue defineInRange(List<String> path, long defaultValue, long min, long max) {
return defineInRange(path, (Supplier<Long>)() -> defaultValue, min, max);
}
public LongValue defineInRange(String path, Supplier<Long> defaultSupplier, long min, long max) {
return defineInRange(split(path), defaultSupplier, min, max);
}
public LongValue defineInRange(List<String> path, Supplier<Long> defaultSupplier, long min, long max) {
return new LongValue(this, defineInRange(path, defaultSupplier, min, max, Long.class).getPath(), defaultSupplier);
}
public Builder comment(String comment)
{
context.setComment(comment);
return this;
}
public Builder comment(String... comment)
{
context.setComment(comment);
return this;
}
public Builder translation(String translationKey)
{
context.setTranslationKey(translationKey);
return this;
}
public Builder worldRestart()
{
context.worldRestart();
return this;
}
public Builder push(String path) {
return push(split(path));
}
public Builder push(List<String> path) {
currentPath.addAll(path);
if (context.getComment() != null) {
levelComments.put(new ArrayList<String>(currentPath), LINE_JOINER.join(context.getComment()));
context.setComment((String[])null);
}
context.ensureEmpty();
return this;
}
public Builder pop() {
return pop(1);
}
public Builder pop(int count) {
if (count > currentPath.size())
throw new IllegalArgumentException("Attempted to pop " + count + " elements when we only had: " + currentPath);
for (int x = 0; x < count; x++)
currentPath.remove(currentPath.size() - 1);
return this;
}
public <T> Pair<T, ForgeConfigSpec> configure(Function<Builder, T> consumer) {
T o = consumer.apply(this);
return Pair.of(o, this.build());
}
public ForgeConfigSpec build()
{
context.ensureEmpty();
ForgeConfigSpec ret = new ForgeConfigSpec(storage, levelComments);
values.forEach(v -> v.spec = ret);
return ret;
}
public interface BuilderConsumer {
void accept(Builder builder);
}
}
private static class BuilderContext
{
private String[] comment;
private String langKey;
private Range<?> range;
private boolean worldRestart = false;
private Class<?> clazz;
public void setComment(String... value) { this.comment = value; }
public String[] getComment() { return this.comment; }
public void setTranslationKey(String value) { this.langKey = value; }
public String getTranslationKey() { return this.langKey; }
public <V extends Comparable<? super V>> void setRange(Range<V> value)
{
this.range = value;
this.setClazz(value.getClazz());
}
@SuppressWarnings("unchecked")
public <V extends Comparable<? super V>> Range<V> getRange() { return (Range<V>)this.range; }
public void worldRestart() { this.worldRestart = true; }
public boolean needsWorldRestart() { return this.worldRestart; }
public void setClazz(Class<?> clazz) { this.clazz = clazz; }
public Class<?> getClazz(){ return this.clazz; }
public void ensureEmpty()
{
validate(comment, "Non-null comment when null expected");
validate(langKey, "Non-null translation key when null expected");
validate(range, "Non-null range when null expected");
validate(worldRestart, "Dangeling world restart value set to true");
}
private void validate(Object value, String message)
{
if (value != null)
throw new IllegalStateException(message);
}
private void validate(boolean value, String message)
{
if (value)
throw new IllegalStateException(message);
}
}
@SuppressWarnings("unused")
private static class Range<V extends Comparable<? super V>> implements Predicate<Object>
{
private final Class<V> clazz;
private final V min;
private final V max;
private Range(Class<V> clazz, V min, V max)
{
this.clazz = clazz;
this.min = min;
this.max = max;
}
public Class<V> getClazz() { return clazz; }
public V getMin() { return min; }
public V getMax() { return max; }
@Override
public boolean test(Object t)
{
if (!clazz.isInstance(t)) return false;
V c = clazz.cast(t);
return c.compareTo(min) >= 0 && c.compareTo(max) <= 0;
}
}
public static class ValueSpec
{
private final String comment;
private final String langKey;
private final Range<?> range;
private final boolean worldRestart;
private final Class<?> clazz;
private final Supplier<?> supplier;
private final Predicate<Object> validator;
private Object _default = null;
private ValueSpec(Supplier<?> supplier, Predicate<Object> validator, BuilderContext context)
{
Objects.requireNonNull(supplier, "Default supplier can not be null");
Objects.requireNonNull(validator, "Validator can not be null");
this.comment = context.getComment() == null ? null : LINE_JOINER.join(context.getComment());
this.langKey = context.getTranslationKey();
this.range = context.getRange();
this.worldRestart = context.needsWorldRestart();
this.clazz = context.getClazz();
this.supplier = supplier;
this.validator = validator;
}
public String getComment() { return comment; }
public String getTranslationKey() { return langKey; }
@SuppressWarnings("unchecked")
public <V extends Comparable<? super V>> Range<V> getRange() { return (Range<V>)this.range; }
public boolean needsWorldRestart() { return this.worldRestart; }
public Class<?> getClazz(){ return this.clazz; }
public boolean test(Object value) { return validator.test(value); }
public Object correct(Object value) { return getDefault(); }
public Object getDefault()
{
if (_default == null)
_default = supplier.get();
return _default;
}
}
public static class ConfigValue<T>
{
private final Builder parent;
private final List<String> path;
private final Supplier<T> defaultSupplier;
private ForgeConfigSpec spec;
ConfigValue(Builder parent, List<String> path, Supplier<T> defaultSupplier)
{
this.parent = parent;
this.path = path;
this.defaultSupplier = defaultSupplier;
this.parent.values.add(this);
}
public List<String> getPath()
{
return Lists.newArrayList(path);
}
public T get()
{
Preconditions.checkNotNull(spec, "Cannot get config value before spec is built");
Preconditions.checkNotNull(spec.childConfig, "Cannot get config value without assigned Config object present");
return spec.childConfig.getOrElse(path, defaultSupplier);
}
public Builder next()
{
return parent;
}
}
public static class BooleanValue extends ConfigValue<Boolean>
{
BooleanValue(Builder parent, List<String> path, Supplier<Boolean> defaultSupplier)
{
super(parent, path, defaultSupplier);
}
}
public static class IntValue extends ConfigValue<Integer>
{
IntValue(Builder parent, List<String> path, Supplier<Integer> defaultSupplier)
{
super(parent, path, defaultSupplier);
}
}
public static class LongValue extends ConfigValue<Long>
{
LongValue(Builder parent, List<String> path, Supplier<Long> defaultSupplier)
{
super(parent, path, defaultSupplier);
}
}
public static class DoubleValue extends ConfigValue<Double>
{
DoubleValue(Builder parent, List<String> path, Supplier<Double> defaultSupplier)
{
super(parent, path, defaultSupplier);
}
}
private static final Joiner LINE_JOINER = Joiner.on("\n");
private static final Joiner DOT_JOINER = Joiner.on(".");
private static final Splitter DOT_SPLITTER = Splitter.on(".");
private static List<String> split(String path)
{
return Lists.newArrayList(DOT_SPLITTER.split(path));
}
}