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    }