001/*
002 * Forge Mod Loader
003 * Copyright (c) 2012-2013 cpw.
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser Public License v2.1
006 * which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
008 *
009 * Contributors:
010 *     cpw - implementation
011 */
012
013package cpw.mods.fml.common;
014
015import java.io.File;
016import java.io.FileReader;
017import java.io.IOException;
018import java.net.MalformedURLException;
019import java.util.Comparator;
020import java.util.HashSet;
021import java.util.List;
022import java.util.Map;
023import java.util.Properties;
024import java.util.Set;
025import java.util.concurrent.Callable;
026import java.util.logging.Level;
027
028import net.minecraft.crash.CallableMinecraftVersion;
029
030import com.google.common.base.CharMatcher;
031import com.google.common.base.Function;
032import com.google.common.base.Joiner;
033import com.google.common.base.Splitter;
034import com.google.common.collect.ArrayListMultimap;
035import com.google.common.collect.BiMap;
036import com.google.common.collect.HashBiMap;
037import com.google.common.collect.ImmutableList;
038import com.google.common.collect.ImmutableMap;
039import com.google.common.collect.ImmutableMultiset;
040import com.google.common.collect.Iterables;
041import com.google.common.collect.LinkedHashMultimap;
042import com.google.common.collect.Lists;
043import com.google.common.collect.Maps;
044import com.google.common.collect.SetMultimap;
045import com.google.common.collect.Sets;
046import com.google.common.collect.Multiset.Entry;
047import com.google.common.collect.Multisets;
048import com.google.common.collect.Ordering;
049import com.google.common.collect.Sets.SetView;
050import com.google.common.collect.TreeMultimap;
051
052import cpw.mods.fml.common.LoaderState.ModState;
053import cpw.mods.fml.common.discovery.ModDiscoverer;
054import cpw.mods.fml.common.event.FMLInterModComms;
055import cpw.mods.fml.common.event.FMLLoadEvent;
056import cpw.mods.fml.common.functions.ModIdFunction;
057import cpw.mods.fml.common.modloader.BaseModProxy;
058import cpw.mods.fml.common.registry.GameData;
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        // Handle being called before we've begun setup
575        if (modController == null)
576        {
577            return "";
578        }
579        StringBuilder ret = new StringBuilder();
580        List<String> branding = FMLCommonHandler.instance().getBrandings();
581
582        Joiner.on(' ').skipNulls().appendTo(ret, branding.subList(1, branding.size()));
583        if (modController!=null)
584        {
585            modController.printModStates(ret);
586        }
587        return ret.toString();
588    }
589
590    public String getFMLVersionString()
591    {
592        return String.format("%s.%s.%s.%s", major, minor, rev, build);
593    }
594
595    public ClassLoader getModClassLoader()
596    {
597        return modClassLoader;
598    }
599
600    public void computeDependencies(String dependencyString, Set<ArtifactVersion> requirements, List<ArtifactVersion> dependencies, List<ArtifactVersion> dependants)
601    {
602        if (dependencyString == null || dependencyString.length() == 0)
603        {
604            return;
605        }
606
607        boolean parseFailure=false;
608
609        for (String dep : DEPENDENCYSPLITTER.split(dependencyString))
610        {
611            List<String> depparts = Lists.newArrayList(DEPENDENCYPARTSPLITTER.split(dep));
612            // Need two parts to the string
613            if (depparts.size() != 2)
614            {
615                parseFailure=true;
616                continue;
617            }
618            String instruction = depparts.get(0);
619            String target = depparts.get(1);
620            boolean targetIsAll = target.startsWith("*");
621
622            // Cannot have an "all" relationship with anything except pure *
623            if (targetIsAll && target.length()>1)
624            {
625                parseFailure = true;
626                continue;
627            }
628
629            // If this is a required element, add it to the required list
630            if ("required-before".equals(instruction) || "required-after".equals(instruction))
631            {
632                // You can't require everything
633                if (!targetIsAll)
634                {
635                    requirements.add(VersionParser.parseVersionReference(target));
636                }
637                else
638                {
639                    parseFailure=true;
640                    continue;
641                }
642            }
643
644            // You cannot have a versioned dependency on everything
645            if (targetIsAll && target.indexOf('@')>-1)
646            {
647                parseFailure = true;
648                continue;
649            }
650            // before elements are things we are loaded before (so they are our dependants)
651            if ("required-before".equals(instruction) || "before".equals(instruction))
652            {
653                dependants.add(VersionParser.parseVersionReference(target));
654            }
655            // after elements are things that load before we do (so they are out dependencies)
656            else if ("required-after".equals(instruction) || "after".equals(instruction))
657            {
658                dependencies.add(VersionParser.parseVersionReference(target));
659            }
660            else
661            {
662                parseFailure=true;
663            }
664        }
665
666        if (parseFailure)
667        {
668            FMLLog.log(Level.WARNING, "Unable to parse dependency string %s", dependencyString);
669            throw new LoaderException();
670        }
671    }
672
673    public Map<String,ModContainer> getIndexedModList()
674    {
675        return ImmutableMap.copyOf(namedMods);
676    }
677
678    public void initializeMods()
679    {
680        // Mod controller should be in the initialization state here
681        modController.distributeStateMessage(LoaderState.INITIALIZATION);
682        modController.transition(LoaderState.POSTINITIALIZATION);
683        // Construct the "mod object table" so mods can refer to it in IMC and postinit
684        GameData.buildModObjectTable();
685        modController.distributeStateMessage(FMLInterModComms.IMCEvent.class);
686        modController.distributeStateMessage(LoaderState.POSTINITIALIZATION);
687        modController.transition(LoaderState.AVAILABLE);
688        modController.distributeStateMessage(LoaderState.AVAILABLE);
689        FMLLog.info("Forge Mod Loader has successfully loaded %d mod%s", mods.size(), mods.size()==1 ? "" : "s");
690    }
691
692    public ICrashCallable getCallableCrashInformation()
693    {
694        return new ICrashCallable() {
695            @Override
696            public String call() throws Exception
697            {
698                return getCrashInformation();
699            }
700
701            @Override
702            public String getLabel()
703            {
704                return "FML";
705            }
706        };
707    }
708
709    public List<ModContainer> getActiveModList()
710    {
711        return modController != null ? modController.getActiveModList() : ImmutableList.<ModContainer>of();
712    }
713
714    public ModState getModState(ModContainer selectedMod)
715    {
716        return modController.getModState(selectedMod);
717    }
718
719    public String getMCVersionString()
720    {
721        return "Minecraft " + mccversion;
722    }
723
724    public boolean serverStarting(Object server)
725    {
726        try
727        {
728            modController.distributeStateMessage(LoaderState.SERVER_STARTING, server);
729            modController.transition(LoaderState.SERVER_STARTING);
730        }
731        catch (Throwable t)
732        {
733            FMLLog.log(Level.SEVERE, t, "A fatal exception occurred during the server starting event");
734            return false;
735        }
736        return true;
737    }
738
739    public void serverStarted()
740    {
741        modController.distributeStateMessage(LoaderState.SERVER_STARTED);
742        modController.transition(LoaderState.SERVER_STARTED);
743    }
744
745    public void serverStopping()
746    {
747        modController.distributeStateMessage(LoaderState.SERVER_STOPPING);
748        modController.transition(LoaderState.SERVER_STOPPING);
749    }
750
751    public BiMap<ModContainer, Object> getModObjectList()
752    {
753        return modController.getModObjectList();
754    }
755
756    public BiMap<Object, ModContainer> getReversedModObjectList()
757    {
758        return getModObjectList().inverse();
759    }
760
761    public ModContainer activeModContainer()
762    {
763        return modController != null ? modController.activeContainer() : null;
764    }
765
766    public boolean isInState(LoaderState state)
767    {
768        return modController.isInState(state);
769    }
770
771    public MinecraftDummyContainer getMinecraftModContainer()
772    {
773        return minecraft;
774    }
775
776    public boolean hasReachedState(LoaderState state) {
777        return modController != null ? modController.hasReachedState(state) : false;
778    }
779
780    public String getMCPVersionString() {
781        return String.format("MCP v%s", mcpversion);
782    }
783
784    public void serverStopped()
785    {
786        modController.distributeStateMessage(LoaderState.SERVER_STOPPED);
787        try
788        {
789            modController.transition(LoaderState.SERVER_STOPPED);
790        }
791        catch (LoaderException e)
792        {
793            modController.forceState(LoaderState.SERVER_STOPPED);
794            // Discard any exceptions here - they mask other, real, exceptions
795        }
796        modController.transition(LoaderState.AVAILABLE);
797    }
798
799    public boolean serverAboutToStart(Object server)
800    {
801        try
802        {
803            modController.distributeStateMessage(LoaderState.SERVER_ABOUT_TO_START, server);
804            modController.transition(LoaderState.SERVER_ABOUT_TO_START);
805        }
806        catch (Throwable t)
807        {
808            FMLLog.log(Level.SEVERE, t, "A fatal exception occurred during the server about to start event");
809            return false;
810        }
811        return true;
812    }
813}