mirror of
https://github.com/FabricMC/fabric.git
synced 2025-04-08 21:14:41 -04:00
Add event phases (#1669)
* Proof of concept
* Simplify and document
* Allow events to be registered with default phases
* Use modified Kosaraju for the toposort, and add test for cyclic dependency graphs
* Separate phase-related functionality in an EventPhase class
* Revert "Separate phase-related functionality in an EventPhase class"
This reverts commit e433f348f4
.
* Ensure that the phase order is deterministic
* Add pretty graphs
* Add a test, fix a bug, only do one sort for every constraint registration
This commit is contained in:
parent
d2f76b0fdf
commit
5d8be2ee16
9 changed files with 685 additions and 13 deletions
fabric-api-base/src
main/java/net/fabricmc/fabric
api/event
impl/base/event
testmod/java/net/fabricmc/fabric/test/base
|
@ -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 <T> The listener type.
|
||||
* @see EventFactory
|
||||
*/
|
||||
@ApiStatus.NonExtendable // Should only be extended by fabric API.
|
||||
public abstract class Event<T> {
|
||||
/**
|
||||
* The invoker field. This should be updated by the implementation to
|
||||
|
@ -44,9 +49,43 @@ public abstract class Event<T> {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* <p>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.
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,8 @@ package net.fabricmc.fabric.api.event;
|
|||
|
||||
import java.util.function.Function;
|
||||
|
||||
import net.minecraft.util.Identifier;
|
||||
|
||||
import net.fabricmc.fabric.impl.base.event.EventFactoryImpl;
|
||||
|
||||
/**
|
||||
|
@ -93,6 +95,39 @@ public final class EventFactory {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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 <T> The listener type.
|
||||
* @return The Event instance.
|
||||
*/
|
||||
public static <T> Event<T> createWithPhases(Class<? super T> type, Function<T[], T> invokerFactory, Identifier... defaultPhases) {
|
||||
EventFactoryImpl.ensureContainsDefault(defaultPhases);
|
||||
EventFactoryImpl.ensureNoDuplicates(defaultPhases);
|
||||
|
||||
Event<T> 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.
|
||||
|
|
|
@ -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<T> extends Event<T> {
|
||||
static final Logger LOGGER = LogManager.getLogger("fabric-api-base");
|
||||
|
||||
private final Function<T[], T> invokerFactory;
|
||||
private final Lock lock = new ReentrantLock();
|
||||
private final Object lock = new Object();
|
||||
private T[] handlers;
|
||||
/**
|
||||
* Registered event phases.
|
||||
*/
|
||||
private final Map<Identifier, EventPhaseData<T>> phases = new LinkedHashMap<>();
|
||||
/**
|
||||
* Phases sorted in the correct dependency order.
|
||||
*/
|
||||
private final List<EventPhaseData<T>> sortedPhases = new ArrayList<>();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArrayBackedEvent(Class<? super T> type, Function<T[], T> invokerFactory) {
|
||||
|
@ -43,16 +59,72 @@ class ArrayBackedEvent<T> extends Event<T> {
|
|||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
handlers = Arrays.copyOf(handlers, handlers.length + 1);
|
||||
handlers[handlers.length - 1] = listener;
|
||||
update();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
private EventPhaseData<T> getOrCreatePhase(Identifier id, boolean sortIfCreate) {
|
||||
EventPhaseData<T> 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<T> 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.");
|
||||
|
||||
synchronized (lock) {
|
||||
EventPhaseData<T> first = getOrCreatePhase(firstPhase, false);
|
||||
EventPhaseData<T> second = getOrCreatePhase(secondPhase, false);
|
||||
first.subsequentPhases.add(second);
|
||||
second.previousPhases.add(first);
|
||||
PhaseSorting.sortPhases(this.sortedPhases);
|
||||
rebuildInvoker(handlers.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ import java.util.ArrayList;
|
|||
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 final class EventFactoryImpl {
|
|||
return event;
|
||||
}
|
||||
|
||||
public static void ensureContainsDefault(Identifier[] defaultPhases) {
|
||||
for (Identifier id : defaultPhases) {
|
||||
if (id.equals(Event.DEFAULT_PHASE)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("The event phases must contain Event.DEFAULT_PHASE.");
|
||||
}
|
||||
|
||||
public static void ensureNoDuplicates(Identifier[] defaultPhases) {
|
||||
for (int i = 0; i < defaultPhases.length; ++i) {
|
||||
for (int j = i+1; j < defaultPhases.length; ++j) {
|
||||
if (defaultPhases[i].equals(defaultPhases[j])) {
|
||||
throw new IllegalArgumentException("Duplicate event phase: " + defaultPhases[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Code originally by sfPlayer1.
|
||||
// Unfortunately, it's slightly slower than just passing an empty array in the first place.
|
||||
private static <T> T buildEmptyInvoker(Class<T> handlerClass, Function<T[], T> invokerSetup) {
|
||||
|
|
|
@ -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<T> {
|
||||
final Identifier id;
|
||||
T[] listeners;
|
||||
final List<EventPhaseData<T>> subsequentPhases = new ArrayList<>();
|
||||
final List<EventPhaseData<T>> 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;
|
||||
}
|
||||
}
|
|
@ -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 <T> void sortPhases(List<EventPhaseData<T>> sortedPhases) {
|
||||
// FIRST KOSARAJU SCC VISIT
|
||||
List<EventPhaseData<T>> toposort = new ArrayList<>(sortedPhases.size());
|
||||
|
||||
for (EventPhaseData<T> phase : sortedPhases) {
|
||||
forwardVisit(phase, null, toposort);
|
||||
}
|
||||
|
||||
clearStatus(toposort);
|
||||
Collections.reverse(toposort);
|
||||
|
||||
// SECOND KOSARAJU SCC VISIT
|
||||
Map<EventPhaseData<T>, PhaseScc<T>> phaseToScc = new IdentityHashMap<>();
|
||||
|
||||
for (EventPhaseData<T> phase : toposort) {
|
||||
if (phase.visitStatus == 0) {
|
||||
List<EventPhaseData<T>> 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<T> scc = new PhaseScc<>(sccPhases);
|
||||
|
||||
for (EventPhaseData<T> phaseInScc : sccPhases) {
|
||||
phaseToScc.put(phaseInScc, scc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearStatus(toposort);
|
||||
|
||||
// Build SCC graph
|
||||
for (PhaseScc<T> scc : phaseToScc.values()) {
|
||||
for (EventPhaseData<T> phase : scc.phases) {
|
||||
for (EventPhaseData<T> subsequentPhase : phase.subsequentPhases) {
|
||||
PhaseScc<T> 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<PhaseScc<T>> pq = new PriorityQueue<>(Comparator.comparing(scc -> scc.phases.get(0).id));
|
||||
sortedPhases.clear();
|
||||
|
||||
for (PhaseScc<T> 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<T> scc = pq.poll();
|
||||
sortedPhases.addAll(scc.phases);
|
||||
|
||||
for (PhaseScc<T> subsequentScc : scc.subsequentSccs) {
|
||||
subsequentScc.inDegree--;
|
||||
|
||||
if (subsequentScc.inDegree == 0) {
|
||||
pq.add(subsequentScc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> void forwardVisit(EventPhaseData<T> phase, EventPhaseData<T> parent, List<EventPhaseData<T>> toposort) {
|
||||
if (phase.visitStatus == 0) {
|
||||
// Not yet visited.
|
||||
phase.visitStatus = 1;
|
||||
|
||||
for (EventPhaseData<T> 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 <T> void clearStatus(List<EventPhaseData<T>> phases) {
|
||||
for (EventPhaseData<T> phase : phases) {
|
||||
phase.visitStatus = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> void backwardVisit(EventPhaseData<T> phase, List<EventPhaseData<T>> sccPhases) {
|
||||
if (phase.visitStatus == 0) {
|
||||
phase.visitStatus = 1;
|
||||
sccPhases.add(phase);
|
||||
|
||||
for (EventPhaseData<T> data : phase.previousPhases) {
|
||||
backwardVisit(data, sccPhases);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class PhaseScc<T> {
|
||||
final List<EventPhaseData<T>> phases;
|
||||
final List<PhaseScc<T>> subsequentSccs = new ArrayList<>();
|
||||
int inDegree = 0;
|
||||
|
||||
private PhaseScc(List<EventPhaseData<T>> phases) {
|
||||
this.phases = phases;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Test[], Test> INVOKER_FACTORY = listeners -> () -> {
|
||||
for (Test test : listeners) {
|
||||
test.onTest();
|
||||
}
|
||||
};
|
||||
|
||||
private static int currentListener = 0;
|
||||
|
||||
private static Event<Test> createEvent() {
|
||||
return EventFactory.createArrayBacked(Test.class, INVOKER_FACTORY);
|
||||
}
|
||||
|
||||
private static Test ensureOrder(int order) {
|
||||
return () -> {
|
||||
assertEquals(order, currentListener);
|
||||
++currentListener;
|
||||
};
|
||||
}
|
||||
|
||||
private static void testDefaultPhaseOnly() {
|
||||
Event<Test> 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<Test> 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<Test> 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<Test> 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.
|
||||
*
|
||||
* <p>The graph is displayed here as ascii art, and also in the file graph.png.
|
||||
* <pre>
|
||||
* +-------------------+
|
||||
* v |
|
||||
* +---+ +---+ +---+ +---+
|
||||
* | a | --> | z | --> | b | --> | y |
|
||||
* +---+ +---+ +---+ +---+
|
||||
* ^
|
||||
* |
|
||||
* |
|
||||
* +---+ +---+
|
||||
* | d | --> | e |
|
||||
* +---+ +---+
|
||||
* +---+
|
||||
* | f |
|
||||
* +---+
|
||||
* </pre>
|
||||
* 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<Consumer<Event<Test>>> 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<Test> event = createEvent();
|
||||
|
||||
for (Consumer<Event<Test>> 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.
|
||||
* <pre>
|
||||
* e --> a <--> b <-- d <--> c
|
||||
* </pre>
|
||||
*/
|
||||
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<Consumer<Event<Test>>> 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<Test> event = createEvent();
|
||||
|
||||
for (Consumer<Event<Test>> 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 <T> void testAllPermutations(List<T> selected, List<T> toSelect, Consumer<List<T>> action) {
|
||||
if (toSelect.size() == 0) {
|
||||
action.accept(selected);
|
||||
} else {
|
||||
for (int i = 0; i < toSelect.size(); ++i) {
|
||||
selected.add(toSelect.get(i));
|
||||
List<T> 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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -59,5 +59,7 @@ public class FabricApiBaseTestInit implements ModInitializer {
|
|||
return 1;
|
||||
}));
|
||||
});
|
||||
|
||||
EventTests.run();
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
After ![]() (image error) Size: 20 KiB |
Loading…
Add table
Reference in a new issue