    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    }