diff options
Diffstat (limited to 'src/main/java/dev/isxander/yacl3/impl')
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; + } +} |