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