aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-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
12 files changed, 1337 insertions, 306 deletions
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;
<