Skip to content

Commit

Permalink
Implement automatic downloading if ModPatcher
Browse files Browse the repository at this point in the history
  • Loading branch information
LunNova committed Feb 2, 2016
1 parent b401810 commit 92abcae
Show file tree
Hide file tree
Showing 5 changed files with 258 additions and 134 deletions.
13 changes: 7 additions & 6 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,16 @@ ext.mcVersion = '1.8.9'
ext.forgeVersion = "11.15.0.1683"
ext.fullForgeVersion = mcVersion + "-" + forgeVersion

// ci information
ext.buildNumber = System.getenv("BUILD_NUMBER") ?: 0
ext.ciSystem = System.getenv("JENKINS_URL") ? 'Jenkins' : 'unknown'
ext.commit = System.getenv("GIT_COMMIT") ?: 'unknown'

minecraft {
version = fullForgeVersion
mappings = "stable_20"

replace "@VERSION@", project.version
replace "@VERSION@", project.buildNumber != 0 ? (project.version.replace("-SNAPSHOT", "") + '.' + project.buildNumber) : project.version
replace "@MC_VERSION@", version
}

Expand All @@ -51,11 +56,6 @@ targetCompatibility = 1.8

version = mcVersion + '-SNAPSHOT'

// Define variables
ext.buildNumber = System.getenv("BUILD_NUMBER") ?: 0
ext.ciSystem = System.getenv("JENKINS_URL") ? 'Jenkins' : 'unknown'
ext.commit = System.getenv("GIT_COMMIT") ?: 'unknown'

repositories {
mavenCentral()
maven { url 'http://repo.nallar.me/' }
Expand All @@ -68,6 +68,7 @@ configurations {
}

dependencies {
testCompile 'junit:junit:4.12'
compile group: 'me.nallar', name: 'javapatcher', version: '1.1', changing: false
compile group: 'me.nallar', name: 'Mixin', version: '1.0-SNAPSHOT', changing: true
compile 'org.javassist:javassist:3.18.2-GA'
Expand Down
298 changes: 230 additions & 68 deletions src/main/java/me/nallar/modpatcher/ModPatcher.java
Original file line number Diff line number Diff line change
@@ -1,87 +1,84 @@
package me.nallar.modpatcher;

import com.google.common.base.Charsets;
import com.google.common.io.Resources;
import me.nallar.javapatcher.patcher.Patcher;
import net.minecraft.launchwrapper.LaunchClassLoader;
import net.minecraftforge.fml.relauncher.ReflectionHelper;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.*;
import java.lang.reflect.*;
import java.net.*;
import java.nio.file.FileSystem;
import java.nio.file.*;
import java.util.concurrent.*;

/**
* ModPatcher API
*
* This class is the public facing API of ModPatcher
*/
public class ModPatcher {
private static final String modPatcherDownloadUrl = "https://modpatcher.nallar.me/ModPatcher-lib.jar";
private static Path modPatcherPath = Paths.get("./libs/me/nallar/modpatcher/ModPatcher-lib.jar").toAbsolutePath();
private static final Logger log = LogManager.getLogger("ModPatcher");
private static final String mcVersion = "@MC_VERSION@";
private static final Path modPatcherPath = Paths.get("./libs/me/nallar/modpatcher/ModPatcher-lib.jar").toAbsolutePath();
private static final Future<Boolean> defaultUpdateRequired = CompletableFuture.completedFuture(Files.exists(modPatcherPath));
private static String modPatcherRelease;
private static Future<Boolean> updateRequired = defaultUpdateRequired;
private static Version requiredVersion;
private static Version lastVersion;

static {
try {
checkClassLoading();
} catch (NoClassDefFoundError e) {
loadModPatcher();
}
}

private static void loadModPatcher() {
downloadIfNeeded();

addToCurrentClassLoader();

checkClassLoading();
}

@SuppressWarnings("unchecked")
private static void addToCurrentClassLoader() {
ClassLoader cl = ModPatcher.class.getClassLoader();

LaunchClassLoader lcl = null;
if (cl instanceof LaunchClassLoader) {
lcl = (LaunchClassLoader) cl;
cl = ReflectionHelper.<ClassLoader, LaunchClassLoader>getPrivateValue(LaunchClassLoader.class, lcl, "parent");
lcl.addClassLoaderExclusion("me.nallar.modpatcher");
}

try {
Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
method.setAccessible(true);
method.invoke(cl, modPatcherPath.toUri().toURL());
} catch (Exception e) {
throw new Error(e);
}

ModPatcherLoadHook.loadedAfterDownload(lcl);
}

private static void downloadIfNeeded() {
if (!Files.exists(modPatcherPath))
download();
}

private static void download() {
try (InputStream in = new URL(modPatcherDownloadUrl).openConnection().getInputStream()) {
Files.copy(in, modPatcherPath);
} catch (IOException e) {
e.printStackTrace();
}
/**
* Gets the name of the setup class to use in your IFMLLoadingPlugin
*
* @return Name of the ModPatcher setup class
*/
public static String getSetupClass() {
return getSetupClass(null);
}

private static void checkClassLoading() {
if (ModPatcherLoadHook.class.getClassLoader().getClass().getName().contains("LaunchClassLoader")) {
throw new Error("ModPatcher not be loaded under LaunchClassLoader");
}
/**
* Gets the name of the setup class to use in your IFMLLoadingPlugin
*
* @param versionString Minimum version of ModPatcher required. Special value "latest" always uses latest version
* @return Name of the ModPatcher setup class
*/
public static String getSetupClass(String versionString) {
return getSetupClass(versionString, null);
}

/**
* Ensures that ModPatcher is at least the given version. Triggers auto-updating if not.
* Gets the name of the setup class to use in your IFMLLoadingPlugin
*
* @param version minimum required version
* @param versionString Minimum version of ModPatcher required. Special value "latest" always uses latest version
* @param release Release stream to use
* @return Name of the ModPatcher setup class
*/
public static void ensureVersion(String version) {
ModPatcherLoadHook.ensureVersion(version);
public static String getSetupClass(String versionString, String release) {
if (versionString != null || release != null) {
if (updateRequired == null) {
throw new Error("Modpatcher has already been loaded, it is too late to call getSetupClass");
}
if (release != null) {
if (modPatcherRelease == null) {
modPatcherRelease = release;
startVersionCheck();
} else {
log.warn("Conflicting ModPatcher release requests. Set to " + modPatcherRelease + ", requested: " + release);
}
}
if (versionString != null) {
Version requested = Version.of(versionString);
if (requested.compareTo(requiredVersion) > 0) {
requiredVersion = requested;
startVersionCheck();
}
}
}

return "me.nallar.modpatcher.ModPatcherSetup";
}

/**
Expand All @@ -99,6 +96,7 @@ public static Patcher getPatcher() {
* @param mixinClass Class to load mixins from package of
*/
public static void loadMixins(Class<?> mixinClass) {
checkClassLoading();
ModPatcherTransformer.loadMixins(mixinClass);
}

Expand All @@ -108,6 +106,7 @@ public static void loadMixins(Class<?> mixinClass) {
* @param path Path to load mixins from
*/
public static void loadMixins(Path path) {
checkClassLoading();
ModPatcherTransformer.loadMixins(path);
}

Expand All @@ -117,25 +116,188 @@ public static void loadMixins(Path path) {
* @param path Path to load mixins from
*/
public static void loadMixins(Path path, String packageName) {
checkClassLoading();
ModPatcherTransformer.loadMixins(path, packageName);
}

/**
* Gets the name of the setup class to use in your IFMLLoadingPlugin
*
* @return Name of the ModPatcher setup class
*/
public static String getSetupClass() {
return "me.nallar.modpatcher.ModPatcherSetup";
}

/**
* Gets the default patches directory. Any patches in this directory are loaded by ModPatcher on startup.
*
* @return default patches directory
*/
public static String getDefaultPatchesDirectory() {
checkClassLoading();
return ModPatcherTransformer.getDefaultPatchesDirectory();
}


private static void loadModPatcher() {
download();

updateRequired = null;

addToCurrentClassLoader();

checkClassLoading();
}

private static String getModPatcherRelease() {
return mcVersion + '-' + System.getProperty("modpatcher.release", modPatcherRelease == null ? "stable" : modPatcherRelease);
}

@SuppressWarnings("unchecked")
private static void addToCurrentClassLoader() {
ClassLoader cl = ModPatcher.class.getClassLoader();

if (cl instanceof LaunchClassLoader) {
if (System.getProperty("modpatcher.allowLoadingUnderLCL").equals("true")) {
LaunchClassLoader lcl = (LaunchClassLoader) cl;
cl = ReflectionHelper.<ClassLoader, LaunchClassLoader>getPrivateValue(LaunchClassLoader.class, lcl, "parent");
lcl.addClassLoaderExclusion("me.nallar.modpatcher");
} else {
throw new Error("Can't load ModPatcher under LaunchClassLoader");
}
}

try {
Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
method.setAccessible(true);
method.invoke(cl, modPatcherPath.toUri().toURL());
} catch (Exception e) {
throw new Error(e);
}

ModPatcherLoadHook.loadHook(requiredVersion, getModPatcherRelease());
}

private static boolean isDownloadNeeded() {
try {
return updateRequired.get();
} catch (InterruptedException | ExecutionException e) {
log.warn("Failed to check if updates are required", e);
}
return false;
}

private static void download() {
if (!isDownloadNeeded())
return;

try (InputStream in = new URL(System.getProperty("modpatcher.downloadUrl", "https://modpatcher.nallar.me/" + getModPatcherRelease() + "/ModPatcher-lib.jar")).openConnection().getInputStream()) {
Files.deleteIfExists(modPatcherPath);
Files.copy(in, modPatcherPath);
} catch (IOException e) {
e.printStackTrace();
}
}

private static void checkClassLoading() {
try {
ModPatcherLoadHook.class.getName();
} catch (NoClassDefFoundError e) {
loadModPatcher();
}
}

private static void startVersionCheck() {
updateRequired.cancel(true);

try {
if (!updateRequired.isDone() || updateRequired.isCancelled() || !updateRequired.get()) {
updateRequired = new FutureTask<>(() -> {
Version current = getLastVersion();
if (requiredVersion.newerThan(current)) {
try {
Version online = new Version(Resources.toString(new URL(System.getProperty("modpatcher.versionUrl", "https://modpatcher.nallar.me/" + getModPatcherRelease() + "latest-version.txt")), Charsets.UTF_8).trim());
return online.compareTo(current) > 0;
} catch (InterruptedIOException ignored) {
} catch (Throwable t) {
log.warn("Failed to check for update", t);
}
}
return false;
});
}
} catch (InterruptedException | ExecutionException e) {
log.warn("Interrupted when checking done/not cancelled future", e);
}
}

static Version getLastVersion() {
if (lastVersion != null)
return lastVersion;

if (!Files.exists(modPatcherPath))
return Version.NONE;

try (FileSystem fs = FileSystems.newFileSystem(modPatcherPath, null)) {
return lastVersion = new Version(Files.readAllLines(fs.getPath("modpatcher.version"), Charsets.UTF_8).get(0));
} catch (IOException e) {
throw new IOError(e);
}
}

static class Version implements Comparable<Version> {
public static final Version LATEST = new Version(String.valueOf(Integer.MAX_VALUE));
public static final Version NONE = new Version("0");
private String version;

private Version(String version) {
if (version == null)
throw new IllegalArgumentException("Version can not be null");
if (!version.matches("[0-9]+(\\.[0-9]+)*"))
throw new IllegalArgumentException("Invalid version format");
this.version = version;
}

static Version of(String s) {
if (s.equalsIgnoreCase("latest")) {
return LATEST;
}
return new Version(s);
}

public final String get() {
return this.version;
}

@Override
public int compareTo(Version that) {
if (that == null)
return 1;

if (this == that || version.equals(that.version))
return 0;

String[] thisParts = this.get().split("\\.");
String[] thatParts = that.get().split("\\.");
int length = Math.max(thisParts.length, thatParts.length);
for (int i = 0; i < length; i++) {
int thisPart = i < thisParts.length ?
Integer.parseInt(thisParts[i]) : 0;
int thatPart = i < thatParts.length ?
Integer.parseInt(thatParts[i]) : 0;
if (thisPart < thatPart)
return -1;
if (thisPart > thatPart)
return 1;
}

return 0;
}

@Override
public int hashCode() {
return version.hashCode();
}

@Override
public boolean equals(Object that) {
return this == that || that != null && this.getClass() == that.getClass() && this.compareTo((Version) that) == 0;
}

public boolean newerThan(Version other) {
return compareTo(other) > 0;
}
}
}

0 comments on commit 92abcae

Please sign in to comment.