/*
* Forge Mod Loader
* Copyright (c) 2012-2013 cpw.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser Public License v2.1
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
*
* Contributors:
* cpw - implementation
*/
package net.minecraftforge.fml.relauncher;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import net.minecraft.launchwrapper.ITweaker;
import net.minecraft.launchwrapper.Launch;
import net.minecraft.launchwrapper.LaunchClassLoader;
import net.minecraftforge.fml.common.FMLLog;
import net.minecraftforge.fml.common.asm.ASMTransformerWrapper;
import net.minecraftforge.fml.common.asm.transformers.ModAccessTransformer;
import net.minecraftforge.fml.common.launcher.FMLInjectionAndSortingTweaker;
import net.minecraftforge.fml.common.launcher.FMLTweaker;
import net.minecraftforge.fml.common.toposort.TopologicalSort;
import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin.DependsOn;
import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin.MCVersion;
import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin.Name;
import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin.SortingIndex;
import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin.TransformerExclusions;
import org.apache.logging.log4j.Level;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.ObjectArrays;
import com.google.common.collect.Sets;
import com.google.common.primitives.Ints;
public class CoreModManager {
private static final Attributes.Name COREMODCONTAINSFMLMOD = new Attributes.Name("FMLCorePluginContainsFMLMod");
private static final Attributes.Name MODTYPE = new Attributes.Name("ModType");
private static final Attributes.Name MODSIDE = new Attributes.Name("ModSide");
private static String[] rootPlugins = { "net.minecraftforge.fml.relauncher.FMLCorePlugin", "net.minecraftforge.classloading.FMLForgePlugin" };
private static List<String> loadedCoremods = Lists.newArrayList();
private static Map<String, List<String>> transformers = Maps.newHashMap();
private static List<FMLPluginWrapper> loadPlugins;
private static boolean deobfuscatedEnvironment;
private static FMLTweaker tweaker;
private static File mcDir;
private static List<String> reparsedCoremods = Lists.newArrayList();
private static List<String> accessTransformers = Lists.newArrayList();
private static Set<String> rootNames = Sets.newHashSet();
static
{
for(String cls : rootPlugins)
{
rootNames.add(cls.substring(cls.lastIndexOf('.') + 1));
}
}
private static class FMLPluginWrapper implements ITweaker {
public final String name;
public final IFMLLoadingPlugin coreModInstance;
public final List<String> predepends;
public final File location;
public final int sortIndex;
public FMLPluginWrapper(String name, IFMLLoadingPlugin coreModInstance, File location, int sortIndex, String... predepends)
{
super();
this.name = name;
this.coreModInstance = coreModInstance;
this.location = location;
this.sortIndex = sortIndex;
this.predepends = Lists.newArrayList(predepends);
}
@Override
public String toString()
{
return String.format("%s {%s}", this.name, this.predepends);
}
@Override
public void acceptOptions(List<String> args, File gameDir, File assetsDir, String profile)
{
// NO OP
}
@Override
public void injectIntoClassLoader(LaunchClassLoader classLoader)
{
FMLRelaunchLog.fine("Injecting coremod %s {%s} class transformers", name, coreModInstance.getClass().getName());
List<String> ts = Lists.newArrayList();
if (coreModInstance.getASMTransformerClass() != null) for (String transformer : coreModInstance.getASMTransformerClass())
{
FMLRelaunchLog.finer("Registering transformer %s", transformer);
classLoader.registerTransformer(ASMTransformerWrapper.getTransformerWrapper(classLoader, transformer, name));
ts.add(transformer);
}
if(!rootNames.contains(name))
{
String loc;
if(location == null) loc = "unknown";
else loc = location.getName();
transformers.put(name + " (" + loc + ")", ts);
}
FMLRelaunchLog.fine("Injection complete");
FMLRelaunchLog.fine("Running coremod plugin for %s {%s}", name, coreModInstance.getClass().getName());
Map<String, Object> data = new HashMap<String, Object>();
data.put("mcLocation", mcDir);
data.put("coremodList", loadPlugins);
data.put("runtimeDeobfuscationEnabled", !deobfuscatedEnvironment);
FMLRelaunchLog.fine("Running coremod plugin %s", name);
data.put("coremodLocation", location);
coreModInstance.injectData(data);
String setupClass = coreModInstance.getSetupClass();
if (setupClass != null)
{
try
{
IFMLCallHook call = (IFMLCallHook) Class.forName(setupClass, true, classLoader).newInstance();
Map<String, Object> callData = new HashMap<String, Object>();
callData.put("runtimeDeobfuscationEnabled", !deobfuscatedEnvironment);
callData.put("mcLocation", mcDir);
callData.put("classLoader", classLoader);
callData.put("coremodLocation", location);
callData.put("deobfuscationFileName", FMLInjectionData.debfuscationDataName());
call.injectData(callData);
call.call();
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
FMLRelaunchLog.fine("Coremod plugin class %s run successfully", coreModInstance.getClass().getSimpleName());
String modContainer = coreModInstance.getModContainerClass();
if (modContainer != null)
{
FMLInjectionData.containers.add(modContainer);
}
}
@Override
public String getLaunchTarget()
{
return "";
}
@Override
public String[] getLaunchArguments()
{
return new String[0];
}
}
public static void handleLaunch(File mcDir, LaunchClassLoader classLoader, FMLTweaker tweaker)
{
CoreModManager.mcDir = mcDir;
CoreModManager.tweaker = tweaker;
try
{
// Are we in a 'decompiled' environment?
byte[] bs = classLoader.getClassBytes("net.minecraft.world.World");
if (bs != null)
{
FMLRelaunchLog.info("Managed to load a deobfuscated Minecraft name- we are in a deobfuscated environment. Skipping runtime deobfuscation");
deobfuscatedEnvironment = true;
}
}
catch (IOException e1)
{
}
if (!deobfuscatedEnvironment)
{
FMLRelaunchLog.fine("Enabling runtime deobfuscation");
}
tweaker.injectCascadingTweak("net.minecraftforge.fml.common.launcher.FMLInjectionAndSortingTweaker");
try
{
classLoader.registerTransformer("net.minecraftforge.fml.common.asm.transformers.PatchingTransformer");
}
catch (Exception e)
{
FMLRelaunchLog.log(Level.ERROR, e, "The patch transformer failed to load! This is critical, loading cannot continue!");
throw Throwables.propagate(e);
}
loadPlugins = new ArrayList<FMLPluginWrapper>();
for (String rootPluginName : rootPlugins)
{
loadCoreMod(classLoader, rootPluginName, new File(FMLTweaker.getJarLocation()));
}
if (loadPlugins.isEmpty())
{
throw new RuntimeException("A fatal error has occured - no valid fml load plugin was found - this is a completely corrupt FML installation.");
}
FMLRelaunchLog.fine("All fundamental core mods are successfully located");
// Now that we have the root plugins loaded - lets see what else might
// be around
String commandLineCoremods = System.getProperty("fml.coreMods.load", "");
for (String coreModClassName : commandLineCoremods.split(","))
{
if (coreModClassName.isEmpty())
{
continue;
}
FMLRelaunchLog.info("Found a command line coremod : %s", coreModClassName);
loadCoreMod(classLoader, coreModClassName, null);
}
discoverCoreMods(mcDir, classLoader);
}
private static void discoverCoreMods(File mcDir, LaunchClassLoader classLoader)
{
ModListHelper.parseModList(mcDir);
FMLRelaunchLog.fine("Discovering coremods");
File coreMods = setupCoreModDir(mcDir);
FilenameFilter ff = new FilenameFilter() {
@Override
public boolean accept(File dir, String name)
{
return name.endsWith(".jar");
}
};
FilenameFilter derpfilter = new FilenameFilter() {
@Override
public boolean accept(File dir, String name)
{
return name.endsWith(".jar.zip");
}
};
File[] derplist = coreMods.listFiles(derpfilter);
if (derplist != null && derplist.length > 0)
{
FMLRelaunchLog.severe("FML has detected several badly downloaded jar files, which have been named as zip files. You probably need to download them again, or they may not work properly");
for (File f : derplist)
{
FMLRelaunchLog.severe("Problem file : %s", f.getName());
}
}
File[] coreModList = coreMods.listFiles(ff);
File versionedModDir = new File(coreMods, FMLInjectionData.mccversion);
if (versionedModDir.isDirectory())
{
File[] versionedCoreMods = versionedModDir.listFiles(ff);
coreModList = ObjectArrays.concat(coreModList, versionedCoreMods, File.class);
}
coreModList = ObjectArrays.concat(coreModList, ModListHelper.additionalMods.values().toArray(new File[0]), File.class);
coreModList = FileListHelper.sortFileList(coreModList);
for (File coreMod : coreModList)
{
FMLRelaunchLog.fine("Examining for coremod candidacy %s", coreMod.getName());
JarFile jar = null;
Attributes mfAttributes;
try
{
jar = new JarFile(coreMod);
if (jar.getManifest() == null)
{
// Not a coremod and no access transformer list
continue;
}
ModAccessTransformer.addJar(jar);
mfAttributes = jar.getManifest().getMainAttributes();
}
catch (IOException ioe)
{
FMLRelaunchLog.log(Level.ERROR, ioe, "Unable to read the jar file %s - ignoring", coreMod.getName());
continue;
}
finally
{
if (jar != null)
{
try
{
jar.close();
}
catch (IOException e)
{
// Noise
}
}
}
String cascadedTweaker = mfAttributes.getValue("TweakClass");
if (cascadedTweaker != null)
{
FMLRelaunchLog.info("Loading tweaker %s from %s", cascadedTweaker, coreMod.getName());
Integer sortOrder = Ints.tryParse(Strings.nullToEmpty(mfAttributes.getValue("TweakOrder")));
sortOrder = (sortOrder == null ? Integer.valueOf(0) : sortOrder);
handleCascadingTweak(coreMod, jar, cascadedTweaker, classLoader, sortOrder);
loadedCoremods.add(coreMod.getName());
continue;
}
List<String> modTypes = mfAttributes.containsKey(MODTYPE) ? Arrays.asList(mfAttributes.getValue(MODTYPE).split(",")) : ImmutableList.of("FML");
if (!modTypes.contains("FML"))
{
FMLRelaunchLog.fine("Adding %s to the list of things to skip. It is not an FML mod, it has types %s", coreMod.getName(), modTypes);
loadedCoremods.add(coreMod.getName());
continue;
}
String modSide = mfAttributes.containsKey(MODSIDE) ? mfAttributes.getValue(MODSIDE) : "BOTH";
if (! ("BOTH".equals(modSide) || FMLLaunchHandler.side.name().equals(modSide)))
{
FMLRelaunchLog.fine("Mod %s has ModSide meta-inf value %s, and we're %s. It will be ignored", coreMod.getName(), modSide, FMLLaunchHandler.side.name());
loadedCoremods.add(coreMod.getName());
continue;
}
String fmlCorePlugin = mfAttributes.getValue("FMLCorePlugin");
if (fmlCorePlugin == null)
{
// Not a coremod
FMLRelaunchLog.fine("Not found coremod data in %s", coreMod.getName());
continue;
}
// Support things that are mod jars, but not FML mod jars
try
{
classLoader.addURL(coreMod.toURI().toURL());
if (!mfAttributes.containsKey(COREMODCONTAINSFMLMOD))
{
FMLRelaunchLog.finer("Adding %s to the list of known coremods, it will not be examined again", coreMod.getName());
loadedCoremods.add(coreMod.getName());
}
else
{
FMLRelaunchLog.finer("Found FMLCorePluginContainsFMLMod marker in %s, it will be examined later for regular @Mod instances",
coreMod.getName());
reparsedCoremods.add(coreMod.getName());
}
}
catch (MalformedURLException e)
{
FMLRelaunchLog.log(Level.ERROR, e, "Unable to convert file into a URL. weird");
continue;
}
loadCoreMod(classLoader, fmlCorePlugin, coreMod);
}
}
private static Method ADDURL;
private static void handleCascadingTweak(File coreMod, JarFile jar, String cascadedTweaker, LaunchClassLoader classLoader, Integer sortingOrder)
{
try
{
// Have to manually stuff the tweaker into the parent classloader
if (ADDURL == null)
{
ADDURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
ADDURL.setAccessible(true);
}
ADDURL.invoke(classLoader.getClass().getClassLoader(), coreMod.toURI().toURL());
classLoader.addURL(coreMod.toURI().toURL());
CoreModManager.tweaker.injectCascadingTweak(cascadedTweaker);
tweakSorting.put(cascadedTweaker,sortingOrder);
}
catch (Exception e)
{
FMLRelaunchLog.log(Level.INFO, e, "There was a problem trying to load the mod dir tweaker %s", coreMod.getAbsolutePath());
}
}
/**
* @param mcDir
* the minecraft home directory
* @return the coremod directory
*/
private static File setupCoreModDir(File mcDir)
{
File coreModDir = new File(mcDir, "mods");
try
{
coreModDir = coreModDir.getCanonicalFile();
}
catch (IOException e)
{
throw new RuntimeException(String.format("Unable to canonicalize the coremod dir at %s", mcDir.getName()), e);
}
if (!coreModDir.exists())
{
coreModDir.mkdir();
}
else if (coreModDir.exists() && !coreModDir.isDirectory())
{
throw new RuntimeException(String.format("Found a coremod file in %s that's not a directory", mcDir.getName()));
}
return coreModDir;
}
public static List<String> getLoadedCoremods()
{
return loadedCoremods;
}
public static Map<String, List<String>> getTransformers()
{
return transformers;
}
public static List<String> getReparseableCoremods()
{
return reparsedCoremods;
}
private static FMLPluginWrapper loadCoreMod(LaunchClassLoader classLoader, String coreModClass, File location)
{
String coreModName = coreModClass.substring(coreModClass.lastIndexOf('.') + 1);
try
{
FMLRelaunchLog.fine("Instantiating coremod class %s", coreModName);
classLoader.addTransformerExclusion(coreModClass);
Class<?> coreModClazz = Class.forName(coreModClass, true, classLoader);
Name coreModNameAnn = coreModClazz.getAnnotation(IFMLLoadingPlugin.Name.class);
if (coreModNameAnn != null && !Strings.isNullOrEmpty(coreModNameAnn.value()))
{
coreModName = coreModNameAnn.value();
FMLRelaunchLog.finer("coremod named %s is loading", coreModName);
}
MCVersion requiredMCVersion = coreModClazz.getAnnotation(IFMLLoadingPlugin.MCVersion.class);
if (!Arrays.asList(rootPlugins).contains(coreModClass) && (requiredMCVersion == null || Strings.isNullOrEmpty(requiredMCVersion.value())))
{
FMLRelaunchLog.log(Level.WARN, "The coremod %s does not have a MCVersion annotation, it may cause issues with this version of Minecraft",
coreModClass);
}
else if (requiredMCVersion != null && !FMLInjectionData.mccversion.equals(requiredMCVersion.value()))
{
FMLRelaunchLog.log(Level.ERROR, "The coremod %s is requesting minecraft version %s and minecraft is %s. It will be ignored.", coreModClass,
requiredMCVersion.value(), FMLInjectionData.mccversion);
return null;
}
else if (requiredMCVersion != null)
{
FMLRelaunchLog.log(Level.DEBUG, "The coremod %s requested minecraft version %s and minecraft is %s. It will be loaded.", coreModClass,
requiredMCVersion.value(), FMLInjectionData.mccversion);
}
TransformerExclusions trExclusions = coreModClazz.getAnnotation(IFMLLoadingPlugin.TransformerExclusions.class);
if (trExclusions != null)
{
for (String st : trExclusions.value())
{
classLoader.addTransformerExclusion(st);
}
}
DependsOn deplist = coreModClazz.getAnnotation(IFMLLoadingPlugin.DependsOn.class);
String[] dependencies = new String[0];
if (deplist != null)
{
dependencies = deplist.value();
}
SortingIndex index = coreModClazz.getAnnotation(IFMLLoadingPlugin.SortingIndex.class);
int sortIndex = index != null ? index.value() : 0;
IFMLLoadingPlugin plugin = (IFMLLoadingPlugin) coreModClazz.newInstance();
String accessTransformerClass = plugin.getAccessTransformerClass();
if (accessTransformerClass != null)
{
FMLRelaunchLog.log(Level.DEBUG, "Added access transformer class %s to enqueued access transformers", accessTransformerClass);
accessTransformers.add(accessTransformerClass);
}
FMLPluginWrapper wrap = new FMLPluginWrapper(coreModName, plugin, location, sortIndex, dependencies);
loadPlugins.add(wrap);
FMLRelaunchLog.fine("Enqueued coremod %s", coreModName);
return wrap;
}
catch (ClassNotFoundException cnfe)
{
if (!Lists.newArrayList(rootPlugins).contains(coreModClass))
FMLRelaunchLog.log(Level.ERROR, cnfe, "Coremod %s: Unable to class load the plugin %s", coreModName, coreModClass);
else
FMLRelaunchLog.fine("Skipping root plugin %s", coreModClass);
}
catch (ClassCastException cce)
{
FMLRelaunchLog.log(Level.ERROR, cce, "Coremod %s: The plugin %s is not an implementor of IFMLLoadingPlugin", coreModName, coreModClass);
}
catch (InstantiationException ie)
{
FMLRelaunchLog.log(Level.ERROR, ie, "Coremod %s: The plugin class %s was not instantiable", coreModName, coreModClass);
}
catch (IllegalAccessException iae)
{
FMLRelaunchLog.log(Level.ERROR, iae, "Coremod %s: The plugin class %s was not accessible", coreModName, coreModClass);
}
return null;
}
private static void sortCoreMods()
{
TopologicalSort.DirectedGraph<FMLPluginWrapper> sortGraph = new TopologicalSort.DirectedGraph<FMLPluginWrapper>();
Map<String, FMLPluginWrapper> pluginMap = Maps.newHashMap();
for (FMLPluginWrapper plug : loadPlugins)
{
sortGraph.addNode(plug);
pluginMap.put(plug.name, plug);
}
for (FMLPluginWrapper plug : loadPlugins)
{
for (String dep : plug.predepends)
{
if (!pluginMap.containsKey(dep))
{
FMLRelaunchLog.log(Level.ERROR, "Missing coremod dependency - the coremod %s depends on coremod %s which isn't present.", plug.name, dep);
throw new RuntimeException();
}
sortGraph.addEdge(plug, pluginMap.get(dep));
}
}
try
{
loadPlugins = TopologicalSort.topologicalSort(sortGraph);
FMLRelaunchLog.fine("Sorted coremod list %s", loadPlugins);
}
catch (Exception e)
{
FMLLog.log(Level.ERROR, e, "There was a problem performing the coremod sort");
throw Throwables.propagate(e);
}
}
public static void injectTransformers(LaunchClassLoader classLoader)
{
Launch.blackboard.put("fml.deobfuscatedEnvironment", deobfuscatedEnvironment);
tweaker.injectCascadingTweak("net.minecraftforge.fml.common.launcher.FMLDeobfTweaker");
tweakSorting.put("net.minecraftforge.fml.common.launcher.FMLDeobfTweaker", Integer.valueOf(1000));
}
public static void injectCoreModTweaks(FMLInjectionAndSortingTweaker fmlInjectionAndSortingTweaker)
{
@SuppressWarnings("unchecked")
List<ITweaker> tweakers = (List<ITweaker>) Launch.blackboard.get("Tweaks");
// Add the sorting tweaker first- it'll appear twice in the list
tweakers.add(0, fmlInjectionAndSortingTweaker);
for (FMLPluginWrapper wrapper : loadPlugins)
{
tweakers.add(wrapper);
}
}
private static Map<String,Integer> tweakSorting = Maps.newHashMap();
public static void sortTweakList()
{
@SuppressWarnings("unchecked")
List<ITweaker> tweakers = (List<ITweaker>) Launch.blackboard.get("Tweaks");
// Basically a copy of Collections.sort pre 8u20, optimized as we know we're an array list.
// Thanks unhelpful fixer of http://bugs.java.com/view_bug.do?bug_id=8032636
ITweaker[] toSort = tweakers.toArray(new ITweaker[tweakers.size()]);
Arrays.sort(toSort, new Comparator<ITweaker>() {
@Override
public int compare(ITweaker o1, ITweaker o2)
{
Integer first = null;
Integer second = null;
if (o1 instanceof FMLInjectionAndSortingTweaker)
{
first = Integer.MIN_VALUE;
}
if (o2 instanceof FMLInjectionAndSortingTweaker)
{
second = Integer.MIN_VALUE;
}
if (o1 instanceof FMLPluginWrapper)
{
first = ((FMLPluginWrapper) o1).sortIndex;
}
else if (first == null)
{
first = tweakSorting.get(o1.getClass().getName());
}
if (o2 instanceof FMLPluginWrapper)
{
second = ((FMLPluginWrapper) o2).sortIndex;
}
else if (second == null)
{
second = tweakSorting.get(o2.getClass().getName());
}
if (first == null)
{
first = 0;
}
if (second == null)
{
second = 0;
}
return Ints.saturatedCast((long)first - (long)second);
}
});
// Basically a copy of Collections.sort, optimized as we know we're an array list.
// Thanks unhelpful fixer of http://bugs.java.com/view_bug.do?bug_id=8032636
for (int j = 0; j < toSort.length; j++) {
tweakers.set(j, toSort[j]);
}
}
public static List<String> getAccessTransformers()
{
return accessTransformers;
}
public static void onCrash(StringBuilder builder)
{
if(!loadedCoremods.isEmpty() || !reparsedCoremods.isEmpty())
{
builder.append("\nWARNING: coremods are present:\n");
for(String coreMod : transformers.keySet())
{
builder.append(" ").append(coreMod).append('\n');
}
builder.append("Contact their authors BEFORE contacting forge\n\n");
}
}
}