001/*
002 * The FML Forge Mod Loader suite.
003 * Copyright (C) 2012 cpw
004 *
005 * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
006 * Software Foundation; either version 2.1 of the License, or any later version.
007 *
008 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
009 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
010 *
011 * You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51
012 * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
013 */
014package cpw.mods.fml.common;
015
016import java.io.File;
017import java.io.FileReader;
018import java.io.IOException;
019import java.net.MalformedURLException;
020import java.util.Comparator;
021import java.util.HashSet;
022import java.util.List;
023import java.util.Map;
024import java.util.Properties;
025import java.util.Set;
026import java.util.concurrent.Callable;
027import java.util.logging.Level;
028
029import net.minecraft.crash.CallableMinecraftVersion;
030import net.minecraft.server.MinecraftServer;
031
032import com.google.common.base.CharMatcher;
033import com.google.common.base.Function;
034import com.google.common.base.Joiner;
035import com.google.common.base.Splitter;
036import com.google.common.collect.ArrayListMultimap;
037import com.google.common.collect.BiMap;
038import com.google.common.collect.HashBiMap;
039import com.google.common.collect.ImmutableList;
040import com.google.common.collect.ImmutableMap;
041import com.google.common.collect.ImmutableMultiset;
042import com.google.common.collect.Iterables;
043import com.google.common.collect.LinkedHashMultimap;
044import com.google.common.collect.Lists;
045import com.google.common.collect.Maps;
046import com.google.common.collect.SetMultimap;
047import com.google.common.collect.Sets;
048import com.google.common.collect.Multiset.Entry;
049import com.google.common.collect.Multisets;
050import com.google.common.collect.Ordering;
051import com.google.common.collect.Sets.SetView;
052import com.google.common.collect.TreeMultimap;
053
054import cpw.mods.fml.common.LoaderState.ModState;
055import cpw.mods.fml.common.discovery.ModDiscoverer;
056import cpw.mods.fml.common.event.FMLInterModComms;
057import cpw.mods.fml.common.event.FMLLoadEvent;
058import cpw.mods.fml.common.functions.ModIdFunction;
059import cpw.mods.fml.common.modloader.BaseModProxy;
060import cpw.mods.fml.common.toposort.ModSorter;
061import cpw.mods.fml.common.toposort.ModSortingException;
062import cpw.mods.fml.common.toposort.TopologicalSort;
063import cpw.mods.fml.common.versioning.ArtifactVersion;
064import cpw.mods.fml.common.versioning.VersionParser;
065
066/**
067 * The loader class performs the actual loading of the mod code from disk.
068 *
069 * <p>
070 * There are several {@link LoaderState}s to mod loading, triggered in two
071 * different stages from the FML handler code's hooks into the minecraft code.
072 * </p>
073 *
074 * <ol>
075 * <li>LOADING. Scanning the filesystem for mod containers to load (zips, jars,
076 * directories), adding them to the {@link #modClassLoader} Scanning, the loaded
077 * containers for mod classes to load and registering them appropriately.</li>
078 * <li>PREINIT. The mod classes are configured, they are sorted into a load
079 * order, and instances of the mods are constructed.</li>
080 * <li>INIT. The mod instances are initialized. For BaseMod mods, this involves
081 * calling the load method.</li>
082 * <li>POSTINIT. The mod instances are post initialized. For BaseMod mods this
083 * involves calling the modsLoaded method.</li>
084 * <li>UP. The Loader is complete</li>
085 * <li>ERRORED. The loader encountered an error during the LOADING phase and
086 * dropped to this state instead. It will not complete loading from this state,
087 * but it attempts to continue loading before abandoning and giving a fatal
088 * error.</li>
089 * </ol>
090 *
091 * Phase 1 code triggers the LOADING and PREINIT states. Phase 2 code triggers
092 * the INIT and POSTINIT states.
093 *
094 * @author cpw
095 *
096 */
097public class Loader
098{
099    private static final Splitter DEPENDENCYPARTSPLITTER = Splitter.on(":").omitEmptyStrings().trimResults();
100    private static final Splitter DEPENDENCYSPLITTER = Splitter.on(";").omitEmptyStrings().trimResults();
101    /**
102     * The singleton instance
103     */
104    private static Loader instance;
105    /**
106     * Build information for tracking purposes.
107     */
108    private static String major;
109    private static String minor;
110    private static String rev;
111    private static String build;
112    private static String mccversion;
113    private static String mcpversion;
114
115    /**
116     * The class loader we load the mods into.
117     */
118    private ModClassLoader modClassLoader;
119    /**
120     * The sorted list of mods.
121     */
122    private List<ModContainer> mods;
123    /**
124     * A named list of mods
125     */
126    private Map<String, ModContainer> namedMods;
127    /**
128     * The canonical configuration directory
129     */
130    private File canonicalConfigDir;
131    /**
132     * The canonical minecraft directory
133     */
134    private File canonicalMinecraftDir;
135    /**
136     * The captured error
137     */
138    private Exception capturedError;
139    private File canonicalModsDir;
140    private LoadController modController;
141    private MinecraftDummyContainer minecraft;
142    private MCPDummyContainer mcp;
143
144    private static File minecraftDir;
145    private static List<String> injectedContainers;
146
147    public static Loader instance()
148    {
149        if (instance == null)
150        {
151            instance = new Loader();
152        }
153
154        return instance;
155    }
156
157    public static void injectData(Object... data)
158    {
159        major = (String) data[0];
160        minor = (String) data[1];
161        rev = (String) data[2];
162        build = (String) data[3];
163        mccversion = (String) data[4];
164        mcpversion = (String) data[5];
165        minecraftDir = (File) data[6];
166        injectedContainers = (List<String>)data[7];
167    }
168
169    private Loader()
170    {
171        modClassLoader = new ModClassLoader(getClass().getClassLoader());
172        String actualMCVersion = new CallableMinecraftVersion(null).minecraftVersion();
173        if (!mccversion.equals(actualMCVersion))
174        {
175            FMLLog.severe("This version of FML is built for Minecraft %s, we have detected Minecraft %s in your minecraft jar file", mccversion, actualMCVersion);
176            throw new LoaderException();
177        }
178
179        minecraft = new MinecraftDummyContainer(actualMCVersion);
180        mcp = new MCPDummyContainer(MetadataCollection.from(getClass().getResourceAsStream("/mcpmod.info"), "MCP").getMetadataForId("mcp", null));
181    }
182
183    /**
184     * Sort the mods into a sorted list, using dependency information from the
185     * containers. The sorting is performed using a {@link TopologicalSort}
186     * based on the pre- and post- dependency information provided by the mods.
187     */
188    private void sortModList()
189    {
190        FMLLog.fine("Verifying mod requirements are satisfied");
191        try
192        {
193            BiMap<String, ArtifactVersion> modVersions = HashBiMap.create();
194            for (ModContainer mod : getActiveModList())
195            {
196                modVersions.put(mod.getModId(), mod.getProcessedVersion());
197            }
198
199            for (ModContainer mod : getActiveModList())
200            {
201                if (!mod.acceptableMinecraftVersionRange().containsVersion(minecraft.getProcessedVersion()))
202                {
203                    FMLLog.severe("The mod %s does not wish to run in Minecraft version %s. You will have to remove it to play.", mod.getModId(), getMCVersionString());
204                    throw new WrongMinecraftVersionException(mod);
205                }
206                Map<String,ArtifactVersion> names = Maps.uniqueIndex(mod.getRequirements(), new Function<ArtifactVersion, String>()
207                {
208                    public String apply(ArtifactVersion v)
209                    {
210                        return v.getLabel();
211                    }
212                });
213                Set<ArtifactVersion> versionMissingMods = Sets.newHashSet();
214                Set<String> missingMods = Sets.difference(names.keySet(), modVersions.keySet());
215                if (!missingMods.isEmpty())
216                {
217                    FMLLog.severe("The mod %s (%s) requires mods %s to be available", mod.getModId(), mod.getName(), missingMods);
218                    for (String modid : missingMods)
219                    {
220                        versionMissingMods.add(names.get(modid));
221                    }
222                    throw new MissingModsException(versionMissingMods);
223                }
224                ImmutableList<ArtifactVersion> allDeps = ImmutableList.<ArtifactVersion>builder().addAll(mod.getDependants()).addAll(mod.getDependencies()).build();
225                for (ArtifactVersion v : allDeps)
226                {
227                    if (modVersions.containsKey(v.getLabel()))
228                    {
229                        if (!v.containsVersion(modVersions.get(v.getLabel())))
230                        {
231                            versionMissingMods.add(v);
232                        }
233                    }
234                }
235                if (!versionMissingMods.isEmpty())
236                {
237                    FMLLog.severe("The mod %s (%s) requires mod versions %s to be available", mod.getModId(), mod.getName(), versionMissingMods);
238                    throw new MissingModsException(versionMissingMods);
239                }
240            }
241
242            FMLLog.fine("All mod requirements are satisfied");
243
244            ModSorter sorter = new ModSorter(getActiveModList(), namedMods);
245
246            try
247            {
248                FMLLog.fine("Sorting mods into an ordered list");
249                List<ModContainer> sortedMods = sorter.sort();
250                // Reset active list to the sorted list
251                modController.getActiveModList().clear();
252                modController.getActiveModList().addAll(sortedMods);
253                // And inject the sorted list into the overall list
254                mods.removeAll(sortedMods);
255                sortedMods.addAll(mods);
256                mods = sortedMods;
257                FMLLog.fine("Mod sorting completed successfully");
258            }
259            catch (ModSortingException sortException)
260            {
261                FMLLog.severe("A dependency cycle was detected in the input mod set so an ordering cannot be determined");
262                FMLLog.severe("The visited mod list is %s", sortException.getExceptionData().getVisitedNodes());
263                FMLLog.severe("The first mod in the cycle is %s", sortException.getExceptionData().getFirstBadNode());
264                FMLLog.log(Level.SEVERE, sortException, "The full error");
265                throw new LoaderException(sortException);
266            }
267        }
268        finally
269        {
270            FMLLog.fine("Mod sorting data:");
271            for (ModContainer mod : getActiveModList())
272            {
273                if (!mod.isImmutable())
274                {
275                    FMLLog.fine("\t%s(%s:%s): %s (%s)", mod.getModId(), mod.getName(), mod.getVersion(), mod.getSource().getName(), mod.getSortingRules());
276                }
277            }
278            if (mods.size()==0)
279            {
280                FMLLog.fine("No mods found to sort");
281            }
282        }
283
284    }
285
286    /**
287     * The primary loading code
288     *
289     * This is visited during first initialization by Minecraft to scan and load
290     * the mods from all sources 1. The minecraft jar itself (for loading of in
291     * jar mods- I would like to remove this if possible but forge depends on it
292     * at present) 2. The mods directory with expanded subdirs, searching for
293     * mods named mod_*.class 3. The mods directory for zip and jar files,
294     * searching for mod classes named mod_*.class again
295     *
296     * The found resources are first loaded into the {@link #modClassLoader}
297     * (always) then scanned for class resources matching the specification
298     * above.
299     *
300     * If they provide the {@link Mod} annotation, they will be loaded as
301     * "FML mods", which currently is effectively a NO-OP. If they are
302     * determined to be {@link BaseModProxy} subclasses they are loaded as such.
303     *
304     * Finally, if they are successfully loaded as classes, they are then added
305     * to the available mod list.
306     */
307    private ModDiscoverer identifyMods()
308    {
309        FMLLog.fine("Building injected Mod Containers %s", injectedContainers);
310        // Add in the MCP mod container
311        mods.add(new InjectedModContainer(mcp,new File("minecraft.jar")));
312        File coremod = new File(minecraftDir,"coremods");
313        for (String cont : injectedContainers)
314        {
315            ModContainer mc;
316            try
317            {
318                mc = (ModContainer) Class.forName(cont,true,modClassLoader).newInstance();
319            }
320            catch (Exception e)
321            {
322                FMLLog.log(Level.SEVERE, e, "A problem occured instantiating the injected mod container %s", cont);
323                throw new LoaderException(e);
324            }
325            mods.add(new InjectedModContainer(mc,coremod));
326        }
327        ModDiscoverer discoverer = new ModDiscoverer();
328        FMLLog.fine("Attempting to load mods contained in the minecraft jar file and associated classes");
329        discoverer.findClasspathMods(modClassLoader);
330        FMLLog.fine("Minecraft jar mods loaded successfully");
331
332        FMLLog.info("Searching %s for mods", canonicalModsDir.getAbsolutePath());
333        discoverer.findModDirMods(canonicalModsDir);
334
335        mods.addAll(discoverer.identifyMods());
336        identifyDuplicates(mods);
337        namedMods = Maps.uniqueIndex(mods, new ModIdFunction());
338        FMLLog.info("Forge Mod Loader has identified %d mod%s to load", mods.size(), mods.size() != 1 ? "s" : "");
339        return discoverer;
340    }
341
342    private class ModIdComparator implements Comparator<ModContainer>
343    {
344        @Override
345        public int compare(ModContainer o1, ModContainer o2)
346        {
347            return o1.getModId().compareTo(o2.getModId());
348        }
349
350    }
351
352    private void identifyDuplicates(List<ModContainer> mods)
353    {
354        TreeMultimap<ModContainer, File> dupsearch = TreeMultimap.create(new ModIdComparator(), Ordering.arbitrary());
355        for (ModContainer mc : mods)
356        {
357            if (mc.getSource() != null)
358            {
359                dupsearch.put(mc, mc.getSource());
360            }
361        }
362
363        ImmutableMultiset<ModContainer> duplist = Multisets.copyHighestCountFirst(dupsearch.keys());
364        SetMultimap<ModContainer, File> dupes = LinkedHashMultimap.create();
365        for (Entry<ModContainer> e : duplist.entrySet())
366        {
367            if (e.getCount() > 1)
368            {
369                FMLLog.severe("Found a duplicate mod %s at %s", e.getElement().getModId(), dupsearch.get(e.getElement()));
370                dupes.putAll(e.getElement(),dupsearch.get(e.getElement()));
371            }
372        }
373        if (!dupes.isEmpty())
374        {
375            throw new DuplicateModsFoundException(dupes);
376        }
377    }
378
379    /**
380     * @return
381     */
382    private void initializeLoader()
383    {
384        File modsDir = new File(minecraftDir, "mods");
385        File configDir = new File(minecraftDir, "config");
386        String canonicalModsPath;
387        String canonicalConfigPath;
388
389        try
390        {
391            canonicalMinecraftDir = minecraftDir.getCanonicalFile();
392            canonicalModsPath = modsDir.getCanonicalPath();
393            canonicalConfigPath = configDir.getCanonicalPath();
394            canonicalConfigDir = configDir.getCanonicalFile();
395            canonicalModsDir = modsDir.getCanonicalFile();
396        }
397        catch (IOException ioe)
398        {
399            FMLLog.log(Level.SEVERE, ioe, "Failed to resolve loader directories: mods : %s ; config %s", canonicalModsDir.getAbsolutePath(),
400                            configDir.getAbsolutePath());
401            throw new LoaderException(ioe);
402        }
403
404        if (!canonicalModsDir.exists())
405        {
406            FMLLog.info("No mod directory found, creating one: %s", canonicalModsPath);
407            boolean dirMade = canonicalModsDir.mkdir();
408            if (!dirMade)
409            {
410                FMLLog.severe("Unable to create the mod directory %s", canonicalModsPath);
411                throw new LoaderException();
412            }
413            FMLLog.info("Mod directory created successfully");
414        }
415
416        if (!canonicalConfigDir.exists())
417        {
418            FMLLog.fine("No config directory found, creating one: %s", canonicalConfigPath);
419            boolean dirMade = canonicalConfigDir.mkdir();
420            if (!dirMade)
421            {
422                FMLLog.severe("Unable to create the config directory %s", canonicalConfigPath);
423                throw new LoaderException();
424            }
425            FMLLog.info("Config directory created successfully");
426        }
427
428        if (!canonicalModsDir.isDirectory())
429        {
430            FMLLog.severe("Attempting to load mods from %s, which is not a directory", canonicalModsPath);
431            throw new LoaderException();
432        }
433
434        if (!configDir.isDirectory())
435        {
436            FMLLog.severe("Attempting to load configuration from %s, which is not a directory", canonicalConfigPath);
437            throw new LoaderException();
438        }
439    }
440
441    public List<ModContainer> getModList()
442    {
443        return instance().mods != null ? ImmutableList.copyOf(instance().mods) : ImmutableList.<ModContainer>of();
444    }
445
446    /**
447     * Called from the hook to start mod loading. We trigger the
448     * {@link #identifyMods()} and Constructing, Preinitalization, and Initalization phases here. Finally,
449     * the mod list is frozen completely and is consider immutable from then on.
450     */
451    public void loadMods()
452    {
453        initializeLoader();
454        mods = Lists.newArrayList();
455        namedMods = Maps.newHashMap();
456        modController = new LoadController(this);
457        modController.transition(LoaderState.LOADING);
458        ModDiscoverer disc = identifyMods();
459        disableRequestedMods();
460        modController.distributeStateMessage(FMLLoadEvent.class);
461        sortModList();
462        mods = ImmutableList.copyOf(mods);
463        for (File nonMod : disc.getNonModLibs())
464        {
465            if (nonMod.isFile())
466            {
467                FMLLog.info("FML has found a non-mod file %s in your mods directory. It will now be injected into your classpath. This could severe stability issues, it should be removed if possible.", nonMod.getName());
468                try
469                {
470                    modClassLoader.addFile(nonMod);
471                }
472                catch (MalformedURLException e)
473                {
474                    FMLLog.log(Level.SEVERE, e, "Encountered a weird problem with non-mod file injection : %s", nonMod.getName());
475                }
476            }
477        }
478        modController.transition(LoaderState.CONSTRUCTING);
479        modController.distributeStateMessage(LoaderState.CONSTRUCTING, modClassLoader, disc.getASMTable());
480        FMLLog.fine("Mod signature data:");
481        for (ModContainer mod : getActiveModList())
482        {
483            FMLLog.fine("\t%s(%s:%s): %s (%s)", mod.getModId(), mod.getName(), mod.getVersion(), mod.getSource().getName(), CertificateHelper.getFingerprint(mod.getSigningCertificate()));
484        }
485        modController.transition(LoaderState.PREINITIALIZATION);
486        modController.distributeStateMessage(LoaderState.PREINITIALIZATION, disc.getASMTable(), canonicalConfigDir);
487        modController.transition(LoaderState.INITIALIZATION);
488    }
489
490    private void disableRequestedMods()
491    {
492        String forcedModList = System.getProperty("fml.modStates", "");
493        FMLLog.fine("Received a system property request \'%s\'",forcedModList);
494        Map<String, String> sysPropertyStateList = Splitter.on(CharMatcher.anyOf(";:"))
495                .omitEmptyStrings().trimResults().withKeyValueSeparator("=")
496                .split(forcedModList);
497        FMLLog.fine("System property request managing the state of %d mods", sysPropertyStateList.size());
498        Map<String, String> modStates = Maps.newHashMap();
499
500        File forcedModFile = new File(canonicalConfigDir, "fmlModState.properties");
501        Properties forcedModListProperties = new Properties();
502        if (forcedModFile.exists() && forcedModFile.isFile())
503        {
504            FMLLog.fine("Found a mod state file %s", forcedModFile.getName());
505            try
506            {
507                forcedModListProperties.load(new FileReader(forcedModFile));
508                FMLLog.fine("Loaded states for %d mods from file", forcedModListProperties.size());
509            }
510            catch (Exception e)
511            {
512                FMLLog.log(Level.INFO, e, "An error occurred reading the fmlModState.properties file");
513            }
514        }
515        modStates.putAll(Maps.fromProperties(forcedModListProperties));
516        modStates.putAll(sysPropertyStateList);
517        FMLLog.fine("After merging, found state information for %d mods", modStates.size());
518
519        Map<String, Boolean> isEnabled = Maps.transformValues(modStates, new Function<String, Boolean>()
520        {
521            public Boolean apply(String input)
522            {
523                return Boolean.parseBoolean(input);
524            }
525        });
526
527        for (Map.Entry<String, Boolean> entry : isEnabled.entrySet())
528        {
529            if (namedMods.containsKey(entry.getKey()))
530            {
531                FMLLog.info("Setting mod %s to enabled state %b", entry.getKey(), entry.getValue());
532                namedMods.get(entry.getKey()).setEnabledState(entry.getValue());
533            }
534        }
535    }
536
537    /**
538     * Query if we know of a mod named modname
539     *
540     * @param modname
541     * @return If the mod is loaded
542     */
543    public static boolean isModLoaded(String modname)
544    {
545        return instance().namedMods.containsKey(modname) && instance().modController.getModState(instance.namedMods.get(modname))!=ModState.DISABLED;
546    }
547
548    public File getConfigDir()
549    {
550        return canonicalConfigDir;
551    }
552
553    public String getCrashInformation()
554    {
555        StringBuilder ret = new StringBuilder();
556        List<String> branding = FMLCommonHandler.instance().getBrandings();
557
558        Joiner.on(' ').skipNulls().appendTo(ret, branding.subList(1, branding.size()));
559        if (modController!=null)
560        {
561            modController.printModStates(ret);
562        }
563        return ret.toString();
564    }
565
566    public String getFMLVersionString()
567    {
568        return String.format("%s.%s.%s.%s", major, minor, rev, build);
569    }
570
571    public ClassLoader getModClassLoader()
572    {
573        return modClassLoader;
574    }
575
576    public void computeDependencies(String dependencyString, Set<ArtifactVersion> requirements, List<ArtifactVersion> dependencies, List<ArtifactVersion> dependants)
577    {
578        if (dependencyString == null || dependencyString.length() == 0)
579        {
580            return;
581        }
582
583        boolean parseFailure=false;
584
585        for (String dep : DEPENDENCYSPLITTER.split(dependencyString))
586        {
587            List<String> depparts = Lists.newArrayList(DEPENDENCYPARTSPLITTER.split(dep));
588            // Need two parts to the string
589            if (depparts.size() != 2)
590            {
591                parseFailure=true;
592                continue;
593            }
594            String instruction = depparts.get(0);
595            String target = depparts.get(1);
596            boolean targetIsAll = target.startsWith("*");
597
598            // Cannot have an "all" relationship with anything except pure *
599            if (targetIsAll && target.length()>1)
600            {
601                parseFailure = true;
602                continue;
603            }
604
605            // If this is a required element, add it to the required list
606            if ("required-before".equals(instruction) || "required-after".equals(instruction))
607            {
608                // You can't require everything
609                if (!targetIsAll)
610                {
611                    requirements.add(VersionParser.parseVersionReference(target));
612                }
613                else
614                {
615                    parseFailure=true;
616                    continue;
617                }
618            }
619
620            // You cannot have a versioned dependency on everything
621            if (targetIsAll && target.indexOf('@')>-1)
622            {
623                parseFailure = true;
624                continue;
625            }
626            // before elements are things we are loaded before (so they are our dependants)
627            if ("required-before".equals(instruction) || "before".equals(instruction))
628            {
629                dependants.add(VersionParser.parseVersionReference(target));
630            }
631            // after elements are things that load before we do (so they are out dependencies)
632            else if ("required-after".equals(instruction) || "after".equals(instruction))
633            {
634                dependencies.add(VersionParser.parseVersionReference(target));
635            }
636            else
637            {
638                parseFailure=true;
639            }
640        }
641
642        if (parseFailure)
643        {
644            FMLLog.log(Level.WARNING, "Unable to parse dependency string %s", dependencyString);
645            throw new LoaderException();
646        }
647    }
648
649    public Map<String,ModContainer> getIndexedModList()
650    {
651        return ImmutableMap.copyOf(namedMods);
652    }
653
654    public void initializeMods()
655    {
656        // Mod controller should be in the initialization state here
657        modController.distributeStateMessage(LoaderState.INITIALIZATION);
658        modController.transition(LoaderState.POSTINITIALIZATION);
659        modController.distributeStateMessage(FMLInterModComms.IMCEvent.class);
660        modController.distributeStateMessage(LoaderState.POSTINITIALIZATION);
661        modController.transition(LoaderState.AVAILABLE);
662        modController.distributeStateMessage(LoaderState.AVAILABLE);
663        FMLLog.info("Forge Mod Loader has successfully loaded %d mod%s", mods.size(), mods.size()==1 ? "" : "s");
664    }
665
666    public ICrashCallable getCallableCrashInformation()
667    {
668        return new ICrashCallable() {
669            @Override
670            public String call() throws Exception
671            {
672                return getCrashInformation();
673            }
674
675            @Override
676            public String getLabel()
677            {
678                return "FML";
679            }
680        };
681    }
682
683    public List<ModContainer> getActiveModList()
684    {
685        return modController != null ? modController.getActiveModList() : ImmutableList.<ModContainer>of();
686    }
687
688    public ModState getModState(ModContainer selectedMod)
689    {
690        return modController.getModState(selectedMod);
691    }
692
693    public String getMCVersionString()
694    {
695        return "Minecraft " + mccversion;
696    }
697
698    public void serverStarting(Object server)
699    {
700        modController.distributeStateMessage(LoaderState.SERVER_STARTING, server);
701        modController.transition(LoaderState.SERVER_STARTING);
702    }
703
704    public void serverStarted()
705    {
706        modController.distributeStateMessage(LoaderState.SERVER_STARTED);
707        modController.transition(LoaderState.SERVER_STARTED);
708    }
709
710    public void serverStopping()
711    {
712        modController.distributeStateMessage(LoaderState.SERVER_STOPPING);
713        modController.transition(LoaderState.SERVER_STOPPING);
714    }
715
716    public BiMap<ModContainer, Object> getModObjectList()
717    {
718        return modController.getModObjectList();
719    }
720
721    public BiMap<Object, ModContainer> getReversedModObjectList()
722    {
723        return getModObjectList().inverse();
724    }
725
726    public ModContainer activeModContainer()
727    {
728        return modController != null ? modController.activeContainer() : null;
729    }
730
731    public boolean isInState(LoaderState state)
732    {
733        return modController.isInState(state);
734    }
735
736    public MinecraftDummyContainer getMinecraftModContainer()
737    {
738        return minecraft;
739    }
740
741    public boolean hasReachedState(LoaderState state) {
742        return modController != null ? modController.hasReachedState(state) : false;
743    }
744
745    public String getMCPVersionString() {
746        return String.format("MCP v%s", mcpversion);
747    }
748
749    public void serverStopped()
750    {
751        modController.distributeStateMessage(LoaderState.SERVER_STOPPED);
752        modController.transition(LoaderState.SERVER_STOPPED);
753        modController.transition(LoaderState.AVAILABLE);
754    }
755}