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}