aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/dev/isxander/yacl3/config/v2
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/dev/isxander/yacl3/config/v2')
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/ConfigClassHandler.java107
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/ConfigField.java40
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/ConfigSerializer.java64
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/FieldAccess.java14
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/ReadOnlyFieldAccess.java36
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/SerialEntry.java39
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/SerialField.java16
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/AutoGen.java32
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/AutoGenField.java12
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Boolean.java41
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/ColorField.java21
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/CustomDescription.java12
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/CustomFormat.java17
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/CustomImage.java69
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/CustomName.java18
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/DoubleField.java46
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/DoubleSlider.java48
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Dropdown.java43
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/EnumCycler.java35
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/FloatField.java46
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/FloatSlider.java48
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/FormatTranslation.java25
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/IntField.java41
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/IntSlider.java35
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/ItemField.java17
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Label.java18
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/ListGroup.java60
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/LongField.java41
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/LongSlider.java35
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/MasterTickBox.java26
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/OptionAccess.java35
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/OptionFactory.java40
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/SimpleOptionFactory.java138
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/StringField.java17
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/TickBox.java17
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/serializer/GsonConfigSerializerBuilder.java98
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigClassHandlerImpl.java274
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigFieldImpl.java75
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/FieldBackedBinding.java22
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/ReflectionFieldAccess.java49
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/AutoGenUtils.java54
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/BooleanImpl.java25
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/ColorFieldImpl.java19
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DoubleFieldImpl.java32
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DoubleSliderImpl.java33
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DropdownImpl.java19
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/EmptyCustomImageFactory.java17
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/EnumCyclerImpl.java42
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/FloatFieldImpl.java32
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/FloatSliderImpl.java33
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/IntFieldImpl.java28
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/IntSliderImpl.java29
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/ItemFieldImpl.java17
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/LabelImpl.java16
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/ListGroupImpl.java102
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/LongFieldImpl.java28
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/LongSliderImpl.java29
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/MasterTickBoxImpl.java25
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/OptionAccessImpl.java44
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/OptionFactoryRegistry.java64
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/StringFieldImpl.java16
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/TickBoxImpl.java16
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/YACLAutoGenException.java11
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java275
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);
+ }
+ }
+}