From 0cd83156930cf320578e09fafa17069bedd8230f Mon Sep 17 00:00:00 2001 From: Anthony Hilyard Date: Tue, 25 Jan 2022 09:18:04 -0800 Subject: Rewrote new config system, added support for Config Menus for Forge mod. --- CHANGELOG.md | 2 +- build.gradle | 8 + .../iceberg/config/IcebergConfig.java | 89 +++ .../iceberg/config/IcebergConfigSpec.java | 598 +++++++++++++++++++++ .../mixin/ConfigMenusForgeConfigScreenMixin.java | 217 ++++++++ .../mixin/ConfigMenusForgeIEntryDataMixin.java | 56 ++ .../ConfigMenusForgeServerConfigUploaderMixin.java | 40 ++ .../iceberg/mixin/ForgeConfigMenusPlugin.java | 59 ++ .../iceberg/mixin/ForgeConfigSpecMixin.java | 160 ------ .../iceberg/mixin/TextColorMixin.java | 8 +- .../iceberg/util/ConfigMenusForgeHelper.java | 266 +++++++++ .../iceberg/util/DynamicSubconfig.java | 138 ----- src/main/resources/META-INF/accesstransformer.cfg | 3 +- src/main/resources/iceberg.mixins.json | 9 +- 14 files changed, 1346 insertions(+), 307 deletions(-) create mode 100644 src/main/java/com/anthonyhilyard/iceberg/config/IcebergConfig.java create mode 100644 src/main/java/com/anthonyhilyard/iceberg/config/IcebergConfigSpec.java create mode 100644 src/main/java/com/anthonyhilyard/iceberg/mixin/ConfigMenusForgeConfigScreenMixin.java create mode 100644 src/main/java/com/anthonyhilyard/iceberg/mixin/ConfigMenusForgeIEntryDataMixin.java create mode 100644 src/main/java/com/anthonyhilyard/iceberg/mixin/ConfigMenusForgeServerConfigUploaderMixin.java create mode 100644 src/main/java/com/anthonyhilyard/iceberg/mixin/ForgeConfigMenusPlugin.java delete mode 100644 src/main/java/com/anthonyhilyard/iceberg/mixin/ForgeConfigSpecMixin.java create mode 100644 src/main/java/com/anthonyhilyard/iceberg/util/ConfigMenusForgeHelper.java delete mode 100644 src/main/java/com/anthonyhilyard/iceberg/util/DynamicSubconfig.java 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> +{ + private static IcebergConfigSpec SPEC = null; + private static IcebergConfig INSTANCE = null; + private static String modId = null; + private static boolean registered = false; + + protected abstract > 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> superClass, @Nonnull String modId) + { + if (registered) + { + return false; + } + + IcebergConfig.modId = modId; + + Pair, 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 implements IConfigSpec +{ + private Map, String> levelComments = new HashMap<>(); + + private UnmodifiableConfig values; + private Config childConfig; + + private boolean isCorrecting = false; + + private IcebergConfigSpec(UnmodifiableConfig storage, UnmodifiableConfig values, Map, 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)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 getRaw(List path) + { + T value = super.getRaw(path); + if (value != null) + { + return value; + } + // Try to "recursively" get the value if needed. + List 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 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 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 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 parentPath, List parentPathUnmodifiable, CorrectionListener listener, CorrectionListener commentListener, boolean dryRun) + { + int count = 0; + + Map specMap = spec.valueMap(); + Map configMap = config.valueMap(); + + for (Map.Entry 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> iterator = configMap.entrySet().iterator(); iterator.hasNext();) + { + Map.Entry 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 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 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 defineSubconfig(String path, UnmodifiableConfig defaultValue, Predicate keyValidator, Predicate valueValidator) + { + return defineSubconfig(split(path), defaultValue, keyValidator, valueValidator); + } + + public ConfigValue defineSubconfig(List path, UnmodifiableConfig defaultValue, Predicate keyValidator, Predicate valueValidator) + { + return defineSubconfig(path, () -> defaultValue, keyValidator, valueValidator); + } + + public ConfigValue defineSubconfig(String path, Supplier defaultSupplier, Predicate keyValidator, Predicate valueValidator) + { + return defineSubconfig(split(path), defaultSupplier, keyValidator, valueValidator); + } + + public ConfigValue defineSubconfig(List path, Supplier defaultSupplier, Predicate keyValidator, Predicate 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> values = UnsafeHacks.>>getField(valuesField, this); + Config storage = UnsafeHacks.getField(storageField, this); + Map, String> levelComments = UnsafeHacks., 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; + } + + Pair finish(Function consumer) + { + T o = consumer.apply(this); + return Pair.of(o, this.finishBuild()); + } + + @Deprecated + @Override + public Pair configure(Function 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 keyValidator; + private final Predicate 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 keyValidator, Predicate 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 keyValidator() + { + return keyValidator; + } + + public Predicate 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 keyValidator, Predicate 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 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 valueToData, CallbackInfoReturnable 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 = "(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 filteredEntries(Collection 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 searchEntries; + + // @Shadow(remap = false) + // @Final + // @Mutable + // private List screenEntries; + + // @Shadow(remap = false) + // @Final + // @Mutable + // Map valueToData; + + // @Shadow(remap = false) + // EditBox searchTextField; + + // @Shadow(remap = false) + // @Final + // ResourceLocation background; + + // @Shadow(remap = false) + // List getConfigListEntries(List 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> info) + // { + // query = query.toLowerCase(Locale.ROOT).trim(); + // if (query.isEmpty()) + // { + // List 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 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 allEntries, CallbackInfoReturnable> info) + { + // Store this config for later. + this.mainConfig = mainConfig; + + List entries = Lists.newArrayList(); + gatherEntries(mainConfig, entries, allEntries); + info.setReturnValue(ImmutableList.copyOf(entries)); + info.cancel(); + } + + @Unique + private static void gatherEntries(UnmodifiableConfig mainConfig, List entries, Map 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 makeValueToDataMap(ModConfig config) + { + if (checkInvalid(config)) + { + return ImmutableMap.of(); + } + Map 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 myTargets, Set otherTargets) { } + + @Override + public List 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, 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 parentPath, List parentPathUnmodifiable, CorrectionListener listener, CorrectionListener commentListener, boolean dryRun) - { - int count = 0; - - Map specMap = spec.valueMap(); - Map configMap = config.valueMap(); - - for (Map.Entry 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> ittr = configMap.entrySet().iterator(); ittr.hasNext();) - { - Map.Entry 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 info) + private static void parseColor(String colorString, CallbackInfoReturnable 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, List> configSpecMethodHandles = Maps.newHashMap(); + private final static Map, 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 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 extends EntryData.ConfigEntryData + { + private final ForgeConfigSpec.ValueSpec valueSpec; + private T currentValue; + private T configValue; + private final List 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 fullPath, T configValue, ForgeConfigSpec.ValueSpec valueSpec, UnmodifiableConfig spec) + { + super(fullPath.get(fullPath.size() - 1), (ForgeConfigSpec.ConfigValue) 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 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 stream1 = list1.stream().map(o -> o instanceof Enum e ? e.name() : o.toString()); + final Stream 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 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 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> 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> 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> 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 (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 -- cgit