diff options
author | isxander <xander@isxander.dev> | 2024-04-11 18:43:06 +0100 |
---|---|---|
committer | isxander <xander@isxander.dev> | 2024-04-11 18:43:06 +0100 |
commit | 04fe933f4c24817100f3101f088accf55a621f8a (patch) | |
tree | feff94ca3ab4484160e69a24f4ee38522381950e /src/main/java/dev/isxander/yacl3/config/v2 | |
parent | 831b894fdb7fe3e173d81387c8f6a2402b8ccfa9 (diff) | |
download | YetAnotherConfigLib-04fe933f4c24817100f3101f088accf55a621f8a.tar.gz YetAnotherConfigLib-04fe933f4c24817100f3101f088accf55a621f8a.tar.bz2 YetAnotherConfigLib-04fe933f4c24817100f3101f088accf55a621f8a.zip |
Extremely fragile and broken multiversion build with stonecutter
Diffstat (limited to 'src/main/java/dev/isxander/yacl3/config/v2')
64 files changed, 2873 insertions, 0 deletions
diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigClassHandler.java b/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigClassHandler.java new file mode 100644 index 0000000..d94280f --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigClassHandler.java @@ -0,0 +1,107 @@ +package dev.isxander.yacl3.config.v2.api; + +import dev.isxander.yacl3.api.YetAnotherConfigLib; +import dev.isxander.yacl3.config.v2.impl.ConfigClassHandlerImpl; +import net.minecraft.resources.ResourceLocation; + +import java.util.function.Function; + +/** + * Represents a handled config class. + * + * @param <T> the backing config class to be managed + */ +public interface ConfigClassHandler<T> { + /** + * Gets the working instance of the config class. + * This should be used to get and set fields like usual. + */ + T instance(); + + /** + * Gets a second instance of the config class that + * should be used to get default values only. No fields + * should be modified in this instance. + */ + T defaults(); + + /** + * Gets the class of the config. + */ + Class<T> configClass(); + + /** + * Get all eligible fields in the config class. + * They could either be annotated with {@link dev.isxander.yacl3.config.v2.api.autogen.AutoGen} + * or {@link SerialEntry}, do not assume that a field has both of these. + */ + ConfigField<?>[] fields(); + + /** + * The unique identifier of this config handler. + */ + ResourceLocation id(); + + /** + * Auto-generates a GUI for this config class. + * This throws an exception if auto-gen is not supported. + */ + YetAnotherConfigLib generateGui(); + + /** + * Whether this config class supports auto-gen. + * If on a dedicated server, this returns false. + */ + boolean supportsAutoGen(); + + /** + * Safely loads the config class using the provided serializer. + * @return if the config was loaded successfully + */ + boolean load(); + + /** + * Safely saves the config class using the provided serializer. + */ + void save(); + + /** + * The serializer for this config class. + * Manages saving and loading of the config with fields + * annotated with {@link SerialEntry}. + * + * @deprecated use {@link #load()} and {@link #save()} instead. + */ + @Deprecated + ConfigSerializer<T> serializer(); + + /** + * Creates a builder for a config class. + * + * @param configClass the config class to build + * @param <T> the type of the config class + * @return the builder + */ + static <T> Builder<T> createBuilder(Class<T> configClass) { + return new ConfigClassHandlerImpl.BuilderImpl<>(configClass); + } + + interface Builder<T> { + /** + * The unique identifier of this config handler. + * The namespace should be your modid. + * + * @return this builder + */ + Builder<T> id(ResourceLocation id); + + /** + * The function to create the serializer for this config class. + * + * @return this builder + */ + Builder<T> serializer(Function<ConfigClassHandler<T>, ConfigSerializer<T>> serializerFactory); + + ConfigClassHandler<T> build(); + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigField.java b/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigField.java new file mode 100644 index 0000000..181a4d4 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigField.java @@ -0,0 +1,40 @@ +package dev.isxander.yacl3.config.v2.api; + +import dev.isxander.yacl3.config.v2.api.autogen.AutoGenField; + +import java.util.Optional; + +/** + * Represents a field in a config class. + * This is used to get all metadata on a field, + * and access the field and its default value. + * + * @param <T> the field's type + */ +public interface ConfigField<T> { + /** + * Gets the accessor for the field on the main instance. + * (Accessed through {@link ConfigClassHandler#instance()}) + */ + FieldAccess<T> access(); + + /** + * Gets the accessor for the field on the default instance. + */ + ReadOnlyFieldAccess<T> defaultAccess(); + + /** + * @return the parent config class handler that manages this field. + */ + ConfigClassHandler<?> parent(); + + /** + * The serial entry metadata for this field, if it exists. + */ + Optional<SerialField> serial(); + + /** + * The auto-gen metadata for this field, if it exists. + */ + Optional<AutoGenField> autoGen(); +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigSerializer.java b/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigSerializer.java new file mode 100644 index 0000000..4ac988c --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/ConfigSerializer.java @@ -0,0 +1,64 @@ +package dev.isxander.yacl3.config.v2.api; + +import java.util.Map; + +/** + * The base class for config serializers, + * offering a method to save and load. + * @param <T> the config class to be (de)serialized + */ +public abstract class ConfigSerializer<T> { + protected final ConfigClassHandler<T> config; + + public ConfigSerializer(ConfigClassHandler<T> config) { + this.config = config; + } + + /** + * Saves all fields in the config class. + * This can be done any way as it's abstract, but most + * commonly it is saved to a file. + */ + public abstract void save(); + + /** + * Loads all fields into the config class. + * @param bufferAccessMap a map of the field accesses. instead of directly setting the field with + * {@link ConfigField#access()}, use this parameter. This loads into a temporary object, + * and the class handler handles pushing these changes to the instance. + * @return the result of the load + */ + public LoadResult loadSafely(Map<ConfigField<?>, FieldAccess<?>> bufferAccessMap) { + this.load(); + return LoadResult.NO_CHANGE; + } + + /** + * Loads all fields in the config class. + * + * @deprecated use {@link #loadSafely(Map)} instead. + */ + @Deprecated + public void load() { + throw new IllegalArgumentException("load() is deprecated, use loadSafely() instead."); + } + + public enum LoadResult { + /** + * Indicates that the config was loaded successfully and the temporary object should be applied. + */ + SUCCESS, + /** + * Indicates that the config was not loaded successfully and the load should be abandoned. + */ + FAILURE, + /** + * Indicates that the config has not changed after a load and the temporary object should be ignored. + */ + NO_CHANGE, + /** + * Indicates the config was loaded successfully, but the config should be re-saved straight away. + */ + DIRTY + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/FieldAccess.java b/src/main/java/dev/isxander/yacl3/config/v2/api/FieldAccess.java new file mode 100644 index 0000000..ea30cd8 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/FieldAccess.java @@ -0,0 +1,14 @@ +package dev.isxander.yacl3.config.v2.api; + +/** + * A writable field instance access. + * + * @param <T> the type of the field + */ +public interface FieldAccess<T> extends ReadOnlyFieldAccess<T> { + /** + * Sets the value of the field. + * @param value the value to set + */ + void set(T value); +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/ReadOnlyFieldAccess.java b/src/main/java/dev/isxander/yacl3/config/v2/api/ReadOnlyFieldAccess.java new file mode 100644 index 0000000..566d60d --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/ReadOnlyFieldAccess.java @@ -0,0 +1,36 @@ +package dev.isxander.yacl3.config.v2.api; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.Optional; + +/** + * An abstract interface for accessing properties of an instance of a field. + * You do not need to worry about exceptions as the implementation + * will handle them. + * + * @param <T> the type of the field + */ +public interface ReadOnlyFieldAccess<T> { + /** + * @return the current value of the field. + */ + T get(); + + /** + * @return the name of the field. + */ + String name(); + + /** + * @return the type of the field. + */ + Type type(); + + /** + * @return the class of the field. + */ + Class<T> typeClass(); + + <A extends Annotation> Optional<A> getAnnotation(Class<A> annotationClass); +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/SerialEntry.java b/src/main/java/dev/isxander/yacl3/config/v2/api/SerialEntry.java new file mode 100644 index 0000000..94bf785 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/SerialEntry.java @@ -0,0 +1,39 @@ +package dev.isxander.yacl3.config.v2.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a field as serializable, so it can be used in a {@link ConfigSerializer}. + * Any field without this annotation will not be saved or loaded, but can still be turned + * into an auto-generated option. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface SerialEntry { + /** + * The serial name of the field. + * If empty, the serializer will decide the name. + */ + String value() default ""; + + /** + * The comment to add to the field. + * Some serializers may not support this. + * If empty, the serializer will not add a comment. + */ + String comment() default ""; + + /** + * Whether the field is required in the loaded config to be valid. + * If it's not, the config will be marked as dirty and re-saved with the default value. + */ + boolean required() default true; + + /** + * Whether the field can be null. + */ + boolean nullable() default false; +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/SerialField.java b/src/main/java/dev/isxander/yacl3/config/v2/api/SerialField.java new file mode 100644 index 0000000..cf6abfc --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/SerialField.java @@ -0,0 +1,16 @@ +package dev.isxander.yacl3.config.v2.api; + +import java.util.Optional; + +/** + * The backing interface for the {@link SerialEntry} annotation. + */ +public interface SerialField { + String serialName(); + + Optional<String> comment(); + + boolean required(); + + boolean nullable(); +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/AutoGen.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/AutoGen.java new file mode 100644 index 0000000..4187caf --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/AutoGen.java @@ -0,0 +1,32 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Any field that is annotated with this will generate a config option + * in the auto-generated config GUI. This should be paired with an + * {@link OptionFactory} annotation to define how to create the option. + * Some examples of this are {@link TickBox}, {@link FloatSlider}, {@link Label} or {@link StringField}. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface AutoGen { + /** + * Should be the id of the category. This is used to group options. + * The translation keys also use this. Category IDs can be set as a + * {@code private static final String} and used in the annotation to prevent + * repeating yourself. + */ + String category(); + + /** + * If left blank, the option will go in the root group, where it is + * listed at the top of the category with no group header. If set, + * this also appends to the translation key. Group IDs can be reused + * between multiple categories. + */ + String group() default ""; +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/AutoGenField.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/AutoGenField.java new file mode 100644 index 0000000..7f751fb --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/AutoGenField.java @@ -0,0 +1,12 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import java.util.Optional; + +/** + * Backing interface for the {@link AutoGen} annotation. + */ +public interface AutoGenField { + String category(); + + Optional<String> group(); +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Boolean.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Boolean.java new file mode 100644 index 0000000..5598389 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Boolean.java @@ -0,0 +1,41 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An option factory. + * <p> + * This creates a regular option with a + * {@link dev.isxander.yacl3.api.controller.BooleanControllerBuilder} controller. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Boolean { + enum Formatter { + YES_NO, + TRUE_FALSE, + ON_OFF, + /** + * Uses the translation keys: + * <ul> + * <li>true: {@code yacl3.config.$configId.$fieldName.fmt.true}</li> + * <li>false: {@code yacl3.config.$configId.$fieldName.fmt.false}</li> + * </ul> + */ + CUSTOM, + } + + /** + * The format used to display the boolean. + */ + Formatter formatter() default Formatter.TRUE_FALSE; + + /** + * Whether to color the formatted text green and red + * depending on the value: true or false respectively. + */ + boolean colored() default false; +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/ColorField.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/ColorField.java new file mode 100644 index 0000000..74937b4 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/ColorField.java @@ -0,0 +1,21 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An option factory. + * <p> + * This creates a regular option with a + * {@link dev.isxander.yacl3.api.controller.ColorControllerBuilder} controller. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface ColorField { + /** + * Whether to show/allow the alpha channel in the color field. + */ + boolean allowAlpha() default false; +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/CustomDescription.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/CustomDescription.java new file mode 100644 index 0000000..08624b4 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/CustomDescription.java @@ -0,0 +1,12 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface CustomDescription { + String[] value() default ""; +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/CustomFormat.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/CustomFormat.java new file mode 100644 index 0000000..15f6336 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/CustomFormat.java @@ -0,0 +1,17 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import dev.isxander.yacl3.api.controller.ValueFormatter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Allows you to specify a custom {@link ValueFormatter} for a field. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface CustomFormat { + Class<? extends ValueFormatter<?>> value(); +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/CustomImage.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/CustomImage.java new file mode 100644 index 0000000..d193f42 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/CustomImage.java @@ -0,0 +1,69 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.impl.autogen.EmptyCustomImageFactory; +import dev.isxander.yacl3.gui.image.ImageRenderer; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +/** + * Defines a custom image for an option. + * Without this annotation, the option factory will look + * for the resource {@code modid:textures/yacl3/$config_id_path/$fieldName.webp}. + * WEBP was chosen as the default format because file sizes are greatly reduced, + * which is important to keep your JAR size down, if you're so bothered. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface CustomImage { + /** + * The resource path to the image, a {@link net.minecraft.resources.ResourceLocation} + * is constructed with the namespace being the modid of the config, and the path being + * this value. + * <p> + * The following file formats are supported: + * <ul> + * <li>{@code .png}</li> + * <li>{@code .webp}</li> + * <li>{@code .jpg}, {@code .jpeg}</li> + * <li>{@code .gif} - <strong>HIGHLY DISCOURAGED DUE TO LARGE FILE SIZE</strong></li> + * </ul> + * <p> + * If left blank, then {@link CustomImage#factory()} is used. + */ + String value() default ""; + + /** + * The width of the image, in pixels. + * <strong>This is only required when using a PNG with {@link CustomImage#value()}</strong> + */ + int width() default 0; + + /** + * The width of the image, in pixels. + * <strong>This is only required when using a PNG with {@link CustomImage#value()}</strong> + */ + int height() default 0; + + /** + * The factory to create the image with. + * For the average user, this should not be used as it breaks out of the + * API-safe environment where things could change at any time, but required + * when creating anything advanced with the {@link ImageRenderer}. + * <p> + * The factory should contain a public, no-args constructor that will be + * invoked via reflection. + * + * @return the class of the factory + */ + Class<? extends CustomImageFactory<?>> factory() default EmptyCustomImageFactory.class; + + interface CustomImageFactory<T> { + CompletableFuture<ImageRenderer> createImage(T value, ConfigField<T> field, OptionAccess access); + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/CustomName.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/CustomName.java new file mode 100644 index 0000000..aa235bb --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/CustomName.java @@ -0,0 +1,18 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Overrides the name of an auto-generated option. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface CustomName { + /** + * The translation key to use for the option's name. + */ + String value() default ""; +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/DoubleField.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/DoubleField.java new file mode 100644 index 0000000..963cefd --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/DoubleField.java @@ -0,0 +1,46 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A regular option factory. + * <p> + * This creates a regular option with a + * {@link dev.isxander.yacl3.api.controller.DoubleFieldControllerBuilder} controller. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface DoubleField { + /** + * The minimum value of the field. If a user enters a value less + * than this, it will be clamped to this value. + * <p> + * If this is set to {@code -Double.MAX_VALUE}, there will be no minimum. + * <p> + * If the current value is at this minimum, if available, + * the translation key {@code yacl3.config.$configId.$fieldName.fmt.min} + * will be used. + */ + double min() default -Double.MAX_VALUE; + + /** + * The maximum value of the field. If a user enters a value more + * than this, it will be clamped to this value. + * <p> + * If this is set to {@code Double.MAX_VALUE}, there will be no minimum. + * <p> + * If the current value is at this maximum, if available, + * the translation key {@code yacl3.config.$configId.$fieldName.fmt.max} + * will be used. + */ + double max() default Double.MAX_VALUE; + + /** + * The format used to display the double. + * This is the syntax used in {@link String#format(String, Object...)}. + */ + String format() default "%.2f"; +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/DoubleSlider.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/DoubleSlider.java new file mode 100644 index 0000000..268f6a4 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/DoubleSlider.java @@ -0,0 +1,48 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A regular option factory. + * <p> + * This creates a regular option with a + * {@link dev.isxander.yacl3.api.controller.DoubleSliderControllerBuilder} controller. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface DoubleSlider { + /** + * The minimum value of the slider. + * <p> + * If the current value is at this minimum, if available, + * the translation key {@code yacl3.config.$configId.$fieldName.fmt.min} + * will be used. + */ + double min(); + + /** + * The maximum value of the slider. + * <p> + * If the current value is at this maximum, if available, + * the translation key {@code yacl3.config.$configId.$fieldName.fmt.max} + * will be used. + */ + double max(); + + /** + * The step size of this slider. + * For example, if this is set to 0.1, the slider will + * increment/decrement by 0.1 when dragging, no less, no more and + * will always be a multiple of 0.1. + */ + double step(); + + /** + * The format used to display the double. + * This is the syntax used in {@link String#format(String, Object...)}. + */ + String format() default "%.2f"; +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Dropdown.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Dropdown.java new file mode 100644 index 0000000..44239d5 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Dropdown.java @@ -0,0 +1,43 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An option factory. + * <p> + * This creates a regular option with a + * {@link dev.isxander.yacl3.api.controller.DropdownStringControllerBuilder} controller. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Dropdown { + /** + * The allowed values for the field. These will be shown in a dropdown + * that the user can filter and select from. + * <p> + * Only values in this list will be accepted and written to the config + * file, unless {@link #allow()} is set to ${@code ALLOW_ANY}. + * <p> + * Empty string is a valid value only if it appears in this list, or if + * {@link #allow()} is set to {@code ALLOW_EMPTY} or {@code ALLOW_ANY}. + */ + String[] values(); + + /** + * Whether to accept the empty string as a valid value if it does not + * already appear in {@link #values()}. If it already appears there, + * the value of this does not apply. + */ + boolean allowEmptyValue() default false; + + /** + * Whether to accept any string as a valid value. The list of strings + * supplied in {@link #values()} are only used as dropdown suggestions. + * Empty strings are still prohibited unless the empty string appears in + * {@link #values()} or {@link #allowEmptyValue()}. + */ + boolean allowAnyValue() default false; +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/EnumCycler.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/EnumCycler.java new file mode 100644 index 0000000..98d94f9 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/EnumCycler.java @@ -0,0 +1,35 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import dev.isxander.yacl3.api.NameableEnum; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An option factory. + * <p> + * This creates a regular option with a {@link dev.isxander.yacl3.api.controller.CyclingListControllerBuilder} + * controller. If the enum implements {@link CyclableEnum}, the allowed values will be used from that, + * rather than every single enum constant in the class. If not, {@link EnumCycler#allowedOrdinals()} is used. + * <p> + * There are two methods of formatting for enum values. First, if the enum implements + * {@link dev.isxander.yacl3.api.NameableEnum}, {@link NameableEnum#getDisplayName()} is used. + * Otherwise, the translation key {@code yacl3.config.enum.$enumClassName.$enumName} where + * {@code $enumClassName} is the exact name of the class and {@code $enumName} is equal to the lower + * case of {@link Enum#name()}. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface EnumCycler { + /** + * The allowed ordinals of the enum class. If empty, all ordinals are allowed. + * This is only used if the enum does not implement {@link CyclableEnum}. + */ + int[] allowedOrdinals() default {}; + + interface CyclableEnum<T extends Enum<T>> { + T[] allowedValues(); + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/FloatField.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/FloatField.java new file mode 100644 index 0000000..1e7e71e --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/FloatField.java @@ -0,0 +1,46 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A regular option factory. + * <p> + * This creates a regular option with a + * {@link dev.isxander.yacl3.api.controller.FloatFieldControllerBuilder} controller. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface FloatField { + /** + * The minimum value of the field. If a user enters a value less + * than this, it will be clamped to this value. + * <p> + * If this is set to {@code -Float.MAX_VALUE}, there will be no minimum. + * <p> + * If the current value is at this minimum, if available, + * the translation key {@code yacl3.config.$configId.$fieldName.fmt.min} + * will be used. + */ + float min() default -Float.MAX_VALUE; + + /** + * The maximum value of the field. If a user enters a value more + * than this, it will be clamped to this value. + * <p> + * If this is set to {@code Float.MAX_VALUE}, there will be no minimum. + * <p> + * If the current value is at this maximum, if available, + * the translation key {@code yacl3.config.$configId.$fieldName.fmt.max} + * will be used. + */ + float max() default Float.MAX_VALUE; + + /** + * The format used to display the float. + * This is the syntax used in {@link String#format(String, Object...)}. + */ + String format() default "%.1f"; +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/FloatSlider.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/FloatSlider.java new file mode 100644 index 0000000..19ae9db --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/FloatSlider.java @@ -0,0 +1,48 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A regular option factory. + * <p> + * This creates a regular option with a + * {@link dev.isxander.yacl3.api.controller.FloatSliderControllerBuilder} controller. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface FloatSlider { + /** + * The minimum value of the slider. + * <p> + * If the current value is at this minimum, if available, + * the translation key {@code yacl3.config.$configId.$fieldName.fmt.min} + * will be used. + */ + float min(); + + /** + * The maximum value of the slider. + * <p> + * If the current value is at this maximum, if available, + * the translation key {@code yacl3.config.$configId.$fieldName.fmt.max} + * will be used. + */ + float max(); + + /** + * The step size of this slider. + * For example, if this is set to 0.1, the slider will + * increment/decrement by 0.1 when dragging, no less, no more and + * will always be a multiple of 0.1. + */ + float step(); + + /** + * The format used to display the float. + * This is the syntax used in {@link String#format(String, Object...)}. + */ + String format() default "%.1f"; +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/FormatTranslation.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/FormatTranslation.java new file mode 100644 index 0000000..7cc4ded --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/FormatTranslation.java @@ -0,0 +1,25 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Allows you to specify a custom value formatter + * in the form of a translation key. + * <p> + * Without this annotation, the value will be formatted + * according to the option factory, implementation details + * for that should be found in the javadoc for the factory. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface FormatTranslation { + /** + * The translation key for the value formatter. + * One parameter is passed to this key: the option's value, + * using {@link Object#toString()}. + */ + String value() default ""; +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/IntField.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/IntField.java new file mode 100644 index 0000000..9945d01 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/IntField.java @@ -0,0 +1,41 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A regular option factory. + * <p> + * This creates a regular option with a + * {@link dev.isxander.yacl3.api.controller.IntegerFieldControllerBuilder} controller. + * <p> + * If available, the translation key {@code yacl3.config.$configId.$fieldName.fmt.$value} + * is used where {@code $value} is the current value of the option, for example, {@code 5}. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface IntField { + /** + * The minimum value of the field. If a user enters a value less + * than this, it will be clamped to this value. + * <p> + * If this is set to {@code Integer.MIN_VALUE}, there will be no minimum. + */ + int min() default Integer.MIN_VALUE; + + /** + * The minimum value of the field. If a user enters a value more + * than this, it will be clamped to this value. + * <p> + * If this is set to {@code Integer.MAX_VALUE}, there will be no minimum. + */ + int max() default Integer.MAX_VALUE; + + /** + * The format used to display the integer. + * This is the syntax used in {@link String#format(String, Object...)}. + */ + String format() default "%.0f"; +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/IntSlider.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/IntSlider.java new file mode 100644 index 0000000..7fd2282 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/IntSlider.java @@ -0,0 +1,35 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A regular option factory. + * <p> + * This creates a regular option with a + * {@link dev.isxander.yacl3.api.controller.IntegerSliderControllerBuilder} controller. + * <p> + * If available, the translation key {@code yacl3.config.$configId.$fieldName.fmt.$value} + * is used where {@code $value} is the current value of the option, for example, {@code 5}. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface IntSlider { + /** + * The minimum value of the slider. + */ + int min(); + + /** + * The maximum value of the slider. + */ + int max(); + + /** + * The format used to display the integer. + * This is the syntax used in {@link String#format(String, Object...)}. + */ + int step(); +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/ItemField.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/ItemField.java new file mode 100644 index 0000000..84d2c7a --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/ItemField.java @@ -0,0 +1,17 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An option factory. + * <p> + * This creates a regular option with a + * {@link dev.isxander.yacl3.api.controller.ItemControllerBuilder} controller. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface ItemField { +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Label.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Label.java new file mode 100644 index 0000000..41e026f --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Label.java @@ -0,0 +1,18 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An option factory that creates an instance + * of a {@link dev.isxander.yacl3.api.LabelOption}. + * <p> + * The backing field can be private and final and + * must be of type {@link net.minecraft.network.chat.Component}. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Label { +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/ListGroup.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/ListGroup.java new file mode 100644 index 0000000..c664f71 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/ListGroup.java @@ -0,0 +1,60 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.controller.ControllerBuilder; +import dev.isxander.yacl3.config.v2.api.ConfigField; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.List; + +/** + * An option factory. + * <p> + * This creates a List option with a custom controller. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface ListGroup { + /** + * The {@link Class} representing a class that implements {@link ValueFactory}. + * To create a new instance for the list when the user adds a new entry to the list. + * Remember this class can be shared with {@link ControllerFactory} as well. + */ + Class<? extends ValueFactory<?>> valueFactory(); + + /** + * The {@link Class} representing a class that implements {@link ControllerBuilder} + * to add a controller to every entry in the list. + * Remember this class can be shared with {@link ValueFactory} as well. + */ + Class<? extends ControllerFactory<?>> controllerFactory(); + + /** + * The maximum number of entries that can be added to the list. + * Once at this limit, the add button is disabled. + * If this is equal to {@code 0}, there is no limit. + */ + int maxEntries() default 0; + + /** + * The minimum number of entries that must be in the list. + * When at this limit, the remove button of the entries is disabled. + */ + int minEntries() default 0; + + /** + * Whether to add new entries at the bottom of the list rather than the top. + */ + boolean addEntriesToBottom() default false; + + interface ValueFactory<T> { + T provideNewValue(); + } + + interface ControllerFactory<T> { + ControllerBuilder<T> createController(ListGroup annotation, ConfigField<List<T>> field, OptionAccess storage, Option<T> option); + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/LongField.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/LongField.java new file mode 100644 index 0000000..01c3a7e --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/LongField.java @@ -0,0 +1,41 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A regular option factory. + * <p> + * This creates a regular option with a + * {@link dev.isxander.yacl3.api.controller.LongFieldControllerBuilder} controller. + * <p> + * If available, the translation key {@code yacl3.config.$configId.$fieldName.fmt.$value} + * is used where {@code $value} is the current value of the option, for example, {@code 5}. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface LongField { + /** + * The minimum value of the field. If a user enters a value less + * than this, it will be clamped to this value. + * <p> + * If this is set to {@code Long.MIN_VALUE}, there will be no minimum. + */ + long min() default Long.MIN_VALUE; + + /** + * The maximum value of the field. If a user enters a value more + * than this, it will be clamped to this value. + * <p> + * If this is set to {@code Long.MAX_VALUE}, there will be no minimum. + */ + long max() default Long.MAX_VALUE; + + /** + * The format used to display the long. + * This is the syntax used in {@link String#format(String, Object...)}. + */ + String format() default "%.0f"; +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/LongSlider.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/LongSlider.java new file mode 100644 index 0000000..5563bd0 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/LongSlider.java @@ -0,0 +1,35 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A regular option factory. + * <p> + * This creates a regular option with a + * {@link dev.isxander.yacl3.api.controller.LongSliderControllerBuilder} controller. + * <p> + * If available, the translation key {@code yacl3.config.$configId.$fieldName.fmt.$value} + * is used where {@code $value} is the current value of the option, for example, {@code 5}. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface LongSlider { + /** + * The minimum value of the slider. + */ + long min(); + + /** + * The maximum value of the slider. + */ + long max(); + + /** + * The format used to display the integer. + * This is the syntax used in {@link String#format(String, Object...)}. + */ + long step(); +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/MasterTickBox.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/MasterTickBox.java new file mode 100644 index 0000000..70dee1a --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/MasterTickBox.java @@ -0,0 +1,26 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An option factory like {@link TickBox} but controls + * other options' availability based on its state. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface MasterTickBox { + /** + * The exact names of the fields with {@link AutoGen} annotation + * to control the availability of. + */ + String[] value(); + + /** + * Whether having the tickbox disabled should enable the options + * rather than disable. + */ + boolean invert() default false; +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/OptionAccess.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/OptionAccess.java new file mode 100644 index 0000000..c55afe4 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/OptionAccess.java @@ -0,0 +1,35 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import dev.isxander.yacl3.api.Option; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; + +/** + * An accessor to all options that are auto-generated + * by the config system. + */ +public interface OptionAccess { + /** + * Gets an option by its field name. + * This could be null if the option hasn't been created yet. It is created + * in order of the fields in the class, so if you are trying to get an option + * lower-down in the class, this will return null. + * + * @param fieldName the exact, case-sensitive name of the field. + * @return the created option, or {@code null} if it hasn't been created yet. + */ + @Nullable Option<?> getOption(String fieldName); + + /** + * Schedules an operation to be performed on an option. + * If the option has already been created, the consumer will be + * accepted immediately upon calling this method, if not, it will + * be added to the queue of operations to be performed on the option + * once it does get created. + * + * @param fieldName the exact, case-sensitive name of the field. + * @param optionConsumer the operation to perform on the option. + */ + void scheduleOptionOperation(String fieldName, Consumer<Option<?>> optionConsumer); +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/OptionFactory.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/OptionFactory.java new file mode 100644 index 0000000..515a40b --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/OptionFactory.java @@ -0,0 +1,40 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.impl.autogen.OptionFactoryRegistry; + +import java.lang.annotation.Annotation; + +/** + * The backing builder for option factories' annotations. + * <p> + * If you want to make a basic option with a controller, it's recommended + * to use {@link SimpleOptionFactory} instead which is a subclass of this. + * + * @param <A> the annotation type + * @param <T> the option's binding type + */ +public interface OptionFactory<A extends Annotation, T> { + /** + * Creates an option from the given annotation, backing field, and storage. + * + * @param annotation the annotation that fields are annotated with to use this factory + * @param field the backing field + * @param optionAccess the option access to access other options in the GUI + * @return the built option to be added to the group/category + */ + Option<T> createOption(A annotation, ConfigField<T> field, OptionAccess optionAccess); + + /** + * Registers an option factory to be used by configs. + * + * @param annotationClass the class of the annotation to use a factory + * @param factory an instance of the factory + * @param <A> the type of the annotation + * @param <T> the type of the option's binding + */ + static <A extends Annotation, T> void register(Class<A> annotationClass, OptionFactory<A, T> factory) { + OptionFactoryRegistry.registerOptionFactory(annotationClass, factory); + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/SimpleOptionFactory.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/SimpleOptionFactory.java new file mode 100644 index 0000000..f7d807f --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/SimpleOptionFactory.java @@ -0,0 +1,138 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.OptionDescription; +import dev.isxander.yacl3.api.OptionFlag; +import dev.isxander.yacl3.api.controller.ControllerBuilder; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.impl.FieldBackedBinding; +import dev.isxander.yacl3.config.v2.impl.autogen.AutoGenUtils; +import dev.isxander.yacl3.config.v2.impl.autogen.EmptyCustomImageFactory; +import dev.isxander.yacl3.config.v2.impl.autogen.YACLAutoGenException; +import net.minecraft.client.Minecraft; +import net.minecraft.locale.Language; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; + +import java.lang.annotation.Annotation; +import java.util.Optional; +import java.util.Set; + +public abstract class SimpleOptionFactory<A extends Annotation, T> implements OptionFactory<A, T> { + @Override + public Option<T> createOption(A annotation, ConfigField<T> field, OptionAccess optionAccess) { + Option<T> option = Option.<T>createBuilder() + .name(this.name(annotation, field, optionAccess)) + .description(v -> this.description(v, annotation, field, optionAccess).build()) + .binding(new FieldBackedBinding<>(field.access(), field.defaultAccess())) + .controller(opt -> { + ControllerBuilder<T> builder = this.createController(annotation, field, optionAccess, opt); + + AutoGenUtils.addCustomFormatterToController(builder, field.access()); + + return builder; + }) + .available(this.available(annotation, field, optionAccess)) + .flags(this.flags(annotation, field, optionAccess)) + .listener((opt, v) -> this.listener(annotation, field, optionAccess, opt, v)) + .build(); + + postInit(annotation, field, optionAccess, option); + return option; + } + + protected abstract ControllerBuilder<T> createController(A annotation, ConfigField<T> field, OptionAccess storage, Option<T> option); + + protected MutableComponent name(A annotation, ConfigField<T> field, OptionAccess storage) { + Optional<CustomName> customName = field.access().getAnnotation(CustomName.class); + return Component.translatable(customName.map(CustomName::value).orElse(this.getTranslationKey(field, null))); + } + + protected OptionDescription.Builder description(T value, A annotation, ConfigField<T> field, OptionAccess storage) { + OptionDescription.Builder builder = OptionDescription.createBuilder(); + + String key = this.getTranslationKey(field, "desc"); + if (Language.getInstance().has(key)) { + builder.text(Component.translatable(key)); + } else { + key += "."; + int i = 1; + while (Language.getInstance().has(key + i)) { + builder.text(Component.translatable(key + i)); + i++; + } + } + + field.access().getAnnotation(CustomDescription.class).ifPresent(customDescription -> { + for (String line : customDescription.value()) { + builder.text(Component.translatable(line)); + } + }); + + Optional<CustomImage> imageOverrideOpt = field.access().getAnnotation(CustomImage.class); + if (imageOverrideOpt.isPresent()) { + CustomImage imageOverride = imageOverrideOpt.get(); + + if (!imageOverride.factory().equals(EmptyCustomImageFactory.class)) { + CustomImage.CustomImageFactory<T> imageFactory; + try { + imageFactory = (CustomImage.CustomImageFactory<T>) AutoGenUtils.constructNoArgsClass( + imageOverride.factory(), + () -> "'%s': The factory class on @OverrideImage has no no-args constructor.".formatted(field.access().name()), + () -> "'%s': Failed to instantiate factory class %s.".formatted(field.access().name(), imageOverride.factory().getName()) + ); + } catch (ClassCastException e) { + throw new YACLAutoGenException("'%s': The factory class on @OverrideImage is of incorrect type. Expected %s, got %s.".formatted(field.access().name(), field.access().type().getTypeName(), imageOverride.factory().getTypeParameters()[0].getName())); + } + + builder.customImage(imageFactory.createImage(value, field, storage).thenApply(Optional::of)); + } else if (!imageOverride.value().isEmpty()) { + String path = imageOverride.value(); + ResourceLocation imageLocation = new ResourceLocation(field.parent().id().getNamespace(), path); + String extension = path.substring(path.lastIndexOf('.') + 1); + + switch (extension) { + case "png", "jpg", "jpeg" -> builder.image(imageLocation, imageOverride.width(), imageOverride.height()); + case "webp" -> builder.webpImage(imageLocation); + case "gif" -> builder.gifImage(imageLocation); + default -> throw new YACLAutoGenException("'%s': Invalid image extension '%s' on @OverrideImage. Expected: ('png','jpg','webp','gif')".formatted(field.access().name(), extension)); + } + } else { + throw new YACLAutoGenException("'%s': @OverrideImage has no value or factory class.".formatted(field.access().name())); + } + } else { + String imagePath = "textures/yacl3/" + field.parent().id().getPath() + "/" + field.access().name() + ".webp"; + imagePath = imagePath.toLowerCase().replaceAll("[^a-z0-9/._:-]", "_"); + ResourceLocation imageLocation = new ResourceLocation(field.parent().id().getNamespace(), imagePath); + if (Minecraft.getInstance().getResourceManager().getResource(imageLocation).isPresent()) { + builder.webpImage(imageLocation); + } + } + + return builder; + } + + protected boolean available(A annotation, ConfigField<T> field, OptionAccess storage) { + return true; + } + + protected Set<OptionFlag> flags(A annotation, ConfigField<T> field, OptionAccess storage) { + return Set.of(); + } + + protected void listener(A annotation, ConfigField<T> field, OptionAccess storage, Option<T> option, T value) { + + } + + protected void postInit(A annotation, ConfigField<T> field, OptionAccess storage, Option<T> option) { + + } + + protected String getTranslationKey(ConfigField<T> field, @Nullable String suffix) { + String key = "yacl3.config.%s.%s".formatted(field.parent().id().toString(), field.access().name()); + if (suffix != null) key += "." + suffix; + return key; + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/StringField.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/StringField.java new file mode 100644 index 0000000..50d638e --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/StringField.java @@ -0,0 +1,17 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A regular option factory. + * <p> + * This creates a regular option with a + * {@link dev.isxander.yacl3.api.controller.StringControllerBuilder} controller. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface StringField { +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/TickBox.java b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/TickBox.java new file mode 100644 index 0000000..0a88c14 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/TickBox.java @@ -0,0 +1,17 @@ +package dev.isxander.yacl3.config.v2.api.autogen; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An option factory. + * <p> + * This creates a regular option with a + * {@link dev.isxander.yacl3.api.controller.TickBoxControllerBuilder} controller. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface TickBox { +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/api/serializer/GsonConfigSerializerBuilder.java b/src/main/java/dev/isxander/yacl3/config/v2/api/serializer/GsonConfigSerializerBuilder.java new file mode 100644 index 0000000..33003d7 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/api/serializer/GsonConfigSerializerBuilder.java @@ -0,0 +1,98 @@ +package dev.isxander.yacl3.config.v2.api.serializer; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import dev.isxander.yacl3.config.ConfigEntry; +import dev.isxander.yacl3.config.v2.api.ConfigClassHandler; +import dev.isxander.yacl3.config.v2.api.ConfigSerializer; +import dev.isxander.yacl3.config.v2.api.SerialEntry; +import dev.isxander.yacl3.config.v2.impl.serializer.GsonConfigSerializer; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; + +import java.awt.*; +import java.nio.file.Path; +import java.util.function.UnaryOperator; + +/** + * Uses GSON to serialize and deserialize config data from JSON to a file. + * <p> + * Only fields annotated with {@link dev.isxander.yacl3.config.v2.api.SerialEntry} are included in the JSON. + * {@link Component}, {@link Style} and {@link Color} have default type adapters, so there is no need to provide them in your GSON instance. + * GSON is automatically configured to format fields as {@code lower_camel_case}. + * <p> + * Optionally, this can also be written under JSON5 spec, allowing comments. + * + * @param <T> config data type + */ +public interface GsonConfigSerializerBuilder<T> { + static <T> GsonConfigSerializerBuilder<T> create(ConfigClassHandler<T> config) { + return new GsonConfigSerializer.Builder<>(config); + } + + /** + * Sets the file path to save and load the config from. + */ + GsonConfigSerializerBuilder<T> setPath(Path path); + + /** + * Sets the GSON instance to use. Overrides all YACL defaults such as: + * <ul> + * <li>lower_camel_case field naming policy</li> + * <li>null serialization</li> + * <li>{@link Component}, {@link Style} and {@link Color} type adapters</li> + * </ul> + * Still respects the exclusion strategy to only serialize {@link ConfigEntry} + * but these can be added to with setExclusionStrategies. + * + * @param gsonBuilder gson builder to use + */ + GsonConfigSerializerBuilder<T> overrideGsonBuilder(GsonBuilder gsonBuilder); + + /** + * Sets the GSON instance to use. Overrides all YACL defaults such as: + * <ul> + * <li>lower_camel_case field naming policy</li> + * <li>null serialization</li> + * <li>{@link Component}, {@link Style} and {@link Color} type adapters</li> + * </ul> + * but these can be added to with setExclusionStrategies. + * + * @param gson gson instance to be converted to a builder + */ + GsonConfigSerializerBuilder<T> overrideGsonBuilder(Gson gson); + + /** + * Appends extra configuration to a GSON builder. + * This is the intended way to add functionality to the GSON instance. + * <p> + * By default, YACL sets the GSON with the following options: + * <ul> + * <li>lower_camel_case field naming policy</li> + * <li>null serialization</li> + * <li>{@link Component}, {@link Style} and {@link Color} type adapters</li> + * </ul> + * For example, if you wanted to revert YACL's lower_camel_case naming policy, + * you could do the following: + * <pre> + * {@code + * GsonConfigSerializerBuilder.create(config) + * .appendGsonBuilder(builder -> builder.setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)) + * } + * </pre> + * + * @param gsonBuilder the function to apply to the builder + */ + GsonConfigSerializerBuilder<T> appendGsonBuilder(UnaryOperator<GsonBuilder> gsonBuilder); + + /** + * Writes the json under JSON5 spec, allowing the use of {@link SerialEntry#comment()}. + * If enabling this option it's recommended to use the file extension {@code .json5}. + * + * @param json5 whether to write under JSON5 spec + * @return this builder + */ + GsonConfigSerializerBuilder<T> setJson5(boolean json5); + + ConfigSerializer<T> build(); +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigClassHandlerImpl.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigClassHandlerImpl.java new file mode 100644 index 0000000..813b3ab --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigClassHandlerImpl.java @@ -0,0 +1,274 @@ +package dev.isxander.yacl3.config.v2.impl; + +import dev.isxander.yacl3.api.*; +import dev.isxander.yacl3.config.ConfigEntry; +import dev.isxander.yacl3.config.v2.api.*; +import dev.isxander.yacl3.config.v2.api.autogen.AutoGen; +import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess; +import dev.isxander.yacl3.config.v2.impl.autogen.OptionFactoryRegistry; +import dev.isxander.yacl3.config.v2.impl.autogen.OptionAccessImpl; +import dev.isxander.yacl3.config.v2.impl.autogen.YACLAutoGenException; +import dev.isxander.yacl3.impl.utils.YACLConstants; +import dev.isxander.yacl3.platform.YACLPlatform; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import org.apache.commons.lang3.Validate; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class ConfigClassHandlerImpl<T> implements ConfigClassHandler<T> { + private final Class<T> configClass; + private final ResourceLocation id; + private final boolean supportsAutoGen; + private final ConfigSerializer<T> serializer; + private final ConfigFieldImpl<?>[] fields; + + private T instance; + private final T defaults; + private final Constructor<T> noArgsConstructor; + + public ConfigClassHandlerImpl(Class<T> configClass, ResourceLocation id, Function<ConfigClassHandler<T>, ConfigSerializer<T>> serializerFactory) { + this.configClass = configClass; + this.id = id; + this.supportsAutoGen = id != null && YACLPlatform.getEnvironment().isClient(); + + try { + noArgsConstructor = configClass.getDeclaredConstructor(); + } catch (NoSuchMethodException e) { + throw new YACLAutoGenException("Failed to find no-args constructor for config class %s.".formatted(configClass.getName()), e); + } + this.instance = createNewObject(); + this.defaults = createNewObject(); + + detectOldAnnotation(configClass.getDeclaredFields()); + + this.fields = discoverFields(); + this.serializer = serializerFactory.apply(this); + } + + private ConfigFieldImpl<?>[] discoverFields() { + return Arrays.stream(configClass.getDeclaredFields()) + .peek(field -> field.setAccessible(true)) + .filter(field -> field.isAnnotationPresent(SerialEntry.class) || field.isAnnotationPresent(AutoGen.class)) + .map(field -> new ConfigFieldImpl<>( + new ReflectionFieldAccess<>(field, instance), + new ReflectionFieldAccess<>(field, defaults), + this, + field.getAnnotation(SerialEntry.class), + field.getAnnotation(AutoGen.class) + )) + .toArray(ConfigFieldImpl[]::new); + } + + @Override + public T instance() { + return this.instance; + } + + @Override + public T defaults() { + return this.defaults; + } + + @Override + public Class<T> configClass() { + return this.configClass; + } + + @Override + public ConfigFieldImpl<?>[] fields() { + return this.fields; + } + + @Override + public ResourceLocation id() { + return this.id; + } + + @Override + public boolean supportsAutoGen() { + return this.supportsAutoGen; + } + + @Override + public YetAnotherConfigLib generateGui() { + if (!supportsAutoGen()) { + throw new YACLAutoGenException("Auto GUI generation is not supported for this config class. You either need to enable it in the builder or you are attempting to create a GUI in a dedicated server environment."); + } + + boolean hasAutoGenFields = Arrays.stream(fields()).anyMatch(field -> field.autoGen().isPresent()); + + if (!hasAutoGenFields) { + throw new YACLAutoGenException("No fields in this config class are annotated with @AutoGen. You must annotate at least one field with @AutoGen to generate a GUI."); + } + + OptionAccessImpl storage = new OptionAccessImpl(); + Map<String, CategoryAndGroups> categories = new LinkedHashMap<>(); + for (ConfigField<?> configField : fields()) { + configField.autoGen().ifPresent(autoGen -> { + CategoryAndGroups groups = categories.computeIfAbsent( + autoGen.category(), + k -> new CategoryAndGroups( + ConfigCategory.createBuilder() + .name(Component.translatable("yacl3.config.%s.category.%s".formatted(id().toString(), k))), + new LinkedHashMap<>() + ) + ); + OptionAddable group = groups.groups().computeIfAbsent(autoGen.group().orElse(""), k -> { + if (k.isEmpty()) + return groups.category(); + return OptionGroup.createBuilder() + .name(Component.translatable("yacl3.config.%s.category.%s.group.%s".formatted(id().toString(), autoGen.category(), k))); + }); + + Option<?> option; + try { + option = createOption(configField, storage); + } catch (Exception e) { + throw new YACLAutoGenException("Failed to create option for field '%s'".formatted(configField.access().name()), e); + } + + storage.putOption(configField.access().name(), option); + group.option(option); + }); + } + storage.checkBadOperations(); + categories.values().forEach(CategoryAndGroups::finaliseGroups); + + YetAnotherConfigLib.Builder yaclBuilder = YetAnotherConfigLib.createBuilder() + .save(this.serializer()::save) + .title(Component.translatable("yacl3.config.%s.title".formatted(this.id().toString()))); + categories.values().forEach(category -> yaclBuilder.category(category.category().build())); + + return yaclBuilder.build(); + } + + private <U> Option<U> createOption(ConfigField<U> configField, OptionAccess storage) { + return OptionFactoryRegistry.createOption(((ReflectionFieldAccess<?>) configField.access()).field(), configField, storage) + .orElseThrow(() -> new YACLAutoGenException("Failed to create option for field %s".formatted(configField.access().name()))); + } + + @Override + public ConfigSerializer<T> serializer() { + return this.serializer; + } + + @Override + public boolean load() { + // create a new instance to load into + T newInstance = createNewObject(); + + // create field accesses for the new object + Map<ConfigFieldImpl<?>, ReflectionFieldAccess<?>> accessBufferImpl = Arrays.stream(fields()) + .map(field -> new AbstractMap.SimpleImmutableEntry<>( + field, + new ReflectionFieldAccess<>(field.access().field(), newInstance) + )) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + // convert the map into API safe field accesses + Map<ConfigField<?>, FieldAccess<?>> accessBuffer = accessBufferImpl.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + // attempt to load the config + ConfigSerializer.LoadResult loadResult = ConfigSerializer.LoadResult.FAILURE; + Throwable error = null; + try { + loadResult = this.serializer().loadSafely(accessBuffer); + } catch (Throwable e) { + // handle any errors later in the loadResult switch case + error = e; + } + + switch (loadResult) { + case DIRTY: + case SUCCESS: + // replace the instance with the newly created one + this.instance = newInstance; + for (ConfigFieldImpl<?> field : fields()) { + // update the field accesses to point to the correct object + ((ConfigFieldImpl<Object>) field).setFieldAccess((ReflectionFieldAccess<Object>) accessBufferImpl.get(field)); + } + + if (loadResult == ConfigSerializer.LoadResult.DIRTY) { + // if the load result is dirty, we need to save the config again + this.save(); + } + case NO_CHANGE: + return true; + case FAILURE: + YACLConstants.LOGGER.error( + "Unsuccessful load of config class '{}'. The load will be abandoned and config remains unchanged.", + configClass.getSimpleName(), error + ); + } + + return false; + } + + @Override + public void save() { + serializer().save(); + } + + private T createNewObject() { + try { + return noArgsConstructor.newInstance(); + } catch (Exception e) { + throw new YACLAutoGenException("Failed to create instance of config class '%s' with no-args constructor.".formatted(configClass.getName()), e); + } + } + + private void detectOldAnnotation(Field[] fields) { + boolean hasOldConfigEntry = Arrays.stream(fields) + .anyMatch(field -> field.isAnnotationPresent(ConfigEntry.class)); + + Validate.isTrue(!hasOldConfigEntry, "At least one field in %s is still annotated with the deprecated @ConfigEntry annotation. This is incorrect. Use @SerialEntry.".formatted(configClass.getName())); + } + + public static class BuilderImpl<T> implements Builder<T> { + private final Class<T> configClass; + private ResourceLocation id; + private Function<ConfigClassHandler<T>, ConfigSerializer<T>> serializerFactory; + + public BuilderImpl(Class<T> configClass) { + this.configClass = configClass; + } + + @Override + public Builder<T> id(ResourceLocation id) { + this.id = id; + return this; + } + + @Override + public Builder<T> serializer(Function<ConfigClassHandler<T>, ConfigSerializer<T>> serializerFactory) { + this.serializerFactory = serializerFactory; + return this; + } + + @Override + public ConfigClassHandler<T> build() { + Validate.notNull(serializerFactory, "serializerFactory must not be null"); + Validate.notNull(configClass, "configClass must not be null"); + + return new ConfigClassHandlerImpl<>(configClass, id, serializerFactory); + } + } + + private record CategoryAndGroups(ConfigCategory.Builder category, Map<String, OptionAddable> groups) { + private void finaliseGroups() { + groups.forEach((name, group) -> { + if (group instanceof OptionGroup.Builder groupBuilder) { + category.group(groupBuilder.build()); + } + }); + } + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigFieldImpl.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigFieldImpl.java new file mode 100644 index 0000000..aeed5ac --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigFieldImpl.java @@ -0,0 +1,75 @@ +package dev.isxander.yacl3.config.v2.impl; + +import dev.isxander.yacl3.config.v2.api.*; +import dev.isxander.yacl3.config.v2.api.autogen.AutoGen; +import dev.isxander.yacl3.config.v2.api.autogen.AutoGenField; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; + +public class ConfigFieldImpl<T> implements ConfigField<T> { + private ReflectionFieldAccess<T> field; + private final ReflectionFieldAccess<T> defaultField; + private final ConfigClassHandler<?> parent; + private final Optional<SerialField> serial; + private final Optional<AutoGenField> autoGen; + + public ConfigFieldImpl(ReflectionFieldAccess<T> field, ReflectionFieldAccess<T> defaultField, ConfigClassHandler<?> parent, @Nullable SerialEntry config, @Nullable AutoGen autoGen) { + this.field = field; + this.defaultField = defaultField; + this.parent = parent; + + this.serial = config != null + ? Optional.of( + new SerialFieldImpl( + "".equals(config.value()) ? field.name() : config.value(), + "".equals(config.comment()) ? Optional.empty() : Optional.of(config.comment()), + config.required(), + config.nullable() + ) + ) + : Optional.empty(); + this.autoGen = autoGen != null + ? Optional.of( + new AutoGenFieldImpl<>( + autoGen.category(), + "".equals(autoGen.group()) ? Optional.empty() : Optional.of(autoGen.group()) + ) + ) + : Optional.empty(); + } + + @Override + public ReflectionFieldAccess<T> access() { + return field; + } + + public void setFieldAccess(ReflectionFieldAccess<T> field) { + this.field = field; + } + + @Override + public ReflectionFieldAccess<T> defaultAccess() { + return defaultField; + } + + @Override + public ConfigClassHandler<?> parent() { + return parent; + } + + @Override + public Optional<SerialField> serial() { + return this.serial; + } + + @Override + public Optional<AutoGenField> autoGen() { + return this.autoGen; + } + + private record SerialFieldImpl(String serialName, Optional<String> comment, boolean required, boolean nullable) implements SerialField { + } + private record AutoGenFieldImpl<T>(String category, Optional<String> group) implements AutoGenField { + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/FieldBackedBinding.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/FieldBackedBinding.java new file mode 100644 index 0000000..f2f36e7 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/FieldBackedBinding.java @@ -0,0 +1,22 @@ +package dev.isxander.yacl3.config.v2.impl; + +import dev.isxander.yacl3.api.Binding; +import dev.isxander.yacl3.config.v2.api.FieldAccess; +import dev.isxander.yacl3.config.v2.api.ReadOnlyFieldAccess; + +public record FieldBackedBinding<T>(FieldAccess<T> field, ReadOnlyFieldAccess<T> defaultField) implements Binding<T> { + @Override + public T getValue() { + return field.get(); + } + + @Override + public void setValue(T value) { + field.set(value); + } + + @Override + public T defaultValue() { + return defaultField.get(); + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/ReflectionFieldAccess.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/ReflectionFieldAccess.java new file mode 100644 index 0000000..e102344 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/ReflectionFieldAccess.java @@ -0,0 +1,49 @@ +package dev.isxander.yacl3.config.v2.impl; + +import dev.isxander.yacl3.config.v2.api.FieldAccess; +import dev.isxander.yacl3.config.v2.impl.autogen.YACLAutoGenException; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Type; +import java.util.Optional; + +public record ReflectionFieldAccess<T>(Field field, Object instance) implements FieldAccess<T> { + @Override + public T get() { + try { + return (T) field.get(instance); + } catch (IllegalAccessException e) { + throw new YACLAutoGenException("Failed to access field '%s'".formatted(name()), e); + } + } + + @Override + public void set(T value) { + try { + field.set(instance, value); + } catch (IllegalAccessException e) { + throw new YACLAutoGenException("Failed to set field '%s'".formatted(name()), e); + } + } + + @Override + public String name() { + return field.getName(); + } + + @Override + public Type type() { + return field.getGenericType(); + } + + @Override + public Class<T> typeClass() { + return (Class<T>) field.getType(); + } + + @Override + public <A extends Annotation> Optional<A> getAnnotation(Class<A> annotationClass) { + return Optional.ofNullable(field.getAnnotation(annotationClass)); + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/AutoGenUtils.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/AutoGenUtils.java new file mode 100644 index 0000000..6f614c1 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/AutoGenUtils.java @@ -0,0 +1,54 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +import dev.isxander.yacl3.api.controller.ControllerBuilder; +import dev.isxander.yacl3.api.controller.ValueFormattableController; +import dev.isxander.yacl3.api.controller.ValueFormatter; +import dev.isxander.yacl3.config.v2.api.ReadOnlyFieldAccess; +import dev.isxander.yacl3.config.v2.api.autogen.CustomFormat; +import dev.isxander.yacl3.config.v2.api.autogen.FormatTranslation; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.ApiStatus; + +import java.util.Optional; +import java.util.function.Supplier; + +@ApiStatus.Internal +public final class AutoGenUtils { + public static <T> void addCustomFormatterToController(ControllerBuilder<T> controller, ReadOnlyFieldAccess<T> field) { + Optional<CustomFormat> formatter = field.getAnnotation(CustomFormat.class); + Optional<FormatTranslation> translation = field.getAnnotation(FormatTranslation.class); + + if (formatter.isPresent() && translation.isPresent()) { + throw new YACLAutoGenException("'%s': Cannot use both @CustomFormatter and @FormatTranslation on the same field.".formatted(field.name())); + } else if (formatter.isEmpty() && translation.isEmpty()) { + return; + } + + if (!(controller instanceof ValueFormattableController<?, ?>)) { + throw new YACLAutoGenException("Attempted to use @CustomFormatter or @FormatTranslation on an option factory for field '%s' that uses a controller that does not support this.".formatted(field.name())); + } + + ValueFormattableController<T, ?> typedBuilder = (ValueFormattableController<T, ?>) controller; + + formatter.ifPresent(formatterClass -> { + try { + typedBuilder.formatValue((ValueFormatter<T>) formatterClass.value().getConstructor().newInstance()); + } catch (Exception e) { + throw new YACLAutoGenException("'%s': Failed to instantiate formatter class %s.".formatted(field.name(), formatterClass.value().getName()), e); + } + }); + + translation.ifPresent(annotation -> + typedBuilder.formatValue(v -> Component.translatable(annotation.value(), v))); + } + + public static <T> T constructNoArgsClass(Class<T> clazz, Supplier<String> constructorNotFoundConsumer, Supplier<String> constructorFailedConsumer) { + try { + return clazz.getConstructor().newInstance(); + } catch (NoSuchMethodException e) { + throw new YACLAutoGenException(constructorNotFoundConsumer.get(), e); + } catch (Exception e) { + throw new YACLAutoGenException(constructorFailedConsumer.get(), e); + } + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/BooleanImpl.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/BooleanImpl.java new file mode 100644 index 0000000..b41836a --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/BooleanImpl.java @@ -0,0 +1,25 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.controller.BooleanControllerBuilder; +import dev.isxander.yacl3.api.controller.ControllerBuilder; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.autogen.SimpleOptionFactory; +import dev.isxander.yacl3.config.v2.api.autogen.Boolean; +import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess; +import net.minecraft.network.chat.Component; + +public class BooleanImpl extends SimpleOptionFactory<Boolean, java.lang.Boolean> { + @Override + protected ControllerBuilder<java.lang.Boolean> createController(Boolean annotation, ConfigField<java.lang.Boolean> field, OptionAccess storage, Option<java.lang.Boolean> option) { + var builder = BooleanControllerBuilder.create(option) + .coloured(annotation.colored()); + switch (annotation.formatter()) { + case ON_OFF -> builder.onOffFormatter(); + case YES_NO -> builder.yesNoFormatter(); + case TRUE_FALSE -> builder.trueFalseFormatter(); + case CUSTOM -> builder.formatValue(v -> Component.translatable(getTranslationKey(field, "fmt." + v))); + } + return builder; + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/ColorFieldImpl.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/ColorFieldImpl.java new file mode 100644 index 0000000..7910c59 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/ColorFieldImpl.java @@ -0,0 +1,19 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.controller.ColorControllerBuilder; +import dev.isxander.yacl3.api.controller.ControllerBuilder; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.autogen.SimpleOptionFactory; +import dev.isxander.yacl3.config.v2.api.autogen.ColorField; +import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess; + +import java.awt.Color; + +public class ColorFieldImpl extends SimpleOptionFactory<ColorField, Color> { + @Override + protected ControllerBuilder<Color> createController(ColorField annotation, ConfigField<Color> field, OptionAccess storage, Option<Color> option) { + return ColorControllerBuilder.create(option) + .allowAlpha(annotation.allowAlpha()); + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DoubleFieldImpl.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DoubleFieldImpl.java new file mode 100644 index 0000000..6445141 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DoubleFieldImpl.java @@ -0,0 +1,32 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.controller.ControllerBuilder; +import dev.isxander.yacl3.api.controller.DoubleFieldControllerBuilder; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.autogen.DoubleField; +import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess; +import dev.isxander.yacl3.config.v2.api.autogen.SimpleOptionFactory; +import net.minecraft.locale.Language; +import net.minecraft.network.chat.Component; + +public class DoubleFieldImpl extends SimpleOptionFactory<DoubleField, Double> { + @Override + protected ControllerBuilder<Double> createController(DoubleField annotation, ConfigField<Double> field, OptionAccess storage, Option<Double> option) { + return DoubleFieldControllerBuilder.create(option) + .formatValue(v -> { + String key = null; + if (v == annotation.min()) + key = getTranslationKey(field, "fmt.min"); + else if (v == annotation.max()) + key = getTranslationKey(field, "fmt.max"); + if (key != null && Language.getInstance().has(key)) + return Component.translatable(key); + key = getTranslationKey(field, "fmt"); + if (Language.getInstance().has(key)) + return Component.translatable(key, v); + return Component.translatable(String.format(annotation.format(), v)); + }) + .range(annotation.min(), annotation.max()); + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DoubleSliderImpl.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DoubleSliderImpl.java new file mode 100644 index 0000000..e6dd05d --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DoubleSliderImpl.java @@ -0,0 +1,33 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.controller.ControllerBuilder; +import dev.isxander.yacl3.api.controller.DoubleSliderControllerBuilder; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.autogen.SimpleOptionFactory; +import dev.isxander.yacl3.config.v2.api.autogen.DoubleSlider; +import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess; +import net.minecraft.locale.Language; +import net.minecraft.network.chat.Component; + +public class DoubleSliderImpl extends SimpleOptionFactory<DoubleSlider, Double> { + @Override + protected ControllerBuilder<Double> createController(DoubleSlider annotation, ConfigField<Double> field, OptionAccess storage, Option<Double> option) { + return DoubleSliderControllerBuilder.create(option) + .formatValue(v -> { + String key = null; + if (v == annotation.min()) + key = getTranslationKey(field, "fmt.min"); + else if (v == annotation.max()) + key = getTranslationKey(field, "fmt.max"); + if (key != null && Language.getInstance().has(key)) + return Component.translatable(key); + key = getTranslationKey(field, "fmt"); + if (Language.getInstance().has(key)) + return Component.translatable(key, v); + return Component.translatable(String.format(annotation.format(), v)); + }) + .range(annotation.min(), annotation.max()) + .step(annotation.step()); + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DropdownImpl.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DropdownImpl.java new file mode 100644 index 0000000..c487aab --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DropdownImpl.java @@ -0,0 +1,19 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.controller.ControllerBuilder; +import dev.isxander.yacl3.api.controller.DropdownStringControllerBuilder; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.autogen.Dropdown; +import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess; +import dev.isxander.yacl3.config.v2.api.autogen.SimpleOptionFactory; + +public class DropdownImpl extends SimpleOptionFactory<Dropdown, String> { + @Override + protected ControllerBuilder<String> createController(Dropdown annotation, ConfigField<String> field, OptionAccess storage, Option<String> option) { + return DropdownStringControllerBuilder.create(option) + .values(annotation.values()) + .allowEmptyValue(annotation.allowEmptyValue()) + .allowAnyValue(annotation.allowAnyValue()); + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/EmptyCustomImageFactory.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/EmptyCustomImageFactory.java new file mode 100644 index 0000000..1500864 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/EmptyCustomImageFactory.java @@ -0,0 +1,17 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess; +import dev.isxander.yacl3.config.v2.api.autogen.CustomImage; +import dev.isxander.yacl3.gui.image.ImageRenderer; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +public class EmptyCustomImageFactory implements CustomImage.CustomImageFactory<Object> { + + @Override + public CompletableFuture<ImageRenderer> createImage(Object value, ConfigField<Object> field, OptionAccess access) { + throw new IllegalStateException(); + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/EnumCyclerImpl.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/EnumCyclerImpl.java new file mode 100644 index 0000000..f15d862 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/EnumCyclerImpl.java @@ -0,0 +1,42 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +import dev.isxander.yacl3.api.NameableEnum; +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.controller.ControllerBuilder; +import dev.isxander.yacl3.api.controller.CyclingListControllerBuilder; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.autogen.SimpleOptionFactory; +import dev.isxander.yacl3.config.v2.api.autogen.EnumCycler; +import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess; +import net.minecraft.network.chat.Component; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.IntStream; + +public class EnumCyclerImpl extends SimpleOptionFactory<EnumCycler, Enum<?>> { + @Override + protected ControllerBuilder<Enum<?>> createController(EnumCycler annotation, ConfigField<Enum<?>> field, OptionAccess storage, Option<Enum<?>> option) { + List<? extends Enum<?>> values; + + if (option.pendingValue() instanceof EnumCycler.CyclableEnum<?> cyclableEnum) { + values = Arrays.asList(cyclableEnum.allowedValues()); + } else { + Enum<?>[] constants = field.access().typeClass().getEnumConstants(); + values = IntStream.range(0, constants.length) + .filter(ordinal -> annotation.allowedOrdinals().length == 0 || Arrays.stream(annotation.allowedOrdinals()).noneMatch(allowed -> allowed == ordinal)) + .mapToObj(ordinal -> constants[ordinal]) + .toList(); + } + + // EnumController doesn't support filtering + var builder = CyclingListControllerBuilder.create(option) + .values(values); + if (NameableEnum.class.isAssignableFrom(field.access().typeClass())) { + builder.formatValue(v -> ((NameableEnum) v).getDisplayName()); + } else { + builder.formatValue(v -> Component.translatable("yacl3.config.enum.%s.%s".formatted(field.access().typeClass().getSimpleName(), v.name().toLowerCase()))); + } + return builder; + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/FloatFieldImpl.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/FloatFieldImpl.java new file mode 100644 index 0000000..acdabd6 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/FloatFieldImpl.java @@ -0,0 +1,32 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.controller.ControllerBuilder; +import dev.isxander.yacl3.api.controller.FloatFieldControllerBuilder; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.autogen.FloatField; +import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess; +import dev.isxander.yacl3.config.v2.api.autogen.SimpleOptionFactory; +import net.minecraft.locale.Language; +import net.minecraft.network.chat.Component; + +public class FloatFieldImpl extends SimpleOptionFactory<FloatField, Float> { + @Override + protected ControllerBuilder<Float> createController(FloatField annotation, ConfigField<Float> field, OptionAccess storage, Option<Float> option) { + return FloatFieldControllerBuilder.create(option) + .formatValue(v -> { + String key = null; + if (v == annotation.min()) + key = getTranslationKey(field, "fmt.min"); + else if (v == annotation.max()) + key = getTranslationKey(field, "fmt.max"); + if (key != null && Language.getInstance().has(key)) + return Component.translatable(key); + key = getTranslationKey(field, "fmt"); + if (Language.getInstance().has(key)) + return Component.translatable(key, v); + return Component.translatable(String.format(annotation.format(), v)); + }) + .range(annotation.min(), annotation.max()); + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/FloatSliderImpl.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/FloatSliderImpl.java new file mode 100644 index 0000000..f22302f --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/FloatSliderImpl.java @@ -0,0 +1,33 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.controller.ControllerBuilder; +import dev.isxander.yacl3.api.controller.FloatSliderControllerBuilder; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.autogen.SimpleOptionFactory; +import dev.isxander.yacl3.config.v2.api.autogen.FloatSlider; +import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess; +import net.minecraft.locale.Language; +import net.minecraft.network.chat.Component; + +public class FloatSliderImpl extends SimpleOptionFactory<FloatSlider, Float> { + @Override + protected ControllerBuilder<Float> createController(FloatSlider annotation, ConfigField<Float> field, OptionAccess storage, Option<Float> option) { + return FloatSliderControllerBuilder.create(option) + .formatValue(v -> { + String key = null; + if (v == annotation.min()) + key = getTranslationKey(field, "fmt.min"); + else if (v == annotation.max()) + key = getTranslationKey(field, "fmt.max"); + if (key != null && Language.getInstance().has(key)) + return Component.translatable(key); + key = getTranslationKey(field, "fmt"); + if (Language.getInstance().has(key)) + return Component.translatable(key, v); + return Component.translatable(String.format(annotation.format(), v)); + }) + .range(annotation.min(), annotation.max()) + .step(annotation.step()); + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/IntFieldImpl.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/IntFieldImpl.java new file mode 100644 index 0000000..a3b759a --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/IntFieldImpl.java @@ -0,0 +1,28 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.controller.ControllerBuilder; +import dev.isxander.yacl3.api.controller.IntegerFieldControllerBuilder; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.autogen.IntField; +import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess; +import dev.isxander.yacl3.config.v2.api.autogen.SimpleOptionFactory; +import net.minecraft.locale.Language; +import net.minecraft.network.chat.Component; + +public class IntFieldImpl extends SimpleOptionFactory<IntField, Integer> { + @Override + protected ControllerBuilder<Integer> createController(IntField annotation, ConfigField<Integer> field, OptionAccess storage, Option<Integer> option) { + return IntegerFieldControllerBuilder.create(option) + .formatValue(v -> { + String key = getTranslationKey(field, "fmt." + v); + if (Language.getInstance().has(key)) + return Component.translatable(key); + key = getTranslationKey(field, "fmt"); + if (Language.getInstance().has(key)) + return Component.translatable(key, v); + return Component.literal(Integer.toString(v)); + }) + .range(annotation.min(), annotation.max()); + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/IntSliderImpl.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/IntSliderImpl.java new file mode 100644 index 0000000..b570b44 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/IntSliderImpl.java @@ -0,0 +1,29 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.controller.ControllerBuilder; +import dev.isxander.yacl3.api.controller.IntegerSliderControllerBuilder; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.autogen.SimpleOptionFactory; +import dev.isxander.yacl3.config.v2.api.autogen.IntSlider; +import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess; +import net.minecraft.locale.Language; +import net.minecraft.network.chat.Component; + +public class IntSliderImpl extends SimpleOptionFactory<IntSlider, Integer> { + @Override + protected ControllerBuilder<Integer> createController(IntSlider annotation, ConfigField<Integer> field, OptionAccess storage, Option<Integer> option) { + return IntegerSliderControllerBuilder.create(option) + .formatValue(v -> { + String key = getTranslationKey(field, "fmt." + v); + if (Language.getInstance().has(key)) + return Component.translatable(key); + key = getTranslationKey(field, "fmt"); + if (Language.getInstance().has(key)) + return Component.translatable(key, v); + return Component.literal(Integer.toString(v)); + }) + .range(annotation.min(), annotation.max()) + .step(annotation.step()); + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/ItemFieldImpl.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/ItemFieldImpl.java new file mode 100644 index 0000000..2802f5c --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/ItemFieldImpl.java @@ -0,0 +1,17 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.controller.ControllerBuilder; +import dev.isxander.yacl3.api.controller.ItemControllerBuilder; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.autogen.ItemField; +import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess; +import dev.isxander.yacl3.config.v2.api.autogen.SimpleOptionFactory; +import net.minecraft.world.item.Item; + +public class ItemFieldImpl extends SimpleOptionFactory<ItemField, Item> { + @Override + protected ControllerBuilder<Item> createController(ItemField annotation, ConfigField<Item> field, OptionAccess storage, Option<Item> option) { + return ItemControllerBuilder.create(option); + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/LabelImpl.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/LabelImpl.java new file mode 100644 index 0000000..6f9b368 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/LabelImpl.java @@ -0,0 +1,16 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +import dev.isxander.yacl3.api.LabelOption; +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.autogen.OptionFactory; +import dev.isxander.yacl3.config.v2.api.autogen.Label; +import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess; +import net.minecraft.network.chat.Component; + +public class LabelImpl implements OptionFactory<Label, Component> { + @Override + public Option<Component> createOption(Label annotation, ConfigField<Component> field, OptionAccess optionAccess) { + return LabelOption.create(field.access().get()); + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/ListGroupImpl.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/ListGroupImpl.java new file mode 100644 index 0000000..f78d4ba --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/ListGroupImpl.java @@ -0,0 +1,102 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +import dev.isxander.yacl3.api.ListOption; +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.OptionDescription; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.autogen.ListGroup; +import dev.isxander.yacl3.config.v2.api.autogen.OptionFactory; +import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess; +import dev.isxander.yacl3.config.v2.impl.FieldBackedBinding; +import net.minecraft.client.Minecraft; +import net.minecraft.locale.Language; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.List; + +public class ListGroupImpl<T> implements OptionFactory<ListGroup, List<T>> { + @Override + public Option<List<T>> createOption(ListGroup annotation, ConfigField<List<T>> field, OptionAccess optionAccess) { + if (field.autoGen().orElseThrow().group().isPresent()) { + throw new YACLAutoGenException("@ListGroup fields ('%s') cannot be inside a group as lists act as groups.".formatted(field.access().name())); + } + + ListGroup.ValueFactory<T> valueFactory = createValueFactory((Class<? extends ListGroup.ValueFactory<T>>) annotation.valueFactory()); + ListGroup.ControllerFactory<T> controllerFactory = createControllerFactory((Class<? extends ListGroup.ControllerFactory<T>>) annotation.controllerFactory()); + + return ListOption.<T>createBuilder() + .name(Component.translatable(this.getTranslationKey(field, null))) + .description(this.description(field)) + .initial(valueFactory::provideNewValue) + .controller(opt -> controllerFactory.createController(annotation, field, optionAccess, opt)) + .binding(new FieldBackedBinding<>(field.access(), field.defaultAccess())) + .minimumNumberOfEntries(annotation.minEntries()) + .maximumNumberOfEntries(annotation.maxEntries() == 0 ? Integer.MAX_VALUE : annotation.maxEntries()) + .insertEntriesAtEnd(annotation.addEntriesToBottom()) + .build(); + } + + private OptionDescription description(ConfigField<List<T>> field) { + OptionDescription.Builder builder = OptionDescription.createBuilder(); + + String key = this.getTranslationKey(field, "desc"); + if (Language.getInstance().has(key)) { + builder.text(Component.translatable(key)); + } else { + key += "."; + int i = 0; + while (Language.getInstance().has(key + i++)) { + builder.text(Component.translatable(key + i)); + } + } + + String imagePath = "textures/yacl3/" + field.parent().id().getPath() + "/" + field.access().name() + ".webp"; + imagePath = imagePath.toLowerCase().replaceAll("[^a-z0-9/._:-]", "_"); + ResourceLocation imageLocation = new ResourceLocation(field.parent().id().getNamespace(), imagePath); + if (Minecraft.getInstance().getResourceManager().getResource(imageLocation).isPresent()) { + builder.webpImage(imageLocation); + } + + return builder.build(); + } + + private ListGroup.ValueFactory<T> createValueFactory(Class<? extends ListGroup.ValueFactory<T>> clazz) { + Constructor<? extends ListGroup.ValueFactory<T>> constructor; + try { + constructor = clazz.getConstructor(); + } catch (NoSuchMethodException e) { + throw new YACLAutoGenException("Could not find no-args constructor for `valueFactory` on '%s' for @ListGroup field.".formatted(clazz.getName()), e); + } + + try { + return constructor.newInstance(); + } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) { + throw new YACLAutoGenException("Couldn't invoke no-args constructor for `valueFactory` on '%s' for @ListGroup field.".formatted(clazz.getName()), e); + } + } + + private ListGroup.ControllerFactory<T> createControllerFactory(Class<? extends ListGroup.ControllerFactory<T>> clazz) { + Constructor<? extends ListGroup.ControllerFactory<T>> constructor; + try { + constructor = clazz.getConstructor(); + } catch (NoSuchMethodException e) { + throw new YACLAutoGenException("Could not find no-args constructor on `controllerFactory`, '%s' for @ListGroup field.".formatted(clazz.getName()), e); + } + + try { + return constructor.newInstance(); + } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) { + throw new YACLAutoGenException("Couldn't invoke no-args constructor on `controllerFactory`, '%s' for @ListGroup field.".formatted(clazz.getName()), e); + } + } + + private String getTranslationKey(ConfigField<List<T>> field, @Nullable String suffix) { + String key = "yacl3.config.%s.%s".formatted(field.parent().id().toString(), field.access().name()); + if (suffix != null) key += "." + suffix; + return key; + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/LongFieldImpl.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/LongFieldImpl.java new file mode 100644 index 0000000..5da7d20 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/LongFieldImpl.java @@ -0,0 +1,28 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.controller.ControllerBuilder; +import dev.isxander.yacl3.api.controller.LongFieldControllerBuilder; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.autogen.LongField; +import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess; +import dev.isxander.yacl3.config.v2.api.autogen.SimpleOptionFactory; +import net.minecraft.locale.Language; +import net.minecraft.network.chat.Component; + +public class LongFieldImpl extends SimpleOptionFactory<LongField, Long> { + @Override + protected ControllerBuilder<Long> createController(LongField annotation, ConfigField<Long> field, OptionAccess storage, Option<Long> option) { + return LongFieldControllerBuilder.create(option) + .formatValue(v -> { + String key = getTranslationKey(field, "fmt." + v); + if (Language.getInstance().has(key)) + return Component.translatable(key); + key = getTranslationKey(field, "fmt"); + if (Language.getInstance().has(key)) + return Component.translatable(key, v); + return Component.literal(Long.toString(v)); + }) + .range(annotation.min(), annotation.max()); + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/LongSliderImpl.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/LongSliderImpl.java new file mode 100644 index 0000000..95c5254 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/LongSliderImpl.java @@ -0,0 +1,29 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.controller.ControllerBuilder; +import dev.isxander.yacl3.api.controller.LongSliderControllerBuilder; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.autogen.LongSlider; +import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess; +import dev.isxander.yacl3.config.v2.api.autogen.SimpleOptionFactory; +import net.minecraft.locale.Language; +import net.minecraft.network.chat.Component; + +public class LongSliderImpl extends SimpleOptionFactory<LongSlider, Long> { + @Override + protected ControllerBuilder<Long> createController(LongSlider annotation, ConfigField<Long> field, OptionAccess storage, Option<Long> option) { + return LongSliderControllerBuilder.create(option) + .formatValue(v -> { + String key = getTranslationKey(field, "fmt." + v); + if (Language.getInstance().has(key)) + return Component.translatable(key); + key = getTranslationKey(field, "fmt"); + if (Language.getInstance().has(key)) + return Component.translatable(key, v); + return Component.literal(Long.toString(v)); + }) + .range(annotation.min(), annotation.max()) + .step(annotation.step()); + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/MasterTickBoxImpl.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/MasterTickBoxImpl.java new file mode 100644 index 0000000..2d37f03 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/MasterTickBoxImpl.java @@ -0,0 +1,25 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.controller.ControllerBuilder; +import dev.isxander.yacl3.api.controller.TickBoxControllerBuilder; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.autogen.SimpleOptionFactory; +import dev.isxander.yacl3.config.v2.api.autogen.MasterTickBox; +import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess; + +public class MasterTickBoxImpl extends SimpleOptionFactory<MasterTickBox, Boolean> { + @Override + protected ControllerBuilder<Boolean> createController(MasterTickBox annotation, ConfigField<Boolean> field, OptionAccess storage, Option<Boolean> option) { + return TickBoxControllerBuilder.create(option); + } + + @Override + protected void listener(MasterTickBox annotation, ConfigField<Boolean> field, OptionAccess storage, Option<Boolean> option, Boolean value) { + for (String child : annotation.value()) { + storage.scheduleOptionOperation(child, childOpt -> { + childOpt.setAvailable(annotation.invert() != value); + }); + } + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/OptionAccessImpl.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/OptionAccessImpl.java new file mode 100644 index 0000000..579f776 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/OptionAccessImpl.java @@ -0,0 +1,44 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess; +import dev.isxander.yacl3.impl.utils.YACLConstants; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +public class OptionAccessImpl implements OptionAccess { + private final Map<String, Option<?>> storage = new HashMap<>(); + private final Map<String, Consumer<Option<?>>> scheduledOperations = new HashMap<>(); + + @Override + public @Nullable Option<?> getOption(String fieldName) { + return storage.get(fieldName); + } + + @Override + public void scheduleOptionOperation(String fieldName, Consumer<Option<?>> optionConsumer) { + if (storage.containsKey(fieldName)) { + optionConsumer.accept(storage.get(fieldName)); + } else { + scheduledOperations.merge(fieldName, optionConsumer, Consumer::andThen); + } + } + + public void putOption(String fieldName, Option<?> option) { + storage.put(fieldName, option); + + Consumer<Option<?>> consumer = scheduledOperations.remove(fieldName); + if (consumer != null) { + consumer.accept(option); + } + } + + public void checkBadOperations() { + if (!scheduledOperations.isEmpty()) { + YACLConstants.LOGGER.warn("There are scheduled operations on the `OptionAccess` that tried to reference fields that do not exist. The following have been referenced that do not exist: " + String.join(", ", scheduledOperations.keySet())); + } + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/OptionFactoryRegistry.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/OptionFactoryRegistry.java new file mode 100644 index 0000000..4f6e3c7 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/OptionFactoryRegistry.java @@ -0,0 +1,64 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.autogen.OptionFactory; +import dev.isxander.yacl3.config.v2.api.autogen.*; +import dev.isxander.yacl3.config.v2.api.autogen.Boolean; +import dev.isxander.yacl3.impl.utils.YACLConstants; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class OptionFactoryRegistry { + private static final Map<Class<?>, OptionFactory<?, ?>> factoryMap = new HashMap<>(); + + static { + registerOptionFactory(TickBox.class, new TickBoxImpl()); + registerOptionFactory(Boolean.class, new BooleanImpl()); + registerOptionFactory(IntSlider.class, new IntSliderImpl()); + registerOptionFactory(LongSlider.class, new LongSliderImpl()); + registerOptionFactory(FloatSlider.class, new FloatSliderImpl()); + registerOptionFactory(DoubleSlider.class, new DoubleSliderImpl()); + registerOptionFactory(IntField.class, new IntFieldImpl()); + registerOptionFactory(LongField.class, new LongFieldImpl()); + registerOptionFactory(FloatField.class, new FloatFieldImpl()); + registerOptionFactory(DoubleField.class, new DoubleFieldImpl()); + registerOptionFactory(EnumCycler.class, new EnumCyclerImpl()); + registerOptionFactory(StringField.class, new StringFieldImpl()); + registerOptionFactory(ColorField.class, new ColorFieldImpl()); + registerOptionFactory(Dropdown.class, new DropdownImpl()); + registerOptionFactory(ItemField.class, new ItemFieldImpl()); + registerOptionFactory(Label.class, new LabelImpl()); + registerOptionFactory(ListGroup.class, new ListGroupImpl<>()); + + registerOptionFactory(MasterTickBox.class, new MasterTickBoxImpl()); + } + + public static <A extends Annotation, T> void registerOptionFactory(Class<A> annotation, OptionFactory<A, T> factory) { + factoryMap.put(annotation, factory); + } + + public static <T> Optional<Option<T>> createOption(Field field, ConfigField<T> configField, OptionAccess storage) { + Annotation[] annotations = Arrays.stream(field.getAnnotations()) + .filter(annotation -> factoryMap.containsKey(annotation.annotationType())) + .toArray(Annotation[]::new); + + if (annotations.length != 1) { + YACLConstants.LOGGER.warn("Found {} option factory annotations on field {}, expected 1", annotations.length, field); + + if (annotations.length == 0) { + return Optional.empty(); + } + } + + Annotation annotation = annotations[0]; + // noinspection unchecked + OptionFactory<Annotation, T> factory = (OptionFactory<Annotation, T>) factoryMap.get(annotation.annotationType()); + return Optional.of(factory.createOption(annotation, configField, storage)); + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/StringFieldImpl.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/StringFieldImpl.java new file mode 100644 index 0000000..96b63a7 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/StringFieldImpl.java @@ -0,0 +1,16 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.controller.ControllerBuilder; +import dev.isxander.yacl3.api.controller.StringControllerBuilder; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.autogen.SimpleOptionFactory; +import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess; +import dev.isxander.yacl3.config.v2.api.autogen.StringField; + +public class StringFieldImpl extends SimpleOptionFactory<StringField, String> { + @Override + protected ControllerBuilder<String> createController(StringField annotation, ConfigField<String> field, OptionAccess storage, Option<String> option) { + return StringControllerBuilder.create(option); + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/TickBoxImpl.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/TickBoxImpl.java new file mode 100644 index 0000000..050257c --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/TickBoxImpl.java @@ -0,0 +1,16 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.controller.ControllerBuilder; +import dev.isxander.yacl3.api.controller.TickBoxControllerBuilder; +import dev.isxander.yacl3.config.v2.api.ConfigField; +import dev.isxander.yacl3.config.v2.api.autogen.SimpleOptionFactory; +import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess; +import dev.isxander.yacl3.config.v2.api.autogen.TickBox; + +public class TickBoxImpl extends SimpleOptionFactory<TickBox, Boolean> { + @Override + protected ControllerBuilder<Boolean> createController(TickBox annotation, ConfigField<Boolean> field, OptionAccess storage, Option<Boolean> option) { + return TickBoxControllerBuilder.create(option); + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/YACLAutoGenException.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/YACLAutoGenException.java new file mode 100644 index 0000000..68b375d --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/YACLAutoGenException.java @@ -0,0 +1,11 @@ +package dev.isxander.yacl3.config.v2.impl.autogen; + +public class YACLAutoGenException extends RuntimeException { + public YACLAutoGenException(String message) { + super(message); + } + + public YACLAutoGenException(String message, Throwable e) { + super(message, e); + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java b/src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java new file mode 100644 index 0000000..9f6d8c8 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java @@ -0,0 +1,275 @@ +package dev.isxander.yacl3.config.v2.impl.serializer; + +import com.google.gson.*; +import com.mojang.serialization.JsonOps; +import dev.isxander.yacl3.config.v2.api.*; +import dev.isxander.yacl3.config.v2.api.serializer.GsonConfigSerializerBuilder; +import dev.isxander.yacl3.gui.utils.ItemRegistryHelper; +import dev.isxander.yacl3.impl.utils.YACLConstants; +import dev.isxander.yacl3.platform.YACLPlatform; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; +import net.minecraft.world.item.Item; +import org.jetbrains.annotations.ApiStatus; +import org.quiltmc.parsers.json.JsonReader; +import org.quiltmc.parsers.json.JsonWriter; +import org.quiltmc.parsers.json.gson.GsonReader; +import org.quiltmc.parsers.json.gson.GsonWriter; + +import java.awt.Color; +import java.io.IOException; +import java.io.StringWriter; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; + +public class GsonConfigSerializer<T> extends ConfigSerializer<T> { + private final Gson gson; + private final Path path; + private final boolean json5; + + private GsonConfigSerializer(ConfigClassHandler<T> config, Path path, Gson gson, boolean json5) { + super(config); + this.gson = gson; + this.path = path; + this.json5 = json5; + } + + @Override + public void save() { + YACLConstants.LOGGER.info("Serializing {} to '{}'", config.configClass(), path); + + try (StringWriter stringWriter = new StringWriter()) { + JsonWriter jsonWriter = json5 ? JsonWriter.json5(stringWriter) : JsonWriter.json(stringWriter); + GsonWriter gsonWriter = new GsonWriter(jsonWriter); + + jsonWriter.beginObject(); + + for (ConfigField<?> field : config.fields()) { + SerialField serial = field.serial().orElse(null); + if (serial == null) continue; + + if (!json5 && serial.comment().isPresent() && YACLPlatform.isDevelopmentEnv()) { + YACLConstants.LOGGER.warn("Found comment in config field '{}', but json5 is not enabled. Enable it with `.setJson5(true)` on the `GsonConfigSerializerBuilder`. Comments will not be serialized. This warning is only visible in development environments.", serial.serialName()); + } + jsonWriter.comment(serial.comment().orElse(null)); + + jsonWriter.name(serial.serialName()); + + JsonElement element; + try { + element = gson.toJsonTree(field.access().get(), field.access().type()); + } catch (Exception e) { + YACLConstants.LOGGER.error("Failed to serialize config field '{}'. Serializing as null.", serial.serialName(), e); + jsonWriter.nullValue(); + continue; + } + + try { + gson.toJson(element, gsonWriter); + } catch (Exception e) { + YACLConstants.LOGGER.error("Failed to serialize config field '{}'. Due to the error state this JSON writer cannot continue safely and the save will be abandoned.", serial.serialName(), e); + return; + } + } + + jsonWriter.endObject(); + jsonWriter.flush(); + + Files.createDirectories(path.getParent()); + Files.writeString(path, stringWriter.toString(), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); + } catch (IOException e) { + YACLConstants.LOGGER.error("Failed to serialize config class '{}'.", config.configClass().getSimpleName(), e); + } + } + + @Override + public LoadResult loadSafely(Map<ConfigField<?>, FieldAccess<?>> bufferAccessMap) { + if (!Files.exists(path)) { + YACLConstants.LOGGER.info("Config file '{}' does not exist. Creating it with default values.", path); + save(); + return LoadResult.NO_CHANGE; + } + + YACLConstants.LOGGER.info("Deserializing {} from '{}'", config.configClass().getSimpleName(), path); + + Map<String, ConfigField<?>> fieldMap = Arrays.stream(config.fields()) + .filter(field -> field.serial().isPresent()) + .collect(Collectors.toMap(f -> f.serial().orElseThrow().serialName(), Function.identity())); + Set<String> missingFields = fieldMap.keySet(); + boolean dirty = false; + + try (JsonReader jsonReader = json5 ? JsonReader.json5(path) : JsonReader.json(path)) { + GsonReader gsonReader = new GsonReader(jsonReader); + + jsonReader.beginObject(); + + while (jsonReader.hasNext()) { + String name = jsonReader.nextName(); + ConfigField<?> field = fieldMap.get(name); + missingFields.remove(name); + + if (field == null) { + YACLConstants.LOGGER.warn("Found unknown config field '{}'.", name); + jsonReader.skipValue(); + continue; + } + + FieldAccess<?> bufferAccess = bufferAccessMap.get(field); + SerialField serial = field.serial().orElse(null); + if (serial == null) continue; + + JsonElement element; + try { + element = gson.fromJson(gsonReader, JsonElement.class); + } catch (Exception e) { + YACLConstants.LOGGER.error("Failed to deserialize config field '{}'. Due to the error state this JSON reader cannot be re-used and loading will be aborted.", name, e); + return LoadResult.FAILURE; + } + + if (element.isJsonNull() && !serial.nullable()) { + YACLConstants.LOGGER.warn("Found null value in non-nullable config field '{}'. Leaving field as default and marking as dirty.", name); + dirty = true; + continue; + } + + try { + bufferAccess.set(gson.fromJson(element, bufferAccess.type())); + } catch (Exception e) { + YACLConstants.LOGGER.error("Failed to deserialize config field '{}'. Leaving as default.", name, e); + } + } + + jsonReader.endObject(); + } catch (IOException e) { + YACLConstants.LOGGER.error("Failed to deserialize config class.", e); + return LoadResult.FAILURE; + } + + if (!missingFields.isEmpty()) { + for (String missingField : missingFields) { + if (fieldMap.get(missingField).serial().orElseThrow().required()) { + dirty = true; + YACLConstants.LOGGER.warn("Missing required config field '{}''. Re-saving as default.", missingField); + } + } + } + + return dirty ? LoadResult.DIRTY : LoadResult.SUCCESS; + } + + @Override + @Deprecated + @SuppressWarnings("deprecation") + public void load() { + YACLConstants.LOGGER.warn("Calling ConfigSerializer#load() directly is deprecated. Please use ConfigClassHandler#load() instead."); + config.load(); + } + + /*? if >=1.20.4 {*/ + public static class StyleTypeAdapter implements JsonSerializer<Style>, JsonDeserializer<Style> { + @Override + public Style deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return Style.Serializer.CODEC.parse(JsonOps.INSTANCE, json).result().orElse(Style.EMPTY); + } + + @Override + public JsonElement serialize(Style src, Type typeOfSrc, JsonSerializationContext context) { + return Style.Serializer.CODEC.encodeStart(JsonOps.INSTANCE, src).result().orElse(JsonNull.INSTANCE); + } + } + /*?}*/ + + public static class ColorTypeAdapter implements JsonSerializer<Color>, JsonDeserializer<Color> { + @Override + public Color deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { + return new Color(jsonElement.getAsInt(), true); + } + + @Override + public JsonElement serialize(Color color, Type type, JsonSerializationContext jsonSerializationContext) { + return new JsonPrimitive(color.getRGB()); + } + } + + public static class ItemTypeAdapter implements JsonSerializer<Item>, JsonDeserializer<Item> { + @Override + public Item deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { + return ItemRegistryHelper.getItemFromName(jsonElement.getAsString()); + } + + @Override + public JsonElement serialize(Item item, Type type, JsonSerializationContext jsonSerializationContext) { + return new JsonPrimitive(BuiltInRegistries.ITEM.getKey(item).toString()); + } + } + + @ApiStatus.Internal + public static class Builder<T> implements GsonConfigSerializerBuilder<T> { + private final ConfigClassHandler<T> config; + private Path path; + private boolean json5; + private UnaryOperator<GsonBuilder> gsonBuilder = builder -> builder + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .serializeNulls() + /*? if >1.20.4 { *//* + .registerTypeHierarchyAdapter(Component.class, new Component.SerializerAdapter(RegistryAccess.EMPTY)) + *//*? } elif =1.20.4 {*/ + .registerTypeHierarchyAdapter(Component.class, new Component.SerializerAdapter()) + /*? } else {*//* + .registerTypeHierarchyAdapter(Component.class, new Component.Serializer()) + *//*?}*/ + .registerTypeHierarchyAdapter(Style.class, /*? if >=1.20.4 {*/new StyleTypeAdapter()/*?} else {*//*new Style.Serializer()*//*?}*/) + .registerTypeHierarchyAdapter(Color.class, new ColorTypeAdapter()) + .registerTypeHierarchyAdapter(Item.class, new ItemTypeAdapter()) + .setPrettyPrinting(); + + public Builder(ConfigClassHandler<T> config) { + this.config = config; + } + + @Override + public Builder<T> setPath(Path path) { + this.path = path; + return this; + } + + @Override + public Builder<T> overrideGsonBuilder(GsonBuilder gsonBuilder) { + this.gsonBuilder = builder -> gsonBuilder; + return this; + } + + @Override + public Builder<T> overrideGsonBuilder(Gson gson) { + return this.overrideGsonBuilder(gson.newBuilder()); + } + + @Override + public Builder<T> appendGsonBuilder(UnaryOperator<GsonBuilder> gsonBuilder) { + UnaryOperator<GsonBuilder> prev = this.gsonBuilder; + this.gsonBuilder = builder -> gsonBuilder.apply(prev.apply(builder)); + return this; + } + + @Override + public Builder<T> setJson5(boolean json5) { + this.json5 = json5; + return this; + } + + @Override + public GsonConfigSerializer<T> build() { + return new GsonConfigSerializer<>(config, path, gsonBuilder.apply(new GsonBuilder()).create(), json5); + } + } +} |