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);
    }
}

相关条目