diff options
-rw-r--r-- | README.md | 65 | ||||
-rwxr-xr-x | build.gradle | 38 | ||||
-rw-r--r-- | settings.gradle | 1 | ||||
-rw-r--r-- | tweaker/build.gradle | 26 | ||||
-rw-r--r-- | tweaker/src/main/java/net/hypixel/modapi/tweaker/HypixelModAPITweaker.java | 225 |
5 files changed, 342 insertions, 13 deletions
@@ -1,6 +1,69 @@ # Forge Mod API -This repository contains the implementation of the Hypixel Mod API for the Froge Mod Loader in 1.8.9 for end-users. If you are a developer, and are looking to utilise the API you should look at the core [Hypixel Mod API](https://github.com/HypixelDev/ModAPI) repository. +This repository contains the implementation of the Hypixel Mod API for the Forge Mod Loader in 1.8.9 for end-users. If you are a developer, and are looking to utilise the API you should look at the core [Hypixel Mod API](https://github.com/HypixelDev/ModAPI) repository. + +## Usage + +Add the HyPixel Maven repository to your build: + +```kotlin +repositories { + maven("https://repo.hypixel.net/repository/Hypixel/") +} +``` + +Then depend on the Forge Mod API. This will automatically pull in dependencies too. + +```kotlin +val version = "1.0.0.2" +dependencies { + modImplementation("net.hypixel:mod-api-forge:$version") + // If you use ForgeGradle 2 you might need to use fg.deobf or deobfCompile instead. Consult your MDK for tips on how + // to depend on an obfuscated dependency +} +``` + +From here on out you can use the [HyPixel Mod API](https://github.com/HypixelDev/ModAPI#example-usage) directly. + +### Bundling the HyPixel Mod API + +When using the HyPixel Mod API you need to instruct your users to +[download](https://modrinth.com/mod/hypixel-mod-api/versions?l=forge) the mod api and put it in their mods folders. + +Alternatively you can bundle a loading tweaker instead. This involves a bit more setup, but will result in your mod +containing a copy of the mod api and at runtime selecting the newest available version of the mod api. + +First you need to have a shadow plugin in your gradle setup. Note that normal `fileTree` based JAR copying does not +work, since we will need to relocate some files. Instead, use the [shadow plugin](https://github.com/johnrengelman/shadow) +or make use of a [template](https://github.com/nea89o/Forge1.8.9Template) with the plugin already set up. + +Once you have your shadow plugin set up you will need to include a new dependency: +```kotlin +dependencies { + modImplementation("net.hypixel:mod-api-forge:$version") // You should already have this dependency from earlier + shadowImpl("net.hypixel:mod-api-forge-tweaker:$version") // You need to add this dependency +} +``` + +Make sure to relocate the `net.hypixel.modapi.tweaker` package to a unique location such as `my.modid.modapitweaker`. + +Finally, add a manifest entry to your JAR pointing the `TweakClass` to `my.modid.modapitweaker.HypixelModAPITweaker` +(or otherwise load the tweaker using delegation). + +```kotlin +tasks.shadowJar { + configurations = listOf(shadowImpl) + relocate("net.hypixel.modapi.tweaker", "my.modid.modapitweaker.HypixelModAPITweaker") +} + +tasks.withType(org.gradle.jvm.tasks.Jar::class) { + manifest.attributes.run { + this["TweakClass"] = "my.modid.modapitweaker.HypixelModAPITweaker" + } +} +``` + +Now your users will automatically use the bundled version of the mod api. ## Contributing diff --git a/build.gradle b/build.gradle index e198753..839d15b 100755 --- a/build.gradle +++ b/build.gradle @@ -11,23 +11,37 @@ buildscript { } } apply plugin: 'net.minecraftforge.gradle.forge' +allprojects { + apply plugin: 'maven-publish' + version = "1.0.0.2" // 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" // 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/" + } + maven { + url "https://pkgs.dev.azure.com/djtheredstoner/DevAuth/_packaging/public/maven/v1" + } + mavenLocal() } - maven { - url "https://pkgs.dev.azure.com/djtheredstoner/DevAuth/_packaging/public/maven/v1" + + publishing { + publications { + maven(MavenPublication) { + groupId = project.group + artifactId = project == rootProject ? 'mod-api-forge' : ('mod-api-forge-' + project.name) + version = project.version + from components.java + } + } } - 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..e8eada8 --- /dev/null +++ b/tweaker/build.gradle @@ -0,0 +1,26 @@ +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 { + dependsOn(project(":").reobfJar) + from(project(":").jar) + from(generateVersionInfo) + manifest { + attributes("TweakClass": "net.hypixel.modapi.tweaker.HypixelModAPITweaker") + } +} 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..4464cd7 --- /dev/null +++ b/tweaker/src/main/java/net/hypixel/modapi/tweaker/HypixelModAPITweaker.java @@ -0,0 +1,225 @@ +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 org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.MalformedURLException; +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 { + + private static Logger LOGGER = LogManager.getLogger(); + 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) { + LOGGER.error("Could not load version information for bundled hypixel mod API", e); + } + VERSION_NAME = properties.getProperty("version", "0.0.0.0"); + LOGGER.info("Loaded bundled hypixel mod API version as {}", VERSION_NAME); + 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; + LOGGER.info("Loaded bundled hypixel mod API numeric version as {}", 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) { + LOGGER.info("Blackboard version newer than our version {}. Skipping injecting API.", VERSION); + return; + } + // If we didn't offer to install this version return + if (!hasOfferedVersion) { + LOGGER.info("Someone else with the same version number {} offered to inject themselves first. Skipping injecting API.", VERSION); + 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(); + LOGGER.info("Unpacking mod API to {}", extractedFile); + //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); + LOGGER.info("Successfully extracted mod API file"); + return extractedFile; + } catch (IOException e) { + LOGGER.error("Could not extract mod API file", 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() { + LOGGER.info("Injecting mod API of version {}", VERSION_NAME); + try { + Launch.classLoader.addURL(unpackAPI().toURI().toURL()); + LOGGER.info("Added mod API to classpath"); + } catch (MalformedURLException e) { + LOGGER.error("Could not add mod API to classpath", e); + } + } + + /** + * 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()); + LOGGER.info("Re-added mod {} to the mod candidate list.", file); + } else { + LOGGER.warn("Did not find JAR including this tweaker, cannot re-add mod."); + } + } + + /** + * 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) { + LOGGER.info("Offering newer version {} > {}", VERSION, getBlackboardVersion()); + 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<String> args, File gameDir, File assetsDir, String profile) { + offerVersionToBlackboard(); + allowModLoading(); + } + + @Override + public void injectIntoClassLoader(LaunchClassLoader classLoader) { + } + + @Override + public String getLaunchTarget() { + return null; + } + + @Override + public String[] getLaunchArguments() { + tryInjectAPI(); + return new String[0]; + } +} |