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