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