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(); } }