Parcourir la source

Merge branch 'Hex-Dragon:main' into main

Silverteal il y a 2 ans
Parent
commit
9402523e01
67 fichiers modifiés avec 5157 ajouts et 1550 suppressions
  1. 60 0
      .github/workflows/dotnet-desktop.yml
  2. 1 12
      .gitignore
  3. 18 12
      LICENCE
  4. 37 0
      Plain Craft Launcher 2.sln
  5. 18 16
      Plain Craft Launcher 2/Application.xaml.vb
  6. 26 8
      Plain Craft Launcher 2/Controls/MyCard.vb
  7. 2 2
      Plain Craft Launcher 2/Controls/MyListItem.xaml.vb
  8. 6 4
      Plain Craft Launcher 2/Controls/MyScrollViewer.vb
  9. 105 66
      Plain Craft Launcher 2/FormMain.xaml.vb
  10. 115 57
      Plain Craft Launcher 2/Modules/Base/ModBase.vb
  11. 4 5
      Plain Craft Launcher 2/Modules/Base/ModLoader.vb
  12. 239 302
      Plain Craft Launcher 2/Modules/Base/ModNet.vb
  13. 1 24
      Plain Craft Launcher 2/Modules/Base/ModValidate.vb
  14. 21 21
      Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb
  15. 113 81
      Plain Craft Launcher 2/Modules/Minecraft/ModCrash.vb
  16. 6 1
      Plain Craft Launcher 2/Modules/Minecraft/ModDownload.vb
  17. 58 39
      Plain Craft Launcher 2/Modules/Minecraft/ModJava.vb
  18. 219 116
      Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb
  19. 95 78
      Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb
  20. 17 16
      Plain Craft Launcher 2/Modules/Minecraft/ModMod.vb
  21. 59 95
      Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb
  22. 13 11
      Plain Craft Launcher 2/Modules/Minecraft/ModWatcher.vb
  23. 2 0
      Plain Craft Launcher 2/Modules/Minecraft/MyLocalModItem.xaml.vb
  24. 3 0
      Plain Craft Launcher 2/Modules/ModDevelop.vb
  25. 3 4
      Plain Craft Launcher 2/Modules/ModEvent.vb
  26. 877 0
      Plain Craft Launcher 2/Modules/ModMain.vb
  27. 16 14
      Plain Craft Launcher 2/Modules/ModMusic.vb
  28. 281 0
      Plain Craft Launcher 2/Modules/ModSecret.vb
  29. 5 5
      Plain Craft Launcher 2/My Project/AssemblyInfo.vb
  30. 74 61
      Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb
  31. 4 4
      Plain Craft Launcher 2/Pages/PageDownload/PageDownloadCompDetail.xaml.vb
  32. 21 21
      Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml
  33. 94 68
      Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb
  34. 2 6
      Plain Craft Launcher 2/Pages/PageDownload/PageDownloadMod.xaml
  35. 2 6
      Plain Craft Launcher 2/Pages/PageDownload/PageDownloadPack.xaml
  36. 1 1
      Plain Craft Launcher 2/Pages/PageLaunch/FormLoginOAuth.xaml.vb
  37. 1 1
      Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml.vb
  38. 48 46
      Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchLeft.xaml.vb
  39. 23 17
      Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.vb
  40. 2 2
      Plain Craft Launcher 2/Pages/PageLaunch/PageLoginAuth.xaml.vb
  41. 1 1
      Plain Craft Launcher 2/Pages/PageLaunch/PageLoginLegacy.xaml.vb
  42. 37 33
      Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMs.xaml.vb
  43. 44 43
      Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMsSkin.xaml.vb
  44. 2 2
      Plain Craft Launcher 2/Pages/PageLaunch/PageLoginNide.xaml.vb
  45. 0 3
      Plain Craft Launcher 2/Pages/PageLink/PageLinkIoi.xaml.vb
  46. 332 0
      Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml
  47. 40 0
      Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml.vb
  48. 12 0
      Plain Craft Launcher 2/Pages/PageOther/PageOtherTest.xaml
  49. 27 0
      Plain Craft Launcher 2/Pages/PageOther/PageOtherTest.xaml.vb
  50. 3 3
      Plain Craft Launcher 2/Pages/PageSelectLeft.xaml.vb
  51. 691 0
      Plain Craft Launcher 2/Pages/PageSetup/ModSetup.vb
  52. 12 8
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml
  53. 45 34
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml.vb
  54. 0 1
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml
  55. 1 1
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupSystem.xaml
  56. 16 18
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupSystem.xaml.vb
  57. 295 0
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml
  58. 705 0
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml.vb
  59. 19 12
      Plain Craft Launcher 2/Pages/PageSpeedLeft.xaml.vb
  60. 3 3
      Plain Craft Launcher 2/Pages/PageVersion/PageVersionMod.xaml.vb
  61. 2 2
      Plain Craft Launcher 2/Pages/PageVersion/PageVersionSetup.xaml.vb
  62. 9 1
      Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj
  63. BIN
      Plain Craft Launcher 2/Resources/Help.zip
  64. 150 150
      Plain Craft Launcher 2/Resources/ModData.txt
  65. 3 3
      Plain Craft Launcher 2/app.manifest
  66. 16 10
      README.md
  67. BIN
      最新正式版.zip

+ 60 - 0
.github/workflows/dotnet-desktop.yml

@@ -0,0 +1,60 @@
+name: .NET Core Desktop
+
+on:
+  push:
+    paths:
+      - 'Plain Craft Launcher 2.sln'
+      - 'Plain Craft Launcher 2/**'
+      - '.github/workflows/**'
+  pull_request:
+    paths:
+      - 'Plain Craft Launcher 2.sln'
+      - 'Plain Craft Launcher 2/**'
+      - '.github/workflows/**'
+
+jobs:
+
+  build:
+
+    strategy:
+      matrix:
+        configuration: [Beta, Debug, Release, ReleaseUpdate]
+
+    runs-on: windows-latest
+
+    env:
+      Solution_Name: Plain Craft Launcher 2.sln
+      Wap_Project_Directory: Plain Craft Launcher 2
+      Wap_Project_Path: Plain Craft Launcher 2\Plain Craft Launcher 2.vbproj
+
+    steps:
+    - name: Checkout
+      uses: actions/checkout@v4
+      with:
+        fetch-depth: 0
+
+    # Add  MSBuild to the PATH: https://github.com/microsoft/setup-msbuild
+    - name: Setup MSBuild.exe
+      uses: microsoft/setup-msbuild@v2
+
+    # Restore the application to populate the obj folder with RuntimeIdentifiers
+    - name: Restore the application
+      run: msbuild $env:Solution_Name /t:Restore /p:Configuration=$env:Configuration
+      env:
+        Configuration: ${{ matrix.configuration }}
+
+    # Create the app package by building and packaging the Windows Application Packaging project
+    - name: Create the app package
+      run: msbuild $env:Wap_Project_Path /p:Configuration=$env:Configuration /p:UapAppxPackageBuildMode=$env:Appx_Package_Build_Mode /p:AppxBundle=$env:Appx_Bundle
+      env:
+        Appx_Bundle: Always
+        Appx_Bundle_Platforms: x86|x64|ARM|ARM64
+        Appx_Package_Build_Mode: StoreUpload
+        Configuration: ${{ matrix.configuration }}
+
+    # Upload the MSIX package: https://github.com/marketplace/actions/upload-a-build-artifact
+    - name: Upload build artifacts
+      uses: actions/upload-artifact@v4
+      with:
+        name: ${{ matrix.configuration }}
+        path: ${{ env.Wap_Project_Directory }}\obj\${{ matrix.configuration }}\Plain Craft Launcher 2.exe

+ 1 - 12
.gitignore

@@ -341,15 +341,4 @@ ASALocalRun/
 # BeatPulse healthcheck temp database
 # BeatPulse healthcheck temp database
 healthchecksdb
 healthchecksdb
 
 
-# ------------------------------------------------------------
-
-# 包含主题解锁、加密解密等部分的代码
-ModSecret.vb
-ModSetup.vb
-PageOtherAbout.*
-PageOtherTest.*
-PageSetupUI.*
-# 开发者工具
-ModDevelop.vb
-# 不公开底层函数库,以免抄袭
-ModMain.vb
+MaskFiles/

+ 18 - 12
LICENCE

@@ -1,15 +1,21 @@
-本文件不是法律文件或开源协议。
-龙腾猫跃(下文简称为龙猫)承诺在您遵守本文件指引的情况下,不会追究您的法律责任,但裁量您 “是否遵守本文件指引” 的最终权利由龙猫保留。
+本文件仅作为指引您合理使用本存储库内容的指南,不是法律文件或协议。
 
 
-1、本存储库中的内容以帮助公共研究为目的而公开。
-2、在没有附加许可的情况下,龙猫仍然对本存储库的内容保留所有权利,故以下所有允许性表述均无法作出永久性保证。
-3、不允许以本存储库的内容作为内容主体创建、发布、传播其衍生作品,且您不可以发布本存储库及其相关作品的修改版。
-4、在满足第 3 款,且在可见位置标注内容来源(即本存储库链接)的前提下,您可以使用或引用本存储库的小部分内容或是其实现思路。
-5、龙猫可能随时修改本文件,且可能不会提前通知。本文件不是法律文件或开源协议,故本文件的修改不构成与您法律关系的改变。
+一、承诺。
+  开发者承诺,在您遵守本指南的情况下,不会追究您的法律责任,但是否遵守的最终裁量权由开发者保留。
+二、若大量使用,则不能分发。
+  如果您再混合、转换、或者基于该存储库创作,除非满足第三款(少量使用),否则您不可以分发修改后的内容。
+三、可少量使用,但必须署名。
+  如果您仅使用了本存储库的少量内容或是实现思路,您必须给出适当的署名,且提供指向本存储库的链接。
+  您可以用任何合理的方式来署名,但是不得以任何方式暗示开发者为您或您的使用背书。
+四、标注修改内容。
+  如果您要发布任何修改后的版本的衍生内容,您必须给出适当的说明,表示这不是官方版本。
+五、保留权利。
+  除上述表述外,开发者对本存储库保留所有权利(All Rights Reserved),且开发者可能随时修改本指南,上述表述均无法作出永久性保证。
 
 
-——————
+————————————
 
 
-说人话就是,随便你参考这堆代码的思路,乃至复制一两个函数什么的也可以,别太过火就行,顺便记得声明一下这段是从这里来的。
-不过,你不能把它们直接复制下来然后再小修小补,就说这是你自己的东西。也不能把其中一小部分单独抠出来说是你自己的东西。
-没有法律顾问,所以这只是明确说明一下,你可以借鉴其中的小部分内容,我也不会且懒得因此找你麻烦。
-如果有专业人士愿意帮帮忙把这堆东西写得专业一点我也很乐意……
+说人话版本:
+因为没有法律专业人士检查,所以这不敢作为正式的法律协议,如果有专业人士愿意帮忙改一改我也很乐意……
+如果你只是想参考一下,或者使用一小部分代码,署个名就行,不必担心。
+如果你整了个魔改版,你不能把它发给其他人,但可以私下使用;如果要用魔改版来录视频、发图片什么的,你必须标注这是魔改版,不是官方版本。
+这个指南主要是为了阻止恶意的使用和 “山寨版” 的出现,常规、善意的使用都问题不大,放心吧……

+ 37 - 0
Plain Craft Launcher 2.sln

@@ -0,0 +1,37 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.2.32616.157
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "Plain Craft Launcher 2", "Plain Craft Launcher 2\Plain Craft Launcher 2.vbproj", "{4499F2D2-18EF-4238-B1F0-7BD547AFF925}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Beta|Any CPU = Beta|Any CPU
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+		ReleaseUpdate|Any CPU = ReleaseUpdate|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{4499F2D2-18EF-4238-B1F0-7BD547AFF925}.Beta|Any CPU.ActiveCfg = BETA|Any CPU
+		{4499F2D2-18EF-4238-B1F0-7BD547AFF925}.Beta|Any CPU.Build.0 = BETA|Any CPU
+		{4499F2D2-18EF-4238-B1F0-7BD547AFF925}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{4499F2D2-18EF-4238-B1F0-7BD547AFF925}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{4499F2D2-18EF-4238-B1F0-7BD547AFF925}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{4499F2D2-18EF-4238-B1F0-7BD547AFF925}.Release|Any CPU.Build.0 = Release|Any CPU
+		{4499F2D2-18EF-4238-B1F0-7BD547AFF925}.ReleaseUpdate|Any CPU.ActiveCfg = ReleaseUpdate|Any CPU
+		{4499F2D2-18EF-4238-B1F0-7BD547AFF925}.ReleaseUpdate|Any CPU.Build.0 = ReleaseUpdate|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {838A572E-025C-4A2B-B682-68732EC0FE76}
+	EndGlobalSection
+	GlobalSection(Performance) = preSolution
+		HasPerformanceSessions = true
+	EndGlobalSection
+	GlobalSection(Performance) = preSolution
+		HasPerformanceSessions = true
+	EndGlobalSection
+EndGlobal

+ 18 - 16
Plain Craft Launcher 2/Application.xaml.vb

@@ -12,26 +12,21 @@ Public Class Application
         Try
         Try
             ModDevelop.Start()
             ModDevelop.Start()
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "开发者模式测试出错", LogLevel.Feedback)
+            Log(ex, "开发者模式测试出错", LogLevel.Msgbox)
         End Try
         End Try
     End Sub
     End Sub
 #End If
 #End If
 
 
     '开始
     '开始
     Private Sub Application_Startup(sender As Object, e As StartupEventArgs) Handles Me.Startup
     Private Sub Application_Startup(sender As Object, e As StartupEventArgs) Handles Me.Startup
-        ApplicationStartTick = GetTimeTick()
-        SecretOnApplicationStart()
         Try
         Try
+            SecretOnApplicationStart()
             '检查参数调用
             '检查参数调用
             If e.Args.Length > 0 Then
             If e.Args.Length > 0 Then
                 If e.Args(0) = "--update" Then
                 If e.Args(0) = "--update" Then
                     '自动更新
                     '自动更新
                     UpdateReplace(e.Args(1), e.Args(2).Trim(""""), e.Args(3).Trim(""""), e.Args(4))
                     UpdateReplace(e.Args(1), e.Args(2).Trim(""""), e.Args(3).Trim(""""), e.Args(4))
                     Environment.Exit(Result.Cancel)
                     Environment.Exit(Result.Cancel)
-                    'ElseIf e.Args(0).StartsWithF("--link") Then
-                    '    '稍作等待后切换到联机页面
-                    '    Thread.Sleep(1000)
-                    '    FormMain.IsLinkRestart = True
                 ElseIf e.Args(0).StartsWithF("--memory") Then
                 ElseIf e.Args(0).StartsWithF("--memory") Then
                     '内存优化
                     '内存优化
                     Dim Ram = My.Computer.Info.AvailablePhysicalMemory
                     Dim Ram = My.Computer.Info.AvailablePhysicalMemory
@@ -104,9 +99,10 @@ Public Class Application
             '日志初始化
             '日志初始化
             LogStart()
             LogStart()
             '添加日志
             '添加日志
-            Log("[Start] 程序版本:" & VersionDisplayName & "(" & VersionCode & ")")
-            Log("[Start] 识别码:" & UniqueAddress & If(ThemeCheckOne(9), ",已解锁反馈主题", ""))
-            Log("[Start] 程序路径:" & PathWithName)
+            Log($"[Start] 程序版本:{VersionDisplayName} ({VersionCode})")
+            Log($"[Start] 识别码:{UniqueAddress}{If(ThemeCheckOne(9), ",已解锁反馈主题", "")}")
+            Log($"[Start] 程序路径:{PathWithName}")
+            Log($"[Start] 系统编码:{Encoding.Default} ({Encoding.Default.CodePage}, GBK={IsGBKEncoding})")
             '检测压缩包运行
             '检测压缩包运行
             If Path.Contains(IO.Path.GetTempPath()) OrElse Path.Contains("AppData\Local\Temp\") Then
             If Path.Contains(IO.Path.GetTempPath()) OrElse Path.Contains("AppData\Local\Temp\") Then
                 MyMsgBox("PCL 正在临时文件夹运行,设置、游戏存档等很可能无法保存,且部分功能会无法使用或出错。" & vbCrLf & "请将 PCL 从压缩文件中解压,或是更换文件夹后再继续使用!", "环境警告", "我知道了", IsWarn:=True)
                 MyMsgBox("PCL 正在临时文件夹运行,设置、游戏存档等很可能无法保存,且部分功能会无法使用或出错。" & vbCrLf & "请将 PCL 从压缩文件中解压,或是更换文件夹后再继续使用!", "环境警告", "我知道了", IsWarn:=True)
@@ -129,7 +125,12 @@ Public Class Application
 #End If
 #End If
             AniControlEnabled += 1
             AniControlEnabled += 1
         Catch ex As Exception
         Catch ex As Exception
-            MsgBox(GetExceptionDetail(ex, True), MsgBoxStyle.Critical, "PCL 初始化错误")
+            Dim FilePath As String = Nothing
+            Try
+                FilePath = PathWithName
+            Catch
+            End Try
+            MsgBox(GetExceptionDetail(ex, True) & vbCrLf & "PCL 所在路径:" & If(String.IsNullOrEmpty(FilePath), "获取失败", FilePath), MsgBoxStyle.Critical, "PCL 初始化错误")
             FormMain.EndProgramForce(Result.Exception)
             FormMain.EndProgramForce(Result.Exception)
         End Try
         End Try
     End Sub
     End Sub
@@ -212,6 +213,7 @@ Public Class Application
                 Dim MsJson As JObject = GetJson(Setup.Get("LoginMsJson"))
                 Dim MsJson As JObject = GetJson(Setup.Get("LoginMsJson"))
                 MsJson.Remove(sender.Tag)
                 MsJson.Remove(sender.Tag)
                 Setup.Set("LoginMsJson", MsJson.ToString(Newtonsoft.Json.Formatting.None))
                 Setup.Set("LoginMsJson", MsJson.ToString(Newtonsoft.Json.Formatting.None))
+                If FrmLoginMs.ComboAccounts.SelectedItem Is sender.Parent Then FrmLoginMs.ComboAccounts.SelectedIndex = 0
                 FrmLoginMs.ComboAccounts.Items.Remove(sender.Parent)
                 FrmLoginMs.ComboAccounts.Items.Remove(sender.Parent)
             Case McLoginType.Legacy
             Case McLoginType.Legacy
                 '离线
                 '离线
@@ -220,7 +222,7 @@ Public Class Application
                 Names.Remove(sender.Tag)
                 Names.Remove(sender.Tag)
                 Setup.Set("LoginLegacyName", Join(Names, "¨"))
                 Setup.Set("LoginLegacyName", Join(Names, "¨"))
                 FrmLoginLegacy.ComboName.ItemsSource = Names
                 FrmLoginLegacy.ComboName.ItemsSource = Names
-                FrmLoginLegacy.ComboName.Text = If(Names.Count > 0, Names(0), "")
+                FrmLoginLegacy.ComboName.Text = If(Names.Any, Names(0), "")
             Case Else
             Case Else
                 '第三方
                 '第三方
                 Dim Token As String = GetStringFromEnum(Setup.Get("LoginType"))
                 Dim Token As String = GetStringFromEnum(Setup.Get("LoginType"))
@@ -238,12 +240,12 @@ Public Class Application
                 Select Case Token
                 Select Case Token
                     Case "Nide"
                     Case "Nide"
                         FrmLoginNide.ComboName.ItemsSource = Dict.Keys
                         FrmLoginNide.ComboName.ItemsSource = Dict.Keys
-                        FrmLoginNide.ComboName.Text = If(Dict.Keys.Count > 0, Dict.Keys(0), "")
-                        FrmLoginNide.TextPass.Password = If(Dict.Values.Count > 0, Dict.Values(0), "")
+                        FrmLoginNide.ComboName.Text = If(Dict.Keys.Any, Dict.Keys(0), "")
+                        FrmLoginNide.TextPass.Password = If(Dict.Values.Any, Dict.Values(0), "")
                     Case "Auth"
                     Case "Auth"
                         FrmLoginAuth.ComboName.ItemsSource = Dict.Keys
                         FrmLoginAuth.ComboName.ItemsSource = Dict.Keys
-                        FrmLoginAuth.ComboName.Text = If(Dict.Keys.Count > 0, Dict.Keys(0), "")
-                        FrmLoginAuth.TextPass.Password = If(Dict.Values.Count > 0, Dict.Values(0), "")
+                        FrmLoginAuth.ComboName.Text = If(Dict.Keys.Any, Dict.Keys(0), "")
+                        FrmLoginAuth.TextPass.Password = If(Dict.Values.Any, Dict.Values(0), "")
                 End Select
                 End Select
         End Select
         End Select
     End Sub
     End Sub

+ 26 - 8
Plain Craft Launcher 2/Controls/MyCard.vb

@@ -13,8 +13,26 @@
             MainBorder.Child = value
             MainBorder.Child = value
         End Set
         End Set
     End Property
     End Property
-    Public MainTextBlock As TextBlock
-    Public MainSwap As Shapes.Path
+    Private _MainTextBlock As TextBlock
+    Public Property MainTextBlock As TextBlock
+        Get
+            Init() '当父级触发 Loaded 时,本卡片可能尚未触发 Loaded(该事件从父级向子级调用),因此这会是 null。手动触发以确保控件已加载。
+            Return _MainTextBlock
+        End Get
+        Set(value As TextBlock)
+            _MainTextBlock = value
+        End Set
+    End Property
+    Private _MainSwap As Shapes.Path
+    Public Property MainSwap As Shapes.Path
+        Get
+            Init()
+            Return _MainSwap
+        End Get
+        Set(value As Shapes.Path)
+            _MainSwap = value
+        End Set
+    End Property
 
 
     '属性
     '属性
     Public Uuid As Integer = GetUuid()
     Public Uuid As Integer = GetUuid()
@@ -24,7 +42,7 @@
         End Get
         End Get
         Set(value As String)
         Set(value As String)
             SetValue(TitleProperty, value)
             SetValue(TitleProperty, value)
-            If MainTextBlock IsNot Nothing Then MainTextBlock.Text = value
+            If _MainTextBlock IsNot Nothing Then MainTextBlock.Text = value
         End Set
         End Set
     End Property
     End Property
     Public Shared ReadOnly TitleProperty As DependencyProperty = DependencyProperty.Register("Title", GetType(String), GetType(MyCard), New PropertyMetadata(""))
     Public Shared ReadOnly TitleProperty As DependencyProperty = DependencyProperty.Register("Title", GetType(String), GetType(MyCard), New PropertyMetadata(""))
@@ -40,10 +58,11 @@
         Children.Add(MainGrid)
         Children.Add(MainGrid)
     End Sub
     End Sub
     Private IsLoad As Boolean = False
     Private IsLoad As Boolean = False
-    Private Sub MyCard_Loaded() Handles Me.Loaded
+    Private Sub Init() Handles Me.Loaded
         If IsLoad Then Exit Sub
         If IsLoad Then Exit Sub
+        IsLoad = True
         '初次加载限定
         '初次加载限定
-        If Not Title = "" AndAlso IsNothing(MainTextBlock) Then
+        If Title <> "" AndAlso MainTextBlock Is Nothing Then
             MainTextBlock = New TextBlock With {.HorizontalAlignment = HorizontalAlignment.Left, .VerticalAlignment = VerticalAlignment.Top, .Margin = New Thickness(15, 12, 0, 0), .FontWeight = FontWeights.Bold, .FontSize = 13, .IsHitTestVisible = False}
             MainTextBlock = New TextBlock With {.HorizontalAlignment = HorizontalAlignment.Left, .VerticalAlignment = VerticalAlignment.Top, .Margin = New Thickness(15, 12, 0, 0), .FontWeight = FontWeights.Bold, .FontSize = 13, .IsHitTestVisible = False}
             MainTextBlock.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrush1")
             MainTextBlock.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrush1")
             MainTextBlock.SetBinding(TextBlock.TextProperty, New Binding("Title") With {.Source = Me, .Mode = BindingMode.OneWay})
             MainTextBlock.SetBinding(TextBlock.TextProperty, New Binding("Title") With {.Source = Me, .Mode = BindingMode.OneWay})
@@ -55,7 +74,6 @@
             MainSwap.SetResourceReference(Shapes.Path.FillProperty, "ColorBrush1")
             MainSwap.SetResourceReference(Shapes.Path.FillProperty, "ColorBrush1")
             MainGrid.Children.Add(MainSwap)
             MainGrid.Children.Add(MainSwap)
         End If
         End If
-        IsLoad = True
         '改变默认的折叠
         '改变默认的折叠
         If IsSwaped AndAlso SwapControl IsNot Nothing Then
         If IsSwaped AndAlso SwapControl IsNot Nothing Then
             MainSwap.RenderTransform = New RotateTransform(If(SwapLogoRight, 270, 0))
             MainSwap.RenderTransform = New RotateTransform(If(SwapLogoRight, 270, 0))
@@ -130,7 +148,7 @@
                                                                     FrmDownloadInstall.MinecraftSelected(sender, e)
                                                                     FrmDownloadInstall.MinecraftSelected(sender, e)
                                                                 End Sub, False))
                                                                 End Sub, False))
                 Case 8
                 Case 8
-                    If Distinct(CType(Stack.Tag, List(Of CompFile)), Function(a, b) a.DisplayName = b.DisplayName).Count <>
+                    If CType(Stack.Tag, List(Of CompFile)).Distinct(Function(a, b) a.DisplayName = b.DisplayName).Count <>
                        CType(Stack.Tag, List(Of CompFile)).Count Then
                        CType(Stack.Tag, List(Of CompFile)).Count Then
                         '存在重复的名称(#1344)
                         '存在重复的名称(#1344)
                         Stack.Children.Add(CType(Data, CompFile).ToListItem(AddressOf FrmDownloadCompDetail.Save_Click, BadDisplayName:=True))
                         Stack.Children.Add(CType(Data, CompFile).ToListItem(AddressOf FrmDownloadCompDetail.Save_Click, BadDisplayName:=True))
@@ -139,7 +157,7 @@
                         Stack.Children.Add(CType(Data, CompFile).ToListItem(AddressOf FrmDownloadCompDetail.Save_Click))
                         Stack.Children.Add(CType(Data, CompFile).ToListItem(AddressOf FrmDownloadCompDetail.Save_Click))
                     End If
                     End If
                 Case 9
                 Case 9
-                    If Distinct(CType(Stack.Tag, List(Of CompFile)), Function(a, b) a.DisplayName = b.DisplayName).Count <>
+                    If CType(Stack.Tag, List(Of CompFile)).Distinct(Function(a, b) a.DisplayName = b.DisplayName).Count <>
                        CType(Stack.Tag, List(Of CompFile)).Count Then
                        CType(Stack.Tag, List(Of CompFile)).Count Then
                         '存在重复的名称(#1344)
                         '存在重复的名称(#1344)
                         Stack.Children.Add(CType(Data, CompFile).ToListItem(AddressOf FrmDownloadCompDetail.Install_Click, AddressOf FrmDownloadCompDetail.Save_Click, BadDisplayName:=True))
                         Stack.Children.Add(CType(Data, CompFile).ToListItem(AddressOf FrmDownloadCompDetail.Install_Click, AddressOf FrmDownloadCompDetail.Save_Click, BadDisplayName:=True))

+ 2 - 2
Plain Craft Launcher 2/Controls/MyListItem.xaml.vb

@@ -656,9 +656,9 @@
             SetResourceReference(ForegroundProperty, "ColorBrush1")
             SetResourceReference(ForegroundProperty, "ColorBrush1")
         End If
         End If
         ColumnPaddingRight.Width = New GridLength(MinPaddingRight)
         ColumnPaddingRight.Width = New GridLength(MinPaddingRight)
-        If EventType = "打开帮助" Then
+        If EventType = "打开帮助" AndAlso Not (Title <> "" AndAlso Info <> "") Then '#3266
             Try
             Try
-                Dim NewElem = New HelpEntry(GetEventAbsoluteUrls(EventData, EventType)(0)).SetToListItem(Me)
+                Dim Unused = New HelpEntry(GetEventAbsoluteUrls(EventData, EventType)(0)).SetToListItem(Me)
             Catch ex As Exception
             Catch ex As Exception
                 Log(ex, "设置帮助 MyListItem 失败", LogLevel.Msgbox)
                 Log(ex, "设置帮助 MyListItem 失败", LogLevel.Msgbox)
             End Try
             End Try

+ 6 - 4
Plain Craft Launcher 2/Controls/MyScrollViewer.vb

@@ -8,10 +8,12 @@
     Private Sub MyScrollViewer_PreviewMouseWheel(sender As Object, e As MouseWheelEventArgs) Handles Me.PreviewMouseWheel
     Private Sub MyScrollViewer_PreviewMouseWheel(sender As Object, e As MouseWheelEventArgs) Handles Me.PreviewMouseWheel
         If e.Delta = 0 OrElse ActualHeight = 0 OrElse ScrollableHeight = 0 Then Exit Sub
         If e.Delta = 0 OrElse ActualHeight = 0 OrElse ScrollableHeight = 0 Then Exit Sub
         Dim SourceType = e.Source.GetType
         Dim SourceType = e.Source.GetType
-        If Content.TemplatedParent Is Nothing AndAlso
-            ((GetType(ComboBox).IsAssignableFrom(SourceType) AndAlso CType(e.Source, ComboBox).IsDropDownOpen) OrElse
-            (GetType(TextBox).IsAssignableFrom(SourceType) AndAlso CType(e.Source, TextBox).AcceptsReturn) OrElse
-            GetType(ComboBoxItem).IsAssignableFrom(SourceType)) Then
+        If Content.TemplatedParent Is Nothing AndAlso (
+                (GetType(ComboBox).IsAssignableFrom(SourceType) AndAlso CType(e.Source, ComboBox).IsDropDownOpen) OrElse
+                (GetType(TextBox).IsAssignableFrom(SourceType) AndAlso CType(e.Source, TextBox).AcceptsReturn) OrElse
+                GetType(ComboBoxItem).IsAssignableFrom(SourceType) OrElse
+                TypeOf e.Source Is CheckBox
+           ) Then
             '如果当前是在对有滚动条的下拉框或文本框执行,则不接管操作
             '如果当前是在对有滚动条的下拉框或文本框执行,则不接管操作
             Exit Sub
             Exit Sub
         End If
         End If

+ 105 - 66
Plain Craft Launcher 2/FormMain.xaml.vb

@@ -10,6 +10,18 @@ Public Class FormMain
         Dim FeatureList As New List(Of KeyValuePair(Of Integer, String))
         Dim FeatureList As New List(Of KeyValuePair(Of Integer, String))
         '统计更新日志条目
         '统计更新日志条目
 #If BETA Then
 #If BETA Then
+        If LastVersion < 323 Then 'Release 2.7.3
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "添加 启动游戏前进行内存优化 设置"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化 MC 性能"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复安装 OptiFine 有概率失败的 Bug"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复启动 Fabric 1.20.5+ 时无法正确选择 Java 的 Bug"))
+            FeatureCount += 22
+            BugCount += 21
+        End If
+        If LastVersion < 321 Then 'Release 2.7.1
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复启动部分整合包导致设置丢失的 Bug"))
+            BugCount += 1
+        End If
         If LastVersion < 319 Then 'Release 2.7.0
         If LastVersion < 319 Then 'Release 2.7.0
             FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "支持更新 Mod"))
             FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "支持更新 Mod"))
             FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "支持查看可更新的 Mod 的更新日志"))
             FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "支持查看可更新的 Mod 的更新日志"))
@@ -96,7 +108,25 @@ Public Class FormMain
         '3:BUG+ IMP* FEAT-
         '3:BUG+ IMP* FEAT-
         '2:BUG* IMP-
         '2:BUG* IMP-
         '1:BUG-
         '1:BUG-
-        If LastVersion < 318 Then 'Snapshot 2.7.1
+        If LastVersion < 325 Then 'Snapshot 2.7.4
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "会自动隐藏明显不可用的自动安装选项"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化正版登录流程和 MC 性能"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复正版登录时弹出脚本错误提示的 Bug"))
+            FeatureCount += 17
+            BugCount += 19
+        End If
+        If LastVersion < 324 Then 'Snapshot 2.7.3
+            FeatureCount += 4
+            BugCount += 3
+        End If
+        If LastVersion < 322 Then 'Snapshot 2.7.2
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "添加 启动游戏前进行内存优化 设置"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复安装 OptiFine 有概率失败的 Bug"))
+            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复启动 Fabric 1.20.5+ 时无法正确选择 Java 的 Bug"))
+            FeatureCount += 18
+            BugCount += 18
+        End If
+        If LastVersion < 320 Then 'Snapshot 2.7.1
             FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复启动部分整合包导致设置丢失的 Bug"))
             FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复启动部分整合包导致设置丢失的 Bug"))
             BugCount += 1
             BugCount += 1
         End If
         End If
@@ -229,7 +259,7 @@ Public Class FormMain
         Next
         Next
         If SortedFeatures.Count > 10 Then FeatureCount += SortedFeatures.Count - 10
         If SortedFeatures.Count > 10 Then FeatureCount += SortedFeatures.Count - 10
         If FeatureCount > 0 OrElse BugCount > 0 Then
         If FeatureCount > 0 OrElse BugCount > 0 Then
-            ContentList.Add(If(FeatureCount > 0, "其他 " & FeatureCount & " 项小调整与修改", "") &
+            ContentList.Add(If(FeatureCount > 0, FeatureCount & " 项小调整与修改", "") &
                 If(FeatureCount > 0 AndAlso BugCount > 0, ",", "") &
                 If(FeatureCount > 0 AndAlso BugCount > 0, ",", "") &
                 If(BugCount > 0, "修复了 " & BugCount & " 个 Bug", "") &
                 If(BugCount > 0, "修复了 " & BugCount & " 个 Bug", "") &
                 ",详见完整更新日志")
                 ",详见完整更新日志")
@@ -306,7 +336,7 @@ Public Class FormMain
         Setup.Load("UiBackgroundBlur")
         Setup.Load("UiBackgroundBlur")
         Setup.Load("UiLogoType")
         Setup.Load("UiLogoType")
         Setup.Load("UiHiddenPageDownload")
         Setup.Load("UiHiddenPageDownload")
-        BackgroundRefresh(False, True)
+        PageSetupUI.BackgroundRefresh(False, True)
         MusicRefreshPlay(False, True)
         MusicRefreshPlay(False, True)
         '扩展按钮
         '扩展按钮
         BtnExtraDownload.ShowCheck = AddressOf BtnExtraDownload_ShowCheck
         BtnExtraDownload.ShowCheck = AddressOf BtnExtraDownload_ShowCheck
@@ -352,44 +382,46 @@ Public Class FormMain
             AaOpacity(Me, Setup.Get("UiLauncherTransparent") / 1000 + 0.4, 250, 100),
             AaOpacity(Me, Setup.Get("UiLauncherTransparent") / 1000 + 0.4, 250, 100),
             AaDouble(Sub(i) TransformPos.Y += i, -TransformPos.Y, 600, 100, New AniEaseOutBack(AniEasePower.Weak)),
             AaDouble(Sub(i) TransformPos.Y += i, -TransformPos.Y, 600, 100, New AniEaseOutBack(AniEasePower.Weak)),
             AaDouble(Sub(i) TransformRotate.Angle += i, -TransformRotate.Angle, 500, 100, New AniEaseOutBack(AniEasePower.Weak)),
             AaDouble(Sub(i) TransformRotate.Angle += i, -TransformRotate.Angle, 500, 100, New AniEaseOutBack(AniEasePower.Weak)),
-            AaCode(Sub()
-                       PanBack.RenderTransform = Nothing
-                       IsWindowLoadFinished = True
-                       Log($"[System] DPI:{DPI},系统版本:{OsVersion},PCL 位置:{PathWithName}")
-                   End Sub, , True)
+            AaCode(
+            Sub()
+                PanBack.RenderTransform = Nothing
+                IsWindowLoadFinished = True
+                Log($"[System] DPI:{DPI},系统版本:{OsVersion},PCL 位置:{PathWithName}")
+            End Sub, , True)
         }, "Form Show")
         }, "Form Show")
         'Timer 启动
         'Timer 启动
         AniStart()
         AniStart()
         TimerMainStart()
         TimerMainStart()
         '加载池
         '加载池
-        RunInNewThread(Sub()
-                           'EULA 提示
-                           If Not Setup.Get("SystemEula") Then
-                               Select Case MyMsgBox("在使用 PCL 前,请同意 PCL 的用户协议与免责声明。", "协议授权", "同意", "拒绝", "查看用户协议与免责声明",
+        RunInNewThread(
+        Sub()
+            'EULA 提示
+            If Not Setup.Get("SystemEula") Then
+                Select Case MyMsgBox("在使用 PCL 前,请同意 PCL 的用户协议与免责声明。", "协议授权", "同意", "拒绝", "查看用户协议与免责声明",
                                    Button3Action:=Sub() OpenWebsite("https://shimo.im/docs/rGrd8pY8xWkt6ryW"))
                                    Button3Action:=Sub() OpenWebsite("https://shimo.im/docs/rGrd8pY8xWkt6ryW"))
-                                   Case 1
-                                       Setup.Set("SystemEula", True)
-                                   Case 2
-                                       EndProgram(False)
-                               End Select
-                           End If
-                           '启动加载器池
-                           Try
-                               JavaListInit() '延后到同意协议后再执行,避免在初次启动时进行进程操作
-                               Thread.Sleep(200)
-                               DlClientListMojangLoader.Start(1)
-                               RunCountSub()
-                               ServerLoader.Start(1)
-                           Catch ex As Exception
-                               Log(ex, "初始化加载池运行失败", LogLevel.Feedback)
-                           End Try
-                           '清理自动更新文件
-                           Try
-                               If File.Exists(Path & "PCL\Plain Craft Launcher 2.exe") Then File.Delete(Path & "PCL\Plain Craft Launcher 2.exe")
-                           Catch ex As Exception
-                               Log(ex, "清理自动更新文件失败")
-                           End Try
-                       End Sub, "Start Loader", ThreadPriority.Lowest)
+                    Case 1
+                        Setup.Set("SystemEula", True)
+                    Case 2
+                        EndProgram(False)
+                End Select
+            End If
+            '启动加载器池
+            Try
+                JavaListInit() '延后到同意协议后再执行,避免在初次启动时进行进程操作
+                Thread.Sleep(200)
+                DlClientListMojangLoader.Start(1)
+                RunCountSub()
+                ServerLoader.Start(1)
+            Catch ex As Exception
+                Log(ex, "初始化加载池运行失败", LogLevel.Feedback)
+            End Try
+            '清理自动更新文件
+            Try
+                If File.Exists(Path & "PCL\Plain Craft Launcher 2.exe") Then File.Delete(Path & "PCL\Plain Craft Launcher 2.exe")
+            Catch ex As Exception
+                Log(ex, "清理自动更新文件失败")
+            End Try
+        End Sub, "Start Loader", ThreadPriority.Lowest)
 
 
         Log("[Start] 第三阶段加载用时:" & GetTimeTick() - ApplicationStartTick & " ms")
         Log("[Start] 第三阶段加载用时:" & GetTimeTick() - ApplicationStartTick & " ms")
     End Sub
     End Sub
@@ -430,6 +462,8 @@ Public Class FormMain
             Log("[Start] 最高版本号从 " & LowerVersionCode & " 升高到 " & VersionCode)
             Log("[Start] 最高版本号从 " & LowerVersionCode & " 升高到 " & VersionCode)
         End If
         End If
 #End If
 #End If
+        '被移除的窗口设置选项
+        If Setup.Get("LaunchArgumentWindowType") = 5 Then Setup.Set("LaunchArgumentWindowType", 1)
         '修改主题设置项名称
         '修改主题设置项名称
         If LowerVersionCode <= 207 Then
         If LowerVersionCode <= 207 Then
             Dim UnlockedTheme As New List(Of String) From {"2"}
             Dim UnlockedTheme As New List(Of String) From {"2"}
@@ -495,12 +529,13 @@ Public Class FormMain
         If SendWarning AndAlso HasDownloadingTask() Then
         If SendWarning AndAlso HasDownloadingTask() Then
             If MyMsgBox("还有下载任务尚未完成,是否确定退出?", "提示", "确定", "取消") = 1 Then
             If MyMsgBox("还有下载任务尚未完成,是否确定退出?", "提示", "确定", "取消") = 1 Then
                 '强行结束下载任务
                 '强行结束下载任务
-                RunInNewThread(Sub()
-                                   Log("[System] 正在强行停止任务")
-                                   For Each Task As LoaderBase In LoaderTaskbar.ToArray
-                                       Task.Abort()
-                                   Next
-                               End Sub, "强行停止下载任务")
+                RunInNewThread(
+                Sub()
+                    Log("[System] 正在强行停止任务")
+                    For Each Task As LoaderBase In LoaderTaskbar.ToArray
+                        Task.Abort()
+                    Next
+                End Sub, "强行停止下载任务")
             Else
             Else
                 Exit Sub
                 Exit Sub
             End If
             End If
@@ -516,17 +551,19 @@ Public Class FormMain
                     PanBack.RenderTransform = New TransformGroup() With {.Children = New TransformCollection({TransformRotate, TransformPos, TransformScale})}
                     PanBack.RenderTransform = New TransformGroup() With {.Children = New TransformCollection({TransformRotate, TransformPos, TransformScale})}
                     AniStart({
                     AniStart({
                         AaOpacity(Me, -Opacity, 140, 40, New AniEaseOutFluent(AniEasePower.Weak)),
                         AaOpacity(Me, -Opacity, 140, 40, New AniEaseOutFluent(AniEasePower.Weak)),
-                        AaDouble(Sub(i)
-                                     TransformScale.ScaleX += i
-                                     TransformScale.ScaleY += i
-                                 End Sub, 0.88 - TransformScale.ScaleX, 180),
+                        AaDouble(
+                        Sub(i)
+                            TransformScale.ScaleX += i
+                            TransformScale.ScaleY += i
+                        End Sub, 0.88 - TransformScale.ScaleX, 180),
                         AaDouble(Sub(i) TransformPos.Y += i, 20 - TransformPos.Y, 180, 0, New AniEaseOutFluent(AniEasePower.Weak)),
                         AaDouble(Sub(i) TransformPos.Y += i, 20 - TransformPos.Y, 180, 0, New AniEaseOutFluent(AniEasePower.Weak)),
                         AaDouble(Sub(i) TransformRotate.Angle += i, 0.6 - TransformRotate.Angle, 180, 0, New AniEaseInoutFluent(AniEasePower.Weak)),
                         AaDouble(Sub(i) TransformRotate.Angle += i, 0.6 - TransformRotate.Angle, 180, 0, New AniEaseInoutFluent(AniEasePower.Weak)),
-                        AaCode(Sub()
-                                   IsHitTestVisible = False
-                                   Top = -10000
-                                   ShowInTaskbar = False
-                               End Sub, 210),
+                        AaCode(
+                        Sub()
+                            IsHitTestVisible = False
+                            Top = -10000
+                            ShowInTaskbar = False
+                        End Sub, 210),
                         AaCode(AddressOf EndProgramForce, 230)
                         AaCode(AddressOf EndProgramForce, 230)
                     }, "Form Close")
                     }, "Form Close")
                 Else
                 Else
@@ -653,7 +690,7 @@ Public Class FormMain
         If e.ChangedButton = MouseButton.XButton1 OrElse e.ChangedButton = MouseButton.XButton2 Then TriggerPageBack()
         If e.ChangedButton = MouseButton.XButton1 OrElse e.ChangedButton = MouseButton.XButton2 Then TriggerPageBack()
     End Sub
     End Sub
     Private Sub TriggerPageBack()
     Private Sub TriggerPageBack()
-        If PageCurrent = PageType.Download AndAlso PageCurrentSub = PageSubType.DownloadInstall Then
+        If PageCurrent = PageType.Download AndAlso PageCurrentSub = PageSubType.DownloadInstall AndAlso FrmDownloadInstall.IsInSelectPage Then
             FrmDownloadInstall.ExitSelectPage()
             FrmDownloadInstall.ExitSelectPage()
         Else
         Else
             PageBack()
             PageBack()
@@ -761,7 +798,7 @@ Public Class FormMain
         RunInNewThread(
         RunInNewThread(
         Sub()
         Sub()
             Dim FilePath As String = FilePathList.First
             Dim FilePath As String = FilePathList.First
-            Log("[System] 接受文件拖拽:" & FilePath & If(FilePathList.Count > 0, $" 等 {FilePathList.Count} 个文件", ""), LogLevel.Developer)
+            Log("[System] 接受文件拖拽:" & FilePath & If(FilePathList.Any, $" 等 {FilePathList.Count} 个文件", ""), LogLevel.Developer)
             '基础检查
             '基础检查
             If Directory.Exists(FilePathList.First) AndAlso Not File.Exists(FilePathList.First) Then
             If Directory.Exists(FilePathList.First) AndAlso Not File.Exists(FilePathList.First) Then
                 Hint("请拖入一个文件,而非文件夹!", HintType.Critical)
                 Hint("请拖入一个文件,而非文件夹!", HintType.Critical)
@@ -774,14 +811,14 @@ Public Class FormMain
             If FilePathList.Count > 1 Then
             If FilePathList.Count > 1 Then
                 '必须要求全部为 jar 文件
                 '必须要求全部为 jar 文件
                 For Each File In FilePathList
                 For Each File In FilePathList
-                    If Not {"jar", "litemod", "disabled", "old"}.Contains(File.Split(".").Last.ToLower) Then
+                    If Not {"jar", "litemod", "disabled", "old"}.Contains(File.After(".").ToLower) Then
                         Hint("一次请只拖入一个文件!", HintType.Critical)
                         Hint("一次请只拖入一个文件!", HintType.Critical)
                         Exit Sub
                         Exit Sub
                     End If
                     End If
                 Next
                 Next
             End If
             End If
             '自定义主页
             '自定义主页
-            Dim Extension As String = FilePath.Split(".").Last.ToLower
+            Dim Extension As String = FilePath.After(".").ToLower
             If Extension = "xaml" Then
             If Extension = "xaml" Then
                 Log("[System] 文件后缀为 XAML,作为自定义主页加载")
                 Log("[System] 文件后缀为 XAML,作为自定义主页加载")
                 If File.Exists(Path & "PCL\Custom.xaml") Then
                 If File.Exists(Path & "PCL\Custom.xaml") Then
@@ -1176,8 +1213,11 @@ Install:
     ''' 通过点击返回按钮或手动触发返回来改变页面。
     ''' 通过点击返回按钮或手动触发返回来改变页面。
     ''' </summary>
     ''' </summary>
     Public Sub PageBack() Handles BtnTitleInner.Click
     Public Sub PageBack() Handles BtnTitleInner.Click
-        If Not PageStack.Any() Then Exit Sub
-        PageChangeActual(PageStack(0))
+        If PageStack.Any() Then
+            PageChangeActual(PageStack(0))
+        Else
+            PageChange(PageType.Launch)
+        End If
     End Sub
     End Sub
 
 
     '实际处理页面切换
     '实际处理页面切换
@@ -1357,23 +1397,22 @@ Install:
     End Sub
     End Sub
     Private Sub PanMainLeft_Resize(NewWidth As Double)
     Private Sub PanMainLeft_Resize(NewWidth As Double)
         Dim Delta As Double = NewWidth - RectLeftBackground.Width
         Dim Delta As Double = NewWidth - RectLeftBackground.Width
-        If Math.Abs(Delta) < 0.1 Then Exit Sub
-        If AniControlEnabled = 0 Then
+        If Math.Abs(Delta) > 0.1 AndAlso AniControlEnabled = 0 Then
             If PanMain.Opacity < 0.1 Then PanMainLeft.IsHitTestVisible = False '避免左边栏指向背景未能完美覆盖左边栏
             If PanMain.Opacity < 0.1 Then PanMainLeft.IsHitTestVisible = False '避免左边栏指向背景未能完美覆盖左边栏
             If NewWidth > 0 Then
             If NewWidth > 0 Then
                 '宽度足够,显示
                 '宽度足够,显示
                 AniStart({
                 AniStart({
-                              AaWidth(RectLeftBackground, NewWidth - RectLeftBackground.Width, 400,, New AniEaseOutFluent(AniEasePower.ExtraStrong)),
-                              AaOpacity(RectLeftShadow, 1 - RectLeftShadow.Opacity, 200),
-                              AaCode(Sub() PanMainLeft.IsHitTestVisible = True, 250)
-                         }, "FrmMain LeftChange", True)
+                     AaWidth(RectLeftBackground, NewWidth - RectLeftBackground.Width, 400,, New AniEaseOutFluent(AniEasePower.ExtraStrong)),
+                     AaOpacity(RectLeftShadow, 1 - RectLeftShadow.Opacity, 200),
+                     AaCode(Sub() PanMainLeft.IsHitTestVisible = True, 250)
+                }, "FrmMain LeftChange", True)
             Else
             Else
                 '宽度不足,隐藏
                 '宽度不足,隐藏
                 AniStart({
                 AniStart({
-                              AaWidth(RectLeftBackground, -RectLeftBackground.Width, 200,, New AniEaseOutFluent),
-                              AaOpacity(RectLeftShadow, -RectLeftShadow.Opacity, 200),
-                              AaCode(Sub() PanMainLeft.IsHitTestVisible = True, 170)
-                         }, "FrmMain LeftChange", True)
+                     AaWidth(RectLeftBackground, -RectLeftBackground.Width, 200,, New AniEaseOutFluent),
+                     AaOpacity(RectLeftShadow, -RectLeftShadow.Opacity, 200),
+                     AaCode(Sub() PanMainLeft.IsHitTestVisible = True, 170)
+                }, "FrmMain LeftChange", True)
             End If
             End If
         Else
         Else
             RectLeftBackground.Width = NewWidth
             RectLeftBackground.Width = NewWidth

+ 115 - 57
Plain Craft Launcher 2/Modules/Base/ModBase.vb

@@ -11,12 +11,12 @@ Public Module ModBase
 #Region "声明"
 #Region "声明"
 
 
     '下列版本信息由更新器自动修改
     '下列版本信息由更新器自动修改
-    Public Const VersionBaseName As String = "2.7.1" '不含分支前缀的显示用版本名
-    Public Const VersionStandardCode As String = "2.7.1." & VersionBranchCode '标准格式的四段式版本号
+    Public Const VersionBaseName As String = "2.7.4" '不含分支前缀的显示用版本名
+    Public Const VersionStandardCode As String = "2.7.4." & VersionBranchCode '标准格式的四段式版本号
 #If BETA Then
 #If BETA Then
-    Public Const VersionCode As Integer = 319 'Release
+    Public Const VersionCode As Integer = 323 'Release
 #Else
 #Else
-    Public Const VersionCode As Integer = 320 'Snapshot
+    Public Const VersionCode As Integer = 325 'Snapshot
 #End If
 #End If
     '自动生成的版本信息
     '自动生成的版本信息
     Public Const VersionDisplayName As String = VersionBranchName & " " & VersionBaseName
     Public Const VersionDisplayName As String = VersionBranchName & " " & VersionBaseName
@@ -26,7 +26,7 @@ Public Module ModBase
 #ElseIf BETA Then
 #ElseIf BETA Then
     Public Const VersionBranchName As String = "Release"
     Public Const VersionBranchName As String = "Release"
     Public Const VersionBranchCode As String = "50"
     Public Const VersionBranchCode As String = "50"
-#ElseIf DEBUG Then
+#Else
     Public Const VersionBranchName As String = "Debug"
     Public Const VersionBranchName As String = "Debug"
     Public Const VersionBranchCode As String = "100"
     Public Const VersionBranchCode As String = "100"
 #End If
 #End If
@@ -58,7 +58,7 @@ Public Module ModBase
     ''' <summary>
     ''' <summary>
     ''' 程序的打开计时。
     ''' 程序的打开计时。
     ''' </summary>
     ''' </summary>
-    Public ApplicationStartTick As Long
+    Public ApplicationStartTick As Long = GetTimeTick()
     ''' <summary>
     ''' <summary>
     ''' 程序打开时的时间。
     ''' 程序打开时的时间。
     ''' </summary>
     ''' </summary>
@@ -76,6 +76,10 @@ Public Module ModBase
     ''' </summary>
     ''' </summary>
     Public Is32BitSystem As Boolean = Not Environment.Is64BitOperatingSystem
     Public Is32BitSystem As Boolean = Not Environment.Is64BitOperatingSystem
     ''' <summary>
     ''' <summary>
+    ''' 是否使用 GBK 编码。
+    ''' </summary>
+    Public IsGBKEncoding As Boolean = Encoding.Default.CodePage = 936
+    ''' <summary>
     ''' 操作系统版本。Win10 为 10.0。
     ''' 操作系统版本。Win10 为 10.0。
     ''' </summary>
     ''' </summary>
     Public OsVersion As Version = Environment.OSVersion.Version
     Public OsVersion As Version = Environment.OSVersion.Version
@@ -591,7 +595,7 @@ Public Module ModBase
         Try
         Try
             Dim parentKey As Microsoft.Win32.RegistryKey, softKey As Microsoft.Win32.RegistryKey
             Dim parentKey As Microsoft.Win32.RegistryKey, softKey As Microsoft.Win32.RegistryKey
             parentKey = My.Computer.Registry.CurrentUser
             parentKey = My.Computer.Registry.CurrentUser
-            softKey = parentKey.OpenSubKey("Software\PCL", True)
+            softKey = parentKey.OpenSubKey("Software\" & RegFolder, True)
             If softKey Is Nothing Then
             If softKey Is Nothing Then
                 ReadReg = DefaultValue '不存在则返回默认值
                 ReadReg = DefaultValue '不存在则返回默认值
             Else
             Else
@@ -612,8 +616,8 @@ Public Module ModBase
         Try
         Try
             Dim parentKey As Microsoft.Win32.RegistryKey, softKey As Microsoft.Win32.RegistryKey
             Dim parentKey As Microsoft.Win32.RegistryKey, softKey As Microsoft.Win32.RegistryKey
             parentKey = My.Computer.Registry.CurrentUser
             parentKey = My.Computer.Registry.CurrentUser
-            softKey = parentKey.OpenSubKey("Software\PCL", True)
-            If softKey Is Nothing Then softKey = parentKey.CreateSubKey("Software\PCL") '如果不存在就创建  
+            softKey = parentKey.OpenSubKey("Software\" & RegFolder, True)
+            If softKey Is Nothing Then softKey = parentKey.CreateSubKey("Software\" & RegFolder) '如果不存在就创建  
             softKey.SetValue(Key, Value)
             softKey.SetValue(Key, Value)
         Catch ex As Exception
         Catch ex As Exception
             Log(ex, "写入注册表出错:" & Key, If(ShowException, LogLevel.Hint, LogLevel.Developer))
             Log(ex, "写入注册表出错:" & Key, If(ShowException, LogLevel.Hint, LogLevel.Developer))
@@ -726,8 +730,8 @@ Public Module ModBase
     Public Function GetFileNameFromPath(FilePath As String) As String
     Public Function GetFileNameFromPath(FilePath As String) As String
         FilePath = FilePath.Replace("/", "\")
         FilePath = FilePath.Replace("/", "\")
         If FilePath.EndsWithF("\") Then Throw New Exception("不包含文件名:" & FilePath)
         If FilePath.EndsWithF("\") Then Throw New Exception("不包含文件名:" & FilePath)
-        If FilePath.Contains("\") Then FilePath = FilePath.Substring(FilePath.LastIndexOfF("\") + 1)
         If FilePath.Contains("?") Then FilePath = FilePath.Substring(0, FilePath.IndexOfF("?")) '去掉网络参数后的 ?
         If FilePath.Contains("?") Then FilePath = FilePath.Substring(0, FilePath.IndexOfF("?")) '去掉网络参数后的 ?
+        If FilePath.Contains("\") Then FilePath = FilePath.Substring(FilePath.LastIndexOfF("\") + 1)
         Dim length As Integer = FilePath.Length
         Dim length As Integer = FilePath.Length
         If length = 0 Then Throw New Exception("不包含文件名:" & FilePath)
         If length = 0 Then Throw New Exception("不包含文件名:" & FilePath)
         If length > 250 Then Throw New PathTooLongException("文件名过长:" & FilePath)
         If length > 250 Then Throw New PathTooLongException("文件名过长:" & FilePath)
@@ -1215,43 +1219,35 @@ Re:
     ''' <summary>
     ''' <summary>
     ''' 尝试根据后缀名判断文件种类并解压文件,支持 gz 与 zip,会尝试将 jar 以 zip 方式解压。
     ''' 尝试根据后缀名判断文件种类并解压文件,支持 gz 与 zip,会尝试将 jar 以 zip 方式解压。
     ''' 会尝试创建,但不会清空目标文件夹。
     ''' 会尝试创建,但不会清空目标文件夹。
-    ''' 成功返回 True,并非压缩文件或失败返回 False。
     ''' </summary>
     ''' </summary>
-    Public Function ExtractFile(CompressFilePath As String, DestDirectory As String, Optional Encode As Encoding = Nothing) As Boolean
-        Try
-            Directory.CreateDirectory(DestDirectory)
-            If CompressFilePath.EndsWithF(".gz", True) Then
-                '以 gz 方式解压
-                Dim stream As New GZipStream(New FileStream(CompressFilePath, FileMode.Open, FileAccess.ReadWrite), CompressionMode.Decompress)
-                Dim decompressedFile As New FileStream(DestDirectory & GetFileNameFromPath(CompressFilePath).ToLower.Replace(".tar", "").Replace(".gz", ""), FileMode.OpenOrCreate, FileAccess.Write)
-                Dim data As Integer = stream.ReadByte()
-                While data <> -1
-                    decompressedFile.WriteByte(data)
-                    data = stream.ReadByte()
-                End While
-                decompressedFile.Close()
-                stream.Close()
-                Return True
-            Else
-                '以 zip 方式解压
-                Using Archive = ZipFile.Open(CompressFilePath, ZipArchiveMode.Read, If(Encode, Encoding.GetEncoding("GB18030")))
-                    For Each Entry As ZipArchiveEntry In Archive.Entries
-                        Dim DestinationPath As String = IO.Path.Combine(DestDirectory, Entry.FullName)
-                        If DestinationPath.EndsWithF("\") OrElse DestinationPath.EndsWithF("/") Then
-                            Continue For '不创建空文件夹
-                        Else
-                            Directory.CreateDirectory(GetPathFromFullPath(DestinationPath))
-                            Entry.ExtractToFile(DestinationPath, True)
-                        End If
-                    Next
-                End Using
-                Return True
-            End If
-        Catch ex As Exception
-            Log(ex, "尝试解压文件失败")
-            Return False
-        End Try
-    End Function
+    Public Sub ExtractFile(CompressFilePath As String, DestDirectory As String, Optional Encode As Encoding = Nothing)
+        Directory.CreateDirectory(DestDirectory)
+        If CompressFilePath.EndsWithF(".gz", True) Then
+            '以 gz 方式解压
+            Dim stream As New GZipStream(New FileStream(CompressFilePath, FileMode.Open, FileAccess.ReadWrite), CompressionMode.Decompress)
+            Dim decompressedFile As New FileStream(DestDirectory & GetFileNameFromPath(CompressFilePath).ToLower.Replace(".tar", "").Replace(".gz", ""), FileMode.OpenOrCreate, FileAccess.Write)
+            Dim data As Integer = stream.ReadByte()
+            While data <> -1
+                decompressedFile.WriteByte(data)
+                data = stream.ReadByte()
+            End While
+            decompressedFile.Close()
+            stream.Close()
+        Else
+            '以 zip 方式解压
+            Using Archive = ZipFile.Open(CompressFilePath, ZipArchiveMode.Read, If(Encode, Encoding.GetEncoding("GB18030")))
+                For Each Entry As ZipArchiveEntry In Archive.Entries
+                    Dim DestinationPath As String = IO.Path.Combine(DestDirectory, Entry.FullName)
+                    If DestinationPath.EndsWithF("\") OrElse DestinationPath.EndsWithF("/") Then
+                        Continue For '不创建空文件夹
+                    Else
+                        Directory.CreateDirectory(GetPathFromFullPath(DestinationPath))
+                        Entry.ExtractToFile(DestinationPath, True)
+                    End If
+                Next
+            End Using
+        End If
+    End Sub
 
 
     ''' <summary>
     ''' <summary>
     ''' 删除文件夹,返回删除的文件个数。通过参数选择是否抛出异常。
     ''' 删除文件夹,返回删除的文件个数。通过参数选择是否抛出异常。
@@ -1342,7 +1338,7 @@ Re:
         Loop
         Loop
         DescList = DescList.Distinct.ToList
         DescList = DescList.Distinct.ToList
         Dim Desc As String = Join(DescList, vbCrLf & "→ ")
         Dim Desc As String = Join(DescList, vbCrLf & "→ ")
-        Dim Stack As String = If(StackList.Count > 0, vbCrLf & Join(StackList, vbCrLf), "")
+        Dim Stack As String = If(StackList.Any, vbCrLf & Join(StackList, vbCrLf), "")
 
 
         '常见错误(记得同时修改下面的)
         '常见错误(记得同时修改下面的)
         Dim CommonReason As String = Nothing
         Dim CommonReason As String = Nothing
@@ -1455,7 +1451,8 @@ Re:
         Try
         Try
             Return JsonConvert.DeserializeObject(Data, New JsonSerializerSettings With {.DateTimeZoneHandling = DateTimeZoneHandling.Local})
             Return JsonConvert.DeserializeObject(Data, New JsonSerializerSettings With {.DateTimeZoneHandling = DateTimeZoneHandling.Local})
         Catch ex As Exception
         Catch ex As Exception
-            Throw New Exception("格式化 json 对象失败:" & If(If(Data, "").Length > 10000, Data.Substring(0, 100) & "...", Data))
+            Dim Length As Integer = If(Data, "").Length
+            Throw New Exception("格式化 json 对象失败:" & If(Length > 10000, Data.Substring(0, 100) & $"...(全长 {Length} 个字符)..." & Right(Data, 100), Data))
         End Try
         End Try
     End Function
     End Function
 
 
@@ -1541,6 +1538,57 @@ Re:
         Next
         Next
         Return tmp.ToString()
         Return tmp.ToString()
     End Function
     End Function
+    ''' <summary>
+    ''' 检查字符串中的字符是否均为 ASCII 字符。
+    ''' </summary>
+    <Extension> Public Function IsASCII(Input As String) As Boolean
+        Return Input.All(Function(c) AscW(c) < 128)
+    End Function
+
+    ''' <summary>
+    ''' 获取在子字符串之前的部分。
+    ''' 会裁切尽可能多的内容,但如果未找到子字符串则不裁切。
+    ''' </summary>
+    <Extension> Public Function Before(Str As String, Text As String, Optional IgnoreCase As Boolean = False) As String
+        Dim Pos As Integer = If(String.IsNullOrEmpty(Text), -1, Str.IndexOfF(Text, IgnoreCase))
+        If Pos >= 0 Then
+            Return Str.Substring(0, Pos)
+        Else
+            Return Str
+        End If
+    End Function
+    ''' <summary>
+    ''' 获取在子字符串之后的部分。
+    ''' 会裁切尽可能多的内容,但如果未找到子字符串则不裁切。
+    ''' </summary>
+    <Extension> Public Function After(Str As String, Text As String, Optional IgnoreCase As Boolean = False) As String
+        Dim Pos As Integer = If(String.IsNullOrEmpty(Text), -1, Str.LastIndexOfF(Text, IgnoreCase))
+        If Pos >= 0 Then
+            Return Str.Substring(Pos + Text.Length)
+        Else
+            Return Str
+        End If
+    End Function
+    ''' <summary>
+    ''' 获取处于两个子字符串之间的部分。
+    ''' 会裁切尽可能多的内容:匹配开始使用 LastIndexOf,匹配结束使用 IndexOf,但如果未找到子字符串则不裁切。
+    ''' </summary>
+    <Extension> Public Function Between(Str As String, Before As String, After As String, Optional IgnoreCase As Boolean = False) As String
+        Dim StartPos As Integer = If(String.IsNullOrEmpty(Before), -1, Str.LastIndexOfF(Before, IgnoreCase))
+        If StartPos >= 0 Then
+            StartPos += Before.Length
+        Else
+            StartPos = 0
+        End If
+        Dim EndPos As Integer = If(String.IsNullOrEmpty(After), -1, Str.IndexOfF(After, StartPos, IgnoreCase))
+        If EndPos >= 0 Then
+            Return Str.Substring(StartPos, EndPos - StartPos)
+        ElseIf StartPos > 0 Then
+            Return Str.Substring(StartPos)
+        Else
+            Return Str
+        End If
+    End Function
 
 
     ''' <summary>
     ''' <summary>
     ''' 高速的 StartsWith。
     ''' 高速的 StartsWith。
@@ -1571,12 +1619,26 @@ Re:
         Return Str.IndexOf(SubStr, If(IgnoreCase, StringComparison.OrdinalIgnoreCase, StringComparison.Ordinal))
         Return Str.IndexOf(SubStr, If(IgnoreCase, StringComparison.OrdinalIgnoreCase, StringComparison.Ordinal))
     End Function
     End Function
     ''' <summary>
     ''' <summary>
+    ''' 高速的 IndexOf。
+    ''' </summary>
+    <Extension> <MethodImpl(MethodImplOptions.AggressiveInlining)>
+    Public Function IndexOfF(Str As String, SubStr As String, StartIndex As Integer, Optional IgnoreCase As Boolean = False) As Integer
+        Return Str.IndexOf(SubStr, StartIndex, If(IgnoreCase, StringComparison.OrdinalIgnoreCase, StringComparison.Ordinal))
+    End Function
+    ''' <summary>
     ''' 高速的 LastIndexOf。
     ''' 高速的 LastIndexOf。
     ''' </summary>
     ''' </summary>
     <Extension> <MethodImpl(MethodImplOptions.AggressiveInlining)>
     <Extension> <MethodImpl(MethodImplOptions.AggressiveInlining)>
     Public Function LastIndexOfF(Str As String, SubStr As String, Optional IgnoreCase As Boolean = False) As Integer
     Public Function LastIndexOfF(Str As String, SubStr As String, Optional IgnoreCase As Boolean = False) As Integer
         Return Str.LastIndexOf(SubStr, If(IgnoreCase, StringComparison.OrdinalIgnoreCase, StringComparison.Ordinal))
         Return Str.LastIndexOf(SubStr, If(IgnoreCase, StringComparison.OrdinalIgnoreCase, StringComparison.Ordinal))
     End Function
     End Function
+    ''' <summary>
+    ''' 高速的 LastIndexOf。
+    ''' </summary>
+    <Extension> <MethodImpl(MethodImplOptions.AggressiveInlining)>
+    Public Function LastIndexOfF(Str As String, SubStr As String, StartIndex As Integer, Optional IgnoreCase As Boolean = False) As Integer
+        Return Str.LastIndexOf(SubStr, StartIndex, If(IgnoreCase, StringComparison.OrdinalIgnoreCase, StringComparison.Ordinal))
+    End Function
 
 
     ''' <summary>
     ''' <summary>
     ''' 输入 And 字符不会报错的 Val。
     ''' 输入 And 字符不会报错的 Val。
@@ -1815,15 +1877,11 @@ Re:
     ''' <summary>
     ''' <summary>
     ''' 数组去重。
     ''' 数组去重。
     ''' </summary>
     ''' </summary>
-    Public Function Distinct(Of T)(Arr As ICollection(Of T), Optional IsEqual As CompareThreadStart(Of T) = Nothing) As List(Of T)
+    <Extension> Public Function Distinct(Of T)(Arr As ICollection(Of T), IsEqual As CompareThreadStart(Of T)) As List(Of T)
         Dim ResultArray As New List(Of T)
         Dim ResultArray As New List(Of T)
         For i = 0 To Arr.Count - 1
         For i = 0 To Arr.Count - 1
             For ii = i + 1 To Arr.Count - 1
             For ii = i + 1 To Arr.Count - 1
-                If IsEqual Is Nothing Then
-                    If Arr(i).Equals(Arr(ii)) Then GoTo NextElement
-                Else
-                    If IsEqual(Arr(i), Arr(ii)) Then GoTo NextElement
-                End If
+                If IsEqual(Arr(i), Arr(ii)) Then GoTo NextElement
             Next
             Next
             ResultArray.Add(Arr(i))
             ResultArray.Add(Arr(i))
 NextElement:
 NextElement:
@@ -2071,9 +2129,9 @@ NextElement:
     ''' 按照既定的函数进行选择排序。
     ''' 按照既定的函数进行选择排序。
     ''' </summary>
     ''' </summary>
     ''' <param name="SortRule">传入两个对象,若第一个对象应该排在前面,则返回 True。</param>
     ''' <param name="SortRule">传入两个对象,若第一个对象应该排在前面,则返回 True。</param>
-    Public Function Sort(Of T)(List As IList(Of T), SortRule As CompareThreadStart(Of T)) As List(Of T)
+    <Extension> Public Function Sort(Of T)(List As IList(Of T), SortRule As CompareThreadStart(Of T)) As List(Of T)
         Dim NewList As New List(Of T)
         Dim NewList As New List(Of T)
-        While List.Count > 0
+        While List.Any
             Dim Highest = List(0)
             Dim Highest = List(0)
             For i = 1 To List.Count - 1
             For i = 1 To List.Count - 1
                 If SortRule(List(i), Highest) Then Highest = List(i)
                 If SortRule(List(i), Highest) Then Highest = List(i)
@@ -2646,7 +2704,7 @@ Retry:
     ''' </summary>
     ''' </summary>
     Public Function Shuffle(Of T)(array As IList(Of T)) As IList(Of T)
     Public Function Shuffle(Of T)(array As IList(Of T)) As IList(Of T)
         Shuffle = New List(Of T)
         Shuffle = New List(Of T)
-        Do While array.Count > 0
+        Do While array.Any
             Dim i As Integer = RandomInteger(0, array.Count - 1)
             Dim i As Integer = RandomInteger(0, array.Count - 1)
             Shuffle.Add(array(i))
             Shuffle.Add(array(i))
             array.RemoveAt(i)
             array.RemoveAt(i)

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

@@ -266,7 +266,7 @@
         Public Output As OutputType = Nothing
         Public Output As OutputType = Nothing
 
 
         '获取输入
         '获取输入
-        Public Function StartGetInput(Optional Input As InputType = Nothing, Optional InputDelegate As Func(Of Object) = Nothing) As InputType
+        Public Function StartGetInput(Optional Input As InputType = Nothing, Optional InputDelegate As Func(Of Object) = Nothing) As InputType 'InputDelegate 参数存在匿名调用
             If InputDelegate Is Nothing Then InputDelegate = Me.InputDelegate
             If InputDelegate Is Nothing Then InputDelegate = Me.InputDelegate
             Dim NewInput As InputType = Nothing '若 InputType 不能为 Nothing,则会导致 Input Is Nothing 永远失败,因此需要额外判断
             Dim NewInput As InputType = Nothing '若 InputType 不能为 Nothing,则会导致 Input Is Nothing 永远失败,因此需要额外判断
             If (Input Is Nothing OrElse (NewInput IsNot Nothing AndAlso Input.Equals(NewInput))) AndAlso InputDelegate IsNot Nothing Then
             If (Input Is Nothing OrElse (NewInput IsNot Nothing AndAlso Input.Equals(NewInput))) AndAlso InputDelegate IsNot Nothing Then
@@ -571,7 +571,7 @@ Restart:
     Public LoaderTaskbarProgress As Double = 0 '平滑后的进度
     Public LoaderTaskbarProgress As Double = 0 '平滑后的进度
     Private LoaderTaskbarProgressLast As Shell.TaskbarItemProgressState = Shell.TaskbarItemProgressState.None
     Private LoaderTaskbarProgressLast As Shell.TaskbarItemProgressState = Shell.TaskbarItemProgressState.None
 
 
-    Public Sub LoaderTaskbarAdd(Loader As LoaderBase)
+    Public Sub LoaderTaskbarAdd(Of T)(Loader As LoaderCombo(Of T))
         If FrmSpeedLeft IsNot Nothing Then FrmSpeedLeft.TaskRemove(Loader)
         If FrmSpeedLeft IsNot Nothing Then FrmSpeedLeft.TaskRemove(Loader)
         SyncLock LoaderTaskbarLock
         SyncLock LoaderTaskbarLock
             LoaderTaskbar.Add(Loader)
             LoaderTaskbar.Add(Loader)
@@ -587,11 +587,11 @@ Restart:
                 '外显任务是否已经全部完成
                 '外显任务是否已经全部完成
                 Dim IsAllDownloadTaskCompleted As Boolean = True
                 Dim IsAllDownloadTaskCompleted As Boolean = True
                 For Each Loader In LoaderTaskbar
                 For Each Loader In LoaderTaskbar
-                    If Loader.Show AndAlso Loader.State = LoadState.Loading Then IsAllDownloadTaskCompleted = False
+                    If Loader.State = LoadState.Loading Then IsAllDownloadTaskCompleted = False
                 Next
                 Next
                 '若单个任务已中止或全部任务已完成,则刷新并移除
                 '若单个任务已中止或全部任务已完成,则刷新并移除
                 For Each Task In LoaderTaskbar.ToList()
                 For Each Task In LoaderTaskbar.ToList()
-                    If (IsAllDownloadTaskCompleted OrElse Task.State = LoadState.Aborted OrElse Task.State = LoadState.Waiting) AndAlso Task.Show Then
+                    If IsAllDownloadTaskCompleted OrElse Task.State = LoadState.Aborted OrElse Task.State = LoadState.Waiting Then
                         If FrmSpeedLeft IsNot Nothing Then FrmSpeedLeft.TaskRefresh(Task)
                         If FrmSpeedLeft IsNot Nothing Then FrmSpeedLeft.TaskRefresh(Task)
                         LoaderTaskbar.Remove(Task)
                         LoaderTaskbar.Remove(Task)
                         Log($"[Taskbar] {Task.Name} 已移出任务列表")
                         Log($"[Taskbar] {Task.Name} 已移出任务列表")
@@ -628,7 +628,6 @@ Restart:
             Dim Total As Double = 0, Count As Integer = 0
             Dim Total As Double = 0, Count As Integer = 0
             SyncLock LoaderTaskbarLock
             SyncLock LoaderTaskbarLock
                 For Each Task In LoaderTaskbar.ToList
                 For Each Task In LoaderTaskbar.ToList
-                    If Not Task.Show Then Continue For
                     Total += Task.Progress
                     Total += Task.Progress
                     Count += 1 '避免多线程影响导致计数出错
                     Count += 1 '避免多线程影响导致计数出错
                 Next
                 Next

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

@@ -1,4 +1,5 @@
 Imports System.Net
 Imports System.Net
+Imports PCL.ModNet
 
 
 Public Module ModNet
 Public Module ModNet
     Public Const NetDownloadEnd As String = ".PCLDownloading"
     Public Const NetDownloadEnd As String = ".PCLDownloading"
@@ -235,11 +236,11 @@ RequestFinished:
     ''' 以多线程下载网页文件的方式获取网页源代码。
     ''' 以多线程下载网页文件的方式获取网页源代码。
     ''' </summary>
     ''' </summary>
     ''' <param name="Urls">网页的 Url 列表。</param>
     ''' <param name="Urls">网页的 Url 列表。</param>
-    Public Function NetGetCodeByDownload(Urls As String(), Optional Timeout As Integer = 45000, Optional IsJson As Boolean = False, Optional UseBrowserUserAgent As Boolean = False) As String
-        Dim Temp As String = PathTemp & "Cache\Code\" & Urls(0).GetHashCode() & "_" & GetUuid()
+    Public Function NetGetCodeByDownload(Urls As IEnumerable(Of String), Optional Timeout As Integer = 45000, Optional IsJson As Boolean = False, Optional UseBrowserUserAgent As Boolean = False) As String
+        Dim Temp As String = PathTemp & "Cache\Code\" & Urls.First.GetHashCode() & "_" & GetUuid()
         Dim NewTask As New LoaderDownload("源码获取 " & GetUuid() & "#", New List(Of NetFile) From {New NetFile(Urls, Temp, New FileChecker With {.IsJson = IsJson}, UseBrowserUserAgent)})
         Dim NewTask As New LoaderDownload("源码获取 " & GetUuid() & "#", New List(Of NetFile) From {New NetFile(Urls, Temp, New FileChecker With {.IsJson = IsJson}, UseBrowserUserAgent)})
         Try
         Try
-            NewTask.WaitForExitTime(Timeout, TimeoutMessage:="连接服务器超时(第一下载源:" & Urls(0) & ")")
+            NewTask.WaitForExitTime(Timeout, TimeoutMessage:="连接服务器超时(第一下载源:" & Urls.First & ")")
             NetGetCodeByDownload = ReadFile(Temp)
             NetGetCodeByDownload = ReadFile(Temp)
             File.Delete(Temp)
             File.Delete(Temp)
         Finally
         Finally
@@ -448,7 +449,7 @@ RequestFinished:
     ''' <summary>
     ''' <summary>
     ''' 速度下限。
     ''' 速度下限。
     ''' </summary>
     ''' </summary>
-    Public NetTaskSpeedLimitLow As Long = 1024 * 1024L
+    Public NetTaskSpeedLimitLow As Long = 256 * 1024L '256K/s
     ''' <summary>
     ''' <summary>
     ''' 速度上限。若无限制则为 -1。
     ''' 速度上限。若无限制则为 -1。
     ''' </summary>
     ''' </summary>
@@ -516,7 +517,7 @@ RequestFinished:
         ''' </summary>
         ''' </summary>
         Finish = 6
         Finish = 6
         ''' <summary>
         ''' <summary>
-        ''' 已失败。
+        ''' 已失败或中断。
         ''' </summary>
         ''' </summary>
         [Error] = 7
         [Error] = 7
     End Enum
     End Enum
@@ -601,7 +602,7 @@ RequestFinished:
                 SyncLock Task.LockChain
                 SyncLock Task.LockChain
                     If NextThread Is Nothing Then
                     If NextThread Is Nothing Then
                         If Task.IsUnknownSize Then
                         If Task.IsUnknownSize Then
-                            Return 1024 * 1024 * 1024 '1G
+                            Return 5 * 1024 * 1024 * 1024L '5G
                         Else
                         Else
                             Return Task.FileSize - 1
                             Return Task.FileSize - 1
                         End If
                         End If
@@ -616,7 +617,7 @@ RequestFinished:
         ''' </summary>
         ''' </summary>
         Public ReadOnly Property DownloadUndone As Long
         Public ReadOnly Property DownloadUndone As Long
             Get
             Get
-                Return DownloadEnd + 1 - (DownloadStart + DownloadDone)
+                Return DownloadEnd - (DownloadStart + DownloadDone) + 1
             End Get
             End Get
         End Property
         End Property
         ''' <summary>
         ''' <summary>
@@ -715,7 +716,7 @@ RequestFinished:
                         CurrentSource = Sources(Id)
                         CurrentSource = Sources(Id)
                     End While
                     End While
                     Return CurrentSource
                     Return CurrentSource
-                ElseIf SourcesOnce.Count > 0 Then
+                ElseIf SourcesOnce.Any Then
                     '仅存在单线程可用源
                     '仅存在单线程可用源
                     Return SourcesOnce(0)
                     Return SourcesOnce(0)
                 Else
                 Else
@@ -728,7 +729,7 @@ RequestFinished:
         ''' 是否已经没有可用源了。
         ''' 是否已经没有可用源了。
         ''' </summary>
         ''' </summary>
         Public Function IsSourceFailed(Optional AllowOnceSource As Boolean = True) As Boolean
         Public Function IsSourceFailed(Optional AllowOnceSource As Boolean = True) As Boolean
-            If AllowOnceSource AndAlso SourcesOnce.Count > 0 Then Return False
+            If AllowOnceSource AndAlso SourcesOnce.Any Then Return False
             SyncLock LockSource
             SyncLock LockSource
                 For Each Source As NetSource In Sources
                 For Each Source As NetSource In Sources
                     If Not Source.IsFailed Then Return False
                     If Not Source.IsFailed Then Return False
@@ -754,15 +755,12 @@ RequestFinished:
         ''' 导致下载失败的原因。
         ''' 导致下载失败的原因。
         ''' </summary>
         ''' </summary>
         Public Ex As New List(Of Exception)
         Public Ex As New List(Of Exception)
-        '''' <summary>
-        '''' 文件进行整体重试的计数。
-        '''' </summary>
-        'Private RetryCount As Integer = 0
 
 
         ''' <summary>
         ''' <summary>
         ''' 作为文件组成部分的线程链表。
         ''' 作为文件组成部分的线程链表。
+        ''' 如果没有线程,可以为 Nothing。
         ''' </summary>
         ''' </summary>
-        Private Threads As NetThread
+        Public Threads As NetThread
 
 
         ''' <summary>
         ''' <summary>
         ''' 文件的总大小。若为 -2 则为未获取,若为 -1 则为无法获取准确大小。
         ''' 文件的总大小。若为 -2 则为未获取,若为 -1 则为无法获取准确大小。
@@ -783,7 +781,7 @@ RequestFinished:
         ''' <summary>
         ''' <summary>
         ''' 为不需要分割的小文件进行临时存储。
         ''' 为不需要分割的小文件进行临时存储。
         ''' </summary>
         ''' </summary>
-        Private SmailFileCache As List(Of Byte)
+        Private SmailFileCache As Queue(Of Byte)
 
 
         ''' <summary>
         ''' <summary>
         ''' 文件的已下载大小。
         ''' 文件的已下载大小。
@@ -844,10 +842,10 @@ RequestFinished:
                     Case NetState.Get
                     Case NetState.Get
                         Return 0.04
                         Return 0.04
                     Case NetState.Download
                     Case NetState.Download
-                        '正在下载中,对应 5% ~ 99%
+                        '正在下载中,对应 5% ~ 98%
                         Dim OriginalProgress As Double = If(IsUnknownSize, 0.5, DownloadDone / Math.Max(FileSize, 1))
                         Dim OriginalProgress As Double = If(IsUnknownSize, 0.5, DownloadDone / Math.Max(FileSize, 1))
                         OriginalProgress = 1 - (1 - OriginalProgress) ^ 0.9
                         OriginalProgress = 1 - (1 - OriginalProgress) ^ 0.9
-                        Return OriginalProgress * 0.94 + 0.05
+                        Return OriginalProgress * 0.93 + 0.05
                     Case NetState.Merge
                     Case NetState.Merge
                         Return 0.99
                         Return 0.99
                     Case NetState.Finish, NetState.Error
                     Case NetState.Finish, NetState.Error
@@ -878,14 +876,13 @@ RequestFinished:
         End Property
         End Property
 
 
         Private Const FilePieceLimit As Long = 256 * 1024
         Private Const FilePieceLimit As Long = 256 * 1024
-        Public ReadOnly RandomCode As Integer = RandomInteger(0, 999999)
         Public ReadOnly LockCount As New Object
         Public ReadOnly LockCount As New Object
         Public ReadOnly LockState As New Object
         Public ReadOnly LockState As New Object
         Public ReadOnly LockChain As New Object
         Public ReadOnly LockChain As New Object
         Public ReadOnly LockSource As New Object
         Public ReadOnly LockSource As New Object
         Public ReadOnly LockTasks As New Object
         Public ReadOnly LockTasks As New Object
 
 
-        Private ReadOnly Uuid As Integer = GetUuid()
+        Public ReadOnly Uuid As Integer = GetUuid()
         Public Overrides Function Equals(obj As Object) As Boolean
         Public Overrides Function Equals(obj As Object) As Boolean
             Dim file = TryCast(obj, NetFile)
             Dim file = TryCast(obj, NetFile)
             Return file IsNot Nothing AndAlso Uuid = file.Uuid
             Return file IsNot Nothing AndAlso Uuid = file.Uuid
@@ -914,14 +911,15 @@ RequestFinished:
 
 
         ''' <summary>
         ''' <summary>
         ''' 尝试开始一个新的下载线程。
         ''' 尝试开始一个新的下载线程。
+        ''' 如果失败,返回 Nothing。
         ''' </summary>
         ''' </summary>
-        Public Function TryBeginThread() As Boolean
+        Public Function TryBeginThread() As NetThread
             Try
             Try
 
 
                 '条件检测
                 '条件检测
                 If NetTaskThreadCount >= NetTaskThreadLimit OrElse IsSourceFailed() OrElse
                 If NetTaskThreadCount >= NetTaskThreadLimit OrElse IsSourceFailed() OrElse
-               (IsNoSplit AndAlso Threads IsNot Nothing AndAlso Threads.State <> NetState.Error) Then Return False
-                If State >= NetState.Merge OrElse State = NetState.WaitForCheck Then Return False
+               (IsNoSplit AndAlso Threads IsNot Nothing AndAlso Threads.State <> NetState.Error) Then Return Nothing
+                If State >= NetState.Merge OrElse State = NetState.WaitForCheck Then Return Nothing
                 SyncLock LockState
                 SyncLock LockState
                     If State < NetState.Connect Then State = NetState.Connect
                     If State < NetState.Connect Then State = NetState.Connect
                 End SyncLock
                 End SyncLock
@@ -936,7 +934,7 @@ RequestFinished:
                     '单线程
                     '单线程
                     If IsSourceFailed(False) Then
                     If IsSourceFailed(False) Then
                         '确认没有其他线程正使用此点
                         '确认没有其他线程正使用此点
-                        If SourcesOnce(0).Thread IsNot Nothing AndAlso SourcesOnce(0).Thread.State <> NetState.Error Then Return False
+                        If SourcesOnce(0).Thread IsNot Nothing AndAlso SourcesOnce(0).Thread.State <> NetState.Error Then Return Nothing
                         '占用此点
                         '占用此点
 Capture:
 Capture:
                         SmailFileCache = Nothing
                         SmailFileCache = Nothing
@@ -952,14 +950,8 @@ Capture:
                     If Threads Is Nothing Then
                     If Threads Is Nothing Then
                         StartPosition = 0
                         StartPosition = 0
                         StartSource = GetSource(FirstThreadSource)
                         StartSource = GetSource(FirstThreadSource)
-                        If StartSource Is Nothing Then
-                            '没有可用源了
-                            Throw New Exception("无可用源,请反馈此问题。")
-                        Else
-                            '有可用源
-                            FirstThreadSource = StartSource.Id + 1
-                            GoTo StartThread
-                        End If
+                        FirstThreadSource = StartSource.Id + 1
+                        GoTo StartThread
                     End If
                     End If
                     '寻找失败点
                     '寻找失败点
                     For Each Thread As NetThread In Threads
                     For Each Thread As NetThread In Threads
@@ -971,24 +963,25 @@ Capture:
                     Next
                     Next
                     '是否禁用多线程,以及规定碎片大小
                     '是否禁用多线程,以及规定碎片大小
                     Dim TargetUrl As String = GetSource().Url
                     Dim TargetUrl As String = GetSource().Url
-                    If TargetUrl.Contains("pcl2-server") OrElse TargetUrl.Contains("gitcode.net") OrElse TargetUrl.Contains("github.com") Then Return False
+                    If TargetUrl.Contains("pcl2-server") OrElse TargetUrl.Contains("gitcode.net") OrElse TargetUrl.Contains("github.com") OrElse TargetUrl.Contains("modrinth.com") Then Return Nothing
                     '寻找最大碎片
                     '寻找最大碎片
+                    'FUTURE: 下载引擎重做,计算下载源平均链接时间和线程下载速度,按最高时间节省来开启多线程
                     Dim FilePieceMax As NetThread = Threads
                     Dim FilePieceMax As NetThread = Threads
                     For Each Thread As NetThread In Threads
                     For Each Thread As NetThread In Threads
                         If Thread.DownloadUndone > FilePieceMax.DownloadUndone Then FilePieceMax = Thread
                         If Thread.DownloadUndone > FilePieceMax.DownloadUndone Then FilePieceMax = Thread
                     Next
                     Next
-                    If FilePieceMax Is Nothing OrElse FilePieceMax.DownloadUndone < FilePieceLimit Then Return False
+                    If FilePieceMax Is Nothing OrElse FilePieceMax.DownloadUndone < FilePieceLimit Then Return Nothing
                     StartPosition = FilePieceMax.DownloadEnd - FilePieceMax.DownloadUndone * 0.4
                     StartPosition = FilePieceMax.DownloadEnd - FilePieceMax.DownloadUndone * 0.4
                     StartSource = GetSource()
                     StartSource = GetSource()
 
 
                     '开始线程
                     '开始线程
 StartThread:
 StartThread:
-                    If (StartPosition > FileSize AndAlso FileSize >= 0 AndAlso Not IsUnknownSize) OrElse StartPosition < 0 OrElse IsNothing(StartSource) Then Return False
+                    If (StartPosition > FileSize AndAlso FileSize >= 0 AndAlso Not IsUnknownSize) OrElse StartPosition < 0 OrElse IsNothing(StartSource) Then Return Nothing
                     '构建线程
                     '构建线程
                     Dim ThreadUuid As Integer = GetUuid()
                     Dim ThreadUuid As Integer = GetUuid()
                     SyncLock LockTasks
                     SyncLock LockTasks
-                        If Not Tasks.Any() Then Return False '由于中断,已没有可用任务
-                        Th = New Thread(AddressOf Thread) With {.Name = "NetTask " & Tasks(0).Uuid & "/" & Uuid & " Download " & ThreadUuid & "#", .Priority = ThreadPriority.BelowNormal}
+                        If Not Tasks.Any() Then Return Nothing '由于中断,已没有可用任务
+                        Th = New Thread(AddressOf Thread) With {.Name = $"NetTask {Tasks(0).Uuid}/{Uuid} Download {ThreadUuid}#", .Priority = ThreadPriority.BelowNormal}
                     End SyncLock
                     End SyncLock
                     ThreadInfo = New NetThread With {.Uuid = ThreadUuid, .DownloadStart = StartPosition, .Thread = Th, .Source = StartSource, .Task = Me, .State = NetState.WaitForDownload}
                     ThreadInfo = New NetThread With {.Uuid = ThreadUuid, .DownloadStart = StartPosition, .Thread = Th, .Source = StartSource, .Task = Me, .State = NetState.WaitForDownload}
                     '链表处理
                     '链表处理
@@ -1012,11 +1005,11 @@ StartThread:
                     If IsSourceFailed(False) Then SourcesOnce(0).Thread = ThreadInfo
                     If IsSourceFailed(False) Then SourcesOnce(0).Thread = ThreadInfo
                 End SyncLock
                 End SyncLock
                 Th.Start(ThreadInfo)
                 Th.Start(ThreadInfo)
-                Return True
+                Return ThreadInfo
 
 
             Catch ex As Exception
             Catch ex As Exception
                 Log(ex, "尝试开始下载线程失败(" & If(LocalName, "Nothing") & ")", LogLevel.Hint)
                 Log(ex, "尝试开始下载线程失败(" & If(LocalName, "Nothing") & ")", LogLevel.Hint)
-                Return False
+                Return Nothing
             End Try
             End Try
         End Function
         End Function
         ''' <summary>
         ''' <summary>
@@ -1040,35 +1033,45 @@ StartThread:
                 HttpRequest.Timeout = Timeout
                 HttpRequest.Timeout = Timeout
                 HttpRequest.AddRange(Info.DownloadStart)
                 HttpRequest.AddRange(Info.DownloadStart)
                 SecretHeadersSign(Info.Source.Url, HttpRequest, UseBrowserUserAgent)
                 SecretHeadersSign(Info.Source.Url, HttpRequest, UseBrowserUserAgent)
+                Dim ContentLength As Long = 0
                 Using HttpResponse As HttpWebResponse = HttpRequest.GetResponse()
                 Using HttpResponse As HttpWebResponse = HttpRequest.GetResponse()
+                    If State = NetState.Error Then GoTo SourceBreak '快速中断
+                    If ModeDebug AndAlso HttpResponse.ResponseUri.OriginalString <> Info.Source.Url Then
+                        Log($"[Download] {LocalName} {Info.Uuid}#:重定向至 {HttpResponse.ResponseUri.OriginalString}")
+                    End If
                     '文件大小校验
                     '文件大小校验
-                    Dim ThreadFileSize = HttpResponse.ContentLength
-                    If ThreadFileSize = -1 Then
+                    ContentLength = HttpResponse.ContentLength
+                    If ContentLength = -1 Then
                         If FileSize > 1 Then
                         If FileSize > 1 Then
-                            Log("[Download] " & LocalName & " " & Info.Uuid & "#:文件大小未知,但已从其他下载源获取,不作处理")
+                            If Info.DownloadStart = 0 Then
+                                Log($"[Download] {LocalName} {Info.Uuid}#:文件大小未知,但已从其他下载源获取,不作处理")
+                            Else
+                                Log($"[Download] {LocalName} {Info.Uuid}#:ContentLength 返回了 -1,无法确定是否支持分段下载,视作不支持")
+                                GoTo NotSupportRange
+                            End If
                         Else
                         Else
                             FileSize = -1 : IsUnknownSize = True
                             FileSize = -1 : IsUnknownSize = True
-                            Log("[Download] " & LocalName & " " & Info.Uuid & "#:文件大小未知")
+                            Log($"[Download] {LocalName} {Info.Uuid}#:文件大小未知")
                         End If
                         End If
-                    ElseIf ThreadFileSize < 0 Then
-                        Throw New Exception("获取片大小失败,结果为 " & ThreadFileSize & "。")
+                    ElseIf ContentLength < 0 Then
+                        Throw New Exception("获取片大小失败,结果为 " & ContentLength & "。")
                     ElseIf Info.IsFirstThread Then
                     ElseIf Info.IsFirstThread Then
                         If Check IsNot Nothing Then
                         If Check IsNot Nothing Then
-                            If ThreadFileSize < Check.MinSize AndAlso Check.MinSize > 0 Then
-                                Throw New Exception("文件大小不足,获取结果为 " & ThreadFileSize & ",要求至少为 " & Check.MinSize & "。")
+                            If ContentLength < Check.MinSize AndAlso Check.MinSize > 0 Then
+                                Throw New Exception($"文件大小不足,获取结果为 {ContentLength},要求至少为 {Check.MinSize}。")
                             End If
                             End If
-                            If ThreadFileSize <> Check.ActualSize AndAlso Check.ActualSize > 0 Then
-                                Throw New Exception("文件大小不一致,获取结果为 " & ThreadFileSize & ",要求必须为 " & Check.ActualSize & "。")
+                            If ContentLength <> Check.ActualSize AndAlso Check.ActualSize > 0 Then
+                                Throw New Exception($"文件大小不一致,获取结果为 {ContentLength},要求必须为 {Check.ActualSize}。")
                             End If
                             End If
                         End If
                         End If
-                        FileSize = ThreadFileSize : IsUnknownSize = False
-                        Log("[Download] " & LocalName & ":文件大小 " & ThreadFileSize & " B" & "(" & GetString(ThreadFileSize) & ")")
+                        FileSize = ContentLength : IsUnknownSize = False
+                        Log($"[Download] {LocalName} {Info.Uuid}#:文件大小 {ContentLength}({GetString(ContentLength)})")
                         '若文件大小大于 50 M,进行剩余磁盘空间校验
                         '若文件大小大于 50 M,进行剩余磁盘空间校验
-                        If ThreadFileSize > 50 * 1024 * 1024 Then
+                        If ContentLength > 50 * 1024 * 1024 Then
                             For Each Drive As DriveInfo In DriveInfo.GetDrives
                             For Each Drive As DriveInfo In DriveInfo.GetDrives
                                 Dim DriveName As String = Drive.Name.First.ToString
                                 Dim DriveName As String = Drive.Name.First.ToString
-                                Dim RequiredSpace = If(PathTemp.StartsWithF(DriveName), ThreadFileSize * 1.1, 0) +
-                                                If(LocalPath.StartsWithF(DriveName), ThreadFileSize + 5 * 1024 * 1024, 0)
+                                Dim RequiredSpace = If(PathTemp.StartsWithF(DriveName), ContentLength * 1.1, 0) +
+                                                If(LocalPath.StartsWithF(DriveName), ContentLength + 5 * 1024 * 1024, 0)
                                 If Drive.TotalFreeSpace < RequiredSpace Then
                                 If Drive.TotalFreeSpace < RequiredSpace Then
                                     Throw New Exception(DriveName & " 盘空间不足,无法进行下载。" & vbCrLf & "需要至少 " & GetString(RequiredSpace) & " 空间,但当前仅剩余 " & GetString(Drive.TotalFreeSpace) & "。" &
                                     Throw New Exception(DriveName & " 盘空间不足,无法进行下载。" & vbCrLf & "需要至少 " & GetString(RequiredSpace) & " 空间,但当前仅剩余 " & GetString(Drive.TotalFreeSpace) & "。" &
                                                     If(PathTemp.StartsWithF(DriveName), vbCrLf & vbCrLf & "下载时需要与文件同等大小的空间存放缓存,你可以在设置中调整缓存文件夹的位置。", ""))
                                                     If(PathTemp.StartsWithF(DriveName), vbCrLf & vbCrLf & "下载时需要与文件同等大小的空间存放缓存,你可以在设置中调整缓存文件夹的位置。", ""))
@@ -1076,8 +1079,9 @@ StartThread:
                             Next
                             Next
                         End If
                         End If
                     ElseIf FileSize < 0 Then
                     ElseIf FileSize < 0 Then
-                        Throw New Exception("非首线程运行时,尚未获取文件大小。")
-                    ElseIf Info.DownloadStart > 0 AndAlso ThreadFileSize = FileSize Then
+                        Throw New Exception("非首线程运行时,尚未获取文件大小")
+                    ElseIf Info.DownloadStart > 0 AndAlso ContentLength = FileSize Then
+NotSupportRange:
                         SyncLock LockSource
                         SyncLock LockSource
                             If SourcesOnce.Contains(Info.Source) Then
                             If SourcesOnce.Contains(Info.Source) Then
                                 GoTo SourceBreak
                                 GoTo SourceBreak
@@ -1085,10 +1089,11 @@ StartThread:
                                 SourcesOnce.Add(Info.Source)
                                 SourcesOnce.Add(Info.Source)
                             End If
                             End If
                         End SyncLock
                         End SyncLock
-                        Throw New WebException("该下载源不支持断点续传。")
-                    ElseIf Not FileSize - Info.DownloadStart = ThreadFileSize Then
-                        Throw New WebException("获取到的片大小不一致:线程结果为 " & ThreadFileSize & " B,任务结果为 " & FileSize & "B,起点为 " & Info.DownloadStart & "B。")
+                        Throw New WebException($"该下载源不支持分段下载:Range 起始于 {Info.DownloadStart},预期 ContentLength 为 {FileSize - Info.DownloadStart},返回 ContentLength 为 {ContentLength},总文件大小 {FileSize}")
+                    ElseIf Not FileSize - Info.DownloadStart = ContentLength Then
+                        Throw New WebException($"获取到的分段大小不一致:Range 起始于 {Info.DownloadStart},预期 ContentLength 为 {FileSize - Info.DownloadStart},返回 ContentLength 为 {ContentLength},总文件大小 {FileSize}")
                     End If
                     End If
+                    'Log($"[Download] {LocalName} {Info.Uuid}#:通过大小检查,文件大小 {FileSize},起始点 {Info.DownloadStart},ContentLength {ContentLength}")
                     Info.State = NetState.Get
                     Info.State = NetState.Get
                     SyncLock LockState
                     SyncLock LockState
                         If State < NetState.Get Then State = NetState.Get
                         If State < NetState.Get Then State = NetState.Get
@@ -1096,7 +1101,7 @@ StartThread:
                     '创建缓存文件
                     '创建缓存文件
                     If IsNoSplit Then
                     If IsNoSplit Then
                         Info.Temp = Nothing
                         Info.Temp = Nothing
-                        SmailFileCache = New List(Of Byte)
+                        SmailFileCache = New Queue(Of Byte)
                     Else
                     Else
                         Info.Temp = PathTemp & "Download\" & Uuid & "_" & Info.Uuid & "_" & RandomInteger(0, 999999) & ".tmp"
                         Info.Temp = PathTemp & "Download\" & Uuid & "_" & Info.Uuid & "_" & RandomInteger(0, 999999) & ".tmp"
                         ResultStream = New FileStream(Info.Temp, FileMode.Create, FileAccess.Write, FileShare.Read)
                         ResultStream = New FileStream(Info.Temp, FileMode.Create, FileAccess.Write, FileShare.Read)
@@ -1108,7 +1113,7 @@ StartThread:
                         Dim HttpData As Byte() = New Byte(16384) {}
                         Dim HttpData As Byte() = New Byte(16384) {}
                         HttpDataCount = HttpStream.Read(HttpData, 0, 16384)
                         HttpDataCount = HttpStream.Read(HttpData, 0, 16384)
                         While (IsUnknownSize OrElse Info.DownloadUndone > 0) AndAlso '判断是否下载完成
                         While (IsUnknownSize OrElse Info.DownloadUndone > 0) AndAlso '判断是否下载完成
-                      HttpDataCount > 0 AndAlso Not IsProgramEnded AndAlso State < NetState.Merge AndAlso (Not Info.Source.IsFailed OrElse Info.Equals(Info.Source.Thread))
+                            HttpDataCount > 0 AndAlso Not IsProgramEnded AndAlso State < NetState.Merge AndAlso (Not Info.Source.IsFailed OrElse Info.Equals(Info.Source.Thread))
                             '限速
                             '限速
                             While NetTaskSpeedLimitHigh > 0 AndAlso NetTaskSpeedLimitLeft <= 0
                             While NetTaskSpeedLimitHigh > 0 AndAlso NetTaskSpeedLimitLeft <= 0
                                 Threading.Thread.Sleep(16)
                                 Threading.Thread.Sleep(16)
@@ -1147,9 +1152,15 @@ StartThread:
                                 Info.DownloadDone += RealDataCount
                                 Info.DownloadDone += RealDataCount
                                 If IsNoSplit Then
                                 If IsNoSplit Then
                                     If HttpData.Count = RealDataCount Then
                                     If HttpData.Count = RealDataCount Then
-                                        SmailFileCache.AddRange(HttpData)
+                                        'SmailFileCache.AddRange(HttpData)
+                                        For Each B In HttpData
+                                            SmailFileCache.Enqueue(B)
+                                        Next
                                     Else
                                     Else
-                                        SmailFileCache.AddRange(HttpData.ToList.GetRange(0, RealDataCount))
+                                        'SmailFileCache.AddRange(HttpData.ToList.GetRange(0, RealDataCount))
+                                        For i = 0 To RealDataCount - 1
+                                            SmailFileCache.Enqueue(HttpData(i))
+                                        Next
                                     End If
                                     End If
                                 Else
                                 Else
                                     ResultStream.Write(HttpData, 0, RealDataCount)
                                     ResultStream.Write(HttpData, 0, RealDataCount)
@@ -1170,17 +1181,17 @@ StartThread:
                     End Using
                     End Using
                 End Using
                 End Using
 SourceBreak:
 SourceBreak:
-                If HttpDataCount = 0 AndAlso Info.DownloadUndone > 0 AndAlso Not IsUnknownSize Then
-                    '服务器无返回数据
-                    Throw New Exception("服务器无返回数据,但下载尚未完成")
-                ElseIf State = NetState.Error OrElse Info.Source.IsFailed OrElse (Info.DownloadUndone > 0 AndAlso Not IsUnknownSize) Then
+                If State = NetState.Error OrElse Info.Source.IsFailed OrElse (Info.DownloadUndone > 0 AndAlso Not IsUnknownSize) Then
                     '被外部中断
                     '被外部中断
                     Info.State = NetState.Error
                     Info.State = NetState.Error
-                    Log("[Download] " & LocalName & " " & Info.Uuid & "#:中断")
+                    Log($"[Download] {LocalName} {Info.Uuid}#:中断")
+                ElseIf HttpDataCount = 0 AndAlso Info.DownloadUndone > 0 AndAlso Not IsUnknownSize Then
+                    '服务器无返回数据
+                    Throw New Exception($"返回的 ContentLength 过多:ContentLength 为 {ContentLength},但获取到的总数据量仅为 {Info.DownloadDone}(全文件总数据量 {DownloadDone})")
                 Else
                 Else
                     '本线程完成
                     '本线程完成
                     Info.State = NetState.Finish
                     Info.State = NetState.Finish
-                    If ModeDebug Then Log("[Download] " & LocalName & " " & Info.Uuid & "#:完成,已下载 " & GetString(Info.DownloadDone))
+                    If ModeDebug Then Log($"[Download] {LocalName} {Info.Uuid}#:完成,已下载 {Info.DownloadDone}")
                 End If
                 End If
             Catch ex As Exception
             Catch ex As Exception
                 '状态变更
                 '状态变更
@@ -1212,11 +1223,11 @@ SourceBreak:
                 'End SyncLock
                 'End SyncLock
                 Info.Source.Ex = ex
                 Info.Source.Ex = ex
                 '根据情况判断,是否在多线程下禁用下载源(连续错误过多,或不支持断点续传)
                 '根据情况判断,是否在多线程下禁用下载源(连续错误过多,或不支持断点续传)
-                'BMCLAPI 的部分源在高频率请求下会返回 403,所以不应因此禁用下载源
                 If ex.Message.Contains("该下载源不支持") OrElse ex.Message.Contains("未能解析") OrElse ex.Message.Contains("(404)") OrElse
                 If ex.Message.Contains("该下载源不支持") OrElse ex.Message.Contains("未能解析") OrElse ex.Message.Contains("(404)") OrElse
-                   ex.Message.Contains("(502)") OrElse ex.Message.Contains("无返回数据") OrElse ex.Message.Contains("空间不足") OrElse ex.Message.Contains("获取到的片大小不一致") OrElse
-                    (Info.Source.FailCount >= MathClamp(NetTaskThreadLimit, 5, 40) AndAlso DownloadDone < 1) OrElse
-                    Info.Source.FailCount > NetTaskThreadLimit Then
+                   ex.Message.Contains("(502)") OrElse ex.Message.Contains("无返回数据") OrElse ex.Message.Contains("空间不足") OrElse ex.Message.Contains("获取到的分段大小不一致") OrElse
+                   (ex.Message.Contains("(403)") AndAlso Not Info.Source.Url.ContainsF("bmclapi")) OrElse 'BMCLAPI 的部分源在高频率请求下会返回 403,所以不应因此禁用下载源
+                   (Info.Source.FailCount >= MathClamp(NetTaskThreadLimit, 5, 30) AndAlso DownloadDone < 1) OrElse
+                    Info.Source.FailCount > NetTaskThreadLimit + 2 Then
                     Dim IsThisFail As Boolean = False
                     Dim IsThisFail As Boolean = False
                     SyncLock LockSource
                     SyncLock LockSource
                         If Info.Source.Thread IsNot Nothing AndAlso Info.Source.Thread.Equals(Info) Then
                         If Info.Source.Thread IsNot Nothing AndAlso Info.Source.Thread.Equals(Info) Then
@@ -1232,8 +1243,8 @@ Wrong:
                     End SyncLock
                     End SyncLock
                     '本线程引发下载源被禁用
                     '本线程引发下载源被禁用
                     If IsThisFail Then
                     If IsThisFail Then
-                        Log("[Download] " & LocalName & " " & Uuid & "#:下载源被禁用(" & Info.Source.Id & "):" & Info.Source.Url)
-                        Log(ex, "下载源 " & Info.Source.Id & " 已被禁用", If(ex.Message.Contains("不支持断点续传") OrElse ex.Message.Contains("(404)") OrElse ex.Message.Contains("(416)"), LogLevel.Developer, LogLevel.Debug))
+                        Log($"[Download] {LocalName} {Info.Uuid}#:下载源被禁用({Info.Source.Id}):{Info.Source.Url}")
+                        Log(ex, "下载源 " & Info.Source.Id & " 已被禁用", If(ex.Message.Contains("不支持分段下载") OrElse ex.Message.Contains("(404)") OrElse ex.Message.Contains("(416)"), LogLevel.Developer, LogLevel.Debug))
                         If IsSourceFailed() Then
                         If IsSourceFailed() Then
                             '没有可用源
                             '没有可用源
                             Log("[Download] 文件 " & LocalName & " 已无可用下载源")
                             Log("[Download] 文件 " & LocalName & " 已无可用下载源")
@@ -1265,6 +1276,7 @@ Wrong:
                 SyncLock NetTaskThreadCountLock
                 SyncLock NetTaskThreadCountLock
                     NetTaskThreadCount -= 1
                     NetTaskThreadCount -= 1
                 End SyncLock
                 End SyncLock
+                '可能在没有下载完的时候开始合并文件了,这造成了大多数合并失败
                 If ((FileSize >= 0 AndAlso DownloadDone >= FileSize) OrElse (FileSize = -1 AndAlso DownloadDone > 0)) AndAlso State < NetState.Merge Then Merge()
                 If ((FileSize >= 0 AndAlso DownloadDone >= FileSize) OrElse (FileSize = -1 AndAlso DownloadDone > 0)) AndAlso State < NetState.Merge Then Merge()
             End Try
             End Try
         End Sub
         End Sub
@@ -1287,7 +1299,6 @@ Wrong:
             Try
             Try
 Retry:
 Retry:
                 SyncLock LockChain
                 SyncLock LockChain
-                    If ModeDebug Then Log("[Download] " & LocalName & ":正在合并文件")
                     '创建文件夹
                     '创建文件夹
                     If File.Exists(LocalPath) Then File.Delete(LocalPath)
                     If File.Exists(LocalPath) Then File.Delete(LocalPath)
                     Dim Info As New FileInfo(LocalPath)
                     Dim Info As New FileInfo(LocalPath)
@@ -1295,6 +1306,7 @@ Retry:
                     '合并文件
                     '合并文件
                     If IsNoSplit Then
                     If IsNoSplit Then
                         '仅有一个线程,从缓存中输出
                         '仅有一个线程,从缓存中输出
+                        If ModeDebug Then Log($"[Download] {LocalName}:下载结束,从缓存输出文件")
                         MergeFile = New FileStream(LocalPath, FileMode.Create)
                         MergeFile = New FileStream(LocalPath, FileMode.Create)
                         AddWriter = New BinaryWriter(MergeFile)
                         AddWriter = New BinaryWriter(MergeFile)
                         AddWriter.Write(SmailFileCache.ToArray)
                         AddWriter.Write(SmailFileCache.ToArray)
@@ -1302,17 +1314,19 @@ Retry:
                         MergeFile.Dispose() : MergeFile = Nothing
                         MergeFile.Dispose() : MergeFile = Nothing
                     ElseIf Threads.DownloadDone = DownloadDone AndAlso Threads.Temp IsNot Nothing Then
                     ElseIf Threads.DownloadDone = DownloadDone AndAlso Threads.Temp IsNot Nothing Then
                         '仅有一个文件,直接复制
                         '仅有一个文件,直接复制
+                        If ModeDebug Then Log($"[Download] {LocalName}:下载结束,仅有一个文件,无需合并")
                         CopyFile(Threads.Temp, LocalPath)
                         CopyFile(Threads.Temp, LocalPath)
                     Else
                     Else
                         '有多个线程,合并
                         '有多个线程,合并
+                        If ModeDebug Then Log($"[Download] {LocalName}:下载结束,开始合并文件")
                         MergeFile = New FileStream(LocalPath, FileMode.Create)
                         MergeFile = New FileStream(LocalPath, FileMode.Create)
                         AddWriter = New BinaryWriter(MergeFile)
                         AddWriter = New BinaryWriter(MergeFile)
                         For Each Thread As NetThread In Threads
                         For Each Thread As NetThread In Threads
                             If Thread.DownloadDone = 0 OrElse Thread.Temp Is Nothing Then Continue For
                             If Thread.DownloadDone = 0 OrElse Thread.Temp Is Nothing Then Continue For
                             Using fs As New FileStream(Thread.Temp, FileMode.Open, FileAccess.Read, FileShare.Read)
                             Using fs As New FileStream(Thread.Temp, FileMode.Open, FileAccess.Read, FileShare.Read)
-                                Dim TempReader As New BinaryReader(fs)
-                                AddWriter.Write(TempReader.ReadBytes(fs.Length))
-                                TempReader.Close()
+                                Using TempReader As New BinaryReader(fs)
+                                    AddWriter.Write(TempReader.ReadBytes(Thread.DownloadDone))
+                                End Using
                             End Using
                             End Using
                         Next
                         Next
                         AddWriter.Dispose() : AddWriter = Nothing
                         AddWriter.Dispose() : AddWriter = Nothing
@@ -1329,9 +1343,9 @@ Retry:
                     '检查文件
                     '检查文件
                     Dim CheckResult As String = Check?.Check(LocalPath)
                     Dim CheckResult As String = Check?.Check(LocalPath)
                     If CheckResult IsNot Nothing Then
                     If CheckResult IsNot Nothing Then
-                        Log("[Download] " & Uuid & "# 的下载线程细节:")
+                        Log($"[Download] {LocalName} 合并文件失败,下载线程细节:")
                         For Each Th As NetThread In Threads
                         For Each Th As NetThread In Threads
-                            Log("[Download] " & Th.Uuid & "#,状态 " & GetStringFromEnum(CType(Th.State, [Enum])) & ",字节范围 " & Th.DownloadStart & "~" & (Th.DownloadStart + Th.DownloadDone) & ",字节剩余 " & Th.DownloadUndone)
+                            Log($"[Download]     {Th.Uuid}#,状态 {GetStringFromEnum(Th.State)},范围 {Th.DownloadStart}~{Th.DownloadStart + Th.DownloadDone},完成 {Th.DownloadDone},剩余 {Th.DownloadUndone}")
                         Next
                         Next
                         Throw New Exception(CheckResult)
                         Throw New Exception(CheckResult)
                     End If
                     End If
@@ -1372,19 +1386,7 @@ Retry:
                 '凉凉
                 '凉凉
                 State = NetState.Error
                 State = NetState.Error
             End SyncLock
             End SyncLock
-            Try
-                If File.Exists(LocalPath) Then File.Delete(LocalPath)
-            Catch
-            End Try
-            SyncLock NetManager.LockRemain
-                NetManager.FileRemain -= 1
-                Log("[Download] " & LocalName & ":已失败,剩余文件 " & NetManager.FileRemain)
-                'If NetManage.FileRemainList.Contains(LocalPath) Then
-                '    NetManage.FileRemainList.Remove(LocalPath)
-                'Else
-                '    Log("ERROR " & LocalPath)
-                'End If
-            End SyncLock
+            InterruptAndDelete()
             For Each Task In Tasks
             For Each Task In Tasks
                 Task.OnFileFail(Me)
                 Task.OnFileFail(Me)
             Next
             Next
@@ -1393,24 +1395,24 @@ Retry:
         ''' 下载中断。
         ''' 下载中断。
         ''' </summary>
         ''' </summary>
         Public Sub Abort(CausedByTask As LoaderDownload)
         Public Sub Abort(CausedByTask As LoaderDownload)
-            '确认任务移除
+            '从特定任务中移除,如果它还属于其他任务,则继续下载
             SyncLock LockTasks
             SyncLock LockTasks
                 Tasks.Remove(CausedByTask)
                 Tasks.Remove(CausedByTask)
-                If Tasks.Count > 0 Then Exit Sub
+                If Tasks.Any Then Exit Sub
             End SyncLock
             End SyncLock
             '确认中断
             '确认中断
             SyncLock LockState
             SyncLock LockState
                 If State >= NetState.Finish Then Exit Sub
                 If State >= NetState.Finish Then Exit Sub
                 State = NetState.Error
                 State = NetState.Error
             End SyncLock
             End SyncLock
+            InterruptAndDelete()
+        End Sub
+        Private Sub InterruptAndDelete()
+            On Error Resume Next
+            If File.Exists(LocalPath) Then File.Delete(LocalPath)
             SyncLock NetManager.LockRemain
             SyncLock NetManager.LockRemain
                 NetManager.FileRemain -= 1
                 NetManager.FileRemain -= 1
-                If ModeDebug Then Log("[Download] " & LocalName & ":已取消,剩余文件 " & NetManager.FileRemain)
-                'If NetManage.FileRemainList.Contains(LocalPath) Then
-                '    NetManage.FileRemainList.Remove(LocalPath)
-                'Else
-                '    Log("ERROR " & LocalPath)
-                'End If
+                Log($"[Download] {LocalName}:状态 {State},剩余文件 {NetManager.FileRemain}")
             End SyncLock
             End SyncLock
         End Sub
         End Sub
 
 
@@ -1426,11 +1428,6 @@ Retry:
             SyncLock NetManager.LockRemain
             SyncLock NetManager.LockRemain
                 NetManager.FileRemain -= 1
                 NetManager.FileRemain -= 1
                 If PrintLog Then Log("[Download] " & LocalName & ":已完成,剩余文件 " & NetManager.FileRemain)
                 If PrintLog Then Log("[Download] " & LocalName & ":已完成,剩余文件 " & NetManager.FileRemain)
-                'If NetManage.FileRemainList.Contains(LocalPath) Then
-                '    NetManage.FileRemainList.Remove(LocalPath)
-                'Else
-                '    Log("ERROR " & LocalPath)
-                'End If
             End SyncLock
             End SyncLock
             SyncLock LockTasks
             SyncLock LockTasks
                 For Each Task In Tasks
                 For Each Task In Tasks
@@ -1465,7 +1462,9 @@ Retry:
         Public Overrides Property Progress As Double
         Public Overrides Property Progress As Double
             Get
             Get
                 If State >= LoadState.Finished Then Return 1
                 If State >= LoadState.Finished Then Return 1
-                If Not Files.Any() Then Return 0 '必须返回 0,否则在获取列表的时候会错觉已经下载完了
+                SyncLock FilesLock
+                    If Not Files.Any() Then Return 0 '必须返回 0,否则在获取列表的时候会错觉已经下载完了
+                End SyncLock
                 Return _Progress
                 Return _Progress
             End Get
             End Get
             Set(value As Double)
             Set(value As Double)
@@ -1507,7 +1506,7 @@ FinishExCatch:
 #End Region
 #End Region
 
 
         ''' <summary>
         ''' <summary>
-        ''' 刷新公开属性。由 NetManager 每 0.2 秒调用一次。
+        ''' 刷新公开属性。由 NetManager 每 0.1 秒调用一次。
         ''' </summary>
         ''' </summary>
         Public Sub RefreshStat()
         Public Sub RefreshStat()
             '计算进度
             '计算进度
@@ -1519,15 +1518,15 @@ FinishExCatch:
                         NewProgress += File.Progress * 0.2
                         NewProgress += File.Progress * 0.2
                         TotalProgress += 0.2
                         TotalProgress += 0.2
                     Else
                     Else
-                        NewProgress += File.Progress '* If(File.Progress < 1, 0.9, 1)
+                        NewProgress += File.Progress
                         TotalProgress += 1
                         TotalProgress += 1
                     End If
                     End If
                 Next
                 Next
             End SyncLock
             End SyncLock
             If TotalProgress > 0 Then NewProgress /= TotalProgress
             If TotalProgress > 0 Then NewProgress /= TotalProgress
             '刷新进度
             '刷新进度
-            If NewProgress < 1 AndAlso NewProgress > 0 Then NewProgress = 1 - (1 - NewProgress) ^ 0.9 'MathBezier(NewProgress, 0, 0.05, 1, 0.8, 0.0001)
-            _Progress = NewProgress * 0.99 + 0.01
+            If NewProgress < 1 AndAlso NewProgress > 0 Then NewProgress = 2 * (NewProgress ^ 3) - 3 * (NewProgress ^ 2) + 2 * NewProgress '2x^3-3x^2+2x,模拟开头和结尾更慢的情况
+            _Progress = NewProgress
         End Sub
         End Sub
 
 
         Public Sub New(Name As String, FileTasks As List(Of NetFile))
         Public Sub New(Name As String, FileTasks As List(Of NetFile))
@@ -1556,71 +1555,72 @@ NextElement:
             End SyncLock
             End SyncLock
             State = LoadState.Loading
             State = LoadState.Loading
             '开始执行
             '开始执行
-            RunInNewThread(Sub()
-                               Try
-                                   '输入检测
-                                   If Not Files.Any() Then
-                                       OnFinish()
-                                       Exit Sub
-                                   End If
-                                   For Each File As NetFile In Files
-                                       If File Is Nothing Then Throw New ArgumentException("存在空文件请求!")
-                                       For Each Source As NetSource In File.Sources
-                                           If Not (Source.Url.StartsWithF("https://", True) OrElse Source.Url.StartsWithF("http://", True)) Then
-                                               Source.Ex = New ArgumentException("输入的下载链接不正确!")
-                                               Source.IsFailed = True
-                                           End If
-                                       Next
-                                       If File.IsSourceFailed() Then Throw New ArgumentException("输入的下载链接不正确!")
-                                       If Not File.LocalPath.ToLower.Contains(":\") Then Throw New ArgumentException("输入的本地文件地址不正确!")
-                                       If File.LocalPath.EndsWithF("\") Then Throw New ArgumentException("请输入含文件名的完整文件路径!")
-                                       '文件夹检测
-                                       Dim DirPath As String = New FileInfo(File.LocalPath).Directory.FullName
-                                       If Not Directory.Exists(DirPath) Then Directory.CreateDirectory(DirPath)
-                                   Next
-                                   '接入下载管理器
-                                   NetManager.Start(Me)
-                                   '将文件分配给多个线程以进行已存在查找
-                                   Dim Folders As New List(Of String) '可能会用于已存在查找的文件夹列表
-                                   Dim FoldersFinal As New List(Of String) '最终用于查找的列表
-                                   If Not Setup.Get("SystemDebugSkipCopy") Then '在设置中禁用复制
-                                       Folders.Add(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) & "\.minecraft\") '总是添加官启文件夹,因为 HMCL 会把所有文件存在这里
-                                       For Each Folder In McFolderList
-                                           Folders.Add(Folder.Path)
-                                       Next
-                                       Folders = Folders.Distinct.ToList
-                                       For Each Folder In Folders
-                                           If Folder <> PathMcFolder AndAlso Directory.Exists(Folder) Then FoldersFinal.Add(Folder)
-                                       Next
-                                   End If
-                                   '最多 5 个线程,最少每个线程分配 10 个文件
-                                   SyncLock FilesLock
-                                       Dim FilesPerThread As Integer = Math.Max(5, Files.Count / 10 + 1)
-                                       Dim FilesInThread As New List(Of NetFile)
-                                       For Each File In Files
-                                           FilesInThread.Add(File)
-                                           If FilesInThread.Count = FilesPerThread Then
-                                               Dim FilesToRun As New List(Of NetFile)
-                                               FilesToRun.AddRange(FilesInThread)
-                                               RunInNewThread(Sub() StartCopy(FilesToRun, FoldersFinal), "NetTask FileCopy " & Uuid)
-                                               FilesInThread.Clear()
-                                           End If
-                                       Next
-                                       If FilesInThread.Count > 0 Then
-                                           Dim FilesToRun As New List(Of NetFile)
-                                           FilesToRun.AddRange(FilesInThread)
-                                           RunInNewThread(Sub() StartCopy(FilesToRun, FoldersFinal), "NetTask FileCopy " & Uuid)
-                                           FilesInThread.Clear()
-                                       End If
-                                   End SyncLock
-                               Catch ex As Exception
-                                   OnFail(New List(Of Exception) From {ex})
-                               End Try
-                           End Sub, "NetTask " & Uuid & " Main")
+            RunInNewThread(
+            Sub()
+                Try
+                    '输入检测
+                    If Not Files.Any() Then
+                        OnFinish()
+                        Exit Sub
+                    End If
+                    For Each File As NetFile In Files
+                        If File Is Nothing Then Throw New ArgumentException("存在空文件请求!")
+                        For Each Source As NetSource In File.Sources
+                            If Not (Source.Url.StartsWithF("https://", True) OrElse Source.Url.StartsWithF("http://", True)) Then
+                                Source.Ex = New ArgumentException("输入的下载链接不正确!")
+                                Source.IsFailed = True
+                            End If
+                        Next
+                        If File.IsSourceFailed() Then Throw New ArgumentException("输入的下载链接不正确!")
+                        If Not File.LocalPath.ToLower.Contains(":\") Then Throw New ArgumentException("输入的本地文件地址不正确!")
+                        If File.LocalPath.EndsWithF("\") Then Throw New ArgumentException("请输入含文件名的完整文件路径!")
+                        '文件夹检测
+                        Dim DirPath As String = New FileInfo(File.LocalPath).Directory.FullName
+                        If Not Directory.Exists(DirPath) Then Directory.CreateDirectory(DirPath)
+                    Next
+                    '接入下载管理器
+                    NetManager.Start(Me)
+                    '将文件分配给多个线程以进行已存在查找
+                    Dim Folders As New List(Of String) '可能会用于已存在查找的文件夹列表
+                    Dim FoldersFinal As New List(Of String) '最终用于查找的列表
+                    If Not Setup.Get("SystemDebugSkipCopy") Then '在设置中禁用复制
+                        Folders.Add(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) & "\.minecraft\") '总是添加官启文件夹,因为 HMCL 会把所有文件存在这里
+                        For Each Folder In McFolderList
+                            Folders.Add(Folder.Path)
+                        Next
+                        Folders = Folders.Distinct.ToList
+                        For Each Folder In Folders
+                            If Folder <> PathMcFolder AndAlso Directory.Exists(Folder) Then FoldersFinal.Add(Folder)
+                        Next
+                    End If
+                    '最多 5 个线程,最少每个线程分配 10 个文件
+                    SyncLock FilesLock
+                        Dim FilesPerThread As Integer = Math.Max(5, Files.Count / 10 + 1)
+                        Dim FilesInThread As New List(Of NetFile)
+                        For Each File In Files
+                            FilesInThread.Add(File)
+                            If FilesInThread.Count = FilesPerThread Then
+                                Dim FilesToRun As New List(Of NetFile)
+                                FilesToRun.AddRange(FilesInThread)
+                                RunInNewThread(Sub() StartCopy(FilesToRun, FoldersFinal), "NetTask FileCopy " & Uuid)
+                                FilesInThread.Clear()
+                            End If
+                        Next
+                        If FilesInThread.Any Then
+                            Dim FilesToRun As New List(Of NetFile)
+                            FilesToRun.AddRange(FilesInThread)
+                            RunInNewThread(Sub() StartCopy(FilesToRun, FoldersFinal), "NetTask FileCopy " & Uuid)
+                            FilesInThread.Clear()
+                        End If
+                    End SyncLock
+                Catch ex As Exception
+                    OnFail(New List(Of Exception) From {ex})
+                End Try
+            End Sub, "NetTask " & Uuid & " Main")
         End Sub
         End Sub
         Private Sub StartCopy(Files As List(Of NetFile), FolderList As List(Of String))
         Private Sub StartCopy(Files As List(Of NetFile), FolderList As List(Of String))
             Try
             Try
-                If ModeDebug Then Log("[Download] 检查线程分配文件数:" & Files.Count)
+                If ModeDebug Then Log("[Download] 检查线程分配文件数:" & Files.Count & ",线程名:" & Thread.CurrentThread.Name)
                 '试图从已存在的 Minecraft 文件夹中寻找目标文件
                 '试图从已存在的 Minecraft 文件夹中寻找目标文件
                 Dim ExistFiles As New List(Of KeyValuePair(Of NetFile, String)) '{NetFile, Target As String}
                 Dim ExistFiles As New List(Of KeyValuePair(Of NetFile, String)) '{NetFile, Target As String}
                 For Each File As NetFile In Files
                 For Each File As NetFile In Files
@@ -1702,14 +1702,13 @@ Retry:
                 If ExList Is Nothing OrElse Not ExList.Any() Then ExList = New List(Of Exception) From {New Exception("未知错误!")}
                 If ExList Is Nothing OrElse Not ExList.Any() Then ExList = New List(Of Exception) From {New Exception("未知错误!")}
                 '寻找第一个不是 404 的下载源
                 '寻找第一个不是 404 的下载源
                 Dim UsefulExs = ExList.Where(Function(e) Not e.Message.Contains("(404)")).ToList
                 Dim UsefulExs = ExList.Where(Function(e) Not e.Message.Contains("(404)")).ToList
-                [Error] = If(UsefulExs.Count > 0, UsefulExs(0), ExList(0))
+                [Error] = If(UsefulExs.Any, UsefulExs(0), ExList(0))
                 '获取实际失败的文件
                 '获取实际失败的文件
                 SyncLock FilesLock
                 SyncLock FilesLock
                     For Each File In Files
                     For Each File In Files
                         If File.State = NetState.Error Then
                         If File.State = NetState.Error Then
                             [Error] = New Exception("文件下载失败:" & File.LocalPath & vbCrLf & Join(
                             [Error] = New Exception("文件下载失败:" & File.LocalPath & vbCrLf & Join(
-                                                        File.Sources.Select(Function(s) If(s.Ex Is Nothing, s.Url, s.Ex.Message & "(" & s.Url & ")")).ToList(),
-                                                    vbCrLf), [Error])
+                                File.Sources.Select(Function(s) If(s.Ex Is Nothing, s.Url, s.Ex.Message & "(" & s.Url & ")")), vbCrLf), [Error])
                             Exit For
                             Exit For
                         End If
                         End If
                     Next
                     Next
@@ -1794,9 +1793,9 @@ Retry:
         ''' </summary>
         ''' </summary>
         Private SpeedLastDone As Long = 0
         Private SpeedLastDone As Long = 0
         ''' <summary>
         ''' <summary>
-        ''' 最近 10 次下载速度的记录,较新的在后面。间隔为 0.2 秒,故记录了 2 秒的速度。
+        ''' 至多最近 30 次下载速度的记录,较新的在前面。
         ''' </summary>
         ''' </summary>
-        Public SpeedLast As New List(Of Long) From {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
+        Private SpeedLast As New List(Of Long)
         '这些属性由 RefreshStat 刷新
         '这些属性由 RefreshStat 刷新
         ''' <summary>
         ''' <summary>
         ''' 当前的全局下载速度,单位为 Byte / 秒。
         ''' 当前的全局下载速度,单位为 Byte / 秒。
@@ -1808,28 +1807,29 @@ Retry:
 #End Region
 #End Region
 
 
         ''' <summary>
         ''' <summary>
-        ''' 进度与下载速度由下载管理线程每隔约 0.2 秒刷新一次。
+        ''' 进度与下载速度由下载管理线程每隔约 0.1 秒刷新一次。
         ''' </summary>
         ''' </summary>
         Private Sub RefreshStat()
         Private Sub RefreshStat()
             Try
             Try
                 Dim DeltaTime As Long = GetTimeTick() - RefreshStatLast
                 Dim DeltaTime As Long = GetTimeTick() - RefreshStatLast
                 RefreshStatLast += DeltaTime
                 RefreshStatLast += DeltaTime
 #Region "刷新整体速度"
 #Region "刷新整体速度"
-                '计算即时速度
+                '计算瞬时速度
                 Dim ActualSpeed As Double = Math.Max(0, (DownloadDone - SpeedLastDone) / (DeltaTime / 1000))
                 Dim ActualSpeed As Double = Math.Max(0, (DownloadDone - SpeedLastDone) / (DeltaTime / 1000))
-                SpeedLast.Add(ActualSpeed)
-                SpeedLast.RemoveAt(0)
+                SpeedLast.Insert(0, ActualSpeed)
+                If SpeedLast.Count >= 31 Then SpeedLast.RemoveAt(30)
                 SpeedLastDone = DownloadDone
                 SpeedLastDone = DownloadDone
                 '计算用于显示的速度
                 '计算用于显示的速度
-                'If SpeedLast(9) = 0 Then
-                '    Speed = 0
-                'Else
-                Speed = (SpeedLast(9) + SpeedLast(8) * 0.9 + SpeedLast(7) * 0.8 + SpeedLast(6) * 0.7 + SpeedLast(5) * 0.6 + SpeedLast(4) * 0.5 + SpeedLast(3) * 0.4 + SpeedLast(2) * 0.3 + SpeedLast(1) * 0.2 + SpeedLast(0) * 0.1) / 5.5
-                'End If
+                Dim SpeedSum As Long = 0, SpeedDiv As Long = 0, Weight = SpeedLast.Count
+                For Each SpeedRecord In SpeedLast
+                    SpeedSum += SpeedRecord * Weight
+                    SpeedDiv += Weight
+                    Weight -= 1
+                Next
+                Speed = If(SpeedDiv > 0, SpeedSum / SpeedDiv, 0)
                 '计算新的速度下限
                 '计算新的速度下限
-                SpeedLast.Add(ActualSpeed)
-                SpeedLast.RemoveAt(0)
-                Dim Limit As Long = Math.Min(Math.Min(Math.Min(Math.Min(SpeedLast(5), SpeedLast(6)), SpeedLast(7)), SpeedLast(8)), SpeedLast(9)) * 0.9
+                Dim Limit As Long = 0
+                If SpeedLast.Count >= 10 Then Limit = SpeedLast.Take(10).Average * 0.85 '取近 1 秒的平均速度的 85%
                 If Limit > NetTaskSpeedLimitLow Then
                 If Limit > NetTaskSpeedLimitLow Then
                     NetTaskSpeedLimitLow = Limit
                     NetTaskSpeedLimitLow = Limit
                     Log("[Download] " & "速度下限已提升到 " & GetString(Limit))
                     Log("[Download] " & "速度下限已提升到 " & GetString(Limit))
@@ -1854,128 +1854,65 @@ Retry:
         Private Sub StartManager()
         Private Sub StartManager()
             If IsManagerStarted Then Exit Sub
             If IsManagerStarted Then Exit Sub
             IsManagerStarted = True
             IsManagerStarted = True
-            RunInNewThread(
-            Sub()
+            Dim ThreadStarter =
+            Sub(Id As Integer) '0 或 1
                 Try
                 Try
-                    Dim LastLoopTime As Long
                     While True
                     While True
-                        LastLoopTime = GetTimeTick()
+                        Thread.Sleep(20)
                         '若已完成,则清空
                         '若已完成,则清空
-                        If FileRemain = 0 AndAlso Files.Count > 0 Then
+                        If Id = 0 AndAlso FileRemain = 0 AndAlso Files.Any() Then
                             SyncLock LockFiles
                             SyncLock LockFiles
                                 Files.Clear()
                                 Files.Clear()
                             End SyncLock
                             End SyncLock
                         End If
                         End If
-                        '开启新线程
-                        If Speed < NetTaskSpeedLimitLow OrElse FileRemain > NetTaskThreadLimit Then
-                            '速度小于下限或剩余文件还贼多,尝试开启线程
-                            Dim IsSuccess As Boolean = False
-                            Dim NewThreadCount As Integer
-                            '确定最大线程追加数
-                            NewThreadCount = Math.Max(FileRemain, MathClamp(NetTaskThreadCount / 2, 1, 4))
-                            NewThreadCount = Math.Floor(NewThreadCount / 2) '双线程启用减半
-                            NewThreadCount = MathClamp(NewThreadCount, 1, NetTaskThreadLimit)
-                            '循环追加
-                            Do
-                                IsSuccess = False
-                                '启动 Wait 的文件后立即会变成 Connect,导致第二次循环再次调用,所以需要先暂时存储进去……
-                                '此外为了减少 LockFiles 的占用时间,所以先遍历列表再开始
-                                Dim FilesWaiting As New List(Of NetFile)
-                                Dim FilesLoading As New List(Of NetFile)
-                                SyncLock LockFiles
-                                    For Each File As NetFile In Files.Values
-                                        If File.RandomCode Mod 2 = 0 Then Continue For
-                                        If File.State = NetState.WaitForDownload Then
-                                            FilesWaiting.Add(File)
-                                        ElseIf File.State < NetState.Merge Then
-                                            FilesLoading.Add(File)
-                                        End If
-                                    Next
-                                End SyncLock
-                                '为文件列表中的文件开始线程
-                                For Each File As NetFile In FilesWaiting
-                                    If NewThreadCount = 0 Then Exit For
-                                    If File.TryBeginThread() Then
-                                        IsSuccess = True
-                                        NewThreadCount -= 1
-                                    End If
-                                Next
-                                For Each File As NetFile In FilesLoading
-                                    If NewThreadCount = 0 Then Exit For
-                                    If File.TryBeginThread() Then
-                                        IsSuccess = True
-                                        NewThreadCount -= 1
-                                    End If
-                                Next
-                                '等待 40 ms:BMCLAPI 部分时候有 20QPS 的限制
-                                Thread.Sleep(40)
-                            Loop While NewThreadCount > 0 AndAlso IsSuccess
-                        End If
-                        '等待 40 ms
-                        Thread.Sleep(40)
-                    End While
-                Catch ex As Exception
-                    Log(ex, "下载管理启动线程 1 出错", LogLevel.Assert)
-                End Try
-            End Sub, "NetManager ThreadStarter Single")
-            RunInNewThread(
-            Sub()
-                Try
-                    Dim LastLoopTime As Long
-                    While True
-                        LastLoopTime = GetTimeTick()
-                        '开启新线程
-                        If Speed < NetTaskSpeedLimitLow OrElse FileRemain > NetTaskThreadLimit Then
-                            '速度小于下限或剩余文件还贼多,尝试开启线程
-                            Dim IsSuccess As Boolean = False
-                            Dim NewThreadCount As Integer
-                            '确定最大线程追加数
-                            NewThreadCount = Math.Max(FileRemain, MathClamp(NetTaskThreadCount / 2, 1, 4))
-                            NewThreadCount = Math.Floor(NewThreadCount / 2) '双线程启用减半
-                            NewThreadCount = MathClamp(NewThreadCount, 1, NetTaskThreadLimit)
-                            '循环追加
-                            Do
-                                IsSuccess = False
-                                '启动 Wait 的文件后立即会变成 Connect,导致第二次循环再次调用,所以需要先暂时存储进去……
-                                '此外为了减少 LockFiles 的占用时间,所以先遍历列表再开始
-                                Dim FilesWaiting As New List(Of NetFile)
-                                Dim FilesLoading As New List(Of NetFile)
-                                SyncLock LockFiles
-                                    For Each File As NetFile In Files.Values
-                                        If File.RandomCode Mod 2 = 1 Then Continue For
-                                        If File.State = NetState.WaitForDownload Then
-                                            FilesWaiting.Add(File)
-                                        ElseIf File.State < NetState.Merge Then
-                                            FilesLoading.Add(File)
-                                        End If
-                                    Next
-                                End SyncLock
-                                '为文件列表中的文件开始线程
-                                For Each File As NetFile In FilesWaiting
-                                    If NewThreadCount = 0 Then Exit For
-                                    If File.TryBeginThread() Then
-                                        IsSuccess = True
-                                        NewThreadCount -= 1
-                                    End If
-                                Next
-                                For Each File As NetFile In FilesLoading
-                                    If NewThreadCount = 0 Then Exit For
-                                    If File.TryBeginThread() Then
-                                        IsSuccess = True
-                                        NewThreadCount -= 1
+                        '获取文件列表
+                        Dim AllFiles As List(Of NetFile)
+                        SyncLock LockFiles
+                            AllFiles = Files.Values.ToList()
+                        End SyncLock
+                        Dim WaitingFiles As New List(Of NetFile)
+                        Dim OngoingFiles As New List(Of NetFile)
+                        For Each File As NetFile In AllFiles
+                            If File.Uuid Mod 2 = Id Then Continue For
+                            If File.State = NetState.WaitForDownload Then
+                                WaitingFiles.Add(File)
+                            ElseIf File.State < NetState.Merge Then
+                                OngoingFiles.Add(File)
+                            End If
+                        Next
+                        '为等待中的文件开始线程
+                        For Each File As NetFile In WaitingFiles
+                            If NetTaskThreadCount >= NetTaskThreadLimit Then Continue While '最大线程数检查
+                            Dim NewThread = File.TryBeginThread()
+                            If NewThread IsNot Nothing AndAlso NewThread.Source.Url.Contains("bmclapi.") Then Thread.Sleep(40) '减少 BMCLAPI 请求频率,避免 Too Many Requests
+                        Next
+                        '为进行中的文件追加线程
+                        If Speed >= NetTaskSpeedLimitLow Then Continue While '下载速度足够,无需新增
+                        For Each File As NetFile In OngoingFiles
+                            If NetTaskThreadCount >= NetTaskThreadLimit Then Continue While '最大线程数检查
+                            '线程种类计数
+                            Dim PreparingCount = 0, DownloadingCount = 0
+                            If File.Threads IsNot Nothing Then
+                                For Each Thread As NetThread In File.Threads.ToList
+                                    If Thread.State < NetState.Download Then
+                                        PreparingCount += 1
+                                    ElseIf Thread.State = NetState.Download Then
+                                        DownloadingCount += 1
                                     End If
                                     End If
                                 Next
                                 Next
-                                '等待 40 ms:BMCLAPI 部分时候有 20QPS 的限制
-                                Thread.Sleep(40)
-                            Loop While NewThreadCount > 0 AndAlso IsSuccess
-                        End If
-                        '等待 40 ms
-                        Thread.Sleep(40)
+                            End If
+                            '新增线程
+                            If PreparingCount > DownloadingCount Then Continue For '准备中的线程已多于下载中的线程,不再新增
+                            Dim NewThread = File.TryBeginThread()
+                            If NewThread IsNot Nothing AndAlso NewThread.Source.Url.Contains("bmclapi.") Then Thread.Sleep(40) '减少 BMCLAPI 请求频率,避免 Too Many Requests
+                        Next
                     End While
                     End While
                 Catch ex As Exception
                 Catch ex As Exception
-                    Log(ex, "下载管理启动线程 2 出错", LogLevel.Assert)
+                    Log(ex, $"下载管理启动线程 {Id} 出错", LogLevel.Assert)
                 End Try
                 End Try
-            End Sub, "NetManager ThreadStarter Odd")
+            End Sub
+            RunInNewThread(Sub() ThreadStarter(0), "NetManager ThreadStarter 0")
+            RunInNewThread(Sub() ThreadStarter(1), "NetManager ThreadStarter 1")
             RunInNewThread(
             RunInNewThread(
             Sub()
             Sub()
                 Try
                 Try
@@ -1989,8 +1926,8 @@ Retry:
                         NetTaskSpeedLimitLeftLast = TimeNow
                         NetTaskSpeedLimitLeftLast = TimeNow
                         '刷新公开属性
                         '刷新公开属性
                         RefreshStat()
                         RefreshStat()
-                        '等待直至 170 ms
-                        Do While GetTimeTick() - LastLoopTime < 170
+                        '等待直至 80 ms
+                        Do While GetTimeTick() - LastLoopTime < 80
                             Thread.Sleep(10)
                             Thread.Sleep(10)
                         Loop
                         Loop
                     End While
                     End While

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

@@ -149,7 +149,7 @@ Public Class ValidateExcept
     End Sub
     End Sub
     Public Overrides Function Validate(Str As String) As String
     Public Overrides Function Validate(Str As String) As String
         For Each Ch As String In Excepts
         For Each Ch As String In Excepts
-            If Str.IndexOfF(Ch, StringComparison.OrdinalIgnoreCase) >= 0 Then
+            If Str.IndexOfF(Ch, True) >= 0 Then
                 If IsNothing(ErrorMessage) Then ErrorMessage = ""
                 If IsNothing(ErrorMessage) Then ErrorMessage = ""
                 Return ErrorMessage.Replace("%", Ch)
                 Return ErrorMessage.Replace("%", Ch)
             End If
             End If
@@ -355,26 +355,3 @@ Fin:
         Return ""
         Return ""
     End Function
     End Function
 End Class
 End Class
-
-''' <summary>
-''' 敏感词过滤。
-''' </summary>
-Public Class ValidateFilter
-    Inherits Validate
-    Private Shared Excepts As String() = SecretDecrypt(SecretSensitiveWordsRaw).Split("|")
-    Public Sub New()
-    End Sub '用于 XAML 初始化
-    Public Overrides Function Validate(Str As String) As String
-        '提取无间隔符的字符串
-        Dim RealStr As String = ""
-        For Each St In If(Str, "").ToLower
-            If String.IsNullOrWhiteSpace(St.ToString) OrElse ("!@#$%^&*()_+-={}[]|\;':"",./<>? ~ ·!¥…()—,。、《》?‘;:【】{}" & vbLQ & vbRQ).ToCharArray.Contains(St) Then Continue For
-            RealStr += St
-        Next
-        '检查
-        For Each Except In Excepts
-            If RealStr.Contains(Except) Then Return "存在敏感内容!"
-        Next
-        Return ""
-    End Function
-End Class

+ 21 - 21
Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb

@@ -265,7 +265,7 @@
                     Next
                     Next
                     CurseForgeFileIds = Files.Select(Function(f) f.Key).Distinct.ToList
                     CurseForgeFileIds = Files.Select(Function(f) f.Key).Distinct.ToList
                     GameVersions = Files.SelectMany(Function(f) f.Value).Where(Function(v) v.StartsWithF("1.")).
                     GameVersions = Files.SelectMany(Function(f) f.Value).Where(Function(v) v.StartsWithF("1.")).
-                        Select(Function(v) CInt(Val(v.Split(".")(1).Split("-").First))).Where(Function(v) v > 0).
+                        Select(Function(v) CInt(Val(v.Split(".")(1).Before("-")))).Where(Function(v) v > 0).
                         Distinct.OrderByDescending(Function(v) v).ToList
                         Distinct.OrderByDescending(Function(v) v).ToList
                     ModLoaders = ModLoaders.Distinct.OrderBy(Of Integer)(Function(t) t).ToList
                     ModLoaders = ModLoaders.Distinct.OrderBy(Of Integer)(Function(t) t).ToList
                     'Type
                     'Type
@@ -342,7 +342,7 @@
                     '搜索结果的键为 versions,获取特定工程的键为 game_versions
                     '搜索结果的键为 versions,获取特定工程的键为 game_versions
                     GameVersions = If(CType(If(Data("game_versions"), Data("versions")), JArray), New JArray).
                     GameVersions = If(CType(If(Data("game_versions"), Data("versions")), JArray), New JArray).
                                        Select(Function(v) v.ToString).Where(Function(v) v.StartsWithF("1.")).
                                        Select(Function(v) v.ToString).Where(Function(v) v.StartsWithF("1.")).
-                                       Select(Of Integer)(Function(v) Val(v.Split(".")(1).Split("-").First)).Where(Function(v) v > 0).
+                                       Select(Of Integer)(Function(v) Val(v.Split(".")(1).Before("-"))).Where(Function(v) v > 0).
                                        Distinct.OrderByDescending(Function(v) v).ToList
                                        Distinct.OrderByDescending(Function(v) v).ToList
                     'Type
                     'Type
                     Select Case Data("project_type").ToString
                     Select Case Data("project_type").ToString
@@ -414,10 +414,10 @@
             Json("Website") = Website
             Json("Website") = Website
             If LastUpdate IsNot Nothing Then Json("LastUpdate") = LastUpdate
             If LastUpdate IsNot Nothing Then Json("LastUpdate") = LastUpdate
             Json("DownloadCount") = DownloadCount
             Json("DownloadCount") = DownloadCount
-            If ModLoaders IsNot Nothing AndAlso ModLoaders.Count > 0 Then Json("ModLoaders") = New JArray(ModLoaders.Select(Function(m) CInt(m)))
+            If ModLoaders IsNot Nothing AndAlso ModLoaders.Any Then Json("ModLoaders") = New JArray(ModLoaders.Select(Function(m) CInt(m)))
             Json("Tags") = New JArray(Tags)
             Json("Tags") = New JArray(Tags)
             If LogoUrl IsNot Nothing Then Json("LogoUrl") = LogoUrl
             If LogoUrl IsNot Nothing Then Json("LogoUrl") = LogoUrl
-            If GameVersions.Count > 0 Then Json("GameVersions") = New JArray(GameVersions)
+            If GameVersions.Any Then Json("GameVersions") = New JArray(GameVersions)
             Json("CacheTime") = Date.Now '用于检查缓存时间
             Json("CacheTime") = Date.Now '用于检查缓存时间
             Return Json
             Return Json
         End Function
         End Function
@@ -576,9 +576,9 @@
                 '有中文翻译
                 '有中文翻译
                 '尝试将文本分为三段:Title (EnglishName) - Suffix
                 '尝试将文本分为三段:Title (EnglishName) - Suffix
                 '检查时注意 Carpet:它没有中文译名,但有 Suffix
                 '检查时注意 Carpet:它没有中文译名,但有 Suffix
-                Title = TranslatedName.Split(" (").First.Split(" - ").First
+                Title = TranslatedName.Before(" (").Before(" - ")
                 Dim Suffix As String = ""
                 Dim Suffix As String = ""
-                If TranslatedName.Split(")").Last.Contains(" - ") Then Suffix = TranslatedName.Split(")").Last.Split(" - ").Last
+                If TranslatedName.After(")").Contains(" - ") Then Suffix = TranslatedName.After(")").After(" - ")
                 Dim EnglishName As String = TranslatedName
                 Dim EnglishName As String = TranslatedName
                 If Suffix <> "" Then EnglishName = EnglishName.Replace(" - " & Suffix, "")
                 If Suffix <> "" Then EnglishName = EnglishName.Replace(" - " & Suffix, "")
                 EnglishName = EnglishName.Replace(Title, "").Trim("("c, ")"c, " "c)
                 EnglishName = EnglishName.Replace(Title, "").Trim("("c, ")"c, " "c)
@@ -593,10 +593,10 @@
                 '添加后缀
                 '添加后缀
                 If Suffix <> "" Then SubtitleList.Add(Suffix)
                 If Suffix <> "" Then SubtitleList.Add(Suffix)
             End If
             End If
-            SubtitleList = SubtitleList.Distinct().ToList()
+            SubtitleList = SubtitleList.Distinct.ToList()
             '设置标题与描述
             '设置标题与描述
             Dim Subtitle As String = ""
             Dim Subtitle As String = ""
-            If SubtitleList.Count > 0 Then
+            If SubtitleList.Any Then
                 For Each Ex In SubtitleList
                 For Each Ex In SubtitleList
                     Dim IsModLoaderDescription As Boolean =
                     Dim IsModLoaderDescription As Boolean =
                         Ex.ToLower.Contains("forge") OrElse Ex.ToLower.Contains("fabric") OrElse Ex.ToLower.Contains("quilt")
                         Ex.ToLower.Contains("forge") OrElse Ex.ToLower.Contains("fabric") OrElse Ex.ToLower.Contains("quilt")
@@ -751,7 +751,7 @@ NoSubtitle:
                 Case CompType.ResourcePack
                 Case CompType.ResourcePack
                     'FUTURE: Res
                     'FUTURE: Res
             End Select
             End Select
-            Address += "&categoryId=" & If(Tag = "", "0", Tag.Split("/").First)
+            Address += "&categoryId=" & If(Tag = "", "0", Tag.Before("/"))
             If ModLoader <> CompModLoaderType.Any Then Address += "&modLoaderType=" & CType(ModLoader, Integer)
             If ModLoader <> CompModLoaderType.Any Then Address += "&modLoaderType=" & CType(ModLoader, Integer)
             If Not String.IsNullOrEmpty(GameVersion) Then Address += "&gameVersion=" & GameVersion
             If Not String.IsNullOrEmpty(GameVersion) Then Address += "&gameVersion=" & GameVersion
             If Not String.IsNullOrEmpty(SearchText) Then Address += "&searchFilter=" & Net.WebUtility.UrlEncode(SearchText)
             If Not String.IsNullOrEmpty(SearchText) Then Address += "&searchFilter=" & Net.WebUtility.UrlEncode(SearchText)
@@ -772,7 +772,7 @@ NoSubtitle:
             'facets=[["categories:'game-mechanics'"],["categories:'forge'"],["versions:1.19.3"],["project_type:mod"]]
             'facets=[["categories:'game-mechanics'"],["categories:'forge'"],["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:{GetStringFromEnum(Type).ToLower}""]")
             Facets.Add($"[""project_type:{GetStringFromEnum(Type).ToLower}""]")
-            If Not String.IsNullOrEmpty(Tag) Then Facets.Add($"[""categories:'{Tag.Split("/").Last}'""]")
+            If Not String.IsNullOrEmpty(Tag) Then Facets.Add($"[""categories:'{Tag.After("/")}'""]")
             If ModLoader <> CompModLoaderType.Any Then Facets.Add($"[""categories:'{GetStringFromEnum(ModLoader).ToLower}'""]")
             If ModLoader <> CompModLoaderType.Any Then Facets.Add($"[""categories:'{GetStringFromEnum(ModLoader).ToLower}'""]")
             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) & "]"
@@ -880,7 +880,7 @@ NoSubtitle:
                 If Not SearchResults(i).AbsoluteRight AndAlso i >= Math.Min(2, SearchResults.Count - 1) Then Exit For '把 3 个结果拼合以提高准确度
                 If Not SearchResults(i).AbsoluteRight AndAlso i >= Math.Min(2, SearchResults.Count - 1) Then Exit For '把 3 个结果拼合以提高准确度
                 If SearchResults(i).Item.CurseForgeSlug IsNot Nothing Then SearchResult += SearchResults(i).Item.CurseForgeSlug.Replace("-", " ").Replace("/", " ") & " "
                 If SearchResults(i).Item.CurseForgeSlug IsNot Nothing Then SearchResult += SearchResults(i).Item.CurseForgeSlug.Replace("-", " ").Replace("/", " ") & " "
                 If SearchResults(i).Item.ModrinthSlug IsNot Nothing Then SearchResult += SearchResults(i).Item.ModrinthSlug.Replace("-", " ").Replace("/", " ") & " "
                 If SearchResults(i).Item.ModrinthSlug IsNot Nothing Then SearchResult += SearchResults(i).Item.ModrinthSlug.Replace("-", " ").Replace("/", " ") & " "
-                SearchResult += SearchResults(i).Item.ChineseName.Split(" (").Last.TrimEnd(") ").Split(" - ").First.
+                SearchResult += SearchResults(i).Item.ChineseName.After(" (").TrimEnd(") ").Before(" - ").
                     Replace(":", "").Replace("(", "").Replace(")", "").ToLower.Replace("/", " ") & " "
                     Replace(":", "").Replace("(", "").Replace(")", "").ToLower.Replace("/", " ") & " "
             Next
             Next
             Log("[Comp] 中文搜索原始关键词:" & SearchResult, LogLevel.Developer)
             Log("[Comp] 中文搜索原始关键词:" & SearchResult, LogLevel.Developer)
@@ -1046,7 +1046,7 @@ Retry:
         '这样做的话,去重后将优先保留 CurseForge 内容(考虑到 CurseForge 热度更高)
         '这样做的话,去重后将优先保留 CurseForge 内容(考虑到 CurseForge 热度更高)
         RawResults = RawResults.Where(Function(x) Not x.FromCurseForge).Concat(RawResults.Where(Function(x) x.FromCurseForge)).ToList
         RawResults = RawResults.Where(Function(x) Not x.FromCurseForge).Concat(RawResults.Where(Function(x) x.FromCurseForge)).ToList
         'RawResults 去重
         'RawResults 去重
-        RawResults = Distinct(RawResults, Function(a, b) a.IsLike(b))
+        RawResults = RawResults.Distinct(Function(a, b) a.IsLike(b))
         '已有内容去重
         '已有内容去重
         RawResults = RawResults.Where(Function(r) Not RealResults.Any(Function(b) r.IsLike(b)) AndAlso
         RawResults = RawResults.Where(Function(r) Not RealResults.Any(Function(b) r.IsLike(b)) AndAlso
                                                   Not Storage.Results.Any(Function(b) r.IsLike(b))).ToList
                                                   Not Storage.Results.Any(Function(b) r.IsLike(b))).ToList
@@ -1133,7 +1133,7 @@ Retry:
         '描述性信息
         '描述性信息
 
 
         ''' <summary>
         ''' <summary>
-        ''' 文件描述名(并非文件名,是自定义的字段)。
+        ''' 文件描述名(并非文件名,是自定义的字段)。对很多 Mod,这会给出 Mod 版本号。
         ''' </summary>
         ''' </summary>
         Public DisplayName As String
         Public DisplayName As String
         ''' <summary>
         ''' <summary>
@@ -1305,7 +1305,7 @@ Retry:
                     'GameVersions
                     'GameVersions
                     Dim RawVersions As List(Of String) = Data("game_versions").Select(Function(t) t.ToString.Trim.ToLower).ToList
                     Dim RawVersions As List(Of String) = Data("game_versions").Select(Function(t) t.ToString.Trim.ToLower).ToList
                     GameVersions = RawVersions.Where(Function(v) v.StartsWithF("1.") OrElse v.StartsWithF("b1.")).
                     GameVersions = RawVersions.Where(Function(v) v.StartsWithF("1.") OrElse v.StartsWithF("b1.")).
-                                               Select(Function(v) If(v.Contains("-"), v.Split("-").First & " 快照", If(v.StartsWithF("b1."), "远古版本", v))).ToList
+                                               Select(Function(v) If(v.Contains("-"), v.Before("-") & " 快照", If(v.StartsWithF("b1."), "远古版本", v))).ToList
                     If GameVersions.Count > 1 Then
                     If GameVersions.Count > 1 Then
                         GameVersions = Sort(GameVersions, AddressOf VersionSortBoolean).ToList
                         GameVersions = Sort(GameVersions, AddressOf VersionSortBoolean).ToList
                         If Type = CompType.ModPack Then GameVersions = New List(Of String) From {GameVersions(0)}
                         If Type = CompType.ModPack Then GameVersions = New List(Of String) From {GameVersions(0)}
@@ -1356,9 +1356,9 @@ Retry:
             Dim Info As String = ""
             Dim Info As String = ""
             Select Case Type
             Select Case Type
                 Case CompType.Mod
                 Case CompType.Mod
-                    Info += If(ModLoaders.Count > 0,
+                    Info += If(ModLoaders.Any,
                         "适用于 " & Join(ModLoaders.Select(Function(m) GetStringFromEnum(m)).ToList, "/") & ",", "")
                         "适用于 " & Join(ModLoaders.Select(Function(m) GetStringFromEnum(m)).ToList, "/") & ",", "")
-                    Info += If(ModeDebug AndAlso Dependencies.Count > 0, Dependencies.Count & " 个前置 Mod,", "")
+                    Info += If(ModeDebug AndAlso Dependencies.Any, Dependencies.Count & " 个前置 Mod,", "")
                 Case CompType.ModPack
                 Case CompType.ModPack
                     If GameVersions.All(Function(v) v.Contains("w")) Then
                     If GameVersions.All(Function(v) v.Contains("w")) Then
                         Info += $"游戏版本 {Join(GameVersions, "、")},"
                         Info += $"游戏版本 {Join(GameVersions, "、")},"
@@ -1403,7 +1403,7 @@ Retry:
         '辅助函数
         '辅助函数
 
 
         Public Overrides Function ToString() As String
         Public Overrides Function ToString() As String
-            Return $"{Id}: {DisplayName}"
+            Return $"{Id}: {FileName}"
         End Function
         End Function
 
 
     End Class
     End Class
@@ -1444,15 +1444,15 @@ Retry:
             'Modrinth
             'Modrinth
             ResultJsonArray = NetGetCodeByRequestRetry($"https://api.modrinth.com/v2/project/{ProjectId}/version", Accept:="application/json", IsJson:=True)
             ResultJsonArray = NetGetCodeByRequestRetry($"https://api.modrinth.com/v2/project/{ProjectId}/version", Accept:="application/json", IsJson:=True)
         End If
         End If
-        CompFilesCache(ProjectId) = Distinct(ResultJsonArray.Select(Function(a) New CompFile(a, TargetProject.Type)).
-            Where(Function(a) a.Available).ToList, Function(a, b) a.Id = b.Id) 'CurseForge 可能会重复返回相同项(#1330)
+        CompFilesCache(ProjectId) = ResultJsonArray.Select(Function(a) New CompFile(a, TargetProject.Type)).
+            Where(Function(a) a.Available).ToList.Distinct(Function(a, b) a.Id = b.Id) 'CurseForge 可能会重复返回相同项(#1330)
         '获取前置 Mod 列表
         '获取前置 Mod 列表
         If TargetProject.Type <> CompType.Mod Then Return CompFilesCache(ProjectId)
         If TargetProject.Type <> CompType.Mod Then Return CompFilesCache(ProjectId)
         Dim Deps As List(Of String) = CompFilesCache(ProjectId).
         Dim Deps As List(Of String) = CompFilesCache(ProjectId).
             SelectMany(Function(f) f.RawDependencies).Distinct().ToList
             SelectMany(Function(f) f.RawDependencies).Distinct().ToList
         Dim UndoneDeps = Deps.Where(Function(f) Not CompProjectCache.ContainsKey(f)).ToList
         Dim UndoneDeps = Deps.Where(Function(f) Not CompProjectCache.ContainsKey(f)).ToList
         '获取前置 Mod 工程信息
         '获取前置 Mod 工程信息
-        If UndoneDeps.Count > 0 Then
+        If UndoneDeps.Any Then
             Log($"[Comp] {ProjectId} 文件列表中还需要获取信息的前置 Mod:{Join(UndoneDeps, ",")}")
             Log($"[Comp] {ProjectId} 文件列表中还需要获取信息的前置 Mod:{Join(UndoneDeps, ",")}")
             Dim Projects As JArray
             Dim Projects As JArray
             If TargetProject.FromCurseForge Then
             If TargetProject.FromCurseForge Then
@@ -1467,7 +1467,7 @@ Retry:
             Next
             Next
         End If
         End If
         '更新前置 Mod 信息
         '更新前置 Mod 信息
-        If Deps.Count > 0 Then
+        If Deps.Any Then
             For Each DepProject In Deps.Select(Function(id) CompProjectCache(id))
             For Each DepProject In Deps.Select(Function(id) CompProjectCache(id))
                 For Each File In CompFilesCache(ProjectId)
                 For Each File In CompFilesCache(ProjectId)
                     If File.RawDependencies.Contains(DepProject.Id) AndAlso DepProject.Id <> ProjectId Then
                     If File.RawDependencies.Contains(DepProject.Id) AndAlso DepProject.Id <> ProjectId Then

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

@@ -94,7 +94,7 @@
                 Log(ex, "读取可能的崩溃日志文件失败(" & FilePath & ")")
                 Log(ex, "读取可能的崩溃日志文件失败(" & FilePath & ")")
             End Try
             End Try
         Next
         Next
-        If LatestLog IsNot Nothing AndAlso LatestLog.Count > 0 Then
+        If LatestLog IsNot Nothing AndAlso LatestLog.Any Then
             Dim RawOutput As String = Join(LatestLog, vbCrLf)
             Dim RawOutput As String = Join(LatestLog, vbCrLf)
             Log("[Crash] 以下为游戏输出的最后一段内容:" & vbCrLf & RawOutput)
             Log("[Crash] 以下为游戏输出的最后一段内容:" & vbCrLf & RawOutput)
             WriteFile(TempFolder & "RawOutput.log", RawOutput)
             WriteFile(TempFolder & "RawOutput.log", RawOutput)
@@ -110,21 +110,20 @@
     Public Sub Import(FilePath As String)
     Public Sub Import(FilePath As String)
         Log("[Crash] 步骤 1:自主导入日志文件")
         Log("[Crash] 步骤 1:自主导入日志文件")
 
 
-        '解压压缩包
+        '尝试视作压缩包解压
         Try
         Try
             Dim Info As New FileInfo(FilePath)
             Dim Info As New FileInfo(FilePath)
-            If Not Info.Exists OrElse Info.Length = 0 Then Exit Try
-            If Not FilePath.EndsWithF(".jar", True) AndAlso ExtractFile(FilePath, TempFolder & "Temp\") Then
-                '解压成功
+            If Info.Exists AndAlso Info.Length > 0 AndAlso Not FilePath.EndsWithF(".jar", True) Then
+                ExtractFile(FilePath, TempFolder & "Temp\")
                 Log("[Crash] 已解压导入的日志文件:" & FilePath)
                 Log("[Crash] 已解压导入的日志文件:" & FilePath)
-            Else
-                '解压失败
-                CopyFile(FilePath, TempFolder & "Temp\" & GetFileNameFromPath(FilePath))
-                Log("[Crash] 已复制导入的日志文件:" & FilePath)
+                GoTo Extracted
             End If
             End If
-        Catch ex As Exception
-            Log(ex, "解压导入文件中的压缩包失败")
+        Catch
         End Try
         End Try
+        '并非压缩包
+        CopyFile(FilePath, TempFolder & "Temp\" & GetFileNameFromPath(FilePath))
+        Log("[Crash] 已复制导入的日志文件:" & FilePath)
+Extracted:
 
 
         '导入其中的日志文件
         '导入其中的日志文件
         For Each TargetFile As FileInfo In New DirectoryInfo(TempFolder & "Temp\").EnumerateFiles.ToList()
         For Each TargetFile As FileInfo In New DirectoryInfo(TempFolder & "Temp\").EnumerateFiles.ToList()
@@ -313,16 +312,30 @@
 
 
     End Function
     End Function
     ''' <summary>
     ''' <summary>
-    ''' 输出字符串的前后某些行,并统一行尾为 vbLf (\r)、删除空行。
+    ''' 输出字符串的前后某些行,并统一行尾为 vbLf (正则 \n)、删除空行和重复行。
     ''' </summary>
     ''' </summary>
     Private Function GetHeadTailLines(Raw As String(), HeadLines As Integer, TailLines As Integer) As String
     Private Function GetHeadTailLines(Raw As String(), HeadLines As Integer, TailLines As Integer) As String
-        If Raw.Length <= HeadLines + TailLines Then Return Join(Raw, vbLf)
+        If Raw.Length <= HeadLines + TailLines Then Return Join(Raw.Distinct, vbLf)
+        Dim Lines As New List(Of String)
+        Dim RealHeadLines As Integer = 0, ViewedLines As Integer
+        For ViewedLines = 0 To Raw.Length - 1
+            If Lines.Contains(Raw(ViewedLines)) Then Continue For
+            RealHeadLines += 1
+            Lines.Add(Raw(ViewedLines))
+            If RealHeadLines >= HeadLines Then Exit For
+        Next
+        Dim RealTailLines = 0
+        For i = Raw.Length - 1 To ViewedLines Step -1
+            If Lines.Contains(Raw(i)) Then Continue For
+            RealTailLines += 1
+            Lines.Insert(RealHeadLines, Raw(i))
+            If RealTailLines >= TailLines Then Exit For
+        Next
         Dim Result As New StringBuilder
         Dim Result As New StringBuilder
-        For i = 0 To Raw.Count - 1
-            If i < HeadLines OrElse Raw.Count - i < TailLines Then
-                If Raw(i) = "" Then Continue For
-                Result.Append(Raw(i) & vbLf)
-            End If
+        For Each Line In Lines
+            If Line = "" Then Continue For
+            Result.Append(Line)
+            Result.Append(vbLf)
         Next
         Next
         Return Result.ToString
         Return Result.ToString
     End Function
     End Function
@@ -354,6 +367,7 @@
         光影或资源包导致OpenGL1282错误
         光影或资源包导致OpenGL1282错误
         文件或内容校验失败
         文件或内容校验失败
         确定Mod导致游戏崩溃
         确定Mod导致游戏崩溃
+        怀疑Mod导致游戏崩溃
         Mod配置文件导致游戏崩溃
         Mod配置文件导致游戏崩溃
         ModMixin失败
         ModMixin失败
         Mod加载器报错
         Mod加载器报错
@@ -389,9 +403,9 @@
 
 
         '1. 精准日志匹配,中/高优先级
         '1. 精准日志匹配,中/高优先级
         AnalyzeCrit1()
         AnalyzeCrit1()
-        If CrashReasons.Count > 0 Then GoTo Done
+        If CrashReasons.Any Then GoTo Done
         AnalyzeCrit2()
         AnalyzeCrit2()
-        If CrashReasons.Count > 0 Then GoTo Done
+        If CrashReasons.Any Then GoTo Done
 
 
         '2. 堆栈分析
         '2. 堆栈分析
         If LogAll.Contains("orge") OrElse LogAll.Contains("abric") OrElse LogAll.Contains("uilt") OrElse LogAll.Contains("iteloader") Then
         If LogAll.Contains("orge") OrElse LogAll.Contains("abric") OrElse LogAll.Contains("uilt") OrElse LogAll.Contains("iteloader") Then
@@ -399,27 +413,25 @@
             '崩溃日志
             '崩溃日志
             If LogCrash IsNot Nothing Then
             If LogCrash IsNot Nothing Then
                 Log("[Crash] 开始进行崩溃日志堆栈分析")
                 Log("[Crash] 开始进行崩溃日志堆栈分析")
-                Dim StackLogs As String = LogCrash.Split("System Details").First
-                Keywords.AddRange(AnalyzeStackKeyword(StackLogs))
+                Keywords.AddRange(AnalyzeStackKeyword(LogCrash.Before("System Details")))
             End If
             End If
             'Minecraft 日志
             'Minecraft 日志
             If LogMc IsNot Nothing Then
             If LogMc IsNot Nothing Then
-                Dim Fatals = RegexSearch(LogMc, "/FATAL] [\w\W]+?(?=[\n]+\[)")
+                Dim Fatals As List(Of String) = RegexSearch(LogMc, "/FATAL] .+?(?=[\n]+\[)")
+                If LogMc.Contains("Unreported exception thrown!") Then Fatals.Add(LogMc.Between("Unreported exception thrown!", "at oolloo.jlw.Wrapper"))
                 Log("[Crash] 开始进行 Minecraft 日志堆栈分析,发现 " & Fatals.Count & " 个报错项")
                 Log("[Crash] 开始进行 Minecraft 日志堆栈分析,发现 " & Fatals.Count & " 个报错项")
-                If Fatals.Count > 0 Then
-                    For Each Fatal In Fatals
-                        Keywords.AddRange(AnalyzeStackKeyword(Fatal))
-                    Next
-                End If
+                For Each Fatal In Fatals
+                    Keywords.AddRange(AnalyzeStackKeyword(Fatal))
+                Next
             End If
             End If
             '虚拟机日志
             '虚拟机日志
             If LogHs IsNot Nothing Then
             If LogHs IsNot Nothing Then
                 Log("[Crash] 开始进行虚拟机堆栈分析")
                 Log("[Crash] 开始进行虚拟机堆栈分析")
-                Dim StackLogs As String = LogHs.Split("Registers:").First.Split("T H R E A D").Last
+                Dim StackLogs As String = LogHs.Between("T H R E A D", "Registers:")
                 Keywords.AddRange(AnalyzeStackKeyword(StackLogs))
                 Keywords.AddRange(AnalyzeStackKeyword(StackLogs))
             End If
             End If
             'Mod 名称分析
             'Mod 名称分析
-            If Keywords.Count > 0 Then
+            If Keywords.Any Then
                 Dim Names = AnalyzeModName(Keywords)
                 Dim Names = AnalyzeModName(Keywords)
                 If Names Is Nothing Then
                 If Names Is Nothing Then
                     AppendReason(CrashReason.堆栈分析发现关键字, Keywords)
                     AppendReason(CrashReason.堆栈分析发现关键字, Keywords)
@@ -442,7 +454,7 @@ Done:
         Else
         Else
             Log("[Crash] 步骤 3:分析崩溃原因完成,找到 " & CrashReasons.Count & " 条可能的原因")
             Log("[Crash] 步骤 3:分析崩溃原因完成,找到 " & CrashReasons.Count & " 条可能的原因")
             For Each Reason In CrashReasons
             For Each Reason In CrashReasons
-                Log("[Crash]  - " & GetStringFromEnum(Reason.Key) & If(Reason.Value.Count > 0, "(" & Join(Reason.Value, ";") & ")", ""))
+                Log("[Crash]  - " & GetStringFromEnum(Reason.Key) & If(Reason.Value.Any, "(" & Join(Reason.Value, ";") & ")", ""))
             Next
             Next
         End If
         End If
     End Sub
     End Sub
@@ -458,7 +470,7 @@ Done:
         Else
         Else
             CrashReasons.Add(Reason, New List(Of String)(If(Additional, {})))
             CrashReasons.Add(Reason, New List(Of String)(If(Additional, {})))
         End If
         End If
-        Log("[Crash] 可能的崩溃原因:" & GetStringFromEnum(Reason) & If(Additional IsNot Nothing AndAlso Additional.Count > 0, "(" & Join(Additional, ";") & ")", ""))
+        Log("[Crash] 可能的崩溃原因:" & GetStringFromEnum(Reason) & If(Additional IsNot Nothing AndAlso Additional.Any, "(" & Join(Additional, ";") & ")", ""))
     End Sub
     End Sub
     Private Sub AppendReason(Reason As CrashReason, Additional As String)
     Private Sub AppendReason(Reason As CrashReason, Additional As String)
         AppendReason(Reason, If(String.IsNullOrEmpty(Additional), Nothing, New List(Of String) From {Additional}))
         AppendReason(Reason, If(String.IsNullOrEmpty(Additional), Nothing, New List(Of String) From {Additional}))
@@ -503,7 +515,7 @@ Done:
             If LogMc.Contains("Extracted mod jars found, loading will NOT continue") Then AppendReason(CrashReason.Mod文件被解压)
             If LogMc.Contains("Extracted mod jars found, loading will NOT continue") Then AppendReason(CrashReason.Mod文件被解压)
             If LogMc.Contains("java.lang.ClassNotFoundException: org.spongepowered.asm.launch.MixinTweaker") Then AppendReason(CrashReason.MixinBootstrap缺失)
             If LogMc.Contains("java.lang.ClassNotFoundException: org.spongepowered.asm.launch.MixinTweaker") Then AppendReason(CrashReason.MixinBootstrap缺失)
             If LogMc.Contains("Couldn't set pixel format") Then AppendReason(CrashReason.显卡驱动不支持导致无法设置像素格式)
             If LogMc.Contains("Couldn't set pixel format") Then AppendReason(CrashReason.显卡驱动不支持导致无法设置像素格式)
-            If LogMc.Contains("java.lang.OutOfMemoryError") Then AppendReason(CrashReason.内存不足)
+            If LogMc.Contains("java.lang.OutOfMemoryError") OrElse LogMc.Contains("an out of memory error") Then AppendReason(CrashReason.内存不足)
             If LogMc.Contains("java.lang.RuntimeException: Shaders Mod detected. Please remove it, OptiFine has built-in support for shaders.") Then AppendReason(CrashReason.ShadersMod与Optifine同时安装)
             If LogMc.Contains("java.lang.RuntimeException: Shaders Mod detected. Please remove it, OptiFine has built-in support for shaders.") Then AppendReason(CrashReason.ShadersMod与Optifine同时安装)
             If LogMc.Contains("java.lang.NoSuchMethodError: sun.security.util.ManifestEntryVerifier") Then AppendReason(CrashReason.低版本Forge与高版本Java不兼容)
             If LogMc.Contains("java.lang.NoSuchMethodError: sun.security.util.ManifestEntryVerifier") Then AppendReason(CrashReason.低版本Forge与高版本Java不兼容)
             If LogMc.Contains("1282: Invalid operation") Then AppendReason(CrashReason.光影或资源包导致OpenGL1282错误)
             If LogMc.Contains("1282: Invalid operation") Then AppendReason(CrashReason.光影或资源包导致OpenGL1282错误)
@@ -531,6 +543,7 @@ Done:
             'Mod 重复安装
             'Mod 重复安装
             If LogMc.Contains("DuplicateModsFoundException") Then AppendReason(CrashReason.Mod重复安装, RegexSearch(LogMc, "(?<=\n\t[\w]+ : [A-Z]{1}:[^\n]+(/|\\))[^/\\\n]+?.jar", RegularExpressions.RegexOptions.IgnoreCase))
             If LogMc.Contains("DuplicateModsFoundException") Then AppendReason(CrashReason.Mod重复安装, RegexSearch(LogMc, "(?<=\n\t[\w]+ : [A-Z]{1}:[^\n]+(/|\\))[^/\\\n]+?.jar", RegularExpressions.RegexOptions.IgnoreCase))
             If LogMc.Contains("Found a duplicate mod") Then AppendReason(CrashReason.Mod重复安装, RegexSearch(If(RegexSeek(LogMc, "Found a duplicate mod[^\n]+"), ""), "[^\\/]+.jar", RegularExpressions.RegexOptions.IgnoreCase))
             If LogMc.Contains("Found a duplicate mod") Then AppendReason(CrashReason.Mod重复安装, RegexSearch(If(RegexSeek(LogMc, "Found a duplicate mod[^\n]+"), ""), "[^\\/]+.jar", RegularExpressions.RegexOptions.IgnoreCase))
+            If LogMc.Contains("Found duplicate mods") Then AppendReason(CrashReason.Mod重复安装, RegexSearch(LogMc, "(?<=Mod ID: ')\w+?(?=' from mod files:)").Distinct.ToList)
             If LogMc.Contains("ModResolutionException: Duplicate") Then AppendReason(CrashReason.Mod重复安装, RegexSearch(If(RegexSeek(LogMc, "ModResolutionException: Duplicate[^\n]+"), ""), "[^\\/]+.jar", RegularExpressions.RegexOptions.IgnoreCase))
             If LogMc.Contains("ModResolutionException: Duplicate") Then AppendReason(CrashReason.Mod重复安装, RegexSearch(If(RegexSeek(LogMc, "ModResolutionException: Duplicate[^\n]+"), ""), "[^\\/]+.jar", RegularExpressions.RegexOptions.IgnoreCase))
             'Mod 缺少前置
             'Mod 缺少前置
             If LogMc.Contains("Missing or unsupported mandatory dependencies:") Then
             If LogMc.Contains("Missing or unsupported mandatory dependencies:") Then
@@ -557,17 +570,16 @@ Done:
             If LogCrash.Contains("java.lang.OutOfMemoryError") Then AppendReason(CrashReason.内存不足)
             If LogCrash.Contains("java.lang.OutOfMemoryError") Then AppendReason(CrashReason.内存不足)
             If LogCrash.Contains("Pixel format not accelerated") Then AppendReason(CrashReason.显卡驱动不支持导致无法设置像素格式)
             If LogCrash.Contains("Pixel format not accelerated") Then AppendReason(CrashReason.显卡驱动不支持导致无法设置像素格式)
             If LogCrash.Contains("Manually triggered debug crash") Then AppendReason(CrashReason.玩家手动触发调试崩溃)
             If LogCrash.Contains("Manually triggered debug crash") Then AppendReason(CrashReason.玩家手动触发调试崩溃)
+            If LogCrash.Contains("has mods that were not found") AndAlso RegexCheck(LogCrash, "The Mod File [^\n]+optifine\\OptiFine[^\n]+ has mods that were not found") Then AppendReason(CrashReason.OptiFine与Forge不兼容)
             'Mod 导致的崩溃
             'Mod 导致的崩溃
             If LogCrash.Contains("-- MOD ") Then
             If LogCrash.Contains("-- MOD ") Then
-                Dim LogLeft = LogCrash.Split("-- MOD").Last
-                If LogLeft.Contains("Failure message: MISSING") Then
+                If LogCrash.Between("-- MOD ", "Failure message:").ContainsF(".jar", True) Then
                     AppendReason(CrashReason.确定Mod导致游戏崩溃, If(RegexSeek(LogCrash, "(?<=Mod File: ).+"), "").TrimEnd((vbCrLf & " ").ToCharArray))
                     AppendReason(CrashReason.确定Mod导致游戏崩溃, If(RegexSeek(LogCrash, "(?<=Mod File: ).+"), "").TrimEnd((vbCrLf & " ").ToCharArray))
                 Else
                 Else
                     AppendReason(CrashReason.Mod加载器报错, If(RegexSeek(LogCrash, "(?<=Failure message: )[\w\W]+?(?=\tMod)"), "").Replace(vbTab, " ").TrimEnd((vbCrLf & " ").ToCharArray))
                     AppendReason(CrashReason.Mod加载器报错, If(RegexSeek(LogCrash, "(?<=Failure message: )[\w\W]+?(?=\tMod)"), "").Replace(vbTab, " ").TrimEnd((vbCrLf & " ").ToCharArray))
                 End If
                 End If
             End If
             End If
             If LogCrash.Contains("Multiple entries with same key: ") Then AppendReason(CrashReason.确定Mod导致游戏崩溃, TryAnalyzeModName(If(RegexSeek(LogCrash, "(?<=Multiple entries with same key: )[^=]+"), "").TrimEnd((vbCrLf & " ").ToCharArray)))
             If LogCrash.Contains("Multiple entries with same key: ") Then AppendReason(CrashReason.确定Mod导致游戏崩溃, TryAnalyzeModName(If(RegexSeek(LogCrash, "(?<=Multiple entries with same key: )[^=]+"), "").TrimEnd((vbCrLf & " ").ToCharArray)))
-            If LogCrash.Contains("due to errors, provided by ") Then AppendReason(CrashReason.确定Mod导致游戏崩溃, TryAnalyzeModName(If(RegexSeek(LogCrash, "(?<=due to errors, provided by ')[^']+"), "").TrimEnd((vbCrLf & " ").ToCharArray)))
             If LogCrash.Contains("LoaderExceptionModCrash: Caught exception from ") Then AppendReason(CrashReason.确定Mod导致游戏崩溃, TryAnalyzeModName(If(RegexSeek(LogCrash, "(?<=LoaderExceptionModCrash: Caught exception from )[^\n]+"), "").TrimEnd((vbCrLf & " ").ToCharArray)))
             If LogCrash.Contains("LoaderExceptionModCrash: Caught exception from ") Then AppendReason(CrashReason.确定Mod导致游戏崩溃, TryAnalyzeModName(If(RegexSeek(LogCrash, "(?<=LoaderExceptionModCrash: Caught exception from )[^\n]+"), "").TrimEnd((vbCrLf & " ").ToCharArray)))
             If LogCrash.Contains("Failed loading config file ") Then AppendReason(CrashReason.Mod配置文件导致游戏崩溃, {TryAnalyzeModName(If(RegexSeek(LogCrash, "(?<=Failed loading config file .+ for modid )[^\n]+"), "").TrimEnd(vbCrLf)).First, If(RegexSeek(LogCrash, "(?<=Failed loading config file ).+(?= of type)"), "").TrimEnd(vbCrLf)})
             If LogCrash.Contains("Failed loading config file ") Then AppendReason(CrashReason.Mod配置文件导致游戏崩溃, {TryAnalyzeModName(If(RegexSeek(LogCrash, "(?<=Failed loading config file .+ for modid )[^\n]+"), "").TrimEnd(vbCrLf)).First, If(RegexSeek(LogCrash, "(?<=Failed loading config file ).+(?= of type)"), "").TrimEnd(vbCrLf)})
         End If
         End If
@@ -586,19 +598,9 @@ Done:
             If LogMc.Contains("A potential solution has been determined:") Then AppendReason(CrashReason.Fabric报错并给出解决方案, Join(RegexSearch(If(RegexSeek(LogMc, "(?<=A potential solution has been determined:\n)((\t)+ - [^\n]+\n)+"), ""), "(?<=(\t)+)[^\n]+"), vbLf))
             If LogMc.Contains("A potential solution has been determined:") Then AppendReason(CrashReason.Fabric报错并给出解决方案, Join(RegexSearch(If(RegexSeek(LogMc, "(?<=A potential solution has been determined:\n)((\t)+ - [^\n]+\n)+"), ""), "(?<=(\t)+)[^\n]+"), vbLf))
             If LogMc.Contains("A potential solution has been determined, this may resolve your problem:") Then AppendReason(CrashReason.Fabric报错并给出解决方案, Join(RegexSearch(If(RegexSeek(LogMc, "(?<=A potential solution has been determined, this may resolve your problem:\n)((\t)+ - [^\n]+\n)+"), ""), "(?<=(\t)+)[^\n]+"), vbLf))
             If LogMc.Contains("A potential solution has been determined, this may resolve your problem:") Then AppendReason(CrashReason.Fabric报错并给出解决方案, Join(RegexSearch(If(RegexSeek(LogMc, "(?<=A potential solution has been determined, this may resolve your problem:\n)((\t)+ - [^\n]+\n)+"), ""), "(?<=(\t)+)[^\n]+"), vbLf))
             If LogMc.Contains("确定了一种可能的解决方法,这样做可能会解决你的问题:") Then AppendReason(CrashReason.Fabric报错并给出解决方案, Join(RegexSearch(If(RegexSeek(LogMc, "(?<=确定了一种可能的解决方法,这样做可能会解决你的问题:\n)((\t)+ - [^\n]+\n)+"), ""), "(?<=(\t)+)[^\n]+"), vbLf))
             If LogMc.Contains("确定了一种可能的解决方法,这样做可能会解决你的问题:") Then AppendReason(CrashReason.Fabric报错并给出解决方案, Join(RegexSearch(If(RegexSeek(LogMc, "(?<=确定了一种可能的解决方法,这样做可能会解决你的问题:\n)((\t)+ - [^\n]+\n)+"), ""), "(?<=(\t)+)[^\n]+"), vbLf))
-        End If
-
-    End Sub
-    ''' <summary>
-    ''' 进行精准日志匹配。匹配优先级低于堆栈分析的崩溃。
-    ''' </summary>
-    Private Sub AnalyzeCrit3()
-
-        '游戏日志分析
-        If LogMc IsNot Nothing Then
             'Mixin 崩溃
             'Mixin 崩溃
             If LogMc.Contains("Mixin prepare failed ") OrElse LogMc.Contains("Mixin apply failed ") OrElse LogMc.Contains("MixinApplyError") OrElse
             If LogMc.Contains("Mixin prepare failed ") OrElse LogMc.Contains("Mixin apply failed ") OrElse LogMc.Contains("MixinApplyError") OrElse
-               LogMc.Contains("mixin.injection.throwables.") OrElse LogMc.Contains(".mixins.json] FAILED during )") Then
+               LogMc.Contains("mixin.injection.throwables.") OrElse LogMc.Contains(".json] FAILED during )") Then
                 Dim ModId As String = RegexSeek(LogMc, "(?<=in )[^./ ]+(?=.mixins.json.+failed injection check)")
                 Dim ModId As String = RegexSeek(LogMc, "(?<=in )[^./ ]+(?=.mixins.json.+failed injection check)")
                 If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<=in mixins.)[^./ ]+(?=.json.+failed injection check)")
                 If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<=in mixins.)[^./ ]+(?=.json.+failed injection check)")
                 If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<= failed .+ in )[^./ ]+(?=.mixins.json)")
                 If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<= failed .+ in )[^./ ]+(?=.mixins.json)")
@@ -612,7 +614,28 @@ Done:
                 If ModId Is Nothing Then ModId = RegexSeek(LogMc, "[^./ \]]+(?=.mixins.json)")
                 If ModId Is Nothing Then ModId = RegexSeek(LogMc, "[^./ \]]+(?=.mixins.json)")
                 If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<=mixins.)[^./ \]]+(?=.json)")
                 If ModId Is Nothing Then ModId = RegexSeek(LogMc, "(?<=mixins.)[^./ \]]+(?=.json)")
                 AppendReason(CrashReason.ModMixin失败, TryAnalyzeModName(If(ModId, "").TrimEnd((vbCrLf & " ").ToCharArray)))
                 AppendReason(CrashReason.ModMixin失败, TryAnalyzeModName(If(ModId, "").TrimEnd((vbCrLf & " ").ToCharArray)))
+            Else
+                '在 #3104 的情况下,这一句导致 OptiFabric 的 Mixin 失败错判为 Fabric Loader 加载失败
+                If LogMc.Contains("due to errors, provided by ") Then AppendReason(CrashReason.确定Mod导致游戏崩溃, TryAnalyzeModName(If(RegexSeek(LogMc, "(?<=due to errors, provided by ')[^']+"), "").TrimEnd((vbCrLf & " ").ToCharArray)))
+            End If
+        End If
+
+        '崩溃报告分析
+        If LogCrash IsNot Nothing Then
+            If LogCrash.Contains("Suspected Mod") Then
+                Dim Suspects = RegexSearch(LogCrash.Between("Suspected Mod", "Stacktrace"), "(?<=\n\t[^(\t]+\()[^)\n]+")
+                If Suspects.Any Then AppendReason(CrashReason.怀疑Mod导致游戏崩溃, TryAnalyzeModName(Suspects))
             End If
             End If
+        End If
+
+    End Sub
+    ''' <summary>
+    ''' 进行精准日志匹配。匹配优先级低于堆栈分析的崩溃。
+    ''' </summary>
+    Private Sub AnalyzeCrit3()
+
+        '游戏日志分析
+        If LogMc IsNot Nothing Then
             '极短的程序输出
             '极短的程序输出
             If Not (LogMc.Contains("at net.") OrElse LogMc.Contains("INFO]")) AndAlso LogHs Is Nothing AndAlso LogCrash Is Nothing AndAlso LogMc.Length < 100 Then
             If Not (LogMc.Contains("at net.") OrElse LogMc.Contains("INFO]")) AndAlso LogHs Is Nothing AndAlso LogCrash Is Nothing AndAlso LogMc.Length < 100 Then
                 AppendReason(CrashReason.极短的程序输出, LogMc)
                 AppendReason(CrashReason.极短的程序输出, LogMc)
@@ -633,9 +656,13 @@ Done:
     ''' 从堆栈中提取 Mod ID 关键字。若失败则返回空列表。
     ''' 从堆栈中提取 Mod ID 关键字。若失败则返回空列表。
     ''' </summary>
     ''' </summary>
     Private Function AnalyzeStackKeyword(ErrorStack As String) As List(Of String)
     Private Function AnalyzeStackKeyword(ErrorStack As String) As List(Of String)
+        ErrorStack = vbLf & If(ErrorStack, "") & vbLf
 
 
         '进行正则匹配
         '进行正则匹配
-        Dim StackSearchResults As List(Of String) = RegexSearch(If(ErrorStack, "") & vbCrLf, "(?<=\n[^{]+)[a-zA-Z]+\w+\.[a-zA-Z]+[\w\.]+(?=\.[\w\.$]+\.)")
+        Dim StackSearchResults As New List(Of String)
+        StackSearchResults.AddRange(RegexSearch(ErrorStack, "(?<=\n[^{]+)[a-zA-Z_]+\w+\.[a-zA-Z_]+[\w\.]+(?=\.[\w\.$]+\.)"))
+        StackSearchResults.AddRange(RegexSearch(ErrorStack, "(?<=at [^(]+?\.\w+\$\w+\$)[\w\$]+?(?=\$\w+\()").Select(Function(s) s.Replace("$", "."))) 'Mixin 堆栈:xxx.xxx.xxxx$xxxx$xxx
+        StackSearchResults = StackSearchResults.Distinct.ToList
 
 
         '检查堆栈开头
         '检查堆栈开头
         Dim PossibleStacks As New List(Of String)
         Dim PossibleStacks As New List(Of String)
@@ -648,7 +675,7 @@ Done:
                 "MojangTricksIntelDriversForPerformance_javaw"}
                 "MojangTricksIntelDriversForPerformance_javaw"}
                 If Stack.StartsWithF(IgnoreStack) Then GoTo NextStack
                 If Stack.StartsWithF(IgnoreStack) Then GoTo NextStack
             Next
             Next
-            PossibleStacks.Add(Stack.Trim) '.Split("$").First)
+            PossibleStacks.Add(Stack.Trim)
 NextStack:
 NextStack:
         Next
         Next
         PossibleStacks = PossibleStacks.Distinct.ToList
         PossibleStacks = PossibleStacks.Distinct.ToList
@@ -665,20 +692,20 @@ NextStack:
             For i = 0 To Math.Min(3, Splited.Count - 1) '最多取前 4 节
             For i = 0 To Math.Min(3, Splited.Count - 1) '最多取前 4 节
                 Dim Word As String = Splited(i)
                 Dim Word As String = Splited(i)
                 If Word.Length <= 2 OrElse Word.StartsWithF("func_") Then Continue For
                 If Word.Length <= 2 OrElse Word.StartsWithF("func_") Then Continue For
-                If {"com", "org", "net", "asm", "fml", "mod", "jar", "sun", "lib", "map", "gui", "dev", "nio", "api", "dsi", "top",
-                    "core", "init", "mods", "main", "file", "game", "load", "read", "done", "util", "tile", "item", "base", "oshi",
-                    "forge", "setup", "block", "model", "mixin", "event", "unimi", "netty",
-                    "gitlab", "common", "server", "config", "loader", "launch", "entity", "assist", "client", "plugin", "modapi", "mojang", "shader", "events", "github", "recipe",
-                    "preinit", "preload", "machine", "reflect", "channel", "general", "handler", "content", "systems", "modules",
-                    "fastutil", "optifine", "internal", "platform", "override",
-                    "listeners", "scheduler", "minecraft", "transformers", "universal", "multipart", "minecraftforge", "blockentity"
+                If {"com", "org", "net", "asm", "fml", "mod", "jar", "sun", "lib", "map", "gui", "dev", "nio", "api", "dsi", "top", "mcp",
+                    "core", "init", "mods", "main", "file", "game", "load", "read", "done", "util", "tile", "item", "base", "oshi", "impl", "data", "pool", "task",
+                    "forge", "setup", "block", "model", "mixin", "event", "unimi", "netty", "world",
+                    "gitlab", "common", "server", "config", "loader", "launch", "entity", "assist", "client", "plugin", "modapi", "mojang", "shader", "events", "github", "recipe", "render", "packet", "events",
+                    "preinit", "preload", "machine", "reflect", "channel", "general", "handler", "content", "systems", "modules", "service",
+                    "fastutil", "optifine", "internal", "platform", "override", "fabricmc",
+                    "injection", "listeners", "scheduler", "minecraft", "transformer", "transformers", "universal", "multipart", "minecraftforge", "blockentity", "spongepowered", "electronwill"
                    }.Contains(Word.ToLower) Then Continue For
                    }.Contains(Word.ToLower) Then Continue For
                 PossibleWords.Add(Word.Trim)
                 PossibleWords.Add(Word.Trim)
             Next
             Next
         Next
         Next
         PossibleWords = PossibleWords.Distinct.ToList
         PossibleWords = PossibleWords.Distinct.ToList
         Log("[Crash] 从堆栈信息中找到 " & PossibleWords.Count & " 个可能的 Mod ID 关键词")
         Log("[Crash] 从堆栈信息中找到 " & PossibleWords.Count & " 个可能的 Mod ID 关键词")
-        If PossibleWords.Count > 0 Then Log("[Crash]  - " & Join(PossibleWords, ", "))
+        If PossibleWords.Any Then Log("[Crash]  - " & Join(PossibleWords, ", "))
         If PossibleWords.Count > 10 Then
         If PossibleWords.Count > 10 Then
             Log("[Crash] 关键词过多,考虑匹配出错,不纳入考虑")
             Log("[Crash] 关键词过多,考虑匹配出错,不纳入考虑")
             Return New List(Of String)
             Return New List(Of String)
@@ -711,7 +738,7 @@ NextStack:
                 Details = Details.Replace("Fabric Mods", "¨")
                 Details = Details.Replace("Fabric Mods", "¨")
                 Log("[Crash] 崩溃报告中检测到 Fabric Mod 信息格式")
                 Log("[Crash] 崩溃报告中检测到 Fabric Mod 信息格式")
             End If
             End If
-            Details = Details.Split("¨").Last
+            Details = Details.After("¨")
 
 
             '[Forge] 获取所有包含 .jar 的行
             '[Forge] 获取所有包含 .jar 的行
             '[Fabric] 获取所有包含 Mod 信息的行
             '[Fabric] 获取所有包含 Mod 信息的行
@@ -794,13 +821,20 @@ NextStack:
         End If
         End If
     End Function
     End Function
     ''' <summary>
     ''' <summary>
-    ''' 尝试获取 Mod 名称,若失败则返回原关键字。
+    ''' 尝试从关键字获取 Mod 名称,若失败则返回原关键字。
     ''' </summary>
     ''' </summary>
-    Private Function TryAnalyzeModName(Keywords As String) As List(Of String)
-        Dim RawList As New List(Of String) From {If(Keywords, "")}
-        If String.IsNullOrEmpty(Keywords) Then Return RawList
+    Private Function TryAnalyzeModName(Keyword As String) As List(Of String)
+        Dim RawList As New List(Of String) From {If(Keyword, "")}
+        If String.IsNullOrEmpty(Keyword) Then Return RawList
         Return If(AnalyzeModName(RawList), RawList)
         Return If(AnalyzeModName(RawList), RawList)
     End Function
     End Function
+    ''' <summary>
+    ''' 尝试从关键字获取 Mod 名称,若失败则返回原关键字。
+    ''' </summary>
+    Private Function TryAnalyzeModName(Keywords As List(Of String)) As List(Of String)
+        If Not Keywords.Any Then Return Keywords
+        Return If(AnalyzeModName(Keywords), Keywords)
+    End Function
 
 
     '4:根据原因输出信息
     '4:根据原因输出信息
     Private OutputFiles As New List(Of String)
     Private OutputFiles As New List(Of String)
@@ -813,7 +847,7 @@ NextStack:
         Dim ShowLog As Action =
         Dim ShowLog As Action =
             Sub()
             Sub()
                 If File.Exists(DirectFile.Value.Key) Then
                 If File.Exists(DirectFile.Value.Key) Then
-                    ShellOnly(DirectFile.Value.Key)
+                    ShellOnly("notepad", DirectFile.Value.Key)
                 Else
                 Else
                     Dim FilePath As String = PathTemp & "Crash.txt"
                     Dim FilePath As String = PathTemp & "Crash.txt"
                     WriteFile(FilePath, Join(DirectFile.Value.Value, vbCrLf))
                     WriteFile(FilePath, Join(DirectFile.Value.Value, vbCrLf))
@@ -847,7 +881,7 @@ NextStack:
                                 FileName = "游戏崩溃前的输出.txt"
                                 FileName = "游戏崩溃前的输出.txt"
                         End Select
                         End Select
                         If File.Exists(OutputFile) Then
                         If File.Exists(OutputFile) Then
-                            WriteFile(TempFolder & "Report\" & FileName, SecretFilter(ReadFile(OutputFile)))
+                            WriteFile(TempFolder & "Report\" & FileName, SecretFilter(ReadFile(OutputFile), If(FileName = "启动脚本.bat", "F", "*")))
                         End If
                         End If
                     Next
                     Next
                     '导出报告
                     '导出报告
@@ -903,46 +937,46 @@ NextStack:
                         Results.Add("你正在使用 32 位的操作系统,这会导致 Minecraft 无法使用所需的内存,进而造成崩溃。\n\n你或许只能重装 64 位的操作系统来解决此问题。\n如果你的电脑内存在 2GB 以内,那或许只能换台电脑了……\h")
                         Results.Add("你正在使用 32 位的操作系统,这会导致 Minecraft 无法使用所需的内存,进而造成崩溃。\n\n你或许只能重装 64 位的操作系统来解决此问题。\n如果你的电脑内存在 2GB 以内,那或许只能换台电脑了……\h")
                     End If
                     End If
                 Case CrashReason.Mod缺少前置或MC版本错误
                 Case CrashReason.Mod缺少前置或MC版本错误
-                    If Additional.Count > 0 Then
+                    If Additional.Any Then
                         Results.Add("由于未满足 Mod 的依赖项,导致游戏退出。\n未满足的依赖项:\n - " & Join(Additional, "\n - ") & "\n\n请根据上述信息进行对应处理,如果看不懂英文可以使用翻译软件。")
                         Results.Add("由于未满足 Mod 的依赖项,导致游戏退出。\n未满足的依赖项:\n - " & Join(Additional, "\n - ") & "\n\n请根据上述信息进行对应处理,如果看不懂英文可以使用翻译软件。")
                     Else
                     Else
                         Results.Add("由于未满足 Mod 的依赖项,导致游戏退出。\n请根据错误报告中的日志信息进行对应处理,如果看不懂英文可以使用翻译软件。\h")
                         Results.Add("由于未满足 Mod 的依赖项,导致游戏退出。\n请根据错误报告中的日志信息进行对应处理,如果看不懂英文可以使用翻译软件。\h")
                     End If
                     End If
                 Case CrashReason.堆栈分析发现关键字
                 Case CrashReason.堆栈分析发现关键字
                     If Additional.Count = 1 Then
                     If Additional.Count = 1 Then
-                        Results.Add("你的游戏遇到了一些问题,这可能是某些 Mod 所引起的,PCL 找到了一个可疑的关键词:" & Additional.First & "。\n\n如果你知道它对应的 Mod,那么有可能就是它引起的错误,你也可以查看错误报告获取详情。\h")
+                        Results.Add("你的游戏遇到了一些问题,PCL 为此找到了一个可疑的关键词:" & Additional.First & "。\n\n如果你知道某个关键词对应的 Mod,那么有可能就是它引起的错误,你也可以查看错误报告获取详情。\h")
                     Else
                     Else
-                        Results.Add("你的游戏遇到了一些问题,这可能是某些 Mod 所引起的,PCL 找到了以下可疑的关键词:\n - " & Join(Additional, ", ") & "\n\n如果你知道这些关键词对应的 Mod,那么有可能就是它引起的错误,你也可以查看错误报告获取详情。\h")
+                        Results.Add("你的游戏遇到了一些问题,PCL 为此找到了以下可疑的关键词:\n - " & Join(Additional, ", ") & "\n\n如果你知道某个关键词对应的 Mod,那么有可能就是它引起的错误,你也可以查看错误报告获取详情。\h")
                     End If
                     End If
-                Case CrashReason.堆栈分析发现Mod名称
+                Case CrashReason.堆栈分析发现Mod名称, CrashReason.怀疑Mod导致游戏崩溃
                     If Additional.Count = 1 Then
                     If Additional.Count = 1 Then
-                        Results.Add("名为 " & Additional.First & " 的 Mod 可能导致了游戏出错。\n\e\h")
+                        Results.Add("PCL 怀疑名为 " & Additional.First & " 的 Mod 导致了游戏出错,但不能完全确定。\n你可以尝试禁用此 Mod,然后观察游戏是否还会崩溃。\n\e\h")
                     Else
                     Else
-                        Results.Add("可能是以下 Mod 导致了游戏出错:\n - " & Join(Additional, "\n - ") & "\n\e\h")
+                        Results.Add("PCL 怀疑以下 Mod 导致了游戏出错,但不能完全确定:\n - " & Join(Additional, "\n - ") & "\n\n你可以尝试依次禁用上述 Mod,然后观察游戏是否还会崩溃。\n\e\h")
                     End If
                     End If
                 Case CrashReason.确定Mod导致游戏崩溃
                 Case CrashReason.确定Mod导致游戏崩溃
                     If Additional.Count = 1 Then
                     If Additional.Count = 1 Then
-                        Results.Add("名称或 ID 为 " & Additional.First & " 的 Mod 导致了游戏出错。\n\e\h")
+                        Results.Add("名为 " & Additional.First & " 的 Mod 导致了游戏出错。\n你可以尝试禁用此 Mod,然后观察游戏是否还会崩溃。\n\e\h")
                     Else
                     Else
-                        Results.Add("以下 Mod 导致了游戏出错:\n - " & Join(Additional, "\n - ") & "\n\e\h")
+                        Results.Add("以下 Mod 导致了游戏出错:\n - " & Join(Additional, "\n - ") & "\n\n你可以尝试依次禁用上述 Mod,然后观察游戏是否还会崩溃。\n\e\h")
                     End If
                     End If
                 Case CrashReason.ModMixin失败
                 Case CrashReason.ModMixin失败
                     If Additional.Count = 1 Then
                     If Additional.Count = 1 Then
-                        Results.Add("名称或 ID 为 " & Additional.First & " 的 Mod 注入失败,导致游戏出错。\n这一般代表着该 Mod 存在 Bug,或与当前环境不兼容。\n\e\h")
+                        Results.Add("名为 " & Additional.First & " 的 Mod 注入失败,导致游戏出错。\n这一般代表着它与其他 Mod 或当前环境不兼容,或是它存在 Bug。\n你可以尝试禁用此 Mod,然后观察游戏是否还会崩溃。\n\e\h")
                     Else
                     Else
-                        Results.Add("以下 Mod 导致了游戏出错:\n - " & Join(Additional, "\n - ") & "\n这一般代表着这些 Mod 存在 Bug,或与当前环境不兼容。\n\e\h")
+                        Results.Add("以下 Mod 导致了游戏出错:\n - " & Join(Additional, "\n - ") & "\n这一般代表着它们与其他 Mod 或当前环境不兼容,或是它存在 Bug。\n你可以尝试依次禁用上述 Mod,然后观察游戏是否还会崩溃。\n\e\h")
                     End If
                     End If
                 Case CrashReason.Mod配置文件导致游戏崩溃
                 Case CrashReason.Mod配置文件导致游戏崩溃
                     If Additional(1) Is Nothing Then
                     If Additional(1) Is Nothing Then
-                        Results.Add("名称或 ID 为 " & Additional.First & " 的 Mod 导致了游戏出错。\n\e\h")
+                        Results.Add("名为 " & Additional.First & " 的 Mod 导致了游戏出错。\n\e\h")
                     Else
                     Else
-                        Results.Add("名称或 ID 为 " & Additional.First & " 的 Mod 导致了游戏出错:\n其配置文件 " & Additional(1) & " 存在异常,无法读取。")
+                        Results.Add("名为 " & Additional.First & " 的 Mod 导致了游戏出错:\n其配置文件 " & Additional(1) & " 存在异常,无法读取。")
                     End If
                     End If
                 Case CrashReason.Mod初始化失败
                 Case CrashReason.Mod初始化失败
                     If Additional.Count = 1 Then
                     If Additional.Count = 1 Then
-                        Results.Add("名为 " & Additional.First & " 的 Mod 初始化失败,导致游戏无法继续加载。\n\e\h")
+                        Results.Add("名为 " & Additional.First & " 的 Mod 初始化失败,导致游戏无法继续加载。\n你可以尝试禁用此 Mod,然后观察游戏是否还会崩溃。\n\e\h")
                     Else
                     Else
-                        Results.Add("以下 Mod 初始化失败,导致游戏无法继续加载:\n - " & Join(Additional, "\n - ") & "\n\e\h")
+                        Results.Add("以下 Mod 初始化失败,导致游戏无法继续加载:\n - " & Join(Additional, "\n - ") & "\n\n你可以尝试依次禁用上述 Mod,然后观察游戏是否还会崩溃。\n\e\h")
                     End If
                     End If
                 Case CrashReason.特定方块导致崩溃
                 Case CrashReason.特定方块导致崩溃
                     If Additional.Count = 1 Then
                     If Additional.Count = 1 Then
@@ -1027,8 +1061,6 @@ NextStack:
             End Select
             End Select
         Next
         Next
 
 
-        '整理多个原因
-        IsHandAnalyze = False
         Return Join(Results, "\n\n此外,").
         Return Join(Results, "\n\n此外,").
                     Replace("\n", vbCrLf).
                     Replace("\n", vbCrLf).
                     Replace("\h", "").
                     Replace("\h", "").
@@ -1038,7 +1070,7 @@ NextStack:
                 If(Not Results.Any(Function(r) r.EndsWithF("\h")) OrElse IsHandAnalyze, "",
                 If(Not Results.Any(Function(r) r.EndsWithF("\h")) OrElse IsHandAnalyze, "",
                     vbCrLf & "如果要寻求帮助,请向他人发送错误报告文件,而不是发送这个窗口的截图。" &
                     vbCrLf & "如果要寻求帮助,请向他人发送错误报告文件,而不是发送这个窗口的截图。" &
                     If(If(PageSetupSystem.IsLauncherNewest(), True), "",
                     If(If(PageSetupSystem.IsLauncherNewest(), True), "",
-                    vbCrLf & vbCrLf & "此外,你正在使用老版本 PCL,更新到最新版可能会修复这个问题。" & vbCrLf & "你可以点击 设置 → 启动器 → 检查更新 来更新 PCL。"))
+                    vbCrLf & vbCrLf & "此外,你正在使用老版本 PCL,更新 PCL 或许也能解决这个问题。" & vbCrLf & "你可以点击 设置 → 启动器 → 检查更新 来更新 PCL。"))
     End Function
     End Function
 
 
 End Class
 End Class

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

@@ -98,6 +98,11 @@
                     TempAddress = PathTemp & "Cache\" & BackAssetsFile.LocalName
                     TempAddress = PathTemp & "Cache\" & BackAssetsFile.LocalName
                     BackAssetsFile.LocalPath = TempAddress
                     BackAssetsFile.LocalPath = TempAddress
                     Task.Output = New List(Of NetFile) From {BackAssetsFile}
                     Task.Output = New List(Of NetFile) From {BackAssetsFile}
+                    '检查是否需要更新:每天只更新一次
+                    If File.Exists(RealAddress) AndAlso Math.Abs((File.GetLastWriteTime(RealAddress).Date - Now.Date).TotalDays) < 1 Then
+                        Log("[Download] 无需更新资源文件索引")
+                        Task.Abort()
+                    End If
                 End Sub))
                 End Sub))
                 LoadersAssetsUpdate.Add(New LoaderDownload("后台下载资源文件索引", New List(Of NetFile)))
                 LoadersAssetsUpdate.Add(New LoaderDownload("后台下载资源文件索引", New List(Of NetFile)))
                 LoadersAssetsUpdate.Add(New LoaderTask(Of List(Of NetFile), String)("后台复制资源文件索引",
                 LoadersAssetsUpdate.Add(New LoaderTask(Of List(Of NetFile), String)("后台复制资源文件索引",
@@ -106,7 +111,7 @@
                     McLaunchLog("后台更新资源文件索引成功:" & TempAddress)
                     McLaunchLog("后台更新资源文件索引成功:" & TempAddress)
                 End Sub))
                 End Sub))
                 Dim Updater As New LoaderCombo(Of String)("后台更新资源文件索引", LoadersAssetsUpdate)
                 Dim Updater As New LoaderCombo(Of String)("后台更新资源文件索引", LoadersAssetsUpdate)
-                Log("[Download] 开始后台更新资源文件索引")
+                Log("[Download] 开始后台检查资源文件索引")
                 Updater.Start()
                 Updater.Start()
             End If
             End If
             '获取资源文件地址
             '获取资源文件地址

+ 58 - 39
Plain Craft Launcher 2/Modules/Minecraft/ModJava.vb

@@ -1,5 +1,5 @@
 Public Module ModJava
 Public Module ModJava
-    Public JavaListCacheVersion As Integer = 5
+    Public JavaListCacheVersion As Integer = 6
 
 
     ''' <summary>
     ''' <summary>
     ''' 目前所有可用的 Java。
     ''' 目前所有可用的 Java。
@@ -79,7 +79,7 @@
         Public Overrides Function ToString() As String
         Public Overrides Function ToString() As String
             Dim VersionString = Version.ToString
             Dim VersionString = Version.ToString
             If VersionString.StartsWithF("1.") Then VersionString = Mid(VersionString, 3)
             If VersionString.StartsWithF("1.") Then VersionString = Mid(VersionString, 3)
-            Return If(IsJre, "JRE ", "JDK ") & VersionCode & " (" & VersionString & ")," & If(Is64Bit, "64", "32") & " 位" & If(IsUserImport, ",手动导入", "") & ":" & PathFolder
+            Return If(IsJre, "JRE ", "JDK ") & VersionCode & " (" & VersionString & ")" & If(Is64Bit, "", ",32 位") & If(IsUserImport, ",手动导入", "") & ":" & PathFolder
         End Function
         End Function
 
 
         '构造
         '构造
@@ -131,7 +131,9 @@
                     Version = New Version(1, Version.Major, Version.Build, Version.Revision)
                     Version = New Version(1, Version.Major, Version.Build, Version.Revision)
                 End If
                 End If
                 Is64Bit = Output.Contains("64-bit")
                 Is64Bit = Output.Contains("64-bit")
-                If Version.Minor <= 4 OrElse Version.Minor >= 25 Then Throw New Exception("分析详细信息失败,获取的版本为 " & Version.ToString)
+                If Version.Minor <= 4 OrElse Version.Minor >= 25 Then Throw New ApplicationException("分析详细信息失败,获取的版本为 " & Version.ToString)
+                '基于 #3649,在 64 位系统上禁用 32 位 Java
+                If Not Is64Bit AndAlso Not Is32BitSystem Then Throw New Exception("该 Java 为 32 位版本,请安装 64 位的 Java")
                 '基于 #2249 发现 JRE 17 似乎也导致了 Forge 安装失败,干脆禁用更多版本的 JRE
                 '基于 #2249 发现 JRE 17 似乎也导致了 Forge 安装失败,干脆禁用更多版本的 JRE
                 If IsJre AndAlso VersionCode >= 16 Then Throw New Exception("由于高版本 JRE 对游戏的兼容性很差,因此不再允许使用。你可以使用对应版本的 JDK,而非 JRE!")
                 If IsJre AndAlso VersionCode >= 16 Then Throw New Exception("由于高版本 JRE 对游戏的兼容性很差,因此不再允许使用。你可以使用对应版本的 JDK,而非 JRE!")
             Catch ex As ApplicationException
             Catch ex As ApplicationException
@@ -197,10 +199,15 @@
             Setup.Set("LaunchArgumentJavaAll", "[]")
             Setup.Set("LaunchArgumentJavaAll", "[]")
         End Try
         End Try
     End Sub
     End Sub
+
+    ''' <summary>
+    ''' 防止多个需要 Java 的部分同时要求下载 Java(#3797)。
+    ''' </summary>
+    Public JavaLock As New Object
     ''' <summary>
     ''' <summary>
     ''' 根据要求返回最适合的 Java,若找不到则返回 Nothing。
     ''' 根据要求返回最适合的 Java,若找不到则返回 Nothing。
     ''' 最小与最大版本在与输入相同时也会通过。
     ''' 最小与最大版本在与输入相同时也会通过。
-    ''' 必须在工作线程调用。
+    ''' 必须在工作线程调用,且必须包括 SyncLock JavaLock。
     ''' </summary>
     ''' </summary>
     Public Function JavaSelect(CancelException As String, Optional MinVersion As Version = Nothing, Optional MaxVersion As Version = Nothing,
     Public Function JavaSelect(CancelException As String, Optional MinVersion As Version = Nothing, Optional MaxVersion As Version = Nothing,
                                Optional RelatedVersion As McVersion = Nothing) As JavaEntry
                                Optional RelatedVersion As McVersion = Nothing) As JavaEntry
@@ -220,7 +227,7 @@
             Next
             Next
 
 
             '检查特定的 Java
             '检查特定的 Java
-            If TargetJavaList.Count > 0 Then
+            If TargetJavaList.Any Then
                 TargetJavaList = JavaCheckList(TargetJavaList)
                 TargetJavaList = JavaCheckList(TargetJavaList)
                 Log("[Java] 检查后找到 " & TargetJavaList.Count & " 个特定路径下的 Java:")
                 Log("[Java] 检查后找到 " & TargetJavaList.Count & " 个特定路径下的 Java:")
                 For Each Java In TargetJavaList
                 For Each Java In TargetJavaList
@@ -460,8 +467,8 @@ NoUserJava:
         If Not Left.IsJre AndAlso Right.IsJre Then Return False
         If Not Left.IsJre AndAlso Right.IsJre Then Return False
         '4. Java 大版本
         '4. Java 大版本
         If Left.VersionCode <> Right.VersionCode Then
         If Left.VersionCode <> Right.VersionCode Then
-            '                             Java  7   8   9  10  11  12 13 14 15  16  17  18  19  20  21...
-            Dim Weight = {0, 1, 2, 3, 4, 5, 6, 14, 30, 10, 12, 15, 13, 9, 8, 7, 11, 31, 29, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28}
+            '                             Java  7   8   9  10  11  12 13 14 15  16  17  18  19  20  21  22  23...
+            Dim Weight = {0, 1, 2, 3, 4, 5, 6, 14, 30, 10, 12, 15, 13, 9, 8, 7, 11, 31, 29, 16, 17, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18}
             Return Weight.ElementAtOrDefault(Left.VersionCode) >= Weight.ElementAtOrDefault(Right.VersionCode)
             Return Weight.ElementAtOrDefault(Left.VersionCode) >= Weight.ElementAtOrDefault(Right.VersionCode)
         End If
         End If
         '5. 最次级版本号更接近 51
         '5. 最次级版本号更接近 51
@@ -534,7 +541,7 @@ NoUserJava:
                 Log("[Java] 位于 " & Folder & " 的 Java 不含符号链接")
                 Log("[Java] 位于 " & Folder & " 的 Java 不含符号链接")
                 JavaWithoutReparse.Add(Pair.Key, Pair.Value)
                 JavaWithoutReparse.Add(Pair.Key, Pair.Value)
             Next
             Next
-            If JavaWithoutReparse.Count > 0 Then JavaPreList = JavaWithoutReparse
+            If JavaWithoutReparse.Any Then JavaPreList = JavaWithoutReparse
 
 
             '若不全为特殊引用,则清除特殊引用的地址
             '若不全为特殊引用,则清除特殊引用的地址
             Dim JavaWithoutInherit As New Dictionary(Of String, Boolean)
             Dim JavaWithoutInherit As New Dictionary(Of String, Boolean)
@@ -546,7 +553,7 @@ NoUserJava:
                     JavaWithoutInherit.Add(Pair.Key, Pair.Value)
                     JavaWithoutInherit.Add(Pair.Key, Pair.Value)
                 End If
                 End If
             Next
             Next
-            If JavaWithoutInherit.Count > 0 Then JavaPreList = JavaWithoutInherit
+            If JavaWithoutInherit.Any Then JavaPreList = JavaWithoutInherit
 
 
 #End Region
 #End Region
 
 
@@ -567,7 +574,7 @@ NoUserJava:
 
 
             '确保可用并获取详细信息,转入正式列表
             '确保可用并获取详细信息,转入正式列表
             Dim NewJavaList As New List(Of JavaEntry)
             Dim NewJavaList As New List(Of JavaEntry)
-            For Each Entry In Distinct(JavaPreList.ToList, Function(a, b) a.Key.ToLower = b.Key.ToLower) '#794
+            For Each Entry In JavaPreList.Distinct(Function(a, b) a.Key.ToLower = b.Key.ToLower) '#794
                 NewJavaList.Add(New JavaEntry(Entry.Key, Entry.Value))
                 NewJavaList.Add(New JavaEntry(Entry.Key, Entry.Value))
             Next
             Next
             NewJavaList = Sort(JavaCheckList(NewJavaList), AddressOf JavaSorter)
             NewJavaList = Sort(JavaCheckList(NewJavaList), AddressOf JavaSorter)
@@ -601,22 +608,23 @@ NoUserJava:
         '启动检查线程
         '启动检查线程
         Dim CheckThreads As New List(Of Thread)
         Dim CheckThreads As New List(Of Thread)
         For Each Entry In JavaEntries
         For Each Entry In JavaEntries
-            Dim CheckThread As New Thread(Sub()
-                                              Try
-                                                  Entry.Check()
-                                                  If ModeDebug Then Log("[Java]  - " & Entry.ToString)
-                                                  SyncLock ListLock
-                                                      JavaCheckList.Add(Entry)
-                                                  End SyncLock
-                                              Catch ex As ThreadInterruptedException
-                                              Catch ex As Exception
-                                                  If Entry.IsUserImport Then
-                                                      Log(ex, "位于 " & Entry.PathFolder & " 的 Java 存在异常,将被自动移除", LogLevel.Hint)
-                                                  Else
-                                                      Log(ex, "位于 " & Entry.PathFolder & " 的 Java 存在异常")
-                                                  End If
-                                              End Try
-                                          End Sub)
+            Dim CheckThread As New Thread(
+            Sub()
+                Try
+                    Entry.Check()
+                    If ModeDebug Then Log("[Java]  - " & Entry.ToString)
+                    SyncLock ListLock
+                        JavaCheckList.Add(Entry)
+                    End SyncLock
+                Catch ex As ThreadInterruptedException
+                Catch ex As Exception
+                    If Entry.IsUserImport Then
+                        Log(ex, "位于 " & Entry.PathFolder & " 的 Java 存在异常,将被自动移除", LogLevel.Hint)
+                    Else
+                        Log(ex, "位于 " & Entry.PathFolder & " 的 Java 存在异常")
+                    End If
+                End Try
+            End Sub)
             CheckThreads.Add(CheckThread)
             CheckThreads.Add(CheckThread)
             CheckThread.Start()
             CheckThread.Start()
         Next
         Next
@@ -719,12 +727,25 @@ Wait:
     ''' 获取下载 Java 8/14/17/21 的加载器。需要开启 IsForceRestart 以正常刷新 Java 列表。
     ''' 获取下载 Java 8/14/17/21 的加载器。需要开启 IsForceRestart 以正常刷新 Java 列表。
     ''' </summary>
     ''' </summary>
     Public Function JavaFixLoaders(Version As Integer) As LoaderCombo(Of Integer)
     Public Function JavaFixLoaders(Version As Integer) As LoaderCombo(Of Integer)
-        Return New LoaderCombo(Of Integer)($"下载 Java {Version}", {
+        Dim JavaDownloadLoader As New LoaderDownload("下载 Java 文件", New List(Of NetFile)) With {.ProgressWeight = 10}
+        Dim Loader = New LoaderCombo(Of Integer)($"下载 Java {Version}", {
             New LoaderTask(Of Integer, List(Of NetFile))("获取 Java 下载信息", AddressOf JavaFileList) With {.ProgressWeight = 2},
             New LoaderTask(Of Integer, List(Of NetFile))("获取 Java 下载信息", AddressOf JavaFileList) With {.ProgressWeight = 2},
-            New LoaderDownload("下载 Java 文件", New List(Of NetFile)) With {.ProgressWeight = 10},
+            JavaDownloadLoader,
             JavaSearchLoader
             JavaSearchLoader
         })
         })
+        AddHandler JavaDownloadLoader.OnStateChangedThread,
+        Sub(Raw As LoaderBase, NewState As LoadState, OldState As LoadState)
+            If (NewState = LoadState.Failed OrElse NewState = LoadState.Aborted) AndAlso LastJavaBaseDir IsNot Nothing Then
+                Log($"[Java] 由于下载未完成,清理未下载完成的 Java 文件:{LastJavaBaseDir}", LogLevel.Debug)
+                DeleteDirectory(LastJavaBaseDir)
+            ElseIf NewState = LoadState.Finished Then
+                LastJavaBaseDir = Nothing
+            End If
+        End Sub
+        JavaDownloadLoader.HasOnStateChangedThread = True
+        Return Loader
     End Function
     End Function
+    Private LastJavaBaseDir As String = Nothing '用于在下载中断或失败时删除未完成下载的 Java 文件夹,防止残留只下了一半但 -version 能跑的 Java
     Private Sub JavaFileList(Loader As LoaderTask(Of Integer, List(Of NetFile)))
     Private Sub JavaFileList(Loader As LoaderTask(Of Integer, List(Of NetFile)))
         Log("[Java] 开始获取 Java 下载信息")
         Log("[Java] 开始获取 Java 下载信息")
         Dim IndexFileStr As String = NetGetCodeByDownload(
         Dim IndexFileStr As String = NetGetCodeByDownload(
@@ -739,25 +760,23 @@ Wait:
         Dim Address As String = TargetEntry.Value("manifest")("url")
         Dim Address As String = TargetEntry.Value("manifest")("url")
         Log($"[Java] 准备下载 Java {TargetEntry.Value("version")("name")}({TargetEntry.Key}):{Address}")
         Log($"[Java] 准备下载 Java {TargetEntry.Value("version")("name")}({TargetEntry.Key}):{Address}")
         '获取文件列表
         '获取文件列表
-        Dim ListFileStr As String = NetGetCodeByDownload(
-            {Address.Replace("piston-meta.mojang.com", "bmclapi2.bangbang93.com"),
-             Address},
-        IsJson:=True)
-        Dim BaseDir As String = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) & "\.minecraft\runtime\" & TargetEntry.Key & "\"
+        Dim ListFileStr As String = NetGetCodeByDownload({Address.Replace("piston-meta.mojang.com", "bmclapi2.bangbang93.com"), Address}, IsJson:=True)
+        LastJavaBaseDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) & "\.minecraft\runtime\" & TargetEntry.Key & "\"
         Dim Results As New List(Of NetFile)
         Dim Results As New List(Of NetFile)
         For Each File As JProperty In CType(GetJson(ListFileStr), JObject)("files")
         For Each File As JProperty In CType(GetJson(ListFileStr), JObject)("files")
             If CType(File.Value, JObject)("downloads")?("raw") Is Nothing Then Continue For
             If CType(File.Value, JObject)("downloads")?("raw") Is Nothing Then Continue For
             Dim Info As JObject = CType(File.Value, JObject)("downloads")("raw")
             Dim Info As JObject = CType(File.Value, JObject)("downloads")("raw")
             Dim Checker As New FileChecker(ActualSize:=Info("size"), Hash:=Info("sha1"))
             Dim Checker As New FileChecker(ActualSize:=Info("size"), Hash:=Info("sha1"))
-            If Checker.Check(BaseDir & File.Name) Is Nothing Then Continue For '跳过已存在的文件
+            If Checker.Hash = "12976a6c2b227cbac58969c1455444596c894656" OrElse Checker.Hash = "c80e4bab46e34d02826eab226a4441d0970f2aba" OrElse Checker.Hash = "84d2102ad171863db04e7ee22a259d1f6c5de4a5" Then
+                '跳过 3 个无意义大量重复文件(#3827)
+                Continue For
+            End If
+            If Checker.Check(LastJavaBaseDir & File.Name) Is Nothing Then Continue For '跳过已存在的文件
             Dim Url As String = Info("url")
             Dim Url As String = Info("url")
-            Results.Add(New NetFile(
-                            {Url.Replace("piston-data.mojang.com", "bmclapi2.bangbang93.com"),
-                             Url},
-                        BaseDir & File.Name, Checker))
+            Results.Add(New NetFile({Url.Replace("piston-data.mojang.com", "bmclapi2.bangbang93.com"), Url}, LastJavaBaseDir & File.Name, Checker))
         Next
         Next
         Loader.Output = Results
         Loader.Output = Results
-        Log($"[Java] 需要下载 {Results.Count} 个文件,目标文件夹:{BaseDir}")
+        Log($"[Java] 需要下载 {Results.Count} 个文件,目标文件夹:{LastJavaBaseDir}")
     End Sub
     End Sub
 
 
 #End Region
 #End Region

+ 219 - 116
Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb

@@ -62,7 +62,7 @@ Public Module ModLaunch
     ''' 记录启动日志。
     ''' 记录启动日志。
     ''' </summary>
     ''' </summary>
     Public Sub McLaunchLog(Text As String)
     Public Sub McLaunchLog(Text As String)
-        Text = SecretFilter(Text)
+        Text = SecretFilter(Text, "*")
         RunInUi(Sub() FrmLaunchRight.LabLog.Text += vbCrLf & "[" & GetTimeNow() & "] " & Text)
         RunInUi(Sub() FrmLaunchRight.LabLog.Text += vbCrLf & "[" & GetTimeNow() & "] " & Text)
         Log("[Launch] " & Text)
         Log("[Launch] " & Text)
     End Sub
     End Sub
@@ -101,10 +101,10 @@ Public Module ModLaunch
         '正式加载
         '正式加载
         Try
         Try
             '构造主加载器
             '构造主加载器
-            Dim LaunchLoader As New LoaderCombo(Of Object)("Minecraft 启动", {
-                New LoaderTask(Of Integer, Integer)("获取 Java", AddressOf McLaunchJava) With {.ProgressWeight = 4, .Block = False, .Show = False},
+            Dim Loaders As New List(Of LoaderBase) From {
+                New LoaderTask(Of Integer, Integer)("获取 Java", AddressOf McLaunchJava) With {.ProgressWeight = 4, .Block = False},
                 McLoginLoader,
                 McLoginLoader,
-                New LoaderCombo(Of String)("补全文件", DlClientFix(McVersionCurrent, False, AssetsIndexExistsBehaviour.DownloadInBackground, True)) With {.ProgressWeight = 15, .Show = False, .Block = True},
+                New LoaderCombo(Of String)("补全文件", DlClientFix(McVersionCurrent, False, AssetsIndexExistsBehaviour.DownloadInBackground, True)) With {.ProgressWeight = 15, .Show = False},
                 New LoaderTask(Of String, List(Of McLibToken))("获取启动参数", AddressOf McLaunchArgumentMain) With {.ProgressWeight = 2},
                 New LoaderTask(Of String, List(Of McLibToken))("获取启动参数", AddressOf McLaunchArgumentMain) With {.ProgressWeight = 2},
                 New LoaderTask(Of List(Of McLibToken), Integer)("解压文件", AddressOf McLaunchNatives) With {.ProgressWeight = 2},
                 New LoaderTask(Of List(Of McLibToken), Integer)("解压文件", AddressOf McLaunchNatives) With {.ProgressWeight = 2},
                 New LoaderTask(Of Integer, Integer)("预启动处理", AddressOf McLaunchPrerun) With {.ProgressWeight = 1},
                 New LoaderTask(Of Integer, Integer)("预启动处理", AddressOf McLaunchPrerun) With {.ProgressWeight = 1},
@@ -112,7 +112,12 @@ Public Module ModLaunch
                 New LoaderTask(Of Integer, Process)("启动进程", AddressOf McLaunchRun) With {.ProgressWeight = 2},
                 New LoaderTask(Of Integer, Process)("启动进程", AddressOf McLaunchRun) With {.ProgressWeight = 2},
                 New LoaderTask(Of Process, Integer)("等待游戏窗口出现", AddressOf McLaunchWait) With {.ProgressWeight = 1},
                 New LoaderTask(Of Process, Integer)("等待游戏窗口出现", AddressOf McLaunchWait) With {.ProgressWeight = 1},
                 New LoaderTask(Of Integer, Integer)("结束处理", AddressOf McLaunchEnd) With {.ProgressWeight = 1}
                 New LoaderTask(Of Integer, Integer)("结束处理", AddressOf McLaunchEnd) With {.ProgressWeight = 1}
-            }) With {.Show = False}
+            }
+            If Setup.Get("LaunchArgumentRam") Then
+                CType(Loaders(2), LoaderCombo(Of String)).Block = False
+                Loaders.Insert(3, New LoaderTask(Of Integer, Integer)("内存优化", AddressOf McLaunchMemoryOptimize) With {.ProgressWeight = 30})
+            End If
+            Dim LaunchLoader As New LoaderCombo(Of Object)("Minecraft 启动", Loaders) With {.Show = False}
             If McLoginLoader.State = LoadState.Finished Then McLoginLoader.State = LoadState.Waiting '要求重启登录主加载器,它会自行决定是否启动副加载器
             If McLoginLoader.State = LoadState.Finished Then McLoginLoader.State = LoadState.Waiting '要求重启登录主加载器,它会自行决定是否启动副加载器
             '等待加载器执行并更新 UI
             '等待加载器执行并更新 UI
             McLaunchLoaderReal = LaunchLoader
             McLaunchLoaderReal = LaunchLoader
@@ -163,6 +168,28 @@ NextInner:
 
 
 #End Region
 #End Region
 
 
+#Region "内存优化"
+
+    Private Sub McLaunchMemoryOptimize(Loader As LoaderTask(Of Integer, Integer))
+        McLaunchLog("内存优化开始")
+        Dim Finished As Boolean = False
+        RunInNewThread(
+        Sub()
+            PageOtherTest.MemoryOptimize(False)
+            Finished = True
+        End Sub, "Launch Memory Optimize")
+        Do While Not Finished AndAlso Not Loader.IsAborted
+            If Loader.Progress < 0.7 Then
+                Loader.Progress += 0.007 '10s
+            Else
+                Loader.Progress += (0.95 - Loader.Progress) * 0.02 '最快 += 0.005
+            End If
+            Thread.Sleep(100)
+        Loop
+    End Sub
+
+#End Region
+
 #Region "预检测"
 #Region "预检测"
 
 
     Private Sub McLaunchPrecheck()
     Private Sub McLaunchPrecheck()
@@ -247,7 +274,7 @@ NextInner:
         ''' </summary>
         ''' </summary>
         Public BaseUrl As String
         Public BaseUrl As String
         ''' <summary>
         ''' <summary>
-        ''' 登录所使用的标识符,如 “Mojang”、“Nide”,用于存储缓存等。
+        ''' 登录所使用的标识符,目前只可能为 “Auth” 或 “Nide”,用于存储缓存等。
         ''' </summary>
         ''' </summary>
         Public Token As String
         Public Token As String
         ''' <summary>
         ''' <summary>
@@ -331,7 +358,7 @@ NextInner:
             Case McLoginType.Ms
             Case McLoginType.Ms
                 If Setup.Get("CacheMsName") <> "" Then Return Setup.Get("CacheMsName")
                 If Setup.Get("CacheMsName") <> "" Then Return Setup.Get("CacheMsName")
             Case McLoginType.Legacy
             Case McLoginType.Legacy
-                If Setup.Get("LoginLegacyName") <> "" Then Return Setup.Get("LoginLegacyName").ToString.Split("¨")(0)
+                If Setup.Get("LoginLegacyName") <> "" Then Return Setup.Get("LoginLegacyName").ToString.Before("¨")
             Case McLoginType.Nide
             Case McLoginType.Nide
                 If Setup.Get("CacheNideName") <> "" Then Return Setup.Get("CacheNideName")
                 If Setup.Get("CacheNideName") <> "" Then Return Setup.Get("CacheNideName")
             Case McLoginType.Auth
             Case McLoginType.Auth
@@ -341,7 +368,7 @@ NextInner:
         If Setup.Get("CacheMsName") <> "" Then Return Setup.Get("CacheMsName")
         If Setup.Get("CacheMsName") <> "" Then Return Setup.Get("CacheMsName")
         If Setup.Get("CacheNideName") <> "" Then Return Setup.Get("CacheNideName")
         If Setup.Get("CacheNideName") <> "" Then Return Setup.Get("CacheNideName")
         If Setup.Get("CacheAuthName") <> "" Then Return Setup.Get("CacheAuthName")
         If Setup.Get("CacheAuthName") <> "" Then Return Setup.Get("CacheAuthName")
-        If Setup.Get("LoginLegacyName") <> "" Then Return Setup.Get("LoginLegacyName").ToString.Split("¨")(0)
+        If Setup.Get("LoginLegacyName") <> "" Then Return Setup.Get("LoginLegacyName").ToString.Before("¨")
         Return Nothing
         Return Nothing
     End Function
     End Function
     ''' <summary>
     ''' <summary>
@@ -497,7 +524,10 @@ Relogin:
         Dim AccessToken As String = MsLoginStep5(Tokens)
         Dim AccessToken As String = MsLoginStep5(Tokens)
         Data.Progress = 0.8
         Data.Progress = 0.8
         If Data.IsAborted Then Throw New ThreadInterruptedException
         If Data.IsAborted Then Throw New ThreadInterruptedException
-        Dim Result = MsLoginStep6(AccessToken)
+        MsLoginStep6(AccessToken)
+        Data.Progress = 0.9
+        If Data.IsAborted Then Throw New ThreadInterruptedException
+        Dim Result = MsLoginStep7(AccessToken)
         '输出登录结果
         '输出登录结果
         Setup.Set("CacheMsOAuthRefresh", OAuthRefreshToken)
         Setup.Set("CacheMsOAuthRefresh", OAuthRefreshToken)
         Setup.Set("CacheMsAccess", AccessToken)
         Setup.Set("CacheMsAccess", AccessToken)
@@ -513,7 +543,7 @@ Relogin:
 SkipLogin:
 SkipLogin:
         McLaunchLog("微软登录完成")
         McLaunchLog("微软登录完成")
         Setup.Set("HintBuy", True) '关闭正版购买提示
         Setup.Set("HintBuy", True) '关闭正版购买提示
-        Data.Progress = 0.95
+        Data.Progress = 0.98
         If ThemeUnlock(10, False) Then MyMsgBox("感谢你对正版游戏的支持!" & vbCrLf & "隐藏主题 跳票红 已解锁!", "提示")
         If ThemeUnlock(10, False) Then MyMsgBox("感谢你对正版游戏的支持!" & vbCrLf & "隐藏主题 跳票红 已解锁!", "提示")
     End Sub
     End Sub
     Private Sub McLoginServerStart(Data As LoaderTask(Of McLoginServer, McLoginResult))
     Private Sub McLoginServerStart(Data As LoaderTask(Of McLoginServer, McLoginResult))
@@ -526,7 +556,13 @@ SkipLogin:
         McLaunchLog("登录方式:" & Input.Description & "(" & LogUsername & ")")
         McLaunchLog("登录方式:" & Input.Description & "(" & LogUsername & ")")
         Data.Progress = 0.05
         Data.Progress = 0.05
         '尝试登录
         '尝试登录
-        If (Not Data.Input.ForceReselectProfile) AndAlso Setup.Get("Cache" & Input.Token & "Username") = Data.Input.UserName AndAlso Setup.Get("Cache" & Input.Token & "Pass") = Data.Input.Password AndAlso Not Setup.Get("Cache" & Input.Token & "Access") = "" AndAlso Not Setup.Get("Cache" & Input.Token & "Client") = "" AndAlso Not Setup.Get("Cache" & Input.Token & "Uuid") = "" AndAlso Not Setup.Get("Cache" & Input.Token & "Name") = "" Then
+        If (Not Data.Input.ForceReselectProfile) AndAlso
+            Setup.Get("Cache" & Input.Token & "Username") = Data.Input.UserName AndAlso
+            Setup.Get("Cache" & Input.Token & "Pass") = Data.Input.Password AndAlso
+            Setup.Get("Cache" & Input.Token & "Access") <> "" AndAlso
+            Setup.Get("Cache" & Input.Token & "Client") <> "" AndAlso
+            Setup.Get("Cache" & Input.Token & "Uuid") <> "" AndAlso
+            Setup.Get("Cache" & Input.Token & "Name") <> "" Then
             '尝试验证登录
             '尝试验证登录
             Try
             Try
                 If Data.IsAborted Then Throw New ThreadInterruptedException
                 If Data.IsAborted Then Throw New ThreadInterruptedException
@@ -764,14 +800,14 @@ LoginFinish:
     '微软登录步骤 1:打开网页认证,获取 OAuth Code
     '微软登录步骤 1:打开网页认证,获取 OAuth Code
     Private Function MsLoginStep1(Data As LoaderTask(Of McLoginMs, McLoginResult)) As String
     Private Function MsLoginStep1(Data As LoaderTask(Of McLoginMs, McLoginResult)) As String
         McLaunchLog("开始微软登录步骤 1")
         McLaunchLog("开始微软登录步骤 1")
-        If OsVersion <= New Version(10, 0, 17763, 0) Then
+        If True OrElse OsVersion <= New Version(10, 0, 17763, 0) Then '由于 #3849,WebBrowser 已经无法正常加载微软登录网页了
 SystemBrowser:
 SystemBrowser:
             'Windows 7 或老版 Windows 10 登录
             'Windows 7 或老版 Windows 10 登录
             OpenWebsite(FormLoginOAuth.LoginUrl1)
             OpenWebsite(FormLoginOAuth.LoginUrl1)
             Dim Result As String =
             Dim Result As String =
                 MyMsgBoxInput("等待网页登录",
                 MyMsgBoxInput("等待网页登录",
-                              "在登录成功后,网页会变成一片空白,请将该空白页面的网址复制到下面的框中。" & vbCrLf &
-                              "如果网络环境不佳,登录网页可能一直加载不出来,此时请尝试使用 VPN 或加速器,然后再试。",
+                              "登录完成后,网页会变得完全空白,把那个空白网页的网址复制到下面的框中就行了!" & vbCrLf &
+                              "如果网络环境不佳,它可能一直加载不出来,那就只能试试用 VPN 或加速器了。",
                               ValidateRules:=New ObjectModel.Collection(Of Validate) From {New ValidateRegex("(?<=code\=)[^&]+", "返回网址应以 https://login.live.com/oauth20_desktop.srf?code= 开头")},
                               ValidateRules:=New ObjectModel.Collection(Of Validate) From {New ValidateRegex("(?<=code\=)[^&]+", "返回网址应以 https://login.live.com/oauth20_desktop.srf?code= 开头")},
                               HintText:="https://login.live.com/oauth20_desktop.srf?code=XXXXXX")
                               HintText:="https://login.live.com/oauth20_desktop.srf?code=XXXXXX")
             If Result Is Nothing Then
             If Result Is Nothing Then
@@ -780,7 +816,7 @@ SystemBrowser:
             Else
             Else
                 Return RegexSeek(Result, "(?<=code\=)[^&]+")
                 Return RegexSeek(Result, "(?<=code\=)[^&]+")
             End If
             End If
-            Hint("网页登录已完成,你现在可以关闭浏览器上的那个网页了", HintType.Finish)
+            Hint("网页登录成功,你可以关闭浏览器啦!", HintType.Finish)
         Else
         Else
             'Windows 10 登录
             'Windows 10 登录
             Dim ReturnCode As String = Nothing
             Dim ReturnCode As String = Nothing
@@ -893,12 +929,18 @@ SystemBrowser:
         Try
         Try
             Result = NetRequestMulty("https://xsts.auth.xboxlive.com/xsts/authorize", "POST", Request, "application/json", 3)
             Result = NetRequestMulty("https://xsts.auth.xboxlive.com/xsts/authorize", "POST", Request, "application/json", 3)
         Catch ex As Net.WebException
         Catch ex As Net.WebException
-            If ex.Message.Contains("2148916233") Then
-                If MyMsgBox("该微软账号尚未购买 Minecraft Java 版,或尚未注册 Xbox 账户。" & vbCrLf &
-                            "如果你确定该账号完成了上述步骤,请先在游戏官网登录一次,然后再在启动器登录。", "登录提示", "打开游戏官网", "取消") = 1 Then
-                    OpenWebsite("https://www.minecraft.net/")
+            '参考 https://github.com/PrismarineJS/prismarine-auth/blob/master/src/common/Constants.js
+            If ex.Message.Contains("2148916227") Then
+                MyMsgBox("该账号似乎已被微软封禁,无法登录。", "登录失败", "我知道了", IsWarn:=True)
+                Throw New Exception("$$")
+            ElseIf ex.Message.Contains("2148916233") Then
+                If MyMsgBox("你尚未注册 Xbox 账户,请在注册后再登录。", "登录提示", "注册", "取消") = 1 Then
+                    OpenWebsite("https://signup.live.com/signup")
                 End If
                 End If
                 Throw New Exception("$$")
                 Throw New Exception("$$")
+            ElseIf ex.Message.Contains("2148916235") Then
+                MyMsgBox($"你的网络所在的国家或地区无法登录微软账号。{vbCrLf}请尝试使用加速器或 VPN。", "登录失败", "我知道了")
+                Throw New Exception("$$")
             ElseIf ex.Message.Contains("2148916238") Then
             ElseIf ex.Message.Contains("2148916238") Then
                 If MyMsgBox("该账号年龄不足,你需要先修改出生日期,然后才能登录。" & vbCrLf &
                 If MyMsgBox("该账号年龄不足,你需要先修改出生日期,然后才能登录。" & vbCrLf &
                             "该账号目前填写的年龄是否在 13 岁以上?", "登录提示", "13 岁以上", "12 岁以下", "我不知道") = 1 Then
                             "该账号目前填写的年龄是否在 13 岁以上?", "登录提示", "13 岁以上", "12 岁以下", "我不知道") = 1 Then
@@ -946,29 +988,46 @@ SystemBrowser:
         Dim AccessToken As String = ResultJson("access_token").ToString
         Dim AccessToken As String = ResultJson("access_token").ToString
         Return AccessToken
         Return AccessToken
     End Function
     End Function
-    '微软登录步骤 6:从 Minecraft AccessToken 获取 {UUID, UserName, ProfileJson}
-    Private Function MsLoginStep6(AccessToken As String) As String()
+    '微软登录步骤 6:验证微软账号是否持有 MC,这也会刷新 XGP
+    Private Sub MsLoginStep6(AccessToken As String)
         McLaunchLog("开始微软登录步骤 6")
         McLaunchLog("开始微软登录步骤 6")
 
 
+        Dim Result As String = NetRequestMulty("https://api.minecraftservices.com/entitlements/mcstore", "GET", "", "application/json", 2, New Dictionary(Of String, String) From {{"Authorization", "Bearer " & AccessToken}})
+        Try
+            Dim ResultJson As JObject = GetJson(Result)
+            If Not (ResultJson.ContainsKey("items") AndAlso ResultJson("items").Any) Then
+                Select Case MyMsgBox("你尚未购买正版 Minecraft,或者 Xbox Game Pass 已到期。", "登录失败", "购买 Minecraft", "取消")
+                    Case 1
+                        OpenWebsite("https://www.xbox.com/zh-cn/games/store/minecraft-java-bedrock-edition-for-pc/9nxp44l49shj")
+                End Select
+                Throw New Exception("$$")
+            End If
+        Catch ex As Exception
+            Log(ex, "微软登录第 6 步异常:" & Result)
+            Throw
+        End Try
+    End Sub
+    '微软登录步骤 7:从 Minecraft AccessToken 获取 {UUID, UserName, ProfileJson}
+    Private Function MsLoginStep7(AccessToken As String) As String()
+        McLaunchLog("开始微软登录步骤 7")
+
         Dim Result As String
         Dim Result As String
         Try
         Try
             Result = NetRequestMulty("https://api.minecraftservices.com/minecraft/profile", "GET", "", "application/json", 2, New Dictionary(Of String, String) From {{"Authorization", "Bearer " & AccessToken}})
             Result = NetRequestMulty("https://api.minecraftservices.com/minecraft/profile", "GET", "", "application/json", 2, New Dictionary(Of String, String) From {{"Authorization", "Bearer " & AccessToken}})
         Catch ex As Net.WebException
         Catch ex As Net.WebException
             Dim Message As String = GetExceptionSummary(ex)
             Dim Message As String = GetExceptionSummary(ex)
             If Message.Contains("(429)") Then
             If Message.Contains("(429)") Then
-                Log(ex, "微软登录第 6 步汇报 429")
+                Log(ex, "微软登录第 7 步汇报 429")
                 Throw New Exception("$登录尝试太过频繁,请等待几分钟后再试!")
                 Throw New Exception("$登录尝试太过频繁,请等待几分钟后再试!")
             ElseIf Message.Contains("(404)") Then
             ElseIf Message.Contains("(404)") Then
-                Log(ex, "微软登录第 6 步汇报 404")
-                RunInNewThread(Sub()
-                                   Select Case MyMsgBox("你可能没有创建 Minecraft 玩家档案、Xbox Game Pass 已到期,或者还没有购买 Minecraft。" & vbCrLf &
-                                            "请先使用官方启动器登录一次,之后即可使用 PCL 登录。", "登录失败", "创建档案", "购买 Minecraft", "取消")
-                                       Case 1
-                                           OpenWebsite("https://www.minecraft.net/zh-hans/msaprofile/mygames/editprofile")
-                                       Case 2
-                                           OpenWebsite("https://www.xbox.com/zh-cn/games/store/minecraft-java-bedrock-edition-for-pc/9nxp44l49shj")
-                                   End Select
-                               End Sub, "Login Failed: Create Profile")
+                Log(ex, "微软登录第 7 步汇报 404")
+                RunInNewThread(
+                Sub()
+                    Select Case MyMsgBox("请先创建 Minecraft 玩家档案,然后再重新登录。", "登录失败", "创建档案", "取消")
+                        Case 1
+                            OpenWebsite("https://www.minecraft.net/zh-hans/msaprofile/mygames/editprofile")
+                    End Select
+                End Sub, "Login Failed: Create Profile")
                 Throw New Exception("$$")
                 Throw New Exception("$$")
             Else
             Else
                 Throw
                 Throw
@@ -990,14 +1049,14 @@ SystemBrowser:
             Case 1
             Case 1
                 'Steve
                 'Steve
                 Do Until McSkinSex(Uuid) = "Steve"
                 Do Until McSkinSex(Uuid) = "Steve"
-                    If Uuid.EndsWithF("FFFFF") Then Uuid = Mid(Uuid, 1, 32 - 5) & "00000"
-                    Uuid = Mid(Uuid, 1, 32 - 5) & (Long.Parse(Right(Uuid, 5), Globalization.NumberStyles.AllowHexSpecifier) + 1).ToString("X")
+                    If Uuid.EndsWithF("FFFFF") Then Uuid = Uuid.Substring(0, 27) & "00000"
+                    Uuid = Uuid.Substring(0, 27) & (Long.Parse(Uuid.Substring(27), Globalization.NumberStyles.AllowHexSpecifier) + 1).ToString("X").PadLeft(5, "0")
                 Loop
                 Loop
             Case 2
             Case 2
                 'Alex
                 'Alex
                 Do Until McSkinSex(Uuid) = "Alex"
                 Do Until McSkinSex(Uuid) = "Alex"
-                    If Uuid.EndsWithF("FFFFF") Then Uuid = Mid(Uuid, 1, 32 - 5) & "00000"
-                    Uuid = Mid(Uuid, 1, 32 - 5) & (Long.Parse(Right(Uuid, 5), Globalization.NumberStyles.AllowHexSpecifier) + 1).ToString("X")
+                    If Uuid.EndsWithF("FFFFF") Then Uuid = Uuid.Substring(0, 27) & "00000"
+                    Uuid = Uuid.Substring(0, 27) & (Long.Parse(Uuid.Substring(27), Globalization.NumberStyles.AllowHexSpecifier) + 1).ToString("X").PadLeft(5, "0")
                 Loop
                 Loop
             Case 3
             Case 3
                 '使用正版用户名
                 '使用正版用户名
@@ -1096,22 +1155,25 @@ SystemBrowser:
         If McVersionCurrent.Version.HasForge Then
         If McVersionCurrent.Version.HasForge Then
             If McVersionCurrent.Version.McName = "1.7.2" Then
             If McVersionCurrent.Version.McName = "1.7.2" Then
                 '1.7.2:必须 Java 7
                 '1.7.2:必须 Java 7
-                MinVer = New Version(1, 7, 0, 0) : MaxVer = New Version(1, 7, 999, 999)
+                MinVer = If(New Version(1, 7, 0, 0) > MinVer, New Version(1, 7, 0, 0), MinVer)
+                MaxVer = If(New Version(1, 7, 999, 999) < MaxVer, New Version(1, 7, 999, 999), MaxVer)
             ElseIf McVersionCurrent.Version.McCodeMain <= 12 AndAlso McVersionCurrent.Version.McCodeMain > 0 Then
             ElseIf McVersionCurrent.Version.McCodeMain <= 12 AndAlso McVersionCurrent.Version.McCodeMain > 0 Then
                 '<=1.12:Java 8
                 '<=1.12:Java 8
                 MaxVer = New Version(1, 8, 999, 999)
                 MaxVer = New Version(1, 8, 999, 999)
             ElseIf McVersionCurrent.Version.McCodeMain <= 14 AndAlso McVersionCurrent.Version.McCodeMain >= 13 Then
             ElseIf McVersionCurrent.Version.McCodeMain <= 14 AndAlso McVersionCurrent.Version.McCodeMain >= 13 Then
                 '1.13 - 1.14:Java 8 - 10
                 '1.13 - 1.14:Java 8 - 10
-                MinVer = New Version(1, 8, 0, 0) : MaxVer = New Version(1, 10, 999, 999)
+                MinVer = If(New Version(1, 8, 0, 0) > MinVer, New Version(1, 8, 0, 0), MinVer)
+                MaxVer = If(New Version(1, 10, 999, 999) < MaxVer, New Version(1, 10, 999, 999), MaxVer)
             ElseIf McVersionCurrent.Version.McCodeMain = 15 Then
             ElseIf McVersionCurrent.Version.McCodeMain = 15 Then
                 '1.15:Java 8 - 15
                 '1.15:Java 8 - 15
-                MinVer = New Version(1, 8, 0, 0) : MaxVer = New Version(1, 15, 999, 999)
+                MinVer = If(New Version(1, 8, 0, 0) > MinVer, New Version(1, 8, 0, 0), MinVer)
+                MaxVer = If(New Version(1, 15, 999, 999) < MaxVer, New Version(1, 15, 999, 999), MaxVer)
             ElseIf VersionSortBoolean(McVersionCurrent.Version.ForgeVersion, "34.0.0") AndAlso VersionSortBoolean("36.2.25", McVersionCurrent.Version.ForgeVersion) Then
             ElseIf VersionSortBoolean(McVersionCurrent.Version.ForgeVersion, "34.0.0") AndAlso VersionSortBoolean("36.2.25", McVersionCurrent.Version.ForgeVersion) Then
                 '1.16,Forge 34.X ~ 36.2.25:最高 Java 8u320
                 '1.16,Forge 34.X ~ 36.2.25:最高 Java 8u320
-                MaxVer = New Version(1, 8, 0, 320)
+                MaxVer = If(New Version(1, 8, 0, 320) < MaxVer, New Version(1, 8, 0, 320), MaxVer)
             ElseIf McVersionCurrent.Version.McCodeMain >= 18 AndAlso McVersionCurrent.Version.McCodeMain < 19 AndAlso McVersionCurrent.Version.HasOptiFine Then '#305
             ElseIf McVersionCurrent.Version.McCodeMain >= 18 AndAlso McVersionCurrent.Version.McCodeMain < 19 AndAlso McVersionCurrent.Version.HasOptiFine Then '#305
                 '1.18:若安装了 OptiFine,最高 Java 18
                 '1.18:若安装了 OptiFine,最高 Java 18
-                MaxVer = New Version(1, 18, 999, 999)
+                MaxVer = If(New Version(1, 18, 999, 999) < MaxVer, New Version(1, 18, 999, 999), MaxVer)
             End If
             End If
         End If
         End If
 
 
@@ -1119,10 +1181,10 @@ SystemBrowser:
         If McVersionCurrent.Version.HasFabric Then
         If McVersionCurrent.Version.HasFabric Then
             If McVersionCurrent.Version.McCodeMain >= 15 AndAlso McVersionCurrent.Version.McCodeMain <= 16 AndAlso McVersionCurrent.Version.McCodeMain <> -1 Then
             If McVersionCurrent.Version.McCodeMain >= 15 AndAlso McVersionCurrent.Version.McCodeMain <= 16 AndAlso McVersionCurrent.Version.McCodeMain <> -1 Then
                 '1.15 - 1.16:Java 8+
                 '1.15 - 1.16:Java 8+
-                MinVer = New Version(1, 8, 0, 0)
+                MinVer = If(New Version(1, 8, 0, 0) > MinVer, New Version(1, 8, 0, 0), MinVer)
             ElseIf McVersionCurrent.Version.McCodeMain >= 18 AndAlso McVersionCurrent.Version.McCodeMain < 99 Then
             ElseIf McVersionCurrent.Version.McCodeMain >= 18 AndAlso McVersionCurrent.Version.McCodeMain < 99 Then
                 '1.18+:Java 17+
                 '1.18+:Java 17+
-                MinVer = New Version(1, 17, 0, 0)
+                MinVer = If(New Version(1, 17, 0, 0) > MinVer, New Version(1, 17, 0, 0), MinVer)
             End If
             End If
         End If
         End If
 
 
@@ -1132,63 +1194,68 @@ SystemBrowser:
             MinVer = If(New Version(1, 8, 0, 101) > MinVer, New Version(1, 8, 0, 101), MinVer)
             MinVer = If(New Version(1, 8, 0, 101) > MinVer, New Version(1, 8, 0, 101), MinVer)
         End If
         End If
 
 
-        '选择 Java
-        McLaunchLog("Java 版本需求:最低 " & MinVer.ToString & ",最高 " & MaxVer.ToString)
-        McLaunchJavaSelected = JavaSelect("$$", MinVer, MaxVer, McVersionCurrent)
-        If Task.IsAborted Then Exit Sub
-        If McLaunchJavaSelected IsNot Nothing Then
-            McLaunchLog("选择的 Java:" & McLaunchJavaSelected.ToString)
-            Exit Sub
-        End If
+        SyncLock JavaLock
 
 
-        '无合适的 Java
-        If Task.IsAborted Then Exit Sub '中断加载会导致 JavaSelect 异常地返回空值,误判找不到 Java
-        McLaunchLog("无合适的 Java,需要确认是否自动下载")
-        Dim JavaCode As String
-        If MinVer >= New Version(1, 21, 0, 0) Then
-            JavaCode = 21
-            If Not JavaDownloadConfirm("Java 21") Then Throw New Exception("$$")
-        ElseIf MinVer >= New Version(1, 9, 0, 0) Then
-            JavaCode = 17
-            If Not JavaDownloadConfirm("Java 17") Then Throw New Exception("$$")
-        ElseIf MaxVer < New Version(1, 8, 0, 0) Then
-            JavaCode = 7
-            If Not JavaDownloadConfirm("Java 7") Then Throw New Exception("$$")
-        ElseIf MinVer > New Version(1, 8, 0, 100) AndAlso MaxVer < New Version(1, 8, 0, 321) Then
-            JavaCode = "8u101"
-            JavaDownloadConfirm("Java 8.0.101 ~ 8.0.320", True)
-            Throw New Exception("$$")
-        ElseIf MinVer > New Version(1, 8, 0, 100) Then
-            JavaCode = "8u101"
-            JavaDownloadConfirm("Java 8.0.101 或更高版本的 Java 8", True)
-            Throw New Exception("$$")
-        ElseIf MaxVer < New Version(1, 8, 0, 321) Then
-            JavaCode = 8
-            If Not JavaDownloadConfirm("Java 8.0.320 或更低版本的 Java 8") Then Throw New Exception("$$")
-        Else
-            JavaCode = 8
-            If Not JavaDownloadConfirm("Java 8") Then Throw New Exception("$$")
-        End If
+            '选择 Java
+            McLaunchLog("Java 版本需求:最低 " & MinVer.ToString & ",最高 " & MaxVer.ToString)
+            McLaunchJavaSelected = JavaSelect("$$", MinVer, MaxVer, McVersionCurrent)
+            If Task.IsAborted Then Exit Sub
+            If McLaunchJavaSelected IsNot Nothing Then
+                McLaunchLog("选择的 Java:" & McLaunchJavaSelected.ToString)
+                Exit Sub
+            End If
 
 
-        '开始自动下载
-        Dim JavaLoader = JavaFixLoaders(JavaCode)
-        JavaLoader.Start(JavaCode, IsForceRestart:=True)
-        Do While JavaLoader.State = LoadState.Loading AndAlso Not Task.IsAborted
-            Task.Progress = JavaLoader.Progress
-            Thread.Sleep(10)
-        Loop
+            '无合适的 Java
+            If Task.IsAborted Then Exit Sub '中断加载会导致 JavaSelect 异常地返回空值,误判找不到 Java
+            McLaunchLog("无合适的 Java,需要确认是否自动下载")
+            Dim JavaCode As String
+            If MinVer >= New Version(1, 21, 0, 0) Then
+                JavaCode = 21
+                If Not JavaDownloadConfirm("Java 21") Then Throw New Exception("$$")
+            ElseIf MinVer >= New Version(1, 9, 0, 0) Then
+                JavaCode = 17
+                If Not JavaDownloadConfirm("Java 17") Then Throw New Exception("$$")
+            ElseIf MaxVer < New Version(1, 8, 0, 0) Then
+                JavaCode = 7
+                If Not JavaDownloadConfirm("Java 7", True) Then Throw New Exception("$$")
+            ElseIf MinVer > New Version(1, 8, 0, 100) AndAlso MaxVer < New Version(1, 8, 0, 321) Then
+                JavaCode = "8u101"
+                If Not JavaDownloadConfirm("Java 8.0.101 ~ 8.0.320", True) Then Throw New Exception("$$")
+            ElseIf MinVer > New Version(1, 8, 0, 100) Then
+                JavaCode = "8u101"
+                If Not JavaDownloadConfirm("Java 8.0.101 或更高版本的 Java 8", True) Then Throw New Exception("$$")
+            ElseIf MaxVer < New Version(1, 8, 0, 321) Then
+                JavaCode = 8
+                If Not JavaDownloadConfirm("Java 8.0.320 或更低版本的 Java 8") Then Throw New Exception("$$")
+            Else
+                JavaCode = 8
+                If Not JavaDownloadConfirm("Java 8") Then Throw New Exception("$$")
+            End If
 
 
-        '检查下载结果
-        If JavaSearchLoader.State <> LoadState.Loading Then JavaSearchLoader.State = LoadState.Waiting '2872#
-        McLaunchJavaSelected = JavaSelect("$$", MinVer, MaxVer, McVersionCurrent)
-        If Task.IsAborted Then Exit Sub
-        If McLaunchJavaSelected IsNot Nothing Then
-            McLaunchLog("选择的 Java:" & McLaunchJavaSelected.ToString)
-        Else
-            Hint("没有可用的 Java,已取消启动!", HintType.Critical)
-            Throw New Exception("$$")
-        End If
+            '开始自动下载
+            Dim JavaLoader = JavaFixLoaders(JavaCode)
+            Try
+                JavaLoader.Start(JavaCode, IsForceRestart:=True)
+                Do While JavaLoader.State = LoadState.Loading AndAlso Not Task.IsAborted
+                    Task.Progress = JavaLoader.Progress
+                    Thread.Sleep(10)
+                Loop
+            Finally
+                JavaLoader.Abort() '确保取消时中止 Java 下载
+            End Try
 
 
+            '检查下载结果
+            If JavaSearchLoader.State <> LoadState.Loading Then JavaSearchLoader.State = LoadState.Waiting '2872#
+            McLaunchJavaSelected = JavaSelect("$$", MinVer, MaxVer, McVersionCurrent)
+            If Task.IsAborted Then Exit Sub
+            If McLaunchJavaSelected IsNot Nothing Then
+                McLaunchLog("选择的 Java:" & McLaunchJavaSelected.ToString)
+            Else
+                Hint("没有可用的 Java,已取消启动!", HintType.Critical)
+                Throw New Exception("$$")
+            End If
+
+        End SyncLock
     End Sub
     End Sub
 
 
 #End Region
 #End Region
@@ -1202,17 +1269,20 @@ SystemBrowser:
     ''' </summary>
     ''' </summary>
     Public Function ExtractJavaWrapper() As String
     Public Function ExtractJavaWrapper() As String
         Dim WrapperPath As String = GetJavaWrapperDir() & "\JavaWrapper.jar"
         Dim WrapperPath As String = GetJavaWrapperDir() & "\JavaWrapper.jar"
-        WriteFile(WrapperPath, GetResources("JavaWrapper"))
+        SyncLock ExtractJavaWrapperLock '避免 OptiFine 和 Forge 安装时同时释放 Java Wrapper 导致冲突
+            WriteFile(WrapperPath, GetResources("JavaWrapper"))
+        End SyncLock
         Log("[Java] 已释放 Java Wrapper:" & WrapperPath)
         Log("[Java] 已释放 Java Wrapper:" & WrapperPath)
         Return WrapperPath
         Return WrapperPath
     End Function
     End Function
+    Private ExtractJavaWrapperLock As New Object
     ''' <summary>
     ''' <summary>
     ''' 获取 Java Wrapper 所在的文件夹,不以 \ 结尾。
     ''' 获取 Java Wrapper 所在的文件夹,不以 \ 结尾。
     ''' </summary>
     ''' </summary>
     Public Function GetJavaWrapperDir() As String
     Public Function GetJavaWrapperDir() As String
-        If Encoding.UTF8.GetByteCount(PathAppdata) = PathAppdata.Length Then
+        If PathAppdata.IsASCII() Then
             Return PathAppdata.TrimEnd("\")
             Return PathAppdata.TrimEnd("\")
-        ElseIf Encoding.UTF8.GetByteCount(PathTemp) = PathTemp.Length Then
+        ElseIf PathTemp.IsASCII() Then
             Log("[Java] Wrapper:AppData 路径中包含非 ASCII 字符,换用 Temp 目录")
             Log("[Java] Wrapper:AppData 路径中包含非 ASCII 字符,换用 Temp 目录")
             Return PathTemp.TrimEnd("\")
             Return PathTemp.TrimEnd("\")
         Else
         Else
@@ -1276,9 +1346,7 @@ SystemBrowser:
                     '不包含端口号
                     '不包含端口号
                     Arguments += " --server " & Server & " --port 25565"
                     Arguments += " --server " & Server & " --port 25565"
                 End If
                 End If
-                If McVersionCurrent.Version.HasOptiFine Then 'OptiFine 警告
-                    Hint("OptiFine 与自动进入服务器可能不兼容,有概率导致材质丢失甚至游戏崩溃!", HintType.Critical)
-                End If
+                If McVersionCurrent.Version.HasOptiFine Then Hint("OptiFine 与自动进入服务器可能不兼容,有概率导致材质丢失甚至游戏崩溃!", HintType.Critical)
             End If
             End If
         End If
         End If
         '自定义
         '自定义
@@ -1300,10 +1368,11 @@ SystemBrowser:
         Dim ArgumentJvm As String = Setup.Get("VersionAdvanceJvm", Version:=McVersionCurrent)
         Dim ArgumentJvm As String = Setup.Get("VersionAdvanceJvm", Version:=McVersionCurrent)
         If ArgumentJvm = "" Then ArgumentJvm = Setup.Get("LaunchAdvanceJvm")
         If ArgumentJvm = "" Then ArgumentJvm = Setup.Get("LaunchAdvanceJvm")
         If Not ArgumentJvm.Contains("-Dlog4j2.formatMsgNoLookups=true") Then ArgumentJvm += " -Dlog4j2.formatMsgNoLookups=true"
         If Not ArgumentJvm.Contains("-Dlog4j2.formatMsgNoLookups=true") Then ArgumentJvm += " -Dlog4j2.formatMsgNoLookups=true"
+        ArgumentJvm = ArgumentJvm.Replace(" -XX:MaxDirectMemorySize=256M", "") '#3511 的清理
         DataList.Insert(0, ArgumentJvm) '可变 JVM 参数
         DataList.Insert(0, ArgumentJvm) '可变 JVM 参数
-        DataList.Add("-Xmn256m")
+        DataList.Add("-Xmn" & Math.Floor(PageVersionSetup.GetRam(McVersionCurrent, Not McLaunchJavaSelected.Is64Bit) * 1024 * 0.15) & "m")
         DataList.Add("-Xmx" & Math.Floor(PageVersionSetup.GetRam(McVersionCurrent, Not McLaunchJavaSelected.Is64Bit) * 1024) & "m")
         DataList.Add("-Xmx" & Math.Floor(PageVersionSetup.GetRam(McVersionCurrent, Not McLaunchJavaSelected.Is64Bit) * 1024) & "m")
-        DataList.Add("""-Djava.library.path=" & Version.Path & Version.Name & "-natives""")
+        DataList.Add("""-Djava.library.path=" & GetNativesFolder() & """")
         DataList.Add("-cp ${classpath}") '把支持库添加进启动参数表
         DataList.Add("-cp ${classpath}") '把支持库添加进启动参数表
 
 
         '统一通行证
         '统一通行证
@@ -1415,6 +1484,9 @@ NextVersion:
             DeDuplicateDataList.Add(CurrentEntry.Trim.Replace("McEmu= ", "McEmu="))
             DeDuplicateDataList.Add(CurrentEntry.Trim.Replace("McEmu= ", "McEmu="))
         Next
         Next
 
 
+        '#3511 的清理
+        DeDuplicateDataList.Remove("-XX:MaxDirectMemorySize=256M")
+
         '去重
         '去重
         Dim Result As String = Join(DeDuplicateDataList.Distinct.ToList, " ")
         Dim Result As String = Join(DeDuplicateDataList.Distinct.ToList, " ")
 
 
@@ -1535,7 +1607,7 @@ NextVersion:
 
 
         '基础参数
         '基础参数
         GameArguments.Add("${classpath_separator}", ";")
         GameArguments.Add("${classpath_separator}", ";")
-        GameArguments.Add("${natives_directory}", Version.Path & Version.Name & "-natives")
+        GameArguments.Add("${natives_directory}", GetNativesFolder())
         GameArguments.Add("${library_directory}", PathMcFolder & "libraries")
         GameArguments.Add("${library_directory}", PathMcFolder & "libraries")
         GameArguments.Add("${libraries_directory}", PathMcFolder & "libraries")
         GameArguments.Add("${libraries_directory}", PathMcFolder & "libraries")
         GameArguments.Add("${launcher_name}", "PCL")
         GameArguments.Add("${launcher_name}", "PCL")
@@ -1552,9 +1624,27 @@ NextVersion:
         GameArguments.Add("${access_token}", McLoginLoader.Output.AccessToken)
         GameArguments.Add("${access_token}", McLoginLoader.Output.AccessToken)
         GameArguments.Add("${auth_session}", McLoginLoader.Output.AccessToken)
         GameArguments.Add("${auth_session}", McLoginLoader.Output.AccessToken)
         GameArguments.Add("${user_type}", "msa") '#1221
         GameArguments.Add("${user_type}", "msa") '#1221
-        Dim GameSize As Size = Setup.GetLaunchArgumentWindowSize()
-        GameArguments.Add("${resolution_width}", GameSize.Width)
-        GameArguments.Add("${resolution_height}", GameSize.Height)
+
+        '窗口尺寸参数
+        Dim GameSize As Size
+        Select Case Setup.Get("LaunchArgumentWindowType")
+            Case 2
+                Dim Result As Size
+                RunInUiWait(Sub() Result = New Size(GetPixelSize(FrmMain.PanForm.ActualWidth), GetPixelSize(FrmMain.PanForm.ActualHeight)))
+                GameSize = Result
+            Case 3
+                GameSize = New Size(Math.Max(100, Setup.Get("LaunchArgumentWindowWidth") - 2), Math.Max(100, Setup.Get("LaunchArgumentWindowHeight") - 2))
+            Case Else
+                GameSize = New Size(875 - 2, 540 - 2)
+        End Select
+        GameSize.Height -= 29.5 * DPI / 96 '标题栏高度
+        If McVersionCurrent.Version.McCodeMain <= 12 AndAlso McLaunchJavaSelected.VersionCode <= 8 AndAlso
+            Not McVersionCurrent.Version.HasOptiFine AndAlso Not McVersionCurrent.Version.HasForge Then '修复 #3463:1.12.2-,JRE 8 下窗口大小为设置大小的 DPI% 倍
+            GameSize.Width /= DPI / 96
+            GameSize.Height /= DPI / 96
+        End If
+        GameArguments.Add("${resolution_width}", Math.Round(GameSize.Width))
+        GameArguments.Add("${resolution_height}", Math.Round(GameSize.Height))
 
 
         'Assets 相关参数
         'Assets 相关参数
         GameArguments.Add("${game_assets}", PathMcFolder & "assets\virtual\legacy") '1.5.2 的 pre-1.6 资源索引应与 legacy 合并
         GameArguments.Add("${game_assets}", PathMcFolder & "assets\virtual\legacy") '1.5.2 的 pre-1.6 资源索引应与 legacy 合并
@@ -1586,7 +1676,8 @@ NextVersion:
     Private Sub McLaunchNatives(Loader As LoaderTask(Of List(Of McLibToken), Integer))
     Private Sub McLaunchNatives(Loader As LoaderTask(Of List(Of McLibToken), Integer))
 
 
         '创建文件夹
         '创建文件夹
-        Directory.CreateDirectory(McVersionCurrent.Path & McVersionCurrent.Name & "-natives")
+        Dim Target As String = GetNativesFolder() & "\"
+        Directory.CreateDirectory(Target)
 
 
         '解压文件
         '解压文件
         McLaunchLog("正在解压 Natives 文件")
         McLaunchLog("正在解压 Natives 文件")
@@ -1605,7 +1696,7 @@ NextVersion:
                 Dim FileName As String = Entry.FullName
                 Dim FileName As String = Entry.FullName
                 If FileName.EndsWithF(".dll", True) Then
                 If FileName.EndsWithF(".dll", True) Then
                     '实际解压文件的步骤
                     '实际解压文件的步骤
-                    Dim FilePath As String = McVersionCurrent.Path & McVersionCurrent.Name & "-natives\" & FileName
+                    Dim FilePath As String = Target & FileName
                     ExistFiles.Add(FilePath)
                     ExistFiles.Add(FilePath)
                     Dim OriginalFile As New FileInfo(FilePath)
                     Dim OriginalFile As New FileInfo(FilePath)
                     If OriginalFile.Exists Then
                     If OriginalFile.Exists Then
@@ -1631,7 +1722,7 @@ NextVersion:
         Next
         Next
 
 
         '删除多余文件
         '删除多余文件
-        For Each FileName As String In Directory.GetFiles(McVersionCurrent.Path & McVersionCurrent.Name & "-natives")
+        For Each FileName As String In Directory.GetFiles(Target)
             If ExistFiles.Contains(FileName) Then Continue For
             If ExistFiles.Contains(FileName) Then Continue For
             Try
             Try
                 McLaunchLog("删除:" & FileName)
                 McLaunchLog("删除:" & FileName)
@@ -1644,6 +1735,16 @@ NextVersion:
         Next
         Next
 
 
     End Sub
     End Sub
+    ''' <summary>
+    ''' 获取 Natives 文件夹路径,不以 \ 结尾。
+    ''' </summary>
+    Private Function GetNativesFolder() As String
+        Dim Result As String = McVersionCurrent.Path & McVersionCurrent.Name & "-natives"
+        If IsGBKEncoding OrElse Result.IsASCII() Then Return Result
+        Result = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) & "\.minecraft\bin\natives"
+        If Result.IsASCII() Then Return Result
+        Return OsDrive & "ProgramData\PCL\natives"
+    End Function
 
 
 #End Region
 #End Region
 
 
@@ -1737,13 +1838,14 @@ NextVersion:
             '1.11 ~ 12:zh_cn 时正常,zh_CN 时虽然显示了中文但语言设置会错误地显示选择英文
             '1.11 ~ 12:zh_cn 时正常,zh_CN 时虽然显示了中文但语言设置会错误地显示选择英文
             '1.13+    :zh_cn 时正常,zh_CN 时自动切换为英文
             '1.13+    :zh_cn 时正常,zh_CN 时自动切换为英文
             Dim CurrentLang As String = ReadIni(SetupFileAddress, "lang", "none")
             Dim CurrentLang As String = ReadIni(SetupFileAddress, "lang", "none")
-            Dim RequiredLang As String = If(CurrentLang = "none", If(Setup.Get("ToolHelpChinese"), "zh_cn", "en_us"), CurrentLang.ToLower)
+            Dim RequiredLang As String = If(CurrentLang = "none" OrElse Not Directory.Exists(McVersionCurrent.PathIndie & "saves"), '#3844,整合包可能已经自带了 options.txt
+                If(Setup.Get("ToolHelpChinese"), "zh_cn", "en_us"), CurrentLang.ToLower)
             If McVersionCurrent.Version.McCodeMain < 12 Then '注意老版本(包含 MC 1.1)的 McCodeMain 可能为 -1
             If McVersionCurrent.Version.McCodeMain < 12 Then '注意老版本(包含 MC 1.1)的 McCodeMain 可能为 -1
                 '将最后两位改为大写,前面的部分保留
                 '将最后两位改为大写,前面的部分保留
                 RequiredLang = RequiredLang.Substring(0, RequiredLang.Length - 2) & RequiredLang.Substring(RequiredLang.Length - 2).ToUpper
                 RequiredLang = RequiredLang.Substring(0, RequiredLang.Length - 2) & RequiredLang.Substring(RequiredLang.Length - 2).ToUpper
             End If
             End If
             If CurrentLang = RequiredLang Then
             If CurrentLang = RequiredLang Then
-                McLaunchLog($"当前语言为 {CurrentLang},无需修改")
+                McLaunchLog($"需要的语言为 {RequiredLang},当前语言为 {CurrentLang},无需修改")
             Else
             Else
                 WriteIni(SetupFileAddress, "lang", "-") '触发缓存更改,避免删除后重新下载残留缓存
                 WriteIni(SetupFileAddress, "lang", "-") '触发缓存更改,避免删除后重新下载残留缓存
                 WriteIni(SetupFileAddress, "lang", RequiredLang)
                 WriteIni(SetupFileAddress, "lang", RequiredLang)
@@ -1758,7 +1860,7 @@ NextVersion:
             Select Case Setup.Get("LaunchArgumentWindowType")
             Select Case Setup.Get("LaunchArgumentWindowType")
                 Case 0 '全屏
                 Case 0 '全屏
                     WriteIni(SetupFileAddress, "fullscreen", "true")
                     WriteIni(SetupFileAddress, "fullscreen", "true")
-                Case 5 '保持默认
+                Case 1 '默认
                 Case Else '其他
                 Case Else '其他
                     WriteIni(SetupFileAddress, "fullscreen", "false")
                     WriteIni(SetupFileAddress, "fullscreen", "false")
             End Select
             End Select
@@ -1936,7 +2038,7 @@ IgnoreCustomSkin:
                 """" & McLaunchJavaSelected.PathJava & """ " & McLaunchArgument & vbCrLf &
                 """" & McLaunchJavaSelected.PathJava & """ " & McLaunchArgument & vbCrLf &
                 "echo 游戏已退出。" & vbCrLf &
                 "echo 游戏已退出。" & vbCrLf &
                 "pause"
                 "pause"
-            WriteFile(If(McLaunchLoader.Input.SaveBatch, Path & "PCL\LatestLaunch.bat"), SecretFilter(CmdString),
+            WriteFile(If(McLaunchLoader.Input.SaveBatch, Path & "PCL\LatestLaunch.bat"), SecretFilter(CmdString, "F"),
                       Encoding:=If(Encoding.Default.Equals(Encoding.UTF8), Encoding.UTF8, Encoding.GetEncoding("GB18030")))
                       Encoding:=If(Encoding.Default.Equals(Encoding.UTF8), Encoding.UTF8, Encoding.GetEncoding("GB18030")))
             If McLaunchLoader.Input.SaveBatch IsNot Nothing Then
             If McLaunchLoader.Input.SaveBatch IsNot Nothing Then
                 McLaunchLog("导出启动脚本完成,强制结束启动过程")
                 McLaunchLog("导出启动脚本完成,强制结束启动过程")
@@ -2058,7 +2160,7 @@ IgnoreCustomSkin:
         McLaunchLog("")
         McLaunchLog("")
         McLaunchLog("~ 基础参数 ~")
         McLaunchLog("~ 基础参数 ~")
         McLaunchLog("PCL 版本:" & VersionDisplayName & " (" & VersionCode & ")")
         McLaunchLog("PCL 版本:" & VersionDisplayName & " (" & VersionCode & ")")
-        McLaunchLog("游戏版本:" & McVersionCurrent.Version.ToString & "(" & McVersionCurrent.Version.McCodeMain & "." & McVersionCurrent.Version.McCodeSub & ")")
+        McLaunchLog("游戏版本:" & McVersionCurrent.Version.ToString & "(识别为 1." & McVersionCurrent.Version.McCodeMain & "." & McVersionCurrent.Version.McCodeSub & ")")
         McLaunchLog("资源版本:" & McAssetsGetIndexName(McVersionCurrent))
         McLaunchLog("资源版本:" & McAssetsGetIndexName(McVersionCurrent))
         McLaunchLog("版本继承:" & If(McVersionCurrent.InheritVersion = "", "无", McVersionCurrent.InheritVersion))
         McLaunchLog("版本继承:" & If(McVersionCurrent.InheritVersion = "", "无", McVersionCurrent.InheritVersion))
         McLaunchLog("分配的内存:" & PageVersionSetup.GetRam(McVersionCurrent, Not McLaunchJavaSelected.Is64Bit) & " GB(" & Math.Round(PageVersionSetup.GetRam(McVersionCurrent, Not McLaunchJavaSelected.Is64Bit) * 1024) & " MB)")
         McLaunchLog("分配的内存:" & PageVersionSetup.GetRam(McVersionCurrent, Not McLaunchJavaSelected.Is64Bit) & " GB(" & Math.Round(PageVersionSetup.GetRam(McVersionCurrent, Not McLaunchJavaSelected.Is64Bit) * 1024) & " MB)")
@@ -2068,6 +2170,7 @@ IgnoreCustomSkin:
         McLaunchLog("HMCL 格式:" & McVersionCurrent.IsHmclFormatJson)
         McLaunchLog("HMCL 格式:" & McVersionCurrent.IsHmclFormatJson)
         McLaunchLog("Java 信息:" & If(McLaunchJavaSelected IsNot Nothing, McLaunchJavaSelected.ToString, "无可用 Java"))
         McLaunchLog("Java 信息:" & If(McLaunchJavaSelected IsNot Nothing, McLaunchJavaSelected.ToString, "无可用 Java"))
         McLaunchLog("环境变量:" & If(McLaunchJavaSelected IsNot Nothing, If(McLaunchJavaSelected.HasEnvironment, "已设置", "未设置"), "未设置"))
         McLaunchLog("环境变量:" & If(McLaunchJavaSelected IsNot Nothing, If(McLaunchJavaSelected.HasEnvironment, "已设置", "未设置"), "未设置"))
+        McLaunchLog("Natives 文件夹:" & GetNativesFolder())
         McLaunchLog("")
         McLaunchLog("")
         McLaunchLog("~ 登录参数 ~")
         McLaunchLog("~ 登录参数 ~")
         McLaunchLog("玩家用户名:" & McLoginLoader.Output.Name)
         McLaunchLog("玩家用户名:" & McLoginLoader.Output.Name)

+ 95 - 78
Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb

@@ -1,4 +1,6 @@
-Public Module ModMinecraft
+Imports System.IO.Compression
+
+Public Module ModMinecraft
 
 
 #Region "文件夹"
 #Region "文件夹"
 
 
@@ -149,7 +151,7 @@
 
 
 #Region "版本处理"
 #Region "版本处理"
 
 
-    Public Const McVersionCacheVersion As Integer = 27
+    Public Const McVersionCacheVersion As Integer = 29
 
 
     Private _McVersionCurrent As McVersion
     Private _McVersionCurrent As McVersion
     Private _McVersionLast = 0 '为 0 以保证与 Nothing 不相同,使得 UI 显示可以正常初始化
     Private _McVersionLast = 0 '为 0 以保证与 Nothing 不相同,使得 UI 显示可以正常初始化
@@ -181,7 +183,7 @@
             If AniControlEnabled = 0 AndAlso
             If AniControlEnabled = 0 AndAlso
                Setup.Get("VersionServerAuthServer", Version:=value) <> Setup.Get("CacheAuthServerServer") AndAlso
                Setup.Get("VersionServerAuthServer", Version:=value) <> Setup.Get("CacheAuthServerServer") AndAlso
                Setup.Get("VersionServerLogin", Version:=value) = 4 Then
                Setup.Get("VersionServerLogin", Version:=value) = 4 Then
-                Setup.Set("CacheNideServer", "")
+                Setup.Set("CacheAuthAccess", "")
                 Log("[Launch] 服务器改变,要求重新登录 Authlib-Injector")
                 Log("[Launch] 服务器改变,要求重新登录 Authlib-Injector")
             End If
             End If
             If Setup.Get("VersionServerLogin", Version:=value) = 4 Then
             If Setup.Get("VersionServerLogin", Version:=value) = 4 Then
@@ -216,8 +218,8 @@
                     '尚未判断
                     '尚未判断
                     Dim ModFolder As New DirectoryInfo(Path & "mods\")
                     Dim ModFolder As New DirectoryInfo(Path & "mods\")
                     Dim SaveFolder As New DirectoryInfo(Path & "saves\")
                     Dim SaveFolder As New DirectoryInfo(Path & "saves\")
-                    If (ModFolder.Exists AndAlso ModFolder.EnumerateFiles.Count > 0) OrElse
-                       (SaveFolder.Exists AndAlso SaveFolder.EnumerateFiles.Count > 0) Then
+                    If (ModFolder.Exists AndAlso ModFolder.EnumerateFiles.Any) OrElse
+                       (SaveFolder.Exists AndAlso SaveFolder.EnumerateFiles.Any) Then
                         '自动开启
                         '自动开启
                         Setup.Set("VersionArgumentIndie", 1, Version:=Me)
                         Setup.Set("VersionArgumentIndie", 1, Version:=Me)
                         Log("[Setup] 已自动开启单版本隔离:" & Name)
                         Log("[Setup] 已自动开启单版本隔离:" & Name)
@@ -391,6 +393,29 @@
                             _Version.McName = JsonObject("jar").ToString
                             _Version.McName = JsonObject("jar").ToString
                             GoTo VersionSearchFinish
                             GoTo VersionSearchFinish
                         End If
                         End If
+                        '从 jar 文件的 version.json 中获取版本号
+                        If File.Exists(Path & Name & ".jar") Then
+                            Try
+                                Using JarArchive As New ZipArchive(New FileStream(Path & Name & ".jar", FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+                                    Dim VersionJson As ZipArchiveEntry = JarArchive.GetEntry("version.json")
+                                    If VersionJson IsNot Nothing Then
+                                        Using VersionJsonStream As New StreamReader(VersionJson.Open)
+                                            Dim VersionJsonObj As JObject = GetJson(VersionJsonStream.ReadToEnd)
+                                            If VersionJsonObj("id") IsNot Nothing Then
+                                                Dim VersionId As String = VersionJsonObj("id").ToString
+                                                If VersionId.Length < 32 Then '因为 wiki 说这玩意儿可能是个 hash,虽然我没发现
+                                                    _Version.McName = VersionId
+                                                    Log("[Minecraft] 从版本 jar 中的 version.json 获取到版本号:" & VersionId)
+                                                    GoTo VersionSearchFinish
+                                                End If
+                                            End If
+                                        End Using
+                                    End If
+                                End Using
+                            Catch ex As Exception
+                                Log(ex, "从版本 jar 中的 version.json 获取版本号失败")
+                            End Try
+                        End If
                         '非准确的版本判断警告
                         '非准确的版本判断警告
                         Log("[Minecraft] 无法完全确认 MC 版本号的版本:" & Name)
                         Log("[Minecraft] 无法完全确认 MC 版本号的版本:" & Name)
                         '从文件夹名中获取
                         '从文件夹名中获取
@@ -1034,7 +1059,7 @@ ExitDataLoad:
         Dim Path As String = Loader.Input
         Dim Path As String = Loader.Input
         Try
         Try
             '初始化
             '初始化
-            McVersionList.Clear()
+            McVersionList = New Dictionary(Of McVersionCardType, List(Of McVersion))
 
 
             '检测缓存是否需要更新
             '检测缓存是否需要更新
             Dim FolderList As New List(Of String)
             Dim FolderList As New List(Of String)
@@ -1075,10 +1100,10 @@ Reload:
 
 
             '改变当前选择的版本
             '改变当前选择的版本
 OnLoaded:
 OnLoaded:
-            If McVersionList.Any(Function(v) v.Key <> McVersionCardType.Error) Then '不能判断 Count = 0,这在只有错误版本时导致了误判
+            If McVersionList.Any(Function(v) v.Key <> McVersionCardType.Error) Then
                 '尝试读取已储存的选择
                 '尝试读取已储存的选择
                 Dim SavedSelection As String = ReadIni(Path & "PCL.ini", "Version")
                 Dim SavedSelection As String = ReadIni(Path & "PCL.ini", "Version")
-                If Not SavedSelection = "" Then
+                If SavedSelection <> "" Then
                     For Each Card As KeyValuePair(Of McVersionCardType, List(Of McVersion)) In McVersionList
                     For Each Card As KeyValuePair(Of McVersionCardType, List(Of McVersion)) In McVersionList
                         For Each Version As McVersion In Card.Value
                         For Each Version As McVersion In Card.Value
                             If Version.Name = SavedSelection AndAlso Not Version.State = McVersionState.Error Then
                             If Version.Name = SavedSelection AndAlso Not Version.State = McVersionState.Error Then
@@ -1190,7 +1215,7 @@ OnLoaded:
                     End Try
                     End Try
                 Next
                 Next
 
 
-                ResultVersionList.Add(CardType, VersionList)
+                If VersionList.Any Then ResultVersionList.Add(CardType, VersionList)
             Next
             Next
             Return ResultVersionList
             Return ResultVersionList
         Catch ex As Exception
         Catch ex As Exception
@@ -1200,10 +1225,14 @@ OnLoaded:
     End Function
     End Function
     Private Function McVersionListLoadNoCache(Path As String) As Dictionary(Of McVersionCardType, List(Of McVersion))
     Private Function McVersionListLoadNoCache(Path As String) As Dictionary(Of McVersionCardType, List(Of McVersion))
         Dim VersionList As New List(Of McVersion)
         Dim VersionList As New List(Of McVersion)
-        Dim ResultVersionList As New Dictionary(Of McVersionCardType, List(Of McVersion))
+
 #Region "循环加载每个版本的信息"
 #Region "循环加载每个版本的信息"
         Dim Dirs As DirectoryInfo() = New DirectoryInfo(Path & "versions").GetDirectories
         Dim Dirs As DirectoryInfo() = New DirectoryInfo(Path & "versions").GetDirectories
         For Each Folder As DirectoryInfo In Dirs
         For Each Folder As DirectoryInfo In Dirs
+            If Not Folder.EnumerateFiles.Any Then
+                Log("[Minecraft] 跳过空文件夹:" & Folder.FullName)
+                Continue For
+            End If
             If (Folder.Name = "cache" OrElse Folder.Name = "BLClient" OrElse Folder.Name = "PCL") AndAlso Not File.Exists(Folder.FullName & "\" & Folder.Name & ".json") Then
             If (Folder.Name = "cache" OrElse Folder.Name = "BLClient" OrElse Folder.Name = "PCL") AndAlso Not File.Exists(Folder.FullName & "\" & Folder.Name & ".json") Then
                 Log("[Minecraft] 跳过可能不是版本文件夹的项目:" & Folder.FullName)
                 Log("[Minecraft] 跳过可能不是版本文件夹的项目:" & Folder.FullName)
                 Continue For
                 Continue For
@@ -1223,6 +1252,9 @@ OnLoaded:
             Version.Load()
             Version.Load()
         Next
         Next
 #End Region
 #End Region
+
+        Dim ResultVersionList As New Dictionary(Of McVersionCardType, List(Of McVersion))
+
 #Region "将版本分类到各个卡片"
 #Region "将版本分类到各个卡片"
         Try
         Try
 
 
@@ -1234,9 +1266,9 @@ OnLoaded:
             For Each Version As McVersion In VersionList
             For Each Version As McVersion In VersionList
                 If Version.IsStar AndAlso Not Version.DisplayType = McVersionCardType.Hidden Then StaredVersions.Add(Version)
                 If Version.IsStar AndAlso Not Version.DisplayType = McVersionCardType.Hidden Then StaredVersions.Add(Version)
             Next
             Next
-            If StaredVersions.Count > 0 Then VersionListOriginal.Add(McVersionCardType.Star, StaredVersions)
+            If StaredVersions.Any Then VersionListOriginal.Add(McVersionCardType.Star, StaredVersions)
 
 
-            '预先筛选出愚人节的版本
+            '预先筛选出愚人节和错误的版本
             McVersionFilter(VersionList, VersionListOriginal, {McVersionState.Error}, McVersionCardType.Error)
             McVersionFilter(VersionList, VersionListOriginal, {McVersionState.Error}, McVersionCardType.Error)
             McVersionFilter(VersionList, VersionListOriginal, {McVersionState.Fool}, McVersionCardType.Fool)
             McVersionFilter(VersionList, VersionListOriginal, {McVersionState.Fool}, McVersionCardType.Fool)
 
 
@@ -1308,8 +1340,8 @@ OnLoaded:
 
 
             '将剩余的东西添加进去
             '将剩余的东西添加进去
             VersionRubbish.AddRange(VersionList)
             VersionRubbish.AddRange(VersionList)
-            If VersionUseful.Count > 0 Then VersionListOriginal.Add(McVersionCardType.OriginalLike, VersionUseful)
-            If VersionRubbish.Count > 0 Then VersionListOriginal.Add(McVersionCardType.Rubbish, VersionRubbish)
+            If VersionUseful.Any Then VersionListOriginal.Add(McVersionCardType.OriginalLike, VersionUseful)
+            If VersionRubbish.Any Then VersionListOriginal.Add(McVersionCardType.Rubbish, VersionRubbish)
 
 
             '按照自定义版本分类重新添加
             '按照自定义版本分类重新添加
             For Each VersionPair In VersionListOriginal
             For Each VersionPair In VersionListOriginal
@@ -1347,9 +1379,7 @@ OnLoaded:
             Next
             Next
             If Not IsNothing(Snapshot) Then OldList.Remove(Snapshot)
             If Not IsNothing(Snapshot) Then OldList.Remove(Snapshot)
             '按版本号排序
             '按版本号排序
-            Dim NewList As List(Of McVersion) = Sort(OldList, Function(Left As McVersion, Right As McVersion)
-                                                                  Return Left.Version.McCodeMain > Right.Version.McCodeMain
-                                                              End Function)
+            Dim NewList As List(Of McVersion) = Sort(OldList, Function(Left, Right) Left.Version.McCodeMain > Right.Version.McCodeMain)
             '回设
             '回设
             If Not IsNothing(Snapshot) Then NewList.Insert(0, Snapshot)
             If Not IsNothing(Snapshot) Then NewList.Insert(0, Snapshot)
             ResultVersionList(McVersionCardType.OriginalLike) = NewList
             ResultVersionList(McVersionCardType.OriginalLike) = NewList
@@ -1357,43 +1387,45 @@ OnLoaded:
 
 
         '不常用版本:按发布时间新旧排序,如果不可用则按名称排序
         '不常用版本:按发布时间新旧排序,如果不可用则按名称排序
         If ResultVersionList.ContainsKey(McVersionCardType.Rubbish) Then
         If ResultVersionList.ContainsKey(McVersionCardType.Rubbish) Then
-            ResultVersionList(McVersionCardType.Rubbish) = Sort(ResultVersionList(McVersionCardType.Rubbish), Function(Left As McVersion, Right As McVersion)
-                                                                                                                  Dim LeftYear As Integer = Left.ReleaseTime.Year '+ If(Left.State = McVersionState.Original OrElse Left.Version.HasOptiFine, 100, 0)
-                                                                                                                  Dim RightYear As Integer = Right.ReleaseTime.Year '+ If(Right.State = McVersionState.Original OrElse Left.Version.HasOptiFine, 100, 0)
-                                                                                                                  If LeftYear > 2000 AndAlso RightYear > 2000 Then
-                                                                                                                      If LeftYear <> RightYear Then
-                                                                                                                          Return LeftYear > RightYear
-                                                                                                                      Else
-                                                                                                                          Return Left.ReleaseTime > Right.ReleaseTime
-                                                                                                                      End If
-                                                                                                                  ElseIf LeftYear > 2000 AndAlso RightYear < 2000 Then
-                                                                                                                      Return True
-                                                                                                                  ElseIf LeftYear < 2000 AndAlso RightYear > 2000 Then
-                                                                                                                      Return False
-                                                                                                                  Else
-                                                                                                                      Return Left.Name > Right.Name
-                                                                                                                  End If
-                                                                                                              End Function)
+            ResultVersionList(McVersionCardType.Rubbish) = Sort(ResultVersionList(McVersionCardType.Rubbish),
+            Function(Left As McVersion, Right As McVersion)
+                Dim LeftYear As Integer = Left.ReleaseTime.Year '+ If(Left.State = McVersionState.Original OrElse Left.Version.HasOptiFine, 100, 0)
+                Dim RightYear As Integer = Right.ReleaseTime.Year '+ If(Right.State = McVersionState.Original OrElse Left.Version.HasOptiFine, 100, 0)
+                If LeftYear > 2000 AndAlso RightYear > 2000 Then
+                    If LeftYear <> RightYear Then
+                        Return LeftYear > RightYear
+                    Else
+                        Return Left.ReleaseTime > Right.ReleaseTime
+                    End If
+                ElseIf LeftYear > 2000 AndAlso RightYear < 2000 Then
+                    Return True
+                ElseIf LeftYear < 2000 AndAlso RightYear > 2000 Then
+                    Return False
+                Else
+                    Return Left.Name > Right.Name
+                End If
+            End Function)
         End If
         End If
 
 
         'API 版本:优先按版本排序,此后【先放 Fabric,再放 Forge(按版本号从高到低排序),最后放 LiteLoader(按名称排序)】
         'API 版本:优先按版本排序,此后【先放 Fabric,再放 Forge(按版本号从高到低排序),最后放 LiteLoader(按名称排序)】
         If ResultVersionList.ContainsKey(McVersionCardType.API) Then
         If ResultVersionList.ContainsKey(McVersionCardType.API) Then
-            ResultVersionList(McVersionCardType.API) = Sort(ResultVersionList(McVersionCardType.API), Function(Left As McVersion, Right As McVersion)
-                                                                                                          Dim Basic = VersionSortInteger(Left.Version.McName, Right.Version.McName)
-                                                                                                          If Basic <> 0 Then
-                                                                                                              Return Basic > 0
-                                                                                                          Else
-                                                                                                              If Left.Version.HasFabric Xor Right.Version.HasFabric Then
-                                                                                                                  Return Left.Version.HasFabric
-                                                                                                              ElseIf Left.Version.HasForge Xor Right.Version.HasForge Then
-                                                                                                                  Return Left.Version.HasForge
-                                                                                                              ElseIf Not Left.Version.SortCode <> Right.Version.SortCode Then
-                                                                                                                  Return Left.Version.SortCode > Right.Version.SortCode
-                                                                                                              Else
-                                                                                                                  Return Left.Name > Right.Name
-                                                                                                              End If
-                                                                                                          End If
-                                                                                                      End Function)
+            ResultVersionList(McVersionCardType.API) = Sort(ResultVersionList(McVersionCardType.API),
+            Function(Left As McVersion, Right As McVersion)
+                Dim Basic = VersionSortInteger(Left.Version.McName, Right.Version.McName)
+                If Basic <> 0 Then
+                    Return Basic > 0
+                Else
+                    If Left.Version.HasFabric Xor Right.Version.HasFabric Then
+                        Return Left.Version.HasFabric
+                    ElseIf Left.Version.HasForge Xor Right.Version.HasForge Then
+                        Return Left.Version.HasForge
+                    ElseIf Not Left.Version.SortCode <> Right.Version.SortCode Then
+                        Return Left.Version.SortCode > Right.Version.SortCode
+                    Else
+                        Return Left.Name > Right.Name
+                    End If
+                End If
+            End Function)
         End If
         End If
 
 
 #End Region
 #End Region
@@ -1417,19 +1449,9 @@ OnLoaded:
     ''' <param name="Formula">需要筛选出的版本类型。-2 代表隐藏的版本。</param>
     ''' <param name="Formula">需要筛选出的版本类型。-2 代表隐藏的版本。</param>
     ''' <param name="CardType">卡片的名称。</param>
     ''' <param name="CardType">卡片的名称。</param>
     Private Sub McVersionFilter(ByRef VersionList As List(Of McVersion), ByRef Target As Dictionary(Of McVersionCardType, List(Of McVersion)), Formula As McVersionState(), CardType As McVersionCardType)
     Private Sub McVersionFilter(ByRef VersionList As List(Of McVersion), ByRef Target As Dictionary(Of McVersionCardType, List(Of McVersion)), Formula As McVersionState(), CardType As McVersionCardType)
-        '将某种版本筛选出来
-        Dim KeepList As New List(Of McVersion)
-        For Each Version As McVersion In VersionList
-            For Each Type As Integer In Formula
-                If Type = Version.State Then
-                    KeepList.Add(Version)
-                    GoTo NextVersion
-                End If
-            Next
-NextVersion:
-        Next
+        Dim KeepList = VersionList.Where(Function(v) Formula.Contains(v.State)).ToList
         '加入版本列表,并从剩余中删除
         '加入版本列表,并从剩余中删除
-        If KeepList.Count > 0 Then
+        If KeepList.Any Then
             Target.Add(CardType, KeepList)
             Target.Add(CardType, KeepList)
             For Each Version As McVersion In KeepList
             For Each Version As McVersion In KeepList
                 VersionList.Remove(Version)
                 VersionList.Remove(Version)
@@ -1443,14 +1465,9 @@ NextVersion:
     ''' <param name="Formula">需要筛选出的版本类型。-2 代表隐藏的版本。</param>
     ''' <param name="Formula">需要筛选出的版本类型。-2 代表隐藏的版本。</param>
     ''' <param name="KeepList">传入需要增加入的列表。</param>
     ''' <param name="KeepList">传入需要增加入的列表。</param>
     Private Sub McVersionFilter(ByRef VersionList As List(Of McVersion), Formula As McVersionState(), ByRef KeepList As List(Of McVersion))
     Private Sub McVersionFilter(ByRef VersionList As List(Of McVersion), Formula As McVersionState(), ByRef KeepList As List(Of McVersion))
-        For Each Version As McVersion In VersionList
-            For Each Type As McVersionState In Formula
-                If Type <> Version.State Then Continue For
-                KeepList.Add(Version) : Exit For
-            Next
-NextVersion:
-        Next
-        If KeepList.Count > 0 Then
+        KeepList.AddRange(VersionList.Where(Function(v) Formula.Contains(v.State)))
+        '加入版本列表,并从剩余中删除
+        If KeepList.Any Then
             For Each Version As McVersion In KeepList
             For Each Version As McVersion In KeepList
                 VersionList.Remove(Version)
                 VersionList.Remove(Version)
             Next
             Next
@@ -1515,7 +1532,7 @@ NextVersion:
     ''' </summary>
     ''' </summary>
     Public Function McSkinGetAddress(Uuid As String, Type As String) As String
     Public Function McSkinGetAddress(Uuid As String, Type As String) As String
         If Uuid = "" Then Throw New Exception("Uuid 为空。")
         If Uuid = "" Then Throw New Exception("Uuid 为空。")
-        If Uuid.StartsWithF("000000") Then Throw New Exception("离线 Uuid 无正版皮肤文件。")
+        If Uuid.StartsWithF("00000") Then Throw New Exception("离线 Uuid 无正版皮肤文件。")
         '尝试读取缓存
         '尝试读取缓存
         Dim CacheSkinAddress As String = ReadIni(PathTemp & "Cache\Skin\Index" & Type & ".ini", Uuid)
         Dim CacheSkinAddress As String = ReadIni(PathTemp & "Cache\Skin\Index" & Type & ".ini", Uuid)
         If Not CacheSkinAddress = "" Then Return CacheSkinAddress
         If Not CacheSkinAddress = "" Then Return CacheSkinAddress
@@ -2001,7 +2018,7 @@ NextVersion:
             Result.Add(New NetFile(Urls.Distinct.ToArray, Token.LocalPath, Checker))
             Result.Add(New NetFile(Urls.Distinct.ToArray, Token.LocalPath, Checker))
         Next
         Next
         '去重并返回
         '去重并返回
-        Return Distinct(Result, Function(a, b) a.LocalPath = b.LocalPath)
+        Return Result.Distinct(Function(a, b) a.LocalPath = b.LocalPath)
     End Function
     End Function
     ''' <summary>
     ''' <summary>
     ''' 获取对应的支持库文件地址。
     ''' 获取对应的支持库文件地址。
@@ -2048,8 +2065,7 @@ NextVersion:
                 If Version.InheritVersion = "" Then Exit Do
                 If Version.InheritVersion = "" Then Exit Do
                 Version = New McVersion(PathMcFolder & "versions\" & Version.InheritVersion)
                 Version = New McVersion(PathMcFolder & "versions\" & Version.InheritVersion)
             Loop
             Loop
-        Catch ex As Exception
-            Log(ex, "获取资源文件索引下载地址失败")
+        Catch
         End Try
         End Try
         '无法获取到下载地址
         '无法获取到下载地址
         If ReturnLegacyOnError Then
         If ReturnLegacyOnError Then
@@ -2239,10 +2255,11 @@ NextVersion:
             '弹窗结果
             '弹窗结果
             If MsgResult = 2 Then
             If MsgResult = 2 Then
                 '下载
                 '下载
-                RunInUi(Sub()
-                            PageDownloadInstall.McVersionWaitingForSelect = VersionName
-                            FrmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadInstall)
-                        End Sub)
+                RunInUi(
+                Sub()
+                    PageDownloadInstall.McVersionWaitingForSelect = VersionName
+                    FrmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadInstall)
+                End Sub)
             End If
             End If
 
 
         Catch ex As Exception
         Catch ex As Exception

+ 17 - 16
Plain Craft Launcher 2/Modules/Minecraft/ModMod.vb

@@ -337,7 +337,7 @@ Public Module ModMod
                     For Each Token In AuthorJson
                     For Each Token In AuthorJson
                         Author.Add(Token.ToString)
                         Author.Add(Token.ToString)
                     Next
                     Next
-                    If Author.Count > 0 Then Authors = Join(Author, ", ")
+                    If Author.Any Then Authors = Join(Author, ", ")
                 End If
                 End If
                 Dim Reqs As JArray = InfoObject("requiredMods")
                 Dim Reqs As JArray = InfoObject("requiredMods")
                 If Reqs IsNot Nothing Then
                 If Reqs IsNot Nothing Then
@@ -393,7 +393,7 @@ GotFabric:
                     For Each Token In AuthorJson
                     For Each Token In AuthorJson
                         Author.Add(Token.ToString)
                         Author.Add(Token.ToString)
                     Next
                     Next
-                    If Author.Count > 0 Then Authors = Join(Author, ", ")
+                    If Author.Any Then Authors = Join(Author, ", ")
                 End If
                 End If
                 'If (Not FabricObject.ContainsKey("serverSideOnly")) OrElse FabricObject("serverSideOnly")("value").ToObject(Of Boolean) = False Then
                 'If (Not FabricObject.ContainsKey("serverSideOnly")) OrElse FabricObject("serverSideOnly")("value").ToObject(Of Boolean) = False Then
                 '    '添加 Minecraft 依赖
                 '    '添加 Minecraft 依赖
@@ -438,7 +438,7 @@ GotFabric:
                         Line = Line.Substring(0, Line.IndexOfF("#"))
                         Line = Line.Substring(0, Line.IndexOfF("#"))
                     End If
                     End If
                     Line = Line.Trim(New Char() {" "c, "	"c, " "c}) '去除头尾的空格
                     Line = Line.Trim(New Char() {" "c, "	"c, " "c}) '去除头尾的空格
-                    If Line.Count > 0 Then Lines.Add(Line) '去除空行
+                    If Line.Any Then Lines.Add(Line) '去除空行
                 Next
                 Next
                 '读取文件数据
                 '读取文件数据
                 Dim TomlData As New List(Of KeyValuePair(Of String, Dictionary(Of String, Object))) From {New KeyValuePair(Of String, Dictionary(Of String, Object))("", New Dictionary(Of String, Object))}
                 Dim TomlData As New List(Of KeyValuePair(Of String, Dictionary(Of String, Object))) From {New KeyValuePair(Of String, Dictionary(Of String, Object))("", New Dictionary(Of String, Object))}
@@ -887,11 +887,11 @@ Finished:
                 ModList.Add(ModEntry)
                 ModList.Add(ModEntry)
                 '读取 Comp 缓存
                 '读取 Comp 缓存
                 If ModEntry.State = McMod.McModState.Unavaliable Then Continue For
                 If ModEntry.State = McMod.McModState.Unavaliable Then Continue For
-                Dim Hash = ModEntry.ModrinthHash
-                If Cache.ContainsKey(Hash) Then
-                    ModEntry.FromJson(Cache(Hash))
+                Dim CacheKey = ModEntry.ModrinthHash & PageVersionLeft.Version.Version.McName
+                If Cache.ContainsKey(CacheKey) Then
+                    ModEntry.FromJson(Cache(CacheKey))
                     '如果缓存中的信息在 6 小时以内更新过,则无需重新获取
                     '如果缓存中的信息在 6 小时以内更新过,则无需重新获取
-                    If ModEntry.CompLoaded AndAlso Date.Now - Cache(Hash)("Comp")("CacheTime").ToObject(Of Date) < New TimeSpan(6, 0, 0) Then Continue For
+                    If ModEntry.CompLoaded AndAlso Date.Now - Cache(CacheKey)("Comp")("CacheTime").ToObject(Of Date) < New TimeSpan(6, 0, 0) Then Continue For
                 End If
                 End If
                 ModUpdateList.Add(ModEntry)
                 ModUpdateList.Add(ModEntry)
             Next
             Next
@@ -925,7 +925,7 @@ Finished:
     End Sub
     End Sub
 
 
     '联网加载 Mod 详情
     '联网加载 Mod 详情
-    Private Const LocalModCacheVersion As Integer = 2
+    Private Const LocalModCacheVersion As Integer = 4
     Public McModDetailLoader As New LoaderTask(Of KeyValuePair(Of List(Of McMod), JObject), Integer)("Mod List Detail Loader", AddressOf McModDetailLoad)
     Public McModDetailLoader As New LoaderTask(Of KeyValuePair(Of List(Of McMod), JObject), Integer)("Mod List Detail Loader", AddressOf McModDetailLoad)
     Private Sub McModDetailLoad(Loader As LoaderTask(Of KeyValuePair(Of List(Of McMod), JObject), Integer))
     Private Sub McModDetailLoad(Loader As LoaderTask(Of KeyValuePair(Of List(Of McMod), JObject), Integer))
         Dim Mods As List(Of McMod) = Loader.Input.Key
         Dim Mods As List(Of McMod) = Loader.Input.Key
@@ -937,7 +937,7 @@ Finished:
         If TargetMcVersion.HasFabric Then ModLoaders.Add(CompModLoaderType.Fabric)
         If TargetMcVersion.HasFabric Then ModLoaders.Add(CompModLoaderType.Fabric)
         If TargetMcVersion.HasLiteLoader Then ModLoaders.Add(CompModLoaderType.LiteLoader)
         If TargetMcVersion.HasLiteLoader Then ModLoaders.Add(CompModLoaderType.LiteLoader)
         If Not ModLoaders.Any() Then ModLoaders.AddRange({CompModLoaderType.Forge, CompModLoaderType.Fabric, CompModLoaderType.LiteLoader, CompModLoaderType.Quilt})
         If Not ModLoaders.Any() Then ModLoaders.AddRange({CompModLoaderType.Forge, CompModLoaderType.Fabric, CompModLoaderType.LiteLoader, CompModLoaderType.Quilt})
-        Dim McVersions As New List(Of String) From {TargetMcVersion.McName}
+        Dim McVersion = TargetMcVersion.McName
         '暂不向下扩展检查的 MC 小版本
         '暂不向下扩展检查的 MC 小版本
         '例如:Mod 在更新 1.16.5 后,对早期的 1.16.2 版本发布了修补补丁,这会导致 PCL 将 1.16.5 版本的 Mod 降级到 1.16.2
         '例如:Mod 在更新 1.16.5 后,对早期的 1.16.2 版本发布了修补补丁,这会导致 PCL 将 1.16.5 版本的 Mod 降级到 1.16.2
         'If TargetMcVersion.McCodeMain > 0 AndAlso TargetMcVersion.McCodeMain < 99 Then
         'If TargetMcVersion.McCodeMain > 0 AndAlso TargetMcVersion.McCodeMain < 99 Then
@@ -948,7 +948,7 @@ Finished:
         'End If
         'End If
         'McVersions = McVersions.Distinct().ToList()
         'McVersions = McVersions.Distinct().ToList()
         '开始网络获取
         '开始网络获取
-        Log($"[Mod] 目标加载器:{ModLoaders.Join("/")},版本:{McVersions.Join("/")}")
+        Log($"[Mod] 目标加载器:{ModLoaders.Join("/")},版本:{McVersion}")
         Dim EndedThreadCount As Integer = 0, SucceedThreadCount As Integer = 0
         Dim EndedThreadCount As Integer = 0, SucceedThreadCount As Integer = 0
         Dim MainThread As Thread = Thread.CurrentThread
         Dim MainThread As Thread = Thread.CurrentThread
         '从 Modrinth 获取信息
         '从 Modrinth 获取信息
@@ -965,6 +965,7 @@ Finished:
                 Dim ModrinthMapping As New Dictionary(Of String, List(Of McMod))
                 Dim ModrinthMapping As New Dictionary(Of String, List(Of McMod))
                 For Each Entry In Mods
                 For Each Entry In Mods
                     If Not ModrinthVersion.ContainsKey(Entry.ModrinthHash) Then Continue For
                     If Not ModrinthVersion.ContainsKey(Entry.ModrinthHash) Then Continue For
+                    If ModrinthVersion(Entry.ModrinthHash)("files")(0)("hashes")("sha1") <> Entry.ModrinthHash Then Continue For
                     Dim ProjectId = ModrinthVersion(Entry.ModrinthHash)("project_id").ToString
                     Dim ProjectId = ModrinthVersion(Entry.ModrinthHash)("project_id").ToString
                     If CompProjectCache.ContainsKey(ProjectId) AndAlso Entry.Comp Is Nothing Then Entry.Comp = CompProjectCache(ProjectId) '读取已加载的缓存,加快结果出现速度
                     If CompProjectCache.ContainsKey(ProjectId) AndAlso Entry.Comp Is Nothing Then Entry.Comp = CompProjectCache(ProjectId) '读取已加载的缓存,加快结果出现速度
                     If Not ModrinthMapping.ContainsKey(ProjectId) Then ModrinthMapping(ProjectId) = New List(Of McMod)
                     If Not ModrinthMapping.ContainsKey(ProjectId) Then ModrinthMapping(ProjectId) = New List(Of McMod)
@@ -990,8 +991,8 @@ Finished:
                 Log($"[Mod] 已从 Modrinth 获取本地 Mod 信息,继续获取更新信息")
                 Log($"[Mod] 已从 Modrinth 获取本地 Mod 信息,继续获取更新信息")
                 '步骤 4:获取更新信息
                 '步骤 4:获取更新信息
                 Dim ModrinthUpdate = CType(GetJson(NetRequestRetry("https://api.modrinth.com/v2/version_files/update", "POST",
                 Dim ModrinthUpdate = CType(GetJson(NetRequestRetry("https://api.modrinth.com/v2/version_files/update", "POST",
-                    $"{{""hashes"": [""{ModrinthHashes.Join(""",""")}""], ""algorithm"": ""sha1"", 
-                    ""loaders"": [""{ModLoaders.Join(""",""").ToLower}""],""game_versions"": [""{McVersions.Join(""",""")}""]}}", "application/json")), JObject)
+                    $"{{""hashes"": [""{ModrinthMapping.SelectMany(Function(l) l.Value.Select(Function(m) m.ModrinthHash)).Join(""",""")}""], ""algorithm"": ""sha1"", 
+                    ""loaders"": [""{ModLoaders.Join(""",""").ToLower}""],""game_versions"": [""{McVersion}""]}}", "application/json")), JObject)
                 For Each Entry In Mods
                 For Each Entry In Mods
                     If Not ModrinthUpdate.ContainsKey(Entry.ModrinthHash) OrElse Entry.CompFile Is Nothing Then Continue For
                     If Not ModrinthUpdate.ContainsKey(Entry.ModrinthHash) OrElse Entry.CompFile Is Nothing Then Continue For
                     Dim UpdateFile As New CompFile(ModrinthUpdate(Entry.ModrinthHash), CompType.Mod)
                     Dim UpdateFile As New CompFile(ModrinthUpdate(Entry.ModrinthHash), CompType.Mod)
@@ -1000,11 +1001,11 @@ Finished:
                     If Entry.CompFile.ReleaseDate >= UpdateFile.ReleaseDate OrElse Entry.CompFile.Hash = UpdateFile.Hash Then Continue For
                     If Entry.CompFile.ReleaseDate >= UpdateFile.ReleaseDate OrElse Entry.CompFile.Hash = UpdateFile.Hash Then Continue For
                     '设置更新日志与更新文件
                     '设置更新日志与更新文件
                     If Entry.UpdateFile IsNot Nothing AndAlso UpdateFile.Hash = Entry.UpdateFile.Hash Then '合并
                     If Entry.UpdateFile IsNot Nothing AndAlso UpdateFile.Hash = Entry.UpdateFile.Hash Then '合并
-                        Entry.ChangelogUrls.Add($"https://modrinth.com/mod/{ModrinthUpdate(Entry.ModrinthHash)("project_id")}/changelog?{McVersions.Select(Function(v) $"g={v}&").Join("")}")
+                        Entry.ChangelogUrls.Add($"https://modrinth.com/mod/{ModrinthUpdate(Entry.ModrinthHash)("project_id")}/changelog?g={McVersion}")
                         UpdateFile.DownloadUrls.AddRange(Entry.UpdateFile.DownloadUrls) '合并下载源
                         UpdateFile.DownloadUrls.AddRange(Entry.UpdateFile.DownloadUrls) '合并下载源
                         Entry.UpdateFile = UpdateFile '优先使用 Modrinth 的文件
                         Entry.UpdateFile = UpdateFile '优先使用 Modrinth 的文件
                     ElseIf Entry.UpdateFile Is Nothing OrElse UpdateFile.ReleaseDate >= Entry.UpdateFile.ReleaseDate Then '替换
                     ElseIf Entry.UpdateFile Is Nothing OrElse UpdateFile.ReleaseDate >= Entry.UpdateFile.ReleaseDate Then '替换
-                        Entry.ChangelogUrls = New List(Of String) From {$"https://modrinth.com/mod/{ModrinthUpdate(Entry.ModrinthHash)("project_id")}/changelog?{McVersions.Select(Function(v) $"g={v}&").Join("")}"}
+                        Entry.ChangelogUrls = New List(Of String) From {$"https://modrinth.com/mod/{ModrinthUpdate(Entry.ModrinthHash)("project_id")}/changelog?g={McVersion}"}
                         Entry.UpdateFile = UpdateFile
                         Entry.UpdateFile = UpdateFile
                     End If
                     End If
                 Next
                 Next
@@ -1072,7 +1073,7 @@ Finished:
                         For Each IndexEntry In ProjectJson("latestFilesIndexes")
                         For Each IndexEntry In ProjectJson("latestFilesIndexes")
                             If ModLoaders.Single <> IndexEntry("modLoader")?.ToObject(Of Integer) Then Continue For 'ModLoader 唯一且匹配
                             If ModLoaders.Single <> IndexEntry("modLoader")?.ToObject(Of Integer) Then Continue For 'ModLoader 唯一且匹配
                             Dim IndexVersion As String = IndexEntry("gameVersion")
                             Dim IndexVersion As String = IndexEntry("gameVersion")
-                            If Not McVersions.Contains(IndexVersion) Then Continue For 'MC 版本匹配
+                            If McVersion <> IndexVersion Then Continue For 'MC 版本匹配
                             '由于 latestFilesIndexes 是按时间从新到老排序的,所以只需取第一个;如果需要检查多个 releaseType 下的文件,将 > -1 改为 = 1,但这应当并不会获取到更新的文件
                             '由于 latestFilesIndexes 是按时间从新到老排序的,所以只需取第一个;如果需要检查多个 releaseType 下的文件,将 > -1 改为 = 1,但这应当并不会获取到更新的文件
                             If NewestVersion IsNot Nothing AndAlso VersionSortInteger(NewestVersion, IndexVersion) > -1 Then Continue For '只保留最新 MC 版本
                             If NewestVersion IsNot Nothing AndAlso VersionSortInteger(NewestVersion, IndexVersion) > -1 Then Continue For '只保留最新 MC 版本
                             If NewestVersion <> IndexVersion Then
                             If NewestVersion <> IndexVersion Then
@@ -1135,7 +1136,7 @@ Finished:
         If Not Mods.Any() Then Exit Sub
         If Not Mods.Any() Then Exit Sub
         For Each Entry In Mods 'TODO: Bookshelf 的 Logo 会在两个网站间横跳
         For Each Entry In Mods 'TODO: Bookshelf 的 Logo 会在两个网站间横跳
             Entry.CompLoaded = SucceedThreadCount = 2
             Entry.CompLoaded = SucceedThreadCount = 2
-            Cache(Entry.ModrinthHash) = Entry.ToJson()
+            Cache(Entry.ModrinthHash & McVersion) = Entry.ToJson()
         Next
         Next
         WriteFile(PathTemp & "Cache\LocalMod.json", Cache.ToString(If(ModeDebug, Newtonsoft.Json.Formatting.Indented, Newtonsoft.Json.Formatting.None)))
         WriteFile(PathTemp & "Cache\LocalMod.json", Cache.ToString(If(ModeDebug, Newtonsoft.Json.Formatting.Indented, Newtonsoft.Json.Formatting.None)))
     End Sub
     End Sub

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

@@ -244,16 +244,17 @@ Retry:
             ModList.Add(ModEntry("fileID"))
             ModList.Add(ModEntry("fileID"))
             If ModEntry("required") IsNot Nothing AndAlso Not ModEntry("required").ToObject(Of Boolean) Then ModOptionalList.Add(ModEntry("fileID"))
             If ModEntry("required") IsNot Nothing AndAlso Not ModEntry("required").ToObject(Of Boolean) Then ModOptionalList.Add(ModEntry("fileID"))
         Next
         Next
-        If ModList.Count > 0 Then
+        If ModList.Any Then
+            Dim ModDownloadLoaders As New List(Of LoaderBase)
             '获取 Mod 下载信息
             '获取 Mod 下载信息
-            InstallLoaders.Add(New LoaderTask(Of Integer, JArray)("获取 Mod 下载信息",
+            ModDownloadLoaders.Add(New LoaderTask(Of Integer, JArray)("获取 Mod 下载信息",
             Sub(Task As LoaderTask(Of Integer, JArray))
             Sub(Task As LoaderTask(Of Integer, JArray))
                 Task.Output = GetJson(NetRequestRetry("https://api.curseforge.com/v1/mods/files", "POST", "{""fileIds"": [" & Join(ModList, ",") & "]}", "application/json"))("data")
                 Task.Output = GetJson(NetRequestRetry("https://api.curseforge.com/v1/mods/files", "POST", "{""fileIds"": [" & Join(ModList, ",") & "]}", "application/json"))("data")
                 '如果文件已被删除,则 API 会跳过那一项
                 '如果文件已被删除,则 API 会跳过那一项
                 If ModList.Count > Task.Output.Count Then Throw New Exception("整合包所需要的部分 Mod 版本已被 Mod 作者删除,因此无法完成整合包安装,请联系整合包作者更新整合包中的 Mod 版本")
                 If ModList.Count > Task.Output.Count Then Throw New Exception("整合包所需要的部分 Mod 版本已被 Mod 作者删除,因此无法完成整合包安装,请联系整合包作者更新整合包中的 Mod 版本")
             End Sub) With {.ProgressWeight = ModList.Count / 10}) '每 10 Mod 需要 1s
             End Sub) With {.ProgressWeight = ModList.Count / 10}) '每 10 Mod 需要 1s
             '构造 NetFile
             '构造 NetFile
-            InstallLoaders.Add(New LoaderTask(Of JArray, List(Of NetFile))("构造 Mod 下载信息",
+            ModDownloadLoaders.Add(New LoaderTask(Of JArray, List(Of NetFile))("构造 Mod 下载信息",
             Sub(Task As LoaderTask(Of JArray, List(Of NetFile)))
             Sub(Task As LoaderTask(Of JArray, List(Of NetFile)))
                 Dim FileList As New Dictionary(Of Integer, NetFile)
                 Dim FileList As New Dictionary(Of Integer, NetFile)
                 For Each ModJson In Task.Input
                 For Each ModJson In Task.Input
@@ -288,7 +289,10 @@ Retry:
                 Task.Output = FileList.Values.ToList
                 Task.Output = FileList.Values.ToList
             End Sub) With {.ProgressWeight = ModList.Count / 200, .Show = False}) '每 200 Mod 需要 1s
             End Sub) With {.ProgressWeight = ModList.Count / 200, .Show = False}) '每 200 Mod 需要 1s
             '下载 Mod 文件
             '下载 Mod 文件
-            InstallLoaders.Add(New LoaderDownload("下载 Mod", New List(Of NetFile)) With {.ProgressWeight = ModList.Count * 1.5}) '每个 Mod 需要 1.5s
+            ModDownloadLoaders.Add(New LoaderDownload("下载 Mod", New List(Of NetFile)) With {.ProgressWeight = ModList.Count * 1.5}) '每个 Mod 需要 1.5s
+            '构造加载器
+            InstallLoaders.Add(New LoaderCombo(Of Integer)("下载 Mod(主加载器)", ModDownloadLoaders) With
+                {.Show = False, .ProgressWeight = ModDownloadLoaders.Sum(Function(l) l.ProgressWeight)})
         End If
         End If
 
 
         '构造加载器
         '构造加载器
@@ -299,43 +303,35 @@ Retry:
             .ForgeVersion = ForgeVersion,
             .ForgeVersion = ForgeVersion,
             .FabricVersion = FabricVersion
             .FabricVersion = FabricVersion
         }
         }
-        Dim InstallExpectTime As Double = 0
-        For Each InstallLoader In InstallLoaders
-            InstallExpectTime += InstallLoader.ProgressWeight
-        Next
         Dim MergeLoaders As List(Of LoaderBase) = McInstallLoader(Request, True)
         Dim MergeLoaders As List(Of LoaderBase) = McInstallLoader(Request, True)
         If MergeLoaders Is Nothing Then Exit Sub
         If MergeLoaders Is Nothing Then Exit Sub
-        Dim MergeExpectTime As Double = 0
-        For Each MergeLoader In MergeLoaders
-            MergeExpectTime += MergeLoader.ProgressWeight
-        Next
-        '构造 Libraries 加载器(为了使得 Mods 下载结束后再构造,这样才会下载 JumpLoader 文件)
+        '构造 Libraries 加载器
         Dim LoadersLib As New List(Of LoaderBase)
         Dim LoadersLib As New List(Of LoaderBase)
         LoadersLib.Add(New LoaderTask(Of String, List(Of NetFile))("分析游戏支持库文件(副加载器)", Sub(Task As LoaderTask(Of String, List(Of NetFile))) Task.Output = McLibFix(New McVersion(VersionName))) With {.ProgressWeight = 1, .Show = False})
         LoadersLib.Add(New LoaderTask(Of String, List(Of NetFile))("分析游戏支持库文件(副加载器)", Sub(Task As LoaderTask(Of String, List(Of NetFile))) Task.Output = McLibFix(New McVersion(VersionName))) With {.ProgressWeight = 1, .Show = False})
         LoadersLib.Add(New LoaderDownload("下载游戏支持库文件(副加载器)", New List(Of NetFile)) With {.ProgressWeight = 7, .Show = False})
         LoadersLib.Add(New LoaderDownload("下载游戏支持库文件(副加载器)", New List(Of NetFile)) With {.ProgressWeight = 7, .Show = False})
         '构造总加载器
         '构造总加载器
         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 = InstallExpectTime})
-        Loaders.Add(New LoaderCombo(Of String)("游戏安装", MergeLoaders) With {.Show = False, .ProgressWeight = MergeExpectTime})
+        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)("游戏安装", MergeLoaders) With {.Show = False, .ProgressWeight = MergeLoaders.Sum(Function(l) l.ProgressWeight)})
         Loaders.Add(New LoaderCombo(Of String)("下载游戏支持库文件", LoadersLib) With {.ProgressWeight = 8})
         Loaders.Add(New LoaderCombo(Of String)("下载游戏支持库文件", LoadersLib) With {.ProgressWeight = 8})
         Loaders.Add(New LoaderTask(Of String, String)("最终整理文件",
         Loaders.Add(New LoaderTask(Of String, String)("最终整理文件",
-                                                      Sub(Task As LoaderTask(Of String, String))
-                                                          '设置图标
-                                                          Dim VersionFolder As String = PathMcFolder & "versions\" & VersionName & "\"
-                                                          If Logo IsNot Nothing AndAlso File.Exists(Logo) Then
-                                                              File.Copy(Logo, VersionFolder & "PCL\Logo.png", True)
-                                                              WriteIni(VersionFolder & "PCL\Setup.ini", "Logo", "PCL\Logo.png")
-                                                              WriteIni(VersionFolder & "PCL\Setup.ini", "LogoCustom", "True")
-                                                              Log("[ModPack] 已设置整合包 Logo:" & Logo)
-                                                          End If
-                                                          '删除原始整合包文件
-                                                          For Each Target As String In {VersionFolder & "原始整合包.zip", VersionFolder & "原始整合包.mrpack"}
-                                                              If Not Setup.Get("ToolDownloadKeepModpack") AndAlso File.Exists(Target) Then
-                                                                  Log("[ModPack] 根据设置要求删除原始整合包文件:" & Target)
-                                                                  File.Delete(Target)
-                                                              End If
-                                                          Next
-                                                      End Sub) With {.ProgressWeight = 0.1, .Show = False})
+        Sub(Task As LoaderTask(Of String, String))
+            '设置图标
+            Dim VersionFolder As String = PathMcFolder & "versions\" & VersionName & "\"
+            If Logo IsNot Nothing AndAlso File.Exists(Logo) Then
+                File.Copy(Logo, VersionFolder & "PCL\Logo.png", True)
+                WriteIni(VersionFolder & "PCL\Setup.ini", "Logo", "PCL\Logo.png")
+                WriteIni(VersionFolder & "PCL\Setup.ini", "LogoCustom", "True")
+                Log("[ModPack] 已设置整合包 Logo:" & Logo)
+            End If
+            '删除原始整合包文件
+            For Each Target As String In {VersionFolder & "原始整合包.zip", VersionFolder & "原始整合包.mrpack"}
+                If Not Setup.Get("ToolDownloadKeepModpack") AndAlso File.Exists(Target) Then
+                    Log("[ModPack] 根据设置要求删除原始整合包文件:" & Target)
+                    File.Delete(Target)
+                End If
+            Next
+        End Sub) With {.ProgressWeight = 0.1, .Show = False})
 
 
         '重复任务检查
         '重复任务检查
         Dim LoaderName As String = "CurseForge 整合包安装:" & VersionName & " "
         Dim LoaderName As String = "CurseForge 整合包安装:" & VersionName & " "
@@ -438,10 +434,10 @@ Retry:
             End If
             End If
             '添加下载文件
             '添加下载文件
             FileList.Add(New NetFile(File("downloads").Select(Function(t) t.ToString.Replace("://edge.forgecdn", "://media.forgecdn")).ToArray, '修复 #2390
             FileList.Add(New NetFile(File("downloads").Select(Function(t) t.ToString.Replace("://edge.forgecdn", "://media.forgecdn")).ToArray, '修复 #2390
-                                     PathMcFolder & "versions\" & VersionName & "\" & File("path").ToString,
-                                     New FileChecker(ActualSize:=File("fileSize").ToObject(Of Long), Hash:=File("hashes")("sha1").ToString), True))
+                PathMcFolder & "versions\" & VersionName & "\" & File("path").ToString,
+                New FileChecker(ActualSize:=File("fileSize").ToObject(Of Long), Hash:=File("hashes")("sha1").ToString), True))
         Next
         Next
-        If FileList.Count > 0 Then
+        If FileList.Any Then
             InstallLoaders.Add(New LoaderDownload("下载额外文件", FileList) With {.ProgressWeight = FileList.Count * 1.5}) '每个 Mod 需要 1.5s
             InstallLoaders.Add(New LoaderDownload("下载额外文件", FileList) With {.ProgressWeight = FileList.Count * 1.5}) '每个 Mod 需要 1.5s
         End If
         End If
 
 
@@ -453,43 +449,35 @@ Retry:
             .ForgeVersion = ForgeVersion,
             .ForgeVersion = ForgeVersion,
             .FabricVersion = FabricVersion
             .FabricVersion = FabricVersion
         }
         }
-        Dim InstallExpectTime As Double = 0
-        For Each InstallLoader In InstallLoaders
-            InstallExpectTime += InstallLoader.ProgressWeight
-        Next
         Dim MergeLoaders As List(Of LoaderBase) = McInstallLoader(Request, True)
         Dim MergeLoaders As List(Of LoaderBase) = McInstallLoader(Request, True)
         If MergeLoaders Is Nothing Then Exit Sub
         If MergeLoaders Is Nothing Then Exit Sub
-        Dim MergeExpectTime As Double = 0
-        For Each MergeLoader In MergeLoaders
-            MergeExpectTime += MergeLoader.ProgressWeight
-        Next
-        '构造 Libraries 加载器(为了使得 Mods 下载结束后再构造,这样才会下载 JumpLoader 文件)
+        '构造 Libraries 加载器
         Dim LoadersLib As New List(Of LoaderBase)
         Dim LoadersLib As New List(Of LoaderBase)
         LoadersLib.Add(New LoaderTask(Of String, List(Of NetFile))("分析游戏支持库文件(副加载器)", Sub(Task As LoaderTask(Of String, List(Of NetFile))) Task.Output = McLibFix(New McVersion(VersionName))) With {.ProgressWeight = 1, .Show = False})
         LoadersLib.Add(New LoaderTask(Of String, List(Of NetFile))("分析游戏支持库文件(副加载器)", Sub(Task As LoaderTask(Of String, List(Of NetFile))) Task.Output = McLibFix(New McVersion(VersionName))) With {.ProgressWeight = 1, .Show = False})
         LoadersLib.Add(New LoaderDownload("下载游戏支持库文件(副加载器)", New List(Of NetFile)) With {.ProgressWeight = 7, .Show = False})
         LoadersLib.Add(New LoaderDownload("下载游戏支持库文件(副加载器)", New List(Of NetFile)) With {.ProgressWeight = 7, .Show = False})
         '构造总加载器
         '构造总加载器
         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 = InstallExpectTime})
-        Loaders.Add(New LoaderCombo(Of String)("游戏安装", MergeLoaders) With {.Show = False, .ProgressWeight = MergeExpectTime})
+        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)("游戏安装", MergeLoaders) With {.Show = False, .ProgressWeight = MergeLoaders.Sum(Function(l) l.ProgressWeight)})
         Loaders.Add(New LoaderCombo(Of String)("下载游戏支持库文件", LoadersLib) With {.ProgressWeight = 8})
         Loaders.Add(New LoaderCombo(Of String)("下载游戏支持库文件", LoadersLib) With {.ProgressWeight = 8})
         Loaders.Add(New LoaderTask(Of String, String)("最终整理文件",
         Loaders.Add(New LoaderTask(Of String, String)("最终整理文件",
-                                                      Sub(Task As LoaderTask(Of String, String))
-                                                          '设置图标
-                                                          Dim VersionFolder As String = PathMcFolder & "versions\" & VersionName & "\"
-                                                          If Logo IsNot Nothing AndAlso File.Exists(Logo) Then
-                                                              File.Copy(Logo, VersionFolder & "PCL\Logo.png", True)
-                                                              WriteIni(VersionFolder & "PCL\Setup.ini", "Logo", "PCL\Logo.png")
-                                                              WriteIni(VersionFolder & "PCL\Setup.ini", "LogoCustom", "True")
-                                                              Log("[ModPack] 已设置整合包 Logo:" & Logo)
-                                                          End If
-                                                          '删除原始整合包文件
-                                                          For Each Target As String In {VersionFolder & "原始整合包.zip", VersionFolder & "原始整合包.mrpack"}
-                                                              If Not Setup.Get("ToolDownloadKeepModpack") AndAlso File.Exists(Target) Then
-                                                                  Log("[ModPack] 根据设置要求删除原始整合包文件:" & Target)
-                                                                  File.Delete(Target)
-                                                              End If
-                                                          Next
-                                                      End Sub) With {.ProgressWeight = 0.1, .Show = False})
+        Sub(Task As LoaderTask(Of String, String))
+            '设置图标
+            Dim VersionFolder As String = PathMcFolder & "versions\" & VersionName & "\"
+            If Logo IsNot Nothing AndAlso File.Exists(Logo) Then
+                File.Copy(Logo, VersionFolder & "PCL\Logo.png", True)
+                WriteIni(VersionFolder & "PCL\Setup.ini", "Logo", "PCL\Logo.png")
+                WriteIni(VersionFolder & "PCL\Setup.ini", "LogoCustom", "True")
+                Log("[ModPack] 已设置整合包 Logo:" & Logo)
+            End If
+            '删除原始整合包文件
+            For Each Target As String In {VersionFolder & "原始整合包.zip", VersionFolder & "原始整合包.mrpack"}
+                If Not Setup.Get("ToolDownloadKeepModpack") AndAlso File.Exists(Target) Then
+                    Log("[ModPack] 根据设置要求删除原始整合包文件:" & Target)
+                    File.Delete(Target)
+                End If
+            Next
+        End Sub) With {.ProgressWeight = 0.1, .Show = False})
 
 
         '重复任务检查
         '重复任务检查
         Dim LoaderName As String = "Modrinth 整合包安装:" & VersionName & " "
         Dim LoaderName As String = "Modrinth 整合包安装:" & VersionName & " "
@@ -554,16 +542,8 @@ Retry:
             .TargetVersionFolder = $"{PathMcFolder}versions\{VersionName}\",
             .TargetVersionFolder = $"{PathMcFolder}versions\{VersionName}\",
             .MinecraftName = Json("gameVersion").ToString
             .MinecraftName = Json("gameVersion").ToString
         }
         }
-        Dim InstallExpectTime As Double = 0
-        For Each InstallLoader In InstallLoaders
-            InstallExpectTime += InstallLoader.ProgressWeight
-        Next
         Dim MergeLoaders As List(Of LoaderBase) = McInstallLoader(Request, True)
         Dim MergeLoaders As List(Of LoaderBase) = McInstallLoader(Request, True)
         If MergeLoaders Is Nothing Then Exit Sub
         If MergeLoaders Is Nothing Then Exit Sub
-        Dim MergeExpectTime As Double = 0
-        For Each MergeLoader In MergeLoaders
-            MergeExpectTime += MergeLoader.ProgressWeight
-        Next
         '构造 Libraries 加载器(为了使得 Mods 下载结束后再构造,这样才会下载 JumpLoader 文件)
         '构造 Libraries 加载器(为了使得 Mods 下载结束后再构造,这样才会下载 JumpLoader 文件)
         Dim LoadersLib As New List(Of LoaderBase)
         Dim LoadersLib As New List(Of LoaderBase)
         LoadersLib.Add(New LoaderTask(Of String, String)("重命名版本 Json(副加载器)",
         LoadersLib.Add(New LoaderTask(Of String, String)("重命名版本 Json(副加载器)",
@@ -584,8 +564,8 @@ Retry:
         LoadersLib.Add(New LoaderDownload("下载游戏支持库文件(副加载器)", New List(Of NetFile)) With {.ProgressWeight = 7, .Show = False})
         LoadersLib.Add(New LoaderDownload("下载游戏支持库文件(副加载器)", New List(Of NetFile)) With {.ProgressWeight = 7, .Show = False})
         '构造总加载器
         '构造总加载器
         Dim Loaders As New List(Of LoaderBase) From {
         Dim Loaders As New List(Of LoaderBase) From {
-            New LoaderCombo(Of String)("游戏安装", MergeLoaders) With {.Show = False, .Block = False, .ProgressWeight = MergeExpectTime},
-            New LoaderCombo(Of String)("整合包安装", InstallLoaders) With {.Show = False, .ProgressWeight = InstallExpectTime},
+            New LoaderCombo(Of String)("游戏安装", MergeLoaders) With {.Show = False, .Block = False, .ProgressWeight = MergeLoaders.Sum(Function(l) l.ProgressWeight)},
+            New LoaderCombo(Of String)("整合包安装", InstallLoaders) With {.Show = False, .ProgressWeight = InstallLoaders.Sum(Function(l) l.ProgressWeight)},
             New LoaderCombo(Of String)("下载游戏支持库文件", LoadersLib) With {.ProgressWeight = 8}
             New LoaderCombo(Of String)("下载游戏支持库文件", LoadersLib) With {.ProgressWeight = 8}
         }
         }
 
 
@@ -704,24 +684,16 @@ Retry:
             End Select
             End Select
         Next
         Next
         '构造加载器
         '构造加载器
-        Dim InstallExpectTime As Double = 0
-        For Each InstallLoader In InstallLoaders
-            InstallExpectTime += InstallLoader.ProgressWeight
-        Next
         Dim MergeLoaders As List(Of LoaderBase) = McInstallLoader(Request, True)
         Dim MergeLoaders As List(Of LoaderBase) = McInstallLoader(Request, True)
         If MergeLoaders Is Nothing Then Exit Sub
         If MergeLoaders Is Nothing Then Exit Sub
-        Dim MergeExpectTime As Double = 0
-        For Each MergeLoader In MergeLoaders
-            MergeExpectTime += MergeLoader.ProgressWeight
-        Next
-        '构造 Libraries 加载器(为了使得 Mods 下载结束后再构造,这样才会下载 JumpLoader 文件)
+        '构造 Libraries 加载器
         Dim LoadersLib As New List(Of LoaderBase)
         Dim LoadersLib As New List(Of LoaderBase)
         LoadersLib.Add(New LoaderTask(Of String, List(Of NetFile))("分析游戏支持库文件(副加载器)", Sub(Task As LoaderTask(Of String, List(Of NetFile))) Task.Output = McLibFix(New McVersion(VersionName))) With {.ProgressWeight = 1, .Show = False})
         LoadersLib.Add(New LoaderTask(Of String, List(Of NetFile))("分析游戏支持库文件(副加载器)", Sub(Task As LoaderTask(Of String, List(Of NetFile))) Task.Output = McLibFix(New McVersion(VersionName))) With {.ProgressWeight = 1, .Show = False})
         LoadersLib.Add(New LoaderDownload("下载游戏支持库文件(副加载器)", New List(Of NetFile)) With {.ProgressWeight = 7, .Show = False})
         LoadersLib.Add(New LoaderDownload("下载游戏支持库文件(副加载器)", New List(Of NetFile)) With {.ProgressWeight = 7, .Show = False})
         '构造总加载器
         '构造总加载器
         Dim Loaders As New List(Of LoaderBase)
         Dim Loaders As New List(Of LoaderBase)
-        Loaders.Add(New LoaderCombo(Of String)("游戏安装", MergeLoaders) With {.Show = False, .Block = False, .ProgressWeight = MergeExpectTime})
-        Loaders.Add(New LoaderCombo(Of String)("整合包安装", InstallLoaders) With {.Show = False, .ProgressWeight = InstallExpectTime})
+        Loaders.Add(New LoaderCombo(Of String)("游戏安装", MergeLoaders) With {.Show = False, .Block = False, .ProgressWeight = MergeLoaders.Sum(Function(l) l.ProgressWeight)})
+        Loaders.Add(New LoaderCombo(Of String)("整合包安装", InstallLoaders) With {.Show = False, .ProgressWeight = InstallLoaders.Sum(Function(l) l.ProgressWeight)})
         Loaders.Add(New LoaderCombo(Of String)("下载游戏支持库文件", LoadersLib) With {.ProgressWeight = 8})
         Loaders.Add(New LoaderCombo(Of String)("下载游戏支持库文件", LoadersLib) With {.ProgressWeight = 8})
 
 
         '重复任务检查
         '重复任务检查
@@ -802,24 +774,16 @@ Retry:
             .ForgeVersion = If(Addons.ContainsKey("forge"), Addons("forge"), Nothing),
             .ForgeVersion = If(Addons.ContainsKey("forge"), Addons("forge"), Nothing),
             .FabricVersion = If(Addons.ContainsKey("fabric"), Addons("fabric"), Nothing)
             .FabricVersion = If(Addons.ContainsKey("fabric"), Addons("fabric"), Nothing)
         }
         }
-        Dim InstallExpectTime As Double = 0
-        For Each InstallLoader In InstallLoaders
-            InstallExpectTime += InstallLoader.ProgressWeight
-        Next
         Dim MergeLoaders As List(Of LoaderBase) = McInstallLoader(Request, True)
         Dim MergeLoaders As List(Of LoaderBase) = McInstallLoader(Request, True)
         If MergeLoaders Is Nothing Then Exit Sub
         If MergeLoaders Is Nothing Then Exit Sub
-        Dim MergeExpectTime As Double = 0
-        For Each MergeLoader In MergeLoaders
-            MergeExpectTime += MergeLoader.ProgressWeight
-        Next
-        '构造 Libraries 加载器(为了使得 Mods 下载结束后再构造,这样才会下载 JumpLoader 文件)
+        '构造 Libraries 加载器
         Dim LoadersLib As New List(Of LoaderBase)
         Dim LoadersLib As New List(Of LoaderBase)
         LoadersLib.Add(New LoaderTask(Of String, List(Of NetFile))("分析游戏支持库文件(副加载器)", Sub(Task As LoaderTask(Of String, List(Of NetFile))) Task.Output = McLibFix(New McVersion(VersionName))) With {.ProgressWeight = 1, .Show = False})
         LoadersLib.Add(New LoaderTask(Of String, List(Of NetFile))("分析游戏支持库文件(副加载器)", Sub(Task As LoaderTask(Of String, List(Of NetFile))) Task.Output = McLibFix(New McVersion(VersionName))) With {.ProgressWeight = 1, .Show = False})
         LoadersLib.Add(New LoaderDownload("下载游戏支持库文件(副加载器)", New List(Of NetFile)) With {.ProgressWeight = 7, .Show = False})
         LoadersLib.Add(New LoaderDownload("下载游戏支持库文件(副加载器)", New List(Of NetFile)) With {.ProgressWeight = 7, .Show = False})
         '构造总加载器
         '构造总加载器
         Dim Loaders As New List(Of LoaderBase)
         Dim Loaders As New List(Of LoaderBase)
-        Loaders.Add(New LoaderCombo(Of String)("游戏安装", MergeLoaders) With {.Show = False, .Block = False, .ProgressWeight = MergeExpectTime})
-        Loaders.Add(New LoaderCombo(Of String)("整合包安装", InstallLoaders) With {.Show = False, .ProgressWeight = InstallExpectTime})
+        Loaders.Add(New LoaderCombo(Of String)("游戏安装", MergeLoaders) With {.Show = False, .Block = False, .ProgressWeight = MergeLoaders.Sum(Function(l) l.ProgressWeight)})
+        Loaders.Add(New LoaderCombo(Of String)("整合包安装", InstallLoaders) With {.Show = False, .ProgressWeight = InstallLoaders.Sum(Function(l) l.ProgressWeight)})
         Loaders.Add(New LoaderCombo(Of String)("下载游戏支持库文件", LoadersLib) With {.ProgressWeight = 8})
         Loaders.Add(New LoaderCombo(Of String)("下载游戏支持库文件", LoadersLib) With {.ProgressWeight = 8})
 
 
         '重复任务检查
         '重复任务检查

+ 13 - 11
Plain Craft Launcher 2/Modules/Minecraft/ModWatcher.vb

@@ -276,17 +276,18 @@
                     IsWindowFinished = True
                     IsWindowFinished = True
                     '最大化
                     '最大化
                     If Setup.Get("LaunchArgumentWindowType") = 4 Then
                     If Setup.Get("LaunchArgumentWindowType") = 4 Then
-                        RunInNewThread(Sub()
-                                           Try
-                                               '如果最大化导致屏幕渲染大小不对,那是 MC 的 Bug,不是我的 Bug
-                                               '……虽然我很想这样说,但总有人反馈,算了
-                                               Thread.Sleep(2000)
-                                               ShowWindow(WindowHandle, 3)
-                                               WatcherLog($"已最大化 Minecraft 窗口:{MinecraftWindowHandle.ToInt64}")
-                                           Catch ex As Exception
-                                               Log(ex, "最大化 Minecraft 窗口时出现错误")
-                                           End Try
-                                       End Sub, "MinecraftWindowMaximize")
+                        RunInNewThread(
+                        Sub()
+                            Try
+                                '如果最大化导致屏幕渲染大小不对,那是 MC 的 Bug,不是我的 Bug
+                                '……虽然我很想这样说,但总有人反馈,算了
+                                Thread.Sleep(2000)
+                                ShowWindow(WindowHandle, 3)
+                                WatcherLog($"已最大化 Minecraft 窗口:{MinecraftWindowHandle.ToInt64}")
+                            Catch ex As Exception
+                                Log(ex, "最大化 Minecraft 窗口时出现错误")
+                            End Try
+                        End Sub, "MinecraftWindowMaximize")
                     End If
                     End If
                 ElseIf Not IsWindowAppeared Then
                 ElseIf Not IsWindowAppeared Then
                     '已找到 FML 窗口
                     '已找到 FML 窗口
@@ -347,6 +348,7 @@
             '崩溃分析
             '崩溃分析
             WatcherLog("Minecraft 已崩溃,将在 2 秒后开始崩溃分析")
             WatcherLog("Minecraft 已崩溃,将在 2 秒后开始崩溃分析")
             Hint("检测到 Minecraft 出现错误,错误分析已开始……")
             Hint("检测到 Minecraft 出现错误,错误分析已开始……")
+            FeedbackInfo()
             RunInNewThread(Sub()
             RunInNewThread(Sub()
                                Try
                                Try
                                    Thread.Sleep(2000)
                                    Thread.Sleep(2000)

+ 2 - 0
Plain Craft Launcher 2/Modules/Minecraft/MyLocalModItem.xaml.vb

@@ -195,6 +195,7 @@ RetryStart:
     Private Shared Swiping As Boolean = False
     Private Shared Swiping As Boolean = False
     Private Shared SwipToState As Boolean '被滑动到的目标应将 Checked 改为此值
     Private Shared SwipToState As Boolean '被滑动到的目标应将 Checked 改为此值
     Private Sub Button_MouseSwipeStart(sender As Object, e As Object) Handles Me.MouseLeftButtonDown
     Private Sub Button_MouseSwipeStart(sender As Object, e As Object) Handles Me.MouseLeftButtonDown
+        If Parent Is Nothing Then Exit Sub 'Mod 可能已被删除(#3824)
         '开始滑动
         '开始滑动
         Dim Index = CType(Parent, StackPanel).Children.IndexOf(Me)
         Dim Index = CType(Parent, StackPanel).Children.IndexOf(Me)
         SwipeStart = Index
         SwipeStart = Index
@@ -204,6 +205,7 @@ RetryStart:
         FrmVersionMod.CardSelect.IsHitTestVisible = False '暂时禁用下边栏
         FrmVersionMod.CardSelect.IsHitTestVisible = False '暂时禁用下边栏
     End Sub
     End Sub
     Private Sub Button_MouseSwipe(sender As Object, e As Object) Handles Me.MouseEnter, Me.MouseLeave, Me.MouseLeftButtonUp
     Private Sub Button_MouseSwipe(sender As Object, e As Object) Handles Me.MouseEnter, Me.MouseLeave, Me.MouseLeftButtonUp
+        If Parent Is Nothing Then Exit Sub 'Mod 可能已被删除(#3824)
         '结束滑动
         '结束滑动
         If Mouse.LeftButton <> MouseButtonState.Pressed OrElse Not Swiping Then
         If Mouse.LeftButton <> MouseButtonState.Pressed OrElse Not Swiping Then
             Swiping = False
             Swiping = False

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

@@ -0,0 +1,3 @@
+Public Module ModDevelop
+
+End Module

+ 3 - 4
Plain Craft Launcher 2/Modules/ModEvent.vb

@@ -73,8 +73,8 @@
                 Case "切换页面"
                 Case "切换页面"
                     FrmMain.PageChange(Val(Data(0)), Val(Data(1)))
                     FrmMain.PageChange(Val(Data(0)), Val(Data(1)))
 
 
-                Case "导入整合包"
-                    RunInThread(Sub() ModpackInstall())
+                Case "导入整合包", "安装整合包"
+                    RunInUi(Sub() ModpackInstall())
 
 
                 Case "下载文件"
                 Case "下载文件"
                     Data(0) = Data(0).Replace("\", "/")
                     Data(0) = Data(0).Replace("\", "/")
@@ -101,8 +101,7 @@
         '网页确认
         '网页确认
         If RelativeUrl.StartsWithF("http", True) Then
         If RelativeUrl.StartsWithF("http", True) Then
             If RunInUi() Then
             If RunInUi() Then
-                Throw New Exception("MyListItem 在界面初始化时就需要获取帮助标题等信息,这会导致程序在网络请求时卡死。" & vbCrLf &
-                                    "因此,请换用 MyListItem 以外的控件(例如 MyButton)作为联网帮助页面的入口!")
+                Throw New Exception("能打开联网帮助页面的 MyListItem 必须手动设置 Title、Info 属性!")
             End If
             End If
             '获取文件名
             '获取文件名
             Dim RawFileName As String
             Dim RawFileName As String

+ 877 - 0
Plain Craft Launcher 2/Modules/ModMain.vb

@@ -0,0 +1,877 @@
+Imports System.Windows.Interop
+Imports System.Windows.Threading
+
+Public Module ModMain
+
+#Region "弹出提示"
+
+    ''' <summary>
+    ''' 提示信息的种类。
+    ''' </summary>
+    Public Enum HintType
+        ''' <summary>
+        ''' 信息,通常是蓝色的“i”。
+        ''' </summary>
+        ''' <remarks></remarks>
+        Info
+        ''' <summary>
+        ''' 已完成,通常是绿色的“√”。
+        ''' </summary>
+        ''' <remarks></remarks>
+        Finish
+        ''' <summary>
+        ''' 错误,通常是红色的“×”。
+        ''' </summary>
+        ''' <remarks></remarks>
+        Critical
+    End Enum
+    Private Structure HintMessage
+        Public Text As String
+        Public Type As HintType
+        Public Log As Boolean
+    End Structure
+
+    ''' <summary>
+    ''' 等待弹出的提示列表。以 {String, HintType, Log As Boolean} 形式存储为数组。
+    ''' </summary>
+    Private HintWaiting As List(Of HintMessage) = If(HintWaiting, New List(Of HintMessage))
+    ''' <summary>
+    ''' 在窗口左下角弹出提示文本。
+    ''' </summary>
+    Public Sub Hint(Text As String, Optional Type As HintType = HintType.Info, Optional Log As Boolean = True)
+        If HintWaiting Is Nothing Then HintWaiting = New List(Of HintMessage)
+        HintWaiting.Add(New HintMessage With {.Text = If(Text, ""), .Type = Type, .Log = Log})
+    End Sub
+
+    Private Sub HintTick()
+        Try
+
+            'Tag 存储了:{ 是否可以重用, Uuid }
+            If Not HintWaiting.Any() Then Exit Sub
+            Do While HintWaiting.Any
+                ''清除空提示
+                'If IsNothing(HintWaiting(0)) OrElse IsNothing(HintWaiting(0)(0)) Then
+                '    HintWaiting.RemoveAt(0)
+                '    Continue Do
+                'End If
+                Dim CurrentHint = HintWaiting(0)
+                '去回车
+                CurrentHint.Text = CurrentHint.Text.Replace(vbCrLf, " ").Replace(vbCr, " ").Replace(vbLf, " ")
+                '超量提示直接忽略
+                If FrmMain.PanHint.Children.Count >= 20 Then GoTo EndHint
+                '检查是否有重复提示
+                Dim DoubleStack As Border = Nothing
+                For Each stack As Border In FrmMain.PanHint.Children
+                    If stack.Tag(0) AndAlso CType(stack.Child, TextBlock).Text = CurrentHint.Text Then DoubleStack = stack
+                Next
+                '获取渐变颜色
+                Dim TargetColor0, TargetColor1 As MyColor
+                Dim Percent As Double = 0.3
+                Select Case CurrentHint.Type
+                    Case HintType.Info
+                        TargetColor0 = New MyColor(37, 155, 252)
+                        TargetColor1 = New MyColor(10, 142, 252)
+                    Case HintType.Finish
+                        TargetColor0 = New MyColor(33, 177, 33)
+                        TargetColor1 = New MyColor(29, 160, 29)
+                    Case Else 'HintType.Critical
+                        TargetColor0 = New MyColor(255, 53, 11)
+                        TargetColor1 = New MyColor(255, 43, 0)
+                End Select
+                If Not IsNothing(DoubleStack) Then
+                    '有重复提示,且该提示的进入动画已播放
+                    If Not AniIsRun("Hint Show " & DoubleStack.Tag(1)) Then
+                        AniStop("Hint Hide " & DoubleStack.Tag(1))
+                        Dim Delay As Double = (800 + MathClamp(CurrentHint.Text.Length, 5, 23) * 180) * AniSpeed
+                        AniStart({
+                            AaX(DoubleStack, -12 - DoubleStack.Margin.Left, 50,, New AniEaseOutFluent),
+                            AaX(DoubleStack, -8, 50, 50, New AniEaseInFluent),
+                            AaX(DoubleStack, 8, 50, 100, New AniEaseOutFluent),
+                            AaX(DoubleStack, -8, 50, 150, New AniEaseInFluent),
+                            AaDouble(Sub(i)
+                                         Percent += i
+                                         Dim Gradient As LinearGradientBrush = DoubleStack.Background
+                                         Gradient.GradientStops(0).Color = TargetColor0 * Percent + New MyColor(255, 255, 255) * (1 - Percent)
+                                         Gradient.GradientStops(1).Color = TargetColor1 * Percent + New MyColor(255, 255, 255) * (1 - Percent)
+                                     End Sub, 0.7, 250),
+                            AaX(DoubleStack, -50, 200, Delay, New AniEaseInFluent),
+                            AaOpacity(DoubleStack, -1, 150, Delay),
+                            AaCode(Sub() DoubleStack.Tag(0) = False, Delay),
+                            AaHeight(DoubleStack, -26, 100,, New AniEaseOutFluent, True),
+                            AaCode(Sub() FrmMain.PanHint.Children.Remove(DoubleStack), , True)
+                      }, "Hint Hide " & DoubleStack.Tag(1))
+                    End If
+                Else
+                    '准备控件
+                    Dim NewHintControl As New Border With {.Tag = {True, GetUuid()}, .Margin = New Thickness(-70, 0, 20, 0), .Opacity = 0, .Height = 0, .HorizontalAlignment = HorizontalAlignment.Left, .CornerRadius = New CornerRadius(0, 6, 6, 0)}
+                    NewHintControl.Background = New LinearGradientBrush(New GradientStopCollection(New List(Of GradientStop) From {
+                        New GradientStop(TargetColor0 * Percent + New MyColor(255, 255, 255) * (1 - Percent), 0),
+                        New GradientStop(TargetColor1 * Percent + New MyColor(255, 255, 255) * (1 - Percent), 1)}), 90)
+                    NewHintControl.Child = New TextBlock With {.TextTrimming = TextTrimming.CharacterEllipsis, .FontSize = 13, .Text = CurrentHint.Text, .Foreground = New MyColor(255, 255, 255), .Margin = New Thickness(33, 5, 8, 5)}
+                    'AddHandler NewHintControl.MouseLeftButtonDown, AddressOf HideAllHint
+                    FrmMain.PanHint.Children.Add(NewHintControl)
+                    '控件动画
+                    Dim Animations As New List(Of AniData)
+                    If FrmMain.PanHint.Children.Count > 1 Then
+                        '已有提示
+                        Animations.Add(AaHeight(NewHintControl, 26, 150, , New AniEaseOutFluent))
+                    Else
+                        '是唯一提示
+                        NewHintControl.Height = 26
+                    End If
+                    '开始动画
+                    Animations.AddRange({
+                        AaX(NewHintControl, 30, 400, , New AniEaseOutElastic(AniEasePower.Weak)),
+                        AaX(NewHintControl, 20, 200, , New AniEaseOutFluent),
+                        AaOpacity(NewHintControl, 1, 100),
+                        AaDouble(Sub(i)
+                                     Percent += i
+                                     Dim Gradient As LinearGradientBrush = NewHintControl.Background
+                                     Gradient.GradientStops(0).Color = TargetColor0 * Percent + New MyColor(255, 255, 255) * (1 - Percent)
+                                     Gradient.GradientStops(1).Color = TargetColor1 * Percent + New MyColor(255, 255, 255) * (1 - Percent)
+                                 End Sub, 0.7, 250, 100)
+                    })
+                    AniStart(Animations, "Hint Show " & NewHintControl.Tag(1))
+                    '结束动画
+                    Dim Delay As Double = (800 + MathClamp(CurrentHint.Text.Length, 5, 23) * 180) * AniSpeed
+                    AniStart({
+                        AaX(NewHintControl, -50, 200, Delay, New AniEaseInFluent),
+                        AaOpacity(NewHintControl, -1, 150, Delay),
+                        AaCode(Sub() NewHintControl.Tag(0) = False, Delay),
+                        AaHeight(NewHintControl, -26, 100,, New AniEaseOutFluent, True),
+                        AaCode(Sub() FrmMain.PanHint.Children.Remove(NewHintControl), , True)
+                    }, "Hint Hide " & NewHintControl.Tag(1))
+                End If
+                '结束处理
+EndHint:
+                If CurrentHint.Log Then Log("[UI] 弹出提示:" & CurrentHint.Text)
+                HintWaiting.RemoveAt(0)
+            Loop
+        Catch ex As Exception
+            Log(ex, "显示弹出提示失败", LogLevel.Normal)
+        End Try
+    End Sub
+    Private Sub HideAllHint()
+        For Each Control As Border In FrmMain.PanHint.Children
+            Control.IsHitTestVisible = False
+            AniStart({
+                AaX(Control, -50, 200, , New AniEaseInFluent),
+                AaOpacity(Control, -1, 150, , New AniEaseInFluent),
+                AaCode(Sub() Control.Tag(0) = False),
+                AaHeight(Control, -26, 100,, New AniEaseOutFluent, True),
+                AaCode(Sub() FrmMain.PanHint.Children.Remove(Control), , True)
+            }, "Hint Hide " & Control.Tag(1))
+        Next
+    End Sub
+
+#End Region
+
+#Region "弹窗"
+
+    ''' <summary>
+    ''' 存储弹窗信息的转换器。
+    ''' </summary>
+    Public Class MyMsgBoxConverter
+        Public Type As MyMsgBoxType
+        Public Title As String
+        Public Text As String
+        ''' <summary>
+        ''' 输入框模式:文本框的文本;选择模式:需要放进去的 List(Of MyListItem)。
+        ''' </summary>
+        Public Content As Object
+        ''' <summary>
+        ''' 仅输入框模式:输入验证规则。
+        ''' </summary>
+        Public ValidateRules As ObjectModel.Collection(Of Validate)
+        ''' <summary>
+        ''' 仅输入框模式:提示文本。
+        ''' </summary>
+        Public HintText As String = ""
+        ''' <summary>
+        ''' 有多个按钮时,是否给第一个按钮加高亮。
+        ''' </summary>
+        Public HighLight As Boolean
+        Public Button1 As String = "确定"
+        Public Button2 As String = ""
+        Public Button3 As String = ""
+        ''' <summary>
+        ''' 点击第一个按钮将执行该方法,不关闭弹窗。
+        ''' </summary>
+        Public Button1Action As Action = Nothing
+        ''' <summary>
+        ''' 点击第二个按钮将执行该方法,不关闭弹窗。
+        ''' </summary>
+        Public Button2Action As Action = Nothing
+        ''' <summary>
+        ''' 点击第三个按钮将执行该方法,不关闭弹窗。
+        ''' </summary>
+        Public Button3Action As Action = Nothing
+        Public IsWarn As Boolean = False
+        Public ForceWait As Boolean = False
+        Public WaitFrame As New DispatcherFrame(True)
+        ''' <summary>
+        ''' 弹窗是否已经关闭。
+        ''' </summary>
+        Public IsExited As Boolean = False
+        ''' <summary>
+        ''' 点击的按钮编号或输入的文本。若输入弹窗点击了非第一个按钮,则为 Nothing。
+        ''' </summary>
+        Public Result As Object
+    End Class
+    Public Enum MyMsgBoxType
+        Text
+        [Select]
+        Input
+    End Enum
+
+    ''' <summary>
+    ''' 显示弹窗,返回点击按钮的编号(从 1 开始)。
+    ''' </summary>
+    ''' <param name="Title">弹窗的标题。</param>
+    ''' <param name="Caption">弹窗的内容。</param>
+    ''' <param name="Button1">显示的第一个按钮,默认为“确定”。</param>
+    ''' <param name="Button2">显示的第二个按钮,默认为空。</param>
+    ''' <param name="Button3">显示的第三个按钮,默认为空。</param>
+    ''' <param name="Button1Action">点击第一个按钮将执行该方法,不关闭弹窗。</param>
+    ''' <param name="Button2Action">点击第二个按钮将执行该方法,不关闭弹窗。</param>
+    ''' <param name="Button3Action">点击第三个按钮将执行该方法,不关闭弹窗。</param>
+    ''' <param name="IsWarn">是否为警告弹窗,若为 True,弹窗配色和背景会变为红色。</param>
+    Public Function MyMsgBox(Caption As String, Optional Title As String = "提示",
+                             Optional Button1 As String = "确定", Optional Button2 As String = "", Optional Button3 As String = "",
+                             Optional IsWarn As Boolean = False, Optional HighLight As Boolean = True, Optional ForceWait As Boolean = False,
+                             Optional Button1Action As Action = Nothing, Optional Button2Action As Action = Nothing, Optional Button3Action As Action = Nothing) As Integer
+        '将弹窗列入队列
+        Dim Converter As New MyMsgBoxConverter With {.Type = MyMsgBoxType.Text, .Button1 = Button1, .Button2 = Button2, .Button3 = Button3, .Text = Caption, .IsWarn = IsWarn, .Title = Title, .HighLight = HighLight, .ForceWait = True, .Button1Action = Button1Action, .Button2Action = Button2Action, .Button3Action = Button3Action}
+        WaitingMyMsgBox.Add(Converter)
+        If RunInUi() Then
+            '若为 UI 线程,立即执行弹窗刻, 避免快速(连点器)点击时多次弹窗
+            MyMsgBoxTick()
+        End If
+        If Button2.Length > 0 OrElse ForceWait Then
+            '若有多个按钮则开始等待
+            If FrmMain Is Nothing OrElse FrmMain.PanMsg Is Nothing AndAlso RunInUi() Then
+                '主窗体尚未加载,用老土的弹窗来替代
+                WaitingMyMsgBox.Remove(Converter)
+                If Button2.Length > 0 Then
+                    Dim RawResult As MsgBoxResult = MsgBox(Caption, If(Button3.Length > 0, MsgBoxStyle.YesNoCancel, MsgBoxStyle.YesNo) + If(IsWarn, MsgBoxStyle.Critical, MsgBoxStyle.Question), Title)
+                    Select Case RawResult
+                        Case MsgBoxResult.Yes
+                            Converter.Result = 1
+                        Case MsgBoxResult.No
+                            Converter.Result = 2
+                        Case MsgBoxResult.Cancel
+                            Converter.Result = 3
+                    End Select
+                Else
+                    MsgBox(Caption, MsgBoxStyle.OkOnly + If(IsWarn, MsgBoxStyle.Critical, MsgBoxStyle.Question), Title)
+                    Converter.Result = 1
+                End If
+                Log("[Control] 主窗体加载完成前出现意料外的等待弹窗:" & Button1 & "," & Button2 & "," & Button3, LogLevel.Debug)
+            Else
+                Try
+                    FrmMain.DragStop()
+                    ComponentDispatcher.PushModal()
+                    Dispatcher.PushFrame(Converter.WaitFrame)
+                Finally
+                    ComponentDispatcher.PopModal()
+                End Try
+            End If
+            Log("[Control] 普通弹框返回:" & If(Converter.Result, "null"))
+            Return Converter.Result
+        Else
+            '不进行等待,直接返回
+            Return 1
+        End If
+    End Function
+    ''' <summary>
+    ''' 显示输入框并返回输入的文本。若点击第二个按钮,则返回 Nothing。
+    ''' </summary>
+    ''' <param name="Title">弹窗的标题。</param>
+    ''' <param name="ValidateRules">文本框的输入检测。</param>
+    ''' <param name="Text">弹窗的介绍文本。</param>
+    ''' <param name="DefaultInput">文本框的默认内容。</param>
+    ''' <param name="HintText">文本框的提示内容。</param>
+    ''' <param name="Button1">显示的第一个按钮,默认为“确定”。</param>
+    ''' <param name="Button2">显示的第二个按钮,默认为“取消”。</param>
+    ''' <param name="IsWarn">是否为警告弹窗,若为 True,弹窗配色和背景会变为红色。</param>
+    Public Function MyMsgBoxInput(Title As String, Optional Text As String = "", Optional DefaultInput As String = "", Optional ValidateRules As ObjectModel.Collection(Of Validate) = Nothing, Optional HintText As String = "", Optional Button1 As String = "确定", Optional Button2 As String = "取消", Optional IsWarn As Boolean = False) As String
+        '将弹窗列入队列
+        Dim Converter As New MyMsgBoxConverter With {.Text = Text, .HintText = HintText, .Type = MyMsgBoxType.Input, .ValidateRules = If(ValidateRules, New ObjectModel.Collection(Of Validate)), .Button1 = Button1, .Button2 = Button2, .Content = DefaultInput, .IsWarn = IsWarn, .Title = Title}
+        WaitingMyMsgBox.Add(Converter)
+        '虽然我也不知道这是啥但是能用就成了 :)
+        Try
+            If FrmMain IsNot Nothing Then FrmMain.DragStop()
+            ComponentDispatcher.PushModal()
+            Dispatcher.PushFrame(Converter.WaitFrame)
+        Finally
+            ComponentDispatcher.PopModal()
+        End Try
+        Log("[Control] 输入弹框返回:" & If(Converter.Result, "null"))
+        Return Converter.Result
+    End Function
+    ''' <summary>
+    ''' 显示选择框并返回选择的第几项(从 0 开始)。若点击第二个按钮,则返回 Nothing。
+    ''' </summary>
+    ''' <param name="Title">弹窗的标题。</param>
+    ''' <param name="Button1">显示的第一个按钮,默认为 “确定”。</param>
+    ''' <param name="Button2">显示的第二个按钮,默认为空。</param>
+    ''' <param name="IsWarn">是否为警告弹窗,若为 True,弹窗配色和背景会变为红色。</param>
+    Public Function MyMsgBoxSelect(Selections As List(Of IMyRadio), Optional Title As String = "提示", Optional Button1 As String = "确定", Optional Button2 As String = "", Optional IsWarn As Boolean = False) As Integer?
+        '将弹窗列入队列
+        Dim Converter As New MyMsgBoxConverter With {.Type = MyMsgBoxType.Select, .Button1 = Button1, .Button2 = Button2, .Content = Selections, .IsWarn = IsWarn, .Title = Title}
+        WaitingMyMsgBox.Add(Converter)
+        '虽然我也不知道这是啥但是能用就成了 :)
+        Try
+            If FrmMain IsNot Nothing Then FrmMain.DragStop()
+            ComponentDispatcher.PushModal()
+            Dispatcher.PushFrame(Converter.WaitFrame)
+        Finally
+            ComponentDispatcher.PopModal()
+        End Try
+        Log("[Control] 选择弹框返回:" & If(Converter.Result, "null"))
+        Return Converter.Result
+    End Function
+
+    ''' <summary>
+    ''' 等待显示的弹窗。
+    ''' </summary>
+    Public WaitingMyMsgBox As List(Of MyMsgBoxConverter) = If(WaitingMyMsgBox, New List(Of MyMsgBoxConverter))
+    Public Sub MyMsgBoxTick()
+        Try
+            If FrmMain Is Nothing OrElse FrmMain.PanMsg Is Nothing OrElse FrmMain.WindowState = WindowState.Minimized Then Exit Sub
+            If FrmMain.PanMsg.Children.Count > 0 Then
+                '弹窗中
+                FrmMain.PanMsg.Visibility = Visibility.Visible
+            ElseIf WaitingMyMsgBox.Any Then
+                '没有弹窗,显示一个等待的弹窗
+                FrmMain.PanMsg.Visibility = Visibility.Visible
+                Select Case CType(WaitingMyMsgBox(0), MyMsgBoxConverter).Type
+                    Case MyMsgBoxType.Input
+                        FrmMain.PanMsg.Children.Add(New MyMsgInput(WaitingMyMsgBox(0)))
+                    Case MyMsgBoxType.Select
+                        FrmMain.PanMsg.Children.Add(New MyMsgSelect(WaitingMyMsgBox(0)))
+                    Case MyMsgBoxType.Text
+                        FrmMain.PanMsg.Children.Add(New MyMsgText(WaitingMyMsgBox(0)))
+                End Select
+                WaitingMyMsgBox.RemoveAt(0)
+            Else
+                '没有弹窗,没有等待的弹窗
+                If Not FrmMain.PanMsg.Visibility = Visibility.Collapsed Then FrmMain.PanMsg.Visibility = Visibility.Collapsed
+            End If
+        Catch ex As Exception
+            Log(ex, "处理等待中的弹窗失败", LogLevel.Feedback)
+        End Try
+    End Sub
+
+#End Region
+
+#Region "页面声明"
+    '在最后进行页面声明,避免颜色尚未加载完毕
+
+    '窗体声明
+    Public FrmMain As FormMain
+    Public FrmStart As SplashScreen
+
+    '页面声明(出于单元测试考虑,初始化页面已转入 FormMain 中)
+    Public FrmLaunchLeft As PageLaunchLeft
+    Public FrmLaunchRight As PageLaunchRight
+    Public FrmSelectLeft As PageSelectLeft
+    Public FrmSelectRight As PageSelectRight
+    Public FrmSpeedLeft As PageSpeedLeft
+    Public FrmSpeedRight As PageSpeedRight
+
+    '联机页面声明
+    Public FrmLinkLeft As PageLinkLeft
+    Public FrmLinkIoi As PageLinkIoi
+    Public FrmLinkHiper As PageLinkHiper
+    Public FrmLinkHelp As PageOtherHelpDetail
+    Public FrmLinkFeedback As PageLinkFeedback
+
+    '下载页面声明
+    Public FrmDownloadLeft As PageDownloadLeft
+    Public FrmDownloadInstall As PageDownloadInstall
+    Public FrmDownloadClient As PageDownloadClient
+    Public FrmDownloadOptiFine As PageDownloadOptiFine
+    Public FrmDownloadLiteLoader As PageDownloadLiteLoader
+    Public FrmDownloadForge As PageDownloadForge
+    Public FrmDownloadFabric As PageDownloadFabric
+    Public FrmDownloadMod As PageDownloadMod
+    Public FrmDownloadPack As PageDownloadPack
+
+    '设置页面声明
+    Public FrmSetupLeft As PageSetupLeft
+    Public FrmSetupLaunch As PageSetupLaunch
+    Public FrmSetupUI As PageSetupUI
+    Public FrmSetupSystem As PageSetupSystem
+    Public FrmSetupLink As PageSetupLink
+
+    '其他页面声明
+    Public FrmOtherLeft As PageOtherLeft
+    Public FrmOtherHelp As PageOtherHelp
+    Public FrmOtherAbout As PageOtherAbout
+    Public FrmOtherTest As PageOtherTest
+
+    '登录页面声明
+    Public FrmLoginLegacy As PageLoginLegacy
+    Public FrmLoginNide As PageLoginNide
+    Public FrmLoginNideSkin As PageLoginNideSkin
+    Public FrmLoginAuth As PageLoginAuth
+    Public FrmLoginAuthSkin As PageLoginAuthSkin
+    Public FrmLoginMs As PageLoginMs
+    Public FrmLoginMsSkin As PageLoginMsSkin
+
+    '版本设置页面声明
+    Public FrmVersionLeft As PageVersionLeft
+    Public FrmVersionOverall As PageVersionOverall
+    Public FrmVersionMod As PageVersionMod
+    Public FrmVersionModDisabled As PageVersionModDisabled
+    Public FrmVersionSetup As PageVersionSetup
+
+    '资源信息分页声明
+    Public FrmDownloadCompDetail As PageDownloadCompDetail
+
+#End Region
+
+#Region "帮助"
+
+    Public Class HelpEntry
+
+        '基础
+
+        ''' <summary>
+        ''' 显示标题。
+        ''' </summary>
+        Public Title As String
+        ''' <summary>
+        ''' 显示描述。
+        ''' </summary>
+        Public Desc As String
+        ''' <summary>
+        ''' 检索关键字。
+        ''' </summary>
+        Public Search As String
+        ''' <summary>
+        ''' 用于分类的标签列表。
+        ''' </summary>
+        Public Types As List(Of String)
+
+        '显示(可选)
+
+        ''' <summary>
+        ''' 帮助项的自定义图标。可能为 Nothing。
+        ''' </summary>
+        Public Logo As String = Nothing
+        ''' <summary>
+        ''' 是否显示在搜索结果。默认为 True。
+        ''' </summary>
+        Public ShowInSearch As Boolean = True
+        ''' <summary>
+        ''' 是否在公开版的 PCL 中显示(这会影响主页与搜索)。默认为 True。
+        ''' </summary>
+        Public ShowInPublic As Boolean = True
+        ''' <summary>
+        ''' 是否在快照版的 PCL 中显示(这会影响主页与搜索)。默认为 True。
+        ''' </summary>
+        Public ShowInSnapshot As Boolean = True
+
+        '动作
+
+        ''' <summary>
+        ''' 是否为 “执行事件”。
+        ''' </summary>
+        Public IsEvent As Boolean
+        Public EventType As String
+        Public EventData As String
+        ''' <summary>
+        ''' 若非执行事件,其对应的 .xaml 本地文件内容。
+        ''' </summary>
+        Public XamlContent As String
+
+        '转换
+
+        ''' <summary>
+        ''' 从文件初始化 HelpEntry 对象,失败会抛出异常。
+        ''' </summary>
+        Public Sub New(FilePath As String)
+            Dim JsonData As JObject = GetJson(HelpArgumentReplace(ReadFile(FilePath)))
+            If JsonData Is Nothing Then Throw New FileNotFoundException("未找到帮助文件:" & FilePath, FilePath)
+            '加载常规信息
+            If JsonData("Title") IsNot Nothing Then
+                Title = JsonData("Title")
+            Else
+                Throw New ArgumentException("未找到 Title 项")
+            End If
+            Desc = If(JsonData("Description"), "")
+            Search = If(JsonData("Keywords"), "")
+            Logo = JsonData("Logo") '为保持 Nothing,不要加 If
+            ShowInSearch = If(JsonData("ShowInSearch"), ShowInSearch)
+            ShowInPublic = If(JsonData("ShowInPublic"), ShowInPublic)
+            ShowInSnapshot = If(JsonData("ShowInSnapshot"), ShowInSnapshot)
+            Types = New List(Of String)
+            For Each NameOfType In If(JsonData("Types"), GetJson("[]"))
+                Types.Add(NameOfType)
+            Next
+            '加载事件信息
+            If If(JsonData("IsEvent"), False) Then
+                EventType = JsonData("EventType")
+                If EventType Is Nothing Then Throw New ArgumentException("未找到 EventType 项")
+                EventData = If(JsonData("EventData"), "")
+                IsEvent = True
+            Else
+                Dim XamlAddress As String = FilePath.ToLower.Replace(".json", ".xaml")
+                If File.Exists(XamlAddress) Then
+                    XamlContent = ReadFile(XamlAddress)
+                    IsEvent = False
+                Else
+                    Throw New FileNotFoundException("未找到帮助条目 .json 对应的 .xaml 文件(" & XamlAddress & ")")
+                End If
+            End If
+        End Sub
+        ''' <summary>
+        ''' 获取该 HelpEntry 对应的 MyListItem。
+        ''' </summary>
+        Public Function ToListItem() As MyListItem
+            Return SetToListItem(New MyListItem)
+        End Function
+        ''' <summary>
+        ''' 将属性设置入一个现有的 ListItem。
+        ''' </summary>
+        Public Function SetToListItem(Item As MyListItem) As MyListItem
+            Dim Logo As String
+            If IsEvent Then
+                If EventType = "弹出窗口" Then
+                    Logo = PathImage & "Blocks/GrassPath.png"
+                Else
+                    Logo = PathImage & "Blocks/CommandBlock.png"
+                End If
+            Else
+                Logo = PathImage & "Blocks/Grass.png"
+            End If
+            '设置属性
+            With Item
+                .SnapsToDevicePixels = True
+                .Title = Title
+                .Info = Desc
+                .Logo = If(Me.Logo, Logo)
+                .Height = 42
+                .Type = MyListItem.CheckType.Clickable
+                .Tag = Me
+                .EventType = Nothing
+                .EventData = Nothing
+            End With
+            '项目的点击事件
+            AddHandler Item.Click, Sub(sender, e) PageOtherHelp.OnItemClick(sender.Tag)
+            Return Item
+        End Function
+
+    End Class
+
+    Public HelpLoader As New LoaderTask(Of Integer, List(Of HelpEntry))("Help Page", AddressOf HelpLoad,, ThreadPriority.BelowNormal)
+    Private ReadOnly HelpLoadLock As New Object
+    ''' <summary>
+    ''' 初始化帮助列表对象。
+    ''' </summary>
+    Private Sub HelpLoad(Loader As LoaderTask(Of Integer, List(Of HelpEntry)))
+        SyncLock HelpLoadLock '避免重复解压文件导致出错
+            Try
+
+                '解压内置文件
+                HelpTryExtract()
+
+                '遍历文件
+                Dim FileList As New List(Of String)
+                Try
+                    Dim IgnoreList As New List(Of String)
+                    '读取自定义文件
+                    If Directory.Exists(Path & "PCL\Help\") Then
+                        For Each File In EnumerateFiles(Path & "PCL\Help\")
+                            Select Case File.Extension.ToLower
+                                Case ".helpignore"
+                                    '加载忽略列表
+                                    Log("[Help] 发现 .helpignore 文件:" & File.FullName)
+                                    For Each Line In ReadFile(File.FullName).Split(vbCrLf.ToCharArray)
+                                        Dim RealString As String = Line.Before("#").Trim
+                                        If String.IsNullOrWhiteSpace(RealString) Then Continue For
+                                        IgnoreList.Add(RealString)
+                                        If ModeDebug Then Log("[Help]  > " & RealString)
+                                    Next
+                                Case ".json"
+                                    FileList.Add(File.FullName)
+                            End Select
+                        Next
+                    End If
+                    Log("[Help] 已扫描 PCL 文件夹下的帮助文件,目前总计 " & FileList.Count & " 条")
+                    '读取自带文件
+                    For Each File In EnumerateFiles(PathTemp & "Help")
+                        '跳过非 json 文件与以 . 开头的文件夹
+                        If File.Extension.ToLower <> ".json" OrElse File.Directory.FullName.Replace(PathTemp & "Help", "").Contains("\.") Then Continue For
+                        '检查忽略列表
+                        Dim RealPath As String = File.FullName.Replace(PathTemp & "Help\", "")
+                        For Each Ignore In IgnoreList
+                            If RegexCheck(RealPath, Ignore) Then
+                                If ModeDebug Then Log("[Help] 已忽略 " & RealPath & ":" & Ignore)
+                                GoTo NextFile
+                            End If
+                        Next
+                        FileList.Add(File.FullName)
+NextFile:
+                    Next
+                    Log("[Help] 已扫描缓存文件夹下的帮助文件,目前总计 " & FileList.Count & " 条")
+                Catch ex As Exception
+                    Log(ex, "检查帮助文件夹失败", LogLevel.Msgbox)
+                End Try
+                If Loader.IsAborted Then Exit Sub
+
+                '将文件实例化
+                Dim Dict As New List(Of HelpEntry)
+                For Each FilePath As String In FileList
+                    Try
+                        Dim Entry As New HelpEntry(FilePath)
+                        Dict.Add(Entry)
+                        If ModeDebug Then Log("[Help] 已加载的帮助条目:" & Entry.Title & " ← " & FilePath)
+                    Catch ex As Exception
+                        Log(ex, "初始化帮助条目失败(" & FilePath & ")", LogLevel.Msgbox)
+                    End Try
+                Next
+
+                '回设
+                If Not Dict.Any() Then Throw New Exception("未找到可用的帮助;若不需要帮助页面,可以在 设置 → 个性化 → 功能隐藏 中将其隐藏")
+                If Loader.IsAborted Then Exit Sub
+                Loader.Output = Dict
+
+            Catch ex As Exception
+                Log(ex, "帮助列表初始化失败")
+                Throw
+            End Try
+        End SyncLock
+    End Sub
+    ''' <summary>
+    ''' 尝试解压内置帮助文件。
+    ''' </summary>
+    Public Sub HelpTryExtract()
+        If Setup.Get("SystemHelpVersion") <> VersionCode OrElse Not File.Exists(PathTemp & "Help\启动器\备份设置.xaml") Then
+            DeleteDirectory(PathTemp & "Help")
+            Directory.CreateDirectory(PathTemp & "Help")
+            WriteFile(PathTemp & "Cache\Help.zip", GetResources("Help"))
+            ExtractFile(PathTemp & "Cache\Help.zip", PathTemp & "Help", Encoding.UTF8)
+            Setup.Set("SystemHelpVersion", VersionCode)
+            Log("[Help] 已解压内置帮助文件,目前状态:" & File.Exists(PathTemp & "Help\启动器\备份设置.xaml"), LogLevel.Debug)
+        End If
+    End Sub
+    ''' <summary>
+    ''' 对帮助文件约定的替换标记进行处理,如果遇到需要转义的字符会进行转义。
+    ''' </summary>
+    Public Function HelpArgumentReplace(Xaml As String) As String
+        Dim Result = Xaml.Replace("{path}", EscapeXML(Path))
+        Result = RegexReplaceEach(Result, Function() EscapeXML(PageOtherTest.GetRandomHint()), "\{hint\}")
+        Result = RegexReplaceEach(Result, Function() EscapeXML(PageOtherTest.GetRandomCave()), "\{cave\}")
+        Return Result
+    End Function
+
+#End Region
+
+#Region "愚人节"
+
+    Public IsAprilEnabled As Boolean = Date.Now.Month = 4 AndAlso Date.Now.Day = 1
+    Public IsAprilGiveup As Boolean = False
+    Private AprilSpeed As New Vector(0, 0)
+    Private AprilIdieCount As Integer = 0, AprilMousePosLast As New Point(0, 0)
+    Private AprilDistance As Integer = 0
+    Private Sub TimerFool()
+        Try
+            If FrmLaunchLeft Is Nothing OrElse FrmLaunchLeft.AprilPosTrans Is Nothing OrElse FrmMain.lastMouseArg Is Nothing Then Exit Sub
+            If IsAprilGiveup OrElse FrmMain.PageCurrent <> FormMain.PageType.Launch OrElse AniControlEnabled <> 0 OrElse Not FrmLaunchLeft.BtnLaunch.IsLoaded Then Exit Sub
+
+            '计算是否空闲
+            Dim MousePos = FrmMain.lastMouseArg.GetPosition(FrmMain)
+            If MousePos = AprilMousePosLast Then
+                AprilIdieCount += 1
+            Else
+                AprilMousePosLast = MousePos
+                AprilIdieCount = 0
+            End If
+            '计算躲避移动
+            Dim Direction As Vector
+            Dim Distance As Double
+            Dim ButtonWidth = FrmLaunchLeft.BtnLaunch.ActualWidth / 2, ButtonHeight = FrmLaunchLeft.BtnLaunch.ActualHeight / 2
+            Dim Vec As Vector = FrmMain.lastMouseArg.GetPosition(FrmLaunchLeft.BtnLaunch) - New Vector(ButtonWidth, ButtonHeight)
+            Dim Dir As New Vector(Vec.X, Vec.Y)
+            Dir.Normalize()
+            Direction = -Dir
+            Distance = New Vector(Math.Max(0, Math.Abs(Vec.X) - ButtonWidth), Math.Max(0, Math.Abs(Vec.Y) - ButtonHeight)).Length
+            Dim BreathScale = Math.Sin(Timer150Count / 37.5 * Math.PI)
+            Dim Acc = Math.Max(0, BreathScale * 0.25 - 0.65 - Math.Log((Distance + 0.4) / 200)) * Direction '加速度
+            '计算回归移动
+            If AprilIdieCount >= 64 * 5 Then
+                Dim SafeDist As Vector = FrmMain.lastMouseArg.GetPosition(FrmMain.PanMain) - New Vector(ButtonWidth, FrmMain.PanMain.ActualHeight - ButtonHeight * 3)
+                Dim Back As New Vector(FrmLaunchLeft.AprilPosTrans.X, FrmLaunchLeft.AprilPosTrans.Y)
+                If SafeDist.Length > 250 AndAlso Back.Length > 0.4 Then
+                    Acc -= Back * 0.0005
+                    Back.Normalize()
+                    Acc -= Back * 0.15
+                End If
+            End If
+            '回到边界
+            Dim Relative As Point = FrmLaunchLeft.BtnLaunch.TranslatePoint(New Point(0, 0), FrmMain.PanForm)
+            If Relative.X < -ButtonWidth * 2 Then
+                FrmLaunchLeft.AprilPosTrans.X += FrmMain.PanForm.ActualWidth + ButtonWidth * 2 '离开左边界
+                AprilSpeed.X -= 80
+                If Relative.Y < 0 Then
+                    FrmLaunchLeft.AprilPosTrans.Y += ButtonHeight * 2.5
+                ElseIf Relative.Y > FrmMain.PanForm.ActualHeight - ButtonHeight * 2 Then
+                    FrmLaunchLeft.AprilPosTrans.Y -= ButtonHeight * 2.5
+                End If
+            ElseIf Relative.X > FrmMain.PanForm.ActualWidth Then
+                FrmLaunchLeft.AprilPosTrans.X -= FrmMain.PanForm.ActualWidth + ButtonWidth * 2 '离开右边界
+                AprilSpeed.X += 80
+                If Relative.Y < 0 Then
+                    FrmLaunchLeft.AprilPosTrans.Y += ButtonHeight * 2.5
+                ElseIf Relative.Y > FrmMain.PanForm.ActualHeight - ButtonHeight * 2 Then
+                    FrmLaunchLeft.AprilPosTrans.Y -= ButtonHeight * 2.5
+                End If
+            ElseIf Relative.Y < -ButtonHeight * 2 Then
+                FrmLaunchLeft.AprilPosTrans.Y += FrmMain.PanForm.ActualHeight + ButtonHeight * 2 '离开上边界
+                AprilSpeed.Y -= 25
+                If Relative.X < 0 Then
+                    FrmLaunchLeft.AprilPosTrans.X += ButtonWidth * 2
+                ElseIf Relative.X > FrmMain.PanForm.ActualWidth - ButtonWidth * 2 Then
+                    FrmLaunchLeft.AprilPosTrans.X -= ButtonWidth * 2
+                End If
+            ElseIf Relative.Y > FrmMain.PanForm.ActualHeight Then
+                FrmLaunchLeft.AprilPosTrans.Y -= FrmMain.PanForm.ActualHeight + ButtonHeight * 2 '离开下边界
+                AprilSpeed.Y += 25
+                If Relative.X < 0 Then
+                    FrmLaunchLeft.AprilPosTrans.X += ButtonWidth * 2
+                ElseIf Relative.X > FrmMain.PanForm.ActualWidth - ButtonWidth * 2 Then
+                    FrmLaunchLeft.AprilPosTrans.X -= ButtonWidth * 2
+                End If
+            End If
+            '移动
+            AprilSpeed = AprilSpeed * 0.8 + Acc
+            Dim SpeedValue = Math.Min(60, AprilSpeed.Length)
+            If SpeedValue < 0.01 Then Exit Sub
+            AprilSpeed.Normalize()
+            AprilSpeed *= SpeedValue
+            AprilDistance += SpeedValue
+            FrmLaunchLeft.AprilPosTrans.X += AprilSpeed.X
+            FrmLaunchLeft.AprilPosTrans.Y += AprilSpeed.Y
+            '大小改变
+            FrmLaunchLeft.AprilScaleTrans.ScaleX = MathClamp(1 - (Math.Abs(Direction.X) - Math.Abs(Direction.Y)) * (SpeedValue / 160), 0.2, 1.8)
+            FrmLaunchLeft.AprilScaleTrans.ScaleY = MathClamp(1 - (Math.Abs(Direction.Y) - Math.Abs(Direction.X)) * (SpeedValue / 100), 0.2, 1.8)
+            '放弃提示
+            If AprilDistance > 4000 Then
+                AprilDistance = -4000
+                Select Case RandomInteger(0, 3)
+                    Case 0
+                        Hint("放弃吧!只需要点一下右下角的小白旗……")
+                    Case 1
+                        Hint("看到右下角的那面小白旗了吗?")
+                    Case 2
+                        Hint("这里建议点一下右下角的小白旗投降呢.jpg")
+                    Case 3
+                        Hint("右下角的小白旗永远等着你……")
+                End Select
+            End If
+
+        Catch ex As Exception
+            Log(ex, "愚人节移动出错", LogLevel.Feedback)
+        End Try
+    End Sub
+
+#End Region
+
+#Region "系统"
+
+    ''' <summary>
+    ''' 把某个 PCL 窗口拖到最前面。
+    ''' </summary>
+    Public Sub ShowWindowToTop(Handle As IntPtr)
+        Try
+            PostMessage(Handle, 400 * 16 + 2, 0, 0)
+            SetForegroundWindow(Handle) '不在这里放不行,神秘 WinAPI,建议别动
+        Catch ex As Exception
+            Log(ex, "设置窗口置顶失败", LogLevel.Hint)
+        End Try
+    End Sub
+    Public Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ClassName As String, WindowName As String) As IntPtr
+    Public Declare Function SetForegroundWindow Lib "user32" (hWnd As IntPtr) As Integer
+    Private Declare Function PostMessage Lib "user32" Alias "PostMessageA" (hWnd As IntPtr, msg As UInteger, wParam As Long, lParam As Long) As Boolean
+
+#End Region
+
+    Public DragControl = Nothing
+    Private Timer4Count As Integer = 0
+    Private Timer150Count As Integer = 0
+    Private Sub TimerMain()
+        Try
+#Region "每 50ms 执行一次的代码"
+            HintTick()
+            MyMsgBoxTick()
+            FrmMain.DragTick()
+            LoaderTaskbarProgressRefresh()
+            If ThemeDontClick = 2 Then ThemeRefresh()
+#End Region
+        Catch ex As Exception
+            Log(ex, "短程主时钟执行异常", LogLevel.Assert)
+        End Try
+        Timer4Count += 1
+        If Timer4Count = 4 Then
+            Timer4Count = 0
+            Try
+#Region "每 250ms 执行一次的代码"
+                If ThemeNow = 12 Then ThemeRefresh()
+#End Region
+            Catch ex As Exception
+                Log(ex, "中程主时钟执行异常", LogLevel.Debug)
+            End Try
+        End If
+        Timer150Count += 1
+        If Timer150Count = 150 Then
+            Timer150Count = 0
+            Try
+#Region "每 7.5s 执行一次的代码"
+                If FrmMain.BtnExtraApril_ShowCheck AndAlso AprilDistance <> 0 Then FrmMain.BtnExtraApril.Ribble()
+                '以未知原因窗口被丢到一边去的修复(Top、Left = -25600),还有 #745
+                RunInUi(Sub()
+                            If Not FrmMain.Hidden Then
+                                If FrmMain.Top < -9000 Then FrmMain.Top = 100
+                                If FrmMain.Left < -9000 Then FrmMain.Left = 100 '窗口拉至最大时 Left = -18.8
+                            End If
+                        End Sub)
+#End Region
+            Catch ex As Exception
+                Log(ex, "长程主时钟执行异常", LogLevel.Assert)
+            End Try
+        End If
+    End Sub
+    Public Sub TimerMainStart()
+        RunInNewThread(
+        Sub()
+            Try
+                Do While True
+                    RunInUiWait(AddressOf TimerMain)
+                    Thread.Sleep(50 * 0.98)
+                Loop
+            Catch ex As Exception
+                Log(ex, "程序主时钟出错", LogLevel.Feedback)
+            End Try
+        End Sub, "Timer Main")
+        If Not IsAprilEnabled Then Exit Sub
+        RunInNewThread(
+        Sub()
+            Try
+                Dim LastTime = My.Computer.Clock.TickCount
+                Do While True
+                    If LastTime <> My.Computer.Clock.TickCount Then
+                        LastTime = My.Computer.Clock.TickCount
+                        RunInUiWait(AddressOf TimerFool)
+                    End If
+                    Thread.Sleep(1)
+                Loop
+            Catch ex As Exception
+                Log(ex, "愚人节主时钟出错", LogLevel.Feedback)
+            End Try
+        End Sub, "Timer Main Fool")
+    End Sub
+
+End Module

+ 16 - 14
Plain Craft Launcher 2/Modules/ModMusic.vb

@@ -1,6 +1,4 @@
-Imports System.Security.Cryptography
-
-Public Module ModMusic
+Public Module ModMusic
 
 
 #Region "播放列表"
 #Region "播放列表"
 
 
@@ -199,13 +197,15 @@ Public Module ModMusic
     ''' </summary>
     ''' </summary>
     Public Function MusicPause() As Boolean
     Public Function MusicPause() As Boolean
         If MusicState = MusicStates.Play Then
         If MusicState = MusicStates.Play Then
-            RunInThread(Sub()
-                            MusicNAudio.Pause()
-                            MusicRefreshUI()
-                            Log("[Music] 已暂停播放")
-                        End Sub)
+            RunInThread(
+            Sub()
+                Log("[Music] 已暂停播放")
+                MusicNAudio?.Pause()
+                MusicRefreshUI()
+            End Sub)
             Return True
             Return True
         Else
         Else
+            Log($"[Music] 无需暂停播放,当前状态为 {MusicState}")
             Return False
             Return False
         End If
         End If
     End Function
     End Function
@@ -214,13 +214,15 @@ Public Module ModMusic
     ''' </summary>
     ''' </summary>
     Public Function MusicResume() As Boolean
     Public Function MusicResume() As Boolean
         If MusicState = MusicStates.Play OrElse Not MusicAllList.Any() Then
         If MusicState = MusicStates.Play OrElse Not MusicAllList.Any() Then
+            Log($"[Music] 无需继续播放,当前状态为 {MusicState}")
             Return False
             Return False
         Else
         Else
-            RunInThread(Sub()
-                            MusicNAudio.Play()
-                            MusicRefreshUI()
-                            Log("[Music] 已恢复播放")
-                        End Sub)
+            RunInThread(
+            Sub()
+                Log("[Music] 已恢复播放")
+                MusicNAudio?.Play()
+                MusicRefreshUI()
+            End Sub)
             Return True
             Return True
         End If
         End If
     End Function
     End Function
@@ -266,7 +268,7 @@ Public Module ModMusic
                 Thread.Sleep(100)
                 Thread.Sleep(100)
             End While
             End While
             '当前音乐已播放结束,继续下一曲
             '当前音乐已播放结束,继续下一曲
-            If CurrentWave.PlaybackState = NAudio.Wave.PlaybackState.Stopped AndAlso MusicAllList.Count > 0 Then MusicStartPlay(DequeueNextMusicAddress)
+            If CurrentWave.PlaybackState = NAudio.Wave.PlaybackState.Stopped AndAlso MusicAllList.Any Then MusicStartPlay(DequeueNextMusicAddress)
         Catch ex As Exception
         Catch ex As Exception
             Log("[Music] 播放音乐失败的文件完整路径:" & MusicCurrent)
             Log("[Music] 播放音乐失败的文件完整路径:" & MusicCurrent)
             If TypeOf ex Is NAudio.MmException AndAlso (ex.Message.Contains("NoDriver") OrElse ex.Message.Contains("BadDeviceId")) Then
             If TypeOf ex Is NAudio.MmException AndAlso (ex.Message.Contains("NoDriver") OrElse ex.Message.Contains("BadDeviceId")) Then

+ 281 - 0
Plain Craft Launcher 2/Modules/ModSecret.vb

@@ -0,0 +1,281 @@
+'由于包含加解密等安全信息,本文件中的部分代码已被删除
+
+Imports System.Net
+Imports System.Reflection
+Imports System.Security.Cryptography
+
+Friend Module ModSecret
+
+#Region "杂项"
+
+    '在开源内容的注册表键与普通内容的注册表键隔离
+    Public Const RegFolder As String = "PCLDebug"
+
+    Friend Sub SecretOnApplicationStart()
+        '提升 UI 线程优先级
+        Thread.CurrentThread.Priority = ThreadPriority.Highest
+        '确保 .NET Framework 版本
+        Try
+            Dim VersionTest As New FormattedText("", Globalization.CultureInfo.CurrentCulture, FlowDirection.LeftToRight, Fonts.SystemTypefaces.First, 96, New MyColor, DPI)
+        Catch ex As UriFormatException '修复 #3555
+            Environment.SetEnvironmentVariable("windir", Environment.GetEnvironmentVariable("SystemRoot"), EnvironmentVariableTarget.User)
+            Dim VersionTest As New FormattedText("", Globalization.CultureInfo.CurrentCulture, FlowDirection.LeftToRight, Fonts.SystemTypefaces.First, 96, New MyColor, DPI)
+        End Try
+        '检测当前文件夹权限
+        Try
+            Directory.CreateDirectory(Path & "PCL")
+        Catch ex As Exception
+            MsgBox($"PCL 无法创建 PCL 文件夹({Path & "PCL"}),请尝试:" & vbCrLf &
+                  "1. 将 PCL 移动到其他文件夹" & If(Path.StartsWithF("C:", True), ",例如 C 盘和桌面以外的其他位置。", "。") & vbCrLf &
+                  "2. 删除当前目录中的 PCL 文件夹,然后再试。" & vbCrLf &
+                  "3. 右键 PCL 选择属性,打开 兼容性 中的 以管理员身份运行此程序。",
+                MsgBoxStyle.Critical, "运行环境错误")
+            Environment.[Exit](Result.Cancel)
+        End Try
+        If Not CheckPermission(Path & "PCL") Then
+            MsgBox("PCL 没有对当前文件夹的写入权限,请尝试:" & vbCrLf &
+                  "1. 将 PCL 移动到其他文件夹" & If(Path.StartsWithF("C:", True), ",例如 C 盘和桌面以外的其他位置。", "。") & vbCrLf &
+                  "2. 删除当前目录中的 PCL 文件夹,然后再试。" & vbCrLf &
+                  "3. 右键 PCL 选择属性,打开 兼容性 中的 以管理员身份运行此程序。",
+                MsgBoxStyle.Critical, "运行环境错误")
+            Environment.[Exit](Result.Cancel)
+        End If
+        '开源版本提示
+        MyMsgBox($"该版本中不包含以下特性:
+- CurseForge 查询:需要 API Key(你可以申请一个,然后添加到 SecretHeadersSign 方法中)
+- 更新与联网通知:避免滥用隐患
+- 主题切换:这是需要赞助解锁的纪念性质的功能,别让赞助者太伤心啦……
+- 百宝箱:开发早期往里面塞了些开发工具,整理起来太麻烦了", "开源版本说明")
+    End Sub
+
+    ''' <summary>
+    ''' 获取设备标识码。
+    ''' </summary>
+    Friend Function SecretGetUniqueAddress() As String
+        Return "0000-0000-0000-0000"
+    End Function
+
+    Friend Sub SecretLaunchJvmArgs(ByRef DataList As List(Of String))
+        Dim DataJvmCustom As String = Setup.Get("VersionAdvanceJvm", Version:=McVersionCurrent)
+        DataList.Insert(0, If(DataJvmCustom = "", Setup.Get("LaunchAdvanceJvm"), DataJvmCustom)) '可变 JVM 参数
+        McLaunchLog("当前剩余内存:" & Math.Round(My.Computer.Info.AvailablePhysicalMemory / 1024 / 1024 / 1024 * 10) / 10 & "G")
+        DataList.Add("-Xmn" & Math.Floor(PageVersionSetup.GetRam(McVersionCurrent) * 1024 * 0.15) & "m")
+        DataList.Add("-Xmx" & Math.Floor(PageVersionSetup.GetRam(McVersionCurrent) * 1024) & "m")
+        If Not DataList.Any(Function(d) d.Contains("-Dlog4j2.formatMsgNoLookups=true")) Then DataList.Add("-Dlog4j2.formatMsgNoLookups=true")
+    End Sub
+
+    ''' <summary>
+    ''' 打码字符串中的 AccessToken。
+    ''' </summary>
+    Friend Function SecretFilter(Raw As String, FilterChar As Char) As String
+        '打码 "accessToken " 后的内容
+        If Raw.Contains("accessToken ") Then
+            For Each Token In RegexSearch(Raw, "(?<=accessToken ([^ ]{5}))[^ ]+(?=[^ ]{5})")
+                Raw = Raw.Replace(Token, New String(FilterChar, Token.Count))
+            Next
+        End If
+        '打码当前登录的结果
+        Dim AccessToken As String = McLoginLoader.Output.AccessToken
+        If AccessToken Is Nothing OrElse AccessToken.Length < 10 OrElse Not Raw.ContainsF(AccessToken, True) OrElse
+            McLoginLoader.Output.Uuid = McLoginLoader.Output.AccessToken Then 'UUID 和 AccessToken 一样则不打码
+            Return Raw
+        Else
+            Return Raw.Replace(AccessToken, Left(AccessToken, 5) & New String(FilterChar, AccessToken.Length - 10) & Right(AccessToken, 5))
+        End If
+    End Function
+
+#End Region
+
+#Region "网络鉴权"
+
+    Friend Function SecretCdnSign(UrlWithMark As String)
+        '只处理以 {CDN} 结尾的链接
+        If Not UrlWithMark.EndsWithF("{CDN}") Then Return UrlWithMark
+        Return UrlWithMark.Replace("{CDN}", "").Replace(" ", "%20")
+    End Function
+    ''' <summary>
+    ''' 设置 Headers 的 UA、Referer。
+    ''' </summary>
+    Friend Sub SecretHeadersSign(Url As String, ByRef Client As CookieWebClient, Optional UseBrowserUserAgent As Boolean = False)
+        If ApplicationStartTick < 1 Then Return '禁止在没有运行程序的情况下通过反射调用
+        If UseBrowserUserAgent Then
+            Client.Headers("User-Agent") = "PCL2/" & VersionStandardCode & " Mozilla/5.0 AppleWebKit/537.36 Chrome/63.0.3239.132 Safari/537.36"
+        Else
+            Client.Headers("User-Agent") = "PCL2/" & VersionStandardCode
+        End If
+        Client.Headers("Referer") = "http://" & VersionCode & ".pcl2.server/"
+        '如果你有 CurseForge API Key,请添加到 Headers 中,以恢复对 CurseForge 的访问
+    End Sub
+    ''' <summary>
+    ''' 设置 Headers 的 UA、Referer。
+    ''' </summary>
+    Friend Sub SecretHeadersSign(Url As String, ByRef Request As HttpWebRequest, Optional UseBrowserUserAgent As Boolean = False)
+        If ApplicationStartTick < 1 Then Return '禁止在没有运行程序的情况下通过反射调用
+        If UseBrowserUserAgent Then
+            Request.UserAgent = "PCL2/" & VersionStandardCode & " Mozilla/5.0 AppleWebKit/537.36 Chrome/63.0.3239.132 Safari/537.36"
+        Else
+            Request.UserAgent = "PCL2/" & VersionStandardCode
+        End If
+        Request.Referer = "http://" & VersionCode & ".pcl2.server/"
+        '如果你有 CurseForge API Key,请添加到 Headers 中,以恢复对 CurseForge 的访问
+    End Sub
+
+#End Region
+
+#Region "字符串加解密"
+
+    ''' <summary>
+    ''' 获取八位密钥。
+    ''' </summary>
+    Private Function SecretKeyGet(Key As String) As String
+        Return "00000000"
+    End Function
+    ''' <summary>
+    ''' 加密字符串。
+    ''' </summary>
+    Friend Function SecretEncrypt(SourceString As String, Optional Key As String = "") As String
+        Key = SecretKeyGet(Key)
+        Dim btKey As Byte() = Encoding.UTF8.GetBytes(Key)
+        Dim btIV As Byte() = Encoding.UTF8.GetBytes("87160295")
+        Dim des As New DESCryptoServiceProvider
+        Using MS As New MemoryStream
+            Dim inData As Byte() = Encoding.UTF8.GetBytes(SourceString)
+            Using cs As New CryptoStream(MS, des.CreateEncryptor(btKey, btIV), CryptoStreamMode.Write)
+                cs.Write(inData, 0, inData.Length)
+                cs.FlushFinalBlock()
+                Return Convert.ToBase64String(MS.ToArray())
+            End Using
+        End Using
+    End Function
+    ''' <summary>
+    ''' 解密字符串。
+    ''' </summary>
+    Friend Function SecretDecrypt(SourceString As String, Optional Key As String = "") As String
+        Key = SecretKeyGet(Key)
+        Dim btKey As Byte() = Encoding.UTF8.GetBytes(Key)
+        Dim btIV As Byte() = Encoding.UTF8.GetBytes("87160295")
+        Dim des As New DESCryptoServiceProvider
+        Using MS As New MemoryStream
+            Dim inData As Byte() = Convert.FromBase64String(SourceString)
+            Using cs As New CryptoStream(MS, des.CreateDecryptor(btKey, btIV), CryptoStreamMode.Write)
+                cs.Write(inData, 0, inData.Length)
+                cs.FlushFinalBlock()
+                Return Encoding.UTF8.GetString(MS.ToArray())
+            End Using
+        End Using
+    End Function
+
+#End Region
+
+#Region "主题"
+
+    Public Color1 As New MyColor(52, 61, 74)
+    Public Color2 As New MyColor(11, 91, 203)
+    Public Color3 As New MyColor(19, 112, 243)
+    Public Color4 As New MyColor(72, 144, 245)
+    Public Color5 As New MyColor(150, 192, 249)
+    Public Color6 As New MyColor(213, 230, 253)
+    Public Color7 As New MyColor(222, 236, 253)
+    Public Color8 As New MyColor(234, 242, 254)
+    Public ColorBg0 As New MyColor(150, 192, 249)
+    Public ColorBg1 As New MyColor(190, Color7)
+    Public ColorGray1 As New MyColor(64, 64, 64)
+    Public ColorGray2 As New MyColor(115, 115, 115)
+    Public ColorGray3 As New MyColor(140, 140, 140)
+    Public ColorGray4 As New MyColor(166, 166, 166)
+    Public ColorGray5 As New MyColor(204, 204, 204)
+    Public ColorGray6 As New MyColor(235, 235, 235)
+    Public ColorGray7 As New MyColor(240, 240, 240)
+    Public ColorGray8 As New MyColor(245, 245, 245)
+    Public ColorSemiTransparent As New MyColor(1, Color8)
+
+    Public ThemeNow As Integer = -1
+    Public ColorHue As Integer = 210, ColorSat As Integer = 85, ColorLightAdjust As Integer = 0, ColorHueTopbarDelta As Object = 0
+    Public ThemeDontClick As Integer = 0
+
+    Public Sub ThemeRefresh(Optional NewTheme As Integer = -1)
+        Hint("该版本中不包含主题功能……")
+    End Sub
+    Public Sub ThemeRefreshMain()
+        RunInUi(
+        Sub()
+            If Not FrmMain.IsLoaded Then Exit Sub
+            '顶部条背景
+            Dim Brush = New LinearGradientBrush With {.EndPoint = New Point(1, 0), .StartPoint = New Point(0, 0)}
+            If ThemeNow = 5 Then
+                Brush.GradientStops.Add(New GradientStop With {.Offset = 0, .Color = New MyColor().FromHSL2(ColorHue, ColorSat, 25)})
+                Brush.GradientStops.Add(New GradientStop With {.Offset = 0.5, .Color = New MyColor().FromHSL2(ColorHue, ColorSat, 15)})
+                Brush.GradientStops.Add(New GradientStop With {.Offset = 1, .Color = New MyColor().FromHSL2(ColorHue, ColorSat, 25)})
+                FrmMain.PanTitle.Background = Brush
+                FrmMain.PanTitle.Background.Freeze()
+            ElseIf Not (ThemeNow = 12 OrElse ThemeDontClick = 2) Then
+                If TypeOf ColorHueTopbarDelta Is Integer Then
+                    Brush.GradientStops.Add(New GradientStop With {.Offset = 0, .Color = New MyColor().FromHSL2(ColorHue - ColorHueTopbarDelta, ColorSat, 48 + ColorLightAdjust)})
+                    Brush.GradientStops.Add(New GradientStop With {.Offset = 0.5, .Color = New MyColor().FromHSL2(ColorHue, ColorSat, 54 + ColorLightAdjust)})
+                    Brush.GradientStops.Add(New GradientStop With {.Offset = 1, .Color = New MyColor().FromHSL2(ColorHue + ColorHueTopbarDelta, ColorSat, 48 + ColorLightAdjust)})
+                Else
+                    Brush.GradientStops.Add(New GradientStop With {.Offset = 0, .Color = New MyColor().FromHSL2(ColorHue + ColorHueTopbarDelta(0), ColorSat, 48 + ColorLightAdjust)})
+                    Brush.GradientStops.Add(New GradientStop With {.Offset = 0.5, .Color = New MyColor().FromHSL2(ColorHue + ColorHueTopbarDelta(1), ColorSat, 54 + ColorLightAdjust)})
+                    Brush.GradientStops.Add(New GradientStop With {.Offset = 1, .Color = New MyColor().FromHSL2(ColorHue + ColorHueTopbarDelta(2), ColorSat, 48 + ColorLightAdjust)})
+                End If
+                FrmMain.PanTitle.Background = Brush
+                FrmMain.PanTitle.Background.Freeze()
+            Else
+                Brush.GradientStops.Add(New GradientStop With {.Offset = 0, .Color = New MyColor().FromHSL2(ColorHue - 21, ColorSat, 53 + ColorLightAdjust)})
+                Brush.GradientStops.Add(New GradientStop With {.Offset = 0.33, .Color = New MyColor().FromHSL2(ColorHue - 7, ColorSat, 47 + ColorLightAdjust)})
+                Brush.GradientStops.Add(New GradientStop With {.Offset = 0.67, .Color = New MyColor().FromHSL2(ColorHue + 7, ColorSat, 47 + ColorLightAdjust)})
+                Brush.GradientStops.Add(New GradientStop With {.Offset = 1, .Color = New MyColor().FromHSL2(ColorHue + 21, ColorSat, 53 + ColorLightAdjust)})
+                FrmMain.PanTitle.Background = Brush
+            End If
+            '主页面背景
+            If Setup.Get("UiBackgroundColorful") Then
+                Brush = New LinearGradientBrush With {.EndPoint = New Point(0.1, 1), .StartPoint = New Point(0.9, 0)}
+                Brush.GradientStops.Add(New GradientStop With {.Offset = -0.1, .Color = New MyColor().FromHSL2(ColorHue - 20, Math.Min(60, ColorSat) * 0.5, 80)})
+                Brush.GradientStops.Add(New GradientStop With {.Offset = 0.4, .Color = New MyColor().FromHSL2(ColorHue, ColorSat * 0.9, 90)})
+                Brush.GradientStops.Add(New GradientStop With {.Offset = 1.1, .Color = New MyColor().FromHSL2(ColorHue + 20, Math.Min(60, ColorSat) * 0.5, 80)})
+                FrmMain.PanForm.Background = Brush
+            Else
+                FrmMain.PanForm.Background = New MyColor(245, 245, 245)
+            End If
+            FrmMain.PanForm.Background.Freeze()
+        End Sub)
+    End Sub
+    Public Sub ThemeCheckAll(EffectSetup As Boolean)
+    End Sub
+    Public Function ThemeCheckOne(Id As Integer) As Boolean
+        Return True
+    End Function
+    Friend Function ThemeUnlock(Id As Integer, Optional ShowDoubleHint As Boolean = True, Optional UnlockHint As String = Nothing) As Boolean
+        Return False
+    End Function
+    Public Function ThemeCheckGold(Optional Code As String = Nothing) As Boolean
+        Return False
+    End Function
+    Public Function DonateCodeInput() As Boolean?
+    End Function
+
+#End Region
+
+#Region "更新"
+
+    Public IsUpdateStarted As Boolean = False
+    Public IsUpdateWaitingRestart As Boolean = False
+    Public Sub UpdateCheckByButton()
+        Hint("该版本中不包含更新功能……")
+    End Sub
+    Public Sub UpdateStart(BaseUrl As String, Slient As Boolean, Optional ReceivedKey As String = Nothing, Optional ForceValidated As Boolean = False)
+    End Sub
+    Public Sub UpdateRestart(TriggerRestartAndByEnd As Boolean)
+    End Sub
+    Public Sub UpdateReplace(ProcessId As Integer, OldFileName As String, NewFileName As String, TriggerRestart As Boolean)
+    End Sub
+
+#End Region
+
+#Region "联网通知"
+
+    Public ServerLoader As New LoaderTask(Of Integer, Integer)("PCL 服务", Sub() Log("[Server] 该版本中不包含更新通知功能……"), Priority:=ThreadPriority.BelowNormal)
+
+#End Region
+
+End Module

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

@@ -8,11 +8,11 @@ Imports System.Runtime.InteropServices
 
 
 '查看程序集特性的值
 '查看程序集特性的值
 
 
-<Assembly: AssemblyTitle("Plain Craft Launcher 2 启动器")>
+<Assembly: AssemblyTitle("Plain Craft Launcher 启动器")>
 <Assembly: AssemblyDescription("Minecraft 启动器 (制作:龙腾猫跃)")>
 <Assembly: AssemblyDescription("Minecraft 启动器 (制作:龙腾猫跃)")>
 <Assembly: AssemblyCompany("")>
 <Assembly: AssemblyCompany("")>
-<Assembly: AssemblyProduct("Plain Craft Launcher 2")>
-<Assembly: AssemblyCopyright("Copyright © 龙腾猫跃 2016-2024. All Rights Reserved.")>
+<Assembly: AssemblyProduct("Plain Craft Launcher")>
+<Assembly: AssemblyCopyright("Copyright © 龙腾猫跃 2016. All Rights Reserved.")>
 <Assembly: AssemblyConfiguration("PCL2 Config Mark")>
 <Assembly: AssemblyConfiguration("PCL2 Config Mark")>
 <Assembly: ComVisible(False)>
 <Assembly: ComVisible(False)>
 
 
@@ -51,6 +51,6 @@ Imports System.Runtime.InteropServices
 ' 可以指定所有值,也可以使用以下所示的 "*" 预置版本号和修订号
 ' 可以指定所有值,也可以使用以下所示的 "*" 预置版本号和修订号
 ' 方法是按如下所示使用“*”
 ' 方法是按如下所示使用“*”
 
 
-<Assembly: AssemblyVersion("2.7.1.0")>
-<Assembly: AssemblyFileVersion("2.7.1.0")>
+<Assembly: AssemblyVersion("2.7.4.0")>
+<Assembly: AssemblyFileVersion("2.7.4.0")>
 <Assembly: NeutralResourcesLanguage("")>
 <Assembly: NeutralResourcesLanguage("")>

+ 74 - 61
Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb

@@ -384,20 +384,27 @@ Public Module ModDownloadLib
     End Sub
     End Sub
     Private Sub McDownloadOptiFineInstall(BaseMcFolderHome As String, Target As String, Task As LoaderTask(Of List(Of NetFile), Boolean), UseJavaWrapper As Boolean)
     Private Sub McDownloadOptiFineInstall(BaseMcFolderHome As String, Target As String, Task As LoaderTask(Of List(Of NetFile), Boolean), UseJavaWrapper As Boolean)
         '选择 Java
         '选择 Java
-        Dim Java = JavaSelect("已取消安装。", New Version(1, 8, 0, 0))
-        If Java Is Nothing Then
-            If Not JavaDownloadConfirm("Java 8 或更高版本") Then Throw New Exception("由于未找到 Java,已取消安装。")
-            '开始自动下载
-            Dim JavaLoader = JavaFixLoaders(17)
-            JavaLoader.Start(17, IsForceRestart:=True)
-            Do While JavaLoader.State = LoadState.Loading AndAlso Not Task.IsAborted
-                Thread.Sleep(10)
-            Loop
-            '检查下载结果
+        Dim Java As JavaEntry
+        SyncLock JavaLock
             Java = JavaSelect("已取消安装。", New Version(1, 8, 0, 0))
             Java = JavaSelect("已取消安装。", New Version(1, 8, 0, 0))
-            If Task.IsAborted Then Exit Sub
-            If Java Is Nothing Then Throw New Exception("由于未找到 Java,已取消安装。")
-        End If
+            If Java Is Nothing Then
+                If Not JavaDownloadConfirm("Java 8 或更高版本") Then Throw New Exception("由于未找到 Java,已取消安装。")
+                '开始自动下载
+                Dim JavaLoader = JavaFixLoaders(17)
+                Try
+                    JavaLoader.Start(17, IsForceRestart:=True)
+                    Do While JavaLoader.State = LoadState.Loading AndAlso Not Task.IsAborted
+                        Thread.Sleep(10)
+                    Loop
+                Finally
+                    JavaLoader.Abort() '确保取消时中止 Java 下载
+                End Try
+                '检查下载结果
+                Java = JavaSelect("已取消安装。", New Version(1, 8, 0, 0))
+                If Task.IsAborted Then Exit Sub
+                If Java Is Nothing Then Throw New Exception("由于未找到 Java,已取消安装。")
+            End If
+        End SyncLock
         '添加 Java Wrapper 作为主 Jar
         '添加 Java Wrapper 作为主 Jar
         Dim Arguments As String
         Dim Arguments As String
         If UseJavaWrapper Then
         If UseJavaWrapper Then
@@ -487,7 +494,7 @@ Public Module ModDownloadLib
                     outputWaitHandle.WaitOne(10000)
                     outputWaitHandle.WaitOne(10000)
                     errorWaitHandle.WaitOne(10000)
                     errorWaitHandle.WaitOne(10000)
                     process.Dispose()
                     process.Dispose()
-                    If TotalLength < 1000 Then Throw New Exception("安装器运行出错,末行为 " & LastResult)
+                    If TotalLength < 1000 OrElse LastResult.Contains("at ") Then Throw New Exception("安装器运行出错,末行为 " & LastResult)
                 End Using
                 End Using
             End Using
             End Using
         End SyncLock
         End SyncLock
@@ -538,7 +545,7 @@ Public Module ModDownloadLib
                 'OptiFine 中文镜像源
                 'OptiFine 中文镜像源
                 Sources.Add("https://optifine.cn/download/" & DownloadInfo.NameFile)
                 Sources.Add("https://optifine.cn/download/" & DownloadInfo.NameFile)
                 '构造文件请求
                 '构造文件请求
-                Task.Output = New List(Of NetFile) From {New NetFile(Sources.ToArray, Target, New FileChecker(MinSize:=64 * 1024))}
+                Task.Output = New List(Of NetFile) From {New NetFile(Sources.ToArray, Target, New FileChecker(MinSize:=300 * 1024))}
             End Sub) With {.ProgressWeight = 8})
             End Sub) With {.ProgressWeight = 8})
         Loaders.Add(New LoaderDownload("下载 OptiFine 主文件", New List(Of NetFile)) With {.ProgressWeight = 8})
         Loaders.Add(New LoaderDownload("下载 OptiFine 主文件", New List(Of NetFile)) With {.ProgressWeight = 8})
         Loaders.Add(New LoaderTask(Of List(Of NetFile), Boolean)("等待原版下载",
         Loaders.Add(New LoaderTask(Of List(Of NetFile), Boolean)("等待原版下载",
@@ -548,8 +555,8 @@ Public Module ModDownloadLib
                 Dim TargetLoaders As List(Of LoaderBase) =
                 Dim TargetLoaders As List(Of LoaderBase) =
                    ClientDownloadLoader.GetLoaderList.Where(Function(l) l.Name = McDownloadClientLibName OrElse l.Name = McDownloadClientJsonName).
                    ClientDownloadLoader.GetLoaderList.Where(Function(l) l.Name = McDownloadClientLibName OrElse l.Name = McDownloadClientJsonName).
                    Where(Function(l) l.State <> LoadState.Finished).ToList
                    Where(Function(l) l.State <> LoadState.Finished).ToList
-                If TargetLoaders.Count > 0 Then Log("[Download] OptiFine 安装正在等待原版文件下载完成")
-                Do While TargetLoaders.Count > 0 AndAlso Not Task.IsAborted
+                If TargetLoaders.Any Then Log("[Download] OptiFine 安装正在等待原版文件下载完成")
+                Do While TargetLoaders.Any AndAlso Not Task.IsAborted
                     TargetLoaders = TargetLoaders.Where(Function(l) l.State <> LoadState.Finished).ToList
                     TargetLoaders = TargetLoaders.Where(Function(l) l.State <> LoadState.Finished).ToList
                     Thread.Sleep(50)
                     Thread.Sleep(50)
                 Loop
                 Loop
@@ -560,12 +567,10 @@ Public Module ModDownloadLib
                     Dim ClientName As String = GetFolderNameFromPath(ClientFolder)
                     Dim ClientName As String = GetFolderNameFromPath(ClientFolder)
                     Directory.CreateDirectory(McFolder & "versions\" & DownloadInfo.Inherit)
                     Directory.CreateDirectory(McFolder & "versions\" & DownloadInfo.Inherit)
                     If Not File.Exists(McFolder & "versions\" & DownloadInfo.Inherit & "\" & DownloadInfo.Inherit & ".json") Then
                     If Not File.Exists(McFolder & "versions\" & DownloadInfo.Inherit & "\" & DownloadInfo.Inherit & ".json") Then
-                        CopyFile(ClientFolder & ClientName & ".json",
-                                                                                          McFolder & "versions\" & DownloadInfo.Inherit & "\" & DownloadInfo.Inherit & ".json")
+                        CopyFile($"{ClientFolder}{ClientName}.json", $"{McFolder}versions\{DownloadInfo.Inherit}\{DownloadInfo.Inherit}.json")
                     End If
                     End If
                     If Not File.Exists(McFolder & "versions\" & DownloadInfo.Inherit & "\" & DownloadInfo.Inherit & ".jar") Then
                     If Not File.Exists(McFolder & "versions\" & DownloadInfo.Inherit & "\" & DownloadInfo.Inherit & ".jar") Then
-                        CopyFile(ClientFolder & ClientName & ".jar",
-                                                                                              McFolder & "versions\" & DownloadInfo.Inherit & "\" & DownloadInfo.Inherit & ".jar")
+                        CopyFile($"{ClientFolder}{ClientName}.jar", $"{McFolder}versions\{DownloadInfo.Inherit}\{DownloadInfo.Inherit}.jar")
                     End If
                     End If
                 End SyncLock
                 End SyncLock
             End Sub) With {.ProgressWeight = 0.1, .Show = False})
             End Sub) With {.ProgressWeight = 0.1, .Show = False})
@@ -1109,20 +1114,27 @@ Retry:
 
 
     Private Sub ForgeInjector(Target As String, Task As LoaderTask(Of Boolean, Boolean), McFolder As String, UseJavaWrapper As Boolean)
     Private Sub ForgeInjector(Target As String, Task As LoaderTask(Of Boolean, Boolean), McFolder As String, UseJavaWrapper As Boolean)
         '选择 Java
         '选择 Java
-        Dim Java = JavaSelect("已取消安装。", New Version(1, 8, 0, 60))
-        If Java Is Nothing Then
-            If Not JavaDownloadConfirm("Java 8 或更高版本") Then Throw New Exception("由于未找到 Java,已取消安装。")
-            '开始自动下载
-            Dim JavaLoader = JavaFixLoaders(17)
-            JavaLoader.Start(17, IsForceRestart:=True)
-            Do While JavaLoader.State = LoadState.Loading AndAlso Not Task.IsAborted
-                Thread.Sleep(10)
-            Loop
-            '检查下载结果
+        Dim Java As JavaEntry
+        SyncLock JavaLock
             Java = JavaSelect("已取消安装。", New Version(1, 8, 0, 60))
             Java = JavaSelect("已取消安装。", New Version(1, 8, 0, 60))
-            If Task.IsAborted Then Exit Sub
-            If Java Is Nothing Then Throw New Exception("由于未找到 Java,已取消安装。")
-        End If
+            If Java Is Nothing Then
+                If Not JavaDownloadConfirm("Java 8 或更高版本") Then Throw New Exception("由于未找到 Java,已取消安装。")
+                '开始自动下载
+                Dim JavaLoader = JavaFixLoaders(17)
+                Try
+                    JavaLoader.Start(17, IsForceRestart:=True)
+                    Do While JavaLoader.State = LoadState.Loading AndAlso Not Task.IsAborted
+                        Thread.Sleep(10)
+                    Loop
+                Finally
+                    JavaLoader.Abort() '确保取消时中止 Java 下载
+                End Try
+                '检查下载结果
+                Java = JavaSelect("已取消安装。", New Version(1, 8, 0, 60))
+                If Task.IsAborted Then Exit Sub
+                If Java Is Nothing Then Throw New Exception("由于未找到 Java,已取消安装。")
+            End If
+        End SyncLock
         '添加 Java Wrapper 作为主 Jar
         '添加 Java Wrapper 作为主 Jar
         Dim Arguments As String
         Dim Arguments As String
         If UseJavaWrapper Then
         If UseJavaWrapper Then
@@ -1269,8 +1281,8 @@ Retry:
         McFolder = If(McFolder, PathMcFolder)
         McFolder = If(McFolder, PathMcFolder)
         If Version.StartsWithF("1.") AndAlso Version.Contains("-") Then
         If Version.StartsWithF("1.") AndAlso Version.Contains("-") Then
             '类似 1.19.3-41.2.8 格式,优先使用 Version 中要求的版本而非 Inherit(例如 1.19.3 却使用了 1.19 的 Forge)
             '类似 1.19.3-41.2.8 格式,优先使用 Version 中要求的版本而非 Inherit(例如 1.19.3 却使用了 1.19 的 Forge)
-            Inherit = Version.Split("-").First
-            Version = Version.Split("-").Last
+            Inherit = Version.Before("-")
+            Version = Version.After("-")
         End If
         End If
         Dim IsCustomFolder As Boolean = McFolder <> PathMcFolder
         Dim IsCustomFolder As Boolean = McFolder <> PathMcFolder
         Dim Id As String = Inherit & "-forge-" & Version
         Dim Id As String = Inherit & "-forge-" & Version
@@ -1283,20 +1295,20 @@ Retry:
         '获取下载信息
         '获取下载信息
         If DownloadInfo Is Nothing Then
         If DownloadInfo Is Nothing Then
             Loaders.Add(New LoaderTask(Of String, String)("获取 Mod 加载器详细信息",
             Loaders.Add(New LoaderTask(Of String, String)("获取 Mod 加载器详细信息",
-                                                            Sub(Task As LoaderTask(Of String, String))
-                                                                '获取 Forge 版本列表
-                                                                Dim ForgeLoader = New LoaderTask(Of String, List(Of DlForgeVersionEntry))("McDownloadForgeLoader " & Inherit, AddressOf DlForgeVersionMain)
-                                                                ForgeLoader.WaitForExit(Inherit)
-                                                                Task.Progress = 0.8
-                                                                '查找对应版本
-                                                                For Each ForgeVersion In ForgeLoader.Output
-                                                                    If ForgeVersion.Version = Version Then
-                                                                        DownloadInfo = ForgeVersion
-                                                                        Exit Sub
-                                                                    End If
-                                                                Next
-                                                                Throw New Exception("未能找到 Forge " & Inherit & "-" & Version & " 的详细信息!")
-                                                            End Sub) With {.ProgressWeight = 3})
+            Sub(Task As LoaderTask(Of String, String))
+                '获取 Forge 版本列表
+                Dim ForgeLoader = New LoaderTask(Of String, List(Of DlForgeVersionEntry))("McDownloadForgeLoader " & Inherit, AddressOf DlForgeVersionMain)
+                ForgeLoader.WaitForExit(Inherit)
+                Task.Progress = 0.8
+                '查找对应版本
+                For Each ForgeVersion In ForgeLoader.Output
+                    If ForgeVersion.Version = Version Then
+                        DownloadInfo = ForgeVersion
+                        Exit Sub
+                    End If
+                Next
+                Throw New Exception("未能找到 Forge " & Inherit & "-" & Version & " 的详细信息!")
+            End Sub) With {.ProgressWeight = 3})
         End If
         End If
         '下载 Forge 主文件
         '下载 Forge 主文件
         Loaders.Add(New LoaderTask(Of String, List(Of NetFile))("准备 Mod 加载器下载",
         Loaders.Add(New LoaderTask(Of String, List(Of NetFile))("准备 Mod 加载器下载",
@@ -1316,7 +1328,7 @@ Retry:
         Loaders.Add(New LoaderDownload("下载 Mod 加载器主文件", New List(Of NetFile)) With {.ProgressWeight = 9})
         Loaders.Add(New LoaderDownload("下载 Mod 加载器主文件", New List(Of NetFile)) With {.ProgressWeight = 9})
 
 
         '安装(仅在新版安装时需要原版 Jar)
         '安装(仅在新版安装时需要原版 Jar)
-        If Version.Split(".")(0) >= 20 Then
+        If Version.Before(".") >= 20 Then
             Log("[Download] 检测为新版 Forge:" & Version)
             Log("[Download] 检测为新版 Forge:" & Version)
             Dim Libs As List(Of McLibToken) = Nothing
             Dim Libs As List(Of McLibToken) = Nothing
             Loaders.Add(New LoaderTask(Of String, List(Of NetFile))("分析 Mod 加载器支持库文件",
             Loaders.Add(New LoaderTask(Of String, List(Of NetFile))("分析 Mod 加载器支持库文件",
@@ -1338,7 +1350,7 @@ Retry:
                         Task.Progress = 0.4
                         Task.Progress = 0.4
                         Dim RawJson As JObject = GetJson(NetGetCodeByDownload(DlSourceLauncherOrMetaGet(DlClientListGet(Inherit)), IsJson:=True))
                         Dim RawJson As JObject = GetJson(NetGetCodeByDownload(DlSourceLauncherOrMetaGet(DlClientListGet(Inherit)), IsJson:=True))
                         '[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()).Split("@")(0)
+                        Dim OriginalName As String = Json("data")("MOJMAPS")("client").ToString.Trim("[]".ToCharArray()).Before("@")
                         Dim Address = McLibGet(OriginalName).Replace(".jar", "-mappings." & Json("data")("MOJMAPS")("client").ToString.Trim("[]".ToCharArray()).Split("@")(1))
                         Dim Address = McLibGet(OriginalName).Replace(".jar", "-mappings." & Json("data")("MOJMAPS")("client").ToString.Trim("[]".ToCharArray()).Split("@")(1))
                         Libs.Add(New McLibToken With {.IsJumpLoader = False, .IsNatives = False, .LocalPath = Address, .OriginalName = OriginalName,
                         Libs.Add(New McLibToken With {.IsJumpLoader = False, .IsNatives = False, .LocalPath = Address, .OriginalName = OriginalName,
                                                                                                             .Url = RawJson("downloads")("client_mappings")("url"), .Size = RawJson("downloads")("client_mappings")("size"), .SHA1 = RawJson("downloads")("client_mappings")("sha1")})
                                                                                                             .Url = RawJson("downloads")("client_mappings")("url"), .Size = RawJson("downloads")("client_mappings")("size"), .SHA1 = RawJson("downloads")("client_mappings")("sha1")})
@@ -1383,8 +1395,8 @@ Retry:
                 Dim TargetLoaders As List(Of LoaderBase) =
                 Dim TargetLoaders As List(Of LoaderBase) =
                                                                                 ClientDownloadLoader.GetLoaderList.Where(Function(l) l.Name = McDownloadClientLibName OrElse l.Name = McDownloadClientJsonName).
                                                                                 ClientDownloadLoader.GetLoaderList.Where(Function(l) l.Name = McDownloadClientLibName OrElse l.Name = McDownloadClientJsonName).
                                                                                 Where(Function(l) l.State <> LoadState.Finished).ToList
                                                                                 Where(Function(l) l.State <> LoadState.Finished).ToList
-                If TargetLoaders.Count > 0 Then Log("[Download] Forge 安装正在等待原版文件下载完成")
-                Do While TargetLoaders.Count > 0 AndAlso Not Task.IsAborted
+                If TargetLoaders.Any Then Log("[Download] Forge 安装正在等待原版文件下载完成")
+                Do While TargetLoaders.Any AndAlso Not Task.IsAborted
                     TargetLoaders = TargetLoaders.Where(Function(l) l.State <> LoadState.Finished).ToList
                     TargetLoaders = TargetLoaders.Where(Function(l) l.State <> LoadState.Finished).ToList
                     Thread.Sleep(50)
                     Thread.Sleep(50)
                 Loop
                 Loop
@@ -1441,7 +1453,7 @@ Retry:
                     End Try
                     End Try
                     '拷贝新增的版本 Json
                     '拷贝新增的版本 Json
                     Dim DeltaList = New DirectoryInfo(McFolder & "versions\").EnumerateDirectories.
                     Dim DeltaList = New DirectoryInfo(McFolder & "versions\").EnumerateDirectories.
-                                                                        SkipWhile(Function(i) OldList.Contains(i.FullName)).ToList()
+                        SkipWhile(Function(i) OldList.Contains(i.FullName)).ToList()
                     If DeltaList.Count > 1 Then
                     If DeltaList.Count > 1 Then
                         '它可能和 OptiFine 安装同时运行,导致增加的文件不止一个(这导致了 #151)
                         '它可能和 OptiFine 安装同时运行,导致增加的文件不止一个(这导致了 #151)
                         '也可能是因为 Forge 安装器的 Bug,生成了一个名字错误的文件夹,所以需要检查文件夹是否为空
                         '也可能是因为 Forge 安装器的 Bug,生成了一个名字错误的文件夹,所以需要检查文件夹是否为空
@@ -1537,7 +1549,7 @@ Retry:
     Public Sub ForgeDownloadListItemPreload(Stack As StackPanel, Entrys As List(Of DlForgeVersionEntry), OnClick As MyListItem.ClickEventHandler, IsSaveOnly As Boolean)
     Public Sub ForgeDownloadListItemPreload(Stack As StackPanel, Entrys As List(Of DlForgeVersionEntry), OnClick As MyListItem.ClickEventHandler, IsSaveOnly As Boolean)
         '获取推荐版本与最新版本
         '获取推荐版本与最新版本
         Dim FreshVersion As DlForgeVersionEntry = Nothing
         Dim FreshVersion As DlForgeVersionEntry = Nothing
-        If Entrys.Count > 0 Then
+        If Entrys.Any Then
             FreshVersion = Entrys(0)
             FreshVersion = Entrys(0)
         Else
         Else
             Log("[System] 未找到可用的 Forge 版本", LogLevel.Debug)
             Log("[System] 未找到可用的 Forge 版本", LogLevel.Debug)
@@ -1782,7 +1794,7 @@ Retry:
     Public Function FabricApiDownloadListItem(Entry As CompFile, OnClick As MyListItem.ClickEventHandler) As MyListItem
     Public Function FabricApiDownloadListItem(Entry As CompFile, OnClick As MyListItem.ClickEventHandler) As MyListItem
         '建立控件
         '建立控件
         Dim NewItem As New MyListItem With {
         Dim NewItem As New MyListItem With {
-            .Title = Entry.DisplayName.Split("]")(1).Replace("Fabric API ", "").Replace(" build ", ".").Split("+").First.Trim, .SnapsToDevicePixels = True, .Height = 42, .Type = MyListItem.CheckType.Clickable, .Tag = Entry,
+            .Title = Entry.DisplayName.Split("]")(1).Replace("Fabric API ", "").Replace(" build ", ".").Before("+").Trim, .SnapsToDevicePixels = True, .Height = 42, .Type = MyListItem.CheckType.Clickable, .Tag = Entry,
             .Info = Entry.StatusDescription & ",发布于 " & Entry.ReleaseDate.ToString("yyyy'/'MM'/'dd HH':'mm"),
             .Info = Entry.StatusDescription & ",发布于 " & Entry.ReleaseDate.ToString("yyyy'/'MM'/'dd HH':'mm"),
             .Logo = PathImage & "Blocks/Fabric.png"
             .Logo = PathImage & "Blocks/Fabric.png"
         }
         }
@@ -1972,7 +1984,7 @@ Retry:
         If Directory.Exists(TempMcFolder) Then DeleteDirectory(TempMcFolder)
         If Directory.Exists(TempMcFolder) Then DeleteDirectory(TempMcFolder)
         Dim OptiFineFolder As String = Nothing
         Dim OptiFineFolder As String = Nothing
         If Request.OptiFineVersion IsNot Nothing Then
         If Request.OptiFineVersion IsNot Nothing Then
-            If Request.OptiFineVersion.Contains("_HD_U_") Then Request.OptiFineVersion = "HD_U_" & Request.OptiFineVersion.Split("_HD_U_").Last '#735
+            If Request.OptiFineVersion.Contains("_HD_U_") Then Request.OptiFineVersion = "HD_U_" & Request.OptiFineVersion.After("_HD_U_") '#735
             Request.OptiFineEntry = New DlOptiFineListEntry With {
             Request.OptiFineEntry = New DlOptiFineListEntry With {
                 .NameDisplay = Request.MinecraftName & " " & Request.OptiFineVersion.Replace("HD_U_", "").Replace("_", "").Replace("pre", " pre"),
                 .NameDisplay = Request.MinecraftName & " " & Request.OptiFineVersion.Replace("HD_U_", "").Replace("_", "").Replace("pre", " pre"),
                 .Inherit = Request.MinecraftName,
                 .Inherit = Request.MinecraftName,
@@ -2014,7 +2026,7 @@ Retry:
 
 
         Dim LoaderList As New List(Of LoaderBase)
         Dim LoaderList As New List(Of LoaderBase)
         '添加忽略标识
         '添加忽略标识
-        LoaderList.Add(New LoaderTask(Of Integer, Integer)("添加忽略标识", Sub() WriteFile(OutputFolder & ".pclignore", "用于临时地 PCL 的版本列表中屏蔽此版本。")) With {.Show = False, .Block = False})
+        LoaderList.Add(New LoaderTask(Of Integer, Integer)("添加忽略标识", Sub() WriteFile(OutputFolder & ".pclignore", "用于临时地在 PCL 的版本列表中屏蔽此版本。")) With {.Show = False, .Block = False})
         'Fabric API
         'Fabric API
         If Request.FabricApi IsNot Nothing Then
         If Request.FabricApi IsNot Nothing Then
             LoaderList.Add(New LoaderDownload("下载 Fabric API", New List(Of NetFile) From {Request.FabricApi.ToNetFile(New McVersion(OutputFolder).GetPathIndie(True) & "mods\")}) With {.ProgressWeight = 3, .Block = False})
             LoaderList.Add(New LoaderDownload("下载 Fabric API", New List(Of NetFile) From {Request.FabricApi.ToNetFile(New McVersion(OutputFolder).GetPathIndie(True) & "mods\")}) With {.ProgressWeight = 3, .Block = False})
@@ -2056,7 +2068,7 @@ Retry:
             End Sub) With {.ProgressWeight = 2, .Block = True})
             End Sub) With {.ProgressWeight = 2, .Block = True})
         '补全文件
         '补全文件
         If Not DontFixLibraries AndAlso
         If Not DontFixLibraries AndAlso
-        (Request.OptiFineEntry IsNot Nothing OrElse (Request.ForgeVersion IsNot Nothing AndAlso Request.ForgeVersion.Split(".")(0) >= 20) OrElse Request.FabricVersion IsNot Nothing OrElse Request.LiteLoaderEntry IsNot Nothing) Then
+        (Request.OptiFineEntry IsNot Nothing OrElse (Request.ForgeVersion IsNot Nothing AndAlso Request.ForgeVersion.Before(".") >= 20) OrElse Request.FabricVersion IsNot Nothing OrElse Request.LiteLoaderEntry IsNot Nothing) Then
             Dim LoadersLib As New List(Of LoaderBase)
             Dim LoadersLib As New List(Of LoaderBase)
             LoadersLib.Add(New LoaderTask(Of String, List(Of NetFile))("分析游戏支持库文件(副加载器)", Sub(Task As LoaderTask(Of String, List(Of NetFile))) Task.Output = McLibFix(New McVersion(OutputFolder))) With {.ProgressWeight = 1, .Show = False})
             LoadersLib.Add(New LoaderTask(Of String, List(Of NetFile))("分析游戏支持库文件(副加载器)", Sub(Task As LoaderTask(Of String, List(Of NetFile))) Task.Output = McLibFix(New McVersion(OutputFolder))) With {.ProgressWeight = 1, .Show = False})
             LoadersLib.Add(New LoaderDownload("下载游戏支持库文件(副加载器)", New List(Of NetFile)) With {.ProgressWeight = 7, .Show = False})
             LoadersLib.Add(New LoaderDownload("下载游戏支持库文件(副加载器)", New List(Of NetFile)) With {.ProgressWeight = 7, .Show = False})
@@ -2164,7 +2176,7 @@ Retry:
         For i = 0 To RawArguments.Count - 1
         For i = 0 To RawArguments.Count - 1
             If RawArguments(i).StartsWithF("-") Then
             If RawArguments(i).StartsWithF("-") Then
                 SplitArguments.Add(RawArguments(i))
                 SplitArguments.Add(RawArguments(i))
-            ElseIf SplitArguments.Count > 0 AndAlso SplitArguments.Last.StartsWithF("-") AndAlso Not SplitArguments.Last.Contains(" ") Then
+            ElseIf SplitArguments.Any AndAlso SplitArguments.Last.StartsWithF("-") AndAlso Not SplitArguments.Last.Contains(" ") Then
                 SplitArguments(SplitArguments.Count - 1) = SplitArguments.Last & " " & RawArguments(i)
                 SplitArguments(SplitArguments.Count - 1) = SplitArguments.Last & " " & RawArguments(i)
             Else
             Else
                 SplitArguments.Add(RawArguments(i))
                 SplitArguments.Add(RawArguments(i))
@@ -2172,6 +2184,7 @@ Retry:
         Next
         Next
         Dim RealArguments As String = Join(SplitArguments.Distinct.ToList, " ")
         Dim RealArguments As String = Join(SplitArguments.Distinct.ToList, " ")
         '合并
         '合并
+        '相关讨论见 #2801
         OutputJson = MinecraftJson
         OutputJson = MinecraftJson
         If HasOptiFine Then
         If HasOptiFine Then
             '合并 OptiFine
             '合并 OptiFine

+ 4 - 4
Plain Craft Launcher 2/Pages/PageDownload/PageDownloadCompDetail.xaml.vb

@@ -210,10 +210,10 @@
                 If Project.Type = CompType.Mod Then
                 If Project.Type = CompType.Mod Then
                     '获取 Mod 所需的加载器种类
                     '获取 Mod 所需的加载器种类
                     Dim AllowForge As Boolean? = Nothing, AllowFabric As Boolean? = Nothing
                     Dim AllowForge As Boolean? = Nothing, AllowFabric As Boolean? = Nothing
-                    If File.ModLoaders.Count > 0 Then '从文件中获取
+                    If File.ModLoaders.Any Then '从文件中获取
                         AllowForge = File.ModLoaders.Contains(CompModLoaderType.Forge)
                         AllowForge = File.ModLoaders.Contains(CompModLoaderType.Forge)
                         AllowFabric = File.ModLoaders.Contains(CompModLoaderType.Fabric)
                         AllowFabric = File.ModLoaders.Contains(CompModLoaderType.Fabric)
-                    ElseIf Project.ModLoaders.Count > 0 Then '从工程中获取
+                    ElseIf Project.ModLoaders.Any Then '从工程中获取
                         AllowForge = Project.ModLoaders.Contains(CompModLoaderType.Forge)
                         AllowForge = Project.ModLoaders.Contains(CompModLoaderType.Forge)
                         AllowFabric = Project.ModLoaders.Contains(CompModLoaderType.Fabric)
                         AllowFabric = Project.ModLoaders.Contains(CompModLoaderType.Fabric)
                     End If
                     End If
@@ -275,7 +275,7 @@
                 If Project.TranslatedName = Project.RawName Then
                 If Project.TranslatedName = Project.RawName Then
                     FileName = File.FileName
                     FileName = File.FileName
                 Else
                 Else
-                    Dim ChineseName As String = Project.TranslatedName.Split(" (").First.Split(" - ").First.
+                    Dim ChineseName As String = Project.TranslatedName.Before(" (").Before(" - ").
                         Replace("\", "\").Replace("/", "/").Replace("|", "|").Replace(":", ":").Replace("<", "<").Replace(">", ">").Replace("*", "*").Replace("?", "?").Replace("""", "").Replace(": ", ":")
                         Replace("\", "\").Replace("/", "/").Replace("|", "|").Replace(":", ":").Replace("<", "<").Replace(">", ">").Replace("*", "*").Replace("?", "?").Replace("""", "").Replace(": ", ":")
                     Select Case Setup.Get("ToolDownloadTranslate")
                     Select Case Setup.Get("ToolDownloadTranslate")
                         Case 0
                         Case 0
@@ -299,7 +299,7 @@
                                           If(File.FileName.EndsWith(".mrpack"), "*.mrpack", "*.zip")), DefaultFolder)
                                           If(File.FileName.EndsWith(".mrpack"), "*.mrpack", "*.zip")), DefaultFolder)
                     If Not Target.Contains("\") Then Exit Sub
                     If Not Target.Contains("\") Then Exit Sub
                     '构造步骤加载器
                     '构造步骤加载器
-                    Dim LoaderName As String = Desc & "下载:" & File.DisplayName & " "
+                    Dim LoaderName As String = Desc & "下载:" & GetFileNameWithoutExtentionFromPath(Target) & " "
                     If Target <> DefaultFolder AndAlso Project.Type = CompType.Mod Then CachedFolder = GetPathFromFullPath(Target)
                     If Target <> DefaultFolder AndAlso Project.Type = CompType.Mod Then CachedFolder = GetPathFromFullPath(Target)
                     Dim Loaders As New List(Of LoaderBase)
                     Dim Loaders As New List(Of LoaderBase)
                     Loaders.Add(New LoaderDownload("下载文件", New List(Of NetFile) From {File.ToNetFile(Target)}) With {.ProgressWeight = 6, .Block = True})
                     Loaders.Add(New LoaderDownload("下载文件", New List(Of NetFile) From {File.ToNetFile(Target)}) With {.ProgressWeight = 6, .Block = True})

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

@@ -20,7 +20,7 @@
                         <StackPanel Margin="25,37,25,15">
                         <StackPanel Margin="25,37,25,15">
                             <local:MyHint Text="你尚未选择安装 Fabric API,这会导致大多数 Mod 无法使用!" Margin="0,1,0,7" x:Name="HintFabricAPI" IsWarn="True" />
                             <local:MyHint Text="你尚未选择安装 Fabric API,这会导致大多数 Mod 无法使用!" Margin="0,1,0,7" x:Name="HintFabricAPI" IsWarn="True" />
                             <local:MyHint Text="你尚未选择安装 OptiFabric,这会导致 OptiFine 无法使用!" Margin="0,1,0,7" x:Name="HintOptiFabric" IsWarn="True" />
                             <local:MyHint Text="你尚未选择安装 OptiFabric,这会导致 OptiFine 无法使用!" Margin="0,1,0,7" x:Name="HintOptiFabric" IsWarn="True" />
-                            <local:MyHint Text="安装结束后,请在 Mod 下载中搜索 OptiFabric Origins 并下载,否则 OptiFine 会无法使用!" Margin="0,1,0,7" x:Name="HintOptiFabricOld" IsWarn="False" />
+                            <local:MyHint Text="OptiFine 与一部分 Mod 的兼容性不佳,请谨慎安装。" Margin="0,1,0,7" x:Name="HintModOptiFine" IsWarn="False" />
                             <Grid Margin="0,0,0,11">
                             <Grid Margin="0,0,0,11">
                                 <Grid.ColumnDefinitions>
                                 <Grid.ColumnDefinitions>
                                     <ColumnDefinition Width="1*" />
                                     <ColumnDefinition Width="1*" />
@@ -61,26 +61,6 @@
                             <TextBlock VerticalAlignment="Center" Text="返回" Grid.Column="3" Foreground="{StaticResource ColorBrushGray4}" />
                             <TextBlock VerticalAlignment="Center" Text="返回" Grid.Column="3" Foreground="{StaticResource ColorBrushGray4}" />
                         </Grid>
                         </Grid>
                     </local:MyCard>
                     </local:MyCard>
-                    <local:MyCard Title="OptiFine" Height="40" Margin="0,15,0,0" x:Name="CardOptiFine" IsSwaped="True" CanSwap="True" SwapLogoRight="True">
-                        <StackPanel Margin="20,40,18,15" VerticalAlignment="Top">
-                            <StackPanel Name="PanOptiFine" />
-                        </StackPanel>
-                        <Grid x:Name="PanOptiFineInfo" Height="18" Margin="175,11,15,0" VerticalAlignment="Top" Tag="True">
-                            <Grid.RenderTransform>
-                                <TranslateTransform />
-                            </Grid.RenderTransform>
-                            <Grid.ColumnDefinitions>
-                                <ColumnDefinition Width="Auto" />
-                                <ColumnDefinition Width="1*" />
-                            </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" />
-                            <TextBlock x:Name="LabOptiFine" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" Grid.Column="1" />
-                        </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>
-                    </local:MyCard>
                     <local:MyCard Title="Forge" Height="40" Margin="0,15,0,0" x:Name="CardForge" IsSwaped="True" CanSwap="True" SwapLogoRight="True">
                     <local:MyCard Title="Forge" Height="40" Margin="0,15,0,0" x:Name="CardForge" IsSwaped="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="175,11,15,0" VerticalAlignment="Top" Tag="True">
                         <Grid x:Name="PanForgeInfo" Height="18" Margin="175,11,15,0" VerticalAlignment="Top" Tag="True">
@@ -135,6 +115,26 @@
                                         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" />
                                         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>
                         </Grid>
                     </local:MyCard>
                     </local:MyCard>
+                    <local:MyCard Title="OptiFine" Height="40" Margin="0,15,0,0" x:Name="CardOptiFine" IsSwaped="True" CanSwap="True" SwapLogoRight="True">
+                        <StackPanel Margin="20,40,18,15" VerticalAlignment="Top">
+                            <StackPanel Name="PanOptiFine" />
+                        </StackPanel>
+                        <Grid x:Name="PanOptiFineInfo" Height="18" Margin="175,11,15,0" VerticalAlignment="Top" Tag="True">
+                            <Grid.RenderTransform>
+                                <TranslateTransform />
+                            </Grid.RenderTransform>
+                            <Grid.ColumnDefinitions>
+                                <ColumnDefinition Width="Auto" />
+                                <ColumnDefinition Width="1*" />
+                            </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" />
+                            <TextBlock x:Name="LabOptiFine" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" Grid.Column="1" />
+                        </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>
+                    </local:MyCard>
                     <local:MyCard Title="OptiFabric" Height="40" Margin="0,15,0,0" x:Name="CardOptiFabric" IsSwaped="True" CanSwap="True" SwapLogoRight="True">
                     <local:MyCard Title="OptiFabric" Height="40" Margin="0,15,0,0" x:Name="CardOptiFabric" IsSwaped="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" />
                         <Grid x:Name="PanOptiFabricInfo" Height="18" Margin="175,11,15,0" VerticalAlignment="Top" Tag="True">
                         <Grid x:Name="PanOptiFabricInfo" Height="18" Margin="175,11,15,0" VerticalAlignment="Top" Tag="True">

+ 94 - 68
Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb

@@ -32,7 +32,7 @@
 #Region "页面切换"
 #Region "页面切换"
 
 
     '页面切换动画
     '页面切换动画
-    Private IsInSelectPage As Boolean = False
+    Public IsInSelectPage As Boolean = False
     Private IsFirstLoaded As Boolean = False
     Private IsFirstLoaded As Boolean = False
     Private Sub EnterSelectPage()
     Private Sub EnterSelectPage()
         If IsInSelectPage Then Exit Sub
         If IsInSelectPage Then Exit Sub
@@ -273,11 +273,13 @@
         End If
         End If
     End Sub
     End Sub
 
 
+    Private IsReloading As Boolean = False '#3742 中,LoadOptiFineGetError 会初始化 LoadOptiFine,触发事件 LoadOptiFine.StateChanged,导致再次调用 SelectReload
     ''' <summary>
     ''' <summary>
     ''' 重载已选择的项目的显示。
     ''' 重载已选择的项目的显示。
     ''' </summary>
     ''' </summary>
     Private Sub SelectReload() Handles CardOptiFine.Swap, LoadOptiFine.StateChanged, CardForge.Swap, LoadForge.StateChanged, CardFabric.Swap, LoadFabric.StateChanged, CardFabricApi.Swap, LoadFabricApi.StateChanged, CardOptiFabric.Swap, LoadOptiFabric.StateChanged, CardLiteLoader.Swap, LoadLiteLoader.StateChanged
     Private Sub SelectReload() Handles CardOptiFine.Swap, LoadOptiFine.StateChanged, CardForge.Swap, LoadForge.StateChanged, CardFabric.Swap, LoadFabric.StateChanged, CardFabricApi.Swap, LoadFabricApi.StateChanged, CardOptiFabric.Swap, LoadOptiFabric.StateChanged, CardLiteLoader.Swap, LoadLiteLoader.StateChanged
-        If SelectedMinecraftId Is Nothing Then Exit Sub
+        If SelectedMinecraftId Is Nothing OrElse IsReloading Then Exit Sub
+        IsReloading = True
         '主预览
         '主预览
         SelectNameUpdate()
         SelectNameUpdate()
         ItemSelect.Title = TextSelectName.Text
         ItemSelect.Title = TextSelectName.Text
@@ -303,20 +305,25 @@
             LabOptiFine.Foreground = ColorGray1
             LabOptiFine.Foreground = ColorGray1
         End If
         End If
         'LiteLoader
         'LiteLoader
-        Dim LiteLoaderError As String = LoadLiteLoaderGetError()
-        CardLiteLoader.MainSwap.Visibility = If(LiteLoaderError Is Nothing, Visibility.Visible, Visibility.Collapsed)
-        If LiteLoaderError IsNot Nothing Then CardLiteLoader.IsSwaped = True '例如在同时展开卡片时选择了不兼容项则强制折叠
-        SetLiteLoaderInfoShow(CardLiteLoader.IsSwaped)
-        If SelectedLiteLoader Is Nothing Then
-            BtnLiteLoaderClear.Visibility = Visibility.Collapsed
-            ImgLiteLoader.Visibility = Visibility.Collapsed
-            LabLiteLoader.Text = If(LiteLoaderError, "点击选择")
-            LabLiteLoader.Foreground = ColorGray4
+        If Not SelectedMinecraftId.Contains("1.") OrElse Val(SelectedMinecraftId.Split(".")(1)) > 12 Then
+            CardLiteLoader.Visibility = Visibility.Collapsed
         Else
         Else
-            BtnLiteLoaderClear.Visibility = Visibility.Visible
-            ImgLiteLoader.Visibility = Visibility.Visible
-            LabLiteLoader.Text = SelectedLiteLoader.Inherit
-            LabLiteLoader.Foreground = ColorGray1
+            CardLiteLoader.Visibility = Visibility.Visible
+            Dim LiteLoaderError As String = LoadLiteLoaderGetError()
+            CardLiteLoader.MainSwap.Visibility = If(LiteLoaderError Is Nothing, Visibility.Visible, Visibility.Collapsed)
+            If LiteLoaderError IsNot Nothing Then CardLiteLoader.IsSwaped = True '例如在同时展开卡片时选择了不兼容项则强制折叠
+            SetLiteLoaderInfoShow(CardLiteLoader.IsSwaped)
+            If SelectedLiteLoader Is Nothing Then
+                BtnLiteLoaderClear.Visibility = Visibility.Collapsed
+                ImgLiteLoader.Visibility = Visibility.Collapsed
+                LabLiteLoader.Text = If(LiteLoaderError, "点击选择")
+                LabLiteLoader.Foreground = ColorGray4
+            Else
+                BtnLiteLoaderClear.Visibility = Visibility.Visible
+                ImgLiteLoader.Visibility = Visibility.Visible
+                LabLiteLoader.Text = SelectedLiteLoader.Inherit
+                LabLiteLoader.Foreground = ColorGray1
+            End If
         End If
         End If
         'Forge
         'Forge
         Dim ForgeError As String = LoadForgeGetError()
         Dim ForgeError As String = LoadForgeGetError()
@@ -335,67 +342,87 @@
             LabForge.Foreground = ColorGray1
             LabForge.Foreground = ColorGray1
         End If
         End If
         'Fabric
         'Fabric
-        Dim FabricError As String = LoadFabricGetError()
-        CardFabric.MainSwap.Visibility = If(FabricError Is Nothing, Visibility.Visible, Visibility.Collapsed)
-        If FabricError IsNot Nothing Then CardFabric.IsSwaped = True
-        SetFabricInfoShow(CardFabric.IsSwaped)
-        If SelectedFabric Is Nothing Then
-            BtnFabricClear.Visibility = Visibility.Collapsed
-            ImgFabric.Visibility = Visibility.Collapsed
-            LabFabric.Text = If(FabricError, "点击选择")
-            LabFabric.Foreground = ColorGray4
+        If SelectedMinecraftId.Contains("1.") AndAlso Val(SelectedMinecraftId.Split(".")(1)) <= 13 Then
+            CardFabric.Visibility = Visibility.Collapsed
         Else
         Else
-            BtnFabricClear.Visibility = Visibility.Visible
-            ImgFabric.Visibility = Visibility.Visible
-            LabFabric.Text = SelectedFabric.Replace("+build", "")
-            LabFabric.Foreground = ColorGray1
+            CardFabric.Visibility = Visibility.Visible
+            Dim FabricError As String = LoadFabricGetError()
+            CardFabric.MainSwap.Visibility = If(FabricError Is Nothing, Visibility.Visible, Visibility.Collapsed)
+            If FabricError IsNot Nothing Then CardFabric.IsSwaped = True
+            SetFabricInfoShow(CardFabric.IsSwaped)
+            If SelectedFabric Is Nothing Then
+                BtnFabricClear.Visibility = Visibility.Collapsed
+                ImgFabric.Visibility = Visibility.Collapsed
+                LabFabric.Text = If(FabricError, "点击选择")
+                LabFabric.Foreground = ColorGray4
+            Else
+                BtnFabricClear.Visibility = Visibility.Visible
+                ImgFabric.Visibility = Visibility.Visible
+                LabFabric.Text = SelectedFabric.Replace("+build", "")
+                LabFabric.Foreground = ColorGray1
+            End If
         End If
         End If
         'FabricApi
         'FabricApi
-        Dim FabricApiError As String = LoadFabricApiGetError()
-        CardFabricApi.MainSwap.Visibility = If(FabricApiError Is Nothing, Visibility.Visible, Visibility.Collapsed)
-        If FabricApiError IsNot Nothing OrElse SelectedFabric Is Nothing Then CardFabricApi.IsSwaped = True
-        SetFabricApiInfoShow(CardFabricApi.IsSwaped)
-        If SelectedFabricApi Is Nothing Then
-            BtnFabricApiClear.Visibility = Visibility.Collapsed
-            ImgFabricApi.Visibility = Visibility.Collapsed
-            LabFabricApi.Text = If(FabricApiError, "点击选择")
-            LabFabricApi.Foreground = ColorGray4
+        If SelectedFabric Is Nothing Then
+            CardFabricApi.Visibility = Visibility.Collapsed
         Else
         Else
-            BtnFabricApiClear.Visibility = Visibility.Visible
-            ImgFabricApi.Visibility = Visibility.Visible
-            LabFabricApi.Text = SelectedFabricApi.DisplayName.Split("]")(1).Replace("Fabric API ", "").Replace(" build ", ".").Split("+").First.Trim
-            LabFabricApi.Foreground = ColorGray1
+            CardFabricApi.Visibility = Visibility.Visible
+            Dim FabricApiError As String = LoadFabricApiGetError()
+            CardFabricApi.MainSwap.Visibility = If(FabricApiError Is Nothing, Visibility.Visible, Visibility.Collapsed)
+            If FabricApiError IsNot Nothing OrElse SelectedFabric Is Nothing Then CardFabricApi.IsSwaped = True
+            SetFabricApiInfoShow(CardFabricApi.IsSwaped)
+            If SelectedFabricApi Is Nothing Then
+                BtnFabricApiClear.Visibility = Visibility.Collapsed
+                ImgFabricApi.Visibility = Visibility.Collapsed
+                LabFabricApi.Text = If(FabricApiError, "点击选择")
+                LabFabricApi.Foreground = ColorGray4
+            Else
+                BtnFabricApiClear.Visibility = Visibility.Visible
+                ImgFabricApi.Visibility = Visibility.Visible
+                LabFabricApi.Text = SelectedFabricApi.DisplayName.Split("]")(1).Replace("Fabric API ", "").Replace(" build ", ".").Split("+").First.Trim
+                LabFabricApi.Foreground = ColorGray1
+            End If
         End If
         End If
         'OptiFabric
         'OptiFabric
-        Dim OptiFabricError As String = LoadOptiFabricGetError()
-        CardOptiFabric.MainSwap.Visibility = If(OptiFabricError Is Nothing, Visibility.Visible, Visibility.Collapsed)
-        If OptiFabricError IsNot Nothing OrElse SelectedFabric Is Nothing Then CardOptiFabric.IsSwaped = True
-        SetOptiFabricInfoShow(CardOptiFabric.IsSwaped)
-        If SelectedOptiFabric Is Nothing Then
-            BtnOptiFabricClear.Visibility = Visibility.Collapsed
-            ImgOptiFabric.Visibility = Visibility.Collapsed
-            LabOptiFabric.Text = If(OptiFabricError, "点击选择")
-            LabOptiFabric.Foreground = ColorGray4
+        If SelectedFabric Is Nothing OrElse SelectedOptiFine Is Nothing Then
+            CardOptiFabric.Visibility = Visibility.Collapsed
         Else
         Else
-            BtnOptiFabricClear.Visibility = Visibility.Visible
-            ImgOptiFabric.Visibility = Visibility.Visible
-            LabOptiFabric.Text = SelectedOptiFabric.DisplayName.ToLower.Replace("optifabric-", "").Replace(".jar", "").Trim.TrimStart("v")
-            LabOptiFabric.Foreground = ColorGray1
+            CardOptiFabric.Visibility = Visibility.Visible
+            Dim OptiFabricError As String = LoadOptiFabricGetError()
+            CardOptiFabric.MainSwap.Visibility = If(OptiFabricError Is Nothing, Visibility.Visible, Visibility.Collapsed)
+            If OptiFabricError IsNot Nothing OrElse SelectedFabric Is Nothing Then CardOptiFabric.IsSwaped = True
+            SetOptiFabricInfoShow(CardOptiFabric.IsSwaped)
+            If SelectedOptiFabric Is Nothing Then
+                BtnOptiFabricClear.Visibility = Visibility.Collapsed
+                ImgOptiFabric.Visibility = Visibility.Collapsed
+                LabOptiFabric.Text = If(OptiFabricError, "点击选择")
+                LabOptiFabric.Foreground = ColorGray4
+            Else
+                BtnOptiFabricClear.Visibility = Visibility.Visible
+                ImgOptiFabric.Visibility = Visibility.Visible
+                LabOptiFabric.Text = SelectedOptiFabric.DisplayName.ToLower.Replace("optifabric-", "").Replace(".jar", "").Trim.TrimStart("v")
+                LabOptiFabric.Foreground = ColorGray1
+            End If
         End If
         End If
         '主警告
         '主警告
-        HintFabricAPI.Visibility = If(SelectedFabric IsNot Nothing AndAlso SelectedFabricApi Is Nothing, Visibility.Visible, Visibility.Collapsed)
+        If SelectedFabric IsNot Nothing AndAlso SelectedFabricApi Is Nothing Then
+            HintFabricAPI.Visibility = Visibility.Visible
+        Else
+            HintFabricAPI.Visibility = Visibility.Collapsed
+        End If
         If SelectedFabric IsNot Nothing AndAlso SelectedOptiFine IsNot Nothing AndAlso SelectedOptiFabric Is Nothing Then
         If SelectedFabric IsNot Nothing AndAlso SelectedOptiFine IsNot Nothing AndAlso SelectedOptiFabric Is Nothing Then
-            If SelectedMinecraftId.StartsWith("1.14") OrElse SelectedMinecraftId.StartsWith("1.15") Then
-                HintOptiFabric.Visibility = Visibility.Collapsed
-                HintOptiFabricOld.Visibility = Visibility.Visible
-            Else
-                HintOptiFabric.Visibility = Visibility.Visible
-                HintOptiFabricOld.Visibility = Visibility.Collapsed
-            End If
+            HintOptiFabric.Visibility = Visibility.Visible
         Else
         Else
             HintOptiFabric.Visibility = Visibility.Collapsed
             HintOptiFabric.Visibility = Visibility.Collapsed
-            HintOptiFabricOld.Visibility = Visibility.Collapsed
         End If
         End If
+        If SelectedMinecraftId.Contains("1.") AndAlso Val(SelectedMinecraftId.Split(".")(1)) >= 16 AndAlso SelectedOptiFine IsNot Nothing AndAlso
+           (SelectedForge IsNot Nothing OrElse SelectedFabric IsNot Nothing) Then
+            HintModOptiFine.Visibility = Visibility.Visible
+        Else
+            HintModOptiFine.Visibility = Visibility.Collapsed
+        End If
+        '结束
+        IsReloading = False
     End Sub
     End Sub
     ''' <summary>
     ''' <summary>
     ''' 清空已选择的项目。
     ''' 清空已选择的项目。
@@ -643,7 +670,7 @@
         Return (OptiFine.RequiredForgeVersion.Contains(".") AndAlso 'XX.X.XXX
         Return (OptiFine.RequiredForgeVersion.Contains(".") AndAlso 'XX.X.XXX
                 VersionSortInteger(Forge.Version, OptiFine.RequiredForgeVersion) >= 0) OrElse
                 VersionSortInteger(Forge.Version, OptiFine.RequiredForgeVersion) >= 0) OrElse
                (Not OptiFine.RequiredForgeVersion.Contains(".") AndAlso '#XXXX
                (Not OptiFine.RequiredForgeVersion.Contains(".") AndAlso '#XXXX
-                VersionSortInteger(Forge.Version.Split(".").Last, OptiFine.RequiredForgeVersion) >= 0)
+                Forge.Version.After(".") >= OptiFine.RequiredForgeVersion)
     End Function
     End Function
 
 
     '限制展开
     '限制展开
@@ -920,12 +947,12 @@
             If DisplayName.StartsWith("[" & MinecraftVersion & "]") Then Return True
             If DisplayName.StartsWith("[" & MinecraftVersion & "]") Then Return True
             If Not DisplayName.Contains("/") OrElse Not DisplayName.Contains("]") Then Return False
             If Not DisplayName.Contains("/") OrElse Not DisplayName.Contains("]") Then Return False
             '直接的判断(例如 1.18.1/22w03a)
             '直接的判断(例如 1.18.1/22w03a)
-            For Each Part As String In DisplayName.Split("]")(0).TrimStart("[").Split("/")
+            For Each Part As String In DisplayName.Before("]").TrimStart("[").Split("/")
                 If Part = MinecraftVersion Then Return True
                 If Part = MinecraftVersion Then Return True
             Next
             Next
             '将版本名分割语素(例如 1.16.4/5)
             '将版本名分割语素(例如 1.16.4/5)
-            Dim Lefts = RegexSearch(DisplayName.Split("]")(0), "[a-z/]+|[0-9/]+")
-            Dim Rights = RegexSearch(MinecraftVersion.Split("]")(0), "[a-z/]+|[0-9/]+")
+            Dim Lefts = RegexSearch(DisplayName.Before("]"), "[a-z/]+|[0-9/]+")
+            Dim Rights = RegexSearch(MinecraftVersion.Before("]"), "[a-z/]+|[0-9/]+")
             '对每段进行判断
             '对每段进行判断
             Dim i As Integer = 0
             Dim i As Integer = 0
             While True
             While True
@@ -1045,7 +1072,6 @@
     ''' 获取 OptiFabric 的加载异常信息。若正常则返回 Nothing。
     ''' 获取 OptiFabric 的加载异常信息。若正常则返回 Nothing。
     ''' </summary>
     ''' </summary>
     Private Function LoadOptiFabricGetError() As String
     Private Function LoadOptiFabricGetError() As String
-        If SelectedMinecraftId.StartsWith("1.14") OrElse SelectedMinecraftId.StartsWith("1.15") Then Return "不兼容老版本 Fabric,请手动下载 OptiFabric Origins"
         If LoadOptiFabric Is Nothing OrElse LoadOptiFabric.State.LoadingState = MyLoading.MyLoadingState.Run Then Return "正在获取版本列表……"
         If LoadOptiFabric Is Nothing OrElse LoadOptiFabric.State.LoadingState = MyLoading.MyLoadingState.Run Then Return "正在获取版本列表……"
         If LoadOptiFabric.State.LoadingState = MyLoading.MyLoadingState.Error Then Return "获取版本列表失败:" & CType(LoadOptiFabric.State, Object).Error.Message
         If LoadOptiFabric.State.LoadingState = MyLoading.MyLoadingState.Error Then Return "获取版本列表失败:" & CType(LoadOptiFabric.State, Object).Error.Message
         If DlOptiFabricLoader.Output Is Nothing Then
         If DlOptiFabricLoader.Output Is Nothing Then

+ 2 - 6
Plain Craft Launcher 2/Pages/PageDownload/PageDownloadMod.xaml

@@ -33,23 +33,19 @@
                         <TextBlock VerticalAlignment="Center" Grid.Row="2" HorizontalAlignment="Left" Text="版本" Margin="0,0,18,0" />
                         <TextBlock VerticalAlignment="Center" Grid.Row="2" HorizontalAlignment="Left" Text="版本" Margin="0,0,18,0" />
                         <local:MyComboBox x:Name="TextSearchVersion" Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" IsEditable="True" MaxDropDownHeight="320">
                         <local:MyComboBox x:Name="TextSearchVersion" Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" IsEditable="True" MaxDropDownHeight="320">
                             <local:MyComboBoxItem Content="全部 (也可自行输入)" IsSelected="True" />
                             <local:MyComboBoxItem Content="全部 (也可自行输入)" IsSelected="True" />
-                            <local:MyComboBoxItem Content="1.20.5" />
+                            <local:MyComboBoxItem Content="1.20.6" />
                             <local:MyComboBoxItem Content="1.20.4" />
                             <local:MyComboBoxItem Content="1.20.4" />
-                            <local:MyComboBoxItem Content="1.20.3" />
-                            <local:MyComboBoxItem Content="1.20.2" />
                             <local:MyComboBoxItem Content="1.20.1" />
                             <local:MyComboBoxItem Content="1.20.1" />
-                            <local:MyComboBoxItem Content="1.20" />
                             <local:MyComboBoxItem Content="1.19.4" />
                             <local:MyComboBoxItem Content="1.19.4" />
+                            <local:MyComboBoxItem Content="1.19.2" />
                             <local:MyComboBoxItem Content="1.18.2" />
                             <local:MyComboBoxItem Content="1.18.2" />
                             <local:MyComboBoxItem Content="1.17.1" />
                             <local:MyComboBoxItem Content="1.17.1" />
                             <local:MyComboBoxItem Content="1.16.5" />
                             <local:MyComboBoxItem Content="1.16.5" />
                             <local:MyComboBoxItem Content="1.15.2" />
                             <local:MyComboBoxItem Content="1.15.2" />
                             <local:MyComboBoxItem Content="1.14.4" />
                             <local:MyComboBoxItem Content="1.14.4" />
-                            <local:MyComboBoxItem Content="1.13.2" />
                             <local:MyComboBoxItem Content="1.12.2" />
                             <local:MyComboBoxItem Content="1.12.2" />
                             <local:MyComboBoxItem Content="1.11.2" />
                             <local:MyComboBoxItem Content="1.11.2" />
                             <local:MyComboBoxItem Content="1.10.2" />
                             <local:MyComboBoxItem Content="1.10.2" />
-                            <local:MyComboBoxItem Content="1.9.4" />
                             <local:MyComboBoxItem Content="1.8.9" />
                             <local:MyComboBoxItem Content="1.8.9" />
                             <local:MyComboBoxItem Content="1.7.10" />
                             <local:MyComboBoxItem Content="1.7.10" />
                         </local:MyComboBox>
                         </local:MyComboBox>

+ 2 - 6
Plain Craft Launcher 2/Pages/PageDownload/PageDownloadPack.xaml

@@ -32,23 +32,19 @@
                         <TextBlock VerticalAlignment="Center" Grid.Row="2" HorizontalAlignment="Left" Text="版本" Margin="0,0,18,0" />
                         <TextBlock VerticalAlignment="Center" Grid.Row="2" HorizontalAlignment="Left" Text="版本" Margin="0,0,18,0" />
                         <local:MyComboBox x:Name="TextSearchVersion" Grid.Row="2" Grid.Column="1" IsEditable="True" MaxDropDownHeight="320">
                         <local:MyComboBox x:Name="TextSearchVersion" Grid.Row="2" Grid.Column="1" IsEditable="True" MaxDropDownHeight="320">
                             <local:MyComboBoxItem Content="全部 (也可自行输入)" IsSelected="True" />
                             <local:MyComboBoxItem Content="全部 (也可自行输入)" IsSelected="True" />
-                            <local:MyComboBoxItem Content="1.20.5" />
+                            <local:MyComboBoxItem Content="1.20.6" />
                             <local:MyComboBoxItem Content="1.20.4" />
                             <local:MyComboBoxItem Content="1.20.4" />
-                            <local:MyComboBoxItem Content="1.20.3" />
-                            <local:MyComboBoxItem Content="1.20.2" />
                             <local:MyComboBoxItem Content="1.20.1" />
                             <local:MyComboBoxItem Content="1.20.1" />
-                            <local:MyComboBoxItem Content="1.20" />
                             <local:MyComboBoxItem Content="1.19.4" />
                             <local:MyComboBoxItem Content="1.19.4" />
+                            <local:MyComboBoxItem Content="1.19.2" />
                             <local:MyComboBoxItem Content="1.18.2" />
                             <local:MyComboBoxItem Content="1.18.2" />
                             <local:MyComboBoxItem Content="1.17.1" />
                             <local:MyComboBoxItem Content="1.17.1" />
                             <local:MyComboBoxItem Content="1.16.5" />
                             <local:MyComboBoxItem Content="1.16.5" />
                             <local:MyComboBoxItem Content="1.15.2" />
                             <local:MyComboBoxItem Content="1.15.2" />
                             <local:MyComboBoxItem Content="1.14.4" />
                             <local:MyComboBoxItem Content="1.14.4" />
-                            <local:MyComboBoxItem Content="1.13.2" />
                             <local:MyComboBoxItem Content="1.12.2" />
                             <local:MyComboBoxItem Content="1.12.2" />
                             <local:MyComboBoxItem Content="1.11.2" />
                             <local:MyComboBoxItem Content="1.11.2" />
                             <local:MyComboBoxItem Content="1.10.2" />
                             <local:MyComboBoxItem Content="1.10.2" />
-                            <local:MyComboBoxItem Content="1.9.4" />
                             <local:MyComboBoxItem Content="1.8.9" />
                             <local:MyComboBoxItem Content="1.8.9" />
                             <local:MyComboBoxItem Content="1.7.10" />
                             <local:MyComboBoxItem Content="1.7.10" />
                         </local:MyComboBox>
                         </local:MyComboBox>

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

@@ -93,7 +93,7 @@ Public Class FormLoginOAuth
         If Not IsLoginSuccessed Then RaiseEvent OnLoginCanceled(IsSwitch)
         If Not IsLoginSuccessed Then RaiseEvent OnLoginCanceled(IsSwitch)
         FrmMain.Focus()
         FrmMain.Focus()
     End Sub
     End Sub
-    Private Sub HintSwitch_Click(sender As Object, e As EventArgs) Handles HintSwitch.Click
+    Private Sub HintSwitch_Click() Handles HintSwitch.Click
         IsSwitch = True
         IsSwitch = True
         Close()
         Close()
     End Sub
     End Sub

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

@@ -241,7 +241,7 @@ Retry:
                             {"Translator-Chinese", "Crowdin 中文翻译者披风"}, {"Translator", "Crowdin 翻译者披风"}, {"Cobalt", "Cobalt 披风"},
                             {"Translator-Chinese", "Crowdin 中文翻译者披风"}, {"Translator", "Crowdin 翻译者披风"}, {"Cobalt", "Cobalt 披风"},
                             {"Vanilla", "原版披风"}, {"Minecon2011", "Minecon 2011 参与者披风"}, {"Minecon2012", "Minecon 2012 参与者披风"},
                             {"Vanilla", "原版披风"}, {"Minecon2011", "Minecon 2011 参与者披风"}, {"Minecon2012", "Minecon 2012 参与者披风"},
                             {"Minecon2013", "Minecon 2013 参与者披风"}, {"Minecon2015", "Minecon 2015 参与者披风"}, {"Minecon2016", "Minecon 2016 参与者披风"},
                             {"Minecon2013", "Minecon 2013 参与者披风"}, {"Minecon2015", "Minecon 2015 参与者披风"}, {"Minecon2016", "Minecon 2016 参与者披风"},
-                            {"Cherry Blossom", "樱花披风"}
+                            {"Cherry Blossom", "樱花披风"}, {"15th Anniversary", "15 周年纪念披风"}, {"Purple Heart", "紫色心形披风"}
                         }
                         }
                         Dim SelectionControl As New List(Of IMyRadio) From {New MyRadioBox With {.Text = "无披风"}}
                         Dim SelectionControl As New List(Of IMyRadio) From {New MyRadioBox With {.Text = "无披风"}}
                         For Each Cape In SkinData("capes")
                         For Each Cape In SkinData("capes")

+ 48 - 46
Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchLeft.xaml.vb

@@ -19,51 +19,53 @@
         RefreshButtonsUI()
         RefreshButtonsUI()
 
 
         '加载版本
         '加载版本
-        RunInNewThread(Sub()
-                           '确认 Minecraft 文件夹存在
-                           PathMcFolder = Setup.Get("LaunchFolderSelect").ToString.Replace("$", Path)
-                           If PathMcFolder = "" OrElse Not Directory.Exists(PathMcFolder) Then
-                               '无效的文件夹
-                               If PathMcFolder = "" Then
-                                   Log("[Launch] 没有已储存的 Minecraft 文件夹")
-                               Else
-                                   Log("[Launch] Minecraft 文件夹无效,该文件夹已不存在:" & PathMcFolder, LogLevel.Debug)
-                               End If
-                               McFolderListLoader.WaitForExit(IsForceRestart:=True)
-                               Setup.Set("LaunchFolderSelect", McFolderList(0).Path.Replace(Path, "$"))
-                           End If
-                           Log("[Launch] Minecraft 文件夹:" & PathMcFolder)
-                           If Setup.Get("SystemDebugDelay") Then Thread.Sleep(RandomInteger(500, 3000))
-                           '确认 Minecraft 版本存在
-                           Dim Selection As String = Setup.Get("LaunchVersionSelect")
-                           Dim Version As McVersion
-                           If Selection = "" Then
-                               Version = Nothing
-                           Else
-                               Version = New McVersion(Selection)
-                           End If
-                           If IsNothing(Version) OrElse Not (Version.Path.StartsWith(PathMcFolder) AndAlso Version.Check) Then
-                               '无效的版本
-                               Log("[Launch] Minecraft 版本无效" & If(IsNothing(Version), ",没有有效版本", ":" & Version.Path), If(IsNothing(Version), LogLevel.Normal, LogLevel.Debug))
-                               If Not McVersionListLoader.State = LoadState.Finished Then LoaderFolderRun(McVersionListLoader, PathMcFolder, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\", WaitForExit:=True)
-                               If Not McVersionList.Any() OrElse McVersionList.First.Value(0).Logo.Contains("RedstoneBlock") Then
-                                   Version = Nothing
-                                   Setup.Set("LaunchVersionSelect", "")
-                                   Log("[Launch] 无可用 Minecraft 版本")
-                               Else
-                                   Version = McVersionList.First.Value(0)
-                                   Setup.Set("LaunchVersionSelect", Version.Name)
-                                   Log("[Launch] 自动选择 Minecraft 版本:" & Version.Path)
-                               End If
-                           End If
-                           RunInUi(Sub()
-                                       McVersionCurrent = Version '绕这一圈是为了避免 McVersionCheck 触发第二次版本改变
-                                       IsLoadFinished = True
-                                       RefreshButtonsUI()
-                                       RefreshPage(False, False) '有可能选择的版本变化了,需要重新刷新
-                                       If McLoginAble() = "" Then McLoginLoader.Start() '自动登录
-                                   End Sub)
-                       End Sub, "Version Check", ThreadPriority.AboveNormal)
+        RunInNewThread(
+        Sub()
+            '确认 Minecraft 文件夹存在
+            PathMcFolder = Setup.Get("LaunchFolderSelect").ToString.Replace("$", Path)
+            If PathMcFolder = "" OrElse Not Directory.Exists(PathMcFolder) Then
+                '无效的文件夹
+                If PathMcFolder = "" Then
+                    Log("[Launch] 没有已储存的 Minecraft 文件夹")
+                Else
+                    Log("[Launch] Minecraft 文件夹无效,该文件夹已不存在:" & PathMcFolder, LogLevel.Debug)
+                End If
+                McFolderListLoader.WaitForExit(IsForceRestart:=True)
+                Setup.Set("LaunchFolderSelect", McFolderList(0).Path.Replace(Path, "$"))
+            End If
+            Log("[Launch] Minecraft 文件夹:" & PathMcFolder)
+            If Setup.Get("SystemDebugDelay") Then Thread.Sleep(RandomInteger(500, 3000))
+            '确认 Minecraft 版本存在
+            Dim Selection As String = Setup.Get("LaunchVersionSelect")
+            Dim Version As McVersion
+            If Selection = "" Then
+                Version = Nothing
+            Else
+                Version = New McVersion(Selection)
+            End If
+            If IsNothing(Version) OrElse Not (Version.Path.StartsWith(PathMcFolder) AndAlso Version.Check) Then
+                '无效的版本
+                Log("[Launch] Minecraft 版本无效" & If(IsNothing(Version), ",没有有效版本", ":" & Version.Path), If(IsNothing(Version), LogLevel.Normal, LogLevel.Debug))
+                If Not McVersionListLoader.State = LoadState.Finished Then LoaderFolderRun(McVersionListLoader, PathMcFolder, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\", WaitForExit:=True)
+                If Not McVersionList.Any() OrElse McVersionList.First.Value(0).Logo.Contains("RedstoneBlock") Then
+                    Version = Nothing
+                    Setup.Set("LaunchVersionSelect", "")
+                    Log("[Launch] 无可用 Minecraft 版本")
+                Else
+                    Version = McVersionList.First.Value(0)
+                    Setup.Set("LaunchVersionSelect", Version.Name)
+                    Log("[Launch] 自动选择 Minecraft 版本:" & Version.Path)
+                End If
+            End If
+            RunInUi(
+            Sub()
+                McVersionCurrent = Version '绕这一圈是为了避免 McVersionCheck 触发第二次版本改变
+                IsLoadFinished = True
+                RefreshButtonsUI()
+                RefreshPage(False, False) '有可能选择的版本变化了,需要重新刷新
+                If McLoginAble() = "" Then McLoginLoader.Start() '自动登录
+            End Sub)
+        End Sub, "Version Check", ThreadPriority.AboveNormal)
 
 
         '改变页面
         '改变页面
         Dim LoginType As McLoginType = Setup.Get("LoginType")
         Dim LoginType As McLoginType = Setup.Get("LoginType")
@@ -398,7 +400,7 @@ Finish:
                 ElseIf Setup.Get("LoginLegacyName") = "" Then
                 ElseIf Setup.Get("LoginLegacyName") = "" Then
                     Return New EqualableList(Of String) From {0, ""}
                     Return New EqualableList(Of String) From {0, ""}
                 Else
                 Else
-                    Return New EqualableList(Of String) From {0, If(Setup.Get("LoginLegacyName").ToString.Split("¨")(0), "")}
+                    Return New EqualableList(Of String) From {0, If(Setup.Get("LoginLegacyName").ToString.Before("¨"), "")}
                 End If
                 End If
             Case 3
             Case 3
                 Return New EqualableList(Of String) From {3, Setup.Get("LaunchSkinID")}
                 Return New EqualableList(Of String) From {3, Setup.Get("LaunchSkinID")}

+ 23 - 17
Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.vb

@@ -67,7 +67,7 @@ Download:
                 End If
                 End If
             Case 3
             Case 3
                 Select Case Setup.Get("UiCustomPreset")
                 Select Case Setup.Get("UiCustomPreset")
-                    Case 0 '你知道吗
+                    Case 0
                         Log("[Page] 主页预设:你知道吗")
                         Log("[Page] 主页预设:你知道吗")
                         Content = "
                         Content = "
                             <local:MyCard Title=""你知道吗?"" Margin=""0,0,0,15"">
                             <local:MyCard Title=""你知道吗?"" Margin=""0,0,0,15"">
@@ -76,7 +76,7 @@ Download:
                                     EventType=""刷新主页"" EventData=""/""
                                     EventType=""刷新主页"" EventData=""/""
                                     Logo=""M875.52 148.48C783.36 56.32 655.36 0 512 0 291.84 0 107.52 138.24 30.72 332.8l122.88 46.08C204.8 230.4 348.16 128 512 128c107.52 0 199.68 40.96 271.36 112.64L640 384h384V0L875.52 148.48zM512 896c-107.52 0-199.68-40.96-271.36-112.64L384 640H0v384l148.48-148.48C240.64 967.68 368.64 1024 512 1024c220.16 0 404.48-138.24 481.28-332.8L870.4 645.12C819.2 793.6 675.84 896 512 896z"" />
                                     Logo=""M875.52 148.48C783.36 56.32 655.36 0 512 0 291.84 0 107.52 138.24 30.72 332.8l122.88 46.08C204.8 230.4 348.16 128 512 128c107.52 0 199.68 40.96 271.36 112.64L640 384h384V0L875.52 148.48zM512 896c-107.52 0-199.68-40.96-271.36-112.64L384 640H0v384l148.48-148.48C240.64 967.68 368.64 1024 512 1024c220.16 0 404.48-138.24 481.28-332.8L870.4 645.12C819.2 793.6 675.84 896 512 896z"" />
                             </local:MyCard>"
                             </local:MyCard>"
-                    Case 1 '回声洞
+                    Case 1
                         Log("[Page] 主页预设:回声洞")
                         Log("[Page] 主页预设:回声洞")
                         Content = "
                         Content = "
                             <local:MyCard Title=""回声洞"" Margin=""0,0,0,15"">
                             <local:MyCard Title=""回声洞"" Margin=""0,0,0,15"">
@@ -85,17 +85,21 @@ Download:
                                     EventType=""刷新主页"" EventData=""/""
                                     EventType=""刷新主页"" EventData=""/""
                                     Logo=""M875.52 148.48C783.36 56.32 655.36 0 512 0 291.84 0 107.52 138.24 30.72 332.8l122.88 46.08C204.8 230.4 348.16 128 512 128c107.52 0 199.68 40.96 271.36 112.64L640 384h384V0L875.52 148.48zM512 896c-107.52 0-199.68-40.96-271.36-112.64L384 640H0v384l148.48-148.48C240.64 967.68 368.64 1024 512 1024c220.16 0 404.48-138.24 481.28-332.8L870.4 645.12C819.2 793.6 675.84 896 512 896z"" />
                                     Logo=""M875.52 148.48C783.36 56.32 655.36 0 512 0 291.84 0 107.52 138.24 30.72 332.8l122.88 46.08C204.8 230.4 348.16 128 512 128c107.52 0 199.68 40.96 271.36 112.64L640 384h384V0L875.52 148.48zM512 896c-107.52 0-199.68-40.96-271.36-112.64L384 640H0v384l148.48-148.48C240.64 967.68 368.64 1024 512 1024c220.16 0 404.48-138.24 481.28-332.8L870.4 645.12C819.2 793.6 675.84 896 512 896z"" />
                             </local:MyCard>"
                             </local:MyCard>"
-                    Case 2 '新闻
+                    Case 2
                         Log("[Page] 主页预设:Minecraft 新闻")
                         Log("[Page] 主页预设:Minecraft 新闻")
                         Url = "http://pcl.mcnews.thestack.top"
                         Url = "http://pcl.mcnews.thestack.top"
                         GoTo Download
                         GoTo Download
-                    Case 3 '简单主页
+                    Case 3
                         Log("[Page] 主页预设:简单主页")
                         Log("[Page] 主页预设:简单主页")
                         Url = "https://gitee.com/mfn233/PCL-Mainpage/raw/main/Custom.xaml"
                         Url = "https://gitee.com/mfn233/PCL-Mainpage/raw/main/Custom.xaml"
                         GoTo Download
                         GoTo Download
-                    Case 4 '历史上的今天
-                        Log("[Page] 主页预设:历史上的今天")
-                        Url = "https://pclnews.52chye.cn/custom.xaml"
+                    Case 4
+                        Log("[Page] 主页预设:每日整合包推荐")
+                        Url = "https://pclsub.sodamc.com/"
+                        GoTo Download
+                    Case 5
+                        Log("[Page] 主页预设:Minecraft 皮肤推荐")
+                        Url = "https://forgepixel.com/pcl_sub_file"
                         GoTo Download
                         GoTo Download
                 End Select
                 End Select
         End Select
         End Select
@@ -106,16 +110,17 @@ Download:
     '联网获取自定义主页文件
     '联网获取自定义主页文件
     Private OnlineLoader As New LoaderTask(Of String, Integer)("自定义主页获取", AddressOf OnlineLoaderSub) With {.ReloadTimeout = 10 * 60 * 1000}
     Private OnlineLoader As New LoaderTask(Of String, Integer)("自定义主页获取", AddressOf OnlineLoaderSub) With {.ReloadTimeout = 10 * 60 * 1000}
     Private Sub OnlineLoaderSub(Task As LoaderTask(Of String, Integer))
     Private Sub OnlineLoaderSub(Task As LoaderTask(Of String, Integer))
+        Dim Address As String = Task.Input '#3721 中连续触发两次导致内容变化
         Try
         Try
             '获取版本校验地址
             '获取版本校验地址
             Dim VersionAddress As String
             Dim VersionAddress As String
-            If Task.Input.Contains(".xaml") Then
-                VersionAddress = Task.Input.Replace(".xaml", ".xaml.ini")
+            If Address.Contains(".xaml") Then
+                VersionAddress = Address.Replace(".xaml", ".xaml.ini")
             Else
             Else
-                VersionAddress = Task.Input.Split("?").First
+                VersionAddress = Address.Before("?")
                 If Not VersionAddress.EndsWith("/") Then VersionAddress += "/"
                 If Not VersionAddress.EndsWith("/") Then VersionAddress += "/"
                 VersionAddress += "version"
                 VersionAddress += "version"
-                If Task.Input.Contains("?") Then VersionAddress += Task.Input.Split("?").Last
+                If Address.Contains("?") Then VersionAddress += Address.After("?")
             End If
             End If
             '校验版本
             '校验版本
             Dim Version As String = ""
             Dim Version As String = ""
@@ -136,9 +141,9 @@ Download:
             End Try
             End Try
             '实际下载
             '实际下载
             If NeedDownload Then
             If NeedDownload Then
-                Dim FileContent As String = NetGetCodeByRequestRetry(Task.Input)
-                Log($"[Page] 已联网下载自定义主页,内容长度:{FileContent.Length},来源:{Task.Input}")
-                Setup.Set("CacheSavedPageUrl", Task.Input)
+                Dim FileContent As String = NetGetCodeByRequestRetry(Address)
+                Log($"[Page] 已联网下载自定义主页,内容长度:{FileContent.Length},来源:{Address}")
+                Setup.Set("CacheSavedPageUrl", Address)
                 Setup.Set("CacheSavedPageVersion", Version)
                 Setup.Set("CacheSavedPageVersion", Version)
                 WriteFile(PathTemp & "Cache\Custom.xaml", FileContent)
                 WriteFile(PathTemp & "Cache\Custom.xaml", FileContent)
             End If
             End If
@@ -146,11 +151,11 @@ Download:
             Refresh()
             Refresh()
         Catch ex As Exception
         Catch ex As Exception
             If Setup.Get("CacheSavedPageVersion") = "" Then
             If Setup.Get("CacheSavedPageVersion") = "" Then
-                Log(ex, $"联网下载自定义主页失败({Task.Input})", LogLevel.Msgbox)
+                Log(ex, $"联网下载自定义主页失败({Address})", LogLevel.Msgbox)
             Else
             Else
-                Log(ex, $"联网下载自定义主页失败({Task.Input})")
+                Log(ex, $"联网下载自定义主页失败({Address})")
             End If
             End If
-            Task.Input = ""
+            Address = ""
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -208,6 +213,7 @@ Download:
                     GoTo Refresh '防止 SyncLock 死锁
                     GoTo Refresh '防止 SyncLock 死锁
                 End If
                 End If
             End Try
             End Try
+            Log($"[Page] 实例化:加载自定义主页 UI 完成")
         End SyncLock
         End SyncLock
         Return
         Return
 Refresh:
 Refresh:

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

@@ -17,8 +17,8 @@
                 ComboName.ItemsSource = Nothing
                 ComboName.ItemsSource = Nothing
             Else
             Else
                 ComboName.ItemsSource = Setup.Get("LoginAuthEmail").ToString.Split("¨")
                 ComboName.ItemsSource = Setup.Get("LoginAuthEmail").ToString.Split("¨")
-                ComboName.Text = Setup.Get("LoginAuthEmail").ToString.Split("¨")(0)
-                If Setup.Get("LoginRemember") Then TextPass.Password = Setup.Get("LoginAuthPass").ToString.Split("¨")(0).Trim
+                ComboName.Text = Setup.Get("LoginAuthEmail").ToString.Before("¨")
+                If Setup.Get("LoginRemember") Then TextPass.Password = Setup.Get("LoginAuthPass").ToString.Before("¨").Trim
             End If
             End If
         End If
         End If
         IsFirstLoad = False
         IsFirstLoad = False

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

@@ -26,7 +26,7 @@ Public Class PageLoginLegacy
                 ComboName.ItemsSource = Nothing
                 ComboName.ItemsSource = Nothing
             Else
             Else
                 ComboName.ItemsSource = Setup.Get("LoginLegacyName").ToString.Split("¨")
                 ComboName.ItemsSource = Setup.Get("LoginLegacyName").ToString.Split("¨")
-                ComboName.Text = Setup.Get("LoginLegacyName").ToString.Split("¨")(0).Trim
+                ComboName.Text = Setup.Get("LoginLegacyName").ToString.Before("¨").Trim
             End If
             End If
         End If
         End If
         IsReloaded = True
         IsReloaded = True

+ 37 - 33
Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMs.xaml.vb

@@ -67,39 +67,43 @@
         ComboAccounts.IsEnabled = False
         ComboAccounts.IsEnabled = False
         BtnLogin.IsEnabled = False
         BtnLogin.IsEnabled = False
         BtnLogin.Text = "0%"
         BtnLogin.Text = "0%"
-        RunInNewThread(Sub()
-                           Try
-                               McLoginMsLoader.Start(GetLoginData(), IsForceRestart:=True)
-                               Do While McLoginMsLoader.State = LoadState.Loading
-                                   RunInUi(Sub() BtnLogin.Text = Math.Round(McLoginMsLoader.Progress * 100) & "%")
-                                   Thread.Sleep(50)
-                               Loop
-                               If McLoginMsLoader.State = LoadState.Finished Then
-                                   RunInUi(Sub() FrmLaunchLeft.RefreshPage(False, True))
-                               ElseIf McLoginMsLoader.State = LoadState.Aborted Then
-                                   Throw New ThreadInterruptedException
-                               ElseIf McLoginMsLoader.Error Is Nothing Then
-                                   Throw New Exception("未知错误!")
-                               Else
-                                   Throw New Exception(McLoginMsLoader.Error.Message, McLoginMsLoader.Error)
-                               End If
-                           Catch ex As ThreadInterruptedException
-                               Hint("已取消登录!")
-                           Catch ex As Exception
-                               If ex.Message = "$$" Then
-                               ElseIf ex.Message.StartsWith("$") Then
-                                   Hint(ex.Message.TrimStart("$"), HintType.Critical)
-                               Else
-                                   Log(ex, "微软登录尝试失败", LogLevel.Msgbox)
-                               End If
-                           Finally
-                               RunInUi(Sub()
-                                           ComboAccounts.IsEnabled = True
-                                           BtnLogin.IsEnabled = True
-                                           BtnLogin.Text = "登录"
-                                       End Sub)
-                           End Try
-                       End Sub, "Ms Login")
+        RunInNewThread(
+        Sub()
+            Try
+                McLoginMsLoader.Start(GetLoginData(), IsForceRestart:=True)
+                Do While McLoginMsLoader.State = LoadState.Loading
+                    RunInUi(Sub() BtnLogin.Text = Math.Round(McLoginMsLoader.Progress * 100) & "%")
+                    Thread.Sleep(50)
+                Loop
+                If McLoginMsLoader.State = LoadState.Finished Then
+                    RunInUi(Sub() FrmLaunchLeft.RefreshPage(False, True))
+                ElseIf McLoginMsLoader.State = LoadState.Aborted Then
+                    Throw New ThreadInterruptedException
+                ElseIf McLoginMsLoader.Error Is Nothing Then
+                    Throw New Exception("未知错误!")
+                Else
+                    Throw New Exception(McLoginMsLoader.Error.Message, McLoginMsLoader.Error)
+                End If
+            Catch ex As ThreadInterruptedException
+                Hint("已取消登录!")
+            Catch ex As Exception
+                If ex.Message = "$$" Then
+                ElseIf ex.Message.StartsWith("$") Then
+                    Hint(ex.Message.TrimStart("$"), HintType.Critical)
+                ElseIf TypeOf ex Is Security.Authentication.AuthenticationException AndAlso ex.Message.ContainsF("SSL/TLS") Then
+                    Log(ex, "正版登录验证失败,请尝试在 [设置 → 启动器] 中关闭 [验证 SSL 证书] 然后再试。" & vbCrLf & vbCrLf & "原始错误信息:", LogLevel.Msgbox)
+                Else
+                    Log(ex, "正版登录尝试失败", LogLevel.Msgbox)
+                End If
+            Finally
+                RunInUi(
+                Sub()
+                    ComboAccounts.IsEnabled = True
+                    BtnLogin.IsEnabled = True
+                    BtnLogin.Text = "登录"
+                End Sub)
+            End Try
+        End Sub, "Ms Login")
     End Sub
     End Sub
 
 
 End Class
 End Class

+ 44 - 43
Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMsSkin.xaml.vb

@@ -85,50 +85,51 @@
         Hint("正在更改皮肤……")
         Hint("正在更改皮肤……")
         IsChanging = True
         IsChanging = True
         '开始实际获取
         '开始实际获取
-        RunInNewThread(Async Sub()
-                           Try
+        RunInNewThread(
+        Async Sub()
+            Try
 Retry:
 Retry:
-                               If McLoginMsLoader.State = LoadState.Loading Then McLoginMsLoader.WaitForExit() '等待登录结束
-                               Dim AccessToken As String = Setup.Get("CacheMsAccess")
-                               Dim Uuid As String = Setup.Get("CacheMsUuid")
-
-                               Dim Client As New Net.Http.HttpClient With {.Timeout = New TimeSpan(0, 0, 30)}
-                               Client.DefaultRequestHeaders.Authorization = New Net.Http.Headers.AuthenticationHeaderValue("Bearer", AccessToken)
-                               Client.DefaultRequestHeaders.Accept.Add(New Net.Http.Headers.MediaTypeWithQualityHeaderValue("*/*"))
-                               Client.DefaultRequestHeaders.UserAgent.Add(New Net.Http.Headers.ProductInfoHeaderValue("MojangSharp", "0.1"))
-                               Dim Contents As New Net.Http.MultipartFormDataContent From {
-                                      {New Net.Http.StringContent(If(SkinInfo.IsSlim, "slim", "classic")), "variant"},
-                                      {New Net.Http.ByteArrayContent(File.ReadAllBytes(SkinInfo.LocalFile)), "file", GetFileNameFromPath(SkinInfo.LocalFile)}
-                                  }
-                               Dim Result As String = Await (Await Client.PostAsync(New Uri("https://api.minecraftservices.com/minecraft/profile/skins"), Contents)).Content.ReadAsStringAsync
-                               If Result.Contains("request requires user authentication") Then
-                                   Hint("正在登录,将在登录完成后继续更改皮肤……")
-                                   McLoginMsLoader.Start(GetLoginData(), IsForceRestart:=True)
-                                   GoTo Retry
-                               ElseIf Result.Contains("""error""") Then
-                                   Hint("更改皮肤失败:" & GetJson(Result)("error"), HintType.Critical)
-                                   Exit Sub
-                               End If
-                               '获取新皮肤地址
-                               Log("[Skin] 皮肤修改返回值:" & vbCrLf & Result)
-                               Dim ResultJson As JObject = GetJson(Result)
-                               For Each Skin As JObject In ResultJson("skins")
-                                   If Skin("state").ToString = "ACTIVE" Then
-                                       MySkin.ReloadCache(Skin("url"))
-                                       Exit Sub
-                                   End If
-                               Next
-                               Throw New Exception("未知错误(" & Result & ")")
-                           Catch ex As Exception
-                               If ex.GetType.Equals(GetType(Tasks.TaskCanceledException)) Then
-                                   Hint("更改皮肤失败:与 Mojang 皮肤服务器的连接超时,请检查你的网络是否通畅!", HintType.Critical)
-                               Else
-                                   Log(ex, "更改皮肤失败", LogLevel.Hint)
-                               End If
-                           Finally
-                               IsChanging = False
-                           End Try
-                       End Sub, "Ms Skin Upload")
+                If McLoginMsLoader.State = LoadState.Loading Then McLoginMsLoader.WaitForExit() '等待登录结束
+                Dim AccessToken As String = Setup.Get("CacheMsAccess")
+                Dim Uuid As String = Setup.Get("CacheMsUuid")
+
+                Dim Client As New Net.Http.HttpClient With {.Timeout = New TimeSpan(0, 0, 30)}
+                Client.DefaultRequestHeaders.Authorization = New Net.Http.Headers.AuthenticationHeaderValue("Bearer", AccessToken)
+                Client.DefaultRequestHeaders.Accept.Add(New Net.Http.Headers.MediaTypeWithQualityHeaderValue("*/*"))
+                Client.DefaultRequestHeaders.UserAgent.Add(New Net.Http.Headers.ProductInfoHeaderValue("MojangSharp", "0.1"))
+                Dim Contents As New Net.Http.MultipartFormDataContent From {
+                    {New Net.Http.StringContent(If(SkinInfo.IsSlim, "slim", "classic")), "variant"},
+                    {New Net.Http.ByteArrayContent(File.ReadAllBytes(SkinInfo.LocalFile)), "file", GetFileNameFromPath(SkinInfo.LocalFile)}
+                }
+                Dim Result As String = Await (Await Client.PostAsync(New Uri("https://api.minecraftservices.com/minecraft/profile/skins"), Contents)).Content.ReadAsStringAsync
+                If Result.Contains("request requires user authentication") Then
+                    Hint("正在登录,将在登录完成后继续更改皮肤……")
+                    McLoginMsLoader.Start(GetLoginData(), IsForceRestart:=True)
+                    GoTo Retry
+                ElseIf Result.Contains("""error""") Then
+                    Hint("更改皮肤失败:" & GetJson(Result)("error"), HintType.Critical)
+                    Exit Sub
+                End If
+                '获取新皮肤地址
+                Log("[Skin] 皮肤修改返回值:" & vbCrLf & Result)
+                Dim ResultJson As JObject = GetJson(Result)
+                For Each Skin As JObject In ResultJson("skins")
+                    If Skin("state").ToString = "ACTIVE" Then
+                        MySkin.ReloadCache(Skin("url"))
+                        Exit Sub
+                    End If
+                Next
+                Throw New Exception("未知错误(" & Result & ")")
+            Catch ex As Exception
+                If ex.GetType.Equals(GetType(Tasks.TaskCanceledException)) Then
+                    Hint("更改皮肤失败:与 Mojang 皮肤服务器的连接超时,请检查你的网络是否通畅!", HintType.Critical)
+                Else
+                    Log(ex, "更改皮肤失败", LogLevel.Hint)
+                End If
+            Finally
+                IsChanging = False
+            End Try
+        End Sub, "Ms Skin Upload")
     End Sub
     End Sub
 
 
     '保存皮肤
     '保存皮肤

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

@@ -17,8 +17,8 @@
                 ComboName.ItemsSource = Nothing
                 ComboName.ItemsSource = Nothing
             Else
             Else
                 ComboName.ItemsSource = Setup.Get("LoginNideEmail").ToString.Split("¨")
                 ComboName.ItemsSource = Setup.Get("LoginNideEmail").ToString.Split("¨")
-                ComboName.Text = Setup.Get("LoginNideEmail").ToString.Split("¨")(0)
-                If Setup.Get("LoginRemember") Then TextPass.Password = Setup.Get("LoginNidePass").ToString.Split("¨")(0).Trim
+                ComboName.Text = Setup.Get("LoginNideEmail").ToString.Before("¨")
+                If Setup.Get("LoginRemember") Then TextPass.Password = Setup.Get("LoginNidePass").ToString.Before("¨").Trim
             End If
             End If
         End If
         End If
         IsFirstLoad = False
         IsFirstLoad = False

+ 0 - 3
Plain Craft Launcher 2/Pages/PageLink/PageLinkIoi.xaml.vb

@@ -149,9 +149,6 @@ Public Class PageLinkIoi
     ''' 检查某个玩家名是否合法。
     ''' 检查某个玩家名是否合法。
     ''' </summary>
     ''' </summary>
     Private Shared Function IsPlayerNameValid(Name As String) As Boolean
     Private Shared Function IsPlayerNameValid(Name As String) As Boolean
-        For Each ValidateRule As Validate In {New ValidateNullOrWhiteSpace, New ValidateLength(0, 20), New ValidateFilter}
-            If Not String.IsNullOrEmpty(ValidateRule.Validate(Name)) Then Return False
-        Next
         Return True
         Return True
     End Function
     End Function
 
 

+ 332 - 0
Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml

@@ -0,0 +1,332 @@
+<local:MyPageRight
+    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+    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" 
+    mc:Ignorable="d" x:Class="PageOtherAbout"
+    PanScroll="{Binding ElementName=PanBack}" RenderOptions.BitmapScalingMode="LowQuality">
+    <local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" x:Name="PanBack">
+        <StackPanel x:Name="PanMain" Margin="25,10" Grid.IsSharedSizeScope="True">
+            <local:MyCard Margin="0,15" Title="关于">
+                <Grid Margin="21,40,21,16">
+                    <Grid.RowDefinitions>
+                        <RowDefinition Height="45" />
+                        <RowDefinition Height="45" />
+                    </Grid.RowDefinitions>
+                    <Grid.ColumnDefinitions>
+                        <ColumnDefinition Width="40" />
+                        <ColumnDefinition />
+                        <ColumnDefinition Width="150" />
+                    </Grid.ColumnDefinitions>
+                    <local:MyListItem IsHitTestVisible="False" Title="龙腾猫跃" Margin="-5,0,15,0" Info="Plain Craft Launcher 的作者!" Grid.Row="0" Grid.Column="1" />
+                    <local:MyListItem IsHitTestVisible="False" Title="Plain Craft Launcher 2" Margin="-5,0,15,0" Info="当前版本:%VERSION%(%VERSIONCODE%.%BRANCH%)" Grid.Row="1" Grid.Column="1" x:Name="ItemAboutPcl" />
+                    <Image Source="/Plain Craft Launcher 2;component/Images/Heads/Logo.png" Grid.Row="1" Margin="3">
+                        <Image.Clip>
+                            <EllipseGeometry Center="17 17" RadiusX="17" RadiusY="17" />
+                        </Image.Clip>
+                    </Image>
+                    <Image Source="/Plain Craft Launcher 2;component/Images/Heads/LTCat.jpg" Grid.Row="0" Margin="3">
+                        <Image.Clip>
+                            <EllipseGeometry Center="17 17" RadiusX="17" RadiusY="17" />
+                        </Image.Clip>
+                    </Image>
+                    <local:MyButton Text="赞助作者" Grid.Column="2" Grid.Row="0" Height="35" EventType="打开网页" EventData="https://afdian.net/a/LTCat" />
+                    <local:MyButton Text="查看源代码" Grid.Column="2" Grid.Row="1" Height="35" EventType="打开网页" EventData="https://github.com/Hex-Dragon/PCL2" />
+                </Grid>
+            </local:MyCard>
+            <local:MyCard Margin="0,0,0,15" Title="特别鸣谢">
+                <Grid Margin="21,40,21,16">
+                    <Grid.RowDefinitions>
+                        <RowDefinition Height="45" />
+                        <RowDefinition Height="45" />
+                        <RowDefinition Height="45" />
+                        <RowDefinition Height="45" />
+                        <RowDefinition Height="45" />
+                        <RowDefinition Height="45" />
+                        <RowDefinition Height="45" />
+                        <RowDefinition Height="45" />
+                    </Grid.RowDefinitions>
+                    <Grid.ColumnDefinitions>
+                        <ColumnDefinition Width="40" />
+                        <ColumnDefinition />
+                        <ColumnDefinition Width="150" />
+                    </Grid.ColumnDefinitions>
+                    <local:MyListItem Title="bangbang93" Margin="-5,0,15,0" IsHitTestVisible="False" Info="提供 BMCLAPI 镜像源和 Forge 安装工具,详见 https://bmclapi.bangbang93.com" Grid.Row="0" Grid.Column="1" />
+                    <local:MyButton x:Name="BtnAboutBmclapi" Text="赞助镜像源" Grid.Row="0" Grid.Column="2" Height="35" />
+                    <local:MyListItem Title="MC 百科" Margin="-5,0,0,0" IsHitTestVisible="False" Grid.ColumnSpan="2" Info="提供了 Mod 名称的中文翻译和更多相关信息!" Grid.Row="1" Grid.Column="1" />
+                    <local:MyButton x:Name="BtnAboutWiki" Text="打开百科" Grid.Row="1" Grid.Column="2" Height="35" />
+                    <local:MyListItem Title="00ll00" Margin="-5,0,0,0" IsHitTestVisible="False" Grid.ColumnSpan="2" Info="提供了 Java Launch Wrapper 和一些重要服务支持!" Grid.Row="2" Grid.Column="1" />
+                    <local:MyListItem Title="DoodleHuang" Margin="-5,0,0,0" IsHitTestVisible="False" Grid.ColumnSpan="2" Info="提供了部分很难找到的 Minecraft 版本与一些技术支持!" Grid.Row="3" Grid.Column="1" />
+                    <local:MyListItem Title="Patrick" Margin="-5,0,0,0" IsHitTestVisible="False" Grid.ColumnSpan="2" Info="设计并制作了 PCL 图标,让我从做图标的水深火热中得到了解脱……" Grid.Row="4" Grid.Column="1" />
+                    <local:MyListItem Title="Hao_Tian" Margin="-5,0,0,0" IsHitTestVisible="False" Grid.ColumnSpan="2" Info="在内测中找出了一大堆没人想得到的诡异 Bug,有非同寻常的 Bug 体质" Grid.Row="5" Grid.Column="1" />
+                    <local:MyListItem Title="Minecraft 中文论坛" Margin="-5,0,15,0" IsHitTestVisible="False" Info="虽然已经关站了,但感谢此前提供了 MCBBS 镜像源……" Grid.Row="6" Grid.Column="1" />
+                    <local:MyListItem Title="PCL 内群的各位" Margin="-5,0,0,0" IsHitTestVisible="False" Grid.ColumnSpan="2" Info="感谢内群的沙雕网友们这么久以来对我和 PCL 的支持与鼓励!" Grid.Row="7" Grid.Column="1" />
+                    <Image Source="/Plain Craft Launcher 2;component/Images/Heads/bangbang93.png" Grid.Row="0" Margin="3">
+                        <Image.Clip>
+                            <EllipseGeometry Center="17 17" RadiusX="17" RadiusY="17" />
+                        </Image.Clip>
+                    </Image>
+                    <Image Source="/Plain Craft Launcher 2;component/Images/Heads/wiki.png" Grid.Row="1" Margin="3">
+                        <Image.Clip>
+                            <EllipseGeometry Center="17 17" RadiusX="17" RadiusY="17" />
+                        </Image.Clip>
+                    </Image>
+                    <Image Source="/Plain Craft Launcher 2;component/Images/Heads/00ll00.png" Grid.Row="2" Margin="3">
+                        <Image.Clip>
+                            <EllipseGeometry Center="17 17" RadiusX="17" RadiusY="17" />
+                        </Image.Clip>
+                    </Image>
+                    <Image Source="/Plain Craft Launcher 2;component/Images/Heads/DoodleHuang.png" Grid.Row="3" Margin="3">
+                        <Image.Clip>
+                            <EllipseGeometry Center="17 17" RadiusX="17" RadiusY="17" />
+                        </Image.Clip>
+                    </Image>
+                    <Image Source="/Plain Craft Launcher 2;component/Images/Heads/Patrick.png" Grid.Row="4" Margin="3">
+                        <Image.Clip>
+                            <EllipseGeometry Center="17 17" RadiusX="17" RadiusY="17" />
+                        </Image.Clip>
+                    </Image>
+                    <Image Source="/Plain Craft Launcher 2;component/Images/Heads/Hao_Tian.jpg" Grid.Row="5" Margin="3">
+                        <Image.Clip>
+                            <EllipseGeometry Center="17 17" RadiusX="17" RadiusY="17" />
+                        </Image.Clip>
+                    </Image>
+                    <Image Source="/Plain Craft Launcher 2;component/Images/Heads/MCBBS.png" Grid.Row="6" Margin="3">
+                        <Image.Clip>
+                            <EllipseGeometry Center="17 17" RadiusX="17" RadiusY="17" />
+                        </Image.Clip>
+                    </Image>
+                    <Image Source="/Plain Craft Launcher 2;component/Images/Heads/PCL2.png" Grid.Row="7" Margin="3">
+                        <Image.Clip>
+                            <EllipseGeometry Center="17 17" RadiusX="17" RadiusY="17" />
+                        </Image.Clip>
+                    </Image>
+                </Grid>
+            </local:MyCard>
+            <local:MyCard Margin="0,0,0,15" Title="赞助者">
+                <StackPanel Margin="25,40,10,20">
+                    <TextBlock HorizontalAlignment="Left" Text="以下是 2024 年 4 月的高级赞助者们!感谢各位对 PCL 和伟大的咕咕咕事业的支持!" Margin="0,0,25,5.5" TextTrimming="None" TextWrapping="WrapWithOverflow" />
+                    <WrapPanel Margin="0,0,0,10">
+                        <TextBlock Width="120" Margin="0,1.5" Text="道士师傅" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="狗群主群狗" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Alioth陳" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="shyzysa" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="死んだライオン" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="mc沙漠之狐隆帅" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="123456胡杨" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="香蕉风味" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="毛哥" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="leon" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="clnb" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="123" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="lihaoran0313" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="贵友" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="蒸福啦" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="狐狸涩" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="皓垣Faraway" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="冷月溪影" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="狮青椒" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="多金的爸爸" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="陈师傅" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="王舒心" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Akn123" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Dfkeit" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="CloudLonngLee" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="别再错过月亮了" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="特别周冠宇" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="臻榛搸殝" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="GT" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="夜也" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="is步行街" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="狮心人面人" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="小刀李" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="空染" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="歪歪的示波器" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="SYF" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="帝王尬笑" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="DarkKCat" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="我将如闪电般归来" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="TSLOLY" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="JackX" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="挽安" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="花衔镜" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="玄言" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="硫酸梅" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="ムーの小雨" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="潘某" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="颐葭" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Vaincre_VV" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="小明爱睡觉" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="YHLin" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="翁yy" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="午睡不爱睡觉" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="tanglin" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Quittskii" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="W2erty" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Chris_Dream" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="sanrenxin" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="BBK" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="京北的洋" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="喵木仔" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="abstudio" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="洛惜惜缘" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="熊心万花镜" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="tianguaren" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="bitlogi" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="彬彬爆" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="超爱小辣椒" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Kkigoo" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="于书墨" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="御坂1354号" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="t7fihb8" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="44.5GB" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="寰宇第一激推希儿" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="拳打羽酱脚踢军师" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="戌亥ピカ" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="乔巴诺" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="LING-DIAN" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="你太迷人" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="夏目龙羽" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="小烨爱吃尖椒鸡" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="七色的魔法死" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="沫世" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="我的神a啊" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="转角" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="做强做盛.步步升高" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="火箭空降" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="嚯嚯嚯哈哈哈哈" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="醉梦" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="zzyd" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="御影" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Xephyrs" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Sovtheia" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="CRANK" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="本太子的作业满天飞" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="超级无敌旋风小猪猪" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="泽希尔·S+" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="erliang_ya" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="明酱酱" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="玐煲粥" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="The_follow" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="王八" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="joker" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="north-cool" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="JJ" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="小石" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="XiaomQ928" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Fly with You!!" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="海上乘鲸客" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="miku" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="小雯" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="woainad" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="wqaycm" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="白豆" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Alex" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="大地风暴" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="曾and田某人" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="栀玖夕夏" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="小卌丯" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="旧言" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="?的不行  ?的难受" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="ElliottZChan" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="沐子冷落" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="_Blessing" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="友酒歌" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="鲸以歌" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="花好月圆" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="n3v3r" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="七星灯" />
+                        <TextBlock Width="120" Margin="0,1.5" Text="Theia" />
+
+                    </WrapPanel>
+                    <Grid Height="35">
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
+                            <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
+                            <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
+                        </Grid.ColumnDefinitions>
+                        <local:MyButton x:Name="BtnDonateOutput" Grid.Column="0" MinWidth="140" Text="生成解锁码" Padding="13,0" Margin="0,0,20,0" HorizontalAlignment="Left" ColorType="Highlight" Visibility="Collapsed" />
+                        <local:MyButton x:Name="BtnDonateDonate" Grid.Column="0" MinWidth="140" Text="赞助作者" Padding="13,0" Margin="0,0,20,0" HorizontalAlignment="Left" ColorType="Highlight" EventType="打开网页" EventData="https://afdian.net/a/LTCat" />
+                        <local:MyButton x:Name="BtnDonateCopy" Grid.Column="1" MinWidth="140" Text="复制识别码" Padding="13,0" Margin="0,0,20,0" HorizontalAlignment="Left" />
+                        <local:MyButton x:Name="BtnDonateInput" Grid.Column="2" MinWidth="140" Text="输入解锁码" Padding="13,0" Margin="0,0,20,0" HorizontalAlignment="Left" />
+                    </Grid>
+                </StackPanel>
+            </local:MyCard>
+            <local:MyCard Margin="0,0,0,15" Title="法律信息" IsSwaped="True" CanSwap="True">
+                <StackPanel Margin="25,39,23,20">
+                    <TextBlock Text="隐私说明" TextWrapping="Wrap" Margin="0,6,0,4" FontWeight="Bold" />
+                    <TextBlock Margin="0,0,0,2" LineHeight="17" Text="本软件会汇报自己的版本名与版本号,以统计用户数量与各版本的用户数量占比。除此之外,本软件不会上传您的任何信息。&#13;若您能提供未经外部修改的本软件本体上传隐私信息的确凿证据,开发者会向您支付 PCL 开发所获全部净收入。" TextWrapping="Wrap" />
+                    <TextBlock Text="其他信息" TextWrapping="Wrap" Margin="0,12,0,4" FontWeight="Bold" />
+                    <TextBlock Margin="0,0,0,2" LineHeight="17" Text="Copyright © 龙腾猫跃 2016. All Rights Reserved.&#13;计算机软件著作权登记号:2020SR0875133&#13;违法违规行为举报投诉邮箱:[email protected]" TextWrapping="Wrap" />
+                    <StackPanel Orientation="Horizontal" Height="35" HorizontalAlignment="Left" Margin="0,12,0,0">
+                        <local:MyButton Text="用户协议与免责声明" Width="170" Padding="13,0" Margin="0,0,20,0" ColorType="Highlight" EventType="打开网页" EventData="https://shimo.im/docs/rGrd8pY8xWkt6ryW" />
+                        <local:MyButton Text="查看源代码" Width="140" Padding="13,0" EventType="打开网页" EventData="https://github.com/Hex-Dragon/PCL2" />
+                    </StackPanel>
+                </StackPanel>
+            </local:MyCard>
+            <local:MyCard Margin="0,0,0,15" Title="许可与版权声明" IsSwaped="True" CanSwap="True">
+                <Grid Margin="25,42,23,4">
+                    <Grid.ColumnDefinitions>
+                        <ColumnDefinition Width="Auto" />
+                        <ColumnDefinition Width="20" />
+                        <ColumnDefinition Width="Auto" />
+                        <ColumnDefinition />
+                    </Grid.ColumnDefinitions>
+                    <Grid.RowDefinitions>
+                        <RowDefinition Height="Auto" />
+                        <RowDefinition Height="60" />
+                        <RowDefinition Height="Auto" />
+                        <RowDefinition Height="60" />
+                        <RowDefinition Height="Auto" />
+                        <RowDefinition Height="60" />
+                        <RowDefinition Height="Auto" />
+                        <RowDefinition Height="60" />
+                        <RowDefinition Height="Auto" />
+                        <RowDefinition Height="60" />
+                        <RowDefinition Height="Auto" />
+                        <RowDefinition Height="60" />
+                        <RowDefinition Height="Auto" />
+                    </Grid.RowDefinitions>
+                    <TextBlock Grid.Row="0" Grid.Column="0" Text="Java Launch Wrapper" TextWrapping="Wrap" FontWeight="Bold" />
+                    <TextBlock Grid.Row="0" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © 2022 00ll00&#xa;Licensed under the MIT License." TextWrapping="Wrap" />
+                    <local:MyButton Grid.Row="1" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
+                                    EventType="打开网页" EventData="https://github.com/00ll00/java_launch_wrapper" />
+                    <local:MyButton Grid.Row="1" Grid.Column="3" MinWidth="140" Text="查看许可文档" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
+                                    EventType="打开网页" EventData="https://github.com/00ll00/java_launch_wrapper?tab=MIT-1-ov-file#readme" />
+
+                    <TextBlock Grid.Row="2" Grid.Column="0" Text="Newtonsoft.Json" TextWrapping="Wrap" FontWeight="Bold" />
+                    <TextBlock Grid.Row="2" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © 2007 James Newton-King&#xa;Licensed under the MIT License." TextWrapping="Wrap" />
+                    <local:MyButton Grid.Row="3" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
+                                    EventType="打开网页" EventData="https://www.newtonsoft.com/json" />
+                    <local:MyButton Grid.Row="3" Grid.Column="3" MinWidth="140" Text="查看许可文档" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
+                                    EventType="打开网页" EventData="https://opensource.org/licenses/MIT" />
+                    
+                    <TextBlock Grid.Row="4" Grid.Column="0" Text="NAudio" TextWrapping="Wrap" FontWeight="Bold" />
+                    <TextBlock Grid.Row="4" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © 2015 Mark Heath ([email protected])&#xa;Licensed under the MIT License." TextWrapping="Wrap" />
+                    <local:MyButton Grid.Row="5" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
+                                    EventType="打开网页" EventData="https://github.com/naudio/NAudio" />
+                    <local:MyButton Grid.Row="5" Grid.Column="3" MinWidth="140" Text="查看许可文档" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left" 
+                                    EventType="打开网页" EventData="https://github.com/naudio/NAudio/blob/master/license.txt" />
+                    
+                    <TextBlock Grid.Row="6" Grid.Column="0" Text="Ookii.Dialogs" TextWrapping="Wrap" FontWeight="Bold" />
+                    <TextBlock Grid.Row="6" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © 2009 Sven Groot (Ookii.org)" TextWrapping="Wrap" />
+                    <local:MyButton Grid.Row="7" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
+                                    EventType="打开网页" EventData="https://www.ookii.org/Software/Dialogs/" />
+                    <local:MyButton Grid.Row="7" Grid.Column="3" MinWidth="140" Text="查看许可文档" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
+                                    EventType="打开网页" EventData="https://github.com/caioproiete/ookii-dialogs-wpf/blob/master/LICENSE" />
+                    
+                    <TextBlock Grid.Row="8" Grid.Column="0" Text="Iconmonstr" TextWrapping="Wrap" FontWeight="Bold" />
+                    <TextBlock Grid.Row="8" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © 2019 iconmonstr (Alexander Kahlkopf)" TextWrapping="Wrap" />
+                    <local:MyButton Grid.Row="9" Grid.Column="2" MinWidth="140" Text="查看来源网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
+                                    EventType="打开网页" EventData="https://iconmonstr.com/" />
+                    <local:MyButton Grid.Row="9" Grid.Column="3" MinWidth="140" Text="查看许可文档" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
+                                    EventType="打开网页" EventData="https://iconmonstr.com/license/" />
+                    
+                    <TextBlock Grid.Row="10" Grid.Column="0" Text="Iconfont" TextWrapping="Wrap" FontWeight="Bold" />
+                    <TextBlock Grid.Row="10" Grid.Column="2" Grid.ColumnSpan="2" Text="Copyright © ALIMAMA MUX, powered by alimama THX." TextWrapping="Wrap" />
+                    <local:MyButton Grid.Row="11" Grid.Column="2" MinWidth="140" Text="查看原网站" Padding="13,0" Margin="0,7,20,18" HorizontalAlignment="Left"
+                                    EventType="打开网页" EventData="https://www.iconfont.cn/" />
+                </Grid>
+            </local:MyCard>
+        </StackPanel>
+    </local:MyScrollViewer>
+</local:MyPageRight>

+ 40 - 0
Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml.vb

@@ -0,0 +1,40 @@
+Public Class PageOtherAbout
+
+    Private Shadows IsLoaded As Boolean = False
+    Private Sub PageOtherAbout_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
+
+        '重复加载部分
+        PanBack.ScrollToHome()
+
+        '非重复加载部分
+        If IsLoaded Then Exit Sub
+        IsLoaded = True
+
+        ItemAboutPcl.Info = ItemAboutPcl.Info.Replace("%VERSION%", VersionDisplayName).Replace("%VERSIONCODE%", VersionCode).Replace("%BRANCH%", VersionBranchCode)
+#If DEBUG Then
+        BtnDonateDonate.Visibility = Visibility.Collapsed
+        BtnDonateOutput.Visibility = Visibility.Visible
+#End If
+
+    End Sub
+
+    Private Sub BtnAboutBmclapi_Click(sender As Object, e As EventArgs) Handles BtnAboutBmclapi.Click
+        OpenWebsite("https://afdian.net/a/bangbang93")
+    End Sub
+    Private Sub BtnAboutWiki_Click(sender As Object, e As EventArgs) Handles BtnAboutWiki.Click
+        OpenWebsite("https://www.mcmod.cn")
+    End Sub
+    Public Shared Sub CopyUniqueAddress() Handles BtnDonateCopy.Click
+        ClipboardSet(UniqueAddress)
+    End Sub
+    Private Sub DonateCodeInput() Handles BtnDonateInput.Click
+        DonateCodeInput()
+    End Sub
+
+#If DEBUG Then
+    Private Sub BtnDonateOutput_Click(sender As Object, e As EventArgs) Handles BtnDonateOutput.Click
+        DonateCodeGenerate()
+    End Sub
+#End If
+
+End Class

+ 12 - 0
Plain Craft Launcher 2/Pages/PageOther/PageOtherTest.xaml

@@ -0,0 +1,12 @@
+<local:MyPageRight x:Class="PageOtherTest"
+    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
+    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
+    xmlns:local="clr-namespace:PCL"
+    xmlns:sys="clr-namespace:System;assembly=mscorlib"
+    PanScroll="{Binding ElementName=PanBack}">
+    <local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" x:Name="PanBack">
+        <TextBlock Text="为便于维护,开源内容中不包含百宝箱功能……" HorizontalAlignment="Center" FontSize="15" Height="30" VerticalAlignment="Center" />
+    </local:MyScrollViewer>
+</local:MyPageRight>

+ 27 - 0
Plain Craft Launcher 2/Pages/PageOther/PageOtherTest.xaml.vb

@@ -0,0 +1,27 @@
+Public Class PageOtherTest
+
+    Public Shared Sub StartCustomDownload(Url As String, FileName As String, Optional Folder As String = Nothing)
+        Hint("为便于维护,开源内容中不包含百宝箱功能……")
+    End Sub
+    Public Shared Sub Jrrp()
+        Hint("为便于维护,开源内容中不包含百宝箱功能……")
+    End Sub
+    Public Shared Sub RubbishClear()
+        Hint("为便于维护,开源内容中不包含百宝箱功能……")
+    End Sub
+    Public Shared Sub MemoryOptimize(ShowHint As Boolean)
+        If ShowHint Then Hint("为便于维护,开源内容中不包含百宝箱功能……")
+    End Sub
+    Public Shared Sub MemoryOptimizeInternal()
+    End Sub
+    Public Shared Function GetRandomCave() As String
+        Return "为便于维护,开源内容中不包含百宝箱功能……"
+    End Function
+    Public Shared Function GetRandomHint() As String
+        Return "为便于维护,开源内容中不包含百宝箱功能……"
+    End Function
+    Public Shared Function GetRandomPresetHint() As String
+        Return "为便于维护,开源内容中不包含百宝箱功能……"
+    End Function
+
+End Class

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

@@ -237,7 +237,7 @@
                     If Not Version.Modable Then Continue For
                     If Not Version.Modable Then Continue For
                     '4. 该版本的隔离文件夹下不存在 mods
                     '4. 该版本的隔离文件夹下不存在 mods
                     Dim ModIndieFolder As New DirectoryInfo(Version.Path & "mods\")
                     Dim ModIndieFolder As New DirectoryInfo(Version.Path & "mods\")
-                    If ModIndieFolder.Exists AndAlso ModIndieFolder.EnumerateFiles.Count > 0 Then Exit Sub
+                    If ModIndieFolder.Exists AndAlso ModIndieFolder.EnumerateFiles.Any Then Exit Sub
                     '满足以上全部条件则视为根目录整合包
                     '满足以上全部条件则视为根目录整合包
                     Setup.Set("VersionArgumentIndie", 2, Version:=Version)
                     Setup.Set("VersionArgumentIndie", 2, Version:=Version)
                     Log("[Setup] 已自动关闭单版本隔离:" & Version.Name, LogLevel.Debug)
                     Log("[Setup] 已自动关闭单版本隔离:" & Version.Name, LogLevel.Debug)
@@ -295,7 +295,7 @@
             For i = 0 To Folders.Count - 1
             For i = 0 To Folders.Count - 1
                 If Folders(i) = "" Then Exit For
                 If Folders(i) = "" Then Exit For
                 If Folders(i).ToString.EndsWith(Folder.Path) Then
                 If Folders(i).ToString.EndsWith(Folder.Path) Then
-                    Name = Folders(i).ToString.Split(">")(0)
+                    Name = Folders(i).ToString.Before(">")
                     Folders.RemoveAt(i)
                     Folders.RemoveAt(i)
                     Exit For
                     Exit For
                 End If
                 End If
@@ -320,7 +320,7 @@
             For i = 0 To Folders.Count - 1
             For i = 0 To Folders.Count - 1
                 If Folders(i) = "" Then Exit For
                 If Folders(i) = "" Then Exit For
                 If Folders(i).ToString.EndsWith(Folder.Path) Then
                 If Folders(i).ToString.EndsWith(Folder.Path) Then
-                    'Name = Folders(i).ToString.Split(">")(0)
+                    'Name = Folders(i).ToString.Before(">")
                     Folders.RemoveAt(i)
                     Folders.RemoveAt(i)
                     Exit For
                     Exit For
                 End If
                 End If

+ 691 - 0
Plain Craft Launcher 2/Pages/PageSetup/ModSetup.vb

@@ -0,0 +1,691 @@
+Public Class ModSetup
+
+    ''' <summary>
+    ''' 设置的更新号。
+    ''' </summary>
+    Public Const VersionSetup As Integer = 1
+    ''' <summary>
+    ''' 设置列表。
+    ''' </summary>
+    Private ReadOnly SetupDict As New Dictionary(Of String, SetupEntry) From {
+        {"Identify", New SetupEntry("", Source:=SetupSource.Registry)},
+        {"HintDownloadThread", New SetupEntry(False, Source:=SetupSource.Registry)},
+        {"HintNotice", New SetupEntry(0, Source:=SetupSource.Registry)},
+        {"HintDownload", New SetupEntry(0, Source:=SetupSource.Registry)},
+        {"HintInstallBack", New SetupEntry(False, Source:=SetupSource.Registry)},
+        {"HintHide", New SetupEntry(False, Source:=SetupSource.Registry)},
+        {"HintHandInstall", New SetupEntry(False, Source:=SetupSource.Registry)},
+        {"HintBuy", New SetupEntry(False, Source:=SetupSource.Registry)},
+        {"HintClearRubbish", New SetupEntry(0, Source:=SetupSource.Registry)},
+        {"HintUpdateMod", New SetupEntry(False, Source:=SetupSource.Registry)},
+        {"TestSetupReader", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"SystemEula", New SetupEntry(False, Source:=SetupSource.Registry)},
+        {"SystemCount", New SetupEntry(0, Source:=SetupSource.Registry, Encoded:=True)},
+        {"SystemLaunchCount", New SetupEntry(0, Source:=SetupSource.Registry, Encoded:=True)},
+        {"SystemLastVersionReg", New SetupEntry(0, Source:=SetupSource.Registry, Encoded:=True)},
+        {"SystemHighestBetaVersionReg", New SetupEntry(0, Source:=SetupSource.Registry, Encoded:=True)},
+        {"SystemHighestAlphaVersionReg", New SetupEntry(0, Source:=SetupSource.Registry, Encoded:=True)},
+        {"SystemSetupVersionReg", New SetupEntry(VersionSetup, Source:=SetupSource.Registry)},
+        {"SystemSetupVersionIni", New SetupEntry(VersionSetup)},
+        {"SystemHelpVersion", New SetupEntry(0, Source:=SetupSource.Registry)},
+        {"SystemDebugMode", New SetupEntry(False, Source:=SetupSource.Registry)},
+        {"SystemDebugAnim", New SetupEntry(9, Source:=SetupSource.Registry)},
+        {"SystemDebugDelay", New SetupEntry(False, Source:=SetupSource.Registry)},
+        {"SystemDebugSkipCopy", New SetupEntry(False, Source:=SetupSource.Registry)},
+        {"SystemSystemCache", New SetupEntry("", Source:=SetupSource.Registry)},
+        {"SystemSystemUpdate", New SetupEntry(0)},
+        {"SystemSystemActivity", New SetupEntry(0)},
+        {"CacheSavedPageUrl", New SetupEntry("", Source:=SetupSource.Registry)},
+        {"CacheSavedPageVersion", New SetupEntry("", Source:=SetupSource.Registry)},
+        {"CacheMsOAuthRefresh", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"CacheMsAccess", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"CacheMsProfileJson", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"CacheMsUuid", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"CacheMsName", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"CacheNideAccess", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"CacheNideClient", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"CacheNideUuid", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"CacheNideName", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"CacheNideUsername", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"CacheNidePass", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"CacheNideServer", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"CacheAuthAccess", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"CacheAuthClient", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"CacheAuthUuid", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"CacheAuthName", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"CacheAuthUsername", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"CacheAuthPass", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"CacheAuthServerServer", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"CacheAuthServerName", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"CacheAuthServerRegister", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"CacheDownloadFolder", New SetupEntry("", Source:=SetupSource.Registry)},
+        {"CacheJavaListVersion", New SetupEntry(0, Source:=SetupSource.Registry)},
+        {"LoginRemember", New SetupEntry(True, Source:=SetupSource.Registry, Encoded:=True)},
+        {"LoginLegacyName", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"LoginMsJson", New SetupEntry("{}", Source:=SetupSource.Registry, Encoded:=True)}, '{UserName: OAuthToken, ...}
+        {"LoginNideEmail", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"LoginNidePass", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"LoginAuthEmail", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"LoginAuthPass", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"LoginType", New SetupEntry(McLoginType.Legacy, Source:=SetupSource.Registry)},
+        {"LoginPageType", New SetupEntry(0)},
+        {"LaunchSkinID", New SetupEntry("", Source:=SetupSource.Registry)},
+        {"LaunchSkinType", New SetupEntry(0, Source:=SetupSource.Registry)},
+        {"LaunchSkinSlim", New SetupEntry(False, Source:=SetupSource.Registry)},
+        {"LaunchVersionSelect", New SetupEntry("")},
+        {"LaunchFolderSelect", New SetupEntry("")},
+        {"LaunchFolders", New SetupEntry("", Source:=SetupSource.Registry)},
+        {"LaunchArgumentTitle", New SetupEntry("")},
+        {"LaunchArgumentInfo", New SetupEntry("PCL")},
+        {"LaunchArgumentJavaSelect", New SetupEntry("", Source:=SetupSource.Registry)},
+        {"LaunchArgumentJavaAll", New SetupEntry("[]", Source:=SetupSource.Registry)},
+        {"LaunchArgumentIndie", New SetupEntry(0)},
+        {"LaunchArgumentVisible", New SetupEntry(5, Source:=SetupSource.Registry)},
+        {"LaunchArgumentPriority", New SetupEntry(1, Source:=SetupSource.Registry)},
+        {"LaunchArgumentWindowWidth", New SetupEntry(854)},
+        {"LaunchArgumentWindowHeight", New SetupEntry(480)},
+        {"LaunchArgumentWindowType", New SetupEntry(1)},
+        {"LaunchArgumentRam", New SetupEntry(False, Source:=SetupSource.Registry)},
+        {"LaunchAdvanceJvm", New SetupEntry("-XX:+UseG1GC -XX:-UseAdaptiveSizePolicy -XX:-OmitStackTraceInFastThrow -Dfml.ignoreInvalidMinecraftCertificates=True -Dfml.ignorePatchDiscrepancies=True -Dlog4j2.formatMsgNoLookups=true")},
+        {"LaunchAdvanceGame", New SetupEntry("")},
+        {"LaunchAdvanceRun", New SetupEntry("")},
+        {"LaunchAdvanceRunWait", New SetupEntry(True)},
+        {"LaunchAdvanceAssets", New SetupEntry(False)},
+        {"LaunchAdvanceJava", New SetupEntry(False)},
+        {"LaunchRamType", New SetupEntry(0)},
+        {"LaunchRamCustom", New SetupEntry(15)},
+        {"LinkEula", New SetupEntry(False, Source:=SetupSource.Registry)},
+        {"LinkName", New SetupEntry("", Source:=SetupSource.Registry)},
+        {"LinkHiperCertLast", New SetupEntry("", Source:=SetupSource.Registry)},
+        {"LinkHiperCertTime", New SetupEntry("", Source:=SetupSource.Registry)},
+        {"LinkHiperCertWarn", New SetupEntry(True, Source:=SetupSource.Registry)},
+        {"LinkIoiVersion", New SetupEntry(0, Source:=SetupSource.Registry, Encoded:=True)},
+        {"ToolHelpChinese", New SetupEntry(True, Source:=SetupSource.Registry)},
+        {"ToolDownloadThread", New SetupEntry(63, Source:=SetupSource.Registry)},
+        {"ToolDownloadSpeed", New SetupEntry(42, Source:=SetupSource.Registry)},
+        {"ToolDownloadVersion", New SetupEntry(0, Source:=SetupSource.Registry)},
+        {"ToolDownloadTranslate", New SetupEntry(0, Source:=SetupSource.Registry)},
+        {"ToolDownloadKeepModpack", New SetupEntry(False, Source:=SetupSource.Registry)},
+        {"ToolDownloadIgnoreQuilt", New SetupEntry(True, Source:=SetupSource.Registry)},
+        {"ToolDownloadCert", New SetupEntry(True, Source:=SetupSource.Registry)},
+        {"ToolUpdateAlpha", New SetupEntry(0, Source:=SetupSource.Registry, Encoded:=True)},
+        {"ToolUpdateRelease", New SetupEntry(False, Source:=SetupSource.Registry)},
+        {"ToolUpdateSnapshot", New SetupEntry(False, Source:=SetupSource.Registry)},
+        {"ToolUpdateReleaseLast", New SetupEntry("", Source:=SetupSource.Registry)},
+        {"ToolUpdateSnapshotLast", New SetupEntry("", Source:=SetupSource.Registry)},
+        {"UiLauncherTransparent", New SetupEntry(600)}, '避免与 PCL1 设置冲突(UiLauncherOpacity)
+        {"UiLauncherHue", New SetupEntry(180)},
+        {"UiLauncherSat", New SetupEntry(80)},
+        {"UiLauncherDelta", New SetupEntry(90)},
+        {"UiLauncherLight", New SetupEntry(20)},
+        {"UiLauncherTheme", New SetupEntry(0)},
+        {"UiLauncherThemeGold", New SetupEntry("", Source:=SetupSource.Registry, Encoded:=True)},
+        {"UiLauncherThemeHide", New SetupEntry("0|1|2|3|4", Source:=SetupSource.Registry, Encoded:=True)},
+        {"UiLauncherThemeHide2", New SetupEntry("0|1|2|3|4", Source:=SetupSource.Registry, Encoded:=True)},
+        {"UiLauncherLogo", New SetupEntry(True)},
+        {"UiLauncherEmail", New SetupEntry(False)},
+        {"UiBackgroundColorful", New SetupEntry(True)},
+        {"UiBackgroundOpacity", New SetupEntry(1000)},
+        {"UiBackgroundBlur", New SetupEntry(0)},
+        {"UiBackgroundSuit", New SetupEntry(0)},
+        {"UiCustomType", New SetupEntry(0)},
+        {"UiCustomPreset", New SetupEntry(0)},
+        {"UiCustomNet", New SetupEntry("")},
+        {"UiLogoType", New SetupEntry(1)},
+        {"UiLogoText", New SetupEntry("")},
+        {"UiLogoLeft", New SetupEntry(False)},
+        {"UiMusicVolume", New SetupEntry(500)},
+        {"UiMusicStop", New SetupEntry(False)},
+        {"UiMusicStart", New SetupEntry(False)},
+        {"UiMusicRandom", New SetupEntry(True)},
+        {"UiMusicAuto", New SetupEntry(True)},
+        {"UiHiddenPageDownload", New SetupEntry(False)},
+        {"UiHiddenPageLink", New SetupEntry(False)},
+        {"UiHiddenPageSetup", New SetupEntry(False)},
+        {"UiHiddenPageOther", New SetupEntry(False)},
+        {"UiHiddenFunctionSelect", New SetupEntry(False)},
+        {"UiHiddenFunctionHidden", New SetupEntry(False)},
+        {"UiHiddenSetupLaunch", New SetupEntry(False)},
+        {"UiHiddenSetupUi", New SetupEntry(False)},
+        {"UiHiddenSetupLink", New SetupEntry(False)},
+        {"UiHiddenSetupSystem", New SetupEntry(False)},
+        {"UiHiddenOtherHelp", New SetupEntry(False)},
+        {"UiHiddenOtherFeedback", New SetupEntry(False)},
+        {"UiHiddenOtherVote", New SetupEntry(False)},
+        {"UiHiddenOtherAbout", New SetupEntry(False)},
+        {"UiHiddenOtherTest", New SetupEntry(False)},
+        {"VersionAdvanceJvm", New SetupEntry("", Source:=SetupSource.Version)},
+        {"VersionAdvanceGame", New SetupEntry("", Source:=SetupSource.Version)},
+        {"VersionAdvanceAssets", New SetupEntry(0, Source:=SetupSource.Version)},
+        {"VersionAdvanceAssetsV2", New SetupEntry(False, Source:=SetupSource.Version)},
+        {"VersionAdvanceJava", New SetupEntry(False, Source:=SetupSource.Version)},
+        {"VersionAdvanceRun", New SetupEntry("", Source:=SetupSource.Version)},
+        {"VersionAdvanceRunWait", New SetupEntry(True, Source:=SetupSource.Version)},
+        {"VersionRamType", New SetupEntry(2, Source:=SetupSource.Version)},
+        {"VersionRamCustom", New SetupEntry(15, Source:=SetupSource.Version)},
+        {"VersionArgumentTitle", New SetupEntry("", Source:=SetupSource.Version)},
+        {"VersionArgumentInfo", New SetupEntry("", Source:=SetupSource.Version)},
+        {"VersionArgumentIndie", New SetupEntry(-1, Source:=SetupSource.Version)},
+        {"VersionArgumentJavaSelect", New SetupEntry("使用全局设置", Source:=SetupSource.Version)},
+        {"VersionServerEnter", New SetupEntry("", Source:=SetupSource.Version)},
+        {"VersionServerLogin", New SetupEntry(0, Source:=SetupSource.Version)},
+        {"VersionServerNide", New SetupEntry("", Source:=SetupSource.Version)},
+        {"VersionServerAuthRegister", New SetupEntry("", Source:=SetupSource.Version)},
+        {"VersionServerAuthName", New SetupEntry("", Source:=SetupSource.Version)},
+        {"VersionServerAuthServer", New SetupEntry("", Source:=SetupSource.Version)}
+    }
+
+#Region "基础"
+
+    Private Enum SetupSource
+        Normal
+        Registry
+        Version
+    End Enum
+    Private Class SetupEntry
+
+        Public Encoded As Boolean
+        Public DefaultValue
+        Public DefaultValueEncoded
+        Public Value
+        Public Source As SetupSource
+
+        '加载状态:0/未读取  1/已读取未处理  2/已处理
+        Public State As Byte = 0
+        Public Type
+
+        Public Sub New(Value, Optional Source = SetupSource.Normal, Optional Encoded = False)
+            Me.DefaultValue = Value
+            Me.DefaultValueEncoded = If(Encoded, SecretEncrypt(Value, "PCL" & UniqueAddress), Value)
+            Me.Encoded = Encoded
+            Me.Value = Value
+            Me.Source = Source
+            Type = If(Value, New Object).GetType
+        End Sub
+
+    End Class
+
+    ''' <summary>
+    ''' 改变某个设置项的值。
+    ''' </summary>
+    Public Sub [Set](Key As String, Value As Object, Optional ForceReload As Boolean = False, Optional Version As McVersion = Nothing)
+        [Set](Key, Value, SetupDict(Key), ForceReload, Version)
+    End Sub
+    Private Sub [Set](Key As String, Value As Object, E As SetupEntry, ForceReload As Boolean, Version As McVersion)
+        Try
+
+            Value = CTypeDynamic(Value, E.Type)
+            If E.State = 2 Then
+                '如果已应用,且值相同,则无需再次更改
+                If E.Value = Value AndAlso Not ForceReload Then Exit Sub
+            Else
+                '如果未应用,则直接更改并应用
+                If E.Source <> SetupSource.Version Then E.State = 2
+            End If
+            '设置新值
+            E.Value = Value
+            '写入值
+            If E.Encoded Then
+                Try
+                    If Value Is Nothing Then Value = ""
+                    Value = SecretEncrypt(Value, "PCL" & UniqueAddress)
+                Catch ex As Exception
+                    Log(ex, "加密设置失败:" & Key, LogLevel.Developer)
+                End Try
+            End If
+            Select Case E.Source
+                Case SetupSource.Normal
+                    WriteIni("Setup", Key, Value)
+                Case SetupSource.Registry
+                    WriteReg(Key, Value)
+                Case SetupSource.Version
+                    If Version Is Nothing Then
+                        Throw New Exception("更改版本设置 " & Key & " 时未提供目标版本")
+                    Else
+                        WriteIni(Version.Path & "PCL\Setup.ini", Key, Value)
+                    End If
+            End Select
+            '应用
+            '例如 VersionServerLogin 要求在设置之后再引发事件
+            Dim Method As Reflection.MethodInfo = GetType(ModSetup).GetMethod(Key)
+            If Method IsNot Nothing Then Method.Invoke(Me, {Value})
+
+        Catch ex As Exception
+            Log(ex, "设置设置项时出错(" & Key & ", " & Value & ")", LogLevel.Feedback)
+        End Try
+    End Sub
+
+    ''' <summary>
+    ''' 应用某个设置项的值。
+    ''' </summary>
+    Public Function Load(Key As String, Optional ForceReload As Boolean = False, Optional Version As McVersion = Nothing)
+        Return Load(Key, SetupDict(Key), ForceReload, Version)
+    End Function
+    Private Function Load(Key As String, E As SetupEntry, ForceReload As Boolean, Version As McVersion)
+        '如果已经应用过,则什么也不干
+        If E.State = 2 AndAlso Not ForceReload Then Return E.Value
+        '读取,应用并设置状态
+        Read(Key, E, Version)
+        If E.Source <> SetupSource.Version Then E.State = 2
+        Dim Method As Reflection.MethodInfo = GetType(ModSetup).GetMethod(Key)
+        If Method IsNot Nothing Then Method.Invoke(Me, {E.Value})
+        Return E.Value
+    End Function
+
+    ''' <summary>
+    ''' 获取某个设置项的值。
+    ''' </summary>
+    Public Function [Get](Key As String, Optional Version As McVersion = Nothing)
+        If Not SetupDict.ContainsKey(Key) Then Throw New KeyNotFoundException("未找到设置项:" & Key) With {.Source = Key}
+        Return [Get](Key, SetupDict(Key), Version)
+    End Function
+    Private Function [Get](Key As String, E As SetupEntry, Version As McVersion)
+        '获取强制值
+        Dim Force As String = ForceValue(Key)
+        If Force IsNot Nothing Then
+            E.Value = CTypeDynamic(Force, E.Type)
+            E.State = 1
+        End If
+        '如果尚未读取过,则读取
+        If E.State = 0 Then
+            Read(Key, E, Version)
+            If E.Source <> SetupSource.Version Then E.State = 1
+        End If
+        '返回现在的值
+        Return E.Value
+    End Function
+
+    ''' <summary>
+    ''' 初始化某个设置项的值。
+    ''' </summary>
+    Public Sub Reset(Key As String, Optional ForceReload As Boolean = False, Optional Version As McVersion = Nothing)
+        Dim E As SetupEntry = SetupDict(Key)
+        [Set](Key, E.DefaultValue, E, ForceReload, Version)
+    End Sub
+
+    Private Sub Read(Key As String, ByRef E As SetupEntry, Version As McVersion)
+        Try
+            If Not E.State = 0 Then Exit Sub
+            Dim SourceValue As String '先用 String 储存,避免类型转换
+            Select Case E.Source
+                Case SetupSource.Normal
+                    SourceValue = ReadIni("Setup", Key, E.DefaultValueEncoded)
+                Case SetupSource.Registry
+                    SourceValue = ReadReg(Key, E.DefaultValueEncoded)
+                Case Else 'SetupSource.Version
+                    If Version Is Nothing Then
+                        Throw New Exception("读取版本设置 " & Key & " 时未提供目标版本")
+                    Else
+                        SourceValue = ReadIni(Version.Path & "PCL\Setup.ini", Key, E.DefaultValueEncoded)
+                    End If
+            End Select
+            If E.Encoded Then
+                If SourceValue.Equals(E.DefaultValueEncoded) Then
+                    SourceValue = E.DefaultValue
+                Else
+                    Try
+                        SourceValue = SecretDecrypt(SourceValue, "PCL" & UniqueAddress)
+                    Catch ex As Exception
+                        Log(ex, "解密设置失败:" & Key, LogLevel.Developer)
+                        SourceValue = E.DefaultValue
+                        Setup.Set(Key, E.DefaultValue, True)
+                    End Try
+                End If
+            End If
+            E.Value = CTypeDynamic(SourceValue, E.Type)
+        Catch ex As Exception
+            Log(ex, "读取设置失败:" & Key, LogLevel.Hint)
+            E.Value = CTypeDynamic(E.DefaultValue, E.Type)
+        End Try
+    End Sub
+
+    '对部分设置强制赋值
+    Private Function ForceValue(Key As String) As String
+#If BETA Then
+        If Key = "UiLauncherTheme" Then Return "0"
+#End If
+        If Key = "UiHiddenPageLink" Then Return True
+        If Key = "UiHiddenSetupLink" Then Return True
+        Return Nothing
+    End Function
+
+#End Region
+
+#Region "Launch"
+
+    '切换选择
+    Public Sub LaunchVersionSelect(Value As String)
+        Log("[Setup] 当前选择的 Minecraft 版本:" & Value)
+        WriteIni(PathMcFolder & "PCL.ini", "Version", If(IsNothing(McVersionCurrent), "", McVersionCurrent.Name))
+    End Sub
+    Public Sub LaunchFolderSelect(Value As String)
+        Log("[Setup] 当前选择的 Minecraft 文件夹:" & Value.ToString.Replace("$", Path))
+        PathMcFolder = Value.ToString.Replace("$", Path)
+    End Sub
+
+    '游戏内存
+    Public Sub LaunchRamType(Type As Integer)
+        If FrmSetupLaunch Is Nothing Then Exit Sub
+        FrmSetupLaunch.RamType(Type)
+    End Sub
+
+    '离线皮肤
+    Public Sub LaunchSkinType(Value As Integer)
+        RunInUi(Sub()
+                    If Not IsNothing(FrmSetupLaunch) Then
+                        Select Case Value
+                            Case 0, 1, 2 '默认
+                                FrmSetupLaunch.PanSkinID.Visibility = Visibility.Collapsed
+                                FrmSetupLaunch.PanSkinChange.Visibility = Visibility.Collapsed
+                            Case 3 '正版
+                                FrmSetupLaunch.PanSkinID.Visibility = Visibility.Visible
+                                FrmSetupLaunch.PanSkinChange.Visibility = Visibility.Collapsed
+                            Case 4 '自定义
+                                FrmSetupLaunch.PanSkinID.Visibility = Visibility.Collapsed
+                                FrmSetupLaunch.PanSkinChange.Visibility = Visibility.Visible
+                        End Select
+                        FrmSetupLaunch.CardSkin.TriggerForceResize()
+                    End If
+                    PageLaunchLeft.SkinLegacy.Start()
+                End Sub)
+    End Sub
+    Public Sub LaunchSkinID(Value As String)
+        PageLaunchLeft.SkinLegacy.Start()
+    End Sub
+
+#End Region
+
+#Region "Tool"
+
+    Public Sub ToolDownloadThread(Value As Integer)
+        NetTaskThreadLimit = Value + 1
+    End Sub
+    Public Sub ToolDownloadCert(Value As Boolean)
+        Net.ServicePointManager.ServerCertificateValidationCallback =
+            If(Value, Nothing, New Net.Security.RemoteCertificateValidationCallback(Function() True))
+    End Sub
+    Public Sub ToolDownloadSpeed(Value As Integer)
+        If Value <= 14 Then
+            NetTaskSpeedLimitHigh = (Value + 1) * 0.1 * 1024 * 1024L
+        ElseIf Value <= 31 Then
+            NetTaskSpeedLimitHigh = (Value - 11) * 0.5 * 1024 * 1024L
+        ElseIf Value <= 41 Then
+            NetTaskSpeedLimitHigh = (Value - 21) * 1024 * 1024L
+        Else
+            NetTaskSpeedLimitHigh = -1
+        End If
+    End Sub
+
+#End Region
+
+#Region "UI"
+
+    '启动器
+    Public Sub UiLauncherTransparent(Value As Integer)
+        FrmMain.Opacity = Value / 1000 + 0.4
+    End Sub
+#If Not BETA Then
+    Public Sub UiLauncherTheme(Value As Integer)
+        ThemeRefresh(Value)
+    End Sub
+#End If
+    Public Sub UiBackgroundColorful(Value As Boolean)
+        ThemeRefresh()
+    End Sub
+
+    '背景图片
+    Public Sub UiBackgroundOpacity(Value As Integer)
+        FrmMain.ImgBack.Opacity = Value / 1000
+    End Sub
+    Public Sub UiBackgroundBlur(Value As Integer)
+        If Value = 0 Then
+            FrmMain.ImgBack.Effect = Nothing
+        Else
+            FrmMain.ImgBack.Effect = New Effects.BlurEffect With {.Radius = Value + 1}
+        End If
+        FrmMain.ImgBack.Margin = New Thickness(-(Value + 1) / 1.8)
+    End Sub
+    Public Sub UiBackgroundSuit(Value As Integer)
+        If IsNothing(FrmMain.ImgBack.Background) Then Exit Sub
+        Dim Width As Double = CType(FrmMain.ImgBack.Background, ImageBrush).ImageSource.Width
+        Dim Height As Double = CType(FrmMain.ImgBack.Background, ImageBrush).ImageSource.Height
+        If Value = 0 Then
+            '智能:当图片较小时平铺,较大时适应
+            If Width < FrmMain.PanMain.ActualWidth / 2 AndAlso Height < FrmMain.PanMain.ActualHeight / 2 Then
+                Value = 4 '平铺
+            Else
+                Value = 2 '适应
+            End If
+        End If
+        CType(FrmMain.ImgBack.Background, ImageBrush).TileMode = TileMode.None
+        CType(FrmMain.ImgBack.Background, ImageBrush).Viewport = New Rect(0, 0, 1, 1)
+        CType(FrmMain.ImgBack.Background, ImageBrush).ViewportUnits = BrushMappingMode.RelativeToBoundingBox
+        Select Case Value
+            Case 1 '居中
+                FrmMain.ImgBack.HorizontalAlignment = HorizontalAlignment.Center
+                FrmMain.ImgBack.VerticalAlignment = VerticalAlignment.Center
+                CType(FrmMain.ImgBack.Background, ImageBrush).Stretch = Stretch.None
+                FrmMain.ImgBack.Width = CType(FrmMain.ImgBack.Background, ImageBrush).ImageSource.Width
+                FrmMain.ImgBack.Height = CType(FrmMain.ImgBack.Background, ImageBrush).ImageSource.Height
+            Case 2 '适应
+                FrmMain.ImgBack.HorizontalAlignment = HorizontalAlignment.Stretch
+                FrmMain.ImgBack.VerticalAlignment = VerticalAlignment.Stretch
+                CType(FrmMain.ImgBack.Background, ImageBrush).Stretch = Stretch.UniformToFill
+                FrmMain.ImgBack.Width = Double.NaN
+                FrmMain.ImgBack.Height = Double.NaN
+            Case 3 '拉伸
+                FrmMain.ImgBack.HorizontalAlignment = HorizontalAlignment.Stretch
+                FrmMain.ImgBack.VerticalAlignment = VerticalAlignment.Stretch
+                CType(FrmMain.ImgBack.Background, ImageBrush).Stretch = Stretch.Fill
+                FrmMain.ImgBack.Width = Double.NaN
+                FrmMain.ImgBack.Height = Double.NaN
+            Case 4 '平铺
+                FrmMain.ImgBack.HorizontalAlignment = HorizontalAlignment.Stretch
+                FrmMain.ImgBack.VerticalAlignment = VerticalAlignment.Stretch
+                CType(FrmMain.ImgBack.Background, ImageBrush).Stretch = Stretch.None
+                CType(FrmMain.ImgBack.Background, ImageBrush).TileMode = TileMode.Tile
+                CType(FrmMain.ImgBack.Background, ImageBrush).Viewport = New Rect(0, 0, CType(FrmMain.ImgBack.Background, ImageBrush).ImageSource.Width, CType(FrmMain.ImgBack.Background, ImageBrush).ImageSource.Height)
+                CType(FrmMain.ImgBack.Background, ImageBrush).ViewportUnits = BrushMappingMode.Absolute
+                FrmMain.ImgBack.Width = Double.NaN
+                FrmMain.ImgBack.Height = Double.NaN
+            Case 5 '左上
+                FrmMain.ImgBack.HorizontalAlignment = HorizontalAlignment.Left
+                FrmMain.ImgBack.VerticalAlignment = VerticalAlignment.Top
+                CType(FrmMain.ImgBack.Background, ImageBrush).Stretch = Stretch.None
+                FrmMain.ImgBack.Width = CType(FrmMain.ImgBack.Background, ImageBrush).ImageSource.Width
+                FrmMain.ImgBack.Height = CType(FrmMain.ImgBack.Background, ImageBrush).ImageSource.Height
+            Case 6 '右上
+                FrmMain.ImgBack.HorizontalAlignment = HorizontalAlignment.Right
+                FrmMain.ImgBack.VerticalAlignment = VerticalAlignment.Top
+                CType(FrmMain.ImgBack.Background, ImageBrush).Stretch = Stretch.None
+                FrmMain.ImgBack.Width = CType(FrmMain.ImgBack.Background, ImageBrush).ImageSource.Width
+                FrmMain.ImgBack.Height = CType(FrmMain.ImgBack.Background, ImageBrush).ImageSource.Height
+            Case 7 '左下
+                FrmMain.ImgBack.HorizontalAlignment = HorizontalAlignment.Left
+                FrmMain.ImgBack.VerticalAlignment = VerticalAlignment.Bottom
+                CType(FrmMain.ImgBack.Background, ImageBrush).Stretch = Stretch.None
+                FrmMain.ImgBack.Width = CType(FrmMain.ImgBack.Background, ImageBrush).ImageSource.Width
+                FrmMain.ImgBack.Height = CType(FrmMain.ImgBack.Background, ImageBrush).ImageSource.Height
+            Case 8 '右下
+                FrmMain.ImgBack.HorizontalAlignment = HorizontalAlignment.Right
+                FrmMain.ImgBack.VerticalAlignment = VerticalAlignment.Bottom
+                CType(FrmMain.ImgBack.Background, ImageBrush).Stretch = Stretch.None
+                FrmMain.ImgBack.Width = CType(FrmMain.ImgBack.Background, ImageBrush).ImageSource.Width
+                FrmMain.ImgBack.Height = CType(FrmMain.ImgBack.Background, ImageBrush).ImageSource.Height
+        End Select
+    End Sub
+
+    '主页
+    Public Sub UiCustomType(Value As Integer)
+        If FrmSetupUI Is Nothing Then Exit Sub
+        Select Case Value
+            Case 0 '无
+                FrmSetupUI.PanCustomPreset.Visibility = Visibility.Collapsed
+                FrmSetupUI.PanCustomLocal.Visibility = Visibility.Collapsed
+                FrmSetupUI.PanCustomNet.Visibility = Visibility.Collapsed
+                FrmSetupUI.HintCustom.Visibility = Visibility.Collapsed
+            Case 1 '本地
+                FrmSetupUI.PanCustomPreset.Visibility = Visibility.Collapsed
+                FrmSetupUI.PanCustomLocal.Visibility = Visibility.Visible
+                FrmSetupUI.PanCustomNet.Visibility = Visibility.Collapsed
+                FrmSetupUI.HintCustom.Visibility = Visibility.Visible
+                FrmSetupUI.HintCustom.Text = $"从 PCL 文件夹下的 Custom.xaml 读取主页内容。{vbCrLf}你可以手动编辑该文件,向主页添加文本、图片、常用网站、快捷启动等功能。"
+                FrmSetupUI.HintCustom.EventType = ""
+                FrmSetupUI.HintCustom.EventData = ""
+            Case 2 '联网
+                FrmSetupUI.PanCustomPreset.Visibility = Visibility.Collapsed
+                FrmSetupUI.PanCustomLocal.Visibility = Visibility.Collapsed
+                FrmSetupUI.PanCustomNet.Visibility = Visibility.Visible
+                FrmSetupUI.HintCustom.Visibility = Visibility.Visible
+                FrmSetupUI.HintCustom.Text = $"从指定网址联网获取主页内容。服主也可以用于动态更新服务器公告。{vbCrLf}如果你制作了稳定运行的联网主页,可以点击这条提示投稿,若合格即可加入预设!"
+                FrmSetupUI.HintCustom.EventType = "打开网页"
+                FrmSetupUI.HintCustom.EventData = "https://github.com/Hex-Dragon/PCL2/discussions/2528"
+            Case 3 '预设
+                FrmSetupUI.PanCustomPreset.Visibility = Visibility.Visible
+                FrmSetupUI.PanCustomLocal.Visibility = Visibility.Collapsed
+                FrmSetupUI.PanCustomNet.Visibility = Visibility.Collapsed
+                FrmSetupUI.HintCustom.Visibility = Visibility.Collapsed
+        End Select
+        FrmSetupUI.CardCustom.TriggerForceResize()
+    End Sub
+
+    '顶部栏
+    Public Sub UiLogoType(Value As Integer)
+        Select Case Value
+            Case 0 '无
+                FrmMain.ShapeTitleLogo.Visibility = Visibility.Collapsed
+                FrmMain.LabTitleLogo.Visibility = Visibility.Collapsed
+                FrmMain.ImageTitleLogo.Visibility = Visibility.Collapsed
+                If Not IsNothing(FrmSetupUI) Then
+                    FrmSetupUI.CheckLogoLeft.Visibility = Visibility.Visible
+                    FrmSetupUI.PanLogoText.Visibility = Visibility.Collapsed
+                    FrmSetupUI.PanLogoChange.Visibility = Visibility.Collapsed
+                End If
+            Case 1 '默认
+                FrmMain.ShapeTitleLogo.Visibility = Visibility.Visible
+                FrmMain.LabTitleLogo.Visibility = Visibility.Collapsed
+                FrmMain.ImageTitleLogo.Visibility = Visibility.Collapsed
+                If Not IsNothing(FrmSetupUI) Then
+                    FrmSetupUI.CheckLogoLeft.Visibility = Visibility.Collapsed
+                    FrmSetupUI.PanLogoText.Visibility = Visibility.Collapsed
+                    FrmSetupUI.PanLogoChange.Visibility = Visibility.Collapsed
+                End If
+            Case 2 '文本
+                FrmMain.ShapeTitleLogo.Visibility = Visibility.Collapsed
+                FrmMain.LabTitleLogo.Visibility = Visibility.Visible
+                FrmMain.ImageTitleLogo.Visibility = Visibility.Collapsed
+                If Not IsNothing(FrmSetupUI) Then
+                    FrmSetupUI.CheckLogoLeft.Visibility = Visibility.Collapsed
+                    FrmSetupUI.PanLogoText.Visibility = Visibility.Visible
+                    FrmSetupUI.PanLogoChange.Visibility = Visibility.Collapsed
+                End If
+                Setup.Load("UiLogoText", True)
+            Case 3 '图片
+                FrmMain.ShapeTitleLogo.Visibility = Visibility.Collapsed
+                FrmMain.LabTitleLogo.Visibility = Visibility.Collapsed
+                FrmMain.ImageTitleLogo.Visibility = Visibility.Visible
+                If Not IsNothing(FrmSetupUI) Then
+                    FrmSetupUI.CheckLogoLeft.Visibility = Visibility.Collapsed
+                    FrmSetupUI.PanLogoText.Visibility = Visibility.Collapsed
+                    FrmSetupUI.PanLogoChange.Visibility = Visibility.Visible
+                End If
+                Try
+                    FrmMain.ImageTitleLogo.Source = New MyBitmap(Path & "PCL\Logo.png")
+                Catch ex As Exception
+                    FrmMain.ImageTitleLogo.Source = Nothing
+                    Log(ex, "显示标题栏图片失败", LogLevel.Msgbox)
+                End Try
+        End Select
+        Setup.Load("UiLogoLeft", True)
+        If Not IsNothing(FrmSetupUI) Then FrmSetupUI.CardLogo.TriggerForceResize()
+    End Sub
+    Public Sub UiLogoText(Value As String)
+        FrmMain.LabTitleLogo.Text = Value
+    End Sub
+    Public Sub UiLogoLeft(Value As Boolean)
+        FrmMain.PanTitleMain.ColumnDefinitions(0).Width = New GridLength(If(Value AndAlso (Setup.Get("UiLogoType") = 0), 0, 1), GridUnitType.Star)
+    End Sub
+
+    '功能隐藏
+    Public Sub UiHiddenPageLink(Value As Boolean)
+        PageSetupUI.HiddenRefresh()
+    End Sub
+    Public Sub UiHiddenPageDownload(Value As Boolean)
+        PageSetupUI.HiddenRefresh()
+    End Sub
+    Public Sub UiHiddenPageSetup(Value As Boolean)
+        PageSetupUI.HiddenRefresh()
+    End Sub
+    Public Sub UiHiddenPageOther(Value As Boolean)
+        PageSetupUI.HiddenRefresh()
+    End Sub
+    Public Sub UiHiddenFunctionSelect(Value As Boolean)
+        PageSetupUI.HiddenRefresh()
+    End Sub
+    Public Sub UiHiddenFunctionHidden(Value As Boolean)
+        PageSetupUI.HiddenRefresh()
+    End Sub
+    Public Sub UiHiddenSetupLaunch(Value As Boolean)
+        PageSetupUI.HiddenRefresh()
+    End Sub
+    Public Sub UiHiddenSetupUi(Value As Boolean)
+        PageSetupUI.HiddenRefresh()
+    End Sub
+    Public Sub UiHiddenSetupLink(Value As Boolean)
+        PageSetupUI.HiddenRefresh()
+    End Sub
+    Public Sub UiHiddenSetupSystem(Value As Boolean)
+        PageSetupUI.HiddenRefresh()
+    End Sub
+    Public Sub UiHiddenOtherHelp(Value As Boolean)
+        PageSetupUI.HiddenRefresh()
+    End Sub
+    Public Sub UiHiddenOtherFeedback(Value As Boolean)
+        PageSetupUI.HiddenRefresh()
+    End Sub
+    Public Sub UiHiddenOtherVote(Value As Boolean)
+        PageSetupUI.HiddenRefresh()
+    End Sub
+    Public Sub UiHiddenOtherAbout(Value As Boolean)
+        PageSetupUI.HiddenRefresh()
+    End Sub
+    Public Sub UiHiddenOtherTest(Value As Boolean)
+        PageSetupUI.HiddenRefresh()
+    End Sub
+
+
+#End Region
+
+#Region "System"
+
+    '调试选项
+    Public Sub SystemDebugMode(Value As Boolean)
+        ModeDebug = Value
+    End Sub
+    Public Sub SystemDebugAnim(Value As Integer)
+        AniSpeed = If(Value >= 30, 200, MathClamp(Value * 0.1 + 0.1, 0.1, 3))
+    End Sub
+
+#End Region
+
+#Region "Version"
+
+    '游戏内存
+    Public Sub VersionRamType(Type As Integer)
+        If FrmVersionSetup Is Nothing Then Exit Sub
+        FrmVersionSetup.RamType(Type)
+    End Sub
+
+    '服务器
+    Public Sub VersionServerLogin(Type As Integer)
+        If FrmVersionSetup Is Nothing Then Exit Sub
+        '为第三方登录清空缓存以更新描述
+        WriteIni(PathMcFolder & "PCL.ini", "VersionCache", "")
+        If PageVersionLeft.Version Is Nothing Then Exit Sub
+        PageVersionLeft.Version = New McVersion(PageVersionLeft.Version.Name).Load()
+        LoaderFolderRun(McVersionListLoader, PathMcFolder, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
+    End Sub
+
+#End Region
+
+End Class

+ 12 - 8
Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml

@@ -81,7 +81,7 @@
                         <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:MyTextBox x:Name="TextArgumentTitle" Grid.Row="0" Grid.ColumnSpan="2" Tag="LaunchArgumentTitle" Grid.Column="1" ToolTip="自定义游戏窗口的标题,若留空则不进行更改。&#xa;支持以下替换标记:&#xa; · {user}:玩家名字&#xa; · {login}:玩家的登录方式&#xa; · {name}:游戏版本名&#xa; · {date}、{time}:当前的系统时间&#xa; · {version}:游戏对应的原版版本号" HintText="默认" MaxLength="100" />
                         <local:MyTextBox x:Name="TextArgumentTitle" Grid.Row="0" Grid.ColumnSpan="2" Tag="LaunchArgumentTitle" Grid.Column="1" ToolTip="自定义游戏窗口的标题,若留空则不进行更改。&#xa;支持以下替换标记:&#xa; · {user}:玩家名字&#xa; · {login}:玩家的登录方式&#xa; · {name}:游戏版本名&#xa; · {date}、{time}:当前的系统时间&#xa; · {version}:游戏对应的原版版本号" HintText="默认" MaxLength="100" />
                         <TextBlock Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Left" Text="自定义信息" Margin="0,0,25,0" />
                         <TextBlock Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Left" Text="自定义信息" Margin="0,0,25,0" />
-                        <local:MyTextBox x:Name="TextArgumentInfo" Grid.Row="2" Grid.ColumnSpan="2" Tag="LaunchArgumentInfo" Text="Plain Craft Launcher 2" Grid.Column="1" ToolTip="该信息会显示在游戏主界面的左下角,与 F3 调试页面的左上角">
+                        <local:MyTextBox x:Name="TextArgumentInfo" Grid.Row="2" Grid.ColumnSpan="2" Tag="LaunchArgumentInfo" Text="PCL" Grid.Column="1" ToolTip="该信息会显示在游戏主界面的左下角,与 F3 调试页面的左上角">
                             <local:MyTextBox.ValidateRules>
                             <local:MyTextBox.ValidateRules>
                                 <local:ValidateExcept>
                                 <local:ValidateExcept>
                                     <local:ValidateExcept.Excepts>
                                     <local:ValidateExcept.Excepts>
@@ -115,7 +115,7 @@
                             <local:MyComboBoxItem Content="中(平衡)" IsSelected="True" />
                             <local:MyComboBoxItem Content="中(平衡)" IsSelected="True" />
                             <local:MyComboBoxItem Content="低(优先保证其他程序运行,但可能让游戏卡顿,适合挂机时使用)" />
                             <local:MyComboBoxItem Content="低(优先保证其他程序运行,但可能让游戏卡顿,适合挂机时使用)" />
                         </local:MyComboBox>
                         </local:MyComboBox>
-                        <TextBlock VerticalAlignment="Center" Grid.Row="10" HorizontalAlignment="Left" Text="游戏窗口" Margin="0,0,25,0" />
+                        <TextBlock VerticalAlignment="Center" Grid.Row="10" HorizontalAlignment="Left" Text="窗口大小" Margin="0,0,25,0" />
                         <Grid x:Name="PanArgumentWindow" Grid.Row="10" Grid.ColumnSpan="2" Grid.Column="1">
                         <Grid x:Name="PanArgumentWindow" Grid.Row="10" Grid.ColumnSpan="2" Grid.Column="1">
                             <Grid.ColumnDefinitions>
                             <Grid.ColumnDefinitions>
                                 <ColumnDefinition />
                                 <ColumnDefinition />
@@ -125,11 +125,10 @@
                             </Grid.ColumnDefinitions>
                             </Grid.ColumnDefinitions>
                             <local:MyComboBox x:Name="ComboArgumentWindowType" Tag="LaunchArgumentWindowType">
                             <local:MyComboBox x:Name="ComboArgumentWindowType" Tag="LaunchArgumentWindowType">
                                 <local:MyComboBoxItem Content="全屏" />
                                 <local:MyComboBoxItem Content="全屏" />
-                                <local:MyComboBoxItem Content="默认大小" IsSelected="True" ToolTip="以 854×480 的默认窗口大小启动 Minecraft" />
-                                <local:MyComboBoxItem Content="与启动器窗口大小一致" />
-                                <local:MyComboBoxItem Content="自定义大小" ToolTip="注意:Forge 1.12 存在 Bug,会导致高度设定无效" />
+                                <local:MyComboBoxItem Content="默认" IsSelected="True" />
+                                <local:MyComboBoxItem Content="与启动器尺寸一致" />
+                                <local:MyComboBoxItem Content="自定义尺寸" ToolTip="Forge 1.12.2 等个别版本存在 Bug,会导致高度设定无效" />
                                 <local:MyComboBoxItem Content="最大化" />
                                 <local:MyComboBoxItem Content="最大化" />
-                                <local:MyComboBoxItem Content="保持游戏默认" />
                             </local:MyComboBox>
                             </local:MyComboBox>
                             <local:MyTextBox Padding="0,0,5,0" Visibility="Collapsed" Grid.Column="1" Margin="50,0,0,0" x:Name="TextArgumentWindowWidth" Tag="LaunchArgumentWindowWidth" Width="60" HorizontalContentAlignment="Center" Text="854">
                             <local:MyTextBox Padding="0,0,5,0" Visibility="Collapsed" Grid.Column="1" Margin="50,0,0,0" x:Name="TextArgumentWindowWidth" Tag="LaunchArgumentWindowWidth" Width="60" HorizontalContentAlignment="Center" Text="854">
                                 <local:MyTextBox.ValidateRules>
                                 <local:MyTextBox.ValidateRules>
@@ -138,7 +137,7 @@
                             </local:MyTextBox>
                             </local:MyTextBox>
                             <TextBlock x:Name="LabArgumentWindowMiddle" Visibility="Collapsed" Grid.Column="2" Text=" × " VerticalAlignment="Center" FontSize="18" />
                             <TextBlock x:Name="LabArgumentWindowMiddle" Visibility="Collapsed" Grid.Column="2" Text=" × " VerticalAlignment="Center" FontSize="18" />
                             <local:MyTextBox Padding="0,0,5,0" Visibility="Collapsed" Grid.Column="3" Width="60" HorizontalContentAlignment="Center" x:Name="TextArgumentWindowHeight" Tag="LaunchArgumentWindowHeight" Text="480"
                             <local:MyTextBox Padding="0,0,5,0" Visibility="Collapsed" Grid.Column="3" Width="60" HorizontalContentAlignment="Center" x:Name="TextArgumentWindowHeight" Tag="LaunchArgumentWindowHeight" Text="480"
-                                              ToolTip="注意:Forge 1.12.2 等个别版本存在 Bug,会导致高度设定无效" ToolTipService.Placement="Right" ToolTipService.HorizontalOffset="5">
+                                              ToolTip="Forge 1.12.2 等个别版本存在 Bug,会导致高度设定无效" ToolTipService.Placement="Right" ToolTipService.HorizontalOffset="5">
                                 <local:MyTextBox.ValidateRules>
                                 <local:MyTextBox.ValidateRules>
                                     <local:ValidateInteger Min="1" Max="9999" />
                                     <local:ValidateInteger Min="1" Max="9999" />
                                 </local:MyTextBox.ValidateRules>
                                 </local:MyTextBox.ValidateRules>
@@ -175,11 +174,16 @@
                             <RowDefinition Height="Auto" />
                             <RowDefinition Height="Auto" />
                             <RowDefinition Height="9" />
                             <RowDefinition Height="9" />
                             <RowDefinition Height="Auto" />
                             <RowDefinition Height="Auto" />
+                            <RowDefinition Height="9" />
+                            <RowDefinition Height="Auto" />
                         </Grid.RowDefinitions>
                         </Grid.RowDefinitions>
                         <local:MyRadioBox Text="自动配置" Width="110" Checked="True" x:Name="RadioRamType0" Tag="LaunchRamType/0" Margin="0,0,20,0" 
                         <local:MyRadioBox Text="自动配置" Width="110" Checked="True" x:Name="RadioRamType0" Tag="LaunchRamType/0" Margin="0,0,20,0" 
                                           ToolTip="根据安装的 Mod 量与电脑剩余内存,动态调整为游戏分配的内存" ToolTipService.Placement="Right" ToolTipService.HorizontalOffset="-10" ToolTipService.VerticalOffset="-3" />
                                           ToolTip="根据安装的 Mod 量与电脑剩余内存,动态调整为游戏分配的内存" ToolTipService.Placement="Right" ToolTipService.HorizontalOffset="-10" ToolTipService.VerticalOffset="-3" />
                         <local:MyRadioBox Grid.Row="2" Text="自定义" x:Name="RadioRamType1" Tag="LaunchRamType/1" Margin="0,0,20,0" />
                         <local:MyRadioBox Grid.Row="2" Text="自定义" x:Name="RadioRamType1" Tag="LaunchRamType/1" Margin="0,0,20,0" />
                         <local:MySlider Grid.Row="2" Grid.Column="1" IsEnabled="False" x:Name="SliderRamCustom" Tag="LaunchRamCustom" MaxValue="49" Value="13" />
                         <local:MySlider Grid.Row="2" Grid.Column="1" IsEnabled="False" x:Name="SliderRamCustom" Tag="LaunchRamCustom" MaxValue="49" Value="13" />
+                        <local:MyCheckBox Grid.Row="4" Grid.ColumnSpan="3" Text="启动游戏前进行内存优化" Height="22" x:Name="CheckArgumentRam" Tag="LaunchArgumentRam"
+                                          ToolTipService.Placement="Left" ToolTipService.HorizontalOffset="525" ToolTipService.VerticalOffset="-30"
+                                          ToolTip="内存优化能将当前物理内存占用降低约 1/3,不仅限于 MC!&#xa;如果使用机械硬盘,这还可能会导致一小段时间的严重卡顿。&#xa;使用 --memory 参数启动 PCL 可以静默执行内存优化。" />
                         <!--<local:MyTextBox Grid.Row="2" Grid.Column="3" Width="100" VerticalAlignment="Center" Margin="30,0,0,0" Text="2048 MB" />-->
                         <!--<local:MyTextBox Grid.Row="2" Grid.Column="3" Width="100" VerticalAlignment="Center" Margin="30,0,0,0" Text="2048 MB" />-->
                     </Grid>
                     </Grid>
                     <Grid x:Name="PanRamDisplay" Margin="0,11,2,0" SnapsToDevicePixels="True" Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="1">
                     <Grid x:Name="PanRamDisplay" Margin="0,11,2,0" SnapsToDevicePixels="True" Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="1">
@@ -229,7 +233,7 @@
                         <RowDefinition Height="Auto" />
                         <RowDefinition Height="Auto" />
                     </Grid.RowDefinitions>
                     </Grid.RowDefinitions>
                     <TextBlock Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Left" Text="JVM 参数头" Margin="0,0,25,0" />
                     <TextBlock Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Left" Text="JVM 参数头" Margin="0,0,25,0" />
-                    <local:MyTextBox x:Name="TextAdvanceJvm" Grid.Row="0" Grid.ColumnSpan="2" Tag="LaunchAdvanceJvm" Grid.Column="1" ToolTip="启动 Minecraft 时使用的额外 JVM 参数,在没有确定把握的情况下请不要尝试修改。&#xa;你可以通过点击左侧选项卡的初始化按钮来恢复到默认设置。" MaxLength="4000" Text="-XX:+UseG1GC -XX:-UseAdaptiveSizePolicy -XX:-OmitStackTraceInFastThrow -Dfml.ignoreInvalidMinecraftCertificates=True -Dfml.ignorePatchDiscrepancies=True -Dlog4j2.formatMsgNoLookups=true" 
+                    <local:MyTextBox x:Name="TextAdvanceJvm" Grid.Row="0" Grid.ColumnSpan="2" Tag="LaunchAdvanceJvm" Grid.Column="1" ToolTip="启动 Minecraft 时使用的额外 JVM 参数,在没有确定把握的情况下请不要尝试修改。&#xa;你可以通过点击左侧选项卡的初始化按钮来恢复到默认设置。" MaxLength="4000"
                                      Height="57" AcceptsReturn="True" TextWrapping="Wrap" VerticalContentAlignment="Top" Padding="0,5" />
                                      Height="57" AcceptsReturn="True" TextWrapping="Wrap" VerticalContentAlignment="Top" Padding="0,5" />
                     <TextBlock Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Left" Text="JVM 参数尾" Margin="0,0,25,0" />
                     <TextBlock Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Left" Text="JVM 参数尾" Margin="0,0,25,0" />
                     <local:MyTextBox x:Name="TextAdvanceGame" Grid.Row="2" Grid.ColumnSpan="2" Tag="LaunchAdvanceGame" Grid.Column="1" ToolTip="文本框中的内容将会被直接拼合在启动参数的末尾。&#xa;例如输入 --demo 则会以试玩模式启动游戏。" />
                     <local:MyTextBox x:Name="TextAdvanceGame" Grid.Row="2" Grid.ColumnSpan="2" Tag="LaunchAdvanceGame" Grid.Column="1" ToolTip="文本框中的内容将会被直接拼合在启动参数的末尾。&#xa;例如输入 --demo 则会以试玩模式启动游戏。" />

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

@@ -38,6 +38,7 @@
             ComboArgumentWindowType.SelectedIndex = Setup.Get("LaunchArgumentWindowType")
             ComboArgumentWindowType.SelectedIndex = Setup.Get("LaunchArgumentWindowType")
             TextArgumentWindowWidth.Text = Setup.Get("LaunchArgumentWindowWidth")
             TextArgumentWindowWidth.Text = Setup.Get("LaunchArgumentWindowWidth")
             TextArgumentWindowHeight.Text = Setup.Get("LaunchArgumentWindowHeight")
             TextArgumentWindowHeight.Text = Setup.Get("LaunchArgumentWindowHeight")
+            CheckArgumentRam.Checked = Setup.Get("LaunchArgumentRam")
             RefreshJavaComboBox()
             RefreshJavaComboBox()
 
 
             '游戏内存
             '游戏内存
@@ -71,6 +72,7 @@
             Setup.Reset("LaunchArgumentWindowWidth")
             Setup.Reset("LaunchArgumentWindowWidth")
             Setup.Reset("LaunchArgumentWindowHeight")
             Setup.Reset("LaunchArgumentWindowHeight")
             Setup.Reset("LaunchArgumentPriority")
             Setup.Reset("LaunchArgumentPriority")
+            Setup.Reset("LaunchArgumentRam")
             Setup.Reset("LaunchRamType")
             Setup.Reset("LaunchRamType")
             Setup.Reset("LaunchRamCustom")
             Setup.Reset("LaunchRamCustom")
             Setup.Reset("LaunchSkinType")
             Setup.Reset("LaunchSkinType")
@@ -108,7 +110,7 @@
     Private Shared Sub ComboChange(sender As MyComboBox, e As Object) Handles ComboArgumentIndie.SelectionChanged, ComboArgumentVisibie.SelectionChanged, ComboArgumentWindowType.SelectionChanged, ComboArgumentPriority.SelectionChanged
     Private Shared Sub ComboChange(sender As MyComboBox, e As Object) Handles ComboArgumentIndie.SelectionChanged, ComboArgumentVisibie.SelectionChanged, ComboArgumentWindowType.SelectionChanged, ComboArgumentPriority.SelectionChanged
         If AniControlEnabled = 0 Then Setup.Set(sender.Tag, sender.SelectedIndex)
         If AniControlEnabled = 0 Then Setup.Set(sender.Tag, sender.SelectedIndex)
     End Sub
     End Sub
-    Private Shared Sub CheckBoxChange(sender As MyCheckBox, e As Object) Handles CheckAdvanceAssets.Change, CheckAdvanceJava.Change, CheckAdvanceRunWait.Change
+    Private Shared Sub CheckBoxChange(sender As MyCheckBox, e As Object) Handles CheckAdvanceAssets.Change, CheckAdvanceJava.Change, CheckAdvanceRunWait.Change, CheckArgumentRam.Change
         If AniControlEnabled = 0 Then Setup.Set(sender.Tag, sender.Checked)
         If AniControlEnabled = 0 Then Setup.Set(sender.Tag, sender.Checked)
     End Sub
     End Sub
 
 
@@ -352,19 +354,19 @@
                 '可安装 Mod 的版本
                 '可安装 Mod 的版本
                 Dim ModDir As New DirectoryInfo(Version.PathIndie & "mods\")
                 Dim ModDir As New DirectoryInfo(Version.PathIndie & "mods\")
                 Dim ModCount As Integer = If(ModDir.Exists, ModDir.GetFiles.Length, 0)
                 Dim ModCount As Integer = If(ModDir.Exists, ModDir.GetFiles.Length, 0)
-                RamMininum = 0.4 + ModCount / 150
-                RamTarget1 = 1.5 + ModCount / 100
-                RamTarget2 = 3 + ModCount / 60
-                RamTarget3 = 6 + ModCount / 30
+                RamMininum = 0.5 + ModCount / 150
+                RamTarget1 = 1.5 + ModCount / 90
+                RamTarget2 = 2.7 + ModCount / 50
+                RamTarget3 = 4.5 + ModCount / 25
             ElseIf Version IsNot Nothing AndAlso Version.Version.HasOptiFine Then
             ElseIf Version IsNot Nothing AndAlso Version.Version.HasOptiFine Then
                 'OptiFine 版本
                 'OptiFine 版本
-                RamMininum = 0.3
+                RamMininum = 0.5
                 RamTarget1 = 1.5
                 RamTarget1 = 1.5
                 RamTarget2 = 3
                 RamTarget2 = 3
-                RamTarget3 = 6
+                RamTarget3 = 5
             Else
             Else
                 '普通版本
                 '普通版本
-                RamMininum = 0.3
+                RamMininum = 0.5
                 RamTarget1 = 1.5
                 RamTarget1 = 1.5
                 RamTarget2 = 2.5
                 RamTarget2 = 2.5
                 RamTarget3 = 4
                 RamTarget3 = 4
@@ -372,27 +374,23 @@
             Dim RamDelta As Double
             Dim RamDelta As Double
             '预分配内存,阶段一,0 ~ T1,100%
             '预分配内存,阶段一,0 ~ T1,100%
             RamDelta = RamTarget1
             RamDelta = RamTarget1
-            RamAvailable = Math.Max(0, RamAvailable - 0.1)
-            RamGive += Math.Min(RamAvailable * 1, RamDelta)
-            RamAvailable -= RamDelta / 1 + 0.1
+            RamGive += Math.Min(RamAvailable, RamDelta)
+            RamAvailable -= RamDelta
             If RamAvailable < 0.1 Then GoTo PreFin
             If RamAvailable < 0.1 Then GoTo PreFin
-            '预分配内存,阶段二,T1 ~ T2,80%
+            '预分配内存,阶段二,T1 ~ T2,70%
             RamDelta = RamTarget2 - RamTarget1
             RamDelta = RamTarget2 - RamTarget1
-            RamAvailable = Math.Max(0, RamAvailable - 0.1)
-            RamGive += Math.Min(RamAvailable * 0.8, RamDelta)
-            RamAvailable -= RamDelta / 0.8 + 0.1
+            RamGive += Math.Min(RamAvailable * 0.7, RamDelta)
+            RamAvailable -= RamDelta / 0.7
             If RamAvailable < 0.1 Then GoTo PreFin
             If RamAvailable < 0.1 Then GoTo PreFin
-            '预分配内存,阶段三,T2 ~ T3,60%
+            '预分配内存,阶段三,T2 ~ T3,40%
             RamDelta = RamTarget3 - RamTarget2
             RamDelta = RamTarget3 - RamTarget2
-            RamAvailable = Math.Max(0, RamAvailable - 0.2)
-            RamGive += Math.Min(RamAvailable * 0.6, RamDelta)
-            RamAvailable -= RamDelta / 0.6 + 0.2
+            RamGive += Math.Min(RamAvailable * 0.4, RamDelta)
+            RamAvailable -= RamDelta / 0.4
             If RamAvailable < 0.1 Then GoTo PreFin
             If RamAvailable < 0.1 Then GoTo PreFin
-            '预分配内存,阶段四,T3 ~ T3 * 2,40%
+            '预分配内存,阶段四,T3 ~ T3 * 2,15%
             RamDelta = RamTarget3
             RamDelta = RamTarget3
-            RamAvailable = Math.Max(0, RamAvailable - 0.3)
-            RamGive += Math.Min(RamAvailable * 0.4, RamDelta)
-            RamAvailable -= RamDelta / 0.4 + 0.3
+            RamGive += Math.Min(RamAvailable * 0.15, RamDelta)
+            RamAvailable -= RamDelta / 0.15
             If RamAvailable < 0.1 Then GoTo PreFin
             If RamAvailable < 0.1 Then GoTo PreFin
 PreFin:
 PreFin:
             '不低于最低值
             '不低于最低值
@@ -444,7 +442,7 @@ PreFin:
             Log(ex, "更新设置 Java 下拉框失败", LogLevel.Feedback)
             Log(ex, "更新设置 Java 下拉框失败", LogLevel.Feedback)
         End Try
         End Try
         '更新选择项
         '更新选择项
-        If SelectedItem Is Nothing AndAlso JavaList.Count > 0 Then SelectedItem = ComboArgumentJava.Items(0) '选中 “自动选择”
+        If SelectedItem Is Nothing AndAlso JavaList.Any Then SelectedItem = ComboArgumentJava.Items(0) '选中 “自动选择”
         ComboArgumentJava.SelectedItem = SelectedItem
         ComboArgumentJava.SelectedItem = SelectedItem
         '结束处理
         '结束处理
         If SelectedItem Is Nothing Then
         If SelectedItem Is Nothing Then
@@ -515,15 +513,16 @@ PreFin:
             Hint("正在搜索 Java,请稍候!", HintType.Critical)
             Hint("正在搜索 Java,请稍候!", HintType.Critical)
             Exit Sub
             Exit Sub
         End If
         End If
-        RunInThread(Sub()
-                        Hint("正在搜索 Java!")
-                        JavaSearchLoader.WaitForExit(IsForceRestart:=True)
-                        If Not JavaList.Any() Then
-                            Hint("未找到可用的 Java!", HintType.Critical)
-                        Else
-                            Hint("已找到 " & JavaList.Count & " 个 Java,请检查下拉框查看列表!", HintType.Finish)
-                        End If
-                    End Sub)
+        RunInThread(
+        Sub()
+            Hint("正在搜索 Java!")
+            JavaSearchLoader.WaitForExit(IsForceRestart:=True)
+            If Not JavaList.Any() Then
+                Hint("未找到可用的 Java!", HintType.Critical)
+            Else
+                Hint("已找到 " & JavaList.Count & " 个 Java,请检查下拉框查看列表!", HintType.Finish)
+            End If
+        End Sub)
     End Sub
     End Sub
 
 
 #End Region
 #End Region
@@ -548,12 +547,24 @@ PreFin:
         If AniControlEnabled <> 0 Then Exit Sub
         If AniControlEnabled <> 0 Then Exit Sub
         If ComboArgumentVisibie.SelectedIndex = 0 Then
         If ComboArgumentVisibie.SelectedIndex = 0 Then
             If MyMsgBox("若在游戏启动后立即关闭启动器,崩溃检测、更改游戏标题等功能将失效。" & vbCrLf &
             If MyMsgBox("若在游戏启动后立即关闭启动器,崩溃检测、更改游戏标题等功能将失效。" & vbCrLf &
-                        "如果想保留这些功能,可以选择让启动器在游戏启动后隐藏,游戏退出后自动关闭。", "提示", "继续", "取消") = 2 Then
+                        "如果想保留这些功能,可以选择让启动器在游戏启动后隐藏,游戏退出后自动关闭。", "提醒", "继续", "取消") = 2 Then
                 ComboArgumentVisibie.SelectedItem = e.RemovedItems(0)
                 ComboArgumentVisibie.SelectedItem = e.RemovedItems(0)
             End If
             End If
         End If
         End If
     End Sub
     End Sub
 
 
+    '开启自动内存优化的警告
+    Private Sub CheckArgumentRam_Change() Handles CheckArgumentRam.Change
+        If AniControlEnabled <> 0 Then Exit Sub
+        If Not CheckArgumentRam.Checked Then Return
+        If MyMsgBox("内存优化耗时较长,这会拖慢游戏的启动过程,建议仅在电脑内存不充裕时开启此选项。" & vbCrLf &
+                    "如果你在使用机械硬盘,这还可能会导致一小段时间的严重卡顿。" &
+                    If(IsAdmin(), "", $"{vbCrLf}{vbCrLf}每次启动游戏,PCL 都需要申请管理员权限以进行内存优化。{vbCrLf}若想自动授予权限,可以右键 PCL,打开 属性 → 兼容性 → 以管理员身份运行此程序。"),
+                    "提醒", "确定", "取消") = 2 Then
+            CheckArgumentRam.Checked = False
+        End If
+    End Sub
+
 #End Region
 #End Region
 
 
 #Region "高级设置"
 #Region "高级设置"

+ 0 - 1
Plain Craft Launcher 2/Pages/PageSetup/PageSetupLink.xaml

@@ -28,7 +28,6 @@
                                 <local:ValidateNullable />
                                 <local:ValidateNullable />
                                 <local:ValidateNullOrWhiteSpace />
                                 <local:ValidateNullOrWhiteSpace />
                                 <local:ValidateLength Max="20" />
                                 <local:ValidateLength Max="20" />
-                                <local:ValidateFilter />
                             </local:MyTextBox.ValidateRules>
                             </local:MyTextBox.ValidateRules>
                         </local:MyTextBox>
                         </local:MyTextBox>
                     </Grid>
                     </Grid>

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

@@ -58,7 +58,7 @@
                                           ToolTip="下载 Mod 时,若该 Mod 支持 Forge 和 Fabric,则显示为支持任意 Mod 加载器。&#xa;例如,开启时 JEI 显示支持全版本,关闭后 JEI 就会显示仅支持 Forge / Fabric 全版本。" />
                                           ToolTip="下载 Mod 时,若该 Mod 支持 Forge 和 Fabric,则显示为支持任意 Mod 加载器。&#xa;例如,开启时 JEI 显示支持全版本,关闭后 JEI 就会显示仅支持 Forge / Fabric 全版本。" />
                         <local:MyCheckBox Margin="0,2,0,4" Text="验证 SSL 证书" Grid.Row="9" Height="22" Grid.ColumnSpan="2"
                         <local:MyCheckBox Margin="0,2,0,4" Text="验证 SSL 证书" Grid.Row="9" Height="22" Grid.ColumnSpan="2"
                                           x:Name="CheckDownloadCert" Tag="ToolDownloadCert" 
                                           x:Name="CheckDownloadCert" Tag="ToolDownloadCert" 
-                                          ToolTip="在进行连接时,检查 SSL 证书是否正常。&#xa;进行验证会提高安全性,但疑似会导致少部分玩家的微软登录失败。" />
+                                          ToolTip="验证证书会提高安全性,但疑似有概率导致正版登录失败(#3018)。&#xa;如果在正版登录时因为 [根据验证过程,远程证书无效] 失败,可以尝试关闭此项。" />
                     </Grid>
                     </Grid>
                 </StackPanel>
                 </StackPanel>
             </local:MyCard>
             </local:MyCard>

+ 16 - 18
Plain Craft Launcher 2/Pages/PageSetup/PageSetupSystem.xaml.vb

@@ -126,23 +126,21 @@
 
 
     '滑动条
     '滑动条
     Private Sub SliderLoad()
     Private Sub SliderLoad()
-        SliderDownloadThread.GetHintText = Function(Value As Integer)
-                                               Return Value + 1
-                                           End Function
-        SliderDownloadSpeed.GetHintText = Function(Value As Integer)
-                                              If Value <= 14 Then
-                                                  Return (Value + 1) * 0.1 & " M/s"
-                                              ElseIf Value <= 31 Then
-                                                  Return (Value - 11) * 0.5 & " M/s"
-                                              ElseIf Value <= 41 Then
-                                                  Return (Value - 21) & " M/s"
-                                              Else
-                                                  Return "无限制"
-                                              End If
-                                          End Function
-        SliderDebugAnim.GetHintText = Function(Value As Integer)
-                                          Return If(Value > 29, "关闭", (Value / 10 + 0.1) & "x")
-                                      End Function
+        SliderDownloadThread.GetHintText = Function(v) v + 1
+        SliderDownloadSpeed.GetHintText =
+        Function(v)
+            Select Case v
+                Case Is <= 14
+                    Return (v + 1) * 0.1 & " M/s"
+                Case Is <= 31
+                    Return (v - 11) * 0.5 & " M/s"
+                Case Is <= 41
+                    Return (v - 21) & " M/s"
+                Case Else
+                    Return "无限制"
+            End Select
+        End Function
+        SliderDebugAnim.GetHintText = Function(v) If(v > 29, "关闭", (v / 10 + 0.1) & "x")
     End Sub
     End Sub
     Private Sub SliderDownloadThread_PreviewChange(sender As Object, e As RouteEventArgs) Handles SliderDownloadThread.PreviewChange
     Private Sub SliderDownloadThread_PreviewChange(sender As Object, e As RouteEventArgs) Handles SliderDownloadThread.PreviewChange
         If SliderDownloadThread.Value < 100 Then Exit Sub
         If SliderDownloadThread.Value < 100 Then Exit Sub
@@ -158,7 +156,7 @@
         PageOtherAbout.CopyUniqueAddress()
         PageOtherAbout.CopyUniqueAddress()
     End Sub
     End Sub
     Private Sub BtnSystemUnlock_Click(sender As Object, e As MouseButtonEventArgs) Handles BtnSystemUnlock.Click
     Private Sub BtnSystemUnlock_Click(sender As Object, e As MouseButtonEventArgs) Handles BtnSystemUnlock.Click
-        PageOtherAbout.DonateCodeInput()
+        DonateCodeInput()
     End Sub
     End Sub
 
 
     '调试模式
     '调试模式

+ 295 - 0
Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml

@@ -0,0 +1,295 @@
+<local:MyPageRight
+    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+    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" 
+    mc:Ignorable="d" x:Class="PageSetupUI"
+    PanScroll="{Binding ElementName=PanBack}">
+    <local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" x:Name="PanBack">
+        <StackPanel x:Name="PanMain" Margin="25,10">
+            <local:MyCard Margin="0,15" Title="基础" x:Name="CardLauncher">
+                <Grid Margin="25,40,25,17">
+                    <StackPanel>
+                        <Grid Margin="0,0,0,6">
+                            <Grid.ColumnDefinitions>
+                                <ColumnDefinition Width="Auto" SharedSizeGroup="Name1" />
+                                <ColumnDefinition Width="1*" />
+                                <ColumnDefinition Width="30" />
+                                <ColumnDefinition Width="Auto" />
+                                <ColumnDefinition Width="1*" />
+                            </Grid.ColumnDefinitions>
+                            <Grid.RowDefinitions>
+                                <RowDefinition Height="Auto" />
+                                <RowDefinition Height="Auto" />
+                                <RowDefinition Height="Auto" />
+                            </Grid.RowDefinitions>
+                            <TextBlock VerticalAlignment="Center" HorizontalAlignment="Left" Text="不透明度" Margin="0,0,20,0" />
+                            <local:MySlider Grid.ColumnSpan="5" x:Name="SliderLauncherOpacity" Tag="UiLauncherTransparent" MaxValue="600" ValueByKey="10" Grid.Column="1" />
+                            <TextBlock Grid.Row="1" VerticalAlignment="Center" Name="LabLauncherHue" Visibility="Collapsed" HorizontalAlignment="Left" Text="色调" Margin="0,10,20,0" />
+                            <local:MySlider Grid.Row="1" Grid.Column="1" x:Name="SliderLauncherHue" Tag="UiLauncherHue" Visibility="Collapsed" MaxValue="360" ValueByKey="10" Margin="0,10,0,0" />
+                            <TextBlock Grid.Row="2" VerticalAlignment="Center" Name="LabLauncherDelta" Visibility="Collapsed" HorizontalAlignment="Left" Text="色调渐变" Margin="0,10,20,0" />
+                            <local:MySlider Grid.Row="2" Grid.Column="1" x:Name="SliderLauncherDelta" Tag="UiLauncherDelta" Visibility="Collapsed" MaxValue="180" ValueByKey="5" Margin="0,10,0,0" />
+                            <TextBlock Grid.Row="1" Grid.Column="3" VerticalAlignment="Center" Name="LabLauncherSat" Visibility="Collapsed" HorizontalAlignment="Left" Text="饱和度" Margin="0,10,20,0" />
+                            <local:MySlider Grid.Row="1" Grid.Column="4" x:Name="SliderLauncherSat" Tag="UiLauncherSat" Visibility="Collapsed" MaxValue="100" ValueByKey="5" Margin="0,10,0,0" />
+                            <TextBlock Grid.Row="2" Grid.Column="3" VerticalAlignment="Center" Name="LabLauncherLight" Visibility="Collapsed" HorizontalAlignment="Left" Text="亮度" Margin="0,10,20,0" />
+                            <local:MySlider Grid.Row="2" Grid.Column="4" x:Name="SliderLauncherLight" Tag="UiLauncherLight" Visibility="Collapsed" MaxValue="40" ValueByKey="5" Margin="0,10,0,0" />
+                        </Grid>
+                        <Grid x:Name="PanLauncherTheme" Height="90" Margin="0,0,0,7">
+                            <Grid.ColumnDefinitions>
+                                <ColumnDefinition Width="Auto" SharedSizeGroup="Name1" />
+                                <ColumnDefinition />
+                                <ColumnDefinition />
+                                <ColumnDefinition />
+                                <ColumnDefinition />
+                                <ColumnDefinition />
+                            </Grid.ColumnDefinitions>
+                            <Grid.RowDefinitions>
+                                <RowDefinition />
+                                <RowDefinition />
+                                <RowDefinition />
+                            </Grid.RowDefinitions>
+                            <TextBlock VerticalAlignment="Center" HorizontalAlignment="Left" Text="主题" Margin="0,0,20,0" />
+                            <TextBlock VerticalAlignment="Center" HorizontalAlignment="Left" Text="隐藏主题" Margin="0,0,20,0" Grid.Row="1" />
+                            <local:MyRadioBox Grid.Column="1" Grid.Row="0" Text="龙猫蓝" x:Name="RadioLauncherTheme0" Tag="UiLauncherTheme/0" />
+                            <local:MyRadioBox Grid.Column="2" Grid.Row="0" Text="甜柠青" x:Name="RadioLauncherTheme1" Tag="UiLauncherTheme/1" />
+                            <local:MyRadioBox Grid.Column="3" Grid.Row="0" Text="小草绿" x:Name="RadioLauncherTheme2" Tag="UiLauncherTheme/2" />
+                            <local:MyRadioBox Grid.Column="4" Grid.Row="0" Text="菠萝黄" x:Name="RadioLauncherTheme3" Tag="UiLauncherTheme/3" />
+                            <local:MyRadioBox Grid.Column="5" Grid.Row="0" Text="橡木棕" x:Name="RadioLauncherTheme4" Tag="UiLauncherTheme/4" />
+                            <local:MyRadioBox Grid.Column="1" Grid.Row="1" Text="玄素黑" x:Name="RadioLauncherTheme5" Tag="UiLauncherTheme/5" Opacity="0" ToolTip="灰色是个谎言" ToolTipService.Placement="Bottom" />
+                            <local:MyRadioBox Grid.Column="1" Grid.Row="1" Text="???" x:Name="RadioLauncherTheme5Gray" IsEnabled="False" Opacity="1" />
+                            <TextBlock Grid.Column="1" Grid.Row="1" x:Name="LabLauncherTheme5Unlock" Background="{StaticResource ColorBrushSemiTransparent}" ToolTip="灰色是个谎言" ToolTipService.Placement="Bottom" />
+                            <local:MyRadioBox Grid.Column="2" Grid.Row="1" Text="???" x:Name="RadioLauncherTheme12" Tag="UiLauncherTheme/12" IsEnabled="False" ToolTip="滑稽树上滑稽果,滑稽树下你和我,滑稽之日搞事情,欢乐多又多" ToolTipService.ShowOnDisabled="True" ToolTipService.Placement="Bottom" />
+                            <local:MyRadioBox Grid.Column="3" Grid.Row="1" Text="???" x:Name="RadioLauncherTheme6" Tag="UiLauncherTheme/6" IsEnabled="False" ToolTip="99 次重逢的喜悦" ToolTipService.ShowOnDisabled="True" ToolTipService.Placement="Bottom" />
+                            <local:MyRadioBox Grid.Column="4" Grid.Row="1" Text="???" x:Name="RadioLauncherTheme7" Tag="UiLauncherTheme/7" IsEnabled="False" ToolTip="072 101 108 112 032 068 101 118 032 048 046 052 046 052" ToolTipService.ShowOnDisabled="True" ToolTipService.Placement="Bottom" />
+                            <local:MyRadioBox Grid.Column="5" Grid.Row="1" Text="???" x:Name="RadioLauncherTheme13" Tag="UiLauncherTheme/13" IsEnabled="False" ToolTip="这就是传说中的欧皇了吧" ToolTipService.ShowOnDisabled="True" ToolTipService.Placement="Bottom" />
+                            <local:MyRadioBox Grid.Column="1" Grid.Row="2" Text="???" x:Name="RadioLauncherTheme8" Tag="UiLauncherTheme/8" IsEnabled="False" ToolTipService.Placement="Bottom" />
+                            <TextBlock Grid.Column="1" Grid.Row="2" x:Name="LabLauncherTheme8Copy" Background="{StaticResource ColorBrushSemiTransparent}" ToolTipService.Placement="Bottom" />
+                            <local:MyRadioBox Grid.Column="2" Grid.Row="2" Text="???" x:Name="RadioLauncherTheme9" Tag="UiLauncherTheme/9" IsEnabled="False" ToolTipService.Placement="Bottom" />
+                            <TextBlock Grid.Column="2" Grid.Row="2" x:Name="LabLauncherTheme9Copy" Background="{StaticResource ColorBrushSemiTransparent}" ToolTipService.Placement="Bottom" />
+                            <local:MyRadioBox Grid.Column="3" Grid.Row="2" Text="???" x:Name="RadioLauncherTheme10" Tag="UiLauncherTheme/10" IsEnabled="False" ToolTip="进行一次正版登录,支持正版游戏!" ToolTipService.ShowOnDisabled="True" ToolTipService.Placement="Bottom" />
+                            <local:MyRadioBox Grid.Column="4" Grid.Row="2" Text="???" x:Name="RadioLauncherTheme11" Tag="UiLauncherTheme/11" IsEnabled="False" ToolTipService.Placement="Bottom" ToolTip="右键打开解密游戏入口" />
+                            <TextBlock Grid.Column="4" Grid.Row="2" x:Name="LabLauncherTheme11Click" Background="{StaticResource ColorBrushSemiTransparent}" ToolTipService.Placement="Bottom" />
+                            <local:MyRadioBox Grid.Column="5" Grid.Row="2" Text="自定义" x:Name="RadioLauncherTheme14" Tag="UiLauncherTheme/14" IsEnabled="False" ToolTipService.Placement="Bottom" ToolTip="需要解锁五个隐藏主题" ToolTipService.ShowOnDisabled="True" />
+                        </Grid>
+                        <local:MyCheckBox Text="打开启动器时显示 PCL 图标" Margin="-1,0,0,1" Height="22" x:Name="CheckLauncherLogo" Tag="UiLauncherLogo" />
+                    </StackPanel>
+                    <Border Background="#CCCCCCCC" Margin="-5,22,-5,-5" CornerRadius="4" Name="PanLauncherHide" Visibility="Collapsed">
+                        <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
+                            <TextBlock Text="需要小小赞助一下作者,才能修改主题颜色(悲)……" FontSize="15" Margin="0,2,0,0" />
+                            <local:MyButton Height="35" x:Name="BtnLauncherDonate" MinWidth="150" Text="支持作者" Padding="13,0" HorizontalAlignment="Center" Margin="0,9,0,0" ColorType="Highlight" />
+                        </StackPanel>
+                    </Border>
+                </Grid>
+            </local:MyCard>
+            <local:MyCard x:Name="CardBackground" Margin="0,0,0,15" Title="背景图片">
+                <StackPanel Margin="25,40,25,20">
+                    <Grid x:Name="PanBackgroundSuit" Height="28" Margin="0,0,0,10" Visibility="Collapsed">
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="Auto" SharedSizeGroup="Name" />
+                            <ColumnDefinition />
+                        </Grid.ColumnDefinitions>
+                        <TextBlock VerticalAlignment="Center" HorizontalAlignment="Left" Text="自适应方式" Margin="0,0,25,0" />
+                        <local:MyComboBox x:Name="ComboBackgroundSuit" Tag="UiBackgroundSuit" Grid.Column="1">
+                            <local:MyComboBoxItem Content="智能" IsSelected="True" ToolTip="当图片较小时选择平铺,较大时选择适应" />
+                            <local:MyComboBoxItem Content="居中" />
+                            <local:MyComboBoxItem Content="适应" ToolTip="拉伸图片并保持长宽比不变,直至填满整个窗口" />
+                            <local:MyComboBoxItem Content="拉伸" ToolTip="拉伸图片使其恰好填满整个窗口,很可能会导致长宽比的改变" />
+                            <local:MyComboBoxItem Content="平铺" />
+                            <local:MyComboBoxItem Content="居于左上" />
+                            <local:MyComboBoxItem Content="居于右上" />
+                            <local:MyComboBoxItem Content="居于左下" />
+                            <local:MyComboBoxItem Content="居于右下" />
+                        </local:MyComboBox>
+                    </Grid>
+                    <Grid x:Name="PanBackgroundOpacity" Height="22" Margin="0,0,0,7" Visibility="Collapsed">
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="Auto" SharedSizeGroup="Name" />
+                            <ColumnDefinition />
+                        </Grid.ColumnDefinitions>
+                        <TextBlock VerticalAlignment="Center" HorizontalAlignment="Left" Text="不透明度" Margin="0,0,25,0" />
+                        <local:MySlider x:Name="SliderBackgroundOpacity" Tag="UiBackgroundOpacity" MaxValue="1000" ValueByKey="10" Grid.Column="1" />
+                    </Grid>
+                    <Grid x:Name="PanBackgroundBlur" Height="22" Margin="0,0,0,10" Visibility="Collapsed">
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="Auto" SharedSizeGroup="Name" />
+                            <ColumnDefinition />
+                        </Grid.ColumnDefinitions>
+                        <TextBlock VerticalAlignment="Center" HorizontalAlignment="Left" Text="背景模糊" Margin="0,0,25,0" />
+                        <local:MySlider x:Name="SliderBackgroundBlur" Tag="UiBackgroundBlur" MaxValue="40" Grid.Column="1" ToolTip="较强的模糊会导致启动器严重掉帧,请谨慎使用。&#13;不建议在制作整合包时开启模糊,以确保 PCL 在低性能电脑上可以流畅运行。" />
+                    </Grid>
+                    <local:MyCheckBox Text="使用彩色背景" Margin="-1,0,0,8" Height="22" x:Name="CheckBackgroundColorful" Tag="UiBackgroundColorful" />
+                    <Grid Height="35" Margin="0,2,0,0">
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
+                            <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
+                            <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
+                        </Grid.ColumnDefinitions>
+                        <local:MyButton x:Name="BtnBackgroundOpen" MinWidth="140" Text="打开文件夹" Padding="13,0" Margin="0,0,20,0" ToolTip="将背景图片放到该文件夹,PCL 将会在每次打开时随机加载其中的一张图片"/>
+                        <local:MyButton x:Name="BtnBackgroundRefresh" MinWidth="140" Text="刷新背景图片" Padding="13,0" Margin="0,0,20,0" ToolTip="随机加载文件夹内的一张背景图片" Grid.Column="1"/>
+                        <local:MyButton x:Name="BtnBackgroundClear" MinWidth="140" Text="清空背景图片" Visibility="Collapsed" Padding="13,0" Margin="0,0,20,0" ColorType="Red" Grid.Column="2"/>
+                    </Grid>
+                </StackPanel>
+            </local:MyCard>
+            <local:MyCard x:Name="CardMusic" Margin="0,0,0,15" Title="背景音乐">
+                <StackPanel Margin="25,42,25,20">
+                    <Grid x:Name="PanMusicVolume" Visibility="Collapsed" Height="22" Margin="0,0,0,10">
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="Auto" SharedSizeGroup="Name" />
+                            <ColumnDefinition />
+                        </Grid.ColumnDefinitions>
+                        <TextBlock VerticalAlignment="Center" HorizontalAlignment="Left" Text="音量" Margin="0,0,25,0" />
+                        <local:MySlider x:Name="SliderMusicVolume" Tag="UiMusicVolume" MaxValue="1000" ValueByKey="10" Grid.Column="1" />
+                    </Grid>
+                    <StackPanel x:Name="PanMusicDetail" Visibility="Collapsed" Margin="-1,0,0,3">
+                        <local:MyCheckBox Text="随机播放" Margin="0,0,0,5" Height="22" x:Name="CheckMusicRandom" Tag="UiMusicRandom" />
+                        <local:MyCheckBox Text="打开启动器自动开始播放" Margin="0,0,0,5" Height="22" x:Name="CheckMusicAuto" Tag="UiMusicAuto" />
+                        <local:MyCheckBox Text="游戏启动后自动开始播放,游戏退出后自动暂停播放" Margin="0,0,0,5" Height="22" x:Name="CheckMusicStart" Tag="UiMusicStart" />
+                        <local:MyCheckBox Text="游戏启动后自动暂停播放,游戏退出后自动开始播放" Margin="0,0,0,5" Height="22" x:Name="CheckMusicStop" Tag="UiMusicStop" />
+                    </StackPanel>
+                    <Grid Height="35">
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
+                            <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
+                            <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
+                        </Grid.ColumnDefinitions>
+                        <local:MyButton x:Name="BtnMusicOpen" Text="打开文件夹" MinWidth="140" Padding="13,0" Margin="0,0,20,0" ToolTip="将背景音乐放到该文件夹,PCL 将会播放其中的所有音乐"/>
+                        <local:MyButton x:Name="BtnMusicRefresh" Text="刷新背景音乐" MinWidth="140" Padding="13,0" Margin="0,0,20,0" ToolTip="刷新播放列表,并播放列表中的音乐" Grid.Column="1"/>
+                        <local:MyButton x:Name="BtnMusicClear" Text="清空背景音乐" MinWidth="140" Visibility="Collapsed" Padding="13,0" Margin="0,0,20,0" ColorType="Red" Grid.Column="2"/>
+                    </Grid>
+                </StackPanel>
+            </local:MyCard>
+            <local:MyCard x:Name="CardLogo" Margin="0,0,0,15" Title="标题栏">
+                <StackPanel Margin="25,40,25,15">
+                    <Grid Margin="-1,0,0,0">
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="1*" />
+                            <ColumnDefinition Width="1*" />
+                            <ColumnDefinition Width="1*" />
+                            <ColumnDefinition Width="1*" />
+                            <ColumnDefinition Width="0.2*" />
+                        </Grid.ColumnDefinitions>
+                        <local:MyRadioBox Text="无" x:Name="RadioLogoType0" Height="22" Tag="UiLogoType/0" Grid.Column="0" />
+                        <local:MyRadioBox Text="默认" x:Name="RadioLogoType1" Height="22" Tag="UiLogoType/1" Grid.Column="1" />
+                        <local:MyRadioBox Text="文本" x:Name="RadioLogoType2" Height="22" Tag="UiLogoType/2" Grid.Column="2" />
+                        <local:MyRadioBox Text="图片" x:Name="RadioLogoType3" Height="22" Tag="UiLogoType/3" Grid.Column="3" />
+                    </Grid>
+                    <local:MyCheckBox Text="标题栏居左" Margin="-1,8,0,3" Height="22" x:Name="CheckLogoLeft" Tag="UiLogoLeft" Visibility="Collapsed" />
+                    <Grid x:Name="PanLogoText" Margin="0,14,0,5" Visibility="Collapsed">
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="Auto" SharedSizeGroup="Name" />
+                            <ColumnDefinition />
+                        </Grid.ColumnDefinitions>
+                        <TextBlock VerticalAlignment="Center" HorizontalAlignment="Left" Text="标题栏文本" Margin="0,0,25,0" />
+                        <local:MyTextBox x:Name="TextLogoText" Tag="UiLogoText" Grid.Column="1" MaxLength="30" />
+                    </Grid>
+                    <Grid x:Name="PanLogoChange" Margin="0,14,0,5" Height="35" Visibility="Collapsed">
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
+                            <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
+                        </Grid.ColumnDefinitions>
+                        <local:MyButton x:Name="BtnLogoChange" Text="更改图片" MinWidth="140" Padding="13,0" Margin="0,0,20,0" />
+                        <local:MyButton x:Name="BtnLogoDelete" Text="清空图片" MinWidth="140" Padding="13,0" Margin="0,0,20,0" ColorType="Red" Grid.Column="1" />
+                    </Grid>
+                </StackPanel>
+            </local:MyCard>
+            <local:MyCard Margin="0,0,0,15" Title="主页" x:Name="CardCustom">
+                <StackPanel Margin="25,40,25,15">
+                    <Grid Margin="-1,0,0,0">
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="1*" />
+                            <ColumnDefinition Width="1*" />
+                            <ColumnDefinition Width="1*" />
+                            <ColumnDefinition Width="1*" />
+                            <ColumnDefinition Width="0.2*" />
+                        </Grid.ColumnDefinitions>
+                        <local:MyRadioBox Text="空白" x:Name="RadioCustomType0" Height="22" Tag="UiCustomType/0" Grid.Column="0" />
+                        <local:MyRadioBox Text="预设" x:Name="RadioCustomType3" Height="22" Tag="UiCustomType/3" Grid.Column="1" />
+                        <local:MyRadioBox Text="读取本地文件" x:Name="RadioCustomType1" Height="22" Tag="UiCustomType/1" Grid.Column="2" />
+                        <local:MyRadioBox Text="联网更新" x:Name="RadioCustomType2" Height="22" Tag="UiCustomType/2" Grid.Column="3" />
+                    </Grid>
+                    <local:MyHint Margin="0,15,0,1" Text="内容见 ModSetup" IsWarn="False" x:Name="HintCustom" />
+                    <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" />
+                        </Grid.ColumnDefinitions>
+                        <local:MyButton x:Name="BtnCustomRefresh" Grid.Column="0" MinWidth="140" Text="刷新主页" Padding="13,0" Margin="0,0,20,0" HorizontalAlignment="Left" ColorType="Highlight" />
+                        <local:MyButton x:Name="BtnCustomFile" Grid.Column="1" MinWidth="140" Text="生成教学文件" Padding="13,0" Margin="0,0,20,0" HorizontalAlignment="Left" />
+                        <local:MyButton x:Name="BtnCustomTutorial" Grid.Column="2" MinWidth="140" Text="查看教程" Padding="13,0" Margin="0,0,20,0" HorizontalAlignment="Left" />
+                    </Grid>
+                    <Grid x:Name="PanCustomNet" Visibility="Collapsed" Margin="0,10,0,5">
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="Auto" SharedSizeGroup="Name" />
+                            <ColumnDefinition />
+                        </Grid.ColumnDefinitions>
+                        <TextBlock VerticalAlignment="Center" HorizontalAlignment="Left" Text="下载地址" Margin="0,0,25,0" />
+                        <local:MyTextBox x:Name="TextCustomNet" Tag="UiCustomNet" Grid.Column="1" MaxHeight="28">
+                            <local:MyTextBox.ValidateRules>
+                                <local:ValidateHttp />
+                            </local:MyTextBox.ValidateRules>
+                        </local:MyTextBox>
+                    </Grid>
+                    <Grid x:Name="PanCustomPreset" Visibility="Collapsed" Margin="0,12,10,5" Height="28">
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="Auto" SharedSizeGroup="Name" />
+                            <ColumnDefinition />
+                        </Grid.ColumnDefinitions>
+                        <TextBlock VerticalAlignment="Center" HorizontalAlignment="Left" Text="主页预设" Margin="0,0,25,0" />
+                        <local:MyComboBox x:Name="ComboCustomPreset" Grid.ColumnSpan="2" Tag="UiCustomPreset" Grid.Column="1">
+                            <local:MyComboBoxItem Content="你知道吗?" IsSelected="True" />
+                            <local:MyComboBoxItem Content="回声洞" />
+                            <local:MyComboBoxItem Content="Minecraft 新闻(作者:最亮的信标)" />
+                            <local:MyComboBoxItem Content="简单主页(作者:MFn233)" />
+                            <local:MyComboBoxItem Content="每日整合包推荐(作者:wkea)" />
+                            <local:MyComboBoxItem Content="Minecraft 皮肤推荐(作者:wkea)" />
+                        </local:MyComboBox>
+                    </Grid>
+                </StackPanel>
+            </local:MyCard>
+            <local:MyCard x:Name="CardSwitch" Margin="0,0,0,15" Title="功能隐藏">
+                <StackPanel Margin="25,38,15,15">
+                    <TextBlock Margin="0,1,0,1" Text="你可以隐藏不需要的页面或关闭特定功能。在任意界面按 F12 可以暂时显示被隐藏的功能。" TextWrapping="Wrap" />
+                    <Grid>
+                        <Grid.ColumnDefinitions>
+                            <ColumnDefinition Width="Auto" />
+                            <ColumnDefinition Width="18" />
+                            <ColumnDefinition Width="0.8*" />
+                            <ColumnDefinition Width="0.9*" />
+                            <ColumnDefinition Width="0.8*" />
+                            <ColumnDefinition Width="0.6*" />
+                            <ColumnDefinition Width="1.0*" />
+                        </Grid.ColumnDefinitions>
+                        <Grid.RowDefinitions>
+                            <RowDefinition Height="4" />
+                            <RowDefinition Height="30" />
+                            <RowDefinition Height="30" />
+                            <RowDefinition Height="30" />
+                            <RowDefinition Height="30" />
+                            <RowDefinition Height="Auto" />
+                        </Grid.RowDefinitions>
+                        <TextBlock Grid.Row="1" VerticalAlignment="Center" HorizontalAlignment="Left" Text="主页面" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="1" Grid.Column="2" Text="下载" x:Name="CheckHiddenPageDownload" Tag="UiHiddenPageDownload" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="1" Grid.Column="3" Visibility="Collapsed" Text="联机" x:Name="CheckHiddenPageLink" Tag="UiHiddenPageLink" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="1" Grid.Column="3" Text="设置" x:Name="CheckHiddenPageSetup" Tag="UiHiddenPageSetup" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="1" Grid.Column="4" Text="更多" x:Name="CheckHiddenPageOther" Tag="UiHiddenPageOther" />
+                        <TextBlock Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Left" Text="设置 子页面" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="2" Grid.Column="2" Text="游戏" x:Name="CheckHiddenSetupLaunch" Tag="UiHiddenSetupLaunch" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="2" Grid.Column="3" Text="个性化" x:Name="CheckHiddenSetupUI" Tag="UiHiddenSetupUi" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="2" Grid.Column="4" Text="启动器" x:Name="CheckHiddenSetupSystem" Tag="UiHiddenSetupSystem" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="2" Grid.Column="5" Text="联机" x:Name="CheckHiddenSetupLink" Tag="UiHiddenSetupLink" Visibility="Collapsed" />
+                        <TextBlock Grid.Row="3" VerticalAlignment="Center" HorizontalAlignment="Left" Text="更多 子页面" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="3" Grid.Column="2" Text="帮助" x:Name="CheckHiddenOtherHelp" Tag="UiHiddenOtherHelp" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="3" Grid.Column="3" Text="关于与鸣谢" x:Name="CheckHiddenOtherAbout" Tag="UiHiddenOtherAbout" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="3" Grid.Column="4" Text="百宝箱" x:Name="CheckHiddenOtherTest" Tag="UiHiddenOtherTest" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="3" Grid.Column="5" Text="反馈" x:Name="CheckHiddenOtherFeedback" Tag="UiHiddenOtherFeedback" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="3" Grid.Column="6" Text="新功能投票" x:Name="CheckHiddenOtherVote" Tag="UiHiddenOtherVote" />
+                        <TextBlock Grid.Row="4" VerticalAlignment="Center" HorizontalAlignment="Left" Text="特定功能" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="4" Grid.Column="2" Text="邮箱隐藏" x:Name="CheckLauncherEmail" Tag="UiLauncherEmail" ToolTip="隐藏第三方登录的账号邮箱显示,并尽量将邮箱打码,以避免隐私泄露。&#xa;此选项的更改将在重启启动器后生效。" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="4" Grid.Column="3" Text="版本选择" x:Name="CheckHiddenFunctionSelect" Tag="UiHiddenFunctionSelect" />
+                        <local:MyCheckBox Height="22" VerticalAlignment="Center" Grid.Row="4" Grid.Column="4" Text="功能隐藏" x:Name="CheckHiddenFunctionHidden" Tag="UiHiddenFunctionHidden" ToolTip="仅隐藏本选项卡,设置的禁用内容依然有效。&#xa;可以在保留个性化设置页面的情况下避免隐藏设置被修改。" />
+                    </Grid>
+                </StackPanel>
+            </local:MyCard>
+        </StackPanel>
+    </local:MyScrollViewer>
+</local:MyPageRight>

+ 705 - 0
Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml.vb

@@ -0,0 +1,705 @@
+Public Class PageSetupUI
+
+    Public Shadows IsLoaded As Boolean = False
+
+    Private Sub PageSetupUI_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
+
+        '重复加载部分
+        PanBack.ScrollToHome()
+        ThemeCheckAll(True)
+
+        If ThemeDontClick <> 0 Then
+            Dim NewText As String
+            Select Case ThemeDontClick
+                Case 1
+                    NewText = "眼瞎白"
+                Case 2
+                    NewText = "真·滑稽彩"
+                Case Else
+                    NewText = "???"
+            End Select
+            For Each Control In PanLauncherTheme.Children
+                If (TypeOf Control Is MyRadioBox) AndAlso CType(Control, MyRadioBox).IsEnabled Then
+                    CType(Control, MyRadioBox).Text = NewText
+                End If
+            Next
+        End If
+
+        '非重复加载部分
+        If IsLoaded Then Exit Sub
+        IsLoaded = True
+
+        AniControlEnabled += 1
+        SliderLoad()
+        Reload()
+        AniControlEnabled -= 1
+
+#If BETA Then
+        PanLauncherHide.Visibility = Visibility.Visible
+#End If
+
+        '设置解锁
+        If Not RadioLauncherTheme8.IsEnabled Then LabLauncherTheme8Copy.ToolTip = "累积赞助达到 ¥23.33 后,在爱发电私信发送【解锁码】以解锁。" & vbCrLf & "右键打开赞助页面,如果觉得 PCL 做得还不错就支持一下吧 =w=!"
+        RadioLauncherTheme8.ToolTip = "累积赞助达到 ¥23.33 后,在爱发电私信发送【解锁码】以解锁"
+        If Not RadioLauncherTheme9.IsEnabled Then LabLauncherTheme9Copy.ToolTip = "· 反馈一个 Bug,在 Issue 标记为 [已完成] 后回复 Issue 要求解锁(右键打开反馈页面)" & vbCrLf & "· 向帮助库提交 Pull Request,在龙猫合并后解锁"
+        RadioLauncherTheme9.ToolTip = "· 反馈一个 Bug,在反馈标记为 [完成] 后回复 Issue 要求解锁" & vbCrLf & "· 向帮助库提交 Pull Request,在龙猫合并后解锁"
+        '极客蓝的处理在 ThemeCheck 中
+
+    End Sub
+    Public Sub Reload()
+        Try
+
+            '启动器
+            SliderLauncherOpacity.Value = Setup.Get("UiLauncherTransparent")
+            SliderLauncherHue.Value = Setup.Get("UiLauncherHue")
+            SliderLauncherSat.Value = Setup.Get("UiLauncherSat")
+            SliderLauncherDelta.Value = Setup.Get("UiLauncherDelta")
+            SliderLauncherLight.Value = Setup.Get("UiLauncherLight")
+            If Setup.Get("UiLauncherTheme") <= 14 Then CType(FindName("RadioLauncherTheme" & Setup.Get("UiLauncherTheme")), MyRadioBox).Checked = True
+            CheckLauncherLogo.Checked = Setup.Get("UiLauncherLogo")
+            CheckLauncherEmail.Checked = Setup.Get("UiLauncherEmail")
+
+            '背景图片
+            SliderBackgroundOpacity.Value = Setup.Get("UiBackgroundOpacity")
+            SliderBackgroundBlur.Value = Setup.Get("UiBackgroundBlur")
+            ComboBackgroundSuit.SelectedIndex = Setup.Get("UiBackgroundSuit")
+            CheckBackgroundColorful.Checked = Setup.Get("UiBackgroundColorful")
+            BackgroundRefresh(False, False)
+
+            '标题栏
+            CType(FindName("RadioLogoType" & Setup.Get("UiLogoType")), MyRadioBox).Checked = True
+            CheckLogoLeft.Visibility = If(RadioLogoType0.Checked, Visibility.Visible, Visibility.Collapsed)
+            PanLogoText.Visibility = If(RadioLogoType2.Checked, Visibility.Visible, Visibility.Collapsed)
+            PanLogoChange.Visibility = If(RadioLogoType3.Checked, Visibility.Visible, Visibility.Collapsed)
+            TextLogoText.Text = Setup.Get("UiLogoText")
+            CheckLogoLeft.Checked = Setup.Get("UiLogoLeft")
+
+            '背景音乐
+            CheckMusicRandom.Checked = Setup.Get("UiMusicRandom")
+            CheckMusicAuto.Checked = Setup.Get("UiMusicAuto")
+            CheckMusicStop.Checked = Setup.Get("UiMusicStop")
+            CheckMusicStart.Checked = Setup.Get("UiMusicStart")
+            SliderMusicVolume.Value = Setup.Get("UiMusicVolume")
+            MusicRefreshUI()
+
+            '主页
+            ComboCustomPreset.SelectedIndex = Setup.Get("UiCustomPreset")
+            CType(FindName("RadioCustomType" & Setup.Load("UiCustomType")), MyRadioBox).Checked = True
+            TextCustomNet.Text = Setup.Get("UiCustomNet")
+
+            '功能隐藏
+            CheckHiddenPageDownload.Checked = Setup.Get("UiHiddenPageDownload")
+            CheckHiddenPageLink.Checked = Setup.Get("UiHiddenPageLink")
+            CheckHiddenPageSetup.Checked = Setup.Get("UiHiddenPageSetup")
+            CheckHiddenPageOther.Checked = Setup.Get("UiHiddenPageOther")
+            CheckHiddenFunctionSelect.Checked = Setup.Get("UiHiddenFunctionSelect")
+            CheckHiddenFunctionHidden.Checked = Setup.Get("UiHiddenFunctionHidden")
+            CheckHiddenSetupLaunch.Checked = Setup.Get("UiHiddenSetupLaunch")
+            CheckHiddenSetupUI.Checked = Setup.Get("UiHiddenSetupUi")
+            CheckHiddenSetupLink.Checked = Setup.Get("UiHiddenSetupLink")
+            CheckHiddenSetupSystem.Checked = Setup.Get("UiHiddenSetupSystem")
+            CheckHiddenOtherAbout.Checked = Setup.Get("UiHiddenOtherAbout")
+            CheckHiddenOtherFeedback.Checked = Setup.Get("UiHiddenOtherFeedback")
+            CheckHiddenOtherVote.Checked = Setup.Get("UiHiddenOtherVote")
+            CheckHiddenOtherHelp.Checked = Setup.Get("UiHiddenOtherHelp")
+            CheckHiddenOtherTest.Checked = Setup.Get("UiHiddenOtherTest")
+
+        Catch ex As NullReferenceException
+            Log(ex, "个性化设置项存在异常,已被自动重置", LogLevel.Msgbox)
+            Reset()
+        Catch ex As Exception
+            Log(ex, "重载个性化设置时出错", LogLevel.Feedback)
+        End Try
+    End Sub
+
+    '初始化
+    Public Sub Reset()
+        Try
+            Setup.Reset("UiLauncherTransparent")
+            Setup.Reset("UiLauncherTheme")
+            Setup.Reset("UiLauncherLogo")
+            Setup.Reset("UiLauncherHue")
+            Setup.Reset("UiLauncherSat")
+            Setup.Reset("UiLauncherDelta")
+            Setup.Reset("UiLauncherLight")
+            Setup.Reset("UiLauncherEmail")
+            Setup.Reset("UiBackgroundColorful")
+            Setup.Reset("UiBackgroundOpacity")
+            Setup.Reset("UiBackgroundBlur")
+            Setup.Reset("UiBackgroundSuit")
+            Setup.Reset("UiLogoType")
+            Setup.Reset("UiLogoText")
+            Setup.Reset("UiLogoLeft")
+            Setup.Reset("UiMusicVolume")
+            Setup.Reset("UiMusicStop")
+            Setup.Reset("UiMusicStart")
+            Setup.Reset("UiMusicRandom")
+            Setup.Reset("UiMusicAuto")
+            Setup.Reset("UiCustomType")
+            Setup.Reset("UiCustomPreset")
+            Setup.Reset("UiCustomNet")
+            Setup.Reset("UiHiddenPageDownload")
+            Setup.Reset("UiHiddenPageLink")
+            Setup.Reset("UiHiddenPageSetup")
+            Setup.Reset("UiHiddenPageOther")
+            Setup.Reset("UiHiddenFunctionSelect")
+            Setup.Reset("UiHiddenFunctionHidden")
+            Setup.Reset("UiHiddenSetupLaunch")
+            Setup.Reset("UiHiddenSetupUi")
+            Setup.Reset("UiHiddenSetupLink")
+            Setup.Reset("UiHiddenSetupSystem")
+            Setup.Reset("UiHiddenOtherAbout")
+            Setup.Reset("UiHiddenOtherFeedback")
+            Setup.Reset("UiHiddenOtherVote")
+            Setup.Reset("UiHiddenOtherHelp")
+            Setup.Reset("UiHiddenOtherTest")
+
+            Log("[Setup] 已初始化个性化设置!")
+            Hint("已初始化个性化设置", HintType.Finish, False)
+        Catch ex As Exception
+            Log(ex, "初始化个性化设置失败", LogLevel.Msgbox)
+        End Try
+
+        Reload()
+    End Sub
+
+    '将控件改变路由到设置改变
+    Private Shared Sub SliderChange(sender As MySlider, e As Object) Handles SliderBackgroundOpacity.Change, SliderBackgroundBlur.Change, SliderLauncherOpacity.Change, SliderMusicVolume.Change, SliderLauncherHue.Change, SliderLauncherLight.Change, SliderLauncherSat.Change, SliderLauncherDelta.Change
+        If AniControlEnabled = 0 Then Setup.Set(sender.Tag, sender.Value)
+    End Sub
+    Private Shared Sub ComboChange(sender As MyComboBox, e As Object) Handles ComboBackgroundSuit.SelectionChanged, ComboCustomPreset.SelectionChanged
+        If AniControlEnabled = 0 Then Setup.Set(sender.Tag, sender.SelectedIndex)
+    End Sub
+    Private Shared Sub CheckBoxChange(sender As MyCheckBox, e As Object) Handles CheckMusicStop.Change, CheckMusicRandom.Change, CheckMusicAuto.Change, CheckBackgroundColorful.Change, CheckLogoLeft.Change, CheckLauncherLogo.Change, CheckHiddenFunctionHidden.Change, CheckHiddenFunctionSelect.Change, CheckHiddenPageDownload.Change, CheckHiddenPageLink.Change, CheckHiddenPageOther.Change, CheckHiddenPageSetup.Change, CheckHiddenSetupLaunch.Change, CheckHiddenSetupSystem.Change, CheckHiddenSetupLink.Change, CheckHiddenSetupUI.Change, CheckHiddenOtherAbout.Change, CheckHiddenOtherFeedback.Change, CheckHiddenOtherVote.Change, CheckHiddenOtherHelp.Change, CheckHiddenOtherTest.Change, CheckMusicStart.Change, CheckLauncherEmail.Change
+        If AniControlEnabled = 0 Then Setup.Set(sender.Tag, sender.Checked)
+    End Sub
+    Private Shared Sub TextBoxChange(sender As MyTextBox, e As Object) Handles TextLogoText.ValidatedTextChanged, TextCustomNet.ValidatedTextChanged
+        If AniControlEnabled = 0 Then Setup.Set(sender.Tag, sender.Text)
+    End Sub
+    Private Shared Sub RadioBoxChange(sender As MyRadioBox, e As Object) Handles RadioLogoType0.Check, RadioLogoType1.Check, RadioLogoType2.Check, RadioLogoType3.Check, RadioLauncherTheme0.Check, RadioLauncherTheme1.Check, RadioLauncherTheme2.Check, RadioLauncherTheme3.Check, RadioLauncherTheme4.Check, RadioLauncherTheme5.Check, RadioLauncherTheme6.Check, RadioLauncherTheme7.Check, RadioLauncherTheme8.Check, RadioLauncherTheme9.Check, RadioLauncherTheme10.Check, RadioLauncherTheme11.Check, RadioLauncherTheme12.Check, RadioLauncherTheme13.Check, RadioLauncherTheme14.Check, RadioCustomType0.Check, RadioCustomType1.Check, RadioCustomType2.Check, RadioCustomType3.Check
+        If AniControlEnabled = 0 Then Setup.Set(sender.Tag.ToString.Split("/")(0), Val(sender.Tag.ToString.Split("/")(1)))
+    End Sub
+
+    '背景图片
+    Private Sub BtnUIBgOpen_Click(sender As Object, e As EventArgs) Handles BtnBackgroundOpen.Click
+        OpenExplorer("""" & Path & "PCL\Pictures""")
+    End Sub
+    Private Sub BtnBackgroundRefresh_Click(sender As Object, e As EventArgs) Handles BtnBackgroundRefresh.Click
+        BackgroundRefresh(True, True)
+    End Sub
+    Public Sub BackgroundRefreshUI(Show As Boolean, Count As Integer)
+        If IsNothing(PanBackgroundOpacity) Then Exit Sub
+        If Show Then
+            PanBackgroundOpacity.Visibility = Visibility.Visible
+            PanBackgroundBlur.Visibility = Visibility.Visible
+            PanBackgroundSuit.Visibility = Visibility.Visible
+            BtnBackgroundClear.Visibility = Visibility.Visible
+            CardBackground.Title = "背景图片(" & Count & " 张)"
+        Else
+            PanBackgroundOpacity.Visibility = Visibility.Collapsed
+            PanBackgroundBlur.Visibility = Visibility.Collapsed
+            PanBackgroundSuit.Visibility = Visibility.Collapsed
+            BtnBackgroundClear.Visibility = Visibility.Collapsed
+            CardBackground.Title = "背景图片"
+        End If
+        CardBackground.TriggerForceResize()
+    End Sub
+    Private Sub BtnBackgroundClear_Click(sender As Object, e As EventArgs) Handles BtnBackgroundClear.Click
+        If MyMsgBox("即将删除背景图片文件夹中的所有文件。" & vbCrLf & "此操作不可撤销,是否确定?", "警告",, "取消", IsWarn:=True) = 1 Then
+            DeleteDirectory(Path & "PCL\Pictures")
+            BackgroundRefresh(False, True)
+            Hint("背景图片已清空!", HintType.Finish)
+        End If
+    End Sub
+    ''' <summary>
+    ''' 刷新背景图片及设置页 UI。
+    ''' </summary>
+    ''' <param name="IsHint">是否显示刷新提示。</param>
+    ''' <param name="Refresh">是否刷新图片显示。</param>
+    Public Shared Sub BackgroundRefresh(IsHint As Boolean, Refresh As Boolean)
+        Try
+
+            '获取可用的图片文件
+            Directory.CreateDirectory(Path & "PCL\Pictures\")
+            Dim Pic As New List(Of String)
+            For Each File In EnumerateFiles(Path & "PCL\Pictures\")
+                If File.Extension.ToLower <> ".ini" AndAlso File.Extension.ToLower <> ".db" Then '文件夹可能会被加入 .ini 和 thumbs.db
+                    Pic.Add(File.FullName)
+                End If
+            Next
+            '加载
+            If Not Pic.Any() Then
+                If Refresh Then
+                    If FrmMain.ImgBack.Visibility = Visibility.Collapsed Then
+                        If IsHint Then Hint("未检测到可用背景图片!", HintType.Critical)
+                    Else
+                        FrmMain.ImgBack.Visibility = Visibility.Collapsed
+                        If IsHint Then Hint("背景图片已清除!", HintType.Finish)
+                    End If
+                End If
+                If Not IsNothing(FrmSetupUI) Then FrmSetupUI.BackgroundRefreshUI(False, 0)
+            Else
+                If Refresh Then
+                    Dim Address As String = RandomOne(Pic)
+                    Try
+                        Log("[UI] 加载背景图片:" & Address)
+                        FrmMain.ImgBack.Background = New MyBitmap(Address)
+                        Setup.Load("UiBackgroundSuit", True)
+                        FrmMain.ImgBack.Visibility = Visibility.Visible
+                        If IsHint Then Hint("背景图片已刷新:" & GetFileNameFromPath(Address), HintType.Finish, False)
+                    Catch ex As Exception
+                        If ex.Message.Contains("参数无效") Then
+                            Log("刷新背景图片失败,该图片文件可能并非标准格式。" & vbCrLf &
+                                "你可以尝试使用画图打开该文件并重新保存,这会让图片变为标准格式。" & vbCrLf &
+                                "文件:" & Address, LogLevel.Msgbox)
+                        Else
+                            Log(ex, "刷新背景图片失败(" & Address & ")", LogLevel.Msgbox)
+                        End If
+                    End Try
+                End If
+                If Not IsNothing(FrmSetupUI) Then FrmSetupUI.BackgroundRefreshUI(True, Pic.Count)
+            End If
+
+        Catch ex As Exception
+            Log(ex, "刷新背景图片时出现未知错误", LogLevel.Feedback)
+        End Try
+    End Sub
+
+
+    '顶部栏
+    Private Sub BtnLogoChange_Click(sender As Object, e As EventArgs) Handles BtnLogoChange.Click
+        Dim FileName As String = SelectFile("常用图片文件(*.png;*.jpg;*.gif)|*.png;*.jpg;*.gif", "选择图片")
+        If FileName = "" Then Exit Sub
+        Try
+            '拷贝文件
+            File.Delete(Path & "PCL\Logo.png")
+            CopyFile(FileName, Path & "PCL\Logo.png")
+            '设置当前显示
+            FrmMain.ImageTitleLogo.Source = New MyBitmap(Path & "PCL\Logo.png")
+        Catch ex As Exception
+            If ex.Message.Contains("参数无效") Then
+                Log("改变标题栏图片失败,该图片文件可能并非标准格式。" & vbCrLf &
+                    "你可以尝试使用画图打开该文件并重新保存,这会让图片变为标准格式。", LogLevel.Msgbox)
+            Else
+                Log(ex, "设置标题栏图片失败", LogLevel.Msgbox)
+            End If
+            FrmMain.ImageTitleLogo.Source = Nothing
+        End Try
+    End Sub
+    Private Sub RadioLogoType3_Check(sender As Object, e As RouteEventArgs) Handles RadioLogoType3.PreviewCheck
+        If Not (AniControlEnabled = 0 AndAlso e.RaiseByMouse) Then Exit Sub
+Refresh:
+        '已有图片则不再选择
+        If File.Exists(Path & "PCL\Logo.png") Then
+            Try
+                FrmMain.ImageTitleLogo.Source = New MyBitmap(Path & "PCL\Logo.png")
+            Catch ex As Exception
+                If ex.Message.Contains("参数无效") Then
+                    Log("调整标题栏图片失败,该图片文件可能并非标准格式。" & vbCrLf &
+                    "你可以尝试使用画图打开该文件并重新保存,这会让图片变为标准格式。", LogLevel.Msgbox)
+                Else
+                    Log(ex, "调整标题栏图片失败", LogLevel.Msgbox)
+                End If
+                FrmMain.ImageTitleLogo.Source = Nothing
+                e.Handled = True
+                Try
+                    File.Delete(Path & "PCL\Logo.png")
+                Catch exx As Exception
+                    Log(exx, "清理错误的标题栏图片失败", LogLevel.Msgbox)
+                End Try
+            End Try
+            Exit Sub
+        End If
+        '没有图片则要求选择
+        Dim FileName As String = SelectFile("常用图片文件(*.png;*.jpg;*.gif)|*.png;*.jpg;*.gif", "选择图片")
+        If FileName = "" Then
+            FrmMain.ImageTitleLogo.Source = Nothing
+            e.Handled = True
+        Else
+            Try
+                '拷贝文件
+                File.Delete(Path & "PCL\Logo.png")
+                CopyFile(FileName, Path & "PCL\Logo.png")
+                GoTo Refresh
+            Catch ex As Exception
+                Log(ex, "复制标题栏图片失败", LogLevel.Msgbox)
+            End Try
+        End If
+    End Sub
+    Private Sub BtnLogoDelete_Click(sender As Object, e As EventArgs) Handles BtnLogoDelete.Click
+        Try
+            File.Delete(Path & "PCL\Logo.png")
+            RadioLogoType1.SetChecked(True, True, True)
+            Hint("标题栏图片已清空!", HintType.Finish)
+        Catch ex As Exception
+            Log(ex, "清空标题栏图片失败", LogLevel.Msgbox)
+        End Try
+    End Sub
+
+    '背景音乐
+    Private Sub BtnMusicOpen_Click(sender As Object, e As EventArgs) Handles BtnMusicOpen.Click
+        OpenExplorer("""" & Path & "PCL\Musics""")
+    End Sub
+    Private Sub BtnMusicRefresh_Click(sender As Object, e As EventArgs) Handles BtnMusicRefresh.Click
+        MusicRefreshPlay(True)
+    End Sub
+    Public Sub MusicRefreshUI()
+        If PanBackgroundOpacity Is Nothing Then Exit Sub
+        If MusicAllList.Any Then
+            PanMusicVolume.Visibility = Visibility.Visible
+            PanMusicDetail.Visibility = Visibility.Visible
+            BtnMusicClear.Visibility = Visibility.Visible
+            CardMusic.Title = "背景音乐(" & EnumerateFiles(Path & "PCL\Musics\").Count & " 首)"
+        Else
+            PanMusicVolume.Visibility = Visibility.Collapsed
+            PanMusicDetail.Visibility = Visibility.Collapsed
+            BtnMusicClear.Visibility = Visibility.Collapsed
+            CardMusic.Title = "背景音乐"
+        End If
+        CardMusic.TriggerForceResize()
+    End Sub
+    Private Sub BtnMusicClear_Click(sender As Object, e As EventArgs) Handles BtnMusicClear.Click
+        If MyMsgBox("即将删除背景音乐文件夹中的所有文件。" & vbCrLf & "此操作不可撤销,是否确定?", "警告",, "取消", IsWarn:=True) = 1 Then
+            RunInThread(Sub()
+                            Hint("正在删除背景音乐……")
+                            '停止播放音乐
+                            MusicNAudio = Nothing
+                            MusicWaitingList = New List(Of String)
+                            MusicAllList = New List(Of String)
+                            Thread.Sleep(200)
+                            '删除文件
+                            Try
+                                DeleteDirectory(Path & "PCL\Musics")
+                                Hint("背景音乐已删除!", HintType.Finish)
+                            Catch ex As Exception
+                                Log(ex, "删除背景音乐失败", LogLevel.Msgbox)
+                            End Try
+                            Try
+                                Directory.CreateDirectory(Path & "PCL\Musics")
+                                RunInUi(Sub() MusicRefreshPlay(False))
+                            Catch ex As Exception
+                                Log(ex, "重建背景音乐文件夹失败", LogLevel.Msgbox)
+                            End Try
+                        End Sub)
+        End If
+    End Sub
+    Private Sub CheckMusicStart_Change() Handles CheckMusicStart.Change
+        If AniControlEnabled <> 0 Then Exit Sub
+        If CheckMusicStart.Checked Then CheckMusicStop.Checked = False
+    End Sub
+    Private Sub CheckMusicStop_Change() Handles CheckMusicStop.Change
+        If AniControlEnabled <> 0 Then Exit Sub
+        If CheckMusicStop.Checked Then CheckMusicStart.Checked = False
+    End Sub
+
+    '主页
+    Private Sub BtnCustomFile_Click(sender As Object, e As EventArgs) Handles BtnCustomFile.Click
+        Try
+            If File.Exists(Path & "PCL\Custom.xaml") Then
+                If MyMsgBox("当前已存在布局文件,继续生成教学文件将会覆盖现有布局文件!", "覆盖确认", "继续", "取消", IsWarn:=True) = 2 Then Exit Sub
+            End If
+            WriteFile(Path & "PCL\Custom.xaml", GetResources("Custom"))
+            Hint("教学文件已生成!", HintType.Finish)
+            OpenExplorer("/select,""" & Path & "PCL\Custom.xaml""")
+        Catch ex As Exception
+            Log(ex, "生成教学文件失败", LogLevel.Feedback)
+        End Try
+    End Sub
+    Private Sub BtnCustomRefresh_Click() Handles BtnCustomRefresh.Click
+        FrmLaunchRight.ForceRefresh()
+        Hint("已刷新主页!", HintType.Finish)
+    End Sub
+    Private Sub BtnCustomTutorial_Click(sender As Object, e As EventArgs) Handles BtnCustomTutorial.Click
+        MyMsgBox("1. 点击 生成教学文件 按钮,这会在 PCL 文件夹下生成 Custom.xaml 布局文件。" & vbCrLf &
+                 "2. 使用记事本等工具打开这个文件并进行修改,修改完记得保存。" & vbCrLf &
+                 "3. 点击 刷新主页 按钮,查看主页现在长啥样了。" & vbCrLf &
+                 vbCrLf &
+                 "你可以在生成教学文件后直接刷新主页,对照着进行修改,更有助于理解。" & vbCrLf &
+                 "直接将自定义主页文件拖进 PCL 窗口也可以快捷加载。", "主页自定义教程")
+    End Sub
+
+    '主题
+    Private Sub LabLauncherTheme5Unlock_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles LabLauncherTheme5Unlock.MouseLeftButtonUp
+        RadioLauncherTheme5Gray.Opacity -= 0.23
+        RadioLauncherTheme5.Opacity += 0.23
+        AniStart({
+                     AaOpacity(RadioLauncherTheme5Gray, 1, 1000),
+                     AaOpacity(RadioLauncherTheme5, -1, 1000)
+                 }, "ThemeUnlock")
+        If RadioLauncherTheme5Gray.Opacity < 0.08 Then
+            ThemeUnlock(5, UnlockHint:="隐藏主题 玄素黑 已解锁!")
+            AniStop("ThemeUnlock")
+            RadioLauncherTheme5.Checked = True
+        End If
+    End Sub
+    Private Sub LabLauncherTheme11Click_MouseLeftButtonUp() Handles LabLauncherTheme11Click.MouseLeftButtonUp, RadioLauncherTheme11.MouseRightButtonUp
+        If LabLauncherTheme11Click.Visibility = Visibility.Collapsed OrElse LabLauncherTheme11Click.ToolTip.ToString.Contains("点击") Then
+            If MyMsgBox(
+                "1. 不爬取或攻击相关服务或网站,不盗取相关账号,没有谜题可以或需要以此来解决。" & vbCrLf &
+                "2. 不得篡改或损毁相关公开信息,请尽量让它们保持原状。" & vbCrLf &
+                "3. 在你感到迷茫的时候,看看回声洞可能会给你带来惊喜。" & vbCrLf & vbCrLf &
+                "若违规,可能会被从任意相关群中踢出!",
+                "解密游戏的基本规则", "我知道了", "恕我拒绝") = 1 Then
+                MyMsgBox("你需要用自己的智慧来找到下一步的线索……" & vbCrLf &
+                         "初始线索:gnp.dorC61\60\20\0202\moc.x1xa.2s\\:sp" & "T".ToLower & "th", "解密游戏") '防止触发病毒检测规则
+            End If
+        End If
+    End Sub
+    Private Sub LabLauncherTheme8Copy_MouseRightButtonUp() Handles LabLauncherTheme8Copy.MouseRightButtonUp
+        OpenWebsite("https://afdian.net/a/LTCat")
+    End Sub
+    Private Sub LabLauncherTheme9Copy_MouseRightButtonUp() Handles LabLauncherTheme9Copy.MouseRightButtonUp
+        PageOtherLeft.TryFeedback()
+    End Sub
+
+    '主题自定义
+    Private Sub RadioLauncherTheme14_Change(sender As Object, e As RouteEventArgs) Handles RadioLauncherTheme14.Changed
+        If RadioLauncherTheme14.Checked Then
+            If LabLauncherHue.Visibility = Visibility.Visible Then Exit Sub
+            LabLauncherHue.Visibility = Visibility.Visible
+            SliderLauncherHue.Visibility = Visibility.Visible
+            LabLauncherSat.Visibility = Visibility.Visible
+            SliderLauncherSat.Visibility = Visibility.Visible
+            LabLauncherDelta.Visibility = Visibility.Visible
+            SliderLauncherDelta.Visibility = Visibility.Visible
+            LabLauncherLight.Visibility = Visibility.Visible
+            SliderLauncherLight.Visibility = Visibility.Visible
+        Else
+            If LabLauncherHue.Visibility = Visibility.Collapsed Then Exit Sub
+            LabLauncherHue.Visibility = Visibility.Collapsed
+            SliderLauncherHue.Visibility = Visibility.Collapsed
+            LabLauncherSat.Visibility = Visibility.Collapsed
+            SliderLauncherSat.Visibility = Visibility.Collapsed
+            LabLauncherDelta.Visibility = Visibility.Collapsed
+            SliderLauncherDelta.Visibility = Visibility.Collapsed
+            LabLauncherLight.Visibility = Visibility.Collapsed
+            SliderLauncherLight.Visibility = Visibility.Collapsed
+        End If
+        CardLauncher.TriggerForceResize()
+    End Sub
+    Private Sub HSL_Change() Handles SliderLauncherHue.Change, SliderLauncherLight.Change, SliderLauncherSat.Change, SliderLauncherDelta.Change
+        If AniControlEnabled <> 0 OrElse SliderLauncherSat Is Nothing OrElse Not SliderLauncherSat.IsLoaded Then Exit Sub
+        ThemeRefresh()
+    End Sub
+
+#Region "功能隐藏"
+
+    Private Shared _HiddenForceShow As Boolean = False
+    ''' <summary>
+    ''' 是否强制显示被禁用的功能。
+    ''' </summary>
+    Public Shared Property HiddenForceShow As Boolean
+        Get
+            Return _HiddenForceShow
+        End Get
+        Set(value As Boolean)
+            _HiddenForceShow = value
+            HiddenRefresh()
+        End Set
+    End Property
+
+    ''' <summary>
+    ''' 更新功能隐藏带来的显示变化。
+    ''' </summary>
+    Public Shared Sub HiddenRefresh() Handles Me.Loaded
+        If FrmMain.PanTitleSelect Is Nothing OrElse Not FrmMain.PanTitleSelect.IsLoaded Then Exit Sub
+        Try
+            '顶部栏
+            If Not HiddenForceShow AndAlso Setup.Get("UiHiddenPageDownload") AndAlso Setup.Get("UiHiddenPageLink") AndAlso Setup.Get("UiHiddenPageSetup") AndAlso Setup.Get("UiHiddenPageOther") Then
+                '顶部栏已被全部隐藏
+                FrmMain.PanTitleSelect.Visibility = Visibility.Collapsed
+            Else
+                '顶部栏未被全部隐藏
+                FrmMain.PanTitleSelect.Visibility = Visibility.Visible
+                FrmMain.BtnTitleSelect1.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenPageDownload"), Visibility.Collapsed, Visibility.Visible)
+                FrmMain.BtnTitleSelect2.Visibility = Visibility.Collapsed 'If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenPageLink"), Visibility.Collapsed, Visibility.Visible)
+                FrmMain.BtnTitleSelect3.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenPageSetup"), Visibility.Collapsed, Visibility.Visible)
+                FrmMain.BtnTitleSelect4.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenPageOther"), Visibility.Collapsed, Visibility.Visible)
+            End If
+            '功能
+            FrmLaunchLeft.RefreshButtonsUI()
+            If FrmSetupUI IsNot Nothing Then
+                FrmSetupUI.CardSwitch.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenFunctionHidden"), Visibility.Collapsed, Visibility.Visible)
+            End If
+            '设置子页面
+            If FrmSetupLeft IsNot Nothing Then
+                FrmSetupLeft.ItemLaunch.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenSetupLaunch"), Visibility.Collapsed, Visibility.Visible)
+                FrmSetupLeft.ItemUI.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenSetupUi"), Visibility.Collapsed, Visibility.Visible)
+                FrmSetupLeft.ItemLink.Visibility = Visibility.Collapsed 'If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenSetupLink"), Visibility.Collapsed, Visibility.Visible)
+                FrmSetupLeft.ItemSystem.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenSetupSystem"), Visibility.Collapsed, Visibility.Visible)
+                '隐藏左边选择卡
+                Dim AvaliableCount As Integer = 0
+                If Not Setup.Get("UiHiddenSetupLaunch") Then AvaliableCount += 1
+                If Not Setup.Get("UiHiddenSetupUi") Then AvaliableCount += 1
+                If Not Setup.Get("UiHiddenSetupLink") Then AvaliableCount += 1
+                If Not Setup.Get("UiHiddenSetupSystem") Then AvaliableCount += 1
+                FrmSetupLeft.PanItem.Visibility = If(AvaliableCount < 2 AndAlso Not HiddenForceShow, Visibility.Collapsed, Visibility.Visible)
+            End If
+            '更多子页面
+            Dim OtherAvaliableCount As Integer = 0
+            If Not Setup.Get("UiHiddenOtherHelp") Then OtherAvaliableCount += 1
+            If Not Setup.Get("UiHiddenOtherAbout") Then OtherAvaliableCount += 1
+            If Not Setup.Get("UiHiddenOtherTest") Then OtherAvaliableCount += 1
+            If Not Setup.Get("UiHiddenOtherFeedback") Then OtherAvaliableCount += 1
+            If Not Setup.Get("UiHiddenOtherVote") Then OtherAvaliableCount += 1
+            If FrmOtherLeft IsNot Nothing Then
+                FrmOtherLeft.ItemHelp.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenOtherHelp"), Visibility.Collapsed, Visibility.Visible)
+                FrmOtherLeft.ItemFeedback.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenOtherFeedback"), Visibility.Collapsed, Visibility.Visible)
+                FrmOtherLeft.ItemVote.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenOtherVote"), Visibility.Collapsed, Visibility.Visible)
+                FrmOtherLeft.ItemAbout.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenOtherAbout"), Visibility.Collapsed, Visibility.Visible)
+                FrmOtherLeft.ItemTest.Visibility = If(Not HiddenForceShow AndAlso Setup.Get("UiHiddenOtherTest"), Visibility.Collapsed, Visibility.Visible)
+                '隐藏左边选择卡
+                FrmOtherLeft.PanItem.Visibility = If(OtherAvaliableCount < 2 AndAlso Not HiddenForceShow, Visibility.Collapsed, Visibility.Visible)
+            End If
+            If OtherAvaliableCount = 1 AndAlso Not HiddenForceShow Then
+                If Not Setup.Get("UiHiddenOtherHelp") Then
+                    FrmMain.BtnTitleSelect4.Text = "帮助"
+                ElseIf Not Setup.Get("UiHiddenOtherAbout") Then
+                    FrmMain.BtnTitleSelect4.Text = "关于"
+                Else
+                    FrmMain.BtnTitleSelect4.Text = "百宝箱"
+                End If
+            Else
+                FrmMain.BtnTitleSelect4.Text = "更多"
+            End If
+            '各个页面的入口
+            If FrmMain.PageCurrent = FormMain.PageType.VersionSelect Then FrmSelectRight.BtnEmptyDownload_Loaded()
+            If FrmMain.PageCurrent = FormMain.PageType.Launch Then FrmLaunchLeft.RefreshButtonsUI()
+            If FrmMain.PageCurrent = FormMain.PageType.VersionSetup AndAlso FrmVersionModDisabled IsNot Nothing Then FrmVersionModDisabled.BtnDownload_Loaded()
+            '备注
+            If FrmSetupUI IsNot Nothing Then FrmSetupUI.CardSwitch.Title = If(HiddenForceShow, "功能隐藏(已暂时关闭,按 F12 以重新启用)", "功能隐藏")
+        Catch ex As Exception
+            Log(ex, "刷新功能隐藏项目失败", LogLevel.Feedback)
+        End Try
+    End Sub
+
+    'UI 协同改变
+    Private Sub HiddenSetupMain() Handles CheckHiddenPageSetup.Change
+        '设置主页面
+        If CheckHiddenPageSetup.Checked Then
+            '开启
+            CheckHiddenSetupLaunch.Checked = True
+            CheckHiddenSetupSystem.Checked = True
+            CheckHiddenSetupLink.Checked = True
+            CheckHiddenSetupUI.Checked = True
+        Else
+            '关闭
+            If Setup.Get("UiHiddenSetupLaunch") AndAlso Setup.Get("UiHiddenSetupUi") AndAlso Setup.Get("UiHiddenSetupSystem") AndAlso Setup.Get("UiHiddenSetupLink") Then
+                CheckHiddenSetupLaunch.Checked = False
+                CheckHiddenSetupSystem.Checked = False
+                CheckHiddenSetupLink.Checked = False
+                CheckHiddenSetupUI.Checked = False
+            End If
+        End If
+    End Sub
+    Private Sub HiddenSetupSub() Handles CheckHiddenSetupLaunch.Change, CheckHiddenSetupSystem.Change, CheckHiddenSetupLink.Change, CheckHiddenSetupUI.Change
+        '设置子页面
+        If Setup.Get("UiHiddenSetupLaunch") AndAlso Setup.Get("UiHiddenSetupUi") AndAlso Setup.Get("UiHiddenSetupSystem") AndAlso Setup.Get("UiHiddenSetupLink") Then
+            '已被全部隐藏
+            CheckHiddenPageSetup.Checked = True
+        Else
+            '未被全部隐藏
+            CheckHiddenPageSetup.Checked = False
+        End If
+    End Sub
+    Private Sub HiddenOtherMain() Handles CheckHiddenPageOther.Change
+        '更多主页面
+        If CheckHiddenPageOther.Checked Then
+            '开启
+            CheckHiddenOtherAbout.Checked = True
+            CheckHiddenOtherTest.Checked = True
+            CheckHiddenOtherFeedback.Checked = True
+            CheckHiddenOtherVote.Checked = True
+            CheckHiddenOtherHelp.Checked = True
+        Else
+            '关闭
+            If Setup.Get("UiHiddenOtherHelp") AndAlso Setup.Get("UiHiddenOtherAbout") AndAlso Setup.Get("UiHiddenOtherTest") AndAlso
+                Setup.Get("UiHiddenOtherVote") AndAlso Setup.Get("UiHiddenOtherFeedback") Then
+                CheckHiddenOtherAbout.Checked = False
+                CheckHiddenOtherTest.Checked = False
+                CheckHiddenOtherFeedback.Checked = False
+                CheckHiddenOtherVote.Checked = False
+                CheckHiddenOtherHelp.Checked = False
+            End If
+        End If
+    End Sub
+    Private Sub HiddenOtherSub(sender As Object, user As Boolean) Handles CheckHiddenOtherHelp.Change, CheckHiddenOtherAbout.Change, CheckHiddenOtherTest.Change
+        '更多子页面(有具体内容的)
+        If Setup.Get("UiHiddenOtherHelp") AndAlso Setup.Get("UiHiddenOtherAbout") AndAlso Setup.Get("UiHiddenOtherTest") Then
+            '已被全部隐藏
+            CheckHiddenPageOther.Checked = True
+        Else
+            '未被全部隐藏
+            CheckHiddenPageOther.Checked = False
+        End If
+        '修改无具体内容的项
+        If Not user Then Exit Sub
+        If Setup.Get("UiHiddenOtherHelp") AndAlso Setup.Get("UiHiddenOtherAbout") AndAlso Setup.Get("UiHiddenOtherTest") Then
+            CheckHiddenOtherFeedback.Checked = True
+            CheckHiddenOtherVote.Checked = True
+        End If
+    End Sub
+    Private Sub HiddenOtherNet(sender As Object, user As Boolean) Handles CheckHiddenOtherFeedback.Change, CheckHiddenOtherVote.Change
+        '更多子页面(无具体内容的)
+        If Not user Then Exit Sub
+        If Setup.Get("UiHiddenOtherHelp") AndAlso Setup.Get("UiHiddenOtherAbout") AndAlso Setup.Get("UiHiddenOtherTest") AndAlso
+            (Not Setup.Get("UiHiddenOtherFeedback") OrElse Not Setup.Get("UiHiddenOtherVote")) Then
+            CheckHiddenOtherAbout.Checked = False
+            CheckHiddenOtherTest.Checked = False
+            CheckHiddenOtherHelp.Checked = False
+        End If
+    End Sub
+
+    '警告提示
+    Private Sub HiddenHint(sender As Object, user As Boolean) Handles CheckHiddenFunctionHidden.Change, CheckHiddenPageSetup.Change, CheckHiddenSetupUI.Change
+        If AniControlEnabled = 0 AndAlso sender.Checked Then Hint("按 F12 即可暂时关闭功能隐藏设置。千万别忘了,要不然设置就改不回来了……")
+    End Sub
+
+#End Region
+
+    '公开预览版
+    Private Sub BtnLauncherDonate_Click(sender As Object, e As EventArgs) Handles BtnLauncherDonate.Click
+        OpenWebsite("https://afdian.net/a/LTCat")
+    End Sub
+
+    '滑动条
+    Private Sub SliderLoad()
+        SliderMusicVolume.GetHintText = Function(Value As Integer)
+                                            Return Math.Ceiling(Value * 0.1) & "%"
+                                        End Function
+        SliderLauncherOpacity.GetHintText = Function(Value As Integer)
+                                                Return Math.Round(40 + Value * 0.1) & "%"
+                                            End Function
+        SliderLauncherHue.GetHintText = Function(Value As Integer)
+                                            Return Value & "°"
+                                        End Function
+        SliderLauncherSat.GetHintText = Function(Value As Integer)
+                                            Return Value & "%"
+                                        End Function
+        SliderLauncherDelta.GetHintText = Function(Value As Integer)
+                                              If Value > 90 Then
+                                                  Return "+" & (Value - 90)
+                                              ElseIf Value = 90 Then
+                                                  Return 0
+                                              Else
+                                                  Return Value - 90
+                                              End If
+                                          End Function
+        SliderLauncherLight.GetHintText = Function(Value As Integer)
+                                              If Value > 20 Then
+                                                  Return "+" & (Value - 20)
+                                              ElseIf Value = 20 Then
+                                                  Return 0
+                                              Else
+                                                  Return Value - 20
+                                              End If
+                                          End Function
+        SliderBackgroundOpacity.GetHintText = Function(Value As Integer)
+                                                  Return Math.Round(Value * 0.1) & "%"
+                                              End Function
+        SliderBackgroundBlur.GetHintText = Function(Value As Integer)
+                                               Return Value & " 像素"
+                                           End Function
+    End Sub
+
+End Class

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

@@ -1,4 +1,6 @@
-Public Class PageSpeedLeft
+Imports System.Threading.Tasks
+
+Public Class PageSpeedLeft
     Private Const WatcherInterval As Integer = 300
     Private Const WatcherInterval As Integer = 300
 
 
     '初始化
     '初始化
@@ -59,8 +61,8 @@
         If FrmSpeedRight Is Nothing OrElse FrmSpeedRight.PanMain Is Nothing Then Exit Sub
         If FrmSpeedRight Is Nothing OrElse FrmSpeedRight.PanMain Is Nothing Then Exit Sub
         Try
         Try
             SyncLock LoaderTaskbarLock
             SyncLock LoaderTaskbarLock
-                For i = 0 To LoaderTaskbar.Count - 1
-                    TaskRefresh(LoaderTaskbar(i))
+                For Each Loader In LoaderTaskbar.ToList
+                    TaskRefresh(Loader)
                 Next
                 Next
             End SyncLock
             End SyncLock
         Catch ex As Exception
         Catch ex As Exception
@@ -110,7 +112,7 @@
 #Region "进度不同,更新卡片"
 #Region "进度不同,更新卡片"
                             Try
                             Try
                                 If Card.Children.Count < LoaderList.Count * 2 Then
                                 If Card.Children.Count < LoaderList.Count * 2 Then
-                                    Log("刷新下载管理卡片失败:卡片中仅有 " & Card.Children.Count & " 个子项", LogLevel.Debug)
+                                    Log($"[Watcher] 刷新下载管理卡片 {Loader.Name} 失败:卡片中仅有 {Card.Children.Count} 个子项,要求至少有 {LoaderList.Count * 2} 个子项", LogLevel.Debug)
                                     Exit Try
                                     Exit Try
                                 End If
                                 End If
                                 Dim Row As Integer = 0
                                 Dim Row As Integer = 0
@@ -137,7 +139,7 @@
                                     Row += 1
                                     Row += 1
                                 Next
                                 Next
                             Catch ex As Exception
                             Catch ex As Exception
-                                Log(ex, "刷新下载管理卡片失败", LogLevel.Feedback)
+                                Log(ex, $"刷新下载管理卡片 {Loader.Name} 失败", LogLevel.Feedback)
                             End Try
                             End Try
 #End Region
 #End Region
                     End Select
                     End Select
@@ -191,13 +193,18 @@
                     '添加取消按钮
                     '添加取消按钮
                     Dim Cancel As New MyIconButton With {.Name = "BtnCancel", .Logo = "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", .Height = 20, .Margin = New Thickness(0, 10, 10, 0), .LogoScale = 1.1, .HorizontalAlignment = HorizontalAlignment.Right, .VerticalAlignment = VerticalAlignment.Top}
                     Dim Cancel As New MyIconButton With {.Name = "BtnCancel", .Logo = "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", .Height = 20, .Margin = New Thickness(0, 10, 10, 0), .LogoScale = 1.1, .HorizontalAlignment = HorizontalAlignment.Right, .VerticalAlignment = VerticalAlignment.Top}
                     Card.Children.Add(Cancel)
                     Card.Children.Add(Cancel)
-                    AddHandler Cancel.Click, Sub(sender As MyIconButton, e As EventArgs)
-                                                 AniDispose(sender, False)
-                                                 AniDispose(Card, True, Sub() If FrmSpeedRight.PanMain.Children.Count = 0 AndAlso FrmMain.PageCurrent = FormMain.PageType.DownloadManager Then FrmMain.PageBack())
-                                                 RightCards.Remove(Loader.Name)
-                                                 Log($"[Watcher] 关闭下载管理卡片:{Loader.Name}")
-                                                 RunInThread(Sub() Loader.Abort())
-                                             End Sub
+                    AddHandler Cancel.Click,
+                    Sub(sender As MyIconButton, e As EventArgs)
+                        AniDispose(sender, False)
+                        AniDispose(Card, True, Sub() If FrmSpeedRight.PanMain.Children.Count = 0 AndAlso FrmMain.PageCurrent = FormMain.PageType.DownloadManager Then FrmMain.PageBack())
+                        RightCards.Remove(Loader.Name)
+                        SyncLock LoaderTaskbarLock
+                            LoaderTaskbar.Remove(Loader)
+                            Log($"[Taskbar] 由于下载管理卡片关闭,{Loader.Name} 已移出任务列表")
+                        End SyncLock
+                        Log($"[Watcher] 关闭下载管理卡片:{Loader.Name}")
+                        RunInThread(Sub() Loader.Abort())
+                    End Sub
                     '如果已经失败,再刷新一次,修改成失败的控件
                     '如果已经失败,再刷新一次,修改成失败的控件
                     If Loader.State = LoadState.Failed Then
                     If Loader.State = LoadState.Failed Then
                         Card.Tag = Nothing '避免重复导致刷新无效
                         Card.Tag = Nothing '避免重复导致刷新无效

+ 3 - 3
Plain Craft Launcher 2/Pages/PageVersion/PageVersionMod.xaml.vb

@@ -177,7 +177,7 @@
     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
             Dim Result = McModCheck(PageVersionLeft.Version, McModLoader.Output)
             Dim Result = McModCheck(PageVersionLeft.Version, McModLoader.Output)
-            If Result.Count > 0 Then
+            If Result.Any Then
                 MyMsgBox(Join(Result, vbCrLf & vbCrLf), "Mod 检查结果")
                 MyMsgBox(Join(Result, vbCrLf & vbCrLf), "Mod 检查结果")
             Else
             Else
                 Hint("Mod 检查完成,未发现任何问题!", HintType.Finish)
                 Hint("Mod 检查完成,未发现任何问题!", HintType.Finish)
@@ -620,13 +620,13 @@
                 If ModEntry.ModId IsNot Nothing Then
                 If ModEntry.ModId IsNot Nothing Then
                     DebugInfo.Add("Mod ID:" & ModEntry.ModId)
                     DebugInfo.Add("Mod ID:" & ModEntry.ModId)
                 End If
                 End If
-                If ModEntry.Dependencies.Count > 0 Then
+                If ModEntry.Dependencies.Any Then
                     DebugInfo.Add("依赖于:")
                     DebugInfo.Add("依赖于:")
                     For Each Dep In ModEntry.Dependencies
                     For Each Dep In ModEntry.Dependencies
                         DebugInfo.Add(" - " & Dep.Key & If(Dep.Value Is Nothing, "", ",版本:" & Dep.Value))
                         DebugInfo.Add(" - " & Dep.Key & If(Dep.Value Is Nothing, "", ",版本:" & Dep.Value))
                     Next
                     Next
                 End If
                 End If
-                If DebugInfo.Count > 0 Then
+                If DebugInfo.Any Then
                     ContentLines.Add("")
                     ContentLines.Add("")
                     ContentLines.AddRange(DebugInfo)
                     ContentLines.AddRange(DebugInfo)
                 End If
                 End If

+ 2 - 2
Plain Craft Launcher 2/Pages/PageVersion/PageVersionSetup.xaml.vb

@@ -33,7 +33,7 @@
             If Indie = -1 Then
             If Indie = -1 Then
                 Dim ModFolder As New DirectoryInfo(PageVersionLeft.Version.Path & "mods\")
                 Dim ModFolder As New DirectoryInfo(PageVersionLeft.Version.Path & "mods\")
                 Dim SaveFolder As New DirectoryInfo(PageVersionLeft.Version.Path & "saves\")
                 Dim SaveFolder As New DirectoryInfo(PageVersionLeft.Version.Path & "saves\")
-                If (ModFolder.Exists AndAlso ModFolder.EnumerateFiles.Count > 0) OrElse (SaveFolder.Exists AndAlso SaveFolder.EnumerateFiles.Count > 0) Then
+                If (ModFolder.Exists AndAlso ModFolder.EnumerateFiles.Any) OrElse (SaveFolder.Exists AndAlso SaveFolder.EnumerateFiles.Any) Then
                     '自动开启
                     '自动开启
                     Setup.Set("VersionArgumentIndie", 1, Version:=PageVersionLeft.Version)
                     Setup.Set("VersionArgumentIndie", 1, Version:=PageVersionLeft.Version)
                     Log("[Setup] 已自动开启单版本隔离:" & PageVersionLeft.Version.Name)
                     Log("[Setup] 已自动开启单版本隔离:" & PageVersionLeft.Version.Name)
@@ -438,7 +438,7 @@ PreFin:
             Log(ex, "更新版本设置 Java 下拉框失败", LogLevel.Feedback)
             Log(ex, "更新版本设置 Java 下拉框失败", LogLevel.Feedback)
         End Try
         End Try
         '更新选择项
         '更新选择项
-        If SelectedItem Is Nothing AndAlso JavaList.Count > 0 Then
+        If SelectedItem Is Nothing AndAlso JavaList.Any Then
             If SelectedBySetup = "" Then
             If SelectedBySetup = "" Then
                 SelectedItem = ComboArgumentJava.Items(1) '选中 “自动选择”
                 SelectedItem = ComboArgumentJava.Items(1) '选中 “自动选择”
             Else
             Else

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

@@ -35,7 +35,7 @@
     <PlatformTarget>AnyCPU</PlatformTarget>
     <PlatformTarget>AnyCPU</PlatformTarget>
     <DebugSymbols>true</DebugSymbols>
     <DebugSymbols>true</DebugSymbols>
     <DebugType>full</DebugType>
     <DebugType>full</DebugType>
-    <DefineDebug>true</DefineDebug>
+    <DefineDebug>false</DefineDebug>
     <DefineTrace>false</DefineTrace>
     <DefineTrace>false</DefineTrace>
     <IncrementalBuild>true</IncrementalBuild>
     <IncrementalBuild>true</IncrementalBuild>
     <OutputPath>D:\Minecraft\程序\</OutputPath>
     <OutputPath>D:\Minecraft\程序\</OutputPath>
@@ -185,6 +185,9 @@
     <Compile Include="Controls\MyIconTextButton.xaml.vb">
     <Compile Include="Controls\MyIconTextButton.xaml.vb">
       <DependentUpon>MyIconTextButton.xaml</DependentUpon>
       <DependentUpon>MyIconTextButton.xaml</DependentUpon>
     </Compile>
     </Compile>
+
+
+
     <Compile Include="Modules\Minecraft\ModComp.vb" />
     <Compile Include="Modules\Minecraft\ModComp.vb" />
     <Compile Include="Modules\Minecraft\ModJava.vb" />
     <Compile Include="Modules\Minecraft\ModJava.vb" />
     <Compile Include="Modules\Minecraft\ModMod.vb" />
     <Compile Include="Modules\Minecraft\ModMod.vb" />
@@ -466,6 +469,7 @@
       <DependentUpon>FormMain.xaml</DependentUpon>
       <DependentUpon>FormMain.xaml</DependentUpon>
       <SubType>Code</SubType>
       <SubType>Code</SubType>
     </Compile>
     </Compile>
+
     <Page Include="Modules\Minecraft\MyLocalModItem.xaml">
     <Page Include="Modules\Minecraft\MyLocalModItem.xaml">
       <Generator>MSBuild:Compile</Generator>
       <Generator>MSBuild:Compile</Generator>
       <SubType>Designer</SubType>
       <SubType>Designer</SubType>
@@ -916,5 +920,9 @@
   <ItemGroup>
   <ItemGroup>
     <Resource Include="Images\Heads\MCBBS.png" />
     <Resource Include="Images\Heads\MCBBS.png" />
   </ItemGroup>
   </ItemGroup>
+  <ItemGroup>
+
+  </ItemGroup>
+  <ItemGroup />
   <Import Project="$(MSBuildToolsPath)\Microsoft.VisualBasic.targets" />
   <Import Project="$(MSBuildToolsPath)\Microsoft.VisualBasic.targets" />
 </Project>
 </Project>

BIN
Plain Craft Launcher 2/Resources/Help.zip


Fichier diff supprimé car celui-ci est trop grand
+ 150 - 150
Plain Craft Launcher 2/Resources/ModData.txt


+ 3 - 3
Plain Craft Launcher 2/app.manifest

@@ -29,8 +29,8 @@
       <!-- Windows Vista -->
       <!-- Windows Vista -->
       <!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
       <!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
 
 
-      <!-- Windows 7 -->
-      <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
+      <!-- Windows 7 --><!--
+      <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->
 
 
       <!-- Windows 8 -->
       <!-- Windows 8 -->
       <!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
       <!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
@@ -38,7 +38,7 @@
       <!-- Windows 8.1 -->
       <!-- Windows 8.1 -->
       <!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
       <!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
 
 
-      <!-- Windows 10 -->
+      <!-- Windows 10+ -->
       <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
       <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
 
 
     </application>
     </application>

+ 16 - 10
README.md

@@ -1,21 +1,27 @@
-## Plain Craft Launcher 2 源代码库
+# Plain Craft Launcher 源代码库
 
 
-这里提供了 PCL 的大多数源代码,包括 UI 库、动画模块、下载模块、Minecraft 启动模块等!
+[![Stars](https://img.shields.io/github/stars/Hex-Dragon/PCL2?style=flat&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEiIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZD0iTTggLjI1YS43NS43NSAwIDAgMSAuNjczLjQxOGwxLjg4MiAzLjgxNSA0LjIxLjYxMmEuNzUuNzUgMCAwIDEgLjQxNiAxLjI3OWwtMy4wNDYgMi45Ny43MTkgNC4xOTJhLjc1MS43NTEgMCAwIDEtMS4wODguNzkxTDggMTIuMzQ3bC0zLjc2NiAxLjk4YS43NS43NSAwIDAgMS0xLjA4OC0uNzlsLjcyLTQuMTk0TC44MTggNi4zNzRhLjc1Ljc1IDAgMCAxIC40MTYtMS4yOGw0LjIxLS42MTFMNy4zMjcuNjY4QS43NS43NSAwIDAgMSA4IC4yNVoiIGZpbGw9IiNlYWM1NGYiLz48L3N2Zz4=&logoSize=auto&label=Stars&labelColor=666666&color=eac54f)](https://github.com/Hex-Dragon/PCL2/)
+[![Issues](https://img.shields.io/github/issues/Hex-Dragon/PCL2?style=flat&label=Issues&labelColor=666666&color=1a7f37)](https://github.com/Hex-Dragon/PCL2/issues)
+[![爱发电](https://img.shields.io/badge/赞助-%E7%88%B1%E5%8F%91%E7%94%B5-946ce6?style=flat&labelColor=666666&logoSize=auto)](https://afdian.net/@LTCat)
+
+<br/>
 
 
-PCL 的代码绝大多数其实都是几年前学生时代的产物了……那时候英语还不过关……所以经常出现奇葩命名,还有令人高血压的高耦合啊,没做单例啊,瞎勾八乱糊啊之类的问题……额,我也不可能把这一堆玩意儿再从头写一次,各位就基于能跑就行的原则凑合凑合着看吧,求求别喷了(run
+## 介绍
+
+这里提供了 PCL 的大多数源代码,包括 UI 库、动画模块、下载模块、Minecraft 启动模块等!
 
 
-你也可以丢 Pull Request,虽然这个源代码库并不能直接编译,但一些简单的修改应该还是没问题的……
+PCL 的代码绝大多数其实都是几年前学生时代的产物了……那时候英语还不过关……所以经常出现奇葩命名,还有令人高血压的高耦合啊,没做单例啊,瞎勾八乱糊啊之类的问题……额,我也不可能把这一堆玩意儿再从头写一次,各位就基于能跑就行的原则凑合凑合着看吧,求求别喷了……
 
 
- 
+<br/>
 
 
-**相关内容:**
+## 相关内容
 - [PCL 功能投票](https://github.com/Hex-Dragon/PCL2/discussions/2):来参加投票吧,开发者会优先处理票数较高的帖子!
 - [PCL 功能投票](https://github.com/Hex-Dragon/PCL2/discussions/2):来参加投票吧,开发者会优先处理票数较高的帖子!
 - [程序下载](https://afdian.net/p/0164034c016c11ebafcb52540025c377):下载免费的正式版 PCL。
 - [程序下载](https://afdian.net/p/0164034c016c11ebafcb52540025c377):下载免费的正式版 PCL。
 - [帮助文档库](https://github.com/LTCatt/PCL2Help):PCL 帮助文档在 GitHub 上的存储库(是的,帮助库在另一个 Repo……)
 - [帮助文档库](https://github.com/LTCatt/PCL2Help):PCL 帮助文档在 GitHub 上的存储库(是的,帮助库在另一个 Repo……)
 
 
- 
+<br/>
 
 
-**一些无趣的小细节:**
-- 别想在这里面找到解密游戏的线索,那部分的代码被我抠掉了(笑)
-- 哦,对,解锁隐藏主题的相关代码也都被抠掉了,所以别想着用翻源代码这种歪门邪道来绕过解密了.jpg
+## 一些无趣的小细节
+- 别想在这里面找到解密游戏的线索,那部分的代码被我抠掉了
+- 解锁隐藏主题的相关代码也被抠掉了,所以别想着用翻源代码这种歪门邪道来绕过解密了.jpg
 - 源代码库并不是即时更新的,而是在每次 PCL 发布更新时(手动)同步一次
 - 源代码库并不是即时更新的,而是在每次 PCL 发布更新时(手动)同步一次

BIN
最新正式版.zip


Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff