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