aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/dev/isxander/yacl3/impl
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/dev/isxander/yacl3/impl')
-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
13 files changed, 544 insertions, 229 deletions
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;
+ }
+}