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