重复 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})
相关条目
- Coremod 豁免清单 - 提供
isExempt - ModCandidateV2Sorter - 调度 handleDuplicates
- LoaderV2 -
sortModList()调用 sorter - CoreModManagerV2 - 写 disabledFiles blackboard