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