LTCat 1 hafta önce
ebeveyn
işleme
ade6e04c6e
65 değiştirilmiş dosya ile 3958 ekleme ve 2238 silme
  1. 1 1
      MeloongCore
  2. 10 2
      PCLCS/Configs.cs
  3. 6 2
      PCLCS/Constants.cs
  4. 3 3
      PCLCS/Java.cs
  5. 47 0
      PCLCS/Minecraft/MCUtils.cs
  6. 71 0
      PCLCS/Minecraft/McInstallManifest.cs
  7. 295 0
      PCLCS/Minecraft/McVersion.cs
  8. 423 0
      PCLCS/Minecraft/OptifineHashes.json
  9. 2 0
      PCLCS/PCLCS.csproj
  10. 115 115
      PCLCS/Resource/WikiEntries.txt
  11. 8 7
      Plain Craft Launcher 2/Application.xaml.vb
  12. 47 30
      Plain Craft Launcher 2/Controls/Behaviors/ClipboardInterceptor.vb
  13. 31 72
      Plain Craft Launcher 2/Controls/MyCard.vb
  14. 1 1
      Plain Craft Launcher 2/Controls/MyImage.vb
  15. 34 20
      Plain Craft Launcher 2/FormMain.xaml.vb
  16. 28 20
      Plain Craft Launcher 2/Modules/Base/ModBase.vb
  17. 4 4
      Plain Craft Launcher 2/Modules/Base/ModNet.vb
  18. 1 1
      Plain Craft Launcher 2/Modules/Base/ModValidate.vb
  19. 3 3
      Plain Craft Launcher 2/Modules/Minecraft/ModCrash.vb
  20. 2 4
      Plain Craft Launcher 2/Modules/Minecraft/ModDownload.vb
  21. 24 25
      Plain Craft Launcher 2/Modules/Minecraft/ModJava.vb
  22. 62 89
      Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb
  23. 193 565
      Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb
  24. 95 94
      Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb
  25. 3 0
      Plain Craft Launcher 2/Modules/Minecraft/ModWatcher.vb
  26. 27 10
      Plain Craft Launcher 2/Modules/ModEvent.vb
  27. 8 7
      Plain Craft Launcher 2/Modules/ModMain.vb
  28. 1 1
      Plain Craft Launcher 2/Modules/ModMusic.vb
  29. 4 4
      Plain Craft Launcher 2/Modules/Resource/LocalResourceFile.vb
  30. 6 10
      Plain Craft Launcher 2/Modules/Resource/LocalResourceLoaders.vb
  31. 2 2
      Plain Craft Launcher 2/Modules/Resource/ResourceProject.vb
  32. 13 15
      Plain Craft Launcher 2/Modules/Resource/ResourceSearcher.vb
  33. 9 8
      Plain Craft Launcher 2/Modules/Resource/ResourceVersion.vb
  34. 2 2
      Plain Craft Launcher 2/My Project/AssemblyInfo.vb
  35. 403 395
      Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb
  36. 195 34
      Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml
  37. 182 160
      Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb
  38. 727 0
      Plain Craft Launcher 2/Pages/PageDownload/PageDownloadModify.vb
  39. 56 36
      Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadCompDetail.xaml.vb
  40. 3 3
      Plain Craft Launcher 2/Pages/PageDownload/Resource/PageResource.xaml.vb
  41. 1 1
      Plain Craft Launcher 2/Pages/PageInstance/MyLocalModItem.xaml.vb
  42. 4 4
      Plain Craft Launcher 2/Pages/PageInstance/PageInstanceExport.xaml
  43. 18 18
      Plain Craft Launcher 2/Pages/PageInstance/PageInstanceExport.xaml.vb
  44. 13 5
      Plain Craft Launcher 2/Pages/PageInstance/PageInstanceLeft.xaml
  45. 21 3
      Plain Craft Launcher 2/Pages/PageInstance/PageInstanceLeft.xaml.vb
  46. 6 6
      Plain Craft Launcher 2/Pages/PageInstance/PageInstanceMod.xaml.vb
  47. 15 37
      Plain Craft Launcher 2/Pages/PageInstance/PageInstanceOverall.xaml.vb
  48. 1 1
      Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSetup.xaml.vb
  49. 19 3
      Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchLeft.xaml.vb
  50. 5 2
      Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml
  51. 22 7
      Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.vb
  52. 1 1
      Plain Craft Launcher 2/Pages/PageLaunch/PageLoginLegacy.xaml.vb
  53. 4 2
      Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml.vb
  54. 585 333
      Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml
  55. 4 3
      Plain Craft Launcher 2/Pages/PageOther/PageOtherHelp.xaml.vb
  56. 29 18
      Plain Craft Launcher 2/Pages/PageSelectLeft.xaml.vb
  57. 29 17
      Plain Craft Launcher 2/Pages/PageSelectRight.xaml.vb
  58. 1 5
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml
  59. 1 1
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml.vb
  60. 16 12
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml
  61. 12 13
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml.vb
  62. 1 0
      Plain Craft Launcher 2/Pages/PageSetup/Settings.vb
  63. 2 1
      Plain Craft Launcher 2/Pages/PageSpeedLeft.xaml.vb
  64. 1 0
      Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj
  65. BIN
      Plain Craft Launcher 2/Resources/Help.zip

+ 1 - 1
MeloongCore

@@ -1 +1 @@
-Subproject commit 811438aa3c7c0437c7e60e85b516cc593dcf6cb1
+Subproject commit c774ae4c85fd94e0eaa171db21e78a65870df19b

+ 10 - 2
PCLCS/Configs.cs

@@ -1,13 +1,21 @@
 namespace PCLCS;
 namespace PCLCS;
 public static class Configs {
 public static class Configs {
 
 
-    /// <summary>当前 Java 相关配置的版本。0 代表尚未从老的 Settings 系统中迁移。</summary>
+    public static readonly DynamicConfigEntry<McInstallManifest> McInstallManifest = new("MinecraftInstallManifest", null);
+
+    // Java
+
+    /// <summary> 当前 Java 相关配置的版本。0 代表尚未从老的 Settings 系统中迁移。</summary>
     public static readonly ConfigEntry<int> JavaConfigVersion = new("JavaConfigVersion", 0);
     public static readonly ConfigEntry<int> JavaConfigVersion = new("JavaConfigVersion", 0);
     public static readonly ConfigEntry<ConcurrentList<Java>> JavaList = new("JavaList", []);
     public static readonly ConfigEntry<ConcurrentList<Java>> JavaList = new("JavaList", []);
     public static readonly ConfigEntry<List<string>> JavaRemovedList = new("JavaRemovedList", []);
     public static readonly ConfigEntry<List<string>> JavaRemovedList = new("JavaRemovedList", []);
     public static readonly DynamicConfigEntry<bool> JavaMigrated = new("InstanceMigratedJava", false);
     public static readonly DynamicConfigEntry<bool> JavaMigrated = new("InstanceMigratedJava", false);
     public static readonly DynamicConfigEntry<Java> JavaForced = new("InstanceForcedJava", null);
     public static readonly DynamicConfigEntry<Java> JavaForced = new("InstanceForcedJava", null);
 
 
+    // 提示
+
+    public static readonly ConfigEntry<bool> HintModifyInstanceWarning = new("HintModifyInstanceWarning", false);
+
     static Configs() {
     static Configs() {
         /// 当 JavaForced 获取到一个不在 JavaList 中的值时,将其重置为 null。
         /// 当 JavaForced 获取到一个不在 JavaList 中的值时,将其重置为 null。
         JavaForced.PreviewGet += (args, provider) => {
         JavaForced.PreviewGet += (args, provider) => {
@@ -17,4 +25,4 @@ public static class Configs {
             }
             }
         };
         };
     }
     }
-}
+}

+ 6 - 2
PCLCS/Constants.cs

@@ -68,13 +68,17 @@ public static class Versions {
     /// <summary>
     /// <summary>
     /// 版本管理中 Mod 信息缓存的版本号。
     /// 版本管理中 Mod 信息缓存的版本号。
     /// </summary>
     /// </summary>
-    public const int LocalModCacheVersion = 19;
+    public const int LocalModCacheVersion = 21;
     /// <summary>
     /// <summary>
     /// Minecraft 本地实例信息缓存的版本号。
     /// Minecraft 本地实例信息缓存的版本号。
     /// </summary>
     /// </summary>
-    public const int McInstanceCacheVersion = 38;
+    public const int McInstanceCacheVersion = 39;
     /// <summary>
     /// <summary>
     /// Java 相关配置的版本号。
     /// Java 相关配置的版本号。
     /// </summary>
     /// </summary>
     public const int JavaConfigVersion = 1;
     public const int JavaConfigVersion = 1;
+    /// <summary>
+    /// Minecraft 安装清单的版本号。
+    /// </summary>
+    public const int McInstallManifestVersion = 1;
 }
 }

+ 3 - 3
PCLCS/Java.cs

@@ -87,9 +87,9 @@ namespace PCLCS;
 
 
     // 相同检查
     // 相同检查
     public bool Equals(Java? other)
     public bool Equals(Java? other)
-        => other is not null && string.Equals(Folder, other.Folder, StringComparison.OrdinalIgnoreCase);
+        => other is not null && PathUtils.IsSame(Folder, other.Folder);
     public override bool Equals(object? obj) => obj is Java other && Equals(other);
     public override bool Equals(object? obj) => obj is Java other && Equals(other);
-    public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Folder);
+    public override int GetHashCode() => StringComparer.CurrentCultureIgnoreCase.GetHashCode(PathUtils.ForCompare(Folder));
 
 
     /// <summary>用户友好的描述文本。</summary>
     /// <summary>用户友好的描述文本。</summary>
     public override string ToString()
     public override string ToString()
@@ -211,7 +211,7 @@ public static class JavaUtils {
                         }
                         }
                         // 结果
                         // 结果
                         var hasReparsePoint = HasReparsePoint(FileUtils.GetInfo(Path.Combine(targetFolder, "java.exe")));
                         var hasReparsePoint = HasReparsePoint(FileUtils.GetInfo(Path.Combine(targetFolder, "java.exe")));
-                        var isSpecialPath = targetFolder.Contains("java8path_target_") || targetFolder.Contains("javapath_target_") || targetFolder.Contains("javatmp") || targetFolder.Contains("system32");
+                        var isSpecialPath = targetFolder.ContainsIgnoreCase("java8path_target_") || targetFolder.ContainsIgnoreCase("javapath_target_") || targetFolder.ContainsIgnoreCase("javatmp") || targetFolder.ContainsIgnoreCase("system32");
                         results.Add((targetFolder, hasReparsePoint, isSpecialPath));
                         results.Add((targetFolder, hasReparsePoint, isSpecialPath));
                     }
                     }
                 } catch (UnauthorizedAccessException) {
                 } catch (UnauthorizedAccessException) {

+ 47 - 0
PCLCS/Minecraft/MCUtils.cs

@@ -0,0 +1,47 @@
+using System.Globalization;
+
+namespace PCLCS;
+
+public static class McUtils {
+
+    /// <summary>
+    /// 比较两个版本名;左侧较新返回正数,相同返回 0,右侧较新返回负数。
+    /// 无法比较两个预发布版的大小。
+    /// 支持的格式:未知版本, 26.1-snapshot-1,1.13.2, 1.7.10-pre4, 1.8_pre, 1.14 Pre-Release 2, 1.14.4 C6
+    /// </summary>
+    public static int CompareVersion(string left, string right) {
+        if (left == right) return 0;
+        if (left == "未知版本" || right == "未知版本") return left == "未知版本" ? 1 : -1;
+        left = left.Lower();
+        right = right.Lower();
+        var leftParts = left.Replace("快照", "snapshot").Replace("预览版", "pre").RegexSearch("[a-z]+|[0-9]+").ToList();
+        var rightParts = right.Replace("快照", "snapshot").Replace("预览版", "pre").RegexSearch("[a-z]+|[0-9]+").ToList();
+        for (int i = 0; ; i++) {
+            if (i >= leftParts.Count && i >= rightParts.Count) return string.Compare(left, right, StringComparison.Ordinal); // 两边均缺失,感觉是一个东西
+            string leftValue = i < leftParts.Count ? leftParts[i] : "0";
+            string rightValue = i < rightParts.Count ? rightParts[i] : "0";
+            if (leftValue == rightValue) continue;
+            int leftNumber = leftValue switch {
+                "rc" => -1,
+                "pre" => -2,
+                "snapshot" => -3,
+                "experimental" => -4,
+                _ => int.TryParse(leftValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out int leftResult) ? leftResult : 0
+            };
+            int rightNumber = rightValue switch {
+                "rc" => -1,
+                "pre" => -2,
+                "snapshot" => -3,
+                "experimental" => -4,
+                _ => int.TryParse(rightValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out int rightResult) ? rightResult : 0
+            };
+            if (leftNumber == 0 && rightNumber == 0) return string.Compare(leftValue, rightValue, StringComparison.Ordinal);
+            if (leftNumber != rightNumber) return leftNumber.CompareTo(rightNumber);
+        }
+    }
+    /// <summary>
+    /// 比较两个版本名;左侧不旧于右侧返回 True。
+    /// </summary>
+    public static bool CompareVersionGE(string left, string right) => CompareVersion(left, right) >= 0;
+
+}

+ 71 - 0
PCLCS/Minecraft/McInstallManifest.cs

@@ -0,0 +1,71 @@
+namespace PCLCS;
+
+/// <summary>
+/// MC 版本安装清单及 PCL 管理文件清单。
+/// </summary>
+/// <param name="SchemaVersion"> 清单格式版本;无版本的旧清单为 0。</param>
+/// <param name="Components"> 安装目标及受管理组件版本。</param>
+/// <param name="LastModifiedUtc"> 最后修改时间(UTC)。</param>
+/// <param name="ManagedFiles"> 受管理文件;没有文件时为空列表。</param>
+public record McInstallManifest(int SchemaVersion, McComponents Components, DateTime LastModifiedUtc, IReadOnlyList<McInstallManifest.ManagedFile> ManagedFiles) {
+    /// <summary>
+    /// 清理以 Mod 形式安装的、已删除或修改的组件的记录,并更新清单。
+    /// </summary>
+    public McInstallManifest RefreshModComponents(string gameFolder) {
+        List<ManagedFile> invalidFiles = [];
+        foreach (ManagedFile entry in ManagedFiles) {
+            if (entry.Component is null) continue; // 不检查非组件文件
+            FileInfo file = new(Path.Combine(gameFolder, entry.RelativePath));
+            if (!file.Exists || file.LastWriteTimeUtc != entry.LastWriteTimeUtc.ToUniversalTime()) invalidFiles.Add(entry);
+        }
+        if (!invalidFiles.Any()) return this;
+        return this with {
+            Components = Components with {
+                OptiFineEntry = invalidFiles.Any(file => file.Component == "OptiFine") ? null : Components.OptiFineEntry,
+                FabricApiVersion = invalidFiles.Any(file => file.Component == "FabricApi") ? null : Components.FabricApiVersion,
+                OptiFabricVersion = invalidFiles.Any(file => file.Component == "OptiFabric") ? null : Components.OptiFabricVersion
+            },
+            ManagedFiles = ManagedFiles.Where(file => !invalidFiles.Contains(file)).ToList(),
+            LastModifiedUtc = DateTime.UtcNow
+        };
+    }
+
+    /// <summary>
+    /// 由 PCL 管理并记录的版本内文件。
+    /// </summary>
+    /// <param name="RelativePath"> 相对于游戏目录的文件路径。确保路径为全小写且可比较。</param>
+    /// <param name="Sha1"> 小写的 SHA1。</param>
+    /// <param name="LastWriteTimeUtc"> 目标文件的最后修改时间(UTC)。</param>
+    /// <param name="Component"> 安装器组件名;为空时表示文件由整合包管理。</param>
+    public record ManagedFile(string RelativePath, string Sha1, DateTime LastWriteTimeUtc,
+        [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Component = null);
+}
+
+/// <param name="MinecraftVersion"> 目标 Minecraft 版本。</param>
+/// <param name="ModLoaderVersions"> 目标中各个 Mod Loader 的版本;原版为空字典。</param>
+/// <param name="OptiFineEntry"> OptiFine 条目信息,不安装时为 <see langword="null"/>。</param>
+/// <param name="FabricApiVersion"> 由 PCL 管理的 Fabric API 版本;不安装时为 <see langword="null"/>。</param>
+/// <param name="OptiFabricVersion"> 由 PCL 管理的 OptiFabric 版本;不安装时为 <see langword="null"/>。</param>
+[method: JsonConstructor]
+public record McComponents(string MinecraftVersion, Dictionary<ModLoaders, string> ModLoaderVersions,
+        [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] JObject? OptiFineEntry = null,
+        [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? FabricApiVersion = null,
+        [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? OptiFabricVersion = null) {
+    [JsonIgnore]
+    public string? OptiFineVersion => OptiFineEntry?["InstanceName"]?.Value<string>()?.AfterLast("-OptiFine_");
+    public McComponents(string minecraftVersion, JObject? optiFineEntry, string? forgeVersion,
+        string? neoForgeVersion, string? fabricVersion, string? fabricApiVersion, string? optiFabricVersion, string? liteLoaderVersion)
+        : this(minecraftVersion, [], optiFineEntry, fabricApiVersion, optiFabricVersion) {
+        if (forgeVersion is not null) ModLoaderVersions[ModLoaders.Forge] = forgeVersion;
+        if (neoForgeVersion is not null) ModLoaderVersions[ModLoaders.NeoForge] = neoForgeVersion;
+        if (fabricVersion is not null) ModLoaderVersions[ModLoaders.Fabric] = fabricVersion;
+        if (liteLoaderVersion is not null) ModLoaderVersions[ModLoaders.LiteLoader] = liteLoaderVersion;
+    }
+    public bool IsEquivalentTo(McComponents other) =>
+        MinecraftVersion == other.MinecraftVersion &&
+        OptiFineVersion == other.OptiFineVersion &&
+        FabricApiVersion == other.FabricApiVersion &&
+        OptiFabricVersion == other.OptiFabricVersion &&
+        ModLoaderVersions.Count == other.ModLoaderVersions.Count &&
+        ModLoaderVersions.All(entry => other.ModLoaderVersions.TryGetValue(entry.Key, out string? version) && version == entry.Value);
+}

+ 295 - 0
PCLCS/Minecraft/McVersion.cs

@@ -0,0 +1,295 @@
+using System.Globalization;
+
+namespace PCLCS;
+
+/// <summary>
+/// Minecraft 版本号和组件信息
+/// </summary>
+public sealed class McVersion {
+    /// <summary>
+    /// 原版版本名,如 26.1、26.1-snapshot-1、1.12.2、16w01a。
+    /// </summary>
+    public string VanillaName {
+        get => _vanillaName;
+        set {
+            _vanillaName = value ?? "Unknown";
+            Vanilla = NameToVersion(_vanillaName);
+        }
+    }
+    private string _vanillaName = "Unknown";
+    /// <summary>
+    /// 可比较的三段式原版版本号。
+    /// 旧格式会将次版本号移到主版本号,例如 1.20.1 会转换为 20.0.1,若没有版本号,例如旧快照,则为 9999.0.0。
+    /// </summary>
+    public Version Vanilla = new(9999, 0, 0);
+    /// <summary> 原版版本号是否可靠(不是通过猜测获取)。</summary>
+    public bool Reliable;
+    /// <summary> 原版版本号是否有效。</summary>
+    public bool Valid => Vanilla.Major < 1000;
+    /// <summary> 可供比较的原版 Drop 序数,例如 26.3.2 为 263,1.21.5 为 210;若没有版本号,例如旧快照,则直接指定为 209。</summary>
+    public int Drop => Valid ? Vanilla.Major * 10 + Vanilla.Minor : 209;
+    /// <summary> 已安装的 Mod Loader,可能有多个。未安装时为 <see cref="ModLoaders.None"/>。</summary>
+    public ModLoaders ModLoaders => ModLoaderVersions.Keys.Aggregate(ModLoaders.None, (result, loader) => result | loader);
+    /// <summary> 已安装的各个 Mod Loader 及其版本;无法确定精确版本时为“未知版本”。</summary>
+    public Dictionary<ModLoaders, string> ModLoaderVersions { get; } = [];
+    /// <summary> OptiFine 版本名,如 C8、C9_pre10;未识别到时为 null。</summary>
+    public string? OptiFine;
+    /// <summary> OptiFine 是否以 Mod 形式安装。</summary>
+    public bool OptiFineAsMod;
+    /// <summary> 版本的发布时间。</summary>
+    public DateTime ReleaseTime = new(1970, 1, 1, 15, 0, 0);
+    /// <summary> 是否为愚人节版本。</summary>
+    public bool IsFool;
+    /// <summary> 是否为快照、预发布版、发布候选或实验性版本。</summary>
+    public bool IsSnapshot;
+    /// <summary> 是否识别到已安装的 OptiFine。</summary>
+    public bool HasOptiFine => OptiFine is not null;
+    /// <summary>
+    /// 可供比较的 OptiFine 版本序数。
+    /// </summary>
+    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;
+        }
+    }
+    /// <summary>
+    /// 可供比较的类 Forge 版本序数。
+    /// </summary>
+    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
+            };
+        }
+    }
+
+    /// <summary>
+    /// 创建默认 Minecraft 版本。
+    /// </summary>
+    /// <param name="vanillaName">原版版本名。</param>
+    /// <param name="reliable">版本名是否来自可靠依据。</param>
+    public McVersion(string vanillaName, bool reliable = true) {
+        Reliable = reliable;
+        VanillaName = vanillaName;
+        IsFool = IsFoolName(VanillaName);
+        IsSnapshot = IsSnapshotName(VanillaName);
+    }
+
+    /// <summary>
+    /// 通过 Components 创建 Minecraft 版本信息。
+    /// </summary>
+    public McVersion(McComponents components) : this(components.MinecraftVersion) {
+        OptiFine = components.OptiFineVersion?.AfterLast("HD_").AfterFirst("_");
+        foreach (var loader in components.ModLoaderVersions) ModLoaderVersions[loader.Key] = loader.Value;
+    }
+
+    /// <summary>
+    /// 通过 JSON 识别 Minecraft 版本和已安装组件。
+    /// </summary>
+    /// <param name="instanceName">实例文件夹名。</param>
+    /// <param name="json">展开的版本 JSON。</param>
+    /// <param name="jarVersionJson">版本 JAR 中的 version.json。</param>
+    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<DateTime>();
+        } 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<string>("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<string>("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<JObject>().FirstOrDefault(patch => patch.Value<string>("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);
+
+    /// <summary>
+    /// 读取 arguments 中指定参数的字符串值。
+    /// </summary>
+    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;
+    }
+
+    /// <summary>
+    /// 判断版本字符串是否符合 Minecraft 原版格式,例如 1.x、26.x。
+    /// </summary>
+    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;
+    }
+
+    /// <summary>
+    /// 将版本名转换为 Drop 序数;无法转换时返回 209。
+    /// </summary>
+    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;
+    }
+
+    /// <summary>
+    /// 将 Drop 序数转换为版本字符串。
+    /// </summary>
+    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";
+    }
+
+    /// <summary>
+    /// 将版本名转换为可比较的三段式原版版本号。
+    /// 对老版本格式,例如 1.20.3,会被转换为 20.0.3。
+    /// 对愚人节版本,返回其基于的正式版版本号。
+    /// 若没有版本号,例如旧快照,则为 9999.0.0。
+    /// </summary>
+    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);
+        }
+    }
+}

+ 423 - 0
PCLCS/Minecraft/OptifineHashes.json

@@ -0,0 +1,423 @@
+{
+  "f3a3c9d792bdb48595405d52c02cc7baf1db4ecb": "OptiFine_1.7.2_HD_U_E3.jar",
+  "657169c421ce280f80c73b05ab71ad582d2b37fd": "OptiFine_1.7.2_HD_U_E4.jar",
+  "99f20d449912cd860b409e4679c54dd8da43d55e": "OptiFine_1.7.2_HD_U_E7.jar",
+  "8253f6ed418a2a61a612cc9e69fd1fea1d11fa7d": "OptiFine_1.7.2_HD_U_E8.jar",
+  "379e52fc02cd75562d201b0c505269b6109b2136": "OptiFine_1.7.2_HD_U_F3.jar",
+  "fb04b91e47b5eaa172d2b76c843e488dba8f909b": "OptiFine_1.7.2_HD_U_F7.jar",
+  "b4b6c2c99f5734e4267030a47f2eaad2796eb2a7": "OptiFine_1.7.10_HD_U_D3.jar",
+  "a0b533bc97b24fe771f5b08536c6339d04f92bae": "OptiFine_1.7.10_HD_U_D4.jar",
+  "5e9327dd05a1e762c23c3c0227445d90f193922f": "OptiFine_1.7.10_HD_U_D6.jar",
+  "7a7d2adf21038d241dd375faa7316fb2c74d34d3": "OptiFine_1.7.10_HD_U_D7.jar",
+  "2345677be44dd56a0ae5b90e747a590c3cc00ff8": "OptiFine_1.7.10_HD_U_D8.jar",
+  "23f133173428aee712eb11f200b461e3114bde31": "OptiFine_1.7.10_HD_U_E3.jar",
+  "2130d7266224da6a7325051e37444f7e7c19277c": "OptiFine_1.7.10_HD_U_E7.jar",
+  "55c9c475ecd0f62cc1776ce52be04c5937112936": "OptiFine_1.8.0_HD_U_H5.jar",
+  "bb2c2997cdc66cfb3c311a3dc065752a4845c1a8": "OptiFine_1.8.0_HD_U_H6.jar",
+  "692b0b7e878c3416f21b4347fab4516f3cc86dfc": "OptiFine_1.8.0_HD_U_H7.jar",
+  "5464962b66b9b0dacb3d787e6a71d2365ae50bca": "OptiFine_1.8.0_HD_U_H8.jar",
+  "b7718b03111bec04036fd3352ce374eea273171c": "OptiFine_1.8.0_HD_U_I3.jar",
+  "0d1dcf64b4d104a71e8f65a1362a163ed81a9030": "OptiFine_1.8.0_HD_U_I7.jar",
+  "e5e5eb03748af0fa160b502a28359c0144e254d2": "OptiFine_1.8.8_HD_U_H5.jar",
+  "9294bc4562a143c35ece03a6b3d3a000109def5f": "OptiFine_1.8.8_HD_U_H6.jar",
+  "1c06c011904ab22952a96f73d4b575a5c265d207": "OptiFine_1.8.8_HD_U_H7.jar",
+  "3984e4dd222bb62cb7e28e4d3092029cc7534fd1": "OptiFine_1.8.8_HD_U_H8.jar",
+  "0dc33a9761ceac19ca0bbb1ea6813c0160d43aa1": "OptiFine_1.8.8_HD_U_I3.jar",
+  "30cbf4d61052cbfb736a78117da333df23c129a3": "OptiFine_1.8.8_HD_U_I7.jar",
+  "d8240cfa7387b994fdf2a64bdc8a8860857adc18": "OptiFine_1.8.9_HD_U_H5.jar",
+  "2636689b31810896cf80493f6fc4d8c1783048ea": "OptiFine_1.8.9_HD_U_H6.jar",
+  "e4544c9210bc056abffa1986a33c4e72623d4f31": "OptiFine_1.8.9_HD_U_H7.jar",
+  "4b73738913453bcbca969d3e77f05ae0cf5b8a7e": "OptiFine_1.8.9_HD_U_H8.jar",
+  "d5cd8b902077075bbf1d865cfd5b3362a6863ace": "OptiFine_1.8.9_HD_U_I3.jar",
+  "58bec8f8766918dcad2b8412f1fbb6f30cc9319f": "OptiFine_1.8.9_HD_U_I7.jar",
+  "dcab4849a24ccad5a6a3496c67bc5a88411d2a35": "OptiFine_1.8.9_HD_U_L5.jar",
+  "d362d58a28f5373b141b9e426e8e160638bfafcd": "OptiFine_1.8.9_HD_U_M5.jar",
+  "1f9dcc9735e94ebb5195c4014300ea462eb12878": "preview_OptiFine_1.8.9_HD_U_L6_pre1.jar",
+  "6e0b3b86e51c2647c98e277475ee7e68f32a0d0e": "preview_OptiFine_1.8.9_HD_U_M5_pre2.jar",
+  "7287fcd1a415ab87eca5e89457cc3507aa3fa579": "preview_OptiFine_1.8.9_HD_U_M6_pre1.jar",
+  "0bb792be16ec78d8b62586cc4856950befb3391b": "preview_OptiFine_1.8.9_HD_U_M6_pre2.jar",
+  "fd87c53197312614c34669feb7608f74a30da712": "OptiFine_1.9.0_HD_U_B1.jar",
+  "f99941037f3633926ffebb3eda67f296a701c019": "OptiFine_1.9.0_HD_U_B2.jar",
+  "34c47106cda1371ecf13e61aed4c1b53a3a53163": "OptiFine_1.9.0_HD_U_B3.jar",
+  "ebc7258e13344097588c73a0b48457083069e1df": "OptiFine_1.9.0_HD_U_B5.jar",
+  "999df92e8904f9268e549135edbec322d332ef62": "OptiFine_1.9.0_HD_U_D7.jar",
+  "0fe031e95f9c01481c9376d9a1df68f9e9857db8": "OptiFine_1.9.0_HD_U_D8.jar",
+  "f4bc2eb5e039e842eb07181ea564eba487513228": "OptiFine_1.9.0_HD_U_E3.jar",
+  "78870a94ec3c4db8aebc4c29d4d39bdda2bf05da": "OptiFine_1.9.0_HD_U_E7.jar",
+  "d3d55bb37e76d1932cb3ad9dde4b9127993f856d": "OptiFine_1.9.0_HD_U_H5.jar",
+  "b5faec3b84f42dd396512a3ce7c1b57c0b972b70": "OptiFine_1.9.0_HD_U_I5.jar",
+  "92742ecac143b1d28c215ca145bd9775a01c5085": "preview_OptiFine_1.9.0_HD_U_I5_pre1.jar",
+  "c87029938376017deba9108322bcb8424d64e92c": "OptiFine_1.9.2_HD_U_B1.jar",
+  "d3e5e1fa480e66603ca037d74966a95d25968d51": "OptiFine_1.9.2_HD_U_B2.jar",
+  "fbd545f921d098235a28f87cda55a0504e831954": "OptiFine_1.9.2_HD_U_B3.jar",
+  "1ae8e6daeeccb6ee48618e8ccdc0b6276d47944f": "OptiFine_1.9.2_HD_U_D7.jar",
+  "3350c829b29d170e9ac19809a3ae45cb099277b5": "OptiFine_1.9.2_HD_U_D8.jar",
+  "c032b2ae0b6087077efef8ae89b612e0d4ea81b7": "OptiFine_1.9.2_HD_U_E3.jar",
+  "3d8cbc19bcad9e9ecc98f607e10f65673a09b482": "OptiFine_1.9.4_HD_U_B4.jar",
+  "b6b9e66b34ddcda5294b016602fb17d741ff6f54": "OptiFine_1.9.4_HD_U_B5.jar",
+  "060ddd5a0b7b7ac5586de04ae53cf923ae5c7290": "OptiFine_1.9.4_HD_U_B6.jar",
+  "2c39611a89dc583cdb403f402c2efd2bd8c6da63": "OptiFine_1.9.4_HD_U_D7.jar",
+  "4b26926208de270225eb9d3af24a4ff3841238f5": "OptiFine_1.9.4_HD_U_D8.jar",
+  "012f82fce41aee5e6e56e551d450a07b2b361646": "OptiFine_1.9.4_HD_U_E3.jar",
+  "cf9c075fa71dab34cdf1c376a76bd08295c69ce6": "OptiFine_1.9.4_HD_U_E7.jar",
+  "b3a76b92e0e9594f6fa135f1156b9cc13712db20": "OptiFine_1.9.4_HD_U_H5.jar",
+  "68a206db0d425dd85f6eba33e52122b01e844c46": "OptiFine_1.9.4_HD_U_I5.jar",
+  "2e48d20605340cfad844a6587d3848e328fd4399": "preview_OptiFine_1.9.4_HD_U_I5_pre1.jar",
+  "d7a1e5f40e28fa3283b82bf3af7d3f433b91df08": "OptiFine_1.10_HD_U_B6.jar",
+  "717a02b21136e022cdb4d7d6ec6a4125f4ed2312": "OptiFine_1.10_HD_U_B7.jar",
+  "0b5d1719592a57cf414643c5b56f4bb3c23a5b94": "OptiFine_1.10_HD_U_C1.jar",
+  "236747126edcf0c97fc6a20fa7f6491accbf0108": "OptiFine_1.10_HD_U_D7.jar",
+  "c5fd78710e821f06f871994032e951065c15c40d": "OptiFine_1.10_HD_U_D8.jar",
+  "1bea8b0bae04ca4b25c6824a6f23a5442d5aa926": "OptiFine_1.10_HD_U_E3.jar",
+  "7a7195348ea583458b657573b5fcc72e4adc8308": "OptiFine_1.10_HD_U_E7.jar",
+  "3bcc46f718711cd6fe3db49de4cc810b75420de8": "OptiFine_1.10_HD_U_H5.jar",
+  "a0070afe9c1c0ced4bcfcda85359e07917d569bd": "OptiFine_1.10_HD_U_I5.jar",
+  "f9a36eb0246ad6ba303e670082be2c2515c50dcd": "preview_OptiFine_1.10_HD_U_I5_pre1.jar",
+  "3ea5dae651b6d64900562d1cde5070ebd9db2936": "OptiFine_1.10.2_HD_U_C1.jar",
+  "41968442e8d1e30e9a32e7a5594caca22312d9d6": "OptiFine_1.10.2_HD_U_C2.jar",
+  "9c8bc96d3125199e96f5dbfda685ea5dd92c253e": "OptiFine_1.10.2_HD_U_C3.jar",
+  "7ec4ccc213b482936baaffb9f1937f7147796893": "OptiFine_1.10.2_HD_U_D1.jar",
+  "3ee5aecc8e11567fb46de0b94e23cf1428f4c9fc": "OptiFine_1.10.2_HD_U_D2.jar",
+  "98059ed7e93d0fc058203569ccfe965b022fb18f": "OptiFine_1.10.2_HD_U_D3.jar",
+  "c3165d0a0207d72e18f4533e7aa5ca30e96819a9": "OptiFine_1.10.2_HD_U_D4.jar",
+  "347359aef422d64711b470f4cf0803cb072da0fa": "OptiFine_1.10.2_HD_U_D6.jar",
+  "4ddf7c45e00839031f5793451541bca002f9257f": "OptiFine_1.10.2_HD_U_D7.jar",
+  "5c3685e279c664a818c41ca213b2bc9bfc4340b0": "OptiFine_1.10.2_HD_U_D8.jar",
+  "129581af3d7d73b506b42d822cb2d3fce68c8e2e": "OptiFine_1.10.2_HD_U_E3.jar",
+  "f052d269109154d851cc7a8e766c78802a76e9b2": "OptiFine_1.10.2_HD_U_E7.jar",
+  "f285cfe6753c5d987e4cb0940da05e9513334d98": "OptiFine_1.10.2_HD_U_H5.jar",
+  "12fd8dd33150f9e4436f23bac08265305f949e20": "OptiFine_1.10.2_HD_U_I5.jar",
+  "adac69b25bb95242422a635965c7f17503c694be": "preview_OptiFine_1.10.2_HD_U_I5_pre1.jar",
+  "06031e4c828b66facefdfa32cd29c585909d9e45": "OptiFine_1.11_HD_U_B1.jar",
+  "449aef14cee17cf1c79d4bad73b69107202d3c1c": "OptiFine_1.11_HD_U_B2.jar",
+  "7663e1ee572c742e96a71a355f0775baf7cbc6ef": "OptiFine_1.11_HD_U_B3.jar",
+  "bde9d34f84a030734c7b9d09eda700c7c828eb96": "OptiFine_1.11_HD_U_B5.jar",
+  "579cfb75b86f3404c5f6e8f694c51b09b7c9b261": "OptiFine_1.11_HD_U_B6.jar",
+  "350af22b31dbc27d1b928e7144582fd361deb193": "OptiFine_1.11_HD_U_B7.jar",
+  "236161183cc5cd5faf84dd27824cc3eaa107ed5f": "OptiFine_1.11_HD_U_B8.jar",
+  "9ca32d42c7e5ad9de9665ffee9ddd41dddd33b66": "OptiFine_1.11_HD_U_C3.jar",
+  "2e45de9e9fb385f24c6eecb1a64537de78778105": "OptiFine_1.11_HD_U_C7.jar",
+  "32df58fec7148e75b599b18691f80e69b7f9cd1b": "OptiFine_1.11_HD_U_F5.jar",
+  "71057cb7a3d0971c3b56e1fbbc333304a72e1352": "OptiFine_1.11_HD_U_G5.jar",
+  "c2ee1e1019ab26bee56447f4df8a2639d2029925": "preview_OptiFine_1.11_HD_U_G5_pre1.jar",
+  "083a002215e2174c0d40af20d2f3844ea349a354": "OptiFine_1.11.2_HD_U_B5.jar",
+  "eb92ce00048faa95b184d121fa667074532fb9db": "OptiFine_1.11.2_HD_U_B6.jar",
+  "7449a2db44e86303d588b526dc73127c16b0e94f": "OptiFine_1.11.2_HD_U_B7.jar",
+  "3e1006c935fe27d83459e77d336460ac5b5c81b4": "OptiFine_1.11.2_HD_U_B8.jar",
+  "049e7e134e8c7364ab61810d84289394e17921de": "OptiFine_1.11.2_HD_U_B9.jar",
+  "4871f2edf7ded30c374aa770e9ea4adcc0b786d0": "OptiFine_1.11.2_HD_U_C1.jar",
+  "e7a54d9e203a1b951f8373ab6dcdedb348e536b1": "OptiFine_1.11.2_HD_U_C2.jar",
+  "8d7fd0facc2299f3dad9dc7c19cc13f72a780343": "OptiFine_1.11.2_HD_U_C3.jar",
+  "20db26eaafb1c4ebd99e1dc2fffe4a62456bf60e": "OptiFine_1.11.2_HD_U_C7.jar",
+  "e4efcf05086389fe18d6fe62eb8ab7512a07323c": "OptiFine_1.11.2_HD_U_F5.jar",
+  "65c425ab628d1748b489dee8619271402428509e": "OptiFine_1.11.2_HD_U_G5.jar",
+  "f253807a4689e44da030acd5c2187d2184885e87": "preview_OptiFine_1.11.2_HD_U_G5_pre1.jar",
+  "1fdacab6f3b0df1f46142acf9c427db2a653d680": "OptiFine_1.12_HD_U_C2.jar",
+  "a62a02c8437a0dccaf2ea89ab8392fd40dc92eb1": "OptiFine_1.12_HD_U_C3.jar",
+  "36d6f6f64048f68dd248a4d3c43a6f5e51fbb9c5": "OptiFine_1.12_HD_U_C4.jar",
+  "fd8e0e3ba43568e5ca0ce31c87373806d269b7e5": "OptiFine_1.12_HD_U_C5.jar",
+  "25ba634e5105e6d1e615dad0046905b68313cc9f": "OptiFine_1.12_HD_U_C7.jar",
+  "b9cd2775fc49bf009f82d3ce36235b1e702d46bb": "OptiFine_1.12_HD_U_F5.jar",
+  "3b8369d33c3257cb247c22b13a41b7db0924e948": "OptiFine_1.12_HD_U_G5.jar",
+  "aeee885ccf821c34d86537184389873d8c312ae1": "preview_OptiFine_1.12_HD_U_G5_pre1.jar",
+  "c754771bf6a7371a7084459505c244cbec61a7cf": "OptiFine_1.12.1_HD_U_C5.jar",
+  "4362a9d38542b5f4ba07782fc4d07949f4fede08": "OptiFine_1.12.1_HD_U_C6.jar",
+  "38785968beeb796cde55f91995c6cf8bfbc3b062": "OptiFine_1.12.1_HD_U_C7.jar",
+  "e6f9e6b573119c052062192695fed4ef388db9c1": "OptiFine_1.12.1_HD_U_F5.jar",
+  "3c9cd74810cad75de5091633a1e0c7d3d76ea09c": "OptiFine_1.12.1_HD_U_G5.jar",
+  "aaaaff3ae13a0cdffa2475632cf6303b9f86e0b0": "preview_OptiFine_1.12.1_HD_U_G5_pre1.jar",
+  "a9d21f084a485bc12fce8c9bea1660c6d4b08855": "OptiFine_1.12.2_HD_U_C5.jar",
+  "2159a0a754828d63446fca7b6f8fa57b710c6048": "OptiFine_1.12.2_HD_U_C6.jar",
+  "8ed8009fc82b43909190882927f335d5250a0e19": "OptiFine_1.12.2_HD_U_C7.jar",
+  "a55768a83db4ad0599f68d9bf3fa8041e2ae3852": "OptiFine_1.12.2_HD_U_C8.jar",
+  "e0236d21a2a164f2af1cfff8f9f325f53c9125f2": "OptiFine_1.12.2_HD_U_C9.jar",
+  "b91a0d82c24eef95be0af92993b14e94b842f166": "OptiFine_1.12.2_HD_U_D1.jar",
+  "04c28238506dc3561addb6efb87c9878d5b82ffe": "OptiFine_1.12.2_HD_U_D2.jar",
+  "b784000951d41f54d67685a0a275d9f61f810514": "OptiFine_1.12.2_HD_U_D3.jar",
+  "343f814410401ebdbb3314deb17c27cbab67bfac": "OptiFine_1.12.2_HD_U_E1.jar",
+  "5c87706b6d14061fed248506d8e3f95b7824ad18": "OptiFine_1.12.2_HD_U_E2.jar",
+  "e805d4be5c2a3343488c573145606e90bb13816d": "OptiFine_1.12.2_HD_U_E3.jar",
+  "0e8d45666e80829c9ab74527628dace9187fbfb5": "OptiFine_1.12.2_HD_U_F4.jar",
+  "84ada0f48bcfb16c37c49283627597bb74d935f6": "OptiFine_1.12.2_HD_U_F5.jar",
+  "ca3aea3a09ce215906c346fe190907fe0347b0c4": "OptiFine_1.12.2_HD_U_G5.jar",
+  "23a8be042f354ec97c99672906c3e61fdf54ca2c": "preview_OptiFine_1.12.2_HD_U_F6_pre1.jar",
+  "b71f993a364c7945081cc6cde9a20d29b4e43f1f": "preview_OptiFine_1.12.2_HD_U_G5_pre1.jar",
+  "ab2118b1ed23444d6decf081e0295e4c679af6c2": "preview_OptiFine_1.12.2_HD_U_G6_pre1.jar",
+  "25498de48dd9b7ed2ba5b83464950ad132e61c1f": "preview_OptiFine_1.14_HD_U_F1_pre6.jar",
+  "396ce7f0e22aae3543134baabd61ad547334c0aa": "OptiFine_1.14.2_HD_U_F1.jar",
+  "c8c6da3e2ae32d020d9001d4489aad702680f50f": "preview_OptiFine_1.14.2_HD_U_F1_pre15.jar",
+  "52420ef34128a8b15d7c8f5ed8f7ebc94b708efd": "OptiFine_1.14.3_HD_U_F1.jar",
+  "887d648f26f7ba17ea34c0c2dcff4096f7a226b1": "OptiFine_1.14.3_HD_U_F2.jar",
+  "ad8a8f60931f48e2873238d1460268bb4111f6c9": "OptiFine_1.14.4_HD_U_F2.jar",
+  "c9710081fb7012ddb56838ac54653ac2258a2404": "OptiFine_1.14.4_HD_U_F3.jar",
+  "c09f4d1ad66df8e3039334023422f1bc8cc211b8": "OptiFine_1.14.4_HD_U_F4.jar",
+  "3408258990309de773cc5a8ab71cb4137718a82b": "OptiFine_1.14.4_HD_U_F5.jar",
+  "117c01a5431244a8b6a3ac393374dac9624f9be6": "OptiFine_1.14.4_HD_U_G5.jar",
+  "ac602fe6a39d2fb4cadfe08e87af0b0daa2d8ac9": "preview_OptiFine_1.14.4_HD_U_G5_pre1.jar",
+  "630a32be9b711b5a479507159d5d2598189fed33": "OptiFine_1.15.2_HD_U_G6.jar",
+  "8339858b33056ec8975f6c0c9e1694a11f0d209a": "preview_OptiFine_1.15.2_HD_U_G1_pre1.jar",
+  "6cdf96d972b5f70c4575ff875a031c4931985686": "preview_OptiFine_1.15.2_HD_U_G1_pre10.jar",
+  "489d2284f2d50bd7c7cf4c216a766503d37e1f3c": "preview_OptiFine_1.15.2_HD_U_G1_pre12.jar",
+  "c66d412c4cb46c6f2c76c224dc45a62d30ee8b43": "preview_OptiFine_1.15.2_HD_U_G1_pre13.jar",
+  "582eb1d6470cc133db563bd1c2c8bc2068215806": "preview_OptiFine_1.15.2_HD_U_G1_pre18.jar",
+  "64d738862683c54a22e1856b2c13379599ca79ce": "preview_OptiFine_1.15.2_HD_U_G1_pre19.jar",
+  "3da9d466c847c50a73dc634dabc6a57f3b24e2f7": "preview_OptiFine_1.15.2_HD_U_G1_pre2.jar",
+  "e6583246a85dba26a478dd4925c298bb5b3f277a": "preview_OptiFine_1.15.2_HD_U_G1_pre20.jar",
+  "84c5d50e26aaa02733fa15a8c13ab845fbdea97f": "preview_OptiFine_1.15.2_HD_U_G1_pre21.jar",
+  "c55e449386eeca34df5b428d85927242fc841438": "preview_OptiFine_1.15.2_HD_U_G1_pre22.jar",
+  "459e17c77059671ef5daa9ab794400e8ae25095d": "preview_OptiFine_1.15.2_HD_U_G1_pre23.jar",
+  "2c6e4f61b1ceea92b76e0be40bc672e5b6d58efe": "preview_OptiFine_1.15.2_HD_U_G1_pre24.jar",
+  "4ea53e3d202239c5ffa2affa809e36998194f1bc": "preview_OptiFine_1.15.2_HD_U_G1_pre26.jar",
+  "29861ba50021b08200ef33f77c776f9f5b7135f7": "preview_OptiFine_1.15.2_HD_U_G1_pre27.jar",
+  "454dc0e03dab9d30a2ccce5823b7a0b309f5c859": "preview_OptiFine_1.15.2_HD_U_G1_pre28.jar",
+  "3c3623d75dc47722149edfb93fdca121c32e8987": "preview_OptiFine_1.15.2_HD_U_G1_pre29.jar",
+  "942e23fa0169c693fb1b645fae4db9217c6b5916": "preview_OptiFine_1.15.2_HD_U_G1_pre3.jar",
+  "bdbd92e5252c0afc0f22d6d5840f96762e5d01ea": "preview_OptiFine_1.15.2_HD_U_G1_pre30.jar",
+  "5daa9461c5762bdd647665f49d2e0ef0eb348910": "preview_OptiFine_1.15.2_HD_U_G1_pre4.jar",
+  "0029ab50050d19f4009f0a7d708982dde736f022": "preview_OptiFine_1.15.2_HD_U_G1_pre5.jar",
+  "646b279927cab6036e8f295efd5bda268acd85cf": "preview_OptiFine_1.15.2_HD_U_G1_pre7.jar",
+  "f11e79a7bc91a5fd985cb756445fc88e8c2dd31b": "preview_OptiFine_1.15.2_HD_U_G1_pre9.jar",
+  "1e9869b63a89a8ab81a58e1473dd2c2291fbd943": "preview_OptiFine_1.15.2_HD_U_G6_pre5.jar",
+  "87533b8ac0dfeb11241041f197f8f95c0ffa8c08": "OptiFine_1.16.1_HD_U_G2.jar",
+  "3e610c9bd89ae2a54cc06de6dfb1b00cd45e9895": "preview_OptiFine_1.16.1_HD_U_G2_pre1.jar",
+  "df817e1ab0e577be1dfb6c20a9b628d3d725c780": "preview_OptiFine_1.16.1_HD_U_G2_pre10.jar",
+  "dbca331e7abbe64d6aa360e6a93beec2935d24e3": "preview_OptiFine_1.16.1_HD_U_G2_pre11.jar",
+  "6fef880338cabe32443557762ca43fb36fe01f7c": "preview_OptiFine_1.16.1_HD_U_G2_pre12.jar",
+  "ea7bea6511f3b20aa672544d28dbc6d1a2ee8b41": "preview_OptiFine_1.16.1_HD_U_G2_pre13.jar",
+  "efdc788f59e1c1bcde3b64430322d8990ce7c7d6": "preview_OptiFine_1.16.1_HD_U_G2_pre14.jar",
+  "388100110b4dc8ef28eaa2f3a4ae2f33a8beb5ae": "preview_OptiFine_1.16.1_HD_U_G2_pre15.jar",
+  "63f5f097ce2e678d69767981b09b133ab8eac4f5": "preview_OptiFine_1.16.1_HD_U_G2_pre16.jar",
+  "c56031c3f6e5dcad536115e9f6a1fb5710b72391": "preview_OptiFine_1.16.1_HD_U_G2_pre2.jar",
+  "da269cdb109b6441ebed179a8d9f974590006cbb": "preview_OptiFine_1.16.1_HD_U_G2_pre3.jar",
+  "ad9b5d4e0bdd5697d4f6a6b9c30a64977bb8834e": "preview_OptiFine_1.16.1_HD_U_G2_pre4.jar",
+  "f6d5ea6cedff1f80bed298f9b456de2448b1f268": "preview_OptiFine_1.16.1_HD_U_G2_pre5.jar",
+  "f57a4269d404d4025de4d864b88510504e785e19": "preview_OptiFine_1.16.1_HD_U_G2_pre6.jar",
+  "28b46592e2f510a246a8bf62a2b12e808094167c": "preview_OptiFine_1.16.1_HD_U_G2_pre7.jar",
+  "26207daec6acc66250116e4f73295c9a031b491a": "preview_OptiFine_1.16.1_HD_U_G2_pre8.jar",
+  "6902e902866b765ff8e31716980c2067d491e004": "preview_OptiFine_1.16.1_HD_U_G2_pre9.jar",
+  "dde4274c696063496323731ad7b58e0f6d7265af": "OptiFine_1.16.2_HD_U_G3.jar",
+  "5b5627a814e8c95aa7ac27fa4cf2cc0b8bd4519b": "OptiFine_1.16.2_HD_U_G5.jar",
+  "5dafc10061020c519ba4abe4982a7dc80eae30f6": "preview_OptiFine_1.16.2_HD_U_G3_pre1.jar",
+  "0c0d9828984da34f9d5967027c244bb2c83a4396": "preview_OptiFine_1.16.2_HD_U_G3_pre2.jar",
+  "7c791e4348808132b8c7b59e6350ae2ee25a1918": "preview_OptiFine_1.16.2_HD_U_G3_pre3.jar",
+  "9a30a2cbc81095d38b4dc17e7d30a4b84e10e235": "preview_OptiFine_1.16.2_HD_U_G3_pre4.jar",
+  "ec1a4ce97e29c9ee2ac259d98e35bc386035a66d": "preview_OptiFine_1.16.2_HD_U_G3_pre5.jar",
+  "e03b0cdfbbcf01b1371b3e1090dee64e60a759d8": "preview_OptiFine_1.16.2_HD_U_G3_pre6.jar",
+  "40ae5fbb4e3724d6e5525d2688445c09ecfe5d39": "preview_OptiFine_1.16.2_HD_U_G3_pre7.jar",
+  "dc83d55f74722e1e6f503817e00e5ba2e6cb4abb": "OptiFine_1.16.3_HD_U_G3.jar",
+  "89d9472ee507946caa227e226524e5260ed944db": "OptiFine_1.16.3_HD_U_G4.jar",
+  "6a8f57ca84628da47b011e608ff449cde7ec13ed": "OptiFine_1.16.3_HD_U_G5.jar",
+  "f53bad69adaac9e523577dcb1c65b158802f3dee": "preview_OptiFine_1.16.3_HD_U_G3_pre2.jar",
+  "5b970eeae2ba07316b3e3a53da1d71de87c9f9f2": "preview_OptiFine_1.16.3_HD_U_G4_pre1.jar",
+  "599d9de6b656077b03fbebc9478924c548a6e9e0": "OptiFine_1.16.4_HD_U_G5.jar",
+  "7cce72db1f77c6178b549fa430e241eadb318f69": "OptiFine_1.16.4_HD_U_G6.jar",
+  "f49c042fa099907eafcbe22d7035d84f8b5715f1": "OptiFine_1.16.4_HD_U_G7.jar",
+  "f0b1eb59acdae57e10bb928046ee26012ef220ad": "preview_OptiFine_1.16.4_HD_U_G6_pre12.jar",
+  "ded7894eebda787b7bd5013aa8c66577f2ba03fc": "preview_OptiFine_1.16.4_HD_U_G7_pre1.jar",
+  "57cfcec3dd99b9f0950d0d6305bc95ecd8563212": "OptiFine_1.16.5_HD_U_G6.jar",
+  "706fb33784045d69e7ac5730426d813a39511dcd": "OptiFine_1.16.5_HD_U_G7.jar",
+  "137363842c5bfdebb72356a4079faeef3157eb3c": "OptiFine_1.16.5_HD_U_G8.jar",
+  "ad0f1577ce02a703185e46ee9e24fdeb65479fd5": "preview_OptiFine_1.16.5_HD_U_G7_pre10.jar",
+  "11770876367980da546c68bc95c4f794582907f2": "preview_OptiFine_1.16.5_HD_U_G7_pre11.jar",
+  "c5b32d2bad3bd54c31ddb29869a55fe44409cfe2": "preview_OptiFine_1.16.5_HD_U_G7_pre2.jar",
+  "9fc9cc09e72a3060d577f62f3535820056b0c2d4": "preview_OptiFine_1.16.5_HD_U_G7_pre4.jar",
+  "577fae32b60ac8d1b91e3fa645dc004d4a9e513e": "preview_OptiFine_1.16.5_HD_U_G7_pre5.jar",
+  "4bccf7f371ac83523796b4706569cc5077c46aea": "preview_OptiFine_1.16.5_HD_U_G7_pre6.jar",
+  "97b8d413a909354e2e0f42943cf292941dfe4c38": "preview_OptiFine_1.16.5_HD_U_G7_pre7.jar",
+  "9c48a804ee990e526000c7061bde9aa6f75d62a6": "preview_OptiFine_1.16.5_HD_U_G7_pre8.jar",
+  "9a9b69a52a4514f5d25c9f47127a822e93458fa3": "preview_OptiFine_1.16.5_HD_U_G7_pre9.jar",
+  "d16a450ec4205d02319480799008d8177ba05d69": "preview_OptiFine_1.16.5_HD_U_G8_pre1.jar",
+  "539c215aa0e8b8f696b325e89f3f9a3658ab32c8": "preview_OptiFine_1.16.5_HD_U_G8_pre10.jar",
+  "311ed81a27d24047b6ade33f46ac21c962897016": "preview_OptiFine_1.16.5_HD_U_G8_pre11.jar",
+  "4bc50f85809a299d30c3e3e73cfd87cad61cf7e6": "preview_OptiFine_1.16.5_HD_U_G8_pre12.jar",
+  "7de1b8e9f7cd6d7ca941d90121e50206aba80c68": "preview_OptiFine_1.16.5_HD_U_G8_pre13.jar",
+  "a4e021a75aab7ed1eb83ff7b45182d497aa930f2": "preview_OptiFine_1.16.5_HD_U_G8_pre14.jar",
+  "0ddcc4f47e10b2d671ebbf9147eb16cf8f1bb92a": "preview_OptiFine_1.16.5_HD_U_G8_pre2.jar",
+  "b8600be4fde6f8cc3c28173cc9bde6923956ff42": "preview_OptiFine_1.16.5_HD_U_G8_pre3.jar",
+  "d46b10098309929d9a6583323f059e65ba40a5e5": "preview_OptiFine_1.16.5_HD_U_G8_pre4.jar",
+  "e835424b36fdae3d5f46c6bf096852848127f1e7": "preview_OptiFine_1.16.5_HD_U_G8_pre5.jar",
+  "5a84983221331597f58e9636d41f1fb0c9e3de61": "preview_OptiFine_1.16.5_HD_U_G8_pre6.jar",
+  "d03ce2209d787539e5e51f80a7cbffc69a49ce9f": "preview_OptiFine_1.16.5_HD_U_G8_pre7.jar",
+  "1c646dc058aa32898917589858d900407883ba3f": "preview_OptiFine_1.16.5_HD_U_G8_pre8.jar",
+  "9bf369aa609e5612247473326dc298e6e6c9fecf": "preview_OptiFine_1.16.5_HD_U_G8_pre9.jar",
+  "b88b05278fbbf400cb5ec0cc26e0c5b7306e0c1e": "preview_OptiFine_1.17_HD_U_G9_pre16.jar",
+  "a6beac3091d17a4ba420116c75c89faeaee48539": "preview_OptiFine_1.17_HD_U_G9_pre17.jar",
+  "2839c5e74f64500561ee3e8605c51d9f06727c22": "preview_OptiFine_1.17_HD_U_G9_pre18.jar",
+  "455243ff55177189fff140ba7f4c4777cc977ef6": "preview_OptiFine_1.17_HD_U_G9_pre19.jar",
+  "bcc390e34bd529a86e6ae0e25d50417f46ba482f": "preview_OptiFine_1.17_HD_U_G9_pre20.jar",
+  "d30c23d745df54fd6ea2374e9c0b4392f0a78199": "preview_OptiFine_1.17_HD_U_G9_pre21.jar",
+  "075e7f8473b6785c3c6bbeeb41a3728f6ff3c3ba": "preview_OptiFine_1.17_HD_U_G9_pre22.jar",
+  "2ffaee36c717ae918cc93661df739740636900c9": "preview_OptiFine_1.17_HD_U_G9_pre23.jar",
+  "4385493e7ce4c6f4a3945f1d96246bce0b151c7e": "preview_OptiFine_1.17_HD_U_G9_pre24.jar",
+  "bcc33e67f134861eb30b1f0b9a829183c8afdebf": "preview_OptiFine_1.17_HD_U_G9_pre25.jar",
+  "6a8e34605cc828091027d9f011b804d916808af4": "preview_OptiFine_1.17_HD_U_G9_pre26.jar",
+  "4714bdceaefc15e0af9d5c72de8c05cf3051d760": "OptiFine_1.17.1_HD_U_G9.jar",
+  "cc64084e73d6423baf7ccbdd9f3760dd56f2fce5": "OptiFine_1.17.1_HD_U_H1.jar",
+  "1ee73bd882385a7abb9896b9469586741a6850a8": "preview_OptiFine_1.17.1_HD_U_G9_pre26.jar",
+  "537c1fbee9ebe25b49a8e6fb93db162d217f1c83": "preview_OptiFine_1.17.1_HD_U_G9_pre27.jar",
+  "68e1124f4b0376773d8d581da78f9bb6fc76486e": "preview_OptiFine_1.17.1_HD_U_G9_pre28.jar",
+  "3f80a562b39439831491c650c65ecb762e70d5f7": "preview_OptiFine_1.17.1_HD_U_G9_pre29.jar",
+  "9751c69568a61ce5b2a602b4dcf77c1cea57c709": "preview_OptiFine_1.17.1_HD_U_G9_pre30.jar",
+  "162fa1b4a0d5b89dad6030b5fd5903a3f24d39c7": "preview_OptiFine_1.17.1_HD_U_G9_pre31.jar",
+  "c4f3a33d892948927cecfaa60b284217325557c5": "preview_OptiFine_1.17.1_HD_U_G9_pre32.jar",
+  "12dbbc8ccd8dc02806768fb3fbe4a2d8c7c11301": "preview_OptiFine_1.17.1_HD_U_G9_pre33.jar",
+  "f74d1dfcb24c43e633076c63dd4492b29fca14b3": "preview_OptiFine_1.17.1_HD_U_G9_pre34.jar",
+  "42e55681fb106475ef13d5c5ef7113fb631392d4": "preview_OptiFine_1.17.1_HD_U_G9_pre35.jar",
+  "245ddb68349f5f81d067f4d9402d62db3d439d50": "preview_OptiFine_1.17.1_HD_U_H1_pre1.jar",
+  "0ac033f5ba4089311af55172281e73df143b9ac4": "preview_OptiFine_1.17.1_HD_U_H1_pre2.jar",
+  "68a3727bcdb0eac0986ff4e883d93fef406eb1c3": "preview_OptiFine_1.17.1_HD_U_H1_pre3.jar",
+  "a6534877558f0438bda604dc43093d61c6de88ae": "preview_OptiFine_1.17.1_HD_U_H1_pre4.jar",
+  "3e18db17b8695feb0a188ecdcb81b561d9d7030c": "preview_OptiFine_1.17.1_HD_U_H1_pre5.jar",
+  "c2e0c4391a671cd06931f7b6e965085c04c58762": "preview_OptiFine_1.17.1_HD_U_H1_pre6.jar",
+  "1ffb7325df5bf01b1d99bbbed8006a477b861394": "preview_OptiFine_1.17.1_HD_U_H1_pre7.jar",
+  "f2fa727e67b4795ddea3cc37cc5ced42e9c3b0c5": "preview_OptiFine_1.17.1_HD_U_H1_pre8.jar",
+  "aee167aff9937a068d514c85e3f8c766db99ed43": "preview_OptiFine_1.17.1_HD_U_H1_pre9.jar",
+  "59709e09632c80ab80ffcc4b5bf380d073bf467f": "preview_OptiFine_1.17.1_HD_U_H2_pre1.jar",
+  "52629bc5319ec203edd42de1776cb8d67a0e6b1b": "OptiFine_1.18_HD_U_H3.jar",
+  "e5077b4be03956118613162b5dbcf90406b27747": "preview_OptiFine_1.18_HD_U_H3_pre7.jar",
+  "326c160481cc849bccb821939992c101a8fe163b": "OptiFine_1.18.1_HD_U_H4.jar",
+  "1679654fdf49fd1216b090d51d3d8ec906094619": "OptiFine_1.18.1_HD_U_H5.jar",
+  "c86534ee821a78efc741c9cb407eec76625e1bab": "OptiFine_1.18.1_HD_U_H6.jar",
+  "34c98382131efa1394cff06528238dc44d255794": "preview_OptiFine_1.18.1_HD_U_H4_pre1.jar",
+  "d6cd0a2d0243ecad107220bff639eb46277762ea": "preview_OptiFine_1.18.1_HD_U_H5_pre1.jar",
+  "e24d766864f9238b94be5b3ad65608d7eb521fad": "preview_OptiFine_1.18.1_HD_U_H5_pre2.jar",
+  "bc1b87a6379acf5a5dca2a56196080ace49e4864": "preview_OptiFine_1.18.1_HD_U_H5_pre3.jar",
+  "555a6b8e420bd63a04d9f338d61f3333b4ccf5d9": "preview_OptiFine_1.18.1_HD_U_H5_pre4.jar",
+  "1d02694393f10992cbbd11a802568b0890b4dc81": "preview_OptiFine_1.18.1_HD_U_H5_pre5.jar",
+  "e3a22adc6931dcd7513f26afa1adecd62a35641b": "preview_OptiFine_1.18.1_HD_U_H5_pre6.jar",
+  "445a8cb8a1d565d8a1fabc0ba984c5936edbf561": "OptiFine_1.18.2_HD_U_H6.jar",
+  "8d83d4d5e325887e000a462029d2bf7a6e77eebc": "OptiFine_1.18.2_HD_U_H7.jar",
+  "2a461188167c1da6b1cfd520d85a656f45c74d70": "OptiFine_1.18.2_HD_U_H9.jar",
+  "e591777f272df533b62260a6dbe0e6cde62066f6": "preview_OptiFine_1.18.2_HD_U_H6_pre1.jar",
+  "f7c727c99e7f58ef9b86cc96fb894f628391016c": "preview_OptiFine_1.18.2_HD_U_H7_pre1.jar",
+  "cb51529f94e0eff5b9da92c8f70b1d498662a115": "preview_OptiFine_1.18.2_HD_U_H7_pre2.jar",
+  "ceea3c6612d2ec36c6ef1ca4a9355a141df9089f": "preview_OptiFine_1.18.2_HD_U_H7_pre3.jar",
+  "596c3a399889c3e2ef1d8fee994e8cf5a91e457e": "preview_OptiFine_1.18.2_HD_U_H8_pre1.jar",
+  "85f545aeb9d232ae970b0efa24e833f9c5d0fe33": "preview_OptiFine_1.18.2_HD_U_H9_pre1.jar",
+  "b9bbabb8f31f2b04ca1a1a8645df1be701f705bd": "preview_OptiFine_1.18.2_HD_U_H9_pre2.jar",
+  "c2bf213b4f60b6a29e402e3eaf219a4c85a5d1e6": "preview_OptiFine_1.18.2_HD_U_H9_pre3.jar",
+  "64c8c35313d25a64a8a8677cb851a2c83556ddc8": "OptiFine_1.19_HD_U_H8.jar",
+  "c668eaf9b6ce4e64f26a29fb27e830229ab4bef3": "OptiFine_1.19_HD_U_H9.jar",
+  "cda9130e0d36f0d6e06950ab393a8929b502a440": "preview_OptiFine_1.19_HD_U_H8_pre1.jar",
+  "cd1ae81f544672df776dc4426269771d2e82d0b2": "preview_OptiFine_1.19_HD_U_H8_pre10.jar",
+  "cb3ad2a9b4c23675ca569058f55dd174eed67a38": "preview_OptiFine_1.19_HD_U_H8_pre2.jar",
+  "2f7139ea0888ed18006e3a39b7475e88a3fd545d": "preview_OptiFine_1.19_HD_U_H8_pre3.jar",
+  "85cd70bb714e65935abf351811889a47a648bb96": "preview_OptiFine_1.19_HD_U_H8_pre4.jar",
+  "c65cbae4c1cfb5b9a5a573e4b83424df2d81b172": "preview_OptiFine_1.19_HD_U_H8_pre5.jar",
+  "10cd57890ebf449f5c258cff1c60d8dcefcc6cf6": "preview_OptiFine_1.19_HD_U_H8_pre6.jar",
+  "eb03e60c2fa9904a47eec1ef4a2c8804ee6cc0b8": "preview_OptiFine_1.19_HD_U_H8_pre7.jar",
+  "40d0b514168161fdefc91594f0d3f774f831d7de": "preview_OptiFine_1.19_HD_U_H8_pre8.jar",
+  "8e5bedd1c1a3641caad0743c4629db51c11f8feb": "preview_OptiFine_1.19_HD_U_H9_pre1.jar",
+  "7745216a810f0d617ed17a520fda44e12456492f": "preview_OptiFine_1.19_HD_U_H9_pre2.jar",
+  "6d6ed920eb99836e76549a7803af85881fcdee34": "preview_OptiFine_1.19_HD_U_H9_pre3.jar",
+  "7604e63ca74876b4e8ea9c3a36dbe80111c776b1": "preview_OptiFine_1.19_HD_U_H9_pre4.jar",
+  "b42dcce86c88ad4bcfa52a9d297afa800477aa78": "OptiFine_1.19.1_HD_U_H9.jar",
+  "648427a02940b3c53b3678cd5df1ff6997bd1e6c": "preview_OptiFine_1.19.1_HD_U_H9_pre1.jar",
+  "7cc221a6df6831b62c3b676b5f958e82a4487fd8": "OptiFine_1.19.2_HD_U_H9.jar",
+  "f304c56c7ab9119ce4269f2b019cd4668f48376d": "OptiFine_1.19.2_HD_U_I1.jar",
+  "708c1ecc148a0bd7d341e8f5e005e670903650f7": "OptiFine_1.19.2_HD_U_I2.jar",
+  "e0e924990bd12cc499fc89439f0b2c50b6e147fa": "preview_OptiFine_1.19.2_HD_U_I1_pre1.jar",
+  "465b229481c0a285195a168bf6581549a5cd7373": "preview_OptiFine_1.19.2_HD_U_I1_pre2.jar",
+  "af4398d0ce0628bcc0204e61c157e3c8951b9ad5": "preview_OptiFine_1.19.2_HD_U_I1_pre3.jar",
+  "35ef18f953bbea9a4cba6e14d0731d56f888114f": "preview_OptiFine_1.19.2_HD_U_I1_pre4.jar",
+  "b24d8725a8d06e1c190d1d2b3bc51c899f74b36d": "OptiFine_1.19.3_HD_U_I3.jar",
+  "f5ce615d9886e377079fd13cc4700a51d5a9c7b6": "preview_OptiFine_1.19.3_HD_U_I2_pre1.jar",
+  "10e6d97ea2c97cf0575a7c587fb11e1ddb73aa71": "preview_OptiFine_1.19.3_HD_U_I2_pre2.jar",
+  "89718cd608225d7c23f7e180d011daa4a47ce6bd": "preview_OptiFine_1.19.3_HD_U_I2_pre3.jar",
+  "204baf0fd59432054aebd2f83c83fe15fdd3e611": "preview_OptiFine_1.19.3_HD_U_I2_pre4.jar",
+  "90a52b3adc94a1b9a70f03f4c4ea14dfdf3424d8": "preview_OptiFine_1.19.3_HD_U_I2_pre5.jar",
+  "050d787936cc239ea8db0aa250d2ed72a44fa71d": "OptiFine_1.19.4_HD_U_I3.jar",
+  "253e9d29c6826c134fbdeea074d1d91c3410225c": "OptiFine_1.19.4_HD_U_I4.jar",
+  "068b998d6c5db5b3bb7ad6a0a7d0c6aefc4dc191": "preview_OptiFine_1.19.4_HD_U_I3_pre3.jar",
+  "a8b9156811c4745dbd612ebbb1df6f93d2fa734a": "preview_OptiFine_1.19.4_HD_U_I3_pre4.jar",
+  "6f2793e6236a27c06a74e8a1876ea6b29be038a7": "preview_OptiFine_1.19.4_HD_U_I3_pre5.jar",
+  "9ad82ef9f32fb5a0e7e7e0c7e8906c9f319a9705": "preview_OptiFine_1.19.4_HD_U_I3_pre6.jar",
+  "cf02ee8b5cf6922fbc28d6ffc8183949fff55d49": "preview_OptiFine_1.19.4_HD_U_I3_pre7.jar",
+  "702e0e9987209f66a5b4f825e7ac658fd49dd4b1": "preview_OptiFine_1.19.4_HD_U_I3_pre8.jar",
+  "04c8eaeadf24c42929500812a8f3f5fe634f0d1f": "preview_OptiFine_1.19.4_HD_U_I3_pre9.jar",
+  "968784e051c2c44f10a4ab448c979572977969c0": "preview_OptiFine_1.19.4_HD_U_I4_pre1.jar",
+  "abe4b1dbda7d95eb47953ef8aa11ed8ed1d8e238": "preview_OptiFine_1.20_HD_U_I5_pre3.jar",
+  "6612ce97d846018126f91cdbae23f0eff6bd2bab": "preview_OptiFine_1.20_HD_U_I5_pre4.jar",
+  "f37d62b6a071f6cd3d6bdfa378b56f7f5b9ec7c6": "preview_OptiFine_1.20_HD_U_I5_pre5.jar",
+  "c44bd276edd10fa910d929a4e7f6b8c7ffe6c23f": "OptiFine_1.20.1_HD_U_I5.jar",
+  "227914ab1037762eae44a2e66a568b87d7a02365": "OptiFine_1.20.1_HD_U_I6.jar",
+  "7484cf60bd8e019c40fa448daa294bd31f52962b": "preview_OptiFine_1.20.1_HD_U_I5_pre4.jar",
+  "953b1b4f15f56329bc4ade851846ef004ebbd5d0": "preview_OptiFine_1.20.1_HD_U_I5_pre5.jar",
+  "ebbdb3076f8291d7f8e10cbec3b1d3c49a0a701d": "preview_OptiFine_1.20.1_HD_U_I5_pre6.jar",
+  "b831d59eeb63122893f87a62ec3e5b3f1cc5586a": "preview_OptiFine_1.20.1_HD_U_I5_pre7.jar",
+  "dc93d991b07b2c5c6d2cd50a553599900c256264": "preview_OptiFine_1.20.1_HD_U_I5_pre8.jar",
+  "040a73427541170bccf162c04f4129b3909b8da1": "preview_OptiFine_1.20.1_HD_U_I5_pre9.jar",
+  "9f5fd75ede7770ad35383443b0a203f026cef5db": "preview_OptiFine_1.20.1_HD_U_I6_pre1.jar",
+  "85d5566b440036b926c5dd306108e48e7fb9b6e8": "preview_OptiFine_1.20.1_HD_U_I6_pre2.jar",
+  "d09c110358117336472414720bf961a581690dc5": "preview_OptiFine_1.20.1_HD_U_I6_pre3.jar",
+  "3365577f5161f9a1bbfefb15675bb5ece27600e4": "preview_OptiFine_1.20.1_HD_U_I6_pre4.jar",
+  "f317c701ae8c533fc34809dbce914bcced4e9a03": "preview_OptiFine_1.20.1_HD_U_I6_pre5.jar",
+  "445b32b14a2c2802d5a0832a708712d2fabafbc2": "preview_OptiFine_1.20.1_HD_U_I6_pre6.jar",
+  "9ea6b1389e88b81797f54b9d8d1e870d3fe8532c": "preview_OptiFine_1.20.2_HD_U_I7_pre1.jar",
+  "f2475e8d5746477d1d2972cd5aae62a5d9c04264": "OptiFine_1.20.4_HD_U_I7.jar",
+  "fec28a2fa152744bc2b47b3c5db3217974aa57ee": "preview_OptiFine_1.20.4_HD_U_I7_pre2.jar",
+  "4068365a57f4b154f4ebbc670894dc9038cf3819": "preview_OptiFine_1.20.4_HD_U_I7_pre3.jar",
+  "2481e0bed7610bfdfe26dea5a6bd8f01537c4ea8": "preview_OptiFine_1.20.4_HD_U_I7_pre4.jar",
+  "150990ba068cfb55bbf9e525c5ab84db0b8903e1": "preview_OptiFine_1.20.4_HD_U_I7_pre5.jar",
+  "fcf0f0f278d1901e4341d5ec8022bede03572430": "preview_OptiFine_1.20.4_HD_U_I7_pre6.jar",
+  "1c3395efa928758505b808291162a2514523b399": "preview_OptiFine_1.20.4_HD_U_I7_pre7.jar",
+  "4bcfec24e5d917ee96e0de6ef095bfc08782217b": "preview_OptiFine_1.20.4_HD_U_I8_pre1.jar",
+  "c931df21309800f1d1ece59b318e6b3e39c98176": "preview_OptiFine_1.20.4_HD_U_I8_pre2.jar",
+  "83f5ad147536405d86bf4ee47a7f7970e1fa8329": "preview_OptiFine_1.20.4_HD_U_I8_pre3.jar",
+  "7c6669319db18c9622d896da2a061479be88acf5": "preview_OptiFine_1.20.4_HD_U_I8_pre4.jar",
+  "bda7bbdd5e2d8c73148372e8c3b34d7c2485a4e8": "preview_OptiFine_1.20.6_HD_U_J1_pre18.jar",
+  "24a20f949a719480523669669a4e5e7339df6186": "OptiFine_1.21.1_HD_U_J1.jar",
+  "6bc74af8f733864539404a3f087aeb6f0cadc6b1": "preview_OptiFine_1.21.1_HD_U_J1_pre10.jar",
+  "4813a6cde6b0f5b20223d486ba7adf0816236a77": "preview_OptiFine_1.21.1_HD_U_J1_pre11.jar",
+  "c99ea98d8537f99b7470f1bcfa81c7ce405fd97e": "preview_OptiFine_1.21.1_HD_U_J1_pre12.jar",
+  "f2f55e6d61a4b90bdaa31eb520f5af78f8a1833b": "preview_OptiFine_1.21.1_HD_U_J1_pre13.jar",
+  "53a15b75ea9d20b644b0d3e9c7280ba172ade053": "preview_OptiFine_1.21.1_HD_U_J1_pre14.jar",
+  "da5575e1138fc2a4c28779cfc5e7d260253ccbdb": "preview_OptiFine_1.21.1_HD_U_J1_pre15.jar",
+  "0930876341beadf7014f57e9ebdd44be445dacbe": "OptiFine_1.21.3_HD_U_J2.jar",
+  "350f63ec3e59d21227f803d4078883884e482660": "preview_OptiFine_1.21.3_HD_U_J2_pre10.jar",
+  "2c48a4210acd25082c220f9601f17d1cc7769098": "preview_OptiFine_1.21.3_HD_U_J2_pre11.jar",
+  "8f0a097b1f7c9d065396bac9230eab62412a8b54": "preview_OptiFine_1.21.3_HD_U_J2_pre12.jar",
+  "f97df5638fcda7f4380d9284b329569d4e1554aa": "preview_OptiFine_1.21.3_HD_U_J2_pre8.jar",
+  "77df50552deea51687592b60c6d24561ee11aa74": "preview_OptiFine_1.21.3_HD_U_J2_pre9.jar",
+  "bcc6a7a140320566a985b1499a59fcb085255d14": "OptiFine_1.21.4_HD_U_J3.jar",
+  "e7bff4575b7311e11d7111506e855b4ad910481f": "preview_OptiFine_1.21.4_HD_U_J3_pre10.jar",
+  "52b2b6defdb8d1c189afb42125079f1b76ff72e2": "preview_OptiFine_1.21.4_HD_U_J3_pre11.jar",
+  "708f70aa5f70919a40d02ba75fc30d2d60c8e7cd": "preview_OptiFine_1.21.4_HD_U_J3_pre12.jar",
+  "86b796325d03d4571809a155c54cf6cba7c7482b": "preview_OptiFine_1.21.4_HD_U_J3_pre13.jar",
+  "618719a6a69eb674ea50c82cffe3d4e3c2f67abf": "preview_OptiFine_1.21.4_HD_U_J3_pre14.jar",
+  "98dd84b0581fcf7a27aa96238acb615b5b719825": "preview_OptiFine_1.21.4_HD_U_J3_pre15.jar",
+  "5c8573ae921082a069a9e7269500178f0c580c97": "preview_OptiFine_1.21.4_HD_U_J3_pre4.jar",
+  "17d51e1bfc94dde95368ab91aa0bb56e89c706bf": "preview_OptiFine_1.21.4_HD_U_J3_pre5.jar",
+  "1a4311606ac94d682800714793ea887c3276b18d": "preview_OptiFine_1.21.4_HD_U_J3_pre6.jar",
+  "12bf1a1b5607b5c62405a8c6ab4e1bd02bef2c8d": "preview_OptiFine_1.21.4_HD_U_J3_pre7.jar",
+  "f1f56403dc575fd975a000bfb133060285cacd6f": "preview_OptiFine_1.21.4_HD_U_J3_pre8.jar",
+  "9584de0e8204f5c855b60741d58af967dfc599a8": "preview_OptiFine_1.21.4_HD_U_J3_pre9.jar",
+  "0b97631e166ace9346744de33b7772df7bf5fbeb": "preview_OptiFine_1.21.4_HD_U_J4_pre1.jar",
+  "a776ec6c072529d4b731e0e6929a8aee56d04e76": "preview_OptiFine_1.21.4_HD_U_J4_pre2.jar",
+  "2a3fdfcbfc9c572b22c7d15c6129b8ea58ea1712": "preview_OptiFine_1.21.8_HD_U_J6_pre13.jar",
+  "ba7ca56c9b33d2fc7f7d22e20ce2765e4d257299": "preview_OptiFine_1.21.8_HD_U_J6_pre14.jar",
+  "097d3acd315b0682a4565ab9cbb4cdc1cc17d81f": "preview_OptiFine_1.21.8_HD_U_J6_pre15.jar",
+  "d7effca0de34c9442f06776848679a684f1f166a": "preview_OptiFine_1.21.8_HD_U_J6_pre16.jar",
+  "696ed52cdfd0ac2eafab8e98575d146b400de530": "preview_OptiFine_1.21.10_HD_U_J7_pre10.jar",
+  "be7bc81f0c9ce15316f76a059b0a690f62c03a5d": "preview_OptiFine_1.21.10_HD_U_J7_pre11.jar",
+  "731e698abe94bbd90cda628f80b4ce6ac5af8188": "preview_OptiFine_1.21.10_HD_U_J7_pre6.jar",
+  "63f189a4b7ff5b53e5bc56391f5274eab8555def": "preview_OptiFine_1.21.10_HD_U_J7_pre7.jar",
+  "7ccfc0085ccdef3f4ec21918a059ddd05171711a": "preview_OptiFine_1.21.10_HD_U_J7_pre8.jar",
+  "aad53ad3346944a12ed088ffbba98cf9eccc474c": "preview_OptiFine_1.21.10_HD_U_J7_pre9.jar",
+  "2800efeb56565f23494d372e7edac29333113e11": "OptiFine_1.21.11_HD_U_J8.jar",
+  "cee35b83c75da33dc07ffbb344386cae5cdc5c5b": "OptiFine_1.21.11_HD_U_J9.jar",
+  "687046282c370e690ec4b4aa6d90a6b42ada3ee1": "preview_OptiFine_1.21.11_HD_U_J8_pre10.jar",
+  "c09b05637610072bc33023d1c901e2726b7d16ce": "preview_OptiFine_1.21.11_HD_U_J8_pre11.jar",
+  "88682b40f08f462c4daae8e6b25ea99b1ed5cd14": "preview_OptiFine_1.21.11_HD_U_J8_pre12.jar",
+  "1c69de725c249d487df676470c71a7c05ece943c": "preview_OptiFine_1.21.11_HD_U_J8_pre3.jar",
+  "921b68e014539d75b77a47ca2117fc9e09ace4ed": "preview_OptiFine_1.21.11_HD_U_J8_pre4.jar",
+  "2fee496a35eeebf39fbb43017b12d752f5c37dee": "preview_OptiFine_1.21.11_HD_U_J8_pre5.jar",
+  "e5b5da810d43c02dccedfd99f83ae8ad63e49966": "preview_OptiFine_1.21.11_HD_U_J8_pre6.jar",
+  "8309638bab4d99258354b4d13c3cd82ff7c9645b": "preview_OptiFine_1.21.11_HD_U_J8_pre7.jar",
+  "b3d208ec7b5015842fcc77b3399b04b24e0b54cb": "preview_OptiFine_1.21.11_HD_U_J8_pre8.jar",
+  "c4ac7401bb5f668e4e92121501d1537cdbe7343c": "preview_OptiFine_1.21.11_HD_U_J8_pre9.jar",
+  "fd96031b5fc4c41ad71581eea53eb122022fff5b": "preview_OptiFine_1.21.11_HD_U_J9_pre1.jar",
+  "a159a217aba02cd7897394a51bd346c3b658b9f9": "preview_OptiFine_1.21.11_HD_U_J9_pre2.jar",
+  "2a784437076b539ea1a3c1a37ec98ff21bc3952c": "preview_OptiFine_1.21.11_HD_U_J9_pre3.jar",
+  "ee32ce4b406e290a0e99685afd09dc44cad03ddd": "preview_OptiFine_1.21.11_HD_U_J9_pre4.jar"
+}

+ 2 - 0
PCLCS/PCLCS.csproj

@@ -27,6 +27,7 @@
 		<Using Include="System.Collections.Generic" />
 		<Using Include="System.Collections.Generic" />
 		<Using Include="System.Collections.Concurrent" />
 		<Using Include="System.Collections.Concurrent" />
 		<Using Include="System.Text" />
 		<Using Include="System.Text" />
+		<Using Include="System.Text.RegularExpressions" />
 		<Using Include="System.Net" />
 		<Using Include="System.Net" />
 		<Using Include="System.Runtime.CompilerServices" />
 		<Using Include="System.Runtime.CompilerServices" />
 		<Using Include="System.Runtime.InteropServices" />
 		<Using Include="System.Runtime.InteropServices" />
@@ -58,6 +59,7 @@
 		<EmbeddedResource Include="Launch\JavaWrapper.jar" LogicalName="Launch\JavaWrapper.jar" />
 		<EmbeddedResource Include="Launch\JavaWrapper.jar" LogicalName="Launch\JavaWrapper.jar" />
 		<EmbeddedResource Include="Launch\LwjglUnsafeAgent.jar" LogicalName="Launch\LwjglUnsafeAgent.jar" />
 		<EmbeddedResource Include="Launch\LwjglUnsafeAgent.jar" LogicalName="Launch\LwjglUnsafeAgent.jar" />
 		<EmbeddedResource Include="Resource\WikiEntries.txt" LogicalName="Resource\WikiEntries.txt" />
 		<EmbeddedResource Include="Resource\WikiEntries.txt" LogicalName="Resource\WikiEntries.txt" />
+		<EmbeddedResource Include="Minecraft\OptiFineHashes.json" LogicalName="Minecraft\OptiFineHashes.json" />
 	</ItemGroup>
 	</ItemGroup>
 	<ItemGroup>
 	<ItemGroup>
 	  <Reference Include="Microsoft.VisualBasic" />
 	  <Reference Include="Microsoft.VisualBasic" />

Dosya farkı çok büyük olduğundan ihmal edildi
+ 115 - 115
PCLCS/Resource/WikiEntries.txt


+ 8 - 7
Plain Craft Launcher 2/Application.xaml.vb

@@ -9,7 +9,10 @@ Public Class Application
             '核心初始化
             '核心初始化
             MeloongCore.Main.Init("PCL")
             MeloongCore.Main.Init("PCL")
             MeloongCore.Wpf.Main.Init()
             MeloongCore.Wpf.Main.Init()
-            Logger.Instance = New PclLogger With {.logFolder = Paths.BaseThenName, .MinLevel = If(ModeDebug, LogLevel.Trace, LogLevel.Info)}
+            If Not (e.Args.Contains("--update") OrElse e.Args.Contains("--gpu") OrElse e.Args.Contains("--memory")) Then '若是通过命令行调用,则不检查单例、不轮换日志(#9137)
+                WaitingMutex(e)
+                Logger.Instance = New PclLogger With {.logFolder = Paths.BaseThenName, .MinLevel = If(ModeDebug, LogLevel.Trace, LogLevel.Info)}
+            End If
             '提升主线程优先级
             '提升主线程优先级
             Thread.CurrentThread.Priority = ThreadPriority.Highest
             Thread.CurrentThread.Priority = ThreadPriority.Highest
             '执行开发版测试
             '执行开发版测试
@@ -47,7 +50,7 @@ Public Class Application
                     Catch ex As Exception
                     Catch ex As Exception
                         Environment.Exit(ProcessReturnValues.Fail)
                         Environment.Exit(ProcessReturnValues.Fail)
                     End Try
                     End Try
-                ElseIf e.Args(0).StartsWithF("--memory") Then
+                ElseIf e.Args(0) = "--memory" Then
                     '内存优化
                     '内存优化
                     Dim Ram = My.Computer.Info.AvailablePhysicalMemory
                     Dim Ram = My.Computer.Info.AvailablePhysicalMemory
                     Try
                     Try
@@ -70,7 +73,7 @@ Public Class Application
                 CheckPermissionWithException(Paths.Base & "PCL\")
                 CheckPermissionWithException(Paths.Base & "PCL\")
             Catch ex As Exception
             Catch ex As Exception
                 MsgBox($"PCL 没有对当前文件夹的权限({Paths.Base}PCL\),请尝试:" & vbCrLf &
                 MsgBox($"PCL 没有对当前文件夹的权限({Paths.Base}PCL\),请尝试:" & vbCrLf &
-                  "1. 将 PCL 移动到其他文件夹" & If(Paths.Base.StartsWithF("C:", True), ",例如 C 盘和桌面以外的其他位置。", "。") & vbCrLf &
+                  "1. 将 PCL 移动到其他文件夹" & If(PathUtils.GetDiskName(Paths.Base) = "C"c, ",例如 C 盘和桌面以外的其他位置。", "。") & vbCrLf &
                   "2. 删除当前目录中的 PCL 文件夹,然后再试。" & vbCrLf &
                   "2. 删除当前目录中的 PCL 文件夹,然后再试。" & vbCrLf &
                   "3. 右键 PCL 选择属性,打开 兼容性 中的 以管理员身份运行此程序。",
                   "3. 右键 PCL 选择属性,打开 兼容性 中的 以管理员身份运行此程序。",
                 MsgBoxStyle.Critical, "运行环境错误")
                 MsgBoxStyle.Critical, "运行环境错误")
@@ -81,7 +84,7 @@ RetryCacheCheck:
                 DirectoryUtils.Create(PathTemp)
                 DirectoryUtils.Create(PathTemp)
                 CheckPermissionWithException(PathTemp)
                 CheckPermissionWithException(PathTemp)
             Catch ex As Exception
             Catch ex As Exception
-                If PathTemp = Path.GetTempPath() & "PCL\" Then
+                If PathUtils.IsSame(PathTemp, Path.GetTempPath() & "PCL\") Then
                     MyMsgBox("PCL 无法访问缓存文件夹,可能导致程序出错或无法正常使用!" & vbCrLf & vbCrLf & "错误原因:" & ex.GetDisplay(True), "缓存文件夹不可用")
                     MyMsgBox("PCL 无法访问缓存文件夹,可能导致程序出错或无法正常使用!" & vbCrLf & vbCrLf & "错误原因:" & ex.GetDisplay(True), "缓存文件夹不可用")
                 Else
                 Else
                     MyMsgBox("手动设置的缓存文件夹不可用,PCL 将使用默认缓存文件夹。" & vbCrLf & vbCrLf & "错误原因:" & ex.GetDisplay(True), "缓存文件夹不可用")
                     MyMsgBox("手动设置的缓存文件夹不可用,PCL 将使用默认缓存文件夹。" & vbCrLf & vbCrLf & "错误原因:" & ex.GetDisplay(True), "缓存文件夹不可用")
@@ -92,8 +95,6 @@ RetryCacheCheck:
             End Try
             End Try
             DirectoryUtils.Create(PathTemp & "Cache\")
             DirectoryUtils.Create(PathTemp & "Cache\")
             DirectoryUtils.Create(Paths.AppDataThenName)
             DirectoryUtils.Create(Paths.AppDataThenName)
-            '要求单例
-            WaitingMutex(e)
             '设置 ToolTipService 默认值
             '设置 ToolTipService 默认值
             ToolTipService.InitialShowDelayProperty.OverrideMetadata(GetType(DependencyObject), New FrameworkPropertyMetadata(100))
             ToolTipService.InitialShowDelayProperty.OverrideMetadata(GetType(DependencyObject), New FrameworkPropertyMetadata(100))
             '设置网络配置默认值
             '设置网络配置默认值
@@ -231,7 +232,7 @@ RetryCacheCheck:
         End Function
         End Function
     End Sub
     End Sub
     Private Shared Sub ExtractLibwebp() '这个方法会调用 ModBase,进而调用 MeloongCore,所以不能放在 New 里
     Private Shared Sub ExtractLibwebp() '这个方法会调用 ModBase,进而调用 MeloongCore,所以不能放在 New 里
-        SetDllDirectory(PathPure.Value.TrimEnd("\"c))
+        SetDllDirectory(PathUtils.RemoveSlashSuffix(PathPure.Value))
         Try
         Try
             ExtractResources(PathPure.Value & "libwebp.dll", "libwebp64")
             ExtractResources(PathPure.Value & "libwebp.dll", "libwebp64")
         Catch ex As Exception
         Catch ex As Exception

+ 47 - 30
Plain Craft Launcher 2/Controls/Behaviors/ClipboardInterceptor.vb

@@ -44,8 +44,8 @@ Namespace Controls.Behaviors
 
 
         Private Shared Sub AddCommandBindingsToTextBox(tb As TextBox)
         Private Shared Sub AddCommandBindingsToTextBox(tb As TextBox)
             tb.CommandBindings.Add(New CommandBinding(ApplicationCommands.Copy, AddressOf OnCopyTextBox))
             tb.CommandBindings.Add(New CommandBinding(ApplicationCommands.Copy, AddressOf OnCopyTextBox))
-            tb.CommandBindings.Add(New CommandBinding(ApplicationCommands.Cut, AddressOf OnCutTextBox))
-            tb.CommandBindings.Add(New CommandBinding(ApplicationCommands.Paste, AddressOf OnPasteTextBox))
+            tb.CommandBindings.Add(New CommandBinding(ApplicationCommands.Cut, AddressOf OnCutTextBox, AddressOf CanEditTextBox))
+            tb.CommandBindings.Add(New CommandBinding(ApplicationCommands.Paste, AddressOf OnPasteTextBox, AddressOf CanEditTextBox))
         End Sub
         End Sub
 
 
         Private Shared Sub AddCommandBindingsToRichTextBox(rtb As RichTextBox)
         Private Shared Sub AddCommandBindingsToRichTextBox(rtb As RichTextBox)
@@ -58,42 +58,61 @@ Namespace Controls.Behaviors
             dg.CommandBindings.Add(New CommandBinding(ApplicationCommands.Copy, AddressOf OnCopyDataGrid))
             dg.CommandBindings.Add(New CommandBinding(ApplicationCommands.Copy, AddressOf OnCopyDataGrid))
         End Sub
         End Sub
 
 
+        Private Shared Sub SetClipboardText(text As String)
+            Forms.Clipboard.SetDataObject(text, False, retryTimes:=10, retryDelay:=100) '实际的复制
+            Try
+                System.Windows.Clipboard.Flush() '将数据持久化到系统剪贴板,避免应用程序退出后数据丢失
+            Catch ex As Exception '持久化失败不抛出异常
+                Logger.Warn(ex, $"剪贴板持久化失败(错误码:0x{ex.HResult:X8})")
+            End Try
+        End Sub
+
         Private Shared Sub OnCopyTextBox(sender As Object, e As ExecutedRoutedEventArgs)
         Private Shared Sub OnCopyTextBox(sender As Object, e As ExecutedRoutedEventArgs)
             Dim tb = TryCast(sender, TextBox)
             Dim tb = TryCast(sender, TextBox)
             If tb Is Nothing OrElse tb.SelectionLength <= 0 Then Return
             If tb Is Nothing OrElse tb.SelectionLength <= 0 Then Return
 
 
             Try
             Try
-                Forms.Clipboard.Clear()
-                Forms.Clipboard.SetDataObject(tb.SelectedText, True)
+                SetClipboardText(tb.SelectedText)
             Catch ex As Exception
             Catch ex As Exception
-                Logger.Warn(ex, "复制失败,请稍后再试", LogBehavior.Toast)
+                Logger.Warn(ex, $"复制失败,请稍后再试(错误码:0x{ex.HResult:X8})", LogBehavior.Toast)
             End Try
             End Try
 
 
             e.Handled = True
             e.Handled = True
         End Sub
         End Sub
 
 
+        Private Shared Sub CanEditTextBox(sender As Object, e As CanExecuteRoutedEventArgs)
+            Dim tb = TryCast(sender, TextBox)
+            e.CanExecute = tb IsNot Nothing AndAlso Not tb.IsReadOnly AndAlso
+                (e.Command IsNot ApplicationCommands.Cut OrElse tb.SelectionLength > 0)
+            e.Handled = True
+        End Sub
+
         Private Shared Sub OnCutTextBox(sender As Object, e As ExecutedRoutedEventArgs)
         Private Shared Sub OnCutTextBox(sender As Object, e As ExecutedRoutedEventArgs)
             Dim tb = TryCast(sender, TextBox)
             Dim tb = TryCast(sender, TextBox)
-            If tb Is Nothing OrElse tb.SelectionLength <= 0 Then Return
+            e.Handled = True
+            If tb Is Nothing OrElse tb.IsReadOnly OrElse tb.SelectionLength <= 0 Then Return
 
 
             Try
             Try
-                Forms.Clipboard.Clear()
-                Forms.Clipboard.SetDataObject(tb.SelectedText, True)
+                SetClipboardText(tb.SelectedText)
             Catch ex As Exception
             Catch ex As Exception
-                Logger.Warn(ex, "剪切失败,请稍后再试", LogBehavior.Toast)
+                Logger.Warn(ex, $"剪切失败,请稍后再试(错误码:0x{ex.HResult:X8})", LogBehavior.Toast)
+                Return
             End Try
             End Try
 
 
             tb.SelectedText = String.Empty
             tb.SelectedText = String.Empty
-            e.Handled = True
         End Sub
         End Sub
 
 
         Private Shared Sub OnPasteTextBox(sender As Object, e As ExecutedRoutedEventArgs)
         Private Shared Sub OnPasteTextBox(sender As Object, e As ExecutedRoutedEventArgs)
             Dim tb = TryCast(sender, TextBox)
             Dim tb = TryCast(sender, TextBox)
-            If tb Is Nothing Then Return
+            e.Handled = True
+            If tb Is Nothing OrElse tb.IsReadOnly Then Return
 
 
             Try
             Try
-                If Forms.Clipboard.ContainsText() Then
-                    Dim pasteText = Forms.Clipboard.GetText()
+                Dim pasteText = Retrier.Attempt(func:=
+                Function(Attempt) As String
+                    Return If(Forms.Clipboard.ContainsText(), Forms.Clipboard.GetText(), Nothing)
+                End Function, maxAttempts:=10, delay:=Function() TimeSpan.FromMilliseconds(100))
+                If pasteText IsNot Nothing Then
 
 
                     '将换行符替换为空格(#7825)
                     '将换行符替换为空格(#7825)
                     If Not tb.AcceptsReturn Then pasteText = pasteText.ReplaceLineEndings(" ", mergeMultiple:=True)
                     If Not tb.AcceptsReturn Then pasteText = pasteText.ReplaceLineEndings(" ", mergeMultiple:=True)
@@ -105,10 +124,8 @@ Namespace Controls.Behaviors
                     tb.SelectionLength = 0
                     tb.SelectionLength = 0
                 End If
                 End If
             Catch ex As Exception
             Catch ex As Exception
-                Logger.Warn(ex, "粘贴失败,请稍后再试", LogBehavior.Toast)
+                Logger.Warn(ex, $"粘贴失败,请稍后再试(错误码:0x{ex.HResult:X8})", LogBehavior.Toast)
             End Try
             End Try
-
-            e.Handled = True
         End Sub
         End Sub
 
 
         Private Shared Sub OnCopyRichTextBox(sender As Object, e As ExecutedRoutedEventArgs)
         Private Shared Sub OnCopyRichTextBox(sender As Object, e As ExecutedRoutedEventArgs)
@@ -119,10 +136,9 @@ Namespace Controls.Behaviors
             If String.IsNullOrEmpty(textRange.Text) Then Return
             If String.IsNullOrEmpty(textRange.Text) Then Return
 
 
             Try
             Try
-                Forms.Clipboard.Clear()
-                Forms.Clipboard.SetDataObject(textRange.Text, True)
+                SetClipboardText(textRange.Text)
             Catch ex As Exception
             Catch ex As Exception
-                Logger.Warn(ex, "复制失败,请稍后再试", LogBehavior.Toast)
+                Logger.Warn(ex, $"复制失败,请稍后再试(错误码:0x{ex.HResult:X8})", LogBehavior.Toast)
             End Try
             End Try
 
 
             e.Handled = True
             e.Handled = True
@@ -134,16 +150,16 @@ Namespace Controls.Behaviors
 
 
             Dim selection = New TextRange(rtb.Selection.Start, rtb.Selection.End)
             Dim selection = New TextRange(rtb.Selection.Start, rtb.Selection.End)
             If String.IsNullOrEmpty(selection.Text) Then Return
             If String.IsNullOrEmpty(selection.Text) Then Return
+            e.Handled = True
 
 
             Try
             Try
-                Forms.Clipboard.Clear()
-                Forms.Clipboard.SetDataObject(selection.Text, True)
+                SetClipboardText(selection.Text)
             Catch ex As Exception
             Catch ex As Exception
-                Logger.Warn(ex, "剪切失败,请稍后再试", LogBehavior.Toast)
+                Logger.Warn(ex, $"剪切失败,请稍后再试(错误码:0x{ex.HResult:X8})", LogBehavior.Toast)
+                Return
             End Try
             End Try
 
 
             selection.Text = String.Empty
             selection.Text = String.Empty
-            e.Handled = True
         End Sub
         End Sub
 
 
         Private Shared Sub OnPasteRichTextBox(sender As Object, e As ExecutedRoutedEventArgs)
         Private Shared Sub OnPasteRichTextBox(sender As Object, e As ExecutedRoutedEventArgs)
@@ -151,9 +167,11 @@ Namespace Controls.Behaviors
             If rtb Is Nothing Then Return
             If rtb Is Nothing Then Return
 
 
             Try
             Try
-                If Not Forms.Clipboard.ContainsText() Then Return
-
-                Dim pasteText = Forms.Clipboard.GetText()
+                Dim pasteText = Retrier.Attempt(func:=
+                Function(Attempt) As String
+                    Return If(Forms.Clipboard.ContainsText(), Forms.Clipboard.GetText(), Nothing)
+                End Function, maxAttempts:=2, delay:=Function() TimeSpan.FromMilliseconds(200))
+                If pasteText Is Nothing Then Return
                 Dim selection = rtb.Selection
                 Dim selection = rtb.Selection
 
 
                 selection.Text = pasteText
                 selection.Text = pasteText
@@ -162,7 +180,7 @@ Namespace Controls.Behaviors
                 rtb.CaretPosition = caretPos
                 rtb.CaretPosition = caretPos
                 rtb.Selection.Select(caretPos, caretPos)
                 rtb.Selection.Select(caretPos, caretPos)
             Catch ex As Exception
             Catch ex As Exception
-                Logger.Warn(ex, "粘贴失败,请稍后再试", LogBehavior.Toast)
+                Logger.Warn(ex, $"粘贴失败,请稍后再试(错误码:0x{ex.HResult:X8})", LogBehavior.Toast)
             End Try
             End Try
 
 
             e.Handled = True
             e.Handled = True
@@ -187,10 +205,9 @@ Namespace Controls.Behaviors
             Dim sbStr = sb.ToString().TrimEnd(ControlChars.Cr, ControlChars.Lf)
             Dim sbStr = sb.ToString().TrimEnd(ControlChars.Cr, ControlChars.Lf)
 
 
             Try
             Try
-                Forms.Clipboard.Clear()
-                Forms.Clipboard.SetDataObject(sbStr, True)
+                SetClipboardText(sbStr)
             Catch ex As Exception
             Catch ex As Exception
-                Logger.Warn(ex, "复制失败,请稍后再试", LogBehavior.Toast)
+                Logger.Warn(ex, $"复制失败,请稍后再试(错误码:0x{ex.HResult:X8})", LogBehavior.Toast)
             End Try
             End Try
 
 
             e.Handled = True
             e.Handled = True

+ 31 - 72
Plain Craft Launcher 2/Controls/MyCard.vb

@@ -102,73 +102,36 @@ Public Class MyCard
             RunInUi(Sub() UseAnimation = RawUseAnimation, True)
             RunInUi(Sub() UseAnimation = RawUseAnimation, True)
         End If
         End If
     End Sub
     End Sub
-    Public Sub StackInstall()
-        StackInstall(SwapControl, SwapType, Title)
-        TriggerForceResize()
+
+    '填充
+    Private ContentBuilder As Action(Of Panel)
+    Public Sub SetContentBuilder(Builder As Action(Of Panel))
+        ContentBuilder = Builder
     End Sub
     End Sub
-    Public Shared Sub StackInstall(ByRef Stack As StackPanel, Type As Integer, Optional CardTitle As String = "")
-        '这一部分的代码是好几年前留下的究极屎坑,当时还不知道该咋正确调用这种方法,就写了这么一坨屎
-        '但是现在……反正勉强能用……懒得改了就这样吧.jpg
-        '别骂了别骂了.jpg
-        If Stack.Tag Is Nothing Then Return
-        '排序
-        Dim HasDuplicates As Boolean = False
-        Select Case Type
-            Case 3
-                Stack.Tag = CType(Stack.Tag, List(Of DlOptiFineListEntry)).SortByComparison(Function(a, b) CompareVersionGE(a.DisplayName, b.DisplayName))
-            Case 4, 10
-                Stack.Tag = CType(Stack.Tag, List(Of DlLiteLoaderListEntry)).SortByComparison(Function(a, b) CompareVersionGE(a.Inherit, b.Inherit))
-            Case 6
-                Stack.Tag = CType(Stack.Tag, List(Of DlForgeVersionEntry)).OrderByDescending(Function(a) a).ToList
-            Case 8, 9
-                Stack.Tag = CType(Stack.Tag, List(Of ResourceVersion)).SortByComparison(Function(a, b) a.ReleaseDate > b.ReleaseDate)
-                HasDuplicates =
-                    CType(Stack.Tag, List(Of ResourceVersion)).DistinctBy(Function(a) a.Display).Count <>
-                    CType(Stack.Tag, List(Of ResourceVersion)).Count
-        End Select
-        '控件转换
-        Select Case Type
-            Case 6
-                ForgeDownloadListItemPreload(Stack, Stack.Tag, AddressOf ForgeSave_Click, True)
-            Case 8
-                PageDownloadResourceDetail.ResourceFilesCardPreload(Stack, Stack.Tag)
-        End Select
-        '实现控件虚拟化
-        For Each Data As Object In Stack.Tag
-            Select Case Type
-                Case 0
-                    Stack.Children.Add(CType(Data, McInstance).ToListItem(AddressOf PageSelectRight.McInstanceListContent))
-                Case 2
-                    Stack.Children.Add(McDownloadListItem(Data, AddressOf McDownloadMenuSave, True))
-                Case 3
-                    Stack.Children.Add(OptiFineDownloadListItem(Data, AddressOf OptiFineSave_Click, True))
-                Case 6
-                    Stack.Children.Add(ForgeDownloadListItem(Data, AddressOf ForgeSave_Click, True))
-                Case 7
-                    '不能使用 AddressOf,这导致了 #535,原因完全不明,疑似是编译器 Bug
-                    Stack.Children.Add(McDownloadListItem(Data, Sub(sender, e) FrmDownloadInstall.MinecraftSelected(sender, e), False))
-                Case 8
-                    '若存在重复的版本名,则显示文件名而非版本名(#1344)
-                    Stack.Children.Add(CType(Data, ResourceVersion).ToListItem(
-                        AddressOf FrmDownloadResourceDetail.Save_Click, IsBadDisplay:=HasDuplicates))
-                Case 9
-                    '若存在重复的版本名,则显示文件名而非版本名(#1344)
-                    Stack.Children.Add(CType(Data, ResourceVersion).ToListItem(
-                        AddressOf FrmDownloadResourceDetail.Install_Click, AddressOf FrmDownloadResourceDetail.Save_Click, IsBadDisplay:=HasDuplicates))
-                Case 10
-                    Stack.Children.Add(LiteLoaderDownloadListItem(Data, AddressOf LiteLoaderSave_Click, True))
-                Case 11
-                    Stack.Children.Add(CType(Data, HelpEntry).ToListItem)
-                Case 12
-                    Stack.Children.Add(FabricDownloadListItem(CType(Data, JObject), AddressOf FrmDownloadInstall.Fabric_Selected))
-                Case 13
-                    Stack.Children.Add(NeoForgeDownloadListItem(Data, AddressOf NeoForgeSave_Click, True))
-                Case Else
-                    Logger.Error($"未知的虚拟化种类:{Type}")
-            End Select
-        Next
-        Stack.Children.Add(New FrameworkElement With {.Height = 18}) '下边距,同时适应折叠
-        Stack.Tag = Nothing
+    Public Sub SetContentBuilder(Of T)(Items As IEnumerable(Of T), ItemBuilder As Func(Of T, UIElement))
+        SetContentBuilder(
+        Sub(Stack)
+            For Each Item In Items
+                Stack.Children.Add(ItemBuilder(Item))
+            Next
+            Stack.Children.Add(New FrameworkElement With {.Height = 18})
+        End Sub)
+    End Sub
+    ''' <summary>
+    ''' 使用 ContentBuilder 生成卡片内容。
+    ''' </summary>
+    Public Sub BuildContent()
+        If ContentBuilder Is Nothing Then Return
+        Dim Stack = TryCast(SwapControl, Panel)
+        If Stack Is Nothing Then Return
+        Try
+            ContentBuilder(Stack)
+            ContentBuilder = Nothing
+        Catch
+            Stack.Children.Clear()
+            Throw
+        End Try
+        TriggerForceResize()
     End Sub
     End Sub
 
 
     '动画
     '动画
@@ -278,10 +241,6 @@ Public Class MyCard
     Public SwapControl As FrameworkElement
     Public SwapControl As FrameworkElement
     Public Property CanSwap As Boolean = False
     Public Property CanSwap As Boolean = False
     ''' <summary>
     ''' <summary>
-    ''' 被折叠的种类,用于控件虚拟化。
-    ''' </summary>
-    Public Property SwapType As Integer
-    ''' <summary>
     ''' 是否已被折叠。
     ''' 是否已被折叠。
     ''' </summary>
     ''' </summary>
     Public Property IsSwapped As Boolean
     Public Property IsSwapped As Boolean
@@ -293,7 +252,7 @@ Public Class MyCard
             _IsSwapped = value
             _IsSwapped = value
             If SwapControl Is Nothing Then Return
             If SwapControl Is Nothing Then Return
             '展开
             '展开
-            If Not IsSwapped AndAlso TypeOf SwapControl Is StackPanel Then StackInstall(SwapControl, SwapType, Title)
+            If Not IsSwapped Then BuildContent()
             '若尚未加载,会在 Loaded 事件中触发无动画的折叠,不需要在这里进行
             '若尚未加载,会在 Loaded 事件中触发无动画的折叠,不需要在这里进行
             If Not IsLoaded Then Return
             If Not IsLoaded Then Return
             '更新样式
             '更新样式
@@ -397,4 +356,4 @@ Partial Public Module ModAnimation
             If CallBack IsNot Nothing Then CallBack(Control)
             If CallBack IsNot Nothing Then CallBack(Control)
         End If
         End If
     End Sub
     End Sub
-End Module
+End Module

+ 1 - 1
Plain Craft Launcher 2/Controls/MyImage.vb

@@ -90,7 +90,7 @@ Public Class MyImage
             Catch ex As Exception
             Catch ex As Exception
                 Logger.Warn(ex, $"加载图片失败({value})")
                 Logger.Warn(ex, $"加载图片失败({value})")
                 Try
                 Try
-                    If value.StartsWithF(PathTemp) Then FileUtils.Delete(value)
+                    If value.StartsWithF(PathTemp, True) Then FileUtils.Delete(value)
                 Catch
                 Catch
                 End Try
                 End Try
             End Try
             End Try

+ 34 - 20
Plain Craft Launcher 2/FormMain.xaml.vb

@@ -11,6 +11,13 @@ Public Class FormMain
         Dim FeatureList As New List(Of KeyValuePair(Of Integer, String))
         Dim FeatureList As New List(Of KeyValuePair(Of Integer, String))
         '统计更新日志条目
         '统计更新日志条目
         If BuildType = BuildTypes.Release Then
         If BuildType = BuildTypes.Release Then
+            If LastVersion < 411 Then 'Release 2.14.0.0
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "新增:允许修改、添加、移除、更新已安装版本的 MC 版本以及各个组件,可从版本设置中进入"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:启动游戏时允许按 Esc 取消"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:允许复制主页的启动日志中的内容"))
+                FeatureCount += 19
+                BugCount += 26
+            End If
             If LastVersion < 408 Then 'Release 2.13.1.1
             If LastVersion < 408 Then 'Release 2.13.1.1
                 FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:版本设置中,添加打开截图文件夹的快捷方式"))
                 FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:版本设置中,添加打开截图文件夹的快捷方式"))
                 FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:导出整合包时,可以勾选导出光影设置文件"))
                 FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:导出整合包时,可以勾选导出光影设置文件"))
@@ -93,6 +100,13 @@ Public Class FormMain
             '3:BUG+ IMP* FEAT-
             '3:BUG+ IMP* FEAT-
             '2:BUG* IMP-
             '2:BUG* IMP-
             '1:BUG-
             '1:BUG-
+            If LastVersion < 412 Then 'Snapshot 2.14.0.0
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "新增:允许修改、添加、移除、更新已安装版本的 MC 版本以及各个组件,可从版本设置中进入"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:启动游戏时允许按 Esc 取消"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:允许复制主页的启动日志中的内容"))
+                FeatureCount += 19
+                BugCount += 26
+            End If
             If LastVersion < 410 Then 'Snapshot 2.13.1.1
             If LastVersion < 410 Then 'Snapshot 2.13.1.1
                 If LastVersion = 409 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:更新 Mod 时可能更新成另一个 Mod 加载器的版本"))
                 If LastVersion = 409 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:更新 Mod 时可能更新成另一个 Mod 加载器的版本"))
                 FeatureCount += 2
                 FeatureCount += 2
@@ -786,7 +800,7 @@ Public Class FormMain
         If PanMsg.Children.Any Then
         If PanMsg.Children.Any Then
             If e.Key = Key.Enter Then
             If e.Key = Key.Enter Then
                 CType(PanMsg.Children(0), Object).Btn1_Click()
                 CType(PanMsg.Children(0), Object).Btn1_Click()
-            ElseIf e.Key = Key.Escape OrElse (e.Key = Key.W AndAlso Keyboard.Modifiers.HasFlag(ModifierKeys.Control)) Then
+            ElseIf e.Key = Key.Escape Then
                 Dim Msg As Object = PanMsg.Children(0)
                 Dim Msg As Object = PanMsg.Children(0)
                 If TypeOf Msg IsNot MyMsgInput AndAlso TypeOf Msg IsNot MyMsgSelect AndAlso Msg.Btn3.Visibility = Visibility.Visible Then
                 If TypeOf Msg IsNot MyMsgInput AndAlso TypeOf Msg IsNot MyMsgSelect AndAlso Msg.Btn3.Visibility = Visibility.Visible Then
                     Msg.Btn3_Click()
                     Msg.Btn3_Click()
@@ -803,13 +817,13 @@ Public Class FormMain
         ' 在没有弹窗时:继续检查……
         ' 在没有弹窗时:继续检查……
         '==========================
         '==========================
 
 
-        '按 ESC:启动中则取消启动,否则返回上一级
+        '按 ESC 取消启动,或返回上一级
         If e.Key = Key.Escape Then
         If e.Key = Key.Escape Then
-            If McLaunchLoader.State = LoadState.Loading Then
-                McLaunchCancel()
-            Else
-                TriggerPageBack()
+            If PageCurrent = FormMain.PageType.Launch AndAlso McLaunchLoader.State = LoadState.Loading Then
+                FrmLaunchLeft.BtnCancel_Click()
+                Return
             End If
             End If
+            TriggerPageBack()
         End If
         End If
         '更改隐藏版本可见性
         '更改隐藏版本可见性
         If e.Key = Key.F11 AndAlso PageCurrent = FormMain.PageType.InstanceSelect Then
         If e.Key = Key.F11 AndAlso PageCurrent = FormMain.PageType.InstanceSelect Then
@@ -834,11 +848,6 @@ Public Class FormMain
             If TypeOf PageRight Is IRefreshable Then CType(PageRight, IRefreshable).Refresh()
             If TypeOf PageRight Is IRefreshable Then CType(PageRight, IRefreshable).Refresh()
             Return
             Return
         End If
         End If
-        'Ctrl + w 关闭程序
-        If e.Key = Key.W AndAlso Keyboard.Modifiers.HasFlag(ModifierKeys.Control) Then
-            EndProgram(True)
-            Return
-        End If
         '调用启动游戏
         '调用启动游戏
         If e.Key = Key.Enter AndAlso PageCurrent = FormMain.PageType.Launch Then
         If e.Key = Key.Enter AndAlso PageCurrent = FormMain.PageType.Launch Then
             If IsAprilEnabled AndAlso Not IsAprilGiveup Then
             If IsAprilEnabled AndAlso Not IsAprilGiveup Then
@@ -1026,14 +1035,14 @@ Public Class FormMain
             If FilePathList.Count > 1 Then
             If FilePathList.Count > 1 Then
                 '必须要求全部为 jar 文件
                 '必须要求全部为 jar 文件
                 For Each File In PathList
                 For Each File In PathList
-                    If Not {"jar", "litemod", "disabled", "old"}.Contains(File.AfterLast(".").Lower) Then
+                    If Not {"jar", "litemod", "disabled", "old"}.Contains(PathUtils.GetExtension(File)) Then
                         Hint("一次请只拖入一个文件!", HintType.Red)
                         Hint("一次请只拖入一个文件!", HintType.Red)
                         Return
                         Return
                     End If
                     End If
                 Next
                 Next
             End If
             End If
             '主页
             '主页
-            Dim Extension As String = FilePath.AfterLast(".").Lower
+            Dim Extension As String = PathUtils.GetExtension(FilePath)
             If Extension = "xaml" Then
             If Extension = "xaml" Then
                 Logger.Info("文件后缀为 XAML,作为主页加载")
                 Logger.Info("文件后缀为 XAML,作为主页加载")
                 If FileUtils.Exists(Paths.Base & "PCL\Custom.xaml") Then
                 If FileUtils.Exists(Paths.Base & "PCL\Custom.xaml") Then
@@ -1054,9 +1063,9 @@ Public Class FormMain
             If PageInstanceMod.InstallMods(PathList) Then Return
             If PageInstanceMod.InstallMods(PathList) Then Return
             '安装整合包
             '安装整合包
             If {"zip", "rar", "mrpack"}.Any(Function(t) t = Extension) Then '部分压缩包是 zip 格式但后缀为 rar,总之试一试
             If {"zip", "rar", "mrpack"}.Any(Function(t) t = Extension) Then '部分压缩包是 zip 格式但后缀为 rar,总之试一试
-                Logger.Info("文件为压缩包,尝试作为整合包安装")
-                Hint("检测到压缩包拖入,正在准备安装……")
+                Hint("正在读取压缩包……")
                 Try
                 Try
+                    Logger.Info("文件为压缩包,尝试作为整合包安装")
                     ModpackInstall(FilePath)
                     ModpackInstall(FilePath)
                     RunInUi(Sub() FrmMain.PageChange(FormMain.PageType.TaskManager))
                     RunInUi(Sub() FrmMain.PageChange(FormMain.PageType.TaskManager))
                     Return
                     Return
@@ -1067,7 +1076,7 @@ Public Class FormMain
             End If
             End If
             'RAR 处理
             'RAR 处理
             If Extension = "rar" Then
             If Extension = "rar" Then
-                Hint("PCL 无法处理 rar 格式的压缩包,请在解压后重新压缩为 zip 格式再试!")
+                Hint("PCL 无法处理 rar 格式的压缩包,请在解压后重新压缩为 zip 格式再试!", HintType.Red)
                 Return
                 Return
             End If
             End If
             '错误报告分析
             '错误报告分析
@@ -1083,7 +1092,7 @@ Public Class FormMain
                 Logger.Error(ex, "自主错误报告分析失败")
                 Logger.Error(ex, "自主错误报告分析失败")
             End Try
             End Try
             '未知操作
             '未知操作
-            Hint("PCL 无法确定应当执行的文件拖拽操作……")
+            Hint("PCL 无法确定适合的操作……", HintType.Red)
         End Sub, "文件拖拽")
         End Sub, "文件拖拽")
     End Sub
     End Sub
 
 
@@ -1225,9 +1234,10 @@ Public Class FormMain
         OtherTest = 2
         OtherTest = 2
         InstanceOverall = 0
         InstanceOverall = 0
         InstanceSetup = 1
         InstanceSetup = 1
-        InstanceMod = 2
-        InstanceModDisabled = 3
-        InstanceExport = 4
+        InstanceModify = 2
+        InstanceMod = 3
+        InstanceModDisabled = 4
+        InstanceExport = 5
     End Enum
     End Enum
     ''' <summary>
     ''' <summary>
     ''' 获取次级页面的名称。若并非次级页面则返回空字符串,故可以以此判断是否为次级页面。
     ''' 获取次级页面的名称。若并非次级页面则返回空字符串,故可以以此判断是否为次级页面。
@@ -1392,6 +1402,10 @@ Public Class FormMain
     ''' </summary>
     ''' </summary>
     Private Sub PageChangeActual(Stack As PageStackData, Optional SubType As PageSubType = -1)
     Private Sub PageChangeActual(Stack As PageStackData, Optional SubType As PageSubType = -1)
         If PageCurrent = Stack AndAlso (PageCurrentSub = SubType OrElse SubType = -1) Then Return
         If PageCurrent = Stack AndAlso (PageCurrentSub = SubType OrElse SubType = -1) Then Return
+        If Stack.Page = PageType.InstanceSetup AndAlso PageInstanceLeft.Instance IsNot Nothing AndAlso McInstanceLock.IsLocked(PageInstanceLeft.Instance) Then
+            Hint("该版本正在被修改,暂时无法进入版本设置!", HintType.Red)
+            Return
+        End If
         AniControlEnabled += 1
         AniControlEnabled += 1
         Try
         Try
 
 

+ 28 - 20
Plain Craft Launcher 2/Modules/Base/ModBase.vb

@@ -6,12 +6,12 @@ Public Module ModBase
 #Region "声明"
 #Region "声明"
 
 
     '下列版本信息由更新器自动修改
     '下列版本信息由更新器自动修改
-    Public Const VersionBaseName As String = "2.13.1.1" '显示用版本名
+    Public Const VersionBaseName As String = "2.14.0.0" '显示用版本名
     Public Const CommitHash As String = "" 'Commit Hash,由 GitHub Workflow 自动替换
     Public Const CommitHash As String = "" 'Commit Hash,由 GitHub Workflow 自动替换
 #If RELEASE Then
 #If RELEASE Then
-    Public Const VersionCode As Integer = 408 '正式版
+    Public Const VersionCode As Integer = 411 '正式版
 #Else
 #Else
-    Public Const VersionCode As Integer = 410 '快照版
+    Public Const VersionCode As Integer = 412 '快照版
 #End If
 #End If
 
 
     '版本信息
     '版本信息
@@ -47,7 +47,7 @@ Public Module ModBase
     ''' <summary>
     ''' <summary>
     ''' 程序的缓存文件夹路径,以 \ 结尾。
     ''' 程序的缓存文件夹路径,以 \ 结尾。
     ''' </summary>
     ''' </summary>
-    Public PathTemp As String = If(Settings.Get(Of String)("SystemSystemCache") = "", Path.GetTempPath() & "PCL\", Settings.Get(Of String)("SystemSystemCache")).ToString.Replace("/", "\").TrimEnd("\") & "\"
+    Public PathTemp As String = PathUtils.AddSlashSuffix(If(Settings.Get(Of String)("SystemSystemCache") = "", Path.GetTempPath() & "PCL\", Settings.Get(Of String)("SystemSystemCache")).Replace("/", "\"))
     ''' <summary>
     ''' <summary>
     ''' 当前程序的语言。
     ''' 当前程序的语言。
     ''' </summary>
     ''' </summary>
@@ -75,7 +75,7 @@ Public Module ModBase
     ''' <summary>
     ''' <summary>
     ''' 系统盘盘符,以 \ 结尾。例如 “C:\”。
     ''' 系统盘盘符,以 \ 结尾。例如 “C:\”。
     ''' </summary>
     ''' </summary>
-    Public OsDrive As String = Environment.GetLogicalDrives().Where(Function(p) Directory.Exists(p)).First.Upper.First & ":\" '#3799
+    Public OsDrive As String = Environment.GetLogicalDrives().First(Function(p) Directory.Exists(p)) '#3799
 
 
     Public ReadOnly Property ModeDebug As Boolean
     Public ReadOnly Property ModeDebug As Boolean
         Get
         Get
@@ -576,7 +576,7 @@ Public Module ModBase
         Try
         Try
             If String.IsNullOrEmpty(Folder) Then Return False
             If String.IsNullOrEmpty(Folder) Then Return False
             Folder = PathUtils.AddSlashSuffix(Folder)
             Folder = PathUtils.AddSlashSuffix(Folder)
-            If Folder.EndsWithF(":\System Volume Information\") OrElse Folder.EndsWithF(":\$RECYCLE.BIN\") Then Return False
+            If Folder.EndsWithF(":\System Volume Information\", True) OrElse Folder.EndsWithF(":\$RECYCLE.BIN\", True) Then Return False
             If Not DirectoryUtils.Exists(Folder) Then Return False
             If Not DirectoryUtils.Exists(Folder) Then Return False
             Dim TestFilePath As String = $"{Folder}CheckPermission{GetUuid()}.txt"
             Dim TestFilePath As String = $"{Folder}CheckPermission{GetUuid()}.txt"
             FileUtils.Write(TestFilePath, "临时文件,用于检测该文件夹的权限是否正常。")
             FileUtils.Write(TestFilePath, "临时文件,用于检测该文件夹的权限是否正常。")
@@ -593,7 +593,7 @@ Public Module ModBase
     Public Sub CheckPermissionWithException(Folder As String)
     Public Sub CheckPermissionWithException(Folder As String)
         If String.IsNullOrWhiteSpace(Folder) Then Throw New ArgumentNullException("文件夹名不能为空!")
         If String.IsNullOrWhiteSpace(Folder) Then Throw New ArgumentNullException("文件夹名不能为空!")
         Folder = PathUtils.AddSlashSuffix(Folder)
         Folder = PathUtils.AddSlashSuffix(Folder)
-        If Folder.EndsWithF(":\System Volume Information\") OrElse Folder.EndsWithF(":\$RECYCLE.BIN\") Then Throw New UnauthorizedAccessException("没有对系统文件夹的权限!")
+        If Folder.EndsWithF(":\System Volume Information\", True) OrElse Folder.EndsWithF(":\$RECYCLE.BIN\", True) Then Throw New UnauthorizedAccessException("没有对系统文件夹的权限!")
         If Not DirectoryUtils.Exists(Folder) Then Throw New DirectoryNotFoundException("文件夹不存在!")
         If Not DirectoryUtils.Exists(Folder) Then Throw New DirectoryNotFoundException("文件夹不存在!")
         Dim TestFilePath As String = $"{Folder}CheckPermission{GetUuid()}.txt"
         Dim TestFilePath As String = $"{Folder}CheckPermission{GetUuid()}.txt"
         FileUtils.Write(TestFilePath, "临时文件,用于检测该文件夹的权限是否正常。")
         FileUtils.Write(TestFilePath, "临时文件,用于检测该文件夹的权限是否正常。")
@@ -788,7 +788,7 @@ Public Module ModBase
     ''' <summary>
     ''' <summary>
     ''' 可用于临时存放文件的,不含任何特殊字符的文件夹路径,以“\”结尾。
     ''' 可用于临时存放文件的,不含任何特殊字符的文件夹路径,以“\”结尾。
     ''' </summary>
     ''' </summary>
-    Public PathPure As Lazy(Of String) = New Lazy(Of String)(
+    Public PathPure As New Lazy(Of String)(
     Function()
     Function()
         If Paths.Base.IsAsciiOnly() Then
         If Paths.Base.IsAsciiOnly() Then
             Return Paths.Base & "PCL\"
             Return Paths.Base & "PCL\"
@@ -845,6 +845,12 @@ Public Module ModBase
         Return (Now.ToUniversalTime().Ticks - 621355968000000000) / 10000000
         Return (Now.ToUniversalTime().Ticks - 621355968000000000) / 10000000
     End Function
     End Function
     ''' <summary>
     ''' <summary>
+    ''' 获取十进制 Unix 时间戳。
+    ''' </summary>
+    Public Function ToUnixTimestampUtc(Time As Date) As Long
+        Return (Time.ToUniversalTime().Ticks - 621355968000000000) / 10000000
+    End Function
+    ''' <summary>
     ''' 时间戳转化为日期。
     ''' 时间戳转化为日期。
     ''' </summary>
     ''' </summary>
     Public Function GetDate(timeStamp As Integer) As Date
     Public Function GetDate(timeStamp As Integer) As Date
@@ -1021,20 +1027,22 @@ Public Module ModBase
     ''' <summary>
     ''' <summary>
     ''' 设置剪贴板。将在另一线程运行,且不会抛出异常。
     ''' 设置剪贴板。将在另一线程运行,且不会抛出异常。
     ''' </summary>
     ''' </summary>
-    Public Sub ClipboardSet(Text As String, Optional ShowSuccessHint As Boolean = True, Optional SuccessHint As String = "")
+    Public Sub ClipboardSet(Text As String, Optional ShowSuccessHint As Boolean = True)
         RunInThread(
         RunInThread(
         Sub()
         Sub()
-            RunInUi(
-            Sub()
-                Try
-                    My.Computer.Clipboard.Clear()
-                    If Not String.IsNullOrEmpty(Text) Then My.Computer.Clipboard.SetText(Text)
-                    If ShowSuccessHint Then Hint(If(String.IsNullOrEmpty(SuccessHint), "已成功复制!", SuccessHint), HintType.Green)
-                Catch ex As Exception
-                    Logger.Error(ex, behavior:=LogBehavior.None)
-                    Hint("复制失败,可能由于剪贴板被其他程序占用!", HintType.Red)
-                End Try
-            End Sub)
+            Try
+                Retrier.Attempt(delay:=Function(Attempt) TimeSpan.FromMilliseconds(200), maxAttempts:=4, isRetryAllowed:=Function(ex) True, action:=
+                Sub()
+                    RunInUiWait(
+                    Sub()
+                        My.Computer.Clipboard.Clear()
+                        If Not String.IsNullOrEmpty(Text) Then My.Computer.Clipboard.SetText(Text)
+                    End Sub)
+                End Sub)
+                If ShowSuccessHint Then Hint("已成功复制!", HintType.Green)
+            Catch ex As Exception
+                Logger.Error(ex, "可能由于剪贴板被其他程序占用,文本复制失败", LogBehavior.Toast)
+            End Try
         End Sub)
         End Sub)
     End Sub
     End Sub
     ''' <summary>
     ''' <summary>

+ 4 - 4
Plain Craft Launcher 2/Modules/Base/ModNet.vb

@@ -1396,7 +1396,7 @@ FinishExCatch:
         Public Overrides Sub Start(Optional Input As Object = Nothing, Optional IsForceRestart As Boolean = False)
         Public Overrides Sub Start(Optional Input As Object = Nothing, Optional IsForceRestart As Boolean = False)
             If Input IsNot Nothing Then Files = New ConcurrentQueue(Of NetFile)(CType(Input, IEnumerable(Of NetFile)))
             If Input IsNot Nothing Then Files = New ConcurrentQueue(Of NetFile)(CType(Input, IEnumerable(Of NetFile)))
             '去重
             '去重
-            Files = New ConcurrentQueue(Of NetFile)(Files.DistinctBy(Function(a) a.LocalPath))
+            Files = New ConcurrentQueue(Of NetFile)(Files.DistinctBy(Function(a) PathUtils.ForCompare(a.LocalPath).Lower()))
             '设置剩余文件数
             '设置剩余文件数
             SyncLock FileRemainLock
             SyncLock FileRemainLock
                 FileRemain += Files.Where(Function(f) f.State <> NetState.Finished).Count
                 FileRemain += Files.Where(Function(f) f.State <> NetState.Finished).Count
@@ -1421,7 +1421,7 @@ FinishExCatch:
                         Next
                         Next
                         If Not File.HasAvailableSource() Then Throw New ArgumentException("输入的下载链接不正确!")
                         If Not File.HasAvailableSource() Then Throw New ArgumentException("输入的下载链接不正确!")
                         File.LocalPath = File.LocalPath.Replace("/", "\")
                         File.LocalPath = File.LocalPath.Replace("/", "\")
-                        If Not File.LocalPath.Lower.Contains(":\") Then Throw New ArgumentException("输入的本地文件地址不正确:" & File.LocalPath)
+                        If Not File.LocalPath.Contains(":\") Then Throw New ArgumentException("输入的本地文件地址不正确:" & File.LocalPath)
                         If File.LocalPath.EndsWithF("\") Then Throw New ArgumentException("请输入含文件名的完整文件路径:" & File.LocalPath)
                         If File.LocalPath.EndsWithF("\") Then Throw New ArgumentException("请输入含文件名的完整文件路径:" & File.LocalPath)
                         DirectoryUtils.Create(PathUtils.RemoveLastPart(File.LocalPath)) '创建目标文件夹
                         DirectoryUtils.Create(PathUtils.RemoveLastPart(File.LocalPath)) '创建目标文件夹
                     Next
                     Next
@@ -1519,7 +1519,7 @@ FinishExCatch:
                 Select(Function(FolderName) (FolderName, File.LocalPath.IndexOfF(FolderName, True))).
                 Select(Function(FolderName) (FolderName, File.LocalPath.IndexOfF(FolderName, True))).
                 Where(Function(kv) kv.Item2 >= 0).ToList
                 Where(Function(kv) kv.Item2 >= 0).ToList
             If Not TypeIndexes.Any Then
             If Not TypeIndexes.Any Then
-                If File.LocalName.EndsWithF(".jar") Then
+                If PathUtils.GetExtension(File.LocalName) = "jar" Then
                     TypeIndexes.Add(("\versions\", 1)) '总是对 jar 进行版本文件检查,以包括另存为 jar 的情况
                     TypeIndexes.Add(("\versions\", 1)) '总是对 jar 进行版本文件检查,以包括另存为 jar 的情况
                 Else
                 Else
                     Return Nothing
                     Return Nothing
@@ -1547,7 +1547,7 @@ FinishExCatch:
                 Case "versions\"
                 Case "versions\"
                     '版本 jar 或 json:查找 MC 文件夹下的各个版本文件夹
                     '版本 jar 或 json:查找 MC 文件夹下的各个版本文件夹
                     For Each VersionFolder In VersionFolders
                     For Each VersionFolder In VersionFolders
-                        For Each Candidate In DirectoryUtils.EnumerateFiles(VersionFolder, searchPattern:="*." & PathUtils.GetLastPart(File.LocalPath).AfterLast(".").Lower)
+                        For Each Candidate In DirectoryUtils.EnumerateFiles(VersionFolder, searchPattern:="*." & PathUtils.GetExtension(File.LocalPath))
                             If CheckCandidate(Candidate, File.Check) Then Return Candidate
                             If CheckCandidate(Candidate, File.Check) Then Return Candidate
                         Next
                         Next
                     Next
                     Next

+ 1 - 1
Plain Craft Launcher 2/Modules/Base/ModValidate.vb

@@ -382,7 +382,7 @@ Public Class ValidateFolderPath
     Public Overrides Function Validate(Str As String) As String
     Public Overrides Function Validate(Str As String) As String
         '去除尾部斜线,统一为 \
         '去除尾部斜线,统一为 \
         Str = Str.Replace("/", "\")
         Str = Str.Replace("/", "\")
-        If Not Str.TrimEnd("\").EndsWithF(":") Then Str = Str.TrimEnd("\")
+        If Not PathUtils.RemoveSlashSuffix(Str).EndsWithF(":") Then Str = PathUtils.RemoveSlashSuffix(Str)
         '检查是否为空
         '检查是否为空
         Dim LengthCheck As String = New ValidateNullOrWhiteSpace().Validate(Str)
         Dim LengthCheck As String = New ValidateNullOrWhiteSpace().Validate(Str)
         If LengthCheck <> "" Then Return LengthCheck
         If LengthCheck <> "" Then Return LengthCheck

+ 3 - 3
Plain Craft Launcher 2/Modules/Minecraft/ModCrash.vb

@@ -92,7 +92,7 @@ Public Class CrashAnalyzer
         '尝试视作压缩包解压
         '尝试视作压缩包解压
         Try
         Try
             Dim Info = FileUtils.GetInfo(FilePath)
             Dim Info = FileUtils.GetInfo(FilePath)
-            If Info.Exists AndAlso Info.Length > 0 AndAlso Not FilePath.EndsWithF(".jar", True) Then
+            If Info.Exists AndAlso Info.Length > 0 AndAlso Not PathUtils.GetExtension(FilePath) = "jar" Then
                 FileUtils.ExtractToDirectory(FilePath, TempFolder & "Temp\")
                 FileUtils.ExtractToDirectory(FilePath, TempFolder & "Temp\")
                 Logger.Info($"已解压导入的日志文件:{FilePath}")
                 Logger.Info($"已解压导入的日志文件:{FilePath}")
                 GoTo Extracted
                 GoTo Extracted
@@ -163,9 +163,9 @@ Extracted:
                 Else
                 Else
                     TargetType = AnalyzeFileType.ExtraLogFile
                     TargetType = AnalyzeFileType.ExtraLogFile
                 End If
                 End If
-            ElseIf MatchName.EndsWithF(".log", True) Then
+            ElseIf PathUtils.GetExtension(MatchName) = "log" Then
                 TargetType = AnalyzeFileType.ExtraLogFile
                 TargetType = AnalyzeFileType.ExtraLogFile
-            ElseIf MatchName.EndsWithF(".txt", True) Then
+            ElseIf PathUtils.GetExtension(MatchName) = "txt" Then
                 TargetType = AnalyzeFileType.ExtraReportFile
                 TargetType = AnalyzeFileType.ExtraReportFile
             Else
             Else
                 Logger.Info($"{MatchName} 分类为 Ignore")
                 Logger.Info($"{MatchName} 分类为 Ignore")

+ 2 - 4
Plain Craft Launcher 2/Modules/Minecraft/ModDownload.vb

@@ -242,8 +242,6 @@ Public Module ModDownload
                 DlPreferMojang = DeltaTime < 4000
                 DlPreferMojang = DeltaTime < 4000
                 Logger.Info($"Mojang 官方源加载耗时:{DeltaTime}ms,{If(DlPreferMojang, "可优先使用官方源", "不优先使用官方源")}")
                 Logger.Info($"Mojang 官方源加载耗时:{DeltaTime}ms,{If(DlPreferMojang, "可优先使用官方源", "不优先使用官方源")}")
             End If
             End If
-            '添加 PCL 特供项
-            If FileUtils.Exists(PathTemp & "Cache\download.json") Then Versions.Merge(FileUtils.ReadAsJson(PathTemp & "Cache\download.json"))
             '返回
             '返回
             Loader.Output = New DlClientListResult With {.IsOfficial = True, .SourceName = "Mojang 官方源", .Value = Json}
             Loader.Output = New DlClientListResult With {.IsOfficial = True, .SourceName = "Mojang 官方源", .Value = Json}
             'MC 更新提示
             'MC 更新提示
@@ -438,7 +436,7 @@ Public Module ModDownload
                     .FileName = If(Name(i).ContainsIgnoreCase("pre"), "preview_", "") & "OptiFine_" & Name(i).Replace(" ", "_") & ".jar",
                     .FileName = If(Name(i).ContainsIgnoreCase("pre"), "preview_", "") & "OptiFine_" & Name(i).Replace(" ", "_") & ".jar",
                     .RequiredForgeVersion = Forge(i).Replace("Forge ", "").Replace("#", "")}
                     .RequiredForgeVersion = Forge(i).Replace("Forge ", "").Replace("#", "")}
                 If Entry.RequiredForgeVersion.Contains("N/A") Then Entry.RequiredForgeVersion = Nothing
                 If Entry.RequiredForgeVersion.Contains("N/A") Then Entry.RequiredForgeVersion = Nothing
-                Entry.InstanceName = Entry.Inherit & "-OptiFine_" & Name(i).ToString.Replace(" ", "_").Replace(Entry.Inherit & "_", "")
+                Entry.InstanceName = Entry.Inherit & "-OptiFine_" & Name(i).AfterFirst(" ").Replace(" ", "_")
                 Versions.Add(Entry)
                 Versions.Add(Entry)
             Next
             Next
             Loader.Output = New DlOptiFineListResult With {.IsOfficial = True, .SourceName = "OptiFine 官方源", .Value = Versions}
             Loader.Output = New DlOptiFineListResult With {.IsOfficial = True, .SourceName = "OptiFine 官方源", .Value = Versions}
@@ -581,7 +579,7 @@ Public Module ModDownload
             If Version <> other.Version Then
             If Version <> other.Version Then
                 Return Version.CompareTo(other.Version)
                 Return Version.CompareTo(other.Version)
             Else
             Else
-                Return CompareVersion(VersionName, other.VersionName)
+                Return McUtils.CompareVersion(VersionName, other.VersionName)
             End If
             End If
         End Function
         End Function
     End Class
     End Class

+ 24 - 25
Plain Craft Launcher 2/Modules/Minecraft/ModJava.vb

@@ -18,7 +18,7 @@ Public Module ModJava
                     '强制选择的 Java
                     '强制选择的 Java
                     Dim SelectedJavaSetting = Settings.Get(Of String)("LaunchArgumentJavaSelect")
                     Dim SelectedJavaSetting = Settings.Get(Of String)("LaunchArgumentJavaSelect")
                     Dim SelectedJava = If(SelectedJavaSetting.StartsWithF("{"), SelectedJavaSetting.DeserializeJson(Of JObject)()("Path").ToString, "")
                     Dim SelectedJava = If(SelectedJavaSetting.StartsWithF("{"), SelectedJavaSetting.DeserializeJson(Of JObject)()("Path").ToString, "")
-                    Dim Match = OldJavaList.FirstOrDefault(Function(j) j.Folder.Equals(SelectedJava, StringComparison.OrdinalIgnoreCase))
+                    Dim Match = OldJavaList.FirstOrDefault(Function(j) PathUtils.IsSame(j.Folder, SelectedJava))
                     If Match IsNot Nothing Then
                     If Match IsNot Nothing Then
                         OldJavaList.Remove(Match)
                         OldJavaList.Remove(Match)
                         OldJavaList.Insert(0, Match)
                         OldJavaList.Insert(0, Match)
@@ -78,7 +78,7 @@ Public Module ModJava
             Return
             Return
         End If
         End If
         '不再移除它
         '不再移除它
-        Configs.JavaRemovedList.Edit(Sub(ByRef List) List.RemoveIf(Function(f) f.Equals(NewJava.Folder, StringComparison.OrdinalIgnoreCase)))
+        Configs.JavaRemovedList.Edit(Sub(ByRef List) List.RemoveIf(Function(f) PathUtils.IsSame(f, NewJava.Folder)))
         '加入列表顶部
         '加入列表顶部
         Configs.JavaList.Edit(
         Configs.JavaList.Edit(
         Sub(ByRef List)
         Sub(ByRef List)
@@ -150,16 +150,16 @@ Public Module ModJava
         End Sub
         End Sub
 
 
         '原版 26-:通过版本号判断
         '原版 26-:通过版本号判断
-        If (Not Instance.Version.Vaild AndAlso Instance.ReleaseTime >= New Date(2024, 4, 2)) OrElse
-           (Instance.Version.Vaild AndAlso Instance.Version.Vanilla >= New Version(20, 0, 5)) Then
+        If (Not Instance.Version.Valid AndAlso Instance.ReleaseTime >= New Date(2024, 4, 2)) OrElse
+           (Instance.Version.Valid AndAlso Instance.Version.Vanilla >= New Version(20, 0, 5)) Then
             '1.20.5+(24w14a+):至少 Java 21
             '1.20.5+(24w14a+):至少 Java 21
             AddConstraint(ValueRange(Of Version).AtLeast(New Version(21, 0)))
             AddConstraint(ValueRange(Of Version).AtLeast(New Version(21, 0)))
-        ElseIf (Not Instance.Version.Vaild AndAlso Instance.ReleaseTime >= New Date(2021, 11, 16)) OrElse
-               (Instance.Version.Vaild AndAlso Instance.Version.Vanilla.Major >= 18) Then
+        ElseIf (Not Instance.Version.Valid AndAlso Instance.ReleaseTime >= New Date(2021, 11, 16)) OrElse
+               (Instance.Version.Valid AndAlso Instance.Version.Vanilla.Major >= 18) Then
             '1.18 pre2+:至少 Java 17
             '1.18 pre2+:至少 Java 17
             AddConstraint(ValueRange(Of Version).AtLeast(New Version(17, 0)))
             AddConstraint(ValueRange(Of Version).AtLeast(New Version(17, 0)))
-        ElseIf (Not Instance.Version.Vaild AndAlso Instance.ReleaseTime >= New Date(2021, 5, 11)) OrElse
-               (Instance.Version.Vaild AndAlso Instance.Version.Vanilla.Major >= 17) Then
+        ElseIf (Not Instance.Version.Valid AndAlso Instance.ReleaseTime >= New Date(2021, 5, 11)) OrElse
+               (Instance.Version.Valid AndAlso Instance.Version.Vanilla.Major >= 17) Then
             '1.17+ (21w19a+):至少 Java 16
             '1.17+ (21w19a+):至少 Java 16
             AddConstraint(ValueRange(Of Version).AtLeast(New Version(16, 0)))
             AddConstraint(ValueRange(Of Version).AtLeast(New Version(16, 0)))
         ElseIf Instance.ReleaseTime.Year >= 2017 Then 'Minecraft 1.12 与 1.11 的分界线正好是 2017 年,太棒了
         ElseIf Instance.ReleaseTime.Year >= 2017 Then 'Minecraft 1.12 与 1.11 的分界线正好是 2017 年,太棒了
@@ -185,7 +185,7 @@ Public Module ModJava
         End If
         End If
 
 
         'OptiFine 检测
         'OptiFine 检测
-        If Instance.Version.HasOptiFine AndAlso Instance.Version.Vaild Then '不管非标准版本
+        If Instance.Version.HasOptiFine AndAlso Instance.Version.Valid Then '不管非标准版本
             If Instance.Version.Vanilla.Major < 7 Then
             If Instance.Version.Vanilla.Major < 7 Then
                 '<1.7:至多 Java 8
                 '<1.7:至多 Java 8
                 AddConstraint(ValueRange(Of Version).LessThan(New Version(9, 0)))
                 AddConstraint(ValueRange(Of Version).LessThan(New Version(9, 0)))
@@ -199,11 +199,11 @@ Public Module ModJava
         End If
         End If
 
 
         'Forge 检测(部分测试结果见 #8432)
         'Forge 检测(部分测试结果见 #8432)
-        If Instance.Version.HasForge Then
+        If Instance.Version.ModLoaders.HasFlagF(ModLoaders.Forge) Then
             If Instance.Version.Vanilla >= New Version(6, 0, 1) AndAlso Instance.Version.Vanilla <= New Version(7, 0, 2) Then
             If Instance.Version.Vanilla >= New Version(6, 0, 1) AndAlso Instance.Version.Vanilla <= New Version(7, 0, 2) Then
                 '1.6.1 - 1.7.2:必须 Java 7
                 '1.6.1 - 1.7.2:必须 Java 7
                 AddConstraint(ValueRange(Of Version).ClosedOpen(New Version(7, 0), New Version(8, 0)))
                 AddConstraint(ValueRange(Of Version).ClosedOpen(New Version(7, 0), New Version(8, 0)))
-            ElseIf Instance.Version.Vanilla.Major <= 12 OrElse Not Instance.Version.Vaild Then '非标准版本
+            ElseIf Instance.Version.Vanilla.Major <= 12 OrElse Not Instance.Version.Valid Then '非标准版本
                 '<=1.12:Java 8
                 '<=1.12:Java 8
                 AddConstraint(ValueRange(Of Version).LessThan(New Version(9, 0)))
                 AddConstraint(ValueRange(Of Version).LessThan(New Version(9, 0)))
             ElseIf Instance.Version.Vanilla.Major <= 14 Then
             ElseIf Instance.Version.Vanilla.Major <= 14 Then
@@ -212,38 +212,38 @@ Public Module ModJava
             ElseIf Instance.Version.Vanilla.Major = 15 Then
             ElseIf Instance.Version.Vanilla.Major = 15 Then
                 '1.15:Java 8 - 15
                 '1.15:Java 8 - 15
                 AddConstraint(ValueRange(Of Version).ClosedOpen(New Version(8, 0), New Version(16, 0)))
                 AddConstraint(ValueRange(Of Version).ClosedOpen(New Version(8, 0), New Version(16, 0)))
-            ElseIf CompareVersionGE(Instance.Version.Forge, "34.0.0") AndAlso CompareVersionGE("36.2.25", Instance.Version.Forge) Then
+            ElseIf McUtils.CompareVersionGE(Instance.Version.ModLoaderVersions(ModLoaders.Forge), "34.0.0") AndAlso McUtils.CompareVersionGE("36.2.25", Instance.Version.ModLoaderVersions(ModLoaders.Forge)) Then
                 '1.16.3~5,Forge 34.0.0 ~ 36.2.25:最高 Java 8u320
                 '1.16.3~5,Forge 34.0.0 ~ 36.2.25:最高 Java 8u320
                 AddConstraint(ValueRange(Of Version).AtMost(New Version(8, 0, 320)))
                 AddConstraint(ValueRange(Of Version).AtMost(New Version(8, 0, 320)))
-            ElseIf CompareVersionGE(Instance.Version.Forge, "36.2.26") AndAlso CompareVersionGE("36.999999.999999", Instance.Version.Forge) Then
+            ElseIf McUtils.CompareVersionGE(Instance.Version.ModLoaderVersions(ModLoaders.Forge), "36.2.26") AndAlso McUtils.CompareVersionGE("36.999999.999999", Instance.Version.ModLoaderVersions(ModLoaders.Forge)) Then
                 '1.16.5,Forge 36.2.26+,<37.0.0:最高 Java 23
                 '1.16.5,Forge 36.2.26+,<37.0.0:最高 Java 23
                 AddConstraint(ValueRange(Of Version).LessThan(New Version(24, 0)))
                 AddConstraint(ValueRange(Of Version).LessThan(New Version(24, 0)))
-            ElseIf CompareVersionGE(Instance.Version.Forge, "37.0.0") AndAlso CompareVersionGE("37.0.79", Instance.Version.Forge) Then
+            ElseIf McUtils.CompareVersionGE(Instance.Version.ModLoaderVersions(ModLoaders.Forge), "37.0.0") AndAlso McUtils.CompareVersionGE("37.0.79", Instance.Version.ModLoaderVersions(ModLoaders.Forge)) Then
                 '1.17.1,Forge 37.0.0 ~ 37.0.79:最高 Java 16
                 '1.17.1,Forge 37.0.0 ~ 37.0.79:最高 Java 16
                 AddConstraint(ValueRange(Of Version).LessThan(New Version(17, 0)))
                 AddConstraint(ValueRange(Of Version).LessThan(New Version(17, 0)))
             ElseIf Instance.Version.Vanilla.Major = 18 AndAlso Instance.Version.HasOptiFine Then '#305
             ElseIf Instance.Version.Vanilla.Major = 18 AndAlso Instance.Version.HasOptiFine Then '#305
                 '1.18:若安装了 OptiFine,最高 Java 18
                 '1.18:若安装了 OptiFine,最高 Java 18
                 AddConstraint(ValueRange(Of Version).LessThan(New Version(19, 0)))
                 AddConstraint(ValueRange(Of Version).LessThan(New Version(19, 0)))
-            ElseIf CompareVersionGE(Instance.Version.Forge, "45.0.21") AndAlso CompareVersionGE("45.0.65", Instance.Version.Forge) Then
+            ElseIf McUtils.CompareVersionGE(Instance.Version.ModLoaderVersions(ModLoaders.Forge), "45.0.21") AndAlso McUtils.CompareVersionGE("45.0.65", Instance.Version.ModLoaderVersions(ModLoaders.Forge)) Then
                 '1.19.4,Forge 45.0.21 ~ 45.0.65:最高 Java 19
                 '1.19.4,Forge 45.0.21 ~ 45.0.65:最高 Java 19
                 AddConstraint(ValueRange(Of Version).LessThan(New Version(20, 0)))
                 AddConstraint(ValueRange(Of Version).LessThan(New Version(20, 0)))
-            ElseIf CompareVersionGE(Instance.Version.Forge, "45.0.66") AndAlso CompareVersionGE("47.4.8", Instance.Version.Forge) Then
+            ElseIf McUtils.CompareVersionGE(Instance.Version.ModLoaderVersions(ModLoaders.Forge), "45.0.66") AndAlso McUtils.CompareVersionGE("47.4.8", Instance.Version.ModLoaderVersions(ModLoaders.Forge)) Then
                 '1.19.4~1.20.1,Forge 45.0.66 ~ 47.4.8:最高 Java 21
                 '1.19.4~1.20.1,Forge 45.0.66 ~ 47.4.8:最高 Java 21
                 AddConstraint(ValueRange(Of Version).LessThan(New Version(22, 0)))
                 AddConstraint(ValueRange(Of Version).LessThan(New Version(22, 0)))
             End If
             End If
         End If
         End If
 
 
         'NeoForge 检测
         'NeoForge 检测
-        If Instance.Version.HasNeoForge Then
+        If Instance.Version.ModLoaders.HasFlagF(ModLoaders.NeoForge) Then
             If Instance.Version.Vanilla = New Version(20, 0, 1) OrElse
             If Instance.Version.Vanilla = New Version(20, 0, 1) OrElse
-               (CompareVersionGE("20.2.62-beta", Instance.Version.NeoForge) AndAlso Not Instance.Version.NeoForge.Contains("25w14craftmine")) Then
+               (McUtils.CompareVersionGE("20.2.62-beta", Instance.Version.ModLoaderVersions(ModLoaders.NeoForge)) AndAlso Not Instance.Version.ModLoaderVersions(ModLoaders.NeoForge).Contains("25w14craftmine")) Then
                 '1.20.1,以及 1.20.2 的 20.2.62-beta 之前:最高 Java 21
                 '1.20.1,以及 1.20.2 的 20.2.62-beta 之前:最高 Java 21
                 AddConstraint(ValueRange(Of Version).LessThan(New Version(22, 0)))
                 AddConstraint(ValueRange(Of Version).LessThan(New Version(22, 0)))
             End If
             End If
         End If
         End If
 
 
         'Fabric 检测
         'Fabric 检测
-        If Instance.Version.HasFabric AndAlso Instance.Version.Vaild Then '不管非标准版本
+        If Instance.Version.ModLoaders.HasFlagF(ModLoaders.Fabric) AndAlso Instance.Version.Valid Then '不管非标准版本
             If Instance.Version.Vanilla.Major >= 15 AndAlso Instance.Version.Vanilla.Major <= 16 Then
             If Instance.Version.Vanilla.Major >= 15 AndAlso Instance.Version.Vanilla.Major <= 16 Then
                 '1.15 - 1.16:Java 8+
                 '1.15 - 1.16:Java 8+
                 AddConstraint(ValueRange(Of Version).AtLeast(New Version(8, 0)))
                 AddConstraint(ValueRange(Of Version).AtLeast(New Version(8, 0)))
@@ -251,15 +251,14 @@ Public Module ModJava
                 '1.18+:Java 17+
                 '1.18+:Java 17+
                 AddConstraint(ValueRange(Of Version).AtLeast(New Version(17, 0)))
                 AddConstraint(ValueRange(Of Version).AtLeast(New Version(17, 0)))
             End If
             End If
-            If Instance.Version.Fabric <> "未知版本" AndAlso
-               CompareVersion(Instance.Version.Fabric, "0.17.0") < 0 Then
+            If Instance.Version.ModLoaderVersions(ModLoaders.Fabric) <> "未知版本" AndAlso McUtils.CompareVersion(Instance.Version.ModLoaderVersions(ModLoaders.Fabric), "0.17.0") < 0 Then
                 'Fabric Loader 0.16.x 及更早版本的 Mixin/ASM 不兼容 Java 25
                 'Fabric Loader 0.16.x 及更早版本的 Mixin/ASM 不兼容 Java 25
                 AddConstraint(ValueRange(Of Version).LessThan(New Version(25, 0)))
                 AddConstraint(ValueRange(Of Version).LessThan(New Version(25, 0)))
             End If
             End If
         End If
         End If
 
 
         'LiteLoader 检测
         'LiteLoader 检测
-        If Instance.Version.HasLiteLoader AndAlso Instance.Version.Vaild Then '不管非标准版本
+        If Instance.Version.ModLoaders.HasFlagF(ModLoaders.LiteLoader) AndAlso Instance.Version.Valid Then '不管非标准版本
             '最高 Java 8
             '最高 Java 8
             AddConstraint(ValueRange(Of Version).LessThan(New Version(9, 0)))
             AddConstraint(ValueRange(Of Version).LessThan(New Version(9, 0)))
         End If
         End If
@@ -411,9 +410,9 @@ Public Module ModJava
                         Dim Components = DownloadListWorker.LastResult
                         Dim Components = DownloadListWorker.LastResult
 
 
                         '确定要下载的目标 Java
                         '确定要下载的目标 Java
-                        Dim JavaToDownload As JavaDownloadInfo = If(
-                        Components.FirstOrDefault(Function(Comp) Comp.Java("version")("name").ToString = RecommendedComponent), '优先选择指定的 Component
-                        Components.FirstOrDefault(Function(Comp) Range.Contains(Comp.JavaVersion)))
+                        Dim JavaToDownload As JavaDownloadInfo = Components.FirstOrDefault(
+                            Function(Comp) Comp.Java("version")("name").ToString = RecommendedComponent, '优先选择指定的 Component
+                            Function(Comp) Range.Contains(Comp.JavaVersion))
                         If JavaToDownload Is Nothing Then
                         If JavaToDownload Is Nothing Then
                             Logger.Warn($"Mojang 提供的 Java 列表中没有合适的 Java,无法自动下载")
                             Logger.Warn($"Mojang 提供的 Java 列表中没有合适的 Java,无法自动下载")
                             '提示需要手动下载 Java
                             '提示需要手动下载 Java

+ 62 - 89
Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb

@@ -48,7 +48,7 @@ Public Module ModLaunch
             End Try
             End Try
             '检查版本
             '检查版本
             CurrentLaunchOptions.Instance.Load()
             CurrentLaunchOptions.Instance.Load()
-            If CurrentLaunchOptions.Instance.State = McInstanceState.Error Then
+            If CurrentLaunchOptions.Instance.State = McInstance.McInstanceState.Error Then
                 Hint($"无法启动 {CurrentLaunchOptions.Instance.Name}:{CurrentLaunchOptions.Instance.Info}", HintType.Red)
                 Hint($"无法启动 {CurrentLaunchOptions.Instance.Name}:{CurrentLaunchOptions.Instance.Info}", HintType.Red)
                 Return False
                 Return False
             End If
             End If
@@ -57,6 +57,10 @@ Public Module ModLaunch
             FrmLaunchLeft.RefreshButtonsUI()
             FrmLaunchLeft.RefreshButtonsUI()
             FrmLaunchLeft.RefreshPage(False, False)
             FrmLaunchLeft.RefreshPage(False, False)
         End If
         End If
+        If McInstanceSelected IsNot Nothing AndAlso McInstanceLock.IsLocked(McInstanceSelected) Then
+            Hint($"版本 {McInstanceSelected.Name} 正在被修改,请稍后再启动!", HintType.Red)
+            Return False
+        End If
         FrmMain.AprilGiveup()
         FrmMain.AprilGiveup()
         '禁止进入版本选择页面(否则就可以在启动中切换 McInstanceSelected 了)
         '禁止进入版本选择页面(否则就可以在启动中切换 McInstanceSelected 了)
         FrmMain.PageStack = FrmMain.PageStack.Where(Function(p) p.Page <> FormMain.PageType.InstanceSelect).ToList
         FrmMain.PageStack = FrmMain.PageStack.Where(Function(p) p.Page <> FormMain.PageType.InstanceSelect).ToList
@@ -70,7 +74,12 @@ Public Module ModLaunch
     ''' </summary>
     ''' </summary>
     Public Sub McLaunchLog(Text As String, Optional Level As LogLevel = LogLevel.Info)
     Public Sub McLaunchLog(Text As String, Optional Level As LogLevel = LogLevel.Info)
         Text = FilterUserName(FilterAccessToken(Text, "*"), "*")
         Text = FilterUserName(FilterAccessToken(Text, "*"), "*")
-        If ModeDebug Then RunInUi(Sub() FrmLaunchRight.LabLog.Text += $"{vbCrLf}[{Date.Now:HH':'mm':'ss'.'fff}] {Text}")
+        If ModeDebug Then
+            RunInUi(
+            Sub()
+                FrmLaunchRight.LabLog.AppendText($"[{Date.Now:HH':'mm':'ss'.'fff}] {Text}{vbCrLf}")
+            End Sub)
+        End If
         Logger.Log(Text, Level)
         Logger.Log(Text, Level)
     End Sub
     End Sub
 
 
@@ -196,25 +205,6 @@ NextInner:
         End Try
         End Try
     End Sub
     End Sub
 
 
-    ''' <summary>
-    ''' 取消正在进行的游戏启动。
-    ''' </summary>
-    Public Sub McLaunchCancel()
-        If McLaunchLoaderReal IsNot Nothing Then
-            McLaunchLoaderReal.Cancel()
-            McLaunchLog("已取消启动")
-            Try
-                If McLaunchWatcher IsNot Nothing Then
-                    McLaunchWatcher.Kill()
-                ElseIf McLaunchProcess IsNot Nothing Then
-                    If Not McLaunchProcess.HasExited Then McLaunchProcess.Kill()
-                End If
-            Catch ex As Exception
-                Logger.Error(ex, "取消启动结束进程失败", LogBehavior.Toast)
-            End Try
-        End If
-    End Sub
-
 #End Region
 #End Region
 
 
 #Region "内存优化"
 #Region "内存优化"
@@ -249,7 +239,7 @@ NextInner:
         '检查版本
         '检查版本
         If McInstanceSelected Is Nothing Then Throw New Exception("未选择 Minecraft 版本!")
         If McInstanceSelected Is Nothing Then Throw New Exception("未选择 Minecraft 版本!")
         McInstanceSelected.Load()
         McInstanceSelected.Load()
-        If McInstanceSelected.State = McInstanceState.Error Then Throw New Exception("Minecraft 存在问题:" & McInstanceSelected.Info)
+        If McInstanceSelected.State = McInstance.McInstanceState.Error Then Throw New Exception("Minecraft 存在问题:" & McInstanceSelected.Info)
         '检查输入信息
         '检查输入信息
         Dim CheckResult As String = ""
         Dim CheckResult As String = ""
         RunInUiWait(Sub() CheckResult = McLoginAble(McLoginInput()))
         RunInUiWait(Sub() CheckResult = McLoginAble(McLoginInput()))
@@ -557,7 +547,7 @@ NextInner:
         '检查是否已经登录完成
         '检查是否已经登录完成
         Dim ExpiresAt = Settings.Get(Of Long)("CacheMsV2Expires")
         Dim ExpiresAt = Settings.Get(Of Long)("CacheMsV2Expires")
         If Not Data.IsForceRestarting AndAlso '不要求强行重启
         If Not Data.IsForceRestarting AndAlso '不要求强行重启
-           ExpiresAt > 0 AndAlso ExpiresAt > GetUnixTimestampUtc() AndAlso 'AccessToken 尚未过期
+           ExpiresAt > 0 AndAlso ExpiresAt > ToUnixTimestampUtc(Date.UtcNow) AndAlso 'AccessToken 尚未过期
            Input.UserName = Settings.Get(Of String)("CacheMsV2Name") Then
            Input.UserName = Settings.Get(Of String)("CacheMsV2Name") Then
             Data.Output = New McLoginResult With {
             Data.Output = New McLoginResult With {
                 .Name = Input.UserName, .Type = "Microsoft",
                 .Name = Input.UserName, .Type = "Microsoft",
@@ -1052,7 +1042,7 @@ Retry:
         Dim ResultJson As JObject = Result.DeserializeJson()
         Dim ResultJson As JObject = Result.DeserializeJson()
         Return (
         Return (
             ResultJson("access_token").ToString,
             ResultJson("access_token").ToString,
-            ResultJson("expires_in").ToObject(Of Integer) + GetUnixTimestampUtc() - 1200) '提前 20 分钟视作过期
+            ResultJson("expires_in").ToObject(Of Integer) + GetUnixTimestampUtc() - 3600) '提前 1 小时视作过期
     End Function
     End Function
     '微软登录步骤 5:验证微软账号是否持有 MC,这也会刷新 XGP
     '微软登录步骤 5:验证微软账号是否持有 MC,这也会刷新 XGP
     Private Sub MsLoginStep5(AccessToken As String)
     Private Sub MsLoginStep5(AccessToken As String)
@@ -1382,16 +1372,11 @@ NextInstance:
         'Log4j 漏洞防御参数
         'Log4j 漏洞防御参数
         Args.Add("-Dlog4j2.formatMsgNoLookups=true")
         Args.Add("-Dlog4j2.formatMsgNoLookups=true")
 
 
-        '编码(#4700、#5892、#5909、#6934、#8950)
-        Dim EncodingName As String
-        Select Case McLaunchJavaSelected.Version.Major
-            Case Is >= 21 'Java 21+ 会固定使用 UTF-8
-                EncodingName = "UTF-8"
-            Case Is >= 18 'Java 18~20 时,启动 MC 时会添加 COMPAT 参数,因此 Java 会回退到 native.encoding
-                EncodingName = McLaunchJavaSelected.NativeEncodingName
-            Case Else 'Java 17- 时,直接使用 file.encoding
-                EncodingName = McLaunchJavaSelected.FileEncodingName
-        End Select
+        '编码(#4700、#5892、#5909、#6934、#8950、#9241)
+        Dim UseCompatEncoding = McLaunchJavaSelected.Version.Major >= 18
+        Dim EncodingName = If(UseCompatEncoding,
+                              McLaunchJavaSelected.NativeEncodingName, 'Java 18+ 会添加 COMPAT,因此 Java 会回退到 native.encoding
+                              McLaunchJavaSelected.FileEncodingName) 'Java 17- 直接使用 file.encoding
         Try
         Try
             McLaunchEncoding = Encoding.GetEncoding(If(EncodingName.Equals("GBK", StringComparison.OrdinalIgnoreCase), "GB18030", EncodingName))
             McLaunchEncoding = Encoding.GetEncoding(If(EncodingName.Equals("GBK", StringComparison.OrdinalIgnoreCase), "GB18030", EncodingName))
         Catch ex As Exception
         Catch ex As Exception
@@ -1405,7 +1390,7 @@ NextInstance:
         Dim StderrArgName = If(McLaunchJavaSelected.Version.Major < 19, "sun.stderr.encoding", "stderr.encoding")
         Dim StderrArgName = If(McLaunchJavaSelected.Version.Major < 19, "sun.stderr.encoding", "stderr.encoding")
         If Not Args.Any(Function(a) a.StartsWithF($"-D{StdoutArgName}=")) Then Args.Add($"-D{StdoutArgName}={LaunchEncodingName}")
         If Not Args.Any(Function(a) a.StartsWithF($"-D{StdoutArgName}=")) Then Args.Add($"-D{StdoutArgName}={LaunchEncodingName}")
         If Not Args.Any(Function(a) a.StartsWithF($"-D{StderrArgName}=")) Then Args.Add($"-D{StderrArgName}={LaunchEncodingName}")
         If Not Args.Any(Function(a) a.StartsWithF($"-D{StderrArgName}=")) Then Args.Add($"-D{StderrArgName}={LaunchEncodingName}")
-        If McLaunchJavaSelected.Version.Major >= 18 AndAlso McLaunchJavaSelected.Version.Major < 21 Then Args.Add("-Dfile.encoding=COMPAT") '使用 COMPAT 防止部分 Mod 配置文件不兼容 UTF-8
+        If UseCompatEncoding AndAlso Not Args.Any(Function(a) a.StartsWithF("-Dfile.encoding=")) Then Args.Add("-Dfile.encoding=COMPAT") '防止老版本 MC 与部分 Mod 不兼容 UTF-8
 
 
         '为 JLW 添加 -jar,它必须放在最后
         '为 JLW 添加 -jar,它必须放在最后
         If UseJLW Then
         If UseJLW Then
@@ -1465,7 +1450,8 @@ NextInstance:
         Arg &= " " & CustomArg
         Arg &= " " & CustomArg
 
 
         '把 OptiFineForgeTweaker 放在参数的末尾
         '把 OptiFineForgeTweaker 放在参数的末尾
-        If (McInstanceSelected.Version.HasForge OrElse McInstanceSelected.Version.HasLiteLoader) AndAlso McInstanceSelected.Version.HasOptiFine Then
+        If (McInstanceSelected.Version.ModLoaders.HasFlagF(ModLoaders.Forge) OrElse McInstanceSelected.Version.ModLoaders.HasFlagF(ModLoaders.LiteLoader)) AndAlso
+                McInstanceSelected.Version.HasOptiFine AndAlso Not McInstanceSelected.Version.OptiFineAsMod Then
             If Arg.Contains("--tweakClass optifine.OptiFineForgeTweaker") Then
             If Arg.Contains("--tweakClass optifine.OptiFineForgeTweaker") Then
                 McLaunchLog("发现正确的 OptiFineForge TweakClass,目前的游戏参数:" & Arg)
                 McLaunchLog("发现正确的 OptiFineForge TweakClass,目前的游戏参数:" & Arg)
                 Arg = Arg.Replace(" --tweakClass optifine.OptiFineForgeTweaker", "") & " --tweakClass optifine.OptiFineForgeTweaker"
                 Arg = Arg.Replace(" --tweakClass optifine.OptiFineForgeTweaker", "") & " --tweakClass optifine.OptiFineForgeTweaker"
@@ -1520,10 +1506,10 @@ NextInstance:
     Private Iterator Function McLaunchArgumentsReplace(Arguments As List(Of String)) As IEnumerable(Of (Key As String, Value As String))
     Private Iterator Function McLaunchArgumentsReplace(Arguments As List(Of String)) As IEnumerable(Of (Key As String, Value As String))
         '基础参数
         '基础参数
         Yield ("${classpath_separator}", ";")
         Yield ("${classpath_separator}", ";")
-        Yield ("${natives_directory}", GetNativesFolder().TrimEnd("\"c))
+        Yield ("${natives_directory}", PathUtils.RemoveSlashSuffix(GetNativesFolder()))
         Yield ("${library_directory}", PathUtils.ToShortPath(McFolderSelected & "libraries"))
         Yield ("${library_directory}", PathUtils.ToShortPath(McFolderSelected & "libraries"))
         Yield ("${libraries_directory}", PathUtils.ToShortPath(McFolderSelected & "libraries"))
         Yield ("${libraries_directory}", PathUtils.ToShortPath(McFolderSelected & "libraries"))
-        Yield ("${pure_directory}", PathPure.Value.TrimEnd("\"c)) '由 PCL 添加,这会允许在分割参数并去重后再替换路径,防止路径中的特殊字符影响参数分割和去重
+        Yield ("${pure_directory}", PathUtils.RemoveSlashSuffix(PathPure.Value)) '由 PCL 添加,这会允许在分割参数并去重后再替换路径,防止路径中的特殊字符影响参数分割和去重
         Yield ("${launcher_name}", "PCL")
         Yield ("${launcher_name}", "PCL")
         Yield ("${launcher_version}", VersionCode)
         Yield ("${launcher_version}", VersionCode)
         Yield ("${version_name}", McInstanceSelected.Name)
         Yield ("${version_name}", McInstanceSelected.Name)
@@ -1568,9 +1554,8 @@ NextInstance:
             Case Else
             Case Else
                 TargetSize = New Size(854, 480)
                 TargetSize = New Size(854, 480)
         End Select
         End Select
-        If McInstanceSelected.Version.Vanilla.Major <= 12 AndAlso
-            McLaunchJavaSelected.Version >= New Version(8, 0, 200) AndAlso McLaunchJavaSelected.Version <= New Version(8, 0, 321) AndAlso
-            Not McInstanceSelected.Version.HasOptiFine AndAlso Not McInstanceSelected.Version.HasForge Then '修复 1.12.2- 下 JRE 8u200~321 下窗口大小为设置大小的 DPI% 倍(#3463)
+        If McInstanceSelected.Version.Vanilla.Major <= 12 AndAlso McLaunchJavaSelected.Version >= New Version(8, 0, 200) AndAlso McLaunchJavaSelected.Version <= New Version(8, 0, 321) AndAlso
+                Not McInstanceSelected.Version.HasOptiFine AndAlso Not McInstanceSelected.Version.ModLoaders.HasFlagF(ModLoaders.Forge) Then '修复 1.12.2- 下 JRE 8u200~321 下窗口大小为设置大小的 DPI% 倍(#3463)
             McLaunchLog($"已应用窗口大小过大修复(Java 版本:{McLaunchJavaSelected.Version})")
             McLaunchLog($"已应用窗口大小过大修复(Java 版本:{McLaunchJavaSelected.Version})")
             TargetSize.Width /= DPI / 96
             TargetSize.Width /= DPI / 96
             TargetSize.Height /= DPI / 96
             TargetSize.Height /= DPI / 96
@@ -1694,7 +1679,7 @@ NextInstance:
             End Try
             End Try
             For Each Entry In Zip.Entries
             For Each Entry In Zip.Entries
                 Dim FileName As String = Entry.FullName
                 Dim FileName As String = Entry.FullName
-                If FileName.EndsWithF(".dll", True) Then
+                If PathUtils.GetExtension(FileName) = "dll" Then
                     '实际解压文件的步骤
                     '实际解压文件的步骤
                     Dim FilePath As String = Target & FileName
                     Dim FilePath As String = Target & FileName
                     ExistFiles.Add(FilePath)
                     ExistFiles.Add(FilePath)
@@ -1738,8 +1723,9 @@ NextInstance:
     ''' <summary>
     ''' <summary>
     ''' 获取 Natives 文件夹路径,以 \ 结尾。
     ''' 获取 Natives 文件夹路径,以 \ 结尾。
     ''' </summary>
     ''' </summary>
-    Private Function GetNativesFolder() As String
-        Dim Result As String = PathUtils.AddSlashSuffix(PathUtils.ToShortPath(McInstanceSelected.PathVersion)) & McInstanceSelected.Name & "-natives\"
+    Public Function GetNativesFolder(Optional Instance As McInstance = Nothing) As String
+        If Instance Is Nothing Then Instance = McInstanceSelected
+        Dim Result As String = PathUtils.AddSlashSuffix(PathUtils.ToShortPath(Instance.PathVersion)) & Instance.Name & "-natives\"
         If Not (IsGBKEncoding OrElse Result.IsAsciiOnly()) Then
         If Not (IsGBKEncoding OrElse Result.IsAsciiOnly()) Then
             Result = Paths.AppData & ".minecraft\bin\natives\"
             Result = Paths.AppData & ".minecraft\bin\natives\"
             If Not Result.IsAsciiOnly() Then
             If Not Result.IsAsciiOnly() Then
@@ -1865,7 +1851,7 @@ NextInstance:
             Dim CurrentLang As String = ReadIni(SetupFileAddress, "lang", "none")
             Dim CurrentLang As String = ReadIni(SetupFileAddress, "lang", "none")
             Dim RequiredLang As String = If(CurrentLang = "none" OrElse Not DirectoryUtils.Exists(McInstanceSelected.PathIndie & "saves"), '#3844,整合包可能已经自带了 options.txt
             Dim RequiredLang As String = If(CurrentLang = "none" OrElse Not DirectoryUtils.Exists(McInstanceSelected.PathIndie & "saves"), '#3844,整合包可能已经自带了 options.txt
                 If(Settings.Get(Of Boolean)("ToolHelpChinese"), "zh_cn", "en_us"), CurrentLang.Lower)
                 If(Settings.Get(Of Boolean)("ToolHelpChinese"), "zh_cn", "en_us"), CurrentLang.Lower)
-            If Not McInstanceSelected.Version.Vaild OrElse McInstanceSelected.Version.Vanilla.Major <= 10 Then
+            If Not McInstanceSelected.Version.Valid OrElse McInstanceSelected.Version.Vanilla.Major <= 10 Then
                 '将最后两位改为大写,前面的部分保留
                 '将最后两位改为大写,前面的部分保留
                 RequiredLang = RequiredLang.Substring(0, RequiredLang.Length - 2) & RequiredLang.Substring(RequiredLang.Length - 2).Upper
                 RequiredLang = RequiredLang.Substring(0, RequiredLang.Length - 2) & RequiredLang.Substring(RequiredLang.Length - 2).Upper
             End If
             End If
@@ -1908,53 +1894,40 @@ NextInstance:
             If McLoginLoader.Input.Type = McLoginType.Legacy AndAlso Settings.Get(Of Integer)("LaunchSkinType") = 4 AndAlso FileUtils.Exists(Paths.AppDataThenName & "CustomSkin.png") Then
             If McLoginLoader.Input.Type = McLoginType.Legacy AndAlso Settings.Get(Of Integer)("LaunchSkinType") = 4 AndAlso FileUtils.Exists(Paths.AppDataThenName & "CustomSkin.png") Then
                 Dim MetaFileAddress As String = PathTemp & "pack.mcmeta"
                 Dim MetaFileAddress As String = PathTemp & "pack.mcmeta"
                 Dim PackPicAddress As String = PathTemp & "pack.png"
                 Dim PackPicAddress As String = PathTemp & "pack.png"
-                Dim PackFormat As Integer
-                Select Case McInstanceSelected.Version.Vanilla.Major
-                    Case 0, 1, 2, 3, 4, 5
-                        '更早的版本没有资源包
-                        McLaunchLog("Minecraft 版本过老,尚不支持自定义离线皮肤")
-                        GoTo IgnoreCustomSkin
-                    Case 6, 7, 8
-                        PackFormat = 1
-                    Case 9, 10
-                        PackFormat = 2
-                    Case 11, 12
-                        PackFormat = 3
-                    Case 13, 14
-                        PackFormat = 4
-                    Case 15
-                        PackFormat = 5
-                    Case 16
-                        PackFormat = 6
-                    Case 17
-                        PackFormat = 7
-                    Case 18
-                        If McInstanceSelected.Version.Vanilla.Minor <= 2 Then
-                            PackFormat = 8
-                        Else
-                            PackFormat = 9
-                        End If
-                    Case 19
-                        If McInstanceSelected.Version.Vanilla.Minor <= 3 Then
-                            PackFormat = 9
-                        Else
-                            PackFormat = 12
-                        End If
-                    Case 20
-                        If McInstanceSelected.Version.Vanilla.Minor <= 1 Then
-                            PackFormat = 15
-                        Else
-                            PackFormat = 17
-                        End If
-                    Case Else '快照版是 9999
-                        PackFormat = 17
-                        'https://zh.minecraft.wiki/w/数据包#数据包版本
-                End Select
-                McLaunchLog("正在构建自定义皮肤资源包,格式为:" & PackFormat)
+                If McInstanceSelected.Version.Vanilla.Major < 6 Then
+                    McLaunchLog("Minecraft 版本过老,尚不支持自定义离线皮肤")
+                    GoTo IgnoreCustomSkin
+                End If
+                Dim PackVersionJToken As JToken = McInstanceSelected.JsonVersion?("pack_version")
+                If TypeOf PackVersionJToken Is JObject Then PackVersionJToken = DirectCast(PackVersionJToken, JObject)("resource") '从 1.17 开始,可以从 pack_version.resource 获取资源包版本
+                Dim PackVersion As Integer = Val(PackVersionJToken?.ToString)
+                If PackVersion <= 0 Then '没有对应字段的老版本 MC,或兜底
+                    Select Case McInstanceSelected.Version.Vanilla.Major
+                        Case 6, 7, 8
+                            PackVersion = 1
+                        Case 9, 10
+                            PackVersion = 2
+                        Case 11, 12
+                            PackVersion = 3
+                        Case 13, 14
+                            PackVersion = 4
+                        Case 15
+                            PackVersion = 5
+                        Case 16
+                            PackVersion = If(McInstanceSelected.Version.Vanilla.Build <= 1, 5, 6)
+                        Case 17
+                            PackVersion = 7
+                        Case 18
+                            PackVersion = 8
+                        Case Else '旧版快照可能没有可用的资源包格式声明
+                            PackVersion = 17
+                    End Select
+                End If
+                McLaunchLog($"正在构建自定义皮肤资源包,版本号 {PackVersion},MC 版本 {McInstanceSelected.Version.Vanilla}")
                 '准备文件
                 '准备文件
                 Dim Bit As New MyBitmap(PathImage & "Heads/Logo.png")
                 Dim Bit As New MyBitmap(PathImage & "Heads/Logo.png")
                 Bit.Save(PackPicAddress)
                 Bit.Save(PackPicAddress)
-                FileUtils.Write(MetaFileAddress, "{""pack"":{""pack_format"":" & PackFormat & ",""description"":""PCL 自定义离线皮肤资源包""}}")
+                FileUtils.Write(MetaFileAddress, "{""pack"":{""pack_format"":" & PackVersion & ",""description"":""PCL 自定义离线皮肤资源包""}}")
                 Dim Skin As New MyBitmap(Paths.AppDataThenName & "CustomSkin.png")
                 Dim Skin As New MyBitmap(Paths.AppDataThenName & "CustomSkin.png")
                 If (McInstanceSelected.Version.Vanilla.Major = 6 OrElse McInstanceSelected.Version.Vanilla.Major = 7) AndAlso Skin.Pic.Height = 64 Then
                 If (McInstanceSelected.Version.Vanilla.Major = 6 OrElse McInstanceSelected.Version.Vanilla.Major = 7) AndAlso Skin.Pic.Height = 64 Then
                     McLaunchLog("该 Minecraft 版本不支持双层皮肤,已进行裁剪")
                     McLaunchLog("该 Minecraft 版本不支持双层皮肤,已进行裁剪")

Dosya farkı çok büyük olduğundan ihmal edildi
+ 193 - 565
Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb


+ 95 - 94
Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb

@@ -74,7 +74,7 @@ Public Module ModModpack
             Catch ex As Exception
             Catch ex As Exception
                 If ex.GetDisplay(True).Contains("Error.WinIOError") Then
                 If ex.GetDisplay(True).Contains("Error.WinIOError") Then
                     Throw New Exception("打开整合包文件失败", ex)
                     Throw New Exception("打开整合包文件失败", ex)
-                ElseIf File.EndsWithF(".rar", True) Then
+                ElseIf PathUtils.GetExtension(File) = "rar" Then
                     Throw New Exception("PCL 无法处理 rar 格式的压缩包,请在解压后重新压缩为 zip 格式再试", ex)
                     Throw New Exception("PCL 无法处理 rar 格式的压缩包,请在解压后重新压缩为 zip 格式再试", ex)
                 Else
                 Else
                     Throw New Exception("打开整合包文件失败,文件可能损坏或为不支持的压缩包格式", ex)
                     Throw New Exception("打开整合包文件失败,文件可能损坏或为不支持的压缩包格式", ex)
@@ -119,8 +119,8 @@ Public Module ModModpack
     ''' 对路径末尾是否为 \ 没有要求。
     ''' 对路径末尾是否为 \ 没有要求。
     ''' </summary>
     ''' </summary>
     Private Sub CopyOverrideDirectory(OverridesFolder As String, VersionFolder As String, Loader As LoaderBase)
     Private Sub CopyOverrideDirectory(OverridesFolder As String, VersionFolder As String, Loader As LoaderBase)
-        If Not OverridesFolder.EndsWithF("\") Then OverridesFolder += "\"
-        If Not VersionFolder.EndsWithF("\") Then VersionFolder += "\"
+        OverridesFolder = PathUtils.AddSlashSuffix(OverridesFolder)
+        VersionFolder = PathUtils.AddSlashSuffix(VersionFolder)
         '复制文件
         '复制文件
         If DirectoryUtils.Exists(OverridesFolder) Then
         If DirectoryUtils.Exists(OverridesFolder) Then
             Logger.Info($"处理整合包覆写文件夹:{OverridesFolder} → {VersionFolder}")
             Logger.Info($"处理整合包覆写文件夹:{OverridesFolder} → {VersionFolder}")
@@ -174,25 +174,25 @@ Public Module ModModpack
             If InstanceName = "" Then InstanceName = MyMsgBoxInput("输入版本名称", "", "", New Collection(Of Validate) From {Validate})
             If InstanceName = "" Then InstanceName = MyMsgBoxInput("输入版本名称", "", "", New Collection(Of Validate) From {Validate})
             If String.IsNullOrEmpty(InstanceName) Then Throw New OperationCanceledException
             If String.IsNullOrEmpty(InstanceName) Then Throw New OperationCanceledException
         End If
         End If
+        Dim VersionFolder As String = PathUtils.AddSlashSuffix(Path.Combine(McFolderSelected, "versions", InstanceName) & "\")
 
 
         '获取 Mod API 版本信息
         '获取 Mod API 版本信息
-        Dim TargetVersion As New McVersion
-        TargetVersion.VanillaName = Json("minecraft")("version").ToString
+        Dim TargetVersion As New McVersion(Json("minecraft")("version").ToString)
         For Each Entry In If(Json("minecraft")("modLoaders"), {})
         For Each Entry In If(Json("minecraft")("modLoaders"), {})
             Dim Id As String = If(Entry("id"), "").ToString.Lower
             Dim Id As String = If(Entry("id"), "").ToString.Lower
             If Id.StartsWithF("forge-") Then
             If Id.StartsWithF("forge-") Then
                 'Forge 指定
                 'Forge 指定
                 If Id.Contains("recommended") Then Throw New Exception("该整合包版本过老,已不支持进行安装!")
                 If Id.Contains("recommended") Then Throw New Exception("该整合包版本过老,已不支持进行安装!")
                 Logger.Info($"整合包 Forge 版本:{Id}")
                 Logger.Info($"整合包 Forge 版本:{Id}")
-                TargetVersion.Forge = Id.Replace("forge-", "")
+                TargetVersion.ModLoaderVersions(ModLoaders.Forge) = Id.Replace("forge-", "")
             ElseIf Id.StartsWithF("neoforge-") Then
             ElseIf Id.StartsWithF("neoforge-") Then
                 'NeoForge 指定
                 'NeoForge 指定
                 Logger.Info($"整合包 NeoForge 版本:{Id}")
                 Logger.Info($"整合包 NeoForge 版本:{Id}")
-                TargetVersion.NeoForge = Id.Replace("neoforge-", "")
+                TargetVersion.ModLoaderVersions(ModLoaders.NeoForge) = Id.Replace("neoforge-", "")
             ElseIf Id.StartsWithF("fabric-") Then
             ElseIf Id.StartsWithF("fabric-") Then
                 'Fabric 指定
                 'Fabric 指定
                 Logger.Info($"整合包 Fabric 版本:{Id}")
                 Logger.Info($"整合包 Fabric 版本:{Id}")
-                TargetVersion.Fabric = Id.Replace("fabric-", "")
+                TargetVersion.ModLoaderVersions(ModLoaders.Fabric) = Id.Replace("fabric-", "")
             Else
             Else
                 NotifyIncompatibleLoader(Id)
                 NotifyIncompatibleLoader(Id)
             End If
             End If
@@ -202,12 +202,12 @@ Public Module ModModpack
         Dim InstallLoaders As New List(Of LoaderBase)
         Dim InstallLoaders As New List(Of LoaderBase)
         Dim OverrideHome As String = If(Json("overrides"), "")
         Dim OverrideHome As String = If(Json("overrides"), "")
         If OverrideHome <> "" Then
         If OverrideHome <> "" Then
+            Dim OverridesFolder As String = InstallTemp & ArchiveBaseFolder & If(OverrideHome = "." OrElse OverrideHome = "./", "", OverrideHome) '#5613
+            If Not PathUtils.IsParentOf(InstallTemp, OverridesFolder) Then Throw New Exception("整合包的 overrides 路径超出了解压目录,请向整合包作者反馈此问题!")
             InstallLoaders.Add(New LoaderTask(Of String, Integer)("解压整合包文件",
             InstallLoaders.Add(New LoaderTask(Of String, Integer)("解压整合包文件",
             Sub(Task As LoaderTask(Of String, Integer))
             Sub(Task As LoaderTask(Of String, Integer))
                 ExtractModpackFiles(InstallTemp, FileAddress, Task.CreateCancellationToken(), Task.CreateSyncProgressProvider(ToPercentage:=0.6))
                 ExtractModpackFiles(InstallTemp, FileAddress, Task.CreateCancellationToken(), Task.CreateSyncProgressProvider(ToPercentage:=0.6))
-                CopyOverrideDirectory(
-                    InstallTemp & ArchiveBaseFolder & If(OverrideHome = "." OrElse OverrideHome = "./", "", OverrideHome), '#5613
-                    $"{McFolderSelected}versions\{InstanceName}", Task)
+                CopyOverrideDirectory(OverridesFolder, VersionFolder, Task)
             End Sub) With {
             End Sub) With {
             .ProgressWeight = FileUtils.GetInfo(FileAddress).Length / 1024 / 1024 / 6, .Block = False}) '每 6M 需要 1s
             .ProgressWeight = FileUtils.GetInfo(FileAddress).Length / 1024 / 1024 / 6, .Block = False}) '每 6M 需要 1s
         End If
         End If
@@ -293,7 +293,7 @@ Public Module ModModpack
                     Dim File = ResourceVersion.FromPlatformJson(ModJson, Type)
                     Dim File = ResourceVersion.FromPlatformJson(ModJson, Type)
                     If Not File.DownloadAvailable Then Continue For
                     If Not File.DownloadAvailable Then Continue For
                     '实际的添加
                     '实际的添加
-                    FileList.Add(Id, File.ToNetFile($"{McFolderSelected}versions\{InstanceName}\{TargetFolder}\",
+                    FileList.Add(Id, File.ToNetFile(PathUtils.AddSlashSuffix(Path.Combine(VersionFolder, TargetFolder)),
                         ResourceVersion.DownloadReason.ModPack, TargetVersion.VanillaName, TargetVersion.ModLoaders))
                         ResourceVersion.DownloadReason.ModPack, TargetVersion.VanillaName, TargetVersion.ModLoaders))
                     Task.Progress += 1 / (1 + ModList.Count)
                     Task.Progress += 1 / (1 + ModList.Count)
                 Next
                 Next
@@ -307,15 +307,9 @@ Public Module ModModpack
         End If
         End If
 
 
         '构造加载器
         '构造加载器
-        Dim Request As New McInstallRequest With {
-            .NewInstanceName = InstanceName,
-            .VersionFolder = $"{McFolderSelected}versions\{InstanceName}\",
-            .MinecraftName = TargetVersion.VanillaName,
-            .ForgeVersion = TargetVersion.Forge,
-            .NeoForgeVersion = TargetVersion.NeoForge,
-            .FabricVersion = TargetVersion.Fabric
-        }
-        Dim MergeLoaders As List(Of LoaderBase) = McInstallLoader(Request)
+        Dim Target As New McComponents(TargetVersion.VanillaName, TargetVersion.ModLoaderVersions)
+        Dim MergeLoaders = McInstallLoadersGet(New McInstallRequest With {
+            .NewInstanceName = InstanceName, .InstanceFolder = VersionFolder, .Components = Target})
         '构造总加载器
         '构造总加载器
         Dim Loaders As New List(Of LoaderBase)
         Dim Loaders As New List(Of LoaderBase)
         Loaders.Add(New LoaderCombo(Of String)("整合包安装", InstallLoaders) With {.Show = False, .Block = False, .ProgressWeight = InstallLoaders.Sum(Function(l) l.ProgressWeight)})
         Loaders.Add(New LoaderCombo(Of String)("整合包安装", InstallLoaders) With {.Show = False, .Block = False, .ProgressWeight = InstallLoaders.Sum(Function(l) l.ProgressWeight)})
@@ -323,7 +317,6 @@ Public Module ModModpack
         Loaders.Add(New LoaderTask(Of String, String)("最终整理文件",
         Loaders.Add(New LoaderTask(Of String, String)("最终整理文件",
         Sub(Task As LoaderTask(Of String, String))
         Sub(Task As LoaderTask(Of String, String))
             '设置图标
             '设置图标
-            Dim VersionFolder As String = $"{McFolderSelected}versions\{InstanceName}\"
             If Logo IsNot Nothing AndAlso FileUtils.Exists(Logo) Then
             If Logo IsNot Nothing AndAlso FileUtils.Exists(Logo) Then
                 FileUtils.Copy(Logo, VersionFolder & "PCL\Logo.png")
                 FileUtils.Copy(Logo, VersionFolder & "PCL\Logo.png")
                 WriteIni(VersionFolder & "PCL\Setup.ini", "Logo", "PCL\Logo.png")
                 WriteIni(VersionFolder & "PCL\Setup.ini", "Logo", "PCL\Logo.png")
@@ -337,10 +330,10 @@ Public Module ModModpack
                 FileUtils.Delete(MapFile)
                 FileUtils.Delete(MapFile)
             Next
             Next
             '删除原始整合包文件
             '删除原始整合包文件
-            For Each Target As String In {VersionFolder & "原始整合包.zip", VersionFolder & "原始整合包.mrpack"}
-                If FileUtils.Exists(Target) Then
-                    Logger.Info($"删除原始整合包文件:{Target}")
-                    FileUtils.Delete(Target)
+            For Each FileToDelete As String In {VersionFolder & "原始整合包.zip", VersionFolder & "原始整合包.mrpack"}
+                If FileUtils.Exists(FileToDelete) Then
+                    Logger.Info($"删除原始整合包文件:{FileToDelete}")
+                    FileUtils.Delete(FileToDelete)
                 End If
                 End If
             Next
             Next
             If FileUtils.Exists(FileAddress) AndAlso PathUtils.GetFileNameWithoutExtension(FileAddress) = "modpack" Then
             If FileUtils.Exists(FileAddress) AndAlso PathUtils.GetFileNameWithoutExtension(FileAddress) = "modpack" Then
@@ -358,7 +351,7 @@ Public Module ModModpack
 
 
         '启动
         '启动
         Dim Loader As New LoaderCombo(Of String)(LoaderName, Loaders) With {.OnStateChanged = AddressOf McInstallState}
         Dim Loader As New LoaderCombo(Of String)(LoaderName, Loaders) With {.OnStateChanged = AddressOf McInstallState}
-        Loader.Start(Request.VersionFolder)
+        Loader.Start(VersionFolder)
         LoaderTaskbarAdd(Loader)
         LoaderTaskbarAdd(Loader)
         FrmMain.BtnExtraDownload.ShowRefresh()
         FrmMain.BtnExtraDownload.ShowRefresh()
         Return Loader
         Return Loader
@@ -376,20 +369,21 @@ Public Module ModModpack
         End Try
         End Try
         If Json("dependencies") Is Nothing OrElse Json("dependencies")("minecraft") Is Nothing Then Throw New Exception("Modrinth 整合包未提供 Minecraft 版本信息")
         If Json("dependencies") Is Nothing OrElse Json("dependencies")("minecraft") Is Nothing Then Throw New Exception("Modrinth 整合包未提供 Minecraft 版本信息")
         '获取 Mod API 版本信息
         '获取 Mod API 版本信息
-        Dim TargetVersion As New McVersion
+        'TODO: 改为使用类似 InstallPackMMC 中,一个版本名一个字典的方式来存储它,不要用 Unknown
+        Dim TargetVersion As New McVersion("Unknown")
         For Each Entry As JProperty In If(Json("dependencies"), {})
         For Each Entry As JProperty In If(Json("dependencies"), {})
             Select Case Entry.Name.Lower
             Select Case Entry.Name.Lower
                 Case "minecraft"
                 Case "minecraft"
                     TargetVersion.VanillaName = Entry.Value.ToString
                     TargetVersion.VanillaName = Entry.Value.ToString
                 Case "forge" 'eg. 14.23.5.2859 / 1.19-41.1.0
                 Case "forge" 'eg. 14.23.5.2859 / 1.19-41.1.0
-                    TargetVersion.Forge = Entry.Value.ToString
-                    Logger.Info($"整合包 Forge 版本:{TargetVersion.Forge}")
+                    TargetVersion.ModLoaderVersions(ModLoaders.Forge) = Entry.Value.ToString
+                    Logger.Info($"整合包 Forge 版本:{TargetVersion.ModLoaderVersions(ModLoaders.Forge)}")
                 Case "neoforge", "neo-forge" 'eg. 20.6.98-beta
                 Case "neoforge", "neo-forge" 'eg. 20.6.98-beta
-                    TargetVersion.NeoForge = Entry.Value.ToString
-                    Logger.Info($"整合包 NeoForge 版本:{TargetVersion.NeoForge}")
+                    TargetVersion.ModLoaderVersions(ModLoaders.NeoForge) = Entry.Value.ToString
+                    Logger.Info($"整合包 NeoForge 版本:{TargetVersion.ModLoaderVersions(ModLoaders.NeoForge)}")
                 Case "fabric-loader" 'eg. 0.14.14
                 Case "fabric-loader" 'eg. 0.14.14
-                    TargetVersion.Fabric = Entry.Value.ToString
-                    Logger.Info($"整合包 Fabric 版本:{TargetVersion.Fabric}")
+                    TargetVersion.ModLoaderVersions(ModLoaders.Fabric) = Entry.Value.ToString
+                    Logger.Info($"整合包 Fabric 版本:{TargetVersion.ModLoaderVersions(ModLoaders.Fabric)}")
                 Case Else
                 Case Else
                     NotifyIncompatibleLoader(Entry.Name)
                     NotifyIncompatibleLoader(Entry.Name)
             End Select
             End Select
@@ -402,18 +396,15 @@ Public Module ModModpack
             If InstanceName = "" Then InstanceName = MyMsgBoxInput("输入版本名称", "", "", New Collection(Of Validate) From {Validate})
             If InstanceName = "" Then InstanceName = MyMsgBoxInput("输入版本名称", "", "", New Collection(Of Validate) From {Validate})
             If String.IsNullOrEmpty(InstanceName) Then Throw New OperationCanceledException
             If String.IsNullOrEmpty(InstanceName) Then Throw New OperationCanceledException
         End If
         End If
+        Dim VersionFolder As String = Path.Combine(McFolderSelected, "versions", InstanceName) & "\"
         '解压
         '解压
         Dim InstallTemp As String = RequestTaskTempFolder()
         Dim InstallTemp As String = RequestTaskTempFolder()
         Dim InstallLoaders As New List(Of LoaderBase)
         Dim InstallLoaders As New List(Of LoaderBase)
         InstallLoaders.Add(New LoaderTask(Of String, Integer)("解压整合包文件",
         InstallLoaders.Add(New LoaderTask(Of String, Integer)("解压整合包文件",
         Sub(Task As LoaderTask(Of String, Integer))
         Sub(Task As LoaderTask(Of String, Integer))
             ExtractModpackFiles(InstallTemp, FileAddress, Task.CreateCancellationToken(), Task.CreateSyncProgressProvider(ToPercentage:=0.5))
             ExtractModpackFiles(InstallTemp, FileAddress, Task.CreateCancellationToken(), Task.CreateSyncProgressProvider(ToPercentage:=0.5))
-            CopyOverrideDirectory(
-                InstallTemp & ArchiveBaseFolder & "overrides",
-                McFolderSelected & "versions\" & InstanceName, Task)
-            CopyOverrideDirectory(
-                InstallTemp & ArchiveBaseFolder & "client-overrides",
-                McFolderSelected & "versions\" & InstanceName, Task)
+            CopyOverrideDirectory(InstallTemp & ArchiveBaseFolder & "overrides", VersionFolder, Task)
+            CopyOverrideDirectory(InstallTemp & ArchiveBaseFolder & "client-overrides", VersionFolder, Task)
         End Sub) With {.ProgressWeight = FileUtils.GetInfo(FileAddress).Length / 1024 / 1024 / 6, .Block = False}) '每 6M 需要 1s
         End Sub) With {.ProgressWeight = FileUtils.GetInfo(FileAddress).Length / 1024 / 1024 / 6, .Block = False}) '每 6M 需要 1s
         '获取下载文件列表
         '获取下载文件列表
         Dim FileList As New List(Of NetFile)
         Dim FileList As New List(Of NetFile)
@@ -436,8 +427,8 @@ Public Module ModModpack
             Urls = ResourceVersion.ParseModrinthTrackArguments(Urls,
             Urls = ResourceVersion.ParseModrinthTrackArguments(Urls,
                 ResourceVersion.DownloadReason.ModPack, TargetVersion.VanillaName, TargetVersion.ModLoaders).ToList 'Modrinth 来源追踪信息
                 ResourceVersion.DownloadReason.ModPack, TargetVersion.VanillaName, TargetVersion.ModLoaders).ToList 'Modrinth 来源追踪信息
             Urls = Urls.Distinct.ToList()
             Urls = Urls.Distinct.ToList()
-            Dim TargetPath As String = Path.Combine(McFolderSelected, "versions", InstanceName, File("path").ToString().TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))
-            If Not PathUtils.IsParentOf($"{McFolderSelected}versions\{InstanceName}\", TargetPath) Then
+            Dim TargetPath As String = Path.Combine(VersionFolder, File("path").ToString().TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))
+            If Not PathUtils.IsParentOf(VersionFolder, TargetPath) Then
                 MyMsgBox($"整合包的文件路径超出了版本文件夹,请向整合包作者反馈此问题!{vbCrLf}目标:{Path.GetFullPath(TargetPath)}{vbCrLf}错误的文件:{TargetPath}", "文件路径校验失败", IsWarn:=True)
                 MyMsgBox($"整合包的文件路径超出了版本文件夹,请向整合包作者反馈此问题!{vbCrLf}目标:{Path.GetFullPath(TargetPath)}{vbCrLf}错误的文件:{TargetPath}", "文件路径校验失败", IsWarn:=True)
                 Throw New OperationCanceledException
                 Throw New OperationCanceledException
             End If
             End If
@@ -449,15 +440,10 @@ Public Module ModModpack
         End If
         End If
 
 
         '构造加载器
         '构造加载器
-        Dim Request As New McInstallRequest With {
-            .NewInstanceName = InstanceName,
-            .VersionFolder = $"{McFolderSelected}versions\{InstanceName}\",
-            .MinecraftName = TargetVersion.VanillaName,
-            .ForgeVersion = TargetVersion.Forge,
-            .NeoForgeVersion = TargetVersion.NeoForge,
-            .FabricVersion = TargetVersion.Fabric
-        }
-        Dim MergeLoaders As List(Of LoaderBase) = McInstallLoader(Request)
+        Dim Target As New McComponents(TargetVersion.VanillaName, TargetVersion.ModLoaderVersions)
+        Dim MergeLoaders = McInstallLoadersGet(New McInstallRequest With {
+            .NewInstanceName = InstanceName, .InstanceFolder = VersionFolder,
+            .Components = Target})
         '构造总加载器
         '构造总加载器
         Dim Loaders As New List(Of LoaderBase)
         Dim Loaders As New List(Of LoaderBase)
         Loaders.Add(New LoaderCombo(Of String)("整合包安装", InstallLoaders) With {.Show = False, .Block = False, .ProgressWeight = InstallLoaders.Sum(Function(l) l.ProgressWeight)})
         Loaders.Add(New LoaderCombo(Of String)("整合包安装", InstallLoaders) With {.Show = False, .Block = False, .ProgressWeight = InstallLoaders.Sum(Function(l) l.ProgressWeight)})
@@ -465,7 +451,6 @@ Public Module ModModpack
         Loaders.Add(New LoaderTask(Of String, String)("最终整理文件",
         Loaders.Add(New LoaderTask(Of String, String)("最终整理文件",
         Sub(Task As LoaderTask(Of String, String))
         Sub(Task As LoaderTask(Of String, String))
             '设置图标
             '设置图标
-            Dim VersionFolder As String = $"{McFolderSelected}versions\{InstanceName}\"
             If Logo IsNot Nothing AndAlso FileUtils.Exists(Logo) Then
             If Logo IsNot Nothing AndAlso FileUtils.Exists(Logo) Then
                 FileUtils.Copy(Logo, VersionFolder & "PCL\Logo.png")
                 FileUtils.Copy(Logo, VersionFolder & "PCL\Logo.png")
                 WriteIni(VersionFolder & "PCL\Setup.ini", "Logo", "PCL\Logo.png")
                 WriteIni(VersionFolder & "PCL\Setup.ini", "Logo", "PCL\Logo.png")
@@ -473,10 +458,10 @@ Public Module ModModpack
                 Logger.Info($"已设置整合包 Logo:{Logo}")
                 Logger.Info($"已设置整合包 Logo:{Logo}")
             End If
             End If
             '删除原始整合包文件
             '删除原始整合包文件
-            For Each Target As String In {VersionFolder & "原始整合包.zip", VersionFolder & "原始整合包.mrpack"}
-                If FileUtils.Exists(Target) Then
-                    Logger.Info($"删除原始整合包文件:{Target}")
-                    FileUtils.Delete(Target)
+            For Each FileToDelete As String In {VersionFolder & "原始整合包.zip", VersionFolder & "原始整合包.mrpack"}
+                If FileUtils.Exists(FileToDelete) Then
+                    Logger.Info($"删除原始整合包文件:{FileToDelete}")
+                    FileUtils.Delete(FileToDelete)
                 End If
                 End If
             Next
             Next
             If FileUtils.Exists(FileAddress) AndAlso PathUtils.GetFileNameWithoutExtension(FileAddress) = "modpack" Then
             If FileUtils.Exists(FileAddress) AndAlso PathUtils.GetFileNameWithoutExtension(FileAddress) = "modpack" Then
@@ -494,7 +479,7 @@ Public Module ModModpack
 
 
         '启动
         '启动
         Dim Loader As New LoaderCombo(Of String)(LoaderName, Loaders) With {.OnStateChanged = AddressOf McInstallState}
         Dim Loader As New LoaderCombo(Of String)(LoaderName, Loaders) With {.OnStateChanged = AddressOf McInstallState}
-        Loader.Start(Request.VersionFolder)
+        Loader.Start(VersionFolder)
         LoaderTaskbarAdd(Loader)
         LoaderTaskbarAdd(Loader)
         FrmMain.BtnExtraDownload.ShowRefresh()
         FrmMain.BtnExtraDownload.ShowRefresh()
         Return Loader
         Return Loader
@@ -515,24 +500,21 @@ Public Module ModModpack
         If Validate.Validate(InstanceName) <> "" Then InstanceName = ""
         If Validate.Validate(InstanceName) <> "" Then InstanceName = ""
         If InstanceName = "" Then InstanceName = MyMsgBoxInput("输入版本名称", "", "", New Collection(Of Validate) From {Validate})
         If InstanceName = "" Then InstanceName = MyMsgBoxInput("输入版本名称", "", "", New Collection(Of Validate) From {Validate})
         If String.IsNullOrEmpty(InstanceName) Then Throw New OperationCanceledException
         If String.IsNullOrEmpty(InstanceName) Then Throw New OperationCanceledException
+        Dim VersionFolder As String = Path.Combine(McFolderSelected, "versions", InstanceName) & "\"
         '解压
         '解压
         Dim InstallTemp As String = RequestTaskTempFolder()
         Dim InstallTemp As String = RequestTaskTempFolder()
         Dim InstallLoaders As New List(Of LoaderBase)
         Dim InstallLoaders As New List(Of LoaderBase)
         InstallLoaders.Add(New LoaderTask(Of String, Integer)("解压整合包文件",
         InstallLoaders.Add(New LoaderTask(Of String, Integer)("解压整合包文件",
         Sub(Task As LoaderTask(Of String, Integer))
         Sub(Task As LoaderTask(Of String, Integer))
             ExtractModpackFiles(InstallTemp, FileAddress, Task.CreateCancellationToken(), Task.CreateSyncProgressProvider(ToPercentage:=0.6))
             ExtractModpackFiles(InstallTemp, FileAddress, Task.CreateCancellationToken(), Task.CreateSyncProgressProvider(ToPercentage:=0.6))
-            CopyOverrideDirectory(
-                InstallTemp & ArchiveBaseFolder & "minecraft",
-                McFolderSelected & "versions\" & InstanceName, Task)
+            CopyOverrideDirectory(InstallTemp & ArchiveBaseFolder & "minecraft", VersionFolder, Task)
         End Sub) With {.ProgressWeight = FileUtils.GetInfo(FileAddress).Length / 1024 / 1024 / 6, .Block = False}) '每 6M 需要 1s
         End Sub) With {.ProgressWeight = FileUtils.GetInfo(FileAddress).Length / 1024 / 1024 / 6, .Block = False}) '每 6M 需要 1s
         '构造游戏本体安装加载器
         '构造游戏本体安装加载器
         If Json("gameVersion") Is Nothing Then Throw New Exception("该 HMCL 整合包未提供游戏版本信息,无法安装!")
         If Json("gameVersion") Is Nothing Then Throw New Exception("该 HMCL 整合包未提供游戏版本信息,无法安装!")
-        Dim Request As New McInstallRequest With {
-            .NewInstanceName = InstanceName,
-            .VersionFolder = $"{McFolderSelected}versions\{InstanceName}\",
-            .MinecraftName = Json("gameVersion").ToString
-        }
-        Dim MergeLoaders As List(Of LoaderBase) = McInstallLoader(Request)
+        Dim Target As New McComponents(Json("gameVersion").ToString, New Dictionary(Of ModLoaders, String))
+        Dim MergeLoaders = McInstallLoadersGet(New McInstallRequest With {
+            .NewInstanceName = InstanceName, .InstanceFolder = VersionFolder,
+            .Components = Target})
         '构造总加载器
         '构造总加载器
         Dim Loaders As New List(Of LoaderBase) From {
         Dim Loaders As New List(Of LoaderBase) From {
             New LoaderCombo(Of String)("整合包安装", InstallLoaders) With {.Show = False, .Block = False, .ProgressWeight = InstallLoaders.Sum(Function(l) l.ProgressWeight)},
             New LoaderCombo(Of String)("整合包安装", InstallLoaders) With {.Show = False, .Block = False, .ProgressWeight = InstallLoaders.Sum(Function(l) l.ProgressWeight)},
@@ -546,7 +528,7 @@ Public Module ModModpack
         End If
         End If
         '启动
         '启动
         Dim Loader As New LoaderCombo(Of String)(LoaderName, Loaders) With {.OnStateChanged = AddressOf McInstallState}
         Dim Loader As New LoaderCombo(Of String)(LoaderName, Loaders) With {.OnStateChanged = AddressOf McInstallState}
-        Loader.Start(Request.VersionFolder)
+        Loader.Start(VersionFolder)
         LoaderTaskbarAdd(Loader)
         LoaderTaskbarAdd(Loader)
         FrmMain.BtnExtraDownload.ShowRefresh()
         FrmMain.BtnExtraDownload.ShowRefresh()
         Return Loader
         Return Loader
@@ -568,16 +550,22 @@ Public Module ModModpack
         If Validate.Validate(InstanceName) <> "" Then InstanceName = ""
         If Validate.Validate(InstanceName) <> "" Then InstanceName = ""
         If InstanceName = "" Then InstanceName = MyMsgBoxInput("输入版本名称", "", "", New Collection(Of Validate) From {Validate})
         If InstanceName = "" Then InstanceName = MyMsgBoxInput("输入版本名称", "", "", New Collection(Of Validate) From {Validate})
         If String.IsNullOrEmpty(InstanceName) Then Throw New OperationCanceledException
         If String.IsNullOrEmpty(InstanceName) Then Throw New OperationCanceledException
+        Dim VersionFolder As String = Path.Combine(McFolderSelected, "versions", InstanceName) & "\"
         '解压
         '解压
         Dim InstallTemp As String = RequestTaskTempFolder()
         Dim InstallTemp As String = RequestTaskTempFolder()
-        Dim SetupFile As String = $"{McFolderSelected}versions\{InstanceName}\PCL\Setup.ini"
+        Dim SetupFile As String = VersionFolder & "PCL\Setup.ini"
         Dim InstallLoaders As New List(Of LoaderBase)
         Dim InstallLoaders As New List(Of LoaderBase)
         InstallLoaders.Add(New LoaderTask(Of String, Integer)("解压整合包文件",
         InstallLoaders.Add(New LoaderTask(Of String, Integer)("解压整合包文件",
         Sub(Task As LoaderTask(Of String, Integer))
         Sub(Task As LoaderTask(Of String, Integer))
             ExtractModpackFiles(InstallTemp, FileAddress, Task.CreateCancellationToken(), Task.CreateSyncProgressProvider(ToPercentage:=0.55))
             ExtractModpackFiles(InstallTemp, FileAddress, Task.CreateCancellationToken(), Task.CreateSyncProgressProvider(ToPercentage:=0.55))
-            CopyOverrideDirectory(
-                InstallTemp & ArchiveBaseFolder & ".minecraft",
-                McFolderSelected & "versions\" & InstanceName, Task)
+            'Prism 优先使用 minecraft/ 而非 .minecraft/(#9272)
+            Dim MmcGameRoot As String
+            If DirectoryUtils.Exists(InstallTemp & ArchiveBaseFolder & ".minecraft") AndAlso Not DirectoryUtils.Exists(InstallTemp & ArchiveBaseFolder & "minecraft") Then
+                MmcGameRoot = InstallTemp & ArchiveBaseFolder & ".minecraft"
+            Else
+                MmcGameRoot = InstallTemp & ArchiveBaseFolder & "minecraft"
+            End If
+            CopyOverrideDirectory(MmcGameRoot, VersionFolder, Task)
             '读取 MMC 设置文件(#2655)
             '读取 MMC 设置文件(#2655)
             Try
             Try
                 Dim MmcSetupFile As String = InstallTemp & ArchiveBaseFolder & "instance.cfg"
                 Dim MmcSetupFile As String = InstallTemp & ArchiveBaseFolder & "instance.cfg"
@@ -611,7 +599,7 @@ Public Module ModModpack
                     If Logo <> "" AndAlso FileUtils.Exists($"{InstallTemp}{ArchiveBaseFolder}{Logo}.png") Then
                     If Logo <> "" AndAlso FileUtils.Exists($"{InstallTemp}{ArchiveBaseFolder}{Logo}.png") Then
                         WriteIni(SetupFile, "LogoCustom", True)
                         WriteIni(SetupFile, "LogoCustom", True)
                         WriteIni(SetupFile, "Logo", "PCL\Logo.png")
                         WriteIni(SetupFile, "Logo", "PCL\Logo.png")
-                        FileUtils.Copy($"{InstallTemp}{ArchiveBaseFolder}{Logo}.png", $"{McFolderSelected}versions\{InstanceName}\PCL\Logo.png")
+                        FileUtils.Copy($"{InstallTemp}{ArchiveBaseFolder}{Logo}.png", VersionFolder & "PCL\Logo.png")
                         Logger.Info($"迁移 MultiMC 版本独立设置:版本图标({Logo}.png)")
                         Logger.Info($"迁移 MultiMC 版本独立设置:版本图标({Logo}.png)")
                     End If
                     End If
                     'JVM 参数
                     'JVM 参数
@@ -633,19 +621,20 @@ Public Module ModModpack
         End Sub) With {.ProgressWeight = FileUtils.GetInfo(FileAddress).Length / 1024 / 1024 / 6, .Block = False}) '每 6M 需要 1s
         End Sub) With {.ProgressWeight = FileUtils.GetInfo(FileAddress).Length / 1024 / 1024 / 6, .Block = False}) '每 6M 需要 1s
         '构造版本安装请求
         '构造版本安装请求
         If PackJson("components") Is Nothing Then Throw New Exception("该 MMC 整合包未提供游戏版本信息,无法安装!")
         If PackJson("components") Is Nothing Then Throw New Exception("该 MMC 整合包未提供游戏版本信息,无法安装!")
-        Dim Request As New McInstallRequest With {.NewInstanceName = InstanceName, .VersionFolder = $"{McFolderSelected}versions\{InstanceName}\"}
+        Dim MinecraftVersion As String = Nothing
+        Dim ModLoaderVersions As New Dictionary(Of ModLoaders, String)
         For Each Component As JObject In PackJson("components")
         For Each Component As JObject In PackJson("components")
             If Not Component.ContainsKey("uid") Then Continue For
             If Not Component.ContainsKey("uid") Then Continue For
             Dim UID As String = Component("uid").ToString
             Dim UID As String = Component("uid").ToString
             Select Case UID
             Select Case UID
                 Case "net.minecraft"
                 Case "net.minecraft"
-                    Request.MinecraftName = Component("version")
+                    MinecraftVersion = Component("version")
                 Case "net.minecraftforge"
                 Case "net.minecraftforge"
-                    Request.ForgeVersion = Component("version")
+                    ModLoaderVersions(ModLoaders.Forge) = Component("version")
                 Case "net.neoforged"
                 Case "net.neoforged"
-                    Request.NeoForgeVersion = Component("version")
+                    ModLoaderVersions(ModLoaders.NeoForge) = Component("version")
                 Case "net.fabricmc.fabric-loader"
                 Case "net.fabricmc.fabric-loader"
-                    Request.FabricVersion = Component("version")
+                    ModLoaderVersions(ModLoaders.Fabric) = Component("version")
                 Case Else
                 Case Else
                     If UID.StartsWithF("org.lwjgl") Then '#8210
                     If UID.StartsWithF("org.lwjgl") Then '#8210
                         Logger.Info($"已跳过 LWJGL 项:{UID}")
                         Logger.Info($"已跳过 LWJGL 项:{UID}")
@@ -655,7 +644,10 @@ Public Module ModModpack
             End Select
             End Select
         Next
         Next
         '构造加载器
         '构造加载器
-        Dim MergeLoaders As List(Of LoaderBase) = McInstallLoader(Request)
+        Dim Target As New McComponents(MinecraftVersion, ModLoaderVersions)
+        Dim MergeLoaders = McInstallLoadersGet(New McInstallRequest With {
+            .NewInstanceName = InstanceName, .InstanceFolder = VersionFolder,
+            .Components = Target})
         '构造总加载器
         '构造总加载器
         Dim Loaders As New List(Of LoaderBase)
         Dim Loaders As New List(Of LoaderBase)
         Loaders.Add(New LoaderCombo(Of String)("整合包安装", InstallLoaders) With {.Show = False, .Block = False, .ProgressWeight = InstallLoaders.Sum(Function(l) l.ProgressWeight)})
         Loaders.Add(New LoaderCombo(Of String)("整合包安装", InstallLoaders) With {.Show = False, .Block = False, .ProgressWeight = InstallLoaders.Sum(Function(l) l.ProgressWeight)})
@@ -670,7 +662,7 @@ Public Module ModModpack
 
 
         '启动
         '启动
         Dim Loader As New LoaderCombo(Of String)(LoaderName, Loaders) With {.OnStateChanged = AddressOf McInstallState}
         Dim Loader As New LoaderCombo(Of String)(LoaderName, Loaders) With {.OnStateChanged = AddressOf McInstallState}
-        Loader.Start(Request.VersionFolder)
+        Loader.Start(VersionFolder)
         LoaderTaskbarAdd(Loader)
         LoaderTaskbarAdd(Loader)
         FrmMain.BtnExtraDownload.ShowRefresh()
         FrmMain.BtnExtraDownload.ShowRefresh()
         Return Loader
         Return Loader
@@ -695,16 +687,15 @@ Public Module ModModpack
             If InstanceName = "" Then InstanceName = MyMsgBoxInput("输入版本名称", "", "", New Collection(Of Validate) From {Validate})
             If InstanceName = "" Then InstanceName = MyMsgBoxInput("输入版本名称", "", "", New Collection(Of Validate) From {Validate})
             If String.IsNullOrEmpty(InstanceName) Then Throw New OperationCanceledException
             If String.IsNullOrEmpty(InstanceName) Then Throw New OperationCanceledException
         End If
         End If
+        Dim VersionFolder As String = Path.Combine(McFolderSelected, "versions", InstanceName) & "\"
         '解压
         '解压
         Dim InstallTemp As String = RequestTaskTempFolder()
         Dim InstallTemp As String = RequestTaskTempFolder()
-        Dim SetupFile As String = $"{McFolderSelected}versions\{InstanceName}\PCL\Setup.ini"
+        Dim SetupFile As String = VersionFolder & "PCL\Setup.ini"
         Dim InstallLoaders As New List(Of LoaderBase)
         Dim InstallLoaders As New List(Of LoaderBase)
         InstallLoaders.Add(New LoaderTask(Of String, Integer)("解压整合包文件",
         InstallLoaders.Add(New LoaderTask(Of String, Integer)("解压整合包文件",
         Sub(Task As LoaderTask(Of String, Integer))
         Sub(Task As LoaderTask(Of String, Integer))
             ExtractModpackFiles(InstallTemp, FileAddress, Task.CreateCancellationToken(), Task.CreateSyncProgressProvider(ToPercentage:=0.6))
             ExtractModpackFiles(InstallTemp, FileAddress, Task.CreateCancellationToken(), Task.CreateSyncProgressProvider(ToPercentage:=0.6))
-            CopyOverrideDirectory(
-                InstallTemp & ArchiveBaseFolder & "overrides",
-                McFolderSelected & "versions\" & InstanceName, Task)
+            CopyOverrideDirectory(InstallTemp & ArchiveBaseFolder & "overrides", VersionFolder, Task)
             'JVM 参数
             'JVM 参数
             If Json("launchInfo") IsNot Nothing Then
             If Json("launchInfo") IsNot Nothing Then
                 Dim LaunchInfo As JObject = Json("launchInfo")
                 Dim LaunchInfo As JObject = Json("launchInfo")
@@ -723,16 +714,26 @@ Public Module ModModpack
             Hint("PCL 不支持安装 Quilt 整合包!", HintType.Red)
             Hint("PCL 不支持安装 Quilt 整合包!", HintType.Red)
             Throw New OperationCanceledException
             Throw New OperationCanceledException
         End If
         End If
-        Dim Request As New McInstallRequest With {
-            .NewInstanceName = InstanceName,
-            .VersionFolder = $"{McFolderSelected}versions\{InstanceName}\",
-            .MinecraftName = Addons("game"),
-            .OptiFineVersion = If(Addons.ContainsKey("optifine"), Addons("optifine"), Nothing),
-            .ForgeVersion = If(Addons.ContainsKey("forge"), Addons("forge"), Nothing),
-            .NeoForgeVersion = If(Addons.ContainsKey("neoforge"), Addons("neoforge"), Nothing),
-            .FabricVersion = If(Addons.ContainsKey("fabric"), Addons("fabric"), Nothing)
-        }
-        Dim MergeLoaders As List(Of LoaderBase) = McInstallLoader(Request)
+        Dim ModLoaderVersions As New Dictionary(Of ModLoaders, String)
+        If Addons.ContainsKey("forge") Then ModLoaderVersions(ModLoaders.Forge) = Addons("forge")
+        If Addons.ContainsKey("neoforge") Then ModLoaderVersions(ModLoaders.NeoForge) = Addons("neoforge")
+        If Addons.ContainsKey("fabric") Then ModLoaderVersions(ModLoaders.Fabric) = Addons("fabric")
+        Dim OptiFineEntry As JObject = Nothing
+        If Addons.ContainsKey("optifine") Then
+            Dim MinecraftVersion = Addons("game")
+            If MinecraftVersion.EndsWithF(".0") Then MinecraftVersion = MinecraftVersion.Substring(0, MinecraftVersion.Length - 2)
+            Dim OptiFineFileSuffix = "_HD_" & Addons("optifine").AfterLast("HD_") & ".jar"
+            '从镜像源拉取 OptiFine 列表
+            If DlOptiFineListBmclapiLoader.State <> LoadState.Finished Then DlOptiFineListBmclapiLoader.WaitForExit()
+            Dim Entry = DlOptiFineListBmclapiLoader.Output.Value.FirstOrDefault(
+                Function(v) v.Inherit = MinecraftVersion AndAlso v.FileName.EndsWithF(OptiFineFileSuffix))
+            If Entry Is Nothing Then Throw New Exception($"OptiFine 列表中没有整合包所需的 OptiFine {Addons("optifine")}(Minecraft {Addons("game")})")
+            OptiFineEntry = JObject.FromObject(Entry)
+        End If
+        Dim Target As New McComponents(Addons("game"), ModLoaderVersions, OptiFineEntry)
+        Dim MergeLoaders = McInstallLoadersGet(New McInstallRequest With {
+            .NewInstanceName = InstanceName, .InstanceFolder = VersionFolder,
+            .Components = Target})
         '构造总加载器
         '构造总加载器
         Dim Loaders As New List(Of LoaderBase)
         Dim Loaders As New List(Of LoaderBase)
         Loaders.Add(New LoaderCombo(Of String)("整合包安装", InstallLoaders) With {.Show = False, .Block = False, .ProgressWeight = InstallLoaders.Sum(Function(l) l.ProgressWeight)})
         Loaders.Add(New LoaderCombo(Of String)("整合包安装", InstallLoaders) With {.Show = False, .Block = False, .ProgressWeight = InstallLoaders.Sum(Function(l) l.ProgressWeight)})
@@ -748,7 +749,7 @@ Public Module ModModpack
         '启动
         '启动
         Dim Loader As New LoaderCombo(Of String)(LoaderName, Loaders) With {.OnStateChanged = AddressOf McInstallState}
         Dim Loader As New LoaderCombo(Of String)(LoaderName, Loaders) With {.OnStateChanged = AddressOf McInstallState}
         'If Archive IsNot Nothing Then Archive.Dispose() '解除占用,以免在加载器中触发 “正由另一进程使用,因此该进程无法访问此文件”
         'If Archive IsNot Nothing Then Archive.Dispose() '解除占用,以免在加载器中触发 “正由另一进程使用,因此该进程无法访问此文件”
-        Loader.Start(Request.VersionFolder)
+        Loader.Start(VersionFolder)
         LoaderTaskbarAdd(Loader)
         LoaderTaskbarAdd(Loader)
         FrmMain.BtnExtraDownload.ShowRefresh()
         FrmMain.BtnExtraDownload.ShowRefresh()
         Return Loader
         Return Loader

+ 3 - 0
Plain Craft Launcher 2/Modules/Minecraft/ModWatcher.vb

@@ -319,6 +319,8 @@ Public Module ModWatcher
                     GetClassName(hwnd, str, str.Capacity)
                     GetClassName(hwnd, str, str.Capacity)
                     Dim ClassName As String = str.ToString
                     Dim ClassName As String = str.ToString
                     If Not (ClassName = "GLFW30" OrElse ClassName = "SDL_app" OrElse ClassName = "LWJGL" OrElse ClassName = "SunAwtFrame") Then Return
                     If Not (ClassName = "GLFW30" OrElse ClassName = "SDL_app" OrElse ClassName = "LWJGL" OrElse ClassName = "SunAwtFrame") Then Return
+                    'SDL 会先创建隐藏窗口,待初始化完成后才显示(#9073)
+                    If ClassName = "SDL_app" AndAlso Not IsWindowVisible(hwnd) Then Return
                     '获取窗口标题名
                     '获取窗口标题名
                     str = New StringBuilder(512)
                     str = New StringBuilder(512)
                     GetWindowText(hwnd, str, str.Capacity)
                     GetWindowText(hwnd, str, str.Capacity)
@@ -347,6 +349,7 @@ Public Module ModWatcher
         Private Declare Function GetWindowText Lib "user32" Alias "GetWindowTextA" (hWnd As Integer, str As StringBuilder, maxCount As Integer) As Integer
         Private Declare Function GetWindowText Lib "user32" Alias "GetWindowTextA" (hWnd As Integer, str As StringBuilder, maxCount As Integer) As Integer
         Private Declare Function SetWindowText Lib "user32" Alias "SetWindowTextA" (hWnd As Integer, str As String) As Boolean
         Private Declare Function SetWindowText Lib "user32" Alias "SetWindowTextA" (hWnd As Integer, str As String) As Boolean
         Private Declare Function ShowWindow Lib "user32" (hWnd As IntPtr, cmdWindow As UInteger) As Boolean
         Private Declare Function ShowWindow Lib "user32" (hWnd As IntPtr, cmdWindow As UInteger) As Boolean
+        Private Declare Function IsWindowVisible Lib "user32" (hWnd As IntPtr) As Boolean
         Private Declare Function GetWindowThreadProcessId Lib "user32" (hWnd As IntPtr, ByRef lpdwProcessId As Integer) As Integer
         Private Declare Function GetWindowThreadProcessId Lib "user32" (hWnd As IntPtr, ByRef lpdwProcessId As Integer) As Integer
 
 
         '崩溃处理
         '崩溃处理

+ 27 - 10
Plain Craft Launcher 2/Modules/ModEvent.vb

@@ -1,6 +1,9 @@
 
 
+
 #Region "附加属性"
 #Region "附加属性"
 
 
+Imports PCL.FormMain
+
 ''' <summary>
 ''' <summary>
 ''' 用于在 XAML 中初始化列表对象。
 ''' 用于在 XAML 中初始化列表对象。
 ''' 附加属性无法在 XAML 中为每个对象初始化独立的列表对象,因此需要一个包装类,然后在 XAML 中显式初始化。
 ''' 附加属性无法在 XAML 中为每个对象初始化独立的列表对象,因此需要一个包装类,然后在 XAML 中显式初始化。
@@ -272,9 +275,23 @@ Public Class CustomEvent
                     Hint(Arg0.Replace("\n", vbCrLf), If(Args.Length = 1, HintType.Blue, EnumUtils.FromString(Of HintType)(Arg1)))
                     Hint(Arg0.Replace("\n", vbCrLf), If(Args.Length = 1, HintType.Blue, EnumUtils.FromString(Of HintType)(Arg1)))
 
 
                 Case EventType.切换页面
                 Case EventType.切换页面
-                    RunInUi(Sub() FrmMain.PageChange(
-                                EnumUtils.FromString(Of FormMain.PageType)(Arg0),
-                                If(Args.Length = 1, FormMain.PageSubType.Default, EnumUtils.FromString(Of FormMain.PageSubType)(Arg1))))
+                    RunInUi(
+                    Sub()
+                        Dim TargetPage = EnumUtils.FromString(Of PageType)(Arg0)
+                        If TargetPage = PageType.InstanceSetup Then
+                            If McInstanceSelected Is Nothing Then
+                                Hint("请先选择一个 Minecraft 版本!", HintType.Red)
+                                Return
+                            End If
+                            If McInstanceLock.IsLocked(McInstanceSelected) Then
+                                Hint("该版本正在被修改,暂时无法进入版本设置!", HintType.Red)
+                                Return
+                            End If
+                            PageInstanceLeft.Instance = McInstanceSelected
+                        End If
+                        FrmMain.PageChange(TargetPage,
+                            If(Args.Length = 1, PageSubType.Default, EnumUtils.FromString(Of PageSubType)(Arg1)))
+                    End Sub)
 
 
                 Case EventType.导入整合包, EventType.安装整合包
                 Case EventType.导入整合包, EventType.安装整合包
                     RunInUi(Sub() ModpackInstall())
                     RunInUi(Sub() ModpackInstall())
@@ -295,7 +312,7 @@ Public Class CustomEvent
                                 Case 2
                                 Case 2
                                     PageOtherTest.StartCustomDownload(Arg0, Arg1)
                                     PageOtherTest.StartCustomDownload(Arg0, Arg1)
                                 Case Else
                                 Case Else
-                                    PageOtherTest.StartCustomDownload(Arg0, Arg(1), Args(2))
+                                    PageOtherTest.StartCustomDownload(Arg0, Args(1), Args(2))
                             End Select
                             End Select
                         Catch
                         Catch
                             PageOtherTest.StartCustomDownload(Arg0, "未知")
                             PageOtherTest.StartCustomDownload(Arg0, "未知")
@@ -304,16 +321,16 @@ Public Class CustomEvent
 
 
                 Case EventType.修改设置, EventType.写入设置
                 Case EventType.修改设置, EventType.写入设置
                     If Args.Length = 1 Then Throw New Exception($"EventType {Type} 需要至少 2 个以 | 分割的参数,例如 UiLauncherTransparent|400")
                     If Args.Length = 1 Then Throw New Exception($"EventType {Type} 需要至少 2 个以 | 分割的参数,例如 UiLauncherTransparent|400")
-                    Settings.SetSafe(Arg0, Arg1, Instance:=McInstanceSelected)
-                    If Args.Length = 2 Then Hint($"已写入设置:{Arg0} → {Arg1}", HintType.Green)
+                    Settings.SetSafe(Arg0, Args(1), Instance:=McInstanceSelected)
+                    If Args.Length = 2 Then Hint($"已写入设置:{Arg0} → {Args(1)}", HintType.Green)
 
 
                 Case EventType.修改变量, EventType.写入变量
                 Case EventType.修改变量, EventType.写入变量
                     If Args.Length = 1 Then Throw New Exception($"EventType {Type} 需要至少 2 个以 | 分割的参数,例如 VariableName|SomeValue")
                     If Args.Length = 1 Then Throw New Exception($"EventType {Type} 需要至少 2 个以 | 分割的参数,例如 VariableName|SomeValue")
                     Try
                     Try
-                        RegistryUtils.Write($"HKEY_CURRENT_USER\Software\{RegFolder}", $"CustomEvent{Arg0}", Arg1)
-                        If Args.Length = 2 Then Hint($"已写入变量:{Arg0} → {Arg1}", HintType.Green)
+                        RegistryUtils.Write($"HKEY_CURRENT_USER\Software\{RegFolder}", $"CustomEvent{Arg0}", Args(1))
+                        If Args.Length = 2 Then Hint($"已写入变量:{Arg0} → {Args(1)}", HintType.Green)
                     Catch ex As Exception
                     Catch ex As Exception
-                        Logger.Warn(ex, $"写入变量失败:{Arg0} → {Arg1}", LogBehavior.Toast)
+                        Logger.Warn(ex, $"写入变量失败:{Arg0} → {Args(1)}", LogBehavior.Toast)
                     End Try
                     End Try
 
 
                 Case EventType.加入房间
                 Case EventType.加入房间
@@ -353,7 +370,7 @@ Public Class CustomEvent
             Dim RawFileName As String
             Dim RawFileName As String
             Try
             Try
                 RawFileName = PathUtils.GetLastPart(RelativeUrl)
                 RawFileName = PathUtils.GetLastPart(RelativeUrl)
-                If Not RawFileName.EndsWithF(".json", True) Then Throw New Exception("未指向 .json 后缀的文件")
+                If Not PathUtils.GetExtension(RawFileName) = "json" Then Throw New Exception("未指向 .json 后缀的文件")
             Catch ex As Exception
             Catch ex As Exception
                 Throw New Exception("联网帮助页面须指向一个帮助 JSON 文件,并在同路径下包含相应 XAML 文件!" & vbCrLf &
                 Throw New Exception("联网帮助页面须指向一个帮助 JSON 文件,并在同路径下包含相应 XAML 文件!" & vbCrLf &
                                     "例如:" & vbCrLf &
                                     "例如:" & vbCrLf &

+ 8 - 7
Plain Craft Launcher 2/Modules/ModMain.vb

@@ -414,6 +414,7 @@ EndHint:
     Public FrmInstanceModDisabled As PageInstanceModDisabled
     Public FrmInstanceModDisabled As PageInstanceModDisabled
     Public FrmInstanceSetup As PageInstanceSetup
     Public FrmInstanceSetup As PageInstanceSetup
     Public FrmInstanceExport As PageInstanceExport
     Public FrmInstanceExport As PageInstanceExport
+    Public FrmDownloadModify As PageDownloadModify
 
 
     '资源信息分页声明
     '资源信息分页声明
     Public FrmDownloadResourceDetail As PageDownloadResourceDetail
     Public FrmDownloadResourceDetail As PageDownloadResourceDetail
@@ -510,7 +511,7 @@ EndHint:
                 EventData = If(JsonData("EventData"), "")
                 EventData = If(JsonData("EventData"), "")
                 IsEvent = True
                 IsEvent = True
             Else
             Else
-                Dim XamlAddress As String = FilePath.Lower.Replace(".json", ".xaml")
+                Dim XamlAddress As String = Path.ChangeExtension(FilePath, ".xaml")
                 If FileUtils.Exists(XamlAddress) Then
                 If FileUtils.Exists(XamlAddress) Then
                     XamlContent = FileUtils.ReadAsString(XamlAddress)
                     XamlContent = FileUtils.ReadAsString(XamlAddress)
                     IsEvent = False
                     IsEvent = False
@@ -596,9 +597,9 @@ EndHint:
                     '读取自带文件
                     '读取自带文件
                     For Each File In DirectoryUtils.EnumerateFiles(PathTemp & "Help", True)
                     For Each File In DirectoryUtils.EnumerateFiles(PathTemp & "Help", True)
                         '跳过非 json 文件与以 . 开头的文件夹
                         '跳过非 json 文件与以 . 开头的文件夹
-                        If PathUtils.RemoveLastPart(File).Replace(PathTemp & "Help", "").Contains("\.") OrElse PathUtils.GetExtension(File) <> "json" Then Continue For
+                        If PathUtils.RemoveLastPart(File).Substring((PathTemp & "Help").Length).Contains("\.") OrElse PathUtils.GetExtension(File) <> "json" Then Continue For
                         '检查忽略列表
                         '检查忽略列表
-                        Dim RealPath As String = File.Replace(PathTemp & "Help\", "")
+                        Dim RealPath As String = File.Substring((PathTemp & "Help\").Length)
                         For Each Ignore In IgnoreList
                         For Each Ignore In IgnoreList
                             If RealPath.RegexCheck(Ignore) Then
                             If RealPath.RegexCheck(Ignore) Then
                                 Logger.Trace($"已忽略 {RealPath}:{Ignore}")
                                 Logger.Trace($"已忽略 {RealPath}:{Ignore}")
@@ -857,10 +858,10 @@ NextFile:
             Else
             Else
                 Text = Text.Replace("{version}", Replacer(McInstanceSelected.Version.VanillaName))
                 Text = Text.Replace("{version}", Replacer(McInstanceSelected.Version.VanillaName))
             End If
             End If
-            Text = Text.Replace("{forge}", Replacer(If(McInstanceSelected.Version.HasForge, McInstanceSelected.Version.Forge, "")))
-            Text = Text.Replace("{fabric}", Replacer(If(McInstanceSelected.Version.HasFabric, McInstanceSelected.Version.Fabric, "")))
-            Text = Text.Replace("{neoforge}", Replacer(If(McInstanceSelected.Version.HasNeoForge, McInstanceSelected.Version.NeoForge, "")))
-            Text = Text.Replace("{liteloader}", Replacer(If(McInstanceSelected.Version.HasLiteLoader, "true", "")))
+            Text = Text.Replace("{forge}", Replacer(If(McInstanceSelected.Version.ModLoaders.HasFlagF(ModLoaders.Forge), McInstanceSelected.Version.ModLoaderVersions(ModLoaders.Forge), "")))
+            Text = Text.Replace("{fabric}", Replacer(If(McInstanceSelected.Version.ModLoaders.HasFlagF(ModLoaders.Fabric), McInstanceSelected.Version.ModLoaderVersions(ModLoaders.Fabric), "")))
+            Text = Text.Replace("{neoforge}", Replacer(If(McInstanceSelected.Version.ModLoaders.HasFlagF(ModLoaders.NeoForge), McInstanceSelected.Version.ModLoaderVersions(ModLoaders.NeoForge), "")))
+            Text = Text.Replace("{liteloader}", Replacer(If(McInstanceSelected.Version.ModLoaders.HasFlagF(ModLoaders.LiteLoader), "true", "")))
         Else
         Else
             Text = Text.Replace("{version_path}", Replacer(Nothing)) : Text = Text.Replace("{verpath}", Replacer(Nothing))
             Text = Text.Replace("{version_path}", Replacer(Nothing)) : Text = Text.Replace("{verpath}", Replacer(Nothing))
             Text = Text.Replace("{version_indie}", Replacer(Nothing)) : Text = Text.Replace("{verindie}", Replacer(Nothing))
             Text = Text.Replace("{version_indie}", Replacer(Nothing)) : Text = Text.Replace("{verindie}", Replacer(Nothing))

+ 1 - 1
Plain Craft Launcher 2/Modules/ModMusic.vb

@@ -320,7 +320,7 @@ Public Module ModMusic
                 Hint("由于音频设备变更,音乐播放功能在重启 PCL 后才能恢复!", HintType.Red)
                 Hint("由于音频设备变更,音乐播放功能在重启 PCL 后才能恢复!", HintType.Red)
                 Thread.Sleep(1000000000)
                 Thread.Sleep(1000000000)
             End If
             End If
-            If Not (MusicCurrent.EndsWithF(".wav", True) OrElse MusicCurrent.EndsWithF(".mp3", True) OrElse MusicCurrent.EndsWithF(".flac", True)) OrElse
+            If Not ({"wav", "mp3", "flac"}.Contains(PathUtils.GetExtension(MusicCurrent))) OrElse
                 ex.Message.Contains("0xC00D36C4") Then '#5096:不支持给定的 URL 的字节流类型。 (异常来自 HRESULT:0xC00D36C4)
                 ex.Message.Contains("0xC00D36C4") Then '#5096:不支持给定的 URL 的字节流类型。 (异常来自 HRESULT:0xC00D36C4)
                 Hint("播放音乐失败(" & PathUtils.GetLastPart(MusicCurrent) & "):PCL 可能不支持此音乐格式,请将格式转换为 .wav、.mp3 或 .flac 后再试", HintType.Red)
                 Hint("播放音乐失败(" & PathUtils.GetLastPart(MusicCurrent) & "):PCL 可能不支持此音乐格式,请将格式转换为 .wav、.mp3 或 .flac 后再试", HintType.Red)
             Else
             Else

+ 4 - 4
Plain Craft Launcher 2/Modules/Resource/LocalResourceFile.vb

@@ -25,7 +25,7 @@ Public Class LocalResourceFile
         End Get
         End Get
     End Property
     End Property
     ''' <summary>
     ''' <summary>
-    ''' 假若该资源未被禁用时,文件的文件名。
+    ''' 假若该资源未被禁用时,文件的文件名。包含 .jar/.litemod 等后缀。
     ''' </summary>
     ''' </summary>
     Public ReadOnly Property EnabledName As String
     Public ReadOnly Property EnabledName As String
         Get
         Get
@@ -57,7 +57,7 @@ Public Class LocalResourceFile
     Public Property Display As String
     Public Property Display As String
         Get
         Get
             If _Display Is Nothing Then _Display = ProjectVersion?.Display
             If _Display Is Nothing Then _Display = ProjectVersion?.Display
-            If _Display Is Nothing Then _Display = PathUtils.GetFileNameWithoutExtension(File.FullName)
+            If _Display Is Nothing Then _Display = PathUtils.GetFileNameWithoutExtension(EnabledName)
             Return _Display
             Return _Display
         End Get
         End Get
         Set(value As String)
         Set(value As String)
@@ -238,7 +238,7 @@ Public Class LocalResourceFile
     Private _CurseForgeHash As UInteger?
     Private _CurseForgeHash As UInteger?
 
 
     ''' <summary>
     ''' <summary>
-    ''' 获取用于 Modrinth 信息获取的 Hash 值(SHA1)。
+    ''' 获取用于 Modrinth 信息获取的 Hash 值(SHA1)。固定为小写。
     ''' </summary>
     ''' </summary>
     Public ReadOnly Property ModrinthHash As String
     Public ReadOnly Property ModrinthHash As String
         Get
         Get
@@ -269,7 +269,7 @@ Public Class LocalResourceFile
     End Function
     End Function
     Public Overrides Function Equals(obj As Object) As Boolean
     Public Overrides Function Equals(obj As Object) As Boolean
         Dim Target = TryCast(obj, LocalResourceFile)
         Dim Target = TryCast(obj, LocalResourceFile)
-        Return Target IsNot Nothing AndAlso File.FullName = Target.File.FullName
+        Return Target IsNot Nothing AndAlso PathUtils.IsSame(File.FullName, Target.File.FullName)
     End Function
     End Function
 
 
 #End Region
 #End Region

+ 6 - 10
Plain Craft Launcher 2/Modules/Resource/LocalResourceLoaders.vb

@@ -24,11 +24,10 @@ Public Module LocalResourceLoaders
             '获取 Mod 文件夹下的可用文件列表
             '获取 Mod 文件夹下的可用文件列表
             Dim ModFileList As New List(Of String)
             Dim ModFileList As New List(Of String)
             If DirectoryUtils.Exists(Loader.Input) Then
             If DirectoryUtils.Exists(Loader.Input) Then
-                Dim RawName As String = Loader.Input.Lower
                 For Each File In DirectoryUtils.EnumerateFiles(Loader.Input, True)
                 For Each File In DirectoryUtils.EnumerateFiles(Loader.Input, True)
-                    If File.BeforeLast("\").Lower & "\" <> RawName Then
+                    If Not PathUtils.IsSame(PathUtils.RemoveLastPart(File), Loader.Input) Then
                         '仅当 Forge 1.13- 且文件夹名与版本号相同时,才加载该子文件夹下的 Mod
                         '仅当 Forge 1.13- 且文件夹名与版本号相同时,才加载该子文件夹下的 Mod
-                        If Not (PageInstanceLeft.Instance IsNot Nothing AndAlso PageInstanceLeft.Instance.Version.HasForge AndAlso
+                        If Not (PageInstanceLeft.Instance IsNot Nothing AndAlso PageInstanceLeft.Instance.Version.ModLoaders.HasFlagF(ModLoaders.Forge) AndAlso
                                 PageInstanceLeft.Instance.Version.Vanilla.Major < 13 AndAlso
                                 PageInstanceLeft.Instance.Version.Vanilla.Major < 13 AndAlso
                                 PathUtils.GetLastPart(PathUtils.RemoveLastPart(File)) = $"1.{PageInstanceLeft.Instance.Version.Vanilla.Major}.{PageInstanceLeft.Instance.Version.Vanilla.Build}") Then
                                 PathUtils.GetLastPart(PathUtils.RemoveLastPart(File)) = $"1.{PageInstanceLeft.Instance.Version.Vanilla.Major}.{PageInstanceLeft.Instance.Version.Vanilla.Build}") Then
                             Continue For
                             Continue For
@@ -36,12 +35,9 @@ Public Module LocalResourceLoaders
                     End If
                     End If
                     Static IsModFile As Func(Of String, Boolean) =
                     Static IsModFile As Func(Of String, Boolean) =
                     Function(FullName As String)
                     Function(FullName As String)
-                        If FullName Is Nothing OrElse Not FullName.Contains(".") Then Return False
-                        FullName = FullName.Lower
-                        If FullName.EndsWithF(".jar", True) OrElse FullName.EndsWithF(".zip", True) OrElse FullName.EndsWithF(".litemod", True) OrElse
-                            FullName.EndsWithF(".jar.disabled", True) OrElse FullName.EndsWithF(".zip.disabled", True) OrElse FullName.EndsWithF(".litemod.disabled", True) OrElse
-                            FullName.EndsWithF(".jar.old", True) OrElse FullName.EndsWithF(".zip.old", True) OrElse FullName.EndsWithF(".litemod.old", True) Then Return True
-                        Return False
+                        If FullName Is Nothing Then Return False
+                        If {"disabled", "old"}.Contains(PathUtils.GetExtension(FullName)) Then FullName = PathUtils.GetFileNameWithoutExtension(FullName)
+                        Return {"jar", "zip", "litemod"}.Contains(PathUtils.GetExtension(FullName))
                     End Function
                     End Function
                     If IsModFile(File) Then ModFileList.Add(File)
                     If IsModFile(File) Then ModFileList.Add(File)
                 Next
                 Next
@@ -271,7 +267,7 @@ Public Module LocalResourceLoaders
                             Dim EntryVersion As String = Entry("gameVersion")
                             Dim EntryVersion As String = Entry("gameVersion")
                             Return EntryVersion IsNot Nothing AndAlso EntryVersion = VanillaVersion
                             Return EntryVersion IsNot Nothing AndAlso EntryVersion = VanillaVersion
                         End Function).
                         End Function).
-                        MaxByAll(Function(Entry) Entry("gameVersion").ToString, New VersionComparer) '只保留最新的 MC 版本
+                        MaxByAll(Function(Entry) Entry("gameVersion").ToString, Comparer(Of String).Create(Function(x, y) McUtils.CompareVersion(x, y))) '只保留最新的 MC 版本
                         For Each Entry In LatestFiles
                         For Each Entry In LatestFiles
                             Dim FileId = Entry("fileId").ToObject(Of Integer)
                             Dim FileId = Entry("fileId").ToObject(Of Integer)
                             If Not UpdateFileIds.ContainsKey(FileId) Then UpdateFileIds(FileId) = New List(Of LocalResourceFile)
                             If Not UpdateFileIds.ContainsKey(FileId) Then UpdateFileIds(FileId) = New List(Of LocalResourceFile)

+ 2 - 2
Plain Craft Launcher 2/Modules/Resource/ResourceProject.vb

@@ -184,11 +184,11 @@ Public Class ResourceProject
                     If Not NewFile.DownloadAvailable Then Continue For
                     If Not NewFile.DownloadAvailable Then Continue For
                     ModLoaders = ModLoaders Or NewFile.ModLoaders
                     ModLoaders = ModLoaders Or NewFile.ModLoaders
                     Dim GameVersions = File("gameVersions").ToObject(Of List(Of String))
                     Dim GameVersions = File("gameVersions").ToObject(Of List(Of String))
-                    If Not GameVersions.Any(Function(v) McVersion.IsFormatFit(v)) Then Continue For
+                    If Not GameVersions.Any(Function(v) McVersion.IsStandardFormat(v)) Then Continue For
                     Files.Add(New KeyValuePair(Of Integer, List(Of String))(File("id"), GameVersions))
                     Files.Add(New KeyValuePair(Of Integer, List(Of String))(File("id"), GameVersions))
                 Next
                 Next
                 For Each File In If(Data("latestFilesIndexes"), New JArray) '这俩玩意儿包含的文件不一样,见 #3599
                 For Each File In If(Data("latestFilesIndexes"), New JArray) '这俩玩意儿包含的文件不一样,见 #3599
-                    If Not McVersion.IsFormatFit(File("gameVersion")) Then Continue For
+                    If Not McVersion.IsStandardFormat(File("gameVersion")) Then Continue For
                     Files.Add(New KeyValuePair(Of Integer, List(Of String))(File("fileId"), {File("gameVersion").ToString}.ToList))
                     Files.Add(New KeyValuePair(Of Integer, List(Of String))(File("fileId"), {File("gameVersion").ToString}.ToList))
                 Next
                 Next
                 CurseForgeFileIds = Files.Select(Function(f) f.Key).Distinct.ToList
                 CurseForgeFileIds = Files.Select(Function(f) f.Key).Distinct.ToList

+ 13 - 15
Plain Craft Launcher 2/Modules/Resource/ResourceSearcher.vb

@@ -99,12 +99,12 @@ Public Class ResourceSearcher
             Dim Address As String = $"https://api.modrinth.com/v2/search?limit={RESULT_PAGE_SIZE}&index=relevance"
             Dim Address As String = $"https://api.modrinth.com/v2/search?limit={RESULT_PAGE_SIZE}&index=relevance"
             If Not String.IsNullOrEmpty(SearchText) Then Address += "&query=" & StringUtils.UrlEscape(SearchText)
             If Not String.IsNullOrEmpty(SearchText) Then Address += "&query=" & StringUtils.UrlEscape(SearchText)
             If Storage.ModrinthOffset > 0 Then Address += "&offset=" & Storage.ModrinthOffset
             If Storage.ModrinthOffset > 0 Then Address += "&offset=" & Storage.ModrinthOffset
-            'facets=[["categories:'game-mechanics'"],["categories:'forge',categories:'fabric'"],["versions:1.19.3"],["project_type:mod"]]
+            'facets=[["categories:'game-mechanics'"],["categories:forge","categories:fabric"],["versions:1.19.3"],["project_type:mod"]]
             Dim Facets As New List(Of String)
             Dim Facets As New List(Of String)
             Facets.Add($"[""project_type:{Type.ToString.Lower}""]")
             Facets.Add($"[""project_type:{Type.ToString.Lower}""]")
             If Not String.IsNullOrEmpty(Tag) Then Facets.Add($"[""categories:'{Tag.AfterLast("/")}'""]")
             If Not String.IsNullOrEmpty(Tag) Then Facets.Add($"[""categories:'{Tag.AfterLast("/")}'""]")
             If ModLoaders <> ModLoaders.None AndAlso Not IgnoreModLoaderFilter Then
             If ModLoaders <> ModLoaders.None AndAlso Not IgnoreModLoaderFilter Then
-                Facets.Add($"[""categories:'{ModLoaders.Flags.Select(Function(f) f.ToString.Lower).Join("',categories:'")}'""]")
+                Facets.Add($"[""categories:{ModLoaders.Flags.Select(Function(f) f.ToString.Lower).Join(""",""categories:")}""]")
             End If
             End If
             If Not String.IsNullOrEmpty(GameVersion) Then Facets.Add($"[""versions:'{GameVersion}'""]")
             If Not String.IsNullOrEmpty(GameVersion) Then Facets.Add($"[""versions:'{GameVersion}'""]")
             Address += "&facets=[" & String.Join(",", Facets) & "]"
             Address += "&facets=[" & String.Join(",", Facets) & "]"
@@ -378,21 +378,19 @@ NextPage:
                             If Request.Type <> ResourceTypes.Any AndAlso Not Project.Types.HasFlagF(Request.Type) Then Continue For
                             If Request.Type <> ResourceTypes.Any AndAlso Not Project.Types.HasFlagF(Request.Type) Then Continue For
                             If Not String.IsNullOrEmpty(Request.Tag) AndAlso
                             If Not String.IsNullOrEmpty(Request.Tag) AndAlso
                                 Not JsonEntry("categories").Any(Function(c) c.ToString = Request.Tag.AfterLast("/")) Then Continue For 'Project.Tags 已经转换成中文了,只能从 json 判
                                 Not JsonEntry("categories").Any(Function(c) c.ToString = Request.Tag.AfterLast("/")) Then Continue For 'Project.Tags 已经转换成中文了,只能从 json 判
+                            If Request.ModLoaders <> ModLoaders.None AndAlso Not IgnoreModLoaderFilter AndAlso
+                                Not Project.ModLoaders.Flags.Intersect(Request.ModLoaders.Flags).Any() Then Continue For
+                            If Not String.IsNullOrEmpty(Request.GameVersion) AndAlso
+                                Not Project.UnsafeGameVersions.Any(Function(d) d = Request.GameVersion) Then Continue For
                             ProjectList.Add(Project)
                             ProjectList.Add(Project)
                         Next
                         Next
-                        Dim Facets As New List(Of String)
-                        If Request.ModLoaders <> ModLoaders.None AndAlso Not IgnoreModLoaderFilter Then
-                            Facets.Add($"[""categories:'{Request.ModLoaders.Flags.Select(Function(f) f.ToString.Lower).Join("'"",""categories:'")}'""]")
-                        End If
-                        If Not String.IsNullOrEmpty(Request.GameVersion) Then Facets.Add($"[""versions:'{Request.GameVersion}'""]")
-                        If ProjectList.Any AndAlso Facets.Any Then
-                            Facets.Add("[""project_id:" & ProjectList.Select(Function(p) p.Id).Join(""",""project_id:") & """]")
-                            Dim FilterUrl As String = $"https://api.modrinth.com/v2/search?limit={Math.Min(ProjectList.Count, 100)}&facets=[" & String.Join(",", Facets) & "]"
-                            Logger.Info($"开始 Modrinth 直接获取筛选:{FilterUrl}")
-                            Dim MatchedIds As New List(Of String)
-                            For Each JsonEntry As JObject In DlModRequest(FilterUrl)("hits")
-                                MatchedIds.Add(JsonEntry("project_id").ToString)
-                            Next
+                        If ProjectList.Any AndAlso Request.ModLoaders <> ModLoaders.None AndAlso Not IgnoreModLoaderFilter AndAlso
+                                Not String.IsNullOrEmpty(Request.GameVersion) Then
+                            Dim ProjectIds = ProjectList.Select(Function(p) p.Id).Join(""",""project_id:")
+                            Dim Loaders = Request.ModLoaders.Flags.Select(Function(f) f.ToString.Lower).Join(""",""categories:")
+                            Dim FilterUrl = $"https://api.modrinth.com/v2/search?limit={ProjectList.Count}&facets=" &
+                                $"[[""project_id:{ProjectIds}""],[""categories:{Loaders}""],[""versions:{Request.GameVersion}""]]"
+                            Dim MatchedIds = DlModRequest(FilterUrl)("hits").Select(Function(h) h("project_id").ToString).ToList
                             ProjectList.KeepIf(Function(p) MatchedIds.Contains(p.Id))
                             ProjectList.KeepIf(Function(p) MatchedIds.Contains(p.Id))
                         End If
                         End If
                         '更新结果
                         '更新结果

+ 9 - 8
Plain Craft Launcher 2/Modules/Resource/ResourceVersion.vb

@@ -90,7 +90,7 @@ Public Class ResourceVersion
     ''' </summary>
     ''' </summary>
     Public DownloadUrls As List(Of String)
     Public DownloadUrls As List(Of String)
     ''' <summary>
     ''' <summary>
-    ''' 对应文件的 SHA1 或 MD5。
+    ''' 对应文件的 SHA1 或 MD5。固定为小写。
     ''' </summary>
     ''' </summary>
     Public Hash As String = Nothing
     Public Hash As String = Nothing
     ''' <summary>
     ''' <summary>
@@ -127,8 +127,9 @@ Public Class ResourceVersion
                 .DownloadCount = Data("downloadCount")
                 .DownloadCount = Data("downloadCount")
                 .FileName = Data("fileName")
                 .FileName = Data("fileName")
                 .Size = Data("fileLength")
                 .Size = Data("fileLength")
-                .Hash = CType(Data("hashes"), JArray).ToList.FirstOrDefault(Function(s) s("algo").ToObject(Of Integer) = 1)?("value")
-                If .Hash Is Nothing Then .Hash = CType(Data("hashes"), JArray).ToList.FirstOrDefault(Function(s) s("algo").ToObject(Of Integer) = 2)?("value")
+                If .Size = 0 Then .Size = -1 'CurseForge 可能错误地返回 0 尺寸(#9104)
+                .Hash = CType(Data("hashes"), JArray).ToList.FirstOrDefault(Function(s) s("algo").ToObject(Of Integer) = 1)?("value")?.ToString().Lower()
+                If .Hash Is Nothing Then .Hash = CType(Data("hashes"), JArray).ToList.FirstOrDefault(Function(s) s("algo").ToObject(Of Integer) = 2)?("value")?.ToString().Lower()
                 'DownloadAddress
                 'DownloadAddress
                 Dim Url = Data("downloadUrl").ToString
                 Dim Url = Data("downloadUrl").ToString
                 If Url = "" Then Url = $"https://edge.forgecdn.net/files/{CInt(.Id.ToString.Substring(0, 4))}/{CInt(.Id.ToString.Substring(4))}/{ .FileName}"
                 If Url = "" Then Url = $"https://edge.forgecdn.net/files/{CInt(.Id.ToString.Substring(0, 4))}/{CInt(.Id.ToString.Substring(4))}/{ .FileName}"
@@ -145,13 +146,13 @@ Public Class ResourceVersion
                 'GameVersions
                 'GameVersions
                 Dim RawVersions As List(Of String) = Data("gameVersions").Select(Function(t) t.ToString.Trim.Lower).ToList
                 Dim RawVersions As List(Of String) = Data("gameVersions").Select(Function(t) t.ToString.Trim.Lower).ToList
                 .GameVersions = RawVersions.
                 .GameVersions = RawVersions.
-                        Where(Function(v) McVersion.IsFormatFit(v)).
+                        Where(Function(v) McVersion.IsStandardFormat(v)).
                         Select(Function(v) v.Replace("-snapshot", " 预览版")).
                         Select(Function(v) v.Replace("-snapshot", " 预览版")).
                         Distinct.ToList
                         Distinct.ToList
                 If .GameVersions.IsSingle Then
                 If .GameVersions.IsSingle Then
                     .GameVersions = .GameVersions.ToList
                     .GameVersions = .GameVersions.ToList
                 ElseIf .GameVersions.Count > 1 Then
                 ElseIf .GameVersions.Count > 1 Then
-                    .GameVersions = .GameVersions.SortByComparison(AddressOf CompareVersionGE).ToList
+                    .GameVersions = .GameVersions.SortByComparison(AddressOf McUtils.CompareVersionGE).ToList
                     If .ResourceType = ResourceTypes.ModPack Then .GameVersions = { .GameVersions(0)}.ToList '整合包理应只 “支持” 一个版本
                     If .ResourceType = ResourceTypes.ModPack Then .GameVersions = { .GameVersions(0)}.ToList '整合包理应只 “支持” 一个版本
                 Else
                 Else
                     .GameVersions = New List(Of String) From {"未知版本"}
                     .GameVersions = New List(Of String) From {"未知版本"}
@@ -176,7 +177,7 @@ Public Class ResourceVersion
                     .FileName = File("filename")
                     .FileName = File("filename")
                     .DownloadUrls = New List(Of String) From {File("url"), DlSourceModGet(File("url"))}.Distinct.ToList '同时添加了镜像源
                     .DownloadUrls = New List(Of String) From {File("url"), DlSourceModGet(File("url"))}.Distinct.ToList '同时添加了镜像源
                     .Size = File("size")
                     .Size = File("size")
-                    .Hash = File("hashes")("sha1")
+                    .Hash = File("hashes")("sha1")?.ToString.Lower()
                 End If
                 End If
                 '类别与 Loaders
                 '类别与 Loaders
                 '结果可能混杂着 Mod、数据包和服务端插件
                 '结果可能混杂着 Mod、数据包和服务端插件
@@ -208,7 +209,7 @@ Public Class ResourceVersion
                 If .GameVersions.IsSingle Then
                 If .GameVersions.IsSingle Then
                     '无需处理
                     '无需处理
                 ElseIf .GameVersions.Count > 1 Then
                 ElseIf .GameVersions.Count > 1 Then
-                    .GameVersions = .GameVersions.SortByComparison(AddressOf CompareVersionGE).ToList
+                    .GameVersions = .GameVersions.SortByComparison(AddressOf McUtils.CompareVersionGE).ToList
                     If .ResourceType = ResourceTypes.ModPack Then .GameVersions = New List(Of String) From { .GameVersions(0)} '整合包理应只 “支持” 一个版本
                     If .ResourceType = ResourceTypes.ModPack Then .GameVersions = New List(Of String) From { .GameVersions(0)} '整合包理应只 “支持” 一个版本
                 ElseIf RawVersions.Any(Function(v) v.RegexCheck("[0-9]{2}w[0-9]{2}[a-z]")) Then
                 ElseIf RawVersions.Any(Function(v) v.RegexCheck("[0-9]{2}w[0-9]{2}[a-z]")) Then
                     .GameVersions = RawVersions.Where(Function(v) v.RegexCheck("[0-9]{2}w[0-9]{2}[a-z]")).ToList
                     .GameVersions = RawVersions.Where(Function(v) v.RegexCheck("[0-9]{2}w[0-9]{2}[a-z]")).ToList
@@ -355,7 +356,7 @@ Public Class ResourceVersion
                 .Info =
                 .Info =
                 Iterator Function()
                 Iterator Function()
                     If ModLoaders <> ModLoaders.None Then Yield ModLoaders.Flags.Join("、"c)
                     If ModLoaders <> ModLoaders.None Then Yield ModLoaders.Flags.Join("、"c)
-                    If .Title <> FileName.BeforeLast(".") Then Yield FileName.BeforeLast(".")
+                    If .Title <> PathUtils.GetFileNameWithoutExtension(FileName) Then Yield PathUtils.GetFileNameWithoutExtension(FileName)
                     If Dependencies.Any Then Yield $"{Dependencies.Count} 项前置"
                     If Dependencies.Any Then Yield $"{Dependencies.Count} 项前置"
                     If GameVersions.All(Function(v) Not v.Contains(".") OrElse {"w", "snapshot", "rc", "pre", "experimental", "-"}.Any(Function(s) v.ContainsIgnoreCase(s))) Then Yield $"游戏版本 {GameVersions.Join("、"c)}"
                     If GameVersions.All(Function(v) Not v.Contains(".") OrElse {"w", "snapshot", "rc", "pre", "experimental", "-"}.Any(Function(s) v.ContainsIgnoreCase(s))) Then Yield $"游戏版本 {GameVersions.Join("、"c)}"
                     If DownloadCount > 0 Then Yield $"下载 {If(DownloadCount > 100000, Math.Round(DownloadCount / 10000) & " 万", DownloadCount.ToString() + " ")}次" 'CurseForge 的下载次数经常错误地返回 0
                     If DownloadCount > 0 Then Yield $"下载 {If(DownloadCount > 100000, Math.Round(DownloadCount / 10000) & " 万", DownloadCount.ToString() + " ")}次" 'CurseForge 的下载次数经常错误地返回 0

+ 2 - 2
Plain Craft Launcher 2/My Project/AssemblyInfo.vb

@@ -51,6 +51,6 @@ Imports System.Runtime.InteropServices
 ' 可以指定所有值,也可以使用以下所示的 "*" 预置版本号和修订号
 ' 可以指定所有值,也可以使用以下所示的 "*" 预置版本号和修订号
 ' 方法是按如下所示使用“*”
 ' 方法是按如下所示使用“*”
 
 
-<Assembly: AssemblyVersion("2.13.1.1")>
-<Assembly: AssemblyFileVersion("2.13.1.1")>
+<Assembly: AssemblyVersion("2.14.0.0")>
+<Assembly: AssemblyFileVersion("2.14.0.0")>
 <Assembly: NeutralResourcesLanguage("")>
 <Assembly: NeutralResourcesLanguage("")>

+ 403 - 395
Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb

@@ -4,12 +4,10 @@ Public Module ModDownloadLib
 
 
     ''' <summary>
     ''' <summary>
     ''' 获取下载某个 Minecraft 版本的加载器列表。
     ''' 获取下载某个 Minecraft 版本的加载器列表。
-    ''' 它必须安装到 McFolderSelected,但是可以自定义版本名(不过自定义的版本名不会修改 Json 中的 id 项)。
     ''' </summary>
     ''' </summary>
-    Private Function McDownloadClientLoader(Id As String, Optional JsonUrl As String = Nothing, Optional InstanceName As String = Nothing) As List(Of LoaderBase)
-        InstanceName = If(InstanceName, Id)
-        Dim VersionFolder As String = McFolderSelected & "versions\" & InstanceName & "\"
-
+    ''' <param name="VersionFolder">版本文件输出目录。</param>
+    Private Function McDownloadClientLoader(Id As String, JsonUrl As String, VersionFolder As String) As List(Of LoaderBase)
+        Dim InstanceName = PathUtils.GetLastPart(VersionFolder)
         Dim Loaders As New List(Of LoaderBase)
         Dim Loaders As New List(Of LoaderBase)
         Dim NewInstance As McInstance = Nothing
         Dim NewInstance As McInstance = Nothing
 
 
@@ -54,7 +52,7 @@ Public Module ModDownloadLib
             '顺手添加 Json 项目
             '顺手添加 Json 项目
             Try
             Try
                 Dim InstanceJson As JObject = FileUtils.ReadAsJson(VersionFolder & InstanceName & ".json")
                 Dim InstanceJson As JObject = FileUtils.ReadAsJson(VersionFolder & InstanceName & ".json")
-                InstanceJson.Add("clientVersion", Id)
+                InstanceJson("clientVersion") = Id
                 FileUtils.Write($"{VersionFolder}{InstanceName}.json", InstanceJson.ToString)
                 FileUtils.Write($"{VersionFolder}{InstanceName}.json", InstanceJson.ToString)
             Catch ex As Exception
             Catch ex As Exception
                 Throw New Exception("添加客户端版本失败", ex)
                 Throw New Exception("添加客户端版本失败", ex)
@@ -242,11 +240,11 @@ pause"
                 New NetFile(DlSourceLauncherOrMetaGet(JsonUrl), VersionFolder & Id & ".json", New FileChecker With {.IsJson = True})
                 New NetFile(DlSourceLauncherOrMetaGet(JsonUrl), VersionFolder & Id & ".json", New FileChecker With {.IsJson = True})
             }) With {.ProgressWeight = 2})
             }) With {.ProgressWeight = 2})
             '获取支持库文件地址
             '获取支持库文件地址
-            Loaders.Add(New LoaderTask(Of String, List(Of NetFile))("分析核心 JAR 文件下载地址",
+            Loaders.Add(New LoaderTask(Of String, List(Of NetFile))("分析版本 JAR 文件下载地址",
                 Sub(Task) Task.Output = New List(Of NetFile) From {DlClientJarGet(New McInstance(VersionFolder), False)}
                 Sub(Task) Task.Output = New List(Of NetFile) From {DlClientJarGet(New McInstance(VersionFolder), False)}
             ) With {.ProgressWeight = 0.5, .Show = False})
             ) With {.ProgressWeight = 0.5, .Show = False})
             '下载支持库文件
             '下载支持库文件
-            Loaders.Add(New LoaderDownload("下载核心 JAR 文件", New List(Of NetFile)) With {.ProgressWeight = 5})
+            Loaders.Add(New LoaderDownload("下载版本 JAR 文件", New List(Of NetFile)) With {.ProgressWeight = 5})
 
 
             '启动
             '启动
             Dim Loader As New LoaderCombo(Of String)("Minecraft " & Id & " 下载", Loaders) With {.OnStateChanged = AddressOf LoaderStateChangedHintOnly}
             Dim Loader As New LoaderCombo(Of String)("Minecraft " & Id & " 下载", Loaders) With {.OnStateChanged = AddressOf LoaderStateChangedHintOnly}
@@ -326,7 +324,6 @@ pause"
 
 
     Private Sub McDownloadOptiFineSave(DownloadInfo As DlOptiFineListEntry)
     Private Sub McDownloadOptiFineSave(DownloadInfo As DlOptiFineListEntry)
         Try
         Try
-            Dim Id As String = DownloadInfo.InstanceName
             Dim Target As String = Dialogs.SaveFile("选择保存位置", DownloadInfo.FileName, filter:={("jar", "OptiFine Jar")})
             Dim Target As String = Dialogs.SaveFile("选择保存位置", DownloadInfo.FileName, filter:={("jar", "OptiFine Jar")})
             If Target Is Nothing Then Return
             If Target Is Nothing Then Return
 
 
@@ -374,9 +371,9 @@ pause"
         '添加 Java Wrapper 作为主 Jar
         '添加 Java Wrapper 作为主 Jar
         Dim Arguments As String
         Dim Arguments As String
         If UseJavaWrapper AndAlso Not Settings.Get(Of Boolean)("LaunchAdvanceDisableJLW") Then
         If UseJavaWrapper AndAlso Not Settings.Get(Of Boolean)("LaunchAdvanceDisableJLW") Then
-            Arguments = $"-Doolloo.jlw.tmpdir=""{PathPure.Value.TrimEnd("\")}"" -Duser.home=""{BaseMcFolderHome.TrimEnd("\")}"" -cp ""{Target}"" -jar ""{LaunchUtils.ExtractPatch("Launch\JavaWrapper.jar", PathPure.Value)}"" optifine.Installer"
+            Arguments = $"-Doolloo.jlw.tmpdir=""{PathUtils.RemoveSlashSuffix(PathPure.Value)}"" -Duser.home=""{PathUtils.RemoveSlashSuffix(BaseMcFolderHome)}"" -cp ""{Target}"" -jar ""{LaunchUtils.ExtractPatch("Launch\JavaWrapper.jar", PathPure.Value)}"" optifine.Installer"
         Else
         Else
-            Arguments = $"-Duser.home=""{BaseMcFolderHome.TrimEnd("\")}"" -cp ""{Target}"" optifine.Installer"
+            Arguments = $"-Duser.home=""{PathUtils.RemoveSlashSuffix(BaseMcFolderHome)}"" -cp ""{Target}"" optifine.Installer"
         End If
         End If
         If Java.Version.Major >= 9 Then Arguments = "--add-exports cpw.mods.bootstraplauncher/cpw.mods.bootstraplauncher=ALL-UNNAMED " & Arguments
         If Java.Version.Major >= 9 Then Arguments = "--add-exports cpw.mods.bootstraplauncher/cpw.mods.bootstraplauncher=ALL-UNNAMED " & Arguments
         '开始启动
         '开始启动
@@ -473,13 +470,12 @@ pause"
     Private Function McDownloadOptiFineLoader(DownloadInfo As DlOptiFineListEntry, McFolder As String, ClientDownloadLoader As LoaderCombo(Of String), ClientFolder As String) As List(Of LoaderBase)
     Private Function McDownloadOptiFineLoader(DownloadInfo As DlOptiFineListEntry, McFolder As String, ClientDownloadLoader As LoaderCombo(Of String), ClientFolder As String) As List(Of LoaderBase)
 
 
         '参数初始化
         '参数初始化
-        Dim IsCustomFolder As Boolean = McFolder <> McFolderSelected
         Dim Id As String = DownloadInfo.InstanceName
         Dim Id As String = DownloadInfo.InstanceName
         Dim VersionFolder As String = McFolder & "versions\" & Id & "\"
         Dim VersionFolder As String = McFolder & "versions\" & Id & "\"
         Dim IsNewerVersion As Boolean = DownloadInfo.Inherit.Contains("w") OrElse McVersion.VersionToDrop(DownloadInfo.Inherit) >= 140
         Dim IsNewerVersion As Boolean = DownloadInfo.Inherit.Contains("w") OrElse McVersion.VersionToDrop(DownloadInfo.Inherit) >= 140
         Dim Target As String = If(IsNewerVersion,
         Dim Target As String = If(IsNewerVersion,
-            $"{RequestTaskTempFolder()}OptiFine.jar",
-            $"{McFolder}libraries\optifine\OptiFine\{DownloadInfo.FileName.Replace("OptiFine_", "").Replace(".jar", "").Replace("preview_", "")}\{DownloadInfo.FileName.Replace("OptiFine_", "OptiFine-").Replace("preview_", "")}")
+            $"{RequestTaskTempFolder(True)}OptiFine.jar",
+            $"{McFolder}libraries\optifine\OptiFine\{PathUtils.GetFileNameWithoutExtension(DownloadInfo.FileName).Replace("OptiFine_", "").Replace("preview_", "")}\{DownloadInfo.FileName.Replace("OptiFine_", "OptiFine-").Replace("preview_", "")}")
         Dim Loaders As New List(Of LoaderBase)
         Dim Loaders As New List(Of LoaderBase)
 
 
         '获取下载地址
         '获取下载地址
@@ -522,7 +518,6 @@ pause"
             Loop
             Loop
             If Task.IsCanceled Then Return
             If Task.IsCanceled Then Return
             '复制原版文件
             '复制原版文件
-            If Not IsCustomFolder Then Return
             SyncLock VanillaSyncLock
             SyncLock VanillaSyncLock
                 Dim ClientName As String = PathUtils.GetLastPart(ClientFolder)
                 Dim ClientName As String = PathUtils.GetLastPart(ClientFolder)
                 DirectoryUtils.Create(McFolder & "versions\" & DownloadInfo.Inherit)
                 DirectoryUtils.Create(McFolder & "versions\" & DownloadInfo.Inherit)
@@ -540,7 +535,7 @@ pause"
             Logger.Info($"检测为新版 OptiFine:{DownloadInfo.Inherit}")
             Logger.Info($"检测为新版 OptiFine:{DownloadInfo.Inherit}")
             Loaders.Add(New LoaderTask(Of List(Of NetFile), Boolean)("安装 OptiFine(方式 A)",
             Loaders.Add(New LoaderTask(Of List(Of NetFile), Boolean)("安装 OptiFine(方式 A)",
             Sub(Task As LoaderTask(Of List(Of NetFile), Boolean))
             Sub(Task As LoaderTask(Of List(Of NetFile), Boolean))
-                Dim BaseMcFolderHome As String = RequestTaskTempFolder()
+                Dim BaseMcFolderHome As String = RequestTaskTempFolder(True)
                 Dim BaseMcFolder As String = BaseMcFolderHome & ".minecraft\"
                 Dim BaseMcFolder As String = BaseMcFolderHome & ".minecraft\"
                 Try
                 Try
                     '准备安装环境
                     '准备安装环境
@@ -597,7 +592,7 @@ Retry:
     ""releaseTime"": """ & If(DownloadInfo.ReleaseTime = "", InheritInstance.ReleaseTime.ToString("yyyy'-'MM'-'dd"), DownloadInfo.ReleaseTime.Replace("/", "-")) & "T23:33:33+08:00"",
     ""releaseTime"": """ & If(DownloadInfo.ReleaseTime = "", InheritInstance.ReleaseTime.ToString("yyyy'-'MM'-'dd"), DownloadInfo.ReleaseTime.Replace("/", "-")) & "T23:33:33+08:00"",
     ""type"": ""release"",
     ""type"": ""release"",
     ""libraries"": [
     ""libraries"": [
-        {""name"": ""optifine:OptiFine:" & DownloadInfo.FileName.Replace("OptiFine_", "").Replace(".jar", "").Replace("preview_", "") & """},
+        {""name"": ""optifine:OptiFine:" & PathUtils.GetFileNameWithoutExtension(DownloadInfo.FileName).Replace("OptiFine_", "").Replace("preview_", "") & """},
         {""name"": ""net.minecraft:launchwrapper:1.12""}
         {""name"": ""net.minecraft:launchwrapper:1.12""}
     ],
     ],
     ""mainClass"": ""net.minecraft.launchwrapper.Launch"","
     ""mainClass"": ""net.minecraft.launchwrapper.Launch"","
@@ -785,19 +780,14 @@ Retry:
     ''' 这不会补全所需的支持库文件,需要后续手动补全支持库。
     ''' 这不会补全所需的支持库文件,需要后续手动补全支持库。
     ''' </summary>
     ''' </summary>
     Private Function McDownloadLiteLoaderLoader(DownloadInfo As DlLiteLoaderListEntry, McFolder As String) As List(Of LoaderBase)
     Private Function McDownloadLiteLoaderLoader(DownloadInfo As DlLiteLoaderListEntry, McFolder As String) As List(Of LoaderBase)
-
-        '参数初始化
-        Dim IsCustomFolder As Boolean = McFolder <> McFolderSelected
-        Dim Id As String = DownloadInfo.Inherit
-        Dim Target As String = PathTemp & "Download\" & Id & "-Liteloader.jar"
-        Dim VersionName As String = DownloadInfo.Inherit & "-LiteLoader"
-        Dim VersionFolder As String = McFolder & "versions\" & VersionName & "\"
         Dim Loaders As New List(Of LoaderBase)
         Dim Loaders As New List(Of LoaderBase)
 
 
         '安装
         '安装
         Loaders.Add(New LoaderTask(Of String, String)("安装 LiteLoader",
         Loaders.Add(New LoaderTask(Of String, String)("安装 LiteLoader",
         Sub(Task As LoaderTask(Of String, String))
         Sub(Task As LoaderTask(Of String, String))
             Try
             Try
+                Dim VersionName As String = DownloadInfo.Inherit & "-LiteLoader"
+                Dim VersionFolder As String = McFolder & "versions\" & VersionName & "\"
                 '构造版本 Json
                 '构造版本 Json
                 Dim VersionJson As New JObject
                 Dim VersionJson As New JObject
                 VersionJson.Add("id", VersionName)
                 VersionJson.Add("id", VersionName)
@@ -945,7 +935,7 @@ Retry:
         '添加 Java Wrapper 作为主 Jar
         '添加 Java Wrapper 作为主 Jar
         Dim Arguments As String
         Dim Arguments As String
         If UseJavaWrapper AndAlso Not Settings.Get(Of Boolean)("LaunchAdvanceDisableJLW") Then
         If UseJavaWrapper AndAlso Not Settings.Get(Of Boolean)("LaunchAdvanceDisableJLW") Then
-            Arguments = $"-Doolloo.jlw.tmpdir=""{PathPure.Value.TrimEnd("\")}"" -cp ""{PathTemp}Cache\forge_installer.jar;{Target}"" -jar ""{LaunchUtils.ExtractPatch("Launch\JavaWrapper.jar", PathPure.Value)}"" com.bangbang93.ForgeInstaller ""{McFolder}"
+            Arguments = $"-Doolloo.jlw.tmpdir=""{PathUtils.RemoveSlashSuffix(PathPure.Value)}"" -cp ""{PathTemp}Cache\forge_installer.jar;{Target}"" -jar ""{LaunchUtils.ExtractPatch("Launch\JavaWrapper.jar", PathPure.Value)}"" com.bangbang93.ForgeInstaller ""{McFolder}"
         Else
         Else
             Arguments = $"-cp ""{PathTemp}Cache\forge_installer.jar;{Target}"" com.bangbang93.ForgeInstaller ""{McFolder}"
             Arguments = $"-cp ""{PathTemp}Cache\forge_installer.jar;{Target}"" com.bangbang93.ForgeInstaller ""{McFolder}"
         End If
         End If
@@ -1078,9 +1068,7 @@ Retry:
     ''' 获取下载某个 Forgelike 版本的加载器列表。
     ''' 获取下载某个 Forgelike 版本的加载器列表。
     ''' </summary>
     ''' </summary>
     Private Function McDownloadForgelikeLoader(IsNeoForge As Boolean, LoaderVersion As String, NewInstanceName As String, Inherit As String, Info As DlForgelikeEntry, McFolder As String, ClientDownloadLoader As LoaderCombo(Of String), ClientFolder As String) As List(Of LoaderBase)
     Private Function McDownloadForgelikeLoader(IsNeoForge As Boolean, LoaderVersion As String, NewInstanceName As String, Inherit As String, Info As DlForgelikeEntry, McFolder As String, ClientDownloadLoader As LoaderCombo(Of String), ClientFolder As String) As List(Of LoaderBase)
-
         '参数初始化
         '参数初始化
-        McFolder = If(McFolder, McFolderSelected)
         If IsNeoForge AndAlso Info Is Nothing Then
         If IsNeoForge AndAlso Info Is Nothing Then
             '需要传入 API Name,但整合包版本可能不以 1.20.1- 开头,所以需要进行特别处理
             '需要传入 API Name,但整合包版本可能不以 1.20.1- 开头,所以需要进行特别处理
             If Inherit = "1.20.1" AndAlso Not LoaderVersion.StartsWithF("1.20.1-") Then
             If Inherit = "1.20.1" AndAlso Not LoaderVersion.StartsWithF("1.20.1-") Then
@@ -1095,12 +1083,9 @@ Retry:
             LoaderVersion = LoaderVersion.AfterLast("-")
             LoaderVersion = LoaderVersion.AfterLast("-")
         End If
         End If
         Dim LoaderName As String = If(IsNeoForge, "NeoForge", "Forge")
         Dim LoaderName As String = If(IsNeoForge, "NeoForge", "Forge")
-        Dim IsCustomFolder As Boolean = McFolder <> McFolderSelected
-        Dim InstallerAddress As String = RequestTaskTempFolder() & "forge_installer.jar"
+        Dim InstallerAddress As String = RequestTaskTempFolder(True) & "forge_installer.jar"
         Dim VersionFolder As String = $"{McFolder}versions\{NewInstanceName}\"
         Dim VersionFolder As String = $"{McFolder}versions\{NewInstanceName}\"
-        Dim DisplayName As String = $"{LoaderName} {Inherit} - {LoaderVersion}"
         Dim Loaders As New List(Of LoaderBase)
         Dim Loaders As New List(Of LoaderBase)
-        Dim LibVersionFolder As String = $"{McFolderSelected}versions\{NewInstanceName}\" '作为 Lib 文件目标的版本文件夹
 
 
         '获取 Forge 下载信息
         '获取 Forge 下载信息
         If Info Is Nothing Then
         If Info Is Nothing Then
@@ -1112,7 +1097,7 @@ Retry:
                 Task.Progress = 0.8
                 Task.Progress = 0.8
                 '查找对应版本
                 '查找对应版本
                 For Each ForgeVersion In ForgeLoader.Output
                 For Each ForgeVersion In ForgeLoader.Output
-                    If CompareVersion(ForgeVersion.Version.ToString, LoaderVersion) = 0 Then
+                    If McUtils.CompareVersion(ForgeVersion.Version.ToString, LoaderVersion) = 0 Then
                         Info = ForgeVersion
                         Info = ForgeVersion
                         Return
                         Return
                     End If
                     End If
@@ -1147,6 +1132,7 @@ Retry:
         Loaders.Add(New LoaderDownload($"下载 {LoaderName} 主文件", New List(Of NetFile)) With {.ProgressWeight = 9})
         Loaders.Add(New LoaderDownload($"下载 {LoaderName} 主文件", New List(Of NetFile)) With {.ProgressWeight = 9})
 
 
         '安装(仅在新版安装时需要原版 Jar)
         '安装(仅在新版安装时需要原版 Jar)
+        Dim CurrentMcFolder = McFolderSelected
         If IsNeoForge OrElse LoaderVersion.BeforeFirst(".") >= 20 Then
         If IsNeoForge OrElse LoaderVersion.BeforeFirst(".") >= 20 Then
             Logger.Info($"检测为{If(IsNeoForge, " Neo", "新版 ")}Forge:{LoaderVersion}")
             Logger.Info($"检测为{If(IsNeoForge, " Neo", "新版 ")}Forge:{LoaderVersion}")
             Dim Libs As List(Of McLibEntry) = Nothing
             Dim Libs As List(Of McLibEntry) = Nothing
@@ -1162,7 +1148,7 @@ Retry:
                     Dim Json2 As JObject = Installer.GetEntry("version.json").Open().ReadString().DeserializeJson()
                     Dim Json2 As JObject = Installer.GetEntry("version.json").Open().ReadString().DeserializeJson()
                     Json.Merge(Json2)
                     Json.Merge(Json2)
                     '获取 Lib 下载信息
                     '获取 Lib 下载信息
-                    Libs = McLibListGetWithJson(Json, True)
+                    Libs = McLibListGetWithJson(Json, True, CurrentMcFolder)
                     '添加 Mappings 下载信息
                     '添加 Mappings 下载信息
                     If Json("data") IsNot Nothing AndAlso Json("data")("MOJMAPS") IsNot Nothing Then
                     If Json("data") IsNot Nothing AndAlso Json("data")("MOJMAPS") IsNot Nothing Then
                         '下载原版 Json 文件
                         '下载原版 Json 文件
@@ -1170,7 +1156,7 @@ Retry:
                         Dim RawJson As JObject = NetRequestByLoader(DlSourceLauncherOrMetaGet(DlClientListGet(Inherit)), IsJson:=True).DeserializeJson()
                         Dim RawJson As JObject = NetRequestByLoader(DlSourceLauncherOrMetaGet(DlClientListGet(Inherit)), IsJson:=True).DeserializeJson()
                         '[net.minecraft:client:1.17.1-20210706.113038:mappings@txt] 或 @tsrg]
                         '[net.minecraft:client:1.17.1-20210706.113038:mappings@txt] 或 @tsrg]
                         Dim OriginalName As String = Json("data")("MOJMAPS")("client").ToString.Trim("[]".ToCharArray()).BeforeFirst("@")
                         Dim OriginalName As String = Json("data")("MOJMAPS")("client").ToString.Trim("[]".ToCharArray()).BeforeFirst("@")
-                        Dim Address = McLibGet(OriginalName).Replace(".jar", "." & Json("data")("MOJMAPS")("client").ToString.Trim("[]".ToCharArray()).Split("@")(1))
+                        Dim Address = Path.ChangeExtension(McLibGet(OriginalName, CustomMcFolder:=CurrentMcFolder), "." & Json("data")("MOJMAPS")("client").ToString.Trim("[]".ToCharArray()).Split("@")(1))
                         Dim ClientMappings As JToken = RawJson("downloads")("client_mappings")
                         Dim ClientMappings As JToken = RawJson("downloads")("client_mappings")
                         Libs.Add(New McLibEntry With {
                         Libs.Add(New McLibEntry With {
                                  .IsNatives = False, .LocalPath = Address, .OriginalName = OriginalName,
                                  .IsNatives = False, .LocalPath = Address, .OriginalName = OriginalName,
@@ -1187,7 +1173,7 @@ Retry:
                             Exit For
                             Exit For
                         End If
                         End If
                     Next
                     Next
-                    Task.Output = McLibNetFilesFromTokens(Libs, McFolderSelected)
+                    Task.Output = McLibNetFilesFromTokens(Libs, CurrentMcFolder)
                 Catch ex As Exception
                 Catch ex As Exception
                     Throw New Exception($"获取{If(IsNeoForge, " Neo", "新版 ")}Forge 支持库列表失败", ex)
                     Throw New Exception($"获取{If(IsNeoForge, " Neo", "新版 ")}Forge 支持库列表失败", ex)
                 Finally
                 Finally
@@ -1198,13 +1184,11 @@ Retry:
             Loaders.Add(New LoaderTask(Of List(Of NetFile), Boolean)($"获取 {LoaderName} 支持库文件",
             Loaders.Add(New LoaderTask(Of List(Of NetFile), Boolean)($"获取 {LoaderName} 支持库文件",
             Sub(Task As LoaderTask(Of List(Of NetFile), Boolean))
             Sub(Task As LoaderTask(Of List(Of NetFile), Boolean))
 #Region "Forgelike 文件"
 #Region "Forgelike 文件"
-                If IsCustomFolder Then
-                    For Each LibFile As McLibEntry In Libs
-                        Dim RealPath As String = LibFile.LocalPath.Replace(McFolderSelected, McFolder)
-                        If Not FileUtils.Exists(RealPath) Then FileUtils.Copy(LibFile.LocalPath, RealPath)
-                        Logger.Trace($"复制的 {LoaderName} 支持库文件:{LibFile.LocalPath}")
-                    Next
-                End If
+                For Each LibFile As McLibEntry In Libs
+                    Dim RealPath As String = Path.Combine(McFolder, LibFile.LocalPath.Substring(CurrentMcFolder.Length))
+                    If Not FileUtils.Exists(RealPath) Then FileUtils.Copy(LibFile.LocalPath, RealPath)
+                    Logger.Trace($"复制的 {LoaderName} 支持库文件:{LibFile.LocalPath}")
+                Next
 #End Region
 #End Region
 #Region "原版文件"
 #Region "原版文件"
                 '等待原版文件下载完成
                 '等待原版文件下载完成
@@ -1218,7 +1202,6 @@ Retry:
                 Loop
                 Loop
                 If Task.IsCanceled Then Return
                 If Task.IsCanceled Then Return
                 '复制原版文件
                 '复制原版文件
-                If Not IsCustomFolder Then Return
                 SyncLock VanillaSyncLock
                 SyncLock VanillaSyncLock
                     Dim ClientName As String = PathUtils.GetLastPart(ClientFolder)
                     Dim ClientName As String = PathUtils.GetLastPart(ClientFolder)
                     DirectoryUtils.Create(McFolder & "versions\" & Inherit)
                     DirectoryUtils.Create(McFolder & "versions\" & Inherit)
@@ -1233,14 +1216,10 @@ Retry:
             End Sub) With {.ProgressWeight = 0.1, .Show = False})
             End Sub) With {.ProgressWeight = 0.1, .Show = False})
             Loaders.Add(New LoaderTask(Of Boolean, Boolean)(If(IsNeoForge, "安装 NeoForge", "安装 Forge(方式 A)"),
             Loaders.Add(New LoaderTask(Of Boolean, Boolean)(If(IsNeoForge, "安装 NeoForge", "安装 Forge(方式 A)"),
             Sub(Task As LoaderTask(Of Boolean, Boolean))
             Sub(Task As LoaderTask(Of Boolean, Boolean))
-                Dim Installer As ZipArchive = Nothing
                 Try
                 Try
                     Logger.Info($"开始进行 Forgelike 安装:{InstallerAddress}")
                     Logger.Info($"开始进行 Forgelike 安装:{InstallerAddress}")
                     '记录当前文件夹列表(在新建目标文件夹之前)
                     '记录当前文件夹列表(在新建目标文件夹之前)
                     Dim OldList = DirectoryUtils.EnumerateDirectories(McFolder & "versions\").ToList()
                     Dim OldList = DirectoryUtils.EnumerateDirectories(McFolder & "versions\").ToList()
-                    '解压并获取信息
-                    Installer = FileUtils.OpenZip(InstallerAddress)
-                    Dim Json As JObject = Installer.GetEntry("install_profile.json").Open().ReadString().DeserializeJson()
                     '新建目标版本文件夹
                     '新建目标版本文件夹
                     DirectoryUtils.Create(VersionFolder)
                     DirectoryUtils.Create(VersionFolder)
                     Task.Progress = 0.04
                     Task.Progress = 0.04
@@ -1292,7 +1271,6 @@ Retry:
                 Finally
                 Finally
                     '清理文件
                     '清理文件
                     Try
                     Try
-                        Installer?.Dispose()
                         FileUtils.Delete(InstallerAddress)
                         FileUtils.Delete(InstallerAddress)
                     Catch ex As Exception
                     Catch ex As Exception
                         Logger.Warn(ex, $"安装 {LoaderName} 清理文件时出错")
                         Logger.Warn(ex, $"安装 {LoaderName} 清理文件时出错")
@@ -1487,19 +1465,19 @@ Retry:
     ''' <summary>
     ''' <summary>
     ''' 尝试获取某个 MC 版本对应的 Forge 推荐版本。如果不可用会返回 Nothing。
     ''' 尝试获取某个 MC 版本对应的 Forge 推荐版本。如果不可用会返回 Nothing。
     ''' </summary>
     ''' </summary>
-    Public Function McDownloadForgeRecommendedGet(McVersion As String) As String
+    Public Function McDownloadForgeRecommendedGet(McVersionName As String) As String
         Try
         Try
-            If McVersion Is Nothing Then Return Nothing
+            If McVersionName Is Nothing Then Return Nothing
             Dim List As String = FileUtils.TryReadAsString(PathTemp & "Cache\ForgeRecommendedList.json")
             Dim List As String = FileUtils.TryReadAsString(PathTemp & "Cache\ForgeRecommendedList.json")
             If String.IsNullOrEmpty(List) Then
             If String.IsNullOrEmpty(List) Then
                 Logger.Info("没有 Forge 推荐版本缓存文件")
                 Logger.Info("没有 Forge 推荐版本缓存文件")
                 Return Nothing
                 Return Nothing
             End If
             End If
             Dim Json As JObject = List.DeserializeJson()
             Dim Json As JObject = List.DeserializeJson()
-            If Json Is Nothing OrElse (Not If(McVersion, "").Contains(".")) OrElse Not Json.ContainsKey(McVersion) Then Return Nothing
-            Return If(Json(McVersion), "").ToString
+            If Json Is Nothing OrElse (Not If(McVersionName, "").Contains(".")) OrElse Not Json.ContainsKey(McVersionName) Then Return Nothing
+            Return If(Json(McVersionName), "").ToString
         Catch ex As Exception
         Catch ex As Exception
-            Logger.Error(ex, $"获取 Forge 推荐版本失败({If(McVersion, "null")})")
+            Logger.Error(ex, $"获取 Forge 推荐版本失败({If(McVersionName, "null")})")
             Return Nothing
             Return Nothing
         End Try
         End Try
     End Function
     End Function
@@ -1640,7 +1618,6 @@ Retry:
     Private Function McDownloadFabricLoader(FabricVersion As String, MinecraftName As String, McFolder As String) As List(Of LoaderBase)
     Private Function McDownloadFabricLoader(FabricVersion As String, MinecraftName As String, McFolder As String) As List(Of LoaderBase)
 
 
         '参数初始化
         '参数初始化
-        Dim IsCustomFolder As Boolean = McFolder <> McFolderSelected
         Dim Id As String = "fabric-loader-" & FabricVersion & "-" & MinecraftName
         Dim Id As String = "fabric-loader-" & FabricVersion & "-" & MinecraftName
         Dim VersionFolder As String = McFolder & "versions\" & Id & "\"
         Dim VersionFolder As String = McFolder & "versions\" & Id & "\"
         Dim Loaders As New List(Of LoaderBase)
         Dim Loaders As New List(Of LoaderBase)
@@ -1702,195 +1679,82 @@ Retry:
 
 
 #End Region
 #End Region
 
 
-#Region "合并安装"
+#Region "安装"
 
 
     ''' <summary>
     ''' <summary>
     ''' 安装请求。
     ''' 安装请求。
     ''' </summary>
     ''' </summary>
     Public Class McInstallRequest
     Public Class McInstallRequest
-
         ''' <summary>
         ''' <summary>
-        ''' 必填。安装目标版本名称。
+        ''' 新版本安装必填。安装目标版本名称。
         ''' </summary>
         ''' </summary>
         Public NewInstanceName As String
         Public NewInstanceName As String
         ''' <summary>
         ''' <summary>
         ''' 必填。安装目标文件夹。
         ''' 必填。安装目标文件夹。
         ''' </summary>
         ''' </summary>
-        Public VersionFolder As String
-
+        Public InstanceFolder As String
         ''' <summary>
         ''' <summary>
-        ''' 必填。欲下载的 Minecraft 的版本名。
+        ''' 必填。欲安装的 Minecraft 与组件版本。
         ''' </summary>
         ''' </summary>
-        Public MinecraftName As String = Nothing
+        Public Components As McComponents
         ''' <summary>
         ''' <summary>
         ''' 可选。欲下载的 Minecraft Json 地址。
         ''' 可选。欲下载的 Minecraft Json 地址。
         ''' </summary>
         ''' </summary>
         Public MinecraftJson As String = Nothing
         Public MinecraftJson As String = Nothing
-
-        '若要下载 OptiFine,则需要在下面两项中完成至少一项
-        ''' <summary>
-        ''' 欲下载的 OptiFine 版本名。例如 HD_U_F6_pre1。
-        ''' </summary>
-        Public OptiFineVersion As String = Nothing
-        ''' <summary>
-        ''' 欲下载的 OptiFine 详细信息。
-        ''' </summary>
-        Public OptiFineEntry As DlOptiFineListEntry = Nothing
-
-        '若要下载 Forge,则需要在下面两项中完成至少一项
-        ''' <summary>
-        ''' 欲下载的 Forge 版本名。接受例如 36.1.4 / 14.23.5.2859 / 1.19-41.1.0 的输入。
-        ''' </summary>
-        Public ForgeVersion As String = Nothing
         ''' <summary>
         ''' <summary>
         ''' 欲下载的 Forge。
         ''' 欲下载的 Forge。
         ''' </summary>
         ''' </summary>
         Public ForgeEntry As DlForgeVersionEntry = Nothing
         Public ForgeEntry As DlForgeVersionEntry = Nothing
-
-        '若要下载 NeoForge,则需要在下面两项中完成至少一项
-        ''' <summary>
-        ''' 欲下载的 NeoForge 版本名。
-        ''' </summary>
-        Public NeoForgeVersion As String = Nothing
         ''' <summary>
         ''' <summary>
         ''' 欲下载的 NeoForge。
         ''' 欲下载的 NeoForge。
         ''' </summary>
         ''' </summary>
         Public NeoForgeEntry As DlNeoForgeListEntry = Nothing
         Public NeoForgeEntry As DlNeoForgeListEntry = Nothing
-
-        ''' <summary>
-        ''' 欲下载的 Fabric Loader 版本名。
-        ''' </summary>
-        Public FabricVersion As String = Nothing
-
         ''' <summary>
         ''' <summary>
         ''' 欲下载的 Fabric API 信息。
         ''' 欲下载的 Fabric API 信息。
         ''' </summary>
         ''' </summary>
         Public FabricApi As ResourceVersion = Nothing
         Public FabricApi As ResourceVersion = Nothing
-
         ''' <summary>
         ''' <summary>
         ''' 欲下载的 OptiFabric 信息。
         ''' 欲下载的 OptiFabric 信息。
         ''' </summary>
         ''' </summary>
         Public OptiFabric As ResourceVersion = Nothing
         Public OptiFabric As ResourceVersion = Nothing
-
         ''' <summary>
         ''' <summary>
         ''' 欲下载的 LiteLoader 详细信息。
         ''' 欲下载的 LiteLoader 详细信息。
         ''' </summary>
         ''' </summary>
         Public LiteLoaderEntry As DlLiteLoaderListEntry = Nothing
         Public LiteLoaderEntry As DlLiteLoaderListEntry = Nothing
-
+        ''' <summary>
+        ''' 是否为中间产物。
+        ''' </summary>
+        Public IsIntermediateOutput As Boolean = False
     End Class
     End Class
 
 
     ''' <summary>
     ''' <summary>
-    ''' 在加载器状态改变后显示一条提示。
-    ''' 不会进行任何其他操作。
-    ''' </summary>
-    Public Sub LoaderStateChangedHintOnly(Loader As LoaderBase)
-        Select Case Loader.State
-            Case LoadState.Finished
-                Hint(Loader.Name & "成功!", HintType.Green)
-            Case LoadState.Failed
-                Hint(Loader.Name & "失败:" & Loader.Error.GetDisplay(False), HintType.Red)
-            Case LoadState.Canceled
-                Hint(Loader.Name & "已取消!", HintType.Blue)
-        End Select
-    End Sub
-    ''' <summary>
-    ''' 安装加载器状态改变后进行提示和重载文件夹列表的方法。
-    ''' </summary>
-    Public Sub McInstallState(Loader As LoaderBase)
-        Select Case Loader.State
-            Case LoadState.Finished
-                WriteIni(McFolderSelected & "PCL.ini", "InstanceCache", "") '清空缓存(合并安装会先生成文件夹,这会在刷新时误判为可以使用缓存)
-                Hint(Loader.Name & "成功!", HintType.Green)
-                'TODO: 自动选择安装成功的版本
-            Case LoadState.Failed
-                MyMsgBox(Loader.Error.GetDisplay(True), Loader.Name & "失败", IsWarn:=True)
-            Case LoadState.Canceled
-                Hint(Loader.Name & "已取消!", HintType.Blue)
-            Case LoadState.Loading
-                Return '不重新加载版本列表
-        End Select
-        McInstallFailedClearFolder(Loader)
-        LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
-    End Sub
-    Public Sub McInstallFailedClearFolder(Loader)
-        Try
-            If Loader.State <> LoadState.Failed AndAlso Loader.State <> LoadState.Canceled Then Return
-            Thread.Sleep(1000) '防止存在尚未完全释放的文件,导致清理失败(例如整合包安装)
-            '删除版本文件夹
-            If DirectoryUtils.Exists(Loader.Input & "saves\") OrElse DirectoryUtils.Exists(Loader.Input & "versions\") Then
-                Logger.Warn($"由于版本已被独立启动,不清理版本文件夹:{Loader.Input}")
-            Else
-                Logger.Warn($"由于下载失败或取消,清理版本文件夹:{Loader.Input}")
-                DirectoryUtils.Delete(Loader.Input)
-            End If
-        Catch ex As Exception
-            Logger.Warn(ex, "下载失败或取消后清理版本文件夹失败")
-        End Try
-    End Sub
-
-    ''' <summary>
-    ''' 进行合并安装。返回是否已经开始安装(例如如果没有安装 Java 则会进行提示并返回 False)
-    ''' </summary>
-    Public Function McInstall(Request As McInstallRequest) As Boolean
-        Try
-            Dim SubLoaders = McInstallLoader(Request)
-            If SubLoaders Is Nothing Then Return False
-            Dim Loader As New LoaderCombo(Of String)(Request.NewInstanceName & " 安装", SubLoaders) With {.OnStateChanged = AddressOf McInstallState}
-
-            '启动
-            Loader.Start(Request.VersionFolder)
-            LoaderTaskbarAdd(Loader)
-            FrmMain.BtnExtraDownload.ShowRefresh()
-            FrmMain.BtnExtraDownload.Ribble()
-            Return True
-
-        Catch ex As Exception
-            Logger.Error(ex, "开始合并安装失败")
-            Return False
-        End Try
-    End Function
-    ''' <summary>
-    ''' 获取合并安装加载器列表,并进行前期的缓存清理与 Java 检查工作。
+    ''' 获取合并安装加载器列表。
     ''' </summary>
     ''' </summary>
-    Public Function McInstallLoader(Request As McInstallRequest) As List(Of LoaderBase)
+    Public Function McInstallLoadersGet(Request As McInstallRequest) As List(Of LoaderBase)
         '获取缓存目录(安装 Mod 加载器的文件夹不能包含空格)
         '获取缓存目录(安装 Mod 加载器的文件夹不能包含空格)
-        Dim TempMcFolder As String = RequestTaskTempFolder(Request.OptiFineEntry IsNot Nothing OrElse Request.ForgeEntry IsNot Nothing OrElse Request.NeoForgeEntry IsNot Nothing)
-
-        '获取参数
-        Dim VersionFolder As String = McFolderSelected & "versions\" & Request.NewInstanceName & "\"
-        If DirectoryUtils.Exists(TempMcFolder) Then DirectoryUtils.Delete(TempMcFolder)
-        Dim OptiFineFolder As String = Nothing
-        If Request.OptiFineVersion IsNot Nothing Then
-            If Request.OptiFineVersion.Contains("_HD_U_") Then Request.OptiFineVersion = "HD_U_" & Request.OptiFineVersion.AfterLast("_HD_U_") '#735
-            Request.OptiFineEntry = New DlOptiFineListEntry With {
-                .DisplayName = Request.MinecraftName & " " & Request.OptiFineVersion.Replace("HD_U_", "").Replace("_", "").Replace("pre", " pre"),
-                .Inherit = Request.MinecraftName,
-                .IsPreview = Request.OptiFineVersion.ContainsIgnoreCase("pre"),
-                .InstanceName = Request.MinecraftName & "-OptiFine_" & Request.OptiFineVersion,
-                .FileName = If(Request.OptiFineVersion.ContainsIgnoreCase("pre"), "preview_", "") &
-                    "OptiFine_" & Request.MinecraftName & "_" & Request.OptiFineVersion & ".jar"
-            }
-        End If
-        If Request.OptiFineEntry IsNot Nothing Then OptiFineFolder = TempMcFolder & "versions\" & Request.OptiFineEntry.InstanceName
-        Dim ForgeFolder As String = Nothing
-        If Request.ForgeEntry IsNot Nothing Then Request.ForgeVersion = If(Request.ForgeVersion, Request.ForgeEntry.VersionName)
-        If Request.ForgeVersion IsNot Nothing Then ForgeFolder = TempMcFolder & "versions\forge-" & Request.ForgeVersion
-        Dim NeoForgeFolder As String = Nothing
-        If Request.NeoForgeEntry IsNot Nothing Then Request.NeoForgeVersion = If(Request.NeoForgeVersion, Request.NeoForgeEntry.VersionName)
-        If Request.NeoForgeVersion IsNot Nothing Then NeoForgeFolder = TempMcFolder & "versions\neoforge-" & Request.NeoForgeVersion
-        Dim FabricFolder As String = Nothing
-        If Request.FabricVersion IsNot Nothing Then FabricFolder = TempMcFolder & "versions\fabric-loader-" & Request.FabricVersion & "-" & Request.MinecraftName
-        Dim LiteLoaderFolder As String = Nothing
-        If Request.LiteLoaderEntry IsNot Nothing Then LiteLoaderFolder = TempMcFolder & "versions\" & Request.MinecraftName & "-LiteLoader"
+        Dim TempMcFolder As String = RequestTaskTempFolder(Request.Components.OptiFineEntry IsNot Nothing OrElse
+            Request.Components.ModLoaderVersions.ContainsKey(ModLoaders.Forge) OrElse Request.Components.ModLoaderVersions.ContainsKey(ModLoaders.NeoForge))
+
+        '获取各个 Mod 加载器的缓存文件夹
+        Dim OptiFineEntry = Request.Components.OptiFineEntry?.ToObject(Of DlOptiFineListEntry)()
+        Dim OptiFineFolder = If(OptiFineEntry Is Nothing, Nothing, $"{TempMcFolder}versions\{OptiFineEntry.InstanceName}")
+        Dim ForgeVersion = If(Request.Components.ModLoaderVersions.ContainsKey(ModLoaders.Forge), Request.Components.ModLoaderVersions(ModLoaders.Forge), Nothing)
+        Dim ForgeFolder = If(ForgeVersion Is Nothing, Nothing, $"{TempMcFolder}versions\forge-{ForgeVersion}")
+        Dim NeoForgeVersion = If(Request.Components.ModLoaderVersions.ContainsKey(ModLoaders.NeoForge), Request.Components.ModLoaderVersions(ModLoaders.NeoForge), Nothing)
+        Dim NeoForgeFolder = If(NeoForgeVersion Is Nothing, Nothing, $"{TempMcFolder}versions\neoforge-{NeoForgeVersion}")
+        Dim LiteLoaderVersion = If(Request.Components.ModLoaderVersions.ContainsKey(ModLoaders.LiteLoader), Request.Components.ModLoaderVersions(ModLoaders.LiteLoader), Nothing)
+        Dim LiteLoaderFolder = If(LiteLoaderVersion Is Nothing, Nothing, $"{TempMcFolder}versions\{Request.Components.MinecraftVersion}-LiteLoader")
+        Dim FabricVersion = If(Request.Components.ModLoaderVersions.ContainsKey(ModLoaders.Fabric), Request.Components.ModLoaderVersions(ModLoaders.Fabric), Nothing)
+        Dim FabricFolder = If(FabricVersion Is Nothing, Nothing, $"{TempMcFolder}versions\fabric-loader-{FabricVersion}-{Request.Components.MinecraftVersion}")
 
 
         '判断 OptiFine 是否作为 Mod 进行下载
         '判断 OptiFine 是否作为 Mod 进行下载
-        Dim Modable As Boolean = Request.FabricVersion IsNot Nothing OrElse Request.ForgeEntry IsNot Nothing OrElse Request.NeoForgeEntry IsNot Nothing OrElse Request.LiteLoaderEntry IsNot Nothing
-        Dim ModsTempFolder As String = TempMcFolder & "mods\"
-        Dim OptiFineAsMod As Boolean = Request.OptiFineEntry IsNot Nothing AndAlso Modable '选择了 OptiFine 与任意 Mod 加载器
+        Dim Modable = Request.Components.ModLoaderVersions.Any
+        Dim ModsTempFolder As String = $"{TempMcFolder}mods\"
+        Dim OptiFineAsMod = OptiFineEntry IsNot Nothing AndAlso Modable '选择了 OptiFine 与任意 Mod 加载器
         If OptiFineAsMod Then
         If OptiFineAsMod Then
             Logger.Info("OptiFine 将作为 Mod 进行下载")
             Logger.Info("OptiFine 将作为 Mod 进行下载")
-            If Request.LiteLoaderEntry IsNot Nothing Then
-                OptiFineFolder = ModsTempFolder & Request.MinecraftName & "\" '#8147
+            If LiteLoaderVersion IsNot Nothing Then
+                OptiFineFolder = $"{ModsTempFolder}{Request.Components.MinecraftVersion}\" '#8147
             Else
             Else
                 OptiFineFolder = ModsTempFolder
                 OptiFineFolder = ModsTempFolder
             End If
             End If
@@ -1902,256 +1766,400 @@ Retry:
         If NeoForgeFolder IsNot Nothing Then Logger.Info($"NeoForge 缓存:{NeoForgeFolder}")
         If NeoForgeFolder IsNot Nothing Then Logger.Info($"NeoForge 缓存:{NeoForgeFolder}")
         If FabricFolder IsNot Nothing Then Logger.Info($"Fabric 缓存:{FabricFolder}")
         If FabricFolder IsNot Nothing Then Logger.Info($"Fabric 缓存:{FabricFolder}")
         If LiteLoaderFolder IsNot Nothing Then Logger.Info($"LiteLoader 缓存:{LiteLoaderFolder}")
         If LiteLoaderFolder IsNot Nothing Then Logger.Info($"LiteLoader 缓存:{LiteLoaderFolder}")
-        Logger.Info($"对应的原版版本:{Request.MinecraftName}")
+        Logger.Info($"对应的原版版本:{Request.Components.MinecraftVersion}")
 
 
         '重复版本检查
         '重复版本检查
+        Dim VersionFolder As String = PathUtils.AddSlashSuffix(Request.InstanceFolder)
         If FileUtils.Exists($"{VersionFolder}{Request.NewInstanceName}.json") Then
         If FileUtils.Exists($"{VersionFolder}{Request.NewInstanceName}.json") Then
             Hint("版本 " & Request.NewInstanceName & " 已经存在!", HintType.Red)
             Hint("版本 " & Request.NewInstanceName & " 已经存在!", HintType.Red)
             Throw New OperationCanceledException
             Throw New OperationCanceledException
         End If
         End If
 
 
         Dim LoaderList As New List(Of LoaderBase)
         Dim LoaderList As New List(Of LoaderBase)
+        Dim ManagedFileSources As New Dictionary(Of String, String)
         '添加忽略标识
         '添加忽略标识
-        LoaderList.Add(New LoaderTask(Of Integer, Integer)("添加忽略标识", Sub() FileUtils.Write(VersionFolder & ".pclignore", "用于临时地在 PCL 的版本列表中屏蔽此版本。")) With {.Show = False, .Block = False})
+        LoaderList.Add(New LoaderTask(Of Integer, Integer)("添加忽略标识", Sub() FileUtils.Write(VersionFolder & ".pclignore", "用于临时地在 PCL 的版本列表中屏蔽此版本。")) With {.Show = False})
         'Fabric API
         'Fabric API
-        If Request.FabricApi IsNot Nothing Then
-            LoaderList.Add(New LoaderDownload("下载 Fabric API", New List(Of NetFile) From {Request.FabricApi.ToNetFile(ModsTempFolder, ResourceVersion.DownloadReason.Dependency, Request.MinecraftName, ModLoaders.Fabric)}) With {.ProgressWeight = 3, .Block = False})
+        If Request.Components.FabricApiVersion IsNot Nothing Then
+            Dim DownloadFile = Request.FabricApi.ToNetFile(ModsTempFolder, ResourceVersion.DownloadReason.Dependency, Request.Components.MinecraftVersion, ModLoaders.Fabric)
+            ManagedFileSources.Add("FabricApi", DownloadFile.LocalPath)
+            LoaderList.Add(New LoaderDownload("下载 Fabric API", New List(Of NetFile) From {DownloadFile}) With {
+                .ProgressWeight = 3, .Show = True, .Block = False})
         End If
         End If
         'OptiFabric
         'OptiFabric
-        If Request.OptiFabric IsNot Nothing Then
-            LoaderList.Add(New LoaderDownload("下载 OptiFabric", New List(Of NetFile) From {Request.OptiFabric.ToNetFile(ModsTempFolder, ResourceVersion.DownloadReason.Dependency, Request.MinecraftName, ModLoaders.Fabric)}) With {.ProgressWeight = 3, .Block = False})
+        If Request.Components.OptiFabricVersion IsNot Nothing Then
+            Dim DownloadFile = Request.OptiFabric.ToNetFile(ModsTempFolder, ResourceVersion.DownloadReason.Dependency, Request.Components.MinecraftVersion, ModLoaders.Fabric)
+            ManagedFileSources.Add("OptiFabric", DownloadFile.LocalPath)
+            LoaderList.Add(New LoaderDownload("下载 OptiFabric", New List(Of NetFile) From {DownloadFile}) With {
+                .ProgressWeight = 3, .Show = True, .Block = False})
         End If
         End If
         '原版
         '原版
-        Dim ClientLoader = New LoaderCombo(Of String)("下载原版 " & Request.MinecraftName, McDownloadClientLoader(Request.MinecraftName, Request.MinecraftJson, Request.NewInstanceName)) With {.Show = False, .ProgressWeight = 39,
-            .Block = Request.ForgeVersion Is Nothing AndAlso Request.NeoForgeVersion Is Nothing AndAlso Request.OptiFineEntry Is Nothing AndAlso Request.FabricVersion Is Nothing AndAlso Request.LiteLoaderEntry Is Nothing}
+        Dim ClientLoader = New LoaderCombo(Of String)("下载原版 " & Request.Components.MinecraftVersion,
+            McDownloadClientLoader(Request.Components.MinecraftVersion, Request.MinecraftJson, VersionFolder)) With {
+            .ProgressWeight = 39, .Show = False, .Block = Not Modable AndAlso OptiFineEntry Is Nothing}
         LoaderList.Add(ClientLoader)
         LoaderList.Add(ClientLoader)
         'OptiFine
         'OptiFine
-        If Request.OptiFineEntry IsNot Nothing Then
-            If OptiFineAsMod Then
-                LoaderList.Add(New LoaderCombo(Of String)("下载 OptiFine " & Request.OptiFineEntry.DisplayName, McDownloadOptiFineSaveLoader(Request.OptiFineEntry, OptiFineFolder & Request.OptiFineEntry.FileName)) With {.Show = False, .ProgressWeight = 16,
-                    .Block = Request.ForgeVersion Is Nothing AndAlso Request.NeoForgeVersion Is Nothing AndAlso Request.FabricVersion Is Nothing AndAlso Request.LiteLoaderEntry Is Nothing})
-            Else
-                LoaderList.Add(New LoaderCombo(Of String)("下载 OptiFine " & Request.OptiFineEntry.DisplayName, McDownloadOptiFineLoader(Request.OptiFineEntry, TempMcFolder, ClientLoader, Request.VersionFolder)) With {.Show = False, .ProgressWeight = 24,
-                    .Block = Request.ForgeVersion Is Nothing AndAlso Request.NeoForgeVersion Is Nothing AndAlso Request.FabricVersion Is Nothing AndAlso Request.LiteLoaderEntry Is Nothing})
-            End If
+        If OptiFineEntry IsNot Nothing Then
+            If OptiFineAsMod Then ManagedFileSources.Add("OptiFine", OptiFineFolder & OptiFineEntry.FileName)
+            LoaderList.Add(New LoaderCombo(Of String)("下载 OptiFine " & OptiFineEntry.DisplayName,
+                If(OptiFineAsMod,
+                    McDownloadOptiFineSaveLoader(OptiFineEntry, OptiFineFolder & OptiFineEntry.FileName),
+                    McDownloadOptiFineLoader(OptiFineEntry, TempMcFolder, ClientLoader, VersionFolder))) With {
+                .ProgressWeight = If(Modable, 16, 24), .Show = False, .Block = Not Modable})
         End If
         End If
         'Forge
         'Forge
-        If Request.ForgeVersion IsNot Nothing Then
-            LoaderList.Add(New LoaderCombo(Of String)("下载 Forge " & Request.ForgeVersion, McDownloadForgelikeLoader(False, Request.ForgeVersion, "forge-" & Request.ForgeVersion, Request.MinecraftName, Request.ForgeEntry, TempMcFolder, ClientLoader, Request.VersionFolder)) With {.Show = False, .ProgressWeight = 25,
-                .Block = Request.FabricVersion Is Nothing AndAlso Request.LiteLoaderEntry Is Nothing AndAlso Request.NeoForgeEntry Is Nothing})
+        If ForgeVersion IsNot Nothing Then
+            LoaderList.Add(New LoaderCombo(Of String)("下载 Forge " & ForgeVersion,
+                McDownloadForgelikeLoader(False, ForgeVersion, "forge-" & ForgeVersion, Request.Components.MinecraftVersion, Request.ForgeEntry, TempMcFolder, ClientLoader, VersionFolder)) With {
+                    .ProgressWeight = 25, .Show = False, .Block = FabricVersion Is Nothing AndAlso Request.LiteLoaderEntry Is Nothing AndAlso Request.NeoForgeEntry Is Nothing})
         End If
         End If
         'NeoForge
         'NeoForge
-        If Request.NeoForgeVersion IsNot Nothing Then
-            LoaderList.Add(New LoaderCombo(Of String)("下载 NeoForge " & Request.NeoForgeVersion, McDownloadForgelikeLoader(True, Request.NeoForgeVersion, "neoforge-" & Request.NeoForgeVersion, Request.MinecraftName, Request.NeoForgeEntry, TempMcFolder, ClientLoader, Request.VersionFolder)) With {.Show = False, .ProgressWeight = 25,
-                .Block = Request.ForgeEntry Is Nothing AndAlso Request.FabricVersion Is Nothing AndAlso Request.LiteLoaderEntry Is Nothing})
+        If NeoForgeVersion IsNot Nothing Then
+            LoaderList.Add(New LoaderCombo(Of String)("下载 NeoForge " & NeoForgeVersion,
+                McDownloadForgelikeLoader(True, NeoForgeVersion, "neoforge-" & NeoForgeVersion, Request.Components.MinecraftVersion, Request.NeoForgeEntry, TempMcFolder, ClientLoader, VersionFolder)) With {
+                .ProgressWeight = 25, .Show = False, .Block = Request.ForgeEntry Is Nothing AndAlso FabricVersion Is Nothing AndAlso Request.LiteLoaderEntry Is Nothing})
         End If
         End If
         'LiteLoader
         'LiteLoader
-        If Request.LiteLoaderEntry IsNot Nothing Then
-            LoaderList.Add(New LoaderCombo(Of String)("下载 LiteLoader " & Request.MinecraftName, McDownloadLiteLoaderLoader(Request.LiteLoaderEntry, TempMcFolder)) With {.Show = False, .ProgressWeight = 1,
-                .Block = Request.FabricVersion Is Nothing})
+        If LiteLoaderVersion IsNot Nothing Then
+            LoaderList.Add(New LoaderCombo(Of String)("下载 LiteLoader " & Request.Components.MinecraftVersion,
+                McDownloadLiteLoaderLoader(Request.LiteLoaderEntry, TempMcFolder)) With {
+                    .ProgressWeight = 1, .Show = False, .Block = FabricVersion Is Nothing})
         End If
         End If
         'Fabric
         'Fabric
-        If Request.FabricVersion IsNot Nothing Then
-            LoaderList.Add(New LoaderCombo(Of String)("下载 Fabric " & Request.FabricVersion, McDownloadFabricLoader(Request.FabricVersion, Request.MinecraftName, TempMcFolder)) With {.Show = False, .ProgressWeight = 2,
-                .Block = True})
+        If FabricVersion IsNot Nothing Then
+            LoaderList.Add(New LoaderCombo(Of String)("下载 Fabric " & FabricVersion,
+                McDownloadFabricLoader(FabricVersion, Request.Components.MinecraftVersion, TempMcFolder)) With {
+                .ProgressWeight = 2, .Show = False, .Block = True})
         End If
         End If
         '合并安装
         '合并安装
+        Dim MinecraftFolder = McFolderSelected
+        Dim TargetFolder As String = Nothing
         LoaderList.Add(New LoaderTask(Of String, String)("安装游戏",
         LoaderList.Add(New LoaderTask(Of String, String)("安装游戏",
         Sub(Task As LoaderTask(Of String, String))
         Sub(Task As LoaderTask(Of String, String))
-            '合并 JSON
-            MergeJson(VersionFolder, VersionFolder, OptiFineFolder, OptiFineAsMod, ForgeFolder, Request.ForgeVersion, NeoForgeFolder, Request.NeoForgeVersion, FabricFolder, LiteLoaderFolder)
+            '合并 JSON:将多个版本 JSON 进行合并,如果已存在则直接覆盖
+            MergeJson(VersionFolder, OptiFineFolder, OptiFineAsMod, ForgeFolder, NeoForgeFolder, FabricFolder, LiteLoaderFolder)
             Task.Progress = 0.2
             Task.Progress = 0.2
             '迁移文件
             '迁移文件
-            DirectoryUtils.Copy(TempMcFolder & "libraries", McFolderSelected & "libraries")
+            DirectoryUtils.Copy(TempMcFolder & "libraries", MinecraftFolder & "libraries")
             Task.Progress = 0.8
             Task.Progress = 0.8
             '创建 Mod 和资源包文件夹
             '创建 Mod 和资源包文件夹
-            Dim ModsFolder = New McInstance(VersionFolder).PathIndie & "mods\" '版本隔离信息在此时被决定
+            TargetFolder = If(Request.IsIntermediateOutput, VersionFolder, New McInstance(VersionFolder).PathIndie) '版本隔离信息在此时被决定
+            Dim ModsFolder = TargetFolder & "mods\"
             If DirectoryUtils.Exists(ModsTempFolder) Then
             If DirectoryUtils.Exists(ModsTempFolder) Then
                 DirectoryUtils.Copy(ModsTempFolder, ModsFolder)
                 DirectoryUtils.Copy(ModsTempFolder, ModsFolder)
             ElseIf Modable Then
             ElseIf Modable Then
                 DirectoryUtils.Create(ModsFolder)
                 DirectoryUtils.Create(ModsFolder)
                 Logger.Info($"自动创建 Mod 文件夹:{ModsFolder}")
                 Logger.Info($"自动创建 Mod 文件夹:{ModsFolder}")
             End If
             End If
-            Dim ResourcepacksFolder = New McInstance(VersionFolder).PathIndie & "resourcepacks\"
+            Dim ResourcepacksFolder = TargetFolder & "resourcepacks\"
             DirectoryUtils.Create(ResourcepacksFolder)
             DirectoryUtils.Create(ResourcepacksFolder)
             Logger.Info($"自动创建资源包文件夹:{ResourcepacksFolder}")
             Logger.Info($"自动创建资源包文件夹:{ResourcepacksFolder}")
         End Sub) With {.ProgressWeight = 2, .Block = True})
         End Sub) With {.ProgressWeight = 2, .Block = True})
         '补全文件
         '补全文件
-        If Request.OptiFineEntry IsNot Nothing OrElse (Request.ForgeVersion IsNot Nothing AndAlso Request.ForgeVersion.BeforeFirst(".") >= 20) OrElse Request.NeoForgeVersion IsNot Nothing OrElse Request.FabricVersion IsNot Nothing OrElse Request.LiteLoaderEntry IsNot Nothing Then
-            Dim LoadersLib As New List(Of LoaderBase)
-            LoadersLib.Add(New LoaderTask(Of String, List(Of NetFile))("分析游戏支持库文件(副加载器)", Sub(Task) Task.Output = McLibNetFilesFromInstance(New McInstance(VersionFolder))) With {.ProgressWeight = 1, .Show = False})
-            LoadersLib.Add(New LoaderDownload("下载游戏支持库文件(副加载器)", New List(Of NetFile)) With {.ProgressWeight = 7, .Show = False})
-            LoaderList.Add(New LoaderCombo(Of String)("下载游戏支持库文件", LoadersLib) With {.ProgressWeight = 8})
-        End If
+        Dim LoadersLib As New List(Of LoaderBase)
+        LoadersLib.Add(New LoaderTask(Of String, List(Of NetFile))("分析游戏支持库文件(副加载器)", Sub(Task) Task.Output = McLibNetFilesFromInstance(New McInstance(VersionFolder))) With {.ProgressWeight = 1, .Show = False})
+        LoadersLib.Add(New LoaderDownload("下载游戏支持库文件(副加载器)", New List(Of NetFile)) With {.ProgressWeight = 7, .Show = False})
+        LoaderList.Add(New LoaderCombo(Of String)("下载游戏支持库文件", LoadersLib) With {.ProgressWeight = 8})
+        '记录安装清单
+        LoaderList.Add(New LoaderTask(Of Integer, Integer)("记录安装清单",
+        Sub()
+            Dim ManagedFiles As New List(Of McInstallManifest.ManagedFile)
+            '记录由 PCL 管理的 Mod 信息
+            For Each Entry In ManagedFileSources
+                Dim RelativePath = Entry.Value.Substring(ModsTempFolder.Length)
+                Dim FullPath = PathUtils.ForCompare(Path.Combine(TargetFolder, "mods", RelativePath))
+                ManagedFiles.Add(New McInstallManifest.ManagedFile("mods/" & RelativePath.Replace("\", "/").ToLowerInvariant(),
+                    CryptographyUtils.ComputeFileHash(FullPath, CryptographyUtils.HashMethod.Sha1), FileUtils.GetInfo(FullPath).LastWriteTimeUtc, Entry.Key))
+            Next
+            '处理加载器版本信息
+            Dim TempLoaderVersions As New Dictionary(Of ModLoaders, String)(Request.Components.ModLoaderVersions)
+            If ForgeVersion IsNot Nothing AndAlso ForgeVersion.StartsWithF("1.") AndAlso ForgeVersion.Contains("-") Then
+                TempLoaderVersions(ModLoaders.Forge) = ForgeVersion.AfterLast("-")
+            End If
+            If NeoForgeVersion IsNot Nothing AndAlso NeoForgeVersion.StartsWithF("1.20.1-") Then
+                TempLoaderVersions(ModLoaders.NeoForge) = NeoForgeVersion.Replace("1.20.1-", "")
+            End If
+            '存储 Manifest
+            Dim Target = New McComponents(Request.Components.MinecraftVersion, TempLoaderVersions,
+                Request.Components.OptiFineEntry, Request.Components.FabricApiVersion, Request.Components.OptiFabricVersion)
+            Dim InstanceConfig = New McInstance(VersionFolder).Config
+            Configs.McInstallManifest.Set(New McInstallManifest(Versions.McInstallManifestVersion, Target, Date.UtcNow, ManagedFiles), InstanceConfig)
+            InstanceConfig.Save()
+        End Sub) With {.Show = False})
         '删除忽略标识
         '删除忽略标识
-        LoaderList.Add(New LoaderTask(Of Integer, Integer)("删除忽略标识", Sub() FileUtils.Delete(VersionFolder & ".pclignore")) With {.Show = False})
+        If Not Request.IsIntermediateOutput Then
+            LoaderList.Add(New LoaderTask(Of Integer, Integer)("删除忽略标识", Sub() FileUtils.Delete(VersionFolder & ".pclignore")) With {.Show = False})
+        End If
         '总加载器
         '总加载器
         Return LoaderList
         Return LoaderList
     End Function
     End Function
-
-    ''' <summary>
-    ''' 将多个版本 JSON 进行合并,如果目标已存在则直接覆盖。失败会抛出异常。
-    ''' </summary>
-    Private Sub MergeJson(OutputFolder As String, MinecraftFolder As String, Optional OptiFineFolder As String = Nothing, Optional OptiFineAsMod As Boolean = False, Optional ForgeFolder As String = Nothing, Optional ForgeVersion As String = Nothing, Optional NeoForgeFolder As String = Nothing, Optional NeoForgeVersion As String = Nothing, Optional FabricFolder As String = Nothing, Optional LiteLoaderFolder As String = Nothing)
-        Logger.Info($"开始进行版本合并,输出:{OutputFolder},Minecraft:{MinecraftFolder}" &
-            If(OptiFineFolder IsNot Nothing, ",OptiFine:" & OptiFineFolder, "") &
-            If(ForgeFolder IsNot Nothing, ",Forge:" & ForgeFolder, "") &
-            If(NeoForgeFolder IsNot Nothing, ",NeoForge:" & NeoForgeFolder, "") &
-            If(LiteLoaderFolder IsNot Nothing, ",LiteLoader:" & LiteLoaderFolder, "") &
-            If(FabricFolder IsNot Nothing, ",Fabric:" & FabricFolder, ""))
-        DirectoryUtils.Create(OutputFolder)
-
-        Dim HasOptiFine As Boolean = OptiFineFolder IsNot Nothing AndAlso Not OptiFineAsMod, HasForge As Boolean = ForgeFolder IsNot Nothing, HasNeoForge As Boolean = NeoForgeFolder IsNot Nothing, HasLiteLoader As Boolean = LiteLoaderFolder IsNot Nothing, HasFabric As Boolean = FabricFolder IsNot Nothing
-        Dim OutputName As String, MinecraftName As String, OptiFineName As String, ForgeName As String, NeoForgeName As String, LiteLoaderName As String, FabricName As String
-        Dim OutputJsonPath As String, MinecraftJsonPath As String, OptiFineJsonPath As String = Nothing, ForgeJsonPath As String = Nothing, NeoForgeJsonPath As String = Nothing, LiteLoaderJsonPath As String = Nothing, FabricJsonPath As String = Nothing
-        Dim OutputJar As String, MinecraftJar As String
-#Region "初始化路径信息"
-        If Not OutputFolder.EndsWithF("\") Then OutputFolder += "\"
-        OutputName = PathUtils.GetLastPart(OutputFolder)
-        OutputJsonPath = OutputFolder & OutputName & ".json"
-        OutputJar = OutputFolder & OutputName & ".jar"
-
-        If Not MinecraftFolder.EndsWithF("\") Then MinecraftFolder += "\"
-        MinecraftName = PathUtils.GetLastPart(MinecraftFolder)
-        MinecraftJsonPath = MinecraftFolder & MinecraftName & ".json"
-        MinecraftJar = MinecraftFolder & MinecraftName & ".jar"
-
-        If HasOptiFine Then
-            If Not OptiFineFolder.EndsWithF("\") Then OptiFineFolder += "\"
-            OptiFineName = PathUtils.GetLastPart(OptiFineFolder)
-            OptiFineJsonPath = OptiFineFolder & OptiFineName & ".json"
-        End If
-
-        If HasForge Then
-            If Not ForgeFolder.EndsWithF("\") Then ForgeFolder += "\"
-            ForgeName = PathUtils.GetLastPart(ForgeFolder)
-            ForgeJsonPath = ForgeFolder & ForgeName & ".json"
-        End If
-
-        If HasNeoForge Then
-            If Not NeoForgeFolder.EndsWithF("\") Then NeoForgeFolder += "\"
-            NeoForgeName = PathUtils.GetLastPart(NeoForgeFolder)
-            NeoForgeJsonPath = NeoForgeFolder & NeoForgeName & ".json"
-        End If
-
-        If HasLiteLoader Then
-            If Not LiteLoaderFolder.EndsWithF("\") Then LiteLoaderFolder += "\"
-            LiteLoaderName = PathUtils.GetLastPart(LiteLoaderFolder)
-            LiteLoaderJsonPath = LiteLoaderFolder & LiteLoaderName & ".json"
-        End If
-
-        If HasFabric Then
-            If Not FabricFolder.EndsWithF("\") Then FabricFolder += "\"
-            FabricName = PathUtils.GetLastPart(FabricFolder)
-            FabricJsonPath = FabricFolder & FabricName & ".json"
-        End If
-#End Region
-
-        Dim OutputJson As JObject, MinecraftJson As JObject, OptiFineJson As JObject = Nothing, ForgeJson As JObject = Nothing, NeoForgeJson As JObject = Nothing, LiteLoaderJson As JObject = Nothing, FabricJson As JObject = Nothing
-#Region "读取文件并检查文件是否合规"
-        Dim MinecraftJsonText As String = If(FileUtils.TryReadAsString(MinecraftJsonPath), "")
-        If Not MinecraftJsonText.StartsWithF("{") Then Throw New Exception("Minecraft json 有误,地址:" & MinecraftJsonPath & ",前段内容:" & MinecraftJsonText.Substring(0, Math.Min(MinecraftJsonText.Length, 1000)))
-        MinecraftJson = MinecraftJsonText.DeserializeJson()
-
-        If HasOptiFine Then
-            Dim OptiFineJsonText As String = If(FileUtils.TryReadAsString(OptiFineJsonPath), "")
-            If Not OptiFineJsonText.StartsWithF("{") Then Throw New Exception("OptiFine json 有误,地址:" & OptiFineJsonPath & ",前段内容:" & OptiFineJsonText.Substring(0, Math.Min(OptiFineJsonText.Length, 1000)))
-            OptiFineJson = OptiFineJsonText.DeserializeJson()
-        End If
-
-        If HasForge Then
-            Dim ForgeJsonText As String = If(FileUtils.TryReadAsString(ForgeJsonPath), "")
-            If Not ForgeJsonText.StartsWithF("{") Then Throw New Exception("Forge json 有误,地址:" & ForgeJsonPath & ",前段内容:" & ForgeJsonText.Substring(0, Math.Min(ForgeJsonText.Length, 1000)))
-            ForgeJson = ForgeJsonText.DeserializeJson()
-        End If
-
-        If HasNeoForge Then
-            Dim NeoForgeJsonText As String = If(FileUtils.TryReadAsString(NeoForgeJsonPath), "")
-            If Not NeoForgeJsonText.StartsWithF("{") Then Throw New Exception("NeoForge json 有误,地址:" & NeoForgeJsonPath & ",前段内容:" & NeoForgeJsonText.Substring(0, Math.Min(NeoForgeJsonText.Length, 1000)))
-            NeoForgeJson = NeoForgeJsonText.DeserializeJson()
-        End If
-
-        If HasLiteLoader Then
-            Dim LiteLoaderJsonText As String = If(FileUtils.TryReadAsString(LiteLoaderJsonPath), "")
-            If Not LiteLoaderJsonText.StartsWithF("{") Then Throw New Exception("LiteLoader json 有误,地址:" & LiteLoaderJsonPath & ",前段内容:" & LiteLoaderJsonText.Substring(0, Math.Min(LiteLoaderJsonText.Length, 1000)))
-            LiteLoaderJson = LiteLoaderJsonText.DeserializeJson()
-        End If
-
-        If HasFabric Then
-            Dim FabricJsonText As String = If(FileUtils.TryReadAsString(FabricJsonPath), "")
-            If Not FabricJsonText.StartsWithF("{") Then Throw New Exception("Fabric json 有误,地址:" & FabricJsonPath & ",前段内容:" & FabricJsonText.Substring(0, Math.Min(FabricJsonText.Length, 1000)))
-            FabricJson = FabricJsonText.DeserializeJson()
-        End If
-#End Region
-
-#Region "处理 JSON 文件"
-        '获取 minecraftArguments
-        Dim AllArguments As String =
-            If(MinecraftJson("minecraftArguments"), " ").ToString & " " &
-            If(OptiFineJson IsNot Nothing, If(OptiFineJson("minecraftArguments"), " ").ToString, " ") & " " &
-            If(ForgeJson IsNot Nothing, If(ForgeJson("minecraftArguments"), " ").ToString, " ") & " " &
-            If(NeoForgeJson IsNot Nothing, If(NeoForgeJson("minecraftArguments"), " ").ToString, " ") & " " &
-            If(LiteLoaderJson IsNot Nothing, If(LiteLoaderJson("minecraftArguments"), " ").ToString, " ")
-        '分割参数字符串
-        Dim RawArguments As List(Of String) = AllArguments.Split(" ", True).Select(Function(l) l.Trim).ToList
+    Private Sub MergeJson(VersionFolder As String, OptiFineFolder As String, OptiFineAsMod As Boolean,
+                          ForgeFolder As String, NeoForgeFolder As String, FabricFolder As String, LiteLoaderFolder As String)
+        Dim ProfileSources As New List(Of (Name As String, Folder As String)) From {("Minecraft", VersionFolder)}
+        If OptiFineFolder IsNot Nothing AndAlso Not OptiFineAsMod Then ProfileSources.Add(("OptiFine", OptiFineFolder))
+        If ForgeFolder IsNot Nothing Then ProfileSources.Add(("Forge", ForgeFolder))
+        If NeoForgeFolder IsNot Nothing Then ProfileSources.Add(("NeoForge", NeoForgeFolder))
+        If LiteLoaderFolder IsNot Nothing Then ProfileSources.Add(("LiteLoader", LiteLoaderFolder))
+        If FabricFolder IsNot Nothing Then ProfileSources.Add(("Fabric", FabricFolder))
+        Logger.Info($"开始进行版本合并,输出:{VersionFolder}," & String.Join(",", ProfileSources.Select(Function(Source) $"{Source.Name}:{Source.Folder}")))
+        '读取文件并检查文件是否合规
+        Dim LoadedProfiles As New List(Of (Name As String, Json As JObject))
+        For Each Source In ProfileSources
+            Dim ProfilePath = Path.Combine(Source.Folder, PathUtils.GetLastPart(Source.Folder) & ".json")
+            Dim ProfileText = If(FileUtils.TryReadAsString(ProfilePath), "")
+            If Not ProfileText.StartsWithF("{") Then Throw New Exception($"{Source.Name} json 有误,地址:{ProfilePath},前段内容:{ProfileText.Substring(0, Math.Min(ProfileText.Length, 1000))}")
+            LoadedProfiles.Add((Source.Name, ProfileText.DeserializeJson(Of JObject)()))
+        Next
+        '先获取 minecraftArguments,避免合并时原版参数被覆盖
+        Dim AllArguments = LoadedProfiles.Select(Function(Profile) If(Profile.Json("minecraftArguments")?.ToString(), " ")).Join(" ")
+        '合并(相关讨论见 #2801)
+        Dim MergedJson = LoadedProfiles(0).Json 'Minecraft 原版 JSON
+        For Each Loaded In LoadedProfiles.Skip(1)
+            Loaded.Json.Remove("releaseTime")
+            Loaded.Json.Remove("time")
+            MergedJson.Merge(Loaded.Json)
+        Next
+        '填入 minecraftArguments
         Dim SplitArguments As New List(Of String)
         Dim SplitArguments As New List(Of String)
-        For i = 0 To RawArguments.Count - 1
-            If RawArguments(i).StartsWithF("-") Then
-                SplitArguments.Add(RawArguments(i))
-            ElseIf SplitArguments.Any AndAlso SplitArguments.Last.StartsWithF("-") AndAlso Not SplitArguments.Last.Contains(" ") Then
-                SplitArguments(SplitArguments.Count - 1) = SplitArguments.Last & " " & RawArguments(i)
-            Else
-                SplitArguments.Add(RawArguments(i))
+        For Each Argument In AllArguments.Split(" ", True).Select(Function(Value) Value.Trim())
+            If Not Argument.StartsWithF("-") AndAlso SplitArguments.Any Then
+                Dim LastArgument = SplitArguments.Last
+                If LastArgument.StartsWithF("-") AndAlso Not LastArgument.Contains(" ") Then
+                    SplitArguments(SplitArguments.Count - 1) = LastArgument & " " & Argument
+                    Continue For
+                End If
             End If
             End If
+            SplitArguments.Add(Argument)
         Next
         Next
-        Dim RealArguments As String = SplitArguments.Distinct.Join(" ")
-        '合并
-        '相关讨论见 #2801
-        OutputJson = MinecraftJson
-        If HasOptiFine Then
-            '合并 OptiFine
-            OptiFineJson.Remove("releaseTime")
-            OptiFineJson.Remove("time")
-            OutputJson.Merge(OptiFineJson)
-        End If
-        If HasForge Then
-            '合并 Forge
-            ForgeJson.Remove("releaseTime")
-            ForgeJson.Remove("time")
-            OutputJson.Merge(ForgeJson)
-        End If
-        If HasNeoForge Then
-            '合并 NeoForge
-            NeoForgeJson.Remove("releaseTime")
-            NeoForgeJson.Remove("time")
-            OutputJson.Merge(NeoForgeJson)
-        End If
-        If HasLiteLoader Then
-            '合并 LiteLoader
-            LiteLoaderJson.Remove("releaseTime")
-            LiteLoaderJson.Remove("time")
-            OutputJson.Merge(LiteLoaderJson)
-        End If
-        If HasFabric Then
-            '合并 Fabric
-            FabricJson.Remove("releaseTime")
-            FabricJson.Remove("time")
-            OutputJson.Merge(FabricJson)
-        End If
+        If SplitArguments.Any() Then MergedJson("minecraftArguments") = SplitArguments.Distinct().Join(" ")
         '修改
         '修改
-        If RealArguments IsNot Nothing AndAlso RealArguments.Replace(" ", "") <> "" Then OutputJson("minecraftArguments") = RealArguments
-        OutputJson.Remove("_comment_")
-        OutputJson.Remove("inheritsFrom")
-        OutputJson.Remove("jar")
-        OutputJson("id") = OutputName
-#End Region
+        MergedJson.Remove("_comment_")
+        MergedJson.Remove("inheritsFrom")
+        MergedJson.Remove("jar")
+        Dim VersionName = PathUtils.GetLastPart(VersionFolder)
+        MergedJson("id") = VersionName
+        '保存
+        FileUtils.Write(Path.Combine(VersionFolder, VersionName & ".json"), MergedJson.ToString())
+        Logger.Info($"版本合并 {VersionName} 完成")
+    End Sub
+    ''' <summary>
+    ''' 进行合并安装。返回是否已经开始安装。
+    ''' </summary>
+    Public Function McInstallStart(Request As McInstallRequest) As Boolean
+        Try
+            Dim Loader As New LoaderCombo(Of String)(Request.NewInstanceName & " 安装", McInstallLoadersGet(Request)) With {.OnStateChanged = AddressOf McInstallState}
+            '启动
+            Loader.Start(Request.InstanceFolder)
+            LoaderTaskbarAdd(Loader)
+            FrmMain.BtnExtraDownload.ShowRefresh()
+            FrmMain.BtnExtraDownload.Ribble()
+            Return True
+        Catch ex As Exception
+            Logger.Error(ex, "开始合并安装失败")
+            Return False
+        End Try
+    End Function
+    ''' <summary>
+    ''' 进行修改安装。返回是否已经开始安装。
+    ''' </summary>
+    Public Function McModificationStart(Request As McInstallRequest, Instance As McInstance) As Boolean
+        Try
+            '检查版本目录下的 Natives,防止修改正在运行的游戏
+            Dim TargetFolder = Instance.PathIndie
+            Dim SubLoaders As New List(Of LoaderBase)
+            SubLoaders.Add(New LoaderTask(Of Integer, Integer)("检查 Natives 文件占用",
+            Sub()
+                For Each File In DirectoryUtils.EnumerateFiles(GetNativesFolder(Instance), includeSubDirectories:=True, searchPattern:="*.dll")
+                    If Not FileUtils.IsAvailable(File) Then Throw New Exception($"Natives 文件正在被占用,请关闭游戏后再次尝试:{File}")
+                Next
+            End Sub) With {.Show = False})
+            '安装修改后的 MC 版本
+            Dim BuildFolder = RequestTaskTempFolder() & Instance.Name & "\"
+            Dim BuildRequest As New McInstallRequest With {
+                .NewInstanceName = Instance.Name,
+                .InstanceFolder = BuildFolder,
+                .Components = Request.Components,
+                .MinecraftJson = Request.MinecraftJson,
+                .ForgeEntry = Request.ForgeEntry,
+                .NeoForgeEntry = Request.NeoForgeEntry,
+                .FabricApi = Request.FabricApi,
+                .OptiFabric = Request.OptiFabric,
+                .LiteLoaderEntry = Request.LiteLoaderEntry,
+                .IsIntermediateOutput = True
+            }
+            SubLoaders.AddRange(McInstallLoadersGet(BuildRequest))
+            '版本合并
+            Dim RenameFailed As Boolean = False
+            SubLoaders.Add(New LoaderTask(Of Integer, Integer)("修改版本",
+            Sub(Task)
+                'TODO: 提交前显示文件修改列表(大量文件按“XXX 文件夹下 XXX 项”汇总),可以询问玩家是否覆盖原版本或作为新版本安装(不覆盖)?
+                McInstallCommit(Instance, Configs.McInstallManifest.Get(New McInstance(BuildFolder).Config), BuildFolder, TargetFolder)
+                '重命名和引用交接
+                Dim NewInstance As McInstance = Nothing
+                If Request.NewInstanceName Is Nothing Then
+                    NewInstance = New McInstance(Instance.PathVersion).Load()
+                Else
+                    Try
+                        NewInstance = Instance.Rename(Request.NewInstanceName)
+                    Catch ex As Exception
+                        RenameFailed = True
+                        Logger.Warn(ex, $"版本修改成功,但重命名为 {Request.NewInstanceName} 失败:{ex.GetDisplay(False)}", LogBehavior.None)
+                        NewInstance = New McInstance(Instance.PathVersion).Load()
+                    End Try
+                End If
+                If NewInstance Is Nothing Then NewInstance = New McInstance(Instance.PathVersion).Load()
+                RunInUiWait(
+                Sub()
+                    WriteIni(McFolderSelected & "PCL.ini", "InstanceCache", "")
+                    '未切换则选中修改后的版本
+                    If PageInstanceLeft.Instance IsNot Nothing AndAlso
+                       PathUtils.IsSame(PageInstanceLeft.Instance.PathVersion, Instance.PathVersion) Then
+                        PageInstanceLeft.Instance = NewInstance
+                    End If
+                    If McInstanceSelected IsNot Nothing AndAlso
+                       PathUtils.IsSame(McInstanceSelected.PathVersion, Instance.PathVersion) Then
+                        McInstanceSelected = NewInstance
+                    End If
+                End Sub)
+            End Sub) With {.ProgressWeight = 1, .Block = True})
+            '实际修改任务
+            Dim Loader As New LoaderCombo(Of String)(Instance.Name & " 修改", SubLoaders) With {.OnStateChanged =
+            Sub(CurrentLoader)
+                '流程收尾
+                Select Case CurrentLoader.State
+                    Case LoadState.Finished
+                        If Not RenameFailed Then
+                            Hint(CurrentLoader.Name & "成功!", HintType.Green)
+                        Else
+                            Hint(CurrentLoader.Name & "成功,但重命名失败!", HintType.Green)
+                        End If
+                    Case LoadState.Failed
+                        MyMsgBox(CurrentLoader.Error.GetDisplay(True), CurrentLoader.Name & "失败", IsWarn:=True)
+                    Case LoadState.Canceled
+                        Hint(CurrentLoader.Name & "已取消!", HintType.Blue)
+                End Select
+            End Sub}
+            Dim Worker As New RedoableWorker(Sub() Loader.WaitForExit(Instance.PathVersion))
+            '启动
+            If McInstanceLock.TryAcquire(Worker, Instance.PathVersion) Then
+                AddHandler Worker.Stopped, Sub() RunInUi(Sub() LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\"))
+                Worker.Start()
+                '加锁后刷新版本列表,隐藏正在修改的实例。
+                LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
+            Else
+                Logger.Error("版本被其他 Worker 占用,版本修改失败")
+                Return False
+            End If
+            LoaderTaskbarAdd(Loader)
+            FrmMain.BtnExtraDownload.ShowRefresh()
+            FrmMain.BtnExtraDownload.Ribble()
+            Return True
+        Catch ex As Exception
+            LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
+            Logger.Error(ex, "开始修改版本失败", LogBehavior.Toast)
+            Return False
+        End Try
+    End Function
 
 
-#Region "保存"
-        FileUtils.Write(OutputJsonPath, OutputJson.ToString)
-        FileUtils.Copy(MinecraftJar, OutputJar)
-        Logger.Info($"版本合并 {OutputName} 完成")
-#End Region
+    ''' <summary>
+    ''' 将安装好的临时版本文件夹覆盖到一个现有文件夹中。
+    ''' </summary>
+    Public Sub McInstallCommit(Instance As McInstance, NewManifest As McInstallManifest, BuildFolder As String, TargetFolder As String)
+        Dim OldFiles = If(Configs.McInstallManifest.Get(Instance.Config)?.ManagedFiles, {})
+        Dim NewFiles = NewManifest.ManagedFiles
+
+        '============== 整理变更的文件列表 ==============
+
+        Dim FilesToDelete As New List(Of String)
+        Dim FilesToMove As New List(Of (Source As String, Target As String))
+        'ManagedFiles
+        '新版里有:添加或覆盖新版文件
+        For Each NewFile In NewFiles
+            FilesToMove.Add((Path.Combine(BuildFolder, NewFile.RelativePath), Path.Combine(TargetFolder, NewFile.RelativePath)))
+        Next
+        '新版里没有,老版里有:删除本地文件到回收站
+        Dim NewFilePaths As New HashSet(Of String)(NewFiles.Select(Function(File) File.RelativePath))
+        For Each OldFile In OldFiles
+            If NewFilePaths.Contains(OldFile.RelativePath) Then Continue For
+            FilesToDelete.Add(PathUtils.ForCompare(Path.Combine(TargetFolder, OldFile.RelativePath)))
+        Next
+        '老版本 MC 的 assets 文件
+        If DirectoryUtils.Exists(BuildFolder & "resources\") Then DirectoryUtils.Copy(BuildFolder & "resources\", TargetFolder & "resources\")
+        'json 与 jar
+        Dim OldJsonPath As String = Instance.GetJsonPath()
+        FilesToDelete.Add(OldJsonPath)
+        FilesToDelete.Add(Path.ChangeExtension(OldJsonPath, ".jar"))
+        FilesToDelete.Add(Instance.PathVersion & Instance.Name & ".jar")
+        FilesToDelete = FilesToDelete.Distinct(StringComparer.OrdinalIgnoreCase).ToList()
+
+        '============== 实际修改 ==============
+
+        Instance.Config.Save()
+        For Each TargetPath In FilesToDelete
+            FileUtils.Delete(TargetPath, True)
+        Next
+        FileUtils.Move(BuildFolder & Instance.Name & ".jar", Instance.PathVersion & Instance.Name & ".jar")
+        FileUtils.Move(BuildFolder & Instance.Name & ".json", OldJsonPath)
+        For Each File In FilesToMove
+            DirectoryUtils.Create(PathUtils.RemoveLastPart(File.Target))
+            FileUtils.Copy(File.Source, File.Target)
+        Next
+        Dim ManagedFiles = NewFiles.Select(Function(File) New McInstallManifest.ManagedFile(File.RelativePath, File.Sha1,
+            FileUtils.GetInfo(Path.Combine(TargetFolder, File.RelativePath)).LastWriteTimeUtc, File.Component)).ToList()
+        Configs.McInstallManifest.Set(New McInstallManifest(NewManifest.SchemaVersion, NewManifest.Components, NewManifest.LastModifiedUtc, ManagedFiles), Instance.Config)
+        Instance.Config.Save()
+    End Sub
 
 
+    ''' <summary>
+    ''' 在加载器状态改变后显示一条提示。
+    ''' 不会进行任何其他操作。
+    ''' </summary>
+    Public Sub LoaderStateChangedHintOnly(Loader As LoaderBase)
+        Select Case Loader.State
+            Case LoadState.Finished
+                Hint(Loader.Name & "成功!", HintType.Green)
+            Case LoadState.Failed
+                Hint(Loader.Name & "失败:" & Loader.Error.GetDisplay(False), HintType.Red)
+            Case LoadState.Canceled
+                Hint(Loader.Name & "已取消!", HintType.Blue)
+        End Select
+    End Sub
+    ''' <summary>
+    ''' 安装加载器状态改变后进行提示和重载文件夹列表的方法。
+    ''' </summary>
+    Public Sub McInstallState(Loader As LoaderBase)
+        Select Case Loader.State
+            Case LoadState.Finished
+                WriteIni(McFolderSelected & "PCL.ini", "InstanceCache", "") '清空缓存(合并安装会先生成文件夹,这会在刷新时误判为可以使用缓存)
+                Hint(Loader.Name & "成功!", HintType.Green)
+                'TODO: 自动选择安装成功的版本
+            Case LoadState.Failed
+                MyMsgBox(Loader.Error.GetDisplay(True), Loader.Name & "失败", IsWarn:=True)
+            Case LoadState.Canceled
+                Hint(Loader.Name & "已取消!", HintType.Blue)
+            Case LoadState.Loading
+                Return '不重新加载版本列表
+        End Select
+        McInstallFailedClearFolder(Loader)
+        LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
+    End Sub
+    Public Sub McInstallFailedClearFolder(Loader)
+        If Loader.State <> LoadState.Failed AndAlso Loader.State <> LoadState.Canceled Then Return
+        RunInThread(
+        Sub()
+            Try
+                Thread.Sleep(1000) '防止存在尚未完全释放的文件,导致清理失败(例如整合包安装)
+                '删除版本文件夹
+                If DirectoryUtils.Exists(Loader.Input & "saves\") OrElse DirectoryUtils.Exists(Loader.Input & "versions\") Then
+                    Logger.Warn($"由于版本已被独立启动,不清理版本文件夹:{Loader.Input}")
+                Else
+                    Logger.Warn($"由于下载失败或取消,清理版本文件夹:{Loader.Input}")
+                    DirectoryUtils.Delete(Loader.Input)
+                End If
+            Catch ex As Exception
+                Logger.Warn(ex, "下载失败或取消后清理版本文件夹失败")
+            End Try
+        End Sub)
     End Sub
     End Sub
 
 
 #End Region
 #End Region

+ 195 - 34
Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml

@@ -1,7 +1,7 @@
 <local:MyPageRight
 <local:MyPageRight
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-    xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
+    xmlns:local="clr-namespace:PCL" xmlns:sys="clr-namespace:System;assembly=mscorlib" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
     mc:Ignorable="d" x:Class="PageDownloadInstall"
     mc:Ignorable="d" x:Class="PageDownloadInstall"
     PanScroll="PanBack">
     PanScroll="PanBack">
     <Grid>
     <Grid>
@@ -14,19 +14,96 @@
                         </StackPanel.RenderTransform>
                         </StackPanel.RenderTransform>
                     </StackPanel>
                     </StackPanel>
                     <StackPanel Grid.Row="1" x:Name="PanSelect" Visibility="Collapsed" Opacity="0" IsHitTestVisible="False" Margin="0,0,0,45">
                     <StackPanel Grid.Row="1" x:Name="PanSelect" Visibility="Collapsed" Opacity="0" IsHitTestVisible="False" Margin="0,0,0,45">
+                        <StackPanel.Resources>
+                            <Style x:Key="ModifyCompareIcon" TargetType="{x:Type local:MyImage}">
+                                <Setter Property="Width" Value="18" />
+                                <Setter Property="Height" Value="18" />
+                                <Setter Property="RenderOptions.BitmapScalingMode" Value="Linear" />
+                            </Style>
+                            <Style x:Key="ModifyCompareText" TargetType="{x:Type TextBlock}" BasedOn="{StaticResource BasedOnTextBlock}">
+                                <Setter Property="Margin" Value="7,0,0,0" />
+                                <Setter Property="VerticalAlignment" Value="Center" />
+                            </Style>
+                            <Style x:Key="ModifyCompareArrow" TargetType="{x:Type Path}">
+                                <Setter Property="Data" Value="M3,3 L7,7 3,11 M9,3 L13,7 9,11" />
+                                <Setter Property="Margin" Value="10,0,18,0" />
+                                <Setter Property="Height" Value="14" />
+                                <Setter Property="HorizontalAlignment" Value="Center" />
+                                <Setter Property="VerticalAlignment" Value="Center" />
+                                <Setter Property="Stroke" Value="{DynamicResource ColorBrushGray2}" />
+                                <Setter Property="StrokeThickness" Value="1.4" />
+                                <Setter Property="StrokeStartLineCap" Value="Round" />
+                                <Setter Property="StrokeEndLineCap" Value="Round" />
+                                <Setter Property="StrokeLineJoin" Value="Round" />
+                            </Style>
+                            <sys:String x:Key="ModifyRestoreLogo">M9.83,3.17 A1.18,1.18 0 0 1 9.83,4.83 L6.85,7.82 H14.5 A6.68,6.68 0 0 1 14.5,21.18 H11 A1.18,1.18 0 0 1 11,18.82 H14.5 A4.32,4.32 0 0 0 14.5,10.18 H6.85 L9.83,13.17 A1.18,1.18 0 0 1 8.17,14.83 L3.17,9.83 A1.18,1.18 0 0 1 3.17,8.17 L8.17,3.17 A1.18,1.18 0 0 1 9.83,3.17 Z</sys:String>
+                            <sys:String x:Key="ModifyClearLogo">F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z</sys:String>
+                            <Style x:Key="ModifyRestoreButton" TargetType="{x:Type local:MyIconButton}">
+                                <Setter Property="Visibility" Value="Collapsed" />
+                                <Setter Property="Height" Value="30" />
+                                <Setter Property="Width" Value="30" />
+                                <Setter Property="HorizontalAlignment" Value="Right" />
+                                <Setter Property="VerticalAlignment" Value="Top" />
+                                <Setter Property="Margin" Value="0,5,32,0" />
+                                <Setter Property="ToolTip" Value="撤销更改" />
+                                <Setter Property="Cursor" Value="Hand" />
+                            </Style>
+                            <Style x:Key="ModifyClearButton" TargetType="{x:Type local:MyIconButton}">
+                                <Setter Property="Height" Value="30" />
+                                <Setter Property="Width" Value="30" />
+                                <Setter Property="HorizontalAlignment" Value="Right" />
+                                <Setter Property="VerticalAlignment" Value="Top" />
+                                <Setter Property="Margin" Value="0,5,32,0" />
+                                <Setter Property="ToolTip" Value="取消安装" />
+                            </Style>
+                        </StackPanel.Resources>
                         <StackPanel.RenderTransform>
                         <StackPanel.RenderTransform>
                             <TranslateTransform X="40" />
                             <TranslateTransform X="40" />
                         </StackPanel.RenderTransform>
                         </StackPanel.RenderTransform>
-                        <local:MyCard Margin="0,15,0,15" UseAnimation="False" Height="62">
+                        <local:MyCard x:Name="CardSelectName" Margin="0,15,0,15" MinHeight="62">
                             <local:MyIconButton HorizontalAlignment="Left" Theme="Black" Opacity="0.6" x:Name="BtnBack" Margin="13,0,0,0" Height="26" Width="26"
                             <local:MyIconButton HorizontalAlignment="Left" Theme="Black" Opacity="0.6" x:Name="BtnBack" Margin="13,0,0,0" Height="26" Width="26"
                                              LogoScale="0.87" Logo="M1097 584 250 584 562 896C591 925 591 972 562 1001 533 1030 487 1030 458 1001L21 565C6 550-0 531 0 511L0 511 0 511C-0 492 6 472 21 457L458 21C487-7 533-7 562 21 591 50 591 97 562 126L250 438 1097 438C1137 438 1170 471 1170 511 1170 551 1137 584 1097 584L1097 584Z" />
                                              LogoScale="0.87" Logo="M1097 584 250 584 562 896C591 925 591 972 562 1001 533 1030 487 1030 458 1001L21 565C6 550-0 531 0 511L0 511 0 511C-0 492 6 472 21 457L458 21C487-7 533-7 562 21 591 50 591 97 562 126L250 438 1097 438C1137 438 1170 471 1170 511 1170 551 1137 584 1097 584L1097 584Z" />
-                            <local:MyImage x:Name="ImgLogo" Margin="52,0,0,0" VerticalAlignment="Center" HorizontalAlignment="Left" Height="32" RenderOptions.BitmapScalingMode="HighQuality" />
-                            <local:MyTextBox Margin="93,0,16,0" Padding="-1,0" MinHeight="30" FontSize="15" VerticalAlignment="Center" x:Name="TextSelectName" MaxLength="70" HasBackground="False" />
+                            <local:MyImage x:Name="ImgLogo" Margin="52,15,0,0" VerticalAlignment="Top" HorizontalAlignment="Left" Height="32" RenderOptions.BitmapScalingMode="HighQuality" />
+                            <local:MyTextBox Margin="93,16,16,0" Padding="-1,0" MinHeight="30" FontSize="15" VerticalAlignment="Top" x:Name="TextSelectName" MaxLength="70" HasBackground="False" ShowValidateResult="False" />
+                            <TextBlock x:Name="LabSelectName" Visibility="Collapsed" Height="21" Margin="56,47,42,11" Padding="0,4,0,0" VerticalAlignment="Top" FontSize="12.5" Foreground="{DynamicResource ColorBrushGray3}" TextTrimming="CharacterEllipsis" />
+                            <local:MyIconButton x:Name="BtnOriginalNameRestore" Style="{StaticResource ModifyRestoreButton}" Margin="0,16,6,0"
+                                                Theme="Black" LogoScale="0.55" Logo="{StaticResource ModifyRestoreLogo}" />
                         </local:MyCard>
                         </local:MyCard>
+                        <local:MyHint x:Name="HintSelectName" Theme="Red" Visibility="Collapsed" />
                         <local:MyHint Text="如果不安装 Fabric API,大多数 Mod 都会无法使用!" Margin="0,10,0,0" x:Name="HintFabricAPI" Theme="Red" />
                         <local:MyHint Text="如果不安装 Fabric API,大多数 Mod 都会无法使用!" Margin="0,10,0,0" x:Name="HintFabricAPI" Theme="Red" />
                         <local:MyHint Text="必须安装 OptiFabric 才能正常使用 OptiFine!" Margin="0,10,0,0" x:Name="HintOptiFabric" Theme="Red" />
                         <local:MyHint Text="必须安装 OptiFabric 才能正常使用 OptiFine!" Margin="0,10,0,0" x:Name="HintOptiFabric" Theme="Red" />
                         <local:MyHint Text="安装结束后,请在 Mod 下载中搜索 OptiFabric Origins 并下载,否则 OptiFine 会无法使用!" Margin="0,10,0,0" x:Name="HintOptiFabricOld" Theme="Yellow" />
                         <local:MyHint Text="安装结束后,请在 Mod 下载中搜索 OptiFabric Origins 并下载,否则 OptiFine 会无法使用!" Margin="0,10,0,0" x:Name="HintOptiFabricOld" Theme="Yellow" />
                         <local:MyHint Text="OptiFine 与一部分 Mod 的兼容性不佳,请谨慎安装。" Margin="0,10,0,0" x:Name="HintModOptiFine" Theme="Yellow" />
                         <local:MyHint Text="OptiFine 与一部分 Mod 的兼容性不佳,请谨慎安装。" Margin="0,10,0,0" x:Name="HintModOptiFine" Theme="Yellow" />
+                        <local:MyCard Title="Minecraft" Height="40" Margin="0,12,0,0" x:Name="CardMinecraft" Visibility="Collapsed">
+                            <Grid x:Name="PanMinecraftInfo" Height="18" Margin="132,11,35,0" VerticalAlignment="Top">
+                                <Grid.ColumnDefinitions>
+                                    <ColumnDefinition Width="Auto" /><ColumnDefinition Width="*" />
+                                </Grid.ColumnDefinitions>
+                                <local:MyImage x:Name="ImgMinecraft" Height="18" Width="18" Margin="0,0,7,0" RenderOptions.BitmapScalingMode="Linear" />
+                                <TextBlock x:Name="LabMinecraftVersion" Grid.Column="1" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" />
+                            </Grid>
+                            <Grid x:Name="PanMinecraftCompare" Height="18" Margin="132,11,80,0" VerticalAlignment="Top" Visibility="Collapsed" IsHitTestVisible="False">
+                                <Grid.ColumnDefinitions>
+                                    <ColumnDefinition Width="180" /><ColumnDefinition Width="Auto" /><ColumnDefinition Width="1*" />
+                                </Grid.ColumnDefinitions>
+                                <DockPanel>
+                                    <local:MyImage x:Name="ImgMinecraftOld" Style="{StaticResource ModifyCompareIcon}" DockPanel.Dock="Left" />
+                                    <TextBlock x:Name="LabMinecraftOld" Style="{StaticResource ModifyCompareText}" />
+                                </DockPanel>
+                                <Path Grid.Column="1" Style="{StaticResource ModifyCompareArrow}" />
+                                <DockPanel Grid.Column="2">
+                                    <local:MyImage x:Name="ImgMinecraftNew" Style="{StaticResource ModifyCompareIcon}" DockPanel.Dock="Left" />
+                                    <TextBlock x:Name="LabMinecraftNew" Style="{StaticResource ModifyCompareText}" />
+                                </DockPanel>
+                            </Grid>
+                            <Grid Height="40" VerticalAlignment="Top" Background="Transparent" Cursor="Hand">
+                                <Path HorizontalAlignment="Right" Margin="0,0,16,0" Stretch="Uniform" Height="6" Width="10" Fill="{StaticResource ColorBrush1}" RenderTransformOrigin="0.5,0.5">
+                                    <Path.Data>M2,4 l-2,2 10,10 10,-10 -2,-2 -8,8 -8,-8 z</Path.Data>
+                                    <Path.RenderTransform><RotateTransform Angle="270" /></Path.RenderTransform>
+                                </Path>
+                            </Grid>
+                            <local:MyIconButton x:Name="BtnMinecraftRestore" Style="{StaticResource ModifyRestoreButton}" Theme="Black" LogoScale="0.55" Logo="{StaticResource ModifyRestoreLogo}" />
+                        </local:MyCard>
                         <local:MyCard Title="Forge" Height="40" Margin="0,12,0,0" x:Name="CardForge" IsSwapped="True" CanSwap="True" SwapLogoRight="True">
                         <local:MyCard Title="Forge" Height="40" Margin="0,12,0,0" x:Name="CardForge" IsSwapped="True" CanSwap="True" SwapLogoRight="True">
                         <StackPanel Margin="20,40,18,15" VerticalAlignment="Top" Name="PanForge" />
                         <StackPanel Margin="20,40,18,15" VerticalAlignment="Top" Name="PanForge" />
                         <Grid x:Name="PanForgeInfo" Height="18" Margin="132,11,15,0" VerticalAlignment="Top" Tag="True">
                         <Grid x:Name="PanForgeInfo" Height="18" Margin="132,11,15,0" VerticalAlignment="Top" Tag="True">
@@ -37,13 +114,25 @@
                                 <ColumnDefinition Width="Auto" />
                                 <ColumnDefinition Width="Auto" />
                                 <ColumnDefinition Width="1*" />
                                 <ColumnDefinition Width="1*" />
                             </Grid.ColumnDefinitions>
                             </Grid.ColumnDefinitions>
-                            <Image x:Name="ImgForge" Margin="0,0,7,0" SnapsToDevicePixels="True" Height="18" RenderOptions.BitmapScalingMode="Linear" Source="pack://application:,,,/images/Blocks/Anvil.png" />
+                            <local:MyImage x:Name="ImgForge" Margin="0,0,7,0" SnapsToDevicePixels="True" Height="18" RenderOptions.BitmapScalingMode="Linear" Source="pack://application:,,,/images/Blocks/Anvil.png" />
                             <TextBlock x:Name="LabForge" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" Grid.Column="1" />
                             <TextBlock x:Name="LabForge" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" Grid.Column="1" />
                         </Grid>
                         </Grid>
-                        <Grid x:Name="BtnForgeClear" Height="30" Width="30" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,5,32,0" Background="{StaticResource ColorBrushSemiTransparent}">
-                            <Path x:Name="BtnForgeClearInner" Height="10" Width="10" Stretch="Uniform" Fill="{StaticResource ColorBrushGray1}" HorizontalAlignment="Center" VerticalAlignment="Center"
-                                Data="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z" />
+                        <Grid x:Name="PanForgeCompare" Height="18" Margin="132,11,80,0" VerticalAlignment="Top" Visibility="Collapsed" IsHitTestVisible="False">
+                            <Grid.ColumnDefinitions>
+                                <ColumnDefinition Width="180" /><ColumnDefinition Width="Auto" /><ColumnDefinition Width="1*" />
+                            </Grid.ColumnDefinitions>
+                            <DockPanel>
+                                <local:MyImage Style="{StaticResource ModifyCompareIcon}" Source="pack://application:,,,/images/Blocks/Anvil.png" DockPanel.Dock="Left" />
+                                <TextBlock x:Name="LabForgeOld" Style="{StaticResource ModifyCompareText}" />
+                            </DockPanel>
+                            <Path Grid.Column="1" Style="{StaticResource ModifyCompareArrow}" />
+                            <DockPanel Grid.Column="2">
+                                <local:MyImage Style="{StaticResource ModifyCompareIcon}" Source="pack://application:,,,/images/Blocks/Anvil.png" DockPanel.Dock="Left" />
+                                <TextBlock Style="{StaticResource ModifyCompareText}" />
+                            </DockPanel>
                         </Grid>
                         </Grid>
+                        <local:MyIconButton x:Name="BtnForgeClear" Style="{StaticResource ModifyClearButton}" Theme="Black" LogoScale="0.5" Logo="{StaticResource ModifyClearLogo}" />
+                        <local:MyIconButton x:Name="BtnForgeRestore" Style="{StaticResource ModifyRestoreButton}" Theme="Black" LogoScale="0.55" Logo="{StaticResource ModifyRestoreLogo}" />
                     </local:MyCard>
                     </local:MyCard>
                         <local:MyCard Title="NeoForge" Height="40" Margin="0,12,0,0" x:Name="CardNeoForge" IsSwapped="True" CanSwap="True" SwapLogoRight="True">
                         <local:MyCard Title="NeoForge" Height="40" Margin="0,12,0,0" x:Name="CardNeoForge" IsSwapped="True" CanSwap="True" SwapLogoRight="True">
                         <StackPanel Margin="20,40,18,15" VerticalAlignment="Top" Name="PanNeoForge" />
                         <StackPanel Margin="20,40,18,15" VerticalAlignment="Top" Name="PanNeoForge" />
@@ -55,13 +144,25 @@
                                 <ColumnDefinition Width="Auto" />
                                 <ColumnDefinition Width="Auto" />
                                 <ColumnDefinition Width="1*" />
                                 <ColumnDefinition Width="1*" />
                             </Grid.ColumnDefinitions>
                             </Grid.ColumnDefinitions>
-                            <Image x:Name="ImgNeoForge" Margin="0,0,7,0" SnapsToDevicePixels="True" Height="18" RenderOptions.BitmapScalingMode="Linear" Source="pack://application:,,,/images/Blocks/NeoForge.png" />
+                            <local:MyImage x:Name="ImgNeoForge" Margin="0,0,7,0" SnapsToDevicePixels="True" Height="18" RenderOptions.BitmapScalingMode="Linear" Source="pack://application:,,,/images/Blocks/NeoForge.png" />
                             <TextBlock x:Name="LabNeoForge" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" Grid.Column="1" />
                             <TextBlock x:Name="LabNeoForge" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" Grid.Column="1" />
                         </Grid>
                         </Grid>
-                        <Grid x:Name="BtnNeoForgeClear" Height="30" Width="30" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,5,32,0" Background="{StaticResource ColorBrushSemiTransparent}">
-                            <Path x:Name="BtnNeoForgeClearInner" Height="10" Width="10" Stretch="Uniform" Fill="{StaticResource ColorBrushGray1}" HorizontalAlignment="Center" VerticalAlignment="Center"
-                          Data="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z" />
+                        <Grid x:Name="PanNeoForgeCompare" Height="18" Margin="132,11,80,0" VerticalAlignment="Top" Visibility="Collapsed" IsHitTestVisible="False">
+                            <Grid.ColumnDefinitions>
+                                <ColumnDefinition Width="180" /><ColumnDefinition Width="Auto" /><ColumnDefinition Width="1*" />
+                            </Grid.ColumnDefinitions>
+                            <DockPanel>
+                                <local:MyImage Style="{StaticResource ModifyCompareIcon}" Source="pack://application:,,,/images/Blocks/NeoForge.png" DockPanel.Dock="Left" />
+                                <TextBlock x:Name="LabNeoForgeOld" Style="{StaticResource ModifyCompareText}" />
+                            </DockPanel>
+                            <Path Grid.Column="1" Style="{StaticResource ModifyCompareArrow}" />
+                            <DockPanel Grid.Column="2">
+                                <local:MyImage Style="{StaticResource ModifyCompareIcon}" Source="pack://application:,,,/images/Blocks/NeoForge.png" DockPanel.Dock="Left" />
+                                <TextBlock Style="{StaticResource ModifyCompareText}" />
+                            </DockPanel>
                         </Grid>
                         </Grid>
+                        <local:MyIconButton x:Name="BtnNeoForgeClear" Style="{StaticResource ModifyClearButton}" Theme="Black" LogoScale="0.5" Logo="{StaticResource ModifyClearLogo}" />
+                        <local:MyIconButton x:Name="BtnNeoForgeRestore" Style="{StaticResource ModifyRestoreButton}" Theme="Black" LogoScale="0.55" Logo="{StaticResource ModifyRestoreLogo}" />
                     </local:MyCard>
                     </local:MyCard>
                         <local:MyCard Title="Fabric" Height="40" Margin="0,12,0,0" x:Name="CardFabric" IsSwapped="True" CanSwap="True" SwapLogoRight="True">
                         <local:MyCard Title="Fabric" Height="40" Margin="0,12,0,0" x:Name="CardFabric" IsSwapped="True" CanSwap="True" SwapLogoRight="True">
                         <StackPanel Margin="20,40,18,15" VerticalAlignment="Top" Name="PanFabric" />
                         <StackPanel Margin="20,40,18,15" VerticalAlignment="Top" Name="PanFabric" />
@@ -73,13 +174,25 @@
                                 <ColumnDefinition Width="Auto" />
                                 <ColumnDefinition Width="Auto" />
                                 <ColumnDefinition Width="1*" />
                                 <ColumnDefinition Width="1*" />
                             </Grid.ColumnDefinitions>
                             </Grid.ColumnDefinitions>
-                            <Image x:Name="ImgFabric" Margin="0,0,7,0" SnapsToDevicePixels="True" Height="18" RenderOptions.BitmapScalingMode="Linear" Source="pack://application:,,,/images/Blocks/Fabric.png" />
+                            <local:MyImage x:Name="ImgFabric" Margin="0,0,7,0" SnapsToDevicePixels="True" Height="18" RenderOptions.BitmapScalingMode="Linear" Source="pack://application:,,,/images/Blocks/Fabric.png" />
                             <TextBlock x:Name="LabFabric" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" Grid.Column="1" />
                             <TextBlock x:Name="LabFabric" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" Grid.Column="1" />
                         </Grid>
                         </Grid>
-                        <Grid x:Name="BtnFabricClear" Height="30" Width="30" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,5,32,0" Background="{StaticResource ColorBrushSemiTransparent}">
-                            <Path x:Name="BtnFabricClearInner" Height="10" Width="10" Stretch="Uniform" Fill="{StaticResource ColorBrushGray1}" HorizontalAlignment="Center" VerticalAlignment="Center"
-                                Data="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z" />
+                        <Grid x:Name="PanFabricCompare" Height="18" Margin="132,11,80,0" VerticalAlignment="Top" Visibility="Collapsed" IsHitTestVisible="False">
+                            <Grid.ColumnDefinitions>
+                                <ColumnDefinition Width="180" /><ColumnDefinition Width="Auto" /><ColumnDefinition Width="1*" />
+                            </Grid.ColumnDefinitions>
+                            <DockPanel>
+                                <local:MyImage Style="{StaticResource ModifyCompareIcon}" Source="pack://application:,,,/images/Blocks/Fabric.png" DockPanel.Dock="Left" />
+                                <TextBlock x:Name="LabFabricOld" Style="{StaticResource ModifyCompareText}" />
+                            </DockPanel>
+                            <Path Grid.Column="1" Style="{StaticResource ModifyCompareArrow}" />
+                            <DockPanel Grid.Column="2">
+                                <local:MyImage Style="{StaticResource ModifyCompareIcon}" Source="pack://application:,,,/images/Blocks/Fabric.png" DockPanel.Dock="Left" />
+                                <TextBlock Style="{StaticResource ModifyCompareText}" />
+                            </DockPanel>
                         </Grid>
                         </Grid>
+                        <local:MyIconButton x:Name="BtnFabricClear" Style="{StaticResource ModifyClearButton}" Theme="Black" LogoScale="0.5" Logo="{StaticResource ModifyClearLogo}" />
+                        <local:MyIconButton x:Name="BtnFabricRestore" Style="{StaticResource ModifyRestoreButton}" Theme="Black" LogoScale="0.55" Logo="{StaticResource ModifyRestoreLogo}" />
                     </local:MyCard>
                     </local:MyCard>
                         <local:MyCard Title="Fabric API" Height="40" Margin="0,12,0,0" x:Name="CardFabricApi" IsSwapped="True" CanSwap="True" SwapLogoRight="True">
                         <local:MyCard Title="Fabric API" Height="40" Margin="0,12,0,0" x:Name="CardFabricApi" IsSwapped="True" CanSwap="True" SwapLogoRight="True">
                         <StackPanel Margin="20,40,18,15" VerticalAlignment="Top" Name="PanFabricApi" />
                         <StackPanel Margin="20,40,18,15" VerticalAlignment="Top" Name="PanFabricApi" />
@@ -91,13 +204,25 @@
                                 <ColumnDefinition Width="Auto" />
                                 <ColumnDefinition Width="Auto" />
                                 <ColumnDefinition Width="1*" />
                                 <ColumnDefinition Width="1*" />
                             </Grid.ColumnDefinitions>
                             </Grid.ColumnDefinitions>
-                            <Image x:Name="ImgFabricApi" Margin="0,0,7,0" SnapsToDevicePixels="True" Height="18" RenderOptions.BitmapScalingMode="Linear" Source="pack://application:,,,/images/Blocks/Fabric.png" />
+                            <local:MyImage x:Name="ImgFabricApi" Margin="0,0,7,0" SnapsToDevicePixels="True" Height="18" RenderOptions.BitmapScalingMode="Linear" Source="pack://application:,,,/images/Blocks/Fabric.png" />
                             <TextBlock x:Name="LabFabricApi" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" Grid.Column="1" />
                             <TextBlock x:Name="LabFabricApi" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" Grid.Column="1" />
                         </Grid>
                         </Grid>
-                        <Grid x:Name="BtnFabricApiClear" Height="30" Width="30" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,5,32,0" Background="{StaticResource ColorBrushSemiTransparent}">
-                            <Path x:Name="BtnFabricApiClearInner" Height="10" Width="10" Stretch="Uniform" Fill="{StaticResource ColorBrushGray1}" HorizontalAlignment="Center" VerticalAlignment="Center"
-                                Data="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z" />
+                        <Grid x:Name="PanFabricApiCompare" Height="18" Margin="132,11,80,0" VerticalAlignment="Top" Visibility="Collapsed" IsHitTestVisible="False">
+                            <Grid.ColumnDefinitions>
+                                <ColumnDefinition Width="180" /><ColumnDefinition Width="Auto" /><ColumnDefinition Width="1*" />
+                            </Grid.ColumnDefinitions>
+                            <DockPanel>
+                                <local:MyImage Style="{StaticResource ModifyCompareIcon}" Source="pack://application:,,,/images/Blocks/Fabric.png" DockPanel.Dock="Left" />
+                                <TextBlock x:Name="LabFabricApiOld" Style="{StaticResource ModifyCompareText}" />
+                            </DockPanel>
+                            <Path Grid.Column="1" Style="{StaticResource ModifyCompareArrow}" />
+                            <DockPanel Grid.Column="2">
+                                <local:MyImage Style="{StaticResource ModifyCompareIcon}" Source="pack://application:,,,/images/Blocks/Fabric.png" DockPanel.Dock="Left" />
+                                <TextBlock Style="{StaticResource ModifyCompareText}" />
+                            </DockPanel>
                         </Grid>
                         </Grid>
+                        <local:MyIconButton x:Name="BtnFabricApiClear" Style="{StaticResource ModifyClearButton}" Theme="Black" LogoScale="0.5" Logo="{StaticResource ModifyClearLogo}" />
+                        <local:MyIconButton x:Name="BtnFabricApiRestore" Style="{StaticResource ModifyRestoreButton}" Theme="Black" LogoScale="0.55" Logo="{StaticResource ModifyRestoreLogo}" />
                     </local:MyCard>
                     </local:MyCard>
                         <local:MyCard Title="OptiFine" Height="40" Margin="0,12,0,0" x:Name="CardOptiFine" IsSwapped="True" CanSwap="True" SwapLogoRight="True">
                         <local:MyCard Title="OptiFine" Height="40" Margin="0,12,0,0" x:Name="CardOptiFine" IsSwapped="True" CanSwap="True" SwapLogoRight="True">
                         <StackPanel Margin="20,40,18,15" VerticalAlignment="Top">
                         <StackPanel Margin="20,40,18,15" VerticalAlignment="Top">
@@ -111,13 +236,25 @@
                                 <ColumnDefinition Width="Auto" />
                                 <ColumnDefinition Width="Auto" />
                                 <ColumnDefinition Width="1*" />
                                 <ColumnDefinition Width="1*" />
                             </Grid.ColumnDefinitions>
                             </Grid.ColumnDefinitions>
-                            <Image x:Name="ImgOptiFine" Margin="0,0,7,0" SnapsToDevicePixels="True" Height="18" RenderOptions.BitmapScalingMode="Linear" Source="pack://application:,,,/images/Blocks/GrassPath.png" />
+                            <local:MyImage x:Name="ImgOptiFine" Margin="0,0,7,0" SnapsToDevicePixels="True" Height="18" RenderOptions.BitmapScalingMode="Linear" Source="pack://application:,,,/images/Blocks/GrassPath.png" />
                             <TextBlock x:Name="LabOptiFine" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" Grid.Column="1" />
                             <TextBlock x:Name="LabOptiFine" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" Grid.Column="1" />
                         </Grid>
                         </Grid>
-                        <Grid x:Name="BtnOptiFineClear" Height="30" Width="30" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,5,32,0" Background="{StaticResource ColorBrushSemiTransparent}">
-                            <Path x:Name="BtnOptiFineClearInner" Height="10" Width="10" Stretch="Uniform" Fill="{StaticResource ColorBrushGray1}" HorizontalAlignment="Center" VerticalAlignment="Center"
-                                Data="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z" />
+                        <Grid x:Name="PanOptiFineCompare" Height="18" Margin="132,11,80,0" VerticalAlignment="Top" Visibility="Collapsed" IsHitTestVisible="False">
+                            <Grid.ColumnDefinitions>
+                                <ColumnDefinition Width="180" /><ColumnDefinition Width="Auto" /><ColumnDefinition Width="1*" />
+                            </Grid.ColumnDefinitions>
+                            <DockPanel>
+                                <local:MyImage Style="{StaticResource ModifyCompareIcon}" Source="pack://application:,,,/images/Blocks/GrassPath.png" DockPanel.Dock="Left" />
+                                <TextBlock x:Name="LabOptiFineOld" Style="{StaticResource ModifyCompareText}" />
+                            </DockPanel>
+                            <Path Grid.Column="1" Style="{StaticResource ModifyCompareArrow}" />
+                            <DockPanel Grid.Column="2">
+                                <local:MyImage Style="{StaticResource ModifyCompareIcon}" Source="pack://application:,,,/images/Blocks/GrassPath.png" DockPanel.Dock="Left" />
+                                <TextBlock Style="{StaticResource ModifyCompareText}" />
+                            </DockPanel>
                         </Grid>
                         </Grid>
+                        <local:MyIconButton x:Name="BtnOptiFineClear" Style="{StaticResource ModifyClearButton}" Theme="Black" LogoScale="0.5" Logo="{StaticResource ModifyClearLogo}" />
+                        <local:MyIconButton x:Name="BtnOptiFineRestore" Style="{StaticResource ModifyRestoreButton}" Theme="Black" LogoScale="0.55" Logo="{StaticResource ModifyRestoreLogo}" />
                     </local:MyCard>
                     </local:MyCard>
                         <local:MyCard Title="OptiFabric" Height="40" Margin="0,12,0,0" x:Name="CardOptiFabric" IsSwapped="True" CanSwap="True" SwapLogoRight="True">
                         <local:MyCard Title="OptiFabric" Height="40" Margin="0,12,0,0" x:Name="CardOptiFabric" IsSwapped="True" CanSwap="True" SwapLogoRight="True">
                         <StackPanel Margin="20,40,18,15" VerticalAlignment="Top" Name="PanOptiFabric" />
                         <StackPanel Margin="20,40,18,15" VerticalAlignment="Top" Name="PanOptiFabric" />
@@ -129,13 +266,25 @@
                                 <ColumnDefinition Width="Auto" />
                                 <ColumnDefinition Width="Auto" />
                                 <ColumnDefinition Width="1*" />
                                 <ColumnDefinition Width="1*" />
                             </Grid.ColumnDefinitions>
                             </Grid.ColumnDefinitions>
-                            <Image x:Name="ImgOptiFabric" Margin="0,0,7,0" SnapsToDevicePixels="True" Height="18" RenderOptions.BitmapScalingMode="HighQuality" Source="pack://application:,,,/images/Blocks/OptiFabric.png" />
+                            <local:MyImage x:Name="ImgOptiFabric" Margin="0,0,7,0" SnapsToDevicePixels="True" Height="18" RenderOptions.BitmapScalingMode="HighQuality" Source="pack://application:,,,/images/Blocks/OptiFabric.png" />
                             <TextBlock x:Name="LabOptiFabric" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" Grid.Column="1" />
                             <TextBlock x:Name="LabOptiFabric" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" Grid.Column="1" />
                         </Grid>
                         </Grid>
-                        <Grid x:Name="BtnOptiFabricClear" Height="30" Width="30" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,5,32,0" Background="{StaticResource ColorBrushSemiTransparent}">
-                            <Path x:Name="BtnOptiFabricClearInner" Height="10" Width="10" Stretch="Uniform" Fill="{StaticResource ColorBrushGray1}" HorizontalAlignment="Center" VerticalAlignment="Center"
-                                Data="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z" />
+                        <Grid x:Name="PanOptiFabricCompare" Height="18" Margin="132,11,80,0" VerticalAlignment="Top" Visibility="Collapsed" IsHitTestVisible="False">
+                            <Grid.ColumnDefinitions>
+                                <ColumnDefinition Width="180" /><ColumnDefinition Width="Auto" /><ColumnDefinition Width="1*" />
+                            </Grid.ColumnDefinitions>
+                            <DockPanel>
+                                <local:MyImage Style="{StaticResource ModifyCompareIcon}" Source="pack://application:,,,/images/Blocks/OptiFabric.png" DockPanel.Dock="Left" />
+                                <TextBlock x:Name="LabOptiFabricOld" Style="{StaticResource ModifyCompareText}" />
+                            </DockPanel>
+                            <Path Grid.Column="1" Style="{StaticResource ModifyCompareArrow}" />
+                            <DockPanel Grid.Column="2">
+                                <local:MyImage Style="{StaticResource ModifyCompareIcon}" Source="pack://application:,,,/images/Blocks/OptiFabric.png" DockPanel.Dock="Left" />
+                                <TextBlock Style="{StaticResource ModifyCompareText}" />
+                            </DockPanel>
                         </Grid>
                         </Grid>
+                        <local:MyIconButton x:Name="BtnOptiFabricClear" Style="{StaticResource ModifyClearButton}" Theme="Black" LogoScale="0.5" Logo="{StaticResource ModifyClearLogo}" />
+                        <local:MyIconButton x:Name="BtnOptiFabricRestore" Style="{StaticResource ModifyRestoreButton}" Theme="Black" LogoScale="0.55" Logo="{StaticResource ModifyRestoreLogo}" />
                     </local:MyCard>
                     </local:MyCard>
                         <local:MyCard Title="LiteLoader" Height="40" Margin="0,12,0,0" x:Name="CardLiteLoader" IsSwapped="True" CanSwap="True" SwapLogoRight="True">
                         <local:MyCard Title="LiteLoader" Height="40" Margin="0,12,0,0" x:Name="CardLiteLoader" IsSwapped="True" CanSwap="True" SwapLogoRight="True">
                         <StackPanel Margin="20,40,18,15" VerticalAlignment="Top" Name="PanLiteLoader" />
                         <StackPanel Margin="20,40,18,15" VerticalAlignment="Top" Name="PanLiteLoader" />
@@ -147,13 +296,25 @@
                                 <ColumnDefinition Width="Auto" />
                                 <ColumnDefinition Width="Auto" />
                                 <ColumnDefinition Width="1*" />
                                 <ColumnDefinition Width="1*" />
                             </Grid.ColumnDefinitions>
                             </Grid.ColumnDefinitions>
-                            <Image x:Name="ImgLiteLoader" Margin="-1,0,6,0" SnapsToDevicePixels="True" Height="20" RenderOptions.BitmapScalingMode="Linear" Source="pack://application:,,,/images/Blocks/Egg.png" />
+                            <local:MyImage x:Name="ImgLiteLoader" Margin="-1,0,6,0" SnapsToDevicePixels="True" Height="20" RenderOptions.BitmapScalingMode="Linear" Source="pack://application:,,,/images/Blocks/Egg.png" />
                             <TextBlock x:Name="LabLiteLoader" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" Grid.Column="1" />
                             <TextBlock x:Name="LabLiteLoader" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" Grid.Column="1" />
                         </Grid>
                         </Grid>
-                        <Grid x:Name="BtnLiteLoaderClear" Height="30" Width="30" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,5,32,0" Background="{StaticResource ColorBrushSemiTransparent}">
-                            <Path x:Name="BtnLiteLoaderClearInner" Height="10" Width="10" Stretch="Uniform" Fill="{StaticResource ColorBrushGray1}" HorizontalAlignment="Center" VerticalAlignment="Center"
-                                Data="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z" />
+                        <Grid x:Name="PanLiteLoaderCompare" Height="18" Margin="132,11,80,0" VerticalAlignment="Top" Visibility="Collapsed" IsHitTestVisible="False">
+                            <Grid.ColumnDefinitions>
+                                <ColumnDefinition Width="180" /><ColumnDefinition Width="Auto" /><ColumnDefinition Width="1*" />
+                            </Grid.ColumnDefinitions>
+                            <DockPanel>
+                                <local:MyImage Style="{StaticResource ModifyCompareIcon}" Source="pack://application:,,,/images/Blocks/Egg.png" DockPanel.Dock="Left" />
+                                <TextBlock x:Name="LabLiteLoaderOld" Style="{StaticResource ModifyCompareText}" />
+                            </DockPanel>
+                            <Path Grid.Column="1" Style="{StaticResource ModifyCompareArrow}" />
+                            <DockPanel Grid.Column="2">
+                                <local:MyImage Style="{StaticResource ModifyCompareIcon}" Source="pack://application:,,,/images/Blocks/Egg.png" DockPanel.Dock="Left" />
+                                <TextBlock Style="{StaticResource ModifyCompareText}" />
+                            </DockPanel>
                         </Grid>
                         </Grid>
+                        <local:MyIconButton x:Name="BtnLiteLoaderClear" Style="{StaticResource ModifyClearButton}" Theme="Black" LogoScale="0.5" Logo="{StaticResource ModifyClearLogo}" />
+                        <local:MyIconButton x:Name="BtnLiteLoaderRestore" Style="{StaticResource ModifyRestoreButton}" Theme="Black" LogoScale="0.55" Logo="{StaticResource ModifyRestoreLogo}" />
                     </local:MyCard>
                     </local:MyCard>
                     </StackPanel>
                     </StackPanel>
                 </Grid>
                 </Grid>
@@ -163,7 +324,7 @@
                 LogoScale="0.95" Logo="M955 610h-59c-15 0-29 13-29 29v196c0 15-13 29-29 29h-649c-15 0-29-13-29-29v-196c0-15-13-29-29-29h-59c-15 0-29 13-29 29V905c0 43 35 78 78 78h787c43 0 78-35 78-78V640c0-15-13-29-29-29zM492 740c11 11 29 11 41 0l265-265c11-11 11-29 0-41l-41-41c-11-11-29-11-41 0l-110 110c-11 11-33 3-33-13V68C571 53 555 39 541 39h-59c-15 0-29 13-29 29v417c0 17-21 25-33 13l-110-110c-11-11-29-11-41 0L226 433c-11 11-11 29 0 41L492 740z" />
                 LogoScale="0.95" Logo="M955 610h-59c-15 0-29 13-29 29v196c0 15-13 29-29 29h-649c-15 0-29-13-29-29v-196c0-15-13-29-29-29h-59c-15 0-29 13-29 29V905c0 43 35 78 78 78h787c43 0 78-35 78-78V640c0-15-13-29-29-29zM492 740c11 11 29 11 41 0l265-265c11-11 11-29 0-41l-41-41c-11-11-29-11-41 0l-110 110c-11 11-33 3-33-13V68C571 53 555 39 541 39h-59c-15 0-29 13-29 29v417c0 17-21 25-33 13l-110-110c-11-11-29-11-41 0L226 433c-11 11-11 29 0 41L492 740z" />
         </Grid>
         </Grid>
         <local:MyCard HorizontalAlignment="Center" VerticalAlignment="Center" Margin="40,0" SnapsToDevicePixels="True" x:Name="PanLoad" UseAnimation="False">
         <local:MyCard HorizontalAlignment="Center" VerticalAlignment="Center" Margin="40,0" SnapsToDevicePixels="True" x:Name="PanLoad" UseAnimation="False">
-            <local:MyLoading Text="正在获取版本列表" Margin="20,20,20,17" x:Name="LoadMinecraft" HorizontalAlignment="Center" VerticalAlignment="Center" />
+            <local:MyLoading Text="正在加载" Margin="20,20,20,17" x:Name="LoadMinecraft" HorizontalAlignment="Center" VerticalAlignment="Center" ShowProgress="True" />
             <local:MyLoading Visibility="Collapsed" x:Name="LoadOptiFine" HasAnimation="False" />
             <local:MyLoading Visibility="Collapsed" x:Name="LoadOptiFine" HasAnimation="False" />
             <local:MyLoading Visibility="Collapsed" x:Name="LoadForge" HasAnimation="False" />
             <local:MyLoading Visibility="Collapsed" x:Name="LoadForge" HasAnimation="False" />
             <local:MyLoading Visibility="Collapsed" x:Name="LoadNeoForge" HasAnimation="False" />
             <local:MyLoading Visibility="Collapsed" x:Name="LoadNeoForge" HasAnimation="False" />
@@ -196,4 +357,4 @@
             </StackPanel>
             </StackPanel>
         </Border>
         </Border>
     </Grid>
     </Grid>
--->
+-->

+ 182 - 160
Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb

@@ -1,12 +1,30 @@
 Public Class PageDownloadInstall
 Public Class PageDownloadInstall
 
 
-    Private Sub LoaderInit() Handles Me.Initialized
+    Public Sub ControlInit()
         DisabledPageAnimControls.Add(BtnStart)
         DisabledPageAnimControls.Add(BtnStart)
+        LoadOptiFine.State = DlOptiFineListLoader
+        LoadLiteLoader.State = DlLiteLoaderListLoader
+        LoadFabric.State = DlFabricListLoader
+        LoadFabricApi.State = DlFabricApiLoader
+        LoadNeoForge.State = DlNeoForgeListLoader
+        LoadOptiFabric.State = DlOptiFabricLoader
+
+        McDownloadForgeRecommendedRefresh()
+    End Sub
+
+    Public Overridable Sub LoaderInit() Handles Me.Initialized
+        ControlInit()
         PageLoaderInit(LoadMinecraft, PanLoad, PanAllBack, Nothing, DlClientListLoader, AddressOf LoadMinecraft_OnFinish)
         PageLoaderInit(LoadMinecraft, PanLoad, PanAllBack, Nothing, DlClientListLoader, AddressOf LoadMinecraft_OnFinish)
     End Sub
     End Sub
 
 
     Private IsLoad As Boolean = False
     Private IsLoad As Boolean = False
-    Private Sub Init() Handles Me.Loaded
+    Public Overridable Sub Init() Handles Me.Loaded
+        If IsLoad Then
+            IsSelectNameEdited = False
+            ExitSelectPage()
+            If DlClientListLoader.State = LoadState.Finished Then TrySelectWaitingVersion()
+        End If
+        IsLoad = True
         PanBack.ScrollToHome()
         PanBack.ScrollToHome()
         DlOptiFineListLoader.Start()
         DlOptiFineListLoader.Start()
         DlLiteLoaderListLoader.Start()
         DlLiteLoaderListLoader.Start()
@@ -17,35 +35,19 @@ Public Class PageDownloadInstall
         TextSelectName.ValidateRules = New ObjectModel.Collection(Of Validate) From {New ValidateFolderName(McFolderSelected & "versions")}
         TextSelectName.ValidateRules = New ObjectModel.Collection(Of Validate) From {New ValidateFolderName(McFolderSelected & "versions")}
         TextSelectName.Validate()
         TextSelectName.Validate()
         ReloadSelected()
         ReloadSelected()
-
-        '非重复加载部分
-        If IsLoad Then Return
-        IsLoad = True
-
-        McDownloadForgeRecommendedRefresh()
-
-        LoadOptiFine.State = DlOptiFineListLoader
-        LoadLiteLoader.State = DlLiteLoaderListLoader
-        LoadFabric.State = DlFabricListLoader
-        LoadFabricApi.State = DlFabricApiLoader
-        LoadNeoForge.State = DlNeoForgeListLoader
-        LoadOptiFabric.State = DlOptiFabricLoader
     End Sub
     End Sub
 
 
 #Region "页面切换"
 #Region "页面切换"
 
 
     '页面切换动画
     '页面切换动画
     Public IsInSelectPage As Boolean = False
     Public IsInSelectPage As Boolean = False
-    Private IsFirstLoaded As Boolean = False
-    Private Sub EnterSelectPage()
-        If IsInSelectPage Then Return
-        IsInSelectPage = True
 
 
+    ''' <summary>
+    ''' 选择页面 UI 初始化。
+    ''' </summary>
+    Public Sub PrepareSelectPage()
+        IsInSelectPage = True
         PanInner.Margin = New Thickness(25, 10, 25, 40)
         PanInner.Margin = New Thickness(25, 10, 25, 40)
-
-        AutoSelectedFabricApi = False
-        AutoSelectedOptiFabric = False
-        IsSelectNameEdited = False
         PanSelect.Visibility = Visibility.Visible
         PanSelect.Visibility = Visibility.Visible
         PanSelect.IsHitTestVisible = True
         PanSelect.IsHitTestVisible = True
         PanMinecraft.IsHitTestVisible = False
         PanMinecraft.IsHitTestVisible = False
@@ -54,13 +56,9 @@ Public Class PageDownloadInstall
 
 
         DisabledPageAnimControls.Remove(BtnStart)
         DisabledPageAnimControls.Remove(BtnStart)
         BtnStart.Show = True
         BtnStart.Show = True
-        CardOptiFine.IsSwapped = True
-        CardLiteLoader.IsSwapped = True
-        CardForge.IsSwapped = True
-        CardNeoForge.IsSwapped = True
-        CardFabric.IsSwapped = True
-        CardFabricApi.IsSwapped = True
-        CardOptiFabric.IsSwapped = True
+        For Each Card In {CardOptiFine, CardLiteLoader, CardForge, CardNeoForge, CardFabric, CardFabricApi, CardOptiFabric}
+            Card.IsSwapped = True
+        Next
 
 
         '如果在选择页面按了刷新键,选择页的东西可能会由于动画被隐藏,但不会由于加载结束而再次显示,因此这里需要手动恢复
         '如果在选择页面按了刷新键,选择页的东西可能会由于动画被隐藏,但不会由于加载结束而再次显示,因此这里需要手动恢复
         For Each Control In GetAllAnimControls(PanSelect)
         For Each Control In GetAllAnimControls(PanSelect)
@@ -69,9 +67,16 @@ Public Class PageDownloadInstall
                 Control.RenderTransform = New TranslateTransform
                 Control.RenderTransform = New TranslateTransform
             End If
             End If
         Next
         Next
+    End Sub
+
+    Private Sub EnterSelectPage()
+        If IsInSelectPage Then Return
+        AutoSelectedFabricApi = False
+        AutoSelectedOptiFabric = False
+        PrepareSelectPage()
 
 
         '启动 Forge 加载
         '启动 Forge 加载
-        If McVersion.IsFormatFit(VanillaName) Then
+        If McVersion.IsStandardFormat(VanillaName) Then
             Dim ForgeLoader = New LoaderTask(Of String, List(Of DlForgeVersionEntry))("DlForgeVersion " & VanillaName, AddressOf DlForgeVersionMain)
             Dim ForgeLoader = New LoaderTask(Of String, List(Of DlForgeVersionEntry))("DlForgeVersion " & VanillaName, AddressOf DlForgeVersionMain)
             LoadForge.State = ForgeLoader
             LoadForge.State = ForgeLoader
             ForgeLoader.Start(VanillaName)
             ForgeLoader.Start(VanillaName)
@@ -87,35 +92,27 @@ Public Class PageDownloadInstall
             AaCode(
             AaCode(
             Sub()
             Sub()
                 PanBack.ScrollToHome()
                 PanBack.ScrollToHome()
-                TextSelectName.Validate()
-                OptiFine_Loaded()
-                LiteLoader_Loaded()
-                Forge_Loaded()
-                NeoForge_Loaded()
-                Fabric_Loaded()
-                FabricApi_Loaded()
-                OptiFabric_Loaded()
-                ReloadSelected()
+                ReloadComponentLists()
                 PanMinecraft.Visibility = Visibility.Collapsed
                 PanMinecraft.Visibility = Visibility.Collapsed
             End Sub, After:=True),
             End Sub, After:=True),
             AaOpacity(PanSelect, 1 - PanSelect.Opacity, 70, 100),
             AaOpacity(PanSelect, 1 - PanSelect.Opacity, 70, 100),
             AaTranslateX(PanSelect, -CType(PanSelect.RenderTransform, TranslateTransform).X, 160, 100, Ease:=New AniEaseOutFluent(AniEasePower.ExtraStrong)),
             AaTranslateX(PanSelect, -CType(PanSelect.RenderTransform, TranslateTransform).X, 160, 100, Ease:=New AniEaseOutFluent(AniEasePower.ExtraStrong)),
-            AaCode(
-            Sub()
-                PanBack.IsHitTestVisible = True
-                '初始化 Binding
-                If IsFirstLoaded Then Return
-                IsFirstLoaded = True
-                BtnOptiFineClearInner.SetBinding(Shapes.Path.FillProperty, New Binding("Foreground") With {.Source = CardOptiFine.MainTextBlock, .Mode = BindingMode.OneWay})
-                BtnLiteLoaderClearInner.SetBinding(Shapes.Path.FillProperty, New Binding("Foreground") With {.Source = CardLiteLoader.MainTextBlock, .Mode = BindingMode.OneWay})
-                BtnForgeClearInner.SetBinding(Shapes.Path.FillProperty, New Binding("Foreground") With {.Source = CardForge.MainTextBlock, .Mode = BindingMode.OneWay})
-                BtnNeoForgeClearInner.SetBinding(Shapes.Path.FillProperty, New Binding("Foreground") With {.Source = CardNeoForge.MainTextBlock, .Mode = BindingMode.OneWay})
-                BtnFabricClearInner.SetBinding(Shapes.Path.FillProperty, New Binding("Foreground") With {.Source = CardFabric.MainTextBlock, .Mode = BindingMode.OneWay})
-                BtnFabricApiClearInner.SetBinding(Shapes.Path.FillProperty, New Binding("Foreground") With {.Source = CardFabricApi.MainTextBlock, .Mode = BindingMode.OneWay})
-                BtnOptiFabricClearInner.SetBinding(Shapes.Path.FillProperty, New Binding("Foreground") With {.Source = CardOptiFabric.MainTextBlock, .Mode = BindingMode.OneWay})
-            End Sub,, True)
+            AaCode(Sub() PanBack.IsHitTestVisible = True,, True)
         }, "FrmDownloadInstall SelectPageSwitch", True)
         }, "FrmDownloadInstall SelectPageSwitch", True)
     End Sub
     End Sub
+
+    Public Sub ReloadComponentLists()
+        TextSelectName.Validate()
+        OptiFine_Loaded()
+        LiteLoader_Loaded()
+        Forge_Loaded()
+        NeoForge_Loaded()
+        Fabric_Loaded()
+        FabricApi_Loaded()
+        OptiFabric_Loaded()
+        ReloadSelected()
+    End Sub
+
     Public Sub ExitSelectPage() Handles BtnBack.Click
     Public Sub ExitSelectPage() Handles BtnBack.Click
         If Not IsInSelectPage Then Return
         If Not IsInSelectPage Then Return
         IsInSelectPage = False
         IsInSelectPage = False
@@ -144,10 +141,11 @@ Public Class PageDownloadInstall
             End Sub,, True)
             End Sub,, True)
         }, "FrmDownloadInstall SelectPageSwitch")
         }, "FrmDownloadInstall SelectPageSwitch")
     End Sub
     End Sub
-    Public Sub MinecraftSelected(sender As MyListItem, e As MouseButtonEventArgs)
+    Public Overridable Sub MinecraftSelected(sender As MyListItem, e As MouseButtonEventArgs)
         VanillaName = sender.Title
         VanillaName = sender.Title
         VanillaData = sender.Tag
         VanillaData = sender.Tag
         VanillaIcon = sender.Logo
         VanillaIcon = sender.Logo
+        LabMinecraftVersion.Text = VanillaName
         EnterSelectPage()
         EnterSelectPage()
     End Sub
     End Sub
 
 
@@ -156,9 +154,9 @@ Public Class PageDownloadInstall
 #Region "选择"
 #Region "选择"
 
 
     'Minecraft
     'Minecraft
-    Private VanillaName As String
-    Private VanillaData As JObject
-    Private VanillaIcon As String
+    Public VanillaName As String
+    Public VanillaData As JObject
+    Public VanillaIcon As String
     Private ReadOnly Property VanillaDrop As Integer
     Private ReadOnly Property VanillaDrop As Integer
         Get
         Get
             Return McVersion.VersionToDrop(VanillaName)
             Return McVersion.VersionToDrop(VanillaName)
@@ -166,24 +164,25 @@ Public Class PageDownloadInstall
     End Property
     End Property
 
 
     '附加组件
     '附加组件
-    Private SelectedOptiFine As DlOptiFineListEntry = Nothing
-    Private SelectedLiteLoader As DlLiteLoaderListEntry = Nothing
-    Private SelectedForge As DlForgeVersionEntry = Nothing
-    Private SelectedNeoForge As DlNeoForgeListEntry = Nothing
-    Private SelectedFabric As String = Nothing
-    Private SelectedFabricApi As ResourceVersion = Nothing
-    Private SelectedOptiFabric As ResourceVersion = Nothing
+    Public SelectedOptiFine As DlOptiFineListEntry = Nothing
+    Public SelectedLiteLoader As DlLiteLoaderListEntry = Nothing
+    Public SelectedForge As DlForgeVersionEntry = Nothing
+    Public SelectedNeoForge As DlNeoForgeListEntry = Nothing
+    Public SelectedFabric As String = Nothing
+    Public SelectedFabricApi As ResourceVersion = Nothing
+    Public SelectedOptiFabric As ResourceVersion = Nothing
 
 
     ''' <summary>
     ''' <summary>
     ''' 重载已选择的项目的显示。
     ''' 重载已选择的项目的显示。
     ''' </summary>
     ''' </summary>
-    Private Sub ReloadSelected() Handles CardOptiFine.Swap, LoadOptiFine.StateChanged, CardForge.Swap, LoadForge.StateChanged, CardNeoForge.Swap, LoadNeoForge.StateChanged, CardFabric.Swap, LoadFabric.StateChanged, CardFabricApi.Swap, LoadFabricApi.StateChanged, CardOptiFabric.Swap, LoadOptiFabric.StateChanged, CardLiteLoader.Swap, LoadLiteLoader.StateChanged
+    Public Overridable Sub ReloadSelected() Handles CardOptiFine.Swap, LoadOptiFine.StateChanged, CardForge.Swap, LoadForge.StateChanged, CardNeoForge.Swap, LoadNeoForge.StateChanged, CardFabric.Swap, LoadFabric.StateChanged, CardFabricApi.Swap, LoadFabricApi.StateChanged, CardOptiFabric.Swap, LoadOptiFabric.StateChanged, CardLiteLoader.Swap, LoadLiteLoader.StateChanged
         Static Ongoing As Boolean = False '#3742 中,LoadOptiFineGetError 会初始化 LoadOptiFine,触发事件 LoadOptiFine.StateChanged,导致再次调用 SelectReload
         Static Ongoing As Boolean = False '#3742 中,LoadOptiFineGetError 会初始化 LoadOptiFine,触发事件 LoadOptiFine.StateChanged,导致再次调用 SelectReload
         If VanillaName Is Nothing OrElse Ongoing Then Return
         If VanillaName Is Nothing OrElse Ongoing Then Return
         Ongoing = True
         Ongoing = True
         '主预览
         '主预览
         SelectNameUpdate()
         SelectNameUpdate()
         ImgLogo.Source = GetSelectLogo()
         ImgLogo.Source = GetSelectLogo()
+        ImgMinecraft.Source = VanillaIcon
         'OptiFine
         'OptiFine
         Dim OptiFineError As String = LoadOptiFineGetError()
         Dim OptiFineError As String = LoadOptiFineGetError()
         CardOptiFine.MainSwap.Visibility = If(OptiFineError Is Nothing, Visibility.Visible, Visibility.Collapsed)
         CardOptiFine.MainSwap.Visibility = If(OptiFineError Is Nothing, Visibility.Visible, Visibility.Collapsed)
@@ -222,7 +221,7 @@ Public Class PageDownloadInstall
             End If
             End If
         End If
         End If
         'Forge
         'Forge
-        If Not McVersion.IsFormatFit(VanillaName) Then
+        If Not McVersion.IsStandardFormat(VanillaName) Then
             CardForge.Visibility = Visibility.Collapsed
             CardForge.Visibility = Visibility.Collapsed
         Else
         Else
             CardForge.Visibility = Visibility.Visible
             CardForge.Visibility = Visibility.Visible
@@ -356,7 +355,7 @@ Public Class PageDownloadInstall
     ''' <summary>
     ''' <summary>
     ''' 清空已选择的项目。
     ''' 清空已选择的项目。
     ''' </summary>
     ''' </summary>
-    Private Sub ClearSelected()
+    Public Sub ClearSelected()
         VanillaName = Nothing
         VanillaName = Nothing
         VanillaData = Nothing
         VanillaData = Nothing
         VanillaIcon = Nothing
         VanillaIcon = Nothing
@@ -367,7 +366,10 @@ Public Class PageDownloadInstall
         SelectedFabric = Nothing
         SelectedFabric = Nothing
         SelectedFabricApi = Nothing
         SelectedFabricApi = Nothing
         SelectedOptiFabric = Nothing
         SelectedOptiFabric = Nothing
-        IsSelectNameEdited = False
+        For Each ClearButton In {BtnOptiFineClear, BtnLiteLoaderClear, BtnForgeClear, BtnNeoForgeClear,
+                                 BtnFabricClear, BtnFabricApiClear, BtnOptiFabricClear}
+            ClearButton.Visibility = Visibility.Collapsed
+        Next
     End Sub
     End Sub
     '信息栏动画
     '信息栏动画
     Private Sub SetPanelVisibility(Panel As Grid, IsVisible As Boolean)
     Private Sub SetPanelVisibility(Panel As Grid, IsVisible As Boolean)
@@ -389,7 +391,7 @@ Public Class PageDownloadInstall
     ''' <summary>
     ''' <summary>
     ''' 获取版本图标。
     ''' 获取版本图标。
     ''' </summary>
     ''' </summary>
-    Private Function GetSelectLogo() As String
+    Public Overridable Function GetSelectLogo() As String
         If SelectedFabric IsNot Nothing Then
         If SelectedFabric IsNot Nothing Then
             Return "pack://application:,,,/images/Blocks/Fabric.png"
             Return "pack://application:,,,/images/Blocks/Fabric.png"
         ElseIf SelectedForge IsNot Nothing Then
         ElseIf SelectedForge IsNot Nothing Then
@@ -409,7 +411,7 @@ Public Class PageDownloadInstall
     ''' <summary>
     ''' <summary>
     ''' 获取默认版本名。
     ''' 获取默认版本名。
     ''' </summary>
     ''' </summary>
-    Private Function GetSelectName() As String
+    Public Function GetSelectName() As String
         Dim Name As String = VanillaName
         Dim Name As String = VanillaName
         If SelectedFabric IsNot Nothing Then Name += "-Fabric " & SelectedFabric.BeforeFirst("+")
         If SelectedFabric IsNot Nothing Then Name += "-Fabric " & SelectedFabric.BeforeFirst("+")
         If SelectedForge IsNot Nothing Then Name += "-Forge_" & SelectedForge.VersionName
         If SelectedForge IsNot Nothing Then Name += "-Forge_" & SelectedForge.VersionName
@@ -418,36 +420,39 @@ Public Class PageDownloadInstall
         If SelectedOptiFine IsNot Nothing Then Name += "-OptiFine_" & SelectedOptiFine.DisplayName.Replace(VanillaName & " ", "").Replace(" ", "_")
         If SelectedOptiFine IsNot Nothing Then Name += "-OptiFine_" & SelectedOptiFine.DisplayName.Replace(VanillaName & " ", "").Replace(" ", "_")
         Return Name
         Return Name
     End Function
     End Function
-    Private IsSelectNameEdited As Boolean = False
-    Private IsSelectNameChanging As Boolean = False
+    Public IsSelectNameEdited As Boolean = False
     Private Sub SelectNameUpdate()
     Private Sub SelectNameUpdate()
-        If IsSelectNameEdited OrElse IsSelectNameChanging Then Return
-        IsSelectNameChanging = True
+        If IsSelectNameEdited Then Return
         TextSelectName.Text = GetSelectName()
         TextSelectName.Text = GetSelectName()
-        IsSelectNameChanging = False
+        IsSelectNameEdited = False
     End Sub
     End Sub
     Private Sub TextSelectName_TextChanged(sender As Object, e As TextChangedEventArgs) Handles TextSelectName.TextChanged
     Private Sub TextSelectName_TextChanged(sender As Object, e As TextChangedEventArgs) Handles TextSelectName.TextChanged
-        If IsSelectNameChanging Then Return
         IsSelectNameEdited = True
         IsSelectNameEdited = True
-        ReloadSelected()
     End Sub
     End Sub
     Private Sub TextSelectName_ValidateChanged(sender As Object, e As EventArgs) Handles TextSelectName.ValidateChanged
     Private Sub TextSelectName_ValidateChanged(sender As Object, e As EventArgs) Handles TextSelectName.ValidateChanged
         BtnStart.IsEnabled = TextSelectName.IsValidated
         BtnStart.IsEnabled = TextSelectName.IsValidated
+        HintSelectName.Text = TextSelectName.ValidateResult
+        HintSelectName.Visibility = If(TextSelectName.IsValidated, Visibility.Collapsed, Visibility.Visible)
     End Sub
     End Sub
 
 
 #End Region
 #End Region
 
 
 #Region "加载器"
 #Region "加载器"
+    Public MinecraftVersions As JArray
+    Private Sub LoadMinecraft_OnFinish()
+        MinecraftVersions = CType(DlClientListLoader.Output.Value("versions").DeepClone(), JArray)
+        ShowMinecraftVersionList()
+        TrySelectWaitingVersion() '自动选择版本
+    End Sub
 
 
     '结果数据化
     '结果数据化
-    Private Sub LoadMinecraft_OnFinish()
+    Public Overridable Sub ShowMinecraftVersionList()
         ExitSelectPage() '返回
         ExitSelectPage() '返回
         Try
         Try
             Dim Dict As New Dictionary(Of String, List(Of JObject)) From {
             Dim Dict As New Dictionary(Of String, List(Of JObject)) From {
                 {"正式版", New List(Of JObject)}, {"预览版", New List(Of JObject)}, {"远古版", New List(Of JObject)}, {"愚人节版", New List(Of JObject)}
                 {"正式版", New List(Of JObject)}, {"预览版", New List(Of JObject)}, {"远古版", New List(Of JObject)}, {"愚人节版", New List(Of JObject)}
             }
             }
-            Dim Versions As JArray = DlClientListLoader.Output.Value("versions")
-            For Each Version As JObject In Versions
+            For Each Version As JObject In MinecraftVersions
                 '确定分类
                 '确定分类
                 Dim Type As String = Version("type")
                 Dim Type As String = Version("type")
                 Select Case Type
                 Select Case Type
@@ -498,7 +503,7 @@ Public Class PageDownloadInstall
             '清空当前
             '清空当前
             PanMinecraft.Children.Clear()
             PanMinecraft.Children.Clear()
             '添加最新版本
             '添加最新版本
-            Dim CardInfo As New MyCard With {.Title = "最新版本", .Margin = New Thickness(0, 15, 0, 15), .SwapType = 2}
+            Dim CardInfo As New MyCard With {.Title = "最新版本", .Margin = New Thickness(0, 15, 0, 15)}
             Dim PinnedVersions As New List(Of JObject)
             Dim PinnedVersions As New List(Of JObject)
             Dim Release As JObject = Dict("正式版")(0).DeepClone()
             Dim Release As JObject = Dict("正式版")(0).DeepClone()
             Release("lore") = "最新正式版,发布于 " & Release("releaseTime").Value(Of Date).ToString("yyyy'/'MM'/'dd HH':'mm")
             Release("lore") = "最新正式版,发布于 " & Release("releaseTime").Value(Of Date).ToString("yyyy'/'MM'/'dd HH':'mm")
@@ -508,36 +513,41 @@ Public Class PageDownloadInstall
                 Snapshot("lore") = "最新预览版,发布于 " & Snapshot("releaseTime").Value(Of Date).ToString("yyyy'/'MM'/'dd HH':'mm")
                 Snapshot("lore") = "最新预览版,发布于 " & Snapshot("releaseTime").Value(Of Date).ToString("yyyy'/'MM'/'dd HH':'mm")
                 PinnedVersions.Add(Snapshot)
                 PinnedVersions.Add(Snapshot)
             End If
             End If
-            Dim PanInfo As New StackPanel With {.Margin = New Thickness(20, MyCard.SwapedHeight, 18, 0), .VerticalAlignment = VerticalAlignment.Top, .RenderTransform = New TranslateTransform(0, 0), .Tag = PinnedVersions}
-            MyCard.StackInstall(PanInfo, 7)
+            Dim PanInfo As New StackPanel With {.Margin = New Thickness(20, MyCard.SwapedHeight, 18, 0), .VerticalAlignment = VerticalAlignment.Top, .RenderTransform = New TranslateTransform(0, 0)}
             CardInfo.Children.Add(PanInfo)
             CardInfo.Children.Add(PanInfo)
+            CardInfo.SwapControl = PanInfo
+            CardInfo.SetContentBuilder(PinnedVersions, '使用 Lambda 转发点击事件,避免 #535
+                Function(Version) McDownloadListItem(Version, Sub(sender, e) MinecraftSelected(sender, e), False))
+            CardInfo.BuildContent()
             PanMinecraft.Children.Insert(0, CardInfo)
             PanMinecraft.Children.Insert(0, CardInfo)
             '添加其他版本
             '添加其他版本
             For Each Pair As KeyValuePair(Of String, List(Of JObject)) In Dict
             For Each Pair As KeyValuePair(Of String, List(Of JObject)) In Dict
                 If Not Pair.Value.Any() Then Continue For
                 If Not Pair.Value.Any() Then Continue For
                 '增加卡片
                 '增加卡片
-                Dim NewCard As New MyCard With {.Title = Pair.Key, .Margin = New Thickness(0, 0, 0, 15), .SwapType = 7}
-                Dim NewStack As New StackPanel With {.Margin = New Thickness(20, MyCard.SwapedHeight, 18, 0), .VerticalAlignment = VerticalAlignment.Top, .RenderTransform = New TranslateTransform(0, 0), .Tag = Pair.Value}
+                Dim NewCard As New MyCard With {.Title = Pair.Key, .Margin = New Thickness(0, 0, 0, 15)}
+                Dim NewStack As New StackPanel With {.Margin = New Thickness(20, MyCard.SwapedHeight, 18, 0), .VerticalAlignment = VerticalAlignment.Top, .RenderTransform = New TranslateTransform(0, 0)}
                 NewCard.Children.Add(NewStack)
                 NewCard.Children.Add(NewStack)
                 NewCard.SwapControl = NewStack
                 NewCard.SwapControl = NewStack
+                NewCard.SetContentBuilder(Pair.Value,
+                    Function(Version) McDownloadListItem(Version, Sub(sender, e) MinecraftSelected(sender, e), False))
                 NewCard.IsSwapped = True
                 NewCard.IsSwapped = True
                 PanMinecraft.Children.Add(NewCard)
                 PanMinecraft.Children.Add(NewCard)
             Next
             Next
-            '自动选择版本
-            If VersionWaitingSelect Is Nothing Then Exit Try
-            Logger.Info($"自动选择 MC 版本:{VersionWaitingSelect}")
-            For Each Version As JObject In Versions
-                If Version("id").ToString <> VersionWaitingSelect Then Continue For
-                Dim Item = McDownloadListItem(Version, Sub()
-                                                       End Sub, False).Init()
-                MinecraftSelected(Item, Nothing)
-                VersionWaitingSelect = Nothing
-                Return
-            Next
         Catch ex As Exception
         Catch ex As Exception
             Logger.Error(ex, "可视化安装版本列表出错")
             Logger.Error(ex, "可视化安装版本列表出错")
         End Try
         End Try
     End Sub
     End Sub
+    Private Sub TrySelectWaitingVersion()
+        If VersionWaitingSelect Is Nothing Then Return
+        For Each Version As JObject In MinecraftVersions
+            If Version("id").ToString <> VersionWaitingSelect Then Continue For
+            Logger.Info($"自动选择 MC 版本:{VersionWaitingSelect}")
+            MinecraftSelected(McDownloadListItem(Version, Nothing, False).Init(), Nothing)
+            VersionWaitingSelect = Nothing
+            Return
+        Next
+    End Sub
+
     ''' <summary>
     ''' <summary>
     ''' 当 MC 版本列表加载完时,立即自动选择的版本。用于外部调用。
     ''' 当 MC 版本列表加载完时,立即自动选择的版本。用于外部调用。
     ''' </summary>
     ''' </summary>
@@ -550,16 +560,16 @@ Public Class PageDownloadInstall
     ''' <summary>
     ''' <summary>
     ''' 获取 OptiFine 的加载异常信息。若正常则返回 Nothing。
     ''' 获取 OptiFine 的加载异常信息。若正常则返回 Nothing。
     ''' </summary>
     ''' </summary>
-    Private Function LoadOptiFineGetError() As String
+    Public Function LoadOptiFineGetError() As String
         If SelectedNeoForge IsNot Nothing Then Return "与 NeoForge 不兼容"
         If SelectedNeoForge IsNot Nothing Then Return "与 NeoForge 不兼容"
         '检查 Forge 1.13 - 1.14.3:全部不兼容
         '检查 Forge 1.13 - 1.14.3:全部不兼容
         If SelectedForge IsNot Nothing AndAlso
         If SelectedForge IsNot Nothing AndAlso
-            CompareVersion(VanillaName, "1.13") >= 0 AndAlso CompareVersion("1.14.3", VanillaName) >= 0 Then
+            McUtils.CompareVersionGE(VanillaName, "1.13") AndAlso McUtils.CompareVersionGE("1.14.3", VanillaName) Then
             Return "与 Forge 不兼容"
             Return "与 Forge 不兼容"
         End If
         End If
         '检查 Fabric 1.20.5+:全部不兼容
         '检查 Fabric 1.20.5+:全部不兼容
         If SelectedFabric IsNot Nothing AndAlso
         If SelectedFabric IsNot Nothing AndAlso
-            CompareVersion(VanillaName, "1.20.4") > 0 Then
+            McUtils.CompareVersion(VanillaName, "1.20.4") > 0 Then
             Return "与 Fabric 不兼容"
             Return "与 Fabric 不兼容"
         End If
         End If
         '检查 Loader
         '检查 Loader
@@ -584,12 +594,12 @@ Public Class PageDownloadInstall
     End Function
     End Function
 
 
     '检查某个 OptiFine 是否与某个 Forge 兼容
     '检查某个 OptiFine 是否与某个 Forge 兼容
-    Private Function IsOptiFineSuitForForge(OptiFine As DlOptiFineListEntry, Forge As DlForgeVersionEntry)
+    Public Shared Function IsOptiFineSuitForForge(OptiFine As DlOptiFineListEntry, Forge As DlForgeVersionEntry) As Boolean
         If Forge.Inherit <> OptiFine.Inherit Then Return False '不是同一个大版本
         If Forge.Inherit <> OptiFine.Inherit Then Return False '不是同一个大版本
         If OptiFine.RequiredForgeVersion Is Nothing Then Return False '不兼容 Forge
         If OptiFine.RequiredForgeVersion Is Nothing Then Return False '不兼容 Forge
         If String.IsNullOrWhiteSpace(OptiFine.RequiredForgeVersion) Then Return True '#4183
         If String.IsNullOrWhiteSpace(OptiFine.RequiredForgeVersion) Then Return True '#4183
         If OptiFine.RequiredForgeVersion.Contains(".") Then 'XX.X.XXX
         If OptiFine.RequiredForgeVersion.Contains(".") Then 'XX.X.XXX
-            Return CompareVersion(Forge.Version.ToString, OptiFine.RequiredForgeVersion) = 0
+            Return McUtils.CompareVersion(Forge.Version.ToString, OptiFine.RequiredForgeVersion) = 0
         Else 'XXXX
         Else 'XXXX
             Return Forge.Version.Revision = OptiFine.RequiredForgeVersion
             Return Forge.Version.Revision = OptiFine.RequiredForgeVersion
         End If
         End If
@@ -619,7 +629,7 @@ Public Class PageDownloadInstall
             Function(Left As DlOptiFineListEntry, Right As DlOptiFineListEntry) As Boolean
             Function(Left As DlOptiFineListEntry, Right As DlOptiFineListEntry) As Boolean
                 If Not Left.IsPreview AndAlso Right.IsPreview Then Return True
                 If Not Left.IsPreview AndAlso Right.IsPreview Then Return True
                 If Left.IsPreview AndAlso Not Right.IsPreview Then Return False
                 If Left.IsPreview AndAlso Not Right.IsPreview Then Return False
-                Return CompareVersionGE(Left.DisplayName, Right.DisplayName)
+                Return McUtils.CompareVersionGE(Left.DisplayName, Right.DisplayName)
             End Function)
             End Function)
             '可视化
             '可视化
             PanOptiFine.Children.Clear()
             PanOptiFine.Children.Clear()
@@ -641,12 +651,12 @@ Public Class PageDownloadInstall
         CardOptiFine.IsSwapped = True
         CardOptiFine.IsSwapped = True
         ReloadSelected()
         ReloadSelected()
     End Sub
     End Sub
-    Private Sub OptiFine_Clear(sender As Object, e As MouseButtonEventArgs) Handles BtnOptiFineClear.MouseLeftButtonUp
+    Private Sub OptiFine_Clear(sender As Object, e As EventArgs) Handles BtnOptiFineClear.Click
         SelectedOptiFine = Nothing
         SelectedOptiFine = Nothing
         SelectedOptiFabric = Nothing
         SelectedOptiFabric = Nothing
+        BtnOptiFabricClear.Visibility = Visibility.Collapsed
         AutoSelectedOptiFabric = False
         AutoSelectedOptiFabric = False
         CardOptiFine.IsSwapped = True
         CardOptiFine.IsSwapped = True
-        e.Handled = True
         Forge_Loaded()
         Forge_Loaded()
         NeoForge_Loaded()
         NeoForge_Loaded()
         ReloadSelected()
         ReloadSelected()
@@ -659,7 +669,7 @@ Public Class PageDownloadInstall
     ''' <summary>
     ''' <summary>
     ''' 获取 LiteLoader 的加载异常信息。若正常则返回 Nothing。
     ''' 获取 LiteLoader 的加载异常信息。若正常则返回 Nothing。
     ''' </summary>
     ''' </summary>
-    Private Function LoadLiteLoaderGetError() As String
+    Public Function LoadLiteLoaderGetError() As String
         '检查 Loader
         '检查 Loader
         If GetLoaderError(LoadLiteLoader) IsNot Nothing Then Return GetLoaderError(LoadLiteLoader)
         If GetLoaderError(LoadLiteLoader) IsNot Nothing Then Return GetLoaderError(LoadLiteLoader)
         '检查版本
         '检查版本
@@ -699,10 +709,9 @@ Public Class PageDownloadInstall
         CardLiteLoader.IsSwapped = True
         CardLiteLoader.IsSwapped = True
         ReloadSelected()
         ReloadSelected()
     End Sub
     End Sub
-    Private Sub LiteLoader_Clear(sender As Object, e As MouseButtonEventArgs) Handles BtnLiteLoaderClear.MouseLeftButtonUp
+    Private Sub LiteLoader_Clear(sender As Object, e As EventArgs) Handles BtnLiteLoaderClear.Click
         SelectedLiteLoader = Nothing
         SelectedLiteLoader = Nothing
         CardLiteLoader.IsSwapped = True
         CardLiteLoader.IsSwapped = True
-        e.Handled = True
         ReloadSelected()
         ReloadSelected()
     End Sub
     End Sub
 
 
@@ -713,8 +722,8 @@ Public Class PageDownloadInstall
     ''' <summary>
     ''' <summary>
     ''' 获取 Forge 的加载异常信息。若正常则返回 Nothing。
     ''' 获取 Forge 的加载异常信息。若正常则返回 Nothing。
     ''' </summary>
     ''' </summary>
-    Private Function LoadForgeGetError() As String
-        If CompareVersionGE("1.5.1", VanillaName) AndAlso CompareVersionGE(VanillaName, "1.1") Then Return "无"
+    Public Function LoadForgeGetError() As String
+        If McUtils.CompareVersionGE("1.5.1", VanillaName) AndAlso McUtils.CompareVersionGE(VanillaName, "1.1") Then Return "无"
         '检查 Loader
         '检查 Loader
         If GetLoaderError(LoadForge) IsNot Nothing Then Return GetLoaderError(LoadForge)
         If GetLoaderError(LoadForge) IsNot Nothing Then Return GetLoaderError(LoadForge)
         Dim Loader As LoaderTask(Of String, List(Of DlForgeVersionEntry)) = LoadForge.State
         Dim Loader As LoaderTask(Of String, List(Of DlForgeVersionEntry)) = LoadForge.State
@@ -725,7 +734,7 @@ Public Class PageDownloadInstall
             If SelectedNeoForge IsNot Nothing Then Return "与 NeoForge 不兼容"
             If SelectedNeoForge IsNot Nothing Then Return "与 NeoForge 不兼容"
             If SelectedFabric IsNot Nothing Then Return "与 Fabric 不兼容"
             If SelectedFabric IsNot Nothing Then Return "与 Fabric 不兼容"
             If SelectedOptiFine IsNot Nothing AndAlso
             If SelectedOptiFine IsNot Nothing AndAlso
-                CompareVersionGE(VanillaName, "1.13") AndAlso CompareVersionGE("1.14.3", VanillaName) Then
+                McUtils.CompareVersionGE(VanillaName, "1.13") AndAlso McUtils.CompareVersionGE("1.14.3", VanillaName) Then
                 Return "与 OptiFine 不兼容" '1.13 ~ 1.14.3 OptiFine 检查
                 Return "与 OptiFine 不兼容" '1.13 ~ 1.14.3 OptiFine 检查
             End If
             End If
             If SelectedOptiFine IsNot Nothing AndAlso Not IsOptiFineSuitForForge(SelectedOptiFine, Version) Then Continue For
             If SelectedOptiFine IsNot Nothing AndAlso Not IsOptiFineSuitForForge(SelectedOptiFine, Version) Then Continue For
@@ -775,10 +784,9 @@ Public Class PageDownloadInstall
         OptiFine_Loaded()
         OptiFine_Loaded()
         ReloadSelected()
         ReloadSelected()
     End Sub
     End Sub
-    Private Sub Forge_Clear(sender As Object, e As MouseButtonEventArgs) Handles BtnForgeClear.MouseLeftButtonUp
+    Private Sub Forge_Clear(sender As Object, e As EventArgs) Handles BtnForgeClear.Click
         SelectedForge = Nothing
         SelectedForge = Nothing
         CardForge.IsSwapped = True
         CardForge.IsSwapped = True
-        e.Handled = True
         OptiFine_Loaded()
         OptiFine_Loaded()
         ReloadSelected()
         ReloadSelected()
     End Sub
     End Sub
@@ -790,7 +798,7 @@ Public Class PageDownloadInstall
     ''' <summary>
     ''' <summary>
     ''' 获取 NeoForge 的加载异常信息。若正常则返回 Nothing。
     ''' 获取 NeoForge 的加载异常信息。若正常则返回 Nothing。
     ''' </summary>
     ''' </summary>
-    Private Function LoadNeoForgeGetError() As String
+    Public Function LoadNeoForgeGetError() As String
         If SelectedOptiFine IsNot Nothing Then Return "与 OptiFine 不兼容"
         If SelectedOptiFine IsNot Nothing Then Return "与 OptiFine 不兼容"
         If SelectedForge IsNot Nothing Then Return "与 Forge 不兼容"
         If SelectedForge IsNot Nothing Then Return "与 Forge 不兼容"
         If SelectedFabric IsNot Nothing Then Return "与 Fabric 不兼容"
         If SelectedFabric IsNot Nothing Then Return "与 Fabric 不兼容"
@@ -832,10 +840,9 @@ Public Class PageDownloadInstall
         OptiFine_Loaded()
         OptiFine_Loaded()
         ReloadSelected()
         ReloadSelected()
     End Sub
     End Sub
-    Private Sub NeoForge_Clear(sender As Object, e As MouseButtonEventArgs) Handles BtnNeoForgeClear.MouseLeftButtonUp
+    Private Sub NeoForge_Clear(sender As Object, e As EventArgs) Handles BtnNeoForgeClear.Click
         SelectedNeoForge = Nothing
         SelectedNeoForge = Nothing
         CardNeoForge.IsSwapped = True
         CardNeoForge.IsSwapped = True
-        e.Handled = True
         OptiFine_Loaded()
         OptiFine_Loaded()
         ReloadSelected()
         ReloadSelected()
     End Sub
     End Sub
@@ -847,9 +854,9 @@ Public Class PageDownloadInstall
     ''' <summary>
     ''' <summary>
     ''' 获取 Fabric 的加载异常信息。若正常则返回 Nothing。
     ''' 获取 Fabric 的加载异常信息。若正常则返回 Nothing。
     ''' </summary>
     ''' </summary>
-    Private Function LoadFabricGetError() As String
+    Public Function LoadFabricGetError() As String
         '检查 OptiFine 1.20.5+:没有 OptiFabric 故全部不兼容
         '检查 OptiFine 1.20.5+:没有 OptiFabric 故全部不兼容
-        If SelectedOptiFine IsNot Nothing AndAlso CompareVersionGE(VanillaName, "1.20.5") Then Return "与 OptiFine 不兼容"
+        If SelectedOptiFine IsNot Nothing AndAlso McUtils.CompareVersionGE(VanillaName, "1.20.5") Then Return "与 OptiFine 不兼容"
         '检查 Loader
         '检查 Loader
         If GetLoaderError(LoadFabric) IsNot Nothing Then Return GetLoaderError(LoadFabric)
         If GetLoaderError(LoadFabric) IsNot Nothing Then Return GetLoaderError(LoadFabric)
         '检查版本
         '检查版本
@@ -879,9 +886,10 @@ Public Class PageDownloadInstall
             If Not Versions.Any() Then Return
             If Not Versions.Any() Then Return
             '可视化
             '可视化
             PanFabric.Children.Clear()
             PanFabric.Children.Clear()
-            PanFabric.Tag = Versions
             CardFabric.SwapControl = PanFabric
             CardFabric.SwapControl = PanFabric
-            CardFabric.SwapType = 12
+            CardFabric.SetContentBuilder(Versions.OfType(Of JObject),
+                Function(Version) FabricDownloadListItem(Version, AddressOf Fabric_Selected))
+            If Not CardFabric.IsSwapped Then CardFabric.BuildContent()
         Catch ex As Exception
         Catch ex As Exception
             Logger.Error(ex, "可视化 Fabric 安装版本列表出错")
             Logger.Error(ex, "可视化 Fabric 安装版本列表出错")
         End Try
         End Try
@@ -895,14 +903,15 @@ Public Class PageDownloadInstall
         CardFabric.IsSwapped = True
         CardFabric.IsSwapped = True
         ReloadSelected()
         ReloadSelected()
     End Sub
     End Sub
-    Private Sub Fabric_Clear(sender As Object, e As MouseButtonEventArgs) Handles BtnFabricClear.MouseLeftButtonUp
+    Private Sub Fabric_Clear(sender As Object, e As EventArgs) Handles BtnFabricClear.Click
         SelectedFabric = Nothing
         SelectedFabric = Nothing
         SelectedFabricApi = Nothing
         SelectedFabricApi = Nothing
+        BtnFabricApiClear.Visibility = Visibility.Collapsed
         AutoSelectedFabricApi = False
         AutoSelectedFabricApi = False
         SelectedOptiFabric = Nothing
         SelectedOptiFabric = Nothing
+        BtnOptiFabricClear.Visibility = Visibility.Collapsed
         AutoSelectedOptiFabric = False
         AutoSelectedOptiFabric = False
         CardFabric.IsSwapped = True
         CardFabric.IsSwapped = True
-        e.Handled = True
         ReloadSelected()
         ReloadSelected()
     End Sub
     End Sub
 
 
@@ -911,14 +920,14 @@ Public Class PageDownloadInstall
 #Region "Fabric API 列表"
 #Region "Fabric API 列表"
 
 
     ''' <summary>
     ''' <summary>
-    ''' 判断某 Fabric API 是否适配当前选择的原版版本。
+    ''' 判断某 Fabric API 是否适配指定的原版版本。
     ''' </summary>
     ''' </summary>
-    Public Function IsFabricApiCompatible(FabricApi As ResourceVersion) As Boolean
+    Public Shared Function IsFabricApiCompatible(FabricApi As ResourceVersion, MinecraftVersion As String) As Boolean
         Dim FabricApiName = FabricApi.Display
         Dim FabricApiName = FabricApi.Display
         Try
         Try
-            If FabricApiName Is Nothing OrElse VanillaName Is Nothing Then Return False
+            If FabricApiName Is Nothing OrElse MinecraftVersion Is Nothing Then Return False
             FabricApiName = FabricApiName.Lower
             FabricApiName = FabricApiName.Lower
-            Dim TargetName = VanillaName.Replace("∞", "infinite").Replace("Combat Test 7c", "1.16_combat-3").Lower
+            Dim TargetName = MinecraftVersion.Replace("∞", "infinite").Replace("Combat Test 7c", "1.16_combat-3").Lower
             If FabricApiName.StartsWithF("[" & TargetName & "]") Then Return True
             If FabricApiName.StartsWithF("[" & TargetName & "]") Then Return True
             If Not FabricApiName.Contains("/") OrElse Not FabricApiName.Contains("]") Then Return False
             If Not FabricApiName.Contains("/") OrElse Not FabricApiName.Contains("]") Then Return False
             '直接的判断(例如 1.18.1/22w03a)
             '直接的判断(例如 1.18.1/22w03a)
@@ -946,7 +955,7 @@ Public Class PageDownloadInstall
             End While
             End While
             Return True
             Return True
         Catch ex As Exception
         Catch ex As Exception
-            Logger.Warn(ex, $"判断 Fabric API 版本适配性出错({FabricApiName}, {VanillaName})")
+            Logger.Warn(ex, $"判断 Fabric API 版本适配性出错({FabricApiName}, {MinecraftVersion})")
             Return False
             Return False
         End Try
         End Try
     End Function
     End Function
@@ -954,12 +963,12 @@ Public Class PageDownloadInstall
     ''' <summary>
     ''' <summary>
     ''' 获取 FabricApi 的加载异常信息。若正常则返回 Nothing。
     ''' 获取 FabricApi 的加载异常信息。若正常则返回 Nothing。
     ''' </summary>
     ''' </summary>
-    Private Function LoadFabricApiGetError() As String
+    Public Function LoadFabricApiGetError() As String
         '检查 Loader
         '检查 Loader
         If GetLoaderError(LoadFabricApi) IsNot Nothing Then Return GetLoaderError(LoadFabricApi)
         If GetLoaderError(LoadFabricApi) IsNot Nothing Then Return GetLoaderError(LoadFabricApi)
         If DlFabricApiLoader.Output Is Nothing Then Return If(SelectedFabric Is Nothing, "需要安装 Fabric", "获取中……")
         If DlFabricApiLoader.Output Is Nothing Then Return If(SelectedFabric Is Nothing, "需要安装 Fabric", "获取中……")
         '检查版本
         '检查版本
-        If DlFabricApiLoader.Output.Any(Function(f) IsFabricApiCompatible(f)) Then
+        If DlFabricApiLoader.Output.Any(Function(f) IsFabricApiCompatible(f, VanillaName)) Then
             Return If(SelectedFabric Is Nothing, "需要安装 Fabric", Nothing)
             Return If(SelectedFabric Is Nothing, "需要安装 Fabric", Nothing)
         Else
         Else
             Return "无"
             Return "无"
@@ -971,7 +980,7 @@ Public Class PageDownloadInstall
         If LoadFabricApiGetError() IsNot Nothing Then e.Handled = True
         If LoadFabricApiGetError() IsNot Nothing Then e.Handled = True
     End Sub
     End Sub
 
 
-    Private AutoSelectedFabricApi As Boolean = False
+    Public AutoSelectedFabricApi As Boolean = False
     ''' <summary>
     ''' <summary>
     ''' 尝试重新可视化 FabricApi 版本列表。
     ''' 尝试重新可视化 FabricApi 版本列表。
     ''' </summary>
     ''' </summary>
@@ -982,7 +991,7 @@ Public Class PageDownloadInstall
             '获取版本列表
             '获取版本列表
             Dim Versions As New List(Of ResourceVersion)
             Dim Versions As New List(Of ResourceVersion)
             For Each Version In DlFabricApiLoader.Output
             For Each Version In DlFabricApiLoader.Output
-                If IsFabricApiCompatible(Version) Then
+                If IsFabricApiCompatible(Version, VanillaName) Then
                     If Not Version.Display.StartsWithF("[") Then
                     If Not Version.Display.StartsWithF("[") Then
                         Logger.Warn($"已特判修改 Fabric API 显示名:{Version.Display}")
                         Logger.Warn($"已特判修改 Fabric API 显示名:{Version.Display}")
                         Version.Display = "[" & VanillaName & "] " & Version.Display
                         Version.Display = "[" & VanillaName & "] " & Version.Display
@@ -995,7 +1004,6 @@ Public Class PageDownloadInstall
             '可视化
             '可视化
             PanFabricApi.Children.Clear()
             PanFabricApi.Children.Clear()
             For Each Version In Versions
             For Each Version In Versions
-                If Not IsFabricApiCompatible(Version) Then Continue For
                 PanFabricApi.Children.Add(FabricApiDownloadListItem(Version, AddressOf FabricApi_Selected))
                 PanFabricApi.Children.Add(FabricApiDownloadListItem(Version, AddressOf FabricApi_Selected))
             Next
             Next
             '自动选择 Fabric API
             '自动选择 Fabric API
@@ -1016,10 +1024,9 @@ Public Class PageDownloadInstall
         CardFabricApi.IsSwapped = True
         CardFabricApi.IsSwapped = True
         ReloadSelected()
         ReloadSelected()
     End Sub
     End Sub
-    Private Sub FabricApi_Clear(sender As Object, e As MouseButtonEventArgs) Handles BtnFabricApiClear.MouseLeftButtonUp
+    Private Sub FabricApi_Clear(sender As Object, e As EventArgs) Handles BtnFabricApiClear.Click
         SelectedFabricApi = Nothing
         SelectedFabricApi = Nothing
         CardFabricApi.IsSwapped = True
         CardFabricApi.IsSwapped = True
-        e.Handled = True
         ReloadSelected()
         ReloadSelected()
     End Sub
     End Sub
 
 
@@ -1028,23 +1035,23 @@ Public Class PageDownloadInstall
 #Region "OptiFabric 列表"
 #Region "OptiFabric 列表"
 
 
     ''' <summary>
     ''' <summary>
-    ''' 判断某 OptiFabric 是否适配当前选择的原版版本。
+    ''' 判断某 OptiFabric 是否适配指定的原版版本。
     ''' </summary>
     ''' </summary>
-    Private Function IsOptiFabricCompatible(ModFile As ResourceVersion) As Boolean
+    Public Shared Function IsOptiFabricCompatible(ModFile As ResourceVersion, MinecraftVersion As String) As Boolean
         Try
         Try
-            If VanillaName Is Nothing Then Return False
-            Return ModFile.GameVersions.Contains(VanillaName)
+            If MinecraftVersion Is Nothing Then Return False
+            Return ModFile.GameVersions.Contains(MinecraftVersion)
         Catch ex As Exception
         Catch ex As Exception
-            Logger.Warn(ex, $"判断 OptiFabric 版本适配性出错({VanillaName})")
+            Logger.Warn(ex, $"判断 OptiFabric 版本适配性出错({MinecraftVersion})")
             Return False
             Return False
         End Try
         End Try
     End Function
     End Function
 
 
-    Private AutoSelectedOptiFabric As Boolean = False
+    Public AutoSelectedOptiFabric As Boolean = False
     ''' <summary>
     ''' <summary>
     ''' 获取 OptiFabric 的加载异常信息。若正常则返回 Nothing。
     ''' 获取 OptiFabric 的加载异常信息。若正常则返回 Nothing。
     ''' </summary>
     ''' </summary>
-    Private Function LoadOptiFabricGetError() As String
+    Public Function LoadOptiFabricGetError() As String
         If VanillaDrop >= 140 AndAlso VanillaDrop <= 150 Then Return "不兼容老版本 Fabric,请手动下载 OptiFabric Origins"
         If VanillaDrop >= 140 AndAlso VanillaDrop <= 150 Then Return "不兼容老版本 Fabric,请手动下载 OptiFabric Origins"
         '检查 Loader
         '检查 Loader
         If GetLoaderError(LoadOptiFabric) IsNot Nothing Then Return GetLoaderError(LoadOptiFabric)
         If GetLoaderError(LoadOptiFabric) IsNot Nothing Then Return GetLoaderError(LoadOptiFabric)
@@ -1056,7 +1063,7 @@ Public Class PageDownloadInstall
             Return "获取中……"
             Return "获取中……"
         End If
         End If
         For Each Version In DlOptiFabricLoader.Output
         For Each Version In DlOptiFabricLoader.Output
-            If Not IsOptiFabricCompatible(Version) Then Continue For '2135#
+            If Not IsOptiFabricCompatible(Version, VanillaName) Then Continue For '2135#
             If SelectedFabric Is Nothing AndAlso SelectedOptiFine Is Nothing Then Return "需要安装 OptiFine 与 Fabric"
             If SelectedFabric Is Nothing AndAlso SelectedOptiFine Is Nothing Then Return "需要安装 OptiFine 与 Fabric"
             If SelectedFabric Is Nothing Then Return "需要安装 Fabric"
             If SelectedFabric Is Nothing Then Return "需要安装 Fabric"
             If SelectedOptiFine Is Nothing Then Return "需要安装 OptiFine"
             If SelectedOptiFine Is Nothing Then Return "需要安装 OptiFine"
@@ -1080,7 +1087,7 @@ Public Class PageDownloadInstall
             '获取版本列表
             '获取版本列表
             Dim Versions As New List(Of ResourceVersion)
             Dim Versions As New List(Of ResourceVersion)
             For Each Version In DlOptiFabricLoader.Output
             For Each Version In DlOptiFabricLoader.Output
-                If IsOptiFabricCompatible(Version) Then Versions.Add(Version)
+                If IsOptiFabricCompatible(Version, VanillaName) Then Versions.Add(Version)
             Next
             Next
             If Not Versions.Any() Then Return
             If Not Versions.Any() Then Return
             '排序
             '排序
@@ -1088,7 +1095,6 @@ Public Class PageDownloadInstall
             '可视化
             '可视化
             PanOptiFabric.Children.Clear()
             PanOptiFabric.Children.Clear()
             For Each Version In Versions
             For Each Version In Versions
-                If Not IsOptiFabricCompatible(Version) Then Continue For
                 PanOptiFabric.Children.Add(OptiFabricDownloadListItem(Version, AddressOf OptiFabric_Selected))
                 PanOptiFabric.Children.Add(OptiFabricDownloadListItem(Version, AddressOf OptiFabric_Selected))
             Next
             Next
             '自动选择 OptiFabric
             '自动选择 OptiFabric
@@ -1108,10 +1114,9 @@ Public Class PageDownloadInstall
         CardOptiFabric.IsSwapped = True
         CardOptiFabric.IsSwapped = True
         ReloadSelected()
         ReloadSelected()
     End Sub
     End Sub
-    Private Sub OptiFabric_Clear(sender As Object, e As MouseButtonEventArgs) Handles BtnOptiFabricClear.MouseLeftButtonUp
+    Private Sub OptiFabric_Clear(sender As Object, e As EventArgs) Handles BtnOptiFabricClear.Click
         SelectedOptiFabric = Nothing
         SelectedOptiFabric = Nothing
         CardOptiFabric.IsSwapped = True
         CardOptiFabric.IsSwapped = True
-        e.Handled = True
         ReloadSelected()
         ReloadSelected()
     End Sub
     End Sub
 
 
@@ -1119,35 +1124,52 @@ Public Class PageDownloadInstall
 
 
 #Region "安装"
 #Region "安装"
 
 
+    ''' <summary>
+    ''' 获取当前选择的组件信息。
+    ''' </summary>
+    Public Function GetSelectedComponents() As McComponents
+        Return New McComponents(
+            minecraftVersion:=VanillaName,
+            optiFineEntry:=If(SelectedOptiFine Is Nothing, Nothing, JObject.FromObject(SelectedOptiFine)),
+            forgeVersion:=SelectedForge?.VersionName,
+            neoForgeVersion:=SelectedNeoForge?.VersionName,
+            fabricVersion:=SelectedFabric,
+            fabricApiVersion:=If(SelectedFabricApi?.Version, If(SelectedFabricApi?.Id, SelectedFabricApi?.Display)),
+            optiFabricVersion:=If(SelectedOptiFabric?.Version, If(SelectedOptiFabric?.Id, SelectedOptiFabric?.Display)),
+            liteLoaderVersion:=SelectedLiteLoader?.JsonToken?("version").ToString())
+    End Function
+
     Private Sub TextSelectName_KeyDown(sender As Object, e As KeyEventArgs) Handles TextSelectName.KeyDown
     Private Sub TextSelectName_KeyDown(sender As Object, e As KeyEventArgs) Handles TextSelectName.KeyDown
         If e.Key = Key.Enter AndAlso IsInSelectPage AndAlso BtnStart.IsEnabled Then BtnStart_Click()
         If e.Key = Key.Enter AndAlso IsInSelectPage AndAlso BtnStart.IsEnabled Then BtnStart_Click()
     End Sub
     End Sub
     Private Sub BtnStart_Click() Handles BtnStart.Click
     Private Sub BtnStart_Click() Handles BtnStart.Click
-        '确认版本隔离
-        If (SelectedForge IsNot Nothing OrElse SelectedNeoForge IsNot Nothing OrElse SelectedFabric IsNot Nothing) AndAlso
-           (Settings.Get(Of Integer)("LaunchArgumentIndieV2") = 0 OrElse Settings.Get(Of Integer)("LaunchArgumentIndieV2") = 2) Then
-            If MyMsgBox("你尚未开启版本隔离,多个 MC 版本会共用同一个 Mod 文件夹。" & vbCrLf &
-                        "因此,游戏可能会因为读取到与当前版本不符的 Mod 而崩溃。" & vbCrLf &
-                        "推荐先在 设置 → 启动选项 → 默认版本隔离 中开启版本隔离!", "版本隔离提示", "取消下载", "继续") = 1 Then
-                Return
-            End If
-        End If
         '提交安装申请
         '提交安装申请
         Dim InstanceName As String = TextSelectName.Text
         Dim InstanceName As String = TextSelectName.Text
         Dim Request As New McInstallRequest With {
         Dim Request As New McInstallRequest With {
             .NewInstanceName = InstanceName,
             .NewInstanceName = InstanceName,
-            .VersionFolder = $"{McFolderSelected}versions\{InstanceName}\",
+            .InstanceFolder = $"{McFolderSelected}versions\{InstanceName}\",
+            .Components = GetSelectedComponents(),
             .MinecraftJson = VanillaData("url").ToString,
             .MinecraftJson = VanillaData("url").ToString,
-            .MinecraftName = VanillaName,
-            .OptiFineEntry = SelectedOptiFine,
             .ForgeEntry = SelectedForge,
             .ForgeEntry = SelectedForge,
             .NeoForgeEntry = SelectedNeoForge,
             .NeoForgeEntry = SelectedNeoForge,
-            .FabricVersion = SelectedFabric,
             .FabricApi = SelectedFabricApi,
             .FabricApi = SelectedFabricApi,
             .OptiFabric = SelectedOptiFabric,
             .OptiFabric = SelectedOptiFabric,
             .LiteLoaderEntry = SelectedLiteLoader
             .LiteLoaderEntry = SelectedLiteLoader
         }
         }
-        If Not McInstall(Request) Then Return
+        StartInstall(Request)
+    End Sub
+
+    Public Overridable Sub StartInstall(Request As McInstallRequest)
+        '确认版本隔离
+        If (SelectedForge IsNot Nothing OrElse SelectedNeoForge IsNot Nothing OrElse SelectedFabric IsNot Nothing) AndAlso
+           (Settings.Get(Of Integer)("LaunchArgumentIndieV2") = 0 OrElse Settings.Get(Of Integer)("LaunchArgumentIndieV2") = 2) Then
+            If MyMsgBox("你尚未开启版本隔离,多个 MC 版本会共用同一个 Mod 文件夹。" & vbCrLf &
+                        "因此,游戏可能会因为读取到与当前版本不符的 Mod 而崩溃。" & vbCrLf &
+                        "推荐先在 设置 → 启动选项 → 默认版本隔离 中开启版本隔离!", "版本隔离提示", "取消下载", "继续") = 1 Then
+                Return
+            End If
+        End If
+        If Not McInstallStart(Request) Then Return
         '返回,这样在再次进入安装页面时这个版本就会显示文件夹已重复
         '返回,这样在再次进入安装页面时这个版本就会显示文件夹已重复
         ExitSelectPage()
         ExitSelectPage()
     End Sub
     End Sub

+ 727 - 0
Plain Craft Launcher 2/Pages/PageDownload/PageDownloadModify.vb

@@ -0,0 +1,727 @@
+''' <summary>
+''' 版本修改页:完整还原已安装组件后,复用安装页的选择交互。
+''' </summary>
+Public Class PageDownloadModify
+    Inherits PageDownloadInstall
+
+    Private InitLoader As LoaderTask(Of McInstance, Object)
+    Private ForgeVersionLoader As LoaderTask(Of String, List(Of DlForgeVersionEntry))
+    Private InitialTarget As McComponents '进入时的组件快照
+    ''' <summary>
+    ''' 原版的版本号是否可靠(不是通过 json 模糊分析)。
+    ''' </summary>
+    Private IsVanillaVersionReliable As Boolean
+    Private OriginalName As String
+    Private CustomLogo As String
+    ''' <summary>
+    ''' 开始修改 MC 版本时,记录当前选择的组件,以在修改 MC 版本后尝试自动选中相同组件。
+    ''' </summary>
+    Private ReadOnly PendingComponents As New Dictionary(Of MyLoading, String)
+    Private IsRestoringComponents As Boolean
+
+    Public Overrides Sub LoaderInit() '父级 Handles Me.Initialized
+        ControlInit()
+        InitLoader = New LoaderTask(Of McInstance, Object)("初始化版本修改", AddressOf LoadInstance)
+        PageLoaderInit(LoadMinecraft, PanLoad, PanAllBack, Nothing, InitLoader, AddressOf LoadInstanceFinished, Function() PageInstanceLeft.Instance)
+    End Sub
+    Public Overrides Sub Init() '父级 Handles Me.Loaded
+        PanBack.ScrollToHome()
+        Static IsLoad As Boolean = False
+        If IsLoad Then PageLoaderRestart()
+        IsLoad = True
+    End Sub
+    Private Sub PageUnloaded() Handles Me.Unloaded
+        InitLoader.Cancel()
+    End Sub
+
+    ''' <summary>
+    ''' 读取或初始化版本的安装清单。
+    ''' </summary>
+    Private Sub LoadInstance(Task As LoaderTask(Of McInstance, Object))
+
+        '旧状态清理
+        RunInUiWait(
+        Sub()
+            AniStop("FrmDownloadInstall SelectPageSwitch")
+            IsSelectNameEdited = False
+            ClearSelected()
+            PendingComponents.Clear()
+            InitialTarget = Nothing
+            IsInSelectPage = False
+            PanAllBack.IsHitTestVisible = False
+        End Sub)
+        Task.Input.Config.DiscardCache()
+
+        '获取 Components
+        Dim Manifest = Configs.McInstallManifest.Get(Task.Input.Config)
+        If Manifest IsNot Nothing AndAlso Manifest.SchemaVersion > Versions.McInstallManifestVersion Then
+            Logger.Warn($"安装清单由更新版本的 PCL 创建(清单版本:{Manifest.SchemaVersion},当前版本:{Versions.McInstallManifestVersion})")
+            Throw New Exception($"更新 PCL 后才能修改它!点击【设置 → 其他 → 检查更新】以更新。")
+        End If
+        If Manifest IsNot Nothing Then
+            Dim LatestManifest = Manifest.RefreshModComponents(Task.Input.PathIndie)
+            If LatestManifest IsNot Manifest Then
+                Manifest = LatestManifest
+                Configs.McInstallManifest.Set(Manifest, Task.Input.Config)
+                Task.Input.Config.Save()
+            End If
+        End If
+        Dim Components As McComponents
+        Dim OptiFineVersion As String = Nothing
+        If Manifest IsNot Nothing Then
+            '有清单:从清单读取
+            Components = Manifest.Components
+            If Components Is Nothing Then Throw New Exception("安装清单缺少组件信息,无法还原已安装组件。")
+            IsVanillaVersionReliable = True
+        Else
+            '无清单:通过版本 JSON 获取
+            Dim VersionInfo = New McInstance(Task.Input.PathVersion).Version
+            If VersionInfo.VanillaName = "Unknown" Then Throw New Exception("无法确定 Minecraft 版本,无法修改")
+            Dim UnknownLoaders = VersionInfo.ModLoaderVersions.
+                Where(Function(Loader) String.IsNullOrWhiteSpace(Loader.Value) OrElse Loader.Value = "未知版本").
+                Select(Function(Loader) Loader.Key.ToString()).ToList()
+            If UnknownLoaders.Any() Then Throw New Exception($"无法确定 {UnknownLoaders.Join("、")} 的版本,无法修改")
+            Components = New McComponents(VersionInfo.VanillaName, VersionInfo.ModLoaderVersions)
+            OptiFineVersion = VersionInfo.OptiFine
+            IsVanillaVersionReliable = VersionInfo.Reliable
+        End If
+        Dim MinecraftVersion = Components.MinecraftVersion
+        If String.IsNullOrWhiteSpace(MinecraftVersion) Then Throw New Exception("无法确定当前 Minecraft 版本,请检查版本文件")
+        '等待子列表加载器完成获取
+        Dim LoaderList As New Dictionary(Of String, LoaderBase) From {
+            {"Minecraft", DlClientListLoader}, {"OptiFine", DlOptiFineListLoader}, {"LiteLoader", DlLiteLoaderListLoader},
+            {"NeoForge", DlNeoForgeListLoader}, {"Fabric", DlFabricListLoader}, {"Fabric API", DlFabricApiLoader}, {"OptiFabric", DlOptiFabricLoader}}
+        Dim ForgeLoader As LoaderTask(Of String, List(Of DlForgeVersionEntry)) = Nothing
+        If McVersion.IsStandardFormat(MinecraftVersion) Then
+            ForgeLoader = New LoaderTask(Of String, List(Of DlForgeVersionEntry))("DlForgeVersion " & MinecraftVersion, AddressOf DlForgeVersionMain)
+            LoaderList.Add("Forge", ForgeLoader)
+        End If
+        For Each Loader In LoaderList.Values
+            Loader.Start(If(Loader Is ForgeLoader, MinecraftVersion, Nothing), Task.IsForceRestarting)
+        Next
+        Do
+            If Task.IsCanceled Then Return
+            For Each Pair In LoaderList
+                Dim Loader = Pair.Value
+                If Loader.State = LoadState.Failed Then
+                    If Loader Is ForgeLoader AndAlso Loader.Error.Message = "无" Then Continue For 'Forge 无版本时不报错
+                    Throw New Exception($"获取 {Pair.Key} 版本列表失败,请稍后再试,或使用 VPN 改善网络环境", Loader.Error)
+                ElseIf Loader.State = LoadState.Canceled Then
+                    Throw New Exception($"获取 {Pair.Key} 版本列表被中断,请稍后再试,或使用 VPN 改善网络环境")
+                End If
+            Next
+            If LoaderList.Values.All(Function(Loader) Loader.State = LoadState.Finished OrElse Loader.State = LoadState.Failed) Then Exit Do
+            Task.Progress = 0.7 * LoaderList.Values.Average(Function(Loader) Loader.Progress)
+            Thread.Sleep(10)
+        Loop
+        If Task.IsCanceled Then Return
+        Task.Progress = 0.7
+        'Minecraft
+        MinecraftVersions = CType(DlClientListLoader.Output.Value("versions").DeepClone(), JArray)
+        VanillaData = MinecraftVersions.OfType(Of JObject).FirstOrDefault(
+            Function(v) v("id").ToString.Replace("∞", "infinite") = MinecraftVersion.Replace("∞", "infinite"))
+        If VanillaData Is Nothing Then Throw New Exception($"Minecraft 列表中没有已安装的 {MinecraftVersion}")
+        VanillaData("id") = MinecraftVersion
+        'OptiFine
+        If Manifest IsNot Nothing Then
+            SelectedOptiFine = Components.OptiFineEntry?.ToObject(Of DlOptiFineListEntry)()
+            If SelectedOptiFine IsNot Nothing AndAlso Not DlOptiFineListLoader.Output.Value.Any(Function(v) v.FileName = SelectedOptiFine.FileName) Then
+                '如果 OptiFine 是 pre 版,可能只在 BMCLAPI 下有,官方源删了。如果是这种缺失数据的情况,换用 BMCLAPI 源作为输出
+                If DlOptiFineListBmclapiLoader.State <> LoadState.Finished Then DlOptiFineListBmclapiLoader.WaitForExit()
+                DlOptiFineListLoader.Output = DlOptiFineListBmclapiLoader.Output
+            End If
+        ElseIf OptiFineVersion IsNot Nothing AndAlso OptiFineVersion <> "未知版本" Then
+            SelectedOptiFine = DlOptiFineListLoader.Output.Value.FirstOrDefault(
+                Function(v) v.Inherit = MinecraftVersion AndAlso
+                    v.InstanceName.AfterLast("-OptiFine_").Replace("HD_U_", "") = OptiFineVersion.Replace("HD_U_", ""))
+            If SelectedOptiFine Is Nothing Then Throw New Exception($"OptiFine 列表中没有已安装的 {OptiFineVersion}")
+        End If
+        'Forge
+        If Components.ModLoaderVersions.ContainsKey(ModLoaders.Forge) Then
+            SelectedForge = ForgeLoader?.Output?.FirstOrDefault(
+                Function(v) v.Inherit = MinecraftVersion AndAlso v.VersionName = Components.ModLoaderVersions(ModLoaders.Forge) AndAlso
+                            v.Category <> "universal" AndAlso v.Category <> "client" AndAlso (SelectedOptiFine Is Nothing OrElse IsOptiFineSuitForForge(SelectedOptiFine, v)))
+            If SelectedForge Is Nothing Then Throw New Exception($"Forge 列表中没有已安装的 {Components.ModLoaderVersions(ModLoaders.Forge)}")
+        End If
+        'NeoForge
+        If Components.ModLoaderVersions.ContainsKey(ModLoaders.NeoForge) Then
+            SelectedNeoForge = DlNeoForgeListLoader.Output.Value.FirstOrDefault(
+                Function(v) v.Inherit = MinecraftVersion AndAlso v.VersionName = Components.ModLoaderVersions(ModLoaders.NeoForge))
+            If SelectedNeoForge Is Nothing Then Throw New Exception($"NeoForge 列表中没有已安装的 {Components.ModLoaderVersions(ModLoaders.NeoForge)}")
+        End If
+        'Fabric
+        If Components.ModLoaderVersions.ContainsKey(ModLoaders.Fabric) Then
+            Dim FabricEntry = DlFabricListLoader.Output.Value("loader").OfType(Of JObject).FirstOrDefault(
+                Function(v) v("version").ToString = Components.ModLoaderVersions(ModLoaders.Fabric),
+                Function(v) v("version").ToString.Replace("+build", "") = Components.ModLoaderVersions(ModLoaders.Fabric))
+            If FabricEntry Is Nothing OrElse Not DlFabricListLoader.Output.Value("game").OfType(Of JObject).
+                    Any(Function(v) v("version").ToString = MinecraftVersion.Replace("∞", "infinite").Replace("Combat Test 7c", "1.16_combat-3")) Then
+                Throw New Exception($"Fabric 列表中没有已安装的 {Components.ModLoaderVersions(ModLoaders.Fabric)}")
+            End If
+            SelectedFabric = FabricEntry("version").ToString
+        End If
+        'LiteLoader
+        If Components.ModLoaderVersions.ContainsKey(ModLoaders.LiteLoader) Then
+            SelectedLiteLoader = DlLiteLoaderListLoader.Output.Value.FirstOrDefault(
+                Function(v) v.Inherit = MinecraftVersion AndAlso v.JsonToken("version").ToString = Components.ModLoaderVersions(ModLoaders.LiteLoader))
+            If SelectedLiteLoader Is Nothing Then Throw New Exception($"LiteLoader 列表中没有已安装的 {Components.ModLoaderVersions(ModLoaders.LiteLoader)}")
+        End If
+        'Fabric API
+        If Components.FabricApiVersion IsNot Nothing Then
+            SelectedFabricApi = DlFabricApiLoader.Output.FirstOrDefault(
+                Function(v) (v.Version = Components.FabricApiVersion OrElse v.Id = Components.FabricApiVersion OrElse
+                             v.Display = Components.FabricApiVersion) AndAlso IsFabricApiCompatible(v, MinecraftVersion))
+            If SelectedFabricApi Is Nothing Then Throw New Exception($"Fabric API 列表中没有已安装的 {Components.FabricApiVersion}")
+        End If
+        'OptiFabric
+        If Components.OptiFabricVersion IsNot Nothing Then
+            SelectedOptiFabric = DlOptiFabricLoader.Output.FirstOrDefault(
+                Function(v) (v.Version = Components.OptiFabricVersion OrElse v.Id = Components.OptiFabricVersion OrElse
+                             v.Display = Components.OptiFabricVersion) AndAlso IsOptiFabricCompatible(v, MinecraftVersion))
+            If SelectedOptiFabric Is Nothing Then Throw New Exception($"OptiFabric 列表中没有已安装的 {Components.OptiFabricVersion}")
+        End If
+
+        '扫描尚未识别的 Mod 组件
+        Dim ManagedFiles As New List(Of McInstallManifest.ManagedFile)
+        If Manifest IsNot Nothing Then ManagedFiles.AddRange(Manifest.ManagedFiles)
+
+#Region "从 mods 文件夹中获取以 Mod 形式安装的组件信息"
+
+        '获取 mods 文件夹下的 jar 文件与 SHA1
+        Dim Mods As New Lazy(Of List(Of (File As FileInfo, Sha1 As String)))(
+            Function()
+                Dim ModsFolder = Task.Input.PathIndie & "mods\"
+                Dim Files = DirectoryUtils.EnumerateFiles(ModsFolder, searchPattern:="*.jar").ToList()
+                If SelectedLiteLoader IsNot Nothing Then 'LiteLoader 的 OptiFine 安装目录
+                    Files.AddRange(DirectoryUtils.EnumerateFiles(Path.Combine(ModsFolder, MinecraftVersion), searchPattern:="*.jar"))
+                End If
+                Dim Entries As New List(Of (File As FileInfo, Sha1 As String))
+                For Each FilePath In Files
+                    If Task.IsCanceled Then Return Entries
+                    Dim File = FileUtils.GetInfo(FilePath)
+                    Entries.Add((File, New LocalResourceFile(File).ModrinthHash))
+                    Task.Progress = 0.7 + 0.28 * Entries.Count / Files.Count
+                Next
+                Return Entries
+            End Function)
+        'Fabric API
+        If SelectedFabric IsNot Nothing AndAlso SelectedFabricApi Is Nothing Then
+            Dim FabricApiList = DlFabricApiLoader.Output.Where(Function(v) IsFabricApiCompatible(v, MinecraftVersion)).ToList()
+            Dim Matches As New List(Of (File As FileInfo, Sha1 As String, Version As ResourceVersion))
+            For Each Entry In Mods.Value
+                Dim Match = FabricApiList.FirstOrDefault(Function(v) v.Hash = Entry.Sha1)
+                If Match Is Nothing Then Continue For
+                Matches.Add((Entry.File, Entry.Sha1, Match))
+            Next
+            Dim SelectedFile = SelectMod("Fabric API", Matches)
+            If SelectedFile IsNot Nothing Then
+                SelectedFabricApi = SelectedFile.Value.Version
+                ManagedFiles.Add(New McInstallManifest.ManagedFile("mods/" & SelectedFile.Value.File.Name.Lower(), SelectedFile.Value.Sha1, SelectedFile.Value.File.LastWriteTimeUtc, "FabricApi"))
+            End If
+        End If
+        If Task.IsCanceled Then Return
+        'OptiFabric
+        If SelectedFabric IsNot Nothing AndAlso SelectedOptiFabric Is Nothing Then
+            Dim OptiFabricList = DlOptiFabricLoader.Output.Where(Function(v) IsOptiFabricCompatible(v, MinecraftVersion)).ToList()
+            Dim HasMd5 = OptiFabricList.Any(Function(v) If(v.Hash, "").Length = 32) 'CurseForge 的 OptiFabric 信息可能提供 SHA1 或 MD5
+            Dim Matches As New List(Of (File As FileInfo, Sha1 As String, Version As ResourceVersion))
+            For Each Entry In Mods.Value
+                Dim Match = OptiFabricList.FirstOrDefault(Function(v) v.Hash = Entry.Sha1)
+                If Match Is Nothing AndAlso HasMd5 Then
+                    Dim Md5 = CryptographyUtils.ComputeFileHash(Entry.File.FullName, CryptographyUtils.HashMethod.Md5)
+                    Match = OptiFabricList.FirstOrDefault(Function(v) v.Hash = Md5)
+                End If
+                If Match Is Nothing Then Continue For
+                Matches.Add((Entry.File, Entry.Sha1, Match))
+            Next
+            Dim SelectedFile = SelectMod("OptiFabric", Matches)
+            If SelectedFile IsNot Nothing Then
+                SelectedOptiFabric = SelectedFile.Value.Version
+                ManagedFiles.Add(New McInstallManifest.ManagedFile("mods/" & SelectedFile.Value.File.Name.Lower(), SelectedFile.Value.Sha1, SelectedFile.Value.File.LastWriteTimeUtc, "OptiFabric"))
+            End If
+        End If
+        If Task.IsCanceled Then Return
+        'OptiFine
+        Dim OptiFineFiles As New List(Of (File As FileInfo, Sha1 As String, OriginalName As String))
+        If SelectedOptiFine Is Nothing AndAlso (SelectedForge IsNot Nothing OrElse SelectedLiteLoader IsNot Nothing OrElse SelectedFabric IsNot Nothing) Then 'OptiFine 只在有 Mod 加载器时才看在不在 mods 文件夹中
+            Static OptiFineHashes As JObject = FileUtils.ReadAsJson("Minecraft\OptiFineHashes.json", type:=GetType(Resource)) 'JSON 内容:{"SHA1": "原始文件名", ...}
+            OptiFineFiles = Mods.Value.
+                    Select(Function(Entry) (Entry.File, Entry.Sha1, OriginalName:=OptiFineHashes(Entry.Sha1)?.ToString())). '用本地文件的 SHA1 匹配下载时的原始文件名,再根据原始文件名取得下载源信息
+                    Where(Function(Entry) Entry.OriginalName IsNot Nothing).ToList()
+            Dim Matches As New List(Of (File As FileInfo, Sha1 As String, Version As DlOptiFineListEntry))
+            For Each Entry In OptiFineFiles '遍历 hash 匹配到了的本地文件
+                Dim Match = DlOptiFineListLoader.Output.Value.FirstOrDefault(
+                        Function(v) v.Inherit = MinecraftVersion AndAlso v.FileName = Entry.OriginalName AndAlso (SelectedForge Is Nothing OrElse IsOptiFineSuitForForge(v, SelectedForge)))
+                If Match Is Nothing Then
+                    Hint($"无法从 OptiFine 列表还原 {Entry.File.Name} 对应的 {Entry.OriginalName},将跳过该文件。")
+                    Continue For
+                End If
+                Matches.Add((Entry.File, Entry.Sha1, Match))
+            Next
+            Dim SelectedFile = SelectMod("OptiFine", Matches)
+            If SelectedFile IsNot Nothing Then
+                SelectedOptiFine = SelectedFile.Value.Version
+                Dim RelativePath = PathUtils.RemoveExtendedPrefix(SelectedFile.Value.File.FullName).Substring(Task.Input.PathIndie.Length).Replace("\", "/").Lower()
+                ManagedFiles.Add(New McInstallManifest.ManagedFile(RelativePath, SelectedFile.Value.Sha1, SelectedFile.Value.File.LastWriteTimeUtc, "OptiFine"))
+            End If
+        End If
+        If Task.IsCanceled Then Return
+
+#End Region
+
+        '保存 Manifest
+        If IsVanillaVersionReliable Then
+            Components = New McComponents(MinecraftVersion, Components.ModLoaderVersions,
+                    If(SelectedOptiFine Is Nothing, Nothing, JObject.FromObject(SelectedOptiFine)),
+                    If(SelectedFabricApi?.Version, If(SelectedFabricApi?.Id, SelectedFabricApi?.Display)),
+                    If(SelectedOptiFabric?.Version, If(SelectedOptiFabric?.Id, SelectedOptiFabric?.Display)))
+            If Manifest Is Nothing OrElse Not Components.IsEquivalentTo(Manifest.Components) OrElse Not ManagedFiles.SequenceEqual(Manifest.ManagedFiles) Then
+                Configs.McInstallManifest.Set(New McInstallManifest(Versions.McInstallManifestVersion, Components, Date.UtcNow, ManagedFiles), Task.Input.Config)
+                Task.Input.Config.Save()
+                Logger.Info($"已为 {Task.Input.Name} 更新安装清单")
+            End If
+        End If
+        If Task.IsCanceled Then Return
+        ForgeVersionLoader = ForgeLoader
+    End Sub
+    Private Function SelectMod(Of T)(ComponentName As String, Matches As List(Of (File As FileInfo, Sha1 As String, Version As T))) As (File As FileInfo, Sha1 As String, Version As T)?
+        If Matches.Count = 0 Then Return Nothing
+        If Matches.Count = 1 Then Return Matches(0)
+        Dim Choice As Integer? = Nothing
+        RunInUiWait(Sub() Choice = MyMsgBoxSelect(Matches.Select(Function(Entry) CType(New MyRadioBox With {.Text = Entry.File.Name}, IMyRadio)), $"选择当前使用的 {ComponentName}"))
+        If Not Choice.HasValue Then Throw New Exception($"发现了多个 {ComponentName}")
+        Return Matches(Choice.Value)
+    End Function
+
+    ''' <summary>
+    ''' 初始化修改页
+    ''' </summary>
+    Private Sub LoadInstanceFinished(Task As LoaderTask(Of McInstance, Object))
+        '还原原版信息
+        OriginalName = Task.Input.Name
+        CustomLogo = Nothing
+        If ReadIni(Task.Input.PathVersion & "PCL\Setup.ini", "LogoCustom", False) Then
+            CustomLogo = If(Task.Input.Logo.EndsWithF("PCL\Logo.png"), Task.Input.PathVersion & "PCL\Logo.png", Task.Input.Logo)
+        End If
+        Dim Item = McDownloadListItem(VanillaData, Nothing, False).Init()
+        VanillaName = Item.Title
+        VanillaIcon = Item.Logo
+        LabMinecraftVersion.Text = VanillaName
+        TextSelectName.Text = OriginalName
+        IsSelectNameEdited = Not String.Equals(OriginalName, GetSelectName(), StringComparison.Ordinal)
+        TextSelectName.ValidateRules = New ObjectModel.Collection(Of Validate) From {
+            New ValidateFolderName(McFolderSelected & "versions", IgnoreList:=New List(Of String) From {OriginalName})}
+        '修改页 UI
+        CardMinecraft.Visibility = Visibility.Visible
+        BtnBack.Visibility = Visibility.Collapsed
+        ImgLogo.Margin = New Thickness(15, 15, 0, 0)
+        TextSelectName.Margin = New Thickness(56, 16, 16, 0)
+        LabSelectName.Text = $"原名 :   {OriginalName}"
+        UpdateOriginalNameVisibility()
+        BtnStart.Text = "开始修改"
+        BtnStart.Logo = FrmInstanceLeft.ItemModify.Logo
+        AutoSelectedFabricApi = SelectedFabric IsNot Nothing
+        AutoSelectedOptiFabric = SelectedFabric IsNot Nothing AndAlso SelectedOptiFine IsNot Nothing
+        If ForgeVersionLoader IsNot Nothing Then LoadForge.State = ForgeVersionLoader
+        PrepareSelectPage()
+        ReloadComponentLists()
+        '进入动画
+        PanMinecraft.Visibility = Visibility.Collapsed
+        PanMinecraft.Opacity = 0
+        PanMinecraft.RenderTransform = New TranslateTransform(-50, 0)
+        PanSelect.Opacity = 1
+        PanSelect.RenderTransform = New TranslateTransform
+        PanBack.IsHitTestVisible = True
+        PanAllBack.IsHitTestVisible = True
+        InitialTarget = GetSelectedComponents()
+        '设置原始数据
+        BtnMinecraftRestore.Tag = VanillaData
+        BtnOptiFineRestore.Tag = SelectedOptiFine
+        BtnForgeRestore.Tag = SelectedForge
+        BtnNeoForgeRestore.Tag = SelectedNeoForge
+        BtnFabricRestore.Tag = SelectedFabric
+        BtnFabricApiRestore.Tag = SelectedFabricApi
+        BtnOptiFabricRestore.Tag = SelectedOptiFabric
+        BtnLiteLoaderRestore.Tag = SelectedLiteLoader
+        ImgMinecraftOld.Source = VanillaIcon
+        LabMinecraftOld.Text = LabMinecraftVersion.Text
+        LabOptiFineOld.Text = If(SelectedOptiFine Is Nothing, "无", LabOptiFine.Text)
+        LabForgeOld.Text = If(SelectedForge Is Nothing, "无", LabForge.Text)
+        LabNeoForgeOld.Text = If(SelectedNeoForge Is Nothing, "无", LabNeoForge.Text)
+        LabFabricOld.Text = If(SelectedFabric Is Nothing, "无", LabFabric.Text)
+        LabLiteLoaderOld.Text = If(SelectedLiteLoader Is Nothing, "无", LabLiteLoader.Text)
+        LabFabricApiOld.Text = If(SelectedFabricApi Is Nothing, "无", LabFabricApi.Text)
+        LabOptiFabricOld.Text = If(SelectedOptiFabric Is Nothing, "无", LabOptiFabric.Text)
+        RefreshComparisons()
+    End Sub
+
+    Public Overrides Function GetSelectLogo() As String
+        Return If(CustomLogo, MyBase.GetSelectLogo())
+    End Function
+
+    Public Overrides Sub ShowMinecraftVersionList()
+        If IsInSelectPage Then
+            '把当前已选择的组件记录到 PendingComponents
+            Dim Components = GetSelectedComponents()
+            For Each Component In {
+                (Loader:=LoadOptiFine, Version:=Components.OptiFineVersion),
+                (Loader:=LoadFabricApi, Version:=Components.FabricApiVersion),
+                (Loader:=LoadOptiFabric, Version:=Components.OptiFabricVersion),
+                (Loader:=LoadForge, Version:=SelectedForge?.VersionName),
+                (Loader:=LoadNeoForge, Version:=SelectedNeoForge?.VersionName),
+                (Loader:=LoadFabric, Version:=SelectedFabric),
+                (Loader:=LoadLiteLoader, Version:=SelectedLiteLoader?.JsonToken?("version").ToString())}
+                '保留尚未加载完成的组件选择
+                If Component.Version IsNot Nothing Then PendingComponents(Component.Loader) = Component.Version
+            Next
+        End If
+        MyBase.ShowMinecraftVersionList()
+    End Sub
+
+    Public Overrides Sub ReloadSelected()
+        If IsRestoringComponents Then Return
+        If IsInSelectPage AndAlso VanillaName IsNot Nothing AndAlso PendingComponents.Any() Then
+            IsRestoringComponents = True
+            Try
+                Dim HasProcessed As Boolean = False
+                '优先恢复 Mod 加载器,再匹配兼容的附加组件
+                For Each LoadingComponent In {LoadFabric, LoadForge, LoadNeoForge, LoadLiteLoader, LoadOptiFine, LoadFabricApi, LoadOptiFabric}
+                    Dim Version As String = Nothing
+                    If Not PendingComponents.TryGetValue(LoadingComponent, Version) Then Continue For
+                    If Not LoadingComponent.State.IsLoader Then Continue For
+                    Dim Loader = DirectCast(LoadingComponent.State, LoaderBase)
+                    '检查前置组件
+                    If LoadingComponent Is LoadOptiFine Then
+                        If {LoadFabric, LoadForge, LoadNeoForge, LoadLiteLoader}.Any(Function(Component) PendingComponents.ContainsKey(Component)) Then Continue For
+                    ElseIf LoadingComponent Is LoadFabricApi Then
+                        If PendingComponents.ContainsKey(LoadFabric) Then Continue For
+                    ElseIf LoadingComponent Is LoadForge Then
+                        If Not McVersion.IsStandardFormat(VanillaName) Then
+                            PendingComponents.Remove(LoadingComponent)
+                            Continue For
+                        End If
+                        If DirectCast(Loader, LoaderTask(Of String, List(Of DlForgeVersionEntry))).Input <> VanillaName Then Continue For
+                        If Loader.State = LoadState.Failed AndAlso Loader.Error.Message = "无" Then
+                            PendingComponents.Remove(LoadingComponent)
+                            Continue For
+                        End If
+                    ElseIf LoadingComponent Is LoadOptiFabric Then
+                        If PendingComponents.ContainsKey(LoadFabric) OrElse PendingComponents.ContainsKey(LoadOptiFine) Then Continue For
+                    End If
+                    If Loader.State <> LoadState.Finished Then Continue For
+                    '移除已处理的组件
+                    PendingComponents.Remove(LoadingComponent)
+                    HasProcessed = True
+                    '匹配目标 MC 版本下兼容的组件
+                    If LoadingComponent Is LoadFabric Then
+                        If SelectedFabric Is Nothing AndAlso LoadFabricGetError() Is Nothing Then
+                            Dim Versions = DlFabricListLoader.Output.Value("loader").OfType(Of JObject)
+                            SelectedFabric = If(Versions.FirstOrDefault(Function(v) v("version").ToString = Version), Versions.FirstOrDefault())?("version").ToString()
+                        End If
+                    ElseIf LoadingComponent Is LoadOptiFine Then
+                        If SelectedOptiFine Is Nothing AndAlso LoadOptiFineGetError() Is Nothing Then
+                            Dim Versions = DlOptiFineListLoader.Output.Value.Where(Function(v) v.Inherit = VanillaName AndAlso
+                                    (SelectedForge Is Nothing OrElse IsOptiFineSuitForForge(v, SelectedForge)))
+                            SelectedOptiFine = If(Versions.FirstOrDefault(Function(v) v.InstanceName.AfterLast("-OptiFine_") = Version),
+                                Versions.OrderBy(Function(v) v.IsPreview).
+                                    ThenByDescending(Function(v) v.DisplayName, Comparer(Of String).Create(AddressOf McUtils.CompareVersion)).FirstOrDefault())
+                        End If
+                    ElseIf LoadingComponent Is LoadForge Then
+                        If SelectedForge Is Nothing AndAlso LoadForgeGetError() Is Nothing Then
+                            Dim Versions = DirectCast(Loader, LoaderTask(Of String, List(Of DlForgeVersionEntry))).Output.Where(
+                                Function(v) v.Inherit = VanillaName AndAlso
+                                    v.Category <> "universal" AndAlso v.Category <> "client" AndAlso
+                                    (SelectedOptiFine Is Nothing OrElse IsOptiFineSuitForForge(SelectedOptiFine, v)))
+                            SelectedForge = If(Versions.FirstOrDefault(Function(v) v.VersionName = Version), Versions.OrderByDescending(Function(v) v).FirstOrDefault())
+                        End If
+                    ElseIf LoadingComponent Is LoadNeoForge Then
+                        If SelectedNeoForge Is Nothing AndAlso LoadNeoForgeGetError() Is Nothing Then
+                            Dim Versions = DlNeoForgeListLoader.Output.Value.Where(Function(v) v.Inherit = VanillaName)
+                            SelectedNeoForge = If(Versions.FirstOrDefault(Function(v) v.VersionName = Version), Versions.FirstOrDefault())
+                        End If
+                    ElseIf LoadingComponent Is LoadLiteLoader Then
+                        If SelectedLiteLoader Is Nothing AndAlso LoadLiteLoaderGetError() Is Nothing Then
+                            Dim Versions = DlLiteLoaderListLoader.Output.Value.Where(Function(v) v.Inherit = VanillaName)
+                            SelectedLiteLoader = If(Versions.FirstOrDefault(Function(v) v.JsonToken("version").ToString = Version), Versions.FirstOrDefault())
+                        End If
+                    ElseIf LoadingComponent Is LoadFabricApi Then
+                        If SelectedFabricApi Is Nothing AndAlso LoadFabricApiGetError() Is Nothing Then
+                            Dim Versions = DlFabricApiLoader.Output.Where(Function(v) IsFabricApiCompatible(v, VanillaName))
+                            SelectedFabricApi = If(Versions.FirstOrDefault(Function(v) v.Version = Version OrElse v.Id = Version OrElse v.Display = Version),
+                                Versions.OrderByDescending(Function(v) v.ReleaseDate).FirstOrDefault())
+                        End If
+                        If SelectedFabricApi IsNot Nothing Then AutoSelectedFabricApi = True
+                    ElseIf LoadingComponent Is LoadOptiFabric Then
+                        If SelectedOptiFabric Is Nothing AndAlso LoadOptiFabricGetError() Is Nothing Then
+                            Dim Versions = DlOptiFabricLoader.Output.Where(Function(v) IsOptiFabricCompatible(v, VanillaName))
+                            SelectedOptiFabric = If(Versions.FirstOrDefault(Function(v) v.Version = Version OrElse v.Id = Version OrElse v.Display = Version),
+                                Versions.OrderByDescending(Function(v) v.ReleaseDate).FirstOrDefault())
+                        End If
+                        If SelectedOptiFabric IsNot Nothing Then AutoSelectedOptiFabric = True
+                    End If
+                Next
+                '更新组件列表
+                If HasProcessed Then ReloadComponentLists()
+            Finally
+                IsRestoringComponents = False
+            End Try
+        End If
+        '刷新当前选择与原始版本的对比
+        MyBase.ReloadSelected()
+        If InitialTarget IsNot Nothing Then RefreshComparisons()
+    End Sub
+
+    Private Sub RefreshComparisons()
+        Dim Current = GetSelectedComponents()
+        ImgMinecraftNew.Source = VanillaIcon
+        'Minecraft
+        Dim IsMinecraftUnchanged = InitialTarget.MinecraftVersion = Current.MinecraftVersion
+        ShowComparison(PanMinecraftCompare, PanMinecraftInfo, InitialTarget.MinecraftVersion, Current.MinecraftVersion,
+                       LabMinecraftVersion.Text, BtnMinecraftRestore, True)
+        'OptiFine
+        Dim OriginalOptiFine = TryCast(BtnOptiFineRestore.Tag, DlOptiFineListEntry)
+        ShowComparison(PanOptiFineCompare, PanOptiFineInfo, InitialTarget.OptiFineVersion, Current.OptiFineVersion,
+                       LabOptiFine.Text, BtnOptiFineRestore, InitialTarget.OptiFineEntry Is Nothing OrElse
+                           IsMinecraftUnchanged AndAlso CardOptiFine.MainSwap.Visibility = Visibility.Visible AndAlso OriginalOptiFine IsNot Nothing AndAlso
+                           (SelectedForge Is Nothing OrElse IsOptiFineSuitForForge(OriginalOptiFine, SelectedForge)), BtnOptiFineClear)
+        'Fabric API
+        ShowComparison(PanFabricApiCompare, PanFabricApiInfo, InitialTarget.FabricApiVersion, Current.FabricApiVersion,
+                       LabFabricApi.Text, BtnFabricApiRestore, InitialTarget.FabricApiVersion Is Nothing OrElse
+                             IsMinecraftUnchanged AndAlso SelectedFabric IsNot Nothing AndAlso CardFabricApi.MainSwap.Visibility = Visibility.Visible, BtnFabricApiClear)
+        'OptiFabric
+        ShowComparison(PanOptiFabricCompare, PanOptiFabricInfo, InitialTarget.OptiFabricVersion, Current.OptiFabricVersion,
+                       LabOptiFabric.Text, BtnOptiFabricRestore, InitialTarget.OptiFabricVersion Is Nothing OrElse
+                             IsMinecraftUnchanged AndAlso SelectedFabric IsNot Nothing AndAlso SelectedOptiFine IsNot Nothing AndAlso CardOptiFabric.MainSwap.Visibility = Visibility.Visible, BtnOptiFabricClear)
+        'Mod Loader
+        For Each Loader In {
+            (Kind:=ModLoaders.Forge, Card:=CardForge, ComparePanel:=PanForgeCompare, CurrentPanel:=PanForgeInfo, Label:=LabForge, RestoreButton:=BtnForgeRestore, ClearButton:=BtnForgeClear),
+            (Kind:=ModLoaders.NeoForge, Card:=CardNeoForge, ComparePanel:=PanNeoForgeCompare, CurrentPanel:=PanNeoForgeInfo, Label:=LabNeoForge, RestoreButton:=BtnNeoForgeRestore, ClearButton:=BtnNeoForgeClear),
+            (Kind:=ModLoaders.Fabric, Card:=CardFabric, ComparePanel:=PanFabricCompare, CurrentPanel:=PanFabricInfo, Label:=LabFabric, RestoreButton:=BtnFabricRestore, ClearButton:=BtnFabricClear),
+            (Kind:=ModLoaders.LiteLoader, Card:=CardLiteLoader, ComparePanel:=PanLiteLoaderCompare, CurrentPanel:=PanLiteLoaderInfo, Label:=LabLiteLoader, RestoreButton:=BtnLiteLoaderRestore, ClearButton:=BtnLiteLoaderClear)}
+            Dim OldVersion As String = Nothing, NewVersion As String = Nothing
+            InitialTarget.ModLoaderVersions.TryGetValue(Loader.Kind, OldVersion)
+            Current.ModLoaderVersions.TryGetValue(Loader.Kind, NewVersion)
+            Dim CanRestore = OldVersion Is Nothing OrElse IsMinecraftUnchanged AndAlso
+                Loader.Card.MainSwap.Visibility = Visibility.Visible AndAlso Loader.RestoreButton.Tag IsNot Nothing
+            If Loader.Kind = ModLoaders.Forge AndAlso OldVersion IsNot Nothing AndAlso CanRestore Then
+                CanRestore = SelectedOptiFine Is Nothing OrElse IsOptiFineSuitForForge(SelectedOptiFine, DirectCast(Loader.RestoreButton.Tag, DlForgeVersionEntry))
+            End If
+            If Loader.Kind = ModLoaders.Fabric AndAlso OldVersion IsNot Nothing Then
+                Dim TargetVersion = VanillaName.Replace("∞", "infinite").Replace("Combat Test 7c", "1.16_combat-3")
+                CanRestore = CardFabric.Visibility = Visibility.Visible AndAlso CardFabric.MainSwap.Visibility = Visibility.Visible AndAlso
+                    DlFabricListLoader.Output.Value("loader").OfType(Of JObject).Any(Function(v) v("version").ToString = OldVersion) AndAlso
+                    DlFabricListLoader.Output.Value("game").OfType(Of JObject).Any(Function(v) v("version").ToString = TargetVersion)
+            End If
+            ShowComparison(Loader.ComparePanel, Loader.CurrentPanel, OldVersion, NewVersion, Loader.Label.Text,
+                           Loader.RestoreButton, CanRestore, Loader.ClearButton)
+        Next
+        UpdateStartButton()
+    End Sub
+
+    Private Sub ShowComparison(ComparePanel As Grid, CurrentPanel As FrameworkElement, OldVersion As String, NewVersion As String, NewText As String,
+                               RestoreButton As MyIconButton, CanRestore As Boolean, Optional ClearButton As FrameworkElement = Nothing)
+        '获取不可安装的原因
+        Dim UnavailableReason As String = Nothing
+        If NewVersion Is Nothing Then
+            If RestoreButton Is BtnOptiFineRestore Then
+                UnavailableReason = LoadOptiFineGetError()
+            ElseIf RestoreButton Is BtnForgeRestore Then
+                UnavailableReason = If(McVersion.IsStandardFormat(VanillaName), LoadForgeGetError(), "无")
+            ElseIf RestoreButton Is BtnNeoForgeRestore Then
+                UnavailableReason = If(CardNeoForge.Visibility = Visibility.Collapsed, "无", LoadNeoForgeGetError())
+            ElseIf RestoreButton Is BtnFabricRestore Then
+                UnavailableReason = If(CardFabric.Visibility = Visibility.Collapsed, "无", LoadFabricGetError())
+            ElseIf RestoreButton Is BtnLiteLoaderRestore Then
+                UnavailableReason = If(CardLiteLoader.Visibility = Visibility.Collapsed, "无", LoadLiteLoaderGetError())
+            ElseIf RestoreButton Is BtnFabricApiRestore Then
+                UnavailableReason = LoadFabricApiGetError()
+            ElseIf RestoreButton Is BtnOptiFabricRestore Then
+                UnavailableReason = LoadOptiFabricGetError()
+            End If
+            NewText = If(UnavailableReason, "可以添加")
+        End If
+        '修改对比
+        Dim IsPending = NewVersion Is Nothing AndAlso (NewText = "获取中……" OrElse NewText.StartsWithF("获取失败:"))
+        Dim IsChanged = OldVersion <> NewVersion AndAlso Not IsPending
+        CanRestore = IsChanged AndAlso OldVersion IsNot Nothing AndAlso CanRestore
+        ComparePanel.Visibility = If(IsChanged, Visibility.Visible, Visibility.Collapsed)
+        CurrentPanel.Visibility = If(IsChanged, Visibility.Collapsed, Visibility.Visible)
+        '还原与取消安装按钮
+        RestoreButton.Visibility = If(CanRestore, Visibility.Visible, Visibility.Collapsed)
+        If ClearButton IsNot Nothing Then ClearButton.ToolTip = If(OldVersion Is Nothing, "取消安装", "移除")
+        Dim HasClearButton = ClearButton IsNot Nothing AndAlso ClearButton.Visibility = Visibility.Visible
+        RestoreButton.Margin = New Thickness(0, 5, If(CanRestore AndAlso HasClearButton, 60, 32), 0)
+        ComparePanel.Margin = New Thickness(ComparePanel.Margin.Left, ComparePanel.Margin.Top,
+                                            If(CanRestore AndAlso HasClearButton, 110, 80), ComparePanel.Margin.Bottom)
+        If Not IsChanged Then Return
+        '显示修改前已安装组件的对比卡片
+        If OldVersion IsNot Nothing Then
+            Dim Card = CType(ComparePanel.Parent, MyCard)
+            If Card.Visibility = Visibility.Collapsed Then
+                Card.IsSwapped = True
+                If Card.MainSwap IsNot Nothing Then Card.MainSwap.Visibility = Visibility.Collapsed
+                Card.Visibility = Visibility.Visible
+            End If
+        End If
+        Dim OldSide = CType(ComparePanel.Children(0), DockPanel)
+        Dim NewSide = CType(ComparePanel.Children(2), DockPanel)
+        OldSide.Children(0).Visibility = If(OldVersion Is Nothing, Visibility.Collapsed, Visibility.Visible)
+        CType(OldSide.Children(1), TextBlock).Margin = If(OldVersion Is Nothing, New Thickness(0), New Thickness(7, 0, 0, 0))
+        NewSide.Children(0).Visibility = If(NewVersion Is Nothing, Visibility.Collapsed, Visibility.Visible)
+        Dim NewLabel = CType(NewSide.Children(1), TextBlock)
+        NewLabel.Margin = If(NewVersion Is Nothing, New Thickness(0), New Thickness(7, 0, 0, 0))
+        NewLabel.Text = If(NewVersion Is Nothing, "移除", NewText)
+        If NewVersion Is Nothing AndAlso UnavailableReason IsNot Nothing Then
+            NewLabel.Inlines.Add(New Run($"({If(UnavailableReason = "无", "无可用版本", UnavailableReason)})") With {.Foreground = ColorGray4})
+        End If
+    End Sub
+
+    Private Sub RestoreComponent(sender As Object, e As EventArgs) Handles BtnMinecraftRestore.Click, BtnOptiFineRestore.Click, BtnForgeRestore.Click,
+                BtnNeoForgeRestore.Click, BtnFabricRestore.Click, BtnFabricApiRestore.Click, BtnOptiFabricRestore.Click, BtnLiteLoaderRestore.Click
+        Dim Button = DirectCast(sender, MyIconButton)
+        '恢复初始选择
+        If Button Is BtnMinecraftRestore Then
+            IsRestoringComponents = True
+            Try
+                PendingComponents.Clear()
+                MinecraftSelected(McDownloadListItem(DirectCast(Button.Tag, JObject), Nothing, False).Init(), Nothing)
+                If ForgeVersionLoader IsNot Nothing Then LoadForge.State = ForgeVersionLoader
+                '恢复初始的组件
+                SelectedOptiFine = TryCast(BtnOptiFineRestore.Tag, DlOptiFineListEntry)
+                SelectedForge = TryCast(BtnForgeRestore.Tag, DlForgeVersionEntry)
+                SelectedNeoForge = TryCast(BtnNeoForgeRestore.Tag, DlNeoForgeListEntry)
+                SelectedFabric = TryCast(BtnFabricRestore.Tag, String)
+                SelectedLiteLoader = TryCast(BtnLiteLoaderRestore.Tag, DlLiteLoaderListEntry)
+                SelectedFabricApi = TryCast(BtnFabricApiRestore.Tag, ResourceVersion)
+                SelectedOptiFabric = TryCast(BtnOptiFabricRestore.Tag, ResourceVersion)
+                AutoSelectedFabricApi = SelectedFabric IsNot Nothing
+                AutoSelectedOptiFabric = SelectedFabric IsNot Nothing AndAlso SelectedOptiFine IsNot Nothing
+            Finally
+                IsRestoringComponents = False
+            End Try
+            ReloadComponentLists()
+            Return
+        ElseIf Button Is BtnOptiFineRestore Then
+            SelectedOptiFine = TryCast(Button.Tag, DlOptiFineListEntry)
+            AutoSelectedOptiFabric = SelectedOptiFine IsNot Nothing AndAlso SelectedFabric IsNot Nothing
+            If SelectedOptiFine Is Nothing Then SelectedOptiFabric = Nothing
+        ElseIf Button Is BtnForgeRestore Then
+            SelectedForge = TryCast(Button.Tag, DlForgeVersionEntry)
+        ElseIf Button Is BtnNeoForgeRestore Then
+            SelectedNeoForge = TryCast(Button.Tag, DlNeoForgeListEntry)
+        ElseIf Button Is BtnFabricRestore Then
+            SelectedFabric = TryCast(Button.Tag, String)
+            AutoSelectedFabricApi = SelectedFabric IsNot Nothing
+            AutoSelectedOptiFabric = SelectedFabric IsNot Nothing AndAlso SelectedOptiFine IsNot Nothing
+            If SelectedFabric Is Nothing Then
+                SelectedFabricApi = Nothing
+                SelectedOptiFabric = Nothing
+            End If
+        ElseIf Button Is BtnFabricApiRestore Then
+            SelectedFabricApi = TryCast(Button.Tag, ResourceVersion)
+            AutoSelectedFabricApi = True
+        ElseIf Button Is BtnOptiFabricRestore Then
+            SelectedOptiFabric = TryCast(Button.Tag, ResourceVersion)
+            AutoSelectedOptiFabric = True
+        ElseIf Button Is BtnLiteLoaderRestore Then
+            SelectedLiteLoader = TryCast(Button.Tag, DlLiteLoaderListEntry)
+        End If
+        DirectCast(Button.Parent, MyCard).IsSwapped = True
+        '刷新组件
+        ReloadComponentLists()
+    End Sub
+
+    Private Sub CardMinecraft_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles CardMinecraft.MouseLeftButtonUp
+        ShowMinecraftVersionList()
+        e.Handled = True
+    End Sub
+
+    Private Sub UpdateStartButton() Handles TextSelectName.TextChanged, TextSelectName.ValidateChanged
+        BtnStart.IsEnabled = TextSelectName.IsValidated AndAlso InitialTarget IsNot Nothing AndAlso
+            Not PendingComponents.Any() AndAlso
+            (Not IsVanillaVersionReliable OrElse Not InitialTarget.IsEquivalentTo(GetSelectedComponents()) OrElse
+             Not String.Equals(TextSelectName.Text, OriginalName, StringComparison.Ordinal))
+    End Sub
+
+    Private Sub UpdateOriginalNameVisibility() Handles TextSelectName.TextChanged
+        Dim IsNameChanged = Not String.Equals(TextSelectName.Text, OriginalName, StringComparison.Ordinal)
+        Dim NewVisibility = If(IsNameChanged, Visibility.Visible, Visibility.Collapsed)
+        If LabSelectName.Visibility = NewVisibility Then Return
+        LabSelectName.Visibility = NewVisibility
+        BtnOriginalNameRestore.Visibility = NewVisibility
+        Dim RightMargin = If(IsNameChanged, 40, 16)
+        If IsInSelectPage AndAlso AniControlEnabled = 0 Then
+            AniStart(AaDouble(Sub(Delta) TextSelectName.Margin = New Thickness(56, 16, TextSelectName.Margin.Right + Delta, 0),
+                              RightMargin - TextSelectName.Margin.Right, 150,, New AniEaseOutFluent), "DownloadModify Name Margin")
+        Else
+            AniStop("DownloadModify Name Margin")
+            TextSelectName.Margin = New Thickness(56, 16, RightMargin, 0)
+        End If
+    End Sub
+
+    Private Sub BtnOriginalNameRestore_Click(sender As Object, e As EventArgs) Handles BtnOriginalNameRestore.Click
+        TextSelectName.Text = OriginalName
+        IsSelectNameEdited = Not String.Equals(OriginalName, GetSelectName(), StringComparison.Ordinal)
+    End Sub
+
+    Public Overrides Sub StartInstall(Request As McInstallRequest)
+        UpdateStartButton()
+        If Not BtnStart.IsEnabled Then Return
+        Dim TargetInstance = InitLoader.Input
+        Dim Target = Request.Components
+        Dim InstanceName = Request.NewInstanceName
+        Dim IsNameChanged = Not String.Equals(InstanceName, TargetInstance.Name, StringComparison.Ordinal)
+        Request.NewInstanceName = If(IsNameChanged, InstanceName, Nothing)
+        Request.InstanceFolder = TargetInstance.PathVersion
+        '警告
+        Dim Warnings As New List(Of String)
+        If (SelectedForge IsNot Nothing OrElse SelectedNeoForge IsNot Nothing OrElse SelectedFabric IsNot Nothing) AndAlso
+           Not PathUtils.IsSame(TargetInstance.PathIndie, TargetInstance.PathVersion) Then
+            Warnings.Add("该版本尚未开启版本隔离,可能与别的 MC 版本共用同一个 Mod 和存档文件夹。" & vbCrLf &
+                         "修改版本后,已有的 Mod、存档等需要手动迁移到新的游戏目录。")
+        End If
+        Dim IsMinecraftVersionChanged = InitialTarget.MinecraftVersion <> Target.MinecraftVersion
+        If Not IsVanillaVersionReliable AndAlso Not IsMinecraftVersionChanged Then
+            Warnings.Add($"PCL 未能精准识别该版本,请确认它是不是 Minecraft {InitialTarget.MinecraftVersion},如果不正确,请取消并选择正确的版本!")
+        End If
+        If Not Configs.HintModifyInstanceWarning.Get() Then
+            Warnings.Add("如果该版本安装了 PCL 不兼容的加载器,或经过第三方修改,这些改动将会丢失!")
+        End If
+        Dim IsModLoaderChanged As Boolean = False
+        For Each Loader In InitialTarget.ModLoaderVersions.Keys.Union(Target.ModLoaderVersions.Keys)
+            Dim CurrentVersion As String = Nothing, TargetVersion As String = Nothing
+            Dim HasCurrent = InitialTarget.ModLoaderVersions.TryGetValue(Loader, CurrentVersion)
+            Dim HasTarget = Target.ModLoaderVersions.TryGetValue(Loader, TargetVersion)
+            If HasCurrent <> HasTarget OrElse CurrentVersion <> TargetVersion Then
+                IsModLoaderChanged = True
+                Exit For
+            End If
+        Next
+        If (IsModLoaderChanged OrElse IsMinecraftVersionChanged) AndAlso InitialTarget.ModLoaderVersions.Any Then
+            Warnings.Add("现有的 Mod、资源包等在修改后可能会不兼容!")
+        End If
+        If Warnings.Any() Then
+            If MyMsgBox("请注意:" & vbCrLf & "• " & Warnings.Join(vbCrLf & "• "), "修改确认", "开始修改", "取消", IsWarn:=True) <> 1 Then Return
+        End If
+        If Not Configs.HintModifyInstanceWarning.Get() Then Configs.HintModifyInstanceWarning.Set(True)
+        '开始修改
+        If IsVanillaVersionReliable AndAlso InitialTarget.IsEquivalentTo(Target) Then
+            '仅重命名
+            Try
+                Dim OldPath = TargetInstance.PathVersion
+                Dim RenamedInstance = TargetInstance.Rename(InstanceName)
+                If PageInstanceLeft.Instance IsNot Nothing AndAlso PathUtils.IsSame(PageInstanceLeft.Instance.PathVersion, OldPath) Then PageInstanceLeft.Instance = RenamedInstance
+                If McInstanceSelected IsNot Nothing AndAlso PathUtils.IsSame(McInstanceSelected.PathVersion, OldPath) Then McInstanceSelected = RenamedInstance
+                PageInstanceLeft.ReloadCurrentJava()
+                Hint("重命名成功!", HintType.Green)
+                LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
+                FrmMain.PageBack()
+            Catch ex As Exception
+                Logger.Error(ex, "重命名版本失败", LogBehavior.Alert)
+            End Try
+        Else
+            '实际修改
+            If McModificationStart(Request, TargetInstance) Then FrmMain.PageBack()
+        End If
+    End Sub
+End Class

+ 56 - 36
Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadCompDetail.xaml.vb

@@ -64,10 +64,10 @@ Public Class PageDownloadResourceDetail
             If IsXSpecial Then Return 1
             If IsXSpecial Then Return 1
             If IsYSpecial Then Return -1
             If IsYSpecial Then Return -1
             '比较版本号
             '比较版本号
-            Dim VersionCodeSort = -CompareVersion(x.Replace(x.BeforeFirst(" ") & " ", ""), y.Replace(y.BeforeFirst(" ") & " ", ""))
+            Dim VersionCodeSort = -McUtils.CompareVersion(x.Replace(x.BeforeFirst(" ") & " ", ""), y.Replace(y.BeforeFirst(" ") & " ", ""))
             If VersionCodeSort <> 0 Then Return VersionCodeSort
             If VersionCodeSort <> 0 Then Return VersionCodeSort
             '比较全部
             '比较全部
-            Return -CompareVersion(x, y)
+            Return -McUtils.CompareVersion(x, y)
         End Function
         End Function
         Public Sub New(Optional Topmost As String = "")
         Public Sub New(Optional Topmost As String = "")
             Me.Topmost = If(Topmost, "")
             Me.Topmost = If(Topmost, "")
@@ -98,7 +98,7 @@ Public Class PageDownloadResourceDetail
         Dim UpdateFilters =
         Dim UpdateFilters =
         Sub()
         Sub()
             Filters = Results.SelectMany(Function(v) v.GameVersions).Select(Function(v) GetGroupedVersionName(v, GroupedDrop, GroupedOld)).
             Filters = Results.SelectMany(Function(v) v.GameVersions).Select(Function(v) GetGroupedVersionName(v, GroupedDrop, GroupedOld)).
-                Distinct.OrderByDescending(Function(s) s, New VersionComparer).ToList
+                Distinct.OrderByDescending(Function(s) s, Comparer(Of String).Create(Function(x, y) McUtils.CompareVersion(x, y))).ToList
         End Sub
         End Sub
 
 
         '确定分组方式
         '确定分组方式
@@ -174,7 +174,7 @@ GroupDone:
                 Dim Loaders As New List(Of String)
                 Dim Loaders As New List(Of String)
                 If Project.ModLoaders.Flags.Count > 1 AndAlso '工程至少有两个加载器
                 If Project.ModLoaders.Flags.Count > 1 AndAlso '工程至少有两个加载器
                     Version.ResourceType = ResourceTypes.Mod AndAlso '是 Mod
                     Version.ResourceType = ResourceTypes.Mod AndAlso '是 Mod
-                    McVersion.IsFormatFit(VerName) Then '不是 “快照版本” 之类的
+                    McVersion.IsStandardFormat(VerName) Then '不是 “快照版本” 之类的
                     Loaders.AddRange(Version.ModLoaders.Flags.Select(Function(f) f.ToString & " "))
                     Loaders.AddRange(Version.ModLoaders.Flags.Select(Function(f) f.ToString & " "))
                 End If
                 End If
                 If Not Loaders.Any() Then Loaders.Add("") '保底加一个空的,确保它在一张卡片里
                 If Not Loaders.Any() Then Loaders.Add("") '保底加一个空的,确保它在一张卡片里
@@ -206,16 +206,17 @@ GroupDone:
                 If Not Pair.Value.Any() Then Continue For
                 If Not Pair.Value.Any() Then Continue For
                 If Pair.Key = TargetCardName.Replace("(所选版本)", "") Then Continue For
                 If Pair.Key = TargetCardName.Replace("(所选版本)", "") Then Continue For
                 '增加卡片
                 '增加卡片
-                Dim NewCard As New MyCard With {.Title = Pair.Key, .Margin = New Thickness(0, 0, 0, 15), .SwapType = If(TargetResourceType = ResourceTypes.ModPack, 9, 8)} '9 是安装,8 是另存为
+                Dim NewCard As New MyCard With {.Title = Pair.Key, .Margin = New Thickness(0, 0, 0, 15)}
                 Dim NewStack As New StackPanel With {.Margin = New Thickness(20, MyCard.SwapedHeight, 18, 0), .VerticalAlignment = VerticalAlignment.Top, .RenderTransform = New TranslateTransform(0, 0), .Tag = Pair.Value}
                 Dim NewStack As New StackPanel With {.Margin = New Thickness(20, MyCard.SwapedHeight, 18, 0), .VerticalAlignment = VerticalAlignment.Top, .RenderTransform = New TranslateTransform(0, 0), .Tag = Pair.Value}
                 NewCard.Children.Add(NewStack)
                 NewCard.Children.Add(NewStack)
                 NewCard.SwapControl = NewStack
                 NewCard.SwapControl = NewStack
+                NewCard.SetContentBuilder(AddressOf InitializeStackResourceVersions)
                 PanResults.Children.Add(NewCard)
                 PanResults.Children.Add(NewCard)
                 '确定卡片是否默认展开
                 '确定卡片是否默认展开
                 If Pair.Key = TargetCardName OrElse
                 If Pair.Key = TargetCardName OrElse
                    (FrmMain.PageCurrent.Additional IsNot Nothing AndAlso '#2761
                    (FrmMain.PageCurrent.Additional IsNot Nothing AndAlso '#2761
                    CType(FrmMain.PageCurrent.Additional(1), List(Of String)).Contains(NewCard.Title)) Then
                    CType(FrmMain.PageCurrent.Additional(1), List(Of String)).Contains(NewCard.Title)) Then
-                    MyCard.StackInstall(NewStack, If(TargetResourceType = ResourceTypes.ModPack, 9, 8), Pair.Key) '9 是安装,8 是另存为
+                    NewCard.BuildContent()
                 Else
                 Else
                     NewCard.IsSwapped = True
                     NewCard.IsSwapped = True
                 End If
                 End If
@@ -240,13 +241,53 @@ GroupDone:
             Logger.Error(ex, "可视化工程下载列表出错")
             Logger.Error(ex, "可视化工程下载列表出错")
         End Try
         End Try
     End Sub
     End Sub
+    ''' <summary>
+    ''' 根据卡片中的 StackPanel.Tag 将资源版本列表数据实例化为 UI。
+    ''' </summary>
+    Private Sub InitializeStackResourceVersions(Stack As StackPanel)
+        Dim Versions = CType(Stack.Tag, List(Of ResourceVersion)).SortByComparison(Function(a, b) a.ReleaseDate > b.ReleaseDate)
+        Dim HasDuplicates = Versions.DistinctBy(Function(a) a.Display).Count <> Versions.Count
+        Dim Dependences As List(Of String)
+        '获取并添加前置信息
+        If TargetResourceType = ResourceTypes.ModPack Then
+            Dependences = New List(Of String) '整合包的前置信息会显示其中包含的一大堆 Mod,不是真正意义的前置
+        Else
+            Dependences = Versions.SelectMany(Function(f) f.Dependencies).Distinct.Ordered.ToList()
+        End If
+        If Dependences.Any() Then
+            Dependences = Dependences.Where(
+            Function(d)
+                If Not ResourceProject.Cache.ContainsKey(d) Then Logger.Warn($"未找到 ID {d} 的前置信息")
+                Return ResourceProject.Cache.ContainsKey(d)
+            End Function).ToList
+            '添加开头间隔
+            Stack.Children.Add(New TextBlock With {.Text = "前置资源", .FontSize = 14, .HorizontalAlignment = HorizontalAlignment.Left, .Margin = New Thickness(6, 2, 0, 5)})
+            '添加前置列表
+            For Each Dep In Dependences
+                Dim Item = ResourceProject.Cache(Dep).ToResourceItem(False, False)
+                Stack.Children.Add(Item)
+            Next
+            '添加结尾间隔
+            Stack.Children.Add(New TextBlock With {.Text = "版本列表", .FontSize = 14, .HorizontalAlignment = HorizontalAlignment.Left, .Margin = New Thickness(6, 12, 0, 5)})
+        End If
+        '实例化每个元素
+        For Each Version In Versions
+            If TargetResourceType = ResourceTypes.ModPack Then
+                Stack.Children.Add(Version.ToListItem(AddressOf Install_Click, AddressOf Save_Click, IsBadDisplay:=HasDuplicates))
+            Else
+                Stack.Children.Add(Version.ToListItem(AddressOf Save_Click, IsBadDisplay:=HasDuplicates))
+            End If
+        Next
+        Stack.Children.Add(New FrameworkElement With {.Height = 18})
+        Stack.Tag = Nothing
+    End Sub
     Private Function GetGroupedVersionName(Name As String, GroupedByDrop As Boolean, FoldOld As Boolean) As String
     Private Function GetGroupedVersionName(Name As String, GroupedByDrop As Boolean, FoldOld As Boolean) As String
         Dim Drop = McVersion.VersionToDrop(Name)
         Dim Drop = McVersion.VersionToDrop(Name)
         If Name Is Nothing Then
         If Name Is Nothing Then
             Return "其他"
             Return "其他"
         ElseIf Name.Contains("w") OrElse Drop = 209 Then
         ElseIf Name.Contains("w") OrElse Drop = 209 Then
             Return "快照版"
             Return "快照版"
-        ElseIf Not McVersion.IsFormatFit(Name) OrElse (FoldOld AndAlso Drop < 120) Then
+        ElseIf Not McVersion.IsStandardFormat(Name) OrElse (FoldOld AndAlso Drop < 120) Then
             Return "远古版"
             Return "远古版"
         ElseIf GroupedByDrop Then
         ElseIf GroupedByDrop Then
             Return McVersion.DropToVersion(Drop)
             Return McVersion.DropToVersion(Drop)
@@ -469,19 +510,23 @@ GroupDone:
                     '弹窗要求选择保存位置
                     '弹窗要求选择保存位置
                     Dim Target As String
                     Dim Target As String
                     Dim Ext As String = If(File.ResourceType = ResourceTypes.Mod,
                     Dim Ext As String = If(File.ResourceType = ResourceTypes.Mod,
-                        If(File.FileName.EndsWithF(".litemod"), "litemod", "jar"),
-                        If(File.FileName.EndsWithF(".mrpack"), "mrpack", "zip"))
+                        If(PathUtils.GetExtension(File.FileName) = "litemod", "litemod", "jar"),
+                        If(PathUtils.GetExtension(File.FileName) = "mrpack", "mrpack", "zip"))
                     Target = Dialogs.SaveFile("选择保存位置", FileName, DefaultFolder, {(Ext, Desc & "文件")})
                     Target = Dialogs.SaveFile("选择保存位置", FileName, DefaultFolder, {(Ext, Desc & "文件")})
                     If Target Is Nothing Then Return
                     If Target Is Nothing Then Return
                     '构造步骤加载器
                     '构造步骤加载器
                     Dim LoaderName As String = Desc & "下载:" & PathUtils.GetFileNameWithoutExtension(Target) & " "
                     Dim LoaderName As String = Desc & "下载:" & PathUtils.GetFileNameWithoutExtension(Target) & " "
-                    If Target <> DefaultFolder Then CachedFolder(File.ResourceType) = PathUtils.RemoveLastPart(Target)
+                    If Not PathUtils.IsSame(PathUtils.RemoveLastPart(Target), DefaultFolder) Then CachedFolder(File.ResourceType) = PathUtils.RemoveLastPart(Target)
                     Dim Loaders As New List(Of LoaderBase)
                     Dim Loaders As New List(Of LoaderBase)
                     Loaders.Add(New LoaderDownload("下载文件", {
                     Loaders.Add(New LoaderDownload("下载文件", {
                         File.ToNetFile(Target, If(TargetResourceType = ResourceTypes.Any, ResourceVersion.DownloadReason.Dependency, ResourceVersion.DownloadReason.Standalone))
                         File.ToNetFile(Target, If(TargetResourceType = ResourceTypes.Any, ResourceVersion.DownloadReason.Dependency, ResourceVersion.DownloadReason.Standalone))
                     }) With {.ProgressWeight = 6, .Block = True})
                     }) With {.ProgressWeight = 6, .Block = True})
                     '启动
                     '启动
-                    Dim Loader As New LoaderCombo(Of Integer)(LoaderName, Loaders) With {.OnStateChanged = AddressOf LoaderStateChangedHintOnly}
+                    Dim Loader As New LoaderCombo(Of Integer)(LoaderName, Loaders) With {.OnStateChanged =
+                    Sub(MyLoader)
+                        LoaderStateChangedHintOnly(MyLoader)
+                        If MyLoader.State = LoadState.Finished AndAlso File.ResourceType = ResourceTypes.ModPack Then OpenExplorer(Target)
+                    End Sub}
                     Loader.Start(1)
                     Loader.Start(1)
                     LoaderTaskbarAdd(Loader)
                     LoaderTaskbarAdd(Loader)
                     FrmMain.BtnExtraDownload.ShowRefresh()
                     FrmMain.BtnExtraDownload.ShowRefresh()
@@ -510,29 +555,4 @@ GroupDone:
         PageLoaderRestart(IsForceRestart:=True)
         PageLoaderRestart(IsForceRestart:=True)
     End Sub
     End Sub
 
 
-    ''' <summary>
-    ''' 预载包含大量 ProjectVersion 的卡片,添加必要的元素和前置列表。
-    ''' </summary>
-    Public Shared Sub ResourceFilesCardPreload(Stack As StackPanel, Files As List(Of ResourceVersion))
-        '获取卡片对应的前置 ID
-        '如果为整合包就不会有 Dependencies 信息,所以不用管
-        Dim Deps As List(Of String) = Files.SelectMany(Function(f) f.Dependencies).Distinct.ToList()
-        Deps.Sort()
-        If Not Deps.Any() Then Return
-        Deps = Deps.Where(
-        Function(dep)
-            If Not ResourceProject.Cache.ContainsKey(dep) Then Logger.Warn($"未找到 ID {dep} 的前置信息")
-            Return ResourceProject.Cache.ContainsKey(dep)
-        End Function).ToList
-        '添加开头间隔
-        Stack.Children.Add(New TextBlock With {.Text = "前置资源", .FontSize = 14, .HorizontalAlignment = HorizontalAlignment.Left, .Margin = New Thickness(6, 2, 0, 5)})
-        '添加前置列表
-        For Each Dep In Deps
-            Dim Item = ResourceProject.Cache(Dep).ToResourceItem(False, False)
-            Stack.Children.Add(Item)
-        Next
-        '添加结尾间隔
-        Stack.Children.Add(New TextBlock With {.Text = "版本列表", .FontSize = 14, .HorizontalAlignment = HorizontalAlignment.Left, .Margin = New Thickness(6, 12, 0, 5)})
-    End Sub
-
 End Class
 End Class

+ 3 - 3
Plain Craft Launcher 2/Pages/PageDownload/Resource/PageResource.xaml.vb

@@ -90,11 +90,11 @@ Public Class PageResource
                 Next
                 Next
                 Return ComboSearchLoader.Items(0)
                 Return ComboSearchLoader.Items(0)
             End Function
             End Function
-            If TargetInstance.Version.HasForge Then
+            If TargetInstance.Version.ModLoaders.HasFlagF(ModLoaders.Forge) Then
                 ComboSearchLoader.SelectedItem = GetTargetItemByName("Forge")
                 ComboSearchLoader.SelectedItem = GetTargetItemByName("Forge")
-            ElseIf TargetInstance.Version.HasFabric Then
+            ElseIf TargetInstance.Version.ModLoaders.HasFlagF(ModLoaders.Fabric) Then
                 ComboSearchLoader.SelectedItem = GetTargetItemByName("Fabric")
                 ComboSearchLoader.SelectedItem = GetTargetItemByName("Fabric")
-            ElseIf TargetInstance.Version.HasNeoForge Then
+            ElseIf TargetInstance.Version.ModLoaders.HasFlagF(ModLoaders.NeoForge) Then
                 ComboSearchLoader.SelectedItem = GetTargetItemByName("NeoForge")
                 ComboSearchLoader.SelectedItem = GetTargetItemByName("NeoForge")
             End If
             End If
             TargetInstance = Nothing
             TargetInstance = Nothing

+ 1 - 1
Plain Craft Launcher 2/Pages/PageInstance/MyLocalModItem.xaml.vb

@@ -335,7 +335,7 @@ Public Class MyLocalModItem
             BtnUpdate.Visibility = Visibility.Collapsed
             BtnUpdate.Visibility = Visibility.Collapsed
         End If
         End If
         '标题与描述
         '标题与描述
-        Dim DescFileName As String = Entry.File.Name.BeforeLast(".")
+        Dim DescFileName As String = PathUtils.GetFileNameWithoutExtension(Entry.EnabledName)
         Dim NewDescription As String
         Dim NewDescription As String
         If Settings.Get(Of Integer)("ToolModLocalNameStyle") = 1 Then
         If Settings.Get(Of Integer)("ToolModLocalNameStyle") = 1 Then
             '标题显示文件名,详情显示译名
             '标题显示文件名,详情显示译名

+ 4 - 4
Plain Craft Launcher 2/Pages/PageInstance/PageInstanceExport.xaml

@@ -65,10 +65,10 @@
                         <local:MyCheckBox Margin="30,0,0,0">
                         <local:MyCheckBox Margin="30,0,0,0">
                             <local:MyCheckBox.Tag>
                             <local:MyCheckBox.Tag>
                                 <local:ExportOption 
                                 <local:ExportOption 
-                                Title="OptiFine 设置"
-                                Rules="optionsof.txt|optionsshaders.txt"
-                                DefaultChecked="True"
-                                RequireOptiFine="True" />
+                                 Title="OptiFine 设置"
+                                 Rules="optionsof.txt|optionsshaders.txt"
+                                 DefaultChecked="True"
+                                 RequireOptiFine="True" />
                             </local:MyCheckBox.Tag>
                             </local:MyCheckBox.Tag>
                         </local:MyCheckBox>
                         </local:MyCheckBox>
                         <local:MyCheckBox x:Name="CheckOptionsMod">
                         <local:MyCheckBox x:Name="CheckOptionsMod">

+ 18 - 18
Plain Craft Launcher 2/Pages/PageInstance/PageInstanceExport.xaml.vb

@@ -22,14 +22,14 @@ Public Class PageInstanceExport
     End Sub
     End Sub
     Private Sub PageInstanceExport_Loaded() Handles Me.Loaded
     Private Sub PageInstanceExport_Loaded() Handles Me.Loaded
         AniControlEnabled += 1
         AniControlEnabled += 1
-        If CurrentInstance <> PageInstanceLeft.Instance.PathVersion Then RefreshAll() '切换到了另一个版本,重置页面
+        If Not PathUtils.IsSame(CurrentInstance, PageInstanceLeft.Instance.PathVersion) Then RefreshAll() '切换到了另一个版本,重置页面
         CustomEventService.SetEventData(BtnAdvancedHelp,
         CustomEventService.SetEventData(BtnAdvancedHelp,
             If(BuildType = BuildTypes.Release, "指南/整合包制作 - Public.json", "指南/整合包制作 - Snapshot.json"))
             If(BuildType = BuildTypes.Release, "指南/整合包制作 - Public.json", "指南/整合包制作 - Snapshot.json"))
         AniControlEnabled -= 1
         AniControlEnabled -= 1
     End Sub
     End Sub
     Public Sub RefreshAll() Implements IRefreshable.Refresh
     Public Sub RefreshAll() Implements IRefreshable.Refresh
         Logger.Info("刷新导出页面")
         Logger.Info("刷新导出页面")
-        HintOptiFine.Visibility = If(PageInstanceLeft.Instance.Version.HasOptiFine, Visibility.Visible, Visibility.Collapsed)
+        HintOptiFine.Visibility = If(PageInstanceLeft.Instance.Version.HasOptiFine AndAlso Not PageInstanceLeft.Instance.Version.OptiFineAsMod, Visibility.Visible, Visibility.Collapsed)
         CurrentInstance = PageInstanceLeft.Instance.PathVersion
         CurrentInstance = PageInstanceLeft.Instance.PathVersion
         TextExportName.Text = ""
         TextExportName.Text = ""
         TextExportName.HintText = PageInstanceLeft.Instance.Name
         TextExportName.HintText = PageInstanceLeft.Instance.Name
@@ -114,7 +114,7 @@ Public Class PageInstanceExport
         '确认选项是否应该被显示
         '确认选项是否应该被显示
         Dim IsVisible =
         Dim IsVisible =
         Function(TargetOption As ExportOption) As Boolean
         Function(TargetOption As ExportOption) As Boolean
-            '检查需要 OptiFine 或 Mod 加载器
+            '检查需要 OptiFine(例如光影)或 Mod 加载器(例如 mods/)
             If TargetOption.RequireOptiFine AndAlso Not PageInstanceLeft.Instance.Version.HasOptiFine Then Return False
             If TargetOption.RequireOptiFine AndAlso Not PageInstanceLeft.Instance.Version.HasOptiFine Then Return False
             If TargetOption.RequireModLoader AndAlso Not PageInstanceLeft.Instance.Modable Then Return False
             If TargetOption.RequireModLoader AndAlso Not PageInstanceLeft.Instance.Modable Then Return False
             If TargetOption.RequireModLoaderOrOptiFine AndAlso Not PageInstanceLeft.Instance.Version.HasOptiFine AndAlso Not PageInstanceLeft.Instance.Modable Then Return False
             If TargetOption.RequireModLoaderOrOptiFine AndAlso Not PageInstanceLeft.Instance.Version.HasOptiFine AndAlso Not PageInstanceLeft.Instance.Modable Then Return False
@@ -257,9 +257,9 @@ Public Class PageInstanceExport
                     For Each ShaderPackCheckBox In PanOptionsShaderPacks.Children.OfType(Of MyCheckBox)()
                     For Each ShaderPackCheckBox In PanOptionsShaderPacks.Children.OfType(Of MyCheckBox)()
                         If Not ShaderPackCheckBox.Checked Then Continue For
                         If Not ShaderPackCheckBox.Checked Then Continue For
                         Dim ShaderPackOption = GetExportOption(ShaderPackCheckBox)
                         Dim ShaderPackOption = GetExportOption(ShaderPackCheckBox)
-                        Dim SettingFileName = ShaderPackOption.Rules.TrimEnd("/") & ".txt"
-                        If Not FileUtils.Exists(Path.Combine(PageInstanceLeft.Instance.PathIndie, SettingFileName)) Then Continue For
-                        Rules.Add(StringUtils.LikePatternEscape($"shaderpacks/{SettingFileName}"))
+                        Dim SettingsFile = $"shaderpacks/{ShaderPackOption.Title}.txt"
+                        If Not FileUtils.Exists(Path.Combine(PageInstanceLeft.Instance.PathIndie, SettingsFile)) Then Continue For
+                        Rules.Add(StringUtils.LikePatternEscape(SettingsFile))
                     Next
                     Next
                 End If
                 End If
 
 
@@ -457,11 +457,11 @@ Public Class PageInstanceExport
         '缓存所需参数
         '缓存所需参数
         Dim CacheFolder = RequestTaskTempFolder()
         Dim CacheFolder = RequestTaskTempFolder()
         Dim OverridesFolder = CacheFolder & "modpack\overrides\"
         Dim OverridesFolder = CacheFolder & "modpack\overrides\"
-        Dim McVersion = PageInstanceLeft.Instance
+        Dim TargetInstance = PageInstanceLeft.Instance
         Dim IncludeJava As Boolean =
         Dim IncludeJava As Boolean =
             (RulesOverrides Is Nothing AndAlso CheckOptionsJava.Visibility = Visibility.Visible AndAlso CheckOptionsJava.Checked) OrElse
             (RulesOverrides Is Nothing AndAlso CheckOptionsJava.Visibility = Visibility.Visible AndAlso CheckOptionsJava.Checked) OrElse
-            (RulesOverrides IsNot Nothing AndAlso CheckOptionsJava.Checked AndAlso Settings.Get(Of Integer)("VersionArgumentJavaV2", McVersion) = 2)
-        Dim PathIndie As String = McVersion.PathIndie
+            (RulesOverrides IsNot Nothing AndAlso CheckOptionsJava.Checked AndAlso Settings.Get(Of Integer)("VersionArgumentJavaV2", TargetInstance) = 2)
+        Dim PathIndie As String = TargetInstance.PathIndie
         Dim CheckHostedAssets As Boolean = Not CheckAdvancedInclude.Checked
         Dim CheckHostedAssets As Boolean = Not CheckAdvancedInclude.Checked
         Dim ModrinthUploadMode As Boolean = CheckAdvancedModrinth.Checked
         Dim ModrinthUploadMode As Boolean = CheckAdvancedModrinth.Checked
         Dim IncludePCL As Boolean = CheckOptionsPcl.Checked
         Dim IncludePCL As Boolean = CheckOptionsPcl.Checked
@@ -498,7 +498,7 @@ Public Class PageInstanceExport
                 '文件夹:进一步搜索
                 '文件夹:进一步搜索
                 For Each SubFolder In Folder.EnumerateDirectories("*", SearchOption.TopDirectoryOnly)
                 For Each SubFolder In Folder.EnumerateDirectories("*", SearchOption.TopDirectoryOnly)
                     '跳过部分又没用文件又多的文件夹,加快搜索
                     '跳过部分又没用文件又多的文件夹,加快搜索
-                    If {"assets", "versions", "libraries"}.Contains(SubFolder.Name) AndAlso PathIndie = PathUtils.RemoveExtendedPrefix(Folder.FullName) Then Continue For
+                    If {"assets", "versions", "libraries"}.Contains(SubFolder.Name) AndAlso PathUtils.IsSame(PathIndie, Folder.FullName) Then Continue For
                     If {"structureCacheV1", ".fabric", ".git", "avatar-cache", "cosmetic-cache"}.Contains(SubFolder.Name) Then Continue For
                     If {"structureCacheV1", ".fabric", ".git", "avatar-cache", "cosmetic-cache"}.Contains(SubFolder.Name) Then Continue For
                     SearchFolder(SubFolder)
                     SearchFolder(SubFolder)
                 Next
                 Next
@@ -536,10 +536,10 @@ Public Class PageInstanceExport
             Loader.Progress = 0.85
             Loader.Progress = 0.85
             '复制 Java
             '复制 Java
             If IncludeJava Then
             If IncludeJava Then
-                Dim JavaToExport = SelectOrDownloadJava(McVersion, False, Loader.CreateCancellationToken, Loader.CreateSyncProgressProvider(0.85, 0.93))
+                Dim JavaToExport = SelectOrDownloadJava(TargetInstance, False, Loader.CreateCancellationToken, Loader.CreateSyncProgressProvider(0.85, 0.93))
                 If JavaToExport Is Nothing Then Throw New FileNotFoundException("版本文件夹中没有找到可导出的 Java。")
                 If JavaToExport Is Nothing Then Throw New FileNotFoundException("版本文件夹中没有找到可导出的 Java。")
                 Dim JavaFolder = PathUtils.RemoveLastPart(JavaToExport.Folder)
                 Dim JavaFolder = PathUtils.RemoveLastPart(JavaToExport.Folder)
-                Dim DestFolder As String = Path.Combine(OverridesFolder, JavaFolder.AfterFirst(McVersion.PathVersion, True))
+                Dim DestFolder As String = Path.Combine(OverridesFolder, JavaFolder.AfterFirst(TargetInstance.PathVersion, True))
                 DirectoryUtils.Copy(JavaFolder, DestFolder)
                 DirectoryUtils.Copy(JavaFolder, DestFolder)
                 Logger.Info($"已复制版本文件夹中的 Java:{JavaFolder} → {DestFolder}")
                 Logger.Info($"已复制版本文件夹中的 Java:{JavaFolder} → {DestFolder}")
             End If
             End If
@@ -563,7 +563,7 @@ Public Class PageInstanceExport
             Next
             Next
             Loader.Progress = 0.97
             Loader.Progress = 0.97
             '复制 PCL 版本设置
             '复制 PCL 版本设置
-            If DirectoryUtils.Exists(McVersion.PathVersion & "PCL\") Then DirectoryUtils.Copy(McVersion.PathVersion & "PCL\", OverridesFolder & "PCL\")
+            If DirectoryUtils.Exists(TargetInstance.PathVersion & "PCL\") Then DirectoryUtils.Copy(TargetInstance.PathVersion & "PCL\", OverridesFolder & "PCL\")
             WriteIni(OverridesFolder & "PCL\Setup.ini", "IsStar", False)
             WriteIni(OverridesFolder & "PCL\Setup.ini", "IsStar", False)
             '复制 PCL 本体(正式版)
             '复制 PCL 本体(正式版)
             If BuildType = BuildTypes.Release AndAlso IncludePCL Then FileUtils.Copy(PathExe, CacheFolder & "Plain Craft Launcher.exe")
             If BuildType = BuildTypes.Release AndAlso IncludePCL Then FileUtils.Copy(PathExe, CacheFolder & "Plain Craft Launcher.exe")
@@ -686,16 +686,16 @@ Public Class PageInstanceExport
             Next
             Next
             Loader.Progress = 0.2
             Loader.Progress = 0.2
             '导出最终 JSON 文件
             '导出最终 JSON 文件
-            Dim Dependencies As New JObject From {{"minecraft", McVersion.Version.VanillaName}}
-            If McVersion.Version.HasForge Then Dependencies.Add("forge", McVersion.Version.Forge)
-            If McVersion.Version.HasFabric Then Dependencies.Add("fabric-loader", McVersion.Version.Fabric)
-            If McVersion.Version.HasNeoForge Then Dependencies.Add("neoforge", McVersion.Version.NeoForge)
+            Dim Dependencies As New JObject From {{"minecraft", TargetInstance.Version.VanillaName}}
+            If TargetInstance.Version.ModLoaders.HasFlagF(ModLoaders.Forge) Then Dependencies.Add("forge", TargetInstance.Version.ModLoaderVersions(ModLoaders.Forge))
+            If TargetInstance.Version.ModLoaders.HasFlagF(ModLoaders.Fabric) Then Dependencies.Add("fabric-loader", TargetInstance.Version.ModLoaderVersions(ModLoaders.Fabric))
+            If TargetInstance.Version.ModLoaders.HasFlagF(ModLoaders.NeoForge) Then Dependencies.Add("neoforge", TargetInstance.Version.ModLoaderVersions(ModLoaders.NeoForge))
             Dim ResultJson As New JObject From {
             Dim ResultJson As New JObject From {
                 {"game", "minecraft"},
                 {"game", "minecraft"},
                 {"formatVersion", 1},
                 {"formatVersion", 1},
                 {"versionId", PackVersion},
                 {"versionId", PackVersion},
                 {"name", PackName},
                 {"name", PackName},
-                {"summary", McVersion.Info},
+                {"summary", TargetInstance.Info},
                 {"files", Files},
                 {"files", Files},
                 {"dependencies", Dependencies}
                 {"dependencies", Dependencies}
             }
             }

+ 13 - 5
Plain Craft Launcher 2/Pages/PageInstance/PageInstanceLeft.xaml

@@ -13,21 +13,29 @@
                 </x:Array>
                 </x:Array>
             </local:MyListItem.Buttons>
             </local:MyListItem.Buttons>
         </local:MyListItem>
         </local:MyListItem>
-        <local:MyListItem x:Name="ItemMod" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="2" MinPaddingRight="35" Height="36" VerticalAlignment="Top" Title="Mod 管理"
-                          Logo="M789.504 912.896h-195.072a35.328 35.328 0 0 1-34.816-35.328v-77.312a39.936 39.936 0 0 0-40.448-39.936H481.28a40.448 40.448 0 0 0-40.448 39.936v77.312a34.816 34.816 0 0 1-34.816 35.328H204.8a41.984 41.984 0 0 1-40.448-42.496v-200.192a35.328 35.328 0 0 1 35.328-35.328h72.704a39.936 39.936 0 0 0 39.936-39.936v-37.888a39.936 39.936 0 0 0-39.936-40.448H199.68a35.328 35.328 0 0 1-35.328-35.328V287.744A41.984 41.984 0 0 1 204.8 245.76h176.64v-32.768a102.4 102.4 0 0 1 102.4-102.4h33.792a102.4 102.4 0 0 1 102.4 102.4v32.768h170.496a41.984 41.984 0 0 1 41.984 41.984V460.8h28.672a102.4 102.4 0 0 1 102.4 102.4v33.792a102.4 102.4 0 0 1-102.4 102.4h-28.672V870.4a41.984 41.984 0 0 1-43.008 42.496z m-159.744-70.144h131.584V665.6a34.304 34.304 0 0 1 34.816-34.816h64a31.744 31.744 0 0 0 31.744-31.744V563.2a31.744 31.744 0 0 0-31.744-31.744h-64a34.816 34.816 0 0 1-34.816-35.328V316.416h-177.152a35.328 35.328 0 0 1-35.328-35.328V212.992a31.744 31.744 0 0 0-31.744-31.744h-33.792a31.744 31.744 0 0 0-31.744 31.744v68.096a35.328 35.328 0 0 1-34.816 35.328H234.496v130.048h37.888a110.592 110.592 0 0 1 110.08 110.592v37.888a110.592 110.592 0 0 1-110.08 110.592h-37.888v137.216h136.192v-42.496a110.592 110.592 0 0 1 110.592-110.08h37.888a110.592 110.592 0 0 1 110.592 110.08z" >
+        <local:MyListItem x:Name="ItemModify" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="2" MinPaddingRight="35" Height="36" VerticalAlignment="Top" Title="修改" LogoScale="0.95"
+                          Logo="F1 M652.624 145.904c62.256-62.272 163.2-62.272 225.472 0 62.272 62.256 62.272 163.2 0 225.472L477.808 771.664a28.8 28.8 0 0 1-10.816 6.8L242.4 857.376c-47.04 16.544-92.32-28.736-75.776-75.776l78.896-224.592c1.44-4.08 3.76-7.776 6.816-10.816L652.624 145.904z M828.604 195.396a89.44 89.44 0 0 0-126.488 0L308.48 589.032 241.68 781.12 433.768 714.32 828.604 321.884a89.44 89.44 0 0 0 0-126.488z M606.256 194.816a35 35 0 0 1 49.504 0l173.728 173.728a35 35 0 0 1-49.504 49.504L606.256 244.32a35 35 0 0 1 0-49.504z M261.984 539.104a35 35 0 0 1 49.504 0l173.728 173.728a35 35 0 0 1-49.504 49.504L261.984 588.608a35 35 0 0 1 0-49.504z" >
             <local:MyListItem.Buttons>
             <local:MyListItem.Buttons>
                 <x:Array Type="{x:Type local:MyIconButton}">
                 <x:Array Type="{x:Type local:MyIconButton}">
                     <local:MyIconButton Tag="2" ToolTip="刷新" Click="Refresh_Click" LogoScale="0.85" Logo="M512.0 838.3c-80.2 0-153.4-29.3-210.2-77.4l75.5-75.5c11.5-11.5 25.8-22.0 25.8-37.0a27.2 27.2 0 0 0-27.1-27.1H104.0c-27.1 0-27.1 23.9-27.1 27.1v271.9a27.1 27.1 0 0 0 27.1 27.1c15.0 0 27.8-16.6 42.5-31.2l77.9-77.9c76.6 67.7 177.1 108.9 287.4 108.9 221.7 0 404.5-166.0 431.2-380.6h-109.8c-25.9 154.2-159.7 271.9-321.3 271.9zM919.9 76.6c-15.0 0-27.8 16.6-42.5 31.3L799.5 185.8c-76.5-67.7-177.1-108.9-287.4-108.9-221.8 0-404.5 166.1-431.3 380.6H190.6c25.9-154.2 159.7-271.9 321.4-271.9 80.2 0 153.4 29.3 210.1 77.4l-75.5 75.5c-11.6 11.5-25.8 22.0-25.8 37.1a27.2 27.2 0 0 0 27.1 27.1h271.9c27.1 0 27.1-23.9 27.1-27.1V103.8a27.1 27.1 0 0 0-27.1-27.1z" />
                     <local:MyIconButton Tag="2" ToolTip="刷新" Click="Refresh_Click" LogoScale="0.85" Logo="M512.0 838.3c-80.2 0-153.4-29.3-210.2-77.4l75.5-75.5c11.5-11.5 25.8-22.0 25.8-37.0a27.2 27.2 0 0 0-27.1-27.1H104.0c-27.1 0-27.1 23.9-27.1 27.1v271.9a27.1 27.1 0 0 0 27.1 27.1c15.0 0 27.8-16.6 42.5-31.2l77.9-77.9c76.6 67.7 177.1 108.9 287.4 108.9 221.7 0 404.5-166.0 431.2-380.6h-109.8c-25.9 154.2-159.7 271.9-321.3 271.9zM919.9 76.6c-15.0 0-27.8 16.6-42.5 31.3L799.5 185.8c-76.5-67.7-177.1-108.9-287.4-108.9-221.8 0-404.5 166.1-431.3 380.6H190.6c25.9-154.2 159.7-271.9 321.4-271.9 80.2 0 153.4 29.3 210.1 77.4l-75.5 75.5c-11.6 11.5-25.8 22.0-25.8 37.1a27.2 27.2 0 0 0 27.1 27.1h271.9c27.1 0 27.1-23.9 27.1-27.1V103.8a27.1 27.1 0 0 0-27.1-27.1z" />
                 </x:Array>
                 </x:Array>
             </local:MyListItem.Buttons>
             </local:MyListItem.Buttons>
         </local:MyListItem>
         </local:MyListItem>
-        <local:MyListItem x:Name="ItemModDisabled" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="3" MinPaddingRight="35" Height="36" VerticalAlignment="Top" Title="Mod 管理"
+        <local:MyListItem x:Name="ItemMod" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="3" MinPaddingRight="35" Height="36" VerticalAlignment="Top" Title="Mod 管理"
+                          Logo="M789.504 912.896h-195.072a35.328 35.328 0 0 1-34.816-35.328v-77.312a39.936 39.936 0 0 0-40.448-39.936H481.28a40.448 40.448 0 0 0-40.448 39.936v77.312a34.816 34.816 0 0 1-34.816 35.328H204.8a41.984 41.984 0 0 1-40.448-42.496v-200.192a35.328 35.328 0 0 1 35.328-35.328h72.704a39.936 39.936 0 0 0 39.936-39.936v-37.888a39.936 39.936 0 0 0-39.936-40.448H199.68a35.328 35.328 0 0 1-35.328-35.328V287.744A41.984 41.984 0 0 1 204.8 245.76h176.64v-32.768a102.4 102.4 0 0 1 102.4-102.4h33.792a102.4 102.4 0 0 1 102.4 102.4v32.768h170.496a41.984 41.984 0 0 1 41.984 41.984V460.8h28.672a102.4 102.4 0 0 1 102.4 102.4v33.792a102.4 102.4 0 0 1-102.4 102.4h-28.672V870.4a41.984 41.984 0 0 1-43.008 42.496z m-159.744-70.144h131.584V665.6a34.304 34.304 0 0 1 34.816-34.816h64a31.744 31.744 0 0 0 31.744-31.744V563.2a31.744 31.744 0 0 0-31.744-31.744h-64a34.816 34.816 0 0 1-34.816-35.328V316.416h-177.152a35.328 35.328 0 0 1-35.328-35.328V212.992a31.744 31.744 0 0 0-31.744-31.744h-33.792a31.744 31.744 0 0 0-31.744 31.744v68.096a35.328 35.328 0 0 1-34.816 35.328H234.496v130.048h37.888a110.592 110.592 0 0 1 110.08 110.592v37.888a110.592 110.592 0 0 1-110.08 110.592h-37.888v137.216h136.192v-42.496a110.592 110.592 0 0 1 110.592-110.08h37.888a110.592 110.592 0 0 1 110.592 110.08z" >
+            <local:MyListItem.Buttons>
+                <x:Array Type="{x:Type local:MyIconButton}">
+                    <local:MyIconButton Tag="3" ToolTip="刷新" Click="Refresh_Click" LogoScale="0.85" Logo="M512.0 838.3c-80.2 0-153.4-29.3-210.2-77.4l75.5-75.5c11.5-11.5 25.8-22.0 25.8-37.0a27.2 27.2 0 0 0-27.1-27.1H104.0c-27.1 0-27.1 23.9-27.1 27.1v271.9a27.1 27.1 0 0 0 27.1 27.1c15.0 0 27.8-16.6 42.5-31.2l77.9-77.9c76.6 67.7 177.1 108.9 287.4 108.9 221.7 0 404.5-166.0 431.2-380.6h-109.8c-25.9 154.2-159.7 271.9-321.3 271.9zM919.9 76.6c-15.0 0-27.8 16.6-42.5 31.3L799.5 185.8c-76.5-67.7-177.1-108.9-287.4-108.9-221.8 0-404.5 166.1-431.3 380.6H190.6c25.9-154.2 159.7-271.9 321.4-271.9 80.2 0 153.4 29.3 210.1 77.4l-75.5 75.5c-11.6 11.5-25.8 22.0-25.8 37.1a27.2 27.2 0 0 0 27.1 27.1h271.9c27.1 0 27.1-23.9 27.1-27.1V103.8a27.1 27.1 0 0 0-27.1-27.1z" />
+                </x:Array>
+            </local:MyListItem.Buttons>
+        </local:MyListItem>
+        <local:MyListItem x:Name="ItemModDisabled" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="4" MinPaddingRight="35" Height="36" VerticalAlignment="Top" Title="Mod 管理"
                           Logo="M789.504 912.896h-195.072a35.328 35.328 0 0 1-34.816-35.328v-77.312a39.936 39.936 0 0 0-40.448-39.936H481.28a40.448 40.448 0 0 0-40.448 39.936v77.312a34.816 34.816 0 0 1-34.816 35.328H204.8a41.984 41.984 0 0 1-40.448-42.496v-200.192a35.328 35.328 0 0 1 35.328-35.328h72.704a39.936 39.936 0 0 0 39.936-39.936v-37.888a39.936 39.936 0 0 0-39.936-40.448H199.68a35.328 35.328 0 0 1-35.328-35.328V287.744A41.984 41.984 0 0 1 204.8 245.76h176.64v-32.768a102.4 102.4 0 0 1 102.4-102.4h33.792a102.4 102.4 0 0 1 102.4 102.4v32.768h170.496a41.984 41.984 0 0 1 41.984 41.984V460.8h28.672a102.4 102.4 0 0 1 102.4 102.4v33.792a102.4 102.4 0 0 1-102.4 102.4h-28.672V870.4a41.984 41.984 0 0 1-43.008 42.496z m-159.744-70.144h131.584V665.6a34.304 34.304 0 0 1 34.816-34.816h64a31.744 31.744 0 0 0 31.744-31.744V563.2a31.744 31.744 0 0 0-31.744-31.744h-64a34.816 34.816 0 0 1-34.816-35.328V316.416h-177.152a35.328 35.328 0 0 1-35.328-35.328V212.992a31.744 31.744 0 0 0-31.744-31.744h-33.792a31.744 31.744 0 0 0-31.744 31.744v68.096a35.328 35.328 0 0 1-34.816 35.328H234.496v130.048h37.888a110.592 110.592 0 0 1 110.08 110.592v37.888a110.592 110.592 0 0 1-110.08 110.592h-37.888v137.216h136.192v-42.496a110.592 110.592 0 0 1 110.592-110.08h37.888a110.592 110.592 0 0 1 110.592 110.08z" />
                           Logo="M789.504 912.896h-195.072a35.328 35.328 0 0 1-34.816-35.328v-77.312a39.936 39.936 0 0 0-40.448-39.936H481.28a40.448 40.448 0 0 0-40.448 39.936v77.312a34.816 34.816 0 0 1-34.816 35.328H204.8a41.984 41.984 0 0 1-40.448-42.496v-200.192a35.328 35.328 0 0 1 35.328-35.328h72.704a39.936 39.936 0 0 0 39.936-39.936v-37.888a39.936 39.936 0 0 0-39.936-40.448H199.68a35.328 35.328 0 0 1-35.328-35.328V287.744A41.984 41.984 0 0 1 204.8 245.76h176.64v-32.768a102.4 102.4 0 0 1 102.4-102.4h33.792a102.4 102.4 0 0 1 102.4 102.4v32.768h170.496a41.984 41.984 0 0 1 41.984 41.984V460.8h28.672a102.4 102.4 0 0 1 102.4 102.4v33.792a102.4 102.4 0 0 1-102.4 102.4h-28.672V870.4a41.984 41.984 0 0 1-43.008 42.496z m-159.744-70.144h131.584V665.6a34.304 34.304 0 0 1 34.816-34.816h64a31.744 31.744 0 0 0 31.744-31.744V563.2a31.744 31.744 0 0 0-31.744-31.744h-64a34.816 34.816 0 0 1-34.816-35.328V316.416h-177.152a35.328 35.328 0 0 1-35.328-35.328V212.992a31.744 31.744 0 0 0-31.744-31.744h-33.792a31.744 31.744 0 0 0-31.744 31.744v68.096a35.328 35.328 0 0 1-34.816 35.328H234.496v130.048h37.888a110.592 110.592 0 0 1 110.08 110.592v37.888a110.592 110.592 0 0 1-110.08 110.592h-37.888v137.216h136.192v-42.496a110.592 110.592 0 0 1 110.592-110.08h37.888a110.592 110.592 0 0 1 110.592 110.08z" />
-        <local:MyListItem x:Name="ItemExport" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="4" MinPaddingRight="35" Height="36" VerticalAlignment="Top" Title="导出"
+        <local:MyListItem x:Name="ItemExport" IsScaleAnimationEnabled="False" Type="RadioBox" Tag="5" MinPaddingRight="35" Height="36" VerticalAlignment="Top" Title="导出"
                           Logo="M511 995a128 128 0 0 1-57-13L70 791a126 126 0 0 1-70-113V311a126 126 0 0 1 15-60V248c1-2 3-5 5-8a127 127 0 0 1 49-42L454 13a128 128 0 0 1 112 0l383 190a126 126 0 0 1 72 113v360a126 126 0 0 1-70 115L568 984c-17 7-37 11-57 11z m42-470v370l360-178c14-7 23-21 23-38v-335L554 524zM85 330v347a42 42 0 0 0 23 38l360 178V523L85 330zM135 260l375 189 137-65L286 188 135 260z m245-118l363 197 150-71-365-180a42 42 0 0 0-37 0l-111 53z">
                           Logo="M511 995a128 128 0 0 1-57-13L70 791a126 126 0 0 1-70-113V311a126 126 0 0 1 15-60V248c1-2 3-5 5-8a127 127 0 0 1 49-42L454 13a128 128 0 0 1 112 0l383 190a126 126 0 0 1 72 113v360a126 126 0 0 1-70 115L568 984c-17 7-37 11-57 11z m42-470v370l360-178c14-7 23-21 23-38v-335L554 524zM85 330v347a42 42 0 0 0 23 38l360 178V523L85 330zM135 260l375 189 137-65L286 188 135 260z m245-118l363 197 150-71-365-180a42 42 0 0 0-37 0l-111 53z">
             <local:MyListItem.Buttons>
             <local:MyListItem.Buttons>
                 <x:Array Type="{x:Type local:MyIconButton}">
                 <x:Array Type="{x:Type local:MyIconButton}">
-                    <local:MyIconButton Tag="4" ToolTip="刷新" Click="Refresh_Click" LogoScale="0.85" Logo="M512.0 838.3c-80.2 0-153.4-29.3-210.2-77.4l75.5-75.5c11.5-11.5 25.8-22.0 25.8-37.0a27.2 27.2 0 0 0-27.1-27.1H104.0c-27.1 0-27.1 23.9-27.1 27.1v271.9a27.1 27.1 0 0 0 27.1 27.1c15.0 0 27.8-16.6 42.5-31.2l77.9-77.9c76.6 67.7 177.1 108.9 287.4 108.9 221.7 0 404.5-166.0 431.2-380.6h-109.8c-25.9 154.2-159.7 271.9-321.3 271.9zM919.9 76.6c-15.0 0-27.8 16.6-42.5 31.3L799.5 185.8c-76.5-67.7-177.1-108.9-287.4-108.9-221.8 0-404.5 166.1-431.3 380.6H190.6c25.9-154.2 159.7-271.9 321.4-271.9 80.2 0 153.4 29.3 210.1 77.4l-75.5 75.5c-11.6 11.5-25.8 22.0-25.8 37.1a27.2 27.2 0 0 0 27.1 27.1h271.9c27.1 0 27.1-23.9 27.1-27.1V103.8a27.1 27.1 0 0 0-27.1-27.1z" />
+                    <local:MyIconButton Tag="5" ToolTip="刷新" Click="Refresh_Click" LogoScale="0.85" Logo="M512.0 838.3c-80.2 0-153.4-29.3-210.2-77.4l75.5-75.5c11.5-11.5 25.8-22.0 25.8-37.0a27.2 27.2 0 0 0-27.1-27.1H104.0c-27.1 0-27.1 23.9-27.1 27.1v271.9a27.1 27.1 0 0 0 27.1 27.1c15.0 0 27.8-16.6 42.5-31.2l77.9-77.9c76.6 67.7 177.1 108.9 287.4 108.9 221.7 0 404.5-166.0 431.2-380.6h-109.8c-25.9 154.2-159.7 271.9-321.3 271.9zM919.9 76.6c-15.0 0-27.8 16.6-42.5 31.3L799.5 185.8c-76.5-67.7-177.1-108.9-287.4-108.9-221.8 0-404.5 166.1-431.3 380.6H190.6c25.9-154.2 159.7-271.9 321.4-271.9 80.2 0 153.4 29.3 210.1 77.4l-75.5 75.5c-11.6 11.5-25.8 22.0-25.8 37.1a27.2 27.2 0 0 0 27.1 27.1h271.9c27.1 0 27.1-23.9 27.1-27.1V103.8a27.1 27.1 0 0 0-27.1-27.1z" />
                 </x:Array>
                 </x:Array>
             </local:MyListItem.Buttons>
             </local:MyListItem.Buttons>
         </local:MyListItem>
         </local:MyListItem>

+ 21 - 3
Plain Craft Launcher 2/Pages/PageInstance/PageInstanceLeft.xaml.vb

@@ -6,6 +6,10 @@ Public Class PageInstanceLeft
     ''' </summary>
     ''' </summary>
     Public Shared Instance As McInstance = Nothing
     Public Shared Instance As McInstance = Nothing
 
 
+    Private Sub PageInstanceLeft_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
+        PageSetupUI.HiddenRefresh()
+    End Sub
+
     Public Sub RefreshModDisabled() Handles Me.Loaded
     Public Sub RefreshModDisabled() Handles Me.Loaded
         If Instance IsNot Nothing AndAlso Instance.Modable Then
         If Instance IsNot Nothing AndAlso Instance.Modable Then
             ItemMod.Visibility = Visibility.Visible
             ItemMod.Visibility = Visibility.Visible
@@ -26,7 +30,7 @@ Public Class PageInstanceLeft
     ''' <summary>
     ''' <summary>
     ''' 勾选事件改变页面。
     ''' 勾选事件改变页面。
     ''' </summary>
     ''' </summary>
-    Private Sub PageCheck(sender As FrameworkElement, e As RouteEventArgs) Handles ItemOverall.Check, ItemMod.Check, ItemModDisabled.Check, ItemSetup.Check, ItemExport.Check
+    Private Sub PageCheck(sender As FrameworkElement, e As RouteEventArgs) Handles ItemOverall.Check, ItemMod.Check, ItemModDisabled.Check, ItemSetup.Check, ItemModify.Check, ItemExport.Check
         '尚未初始化控件属性时,sender.Tag 为 Nothing,会导致切换到页面 0
         '尚未初始化控件属性时,sender.Tag 为 Nothing,会导致切换到页面 0
         '若使用 IsLoaded,则会导致模拟点击不被执行(模拟点击切换页面时,控件的 IsLoaded 为 False)
         '若使用 IsLoaded,则会导致模拟点击不被执行(模拟点击切换页面时,控件的 IsLoaded 为 False)
         If sender.Tag IsNot Nothing Then PageChange(Val(sender.Tag))
         If sender.Tag IsNot Nothing Then PageChange(Val(sender.Tag))
@@ -45,11 +49,14 @@ Public Class PageInstanceLeft
                 If FrmInstanceModDisabled Is Nothing Then FrmInstanceModDisabled = New PageInstanceModDisabled
                 If FrmInstanceModDisabled Is Nothing Then FrmInstanceModDisabled = New PageInstanceModDisabled
                 Return FrmInstanceModDisabled
                 Return FrmInstanceModDisabled
             Case FormMain.PageSubType.InstanceSetup
             Case FormMain.PageSubType.InstanceSetup
-                If IsNothing(FrmInstanceSetup) Then FrmInstanceSetup = New PageInstanceSetup
+                If FrmInstanceSetup Is Nothing Then FrmInstanceSetup = New PageInstanceSetup
                 Return FrmInstanceSetup
                 Return FrmInstanceSetup
             Case FormMain.PageSubType.InstanceExport
             Case FormMain.PageSubType.InstanceExport
                 If FrmInstanceExport Is Nothing Then FrmInstanceExport = New PageInstanceExport
                 If FrmInstanceExport Is Nothing Then FrmInstanceExport = New PageInstanceExport
                 Return FrmInstanceExport
                 Return FrmInstanceExport
+            Case FormMain.PageSubType.InstanceModify
+                If FrmDownloadModify Is Nothing Then FrmDownloadModify = New PageDownloadModify
+                Return FrmDownloadModify
             Case Else
             Case Else
                 Throw New Exception("未知的版本设置子页面种类:" & ID)
                 Throw New Exception("未知的版本设置子页面种类:" & ID)
         End Select
         End Select
@@ -60,6 +67,10 @@ Public Class PageInstanceLeft
     ''' </summary>
     ''' </summary>
     Public Sub PageChange(ID As FormMain.PageSubType)
     Public Sub PageChange(ID As FormMain.PageSubType)
         If PageID = ID Then Return
         If PageID = ID Then Return
+        If Instance IsNot Nothing AndAlso McInstanceLock.IsLocked(Instance) Then
+            Hint("该版本正在被修改,请稍后再切换页面!", HintType.Red)
+            Return
+        End If
         AniControlEnabled += 1
         AniControlEnabled += 1
         Try
         Try
             PageChangeRun(PageGet(ID))
             PageChangeRun(PageGet(ID))
@@ -98,13 +109,20 @@ Public Class PageInstanceLeft
         Refresh(FrmMain.PageCurrentSub)
         Refresh(FrmMain.PageCurrentSub)
     End Sub
     End Sub
     Public Sub Refresh(SubType As FormMain.PageSubType)
     Public Sub Refresh(SubType As FormMain.PageSubType)
+        If Instance IsNot Nothing AndAlso McInstanceLock.IsLocked(Instance) Then
+            Hint("该版本正在被修改,请稍后再刷新!", HintType.Red)
+            Return
+        End If
         Select Case SubType
         Select Case SubType
             Case FormMain.PageSubType.InstanceMod
             Case FormMain.PageSubType.InstanceMod
                 PageInstanceMod.Refresh()
                 PageInstanceMod.Refresh()
                 ItemMod.Checked = True
                 ItemMod.Checked = True
             Case FormMain.PageSubType.InstanceExport
             Case FormMain.PageSubType.InstanceExport
-                If FrmInstanceExport IsNot Nothing Then FrmInstanceExport.RefreshAll()
+                FrmInstanceExport?.RefreshAll()
                 ItemExport.Checked = True
                 ItemExport.Checked = True
+            Case FormMain.PageSubType.InstanceModify
+                FrmDownloadModify?.PageLoaderRestart()
+                ItemModify.Checked = True
         End Select
         End Select
     End Sub
     End Sub
 
 

+ 6 - 6
Plain Craft Launcher 2/Pages/PageInstance/PageInstanceMod.xaml.vb

@@ -284,7 +284,7 @@ Public Class PageInstanceMod
     ''' 返回输入的文件是否为一个 Mod 文件,仅用于判断拖拽行为。
     ''' 返回输入的文件是否为一个 Mod 文件,仅用于判断拖拽行为。
     ''' </summary>
     ''' </summary>
     Public Shared Function InstallMods(FilePathList As IEnumerable(Of String)) As Boolean
     Public Shared Function InstallMods(FilePathList As IEnumerable(Of String)) As Boolean
-        Dim Extension As String = FilePathList.First.AfterLast(".").Lower
+        Dim Extension As String = PathUtils.GetExtension(FilePathList.First)
         '检查文件扩展名
         '检查文件扩展名
         If Not {"jar", "litemod", "disabled", "old"}.Any(Function(t) t = Extension) Then Return False
         If Not {"jar", "litemod", "disabled", "old"}.Any(Function(t) t = Extension) Then Return False
         Logger.Info("文件为 jar/litemod 格式,尝试作为 Mod 安装")
         Logger.Info("文件为 jar/litemod 格式,尝试作为 Mod 安装")
@@ -307,15 +307,15 @@ Public Class PageInstanceMod
 Install:
 Install:
             Try
             Try
                 For Each ModFile In FilePathList
                 For Each ModFile In FilePathList
-                    Dim NewFileName = PathUtils.GetLastPart(ModFile).Replace(".disabled", "").Replace(".old", "")
+                    Dim NewFileName = (New LocalResourceFile(ModFile)).EnabledName
                     If Not NewFileName.Contains(".") Then NewFileName += ".jar" '#4227
                     If Not NewFileName.Contains(".") Then NewFileName += ".jar" '#4227
                     NewFileName = Instance.PathIndie & "mods\" & NewFileName
                     NewFileName = Instance.PathIndie & "mods\" & NewFileName
-                    If ModFile = NewFileName Then Continue For '防止安装相同文件导致文件被删除
+                    If PathUtils.IsSame(ModFile, NewFileName) Then Continue For '防止安装相同文件导致文件被删除
                     FileUtils.Delete(NewFileName, True)
                     FileUtils.Delete(NewFileName, True)
                     FileUtils.Copy(ModFile, NewFileName)
                     FileUtils.Copy(ModFile, NewFileName)
                 Next
                 Next
                 If FilePathList.IsSingle Then
                 If FilePathList.IsSingle Then
-                    Hint($"已安装 {PathUtils.GetLastPart(FilePathList.First).Replace(".disabled", "").Replace(".old", "")}!", HintType.Green)
+                    Hint($"已安装 {(New LocalResourceFile(FilePathList.First)).EnabledName}!", HintType.Green)
                 Else
                 Else
                     Hint($"已安装 {FilePathList.Count} 个 Mod!", HintType.Green)
                     Hint($"已安装 {FilePathList.Count} 个 Mod!", HintType.Green)
                 End If
                 End If
@@ -553,8 +553,8 @@ Install:
                 Dim File As ResourceVersion = Entry.UpdateFile
                 Dim File As ResourceVersion = Entry.UpdateFile
                 If Not File.DownloadAvailable Then Continue For
                 If Not File.DownloadAvailable Then Continue For
                 '确认更新后的文件名
                 '确认更新后的文件名
-                Dim CurrentReplaceName = Entry.ProjectVersion.FileName.Replace(".jar", "").Replace(".old", "").Replace(".disabled", "")
-                Dim NewestReplaceName = Entry.UpdateFile.FileName.Replace(".jar", "").Replace(".old", "").Replace(".disabled", "")
+                Dim CurrentReplaceName = PathUtils.GetFileNameWithoutExtension(Entry.ProjectVersion.FileName)
+                Dim NewestReplaceName = PathUtils.GetFileNameWithoutExtension(Entry.UpdateFile.FileName)
                 Dim CurrentSegs = CurrentReplaceName.Split("-"c).ToList()
                 Dim CurrentSegs = CurrentReplaceName.Split("-"c).ToList()
                 Dim NewestSegs = NewestReplaceName.Split("-"c).ToList()
                 Dim NewestSegs = NewestReplaceName.Split("-"c).ToList()
                 Dim Shortened As Boolean = False
                 Dim Shortened As Boolean = False

+ 15 - 37
Plain Craft Launcher 2/Pages/PageInstance/PageInstanceOverall.xaml.vb

@@ -104,51 +104,21 @@ Public Class PageInstanceOverall
     '重命名版本
     '重命名版本
     Private Sub BtnDisplayRename_Click(sender As Object, e As EventArgs) Handles BtnDisplayRename.Click
     Private Sub BtnDisplayRename_Click(sender As Object, e As EventArgs) Handles BtnDisplayRename.Click
         Try
         Try
+            If McInstanceLock.IsLocked(PageInstanceLeft.Instance) Then
+                Hint("该版本正在被修改,请稍后再重命名!", HintType.Red)
+                Return
+            End If
             '确认输入的新名称
             '确认输入的新名称
             Dim OldName As String = PageInstanceLeft.Instance.Name
             Dim OldName As String = PageInstanceLeft.Instance.Name
-            Dim OldPath As String = PageInstanceLeft.Instance.PathVersion
             '修改此部分的同时修改快速安装的版本名检测*
             '修改此部分的同时修改快速安装的版本名检测*
             Dim NewName As String = MyMsgBoxInput("重命名版本", "", OldName, New ObjectModel.Collection(Of Validate) From {
             Dim NewName As String = MyMsgBoxInput("重命名版本", "", OldName, New ObjectModel.Collection(Of Validate) From {
                 New ValidateFolderName(McFolderSelected & "versions", IgnoreList:=New List(Of String) From {OldName})
                 New ValidateFolderName(McFolderSelected & "versions", IgnoreList:=New List(Of String) From {OldName})
             })
             })
             If String.IsNullOrWhiteSpace(NewName) Then Return
             If String.IsNullOrWhiteSpace(NewName) Then Return
-            Dim NewPath As String = McFolderSelected & "versions\" & NewName & "\"
-            '重新读取版本 JSON 信息,避免 JsonObject 中已被合并的项被重新存储
-            Dim JsonObject As JObject
-            Dim OldJsonPath As String = PageInstanceLeft.Instance.GetJsonPath()
-            Try
-                JsonObject = FileUtils.ReadAsJson(OldJsonPath)
-            Catch ex As Exception
-                Logger.Warn(ex, "在重命名读取 JSON 时失败")
-                JsonObject = PageInstanceLeft.Instance.JsonObject
-            End Try
-            '重命名主文件夹
-            DirectoryUtils.Move(OldPath, NewPath)
-            '清理 ini 缓存
-            PageInstanceLeft.Instance.ResetSettingsCache()
-            '重命名 jar 文件与 natives 文件夹
-            '不能进行遍历重命名,否则在版本名很短的时候容易误伤其他文件(#6443)
-            If DirectoryUtils.Exists($"{NewPath}{OldName}-natives") Then DirectoryUtils.Move($"{NewPath}{OldName}-natives", $"{NewPath}{NewName}-natives")
-            If FileUtils.Exists($"{NewPath}{OldName}.jar") Then FileUtils.Move($"{NewPath}{OldName}.jar", $"{NewPath}{NewName}.jar")
-            '替换版本设置文件中的路径
-            If FileUtils.Exists(NewPath & "PCL\Setup.ini") Then
-                FileUtils.Write(NewPath & "PCL\Setup.ini", FileUtils.ReadAsString(NewPath & "PCL\Setup.ini").Replace(OldPath, NewPath))
-            End If
-            '更改已选中的版本
-            If ReadIni(McFolderSelected & "PCL.ini", "Version") = OldName Then
-                WriteIni(McFolderSelected & "PCL.ini", "Version", NewName)
-            End If
-            '更新版本 Json
-            Try
-                JsonObject("id") = NewName
-                FileUtils.Delete(NewPath & PathUtils.GetLastPart(OldJsonPath))
-                FileUtils.Write(NewPath & NewName & ".json", JsonObject.ToString)
-            Catch ex As Exception
-                Logger.Warn(ex, "重命名版本 json 失败")
-            End Try
+            Dim NewInstance = PageInstanceLeft.Instance.Rename(NewName)
             '刷新与提示
             '刷新与提示
             Hint("重命名成功!", HintType.Green)
             Hint("重命名成功!", HintType.Green)
-            PageInstanceLeft.Instance = New McInstance(NewName).Load()
+            PageInstanceLeft.Instance = NewInstance
             If Not IsNothing(McInstanceSelected) AndAlso McInstanceSelected.Equals(PageInstanceLeft.Instance) Then WriteIni(McFolderSelected & "PCL.ini", "Version", NewName)
             If Not IsNothing(McInstanceSelected) AndAlso McInstanceSelected.Equals(PageInstanceLeft.Instance) Then WriteIni(McFolderSelected & "PCL.ini", "Version", NewName)
             Reload()
             Reload()
             LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
             LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
@@ -267,6 +237,10 @@ Public Class PageInstanceOverall
     '补全文件
     '补全文件
     Private Sub BtnManageCheck_Click(sender As Object, e As EventArgs) Handles BtnManageCheck.Click
     Private Sub BtnManageCheck_Click(sender As Object, e As EventArgs) Handles BtnManageCheck.Click
         Try
         Try
+            If McInstanceLock.IsLocked(PageInstanceLeft.Instance) Then
+                Hint("该版本正在被修改,请稍后再补全文件!", HintType.Red)
+                Return
+            End If
             '忽略文件检查提示
             '忽略文件检查提示
             If ShouldIgnoreFileCheck(PageInstanceLeft.Instance) Then
             If ShouldIgnoreFileCheck(PageInstanceLeft.Instance) Then
                 Hint("请先关闭 [版本设置 → 设置 → 高级启动选项 → 关闭文件校验],然后再尝试补全文件!", HintType.Blue)
                 Hint("请先关闭 [版本设置 → 设置 → 高级启动选项 → 关闭文件校验],然后再尝试补全文件!", HintType.Blue)
@@ -304,8 +278,12 @@ Public Class PageInstanceOverall
     '修改此代码时,同时修改 PageSelectRight 中的代码
     '修改此代码时,同时修改 PageSelectRight 中的代码
     Private Sub BtnManageDelete_Click(sender As Object, e As EventArgs) Handles BtnManageDelete.Click
     Private Sub BtnManageDelete_Click(sender As Object, e As EventArgs) Handles BtnManageDelete.Click
         Try
         Try
+            If McInstanceLock.IsLocked(PageInstanceLeft.Instance) Then
+                Hint("该版本正在被修改,请稍后再删除!", HintType.Red)
+                Return
+            End If
             Dim IsShiftPressed As Boolean = My.Computer.Keyboard.ShiftKeyDown
             Dim IsShiftPressed As Boolean = My.Computer.Keyboard.ShiftKeyDown
-            Dim IsHintIndie As Boolean = PageInstanceLeft.Instance.State <> McInstanceState.Error AndAlso PageInstanceLeft.Instance.PathIndie <> McFolderSelected
+            Dim IsHintIndie As Boolean = PageInstanceLeft.Instance.State <> McInstance.McInstanceState.Error AndAlso PageInstanceLeft.Instance.PathIndie <> McFolderSelected
             Dim SavesFolder = DirectoryUtils.GetInfo(PageInstanceLeft.Instance.PathIndie & "saves\")
             Dim SavesFolder = DirectoryUtils.GetInfo(PageInstanceLeft.Instance.PathIndie & "saves\")
             Dim SaveEntries = If(IsHintIndie AndAlso SavesFolder.Exists, SavesFolder.EnumerateDirectories.OrderByDescending(Function(Save) Save.LastWriteTime).Select(Function(Save) $"{Save.Name}(上次修改:{StringUtils.FormatTimeSpan(Save.LastWriteTime - Date.Now, False)})").ToList, New List(Of String))
             Dim SaveEntries = If(IsHintIndie AndAlso SavesFolder.Exists, SavesFolder.EnumerateDirectories.OrderByDescending(Function(Save) Save.LastWriteTime).Select(Function(Save) $"{Save.Name}(上次修改:{StringUtils.FormatTimeSpan(Save.LastWriteTime - Date.Now, False)})").ToList, New List(Of String))
             Select Case MyMsgBox($"你确定要{If(IsShiftPressed, "永久", "")}删除版本 {PageInstanceLeft.Instance.Name} 吗?" &
             Select Case MyMsgBox($"你确定要{If(IsShiftPressed, "永久", "")}删除版本 {PageInstanceLeft.Instance.Name} 吗?" &

+ 1 - 1
Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSetup.xaml.vb

@@ -499,7 +499,7 @@ PreFin:
                 ComboArgumentJavaSelect.Items.Add(JavaItem)
                 ComboArgumentJavaSelect.Items.Add(JavaItem)
                 Dim Buttons As New List(Of MyIconButton)
                 Dim Buttons As New List(Of MyIconButton)
                 '从列表中移除按钮
                 '从列表中移除按钮
-                Dim IsOfficial As Boolean = JavaEntry.Folder.StartsWithF($"{Paths.AppData}.minecraft\runtime\")
+                Dim IsOfficial As Boolean = PathUtils.IsParentOf($"{Paths.AppData}.minecraft\runtime\", JavaEntry.Folder)
                 Dim DeleteButton As New MyIconButton With {.Logo = Logo.IconButtonStop, .Height = 24, .Width = 24}
                 Dim DeleteButton As New MyIconButton With {.Logo = Logo.IconButtonStop, .Height = 24, .Width = 24}
                 DeleteButton.IsEnabled = Not IsOfficial
                 DeleteButton.IsEnabled = Not IsOfficial
                 DeleteButton.ToolTip = If(DeleteButton.IsEnabled, "从列表中移除", "无法移除官方 Java")
                 DeleteButton.ToolTip = If(DeleteButton.IsEnabled, "从列表中移除", "无法移除官方 Java")

+ 19 - 3
Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchLeft.xaml.vb

@@ -65,7 +65,7 @@ Public Class PageLaunchLeft
             '确认 Minecraft 版本存在
             '确认 Minecraft 版本存在
             Dim Selection As String = ReadIni(McFolderSelected & "PCL.ini", "Version")
             Dim Selection As String = ReadIni(McFolderSelected & "PCL.ini", "Version")
             Dim Instance As McInstance = If(Selection = "", Nothing, New McInstance(Selection))
             Dim Instance As McInstance = If(Selection = "", Nothing, New McInstance(Selection))
-            If Instance Is Nothing OrElse Not Instance.PathVersion.StartsWithF(McFolderSelected) OrElse Not Instance.Check() Then
+            If Instance Is Nothing OrElse Not PathUtils.IsParentOf(McFolderSelected, Instance.PathVersion) OrElse Not Instance.Check() Then
                 '无效的版本
                 '无效的版本
                 Logger.Info($"当前选择的 Minecraft 版本无效:{If(Instance Is Nothing, "null", Instance.PathVersion)}", If(IsNothing(Instance), LogBehavior.None, LogBehavior.ToastIfDebug))
                 Logger.Info($"当前选择的 Minecraft 版本无效:{If(Instance Is Nothing, "null", Instance.PathVersion)}", If(IsNothing(Instance), LogBehavior.None, LogBehavior.ToastIfDebug))
                 If Not McInstanceListLoader.State = LoadState.Finished Then LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\", WaitForExit:=True)
                 If Not McInstanceListLoader.State = LoadState.Finished Then LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\", WaitForExit:=True)
@@ -691,12 +691,28 @@ ExitRefresh:
         End If
         End If
     End Sub
     End Sub
     '取消按钮
     '取消按钮
-    Private Sub BtnCancel_Click() Handles BtnCancel.Click
-        McLaunchCancel()
+    Public Sub BtnCancel_Click() Handles BtnCancel.Click
+        If McLaunchLoaderReal IsNot Nothing Then
+            McLaunchLoaderReal.Cancel()
+            McLaunchLog("已取消启动")
+            Try
+                If McLaunchWatcher IsNot Nothing Then
+                    McLaunchWatcher.Kill()
+                ElseIf McLaunchProcess IsNot Nothing Then
+                    If Not McLaunchProcess.HasExited Then McLaunchProcess.Kill()
+                End If
+            Catch ex As Exception
+                Logger.Error(ex, "取消启动结束进程失败", LogBehavior.Toast)
+            End Try
+        End If
     End Sub
     End Sub
     '版本设置按钮
     '版本设置按钮
     Private Sub BtnMore_Click() Handles BtnMore.Click
     Private Sub BtnMore_Click() Handles BtnMore.Click
         If McLaunchLoader.State = LoadState.Loading Then Return
         If McLaunchLoader.State = LoadState.Loading Then Return
+        If McInstanceLock.IsLocked(McInstanceSelected) Then
+            Hint("该版本正在被修改,暂时无法进入版本设置!", HintType.Red)
+            Return
+        End If
         McInstanceSelected.Load()
         McInstanceSelected.Load()
         PageInstanceLeft.Instance = McInstanceSelected
         PageInstanceLeft.Instance = McInstanceSelected
         FrmMain.PageChange(FormMain.PageType.InstanceSetup, 0)
         FrmMain.PageChange(FormMain.PageType.InstanceSetup, 0)

+ 5 - 2
Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml

@@ -3,6 +3,7 @@
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
     xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
     xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
+    xmlns:behaviors="clr-namespace:PCL.Controls.Behaviors"
     xmlns:local="clr-namespace:PCL" PanScroll="PanBack">
     xmlns:local="clr-namespace:PCL" PanScroll="PanBack">
     <local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" x:Name="PanBack">
     <local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" x:Name="PanBack">
         <StackPanel Name="PanMain" Margin="25,25,25,10" Grid.IsSharedSizeScope="True">
         <StackPanel Name="PanMain" Margin="25,25,25,10" Grid.IsSharedSizeScope="True">
@@ -11,8 +12,10 @@
             <StackPanel x:Name="PanCustom">
             <StackPanel x:Name="PanCustom">
             </StackPanel>
             </StackPanel>
             <local:MyCard Margin="0,0,0,15" Title="启动日志" x:Name="PanLog">
             <local:MyCard Margin="0,0,0,15" Title="启动日志" x:Name="PanLog">
-                <TextBlock x:Name="LabLog" Margin="20,38,20,18" TextTrimming="None" TextWrapping="Wrap" />
+                <local:MyTextBox x:Name="LabLog" Margin="15,34,15,13" Padding="0,4" IsReadOnly="True" IsUndoEnabled="False"
+                         AcceptsReturn="True" TextWrapping="Wrap" BorderThickness="0" HasBackground="False"
+                         VerticalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Disabled" />
             </local:MyCard>
             </local:MyCard>
         </StackPanel>
         </StackPanel>
     </local:MyScrollViewer>
     </local:MyScrollViewer>
-</local:MyPageRight>
+</local:MyPageRight>

+ 22 - 7
Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.vb

@@ -101,19 +101,34 @@ Public Class PageLaunchRight
                         Url = "https://pcl.wyc-w.top/index.xaml"
                         Url = "https://pcl.wyc-w.top/index.xaml"
                     Case 15
                     Case 15
                         Logger.Info("主页预设:Minecraft 芝士站")
                         Logger.Info("主页预设:Minecraft 芝士站")
-                        Url = "https://www.xxag.top/mkss"
+                        Url = "https://mkss-services.awaeric.xyz"
                     Case 16
                     Case 16
                         Logger.Info("主页预设:整合包推荐引擎")
                         Logger.Info("主页预设:整合包推荐引擎")
                         Url = "https://qawsedrftgyhujiko.fun/pcl2/Custom.xaml"
                         Url = "https://qawsedrftgyhujiko.fun/pcl2/Custom.xaml"
                     Case 17
                     Case 17
-                        Logger.Info("Bangumi 番剧主页")
-                        Url = "https://bangumi.p.kaphia.qzz.io"
+                        Logger.Info("主页预设:Bangumi 番剧主页")
+                        Url = "https://bangumi.p.kaphia.top"
                     Case 18
                     Case 18
-                        Logger.Info("Bilibili 热门")
-                        Url = "https://bilibili.p.kaphia.qzz.io"
+                        Logger.Info("主页预设:Bilibili 热门")
+                        Url = "https://bilibili.p.kaphia.top"
                     Case 19
                     Case 19
-                        Logger.Info("Music 云音乐热门")
-                        Url = "https://cloudmusic.p.kaphia.qzz.io"
+                        Logger.Info("主页预设:Music 云音乐热门")
+                        Url = "https://cloudmusic.p.kaphia.top"
+                    Case 20
+                        Logger.Info("主页预设:模组推荐")
+                        Url = "https://g-fish.dpdns.org/download/ModPage.xaml"
+                    Case 21
+                        Logger.Info("主页预设:每日游戏推荐")
+                        Url = "https://pcl.xn--9iq209asnop94b.top/download/GamePage.xaml"
+                    Case 22
+                        Logger.Info("主页预设:Wallpaper 每日壁纸")
+                        Url = "https://wallpaper.p.kaphia.top"
+                    Case 23
+                        Logger.Info("主页预设:Weather 天气预报")
+                        Url = "https://weather-p.kaphia.top/{variable:PCLDailyWeatherPage:choose}.xaml"
+                    Case 24
+                        Logger.Info("主页预设:新一代狗屁王大杂烩主页")
+                        Url = "https://end.g-fish.dpdns.org/Custom.xaml"
                 End Select
                 End Select
         End Select
         End Select
         '联网下载
         '联网下载

+ 1 - 1
Plain Craft Launcher 2/Pages/PageLaunch/PageLoginLegacy.xaml.vb

@@ -45,7 +45,7 @@ Public Class PageLoginLegacy
         If LoginData.UserName.Trim = "" Then Return "玩家名不能为空!"
         If LoginData.UserName.Trim = "" Then Return "玩家名不能为空!"
         If LoginData.UserName.Contains("""") Then Return "玩家名不能包含英文引号!"
         If LoginData.UserName.Contains("""") Then Return "玩家名不能包含英文引号!"
         If McInstanceSelected IsNot Nothing AndAlso
         If McInstanceSelected IsNot Nothing AndAlso
-           McInstanceSelected.Version.Vaild AndAlso McInstanceSelected.Version.Vanilla >= New Version(20, 0, 3) AndAlso
+           McInstanceSelected.Version.Valid AndAlso McInstanceSelected.Version.Vanilla >= New Version(20, 0, 3) AndAlso
            LoginData.UserName.Trim.Length > 16 Then
            LoginData.UserName.Trim.Length > 16 Then
             Return "自 1.20.3 起,玩家名至多只能包含 16 个字符!"
             Return "自 1.20.3 起,玩家名至多只能包含 16 个字符!"
         End If
         End If

+ 4 - 2
Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml.vb

@@ -358,8 +358,9 @@ Public Class PageLinkMain
     '复制邀请码
     '复制邀请码
     Private Sub Copy() Handles BtnFinishCopy.Click
     Private Sub Copy() Handles BtnFinishCopy.Click
         Dim CodeText As String = $"在 PCL 启动器中输入邀请码【{GetInviteCode()}】,即可加入联机房间!"
         Dim CodeText As String = $"在 PCL 启动器中输入邀请码【{GetInviteCode()}】,即可加入联机房间!"
-        ClipboardSet(CodeText, SuccessHint:="已复制邀请码!")
+        ClipboardSet(CodeText, False)
         Settings.Set("LinkLastAutoJoinInviteCode", CodeText)
         Settings.Set("LinkLastAutoJoinInviteCode", CodeText)
+        Hint("已复制邀请码!", HintType.Green)
     End Sub
     End Sub
     Private Function GetInviteCode() As String
     Private Function GetInviteCode() As String
         Return $"{NetworkName}-{NetworkSecret}-{INVITE_CODE_VERSION.ToString.PadLeft(2, "0"c)}{ _
         Return $"{NetworkName}-{NetworkSecret}-{INVITE_CODE_VERSION.ToString.PadLeft(2, "0"c)}{ _
@@ -368,7 +369,8 @@ Public Class PageLinkMain
 
 
     '复制 IP
     '复制 IP
     Private Sub BtnFinishIp_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles BtnFinishIp.MouseLeftButtonUp
     Private Sub BtnFinishIp_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles BtnFinishIp.MouseLeftButtonUp
-        ClipboardSet(ClientAddress, SuccessHint:="已复制服务器地址!")
+        ClipboardSet(ClientAddress, False)
+        Hint("已复制服务器地址!", HintType.Green)
     End Sub
     End Sub
 
 
 #End Region
 #End Region

+ 585 - 333
Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml

@@ -26,7 +26,7 @@
                             <EllipseGeometry Center="17 17" RadiusX="17" RadiusY="17" />
                             <EllipseGeometry Center="17 17" RadiusX="17" RadiusY="17" />
                         </Image.Clip>
                         </Image.Clip>
                     </Image>
                     </Image>
-                    <Image Source="/Plain Craft Launcher 2;component/Images/Heads/HerobrineXia.png" Grid.Row="1" Margin="3">
+                    <Image Source="/Plain Craft Launcher 2;component/Images/Heads/HerobrineXia.png" Grid.Row="1" Margin="3">
                         <Image.Clip>
                         <Image.Clip>
                             <EllipseGeometry Center="17 17" RadiusX="17" RadiusY="17" />
                             <EllipseGeometry Center="17 17" RadiusX="17" RadiusY="17" />
                         </Image.Clip>
                         </Image.Clip>
@@ -131,98 +131,64 @@
             <local:MyCard Margin="0,0,0,15" Title="赞助者">
             <local:MyCard Margin="0,0,0,15" Title="赞助者">
                 <StackPanel Margin="25,40,19,20">
                 <StackPanel Margin="25,40,19,20">
                     <TextBlock HorizontalAlignment="Left" Text="以下是近期的高级赞助者们!感谢大家对 PCL 的支持!!!" Margin="0,0,25,5.5" TextTrimming="None" TextWrapping="WrapWithOverflow" />
                     <TextBlock HorizontalAlignment="Left" Text="以下是近期的高级赞助者们!感谢大家对 PCL 的支持!!!" Margin="0,0,25,5.5" TextTrimming="None" TextWrapping="WrapWithOverflow" />
-                    <WrapPanel Margin="0,0,0,10">
-                        <TextBlock Margin="0,1.5,6,0" Text="L" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Z" />
-                        <TextBlock Margin="0,1.5,6,0" Text="N" />
+                    <WrapPanel Margin="0,0,0,10">
                        <TextBlock Margin="0,1.5,6,0" Text="." />
+                        <TextBlock Margin="0,1.5,6,0" Text="-" />
+                        <TextBlock Margin="0,1.5,6,0" Text="J" />
+                        <TextBlock Margin="0,1.5,6,0" Text="tt" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Ω" />
+                        <TextBlock Margin="0,1.5,6,0" Text="D" />
                         <TextBlock Margin="0,1.5,6,0" Text="P." />
                         <TextBlock Margin="0,1.5,6,0" Text="P." />
-                        <TextBlock Margin="0,1.5,6,0" Text="...:" />
-                        <TextBlock Margin="0,1.5,6,0" Text="妖" />
+                        <TextBlock Margin="0,1.5,6,0" Text="hj" />
                         <TextBlock Margin="0,1.5,6,0" Text="FL" />
                         <TextBlock Margin="0,1.5,6,0" Text="FL" />
-                        <TextBlock Margin="0,1.5,6,0" Text="萧" />
-                        <TextBlock Margin="0,1.5,6,0" Text="乔" />
-                        <TextBlock Margin="0,1.5,6,0" Text="cc" />
-                        <TextBlock Margin="0,1.5,6,0" Text="!" />
-                        <TextBlock Margin="0,1.5,6,0" Text="一" />
-                        <TextBlock Margin="0,1.5,6,0" Text="韆" />
-                        <TextBlock Margin="0,1.5,6,0" Text="莫" />
-                        <TextBlock Margin="0,1.5,6,0" Text="无" />
-                        <TextBlock Margin="0,1.5,6,0" Text="澍" />
-                        <TextBlock Margin="0,1.5,6,0" Text="ttt" />
-                        <TextBlock Margin="0,1.5,6,0" Text="88" />
-                        <TextBlock Margin="0,1.5,6,0" Text="33" />
-                        <TextBlock Margin="0,1.5,6,0" Text="kk" />
-                        <TextBlock Margin="0,1.5,6,0" Text="jjb" />
+                        <TextBlock Margin="0,1.5,6,0" Text="遇" />
+                        <TextBlock Margin="0,1.5,6,0" Text="邵" />
+                        <TextBlock Margin="0,1.5,6,0" Text="里" />
+                        <TextBlock Margin="0,1.5,6,0" Text="漓" />
+                        <TextBlock Margin="0,1.5,6,0" Text="颜" />
+                        <TextBlock Margin="0,1.5,6,0" Text="初" />
+                        <TextBlock Margin="0,1.5,6,0" Text="枷" />
+                        <TextBlock Margin="0,1.5,6,0" Text="。" />
+                        <TextBlock Margin="0,1.5,6,0" Text="乖" />
+                        <TextBlock Margin="0,1.5,6,0" Text="吕" />
+                        <TextBlock Margin="0,1.5,6,0" Text="宇" />
+                        <TextBlock Margin="0,1.5,6,0" Text="FF" />
+                        <TextBlock Margin="0,1.5,6,0" Text="金" />
+                        <TextBlock Margin="0,1.5,6,0" Text="夏" />
+                        <TextBlock Margin="0,1.5,6,0" Text="鸿" />
+                        <TextBlock Margin="0,1.5,6,0" Text="ツ" />
+                        <TextBlock Margin="0,1.5,6,0" Text="SF" />
+                        <TextBlock Margin="0,1.5,6,0" Text="kll" />
+                        <TextBlock Margin="0,1.5,6,0" Text="02" />
+                        <TextBlock Margin="0,1.5,6,0" Text="YY" />
                         <TextBlock Margin="0,1.5,6,0" Text="狮." />
                         <TextBlock Margin="0,1.5,6,0" Text="狮." />
                         <TextBlock Margin="0,1.5,6,0" Text="+1" />
                         <TextBlock Margin="0,1.5,6,0" Text="+1" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Iirs" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Lio" />
-                        <TextBlock Margin="0,1.5,6,0" Text="lrw" />
-                        <TextBlock Margin="0,1.5,6,0" Text="LLL" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Sht" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Nul" />
-                        <TextBlock Margin="0,1.5,6,0" Text="250" />
+                        <TextBlock Margin="0,1.5,6,0" Text="lne" />
+                        <TextBlock Margin="0,1.5,6,0" Text="……" />
+                        <TextBlock Margin="0,1.5,6,0" Text="shy" />
+                        <TextBlock Margin="0,1.5,6,0" Text="asd" />
+                        <TextBlock Margin="0,1.5,6,0" Text="123" />
                         <TextBlock Margin="0,1.5,6,0" Text="Life" />
                         <TextBlock Margin="0,1.5,6,0" Text="Life" />
-                        <TextBlock Margin="0,1.5,6,0" Text="kile" />
-                        <TextBlock Margin="0,1.5,6,0" Text="pbc" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Aya" />
-                        <TextBlock Margin="0,1.5,6,0" Text="mjp" />
+                        <TextBlock Margin="0,1.5,6,0" Text="kkll" />
+                        <TextBlock Margin="0,1.5,6,0" Text="777" />
+                        <TextBlock Margin="0,1.5,6,0" Text="sun" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Ray" />
+                        <TextBlock Margin="0,1.5,6,0" Text="lost" />
+                        <TextBlock Margin="0,1.5,6,0" Text="A仔" />
+                        <TextBlock Margin="0,1.5,6,0" Text="tide" />
+                        <TextBlock Margin="0,1.5,6,0" Text="afaf" />
+                        <TextBlock Margin="0,1.5,6,0" Text="pen" />
+                        <TextBlock Margin="0,1.5,6,0" Text="lulu" />
+                        <TextBlock Margin="0,1.5,6,0" Text="xyty" />
                         <TextBlock Margin="0,1.5,6,0" Text="azzt" />
                         <TextBlock Margin="0,1.5,6,0" Text="azzt" />
                         <TextBlock Margin="0,1.5,6,0" Text="小小" />
                         <TextBlock Margin="0,1.5,6,0" Text="小小" />
-                        <TextBlock Margin="0,1.5,6,0" Text="冒子" />
-                        <TextBlock Margin="0,1.5,6,0" Text="王道" />
-                        <TextBlock Margin="0,1.5,6,0" Text="鸡人" />
-                        <TextBlock Margin="0,1.5,6,0" Text="喵根" />
+                        <TextBlock Margin="0,1.5,6,0" Text="懒两" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Den" />
+                        <TextBlock Margin="0,1.5,6,0" Text="莫语" />
+                        <TextBlock Margin="0,1.5,6,0" Text="鬼铁" />
+                        <TextBlock Margin="0,1.5,6,0" Text="暗生" />
                         <TextBlock Margin="0,1.5,6,0" Text="折花" />
                         <TextBlock Margin="0,1.5,6,0" Text="折花" />
-                        <TextBlock Margin="0,1.5,6,0" Text="归墟" />
                         <TextBlock Margin="0,1.5,6,0" Text="墨渊" />
                         <TextBlock Margin="0,1.5,6,0" Text="墨渊" />
-                        <TextBlock Margin="0,1.5,6,0" Text="键来" />
                         <TextBlock Margin="0,1.5,6,0" Text="睚眦" />
                         <TextBlock Margin="0,1.5,6,0" Text="睚眦" />
-                        <TextBlock Margin="0,1.5,6,0" Text="而后" />
-                        <TextBlock Margin="0,1.5,6,0" Text="南安" />
-                        <TextBlock Margin="0,1.5,6,0" Text="曰久" />
-                        <TextBlock Margin="0,1.5,6,0" Text="小翼" />
-                        <TextBlock Margin="0,1.5,6,0" Text="鲤逐" />
-                        <TextBlock Margin="0,1.5,6,0" Text="陌天" />
-                        <TextBlock Margin="0,1.5,6,0" Text="eqw" />
-                        <TextBlock Margin="0,1.5,6,0" Text="缘泠" />
-                        <TextBlock Margin="0,1.5,6,0" Text="烨落" />
-                        <TextBlock Margin="0,1.5,6,0" Text="安安" />
-                        <TextBlock Margin="0,1.5,6,0" Text="寒叙" />
-                        <TextBlock Margin="0,1.5,6,0" Text="诶嘿" />
-                        <TextBlock Margin="0,1.5,6,0" Text="月月" />
-                        <TextBlock Margin="0,1.5,6,0" Text="凌迟" />
-                        <TextBlock Margin="0,1.5,6,0" Text="泰吉" />
-                        <TextBlock Margin="0,1.5,6,0" Text="楚渡" />
-                        <TextBlock Margin="0,1.5,6,0" Text="将夜" />
-                        <TextBlock Margin="0,1.5,6,0" Text="滴雨" />
-                        <TextBlock Margin="0,1.5,6,0" Text="无萤" />
-                        <TextBlock Margin="0,1.5,6,0" Text="秋水" />
-                        <TextBlock Margin="0,1.5,6,0" Text="团子" />
-                        <TextBlock Margin="0,1.5,6,0" Text="川儿" />
-                        <TextBlock Margin="0,1.5,6,0" Text="冥曌" />
-                        <TextBlock Margin="0,1.5,6,0" Text="红中" />
-                        <TextBlock Margin="0,1.5,6,0" Text="小云" />
-                        <TextBlock Margin="0,1.5,6,0" Text="诗人" />
-                        <TextBlock Margin="0,1.5,6,0" Text="狐狸" />
-                        <TextBlock Margin="0,1.5,6,0" Text="游阙" />
-                        <TextBlock Margin="0,1.5,6,0" Text="悠真" />
-                        <TextBlock Margin="0,1.5,6,0" Text="天雨" />
-                        <TextBlock Margin="0,1.5,6,0" Text="章凤" />
-                        <TextBlock Margin="0,1.5,6,0" Text="空空" />
-                        <TextBlock Margin="0,1.5,6,0" Text="去衣" />
-                        <TextBlock Margin="0,1.5,6,0" Text="老抽" />
-                        <TextBlock Margin="0,1.5,6,0" Text="七海" />
-                        <TextBlock Margin="0,1.5,6,0" Text="品茗" />
-                        <TextBlock Margin="0,1.5,6,0" Text="白桦" />
-                        <TextBlock Margin="0,1.5,6,0" Text="狐斋" />
-                        <TextBlock Margin="0,1.5,6,0" Text="猫神" />
-                        <TextBlock Margin="0,1.5,6,0" Text="乙总" />
-                        <TextBlock Margin="0,1.5,6,0" Text="屺野" />
-                        <TextBlock Margin="0,1.5,6,0" Text="极光" />
-                        <TextBlock Margin="0,1.5,6,0" Text="哈哈" />
-                        <TextBlock Margin="0,1.5,6,0" Text="小洛" />
-                        <TextBlock Margin="0,1.5,6,0" Text="璃辰" />
                         <TextBlock Margin="0,1.5,6,0" Text="茉莉" />
                         <TextBlock Margin="0,1.5,6,0" Text="茉莉" />
                         <TextBlock Margin="0,1.5,6,0" Text="旅人" />
                         <TextBlock Margin="0,1.5,6,0" Text="旅人" />
                         <TextBlock Margin="0,1.5,6,0" Text="航卡" />
                         <TextBlock Margin="0,1.5,6,0" Text="航卡" />
@@ -232,76 +198,166 @@
                         <TextBlock Margin="0,1.5,6,0" Text="风?" />
                         <TextBlock Margin="0,1.5,6,0" Text="风?" />
                         <TextBlock Margin="0,1.5,6,0" Text="瓜瓜" />
                         <TextBlock Margin="0,1.5,6,0" Text="瓜瓜" />
                         <TextBlock Margin="0,1.5,6,0" Text="阳光" />
                         <TextBlock Margin="0,1.5,6,0" Text="阳光" />
+                        <TextBlock Margin="0,1.5,6,0" Text="皓仔" />
                         <TextBlock Margin="0,1.5,6,0" Text="菜菜" />
                         <TextBlock Margin="0,1.5,6,0" Text="菜菜" />
                         <TextBlock Margin="0,1.5,6,0" Text="副连" />
                         <TextBlock Margin="0,1.5,6,0" Text="副连" />
                         <TextBlock Margin="0,1.5,6,0" Text="如舟" />
                         <TextBlock Margin="0,1.5,6,0" Text="如舟" />
+                        <TextBlock Margin="0,1.5,6,0" Text="暂用" />
+                        <TextBlock Margin="0,1.5,6,0" Text="幾希" />
+                        <TextBlock Margin="0,1.5,6,0" Text="子神" />
+                        <TextBlock Margin="0,1.5,6,0" Text="东川" />
+                        <TextBlock Margin="0,1.5,6,0" Text="溪漱" />
+                        <TextBlock Margin="0,1.5,6,0" Text="祖阡" />
+                        <TextBlock Margin="0,1.5,6,0" Text="kaiji" />
+                        <TextBlock Margin="0,1.5,6,0" Text="简喻" />
+                        <TextBlock Margin="0,1.5,6,0" Text="太弦" />
+                        <TextBlock Margin="0,1.5,6,0" Text="洛伊" />
+                        <TextBlock Margin="0,1.5,6,0" Text="凌」" />
+                        <TextBlock Margin="0,1.5,6,0" Text="随你" />
+                        <TextBlock Margin="0,1.5,6,0" Text="青暮" />
+                        <TextBlock Margin="0,1.5,6,0" Text="黯栊" />
+                        <TextBlock Margin="0,1.5,6,0" Text="EVIL" />
+                        <TextBlock Margin="0,1.5,6,0" Text="桃花" />
+                        <TextBlock Margin="0,1.5,6,0" Text="萧萧" />
+                        <TextBlock Margin="0,1.5,6,0" Text="天神" />
+                        <TextBlock Margin="0,1.5,6,0" Text="优鱼" />
+                        <TextBlock Margin="0,1.5,6,0" Text="草草" />
+                        <TextBlock Margin="0,1.5,6,0" Text="琉璃" />
+                        <TextBlock Margin="0,1.5,6,0" Text="蠱饁" />
+                        <TextBlock Margin="0,1.5,6,0" Text="沙鱼" />
+                        <TextBlock Margin="0,1.5,6,0" Text="圆周" />
+                        <TextBlock Margin="0,1.5,6,0" Text="余浅" />
+                        <TextBlock Margin="0,1.5,6,0" Text="咕咕" />
+                        <TextBlock Margin="0,1.5,6,0" Text="来信" />
+                        <TextBlock Margin="0,1.5,6,0" Text="叶沂" />
+                        <TextBlock Margin="0,1.5,6,0" Text="版本" />
+                        <TextBlock Margin="0,1.5,6,0" Text="小谢" />
+                        <TextBlock Margin="0,1.5,6,0" Text="画穹" />
+                        <TextBlock Margin="0,1.5,6,0" Text="混子" />
+                        <TextBlock Margin="0,1.5,6,0" Text="千门" />
+                        <TextBlock Margin="0,1.5,6,0" Text="嘉名" />
+                        <TextBlock Margin="0,1.5,6,0" Text="祺祺" />
+                        <TextBlock Margin="0,1.5,6,0" Text="一一" />
+                        <TextBlock Margin="0,1.5,6,0" Text="星逝" />
+                        <TextBlock Margin="0,1.5,6,0" Text="梦魔" />
+                        <TextBlock Margin="0,1.5,6,0" Text="桦学" />
+                        <TextBlock Margin="0,1.5,6,0" Text="您猜" />
+                        <TextBlock Margin="0,1.5,6,0" Text="MW" />
+                        <TextBlock Margin="0,1.5,6,0" Text="排骨" />
+                        <TextBlock Margin="0,1.5,6,0" Text="踏雾" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Zsol" />
+                        <TextBlock Margin="0,1.5,6,0" Text="景仪" />
+                        <TextBlock Margin="0,1.5,6,0" Text="可可" />
+                        <TextBlock Margin="0,1.5,6,0" Text="白夜" />
+                        <TextBlock Margin="0,1.5,6,0" Text="汤圆" />
+                        <TextBlock Margin="0,1.5,6,0" Text="星月" />
+                        <TextBlock Margin="0,1.5,6,0" Text="皓月" />
+                        <TextBlock Margin="0,1.5,6,0" Text="老建" />
+                        <TextBlock Margin="0,1.5,6,0" Text="阿琼" />
+                        <TextBlock Margin="0,1.5,6,0" Text="晓月" />
+                        <TextBlock Margin="0,1.5,6,0" Text="朝雨" />
+                        <TextBlock Margin="0,1.5,6,0" Text="迟雨" />
+                        <TextBlock Margin="0,1.5,6,0" Text="折纸" />
+                        <TextBlock Margin="0,1.5,6,0" Text="张某" />
+                        <TextBlock Margin="0,1.5,6,0" Text="残月" />
+                        <TextBlock Margin="0,1.5,6,0" Text="缘分" />
+                        <TextBlock Margin="0,1.5,6,0" Text="静态" />
+                        <TextBlock Margin="0,1.5,6,0" Text="铭熙" />
+                        <TextBlock Margin="0,1.5,6,0" Text="酒狐" />
+                        <TextBlock Margin="0,1.5,6,0" Text="誌異" />
+                        <TextBlock Margin="0,1.5,6,0" Text="灵喵" />
+                        <TextBlock Margin="0,1.5,6,0" Text="曦宇" />
+                        <TextBlock Margin="0,1.5,6,0" Text="木辛" />
+                        <TextBlock Margin="0,1.5,6,0" Text="轻梦" />
+                        <TextBlock Margin="0,1.5,6,0" Text="熊猫" />
+                        <TextBlock Margin="0,1.5,6,0" Text="鱼柳" />
+                        <TextBlock Margin="0,1.5,6,0" Text="小本" />
+                        <TextBlock Margin="0,1.5,6,0" Text="起媛" />
+                        <TextBlock Margin="0,1.5,6,0" Text="夏天" />
+                        <TextBlock Margin="0,1.5,6,0" Text="泪痕" />
+                        <TextBlock Margin="0,1.5,6,0" Text="南北" />
+                        <TextBlock Margin="0,1.5,6,0" Text="灰鱼" />
+                        <TextBlock Margin="0,1.5,6,0" Text="灰尘" />
+                        <TextBlock Margin="0,1.5,6,0" Text="骗铭" />
+                        <TextBlock Margin="0,1.5,6,0" Text="青轴" />
+                        <TextBlock Margin="0,1.5,6,0" Text="呈祥" />
+                        <TextBlock Margin="0,1.5,6,0" Text="猫玖" />
+                        <TextBlock Margin="0,1.5,6,0" Text="枕月" />
+                        <TextBlock Margin="0,1.5,6,0" Text="刘革" />
+                        <TextBlock Margin="0,1.5,6,0" Text="应星" />
+                        <TextBlock Margin="0,1.5,6,0" Text="没有" />
+                        <TextBlock Margin="0,1.5,6,0" Text="花园" />
+                        <TextBlock Margin="0,1.5,6,0" Text="华烬" />
+                        <TextBlock Margin="0,1.5,6,0" Text="啥猫" />
+                        <TextBlock Margin="0,1.5,6,0" Text="阿伯" />
+                        <TextBlock Margin="0,1.5,6,0" Text="妄想" />
+                        <TextBlock Margin="0,1.5,6,0" Text="树叶" />
+                        <TextBlock Margin="0,1.5,6,0" Text="拾玖" />
+                        <TextBlock Margin="0,1.5,6,0" Text="让人" />
+                        <TextBlock Margin="0,1.5,6,0" Text="晓光" />
+                        <TextBlock Margin="0,1.5,6,0" Text="懿尘" />
+                        <TextBlock Margin="0,1.5,6,0" Text="尾巴" />
+                        <TextBlock Margin="0,1.5,6,0" Text="枯骨" />
+                        <TextBlock Margin="0,1.5,6,0" Text="鹤升" />
+                        <TextBlock Margin="0,1.5,6,0" Text="遊泠" />
+                        <TextBlock Margin="0,1.5,6,0" Text="裕茗" />
+                        <TextBlock Margin="0,1.5,6,0" Text="沁园" />
+                        <TextBlock Margin="0,1.5,6,0" Text="尹伊" />
+                        <TextBlock Margin="0,1.5,6,0" Text="枫音" />
+                        <TextBlock Margin="0,1.5,6,0" Text="古色" />
+                        <TextBlock Margin="0,1.5,6,0" Text="傻鱼" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Eilte" />
                         <TextBlock Margin="0,1.5,6,0" Text="bow" />
                         <TextBlock Margin="0,1.5,6,0" Text="bow" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Byet" />
                         <TextBlock Margin="0,1.5,6,0" Text="Kian" />
                         <TextBlock Margin="0,1.5,6,0" Text="Kian" />
-                        <TextBlock Margin="0,1.5,6,0" Text="ZDD" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Um1" />
-                        <TextBlock Margin="0,1.5,6,0" Text="2514" />
-                        <TextBlock Margin="0,1.5,6,0" Text="4440" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Fu_jc" />
-                        <TextBlock Margin="0,1.5,6,0" Text="LEVA" />
-                        <TextBlock Margin="0,1.5,6,0" Text="ZNLL" />
-                        <TextBlock Margin="0,1.5,6,0" Text="joker" />
-                        <TextBlock Margin="0,1.5,6,0" Text="RXZY" />
-                        <TextBlock Margin="0,1.5,6,0" Text="白成y" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Rree" />
+                        <TextBlock Margin="0,1.5,6,0" Text="ZWL" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Qiee" />
+                        <TextBlock Margin="0,1.5,6,0" Text="XYM" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Leen" />
+                        <TextBlock Margin="0,1.5,6,0" Text="9527" />
+                        <TextBlock Margin="0,1.5,6,0" Text="1234" />
+                        <TextBlock Margin="0,1.5,6,0" Text=" 清空" />
+                        <TextBlock Margin="0,1.5,6,0" Text="7104" />
+                        <TextBlock Margin="0,1.5,6,0" Text="2333" />
+                        <TextBlock Margin="0,1.5,6,0" Text="5-DF" />
+                        <TextBlock Margin="0,1.5,6,0" Text="qqes" />
+                        <TextBlock Margin="0,1.5,6,0" Text="￴ 🧸" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Patxi" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Sage" />
+                        <TextBlock Margin="0,1.5,6,0" Text="R301" />
                         <TextBlock Margin="0,1.5,6,0" Text="mww" />
                         <TextBlock Margin="0,1.5,6,0" Text="mww" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Rofni" />
-                        <TextBlock Margin="0,1.5,6,0" Text="mzqc" />
-                        <TextBlock Margin="0,1.5,6,0" Text="jason" />
+                        <TextBlock Margin="0,1.5,6,0" Text="RXZY" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Siwill" />
+                        <TextBlock Margin="0,1.5,6,0" Text="saber" />
                         <TextBlock Margin="0,1.5,6,0" Text="youzi" />
                         <TextBlock Margin="0,1.5,6,0" Text="youzi" />
-                        <TextBlock Margin="0,1.5,6,0" Text="xxlzta" />
-                        <TextBlock Margin="0,1.5,6,0" Text="end_x" />
-                        <TextBlock Margin="0,1.5,6,0" Text="QWYJ" />
-                        <TextBlock Margin="0,1.5,6,0" Text="qwqw" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Angel" />
+                        <TextBlock Margin="0,1.5,6,0" Text="kassa" />
+                        <TextBlock Margin="0,1.5,6,0" Text="ssdsb" />
+                        <TextBlock Margin="0,1.5,6,0" Text="蓝K龙" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Ztdzz" />
+                        <TextBlock Margin="0,1.5,6,0" Text="xiboa" />
+                        <TextBlock Margin="0,1.5,6,0" Text="ZERO" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Whity" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Meda" />
+                        <TextBlock Margin="0,1.5,6,0" Text="pr1nk" />
+                        <TextBlock Margin="0,1.5,6,0" Text="BBUH" />
+                        <TextBlock Margin="0,1.5,6,0" Text="ALISA" />
                         <TextBlock Margin="0,1.5,6,0" Text="15041" />
                         <TextBlock Margin="0,1.5,6,0" Text="15041" />
-                        <TextBlock Margin="0,1.5,6,0" Text="晓空er" />
+                        <TextBlock Margin="0,1.5,6,0" Text="哀null" />
+                        <TextBlock Margin="0,1.5,6,0" Text="dhvec" />
+                        <TextBlock Margin="0,1.5,6,0" Text="IDXLZ" />
                         <TextBlock Margin="0,1.5,6,0" Text="kaoyu" />
                         <TextBlock Margin="0,1.5,6,0" Text="kaoyu" />
                         <TextBlock Margin="0,1.5,6,0" Text="肥富帅" />
                         <TextBlock Margin="0,1.5,6,0" Text="肥富帅" />
-                        <TextBlock Margin="0,1.5,6,0" Text="小王子" />
-                        <TextBlock Margin="0,1.5,6,0" Text="狐狸涩" />
-                        <TextBlock Margin="0,1.5,6,0" Text="大涡流" />
-                        <TextBlock Margin="0,1.5,6,0" Text="无名氏" />
-                        <TextBlock Margin="0,1.5,6,0" Text="紫风狐" />
+                        <TextBlock Margin="0,1.5,6,0" Text="沙西凯" />
+                        <TextBlock Margin="0,1.5,6,0" Text="老脑瘫" />
+                        <TextBlock Margin="0,1.5,6,0" Text="雪无魂" />
+                        <TextBlock Margin="0,1.5,6,0" Text="叶小羊" />
+                        <TextBlock Margin="0,1.5,6,0" Text="北极星" />
                         <TextBlock Margin="0,1.5,6,0" Text="杨巨焕" />
                         <TextBlock Margin="0,1.5,6,0" Text="杨巨焕" />
-                        <TextBlock Margin="0,1.5,6,0" Text="范桂圆" />
-                        <TextBlock Margin="0,1.5,6,0" Text="芸芸云" />
-                        <TextBlock Margin="0,1.5,6,0" Text="快夏波" />
-                        <TextBlock Margin="0,1.5,6,0" Text="陈知远" />
-                        <TextBlock Margin="0,1.5,6,0" Text="梦华书" />
-                        <TextBlock Margin="0,1.5,6,0" Text="星期天" />
-                        <TextBlock Margin="0,1.5,6,0" Text="铸日者" />
-                        <TextBlock Margin="0,1.5,6,0" Text="晴云雯" />
-                        <TextBlock Margin="0,1.5,6,0" Text="叶龙辉" />
-                        <TextBlock Margin="0,1.5,6,0" Text="小零蚀" />
-                        <TextBlock Margin="0,1.5,6,0" Text="唐墨白" />
-                        <TextBlock Margin="0,1.5,6,0" Text="香子兰" />
-                        <TextBlock Margin="0,1.5,6,0" Text="胡萝北" />
-                        <TextBlock Margin="0,1.5,6,0" Text="乐永昶" />
-                        <TextBlock Margin="0,1.5,6,0" Text="lingke" />
-                        <TextBlock Margin="0,1.5,6,0" Text="东雪莲" />
-                        <TextBlock Margin="0,1.5,6,0" Text="殷疯子" />
-                        <TextBlock Margin="0,1.5,6,0" Text="伊文斯" />
-                        <TextBlock Margin="0,1.5,6,0" Text="丁乙恒" />
-                        <TextBlock Margin="0,1.5,6,0" Text="朔望月" />
-                        <TextBlock Margin="0,1.5,6,0" Text="V0rt3x" />
-                        <TextBlock Margin="0,1.5,6,0" Text="提灯狼" />
-                        <TextBlock Margin="0,1.5,6,0" Text="王孟清" />
-                        <TextBlock Margin="0,1.5,6,0" Text="牛排鸭" />
-                        <TextBlock Margin="0,1.5,6,0" Text="王靖淳" />
-                        <TextBlock Margin="0,1.5,6,0" Text="嘿嘿地" />
-                        <TextBlock Margin="0,1.5,6,0" Text="鹿露璐" />
-                        <TextBlock Margin="0,1.5,6,0" Text="琪露诺" />
-                        <TextBlock Margin="0,1.5,6,0" Text="亚波人" />
-                        <TextBlock Margin="0,1.5,6,0" Text="卿天岚" />
-                        <TextBlock Margin="0,1.5,6,0" Text="哈士奇" />
-                        <TextBlock Margin="0,1.5,6,0" Text="江溪渺" />
-                        <TextBlock Margin="0,1.5,6,0" Text="浮千傀" />
-                        <TextBlock Margin="0,1.5,6,0" Text="恰啦啦" />
+                        <TextBlock Margin="0,1.5,6,0" Text="赤冰雨" />
+                        <TextBlock Margin="0,1.5,6,0" Text="布布曼" />
+                        <TextBlock Margin="0,1.5,6,0" Text="绫曦葉" />
+                        <TextBlock Margin="0,1.5,6,0" Text="竹怀瑾" />
+                        <TextBlock Margin="0,1.5,6,0" Text="小阮阮" />
                         <TextBlock Margin="0,1.5,6,0" Text="梦痕丿" />
                         <TextBlock Margin="0,1.5,6,0" Text="梦痕丿" />
                         <TextBlock Margin="0,1.5,6,0" Text="三叶铭" />
                         <TextBlock Margin="0,1.5,6,0" Text="三叶铭" />
                         <TextBlock Margin="0,1.5,6,0" Text="卿姬昉" />
                         <TextBlock Margin="0,1.5,6,0" Text="卿姬昉" />
@@ -309,211 +365,400 @@
                         <TextBlock Margin="0,1.5,6,0" Text="李困困" />
                         <TextBlock Margin="0,1.5,6,0" Text="李困困" />
                         <TextBlock Margin="0,1.5,6,0" Text="厄尔萨" />
                         <TextBlock Margin="0,1.5,6,0" Text="厄尔萨" />
                         <TextBlock Margin="0,1.5,6,0" Text="伏拉夫" />
                         <TextBlock Margin="0,1.5,6,0" Text="伏拉夫" />
-                        <TextBlock Margin="0,1.5,6,0" Text="α-蒎烯" />
+                        <TextBlock Margin="0,1.5,6,0" Text="暗恋哥" />
+                        <TextBlock Margin="0,1.5,6,0" Text="林柒北" />
+                        <TextBlock Margin="0,1.5,6,0" Text="月残残" />
+                        <TextBlock Margin="0,1.5,6,0" Text="查查姐" />
+                        <TextBlock Margin="0,1.5,6,0" Text="一根筋" />
+                        <TextBlock Margin="0,1.5,6,0" Text="睡觉喵" />
+                        <TextBlock Margin="0,1.5,6,0" Text="天川水" />
+                        <TextBlock Margin="0,1.5,6,0" Text="心未天" />
+                        <TextBlock Margin="0,1.5,6,0" Text="黑大帅" />
+                        <TextBlock Margin="0,1.5,6,0" Text="戏志才" />
+                        <TextBlock Margin="0,1.5,6,0" Text="羊跟钱" />
+                        <TextBlock Margin="0,1.5,6,0" Text="三三酱" />
+                        <TextBlock Margin="0,1.5,6,0" Text="茉九菌" />
+                        <TextBlock Margin="0,1.5,6,0" Text="圣羽卫" />
+                        <TextBlock Margin="0,1.5,6,0" Text="znixey" />
+                        <TextBlock Margin="0,1.5,6,0" Text="翎羽絮" />
+                        <TextBlock Margin="0,1.5,6,0" Text="硫酸梅" />
+                        <TextBlock Margin="0,1.5,6,0" Text="死鱼梦" />
+                        <TextBlock Margin="0,1.5,6,0" Text="秦始皇" />
+                        <TextBlock Margin="0,1.5,6,0" Text="春忆雪" />
+                        <TextBlock Margin="0,1.5,6,0" Text="冷空气" />
+                        <TextBlock Margin="0,1.5,6,0" Text="天崇域" />
+                        <TextBlock Margin="0,1.5,6,0" Text="白与沧" />
+                        <TextBlock Margin="0,1.5,6,0" Text="柳蕴訫" />
+                        <TextBlock Margin="0,1.5,6,0" Text="苟子丶" />
+                        <TextBlock Margin="0,1.5,6,0" Text="午時夢" />
+                        <TextBlock Margin="0,1.5,6,0" Text="清水石" />
+                        <TextBlock Margin="0,1.5,6,0" Text="鹿鹤云" />
+                        <TextBlock Margin="0,1.5,6,0" Text="洛沫兀" />
+                        <TextBlock Margin="0,1.5,6,0" Text="。。。" />
+                        <TextBlock Margin="0,1.5,6,0" Text="樱咲夜" />
+                        <TextBlock Margin="0,1.5,6,0" Text="星期天" />
+                        <TextBlock Margin="0,1.5,6,0" Text="制花人" />
+                        <TextBlock Margin="0,1.5,6,0" Text="aleMk" />
+                        <TextBlock Margin="0,1.5,6,0" Text="张炜国" />
+                        <TextBlock Margin="0,1.5,6,0" Text="咦嘻嘻" />
+                        <TextBlock Margin="0,1.5,6,0" Text="拟态虫" />
+                        <TextBlock Margin="0,1.5,6,0" Text="蜥蜴拔" />
+                        <TextBlock Margin="0,1.5,6,0" Text="鬼画符" />
+                        <TextBlock Margin="0,1.5,6,0" Text="孙无为" />
+                        <TextBlock Margin="0,1.5,6,0" Text="浅陌菌" />
+                        <TextBlock Margin="0,1.5,6,0" Text="黯域星" />
+                        <TextBlock Margin="0,1.5,6,0" Text="愿不惑" />
+                        <TextBlock Margin="0,1.5,6,0" Text="三色堇" />
+                        <TextBlock Margin="0,1.5,6,0" Text="訫卜變" />
+                        <TextBlock Margin="0,1.5,6,0" Text="周家逸" />
+                        <TextBlock Margin="0,1.5,6,0" Text="水芃叁" />
+                        <TextBlock Margin="0,1.5,6,0" Text="无限旡" />
+                        <TextBlock Margin="0,1.5,6,0" Text="???" />
+                        <TextBlock Margin="0,1.5,6,0" Text="小南新" />
+                        <TextBlock Margin="0,1.5,6,0" Text="星千织" />
+                        <TextBlock Margin="0,1.5,6,0" Text="CX330" />
+                        <TextBlock Margin="0,1.5,6,0" Text="林空月" />
+                        <TextBlock Margin="0,1.5,6,0" Text="花开丶" />
+                        <TextBlock Margin="0,1.5,6,0" Text="琉璃子" />
+                        <TextBlock Margin="0,1.5,6,0" Text="冰冰冰" />
+                        <TextBlock Margin="0,1.5,6,0" Text="老花眼" />
+                        <TextBlock Margin="0,1.5,6,0" Text="哈贝吧" />
+                        <TextBlock Margin="0,1.5,6,0" Text="蘑菇头" />
+                        <TextBlock Margin="0,1.5,6,0" Text="小黄刀" />
                         <TextBlock Margin="0,1.5,6,0" Text="你的JK" />
                         <TextBlock Margin="0,1.5,6,0" Text="你的JK" />
                         <TextBlock Margin="0,1.5,6,0" Text="jiujinh" />
                         <TextBlock Margin="0,1.5,6,0" Text="jiujinh" />
-                        <TextBlock Margin="0,1.5,6,0" Text="慕qwq" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Twicat" />
-                        <TextBlock Margin="0,1.5,6,0" Text="stenac" />
-                        <TextBlock Margin="0,1.5,6,0" Text="JOKER" />
-                        <TextBlock Margin="0,1.5,6,0" Text="半日谣." />
+                        <TextBlock Margin="0,1.5,6,0" Text="Yosika" />
+                        <TextBlock Margin="0,1.5,6,0" Text="pand1" />
+                        <TextBlock Margin="0,1.5,6,0" Text="lizi756" />
+                        <TextBlock Margin="0,1.5,6,0" Text="letitbe" />
+                        <TextBlock Margin="0,1.5,6,0" Text="acgxm" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Lancer" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Ikorito" />
                         <TextBlock Margin="0,1.5,6,0" Text="EX星辞" />
                         <TextBlock Margin="0,1.5,6,0" Text="EX星辞" />
                         <TextBlock Margin="0,1.5,6,0" Text="Tefrish" />
                         <TextBlock Margin="0,1.5,6,0" Text="Tefrish" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Triple9" />
-                        <TextBlock Margin="0,1.5,6,0" Text="gediao" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Zen1th" />
-                        <TextBlock Margin="0,1.5,6,0" Text="CH南宫" />
-                        <TextBlock Margin="0,1.5,6,0" Text="qiumin" />
-                        <TextBlock Margin="0,1.5,6,0" Text="cui白烁" />
-                        <TextBlock Margin="0,1.5,6,0" Text="anyayu" />
-                        <TextBlock Margin="0,1.5,6,0" Text="SISENG" />
-                        <TextBlock Margin="0,1.5,6,0" Text="ro-suto" />
-                        <TextBlock Margin="0,1.5,6,0" Text="GAGUA" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Yi_xuan" />
-                        <TextBlock Margin="0,1.5,6,0" Text="MC芬达" />
-                        <TextBlock Margin="0,1.5,6,0" Text="shallow" />
+                        <TextBlock Margin="0,1.5,6,0" Text="beaver" />
+                        <TextBlock Margin="0,1.5,6,0" Text="MCDD" />
+                        <TextBlock Margin="0,1.5,6,0" Text="cheese" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Lemon" />
+                        <TextBlock Margin="0,1.5,6,0" Text="芒果CV" />
+                        <TextBlock Margin="0,1.5,6,0" Text="nihility" />
+                        <TextBlock Margin="0,1.5,6,0" Text="小宇Bo" />
+                        <TextBlock Margin="0,1.5,6,0" Text="2789th" />
+                        <TextBlock Margin="0,1.5,6,0" Text="laohan" />
+                        <TextBlock Margin="0,1.5,6,0" Text="S1NKO" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Rimlue" />
+                        <TextBlock Margin="0,1.5,6,0" Text="FMDQI" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Shargu" />
+                        <TextBlock Margin="0,1.5,6,0" Text="xh1145" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Aevrud" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Larry76" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Miantu" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Nature." />
+                        <TextBlock Margin="0,1.5,6,0" Text="1424CC" />
+                        <TextBlock Margin="0,1.5,6,0" Text="stevelry" />
+                        <TextBlock Margin="0,1.5,6,0" Text="MXUER" />
+                        <TextBlock Margin="0,1.5,6,0" Text="suifeng" />
                         <TextBlock Margin="0,1.5,6,0" Text="ETL蓝夢" />
                         <TextBlock Margin="0,1.5,6,0" Text="ETL蓝夢" />
-                        <TextBlock Margin="0,1.5,6,0" Text="𝕳轩辕氏" />
-                        <TextBlock Margin="0,1.5,6,0" Text="谜茶360" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Yangeh" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Anmisu" />
+                        <TextBlock Margin="0,1.5,6,0" Text="TinkerZ" />
                         <TextBlock Margin="0,1.5,6,0" Text="颜沐Jun" />
                         <TextBlock Margin="0,1.5,6,0" Text="颜沐Jun" />
+                        <TextBlock Margin="0,1.5,6,0" Text="LaoGou" />
+                        <TextBlock Margin="0,1.5,6,0" Text="lkhkykk" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Mr2hao" />
                         <TextBlock Margin="0,1.5,6,0" Text="CS2七松" />
                         <TextBlock Margin="0,1.5,6,0" Text="CS2七松" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Eventful" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Molioos" />
-                        <TextBlock Margin="0,1.5,6,0" Text="logicum" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Eakhfps" />
+                        <TextBlock Margin="0,1.5,6,0" Text="ai魔虚罗" />
+                        <TextBlock Margin="0,1.5,6,0" Text="A6stract" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Vanessa" />
+                        <TextBlock Margin="0,1.5,6,0" Text="lanyuan" />
+                        <TextBlock Margin="0,1.5,6,0" Text="BBBKKK" />
+                        <TextBlock Margin="0,1.5,6,0" Text="MANBA" />
                         <TextBlock Margin="0,1.5,6,0" Text="Mr_loba" />
                         <TextBlock Margin="0,1.5,6,0" Text="Mr_loba" />
-                        <TextBlock Margin="0,1.5,6,0" Text="一叶知秋" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Norb0at" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Tairitsu_" />
+                        <TextBlock Margin="0,1.5,6,0" Text="宇陌୧⍤⃝🇨🇳" />
+                        <TextBlock Margin="0,1.5,6,0" Text="吾乃铠少" />
+                        <TextBlock Margin="0,1.5,6,0" Text="随缘降落" />
                         <TextBlock Margin="0,1.5,6,0" Text="我妻狗卷" />
                         <TextBlock Margin="0,1.5,6,0" Text="我妻狗卷" />
                         <TextBlock Margin="0,1.5,6,0" Text="御神凌霜" />
                         <TextBlock Margin="0,1.5,6,0" Text="御神凌霜" />
-                        <TextBlock Margin="0,1.5,6,0" Text="狼烟居士" />
-                        <TextBlock Margin="0,1.5,6,0" Text="千风伶羽" />
-                        <TextBlock Margin="0,1.5,6,0" Text="mumiao" />
-                        <TextBlock Margin="0,1.5,6,0" Text="欧刃赛卌" />
-                        <TextBlock Margin="0,1.5,6,0" Text="蓝白幻影" />
-                        <TextBlock Margin="0,1.5,6,0" Text="鸭梨山大" />
-                        <TextBlock Margin="0,1.5,6,0" Text="一条鱼瓯" />
-                        <TextBlock Margin="0,1.5,6,0" Text="苇渚泊舟" />
-                        <TextBlock Margin="0,1.5,6,0" Text="花开富贵" />
-                        <TextBlock Margin="0,1.5,6,0" Text="多肉藕片" />
-                        <TextBlock Margin="0,1.5,6,0" Text="浮屠阿仙" />
-                        <TextBlock Margin="0,1.5,6,0" Text="繁星萤火" />
-                        <TextBlock Margin="0,1.5,6,0" Text="沐风沐雨" />
-                        <TextBlock Margin="0,1.5,6,0" Text="金槍羽翼" />
-                        <TextBlock Margin="0,1.5,6,0" Text="龘羅孖倆" />
-                        <TextBlock Margin="0,1.5,6,0" Text="鸽子王雷" />
-                        <TextBlock Margin="0,1.5,6,0" Text="红莲焚霞" />
-                        <TextBlock Margin="0,1.5,6,0" Text="老凌野少" />
-                        <TextBlock Margin="0,1.5,6,0" Text="孤酒青魇" />
-                        <TextBlock Margin="0,1.5,6,0" Text="天空奇己" />
-                        <TextBlock Margin="0,1.5,6,0" Text="霜月秋阳" />
-                        <TextBlock Margin="0,1.5,6,0" Text="啃瓜咸鱼" />
-                        <TextBlock Margin="0,1.5,6,0" Text="淡墨染星" />
-                        <TextBlock Margin="0,1.5,6,0" Text="2k2k114" />
-                        <TextBlock Margin="0,1.5,6,0" Text="肉蛋葱鸡" />
-                        <TextBlock Margin="0,1.5,6,0" Text="傻鹅二号" />
-                        <TextBlock Margin="0,1.5,6,0" Text="动漫阿宅" />
-                        <TextBlock Margin="0,1.5,6,0" Text="只若初见" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Sakuzyo" />
-                        <TextBlock Margin="0,1.5,6,0" Text="1415145" />
-                        <TextBlock Margin="0,1.5,6,0" Text="消失的喵" />
-                        <TextBlock Margin="0,1.5,6,0" Text="春山如笑" />
-                        <TextBlock Margin="0,1.5,6,0" Text="幻天阿绝" />
-                        <TextBlock Margin="0,1.5,6,0" Text="忘道蜂枫" />
-                        <TextBlock Margin="0,1.5,6,0" Text="深海无渊" />
-                        <TextBlock Margin="0,1.5,6,0" Text="心软的神" />
-                        <TextBlock Margin="0,1.5,6,0" Text="砂锅豆腐" />
-                        <TextBlock Margin="0,1.5,6,0" Text="好人好逝" />
+                        <TextBlock Margin="0,1.5,6,0" Text="翌日清晨" />
+                        <TextBlock Margin="0,1.5,6,0" Text="栀玖夕夏" />
+                        <TextBlock Margin="0,1.5,6,0" Text="大唐江山" />
+                        <TextBlock Margin="0,1.5,6,0" Text="镜像折叠" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Arwcraft" />
+                        <TextBlock Margin="0,1.5,6,0" Text="辞忧玖思" />
                         <TextBlock Margin="0,1.5,6,0" Text="登峰破晓" />
                         <TextBlock Margin="0,1.5,6,0" Text="登峰破晓" />
                         <TextBlock Margin="0,1.5,6,0" Text="东隅未殁" />
                         <TextBlock Margin="0,1.5,6,0" Text="东隅未殁" />
                         <TextBlock Margin="0,1.5,6,0" Text="星花银影" />
                         <TextBlock Margin="0,1.5,6,0" Text="星花银影" />
                         <TextBlock Margin="0,1.5,6,0" Text="往事如昔" />
                         <TextBlock Margin="0,1.5,6,0" Text="往事如昔" />
                         <TextBlock Margin="0,1.5,6,0" Text="林克之空" />
                         <TextBlock Margin="0,1.5,6,0" Text="林克之空" />
+                        <TextBlock Margin="0,1.5,6,0" Text="站岗猫猫" />
+                        <TextBlock Margin="0,1.5,6,0" Text="影子爱人" />
+                        <TextBlock Margin="0,1.5,6,0" Text="恋雨夏日" />
+                        <TextBlock Margin="0,1.5,6,0" Text="好运连连" />
+                        <TextBlock Margin="0,1.5,6,0" Text="退订回复" />
+                        <TextBlock Margin="0,1.5,6,0" Text="翻阳博弈" />
+                        <TextBlock Margin="0,1.5,6,0" Text="天将拂晓" />
+                        <TextBlock Margin="0,1.5,6,0" Text="独角戏空" />
+                        <TextBlock Margin="0,1.5,6,0" Text="屁屁噗噗" />
+                        <TextBlock Margin="0,1.5,6,0" Text="旧日珥瑜" />
+                        <TextBlock Margin="0,1.5,6,0" Text="入雨之霖" />
+                        <TextBlock Margin="0,1.5,6,0" Text="看的开师" />
+                        <TextBlock Margin="0,1.5,6,0" Text="莴苣大W" />
+                        <TextBlock Margin="0,1.5,6,0" Text="東泽啊茶" />
+                        <TextBlock Margin="0,1.5,6,0" Text="中野霊乃" />
+                        <TextBlock Margin="0,1.5,6,0" Text="囚仌从众" />
+                        <TextBlock Margin="0,1.5,6,0" Text="无言之期" />
+                        <TextBlock Margin="0,1.5,6,0" Text="火之晨曦" />
+                        <TextBlock Margin="0,1.5,6,0" Text="しょう酱" />
+                        <TextBlock Margin="0,1.5,6,0" Text="泽尤普夫" />
+                        <TextBlock Margin="0,1.5,6,0" Text="辞暮山君" />
+                        <TextBlock Margin="0,1.5,6,0" Text="凤梨果酱" />
+                        <TextBlock Margin="0,1.5,6,0" Text="默宁鳯凉" />
+                        <TextBlock Margin="0,1.5,6,0" Text="我不在线" />
+                        <TextBlock Margin="0,1.5,6,0" Text="南吕十三" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Parapha" />
+                        <TextBlock Margin="0,1.5,6,0" Text="开始学习" />
+                        <TextBlock Margin="0,1.5,6,0" Text="对方和你" />
+                        <TextBlock Margin="0,1.5,6,0" Text="雪辰千叶" />
+                        <TextBlock Margin="0,1.5,6,0" Text="残风尘缘" />
+                        <TextBlock Margin="0,1.5,6,0" Text="黄粱一梦" />
+                        <TextBlock Margin="0,1.5,6,0" Text="腊肉清蒸" />
+                        <TextBlock Margin="0,1.5,6,0" Text="画下花夏" />
+                        <TextBlock Margin="0,1.5,6,0" Text="蛮列后备" />
+                        <TextBlock Margin="0,1.5,6,0" Text="魔法少年" />
+                        <TextBlock Margin="0,1.5,6,0" Text="戒者有八" />
+                        <TextBlock Margin="0,1.5,6,0" Text="躺平如尸" />
+                        <TextBlock Margin="0,1.5,6,0" Text="萝卜奶酪" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Erinneys" />
+                        <TextBlock Margin="0,1.5,6,0" Text="病名為愛" />
+                        <TextBlock Margin="0,1.5,6,0" Text="伊卡洛斯" />
+                        <TextBlock Margin="0,1.5,6,0" Text="云鹰肥将" />
+                        <TextBlock Margin="0,1.5,6,0" Text="爱莉信徒" />
+                        <TextBlock Margin="0,1.5,6,0" Text="user晚曦" />
+                        <TextBlock Margin="0,1.5,6,0" Text="欣某在此" />
+                        <TextBlock Margin="0,1.5,6,0" Text="繁星末路" />
+                        <TextBlock Margin="0,1.5,6,0" Text="鲸鱼橘子" />
+                        <TextBlock Margin="0,1.5,6,0" Text="坠入幻想" />
+                        <TextBlock Margin="0,1.5,6,0" Text="卑微老万" />
+                        <TextBlock Margin="0,1.5,6,0" Text="乌云踏雪" />
+                        <TextBlock Margin="0,1.5,6,0" Text="公积金卡" />
+                        <TextBlock Margin="0,1.5,6,0" Text="各务原念" />
+                        <TextBlock Margin="0,1.5,6,0" Text="风间琉璃" />
+                        <TextBlock Margin="0,1.5,6,0" Text="杨枝甘露" />
+                        <TextBlock Margin="0,1.5,6,0" Text="晚安晚安" />
+                        <TextBlock Margin="0,1.5,6,0" Text="牛奶曲奇" />
+                        <TextBlock Margin="0,1.5,6,0" Text="魔法空间" />
+                        <TextBlock Margin="0,1.5,6,0" Text="百里无焉" />
+                        <TextBlock Margin="0,1.5,6,0" Text="皮蛋肥猫" />
+                        <TextBlock Margin="0,1.5,6,0" Text="身披坚甲" />
+                        <TextBlock Margin="0,1.5,6,0" Text="近则不逊" />
+                        <TextBlock Margin="0,1.5,6,0" Text="卡白小洛" />
+                        <TextBlock Margin="0,1.5,6,0" Text="即墨非墨" />
+                        <TextBlock Margin="0,1.5,6,0" Text="准备冒险" />
+                        <TextBlock Margin="0,1.5,6,0" Text="喰梦优音" />
+                        <TextBlock Margin="0,1.5,6,0" Text="雨夜色中" />
+                        <TextBlock Margin="0,1.5,6,0" Text="沃特拉奇" />
+                        <TextBlock Margin="0,1.5,6,0" Text="闲鱼教授" />
+                        <TextBlock Margin="0,1.5,6,0" Text="北川暮雨" />
+                        <TextBlock Margin="0,1.5,6,0" Text="生根发芽" />
+                        <TextBlock Margin="0,1.5,6,0" Text="暮霭凝缘" />
+                        <TextBlock Margin="0,1.5,6,0" Text="孤灯裹尘" />
+                        <TextBlock Margin="0,1.5,6,0" Text="日薪月亿" />
+                        <TextBlock Margin="0,1.5,6,0" Text="北默千鹤" />
+                        <TextBlock Margin="0,1.5,6,0" Text="咸鱼一条" />
+                        <TextBlock Margin="0,1.5,6,0" Text="禾辞共振" />
                         <TextBlock Margin="0,1.5,6,0" Text="0X00001" />
                         <TextBlock Margin="0,1.5,6,0" Text="0X00001" />
-                        <TextBlock Margin="0,1.5,6,0" Text="MuMAA" />
-                        <TextBlock Margin="0,1.5,6,0" Text="溯枫qwq" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Tacenda" />
-                        <TextBlock Margin="0,1.5,6,0" Text="sky fung" />
-                        <TextBlock Margin="0,1.5,6,0" Text="👁👄👃" />
-                        <TextBlock Margin="0,1.5,6,0" Text="秋慕owo" />
-                        <TextBlock Margin="0,1.5,6,0" Text="船长Yuki" />
                         <TextBlock Margin="0,1.5,6,0" Text="HIMEKO" />
                         <TextBlock Margin="0,1.5,6,0" Text="HIMEKO" />
-                        <TextBlock Margin="0,1.5,6,0" Text="是小61呀" />
-                        <TextBlock Margin="0,1.5,6,0" Text="47号虾球" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Alancnel" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Muertesl" />
-                        <TextBlock Margin="0,1.5,6,0" Text="猫幺Suro" />
-                        <TextBlock Margin="0,1.5,6,0" Text="i&apos;g&apos;d&apos;y&apos;f&apos;s" />
-                        <TextBlock Margin="0,1.5,6,0" Text="xiaochen" />
-                        <TextBlock Margin="0,1.5,6,0" Text="ㅤSIMV9r" />
-                        <TextBlock Margin="0,1.5,6,0" Text="alpHabet" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Charlotte" />
-                        <TextBlock Margin="0,1.5,6,0" Text="paperfish" />
-                        <TextBlock Margin="0,1.5,6,0" Text="stealtoup" />
-                        <TextBlock Margin="0,1.5,6,0" Text="bingyan0" />
-                        <TextBlock Margin="0,1.5,6,0" Text="MselQAQ" />
-                        <TextBlock Margin="0,1.5,6,0" Text="UZQueen" />
-                        <TextBlock Margin="0,1.5,6,0" Text="hihospital" />
+                        <TextBlock Margin="0,1.5,6,0" Text="minedsa" />
+                        <TextBlock Margin="0,1.5,6,0" Text="jinyucao" />
+                        <TextBlock Margin="0,1.5,6,0" Text="派dark星" />
+                        <TextBlock Margin="0,1.5,6,0" Text="AYzh994" />
+                        <TextBlock Margin="0,1.5,6,0" Text="MESGRZ" />
+                        <TextBlock Margin="0,1.5,6,0" Text="qiewang" />
+                        <TextBlock Margin="0,1.5,6,0" Text="bao9898" />
+                        <TextBlock Margin="0,1.5,6,0" Text="幻沫_123" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Yuting.Li" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Zaaaako" />
+                        <TextBlock Margin="0,1.5,6,0" Text="ZephyR7" />
+                        <TextBlock Margin="0,1.5,6,0" Text="XkuanYa" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Tianqiao" />
+                        <TextBlock Margin="0,1.5,6,0" Text="EMILIIIIA" />
+                        <TextBlock Margin="0,1.5,6,0" Text="阿尔法.@" />
+                        <TextBlock Margin="0,1.5,6,0" Text="neymarjr" />
+                        <TextBlock Margin="0,1.5,6,0" Text="LUNRA_F" />
+                        <TextBlock Margin="0,1.5,6,0" Text="nonwind" />
+                        <TextBlock Margin="0,1.5,6,0" Text="waxberry" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Smlieove" />
+                        <TextBlock Margin="0,1.5,6,0" Text="liminalily" />
+                        <TextBlock Margin="0,1.5,6,0" Text="sansisthe" />
+                        <TextBlock Margin="0,1.5,6,0" Text="初心_迷修" />
+                        <TextBlock Margin="0,1.5,6,0" Text="RickyZhu" />
+                        <TextBlock Margin="0,1.5,6,0" Text="TenKings" />
+                        <TextBlock Margin="0,1.5,6,0" Text="kong悲切" />
+                        <TextBlock Margin="0,1.5,6,0" Text="yvnchvan" />
+                        <TextBlock Margin="0,1.5,6,0" Text="喵呜派zzz" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Mz小狐仙" />
+                        <TextBlock Margin="0,1.5,6,0" Text="kyousuke" />
+                        <TextBlock Margin="0,1.5,6,0" Text="huangwu" />
+                        <TextBlock Margin="0,1.5,6,0" Text="玖月Yiyue" />
+                        <TextBlock Margin="0,1.5,6,0" Text="MC真好玩" />
+                        <TextBlock Margin="0,1.5,6,0" Text="UNICORN" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Otherside" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Wstorther" />
+                        <TextBlock Margin="0,1.5,6,0" Text="zzzsssppp" />
+                        <TextBlock Margin="0,1.5,6,0" Text="石头WAW" />
                         <TextBlock Margin="0,1.5,6,0" Text="cutegirl3k" />
                         <TextBlock Margin="0,1.5,6,0" Text="cutegirl3k" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Prifo_lone" />
-                        <TextBlock Margin="0,1.5,6,0" Text="有....暗星!" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Hitsser 1.0" />
-                        <TextBlock Margin="0,1.5,6,0" Text="一只憨火龙" />
-                        <TextBlock Margin="0,1.5,6,0" Text="异色收集癖" />
-                        <TextBlock Margin="0,1.5,6,0" Text="DsxiaKsxia" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Plainsman" />
-                        <TextBlock Margin="0,1.5,6,0" Text="湛蓝一四七" />
-                        <TextBlock Margin="0,1.5,6,0" Text="凛果与百橙" />
-                        <TextBlock Margin="0,1.5,6,0" Text="叶落才知秋" />
-                        <TextBlock Margin="0,1.5,6,0" Text="好喝的氦三" />
-                        <TextBlock Margin="0,1.5,6,0" Text="星河奈叶风" />
-                        <TextBlock Margin="0,1.5,6,0" Text="魔法且河豚" />
-                        <TextBlock Margin="0,1.5,6,0" Text="魏小泓泓泓" />
-                        <TextBlock Margin="0,1.5,6,0" Text="阴冷潮湿酱" />
-                        <TextBlock Margin="0,1.5,6,0" Text="一只白色缨" />
-                        <TextBlock Margin="0,1.5,6,0" Text="结局的椰子" />
-                        <TextBlock Margin="0,1.5,6,0" Text="三玖二拾柒" />
-                        <TextBlock Margin="0,1.5,6,0" Text="旭旭大魔王" />
-                        <TextBlock Margin="0,1.5,6,0" Text="离人如飘萍" />
-                        <TextBlock Margin="0,1.5,6,0" Text="秋葉逐殘夏" />
-                        <TextBlock Margin="0,1.5,6,0" Text="水星有海吗" />
+                        <TextBlock Margin="0,1.5,6,0" Text="YiYi_Sama" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Z1rConSN" />
+                        <TextBlock Margin="0,1.5,6,0" Text="HeiGuang" />
+                        <TextBlock Margin="0,1.5,6,0" Text="弥漫11037" />
+                        <TextBlock Margin="0,1.5,6,0" Text="theancato" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Feng_mee" />
+                        <TextBlock Margin="0,1.5,6,0" Text="dark 寿司s" />
+                        <TextBlock Margin="0,1.5,6,0" Text="zhouuohz" />
+                        <TextBlock Margin="0,1.5,6,0" Text="华燃王小灭" />
+                        <TextBlock Margin="0,1.5,6,0" Text="被哐成蜜柑" />
+                        <TextBlock Margin="0,1.5,6,0" Text="看完就睡吧" />
+                        <TextBlock Margin="0,1.5,6,0" Text="有脚气勿扰" />
                         <TextBlock Margin="0,1.5,6,0" Text="吃饱爱睡觉" />
                         <TextBlock Margin="0,1.5,6,0" Text="吃饱爱睡觉" />
-                        <TextBlock Margin="0,1.5,6,0" Text="ac纯萌新cy" />
-                        <TextBlock Margin="0,1.5,6,0" Text="岁月suiyue" />
-                        <TextBlock Margin="0,1.5,6,0" Text="陌然MajorC" />
-                        <TextBlock Margin="0,1.5,6,0" Text="-Pareidolia." />
-                        <TextBlock Margin="0,1.5,6,0" Text="蛙蛙大王Kzr" />
+                        <TextBlock Margin="0,1.5,6,0" Text="摸鱼的香草" />
+                        <TextBlock Margin="0,1.5,6,0" Text="我心态很好" />
+                        <TextBlock Margin="0,1.5,6,0" Text="吃碗不吃饭" />
+                        <TextBlock Margin="0,1.5,6,0" Text="汉宫秋月夜" />
+                        <TextBlock Margin="0,1.5,6,0" Text="哇哩嘞了啦" />
+                        <TextBlock Margin="0,1.5,6,0" Text="长作东南别" />
+                        <TextBlock Margin="0,1.5,6,0" Text="等夜望星空" />
+                        <TextBlock Margin="0,1.5,6,0" Text="死在火星上" />
+                        <TextBlock Margin="0,1.5,6,0" Text="芙芙不傻!" />
+                        <TextBlock Margin="0,1.5,6,0" Text="梦醉似红颜" />
+                        <TextBlock Margin="0,1.5,6,0" Text="小桃八个人" />
+                        <TextBlock Margin="0,1.5,6,0" Text="躺床上发霉" />
+                        <TextBlock Margin="0,1.5,6,0" Text="装睡的少爷" />
+                        <TextBlock Margin="0,1.5,6,0" Text="无想的贤者" />
+                        <TextBlock Margin="0,1.5,6,0" Text="我不是陌行" />
+                        <TextBlock Margin="0,1.5,6,0" Text="海绵体宝宝" />
+                        <TextBlock Margin="0,1.5,6,0" Text="猫不吃咸鱼" />
+                        <TextBlock Margin="0,1.5,6,0" Text="别太失望啦" />
+                        <TextBlock Margin="0,1.5,6,0" Text="土豆粉底液" />
+                        <TextBlock Margin="0,1.5,6,0" Text="沈默已陌鑫" />
+                        <TextBlock Margin="0,1.5,6,0" Text="向神明祈祷" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Zurkey106" />
+                        <TextBlock Margin="0,1.5,6,0" Text="stargolven" />
+                        <TextBlock Margin="0,1.5,6,0" Text="拒绝过阿姨" />
+                        <TextBlock Margin="0,1.5,6,0" Text="拟态深埋箱" />
+                        <TextBlock Margin="0,1.5,6,0" Text="凤凰院凶真" />
+                        <TextBlock Margin="0,1.5,6,0" Text="时间&轮盘" />
+                        <TextBlock Margin="0,1.5,6,0" Text="粗心忘带头" />
+                        <TextBlock Margin="0,1.5,6,0" Text="提线的人偶" />
+                        <TextBlock Margin="0,1.5,6,0" Text="非慕所不敬" />
+                        <TextBlock Margin="0,1.5,6,0" Text="世界第一矮" />
+                        <TextBlock Margin="0,1.5,6,0" Text="雷焰不虚误" />
+                        <TextBlock Margin="0,1.5,6,0" Text="一株腐竹吖" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Ferrum000" />
+                        <TextBlock Margin="0,1.5,6,0" Text="小白pandk" />
+                        <TextBlock Margin="0,1.5,6,0" Text="171662276" />
+                        <TextBlock Margin="0,1.5,6,0" Text="LapalceXIII" />
+                        <TextBlock Margin="0,1.5,6,0" Text="黑神话·悟能" />
+                        <TextBlock Margin="0,1.5,6,0" Text="charlesmin" />
+                        <TextBlock Margin="0,1.5,6,0" Text="DeepSleep" />
+                        <TextBlock Margin="0,1.5,6,0" Text="rishaoshan" />
+                        <TextBlock Margin="0,1.5,6,0" Text="sakura.YYH" />
+                        <TextBlock Margin="0,1.5,6,0" Text="心在多伦多i" />
+                        <TextBlock Margin="0,1.5,6,0" Text="233qwedsa" />
+                        <TextBlock Margin="0,1.5,6,0" Text="builder天识" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Cathan猫皁" />
                         <TextBlock Margin="0,1.5,6,0" Text="Aurorawalk" />
                         <TextBlock Margin="0,1.5,6,0" Text="Aurorawalk" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Yhcmdbfwz" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Ayers赜蠹嬭" />
-                        <TextBlock Margin="0,1.5,6,0" Text="We1J1uSh1" />
-                        <TextBlock Margin="0,1.5,6,0" Text="xinghen233" />
-                        <TextBlock Margin="0,1.5,6,0" Text="kingkongair" />
-                        <TextBlock Margin="0,1.5,6,0" Text="最后一只斑鸠" />
-                        <TextBlock Margin="0,1.5,6,0" Text="随便写的名字" />
-                        <TextBlock Margin="0,1.5,6,0" Text="芥末味青豌豆" />
-                        <TextBlock Margin="0,1.5,6,0" Text="鹅给金华额度" />
-                        <TextBlock Margin="0,1.5,6,0" Text="LisaPatorsed" />
-                        <TextBlock Margin="0,1.5,6,0" Text="鸡屁股配大蒜" />
-                        <TextBlock Margin="0,1.5,6,0" Text="我真没想开摆" />
-                        <TextBlock Margin="0,1.5,6,0" Text="我不想取名字" />
-                        <TextBlock Margin="0,1.5,6,0" Text="平安京的封兽" />
-                        <TextBlock Margin="0,1.5,6,0" Text="被压力的鸭梨" />
-                        <TextBlock Margin="0,1.5,6,0" Text="小田love白子" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Sasha_Xiong" />
-                        <TextBlock Margin="0,1.5,6,0" Text="赤狸_crisnow" />
+                        <TextBlock Margin="0,1.5,6,0" Text="GeorgeCavi" />
+                        <TextBlock Margin="0,1.5,6,0" Text="heaven伯特" />
+                        <TextBlock Margin="0,1.5,6,0" Text="礼央子sama" />
+                        <TextBlock Margin="0,1.5,6,0" Text="蠢萌der小白" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Dying Bone" />
+                        <TextBlock Margin="0,1.5,6,0" Text="DaisyCrown" />
+                        <TextBlock Margin="0,1.5,6,0" Text="2127864647" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Z123456789" />
+                        <TextBlock Margin="0,1.5,6,0" Text="SinlessLamb" />
+                        <TextBlock Margin="0,1.5,6,0" Text="arrovartsk77" />
+                        <TextBlock Margin="0,1.5,6,0" Text="William_Lun" />
+                        <TextBlock Margin="0,1.5,6,0" Text="MOMOHAN" />
+                        <TextBlock Margin="0,1.5,6,0" Text="舸沫_Official" />
+                        <TextBlock Margin="0,1.5,6,0" Text="JustinZhang" />
+                        <TextBlock Margin="0,1.5,6,0" Text="ξKō Hei-seiξ" />
+                        <TextBlock Margin="0,1.5,6,0" Text="CookieNeko" />
+                        <TextBlock Margin="0,1.5,6,0" Text="平凡的眼镜君" />
+                        <TextBlock Margin="0,1.5,6,0" Text="桑也不是桑野" />
+                        <TextBlock Margin="0,1.5,6,0" Text="时间能看清她" />
+                        <TextBlock Margin="0,1.5,6,0" Text="xianyu11178" />
+                        <TextBlock Margin="0,1.5,6,0" Text="氵厂泉夕田一" />
+                        <TextBlock Margin="0,1.5,6,0" Text="游离态的嘉嘉" />
+                        <TextBlock Margin="0,1.5,6,0" Text="悲伤的比熊犬" />
+                        <TextBlock Margin="0,1.5,6,0" Text="伊吹风子大人" />
+                        <TextBlock Margin="0,1.5,6,0" Text="バベルの悪霊" />
+                        <TextBlock Margin="0,1.5,6,0" Text="沉默的玄晓羲" />
+                        <TextBlock Margin="0,1.5,6,0" Text="为什么水无鱼" />
+                        <TextBlock Margin="0,1.5,6,0" Text="无敌猫猫刺客" />
+                        <TextBlock Margin="0,1.5,6,0" Text="久恋丿如初漾" />
+                        <TextBlock Margin="0,1.5,6,0" Text="肥宅不是胖子" />
+                        <TextBlock Margin="0,1.5,6,0" Text="路边的小野寺" />
+                        <TextBlock Margin="0,1.5,6,0" Text="要一个手抓饼" />
+                        <TextBlock Margin="0,1.5,6,0" Text="LI_JIACHENG" />
+                        <TextBlock Margin="0,1.5,6,0" Text="mouyangjun" />
                         <TextBlock Margin="0,1.5,6,0" Text="dorod月gour" />
                         <TextBlock Margin="0,1.5,6,0" Text="dorod月gour" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Name0emaN" />
-                        <TextBlock Margin="0,1.5,6,0" Text="happypickles" />
-                        <TextBlock Margin="0,1.5,6,0" Text="无中生有为dd" />
-                        <TextBlock Margin="0,1.5,6,0" Text="polarisdream" />
-                        <TextBlock Margin="0,1.5,6,0" Text="YHKooooooy" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Sakura2403ly" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Flying Potato" />
-                        <TextBlock Margin="0,1.5,6,0" Text="陈昊骏泥碳GR" />
-                        <TextBlock Margin="0,1.5,6,0" Text="funnyzhuling" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Turbo_Zhang" />
-                        <TextBlock Margin="0,1.5,6,0" Text="SUBLawrence" />
+                        <TextBlock Margin="0,1.5,6,0" Text="SARAKUHAN" />
+                        <TextBlock Margin="0,1.5,6,0" Text="我no想取名字" />
+                        <TextBlock Margin="0,1.5,6,0" Text="机电2516汪帅" />
+                        <TextBlock Margin="0,1.5,6,0" Text="QueNanNNN" />
+                        <TextBlock Margin="0,1.5,6,0" Text="huamozhuke" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Titanic Rising" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Skymunchrex" />
+                        <TextBlock Margin="0,1.5,6,0" Text="☆百☆岁☆枭☆" />
                         <TextBlock Margin="0,1.5,6,0" Text="Epsilon-9新人" />
                         <TextBlock Margin="0,1.5,6,0" Text="Epsilon-9新人" />
-                        <TextBlock Margin="0,1.5,6,0" Text="TheDovebach" />
-                        <TextBlock Margin="0,1.5,6,0" Text="wobushi_furry" />
-                        <TextBlock Margin="0,1.5,6,0" Text="FTC夏日的余晖" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Isshiki Satoshi" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Srive_Schegler" />
-                        <TextBlock Margin="0,1.5,6,0" Text="1080p安全卫士" />
-                        <TextBlock Margin="0,1.5,6,0" Text="开心上学玩游戏" />
-                        <TextBlock Margin="0,1.5,6,0" Text="重生之发财传奇" />
-                        <TextBlock Margin="0,1.5,6,0" Text="某科学的小狐狸" />
-                        <TextBlock Margin="0,1.5,6,0" Text="爱喝可乐的猫猫" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Vsinger-洛天依" />
-                        <TextBlock Margin="0,1.5,6,0" Text="芙利莲最も強い" />
-                        <TextBlock Margin="0,1.5,6,0" Text="洋葱香菜炒蛋子" />
-                        <TextBlock Margin="0,1.5,6,0" Text="不开心,不快乐" />
-                        <TextBlock Margin="0,1.5,6,0" Text="忧郁的破产鼠鼠" />
-                        <TextBlock Margin="0,1.5,6,0" Text="我的世界爱好者" />
-                        <TextBlock Margin="0,1.5,6,0" Text="半透明的小透明" />
-                        <TextBlock Margin="0,1.5,6,0" Text="到处闲逛的咸鱼" />
-                        <TextBlock Margin="0,1.5,6,0" Text="什么落寒都不是" />
-                        <TextBlock Margin="0,1.5,6,0" Text="没什么名字好取" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Wanderer5147" />
-                        <TextBlock Margin="0,1.5,6,0" Text="取个名字耗阳寿" />
+                        <TextBlock Margin="0,1.5,6,0" Text="爱我Girlfriend" />
+                        <TextBlock Margin="0,1.5,6,0" Text="donghaimugi" />
+                        <TextBlock Margin="0,1.5,6,0" Text="puang puang" />
+                        <TextBlock Margin="0,1.5,6,0" Text="CarbonDHeat" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Jay_Chou_001" />
+                        <TextBlock Margin="0,1.5,6,0" Text="123456789000" />
+                        <TextBlock Margin="0,1.5,6,0" Text="不爱fa电的小安" />
+                        <TextBlock Margin="0,1.5,6,0" Text="快乐将进行到底" />
+                        <TextBlock Margin="0,1.5,6,0" Text="雨夜带刀不带伞" />
                         <TextBlock Margin="0,1.5,6,0" Text="北熊何时与狼行" />
                         <TextBlock Margin="0,1.5,6,0" Text="北熊何时与狼行" />
                         <TextBlock Margin="0,1.5,6,0" Text="向着有风的地方" />
                         <TextBlock Margin="0,1.5,6,0" Text="向着有风的地方" />
-                        <TextBlock Margin="0,1.5,6,0" Text="AAA神秘人批发" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Bangbang2025" />
-                        <TextBlock Margin="0,1.5,6,0" Text="贝吉蒙Begimon" />
-                        <TextBlock Margin="0,1.5,6,0" Text="取个名字真难0.0" />
-                        <TextBlock Margin="0,1.5,6,0" Text="NoTuring_Steve" />
+                        <TextBlock Margin="0,1.5,6,0" Text="xuanyutuci010" />
+                        <TextBlock Margin="0,1.5,6,0" Text="笛笛笛叭笛笛豆" />
+                        <TextBlock Margin="0,1.5,6,0" Text="无敌兵王陈亚蒙" />
+                        <TextBlock Margin="0,1.5,6,0" Text="追随自己的星星" />
+                        <TextBlock Margin="0,1.5,6,0" Text="帅道无上大宗师" />
+                        <TextBlock Margin="0,1.5,6,0" Text="cremebrulee77" />
+                        <TextBlock Margin="0,1.5,6,0" Text="AAA.果粒橙批发" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Sakura Burning" />
+                        <TextBlock Margin="0,1.5,6,0" Text="GenWenbbt0v0" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Omega•Epsilon" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Ries1ing_嗷菲兽" />
+                        <TextBlock Margin="0,1.5,6,0" Text="HyperconductQ" />
                         <TextBlock Margin="0,1.5,6,0" Text="VividerCafe2379" />
                         <TextBlock Margin="0,1.5,6,0" Text="VividerCafe2379" />
-                        <TextBlock Margin="0,1.5,6,0" Text="冰冰watermelon" />
-                        <TextBlock Margin="0,1.5,6,0" Text="DOODOODOOD" />
+                        <TextBlock Margin="0,1.5,6,0" Text="楠泽云鸮Nanzen" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Understand You" />
+                        <TextBlock Margin="0,1.5,6,0" Text="筱筱雨吖_Official" />
+                        <TextBlock Margin="0,1.5,6,0" Text="AAADasAbi9527" />
                         <TextBlock Margin="0,1.5,6,0" Text="怕做苦力的苦力怕" />
                         <TextBlock Margin="0,1.5,6,0" Text="怕做苦力的苦力怕" />
-                        <TextBlock Margin="0,1.5,6,0" Text="小句号就是小句号" />
+                        <TextBlock Margin="0,1.5,6,0" Text="指定没你好果子吃" />
                         <TextBlock Margin="0,1.5,6,0" Text="弹跳爆炸小鱼珠🐟" />
                         <TextBlock Margin="0,1.5,6,0" Text="弹跳爆炸小鱼珠🐟" />
-                        <TextBlock Margin="0,1.5,6,0" Text="Xanadu Silk Fable" />
-                        <TextBlock Margin="0,1.5,6,0" Text="看我的都得白内障了" />
-                        <TextBlock Margin="0,1.5,6,0" Text="【点击请输入文本】" />
-                        <TextBlock Margin="0,1.5,6,0" Text="AAA难办批发商乌鸦" />
-                        <TextBlock Margin="0,1.5,6,0" Text="zhuanzhuanzhaun!" />
-                        <TextBlock Margin="0,1.5,6,0" Text="你上课记得记得你先忙" />
+                        <TextBlock Margin="0,1.5,6,0" Text="XS_star(周鑫阳)" />
+                        <TextBlock Margin="0,1.5,6,0" Text="0223YUKIUKOORI" />
+                        <TextBlock Margin="0,1.5,6,0" Text="☆Etritz__㯊龝.ET☆" />
+                        <TextBlock Margin="0,1.5,6,0" Text="I want to find you" />
+                        <TextBlock Margin="0,1.5,6,0" Text="NineTailed123213" />
+                        <TextBlock Margin="0,1.5,6,0" Text="什么时候网才能不卡" />
+                        <TextBlock Margin="0,1.5,6,0" Text="于电子星海寻极星间" />
+                        <TextBlock Margin="0,1.5,6,0" Text="``Aurora. (极光)" />
+                        <TextBlock Margin="0,1.5,6,0" Text="小鳥遊ホシノ(臨戰)" />
+                        <TextBlock Margin="0,1.5,6,0" Text="来自少女前线的刀客塔" />
+                        <TextBlock Margin="0,1.5,6,0" Text="￴￴ ￴￴此恨绵绵无绝期" />
+                        <TextBlock Margin="0,1.5,6,0" Text="[email protected]" />
+                        <TextBlock Margin="0,1.5,6,0" Text="AdeleineSummy的粉丝" />
+                        <TextBlock Margin="0,1.5,6,0" Text="人族人亚族人属智人种晚期智人亚种" />
                     </WrapPanel>
                     </WrapPanel>
                     <Grid Height="35">
                     <Grid Height="35">
                         <Grid.ColumnDefinitions>
                         <Grid.ColumnDefinitions>
@@ -600,14 +845,14 @@
                                     local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://opensource.org/licenses/MIT" />
                                     local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://opensource.org/licenses/MIT" />
                     
                     
                     <TextBlock Grid.Row="6" Grid.Column="0" Text="NAudio" TextWrapping="Wrap" FontWeight="Bold" />
                     <TextBlock Grid.Row="6" Grid.Column="0" Text="NAudio" TextWrapping="Wrap" FontWeight="Bold" />
-                    <TextBlock Grid.Row="6" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © 2015 Mark Heath ([email protected])&#xa;Licensed under the MIT License." TextWrapping="Wrap" />
+                    <TextBlock Grid.Row="6" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © 2015 Mark Heath ([email protected])&#xa;Licensed under the Microsoft Public License (Ms-PL)." TextWrapping="Wrap" />
                     <local:MyButton Grid.Row="7" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
                     <local:MyButton Grid.Row="7" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
                                     local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/naudio/NAudio" />
                                     local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/naudio/NAudio" />
                     <local:MyButton Grid.Row="7" Grid.Column="3" MinWidth="140" Text="查看许可文档" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left" 
                     <local:MyButton Grid.Row="7" Grid.Column="3" MinWidth="140" Text="查看许可文档" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left" 
                                     local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/naudio/NAudio/blob/f98664ba3d5966a736dd5cf45329e542be5fece8/license.txt" />
                                     local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/naudio/NAudio/blob/f98664ba3d5966a736dd5cf45329e542be5fece8/license.txt" />
 
 
                     <TextBlock Grid.Row="8" Grid.Column="0" Text="Ookii.Dialogs" TextWrapping="Wrap" FontWeight="Bold" />
                     <TextBlock Grid.Row="8" Grid.Column="0" Text="Ookii.Dialogs" TextWrapping="Wrap" FontWeight="Bold" />
-                    <TextBlock Grid.Row="8" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © 2009 Sven Groot (Ookii.org)&#xa;Licensed under BSD 3-Clause License." TextWrapping="Wrap" />
+                    <TextBlock Grid.Row="8" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © 2009 Sven Groot (Ookii.org)&#xa;Licensed under the BSD 3-Clause License." TextWrapping="Wrap" />
                     <local:MyButton Grid.Row="9" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
                     <local:MyButton Grid.Row="9" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
                                     local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://www.ookii.org/Software/Dialogs/" />
                                     local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://www.ookii.org/Software/Dialogs/" />
                     <local:MyButton Grid.Row="9" Grid.Column="3" MinWidth="140" Text="查看许可文档" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
                     <local:MyButton Grid.Row="9" Grid.Column="3" MinWidth="140" Text="查看许可文档" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
@@ -629,29 +874,36 @@
 
 
                     <TextBlock Grid.Row="14" Grid.Column="0" Text="Iconfont" TextWrapping="Wrap" FontWeight="Bold" />
                     <TextBlock Grid.Row="14" Grid.Column="0" Text="Iconfont" TextWrapping="Wrap" FontWeight="Bold" />
                     <TextBlock Grid.Row="14" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © ALIMAMA MUX, powered by alimama THX." TextWrapping="Wrap" />
                     <TextBlock Grid.Row="14" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © ALIMAMA MUX, powered by alimama THX." TextWrapping="Wrap" />
-                    <local:MyButton Grid.Row="15" Grid.Column="2" MinWidth="140" Text="查看原网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
+                    <local:MyButton Grid.Row="15" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
                                     local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://www.iconfont.cn/" />
                                     local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://www.iconfont.cn/" />
 
 
-                    <TextBlock Grid.Row="16" Grid.Column="0" Text="EasyTier" TextWrapping="Wrap" FontWeight="Bold" />
+                    <!--<TextBlock Grid.Row="16" Grid.Column="0" Text="EasyTier" TextWrapping="Wrap" FontWeight="Bold" />
                     <TextBlock Grid.Row="16" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © 2024-present Easytier Programme within The Commons Conservancy&#xa;Licensed under the GNU Lesser General Public License." TextWrapping="Wrap" />
                     <TextBlock Grid.Row="16" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © 2024-present Easytier Programme within The Commons Conservancy&#xa;Licensed under the GNU Lesser General Public License." TextWrapping="Wrap" />
                     <local:MyButton Grid.Row="17" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
                     <local:MyButton Grid.Row="17" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
                                     local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://easytier.cn/" />
                                     local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://easytier.cn/" />
                     <local:MyButton Grid.Row="17" Grid.Column="3" MinWidth="140" Text="查看许可文档" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
                     <local:MyButton Grid.Row="17" Grid.Column="3" MinWidth="140" Text="查看许可文档" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
-                                    local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/EasyTier/EasyTier/blob/main/LICENSE" />
+                                    local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/EasyTier/EasyTier/blob/main/LICENSE" />-->
 
 
-                    <TextBlock Grid.Row="18" Grid.Column="0" Text="ThrottleDebounce" TextWrapping="Wrap" FontWeight="Bold" />
-                    <TextBlock Grid.Row="18" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © 2026 Ben Hutchison&#xa;Licensed under the Apache License, Version 2.0." TextWrapping="Wrap" />
-                    <local:MyButton Grid.Row="19" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
+                    <TextBlock Grid.Row="16" Grid.Column="0" Text="ThrottleDebounce" TextWrapping="Wrap" FontWeight="Bold" />
+                    <TextBlock Grid.Row="16" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © 2026 Ben Hutchison&#xa;Licensed under the Apache License, Version 2.0." TextWrapping="Wrap" />
+                    <local:MyButton Grid.Row="17" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
                                     local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/Aldaviva/ThrottleDebounce" />
                                     local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/Aldaviva/ThrottleDebounce" />
-                    <local:MyButton Grid.Row="19" Grid.Column="3" MinWidth="140" Text="查看许可文档" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
+                    <local:MyButton Grid.Row="17" Grid.Column="3" MinWidth="140" Text="查看许可文档" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
                                     local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://licenses.nuget.org/Apache-2.0" />
                                     local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://licenses.nuget.org/Apache-2.0" />
 
 
-                    <TextBlock Grid.Row="20" Grid.Column="0" Text="CacheCow" TextWrapping="Wrap" FontWeight="Bold" />
-                    <TextBlock Grid.Row="20" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © Ali Kheyrollahi&#xa;Licensed under the MIT License." TextWrapping="Wrap" />
-                    <local:MyButton Grid.Row="21" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
+                    <TextBlock Grid.Row="18" Grid.Column="0" Text="CacheCow" TextWrapping="Wrap" FontWeight="Bold" />
+                    <TextBlock Grid.Row="18" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © Ali Kheyrollahi&#xa;Licensed under the MIT License." TextWrapping="Wrap" />
+                    <local:MyButton Grid.Row="19" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
                                     local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/aliostad/CacheCow" />
                                     local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/aliostad/CacheCow" />
-                    <local:MyButton Grid.Row="21" Grid.Column="3" MinWidth="140" Text="查看许可文档" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
+                    <local:MyButton Grid.Row="19" Grid.Column="3" MinWidth="140" Text="查看许可文档" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
                                     local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://licenses.nuget.org/MIT" />
                                     local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://licenses.nuget.org/MIT" />
+
+                    <TextBlock Grid.Row="20" Grid.Column="0" Text="Transformer Discovery Service" TextWrapping="Wrap" FontWeight="Bold" />
+                    <TextBlock Grid.Row="20" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © 2020 huangyuhui &lt;[email protected]&gt; and contributors&#xa;Licensed under the GNU General Public License, Version 3 or later, with additional terms.&#xa;本组件作为独立组件分发,可依 GPL 复制、修改及再分发。" TextWrapping="Wrap" />
+                    <local:MyButton Grid.Row="21" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
+                                    local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/HMCL-dev/HMCL/tree/main/minecraft/libraries/HMCLTransformerDiscoveryService" />
+                    <local:MyButton Grid.Row="21" Grid.Column="3" MinWidth="140" Text="查看许可文档" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
+                                    local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/HMCL-dev/HMCL/blob/main/LICENSE" />
                 </Grid>
                 </Grid>
             </local:MyCard>
             </local:MyCard>
         </StackPanel>
         </StackPanel>

+ 4 - 3
Plain Craft Launcher 2/Pages/PageOther/PageOtherHelp.xaml.vb

@@ -48,12 +48,13 @@ Public Class PageOtherHelp
                     If Item.Types.Contains(Type) Then TypeItems.Add(Item)
                     If Item.Types.Contains(Type) Then TypeItems.Add(Item)
                 Next
                 Next
                 '增加卡片
                 '增加卡片
-                Dim NewCard As New MyCard With {.Title = Type, .Margin = New Thickness(0, 0, 0, 15), .SwapType = 11}
-                Dim NewStack As New StackPanel With {.Margin = New Thickness(20, MyCard.SwapedHeight, 18, 0), .VerticalAlignment = VerticalAlignment.Top, .RenderTransform = New TranslateTransform(0, 0), .Tag = TypeItems}
+                Dim NewCard As New MyCard With {.Title = Type, .Margin = New Thickness(0, 0, 0, 15)}
+                Dim NewStack As New StackPanel With {.Margin = New Thickness(20, MyCard.SwapedHeight, 18, 0), .VerticalAlignment = VerticalAlignment.Top, .RenderTransform = New TranslateTransform(0, 0)}
                 NewCard.Children.Add(NewStack)
                 NewCard.Children.Add(NewStack)
                 NewCard.SwapControl = NewStack
                 NewCard.SwapControl = NewStack
+                NewCard.SetContentBuilder(TypeItems, Function(Entry) Entry.ToListItem)
                 If Type = "指南" Then
                 If Type = "指南" Then
-                    MyCard.StackInstall(NewStack, 11, "指南")
+                    NewCard.BuildContent()
                 Else
                 Else
                     NewCard.IsSwapped = True
                     NewCard.IsSwapped = True
                 End If
                 End If

+ 29 - 18
Plain Craft Launcher 2/Pages/PageSelectLeft.xaml.vb

@@ -64,7 +64,7 @@ Public Class PageSelectLeft
                                 </ContextMenu>
                                 </ContextMenu>
                         )
                         )
                 End Select
                 End Select
-                If (Folder.Type = McFolder.Types.Vanilla OrElse Folder.Type = McFolder.Types.RenamedVanilla) AndAlso Folder.Location = Paths.Base & ".minecraft\" AndAlso McFolderList.IsSingle Then CType(ContMenu.FindName("Delete"), MyMenuItem).Header = "清空"
+                If (Folder.Type = McFolder.Types.Vanilla OrElse Folder.Type = McFolder.Types.RenamedVanilla) AndAlso PathUtils.IsSame(Folder.Location, Paths.Base & ".minecraft\") AndAlso McFolderList.IsSingle Then CType(ContMenu.FindName("Delete"), MyMenuItem).Header = "清空"
                 '注册事件
                 '注册事件
                 If Not Folder.Type = McFolder.Types.Vanilla Then CType(ContMenu.FindName("Remove"), MyMenuItem).AddHandler(MyMenuItem.ClickEvent, New RoutedEventHandler(AddressOf FrmSelectLeft.Remove_Click))
                 If Not Folder.Type = McFolder.Types.Vanilla Then CType(ContMenu.FindName("Remove"), MyMenuItem).AddHandler(MyMenuItem.ClickEvent, New RoutedEventHandler(AddressOf FrmSelectLeft.Remove_Click))
                 CType(ContMenu.FindName("Open"), MyMenuItem).AddHandler(MyMenuItem.ClickEvent, New RoutedEventHandler(AddressOf FrmSelectLeft.Open_Click))
                 CType(ContMenu.FindName("Open"), MyMenuItem).AddHandler(MyMenuItem.ClickEvent, New RoutedEventHandler(AddressOf FrmSelectLeft.Open_Click))
@@ -116,7 +116,7 @@ Public Class PageSelectLeft
 
 
             '确认勾选状态
             '确认勾选状态
             For i = 0 To McFolderList.Count - 1
             For i = 0 To McFolderList.Count - 1
-                If McFolderList(i).Location = McFolderSelected Then
+                If PathUtils.IsSame(McFolderList(i).Location, McFolderSelected) Then
                     CType(FrmSelectLeft.PanList.Children(i + 1), MyListItem).Checked = True '去掉第一个标题
                     CType(FrmSelectLeft.PanList.Children(i + 1), MyListItem).Checked = True '去掉第一个标题
                     Return
                     Return
                 End If
                 End If
@@ -139,9 +139,9 @@ Public Class PageSelectLeft
     '添加文件夹
     '添加文件夹
     Public Sub Add_Click()
     Public Sub Add_Click()
         Dim NewFolder As String = ""
         Dim NewFolder As String = ""
-        '检查是否有下载任务
-        If HasDownloadingTask() Then
-            Hint("在下载任务进行时,无法添加游戏文件夹!", HintType.Red)
+        '下载或版本修改期间禁止更改游戏文件夹
+        If McInstanceLock.IsAnyLocked OrElse HasDownloadingTask() Then
+            Hint("请在其他任务完成后,再添加游戏文件夹!", HintType.Red)
             Return
             Return
         End If
         End If
         Try
         Try
@@ -150,8 +150,11 @@ Public Class PageSelectLeft
             If NewFolder Is Nothing Then Return
             If NewFolder Is Nothing Then Return
             If NewFolder.Contains("!") OrElse NewFolder.Contains(";") Then Hint("Minecraft 文件夹路径中不能含有感叹号或分号!", HintType.Red) : Return
             If NewFolder.Contains("!") OrElse NewFolder.Contains(";") Then Hint("Minecraft 文件夹路径中不能含有感叹号或分号!", HintType.Red) : Return
             '要求输入显示名称
             '要求输入显示名称
-            Dim SplitedNames As String() = NewFolder.TrimEnd("\").Split("\")
-            Dim DefaultName As String = If(SplitedNames.Last = ".minecraft", If(SplitedNames.Count >= 3, SplitedNames(SplitedNames.Count - 2), ""), SplitedNames.Last)
+            Dim DefaultName = PathUtils.GetLastPart(NewFolder)
+            If DefaultName = ".minecraft" Then
+                Dim ParentFolder = PathUtils.RemoveLastPart(NewFolder)
+                DefaultName = If(ParentFolder.EndsWithF(":"), "", PathUtils.GetLastPart(ParentFolder))
+            End If
             If DefaultName.Length > 40 Then DefaultName = DefaultName.Substring(0, 39)
             If DefaultName.Length > 40 Then DefaultName = DefaultName.Substring(0, 39)
             Dim NewName As String = MyMsgBoxInput("输入显示名称", "输入该文件夹在左边栏列表中显示的名称。", DefaultName,
             Dim NewName As String = MyMsgBoxInput("输入显示名称", "输入该文件夹在左边栏列表中显示的名称。", DefaultName,
                                               New ObjectModel.Collection(Of Validate) From {New ValidateNullOrWhiteSpace, New ValidateLength(1, 30), New ValidateExcept({">", "|"})})
                                               New ObjectModel.Collection(Of Validate) From {New ValidateNullOrWhiteSpace, New ValidateLength(1, 30), New ValidateExcept({">", "|"})})
@@ -169,7 +172,7 @@ Public Class PageSelectLeft
         RunInThread(
         RunInThread(
         Sub()
         Sub()
             Try
             Try
-                If Not FolderPath.EndsWithF("\") Then FolderPath &= "\"
+                FolderPath = PathUtils.AddSlashSuffix(FolderPath)
                 '检查文件夹权限
                 '检查文件夹权限
                 If Not CheckPermission(FolderPath) Then
                 If Not CheckPermission(FolderPath) Then
                     If ShowHint Then
                     If ShowHint Then
@@ -195,7 +198,7 @@ Public Class PageSelectLeft
                 For i = 0 To Folders.Count - 1
                 For i = 0 To Folders.Count - 1
                     Dim Folder As String = Folders(i)
                     Dim Folder As String = Folders(i)
                     If Folder = "" Then Continue For
                     If Folder = "" Then Continue For
-                    If Folder.Split(">")(1) = FolderPath Then
+                    If PathUtils.IsSame(Folder.Split(">")(1), FolderPath) Then
                         IsAdded = True
                         IsAdded = True
                         If Folder.Split(">")(0) = DisplayName Then
                         If Folder.Split(">")(0) = DisplayName Then
                             If ShowHint Then Hint("此文件夹已在列表中!", HintType.Blue)
                             If ShowHint Then Hint("此文件夹已在列表中!", HintType.Blue)
@@ -246,9 +249,9 @@ Public Class PageSelectLeft
 
 
     '创建文件夹
     '创建文件夹
     Public Sub Create_Click()
     Public Sub Create_Click()
-        '检查是否有下载任务
-        If HasDownloadingTask() Then
-            Hint("在下载任务进行时,无法创建游戏文件夹!", HintType.Red)
+        '下载或版本修改期间禁止更改游戏文件夹
+        If McInstanceLock.IsAnyLocked OrElse HasDownloadingTask() Then
+            Hint("请在其他任务完成后,再创建游戏文件夹!", HintType.Red)
             Return
             Return
         End If
         End If
         CreateMcFolderInCurrentPath()
         CreateMcFolderInCurrentPath()
@@ -270,6 +273,10 @@ Public Class PageSelectLeft
             Dim Target As McFolder = CType(CType(CType(sender.Parent, ContextMenu).Parent, Primitives.Popup).PlacementTarget, MyListItem).Tag
             Dim Target As McFolder = CType(CType(CType(sender.Parent, ContextMenu).Parent, Primitives.Popup).PlacementTarget, MyListItem).Tag
             '若为 “移除”,则提醒是否删除 PCL 的配置文件
             '若为 “移除”,则提醒是否删除 PCL 的配置文件
             If Target.Type = McFolder.Types.Custom Then
             If Target.Type = McFolder.Types.Custom Then
+                If McInstanceLock.IsAnyLocked OrElse HasDownloadingTask() Then
+                    Hint("请在其他任务完成后,再移除游戏文件夹!", HintType.Red)
+                    Return
+                End If
                 Select Case MyMsgBox("是否需要清理 PCL 在该文件夹中的配置文件?" & vbCrLf & "这包括各个版本的独立设置(如自定义图标、第三方登录配置)等,对游戏本身没有影响。", "配置文件清理", "删除", "保留", "取消")
                 Select Case MyMsgBox("是否需要清理 PCL 在该文件夹中的配置文件?" & vbCrLf & "这包括各个版本的独立设置(如自定义图标、第三方登录配置)等,对游戏本身没有影响。", "配置文件清理", "删除", "保留", "取消")
                     Case 1
                     Case 1
                         '删除配置文件
                         '删除配置文件
@@ -292,6 +299,10 @@ Public Class PageSelectLeft
         End Try
         End Try
     End Sub
     End Sub
     Public Sub Delete_Click(sender As Object, e As RoutedEventArgs)
     Public Sub Delete_Click(sender As Object, e As RoutedEventArgs)
+        If McInstanceLock.IsAnyLocked OrElse HasDownloadingTask() Then
+            Hint("请在其他任务完成后,再删除游戏文件夹!", HintType.Red)
+            Return
+        End If
         Dim Target As McFolder = CType(CType(CType(sender.Parent, ContextMenu).Parent, Primitives.Popup).PlacementTarget, MyListItem).Tag
         Dim Target As McFolder = CType(CType(CType(sender.Parent, ContextMenu).Parent, Primitives.Popup).PlacementTarget, MyListItem).Tag
         Dim DeleteText As String = If((Target.Type = McFolder.Types.Vanilla OrElse Target.Type = McFolder.Types.RenamedVanilla) AndAlso Target.Location = Paths.Base & ".minecraft\" AndAlso McFolderList.IsSingle, "清空", "删除")
         Dim DeleteText As String = If((Target.Type = McFolder.Types.Vanilla OrElse Target.Type = McFolder.Types.RenamedVanilla) AndAlso Target.Location = Paths.Base & ".minecraft\" AndAlso McFolderList.IsSingle, "清空", "删除")
         If MyMsgBox("你确定要" & DeleteText & "这个文件夹吗?" & vbCrLf &
         If MyMsgBox("你确定要" & DeleteText & "这个文件夹吗?" & vbCrLf &
@@ -328,7 +339,7 @@ Public Class PageSelectLeft
         Dim DeletedName As String = Nothing
         Dim DeletedName As String = Nothing
         Dim Folders = Settings.Get(Of String)("LaunchFolders").Split("|"c, True).ToList
         Dim Folders = Settings.Get(Of String)("LaunchFolders").Split("|"c, True).ToList
         For Each Folder In Folders.ToList
         For Each Folder In Folders.ToList
-            If Not Folder.EndsWithF(Target.Location) Then Continue For
+            If Not Folder.Contains(">") OrElse Not PathUtils.IsSame(Folder.AfterFirst(">"), Target.Location) Then Continue For
             DeletedName = Folder.BeforeFirst(">")
             DeletedName = Folder.BeforeFirst(">")
             Folders.Remove(Folder)
             Folders.Remove(Folder)
             Exit For
             Exit For
@@ -349,7 +360,7 @@ Public Class PageSelectLeft
     End Sub
     End Sub
     Public Shared Sub RefreshCurrent(Folder As String)
     Public Shared Sub RefreshCurrent(Folder As String)
         WriteIni(Folder & "PCL.ini", "InstanceCache", "") '删除缓存以强制要求下一次加载时更新列表
         WriteIni(Folder & "PCL.ini", "InstanceCache", "") '删除缓存以强制要求下一次加载时更新列表
-        If Folder = McFolderSelected Then LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
+        If PathUtils.IsSame(Folder, McFolderSelected) Then LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
     End Sub
     End Sub
     Public Sub Rename_Click(sender As Object, e As RoutedEventArgs)
     Public Sub Rename_Click(sender As Object, e As RoutedEventArgs)
         Dim Folder As McFolder = CType(CType(CType(sender.Parent, ContextMenu).Parent, Primitives.Popup).PlacementTarget, MyListItem).Tag
         Dim Folder As McFolder = CType(CType(CType(sender.Parent, ContextMenu).Parent, Primitives.Popup).PlacementTarget, MyListItem).Tag
@@ -365,7 +376,7 @@ Public Class PageSelectLeft
             For i = 0 To Folders.Count - 1
             For i = 0 To Folders.Count - 1
                 Dim FolderCurrent As String = Folders(i)
                 Dim FolderCurrent As String = Folders(i)
                 If FolderCurrent = "" Then Continue For
                 If FolderCurrent = "" Then Continue For
-                If FolderCurrent.Split(">")(1) = Folder.Location Then
+                If PathUtils.IsSame(FolderCurrent.Split(">")(1), Folder.Location) Then
                     IsAdded = True
                     IsAdded = True
                     If FolderCurrent.Split(">")(0) = NewName Then
                     If FolderCurrent.Split(">")(0) = NewName Then
                         '名称未修改
                         '名称未修改
@@ -390,9 +401,9 @@ Public Class PageSelectLeft
     '点击选项
     '点击选项
     Public Sub Folder_Change(sender As MyListItem, e As RouteEventArgs)
     Public Sub Folder_Change(sender As MyListItem, e As RouteEventArgs)
         If Not e.RaiseByMouse OrElse Not sender.Checked Then Return
         If Not e.RaiseByMouse OrElse Not sender.Checked Then Return
-        '检查是否有下载任务
-        If HasDownloadingTask(True) Then
-            Hint("在下载任务进行时,无法切换游戏文件夹!", HintType.Red)
+        '下载或版本修改期间禁止更改游戏文件夹
+        If McInstanceLock.IsAnyLocked OrElse HasDownloadingTask(True) Then
+            Hint("请在其他任务完成后,再切换游戏文件夹!", HintType.Red)
             e.Handled = True
             e.Handled = True
             Return
             Return
         End If
         End If

+ 29 - 17
Plain Craft Launcher 2/Pages/PageSelectRight.xaml.vb

@@ -30,6 +30,8 @@ Public Class PageSelectRight
             For Each Card As KeyValuePair(Of McInstanceCardType, List(Of McInstance)) In McInstanceList.ToArray
             For Each Card As KeyValuePair(Of McInstanceCardType, List(Of McInstance)) In McInstanceList.ToArray
                 '确认是否为隐藏版本显示状态
                 '确认是否为隐藏版本显示状态
                 If Card.Key = McInstanceCardType.Hidden Xor ShowHidden Then Continue For
                 If Card.Key = McInstanceCardType.Hidden Xor ShowHidden Then Continue For
+                Dim Instances = Card.Value.Where(Function(Instance) Not McInstanceLock.IsLocked(Instance)).ToList
+                If Not Instances.Any Then Continue For
 #Region "确认卡片名称"
 #Region "确认卡片名称"
                 Dim CardName As String = ""
                 Dim CardName As String = ""
                 Select Case Card.Key
                 Select Case Card.Key
@@ -40,11 +42,11 @@ Public Class PageSelectRight
                         Dim IsNeoForgeExists As Boolean = False
                         Dim IsNeoForgeExists As Boolean = False
                         Dim IsFabricExists As Boolean = False
                         Dim IsFabricExists As Boolean = False
                         Dim IsLiteExists As Boolean = False
                         Dim IsLiteExists As Boolean = False
-                        For Each Instance As McInstance In Card.Value
-                            If Instance.Version.HasFabric Then IsFabricExists = True
-                            If Instance.Version.HasLiteLoader Then IsLiteExists = True
-                            If Instance.Version.HasForge Then IsForgeExists = True
-                            If Instance.Version.HasNeoForge Then IsNeoForgeExists = True
+                        For Each Instance As McInstance In Instances
+                            If Instance.Version.ModLoaders.HasFlagF(ModLoaders.Fabric) Then IsFabricExists = True
+                            If Instance.Version.ModLoaders.HasFlagF(ModLoaders.LiteLoader) Then IsLiteExists = True
+                            If Instance.Version.ModLoaders.HasFlagF(ModLoaders.Forge) Then IsForgeExists = True
+                            If Instance.Version.ModLoaders.HasFlagF(ModLoaders.NeoForge) Then IsNeoForgeExists = True
                         Next
                         Next
                         If If(IsLiteExists, 1, 0) + If(IsForgeExists, 1, 0) + If(IsFabricExists, 1, 0) + If(IsNeoForgeExists, 1, 0) > 1 Then
                         If If(IsLiteExists, 1, 0) + If(IsForgeExists, 1, 0) + If(IsFabricExists, 1, 0) + If(IsNeoForgeExists, 1, 0) > 1 Then
                             CardName = "可安装 Mod"
                             CardName = "可安装 Mod"
@@ -72,17 +74,18 @@ Public Class PageSelectRight
                 End Select
                 End Select
 #End Region
 #End Region
                 '建立控件
                 '建立控件
-                Dim CardTitle As String = CardName & If(CardName = "收藏夹", "", " (" & Card.Value.Count & ")")
-                Dim NewCard As New MyCard With {.Title = CardTitle, .Margin = New Thickness(0, 0, 0, 15), .SwapType = 0}
-                Dim NewStack As New StackPanel With {.Margin = New Thickness(20, MyCard.SwapedHeight, 18, 0), .VerticalAlignment = VerticalAlignment.Top, .RenderTransform = New TranslateTransform(0, 0), .Tag = Card.Value}
+                Dim CardTitle As String = CardName & If(CardName = "收藏夹", "", " (" & Instances.Count & ")")
+                Dim NewCard As New MyCard With {.Title = CardTitle, .Margin = New Thickness(0, 0, 0, 15)}
+                Dim NewStack As New StackPanel With {.Margin = New Thickness(20, MyCard.SwapedHeight, 18, 0), .VerticalAlignment = VerticalAlignment.Top, .RenderTransform = New TranslateTransform(0, 0)}
                 NewCard.Children.Add(NewStack)
                 NewCard.Children.Add(NewStack)
                 NewCard.SwapControl = NewStack
                 NewCard.SwapControl = NewStack
+                NewCard.SetContentBuilder(Instances, Function(Instance) Instance.ToListItem(AddressOf McInstanceListContent))
                 PanMain.Children.Add(NewCard)
                 PanMain.Children.Add(NewCard)
                 '确定卡片是否展开
                 '确定卡片是否展开
                 If Card.Key = McInstanceCardType.Rubbish OrElse Card.Key = McInstanceCardType.Error OrElse Card.Key = McInstanceCardType.Fool Then
                 If Card.Key = McInstanceCardType.Rubbish OrElse Card.Key = McInstanceCardType.Error OrElse Card.Key = McInstanceCardType.Fool Then
                     NewCard.IsSwapped = True
                     NewCard.IsSwapped = True
                 Else
                 Else
-                    MyCard.StackInstall(NewStack, 0, CardTitle)
+                    NewCard.BuildContent()
                 End If
                 End If
             Next
             Next
 
 
@@ -128,15 +131,16 @@ Public Class PageSelectRight
             BtnStar.LogoScale = 1.1
             BtnStar.LogoScale = 1.1
             BtnStar.Logo = Logo.IconButtonLikeLine
             BtnStar.Logo = Logo.IconButtonLikeLine
         End If
         End If
-        AddHandler BtnStar.Click, Sub()
-                                      WriteIni(Instance.PathVersion & "PCL\Setup.ini", "IsStar", Not Instance.IsStar)
-                                      McInstanceListForceRefresh = True
-                                      LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
-                                  End Sub
+        AddHandler BtnStar.Click,
+        Sub()
+            WriteIni(Instance.PathVersion & "PCL\Setup.ini", "IsStar", Not Instance.IsStar)
+            McInstanceListForceRefresh = True
+            LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
+        End Sub
         Dim BtnDel As New MyIconButton With {.LogoScale = 1.1, .Logo = Logo.IconButtonDelete}
         Dim BtnDel As New MyIconButton With {.LogoScale = 1.1, .Logo = Logo.IconButtonDelete}
         BtnDel.ToolTip = "删除"
         BtnDel.ToolTip = "删除"
         AddHandler BtnDel.Click, Sub() DeleteInstance(sender, Instance)
         AddHandler BtnDel.Click, Sub() DeleteInstance(sender, Instance)
-        If Instance.State <> McInstanceState.Error Then
+        If Instance.State <> McInstance.McInstanceState.Error Then
             Dim BtnCont As New MyIconButton With {.LogoScale = 1.1, .Logo = Logo.IconButtonSetup}
             Dim BtnCont As New MyIconButton With {.LogoScale = 1.1, .Logo = Logo.IconButtonSetup}
             BtnCont.ToolTip = "设置"
             BtnCont.ToolTip = "设置"
             AddHandler BtnCont.Click,
             AddHandler BtnCont.Click,
@@ -166,6 +170,10 @@ Public Class PageSelectRight
     '点击选项
     '点击选项
     Public Shared Sub Item_Click(sender As MyListItem, e As EventArgs)
     Public Shared Sub Item_Click(sender As MyListItem, e As EventArgs)
         Dim Instance As McInstance = sender.Tag
         Dim Instance As McInstance = sender.Tag
+        If McInstanceLock.IsLocked(Instance) Then
+            Hint("该版本正在被修改,请稍后再选择!", HintType.Red)
+            Return
+        End If
         If New McInstance(Instance.PathVersion).Check Then
         If New McInstance(Instance.PathVersion).Check Then
             '正常版本
             '正常版本
             McInstanceSelected = Instance
             McInstanceSelected = Instance
@@ -183,8 +191,12 @@ Public Class PageSelectRight
     '修改此代码时,同时修改 PageInstanceOverall 中的代码
     '修改此代码时,同时修改 PageInstanceOverall 中的代码
     Public Shared Sub DeleteInstance(Item As MyListItem, Instance As McInstance)
     Public Shared Sub DeleteInstance(Item As MyListItem, Instance As McInstance)
         Try
         Try
+            If McInstanceLock.IsLocked(Instance) Then
+                Hint("该版本正在被修改,请稍后再删除!", HintType.Red)
+                Return
+            End If
             Dim IsShiftPressed As Boolean = My.Computer.Keyboard.ShiftKeyDown
             Dim IsShiftPressed As Boolean = My.Computer.Keyboard.ShiftKeyDown
-            Dim IsHintIndie As Boolean = Instance.State <> McInstanceState.Error AndAlso Instance.PathIndie <> McFolderSelected
+            Dim IsHintIndie As Boolean = Instance.State <> McInstance.McInstanceState.Error AndAlso Instance.PathIndie <> McFolderSelected
             Dim SavesFolder = DirectoryUtils.GetInfo(Instance.PathIndie & "saves\")
             Dim SavesFolder = DirectoryUtils.GetInfo(Instance.PathIndie & "saves\")
             Dim SaveEntries = If(IsHintIndie AndAlso SavesFolder.Exists, SavesFolder.EnumerateDirectories.OrderByDescending(Function(Save) Save.LastWriteTime).Select(Function(Save) $"{Save.Name}(上次修改:{StringUtils.FormatTimeSpan(Save.LastWriteTime - Date.Now, False)})").ToList, New List(Of String))
             Dim SaveEntries = If(IsHintIndie AndAlso SavesFolder.Exists, SavesFolder.EnumerateDirectories.OrderByDescending(Function(Save) Save.LastWriteTime).Select(Function(Save) $"{Save.Name}(上次修改:{StringUtils.FormatTimeSpan(Save.LastWriteTime - Date.Now, False)})").ToList, New List(Of String))
             Select Case MyMsgBox($"你确定要{If(IsShiftPressed, "永久", "")}删除版本 {Instance.Name} 吗?" &
             Select Case MyMsgBox($"你确定要{If(IsShiftPressed, "永久", "")}删除版本 {Instance.Name} 吗?" &
@@ -207,7 +219,7 @@ Public Class PageSelectRight
                     Dim Card As MyCard = Parent.Parent
                     Dim Card As MyCard = Parent.Parent
                     Card.Title = Card.Title.Replace(Parent.Children.Count - 1, Parent.Children.Count - 2) '有一个占位符
                     Card.Title = Card.Title.Replace(Parent.Children.Count - 1, Parent.Children.Count - 2) '有一个占位符
                     Parent.Children.Remove(Item)
                     Parent.Children.Remove(Item)
-                    If McInstanceSelected IsNot Nothing AndAlso Instance.PathVersion = McInstanceSelected.PathVersion Then
+                    If McInstanceSelected IsNot Nothing AndAlso PathUtils.IsSame(Instance.PathVersion, McInstanceSelected.PathVersion) Then
                         '删除当前版本就更改选择
                         '删除当前版本就更改选择
                         McInstanceSelected = MyVirtualizingElement.TryInit(Parent.Children(0)).Tag
                         McInstanceSelected = MyVirtualizingElement.TryInit(Parent.Children(0)).Tag
                     End If
                     End If

+ 1 - 5
Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml

@@ -27,8 +27,6 @@
                             <RowDefinition Height="28" />
                             <RowDefinition Height="28" />
                             <RowDefinition Height="9" />
                             <RowDefinition Height="9" />
                             <RowDefinition Height="28" />
                             <RowDefinition Height="28" />
-                            <RowDefinition Height="9" />
-                            <RowDefinition Height="Auto" />
                         </Grid.RowDefinitions>
                         </Grid.RowDefinitions>
                         <TextBlock Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Left" Text="默认版本隔离" Margin="0,0,25,0" />
                         <TextBlock Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Left" Text="默认版本隔离" Margin="0,0,25,0" />
                         <local:MyComboBox local:SettingService.Key="LaunchArgumentIndieV2" x:Name="ComboArgumentIndieV2"
                         <local:MyComboBox local:SettingService.Key="LaunchArgumentIndieV2" x:Name="ComboArgumentIndieV2"
@@ -106,9 +104,6 @@
                                 </local:MyTextBox.ValidateRules>
                                 </local:MyTextBox.ValidateRules>
                             </local:MyTextBox>
                             </local:MyTextBox>
                         </Grid>
                         </Grid>
-                        <local:MyCheckBox Margin="0,8,0,4" Text="在正版登录时验证 SSL 证书" Grid.Row="12" Height="22" Grid.ColumnSpan="2"
-                                          x:Name="CheckDownloadCert" local:SettingService.Key="ToolDownloadCert"
-                                          ToolTip="开启验证可以提高安全性,但可能会导致正版登录失败(见 #3018)。&#xa;若登录因为 “未能为 SSL/TLS 安全通道建立信任关系” 失败,请考虑关闭此选项。" />
                     </Grid>
                     </Grid>
                 </StackPanel>
                 </StackPanel>
             </local:MyCard>
             </local:MyCard>
@@ -291,6 +286,7 @@
                         <local:MyCheckBox Height="28" Text="禁用 Java Launch Wrapper" local:SettingService.Key="LaunchAdvanceDisableJLW" ToolTip="是否使用 Java Launch Wrapper 修复 Java 18- 在中文路径下可能无法正常启动的问题。&#xa;详见:https://github.com/00ll00/java_launch_wrapper" />
                         <local:MyCheckBox Height="28" Text="禁用 Java Launch Wrapper" local:SettingService.Key="LaunchAdvanceDisableJLW" ToolTip="是否使用 Java Launch Wrapper 修复 Java 18- 在中文路径下可能无法正常启动的问题。&#xa;详见:https://github.com/00ll00/java_launch_wrapper" />
                         <local:MyCheckBox Height="28" Text="禁用 LWJGL Unsafe Agent" local:SettingService.Key="LaunchAdvanceDisableLUA" ToolTip="是否使用 LWJGL Unsafe Agent 修复 LWJGL 3.4.1 的一个性能问题。&#xa;详见:https://github.com/HMCL-dev/lwjgl-unsafe-agent" />
                         <local:MyCheckBox Height="28" Text="禁用 LWJGL Unsafe Agent" local:SettingService.Key="LaunchAdvanceDisableLUA" ToolTip="是否使用 LWJGL Unsafe Agent 修复 LWJGL 3.4.1 的一个性能问题。&#xa;详见:https://github.com/HMCL-dev/lwjgl-unsafe-agent" />
                         <local:MyCheckBox Height="28" Text="使用高性能显卡" local:SettingService.Key="LaunchAdvanceGraphicCard" ToolTip="自动在 Windows 设置 → 系统 → 屏幕 → 显示卡 中将 PCL 和 Java 改为使用高性能显卡。&#xa;如果同时有集成显卡和独立显卡,这能确保 MC 使用独立显卡进行渲染,以大幅提高帧率。" />
                         <local:MyCheckBox Height="28" Text="使用高性能显卡" local:SettingService.Key="LaunchAdvanceGraphicCard" ToolTip="自动在 Windows 设置 → 系统 → 屏幕 → 显示卡 中将 PCL 和 Java 改为使用高性能显卡。&#xa;如果同时有集成显卡和独立显卡,这能确保 MC 使用独立显卡进行渲染,以大幅提高帧率。" />
+                        <local:MyCheckBox Height="28" Text="在正版登录时验证 SSL 证书" local:SettingService.Key="ToolDownloadCert" ToolTip="开启验证可以提高安全性,但可能会导致正版登录失败(见 #3018)。&#xa;若登录因为 “未能为 SSL/TLS 安全通道建立信任关系” 失败,请考虑关闭此选项。" />
                     </StackPanel>
                     </StackPanel>
                     <local:MyHint Text="版本独立设置中还有更多高级选项可供调整。" Margin="0,5,0,10" Theme="Blue" Grid.Row="11" Grid.ColumnSpan="2"
                     <local:MyHint Text="版本独立设置中还有更多高级选项可供调整。" Margin="0,5,0,10" Theme="Blue" Grid.Row="11" Grid.ColumnSpan="2"
                                   CanClose="True" RelativeSetup="HintMoreAdvancedSetup" />
                                   CanClose="True" RelativeSetup="HintMoreAdvancedSetup" />

+ 1 - 1
Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml.vb

@@ -396,7 +396,7 @@ PreFin:
                 AddHandler DownButton.Click, Sub() MoveJavaInList(JavaEntry, 1)
                 AddHandler DownButton.Click, Sub() MoveJavaInList(JavaEntry, 1)
                 Buttons.Add(DownButton)
                 Buttons.Add(DownButton)
                 '从列表中移除按钮
                 '从列表中移除按钮
-                Dim IsOfficial As Boolean = JavaEntry.Folder.StartsWithF($"{Paths.AppData}.minecraft\runtime\")
+                Dim IsOfficial As Boolean = PathUtils.IsParentOf($"{Paths.AppData}.minecraft\runtime\", JavaEntry.Folder)
                 Dim DeleteButton As New MyIconButton With {.Logo = Logo.IconButtonStop, .Height = 24, .Width = 24}
                 Dim DeleteButton As New MyIconButton With {.Logo = Logo.IconButtonStop, .Height = 24, .Width = 24}
                 DeleteButton.IsEnabled = Not IsOfficial
                 DeleteButton.IsEnabled = Not IsOfficial
                 DeleteButton.ToolTip = If(DeleteButton.IsEnabled, "从列表中移除", "无法移除官方 Java")
                 DeleteButton.ToolTip = If(DeleteButton.IsEnabled, "从列表中移除", "无法移除官方 Java")

+ 16 - 12
Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml

@@ -235,18 +235,16 @@
                     <local:MyHint Margin="0,15,0,-4" Text="内容见 ModSetup" Theme="Blue" x:Name="HintCustom" />
                     <local:MyHint Margin="0,15,0,-4" Text="内容见 ModSetup" Theme="Blue" x:Name="HintCustom" />
                     <local:MyHint Margin="0,15,0,6" Text="请谨慎使用陌生人提供的主页,恶意代码可能会造成损害!" Theme="Yellow" x:Name="HintCustomWarn" 
                     <local:MyHint Margin="0,15,0,6" Text="请谨慎使用陌生人提供的主页,恶意代码可能会造成损害!" Theme="Yellow" x:Name="HintCustomWarn" 
                                   CanClose="True" RelativeSetup="HintCustomWarn" />
                                   CanClose="True" RelativeSetup="HintCustomWarn" />
-                    <Grid Height="35" x:Name="PanCustomLocal" Visibility="Collapsed" Margin="0,10,0,5">
-                        <Grid.ColumnDefinitions>
-                            <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
-                            <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
-                            <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
-                            <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
-                        </Grid.ColumnDefinitions>
-                        <local:MyButton x:Name="BtnCustomRefresh" Grid.Column="0" Height="32" MinWidth="140" Text="刷新主页" Padding="13,0" Margin="0,0,20,0" ColorType="Highlight" />
-                        <local:MyButton x:Name="BtnCustomFile" Grid.Column="1" Height="32" MinWidth="140" Text="生成教学文件" Padding="13,0" Margin="0,0,20,0" />
-                        <local:MyButton x:Name="BtnCustomTutorial" Grid.Column="2" Height="32" MinWidth="140" Text="查看教程" Padding="13,0" Margin="0,0,20,0" />
-                        <local:MyButton x:Name="BtnCustomOpen" Grid.Column="3" Height="32" MinWidth="140" Text="打开主页文件夹" Padding="13,0" Margin="0,0,20,0" />
-                    </Grid>
+                    <WrapPanel x:Name="PanCustomLocal" Visibility="Collapsed" Margin="0,0,0,5">
+                        <local:MyButton x:Name="BtnCustomRefresh" Text="刷新主页"  
+                                        MinWidth="140" Height="35" Padding="13,0" Margin="0,10,20,0" HorizontalAlignment="Left" ColorType="Highlight" />
+                        <local:MyButton x:Name="BtnCustomFile" Text="生成教学文件" 
+                                        MinWidth="140" Height="35" Padding="13,0" Margin="0,10,20,0" HorizontalAlignment="Left" />
+                        <local:MyButton x:Name="BtnCustomTutorial" Text="查看教程" 
+                                        MinWidth="140" Height="35" Padding="13,0" Margin="0,10,20,0" HorizontalAlignment="Left" />
+                        <local:MyButton x:Name="BtnCustomFolder" Text="打开主页文件夹" 
+                                        MinWidth="140" Height="35" Padding="13,0" Margin="0,10,20,0" HorizontalAlignment="Left" />
+                    </WrapPanel>
                     <Grid x:Name="PanCustomNet" Visibility="Collapsed" Margin="0,10,0,5">
                     <Grid x:Name="PanCustomNet" Visibility="Collapsed" Margin="0,10,0,5">
                         <Grid.ColumnDefinitions>
                         <Grid.ColumnDefinitions>
                             <ColumnDefinition Width="Auto" SharedSizeGroup="Name" />
                             <ColumnDefinition Width="Auto" SharedSizeGroup="Name" />
@@ -288,6 +286,11 @@
                             <local:MyComboBoxItem local:SettingService.Value="17" Content="Bangumi 番剧主页(作者:wzyaeu)" />
                             <local:MyComboBoxItem local:SettingService.Value="17" Content="Bangumi 番剧主页(作者:wzyaeu)" />
                             <local:MyComboBoxItem local:SettingService.Value="18" Content="Bilibili 热门(作者:wzyaeu)" />
                             <local:MyComboBoxItem local:SettingService.Value="18" Content="Bilibili 热门(作者:wzyaeu)" />
                             <local:MyComboBoxItem local:SettingService.Value="19" Content="Music 云音乐热门(作者:wzyaeu)" />
                             <local:MyComboBoxItem local:SettingService.Value="19" Content="Music 云音乐热门(作者:wzyaeu)" />
+                            <local:MyComboBoxItem local:SettingService.Value="20" Content="模组推荐(作者:llsgllsg)" />
+                            <local:MyComboBoxItem local:SettingService.Value="21" Content="每日游戏推荐(作者:llsgllsg)" />
+                            <local:MyComboBoxItem local:SettingService.Value="22" Content="Wallpaper 每日壁纸(作者:wzyaeu)" />
+                            <local:MyComboBoxItem local:SettingService.Value="23" Content="Weather 天气预报(作者:wzyaeu)" />
+                            <local:MyComboBoxItem local:SettingService.Value="24" Content="新一代狗屁王大杂烩主页(作者:llsgllsg)" />
                         </local:MyComboBox>
                         </local:MyComboBox>
                     </Grid>
                     </Grid>
                 </StackPanel>
                 </StackPanel>
@@ -334,6 +337,7 @@
                         <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="4" Grid.Column="3" Text="版本管理" x:Name="CheckHiddenFunctionSelect" local:SettingService.Key="UiHiddenFunctionSelect" ToolTip="隐藏启动按钮下方的版本选择与版本设置入口。" />
                         <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="4" Grid.Column="3" Text="版本管理" x:Name="CheckHiddenFunctionSelect" local:SettingService.Key="UiHiddenFunctionSelect" ToolTip="隐藏启动按钮下方的版本选择与版本设置入口。" />
                         <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="4" Grid.Column="4" Text="Mod 更新" x:Name="CheckHiddenFunctionModUpdate" local:SettingService.Key="UiHiddenFunctionModUpdate" ToolTip="禁止在 Mod 管理页面更新 Mod。" />
                         <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="4" Grid.Column="4" Text="Mod 更新" x:Name="CheckHiddenFunctionModUpdate" local:SettingService.Key="UiHiddenFunctionModUpdate" ToolTip="禁止在 Mod 管理页面更新 Mod。" />
                         <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="4" Grid.Column="5" Text="功能隐藏" x:Name="CheckHiddenFunctionHidden" local:SettingService.Key="UiHiddenFunctionHidden" ToolTip="仅隐藏本选项卡,设置的禁用内容依然有效。&#xa;可以在保留个性化设置页面的情况下避免隐藏设置被修改。" />
                         <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="4" Grid.Column="5" Text="功能隐藏" x:Name="CheckHiddenFunctionHidden" local:SettingService.Key="UiHiddenFunctionHidden" ToolTip="仅隐藏本选项卡,设置的禁用内容依然有效。&#xa;可以在保留个性化设置页面的情况下避免隐藏设置被修改。" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="4" Grid.Column="6" Text="版本修改" x:Name="CheckHiddenFunctionModify" local:SettingService.Key="UiHiddenFunctionInstanceModify" ToolTip="隐藏版本设置中的版本修改功能。" />
                     </Grid>
                     </Grid>
                 </StackPanel>
                 </StackPanel>
             </local:MyCard>
             </local:MyCard>

+ 12 - 13
Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml.vb

@@ -306,19 +306,6 @@ Refresh:
                  "你可以在生成教学文件后直接刷新主页,对照着进行修改,更有助于理解。" & vbCrLf &
                  "你可以在生成教学文件后直接刷新主页,对照着进行修改,更有助于理解。" & vbCrLf &
                  "直接将主页文件拖进 PCL 窗口也可以快捷加载。", "主页自定义教程")
                  "直接将主页文件拖进 PCL 窗口也可以快捷加载。", "主页自定义教程")
     End Sub
     End Sub
-    Private Sub BtnCustomOpen_Click(sender As Object, e As EventArgs) Handles BtnCustomOpen.Click
-        Try
-            Dim CustomPath As String = Paths.Base & "PCL\Custom.xaml"
-            If FileUtils.Exists(CustomPath) Then
-                OpenExplorer(CustomPath)
-            Else
-                OpenExplorer(Paths.Base & "PCL\")
-                Hint("未找到 Custom.xaml,已打开 PCL 文件夹。", HintType.Blue)
-            End If
-        Catch ex As Exception
-            Logger.Error(ex, "打开主页文件失败")
-        End Try
-    End Sub
     Public Shared Sub OnMainPageTypeChanged()
     Public Shared Sub OnMainPageTypeChanged()
         If FrmSetupUI Is Nothing Then Return
         If FrmSetupUI Is Nothing Then Return
         Select Case CInt(Settings.Get(Of Integer)("UiCustomType"))
         Select Case CInt(Settings.Get(Of Integer)("UiCustomType"))
@@ -354,6 +341,14 @@ Refresh:
         End Select
         End Select
         FrmSetupUI.CardCustom.TriggerForceResize()
         FrmSetupUI.CardCustom.TriggerForceResize()
     End Sub
     End Sub
+    Private Sub BtnCustomFolder_Click(sender As Object, e As MouseButtonEventArgs) Handles BtnCustomFolder.Click
+        Dim CustomPath As String = Paths.Base & "PCL\Custom.xaml"
+        If FileUtils.Exists(CustomPath) Then
+            OpenExplorer(CustomPath)
+        Else
+            OpenExplorer(Paths.Base & "PCL\")
+        End If
+    End Sub
 
 
     '主题
     '主题
     Private Sub LabLauncherTheme5Unlock_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles LabLauncherTheme5Unlock.MouseLeftButtonUp
     Private Sub LabLauncherTheme5Unlock_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles LabLauncherTheme5Unlock.MouseLeftButtonUp
@@ -458,6 +453,10 @@ Refresh:
             If FrmSetupUI IsNot Nothing Then
             If FrmSetupUI IsNot Nothing Then
                 FrmSetupUI.CardSwitch.Visibility = If(Not HiddenForceShow AndAlso Settings.Get(Of Boolean)("UiHiddenFunctionHidden"), Visibility.Collapsed, Visibility.Visible)
                 FrmSetupUI.CardSwitch.Visibility = If(Not HiddenForceShow AndAlso Settings.Get(Of Boolean)("UiHiddenFunctionHidden"), Visibility.Collapsed, Visibility.Visible)
             End If
             End If
+            '版本设置子页面
+            If FrmInstanceLeft IsNot Nothing Then
+                FrmInstanceLeft.ItemModify.Visibility = If(Not HiddenForceShow AndAlso Settings.Get(Of Boolean)("UiHiddenFunctionInstanceModify"), Visibility.Collapsed, Visibility.Visible)
+            End If
             '设置子页面
             '设置子页面
             If FrmSetupLeft IsNot Nothing Then
             If FrmSetupLeft IsNot Nothing Then
                 FrmSetupLeft.ItemLaunch.Visibility = If(Not HiddenForceShow AndAlso Settings.Get(Of Boolean)("UiHiddenSetupLaunch"), Visibility.Collapsed, Visibility.Visible)
                 FrmSetupLeft.ItemLaunch.Visibility = If(Not HiddenForceShow AndAlso Settings.Get(Of Boolean)("UiHiddenSetupLaunch"), Visibility.Collapsed, Visibility.Visible)

+ 1 - 0
Plain Craft Launcher 2/Pages/PageSetup/Settings.vb

@@ -158,6 +158,7 @@ Public Class Settings
         New Setting("UiHiddenPageOther", False, OnChanged:=AddressOf PageSetupUI.HiddenRefresh),
         New Setting("UiHiddenPageOther", False, OnChanged:=AddressOf PageSetupUI.HiddenRefresh),
         New Setting("UiHiddenFunctionSelect", False, OnChanged:=AddressOf PageSetupUI.HiddenRefresh),
         New Setting("UiHiddenFunctionSelect", False, OnChanged:=AddressOf PageSetupUI.HiddenRefresh),
         New Setting("UiHiddenFunctionModUpdate", False, OnChanged:=AddressOf PageSetupUI.HiddenRefresh),
         New Setting("UiHiddenFunctionModUpdate", False, OnChanged:=AddressOf PageSetupUI.HiddenRefresh),
+        New Setting("UiHiddenFunctionInstanceModify", False, OnChanged:=AddressOf PageSetupUI.HiddenRefresh),
         New Setting("UiHiddenFunctionHidden", False, OnChanged:=AddressOf PageSetupUI.HiddenRefresh),
         New Setting("UiHiddenFunctionHidden", False, OnChanged:=AddressOf PageSetupUI.HiddenRefresh),
         New Setting("UiHiddenSetupLaunch", False, OnChanged:=AddressOf PageSetupUI.HiddenRefresh),
         New Setting("UiHiddenSetupLaunch", False, OnChanged:=AddressOf PageSetupUI.HiddenRefresh),
         New Setting("UiHiddenSetupUi", False, OnChanged:=AddressOf PageSetupUI.HiddenRefresh),
         New Setting("UiHiddenSetupUi", False, OnChanged:=AddressOf PageSetupUI.HiddenRefresh),

+ 2 - 1
Plain Craft Launcher 2/Pages/PageSpeedLeft.xaml.vb

@@ -93,7 +93,8 @@ Public Class PageSpeedLeft
                             Tb.Text = Loader.Error.GetDisplay(True)
                             Tb.Text = Loader.Error.GetDisplay(True)
                             AddHandler Tb.MouseLeftButtonDown,
                             AddHandler Tb.MouseLeftButtonDown,
                             Sub(sender As TextBlock, e As EventArgs)
                             Sub(sender As TextBlock, e As EventArgs)
-                                ClipboardSet(sender.Text, SuccessHint:="已复制错误详情!")
+                                ClipboardSet(sender.Text, False)
+                                Hint("已复制错误详情!", HintType.Green)
                             End Sub
                             End Sub
                             Card.Children.Add(Tb)
                             Card.Children.Add(Tb)
 #End Region
 #End Region

+ 1 - 0
Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj

@@ -282,6 +282,7 @@
     <Compile Include="Pages\PageDownload\PageDownloadInstall.xaml.vb">
     <Compile Include="Pages\PageDownload\PageDownloadInstall.xaml.vb">
       <DependentUpon>PageDownloadInstall.xaml</DependentUpon>
       <DependentUpon>PageDownloadInstall.xaml</DependentUpon>
     </Compile>
     </Compile>
+    <Compile Include="Pages\PageDownload\PageDownloadModify.vb" />
     <Compile Include="Pages\PageDownload\Resource\PageDownloadCompDetail.xaml.vb">
     <Compile Include="Pages\PageDownload\Resource\PageDownloadCompDetail.xaml.vb">
       <DependentUpon>PageDownloadCompDetail.xaml</DependentUpon>
       <DependentUpon>PageDownloadCompDetail.xaml</DependentUpon>
     </Compile>
     </Compile>

BIN
Plain Craft Launcher 2/Resources/Help.zip


Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor