Skip to content

模组冲突 ​

mods/ 目录里塞了一堆 jar 之后,最常见的两个问题:同一个模组装了好几个版本,或者 Fabric 和 Forge 的模组混在一起。ModConflictService 负责把这两类问题找出来,并且——只对其中一类——自动处理。

从 jar 里读哪些元数据 ​

模组的元数据在 jar 内部,位置由加载器决定:

文件加载器取的字段
fabric.mod.jsonFabricid / name / version,depends.minecraft
quilt.mod.jsonQuiltquilt_loader.id / quilt_loader.metadata.name / quilt_loader.version
META-INF/mods.tomlForge[[mods]] 段里的 modId / displayName / version
mcmod.infoForge 老包数组第一项的 modid / name / version / mcversion

解析顺序是 fabric → quilt → forge,命中即止。但 hasForgeMeta 的判断同时看 mods.toml 和 mcmod.info:

csharp
var hasFabricMeta = jar.GetEntry("fabric.mod.json") is not null;
var hasForgeMeta = jar.GetEntry("META-INF/mods.toml") is not null
                   || jar.GetEntry("mcmod.info") is not null;

如果一个 jar 同时有 fabric.mod.json 和 Forge 元数据,加载器标成 fabric+forge,这本身就是一条冲突。

NeoForge 的元数据文件目前读不到

NeoForge 用的是 META-INF/neoforge.mods.toml,而代码只判断 META-INF/mods.toml。纯 NeoForge 模组会被判成"未识别到 mod 元数据",其 modId 也拿不到——于是同一个模组装了两个版本不会被判为"重复安装"。

TOML 没有 JSON 那么好读,所以 mods.toml 是逐行扫的:找 [[dependencies.*]] 段里 modId="minecraft" 那一行的 versionRange。同理由 [[mods]] 段取 modId/displayName/version(含 $ 的版本号是构建期变量,跳过)。

检出的四类冲突 ​

BuildConflicts 产出四种:

  • 加载器混装 —— mods 目录里同时有 Fabric 与 Forge 模组。Quilt 兼容 Fabric 生态,所以 PrimaryLoader 把 quilt 归到 fabric。
  • 多加载器混合包 —— 单个 jar 同时含两套元数据,属于异常文件。
  • 重复安装 —— 同一个 modId 出现多个版本。
  • 文件损坏 —— jar 打不开(ZipFile.OpenRead 抛异常)。

前两类和第四类都需要用户自己判断,我们不给自动解。

只自动处理"重复安装" ​

csharp
foreach (var conflict in report.Conflicts.Where(c => c.Kind == "重复安装"))
{
    var group = report.Mods
        .Where(m => conflict.Files.Contains(m.FileName))
        .OrderByDescending(m => m.ModifiedAt).ThenBy(m => m.FileName)
        .ToList();
    foreach (var old in group.Skip(1))
    {
        var target = old.FullPath + ".disabled";
        if (File.Exists(target)) File.Delete(target);   // 之前的停用残留,覆盖
        File.Move(old.FullPath, target);
    }
}

规则是按文件修改时间保留最新的,其余改名 .jar.disabled。同名排序用文件名兜底,保证结果确定。

为什么只做这一类?因为这是唯一一类判断依据完全在文件本身的冲突:同一个 modId 的两个版本,改动时间能告诉我们哪个是后来装的。混装不是这样——Fabric 和 Forge 的模组没有"哪个更好",删哪个都得问用户。自动改用户文件是有风险的,我们的原则是:能确定的就做,不能确定的只给说明。

csharp
var manual = report.Conflicts.Where(c => c.Kind != "重复安装").ToList();
if (manual.Count > 0)
{
    sb.AppendLine("以下冲突需要手动确认,无法自动处理:");
    foreach (var c in manual) sb.AppendLine($"• [{c.Kind}] {c.Detail}");
}

.disabled 而不是删除,是因为用户随时可能想装回来;改名是可逆的。

.disabled 仍然算"已装"

依赖检查枚举 mods 目录时用的是 f.Contains(".jar"),而 foo.jar.disabled 里也含 .jar——所以被停用的模组仍然会被算进"已装集合",依赖检查不会报它缺失。

这其实是我们想要的:停用不等于卸载,用户可能就是临时关掉一个模组。但如果某个模组被停用了、另一个模组又依赖它,依赖检查会说"依赖已满足",而游戏里其实缺了它。要严格判断的话,得在枚举时排除 .jar.disabled 后缀——我们目前没做,因为停用文件的存在本身通常就意味着"用户自己知道自己在干什么"。

相关链接 ​

基于 MIT 许可证(有修改)开源