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    }