    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    }