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