aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/dev/isxander/yacl3/config/v2/impl
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/dev/isxander/yacl3/config/v2/impl')
-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
28 files changed, 1426 insertions, 0 deletions
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())