From 6671846319d1fcc5b8b999a77f297b098763535d Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Fri, 12 Jul 2024 18:23:37 +0200 Subject: Add tweaker loader --- build.gradle | 22 +-- settings.gradle | 1 + tweaker/build.gradle | 22 +++ .../modapi/tweaker/HypixelModAPITweaker.java | 200 +++++++++++++++++++++ 4 files changed, 235 insertions(+), 10 deletions(-) create mode 100644 settings.gradle create mode 100644 tweaker/build.gradle create mode 100644 tweaker/src/main/java/net/hypixel/modapi/tweaker/HypixelModAPITweaker.java diff --git a/build.gradle b/build.gradle index b6ba6fd..36577c8 100755 --- a/build.gradle +++ b/build.gradle @@ -11,20 +11,22 @@ buildscript { } } apply plugin: 'net.minecraftforge.gradle.forge' +allprojects { + version = "1.0.0.1" // First 3 numbers should correspond to the version of the API, last number is for the mod itself for any changes/fixes + group = "net.hypixel.modapi" // http://maven.apache.org/guides/mini/guide-naming-conventions.html + archivesBaseName = "HypixelModAPI" -version = "1.0.0.1" // First 3 numbers should correspond to the version of the API, last number is for the mod itself for any changes/fixes -group = "net.hypixel.modapi" // http://maven.apache.org/guides/mini/guide-naming-conventions.html -archivesBaseName = "HypixelModAPI" -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - -repositories { - maven { - url "https://repo.hypixel.net/repository/Hypixel/" + repositories { + maven { + url "https://repo.hypixel.net/repository/Hypixel/" + } + mavenLocal() } - mavenLocal() } +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + minecraft { version = "1.8.9-11.15.1.1722" // version = "1.8.9-11.15.1.2318-1.8.9" diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..54eaa93 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include 'tweaker' \ No newline at end of file diff --git a/tweaker/build.gradle b/tweaker/build.gradle new file mode 100644 index 0000000..8962b17 --- /dev/null +++ b/tweaker/build.gradle @@ -0,0 +1,22 @@ +apply plugin: 'net.minecraftforge.gradle.forge' + +minecraft { + version = "1.8.9-11.15.1.1722" +// version = "1.8.9-11.15.1.2318-1.8.9" + runDir = "run" + + mappings = "stable_22" +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +task generateVersionInfo(type: WriteProperties) { + outputFile(file(new File(buildDir, "/properties/hypixel-mod-api-bundled.properties"))) + property("version", version) +} + +tasks.jar { + from(project(":").jar) + from(generateVersionInfo) +} diff --git a/tweaker/src/main/java/net/hypixel/modapi/tweaker/HypixelModAPITweaker.java b/tweaker/src/main/java/net/hypixel/modapi/tweaker/HypixelModAPITweaker.java new file mode 100644 index 0000000..857433c --- /dev/null +++ b/tweaker/src/main/java/net/hypixel/modapi/tweaker/HypixelModAPITweaker.java @@ -0,0 +1,200 @@ +package net.hypixel.modapi.tweaker; + +import net.minecraft.launchwrapper.ITweaker; +import net.minecraft.launchwrapper.Launch; +import net.minecraft.launchwrapper.LaunchClassLoader; +import net.minecraftforge.fml.relauncher.CoreModManager; +import org.apache.commons.io.IOUtils; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.util.List; +import java.util.Objects; +import java.util.Properties; + +/** + * A tweaker class to automatically load the hypixel mod api while resolving conflicts + * from multiple mods providing a copy. This saves users from having to download the + * mod api separately. + */ +public class HypixelModAPITweaker implements ITweaker { + + public static final String VERSION_NAME; + public static final long VERSION; + + static { + // Load version information from the .properties generated by the gradle task generateVersionInfo + Properties properties = new Properties(); + try { + properties.load(HypixelModAPITweaker.class.getResourceAsStream("/hypixel-mod-api-bundled.properties")); + } catch (IOException e) { + e.printStackTrace(); + } + VERSION_NAME = properties.getProperty("version", "0.0.0.0"); + String[] versionComponents = VERSION_NAME.split("\\."); + assert versionComponents.length == 4; + // We pack each of the four version components into a long. + // To do so we use the biggest number that can fit 4 times into a long: + //noinspection ConstantValue + assert Math.pow(10000, 4) < Long.MAX_VALUE; + long version = 0; + for (int i = 0; i < 4; i++) { + version *= 10000; + version += Long.parseLong(versionComponents[i]); + } + VERSION = version; + } + + public static final String BUNDLED_JAR_NAME = "HypixelModAPI-" + VERSION_NAME + ".jar"; + + /** + * This is the key which is used in the {@link Launch#blackboard} to perform + * version negotiation. + * + * @see #getBlackboardVersion() + * @see #offerVersionToBlackboard() + */ + public static final String VERSION_KEY = "net.hypixel.mod-api.version:1"; + + private boolean hasOfferedVersion = false; + + /** + * Get the current version declared on the blackboard. + * The blackboard allows us to store arbitrary values. We store an int indicating the max version installed. + * + * @see #VERSION_KEY + */ + public static long getBlackboardVersion() { + Object blackboardVersion = Launch.blackboard.get(VERSION_KEY); + // In case nobody has declared a version on the blackboard yet, we return an incredibly outdated past version + if (blackboardVersion == null) return Long.MIN_VALUE; + // In case we later switch to another version format we declare any non-integer as an incredibly advanced future version + if (!(blackboardVersion instanceof Long)) return Long.MAX_VALUE; + return (Long) blackboardVersion; + } + + /** + * Inject our API jar into forge if we are the highest available version. + * + * @see #injectAPI() + */ + private void tryInjectAPI() { + // If the maximum installed version isn't our version return + if (getBlackboardVersion() != VERSION) return; + // If we didn't offer to install this version return + if (!hasOfferedVersion) return; + + injectAPI(); + } + + /** + * Unpacks the actual API file into a temporary folder from where it can be loaded. + * + * @return the location of the extracted API + */ + private File unpackAPI() { + File extractedFile = new File("hypixel-mod-api/" + BUNDLED_JAR_NAME).getAbsoluteFile(); + //noinspection ResultOfMethodCallIgnored + extractedFile.getParentFile().mkdirs(); + try (InputStream bundledJar = Objects.requireNonNull( + getClass().getResourceAsStream("/" + BUNDLED_JAR_NAME), + "Could not find bundled hypixel mod api"); + OutputStream outputStream = Files.newOutputStream(extractedFile.toPath())) { + IOUtils.copy(bundledJar, outputStream); + return extractedFile; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * @return the jar containing this class + */ + private File getThisJar() { + URL thisJarURL = getClass().getProtectionDomain().getCodeSource().getLocation(); + if (thisJarURL == null) return null; + if (!"file".equals(thisJarURL.getProtocol())) return null; + try { + return new File(thisJarURL.toURI()).getAbsoluteFile(); + } catch (URISyntaxException ignored) { + } + return null; + } + + /** + * Inject the API into Forge to be loaded as a mod. This will also extract the JAR. + */ + private void injectAPI() { + CoreModManager.getReparseableCoremods() + .add(unpackAPI().getPath()); + } + + /** + * Instruct Forge to load the current JAR as a mod. + * By default, Forge does not load mods if they contain a tweaker, only loading the tweaker instead. + * This function will remove this JAR from the list of ignored mods and schedule it to be scanned for mods. + */ + private void allowModLoading() { + File file = getThisJar(); + if (file != null) { + CoreModManager.getReparseableCoremods() + .add(file.getPath()); + CoreModManager.getIgnoredMods() + .remove(file.getPath()); + } + } + + /** + * Offers our bundled version to the {@link Launch#blackboard}. If there is already a + * higher version than ours installed then this will not do anything. Otherwise, it + * will set {@link #hasOfferedVersion} and increment the version in the blackboard. + * + * @see #VERSION_KEY + */ + private void offerVersionToBlackboard() { + if (getBlackboardVersion() < VERSION) { + hasOfferedVersion = true; + Launch.blackboard.put(VERSION_KEY, VERSION); + } + } + + /* + Below here are all the ITweaker methods. The tweaker methods are executed in rounds. + + 1. Run class init and init (constructor) for each tweaker + 2. Run acceptOptions for each tweaker + 3. Run injectIntoClassLoader for each tweaker + 4. If any cascading tweakers have been registered, go back to step 1 + 5. After there are no new tweakers after an entire round: + 6. Run getLaunchArguments for each tweaker + 7. Run getLaunchTarget only on the first tweaker found (and execute that class) + + By first offering our version negotiation in acceptOptions or injectIntoClassloader and + then injection our JARs in getLaunchArguments, we can ensure that every tweaker had time + to make their version announcement heard. + */ + @Override + public void acceptOptions(List args, File gameDir, File assetsDir, String profile) { + offerVersionToBlackboard(); + } + + @Override + public void injectIntoClassLoader(LaunchClassLoader classLoader) { + } + + @Override + public String getLaunchTarget() { + return null; + } + + @Override + public String[] getLaunchArguments() { + tryInjectAPI(); + return new String[0]; + } +} -- cgit