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