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