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;
030
031import com.google.common.base.CharMatcher;
032import com.google.common.base.Function;
033import com.google.common.base.Joiner;
034import com.google.common.base.Splitter;
035import com.google.common.collect.ArrayListMultimap;
036import com.google.common.collect.BiMap;
037import com.google.common.collect.HashBiMap;
038import com.google.common.collect.ImmutableList;
039import com.google.common.collect.ImmutableMap;
040import com.google.common.collect.ImmutableMultiset;
041import com.google.common.collect.Iterables;
042import com.google.common.collect.LinkedHashMultimap;
043import com.google.common.collect.Lists;
044import com.google.common.collect.Maps;
045import com.google.common.collect.SetMultimap;
046import com.google.common.collect.Sets;
047import com.google.common.collect.Multiset.Entry;
048import com.google.common.collect.Multisets;
049import com.google.common.collect.Ordering;
050import com.google.common.collect.Sets.SetView;
051import com.google.common.collect.TreeMultimap;
052
053import cpw.mods.fml.common.LoaderState.ModState;
054import cpw.mods.fml.common.discovery.ModDiscoverer;
055import cpw.mods.fml.common.event.FMLInterModComms;
056import cpw.mods.fml.common.event.FMLLoadEvent;
057import cpw.mods.fml.common.functions.ModIdFunction;
058import cpw.mods.fml.common.modloader.BaseModProxy;
059import cpw.mods.fml.common.toposort.ModSorter;
060import cpw.mods.fml.common.toposort.ModSortingException;
061import cpw.mods.fml.common.toposort.TopologicalSort;
062import cpw.mods.fml.common.versioning.ArtifactVersion;
063import cpw.mods.fml.common.versioning.VersionParser;
064import cpw.mods.fml.relauncher.FMLRelaunchLog;
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    private File loggingProperties;
147
148    public static Loader instance()
149    {
150        if (instance == null)
151        {
152            instance = new Loader();
153        }
154
155        return instance;
156    }
157
158    public static void injectData(Object... data)
159    {
160        major = (String) data[0];
161        minor = (String) data[1];
162        rev = (String) data[2];
163        build = (String) data[3];
164        mccversion = (String) data[4];
165        mcpversion = (String) data[5];
166        minecraftDir = (File) data[6];
167        injectedContainers = (List<String>)data[7];
168    }
169
170    private Loader()
171    {
172        modClassLoader = new ModClassLoader(getClass().getClassLoader());
173        String actualMCVersion = new CallableMinecraftVersion(null).minecraftVersion();
174        if (!mccversion.equals(actualMCVersion))
175        {
176            FMLLog.severe("This version of FML is built for Minecraft %s, we have detected Minecraft %s in your minecraft jar file", mccversion, actualMCVersion);
177            throw new LoaderException();
178        }
179
180        minecraft = new MinecraftDummyContainer(actualMCVersion);
181        mcp = new MCPDummyContainer(MetadataCollection.from(getClass().getResourceAsStream("/mcpmod.info"), "MCP").getMetadataForId("mcp", null));
182    }
183
184    /**
185     * Sort the mods into a sorted list, using dependency information from the
186     * containers. The sorting is performed using a {@link TopologicalSort}
187     * based on the pre- and post- dependency information provided by the mods.
188     */
189    private void sortModList()
190    {
191        FMLLog.finer("Verifying mod requirements are satisfied");
192        try
193        {
194            BiMap<String, ArtifactVersion> modVersions = HashBiMap.create();
195            for (ModContainer mod : getActiveModList())
196            {
197                modVersions.put(mod.getModId(), mod.getProcessedVersion());
198            }
199
200            for (ModContainer mod : getActiveModList())
201            {
202                if (!mod.acceptableMinecraftVersionRange().containsVersion(minecraft.getProcessedVersion()))
203                {
204                    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());
205                    throw new WrongMinecraftVersionException(mod);
206                }
207                Map<String,ArtifactVersion> names = Maps.uniqueIndex(mod.getRequirements(), new Function<ArtifactVersion, String>()
208                {
209                    public String apply(ArtifactVersion v)
210                    {
211                        return v.getLabel();
212                    }
213                });
214                Set<ArtifactVersion> versionMissingMods = Sets.newHashSet();
215                Set<String> missingMods = Sets.difference(names.keySet(), modVersions.keySet());
216                if (!missingMods.isEmpty())
217                {
218                    FMLLog.severe("The mod %s (%s) requires mods %s to be available", mod.getModId(), mod.getName(), missingMods);
219                    for (String modid : missingMods)
220                    {
221                        versionMissingMods.add(names.get(modid));
222                    }
223                    throw new MissingModsException(versionMissingMods);
224                }
225                ImmutableList<ArtifactVersion> allDeps = ImmutableList.<ArtifactVersion>builder().addAll(mod.getDependants()).addAll(mod.getDependencies()).build();
226                for (ArtifactVersion v : allDeps)
227                {
228                    if (modVersions.containsKey(v.getLabel()))
229                    {
230                        if (!v.containsVersion(modVersions.get(v.getLabel())))
231                        {
232                            versionMissingMods.add(v);
233                        }
234                    }
235                }
236                if (!versionMissingMods.isEmpty())
237                {
238                    FMLLog.severe("The mod %s (%s) requires mod versions %s to be available", mod.getModId(), mod.getName(), versionMissingMods);
239                    throw new MissingModsException(versionMissingMods);
240                }
241            }
242
243            FMLLog.finer("All mod requirements are satisfied");
244
245            ModSorter sorter = new ModSorter(getActiveModList(), namedMods);
246
247            try
248            {
249                FMLLog.finer("Sorting mods into an ordered list");
250                List<ModContainer> sortedMods = sorter.sort();
251                // Reset active list to the sorted list
252                modController.getActiveModList().clear();
253                modController.getActiveModList().addAll(sortedMods);
254                // And inject the sorted list into the overall list
255                mods.removeAll(sortedMods);
256                sortedMods.addAll(mods);
257                mods = sortedMods;
258                FMLLog.finer("Mod sorting completed successfully");
259            }
260            catch (ModSortingException sortException)
261            {
262                FMLLog.severe("A dependency cycle was detected in the input mod set so an ordering cannot be determined");
263                FMLLog.severe("The visited mod list is %s", sortException.getExceptionData().getVisitedNodes());
264                FMLLog.severe("The first mod in the cycle is %s", sortException.getExceptionData().getFirstBadNode());
265                FMLLog.log(Level.SEVERE, sortException, "The full error");
266                throw new LoaderException(sortException);
267            }
268        }
269        finally
270        {
271            FMLLog.fine("Mod sorting data");
272            int unprintedMods = mods.size();
273            for (ModContainer mod : getActiveModList())
274            {
275                if (!mod.isImmutable())
276                {
277                    FMLLog.fine("\t%s(%s:%s): %s (%s)", mod.getModId(), mod.getName(), mod.getVersion(), mod.getSource().getName(), mod.getSortingRules());
278                    unprintedMods--;
279                }
280            }
281            if (unprintedMods == mods.size())
282            {
283                FMLLog.fine("No user mods found to sort");
284            }
285        }
286
287    }
288
289    /**
290     * The primary loading code
291     *
292     * This is visited during first initialization by Minecraft to scan and load
293     * the mods from all sources 1. The minecraft jar itself (for loading of in
294     * jar mods- I would like to remove this if possible but forge depends on it
295     * at present) 2. The mods directory with expanded subdirs, searching for
296     * mods named mod_*.class 3. The mods directory for zip and jar files,
297     * searching for mod classes named mod_*.class again
298     *
299     * The found resources are first loaded into the {@link #modClassLoader}
300     * (always) then scanned for class resources matching the specification
301     * above.
302     *
303     * If they provide the {@link Mod} annotation, they will be loaded as
304     * "FML mods", which currently is effectively a NO-OP. If they are
305     * determined to be {@link BaseModProxy} subclasses they are loaded as such.
306     *
307     * Finally, if they are successfully loaded as classes, they are then added
308     * to the available mod list.
309     */
310    private ModDiscoverer identifyMods()
311    {
312        FMLLog.fine("Building injected Mod Containers %s", injectedContainers);
313        // Add in the MCP mod container
314        mods.add(new InjectedModContainer(mcp,new File("minecraft.jar")));
315        File coremod = new File(minecraftDir,"coremods");
316        for (String cont : injectedContainers)
317        {
318            ModContainer mc;
319            try
320            {
321                mc = (ModContainer) Class.forName(cont,true,modClassLoader).newInstance();
322            }
323            catch (Exception e)
324            {
325                FMLLog.log(Level.SEVERE, e, "A problem occured instantiating the injected mod container %s", cont);
326                throw new LoaderException(e);
327            }
328            mods.add(new InjectedModContainer(mc,coremod));
329        }
330        ModDiscoverer discoverer = new ModDiscoverer();
331        FMLLog.fine("Attempting to load mods contained in the minecraft jar file and associated classes");
332        discoverer.findClasspathMods(modClassLoader);
333        FMLLog.fine("Minecraft jar mods loaded successfully");
334
335        FMLLog.info("Searching %s for mods", canonicalModsDir.getAbsolutePath());
336        discoverer.findModDirMods(canonicalModsDir);
337
338        mods.addAll(discoverer.identifyMods());
339        identifyDuplicates(mods);
340        namedMods = Maps.uniqueIndex(mods, new ModIdFunction());
341        FMLLog.info("Forge Mod Loader has identified %d mod%s to load", mods.size(), mods.size() != 1 ? "s" : "");
342        for (String modId: namedMods.keySet())
343        {
344            FMLLog.makeLog(modId);
345        }
346        return discoverer;
347    }
348
349    private class ModIdComparator implements Comparator<ModContainer>
350    {
351        @Override
352        public int compare(ModContainer o1, ModContainer o2)
353        {
354            return o1.getModId().compareTo(o2.getModId());
355        }
356
357    }
358
359    private void identifyDuplicates(List<ModContainer> mods)
360    {
361        TreeMultimap<ModContainer, File> dupsearch = TreeMultimap.create(new ModIdComparator(), Ordering.arbitrary());
362        for (ModContainer mc : mods)
363        {
364            if (mc.getSource() != null)
365            {
366                dupsearch.put(mc, mc.getSource());
367            }
368        }
369
370        ImmutableMultiset<ModContainer> duplist = Multisets.copyHighestCountFirst(dupsearch.keys());
371        SetMultimap<ModContainer, File> dupes = LinkedHashMultimap.create();
372        for (Entry<ModContainer> e : duplist.entrySet())
373        {
374            if (e.getCount() > 1)
375            {
376                FMLLog.severe("Found a duplicate mod %s at %s", e.getElement().getModId(), dupsearch.get(e.getElement()));
377                dupes.putAll(e.getElement(),dupsearch.get(e.getElement()));
378            }
379        }
380        if (!dupes.isEmpty())
381        {
382            throw new DuplicateModsFoundException(dupes);
383        }
384    }
385
386    /**
387     *
388     */
389    private void initializeLoader()
390    {
391        File modsDir = new File(minecraftDir, "mods");
392        File configDir = new File(minecraftDir, "config");
393        String canonicalModsPath;
394        String canonicalConfigPath;
395
396        try
397        {
398            canonicalMinecraftDir = minecraftDir.getCanonicalFile();
399            canonicalModsPath = modsDir.getCanonicalPath();
400            canonicalConfigPath = configDir.getCanonicalPath();
401            canonicalConfigDir = configDir.getCanonicalFile();
402            canonicalModsDir = modsDir.getCanonicalFile();
403        }
404        catch (IOException ioe)
405        {
406            FMLLog.log(Level.SEVERE, ioe, "Failed to resolve loader directories: mods : %s ; config %s", canonicalModsDir.getAbsolutePath(),
407                            configDir.getAbsolutePath());
408            throw new LoaderException(ioe);
409        }
410
411        if (!canonicalModsDir.exists())
412        {
413            FMLLog.info("No mod directory found, creating one: %s", canonicalModsPath);
414            boolean dirMade = canonicalModsDir.mkdir();
415            if (!dirMade)
416            {
417                FMLLog.severe("Unable to create the mod directory %s", canonicalModsPath);
418                throw new LoaderException();
419            }
420            FMLLog.info("Mod directory created successfully");
421        }
422
423        if (!canonicalConfigDir.exists())
424        {
425            FMLLog.fine("No config directory found, creating one: %s", canonicalConfigPath);
426            boolean dirMade = canonicalConfigDir.mkdir();
427            if (!dirMade)
428            {
429                FMLLog.severe("Unable to create the config directory %s", canonicalConfigPath);
430                throw new LoaderException();
431            }
432            FMLLog.info("Config directory created successfully");
433        }
434
435        if (!canonicalModsDir.isDirectory())
436        {
437            FMLLog.severe("Attempting to load mods from %s, which is not a directory", canonicalModsPath);
438            throw new LoaderException();
439        }
440
441        if (!configDir.isDirectory())
442        {
443            FMLLog.severe("Attempting to load configuration from %s, which is not a directory", canonicalConfigPath);
444            throw new LoaderException();
445        }
446
447        loggingProperties = new File(canonicalConfigDir, "logging.properties");
448        FMLLog.info("Reading custom logging properties from %s", loggingProperties.getPath());
449        FMLRelaunchLog.loadLogConfiguration(loggingProperties);
450        FMLLog.log(Level.OFF,"Logging level for ForgeModLoader logging is set to %s", FMLRelaunchLog.log.getLogger().getLevel());
451    }
452
453    public List<ModContainer> getModList()
454    {
455        return instance().mods != null ? ImmutableList.copyOf(instance().mods) : ImmutableList.<ModContainer>of();
456    }
457
458    /**
459     * Called from the hook to start mod loading. We trigger the
460     * {@link #identifyMods()} and Constructing, Preinitalization, and Initalization phases here. Finally,
461     * the mod list is frozen completely and is consider immutable from then on.
462     */
463    public void loadMods()
464    {
465        initializeLoader();
466        mods = Lists.newArrayList();
467        namedMods = Maps.newHashMap();
468        modController = new LoadController(this);
469        modController.transition(LoaderState.LOADING);
470        ModDiscoverer disc = identifyMods();
471        disableRequestedMods();
472        FMLLog.fine("Reloading logging properties from %s", loggingProperties.getPath());
473        FMLRelaunchLog.loadLogConfiguration(loggingProperties);
474        FMLLog.fine("Reloaded logging properties");
475        modController.distributeStateMessage(FMLLoadEvent.class);
476        sortModList();
477        mods = ImmutableList.copyOf(mods);
478        for (File nonMod : disc.getNonModLibs())
479        {
480            if (nonMod.isFile())
481            {
482                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());
483                try
484                {
485                    modClassLoader.addFile(nonMod);
486                }
487                catch (MalformedURLException e)
488                {
489                    FMLLog.log(Level.SEVERE, e, "Encountered a weird problem with non-mod file injection : %s", nonMod.getName());
490                }
491            }
492        }
493        modController.transition(LoaderState.CONSTRUCTING);
494        modController.distributeStateMessage(LoaderState.CONSTRUCTING, modClassLoader, disc.getASMTable());
495        FMLLog.fine("Mod signature data");
496        for (ModContainer mod : getActiveModList())
497        {
498            FMLLog.fine("\t%s(%s:%s): %s (%s)", mod.getModId(), mod.getName(), mod.getVersion(), mod.getSource().getName(), CertificateHelper.getFingerprint(mod.getSigningCertificate()));
499        }
500        if (getActiveModList().isEmpty())
501        {
502            FMLLog.fine("No user mod signature data found");
503        }
504        modController.transition(LoaderState.PREINITIALIZATION);
505        modController.distributeStateMessage(LoaderState.PREINITIALIZATION, disc.getASMTable(), canonicalConfigDir);
506        modController.transition(LoaderState.INITIALIZATION);
507    }
508
509    private void disableRequestedMods()
510    {
511        String forcedModList = System.getProperty("fml.modStates", "");
512        FMLLog.finer("Received a system property request \'%s\'",forcedModList);
513        Map<String, String> sysPropertyStateList = Splitter.on(CharMatcher.anyOf(";:"))
514                .omitEmptyStrings().trimResults().withKeyValueSeparator("=")
515                .split(forcedModList);
516        FMLLog.finer("System property request managing the state of %d mods", sysPropertyStateList.size());
517        Map<String, String> modStates = Maps.newHashMap();
518
519        File forcedModFile = new File(canonicalConfigDir, "fmlModState.properties");
520        Properties forcedModListProperties = new Properties();
521        if (forcedModFile.exists() && forcedModFile.isFile())
522        {
523            FMLLog.finer("Found a mod state file %s", forcedModFile.getName());
524            try
525            {
526                forcedModListProperties.load(new FileReader(forcedModFile));
527                FMLLog.finer("Loaded states for %d mods from file", forcedModListProperties.size());
528            }
529            catch (Exception e)
530            {
531                FMLLog.log(Level.INFO, e, "An error occurred reading the fmlModState.properties file");
532            }
533        }
534        modStates.putAll(Maps.fromProperties(forcedModListProperties));
535        modStates.putAll(sysPropertyStateList);
536        FMLLog.fine("After merging, found state information for %d mods", modStates.size());
537
538        Map<String, Boolean> isEnabled = Maps.transformValues(modStates, new Function<String, Boolean>()
539        {
540            public Boolean apply(String input)
541            {
542                return Boolean.parseBoolean(input);
543            }
544        });
545
546        for (Map.Entry<String, Boolean> entry : isEnabled.entrySet())
547        {
548            if (namedMods.containsKey(entry.getKey()))
549            {
550                FMLLog.info("Setting mod %s to enabled state %b", entry.getKey(), entry.getValue());
551                namedMods.get(entry.getKey()).setEnabledState(entry.getValue());
552            }
553        }
554    }
555
556    /**
557     * Query if we know of a mod named modname
558     *
559     * @param modname
560     * @return If the mod is loaded
561     */
562    public static boolean isModLoaded(String modname)
563    {
564        return instance().namedMods.containsKey(modname) && instance().modController.getModState(instance.namedMods.get(modname))!=ModState.DISABLED;
565    }
566
567    public File getConfigDir()
568    {
569        return canonicalConfigDir;
570    }
571
572    public String getCrashInformation()
573    {
574        StringBuilder ret = new StringBuilder();
575        List<String> branding = FMLCommonHandler.instance().getBrandings();
576
577        Joiner.on(' ').skipNulls().appendTo(ret, branding.subList(1, branding.size()));
578        if (modController!=null)
579        {
580            modController.printModStates(ret);
581        }
582        return ret.toString();
583    }
584
585    public String getFMLVersionString()
586    {
587        return String.format("%s.%s.%s.%s", major, minor, rev, build);
588    }
589
590    public ClassLoader getModClassLoader()
591    {
592        return modClassLoader;
593    }
594
595    public void computeDependencies(String dependencyString, Set<ArtifactVersion> requirements, List<ArtifactVersion> dependencies, List<ArtifactVersion> dependants)
596    {
597        if (dependencyString == null || dependencyString.length() == 0)
598        {
599            return;
600        }
601
602        boolean parseFailure=false;
603
604        for (String dep : DEPENDENCYSPLITTER.split(dependencyString))
605        {
606            List<String> depparts = Lists.newArrayList(DEPENDENCYPARTSPLITTER.split(dep));
607            // Need two parts to the string
608            if (depparts.size() != 2)
609            {
610                parseFailure=true;
611                continue;
612            }
613            String instruction = depparts.get(0);
614            String target = depparts.get(1);
615            boolean targetIsAll = target.startsWith("*");
616
617            // Cannot have an "all" relationship with anything except pure *
618            if (targetIsAll && target.length()>1)
619            {
620                parseFailure = true;
621                continue;
622            }
623
624            // If this is a required element, add it to the required list
625            if ("required-before".equals(instruction) || "required-after".equals(instruction))
626            {
627                // You can't require everything
628                if (!targetIsAll)
629                {
630                    requirements.add(VersionParser.parseVersionReference(target));
631                }
632                else
633                {
634                    parseFailure=true;
635                    continue;
636                }
637            }
638
639            // You cannot have a versioned dependency on everything
640            if (targetIsAll && target.indexOf('@')>-1)
641            {
642                parseFailure = true;
643                continue;
644            }
645            // before elements are things we are loaded before (so they are our dependants)
646            if ("required-before".equals(instruction) || "before".equals(instruction))
647            {
648                dependants.add(VersionParser.parseVersionReference(target));
649            }
650            // after elements are things that load before we do (so they are out dependencies)
651            else if ("required-after".equals(instruction) || "after".equals(instruction))
652            {
653                dependencies.add(VersionParser.parseVersionReference(target));
654            }
655            else
656            {
657                parseFailure=true;
658            }
659        }
660
661        if (parseFailure)
662        {
663            FMLLog.log(Level.WARNING, "Unable to parse dependency string %s", dependencyString);
664            throw new LoaderException();
665        }
666    }
667
668    public Map<String,ModContainer> getIndexedModList()
669    {
670        return ImmutableMap.copyOf(namedMods);
671    }
672
673    public void initializeMods()
674    {
675        // Mod controller should be in the initialization state here
676        modController.distributeStateMessage(LoaderState.INITIALIZATION);
677        modController.transition(LoaderState.POSTINITIALIZATION);
678        modController.distributeStateMessage(FMLInterModComms.IMCEvent.class);
679        modController.distributeStateMessage(LoaderState.POSTINITIALIZATION);
680        modController.transition(LoaderState.AVAILABLE);
681        modController.distributeStateMessage(LoaderState.AVAILABLE);
682        FMLLog.info("Forge Mod Loader has successfully loaded %d mod%s", mods.size(), mods.size()==1 ? "" : "s");
683    }
684
685    public ICrashCallable getCallableCrashInformation()
686    {
687        return new ICrashCallable() {
688            @Override
689            public String call() throws Exception
690            {
691                return getCrashInformation();
692            }
693
694            @Override
695            public String getLabel()
696            {
697                return "FML";
698            }
699        };
700    }
701
702    public List<ModContainer> getActiveModList()
703    {
704        return modController != null ? modController.getActiveModList() : ImmutableList.<ModContainer>of();
705    }
706
707    public ModState getModState(ModContainer selectedMod)
708    {
709        return modController.getModState(selectedMod);
710    }
711
712    public String getMCVersionString()
713    {
714        return "Minecraft " + mccversion;
715    }
716
717    public boolean serverStarting(Object server)
718    {
719        try
720        {
721            modController.distributeStateMessage(LoaderState.SERVER_STARTING, server);
722            modController.transition(LoaderState.SERVER_STARTING);
723        }
724        catch (Throwable t)
725        {
726            FMLLog.log(Level.SEVERE, t, "A fatal exception occurred during the server starting event");
727            return false;
728        }
729        return true;
730    }
731
732    public void serverStarted()
733    {
734        modController.distributeStateMessage(LoaderState.SERVER_STARTED);
735        modController.transition(LoaderState.SERVER_STARTED);
736    }
737
738    public void serverStopping()
739    {
740        modController.distributeStateMessage(LoaderState.SERVER_STOPPING);
741        modController.transition(LoaderState.SERVER_STOPPING);
742    }
743
744    public BiMap<ModContainer, Object> getModObjectList()
745    {
746        return modController.getModObjectList();
747    }
748
749    public BiMap<Object, ModContainer> getReversedModObjectList()
750    {
751        return getModObjectList().inverse();
752    }
753
754    public ModContainer activeModContainer()
755    {
756        return modController != null ? modController.activeContainer() : null;
757    }
758
759    public boolean isInState(LoaderState state)
760    {
761        return modController.isInState(state);
762    }
763
764    public MinecraftDummyContainer getMinecraftModContainer()
765    {
766        return minecraft;
767    }
768
769    public boolean hasReachedState(LoaderState state) {
770        return modController != null ? modController.hasReachedState(state) : false;
771    }
772
773    public String getMCPVersionString() {
774        return String.format("MCP v%s", mcpversion);
775    }
776
777    public void serverStopped()
778    {
779        modController.distributeStateMessage(LoaderState.SERVER_STOPPED);
780        modController.transition(LoaderState.SERVER_STOPPED);
781        modController.transition(LoaderState.AVAILABLE);
782    }
783
784    public boolean serverAboutToStart(Object server)
785    {
786        try
787        {
788            modController.distributeStateMessage(LoaderState.SERVER_ABOUT_TO_START, server);
789            modController.transition(LoaderState.SERVER_ABOUT_TO_START);
790        }
791        catch (Throwable t)
792        {
793            FMLLog.log(Level.SEVERE, t, "A fatal exception occurred during the server about to start event");
794            return false;
795        }
796        return true;
797    }
798}