using System.IO; using System.IO.Compression; using System.Text; using System.Text.RegularExpressions; using fNbt; using PCL.Core.App; using PCL.Core.App.Localization; using PCL.Core.Utils; using PCL.Core.Utils.Exts; using PCL.Core.Utils.Hash; using static PCL.ModComp; using static PCL.ModLoader; namespace PCL; public static class ModLocalComp { private const int localModCacheVersion = 7; private static readonly Lazy _hashCache = new(() => new HashCache(ModBase.pathTemp + @"Cache\HashCache.db")); public class LocalCompFile { /// /// 是否可能为前置 Mod。 /// public bool IsPresetMod() { return !Dependencies.Any() && Name is not null && (Name.ToLower().Contains("core") || Name.ToLower().Contains("lib")); } /// /// 根据完整文件路径的文件扩展名判断是否为 Mod 文件。 /// public static bool IsModFile(string path) { if (path is null || !path.Contains(".")) return false; path = path.ToLower(); if (path.EndsWithF(".jar", true) || path.EndsWithF(".zip", true) || path.EndsWithF(".litemod", true) || path.EndsWithF(".jar.disabled", true) || path.EndsWithF(".zip.disabled", true) || path.EndsWithF(".litemod.disabled", true) || path.EndsWithF(".jar.old", true) || path.EndsWithF(".zip.old", true) || path.EndsWithF(".litemod.old", true)) return true; return false; } /// /// 检查是否为指定类型的组件文件。 /// public static bool IsCompFile(string path, CompType compType) { if (path is null || !path.Contains(".")) return false; path = path.ToLower(); switch (compType) { case CompType.Mod: { return IsModFile(path); } case CompType.ResourcePack: case CompType.Shader: { return path.EndsWithF(".zip", true); } case CompType.DataPack: { return path.EndsWithF(".zip", true) || path.EndsWithF(".zip.disabled", true); } case CompType.Schematic: { return path.EndsWithF(".litematic", true) || path.EndsWithF(".nbt", true) || path.EndsWithF(".schematic", true) || path.EndsWithF(".schem", true); } default: { return false; } } } /// /// 获取图标路径。 /// public string GetLogo() { if (Comp is not null && Comp.LogoUrl is not null) return Comp.LogoUrl; if (Logo is not null) return Logo; // 为文件夹设置特定图标 if (IsFolder) return "pack://application:,,,/images/Icons/Folder.png"; return ModBase.pathImage + "Icons/NoIcon.png"; } #region Litematic 文件处理 /// /// 读取 Litematic 文件的 NBT 数据。 /// private void LoadLitematicNbtData() { try { ModBase.Log($"开始读取 Litematic NBT 数据:{path}", ModBase.LogLevel.Debug); using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) { var scheNbt = new NbtFile(); scheNbt.LoadFromStream(fs, NbtCompression.AutoDetect); // 读取版本信息 var versionTag = (NbtInt)scheNbt.RootTag.Get("Version"); if (versionTag is not null) _litematicVersion = versionTag.Value; // 读取 Metadata 节点 var metadataTag = scheNbt.RootTag.Get("Metadata"); if (metadataTag is not null) { ModBase.Log("找到 Litematic Metadata 节点", ModBase.LogLevel.Debug); // 读取名称 var nameTag = metadataTag.Get("Name"); if (nameTag is not null && !string.IsNullOrWhiteSpace(nameTag.Value) && nameTag.Value != "Unnamed") _litematicOriginalName = nameTag.Value; // 读取描述信息 var descriptionTag = metadataTag.Get("Description"); if (descriptionTag is not null && !string.IsNullOrWhiteSpace(descriptionTag.Value)) _Description = descriptionTag.Value; // 读取作者信息 var authorTag = metadataTag.Get("Author"); if (authorTag is not null && !string.IsNullOrWhiteSpace(authorTag.Value)) _Authors = authorTag.Value; // 读取时间信息 var timeCreatedTag = metadataTag.Get("TimeCreated"); if (timeCreatedTag is not null) _litematicTimeCreated = timeCreatedTag.Value; var timeModifiedTag = metadataTag.Get("TimeModified"); if (timeModifiedTag is not null) _litematicTimeModified = timeModifiedTag.Value; // 读取包围盒大小 var enclosingSizeTag = metadataTag.Get("EnclosingSize"); if (enclosingSizeTag is not null) { var xTag = enclosingSizeTag.Get("x"); var yTag = enclosingSizeTag.Get("y"); var zTag = enclosingSizeTag.Get("z"); if (xTag is not null && yTag is not null && zTag is not null) _litematicEnclosingSize = $"{xTag.Value} × {yTag.Value} × {zTag.Value}"; } // 读取区域数量 var regionCountTag = metadataTag.Get("RegionCount"); if (regionCountTag is not null) _litematicRegionCount = regionCountTag.Value; // 读取总方块数 var totalBlocksTag = metadataTag.Get("TotalBlocks"); if (totalBlocksTag is not null) _litematicTotalBlocks = totalBlocksTag.Value; // 读取总体积 var totalVolumeTag = metadataTag.Get("TotalVolume"); if (totalVolumeTag is not null) _litematicTotalVolume = totalVolumeTag.Value; } else { ModBase.Log("未找到 Litematic Metadata 节点", ModBase.LogLevel.Debug); } } ModBase.Log("Litematic NBT 数据读取完成", ModBase.LogLevel.Debug); } catch (Exception ex) { ModBase.Log(ex, "读取 Litematic NBT 数据时出错(" + path + ")"); } } #endregion #region Schem 文件处理 /// /// 读取 .schem 文件的 NBT 数据(Sponge Schematic 格式)。 /// private void LoadSchemNbtData() { try { ModBase.Log($"开始读取 Schem NBT 数据:{path}", ModBase.LogLevel.Debug); // 使用自动检测压缩格式 using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) { var scheNbt = new NbtFile(); scheNbt.LoadFromStream(fs, NbtCompression.AutoDetect); // 读取Sponge版本信息 var versionTag = scheNbt.RootTag.Get("Version"); if (versionTag is not null) _spongeVersion = versionTag.Value; // 读取数据版本信息 var dataVersionTag = scheNbt.RootTag.Get("DataVersion"); if (dataVersionTag is not null) _structureDataVersion = dataVersionTag.Value; // 读取尺寸信息 var widthTag = scheNbt.RootTag.Get("Width"); var heightTag = scheNbt.RootTag.Get("Height"); var lengthTag = scheNbt.RootTag.Get("Length"); if (widthTag is not null && heightTag is not null && lengthTag is not null) { _litematicEnclosingSize = $"{widthTag.Value} × {heightTag.Value} × {lengthTag.Value}"; _litematicTotalVolume = (short)(widthTag.Value * heightTag.Value) * lengthTag.Value; // 对于Sponge格式,方块数量等于总体积(因为包含空气方块) _litematicTotalBlocks = _litematicTotalVolume; } // 读取调色板信息来计算区域数量 var paletteTag = scheNbt.RootTag.Get("Palette"); if (paletteTag is not null) _litematicRegionCount = 1; // Sponge Schematic 通常只有一个区域 // 读取元数据 var metadataTag = scheNbt.RootTag.Get("Metadata"); if (metadataTag is not null) { // 读取名称 var nameTag = metadataTag.Get("Name"); if (nameTag is not null && !string.IsNullOrWhiteSpace(nameTag.Value)) _schemOriginalName = nameTag.Value; // 读取作者信息 var authorTag = metadataTag.Get("Author"); if (authorTag is not null && !string.IsNullOrWhiteSpace(authorTag.Value)) { _structureAuthor = authorTag.Value; if (_Authors is null) _Authors = _structureAuthor; } } } ModBase.Log("Schem NBT 数据读取完成", ModBase.LogLevel.Debug); } catch (Exception ex) { ModBase.Log(ex, "读取 Schem NBT 数据时出错(" + path + ")"); } } #endregion #region Schematic 文件处理 /// /// 读取 .schematic 文件的 NBT 数据(MCEdit/WorldEdit 格式)。 /// private void LoadSchematicNbtData() { try { ModBase.Log($"开始读取 Schematic NBT 数据:{path}", ModBase.LogLevel.Debug); using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) { var scheNbt = new NbtFile(); scheNbt.LoadFromStream(fs, NbtCompression.AutoDetect); // 读取尺寸信息 var widthTag = scheNbt.RootTag.Get("Width"); var heightTag = scheNbt.RootTag.Get("Height"); var lengthTag = scheNbt.RootTag.Get("Length"); if (widthTag is not null && heightTag is not null && lengthTag is not null) { _litematicEnclosingSize = $"{widthTag.Value} × {heightTag.Value} × {lengthTag.Value}"; _litematicTotalVolume = (short)(widthTag.Value * heightTag.Value) * lengthTag.Value; } // 读取材料列表 var materialsTag = scheNbt.RootTag.Get("Materials"); if (materialsTag is not null) ModBase.Log($"Schematic 材料类型:{materialsTag.Value}", ModBase.LogLevel.Debug); ModBase.Log("Schematic NBT 数据读取完成", ModBase.LogLevel.Debug); } } catch (Exception ex) { ModBase.Log(ex, "读取 Schematic NBT 数据时出错(" + path + ")"); } } #endregion #region NBT 结构文件处理 /// /// 读取 .nbt 文件的 NBT 数据(Minecraft 结构文件格式)。 /// private void LoadStructureNbtData() { try { ModBase.Log($"开始读取 NBT 结构文件数据:{path}", ModBase.LogLevel.Debug); using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) { var scheNbt = new NbtFile(); scheNbt.LoadFromStream(fs, NbtCompression.AutoDetect); // 读取作者信息 var authorTag = scheNbt.RootTag.Get("author"); if (authorTag is not null && !string.IsNullOrWhiteSpace(authorTag.Value)) { _structureAuthor = authorTag.Value; if (_Authors is null) _Authors = _structureAuthor; } // 读取尺寸信息 var sizeTag = scheNbt.RootTag.Get("size"); if (sizeTag is not null) { var sizeElements = sizeTag.ToArray(); if (sizeElements.Length >= 3) { var sizeArray = sizeElements.Take(3).Select(e => e.IntValue).ToArray(); _litematicEnclosingSize = $"{sizeArray[0]} × {sizeArray[1]} × {sizeArray[2]}"; _litematicTotalVolume = sizeArray[0] * sizeArray[1] * sizeArray[2]; } } // 读取方块数量信息 var blocksTag = scheNbt.RootTag.Get("blocks"); if (blocksTag is not null) _litematicTotalBlocks = blocksTag.Where(x => x.TagType == NbtTagType.Compound).Count(); // 读取调色板信息来计算区域数量 var paletteTag = scheNbt.RootTag.Get("palette"); if (paletteTag is not null) _litematicRegionCount = 1; // 原版结构文件通常只有一个区域 } } catch (Exception ex) { ModBase.Log(ex, "读取 NBT 结构文件数据时出错(" + path + ")"); } } #endregion #region 基础 /// /// 资源的文件的地址。 /// public readonly string path; /// /// 是否为文件夹项。 /// public bool IsFolder => path.EndsWithF(@"\__FOLDER__", true); /// /// 获取实际的文件夹路径(去除 __FOLDER__ 标记)。 /// public string ActualPath { get { if (IsFolder) return path.Replace(@"\__FOLDER__", ""); return path; } } public LocalCompFile(string path) { this.path = path ?? ""; } /// /// NBT数据是否已加载(用于延迟加载优化)。 /// private bool _nbtDataLoaded; /// /// Mod 资源的完整路径,去除最后的 .disabled 和 .old。 /// public string RawPath => ModBase.GetPathFromFullPath(path) + RawFileName; /// /// 资源的完整文件名。 /// public string FileName { get { if (IsFolder && !string.IsNullOrEmpty(Name)) return Name; return ModBase.GetFileNameFromPath(path); } } /// /// Mod 资源的完整文件名,去除最后的 .disabled 和 .old。 /// public string RawFileName => FileName.Replace(".disabled", "").Replace(".old", ""); /// /// 资源的状态。对于 Mod 有 Disabled /// public LocalFileStatus State { get { Load(); if (!IsFileAvailable) return LocalFileStatus.Unavailable; if (path.EndsWithF(".disabled", true) || path.EndsWithF(".old", true)) return LocalFileStatus.Disabled; return LocalFileStatus.Fine; } } public enum LocalFileStatus { Fine = 0, Disabled = 1, Unavailable = 2 } #endregion #region 信息项 /// /// Mod 的名称。若不可用则为 ModID 或无扩展的文件名。 /// public string Name { get { if (_Name is null) Load(); if (_Name is null) _Name = _ModId; if (_Name is null) { if (IsFolder) _Name = ModBase.GetFolderNameFromPath(ActualPath); else _Name = ModBase.GetFileNameWithoutExtentionFromPath(path); } return _Name; } set { if (_Name is null && value is not null && !value.Contains("modname") && value.ToLower() != "name" && value.Length > 1 && (ModBase.Val(value).ToString() ?? "") != (value ?? "")) _Name = value; } } private string _Name; /// /// Mod 的描述信息。 /// public string Description { get { if (_Description is null) Load(); if (_Description is null && FileUnavailableReason is not null) _Description = FileUnavailableReason.Message; // If _Description Is Nothing Then _Description = Path return _Description; } set { if (_Description is null && value is not null && value.Length > 2) { _Description = value.Trim('\n'); // 优化显示:若以 [a-zA-Z0-9] 结尾,加上小数点句号 if (_Description.ToLower().LastIndexOfAny("qwertyuiopasdfghjklzxcvbnm0123456789".ToCharArray()) == _Description.Length - 1) _Description += "."; } } } private string _Description; /// /// 文件类型标签。 /// public List Tags { get { if (field is null) { field = new List(); if (IsFolder) { field.Add("文件夹"); } else { var extension = System.IO.Path.GetExtension(RawPath).ToLower(); switch (extension ?? "") { case ".litematic": { field.Add("原理图"); break; } case ".schem": case ".schematic": { field.Add("Schematic结构"); break; } case ".nbt": { field.Add("原版结构"); break; } } } } return field; } } /// /// Mod 的版本,不保证符合版本格式规范。 /// public string Version { get { if (_Version is null) Load(); return _Version; } set { if (_Version is not null && _Version.RegexCheck(@"[0-9.\-]+")) return; if (value?.ContainsF("version", true) == true) value = "version"; // 需要修改的标识 _Version = value; } } public string _Version; /// /// 用于依赖检查的 ModID。 /// public string ModId { get { if (_ModId is null) Load(); return _ModId; } set { if (value is null) return; value = value.RegexSeek(RegexPatterns.ModIdMatch); if (value is null || value.Length <= 1 || (ModBase.Val(value).ToString() ?? "") == (value ?? "")) return; if (value.ContainsF("name", true) || value.ContainsF("modid", true)) return; if (!possibleModId.Contains(value)) possibleModId.Add(value); if (_ModId is null) _ModId = value; } } private string _ModId; /// /// 其他可能的 ModID。 /// public List possibleModId = new(); /// /// Mod 的主页。 /// public string Url { get { if (field is null) Load(); return field; } set { if (field is null && value is not null && value.StartsWithF("http")) field = value; } } /// /// Mod 的作者列表。 /// public string Authors { get { if (_Authors is null) Load(); return _Authors; } set { if (_Authors is null && !string.IsNullOrWhiteSpace(value)) _Authors = value; } } private string _Authors; /// /// Litematic 文件的创建时间戳。 /// public long? LitematicTimeCreated { get { LoadNbtDataIfNeeded(); return _litematicTimeCreated; } } private long? _litematicTimeCreated; /// /// Litematic 文件的修改时间戳。 /// public long? LitematicTimeModified { get { LoadNbtDataIfNeeded(); return _litematicTimeModified; } } private long? _litematicTimeModified; /// /// Schem 读取到的原始名称。 /// public string SchemOriginalName { get { LoadNbtDataIfNeeded(); return _schemOriginalName; } } private string _schemOriginalName; /// /// Litematic 读取到的原始名称。 /// public string LitematicOriginalName { get { LoadNbtDataIfNeeded(); return _litematicOriginalName; } } private string _litematicOriginalName; /// /// Litematic 文件的版本。 /// public int? LitematicVersion { get { LoadNbtDataIfNeeded(); return _litematicVersion; } } private int? _litematicVersion; /// /// Litematic 文件的包围盒大小。 /// public string LitematicEnclosingSize { get { LoadNbtDataIfNeeded(); return _litematicEnclosingSize; } } private string _litematicEnclosingSize; /// /// Litematic 文件的区域数量。 /// public int? LitematicRegionCount { get { LoadNbtDataIfNeeded(); return _litematicRegionCount; } } private int? _litematicRegionCount; /// /// Litematic 文件的总方块数。 /// public int? LitematicTotalBlocks { get { LoadNbtDataIfNeeded(); return _litematicTotalBlocks; } } private int? _litematicTotalBlocks; /// /// Litematic 文件的总体积。 /// public int? LitematicTotalVolume { get { LoadNbtDataIfNeeded(); return _litematicTotalVolume; } } private int? _litematicTotalVolume; /// /// 原版结构文件的游戏版本。 /// public string StructureGameVersion { get { LoadNbtDataIfNeeded(); return _structureGameVersion; } } private string _structureGameVersion; /// /// 原版结构文件的数据版本。 /// public int? StructureDataVersion { get { LoadNbtDataIfNeeded(); return _structureDataVersion; } } private int? _structureDataVersion; /// /// 原版结构文件的作者。 /// public string StructureAuthor { get { LoadNbtDataIfNeeded(); return _structureAuthor; } } private string _structureAuthor; /// /// Sponge Schematic 文件的版本。 /// public int? SpongeVersion { get { LoadNbtDataIfNeeded(); return _spongeVersion; } } private int? _spongeVersion; /// /// Mod 图标路径。 /// public string Logo { get; set; } /// /// 依赖项,其中包括了 Minecraft 的版本要求。格式为 ModID - VersionRequirement,若无版本要求则为 Nothing。 /// public Dictionary Dependencies { get { Load(); return _Dependencies; } } private Dictionary _Dependencies = new(); private void AddDependency(string modID, string versionRequirement = null) { // 确保信息正确 if (modID is null || modID.Length < 2) return; modID = modID.ToLower(); if (modID == "name" || (ModBase.Val(modID).ToString() ?? "") == (modID ?? "")) return; // 跳过 name 与纯数字 id if (versionRequirement is null || (!versionRequirement.Contains(".") && !versionRequirement.Contains("-")) || versionRequirement.Contains("$")) versionRequirement = null; else if (!versionRequirement.StartsWithF("[") && !versionRequirement.StartsWithF("(") && !versionRequirement.EndsWithF("]") && !versionRequirement.EndsWithF(")")) versionRequirement = "[" + versionRequirement + ",)"; // 向依赖项中添加 if (_Dependencies.ContainsKey(modID)) { if (_Dependencies[modID] is null) _Dependencies[modID] = versionRequirement; } else { _Dependencies.Add(modID, versionRequirement); } } #endregion #region 加载步骤标记 // 1. 进行文件可用性检查 // 成功:继续第二步。 // 失败:标记 FileUnavailableReason, 并停止后续加载。 /// /// 是否已进行 Mod 文件的基础加载。(这包括第一步和第二步) /// private bool isLoaded; /// /// 标记为已加载。用于内嵌(Jar-in-Jar)子项——其元数据已由 通过 /// LookupMetadata 从已打开的嵌套流读入,虚拟路径不是真实文件,须避免属性 getter 再触发 Load() 清空元数据。 /// internal void MarkLoaded() => isLoaded = true; /// /// Mod 文件是否可被正常读取。 /// public bool IsFileAvailable { get { Load(); return FileUnavailableReason is null; } } /// /// Mod 文件出错的原因。若无错误,则为 Nothing。 /// public Exception FileUnavailableReason { get { Load(); return _FileUnavailableReason; } } private Exception _FileUnavailableReason; // 2. 进行 .class 以外的信息获取 // 成功:标记 IsInfoWithoutClassAvailable。 // 失败:什么也不干。如果需要补充信息的话,检测到 IsInfoWithoutClassAvailable 为 False,会自动继续加载。 /// /// 是否已在不获取 .class 文件的前提下完成了所需信息的加载。 /// private bool isInfoWithoutClassAvailable = false; // 3. 尝试从 .class 文件中获取信息 // 成功:标记 IsInfoWithClassAvailable。 // 失败:什么也不干。 /// /// 是否已进行 .class 文件的信息获取。 /// private bool isInfoWithClassLoaded; /// /// 是否已在 .class 文件中完成了所需信息的加载。 /// private bool isInfoWithClassAvailable; #endregion #region 加载 /// /// 初始化所有数据。 /// private void Init() { _Name = null; _Description = null; _Version = null; _ModId = null; possibleModId = new List(); _Dependencies = new Dictionary(); isLoaded = false; _FileUnavailableReason = null; isInfoWithClassLoaded = false; isInfoWithClassAvailable = false; } /// /// 加载基本信息(不解析NBT数据)。 /// public void LoadBasicInfo() { try { // 可用性检查 if (IsFolder) { // 文件夹项不需要进一步处理 isLoaded = true; return; } if (!File.Exists(path)) { _FileUnavailableReason = new FileNotFoundException("未找到资源文件(" + path + ")"); isLoaded = true; return; } // 对于原理图文件,只设置基本状态,不解析NBT数据 if (path.EndsWithF(".litematic", true) || path.EndsWithF(".nbt", true) || path.EndsWithF(".schem", true) || path.EndsWithF(".schematic", true)) { _Name = ModBase.GetFileNameWithoutExtentionFromPath(path); isLoaded = true; return; } // 对于其他文件类型,正常加载 Load(); } catch (Exception ex) { ModBase.Log(ex, $"加载基本信息失败:{path}"); } } /// /// 延迟加载NBT数据。 /// public void LoadNbtDataIfNeeded() { try { // 如果已经加载过NBT数据,则跳过 if (_nbtDataLoaded) return; // 根据文件类型加载NBT数据 if (path.EndsWithF(".litematic", true)) LoadLitematicNbtData(); else if (path.EndsWithF(".nbt", true)) LoadStructureNbtData(); else if (path.EndsWithF(".schem", true)) LoadSchemNbtData(); else if (path.EndsWithF(".schematic", true)) LoadSchematicNbtData(); _nbtDataLoaded = true; } catch (Exception ex) { ModBase.Log(ex, $"延迟加载NBT数据失败:{path}"); } } /// /// 进行文件可用性检查与 .class 以外的信息获取。 /// public void Load(bool forceReload = false) { if (isLoaded && !forceReload) return; // 初始化 Init(); // 基础可用性检查 if (path.Length < 2) { _FileUnavailableReason = new FileNotFoundException("错误的资源文件路径(" + (path ?? "null") + ")"); isLoaded = true; return; } // 对于文件夹项,检查实际文件夹路径是否存在 if (IsFolder) { if (!Directory.Exists(ActualPath)) { _FileUnavailableReason = new DirectoryNotFoundException("未找到文件夹(" + ActualPath + ")"); isLoaded = true; return; } // 文件夹项不需要进一步处理 isLoaded = true; return; } if (!File.Exists(path)) { _FileUnavailableReason = new FileNotFoundException("未找到资源文件(" + path + ")"); isLoaded = true; return; } // 对于投影文件,跳过 zip 解析 if (path.EndsWithF(".litematic", true) || path.EndsWithF(".nbt", true) || path.EndsWithF(".schem", true) || path.EndsWithF(".schematic", true)) { try { _Name = ModBase.GetFileNameWithoutExtentionFromPath(path); // 根据文件类型加载数据 if (path.EndsWithF(".litematic", true)) { LoadLitematicNbtData(); } else if (path.EndsWithF(".schem", true) || path.EndsWithF(".schematic", true)) { if (path.EndsWithF(".schem", true)) LoadSchemNbtData(); else LoadSchematicNbtData(); } else if (path.EndsWithF(".nbt", true)) { LoadStructureNbtData(); } _nbtDataLoaded = true; } catch (Exception ex) { ModBase.Log(ex, "投影文件信息获取失败(" + path + ")", ModBase.LogLevel.Developer); _FileUnavailableReason = ex; } isLoaded = true; return; } // 对于其他文件,尝试作为 Jar 文件打开 ZipArchive jar = null; try { jar = new ZipArchive(new FileStream(path, FileMode.Open)); // 信息获取 LookupMetadata(jar); EmbeddedMods = ModJarInJar.Resolve(path, jar); } catch (UnauthorizedAccessException ex) { ModBase.Log(ex, "资源文件由于无权限无法打开(" + path + ")", ModBase.LogLevel.Developer); _FileUnavailableReason = new UnauthorizedAccessException("没有读取此文件的权限,请尝试右键以管理员身份运行 PCL", ex); } catch (Exception ex) { ModBase.Log(ex, "资源文件无法打开(" + path + ")", ModBase.LogLevel.Developer); _FileUnavailableReason = ex; } finally { if (jar is not null) jar.Dispose(); } // 完成标记 isLoaded = true; } /// /// 内嵌模组列表,由 解析填充。 /// public List EmbeddedMods { get; internal set; } = new(); /// /// 从 Jar 文件中获取 Mod 信息。 /// internal void LookupMetadata(ZipArchive jar) { #region 尝试使用 mcmod.info do { try { // 获取信息文件 var infoEntry = jar.GetEntry("mcmod.info"); string infoString = null; if (infoEntry is not null) { infoString = ModBase.ReadFile(infoEntry.Open()); if (infoString.Length < 15) infoString = null; } if (infoString is null) break; // 获取可用 Json 项 JsonObject infoObject; var jsonObject = (JsonNode)ModBase.GetJson(infoString); if (jsonObject.GetValueKind() == JsonValueKind.Array) infoObject = (JsonObject)jsonObject[0]; else infoObject = (JsonObject)jsonObject["modList"][0]; // 从文件中获取 Mod 信息项 Name = (string)infoObject["name"]; Description = (string)infoObject["description"]; Version = (string)infoObject["version"]; Url = (string)infoObject["url"]; ModId = (string)infoObject["modid"]; var authorJson = (JsonArray)infoObject["authorList"]; if (authorJson is not null) { var author = new List(); foreach (var Token in authorJson) author.Add(Token.ToString()); if (author.Any()) Authors = author.Join(", "); } var logoFile = (string)infoObject["logoFile"]; if (logoFile is not null) { var logoItem = jar.GetEntry(logoFile); if (logoItem is not null) { var md5 = ModBase.GetStringMD5(logoItem.Length + logoItem.CompressedLength + path); Logo = System.IO.Path.Combine(ModBase.pathTemp, "Cache", "Images", $"{md5}.png"); using (var entryStream = logoItem.Open()) { ModBase.WriteFile(Logo, entryStream); } } } var reqs = (JsonArray)infoObject["requiredMods"]; if (reqs is not null) foreach (string item in reqs) // 将迭代变量重命名为 item if (!string.IsNullOrEmpty(item)) { // 使用一个局部变量 token 来处理逻辑 var token = item; token = token.Substring(token.IndexOfF(":") + 1); if (token.Contains("@")) { var parts = token.Split("@"); AddDependency(parts[0], parts[1]); } else { AddDependency(token); } } reqs = (JsonArray)infoObject["dependencies"]; if (reqs is not null) foreach (string rawToken in reqs) if (!string.IsNullOrEmpty(rawToken)) { var id = rawToken.Substring(rawToken.IndexOfF(":") + 1); if (id.Contains("@")) { var parts = id.Split("@"); AddDependency(parts[0], parts[1]); } else { AddDependency(id); } } } catch (Exception ex) { ModBase.Log(ex, "读取 mcmod.info 时出现未知错误(" + path + ")", ModBase.LogLevel.Developer); } } while (false); #endregion #region 尝试使用 fabric.mod.json do { try { var fabricEntry = jar.GetEntry("fabric.mod.json"); string fabricText = null; if (fabricEntry is not null) { fabricText = ModBase.ReadFile(fabricEntry.Open(), Encoding.UTF8); if (!fabricText.Contains("schemaVersion")) fabricText = null; } if (fabricText is null) break; var fabricObject = (JsonObject)ModBase.GetJson(fabricText); if (fabricObject.ContainsKey("name")) Name = fabricObject["name"].ToString(); if (fabricObject.ContainsKey("version")) Version = fabricObject["version"].ToString(); if (fabricObject.ContainsKey("description")) Description = fabricObject["description"].ToString(); if (fabricObject.ContainsKey("id")) ModId = fabricObject["id"].ToString(); if (fabricObject.ContainsKey("contact") && fabricObject["contact"]["homepage"] is not null) Url = fabricObject["contact"]["homepage"].ToString(); var authorJson = (JsonArray)fabricObject["authors"]; if (authorJson is not null) { var authorList = authorJson.Select(t => t.ToString()).ToList(); if (authorList.Any()) Authors = string.Join(", ", authorList); } if (fabricObject.ContainsKey("icon")) { var logoFile = fabricObject["icon"].ToString(); var logoItem = jar.GetEntry(logoFile); if (logoItem is not null) { var md5 = ModBase.GetStringMD5(logoItem.Length + logoItem.CompressedLength + path); Logo = System.IO.Path.Combine(ModBase.pathTemp, "Cache", "Images", $"{md5}.png"); using (var entryStream = logoItem.Open()) { ModBase.WriteFile(Logo, entryStream); } } } // 依赖处理 (省略了 VB 中的注释部分,按逻辑实现) if (fabricObject.ContainsKey("depends")) foreach (var dep in (JsonObject)fabricObject["depends"]) AddDependency(dep.Key, dep.Value.ToString()); } catch (Exception ex) { ModBase.Log(ex, "读取 fabric.mod.json 时出错(" + path + ")", ModBase.LogLevel.Developer); } } while (false); #endregion #region 尝试使用 quilt.mod.json do { try { // 获取 quilt.mod.json 文件 var quiltEntry = jar.GetEntry("quilt.mod.json"); string quiltText = null; if (quiltEntry is not null) { quiltText = ModBase.ReadFile(quiltEntry.Open(), Encoding.UTF8); if (!quiltText.Contains("schema_version")) quiltText = null; } if (quiltText is null) break; var quiltObject = (JsonObject)((JsonObject)ModBase.GetJson(quiltText))["quilt_loader"]; // 从文件中获取 Mod 信息项 if (quiltObject.ContainsKey("id")) ModId = (string)quiltObject["id"]; if (quiltObject.ContainsKey("version")) Version = (string)quiltObject["version"]; if (quiltObject.ContainsKey("metadata")) { var quiltMetadata = (JsonObject)quiltObject["metadata"]; if (quiltMetadata.ContainsKey("name")) Name = (string)quiltMetadata["name"]; if (quiltMetadata.ContainsKey("description")) Description = (string)quiltMetadata["description"]; if (quiltMetadata.ContainsKey("contact")) Url = (string)(quiltMetadata["contact"]["homepage"] ?? ""); } if (quiltObject.ContainsKey("icon")) { var logoFile = (string)quiltObject["icon"]; if (logoFile is not null) { var logoItem = jar.GetEntry(logoFile); if (logoItem is not null) { var md5 = ModBase.GetStringMD5(logoItem.Length + logoItem.CompressedLength + path); Logo = System.IO.Path.Combine(ModBase.pathTemp, "Cache", "Images", $"{md5}.png"); using (var entryStream = logoItem.Open()) { ModBase.WriteFile(Logo, entryStream); } } } } goto Finished; } catch (Exception ex) { ModBase.Log(ex, "读取 quilt.mod.json 时出现未知错误(" + path + ")", ModBase.LogLevel.Developer); } } while (false); #endregion #region 尝试使用 mods.toml try { // 获取 mods.toml 文件 var tomlEntry = jar.GetEntry("META-INF/mods.toml") ?? jar.GetEntry("META-INF/neoforge.mods.toml"); string tomlText = null; if (tomlEntry is not null) { using (var reader = new StreamReader(tomlEntry.Open())) { tomlText = reader.ReadToEnd(); } if (tomlText.Length < 15) tomlText = null; } if (tomlText is not null) { // 文件标准化:统一换行符为 \n,去除注释、头尾的空格、空行 var lines = new List(); var rawLines = tomlText.Replace("\r\n", "\n").Replace("\r", "\n").Split('\n'); foreach (var rawLine in rawLines) { var line = rawLine; if (line.StartsWithF("#")) continue; // 去除注释 if (line.Contains("#")) line = line.Substring(0, line.IndexOfF("#")); // 去除头尾的空格(包含全角空格) line = line.Trim(' ', '\t', ' '); if (!string.IsNullOrEmpty(line)) lines.Add(line); } // 读取文件数据 // TomlData 存储段落名及其对应的键值对 var tomlData = new List>> { new("", new Dictionary()) }; for (var i = 0; i < lines.Count; i++) { var line = lines[i]; if (line.StartsWithF("[") && line.EndsWithF("]")) { // 段落标记 var header = line.Trim('[', ']'); tomlData.Add( new KeyValuePair>(header, new Dictionary())); } else if (line.Contains("=")) { // 字段标记 var key = line.Substring(0, line.IndexOfF("=")).TrimEnd(' ', '\t', ' '); var rawValue = line.Substring(line.IndexOfF("=") + 1).TrimStart(' ', '\t', ' '); object value; if (rawValue.StartsWithF("\"") && rawValue.EndsWithF("\"")) { // 单行字符串 value = rawValue.Trim('\"'); } else if (rawValue.StartsWithF("'''")) { // 多行字符串 var valueLines = new List { rawValue.Replace("'''", "") }; if (!rawValue.EndsWithF("'''") || rawValue.Length == 3) while (i < lines.Count - 1) { i++; var valueLine = lines[i]; if (valueLine.EndsWithF("'''")) { valueLines.Add(valueLine.Replace("'''", "")); break; } valueLines.Add(valueLine); } value = string.Join("\n", valueLines).Trim('\n').Replace("\n", "\r\n"); } else if (rawValue.ToLower() == "true" || rawValue.ToLower() == "false") { // 布尔型 value = rawValue.ToLower() == "true"; } else if (double.TryParse(rawValue, out var num)) { // 数字型 (模拟 VB 的 Val) value = num; } else { // 默认当做字符串存储 value = rawValue; } // 将值存入当前最后的段落中 var lastPair = tomlData[tomlData.Count - 1]; lastPair.Value[key] = value; } } // 从解析出的数据中提取 Mod 信息 Dictionary modEntry = null; foreach (var subData in tomlData) if (subData.Key == "mods") { modEntry = subData.Value; break; } if (modEntry is not null && modEntry.ContainsKey("modId")) { ModId = modEntry["modId"].ToString(); // 假设 _ModId 是内部属性,如果为 null 说明设置失败 if (_ModId is not null) { if (modEntry.ContainsKey("displayName")) Name = modEntry["displayName"].ToString(); if (modEntry.ContainsKey("description")) Description = modEntry["description"].ToString(); if (modEntry.ContainsKey("version")) Version = modEntry["version"].ToString(); // [0] 是全局段落(无 Header) if (tomlData[0].Value.ContainsKey("displayURL")) Url = tomlData[0].Value["displayURL"].ToString(); if (tomlData[0].Value.ContainsKey("authors")) Authors = tomlData[0].Value["authors"].ToString(); // 读取依赖 foreach (var subData in tomlData) if (subData.Key.ToLower() == $"dependencies.{ModId.ToLower()}") { var depEntry = subData.Value; if (depEntry.ContainsKey("modId") && depEntry.ContainsKey("mandatory") && (bool)depEntry["mandatory"] && depEntry.ContainsKey("side") && depEntry["side"].ToString().ToLower() != "server") AddDependency( depEntry["modId"].ToString(), depEntry.ContainsKey("versionRange") ? depEntry["versionRange"].ToString() : null ); } // 加载成功,跳转到完成标签 goto Finished; } } } } catch (Exception ex) { ModBase.Log(ex, "读取 mods.toml 时出现未知错误(" + path + ")", ModBase.LogLevel.Developer); } #endregion #region 尝试使用 fml_cache_annotation.json do { try { // 获取 fml_cache_annotation.json 文件 var fmlEntry = jar.GetEntry("META-INF/fml_cache_annotation.json"); string fmlText = null; if (fmlEntry is not null) { fmlText = ModBase.ReadFile(fmlEntry.Open(), Encoding.UTF8); if (!fmlText.Contains("Lnet/minecraftforge/fml/common/Mod;")) fmlText = null; } if (fmlText is null) break; var fmlJson = (JsonObject)ModBase.GetJson(fmlText); // 获取可用 Json 项 JsonObject fmlObject = null; foreach (var ModFilePair in fmlJson) { var modFileAnnos = (JsonArray)ModFilePair.Value["annotations"]; if (modFileAnnos is not null) // 先获取 Mod foreach (var ModFileAnno in modFileAnnos) { var name = (string)(ModFileAnno["name"] ?? ""); if (name == "Lnet/minecraftforge/fml/common/Mod;") { fmlObject = (JsonObject)ModFileAnno["values"]; goto Got; } } } break; Got: ; // 从文件中获取 Mod 信息项 if (fmlObject.ContainsKey("useMetadata") && (fmlObject["useMetadata"]["value"] ?? "").ToString().ToLower() == "true") { // 要求使用 mcmod.info 中的信息 var value = (string)fmlObject["modid"]["value"]; if (value is null) break; value = value.ToLower().RegexSeek(RegexPatterns.ModIdMatch); if (value is not null && value.ToLower() != "name" && value.Length > 1 && (ModBase.Val(value).ToString() ?? "") != (value ?? "")) if (!possibleModId.Contains(value)) possibleModId.Add(value); break; } if (fmlObject.ContainsKey("name")) Name = (string)fmlObject["name"]["value"]; if (fmlObject.ContainsKey("version")) Version = (string)fmlObject["version"]["value"]; if (fmlObject.ContainsKey("modid")) ModId = (string)fmlObject["modid"]["value"]; if (!fmlObject.ContainsKey("serverSideOnly") || !fmlObject["serverSideOnly"]["value"].ToObject()) { // 添加 Minecraft 依赖 var depMinecraft = (string)((fmlObject["acceptedMinecraftVersions"] is not null ? fmlObject["acceptedMinecraftVersions"]["value"] : "") ?? ""); if (!string.IsNullOrEmpty(depMinecraft)) AddDependency("minecraft", depMinecraft); // 添加其他依赖 var deps = (string)((fmlObject["dependencies"] is not null ? fmlObject["dependencies"]["value"] : "") ?? ""); if (!string.IsNullOrEmpty(deps)) foreach (var item in deps.Split(";")) { if (string.IsNullOrEmpty(item) || !item.StartsWithF("required-")) continue; // 使用局部变量处理逻辑,不要直接修改迭代变量 item var dep = item.Substring(item.IndexOfF(":") + 1); if (dep.Contains("@")) { var parts = dep.Split("@"); AddDependency(parts[0], parts[1]); } else { AddDependency(dep); } } } } catch (Exception ex) { ModBase.Log(ex, "读取 fml_cache_annotation.json 时出现未知错误(" + path + ")"); } } while (false); #endregion #region 尝试识别资源包图标 try { // 检查并提取资源包的 pack.png 图标 var packPngEntry = jar.GetEntry("pack.png"); if (packPngEntry is not null) try { var md5 = ModBase.GetStringMD5(packPngEntry.Length + packPngEntry.CompressedLength + path); Logo = System.IO.Path.Combine(ModBase.pathTemp, "Cache", "Images", $"{md5}.png"); using (var entryStream = packPngEntry.Open()) { ModBase.WriteFile(Logo, entryStream); } ModBase.Log("成功提取资源包图标:" + path, ModBase.LogLevel.Developer); } catch (Exception logoEx) { ModBase.Log(logoEx, "提取 pack.png 图标失败(" + path + ")", ModBase.LogLevel.Developer); } } catch (Exception ex) { ModBase.Log(ex, "识别资源包图标时出现未知错误(" + path + ")", ModBase.LogLevel.Developer); } #endregion Finished: ; #region 将 Version 代号转换为 META-INF 中的版本 if (_Version == "version") try { var metaEntry = jar.GetEntry("META-INF/MANIFEST.MF"); if (metaEntry is not null) { var metaString = ModBase.ReadFile(metaEntry.Open()).Replace(" :", ":").Replace(": ", ":"); if (metaString.Contains("Implementation-Version:")) { metaString = metaString.Substring(metaString.IndexOfF("Implementation-Version:") + "Implementation-Version:".Count()); metaString = metaString.Substring(0, metaString.IndexOfAny("\r\n".ToCharArray())) .Trim(); Version = metaString; } } } catch (Exception ex) { ModBase.Log("获取 META-INF 中的版本信息失败(" + path + ")", ModBase.LogLevel.Developer); Version = null; } if (_Version is not null && !(_Version.Contains(".") || _Version.Contains("-"))) Version = null; #endregion } #endregion #region 网络信息 /// /// 当任何网络信息更新时触发。 /// public event OnCompUpdateEventHandler? OnCompUpdate; public delegate void OnCompUpdateEventHandler(LocalCompFile sender); /// /// 该 Mod 关联的网络项目。 /// public CompProject Comp { get => field; set { field = value; OnCompUpdate?.Invoke(this); } } /// /// 本地文件对应的联网文件信息。 /// public CompFile compFile; /// /// 该 Mod 对应的联网最新版本。 /// public CompFile UpdateFile { get => field; set { field = value; OnCompUpdate?.Invoke(this); } } /// /// 该 Mod 的更新日志网址。 /// public List changelogUrls = new(); /// /// 所有网络信息是否已成功加载。 /// public bool compLoaded; /// /// 将网络信息保存为 Json。 /// public JsonObject ToJson() { var json = new JsonObject(); if (Comp is not null) json.Add("Comp", Comp.ToJson()); json.Add("ChangelogUrls", new JsonArray(changelogUrls.Select(s => (JsonNode)s).ToArray())); json.Add("CompLoaded", compLoaded); if (compFile is not null) json.Add("CompFile", compFile.ToJson()); if (UpdateFile is not null) json.Add("UpdateFile", UpdateFile.ToJson()); return json; } /// /// 从 Json 中读取网络信息。 /// public void FromJson(JsonObject json) { compLoaded = (bool)json["CompLoaded"]; if (json.ContainsKey("Comp")) Comp = new CompProject((JsonObject)json["Comp"]); if (json.ContainsKey("ChangelogUrls")) changelogUrls = json["ChangelogUrls"].ToObject>(); if (json.ContainsKey("CompFile")) compFile = new CompFile((JsonObject)json["CompFile"], CompType.Mod); if (json.ContainsKey("UpdateFile")) UpdateFile = new CompFile((JsonObject)json["UpdateFile"], CompType.Mod); } /// /// 该文件是否可以更新。 /// public bool CanUpdate => !Config.Preference.Hide.FunctionModUpdate && changelogUrls.Any(); /// /// 获取用于 CurseForge 信息获取的 Hash 值(MurmurHash2)。 /// public uint CurseForgeHash { get { if (_CurseForgeHash is null) { var buf = _hashCache.Value .GetMurmurHash2Async(path) .GetAwaiter().GetResult() .HexToBytes(); _CurseForgeHash = BitConverter.ToUInt32(buf); } return (uint)_CurseForgeHash; } } private uint? _CurseForgeHash; /// /// 获取用于 Modrinth 信息获取的 Hash 值(SHA1)。 /// public string ModrinthHash { get { if (field is null) field = _hashCache.Value.GetSHA1Async(path).GetAwaiter().GetResult(); return field; } } #endregion #region API public override string ToString() { return $"{State} - {path}"; } public override bool Equals(object obj) { var target = obj as LocalCompFile; return target is not null && (path ?? "") == (target.path ?? ""); } #endregion } /// /// 获取文件夹描述信息。 /// private static string GetFolderDescription(string folderPath) { try { if (!Directory.Exists(folderPath)) return "空文件夹"; return "文件夹"; } catch (Exception ex) { ModBase.Log(ex, $"获取文件夹描述失败:{folderPath}"); return "文件夹"; } } public class CompLocalLoaderData { public string compPath; public CompType compType; public KeyValuePair, JsonObject> detailInfo; public PageInstanceCompResource frm; public McInstance gameVersion; public List loaders; } // 加载资源列表 public static LoaderTask> compResourceListLoader = new("Comp Resource List Loader", CompResourceListLoad); private static void CompResourceListLoad(LoaderTask> loader) { try { ModBase.RunInUiWait(() => { if (loader.input.frm is not null) loader.input.frm.Load.ShowProgress = false; }); // 等待 Mod 更新完成 if (PageInstanceCompResource.updatingVersions.Contains(loader.input.compPath)) { ModBase.Log("[Mod] 等待资源更新完成后才能继续加载资源列表:" + loader.input.compPath); try { ModBase.RunInUiWait(() => { if (loader.input.frm is not null) loader.input.frm.Load.Text = Lang.Text("Instance.Resource.Update.WaitingForUpdate"); }); while (PageInstanceCompResource.updatingVersions.Contains(loader.input.compPath)) { if (loader.IsAborted) return; Thread.Sleep(100); } } finally { ModBase.RunInUiWait(() => { if (loader.input.frm is not null) loader.input.frm.Load.Text = Lang.Text("Instance.Resource.List.Loading"); }); } loader.input.frm.LoaderRun(LoaderFolderRunType.UpdateOnly); } // 获取 Mod 文件夹下的可用文件列表 var modList = new List(); if (Directory.Exists(loader.input.compPath)) { var rawName = loader.input.compPath.ToLower(); if (loader.input.compType == CompType.Schematic) { var currentFolderPath = ""; if (loader.input.frm is not null) currentFolderPath = loader.input.frm.CurrentFolderPath; var searchPath = string.IsNullOrEmpty(currentFolderPath) ? loader.input.compPath : currentFolderPath; try { var dirInfo = new DirectoryInfo(searchPath); foreach (var Dir in dirInfo.EnumerateDirectories("*", SearchOption.AllDirectories)) modList.Add(new LocalCompFile(Path.Combine(Dir.FullName, "__FOLDER__"))); foreach (var File in dirInfo.EnumerateFiles("*", SearchOption.AllDirectories)) try { if (LocalCompFile.IsCompFile(File.FullName, loader.input.compType)) modList.Add(new LocalCompFile(File.FullName)); } catch (Exception ex) { ModBase.Log(ex, $"处理文件失败:{File.FullName}"); } } catch (Exception ex) { ModBase.Log(ex, $"枚举文件失败:{searchPath}"); } } else { try { foreach (var File in ModBase.EnumerateFiles(loader.input.compPath)) try { if ((File.DirectoryName.ToLower() ?? "") != (rawName.TrimEnd('\\') ?? "")) if (!(PageInstanceLeft.McInstance is not null && PageInstanceLeft.McInstance.Info.HasForge && PageInstanceLeft.McInstance.Info.Drop < 130 && (File.Directory.Name ?? "") == (PageInstanceLeft.McInstance.Info.VanillaName ?? ""))) continue; if (LocalCompFile.IsCompFile(File.FullName, loader.input.compType)) modList.Add(new LocalCompFile(File.FullName)); } catch (Exception ex) { ModBase.Log(ex, $"处理文件失败:{File.FullName}"); } } catch (Exception ex) { ModBase.Log(ex, $"枚举文件夹失败:{loader.input.compPath}"); } } } // 确定是否显示进度 loader.Progress = 0.05d; if (modList.Count > 50) ModBase.RunInUi(() => { if (loader.input.frm is not null) loader.input.frm.Load.ShowProgress = true; }); // 获取本地文件缓存 var cachePath = ModBase.pathTemp + @"Cache\LocalComp.json"; var cache = new JsonObject(); try { var cacheContent = ModBase.ReadFile(cachePath); if (!string.IsNullOrWhiteSpace(cacheContent)) { cache = (JsonObject)ModBase.GetJson(cacheContent); if (!cache.ContainsKey("version") || cache["version"].ToObject() != localModCacheVersion) { ModBase.Log("[Mod] 本地 Mod 信息缓存版本已过期,将弃用这些缓存信息", ModBase.LogLevel.Debug); cache = new JsonObject(); } } } catch (Exception ex) { ModBase.Log(ex, "读取本地 Mod 信息缓存失败,已重置"); cache = new JsonObject(); } cache["version"] = localModCacheVersion; // 加载 Mod 列表 - 优化:对于原理图文件,延迟加载NBT数据 var modUpdateList = new List(); foreach (var ModEntry in modList) { loader.Progress += 0.94d / modList.Count; if (loader.IsAborted) return; if (ModEntry.IsFolder) continue; // 优化:对于原理图文件,只进行基础加载,不解析NBT数据 if (loader.input.compType == CompType.Schematic) ModEntry.LoadBasicInfo(); else // 加载 McMod 对象 ModEntry.Load(); // 读取 Comp 缓存 if (ModEntry.State == LocalCompFile.LocalFileStatus.Unavailable) continue; var cacheKey = ModEntry.ModrinthHash + loader.input.gameVersion.Info.VanillaName + loader.input.loaders.Join(""); if (cache.ContainsKey(cacheKey)) { ModEntry.FromJson((JsonObject)cache[cacheKey]); // 如果缓存中的信息在 6 小时以内更新过,则无需重新获取 if (ModEntry.compLoaded && DateTime.Now - cache[cacheKey]["Comp"]["CacheTime"].ToObject() < new TimeSpan(6, 0, 0)) continue; } modUpdateList.Add(ModEntry); } loader.Progress = 0.99d; ModBase.Log( $"[Mod] 共有 {modList.Count} 个 Mod,其中 {modUpdateList.Where(m => m.Comp is null).Count()} 个需要联网获取信息,{modUpdateList.Where(m => m.Comp is not null).Count()} 个需要更新信息"); // 排序 modList.Sort((left, right) => { if (left.State == LocalCompFile.LocalFileStatus.Unavailable != (right.State == LocalCompFile.LocalFileStatus.Unavailable)) return left.State == LocalCompFile.LocalFileStatus.Unavailable ? 1 : -1; return right.FileName.CompareTo(left.FileName); }); // 回设 if (loader.IsAborted) return; loader.output = modList; // 开始联网加载 if (modUpdateList.Any()) { // TODO: 添加信息获取中提示 loader.input.detailInfo = new KeyValuePair, JsonObject>(modUpdateList, cache); compUpdateDetailLoader.Start(loader.input, true); } } catch (Exception ex) { ModBase.Log(ex, "Mod 列表加载失败"); throw; } } // 联网加载 Mod 详情 public static LoaderTask compUpdateDetailLoader = new("Comp List Detail Loader", CompUpdateDetailLoad); private static void CompUpdateDetailLoad(LoaderTask loader) { var mods = loader.input.detailInfo.Key; var cache = loader.input.detailInfo.Value; // 获取作为检查目标的加载器和版本 var modLoaders = loader.input.loaders; var compType = loader.input.compType; var mcInstance = loader.input.gameVersion.Info.VanillaName; // 开始网络获取 ModBase.Log($"[Mod] 目标加载器:{string.Join("/", modLoaders)},版本:{mcInstance}"); var endedThreadCount = 0; var isFailed = false; var currentTaskId = Task.CurrentId ?? -1; // 从 Modrinth 获取信息 ModBase.RunInNewThread(() => { try { // 步骤 1:获取 Hash 与对应的工程 ID var modrinthHashes = mods.Select(m => m.ModrinthHash).ToList(); var modrinthVersion = (JsonObject)ModBase.GetJson(ModDownload.DlModRequest( "https://api.modrinth.com/v2/version_files", "POST", $"{{\"hashes\": [\"{string.Join("\",\"", modrinthHashes)}\"], \"algorithm\": \"sha1\"}}", "application/json")); ModBase.Log($"[Mod] 从 Modrinth 获取到 {modrinthVersion.Count} 个本地 Mod 的对应信息"); // 步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系 if (modrinthVersion.Count == 0) return; var modrinthMapping = new Dictionary>(); foreach (var Entry in mods) { if (modrinthVersion[Entry.ModrinthHash] is null) continue; if (modrinthVersion[Entry.ModrinthHash]["files"][0]["hashes"]["sha1"].ToString() != Entry.ModrinthHash) continue; var projectId = modrinthVersion[Entry.ModrinthHash]["project_id"].ToString(); // 读取已加载的缓存,加快结果出现速度 if (compProjectCache.ContainsKey(projectId) && Entry.Comp is null) Entry.Comp = compProjectCache[projectId]; if (!modrinthMapping.ContainsKey(projectId)) modrinthMapping[projectId] = new List(); modrinthMapping[projectId].Add(Entry); // 记录对应的 CompFile var fileInfo = new CompFile((JsonObject)modrinthVersion[Entry.ModrinthHash], CompType.Mod); if (Entry.compFile is null || Entry.compFile.ReleaseDate < fileInfo.ReleaseDate) Entry.compFile = fileInfo; } if (loader.IsAbortedWithThread(currentTaskId)) return; ModBase.Log($"[Mod] 需要从 Modrinth 获取 {modrinthMapping.Count} 个本地 Mod 的工程信息"); // 步骤 3:获取工程信息 if (!modrinthMapping.Any()) return; var modrinthProject = (JsonArray)ModBase.GetJson(ModDownload.DlModRequest( $"https://api.modrinth.com/v2/projects?ids=[\"{string.Join("\",\"", modrinthMapping.Keys)}\"]", "GET", "", "application/json")); foreach (var ProjectJson in modrinthProject) { var project = new CompProject((JsonObject)ProjectJson); foreach (var Entry in modrinthMapping[project.Id]) Entry.Comp = project; } ModBase.Log("[Mod] 已从 Modrinth 获取本地 Mod 信息,继续获取更新信息"); // 步骤 4:获取更新信息 var targetLoaders = compType == CompType.DataPack ? "datapack" : string.Join("\",\"", modLoaders).ToLower(); var modrinthUpdate = (JsonObject)ModBase.GetJson(ModDownload.DlModRequest( "https://api.modrinth.com/v2/version_files/update", "POST", $"{{\"hashes\": [\"{string.Join("\",\"", modrinthMapping.SelectMany(l => l.Value.Select(m => m.ModrinthHash)))}\"], \"algorithm\": \"sha1\", " + $"\"loaders\": [\"{targetLoaders}\"],\"game_versions\": [\"{mcInstance}\"]}}", "application/json")); foreach (var Entry in mods) { if (modrinthUpdate[Entry.ModrinthHash] is null || Entry.compFile is null) continue; var updateFile = new CompFile((JsonObject)modrinthUpdate[Entry.ModrinthHash], CompType.Mod); if (!updateFile.Available) continue; if (ModBase.modeDebug) ModBase.Log($"[Mod] 本地文件 {Entry.compFile.FileName} 在 Modrinth 上的最新版为 {updateFile.FileName}"); if (Entry.compFile.ReleaseDate >= updateFile.ReleaseDate || Entry.compFile.Hash == updateFile.Hash) continue; // 设置更新日志与更新文件 if (Entry.UpdateFile is not null && updateFile.Hash == Entry.UpdateFile.Hash) { Entry.changelogUrls.Add( $"https://modrinth.com/mod/{modrinthUpdate[Entry.ModrinthHash]["project_id"]}/changelog?g={mcInstance}"); Entry.UpdateFile.DownloadUrls.AddRange(updateFile.DownloadUrls); Entry.UpdateFile = updateFile; } else if (Entry.UpdateFile is null || updateFile.ReleaseDate >= Entry.UpdateFile.ReleaseDate) { Entry.changelogUrls = new List { $"https://modrinth.com/mod/{modrinthUpdate[Entry.ModrinthHash]["project_id"]}/changelog?g={mcInstance}" }; Entry.UpdateFile = updateFile; } } ModBase.Log("[Mod] 从 Modrinth 获取本地 Mod 信息结束"); } catch (Exception ex) { ModBase.Log(ex, "从 Modrinth 获取本地 Mod 信息失败"); isFailed = true; } finally { Interlocked.Increment(ref endedThreadCount); } }, "Mod List Detail Loader Modrinth"); // 从 CurseForge 获取信息(工程 ID 与文件 ID 均为数字) ModBase.RunInNewThread(() => { try { // 步骤 1:获取 Hash 与对应的工程信息 var curseForgeHashes = new List(); foreach (var entry in mods) { curseForgeHashes.Add(entry.CurseForgeHash); if (loader.IsAbortedWithThread(currentTaskId)) return; } var curseForgeResponse = (JsonObject)ModBase.GetJson(ModDownload.DlModRequest( "https://api.curseforge.com/v1/fingerprints/432", "POST", $"{{\"fingerprints\": [{string.Join(",", curseForgeHashes)}]}}", "application/json")); var curseForgeRaw = (JsonArray)curseForgeResponse["data"]["exactMatches"]; ModBase.Log($"[Mod] 从 CurseForge 获取到 {curseForgeRaw.Count} 个本地 Mod 的对应信息"); // 步骤 2:尝试读取工程信息缓存,构建本地文件与工程 ID 的对应关系 if (!curseForgeRaw.Any()) return; var curseForgeMapping = new Dictionary>(); foreach (var project in curseForgeRaw) { var projectId = project["id"].ToString(); var hash = project["file"]["fileFingerprint"].ToObject(); foreach (var Entry in mods) { if (Entry.CurseForgeHash != hash) continue; // 读取已加载的缓存,加快结果出现速度 if (compProjectCache.ContainsKey(projectId) && Entry.Comp is null) Entry.Comp = compProjectCache[projectId]; if (!curseForgeMapping.ContainsKey(projectId)) curseForgeMapping[projectId] = new List(); curseForgeMapping[projectId].Add(Entry); // 记录对应的 CompFile var fileInfo = new CompFile((JsonObject)project["file"], CompType.Mod); if (Entry.compFile is null || Entry.compFile.ReleaseDate < fileInfo.ReleaseDate) Entry.compFile = fileInfo; } } if (loader.IsAbortedWithThread(currentTaskId)) return; ModBase.Log($"[Mod] 需要从 CurseForge 获取 {curseForgeMapping.Count} 个本地 Mod 的工程信息"); // 步骤 3:获取工程信息,同时归纳需要检查更新的文件 ID if (!curseForgeMapping.Any()) return; var curseForgeProject = (JsonArray)((JsonObject)ModBase.GetJson(ModDownload.DlModRequest( "https://api.curseforge.com/v1/mods", "POST", $"{{\"modIds\": [{string.Join(",", curseForgeMapping.Keys)}]}}", "application/json")))["data"]; var updateFileIds = new Dictionary>(); // FileId -> 本地 Mod 文件列表 var fileIdToProjectSlug = new Dictionary(); foreach (var projectJson in curseForgeProject) { if (projectJson["isAvailable"] is not null && !projectJson["isAvailable"].ToObject()) continue; // 设置 Entry 中的工程信息 var project = new CompProject((JsonObject)projectJson); if (!curseForgeMapping.ContainsKey(project.Id)) continue; foreach (var Entry in curseForgeMapping[project.Id]) { // 若已从 Modrinth 获取到工程信息,则保留它,仅再次触发修改事件以刷新 UI if (Entry.Comp is not null && !Entry.Comp.FromCurseForge) { var existing = Entry.Comp; Entry.Comp = existing; continue; } Entry.Comp = project; } // 仅在加载器唯一时查找可能有更新的文件 if (modLoaders.Count != 1) continue; string newestVersion = null; var newestFileIds = new List(); foreach (var indexEntry in (JsonArray)projectJson["latestFilesIndexes"]) { // 加载器唯一且匹配 if (compType != CompType.DataPack && (indexEntry["modLoader"] is null || (int)modLoaders.Single() != indexEntry["modLoader"].ToObject())) continue; var indexVersion = indexEntry["gameVersion"].ToString(); if (indexVersion != mcInstance) continue; // MC 版本匹配 // latestFilesIndexes 按时间从新到老排序,只保留最新的 MC 版本 if (newestVersion is not null && McVersionComparer.CompareVersion(newestVersion, indexVersion) > -1) continue; if (newestVersion != indexVersion) { newestVersion = indexVersion; newestFileIds.Clear(); } newestFileIds.Add(indexEntry["fileId"].ToObject()); } foreach (var fileId in newestFileIds) { if (!updateFileIds.ContainsKey(fileId)) updateFileIds[fileId] = new List(); updateFileIds[fileId].AddRange(curseForgeMapping[project.Id]); fileIdToProjectSlug[fileId] = project.Slug; } } if (loader.IsAbortedWithThread(currentTaskId)) return; ModBase.Log( $"[Mod] 已从 CurseForge 获取本地 Mod 信息,需要获取 {updateFileIds.Count} 个用于检查更新的文件信息"); // 步骤 4:获取更新文件信息 if (!updateFileIds.Any()) return; var curseForgeFiles = (JsonArray)((JsonObject)ModBase.GetJson(ModDownload.DlModRequest( "https://api.curseforge.com/v1/mods/files", "POST", $"{{\"fileIds\": [{string.Join(",", updateFileIds.Keys)}]}}", "application/json")))["data"]; var updateFiles = new Dictionary(); foreach (var fileJson in curseForgeFiles) { var updateFile = new CompFile((JsonObject)fileJson, CompType.Mod); if (!updateFile.Available) continue; if (!int.TryParse(updateFile.Id, out var fileId) || !updateFileIds.ContainsKey(fileId)) continue; foreach (var Entry in updateFileIds[fileId]) { if (updateFiles.ContainsKey(Entry) && updateFiles[Entry].ReleaseDate >= updateFile.ReleaseDate) continue; updateFiles[Entry] = updateFile; } } foreach (var pair in updateFiles) { var Entry = pair.Key; var updateFile = pair.Value; if (Entry.compFile is null) continue; if (ModBase.modeDebug) ModBase.Log( $"[Mod] 本地文件 {Entry.compFile.FileName} 在 CurseForge 上的最新版为 {updateFile.FileName}"); if (Entry.compFile.ReleaseDate >= updateFile.ReleaseDate || Entry.compFile.Hash == updateFile.Hash) continue; var changelogUrl = $"https://www.curseforge.com/minecraft/mc-mods/{fileIdToProjectSlug[int.Parse(updateFile.Id)]}/files/{updateFile.Id}"; // 设置更新日志与更新文件 if (Entry.UpdateFile is not null && updateFile.Hash == Entry.UpdateFile.Hash) { // 合并下载源 Entry.changelogUrls.Add(changelogUrl); Entry.UpdateFile.DownloadUrls.AddRange(updateFile.DownloadUrls); } else if (Entry.UpdateFile is null || updateFile.ReleaseDate > Entry.UpdateFile.ReleaseDate) { // 替换 Entry.changelogUrls = new List { changelogUrl }; Entry.UpdateFile = updateFile; } } ModBase.Log("[Mod] 从 CurseForge 获取 Mod 更新信息结束"); } catch (Exception ex) { ModBase.Log(ex, "从 CurseForge 获取本地 Mod 信息失败"); isFailed = true; } finally { Interlocked.Increment(ref endedThreadCount); } }, "Mod List Detail Loader CurseForge"); // 等待线程结束 while (endedThreadCount < 2) { if (loader.IsAborted) return; Thread.Sleep(10); } // 保存缓存 var cachedMods = mods.Where(m => m.Comp is not null).ToList(); ModBase.Log($"[Mod] 联网获取本地 Mod 信息完成,为 {cachedMods.Count} 个 Mod 更新缓存"); if (!cachedMods.Any()) return; foreach (var Entry in cachedMods) { Entry.compLoaded = !isFailed; cache[Entry.ModrinthHash + mcInstance + string.Join("", modLoaders)] = Entry.ToJson(); } ModBase.WriteFile(Path.Combine(ModBase.pathTemp, "Cache", "LocalComp.json"), cache.ToJsonString(ModBase.modeDebug ? new JsonSerializerOptions(JsonCompat.SerializerOptions) { WriteIndented = true } : null)); // 刷新 UI ModBase.RunInUi(() => { if (ModMain.frmInstanceMod?.Filter == PageInstanceCompResource.FilterType.CanUpdate) ModMain.frmInstanceMod?.RefreshUI(); else ModMain.frmInstanceMod?.RefreshBars(); }); } public static List GetCurrentVersionModLoader() { var modLoaders = new List(); if (PageInstanceLeft.McInstance.Info.HasForge) modLoaders.Add(CompLoaderType.Forge); if (PageInstanceLeft.McInstance.Info.HasNeoForge) modLoaders.Add(CompLoaderType.NeoForge); if (PageInstanceLeft.McInstance.Info.HasFabric) modLoaders.Add(CompLoaderType.Fabric); if (PageInstanceLeft.McInstance.Info.HasQuilt) modLoaders.AddRange(new[] { CompLoaderType.Fabric, CompLoaderType.Quilt }); if (PageInstanceLeft.McInstance.Info.HasLiteLoader) modLoaders.Add(CompLoaderType.LiteLoader); if (!modLoaders.Any()) modLoaders.AddRange(new[] { CompLoaderType.Forge, CompLoaderType.NeoForge, CompLoaderType.Fabric, CompLoaderType.LiteLoader, CompLoaderType.Quilt }); return modLoaders; } public static string GetPathNameByCompType(CompType theType) { switch (theType) { case CompType.Mod: { return "mods"; } case CompType.ResourcePack: { return "resourcepacks"; } case CompType.Shader: { return "shaderpacks"; } case CompType.Schematic: { return "schematics"; } case CompType.World: { return "saves"; } } return "Nothing"; } private static readonly Regex regexIsJarFile = new(@"\.jar(\.disabled)?$"); /// /// 通过文件名关键字和 Mod ID 比如 fabric apifabric-api 来获取给定实例 mods 目录中某个 Mod 的 /// 对象 ///
/// 为了不浪费性能,关键字统一用小写 ///
/// /// 如果文件名包含主关键字,以及其他关键字中的任意一个,同时 Mod ID 一致,即认为匹配,返回对应的对象,若没有匹配的文件则返回空值。 /// public static LocalCompFile GetModLocalCompByKeywords(McInstance instance, string modId, string mainKeyword, params string[] keywords) { if (modId is null) return null; return GetModLocalCompByKeywords(instance, new[] { modId }, mainKeyword, keywords); } public static LocalCompFile GetModLocalCompByKeywords(McInstance instance, string[] modIds, string mainKeyword, params string[] keywords) { if (!instance.Modable) return null; // 跳过不可安装 Mod 实例 var modFolder = $"{instance.PathInstance}mods"; if (!Directory.Exists(modFolder)) return null; // 确保 mods 目录存在 foreach (var file in Directory.EnumerateFiles(modFolder, $"*{mainKeyword}*")) { var lowerFilePath = file.ToLower(); // 统一转为小写 if (!regexIsJarFile.IsMatch(lowerFilePath)) continue; // 检查是否是 jar 文件 if ((keywords.Length > 0) && !keywords.Any(keyword => lowerFilePath.Contains(keyword))) continue; // 检查是否包含关键字 var localComp = new LocalCompFile(file); localComp.Load(); if (modIds.Any(modId => (localComp.ModId ?? "") == (modId ?? ""))) return localComp; } return null; } }