aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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() {
+