aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md2
-rw-r--r--build.gradle8
-rw-r--r--src/main/java/com/anthonyhilyard/iceberg/config/IcebergConfig.java89
-rw-r--r--src/main/java/com/anthonyhilyard/iceberg/config/IcebergConfigSpec.java598
-rw-r--r--src/main/java/com/anthonyhilyard/iceberg/mixin/ConfigMenusForgeConfigScreenMixin.java217
-rw-r--r--src/main/java/com/anthonyhilyard/iceberg/mixin/ConfigMenusForgeIEntryDataMixin.java56
-rw-r--r--src/main/java/com/anthonyhilyard/iceberg/mixin/ConfigMenusForgeServerConfigUploaderMixin.java40
-rw-r--r--src/main/java/com/anthonyhilyard/iceberg/mixin/ForgeConfigMenusPlugin.java59
-rw-r--r--src/main/java/com/anthonyhilyard/iceberg/mixin/ForgeConfigSpecMixin.java160
-rw-r--r--src/main/java/com/anthonyhilyard/iceberg/mixin/TextColorMixin.java8
-rw-r--r--src/main/java/com/anthonyhilyard/iceberg/util/ConfigMenusForgeHelper.java266
-rw-r--r--src/main/java/com/anthonyhilyard/iceberg/util/DynamicSubconfig.java138
-rw-r--r--src/main/resources/META-INF/accesstransformer.cfg3
-rw-r--r--src/main/resources/iceberg.mixins.json9
14 files changed, 1346 insertions, 307 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4ab1705..937c5e8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,7 @@
# Changelog
### 1.0.39
-- Fixed an issue in Forge's configuration system that prevented dynamic subconfigs from working properly.
+- Added new config system that improves upon Forge's with subconfig support, improved reload reliability, and reduced boilerplate.
### 1.0.38
- Added support for tooltip components that use tooltip component generation event.
diff --git a/build.gradle b/build.gradle
index 3d2b6d9..ec698e8 100644
--- a/build.gradle
+++ b/build.gradle
@@ -49,9 +49,17 @@ sourceSets {
}
}
+repositories {
+ maven {
+ name "CurseMaven"
+ url "https://www.cursemaven.com"
+ }
+}
+
dependencies {
minecraft "net.minecraftforge:forge:${project.mcVersion}-${project.forgeVersion}"
annotationProcessor 'org.spongepowered:mixin:0.8.5:processor'
+ compileClasspath fg.deobf('curse.maven:configmenusforge-544048:3570070')
}
jar {
diff --git a/src/main/java/com/anthonyhilyard/iceberg/config/IcebergConfig.java b/src/main/java/com/anthonyhilyard/iceberg/config/IcebergConfig.java
new file mode 100644
index 0000000..e27e08d
--- /dev/null
+++ b/src/main/java/com/anthonyhilyard/iceberg/config/IcebergConfig.java
@@ -0,0 +1,89 @@
+package com.anthonyhilyard.iceberg.config;
+
+import javax.annotation.Nonnull;
+
+import com.anthonyhilyard.iceberg.Loader;
+import com.electronwill.nightconfig.core.Config;
+
+import org.apache.commons.lang3.tuple.Pair;
+
+import net.minecraftforge.eventbus.api.SubscribeEvent;
+import net.minecraftforge.fml.ModLoadingContext;
+import net.minecraftforge.fml.common.Mod.EventBusSubscriber;
+import net.minecraftforge.fml.common.Mod.EventBusSubscriber.Bus;
+import net.minecraftforge.fml.config.ModConfig;
+import net.minecraftforge.fml.event.config.ModConfigEvent;
+
+@EventBusSubscriber(modid = Loader.MODID, bus = Bus.MOD)
+public abstract class IcebergConfig<T extends IcebergConfig<?>>
+{
+ private static IcebergConfigSpec SPEC = null;
+ private static IcebergConfig<?> INSTANCE = null;
+ private static String modId = null;
+ private static boolean registered = false;
+
+ protected abstract <I extends IcebergConfig<?>> void setInstance(I instance);
+ protected void onLoad() {}
+ protected void onReload() {}
+
+ static
+ {
+ Config.setInsertionOrderPreserved(true);
+ }
+
+ @SubscribeEvent
+ private static void onLoadEvent(ModConfigEvent.Loading event)
+ {
+ if (modId != null && INSTANCE != null && event.getConfig().getModId().contentEquals(modId))
+ {
+ INSTANCE.onLoad();
+ }
+ }
+
+ @SubscribeEvent
+ private static void onReloadEvent(ModConfigEvent.Reloading event)
+ {
+ if (modId != null && INSTANCE != null && event.getConfig().getModId().contentEquals(modId))
+ {
+ INSTANCE.onReload();
+ }
+ }
+
+ public static final boolean register(Class<? extends IcebergConfig<?>> superClass, @Nonnull String modId)
+ {
+ if (registered)
+ {
+ return false;
+ }
+
+ IcebergConfig.modId = modId;
+
+ Pair<IcebergConfig<?>, IcebergConfigSpec> specPair = new IcebergConfigSpec.Builder().finish((builder) ->
+ {
+ IcebergConfig<?> result = null;
+ try
+ {
+ result = (IcebergConfig<?>)superClass.getConstructor(IcebergConfigSpec.Builder.class).newInstance(builder);
+ }
+ catch (Exception e)
+ {
+ Loader.LOGGER.warn("Failed to register configuration: {}", e);
+ }
+ return result;
+ });
+
+ if (specPair.getRight() == null || specPair.getLeft() == null)
+ {
+ return false;
+ }
+
+ SPEC = specPair.getRight();
+ INSTANCE = specPair.getLeft();
+ INSTANCE.setInstance(specPair.getLeft());
+
+ ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, SPEC);
+
+ registered = true;
+ return true;
+ }
+}
diff --git a/src/main/java/com/anthonyhilyard/iceberg/config/IcebergConfigSpec.java b/src/main/java/com/anthonyhilyard/iceberg/config/IcebergConfigSpec.java
new file mode 100644
index 0000000..01a5e40
--- /dev/null
+++ b/src/main/java/com/anthonyhilyard/iceberg/config/IcebergConfigSpec.java
@@ -0,0 +1,598 @@
+package com.anthonyhilyard.iceberg.config;
+
+import java.io.File;
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+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 javax.annotation.Nullable;
+
+import net.minecraftforge.common.ForgeConfigSpec;
+import net.minecraftforge.common.ForgeConfigSpec.ConfigValue;
+import net.minecraftforge.common.ForgeConfigSpec.ValueSpec;
+import net.minecraftforge.fml.Logging;
+import net.minecraftforge.fml.config.IConfigSpec;
+import net.minecraftforge.fml.unsafe.UnsafeHacks;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.logging.log4j.LogManager;
+
+import com.anthonyhilyard.iceberg.Loader;
+import com.electronwill.nightconfig.core.AbstractCommentedConfig;
+import com.electronwill.nightconfig.core.CommentedConfig;
+import com.electronwill.nightconfig.core.Config;
+import com.electronwill.nightconfig.core.ConfigFormat;
+import com.electronwill.nightconfig.core.ConfigSpec.CorrectionAction;
+import com.electronwill.nightconfig.core.ConfigSpec.CorrectionListener;
+import com.electronwill.nightconfig.core.InMemoryFormat;
+import com.electronwill.nightconfig.core.UnmodifiableConfig;
+import com.electronwill.nightconfig.core.file.FileConfig;
+import com.electronwill.nightconfig.core.file.FileWatcher;
+import com.electronwill.nightconfig.core.utils.UnmodifiableConfigWrapper;
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Lists;
+
+/*
+ * Basically an improved ForgeConfigSpec that supports subconfigs.
+ */
+public class IcebergConfigSpec extends UnmodifiableConfigWrapper<UnmodifiableConfig> implements IConfigSpec<IcebergConfigSpec>
+{
+ private Map<List<String>, String> levelComments = new HashMap<>();
+
+ private UnmodifiableConfig values;
+ private Config childConfig;
+
+ private boolean isCorrecting = false;
+
+ private IcebergConfigSpec(UnmodifiableConfig storage, UnmodifiableConfig values, Map<List<String>, String> levelComments)
+ {
+ super(storage);
+ this.values = values;
+ this.levelComments = levelComments;
+
+ // Update the filewatcher's default instance to have a more sensible exception handler.
+ try
+ {
+ Field exceptionHandlerField = FileWatcher.class.getDeclaredField("exceptionHandler");
+ UnsafeHacks.setField(exceptionHandlerField, FileWatcher.defaultInstance(), (Consumer<Exception>)e -> {
+ LogManager.getLogger().warn(Logging.CORE, "An error occurred while reloading config:", e);
+ });
+ }
+ catch (Exception e) {}
+ }
+
+ public void setConfig(CommentedConfig config)
+ {
+ this.childConfig = config;
+ if (config != null && !isCorrect(config))
+ {
+ String configName = config instanceof FileConfig fileConfig ? fileConfig.getNioPath().toString() : config.toString();
+ Loader.LOGGER.warn("Configuration file {} is not correct. Correcting", configName);
+ correct(config,
+ (action, path, incorrectValue, correctedValue) ->
+ Loader.LOGGER.warn("Incorrect key {} was corrected from {} to its default, {}. {}", DOT_JOINER.join( path ), incorrectValue, correctedValue, incorrectValue == correctedValue ? "This seems to be an error." : ""),
+ (action, path, incorrectValue, correctedValue) ->
+ Loader.LOGGER.debug("The comment on key {} does not match the spec. This may create a backup.", DOT_JOINER.join( path )));
+
+ if (config instanceof FileConfig fileConfig)
+ {
+ fileConfig.save();
+ }
+ }
+ this.afterReload();
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public <T> T getRaw(List<String> path)
+ {
+ T value = super.getRaw(path);
+ if (value != null)
+ {
+ return value;
+ }
+ // Try to "recursively" get the value if needed.
+ List<String> subPath = path.subList(0, path.size() - 1);
+ Object test = super.getRaw(subPath);
+ if (test instanceof ValueSpec valueSpec && valueSpec.getDefault() instanceof MutableSubconfig subconfig)
+ {
+ // Okay, this is a dynamic subconfig. That means that only values defined in the default have
+ // an actual value spec.
+ value = subconfig.getRaw(path.get(path.size() - 1));
+
+ // Value will be null here for non-default entries. In that case, just return a default value spec.
+ if (value == null)
+ {
+ value = (T) subconfig.defaultValueSpec();
+ }
+ return value;
+ }
+ return null;
+ }
+
+ @Override
+ public void acceptConfig(final CommentedConfig data)
+ {
+ setConfig(data);
+ }
+
+ public boolean isCorrecting()
+ {
+ return isCorrecting;
+ }
+
+ public boolean isLoaded()
+ {
+ return childConfig != null;
+ }
+
+ public UnmodifiableConfig getSpec()
+ {
+ return this.config;
+ }
+
+ public UnmodifiableConfig getValues()
+ {
+ return this.values;
+ }
+
+ public void afterReload()
+ {
+ this.resetCaches(getValues().valueMap().values());
+ }
+
+ private void resetCaches(final Iterable<Object> configValues)
+ {
+ configValues.forEach(value ->
+ {
+ if (value instanceof ConfigValue<?> configValue)
+ {
+ configValue.clearCache();
+ }
+ else if (value instanceof Config innerConfig)
+ {
+ this.resetCaches(innerConfig.valueMap().values());
+ }
+ });
+ }
+
+ public void save()
+ {
+ Preconditions.checkNotNull(childConfig, "Cannot save config value without assigned Config object present!");
+ if (childConfig instanceof FileConfig fileConfig)
+ {
+ fileConfig.save();
+ }
+ }
+
+ public synchronized boolean isCorrect(CommentedConfig config)
+ {
+ LinkedList<String> parentPath = new LinkedList<>();
+
+ // Forge's config watcher isn't properly atomic, so sometimes this method can give false negatives leading
+ // to the entire config file reverting to defaults. To prevent this, we'll check for an invalid state and
+ // skip the correction process when that happens.
+ if (config.valueMap().isEmpty() && config instanceof FileConfig fileConfig)
+ {
+ File configFile = fileConfig.getFile();
+
+ // Sleep for 10 ms to give it a chance to catch up. This shouldn't cause any issues since
+ // this method only runs when the file changes and at startup.
+ try { Thread.sleep(10); } catch (Exception e) { }
+
+ // The file isn't actually empty, so this is an invalid state. Skip this correction phase.
+ if (configFile.length() > 0)
+ {
+ return true;
+ }
+ }
+
+ return correct(this.config, config, parentPath, Collections.unmodifiableList( parentPath ), (a, b, c, d) -> {}, null, true) == 0;
+ }
+
+ public synchronized int correct(CommentedConfig config)
+ {
+ return correct(config, (action, path, incorrectValue, correctedValue) -> {}, null);
+ }
+
+ public synchronized int correct(CommentedConfig config, CorrectionListener listener)
+ {
+ return correct(config, listener, null);
+ }
+
+ public synchronized int correct(CommentedConfig config, CorrectionListener listener, CorrectionListener commentListener)
+ {
+ LinkedList<String> parentPath = new LinkedList<>();
+ int ret = -1;
+ try
+ {
+ isCorrecting = true;
+ ret = correct(this.config, config, parentPath, Collections.unmodifiableList(parentPath), listener, commentListener, false);
+ }
+ finally
+ {
+ isCorrecting = false;
+ }
+ return ret;
+ }
+
+ private synchronized int correct(UnmodifiableConfig spec, CommentedConfig config, LinkedList<String> parentPath, List<String> parentPathUnmodifiable, CorrectionListener listener, CorrectionListener commentListener, 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();
+ Object specValue = specEntry.getValue();
+ final Object configValue = configMap.get(key);
+ final CorrectionAction action = configValue == null ? CorrectionAction.ADD : CorrectionAction.REPLACE;
+
+ parentPath.addLast(key);
+
+ String subConfigComment = null;
+
+ // If this value is a config, use that as the spec value to support subconfigs.
+ if (specValue instanceof ValueSpec valueSpec && valueSpec.getDefault() instanceof UnmodifiableConfig)
+ {
+ subConfigComment = valueSpec.getComment();
+ specValue = valueSpec.getDefault();
+ }
+
+ if (specValue instanceof UnmodifiableConfig)
+ {
+ if (configValue instanceof Config)
+ {
+ count += correct((UnmodifiableConfig)specValue, configValue instanceof CommentedConfig commentedConfig ? commentedConfig : CommentedConfig.copy((Config)configValue), parentPath, parentPathUnmodifiable, listener, commentListener, 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((UnmodifiableConfig)specValue, newValue, parentPath, parentPathUnmodifiable, listener, commentListener, dryRun);
+ }
+
+ String newComment = subConfigComment == null ? levelComments.get(parentPath) : subConfigComment;
+ String oldComment = config.getComment(key);
+ if (!stringsMatchIgnoringNewlines(oldComment, newComment))
+ {
+ if (commentListener != null)
+ {
+ commentListener.onCorrect(action, parentPathUnmodifiable, oldComment, newComment);
+ }
+
+ if (dryRun)
+ {
+ return 1;
+ }
+
+ 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 (!stringsMatchIgnoringNewlines(oldComment, valueSpec.getComment()))
+ {
+ if (commentListener != null)
+ {
+ commentListener.onCorrect(action, parentPathUnmodifiable, oldComment, valueSpec.getComment());
+ }
+
+ if (dryRun)
+ {
+ return 1;
+ }
+
+ config.setComment(key, valueSpec.getComment());
+ }
+ }
+
+ parentPath.removeLast();
+ }
+
+ // Now remove any config values that are not explicitly set in the spec.
+ for (Iterator<Map.Entry<String, Object>> iterator = configMap.entrySet().iterator(); iterator.hasNext();)
+ {
+ Map.Entry<String, Object> entry = iterator.next();
+
+ // If the spec is a dynamic subconfig, don't bother checking the spec since that's the point.
+ if (!(spec instanceof MutableSubconfig) && !specMap.containsKey(entry.getKey()))
+ {
+ if (dryRun)
+ {
+ return 1;
+ }
+
+ iterator.remove();
+ parentPath.addLast(entry.getKey());
+ listener.onCorrect(CorrectionAction.REMOVE, parentPathUnmodifiable, entry.getValue(), null);
+ parentPath.removeLast();
+ count++;
+ }
+ }
+ return count;
+ }
+
+ private boolean stringsMatchIgnoringNewlines(@Nullable Object obj1, @Nullable Object obj2)
+ {
+ if (obj1 instanceof String && obj2 instanceof String)
+ {
+ String string1 = (String) obj1;
+ String string2 = (String) obj2;
+
+ if (string1.length() > 0 && string2.length() > 0)
+ {
+ return string1.replaceAll("\r\n", "\n").equals(string2.replaceAll("\r\n", "\n"));
+ }
+ }
+
+ // Fallback for when we're not given Strings, or one of them is empty
+ return Objects.equals(obj1, obj2);
+ }
+
+ public static ValueSpec createValueSpec(String comment, String langKey, boolean worldRestart, Class<?> clazz, Supplier<?> defaultSupplier, Predicate<Object> validator)
+ {
+ Objects.requireNonNull(defaultSupplier, "Default supplier can not be null!");
+ Objects.requireNonNull(validator, "Validator can not be null!");
+
+ // Instantiate the new ValueSpec instance, then use reflection to set the required fields.
+ ValueSpec result = UnsafeHacks.newInstance(ValueSpec.class);
+ try
+ {
+ Field commentField = ValueSpec.class.getDeclaredField("comment");
+ Field langKeyField = ValueSpec.class.getDeclaredField("langKey");
+ Field rangeField = ValueSpec.class.getDeclaredField("range");
+ Field worldRestartField = ValueSpec.class.getDeclaredField("worldRestart");
+ Field clazzField = ValueSpec.class.getDeclaredField("clazz");
+ Field supplierField = ValueSpec.class.getDeclaredField("supplier");
+ Field validatorField = ValueSpec.class.getDeclaredField("validator");
+ UnsafeHacks.setField(commentField, result, comment);
+ UnsafeHacks.setField(langKeyField, result, langKey);
+ UnsafeHacks.setField(rangeField, result, null);
+ UnsafeHacks.setField(worldRestartField, result, worldRestart);
+ UnsafeHacks.setField(clazzField, result, clazz);
+ UnsafeHacks.setField(supplierField, result, defaultSupplier);
+ UnsafeHacks.setField(validatorField, result, validator);
+ }
+ catch (Exception e) { }
+
+ return result;
+ }
+
+ public static class Builder extends ForgeConfigSpec.Builder
+ {
+ @Override
+ public Builder comment(String comment)
+ {
+ return (Builder) super.comment(comment);
+ }
+
+ @Override
+ public Builder comment(String... comment)
+ {
+ return (Builder) super.comment(comment);
+ }
+
+ @Override
+ public Builder translation(String translationKey)
+ {
+ return (Builder) super.translation(translationKey);
+ }
+
+ @Override
+ public Builder worldRestart()
+ {
+ return (Builder) super.worldRestart();
+ }
+
+ @Override
+ public Builder push(String path)
+ {
+ return (Builder) super.push(path);
+ }
+
+ @Override
+ public Builder push(List<String> path)
+ {
+ return (Builder) super.push(path);
+ }
+
+ @Override
+ public Builder pop()
+ {
+ return (Builder) super.pop();
+ }
+
+ @Override
+ public Builder pop(int count)
+ {
+ return (Builder) super.pop(count);
+ }
+
+ public ConfigValue<UnmodifiableConfig> defineSubconfig(String path, UnmodifiableConfig defaultValue, Predicate<Object> keyValidator, Predicate<Object> valueValidator)
+ {
+ return defineSubconfig(split(path), defaultValue, keyValidator, valueValidator);
+ }
+
+ public ConfigValue<UnmodifiableConfig> defineSubconfig(List<String> path, UnmodifiableConfig defaultValue, Predicate<Object> keyValidator, Predicate<Object> valueValidator)
+ {
+ return defineSubconfig(path, () -> defaultValue, keyValidator, valueValidator);
+ }
+
+ public ConfigValue<UnmodifiableConfig> defineSubconfig(String path, Supplier<UnmodifiableConfig> defaultSupplier, Predicate<Object> keyValidator, Predicate<Object> valueValidator)
+ {
+ return defineSubconfig(split(path), defaultSupplier, keyValidator, valueValidator);
+ }
+
+ public ConfigValue<UnmodifiableConfig> defineSubconfig(List<String> path, Supplier<UnmodifiableConfig> defaultSupplier, Predicate<Object> keyValidator, Predicate<Object> valueValidator)
+ {
+ final UnmodifiableConfig defaultConfig = defaultSupplier.get();
+ return define(path, () -> MutableSubconfig.copy(defaultConfig, keyValidator, valueValidator), o -> o != null);
+ }
+
+ private IcebergConfigSpec finishBuild()
+ {
+ IcebergConfigSpec result = null;
+
+ try
+ {
+ Field valuesField = ForgeConfigSpec.Builder.class.getDeclaredField("values");
+ Field storageField = ForgeConfigSpec.Builder.class.getDeclaredField("storage");
+ Field levelCommentsField = ForgeConfigSpec.Builder.class.getDeclaredField("levelComments");
+
+ List<ConfigValue<?>> values = UnsafeHacks.<List<ConfigValue<?>>>getField(valuesField, this);
+ Config storage = UnsafeHacks.<Config>getField(storageField, this);
+ Map<List<String>, String> levelComments = UnsafeHacks.<Map<List<String>, String>>getField(levelCommentsField, this);
+
+ Config valueCfg = Config.of(Config.getDefaultMapCreator(true, true), InMemoryFormat.withSupport(ConfigValue.class::isAssignableFrom));
+ values.forEach(v -> valueCfg.set(v.getPath(), v));
+
+ final IcebergConfigSpec ret = new IcebergConfigSpec(storage, valueCfg, levelComments);
+ values.forEach(v -> {
+ try
+ {
+ Field specField = ConfigValue.class.getDeclaredField("spec");
+ UnsafeHacks.setField(specField, v, ret);
+ }
+ catch (Exception e) { }
+ });
+ result = ret;
+ }
+ catch (Exception e) { }
+ return result;
+ }
+
+ <T> Pair<T, IcebergConfigSpec> finish(Function<IcebergConfigSpec.Builder, T> consumer)
+ {
+ T o = consumer.apply(this);
+ return Pair.of(o, this.finishBuild());
+ }
+
+ @Deprecated
+ @Override
+ public <T> Pair<T, ForgeConfigSpec> configure(Function<ForgeConfigSpec.Builder, T> consumer)
+ {
+ throw new UnsupportedOperationException("Configure method not supported. Use IcebergConfig instead.");
+ }
+
+ @Deprecated
+ @Override
+ public ForgeConfigSpec build()
+ {
+ throw new UnsupportedOperationException("Build method not supported. Use IcebergConfig instead.");
+ }
+ }
+
+ /**
+ * This class is specifically meant for dynamic subconfigs--subconfigs where both keys and values are mutable.
+ */
+ final public static class MutableSubconfig extends AbstractCommentedConfig
+ {
+ private final ConfigFormat<?> configFormat;
+ private final Predicate<Object> keyValidator;
+ private final Predicate<Object> valueValidator;
+ private static ValueSpec defaultValueSpec = null;
+
+ /**
+ * Creates a Subconfig by copying a config and with the specified format.
+ *
+ * @param toCopy the config to copy
+ * @param configFormat the config's format
+ */
+ MutableSubconfig(UnmodifiableConfig toCopy, ConfigFormat<?> configFormat, boolean concurrent, Predicate<Object> keyValidator, Predicate<Object> valueValidator)
+ {
+ super(toCopy, concurrent);
+ this.configFormat = configFormat;
+ this.keyValidator = keyValidator;
+ this.valueValidator = valueValidator;
+ }
+
+ // Returns a value spec to use for each entry in this subconfig.
+ public ValueSpec defaultValueSpec()
+ {
+ if (defaultValueSpec == null)
+ {
+ defaultValueSpec = createValueSpec(null, null, false, Object.class, () -> null, valueValidator);
+ }
+ return defaultValueSpec;
+ }
+
+ @Override
+ public ConfigFormat<?> configFormat()
+ {
+ return configFormat;
+ }
+
+ public Predicate<Object> keyValidator()
+ {
+ return keyValidator;
+ }
+
+ public Predicate<Object> valueValidator()
+ {
+ return valueValidator;
+ }
+
+ /**
+ * Creates a new Subconfig with the content of the given config. The returned config will have
+ * the same format as the copied config.
+ *
+ * @param config the config to copy
+ * @return a copy of the config
+ */
+ public static MutableSubconfig copy(UnmodifiableConfig config, Predicate<Object> keyValidator, Predicate<Object> valueValidator)
+ {
+ return new MutableSubconfig(config, config.configFormat(), false, keyValidator, valueValidator);
+ }
+
+ @Override
+ public CommentedConfig createSubConfig() { throw new UnsupportedOperationException("Can't make a subconfig of a dynamic subconfig!"); }
+
+ @Override
+ public AbstractCommentedConfig clone() { throw new UnsupportedOperationException("Can't clone a dynamic subconfig!"); }
+ }
+
+ 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));
+ }
+}
diff --git a/src/main/java/com/anthonyhilyard/iceberg/mixin/ConfigMenusForgeConfigScreenMixin.java b/src/main/java/com/anthonyhilyard/iceberg/mixin/ConfigMenusForgeConfigScreenMixin.java
new file mode 100644
index 0000000..42c388d
--- /dev/null
+++ b/src/main/java/com/anthonyhilyard/iceberg/mixin/ConfigMenusForgeConfigScreenMixin.java
@@ -0,0 +1,217 @@
+package com.anthonyhilyard.iceberg.mixin;
+
+import java.lang.reflect.Constructor;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import com.anthonyhilyard.iceberg.Loader;
+import com.anthonyhilyard.iceberg.util.ConfigMenusForgeHelper;
+import com.electronwill.nightconfig.core.UnmodifiableConfig;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.Redirect;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+import fuzs.configmenusforge.client.gui.data.IEntryData;
+import fuzs.configmenusforge.client.gui.screens.ConfigScreen;
+import fuzs.configmenusforge.client.util.ServerConfigUploader;
+import net.minecraft.client.gui.screens.Screen;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraftforge.common.ForgeConfigSpec;
+import net.minecraftforge.fml.config.ModConfig;
+
+
+@Mixin(ConfigScreen.class)
+public abstract class ConfigMenusForgeConfigScreenMixin extends Screen
+{
+ protected ConfigMenusForgeConfigScreenMixin(Component p_96550_) { super(p_96550_); }
+
+ @Unique
+ private UnmodifiableConfig mainConfig = null;
+
+ @Inject(method = "create", at = @At("HEAD"), remap = false, cancellable = true)
+ private static void create(Screen lastScreen, Component title, ResourceLocation background, ModConfig config, Map<Object, IEntryData> valueToData, CallbackInfoReturnable<ConfigScreen> info)
+ {
+ try
+ {
+ Constructor<?> mainConstructor = Class.forName("fuzs.configmenusforge.client.gui.screens.ConfigScreen$Main").getDeclaredConstructor(Screen.class, Component.class, ResourceLocation.class, UnmodifiableConfig.class, Map.class, Runnable.class);
+ mainConstructor.setAccessible(true);
+ info.setReturnValue((ConfigScreen)mainConstructor.newInstance(lastScreen, title, background, ConfigMenusForgeHelper.getValues(config.getSpec()), valueToData, (Runnable)(() -> ServerConfigUploader.saveAndUpload(config))));
+ info.cancel();
+ return;
+ }
+ catch (Exception e)
+ {
+ Loader.LOGGER.warn(ExceptionUtils.getStackTrace(e.getCause()));
+ }
+ }
+
+ @Redirect(method = "<init>(Lnet/minecraft/client/gui/screens/Screen;Lnet/minecraft/network/chat/Component;Lnet/minecraft/resources/ResourceLocation;Lcom/electronwill/nightconfig/core/UnmodifiableConfig;Ljava/util/Map;[I)V",
+ at = @At(value = "INVOKE", target = "Ljava/util/Collection;stream()Ljava/util/stream/Stream;", ordinal = 0, remap = false), remap = false)
+ Stream<Object> filteredEntries(Collection<Object> values)
+ {
+ return values.stream().map(value -> {
+ if (value instanceof ForgeConfigSpec.ConfigValue<?> configValue && configValue.get() instanceof UnmodifiableConfig config)
+ {
+ return config;
+ }
+ else
+ {
+ return value;
+ }
+ });
+ }
+
+ /// TODO: Add extended support for mutable subconfigs by adding an "Add new key" button and ability to delete keys.
+
+ // @Shadow(remap = false)
+ // @Final
+ // @Mutable
+ // private List<IEntryData> searchEntries;
+
+ // @Shadow(remap = false)
+ // @Final
+ // @Mutable
+ // private List<IEntryData> screenEntries;
+
+ // @Shadow(remap = false)
+ // @Final
+ // @Mutable
+ // Map<Object, IEntryData> valueToData;
+
+ // @Shadow(remap = false)
+ // EditBox searchTextField;
+
+ // @Shadow(remap = false)
+ // @Final
+ // ResourceLocation background;
+
+ // @Shadow(remap = false)
+ // List<ConfigScreen.Entry> getConfigListEntries(List<IEntryData> entries, final String searchHighlight) { return null; }
+
+ // @Inject(method = "getConfigListEntries(Ljava/lang/String;)Ljava/util/List;", at = @At("HEAD"), remap = false, cancellable = true)
+ // private void getConfigListEntries(String query, CallbackInfoReturnable<List<ConfigScreen.Entry>> info)
+ // {
+ // query = query.toLowerCase(Locale.ROOT).trim();
+ // if (query.isEmpty())
+ // {
+ // List<ConfigScreen.Entry> entries = Lists.newArrayList(getConfigListEntries(screenEntries, query));
+
+ // // Add an "add new key" button if this is a dynamic subconfig. We can't be sure that's what this is,
+ // // since we don't have access to the spec here, so we're going to have to make an assumption...
+ // try
+ // {
+ // if (mainConfig != null && mainConfig.getClass().isAssignableFrom(Class.forName("com.electronwill.nightconfig.core.SimpleCommentedConfig")))
+ // {
+ // Class<?> categoryEntryClass = Class.forName("fuzs.configmenusforge.client.gui.screens.ConfigScreen$CategoryEntry");
+
+ // Constructor<?> categoryEntryConstructor = categoryEntryClass.getDeclaredConstructor(ConfigScreen.class, CategoryEntryData.class, String.class);
+ // categoryEntryConstructor.setAccessible(true);
+ // ConfigScreen.Entry addNewKeyEntry = (ConfigScreen.Entry) categoryEntryConstructor.newInstance(this, new CategoryEntryData(null, null, null) {
+ // // TODO: Make translatable
+ // private static Component title = new TextComponent("Add new key");
+ // @Override
+ // public String getPath() { return null; }
+ // @Override
+ // public String getComment() { return null; }
+ // @Override
+ // public Component getTitle() { return title; }
+ // @Override
+ // public boolean mayResetValue() { return false; }
+ // @Override
+ // public boolean mayDiscardChanges() { return false; }
+ // @Override
+ // public void resetCurrentValue() { }
+ // @Override
+ // public void discardCurrentValue() { }
+ // @Override
+ // public void saveConfigValue() { }
+ // @Override
+ // public boolean category() { return false; }
+ // }, null);
+
+ // Field buttonField = categoryEntryClass.getDeclaredField("button");
+ // UnsafeHacks.setField(buttonField, addNewKeyEntry, new Button(10, 5, 260, 20, new TextComponent("Add new key"), button -> {
+ // searchTextField.setValue("");
+ // searchTextField.setFocus(false);
+ // Screen editScreen = new EditStringScreen((ConfigScreen)(Object)this, title, background, "", x -> true, currentValue -> {
+ // ((Config)mainConfig).set(currentValue, "");
+ // // Update screen and search entries lists.
+ // List<IEntryData> newEntries = Lists.newArrayList();
+ // ValueSpec newValueSpec = IcebergConfigSpec.createValueSpec(null, null, false, Object.class, () -> null, v -> v != null);
+ // final EntryData.ConfigEntryData<?> data = new DynamicConfigEntryData<>(List.of(currentValue), "", newValueSpec, mainConfig);
+ // valueToData = Maps.newLinkedHashMap(valueToData);
+ // valueToData.put(currentValue, data);
+ // gatherEntries(mainConfig, newEntries, valueToData);
+ // searchEntries = newEntries;
+ // screenEntries = mainConfig.valueMap().values().stream().map(valueToData::get).toList();
+ // ((ConfigScreen)(Object)this).updateList(false);
+ // });
+ // final Minecraft minecraft = Minecraft.getInstance();
+ // minecraft.setScreen(editScreen);
+ // }));
+
+ // entries.add(addNewKeyEntry);
+ // }
+ // }
+ // catch (Exception e)
+ // {
+ // Loader.LOGGER.info(ExceptionUtils.getStackTrace(e));
+ // }
+
+ // info.setReturnValue(entries);
+ // }
+ // else
+ // {
+ // info.setReturnValue(getConfigListEntries(searchEntries, query));
+ // }
+
+ // info.cancel();
+ // }
+
+ @Inject(method = "gatherEntriesRecursive(Lcom/electronwill/nightconfig/core/UnmodifiableConfig;Ljava/util/Map;)Ljava/util/List;",
+ at = @At("HEAD"), remap = false, cancellable = true)
+ private void gatherEntriesRecursiveSubconfigSupport(UnmodifiableConfig mainConfig, Map<Object, IEntryData> allEntries, CallbackInfoReturnable<List<IEntryData>> info)
+ {
+ // Store this config for later.
+ this.mainConfig = mainConfig;
+
+ List<IEntryData> entries = Lists.newArrayList();
+ gatherEntries(mainConfig, entries, allEntries);
+ info.setReturnValue(ImmutableList.copyOf(entries));
+ info.cancel();
+ }
+
+ @Unique
+ private static void gatherEntries(UnmodifiableConfig mainConfig, List<IEntryData> entries, Map<Object, IEntryData> entryMap)
+ {
+ for (Object value : mainConfig.valueMap().values())
+ {
+ if (entryMap.get(value) != null)
+ {
+ entries.add(entryMap.get(value));
+ }
+ if (value instanceof UnmodifiableConfig config)
+ {
+ gatherEntries(config, entries, entryMap);
+ }
+ else if (value instanceof ForgeConfigSpec.ConfigValue<?> configValue && configValue.get() instanceof UnmodifiableConfig config)
+ {
+ if (entryMap.get(config) != null)
+ {
+ entries.add(entryMap.get(config));
+ }
+ gatherEntries(config, entries, entryMap);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/anthonyhilyard/iceberg/mixin/ConfigMenusForgeIEntryDataMixin.java b/src/main/java/com/anthonyhilyard/iceberg/mixin/ConfigMenusForgeIEntryDataMixin.java
new file mode 100644
index 0000000..9bd54f6
--- /dev/null
+++ b/src/main/java/com/anthonyhilyard/iceberg/mixin/ConfigMenusForgeIEntryDataMixin.java
@@ -0,0 +1,56 @@
+package com.anthonyhilyard.iceberg.mixin;
+
+import java.util.Map;
+
+import com.anthonyhilyard.iceberg.util.ConfigMenusForgeHelper;
+import com.electronwill.nightconfig.core.UnmodifiableConfig;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Overwrite;
+
+import fuzs.configmenusforge.client.gui.data.IEntryData;
+import net.minecraftforge.fml.config.IConfigSpec;
+import net.minecraftforge.fml.config.ModConfig;
+
+@Mixin(IEntryData.class)
+public interface ConfigMenusForgeIEntryDataMixin
+{
+ /**
+ * @author Iceberg
+ * @reason Overwriting makeValueToDataMap to allow classes other than ForgeConfigSpec to be supported.
+ */
+ @Overwrite(remap = false)
+ public static Map<Object, IEntryData> makeValueToDataMap(ModConfig config)
+ {
+ if (checkInvalid(config))
+ {
+ return ImmutableMap.of();
+ }
+ Map<Object, IEntryData> allData = Maps.newHashMap();
+ UnmodifiableConfig spec = config.getSpec();
+ ConfigMenusForgeHelper.makeValueToDataMap(spec, ConfigMenusForgeHelper.getValues(spec), config.getConfigData(), allData, "");
+ return ImmutableMap.copyOf(allData);
+ }
+
+ /**
+ * @author Iceberg
+ * @reason Overwriting checkInvalid to allow classes other than ForgeConfigSpec to be supported.
+ */
+ @Overwrite(remap = false)
+ public static boolean checkInvalid(ModConfig config)
+ {
+ IConfigSpec<?> spec = config.getSpec();
+
+ // True / false means the config class has been cached, null means it's new.
+ Boolean cachedValue = ConfigMenusForgeHelper.cachedValidity(spec.getClass());
+ if (cachedValue == null)
+ {
+ // It's not cached, so do the lookup via MethodHandles API and cache the results.
+ ConfigMenusForgeHelper.cacheClass(spec.getClass());
+ }
+
+ return config.getConfigData() == null || !ConfigMenusForgeHelper.cachedValidity(spec.getClass()) || !ConfigMenusForgeHelper.isLoaded(spec);
+ }
+}
diff --git a/src/main/java/com/anthonyhilyard/iceberg/mixin/ConfigMenusForgeServerConfigUploaderMixin.java b/src/main/java/com/anthonyhilyard/iceberg/mixin/ConfigMenusForgeServerConfigUploaderMixin.java
new file mode 100644
index 0000000..003251f
--- /dev/null
+++ b/src/main/java/com/anthonyhilyard/iceberg/mixin/ConfigMenusForgeServerConfigUploaderMixin.java
@@ -0,0 +1,40 @@
+package com.anthonyhilyard.iceberg.mixin;
+
+import java.io.ByteArrayOutputStream;
+
+import com.anthonyhilyard.iceberg.util.ConfigMenusForgeHelper;
+import com.electronwill.nightconfig.toml.TomlFormat;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import fuzs.configmenusforge.ConfigMenusForge;
+import fuzs.configmenusforge.client.util.ModConfigSync;
+import fuzs.configmenusforge.client.util.ServerConfigUploader;
+import fuzs.configmenusforge.network.client.message.C2SSendConfigMessage;
+import net.minecraft.client.Minecraft;
+import net.minecraftforge.fml.config.ModConfig;
+
+@Mixin(ServerConfigUploader.class)
+public class ConfigMenusForgeServerConfigUploaderMixin
+{
+ @Inject(method = "saveAndUpload", at = @At("HEAD"), remap = false, cancellable = true)
+ private static void saveAndUpload(ModConfig config, CallbackInfo info)
+ {
+ ConfigMenusForgeHelper.save(config.getSpec());
+ ModConfigSync.fireReloadingEvent(config);
+ if (config.getType() == ModConfig.Type.SERVER)
+ {
+ final Minecraft minecraft = Minecraft.getInstance();
+ if (minecraft.getConnection() != null && !minecraft.isLocalServer())
+ {
+ final ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ TomlFormat.instance().createWriter().write(config.getConfigData(), stream);
+ ConfigMenusForge.NETWORK.sendToServer(new C2SSendConfigMessage(config.getFileName(), stream.toByteArray()));
+ }
+ }
+ info.cancel();
+ }
+}
diff --git a/src/main/java/com/anthonyhilyard/iceberg/mixin/ForgeConfigMenusPlugin.java b/src/main/java/com/anthonyhilyard/iceberg/mixin/ForgeConfigMenusPlugin.java
new file mode 100644
index 0000000..23c70ad
--- /dev/null
+++ b/src/main/java/com/anthonyhilyard/iceberg/mixin/ForgeConfigMenusPlugin.java
@@ -0,0 +1,59 @@
+package com.anthonyhilyard.iceberg.mixin;
+
+import java.util.List;
+import java.util.Set;
+
+import org.objectweb.asm.tree.ClassNode;
+import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
+import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
+
+import net.minecraftforge.fml.loading.FMLLoader;
+import net.minecraftforge.fml.loading.LoadingModList;
+import net.minecraftforge.fml.loading.moddiscovery.ModInfo;
+
+public class ForgeConfigMenusPlugin implements IMixinConfigPlugin
+{
+ private LoadingModList loadingModList = null;
+
+ @Override
+ public void onLoad(String mixinPackage) { }
+
+ @Override
+ public String getRefMapperConfig() { return null; }
+
+ @Override
+ public boolean shouldApplyMixin(String targetClassName, String mixinClassName)
+ {
+ if (mixinClassName.toLowerCase().contains("configmenusforge"))
+ {
+ if (loadingModList == null)
+ {
+ loadingModList = FMLLoader.getLoadingModList();
+ }
+
+ // Check if Config Menus for Forge is available.
+ for (ModInfo modInfo : loadingModList.getMods())
+ {
+ if (modInfo.getModId().equals("configmenusforge"))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) { }
+
+ @Override
+ public List<String> getMixins() { return null; }
+
+ @Override
+ public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { }
+
+ @Override
+ public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { }
+} \ No newline at end of file
diff --git a/src/main/java/com/anthonyhilyard/iceberg/mixin/ForgeConfigSpecMixin.java b/src/main/java/com/anthonyhilyard/iceberg/mixin/ForgeConfigSpecMixin.java
deleted file mode 100644
index 288af26..0000000
--- a/src/main/java/com/anthonyhilyard/iceberg/mixin/ForgeConfigSpecMixin.java
+++ /dev/null
@@ -1,160 +0,0 @@
-package com.anthonyhilyard.iceberg.mixin;
-
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-
-import javax.annotation.Nullable;
-
-import com.anthonyhilyard.iceberg.util.DynamicSubconfig;
-import com.electronwill.nightconfig.core.CommentedConfig;
-import com.electronwill.nightconfig.core.UnmodifiableConfig;
-import com.electronwill.nightconfig.core.ConfigSpec.CorrectionAction;
-import com.electronwill.nightconfig.core.ConfigSpec.CorrectionListener;
-
-import org.spongepowered.asm.mixin.Mixin;
-import org.spongepowered.asm.mixin.Overwrite;
-import org.spongepowered.asm.mixin.Shadow;
-
-import net.minecraftforge.common.ForgeConfigSpec;
-import net.minecraftforge.common.ForgeConfigSpec.ValueSpec;
-
-
-@Mixin(value = ForgeConfigSpec.class, remap = false)
-public class ForgeConfigSpecMixin
-{
- @Shadow(remap = false)
- private Map<List<String>, String> levelComments;
-
- @Shadow(remap = false)
- private boolean stringsMatchIgnoringNewlines(@Nullable Object obj1, @Nullable Object obj2) { return false; }
-
- /**
- * @author iceberg
- * @reason Overwrite the correct method to fix subconfigs not being handled properly.
- */
- @Overwrite(remap = false)
- private int correct(UnmodifiableConfig spec, CommentedConfig config, LinkedList<String> parentPath, List<String> parentPathUnmodifiable, CorrectionListener listener, CorrectionListener commentListener, 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();
- Object specValue = specEntry.getValue();
- final Object configValue = configMap.get(key);
- final CorrectionAction action = configValue == null ? CorrectionAction.ADD : CorrectionAction.REPLACE;
-
- parentPath.addLast(key);
-
- String subConfigComment = null;
-
- // If this value is a config, use that as the spec value to support subconfigs.
- if (specValue instanceof ValueSpec valueSpec && valueSpec.getDefault() instanceof UnmodifiableConfig)
- {
- subConfigComment = valueSpec.getComment();
- specValue = valueSpec.getDefault();
- }
-
- if (specValue instanceof UnmodifiableConfig)
- {
- if (configValue instanceof CommentedConfig)
- {
- count += correct((UnmodifiableConfig)specValue, (CommentedConfig)configValue, parentPath, parentPathUnmodifiable, listener, commentListener, 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((UnmodifiableConfig)specValue, newValue, parentPath, parentPathUnmodifiable, listener, commentListener, dryRun);
- }
-
- String newComment = subConfigComment == null ? levelComments.get(parentPath) : subConfigComment;
- String oldComment = config.getComment(key);
- if (!stringsMatchIgnoringNewlines(oldComment, newComment))
- {
- if (commentListener != null)
- {
- commentListener.onCorrect(action, parentPathUnmodifiable, oldComment, newComment);
- }
-
- if (dryRun)
- {
- return 1;
- }
-
- 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 (!stringsMatchIgnoringNewlines(oldComment, valueSpec.getComment()))
- {
- if (commentListener != null)
- {
- commentListener.onCorrect(action, parentPathUnmodifiable, oldComment, valueSpec.getComment());
- }
-
- if (dryRun)
- {
- return 1;
- }
-
- 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 the spec is a dynamic subconfig, don't bother checking the spec since that's the point.
- if (!(spec instanceof DynamicSubconfig) && !specMap.containsKey(entry.getKey()))
- {
- if (dryRun)
- {
- return 1;
- }
-
- ittr.remove();
- parentPath.addLast(entry.getKey());
- listener.onCorrect(CorrectionAction.REMOVE, parentPathUnmodifiable, entry.getValue(), null);
- parentPath.removeLast();
- count++;
- }
- }
- return count;
- }
-}
diff --git a/src/main/java/com/anthonyhilyard/iceberg/mixin/TextColorMixin.java b/src/main/java/com/anthonyhilyard/iceberg/mixin/TextColorMixin.java
index 2bd40ba..9a87af8 100644
--- a/src/main/java/com/anthonyhilyard/iceberg/mixin/TextColorMixin.java
+++ b/src/main/java/com/anthonyhilyard/iceberg/mixin/TextColorMixin.java
@@ -14,23 +14,23 @@ public class TextColorMixin
* Fix an issue in TextColor parsing that makes it so only alpha values up to 0x7F are supported.
*/
@Inject(method = "parseColor", at = @At("HEAD"), cancellable = true)
- private static boolean parseColor(String colorString, CallbackInfoReturnable<TextColor> info)
+ private static void parseColor(String colorString, CallbackInfoReturnable<TextColor> info)
{
if (!colorString.startsWith("#"))
{
- return false;
+ return;
}
try
{
int i = Integer.parseUnsignedInt(colorString.substring(1), 16);
info.setReturnValue(TextColor.fromRgb(i));
- return true;
+ info.cancel();
}
catch (NumberFormatException numberformatexception)
{
info.setReturnValue(null);
- return true;
+ info.cancel();
}
}
}
diff --git a/src/main/java/com/anthonyhilyard/iceberg/util/ConfigMenusForgeHelper.java b/src/main/java/com/anthonyhilyard/iceberg/util/ConfigMenusForgeHelper.java
new file mode 100644
index 0000000..ad4f8d5
--- /dev/null
+++ b/src/main/java/com/anthonyhilyard/iceberg/util/ConfigMenusForgeHelper.java
@@ -0,0 +1,266 @@
+package com.anthonyhilyard.iceberg.util;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.Field;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+import com.anthonyhilyard.iceberg.Loader;
+import com.electronwill.nightconfig.core.CommentedConfig;
+import com.electronwill.nightconfig.core.Config;
+import com.electronwill.nightconfig.core.UnmodifiableConfig;
+import com.google.common.collect.Maps;
+import com.google.common.base.Objects;
+import com.google.common.collect.Iterators;
+
+import org.apache.commons.lang3.exception.ExceptionUtils;
+
+import fuzs.configmenusforge.client.gui.data.EntryData;
+import fuzs.configmenusforge.client.gui.data.IEntryData;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.chat.TextComponent;
+import net.minecraftforge.common.ForgeConfigSpec;
+import net.minecraftforge.fml.unsafe.UnsafeHacks;
+
+public class ConfigMenusForgeHelper
+{
+ private final static Map<Class<?>, List<MethodHandle>> configSpecMethodHandles = Maps.newHashMap();
+ private final static Map<Class<?>, Boolean> cachedConfigSpecClasses = Maps.newHashMap();
+ private final static MethodType getValuesMethodType = MethodType.methodType(UnmodifiableConfig.class);
+ private final static MethodType isLoadedMethodType = MethodType.methodType(boolean.class);
+ private final static MethodType saveMethodType = MethodType.methodType(void.class);
+
+ private static Object callMethod(UnmodifiableConfig spec, int methodIndex)
+ {
+ Class<?> specClass = spec.getClass();
+ if (!cachedConfigSpecClasses.containsKey(specClass))
+ {
+ cacheClass(specClass);
+ }
+
+ if (configSpecMethodHandles.containsKey(specClass))
+ {
+ try
+ {
+ return configSpecMethodHandles.get(specClass).get(methodIndex).invoke(spec);
+ }
+ catch (Throwable e)
+ {
+ Loader.LOGGER.warn(ExceptionUtils.getStackTrace(e));
+ }
+ }
+ return null;
+ }
+
+ public static UnmodifiableConfig getValues(UnmodifiableConfig spec)
+ {
+ return (UnmodifiableConfig) callMethod(spec, 0);
+ }
+
+ public static boolean isLoaded(UnmodifiableConfig spec)
+ {
+ return (Boolean) callMethod(spec, 1);
+ }
+
+ public static void save(UnmodifiableConfig spec)
+ {
+ callMethod(spec, 2);
+ }
+
+ public static Boolean cachedValidity(Class<?> specClass)
+ {
+ return cachedConfigSpecClasses.getOrDefault(specClass, null);
+ }
+
+ public static void cacheClass(Class<?> specClass)
+ {
+ MethodHandle getValuesMethod = null;
+ MethodHandle isLoadedMethod = null;
+ MethodHandle saveMethod = null;
+ try
+ {
+ getValuesMethod = MethodHandles.lookup().findVirtual(specClass, "getValues", getValuesMethodType);
+ isLoadedMethod = MethodHandles.lookup().findVirtual(specClass, "isLoaded", isLoadedMethodType);
+ saveMethod = MethodHandles.lookup().findVirtual(specClass, "save", saveMethodType);
+ }
+ catch (Throwable e)
+ {
+ Loader.LOGGER.warn(ExceptionUtils.getStackTrace(e));
+ }
+
+ // If we found valid getValues, isLoaded and save methods, add them to the cache.
+ if (getValuesMethod != null && isLoadedMethod != null && saveMethod != null)
+ {
+ cachedConfigSpecClasses.put(specClass, true);
+ configSpecMethodHandles.put(specClass, List.of(getValuesMethod, isLoadedMethod, saveMethod));
+ }
+ else
+ {
+ cachedConfigSpecClasses.put(specClass, false);
+ }
+ }
+
+ /**
+ * Changed spec from a ForgeConfigSpec to an UnmodifiableConfig.
+ */
+ public static void makeValueToDataMap(UnmodifiableConfig spec, UnmodifiableConfig values, CommentedConfig comments, Map<Object, IEntryData> allData, String basePath)
+ {
+ for (String path : values.valueMap().keySet())
+ {
+ String currentPath = basePath.isEmpty() ? path : basePath + "." + path;
+ Object value = values.valueMap().get(path);
+ if (value instanceof UnmodifiableConfig category)
+ {
+ final EntryData.CategoryEntryData data = new EntryData.CategoryEntryData(path, category, comments.getComment(path));
+ allData.put(category, data);
+ makeValueToDataMap(spec, category, (CommentedConfig) comments.valueMap().get(path), allData, currentPath);
+ }
+ else if (value instanceof ForgeConfigSpec.ConfigValue<?> configValue && configValue.get() instanceof UnmodifiableConfig category)
+ {
+ final EntryData.CategoryEntryData data = new DynamicCategoryEntryData(path, category, comments.getComment(path));
+ allData.put(category, data);
+ makeValueToDataMap(spec, category, (CommentedConfig) comments.valueMap().get(path), allData, currentPath);
+ }
+ else if (value instanceof ForgeConfigSpec.ConfigValue<?> configValue)
+ {
+ final EntryData.ConfigEntryData<?> data = new EntryData.ConfigEntryData<>(path, configValue, spec.getRaw(configValue.getPath()));
+ allData.put(configValue, data);
+ }
+ // Allow non-configvalue values if the parent is a dynamic subconfig.
+ else if (!(value instanceof ForgeConfigSpec.ConfigValue<?>) && allData.containsKey(values) && allData.get(values) instanceof DynamicCategoryEntryData)
+ {
+ final EntryData.ConfigEntryData<?> data = new DynamicConfigEntryData<>(List.of(currentPath.split("\\.")), value, spec.getRaw(currentPath), spec);
+ allData.put(value, data);
+ }
+ }
+ }
+
+ public static class DynamicCategoryEntryData extends EntryData.CategoryEntryData
+ {
+ public DynamicCategoryEntryData(String path, UnmodifiableConfig config, String comment) {
+ super(path, config, comment);
+ }
+ }
+
+ public static class DynamicConfigEntryData<T> extends EntryData.ConfigEntryData<T>
+ {
+ private final ForgeConfigSpec.ValueSpec valueSpec;
+ private T currentValue;
+ private T configValue;
+ private final List<String> fullPath;
+ private final UnmodifiableConfig spec;
+
+ private final static ForgeConfigSpec.ConfigValue<?> dummyConfigValue;
+
+ private Component title;
+
+ static
+ {
+ dummyConfigValue = UnsafeHacks.newInstance(ForgeConfigSpec.ConfigValue.class);
+ try
+ {
+ Field specField = ForgeConfigSpec.ConfigValue.class.getDeclaredField("spec");
+ UnsafeHacks.setField(specField, dummyConfigValue, UnsafeHacks.newInstance(ForgeConfigSpec.class));
+ Field defaultSupplierField = ForgeConfigSpec.ConfigValue.class.getDeclaredField("defaultSupplier");
+ UnsafeHacks.setField(defaultSupplierField, dummyConfigValue, (Supplier<?>)(() -> null));
+ }
+ catch (Exception e) { }
+ }
+
+ @SuppressWarnings("unchecked")
+ public DynamicConfigEntryData(List<String> fullPath, T configValue, ForgeConfigSpec.ValueSpec valueSpec, UnmodifiableConfig spec)
+ {
+ super(fullPath.get(fullPath.size() - 1), (ForgeConfigSpec.ConfigValue<T>) dummyConfigValue, valueSpec);
+ this.configValue = configValue;
+ this.currentValue = configValue;
+ this.valueSpec = valueSpec;
+ this.fullPath = fullPath;
+ this.spec = spec;
+
+ // We will override the normal title functionality since we want it to be unformatted.
+ this.title = new TextComponent(getPath());
+ }
+
+ @Override
+ public Component getTitle()
+ {
+ return this.title;
+ }
+
+ @Override
+ public boolean mayResetValue()
+ {
+ return !listSafeEquals(currentValue, getDefaultValue());
+ }
+
+ @Override
+ public boolean mayDiscardChanges()
+ {
+ return listSafeEquals(configValue, currentValue);
+ }
+
+ private static <T> boolean listSafeEquals(T o1, T o2)
+ {
+ // Attempts to solve an issue where types of lists won't match when one is read from file
+ // (due to enum being converted to string, long to int)
+ if (o1 instanceof List<?> list1 && o2 instanceof List<?> list2)
+ {
+ final Stream<String> stream1 = list1.stream().map(o -> o instanceof Enum<?> e ? e.name() : o.toString());
+ final Stream<String> stream2 = list2.stream().map(o -> o instanceof Enum<?> e ? e.name() : o.toString());
+ return Iterators.elementsEqual(stream1.iterator(), stream2.iterator());
+ }
+ return Objects.equal(o1, o2);
+ }
+
+ @Override
+ public void resetCurrentValue()
+ {
+ currentValue = getDefaultValue();
+ }
+
+ @Override
+ public void discardCurrentValue()
+ {
+ currentValue = configValue;
+ }
+
+ @Override
+ public void saveConfigValue()
+ {
+ try
+ {
+ Field childConfigField = spec.getClass().getDeclaredField("childConfig");
+ Config childConfig = UnsafeHacks.getField(childConfigField, spec);
+ childConfig.set(fullPath, currentValue);
+ }
+ catch (Exception e) { }
+ configValue = currentValue;
+ }
+
+ @SuppressWarnings("unchecked")
+ public T getDefaultValue()
+ {
+ return (T) valueSpec.getDefault();
+ }
+
+ public T getCurrentValue()
+ {
+ return currentValue;
+ }
+
+ public void setCurrentValue(T newValue)
+ {
+ currentValue = newValue;
+ }
+
+ @Override
+ public List<String> getFullPath()
+ {
+ return fullPath;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/anthonyhilyard/iceberg/util/DynamicSubconfig.java b/src/main/java/com/anthonyhilyard/iceberg/util/DynamicSubconfig.java
deleted file mode 100644
index 3ed17f3..0000000
--- a/src/main/java/com/anthonyhilyard/iceberg/util/DynamicSubconfig.java
+++ /dev/null
@@ -1,138 +0,0 @@
-package com.anthonyhilyard.iceberg.util;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.Supplier;
-
-import com.electronwill.nightconfig.core.AbstractCommentedConfig;
-import com.electronwill.nightconfig.core.ConfigFormat;
-import com.electronwill.nightconfig.core.UnmodifiableCommentedConfig;
-import com.electronwill.nightconfig.core.UnmodifiableConfig;
-
-/**
- * An exact copy of SimpleCommentedConfig, but this class is specifically meant for subconfigs.
- * That being said--the class of a config is checked during config validation, and subconfigs are allowed
- * extra leniency in config keys.
- */
-final public class DynamicSubconfig extends AbstractCommentedConfig
-{
- private final ConfigFormat<?> configFormat;
-
- /**
- * Creates a Subconfig with the specified format.
- *
- * @param configFormat the config's format
- */
- DynamicSubconfig(ConfigFormat<?> configFormat, boolean concurrent)
- {
- super(concurrent ? new ConcurrentHashMap<>() : new HashMap<>());
- this.configFormat = configFormat;
- }
-
- /**
- * Creates a Subconfig with the specified data and format. The map is used as it is and
- * isn't copied.
- */
- DynamicSubconfig(Map<String, Object> valueMap, ConfigFormat<?> configFormat)
- {
- super(valueMap);
- this.configFormat = configFormat;
- }
-
- /**
- * Creates a Subconfig with the specified backing map supplier and format.
- *
- * @param mapCreator the supplier for backing maps
- * @param configFormat the config's format
- */
- DynamicSubconfig(Supplier<Map<String, Object>> mapCreator, ConfigFormat<?> configFormat)
- {
- super(mapCreator);
- this.configFormat = configFormat;
- }
-
- /**
- * Creates a Subconfig by copying a config and with the specified format.
- *
- * @param toCopy the config to copy
- * @param configFormat the config's format
- */
- DynamicSubconfig(UnmodifiableConfig toCopy, ConfigFormat<?> configFormat,
- boolean concurrent)
- {
- super(toCopy, concurrent);
- this.configFormat = configFormat;
- }
-
- /**
- * Creates a Subconfig by copying a config, with the specified backing map creator and format.
- *
- * @param toCopy the config to copy
- * @param mapCreator the supplier for backing maps
- * @param configFormat the config's format
- */
- public DynamicSubconfig(UnmodifiableConfig toCopy, Supplier<Map<String, Object>> mapCreator,
- ConfigFormat<?> configFormat)
- {
- super(toCopy, mapCreator);
- this.configFormat = configFormat;
- }
-
- /**
- * Creates a Subconfig by copying a config and with the specified format.
- *
- * @param toCopy the config to copy
- * @param configFormat the config's format
- */
- DynamicSubconfig(UnmodifiableCommentedConfig toCopy, ConfigFormat<?> configFormat,
- boolean concurrent)
- {
- super(toCopy, concurrent);
- this.configFormat = configFormat;
- }
-
- /**
- * Creates a Subconfig by copying a config, with the specified backing map creator and format.
- *
- * @param toCopy the config to copy
- * @param mapCreator the supplier for backing maps
- * @param configFormat the config's format
- */
- public DynamicSubconfig(UnmodifiableCommentedConfig toCopy, Supplier<Map<String, Object>> mapCreator,
- ConfigFormat<?> configFormat)
- {
- super(toCopy, mapCreator);
- this.configFormat = configFormat;
- }
-
- @Override
- public ConfigFormat<?> configFormat()
- {
- return configFormat;
- }
-
- @Override
- public DynamicSubconfig createSubConfig()
- {
- return new DynamicSubconfig(mapCreator, configFormat);
- }
-
- @Override
- public AbstractCommentedConfig clone()
- {
- return new DynamicSubconfig(this, mapCreator, configFormat);
- }
-
- /**
- * Creates a new Subconfig with the content of the given config. The returned config will have
- * the same format as the copied config.
- *
- * @param config the config to copy
- * @return a copy of the config
- */
- static DynamicSubconfig copy(UnmodifiableConfig config)
- {
- return new DynamicSubconfig(config, config.configFormat(), false);
- }
-} \ No newline at end of file
diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg
index b3edbd7..a58652d 100644
--- a/src/main/resources/META-INF/accesstransformer.cfg
+++ b/src/main/resources/META-INF/accesstransformer.cfg
@@ -1 +1,2 @@
-public net.minecraft.client.gui.screens.inventory.tooltip.ClientTextTooltip f_169936_ # text \ No newline at end of file
+public net.minecraft.client.gui.screens.inventory.tooltip.ClientTextTooltip f_169936_ # text
+protected net.minecraftforge.common.ForgeConfigSpec <init>(Lcom/electronwill/nightconfig/core/UnmodifiableConfig;Lcom/electronwill/nightconfig/core/UnmodifiableConfig;Ljava/util/Map;)V # constructor \ No newline at end of file
diff --git a/src/main/resources/iceberg.mixins.json b/src/main/resources/iceberg.mixins.json
index f167048..d722775 100644
--- a/src/main/resources/iceberg.mixins.json
+++ b/src/main/resources/iceberg.mixins.json
@@ -3,15 +3,18 @@
"package": "com.anthonyhilyard.iceberg.mixin",
"compatibilityLevel": "JAVA_17",
"refmap": "iceberg.refmap.json",
+ "plugin": "com.anthonyhilyard.iceberg.mixin.ForgeConfigMenusPlugin",
"mixins": [
"EntityMixin",
- "PlayerAdvancementsMixin",
- "ForgeConfigSpecMixin"
+ "PlayerAdvancementsMixin"
],
"client": [
"ScreenMixin",
"ClientPacketListenerMixin",
- "TextColorMixin"
+ "TextColorMixin",
+ "ConfigMenusForgeIEntryDataMixin",
+ "ConfigMenusForgeConfigScreenMixin",
+ "ConfigMenusForgeServerConfigUploaderMixin"
],
"injectors": {
"defaultRequire": 1