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