Skip to content

统一内容库 ​

启动器会下载模组、资源包、光影包、数据包、整合包。这几类东西如果各有一套下载/去重/装载逻辑,加一个来源就得改五处。所以我们把它们统一进一个「内容库」。

目录结构 ​

%APPDATA%/starcloud-launcher/library/
├── library.json      索引(所有条目的元数据)
├── mods/
├── resourcepacks/
├── shaderpacks/
├── datapacks/
└── modpacks/

kind 到目录名是一张固定映射表,落盘目录和实例内的目标目录都用它:

csharp
private static readonly Dictionary<string, string> KindDirs = new()
{
    ["mod"] = "mods",
    ["resourcepack"] = "resourcepacks",
    ["shader"] = "shaderpacks",
    ["datapack"] = "datapacks",
    ["modpack"] = "modpacks",
};

统一的意义在装载侧最明显。InstallToVersionAsync 对四类内容做的是同一件事——按 kind 找目标目录,把库内文件复制过去:

csharp
var dir = Path.Combine(gameRoot, InstanceLayout.ContentDirFor(entry.Kind));

ContentDirFor 是同一张表(mod → mods、resourcepack → resourcepacks……)。资源包和光影包在 Minecraft 里的目录名跟库里不一样,但差异被收在这两张表里,其他代码只认 kind。

为什么不直接下到实例目录

同一个模组装进两个版本就要下载两次;装进实例后想更新还得先找到"当初下的是哪个文件"。库是唯一副本,实例里的是复制品。

入库:SHA1 是身份 ​

AdoptAsync 先算文件 SHA1,然后分三种情况:

csharp
var sha1 = Convert.ToHexString(SHA1.HashData(File.ReadAllBytes(sourcePath)));

if (FindBySha1(sha1) is { } existing)
{
    try { File.Delete(sourcePath); } catch { }
    return existing;                       // 同 SHA1 → 返回既有条目
}

var target = Path.Combine(dir, Path.GetFileName(sourcePath));
if (File.Exists(target))
    target = InstanceLayout.NextAvailablePath(target);   // 同名不同 SHA1 → " (n)"

也就是说:内容相同就是同一个东西,哪怕文件名不同、来源不同;内容不同但文件名相同,就加后缀(NextAvailablePath:mods → mods (2)、file.txt → file (2).txt)。

去重键是内容哈希而不是文件名。文件名经常被作者改(mod-1.0.jar → mod-1.0-hotfix.jar),按名字去重会把同一个模组收成两份。

RescanAsync 负责修索引与实际文件的不一致:目录里有但索引里没有的补进来,索引里有但文件没了的删掉。索引是派生数据,文件才是真相。## 身份反查与负缓存

库里的文件可能来自任何来源,我们要回答"这个 jar 是哪个 Modrinth 项目、哪个版本"——更新检查和依赖检查都靠这个。做法是按 SHA1 反查:

csharp
var found = await _mods.GetVersionsByHashesAsync(pending, ct);
foreach (var sha1 in pending)
{
    if (found.TryGetValue(sha1, out var version))
        _identityCache[sha1] = new ModrinthIdentity(...);
    else
        _identityCache[sha1] = null;   // ← 负缓存
}

负缓存是关键

_identityCache 是 Dictionary<string, ModrinthIdentity?>——查不到也写一条 null。没有负缓存的话,每次打开模组列表都会把几十个本地文件的 SHA1 发给 Modrinth,而其中大部分是自制模组、配置备份、根本不在 Modrinth 上的东西,永远查不到,于是永远重复请求。

这个缓存是进程内的,不落盘。落盘收益不大:Modrinth 的项目不会从"存在"变成"不存在",真正会变的是版本更新,那由 CheckUpdateAsync 单独查。依赖检查直接复用这个反查结果,把已装文件(含 .disabled)的 SHA1 换成 projectId 集合,再比对三种依赖类型。

图标磁盘缓存 ​

社区资源的封面图标来自 CDN,每次滑动列表都重新请求会很吵。缓存落在 cache/icons/,一 URL 一文件,文件名是 URL 的 SHA1(小写十六进制):

csharp
var hash = Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(url))).ToLowerInvariant();
var ext = ".png";
if (Uri.TryCreate(url, UriKind.Absolute, out var uri))
{
    var fromUrl = Path.GetExtension(uri.AbsolutePath).ToLowerInvariant();
    if (KnownExtensions.Contains(fromUrl)) ext = fromUrl;
}
return hash + ext;

用 URL 的哈希命名而不是 URL 本身,是因为 URL 里有 ?、/、: 这些不能进文件名的字符;扩展名从 URL 里取(只认 png/jpg/jpeg/webp/gif/bmp),取不到就用 .png。

命中缓存后不再联网。写入是后台的,失败静默忽略、下次再试,_inflight 集合保证同一 URL 同时只跑一次。整个缓存受 ResourceCacheEnabled 开关控制;关闭时既不读也不写,界面直连图标地址。

相关链接 ​

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