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 }