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 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 != null ? modController.activeContainer() : null; 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 != null ? modController.hasReachedState(state) : false; 739 } 740 741 public String getMCPVersionString() { 742 return String.format("MCP v%s", mcpversion); 743 } 744 }