001    /*
002     * The FML Forge Mod Loader suite. Copyright (C) 2012 cpw
003     *
004     * 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
005     * Software Foundation; either version 2.1 of the License, or any later version.
006     *
007     * 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
008     * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
009     *
010     * 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
011     * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
012     */
013    package cpw.mods.fml.common;
014    
015    import java.io.File;
016    import java.io.FileInputStream;
017    import java.lang.annotation.Annotation;
018    import java.lang.reflect.Field;
019    import java.lang.reflect.Method;
020    import java.lang.reflect.Modifier;
021    import java.util.Arrays;
022    import java.util.List;
023    import java.util.Map;
024    import java.util.Properties;
025    import java.util.Set;
026    import java.util.logging.Level;
027    import java.util.zip.ZipEntry;
028    import java.util.zip.ZipFile;
029    import java.util.zip.ZipInputStream;
030    
031    import com.google.common.base.Function;
032    import com.google.common.base.Strings;
033    import com.google.common.base.Throwables;
034    import com.google.common.collect.ArrayListMultimap;
035    import com.google.common.collect.BiMap;
036    import com.google.common.collect.ImmutableBiMap;
037    import com.google.common.collect.Lists;
038    import com.google.common.collect.Multimap;
039    import com.google.common.collect.SetMultimap;
040    import com.google.common.collect.Sets;
041    import com.google.common.eventbus.EventBus;
042    import com.google.common.eventbus.Subscribe;
043    
044    import cpw.mods.fml.common.Mod.Instance;
045    import cpw.mods.fml.common.Mod.Metadata;
046    import cpw.mods.fml.common.discovery.ASMDataTable;
047    import cpw.mods.fml.common.discovery.ASMDataTable.ASMData;
048    import cpw.mods.fml.common.event.FMLConstructionEvent;
049    import cpw.mods.fml.common.event.FMLInitializationEvent;
050    import cpw.mods.fml.common.event.FMLPostInitializationEvent;
051    import cpw.mods.fml.common.event.FMLPreInitializationEvent;
052    import cpw.mods.fml.common.event.FMLServerStartedEvent;
053    import cpw.mods.fml.common.event.FMLServerStartingEvent;
054    import cpw.mods.fml.common.event.FMLServerStoppingEvent;
055    import cpw.mods.fml.common.event.FMLStateEvent;
056    import cpw.mods.fml.common.network.FMLNetworkHandler;
057    import cpw.mods.fml.common.versioning.ArtifactVersion;
058    import cpw.mods.fml.common.versioning.DefaultArtifactVersion;
059    import cpw.mods.fml.common.versioning.VersionParser;
060    import cpw.mods.fml.common.versioning.VersionRange;
061    
062    public class FMLModContainer implements ModContainer
063    {
064        private Mod modDescriptor;
065        private Object modInstance;
066        private File source;
067        private ModMetadata modMetadata;
068        private String className;
069        private Map<String, Object> descriptor;
070        private boolean enabled = true;
071        private String internalVersion;
072        private boolean overridesMetadata;
073        private EventBus eventBus;
074        private LoadController controller;
075        private Multimap<Class<? extends Annotation>, Object> annotations;
076        private DefaultArtifactVersion processedVersion;
077        private boolean isNetworkMod;
078    
079        private static final BiMap<Class<? extends FMLStateEvent>, Class<? extends Annotation>> modAnnotationTypes = ImmutableBiMap.<Class<? extends FMLStateEvent>, Class<? extends Annotation>>builder()
080            .put(FMLPreInitializationEvent.class, Mod.PreInit.class)
081            .put(FMLInitializationEvent.class, Mod.Init.class)
082            .put(FMLPostInitializationEvent.class, Mod.PostInit.class)
083            .put(FMLServerStartingEvent.class, Mod.ServerStarting.class)
084            .put(FMLServerStartedEvent.class, Mod.ServerStarted.class)
085            .put(FMLServerStoppingEvent.class, Mod.ServerStopping.class)
086            .build();
087        private static final BiMap<Class<? extends Annotation>, Class<? extends FMLStateEvent>> modTypeAnnotations = modAnnotationTypes.inverse();
088        private String annotationDependencies;
089        private VersionRange minecraftAccepted;
090    
091    
092        public FMLModContainer(String className, File modSource, Map<String,Object> modDescriptor)
093        {
094            this.className = className;
095            this.source = modSource;
096            this.descriptor = modDescriptor;
097        }
098    
099        @Override
100        public String getModId()
101        {
102            return (String) descriptor.get("modid");
103        }
104    
105        @Override
106        public String getName()
107        {
108            return modMetadata.name;
109        }
110    
111        @Override
112        public String getVersion()
113        {
114            return internalVersion;
115        }
116    
117        @Override
118        public File getSource()
119        {
120            return source;
121        }
122    
123        @Override
124        public ModMetadata getMetadata()
125        {
126            return modMetadata;
127        }
128    
129        @Override
130        public void bindMetadata(MetadataCollection mc)
131        {
132            modMetadata = mc.getMetadataForId(getModId(), descriptor);
133    
134            if (descriptor.containsKey("useMetadata"))
135            {
136                overridesMetadata = !((Boolean)descriptor.get("useMetadata")).booleanValue();
137            }
138    
139            if (overridesMetadata || !modMetadata.useDependencyInformation)
140            {
141                Set<ArtifactVersion> requirements = Sets.newHashSet();
142                List<ArtifactVersion> dependencies = Lists.newArrayList();
143                List<ArtifactVersion> dependants = Lists.newArrayList();
144                annotationDependencies = (String) descriptor.get("dependencies");
145                Loader.instance().computeDependencies(annotationDependencies, requirements, dependencies, dependants);
146                modMetadata.requiredMods = requirements;
147                modMetadata.dependencies = dependencies;
148                modMetadata.dependants = dependants;
149                FMLLog.finest("Parsed dependency info : %s %s %s", requirements, dependencies, dependants);
150            }
151            else
152            {
153                FMLLog.finest("Using mcmod dependency info : %s %s %s", modMetadata.requiredMods, modMetadata.dependencies, modMetadata.dependants);
154            }
155            if (Strings.isNullOrEmpty(modMetadata.name))
156            {
157                FMLLog.info("Mod %s is missing the required element 'name'. Substituting %s", getModId(), getModId());
158                modMetadata.name = getModId();
159            }
160            internalVersion = (String) descriptor.get("version");
161            if (Strings.isNullOrEmpty(internalVersion))
162            {
163                Properties versionProps = searchForVersionProperties();
164                if (versionProps != null)
165                {
166                    internalVersion = versionProps.getProperty(getModId()+".version");
167                    FMLLog.fine("Found version %s for mod %s in version.properties, using", internalVersion, getModId());
168                }
169    
170            }
171            if (Strings.isNullOrEmpty(internalVersion) && !Strings.isNullOrEmpty(modMetadata.version))
172            {
173                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);
174                internalVersion = modMetadata.version;
175            }
176            if (Strings.isNullOrEmpty(internalVersion))
177            {
178                FMLLog.warning("Mod %s is missing the required element 'version' and no fallback can be found. Substituting '1.0'.", getModId());
179                modMetadata.version = internalVersion = "1.0";
180            }
181    
182            String mcVersionString = (String) descriptor.get("acceptedMinecraftVersions");
183            if (!Strings.isNullOrEmpty(mcVersionString))
184            {
185                minecraftAccepted = VersionParser.parseRange(mcVersionString);
186            }
187            else
188            {
189                minecraftAccepted = Loader.instance().getMinecraftModContainer().getStaticVersionRange();
190            }
191        }
192    
193        public Properties searchForVersionProperties()
194        {
195            try
196            {
197                FMLLog.fine("Attempting to load the file version.properties from %s to locate a version number for %s", getSource().getName(), getModId());
198                Properties version = null;
199                if (getSource().isFile())
200                {
201                    ZipFile source = new ZipFile(getSource());
202                    ZipEntry versionFile = source.getEntry("version.properties");
203                    if (versionFile!=null)
204                    {
205                        version = new Properties();
206                        version.load(source.getInputStream(versionFile));
207                    }
208                    source.close();
209                }
210                else if (getSource().isDirectory())
211                {
212                    File propsFile = new File(getSource(),"version.properties");
213                    if (propsFile.exists() && propsFile.isFile())
214                    {
215                        version = new Properties();
216                        FileInputStream fis = new FileInputStream(propsFile);
217                        version.load(fis);
218                        fis.close();
219                    }
220                }
221                return version;
222            }
223            catch (Exception e)
224            {
225                Throwables.propagateIfPossible(e);
226                FMLLog.fine("Failed to find a usable version.properties file");
227                return null;
228            }
229        }
230    
231        @Override
232        public void setEnabledState(boolean enabled)
233        {
234            this.enabled = enabled;
235        }
236    
237        @Override
238        public Set<ArtifactVersion> getRequirements()
239        {
240            return modMetadata.requiredMods;
241        }
242    
243        @Override
244        public List<ArtifactVersion> getDependencies()
245        {
246            return modMetadata.dependencies;
247        }
248    
249        @Override
250        public List<ArtifactVersion> getDependants()
251        {
252            return modMetadata.dependants;
253        }
254    
255        @Override
256        public String getSortingRules()
257        {
258            return ((overridesMetadata || !modMetadata.useDependencyInformation) ? Strings.nullToEmpty(annotationDependencies) : modMetadata.printableSortingRules());
259        }
260    
261        @Override
262        public boolean matches(Object mod)
263        {
264            return mod == modInstance;
265        }
266    
267        @Override
268        public Object getMod()
269        {
270            return modInstance;
271        }
272    
273        @Override
274        public boolean registerBus(EventBus bus, LoadController controller)
275        {
276            if (this.enabled)
277            {
278                FMLLog.fine("Enabling mod %s", getModId());
279                this.eventBus = bus;
280                this.controller = controller;
281                eventBus.register(this);
282                return true;
283            }
284            else
285            {
286                return false;
287            }
288        }
289    
290        private Multimap<Class<? extends Annotation>, Object> gatherAnnotations(Class<?> clazz) throws Exception
291        {
292            Multimap<Class<? extends Annotation>,Object> anns = ArrayListMultimap.create();
293    
294            for (Method m : clazz.getDeclaredMethods())
295            {
296                for (Annotation a : m.getAnnotations())
297                {
298                    if (modTypeAnnotations.containsKey(a.annotationType()))
299                    {
300                        Class<?>[] paramTypes = new Class[] { modTypeAnnotations.get(a.annotationType()) };
301    
302                        if (Arrays.equals(m.getParameterTypes(), paramTypes))
303                        {
304                            m.setAccessible(true);
305                            anns.put(a.annotationType(), m);
306                        }
307                        else
308                        {
309                            FMLLog.severe("The mod %s appears to have an invalid method annotation %s. This annotation can only apply to methods with argument types %s -it will not be called", getModId(), a.annotationType().getSimpleName(), Arrays.toString(paramTypes));
310                        }
311                    }
312                }
313            }
314            return anns;
315        }
316    
317        private void processFieldAnnotations(ASMDataTable asmDataTable) throws Exception
318        {
319            SetMultimap<String, ASMData> annotations = asmDataTable.getAnnotationsFor(this);
320    
321            parseSimpleFieldAnnotation(annotations, Instance.class.getName(), new Function<ModContainer, Object>()
322            {
323                public Object apply(ModContainer mc)
324                {
325                    return mc.getMod();
326                }
327            });
328            parseSimpleFieldAnnotation(annotations, Metadata.class.getName(), new Function<ModContainer, Object>()
329            {
330                public Object apply(ModContainer mc)
331                {
332                    return mc.getMetadata();
333                }
334            });
335    
336    //TODO
337    //        for (Object o : annotations.get(Block.class))
338    //        {
339    //            Field f = (Field) o;
340    //            f.set(modInstance, GameRegistry.buildBlock(this, f.getType(), f.getAnnotation(Block.class)));
341    //        }
342        }
343    
344        private void parseSimpleFieldAnnotation(SetMultimap<String, ASMData> annotations, String annotationClassName, Function<ModContainer, Object> retreiver) throws IllegalAccessException
345        {
346            String[] annName = annotationClassName.split("\\.");
347            String annotationName = annName[annName.length - 1];
348            for (ASMData targets : annotations.get(annotationClassName))
349            {
350                String targetMod = (String) targets.getAnnotationInfo().get("value");
351                Field f = null;
352                Object injectedMod = null;
353                ModContainer mc = this;
354                boolean isStatic = false;
355                Class<?> clz = modInstance.getClass();
356                if (!Strings.isNullOrEmpty(targetMod))
357                {
358                    if (Loader.isModLoaded(targetMod))
359                    {
360                        mc = Loader.instance().getIndexedModList().get(targetMod);
361                    }
362                    else
363                    {
364                        mc = null;
365                    }
366                }
367                if (mc != null)
368                {
369                    try
370                    {
371                        clz = Class.forName(targets.getClassName(), true, Loader.instance().getModClassLoader());
372                        f = clz.getDeclaredField(targets.getObjectName());
373                        f.setAccessible(true);
374                        isStatic = Modifier.isStatic(f.getModifiers());
375                        injectedMod = retreiver.apply(mc);
376                    }
377                    catch (Exception e)
378                    {
379                        Throwables.propagateIfPossible(e);
380                        FMLLog.log(Level.WARNING, e, "Attempting to load @%s in class %s for %s and failing", annotationName, targets.getClassName(), mc.getModId());
381                    }
382                }
383                if (f != null)
384                {
385                    Object target = null;
386                    if (!isStatic)
387                    {
388                        target = modInstance;
389                        if (!modInstance.getClass().equals(clz))
390                        {
391                            FMLLog.warning("Unable to inject @%s in non-static field %s.%s for %s as it is NOT the primary mod instance", annotationName, targets.getClassName(), targets.getObjectName(), mc.getModId());
392                            continue;
393                        }
394                    }
395                    f.set(target, injectedMod);
396                }
397            }
398        }
399    
400        @Subscribe
401        public void constructMod(FMLConstructionEvent event)
402        {
403            try
404            {
405                ModClassLoader modClassLoader = event.getModClassLoader();
406                modClassLoader.addFile(source);
407                Class<?> clazz = Class.forName(className, true, modClassLoader);
408                ASMDataTable asmHarvestedAnnotations = event.getASMHarvestedData();
409                // TODO
410                asmHarvestedAnnotations.getAnnotationsFor(this);
411                annotations = gatherAnnotations(clazz);
412                isNetworkMod = FMLNetworkHandler.instance().registerNetworkMod(this, clazz, event.getASMHarvestedData());
413                modInstance = clazz.newInstance();
414                ProxyInjector.inject(this, event.getASMHarvestedData(), FMLCommonHandler.instance().getSide());
415                processFieldAnnotations(event.getASMHarvestedData());
416            }
417            catch (Throwable e)
418            {
419                controller.errorOccurred(this, e);
420                Throwables.propagateIfPossible(e);
421            }
422        }
423    
424        @Subscribe
425        public void handleModStateEvent(FMLStateEvent event)
426        {
427            Class<? extends Annotation> annotation = modAnnotationTypes.get(event.getClass());
428            if (annotation == null)
429            {
430                return;
431            }
432            try
433            {
434                for (Object o : annotations.get(annotation))
435                {
436                    Method m = (Method) o;
437                    m.invoke(modInstance, event);
438                }
439            }
440            catch (Throwable t)
441            {
442                controller.errorOccurred(this, t);
443                Throwables.propagateIfPossible(t);
444            }
445        }
446    
447        @Override
448        public ArtifactVersion getProcessedVersion()
449        {
450            if (processedVersion == null)
451            {
452                processedVersion = new DefaultArtifactVersion(getModId(), getVersion());
453            }
454            return processedVersion;
455        }
456        @Override
457        public boolean isImmutable()
458        {
459            return false;
460        }
461    
462        @Override
463        public boolean isNetworkMod()
464        {
465            return isNetworkMod;
466        }
467    
468        @Override
469        public String getDisplayVersion()
470        {
471            return modMetadata.version;
472        }
473    
474        @Override
475        public VersionRange acceptableMinecraftVersionRange()
476        {
477            return minecraftAccepted;
478        }
479    }