aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md65
-rwxr-xr-xbuild.gradle38
-rw-r--r--settings.gradle1
-rw-r--r--tweaker/build.gradle26
-rw-r--r--tweaker/src/main/java/net/hypixel/modapi/tweaker/HypixelModAPITweaker.java225
5 files changed, 342 insertions, 13 deletions
diff --git a/README.md b/README.md
index bf922d2..b8e8608 100644
--- a/README.md
+++ b/README.md
@@ -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];
+ }
+}