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