重复 mod 去重

基本信息

属性 值
触发类 ModCandidateV2Sorter.handleDuplicates
触发时机 ModCandidateV2Sorter.resolve() 第一阶段
排序器 传入 ModCandidateV2Sorter 的 Comparator<T>(mod 走 MOD_COMPARATOR,coremod 走 COREMOD_COMPARATOR)
豁免列表 CoremodExemptions(来自 jarjar_coremod_exemptions.cfg)

多个 jar 包含同一个 mod id(典型场景:嵌套 jar + 外层 jar;或 modpack 同时放入 dev/prod 两版)时 JarJar 自动保留版本号最高的一份。

算法

private void handleDuplicates() {
    final Map<String, List<T>> idLookup = new HashMap<>(candidates.size());
    for (T candidate : candidates) {
        idLookup.computeIfAbsent(candidate.getId(), _id -> new ArrayList<>(1)).add(candidate);
    }
    for (Map.Entry<String, List<T>> entry : idLookup.entrySet()) {
        final List<T> equalIdCandidates = entry.getValue();
        if (equalIdCandidates.size() < 2) continue;

        // 豁免判定:coremod 且类名在 CoremodExemptions 中 → 跳过整批
        boolean isExemptCoremod = false;
        if (equalIdCandidates.get(0) instanceof ModCandidateV2 firstCandidate && firstCandidate.hasCoreMod()) {
            String coremodClassName = firstCandidate.getCoreMod();
            if (CoremodExemptions.isExempt(coremodClassName)) {
                isExemptCoremod = true;
                LOGGER.info("Skipping duplicate detection for exempt coremod: {} (found in {} mods)",
                            coremodClassName, equalIdCandidates.size());
            }
        }
        if (isExemptCoremod) continue;

        // Multi-Release Jar 校验:同一 jar 文件不能重复
        final Set<File> files = new HashSet<>();
        for (final T it : equalIdCandidates) {
            if (!files.add(it.getFile())) {
                final String msg = String.format("Mod id %s found multiple times in the same jar %s - Likely a Multi-Release Jar issue", entry.getKey(), it.getFile());
                LOGGER.error(msg);
                throw new LoaderException(msg);
            }
        }

        // 选最高版本,禁用其余
        T newest = null;
        for (final T it : equalIdCandidates) {
            if (disabled.contains(it)) continue;
            if (newest == null) {
                newest = it;
            } else {
                if (newest.getVersion().compareTo(it.getVersion()) < 0) {
                    disabled.add(newest);
                    LOGGER.warn("Duplicate mod found: {}, disabling {} ({}) in favor of {} ({})", newest.getId(), newest.getVersion(), newest.getFile(), it.getVersion(), it.getFile());
                    newest = it;
                } else {
                    LOGGER.warn("Duplicate mod found: {}, disabling {} ({}) in favor of {} ({})", it.getId(), it.getVersion(), it.getFile(), newest.getVersion(), newest.getFile());
                    disabled.add(it);
                }
            }
        }
    }
    candidates.removeIf(disabled::contains);
}

关键规则

规则 行为
同 id 不同 jar 保留最高版本,其余 disabled 并在 getDisabledFiles() 输出
同 id 同 jar 抛 LoaderException("Mod id X found multiple times in the same jar Y - Likely a Multi-Release Jar issue")
coremod 豁免类 跳过整批判定,保留所有候选

disabled 输出

public Set<String> getDisabledFiles() {
    return disabled.stream()
        .map(t -> t.getFile().getName())
        .collect(Collectors.toSet());
}

CoreModManagerV2.loadTweakersAndCoreMods 在 sort 后立刻写 Launch.blackboard.put(DISABLED_FILES, candidateSorter.getDisabledFiles())。

DirectoryModCandidateFinder.getDisabledFiles() 后续从 blackboard 读出,用于 isValidFile 过滤 — 防止已禁用的 jar 在 mod 扫描阶段被二次加回。

警告日志

Duplicate mod found: <id>, disabling {minor} ({file}) in favor of {major} ({file})

相关条目