예제 #1
0
 public Properties searchForVersionProperties() {
   try {
     FMLLog.fine(
         "Attempting to load the file version.properties from %s to locate a version number for %s",
         getSource().getName(), getModId());
     Properties version = null;
     if (getSource().isFile()) {
       ZipFile source = new ZipFile(getSource());
       ZipEntry versionFile = source.getEntry("version.properties");
       if (versionFile != null) {
         version = new Properties();
         version.load(source.getInputStream(versionFile));
       }
       source.close();
     } else if (getSource().isDirectory()) {
       File propsFile = new File(getSource(), "version.properties");
       if (propsFile.exists() && propsFile.isFile()) {
         version = new Properties();
         FileInputStream fis = new FileInputStream(propsFile);
         version.load(fis);
         fis.close();
       }
     }
     return version;
   } catch (Exception e) {
     Throwables.propagateIfPossible(e);
     FMLLog.fine("Failed to find a usable version.properties file");
     return null;
   }
 }
  public static void inject(
      ModContainer mod, ASMDataTable data, Side side, ILanguageAdapter languageAdapter) {
    FMLLog.fine("Attempting to inject @SidedProxy classes into %s", mod.getModId());
    Set<ASMData> targets = data.getAnnotationsFor(mod).get(SidedProxy.class.getName());
    ClassLoader mcl = Loader.instance().getModClassLoader();

    for (ASMData targ : targets) {
      try {
        Class<?> proxyTarget = Class.forName(targ.getClassName(), true, mcl);
        Field target = proxyTarget.getDeclaredField(targ.getObjectName());
        if (target == null) {
          // Impossible?
          FMLLog.severe(
              "Attempted to load a proxy type into %s.%s but the field was not found",
              targ.getClassName(), targ.getObjectName());
          throw new LoaderException();
        }

        SidedProxy annotation = target.getAnnotation(SidedProxy.class);
        if (!Strings.isNullOrEmpty(annotation.modId())
            && !annotation.modId().equals(mod.getModId())) {
          FMLLog.fine(
              "Skipping proxy injection for %s.%s since it is not for mod %s",
              targ.getClassName(), targ.getObjectName(), mod.getModId());
          continue;
        }
        String targetType = side.isClient() ? annotation.clientSide() : annotation.serverSide();
        Object proxy = Class.forName(targetType, true, mcl).newInstance();

        if (languageAdapter.supportsStatics() && (target.getModifiers() & Modifier.STATIC) == 0) {
          FMLLog.severe(
              "Attempted to load a proxy type %s into %s.%s, but the field is not static",
              targetType, targ.getClassName(), targ.getObjectName());
          throw new LoaderException();
        }
        if (!target.getType().isAssignableFrom(proxy.getClass())) {
          FMLLog.severe(
              "Attempted to load a proxy type %s into %s.%s, but the types don't match",
              targetType, targ.getClassName(), targ.getObjectName());
          throw new LoaderException();
        }
        languageAdapter.setProxy(target, proxyTarget, proxy);
      } catch (Exception e) {
        FMLLog.log(
            Level.SEVERE,
            e,
            "An error occured trying to load a proxy into %s.%s",
            targ.getAnnotationInfo(),
            targ.getClassName(),
            targ.getObjectName());
        throw new LoaderException(e);
      }
    }

    // Allow language specific proxy injection.
    languageAdapter.setInternalProxies(mod, side, mcl);
  }
예제 #3
0
  @Override
  public void bindMetadata(MetadataCollection mc) {
    modMetadata = mc.getMetadataForId(getModId(), descriptor);

    if (descriptor.containsKey("useMetadata")) {
      overridesMetadata = !((Boolean) descriptor.get("useMetadata")).booleanValue();
    }

    if (overridesMetadata || !modMetadata.useDependencyInformation) {
      Set<ArtifactVersion> requirements = Sets.newHashSet();
      List<ArtifactVersion> dependencies = Lists.newArrayList();
      List<ArtifactVersion> dependants = Lists.newArrayList();
      annotationDependencies = (String) descriptor.get("dependencies");
      Loader.instance()
          .computeDependencies(annotationDependencies, requirements, dependencies, dependants);
      modMetadata.requiredMods = requirements;
      modMetadata.dependencies = dependencies;
      modMetadata.dependants = dependants;
      FMLLog.finest("Parsed dependency info : %s %s %s", requirements, dependencies, dependants);
    } else {
      FMLLog.finest(
          "Using mcmod dependency info : %s %s %s",
          modMetadata.requiredMods, modMetadata.dependencies, modMetadata.dependants);
    }
    if (Strings.isNullOrEmpty(modMetadata.name)) {
      FMLLog.info(
          "Mod %s is missing the required element 'name'. Substituting %s", getModId(), getModId());
      modMetadata.name = getModId();
    }
    internalVersion = (String) descriptor.get("version");
    if (Strings.isNullOrEmpty(internalVersion)) {
      Properties versionProps = searchForVersionProperties();
      if (versionProps != null) {
        internalVersion = versionProps.getProperty(getModId() + ".version");
        FMLLog.fine(
            "Found version %s for mod %s in version.properties, using",
            internalVersion, getModId());
      }
    }
    if (Strings.isNullOrEmpty(internalVersion) && !Strings.isNullOrEmpty(modMetadata.version)) {
      FMLLog.warning(
          "Mod %s is missing the required element 'version' and a version.properties file could not be found. Falling back to metadata version %s",
          getModId(), modMetadata.version);
      internalVersion = modMetadata.version;
    }
    if (Strings.isNullOrEmpty(internalVersion)) {
      FMLLog.warning(
          "Mod %s is missing the required element 'version' and no fallback can be found. Substituting '1.0'.",
          getModId());
      modMetadata.version = internalVersion = "1.0";
    }

    String mcVersionString = (String) descriptor.get("acceptedMinecraftVersions");
    if (!Strings.isNullOrEmpty(mcVersionString)) {
      minecraftAccepted = VersionParser.parseRange(mcVersionString);
    } else {
      minecraftAccepted = Loader.instance().getMinecraftModContainer().getStaticVersionRange();
    }
  }
예제 #4
0
  private void initializeLoader() {
    File modsDir = new File(minecraftDir, "mods");
    File configDir = new File(minecraftDir, "config");
    String canonicalModsPath;
    String canonicalConfigPath;

    try {
      canonicalModsPath = modsDir.getCanonicalPath();
      canonicalConfigPath = configDir.getCanonicalPath();
      canonicalConfigDir = configDir.getCanonicalFile();
      canonicalModsDir = modsDir.getCanonicalFile();
    } catch (IOException ioe) {
      FMLLog.log(
          Level.ERROR,
          ioe,
          "Failed to resolve loader directories: mods : %s ; config %s",
          canonicalModsDir.getAbsolutePath(),
          configDir.getAbsolutePath());
      throw new LoaderException(ioe);
    }

    if (!canonicalModsDir.exists()) {
      FMLLog.info("No mod directory found, creating one: %s", canonicalModsPath);
      boolean dirMade = canonicalModsDir.mkdir();
      if (!dirMade) {
        FMLLog.severe("Unable to create the mod directory %s", canonicalModsPath);
        throw new LoaderException(
            String.format("Unable to create the mod directory %s", canonicalModsPath));
      }
      FMLLog.info("Mod directory created successfully");
    }

    if (!canonicalConfigDir.exists()) {
      FMLLog.fine("No config directory found, creating one: %s", canonicalConfigPath);
      boolean dirMade = canonicalConfigDir.mkdir();
      if (!dirMade) {
        FMLLog.severe("Unable to create the config directory %s", canonicalConfigPath);
        throw new LoaderException();
      }
      FMLLog.info("Config directory created successfully");
    }

    if (!canonicalModsDir.isDirectory()) {
      FMLLog.severe("Attempting to load mods from %s, which is not a directory", canonicalModsPath);
      throw new LoaderException();
    }

    if (!configDir.isDirectory()) {
      FMLLog.severe(
          "Attempting to load configuration from %s, which is not a directory",
          canonicalConfigPath);
      throw new LoaderException();
    }

    readInjectedDependencies();
  }
예제 #5
0
  /**
   * The primary loading code
   *
   * <p>The found resources are first loaded into the {@link #modClassLoader} (always) then scanned
   * for class resources matching the specification above.
   *
   * <p>If they provide the {@link Mod} annotation, they will be loaded as "FML mods"
   *
   * <p>Finally, if they are successfully loaded as classes, they are then added to the available
   * mod list.
   */
  private ModDiscoverer identifyMods() {
    FMLLog.fine("Building injected Mod Containers %s", injectedContainers);
    // Add in the MCP mod container
    mods.add(new InjectedModContainer(mcp, new File("minecraft.jar")));
    for (String cont : injectedContainers) {
      ModContainer mc;
      try {
        mc = (ModContainer) Class.forName(cont, true, modClassLoader).newInstance();
      } catch (Exception e) {
        FMLLog.log(
            Level.ERROR, e, "A problem occured instantiating the injected mod container %s", cont);
        throw new LoaderException(e);
      }
      mods.add(new InjectedModContainer(mc, mc.getSource()));
    }
    ModDiscoverer discoverer = new ModDiscoverer();
    FMLLog.fine(
        "Attempting to load mods contained in the minecraft jar file and associated classes");
    discoverer.findClasspathMods(modClassLoader);
    FMLLog.fine("Minecraft jar mods loaded successfully");

    FMLLog.getLogger()
        .log(
            Level.INFO,
            "Found {} mods from the command line. Injecting into mod discoverer",
            ModListHelper.additionalMods.size());
    FMLLog.info("Searching %s for mods", canonicalModsDir.getAbsolutePath());
    discoverer.findModDirMods(
        canonicalModsDir, ModListHelper.additionalMods.values().toArray(new File[0]));
    File versionSpecificModsDir = new File(canonicalModsDir, mccversion);
    if (versionSpecificModsDir.isDirectory()) {
      FMLLog.info("Also searching %s for mods", versionSpecificModsDir);
      discoverer.findModDirMods(versionSpecificModsDir);
    }

    mods.addAll(discoverer.identifyMods());
    identifyDuplicates(mods);
    namedMods = Maps.uniqueIndex(mods, new ModIdFunction());
    FMLLog.info(
        "Forge Mod Loader has identified %d mod%s to load",
        mods.size(), mods.size() != 1 ? "s" : "");
    return discoverer;
  }
예제 #6
0
 /**
  * Not a fully fleshed out API, may change in future MC versions. However feel free to use and
  * suggest additions.
  */
 public static void pop(ProgressBar bar) {
   if (bar.getSteps() != bar.getStep())
     throw new IllegalStateException("can't pop unfinished ProgressBar " + bar.getTitle());
   bars.remove(bar);
   if (bar.getSteps() != 0) {
     long newTime = System.nanoTime();
     if (bar.timeEachStep)
       FMLLog.fine(
           "Bar Step: %s - %s took %.3fs",
           bar.getTitle(), bar.getMessage(), ((float) (newTime - bar.lastTime) / 1000000 / 1000));
     if (bar.getSteps() == 1)
       FMLLog.fine(
           "Bar Finished: %s - %s took %.3fs",
           bar.getTitle(), bar.getMessage(), ((float) (newTime - bar.startTime) / 1000000 / 1000));
     else
       FMLLog.fine(
           "Bar Finished: %s took %.3fs",
           bar.getTitle(), ((float) (newTime - bar.startTime) / 1000000 / 1000));
   }
   FMLCommonHandler.instance().processWindowMessages();
 }
예제 #7
0
 @Override
 public boolean registerBus(EventBus bus, LoadController controller) {
   if (this.enabled) {
     FMLLog.fine("Enabling mod %s", getModId());
     this.eventBus = bus;
     this.controller = controller;
     eventBus.register(this);
     return true;
   } else {
     return false;
   }
 }
예제 #8
0
 public void step(String message) {
   if (step >= steps) throw new IllegalStateException("too much steps for ProgressBar " + title);
   if (timeEachStep && step != 0) {
     long newTime = System.nanoTime();
     FMLLog.fine(
         "Bar Step: %s - %s took %.3fs",
         getTitle(), getMessage(), ((float) (newTime - lastTime) / 1000000 / 1000));
     lastTime = newTime;
   }
   step++;
   this.message = FMLCommonHandler.instance().stripSpecialChars(message);
   FMLCommonHandler.instance().processWindowMessages();
 }
예제 #9
0
  private void disableRequestedMods() {
    String forcedModList = System.getProperty("fml.modStates", "");
    FMLLog.finer("Received a system property request \'%s\'", forcedModList);
    Map<String, String> sysPropertyStateList =
        Splitter.on(CharMatcher.anyOf(";:"))
            .omitEmptyStrings()
            .trimResults()
            .withKeyValueSeparator("=")
            .split(forcedModList);
    FMLLog.finer(
        "System property request managing the state of %d mods", sysPropertyStateList.size());
    Map<String, String> modStates = Maps.newHashMap();

    forcedModFile = new File(canonicalConfigDir, "fmlModState.properties");
    Properties forcedModListProperties = new Properties();
    if (forcedModFile.exists() && forcedModFile.isFile()) {
      FMLLog.finer("Found a mod state file %s", forcedModFile.getName());
      try {
        forcedModListProperties.load(new FileReader(forcedModFile));
        FMLLog.finer("Loaded states for %d mods from file", forcedModListProperties.size());
      } catch (Exception e) {
        FMLLog.log(Level.INFO, e, "An error occurred reading the fmlModState.properties file");
      }
    }
    modStates.putAll(Maps.fromProperties(forcedModListProperties));
    modStates.putAll(sysPropertyStateList);
    FMLLog.fine("After merging, found state information for %d mods", modStates.size());

    Map<String, Boolean> isEnabled =
        Maps.transformValues(
            modStates,
            new Function<String, Boolean>() {
              @Override
              public Boolean apply(String input) {
                return Boolean.parseBoolean(input);
              }
            });

    for (Map.Entry<String, Boolean> entry : isEnabled.entrySet()) {
      if (namedMods.containsKey(entry.getKey())) {
        FMLLog.info("Setting mod %s to enabled state %b", entry.getKey(), entry.getValue());
        namedMods.get(entry.getKey()).setEnabledState(entry.getValue());
      }
    }
  }
예제 #10
0
 @Subscribe
 public void constructMod(FMLConstructionEvent event) {
   try {
     ModClassLoader modClassLoader = event.getModClassLoader();
     modClassLoader.addFile(source);
     // MCPC - show users what mod is being loaded in case a crash occurs
     FMLLog.fine("Constructing mod from file source : " + source);
     Class<?> clazz = Class.forName(className, true, modClassLoader);
     ASMDataTable asmHarvestedAnnotations = event.getASMHarvestedData();
     // TODO
     asmHarvestedAnnotations.getAnnotationsFor(this);
     annotations = gatherAnnotations(clazz);
     isNetworkMod =
         FMLNetworkHandler.instance().registerNetworkMod(this, clazz, event.getASMHarvestedData());
     modInstance = clazz.newInstance();
     ProxyInjector.inject(
         this, event.getASMHarvestedData(), FMLCommonHandler.instance().getSide());
     processFieldAnnotations(event.getASMHarvestedData());
   } catch (Throwable e) {
     controller.errorOccurred(this, e);
     Throwables.propagateIfPossible(e);
   }
 }
예제 #11
0
  /**
   * Fire a FMLMissingMappingsEvent to let mods determine how blocks/items defined in the world
   * save, but missing from the runtime, are to be handled.
   *
   * @param missing Map containing missing names with their associated id, blocks need to come
   *     before items for remapping.
   * @param isLocalWorld Whether this is executing for a world load (local/server) or a client.
   * @param gameData GameData instance where the new map's config is to be loaded into.
   * @return List with the mapping results.
   */
  public List<String> fireMissingMappingEvent(
      LinkedHashMap<String, Integer> missingBlocks,
      LinkedHashMap<String, Integer> missingItems,
      boolean isLocalWorld,
      GameData gameData,
      Map<String, Integer[]> remapBlocks,
      Map<String, Integer[]> remapItems) {
    if (missingBlocks.isEmpty() && missingItems.isEmpty()) // nothing to do
    {
      return ImmutableList.of();
    }

    FMLLog.fine(
        "There are %d mappings missing - attempting a mod remap",
        missingBlocks.size() + missingItems.size());
    ArrayListMultimap<String, MissingMapping> missingMappings = ArrayListMultimap.create();

    for (Map.Entry<String, Integer> mapping : missingBlocks.entrySet()) {
      MissingMapping m =
          new MissingMapping(GameRegistry.Type.BLOCK, mapping.getKey(), mapping.getValue());
      missingMappings.put(m.name.substring(0, m.name.indexOf(':')), m);
    }
    for (Map.Entry<String, Integer> mapping : missingItems.entrySet()) {
      MissingMapping m =
          new MissingMapping(GameRegistry.Type.ITEM, mapping.getKey(), mapping.getValue());
      missingMappings.put(m.name.substring(0, m.name.indexOf(':')), m);
    }

    FMLMissingMappingsEvent missingEvent = new FMLMissingMappingsEvent(missingMappings);
    modController.propogateStateMessage(missingEvent);

    if (isLocalWorld) // local world, warn about entries still being set to the default action
    {
      boolean didWarn = false;

      for (MissingMapping mapping : missingMappings.values()) {
        if (mapping.getAction() == FMLMissingMappingsEvent.Action.DEFAULT) {
          if (!didWarn) {
            FMLLog.severe(
                "There are unidentified mappings in this world - we are going to attempt to process anyway");
            didWarn = true;
          }

          FMLLog.severe(
              "Unidentified %s: %s, id %d",
              mapping.type == Type.BLOCK ? "block" : "item", mapping.name, mapping.id);
        }
      }
    } else // remote world, fail on entries with the default action
    {
      List<String> missedMapping = new ArrayList<String>();

      for (MissingMapping mapping : missingMappings.values()) {
        if (mapping.getAction() == FMLMissingMappingsEvent.Action.DEFAULT) {
          missedMapping.add(mapping.name);
        }
      }

      if (!missedMapping.isEmpty()) {
        return ImmutableList.copyOf(missedMapping);
      }
    }

    return GameData.processIdRematches(
        missingMappings.values(), isLocalWorld, gameData, remapBlocks, remapItems);
  }
예제 #12
0
  /**
   * Called from the hook to start mod loading. We trigger the {@link #identifyMods()} and
   * Constructing, Preinitalization, and Initalization phases here. Finally, the mod list is frozen
   * completely and is consider immutable from then on.
   */
  public void loadMods() {
    progressBar = ProgressManager.push("Loading", 7);
    progressBar.step("Constructing Mods");
    initializeLoader();
    mods = Lists.newArrayList();
    namedMods = Maps.newHashMap();
    modController = new LoadController(this);
    modController.transition(LoaderState.LOADING, false);
    discoverer = identifyMods();
    ModAPIManager.INSTANCE.manageAPI(modClassLoader, discoverer);
    disableRequestedMods();
    modController.distributeStateMessage(FMLLoadEvent.class);
    sortModList();
    ModAPIManager.INSTANCE.cleanupAPIContainers(modController.getActiveModList());
    ModAPIManager.INSTANCE.cleanupAPIContainers(mods);
    mods = ImmutableList.copyOf(mods);
    for (File nonMod : discoverer.getNonModLibs()) {
      if (nonMod.isFile()) {
        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());
        try {
          modClassLoader.addFile(nonMod);
        } catch (MalformedURLException e) {
          FMLLog.log(
              Level.ERROR,
              e,
              "Encountered a weird problem with non-mod file injection : %s",
              nonMod.getName());
        }
      }
    }
    modController.transition(LoaderState.CONSTRUCTING, false);
    modController.distributeStateMessage(
        LoaderState.CONSTRUCTING, modClassLoader, discoverer.getASMTable(), reverseDependencies);

    List<ModContainer> mods = Lists.newArrayList();
    mods.addAll(getActiveModList());
    Collections.sort(
        mods,
        new Comparator<ModContainer>() {
          @Override
          public int compare(ModContainer o1, ModContainer o2) {
            return o1.getModId().compareTo(o2.getModId());
          }
        });

    FMLLog.fine("Mod signature data");
    FMLLog.fine(" \tValid Signatures:");
    for (ModContainer mod : getActiveModList()) {
      if (mod.getSigningCertificate() != null)
        FMLLog.fine(
            "\t\t(%s) %s\t(%s\t%s)\t%s",
            CertificateHelper.getFingerprint(mod.getSigningCertificate()),
            mod.getModId(),
            mod.getName(),
            mod.getVersion(),
            mod.getSource().getName());
    }
    FMLLog.fine(" \tMissing Signatures:");
    for (ModContainer mod : getActiveModList()) {
      if (mod.getSigningCertificate() == null)
        FMLLog.fine(
            "\t\t%s\t(%s\t%s)\t%s",
            mod.getModId(), mod.getName(), mod.getVersion(), mod.getSource().getName());
    }
    if (getActiveModList().isEmpty()) {
      FMLLog.fine("No user mod signature data found");
    }
    progressBar.step("Initializing mods Phase 1");
    modController.transition(LoaderState.PREINITIALIZATION, false);
  }
예제 #13
0
  /**
   * Sort the mods into a sorted list, using dependency information from the containers. The sorting
   * is performed using a {@link TopologicalSort} based on the pre- and post- dependency information
   * provided by the mods.
   */
  private void sortModList() {
    FMLLog.finer("Verifying mod requirements are satisfied");
    try {
      BiMap<String, ArtifactVersion> modVersions = HashBiMap.create();
      for (ModContainer mod :
          Iterables.concat(getActiveModList(), ModAPIManager.INSTANCE.getAPIList())) {
        modVersions.put(mod.getModId(), mod.getProcessedVersion());
      }

      ArrayListMultimap<String, String> reqList = ArrayListMultimap.create();
      for (ModContainer mod : getActiveModList()) {
        if (!mod.acceptableMinecraftVersionRange()
            .containsVersion(minecraft.getProcessedVersion())) {
          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());
          throw new WrongMinecraftVersionException(mod);
        }
        Map<String, ArtifactVersion> names =
            Maps.uniqueIndex(mod.getRequirements(), new ArtifactVersionNameFunction());
        Set<ArtifactVersion> versionMissingMods = Sets.newHashSet();

        Set<String> missingMods = Sets.difference(names.keySet(), modVersions.keySet());
        if (!missingMods.isEmpty()) {
          FMLLog.severe(
              "The mod %s (%s) requires mods %s to be available",
              mod.getModId(), mod.getName(), missingMods);
          for (String modid : missingMods) {
            versionMissingMods.add(names.get(modid));
          }
          throw new MissingModsException(versionMissingMods, mod.getModId(), mod.getName());
        }
        reqList.putAll(mod.getModId(), names.keySet());
        ImmutableList<ArtifactVersion> allDeps =
            ImmutableList.<ArtifactVersion>builder()
                .addAll(mod.getDependants())
                .addAll(mod.getDependencies())
                .build();
        for (ArtifactVersion v : allDeps) {
          if (modVersions.containsKey(v.getLabel())) {
            if (!v.containsVersion(modVersions.get(v.getLabel()))) {
              versionMissingMods.add(v);
            }
          }
        }
        if (!versionMissingMods.isEmpty()) {
          FMLLog.severe(
              "The mod %s (%s) requires mod versions %s to be available",
              mod.getModId(), mod.getName(), versionMissingMods);
          throw new MissingModsException(versionMissingMods, mod.getModId(), mod.getName());
        }
      }

      FMLLog.finer("All mod requirements are satisfied");

      reverseDependencies =
          Multimaps.invertFrom(reqList, ArrayListMultimap.<String, String>create());
      ModSorter sorter = new ModSorter(getActiveModList(), namedMods);

      try {
        FMLLog.finer("Sorting mods into an ordered list");
        List<ModContainer> sortedMods = sorter.sort();
        // Reset active list to the sorted list
        modController.getActiveModList().clear();
        modController.getActiveModList().addAll(sortedMods);
        // And inject the sorted list into the overall list
        mods.removeAll(sortedMods);
        sortedMods.addAll(mods);
        mods = sortedMods;
        FMLLog.finer("Mod sorting completed successfully");
      } catch (ModSortingException sortException) {
        FMLLog.severe(
            "A dependency cycle was detected in the input mod set so an ordering cannot be determined");
        SortingExceptionData<ModContainer> exceptionData = sortException.getExceptionData();
        FMLLog.severe("The first mod in the cycle is %s", exceptionData.getFirstBadNode());
        FMLLog.severe("The mod cycle involves");
        for (ModContainer mc : exceptionData.getVisitedNodes()) {
          FMLLog.severe(
              "%s : before: %s, after: %s",
              mc.toString(), mc.getDependants(), mc.getDependencies());
        }
        FMLLog.log(Level.ERROR, sortException, "The full error");
        throw sortException;
      }
    } finally {
      FMLLog.fine("Mod sorting data");
      int unprintedMods = mods.size();
      for (ModContainer mod : getActiveModList()) {
        if (!mod.isImmutable()) {
          FMLLog.fine(
              "\t%s(%s:%s): %s (%s)",
              mod.getModId(),
              mod.getName(),
              mod.getVersion(),
              mod.getSource().getName(),
              mod.getSortingRules());
          unprintedMods--;
        }
      }
      if (unprintedMods == mods.size()) {
        FMLLog.fine("No user mods found to sort");
      }
    }
  }