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