diff --git a/runelite-client/src/main/java/net/runelite/client/Notifier.java b/runelite-client/src/main/java/net/runelite/client/Notifier.java index b8f0b6f592..20bcf83c97 100644 --- a/runelite-client/src/main/java/net/runelite/client/Notifier.java +++ b/runelite-client/src/main/java/net/runelite/client/Notifier.java @@ -73,8 +73,9 @@ public class Notifier private static final int MINIMUM_FLASH_DURATION_MILLIS = 2000; private static final int MINIMUM_FLASH_DURATION_TICKS = MINIMUM_FLASH_DURATION_MILLIS / Constants.CLIENT_TICK_LENGTH; + private static final String appName = RuneLiteProperties.getTitle(); + private final Client client; - private final String appName; private final RuneLiteConfig runeLiteConfig; private final ClientUI clientUI; private final ScheduledExecutorService executorService; @@ -89,12 +90,10 @@ public class Notifier final ClientUI clientUI, final Client client, final RuneLiteConfig runeliteConfig, - final RuneLiteProperties runeLiteProperties, final ScheduledExecutorService executorService, final ChatMessageManager chatMessageManager) { this.client = client; - this.appName = runeLiteProperties.getTitle(); this.clientUI = clientUI; this.runeLiteConfig = runeliteConfig; this.executorService = executorService; 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..0decdb3c48 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLite.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLite.java @@ -37,6 +37,7 @@ import java.util.Locale; import javax.annotation.Nullable; import javax.inject.Provider; import javax.inject.Singleton; +import javax.swing.SwingUtilities; import joptsimple.ArgumentAcceptingOptionSpec; import joptsimple.OptionParser; import joptsimple.OptionSet; @@ -60,6 +61,8 @@ 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.FatalErrorDialog; +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; @@ -77,6 +80,7 @@ public class RuneLite public static final File RUNELITE_DIR = new File(System.getProperty("user.home"), ".runelite"); public static final File PROFILES_DIR = new File(RUNELITE_DIR, "profiles"); public static final File SCREENSHOT_DIR = new File(RUNELITE_DIR, "screenshots"); + public static final File LOGS_DIR = new File(RUNELITE_DIR, "logs"); @Getter private static Injector injector; @@ -197,40 +201,61 @@ 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) + { + SwingUtilities.invokeLater(() -> + new FatalErrorDialog("Developers should enable assertions; Add `-ea` to your JVM arguments`") + .addBuildingGuide() + .open()); + return; + } } + + 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); + } + catch (Exception e) + { + log.warn("Failure during startup", e); + SwingUtilities.invokeLater(() -> + new FatalErrorDialog("RuneLite has encountered an unexpected error during startup.") + .open()); + } + 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 +269,8 @@ public class RuneLite injector.injectMembers(client); } + SplashScreen.stage(.57, null, "Loading configuration"); + // Load user configuration configManager.load(); @@ -257,6 +284,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 +293,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 +332,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/RuneLiteModule.java b/runelite-client/src/main/java/net/runelite/client/RuneLiteModule.java index 23b80ea009..525a0f23b6 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLiteModule.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLiteModule.java @@ -75,7 +75,6 @@ public class RuneLiteModule extends AbstractModule bind(ItemManager.class); bind(Scheduler.class); bind(PluginManager.class); - bind(RuneLiteProperties.class); bind(SessionManager.class); bind(Callbacks.class).to(Hooks.class); diff --git a/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java b/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java index c58c588436..a1ff15e2eb 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLiteProperties.java @@ -28,12 +28,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.Properties; import javax.annotation.Nullable; -import javax.inject.Inject; -import javax.inject.Singleton; -import lombok.extern.slf4j.Slf4j; -@Singleton -@Slf4j public class RuneLiteProperties { private static final String RUNELITE_TITLE = "runelite.title"; @@ -45,58 +40,60 @@ public class RuneLiteProperties private static final String WIKI_LINK = "runelite.wiki.link"; private static final String PATREON_LINK = "runelite.patreon.link"; private static final String LAUNCHER_VERSION_PROPERTY = "runelite.launcher.version"; + private static final String TROUBLESHOOTING_LINK = "runelite.wiki.troubleshooting.link"; + private static final String BUILDING_LINK = "runelite.wiki.building.link"; + private static final String DNS_CHANGE_LINK = "runelite.dnschange.link"; - private final Properties properties = new Properties(); + private static final Properties properties = new Properties(); - @Inject - public RuneLiteProperties() + static { - try (InputStream in = getClass().getResourceAsStream("runelite.properties")) + try (InputStream in = RuneLiteProperties.class.getResourceAsStream("runelite.properties")) { properties.load(in); } catch (IOException ex) { - log.warn("unable to load propertries", ex); + throw new RuntimeException(ex); } } - public String getTitle() + public static String getTitle() { return properties.getProperty(RUNELITE_TITLE); } - public String getVersion() + public static String getVersion() { return properties.getProperty(RUNELITE_VERSION); } - public String getRunescapeVersion() + public static String getRunescapeVersion() { return properties.getProperty(RUNESCAPE_VERSION); } - public String getDiscordAppId() + public static String getDiscordAppId() { return properties.getProperty(DISCORD_APP_ID); } - public String getDiscordInvite() + public static String getDiscordInvite() { return properties.getProperty(DISCORD_INVITE); } - public String getGithubLink() + public static String getGithubLink() { return properties.getProperty(GITHUB_LINK); } - public String getWikiLink() + public static String getWikiLink() { return properties.getProperty(WIKI_LINK); } - public String getPatreonLink() + public static String getPatreonLink() { return properties.getProperty(PATREON_LINK); } @@ -106,4 +103,19 @@ public class RuneLiteProperties { return System.getProperty(LAUNCHER_VERSION_PROPERTY); } + + public static String getTroubleshootingLink() + { + return properties.getProperty(TROUBLESHOOTING_LINK); + } + + public static String getBuildingLink() + { + return properties.getProperty(BUILDING_LINK); + } + + public static String getDNSChangeLink() + { + return properties.getProperty(DNS_CHANGE_LINK); + } } \ No newline at end of file diff --git a/runelite-client/src/main/java/net/runelite/client/discord/DiscordService.java b/runelite-client/src/main/java/net/runelite/client/discord/DiscordService.java index 091c479ac6..0aaa4735e0 100644 --- a/runelite-client/src/main/java/net/runelite/client/discord/DiscordService.java +++ b/runelite-client/src/main/java/net/runelite/client/discord/DiscordService.java @@ -49,7 +49,6 @@ import net.runelite.discord.DiscordUser; public class DiscordService implements AutoCloseable { private final EventBus eventBus; - private final RuneLiteProperties runeLiteProperties; private final ScheduledExecutorService executorService; private final DiscordRPC discordRPC; @@ -62,12 +61,10 @@ public class DiscordService implements AutoCloseable @Inject private DiscordService( final EventBus eventBus, - final RuneLiteProperties runeLiteProperties, final ScheduledExecutorService executorService) { this.eventBus = eventBus; - this.runeLiteProperties = runeLiteProperties; this.executorService = executorService; DiscordRPC discordRPC = null; @@ -106,7 +103,7 @@ public class DiscordService implements AutoCloseable discordEventHandlers.joinGame = this::joinGame; discordEventHandlers.spectateGame = this::spectateGame; discordEventHandlers.joinRequest = this::joinRequest; - discordRPC.Discord_Initialize(runeLiteProperties.getDiscordAppId(), discordEventHandlers, true, null); + discordRPC.Discord_Initialize(RuneLiteProperties.getDiscordAppId(), discordEventHandlers, true, null); executorService.scheduleAtFixedRate(discordRPC::Discord_RunCallbacks, 0, 2, TimeUnit.SECONDS); } 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/plugins/chatnotifications/ChatNotificationsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatnotifications/ChatNotificationsPlugin.java index 16127de3b6..874a446a34 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/chatnotifications/ChatNotificationsPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/chatnotifications/ChatNotificationsPlugin.java @@ -68,9 +68,6 @@ public class ChatNotificationsPlugin extends Plugin @Inject private Notifier notifier; - @Inject - private RuneLiteProperties runeLiteProperties; - //Custom Highlights private Pattern usernameMatcher = null; private String usernameReplacer = ""; @@ -148,7 +145,7 @@ public class ChatNotificationsPlugin extends Plugin break; case CONSOLE: // Don't notify for notification messages - if (chatMessage.getName().equals(runeLiteProperties.getTitle())) + if (chatMessage.getName().equals(RuneLiteProperties.getTitle())) { return; } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordPlugin.java index 443dd8ac12..fb62c2cc8f 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordPlugin.java @@ -91,9 +91,6 @@ public class DiscordPlugin extends Plugin @Inject private ClientToolbar clientToolbar; - @Inject - private RuneLiteProperties properties; - @Inject private DiscordState discordState; @@ -125,7 +122,7 @@ public class DiscordPlugin extends Plugin .tab(false) .tooltip("Join Discord") .icon(icon) - .onClick(() -> LinkBrowser.browse(properties.getDiscordInvite())) + .onClick(() -> LinkBrowser.browse(RuneLiteProperties.getDiscordInvite())) .build(); clientToolbar.addNavigation(discordButton); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordState.java b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordState.java index 0b57424f85..3361b26c52 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordState.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/discord/DiscordState.java @@ -58,16 +58,14 @@ class DiscordState private final DiscordService discordService; private final DiscordConfig config; private PartyService party; - private final RuneLiteProperties properties; private DiscordPresence lastPresence; @Inject - private DiscordState(final DiscordService discordService, final DiscordConfig config, final PartyService party, final RuneLiteProperties properties) + private DiscordState(final DiscordService discordService, final DiscordConfig config, final PartyService party) { this.discordService = discordService; this.config = config; this.party = party; - this.properties = properties; } /** @@ -173,12 +171,12 @@ class DiscordState } // Replace snapshot with + to make tooltip shorter (so it will span only 1 line) - final String versionShortHand = properties.getVersion().replace("-SNAPSHOT", "+"); + final String versionShortHand = RuneLiteProperties.getVersion().replace("-SNAPSHOT", "+"); final DiscordPresence.DiscordPresenceBuilder presenceBuilder = DiscordPresence.builder() .state(MoreObjects.firstNonNull(state, "")) .details(MoreObjects.firstNonNull(details, "")) - .largeImageText(properties.getTitle() + " v" + versionShortHand) + .largeImageText(RuneLiteProperties.getTitle() + " v" + versionShortHand) .startTimestamp(event.getStart()) .smallImageKey(imageKey) .partyMax(PARTY_MAX) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/info/InfoPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/info/InfoPanel.java index f9dc12f4e7..86a31a983c 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/info/InfoPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/info/InfoPanel.java @@ -79,9 +79,6 @@ public class InfoPanel extends PluginPanel @Nullable private Client client; - @Inject - private RuneLiteProperties runeLiteProperties; - @Inject private EventBus eventBus; @@ -117,7 +114,7 @@ public class InfoPanel extends PluginPanel final Font smallFont = FontManager.getRunescapeSmallFont(); - JLabel version = new JLabel(htmlLabel("RuneLite version: ", runeLiteProperties.getVersion())); + JLabel version = new JLabel(htmlLabel("RuneLite version: ", RuneLiteProperties.getVersion())); version.setFont(smallFont); JLabel revision = new JLabel(); @@ -176,10 +173,10 @@ public class InfoPanel extends PluginPanel } }); - actionsContainer.add(buildLinkPanel(GITHUB_ICON, "Report an issue or", "make a suggestion", runeLiteProperties.getGithubLink())); - actionsContainer.add(buildLinkPanel(DISCORD_ICON, "Talk to us on our", "discord server", runeLiteProperties.getDiscordInvite())); - actionsContainer.add(buildLinkPanel(PATREON_ICON, "Become a patron to", "help support RuneLite", runeLiteProperties.getPatreonLink())); - actionsContainer.add(buildLinkPanel(WIKI_ICON, "Information about", "RuneLite and plugins", runeLiteProperties.getWikiLink())); + actionsContainer.add(buildLinkPanel(GITHUB_ICON, "Report an issue or", "make a suggestion", RuneLiteProperties.getGithubLink())); + actionsContainer.add(buildLinkPanel(DISCORD_ICON, "Talk to us on our", "discord server", RuneLiteProperties.getDiscordInvite())); + actionsContainer.add(buildLinkPanel(PATREON_ICON, "Become a patron to", "help support RuneLite", RuneLiteProperties.getPatreonLink())); + actionsContainer.add(buildLinkPanel(WIKI_ICON, "Information about", "RuneLite and plugins", RuneLiteProperties.getWikiLink())); add(versionPanel, BorderLayout.NORTH); add(actionsContainer, BorderLayout.CENTER); 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..43721b6f06 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; @@ -48,11 +49,14 @@ import java.util.Map; import java.util.function.Supplier; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; +import javax.swing.SwingUtilities; import lombok.extern.slf4j.Slf4j; 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.FatalErrorDialog; +import net.runelite.client.ui.SplashScreen; import net.runelite.http.api.RuneLiteAPI; import okhttp3.Request; import okhttp3.Response; @@ -62,7 +66,7 @@ import org.apache.commons.compress.compressors.CompressorException; public class ClientLoader implements Supplier { private ClientUpdateCheckMode updateCheckMode; - private Applet client = null; + private Object client = null; public ClientLoader(ClientUpdateCheckMode updateCheckMode) { @@ -76,10 +80,15 @@ public class ClientLoader implements Supplier { client = doLoad(); } - return client; + + if (client instanceof Throwable) + { + throw new RuntimeException((Throwable) client); + } + return (Applet) client; } - private Applet doLoad() + private Object doLoad() { if (updateCheckMode == NONE) { @@ -88,6 +97,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 +112,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,9 +175,19 @@ public class ClientLoader implements Supplier if (updateCheckMode == AUTO) { + SplashScreen.stage(.35, null, "Patching"); Map hashes; try (InputStream is = ClientLoader.class.getResourceAsStream("/patch/hashes.json")) { + if (is == null) + { + SwingUtilities.invokeLater(() -> + new FatalErrorDialog("The client-patch is missing from the classpath. If you are building " + + "the client you need to re-run maven") + .addBuildingGuide() + .open()); + throw new NullPointerException(); + } hashes = new Gson().fromJson(new InputStreamReader(is), new TypeToken>() { }.getType()); @@ -197,11 +236,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,21 +272,18 @@ 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 | CompressorException | InvalidHeaderException | CertificateException | VerificationException | SecurityException e) { - if (e instanceof ClassNotFoundException) - { - log.error("Unable to load client - class not found. This means you" - + " are not running RuneLite with Maven as the client patch" - + " is not in your classpath."); - } - log.error("Error loading RS!", e); - return null; + + SwingUtilities.invokeLater(() -> FatalErrorDialog.showNetErrorWindow("loading the client", e)); + return e; } } diff --git a/runelite-client/src/main/java/net/runelite/client/rs/VerificationException.java b/runelite-client/src/main/java/net/runelite/client/rs/VerificationException.java index 040370e8f0..4138a12fd3 100644 --- a/runelite-client/src/main/java/net/runelite/client/rs/VerificationException.java +++ b/runelite-client/src/main/java/net/runelite/client/rs/VerificationException.java @@ -24,7 +24,7 @@ */ package net.runelite.client.rs; -class VerificationException extends Exception +public class VerificationException extends Exception { public VerificationException(String message) { 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..e50d312455 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 @@ -53,7 +53,6 @@ import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JOptionPane; -import static javax.swing.JOptionPane.ERROR_MESSAGE; import static javax.swing.JOptionPane.INFORMATION_MESSAGE; import javax.swing.JPanel; import javax.swing.JRootPane; @@ -111,7 +110,6 @@ public class ClientUI @Getter private TrayIcon trayIcon; - private final RuneLiteProperties properties; private final RuneLiteConfig config; private final KeyManager keyManager; private final MouseManager mouseManager; @@ -138,7 +136,6 @@ public class ClientUI @Inject private ClientUI( - RuneLiteProperties properties, RuneLiteConfig config, KeyManager keyManager, MouseManager mouseManager, @@ -146,7 +143,6 @@ public class ClientUI ConfigManager configManager, Provider clientThreadProvider) { - this.properties = properties; this.config = config; this.keyManager = keyManager; this.mouseManager = mouseManager; @@ -261,7 +257,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 @@ -286,18 +282,17 @@ public class ClientUI return false; } - frame.setTitle(properties.getTitle() + " - " + name); + frame.setTitle(RuneLiteProperties.getTitle() + " - " + name); return true; }); } /** * 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(() -> { @@ -316,7 +311,7 @@ public class ClientUI // Try to enable fullscreen on OSX OSXUtil.tryEnableFullscreen(frame); - frame.setTitle(properties.getTitle()); + frame.setTitle(RuneLiteProperties.getTitle()); frame.setIconImage(ICON); frame.getLayeredPane().setCursor(Cursor.getDefaultCursor()); // Prevent substance from using a resize cursor for pointing frame.setLocationRelativeTo(frame.getOwner()); @@ -453,13 +448,19 @@ public class ClientUI titleToolbar.addComponent(sidebarNavigationButton, sidebarNavigationJButton); toggleSidebar(); + }); + } + public void show() + { + SwingUtilities.invokeLater(() -> + { // Layout frame frame.pack(); frame.revalidateMinimumSize(); // Create tray icon (needs to be created after frame is packed) - trayIcon = SwingUtil.createTrayIcon(ICON, properties.getTitle(), frame); + trayIcon = SwingUtil.createTrayIcon(ICON, RuneLiteProperties.getTitle(), frame); // Move frame around (needs to be done after frame is packed) if (config.rememberScreenBounds()) @@ -516,14 +517,7 @@ public class ClientUI }); // Show out of date dialog if needed - if (client == null) - { - SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(frame, - "Error loading client! Check your logs for more details.", - "Unable to load client", - ERROR_MESSAGE)); - } - else if (!(client instanceof Client)) + if (client != null && !(client instanceof Client)) { SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(frame, "RuneLite has not yet been updated to work with the latest\n" @@ -603,10 +597,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) { @@ -839,12 +833,12 @@ public class ClientUI if (player != null && player.getName() != null) { - frame.setTitle(properties.getTitle() + " - " + player.getName()); + frame.setTitle(RuneLiteProperties.getTitle() + " - " + player.getName()); } } else { - frame.setTitle(properties.getTitle()); + frame.setTitle(RuneLiteProperties.getTitle()); } if (frame.isAlwaysOnTopSupported()) diff --git a/runelite-client/src/main/java/net/runelite/client/ui/FatalErrorDialog.java b/runelite-client/src/main/java/net/runelite/client/ui/FatalErrorDialog.java new file mode 100644 index 0000000000..746546fc10 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/ui/FatalErrorDialog.java @@ -0,0 +1,240 @@ +/* + * 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.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Container; +import java.awt.Desktop; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.net.ConnectException; +import java.net.UnknownHostException; +import java.security.GeneralSecurityException; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.imageio.ImageIO; +import javax.swing.BorderFactory; +import javax.swing.BoxLayout; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextArea; +import javax.swing.UIManager; +import javax.swing.border.EmptyBorder; +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.RuneLite; +import net.runelite.client.RuneLiteProperties; +import net.runelite.client.rs.VerificationException; +import net.runelite.client.util.LinkBrowser; + +@Slf4j +public class FatalErrorDialog extends JDialog +{ + private static final AtomicBoolean alreadyOpen = new AtomicBoolean(false); + + private final JPanel rightColumn = new JPanel(); + private final Font font = new Font(Font.DIALOG, Font.PLAIN, 12); + + public FatalErrorDialog(String message) + { + if (alreadyOpen.getAndSet(true)) + { + throw new IllegalStateException("Fatal error during fatal error: " + message); + } + + try + { + UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); + } + catch (Exception e) + { + } + + UIManager.put("Button.select", ColorScheme.DARKER_GRAY_COLOR); + + try + { + BufferedImage logo = ImageIO.read(SplashScreen.class.getResourceAsStream("runelite_transparent.png")); + setIconImage(logo); + + JLabel runelite = new JLabel(); + runelite.setIcon(new ImageIcon(logo)); + runelite.setAlignmentX(Component.CENTER_ALIGNMENT); + runelite.setBackground(ColorScheme.DARK_GRAY_COLOR); + runelite.setOpaque(true); + rightColumn.add(runelite); + } + catch (IOException e) + { + } + + addWindowListener(new WindowAdapter() + { + @Override + public void windowClosing(WindowEvent e) + { + System.exit(-1); + } + }); + + setTitle("Fatal error starting RuneLite"); + setLayout(new BorderLayout()); + + Container pane = getContentPane(); + pane.setBackground(ColorScheme.DARKER_GRAY_COLOR); + + JPanel leftPane = new JPanel(); + leftPane.setBackground(ColorScheme.DARKER_GRAY_COLOR); + leftPane.setLayout(new BorderLayout()); + + JLabel title = new JLabel("There was a fatal error starting RuneLite"); + title.setForeground(Color.WHITE); + title.setFont(font.deriveFont(16.f)); + title.setBorder(new EmptyBorder(10, 10, 10, 10)); + leftPane.add(title, BorderLayout.NORTH); + + leftPane.setPreferredSize(new Dimension(400, 200)); + JTextArea textArea = new JTextArea(message); + textArea.setFont(font); + textArea.setBackground(ColorScheme.DARKER_GRAY_COLOR); + textArea.setForeground(Color.LIGHT_GRAY); + textArea.setLineWrap(true); + textArea.setWrapStyleWord(true); + textArea.setBorder(new EmptyBorder(10, 10, 10, 10)); + textArea.setEditable(false); + leftPane.add(textArea, BorderLayout.CENTER); + + pane.add(leftPane, BorderLayout.CENTER); + + rightColumn.setLayout(new BoxLayout(rightColumn, BoxLayout.Y_AXIS)); + rightColumn.setBackground(ColorScheme.DARK_GRAY_COLOR); + rightColumn.setMaximumSize(new Dimension(200, Integer.MAX_VALUE)); + + addButton("Open logs folder", () -> + { + try + { + Desktop.getDesktop().open(RuneLite.LOGS_DIR); + } + catch (IOException e) + { + log.warn("Unable to open logs", e); + } + }); + addButton("Get help on Discord", () -> LinkBrowser.browse(RuneLiteProperties.getDiscordInvite())); + addButton("Troubleshooting steps", () -> LinkBrowser.browse(RuneLiteProperties.getTroubleshootingLink())); + + pane.add(rightColumn, BorderLayout.EAST); + } + + public void open() + { + addButton("Exit", () -> System.exit(-1)); + + pack(); + SplashScreen.stop(); + setVisible(true); + } + + public FatalErrorDialog addButton(String message, Runnable action) + { + JButton button = new JButton(message); + button.addActionListener(e -> action.run()); + button.setFont(font); + button.setBackground(ColorScheme.DARK_GRAY_COLOR); + button.setForeground(Color.LIGHT_GRAY); + button.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(1, 0, 0, 0, ColorScheme.DARK_GRAY_COLOR.brighter()), + new EmptyBorder(4, 4, 4, 4) + )); + button.setAlignmentX(Component.CENTER_ALIGNMENT); + button.setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)); + button.setFocusPainted(false); + button.addChangeListener(ev -> + { + if (button.getModel().isPressed()) + { + button.setBackground(ColorScheme.DARKER_GRAY_COLOR); + } + else if (button.getModel().isRollover()) + { + button.setBackground(ColorScheme.DARK_GRAY_HOVER_COLOR); + } + else + { + button.setBackground(ColorScheme.DARK_GRAY_COLOR); + } + }); + + rightColumn.add(button); + rightColumn.revalidate(); + + return this; + } + + public FatalErrorDialog addBuildingGuide() + { + return addButton("Building guide", () -> LinkBrowser.browse(RuneLiteProperties.getBuildingLink())); + } + + public static void showNetErrorWindow(String action, Throwable err) + { + if (err instanceof VerificationException || err instanceof GeneralSecurityException) + { + new FatalErrorDialog("RuneLite was unable to verify the security of its connection to the internet while " + + action + ". You may have a misbehaving antivirus, internet service provider, a proxy, or an incomplete" + + " java installation.") + .open(); + return; + } + + if (err instanceof ConnectException) + { + new FatalErrorDialog("RuneLite is unable to connect to a required server while " + action + ". " + + "Please check your internet connection") + .open(); + return; + } + + if (err instanceof UnknownHostException) + { + new FatalErrorDialog("RuneLite is unable to resolve the address of a required server while " + action + ". " + + "Your DNS resolver may be misconfigured, pointing to an inaccurate resolver, or your internet connection may " + + "be down. ") + .addButton("Change your DNS resolver", () -> LinkBrowser.browse(RuneLiteProperties.getDNSChangeLink())) + .open(); + return; + } + + new FatalErrorDialog("RuneLite encountered a fatal error while " + action + ".").open(); + } +} 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/runelite.properties b/runelite-client/src/main/resources/net/runelite/client/runelite.properties index afeed8517f..c29c1e8e39 100644 --- a/runelite-client/src/main/resources/net/runelite/client/runelite.properties +++ b/runelite-client/src/main/resources/net/runelite/client/runelite.properties @@ -5,4 +5,7 @@ runelite.discord.appid=409416265891971072 runelite.discord.invite=https://discord.gg/R4BQ8tU runelite.github.link=https://github.com/runelite runelite.wiki.link=https://github.com/runelite/runelite/wiki -runelite.patreon.link=https://www.patreon.com/runelite \ No newline at end of file +runelite.patreon.link=https://www.patreon.com/runelite +runelite.wiki.troubleshooting.link=https://github.com/runelite/runelite/wiki/Troubleshooting-problems-with-the-client +runelite.wiki.building.link=https://github.com/runelite/runelite/wiki/Building-with-IntelliJ-IDEA#client-failing-to-start +runelite.dnschange.link=https://1.1.1.1/dns/ 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