aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorisXander <xander@isxander.dev>2024-10-19 19:22:45 +0100
committerisXander <xander@isxander.dev>2024-10-19 19:22:45 +0100
commite73a08e6672fb380cab8db71340158969c5ef56b (patch)
treedd08a311f4eff9a91b465ef1854caa1286fc6f9a
parent519ac2fc0e23587defcf4a8259979961d35d0ce2 (diff)
downloadYetAnotherConfigLib-e73a08e6672fb380cab8db71340158969c5ef56b.tar.gz
YetAnotherConfigLib-e73a08e6672fb380cab8db71340158969c5ef56b.tar.bz2
YetAnotherConfigLib-e73a08e6672fb380cab8db71340158969c5ef56b.zip
3.6.0
-rw-r--r--README.md76
-rw-r--r--build.gradle.kts2
-rw-r--r--changelog.md144
-rw-r--r--gradle.properties3
-rw-r--r--gradle/wrapper/gradle-wrapper.properties2
-rw-r--r--settings.gradle.kts5
-rw-r--r--src/main/java/dev/isxander/yacl3/api/LabelOption.java2
-rw-r--r--src/main/java/dev/isxander/yacl3/api/ListOption.java6
-rw-r--r--src/main/java/dev/isxander/yacl3/api/Option.java20
-rw-r--r--src/main/java/dev/isxander/yacl3/api/OptionEventListener.java13
-rw-r--r--src/main/java/dev/isxander/yacl3/api/StateManager.java89
-rw-r--r--src/main/java/dev/isxander/yacl3/config/v2/api/autogen/SimpleOptionFactory.java2
-rw-r--r--src/main/java/dev/isxander/yacl3/gui/AbstractWidget.java49
-rw-r--r--src/main/java/dev/isxander/yacl3/gui/ElementListWidgetExt.java3
-rw-r--r--src/main/java/dev/isxander/yacl3/gui/YACLScreen.java14
-rw-r--r--src/main/java/dev/isxander/yacl3/gui/controllers/ColorController.java8
-rw-r--r--src/main/java/dev/isxander/yacl3/gui/controllers/ColorPickerWidget.java16
-rw-r--r--src/main/java/dev/isxander/yacl3/gui/controllers/LabelController.java2
-rw-r--r--src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownWidget.java19
-rw-r--r--src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemControllerElement.java10
-rw-r--r--src/main/java/dev/isxander/yacl3/gui/controllers/string/StringControllerElement.java7
-rw-r--r--src/main/java/dev/isxander/yacl3/gui/image/YACLImageReloadListener.java10
-rw-r--r--src/main/java/dev/isxander/yacl3/gui/image/impl/AnimatedDynamicTextureImage.java17
-rw-r--r--src/main/java/dev/isxander/yacl3/gui/image/impl/DynamicTextureImage.java3
-rw-r--r--src/main/java/dev/isxander/yacl3/gui/image/impl/ResourceTextureImage.java3
-rw-r--r--src/main/java/dev/isxander/yacl3/gui/utils/GuiUtils.java81
-rw-r--r--src/main/java/dev/isxander/yacl3/gui/utils/ItemRegistryHelper.java9
-rw-r--r--src/main/java/dev/isxander/yacl3/gui/utils/MiscUtil.java14
-rw-r--r--src/main/java/dev/isxander/yacl3/gui/utils/YACLRenderHelper.java2
-rw-r--r--src/main/java/dev/isxander/yacl3/impl/ButtonOptionImpl.java20
-rw-r--r--src/main/java/dev/isxander/yacl3/impl/HiddenNameListOptionEntry.java11
-rw-r--r--src/main/java/dev/isxander/yacl3/impl/ImmutableStateManager.java56
-rw-r--r--src/main/java/dev/isxander/yacl3/impl/InstantStateManager.java68
-rw-r--r--src/main/java/dev/isxander/yacl3/impl/LabelOptionImpl.java160
-rw-r--r--src/main/java/dev/isxander/yacl3/impl/ListOptionEntryImpl.java12
-rw-r--r--src/main/java/dev/isxander/yacl3/impl/ListOptionImpl.java120
-rw-r--r--src/main/java/dev/isxander/yacl3/impl/NotNullBinding.java31
-rw-r--r--src/main/java/dev/isxander/yacl3/impl/OptionImpl.java162
-rw-r--r--src/main/java/dev/isxander/yacl3/impl/ProvidesBindingForDeprecation.java7
-rw-r--r--src/main/java/dev/isxander/yacl3/impl/SafeBinding.java29
-rw-r--r--src/main/java/dev/isxander/yacl3/impl/SelfContainedBinding.java32
-rw-r--r--src/main/java/dev/isxander/yacl3/impl/SimpleStateManager.java65
-rw-r--r--src/testmod/java/dev/isxander/yacl3/test/CodecConfig.java6
-rw-r--r--src/testmod/java/dev/isxander/yacl3/test/GuiTest.java64
-rw-r--r--src/testmod/kotlin/dev/isxander/yacl3/test/DslTest.kt6
-rw-r--r--stonecutter.gradle.kts9
-rw-r--r--versions/1.21-fabric/gradle.properties2
-rw-r--r--versions/1.21.2-fabric/gradle.properties8
48 files changed, 1089 insertions, 410 deletions
diff --git a/README.md b/README.md
index 5e3094d..3ad60b6 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,3 @@
-<center><div align="center">
-
-![](https://raw.githubusercontent.com/isXander/YetAnotherConfigLib/1.19/src/main/resources/yacl-128x.png)
-
# YetAnotherConfigLib
![Enviroment](https://img.shields.io/badge/Enviroment-Client-purple)
@@ -13,42 +9,76 @@
[![Ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/isxander)
-Yet Another Config Lib, like, what were you expecting?
-
-[![](https://www.bisecthosting.com/partners/custom-banners/08bbd3ff-5c0d-4480-8738-de0f070a04dd.png)](https://bisecthosting.com/xander)
+A mod designed to fit a modder's needs for client-side configuration.
-</div></center>
+[![](https://www.bisecthosting.com/partners/custom-banners/08bbd3ff-5c0d-4480-8738-de0f070a04dd.png)](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!
-![java_A3zdbksGkC](https://user-images.githubusercontent.com/43245524/206924832-293b0780-2a8c-4b09-8765-155318d09ed9.png)
+_**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.
+
+![image preview](https://cdn.modrinth.com/data/1eAoo2KR/images/5862570281f5109119c11f21a1bba52b6a2ab17f.png)
+
+## 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() {
+ return false;
+ }
+
+ boolean isDefault();
+
+ void addListener(StateListener<T> stateListener);
+
+ enum ResetAction {
+ BY_OPTION,
+ BY_GLOBAL,
+ }
+
+ interface StateListener<T> {
+ static <T> StateListener<T> noop() {
+ return (oldValue, newValue) -> {};
+ }
+
+ void onStateChange(T oldValue, T newValue);
+
+ default StateListener<T> andThen(StateListener<T> after) {
+ return (oldValue, newValue) -> {
+ this.onStateChange(oldValue, newValue);
+ after.onStateChange(oldValue, newValue);
+ };
+ }
+ }
+}
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
index bbb878d..9adc64f 100644
--- 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
@@ -37,7 +37,7 @@ public abstract class SimpleOptionFactory<A extends Annotation, T> implements Op
})
.available(this.available(annotation, field, optionAccess))
.flags(this.flags(annotation, field, optionAccess))
- .listener((opt, v) -> this.listener(annotation, field, optionAccess, opt, v))
+ .addListener((opt, event) -> this.listener(annotation, field, optionAccess, opt, opt.pendingValue()))
.build();
postInit(annotation, field, optionAccess, option);
diff --git a/src/main/java/dev/isxander/yacl3/gui/AbstractWidget.java b/src/main/java/dev/isxander/yacl3/gui/AbstractWidget.java
index 48f2cc3..111ccdb 100644
--- a/src/main/java/dev/isxander/yacl3/gui/AbstractWidget.java
+++ b/src/main/java/dev/isxander/yacl3/gui/AbstractWidget.java
@@ -2,6 +2,7 @@ package dev.isxander.yacl3.gui;
import com.mojang.blaze3d.vertex.VertexConsumer;
import dev.isxander.yacl3.api.utils.Dimension;
+import dev.isxander.yacl3.gui.utils.GuiUtils;
import dev.isxander.yacl3.gui.utils.YACLRenderHelper;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Font;
@@ -88,23 +89,22 @@ public abstract class AbstractWidget implements GuiEventListener, Renderable, Na
graphics.fill(x1, y1, x1 + width, y2, color);
}
- protected void fillSidewaysGradient(GuiGraphics graphics, int x1, int y1, int x2, int y2, int startColor, int endColor) {
+ protected void fillSidewaysGradient(GuiGraphics graphics, int x1, int y1, int x2, int y2, int startColor, int endColor, VertexConsumer consumer) {
//Fills a gradient, left to right
//Uses practically the same method as the GuiGraphics class, but with the x/y moved
//Has a custom "z" value in case needed for later
- VertexConsumer vertex = graphics.bufferSource().getBuffer(RenderType.gui());
Matrix4f matrix4f = graphics.pose().last().pose();
/*? if >1.20.6 {*/
- vertex.addVertex(matrix4f, x1, y1, 0).setColor(startColor);
- vertex.addVertex(matrix4f, x1, y2, 0).setColor(startColor);
- vertex.addVertex(matrix4f, x2, y2, 0).setColor(endColor);
- vertex.addVertex(matrix4f, x2, y1, 0).setColor(endColor);
+ consumer.addVertex(matrix4f, x1, y1, 0).setColor(startColor);
+ consumer.addVertex(matrix4f, x1, y2, 0).setColor(startColor);
+ consumer.addVertex(matrix4f, x2, y2, 0).setColor(endColor);
+ consumer.addVertex(matrix4f, x2, y1, 0).setColor(endColor);
/*?} else {*/
- /*vertex.vertex(matrix4f, x1, y1, 0).color(startColor).endVertex();
- vertex.vertex(matrix4f, x1, y2, 0).color(startColor).endVertex();
- vertex.vertex(matrix4f, x2, y2, 0).color(endColor).endVertex();
- vertex.vertex(matrix4f, x2, y1, 0).color(endColor).endVertex();
+ /*consumer.vertex(matrix4f, x1, y1, 0).color(startColor).endVertex();
+ consumer.vertex(matrix4f, x1, y2, 0).color(startColor).endVertex();
+ consumer.vertex(matrix4f, x2, y2, 0).color(endColor).endVertex();
+ consumer.vertex(matrix4f, x2, y1, 0).color(endColor).endVertex();
*//*?}*/
}
@@ -115,16 +115,25 @@ public abstract class AbstractWidget implements GuiEventListener, Renderable, Na
Color.cyan.getRGB(), Color.blue.getRGB(), Color.magenta.getRGB(), Color.red.getRGB()}; //all the colors in the gradient
int width = x2 - x1;
int maxColors = colors.length - 1;
- for (int color = 0; color < maxColors; color++) {
- //First checks if the final color is being rendered, if true -> uses x2 int instead of x1
- //if false -> it adds the width divided by the max colors multiplied by the current color plus one to the x1 int
- //the x2 int for the fillSidewaysGradient is the same formula, excluding the additional plus one.
- //The gradient colors is determined by the color int and the color int plus one, which is why red is in the colors array twice
- fillSidewaysGradient(graphics,
- x1 + (width / maxColors * color), y1,
- color == maxColors - 1 ? x2 : x1 + (width / maxColors * (color + 1)), y2,
- colors[color], colors[color + 1]);
- }
+
+ GuiUtils.drawSpecial(graphics, bufferSource -> {
+ VertexConsumer consumer = bufferSource.getBuffer(RenderType.gui());
+
+ for (int color = 0; color < maxColors; color++) {
+ //First checks if the final color is being rendered, if true -> uses x2 int instead of x1
+ //if false -> it adds the width divided by the max colors multiplied by the current color plus one to the x1 int
+ //the x2 int for the fillSidewaysGradient is the same formula, excluding the additional plus one.
+ //The gradient colors is determined by the color int and the color int plus one, which is why red is in the colors array twice
+ fillSidewaysGradient(
+ graphics,
+ x1 + (width / maxColors * color), y1,
+ color == maxColors - 1 ? x2 : x1 + (width / maxColors * (color + 1)), y2,
+ colors[color], colors[color + 1],
+ consumer
+ );
+ }
+ });
+
}
protected int multiplyColor(int hex, float amount) {
diff --git a/src/main/java/dev/isxander/yacl3/gui/ElementListWidgetExt.java b/src/main/java/dev/isxander/yacl3/gui/ElementListWidgetExt.java
index 8412cc8..cbd15fe 100644
--- a/src/main/java/dev/isxander/yacl3/gui/ElementListWidgetExt.java
+++ b/src/main/java/dev/isxander/yacl3/gui/ElementListWidgetExt.java
@@ -173,7 +173,8 @@ public class ElementListWidgetExt<E extends ElementListWidgetExt.Entry<E>> exten
}
@Override
- protected int getRowTop(int index) {
+ /*? if >=1.21.2 {*/ public /*?} else {*/ /*protected *//*?}*/
+ int getRowTop(int index) {
int integer = getY() + 4 - (int) this.getScrollAmount() + headerHeight;
for (int i = 0; i < children().size() && i < index; i++)
integer += children().get(i).getItemHeight();
diff --git a/src/main/java/dev/isxander/yacl3/gui/YACLScreen.java b/src/main/java/dev/isxander/yacl3/gui/YACLScreen.java
index 90bc75f..812a0a1 100644
--- a/src/main/java/dev/isxander/yacl3/gui/YACLScreen.java
+++ b/src/main/java/dev/isxander/yacl3/gui/YACLScreen.java
@@ -2,7 +2,6 @@ package dev.isxander.yacl3.gui;
import com.mojang.blaze3d.platform.InputConstants;
import com.mojang.blaze3d.systems.RenderSystem;
-import com.mojang.blaze3d.vertex.*;
import com.mojang.math.Axis;
import dev.isxander.yacl3.api.*;
import dev.isxander.yacl3.api.utils.Dimension;
@@ -25,12 +24,10 @@ import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.MultiLineLabel;
import net.minecraft.client.gui.components.Tooltip;
import net.minecraft.client.gui.components.tabs.TabManager;
-import net.minecraft.client.gui.components.tabs.TabNavigationBar;
import net.minecraft.client.gui.navigation.ScreenRectangle;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.inventory.tooltip.TooltipRenderUtil;
import net.minecraft.client.gui.screens.worldselection.CreateWorldScreen;
-import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.network.chat.CommonComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
@@ -285,7 +282,8 @@ public class YACLScreen extends Screen {
drawY,
maxWidth,
height,
- 400
+ 400/*? if >=1.21.2 {*/,
+ null/*?}*/
);
graphics.pose().translate(0.0, 0.0, 400.0);
@@ -394,19 +392,19 @@ public class YACLScreen extends Screen {
public void renderBackground(GuiGraphics graphics) {
RenderSystem.enableBlend();
// right pane darker db
- graphics.blit(DARKER_BG, rightPaneDim.left(), rightPaneDim.top(), rightPaneDim.right() + 2, rightPaneDim.bottom() + 2, rightPaneDim.width() + 2, rightPaneDim.height() + 2, 32, 32);
-
+ GuiUtils.blitGuiTex(graphics, DARKER_BG, rightPaneDim.left(), rightPaneDim.top(), rightPaneDim.right() + 2, rightPaneDim.bottom() + 2, rightPaneDim.width() + 2, rightPaneDim.height() + 2, 32, 32);
+
// top separator for right pane
graphics.pose().pushPose();
graphics.pose().translate(0, 0, 10);
- graphics.blit(CreateWorldScreen.HEADER_SEPARATOR, rightPaneDim.left() - 1, rightPaneDim.top() - 2, 0.0F, 0.0F, rightPaneDim.width() + 1, 2, 32, 2);
+ GuiUtils.blitGuiTex(graphics, CreateWorldScreen.HEADER_SEPARATOR, rightPaneDim.left() - 1, rightPaneDim.top() - 2, 0.0F, 0.0F, rightPaneDim.width() + 1, 2, 32, 2);
graphics.pose().popPose();
// left separator for right pane
graphics.pose().pushPose();
graphics.pose().translate(rightPaneDim.left(), rightPaneDim.top() - 1, 0);
graphics.pose().rotateAround(Axis.ZP.rotationDegrees(90), 0, 0, 1);
- graphics.blit(CreateWorldScreen.FOOTER_SEPARATOR, 0, 0, 0f, 0f, rightPaneDim.height() + 1, 2, 32, 2);
+ GuiUtils.blitGuiTex(graphics, CreateWorldScreen.FOOTER_SEPARATOR, 0, 0, 0f, 0f, rightPaneDim.height() + 1, 2, 32, 2);
graphics.pose().popPose();
RenderSystem.disableBlend();
diff --git a/src/main/java/dev/isxander/yacl3/gui/controllers/ColorController.java b/src/main/java/dev/isxander/yacl3/gui/controllers/ColorController.java
index 3c0a5fc..a48bdde 100644
--- a/src/main/java/dev/isxander/yacl3/gui/controllers/ColorController.java
+++ b/src/main/java/dev/isxander/yacl3/gui/controllers/ColorController.java
@@ -260,7 +260,7 @@ public class ColorController implements IStringController<Color> {
@Override
public void unfocus() {
- if(colorPickerVisible) {
+ if (colorPickerVisible) {
removeColorPicker();
}
previewOutlineFadeTicks = 0;
@@ -271,7 +271,7 @@ public class ColorController implements IStringController<Color> {
Color outlineColor = new Color(0xFF000000);
Color highlightedColor = getHighlightedOutlineColor();
- if(!hovered && !colorPreviewHovered) {
+ if (!hovered && !colorPreviewHovered) {
previewOutlineFadeTicks = 0;
return outlineColor;
}
@@ -279,11 +279,11 @@ public class ColorController implements IStringController<Color> {
int fadeInTicks = 80;
int fadeOutTicks = fadeInTicks + 120;
- if(colorPreviewHovered) {
+ if (colorPreviewHovered) {
//white/light grey if the color preview is being hovered
previewOutlineFadeTicks = 0;
return highlightedColor;
- } else if(YACLConfig.HANDLER.instance().showColorPickerIndicator) {
+ } else if (YACLConfig.HANDLER.instance().showColorPickerIndicator) {
if(previewOutlineFadeTicks <= fadeInTicks) {
//fade to white
return getFadedColor(outlineColor, highlightedColor, previewOutlineFadeTicks, fadeInTicks);
diff --git a/src/main/java/dev/isxander/yacl3/gui/controllers/ColorPickerWidget.java b/src/main/java/dev/isxander/yacl3/gui/controllers/ColorPickerWidget.java
index efa1aec..a1828f5 100644
--- a/src/main/java/dev/isxander/yacl3/gui/controllers/ColorPickerWidget.java
+++ b/src/main/java/dev/isxander/yacl3/gui/controllers/ColorPickerWidget.java
@@ -3,9 +3,11 @@ package dev.isxander.yacl3.gui.controllers;
import dev.isxander.yacl3.api.utils.Dimension;
import dev.isxander.yacl3.api.utils.MutableDimension;
import dev.isxander.yacl3.gui.YACLScreen;
+import dev.isxander.yacl3.gui.utils.GuiUtils;
import dev.isxander.yacl3.gui.utils.YACLRenderHelper;
import dev.isxander.yacl3.platform.YACLPlatform;
import net.minecraft.client.gui.GuiGraphics;
+import net.minecraft.client.renderer.RenderType;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.Mth;
@@ -89,7 +91,7 @@ public class ColorPickerWidget extends ControllerPopupWidget<ColorController> {
//Background
/*? if >1.20.3 {*/
- graphics.blitSprite(COLOR_PICKER_LOCATION, colorPickerDim.x() - 5, colorPickerDim.y() - 5, colorPickerDim.width() + 10, colorPickerDim.height() + 10);
+ GuiUtils.blitSprite(graphics, COLOR_PICKER_LOCATION, colorPickerDim.x() - 5, colorPickerDim.y() - 5, colorPickerDim.width() + 10, colorPickerDim.height() + 10);
/*?} else {*/
/*graphics.blitNineSliced(COLOR_PICKER_ATLAS, colorPickerDim.x() - 5, colorPickerDim.y() - 5, colorPickerDim.width() + 10, colorPickerDim.height() + 10, 3, 236, 34, 0, 0);
*//*?}*/
@@ -100,7 +102,7 @@ public class ColorPickerWidget extends ControllerPopupWidget<ColorController> {
//transparent texture - must be rendered BEFORE the main color preview
if(controller.allowAlpha()) {
/*? if >1.20.3 {*/
- graphics.blitSprite(TRANSPARENT_TEXTURE_LOCATION, previewColorDim.x(), previewColorDim.y(), previewColorDim.width(), previewColorDim.height());
+ GuiUtils.blitSprite(graphics, TRANSPARENT_TEXTURE_LOCATION, previewColorDim.x(), previewColorDim.y(), previewColorDim.width(), previewColorDim.height());
/*?} else {*/
/*graphics.blitRepeating(COLOR_PICKER_ATLAS, previewColorDim.x(), previewColorDim.y(), previewColorDim.width(), previewColorDim.height(), 236, 0, 8, 8);
*//*?}*/
@@ -112,7 +114,9 @@ public class ColorPickerWidget extends ControllerPopupWidget<ColorController> {
//outline
graphics.fill(saturationLightDim.x() - outline, saturationLightDim.y() - outline, saturationLightDim.xLimit() + outline, saturationLightDim.yLimit() + outline, Color.black.getRGB());
//White to pending color's RGB from hue, left to right
- fillSidewaysGradient(graphics, saturationLightDim.x(), saturationLightDim.y(), saturationLightDim.xLimit(), saturationLightDim.yLimit(), 0xFFFFFFFF, (int) getRgbFromHueX());
+ GuiUtils.drawSpecial(graphics, bufferSource -> {
+ fillSidewaysGradient(graphics, saturationLightDim.x(), saturationLightDim.y(), saturationLightDim.xLimit(), saturationLightDim.yLimit(), 0xFFFFFFFF, (int) getRgbFromHueX(), bufferSource.getBuffer(RenderType.gui()));
+ });
//Transparent to black, top to bottom
graphics.fillGradient(saturationLightDim.x(), saturationLightDim.y(), saturationLightDim.xLimit(), saturationLightDim.yLimit(), 0x00000000, 0xFF000000);
//Sat/light thumb shadow
@@ -135,12 +139,14 @@ public class ColorPickerWidget extends ControllerPopupWidget<ColorController> {
graphics.fill(alphaGradientDim.x() - outline, alphaGradientDim.y() - outline, alphaGradientDim.xLimit() + outline, alphaGradientDim.yLimit() + outline, Color.black.getRGB());
//Transparent texture
/*? if >1.20.3 {*/
- graphics.blitSprite(TRANSPARENT_TEXTURE_LOCATION, alphaGradientDim.x(), alphaGradientDim.y(), alphaGradientDim.width(), sliderHeight);
+ GuiUtils.blitSprite(graphics, TRANSPARENT_TEXTURE_LOCATION, alphaGradientDim.x(), alphaGradientDim.y(), alphaGradientDim.width(), sliderHeight);
/*?} else {*/
/*graphics.blitRepeating(COLOR_PICKER_ATLAS, alphaGradientDim.x(), alphaGradientDim.y(), alphaGradientDim.width(), sliderHeight, 236, 0, 8, 8);
*//*?}*/
//Pending color to transparent
- fillSidewaysGradient(graphics, alphaGradientDim.x(), alphaGradientDim.y(), alphaGradientDim.xLimit(), alphaGradientDim.yLimit(), getRgbWithoutAlpha(), 0x00000000);
+ GuiUtils.drawSpecial(graphics, bufferSource -> {
+ fillSidewaysGradient(graphics, alphaGradientDim.x(), alphaGradientDim.y(), alphaGradientDim.xLimit(), alphaGradientDim.yLimit(), getRgbWithoutAlpha(), 0x00000000, bufferSource.getBuffer(RenderType.gui()));
+ });
//Alpha slider thumb shadow
graphics.fill(alphaThumbX - thumbWidth / 2 - 1, alphaGradientDim.y() - outline - 1, alphaThumbX + thumbWidth / 2 + 1, alphaGradientDim.yLimit() + outline + 1, 0xFF404040);
//Alpha slider thumb
diff --git a/src/main/java/dev/isxander/yacl3/gui/controllers/LabelController.java b/src/main/java/dev/isxander/yacl3/gui/controllers/LabelController.java
index fee6c19..9f38d3b 100644
--- a/src/main/java/dev/isxander/yacl3/gui/controllers/LabelController.java
+++ b/src/main/java/dev/isxander/yacl3/gui/controllers/LabelController.java
@@ -88,8 +88,6 @@ public class LabelController implements Controller<Component> {
graphics.pose().pushPose();
graphics.pose().translate(0, 0, 100);
if (isMouseOver(mouseX, mouseY)) {
- YACLScreen.renderMultilineTooltip(graphics, textRenderer, wrappedTooltip, getDimension().centerX(), getDimension().y() - 5, getDimension().yLimit() + 5, screen.width, screen.height);
-
Style style = getStyle(mouseX, mouseY);
if (style != null && style.getHoverEvent() != null) {
HoverEvent hoverEvent = style.getHoverEvent();
diff --git a/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownWidget.java b/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownWidget.java
index f799059..464571e 100644
--- a/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownWidget.java
+++ b/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownWidget.java
@@ -5,6 +5,7 @@ import dev.isxander.yacl3.api.utils.Dimension;
import dev.isxander.yacl3.api.utils.MutableDimension;
import dev.isxander.yacl3.gui.YACLScreen;
import dev.isxander.yacl3.gui.controllers.ControllerPopupWidget;
+import dev.isxander.yacl3.gui.utils.GuiUtils;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component;
@@ -59,26 +60,28 @@ public class DropdownWidget<T> extends ControllerPopupWidget<AbstractDropdownCon
matrices.translate(0, 0, 200);
// Background
- graphics.setColor(0.25f, 0.25f, 0.25f, 1.0f);
- graphics.blit(
+ //graphics.setColor(0.25f, 0.25f, 0.25f, 1.0f);
+ GuiUtils.blitGuiTexColor(
+ graphics,
/*? if >1.20.4 {*/
Screen.MENU_BACKGROUND,
/*?} else {*/
/*Screen.BACKGROUND_LOCATION,
*//*?}*/
- dropdownDim.x(), dropdownDim.y(), 0,
+ dropdownDim.x(), dropdownDim.y(),
0.0f, 0.0f,
dropdownDim.width(), dropdownDim.height(),
- 32, 32
+ 32, 32,
+ 0xFF3F3F3F
);
- graphics.setColor(1.0f, 1.0f, 1.0f, 1.0f);
+ //graphics.setColor(1.0f, 1.0f, 1.0f, 1.0f);
graphics.renderOutline(dropdownDim.x(), dropdownDim.y(), dropdownDim.width(), dropdownDim.height(), -1);
// Highlight the currently selected element
- graphics.setColor(0.0f, 0.0f, 0.0f, 0.5f);
+ //graphics.setColor(0.0f, 0.0f, 0.0f, 0.5f);
int y = dropdownDim.y() + 2 + entryHeight() * selectedVisibleIndex();
- graphics.fill(dropdownDim.x(), y, dropdownDim.xLimit(), y + entryHeight(), -1);
- graphics.setColor(1.0f, 1.0f, 1.0f, 1.0f);
+ graphics.fill(dropdownDim.x(), y, dropdownDim.xLimit(), y + entryHeight(), 0x7F000000);
+ //graphics.setColor(1.0f, 1.0f, 1.0f, 1.0f);
graphics.renderOutline(dropdownDim.x(), y, dropdownDim.width(), entryHeight(), -1);
// Render all visible elements
diff --git a/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemControllerElement.java b/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemControllerElement.java
index 2c19c13..37911de 100644
--- a/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemControllerElement.java
+++ b/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemControllerElement.java
@@ -3,6 +3,7 @@ package dev.isxander.yacl3.gui.controllers.dropdown;
import dev.isxander.yacl3.api.utils.Dimension;
import dev.isxander.yacl3.gui.YACLScreen;
import dev.isxander.yacl3.gui.utils.ItemRegistryHelper;
+import dev.isxander.yacl3.gui.utils.MiscUtil;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.chat.Component;
@@ -43,7 +44,7 @@ public class ItemControllerElement extends AbstractDropdownControllerElement<Ite
List<ResourceLocation> identifiers = ItemRegistryHelper.getMatchingItemIdentifiers(inputField).toList();
currentItem = ItemRegistryHelper.getItemFromName(inputField, null);
for (ResourceLocation identifier : identifiers) {
- matchingItems.put(identifier, BuiltInRegistries.ITEM.get(identifier));
+ matchingItems.put(identifier, MiscUtil.getFromRegistry(BuiltInRegistries.ITEM, identifier));
}
return identifiers;
}
@@ -86,6 +87,11 @@ public class ItemControllerElement extends AbstractDropdownControllerElement<Ite
if (inputFieldFocused)
return Component.literal(inputField);
- return itemController.option().pendingValue().getDescription();
+ return itemController.option().pendingValue()
+ //? if >=1.21.2 {
+ .getName();
+ //?} else {
+ /*.getDescription();
+ *///?}
}
}
diff --git a/src/main/java/dev/isxander/yacl3/gui/controllers/string/StringControllerElement.java b/src/main/java/dev/isxander/yacl3/gui/controllers/string/StringControllerElement.java
index 689d8e2..7151f89 100644
--- a/src/main/java/dev/isxander/yacl3/gui/controllers/string/StringControllerElement.java
+++ b/src/main/java/dev/isxander/yacl3/gui/controllers/string/StringControllerElement.java
@@ -1,6 +1,7 @@
package dev.isxander.yacl3.gui.controllers.string;
import com.mojang.blaze3d.platform.InputConstants;
+import dev.isxander.yacl3.api.OptionEventListener;
import dev.isxander.yacl3.api.utils.Dimension;
import dev.isxander.yacl3.gui.YACLScreen;
import dev.isxander.yacl3.gui.controllers.ControllerWidget;
@@ -39,8 +40,10 @@ public class StringControllerElement extends ControllerWidget<IStringController<
inputFieldFocused = false;
selectionLength = 0;
emptyText = Component.literal("Click to type...").withStyle(ChatFormatting.GRAY);
- control.option().addListener((opt, val) -> {
- inputField = control.getString();
+ control.option().addEventListener((opt, event) -> {
+ if (event == OptionEventListener.Event.STATE_CHANGE) {
+ inputField = control.getString();
+ }
});
setDimension(dim);
}
diff --git a/src/main/java/dev/isxander/yacl3/gui/image/YACLImageReloadListener.java b/src/main/java/dev/isxander/yacl3/gui/image/YACLImageReloadListener.java
index fb0695c..235d1d4 100644
--- a/src/main/java/dev/isxander/yacl3/gui/image/YACLImageReloadListener.java
+++ b/src/main/java/dev/isxander/yacl3/gui/image/YACLImageReloadListener.java
@@ -29,19 +29,20 @@ public class YACLImageReloadListener
public @NotNull CompletableFuture<Void> reload(
PreparationBarrier preparationBarrier,
@NotNull ResourceManager resourceManager,
- @NotNull ProfilerFiller preparationsProfiler,
+ //? if <1.21.2 {
+ /*@NotNull ProfilerFiller preparationsProfiler,
@NotNull ProfilerFiller reloadProfiler,
+ *///?}
@NotNull Executor backgroundExecutor,
@NotNull Executor gameExecutor
) {
- return prepare(resourceManager, preparationsProfiler, backgroundExecutor)
+ return prepare(resourceManager, backgroundExecutor)
.thenCompose(preparationBarrier::wait)
- .thenCompose(suppliers -> apply(suppliers, reloadProfiler, gameExecutor));
+ .thenCompose(suppliers -> apply(suppliers, gameExecutor));
}
private CompletableFuture<List<Optional<SupplierPreparation>>> prepare(
ResourceManager manager,
- ProfilerFiller profiler,
Executor executor
) {
Map<ResourceLocation, Resource> imageResources = manager.listResources(
@@ -72,7 +73,6 @@ public class YACLImageReloadListener
private CompletableFuture<Void> apply(
List<Optional<SupplierPreparation>> suppliers,
- ProfilerFiller profiler,
Executor executor
) {
return CompletableFuture.allOf(suppliers.stream()
diff --git a/src/main/java/dev/isxander/yacl3/gui/image/impl/AnimatedDynamicTextureImage.java b/src/main/java/dev/isxander/yacl3/gui/image/impl/AnimatedDynamicTextureImage.java
index 39ddb55..1dd52f2 100644
--- a/src/main/java/dev/isxander/yacl3/gui/image/impl/AnimatedDynamicTextureImage.java
+++ b/src/main/java/dev/isxander/yacl3/gui/image/impl/AnimatedDynamicTextureImage.java
@@ -7,7 +7,7 @@ import com.mojang.blaze3d.platform.NativeImage;
import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi;
import dev.isxander.yacl3.debug.DebugProperties;
import dev.isxander.yacl3.gui.image.ImageRendererFactory;
-import dev.isxander.yacl3.impl.utils.YACLConstants;
+import dev.isxander.yacl3.gui.utils.GuiUtils;
import net.minecraft.CrashReport;
import net.minecraft.CrashReportCategory;
import net.minecraft.ReportedException;
@@ -16,7 +16,6 @@ import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.Resource;
import net.minecraft.server.packs.resources.ResourceManager;
-import net.minecraft.util.FastColor;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
@@ -71,7 +70,8 @@ public class AnimatedDynamicTextureImage extends DynamicTextureImage {
GlStateManager._texParameter(GlConst.GL_TEXTURE_2D, GlConst.GL_TEXTURE_MIN_FILTER, GlConst.GL_LINEAR);
}
- graphics.blit(
+ GuiUtils.blitGuiTex(
+ graphics,
uniqueLocation,
0, 0,
frameWidth * currentCol, frameHeight * currentRow,
@@ -251,19 +251,16 @@ public class AnimatedDynamicTextureImage extends DynamicTextureImage {
for (int w = 0; w < bi.getWidth(); w++) {
for (int h = 0; h < bi.getHeight(); h++) {
- int rgb = bi.getRGB(w, h);
- int r = FastColor.ARGB32.red(rgb);
- int g = FastColor.ARGB32.green(rgb);
- int b = FastColor.ARGB32.blue(rgb);
- int a = FastColor.ARGB32.alpha(rgb);
+ int argb = bi.getRGB(w, h);
int col = i % cols;
int row = (int) Math.floor(i / (double)cols);
- image.setPixelRGBA(
+ GuiUtils.setPixelARGB(
+ image,
frameWidth * col + w + xOffset,
frameHeight * row + h + yOffset,
- FastColor.ABGR32.color(a, b, g, r) // NativeImage uses ABGR for some reason
+ argb
);
}
}
diff --git a/src/main/java/dev/isxander/yacl3/gui/image/impl/DynamicTextureImage.java b/src/main/java/dev/isxander/yacl3/gui/image/impl/DynamicTextureImage.java
index 2d2abb9..edfaebc 100644
--- a/src/main/java/dev/isxander/yacl3/gui/image/impl/DynamicTextureImage.java
+++ b/src/main/java/dev/isxander/yacl3/gui/image/impl/DynamicTextureImage.java
@@ -7,6 +7,7 @@ import com.mojang.blaze3d.systems.RenderSystem;
import dev.isxander.yacl3.debug.DebugProperties;
import dev.isxander.yacl3.gui.image.ImageRenderer;
import dev.isxander.yacl3.gui.image.ImageRendererFactory;
+import dev.isxander.yacl3.gui.utils.GuiUtils;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.renderer.texture.DynamicTexture;
@@ -51,7 +52,7 @@ public class DynamicTextureImage implements ImageRenderer {
GlStateManager._texParameter(GlConst.GL_TEXTURE_2D, GlConst.GL_TEXTURE_MIN_FILTER, GlConst.GL_LINEAR);
}
- graphics.blit(uniqueLocation, 0, 0, 0, 0, this.width, this.height, this.width, this.height);
+ GuiUtils.blitGuiTex(graphics, uniqueLocation, 0, 0, 0, 0, this.width, this.height, this.width, this.height);
graphics.pose().popPose();
diff --git a/src/main/java/dev/isxander/yacl3/gui/image/impl/ResourceTextureImage.java b/src/main/java/dev/isxander/yacl3/gui/image/impl/ResourceTextureImage.java
index abbeec7..baaa4b1 100644
--- a/src/main/java/dev/isxander/yacl3/gui/image/impl/ResourceTextureImage.java
+++ b/src/main/java/dev/isxander/yacl3/gui/image/impl/ResourceTextureImage.java
@@ -5,6 +5,7 @@ import com.mojang.blaze3d.platform.GlStateManager;
import dev.isxander.yacl3.debug.DebugProperties;
import dev.isxander.yacl3.gui.image.ImageRenderer;
import dev.isxander.yacl3.gui.image.ImageRendererFactory;
+import dev.isxander.yacl3.gui.utils.GuiUtils;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.resources.ResourceLocation;
@@ -38,7 +39,7 @@ public class ResourceTextureImage implements ImageRenderer {
GlStateManager._texParameter(GlConst.GL_TEXTURE_2D, GlConst.GL_TEXTURE_MIN_FILTER, GlConst.GL_LINEAR);
}
- graphics.blit(location, 0, 0, this.u, this.v, this.width, this.height, this.textureWidth, this.textureHeight);
+ GuiUtils.blitGuiTex(graphics, location, 0, 0, this.u, this.v, this.width, this.height, this.textureWidth, this.textureHeight);
graphics.pose().popPose();
diff --git a/src/main/java/dev/isxander/yacl3/gui/utils/GuiUtils.java b/src/main/java/dev/isxander/yacl3/gui/utils/GuiUtils.java
index 2910d0f..25c4cb7 100644
--- a/src/main/java/dev/isxander/yacl3/gui/utils/GuiUtils.java
+++ b/src/main/java/dev/isxander/yacl3/gui/utils/GuiUtils.java
@@ -1,11 +1,75 @@
package dev.isxander.yacl3.gui.utils;
+import com.mojang.blaze3d.platform.NativeImage;
import net.minecraft.client.gui.Font;
+import net.minecraft.client.gui.GuiGraphics;
+import net.minecraft.client.renderer.MultiBufferSource;
+import net.minecraft.client.renderer.RenderType;
import net.minecraft.locale.Language;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
+import net.minecraft.resources.ResourceLocation;
+
+import java.util.function.Consumer;
public class GuiUtils {
+ public static void drawSpecial(GuiGraphics graphics, Consumer<MultiBufferSource> consumer) {
+ //? if >=1.21.2 {
+ graphics.drawSpecial(consumer);
+ //?} else {
+ /*MultiBufferSource.BufferSource bufferSource = graphics.bufferSource();
+ consumer.accept(bufferSource);
+ bufferSource.endBatch();
+ *///?}
+ }
+
+ public static void blitGuiTex(GuiGraphics graphics, ResourceLocation texture, int x, int y, float u, float v, int textureWidth, int textureHeight, int width, int height) {
+ graphics.blit(
+ //? if >=1.21.2
+ RenderType::guiTextured,
+ texture,
+ x, y,
+ u, v,
+ textureWidth, textureHeight,
+ width, height
+ );
+ }
+
+ public static void blitGuiTexColor(GuiGraphics graphics, ResourceLocation texture, int x, int y, float u, float v, int textureWidth, int textureHeight, int width, int height, int color) {
+ //? if <1.21.2 {
+ /*float a = (color >> 24 & 255) / 255.0F;
+ float r = (color >> 16 & 255) / 255.0F;
+ float g = (color >> 8 & 255) / 255.0F;
+ float b = (color & 255) / 255.0F;
+ graphics.setColor(r, g, b, a);
+ *///?}
+ graphics.blit(
+ //? if >=1.21.2
+ RenderType::guiTextured,
+ texture,
+ x, y,
+ u, v,
+ textureWidth, textureHeight,
+ width, height
+ //? if >=1.21.2
+ ,color
+ );
+ //? if <1.21.2
+ /*graphics.setColor(1.0F, 1.0F, 1.0F, 1.0F);*/
+ }
+
+ //? if >1.20.1 {
+ public static void blitSprite(GuiGraphics graphics, ResourceLocation sprite, int x, int y, int width, int height) {
+ graphics.blitSprite(
+ //? if >=1.21.2
+ RenderType::guiTextured,
+ sprite,
+ x, y,
+ width, height
+ );
+ }
+ //?}
+
public static MutableComponent translatableFallback(String key, Component fallback) {
if (Language.getInstance().has(key))
return Component.translatable(key);
@@ -29,4 +93,21 @@ public class GuiUtils {
return string;
}
+
+
+ public static void setPixelARGB(NativeImage nativeImage, int x, int y, int argb) {
+ // In 1.21.2+, you set the pixel color in ARGB format, where it internally converts to ABGR.
+ // Before this, you need to directly set the pixel color in ABGR format.
+
+ //? if >=1.21.2 {
+ nativeImage.setPixel(x, y, argb);
+ //?} else {
+ /*int a = (argb >> 24) & 0xFF;
+ int r = (argb >> 16) & 0xFF;
+ int g = (argb >> 8) & 0xFF;
+ int b = argb & 0xFF;
+ int abgr = (a << 24) | (b << 16) | (g << 8) | r;
+ nativeImage.setPixelRGBA(x, y, abgr); // method name is misleading. It's actually ABGR.
+ *///?}
+ }
}
diff --git a/src/main/java/dev/isxander/yacl3/gui/utils/ItemRegistryHelper.java b/src/main/java/dev/isxander/yacl3/gui/utils/ItemRegistryHelper.java
index bb6c664..dc769bc 100644
--- a/src/main/java/dev/isxander/yacl3/gui/utils/ItemRegistryHelper.java
+++ b/src/main/java/dev/isxander/yacl3/gui/utils/ItemRegistryHelper.java
@@ -46,7 +46,7 @@ public final class ItemRegistryHelper {
try {
ResourceLocation itemIdentifier = YACLPlatform.parseRl(identifier.toLowerCase());
if (BuiltInRegistries.ITEM.containsKey(itemIdentifier)) {
- return BuiltInRegistries.ITEM.get(itemIdentifier);
+ return MiscUtil.getFromRegistry(BuiltInRegistries.ITEM, itemIdentifier);
}
} catch (ResourceLocationException ignored) {
}
@@ -80,14 +80,15 @@ public final class ItemRegistryHelper {
if (sep == -1) {
filterPredicate = identifier ->
identifier.getPath().contains(value)
- || BuiltInRegistries.ITEM.get(identifier).getDescription().getString().toLowerCase().contains(value.toLowerCase());
+ || MiscUtil.getFromRegistry(BuiltInRegistries.ITEM, identifier)
+ /*? if >=1.21.2 {*/ .getName() /*?} else {*/ /*.getDescription() *//*?}*/
+ .getString().toLowerCase().contains(value.toLowerCase());
} else {
String namespace = value.substring(0, sep);
String path = value.substring(sep + 1);
filterPredicate = identifier -> identifier.getNamespace().equals(namespace) && identifier.getPath().startsWith(path);
}
- return BuiltInRegistries.ITEM.holders()
- .map(holder -> holder.key().location())
+ return BuiltInRegistries.ITEM.keySet().stream()
.filter(filterPredicate)
/*
Sort items as follows based on the given "value" string's path:
diff --git a/src/main/java/dev/isxander/yacl3/gui/utils/MiscUtil.java b/src/main/java/dev/isxander/yacl3/gui/utils/MiscUtil.java
new file mode 100644
index 0000000..354b38e
--- /dev/null
+++ b/src/main/java/dev/isxander/yacl3/gui/utils/MiscUtil.java
@@ -0,0 +1,14 @@
+package dev.isxander.yacl3.gui.utils;
+
+import net.minecraft.core.Registry;
+import net.minecraft.resources.ResourceLocation;
+
+public class MiscUtil {
+ public static <T> T getFromRegistry(Registry<T> registry, ResourceLocation identifier) {
+ //? if >=1.21.2 {
+ return registry.getValue(identifier);
+ //?} else {
+ /*return registry.get(identifier);
+ *///?}
+ }
+}
diff --git a/src/main/java/dev/isxander/yacl3/gui/utils/YACLRenderHelper.java b/src/main/java/dev/isxander/yacl3/gui/utils/YACLRenderHelper.java
index aadc249..23e97c3 100644
--- a/src/main/java/dev/isxander/yacl3/gui/utils/YACLRenderHelper.java
+++ b/src/main/java/dev/isxander/yacl3/gui/utils/YACLRenderHelper.java
@@ -19,7 +19,7 @@ public class YACLRenderHelper {
public static void renderButtonTexture(GuiGraphics graphics, int x, int y, int width, int height, boolean enabled, boolean focused) {
/*? if >1.20.1 {*/
- graphics.blitSprite(SPRITES.get(enabled, focused), x, y, width, height);
+ GuiUtils.blitSprite(graphics, SPRITES.get(enabled, focused), x, y, width, height);
/*?} else {*/
/*int textureV;
if (enabled) {
diff --git a/src/main/java/dev/isxander/yacl3/impl/ButtonOptionImpl.java b/src/main/java/dev/isxander/yacl3/impl/ButtonOptionImpl.java
index 170b8e0..60a9dc9 100644
--- a/src/main/java/dev/isxander/yacl3/impl/ButtonOptionImpl.java
+++ b/src/main/java/dev/isxander/yacl3/impl/ButtonOptionImpl.java
@@ -17,10 +17,9 @@ import java.util.function.Consumer;
public final class ButtonOptionImpl implements ButtonOption {
private final Component name;
private final OptionDescription description;
- private final BiConsumer<YACLScreen, ButtonOption> action;
+ private final StateManager<BiConsumer<YACLScreen, ButtonOption>> stateManager;
private boolean available;
private final Controller<BiConsumer<YACLScreen, ButtonOption>> controller;
- private final Binding<BiConsumer<YACLScreen, ButtonOption>> binding;
public ButtonOptionImpl(
@NotNull Component name,
@@ -31,10 +30,9 @@ public final class ButtonOptionImpl implements ButtonOption {
) {
this.name = name;
this.description = description;
- this.action = action;
+ this.stateManager = StateManager.createImmutable(action);
this.available = available;
this.controller = text != null ? new ActionController(this, text) : new ActionController(this);
- this.binding = new EmptyBinderImpl();
}
@Override
@@ -54,7 +52,7 @@ public final class ButtonOptionImpl implements ButtonOption {
@Override
public BiConsumer<YACLScreen, ButtonOption> action() {
- return action;
+ return stateManager().get();
}
@Override
@@ -73,8 +71,13 @@ public final class ButtonOptionImpl implements ButtonOption {
}
@Override
+ public @NotNull StateManager<BiConsumer<YACLScreen, ButtonOption>> stateManager() {
+ return this.stateManager;
+ }
+
+ @Override
public @NotNull Binding<BiConsumer<YACLScreen, ButtonOption>> binding() {
- return binding;
+ return new EmptyBinderImpl();
}
@Override
@@ -118,6 +121,11 @@ public final class ButtonOptionImpl implements ButtonOption {
}
@Override
+ public void addEventListener(OptionEventListener<BiConsumer<YACLScreen, ButtonOption>> listener) {
+
+ }
+
+ @Override
public void addListener(BiConsumer<Option<BiConsumer<YACLScreen, ButtonOption>>, BiConsumer<YACLScreen, ButtonOption>> changedListener) {
}
diff --git a/src/main/java/dev/isxander/yacl3/impl/HiddenNameListOptionEntry.java b/src/main/java/dev/isxander/yacl3/impl/HiddenNameListOptionEntry.java
index 64588f2..16d8e14 100644
--- a/src/main/java/dev/isxander/yacl3/impl/HiddenNameListOptionEntry.java
+++ b/src/main/java/dev/isxander/yacl3/impl/HiddenNameListOptionEntry.java
@@ -31,6 +31,11 @@ public class HiddenNameListOptionEntry<T> implements ListOptionEntry<T> {
}
@Override
+ public @NotNull StateManager<T> stateManager() {
+ return option.stateManager();
+ }
+
+ @Override
public @NotNull Controller<T> controller() {
return option.controller();
}
@@ -101,9 +106,13 @@ public class HiddenNameListOptionEntry<T> implements ListOptionEntry<T> {
}
@Override
+ @Deprecated
public void addListener(BiConsumer<Option<T>, T> changedListener) {
option.addListener(changedListener);
}
-
+ @Override
+ public void addEventListener(OptionEventListener<T> listener) {
+ option.addEventListener(listener);
+ }
}
diff --git a/src/main/java/dev/isxander/yacl3/impl/ImmutableStateManager.java b/src/main/java/dev/isxander/yacl3/impl/ImmutableStateManager.java
new file mode 100644
index 0000000..f4fd9b2
--- /dev/null
+++ b/src/main/java/dev/isxander/yacl3/impl/ImmutableStateManager.java
@@ -0,0 +1,56 @@
+package dev.isxander.yacl3.impl;
+
+import dev.isxander.yacl3.api.StateManager;
+
+public class ImmutableStateManager<T> implements StateManager<T> {
+ private final T value;
+
+ public ImmutableStateManager(T value) {
+ this.value = value;
+ }
+
+ @Override
+ public void set(T value) {
+ throw new UnsupportedOperationException("Cannot set value of immutable state manager");
+ }
+
+ @Override
+ public T get() {
+ return value;
+ }
+
+ @Override
+ public void apply() {
+ // no-op
+ }
+
+ @Override
+ public void resetToDefault(ResetAction action) {
+ // always default
+ }
+
+ @Override
+ public void sync() {
+ // always synced
+ }
+
+ @Override
+ public boolean isSynced() {
+ return true;
+ }
+
+ @Override
+ public boolean isAlwaysSynced() {
+ return true;
+ }
+
+ @Override
+ public boolean isDefault() {
+ return true;
+ }
+
+ @Override
+ public void addListener(StateListener<T> stateListener) {
+ // as the values never change, listeners are not needed and would never be called
+ }
+}
diff --git a/src/main/java/dev/isxander/yacl3/impl/InstantStateManager.java b/src/main/java/dev/isxander/yacl3/impl/InstantStateManager.java
new file mode 100644
index 0000000..8806617
--- /dev/null
+++ b/src/main/java/dev/isxander/yacl3/impl/InstantStateManager.java
@@ -0,0 +1,68 @@
+package dev.isxander.yacl3.impl;
+
+import dev.isxander.yacl3.api.Binding;
+import dev.isxander.yacl3.api.StateManager;
+
+public class InstantStateManager<T> implements StateManager<T>, ProvidesBindingForDeprecation<T> {
+ private final Binding<T> binding;
+ private StateListener<T> stateListener;
+
+ public InstantStateManager(Binding<T> binding) {
+ this.binding = binding;
+ this.stateListener = StateListener.noop();
+ }
+
+ @Override
+ public void set(T value) {
+ boolean changed = !this.get().equals(value);
+
+ this.binding.setValue(value);
+
+ if (changed) stateListener.onStateChange(this.get(), value);
+ }
+
+ @Override
+ public T get() {
+ return this.binding.getValue();
+ }
+
+ @Override
+ public void apply() {
+ // no-op, state is always applied
+ }
+
+ @Override
+ public void resetToDefault(ResetAction action) {
+ this.set(binding.defaultValue());
+ }
+
+ @Override
+ public void sync() {
+ // no-op, state is always synced
+ }
+
+ @Override
+ public boolean isSynced() {
+ return true;
+ }
+
+ @Override
+ public boolean isAlwaysSynced() {
+ return true;
+ }
+
+ @Override
+ public boolean isDefault() {
+ return binding.defaultValue().equals(this.get());
+ }
+
+ @Override
+ public void addListener(StateListener<T> stateListener) {
+ this.stateListener = this.stateListener.andThen(stateListener);
+ }
+
+ @Override
+ public Binding<T> getBinding() {
+ return binding;
+ }
+}
diff --git a/src/main/java/dev/isxander/yacl3/impl/LabelOptionImpl.java b/src/main/java/dev/isxander/yacl3/impl/LabelOptionImpl.java
index 2bd2e10..3ad0caf 100644
--- a/src/main/java/dev/isxander/yacl3/impl/LabelOptionImpl.java
+++ b/src/main/java/dev/isxander/yacl3/impl/LabelOptionImpl.java
@@ -13,124 +13,57 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
-import java.util.function.BiConsumer;
-@ApiStatus.Internal
-public final class LabelOptionImpl implements LabelOption {
- private final Component label;
- private final Component name = Component.literal("Label Option");
- private final OptionDescription description;
- private final Component tooltip = Component.empty();
- private final LabelController labelController;
- private final Binding<Component> binding;
+public class LabelOptionImpl extends OptionImpl<Component> implements LabelOption {
+ public LabelOptionImpl(
+ @NotNull StateManager<Component> stateManager,
+ @NotNull Collection<OptionEventListener<Component>> optionEventListeners
+ ) {
+ super(
+ Component.literal("Label Option"),
+ OptionDescription::of,
+ LabelController::new,
+ stateManager,
+ true,
+ ImmutableSet.of(),
+ optionEventListeners
+ );
+ }
public LabelOptionImpl(Component label) {
- Validate.notNull(label, "`label` must not be null");
-
- this.label = label;
- this.labelController = new LabelController(this);
- this.binding = Binding.immutable(label);
- this.description = OptionDescription.createBuilder()
- .text(this.label)
- .build();
+ this(
+ StateManager.createImmutable(label),
+ ImmutableSet.of()
+ );
}
@Override
public @NotNull Component label() {
- return label;
- }
-
- @Override
- public @NotNull Component name() {
- return name;
- }
-
- @Override
- public @NotNull OptionDescription description() {
- return description;
- }
-
- @Override
- public @NotNull Component tooltip() {
- return tooltip;
- }
-
- @Override
- public @NotNull Controller<Component> controller() {
- return labelController;
- }
-
- @Override
- public @NotNull Binding<Component> binding() {
- return binding;
- }
-
- @Override
- public boolean available() {
- return true;
+ return stateManager().get();
}
@Override
public void setAvailable(boolean available) {
- throw new UnsupportedOperationException("Label options cannot be disabled.");
- }
-
- @Override
- public @NotNull ImmutableSet<OptionFlag> flags() {
- return ImmutableSet.of();
- }
-
- @Override
- public boolean changed() {
- return false;
- }
-
- @Override
- public @NotNull Component pendingValue() {
- return label;
- }
-
- @Override
- public void requestSet(@NotNull Component value) {
-
- }
-
- @Override
- public boolean applyValue() {
- return false;
- }
-
- @Override
- public void forgetPendingValue() {
-
- }
-
- @Override
- public void requestSetDefault() {
-
- }
-
- @Override
- public boolean isPendingValueDefault() {
- return true;
- }
-
- @Override
- public boolean canResetToDefault() {
- return false;
- }
-
- @Override
- public void addListener(BiConsumer<Option<Component>, Component> changedListener) {
-
+ throw new UnsupportedOperationException("Cannot change availability of label option");
}
@ApiStatus.Internal
- public static final class BuilderImpl implements Builder {
+ public static final class BuilderImpl implements LabelOption.Builder {
+ private StateManager<Component> stateManager;
private final List<Component> lines = new ArrayList<>();
@Override
- public Builder line(@NotNull Component line) {
+ public LabelOption.Builder state(@NotNull StateManager<Component> stateManager) {
+ Validate.notNull(stateManager, "`stateManager` must not be null");
+ Validate.isTrue(this.lines.isEmpty(), "Cannot set state manager if lines have already been defined");
+
+ this.stateManager = stateManager;
+ return this;
+ }
+
+ @Override
+ public LabelOption.Builder line(@NotNull Component line) {
+ Validate.isTrue(stateManager == null, ".line() is a helper to create a state manager for you at build. If you have defined a custom state manager, do not use .line()");
Validate.notNull(line, "`line` must not be null");
this.lines.add(line);
@@ -138,23 +71,30 @@ public final class LabelOptionImpl implements LabelOption {
}
@Override
- public Builder lines(@NotNull Collection<? extends Component> lines) {
+ public LabelOption.Builder lines(@NotNull Collection<? extends Component> lines) {
+ Validate.isTrue(stateManager == null, ".lines() is a helper to create a state manager for you at build. If you have defined a custom state manager, do not use .lines()");
+
this.lines.addAll(lines);
return this;
}
@Override
public LabelOption build() {
- MutableComponent text = Component.empty();
- Iterator<Component> iterator = lines.iterator();
- while (iterator.hasNext()) {
- text.append(iterator.next());
-
- if (iterator.hasNext())
- text.append("\n");
+ Validate.isTrue(stateManager != null || !lines.isEmpty(), "Cannot build label option without a state manager or lines");
+
+ if (!lines.isEmpty()) {
+ MutableComponent text = Component.empty();
+ Iterator<Component> iterator = lines.iterator();
+ while (iterator.hasNext()) {
+ text.append(iterator.next());
+
+ if (iterator.hasNext())
+ text.append("\n");
+ }
+ this.stateManager = StateManager.createSimple(new SelfContainedBinding<>(text));
}
- return new LabelOptionImpl(text);
+ return new LabelOptionImpl(this.stateManager, ImmutableSet.of());
}
}
}
diff --git a/src/main/java/dev/isxander/yacl3/impl/ListOptionEntryImpl.java b/src/main/java/dev/isxander/yacl3/impl/ListOptionEntryImpl.java
index 1cd5e55..dc4e7ff 100644
--- a/src/main/java/dev/isxander/yacl3/impl/ListOptionEntryImpl.java
+++ b/src/main/java/dev/isxander/yacl3/impl/ListOptionEntryImpl.java
@@ -49,6 +49,11 @@ public final class ListOptionEntryImpl<T> implements ListOptionEntry<T> {
}
@Override
+ public @NotNull StateManager<T> stateManager() {
+ throw new UnsupportedOperationException("ListOptionEntryImpl does not support state managers");
+ }
+
+ @Override
public @NotNull Binding<T> binding() {
return binding;
}
@@ -109,6 +114,11 @@ public final class ListOptionEntryImpl<T> implements ListOptionEntry<T> {
}
@Override
+ public void addEventListener(OptionEventListener<T> listener) {
+
+ }
+
+ @Override
public void addListener(BiConsumer<Option<T>, T> changedListener) {
}
@@ -138,7 +148,7 @@ public final class ListOptionEntryImpl<T> implements ListOptionEntry<T> {
@Override
public void setValue(T newValue) {
value = newValue;
- group.callListeners(true);
+ group.triggerListener(OptionEventListener.Event.OTHER, true);
}
@Override
diff --git a/src/main/java/dev/isxander/yacl3/impl/ListOptionImpl.java b/src/main/java/dev/isxander/yacl3/impl/ListOptionImpl.java
index c77d55f..e9dbb70 100644
--- a/src/main/java/dev/isxander/yacl3/impl/ListOptionImpl.java
+++ b/src/main/java/dev/isxander/yacl3/impl/ListOptionImpl.java
@@ -21,7 +21,7 @@ import java.util.stream.Collectors;
public final class ListOptionImpl<T> implements ListOption<T> {
private final Component name;
private final OptionDescription description;
- private final Binding<List<T>> binding;
+ private final StateManager<List<T>> stateManager;
private final Supplier<T> initialValue;
private final List<ListOptionEntry<T>> entries;
private final boolean collapsed;
@@ -32,14 +32,14 @@ public final class ListOptionImpl<T> implements ListOption<T> {
private final ImmutableSet<OptionFlag> flags;
private final EntryFactory entryFactory;
- private final List<BiConsumer<Option<List<T>>, List<T>>> listeners;
+ private final List<OptionEventListener<List<T>>> listeners;
private final List<Runnable> refreshListeners;
- private int listenerTriggerDepth = 0;
+ private int currentListenerDepth = 0;
- public ListOptionImpl(@NotNull Component name, @NotNull OptionDescription description, @NotNull Binding<List<T>> binding, @NotNull Supplier<T> initialValue, @NotNull Function<ListOptionEntry<T>, Controller<T>> controllerFunction, ImmutableSet<OptionFlag> flags, boolean collapsed, boolean available, int minimumNumberOfEntries, int maximumNumberOfEntries, boolean insertEntriesAtEnd, Collection<BiConsumer<Option<List<T>>, List<T>>> listeners) {
+ public ListOptionImpl(@NotNull Component name, @NotNull OptionDescription description, @NotNull StateManager<List<T>> stateManager, @NotNull Supplier<T> initialValue, @NotNull Function<ListOptionEntry<T>, Controller<T>> controllerFunction, ImmutableSet<OptionFlag> flags, boolean collapsed, boolean available, int minimumNumberOfEntries, int maximumNumberOfEntries, boolean insertEntriesAtEnd, Collection<OptionEventListener<List<T>>> listeners) {
this.name = name;
this.description = description;
- this.binding = new SafeBinding<>(binding);
+ this.stateManager = stateManager;
this.initialValue = initialValue;
this.entryFactory = new EntryFactory(controllerFunction);
this.entries = createEntries(binding().getValue());
@@ -52,7 +52,10 @@ public final class ListOptionImpl<T> implements ListOption<T> {
this.listeners = new ArrayList<>();
this.listeners.addAll(listeners);
this.refreshListeners = new ArrayList<>();
- callListeners(true);
+
+ this.stateManager.addListener((oldValue, newValue) ->
+ triggerListener(OptionEventListener.Event.STATE_CHANGE, false));
+ triggerListener(OptionEventListener.Event.INITIAL, false);
}
@Override
@@ -81,8 +84,17 @@ public final class ListOptionImpl<T> implements ListOption<T> {
}
@Override
+ public @NotNull StateManager<List<T>> stateManager() {
+ return stateManager;
+ }
+
+ @Override
+ @Deprecated
public @NotNull Binding<List<T>> binding() {
- return binding;
+ if (stateManager instanceof ProvidesBindingForDeprecation) {
+ return ((ProvidesBindingForDeprecation<List<T>>) stateManager).getBinding();
+ }
+ throw new UnsupportedOperationException("Binding is not available for this option - using a new state manager which does not directly expose the binding as it may not have one.");
}
@Override
@@ -177,8 +189,12 @@ public final class ListOptionImpl<T> implements ListOption<T> {
this.available = available;
- if (changed)
- callListeners(false);
+ if (changed) {
+ if (!available) {
+ this.stateManager.sync();
+ }
+ this.triggerListener(OptionEventListener.Event.AVAILABILITY_CHANGE, !available);
+ }
}
@Override
@@ -195,8 +211,14 @@ public final class ListOptionImpl<T> implements ListOption<T> {
}
@Override
+ public void addEventListener(OptionEventListener<List<T>> listener) {
+ this.listeners.add(listener);
+ }
+
+ @Override
+ @Deprecated
public void addListener(BiConsumer<Option<List<T>>, List<T>> changedListener) {
- this.listeners.add(changedListener);
+ addEventListener((opt, event) -> changedListener.accept(opt, opt.pendingValue()));
}
@Override
@@ -213,30 +235,26 @@ public final class ListOptionImpl<T> implements ListOption<T> {
return values.stream().map(entryFactory::create).collect(Collectors.toList());
}
- void callListeners(boolean bypass) {
- List<T> pendingValue = pendingValue();
- if (bypass || listenerTriggerDepth == 0) {
- if (listenerTriggerDepth > 10) {
- throw new IllegalStateException("Listener trigger depth exceeded 10! This means a listener triggered a listener etc etc 10 times deep. This is likely a bug in the mod using YACL!");
- }
+ void triggerListener(OptionEventListener.Event event, boolean allowDepth) {
+ if (allowDepth || currentListenerDepth == 0) {
+ Validate.isTrue(
+ currentListenerDepth <= 10,
+ "Listener depth exceeded 10! Possible cyclic listener pattern: a listener triggered an event that triggered the initial event etc etc."
+ );
- this.listenerTriggerDepth++;
+ currentListenerDepth++;
- for (BiConsumer<Option<List<T>>, List<T>> listener : listeners) {
- try {
- listener.accept(this, pendingValue);
- } catch (Exception e) {
- YACLConstants.LOGGER.error("Exception whilst triggering listener for option '%s'".formatted(name.getString()), e);
- }
+ for (OptionEventListener<List<T>> listener : listeners) {
+ listener.onEvent(this, event);
}
- this.listenerTriggerDepth--;
+ currentListenerDepth--;
}
}
private void onRefresh() {
refreshListeners.forEach(Runnable::run);
- callListeners(true);
+ triggerListener(OptionEventListener.Event.OTHER, true);
}
private class EntryFactory {
@@ -256,7 +274,6 @@ public final class ListOptionImpl<T> implements ListOption<T> {
private Component name = Component.empty();
private OptionDescription description = OptionDescription.EMPTY;
private Function<ListOptionEntry<T>, Controller<T>> controllerFunction;
- private Binding<List<T>> binding = null;
private final Set<OptionFlag> flags = new HashSet<>();
private Supplier<T> initialValue;
private boolean collapsed = false;
@@ -264,7 +281,10 @@ public final class ListOptionImpl<T> implements ListOption<T> {
private int minimumNumberOfEntries = 0;
private int maximumNumberOfEntries = Integer.MAX_VALUE;
private boolean insertEntriesAtEnd = false;
- private final List<BiConsumer<Option<List<T>>, List<T>>> listeners = new ArrayList<>();
+ private final List<OptionEventListener<List<T>>> listeners = new ArrayList<>();
+
+ private Binding<List<T>> binding;
+ private StateManager<List<T>> stateManager;
@Override
public Builder<T> name(@NotNull Component name) {
@@ -315,8 +335,18 @@ public final class ListOptionImpl<T> implements ListOption<T> {
}
@Override
+ public Builder<T> state(@NotNull StateManager<List<T>> stateManager) {
+ Validate.notNull(stateManager, "`stateManager` cannot be null");
+ Validate.isTrue(binding == null, "Cannot set state manager if binding is already set");
+
+ this.stateManager = stateManager;
+ return this;
+ }
+
+ @Override
public Builder<T> binding(@NotNull Binding<List<T>> binding) {
Validate.notNull(binding, "`binding` cannot be null");
+ Validate.isTrue(stateManager == null, "Cannot set binding if state manager is already set");
this.binding = binding;
return this;
@@ -379,24 +409,52 @@ public final class ListOptionImpl<T> implements ListOption<T> {
}
@Override
- public Builder<T> listener(@NotNull BiConsumer<Option<List<T>>, List<T>> listener) {
+ public Builder<T> addListener(@NotNull OptionEventListener<List<T>> listener) {
+ Validate.notNull(listener, "`listener` must not be null");
+
this.listeners.add(listener);
return this;
}
@Override
+ public Builder<T> addListeners(@NotNull Collection<@NotNull OptionEventListener<List<T>>> optionEventListeners) {
+ Validate.notNull(optionEventListeners, "`optionEventListeners` must not be null");
+
+ this.listeners.addAll(optionEventListeners);
+ return this;
+ }
+
+ @Override
+ public Builder<T> listener(@NotNull BiConsumer<Option<List<T>>, List<T>> listener) {
+ Validate.notNull(listener, "`listener` must not be null");
+
+ return this.addListener((opt, event) -> listener.accept(opt, opt.pendingValue()));
+ }
+
+ @Override
public Builder<T> listeners(@NotNull Collection<BiConsumer<Option<List<T>>, List<T>>> listeners) {
- this.listeners.addAll(listeners);
+ Validate.notNull(listeners, "`listeners` must not be null");
+
+ this.addListeners(listeners.stream()
+ .map(listener ->
+ (OptionEventListener<List<T>>) (opt, event) ->
+ listener.accept(opt, opt.pendingValue())
+ ).toList()
+ );
return this;
}
@Override
public ListOption<T> build() {
Validate.notNull(controllerFunction, "`controller` must not be null");
- Validate.notNull(binding, "`binding` must not be null");
Validate.notNull(initialValue, "`initialValue` must not be null");
+ Validate.isTrue(stateManager != null || binding != null, "Either a state manager or binding must be set");
+
+ if (stateManager == null) {
+ stateManager = StateManager.createSimple(binding);
+ }
- return new ListOptionImpl<>(name, description, binding, initialValue, controllerFunction, ImmutableSet.copyOf(flags), collapsed, available, minimumNumberOfEntries, maximumNumberOfEntries, insertEntriesAtEnd, listeners);
+ return new ListOptionImpl<>(name, description, stateManager, initialValue, controllerFunction, ImmutableSet.copyOf(flags), collapsed, available, minimumNumberOfEntries, maximumNumberOfEntries, insertEntriesAtEnd, listeners);
}
}
}
diff --git a/src/main/java/dev/isxander/yacl3/impl/NotNullBinding.java b/src/main/java/dev/isxander/yacl3/impl/NotNullBinding.java
new file mode 100644
index 0000000..d367b06
--- /dev/null
+++ b/src/main/java/dev/isxander/yacl3/impl/NotNullBinding.java
@@ -0,0 +1,31 @@
+package dev.isxander.yacl3.impl;
+
+import dev.isxander.yacl3.api.Binding;
+import org.apache.commons.lang3.Validate;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Objects;
+
+public class NotNullBinding<T> implements Binding<T> {
+ private final Binding<T> binding;
+
+ public NotNullBinding(Binding<T> binding) {
+ this.binding = binding;
+ }
+
+ @Override
+ public @NotNull T getValue() {
+ return Validate.notNull(binding.getValue(), "Binding's value must not be null, please use Optionals if you want null behaviour.");
+ }
+
+ @Override
+ public void setValue(@NotNull T value) {
+ Validate.notNull(value, "Binding's value must not be set to null, please use Optionals if you want null behaviour.");
+ binding.setValue(value);
+ }
+
+ @Override
+ public @NotNull T defaultValue() {
+ return Validate.notNull(binding.defaultValue(), "Binding's default value must not be null, please use Optionals if you want null behaviour.");
+ }
+}
diff --git a/src/main/java/dev/isxander/yacl3/impl/OptionImpl.java b/src/main/java/dev/isxander/yacl3/impl/OptionImpl.java
index afe9517..296c01f 100644
--- a/src/main/java/dev/isxander/yacl3/impl/OptionImpl.java
+++ b/src/main/java/dev/isxander/yacl3/impl/OptionImpl.java
@@ -9,6 +9,7 @@ import net.minecraft.network.chat.Component;
import org.apache.commons.lang3.Validate;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.BiConsumer;
@@ -17,40 +18,39 @@ import java.util.function.Function;
import java.util.function.Supplier;
@ApiStatus.Internal
-public final class OptionImpl<T> implements Option<T> {
+public class OptionImpl<T> implements Option<T> {
private final Component name;
private OptionDescription description;
private final Controller<T> controller;
- private final Binding<T> binding;
private boolean available;
private final ImmutableSet<OptionFlag> flags;
- private T pendingValue;
-
- private final List<BiConsumer<Option<T>, T>> listeners;
- private int listenerTriggerDepth = 0;
+ private final StateManager<T> stateManager;
+ private final List<OptionEventListener<T>> listeners;
+ private int currentListenerDepth;
public OptionImpl(
@NotNull Component name,
@NotNull Function<T, OptionDescription> descriptionFunction,
@NotNull Function<Option<T>, Controller<T>> controlGetter,
- @NotNull Binding<T> binding,
+ @NotNull StateManager<T> stateManager,
boolean available,
ImmutableSet<OptionFlag> flags,
- @NotNull Collection<BiConsumer<Option<T>, T>> listeners
+ @NotNull Collection<OptionEventListener<T>> listeners
) {
this.name = name;
- this.binding = new SafeBinding<>(binding);
this.available = available;
this.flags = flags;
this.listeners = new ArrayList<>(listeners);
- this.pendingValue = binding.getValue();
+ this.stateManager = stateManager;
this.controller = controlGetter.apply(this);
- addListener((opt, pending) -> description = descriptionFunction.apply(pending));
- triggerListeners(true);
+ this.stateManager.addListener((oldValue, newValue) ->
+ triggerListener(OptionEventListener.Event.STATE_CHANGE, false));
+ addEventListener((opt, event) -> description = descriptionFunction.apply(opt.pendingValue()));
+ triggerListener(OptionEventListener.Event.INITIAL, false);
}
@Override
@@ -74,8 +74,17 @@ public final class OptionImpl<T> implements Option<T> {
}
@Override
+ public @NotNull StateManager<T> stateManager() {
+ return stateManager;
+ }
+
+ @Override
+ @Deprecated
public @NotNull Binding<T> binding() {
- return binding;
+ if (stateManager instanceof ProvidesBindingForDeprecation) {
+ return ((ProvidesBindingForDeprecation<T>) stateManager).getBinding();
+ }
+ throw new UnsupportedOperationException("Binding is not available for this option - using a new state manager which does not directly expose the binding as it may not have one.");
}
@Override
@@ -91,9 +100,9 @@ public final class OptionImpl<T> implements Option<T> {
if (changed) {
if (!available) {
- this.pendingValue = binding().getValue();
+ this.stateManager.sync();
}
- this.triggerListeners(!available);
+ this.triggerListener(OptionEventListener.Event.AVAILABILITY_CHANGE, !available);
}
}
@@ -104,26 +113,25 @@ public final class OptionImpl<T> implements Option<T> {
@Override
public boolean changed() {
- return !binding().getValue().equals(pendingValue);
+ return !this.stateManager.isSynced();
}
@Override
public @NotNull T pendingValue() {
- return pendingValue;
+ return this.stateManager.get();
}
@Override
public void requestSet(@NotNull T value) {
Validate.notNull(value, "`value` cannot be null");
- pendingValue = value;
- this.triggerListeners(true);
+ this.stateManager.set(value);
}
@Override
public boolean applyValue() {
if (changed()) {
- binding().setValue(pendingValue);
+ this.stateManager.apply();
return true;
}
return false;
@@ -131,41 +139,44 @@ public final class OptionImpl<T> implements Option<T> {
@Override
public void forgetPendingValue() {
- requestSet(binding().getValue());
+ this.stateManager.sync();
}
@Override
public void requestSetDefault() {
- requestSet(binding().defaultValue());
+ this.stateManager.resetToDefault(StateManager.ResetAction.BY_OPTION);
}
@Override
public boolean isPendingValueDefault() {
- return binding().defaultValue().equals(pendingValue());
+ return this.stateManager.isDefault();
}
@Override
+ public void addEventListener(OptionEventListener<T> listener) {
+ this.listeners.add(listener);
+ }
+
+ @Override
+ @Deprecated
public void addListener(BiConsumer<Option<T>, T> changedListener) {
- this.listeners.add(changedListener);
+ addEventListener((opt, event) -> changedListener.accept(opt, opt.pendingValue()));
}
- private void triggerListeners(boolean bypass) {
- if (bypass || listenerTriggerDepth == 0) {
- if (listenerTriggerDepth > 10) {
- throw new IllegalStateException("Listener trigger depth exceeded 10! This means a listener triggered a listener etc etc 10 times deep. This is likely a bug in the mod using YACL!");
- }
+ private void triggerListener(OptionEventListener.Event event, boolean allowDepth) {
+ if (allowDepth || currentListenerDepth == 0) {
+ Validate.isTrue(
+ currentListenerDepth <= 10,
+ "Listener depth exceeded 10! Possible cyclic listener pattern: a listener triggered an event that triggered the initial event etc etc."
+ );
- this.listenerTriggerDepth++;
+ currentListenerDepth++;
- for (BiConsumer<Option<T>, T> listener : listeners) {
- try {
- listener.accept(this, pendingValue);
- } catch (Exception e) {
- YACLConstants.LOGGER.error("Exception whilst triggering listener for option '%s'".formatted(name.getString()), e);
- }
+ for (OptionEventListener<T> listener : listeners) {
+ listener.onEvent(this, event);
}
- this.listenerTriggerDepth--;
+ currentListenerDepth--;
}
}
@@ -177,15 +188,17 @@ public final class OptionImpl<T> implements Option<T> {
private Function<Option<T>, Controller<T>> controlGetter;
- private Binding<T> binding;
private boolean available = true;
- private boolean instant = false;
-
private final Set<OptionFlag> flags = new HashSet<>();
- private final List<BiConsumer<Option<T>, T>> listeners = new ArrayList<>();
+ private final List<OptionEventListener<T>> listeners = new ArrayList<>();
+
+ private @Nullable Binding<T> binding;
+ private boolean instantDeprecated = false;
+
+ private @Nullable StateManager<T> stateManager;
@Override
public Builder<T> name(@NotNull Component name) {
@@ -222,8 +235,20 @@ public final class OptionImpl<T> implements Option<T> {
}
@Override
+ public Builder<T> stateManager(@NotNull StateManager<T> stateManager) {
+ Validate.notNull(stateManager, "`stateManager` cannot be null");
+
+ Validate.isTrue(binding == null, "Cannot set state manager when binding is set");
+ Validate.isTrue(!instantDeprecated, "Cannot set state manager when instant is set");
+
+ this.stateManager = stateManager;
+ return this;
+ }
+
+ @Override
public Builder<T> binding(@NotNull Binding<T> binding) {
Validate.notNull(binding, "`binding` cannot be null");
+ Validate.isTrue(stateManager == null, "Cannot set binding when state manager is set");
this.binding = binding;
return this;
@@ -235,8 +260,7 @@ public final class OptionImpl<T> implements Option<T> {
Validate.notNull(getter, "`getter` must not be null");
Validate.notNull(setter, "`setter` must not be null");
- this.binding = Binding.generic(def, getter, setter);
- return this;
+ return binding(Binding.generic(def, getter, setter));
}
@Override
@@ -262,34 +286,70 @@ public final class OptionImpl<T> implements Option<T> {
}
@Override
+ @Deprecated
public Builder<T> instant(boolean instant) {
- this.instant = instant;
+ Validate.isTrue(stateManager == null, "Cannot set instant when state manager is set");
+ YACLConstants.LOGGER.error("Option.Builder#instant is deprecated behaviour. Please use a custom state manager instead: `.state(StateManager.createInstant(Binding))`");
+
+ this.instantDeprecated = instant;
return this;
}
@Override
- public Builder<T> listener(@NotNull BiConsumer<Option<T>, T> listener) {
+ public Builder<T> addListener(@NotNull OptionEventListener<T> listener) {
+ Validate.notNull(listener, "`listener` must not be null");
+
this.listeners.add(listener);
return this;
}
@Override
+ public Builder<T> addListeners(@NotNull Collection<@NotNull OptionEventListener<T>> optionEventListeners) {
+ Validate.notNull(optionEventListeners, "`optionEventListeners` must not be null");
+
+ this.listeners.addAll(optionEventListeners);
+ return this;
+ }
+
+ @Override
+ public Builder<T> listener(@NotNull BiConsumer<Option<T>, T> listener) {
+ Validate.notNull(listener, "`listener` must not be null");
+
+ return this.addListener((opt, event) -> listener.accept(opt, opt.pendingValue()));
+ }
+
+ @Override
public Builder<T> listeners(@NotNull Collection<BiConsumer<Option<T>, T>> listeners) {
- this.listeners.addAll(listeners);
+ Validate.notNull(listeners, "`listeners` must not be null");
+
+ this.addListeners(listeners.stream()
+ .map(listener ->
+ (OptionEventListener<T>) (opt, event) ->
+ listener.accept(opt, opt.pendingValue())
+ ).toList()
+ );
return this;
}
@Override
public Option<T> build() {
Validate.notNull(controlGetter, "`control` must not be null when building `Option`");
- Validate.notNull(binding, "`binding` must not be null when building `Option`");
- Validate.isTrue(!instant || flags.isEmpty(), "instant application does not support option flags");
- if (instant) {
- listeners.add((opt, pendingValue) -> opt.applyValue());
+ if (instantDeprecated) {
+ if (binding == null) {
+ throw new IllegalStateException("Cannot build option with instant when binding is not set");
+ }
+ Validate.isTrue(flags.isEmpty(), "instant application does not support option flags");
+
+ this.stateManager = StateManager.createInstant(binding);
+ } else if (binding != null) {
+ stateManager = StateManager.createSimple(binding);
}
+ Validate.notNull(stateManager, "State manager must be set, either by using .binding() to create a simple manager or .state() to create an advanced one");
+
+ Validate.isTrue(!stateManager.isAlwaysSynced() || flags.isEmpty(), "Always synced state managers do not support option flags.");
- return new OptionImpl<>(name, descriptionFunction, controlGetter, binding, available, ImmutableSet.copyOf(flags), listeners);
+ return new OptionImpl<>(name, descriptionFunction, controlGetter, stateManager, available, ImmutableSet.copyOf(flags), listeners);
}
}
}
diff --git a/src/main/java/dev/isxander/yacl3/impl/ProvidesBindingForDeprecation.java b/src/main/java/dev/isxander/yacl3/impl/ProvidesBindingForDeprecation.java
new file mode 100644
index 0000000..abff9b1
--- /dev/null
+++ b/src/main/java/dev/isxander/yacl3/impl/ProvidesBindingForDeprecation.java
@@ -0,0 +1,7 @@
+package dev.isxander.yacl3.impl;
+
+import dev.isxander.yacl3.api.Binding;
+
+public interface ProvidesBindingForDeprecation<T> {
+ Binding<T> getBinding();
+}
diff --git a/src/main/java/dev/isxander/yacl3/impl/SafeBinding.java b/src/main/java/dev/isxander/yacl3/impl/SafeBinding.java
deleted file mode 100644
index c55d2be..0000000
--- a/src/main/java/dev/isxander/yacl3/impl/SafeBinding.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package dev.isxander.yacl3.impl;
-
-import dev.isxander.yacl3.api.Binding;
-import org.jetbrains.annotations.NotNull;
-
-import java.util.Objects;
-
-public class SafeBinding<T> implements Binding<T> {
- private final Binding<T> binding;
-
- public SafeBinding(Binding<T> binding) {
- this.binding = binding;
- }
-
- @Override
- public @NotNull T getValue() {
- return Objects.requireNonNull(binding.getValue());
- }
-
- @Override
- public void setValue(@NotNull T value) {
- binding.setValue(Objects.requireNonNull(value));
- }
-
- @Override
- public @NotNull T defaultValue() {
- return Objects.requireNonNull(binding.defaultValue());
- }
-}
diff --git a/src/main/java/dev/isxander/yacl3/impl/SelfContainedBinding.java b/src/main/java/dev/isxander/yacl3/impl/SelfContainedBinding.java
new file mode 100644
index 0000000..022cba7
--- /dev/null
+++ b/src/main/java/dev/isxander/yacl3/impl/SelfContainedBinding.java
@@ -0,0 +1,32 @@
+package dev.isxander.yacl3.impl;
+
+import dev.isxander.yacl3.api.Binding;
+
+public class SelfContainedBinding<T> implements Binding<T> {
+ private T value;
+ private final T defaultValue;
+
+ public SelfContainedBinding(T value, T defaultValue) {
+ this.value = value;
+ this.defaultValue = defaultValue;
+ }
+
+ public SelfContainedBinding(T value) {
+ this(value, value);
+ }
+
+ @Override
+ public void setValue(T value) {
+ this.value = value;
+ }
+
+ @Override
+ public T getValue() {
+ return this.value;
+ }
+
+ @Override
+ public T defaultValue() {
+ return this.defaultValue;
+ }
+}
diff --git a/src/main/java/dev/isxander/yacl3/impl/SimpleStateManager.java b/src/main/java/dev/isxander/yacl3/impl/SimpleStateManager.java
new file mode 100644
index 0000000..e9c7275
--- /dev/null
+++ b/src/main/java/dev/isxander/yacl3/impl/SimpleStateManager.java
@@ -0,0 +1,65 @@
+package dev.isxander.yacl3.impl;
+
+import dev.isxander.yacl3.api.Binding;
+import dev.isxander.yacl3.api.StateManager;
+
+public class SimpleStateManager<T> implements StateManager<T>, ProvidesBindingForDeprecation<T> {
+ private T pendingValue;
+ private final Binding<T> binding;
+ private StateListener<T> stateListener;
+
+ public SimpleStateManager(Binding<T> binding) {
+ this.binding = binding;
+ this.pendingValue = binding.getValue();
+ this.stateListener = StateListener.noop();
+ }
+
+ @Override
+ public void set(T value) {
+ boolean changed = !this.pendingValue.equals(value);
+
+ this.pendingValue = value;
+
+ if (changed) stateListener.onStateChange(this.pendingValue, value);
+ }
+
+ @Override
+ public T get() {
+ return pendingValue;
+ }
+
+ @Override
+ public void apply() {
+ binding.setValue(pendingValue);
+ }
+
+ @Override
+ public void resetToDefault(ResetAction action) {
+ this.set(binding.defaultValue());
+ }
+
+ @Override
+ public void sync() {
+ this.set(binding.getValue());
+ }
+
+ @Override
+ public boolean isSynced() {
+ return binding.getValue().equals(pendingValue);
+ }
+
+ @Override
+ public boolean isDefault() {
+ return binding.defaultValue().equals(pendingValue);
+ }
+
+ @Override
+ public void addListener(StateListener<T> stateListener) {
+ this.stateListener = this.stateListener.andThen(stateListener);
+ }
+
+ @Override
+ public Binding<T> getBinding() {
+ return binding;
+ }
+}
diff --git a/src/testmod/java/dev/isxander/yacl3/test/CodecConfig.java b/src/testmod/java/dev/isxander/yacl3/test/CodecConfig.java
index 1a65eb2..1b185ac 100644
--- a/src/testmod/java/dev/isxander/yacl3/test/CodecConfig.java
+++ b/src/testmod/java/dev/isxander/yacl3/test/CodecConfig.java
@@ -9,7 +9,6 @@ import dev.isxander.yacl3.config.v3.JsonFileCodecConfig;
import dev.isxander.yacl3.config.v3.ReadonlyConfigEntry;
import dev.isxander.yacl3.platform.YACLPlatform;
import net.minecraft.network.chat.Component;
-import net.minecraft.network.chat.ComponentSerialization;
import net.minecraft.resources.ResourceLocation;
public class CodecConfig extends JsonFileCodecConfig<CodecConfig> {
@@ -24,8 +23,11 @@ public class CodecConfig extends JsonFileCodecConfig<CodecConfig> {
public final ConfigEntry<ResourceLocation> myIdentifier =
register("my_identifier", YACLPlatform.rl("test"), ResourceLocation.CODEC);
+ //? if >=1.21.2 {
public final ConfigEntry<Component> myText =
- register("my_text", Component.literal("Hello"), ComponentSerialization.CODEC);
+ register("my_text", Component.literal("Hello"),
+ net.minecraft.network.chat.ComponentSerialization.CODEC);
+ //?}
public final ReadonlyConfigEntry<InnerCodecConfig> myInnerConfig =
register("my_inner_config", InnerCodecConfig.INSTANCE);
diff --git a/src/testmod/java/dev/isxander/yacl3/test/GuiTest.java b/src/testmod/java/dev/isxander/yacl3/test/GuiTest.java
index a515fe0..1552a0a 100644
--- a/src/testmod/java/dev/isxander/yacl3/test/GuiTest.java
+++ b/src/testmod/java/dev/isxander/yacl3/test/GuiTest.java
@@ -14,6 +14,7 @@ import dev.isxander.yacl3.gui.controllers.string.number.DoubleFieldController;
import dev.isxander.yacl3.gui.controllers.string.number.FloatFieldController;
import dev.isxander.yacl3.gui.controllers.string.number.IntegerFieldController;
import dev.isxander.yacl3.gui.controllers.string.number.LongFieldController;
+import dev.isxander.yacl3.impl.SelfContainedBinding;
import dev.isxander.yacl3.platform.YACLPlatform;
import net.minecraft.ChatFormatting;
import net.minecraft.Util;
@@ -116,7 +117,7 @@ public class GuiTest {
.controller(opt -> BooleanControllerBuilder.create(opt)
.formatValue(state -> state ? Component.literal("Amazing") : Component.literal("Not Amazing"))
.coloured(true))
- .listener((opt, val) -> booleanOption.get().setAvailable(val))
+ .addListener((opt, event) -> booleanOption.get().setAvailable(opt.pendingValue()))
.build())
.option(Option.<Boolean>createBuilder()
.name(Component.literal("Tick Box"))
@@ -137,6 +138,7 @@ public class GuiTest {
.option(Option.<Integer>createBuilder()
.name(Component.literal("Int Slider"))
.description(OptionDescription.createBuilder()
+ // noinspection deprecated
.gifImage(imageSample("sample4.gif"))
.build())
.binding(
@@ -462,6 +464,7 @@ public class GuiTest {
.description(OptionDescription.of(Component.literal("Yay!!!!!!")))
.build())
.build())
+ .category(sharedStateCategory())
.category(ConfigCategory.createBuilder()
.name(Component.literal("Category Test"))
.option(LabelOption.create(Component.literal("This is a test category!")))
@@ -496,12 +499,69 @@ public class GuiTest {
.build())
.save(() -> {
Minecraft.getInstance().options.save();
- ConfigTest.GSON.serializer().save();
+ ConfigTest.GSON.save();
})
)
.generateScreen(parent);
}
+ private static ConfigCategory sharedStateCategory() {
+ ConfigCategory.Builder builder = ConfigCategory.createBuilder()
+ .name(Component.literal("Shared State Test"));
+
+ StateManager<Boolean> sharedBoolean = StateManager.createSimple(new SelfContainedBinding<>(true));
+ builder.option(Option.<Boolean>createBuilder()
+ .name(Component.literal("Shared Boolean"))
+ .stateManager(sharedBoolean)
+ .controller(TickBoxControllerBuilder::create)
+ .build());
+ builder.option(Option.<Boolean>createBuilder()
+ .name(Component.literal("Shared Boolean"))
+ .stateManager(sharedBoolean)
+ .controller(BooleanControllerBuilder::create)
+ .build());
+
+ StateManager<String> sharedString = StateManager.createSimple(new SelfContainedBinding<>(""));
+ builder.option(Option.<String>createBuilder()
+ .name(Component.literal("Shared String"))
+ .stateManager(sharedString)
+ .controller(StringControllerBuilder::create)
+ .build());
+ builder.option(Option.<String>createBuilder()
+ .name(Component.literal("Shared String"))
+ .stateManager(sharedString)
+ .controller(StringControllerBuilder::create)
+ .build());
+
+ StateManager<Integer> sharedInt = StateManager.createSimple(new SelfContainedBinding<>(0));
+ builder.option(Option.<Integer>createBuilder()
+ .name(Component.literal("Shared Int"))
+ .stateManager(sharedInt)
+ .controller(opt -> IntegerSliderControllerBuilder.create(opt)
+ .range(0, 10).step(1))
+ .build());
+ builder.option(Option.<Integer>createBuilder()
+ .name(Component.literal("Shared Int"))
+ .stateManager(sharedInt)
+ .controller(IntegerFieldControllerBuilder::create)
+ .build());
+
+ StateManager<Color> sharedColor = StateManager.createSimple(new SelfContainedBinding<>(Color.RED));
+ builder.option(Option.<Color>createBuilder()
+ .name(Component.literal("Shared Color"))
+ .stateManager(sharedColor)
+ .controller(ColorControllerBuilder::create)
+ .build());
+ builder.option(Option.<Color>createBuilder()
+ .name(Component.literal("Shared Color"))
+ .stateManager(sharedColor)
+ .controller(opt -> ColorControllerBuilder.create(opt)
+ .allowAlpha(true))
+ .build());
+
+ return builder.build();
+ }
+
private static ResourceLocation imageSample(String name) {
return YACLPlatform.rl("yacl_test", "textures/images/" + name);
}
diff --git a/src/testmod/kotlin/dev/isxander/yacl3/test/DslTest.kt b/src/testmod/kotlin/dev/isxander/yacl3/test/DslTest.kt
index ac162a1..35e1a4d 100644
--- a/src/testmod/kotlin/dev/isxander/yacl3/test/DslTest.kt
+++ b/src/testmod/kotlin/dev/isxander/yacl3/test/DslTest.kt
@@ -8,6 +8,8 @@ import dev.isxander.yacl3.dsl.*
import dev.isxander.yacl3.platform.YACLPlatform
import net.minecraft.client.gui.screens.Screen
import net.minecraft.network.chat.Component
+// 1.20.1 doesn't have a component codec
+//? if >1.20.1
import net.minecraft.network.chat.ComponentSerialization
import net.minecraft.resources.ResourceLocation
@@ -20,6 +22,8 @@ object CodecConfigKt : JsonFileCodecConfig<CodecConfigKt>(YACLPlatform.getConfig
val myIdentifier by register<ResourceLocation>(YACLPlatform.rl("test"), ResourceLocation.CODEC)
+ // 1.20.1 doesn't have a component codec
+ //? if >1.20.1
val myText by register<Component>(Component.literal("Hello, World!"), ComponentSerialization.CODEC)
init {
@@ -110,7 +114,7 @@ object CodecConfigKt : JsonFileCodecConfig<CodecConfigKt>(YACLPlatform.getConfig
// you can access other options like this!
// `options` field is from the enclosing group dsl
- listener { opt, newVal ->
+ addListener { opt, event ->
options.futureRef<String>("myString").onReady {
}
diff --git a/stonecutter.gradle.kts b/stonecutter.gradle.kts
index 79cc121..5d77d3c 100644
--- a/stonecutter.gradle.kts
+++ b/stonecutter.gradle.kts
@@ -1,13 +1,13 @@
plugins {
id("dev.kikugie.stonecutter")
- id("dev.architectury.loom") version "1.6.+" apply false
+ id("dev.architectury.loom") version "1.7.+" apply false
kotlin("jvm") version "1.9.23" apply false
id("me.modmuss50.mod-publish-plugin") version "0.5.+" apply false
id("org.ajoberstar.grgit") version "5.0.+" apply false
}
-stonecutter active "1.21-fabric" /* [SC] DO NOT EDIT */
+stonecutter active "1.21.2-fabric" /* [SC] DO NOT EDIT */
stonecutter.configureEach {
val platform = project.property("loom.platform")
@@ -37,3 +37,8 @@ stonecutter registerChiseled tasks.register("chiseledPublishSnapshots", stonecut
group = "mod"
ofTask("publishAllPublicationsToXanderSnapshotsRepository")
}
+
+stonecutter registerChiseled tasks.register("chiseledRunTestmod", stonecutter.chiseled) {
+ group = "mod"
+ ofTask("runTestmodClient")
+}
diff --git a/versions/1.21-fabric/gradle.properties b/versions/1.21-fabric/gradle.properties
index df44e87..f67c5e8 100644
--- a/versions/1.21-fabric/gradle.properties
+++ b/versions/1.21-fabric/gradle.properties
@@ -5,4 +5,4 @@ java.version=21
deps.quiltMappings=
deps.fabricApi=0.100.1+1.21
-fmj.mcDep=~1.21
+fmj.mcDep=~1.21 <1.21.2
diff --git a/versions/1.21.2-fabric/gradle.properties b/versions/1.21.2-fabric/gradle.properties
new file mode 100644
index 0000000..ab1a162
--- /dev/null
+++ b/versions/1.21.2-fabric/gradle.properties
@@ -0,0 +1,8 @@
+loom.platform=fabric
+mcVersion=1.21.2-rc1
+
+java.version=21
+
+deps.quiltMappings=
+deps.fabricApi=0.106.0+1.21.2
+fmj.mcDep=~1.21.2-