diff options
Diffstat (limited to 'tweaker/src')
-rw-r--r-- | tweaker/src/main/java/net/hypixel/modapi/tweaker/HypixelModAPITweaker.java | 200 |
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]; + } +} |