Change event behavior for one listener, introduce AutoInvokingEvent, update ArrayBackedEvent implementation ()

* Cleanup Events

* Add the AutoInvokingEvent annotation.

Co-authored-by: Player <sfPlayer1@users.noreply.github.com>
Co-authored-by: Technici4n <13494793+Technici4n@users.noreply.github.com>
This commit is contained in:
Technici4n 2021-05-01 23:06:27 +02:00 committed by Player
parent 1f1ad061d4
commit 4b24d382c1
4 changed files with 85 additions and 37 deletions
fabric-api-base/src/main/java/net/fabricmc/fabric

View file

@ -0,0 +1,55 @@
/*
* 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.api.event;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates that this {@link Event} is auto-invoking:
* it calls the event callback implemented by a context parameter type automatically and without registration.
*
* <p>This means that this event can be listened to in two ways:
* <ul>
* <li>If the consumer is the context parameter and it implements the callback, it will be automatically invoked, don't register manually.
* <li>Otherwise, there is no invocation and the listener needs manual registration as usual.
* </ul>
*
* <p>Do note that there may be more than one context parameter.
*
* <p>A typical use case is feature augmentation, for example to expose raw clicks to slots.
* The event callback has a slot parameter - the context parameter - and the event itself is carrying this annotation.
* All the slot needs to receive slot clicks is to implement {@code SlotClickCallback} on itself.
* It shouldn't do any explicit event registration like {@code SLOT_CLICK_EVENT.register(this::onSlotClick)},
* otherwise it will see extraneous callback invocations.
*
* <p>In general, an auto-invoking event bridges the gap between the flexibility of an event with global reach,
* and the convenience of implementing an interface that gets detected automatically.
*
* <p>This is a documentation-only annotation, the event factory has to implement the functionality explicitly by checking the parameter type and invoking it.
* On top of adding this annotation, the event field or method should document which parameters are context parameters,
* and under which circumstances they are invoked.
*/
// TODO: explore enforcing that auto-invoked listeners don't register themselves.
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.METHOD })
public @interface AutoInvokingEvent {
}

View file

@ -48,6 +48,10 @@ public final class EventFactory {
/**
* Create an "array-backed" Event instance.
*
* <p>If your factory simply delegates to the listeners without adding custom behavior,
* consider using {@linkplain #createArrayBacked(Class, Object, Function) the other overload}
* if performance of this event is critical.
*
* @param type The listener class type.
* @param invokerFactory The invoker factory, combining multiple listeners into one instance.
* @param <T> The listener type.
@ -58,7 +62,14 @@ public final class EventFactory {
}
/**
* Create an "array-backed" Event instance with a custom empty invoker.
* Create an "array-backed" Event instance with a custom empty invoker,
* for an event whose {@code invokerFactory} only delegates to the listeners.
* <ul>
* <li>If there is no listener, the custom empty invoker will be used.</li>
* <li><b>If there is only one listener, that one will be used as the invoker
* and the factory will not be called.</b></li>
* <li>Only when there are at least two listeners will the factory be used.</li>
* </ul>
*
* <p>Having a custom empty invoker (of type (...) -&gt; {}) increases performance
* relative to iterating over an empty array; however, it only really matters
@ -70,9 +81,16 @@ public final class EventFactory {
* @param <T> The listener type.
* @return The Event instance.
*/
// TODO: Deprecate this once we have working codegen
public static <T> Event<T> createArrayBacked(Class<T> type, T emptyInvoker, Function<T[], T> invokerFactory) {
return EventFactoryImpl.createArrayBacked(type, emptyInvoker, invokerFactory);
return createArrayBacked(type, listeners -> {
if (listeners.length == 0) {
return emptyInvoker;
} else if (listeners.length == 1) {
return listeners[0];
} else {
return invokerFactory.apply(listeners);
}
});
}
/**

View file

@ -18,6 +18,7 @@ package net.fabricmc.fabric.impl.base.event;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
@ -25,52 +26,30 @@ import java.util.function.Function;
import net.fabricmc.fabric.api.event.Event;
class ArrayBackedEvent<T> extends Event<T> {
private final Class<? super T> type;
private final Function<T[], T> invokerFactory;
private final T dummyInvoker;
private final Lock lock = new ReentrantLock();
private T[] handlers;
ArrayBackedEvent(Class<? super T> type, T dummyInvoker, Function<T[], T> invokerFactory) {
this.type = type;
this.dummyInvoker = dummyInvoker;
@SuppressWarnings("unchecked")
ArrayBackedEvent(Class<? super T> type, Function<T[], T> invokerFactory) {
this.invokerFactory = invokerFactory;
this.handlers = (T[]) Array.newInstance(type, 0);
update();
}
@SuppressWarnings("unchecked")
void update() {
if (handlers == null) {
if (dummyInvoker != null) {
invoker = dummyInvoker;
} else {
invoker = invokerFactory.apply((T[]) Array.newInstance(type, 0));
}
} else if (handlers.length == 1) {
invoker = handlers[0];
} else {
invoker = invokerFactory.apply(handlers);
}
this.invoker = invokerFactory.apply(handlers);
}
@SuppressWarnings("unchecked")
@Override
public void register(T listener) {
if (listener == null) {
throw new NullPointerException("Tried to register a null listener!");
}
Objects.requireNonNull(listener, "Tried to register a null listener!");
lock.lock();
try {
if (handlers == null) {
handlers = (T[]) Array.newInstance(type, 1);
handlers[0] = listener;
} else {
handlers = Arrays.copyOf(handlers, handlers.length + 1);
handlers[handlers.length - 1] = listener;
}
handlers = Arrays.copyOf(handlers, handlers.length + 1);
handlers[handlers.length - 1] = listener;
update();
} finally {
lock.unlock();

View file

@ -39,11 +39,7 @@ public final class EventFactoryImpl {
}
public static <T> Event<T> createArrayBacked(Class<? super T> type, Function<T[], T> invokerFactory) {
return createArrayBacked(type, null /* buildEmptyInvoker(type, invokerFactory) */, invokerFactory);
}
public static <T> Event<T> createArrayBacked(Class<? super T> type, T emptyInvoker, Function<T[], T> invokerFactory) {
ArrayBackedEvent<T> event = new ArrayBackedEvent<>(type, emptyInvoker, invokerFactory);
ArrayBackedEvent<T> event = new ArrayBackedEvent<>(type, invokerFactory);
ARRAY_BACKED_EVENTS.add(event);
return event;
}