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