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