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).minecraftVersion();
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(), versionMissingMods);
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 Constructing, Preinitalization, and Initalization 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 If the mod is loaded
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        public File getConfigDir()
523        {
524            return canonicalConfigDir;
525        }
526    
527        public String getCrashInformation()
528        {
529            StringBuilder ret = new StringBuilder();
530            List<String> branding = FMLCommonHandler.instance().getBrandings();
531    
532            Joiner.on(' ').skipNulls().appendTo(ret, branding.subList(1, branding.size()));
533            if (modController!=null)
534            {
535                modController.printModStates(ret);
536            }
537            return ret.toString();
538        }
539    
540        public String getFMLVersionString()
541        {
542            return String.format("%s.%s.%s.%s", major, minor, rev, build);
543        }
544    
545        public ClassLoader getModClassLoader()
546        {
547            return modClassLoader;
548        }
549    
550        public void computeDependencies(String dependencyString, Set<ArtifactVersion> requirements, List<ArtifactVersion> dependencies, List<ArtifactVersion> dependants)
551        {
552            if (dependencyString == null || dependencyString.length() == 0)
553            {
554                return;
555            }
556    
557            boolean parseFailure=false;
558    
559            for (String dep : DEPENDENCYSPLITTER.split(dependencyString))
560            {
561                List<String> depparts = Lists.newArrayList(DEPENDENCYPARTSPLITTER.split(dep));
562                // Need two parts to the string
563                if (depparts.size() != 2)
564                {
565                    parseFailure=true;
566                    continue;
567                }
568                String instruction = depparts.get(0);
569                String target = depparts.get(1);
570                boolean targetIsAll = target.startsWith("*");
571    
572                // Cannot have an "all" relationship with anything except pure *
573                if (targetIsAll && target.length()>1)
574                {
575                    parseFailure = true;
576                    continue;
577                }
578    
579                // If this is a required element, add it to the required list
580                if ("required-before".equals(instruction) || "required-after".equals(instruction))
581                {
582                    // You can't require everything
583                    if (!targetIsAll)
584                    {
585                        requirements.add(VersionParser.parseVersionReference(target));
586                    }
587                    else
588                    {
589                        parseFailure=true;
590                        continue;
591                    }
592                }
593    
594                // You cannot have a versioned dependency on everything
595                if (targetIsAll && target.indexOf('@')>-1)
596                {
597                    parseFailure = true;
598                    continue;
599                }
600                // before elements are things we are loaded before (so they are our dependants)
601                if ("required-before".equals(instruction) || "before".equals(instruction))
602                {
603                    dependants.add(VersionParser.parseVersionReference(target));
604                }
605                // after elements are things that load before we do (so they are out dependencies)
606                else if ("required-after".equals(instruction) || "after".equals(instruction))
607                {
608                    dependencies.add(VersionParser.parseVersionReference(target));
609                }
610                else
611                {
612                    parseFailure=true;
613                }
614            }
615    
616            if (parseFailure)
617            {
618                FMLLog.log(Level.WARNING, "Unable to parse dependency string %s", dependencyString);
619                throw new LoaderException();
620            }
621        }
622    
623        public Map<String,ModContainer> getIndexedModList()
624        {
625            return ImmutableMap.copyOf(namedMods);
626        }
627    
628        public void initializeMods()
629        {
630            // Mod controller should be in the initialization state here
631            modController.distributeStateMessage(LoaderState.INITIALIZATION);
632            modController.transition(LoaderState.POSTINITIALIZATION);
633            modController.distributeStateMessage(LoaderState.POSTINITIALIZATION);
634            modController.transition(LoaderState.AVAILABLE);
635            modController.distributeStateMessage(LoaderState.AVAILABLE);
636            FMLLog.info("Forge Mod Loader has successfully loaded %d mod%s", mods.size(), mods.size()==1 ? "" : "s");
637        }
638    
639        public ICrashCallable getCallableCrashInformation()
640        {
641            return new ICrashCallable() {
642                @Override
643                public String call() throws Exception
644                {
645                    return getCrashInformation();
646                }
647    
648                @Override
649                public String getLabel()
650                {
651                    return "FML";
652                }
653            };
654        }
655    
656        public List<ModContainer> getActiveModList()
657        {
658            return modController.getActiveModList();
659        }
660    
661        public ModState getModState(ModContainer selectedMod)
662        {
663            return modController.getModState(selectedMod);
664        }
665    
666        public String getMCVersionString()
667        {
668            return "Minecraft " + mccversion;
669        }
670    
671        public void serverStarting(Object server)
672        {
673            modController.distributeStateMessage(LoaderState.SERVER_STARTING, server);
674            modController.transition(LoaderState.SERVER_STARTING);
675        }
676    
677        public void serverStarted()
678        {
679            modController.distributeStateMessage(LoaderState.SERVER_STARTED);
680            modController.transition(LoaderState.SERVER_STARTED);
681        }
682    
683        public void serverStopping()
684        {
685            modController.distributeStateMessage(LoaderState.SERVER_STOPPING);
686            modController.transition(LoaderState.SERVER_STOPPING);
687            modController.transition(LoaderState.AVAILABLE);
688    
689        }
690    
691        public BiMap<ModContainer, Object> getModObjectList()
692        {
693            return modController.getModObjectList();
694        }
695    
696        public BiMap<Object, ModContainer> getReversedModObjectList()
697        {
698            return getModObjectList().inverse();
699        }
700    
701        public ModContainer activeModContainer()
702        {
703            return modController.activeContainer();
704        }
705    
706        public boolean isInState(LoaderState state)
707        {
708            return modController.isInState(state);
709        }
710    }