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