diff --git a/runelite-api/src/main/java/net/runelite/api/hooks/Callbacks.java b/runelite-api/src/main/java/net/runelite/api/hooks/Callbacks.java index ef121b3ebe..275c599788 100644 --- a/runelite-api/src/main/java/net/runelite/api/hooks/Callbacks.java +++ b/runelite-api/src/main/java/net/runelite/api/hooks/Callbacks.java @@ -24,13 +24,13 @@ */ package net.runelite.api.hooks; -import net.runelite.api.MainBufferProvider; -import net.runelite.api.events.Event; -import net.runelite.api.widgets.WidgetItem; import java.awt.Graphics; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; +import net.runelite.api.MainBufferProvider; +import net.runelite.api.events.Event; +import net.runelite.api.widgets.WidgetItem; /** * Interface of callbacks the injected client uses to send events @@ -42,14 +42,14 @@ public interface Callbacks * * @param event the event */ - void post(Class eventClass, Event event); + void post(Class eventClass, E event); /** * Post a deferred event, which gets delayed until the next cycle. * * @param event the event */ - void postDeferred(Class eventClass, Event event); + void postDeferred(Class eventClass, E event); /** * Called each client cycle. diff --git a/runelite-client/src/main/java/net/runelite/client/callback/Hooks.java b/runelite-client/src/main/java/net/runelite/client/callback/Hooks.java index 94648a1f68..ba1a7d9b7d 100644 --- a/runelite-client/src/main/java/net/runelite/client/callback/Hooks.java +++ b/runelite-client/src/main/java/net/runelite/client/callback/Hooks.java @@ -156,13 +156,13 @@ public class Hooks implements Callbacks } @Override - public void post(Class eventClass, Event event) + public void post(Class eventClass, E event) { eventBus.post(eventClass, event); } @Override - public void postDeferred(Class eventClass, Event event) + public void postDeferred(Class eventClass, E event) { deferredEventBus.post(eventClass, event); } diff --git a/runelite-client/src/main/java/net/runelite/client/eventbus/EventBus.java b/runelite-client/src/main/java/net/runelite/client/eventbus/EventBus.java index 458c101158..d273cf5411 100644 --- a/runelite-client/src/main/java/net/runelite/client/eventbus/EventBus.java +++ b/runelite-client/src/main/java/net/runelite/client/eventbus/EventBus.java @@ -29,7 +29,7 @@ public class EventBus implements EventBusInterface private OpenOSRSConfig openOSRSConfig; @NonNull - private Relay getSubject(Class eventClass) + private Relay getSubject(Class eventClass) { return subjectList.computeIfAbsent(eventClass, k -> PublishRelay.create().toSerialized()); } @@ -49,7 +49,7 @@ public class EventBus implements EventBusInterface @Override // Subscribe on lifecycle (for example from plugin startUp -> shutdown) - public void subscribe(Class eventClass, @NonNull Object lifecycle, @NonNull Consumer action) + public void subscribe(Class eventClass, @NonNull Object lifecycle, @NonNull Consumer action) { if (subscriptionList.containsKey(lifecycle) && eventClass.equals(subscriptionList.get(lifecycle))) { @@ -74,7 +74,7 @@ public class EventBus implements EventBusInterface } @Override - public void subscribe(Class eventClass, @NonNull Object lifecycle, @NonNull Consumer action, int takeUntil) + public void subscribe(Class eventClass, @NonNull Object lifecycle, @NonNull Consumer action, int takeUntil) { if (subscriptionList.containsKey(lifecycle) && eventClass.equals(subscriptionList.get(lifecycle))) { @@ -113,7 +113,7 @@ public class EventBus implements EventBusInterface } @Override - public void post(Class eventClass, @NonNull Event event) + public void post(Class eventClass, @NonNull T event) { getSubject(eventClass).accept(event); } diff --git a/runelite-client/src/main/java/net/runelite/client/eventbus/EventBusInterface.java b/runelite-client/src/main/java/net/runelite/client/eventbus/EventBusInterface.java index d18072891f..5d6669c09e 100644 --- a/runelite-client/src/main/java/net/runelite/client/eventbus/EventBusInterface.java +++ b/runelite-client/src/main/java/net/runelite/client/eventbus/EventBusInterface.java @@ -6,11 +6,11 @@ import net.runelite.api.events.Event; public interface EventBusInterface { - void subscribe(Class eventClass, @NonNull Object lifecycle, @NonNull Consumer action); + void subscribe(Class eventClass, @NonNull Object lifecycle, @NonNull Consumer action); - void subscribe(Class eventClass, @NonNull Object lifecycle, @NonNull Consumer action, int takeUntil); + void subscribe(Class eventClass, @NonNull Object lifecycle, @NonNull Consumer action, int takeUntil); void unregister(@NonNull Object lifecycle); - void post(Class eventClass, @NonNull Event event); + void post(Class eventClass, @NonNull T event); } diff --git a/runelite-client/src/main/java/net/runelite/client/eventbus/Subscribe.java b/runelite-client/src/main/java/net/runelite/client/eventbus/Subscribe.java new file mode 100644 index 0000000000..26560ac6bf --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/eventbus/Subscribe.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2018, Abex + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.eventbus; + +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; + +/** + * Marks a method as an event subscriber. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface Subscribe {} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/ExternalPluginLoader.java b/runelite-client/src/main/java/net/runelite/client/plugins/ExternalPluginLoader.java index 81ca7c7a6f..52e6a25391 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/ExternalPluginLoader.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/ExternalPluginLoader.java @@ -61,7 +61,7 @@ public class ExternalPluginLoader BASE.mkdirs(); } - public void scanAndLoad() + void scanAndLoad() { if (!OpenOSRSConfig.enablePlugins()) { @@ -111,7 +111,7 @@ public class ExternalPluginLoader return; } - if (loadedPlugins.size() != 1) + if (loadedPlugins.size() > 1) { close(loader); log.warn("You can not have more than one plugin per jar"); @@ -119,8 +119,6 @@ public class ExternalPluginLoader } Plugin plugin = loadedPlugins.get(0); - plugin.file = pluginFile; - plugin.loader = loader; // Initialize default configuration Injector injector = plugin.getInjector(); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/Plugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/Plugin.java index 5e2b612474..705637fe9f 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/Plugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/Plugin.java @@ -24,17 +24,27 @@ */ package net.runelite.client.plugins; +import com.google.common.collect.ImmutableSet; import com.google.inject.Binder; import com.google.inject.Injector; import com.google.inject.Module; -import java.io.File; +import io.reactivex.functions.Consumer; +import java.lang.reflect.Method; +import java.util.Set; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Value; +import net.runelite.api.events.Event; +import net.runelite.client.eventbus.EventBus; +import net.runelite.client.eventbus.Subscribe; public abstract class Plugin implements Module { - protected Injector injector; + private final Set annotatedSubscriptions = findSubscriptions(); + private final Object annotatedSubsLock = new Object(); - public File file; - public PluginClassLoader loader; + @Getter(AccessLevel.PROTECTED) + protected Injector injector; @Override public void configure(Binder binder) @@ -49,8 +59,47 @@ public abstract class Plugin implements Module { } - public final Injector getInjector() + @SuppressWarnings("unchecked") + final void addAnnotatedSubscriptions(EventBus eventBus) { - return injector; + annotatedSubscriptions.forEach(sub -> eventBus.subscribe(sub.type, annotatedSubsLock, sub.method)); + } + + final void removeAnnotatedSubscriptions(EventBus eventBus) + { + eventBus.unregister(annotatedSubsLock); + } + + private Set findSubscriptions() + { + ImmutableSet.Builder builder = ImmutableSet.builder(); + + for (Method method : this.getClass().getDeclaredMethods()) + { + if (method.getAnnotation(Subscribe.class) == null) + continue; + + assert method.getParameterCount() == 1 : "Methods annotated with @Subscribe should have only one parameter"; + + Class type = method.getParameterTypes()[0]; + + assert Event.class.isAssignableFrom(type) : "Parameters of methods annotated with @Subscribe should implement net.runelite.api.events.Event"; + assert method.getReturnType() == void.class : "Methods annotated with @Subscribe should have a void return type"; + + method.setAccessible(true); + + Subscription sub = new Subscription(type.asSubclass(Event.class), event -> method.invoke(this, event)); + + builder.add(sub); + } + + return builder.build(); + } + + @Value + private static class Subscription + { + private final Class type; + private final Consumer method; } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java index 10ec3ef9ed..181cd1752e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java @@ -376,6 +376,8 @@ public class PluginManager } }); + plugin.addAnnotatedSubscriptions(eventBus); + log.debug("Plugin {} is now running", plugin.getClass().getSimpleName()); if (!isOutdated && sceneTileManager != null) { @@ -386,7 +388,6 @@ public class PluginManager } } - // eventBus.register(plugin); schedule(plugin); eventBus.post(PluginChanged.class, new PluginChanged(plugin, true)); } @@ -424,6 +425,8 @@ public class PluginManager } }); + plugin.removeAnnotatedSubscriptions(eventBus); + log.debug("Plugin {} is now stopped", plugin.getClass().getSimpleName()); eventBus.post(PluginChanged.class, new PluginChanged(plugin, false)); diff --git a/runelite-client/src/main/java/net/runelite/client/util/DeferredEventBus.java b/runelite-client/src/main/java/net/runelite/client/util/DeferredEventBus.java index 94eb2d4693..71827808ce 100644 --- a/runelite-client/src/main/java/net/runelite/client/util/DeferredEventBus.java +++ b/runelite-client/src/main/java/net/runelite/client/util/DeferredEventBus.java @@ -47,7 +47,7 @@ public class DeferredEventBus extends EventBus } @Override - public void post(Class eventClass, @NonNull Event event) + public void post(Class eventClass, @NonNull T event) { pendingEvents.add(new ImmutablePair<>(eventClass, event)); } diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/PluginManagerTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/PluginManagerTest.java index 0069618dd6..776da7e33a 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/PluginManagerTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/PluginManagerTest.java @@ -46,10 +46,13 @@ import java.util.List; import java.util.Objects; import java.util.Set; import net.runelite.api.Client; +import net.runelite.api.events.Event; import net.runelite.client.RuneLite; import net.runelite.client.RuneLiteModule; import net.runelite.client.config.Config; import net.runelite.client.config.ConfigItem; +import net.runelite.client.eventbus.EventBus; +import net.runelite.client.eventbus.Subscribe; import static org.junit.Assert.assertEquals; import org.junit.Before; import org.junit.Rule; @@ -108,7 +111,6 @@ public class PluginManagerTest } } - @SuppressWarnings("unchecked") @Test public void testLoadPlugins() throws Exception { @@ -190,4 +192,33 @@ public class PluginManagerTest } } + @Test + public void testEventbusAnnotations() throws PluginInstantiationException + { + EventBus eventbus = new EventBus(); + PluginManager pluginManager = new PluginManager(true, eventbus, null, null, null, null) + { + @Override + public boolean isPluginEnabled(Plugin plugin) + { + return true; + } + }; + + class TestEvent implements Event {} + class TestPlugin extends Plugin + { + private boolean thisShouldBeTrue = false; + @Subscribe + private void doSomething(TestEvent event) + { + thisShouldBeTrue = true; + } + } + + TestPlugin plugin = new TestPlugin(); + pluginManager.startPlugin(plugin); + eventbus.post(TestEvent.class, new TestEvent()); + assert plugin.thisShouldBeTrue; + } }