    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 */
    013package cpw.mods.fml.common;
    014
    015import java.io.File;
    016import java.io.FileInputStream;
    017import java.lang.annotation.Annotation;
    018import java.lang.reflect.Field;
    019import java.lang.reflect.Method;
    020import java.lang.reflect.Modifier;
    021import java.security.cert.Certificate;
    022import java.util.Arrays;
    023import java.util.List;
    024import java.util.Map;
    025import java.util.Properties;
    026import java.util.Set;
    027import java.util.logging.Level;
    028import java.util.zip.ZipEntry;
    029import java.util.zip.ZipFile;
    030import java.util.zip.ZipInputStream;
    031
    032import com.google.common.base.Function;
    033import com.google.common.base.Predicates;
    034import com.google.common.base.Strings;
    035import com.google.common.base.Throwables;
    036import com.google.common.collect.ArrayListMultimap;
    037import com.google.common.collect.BiMap;
    038import com.google.common.collect.ImmutableBiMap;
    039import com.google.common.collect.ImmutableList;
    040import com.google.common.collect.ImmutableList.Builder;
    041import com.google.common.collect.ImmutableSet;
    042import com.google.common.collect.Iterators;
    043import com.google.common.collect.Lists;
    044import com.google.common.collect.Multimap;
    045import com.google.common.collect.SetMultimap;
    046import com.google.common.collect.Sets;
    047import com.google.common.eventbus.EventBus;
    048import com.google.common.eventbus.Subscribe;
    049
    050import cpw.mods.fml.common.Mod.Instance;
    051import cpw.mods.fml.common.Mod.Metadata;
    052import cpw.mods.fml.common.discovery.ASMDataTable;
    053import cpw.mods.fml.common.discovery.ASMDataTable.ASMData;
    054import cpw.mods.fml.common.event.FMLConstructionEvent;
    055import cpw.mods.fml.common.event.FMLEvent;
    056import cpw.mods.fml.common.event.FMLInitializationEvent;
    057import cpw.mods.fml.common.event.FMLInterModComms.IMCEvent;
    058import cpw.mods.fml.common.event.FMLFingerprintViolationEvent;
    059import cpw.mods.fml.common.event.FMLPostInitializationEvent;
    060import cpw.mods.fml.common.event.FMLPreInitializationEvent;
    061import cpw.mods.fml.common.event.FMLServerAboutToStartEvent;
    062import cpw.mods.fml.common.event.FMLServerStartedEvent;
    063import cpw.mods.fml.common.event.FMLServerStartingEvent;
    064import cpw.mods.fml.common.event.FMLServerStoppedEvent;
    065import cpw.mods.fml.common.event.FMLServerStoppingEvent;
    066import cpw.mods.fml.common.event.FMLStateEvent;
    067import cpw.mods.fml.common.network.FMLNetworkHandler;
    068import cpw.mods.fml.common.versioning.ArtifactVersion;
    069import cpw.mods.fml.common.versioning.DefaultArtifactVersion;
    070import cpw.mods.fml.common.versioning.VersionParser;
    071import cpw.mods.fml.common.versioning.VersionRange;
    072
    073public class FMLModContainer implements ModContainer
    074{
    075    private Mod modDescriptor;
    076    private Object modInstance;
    077    private File source;
    078    private ModMetadata modMetadata;
    079    private String className;
    080    private Map<String, Object> descriptor;
    081    private boolean enabled = true;
    082    private String internalVersion;
    083    private boolean overridesMetadata;
    084    private EventBus eventBus;
    085    private LoadController controller;
    086    private Multimap<Class<? extends Annotation>, Object> annotations;
    087    private DefaultArtifactVersion processedVersion;
    088    private boolean isNetworkMod;
    089
    090    private static final BiMap<Class<? extends FMLEvent>, Class<? extends Annotation>> modAnnotationTypes = ImmutableBiMap.<Class<? extends FMLEvent>, Class<? extends Annotation>>builder()
    091        .put(FMLPreInitializationEvent.class, Mod.PreInit.class)
    092        .put(FMLInitializationEvent.class, Mod.Init.class)
    093        .put(FMLPostInitializationEvent.class, Mod.PostInit.class)
    094        .put(FMLServerAboutToStartEvent.class, Mod.ServerAboutToStart.class)
    095        .put(FMLServerStartingEvent.class, Mod.ServerStarting.class)
    096        .put(FMLServerStartedEvent.class, Mod.ServerStarted.class)
    097        .put(FMLServerStoppingEvent.class, Mod.ServerStopping.class)
    098        .put(FMLServerStoppedEvent.class, Mod.ServerStopped.class)
    099        .put(IMCEvent.class,Mod.IMCCallback.class)
    100        .put(FMLFingerprintViolationEvent.class, Mod.FingerprintWarning.class)
    101        .build();
    102    private static final BiMap<Class<? extends Annotation>, Class<? extends FMLEvent>> modTypeAnnotations = modAnnotationTypes.inverse();
    103    private String annotationDependencies;
    104    private VersionRange minecraftAccepted;
    105    private boolean fingerprintNotPresent;
    106    private Set<String> sourceFingerprints;
    107    private Certificate certificate;
    108    private String modLanguage;
    109    private ILanguageAdapter languageAdapter;
    110
    111    public FMLModContainer(String className, File modSource, Map<String,Object> modDescriptor)
    112    {
    113        this.className = className;
    114        this.source = modSource;
    115        this.descriptor = modDescriptor;
    116        this.modLanguage = (String) modDescriptor.get("modLanguage");
    117        this.languageAdapter = "scala".equals(modLanguage) ? new ILanguageAdapter.ScalaAdapter() : new ILanguageAdapter.JavaAdapter();
    118    }
    119
    120    private ILanguageAdapter getLanguageAdapter()
    121    {
    122        return languageAdapter;
    123    }
    124    @Override
    125    public String getModId()
    126    {
    127        return (String) descriptor.get("modid");
    128    }
    129
    130    @Override
    131    public String getName()
    132    {
    133        return modMetadata.name;
    134    }
    135
    136    @Override
    137    public String getVersion()
    138    {
    139        return internalVersion;
    140    }
    141
    142    @Override
    143    public File getSource()
    144    {
    145        return source;
    146    }
    147
    148    @Override
    149    public ModMetadata getMetadata()
    150    {
    151        return modMetadata;
    152    }
    153
    154    @Override
    155    public void bindMetadata(MetadataCollection mc)
    156    {
    157        modMetadata = mc.getMetadataForId(getModId(), descriptor);
    158
    159        if (descriptor.containsKey("useMetadata"))
    160        {
    161            overridesMetadata = !((Boolean)descriptor.get("useMetadata")).booleanValue();
    162        }
    163
    164        if (overridesMetadata || !modMetadata.useDependencyInformation)
    165        {
    166            Set<ArtifactVersion> requirements = Sets.newHashSet();
    167            List<ArtifactVersion> dependencies = Lists.newArrayList();
    168            List<ArtifactVersion> dependants = Lists.newArrayList();
    169            annotationDependencies = (String) descriptor.get("dependencies");
    170            Loader.instance().computeDependencies(annotationDependencies, requirements, dependencies, dependants);
    171            modMetadata.requiredMods = requirements;
    172            modMetadata.dependencies = dependencies;
    173            modMetadata.dependants = dependants;
    174            FMLLog.log(getModId(), Level.FINEST, "Parsed dependency info : %s %s %s", requirements, dependencies, dependants);
    175        }
    176        else
    177        {
    178            FMLLog.log(getModId(), Level.FINEST, "Using mcmod dependency info : %s %s %s", modMetadata.requiredMods, modMetadata.dependencies, modMetadata.dependants);
    179        }
    180        if (Strings.isNullOrEmpty(modMetadata.name))
    181        {
    182            FMLLog.log(getModId(), Level.INFO,"Mod %s is missing the required element 'name'. Substituting %s", getModId(), getModId());
    183            modMetadata.name = getModId();
    184        }
    185        internalVersion = (String) descriptor.get("version");
    186        if (Strings.isNullOrEmpty(internalVersion))
    187        {
    188            Properties versionProps = searchForVersionProperties();
    189            if (versionProps != null)
    190            {
    191                internalVersion = versionProps.getProperty(getModId()+".version");
    192                FMLLog.log(getModId(), Level.FINE, "Found version %s for mod %s in version.properties, using", internalVersion, getModId());
    193            }
    194
    195        }
    196        if (Strings.isNullOrEmpty(internalVersion) && !Strings.isNullOrEmpty(modMetadata.version))
    197        {
    198            FMLLog.log(getModId(), Level.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);
    199            internalVersion = modMetadata.version;
    200        }
    201        if (Strings.isNullOrEmpty(internalVersion))
    202        {
    203            FMLLog.log(getModId(), Level.WARNING, "Mod %s is missing the required element 'version' and no fallback can be found. Substituting '1.0'.", getModId());
    204            modMetadata.version = internalVersion = "1.0";
    205        }
    206
    207        String mcVersionString = (String) descriptor.get("acceptedMinecraftVersions");
    208        if (!Strings.isNullOrEmpty(mcVersionString))
    209        {
    210            minecraftAccepted = VersionParser.parseRange(mcVersionString);
    211        }
    212        else
    213        {
    214            minecraftAccepted = Loader.instance().getMinecraftModContainer().getStaticVersionRange();
    215        }
    216    }
    217
    218    public Properties searchForVersionProperties()
    219    {
    220        try
    221        {
    222            FMLLog.log(getModId(), Level.FINE,"Attempting to load the file version.properties from %s to locate a version number for %s", getSource().getName(), getModId());
    223            Properties version = null;
    224            if (getSource().isFile())
    225            {
    226                ZipFile source = new ZipFile(getSource());
    227                ZipEntry versionFile = source.getEntry("version.properties");
    228                if (versionFile!=null)
    229                {
    230                    version = new Properties();
    231                    version.load(source.getInputStream(versionFile));
    232                }
    233                source.close();
    234            }
    235            else if (getSource().isDirectory())
    236            {
    237                File propsFile = new File(getSource(),"version.properties");
    238                if (propsFile.exists() && propsFile.isFile())
    239                {
    240                    version = new Properties();
    241                    FileInputStream fis = new FileInputStream(propsFile);
    242                    version.load(fis);
    243                    fis.close();
    244                }
    245            }
    246            return version;
    247        }
    248        catch (Exception e)
    249        {
    250            Throwables.propagateIfPossible(e);
    251            FMLLog.log(getModId(), Level.FINEST, "Failed to find a usable version.properties file");
    252            return null;
    253        }
    254    }
    255
    256    @Override
    257    public void setEnabledState(boolean enabled)
    258    {
    259        this.enabled = enabled;
    260    }
    261
    262    @Override
    263    public Set<ArtifactVersion> getRequirements()
    264    {
    265        return modMetadata.requiredMods;
    266    }
    267
    268    @Override
    269    public List<ArtifactVersion> getDependencies()
    270    {
    271        return modMetadata.dependencies;
    272    }
    273
    274    @Override
    275    public List<ArtifactVersion> getDependants()
    276    {
    277        return modMetadata.dependants;
    278    }
    279
    280    @Override
    281    public String getSortingRules()
    282    {
    283        return ((overridesMetadata || !modMetadata.useDependencyInformation) ? Strings.nullToEmpty(annotationDependencies) : modMetadata.printableSortingRules());
    284    }
    285
    286    @Override
    287    public boolean matches(Object mod)
    288    {
    289        return mod == modInstance;
    290    }
    291
    292    @Override
    293    public Object getMod()
    294    {
    295        return modInstance;
    296    }
    297
    298    @Override
    299    public boolean registerBus(EventBus bus, LoadController controller)
    300    {
    301        if (this.enabled)
    302        {
    303            FMLLog.log(getModId(), Level.FINE, "Enabling mod %s", getModId());
    304            this.eventBus = bus;
    305            this.controller = controller;
    306            eventBus.register(this);
    307            return true;
    308        }
    309        else
    310        {
    311            return false;
    312        }
    313    }
    314
    315    private Multimap<Class<? extends Annotation>, Object> gatherAnnotations(Class<?> clazz) throws Exception
    316    {
    317        Multimap<Class<? extends Annotation>,Object> anns = ArrayListMultimap.create();
    318
    319        for (Method m : clazz.getDeclaredMethods())
    320        {
    321            for (Annotation a : m.getAnnotations())
    322            {
    323                if (modTypeAnnotations.containsKey(a.annotationType()))
    324                {
    325                    Class<?>[] paramTypes = new Class[] { modTypeAnnotations.get(a.annotationType()) };
    326
    327                    if (Arrays.equals(m.getParameterTypes(), paramTypes))
    328                    {
    329                        m.setAccessible(true);
    330                        anns.put(a.annotationType(), m);
    331                    }
    332                    else
    333                    {
    334                        FMLLog.log(getModId(), Level.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));
    335                    }
    336                }
    337            }
    338        }
    339        return anns;
    340    }
    341
    342    private void processFieldAnnotations(ASMDataTable asmDataTable) throws Exception
    343    {
    344        SetMultimap<String, ASMData> annotations = asmDataTable.getAnnotationsFor(this);
    345
    346        parseSimpleFieldAnnotation(annotations, Instance.class.getName(), new Function<ModContainer, Object>()
    347        {
    348            public Object apply(ModContainer mc)
    349            {
    350                return mc.getMod();
    351            }
    352        });
    353        parseSimpleFieldAnnotation(annotations, Metadata.class.getName(), new Function<ModContainer, Object>()
    354        {
    355            public Object apply(ModContainer mc)
    356            {
    357                return mc.getMetadata();
    358            }
    359        });
    360    }
    361
    362    private void parseSimpleFieldAnnotation(SetMultimap<String, ASMData> annotations, String annotationClassName, Function<ModContainer, Object> retreiver) throws IllegalAccessException
    363    {
    364        String[] annName = annotationClassName.split("\\.");
    365        String annotationName = annName[annName.length - 1];
    366        for (ASMData targets : annotations.get(annotationClassName))
    367        {
    368            String targetMod = (String) targets.getAnnotationInfo().get("value");
    369            Field f = null;
    370            Object injectedMod = null;
    371            ModContainer mc = this;
    372            boolean isStatic = false;
    373            Class<?> clz = modInstance.getClass();
    374            if (!Strings.isNullOrEmpty(targetMod))
    375            {
    376                if (Loader.isModLoaded(targetMod))
    377                {
    378                    mc = Loader.instance().getIndexedModList().get(targetMod);
    379                }
    380                else
    381                {
    382                    mc = null;
    383                }
    384            }
    385            if (mc != null)
    386            {
    387                try
    388                {
    389                    clz = Class.forName(targets.getClassName(), true, Loader.instance().getModClassLoader());
    390                    f = clz.getDeclaredField(targets.getObjectName());
    391                    f.setAccessible(true);
    392                    isStatic = Modifier.isStatic(f.getModifiers());
    393                    injectedMod = retreiver.apply(mc);
    394                }
    395                catch (Exception e)
    396                {
    397                    Throwables.propagateIfPossible(e);
    398                    FMLLog.log(getModId(), Level.WARNING, e, "Attempting to load @%s in class %s for %s and failing", annotationName, targets.getClassName(), mc.getModId());
    399                }
    400            }
    401            if (f != null)
    402            {
    403                Object target = null;
    404                if (!isStatic)
    405                {
    406                    target = modInstance;
    407                    if (!modInstance.getClass().equals(clz))
    408                    {
    409                        FMLLog.log(getModId(), Level.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());
    410                        continue;
    411                    }
    412                }
    413                f.set(target, injectedMod);
    414            }
    415        }
    416    }
    417
    418    @Subscribe
    419    public void constructMod(FMLConstructionEvent event)
    420    {
    421        try
    422        {
    423            ModClassLoader modClassLoader = event.getModClassLoader();
    424            modClassLoader.addFile(source);
    425            Class<?> clazz = Class.forName(className, true, modClassLoader);
    426
    427            Certificate[] certificates = clazz.getProtectionDomain().getCodeSource().getCertificates();
    428            int len = 0;
    429            if (certificates != null)
    430            {
    431                len = certificates.length;
    432            }
    433            Builder<String> certBuilder = ImmutableList.<String>builder();
    434            for (int i = 0; i < len; i++)
    435            {
    436                certBuilder.add(CertificateHelper.getFingerprint(certificates[i]));
    437            }
    438
    439            ImmutableList<String> certList = certBuilder.build();
    440            sourceFingerprints = ImmutableSet.copyOf(certList);
    441
    442            String expectedFingerprint = (String) descriptor.get("certificateFingerprint");
    443
    444            fingerprintNotPresent = true;
    445
    446            if (expectedFingerprint != null && !expectedFingerprint.isEmpty())
    447            {
    448                if (!sourceFingerprints.contains(expectedFingerprint))
    449                {
    450                    Level warnLevel = Level.SEVERE;
    451                    if (source.isDirectory())
    452                    {
    453                        warnLevel = Level.FINER;
    454                    }
    455                    FMLLog.log(getModId(), warnLevel, "The mod %s is expecting signature %s for source %s, however there is no signature matching that description", getModId(), expectedFingerprint, source.getName());
    456                }
    457                else
    458                {
    459                    certificate = certificates[certList.indexOf(expectedFingerprint)];
    460                    fingerprintNotPresent = false;
    461                }
    462            }
    463
    464            annotations = gatherAnnotations(clazz);
    465            isNetworkMod = FMLNetworkHandler.instance().registerNetworkMod(this, clazz, event.getASMHarvestedData());
    466            modInstance = getLanguageAdapter().getNewInstance(this,clazz, modClassLoader);
    467            if (fingerprintNotPresent)
    468            {
    469                eventBus.post(new FMLFingerprintViolationEvent(source.isDirectory(), source, ImmutableSet.copyOf(this.sourceFingerprints), expectedFingerprint));
    470            }
    471            ProxyInjector.inject(this, event.getASMHarvestedData(), FMLCommonHandler.instance().getSide(), getLanguageAdapter());
    472            processFieldAnnotations(event.getASMHarvestedData());
    473        }
    474        catch (Throwable e)
    475        {
    476            controller.errorOccurred(this, e);
    477            Throwables.propagateIfPossible(e);
    478        }
    479    }
    480
    481    @Subscribe
    482    public void handleModStateEvent(FMLEvent event)
    483    {
    484        Class<? extends Annotation> annotation = modAnnotationTypes.get(event.getClass());
    485        if (annotation == null)
    486        {
    487            return;
    488        }
    489        try
    490        {
    491            for (Object o : annotations.get(annotation))
    492            {
    493                Method m = (Method) o;
    494                m.invoke(modInstance, event);
    495            }
    496        }
    497        catch (Throwable t)
    498        {
    499            controller.errorOccurred(this, t);
    500            Throwables.propagateIfPossible(t);
    501        }
    502    }
    503
    504    @Override
    505    public ArtifactVersion getProcessedVersion()
    506    {
    507        if (processedVersion == null)
    508        {
    509            processedVersion = new DefaultArtifactVersion(getModId(), getVersion());
    510        }
    511        return processedVersion;
    512    }
    513    @Override
    514    public boolean isImmutable()
    515    {
    516        return false;
    517    }
    518
    519    @Override
    520    public boolean isNetworkMod()
    521    {
    522        return isNetworkMod;
    523    }
    524
    525    @Override
    526    public String getDisplayVersion()
    527    {
    528        return modMetadata.version;
    529    }
    530
    531    @Override
    532    public VersionRange acceptableMinecraftVersionRange()
    533    {
    534        return minecraftAccepted;
    535    }
    536
    537    @Override
    538    public Certificate getSigningCertificate()
    539    {
    540        return certificate;
    541    }
    542
    543    @Override
    544    public String toString()
    545    {
    546        return "FMLMod:"+getModId()+"{"+getVersion()+"}";
    547    }
    548}