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