aboutsummaryrefslogtreecommitdiff
path: root/tweaker/src
diff options
context:
space:
mode:
Diffstat (limited to 'tweaker/src')
-rw-r--r--tweaker/src/main/java/net/hypixel/modapi/tweaker/HypixelModAPITweaker.java200
1 files changed, 200 insertions, 0 deletions
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<String> 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];
+ }
+}