Monday, June 15, 2015

Adventures in Minecraft Mod Management

My nephews and I play Minecraft. I use the Magic Launcher instead of the regular launcher because I like how it applies mods. My computer is very old (a few months shy of 10 years old), so I need to use Optifine to get a frame rate that even approaches 30 FPS. I could just install Optifine directly into the Minecraft JAR file, but sometimes Optifine causes problems so I like to be able to disable it easily. I could maintain two versions of every Minecraft JAR, one with Optifine and one without, but that seems tedious. The Magic Launcher does a good job managing mods like Optifine but it falls short when managing mods for something like Forge. My nephews had me install a few Forge mods (OreSpawn, Pixelmon, and Wildycraft). Every piece of documentation I could find on Forge says to just put these mods into the Minecraft mods directory. If you only have one such mod at a time, this works perfectly well. I even found some forum posts mentioning that you could create subdirectories for different versions of Minecraft but that was of limited use. Whenever my nephews wanted to switch between Forge mods, I would be forced to rename or relocate mod files. I very much wanted an easy way to tell Forge which mods to load on any given launch of Minecraft. I suspected that a command line argument for this purpose might exist because it seemed like a great solution to what I perceive to be a common problem. Unfortunately, neither Google nor Bing nor searches of Forge's own Wiki turned up any information on the command line arguments I sought. At first, I started poking around the Minecraft launcher's JSON files to see if Forge's versions of those contained any settings I could use. I didn't find any but I did notice the minecraftArguments setting which contained the command line arguments passed to Minecraft. I figured that if I found any command line arguments for Forge, I could just tack them onto this setting for testing purposes.

While searching about online, I accidentally stumbled across the Forge GitHub page. I downloaded the source files and began digging through them to look for any command line arguments. Eventually, I came across the ModListHelper.java file which contained code for two command line arguments: --modListFile [file] and --mods [mod1,mod2,...]. I toyed around with --modListFile first as I thought using a list file would be the most convenient way to handle my situation. Unfortunately, the format for the mod list file is a bit different than I expected. It's a JSON file with a few settings, one of which stores a list of mod file names. It expects each mod file name to be in one of the following two formats A:B:C or A:B:C:D, where A, B, C, and D are strings not containing colons. Depending on whether or not you use the D, those names are converted into these file names: A.B.C.B-C.jar or A.B.C.B-C-D.jar. Unless you're willing to rename your mods to suit this format and its conversion, the --modListFile argument will not be useful.

The --mods argument is much more useful. It takes one argument that is a comma-delimited list of mod files with paths relative to the .minecraft directory. I am now keeping my mods in a subdirectory of .minecraft called ForgeMods, so a --mods argument for me might look like:

--mods "ForgeMods\1.7.10\Pixelmon-1.7.10-3.4.0-universal.jar,ForgeMods\1.7.10\Gameshark.jar,ForgeMods\1.7.10\FinderCompass-1.7.10.jar"

The double quotes are not required if there are no spaces in the mod list but I like to use them anyway so I don't have to worry about adding spaces later if need be. Also, note that I still maintain the use of Minecraft version subdirectories (1.7.10 in this case) but this is completely unnecessary. To test out the --mods argument, I added it to the minecraftArguments setting in the Minecraft launcher JSON for one of my Forge installations. (Remember that double quotes and back slashes need to be escaped when part of a JSON string.) I then loaded up the Minecraft launcher and ran that version of Forge. Success!

I opened up the Magic Launcher to add my new --mods command line argument but I ran into two problems. The latest version of Forge for Minecraft 1.7.10 (1.7.10-Forge10.13.4.1448-1.7.10 at this moment) does not install a JAR file. It seems to use some sort of "inheritsFrom" setting instead. The Magic Launcher is unable to use this installation of Forge. After some searching, I saw someone suggest using the "universal" Forge JAR (for this version of Forge) and have the Magic Launcher apply it as a mod to the standard version of Minecraft. This approach was successful.

I then proceeded to try to add the --mods argument only to find that the launcher does not support adding command line arguments for anything other than the JVM. I tried just adding the argument anyway, but Java was not fond of that. It failed with an "unrecognized option" or something of that nature. I considered trying to just switch over to using the Minecraft launcher full time but that launcher has this annoying habit of downloading (or just creating) a fresh copy of the "natives" directory every time I launch Minecraft. I think it's supposed to delete that directory when Minecraft closes but it doesn't do this on my computer, so I end up with a ton of these directories after a while.

I decided I would try to mod the Magic Launcher itself to see if I could get it to support the --mods argument. The Magic Launcher is some sort of self-executing JAR file. I'm not familiar with the technology, but 7-Zip was able to open it up as a JAR and extract the files. There was a problem though. The Magic Launcher JAR contains some file names that differ only in capitalization. For example, there are both an a.class and an A.class. In some file systems (case-sensitive ones), this is fine, but in a Windows file system (case-insensitive), this causes problems. 7-Zip handled this problem by renaming some of the files. Fortunately, none of the files I needed were any of the ones that were renamed. I used fernflower to decompile the class files into Java source files. I dug through the decompiled code, which wasn't easy because virtually every variable was named by just a letter or two, until I found the code that handled command line arguments for Minecraft. Luckily for me, this same file contained the code that handled the JVM arguments I mentioned earlier, so I would only end up needing to change one file: magic\launcher\ap.java. The class used a single ArrayList to store all of the command line arguments passed to Java and then to Minecraft. The only thing that changed an argument from a JVM argument to a Minecraft argument was where it appeared in the list of arguments (before or after -mainClass, I think). The class used a for loop to add the JVM arguments to the overall arguments list. During that loop, I added a check for "--mods" and stored the mod list into another variable if --mods was found. Later, if this variable wasn't null, I added "--mods" and it to the list of arguments. I used javac to compile my updated ap.java file into ap.class. Oddly enough, I had to manually add the throws clause to one of the methods in ap.java. I don’t know if that was a problem with fernflower or just a question of compiler settings.

Adding my new ap.class file back into the Magic Launcher proved a bit difficult. Initially, I tried to just open the Magic Launcher in 7-Zip and drop in the updated ap.class file. This resulted in an error having to do with files having duplicate names. I assume this was related to the a.class-versus-A.class problem I mentioned earlier. Then I tried just using Java's jar program to update the ap.class file, but that program was unable to work with the self-executing JAR file format used by the Magic Launcher. I searched the Internet a bit for information on self-executing JAR files to see if, at the very least, I could just extract the JAR file from the Magic Launcher executable. I was unable to find any useful information quickly so I opened up a random JAR file in a hex editor to find out what file header it used (hexadecimal bytes 50 4B 03 04, which is probably the same as a standard ZIP header since I believe JARs are just renamed ZIPs). I then opened the Magic Launcher executable in a hex editor and searched for the JAR header. After locating it, I deleted all data before the header and saved this new file as a JAR. The Java jar program was now able to update the ap.class file. I considered trying to re-add the JAR to the executable but since the JAR works quite well on its own on my system, I just left it as a JAR.

The final step was to create configurations for each of my mods in the Magic Launcher, adding the appropriate --mods command line argument for each. It took me the better part of a day to do all of this but it was definitely worth it to no longer have to deal with renaming or relocating mod files every time my nephews decide to play a different mod.