package tc.oc.commons.bukkit.event.targeted; import java.util.HashSet; import java.util.Set; import javax.inject.Inject; import javax.inject.Singleton; import com.google.common.cache.LoadingCache; import com.google.common.collect.SetMultimap; import org.bukkit.event.Event; import org.bukkit.event.EventException; import org.bukkit.event.EventExecutor; import org.bukkit.event.EventHandlerMeta; import org.bukkit.event.EventRegistry; import tc.oc.commons.bukkit.event.EventHandlerInfo; import tc.oc.commons.bukkit.event.EventKey; import tc.oc.commons.core.exception.ExceptionHandler; import tc.oc.commons.core.exception.InvalidMemberException; import tc.oc.commons.core.plugin.PluginFacet; import tc.oc.commons.core.util.CacheUtils; import tc.oc.commons.core.util.TypeMap; import tc.oc.minecraft.api.event.Listener; /** * Listens for targeted events and dispatches them to registered {@link TargetedEventHandler}s */ @Singleton public class TargetedEventBusImpl implements PluginFacet, TargetedEventBus { private final ExceptionHandler exceptionHandler; private final EventRegistry eventRegistry; private final Set<EventKey> events = new HashSet<>(); private final TypeMap<Object, TargetedEventRouter<?>> routers; private final LoadingCache<Class<? extends Listener>, SetMultimap<EventKey<? extends Event>, EventHandlerInfo<? extends Event>>> listenerCache; @Inject TargetedEventBusImpl(ExceptionHandler exceptionHandler, EventRegistry eventRegistry, TargetedEventHandlerScanner eventHandlerScanner, TypeMap<Object, TargetedEventRouter<?>> routers) { this.exceptionHandler = exceptionHandler; this.eventRegistry = eventRegistry; this.routers = routers; // Cache of handler methods per listener type this.listenerCache = CacheUtils.newCache(listener -> { final SetMultimap<EventKey<? extends Event>, EventHandlerInfo<? extends Event>> handlers = eventHandlerScanner.findEventHandlers(listener); for(EventHandlerInfo<? extends Event> info : handlers.values()) { if(this.routers.allAssignableFrom(info.event()).isEmpty()) { throw new InvalidMemberException( info.method(), "No router registered for targeted event type " + info.event().getName() ); } } return handlers; }); } @Override public void registerListener(Listener listener) { registerListenerType(listener.getClass()); } @Override public void unregisterListener(Listener listener) { } /** * Generate MethodHandles for all {@link TargetedEventHandler}s in the given listener class, * and ensure we are listening for their event types. If the given class has already been * registered, do nothing. */ private void registerListenerType(Class<? extends Listener> listenerType) { listenerCache.getUnchecked(listenerType).keySet().forEach(this::registerEvent); } /** * Start listening for the given event/priority, if we aren't already. */ private void registerEvent(EventKey<? extends Event> key) { // The first time we see a particular event at a particular priority level, start listening for it if(events.add(key)) { Event.register(eventRegistry.bindHandler(new EventHandlerMeta<>(key.event(), key.priority(), false), this, new Executor(key))); } } private class Executor implements EventExecutor { final EventKey<? extends Event> key; Executor(EventKey<? extends Event> key) { this.key = key; } @Override public void execute(Listener dispatcher, Event event) throws EventException { final Set<Listener> listeners = new HashSet<>(); for(TargetedEventRouter router : routers.allAssignableFrom(event.getClass())) { router.listeners(event).forEach(listener -> listeners.add((Listener) listener)); } for(Listener listener : listeners) { final SetMultimap<EventKey<? extends Event>, EventHandlerInfo<? extends Event>> handlerMap = listenerCache.getIfPresent(listener.getClass()); if(handlerMap == null) { exceptionHandler.handleException(new Exception("Targeted event listener is not registered: " + listener)); continue; } for(EventHandlerInfo<? extends Event> handlerInfo : handlerMap.get(key)) { if(handlerInfo.method().getParameterTypes()[0].isInstance(event) && !(handlerInfo.ignoreCancelled() && event.isCancelled())) { try { handlerInfo.methodHandle().invokeWithArguments(listener, event); } catch(Throwable throwable) { exceptionHandler.handleException(throwable, null, null); } } } } } } }