diff --git a/fml/common/cpw/mods/fml/common/API.java b/fml/common/cpw/mods/fml/common/API.java new file mode 100644 index 0000000000..b8a2b4b28a --- /dev/null +++ b/fml/common/cpw/mods/fml/common/API.java @@ -0,0 +1,14 @@ +package cpw.mods.fml.common; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PACKAGE) +public @interface API { + String owner(); + String provides(); + String apiVersion(); +} diff --git a/fml/common/cpw/mods/fml/common/Loader.java b/fml/common/cpw/mods/fml/common/Loader.java index 17fad9262a..55d1b86a77 100644 --- a/fml/common/cpw/mods/fml/common/Loader.java +++ b/fml/common/cpw/mods/fml/common/Loader.java @@ -482,12 +482,15 @@ public class Loader modController = new LoadController(this); modController.transition(LoaderState.LOADING, false); ModDiscoverer disc = identifyMods(); + ModAPIManager.INSTANCE.manageAPI(modClassLoader, disc); disableRequestedMods(); FMLLog.fine("Reloading logging properties from %s", loggingProperties.getPath()); FMLRelaunchLog.loadLogConfiguration(loggingProperties); FMLLog.fine("Reloaded logging properties"); modController.distributeStateMessage(FMLLoadEvent.class); sortModList(); + ModAPIManager.INSTANCE.cleanupAPIContainers(modController.getActiveModList()); + ModAPIManager.INSTANCE.cleanupAPIContainers(mods); mods = ImmutableList.copyOf(mods); for (File nonMod : disc.getNonModLibs()) { @@ -504,7 +507,6 @@ public class Loader } } } - ModAPIManager.INSTANCE.buildAPITransformer(modClassLoader, disc); modController.transition(LoaderState.CONSTRUCTING, false); modController.distributeStateMessage(LoaderState.CONSTRUCTING, modClassLoader, disc.getASMTable()); FMLLog.fine("Mod signature data"); diff --git a/fml/common/cpw/mods/fml/common/ModAPIManager.java b/fml/common/cpw/mods/fml/common/ModAPIManager.java index f8c04ddf07..eb714c941c 100644 --- a/fml/common/cpw/mods/fml/common/ModAPIManager.java +++ b/fml/common/cpw/mods/fml/common/ModAPIManager.java @@ -1,22 +1,199 @@ package cpw.mods.fml.common; +import java.io.File; +import java.security.acl.Owner; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; + import cpw.mods.fml.common.asm.transformers.ModAPITransformer; import cpw.mods.fml.common.discovery.ASMDataTable; +import cpw.mods.fml.common.discovery.ASMDataTable.ASMData; +import cpw.mods.fml.common.discovery.ModCandidate; import cpw.mods.fml.common.discovery.ModDiscoverer; +import cpw.mods.fml.common.functions.ModIdFunction; +import cpw.mods.fml.common.versioning.ArtifactVersion; +import cpw.mods.fml.common.versioning.DefaultArtifactVersion; +import cpw.mods.fml.common.versioning.VersionParser; public class ModAPIManager { public static final ModAPIManager INSTANCE = new ModAPIManager(); private ModAPITransformer transformer; private ASMDataTable dataTable; + private Map apiContainers; - public void registerDataTable(ASMDataTable dataTable) + private static class APIContainer extends DummyModContainer { + private List referredMods; + private ArtifactVersion ownerMod; + private ArtifactVersion ourVersion; + private String providedAPI; + private File source; + private String version; + private Set currentReferents; + private Set packages; + + public APIContainer(String providedAPI, String apiVersion, File source, ArtifactVersion ownerMod) + { + this.providedAPI = providedAPI; + this.version = apiVersion; + this.ownerMod = ownerMod; + this.ourVersion = new DefaultArtifactVersion(providedAPI, apiVersion); + this.referredMods = Lists.newArrayList(); + this.source = source; + this.currentReferents = Sets.newHashSet(); + this.packages = Sets.newHashSet(); + } + + @Override + public File getSource() + { + return source; + } + @Override + public String getVersion() + { + return version; + } + @Override + public String getName() + { + return "API: "+providedAPI; + } + @Override + public String getModId() + { + return "API:"+providedAPI; + } + @Override + public List getDependants() + { + return referredMods; + } + + @Override + public List getDependencies() + { + return ImmutableList.of(ownerMod); + } + + @Override + public ArtifactVersion getProcessedVersion() + { + return ourVersion; + } + + public void validate(String providedAPI, String apiOwner, String apiVersion) + { + // TODO Compare this annotation data to the one we first found. Maybe barf if there is inconsistency? + } + + @Override + public String toString() + { + return "APIContainer{"+providedAPI+":"+version+"}"; + } + + public void addAPIReference(String embedded) + { + currentReferents.add(embedded); + referredMods.add(VersionParser.parseVersionReference(embedded)); + } + + public void addOwnedPackage(String apiPackage) + { + packages.add(apiPackage); + } + + public void addAPIReferences(List candidateIds) + { + for (String modId : candidateIds) + { + addAPIReference(modId); + } + } + } + public void registerDataTableAndParseAPI(ASMDataTable dataTable) { this.dataTable = dataTable; + + Set apiList = dataTable.getAll("cpw.mods.fml.common.API"); + + apiContainers = Maps.newHashMap(); + + for (ASMData data : apiList) + { + Map annotationInfo = data.getAnnotationInfo(); + String apiPackage = data.getClassName().substring(0,data.getClassName().indexOf(".package-info")); + String providedAPI = (String) annotationInfo.get("provides"); + String apiOwner = (String) annotationInfo.get("owner"); + String apiVersion = (String) annotationInfo.get("apiVersion"); + APIContainer container = apiContainers.get(providedAPI); + if (container == null) + { + container = new APIContainer(providedAPI, apiVersion, data.getCandidate().getModContainer(), VersionParser.parseVersionReference(apiOwner)); + apiContainers.put(providedAPI, container); + } + else + { + container.validate(providedAPI, apiOwner, apiVersion); + } + container.addOwnedPackage(apiPackage); + for (ModContainer mc : data.getCandidate().getContainedMods()) + { + String embeddedIn = mc.getModId(); + FMLLog.fine("Found API %s (owned by %s providing %s) embedded in %s",apiPackage, apiOwner, providedAPI, embeddedIn); + if (!embeddedIn.equals(apiOwner)) + { + container.addAPIReference(embeddedIn); + } + } + } + + for (APIContainer container : apiContainers.values()) + { + for (String pkg : container.packages) + { + Set candidates = dataTable.getCandidatesFor(pkg); + for (ModCandidate candidate : candidates) + { + List candidateIds = Lists.transform(candidate.getContainedMods(), new ModIdFunction()); + if (!candidateIds.contains(container.ownerMod.getLabel()) && !container.currentReferents.containsAll(candidateIds)) + { + FMLLog.info("Found mod(s) %s containing declared API package %s (owned by %s) without associated API reference",candidateIds, pkg, container.ownerMod); + container.addAPIReferences(candidateIds); + } + } + } + FMLLog.fine("Creating API container dummy for API %s: owner: %s, dependents: %s", container.providedAPI, container.ownerMod, container.referredMods); + } } - public void buildAPITransformer(ModClassLoader modClassLoader, ModDiscoverer discoverer) + public void manageAPI(ModClassLoader modClassLoader, ModDiscoverer discoverer) { - registerDataTable(discoverer.getASMTable()); + registerDataTableAndParseAPI(discoverer.getASMTable()); transformer = modClassLoader.addModAPITransformer(dataTable); } + + public void injectAPIModContainers(List mods, Map nameLookup) + { + mods.addAll(apiContainers.values()); + nameLookup.putAll(apiContainers); + } + + public void cleanupAPIContainers(List mods) + { + mods.removeAll(apiContainers.values()); + } + + public boolean hasAPI(String modId) + { + return apiContainers.containsKey(modId); + } } diff --git a/fml/common/cpw/mods/fml/common/asm/transformers/ModAPITransformer.java b/fml/common/cpw/mods/fml/common/asm/transformers/ModAPITransformer.java index 62f5730c15..5a0330d8a7 100644 --- a/fml/common/cpw/mods/fml/common/asm/transformers/ModAPITransformer.java +++ b/fml/common/cpw/mods/fml/common/asm/transformers/ModAPITransformer.java @@ -20,6 +20,7 @@ import com.google.common.collect.Sets; import cpw.mods.fml.common.FMLLog; import cpw.mods.fml.common.Loader; +import cpw.mods.fml.common.ModAPIManager; import cpw.mods.fml.common.Optional; import cpw.mods.fml.common.discovery.ASMDataTable; import cpw.mods.fml.common.discovery.ASMDataTable.ASMData; @@ -29,7 +30,7 @@ import net.minecraft.launchwrapper.IClassTransformer; public class ModAPITransformer implements IClassTransformer { - private static final boolean logDebugInfo = Boolean.valueOf(System.getProperty("fml.debugAPITransformer", "false")); + private static final boolean logDebugInfo = Boolean.valueOf(System.getProperty("fml.debugAPITransformer", "true")); private ListMultimap optionals; @Override @@ -48,7 +49,7 @@ public class ModAPITransformer implements IClassTransformer { { String modId = (String) optional.getAnnotationInfo().get("modid"); - if (Loader.isModLoaded(modId)) + if (Loader.isModLoaded(modId) || ModAPIManager.INSTANCE.hasAPI(modId)) { if (logDebugInfo) FMLRelaunchLog.finest("Optional removal skipped - mod present %s", modId); continue; diff --git a/fml/common/cpw/mods/fml/common/discovery/ASMDataTable.java b/fml/common/cpw/mods/fml/common/discovery/ASMDataTable.java index 308e6a76fb..1ec55964ea 100644 --- a/fml/common/cpw/mods/fml/common/discovery/ASMDataTable.java +++ b/fml/common/cpw/mods/fml/common/discovery/ASMDataTable.java @@ -17,6 +17,7 @@ import java.util.Map; import java.util.Set; import com.google.common.base.Predicate; +import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSetMultimap; @@ -96,6 +97,7 @@ public class ASMDataTable private Map> containerAnnotationData; private List containers = Lists.newArrayList(); + private SetMultimap packageMap = HashMultimap.create(); public SetMultimap getAnnotationsFor(ModContainer container) { @@ -126,4 +128,14 @@ public class ASMDataTable { this.containers.add(container); } + + public void registerPackage(ModCandidate modCandidate, String pkg) + { + this.packageMap.put(pkg,modCandidate); + } + + public Set getCandidatesFor(String pkg) + { + return this.packageMap.get(pkg); + } } diff --git a/fml/common/cpw/mods/fml/common/discovery/ModCandidate.java b/fml/common/cpw/mods/fml/common/discovery/ModCandidate.java index 172526116f..7581f67ca5 100644 --- a/fml/common/cpw/mods/fml/common/discovery/ModCandidate.java +++ b/fml/common/cpw/mods/fml/common/discovery/ModCandidate.java @@ -34,6 +34,9 @@ public class ModCandidate private boolean isMinecraft; private List baseModCandidateTypes = Lists.newArrayListWithCapacity(1); private Set foundClasses = Sets.newHashSet(); + private List mods; + private List packages = Lists.newArrayList(); + private ASMDataTable table; public ModCandidate(File classPathRoot, File modContainer, ContainerType sourceType) { @@ -64,22 +67,28 @@ public class ModCandidate } public List explore(ASMDataTable table) { - List mods = sourceType.findMods(this, table); + this.table = table; + this.mods = sourceType.findMods(this, table); if (!baseModCandidateTypes.isEmpty()) { FMLLog.info("Attempting to reparse the mod container %s", getModContainer().getName()); - return sourceType.findMods(this, table); - } - else - { - return mods; + this.mods = sourceType.findMods(this, table); } + return this.mods; } public void addClassEntry(String name) { - String className = name.substring(0, name.lastIndexOf('.')).replace('.', '/'); // strip the .class and . -> / - foundClasses.add(className); + String className = name.substring(0, name.lastIndexOf('.')); // strip the .class + foundClasses.add(className.replace('.', '/')); + className = className.replace('/','.'); + int pkgIdx = className.lastIndexOf('.'); + if (pkgIdx > -1) + { + String pkg = className.substring(0,pkgIdx); + packages.add(pkg); + this.table.registerPackage(this,pkg); + } } public boolean isClasspath() @@ -106,4 +115,12 @@ public class ModCandidate { return foundClasses; } + public List getContainedMods() + { + return mods; + } + public List getContainedPackages() + { + return packages; + } } diff --git a/fml/common/cpw/mods/fml/common/toposort/ModSorter.java b/fml/common/cpw/mods/fml/common/toposort/ModSorter.java index bbdaa4e521..491f9800a0 100644 --- a/fml/common/cpw/mods/fml/common/toposort/ModSorter.java +++ b/fml/common/cpw/mods/fml/common/toposort/ModSorter.java @@ -13,13 +13,16 @@ package cpw.mods.fml.common.toposort; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import cpw.mods.fml.common.DummyModContainer; import cpw.mods.fml.common.Loader; +import cpw.mods.fml.common.ModAPIManager; import cpw.mods.fml.common.ModContainer; import cpw.mods.fml.common.toposort.TopologicalSort.DirectedGraph; import cpw.mods.fml.common.versioning.ArtifactVersion; @@ -39,7 +42,9 @@ public class ModSorter public ModSorter(List modList, Map nameLookup) { - buildGraph(modList, nameLookup); + HashMap sortingNameLookup = Maps.newHashMap(nameLookup); + ModAPIManager.INSTANCE.injectAPIModContainers(modList, sortingNameLookup); + buildGraph(modList, sortingNameLookup); } private void buildGraph(List modList, Map nameLookup) @@ -85,7 +90,7 @@ public class ModSorter else { modGraph.addEdge(before, mod); - if (Loader.isModLoaded(modid)) { + if (nameLookup.containsKey(modid) || Loader.isModLoaded(modid)) { modGraph.addEdge(nameLookup.get(modid), mod); } }