1
0

McVersion.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. using System.Globalization;
  2. namespace PCLCS;
  3. /// <summary>
  4. /// Minecraft 版本号和组件信息
  5. /// </summary>
  6. public sealed class McVersion {
  7. /// <summary>
  8. /// 原版版本名,如 26.1、26.1-snapshot-1、1.12.2、16w01a。
  9. /// </summary>
  10. public string VanillaName {
  11. get => _vanillaName;
  12. set {
  13. _vanillaName = value ?? "Unknown";
  14. Vanilla = NameToVersion(_vanillaName);
  15. }
  16. }
  17. private string _vanillaName = "Unknown";
  18. /// <summary>
  19. /// 可比较的三段式原版版本号。
  20. /// 旧格式会将次版本号移到主版本号,例如 1.20.1 会转换为 20.0.1,若没有版本号,例如旧快照,则为 9999.0.0。
  21. /// </summary>
  22. public Version Vanilla = new(9999, 0, 0);
  23. /// <summary> 原版版本号是否可靠(不是通过猜测获取)。</summary>
  24. public bool Reliable;
  25. /// <summary> 原版版本号是否有效。</summary>
  26. public bool Valid => Vanilla.Major < 1000;
  27. /// <summary> 可供比较的原版 Drop 序数,例如 26.3.2 为 263,1.21.5 为 210;若没有版本号,例如旧快照,则直接指定为 209。</summary>
  28. public int Drop => Valid ? Vanilla.Major * 10 + Vanilla.Minor : 209;
  29. /// <summary> 已安装的 Mod Loader,可能有多个。未安装时为 <see cref="ModLoaders.None"/>。</summary>
  30. public ModLoaders ModLoaders => ModLoaderVersions.Keys.Aggregate(ModLoaders.None, (result, loader) => result | loader);
  31. /// <summary> 已安装的各个 Mod Loader 及其版本;无法确定精确版本时为“未知版本”。</summary>
  32. public Dictionary<ModLoaders, string> ModLoaderVersions { get; } = [];
  33. /// <summary> OptiFine 版本名,如 C8、C9_pre10;未识别到时为 null。</summary>
  34. public string? OptiFine;
  35. /// <summary> OptiFine 是否以 Mod 形式安装。</summary>
  36. public bool OptiFineAsMod;
  37. /// <summary> 版本的发布时间。</summary>
  38. public DateTime ReleaseTime = new(1970, 1, 1, 15, 0, 0);
  39. /// <summary> 是否为愚人节版本。</summary>
  40. public bool IsFool;
  41. /// <summary> 是否为快照、预发布版、发布候选或实验性版本。</summary>
  42. public bool IsSnapshot;
  43. /// <summary> 是否识别到已安装的 OptiFine。</summary>
  44. public bool HasOptiFine => OptiFine is not null;
  45. /// <summary>
  46. /// 可供比较的 OptiFine 版本序数。
  47. /// </summary>
  48. public int OptiFineCode {
  49. get {
  50. if (OptiFine is null or "未知版本" || OptiFine.Length == 0) return 0;
  51. // 字母编号,如 G2 中的 G(7)
  52. int result = char.ToUpperInvariant(OptiFine[0]) - 'A' + 1;
  53. // 末尾数字,如 C5 beta4 中的 5
  54. result = result * 100 + (int.TryParse(OptiFine.Substring(1).RegexSeek(@"\d+"), out int mainVersion) ? mainVersion : 0);
  55. // 测试标记(正式版为 99,Pre[x] 为 50+x,Beta[x] 为 x)
  56. result *= 100;
  57. string? preview = OptiFine.RegexSeek(@"(?i)(pre|beta)\d*");
  58. if (preview is null) return result + 99;
  59. int sequence = int.TryParse(preview.RegexSeek(@"\d+"), out int value) ? value : 1; // 为 pre 或 beta 结尾,视作 1
  60. return result + (preview.StartsWithF("pre", true) ? 50 : 0) + sequence;
  61. }
  62. }
  63. /// <summary>
  64. /// 可供比较的类 Forge 版本序数。
  65. /// </summary>
  66. public int ForgelikeCode {
  67. get {
  68. if (!ModLoaderVersions.TryGetValue(ModLoaders.Forge, out string? version)) {
  69. ModLoaderVersions.TryGetValue(ModLoaders.NeoForge, out version);
  70. }
  71. if (version is null or "未知版本") return 0;
  72. // 提取所有数字段,以兼容 beta、alpha 和 snapshot 等非数字后缀。
  73. int[] segments = version.RegexSearch(@"\d+")
  74. .Select(segment => int.TryParse(segment, out int value) ? value : 0).ToArray();
  75. return segments.Length switch {
  76. >= 4 => segments[0] * 1000000 + segments[1] * 10000 + segments[3],
  77. 3 => segments[0] * 1000000 + segments[1] * 10000 + segments[2],
  78. 2 => segments[0] * 1000000 + segments[1] * 10000,
  79. 1 => segments[0] * 1000000,
  80. _ => 0
  81. };
  82. }
  83. }
  84. /// <summary>
  85. /// 创建默认 Minecraft 版本。
  86. /// </summary>
  87. /// <param name="vanillaName">原版版本名。</param>
  88. /// <param name="reliable">版本名是否来自可靠依据。</param>
  89. public McVersion(string vanillaName, bool reliable = true) {
  90. Reliable = reliable;
  91. VanillaName = vanillaName;
  92. IsFool = IsFoolName(VanillaName);
  93. IsSnapshot = IsSnapshotName(VanillaName);
  94. }
  95. /// <summary>
  96. /// 通过 Components 创建 Minecraft 版本信息。
  97. /// </summary>
  98. public McVersion(McComponents components) : this(components.MinecraftVersion) {
  99. OptiFine = components.OptiFineVersion?.AfterLast("HD_").AfterFirst("_");
  100. foreach (var loader in components.ModLoaderVersions) ModLoaderVersions[loader.Key] = loader.Value;
  101. }
  102. /// <summary>
  103. /// 通过 JSON 识别 Minecraft 版本和已安装组件。
  104. /// </summary>
  105. /// <param name="instanceName">实例文件夹名。</param>
  106. /// <param name="json">展开的版本 JSON。</param>
  107. /// <param name="jarVersionJson">版本 JAR 中的 version.json。</param>
  108. public McVersion(string instanceName, JObject json, JObject? jarVersionJson = null) {
  109. string jsonText = json.ToString(Formatting.None);
  110. DateTime releaseTime = new(1970, 1, 1, 15, 0, 0); // 未知版本也可能显示为 1970 年
  111. try {
  112. if (json["releaseTime"] is JToken releaseTimeToken) releaseTime = releaseTimeToken.ToObject<DateTime>();
  113. } catch {
  114. }
  115. bool reliable = true;
  116. // 获取游戏版本
  117. string vanillaName = (GetVanillaName(instanceName, json, jarVersionJson, releaseTime, ref reliable) ?? "Unknown").Replace("_unobfuscated", "").Replace(" Unobfuscated", "");
  118. Reliable = reliable;
  119. VanillaName = vanillaName;
  120. ReleaseTime = releaseTime;
  121. // 确定版本分类
  122. string jsonType = json.Value<string>("type") ?? "";
  123. // OptiFine
  124. OptiFine = jsonText.ContainsIgnoreCase("optifine") ? jsonText.RegexSeek(@"(?<=HD_U_)[^\"":/]+") ?? "未知版本": null;
  125. // 愚人节与快照版本
  126. DateTime fixedReleaseTime = releaseTime.ToUniversalTime().AddHours(2);
  127. IsFool = jsonType == "fool" || IsFoolName(vanillaName) || (fixedReleaseTime.Month == 4 && fixedReleaseTime.Day == 1 && jsonType == "snapshot");
  128. IsSnapshot = IsSnapshotName(vanillaName) || instanceName.ContainsIgnoreCase("combat") || jsonType is "snapshot" or "pending" or "fool";
  129. if (!Reliable) Logger.Info($"无法完全确认 MC 版本号的版本:{VanillaName}");
  130. // LiteLoader
  131. if (jsonText.ContainsIgnoreCase("liteloader")) {
  132. ModLoaderVersions[ModLoaders.LiteLoader] = jsonText.RegexSeek(@"(?<=com\.mumfrey:liteloader:)[^\"":/]+") ?? "未知版本";
  133. }
  134. // Fabric、Forge、NeoForge
  135. if (jsonText.ContainsIgnoreCase("net.fabricmc:fabric-loader") || jsonText.ContainsIgnoreCase("org.quiltmc:quilt-loader")) {
  136. ModLoaderVersions[ModLoaders.Fabric] = jsonText.RegexSeek(@"(?<=(net\.fabricmc:fabric-loader:|org\.quiltmc:quilt-loader:))[0-9\.]+(\+build.[0-9]+)?")?
  137. .Replace("+build", "") ?? "未知版本";
  138. } else if (jsonText.ContainsIgnoreCase("net.neoforge")) {
  139. // 1.20.1 JSON 范例:"--fml.forgeVersion", "47.1.99"
  140. // 1.20.2+ JSON 范例:"--fml.neoForgeVersion", "20.6.119-beta"
  141. ModLoaderVersions[ModLoaders.NeoForge] = FindArgumentValue(json, "--fml.neoForgeVersion") ?? FindArgumentValue(json, "--fml.forgeVersion") ?? "未知版本";
  142. } else if (jsonText.ContainsIgnoreCase("minecraftforge")) {
  143. ModLoaderVersions[ModLoaders.Forge] = jsonText.RegexSeek(@"(?<=forge:[0-9\.]+(?:_pre[0-9]*)?-)[0-9\.]+")
  144. ?? jsonText.RegexSeek(@"(?<=net\.minecraftforge:minecraftforge:)[0-9\.]+")
  145. ?? jsonText.RegexSeek(@"(?<=net\.minecraftforge:fmlloader:[0-9\.]+-)[0-9\.]+") ?? "未知版本";
  146. }
  147. }
  148. private static string? GetVanillaName(string instanceName, JObject json, JObject? jarVersionJson, DateTime releaseTime, ref bool reliable) {
  149. // 获取发布时间并判断是否为老版本
  150. if (releaseTime.Year is > 2000 and < 2013) return "Old";
  151. // 实验性快照
  152. if (json.Value<string>("type") == "pending") return "pending";
  153. // 从 PCL 下载的版本信息中获取版本号
  154. if (json["clientVersion"] is JToken clientVersion) return clientVersion.ToString();
  155. // 从 HMCL 下载的版本信息中获取版本号
  156. if (json["patches"] is JArray patches) {
  157. JToken? gameVersion = patches.OfType<JObject>().FirstOrDefault(patch => patch.Value<string>("id") == "game")?["version"];
  158. if (gameVersion is not null) return gameVersion.ToString();
  159. }
  160. // 从 Forge / NeoForge Arguments 中获取版本号
  161. if (FindArgumentValue(json, "--fml.mcVersion") is { Length: > 0 } fmlVersion) return fmlVersion;
  162. // 从继承版本中获取版本号
  163. string inherit = json["inheritsFrom"]?.ToString() ?? "";
  164. if (inherit.Length > 0) {
  165. string jar = json["jar"]?.ToString() ?? ""; // LiteLoader 优先使用 Jar
  166. return jar.Length > 0 ? jar : inherit;
  167. }
  168. // 从下载地址中获取版本号
  169. string? download = json["downloads"]?.ToString(Formatting.None).RegexSeek(@"(?<=launcher.mojang.com/mc/game/)[^/]*");
  170. if (download is not null) return download;
  171. // 从 Forge/OptiFine/Fabric 版本中获取版本号
  172. string librariesText = json["libraries"]!.ToString();
  173. string[] libraryPatterns = [
  174. @"(?<=net.minecraftforge:forge:)[0-9]{1,2}.[0-9+.]+",
  175. @"(?<=net.minecraftforge:fmlloader:)[0-9]{1,2}.[0-9+.]+",
  176. @"(?<=optifine:OptiFine:)[0-9]{1,2}.[0-9+.]+",
  177. @"(?<=((fabricmc)|(quiltmc)):intermediary:)[^\""]*"
  178. ];
  179. foreach (string pattern in libraryPatterns) {
  180. if (librariesText.RegexSeek(pattern) is string match) return match;
  181. }
  182. // 从 jar 项中获取版本号
  183. if (json["jar"] is JToken jarToken) return jarToken.ToString();
  184. // 从 jar 文件的 version.json 中获取版本号
  185. if (jarVersionJson?["name"] is JToken jarNameToken && jarNameToken.ToString() is { Length: < 32 } jarName) { // 因为 wiki 说这玩意儿可能是个 hash,虽然我没发现
  186. Logger.Info($"从版本 jar 中的 version.json 获取到版本号:{jarName}");
  187. return jarName;
  188. }
  189. 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)?";
  190. // 从 JSON 的 ID 中获取
  191. if (json["id"]!.ToString().RegexSeek(VersionPattern, RegexOptions.IgnoreCase) is string idMatch) return idMatch;
  192. // 非准确的版本判断警告
  193. Logger.Info($"无法完全确认 MC 版本号的版本:{instanceName}");
  194. reliable = false;
  195. // 从文件夹名中获取
  196. if (instanceName.RegexSeek(VersionPattern, RegexOptions.IgnoreCase) is string folderMatch) return folderMatch;
  197. // 从 JSON 出现的版本号中获取
  198. JObject withoutLibraries = (JObject) json.DeepClone();
  199. withoutLibraries.Remove("libraries");
  200. return withoutLibraries.ToString(Formatting.None).RegexSeek(VersionPattern, RegexOptions.IgnoreCase);
  201. }
  202. private static bool IsFoolName(string name) {
  203. name = name.Lower();
  204. return name.StartsWithF("2.0") || name is "15w14a" or "1.rv-pre1" or
  205. "3d shareware v1.34" or "20w14∞" or "22w13oneblockatatime" or "23w13a_or_b" or
  206. "24w14potato" or "25w14craftmine" or "26w14a" || name.StartsWithF("20w14inf");
  207. }
  208. private static bool IsSnapshotName(string name) =>
  209. new[] { "w", "snapshot", "rc", "pre", "experimental", "-" }.Any(name.ContainsIgnoreCase);
  210. /// <summary>
  211. /// 读取 arguments 中指定参数的字符串值。
  212. /// </summary>
  213. private static string? FindArgumentValue(JObject json, string name) {
  214. JArray? arguments = json["arguments"]?["game"] as JArray;
  215. if (arguments is null) return null;
  216. for (int i = 0; i + 1 < arguments.Count; i++) {
  217. if (arguments[i]?.ToString() == name) return arguments[i + 1]?.ToString();
  218. }
  219. return null;
  220. }
  221. /// <summary>
  222. /// 判断版本字符串是否符合 Minecraft 原版格式,例如 1.x、26.x。
  223. /// </summary>
  224. public static bool IsStandardFormat(string? version) {
  225. if (version is null) return false;
  226. if (Regex.IsMatch(version, @"^1\.\d")) return true;
  227. return double.TryParse(version.RegexSeek(@"^[2-9]\d\.\d+"), NumberStyles.Float,
  228. CultureInfo.InvariantCulture, out double value) && value >= 26;
  229. }
  230. /// <summary>
  231. /// 将版本名转换为 Drop 序数;无法转换时返回 209。
  232. /// </summary>
  233. public static int VersionToDrop(string? vanillaName) {
  234. if (vanillaName is null) return 209;
  235. Version version = NameToVersion(vanillaName);
  236. return version.Major >= 1000 ? 209 : version.Major * 10 + version.Minor;
  237. }
  238. /// <summary>
  239. /// 将 Drop 序数转换为版本字符串。
  240. /// </summary>
  241. public static string DropToVersion(int drop) {
  242. if (drop >= 250) return $"{drop / 10}.{drop % 10}";
  243. if (drop != 209) return $"1.{drop / 10}";
  244. Logger.Warn(new Exception("尝试将旧快照版 Drop 序数转换为版本字符串,这不应该发生!"));
  245. return "1.20";
  246. }
  247. /// <summary>
  248. /// 将版本名转换为可比较的三段式原版版本号。
  249. /// 对老版本格式,例如 1.20.3,会被转换为 20.0.3。
  250. /// 对愚人节版本,返回其基于的正式版版本号。
  251. /// 若没有版本号,例如旧快照,则为 9999.0.0。
  252. /// </summary>
  253. public static Version NameToVersion(string? name) {
  254. if (name is null) return new Version(9999, 0, 0);
  255. name = name.Lower().Replace("_unobfuscated", "").Replace(" unobfuscated", "");
  256. // 愚人节版本
  257. if (name.StartsWithF("2.0")) return NameToVersion("1.5.1");
  258. if (name == "15w14a") return NameToVersion("1.8.3");
  259. if (name.ContainsIgnoreCase(".rv-pre")) return NameToVersion("1.9.2");
  260. if (name.ContainsIgnoreCase("shareware")) return NameToVersion("1.13.2");
  261. if (name.StartsWithF("20w14") && name != "20w14a") return NameToVersion("1.15.2");
  262. if (name.ContainsIgnoreCase("oneblockatatime")) return NameToVersion("1.18.2");
  263. if (name.ContainsIgnoreCase("23w13a") && name != "20w13a") return NameToVersion("1.19.4");
  264. if (name == "24w14potato") return NameToVersion("1.20.4");
  265. if (name == "25w14craftmine") return NameToVersion("1.21.4");
  266. if (name == "26w14a") return NameToVersion("26.1.1");
  267. // 其余版本
  268. static int ParseSegment(string[] segments, int index) {
  269. if (index >= segments.Length) return 0;
  270. // 只读取该段开头连续的数字,忽略其后的预览版标记。
  271. return int.TryParse(segments[index].RegexSeek(@"^\d+"), out int value) ? value : 0;
  272. }
  273. string[] segments = name.Split([' ', '_', '-', '.'], StringSplitOptions.None);
  274. if (name.StartsWithF("1.")) {
  275. return new Version(ParseSegment(segments, 1), 0, ParseSegment(segments, 2));
  276. } else if (name.RegexCheck(@"^[2-9][0-9]\.")) {
  277. return new Version(ParseSegment(segments, 0), ParseSegment(segments, 1), ParseSegment(segments, 2));
  278. } else {
  279. return new Version(9999, 0, 0);
  280. }
  281. }
  282. }