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 }