Просмотр исходного кода

Merge branch 'Hex-Dragon:main' into main

Silverteal 2 лет назад
Родитель
Сommit
9402523e01
67 измененных файлов с 5157 добавлено и 1550 удалено
  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
 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
             ModDevelop.Start()
         Catch ex As Exception
-            Log(ex, "开发者模式测试出错", LogLevel.Feedback)
+            Log(ex, "开发者模式测试出错", LogLevel.Msgbox)
         End Try
     End Sub
 #End If
 
     '开始
     Private Sub Application_Startup(sender As Object, e As StartupEventArgs) Handles Me.Startup
-        ApplicationStartTick = GetTimeTick()
-        SecretOnApplicationStart()
         Try
+            SecretOnApplicationStart()
             '检查参数调用
             If e.Args.Length > 0 Then
                 If e.Args(0) = "--update" Then
                     '自动更新
                     UpdateReplace(e.Args(1), e.Args(2).Trim(""""), e.Args(3).Trim(""""), e.Args(4))
                     Environment.Exit(Result.Cancel)
-                    'ElseIf e.Args(0).StartsWithF("--link") Then
-                    '    '稍作等待后切换到联机页面
-                    '    Thread.Sleep(1000)
-                    '    FormMain.IsLinkRestart = True
                 ElseIf e.Args(0).StartsWithF("--memory") Then
                     '内存优化
                     Dim Ram = My.Computer.Info.AvailablePhysicalMemory
@@ -104,9 +99,10 @@ Public Class Application
             '日志初始化
             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
                 MyMsgBox("PCL 正在临时文件夹运行,设置、游戏存档等很可能无法保存,且部分功能会无法使用或出错。" & vbCrLf & "请将 PCL 从压缩文件中解压,或是更换文件夹后再继续使用!", "环境警告", "我知道了", IsWarn:=True)
@@ -129,7 +125,12 @@ Public Class Application
 #End If
             AniControlEnabled += 1
         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)
         End Try
     End Sub
@@ -212,6 +213,7 @@ Public Class Application
                 Dim MsJson As JObject = GetJson(Setup.Get("LoginMsJson"))
                 MsJson.Remove(sender.Tag)
                 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)
             Case McLoginType.Legacy
                 '离线
@@ -220,7 +222,7 @@ Public Class Application
                 Names.Remove(sender.Tag)
                 Setup.Set("LoginLegacyName", Join(Names, "¨"))
                 FrmLoginLegacy.ComboName.ItemsSource = Names
-                FrmLoginLegacy.ComboName.Text = If(Names.Count > 0, Names(0), "")
+                FrmLoginLegacy.ComboName.Text = If(Names.Any, Names(0), "")
             Case Else
                 '第三方
                 Dim Token As String = GetStringFromEnum(Setup.Get("LoginType"))
@@ -238,12 +240,12 @@ Public Class Application
                 Select Case Token
                     Case "Nide"
                         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"
                         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 Sub

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

@@ -13,8 +13,26 @@
             MainBorder.Child = value
         End Set
     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()
@@ -24,7 +42,7 @@
         End Get
         Set(value As String)
             SetValue(TitleProperty, value)
-            If MainTextBlock IsNot Nothing Then MainTextBlock.Text = value
+            If _MainTextBlock IsNot Nothing Then MainTextBlock.Text = value
         End Set
     End Property
     Public Shared ReadOnly TitleProperty As DependencyProperty = DependencyProperty.Register("Title", GetType(String), GetType(MyCard), New PropertyMetadata(""))
@@ -40,10 +58,11 @@
         Children.Add(MainGrid)
     End Sub
     Private IsLoad As Boolean = False
-    Private Sub MyCard_Loaded() Handles Me.Loaded
+    Private Sub Init() Handles Me.Loaded
         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.SetResourceReference(TextBlock.ForegroundProperty, "ColorBrush1")
             MainTextBlock.SetBinding(TextBlock.TextProperty, New Binding("Title") With {.Source = Me, .Mode = BindingMode.OneWay})
@@ -55,7 +74,6 @@
             MainSwap.SetResourceReference(Shapes.Path.FillProperty, "ColorBrush1")
             MainGrid.Children.Add(MainSwap)
         End If
-        IsLoad = True
         '改变默认的折叠
         If IsSwaped AndAlso SwapControl IsNot Nothing Then
             MainSwap.RenderTransform = New RotateTransform(If(SwapLogoRight, 270, 0))
@@ -130,7 +148,7 @@
                                                                     FrmDownloadInstall.MinecraftSelected(sender, e)
                                                                 End Sub, False))
                 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
                         '存在重复的名称(#1344)
                         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))
                     End If
                 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
                         '存在重复的名称(#1344)
                         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")
         End If
         ColumnPaddingRight.Width = New GridLength(MinPaddingRight)
-        If EventType = "打开帮助" Then
+        If EventType = "打开帮助" AndAlso Not (Title <> "" AndAlso Info <> "") Then '#3266
             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
                 Log(ex, "设置帮助 MyListItem 失败", LogLevel.Msgbox)
             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
         If e.Delta = 0 OrElse ActualHeight = 0 OrElse ScrollableHeight = 0 Then Exit Sub
         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
         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))
         '统计更新日志条目
 #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
             FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "支持更新 Mod"))
             FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "支持查看可更新的 Mod 的更新日志"))
@@ -96,7 +108,25 @@ Public Class FormMain
         '3:BUG+ IMP* FEAT-
         '2:BUG* IMP-
         '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"))
             BugCount += 1
         End If
@@ -229,7 +259,7 @@ Public Class FormMain
         Next
         If SortedFeatures.Count > 10 Then FeatureCount += SortedFeatures.Count - 10
         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(BugCount > 0, "修复了 " & BugCount & " 个 Bug", "") &
                 ",详见完整更新日志")
@@ -306,7 +336,7 @@ Public Class FormMain
         Setup.Load("UiBackgroundBlur")
         Setup.Load("UiLogoType")
         Setup.Load("UiHiddenPageDownload")
-        BackgroundRefresh(False, True)
+        PageSetupUI.BackgroundRefresh(False, True)
         MusicRefreshPlay(False, True)
         '扩展按钮
         BtnExtraDownload.ShowCheck = AddressOf BtnExtraDownload_ShowCheck
@@ -352,44 +382,46 @@ Public Class FormMain
             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) 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")
         'Timer 启动
         AniStart()
         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"))
-                                   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")
     End Sub
@@ -430,6 +462,8 @@ Public Class FormMain
             Log("[Start] 最高版本号从 " & LowerVersionCode & " 升高到 " & VersionCode)
         End If
 #End If
+        '被移除的窗口设置选项
+        If Setup.Get("LaunchArgumentWindowType") = 5 Then Setup.Set("LaunchArgumentWindowType", 1)
         '修改主题设置项名称
         If LowerVersionCode <= 207 Then
             Dim UnlockedTheme As New List(Of String) From {"2"}
@@ -495,12 +529,13 @@ Public Class FormMain
         If SendWarning AndAlso HasDownloadingTask() 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
                 Exit Sub
             End If
@@ -516,17 +551,19 @@ Public Class FormMain
                     PanBack.RenderTransform = New TransformGroup() With {.Children = New TransformCollection({TransformRotate, TransformPos, TransformScale})}
                     AniStart({
                         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) 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)
                     }, "Form Close")
                 Else
@@ -653,7 +690,7 @@ Public Class FormMain
         If e.ChangedButton = MouseButton.XButton1 OrElse e.ChangedButton = MouseButton.XButton2 Then TriggerPageBack()
     End Sub
     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()
         Else
             PageBack()
@@ -761,7 +798,7 @@ Public Class FormMain
         RunInNewThread(
         Sub()
             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
                 Hint("请拖入一个文件,而非文件夹!", HintType.Critical)
@@ -774,14 +811,14 @@ Public Class FormMain
             If FilePathList.Count > 1 Then
                 '必须要求全部为 jar 文件
                 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)
                         Exit Sub
                     End If
                 Next
             End If
             '自定义主页
-            Dim Extension As String = FilePath.Split(".").Last.ToLower
+            Dim Extension As String = FilePath.After(".").ToLower
             If Extension = "xaml" Then
                 Log("[System] 文件后缀为 XAML,作为自定义主页加载")
                 If File.Exists(Path & "PCL\Custom.xaml") Then
@@ -1176,8 +1213,11 @@ Install:
     ''' 通过点击返回按钮或手动触发返回来改变页面。
     ''' </summary>
     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
 
     '实际处理页面切换
@@ -1357,23 +1397,22 @@ Install:
     End Sub
     Private Sub PanMainLeft_Resize(NewWidth As Double)
         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 NewWidth > 0 Then
                 '宽度足够,显示
                 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
                 '宽度不足,隐藏
                 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
         Else
             RectLeftBackground.Width = NewWidth

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

@@ -11,12 +11,12 @@ Public Module ModBase
 #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
-    Public Const VersionCode As Integer = 319 'Release
+    Public Const VersionCode As Integer = 323 'Release
 #Else
-    Public Const VersionCode As Integer = 320 'Snapshot
+    Public Const VersionCode As Integer = 325 'Snapshot
 #End If
     '自动生成的版本信息
     Public Const VersionDisplayName As String = VersionBranchName & " " & VersionBaseName
@@ -26,7 +26,7 @@ Public Module ModBase
 #ElseIf BETA Then
     Public Const VersionBranchName As String = "Release"
     Public Const VersionBranchCode As String = "50"
-#ElseIf DEBUG Then
+#Else
     Public Const VersionBranchName As String = "Debug"
     Public Const VersionBranchCode As String = "100"
 #End If
@@ -58,7 +58,7 @@ Public Module ModBase
     ''' <summary>
     ''' 程序的打开计时。
     ''' </summary>
-    Public ApplicationStartTick As Long
+    Public ApplicationStartTick As Long = GetTimeTick()
     ''' <summary>
     ''' 程序打开时的时间。
     ''' </summary>
@@ -76,6 +76,10 @@ Public Module ModBase
     ''' </summary>
     Public Is32BitSystem As Boolean = Not Environment.Is64BitOperatingSystem
     ''' <summary>
+    ''' 是否使用 GBK 编码。
+    ''' </summary>
+    Public IsGBKEncoding As Boolean = Encoding.Default.CodePage = 936
+    ''' <summary>
     ''' 操作系统版本。Win10 为 10.0。
     ''' </summary>
     Public OsVersion As Version = Environment.OSVersion.Version
@@ -591,7 +595,7 @@ Public Module ModBase
         Try
             Dim parentKey As Microsoft.Win32.RegistryKey, softKey As Microsoft.Win32.RegistryKey
             parentKey = My.Computer.Registry.CurrentUser
-            softKey = parentKey.OpenSubKey("Software\PCL", True)
+            softKey = parentKey.OpenSubKey("Software\" & RegFolder, True)
             If softKey Is Nothing Then
                 ReadReg = DefaultValue '不存在则返回默认值
             Else
@@ -612,8 +616,8 @@ Public Module ModBase
         Try
             Dim parentKey As Microsoft.Win32.RegistryKey, softKey As Microsoft.Win32.RegistryKey
             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)
         Catch ex As Exception
             Log(ex, "写入注册表出错:" & Key, If(ShowException, LogLevel.Hint, LogLevel.Developer))
@@ -726,8 +730,8 @@ Public Module ModBase
     Public Function GetFileNameFromPath(FilePath As String) As String
         FilePath = FilePath.Replace("/", "\")
         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(FilePath.LastIndexOfF("\") + 1)
         Dim length As Integer = FilePath.Length
         If length = 0 Then Throw New Exception("不包含文件名:" & FilePath)
         If length > 250 Then Throw New PathTooLongException("文件名过长:" & FilePath)
@@ -1215,43 +1219,35 @@ Re:
     ''' <summary>
     ''' 尝试根据后缀名判断文件种类并解压文件,支持 gz 与 zip,会尝试将 jar 以 zip 方式解压。
     ''' 会尝试创建,但不会清空目标文件夹。
-    ''' 成功返回 True,并非压缩文件或失败返回 False。
     ''' </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>
     ''' 删除文件夹,返回删除的文件个数。通过参数选择是否抛出异常。
@@ -1342,7 +1338,7 @@ Re:
         Loop
         DescList = DescList.Distinct.ToList
         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
@@ -1455,7 +1451,8 @@ Re:
         Try
             Return JsonConvert.DeserializeObject(Data, New JsonSerializerSettings With {.DateTimeZoneHandling = DateTimeZoneHandling.Local})
         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 Function
 
@@ -1541,6 +1538,57 @@ Re:
         Next
         Return tmp.ToString()
     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>
     ''' 高速的 StartsWith。
@@ -1571,12 +1619,26 @@ Re:
         Return Str.IndexOf(SubStr, If(IgnoreCase, StringComparison.OrdinalIgnoreCase, StringComparison.Ordinal))
     End Function
     ''' <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。
     ''' </summary>
     <Extension> <MethodImpl(MethodImplOptions.AggressiveInlining)>
     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))
     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>
     ''' 输入 And 字符不会报错的 Val。
@@ -1815,15 +1877,11 @@ Re:
     ''' <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)
         For i = 0 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
             ResultArray.Add(Arr(i))
 NextElement:
@@ -2071,9 +2129,9 @@ NextElement:
     ''' 按照既定的函数进行选择排序。
     ''' </summary>
     ''' <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)
-        While List.Count > 0
+        While List.Any
             Dim Highest = List(0)
             For i = 1 To List.Count - 1
                 If SortRule(List(i), Highest) Then Highest = List(i)
@@ -2646,7 +2704,7 @@ Retry:
     ''' </summary>
     Public Function Shuffle(Of T)(array As IList(Of T)) As IList(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)
             Shuffle.Add(array(i))
             array.RemoveAt(i)

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

@@ -266,7 +266,7 @@
         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
             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
@@ -571,7 +571,7 @@ Restart:
     Public LoaderTaskbarProgress As Double = 0 '平滑后的进度
     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)
         SyncLock LoaderTaskbarLock
             LoaderTaskbar.Add(Loader)
@@ -587,11 +587,11 @@ Restart:
                 '外显任务是否已经全部完成
                 Dim IsAllDownloadTaskCompleted As Boolean = True
                 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
                 '若单个任务已中止或全部任务已完成,则刷新并移除
                 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)
                         LoaderTaskbar.Remove(Task)
                         Log($"[Taskbar] {Task.Name} 已移出任务列表")
@@ -628,7 +628,6 @@ Restart:
             Dim Total As Double = 0, Count As Integer = 0
             SyncLock LoaderTaskbarLock
                 For Each Task In LoaderTaskbar.ToList
-                    If Not Task.Show Then Continue For
                     Total += Task.Progress
                     Count += 1 '避免多线程影响导致计数出错
                 Next

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

@@ -1,4 +1,5 @@
 Imports System.Net
+Imports PCL.ModNet
 
 Public Module ModNet
     Public Const NetDownloadEnd As String = ".PCLDownloading"
@@ -235,11 +236,11 @@ RequestFinished:
     ''' 以多线程下载网页文件的方式获取网页源代码。
     ''' </summary>
     ''' <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)})
         Try
-            NewTask.WaitForExitTime(Timeout, TimeoutMessage:="连接服务器超时(第一下载源:" & Urls(0) & ")")
+            NewTask.WaitForExitTime(Timeout, TimeoutMessage:="连接服务器超时(第一下载源:" & Urls.First & ")")
             NetGetCodeByDownload = ReadFile(Temp)
             File.Delete(Temp)
         Finally
@@ -448,7 +449,7 @@ RequestFinished:
     ''' <summary>
     ''' 速度下限。
     ''' </summary>
-    Public NetTaskSpeedLimitLow As Long = 1024 * 1024L
+    Public NetTaskSpeedLimitLow As Long = 256 * 1024L '256K/s
     ''' <summary>
     ''' 速度上限。若无限制则为 -1。
     ''' </summary>
@@ -516,7 +517,7 @@ RequestFinished:
         ''' </summary>
         Finish = 6
         ''' <summary>
-        ''' 已失败。
+        ''' 已失败或中断。
         ''' </summary>
         [Error] = 7
     End Enum
@@ -601,7 +602,7 @@ RequestFinished:
                 SyncLock Task.LockChain
                     If NextThread Is Nothing Then
                         If Task.IsUnknownSize Then
-                            Return 1024 * 1024 * 1024 '1G
+                            Return 5 * 1024 * 1024 * 1024L '5G
                         Else
                             Return Task.FileSize - 1
                         End If
@@ -616,7 +617,7 @@ RequestFinished:
         ''' </summary>
         Public ReadOnly Property DownloadUndone As Long
             Get
-                Return DownloadEnd + 1 - (DownloadStart + DownloadDone)
+                Return DownloadEnd - (DownloadStart + DownloadDone) + 1
             End Get
         End Property
         ''' <summary>
@@ -715,7 +716,7 @@ RequestFinished:
                         CurrentSource = Sources(Id)
                     End While
                     Return CurrentSource
-                ElseIf SourcesOnce.Count > 0 Then
+                ElseIf SourcesOnce.Any Then
                     '仅存在单线程可用源
                     Return SourcesOnce(0)
                 Else
@@ -728,7 +729,7 @@ RequestFinished:
         ''' 是否已经没有可用源了。
         ''' </summary>
         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
                 For Each Source As NetSource In Sources
                     If Not Source.IsFailed Then Return False
@@ -754,15 +755,12 @@ RequestFinished:
         ''' 导致下载失败的原因。
         ''' </summary>
         Public Ex As New List(Of Exception)
-        '''' <summary>
-        '''' 文件进行整体重试的计数。
-        '''' </summary>
-        'Private RetryCount As Integer = 0
 
         ''' <summary>
         ''' 作为文件组成部分的线程链表。
+        ''' 如果没有线程,可以为 Nothing。
         ''' </summary>
-        Private Threads As NetThread
+        Public Threads As NetThread
 
         ''' <summary>
         ''' 文件的总大小。若为 -2 则为未获取,若为 -1 则为无法获取准确大小。
@@ -783,7 +781,7 @@ RequestFinished:
         ''' <summary>
         ''' 为不需要分割的小文件进行临时存储。
         ''' </summary>
-        Private SmailFileCache As List(Of Byte)
+        Private SmailFileCache As Queue(Of Byte)
 
         ''' <summary>
         ''' 文件的已下载大小。
@@ -844,10 +842,10 @@ RequestFinished:
                     Case NetState.Get
                         Return 0.04
                     Case NetState.Download
-                        '正在下载中,对应 5% ~ 99%
+                        '正在下载中,对应 5% ~ 98%
                         Dim OriginalProgress As Double = If(IsUnknownSize, 0.5, DownloadDone / Math.Max(FileSize, 1))
                         OriginalProgress = 1 - (1 - OriginalProgress) ^ 0.9
-                        Return OriginalProgress * 0.94 + 0.05
+                        Return OriginalProgress * 0.93 + 0.05
                     Case NetState.Merge
                         Return 0.99
                     Case NetState.Finish, NetState.Error
@@ -878,14 +876,13 @@ RequestFinished:
         End Property
 
         Private Const FilePieceLimit As Long = 256 * 1024
-        Public ReadOnly RandomCode As Integer = RandomInteger(0, 999999)
         Public ReadOnly LockCount As New Object
         Public ReadOnly LockState As New Object
         Public ReadOnly LockChain As New Object
         Public ReadOnly LockSource 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
             Dim file = TryCast(obj, NetFile)
             Return file IsNot Nothing AndAlso Uuid = file.Uuid
@@ -914,14 +911,15 @@ RequestFinished:
 
         ''' <summary>
         ''' 尝试开始一个新的下载线程。
+        ''' 如果失败,返回 Nothing。
         ''' </summary>
-        Public Function TryBeginThread() As Boolean
+        Public Function TryBeginThread() As NetThread
             Try
 
                 '条件检测
                 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
                     If State < NetState.Connect Then State = NetState.Connect
                 End SyncLock
@@ -936,7 +934,7 @@ RequestFinished:
                     '单线程
                     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:
                         SmailFileCache = Nothing
@@ -952,14 +950,8 @@ Capture:
                     If Threads Is Nothing Then
                         StartPosition = 0
                         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
                     '寻找失败点
                     For Each Thread As NetThread In Threads
@@ -971,24 +963,25 @@ Capture:
                     Next
                     '是否禁用多线程,以及规定碎片大小
                     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
                     For Each Thread As NetThread In Threads
                         If Thread.DownloadUndone > FilePieceMax.DownloadUndone Then FilePieceMax = Thread
                     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
                     StartSource = GetSource()
 
                     '开始线程
 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()
                     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
                     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
                 End SyncLock
                 Th.Start(ThreadInfo)
-                Return True
+                Return ThreadInfo
 
             Catch ex As Exception
                 Log(ex, "尝试开始下载线程失败(" & If(LocalName, "Nothing") & ")", LogLevel.Hint)
-                Return False
+                Return Nothing
             End Try
         End Function
         ''' <summary>
@@ -1040,35 +1033,45 @@ StartThread:
                 HttpRequest.Timeout = Timeout
                 HttpRequest.AddRange(Info.DownloadStart)
                 SecretHeadersSign(Info.Source.Url, HttpRequest, UseBrowserUserAgent)
+                Dim ContentLength As Long = 0
                 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
-                            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
                             FileSize = -1 : IsUnknownSize = True
-                            Log("[Download] " & LocalName & " " & Info.Uuid & "#:文件大小未知")
+                            Log($"[Download] {LocalName} {Info.Uuid}#:文件大小未知")
                         End If
-                    ElseIf ThreadFileSize < 0 Then
-                        Throw New Exception("获取片大小失败,结果为 " & ThreadFileSize & "。")
+                    ElseIf ContentLength < 0 Then
+                        Throw New Exception("获取片大小失败,结果为 " & ContentLength & "。")
                     ElseIf Info.IsFirstThread 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
-                            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
-                        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,进行剩余磁盘空间校验
-                        If ThreadFileSize > 50 * 1024 * 1024 Then
+                        If ContentLength > 50 * 1024 * 1024 Then
                             For Each Drive As DriveInfo In DriveInfo.GetDrives
                                 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
                                     Throw New Exception(DriveName & " 盘空间不足,无法进行下载。" & vbCrLf & "需要至少 " & GetString(RequiredSpace) & " 空间,但当前仅剩余 " & GetString(Drive.TotalFreeSpace) & "。" &
                                                     If(PathTemp.StartsWithF(DriveName), vbCrLf & vbCrLf & "下载时需要与文件同等大小的空间存放缓存,你可以在设置中调整缓存文件夹的位置。", ""))
@@ -1076,8 +1079,9 @@ StartThread:
                             Next
                         End If
                     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
                             If SourcesOnce.Contains(Info.Source) Then
                                 GoTo SourceBreak
@@ -1085,10 +1089,11 @@ StartThread:
                                 SourcesOnce.Add(Info.Source)
                             End If
                         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
+                    'Log($"[Download] {LocalName} {Info.Uuid}#:通过大小检查,文件大小 {FileSize},起始点 {Info.DownloadStart},ContentLength {ContentLength}")
                     Info.State = NetState.Get
                     SyncLock LockState
                         If State < NetState.Get Then State = NetState.Get
@@ -1096,7 +1101,7 @@ StartThread:
                     '创建缓存文件
                     If IsNoSplit Then
                         Info.Temp = Nothing
-                        SmailFileCache = New List(Of Byte)
+                        SmailFileCache = New Queue(Of Byte)
                     Else
                         Info.Temp = PathTemp & "Download\" & Uuid & "_" & Info.Uuid & "_" & RandomInteger(0, 999999) & ".tmp"
                         ResultStream = New FileStream(Info.Temp, FileMode.Create, FileAccess.Write, FileShare.Read)
@@ -1108,7 +1113,7 @@ StartThread:
                         Dim HttpData As Byte() = New Byte(16384) {}
                         HttpDataCount = HttpStream.Read(HttpData, 0, 16384)
                         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
                                 Threading.Thread.Sleep(16)
@@ -1147,9 +1152,15 @@ StartThread:
                                 Info.DownloadDone += RealDataCount
                                 If IsNoSplit Then
                                     If HttpData.Count = RealDataCount Then
-                                        SmailFileCache.AddRange(HttpData)
+                                        'SmailFileCache.AddRange(HttpData)
+                                        For Each B In HttpData
+                                            SmailFileCache.Enqueue(B)
+                                        Next
                                     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
                                 Else
                                     ResultStream.Write(HttpData, 0, RealDataCount)
@@ -1170,17 +1181,17 @@ StartThread:
                     End Using
                 End Using
 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
-                    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
                     '本线程完成
                     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
             Catch ex As Exception
                 '状态变更
@@ -1212,11 +1223,11 @@ SourceBreak:
                 'End SyncLock
                 Info.Source.Ex = ex
                 '根据情况判断,是否在多线程下禁用下载源(连续错误过多,或不支持断点续传)
-                'BMCLAPI 的部分源在高频率请求下会返回 403,所以不应因此禁用下载源
                 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
                     SyncLock LockSource
                         If Info.Source.Thread IsNot Nothing AndAlso Info.Source.Thread.Equals(Info) Then
@@ -1232,8 +1243,8 @@ Wrong:
                     End SyncLock
                     '本线程引发下载源被禁用
                     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
                             '没有可用源
                             Log("[Download] 文件 " & LocalName & " 已无可用下载源")
@@ -1265,6 +1276,7 @@ Wrong:
                 SyncLock NetTaskThreadCountLock
                     NetTaskThreadCount -= 1
                 End SyncLock
+                '可能在没有下载完的时候开始合并文件了,这造成了大多数合并失败
                 If ((FileSize >= 0 AndAlso DownloadDone >= FileSize) OrElse (FileSize = -1 AndAlso DownloadDone > 0)) AndAlso State < NetState.Merge Then Merge()
             End Try
         End Sub
@@ -1287,7 +1299,6 @@ Wrong:
             Try
 Retry:
                 SyncLock LockChain
-                    If ModeDebug Then Log("[Download] " & LocalName & ":正在合并文件")
                     '创建文件夹
                     If File.Exists(LocalPath) Then File.Delete(LocalPath)
                     Dim Info As New FileInfo(LocalPath)
@@ -1295,6 +1306,7 @@ Retry:
                     '合并文件
                     If IsNoSplit Then
                         '仅有一个线程,从缓存中输出
+                        If ModeDebug Then Log($"[Download] {LocalName}:下载结束,从缓存输出文件")
                         MergeFile = New FileStream(LocalPath, FileMode.Create)
                         AddWriter = New BinaryWriter(MergeFile)
                         AddWriter.Write(SmailFileCache.ToArray)
@@ -1302,17 +1314,19 @@ Retry:
                         MergeFile.Dispose() : MergeFile = Nothing
                     ElseIf Threads.DownloadDone = DownloadDone AndAlso Threads.Temp IsNot Nothing Then
                         '仅有一个文件,直接复制
+                        If ModeDebug Then Log($"[Download] {LocalName}:下载结束,仅有一个文件,无需合并")
                         CopyFile(Threads.Temp, LocalPath)
                     Else
                         '有多个线程,合并
+                        If ModeDebug Then Log($"[Download] {LocalName}:下载结束,开始合并文件")
                         MergeFile = New FileStream(LocalPath, FileMode.Create)
                         AddWriter = New BinaryWriter(MergeFile)
                         For Each Thread As NetThread In Threads
                             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)
-                                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
                         Next
                         AddWriter.Dispose() : AddWriter = Nothing
@@ -1329,9 +1343,9 @@ Retry:
                     '检查文件
                     Dim CheckResult As String = Check?.Check(LocalPath)
                     If CheckResult IsNot Nothing Then
-                        Log("[Download] " & Uuid & "# 的下载线程细节:")
+                        Log($"[Download] {LocalName} 合并文件失败,下载线程细节:")
                         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
                         Throw New Exception(CheckResult)
                     End If
@@ -1372,19 +1386,7 @@ Retry:
                 '凉凉
                 State = NetState.Error
             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
                 Task.OnFileFail(Me)
             Next
@@ -1393,24 +1395,24 @@ Retry:
         ''' 下载中断。
         ''' </summary>
         Public Sub Abort(CausedByTask As LoaderDownload)
-            '确认任务移除
+            '从特定任务中移除,如果它还属于其他任务,则继续下载
             SyncLock LockTasks
                 Tasks.Remove(CausedByTask)
-                If Tasks.Count > 0 Then Exit Sub
+                If Tasks.Any Then Exit Sub
             End SyncLock
             '确认中断
             SyncLock LockState
                 If State >= NetState.Finish Then Exit Sub
                 State = NetState.Error
             End SyncLock
+            InterruptAndDelete()
+        End Sub
+        Private Sub InterruptAndDelete()
+            On Error Resume Next
+            If File.Exists(LocalPath) Then File.Delete(LocalPath)
             SyncLock NetManager.LockRemain
                 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 Sub
 
@@ -1426,11 +1428,6 @@ Retry:
             SyncLock NetManager.LockRemain
                 NetManager.FileRemain -= 1
                 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
             SyncLock LockTasks
                 For Each Task In Tasks
@@ -1465,7 +1462,9 @@ Retry:
         Public Overrides Property Progress As Double
             Get
                 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
             End Get
             Set(value As Double)
@@ -1507,7 +1506,7 @@ FinishExCatch:
 #End Region
 
         ''' <summary>
-        ''' 刷新公开属性。由 NetManager 每 0.2 秒调用一次。
+        ''' 刷新公开属性。由 NetManager 每 0.1 秒调用一次。
         ''' </summary>
         Public Sub RefreshStat()
             '计算进度
@@ -1519,15 +1518,15 @@ FinishExCatch:
                         NewProgress += File.Progress * 0.2
                         TotalProgress += 0.2
                     Else
-                        NewProgress += File.Progress '* If(File.Progress < 1, 0.9, 1)
+                        NewProgress += File.Progress
                         TotalProgress += 1
                     End If
                 Next
             End SyncLock
             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
 
         Public Sub New(Name As String, FileTasks As List(Of NetFile))
@@ -1556,71 +1555,72 @@ NextElement:
             End SyncLock
             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
         Private Sub StartCopy(Files As List(Of NetFile), FolderList As List(Of String))
             Try
-                If ModeDebug Then Log("[Download] 检查线程分配文件数:" & Files.Count)
+                If ModeDebug Then Log("[Download] 检查线程分配文件数:" & Files.Count & ",线程名:" & Thread.CurrentThread.Name)
                 '试图从已存在的 Minecraft 文件夹中寻找目标文件
                 Dim ExistFiles As New List(Of KeyValuePair(Of NetFile, String)) '{NetFile, Target As String}
                 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("未知错误!")}
                 '寻找第一个不是 404 的下载源
                 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
                     For Each File In Files
                         If File.State = NetState.Error Then
                             [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
                         End If
                     Next
@@ -1794,9 +1793,9 @@ Retry:
         ''' </summary>
         Private SpeedLastDone As Long = 0
         ''' <summary>
-        ''' 最近 10 次下载速度的记录,较新的在后面。间隔为 0.2 秒,故记录了 2 秒的速度。
+        ''' 至多最近 30 次下载速度的记录,较新的在前面。
         ''' </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 刷新
         ''' <summary>
         ''' 当前的全局下载速度,单位为 Byte / 秒。
@@ -1808,28 +1807,29 @@ Retry:
 #End Region
 
         ''' <summary>
-        ''' 进度与下载速度由下载管理线程每隔约 0.2 秒刷新一次。
+        ''' 进度与下载速度由下载管理线程每隔约 0.1 秒刷新一次。
         ''' </summary>
         Private Sub RefreshStat()
             Try
                 Dim DeltaTime As Long = GetTimeTick() - RefreshStatLast
                 RefreshStatLast += DeltaTime
 #Region "刷新整体速度"
-                '计算即时速度
+                '计算瞬时速度
                 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
                 '计算用于显示的速度
-                '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
                     NetTaskSpeedLimitLow = Limit
                     Log("[Download] " & "速度下限已提升到 " & GetString(Limit))
@@ -1854,128 +1854,65 @@ Retry:
         Private Sub StartManager()
             If IsManagerStarted Then Exit Sub
             IsManagerStarted = True
-            RunInNewThread(
-            Sub()
+            Dim ThreadStarter =
+            Sub(Id As Integer) '0 或 1
                 Try
-                    Dim LastLoopTime As Long
                     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
                                 Files.Clear()
                             End SyncLock
                         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
                                 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
                 Catch ex As Exception
-                    Log(ex, "下载管理启动线程 2 出错", LogLevel.Assert)
+                    Log(ex, $"下载管理启动线程 {Id} 出错", LogLevel.Assert)
                 End Try
-            End Sub, "NetManager ThreadStarter Odd")
+            End Sub
+            RunInNewThread(Sub() ThreadStarter(0), "NetManager ThreadStarter 0")
+            RunInNewThread(Sub() ThreadStarter(1), "NetManager ThreadStarter 1")
             RunInNewThread(
             Sub()
                 Try
@@ -1989,8 +1926,8 @@ Retry:
                         NetTaskSpeedLimitLeftLast = TimeNow
                         '刷新公开属性
                         RefreshStat()
-                        '等待直至 170 ms
-                        Do While GetTimeTick() - LastLoopTime < 170
+                        '等待直至 80 ms
+                        Do While GetTimeTick() - LastLoopTime < 80
                             Thread.Sleep(10)
                         Loop
                     End While

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

@@ -149,7 +149,7 @@ Public Class ValidateExcept
     End Sub
     Public Overrides Function Validate(Str As String) As String
         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 = ""
                 Return ErrorMessage.Replace("%", Ch)
             End If
@@ -355,26 +355,3 @@ Fin:
         Return ""
     End Function
 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
                     CurseForgeFileIds = Files.Select(Function(f) f.Key).Distinct.ToList
                     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
                     ModLoaders = ModLoaders.Distinct.OrderBy(Of Integer)(Function(t) t).ToList
                     'Type
@@ -342,7 +342,7 @@
                     '搜索结果的键为 versions,获取特定工程的键为 game_versions
                     GameVersions = If(CType(If(Data("game_versions"), Data("versions")), JArray), New JArray).
                                        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
                     'Type
                     Select Case Data("project_type").ToString
@@ -414,10 +414,10 @@
             Json("Website") = Website
             If LastUpdate IsNot Nothing Then Json("LastUpdate") = LastUpdate
             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)
             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 '用于检查缓存时间
             Return Json
         End Function
@@ -576,9 +576,9 @@
                 '有中文翻译
                 '尝试将文本分为三段:Title (EnglishName) - Suffix
                 '检查时注意 Carpet:它没有中文译名,但有 Suffix
-                Title = TranslatedName.Split(" (").First.Split(" - ").First
+                Title = TranslatedName.Before(" (").Before(" - ")
                 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
                 If Suffix <> "" Then EnglishName = EnglishName.Replace(" - " & Suffix, "")
                 EnglishName = EnglishName.Replace(Title, "").Trim("("c, ")"c, " "c)
@@ -593,10 +593,10 @@
                 '添加后缀
                 If Suffix <> "" Then SubtitleList.Add(Suffix)
             End If
-            SubtitleList = SubtitleList.Distinct().ToList()
+            SubtitleList = SubtitleList.Distinct.ToList()
             '设置标题与描述
             Dim Subtitle As String = ""
-            If SubtitleList.Count > 0 Then
+            If SubtitleList.Any Then
                 For Each Ex In SubtitleList
                     Dim IsModLoaderDescription As Boolean =
                         Ex.ToLower.Contains("forge") OrElse Ex.ToLower.Contains("fabric") OrElse Ex.ToLower.Contains("quilt")
@@ -751,7 +751,7 @@ NoSubtitle:
                 Case CompType.ResourcePack
                     'FUTURE: Res
             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 Not String.IsNullOrEmpty(GameVersion) Then Address += "&gameVersion=" & GameVersion
             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"]]
             Dim Facets As New List(Of String)
             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 Not String.IsNullOrEmpty(GameVersion) Then Facets.Add($"[""versions:'{GameVersion}'""]")
             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 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("/", " ") & " "
-                SearchResult += SearchResults(i).Item.ChineseName.Split(" (").Last.TrimEnd(") ").Split(" - ").First.
+                SearchResult += SearchResults(i).Item.ChineseName.After(" (").TrimEnd(") ").Before(" - ").
                     Replace(":", "").Replace("(", "").Replace(")", "").ToLower.Replace("/", " ") & " "
             Next
             Log("[Comp] 中文搜索原始关键词:" & SearchResult, LogLevel.Developer)
@@ -1046,7 +1046,7 @@ Retry:
         '这样做的话,去重后将优先保留 CurseForge 内容(考虑到 CurseForge 热度更高)
         RawResults = RawResults.Where(Function(x) Not x.FromCurseForge).Concat(RawResults.Where(Function(x) x.FromCurseForge)).ToList
         '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
                                                   Not Storage.Results.Any(Function(b) r.IsLike(b))).ToList
@@ -1133,7 +1133,7 @@ Retry:
         '描述性信息
 
         ''' <summary>
-        ''' 文件描述名(并非文件名,是自定义的字段)。
+        ''' 文件描述名(并非文件名,是自定义的字段)。对很多 Mod,这会给出 Mod 版本号。
         ''' </summary>
         Public DisplayName As String
         ''' <summary>
@@ -1305,7 +1305,7 @@ Retry:
                     'GameVersions
                     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.")).
-                                               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
                         GameVersions = Sort(GameVersions, AddressOf VersionSortBoolean).ToList
                         If Type = CompType.ModPack Then GameVersions = New List(Of String) From {GameVersions(0)}
@@ -1356,9 +1356,9 @@ Retry:
             Dim Info As String = ""
             Select Case Type
                 Case CompType.Mod
-                    Info += If(ModLoaders.Count > 0,
+                    Info += If(ModLoaders.Any,
                         "适用于 " & 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
                     If GameVersions.All(Function(v) v.Contains("w")) Then
                         Info += $"游戏版本 {Join(GameVersions, "、")},"
@@ -1403,7 +1403,7 @@ Retry:
         '辅助函数
 
         Public Overrides Function ToString() As String
-            Return $"{Id}: {DisplayName}"
+            Return $"{Id}: {FileName}"
         End Function
 
     End Class
@@ -1444,15 +1444,15 @@ Retry:
             'Modrinth
             ResultJsonArray = NetGetCodeByRequestRetry($"https://api.modrinth.com/v2/project/{ProjectId}/version", Accept:="application/json", IsJson:=True)
         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 列表
         If TargetProject.Type <> CompType.Mod Then Return CompFilesCache(ProjectId)
         Dim Deps As List(Of String) = CompFilesCache(ProjectId).
             SelectMany(Function(f) f.RawDependencies).Distinct().ToList
         Dim UndoneDeps = Deps.Where(Function(f) Not CompProjectCache.ContainsKey(f)).ToList
         '获取前置 Mod 工程信息
-        If UndoneDeps.Count > 0 Then
+        If UndoneDeps.Any Then
             Log($"[Comp] {ProjectId} 文件列表中还需要获取信息的前置 Mod:{Join(UndoneDeps, ",")}")
             Dim Projects As JArray
             If TargetProject.FromCurseForge Then
@@ -1467,7 +1467,7 @@ Retry:
             Next
         End If
         '更新前置 Mod 信息
-        If Deps.Count > 0 Then
+        If Deps.Any Then
             For Each DepProject In Deps.Select(Function(id) CompProjectCache(id))
                 For Each File In CompFilesCache(ProjectId)
                     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 & ")")
             End Try
         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)
             Log("[Crash] 以下为游戏输出的最后一段内容:" & vbCrLf & RawOutput)
             WriteFile(TempFolder & "RawOutput.log", RawOutput)
@@ -110,21 +110,20 @@
     Public Sub Import(FilePath As String)
         Log("[Crash] 步骤 1:自主导入日志文件")
 
-        '解压压缩包
+        '尝试视作压缩包解压
         Try
             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)
-            Else
-                '解压失败
-                CopyFile(FilePath, TempFolder & "Temp\" & GetFileNameFromPath(FilePath))
-                Log("[Crash] 已复制导入的日志文件:" & FilePath)
+                GoTo Extracted
             End If
-        Catch ex As Exception
-            Log(ex, "解压导入文件中的压缩包失败")
+        Catch
         End Try
+        '并非压缩包
+        CopyFile(FilePath, TempFolder & "Temp\" & GetFileNameFromPath(FilePath))
+        Log("[Crash] 已复制导入的日志文件:" & FilePath)
+Extracted:
 
         '导入其中的日志文件
         For Each TargetFile As FileInfo In New DirectoryInfo(TempFolder & "Temp\").EnumerateFiles.ToList()
@@ -313,16 +312,30 @@
 
     End Function
     ''' <summary>
-    ''' 输出字符串的前后某些行,并统一行尾为 vbLf (\r)、删除空行。
+    ''' 输出字符串的前后某些行,并统一行尾为 vbLf (正则 \n)、删除空行和重复行。
     ''' </summary>
     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
-        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
         Return Result.ToString
     End Function
@@ -354,6 +367,7 @@
         光影或资源包导致OpenGL1282错误
         文件或内容校验失败
         确定Mod导致游戏崩溃
+        怀疑Mod导致游戏崩溃
         Mod配置文件导致游戏崩溃
         ModMixin失败
         Mod加载器报错
@@ -389,9 +403,9 @@
 
         '1. 精准日志匹配,中/高优先级
         AnalyzeCrit1()
-        If CrashReasons.Count > 0 Then GoTo Done
+        If CrashReasons.Any Then GoTo Done
         AnalyzeCrit2()
-        If CrashReasons.Count > 0 Then GoTo Done
+        If CrashReasons.Any Then GoTo Done
 
         '2. 堆栈分析
         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
                 Log("[Crash] 开始进行崩溃日志堆栈分析")
-                Dim StackLogs As String = LogCrash.Split("System Details").First
-                Keywords.AddRange(AnalyzeStackKeyword(StackLogs))
+                Keywords.AddRange(AnalyzeStackKeyword(LogCrash.Before("System Details")))
             End If
             'Minecraft 日志
             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 & " 个报错项")
-                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
             '虚拟机日志
             If LogHs IsNot Nothing Then
                 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))
             End If
             'Mod 名称分析
-            If Keywords.Count > 0 Then
+            If Keywords.Any Then
                 Dim Names = AnalyzeModName(Keywords)
                 If Names Is Nothing Then
                     AppendReason(CrashReason.堆栈分析发现关键字, Keywords)
@@ -442,7 +454,7 @@ Done:
         Else
             Log("[Crash] 步骤 3:分析崩溃原因完成,找到 " & CrashReasons.Count & " 条可能的原因")
             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
         End If
     End Sub
@@ -458,7 +470,7 @@ Done:
         Else
             CrashReasons.Add(Reason, New List(Of String)(If(Additional, {})))
         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
     Private Sub AppendReason(Reason As CrashReason, Additional As String)
         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("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("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.NoSuchMethodError: sun.security.util.ManifestEntryVerifier") Then AppendReason(CrashReason.低版本Forge与高版本Java不兼容)
             If LogMc.Contains("1282: Invalid operation") Then AppendReason(CrashReason.光影或资源包导致OpenGL1282错误)
@@ -531,6 +543,7 @@ Done:
             '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("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))
             'Mod 缺少前置
             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("Pixel format not accelerated") 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 导致的崩溃
             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))
                 Else
                     AppendReason(CrashReason.Mod加载器报错, If(RegexSeek(LogCrash, "(?<=Failure message: )[\w\W]+?(?=\tMod)"), "").Replace(vbTab, " ").TrimEnd((vbCrLf & " ").ToCharArray))
                 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("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("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
@@ -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, 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))
-        End If
-
-    End Sub
-    ''' <summary>
-    ''' 进行精准日志匹配。匹配优先级低于堆栈分析的崩溃。
-    ''' </summary>
-    Private Sub AnalyzeCrit3()
-
-        '游戏日志分析
-        If LogMc IsNot Nothing Then
             'Mixin 崩溃
             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)")
                 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)")
@@ -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)")
                 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 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
                 AppendReason(CrashReason.极短的程序输出, LogMc)
@@ -633,9 +656,13 @@ Done:
     ''' 从堆栈中提取 Mod ID 关键字。若失败则返回空列表。
     ''' </summary>
     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)
@@ -648,7 +675,7 @@ Done:
                 "MojangTricksIntelDriversForPerformance_javaw"}
                 If Stack.StartsWithF(IgnoreStack) Then GoTo NextStack
             Next
-            PossibleStacks.Add(Stack.Trim) '.Split("$").First)
+            PossibleStacks.Add(Stack.Trim)
 NextStack:
         Next
         PossibleStacks = PossibleStacks.Distinct.ToList
@@ -665,20 +692,20 @@ NextStack:
             For i = 0 To Math.Min(3, Splited.Count - 1) '最多取前 4 节
                 Dim Word As String = Splited(i)
                 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
                 PossibleWords.Add(Word.Trim)
             Next
         Next
         PossibleWords = PossibleWords.Distinct.ToList
         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
             Log("[Crash] 关键词过多,考虑匹配出错,不纳入考虑")
             Return New List(Of String)
@@ -711,7 +738,7 @@ NextStack:
                 Details = Details.Replace("Fabric Mods", "¨")
                 Log("[Crash] 崩溃报告中检测到 Fabric Mod 信息格式")
             End If
-            Details = Details.Split("¨").Last
+            Details = Details.After("¨")
 
             '[Forge] 获取所有包含 .jar 的行
             '[Fabric] 获取所有包含 Mod 信息的行
@@ -794,13 +821,20 @@ NextStack:
         End If
     End Function
     ''' <summary>
-    ''' 尝试获取 Mod 名称,若失败则返回原关键字。
+    ''' 尝试从关键字获取 Mod 名称,若失败则返回原关键字。
     ''' </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)
     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:根据原因输出信息
     Private OutputFiles As New List(Of String)
@@ -813,7 +847,7 @@ NextStack:
         Dim ShowLog As Action =
             Sub()
                 If File.Exists(DirectFile.Value.Key) Then
-                    ShellOnly(DirectFile.Value.Key)
+                    ShellOnly("notepad", DirectFile.Value.Key)
                 Else
                     Dim FilePath As String = PathTemp & "Crash.txt"
                     WriteFile(FilePath, Join(DirectFile.Value.Value, vbCrLf))
@@ -847,7 +881,7 @@ NextStack:
                                 FileName = "游戏崩溃前的输出.txt"
                         End Select
                         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
                     Next
                     '导出报告
@@ -903,46 +937,46 @@ NextStack:
                         Results.Add("你正在使用 32 位的操作系统,这会导致 Minecraft 无法使用所需的内存,进而造成崩溃。\n\n你或许只能重装 64 位的操作系统来解决此问题。\n如果你的电脑内存在 2GB 以内,那或许只能换台电脑了……\h")
                     End If
                 Case CrashReason.Mod缺少前置或MC版本错误
-                    If Additional.Count > 0 Then
+                    If Additional.Any Then
                         Results.Add("由于未满足 Mod 的依赖项,导致游戏退出。\n未满足的依赖项:\n - " & Join(Additional, "\n - ") & "\n\n请根据上述信息进行对应处理,如果看不懂英文可以使用翻译软件。")
                     Else
                         Results.Add("由于未满足 Mod 的依赖项,导致游戏退出。\n请根据错误报告中的日志信息进行对应处理,如果看不懂英文可以使用翻译软件。\h")
                     End If
                 Case CrashReason.堆栈分析发现关键字
                     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
-                        Results.Add("你的游戏遇到了一些问题,这可能是某些 Mod 所引起的,PCL 找到了以下可疑的关键词:\n - " & Join(Additional, ", ") & "\n\n如果你知道这些关键词对应的 Mod,那么有可能就是它引起的错误,你也可以查看错误报告获取详情。\h")
+                        Results.Add("你的游戏遇到了一些问题,PCL 为此找到了以下可疑的关键词:\n - " & Join(Additional, ", ") & "\n\n如果你知道某个关键词对应的 Mod,那么有可能就是它引起的错误,你也可以查看错误报告获取详情。\h")
                     End If
-                Case CrashReason.堆栈分析发现Mod名称
+                Case CrashReason.堆栈分析发现Mod名称, CrashReason.怀疑Mod导致游戏崩溃
                     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
-                        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
                 Case CrashReason.确定Mod导致游戏崩溃
                     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
-                        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
                 Case CrashReason.ModMixin失败
                     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
-                        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
                 Case CrashReason.Mod配置文件导致游戏崩溃
                     If Additional(1) Is Nothing Then
-                        Results.Add("名称或 ID 为 " & Additional.First & " 的 Mod 导致了游戏出错。\n\e\h")
+                        Results.Add("名为 " & Additional.First & " 的 Mod 导致了游戏出错。\n\e\h")
                     Else
-                        Results.Add("名称或 ID 为 " & Additional.First & " 的 Mod 导致了游戏出错:\n其配置文件 " & Additional(1) & " 存在异常,无法读取。")
+                        Results.Add("名为 " & Additional.First & " 的 Mod 导致了游戏出错:\n其配置文件 " & Additional(1) & " 存在异常,无法读取。")
                     End If
                 Case CrashReason.Mod初始化失败
                     If Additional.Count = 1 Then
-                        Results.Add("名为 " & Additional.First & " 的 Mod 初始化失败,导致游戏无法继续加载。\n\e\h")
+                        Results.Add("名为 " & Additional.First & " 的 Mod 初始化失败,导致游戏无法继续加载。\n你可以尝试禁用此 Mod,然后观察游戏是否还会崩溃。\n\e\h")
                     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
                 Case CrashReason.特定方块导致崩溃
                     If Additional.Count = 1 Then
@@ -1027,8 +1061,6 @@ NextStack:
             End Select
         Next
 
-        '整理多个原因
-        IsHandAnalyze = False
         Return Join(Results, "\n\n此外,").
                     Replace("\n", vbCrLf).
                     Replace("\h", "").
@@ -1038,7 +1070,7 @@ NextStack:
                 If(Not Results.Any(Function(r) r.EndsWithF("\h")) OrElse IsHandAnalyze, "",
                     vbCrLf & "如果要寻求帮助,请向他人发送错误报告文件,而不是发送这个窗口的截图。" &
                     If(If(PageSetupSystem.IsLauncherNewest(), True), "",
-                    vbCrLf & vbCrLf & "此外,你正在使用老版本 PCL,更新到最新版可能会修复这个问题。" & vbCrLf & "你可以点击 设置 → 启动器 → 检查更新 来更新 PCL。"))
+                    vbCrLf & vbCrLf & "此外,你正在使用老版本 PCL,更新 PCL 或许也能解决这个问题。" & vbCrLf & "你可以点击 设置 → 启动器 → 检查更新 来更新 PCL。"))
     End Function
 
 End Class

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

@@ -98,6 +98,11 @@
                     TempAddress = PathTemp & "Cache\" & BackAssetsFile.LocalName
                     BackAssetsFile.LocalPath = TempAddress
                     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))
                 LoadersAssetsUpdate.Add(New LoaderDownload("后台下载资源文件索引", New List(Of NetFile)))
                 LoadersAssetsUpdate.Add(New LoaderTask(Of List(Of NetFile), String)("后台复制资源文件索引",
@@ -106,7 +111,7 @@
                     McLaunchLog("后台更新资源文件索引成功:" & TempAddress)
                 End Sub))
                 Dim Updater As New LoaderCombo(Of String)("后台更新资源文件索引", LoadersAssetsUpdate)
-                Log("[Download] 开始后台更新资源文件索引")
+                Log("[Download] 开始后台检查资源文件索引")
                 Updater.Start()
             End If
             '获取资源文件地址

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

@@ -1,5 +1,5 @@
 Public Module ModJava
-    Public JavaListCacheVersion As Integer = 5
+    Public JavaListCacheVersion As Integer = 6
 
     ''' <summary>
     ''' 目前所有可用的 Java。
@@ -79,7 +79,7 @@
         Public Overrides Function ToString() As String
             Dim VersionString = Version.ToString
             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
 
         '构造
@@ -131,7 +131,9 @@
                     Version = New Version(1, Version.Major, Version.Build, Version.Revision)
                 End If
                 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
                 If IsJre AndAlso VersionCode >= 16 Then Throw New Exception("由于高版本 JRE 对游戏的兼容性很差,因此不再允许使用。你可以使用对应版本的 JDK,而非 JRE!")
             Catch ex As ApplicationException
@@ -197,10 +199,15 @@
             Setup.Set("LaunchArgumentJavaAll", "[]")
         End Try
     End Sub
+
+    ''' <summary>
+    ''' 防止多个需要 Java 的部分同时要求下载 Java(#3797)。
+    ''' </summary>
+    Public JavaLock As New Object
     ''' <summary>
     ''' 根据要求返回最适合的 Java,若找不到则返回 Nothing。
     ''' 最小与最大版本在与输入相同时也会通过。
-    ''' 必须在工作线程调用。
+    ''' 必须在工作线程调用,且必须包括 SyncLock JavaLock。
     ''' </summary>
     Public Function JavaSelect(CancelException As String, Optional MinVersion As Version = Nothing, Optional MaxVersion As Version = Nothing,
                                Optional RelatedVersion As McVersion = Nothing) As JavaEntry
@@ -220,7 +227,7 @@
             Next
 
             '检查特定的 Java
-            If TargetJavaList.Count > 0 Then
+            If TargetJavaList.Any Then
                 TargetJavaList = JavaCheckList(TargetJavaList)
                 Log("[Java] 检查后找到 " & TargetJavaList.Count & " 个特定路径下的 Java:")
                 For Each Java In TargetJavaList
@@ -460,8 +467,8 @@ NoUserJava:
         If Not Left.IsJre AndAlso Right.IsJre Then Return False
         '4. Java 大版本
         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)
         End If
         '5. 最次级版本号更接近 51
@@ -534,7 +541,7 @@ NoUserJava:
                 Log("[Java] 位于 " & Folder & " 的 Java 不含符号链接")
                 JavaWithoutReparse.Add(Pair.Key, Pair.Value)
             Next
-            If JavaWithoutReparse.Count > 0 Then JavaPreList = JavaWithoutReparse
+            If JavaWithoutReparse.Any Then JavaPreList = JavaWithoutReparse
 
             '若不全为特殊引用,则清除特殊引用的地址
             Dim JavaWithoutInherit As New Dictionary(Of String, Boolean)
@@ -546,7 +553,7 @@ NoUserJava:
                     JavaWithoutInherit.Add(Pair.Key, Pair.Value)
                 End If
             Next
-            If JavaWithoutInherit.Count > 0 Then JavaPreList = JavaWithoutInherit
+            If JavaWithoutInherit.Any Then JavaPreList = JavaWithoutInherit
 
 #End Region
 
@@ -567,7 +574,7 @@ NoUserJava:
 
             '确保可用并获取详细信息,转入正式列表
             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))
             Next
             NewJavaList = Sort(JavaCheckList(NewJavaList), AddressOf JavaSorter)
@@ -601,22 +608,23 @@ NoUserJava:
         '启动检查线程
         Dim CheckThreads As New List(Of Thread)
         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)
             CheckThread.Start()
         Next
@@ -719,12 +727,25 @@ Wait:
     ''' 获取下载 Java 8/14/17/21 的加载器。需要开启 IsForceRestart 以正常刷新 Java 列表。
     ''' </summary>
     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 LoaderDownload("下载 Java 文件", New List(Of NetFile)) With {.ProgressWeight = 10},
+            JavaDownloadLoader,
             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
+    Private LastJavaBaseDir As String = Nothing '用于在下载中断或失败时删除未完成下载的 Java 文件夹,防止残留只下了一半但 -version 能跑的 Java
     Private Sub JavaFileList(Loader As LoaderTask(Of Integer, List(Of NetFile)))
         Log("[Java] 开始获取 Java 下载信息")
         Dim IndexFileStr As String = NetGetCodeByDownload(
@@ -739,25 +760,23 @@ Wait:
         Dim Address As String = TargetEntry.Value("manifest")("url")
         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)
         For Each File As JProperty In CType(GetJson(ListFileStr), JObject)("files")
             If CType(File.Value, JObject)("downloads")?("raw") Is Nothing Then Continue For
             Dim Info As JObject = CType(File.Value, JObject)("downloads")("raw")
             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")
-            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
         Loader.Output = Results
-        Log($"[Java] 需要下载 {Results.Count} 个文件,目标文件夹:{BaseDir}")
+        Log($"[Java] 需要下载 {Results.Count} 个文件,目标文件夹:{LastJavaBaseDir}")
     End Sub
 
 #End Region

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

@@ -62,7 +62,7 @@ Public Module ModLaunch
     ''' 记录启动日志。
     ''' </summary>
     Public Sub McLaunchLog(Text As String)
-        Text = SecretFilter(Text)
+        Text = SecretFilter(Text, "*")
         RunInUi(Sub() FrmLaunchRight.LabLog.Text += vbCrLf & "[" & GetTimeNow() & "] " & Text)
         Log("[Launch] " & Text)
     End Sub
@@ -101,10 +101,10 @@ Public Module ModLaunch
         '正式加载
         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,
-                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 List(Of McLibToken), Integer)("解压文件", AddressOf McLaunchNatives) With {.ProgressWeight = 2},
                 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 Process, Integer)("等待游戏窗口出现", AddressOf McLaunchWait) 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 '要求重启登录主加载器,它会自行决定是否启动副加载器
             '等待加载器执行并更新 UI
             McLaunchLoaderReal = LaunchLoader
@@ -163,6 +168,28 @@ NextInner:
 
 #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 "预检测"
 
     Private Sub McLaunchPrecheck()
@@ -247,7 +274,7 @@ NextInner:
         ''' </summary>
         Public BaseUrl As String
         ''' <summary>
-        ''' 登录所使用的标识符,如 “Mojang”、“Nide”,用于存储缓存等。
+        ''' 登录所使用的标识符,目前只可能为 “Auth” 或 “Nide”,用于存储缓存等。
         ''' </summary>
         Public Token As String
         ''' <summary>
@@ -331,7 +358,7 @@ NextInner:
             Case McLoginType.Ms
                 If Setup.Get("CacheMsName") <> "" Then Return Setup.Get("CacheMsName")
             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
                 If Setup.Get("CacheNideName") <> "" Then Return Setup.Get("CacheNideName")
             Case McLoginType.Auth
@@ -341,7 +368,7 @@ NextInner:
         If Setup.Get("CacheMsName") <> "" Then Return Setup.Get("CacheMsName")
         If Setup.Get("CacheNideName") <> "" Then Return Setup.Get("CacheNideName")
         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
     End Function
     ''' <summary>
@@ -497,7 +524,10 @@ Relogin:
         Dim AccessToken As String = MsLoginStep5(Tokens)
         Data.Progress = 0.8
         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("CacheMsAccess", AccessToken)
@@ -513,7 +543,7 @@ Relogin:
 SkipLogin:
         McLaunchLog("微软登录完成")
         Setup.Set("HintBuy", True) '关闭正版购买提示
-        Data.Progress = 0.95
+        Data.Progress = 0.98
         If ThemeUnlock(10, False) Then MyMsgBox("感谢你对正版游戏的支持!" & vbCrLf & "隐藏主题 跳票红 已解锁!", "提示")
     End Sub
     Private Sub McLoginServerStart(Data As LoaderTask(Of McLoginServer, McLoginResult))
@@ -526,7 +556,13 @@ SkipLogin:
         McLaunchLog("登录方式:" & Input.Description & "(" & LogUsername & ")")
         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
                 If Data.IsAborted Then Throw New ThreadInterruptedException
@@ -764,14 +800,14 @@ LoginFinish:
     '微软登录步骤 1:打开网页认证,获取 OAuth Code
     Private Function MsLoginStep1(Data As LoaderTask(Of McLoginMs, McLoginResult)) As String
         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:
             'Windows 7 或老版 Windows 10 登录
             OpenWebsite(FormLoginOAuth.LoginUrl1)
             Dim Result As String =
                 MyMsgBoxInput("等待网页登录",
-                              "在登录成功后,网页会变成一片空白,请将该空白页面的网址复制到下面的框中。" & vbCrLf &
-                              "如果网络环境不佳,登录网页可能一直加载不出来,此时请尝试使用 VPN 或加速器,然后再试。",
+                              "登录完成后,网页会变得完全空白,把那个空白网页的网址复制到下面的框中就行了!" & vbCrLf &
+                              "如果网络环境不佳,它可能一直加载不出来,那就只能试试用 VPN 或加速器了。",
                               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")
             If Result Is Nothing Then
@@ -780,7 +816,7 @@ SystemBrowser:
             Else
                 Return RegexSeek(Result, "(?<=code\=)[^&]+")
             End If
-            Hint("网页登录已完成,你现在可以关闭浏览器上的那个网页了", HintType.Finish)
+            Hint("网页登录成功,你可以关闭浏览器啦!", HintType.Finish)
         Else
             'Windows 10 登录
             Dim ReturnCode As String = Nothing
@@ -893,12 +929,18 @@ SystemBrowser:
         Try
             Result = NetRequestMulty("https://xsts.auth.xboxlive.com/xsts/authorize", "POST", Request, "application/json", 3)
         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
                 Throw New Exception("$$")
+            ElseIf ex.Message.Contains("2148916235") Then
+                MyMsgBox($"你的网络所在的国家或地区无法登录微软账号。{vbCrLf}请尝试使用加速器或 VPN。", "登录失败", "我知道了")
+                Throw New Exception("$$")
             ElseIf ex.Message.Contains("2148916238") Then
                 If MyMsgBox("该账号年龄不足,你需要先修改出生日期,然后才能登录。" & vbCrLf &
                             "该账号目前填写的年龄是否在 13 岁以上?", "登录提示", "13 岁以上", "12 岁以下", "我不知道") = 1 Then
@@ -946,29 +988,46 @@ SystemBrowser:
         Dim AccessToken As String = ResultJson("access_token").ToString
         Return AccessToken
     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")
 
+        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
         Try
             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
             Dim Message As String = GetExceptionSummary(ex)
             If Message.Contains("(429)") Then
-                Log(ex, "微软登录第 6 步汇报 429")
+                Log(ex, "微软登录第 7 步汇报 429")
                 Throw New Exception("$登录尝试太过频繁,请等待几分钟后再试!")
             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("$$")
             Else
                 Throw
@@ -990,14 +1049,14 @@ SystemBrowser:
             Case 1
                 '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
             Case 2
                 '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
             Case 3
                 '使用正版用户名
@@ -1096,22 +1155,25 @@ SystemBrowser:
         If McVersionCurrent.Version.HasForge Then
             If McVersionCurrent.Version.McName = "1.7.2" Then
                 '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
                 '<=1.12:Java 8
                 MaxVer = New Version(1, 8, 999, 999)
             ElseIf McVersionCurrent.Version.McCodeMain <= 14 AndAlso McVersionCurrent.Version.McCodeMain >= 13 Then
                 '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
                 '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
                 '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
                 '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
 
@@ -1119,10 +1181,10 @@ SystemBrowser:
         If McVersionCurrent.Version.HasFabric Then
             If McVersionCurrent.Version.McCodeMain >= 15 AndAlso McVersionCurrent.Version.McCodeMain <= 16 AndAlso McVersionCurrent.Version.McCodeMain <> -1 Then
                 '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
                 '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
 
@@ -1132,63 +1194,68 @@ SystemBrowser:
             MinVer = If(New Version(1, 8, 0, 101) > MinVer, New Version(1, 8, 0, 101), MinVer)
         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 Region
@@ -1202,17 +1269,20 @@ SystemBrowser:
     ''' </summary>
     Public Function ExtractJavaWrapper() As String
         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)
         Return WrapperPath
     End Function
+    Private ExtractJavaWrapperLock As New Object
     ''' <summary>
     ''' 获取 Java Wrapper 所在的文件夹,不以 \ 结尾。
     ''' </summary>
     Public Function GetJavaWrapperDir() As String
-        If Encoding.UTF8.GetByteCount(PathAppdata) = PathAppdata.Length Then
+        If PathAppdata.IsASCII() Then
             Return PathAppdata.TrimEnd("\")
-        ElseIf Encoding.UTF8.GetByteCount(PathTemp) = PathTemp.Length Then
+        ElseIf PathTemp.IsASCII() Then
             Log("[Java] Wrapper:AppData 路径中包含非 ASCII 字符,换用 Temp 目录")
             Return PathTemp.TrimEnd("\")
         Else
@@ -1276,9 +1346,7 @@ SystemBrowser:
                     '不包含端口号
                     Arguments += " --server " & Server & " --port 25565"
                 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
         '自定义
@@ -1300,10 +1368,11 @@ SystemBrowser:
         Dim ArgumentJvm As String = Setup.Get("VersionAdvanceJvm", Version:=McVersionCurrent)
         If ArgumentJvm = "" Then ArgumentJvm = Setup.Get("LaunchAdvanceJvm")
         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.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("""-Djava.library.path=" & Version.Path & Version.Name & "-natives""")
+        DataList.Add("""-Djava.library.path=" & GetNativesFolder() & """")
         DataList.Add("-cp ${classpath}") '把支持库添加进启动参数表
 
         '统一通行证
@@ -1415,6 +1484,9 @@ NextVersion:
             DeDuplicateDataList.Add(CurrentEntry.Trim.Replace("McEmu= ", "McEmu="))
         Next
 
+        '#3511 的清理
+        DeDuplicateDataList.Remove("-XX:MaxDirectMemorySize=256M")
+
         '去重
         Dim Result As String = Join(DeDuplicateDataList.Distinct.ToList, " ")
 
@@ -1535,7 +1607,7 @@ NextVersion:
 
         '基础参数
         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("${libraries_directory}", PathMcFolder & "libraries")
         GameArguments.Add("${launcher_name}", "PCL")
@@ -1552,9 +1624,27 @@ NextVersion:
         GameArguments.Add("${access_token}", McLoginLoader.Output.AccessToken)
         GameArguments.Add("${auth_session}", McLoginLoader.Output.AccessToken)
         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 相关参数
         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))
 
         '创建文件夹
-        Directory.CreateDirectory(McVersionCurrent.Path & McVersionCurrent.Name & "-natives")
+        Dim Target As String = GetNativesFolder() & "\"
+        Directory.CreateDirectory(Target)
 
         '解压文件
         McLaunchLog("正在解压 Natives 文件")
@@ -1605,7 +1696,7 @@ NextVersion:
                 Dim FileName As String = Entry.FullName
                 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)
                     Dim OriginalFile As New FileInfo(FilePath)
                     If OriginalFile.Exists Then
@@ -1631,7 +1722,7 @@ NextVersion:
         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
             Try
                 McLaunchLog("删除:" & FileName)
@@ -1644,6 +1735,16 @@ NextVersion:
         Next
 
     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
 
@@ -1737,13 +1838,14 @@ NextVersion:
             '1.11 ~ 12:zh_cn 时正常,zh_CN 时虽然显示了中文但语言设置会错误地显示选择英文
             '1.13+    :zh_cn 时正常,zh_CN 时自动切换为英文
             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
                 '将最后两位改为大写,前面的部分保留
                 RequiredLang = RequiredLang.Substring(0, RequiredLang.Length - 2) & RequiredLang.Substring(RequiredLang.Length - 2).ToUpper
             End If
             If CurrentLang = RequiredLang Then
-                McLaunchLog($"当前语言为 {CurrentLang},无需修改")
+                McLaunchLog($"需要的语言为 {RequiredLang},当前语言为 {CurrentLang},无需修改")
             Else
                 WriteIni(SetupFileAddress, "lang", "-") '触发缓存更改,避免删除后重新下载残留缓存
                 WriteIni(SetupFileAddress, "lang", RequiredLang)
@@ -1758,7 +1860,7 @@ NextVersion:
             Select Case Setup.Get("LaunchArgumentWindowType")
                 Case 0 '全屏
                     WriteIni(SetupFileAddress, "fullscreen", "true")
-                Case 5 '保持默认
+                Case 1 '默认
                 Case Else '其他
                     WriteIni(SetupFileAddress, "fullscreen", "false")
             End Select
@@ -1936,7 +2038,7 @@ IgnoreCustomSkin:
                 """" & McLaunchJavaSelected.PathJava & """ " & McLaunchArgument & vbCrLf &
                 "echo 游戏已退出。" & vbCrLf &
                 "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")))
             If McLaunchLoader.Input.SaveBatch IsNot Nothing Then
                 McLaunchLog("导出启动脚本完成,强制结束启动过程")
@@ -2058,7 +2160,7 @@ IgnoreCustomSkin:
         McLaunchLog("")
         McLaunchLog("~ 基础参数 ~")
         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("版本继承:" & If(McVersionCurrent.InheritVersion = "", "无", McVersionCurrent.InheritVersion))
         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("Java 信息:" & If(McLaunchJavaSelected IsNot Nothing, McLaunchJavaSelected.ToString, "无可用 Java"))
         McLaunchLog("环境变量:" & If(McLaunchJavaSelected IsNot Nothing, If(McLaunchJavaSelected.HasEnvironment, "已设置", "未设置"), "未设置"))
+        McLaunchLog("Natives 文件夹:" & GetNativesFolder())
         McLaunchLog("")
         McLaunchLog("~ 登录参数 ~")
         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 "文件夹"
 
@@ -149,7 +151,7 @@
 
 #Region "版本处理"
 
-    Public Const McVersionCacheVersion As Integer = 27
+    Public Const McVersionCacheVersion As Integer = 29
 
     Private _McVersionCurrent As McVersion
     Private _McVersionLast = 0 '为 0 以保证与 Nothing 不相同,使得 UI 显示可以正常初始化
@@ -181,7 +183,7 @@
             If AniControlEnabled = 0 AndAlso
                Setup.Get("VersionServerAuthServer", Version:=value) <> Setup.Get("CacheAuthServerServer") AndAlso
                Setup.Get("VersionServerLogin", Version:=value) = 4 Then
-                Setup.Set("CacheNideServer", "")
+                Setup.Set("CacheAuthAccess", "")
                 Log("[Launch] 服务器改变,要求重新登录 Authlib-Injector")
             End If
             If Setup.Get("VersionServerLogin", Version:=value) = 4 Then
@@ -216,8 +218,8 @@
                     '尚未判断
                     Dim ModFolder As New DirectoryInfo(Path & "mods\")
                     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)
                         Log("[Setup] 已自动开启单版本隔离:" & Name)
@@ -391,6 +393,29 @@
                             _Version.McName = JsonObject("jar").ToString
                             GoTo VersionSearchFinish
                         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)
                         '从文件夹名中获取
@@ -1034,7 +1059,7 @@ ExitDataLoad:
         Dim Path As String = Loader.Input
         Try
             '初始化
-            McVersionList.Clear()
+            McVersionList = New Dictionary(Of McVersionCardType, List(Of McVersion))
 
             '检测缓存是否需要更新
             Dim FolderList As New List(Of String)
@@ -1075,10 +1100,10 @@ Reload:
 
             '改变当前选择的版本
 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")
-                If Not SavedSelection = "" Then
+                If SavedSelection <> "" Then
                     For Each Card As KeyValuePair(Of McVersionCardType, List(Of McVersion)) In McVersionList
                         For Each Version As McVersion In Card.Value
                             If Version.Name = SavedSelection AndAlso Not Version.State = McVersionState.Error Then
@@ -1190,7 +1215,7 @@ OnLoaded:
                     End Try
                 Next
 
-                ResultVersionList.Add(CardType, VersionList)
+                If VersionList.Any Then ResultVersionList.Add(CardType, VersionList)
             Next
             Return ResultVersionList
         Catch ex As Exception
@@ -1200,10 +1225,14 @@ OnLoaded:
     End Function
     Private Function McVersionListLoadNoCache(Path As String) As Dictionary(Of McVersionCardType, List(Of McVersion))
         Dim VersionList As New List(Of McVersion)
-        Dim ResultVersionList As New Dictionary(Of McVersionCardType, List(Of McVersion))
+
 #Region "循环加载每个版本的信息"
         Dim Dirs As DirectoryInfo() = New DirectoryInfo(Path & "versions").GetDirectories
         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
                 Log("[Minecraft] 跳过可能不是版本文件夹的项目:" & Folder.FullName)
                 Continue For
@@ -1223,6 +1252,9 @@ OnLoaded:
             Version.Load()
         Next
 #End Region
+
+        Dim ResultVersionList As New Dictionary(Of McVersionCardType, List(Of McVersion))
+
 #Region "将版本分类到各个卡片"
         Try
 
@@ -1234,9 +1266,9 @@ OnLoaded:
             For Each Version As McVersion In VersionList
                 If Version.IsStar AndAlso Not Version.DisplayType = McVersionCardType.Hidden Then StaredVersions.Add(Version)
             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.Fool}, McVersionCardType.Fool)
 
@@ -1308,8 +1340,8 @@ OnLoaded:
 
             '将剩余的东西添加进去
             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
@@ -1347,9 +1379,7 @@ OnLoaded:
             Next
             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)
             ResultVersionList(McVersionCardType.OriginalLike) = NewList
@@ -1357,43 +1387,45 @@ OnLoaded:
 
         '不常用版本:按发布时间新旧排序,如果不可用则按名称排序
         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
 
         'API 版本:优先按版本排序,此后【先放 Fabric,再放 Forge(按版本号从高到低排序),最后放 LiteLoader(按名称排序)】
         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 Region
@@ -1417,19 +1449,9 @@ OnLoaded:
     ''' <param name="Formula">需要筛选出的版本类型。-2 代表隐藏的版本。</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)
-        '将某种版本筛选出来
-        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)
             For Each Version As McVersion In KeepList
                 VersionList.Remove(Version)
@@ -1443,14 +1465,9 @@ NextVersion:
     ''' <param name="Formula">需要筛选出的版本类型。-2 代表隐藏的版本。</param>
     ''' <param name="KeepList">传入需要增加入的列表。</param>
     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
                 VersionList.Remove(Version)
             Next
@@ -1515,7 +1532,7 @@ NextVersion:
     ''' </summary>
     Public Function McSkinGetAddress(Uuid As String, Type As String) As String
         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)
         If Not CacheSkinAddress = "" Then Return CacheSkinAddress
@@ -2001,7 +2018,7 @@ NextVersion:
             Result.Add(New NetFile(Urls.Distinct.ToArray, Token.LocalPath, Checker))
         Next
         '去重并返回
-        Return Distinct(Result, Function(a, b) a.LocalPath = b.LocalPath)
+        Return Result.Distinct(Function(a, b) a.LocalPath = b.LocalPath)
     End Function
     ''' <summary>
     ''' 获取对应的支持库文件地址。
@@ -2048,8 +2065,7 @@ NextVersion:
                 If Version.InheritVersion = "" Then Exit Do
                 Version = New McVersion(PathMcFolder & "versions\" & Version.InheritVersion)
             Loop
-        Catch ex As Exception
-            Log(ex, "获取资源文件索引下载地址失败")
+        Catch
         End Try
         '无法获取到下载地址
         If ReturnLegacyOnError Then
@@ -2239,10 +2255,11 @@ NextVersion:
             '弹窗结果
             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
 
         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
                         Author.Add(Token.ToString)
                     Next
-                    If Author.Count > 0 Then Authors = Join(Author, ", ")
+                    If Author.Any Then Authors = Join(Author, ", ")
                 End If
                 Dim Reqs As JArray = InfoObject("requiredMods")
                 If Reqs IsNot Nothing Then
@@ -393,7 +393,7 @@ GotFabric:
                     For Each Token In AuthorJson
                         Author.Add(Token.ToString)
                     Next
-                    If Author.Count > 0 Then Authors = Join(Author, ", ")
+                    If Author.Any Then Authors = Join(Author, ", ")
                 End If
                 'If (Not FabricObject.ContainsKey("serverSideOnly")) OrElse FabricObject("serverSideOnly")("value").ToObject(Of Boolean) = False Then
                 '    '添加 Minecraft 依赖
@@ -438,7 +438,7 @@ GotFabric:
                         Line = Line.Substring(0, Line.IndexOfF("#"))
                     End If
                     Line = Line.Trim(New Char() {" "c, "	"c, " "c}) '去除头尾的空格
-                    If Line.Count > 0 Then Lines.Add(Line) '去除空行
+                    If Line.Any Then Lines.Add(Line) '去除空行
                 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))}
@@ -887,11 +887,11 @@ Finished:
                 ModList.Add(ModEntry)
                 '读取 Comp 缓存
                 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 小时以内更新过,则无需重新获取
-                    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
                 ModUpdateList.Add(ModEntry)
             Next
@@ -925,7 +925,7 @@ Finished:
     End Sub
 
     '联网加载 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)
     Private Sub McModDetailLoad(Loader As LoaderTask(Of KeyValuePair(Of List(Of McMod), JObject), Integer))
         Dim Mods As List(Of McMod) = Loader.Input.Key
@@ -937,7 +937,7 @@ Finished:
         If TargetMcVersion.HasFabric Then ModLoaders.Add(CompModLoaderType.Fabric)
         If TargetMcVersion.HasLiteLoader Then ModLoaders.Add(CompModLoaderType.LiteLoader)
         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 小版本
         '例如:Mod 在更新 1.16.5 后,对早期的 1.16.2 版本发布了修补补丁,这会导致 PCL 将 1.16.5 版本的 Mod 降级到 1.16.2
         'If TargetMcVersion.McCodeMain > 0 AndAlso TargetMcVersion.McCodeMain < 99 Then
@@ -948,7 +948,7 @@ Finished:
         'End If
         '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 MainThread As Thread = Thread.CurrentThread
         '从 Modrinth 获取信息
@@ -965,6 +965,7 @@ Finished:
                 Dim ModrinthMapping As New Dictionary(Of String, List(Of McMod))
                 For Each Entry In Mods
                     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
                     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)
@@ -990,8 +991,8 @@ Finished:
                 Log($"[Mod] 已从 Modrinth 获取本地 Mod 信息,继续获取更新信息")
                 '步骤 4:获取更新信息
                 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
                     If Not ModrinthUpdate.ContainsKey(Entry.ModrinthHash) OrElse Entry.CompFile Is Nothing Then Continue For
                     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.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) '合并下载源
                         Entry.UpdateFile = UpdateFile '优先使用 Modrinth 的文件
                     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
                     End If
                 Next
@@ -1072,7 +1073,7 @@ Finished:
                         For Each IndexEntry In ProjectJson("latestFilesIndexes")
                             If ModLoaders.Single <> IndexEntry("modLoader")?.ToObject(Of Integer) Then Continue For 'ModLoader 唯一且匹配
                             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,但这应当并不会获取到更新的文件
                             If NewestVersion IsNot Nothing AndAlso VersionSortInteger(NewestVersion, IndexVersion) > -1 Then Continue For '只保留最新 MC 版本
                             If NewestVersion <> IndexVersion Then
@@ -1135,7 +1136,7 @@ Finished:
         If Not Mods.Any() Then Exit Sub
         For Each Entry In Mods 'TODO: Bookshelf 的 Logo 会在两个网站间横跳
             Entry.CompLoaded = SucceedThreadCount = 2
-            Cache(Entry.ModrinthHash) = Entry.ToJson()
+            Cache(Entry.ModrinthHash & McVersion) = Entry.ToJson()
         Next
         WriteFile(PathTemp & "Cache\LocalMod.json", Cache.ToString(If(ModeDebug, Newtonsoft.Json.Formatting.Indented, Newtonsoft.Json.Formatting.None)))
     End Sub

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

@@ -244,16 +244,17 @@ Retry:
             ModList.Add(ModEntry("fileID"))
             If ModEntry("required") IsNot Nothing AndAlso Not ModEntry("required").ToObject(Of Boolean) Then ModOptionalList.Add(ModEntry("fileID"))
         Next
-        If ModList.Count > 0 Then
+        If ModList.Any Then
+            Dim ModDownloadLoaders As New List(Of LoaderBase)
             '获取 Mod 下载信息
-            InstallLoaders.Add(New LoaderTask(Of Integer, JArray)("获取 Mod 下载信息",
+            ModDownloadLoaders.Add(New LoaderTask(Of Integer, JArray)("获取 Mod 下载信息",
             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")
                 '如果文件已被删除,则 API 会跳过那一项
                 If ModList.Count > Task.Output.Count Then Throw New Exception("整合包所需要的部分 Mod 版本已被 Mod 作者删除,因此无法完成整合包安装,请联系整合包作者更新整合包中的 Mod 版本")
             End Sub) With {.ProgressWeight = ModList.Count / 10}) '每 10 Mod 需要 1s
             '构造 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)))
                 Dim FileList As New Dictionary(Of Integer, NetFile)
                 For Each ModJson In Task.Input
@@ -288,7 +289,10 @@ Retry:
                 Task.Output = FileList.Values.ToList
             End Sub) With {.ProgressWeight = ModList.Count / 200, .Show = False}) '每 200 Mod 需要 1s
             '下载 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
 
         '构造加载器
@@ -299,43 +303,35 @@ Retry:
             .ForgeVersion = ForgeVersion,
             .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)
         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)
         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})
         '构造总加载器
         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 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 & " "
@@ -438,10 +434,10 @@ Retry:
             End If
             '添加下载文件
             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
-        If FileList.Count > 0 Then
+        If FileList.Any Then
             InstallLoaders.Add(New LoaderDownload("下载额外文件", FileList) With {.ProgressWeight = FileList.Count * 1.5}) '每个 Mod 需要 1.5s
         End If
 
@@ -453,43 +449,35 @@ Retry:
             .ForgeVersion = ForgeVersion,
             .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)
         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)
         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})
         '构造总加载器
         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 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 & " "
@@ -554,16 +542,8 @@ Retry:
             .TargetVersionFolder = $"{PathMcFolder}versions\{VersionName}\",
             .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)
         If MergeLoaders Is Nothing Then Exit Sub
-        Dim MergeExpectTime As Double = 0
-        For Each MergeLoader In MergeLoaders
-            MergeExpectTime += MergeLoader.ProgressWeight
-        Next
         '构造 Libraries 加载器(为了使得 Mods 下载结束后再构造,这样才会下载 JumpLoader 文件)
         Dim LoadersLib As New List(Of LoaderBase)
         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})
         '构造总加载器
         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}
         }
 
@@ -704,24 +684,16 @@ Retry:
             End Select
         Next
         '构造加载器
-        Dim InstallExpectTime As Double = 0
-        For Each InstallLoader In InstallLoaders
-            InstallExpectTime += InstallLoader.ProgressWeight
-        Next
         Dim MergeLoaders As List(Of LoaderBase) = McInstallLoader(Request, True)
         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)
         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})
         '构造总加载器
         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})
 
         '重复任务检查
@@ -802,24 +774,16 @@ Retry:
             .ForgeVersion = If(Addons.ContainsKey("forge"), Addons("forge"), 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)
         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)
         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})
         '构造总加载器
         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})
 
         '重复任务检查

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

@@ -276,17 +276,18 @@
                     IsWindowFinished = True
                     '最大化
                     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
                 ElseIf Not IsWindowAppeared Then
                     '已找到 FML 窗口
@@ -347,6 +348,7 @@
             '崩溃分析
             WatcherLog("Minecraft 已崩溃,将在 2 秒后开始崩溃分析")
             Hint("检测到 Minecraft 出现错误,错误分析已开始……")
+            FeedbackInfo()
             RunInNewThread(Sub()
                                Try
                                    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 SwipToState As Boolean '被滑动到的目标应将 Checked 改为此值
     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)
         SwipeStart = Index
@@ -204,6 +205,7 @@ RetryStart:
         FrmVersionMod.CardSelect.IsHitTestVisible = False '暂时禁用下边栏
     End Sub
     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
             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 "切换页面"
                     FrmMain.PageChange(Val(Data(0)), Val(Data(1)))
 
-                Case "导入整合包"
-                    RunInThread(Sub() ModpackInstall())
+                Case "导入整合包", "安装整合包"
+                    RunInUi(Sub() ModpackInstall())
 
                 Case "下载文件"
                     Data(0) = Data(0).Replace("\", "/")
@@ -101,8 +101,7 @@
         '网页确认
         If RelativeUrl.StartsWithF("http", True) Then
             If RunInUi() Then
-                Throw New Exception("MyListItem 在界面初始化时就需要获取帮助标题等信息,这会导致程序在网络请求时卡死。" & vbCrLf &
-                                    "因此,请换用 MyListItem 以外的控件(例如 MyButton)作为联网帮助页面的入口!")
+                Throw New Exception("能打开联网帮助页面的 MyListItem 必须手动设置 Title、Info 属性!")
             End If
             '获取文件名
             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 "播放列表"
 
@@ -199,13 +197,15 @@ Public Module ModMusic
     ''' </summary>
     Public Function MusicPause() As Boolean
         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
         Else
+            Log($"[Music] 无需暂停播放,当前状态为 {MusicState}")
             Return False
         End If
     End Function
@@ -214,13 +214,15 @@ Public Module ModMusic
     ''' </summary>
     Public Function MusicResume() As Boolean
         If MusicState = MusicStates.Play OrElse Not MusicAllList.Any() Then
+            Log($"[Music] 无需继续播放,当前状态为 {MusicState}")
             Return False
         Else
-            RunInThread(Sub()
-                            MusicNAudio.Play()
-                            MusicRefreshUI()
-                            Log("[Music] 已恢复播放")
-                        End Sub)
+            RunInThread(
+            Sub()
+                Log("[Music] 已恢复播放")
+                MusicNAudio?.Play()
+                MusicRefreshUI()
+            End Sub)
             Return True
         End If
     End Function
@@ -266,7 +268,7 @@ Public Module ModMusic
                 Thread.Sleep(100)
             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
             Log("[Music] 播放音乐失败的文件完整路径:" & MusicCurrent)
             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: 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: 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("")>

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

@@ -384,20 +384,27 @@ Public Module ModDownloadLib
     End Sub
     Private Sub McDownloadOptiFineInstall(BaseMcFolderHome As String, Target As String, Task As LoaderTask(Of List(Of NetFile), Boolean), UseJavaWrapper As Boolean)
         '选择 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))
-            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
         Dim Arguments As String
         If UseJavaWrapper Then
@@ -487,7 +494,7 @@ Public Module ModDownloadLib
                     outputWaitHandle.WaitOne(10000)
                     errorWaitHandle.WaitOne(10000)
                     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 SyncLock
@@ -538,7 +545,7 @@ Public Module ModDownloadLib
                 'OptiFine 中文镜像源
                 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})
         Loaders.Add(New LoaderDownload("下载 OptiFine 主文件", New List(Of NetFile)) With {.ProgressWeight = 8})
         Loaders.Add(New LoaderTask(Of List(Of NetFile), Boolean)("等待原版下载",
@@ -548,8 +555,8 @@ Public Module ModDownloadLib
                 Dim TargetLoaders As List(Of LoaderBase) =
                    ClientDownloadLoader.GetLoaderList.Where(Function(l) l.Name = McDownloadClientLibName OrElse l.Name = McDownloadClientJsonName).
                    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
                     Thread.Sleep(50)
                 Loop
@@ -560,12 +567,10 @@ Public Module ModDownloadLib
                     Dim ClientName As String = GetFolderNameFromPath(ClientFolder)
                     Directory.CreateDirectory(McFolder & "versions\" & DownloadInfo.Inherit)
                     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
                     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 SyncLock
             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)
         '选择 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))
-            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
         Dim Arguments As String
         If UseJavaWrapper Then
@@ -1269,8 +1281,8 @@ Retry:
         McFolder = If(McFolder, PathMcFolder)
         If Version.StartsWithF("1.") AndAlso Version.Contains("-") Then
             '类似 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
         Dim IsCustomFolder As Boolean = McFolder <> PathMcFolder
         Dim Id As String = Inherit & "-forge-" & Version
@@ -1283,20 +1295,20 @@ Retry:
         '获取下载信息
         If DownloadInfo Is Nothing Then
             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
         '下载 Forge 主文件
         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})
 
         '安装(仅在新版安装时需要原版 Jar)
-        If Version.Split(".")(0) >= 20 Then
+        If Version.Before(".") >= 20 Then
             Log("[Download] 检测为新版 Forge:" & Version)
             Dim Libs As List(Of McLibToken) = Nothing
             Loaders.Add(New LoaderTask(Of String, List(Of NetFile))("分析 Mod 加载器支持库文件",
@@ -1338,7 +1350,7 @@ Retry:
                         Task.Progress = 0.4
                         Dim RawJson As JObject = GetJson(NetGetCodeByDownload(DlSourceLauncherOrMetaGet(DlClientListGet(Inherit)), IsJson:=True))
                         '[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))
                         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")})
@@ -1383,8 +1395,8 @@ Retry:
                 Dim TargetLoaders As List(Of LoaderBase) =
                                                                                 ClientDownloadLoader.GetLoaderList.Where(Function(l) l.Name = McDownloadClientLibName OrElse l.Name = McDownloadClientJsonName).
                                                                                 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
                     Thread.Sleep(50)
                 Loop
@@ -1441,7 +1453,7 @@ Retry:
                     End Try
                     '拷贝新增的版本 Json
                     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
                         '它可能和 OptiFine 安装同时运行,导致增加的文件不止一个(这导致了 #151)
                         '也可能是因为 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)
         '获取推荐版本与最新版本
         Dim FreshVersion As DlForgeVersionEntry = Nothing
-        If Entrys.Count > 0 Then
+        If Entrys.Any Then
             FreshVersion = Entrys(0)
         Else
             Log("[System] 未找到可用的 Forge 版本", LogLevel.Debug)
@@ -1782,7 +1794,7 @@ Retry:
     Public Function FabricApiDownloadListItem(Entry As CompFile, OnClick As MyListItem.ClickEventHandler) As MyListItem
         '建立控件
         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"),
             .Logo = PathImage & "Blocks/Fabric.png"
         }
@@ -1972,7 +1984,7 @@ Retry:
         If Directory.Exists(TempMcFolder) Then DeleteDirectory(TempMcFolder)
         Dim OptiFineFolder As String = Nothing
         If Request.OptiFineVersion IsNot Nothing Then
-            If Request.OptiFineVersion.Contains("_HD_U_") Then Request.OptiFineVersion = "HD_U_" & Request.OptiFineVersion.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 {
                 .NameDisplay = Request.MinecraftName & " " & Request.OptiFineVersion.Replace("HD_U_", "").Replace("_", "").Replace("pre", " pre"),
                 .Inherit = Request.MinecraftName,
@@ -2014,7 +2026,7 @@ Retry:
 
         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
         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})
@@ -2056,7 +2068,7 @@ Retry:
             End Sub) With {.ProgressWeight = 2, .Block = True})
         '补全文件
         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)
             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})
@@ -2164,7 +2176,7 @@ Retry:
         For i = 0 To RawArguments.Count - 1
             If RawArguments(i).StartsWithF("-") Then
                 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)
             Else
                 SplitArguments.Add(RawArguments(i))
@@ -2172,6 +2184,7 @@ Retry:
         Next
         Dim RealArguments As String = Join(SplitArguments.Distinct.ToList, " ")
         '合并
+        '相关讨论见 #2801
         OutputJson = MinecraftJson
         If HasOptiFine Then
             '合并 OptiFine

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

@@ -210,10 +210,10 @@
                 If Project.Type = CompType.Mod Then
                     '获取 Mod 所需的加载器种类
                     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)
                         AllowFabric = File.ModLoaders.Contains(CompModLoaderType.Fabric)
-                    ElseIf Project.ModLoaders.Count > 0 Then '从工程中获取
+                    ElseIf Project.ModLoaders.Any Then '从工程中获取
                         AllowForge = Project.ModLoaders.Contains(CompModLoaderType.Forge)
                         AllowFabric = Project.ModLoaders.Contains(CompModLoaderType.Fabric)
                     End If
@@ -275,7 +275,7 @@
                 If Project.TranslatedName = Project.RawName Then
                     FileName = File.FileName
                 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(": ", ":")
                     Select Case Setup.Get("ToolDownloadTranslate")
                         Case 0
@@ -299,7 +299,7 @@
                                           If(File.FileName.EndsWith(".mrpack"), "*.mrpack", "*.zip")), DefaultFolder)
                     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)
                     Dim Loaders As New List(Of LoaderBase)
                     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">
                             <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="安装结束后,请在 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.ColumnDefinitions>
                                     <ColumnDefinition Width="1*" />
@@ -61,26 +61,6 @@
                             <TextBlock VerticalAlignment="Center" Text="返回" Grid.Column="3" Foreground="{StaticResource ColorBrushGray4}" />
                         </Grid>
                     </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">
                         <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">
@@ -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" />
                         </Grid>
                     </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">
                         <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">

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

@@ -32,7 +32,7 @@
 #Region "页面切换"
 
     '页面切换动画
-    Private IsInSelectPage As Boolean = False
+    Public IsInSelectPage As Boolean = False
     Private IsFirstLoaded As Boolean = False
     Private Sub EnterSelectPage()
         If IsInSelectPage Then Exit Sub
@@ -273,11 +273,13 @@
         End If
     End Sub
 
+    Private IsReloading As Boolean = False '#3742 中,LoadOptiFineGetError 会初始化 LoadOptiFine,触发事件 LoadOptiFine.StateChanged,导致再次调用 SelectReload
     ''' <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
-        If SelectedMinecraftId Is Nothing Then Exit Sub
+        If SelectedMinecraftId Is Nothing OrElse IsReloading Then Exit Sub
+        IsReloading = True
         '主预览
         SelectNameUpdate()
         ItemSelect.Title = TextSelectName.Text
@@ -303,20 +305,25 @@
             LabOptiFine.Foreground = ColorGray1
         End If
         '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
-            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
         'Forge
         Dim ForgeError As String = LoadForgeGetError()
@@ -335,67 +342,87 @@
             LabForge.Foreground = ColorGray1
         End If
         '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
-            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
         '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
-            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
         '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
-            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
         '主警告
-        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 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
             HintOptiFabric.Visibility = Visibility.Collapsed
-            HintOptiFabricOld.Visibility = Visibility.Collapsed
         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
     ''' <summary>
     ''' 清空已选择的项目。
@@ -643,7 +670,7 @@
         Return (OptiFine.RequiredForgeVersion.Contains(".") AndAlso 'XX.X.XXX
                 VersionSortInteger(Forge.Version, OptiFine.RequiredForgeVersion) >= 0) OrElse
                (Not OptiFine.RequiredForgeVersion.Contains(".") AndAlso '#XXXX
-                VersionSortInteger(Forge.Version.Split(".").Last, OptiFine.RequiredForgeVersion) >= 0)
+                Forge.Version.After(".") >= OptiFine.RequiredForgeVersion)
     End Function
 
     '限制展开
@@ -920,12 +947,12 @@
             If DisplayName.StartsWith("[" & MinecraftVersion & "]") Then Return True
             If Not DisplayName.Contains("/") OrElse Not DisplayName.Contains("]") Then Return False
             '直接的判断(例如 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
             Next
             '将版本名分割语素(例如 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
             While True
@@ -1045,7 +1072,6 @@
     ''' 获取 OptiFabric 的加载异常信息。若正常则返回 Nothing。
     ''' </summary>
     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.State.LoadingState = MyLoading.MyLoadingState.Error Then Return "获取版本列表失败:" & CType(LoadOptiFabric.State, Object).Error.Message
         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" />
                         <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="1.20.5" />
+                            <local:MyComboBoxItem Content="1.20.6" />
                             <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" />
                             <local:MyComboBoxItem Content="1.19.4" />
+                            <local:MyComboBoxItem Content="1.19.2" />
                             <local:MyComboBoxItem Content="1.18.2" />
                             <local:MyComboBoxItem Content="1.17.1" />
                             <local:MyComboBoxItem Content="1.16.5" />
                             <local:MyComboBoxItem Content="1.15.2" />
                             <local:MyComboBoxItem Content="1.14.4" />
-                            <local:MyComboBoxItem Content="1.13.2" />
                             <local:MyComboBoxItem Content="1.12.2" />
                             <local:MyComboBoxItem Content="1.11.2" />
                             <local:MyComboBoxItem Content="1.10.2" />
-                            <local:MyComboBoxItem Content="1.9.4" />
                             <local:MyComboBoxItem Content="1.8.9" />
                             <local:MyComboBoxItem Content="1.7.10" />
                         </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" />
                         <local:MyComboBox x:Name="TextSearchVersion" Grid.Row="2" Grid.Column="1" IsEditable="True" MaxDropDownHeight="320">
                             <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.3" />
-                            <local:MyComboBoxItem Content="1.20.2" />
                             <local:MyComboBoxItem Content="1.20.1" />
-                            <local:MyComboBoxItem Content="1.20" />
                             <local:MyComboBoxItem Content="1.19.4" />
+                            <local:MyComboBoxItem Content="1.19.2" />
                             <local:MyComboBoxItem Content="1.18.2" />
                             <local:MyComboBoxItem Content="1.17.1" />
                             <local:MyComboBoxItem Content="1.16.5" />
                             <local:MyComboBoxItem Content="1.15.2" />
                             <local:MyComboBoxItem Content="1.14.4" />
-                            <local:MyComboBoxItem Content="1.13.2" />
                             <local:MyComboBoxItem Content="1.12.2" />
                             <local:MyComboBoxItem Content="1.11.2" />
                             <local:MyComboBoxItem Content="1.10.2" />
-                            <local:MyComboBoxItem Content="1.9.4" />
                             <local:MyComboBoxItem Content="1.8.9" />
                             <local:MyComboBoxItem Content="1.7.10" />
                         </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)
         FrmMain.Focus()
     End Sub
-    Private Sub HintSwitch_Click(sender As Object, e As EventArgs) Handles HintSwitch.Click
+    Private Sub HintSwitch_Click() Handles HintSwitch.Click
         IsSwitch = True
         Close()
     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 披风"},
                             {"Vanilla", "原版披风"}, {"Minecon2011", "Minecon 2011 参与者披风"}, {"Minecon2012", "Minecon 2012 参与者披风"},
                             {"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 = "无披风"}}
                         For Each Cape In SkinData("capes")

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

@@ -19,51 +19,53 @@
         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")
@@ -398,7 +400,7 @@ Finish:
                 ElseIf Setup.Get("LoginLegacyName") = "" Then
                     Return New EqualableList(Of String) From {0, ""}
                 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
             Case 3
                 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
             Case 3
                 Select Case Setup.Get("UiCustomPreset")
-                    Case 0 '你知道吗
+                    Case 0
                         Log("[Page] 主页预设:你知道吗")
                         Content = "
                             <local:MyCard Title=""你知道吗?"" Margin=""0,0,0,15"">
@@ -76,7 +76,7 @@ Download:
                                     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"" />
                             </local:MyCard>"
-                    Case 1 '回声洞
+                    Case 1
                         Log("[Page] 主页预设:回声洞")
                         Content = "
                             <local:MyCard Title=""回声洞"" Margin=""0,0,0,15"">
@@ -85,17 +85,21 @@ Download:
                                     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"" />
                             </local:MyCard>"
-                    Case 2 '新闻
+                    Case 2
                         Log("[Page] 主页预设:Minecraft 新闻")
                         Url = "http://pcl.mcnews.thestack.top"
                         GoTo Download
-                    Case 3 '简单主页
+                    Case 3
                         Log("[Page] 主页预设:简单主页")
                         Url = "https://gitee.com/mfn233/PCL-Mainpage/raw/main/Custom.xaml"
                         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
                 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 Sub OnlineLoaderSub(Task As LoaderTask(Of String, Integer))
+        Dim Address As String = Task.Input '#3721 中连续触发两次导致内容变化
         Try
             '获取版本校验地址
             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
-                VersionAddress = Task.Input.Split("?").First
+                VersionAddress = Address.Before("?")
                 If Not VersionAddress.EndsWith("/") Then VersionAddress += "/"
                 VersionAddress += "version"
-                If Task.Input.Contains("?") Then VersionAddress += Task.Input.Split("?").Last
+                If Address.Contains("?") Then VersionAddress += Address.After("?")
             End If
             '校验版本
             Dim Version As String = ""
@@ -136,9 +141,9 @@ Download:
             End Try
             '实际下载
             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)
                 WriteFile(PathTemp & "Cache\Custom.xaml", FileContent)
             End If
@@ -146,11 +151,11 @@ Download:
             Refresh()
         Catch ex As Exception
             If Setup.Get("CacheSavedPageVersion") = "" Then
-                Log(ex, $"联网下载自定义主页失败({Task.Input})", LogLevel.Msgbox)
+                Log(ex, $"联网下载自定义主页失败({Address})", LogLevel.Msgbox)
             Else
-                Log(ex, $"联网下载自定义主页失败({Task.Input})")
+                Log(ex, $"联网下载自定义主页失败({Address})")
             End If
-            Task.Input = ""
+            Address = ""
         End Try
     End Sub
 
@@ -208,6 +213,7 @@ Download:
                     GoTo Refresh '防止 SyncLock 死锁
                 End If
             End Try
+            Log($"[Page] 实例化:加载自定义主页 UI 完成")
         End SyncLock
         Return
 Refresh:

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

@@ -17,8 +17,8 @@
                 ComboName.ItemsSource = Nothing
             Else
                 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
         IsFirstLoad = False

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

@@ -26,7 +26,7 @@ Public Class PageLoginLegacy
                 ComboName.ItemsSource = Nothing
             Else
                 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
         IsReloaded = True

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

@@ -67,39 +67,43 @@
         ComboAccounts.IsEnabled = False
         BtnLogin.IsEnabled = False
         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 Class

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

@@ -85,50 +85,51 @@
         Hint("正在更改皮肤……")
         IsChanging = True
         '开始实际获取
-        RunInNewThread(Async Sub()
-                           Try
+        RunInNewThread(
+        Async Sub()
+            Try
 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
 
     '保存皮肤

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

@@ -17,8 +17,8 @@
                 ComboName.ItemsSource = Nothing
             Else
                 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
         IsFirstLoad = False

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

@@ -149,9 +149,6 @@ Public Class PageLinkIoi
     ''' 检查某个玩家名是否合法。
     ''' </summary>
     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
     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
                     '4. 该版本的隔离文件夹下不存在 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)
                     Log("[Setup] 已自动关闭单版本隔离:" & Version.Name, LogLevel.Debug)
@@ -295,7 +295,7 @@
             For i = 0 To Folders.Count - 1
                 If Folders(i) = "" Then Exit For
                 If Folders(i).ToString.EndsWith(Folder.Path) Then
-                    Name = Folders(i).ToString.Split(">")(0)
+                    Name = Folders(i).ToString.Before(">")
                     Folders.RemoveAt(i)
                     Exit For
                 End If
@@ -320,7 +320,7 @@
             For i = 0 To Folders.Count - 1
                 If Folders(i) = "" Then Exit For
                 If Folders(i).ToString.EndsWith(Folder.Path) Then
-                    'Name = Folders(i).ToString.Split(">")(0)
+                    'Name = Folders(i).ToString.Before(">")
                     Folders.RemoveAt(i)
                     Exit For
                 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" />
                         <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" />
-                        <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:ValidateExcept>
                                     <local:ValidateExcept.Excepts>
@@ -115,7 +115,7 @@
                             <local:MyComboBoxItem Content="中(平衡)" IsSelected="True" />
                             <local:MyComboBoxItem Content="低(优先保证其他程序运行,但可能让游戏卡顿,适合挂机时使用)" />
                         </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.ColumnDefinitions>
                                 <ColumnDefinition />
@@ -125,11 +125,10 @@
                             </Grid.ColumnDefinitions>
                             <local:MyComboBox x:Name="ComboArgumentWindowType" Tag="LaunchArgumentWindowType">
                                 <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: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.ValidateRules>
@@ -138,7 +137,7 @@
                             </local:MyTextBox>
                             <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"
-                                              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:ValidateInteger Min="1" Max="9999" />
                                 </local:MyTextBox.ValidateRules>
@@ -175,11 +174,16 @@
                             <RowDefinition Height="Auto" />
                             <RowDefinition Height="9" />
                             <RowDefinition Height="Auto" />
+                            <RowDefinition Height="9" />
+                            <RowDefinition Height="Auto" />
                         </Grid.RowDefinitions>
                         <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" />
                         <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: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" />-->
                     </Grid>
                     <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" />
                     </Grid.RowDefinitions>
                     <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" />
                     <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 则会以试玩模式启动游戏。" />

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

@@ -38,6 +38,7 @@
             ComboArgumentWindowType.SelectedIndex = Setup.Get("LaunchArgumentWindowType")
             TextArgumentWindowWidth.Text = Setup.Get("LaunchArgumentWindowWidth")
             TextArgumentWindowHeight.Text = Setup.Get("LaunchArgumentWindowHeight")
+            CheckArgumentRam.Checked = Setup.Get("LaunchArgumentRam")
             RefreshJavaComboBox()
 
             '游戏内存
@@ -71,6 +72,7 @@
             Setup.Reset("LaunchArgumentWindowWidth")
             Setup.Reset("LaunchArgumentWindowHeight")
             Setup.Reset("LaunchArgumentPriority")
+            Setup.Reset("LaunchArgumentRam")
             Setup.Reset("LaunchRamType")
             Setup.Reset("LaunchRamCustom")
             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
         If AniControlEnabled = 0 Then Setup.Set(sender.Tag, sender.SelectedIndex)
     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)
     End Sub
 
@@ -352,19 +354,19 @@
                 '可安装 Mod 的版本
                 Dim ModDir As New DirectoryInfo(Version.PathIndie & "mods\")
                 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
                 'OptiFine 版本
-                RamMininum = 0.3
+                RamMininum = 0.5
                 RamTarget1 = 1.5
                 RamTarget2 = 3
-                RamTarget3 = 6
+                RamTarget3 = 5
             Else
                 '普通版本
-                RamMininum = 0.3
+                RamMininum = 0.5
                 RamTarget1 = 1.5
                 RamTarget2 = 2.5
                 RamTarget3 = 4
@@ -372,27 +374,23 @@
             Dim RamDelta As Double
             '预分配内存,阶段一,0 ~ T1,100%
             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
-            '预分配内存,阶段二,T1 ~ T2,80%
+            '预分配内存,阶段二,T1 ~ T2,70%
             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
-            '预分配内存,阶段三,T2 ~ T3,60%
+            '预分配内存,阶段三,T2 ~ T3,40%
             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
-            '预分配内存,阶段四,T3 ~ T3 * 2,40%
+            '预分配内存,阶段四,T3 ~ T3 * 2,15%
             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
 PreFin:
             '不低于最低值
@@ -444,7 +442,7 @@ PreFin:
             Log(ex, "更新设置 Java 下拉框失败", LogLevel.Feedback)
         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
         '结束处理
         If SelectedItem Is Nothing Then
@@ -515,15 +513,16 @@ PreFin:
             Hint("正在搜索 Java,请稍候!", HintType.Critical)
             Exit Sub
         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 Region
@@ -548,12 +547,24 @@ PreFin:
         If AniControlEnabled <> 0 Then Exit Sub
         If ComboArgumentVisibie.SelectedIndex = 0 Then
             If MyMsgBox("若在游戏启动后立即关闭启动器,崩溃检测、更改游戏标题等功能将失效。" & vbCrLf &
-                        "如果想保留这些功能,可以选择让启动器在游戏启动后隐藏,游戏退出后自动关闭。", "提示", "继续", "取消") = 2 Then
+                        "如果想保留这些功能,可以选择让启动器在游戏启动后隐藏,游戏退出后自动关闭。", "提醒", "继续", "取消") = 2 Then
                 ComboArgumentVisibie.SelectedItem = e.RemovedItems(0)
             End If
         End If
     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
 
 #Region "高级设置"

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

@@ -28,7 +28,6 @@
                                 <local:ValidateNullable />
                                 <local:ValidateNullOrWhiteSpace />
                                 <local:ValidateLength Max="20" />
-                                <local:ValidateFilter />
                             </local:MyTextBox.ValidateRules>
                         </local:MyTextBox>
                     </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 全版本。" />
                         <local:MyCheckBox Margin="0,2,0,4" Text="验证 SSL 证书" Grid.Row="9" Height="22" Grid.ColumnSpan="2"
                                           x:Name="CheckDownloadCert" Tag="ToolDownloadCert" 
-                                          ToolTip="在进行连接时,检查 SSL 证书是否正常。&#xa;进行验证会提高安全性,但疑似会导致少部分玩家的微软登录失败。" />
+                                          ToolTip="验证证书会提高安全性,但疑似有概率导致正版登录失败(#3018)。&#xa;如果在正版登录时因为 [根据验证过程,远程证书无效] 失败,可以尝试关闭此项。" />
                     </Grid>
                 </StackPanel>
             </local:MyCard>

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

@@ -126,23 +126,21 @@
 
     '滑动条
     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
     Private Sub SliderDownloadThread_PreviewChange(sender As Object, e As RouteEventArgs) Handles SliderDownloadThread.PreviewChange
         If SliderDownloadThread.Value < 100 Then Exit Sub
@@ -158,7 +156,7 @@
         PageOtherAbout.CopyUniqueAddress()
     End Sub
     Private Sub BtnSystemUnlock_Click(sender As Object, e As MouseButtonEventArgs) Handles BtnSystemUnlock.Click
-        PageOtherAbout.DonateCodeInput()
+        DonateCodeInput()
     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
 
     '初始化
@@ -59,8 +61,8 @@
         If FrmSpeedRight Is Nothing OrElse FrmSpeedRight.PanMain Is Nothing Then Exit Sub
         Try
             SyncLock LoaderTaskbarLock
-                For i = 0 To LoaderTaskbar.Count - 1
-                    TaskRefresh(LoaderTaskbar(i))
+                For Each Loader In LoaderTaskbar.ToList
+                    TaskRefresh(Loader)
                 Next
             End SyncLock
         Catch ex As Exception
@@ -110,7 +112,7 @@
 #Region "进度不同,更新卡片"
                             Try
                                 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
                                 End If
                                 Dim Row As Integer = 0
@@ -137,7 +139,7 @@
                                     Row += 1
                                 Next
                             Catch ex As Exception
-                                Log(ex, "刷新下载管理卡片失败", LogLevel.Feedback)
+                                Log(ex, $"刷新下载管理卡片 {Loader.Name} 失败", LogLevel.Feedback)
                             End Try
 #End Region
                     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}
                     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
                         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
         Try
             Dim Result = McModCheck(PageVersionLeft.Version, McModLoader.Output)
-            If Result.Count > 0 Then
+            If Result.Any Then
                 MyMsgBox(Join(Result, vbCrLf & vbCrLf), "Mod 检查结果")
             Else
                 Hint("Mod 检查完成,未发现任何问题!", HintType.Finish)
@@ -620,13 +620,13 @@
                 If ModEntry.ModId IsNot Nothing Then
                     DebugInfo.Add("Mod ID:" & ModEntry.ModId)
                 End If
-                If ModEntry.Dependencies.Count > 0 Then
+                If ModEntry.Dependencies.Any Then
                     DebugInfo.Add("依赖于:")
                     For Each Dep In ModEntry.Dependencies
                         DebugInfo.Add(" - " & Dep.Key & If(Dep.Value Is Nothing, "", ",版本:" & Dep.Value))
                     Next
                 End If
-                If DebugInfo.Count > 0 Then
+                If DebugInfo.Any Then
                     ContentLines.Add("")
                     ContentLines.AddRange(DebugInfo)
                 End If

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

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

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

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

BIN
Plain Craft Launcher 2/Resources/Help.zip


Разница между файлами не показана из-за своего большого размера
+ 150 - 150
Plain Craft Launcher 2/Resources/ModData.txt


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

@@ -29,8 +29,8 @@
       <!-- Windows Vista -->
       <!--<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 -->
       <!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
@@ -38,7 +38,7 @@
       <!-- Windows 8.1 -->
       <!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
 
-      <!-- Windows 10 -->
+      <!-- Windows 10+ -->
       <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
 
     </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):来参加投票吧,开发者会优先处理票数较高的帖子!
 - [程序下载](https://afdian.net/p/0164034c016c11ebafcb52540025c377):下载免费的正式版 PCL。
 - [帮助文档库](https://github.com/LTCatt/PCL2Help):PCL 帮助文档在 GitHub 上的存储库(是的,帮助库在另一个 Repo……)
 
- 
+<br/>
 
-**一些无趣的小细节:**
-- 别想在这里面找到解密游戏的线索,那部分的代码被我抠掉了(笑)
-- 哦,对,解锁隐藏主题的相关代码也都被抠掉了,所以别想着用翻源代码这种歪门邪道来绕过解密了.jpg
+## 一些无趣的小细节
+- 别想在这里面找到解密游戏的线索,那部分的代码被我抠掉了
+- 解锁隐藏主题的相关代码也被抠掉了,所以别想着用翻源代码这种歪门邪道来绕过解密了.jpg
 - 源代码库并不是即时更新的,而是在每次 PCL 发布更新时(手动)同步一次

BIN
最新正式版.zip


Некоторые файлы не были показаны из-за большого количества измененных файлов