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