ProjectAres/PGM/src/main/java/tc/oc/pgm/match/MatchEventRegistry.java

175 lines
7.2 KiB
Java

package tc.oc.pgm.match;
import java.lang.reflect.AnnotatedElement;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableSet;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandlerMeta;
import org.bukkit.event.EventRegistry;
import org.bukkit.event.Listener;
import tc.oc.commons.bukkit.event.BukkitEventHandlerScanner;
import tc.oc.commons.bukkit.event.EventHandlerInfo;
import tc.oc.commons.core.exception.ExceptionHandler;
import tc.oc.commons.core.logging.Loggers;
import tc.oc.commons.core.util.CacheUtils;
import tc.oc.commons.core.util.ThrowingConsumer;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.events.MatchEvent;
/**
* Basically re-implements most of Bukkit's event registration system in order to
* implement per-match event filtering.
*
* TODO: This could be less hacky, and integrated with the whole targeted event system.
*/
@Singleton
public class MatchEventRegistry {
// Thrown to silently skip an event
private static class SkipEvent extends RuntimeException {}
// Generated during registration, and used (internally) to start listening
// with an actual match and listener instance
private interface Loader {
void load(Match match, Listener listener);
}
// Used internally to wrap events just before they are passed to a handler method
private interface Wrapper {
Event wrap(Match match, Event unwrapped);
}
private final Logger logger;
private final EventRegistry eventRegistry;
private final ExceptionHandler exceptionHandler;
private final BukkitEventHandlerScanner bukkitScanner;
private final LoadingCache<Class<? extends Listener>, Loader> loaders;
@Inject MatchEventRegistry(Loggers loggers, EventRegistry eventRegistry, ExceptionHandler exceptionHandler, BukkitEventHandlerScanner bukkitScanner, Set<MatchListenerMeta> listeners) {
this.logger = loggers.get(getClass());
this.eventRegistry = eventRegistry;
this.exceptionHandler = exceptionHandler;
this.bukkitScanner = bukkitScanner;
this.loaders = CacheUtils.newCache(this::createLoader);
listeners.forEach(meta -> registerListener(meta.type(), meta.scope()));
}
/**
* Register event handler methods on the given {@link Listener} type.
*
* This method can be called outside of any match, and will detect most static problems
* right away. Known {@link Listener} types should be registered as early as possible,
* in order to detect bugs. However, this is not required. Unregistered classes will be
* registered automatically the first time an instance is passed to {@link #startListening}.
*
* Handler methods registered through this class have the following special behavior:
*
* If the event type extends {@link MatchEvent}, then only event instances belonging to
* this match will be dispatched to the handler. Events from other matches will be
* quietly ignored.
*
* All event handler methods registered with this bus must specify the {@link MatchScope}
* to listen within. This can be done with a {@link ListenerScope} annotation on the
* method itself, or on the method's declaring class, in which case it will apply to
* all handler methods in that class.
*/
public void registerListener(Class<? extends Listener> listenerType) {
registerListener(listenerType, null);
}
public void registerListener(Class<? extends Listener> listenerType, @Nullable MatchScope listenerScope) {
CacheUtils.getUnchecked(loaders, listenerType, () -> createLoader(listenerType, listenerScope));
}
/**
* Start delivering events from the given match to the given listener. The listener's
* class will be registered, if it isn't already.
*/
public void startListening(Match match, Listener listener) {
loaders.getUnchecked(listener.getClass()).load(match, listener);
}
/**
* Stop delivering events to the given listener
*/
public void stopListening(Match match, Listener listener) {
eventRegistry.unregisterListener(listener);
}
private Loader createLoader(Class<? extends Listener> listener) {
return createLoader(listener, null);
}
private Loader createLoader(Class<? extends Listener> listenerType, @Nullable MatchScope scope) {
scope = listenerScope(listenerType, scope);
if(logger.isLoggable(Level.FINE)) {
logger.fine("Registering listener type " + listenerType.getName() + " in scope " + scope);
}
final ImmutableSet.Builder<Loader> builder = ImmutableSet.builder();
for(EventHandlerInfo<? extends Event> handler : bukkitScanner.findEventHandlers(listenerType).values()) {
builder.add(createLoader(handler, listenerScope(handler.method(), scope)));
}
final ImmutableSet<Loader> loaders = builder.build();
return (match, listener) -> loaders.forEach(loader -> loader.load(match, listener));
}
private Loader createLoader(EventHandlerInfo<? extends Event> handler, @Nullable MatchScope matchScopeOrNull) {
final MatchScope matchScope = matchScopeOrNull != null ? matchScopeOrNull : MatchScope.LOADED;
final Class<? extends Event> eventClass;
final Wrapper wrapper;
eventClass = handler.key().event();
if(logger.isLoggable(Level.FINE)) {
logger.fine(" " + handler.method().getName() + "(" + eventClass.getSimpleName() + ") scope=" + matchScope);
}
if(MatchEvent.class.isAssignableFrom(handler.key().event())) {
// Skip MatchEvents if they belong to a different match
wrapper = (match, event) -> {
if(!match.equals(((MatchEvent) event).getMatch())) throw new SkipEvent();
return event;
};
} else {
// Plain old event event, do nothing special
// TODO: should we try to filter these too, if possible?
wrapper = (match, event) -> event;
}
return (match, listener) -> {
final ThrowingConsumer<Event, Throwable> boundMethod = (ThrowingConsumer<Event, Throwable>) handler.bindTo(listener);
Event.register(eventRegistry.bindHandler(
new EventHandlerMeta<>(eventClass, handler.priority(), handler.ignoreCancelled()),
listener,
(l, event) -> {
try {
if(eventClass.isInstance(event) && match.inScope(matchScope)) {
boundMethod.accept(wrapper.wrap(match, event));
}
} catch(SkipEvent ignored) {
} catch(Throwable throwable) {
exceptionHandler.handleException(throwable);
}
}
));
};
}
private static MatchScope listenerScope(AnnotatedElement thing, @Nullable MatchScope def) {
final ListenerScope annotation = thing.getAnnotation(ListenerScope.class);
return annotation == null ? def : annotation.value();
}
}