diff --git a/injector/src/main/java/com/openosrs/injector/Injector.java b/injector/src/main/java/com/openosrs/injector/Injector.java index b6a5262d86..1a99787dde 100644 --- a/injector/src/main/java/com/openosrs/injector/Injector.java +++ b/injector/src/main/java/com/openosrs/injector/Injector.java @@ -7,6 +7,7 @@ */ package com.openosrs.injector; +import com.google.common.hash.Hashing; import com.openosrs.injector.injection.InjectData; import com.openosrs.injector.injection.InjectTaskHandler; import com.openosrs.injector.injectors.CreateAnnotations; @@ -27,6 +28,7 @@ import com.openosrs.injector.transformers.Java8Ifier; import com.openosrs.injector.transformers.SourceChanger; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Comparator; @@ -78,7 +80,8 @@ public class Injector extends InjectData implements InjectTaskHandler OptionSet options = parser.parse(args); String oprsVer = options.valueOf(oprsVerOption); - injector.vanilla = load(options.valueOf(vanillaFileOption)); + File vanillaFile = options.valueOf(vanillaFileOption); + injector.vanilla = load(vanillaFile); injector.deobfuscated = load( new File("../runescape-client/build/libs/runescape-client-" + oprsVer + ".jar")); injector.rsApi = new RSApi(Objects.requireNonNull( @@ -95,7 +98,7 @@ public class Injector extends InjectData implements InjectTaskHandler injector.initToVanilla(); injector.injectVanilla(); - save(injector.getVanilla(), options.valueOf(outFileOption), options.valueOf(outModeOption)); + save(injector.getVanilla(), options.valueOf(outFileOption), options.valueOf(outModeOption), vanillaFile); } public void injectVanilla() @@ -179,7 +182,7 @@ public class Injector extends InjectData implements InjectTaskHandler log.lifecycle("{} {}", name, transformer.getCompletionMsg()); } - private static void save(ClassGroup group, File output, OutputMode mode) + private static void save(ClassGroup group, File output, OutputMode mode, File vanillaFile) { if (output.exists()) { @@ -189,7 +192,7 @@ public class Injector extends InjectData implements InjectTaskHandler } catch (IOException e) { - log.info("Failed to delete output directory contents."); + log.lifecycle("Failed to delete output directory contents."); throw new RuntimeException(e); } } @@ -204,6 +207,18 @@ public class Injector extends InjectData implements InjectTaskHandler JarUtil.save(group, output); break; } + + try + { + String hash = com.google.common.io.Files.asByteSource(vanillaFile).hash(Hashing.sha256()).toString(); + log.lifecycle("Writing vanilla hash: {}", hash); + Files.write(output.getParentFile().toPath().resolve("client.hash"), hash.getBytes(StandardCharsets.UTF_8)); + } + catch (IOException ex) + { + log.lifecycle("Failed to write vanilla hash file"); + throw new RuntimeException(ex); + } } private static void saveFiles(ClassGroup group, File outDir) 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 f7b2bc9fec..710598356f 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLite.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLite.java @@ -351,6 +351,7 @@ public class RuneLite oprsExternalPluginManager.setupInstance(); oprsExternalPluginManager.startExternalUpdateManager(); oprsExternalPluginManager.startExternalPluginManager(); + oprsExternalPluginManager.setOutdated(isOutdated); // Update external plugins oprsExternalPluginManager.update(); //TODO: Re-enable after fixing actions for new repo @@ -400,6 +401,10 @@ public class RuneLite // legacy method, i cant figure out how to make it work without garbage eventBus.register(xpDropManager.get()); + + //Set the world if specified via CLI args - will not work until clientUI.init is called + Optional worldArg = Optional.ofNullable(System.getProperty("cli.world")).map(Integer::parseInt); + worldArg.ifPresent(this::setWorld); } // Start plugins @@ -408,10 +413,6 @@ public class RuneLite SplashScreen.stop(); clientUI.show(); - - //Set the world if specified via CLI args - will not work until clientUI.init is called - Optional worldArg = Optional.ofNullable(System.getProperty("cli.world")).map(Integer::parseInt); - worldArg.ifPresent(this::setWorld); } @VisibleForTesting diff --git a/runelite-client/src/main/java/net/runelite/client/game/WorldService.java b/runelite-client/src/main/java/net/runelite/client/game/WorldService.java index 4cc102016d..8f2a6a691b 100644 --- a/runelite-client/src/main/java/net/runelite/client/game/WorldService.java +++ b/runelite-client/src/main/java/net/runelite/client/game/WorldService.java @@ -60,7 +60,7 @@ public class WorldService private WorldResult worlds; @Inject - private WorldService(Client client, ScheduledExecutorService scheduledExecutorService, OkHttpClient okHttpClient, + private WorldService(@Nullable Client client, ScheduledExecutorService scheduledExecutorService, OkHttpClient okHttpClient, EventBus eventBus) { this.client = client; diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/OPRSExternalPluginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/OPRSExternalPluginManager.java index 1b2532021b..e8cbf2d979 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/OPRSExternalPluginManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/OPRSExternalPluginManager.java @@ -71,6 +71,7 @@ import javax.inject.Singleton; import javax.swing.JOptionPane; import lombok.AccessLevel; import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import net.runelite.client.RuneLite; import net.runelite.client.config.Config; @@ -127,6 +128,8 @@ public class OPRSExternalPluginManager @Inject @Named("safeMode") private boolean safeMode; + @Setter + boolean isOutdated; public void setupInstance() { @@ -456,6 +459,11 @@ public class OPRSExternalPluginManager continue; } + if (!pluginDescriptor.loadWhenOutdated() && isOutdated) + { + continue; + } + if (safeMode && !pluginDescriptor.loadInSafeMode()) { log.debug("Disabling {} due to safe mode", clazz); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/openosrs/OpenOSRSPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/openosrs/OpenOSRSPlugin.java index fcca577c35..a6f53666cb 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/openosrs/OpenOSRSPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/openosrs/OpenOSRSPlugin.java @@ -28,8 +28,8 @@ package net.runelite.client.plugins.openosrs; import ch.qos.logback.classic.Logger; import com.openosrs.client.config.OpenOSRSConfig; -import net.runelite.client.plugins.openosrs.externals.ExternalPluginManagerPanel; import java.awt.image.BufferedImage; +import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Singleton; import lombok.extern.slf4j.Slf4j; @@ -40,6 +40,7 @@ import net.runelite.client.events.ConfigChanged; import net.runelite.client.input.KeyManager; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.plugins.openosrs.externals.ExternalPluginManagerPanel; import net.runelite.client.ui.ClientToolbar; import net.runelite.client.ui.NavigationButton; import net.runelite.client.util.HotkeyListener; @@ -61,6 +62,7 @@ public class OpenOSRSPlugin extends Plugin @Inject private KeyManager keyManager; + @Nullable @Inject private Client client; @@ -89,6 +91,11 @@ public class OpenOSRSPlugin extends Plugin @Override protected void startUp() { + if (client == null) + { + return; + } + ExternalPluginManagerPanel panel = injector.getInstance(ExternalPluginManagerPanel.class); final BufferedImage icon = ImageUtil.loadImageResource(getClass(), "externalmanager_icon.png"); @@ -108,7 +115,10 @@ public class OpenOSRSPlugin extends Plugin @Override protected void shutDown() { - clientToolbar.removeNavigation(navButton); + if (navButton != null) + { + clientToolbar.removeNavigation(navButton); + } } @Subscribe 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 27324dcbe2..c263d51aa7 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 @@ -27,7 +27,9 @@ package net.runelite.client.rs; import com.google.common.base.Strings; +import com.google.common.hash.Hashing; import com.google.common.io.ByteStreams; +import com.openosrs.client.OpenOSRS; import java.applet.Applet; import java.io.ByteArrayOutputStream; import java.io.File; @@ -37,6 +39,8 @@ import java.io.OutputStream; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.StandardOpenOption; import java.security.cert.Certificate; import java.security.cert.CertificateException; @@ -57,6 +61,7 @@ import net.runelite.client.RuneLite; import net.runelite.client.RuneLiteProperties; 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.client.util.CountingInputStream; @@ -66,7 +71,6 @@ import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; -import org.apache.commons.io.FileUtils; @Slf4j @SuppressWarnings("deprecation") @@ -130,18 +134,44 @@ public class ClientLoader implements Supplier StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE); FileLock flock = lockfile.lock()) { - SplashScreen.stage(.40, null, "Loading client"); - File jarFile = updateCheckMode == AUTO ? PATCHED_CACHE : VANILLA_CACHE; - // create the classloader for the jar while we hold the lock, and eagerly load and link all classes - // in the jar. Otherwise the jar can change on disk and can break future classloads. - File oprsInjected = new File(System.getProperty("user.home") + "/.openosrs/cache/injected-client.jar"); - InputStream initialStream = RuneLite.class.getResourceAsStream(INJECTED_CLIENT_NAME); - if (!oprsInjected.exists() || oprsInjected.length() != RuneLite.class.getResource(INJECTED_CLIENT_NAME).getFile().length()) + SplashScreen.stage(.15, null, "Downloading Old School RuneScape"); + try { - FileUtils.copyInputStreamToFile(initialStream, oprsInjected); + updateVanilla(config); + } + catch (IOException ex) + { + // try again with the fallback config and gamepack + if (javConfigUrl.equals(RuneLiteProperties.getJavConfig()) && !config.isFallback()) + { + log.warn("Unable to download game client, attempting to use fallback config", ex); + config = downloadFallbackConfig(); + updateVanilla(config); + } + else + { + throw ex; + } } - classLoader = createJarClassLoader(oprsInjected); + if (!checkVanillaHash()) + { + log.error("Injected client vanilla hash doesn't match, loading vanilla client."); + updateCheckMode = VANILLA; + } + + SplashScreen.stage(.40, null, "Loading client"); + + File oprsInjected = new File(System.getProperty("user.home") + "/.openosrs/cache/injected-client.jar"); + if (updateCheckMode == AUTO) + { + writeInjectedClient(oprsInjected); + } + + File jarFile = updateCheckMode == AUTO ? oprsInjected : VANILLA_CACHE; + // create the classloader for the jar while we hold the lock, and eagerly load and link all classes + // in the jar. Otherwise the jar can change on disk and can break future classloads. + classLoader = createJarClassLoader(jarFile); } SplashScreen.stage(.465, "Starting", "Starting Old School RuneScape"); @@ -153,7 +183,7 @@ public class ClientLoader implements Supplier return rs; } catch (IOException | ClassNotFoundException | InstantiationException | IllegalAccessException - | SecurityException e) + | VerificationException | SecurityException e) { log.error("Error loading RS!", e); @@ -457,6 +487,48 @@ public class ClientLoader implements Supplier */ } + private boolean checkVanillaHash() + { + try (InputStream is = ClientLoader.class.getResourceAsStream("/client.hash")) + { + String storedHash = new String(is.readAllBytes(), StandardCharsets.UTF_8); + String vanillaHash = Hashing.sha256().hashBytes(Files.readAllBytes(VANILLA_CACHE.toPath())).toString(); + log.debug("Stored vanilla hash: {}", storedHash); + log.debug("Actual vanilla hash: {}", vanillaHash); + + return vanillaHash.equals(storedHash); + } + catch (IOException ex) + { + log.error("Failed to compare vanilla hashes, loading vanilla", ex); + } + + return false; + } + + private void writeInjectedClient(File cachedInjected) throws IOException + { + String cachedHash = ""; + try + { + cachedHash = com.google.common.io.Files.asByteSource(cachedInjected).hash(Hashing.sha256()).toString(); + } + catch (IOException ex) + { + log.error("Failed to calculate hash for cached file, falling back to vanilla", ex); + updateCheckMode = VANILLA; + return; + } + + byte[] currentInjected = ByteStreams.toByteArray(ClientLoader.class.getResourceAsStream(INJECTED_CLIENT_NAME)); + String currentHash = Hashing.sha256().hashBytes(currentInjected).toString(); + + if (!cachedInjected.exists() || !currentHash.equals(cachedHash)) + { + Files.write(cachedInjected.toPath(), currentInjected); + } + } + private ClassLoader createJarClassLoader(File jar) throws IOException, ClassNotFoundException { try (JarFile jarFile = new JarFile(jar)) @@ -529,7 +601,7 @@ public class ClientLoader implements Supplier if (rs instanceof Client) { - //.info("client-patch {}", ((Client) rs).getBuildID()); + log.info("injected-client {}", OpenOSRS.SYSTEM_VERSION); } return rs;