Skip to content

整合包格式与导入导出 ​

整合包有好几种格式,字段名互不通用,但用户只想"拖进来就能玩"。这一页讲我们怎么认出包、怎么装、以及导出时怎么把文件还原成"可下载"的形态。

三种格式 ​

Modrinth .mrpack —— 一个 zip,根目录有 modrinth.index.json:

json
{
  "formatVersion": 1,
  "gameVersion": "1.20.1",
  "name": "My Pack",
  "versionId": "1.0.0",
  "dependencies": { "minecraft": "1.20.1", "fabric-loader": "0.15.11" },
  "files": [
    {
      "path": "mods/sodium-fabric-0.5.8.jar",
      "hashes": { "sha1": "9C2B..." },
      "downloads": ["https://cdn.modrinth.com/data/..."],
      "fileSize": 1548210
    }
  ]
}

再加上 overrides/(和可选的 client-overrides/)目录,内容直接铺到实例根。CurseForge —— 一个 zip,根目录有 manifest.json:

json
{
  "minecraft": { "version": "1.20.1",
                 "modLoaders": [ { "id": "forge-47.2.0", "primary": true } ] },
  "name": "My Pack",
  "version": "1.0.0",
  "files": [ { "projectID": 394468, "fileID": 8756581, "required": true } ],
  "overrides": "overrides"
}

它不给 URL,只有 projectID / fileID,装的时候要回官方 API 查文件名和直链。

MultiMC / Prism —— 实例包,自包含:instance.cfg(INI,含 name=)、mmc-pack.json({ "components": [ { "uid": "net.minecraft", "version": "1.20.1" } ] })、以及 .minecraft/** 游戏内容。components[].uid 映射到加载器依赖:net.minecraft → minecraft、net.fabricmc.fabric-loader → fabric-loader。

三种格式的安装结果统一成 MrPackInstallResult,下游的加载器联动逻辑一行不用改。

按内容嗅探,不靠扩展名 ​

扩展名骗人:CurseForge 的包也是 .zip,有些作者还会把整包套一层目录。ModPackDetector 因此看内容:

csharp
ModPackFormat? format = leaf.ToLowerInvariant() switch
{
    "modrinth.index.json" => ModPackFormat.Modrinth,
    "manifest.json" => LooksLikeCurseForge(entry) ? ModPackFormat.CurseForge : null,
    "mmc-pack.json" or "instance.cfg" => ModPackFormat.MultiMc,
    _ => null,
};

manifest.json 必须带 minecraft 字段

manifest.json 是个很常见的文件名,很多整合包用它装自己的配置。所以只有 JSON 里有 minecraft 字段才算 CurseForge 包:

csharp
return doc.RootElement.ValueKind == JsonValueKind.Object
       && doc.RootElement.TryGetProperty("minecraft", out _);

每种格式只保留深度最浅的候选目录,"外层多套了一层"的情况能自动拍平(ModPackArchive.FlattenIfNested 重打成根级包)。识别优先级是 Modrinth → CurseForge → MultiMC。都没有的话走 Raw:按 .minecraft/ > minecraft/ > mods/ 这样的标记打分找内容根。

三个编码坑 ​

其一,zip 里的文件名编码。 PackZip.Open 先按 UTF-8 打开,看条目名里有没有替换字符 �,有就说明这个包没打 UTF-8 标志位:

csharp
static PackZip() => Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

public static ZipArchive Open(string path)
{
    var utf8 = ZipFile.Open(path, ZipArchiveMode.Read);
    if (!utf8.Entries.Any(e => e.FullName.Contains('�'))) return utf8;
    utf8.Dispose();
    try
    {
        return ZipFile.Open(path, ZipArchiveMode.Read, Encoding.GetEncoding(936));  // GBK
    }
    catch { return ZipFile.Open(path, ZipArchiveMode.Read); }
}

CodePagesEncodingProvider 必须注册,否则 .NET 上 Encoding.GetEncoding(936) 直接抛 NotSupportedException。不这么做的后果是 overrides/ 里的中文文件名全变乱码——而且不报错,只是文件名难看。

其二,JSON 的 BOM。 System.Text.Json 遇到 UTF-8 BOM 会直接抛异常(把 BOM 当成非法的起始字符)。很多中文整合包的 manifest.json 带 BOM,所以包内 JSON 统一走 PackJson(StreamReader 去 BOM 后再解析)。

其三,CurseForge 的文件名来自 API 响应,同样是不可信输入,落盘前压成单段安全名:PackPathGuard.SafeFileName(pf.Filename, "mod.jar")。

路径安全 ​

整合包是第三方产物,不能当可信输入。PackPathGuard.Normalize 做字面检查——绝对路径、盘符、含 .. 的段、含 : 的段,一律返回 null;Resolve 再做一次 GetFullPath 前缀校验,防 zip-slip(a/../../b):

csharp
var rootFull = Path.GetFullPath(root);
var full = Path.GetFullPath(Path.Combine(rootFull, norm.Replace('/', Path.DirectorySeparatorChar)));
var prefix = rootFull.EndsWith(Path.DirectorySeparatorChar) ? rootFull : rootFull + Path.DirectorySeparatorChar;
return full.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) ? full : null;

mrpack 的 files[].path 是包作者可写的字段,下载前也必须过 PackPathGuard.Resolve。

导出:把文件还原成来源 ​

导出时,我们要尽量让文件变成"别人能下载"的形态,而不是把整个实例压进去。反查方式按目标格式分:Modrinth 按 SHA1 走 POST /v2/version_files,命中就写进 files[];CurseForge 按 murmur2 指纹走 POST /mods/fingerprints,命中就写进 manifest.json 的 files[];MultiMC 不反查,全部打进 .minecraft/——它本来就是自包含的。

反查不到的文件由 UnresolvedAsOverrides 决定:开(默认)就进 overrides,包能用但体积大;关掉就丢弃。丢不丢由用户选——我们不替用户决定什么该被丢掉。反查失败也不阻断导出。

一个文件可能同时反查到两边

SHA1 和指纹是两套独立反查。同一份文件在 Modrinth 上叫 A、在 CurseForge 上叫 B,两个都能命中。我们不做"选一个"的仲裁,各自按自己格式写进各自的索引。

装不上也不该整个失败 ​

CurseForge 包里有些文件因为作者没开 allowModDistribution,官方 API 根本不给直链。这类文件我们跳过并计入诊断,不让整包装配失败:

csharp
if (version?.PrimaryFile is not { } pf || string.IsNullOrWhiteSpace(pf.Url))
{
    skipped++;
    LauncherLog.Write("curseforge",
        $"整合包文件无法解析,已跳过:project={file.ProjectId} file={file.FileId}");
    continue;
}

一个整合包十个文件缺一个,剩下九个照装,用户还能玩;整个包失败则一个都装不上。

相关链接 ​

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