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