diff --git a/runelite-client/src/main/java/net/runelite/client/RuneLite.java b/runelite-client/src/main/java/net/runelite/client/RuneLite.java index 91e6a2de93..10fe6cc739 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLite.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLite.java @@ -30,6 +30,8 @@ import com.google.common.annotations.VisibleForTesting; import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.Injector; +import io.reactivex.Completable; +import io.reactivex.schedulers.Schedulers; import java.io.File; import java.lang.management.ManagementFactory; import java.lang.management.RuntimeMXBean; @@ -61,6 +63,7 @@ import net.runelite.client.game.chatbox.ChatboxPanelManager; import net.runelite.client.graphics.ModelOutlineRenderer; import net.runelite.client.menus.MenuManager; import net.runelite.client.plugins.PluginManager; +import net.runelite.client.rs.ClientLoader; import net.runelite.client.rs.ClientUpdateCheckMode; import net.runelite.client.task.Scheduler; import net.runelite.client.ui.ClientUI; @@ -85,8 +88,8 @@ public class RuneLite public static final File PLUGIN_DIR = new File(RUNELITE_DIR, "plugins"); public static final File SCREENSHOT_DIR = new File(RUNELITE_DIR, "screenshots"); public static final File LOGS_DIR = new File(RUNELITE_DIR, "logs"); - public static boolean allowPrivateServer = false; public static final Locale SYSTEM_LOCALE = Locale.getDefault(); + public static boolean allowPrivateServer = false; @Getter private static Injector injector; @@ -198,16 +201,30 @@ public class RuneLite parser.accepts("help", "Show this text").forHelp(); OptionSet options = parser.parse(args); + if (options.has("help")) + { + parser.printHelpOn(System.out); + System.exit(0); + } + + if (options.has("debug")) + { + final Logger logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + logger.setLevel(Level.DEBUG); + } + if (options.has("bootstrap")) { Bootstrapper.main(false); System.exit(0); } + if (options.has("bootstrap-staging")) { Bootstrapper.main(true); System.exit(0); } + if (options.has("proxy")) { String[] proxy = options.valueOf(proxyInfo).split(":"); @@ -238,10 +255,18 @@ public class RuneLite } } - if (options.has("help")) + final ClientLoader clientLoader = new ClientLoader(options.valueOf(updateMode)); + Completable.fromAction(clientLoader::get) + .subscribeOn(Schedulers.single()) + .subscribe(); + + Completable.fromAction(ClassPreloader::preload) + .subscribeOn(Schedulers.computation()) + .subscribe(); + + if (!options.has("no-splash")) { - parser.printHelpOn(System.out); - System.exit(0); + RuneLiteSplashScreen.init(); } final boolean developerMode = options.has("developer-mode"); @@ -252,27 +277,13 @@ public class RuneLite assert assertions = true; if (!assertions) { - java.util.logging.Logger.getAnonymousLogger().warning("Developers should enable assertions; Add `-ea` to your JVM arguments`"); + log.warn("Developers should enable assertions; Add `-ea` to your JVM arguments`"); } } - if (!options.has("no-splash")) - { - RuneLiteSplashScreen.init(); - } - - RuneLiteSplashScreen.stage(0, "Initializing client"); - - PROFILES_DIR.mkdirs(); - - if (options.has("debug")) - { - final Logger logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); - logger.setLevel(Level.DEBUG); - } - Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> { + log.error("Uncaught exception:", throwable); if (throwable instanceof AbstractMethodError) { RuneLiteSplashScreen.setError("Out of date!", "Classes are out of date; Build with Gradle again."); @@ -284,13 +295,16 @@ public class RuneLite RuneLiteSplashScreen.stage(0, "Starting OpenOSRS injector"); + PROFILES_DIR.mkdirs(); + final long start = System.currentTimeMillis(); injector = Guice.createInjector(new RuneLiteModule( - options.valueOf(updateMode), + clientLoader, true)); injector.getInstance(RuneLite.class).start(); + final long end = System.currentTimeMillis(); final RuntimeMXBean rb = ManagementFactory.getRuntimeMXBean(); final long uptime = rb.getUptime(); diff --git a/runelite-client/src/main/java/net/runelite/client/RuneLiteModule.java b/runelite-client/src/main/java/net/runelite/client/RuneLiteModule.java index 2d3b2d95bd..4b6fe543ab 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLiteModule.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLiteModule.java @@ -31,6 +31,7 @@ import java.applet.Applet; import java.io.File; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Supplier; import javax.annotation.Nullable; import javax.inject.Singleton; import net.runelite.api.Client; @@ -46,8 +47,6 @@ import net.runelite.client.eventbus.EventBus; import net.runelite.client.game.ItemManager; import net.runelite.client.menus.MenuManager; import net.runelite.client.plugins.PluginManager; -import net.runelite.client.rs.ClientLoader; -import net.runelite.client.rs.ClientUpdateCheckMode; import net.runelite.client.task.Scheduler; import net.runelite.client.util.DeferredEventBus; import net.runelite.client.util.ExecutorServiceExceptionLogger; @@ -61,19 +60,18 @@ public class RuneLiteModule extends AbstractModule { private static final int MAX_OKHTTP_CACHE_SIZE = 20 * 1024 * 1024; // 20mb - private final ClientUpdateCheckMode updateCheckMode; + private final Supplier clientLoader; private final boolean developerMode; - public RuneLiteModule(final ClientUpdateCheckMode updateCheckMode, final boolean developerMode) + public RuneLiteModule(final Supplier clientLoader, boolean developerMode) { - this.updateCheckMode = updateCheckMode; + this.clientLoader = clientLoader; this.developerMode = developerMode; } @Override protected void configure() { - bindConstant().annotatedWith(Names.named("updateCheckMode")).to(updateCheckMode); bindConstant().annotatedWith(Names.named("developerMode")).to(developerMode); bind(ScheduledExecutorService.class).toInstance(new ExecutorServiceExceptionLogger(Executors.newSingleThreadScheduledExecutor())); bind(OkHttpClient.class).toInstance(RuneLiteAPI.CLIENT.newBuilder() @@ -102,9 +100,9 @@ public class RuneLiteModule extends AbstractModule @Provides @Singleton - Applet provideApplet(ClientLoader clientLoader) + Applet provideApplet() { - return clientLoader.load(); + return clientLoader.get(); } @Provides 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 ce202b1040..9e13a0bebd 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 @@ -52,10 +52,26 @@ public class EventBus implements EventBusInterface Disposable disposable = getSubject(eventClass) .filter(Objects::nonNull) // Filter out null objects, better safe than sorry .cast(eventClass) // Cast it for easier usage - .subscribe(action, error -> - { - log.error("Error in eventbus", error); - }); + .subscribe(action, error -> log.error("Error in eventbus", error)); + + getCompositeDisposable(lifecycle).add(disposable); + subscriptionList.put(lifecycle, eventClass); + } + + @Override + public void subscribe(Class eventClass, @NonNull Object lifecycle, @NonNull Consumer action, int takeUntil) + { + if (subscriptionList.containsKey(lifecycle) && eventClass.equals(subscriptionList.get(lifecycle))) + { + return; + } + + Disposable disposable = getSubject(eventClass) + .filter(Objects::nonNull) // Filter out null objects, better safe than sorry + .cast(eventClass) // Cast it for easier usage + .take(takeUntil) + .doFinally(() -> unregister(lifecycle)) + .subscribe(action, error -> log.error("Error in eventbus", error)); getCompositeDisposable(lifecycle).add(disposable); subscriptionList.put(lifecycle, eventClass); diff --git a/runelite-client/src/main/java/net/runelite/client/rs/ClientConfigLoader.java b/runelite-client/src/main/java/net/runelite/client/rs/ClientConfigLoader.java index 1f96828d6b..56b1079678 100644 --- a/runelite-client/src/main/java/net/runelite/client/rs/ClientConfigLoader.java +++ b/runelite-client/src/main/java/net/runelite/client/rs/ClientConfigLoader.java @@ -25,68 +25,92 @@ */ package net.runelite.client.rs; +import io.reactivex.Single; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import net.runelite.http.api.RuneLiteAPI; +import okhttp3.HttpUrl; import okhttp3.Request; import okhttp3.Response; class ClientConfigLoader { - public ClientConfigLoader() + private ClientConfigLoader() { + throw new RuntimeException(); } private static final String CONFIG_URL = "http://oldschool.runescape.com/jav_config.ws"; + private static final int MAX_ATTEMPTS = 16; - static RSConfig fetch() throws IOException + static Single fetch() { - final Request request = new Request.Builder() - .url(CONFIG_URL) - .build(); - - final RSConfig config = new RSConfig(); - - try (final Response response = RuneLiteAPI.CLIENT.newCall(request).execute()) + return Single.create(obs -> { - if (!response.isSuccessful()) + int attempt = 0; + + HostSupplier supplier = null; + HttpUrl url = HttpUrl.parse(CONFIG_URL); + + final RSConfig config = new RSConfig(); + + while (attempt++ < MAX_ATTEMPTS) { - throw new IOException("Unsuccessful response: " + response.message()); - } + final Request request = new Request.Builder() + .url(url) + .build(); - String str; - final BufferedReader in = new BufferedReader(new InputStreamReader(response.body().byteStream())); - while ((str = in.readLine()) != null) - { - int idx = str.indexOf('='); - - if (idx == -1) + try (final Response response = RuneLiteAPI.CLIENT.newCall(request).execute()) { - continue; - } + if (!response.isSuccessful()) + { + if (supplier == null) + { + supplier = new HostSupplier(); + } - String s = str.substring(0, idx); + url = supplier.get(); + continue; + } - switch (s) - { - case "param": - str = str.substring(idx + 1); - idx = str.indexOf('='); - s = str.substring(0, idx); + String str; + final BufferedReader in = new BufferedReader(new InputStreamReader(response.body().byteStream())); + while ((str = in.readLine()) != null) + { + int idx = str.indexOf('='); - config.getAppletProperties().put(s, str.substring(idx + 1)); - break; - case "msg": - // ignore - break; - default: - config.getClassLoaderProperties().put(s, str.substring(idx + 1)); - break; + if (idx == -1) + { + continue; + } + + String s = str.substring(0, idx); + + switch (s) + { + case "param": + str = str.substring(idx + 1); + idx = str.indexOf('='); + s = str.substring(0, idx); + + config.getAppletProperties().put(s, str.substring(idx + 1)); + break; + case "msg": + // ignore + break; + default: + config.getClassLoaderProperties().put(s, str.substring(idx + 1)); + break; + } + } + + obs.onSuccess(config); + return; } } - } - return config; + obs.onError(new IOException("Too many attempts")); + }); } } diff --git a/runelite-client/src/main/java/net/runelite/client/rs/ClientLoader.java b/runelite-client/src/main/java/net/runelite/client/rs/ClientLoader.java index 86c70afac9..4be5803fd0 100644 --- a/runelite-client/src/main/java/net/runelite/client/rs/ClientLoader.java +++ b/runelite-client/src/main/java/net/runelite/client/rs/ClientLoader.java @@ -32,32 +32,44 @@ import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLClassLoader; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; +import java.util.function.Supplier; import lombok.extern.slf4j.Slf4j; import net.runelite.client.RuneLite; import net.runelite.client.ui.RuneLiteSplashScreen; @Slf4j -@Singleton -public class ClientLoader +public class ClientLoader implements Supplier { - private final ClientUpdateCheckMode updateCheckMode; + private ClientUpdateCheckMode updateCheckMode; + private Object client = null; - @Inject - private ClientLoader( - @Named("updateCheckMode") final ClientUpdateCheckMode updateCheckMode) + public ClientLoader(ClientUpdateCheckMode updateCheckMode) { this.updateCheckMode = updateCheckMode; } - public Applet load() + @Override + public synchronized Applet get() + { + if (client == null) + { + client = doLoad(); + } + + if (client instanceof Throwable) + { + throw new RuntimeException((Throwable) client); + } + return (Applet) client; + } + + private Object doLoad() { try { RuneLiteSplashScreen.stage(.2, "Fetching applet viewer config"); - final RSConfig config = ClientConfigLoader.fetch(); + + final RSConfig config = ClientConfigLoader.fetch().blockingGet(); switch (updateCheckMode) { @@ -92,7 +104,7 @@ public class ClientLoader private static Applet loadRLPlus(final RSConfig config) throws ClassNotFoundException, InstantiationException, IllegalAccessException { - RuneLiteSplashScreen.stage(.465, "Starting Old School RuneScape"); + RuneLiteSplashScreen.stage(.465, "Starting Open Old School RuneScape"); ClassLoader rsClassLoader = new ClassLoader(ClientLoader.class.getClassLoader()) { @@ -126,7 +138,7 @@ public class ClientLoader private static Applet loadVanilla(final RSConfig config) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException { - RuneLiteSplashScreen.stage(.465, "Starting Old School RuneScape"); + RuneLiteSplashScreen.stage(.465, "Starting Vanilla Old School RuneScape"); final String codebase = config.getCodeBase(); final String initialJar = config.getInitialJar(); diff --git a/runelite-client/src/main/java/net/runelite/client/rs/HostSupplier.java b/runelite-client/src/main/java/net/runelite/client/rs/HostSupplier.java new file mode 100644 index 0000000000..502e72c2c9 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/rs/HostSupplier.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2019 Abex + * Copyright (c) 2019, Lucas + * 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.rs; + +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.List; +import java.util.Queue; +import java.util.Random; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import net.runelite.http.api.RuneLiteAPI; +import net.runelite.http.api.worlds.World; +import net.runelite.http.api.worlds.WorldClient; +import net.runelite.http.api.worlds.WorldResult; +import okhttp3.HttpUrl; + +@Slf4j +class HostSupplier implements Supplier +{ + private final Random random = new Random(); + private Queue hosts = new ArrayDeque<>(); + + @Override + public HttpUrl get() + { + if (!hosts.isEmpty()) + { + return hosts.poll(); + } + + List newHosts = new WorldClient(RuneLiteAPI.CLIENT) + .lookupWorlds() + .map(WorldResult::getWorlds) + .blockingSingle() + .stream() + .map(World::getAddress) + .map(HttpUrl::parse) + .collect(Collectors.toList()); + + Collections.shuffle(newHosts, random); + + hosts.addAll(newHosts.subList(0, 16)); + + while (hosts.size() < 2) + { + hosts.add(HttpUrl.parse("oldschool" + (random.nextInt(50) + 1) + ".runescape.COM")); + } + + return hosts.poll(); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/ui/RuneLiteSplashScreen.java b/runelite-client/src/main/java/net/runelite/client/ui/RuneLiteSplashScreen.java index e2571fed01..cd16bc6f2b 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/RuneLiteSplashScreen.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/RuneLiteSplashScreen.java @@ -26,7 +26,6 @@ package net.runelite.client.ui; import java.awt.BorderLayout; import java.awt.Dimension; -import java.lang.reflect.InvocationTargetException; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JProgressBar; @@ -111,29 +110,17 @@ public class RuneLiteSplashScreen extends JFrame public static void init() { - try + SwingUtilities.invokeLater(() -> { - SwingUtilities.invokeAndWait(() -> + try { - if (INSTANCE != null) - { - return; - } - - try - { - INSTANCE = new RuneLiteSplashScreen(); - } - catch (Exception e) - { - log.warn("Unable to start splash screen", e); - } - }); - } - catch (InterruptedException | InvocationTargetException bs) - { - throw new RuntimeException(bs); - } + INSTANCE = new RuneLiteSplashScreen(); + } + catch (Exception e) + { + log.warn("Unable to start splash screen", e); + } + }); } public static void close() 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 505a41ec11..42df2955c0 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 @@ -57,8 +57,7 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import net.runelite.client.rs.ClientUpdateCheckMode; +import org.mockito.runners.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class PluginManagerTest @@ -83,7 +82,7 @@ public class PluginManagerTest public void before() throws IOException { Injector injector = Guice.createInjector(Modules - .override(new RuneLiteModule(ClientUpdateCheckMode.NONE, true)) + .override(new RuneLiteModule(() -> null, true)) .with(BoundFieldModule.of(this))); RuneLite.setInjector(injector); @@ -141,7 +140,7 @@ public class PluginManagerTest { List modules = new ArrayList<>(); modules.add(new GraphvizModule()); - modules.add(new RuneLiteModule(ClientUpdateCheckMode.NONE, true)); + modules.add(new RuneLiteModule(() -> null, true)); PluginManager pluginManager = new PluginManager(true, null, null, null, null, null); pluginManager.loadCorePlugins(); diff --git a/runelite-client/src/test/java/net/runelite/client/rs/ClientConfigLoaderTest.java b/runelite-client/src/test/java/net/runelite/client/rs/ClientConfigLoaderTest.java index beb3400927..3955f4a101 100644 --- a/runelite-client/src/test/java/net/runelite/client/rs/ClientConfigLoaderTest.java +++ b/runelite-client/src/test/java/net/runelite/client/rs/ClientConfigLoaderTest.java @@ -25,7 +25,6 @@ package net.runelite.client.rs; -import java.io.IOException; import org.junit.Test; /** @@ -35,9 +34,9 @@ import org.junit.Test; public class ClientConfigLoaderTest { @Test - public void test() throws IOException + public void test() { - final RSConfig config = ClientConfigLoader.fetch(); + final RSConfig config = ClientConfigLoader.fetch().blockingGet(); for (String key : config.getClassLoaderProperties().keySet()) {