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