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 e309734264..e8b4d60a43 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLite.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLite.java @@ -60,6 +60,7 @@ import net.runelite.client.rs.ClientLoader; import net.runelite.client.rs.ClientUpdateCheckMode; import net.runelite.client.ui.ClientUI; import net.runelite.client.ui.DrawManager; +import net.runelite.client.ui.SplashScreen; import net.runelite.client.ui.overlay.OverlayManager; import net.runelite.client.ui.overlay.OverlayRenderer; import net.runelite.client.ui.overlay.WidgetOverlay; @@ -197,40 +198,50 @@ public class RuneLite } }); - final ClientLoader clientLoader = new ClientLoader(options.valueOf(updateMode)); + SplashScreen.init(); + SplashScreen.stage(0, "Retrieving client", ""); - new Thread(() -> + try { - clientLoader.get(); - ClassPreloader.preload(); - }, "Preloader").start(); + final ClientLoader clientLoader = new ClientLoader(options.valueOf(updateMode)); - final boolean developerMode = options.has("developer-mode") && RuneLiteProperties.getLauncherVersion() == null; - - if (developerMode) - { - boolean assertions = false; - assert assertions = true; - if (!assertions) + new Thread(() -> { - throw new RuntimeException("Developers should enable assertions; Add `-ea` to your JVM arguments`"); + clientLoader.get(); + ClassPreloader.preload(); + }, "Preloader").start(); + + final boolean developerMode = options.has("developer-mode") && RuneLiteProperties.getLauncherVersion() == null; + + if (developerMode) + { + boolean assertions = false; + assert assertions = true; + if (!assertions) + { + throw new RuntimeException("Developers should enable assertions; Add `-ea` to your JVM arguments`"); + } } + + PROFILES_DIR.mkdirs(); + + final long start = System.currentTimeMillis(); + + injector = Guice.createInjector(new RuneLiteModule( + clientLoader, + developerMode)); + + injector.getInstance(RuneLite.class).start(); + + final long end = System.currentTimeMillis(); + final RuntimeMXBean rb = ManagementFactory.getRuntimeMXBean(); + final long uptime = rb.getUptime(); + log.info("Client initialization took {}ms. Uptime: {}ms", end - start, uptime); + } + finally + { + SplashScreen.stop(); } - - PROFILES_DIR.mkdirs(); - - final long start = System.currentTimeMillis(); - - injector = Guice.createInjector(new RuneLiteModule( - clientLoader, - developerMode)); - - injector.getInstance(RuneLite.class).start(); - - final long end = System.currentTimeMillis(); - final RuntimeMXBean rb = ManagementFactory.getRuntimeMXBean(); - final long uptime = rb.getUptime(); - log.info("Client initialization took {}ms. Uptime: {}ms", end - start, uptime); } public void start() throws Exception @@ -244,6 +255,8 @@ public class RuneLite injector.injectMembers(client); } + SplashScreen.stage(.57, null, "Loading configuration"); + // Load user configuration configManager.load(); @@ -257,6 +270,8 @@ public class RuneLite // This will initialize configuration pluginManager.loadCorePlugins(); + SplashScreen.stage(.70, null, "Finalizing configuration"); + // Plugins have provided their config, so set default config // to main settings pluginManager.loadDefaultPluginConfiguration(); @@ -264,8 +279,10 @@ public class RuneLite // Start client session clientSessionManager.start(); + SplashScreen.stage(.75, null, "Starting core interface"); + // Initialize UI - clientUI.open(this); + clientUI.init(this); // Initialize Discord service discordService.init(); @@ -301,6 +318,10 @@ public class RuneLite // Start plugins pluginManager.startCorePlugins(); + + SplashScreen.stop(); + + clientUI.show(); } public void shutdown() 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 73d6bf32e4..b820f2d6b3 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 @@ -70,6 +70,7 @@ import net.runelite.client.events.PluginChanged; import net.runelite.client.task.Schedule; import net.runelite.client.task.ScheduledMethod; import net.runelite.client.task.Scheduler; +import net.runelite.client.ui.SplashScreen; import net.runelite.client.util.GameEventManager; @Singleton @@ -200,6 +201,7 @@ public class PluginManager public void startCorePlugins() { List scannedPlugins = new ArrayList<>(plugins); + int loaded = 0; for (Plugin plugin : scannedPlugins) { try @@ -211,11 +213,15 @@ public class PluginManager log.warn("Unable to start plugin {}. {}", plugin.getClass().getSimpleName(), ex); plugins.remove(plugin); } + + loaded++; + SplashScreen.stage(.80, 1, null, "Starting plugins", loaded, scannedPlugins.size(), false); } } List scanAndInstantiate(ClassLoader classLoader, String packageName) throws IOException { + SplashScreen.stage(.59, null, "Loading Plugins"); MutableGraph> graph = GraphBuilder .directed() .build(); @@ -280,20 +286,22 @@ public class PluginManager List> sortedPlugins = topologicalSort(graph); sortedPlugins = Lists.reverse(sortedPlugins); + int loaded = 0; for (Class pluginClazz : sortedPlugins) { Plugin plugin; try { plugin = instantiate(scannedPlugins, (Class) pluginClazz); + scannedPlugins.add(plugin); } catch (PluginInstantiationException ex) { log.warn("Error instantiating plugin!", ex); - continue; } - scannedPlugins.add(plugin); + loaded++; + SplashScreen.stage(.60, .70, null, "Loading Plugins", loaded, sortedPlugins.size(), false); } return scannedPlugins; 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 e5adedb2f9..f666703a66 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 @@ -34,6 +34,7 @@ import io.sigpipe.jbsdiff.InvalidHeaderException; import io.sigpipe.jbsdiff.Patch; import java.applet.Applet; import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -53,6 +54,7 @@ import net.runelite.api.Client; import static net.runelite.client.rs.ClientUpdateCheckMode.AUTO; import static net.runelite.client.rs.ClientUpdateCheckMode.NONE; import static net.runelite.client.rs.ClientUpdateCheckMode.VANILLA; +import net.runelite.client.ui.SplashScreen; import net.runelite.http.api.RuneLiteAPI; import okhttp3.Request; import okhttp3.Response; @@ -88,6 +90,7 @@ public class ClientLoader implements Supplier try { + SplashScreen.stage(0, null, "Fetching applet viewer config"); RSConfig config = ClientConfigLoader.fetch(); Map zipFile = new HashMap<>(); @@ -102,7 +105,26 @@ public class ClientLoader implements Supplier try (Response response = RuneLiteAPI.CLIENT.newCall(request).execute()) { - JarInputStream jis = new JarInputStream(response.body().byteStream()); + int length = (int) response.body().contentLength(); + if (length < 0) + { + length = 3 * 1024 * 1024; + } + final int flength = length; + InputStream istream = new FilterInputStream(response.body().byteStream()) + { + private int read = 0; + + @Override + public int read(byte[] b, int off, int len) throws IOException + { + int thisRead = super.read(b, off, len); + this.read += thisRead; + SplashScreen.stage(.05, .35, null, "Downloading Old School RuneScape", this.read, flength, true); + return thisRead; + } + }; + JarInputStream jis = new JarInputStream(istream); byte[] tmp = new byte[4096]; ByteArrayOutputStream buffer = new ByteArrayOutputStream(756 * 1024); @@ -146,6 +168,7 @@ public class ClientLoader implements Supplier if (updateCheckMode == AUTO) { + SplashScreen.stage(.35, null, "Patching"); Map hashes; try (InputStream is = ClientLoader.class.getResourceAsStream("/patch/hashes.json")) { @@ -197,11 +220,14 @@ public class ClientLoader implements Supplier file.setValue(patchOs.toByteArray()); ++patchCount; + SplashScreen.stage(.38, .45, null, "Patching", patchCount, zipFile.size(), false); } log.debug("Patched {} classes", patchCount); } + SplashScreen.stage(.465, "Starting", "Starting Old School RuneScape"); + String initialClass = config.getInitialClass(); ClassLoader rsClassLoader = new ClassLoader(ClientLoader.class.getClassLoader()) @@ -230,6 +256,8 @@ public class ClientLoader implements Supplier log.info("client-patch {}", ((Client) rs).getBuildID()); } + SplashScreen.stage(.5, null, "Starting core classes"); + return rs; } catch (IOException | ClassNotFoundException | InstantiationException | IllegalAccessException diff --git a/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java b/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java index 5a175716d4..a61b1c656f 100644 --- a/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java +++ b/runelite-client/src/main/java/net/runelite/client/ui/ClientUI.java @@ -261,7 +261,7 @@ public class ClientUI return; } - final Client client = (Client)this.client; + final Client client = (Client) this.client; final ClientThread clientThread = clientThreadProvider.get(); // Keep scheduling event until we get our name @@ -293,11 +293,10 @@ public class ClientUI /** * Initialize UI. - * * @param runelite runelite instance that will be shut down on exit * @throws Exception exception that can occur during creation of the UI */ - public void open(final RuneLite runelite) throws Exception + public void init(final RuneLite runelite) throws Exception { SwingUtilities.invokeAndWait(() -> { @@ -453,7 +452,13 @@ public class ClientUI titleToolbar.addComponent(sidebarNavigationButton, sidebarNavigationJButton); toggleSidebar(); + }); + } + public void show() + { + SwingUtilities.invokeLater(() -> + { // Layout frame frame.pack(); frame.revalidateMinimumSize(); @@ -603,10 +608,10 @@ public class ClientUI } /** - * Changes cursor for client window. Requires ${@link ClientUI#open(RuneLite)} to be called first. + * Changes cursor for client window. Requires ${@link ClientUI#init(RuneLite)} to be called first. * FIXME: This is working properly only on Windows, Linux and Mac are displaying cursor incorrectly * @param image cursor image - * @param name cursor name + * @param name cursor name */ public void setCursor(final BufferedImage image, final String name) { diff --git a/runelite-client/src/main/java/net/runelite/client/ui/SplashScreen.java b/runelite-client/src/main/java/net/runelite/client/ui/SplashScreen.java new file mode 100644 index 0000000000..6e945cb2a4 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/ui/SplashScreen.java @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2019 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.ui; + +import java.awt.Color; +import java.awt.Container; +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import javax.annotation.Nullable; +import javax.imageio.ImageIO; +import javax.swing.ImageIcon; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JProgressBar; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import javax.swing.Timer; +import javax.swing.UIManager; +import javax.swing.border.EmptyBorder; +import javax.swing.plaf.basic.BasicProgressBarUI; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SplashScreen extends JFrame implements ActionListener +{ + private static final int WIDTH = 200; + private static final int PAD = 10; + + private static SplashScreen INSTANCE; + + private final JLabel action = new JLabel("Loading"); + private final JProgressBar progress = new JProgressBar(); + private final JLabel subAction = new JLabel(); + private final Timer timer; + + private volatile double overallProgress = 0; + private volatile String actionText = "Loading"; + private volatile String subActionText = ""; + private volatile String progressText = null; + + private SplashScreen() throws IOException + { + BufferedImage logo = ImageIO.read(SplashScreen.class.getResourceAsStream("runelite_transparent.png")); + + setTitle("RuneLite Launcher"); + + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setUndecorated(true); + setIconImage(logo); + setLayout(null); + Container pane = getContentPane(); + pane.setBackground(ColorScheme.DARKER_GRAY_COLOR); + + Font font = new Font(Font.DIALOG, Font.PLAIN, 12); + + JLabel logoLabel = new JLabel(new ImageIcon(logo)); + pane.add(logoLabel); + logoLabel.setBounds(0, 0, WIDTH, WIDTH); + + int y = WIDTH; + + pane.add(action); + action.setForeground(Color.WHITE); + action.setBounds(0, y, WIDTH, 16); + action.setHorizontalAlignment(SwingConstants.CENTER); + action.setFont(font); + y += action.getHeight() + PAD; + + pane.add(progress); + progress.setForeground(ColorScheme.BRAND_ORANGE); + progress.setBackground(ColorScheme.BRAND_ORANGE.darker().darker()); + progress.setBorder(new EmptyBorder(0, 0, 0, 0)); + progress.setBounds(0, y, WIDTH, 14); + progress.setFont(font); + progress.setUI(new BasicProgressBarUI() + { + @Override + protected Color getSelectionBackground() + { + return Color.BLACK; + } + + @Override + protected Color getSelectionForeground() + { + return Color.BLACK; + } + }); + y += 12 + PAD; + + pane.add(subAction); + subAction.setForeground(Color.LIGHT_GRAY); + subAction.setBounds(0, y, WIDTH, 16); + subAction.setHorizontalAlignment(SwingConstants.CENTER); + subAction.setFont(font); + y += subAction.getHeight() + PAD; + + setSize(WIDTH, y); + setLocationRelativeTo(null); + + timer = new Timer(100, this); + timer.setRepeats(true); + timer.start(); + + setVisible(true); + } + + @Override + public void actionPerformed(ActionEvent e) + { + action.setText(actionText); + subAction.setText(subActionText); + progress.setMaximum(1000); + progress.setValue((int) (overallProgress * 1000)); + + String progressText = this.progressText; + if (progressText == null) + { + progress.setStringPainted(false); + } + else + { + progress.setStringPainted(true); + progress.setString(progressText); + } + } + + public static void init() + { + try + { + SwingUtilities.invokeAndWait(() -> + { + if (INSTANCE != null) + { + return; + } + + try + { + UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); + INSTANCE = new SplashScreen(); + } + catch (Exception e) + { + log.warn("Unable to start splash screen", e); + } + }); + } + catch (InterruptedException | InvocationTargetException bs) + { + throw new RuntimeException(bs); + } + } + + public static void stop() + { + SwingUtilities.invokeLater(() -> + { + if (INSTANCE == null) + { + return; + } + + INSTANCE.timer.stop(); + INSTANCE.dispose(); + INSTANCE = null; + }); + } + + public static void stage(double overallProgress, @Nullable String actionText, String subActionText) + { + stage(overallProgress, actionText, subActionText, null); + } + + public static void stage(double startProgress, double endProgress, + @Nullable String actionText, String subActionText, + int done, int total, boolean mib) + { + String progress; + if (mib) + { + final double MiB = 1024 * 1042; + progress = String.format("%.1f / %.1f MiB", done / MiB, total / MiB); + } + else + { + progress = done + " / " + total; + } + stage(startProgress + ((endProgress - startProgress) * done / total), actionText, subActionText, progress); + } + + public static void stage(double overallProgress, @Nullable String actionText, String subActionText, @Nullable String progressText) + { + if (INSTANCE != null) + { + INSTANCE.overallProgress = overallProgress; + if (actionText != null) + { + INSTANCE.actionText = actionText; + } + INSTANCE.subActionText = subActionText; + INSTANCE.progressText = progressText; + } + } +} diff --git a/runelite-client/src/main/resources/net/runelite/client/ui/runelite_transparent.png b/runelite-client/src/main/resources/net/runelite/client/ui/runelite_transparent.png new file mode 100644 index 0000000000..1d96a54c6d Binary files /dev/null and b/runelite-client/src/main/resources/net/runelite/client/ui/runelite_transparent.png differ