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 }