diff --git a/build_logic/src/main/kotlin/Constants.kt b/build_logic/src/main/kotlin/Constants.kt
index 45e01ea..6cafb43 100644
--- a/build_logic/src/main/kotlin/Constants.kt
+++ b/build_logic/src/main/kotlin/Constants.kt
@@ -2,7 +2,7 @@ data class Developer(val name: String, val email: String)
object Constants {
const val GROUP = "dev.yumi.commons"
- const val VERSION = "1.0.0-alpha.1"
+ const val VERSION = "1.0.0-alpha.2"
const val JAVA_VERSION = 17
const val PROJECT_NAME = "Yumi Commons"
diff --git a/libraries/event/src/main/java/dev/yumi/commons/event/Event.java b/libraries/event/src/main/java/dev/yumi/commons/event/Event.java
index 9b7c9db..86c950f 100644
--- a/libraries/event/src/main/java/dev/yumi/commons/event/Event.java
+++ b/libraries/event/src/main/java/dev/yumi/commons/event/Event.java
@@ -27,6 +27,8 @@
package dev.yumi.commons.event;
import dev.yumi.commons.collections.toposort.NodeSorting;
+import dev.yumi.commons.collections.toposort.SortableNode;
+import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
@@ -52,7 +54,7 @@
* of implementing an invoker and only allows listener implementations to be done by implementing an interface onto a
* class or extending a class.
*
- * An Event can have phases, each listener is attributed to a phase ({@link Event#getDefaultPhaseId()} if unspecified),
+ * An Event can have phases, each listener is attributed to a phase ({@link Event#defaultPhaseId()} if unspecified),
* and each phase can have a defined ordering. Each event phase is identified, ordering is done
* by explicitly stating that event phase {@code A} will run before event phase {@code B}, for example.
* See {@link Event#addPhaseOrdering(Comparable, Comparable)} for more information.
@@ -114,11 +116,13 @@
* }
*
* @param the phase identifier type
- * @param the type of the invoker used to execute an event and the type of the listener
+ * @param the type of the listeners, and the type of the invoker used to execute an event
* @version 1.0.0
* @since 1.0.0
*/
-public final class Event, T> {
+public sealed class Event, T>
+ implements InvokableEvent
+ permits FilteredEvent {
/**
* The type of listener of this event.
*/
@@ -130,8 +134,8 @@ public final class Event, T> {
/**
* The function used to generate the implementation of the invoker to call the listeners.
*/
- private final Function invokerFactory;
- private final Lock lock = new ReentrantLock();
+ final Function invokerFactory;
+ final Lock lock = new ReentrantLock();
/**
* The invoker to execute the callbacks.
*/
@@ -139,15 +143,15 @@ public final class Event, T> {
/**
* The registered listeners.
*/
- private T[] listeners;
+ T[] listeners;
/**
* The registered event phases.
*/
- private final Map> phases = new LinkedHashMap<>();
+ final Map> phases = new LinkedHashMap<>();
/**
* The event phases sorted in a way that satisfies dependencies.
*/
- private final List> sortedPhases = new ArrayList<>();
+ final List> sortedPhases = new ArrayList<>();
@SuppressWarnings("unchecked")
Event(
@@ -170,7 +174,7 @@ public final class Event, T> {
* {@return the class of the kind of listeners accepted by this event}
*/
@Contract(pure = true)
- public @NotNull Class super T> getType() {
+ public @NotNull Class super T> type() {
return this.type;
}
@@ -178,7 +182,7 @@ public final class Event, T> {
* {@return the default phase identifier of this event}
*/
@Contract(pure = true)
- public @NotNull I getDefaultPhaseId() {
+ public @NotNull I defaultPhaseId() {
return this.defaultPhaseId;
}
@@ -235,44 +239,42 @@ public void addPhaseOrdering(@NotNull I firstPhase, @NotNull I secondPhase) {
var first = this.getOrCreatePhase(firstPhase, false);
var second = this.getOrCreatePhase(secondPhase, false);
- EventPhaseData.link(first, second);
- NodeSorting.sort(this.sortedPhases, "event phases");
+ PhaseData.link(first, second);
+ this.sortPhases();
this.rebuildInvoker(this.listeners.length);
} finally {
this.lock.unlock();
}
}
- /**
- * {@return the invoker instance used to execute this event}
- *
- * The result of this method should not be stored since the invoker may become invalid
- * at any time. Always call this method when you intend to execute an event.
- */
- @Contract(pure = true)
+ @Override
public @NotNull T invoker() {
return this.invoker;
}
/* Implementation */
- private EventPhaseData getOrCreatePhase(I id, boolean sortIfCreate) {
+ PhaseData getOrCreatePhase(@NotNull I id, boolean sortIfCreate) {
var phase = this.phases.get(id);
if (phase == null) {
- phase = new EventPhaseData<>(id, this.type);
+ phase = new PhaseData<>(id, this.type);
this.phases.put(id, phase);
this.sortedPhases.add(phase);
if (sortIfCreate) {
- NodeSorting.sort(this.sortedPhases, "event phases");
+ this.sortPhases();
}
}
return phase;
}
- private void rebuildInvoker(int newLength) {
+ void sortPhases() {
+ NodeSorting.sort(this.sortedPhases, "event phases");
+ }
+
+ void rebuildInvoker(int newLength) {
if (this.sortedPhases.size() == 1) {
// There's a single phase, so we can directly use its listeners.
this.listeners = this.sortedPhases.get(0).listeners;
@@ -297,7 +299,7 @@ private void rebuildInvoker(int newLength) {
this.update();
}
- private void update() {
+ void update() {
// Make a copy of the array given to the invoker factory so the entries cannot be mutated.
this.invoker = this.invokerFactory.apply(
Arrays.copyOf(this.listeners, this.listeners.length)
@@ -315,4 +317,36 @@ public String toString() {
", sortedPhases=" + this.sortedPhases +
'}';
}
+
+ /**
+ * Represents data for a specific event phase.
+ *
+ * @param the phase identifier type
+ * @param the type of the listeners
+ */
+ @ApiStatus.Internal
+ static sealed class PhaseData extends SortableNode>
+ permits FilteredEvent.FilteredPhaseData {
+ private final I id;
+ T[] listeners;
+
+ @SuppressWarnings("unchecked")
+ PhaseData(@NotNull I id, @NotNull Class super T> listenerType) {
+ Objects.requireNonNull(id);
+
+ this.id = id;
+ this.listeners = (T[]) Array.newInstance(listenerType, 0);
+ }
+
+ @Override
+ public @NotNull I getId() {
+ return this.id;
+ }
+
+ void addListener(@NotNull T listener) {
+ int oldLength = this.listeners.length;
+ this.listeners = Arrays.copyOf(this.listeners, oldLength + 1);
+ this.listeners[oldLength] = listener;
+ }
+ }
}
diff --git a/libraries/event/src/main/java/dev/yumi/commons/event/EventManager.java b/libraries/event/src/main/java/dev/yumi/commons/event/EventManager.java
index b958249..c88313b 100644
--- a/libraries/event/src/main/java/dev/yumi/commons/event/EventManager.java
+++ b/libraries/event/src/main/java/dev/yumi/commons/event/EventManager.java
@@ -36,6 +36,7 @@
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
+import java.util.function.Supplier;
/**
* Represents an {@link Event} manager.
@@ -62,7 +63,7 @@ public EventManager(@NotNull I defaultPhaseId, @NotNull Function phas
* Creates a new instance of {@link Event}.
*
* @param invokerFactory the factory which generates an invoker implementation using an array of listeners
- * @param the type of the invoker executed by the event
+ * @param the type of the listeners of the event
* @return a new event instance
* @see #create(Class)
* @see #create(Class, Function)
@@ -86,8 +87,8 @@ public EventManager(@NotNull I defaultPhaseId, @NotNull Function phas
* and whenever a listener returns another value it returns early.
*
*
- * @param type the class representing the type of the invoker that is executed by the event
- * @param the type of the invoker executed by the event
+ * @param type the class representing the type of the listeners of the event
+ * @param the type of the listeners of the event
* @return a new event instance
* @see #create(InvokerFactory)
* @see #create(Class, Function)
@@ -104,9 +105,9 @@ public EventManager(@NotNull I defaultPhaseId, @NotNull Function phas
/**
* Creates a new instance of {@link Event}.
*
- * @param type the class representing the type of the invoker that is executed by the event
+ * @param type the class representing the type of the listeners of the event
* @param implementation a function which generates an invoker implementation using an array of listeners
- * @param the type of the invoker executed by the event
+ * @param the type of the listeners of the event
* @return a new event instance
* @see #create(Class)
* @see #create(InvokerFactory)
@@ -129,10 +130,10 @@ public EventManager(@NotNull I defaultPhaseId, @NotNull Function phas
* as the render or tick loops. Otherwise, the other {@link #create(Class, Function)} method should work
* in 99% of cases with little to no performance overhead.
*
- * @param type the class representing the type of the invoker that is executed by the event
+ * @param type the class representing the type of the listeners of the event
* @param emptyImplementation the implementation of T to use when the array event has no listener registrations
* @param implementation a function which generates an invoker implementation using an array of listeners
- * @param the type of the invoker executed by the event
+ * @param the type of the listeners of the event
* @return a new event instance
* @see #create(Class)
* @see #create(InvokerFactory)
@@ -171,8 +172,8 @@ public EventManager(@NotNull I defaultPhaseId, @NotNull Function phas
*
* @param invokerFactory the factory which generates an invoker implementation using an array of listeners
* @param defaultPhases the default phases of this event, in the correct order.
- * Must contain {@link EventManager#getDefaultPhaseId() the default phase identifier}
- * @param the type of the invoker executed by the event
+ * Must contain {@link EventManager#defaultPhaseId() the default phase identifier}
+ * @param the type of the listeners of the event
* @return a new event instance
* @see #create(Class)
* @see #create(InvokerFactory)
@@ -205,10 +206,10 @@ public EventManager(@NotNull I defaultPhaseId, @NotNull Function phas
*
* This method uses an automatically generated invoker implementation.
*
- * @param type the class representing the type of the invoker that is executed by the event
+ * @param type the class representing the type of the listeners of the event
* @param defaultPhases the default phases of this event, in the correct order.
- * Must contain {@link EventManager#getDefaultPhaseId() the default phase identifier}
- * @param the type of the invoker executed by the event
+ * Must contain {@link EventManager#defaultPhaseId() the default phase identifier}
+ * @param the type of the listeners of the event
* @return a new event instance
* @see #create(Class)
* @see #create(InvokerFactory)
@@ -239,11 +240,11 @@ public EventManager(@NotNull I defaultPhaseId, @NotNull Function phas
*
* Refer to {@link Event#addPhaseOrdering} for an explanation of event phases.
*
- * @param type the class representing the type of the invoker that is executed by the event
+ * @param type the class representing the type of the listeners of the event
* @param implementation a function which generates an invoker implementation using an array of listeners
* @param defaultPhases the default phases of this event, in the correct order.
- * Must contain {@link EventManager#getDefaultPhaseId() the default phase identifier}
- * @param the type of the invoker executed by the event
+ * Must contain {@link EventManager#defaultPhaseId() the default phase identifier}
+ * @param the type of the listeners of the event
* @return a new event instance
* @see #create(Class)
* @see #create(InvokerFactory)
@@ -258,20 +259,234 @@ public EventManager(@NotNull I defaultPhaseId, @NotNull Function phas
@NotNull Function implementation,
@NotNull I... defaultPhases
) {
- this.ensureContainsDefaultPhase(defaultPhases);
- YumiAssertions.ensureNoDuplicates(defaultPhases, id -> new IllegalArgumentException("Duplicate event phase: " + id));
+ return this.createWithPhases(() -> new Event<>(type, this.defaultPhaseId, implementation), defaultPhases);
+ }
- var event = this.create(type, implementation);
+ /**
+ * Creates a new instance of {@link FilteredEvent}.
+ *
+ * @param contextType the class of the context
+ * @param invokerFactory the factory which generates an invoker implementation using an array of listeners
+ * @param the type of the listeners of the event
+ * @param the type of the filtering context
+ * @return a new filtered event instance
+ * @see #createFiltered(Class, Class)
+ * @see #createFiltered(Class, Class, Function)
+ * @see #createFiltered(Class, Class, Object, Function)
+ * @see #createFilteredWithPhases(Class, InvokerFactory, Comparable[])
+ * @see #createFilteredWithPhases(Class, Class, Comparable[])
+ * @see #createFilteredWithPhases(Class, Class, Function, Comparable[])
+ */
+ public @NotNull FilteredEvent createFiltered(@NotNull Class super C> contextType, @NotNull InvokerFactory invokerFactory) {
+ return this.createFiltered(invokerFactory.type(), contextType, invokerFactory);
+ }
- for (int i = 1; i < defaultPhases.length; ++i) {
- event.addPhaseOrdering(defaultPhases[i - 1], defaultPhases[i]);
- }
+ /**
+ * Creates a new instance of {@link FilteredEvent} for which the invoker implementation is automatically generated.
+ *
+ * The invoker implementation is automatically generated given the following conditions:
+ *
+ * - the listener doesn't return anything;
+ * - the listener returns a {@code boolean} as some kind of filter;
+ * - the listener returns a {@link dev.yumi.commons.TriState}, for which {@link dev.yumi.commons.TriState#DEFAULT} is the default return value,
+ * and whenever a listener returns another value it returns early.
+ *
+ *
+ * @param type the class representing the type of the listeners of the event
+ * @param contextType the class of the context
+ * @param the type of the listeners of the event
+ * @param the type of the filtering context
+ * @return a new filtered event instance
+ * @see #createFiltered(Class, InvokerFactory)
+ * @see #createFiltered(Class, Class, Function)
+ * @see #createFiltered(Class, Class, Object, Function)
+ * @see #createFilteredWithPhases(Class, InvokerFactory, Comparable[])
+ * @see #createFilteredWithPhases(Class, Class, Comparable[])
+ * @see #createFilteredWithPhases(Class, Class, Function, Comparable[])
+ * @see DefaultInvokerFactory the invoker factory used for this event
+ */
+ public @NotNull FilteredEvent createFiltered(@NotNull Class super T> type, @NotNull Class super C> contextType) {
+ return this.createFiltered(type, contextType, new DefaultInvokerFactory<>(type));
+ }
+ /**
+ * Creates a new instance of {@link FilteredEvent}.
+ *
+ * @param type the class representing the type of the listeners of the event
+ * @param contextType the class of the context
+ * @param implementation a function which generates an invoker implementation using an array of listeners
+ * @param the type of the listeners of the event
+ * @param the type of the filtering context
+ * @return a new filtered event instance
+ * @see #createFiltered(Class, Class)
+ * @see #createFiltered(Class, InvokerFactory)
+ * @see #createFiltered(Class, Class, Object, Function)
+ * @see #createFilteredWithPhases(Class, InvokerFactory, Comparable[])
+ * @see #createFilteredWithPhases(Class, Class, Comparable[])
+ * @see #createFilteredWithPhases(Class, Class, Function, Comparable[])
+ */
+ public @NotNull FilteredEvent createFiltered(
+ @NotNull Class super T> type,
+ @NotNull Class super C> contextType,
+ @NotNull Function implementation
+ ) {
+ var event = new FilteredEvent(type, this.defaultPhaseId, implementation);
this.creationEvent.invoker().onEventCreation(this, event);
-
return event;
}
+ /**
+ * Creates a new instance of {@link FilteredEvent}.
+ *
+ * This method adds a {@code emptyImplementation} parameter which provides an implementation of the invoker
+ * when no listeners are registered. Generally this method should only be used when the code path is very hot, such
+ * as the render or tick loops. Otherwise, the other {@link #createFiltered(Class, Class, Function)} method should work
+ * in 99% of cases with little to no performance overhead.
+ *
+ * @param type the class representing the type of the listeners of the event
+ * @param contextType the class of the context
+ * @param emptyImplementation the implementation of T to use when the array event has no listener registrations
+ * @param implementation a function which generates an invoker implementation using an array of listeners
+ * @param the type of the listeners of the event
+ * @param the type of the filtering context
+ * @return a new filtered event instance
+ * @see #createFiltered(Class, Class)
+ * @see #createFiltered(Class, InvokerFactory)
+ * @see #createFiltered(Class, Class, Function)
+ * @see #createFilteredWithPhases(Class, InvokerFactory, Comparable[])
+ * @see #createFilteredWithPhases(Class, Class, Comparable[])
+ * @see #createFilteredWithPhases(Class, Class, Function, Comparable[])
+ */
+ public @NotNull FilteredEvent createFiltered(
+ @NotNull Class super T> type,
+ @NotNull Class super C> contextType,
+ @NotNull T emptyImplementation,
+ @NotNull Function implementation
+ ) {
+ return this.createFiltered(type, contextType, listeners -> switch (listeners.length) {
+ case 0 -> emptyImplementation;
+ case 1 -> listeners[0];
+ // We can ensure the implementation may not remove elements from the backing array since the array given to
+ // this method is a copy of the backing array.
+ default -> implementation.apply(listeners);
+ });
+ }
+
+ /**
+ * Create a new instance of {@link FilteredEvent} with a list of default phases that get invoked in order.
+ * Exposing the identifiers of the default phases as {@code public static final} constants is encouraged.
+ *
+ * An event phase is a named group of listeners, which may be ordered before or after other groups of listeners.
+ * This allows some listeners to take priority over other listeners.
+ * Adding separate events should be considered before making use of multiple event phases.
+ *
+ * Phases may be freely added to events created with any of the factory functions,
+ * however using this function is preferred for widely used event phases.
+ * If more phases are necessary, discussion with the author of the event is encouraged.
+ *
+ * Refer to {@link Event#addPhaseOrdering} for an explanation of event phases.
+ *
+ * @param contextType the class of the context
+ * @param invokerFactory the factory which generates an invoker implementation using an array of listeners
+ * @param defaultPhases the default phases of this event, in the correct order.
+ * Must contain {@link EventManager#defaultPhaseId() the default phase identifier}
+ * @param the type of the listeners of the event
+ * @param the type of the filtering context
+ * @return a new filtered event instance
+ * @see #createFiltered(Class, Class)
+ * @see #createFiltered(Class, InvokerFactory)
+ * @see #createFiltered(Class, Class, Function)
+ * @see #createFiltered(Class, Class, Object, Function)
+ * @see #createFilteredWithPhases(Class, Class, Comparable[])
+ * @see #createFilteredWithPhases(Class, Class, Function, Comparable[])
+ */
+ @SuppressWarnings("unchecked")
+ public @NotNull FilteredEvent createFilteredWithPhases(
+ @NotNull Class contextType,
+ @NotNull InvokerFactory invokerFactory,
+ @NotNull I... defaultPhases
+ ) {
+ return this.createFilteredWithPhases(invokerFactory.type(), contextType, invokerFactory, defaultPhases);
+ }
+
+ /**
+ * Create a new instance of {@link FilteredEvent} with a list of default phases that get invoked in order.
+ * Exposing the identifiers of the default phases as {@code public static final} constants is encouraged.
+ *
+ * An event phase is a named group of listeners, which may be ordered before or after other groups of listeners.
+ * This allows some listeners to take priority over other listeners.
+ * Adding separate events should be considered before making use of multiple event phases.
+ *
+ * Phases may be freely added to events created with any of the factory functions,
+ * however using this function is preferred for widely used event phases.
+ * If more phases are necessary, discussion with the author of the event is encouraged.
+ *
+ * Refer to {@link Event#addPhaseOrdering} for an explanation of event phases.
+ *
+ * This method uses an automatically generated invoker implementation.
+ *
+ * @param type the class representing the type of the invoker that is executed by the event
+ * @param contextType the class of the context
+ * @param defaultPhases the default phases of this event, in the correct order.
+ * Must contain {@link EventManager#defaultPhaseId() the default phase identifier}
+ * @param the type of the listeners of the event
+ * @param the type of the filtering context
+ * @return a new filtered event instance
+ * @see #createFiltered(Class, Class)
+ * @see #createFiltered(Class, InvokerFactory)
+ * @see #createFiltered(Class, Class, Function)
+ * @see #createFiltered(Class, Class, Object, Function)
+ * @see #createFilteredWithPhases(Class, InvokerFactory, Comparable[])
+ * @see #createFilteredWithPhases(Class, Class, Function, Comparable[])
+ */
+ @SuppressWarnings("unchecked")
+ public @NotNull FilteredEvent createFilteredWithPhases(
+ @NotNull Class super T> type,
+ @NotNull Class contextType,
+ @NotNull I... defaultPhases
+ ) {
+ return this.createFilteredWithPhases(type, contextType, new DefaultInvokerFactory<>(type), defaultPhases);
+ }
+
+ /**
+ * Create a new instance of {@link FilteredEvent} with a list of default phases that get invoked in order.
+ * Exposing the identifiers of the default phases as {@code public static final} constants is encouraged.
+ *
+ * An event phase is a named group of listeners, which may be ordered before or after other groups of listeners.
+ * This allows some listeners to take priority over other listeners.
+ * Adding separate events should be considered before making use of multiple event phases.
+ *
+ * Phases may be freely added to events created with any of the factory functions,
+ * however using this function is preferred for widely used event phases.
+ * If more phases are necessary, discussion with the author of the event is encouraged.
+ *
+ * Refer to {@link Event#addPhaseOrdering} for an explanation of event phases.
+ *
+ * @param type the class representing the type of the invoker that is executed by the event
+ * @param contextType the class of the context
+ * @param implementation a function which generates an invoker implementation using an array of listeners
+ * @param defaultPhases the default phases of this event, in the correct order.
+ * Must contain {@link EventManager#defaultPhaseId() the default phase identifier}
+ * @param the type of the listeners of the event
+ * @param the type of the filtering context
+ * @return a new filtered event instance
+ * @see #createFiltered(Class, Class)
+ * @see #createFiltered(Class, InvokerFactory)
+ * @see #createFiltered(Class, Class, Function)
+ * @see #createFiltered(Class, Class, Object, Function)
+ * @see #createFilteredWithPhases(Class, InvokerFactory, Comparable[])
+ * @see #createFilteredWithPhases(Class, Class, Comparable[])
+ */
+ @SafeVarargs
+ public final @NotNull FilteredEvent createFilteredWithPhases(
+ @NotNull Class super T> type,
+ @NotNull Class contextType,
+ @NotNull Function implementation,
+ @NotNull I... defaultPhases
+ ) {
+ return this.createWithPhases(() -> new FilteredEvent<>(type, this.defaultPhaseId, implementation), defaultPhases);
+ }
+
/**
* Registers the listener to the specified events.
*
@@ -296,20 +511,20 @@ public final void listenAll(Object listener, Event... events) {
// Check whether we actually can register stuff. We only commit the registration if all events can.
for (var event : events) {
- if (!event.getType().isAssignableFrom(listener.getClass())) {
+ if (!event.type().isAssignableFrom(listener.getClass())) {
throw new IllegalArgumentException("Given object " + listener + " is not a listener of event " + event);
}
- if (event.getType().getTypeParameters().length > 0) {
+ if (event.type().getTypeParameters().length > 0) {
throw new IllegalArgumentException("Cannot register a listener for the event " + event + " which is using generic parameters with listenAll.");
}
- listenedPhases.putIfAbsent(event.getType(), this.defaultPhaseId);
+ listenedPhases.putIfAbsent(event.type(), this.defaultPhaseId);
}
// We can register, so we do!
for (var event : events) {
- ((Event) event).register(listenedPhases.get(event.getType()), listener);
+ ((Event) event).register(listenedPhases.get(event.type()), listener);
}
}
@@ -317,7 +532,7 @@ public final void listenAll(Object listener, Event... events) {
* {@return the default phase identifier of all the events created by this event manager}
*/
@Contract(pure = true)
- public @NotNull I getDefaultPhaseId() {
+ public @NotNull I defaultPhaseId() {
return this.defaultPhaseId;
}
@@ -329,6 +544,26 @@ public final void listenAll(Object listener, Event... events) {
return this.creationEvent;
}
+ /* Implementation */
+
+ private > E createWithPhases(
+ Supplier eventSupplier,
+ @NotNull I... defaultPhases
+ ) {
+ this.ensureContainsDefaultPhase(defaultPhases);
+ YumiAssertions.ensureNoDuplicates(defaultPhases, id -> new IllegalArgumentException("Duplicate event phase: " + id));
+
+ var event = eventSupplier.get();
+
+ for (int i = 1; i < defaultPhases.length; i++) {
+ event.addPhaseOrdering(defaultPhases[i - 1], defaultPhases[i]);
+ }
+
+ this.creationEvent.invoker().onEventCreation(this, event);
+
+ return event;
+ }
+
private Map, I> getListenedPhases(Class> listenerClass) {
var map = new HashMap, I>();
diff --git a/libraries/event/src/main/java/dev/yumi/commons/event/EventPhaseData.java b/libraries/event/src/main/java/dev/yumi/commons/event/EventPhaseData.java
deleted file mode 100644
index 0dc251b..0000000
--- a/libraries/event/src/main/java/dev/yumi/commons/event/EventPhaseData.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright 2023 Yumi Project
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package dev.yumi.commons.event;
-
-import dev.yumi.commons.collections.toposort.SortableNode;
-import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.NotNull;
-
-import java.lang.reflect.Array;
-import java.util.Arrays;
-import java.util.Objects;
-
-@ApiStatus.Internal
-final class EventPhaseData extends SortableNode> {
- private final I id;
- T[] listeners;
-
- @SuppressWarnings("unchecked")
- EventPhaseData(@NotNull I id, @NotNull Class super T> listenerType) {
- Objects.requireNonNull(id);
-
- this.id = id;
- this.listeners = (T[]) Array.newInstance(listenerType, 0);
- }
-
- @Override
- public @NotNull I getId() {
- return this.id;
- }
-
- public void addListener(@NotNull T listener) {
- int oldLength = this.listeners.length;
- this.listeners = Arrays.copyOf(this.listeners, oldLength + 1);
- this.listeners[oldLength] = listener;
- }
-}
diff --git a/libraries/event/src/main/java/dev/yumi/commons/event/FilteredEvent.java b/libraries/event/src/main/java/dev/yumi/commons/event/FilteredEvent.java
new file mode 100644
index 0000000..90c3949
--- /dev/null
+++ b/libraries/event/src/main/java/dev/yumi/commons/event/FilteredEvent.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright 2024 Yumi Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package dev.yumi.commons.event;
+
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+/**
+ * Represents an {@linkplain Event event} which can filter its listeners given an invocation context.
+ *
+ * This type of event will respect the same rules and assumptions as regular events.
+ *
+ *
Example: Registering filtered listeners
+ *
+ * Similar to how you would register a listener in a regular event, you pass an instance of {@code T} into {@link #register}.
+ * Using the same {@link #register(Object) register} methods from {@link Event} will result in registering a global listener
+ * which will be invoked no matter the context.
+ * To make your listener context-specific you need to add a predicate given a context of type {@code C}.
+ *
+ *
{@code
+ * // Events are created and managed by an EventManager.
+ * // They are given a type for the phase identifiers and the default phase identifier.
+ * static final EventManager EVENT_MANAGER = new EventManager("default");
+ *
+ * // Events should use a dedicated functional interface for T rather than overloading multiple events to the same type
+ * // to allow those who implement using a class to implement multiple events.
+ * @FunctionalInterface
+ * public interface Example {
+ * void doSomething();
+ * }
+ *
+ * // Filtered events also have an invocation context.
+ * public record EventContext(String value) {}
+ *
+ * // You can also return this instance of Event from a method, may be useful where a parameter is needed to get
+ * // the right instance of Event.
+ * public static final Event EXAMPLE = EVENT_MANAGER.create(Example.class);
+ *
+ * public void registerEvents() {
+ * // Since T is a functional interface, we can use the lambda form.
+ * EXAMPLE.register(() -> {
+ * // Do something
+ * }, context -> context.value().equals("test"));
+ *
+ * // Or we can use a method reference.
+ * EXAMPLE.register(this::runSomething, context -> context.value().equals("some other context"));
+ *
+ * // Or implement T using a class.
+ * // You can also use an anonymous class here; for brevity that is not included.
+ * EXAMPLE.register(new ImplementedOntoClass()); // This is a global listener.
+ * }
+ *
+ * public void runSomething() {
+ * // Do something else
+ * }
+ *
+ * // When implementing onto a class, the class must implement the same type as the event invoker.
+ * class ImplementedOntoClass implements Example {
+ * public void doSomething() {
+ * // Do something else again
+ * }
+ * }
+ * }
+ *
+ * Example: Executing a filtered event
+ *
+ * While you could execute a filtered event the same way a regular event is executed,
+ * it would only invoke global listeners due to it not being aware of a context.
+ * Executing a filtered event is done by first creating a subset of listeners from the event using {@link #forContext(Object)} for a given context,
+ * then calling a method on the event invoker. Where {@code T} is Example, executing a filtered event
+ * is done through the following:
+ *
+ *
{@code
+ * InvokerSubset subset = EXAMPLE.forContext(new EventContext("test"));
+ *
+ * // Invoke the listeners relevant to the given context.
+ * subset.invoker().doSomething();
+ * }
+ *
+ * This architecture has advantages only if the subset is stored and only re-created if the context is different.
+ * Otherwise, a regular {@linkplain Event event} would do just fine.
+ *
+ * @param the phase identifier type
+ * @param the type of the listeners, and the type of the invoker used to execute an event
+ * @param the type of the context used to filter out which listeners should be invoked
+ * @author LambdAurora
+ * @version 1.0.0
+ * @since 1.0.0
+ */
+public final class FilteredEvent, T, C> extends Event {
+ /**
+ * Reference queue for cleared invoker subsets.
+ */
+ private final ReferenceQueue> queue = new ReferenceQueue<>();
+ /**
+ * A cache of currently alive invoker subsets.
+ */
+ private final Set>> subsets = new HashSet<>();
+
+ FilteredEvent(@NotNull Class super T> type, @NotNull I defaultPhaseId, @NotNull Function invokerFactory) {
+ super(type, defaultPhaseId, invokerFactory);
+ }
+
+ /**
+ * Registers a listener to this event, which will be called only if the execution context matches the filter.
+ *
+ * @param listener the listener to register
+ * @param filter the predicate to test whether the listener to register should be called within a given context
+ * @see #register(Object)
+ * @see #register(Comparable, Object)
+ * @see #register(Comparable, Object, Predicate)
+ */
+ public void register(@NotNull T listener, @NotNull Predicate filter) {
+ this.register(this.defaultPhaseId(), listener, filter);
+ }
+
+ /**
+ * Registers a listener to this event for a specific phase, which will called only if the execution context matches the filter.
+ *
+ * @param phaseIdentifier the identifier of the phase to register the listener in
+ * @param listener the listener to register
+ * @param filter the predicate to test whether the listener to register should be called within a given context
+ * @see #register(Object)
+ * @see #register(Object, Predicate)
+ * @see #register(Comparable, Object)
+ */
+ public void register(@NotNull I phaseIdentifier, @NotNull T listener, @NotNull Predicate filter) {
+ Objects.requireNonNull(phaseIdentifier, "Cannot register a listener for a null phase.");
+ Objects.requireNonNull(listener, "Cannot register a null listener.");
+ Objects.requireNonNull(filter, "Cannot register a listener with a null context filter.");
+
+ this.lock.lock();
+ try {
+ this.getOrCreatePhase(phaseIdentifier, true).addListener(new Listener<>(listener, filter));
+ this.rebuildInvoker(this.listeners.length);
+ } finally {
+ this.lock.unlock();
+ }
+ }
+
+ /**
+ * Creates an invoker for a subset of listeners that matches the given context.
+ *
+ * Each subset will be dynamically updated if new listeners which match the given context are registered to this event.
+ *
+ * @param context the current context
+ * @return an invoker for a subset of listeners
+ */
+ public SubsetInvoker forContext(C context) {
+ var subset = new SubsetInvoker<>(this, context);
+
+ this.lock.lock();
+ try {
+ this.purge();
+ this.subsets.add(new WeakReference<>(subset, this.queue));
+ } finally {
+ this.lock.unlock();
+ }
+
+ return subset;
+ }
+
+ // This may be marked as redundant in some IDEs, but javac is clear: there is an unchecked warning here.
+ // Though, that warning is very weird: the generics of FilteredPhaseData match this event's generics
+ // while the generics in the phase fields also match them.
+ @SuppressWarnings({"unchecked", "RedundantSuppression"})
+ @Override
+ FilteredPhaseData getOrCreatePhase(@NotNull I id, boolean sortIfCreate) {
+ var phase = this.phases.get(id);
+
+ if (phase == null) {
+ phase = new FilteredPhaseData(id, this.type());
+ this.phases.put(id, phase);
+ this.sortedPhases.add(phase);
+
+ if (sortIfCreate) {
+ this.sortPhases();
+ }
+ }
+
+ return (FilteredPhaseData) phase;
+ }
+
+ @Override
+ void rebuildInvoker(int newLength) {
+ super.rebuildInvoker(newLength);
+
+ this.purge();
+
+ for (var subset : this.subsets) {
+ var value = subset.get();
+
+ if (value != null) {
+ value.rebuildInvoker();
+ }
+ }
+ }
+
+ private void purge() {
+ for (var ref = this.queue.poll(); ref != null; ref = this.queue.poll()) {
+ //noinspection SuspiciousMethodCalls
+ this.subsets.remove(ref);
+ }
+ }
+
+ @ApiStatus.Internal
+ final class FilteredPhaseData extends Event.PhaseData {
+ Listener[] listenersData;
+
+ @SuppressWarnings("unchecked")
+ FilteredPhaseData(@NotNull I id, @NotNull Class super T> listenerType) {
+ super(id, listenerType);
+ this.listenersData = new Listener[0];
+ }
+
+ @Override
+ void addListener(@NotNull T listener) {
+ super.addListener(listener);
+ this.addListener(new Listener<>(listener, null));
+ }
+
+ void addListener(@NotNull Listener listener) {
+ int oldLength = this.listenersData.length;
+ this.listenersData = Arrays.copyOf(this.listenersData, oldLength + 1);
+ this.listenersData[oldLength] = listener;
+ }
+ }
+
+ /**
+ * Represents a listener.
+ *
+ * @param listener the listener itself
+ * @param selector the selector of this listener, may be null if this listener is global
+ * @param the type of listener
+ * @param the type of filtering context
+ */
+ @ApiStatus.Internal
+ record Listener(T listener, @Nullable Predicate selector) {
+ /**
+ * {@return {@code true} if this listener should listen to the given context, or {@code false} otherwise}
+ *
+ * @param context the filtering context
+ */
+ boolean shouldListen(C context) {
+ return this.selector == null || this.selector.test(context);
+ }
+ }
+}
diff --git a/libraries/event/src/main/java/dev/yumi/commons/event/InvokableEvent.java b/libraries/event/src/main/java/dev/yumi/commons/event/InvokableEvent.java
new file mode 100644
index 0000000..4503b4a
--- /dev/null
+++ b/libraries/event/src/main/java/dev/yumi/commons/event/InvokableEvent.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 Yumi Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package dev.yumi.commons.event;
+
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Represents an event or event-like object which can be invoked by the same method as their listeners.
+ *
+ * @param the type of the listeners, and the type of the invoker used to execute an event
+ * @author LambdAurora
+ * @version 1.0.0
+ * @since 1.0.0
+ */
+public interface InvokableEvent {
+ /**
+ * {@return the invoker instance used to execute this event}
+ *
+ * The result of this method should not be stored since the invoker may become invalid
+ * at any time. Always call this method when you intend to execute an event.
+ */
+ @Contract(pure = true)
+ @NotNull T invoker();
+}
diff --git a/libraries/event/src/main/java/dev/yumi/commons/event/SubsetInvoker.java b/libraries/event/src/main/java/dev/yumi/commons/event/SubsetInvoker.java
new file mode 100644
index 0000000..93e5a8d
--- /dev/null
+++ b/libraries/event/src/main/java/dev/yumi/commons/event/SubsetInvoker.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2024 Yumi Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package dev.yumi.commons.event;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+
+/**
+ * Represents a subset of listeners of a {@linkplain FilteredEvent filtered event}.
+ *
+ * @param the phase identifier type
+ * @param the type of the listeners, and the type of the invoker used to execute an event
+ * @param the type of the context used to filter out which listeners should be invoked by this subset
+ * @author LambdAurora
+ * @version 1.0.0
+ * @since 1.0.0
+ */
+public final class SubsetInvoker, T, C> implements InvokableEvent {
+ /**
+ * The parent filtered event from which the subset comes from.
+ */
+ private final FilteredEvent parent;
+ /**
+ * The context relevant to this subset of listeners.
+ */
+ private final C context;
+ /**
+ * The invoker to execute the callbacks.
+ */
+ private volatile T invoker;
+
+ public SubsetInvoker(FilteredEvent parent, C context) {
+ this.parent = parent;
+ this.context = context;
+ this.rebuildInvoker();
+ }
+
+ @Override
+ public @NotNull T invoker() {
+ return this.invoker;
+ }
+
+ @SuppressWarnings("unchecked")
+ void rebuildInvoker() {
+ var listeners = new ArrayList();
+
+ for (var entry : this.parent.sortedPhases) {
+ var phase = (FilteredEvent.FilteredPhaseData) entry;
+
+ for (var listener : phase.listenersData) {
+ if (listener.shouldListen(this.context)) {
+ listeners.add(listener.listener());
+ }
+ }
+ }
+
+ this.invoker = this.parent.invokerFactory.apply(
+ listeners.toArray(length -> (T[]) Array.newInstance(this.parent.type(), length))
+ );
+ }
+}
diff --git a/libraries/event/src/main/java/dev/yumi/commons/event/package-info.java b/libraries/event/src/main/java/dev/yumi/commons/event/package-info.java
index 5bcb481..8a849b0 100644
--- a/libraries/event/src/main/java/dev/yumi/commons/event/package-info.java
+++ b/libraries/event/src/main/java/dev/yumi/commons/event/package-info.java
@@ -16,6 +16,7 @@
* and events are created and managed with the help of an {@link dev.yumi.commons.event.EventManager event manager}.
*
* @see dev.yumi.commons.event.Event
+ * @see dev.yumi.commons.event.FilteredEvent
* @see dev.yumi.commons.event.EventManager
*/
diff --git a/libraries/event/src/main/java/module-info.java b/libraries/event/src/main/java/module-info.java
index 5f9ff53..f5f76e5 100644
--- a/libraries/event/src/main/java/module-info.java
+++ b/libraries/event/src/main/java/module-info.java
@@ -24,4 +24,4 @@
exports dev.yumi.commons.event;
exports dev.yumi.commons.event.invoker;
-}
\ No newline at end of file
+}
diff --git a/libraries/event/src/test/java/dev/yumi/commons/event/test/EventCreationTest.java b/libraries/event/src/test/java/dev/yumi/commons/event/test/EventCreationTest.java
index e607bd4..85bf20d 100644
--- a/libraries/event/src/test/java/dev/yumi/commons/event/test/EventCreationTest.java
+++ b/libraries/event/src/test/java/dev/yumi/commons/event/test/EventCreationTest.java
@@ -41,5 +41,10 @@ public void test() {
var filterEvent = events.create(FilterTestCallback.class);
tester.assertCalled(1);
assertSame(filterEvent, lastEvent[0]);
+
+ tester.reset();
+ var phasedEvent = events.createWithPhases(TestCallback.class, "pre", "default", "post");
+ tester.assertCalled(1);
+ assertSame(phasedEvent, lastEvent[0]);
}
}
diff --git a/libraries/event/src/test/java/dev/yumi/commons/event/test/ExecutionTester.java b/libraries/event/src/test/java/dev/yumi/commons/event/test/ExecutionTester.java
index 06279df..5f72988 100644
--- a/libraries/event/src/test/java/dev/yumi/commons/event/test/ExecutionTester.java
+++ b/libraries/event/src/test/java/dev/yumi/commons/event/test/ExecutionTester.java
@@ -9,19 +9,42 @@
package dev.yumi.commons.event.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
public class ExecutionTester {
+ private int callOrder;
+ private boolean strictOrder = true;
private int calls;
- public void reset() {
+ public ExecutionTester useStrictOrder(boolean strictOrder) {
+ this.strictOrder = strictOrder;
+ return this;
+ }
+
+ public ExecutionTester reset() {
+ this.callOrder = 0;
+ this.strictOrder = true;
this.calls = 0;
+
+ return this;
}
public void assertOrder(int order) {
- assertEquals(order, this.calls, "Expected listener n°" + order + " to be called.");
+ if (this.strictOrder) {
+ assertEquals(order, this.callOrder, "Expected listener n°" + order + " to be called.");
+ this.callOrder++;
+ } else {
+ assertTrue(this.callOrder <= order, "Expected any listener before n°" + order + " to be called.");
+ this.callOrder = order;
+ }
+
this.calls++;
}
+ public void skip() {
+ this.callOrder++;
+ }
+
public void assertCalled(int called) {
assertEquals(called, this.calls, "Expected a specific amount of listener calls.");
}
diff --git a/libraries/event/src/test/java/dev/yumi/commons/event/test/FilteredEventTest.java b/libraries/event/src/test/java/dev/yumi/commons/event/test/FilteredEventTest.java
new file mode 100644
index 0000000..7c1fe93
--- /dev/null
+++ b/libraries/event/src/test/java/dev/yumi/commons/event/test/FilteredEventTest.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2024 Yumi Project
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package dev.yumi.commons.event.test;
+
+import dev.yumi.commons.collections.YumiCollections;
+import dev.yumi.commons.event.EventManager;
+import dev.yumi.commons.event.FilteredEvent;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+public class FilteredEventTest {
+ private static final EventManager EVENTS = new EventManager<>("default", Function.identity());
+
+ @Test
+ public void test() {
+ var tester = new ExecutionTester();
+ var event = EVENTS.createFiltered(TestCallback.class, String.class);
+
+ event.register(text -> tester.assertOrder(0));
+ event.register(text -> tester.assertOrder(1));
+ event.register(text -> tester.assertOrder(2));
+ event.register(text -> tester.assertOrder(3), context -> context.equals("test context"));
+
+ // Without filtering.
+ event.invoker().call("3");
+ tester.assertCalled(3);
+
+ tester.reset();
+
+ var filtered = event.forContext("test context");
+ filtered.invoker().call("4");
+ tester.assertCalled(4);
+
+ tester.reset();
+
+ filtered = event.forContext("other context");
+ filtered.invoker().call("3 again");
+ tester.assertCalled(3);
+ }
+
+ @Test
+ public void testPhases() {
+ var tester = new ExecutionTester();
+
+ record Listener(int order, Predicate selector) {
+ void register(String phase, ExecutionTester tester, FilteredEvent event) {
+ if (this.selector == null) {
+ event.register(phase, text -> tester.assertOrder(this.order));
+ } else {
+ event.register(phase, text -> tester.assertOrder(this.order), this.selector);
+ event.register(phase, text -> tester.skip(), this.selector.negate());
+ }
+ }
+ }
+
+ record Entry(String phase, List listeners) {
+ Entry(String phase, Listener listener) {
+ this(phase, List.of(listener));
+ }
+ }
+
+ YumiCollections.forAllPermutations(
+ List.of(
+ new Entry("very_early", new Listener(0, null)),
+ new Entry("early", new Listener(1, null)),
+ new Entry("default", List.of(
+ new Listener(2, context -> context.equals("contextualized")),
+ new Listener(3, null)
+ )),
+ new Entry("late", new Listener(4, context -> context.equals("contextualized"))),
+ new Entry("very_late", List.of(
+ new Listener(5, null),
+ new Listener(6, context -> context.equals("some other context")),
+ new Listener(7, null)
+ ))
+ ),
+ entries -> {
+ var event = EVENTS.createFilteredWithPhases(TestCallback.class, String.class,
+ "very_early", "early", "default", "late", "very_late"
+ );
+
+ tester.reset();
+ for (var entry : entries) {
+ for (var listener : entry.listeners) {
+ listener.register(entry.phase, tester, event);
+ }
+ }
+
+ tester.useStrictOrder(false);
+ event.invoker().call("Hello world!");
+ tester.assertCalled(5);
+
+ tester.reset();
+
+ event.forContext("contextualized").invoker().call("Hello world!");
+ tester.assertCalled(7);
+
+ tester.reset();
+
+ event.forContext("some other context").invoker().call("Hello world!");
+ tester.assertCalled(6);
+ });
+ }
+}