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