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 }