using System.Globalization; namespace PCLCS; /// /// Minecraft 版本号和组件信息 /// public sealed class McVersion { /// /// 原版版本名,如 26.1、26.1-snapshot-1、1.12.2、16w01a。 /// public string VanillaName { get => _vanillaName; set { _vanillaName = value ?? "Unknown"; Vanilla = NameToVersion(_vanillaName); } } private string _vanillaName = "Unknown"; /// /// 可比较的三段式原版版本号。 /// 旧格式会将次版本号移到主版本号,例如 1.20.1 会转换为 20.0.1,若没有版本号,例如旧快照,则为 9999.0.0。 /// public Version Vanilla = new(9999, 0, 0); /// 原版版本号是否可靠(不是通过猜测获取)。 public bool Reliable; /// 原版版本号是否有效。 public bool Valid => Vanilla.Major < 1000; /// 可供比较的原版 Drop 序数,例如 26.3.2 为 263,1.21.5 为 210;若没有版本号,例如旧快照,则直接指定为 209。 public int Drop => Valid ? Vanilla.Major * 10 + Vanilla.Minor : 209; /// 已安装的 Mod Loader,可能有多个。未安装时为 。 public ModLoaders ModLoaders => ModLoaderVersions.Keys.Aggregate(ModLoaders.None, (result, loader) => result | loader); /// 已安装的各个 Mod Loader 及其版本;无法确定精确版本时为“未知版本”。 public Dictionary ModLoaderVersions { get; } = []; /// OptiFine 版本名,如 C8、C9_pre10;未识别到时为 null。 public string? OptiFine; /// OptiFine 是否以 Mod 形式安装。 public bool OptiFineAsMod; /// 版本的发布时间。 public DateTime ReleaseTime = new(1970, 1, 1, 15, 0, 0); /// 是否为愚人节版本。 public bool IsFool; /// 是否为快照、预发布版、发布候选或实验性版本。 public bool IsSnapshot; /// 是否识别到已安装的 OptiFine。 public bool HasOptiFine => OptiFine is not null; /// /// 可供比较的 OptiFine 版本序数。 /// public int OptiFineCode { get { if (OptiFine is null or "未知版本" || OptiFine.Length == 0) return 0; // 字母编号,如 G2 中的 G(7) int result = char.ToUpperInvariant(OptiFine[0]) - 'A' + 1; // 末尾数字,如 C5 beta4 中的 5 result = result * 100 + (int.TryParse(OptiFine.Substring(1).RegexSeek(@"\d+"), out int mainVersion) ? mainVersion : 0); // 测试标记(正式版为 99,Pre[x] 为 50+x,Beta[x] 为 x) result *= 100; string? preview = OptiFine.RegexSeek(@"(?i)(pre|beta)\d*"); if (preview is null) return result + 99; int sequence = int.TryParse(preview.RegexSeek(@"\d+"), out int value) ? value : 1; // 为 pre 或 beta 结尾,视作 1 return result + (preview.StartsWithF("pre", true) ? 50 : 0) + sequence; } } /// /// 可供比较的类 Forge 版本序数。 /// public int ForgelikeCode { get { if (!ModLoaderVersions.TryGetValue(ModLoaders.Forge, out string? version)) { ModLoaderVersions.TryGetValue(ModLoaders.NeoForge, out version); } if (version is null or "未知版本") return 0; // 提取所有数字段,以兼容 beta、alpha 和 snapshot 等非数字后缀。 int[] segments = version.RegexSearch(@"\d+") .Select(segment => int.TryParse(segment, out int value) ? value : 0).ToArray(); return segments.Length switch { >= 4 => segments[0] * 1000000 + segments[1] * 10000 + segments[3], 3 => segments[0] * 1000000 + segments[1] * 10000 + segments[2], 2 => segments[0] * 1000000 + segments[1] * 10000, 1 => segments[0] * 1000000, _ => 0 }; } } /// /// 创建默认 Minecraft 版本。 /// /// 原版版本名。 /// 版本名是否来自可靠依据。 public McVersion(string vanillaName, bool reliable = true) { Reliable = reliable; VanillaName = vanillaName; IsFool = IsFoolName(VanillaName); IsSnapshot = IsSnapshotName(VanillaName); } /// /// 通过 Components 创建 Minecraft 版本信息。 /// public McVersion(McComponents components) : this(components.MinecraftVersion) { OptiFine = components.OptiFineVersion?.AfterLast("HD_").AfterFirst("_"); foreach (var loader in components.ModLoaderVersions) ModLoaderVersions[loader.Key] = loader.Value; } /// /// 通过 JSON 识别 Minecraft 版本和已安装组件。 /// /// 实例文件夹名。 /// 展开的版本 JSON。 /// 版本 JAR 中的 version.json。 public McVersion(string instanceName, JObject json, JObject? jarVersionJson = null) { string jsonText = json.ToString(Formatting.None); DateTime releaseTime = new(1970, 1, 1, 15, 0, 0); // 未知版本也可能显示为 1970 年 try { if (json["releaseTime"] is JToken releaseTimeToken) releaseTime = releaseTimeToken.ToObject(); } catch { } bool reliable = true; // 获取游戏版本 string vanillaName = (GetVanillaName(instanceName, json, jarVersionJson, releaseTime, ref reliable) ?? "Unknown").Replace("_unobfuscated", "").Replace(" Unobfuscated", ""); Reliable = reliable; VanillaName = vanillaName; ReleaseTime = releaseTime; // 确定版本分类 string jsonType = json.Value("type") ?? ""; // OptiFine OptiFine = jsonText.ContainsIgnoreCase("optifine") ? jsonText.RegexSeek(@"(?<=HD_U_)[^\"":/]+") ?? "未知版本": null; // 愚人节与快照版本 DateTime fixedReleaseTime = releaseTime.ToUniversalTime().AddHours(2); IsFool = jsonType == "fool" || IsFoolName(vanillaName) || (fixedReleaseTime.Month == 4 && fixedReleaseTime.Day == 1 && jsonType == "snapshot"); IsSnapshot = IsSnapshotName(vanillaName) || instanceName.ContainsIgnoreCase("combat") || jsonType is "snapshot" or "pending" or "fool"; if (!Reliable) Logger.Info($"无法完全确认 MC 版本号的版本:{VanillaName}"); // LiteLoader if (jsonText.ContainsIgnoreCase("liteloader")) { ModLoaderVersions[ModLoaders.LiteLoader] = jsonText.RegexSeek(@"(?<=com\.mumfrey:liteloader:)[^\"":/]+") ?? "未知版本"; } // Fabric、Forge、NeoForge if (jsonText.ContainsIgnoreCase("net.fabricmc:fabric-loader") || jsonText.ContainsIgnoreCase("org.quiltmc:quilt-loader")) { ModLoaderVersions[ModLoaders.Fabric] = jsonText.RegexSeek(@"(?<=(net\.fabricmc:fabric-loader:|org\.quiltmc:quilt-loader:))[0-9\.]+(\+build.[0-9]+)?")? .Replace("+build", "") ?? "未知版本"; } else if (jsonText.ContainsIgnoreCase("net.neoforge")) { // 1.20.1 JSON 范例:"--fml.forgeVersion", "47.1.99" // 1.20.2+ JSON 范例:"--fml.neoForgeVersion", "20.6.119-beta" ModLoaderVersions[ModLoaders.NeoForge] = FindArgumentValue(json, "--fml.neoForgeVersion") ?? FindArgumentValue(json, "--fml.forgeVersion") ?? "未知版本"; } else if (jsonText.ContainsIgnoreCase("minecraftforge")) { ModLoaderVersions[ModLoaders.Forge] = jsonText.RegexSeek(@"(?<=forge:[0-9\.]+(?:_pre[0-9]*)?-)[0-9\.]+") ?? jsonText.RegexSeek(@"(?<=net\.minecraftforge:minecraftforge:)[0-9\.]+") ?? jsonText.RegexSeek(@"(?<=net\.minecraftforge:fmlloader:[0-9\.]+-)[0-9\.]+") ?? "未知版本"; } } private static string? GetVanillaName(string instanceName, JObject json, JObject? jarVersionJson, DateTime releaseTime, ref bool reliable) { // 获取发布时间并判断是否为老版本 if (releaseTime.Year is > 2000 and < 2013) return "Old"; // 实验性快照 if (json.Value("type") == "pending") return "pending"; // 从 PCL 下载的版本信息中获取版本号 if (json["clientVersion"] is JToken clientVersion) return clientVersion.ToString(); // 从 HMCL 下载的版本信息中获取版本号 if (json["patches"] is JArray patches) { JToken? gameVersion = patches.OfType().FirstOrDefault(patch => patch.Value("id") == "game")?["version"]; if (gameVersion is not null) return gameVersion.ToString(); } // 从 Forge / NeoForge Arguments 中获取版本号 if (FindArgumentValue(json, "--fml.mcVersion") is { Length: > 0 } fmlVersion) return fmlVersion; // 从继承版本中获取版本号 string inherit = json["inheritsFrom"]?.ToString() ?? ""; if (inherit.Length > 0) { string jar = json["jar"]?.ToString() ?? ""; // LiteLoader 优先使用 Jar return jar.Length > 0 ? jar : inherit; } // 从下载地址中获取版本号 string? download = json["downloads"]?.ToString(Formatting.None).RegexSeek(@"(?<=launcher.mojang.com/mc/game/)[^/]*"); if (download is not null) return download; // 从 Forge/OptiFine/Fabric 版本中获取版本号 string librariesText = json["libraries"]!.ToString(); string[] libraryPatterns = [ @"(?<=net.minecraftforge:forge:)[0-9]{1,2}.[0-9+.]+", @"(?<=net.minecraftforge:fmlloader:)[0-9]{1,2}.[0-9+.]+", @"(?<=optifine:OptiFine:)[0-9]{1,2}.[0-9+.]+", @"(?<=((fabricmc)|(quiltmc)):intermediary:)[^\""]*" ]; foreach (string pattern in libraryPatterns) { if (librariesText.RegexSeek(pattern) is string match) return match; } // 从 jar 项中获取版本号 if (json["jar"] is JToken jarToken) return jarToken.ToString(); // 从 jar 文件的 version.json 中获取版本号 if (jarVersionJson?["name"] is JToken jarNameToken && jarNameToken.ToString() is { Length: < 32 } jarName) { // 因为 wiki 说这玩意儿可能是个 hash,虽然我没发现 Logger.Info($"从版本 jar 中的 version.json 获取到版本号:{jarName}"); return jarName; } string VersionPattern = @"(([1-9][0-9]w[0-9]{2}[a-g])|((1|[2-9][0-9])\.[0-9]+(\.[0-9]+)?(-(pre|rc|snapshot-?)[1-9]*| Pre-Release( [1-9])?)?))(_unobfuscated)?"; // 从 JSON 的 ID 中获取 if (json["id"]!.ToString().RegexSeek(VersionPattern, RegexOptions.IgnoreCase) is string idMatch) return idMatch; // 非准确的版本判断警告 Logger.Info($"无法完全确认 MC 版本号的版本:{instanceName}"); reliable = false; // 从文件夹名中获取 if (instanceName.RegexSeek(VersionPattern, RegexOptions.IgnoreCase) is string folderMatch) return folderMatch; // 从 JSON 出现的版本号中获取 JObject withoutLibraries = (JObject) json.DeepClone(); withoutLibraries.Remove("libraries"); return withoutLibraries.ToString(Formatting.None).RegexSeek(VersionPattern, RegexOptions.IgnoreCase); } private static bool IsFoolName(string name) { name = name.Lower(); return name.StartsWithF("2.0") || name is "15w14a" or "1.rv-pre1" or "3d shareware v1.34" or "20w14∞" or "22w13oneblockatatime" or "23w13a_or_b" or "24w14potato" or "25w14craftmine" or "26w14a" || name.StartsWithF("20w14inf"); } private static bool IsSnapshotName(string name) => new[] { "w", "snapshot", "rc", "pre", "experimental", "-" }.Any(name.ContainsIgnoreCase); /// /// 读取 arguments 中指定参数的字符串值。 /// private static string? FindArgumentValue(JObject json, string name) { JArray? arguments = json["arguments"]?["game"] as JArray; if (arguments is null) return null; for (int i = 0; i + 1 < arguments.Count; i++) { if (arguments[i]?.ToString() == name) return arguments[i + 1]?.ToString(); } return null; } /// /// 判断版本字符串是否符合 Minecraft 原版格式,例如 1.x、26.x。 /// public static bool IsStandardFormat(string? version) { if (version is null) return false; if (Regex.IsMatch(version, @"^1\.\d")) return true; return double.TryParse(version.RegexSeek(@"^[2-9]\d\.\d+"), NumberStyles.Float, CultureInfo.InvariantCulture, out double value) && value >= 26; } /// /// 将版本名转换为 Drop 序数;无法转换时返回 209。 /// public static int VersionToDrop(string? vanillaName) { if (vanillaName is null) return 209; Version version = NameToVersion(vanillaName); return version.Major >= 1000 ? 209 : version.Major * 10 + version.Minor; } /// /// 将 Drop 序数转换为版本字符串。 /// public static string DropToVersion(int drop) { if (drop >= 250) return $"{drop / 10}.{drop % 10}"; if (drop != 209) return $"1.{drop / 10}"; Logger.Warn(new Exception("尝试将旧快照版 Drop 序数转换为版本字符串,这不应该发生!")); return "1.20"; } /// /// 将版本名转换为可比较的三段式原版版本号。 /// 对老版本格式,例如 1.20.3,会被转换为 20.0.3。 /// 对愚人节版本,返回其基于的正式版版本号。 /// 若没有版本号,例如旧快照,则为 9999.0.0。 /// public static Version NameToVersion(string? name) { if (name is null) return new Version(9999, 0, 0); name = name.Lower().Replace("_unobfuscated", "").Replace(" unobfuscated", ""); // 愚人节版本 if (name.StartsWithF("2.0")) return NameToVersion("1.5.1"); if (name == "15w14a") return NameToVersion("1.8.3"); if (name.ContainsIgnoreCase(".rv-pre")) return NameToVersion("1.9.2"); if (name.ContainsIgnoreCase("shareware")) return NameToVersion("1.13.2"); if (name.StartsWithF("20w14") && name != "20w14a") return NameToVersion("1.15.2"); if (name.ContainsIgnoreCase("oneblockatatime")) return NameToVersion("1.18.2"); if (name.ContainsIgnoreCase("23w13a") && name != "20w13a") return NameToVersion("1.19.4"); if (name == "24w14potato") return NameToVersion("1.20.4"); if (name == "25w14craftmine") return NameToVersion("1.21.4"); if (name == "26w14a") return NameToVersion("26.1.1"); // 其余版本 static int ParseSegment(string[] segments, int index) { if (index >= segments.Length) return 0; // 只读取该段开头连续的数字,忽略其后的预览版标记。 return int.TryParse(segments[index].RegexSeek(@"^\d+"), out int value) ? value : 0; } string[] segments = name.Split([' ', '_', '-', '.'], StringSplitOptions.None); if (name.StartsWithF("1.")) { return new Version(ParseSegment(segments, 1), 0, ParseSegment(segments, 2)); } else if (name.RegexCheck(@"^[2-9][0-9]\.")) { return new Version(ParseSegment(segments, 0), ParseSegment(segments, 1), ParseSegment(segments, 2)); } else { return new Version(9999, 0, 0); } } }