diff options
48 files changed, 1089 insertions, 410 deletions
@@ -1,7 +1,3 @@ -<center><div align="center"> - - - # YetAnotherConfigLib  @@ -13,42 +9,76 @@ [](https://ko-fi.com/isxander) -Yet Another Config Lib, like, what were you expecting? - -[](https://bisecthosting.com/xander) +A mod designed to fit a modder's needs for client-side configuration. -</div></center> +[](https://bisecthosting.com/xander) ## Why does this mod even exist? This mod was made to fill a hole in this area of Fabric modding. The existing main config libraries don't achieve what I want from them: -- **[Cloth Config API](https://modrinth.com/mod/cloth-config)**:<br/>**It's stale.** The developer of cloth has clarified that they are likely not going to add any more features. They don't want to touch it. ([citation](https://user-images.githubusercontent.com/43245524/206530322-3ae46008-5356-468e-9a73-63b859364d4e.png)) -- **[SpruceUI](https://github.com/LambdAurora/SpruceUI)**:<br/>**It isn't designed for configuration.** In this essence the design feels cluttered. Further details available in [this issue](https://github.com/isXander/Zoomify/issues/85). -- **[MidnightLib](https://modrinth.com/mod/midnightlib)**:<br/>**It has cosmetics among other utilities.** It may not be large but some players (including me) wouldn't want cosmetics out of nowhere. -- **[OwoLib](https://modrinth.com/mod/owo-lib)**:<br/>**It's content focused.** It does a lot of other things as well as config, adding to the size. +- **[Cloth Config API](https://modrinth.com/mod/cloth-config)**: **It's stale.** The developer of cloth has clarified that they are likely not going to add any more features. They don't want to touch it. ([citation](https://user-images.githubusercontent.com/43245524/206530322-3ae46008-5356-468e-9a73-63b859364d4e.png)) +- **[SpruceUI](https://github.com/LambdAurora/SpruceUI)**: **It isn't designed for configuration.** In this essence the design feels cluttered. Further details available in [this issue](https://github.com/isXander/Zoomify/issues/85). +- **[MidnightLib](https://modrinth.com/mod/midnightlib)**: **It has cosmetics among other utilities.** It may not be large but some players (including me) wouldn't want cosmetics out of nowhere. +- **[OwoLib](https://modrinth.com/mod/owo-lib)**: **It's content focused.** It does a lot of other things as well as config, adding to the size. As you can see, there's sadly a drawback with all of them and this is where YetAnotherConfigLib comes in. -## How is YACL better? +## Why use YACL? -YACL has the favour of hindsight. Whilst developing this fresh library, I can make sure that it does everything right: +### Features -- **Client sided library.** YACL is built for client mods only, making it a smaller size. -- **Easy API.** YACL takes inspiration from [Sodium's](https://modrinth.com/mod/sodium) internal configuration library. -- **It's styled to fit in Minecraft.** YACL's GUI is designed to fit right in. +YACL has a ton of configuration features: -## Usage +- Custom control widgets + - Create your own unique "controller" if the default set does not suit your needs +- Rich descriptions + - Clickable & hoverable text, powered by vanilla's Text component system + - WebP (including animated) image previews + - Custom rich-renderable section to replace image +- Multiple controllers for the same type: + - Sliders or fields for numbers + - Dropdowns, cyclers, or raw text fields for strings + - Tickboxes or ON/OFF text display for booleans + - ...and more! +- Fully-featured color picker +- Accessible with full compatibility for keyboard control (optimised for Controlify usage) +- High organisation with tabs (categories) and collapsable groups +- Built-in serialization/deserialization techniques so you can skip the error-prone config code +- Full alternative Kotlin DSL -[The wiki](https://github.com/isXander/YetAnotherConfigLib/wiki/Usage) contains a full documentation on how to use YACL. +### Version support + +YACL supports a huge amount of versions, all kept up to date and released simultaneously, thanks to the amazing +[Stonecutter](https://stonecutter.kikugie.dev/) build tool. -## Screenshots +| Version | Fabric | Forge | NeoForge | +|-------------------------|--------|-------|----------| +| **1.20.1** | ✅ | ✅ | ⛔ | +| **1.20.4** | ✅ | ⛔ | ✅ | +| **1.20.5 - 1.20.6** | ✅ | ⛔ | ✅ | +| **1.21.0 - 1.21.1** | ✅ | ⛔ | ✅ | +| **1.21.2** (RC version) | ✅ | ⛔ | ⛔ | -<center><div align="center"> +That's **9** different targets, supporting versions that are 500+ days old! - +_**Note**: Forge (LexForge) is not and will not be supported past 1.20.1. +If you're a developer, please port to NeoForge. +If you're a user, you may find that all your favourite mods have already done so._ -</div></center> +Each is a separate build, so make sure your users get the correct YACL version for their target of choice. + +### Design + +YACL is designed to fit right in with the vanilla GUI aesthetic, and will evolve with Minecraft itself. Take a look at +the gallery to see how even in all the currently supported versions, YACL's design looks different to fit in with +vanilla GUI updates. + + + +## Usage for Developers + +[The wiki](https://github.com/isXander/YetAnotherConfigLib/wiki/Usage) contains a full documentation on how to use YACL. ## License diff --git a/build.gradle.kts b/build.gradle.kts index 5d5cb9a..2d8162d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,7 +20,7 @@ val isForgeLike = isNeoforge || isForge val mcVersion = findProperty("mcVersion").toString() group = "dev.isxander" -val versionWithoutMC = "3.5.0" +val versionWithoutMC = "3.6.0" version = "$versionWithoutMC+${stonecutter.current.project}" val snapshotVer = "${grgit.branch.current().name.replace('/', '.')}-SNAPSHOT" diff --git a/changelog.md b/changelog.md index ff542b8..45b034a 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,7 @@ -# YetAnotherConfigLib 3.5.0 +# YetAnotherConfigLib 3.6.0 This build supports the following versions: +- Fabric 1.21.2 - Fabric 1.20.1 - Fabric 1.20.4 - Fabric 1.20.6 (also supports 1.20.5) @@ -10,80 +11,83 @@ This build supports the following versions: - NeoForge 1.20.4 - MinecraftForge 1.20.1 -## *Experimental* Codec Config +## State Managers -This update brings a new experimental config API that utilises Mojang's Codec for (de)serialization. +Options now no longer hold their state themselves. This job is now delegated to the `StateManager`. + +This change serves to allow multiple options to share the same state, but with different controllers, descriptions, +names, etc. + +### Example + +Take an example of how this might be useful: + +You have a long range of gamepad bindings, utilising a custom YACL _controller_. They are sorted alphabetically in YACL. +You want a specific gamepad binding, let's call it 'Jump', to be featured at the top of the list, but also still +appear alphabetically in the list. + +You add a second 'Jump' YACL _option_ and place it at the beginning of the _category_, you assign it an identical +_binding_. + +This appears to work, but you discover a problem: you can modify the featured _option_ just fine, but when you hit +'Save Changes', you see an error in your log 'Option value mismatch' and your option defaults to the previous, now +unchanged value. + +Why is this? Each instance of _`Option`_ holds its own _pending value_. Which is then applied to the _binding_ when you +click save. When you click save, YACL iterates over each option in-order and applies the pending value to the _binding._ +Which leads to the following series of events: + +1. YACL successfully applies the featured 'Jump' option to the binding, as it's first in the list. +2. YACL then finds the non-featured 'Jump' option, which retained the original default value because the + _pending value_ has not changed. From step 1, the binding's value has now changed to something none-default, so now + it sets the binding again to the pending value of THIS option, now the binding is back to its default state. +3. YACL finishes saving _options_, and checks over each one again. It checks that the _pending value_ matches the + _binding_. Because the featured 'Jump' option now doesn't match, it creates an error in your log: + 'Option value mismatch' and assigns the _pending value_ to the _binding_ value. + +### Solution + +State managers essentially solve this by moving the _pending value_ into a separate object that can then be given to +multiple _options_. They ensure that each option's controller is kept up to date by emitting events upon the state's +change that controllers then listen to, to keep themselves up to date. + +### Code Examples ```java -public class CodecConfig extends JsonFileCodecConfig/*or*/CodecConfig { - public static final CodecConfig INSTANCE = new CodecConfig(); - - public final ConfigEntry<Integer> myInt = - register("my_int", 0, Codec.INT); - - public final ReadonlyConfigEntry<InnerCodecConfig> myInnerConfig = - register("my_inner_config", InnerCodecConfig.INSTANCE); - - public CodecConfig() { - super(path); - } - - void test() { - loadFromFile(); // load like this - saveToFile(); // save like this - - // or if you just extend CodecConfig instead of JsonFileConfig: - JsonElement element = null; - this.decode(element, JsonOps.INSTANCE); // load - DataResult<JsonElement> encoded = this.encodeStart(JsonOps.INSTANCE); // save - } -} -``` -or in Kotlin... -```kotlin -object CodecConfig : JsonFileCodecConfig(path) { - val myInt by register<Int>(0, Codec.INT) - - val myInnerConfig by register(InnerCodecConfig) - - fun test() { - loadFromFile() - saveToFile() - - // blah blah blah - } -} +StateManager<Boolean> stateManager = StateManager.createSimple(getter, setter, def); + +category.option(Option.<Boolean>createOption() + .name(Component.literal("Sharing Tick Box")) + .stateManager(stateManager) + .controller(TickBoxControllerBuilder::create) + .build()); +category.option(Option.<Boolean>createOption() + .name(Component.literal("Sharing Boolean")) + .stateManager(stateManager) + .controller(BooleanControllerBuilder::create) + .build()); ``` -## Rewritten Kotlin DSL - -Completely rewrote the Kotlin DSL! - -```kotlin -YetAnotherConfigLib("namespace") { - val category by categories.registering { - val option by rootOptions.registering<Int> { - controller = slider(range = 5..10) - binding(::thisProp, default) - - val otherOption by categories["category"]["group"].futureRef<Boolean>() - otherOption.onReady { it.setAvailable(false) } - } - - // translation key is generated automagically - val label by rootOptions.registeringLabel - - val group by groups.registering { - val otherOption = options.register<Boolean>("otherOption") { - controller = tickBox() - } - } - } -} +Here, instead of using a `.binding()`, you use a `.stateManager()`. Now both share and update the same state! + +### Event Changes + +Options that listen to events, `.listener((option, newValue) -> {})`, the syntax has slightly changed. The +aforementioned methods have been deprecated, replaced with `.addListener((option, event) -> {})`, you can retrieve the +new value with `option.pendingValue()`. + +### Consolidation of .instant() behaviour + +Because of this open-ness of how an option's pending value gets applied, users of YACL have a lot more power +on what happens with how an option's state is applied. + +```java +.stateManager(StateManager.createInstant(getter, setter, def)) ``` -## Changes +Is the new way to get instantly applied options. You can also implement `StateManager` yourself. + +## Other Changes -- Fix dropdown controllers erroneously showing their dropdown - Crendgrim -- Make cancel/reset and undo buttons public for accessing -- Add compatibility for 1.21 +- Added support for 1.21.2 +- Label Options now allow change of state, so labels can now be changed dynamically. diff --git a/gradle.properties b/gradle.properties index 79fe14e..eaa637b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,5 @@ org.gradle.jvmargs=-Xmx4G +org.gradle.parallel=true modrinthId=1eAoo2KR curseforgeId=667299 @@ -8,7 +9,7 @@ modId=yet_another_config_lib_v3 modName=YetAnotherConfigLib modDescription=YetAnotherConfigLib (yacl) is just that. A builder-based configuration library for Minecraft. -deps.fabricLoader=0.15.11 +deps.fabricLoader=0.16.7 deps.imageio=3.10.0 deps.quiltParsers=0.2.1 deps.mixinExtras=0.3.5 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6f86915..ce8f04a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Apr 05 20:43:25 CEST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index 35b1b5b..5b12068 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,7 +14,7 @@ pluginManagement { } plugins { - id("dev.kikugie.stonecutter") version "0.4-beta.3" + id("dev.kikugie.stonecutter") version "0.4.5" } extensions.configure<StonecutterSettings> { @@ -27,9 +27,10 @@ extensions.configure<StonecutterSettings> { } } + mc("1.21.2", loaders = listOf("fabric")) + mc("1.21", loaders = listOf("fabric", "neoforge")) mc("1.20.6", loaders = listOf("fabric", "neoforge")) mc("1.20.4", loaders = listOf("fabric", "neoforge")) - mc("1.21", loaders = listOf("fabric", "neoforge")) mc("1.20.1", loaders = listOf("fabric", "forge")) } create(rootProject) diff --git a/src/main/java/dev/isxander/yacl3/api/LabelOption.java b/src/main/java/dev/isxander/yacl3/api/LabelOption.java index a5f015e..16372b0 100644 --- a/src/main/java/dev/isxander/yacl3/api/LabelOption.java +++ b/src/main/java/dev/isxander/yacl3/api/LabelOption.java @@ -26,6 +26,8 @@ public interface LabelOption extends Option<Component> { } interface Builder { + Builder state(@NotNull StateManager<Component> stateManager); + /** * Appends a line to the label */ diff --git a/src/main/java/dev/isxander/yacl3/api/ListOption.java b/src/main/java/dev/isxander/yacl3/api/ListOption.java index 1f4adfa..9103254 100644 --- a/src/main/java/dev/isxander/yacl3/api/ListOption.java +++ b/src/main/java/dev/isxander/yacl3/api/ListOption.java @@ -93,6 +93,8 @@ public interface ListOption<T> extends OptionGroup, Option<List<T>> { */ Builder<T> customController(@NotNull Function<ListOptionEntry<T>, Controller<T>> control); + Builder<T> state(@NotNull StateManager<List<T>> stateManager); + /** * Sets the binding for the option. * Used for default, getter and setter. @@ -159,6 +161,10 @@ public interface ListOption<T> extends OptionGroup, Option<List<T>> { */ Builder<T> collapsed(boolean collapsible); + ListOption.Builder<T> addListener(@NotNull OptionEventListener<List<T>> listener); + + ListOption.Builder<T> addListeners(@NotNull Collection<OptionEventListener<List<T>>> listeners); + /** * Adds a listener to the option. Invoked upon changing any of the list's entries. * diff --git a/src/main/java/dev/isxander/yacl3/api/Option.java b/src/main/java/dev/isxander/yacl3/api/Option.java index 38bd8ca..9190168 100644 --- a/src/main/java/dev/isxander/yacl3/api/Option.java +++ b/src/main/java/dev/isxander/yacl3/api/Option.java @@ -34,12 +34,15 @@ public interface Option<T> { */ @NotNull Controller<T> controller(); + @NotNull StateManager<T> stateManager(); + /** * Binding for the option. * Controls setting, getting and default value. * * @see Binding */ + @Deprecated @NotNull Binding<T> binding(); /** @@ -101,9 +104,12 @@ public interface Option<T> { return true; } + void addEventListener(OptionEventListener<T> listener); + /** * Adds a listener for when the pending value changes */ + @Deprecated void addListener(BiConsumer<Option<T>, T> changedListener); static <T> Builder<T> createBuilder() { @@ -146,6 +152,10 @@ public interface Option<T> { */ Builder<T> description(@NotNull Function<T, OptionDescription> descriptionFunction); + /** + * Supplies a controller for this option. A controller is the GUI control to interact with the option. + * @return this builder + */ Builder<T> controller(@NotNull Function<Option<T>, ControllerBuilder<T>> controllerBuilder); /** @@ -156,9 +166,12 @@ public interface Option<T> { */ Builder<T> customController(@NotNull Function<Option<T>, Controller<T>> control); + Builder<T> stateManager(@NotNull StateManager<T> stateManager); + /** * Sets the binding for the option. * Used for default, getter and setter. + * Under-the-hood, this creates a state manager that is individual to the option, sharing state with no options. * * @see Binding */ @@ -196,12 +209,17 @@ public interface Option<T> { */ Builder<T> flags(@NotNull Collection<? extends OptionFlag> flags); + Builder<T> addListener(@NotNull OptionEventListener<T> listener); + + Builder<T> addListeners(@NotNull Collection<OptionEventListener<T>> listeners); + /** * Instantly invokes the binder's setter when modified in the GUI. * Prevents the user from undoing the change * <p> * Does not support {@link Option#flags()}! */ + @Deprecated Builder<T> instant(boolean instant); /** @@ -209,6 +227,7 @@ public interface Option<T> { * * @see Option#addListener(BiConsumer) */ + @Deprecated Builder<T> listener(@NotNull BiConsumer<Option<T>, T> listener); /** @@ -216,6 +235,7 @@ public interface Option<T> { * * @see Option#addListener(BiConsumer) */ + @Deprecated Builder<T> listeners(@NotNull Collection<BiConsumer<Option<T>, T>> listeners); Option<T> build(); diff --git a/src/main/java/dev/isxander/yacl3/api/OptionEventListener.java b/src/main/java/dev/isxander/yacl3/api/OptionEventListener.java new file mode 100644 index 0000000..c805948 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/OptionEventListener.java @@ -0,0 +1,13 @@ +package dev.isxander.yacl3.api; + +@FunctionalInterface +public interface OptionEventListener<T> { + void onEvent(Option<T> option, Event event); + + enum Event { + INITIAL, + STATE_CHANGE, + AVAILABILITY_CHANGE, + OTHER, + } +} diff --git a/src/main/java/dev/isxander/yacl3/api/StateManager.java b/src/main/java/dev/isxander/yacl3/api/StateManager.java new file mode 100644 index 0000000..07d263e --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/api/StateManager.java @@ -0,0 +1,89 @@ +package dev.isxander.yacl3.api; + +import dev.isxander.yacl3.impl.ImmutableStateManager; +import dev.isxander.yacl3.impl.InstantStateManager; +import dev.isxander.yacl3.impl.SimpleStateManager; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +public interface StateManager<T> { + static <T> StateManager<T> createSimple(Binding<T> binding) { + return new SimpleStateManager<>(binding); + } + + static <T> StateManager<T> createSimple(@NotNull T def, @NotNull Supplier<@NotNull T> getter, @NotNull Consumer<@NotNull T> setter) { + return new SimpleStateManager<>(Binding.generic(def, getter, setter)); + } + + static <T> StateManager<T> createInstant(Binding<T> binding) { + return new InstantStateManager<>(binding); + } + + static <T> StateManager<T> createInstant(@NotNull T def, @NotNull Supplier<@NotNull T> getter, @NotNull Consumer<@NotNull T> setter) { + return new InstantStateManager<>(Binding.generic(def, getter, setter)); + } + + static <T> StateManager<T> createImmutable(@NotNull T value) { + return new ImmutableStateManager<>(value); + } + + /** + * Sets the pending value. + */ + void set(T value); + + /** + * @return the pending value. + */ + T get(); + + /** + * Applies the pending value to the backed binding. + */ + void apply(); + + void resetToDefault(ResetAction action); + + /** + * Essentially "forgets" the pending value and reassigns state as backed by the binding. + */ + void sync(); + + /** + * @return true if the pending value is the same as the backed binding value. + */ + boolean isSynced(); + + /** + * @return true if this state manage will always be synced with the backing binding. + */ + default boolean isAlwaysSynced() { + |
