API 所有权排序
基本信息
| 属性 | 值 |
|---|---|
| 入口 | ModCandidateV2Sorter.computeClasspathOrder |
| 数据源 | @API 注解 + package-info.class 中声明的 apiVersion |
| 算法 | 拓扑排序 (Kahn) + 冲突 fallback 排序 |
| 调用时机 | ModCandidateV2Sorter.resolve() 末段 |
当 modpack 中存在"我打包了你 API 的全部内容"的 repackaging jar(典型:GregTech 风格的 slim jar),必须保证原版 API jar 先于这些 repackaging jar 加入 classpath,否则原版 jar 加载时会找不到自己的 package-info,触发 InvalidModFile。
三步走
Step 1:扫描每个候选的 declaredApiPackages
final Map<String, Integer> ownerByPkg = new HashMap<>();
final Map<String, DefaultArtifactVersion> ownerVersionByPkg = new HashMap<>();
for (int i = 0; i < n; i++) {
for (Map.Entry<String, DefaultArtifactVersion> e : ((ModCandidateV2) candidates.get(i)).getDeclaredApiPackages().entrySet()) {
final DefaultArtifactVersion cur = ownerVersionByPkg.get(e.getKey());
if (cur == null || e.getValue().compareTo(cur) > 0) {
ownerByPkg.put(e.getKey(), i);
ownerVersionByPkg.put(e.getKey(), e.getValue());
}
}
}
getDeclaredApiPackages() 来自 JarUtil.scanPackagesAndApi:扫描 jar 内每个 package-info.class,提取 @API(apiVersion="...")。
Step 2:基于 earlyPackages 构建 successor 图
final List<Set<Integer>> successors = new ArrayList<>(n);
for (int i = 0; i < n; i++) successors.add(new HashSet<>());
final int[] indegree = new int[n];
final Map<String, Set<String>> conflicts = new LinkedHashMap<>();
for (int i = 0; i < n; i++) {
for (String pkg : ((ModCandidateV2) candidates.get(i)).getEarlyPackages()) {
final Integer owner = ownerByPkg.get(pkg);
if (owner == null || owner == i) continue;
if (successors.get(owner).add(i)) indegree[i]++;
conflicts.computeIfAbsent(
((ModCandidateV2) candidates.get(owner)).getFilename() + " ahead of " + ((ModCandidateV2) candidates.get(i)).getFilename(),
k -> new TreeSet<>()).add(pkg);
}
}
候选 i 的 earlyPackages 包含的 pkg,若其它候选 j 声明了更高版本的 @API,则 i 必须在 j 之后。冲突原因记到日志。
Step 3:拓扑排序
final List<T> ordered = new ArrayList<>(n);
final boolean[] emitted = new boolean[n];
final PriorityQueue<Integer> ready = new PriorityQueue<>();
for (int i = 0; i < n; i++) if (indegree[i] == 0) ready.add(i);
while (!ready.isEmpty()) {
final int pick = ready.poll();
ordered.add(candidates.get(pick));
emitted[pick] = true;
for (int s : successors.get(pick)) {
if (--indegree[s] == 0) ready.add(s);
}
}
if (ordered.size() < n) {
LOGGER.warn("API ownership: cyclic API package ownership -- leaving remaining mods in resolved order");
for (int i = 0; i < n; i++) if (!emitted[i]) ordered.add(candidates.get(i));
}
return ordered;
PriorityQueue 自然按索引(原本比较器的相对顺序)排序,保证 output 稳定。若存在环,剩余节点按原序追加。
触发条件
computeClasspathOrder 在以下条件全部成立时启动:
candidates.size() >= 2- 所有
T都是ModCandidateV2 - 至少一个候选拥有
declaredApiPackages
否则直接返回 new ArrayList<>(candidates)。
输出用途
getClasspathOrder() 被 CoreModManagerV2.loadTweakersAndCoreMods 用于 addUrlToLoaderAndParent(classLoader, jar.toURI().toURL()) —— 把 jar URL 按这个顺序加入 classpath,保证 owner 先加载。
for (ModCandidateV2 candidate : candidateSorter.getClasspathOrder()) {
final File jar = candidate.getModContainer();
if (jar == null) continue;
try {
addUrlToLoaderAndParent(classLoader, jar.toURI().toURL());
} catch (MalformedURLException e) {
FMLRelaunchLog.log(Level.WARN, e, "Skipping bad URL for %s", jar);
}
}
相关条目
- ModCandidateV2Sorter - 调度
computeClasspathOrder - ModCandidateV2 数据模型 -
getDeclaredApiPackages/getEarlyPackages - JarUtil 嵌套抽取与版本识别 -
scanPackagesAndApi写这两个集合 - ASMDataTableV2 -
getAll("@API")按 apiVersion 倒序排