From 04fe933f4c24817100f3101f088accf55a621f8a Mon Sep 17 00:00:00 2001 From: isxander Date: Thu, 11 Apr 2024 18:43:06 +0100 Subject: Extremely fragile and broken multiversion build with stonecutter --- src/main/java/dev/isxander/yacl3/api/Binding.java | 64 +++ .../java/dev/isxander/yacl3/api/ButtonOption.java | 55 ++ .../dev/isxander/yacl3/api/ConfigCategory.java | 138 +++++ .../java/dev/isxander/yacl3/api/Controller.java | 28 + .../java/dev/isxander/yacl3/api/LabelOption.java | 41 ++ .../java/dev/isxander/yacl3/api/ListOption.java | 178 +++++++ .../dev/isxander/yacl3/api/ListOptionEntry.java | 18 + .../java/dev/isxander/yacl3/api/NameableEnum.java | 10 + src/main/java/dev/isxander/yacl3/api/Option.java | 223 ++++++++ .../java/dev/isxander/yacl3/api/OptionAddable.java | 51 ++ .../dev/isxander/yacl3/api/OptionDescription.java | 161 ++++++ .../java/dev/isxander/yacl3/api/OptionFlag.java | 23 + .../java/dev/isxander/yacl3/api/OptionGroup.java | 131 +++++ .../isxander/yacl3/api/PlaceholderCategory.java | 55 ++ .../isxander/yacl3/api/YetAnotherConfigLib.java | 113 ++++ .../api/controller/BooleanControllerBuilder.java | 16 + .../api/controller/ColorControllerBuilder.java | 14 + .../yacl3/api/controller/ControllerBuilder.java | 10 + .../controller/CyclingListControllerBuilder.java | 15 + .../controller/DoubleFieldControllerBuilder.java | 10 + .../controller/DoubleSliderControllerBuilder.java | 10 + .../DropdownStringControllerBuilder.java | 18 + .../api/controller/EnumControllerBuilder.java | 12 + .../controller/EnumDropdownControllerBuilder.java | 10 + .../controller/FloatFieldControllerBuilder.java | 10 + .../controller/FloatSliderControllerBuilder.java | 10 + .../controller/IntegerFieldControllerBuilder.java | 10 + .../controller/IntegerSliderControllerBuilder.java | 10 + .../api/controller/ItemControllerBuilder.java | 11 + .../api/controller/LongFieldControllerBuilder.java | 10 + .../controller/LongSliderControllerBuilder.java | 10 + .../controller/NumberFieldControllerBuilder.java | 7 + .../api/controller/SliderControllerBuilder.java | 6 + .../api/controller/StringControllerBuilder.java | 10 + .../api/controller/TickBoxControllerBuilder.java | 10 + .../api/controller/ValueFormattableController.java | 14 + .../yacl3/api/controller/ValueFormatter.java | 7 + .../dev/isxander/yacl3/api/utils/Dimension.java | 33 ++ .../isxander/yacl3/api/utils/MutableDimension.java | 11 + .../dev/isxander/yacl3/api/utils/OptionUtils.java | 39 ++ .../dev/isxander/yacl3/config/ConfigEntry.java | 15 + .../dev/isxander/yacl3/config/ConfigInstance.java | 50 ++ .../isxander/yacl3/config/GsonConfigInstance.java | 259 +++++++++ .../yacl3/config/v2/api/ConfigClassHandler.java | 107 ++++ .../isxander/yacl3/config/v2/api/ConfigField.java | 40 ++ .../yacl3/config/v2/api/ConfigSerializer.java | 64 +++ .../isxander/yacl3/config/v2/api/FieldAccess.java | 14 + .../yacl3/config/v2/api/ReadOnlyFieldAccess.java | 36 ++ .../isxander/yacl3/config/v2/api/SerialEntry.java | 39 ++ .../isxander/yacl3/config/v2/api/SerialField.java | 16 + .../yacl3/config/v2/api/autogen/AutoGen.java | 32 ++ .../yacl3/config/v2/api/autogen/AutoGenField.java | 12 + .../yacl3/config/v2/api/autogen/Boolean.java | 41 ++ .../yacl3/config/v2/api/autogen/ColorField.java | 21 + .../config/v2/api/autogen/CustomDescription.java | 12 + .../yacl3/config/v2/api/autogen/CustomFormat.java | 17 + .../yacl3/config/v2/api/autogen/CustomImage.java | 69 +++ .../yacl3/config/v2/api/autogen/CustomName.java | 18 + .../yacl3/config/v2/api/autogen/DoubleField.java | 46 ++ .../yacl3/config/v2/api/autogen/DoubleSlider.java | 48 ++ .../yacl3/config/v2/api/autogen/Dropdown.java | 43 ++ .../yacl3/config/v2/api/autogen/EnumCycler.java | 35 ++ .../yacl3/config/v2/api/autogen/FloatField.java | 46 ++ .../yacl3/config/v2/api/autogen/FloatSlider.java | 48 ++ .../config/v2/api/autogen/FormatTranslation.java | 25 + .../yacl3/config/v2/api/autogen/IntField.java | 41 ++ .../yacl3/config/v2/api/autogen/IntSlider.java | 35 ++ .../yacl3/config/v2/api/autogen/ItemField.java | 17 + .../yacl3/config/v2/api/autogen/Label.java | 18 + .../yacl3/config/v2/api/autogen/ListGroup.java | 60 +++ .../yacl3/config/v2/api/autogen/LongField.java | 41 ++ .../yacl3/config/v2/api/autogen/LongSlider.java | 35 ++ .../yacl3/config/v2/api/autogen/MasterTickBox.java | 26 + .../yacl3/config/v2/api/autogen/OptionAccess.java | 35 ++ .../yacl3/config/v2/api/autogen/OptionFactory.java | 40 ++ .../config/v2/api/autogen/SimpleOptionFactory.java | 138 +++++ .../yacl3/config/v2/api/autogen/StringField.java | 17 + .../yacl3/config/v2/api/autogen/TickBox.java | 17 + .../serializer/GsonConfigSerializerBuilder.java | 98 ++++ .../config/v2/impl/ConfigClassHandlerImpl.java | 274 ++++++++++ .../yacl3/config/v2/impl/ConfigFieldImpl.java | 75 +++ .../yacl3/config/v2/impl/FieldBackedBinding.java | 22 + .../config/v2/impl/ReflectionFieldAccess.java | 49 ++ .../yacl3/config/v2/impl/autogen/AutoGenUtils.java | 54 ++ .../yacl3/config/v2/impl/autogen/BooleanImpl.java | 25 + .../config/v2/impl/autogen/ColorFieldImpl.java | 19 + .../config/v2/impl/autogen/DoubleFieldImpl.java | 32 ++ .../config/v2/impl/autogen/DoubleSliderImpl.java | 33 ++ .../yacl3/config/v2/impl/autogen/DropdownImpl.java | 19 + .../v2/impl/autogen/EmptyCustomImageFactory.java | 17 + .../config/v2/impl/autogen/EnumCyclerImpl.java | 42 ++ .../config/v2/impl/autogen/FloatFieldImpl.java | 32 ++ .../config/v2/impl/autogen/FloatSliderImpl.java | 33 ++ .../yacl3/config/v2/impl/autogen/IntFieldImpl.java | 28 + .../config/v2/impl/autogen/IntSliderImpl.java | 29 ++ .../config/v2/impl/autogen/ItemFieldImpl.java | 17 + .../yacl3/config/v2/impl/autogen/LabelImpl.java | 16 + .../config/v2/impl/autogen/ListGroupImpl.java | 102 ++++ .../config/v2/impl/autogen/LongFieldImpl.java | 28 + .../config/v2/impl/autogen/LongSliderImpl.java | 29 ++ .../config/v2/impl/autogen/MasterTickBoxImpl.java | 25 + .../config/v2/impl/autogen/OptionAccessImpl.java | 44 ++ .../v2/impl/autogen/OptionFactoryRegistry.java | 64 +++ .../config/v2/impl/autogen/StringFieldImpl.java | 16 + .../yacl3/config/v2/impl/autogen/TickBoxImpl.java | 16 + .../v2/impl/autogen/YACLAutoGenException.java | 11 + .../v2/impl/serializer/GsonConfigSerializer.java | 275 ++++++++++ .../dev/isxander/yacl3/debug/DebugProperties.java | 13 + .../dev/isxander/yacl3/gui/AbstractWidget.java | 100 ++++ .../isxander/yacl3/gui/DescriptionWithName.java | 11 + .../isxander/yacl3/gui/ElementListWidgetExt.java | 274 ++++++++++ .../isxander/yacl3/gui/LowProfileButtonWidget.java | 28 + .../yacl3/gui/OptionDescriptionWidget.java | 222 ++++++++ .../dev/isxander/yacl3/gui/OptionListWidget.java | 578 +++++++++++++++++++++ .../isxander/yacl3/gui/RequireRestartScreen.java | 21 + .../dev/isxander/yacl3/gui/SearchFieldWidget.java | 61 +++ .../isxander/yacl3/gui/TextScaledButtonWidget.java | 34 ++ .../isxander/yacl3/gui/TooltipButtonWidget.java | 21 + .../dev/isxander/yacl3/gui/ValueFormatters.java | 21 + .../java/dev/isxander/yacl3/gui/YACLScreen.java | 426 +++++++++++++++ .../java/dev/isxander/yacl3/gui/YACLTooltip.java | 23 + .../isxander/yacl3/gui/YACLTooltipPositioner.java | 48 ++ .../yacl3/gui/controllers/ActionController.java | 120 +++++ .../yacl3/gui/controllers/BooleanController.java | 164 ++++++ .../yacl3/gui/controllers/ColorController.java | 220 ++++++++ .../yacl3/gui/controllers/ControllerWidget.java | 148 ++++++ .../yacl3/gui/controllers/LabelController.java | 193 +++++++ .../yacl3/gui/controllers/ListEntryWidget.java | 128 +++++ .../yacl3/gui/controllers/TickBoxController.java | 119 +++++ .../cycling/CyclingControllerElement.java | 60 +++ .../controllers/cycling/CyclingListController.java | 86 +++ .../gui/controllers/cycling/EnumController.java | 48 ++ .../controllers/cycling/ICyclingController.java | 38 ++ .../dropdown/AbstractDropdownController.java | 87 ++++ .../AbstractDropdownControllerElement.java | 248 +++++++++ .../dropdown/DropdownStringController.java | 34 ++ .../dropdown/DropdownStringControllerElement.java | 31 ++ .../dropdown/EnumDropdownController.java | 92 ++++ .../dropdown/EnumDropdownControllerElement.java | 25 + .../gui/controllers/dropdown/ItemController.java | 68 +++ .../dropdown/ItemControllerElement.java | 87 ++++ .../yacl3/gui/controllers/package-info.java | 12 + .../controllers/slider/DoubleSliderController.java | 119 +++++ .../controllers/slider/FloatSliderController.java | 119 +++++ .../gui/controllers/slider/ISliderController.java | 54 ++ .../slider/IntegerSliderController.java | 116 +++++ .../controllers/slider/LongSliderController.java | 116 +++++ .../slider/SliderControllerElement.java | 157 ++++++ .../yacl3/gui/controllers/slider/package-info.java | 10 + .../gui/controllers/string/IStringController.java | 44 ++ .../gui/controllers/string/StringController.java | 37 ++ .../string/StringControllerElement.java | 466 +++++++++++++++++ .../string/number/DoubleFieldController.java | 111 ++++ .../string/number/FloatFieldController.java | 111 ++++ .../string/number/IntegerFieldController.java | 111 ++++ .../string/number/LongFieldController.java | 111 ++++ .../string/number/NumberFieldController.java | 80 +++ .../controllers/string/number/package-info.java | 10 + .../isxander/yacl3/gui/image/ImageRenderer.java | 11 + .../yacl3/gui/image/ImageRendererFactory.java | 24 + .../yacl3/gui/image/ImageRendererManager.java | 120 +++++ .../yacl3/gui/image/YACLImageReloadListener.java | 110 ++++ .../image/impl/AnimatedDynamicTextureImage.java | 286 ++++++++++ .../yacl3/gui/image/impl/DynamicTextureImage.java | 72 +++ .../yacl3/gui/image/impl/ResourceTextureImage.java | 56 ++ .../isxander/yacl3/gui/tab/ListHolderWidget.java | 116 +++++ .../yacl3/gui/tab/ScrollableNavigationBar.java | 120 +++++ .../java/dev/isxander/yacl3/gui/tab/TabExt.java | 14 + .../yacl3/gui/utils/ButtonTextureRenderer.java | 34 ++ .../dev/isxander/yacl3/gui/utils/GuiUtils.java | 32 ++ .../yacl3/gui/utils/ItemRegistryHelper.java | 116 +++++ .../isxander/yacl3/gui/utils/UndoRedoHelper.java | 42 ++ .../dev/isxander/yacl3/impl/ButtonOptionImpl.java | 205 ++++++++ .../isxander/yacl3/impl/ConfigCategoryImpl.java | 134 +++++ .../isxander/yacl3/impl/GenericBindingImpl.java | 35 ++ .../yacl3/impl/HiddenNameListOptionEntry.java | 109 ++++ .../dev/isxander/yacl3/impl/LabelOptionImpl.java | 160 ++++++ .../isxander/yacl3/impl/ListOptionEntryImpl.java | 154 ++++++ .../dev/isxander/yacl3/impl/ListOptionImpl.java | 402 ++++++++++++++ .../isxander/yacl3/impl/OptionDescriptionImpl.java | 133 +++++ .../dev/isxander/yacl3/impl/OptionGroupImpl.java | 121 +++++ .../java/dev/isxander/yacl3/impl/OptionImpl.java | 295 +++++++++++ .../yacl3/impl/PlaceholderCategoryImpl.java | 99 ++++ .../java/dev/isxander/yacl3/impl/SafeBinding.java | 29 ++ .../yacl3/impl/YetAnotherConfigLibImpl.java | 122 +++++ .../controller/AbstractControllerBuilderImpl.java | 12 + .../controller/BooleanControllerBuilderImpl.java | 57 ++ .../controller/ColorControllerBuilderImpl.java | 27 + .../CyclingListControllerBuilderImpl.java | 41 ++ .../DoubleFieldControllerBuilderImpl.java | 51 ++ .../DoubleSliderControllerBuilderImpl.java | 44 ++ .../DropdownStringControllerBuilderImpl.java | 49 ++ .../impl/controller/EnumControllerBuilderImpl.java | 42 ++ .../EnumDropdownControllerBuilderImpl.java | 27 + .../FloatFieldControllerBuilderImpl.java | 51 ++ .../FloatSliderControllerBuilderImpl.java | 44 ++ .../IntegerFieldControllerBuilderImpl.java | 51 ++ .../IntegerSliderControllerBuilderImpl.java | 44 ++ .../impl/controller/ItemControllerBuilderImpl.java | 18 + .../controller/LongFieldControllerBuilderImpl.java | 51 ++ .../LongSliderControllerBuilderImpl.java | 44 ++ .../controller/StringControllerBuilderImpl.java | 17 + .../controller/TickBoxControllerBuilderImpl.java | 17 + .../yacl3/impl/utils/DimensionIntegerImpl.java | 115 ++++ .../isxander/yacl3/impl/utils/YACLConstants.java | 8 + .../yacl3/mixin/AbstractSelectionListMixin.java | 25 + .../yacl3/mixin/ContainerEventHandlerMixin.java | 37 ++ .../dev/isxander/yacl3/mixin/MinecraftMixin.java | 16 + .../yacl3/mixin/OptionInstanceAccessor.java | 13 + .../yacl3/mixin/TabNavigationBarAccessor.java | 16 + src/main/java/dev/isxander/yacl3/platform/Env.java | 10 + .../yacl3/platform/PlatformEntrypoint.java | 42 ++ .../dev/isxander/yacl3/platform/YACLPlatform.java | 45 ++ src/main/resources/META-INF/mods.toml | 31 ++ .../assets/yet_another_config_lib/lang/be_by.json | 29 ++ .../assets/yet_another_config_lib/lang/el_gr.json | 23 + .../assets/yet_another_config_lib/lang/en_us.json | 31 ++ .../assets/yet_another_config_lib/lang/et_ee.json | 18 + .../assets/yet_another_config_lib/lang/fr_fr.json | 29 ++ .../assets/yet_another_config_lib/lang/it_it.json | 31 ++ .../assets/yet_another_config_lib/lang/nl_nl.json | 31 ++ .../assets/yet_another_config_lib/lang/pl_pl.json | 23 + .../assets/yet_another_config_lib/lang/pt_br.json | 18 + .../assets/yet_another_config_lib/lang/ru_ru.json | 24 + .../assets/yet_another_config_lib/lang/sl_si.json | 22 + .../assets/yet_another_config_lib/lang/tt_ru.json | 34 ++ .../assets/yet_another_config_lib/lang/zh_cn.json | 29 ++ .../assets/yet_another_config_lib/lang/zh_tw.json | 29 ++ src/main/resources/fabric.mod.json | 38 ++ src/main/resources/pack.mcmeta | 6 + src/main/resources/yacl-128x.png | Bin 0 -> 13813 bytes src/main/resources/yacl-fabric.mixins.json | 11 + src/main/resources/yacl.accesswidener | 12 + src/main/resources/yacl.mixins.json | 14 + .../dev/isxander/yacl3/test/AutogenConfigTest.java | 130 +++++ .../java/dev/isxander/yacl3/test/ConfigTest.java | 78 +++ .../java/dev/isxander/yacl3/test/Entrypoint.java | 23 + .../java/dev/isxander/yacl3/test/GuiTest.java | 453 ++++++++++++++++ .../yacl3/test/mixin/TitleScreenMixin.java | 25 + src/testmod/resources/META-INF/mods.toml | 25 + .../yacl3/textures/reach-around-placement.webp | Bin 0 -> 14840 bytes .../resources/assets/yacl3/textures/sample-1.webp | Bin 0 -> 10474 bytes .../resources/assets/yacl3/textures/sample-2.webp | Bin 0 -> 22308 bytes .../resources/assets/yacl3/textures/sample-3.webp | Bin 0 -> 17078 bytes .../resources/assets/yacl3/textures/sample-4.webp | Bin 0 -> 20772 bytes .../resources/assets/yacl3/textures/sample-5.webp | Bin 0 -> 11166 bytes src/testmod/resources/fabric.mod.json | 33 ++ src/testmod/resources/pack.mcmeta | 6 + src/testmod/resources/yacl-test.mixins.json | 11 + 249 files changed, 15902 insertions(+) create mode 100644 src/main/java/dev/isxander/yacl3/api/Binding.java create mode 100644 src/main/java/dev/isxander/yacl3/api/ButtonOption.java create mode 100644 src/main/java/dev/isxander/yacl3/api/ConfigCategory.java create mode 100644 src/main/java/dev/isxander/yacl3/api/Controller.java create mode 100644 src/main/java/dev/isxander/yacl3/api/LabelOption.java create mode 100644 src/main/java/dev/isxander/yacl3/api/ListOption.java create mode 100644 src/main/java/dev/isxander/yacl3/api/ListOptionEntry.java create mode 100644 src/main/java/dev/isxander/yacl3/api/NameableEnum.java create mode 100644 src/main/java/dev/isxander/yacl3/api/Option.java create mode 100644 src/main/java/dev/isxander/yacl3/api/OptionAddable.java create mode 100644 src/main/java/dev/isxander/yacl3/api/OptionDescription.java create mode 100644 src/main/java/dev/isxander/yacl3/api/OptionFlag.java create mode 100644 src/main/java/dev/isxander/yacl3/api/OptionGroup.java create mode 100644 src/main/java/dev/isxander/yacl3/api/PlaceholderCategory.java create mode 100644 src/main/java/dev/isxander/yacl3/api/YetAnotherConfigLib.java create mode 100644 src/main/java/dev/isxander/yacl3/api/controller/BooleanControllerBuilder.java create mode 100644 src/main/java/dev/isxander/yacl3/api/controller/ColorControllerBuilder.java create mode 100644 src/main/java/dev/isxander/yacl3/api/controller/ControllerBuilder.java create mode 100644 src/main/java/dev/isxander/yacl3/api/controller/CyclingListControllerBuilder.java create mode 100644 src/main/java/dev/isxander/yacl3/api/controller/DoubleFieldControllerBuilder.java create mode 100644 src/main/java/dev/isxander/yacl3/api/controller/DoubleSliderControllerBuilder.java create mode 100644 src/main/java/dev/isxander/yacl3/api/controller/DropdownStringControllerBuilder.java create mode 100644 src/main/java/dev/isxander/yacl3/api/controller/EnumControllerBuilder.java create mode 100644 src/main/java/dev/isxander/yacl3/api/controller/EnumDropdownControllerBuilder.java create mode 100644 src/main/java/dev/isxander/yacl3/api/controller/FloatFieldControllerBuilder.java create mode 100644 src/main/java/dev/isxander/yacl3/api/controller/FloatSliderControllerBuilder.java create mode 100644 src/main/java/dev/isxander/yacl3/api/controller/IntegerFieldControllerBuilder.java create mode 100644 src/main/java/dev/isxander/yacl3/api/controller/IntegerSliderControllerBuilder.java create mode 100644 src/main/java/dev/isxander/yacl3/api/controller/ItemControllerBuilder.java create mode 100644 src/main/java/dev/isxander/yacl3/api/controller/LongFieldControllerBuilder.java create mode 100644 src/main/java/dev/isxander/yacl3/api/controller/LongSliderControllerBuilder.java create mode 100644 src/main/java/dev/isxander/yacl3/api/controller/NumberFieldControllerBuilder.java create mode 100644 src/main/java/dev/isxander/yacl3/api/controller/SliderControllerBuilder.java create mode 100644 src/main/java/dev/isxander/yacl3/api/controller/StringControllerBuilder.java create mode 100644 src/main/java/dev/isxander/yacl3/api/controller/TickBoxControllerBuilder.java create mode 100644 src/main/java/dev/isxander/yacl3/api/controller/ValueFormattableController.java create mode 100644 src/main/java/dev/isxander/yacl3/api/controller/ValueFormatter.java create mode 100644 src/main/java/dev/isxander/yacl3/api/utils/Dimension.java create mode 100644 src/main/java/dev/isxander/yacl3/api/utils/MutableDimension.java create mode 100644 src/main/java/dev/isxander/yacl3/api/utils/OptionUtils.java create mode 100644 src/main/java/dev/isxander/yacl3/config/ConfigEntry.java create mode 100644 src/main/java/dev/isxander/yacl3/config/ConfigInstance.java create mode 100644 src/main/java/dev/isxander/yacl3/config/GsonConfigInstance.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/ConfigClassHandler.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/ConfigField.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/ConfigSerializer.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/FieldAccess.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/ReadOnlyFieldAccess.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/SerialEntry.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/SerialField.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/AutoGen.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/AutoGenField.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Boolean.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/ColorField.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/CustomDescription.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/CustomFormat.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/CustomImage.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/CustomName.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/DoubleField.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/DoubleSlider.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Dropdown.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/EnumCycler.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/FloatField.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/FloatSlider.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/FormatTranslation.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/IntField.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/IntSlider.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/ItemField.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Label.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/ListGroup.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/LongField.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/LongSlider.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/MasterTickBox.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/OptionAccess.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/OptionFactory.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/SimpleOptionFactory.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/StringField.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/autogen/TickBox.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/api/serializer/GsonConfigSerializerBuilder.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigClassHandlerImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/ConfigFieldImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/FieldBackedBinding.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/ReflectionFieldAccess.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/AutoGenUtils.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/BooleanImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/ColorFieldImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DoubleFieldImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DoubleSliderImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DropdownImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/EmptyCustomImageFactory.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/EnumCyclerImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/FloatFieldImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/FloatSliderImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/IntFieldImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/IntSliderImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/ItemFieldImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/LabelImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/ListGroupImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/LongFieldImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/LongSliderImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/MasterTickBoxImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/OptionAccessImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/OptionFactoryRegistry.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/StringFieldImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/TickBoxImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/YACLAutoGenException.java create mode 100644 src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java create mode 100644 src/main/java/dev/isxander/yacl3/debug/DebugProperties.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/AbstractWidget.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/DescriptionWithName.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/ElementListWidgetExt.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/LowProfileButtonWidget.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/OptionDescriptionWidget.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/OptionListWidget.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/RequireRestartScreen.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/SearchFieldWidget.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/TextScaledButtonWidget.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/TooltipButtonWidget.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/ValueFormatters.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/YACLScreen.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/YACLTooltip.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/YACLTooltipPositioner.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/ActionController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/BooleanController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/ColorController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/ControllerWidget.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/LabelController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/ListEntryWidget.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/TickBoxController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/cycling/CyclingControllerElement.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/cycling/CyclingListController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/cycling/EnumController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/cycling/ICyclingController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/AbstractDropdownController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/AbstractDropdownControllerElement.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownStringController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownStringControllerElement.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/EnumDropdownController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/EnumDropdownControllerElement.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemControllerElement.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/package-info.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/slider/DoubleSliderController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/slider/FloatSliderController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/slider/ISliderController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/slider/IntegerSliderController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/slider/LongSliderController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/slider/SliderControllerElement.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/slider/package-info.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/string/IStringController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/string/StringController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/string/StringControllerElement.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/string/number/DoubleFieldController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/string/number/FloatFieldController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/string/number/IntegerFieldController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/string/number/LongFieldController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/string/number/NumberFieldController.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/controllers/string/number/package-info.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/image/ImageRenderer.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/image/ImageRendererFactory.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/image/ImageRendererManager.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/image/YACLImageReloadListener.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/image/impl/AnimatedDynamicTextureImage.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/image/impl/DynamicTextureImage.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/image/impl/ResourceTextureImage.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/tab/ListHolderWidget.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/tab/ScrollableNavigationBar.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/tab/TabExt.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/utils/ButtonTextureRenderer.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/utils/GuiUtils.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/utils/ItemRegistryHelper.java create mode 100644 src/main/java/dev/isxander/yacl3/gui/utils/UndoRedoHelper.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/ButtonOptionImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/ConfigCategoryImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/GenericBindingImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/HiddenNameListOptionEntry.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/LabelOptionImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/ListOptionEntryImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/ListOptionImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/OptionDescriptionImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/OptionGroupImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/OptionImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/PlaceholderCategoryImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/SafeBinding.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/YetAnotherConfigLibImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/controller/AbstractControllerBuilderImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/controller/BooleanControllerBuilderImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/controller/ColorControllerBuilderImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/controller/CyclingListControllerBuilderImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/controller/DoubleFieldControllerBuilderImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/controller/DoubleSliderControllerBuilderImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/controller/DropdownStringControllerBuilderImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/controller/EnumControllerBuilderImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/controller/EnumDropdownControllerBuilderImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/controller/FloatFieldControllerBuilderImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/controller/FloatSliderControllerBuilderImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/controller/IntegerFieldControllerBuilderImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/controller/IntegerSliderControllerBuilderImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/controller/ItemControllerBuilderImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/controller/LongFieldControllerBuilderImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/controller/LongSliderControllerBuilderImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/controller/StringControllerBuilderImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/controller/TickBoxControllerBuilderImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/utils/DimensionIntegerImpl.java create mode 100644 src/main/java/dev/isxander/yacl3/impl/utils/YACLConstants.java create mode 100644 src/main/java/dev/isxander/yacl3/mixin/AbstractSelectionListMixin.java create mode 100644 src/main/java/dev/isxander/yacl3/mixin/ContainerEventHandlerMixin.java create mode 100644 src/main/java/dev/isxander/yacl3/mixin/MinecraftMixin.java create mode 100644 src/main/java/dev/isxander/yacl3/mixin/OptionInstanceAccessor.java create mode 100644 src/main/java/dev/isxander/yacl3/mixin/TabNavigationBarAccessor.java create mode 100644 src/main/java/dev/isxander/yacl3/platform/Env.java create mode 100644 src/main/java/dev/isxander/yacl3/platform/PlatformEntrypoint.java create mode 100644 src/main/java/dev/isxander/yacl3/platform/YACLPlatform.java create mode 100644 src/main/resources/META-INF/mods.toml create mode 100644 src/main/resources/assets/yet_another_config_lib/lang/be_by.json create mode 100644 src/main/resources/assets/yet_another_config_lib/lang/el_gr.json create mode 100644 src/main/resources/assets/yet_another_config_lib/lang/en_us.json create mode 100644 src/main/resources/assets/yet_another_config_lib/lang/et_ee.json create mode 100644 src/main/resources/assets/yet_another_config_lib/lang/fr_fr.json create mode 100644 src/main/resources/assets/yet_another_config_lib/lang/it_it.json create mode 100644 src/main/resources/assets/yet_another_config_lib/lang/nl_nl.json create mode 100644 src/main/resources/assets/yet_another_config_lib/lang/pl_pl.json create mode 100644 src/main/resources/assets/yet_another_config_lib/lang/pt_br.json create mode 100644 src/main/resources/assets/yet_another_config_lib/lang/ru_ru.json create mode 100644 src/main/resources/assets/yet_another_config_lib/lang/sl_si.json create mode 100644 src/main/resources/assets/yet_another_config_lib/lang/tt_ru.json create mode 100644 src/main/resources/assets/yet_another_config_lib/lang/zh_cn.json create mode 100644 src/main/resources/assets/yet_another_config_lib/lang/zh_tw.json create mode 100644 src/main/resources/fabric.mod.json create mode 100644 src/main/resources/pack.mcmeta create mode 100644 src/main/resources/yacl-128x.png create mode 100644 src/main/resources/yacl-fabric.mixins.json create mode 100644 src/main/resources/yacl.accesswidener create mode 100644 src/main/resources/yacl.mixins.json create mode 100644 src/testmod/java/dev/isxander/yacl3/test/AutogenConfigTest.java create mode 100644 src/testmod/java/dev/isxander/yacl3/test/ConfigTest.java create mode 100644 src/testmod/java/dev/isxander/yacl3/test/Entrypoint.java create mode 100644 src/testmod/java/dev/isxander/yacl3/test/GuiTest.java create mode 100644 src/testmod/java/dev/isxander/yacl3/test/mixin/TitleScreenMixin.java create mode 100644 src/testmod/resources/META-INF/mods.toml create mode 100644 src/testmod/resources/assets/yacl3/textures/reach-around-placement.webp create mode 100644 src/testmod/resources/assets/yacl3/textures/sample-1.webp create mode 100644 src/testmod/resources/assets/yacl3/textures/sample-2.webp create mode 100644 src/testmod/resources/assets/yacl3/textures/sample-3.webp create mode 100644 src/testmod/resources/assets/yacl3/textures/sample-4.webp create mode 100644 src/testmod/resources/assets/yacl3/textures/sample-5.webp create mode 100644 src/testmod/resources/fabric.mod.json create mode 100644 src/testmod/resources/pack.mcmeta create mode 100644 src/testmod/resources/yacl-test.mixins.json (limited to 'src') diff --git a/src/main/java/dev/isxander/yacl3/api/Binding.java b/src/main/java/dev/isxander/yacl3/api/Binding.java new file mode 100644 index 0000000..f41b78b --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/Binding.java @@ -0,0 +1,64 @@ +package dev.isxander.yacl3.api; + +import dev.isxander.yacl3.impl.GenericBindingImpl; +import dev.isxander.yacl3.mixin.OptionInstanceAccessor; +import net.minecraft.client.OptionInstance; +import org.apache.commons.lang3.Validate; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Controls modifying the bound option. + * Provides the default value, a setter and a getter. + */ +public interface Binding { + void setValue(T value); + + T getValue(); + + T defaultValue(); + + /** + * Creates a generic binding. + * + * @param def default value of the option, used to reset + * @param getter should return the current value of the option + * @param setter should set the option to the supplied value + */ + static Binding generic(T def, Supplier getter, Consumer setter) { + Validate.notNull(def, "`def` must not be null"); + Validate.notNull(getter, "`getter` must not be null"); + Validate.notNull(setter, "`setter` must not be null"); + + return new GenericBindingImpl<>(def, getter, setter); + } + + /** + * Creates a {@link Binding} for Minecraft's {@link OptionInstance} + */ + static Binding minecraft(OptionInstance minecraftOption) { + Validate.notNull(minecraftOption, "`minecraftOption` must not be null"); + + return new GenericBindingImpl<>( + ((OptionInstanceAccessor) (Object) minecraftOption).getInitialValue(), + minecraftOption::get, + minecraftOption::set + ); + } + + /** + * Creates an immutable binding that has no default and cannot be modified. + * + * @param value the value for the binding + */ + static Binding immutable(T value) { + Validate.notNull(value, "`value` must not be null"); + + return new GenericBindingImpl<>( + value, + () -> value, + changed -> {} + ); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/ButtonOption.java b/src/main/java/dev/isxander/yacl3/api/ButtonOption.java new file mode 100644 index 0000000..4f53dd4 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/ButtonOption.java @@ -0,0 +1,55 @@ +package dev.isxander.yacl3.api; + +import dev.isxander.yacl3.gui.YACLScreen; +import dev.isxander.yacl3.impl.ButtonOptionImpl; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +public interface ButtonOption extends Option> { + /** + * Action to be executed upon button press + */ + BiConsumer action(); + + static Builder createBuilder() { + return new ButtonOptionImpl.BuilderImpl(); + } + + interface Builder { + /** + * Sets the name to be used by the option. + * + * @see Option#name() + */ + Builder name(@NotNull Component name); + + /** + * Sets the button text to be displayed next to the name. + */ + Builder text(@NotNull Component text); + + Builder description(@NotNull OptionDescription description); + + Builder action(@NotNull BiConsumer action); + + /** + * Action to be executed upon button press + * + * @see ButtonOption#action() + */ + @Deprecated + Builder action(@NotNull Consumer action); + + /** + * Sets if the option can be configured + * + * @see Option#available() + */ + Builder available(boolean available); + + ButtonOption build(); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/ConfigCategory.java b/src/main/java/dev/isxander/yacl3/api/ConfigCategory.java new file mode 100644 index 0000000..b3d68fc --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/ConfigCategory.java @@ -0,0 +1,138 @@ +package dev.isxander.yacl3.api; + +import com.google.common.collect.ImmutableList; +import dev.isxander.yacl3.impl.ConfigCategoryImpl; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.function.Supplier; + +/** + * Separates {@link Option}s or {@link OptionGroup}s into multiple distinct sections. + * Served to a user as a button in the left column, + * upon pressing, the options list is filled with options contained within this category. + */ +public interface ConfigCategory { + /** + * Name of category, displayed as a button on the left column. + */ + @NotNull Component name(); + + /** + * Gets every {@link OptionGroup} in this category. + */ + @NotNull ImmutableList groups(); + + /** + * Tooltip (or description) of the category. + * Rendered on hover. + */ + @NotNull Component tooltip(); + + /** + * Creates a builder to construct a {@link ConfigCategory} + */ + static Builder createBuilder() { + return new ConfigCategoryImpl.BuilderImpl(); + } + + interface Builder extends OptionAddable { + /** + * Sets name of the category + * + * @see ConfigCategory#name() + */ + Builder name(@NotNull Component name); + + /** + * Adds an option to the root group of the category. + * To add to another group, use {@link Builder#group(OptionGroup)}. + * To construct an option, use {@link Option#createBuilder()} + * + * @see ConfigCategory#groups() + * @see OptionGroup#isRoot() + */ + @Override + Builder option(@NotNull Option option); + + /** + * Adds an option to the root group of the category. + * To add to another group, use {@link Builder#group(OptionGroup)}. + * To construct an option, use {@link Option#createBuilder()} + * + * @param optionSupplier to be called to initialise the option. called immediately + * @return this + */ + @Override + default Builder option(@NotNull Supplier<@NotNull Option> optionSupplier) { + OptionAddable.super.option(optionSupplier); + return this; + } + + /** + * Adds an option to the root group of the category if a condition is met. + * To add to another group, use {@link Builder#group(OptionGroup)}. + * To construct an option, use {@link Option#createBuilder()} + * + * @param condition only if true is the option added + * @return this + */ + @Override + default Builder optionIf(boolean condition, @NotNull Option option) { + OptionAddable.super.optionIf(condition, option); + return this; + } + + /** + * Adds an option to the root group of the category if a condition is met. + * To add to another group, use {@link Builder#group(OptionGroup)}. + * To construct an option, use {@link Option#createBuilder()} + * + * @param condition only if true is the option added + * @param optionSupplier to be called to initialise the option. called immediately only if condition is true + * @return this + */ + @Override + default Builder optionIf(boolean condition, @NotNull Supplier<@NotNull Option> optionSupplier) { + OptionAddable.super.optionIf(condition, optionSupplier); + return this; + } + + /** + * Adds multiple options to the root group of the category. + * To add to another group, use {@link Builder#groups(Collection)}. + * To construct an option, use {@link Option#createBuilder()} + * + * @see ConfigCategory#groups() + * @see OptionGroup#isRoot() + */ + @Override + Builder options(@NotNull Collection> options); + + /** + * Adds an option group. + * To add an option to the root group, use {@link Builder#option(Option)} + * To construct a group, use {@link OptionGroup#createBuilder()} + */ + Builder group(@NotNull OptionGroup group); + + /** + * Adds multiple option groups. + * To add multiple options to the root group, use {@link Builder#options(Collection)} + * To construct a group, use {@link OptionGroup#createBuilder()} + */ + Builder groups(@NotNull Collection groups); + + /** + * Sets the tooltip to be used by the category. + * Can be invoked twice to append more lines. + * No need to wrap the text yourself, the gui does this itself. + * + * @param tooltips text lines - merged with a new-line on {@link Builder#build()}. + */ + Builder tooltip(@NotNull Component... tooltips); + + ConfigCategory build(); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/Controller.java b/src/main/java/dev/isxander/yacl3/api/Controller.java new file mode 100644 index 0000000..25e4465 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/Controller.java @@ -0,0 +1,28 @@ +package dev.isxander.yacl3.api; + +import dev.isxander.yacl3.api.utils.Dimension; +import dev.isxander.yacl3.gui.AbstractWidget; +import dev.isxander.yacl3.gui.YACLScreen; +import net.minecraft.network.chat.Component; + +/** + * Provides a widget to control the option. + */ +public interface Controller { + /** + * Gets the dedicated {@link Option} for this controller + */ + Option option(); + + /** + * Gets the formatted value based on {@link Option#pendingValue()} + */ + Component formatValue(); + + /** + * Provides a widget to display + * + * @param screen parent screen + */ + AbstractWidget provideWidget(YACLScreen screen, Dimension widgetDimension); +} diff --git a/src/main/java/dev/isxander/yacl3/api/LabelOption.java b/src/main/java/dev/isxander/yacl3/api/LabelOption.java new file mode 100644 index 0000000..a5f015e --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/LabelOption.java @@ -0,0 +1,41 @@ +package dev.isxander.yacl3.api; + +import dev.isxander.yacl3.impl.LabelOptionImpl; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; + +/** + * A label option is an easier way of creating a label with a {@link dev.isxander.yacl3.gui.controllers.LabelController}. + * This option is immutable and cannot be disabled. Tooltips are supported through + * {@link Component} styling. + */ +public interface LabelOption extends Option { + @NotNull Component label(); + + /** + * Creates a new label option with the given label, skipping a builder for ease. + */ + static LabelOption create(@NotNull Component label) { + return new LabelOptionImpl(label); + } + + static Builder createBuilder() { + return new LabelOptionImpl.BuilderImpl(); + } + + interface Builder { + /** + * Appends a line to the label + */ + Builder line(@NotNull Component line); + + /** + * Appends multiple lines to the label + */ + Builder lines(@NotNull Collection lines); + + LabelOption build(); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/ListOption.java b/src/main/java/dev/isxander/yacl3/api/ListOption.java new file mode 100644 index 0000000..1f4adfa --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/ListOption.java @@ -0,0 +1,178 @@ +package dev.isxander.yacl3.api; + +import com.google.common.collect.ImmutableList; +import dev.isxander.yacl3.api.controller.ControllerBuilder; +import dev.isxander.yacl3.impl.ListOptionImpl; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * A list option that takes form as an option group for UX. + * You add this option through {@link ConfigCategory.Builder#group(OptionGroup)}. Do NOT add as an option. + * Users can add remove and reshuffle a list type. You can use any controller you wish, there are no dedicated + * controllers for list types. List options do not manipulate your list but get and set the list with a + * regular binding for simplicity. + * + * You may apply option flags like a normal option and collapse like a normal group, it is a merge of them both. + * Methods in this interface marked with {@link ApiStatus.Internal} should not be used, and could be subject to + * change at any time + * @param + */ +public interface ListOption extends OptionGroup, Option> { + @Override + @NotNull ImmutableList> options(); + + @ApiStatus.Internal + int numberOfEntries(); + + @ApiStatus.Internal + int maximumNumberOfEntries(); + + @ApiStatus.Internal + int minimumNumberOfEntries(); + + @ApiStatus.Internal + ListOptionEntry insertNewEntry(); + + @ApiStatus.Internal + void insertEntry(int index, ListOptionEntry entry); + + @ApiStatus.Internal + int indexOf(ListOptionEntry entry); + + @ApiStatus.Internal + void removeEntry(ListOptionEntry entry); + + @ApiStatus.Internal + void addRefreshListener(Runnable changedListener); + + static Builder createBuilder() { + return new ListOptionImpl.BuilderImpl<>(); + } + + @Deprecated + static Builder createBuilder(Class typeClass) { + return createBuilder(); + } + + interface Builder { + /** + * Sets name of the list, for UX purposes, a name should always be given, + * but isn't enforced. + * + * @see ListOption#name() + */ + Builder name(@NotNull Component name); + + Builder description(@NotNull OptionDescription description); + + /** + * Sets the value that is used when creating new entries + */ + Builder initial(@NotNull Supplier initialValue); + + /** + * Sets the value that is used when creating new entries + */ + Builder initial(@NotNull T initialValue); + + Builder controller(@NotNull Function, ControllerBuilder> controller); + + /** + * Sets the controller for the option. + * This is how you interact and change the options. + * + * @see dev.isxander.yacl3.gui.controllers + */ + Builder customController(@NotNull Function, Controller> control); + + /** + * Sets the binding for the option. + * Used for default, getter and setter. + * + * @see Binding + */ + Builder binding(@NotNull Binding> binding); + + /** + * Sets the binding for the option. + * Shorthand of {@link Binding#generic(Object, Supplier, Consumer)} + * + * @param def default value of the option, used to reset + * @param getter should return the current value of the option + * @param setter should set the option to the supplied value + * @see Binding + */ + Builder binding(@NotNull List def, @NotNull Supplier<@NotNull List> getter, @NotNull Consumer<@NotNull List> setter); + + /** + * Sets if the option can be configured + * + * @see Option#available() + */ + Builder available(boolean available); + + /** + * Sets a minimum size for the list. Once this size is reached, + * no further entries may be removed. + */ + Builder minimumNumberOfEntries(int number); + + /** + * Sets a maximum size for the list. Once this size is reached, + * no further entries may be added. + */ + Builder maximumNumberOfEntries(int number); + + /** + * Dictates if new entries should be added to the end of the list + * rather than the top. + */ + Builder insertEntriesAtEnd(boolean insertAtEnd); + + /** + * Adds a flag to the option. + * Upon applying changes, all flags are executed. + * {@link Option#flags()} + */ + Builder flag(@NotNull OptionFlag... flag); + + /** + * Adds a flag to the option. + * Upon applying changes, all flags are executed. + * {@link Option#flags()} + */ + Builder flags(@NotNull Collection flags); + + /** + * Dictates if the group should be collapsed by default. + * If not set, it will not be collapsed by default. + * + * @see OptionGroup#collapsed() + */ + Builder collapsed(boolean collapsible); + + /** + * Adds a listener to the option. Invoked upon changing any of the list's entries. + * + * @see Option#addListener(BiConsumer) + */ + ListOption.Builder listener(@NotNull BiConsumer>, List> listener); + + /** + * Adds multiple listeners to the option. Invoked upon changing of any of the list's entries. + * + * @see Option#addListener(BiConsumer) + */ + ListOption.Builder listeners(@NotNull Collection>, List>> listeners); + + ListOption build(); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/ListOptionEntry.java b/src/main/java/dev/isxander/yacl3/api/ListOptionEntry.java new file mode 100644 index 0000000..23ec657 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/ListOptionEntry.java @@ -0,0 +1,18 @@ +package dev.isxander.yacl3.api; + +import com.google.common.collect.ImmutableSet; +import org.jetbrains.annotations.NotNull; + +public interface ListOptionEntry extends Option { + ListOption parentGroup(); + + @Override + default @NotNull ImmutableSet flags() { + return parentGroup().flags(); + } + + @Override + default boolean available() { + return parentGroup().available(); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/NameableEnum.java b/src/main/java/dev/isxander/yacl3/api/NameableEnum.java new file mode 100644 index 0000000..425623f --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/NameableEnum.java @@ -0,0 +1,10 @@ +package dev.isxander.yacl3.api; + +import net.minecraft.network.chat.Component; + +/** + * Used for the default value formatter of {@link dev.isxander.yacl3.gui.controllers.cycling.EnumController} and {@link dev.isxander.yacl3.gui.controllers.dropdown.EnumDropdownController} + */ +public interface NameableEnum { + Component getDisplayName(); +} diff --git a/src/main/java/dev/isxander/yacl3/api/Option.java b/src/main/java/dev/isxander/yacl3/api/Option.java new file mode 100644 index 0000000..38bd8ca --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/Option.java @@ -0,0 +1,223 @@ +package dev.isxander.yacl3.api; + +import com.google.common.collect.ImmutableSet; +import dev.isxander.yacl3.api.controller.ControllerBuilder; +import dev.isxander.yacl3.impl.OptionImpl; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +public interface Option { + /** + * Name of the option + */ + @NotNull Component name(); + + @NotNull OptionDescription description(); + + /** + * Tooltip (or description) of the option. + * Rendered on hover. + */ + @Deprecated + @NotNull Component tooltip(); + + /** + * Widget provider for a type of option. + * + * @see dev.isxander.yacl3.gui.controllers + */ + @NotNull Controller controller(); + + /** + * Binding for the option. + * Controls setting, getting and default value. + * + * @see Binding + */ + @NotNull Binding binding(); + + /** + * If the option can be configured + */ + boolean available(); + + /** + * Sets if the option can be configured after being built + * + * @see Option#available() + */ + void setAvailable(boolean available); + + /** + * Tasks that needs to be executed upon applying changes. + */ + @NotNull ImmutableSet flags(); + + /** + * Checks if the pending value is not equal to the current set value + */ + boolean changed(); + + /** + * Value in the GUI, ready to set the actual bound value or be undone. + */ + @NotNull T pendingValue(); + + /** + * Sets the pending value + */ + void requestSet(@NotNull T value); + + /** + * Applies the pending value to the bound value. + * Cannot be undone. + * + * @return if there were changes to apply {@link Option#changed()} + */ + boolean applyValue(); + + /** + * Sets the pending value to the bound value. + */ + void forgetPendingValue(); + + /** + * Sets the pending value to the default bound value. + */ + void requestSetDefault(); + + /** + * Checks if the current pending value is equal to its default value + */ + boolean isPendingValueDefault(); + + default boolean canResetToDefault() { + return true; + } + + /** + * Adds a listener for when the pending value changes + */ + void addListener(BiConsumer, T> changedListener); + + static Builder createBuilder() { + return new OptionImpl.BuilderImpl<>(); + } + + /** + * Creates a builder to construct an {@link Option} + * + * @param type of the option's value + * @param typeClass used to capture the type + */ + @Deprecated + static Builder createBuilder(Class typeClass) { + return createBuilder(); + } + + interface Builder { + /** + * Sets the name to be used by the option. + * + * @see Option#name() + */ + Builder name(@NotNull Component name); + + /** + * Sets the description to be used by the option. + * @see OptionDescription + * @param description the static description. + * @return this builder + */ + Builder description(@NotNull OptionDescription description); + + /** + * Sets the function to get the description by the option's current value. + * + * @see OptionDescription + * @param descriptionFunction the function to get the description by the option's current value. + * @return this builder + */ + Builder description(@NotNull Function descriptionFunction); + + Builder controller(@NotNull Function, ControllerBuilder> controllerBuilder); + + /** + * Sets the controller for the option. + * This is how you interact and change the options. + * + * @see dev.isxander.yacl3.gui.controllers + */ + Builder customController(@NotNull Function, Controller> control); + + /** + * Sets the binding for the option. + * Used for default, getter and setter. + * + * @see Binding + */ + Builder binding(@NotNull Binding binding); + + /** + * Sets the binding for the option. + * Shorthand of {@link Binding#generic(Object, Supplier, Consumer)} + * + * @param def default value of the option, used to reset + * @param getter should return the current value of the option + * @param setter should set the option to the supplied value + * @see Binding + */ + Builder binding(@NotNull T def, @NotNull Supplier<@NotNull T> getter, @NotNull Consumer<@NotNull T> setter); + + /** + * Sets if the option can be configured + * + * @see Option#available() + */ + Builder available(boolean available); + + /** + * Adds a flag to the option. + * Upon applying changes, all flags are executed. + * {@link Option#flags()} + */ + Builder flag(@NotNull OptionFlag... flag); + + /** + * Adds a flag to the option. + * Upon applying changes, all flags are executed. + * {@link Option#flags()} + */ + Builder flags(@NotNull Collection flags); + + /** + * Instantly invokes the binder's setter when modified in the GUI. + * Prevents the user from undoing the change + *

+ * Does not support {@link Option#flags()}! + */ + Builder instant(boolean instant); + + /** + * Adds a listener to the option. Invoked upon changing the pending value. + * + * @see Option#addListener(BiConsumer) + */ + Builder listener(@NotNull BiConsumer, T> listener); + + /** + * Adds multiple listeners to the option. Invoked upon changing the pending value. + * + * @see Option#addListener(BiConsumer) + */ + Builder listeners(@NotNull Collection, T>> listeners); + + Option build(); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/OptionAddable.java b/src/main/java/dev/isxander/yacl3/api/OptionAddable.java new file mode 100644 index 0000000..606e8ca --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/OptionAddable.java @@ -0,0 +1,51 @@ +package dev.isxander.yacl3.api; + +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.function.Supplier; + +public interface OptionAddable { + /** + * Adds an option to an abstract builder. + * To construct an option, use {@link Option#createBuilder()} + */ + OptionAddable option(@NotNull Option option); + + /** + * Adds an option to an abstract builder. + * To construct an option, use {@link Option#createBuilder()} + * @param optionSupplier to be called to initialise the option. called immediately + */ + default OptionAddable option(@NotNull Supplier<@NotNull Option> optionSupplier) { + return option(optionSupplier.get()); + } + + /** + * Adds an option to an abstract builder if a condition is met. + * To construct an option, use {@link Option#createBuilder()} + * @param condition only if true is the option added + * @param option the option to add + * @return this + */ + default OptionAddable optionIf(boolean condition, @NotNull Option option) { + return condition ? option(option) : this; + } + + /** + * Adds an option to an abstract builder if a condition is met. + * To construct an option, use {@link Option#createBuilder()} + * @param condition only if true is the option added + * @param optionSupplier to be called to initialise the option. called immediately only if condition is true + * @return this + */ + default OptionAddable optionIf(boolean condition, @NotNull Supplier<@NotNull Option> optionSupplier) { + return condition ? option(optionSupplier) : this; + } + + /** + * Adds multiple options to an abstract builder. + * To construct an option, use {@link Option#createBuilder()} + */ + OptionAddable options(@NotNull Collection> options); +} diff --git a/src/main/java/dev/isxander/yacl3/api/OptionDescription.java b/src/main/java/dev/isxander/yacl3/api/OptionDescription.java new file mode 100644 index 0000000..7336379 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/OptionDescription.java @@ -0,0 +1,161 @@ +package dev.isxander.yacl3.api; + +import dev.isxander.yacl3.gui.image.ImageRenderer; +import dev.isxander.yacl3.impl.OptionDescriptionImpl; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; + +import java.nio.file.Path; +import java.util.Collection; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +/** + * Provides all information for the description panel in the GUI. + * This provides no functional benefit, and is purely for UX. + */ +public interface OptionDescription { + /** + * The description of the option, this is automatically wrapped and supports all styling, + * including {@link net.minecraft.network.chat.ClickEvent}s and {@link net.minecraft.network.chat.HoverEvent}s. + * @return The description of the option, with all lines merged with \n. + */ + Component text(); + + /** + * The image to display with the description. If the Optional is empty, no image has been provided. + * Usually, the image renderers are constructed asynchronously, so this method returns a {@link CompletableFuture}. + *

+ * Image renderers are cached throughout the whole lifecycle of the game, and should not be generated more than once + * per image. See {@link ImageRenderer#getOrMakeAsync(ResourceLocation, Supplier)} for implementation details. + */ + CompletableFuture> image(); + + /** + * @return a new builder for an {@link OptionDescription}. + */ + static Builder createBuilder() { + return new OptionDescriptionImpl.BuilderImpl(); + } + + static OptionDescription of(Component... description) { + return createBuilder().text(description).build(); + } + + OptionDescription EMPTY = new OptionDescriptionImpl(CommonComponents.EMPTY, CompletableFuture.completedFuture(Optional.empty())); + + interface Builder { + /** + * Appends lines to the main description of the option. This can be called multiple times. + * On {@link Builder#build()}, the lines are merged with \n. + * @see OptionDescription#text() + * + * @param description the lines to append to the description. + * @return this builder + */ + Builder text(Component... description); + + /** + * Appends lines to the main description of the option. This can be called multiple times. + * On {@link Builder#build()}, the lines are merged with \n. + * @see OptionDescription#text() + * + * @param lines the lines to append to the description. + * @return this builder + */ + Builder text(Collection lines); + + /** + * Sets a static image to display with the description. This is backed by a regular minecraft resource + * in your mod's /assets folder. + * + * @param image the location of the image to display from the resource manager + * @param width the width of the texture + * @param height the height of the texture + * @return this builder + */ + Builder image(ResourceLocation image, int width, int height); + + /** + * Sets a static image to display with the description. This is backed by a regular minecraft resource + * in your mod's /assets folder. This overload method allows you to specify a subsection of the texture to render. + * + * @param image the location of the image to display from the resource manager + * @param u the u coordinate + * @param v the v coordinate + * @param width the width of the subsection + * @param height the height of the subsection + * @param textureWidth the width of the whole texture file + * @param textureHeight the height of whole texture file + * @return this builder + */ + Builder image(ResourceLocation image, float u, float v, int width, int height, int textureWidth, int textureHeight); + + /** + * Sets a static image to display with the description. This is backed by a file on disk. + * The width and height is automatically determined from the image processing. + * + * @param path the absolute path to the image file + * @param uniqueLocation the unique identifier for the image, used for caching and resource manager registrar + * @return this builder + */ + Builder image(Path path, ResourceLocation uniqueLocation); + + /** + * Sets a static OR ANIMATED webP image to display with the description. This is backed by a regular minecraft resource + * in your mod's /assets folder. + * + * @param image the location of the image to display from the resource manager + * @return this builder + */ + Builder webpImage(ResourceLocation image); + + /** + * Sets a static OR ANIMATED webP image to display with the description. This is backed by a file on disk. + * The width and height is automatically determined from the image processing. + * + * @param path the absolute path to the image file + * @param uniqueLocation the unique identifier for the image, used for caching and resource manager registrar + * @return this builder + */ + Builder webpImage(Path path, ResourceLocation uniqueLocation); + + /** + * Sets a custom image renderer to display with the description. + * This is useful for rendering other abstract things relevant to your mod. + *

+ * However, THIS IS NOT API SAFE! As part of the gui package, things + * may change that could break compatibility with future versions of YACL. + * A helpful utility (that is also not API safe) is {@link ImageRenderer#getOrMakeAsync(ResourceLocation, Supplier)} + * which will cache the image renderer for the whole game lifecycle and construct it asynchronously to the render thread. + * @param image the image renderer to display + * @return this builder + */ + Builder customImage(CompletableFuture> image); + + /** + * Sets an animated GIF image to display with the description. This is backed by a regular minecraft resource + * in your mod's /assets folder. + * + * @param image the location of the image to display from the resource manager + * @return this builder + */ + @Deprecated + Builder gifImage(ResourceLocation image); + + /** + * Sets an animated GIF image to display with the description. This is backed by a file on disk. + * The width and height is automatically determined from the image processing. + * + * @param path the absolute path to the image file + * @param uniqueLocation the unique identifier for the image, used for caching and resource manager registrar + * @return this builder + */ + @Deprecated + Builder gifImage(Path path, ResourceLocation uniqueLocation); + + OptionDescription build(); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/OptionFlag.java b/src/main/java/dev/isxander/yacl3/api/OptionFlag.java new file mode 100644 index 0000000..6f35495 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/OptionFlag.java @@ -0,0 +1,23 @@ +package dev.isxander.yacl3.api; + +import dev.isxander.yacl3.gui.RequireRestartScreen; +import net.minecraft.client.Minecraft; + +import java.util.function.Consumer; + +/** + * Code that is executed upon certain options being applied. + * Each flag is executed only once per save, no matter the amount of options with the flag. + */ +@FunctionalInterface +public interface OptionFlag extends Consumer { + /** Warns the user that a game restart is required for the changes to take effect */ + OptionFlag GAME_RESTART = client -> client.setScreen(new RequireRestartScreen(client.screen)); + + /** Reloads chunks upon applying (F3+A) */ + OptionFlag RELOAD_CHUNKS = client -> client.levelRenderer.allChanged(); + + OptionFlag WORLD_RENDER_UPDATE = client -> client.levelRenderer.needsUpdate(); + + OptionFlag ASSET_RELOAD = Minecraft::delayTextureReload; +} diff --git a/src/main/java/dev/isxander/yacl3/api/OptionGroup.java b/src/main/java/dev/isxander/yacl3/api/OptionGroup.java new file mode 100644 index 0000000..8f183b9 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/OptionGroup.java @@ -0,0 +1,131 @@ +package dev.isxander.yacl3.api; + +import com.google.common.collect.ImmutableList; +import dev.isxander.yacl3.impl.OptionGroupImpl; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.function.Supplier; + +/** + * Serves as a separator between multiple chunks of options + * that may be too similar or too few to be placed in a separate {@link ConfigCategory}. + * Or maybe you just want your config to feel less dense. + */ +public interface OptionGroup { + /** + * Name of the option group, displayed as a separator in the option lists. + * Can be empty. + */ + Component name(); + + OptionDescription description(); + + /** + * Tooltip displayed on hover. + */ + @Deprecated + Component tooltip(); + + /** + * List of all options in the group + */ + @NotNull ImmutableList> options(); + + /** + * Dictates if the group should be collapsed by default. + */ + boolean collapsed(); + + /** + * Always false when using the {@link Builder} + * used to not render the separator if true + */ + boolean isRoot(); + + /** + * Creates a builder to construct a {@link OptionGroup} + */ + static Builder createBuilder() { + return new OptionGroupImpl.BuilderImpl(); + } + + interface Builder extends OptionAddable { + /** + * Sets name of the group, can be {@link Component#empty()} to just separate options, like sodium. + * + * @see OptionGroup#name() + */ + Builder name(@NotNull Component name); + + Builder description(@NotNull OptionDescription description); + + /** + * Adds an option to group. + * To construct an option, use {@link Option#createBuilder(Class)} + * + * @see OptionGroup#options() + */ + @Override + Builder option(@NotNull Option option); + + /** + * Adds an option to this group. + * To construct an option, use {@link Option#createBuilder()} + * + * @param optionSupplier to be called to initialise the option. called immediately + * @return this + */ + @Override + default Builder option(@NotNull Supplier<@NotNull Option> optionSupplier) { + OptionAddable.super.option(optionSupplier); + return this; + } + + /** + * Adds an option to this group if a condition is met. + * To construct an option, use {@link Option#createBuilder()} + * + * @param condition only if true is the option added + * @return this + */ + @Override + default Builder optionIf(boolean condition, @NotNull Option option) { + OptionAddable.super.optionIf(condition, option); + return this; + } + + /** + * Adds an option to this group if a condition is met. + * To construct an option, use {@link Option#createBuilder()} + * + * @param condition only if true is the option added + * @param optionSupplier to be called to initialise the option. called immediately only if condition is true + * @return this + */ + @Override + default Builder optionIf(boolean condition, @NotNull Supplier<@NotNull Option> optionSupplier) { + OptionAddable.super.optionIf(condition, optionSupplier); + return this; + } + + /** + * Adds multiple options to group. + * To construct an option, use {@link Option#createBuilder()} + * + * @see OptionGroup#options() + */ + @Override + Builder options(@NotNull Collection> options); + + /** + * Dictates if the group should be collapsed by default + * + * @see OptionGroup#collapsed() + */ + Builder collapsed(boolean collapsible); + + OptionGroup build(); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/PlaceholderCategory.java b/src/main/java/dev/isxander/yacl3/api/PlaceholderCategory.java new file mode 100644 index 0000000..6eb82c5 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/PlaceholderCategory.java @@ -0,0 +1,55 @@ +package dev.isxander.yacl3.api; + +import dev.isxander.yacl3.gui.YACLScreen; +import dev.isxander.yacl3.impl.PlaceholderCategoryImpl; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; + +import java.util.function.BiFunction; + +/** + * A placeholder category that actually just opens another screen, + * instead of displaying options. + *

+ * Use of this is discouraged, as it is not very user-friendly and navigating to a placeholder + * tab that opens another screen is not very intuitive, making keyboard navigation impossible. + */ +public interface PlaceholderCategory extends ConfigCategory { + /** + * Function to create a screen to open upon changing to this category + */ + BiFunction screen(); + + static Builder createBuilder() { + return new PlaceholderCategoryImpl.BuilderImpl(); + } + + interface Builder { + /** + * Sets name of the category + * + * @see ConfigCategory#name() + */ + Builder name(@NotNull Component name); + + /** + * Sets the tooltip to be used by the category. + * Can be invoked twice to append more lines. + * No need to wrap the Component yourself, the gui does this itself. + * + * @param tooltips Component lines - merged with a new-line on {@link dev.isxander.yacl3.api.PlaceholderCategory.Builder#build()}. + */ + Builder tooltip(@NotNull Component... tooltips); + + /** + * Screen to open upon selecting this category + * + * @see PlaceholderCategory#screen() + */ + Builder screen(@NotNull BiFunction screenFunction); + + PlaceholderCategory build(); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/YetAnotherConfigLib.java b/src/main/java/dev/isxander/yacl3/api/YetAnotherConfigLib.java new file mode 100644 index 0000000..68e9986 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/YetAnotherConfigLib.java @@ -0,0 +1,113 @@ +package dev.isxander.yacl3.api; + +import com.google.common.collect.ImmutableList; +import dev.isxander.yacl3.config.ConfigInstance; +import dev.isxander.yacl3.config.v2.api.ConfigClassHandler; +import dev.isxander.yacl3.gui.YACLScreen; +import dev.isxander.yacl3.impl.YetAnotherConfigLibImpl; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.function.Consumer; + +/** + * Main class of the mod. + * Contains all data and used to provide a {@link Screen} + */ +public interface YetAnotherConfigLib { + /** + * Title of the GUI. Only used for Minecraft narration. + */ + Component title(); + + /** + * Gets all config categories. + */ + ImmutableList categories(); + + /** + * Ran when changes are saved. Can be used to save config to a file etc. + */ + Runnable saveFunction(); + + /** + * Ran every time the YACL screen initialises. Can be paired with FAPI to add custom widgets. + */ + Consumer initConsumer(); + + /** + * Generates a Screen to display based on this instance. + * + * @param parent parent screen to open once closed + */ + Screen generateScreen(@Nullable Screen parent); + + /** + * Creates a builder to construct YACL + */ + static Builder createBuilder() { + return new YetAnotherConfigLibImpl.BuilderImpl(); + } + + static YetAnotherConfigLib create(ConfigClassHandler configHandler, ConfigBackedBuilder builder) { + return builder.build(configHandler.defaults(), configHandler.instance(), createBuilder().save(configHandler::save)).build(); + } + + /** + * Creates an instance using a {@link ConfigInstance} which autofills the save() builder method. + * This also takes an easy functional interface that provides defaults and config to help build YACL bindings. + */ + @Deprecated + static YetAnotherConfigLib create(ConfigInstance configInstance, ConfigBackedBuilder builder) { + return builder.build(configInstance.getDefaults(), configInstance.getConfig(), createBuilder().save(configInstance::save)).build(); + } + + interface Builder { + /** + * Sets title of GUI for Minecraft narration + * + * @see YetAnotherConfigLib#title() + */ + Builder title(@NotNull Component title); + + /** + * Adds a new category. + * To create a category you need to use {@link ConfigCategory#createBuilder()} + * + * @see YetAnotherConfigLib#categories() + */ + Builder category(@NotNull ConfigCategory category); + + /** + * Adds multiple categories at once. + * To create a category you need to use {@link ConfigCategory#createBuilder()} + * + * @see YetAnotherConfigLib#categories() + */ + Builder categories(@NotNull Collection categories); + + /** + * Used to define a save function for when user clicks the Save Changes button + * + * @see YetAnotherConfigLib#saveFunction() + */ + Builder save(@NotNull Runnable saveFunction); + + /** + * Defines a consumer that is accepted every time the YACL screen initialises + * + * @see YetAnotherConfigLib#initConsumer() + */ + Builder screenInit(@NotNull Consumer initConsumer); + + YetAnotherConfigLib build(); + } + + @FunctionalInterface + interface ConfigBackedBuilder { + YetAnotherConfigLib.Builder build(T defaults, T config, YetAnotherConfigLib.Builder builder); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/controller/BooleanControllerBuilder.java b/src/main/java/dev/isxander/yacl3/api/controller/BooleanControllerBuilder.java new file mode 100644 index 0000000..88f9a77 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/controller/BooleanControllerBuilder.java @@ -0,0 +1,16 @@ +package dev.isxander.yacl3.api.controller; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.impl.controller.BooleanControllerBuilderImpl; + +public interface BooleanControllerBuilder extends ValueFormattableController { + BooleanControllerBuilder coloured(boolean coloured); + + BooleanControllerBuilder onOffFormatter(); + BooleanControllerBuilder yesNoFormatter(); + BooleanControllerBuilder trueFalseFormatter(); + + static BooleanControllerBuilder create(Option option) { + return new BooleanControllerBuilderImpl(option); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/controller/ColorControllerBuilder.java b/src/main/java/dev/isxander/yacl3/api/controller/ColorControllerBuilder.java new file mode 100644 index 0000000..8e442ff --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/controller/ColorControllerBuilder.java @@ -0,0 +1,14 @@ +package dev.isxander.yacl3.api.controller; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.impl.controller.ColorControllerBuilderImpl; + +import java.awt.Color; + +public interface ColorControllerBuilder extends ControllerBuilder { + ColorControllerBuilder allowAlpha(boolean allowAlpha); + + static ColorControllerBuilder create(Option option) { + return new ColorControllerBuilderImpl(option); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/controller/ControllerBuilder.java b/src/main/java/dev/isxander/yacl3/api/controller/ControllerBuilder.java new file mode 100644 index 0000000..bbd49a7 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/controller/ControllerBuilder.java @@ -0,0 +1,10 @@ +package dev.isxander.yacl3.api.controller; + +import dev.isxander.yacl3.api.Controller; +import org.jetbrains.annotations.ApiStatus; + +@FunctionalInterface +public interface ControllerBuilder { + @ApiStatus.Internal + Controller build(); +} diff --git a/src/main/java/dev/isxander/yacl3/api/controller/CyclingListControllerBuilder.java b/src/main/java/dev/isxander/yacl3/api/controller/CyclingListControllerBuilder.java new file mode 100644 index 0000000..8c9ea91 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/controller/CyclingListControllerBuilder.java @@ -0,0 +1,15 @@ +package dev.isxander.yacl3.api.controller; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.impl.controller.CyclingListControllerBuilderImpl; + +public interface CyclingListControllerBuilder extends ValueFormattableController> { + @SuppressWarnings("unchecked") + CyclingListControllerBuilder values(T... values); + + CyclingListControllerBuilder values(Iterable values); + + static CyclingListControllerBuilder create(Option option) { + return new CyclingListControllerBuilderImpl<>(option); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/controller/DoubleFieldControllerBuilder.java b/src/main/java/dev/isxander/yacl3/api/controller/DoubleFieldControllerBuilder.java new file mode 100644 index 0000000..db4af94 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/controller/DoubleFieldControllerBuilder.java @@ -0,0 +1,10 @@ +package dev.isxander.yacl3.api.controller; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.impl.controller.DoubleFieldControllerBuilderImpl; + +public interface DoubleFieldControllerBuilder extends NumberFieldControllerBuilder { + static DoubleFieldControllerBuilder create(Option option) { + return new DoubleFieldControllerBuilderImpl(option); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/controller/DoubleSliderControllerBuilder.java b/src/main/java/dev/isxander/yacl3/api/controller/DoubleSliderControllerBuilder.java new file mode 100644 index 0000000..7e4b6f9 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/controller/DoubleSliderControllerBuilder.java @@ -0,0 +1,10 @@ +package dev.isxander.yacl3.api.controller; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.impl.controller.DoubleSliderControllerBuilderImpl; + +public interface DoubleSliderControllerBuilder extends SliderControllerBuilder { + static DoubleSliderControllerBuilder create(Option option) { + return new DoubleSliderControllerBuilderImpl(option); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/controller/DropdownStringControllerBuilder.java b/src/main/java/dev/isxander/yacl3/api/controller/DropdownStringControllerBuilder.java new file mode 100644 index 0000000..3f5fb33 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/controller/DropdownStringControllerBuilder.java @@ -0,0 +1,18 @@ +package dev.isxander.yacl3.api.controller; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.impl.controller.DropdownStringControllerBuilderImpl; + +import java.util.List; + +public interface DropdownStringControllerBuilder extends StringControllerBuilder { + DropdownStringControllerBuilder values(List values); + DropdownStringControllerBuilder values(String... values); + DropdownStringControllerBuilder allowEmptyValue(boolean allowEmptyValue); + DropdownStringControllerBuilder allowAnyValue(boolean allowAnyValue); + + + static DropdownStringControllerBuilder create(Option option) { + return new DropdownStringControllerBuilderImpl(option); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/controller/EnumControllerBuilder.java b/src/main/java/dev/isxander/yacl3/api/controller/EnumControllerBuilder.java new file mode 100644 index 0000000..decb8f9 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/controller/EnumControllerBuilder.java @@ -0,0 +1,12 @@ +package dev.isxander.yacl3.api.controller; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.impl.controller.EnumControllerBuilderImpl; + +public interface EnumControllerBuilder> extends ValueFormattableController> { + EnumControllerBuilder enumClass(Class enumClass); + + static > EnumControllerBuilder create(Option option) { + return new EnumControllerBuilderImpl<>(option); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/controller/EnumDropdownControllerBuilder.java b/src/main/java/dev/isxander/yacl3/api/controller/EnumDropdownControllerBuilder.java new file mode 100644 index 0000000..0814cc6 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/controller/EnumDropdownControllerBuilder.java @@ -0,0 +1,10 @@ +package dev.isxander.yacl3.api.controller; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.impl.controller.EnumDropdownControllerBuilderImpl; + +public interface EnumDropdownControllerBuilder> extends ValueFormattableController> { + static > EnumDropdownControllerBuilder create(Option option) { + return new EnumDropdownControllerBuilderImpl<>(option); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/controller/FloatFieldControllerBuilder.java b/src/main/java/dev/isxander/yacl3/api/controller/FloatFieldControllerBuilder.java new file mode 100644 index 0000000..de81837 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/controller/FloatFieldControllerBuilder.java @@ -0,0 +1,10 @@ +package dev.isxander.yacl3.api.controller; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.impl.controller.FloatFieldControllerBuilderImpl; + +public interface FloatFieldControllerBuilder extends NumberFieldControllerBuilder { + static FloatFieldControllerBuilder create(Option option) { + return new FloatFieldControllerBuilderImpl(option); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/controller/FloatSliderControllerBuilder.java b/src/main/java/dev/isxander/yacl3/api/controller/FloatSliderControllerBuilder.java new file mode 100644 index 0000000..2a04dde --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/controller/FloatSliderControllerBuilder.java @@ -0,0 +1,10 @@ +package dev.isxander.yacl3.api.controller; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.impl.controller.FloatSliderControllerBuilderImpl; + +public interface FloatSliderControllerBuilder extends SliderControllerBuilder { + static FloatSliderControllerBuilder create(Option option) { + return new FloatSliderControllerBuilderImpl(option); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/controller/IntegerFieldControllerBuilder.java b/src/main/java/dev/isxander/yacl3/api/controller/IntegerFieldControllerBuilder.java new file mode 100644 index 0000000..1e31fac --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/controller/IntegerFieldControllerBuilder.java @@ -0,0 +1,10 @@ +package dev.isxander.yacl3.api.controller; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.impl.controller.IntegerFieldControllerBuilderImpl; + +public interface IntegerFieldControllerBuilder extends NumberFieldControllerBuilder { + static IntegerFieldControllerBuilder create(Option option) { + return new IntegerFieldControllerBuilderImpl(option); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/controller/IntegerSliderControllerBuilder.java b/src/main/java/dev/isxander/yacl3/api/controller/IntegerSliderControllerBuilder.java new file mode 100644 index 0000000..11e089a --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/controller/IntegerSliderControllerBuilder.java @@ -0,0 +1,10 @@ +package dev.isxander.yacl3.api.controller; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.impl.controller.IntegerSliderControllerBuilderImpl; + +public interface IntegerSliderControllerBuilder extends SliderControllerBuilder { + static IntegerSliderControllerBuilder create(Option option) { + return new IntegerSliderControllerBuilderImpl(option); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/controller/ItemControllerBuilder.java b/src/main/java/dev/isxander/yacl3/api/controller/ItemControllerBuilder.java new file mode 100644 index 0000000..5a1f5fa --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/controller/ItemControllerBuilder.java @@ -0,0 +1,11 @@ +package dev.isxander.yacl3.api.controller; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.impl.controller.ItemControllerBuilderImpl; +import net.minecraft.world.item.Item; + +public interface ItemControllerBuilder extends ControllerBuilder { + static ItemControllerBuilder create(Option option) { + return new ItemControllerBuilderImpl(option); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/controller/LongFieldControllerBuilder.java b/src/main/java/dev/isxander/yacl3/api/controller/LongFieldControllerBuilder.java new file mode 100644 index 0000000..c53b464 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/controller/LongFieldControllerBuilder.java @@ -0,0 +1,10 @@ +package dev.isxander.yacl3.api.controller; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.impl.controller.LongFieldControllerBuilderImpl; + +public interface LongFieldControllerBuilder extends NumberFieldControllerBuilder { + static LongFieldControllerBuilder create(Option option) { + return new LongFieldControllerBuilderImpl(option); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/controller/LongSliderControllerBuilder.java b/src/main/java/dev/isxander/yacl3/api/controller/LongSliderControllerBuilder.java new file mode 100644 index 0000000..fc09423 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/controller/LongSliderControllerBuilder.java @@ -0,0 +1,10 @@ +package dev.isxander.yacl3.api.controller; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.impl.controller.LongSliderControllerBuilderImpl; + +public interface LongSliderControllerBuilder extends SliderControllerBuilder { + static LongSliderControllerBuilder create(Option option) { + return new LongSliderControllerBuilderImpl(option); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/controller/NumberFieldControllerBuilder.java b/src/main/java/dev/isxander/yacl3/api/controller/NumberFieldControllerBuilder.java new file mode 100644 index 0000000..b5cfa1f --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/controller/NumberFieldControllerBuilder.java @@ -0,0 +1,7 @@ +package dev.isxander.yacl3.api.controller; + +public interface NumberFieldControllerBuilder> extends ValueFormattableController { + B min(T min); + B max(T max); + B range(T min, T max); +} diff --git a/src/main/java/dev/isxander/yacl3/api/controller/SliderControllerBuilder.java b/src/main/java/dev/isxander/yacl3/api/controller/SliderControllerBuilder.java new file mode 100644 index 0000000..2fb3fec --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/controller/SliderControllerBuilder.java @@ -0,0 +1,6 @@ +package dev.isxander.yacl3.api.controller; + +public interface SliderControllerBuilder> extends ValueFormattableController { + B range(T min, T max); + B step(T step); +} diff --git a/src/main/java/dev/isxander/yacl3/api/controller/StringControllerBuilder.java b/src/main/java/dev/isxander/yacl3/api/controller/StringControllerBuilder.java new file mode 100644 index 0000000..5e2f8c6 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/controller/StringControllerBuilder.java @@ -0,0 +1,10 @@ +package dev.isxander.yacl3.api.controller; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.impl.controller.StringControllerBuilderImpl; + +public interface StringControllerBuilder extends ControllerBuilder { + static StringControllerBuilder create(Option option) { + return new StringControllerBuilderImpl(option); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/controller/TickBoxControllerBuilder.java b/src/main/java/dev/isxander/yacl3/api/controller/TickBoxControllerBuilder.java new file mode 100644 index 0000000..71a2762 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/controller/TickBoxControllerBuilder.java @@ -0,0 +1,10 @@ +package dev.isxander.yacl3.api.controller; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.impl.controller.TickBoxControllerBuilderImpl; + +public interface TickBoxControllerBuilder extends ControllerBuilder { + static TickBoxControllerBuilder create(Option option) { + return new TickBoxControllerBuilderImpl(option); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/controller/ValueFormattableController.java b/src/main/java/dev/isxander/yacl3/api/controller/ValueFormattableController.java new file mode 100644 index 0000000..b886318 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/controller/ValueFormattableController.java @@ -0,0 +1,14 @@ +package dev.isxander.yacl3.api.controller; + +import net.minecraft.network.chat.Component; + +import java.util.function.Function; + +public interface ValueFormattableController> extends ControllerBuilder { + B formatValue(ValueFormatter formatter); + + @Deprecated + default B valueFormatter(Function formatter) { + return formatValue(formatter::apply); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/controller/ValueFormatter.java b/src/main/java/dev/isxander/yacl3/api/controller/ValueFormatter.java new file mode 100644 index 0000000..aecaf65 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/controller/ValueFormatter.java @@ -0,0 +1,7 @@ +package dev.isxander.yacl3.api.controller; + +import net.minecraft.network.chat.Component; + +public interface ValueFormatter { + Component format(T value); +} diff --git a/src/main/java/dev/isxander/yacl3/api/utils/Dimension.java b/src/main/java/dev/isxander/yacl3/api/utils/Dimension.java new file mode 100644 index 0000000..ec09238 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/utils/Dimension.java @@ -0,0 +1,33 @@ +package dev.isxander.yacl3.api.utils; + +import dev.isxander.yacl3.impl.utils.DimensionIntegerImpl; + +public interface Dimension { + T x(); + T y(); + + T width(); + T height(); + + T xLimit(); + T yLimit(); + + T centerX(); + T centerY(); + + boolean isPointInside(T x, T y); + + MutableDimension clone(); + + Dimension withX(T x); + Dimension withY(T y); + Dimension withWidth(T width); + Dimension withHeight(T height); + + Dimension moved(T x, T y); + Dimension expanded(T width, T height); + + static MutableDimension ofInt(int x, int y, int width, int height) { + return new DimensionIntegerImpl(x, y, width, height); + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/utils/MutableDimension.java b/src/main/java/dev/isxander/yacl3/api/utils/MutableDimension.java new file mode 100644 index 0000000..f551232 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/utils/MutableDimension.java @@ -0,0 +1,11 @@ +package dev.isxander.yacl3.api.utils; + +public interface MutableDimension extends Dimension { + MutableDimension setX(T x); + MutableDimension setY(T y); + MutableDimension setWidth(T width); + MutableDimension setHeight(T height); + + MutableDimension move(T x, T y); + MutableDimension expand(T width, T height); +} diff --git a/src/main/java/dev/isxander/yacl3/api/utils/OptionUtils.java b/src/main/java/dev/isxander/yacl3/api/utils/OptionUtils.java new file mode 100644 index 0000000..cf33f0f --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/utils/OptionUtils.java @@ -0,0 +1,39 @@ +package dev.isxander.yacl3.api.utils; + +import dev.isxander.yacl3.api.*; + +import java.util.function.Consumer; +import java.util.function.Function; + +public class OptionUtils { + /** + * Consumes all options, ignoring groups and categories. + * When consumer returns true, this function stops iterating. + */ + public static void consumeOptions(YetAnotherConfigLib yacl, Function, Boolean> consumer) { + for (ConfigCategory category : yacl.categories()) { + for (OptionGroup group : category.groups()) { + if (group instanceof ListOption list) { + if (consumer.apply(list)) return; + } else { + for (Option option : group.options()) { + if (consumer.apply(option)) return; + } + } + + } + } + } + + /** + * Consumes all options, ignoring groups and categories. + * + * @see OptionUtils#consumeOptions(YetAnotherConfigLib, Function) + */ + public static void forEachOptions(YetAnotherConfigLib yacl, Consumer> consumer) { + consumeOptions(yacl, (opt) -> { + consumer.accept(opt); + return false; + }); + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/ConfigEntry.java b/src/main/java/dev/isxander/yacl3/config/ConfigEntry.java new file mode 100644 index 0000000..066cf42 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/ConfigEntry.java @@ -0,0 +1,15 @@ +package dev.isxander.yacl3.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @deprecated Use {@link dev.isxander.yacl3.config.v2.api.SerialEntry} instead. + */ +@Deprecated +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface ConfigEntry { +} diff --git a/src/main/java/dev/isxander/yacl3/config/ConfigInstance.java b/src/main/java/dev/isxander/yacl3/config/ConfigInstance.java new file mode 100644 index 0000000..31d4ca2 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/ConfigInstance.java @@ -0,0 +1,50 @@ +package dev.isxander.yacl3.config; + +import java.lang.reflect.InvocationTargetException; + +/** + * Responsible for handing the actual config data type. + * Holds the instance along with a final default instance + * to reference default values for options and should not be changed. + * + * Abstract methods to save and load the class, implementations are responsible for + * how it saves and load. + * + * @param config data type + * @deprecated upgrade to config v2 {@link dev.isxander.yacl3.config.v2.api.ConfigClassHandler} + */ +@Deprecated +public abstract class ConfigInstance { + private final Class configClass; + private final T defaultInstance; + private T instance; + + public ConfigInstance(Class configClass) { + this.configClass = configClass; + + try { + this.defaultInstance = this.instance = configClass.getConstructor().newInstance(); + } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { + throw new IllegalStateException(String.format("Could not create default instance of config for %s. Make sure there is a default constructor!", this.configClass.getSimpleName())); + } + } + + public abstract void save(); + public abstract void load(); + + public T getConfig() { + return this.instance; + } + + protected void setConfig(T instance) { + this.instance = instance; + } + + public T getDefaults() { + return this.defaultInstance; + } + + public Class getConfigClass() { + return this.configClass; + } +} diff --git a/src/main/java/dev/isxander/yacl3/config/GsonConfigInstance.java b/src/main/java/dev/isxander/yacl3/config/GsonConfigInstance.java new file mode 100644 index 0000000..c47afe2 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/config/GsonConfigInstance.java @@ -0,0 +1,259 @@ +package dev.isxander.yacl3.config; + +import com.google.gson.*; +import dev.isxander.yacl3.config.v2.impl.serializer.GsonConfigSerializer; +import dev.isxander.yacl3.gui.utils.ItemRegistryHelper; +import dev.isxander.yacl3.impl.utils.YACLConstants; +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 java.awt.*; +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.function.UnaryOperator; + +/** + * Uses GSON to serialize and deserialize config data from JSON to a file. + *

+ * Only fields annotated with {@link ConfigEntry} 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}. + * + * @param config data type + * @deprecated upgrade to config v2 {@link dev.isxander.yacl3.config.v2.api.ConfigClassHandler} with {@link dev.isxander.yacl3.config.v2.api.serializer.GsonConfigSerializerBuilder} + *

+ * {@code
+ * public class MyConfig {
+ *     public static ConfigClassHandler HANDLER = ConfigClassHandler.createBuilder(MyConfig.class)
+ *             .id(new ResourceLocation("modid", "config"))
+ *             .serializer(config -> GsonConfigSerializerBuilder.create(config)
+ *                     .setPath(FabricLoader.getInstance().getConfigDir().resolve("my_mod.json")
+ *                     .build())
+ *             .build();
+ *
+ *     @SerialEntry public boolean myBoolean = true;
+ * }
+ * }
+ * 
+ */ +@Deprecated +public class GsonConfigInstance extends ConfigInstance { + private final Gson gson; + private final Path path; + + @Deprecated + public GsonConfigInstance(Class configClass, Path path) { + this(configClass, path, new GsonBuilder()); + } + + @Deprecated + public GsonConfigInstance(Class configClass, Path path, Gson gson) { + this(configClass, path, gson.newBuilder()); + } + + @Deprecated + public GsonConfigInstance(Class configClass, Path path, UnaryOperator builder) { + this(configClass, path, builder.apply(new GsonBuilder())); + } + + @Deprecated + public GsonConfigInstance(Class configClass, Path path, GsonBuilder builder) { + super(configClass); + this.path = path; + this.gson = builder + .setExclusionStrategies(new ConfigExclusionStrategy()) + /*? 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 GsonConfigSerializer.StyleTypeAdapter()/*?} else {*//*new Style.Serializer()*//*?}*/) + .registerTypeHierarchyAdapter(Color.class, new ColorTypeAdapter()) + .registerTypeHierarchyAdapter(Item.class, new ItemTypeAdapter()) + .serializeNulls() + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .create(); + } + + private GsonConfigInstance(Class configClass, Path path, Gson gson, boolean fromBuilder) { + super(configClass); + this.path = path; + this.gson = gson; + } + + @Override + public void save() { + try { + YACLConstants.LOGGER.info("Saving {}...", getConfigClass().getSimpleName()); + Files.writeString(path, gson.toJson(getConfig()), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void load() { + try { + if (Files.notExists(path)) { + save(); + return; + } + + YACLConstants.LOGGER.info("Loading {}...", getConfigClass().getSimpleName()); + setConfig(gson.fromJson(Files.readString(path), getConfigClass())); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public Path getPath() { + return this.path; + } + + private static class ConfigExclusionStrategy implements ExclusionStrategy { + @Override + public boolean shouldSkipField(FieldAttributes fieldAttributes) { + return fieldAttributes.getAnnotation(ConfigEntry.class) == null; + } + + @Override + public boolean shouldSkipClass(Class aClass) { + return false; + } + } + + public static class ColorTypeAdapter implements JsonSerializer, JsonDeserializer { + @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, JsonDeserializer { + @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()); + } + } + + /** + * Creates a builder for a GSON config instance. + * @param configClass the config class + * @return a new builder + * @param the config type + */ + public static Builder createBuilder(Class configClass) { + return new Builder<>(configClass); + } + + public static class Builder { + private final Class configClass; + private Path path; + private UnaryOperator 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 GsonConfigSerializer.StyleTypeAdapter()/*?} else {*//*new Style.Serializer()*//*?}*/) + .registerTypeHierarchyAdapter(Color.class, new ColorTypeAdapter()) + .registerTypeHierarchyAdapter(Item.class, new ItemTypeAdapter()); + + private Builder(Class configClass) { + this.configClass = configClass; + } + + /** + * Sets the file path to save and load the config from. + */ + public Builder setPath(Path path) { + this.path = path; + return this; + } + + /** + * Sets the GSON instance to use. Overrides all YACL defaults such as: + *
    + *
  • lower_camel_case field naming policy
  • + *
  • null serialization
  • + *
  • {@link Component}, {@link Style} and {@link Color} type adapters
  • + *
+ * Still respects the exclusion strategy to only serialize {@link ConfigEntry} + * but these can be added to with setExclusionStrategies. + * + * @param gsonBuilder gson builder to use + */ + public Builder overrideGsonBuilder(GsonBuilder gsonBuilder) { + this.gsonBuilder = builder -> gsonBuilder; + return this; + } + + /** + * Sets the GSON instance to use. Overrides all YACL defaults such as: + *
    + *
  • lower_camel_case field naming policy
  • + *
  • null serialization
  • + *
  • {@link Component}, {@link Style} and {@link Color} type adapters
  • + *
+ * Still respects the exclusion strategy to only serialize {@link ConfigEntry} + * but these can be added to with setExclusionStrategies. + * + * @param gson gson instance to be converted to a builder + */ + public Builder overrideGsonBuilder(Gson gson) { + return this.overrideGsonBuilder(gson.newBuilder()); + } + + /** + * Appends extra configuration to a GSON builder. + * This is the intended way to add functionality to the GSON instance. + *

+ * By default, YACL sets the GSON with the following options: + *

    + *
  • lower_camel_case field naming policy
  • + *
  • null serialization
  • + *
  • {@link Component}, {@link Style} and {@link Color} type adapters
  • + *
+ * + * @param gsonBuilder the function to apply to the builder + */ + public Builder appendGsonBuilder(UnaryOperator gsonBuilder) { + UnaryOperator prev = this.gsonBuilder; + this.gsonBuilder = builder -> gsonBuilder.apply(prev.apply(builder)); + return this; + } + + /** + * Builds the config instance. + * @return the built config instance + */ + public GsonConfigInstance build() { + UnaryOperator gsonBuilder = builder -> this.gsonBuilder.apply(builder) + .addSerializationExclusionStrategy(new ConfigExclusionStrategy()) + .addDeserializationExclusionStrategy(new ConfigExclusionStrategy()); + + return new GsonConfigInstance<>(configClass, path, gsonBuilder.apply(new GsonBuilder()).create(), true); + } + } +} 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 the backing config class to be managed + */ +public interface ConfigClassHandler { + /** + * 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 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 serializer(); + + /** + * Creates a builder for a config class. + * + * @param configClass the config class to build + * @param the type of the config class + * @return the builder + */ + static Builder createBuilder(Class configClass) { + return new ConfigClassHandlerImpl.BuilderImpl<>(configClass); + } + + interface Builder { + /** + * The unique identifier of this config handler. + * The namespace should be your modid. + * + * @return this builder + */ + Builder id(ResourceLocation id); + + /** + * The function to create the serializer for this config class. + * + * @return this builder + */ + Builder serializer(Function, ConfigSerializer> serializerFactory); + + ConfigClassHandler 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 the field's type + */ +public interface ConfigField { + /** + * Gets the accessor for the field on the main instance. + * (Accessed through {@link ConfigClassHandler#instance()}) + */ + FieldAccess access(); + + /** + * Gets the accessor for the field on the default instance. + */ + ReadOnlyFieldAccess defaultAccess(); + + /** + * @return the parent config class handler that manages this field. + */ + ConfigClassHandler parent(); + + /** + * The serial entry metadata for this field, if it exists. + */ + Optional serial(); + + /** + * The auto-gen metadata for this field, if it exists. + */ + Optional 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 the config class to be (de)serialized + */ +public abstract class ConfigSerializer { + protected final ConfigClassHandler config; + + public ConfigSerializer(ConfigClassHandler 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, 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 the type of the field + */ +public interface FieldAccess extends ReadOnlyFieldAccess { + /** + * 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 the type of the field + */ +public interface ReadOnlyFieldAccess { + /** + * @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 typeClass(); + + Optional getAnnotation(Class 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 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 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. + *

+ * 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: + *

    + *
  • true: {@code yacl3.config.$configId.$fieldName.fmt.true}
  • + *
  • false: {@code yacl3.config.$configId.$fieldName.fmt.false}
  • + *
+ */ + 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. + *

+ * 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> 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. + *

+ * The following file formats are supported: + *

    + *
  • {@code .png}
  • + *
  • {@code .webp}
  • + *
  • {@code .jpg}, {@code .jpeg}
  • + *
  • {@code .gif} - HIGHLY DISCOURAGED DUE TO LARGE FILE SIZE
  • + *
+ *

+ * If left blank, then {@link CustomImage#factory()} is used. + */ + String value() default ""; + + /** + * The width of the image, in pixels. + * This is only required when using a PNG with {@link CustomImage#value()} + */ + int width() default 0; + + /** + * The width of the image, in pixels. + * This is only required when using a PNG with {@link CustomImage#value()} + */ + 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}. + *

+ * The factory should contain a public, no-args constructor that will be + * invoked via reflection. + * + * @return the class of the factory + */ + Class> factory() default EmptyCustomImageFactory.class; + + interface CustomImageFactory { + CompletableFuture createImage(T value, ConfigField 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. + *

+ * 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. + *

+ * If this is set to {@code -Double.MAX_VALUE}, there will be no minimum. + *

+ * 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. + *

+ * If this is set to {@code Double.MAX_VALUE}, there will be no minimum. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * Only values in this list will be accepted and written to the config + * file, unless {@link #allow()} is set to ${@code ALLOW_ANY}. + *

+ * 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. + *

+ * 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. + *

+ * 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[] 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. + *

+ * 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. + *

+ * If this is set to {@code -Float.MAX_VALUE}, there will be no minimum. + *

+ * 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. + *

+ * If this is set to {@code Float.MAX_VALUE}, there will be no minimum. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * This creates a regular option with a + * {@link dev.isxander.yacl3.api.controller.IntegerFieldControllerBuilder} controller. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * This creates a regular option with a + * {@link dev.isxander.yacl3.api.controller.IntegerSliderControllerBuilder} controller. + *

+ * 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. + *

+ * 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}. + *

+ * 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. + *

+ * 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> 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> 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 provideNewValue(); + } + + interface ControllerFactory { + ControllerBuilder createController(ListGroup annotation, ConfigField> field, OptionAccess storage, Option 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. + *

+ * This creates a regular option with a + * {@link dev.isxander.yacl3.api.controller.LongFieldControllerBuilder} controller. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * This creates a regular option with a + * {@link dev.isxander.yacl3.api.controller.LongSliderControllerBuilder} controller. + *

+ * 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> 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. + *

+ * 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 the annotation type + * @param the option's binding type + */ +public interface OptionFactory { + /** + * 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 createOption(A annotation, ConfigField 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 the type of the annotation + * @param the type of the option's binding + */ + static void register(Class annotationClass, OptionFactory 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 implements OptionFactory { + @Override + public Option createOption(A annotation, ConfigField field, OptionAccess optionAccess) { + Option option = Option.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 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 createController(A annotation, ConfigField field, OptionAccess storage, Option option); + + protected MutableComponent name(A annotation, ConfigField field, OptionAccess storage) { + Optional 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 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 imageOverrideOpt = field.access().getAnnotation(CustomImage.class); + if (imageOverrideOpt.isPresent()) { + CustomImage imageOverride = imageOverrideOpt.get(); + + if (!imageOverride.factory().equals(EmptyCustomImageFactory.class)) { + CustomImage.CustomImageFactory imageFactory; + try { + imageFactory = (CustomImage.CustomImageFactory) 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 field, OptionAccess storage) { + return true; + } + + protected Set flags(A annotation, ConfigField field, OptionAccess storage) { + return Set.of(); + } + + protected void listener(A annotation, ConfigField field, OptionAccess storage, Option option, T value) { + + } + + protected void postInit(A annotation, ConfigField field, OptionAccess storage, Option option) { + + } + + protected String getTranslationKey(ConfigField 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. + *

+ * 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. + *

+ * 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. + *

+ * 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}. + *

+ * Optionally, this can also be written under JSON5 spec, allowing comments. + * + * @param config data type + */ +public interface GsonConfigSerializerBuilder { + static GsonConfigSerializerBuilder create(ConfigClassHandler config) { + return new GsonConfigSerializer.Builder<>(config); + } + + /** + * Sets the file path to save and load the config from. + */ + GsonConfigSerializerBuilder setPath(Path path); + + /** + * Sets the GSON instance to use. Overrides all YACL defaults such as: + *

+ * 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 overrideGsonBuilder(GsonBuilder gsonBuilder); + + /** + * Sets the GSON instance to use. Overrides all YACL defaults such as: + *
    + *
  • lower_camel_case field naming policy
  • + *
  • null serialization
  • + *
  • {@link Component}, {@link Style} and {@link Color} type adapters
  • + *
+ * but these can be added to with setExclusionStrategies. + * + * @param gson gson instance to be converted to a builder + */ + GsonConfigSerializerBuilder overrideGsonBuilder(Gson gson); + + /** + * Appends extra configuration to a GSON builder. + * This is the intended way to add functionality to the GSON instance. + *

+ * By default, YACL sets the GSON with the following options: + *

    + *
  • lower_camel_case field naming policy
  • + *
  • null serialization
  • + *
  • {@link Component}, {@link Style} and {@link Color} type adapters
  • + *
+ * For example, if you wanted to revert YACL's lower_camel_case naming policy, + * you could do the following: + *
+     * {@code
+     * GsonConfigSerializerBuilder.create(config)
+     *         .appendGsonBuilder(builder -> builder.setFieldNamingPolicy(FieldNamingPolicy.IDENTITY))
+     * }
+     * 
+ * + * @param gsonBuilder the function to apply to the builder + */ + GsonConfigSerializerBuilder appendGsonBuilder(UnaryOperator 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 setJson5(boolean json5); + + ConfigSerializer 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 implements ConfigClassHandler { + private final Class configClass; + private final ResourceLocation id; + private final boolean supportsAutoGen; + private final ConfigSerializer serializer; + private final ConfigFieldImpl[] fields; + + private T instance; + private final T defaults; + private final Constructor noArgsConstructor; + + public ConfigClassHandlerImpl(Class configClass, ResourceLocation id, Function, ConfigSerializer> 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 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 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 Option createOption(ConfigField 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 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, 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, 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) field).setFieldAccess((ReflectionFieldAccess) 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 implements Builder { + private final Class configClass; + private ResourceLocation id; + private Function, ConfigSerializer> serializerFactory; + + public BuilderImpl(Class configClass) { + this.configClass = configClass; + } + + @Override + public Builder id(ResourceLocation id) { + this.id = id; + return this; + } + + @Override + public Builder serializer(Function, ConfigSerializer> serializerFactory) { + this.serializerFactory = serializerFactory; + return this; + } + + @Override + public ConfigClassHandler 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 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 implements ConfigField { + private ReflectionFieldAccess field; + private final ReflectionFieldAccess defaultField; + private final ConfigClassHandler parent; + private final Optional serial; + private final Optional autoGen; + + public ConfigFieldImpl(ReflectionFieldAccess field, ReflectionFieldAccess 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 access() { + return field; + } + + public void setFieldAccess(ReflectionFieldAccess field) { + this.field = field; + } + + @Override + public ReflectionFieldAccess defaultAccess() { + return defaultField; + } + + @Override + public ConfigClassHandler parent() { + return parent; + } + + @Override + public Optional serial() { + return this.serial; + } + + @Override + public Optional autoGen() { + return this.autoGen; + } + + private record SerialFieldImpl(String serialName, Optional comment, boolean required, boolean nullable) implements SerialField { + } + private record AutoGenFieldImpl(String category, Optional 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(FieldAccess field, ReadOnlyFieldAccess defaultField) implements Binding { + @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(Field field, Object instance) implements FieldAccess { + @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 typeClass() { + return (Class) field.getType(); + } + + @Override + public Optional getAnnotation(Class 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 void addCustomFormatterToController(ControllerBuilder controller, ReadOnlyFieldAccess field) { + Optional formatter = field.getAnnotation(CustomFormat.class); + Optional 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 typedBuilder = (ValueFormattableController) controller; + + formatter.ifPresent(formatterClass -> { + try { + typedBuilder.formatValue((ValueFormatter) 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 constructNoArgsClass(Class clazz, Supplier constructorNotFoundConsumer, Supplier 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 { + @Override + protected ControllerBuilder createController(Boolean annotation, ConfigField field, OptionAccess storage, Option 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 { + @Override + protected ControllerBuilder createController(ColorField annotation, ConfigField field, OptionAccess storage, Option 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 { + @Override + protected ControllerBuilder createController(DoubleField annotation, ConfigField field, OptionAccess storage, Option 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 { + @Override + protected ControllerBuilder createController(DoubleSlider annotation, ConfigField field, OptionAccess storage, Option 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 { + @Override + protected ControllerBuilder createController(Dropdown annotation, ConfigField field, OptionAccess storage, Option 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 { + + @Override + public CompletableFuture createImage(Object value, ConfigField 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> { + @Override + protected ControllerBuilder> createController(EnumCycler annotation, ConfigField> field, OptionAccess storage, Option> option) { + List> 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 { + @Override + protected ControllerBuilder createController(FloatField annotation, ConfigField field, OptionAccess storage, Option 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 { + @Override + protected ControllerBuilder createController(FloatSlider annotation, ConfigField field, OptionAccess storage, Option 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 { + @Override + protected ControllerBuilder createController(IntField annotation, ConfigField field, OptionAccess storage, Option 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 { + @Override + protected ControllerBuilder createController(IntSlider annotation, ConfigField field, OptionAccess storage, Option 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 { + @Override + protected ControllerBuilder createController(ItemField annotation, ConfigField field, OptionAccess storage, Option 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 { + @Override + public Option createOption(Label annotation, ConfigField 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 implements OptionFactory> { + @Override + public Option> createOption(ListGroup annotation, ConfigField> 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 valueFactory = createValueFactory((Class>) annotation.valueFactory()); + ListGroup.ControllerFactory controllerFactory = createControllerFactory((Class>) annotation.controllerFactory()); + + return ListOption.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> 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 createValueFactory(Class> clazz) { + Constructor> 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 createControllerFactory(Class> clazz) { + Constructor> 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> 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 { + @Override + protected ControllerBuilder createController(LongField annotation, ConfigField field, OptionAccess storage, Option 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 { + @Override + protected ControllerBuilder createController(LongSlider annotation, ConfigField field, OptionAccess storage, Option 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 { + @Override + protected ControllerBuilder createController(MasterTickBox annotation, ConfigField field, OptionAccess storage, Option option) { + return TickBoxControllerBuilder.create(option); + } + + @Override + protected void listener(MasterTickBox annotation, ConfigField field, OptionAccess storage, Option 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> storage = new HashMap<>(); + private final Map>> scheduledOperations = new HashMap<>(); + + @Override + public @Nullable Option getOption(String fieldName) { + return storage.get(fieldName); + } + + @Override + public void scheduleOptionOperation(String fieldName, Consumer> 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> 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, 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 void registerOptionFactory(Class annotation, OptionFactory factory) { + factoryMap.put(annotation, factory); + } + + public static Optional> createOption(Field field, ConfigField 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 factory = (OptionFactory) 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 { + @Override + protected ControllerBuilder createController(StringField annotation, ConfigField field, OptionAccess storage, Option 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 { + @Override + protected ControllerBuilder createController(TickBox annotation, ConfigField field, OptionAccess storage, Option 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 extends ConfigSerializer { + private final Gson gson; + private final Path path; + private final boolean json5; + + private GsonConfigSerializer(ConfigClassHandler 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, 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> fieldMap = Arrays.stream(config.fields()) + .filter(field -> field.serial().isPresent()) + .collect(Collectors.toMap(f -> f.serial().orElseThrow().serialName(), Function.identity())); + Set 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