diff options
Diffstat (limited to 'src')
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; < |
