diff --git a/fabric-api-base/src/main/java/net/fabricmc/fabric/api/event/Event.java b/fabric-api-base/src/main/java/net/fabricmc/fabric/api/event/Event.java index 8016d5db2e..76a844e0a2 100644 --- a/fabric-api-base/src/main/java/net/fabricmc/fabric/api/event/Event.java +++ b/fabric-api-base/src/main/java/net/fabricmc/fabric/api/event/Event.java @@ -16,12 +16,17 @@ package net.fabricmc.fabric.api.event; +import org.jetbrains.annotations.ApiStatus; + +import net.minecraft.util.Identifier; + /** - * Base class for Event implementations. + * Base class for Fabric's event implementations. * * @param The listener type. * @see EventFactory */ +@ApiStatus.NonExtendable // Should only be extended by fabric API. public abstract class Event { /** * The invoker field. This should be updated by the implementation to @@ -44,9 +49,43 @@ public final T invoker() { } /** - * Register a listener to the event. + * Register a listener to the event, in the default phase. + * Have a look at {@link #addPhaseOrdering} for an explanation of event phases. * * @param listener The desired listener. */ public abstract void register(T listener); + + /** + * The identifier of the default phase. + * Have a look at {@link EventFactory#createWithPhases} for an explanation of event phases. + */ + public static final Identifier DEFAULT_PHASE = new Identifier("fabric", "default"); + + /** + * Register a listener to the event for the specified phase. + * Have a look at {@link EventFactory#createWithPhases} for an explanation of event phases. + * + * @param phase Identifier of the phase this listener should be registered for. It will be created if it didn't exist yet. + * @param listener The desired listener. + */ + public void register(Identifier phase, T listener) { + // This is done to keep compatibility with existing Event subclasses, but they should really not be subclassing Event. + register(listener); + } + + /** + * Request that listeners registered for one phase be executed before listeners registered for another phase. + * Relying on the default phases supplied to {@link EventFactory#createWithPhases} should be preferred over manually + * registering phase ordering dependencies. + * + *

Incompatible ordering constraints such as cycles will lead to inconsistent behavior: + * some constraints will be respected and some will be ignored. If this happens, a warning will be logged. + * + * @param firstPhase The identifier of the phase that should run before the other. It will be created if it didn't exist yet. + * @param secondPhase The identifier of the phase that should run after the other. It will be created if it didn't exist yet. + */ + public void addPhaseOrdering(Identifier firstPhase, Identifier secondPhase) { + // This is not abstract to avoid breaking existing Event subclasses, but they should really not be subclassing Event. + } } diff --git a/fabric-api-base/src/main/java/net/fabricmc/fabric/api/event/EventFactory.java b/fabric-api-base/src/main/java/net/fabricmc/fabric/api/event/EventFactory.java index d54ac3ee0d..98487c9481 100644 --- a/fabric-api-base/src/main/java/net/fabricmc/fabric/api/event/EventFactory.java +++ b/fabric-api-base/src/main/java/net/fabricmc/fabric/api/event/EventFactory.java @@ -18,6 +18,8 @@ import java.util.function.Function; +import net.minecraft.util.Identifier; + import net.fabricmc.fabric.impl.base.event.EventFactoryImpl; /** @@ -93,6 +95,39 @@ public static Event createArrayBacked(Class type, T emptyInvoker, Func }); } + /** + * Create an array-backed event 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 listener class type. + * @param invokerFactory The invoker factory, combining multiple listeners into one instance. + * @param defaultPhases The default phases of this event, in the correct order. Must contain {@link Event#DEFAULT_PHASE}. + * @param The listener type. + * @return The Event instance. + */ + public static Event createWithPhases(Class type, Function invokerFactory, Identifier... defaultPhases) { + EventFactoryImpl.ensureContainsDefault(defaultPhases); + EventFactoryImpl.ensureNoDuplicates(defaultPhases); + + Event event = createArrayBacked(type, invokerFactory); + + for (int i = 1; i < defaultPhases.length; ++i) { + event.addPhaseOrdering(defaultPhases[i-1], defaultPhases[i]); + } + + return event; + } + /** * Get the listener object name. This can be used in debugging/profiling * scenarios. diff --git a/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/ArrayBackedEvent.java b/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/ArrayBackedEvent.java index aa6b1c2fee..96a1b11e73 100644 --- a/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/ArrayBackedEvent.java +++ b/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/ArrayBackedEvent.java @@ -17,18 +17,34 @@ package net.fabricmc.fabric.impl.base.event; import java.lang.reflect.Array; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import java.util.Objects; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import java.util.function.Function; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import net.minecraft.util.Identifier; + import net.fabricmc.fabric.api.event.Event; class ArrayBackedEvent extends Event { + static final Logger LOGGER = LogManager.getLogger("fabric-api-base"); + private final Function invokerFactory; - private final Lock lock = new ReentrantLock(); + private final Object lock = new Object(); private T[] handlers; + /** + * Registered event phases. + */ + private final Map> phases = new LinkedHashMap<>(); + /** + * Phases sorted in the correct dependency order. + */ + private final List> sortedPhases = new ArrayList<>(); @SuppressWarnings("unchecked") ArrayBackedEvent(Class type, Function invokerFactory) { @@ -43,16 +59,72 @@ void update() { @Override public void register(T listener) { + register(DEFAULT_PHASE, listener); + } + + @Override + public void register(Identifier phaseIdentifier, T listener) { + Objects.requireNonNull(phaseIdentifier, "Tried to register a listener for a null phase!"); Objects.requireNonNull(listener, "Tried to register a null listener!"); - lock.lock(); + synchronized (lock) { + getOrCreatePhase(phaseIdentifier, true).addListener(listener); + rebuildInvoker(handlers.length + 1); + } + } + + private EventPhaseData getOrCreatePhase(Identifier id, boolean sortIfCreate) { + EventPhaseData phase = phases.get(id); + + if (phase == null) { + phase = new EventPhaseData<>(id, handlers.getClass().getComponentType()); + phases.put(id, phase); + sortedPhases.add(phase); + + if (sortIfCreate) { + PhaseSorting.sortPhases(sortedPhases); + } + } + + return phase; + } + + private void rebuildInvoker(int newLength) { + // Rebuild handlers. + if (sortedPhases.size() == 1) { + // Special case with a single phase: use the array of the phase directly. + handlers = sortedPhases.get(0).listeners; + } else { + @SuppressWarnings("unchecked") + T[] newHandlers = (T[]) Array.newInstance(handlers.getClass().getComponentType(), newLength); + int newHandlersIndex = 0; + + for (EventPhaseData existingPhase : sortedPhases) { + int length = existingPhase.listeners.length; + System.arraycopy(existingPhase.listeners, 0, newHandlers, newHandlersIndex, length); + newHandlersIndex += length; + } + + handlers = newHandlers; + } + + // Rebuild invoker. + update(); + } + + @Override + public void addPhaseOrdering(Identifier firstPhase, Identifier secondPhase) { + Objects.requireNonNull(firstPhase, "Tried to add an ordering for a null phase."); + Objects.requireNonNull(secondPhase, "Tried to add an ordering for a null phase."); + if (firstPhase.equals(secondPhase)) throw new IllegalArgumentException("Tried to add a phase that depends on itself."); - try { - handlers = Arrays.copyOf(handlers, handlers.length + 1); - handlers[handlers.length - 1] = listener; - update(); - } finally { - lock.unlock(); + synchronized (lock) { + EventPhaseData first = getOrCreatePhase(firstPhase, false); + EventPhaseData second = getOrCreatePhase(secondPhase, false); + first.subsequentPhases.add(second); + second.previousPhases.add(first); + PhaseSorting.sortPhases(this.sortedPhases); + rebuildInvoker(handlers.length); } } } diff --git a/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/EventFactoryImpl.java b/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/EventFactoryImpl.java index ef5ab991f2..dd07f14d04 100644 --- a/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/EventFactoryImpl.java +++ b/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/EventFactoryImpl.java @@ -27,6 +27,8 @@ import java.util.List; import java.util.function.Function; +import net.minecraft.util.Identifier; + import net.fabricmc.fabric.api.event.Event; public final class EventFactoryImpl { @@ -44,6 +46,26 @@ public static Event createArrayBacked(Class type, Function T buildEmptyInvoker(Class handlerClass, Function invokerSetup) { diff --git a/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/EventPhaseData.java b/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/EventPhaseData.java new file mode 100644 index 0000000000..f13744a19d --- /dev/null +++ b/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/EventPhaseData.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.impl.base.event; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import net.minecraft.util.Identifier; + +/** + * Data of an {@link ArrayBackedEvent} phase. + */ +class EventPhaseData { + final Identifier id; + T[] listeners; + final List> subsequentPhases = new ArrayList<>(); + final List> previousPhases = new ArrayList<>(); + int visitStatus = 0; // 0: not visited, 1: visiting, 2: visited + + @SuppressWarnings("unchecked") + EventPhaseData(Identifier id, Class listenerClass) { + this.id = id; + this.listeners = (T[]) Array.newInstance(listenerClass, 0); + } + + void addListener(T listener) { + int oldLength = listeners.length; + listeners = Arrays.copyOf(listeners, oldLength + 1); + listeners[oldLength] = listener; + } +} diff --git a/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/PhaseSorting.java b/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/PhaseSorting.java new file mode 100644 index 0000000000..edeca71e98 --- /dev/null +++ b/fabric-api-base/src/main/java/net/fabricmc/fabric/impl/base/event/PhaseSorting.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.impl.base.event; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.PriorityQueue; + +import com.google.common.annotations.VisibleForTesting; +import org.jetbrains.annotations.ApiStatus; + +/** + * Contains phase-sorting logic for {@link ArrayBackedEvent}. + */ +@ApiStatus.Internal +public class PhaseSorting { + @VisibleForTesting + public static boolean ENABLE_CYCLE_WARNING = true; + + /** + * Deterministically sort a list of phases. + * 1) Compute phase SCCs (i.e. cycles). + * 2) Sort phases by id within SCCs. + * 3) Sort SCCs with respect to each other by respecting constraints, and by id in case of a tie. + */ + static void sortPhases(List> sortedPhases) { + // FIRST KOSARAJU SCC VISIT + List> toposort = new ArrayList<>(sortedPhases.size()); + + for (EventPhaseData phase : sortedPhases) { + forwardVisit(phase, null, toposort); + } + + clearStatus(toposort); + Collections.reverse(toposort); + + // SECOND KOSARAJU SCC VISIT + Map, PhaseScc> phaseToScc = new IdentityHashMap<>(); + + for (EventPhaseData phase : toposort) { + if (phase.visitStatus == 0) { + List> sccPhases = new ArrayList<>(); + // Collect phases in SCC. + backwardVisit(phase, sccPhases); + // Sort phases by id. + sccPhases.sort(Comparator.comparing(p -> p.id)); + // Mark phases as belonging to this SCC. + PhaseScc scc = new PhaseScc<>(sccPhases); + + for (EventPhaseData phaseInScc : sccPhases) { + phaseToScc.put(phaseInScc, scc); + } + } + } + + clearStatus(toposort); + + // Build SCC graph + for (PhaseScc scc : phaseToScc.values()) { + for (EventPhaseData phase : scc.phases) { + for (EventPhaseData subsequentPhase : phase.subsequentPhases) { + PhaseScc subsequentScc = phaseToScc.get(subsequentPhase); + + if (subsequentScc != scc) { + scc.subsequentSccs.add(subsequentScc); + subsequentScc.inDegree++; + } + } + } + } + + // Order SCCs according to priorities. When there is a choice, use the SCC with the lowest id. + // The priority queue contains all SCCs that currently have 0 in-degree. + PriorityQueue> pq = new PriorityQueue<>(Comparator.comparing(scc -> scc.phases.get(0).id)); + sortedPhases.clear(); + + for (PhaseScc scc : phaseToScc.values()) { + if (scc.inDegree == 0) { + pq.add(scc); + // Prevent adding the same SCC multiple times, as phaseToScc may contain the same value multiple times. + scc.inDegree = -1; + } + } + + while (!pq.isEmpty()) { + PhaseScc scc = pq.poll(); + sortedPhases.addAll(scc.phases); + + for (PhaseScc subsequentScc : scc.subsequentSccs) { + subsequentScc.inDegree--; + + if (subsequentScc.inDegree == 0) { + pq.add(subsequentScc); + } + } + } + } + + private static void forwardVisit(EventPhaseData phase, EventPhaseData parent, List> toposort) { + if (phase.visitStatus == 0) { + // Not yet visited. + phase.visitStatus = 1; + + for (EventPhaseData data : phase.subsequentPhases) { + forwardVisit(data, phase, toposort); + } + + toposort.add(phase); + phase.visitStatus = 2; + } else if (phase.visitStatus == 1 && ENABLE_CYCLE_WARNING) { + // Already visiting, so we have found a cycle. + ArrayBackedEvent.LOGGER.warn(String.format( + "Event phase ordering conflict detected.%nEvent phase %s is ordered both before and after event phase %s.", + phase.id, + parent.id + )); + } + } + + private static void clearStatus(List> phases) { + for (EventPhaseData phase : phases) { + phase.visitStatus = 0; + } + } + + private static void backwardVisit(EventPhaseData phase, List> sccPhases) { + if (phase.visitStatus == 0) { + phase.visitStatus = 1; + sccPhases.add(phase); + + for (EventPhaseData data : phase.previousPhases) { + backwardVisit(data, sccPhases); + } + } + } + + private static class PhaseScc { + final List> phases; + final List> subsequentSccs = new ArrayList<>(); + int inDegree = 0; + + private PhaseScc(List> phases) { + this.phases = phases; + } + } +} diff --git a/fabric-api-base/src/testmod/java/net/fabricmc/fabric/test/base/EventTests.java b/fabric-api-base/src/testmod/java/net/fabricmc/fabric/test/base/EventTests.java new file mode 100644 index 0000000000..e3e35c2e1c --- /dev/null +++ b/fabric-api-base/src/testmod/java/net/fabricmc/fabric/test/base/EventTests.java @@ -0,0 +1,291 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.test.base; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; + +import net.minecraft.util.Identifier; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.fabricmc.fabric.impl.base.event.PhaseSorting; + +public class EventTests { + private static final Logger LOGGER = LogManager.getLogger("fabric-api-base"); + + public static void run() { + long time1 = System.currentTimeMillis(); + + testDefaultPhaseOnly(); + testMultipleDefaultPhases(); + testAddedPhases(); + testCycle(); + PhaseSorting.ENABLE_CYCLE_WARNING = false; + testDeterministicOrdering(); + testTwoCycles(); + PhaseSorting.ENABLE_CYCLE_WARNING = true; + + long time2 = System.currentTimeMillis(); + LOGGER.info("Event unit tests succeeded in {} milliseconds.", time2 - time1); + } + + private static final Function INVOKER_FACTORY = listeners -> () -> { + for (Test test : listeners) { + test.onTest(); + } + }; + + private static int currentListener = 0; + + private static Event createEvent() { + return EventFactory.createArrayBacked(Test.class, INVOKER_FACTORY); + } + + private static Test ensureOrder(int order) { + return () -> { + assertEquals(order, currentListener); + ++currentListener; + }; + } + + private static void testDefaultPhaseOnly() { + Event event = createEvent(); + + event.register(ensureOrder(0)); + event.register(Event.DEFAULT_PHASE, ensureOrder(1)); + event.register(ensureOrder(2)); + + event.invoker().onTest(); + assertEquals(3, currentListener); + currentListener = 0; + } + + private static void testMultipleDefaultPhases() { + Identifier first = new Identifier("fabric", "first"); + Identifier second = new Identifier("fabric", "second"); + Event event = EventFactory.createWithPhases(Test.class, INVOKER_FACTORY, first, second, Event.DEFAULT_PHASE); + + event.register(second, ensureOrder(1)); + event.register(ensureOrder(2)); + event.register(first, ensureOrder(0)); + + for (int i = 0; i < 5; ++i) { + event.invoker().onTest(); + assertEquals(3, currentListener); + currentListener = 0; + } + } + + private static void testAddedPhases() { + Event event = createEvent(); + + Identifier veryEarly = new Identifier("fabric", "very_early"); + Identifier early = new Identifier("fabric", "early"); + Identifier late = new Identifier("fabric", "late"); + Identifier veryLate = new Identifier("fabric", "very_late"); + + event.addPhaseOrdering(veryEarly, early); + event.addPhaseOrdering(early, Event.DEFAULT_PHASE); + event.addPhaseOrdering(Event.DEFAULT_PHASE, late); + event.addPhaseOrdering(late, veryLate); + + event.register(ensureOrder(4)); + event.register(ensureOrder(5)); + event.register(veryEarly, ensureOrder(0)); + event.register(early, ensureOrder(2)); + event.register(late, ensureOrder(6)); + event.register(veryLate, ensureOrder(8)); + event.register(veryEarly, ensureOrder(1)); + event.register(veryLate, ensureOrder(9)); + event.register(late, ensureOrder(7)); + event.register(early, ensureOrder(3)); + + for (int i = 0; i < 5; ++i) { + event.invoker().onTest(); + assertEquals(10, currentListener); + currentListener = 0; + } + } + + private static void testCycle() { + Event event = createEvent(); + + Identifier a = new Identifier("fabric", "a"); + Identifier b1 = new Identifier("fabric", "b1"); + Identifier b2 = new Identifier("fabric", "b2"); + Identifier b3 = new Identifier("fabric", "b3"); + Identifier c = Event.DEFAULT_PHASE; + + // A always first and C always last. + event.register(a, ensureOrder(0)); + event.register(c, ensureOrder(4)); + event.register(b1, ensureOrder(1)); + event.register(b1, ensureOrder(2)); + event.register(b1, ensureOrder(3)); + + // A -> B + event.addPhaseOrdering(a, b1); + // B -> C + event.addPhaseOrdering(b3, c); + // loop + event.addPhaseOrdering(b1, b2); + event.addPhaseOrdering(b2, b3); + event.addPhaseOrdering(b3, b1); + + for (int i = 0; i < 5; ++i) { + event.invoker().onTest(); + assertEquals(5, currentListener); + currentListener = 0; + } + } + + /** + * Ensure that phases get sorted deterministically regardless of the order in which constraints are registered. + * + *

The graph is displayed here as ascii art, and also in the file graph.png. + *

+	 *             +-------------------+
+	 *             v                   |
+	 * +---+     +---+     +---+     +---+
+	 * | a | --> | z | --> | b | --> | y |
+	 * +---+     +---+     +---+     +---+
+	 *             ^
+	 *             |
+	 *             |
+	 * +---+     +---+
+	 * | d | --> | e |
+	 * +---+     +---+
+	 * +---+
+	 * | f |
+	 * +---+
+	 * 
+ * Notice the cycle z -> b -> y -> z. The elements of the cycle are ordered [b, y, z], and the cycle itself is ordered with its lowest id "b". + * We get for the final order: [a, d, e, cycle [b, y, z], f]. + */ + private static void testDeterministicOrdering() { + Identifier a = new Identifier("fabric", "a"); + Identifier b = new Identifier("fabric", "b"); + Identifier d = new Identifier("fabric", "d"); + Identifier e = new Identifier("fabric", "e"); + Identifier f = new Identifier("fabric", "f"); + Identifier y = new Identifier("fabric", "y"); + Identifier z = new Identifier("fabric", "z"); + + List>> dependencies = List.of( + ev -> ev.addPhaseOrdering(a, z), + ev -> ev.addPhaseOrdering(d, e), + ev -> ev.addPhaseOrdering(e, z), + ev -> ev.addPhaseOrdering(z, b), + ev -> ev.addPhaseOrdering(b, y), + ev -> ev.addPhaseOrdering(y, z) + ); + + testAllPermutations(new ArrayList<>(), dependencies, selectedDependencies -> { + Event event = createEvent(); + + for (Consumer> dependency : selectedDependencies) { + dependency.accept(event); + } + + event.register(a, ensureOrder(0)); + event.register(d, ensureOrder(1)); + event.register(e, ensureOrder(2)); + event.register(b, ensureOrder(3)); + event.register(y, ensureOrder(4)); + event.register(z, ensureOrder(5)); + event.register(f, ensureOrder(6)); + + event.invoker().onTest(); + assertEquals(7, currentListener); + currentListener = 0; + }); + } + + /** + * Test deterministic phase sorting with two cycles. + *
+	 * e --> a <--> b <-- d <--> c
+	 * 
+ */ + private static void testTwoCycles() { + Identifier a = new Identifier("fabric", "a"); + Identifier b = new Identifier("fabric", "b"); + Identifier c = new Identifier("fabric", "c"); + Identifier d = new Identifier("fabric", "d"); + Identifier e = new Identifier("fabric", "e"); + + List>> dependencies = List.of( + ev -> ev.addPhaseOrdering(e, a), + ev -> ev.addPhaseOrdering(a, b), + ev -> ev.addPhaseOrdering(b, a), + ev -> ev.addPhaseOrdering(d, b), + ev -> ev.addPhaseOrdering(d, c), + ev -> ev.addPhaseOrdering(c, d) + ); + + testAllPermutations(new ArrayList<>(), dependencies, selectedDependencies -> { + Event event = createEvent(); + + for (Consumer> dependency : selectedDependencies) { + dependency.accept(event); + } + + event.register(c, ensureOrder(0)); + event.register(d, ensureOrder(1)); + event.register(e, ensureOrder(2)); + event.register(a, ensureOrder(3)); + event.register(b, ensureOrder(4)); + + event.invoker().onTest(); + assertEquals(5, currentListener); + currentListener = 0; + }); + } + + @SuppressWarnings("SuspiciousListRemoveInLoop") + private static void testAllPermutations(List selected, List toSelect, Consumer> action) { + if (toSelect.size() == 0) { + action.accept(selected); + } else { + for (int i = 0; i < toSelect.size(); ++i) { + selected.add(toSelect.get(i)); + List remaining = new ArrayList<>(toSelect); + remaining.remove(i); + testAllPermutations(selected, remaining, action); + selected.remove(selected.size()-1); + } + } + } + + @FunctionalInterface + interface Test { + void onTest(); + } + + private static void assertEquals(Object expected, Object actual) { + if (!Objects.equals(expected, actual)) { + throw new AssertionError(String.format("assertEquals failed%nexpected: %s%n but was: %s", expected, actual)); + } + } +} diff --git a/fabric-api-base/src/testmod/java/net/fabricmc/fabric/test/base/FabricApiBaseTestInit.java b/fabric-api-base/src/testmod/java/net/fabricmc/fabric/test/base/FabricApiBaseTestInit.java index 7cd17b3256..79c3a5ed69 100644 --- a/fabric-api-base/src/testmod/java/net/fabricmc/fabric/test/base/FabricApiBaseTestInit.java +++ b/fabric-api-base/src/testmod/java/net/fabricmc/fabric/test/base/FabricApiBaseTestInit.java @@ -59,5 +59,7 @@ public void onInitialize() { return 1; })); }); + + EventTests.run(); } } diff --git a/fabric-api-base/src/testmod/java/net/fabricmc/fabric/test/base/graph.png b/fabric-api-base/src/testmod/java/net/fabricmc/fabric/test/base/graph.png new file mode 100644 index 0000000000..905359c290 Binary files /dev/null and b/fabric-api-base/src/testmod/java/net/fabricmc/fabric/test/base/graph.png differ