diff options
author | Anthony Hilyard <anthony.hilyard@gmail.com> | 2022-01-25 09:18:04 -0800 |
---|---|---|
committer | Anthony Hilyard <anthony.hilyard@gmail.com> | 2022-01-25 09:18:04 -0800 |
commit | 0cd83156930cf320578e09fafa17069bedd8230f (patch) | |
tree | 63e9d0322326126d4beaa497e06050ba905f555c /src/main/java/com/anthonyhilyard/iceberg/util | |
parent | 7a3df325b5bb294c2836082f58b719c70792ed61 (diff) | |
download | Iceberg-0cd83156930cf320578e09fafa17069bedd8230f.tar.gz Iceberg-0cd83156930cf320578e09fafa17069bedd8230f.tar.bz2 Iceberg-0cd83156930cf320578e09fafa17069bedd8230f.zip |
Rewrote new config system, added support for Config Menus for Forge mod.
Diffstat (limited to 'src/main/java/com/anthonyhilyard/iceberg/util')
-rw-r--r-- | src/main/java/com/anthonyhilyard/iceberg/util/ConfigMenusForgeHelper.java | 266 | ||||
-rw-r--r-- | src/main/java/com/anthonyhilyard/iceberg/util/DynamicSubconfig.java | 138 |
2 files changed, 266 insertions, 138 deletions
diff --git a/src/main/java/com/anthonyhilyard/iceberg/util/ConfigMenusForgeHelper.java b/src/main/java/com/anthonyhilyard/iceberg/util/ConfigMenusForgeHelper.java new file mode 100644 index 0000000..ad4f8d5 --- /dev/null +++ b/src/main/java/com/anthonyhilyard/iceberg/util/ConfigMenusForgeHelper.java @@ -0,0 +1,266 @@ +package com.anthonyhilyard.iceberg.util; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Field; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import com.anthonyhilyard.iceberg.Loader; +import com.electronwill.nightconfig.core.CommentedConfig; +import com.electronwill.nightconfig.core.Config; +import com.electronwill.nightconfig.core.UnmodifiableConfig; +import com.google.common.collect.Maps; +import com.google.common.base.Objects; +import com.google.common.collect.Iterators; + +import org.apache.commons.lang3.exception.ExceptionUtils; + +import fuzs.configmenusforge.client.gui.data.EntryData; +import fuzs.configmenusforge.client.gui.data.IEntryData; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.TextComponent; +import net.minecraftforge.common.ForgeConfigSpec; +import net.minecraftforge.fml.unsafe.UnsafeHacks; + +public class ConfigMenusForgeHelper +{ + private final static Map<Class<?>, List<MethodHandle>> configSpecMethodHandles = Maps.newHashMap(); + private final static Map<Class<?>, Boolean> cachedConfigSpecClasses = Maps.newHashMap(); + private final static MethodType getValuesMethodType = MethodType.methodType(UnmodifiableConfig.class); + private final static MethodType isLoadedMethodType = MethodType.methodType(boolean.class); + private final static MethodType saveMethodType = MethodType.methodType(void.class); + + private static Object callMethod(UnmodifiableConfig spec, int methodIndex) + { + Class<?> specClass = spec.getClass(); + if (!cachedConfigSpecClasses.containsKey(specClass)) + { + cacheClass(specClass); + } + + if (configSpecMethodHandles.containsKey(specClass)) + { + try + { + return configSpecMethodHandles.get(specClass).get(methodIndex).invoke(spec); + } + catch (Throwable e) + { + Loader.LOGGER.warn(ExceptionUtils.getStackTrace(e)); + } + } + return null; + } + + public static UnmodifiableConfig getValues(UnmodifiableConfig spec) + { + return (UnmodifiableConfig) callMethod(spec, 0); + } + + public static boolean isLoaded(UnmodifiableConfig spec) + { + return (Boolean) callMethod(spec, 1); + } + + public static void save(UnmodifiableConfig spec) + { + callMethod(spec, 2); + } + + public static Boolean cachedValidity(Class<?> specClass) + { + return cachedConfigSpecClasses.getOrDefault(specClass, null); + } + + public static void cacheClass(Class<?> specClass) + { + MethodHandle getValuesMethod = null; + MethodHandle isLoadedMethod = null; + MethodHandle saveMethod = null; + try + { + getValuesMethod = MethodHandles.lookup().findVirtual(specClass, "getValues", getValuesMethodType); + isLoadedMethod = MethodHandles.lookup().findVirtual(specClass, "isLoaded", isLoadedMethodType); + saveMethod = MethodHandles.lookup().findVirtual(specClass, "save", saveMethodType); + } + catch (Throwable e) + { + Loader.LOGGER.warn(ExceptionUtils.getStackTrace(e)); + } + + // If we found valid getValues, isLoaded and save methods, add them to the cache. + if (getValuesMethod != null && isLoadedMethod != null && saveMethod != null) + { + cachedConfigSpecClasses.put(specClass, true); + configSpecMethodHandles.put(specClass, List.of(getValuesMethod, isLoadedMethod, saveMethod)); + } + else + { + cachedConfigSpecClasses.put(specClass, false); + } + } + + /** + * Changed spec from a ForgeConfigSpec to an UnmodifiableConfig. + */ + public static void makeValueToDataMap(UnmodifiableConfig spec, UnmodifiableConfig values, CommentedConfig comments, Map<Object, IEntryData> allData, String basePath) + { + for (String path : values.valueMap().keySet()) + { + String currentPath = basePath.isEmpty() ? path : basePath + "." + path; + Object value = values.valueMap().get(path); + if (value instanceof UnmodifiableConfig category) + { + final EntryData.CategoryEntryData data = new EntryData.CategoryEntryData(path, category, comments.getComment(path)); + allData.put(category, data); + makeValueToDataMap(spec, category, (CommentedConfig) comments.valueMap().get(path), allData, currentPath); + } + else if (value instanceof ForgeConfigSpec.ConfigValue<?> configValue && configValue.get() instanceof UnmodifiableConfig category) + { + final EntryData.CategoryEntryData data = new DynamicCategoryEntryData(path, category, comments.getComment(path)); + allData.put(category, data); + makeValueToDataMap(spec, category, (CommentedConfig) comments.valueMap().get(path), allData, currentPath); + } + else if (value instanceof ForgeConfigSpec.ConfigValue<?> configValue) + { + final EntryData.ConfigEntryData<?> data = new EntryData.ConfigEntryData<>(path, configValue, spec.getRaw(configValue.getPath())); + allData.put(configValue, data); + } + // Allow non-configvalue values if the parent is a dynamic subconfig. + else if (!(value instanceof ForgeConfigSpec.ConfigValue<?>) && allData.containsKey(values) && allData.get(values) instanceof DynamicCategoryEntryData) + { + final EntryData.ConfigEntryData<?> data = new DynamicConfigEntryData<>(List.of(currentPath.split("\\.")), value, spec.getRaw(currentPath), spec); + allData.put(value, data); + } + } + } + + public static class DynamicCategoryEntryData extends EntryData.CategoryEntryData + { + public DynamicCategoryEntryData(String path, UnmodifiableConfig config, String comment) { + super(path, config, comment); + } + } + + public static class DynamicConfigEntryData<T> extends EntryData.ConfigEntryData<T> + { + private final ForgeConfigSpec.ValueSpec valueSpec; + private T currentValue; + private T configValue; + private final List<String> fullPath; + private final UnmodifiableConfig spec; + + private final static ForgeConfigSpec.ConfigValue<?> dummyConfigValue; + + private Component title; + + static + { + dummyConfigValue = UnsafeHacks.newInstance(ForgeConfigSpec.ConfigValue.class); + try + { + Field specField = ForgeConfigSpec.ConfigValue.class.getDeclaredField("spec"); + UnsafeHacks.setField(specField, dummyConfigValue, UnsafeHacks.newInstance(ForgeConfigSpec.class)); + Field defaultSupplierField = ForgeConfigSpec.ConfigValue.class.getDeclaredField("defaultSupplier"); + UnsafeHacks.setField(defaultSupplierField, dummyConfigValue, (Supplier<?>)(() -> null)); + } + catch (Exception e) { } + } + + @SuppressWarnings("unchecked") + public DynamicConfigEntryData(List<String> fullPath, T configValue, ForgeConfigSpec.ValueSpec valueSpec, UnmodifiableConfig spec) + { + super(fullPath.get(fullPath.size() - 1), (ForgeConfigSpec.ConfigValue<T>) dummyConfigValue, valueSpec); + this.configValue = configValue; + this.currentValue = configValue; + this.valueSpec = valueSpec; + this.fullPath = fullPath; + this.spec = spec; + + // We will override the normal title functionality since we want it to be unformatted. + this.title = new TextComponent(getPath()); + } + + @Override + public Component getTitle() + { + return this.title; + } + + @Override + public boolean mayResetValue() + { + return !listSafeEquals(currentValue, getDefaultValue()); + } + + @Override + public boolean mayDiscardChanges() + { + return listSafeEquals(configValue, currentValue); + } + + private static <T> boolean listSafeEquals(T o1, T o2) + { + // Attempts to solve an issue where types of lists won't match when one is read from file + // (due to enum being converted to string, long to int) + if (o1 instanceof List<?> list1 && o2 instanceof List<?> list2) + { + final Stream<String> stream1 = list1.stream().map(o -> o instanceof Enum<?> e ? e.name() : o.toString()); + final Stream<String> stream2 = list2.stream().map(o -> o instanceof Enum<?> e ? e.name() : o.toString()); + return Iterators.elementsEqual(stream1.iterator(), stream2.iterator()); + } + return Objects.equal(o1, o2); + } + + @Override + public void resetCurrentValue() + { + currentValue = getDefaultValue(); + } + + @Override + public void discardCurrentValue() + { + currentValue = configValue; + } + + @Override + public void saveConfigValue() + { + try + { + Field childConfigField = spec.getClass().getDeclaredField("childConfig"); + Config childConfig = UnsafeHacks.getField(childConfigField, spec); + childConfig.set(fullPath, currentValue); + } + catch (Exception e) { } + configValue = currentValue; + } + + @SuppressWarnings("unchecked") + public T getDefaultValue() + { + return (T) valueSpec.getDefault(); + } + + public T getCurrentValue() + { + return currentValue; + } + + public void setCurrentValue(T newValue) + { + currentValue = newValue; + } + + @Override + public List<String> getFullPath() + { + return fullPath; + } + } +}
\ No newline at end of file diff --git a/src/main/java/com/anthonyhilyard/iceberg/util/DynamicSubconfig.java b/src/main/java/com/anthonyhilyard/iceberg/util/DynamicSubconfig.java deleted file mode 100644 index 3ed17f3..0000000 --- a/src/main/java/com/anthonyhilyard/iceberg/util/DynamicSubconfig.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.anthonyhilyard.iceberg.util; - -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Supplier; - -import com.electronwill.nightconfig.core.AbstractCommentedConfig; -import com.electronwill.nightconfig.core.ConfigFormat; -import com.electronwill.nightconfig.core.UnmodifiableCommentedConfig; -import com.electronwill.nightconfig.core.UnmodifiableConfig; - -/** - * An exact copy of SimpleCommentedConfig, but this class is specifically meant for subconfigs. - * That being said--the class of a config is checked during config validation, and subconfigs are allowed - * extra leniency in config keys. - */ -final public class DynamicSubconfig extends AbstractCommentedConfig -{ - private final ConfigFormat<?> configFormat; - - /** - * Creates a Subconfig with the specified format. - * - * @param configFormat the config's format - */ - DynamicSubconfig(ConfigFormat<?> configFormat, boolean concurrent) - { - super(concurrent ? new ConcurrentHashMap<>() : new HashMap<>()); - this.configFormat = configFormat; - } - - /** - * Creates a Subconfig with the specified data and format. The map is used as it is and - * isn't copied. - */ - DynamicSubconfig(Map<String, Object> valueMap, ConfigFormat<?> configFormat) - { - super(valueMap); - this.configFormat = configFormat; - } - - /** - * Creates a Subconfig with the specified backing map supplier and format. - * - * @param mapCreator the supplier for backing maps - * @param configFormat the config's format - */ - DynamicSubconfig(Supplier<Map<String, Object>> mapCreator, ConfigFormat<?> configFormat) - { - super(mapCreator); - this.configFormat = configFormat; - } - - /** - * Creates a Subconfig by copying a config and with the specified format. - * - * @param toCopy the config to copy - * @param configFormat the config's format - */ - DynamicSubconfig(UnmodifiableConfig toCopy, ConfigFormat<?> configFormat, - boolean concurrent) - { - super(toCopy, concurrent); - this.configFormat = configFormat; - } - - /** - * Creates a Subconfig by copying a config, with the specified backing map creator and format. - * - * @param toCopy the config to copy - * @param mapCreator the supplier for backing maps - * @param configFormat the config's format - */ - public DynamicSubconfig(UnmodifiableConfig toCopy, Supplier<Map<String, Object>> mapCreator, - ConfigFormat<?> configFormat) - { - super(toCopy, mapCreator); - this.configFormat = configFormat; - } - - /** - * Creates a Subconfig by copying a config and with the specified format. - * - * @param toCopy the config to copy - * @param configFormat the config's format - */ - DynamicSubconfig(UnmodifiableCommentedConfig toCopy, ConfigFormat<?> configFormat, - boolean concurrent) - { - super(toCopy, concurrent); - this.configFormat = configFormat; - } - - /** - * Creates a Subconfig by copying a config, with the specified backing map creator and format. - * - * @param toCopy the config to copy - * @param mapCreator the supplier for backing maps - * @param configFormat the config's format - */ - public DynamicSubconfig(UnmodifiableCommentedConfig toCopy, Supplier<Map<String, Object>> mapCreator, - ConfigFormat<?> configFormat) - { - super(toCopy, mapCreator); - this.configFormat = configFormat; - } - - @Override - public ConfigFormat<?> configFormat() - { - return configFormat; - } - - @Override - public DynamicSubconfig createSubConfig() - { - return new DynamicSubconfig(mapCreator, configFormat); - } - - @Override - public AbstractCommentedConfig clone() - { - return new DynamicSubconfig(this, mapCreator, configFormat); - } - - /** - * Creates a new Subconfig with the content of the given config. The returned config will have - * the same format as the copied config. - * - * @param config the config to copy - * @return a copy of the config - */ - static DynamicSubconfig copy(UnmodifiableConfig config) - { - return new DynamicSubconfig(config, config.configFormat(), false); - } -}
\ No newline at end of file |