龙腾猫跃 5 месяцев назад
Родитель
Сommit
67764b393d
100 измененных файлов с 4793 добавлено и 4756 удалено
  1. 3 0
      .gitmodules
  2. 60 0
      PCLCS/PCLCS.csproj
  3. 33 6
      Plain Craft Launcher 2.sln
  4. 1 1
      Plain Craft Launcher 2/Application.xaml
  5. 97 83
      Plain Craft Launcher 2/Application.xaml.vb
  6. 1 1
      Plain Craft Launcher 2/Controls/Behaviors/ClipboardInterceptor.vb
  7. 9 9
      Plain Craft Launcher 2/Controls/MyCard.vb
  8. 4 13
      Plain Craft Launcher 2/Controls/MyComboBox.vb
  9. 3 1
      Plain Craft Launcher 2/Controls/MyIconButton.xaml.vb
  10. 3 3
      Plain Craft Launcher 2/Controls/MyImage.vb
  11. 3 3
      Plain Craft Launcher 2/Controls/MyListItem.xaml.vb
  12. 1 1
      Plain Craft Launcher 2/Controls/MyLoading.xaml.vb
  13. 2 2
      Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml.vb
  14. 2 2
      Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml.vb
  15. 2 2
      Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml.vb
  16. 2 1
      Plain Craft Launcher 2/Controls/MyRadioBox.xaml.vb
  17. 1 1
      Plain Craft Launcher 2/Controls/MyRadioButton.xaml.vb
  18. 1 1
      Plain Craft Launcher 2/Controls/MyScrollViewer.vb
  19. 7 5
      Plain Craft Launcher 2/Controls/MySlider.xaml.vb
  20. 1 1
      Plain Craft Launcher 2/Controls/MyTextBox.vb
  21. 297 298
      Plain Craft Launcher 2/FormMain.xaml.vb
  22. BIN
      Plain Craft Launcher 2/Images/Icons/Unavailable.png
  23. 0 0
      Plain Craft Launcher 2/Images/ReleaseTypes/Alpha.png
  24. 0 0
      Plain Craft Launcher 2/Images/ReleaseTypes/Beta.png
  25. 0 0
      Plain Craft Launcher 2/Images/ReleaseTypes/Release.png
  26. 69 39
      Plain Craft Launcher 2/Modules/Base/ModAnimation.vb
  27. 181 450
      Plain Craft Launcher 2/Modules/Base/ModBase.vb
  28. 9 9
      Plain Craft Launcher 2/Modules/Base/ModLoader.vb
  29. 58 61
      Plain Craft Launcher 2/Modules/Base/ModNet.vb
  30. 7 7
      Plain Craft Launcher 2/Modules/Base/ModValidate.vb
  31. 1 1
      Plain Craft Launcher 2/Modules/Base/MyBitmap.vb
  32. 78 0
      Plain Craft Launcher 2/Modules/Constants.vb
  33. 0 1776
      Plain Craft Launcher 2/Modules/Minecraft/ModComp.vb
  34. 10 10
      Plain Craft Launcher 2/Modules/Minecraft/ModCrash.vb
  35. 13 13
      Plain Craft Launcher 2/Modules/Minecraft/ModDownload.vb
  36. 22 22
      Plain Craft Launcher 2/Modules/Minecraft/ModJava.vb
  37. 57 61
      Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb
  38. 76 78
      Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb
  39. 0 876
      Plain Craft Launcher 2/Modules/Minecraft/ModMod.vb
  40. 57 33
      Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb
  41. 9 9
      Plain Craft Launcher 2/Modules/Minecraft/ModWatcher.vb
  42. 3 5
      Plain Craft Launcher 2/Modules/ModDevelop.vb
  43. 12 12
      Plain Craft Launcher 2/Modules/ModEvent.vb
  44. 43 41
      Plain Craft Launcher 2/Modules/ModMain.vb
  45. 26 22
      Plain Craft Launcher 2/Modules/ModMusic.vb
  46. 35 74
      Plain Craft Launcher 2/Modules/ModSecret.vb
  47. 507 0
      Plain Craft Launcher 2/Modules/Resource/ResourceFile.vb
  48. 345 0
      Plain Craft Launcher 2/Modules/Resource/ResourceFileLoader.vb
  49. 726 0
      Plain Craft Launcher 2/Modules/Resource/ResourceProject.vb
  50. 515 0
      Plain Craft Launcher 2/Modules/Resource/ResourceSearcher.vb
  51. 405 0
      Plain Craft Launcher 2/Modules/Resource/ResourceVersion.vb
  52. 85 0
      Plain Craft Launcher 2/Modules/Resource/WikiEntry.vb
  53. 2 2
      Plain Craft Launcher 2/My Project/AssemblyInfo.vb
  54. 30 0
      Plain Craft Launcher 2/My Project/Resources.Designer.vb
  55. 9 0
      Plain Craft Launcher 2/My Project/Resources.resx
  56. 54 58
      Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb
  57. 22 20
      Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb
  58. 8 16
      Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLeft.xaml.vb
  59. 4 4
      Plain Craft Launcher 2/Pages/PageDownload/Resource/MyResourceItem.xaml
  60. 17 17
      Plain Craft Launcher 2/Pages/PageDownload/Resource/MyResourceItem.xaml.vb
  61. 1 1
      Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadCompDetail.xaml
  62. 116 91
      Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadCompDetail.xaml.vb
  63. 2 2
      Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadDataPack.xaml
  64. 0 0
      Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadDataPack.xaml.vb
  65. 2 2
      Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadMod.xaml
  66. 0 0
      Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadMod.xaml.vb
  67. 2 2
      Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadPack.xaml
  68. 0 0
      Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadPack.xaml.vb
  69. 2 2
      Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadResourcePack.xaml
  70. 0 0
      Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadResourcePack.xaml.vb
  71. 2 2
      Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadShader.xaml
  72. 0 0
      Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadShader.xaml.vb
  73. 1 1
      Plain Craft Launcher 2/Pages/PageDownload/Resource/PageResource.xaml
  74. 20 20
      Plain Craft Launcher 2/Pages/PageDownload/Resource/PageResource.xaml.vb
  75. 31 41
      Plain Craft Launcher 2/Pages/PageInstance/MyLocalModItem.xaml.vb
  76. 44 50
      Plain Craft Launcher 2/Pages/PageInstance/PageInstanceExport.xaml.vb
  77. 1 1
      Plain Craft Launcher 2/Pages/PageInstance/PageInstanceLeft.xaml.vb
  78. 112 119
      Plain Craft Launcher 2/Pages/PageInstance/PageInstanceMod.xaml.vb
  79. 18 18
      Plain Craft Launcher 2/Pages/PageInstance/PageInstanceOverall.xaml.vb
  80. 24 23
      Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSetup.xaml
  81. 22 89
      Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSetup.xaml.vb
  82. 2 2
      Plain Craft Launcher 2/Pages/PageLaunch/MyMsgLogin.xaml.vb
  83. 7 7
      Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml.vb
  84. 18 17
      Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchLeft.xaml.vb
  85. 2 9
      Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml
  86. 11 22
      Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.vb
  87. 2 2
      Plain Craft Launcher 2/Pages/PageLaunch/PageLoginAuthSkin.xaml.vb
  88. 4 4
      Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMs.xaml.vb
  89. 2 2
      Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMsSkin.xaml
  90. 2 8
      Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMsSkin.xaml.vb
  91. 2 1
      Plain Craft Launcher 2/Pages/PageLaunch/PageLoginNideSkin.xaml
  92. 0 3
      Plain Craft Launcher 2/Pages/PageLaunch/PageLoginNideSkin.xaml.vb
  93. 22 22
      Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml.vb
  94. 254 3
      Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml
  95. 7 10
      Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml.vb
  96. 10 10
      Plain Craft Launcher 2/Pages/PageOther/PageOtherHelp.xaml.vb
  97. 2 2
      Plain Craft Launcher 2/Pages/PageOther/PageOtherHelpDetail.xaml.vb
  98. 1 1
      Plain Craft Launcher 2/Pages/PageOther/PageOtherLeft.xaml.vb
  99. 1 0
      Plain Craft Launcher 2/Pages/PageOther/PageOtherTest.xaml.vb
  100. 16 17
      Plain Craft Launcher 2/Pages/PageSelectLeft.xaml.vb

+ 3 - 0
.gitmodules

@@ -0,0 +1,3 @@
+[submodule "MeloongCore"]
+	path = MeloongCore
+	url = https://github.com/Meloong-Git/MeloongCore.git

+ 60 - 0
PCLCS/PCLCS.csproj

@@ -0,0 +1,60 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+	<PropertyGroup>
+		<TargetFramework>net462</TargetFramework>
+		<OutputType>Library</OutputType>
+		<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
+		<OutDir>bin\</OutDir>
+		<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
+		<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
+		<Configurations>Debug;Release;Snapshot</Configurations>
+		<Deterministic>true</Deterministic>
+		<LangVersion>13.0</LangVersion>
+		<Nullable>enable</Nullable>
+		<AnalysisLevel>8.0</AnalysisLevel>
+	</PropertyGroup>
+
+	<ItemGroup>
+	  <PackageReference Include="Nullable" Version="1.3.1">
+	    <PrivateAssets>all</PrivateAssets>
+	    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+	  </PackageReference>
+	  <PackageReference Include="System.ValueTuple" Version="4.6.1" />
+	</ItemGroup>
+
+	<ItemGroup>
+	  <ProjectReference Include="..\MeloongCore\Shared\MeloongCore.csproj" />
+	  <ProjectReference Include="..\MeloongCore\Wpf\MeloongCoreWpf.csproj" />
+	</ItemGroup>
+
+	<ItemGroup>
+		<Using Include="System" />
+		<Using Include="System.ComponentModel" />
+		<Using Include="System.Collections" />
+		<Using Include="System.Collections.Generic" />
+		<Using Include="System.Diagnostics.CodeAnalysis" />
+		<Using Include="System.Linq" />
+		<Using Include="System.Text" />
+		<Using Include="System.IO" />
+		<Using Include="System.Net" />
+		<Using Include="System.Threading" />
+		<Using Include="System.Collections.Concurrent" />
+		<Using Include="System.Runtime.CompilerServices" />
+		<Using Include="System.Runtime.InteropServices" />
+		<Using Include="MeloongCore" />
+		<Using Include="MeloongCore.Extensions" />
+		<Using Include="MeloongCore.Wpf" />
+		<Using Include="MeloongCore.Wpf.Extensions" />
+	</ItemGroup>
+
+	<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+		<DefineConstants>$(DefineConstants);SNAPSHOT</DefineConstants>
+	</PropertyGroup>
+
+	<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'" />
+
+	<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Snapshot|AnyCPU'">
+		<Optimize>True</Optimize>
+	</PropertyGroup>
+
+</Project>

+ 33 - 6
Plain Craft Launcher 2.sln

@@ -5,22 +5,49 @@ VisualStudioVersion = 17.2.32616.157
 MinimumVisualStudioVersion = 10.0.40219.1
 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}"
 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
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PCLCS", "PCLCS\PCLCS.csproj", "{32DE4AAF-F4D6-42C5-9604-19236DE96C69}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeloongCore", "MeloongCore\Shared\MeloongCore.csproj", "{D22BBF3C-2177-0208-4E29-8466AA41AC6F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeloongCoreWpf", "MeloongCore\Wpf\MeloongCoreWpf.csproj", "{2DFFE2A0-1101-816B-D240-F644B2D74A68}"
+EndProject
 Global
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
-		Beta|Any CPU = Beta|Any CPU
 		Debug|Any CPU = Debug|Any CPU
 		Debug|Any CPU = Debug|Any CPU
 		Release|Any CPU = Release|Any CPU
 		Release|Any CPU = Release|Any CPU
-		ReleaseUpdate|Any CPU = ReleaseUpdate|Any CPU
+		Snapshot|Any CPU = Snapshot|Any CPU
 	EndGlobalSection
 	EndGlobalSection
 	GlobalSection(ProjectConfigurationPlatforms) = postSolution
 	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.ActiveCfg = Debug|Any CPU
 		{4499F2D2-18EF-4238-B1F0-7BD547AFF925}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
 		{4499F2D2-18EF-4238-B1F0-7BD547AFF925}.Release|Any CPU.Build.0 = 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
+		{4499F2D2-18EF-4238-B1F0-7BD547AFF925}.Snapshot|Any CPU.ActiveCfg = Snapshot|Any CPU
+		{4499F2D2-18EF-4238-B1F0-7BD547AFF925}.Snapshot|Any CPU.Build.0 = Snapshot|Any CPU
+		{32DE4AAF-F4D6-42C5-9604-19236DE96C69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{32DE4AAF-F4D6-42C5-9604-19236DE96C69}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{32DE4AAF-F4D6-42C5-9604-19236DE96C69}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{32DE4AAF-F4D6-42C5-9604-19236DE96C69}.Release|Any CPU.Build.0 = Release|Any CPU
+		{32DE4AAF-F4D6-42C5-9604-19236DE96C69}.Snapshot|Any CPU.ActiveCfg = Snapshot|Any CPU
+		{32DE4AAF-F4D6-42C5-9604-19236DE96C69}.Snapshot|Any CPU.Build.0 = Snapshot|Any CPU
+		{AFB035DA-CB49-49CE-B96F-581BF8368CCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{AFB035DA-CB49-49CE-B96F-581BF8368CCF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{AFB035DA-CB49-49CE-B96F-581BF8368CCF}.Release|Any CPU.ActiveCfg = Debug|Any CPU
+		{AFB035DA-CB49-49CE-B96F-581BF8368CCF}.Release|Any CPU.Build.0 = Debug|Any CPU
+		{AFB035DA-CB49-49CE-B96F-581BF8368CCF}.Snapshot|Any CPU.ActiveCfg = Debug|Any CPU
+		{AFB035DA-CB49-49CE-B96F-581BF8368CCF}.Snapshot|Any CPU.Build.0 = Debug|Any CPU
+		{D22BBF3C-2177-0208-4E29-8466AA41AC6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{D22BBF3C-2177-0208-4E29-8466AA41AC6F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{D22BBF3C-2177-0208-4E29-8466AA41AC6F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{D22BBF3C-2177-0208-4E29-8466AA41AC6F}.Release|Any CPU.Build.0 = Release|Any CPU
+		{D22BBF3C-2177-0208-4E29-8466AA41AC6F}.Snapshot|Any CPU.ActiveCfg = Snapshot|Any CPU
+		{D22BBF3C-2177-0208-4E29-8466AA41AC6F}.Snapshot|Any CPU.Build.0 = Snapshot|Any CPU
+		{2DFFE2A0-1101-816B-D240-F644B2D74A68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{2DFFE2A0-1101-816B-D240-F644B2D74A68}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{2DFFE2A0-1101-816B-D240-F644B2D74A68}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{2DFFE2A0-1101-816B-D240-F644B2D74A68}.Release|Any CPU.Build.0 = Release|Any CPU
+		{2DFFE2A0-1101-816B-D240-F644B2D74A68}.Snapshot|Any CPU.ActiveCfg = Snapshot|Any CPU
+		{2DFFE2A0-1101-816B-D240-F644B2D74A68}.Snapshot|Any CPU.Build.0 = Snapshot|Any CPU
 	EndGlobalSection
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
 		HideSolutionNode = FALSE

+ 1 - 1
Plain Craft Launcher 2/Application.xaml

@@ -220,7 +220,7 @@
                     </Setter.Value>
                     </Setter.Value>
                 </Setter>
                 </Setter>
             </Style>
             </Style>
-            <Style TargetType="local:PageComp" BasedOn="{StaticResource {x:Type local:MyScrollViewer}}" />
+            <Style TargetType="local:PageResource" BasedOn="{StaticResource {x:Type local:MyScrollViewer}}" />
             <Style TargetType="ScrollViewer" x:Key="ScrollViewerFullMargin">
             <Style TargetType="ScrollViewer" x:Key="ScrollViewerFullMargin">
                 <Setter Property="OverridesDefaultStyle" Value="True" />
                 <Setter Property="OverridesDefaultStyle" Value="True" />
                 <Setter Property="CanContentScroll" Value="False" />
                 <Setter Property="CanContentScroll" Value="False" />

+ 97 - 83
Plain Craft Launcher 2/Application.xaml.vb

@@ -3,26 +3,45 @@ Imports System.Windows.Threading
 
 
 Public Class Application
 Public Class Application
 
 
-#If DEBUG Then
-    ''' <summary>
-    ''' 用于开始程序时的一些测试。
-    ''' </summary>
-    Private Sub Test()
-        Try
-            ModDevelop.Start()
-        Catch ex As Exception
-            Log(ex, "开发者模式测试出错", LogLevel.Msgbox)
-        End Try
-    End Sub
-#End If
-
     '开始
     '开始
     Private Sub Application_Startup(sender As Object, e As StartupEventArgs) Handles Me.Startup
     Private Sub Application_Startup(sender As Object, e As StartupEventArgs) Handles Me.Startup
         Try
         Try
-            '动态 DLL 调用(必须尽量在前面,否则模块加载 CacheCow 等 DLL 就可能导致崩溃)
-            AddHandler AppDomain.CurrentDomain.AssemblyResolve, AddressOf AssemblyResolve
-            '开始
-            SecretOnApplicationStart()
+            '执行开发版测试
+            If BuildType = BuildTypes.Debug Then
+                Try
+                    ModDevelop.Start()
+                Catch ex As Exception
+                    Log(ex, "开发者模式测试出错", NotifyLevel.MsgBox)
+                End Try
+            End If
+            '提升主线程优先级
+            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
+                DirectoryUtils.Create(PathExeFolder & "PCL\")
+            Catch ex As Exception
+                MsgBox($"PCL 无法创建 PCL 文件夹({PathExeFolder & "PCL"}),请尝试:" & vbCrLf &
+                  "1. 将 PCL 移动到其他文件夹" & If(PathExeFolder.StartsWithF("C:", True), ",例如 C 盘和桌面以外的其他位置。", "。") & vbCrLf &
+                  "2. 删除当前目录中的 PCL 文件夹,然后再试。" & vbCrLf &
+                  "3. 右键 PCL 选择属性,打开 兼容性 中的 以管理员身份运行此程序。",
+                MsgBoxStyle.Critical, "运行环境错误")
+                Environment.[Exit](ProcessReturnValues.Cancel)
+            End Try
+            If Not CheckPermission(PathExeFolder & "PCL") Then
+                MsgBox("PCL 没有对当前文件夹的写入权限,请尝试:" & vbCrLf &
+                  "1. 将 PCL 移动到其他文件夹" & If(PathExeFolder.StartsWithF("C:", True), ",例如 C 盘和桌面以外的其他位置。", "。") & vbCrLf &
+                  "2. 删除当前目录中的 PCL 文件夹,然后再试。" & vbCrLf &
+                  "3. 右键 PCL 选择属性,打开 兼容性 中的 以管理员身份运行此程序。",
+                MsgBoxStyle.Critical, "运行环境错误")
+                Environment.[Exit](ProcessReturnValues.Cancel)
+            End If
             '检查参数调用
             '检查参数调用
             If e.Args.Length > 0 Then
             If e.Args.Length > 0 Then
                 If e.Args(0) = "--update" Then
                 If e.Args(0) = "--update" Then
@@ -54,42 +73,42 @@ Public Class Application
                 End If
                 End If
             End If
             End If
             '初始化文件结构
             '初始化文件结构
-            Directory.CreateDirectory(Path & "PCL\Pictures")
-            Directory.CreateDirectory(Path & "PCL\Musics")
+            DirectoryUtils.Create(PathExeFolder & "PCL\Pictures\")
+            DirectoryUtils.Create(PathExeFolder & "PCL\Musics\")
             Try
             Try
-                Directory.CreateDirectory(PathTemp)
+                DirectoryUtils.Create(PathTemp)
                 If Not CheckPermission(PathTemp) Then Throw New Exception("PCL 没有对 " & PathTemp & " 的访问权限")
                 If Not CheckPermission(PathTemp) Then Throw New Exception("PCL 没有对 " & PathTemp & " 的访问权限")
             Catch ex As Exception
             Catch ex As Exception
-                If PathTemp = IO.Path.GetTempPath() & "PCL\" Then
+                If PathTemp = Path.GetTempPath() & "PCL\" Then
                     MyMsgBox("PCL 无法访问缓存文件夹,可能导致程序出错或无法正常使用!" & vbCrLf & vbCrLf & "错误原因:" & ex.GetDetail(), "缓存文件夹不可用")
                     MyMsgBox("PCL 无法访问缓存文件夹,可能导致程序出错或无法正常使用!" & vbCrLf & vbCrLf & "错误原因:" & ex.GetDetail(), "缓存文件夹不可用")
                 Else
                 Else
                     MyMsgBox("手动设置的缓存文件夹不可用,PCL 将使用默认缓存文件夹。" & vbCrLf & vbCrLf & "错误原因:" & ex.GetDetail(), "缓存文件夹不可用")
                     MyMsgBox("手动设置的缓存文件夹不可用,PCL 将使用默认缓存文件夹。" & vbCrLf & vbCrLf & "错误原因:" & ex.GetDetail(), "缓存文件夹不可用")
                     Settings.Set("SystemSystemCache", "")
                     Settings.Set("SystemSystemCache", "")
-                    PathTemp = IO.Path.GetTempPath() & "PCL\"
+                    PathTemp = Path.GetTempPath() & "PCL\"
                 End If
                 End If
             End Try
             End Try
-            Directory.CreateDirectory(PathTemp & "Cache")
-            Directory.CreateDirectory(PathAppdata)
-            '检测单例
-#If Not DEBUG Then
-            Dim ShouldWaitForExit As Boolean = e.Args.Length > 0 AndAlso e.Args(0) = "--wait" '要求等待已有的 PCL 退出
-            Dim WaitRetryCount As Integer = 0
+            DirectoryUtils.Create(PathTemp & "Cache\")
+            DirectoryUtils.Create(PathAppdata)
+            '要求单例
+            If BuildType <> BuildTypes.Debug Then
+                Dim ShouldWaitForExit As Boolean = e.Args.Length > 0 AndAlso e.Args(0) = "--wait" '要求等待已有的 PCL 退出
+                Dim WaitRetryCount As Integer = 0
 WaitRetry:
 WaitRetry:
-            Dim WindowHwnd As IntPtr = FindWindow(Nothing, "Plain Craft Launcher ")
-            If WindowHwnd = IntPtr.Zero Then FindWindow(Nothing, "Plain Craft Launcher 2 ")
-            If WindowHwnd <> IntPtr.Zero Then
-                If ShouldWaitForExit AndAlso WaitRetryCount < 20 Then '至多等待 10 秒
-                    WaitRetryCount += 1
-                    Thread.Sleep(500)
-                    GoTo WaitRetry
+                Dim WindowHwnd As IntPtr = FindWindow(Nothing, "Plain Craft Launcher ")
+                If WindowHwnd = IntPtr.Zero Then FindWindow(Nothing, "Plain Craft Launcher 2 ")
+                If WindowHwnd <> IntPtr.Zero Then
+                    If ShouldWaitForExit AndAlso WaitRetryCount < 20 Then '至多等待 10 秒
+                        WaitRetryCount += 1
+                        Thread.Sleep(500)
+                        GoTo WaitRetry
+                    End If
+                    '将已有的 PCL 窗口拖出来
+                    ShowWindowToTop(WindowHwnd)
+                    '播放提示音并退出
+                    Beep()
+                    Environment.[Exit](ProcessReturnValues.Cancel)
                 End If
                 End If
-                '将已有的 PCL 窗口拖出来
-                ShowWindowToTop(WindowHwnd)
-                '播放提示音并退出
-                Beep()
-                Environment.[Exit](ProcessReturnValues.Cancel)
             End If
             End If
-#End If
             '设置 ToolTipService 默认值
             '设置 ToolTipService 默认值
             ToolTipService.InitialShowDelayProperty.OverrideMetadata(GetType(DependencyObject), New FrameworkPropertyMetadata(300))
             ToolTipService.InitialShowDelayProperty.OverrideMetadata(GetType(DependencyObject), New FrameworkPropertyMetadata(300))
             ToolTipService.BetweenShowDelayProperty.OverrideMetadata(GetType(DependencyObject), New FrameworkPropertyMetadata(400))
             ToolTipService.BetweenShowDelayProperty.OverrideMetadata(GetType(DependencyObject), New FrameworkPropertyMetadata(400))
@@ -112,17 +131,17 @@ WaitRetry:
             '日志初始化
             '日志初始化
             LogStart()
             LogStart()
             '添加日志
             '添加日志
-            Log($"[Start] 程序版本:{VersionDisplayName} ({VersionCode}{If(CommitHash = "", "", $",#{CommitHash}")})")
-#If RELEASE Then
-            Log($"[Start] 识别码:{Identify}{If(ThemeCheckOne(9), ",正式版", "")}")
-#Else
-            Log($"[Start] 识别码:{Identify}{If(ThemeCheckOne(9), ",已解锁反馈主题", "")}")
-#End If
-            Log($"[Start] 程序路径:{PathWithName}")
+            Log($"[Start] 程序版本:{VersionDisplay} ({VersionCode}{If(CommitHash = "", "", $",#{CommitHash}")})")
+            If BuildType = BuildTypes.Snapshot Then
+                Log($"[Start] 识别码:{Identify}{If(ThemeCheckOne(9), ",已解锁反馈主题", ",未解锁反馈主题")}")
+            Else
+                Log($"[Start] 识别码:{Identify}")
+            End If
+            Log($"[Start] 程序路径:{PathExe}")
             Log($"[Start] 系统编码:{Encoding.Default.HeaderName} ({Encoding.Default.CodePage}, GBK={IsGBKEncoding})")
             Log($"[Start] 系统编码:{Encoding.Default.HeaderName} ({Encoding.Default.CodePage}, GBK={IsGBKEncoding})")
             Log($"[Start] 管理员权限:{IsAdmin()}")
             Log($"[Start] 管理员权限:{IsAdmin()}")
             '检测异常环境
             '检测异常环境
-            If Path.Contains(IO.Path.GetTempPath()) OrElse Path.Contains("AppData\Local\Temp\") Then
+            If PathExeFolder.Contains(Path.GetTempPath()) OrElse PathExeFolder.Contains("AppData\Local\Temp\") Then
                 MyMsgBox("请将 PCL 从压缩包中解压后再使用!" & vbCrLf & "如果不会解压,可以在网上寻找教程。", "需要解压!", "我知道了", IsWarn:=True, ForceWait:=True)
                 MyMsgBox("请将 PCL 从压缩包中解压后再使用!" & vbCrLf & "如果不会解压,可以在网上寻找教程。", "需要解压!", "我知道了", IsWarn:=True, ForceWait:=True)
                 FormMain.EndProgramForce(ProcessReturnValues.Cancel)
                 FormMain.EndProgramForce(ProcessReturnValues.Cancel)
             End If
             End If
@@ -132,15 +151,11 @@ WaitRetry:
             '计时
             '计时
             Log("[Start] 第一阶段加载用时:" & GetTimeMs() - ApplicationStartTick & " ms")
             Log("[Start] 第一阶段加载用时:" & GetTimeMs() - ApplicationStartTick & " ms")
             ApplicationStartTick = GetTimeMs()
             ApplicationStartTick = GetTimeMs()
-            '执行测试
-#If DEBUG Then
-            Test()
-#End If
             AniControlEnabled += 1
             AniControlEnabled += 1
         Catch ex As Exception
         Catch ex As Exception
             Dim FilePath As String = Nothing
             Dim FilePath As String = Nothing
             Try
             Try
-                FilePath = PathWithName
+                FilePath = PathExe
             Catch
             Catch
             End Try
             End Try
             MsgBox(ex.GetDetail(True) & vbCrLf & "PCL 所在路径:" & If(String.IsNullOrEmpty(FilePath), "获取失败", FilePath), MsgBoxStyle.Critical, "PCL 初始化错误")
             MsgBox(ex.GetDetail(True) & vbCrLf & "PCL 所在路径:" & If(String.IsNullOrEmpty(FilePath), "获取失败", FilePath), MsgBoxStyle.Critical, "PCL 初始化错误")
@@ -173,41 +188,40 @@ WaitRetry:
         If Detail.Contains("System.Windows.Threading.Dispatcher.Invoke") OrElse Detail.Contains("MS.Internal.AppModel.ITaskbarList.HrInit") OrElse Detail.Contains("未能加载文件或程序集") OrElse
         If Detail.Contains("System.Windows.Threading.Dispatcher.Invoke") OrElse Detail.Contains("MS.Internal.AppModel.ITaskbarList.HrInit") OrElse Detail.Contains("未能加载文件或程序集") OrElse
            Detail.Contains(".NET Framework") Then ' “自动错误判断” 的结果分析
            Detail.Contains(".NET Framework") Then ' “自动错误判断” 的结果分析
             OpenWebsite("https://dotnet.microsoft.com/zh-cn/download/dotnet-framework/thank-you/net462-offline-installer")
             OpenWebsite("https://dotnet.microsoft.com/zh-cn/download/dotnet-framework/thank-you/net462-offline-installer")
-            Log(e.Exception, "你的 .NET Framework 版本过低或损坏,请下载并重新安装 .NET Framework 4.6.2!" & vbCrLf & "若无法安装,可在卸载高版本的 .NET Framework 后再试。", LogLevel.Critical, "运行环境错误")
+            Log(e.Exception, "你的 .NET Framework 版本过低或损坏,请下载并重新安装 .NET Framework 4.6.2!" & vbCrLf & "若无法安装,可在卸载高版本的 .NET Framework 后再试。", NotifyLevel.Critical, "运行环境错误")
         Else
         Else
-            Log(e.Exception, "程序出现未知错误", LogLevel.Critical, "锟斤拷烫烫烫")
+            Log(e.Exception, "程序出现未知错误", NotifyLevel.Critical, "锟斤拷烫烫烫")
         End If
         End If
     End Sub
     End Sub
 
 
-    '动态 DLL 调用
-    Private Declare Function SetDllDirectory Lib "kernel32" Alias "SetDllDirectoryA" (lpPathName As String) As Boolean
-    Public Shared Function AssemblyResolve(sender As Object, Args As ResolveEventArgs) As Assembly
-        '缓存
-        Static Prefixes As String() = {"NAudio", "Newtonsoft.Json", "Ookii.Dialogs.Wpf", "Imazen.WebP", "CacheCow.Common", "CacheCow.Client.FileStore", "CacheCow.Client", "System.Net.Http.Formatting", "System.ValueTuple"}
+    '动态 DLL 加载
+    Private Sub New() '这里必须尽早调用,且不能使用任何库,否则加载 MeloongCore 就会导致崩溃
+        Static Prefixes As String() = {"NAudio", "Newtonsoft.Json", "Ookii.Dialogs.Wpf", "Imazen.WebP", "CacheCow.Common", "CacheCow.Client.FileStore", "CacheCow.Client", "System.Net.Http.Formatting", "System.ValueTuple", "PCLCS", "MeloongCore.Wpf", "MeloongCore"}
         Static Locks As New Dictionary(Of String, Object)(StringComparer.Ordinal)
         Static Locks As New Dictionary(Of String, Object)(StringComparer.Ordinal)
-        Static LoadedAssembly As New Dictionary(Of String, Assembly)(StringComparer.Ordinal)
-        '查找对应的 DLL
-        Dim Prefix As String = Prefixes.FirstOrDefault(Function(p) Args.Name.StartsWithF(p))
-        If Prefix Is Nothing Then Return Nothing
-        '加载 DLL
-        If Not Locks.ContainsKey(Prefix) Then Locks(Prefix) = New Object()
-        SyncLock Locks(Prefix)
-            If Not LoadedAssembly.ContainsKey(Prefix) Then
-                Log($"[Start] 加载 DLL:{Prefix}")
-                LoadedAssembly(Prefix) = Assembly.Load(GetResources(Prefix))
-                'WebP 特判
-                If Prefix = "Imazen.WebP" Then
-                    SetDllDirectory(PathPure.TrimEnd("\"c))
-                    Try
-                        WriteFile(PathPure & "libwebp.dll", GetResources("libwebp64"))
-                    Catch ex As Exception
-                        Log(ex, "写入 libwebp.dll 失败") '防止同时加载多个图片时,同时写入文件导致文件占用,进而导致崩溃
-                    End Try
+        Static LoadedAssembly As New Dictionary(Of String, Assembly)(StringComparer.Ordinal) '缓存
+        AddHandler AppDomain.CurrentDomain.AssemblyResolve,
+        Function(sender As Object, Args As ResolveEventArgs) As Assembly
+            Dim Prefix As String = Prefixes.FirstOrDefault(Function(p) Args.Name.StartsWith(p, StringComparison.Ordinal))
+            If Prefix Is Nothing Then Return Nothing
+            If Not Locks.ContainsKey(Prefix) Then Locks(Prefix) = New Object()
+            SyncLock Locks(Prefix)
+                If Not LoadedAssembly.ContainsKey(Prefix) Then
+                    LoadedAssembly(Prefix) = Assembly.Load(My.Resources.ResourceManager.GetObject(Prefix))
+                    If Prefix = "Imazen.WebP" Then ExtractLibwebp() 'WebP 特判
                 End If
                 End If
-            End If
-            Return LoadedAssembly(Prefix)
-        End SyncLock
-    End Function
+                Return LoadedAssembly(Prefix)
+            End SyncLock
+        End Function
+    End Sub
+    Private Shared Sub ExtractLibwebp() '这个方法会调用 ModBase,进而调用 MeloongCore,所以不能放在 New 里
+        SetDllDirectory(PathPure.TrimEnd("\"c))
+        Try
+            FileUtils.Write(PathPure & "libwebp.dll", GetResources("libwebp64"))
+        Catch ex As Exception
+            Log(ex, "写入 libwebp.dll 失败") '防止同时加载多个图片时,同时写入文件导致文件占用,进而导致崩溃
+        End Try
+    End Sub
+    Private Declare Function SetDllDirectory Lib "kernel32" Alias "SetDllDirectoryA" (lpPathName As String) As Boolean
 
 
     '切换窗口
     '切换窗口
 
 

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

@@ -99,7 +99,7 @@ Namespace Controls.Behaviors
                 Dim pasteText = Forms.Clipboard.GetText()
                 Dim pasteText = Forms.Clipboard.GetText()
 
 
                 '将换行符替换为空格(#7825)
                 '将换行符替换为空格(#7825)
-                If Not tb.AcceptsReturn Then pasteText = pasteText.Replace(vbCrLf, " ").Replace(vbCr, " ").Replace(vbLf, " ")
+                If Not tb.AcceptsReturn Then pasteText = pasteText.ReplaceLineEndings(" ", mergeMultiple:=True)
 
 
                 Dim start = tb.SelectionStart
                 Dim start = tb.SelectionStart
 
 

+ 9 - 9
Plain Craft Launcher 2/Controls/MyCard.vb

@@ -121,17 +121,17 @@
             Case 6
             Case 6
                 Stack.Tag = CType(Stack.Tag, List(Of DlForgeVersionEntry)).OrderByDescending(Function(a) a).ToList
                 Stack.Tag = CType(Stack.Tag, List(Of DlForgeVersionEntry)).OrderByDescending(Function(a) a).ToList
             Case 8, 9
             Case 8, 9
-                Stack.Tag = CType(Stack.Tag, List(Of CompFile)).SortByComparison(Function(a, b) a.ReleaseDate > b.ReleaseDate)
+                Stack.Tag = CType(Stack.Tag, List(Of ResourceVersion)).SortByComparison(Function(a, b) a.ReleaseDate > b.ReleaseDate)
                 HasDuplicates =
                 HasDuplicates =
-                    CType(Stack.Tag, List(Of CompFile)).Distinct(Function(a, b) a.DisplayName = b.DisplayName).Count <>
-                    CType(Stack.Tag, List(Of CompFile)).Count
+                    CType(Stack.Tag, List(Of ResourceVersion)).DistinctBy(Function(a) a.Display).Count <>
+                    CType(Stack.Tag, List(Of ResourceVersion)).Count
         End Select
         End Select
         '控件转换
         '控件转换
         Select Case Type
         Select Case Type
             Case 6
             Case 6
                 ForgeDownloadListItemPreload(Stack, Stack.Tag, AddressOf ForgeSave_Click, True)
                 ForgeDownloadListItemPreload(Stack, Stack.Tag, AddressOf ForgeSave_Click, True)
             Case 8
             Case 8
-                CompFilesCardPreload(Stack, Stack.Tag)
+                PageDownloadResourceDetail.ResourceFilesCardPreload(Stack, Stack.Tag)
         End Select
         End Select
         '实现控件虚拟化
         '实现控件虚拟化
         For Each Data As Object In Stack.Tag
         For Each Data As Object In Stack.Tag
@@ -149,12 +149,12 @@
                     Stack.Children.Add(McDownloadListItem(Data, Sub(sender, e) FrmDownloadInstall.MinecraftSelected(sender, e), False))
                     Stack.Children.Add(McDownloadListItem(Data, Sub(sender, e) FrmDownloadInstall.MinecraftSelected(sender, e), False))
                 Case 8
                 Case 8
                     '若存在重复的版本名,则显示文件名而非版本名(#1344)
                     '若存在重复的版本名,则显示文件名而非版本名(#1344)
-                    Stack.Children.Add(CType(Data, CompFile).ToListItem(
-                        AddressOf FrmDownloadCompDetail.Save_Click, BadDisplayName:=HasDuplicates))
+                    Stack.Children.Add(CType(Data, ResourceVersion).ToListItem(
+                        AddressOf FrmDownloadResourceDetail.Save_Click, IsBadDisplay:=HasDuplicates))
                 Case 9
                 Case 9
                     '若存在重复的版本名,则显示文件名而非版本名(#1344)
                     '若存在重复的版本名,则显示文件名而非版本名(#1344)
-                    Stack.Children.Add(CType(Data, CompFile).ToListItem(
-                        AddressOf FrmDownloadCompDetail.Install_Click, AddressOf FrmDownloadCompDetail.Save_Click, BadDisplayName:=HasDuplicates))
+                    Stack.Children.Add(CType(Data, ResourceVersion).ToListItem(
+                        AddressOf FrmDownloadResourceDetail.Install_Click, AddressOf FrmDownloadResourceDetail.Save_Click, IsBadDisplay:=HasDuplicates))
                 Case 10
                 Case 10
                     Stack.Children.Add(LiteLoaderDownloadListItem(Data, AddressOf LiteLoaderSave_Click, True))
                     Stack.Children.Add(LiteLoaderDownloadListItem(Data, AddressOf LiteLoaderSave_Click, True))
                 Case 11
                 Case 11
@@ -164,7 +164,7 @@
                 Case 13
                 Case 13
                     Stack.Children.Add(NeoForgeDownloadListItem(Data, AddressOf NeoForgeSave_Click, True))
                     Stack.Children.Add(NeoForgeDownloadListItem(Data, AddressOf NeoForgeSave_Click, True))
                 Case Else
                 Case Else
-                    Log("未知的虚拟化种类:" & Type, LogLevel.Feedback)
+                    Log("未知的虚拟化种类:" & Type, NotifyLevel.MsgBoxAndFeedback)
             End Select
             End Select
         Next
         Next
         Stack.Children.Add(New FrameworkElement With {.Height = 18}) '下边距,同时适应折叠
         Stack.Children.Add(New FrameworkElement With {.Height = 18}) '下边距,同时适应折叠

+ 4 - 13
Plain Craft Launcher 2/Controls/MyComboBox.vb

@@ -22,7 +22,7 @@
             End If
             End If
             If HintText.Length > 0 Then TextBox.HintText = HintText
             If HintText.Length > 0 Then TextBox.HintText = HintText
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "初始化可编辑文本框失败(" & If(Name, "") & ")", LogLevel.Feedback)
+            Log(ex, "初始化可编辑文本框失败(" & If(Name, "") & ")", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
     Private _Text As String = SelectedItem
     Private _Text As String = SelectedItem
@@ -107,7 +107,7 @@
         Try
         Try
             CType(Template.FindName("PanPopup", Me), Grid).Opacity = FrmMain.Opacity
             CType(Template.FindName("PanPopup", Me), Grid).Opacity = FrmMain.Opacity
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "设置下拉框透明度失败", LogLevel.Feedback)
+            Log(ex, "设置下拉框透明度失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
     Private Sub MyComboBox_DropDownClosed(sender As Object, e As EventArgs) Handles Me.DropDownClosed
     Private Sub MyComboBox_DropDownClosed(sender As Object, e As EventArgs) Handles Me.DropDownClosed
@@ -147,25 +147,16 @@
         ElseIf Items.Count > NewValue Then
         ElseIf Items.Count > NewValue Then
             SelectedIndex = NewValue
             SelectedIndex = NewValue
         Else
         Else
-            Log($"[Setting] 尝试显示设置 {SettingService.GetKey(Me)},但没有找到设置值 {NewValue} 的对应项", LogLevel.Debug)
+            Log($"[Setting] 尝试显示设置 {SettingService.GetKey(Me)},但没有找到设置值 {NewValue} 的对应项", NotifyLevel.DebugModeOnly)
             SelectedIndex = -1
             SelectedIndex = -1
             SelectedItem = Nothing
             SelectedItem = Nothing
-            'Dim Key As String = SettingService.GetKey(Me)
-            'Dim DefaultValue = Settings.GetDefault(Key)
-            'If DefaultValue = NewValue Then
-            '    Log($"[Setting] 尝试显示设置 {Key},但没有找到设置值 {NewValue} 的对应项,这可能是因为程序版本太老了", LogLevel.Hint)
-            'Else
-            '    Log($"[Setting] 尝试显示设置 {Key},但没有找到设置值 {NewValue} 的对应项,已重置该设置", LogLevel.Debug)
-            '    Settings.Reset(Key)
-            '    RefreshSetting(DefaultValue)
-            'End If
         End If
         End If
     End Sub
     End Sub
 
 
     Private Function GetCurrentSetting() As String Implements ISettingControl.GetCurrentSetting
     Private Function GetCurrentSetting() As String Implements ISettingControl.GetCurrentSetting
         If SelectedItem Is Nothing Then Return Nothing
         If SelectedItem Is Nothing Then Return Nothing
         If TypeOf SelectedItem Is DependencyObject Then
         If TypeOf SelectedItem Is DependencyObject Then
-            Return Val(If(SettingService.GetValue(SelectedItem), SelectedIndex))
+            Return If(SettingService.GetValue(SelectedItem), SelectedIndex)
         Else
         Else
             Return SelectedIndex
             Return SelectedIndex
         End If
         End If

+ 3 - 1
Plain Craft Launcher 2/Controls/MyIconButton.xaml.vb

@@ -1,4 +1,6 @@
-Public Class MyIconButton
+Imports System.Windows.Shapes
+
+Public Class MyIconButton
 
 
     '自定义事件
     '自定义事件
     Public Event Click(sender As Object, e As EventArgs)
     Public Event Click(sender As Object, e As EventArgs)

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

@@ -143,11 +143,11 @@
                         Log(ex, $"下载图片失败,使用本地备用图片({Source})")
                         Log(ex, $"下载图片失败,使用本地备用图片({Source})")
                         Return
                         Return
                     Catch exx As Exception
                     Catch exx As Exception
-                        Log(ex, $"下载图片失败({Source})", LogLevel.Hint)
-                        Log(exx, $"加载备用图片失败({FallbackSource})", LogLevel.Hint)
+                        Log(ex, $"下载图片失败({Source})", NotifyLevel.AllUsers)
+                        Log(exx, $"加载备用图片失败({FallbackSource})", NotifyLevel.AllUsers)
                     End Try
                     End Try
                 Else
                 Else
-                    Log(ex, $"下载图片失败({Source},备用:{FallbackSource})", LogLevel.Hint)
+                    Log(ex, $"下载图片失败({Source},备用:{FallbackSource})", NotifyLevel.AllUsers)
                 End If
                 End If
             End Try
             End Try
         End Sub, "MyImage PicLoader " & GetUuid() & "#", ThreadPriority.BelowNormal)
         End Sub, "MyImage PicLoader " & GetUuid() & "#", ThreadPriority.BelowNormal)

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

@@ -174,7 +174,7 @@ Public Class MyListItem
             Return GetValue(TitleProperty)
             Return GetValue(TitleProperty)
         End Get
         End Get
         Set(value As String)
         Set(value As String)
-            SetValue(TitleProperty, value.Replace(vbCr, "").Replace(vbLf, ""))
+            SetValue(TitleProperty, value.ReplaceLineEndings(""))
         End Set
         End Set
     End Property
     End Property
     Public Shared ReadOnly TitleProperty As DependencyProperty = DependencyProperty.Register("Title", GetType(String), GetType(MyListItem))
     Public Shared ReadOnly TitleProperty As DependencyProperty = DependencyProperty.Register("Title", GetType(String), GetType(MyListItem))
@@ -198,7 +198,7 @@ Public Class MyListItem
         End Get
         End Get
         Set(value As String)
         Set(value As String)
             If _Info = value Then Return
             If _Info = value Then Return
-            value = value.Replace(vbCr, "").Replace(vbLf, "")
+            value = value.ReplaceLineEndings("")
             _Info = value
             _Info = value
             LabInfo.Text = value
             LabInfo.Text = value
             LabInfo.Visibility = If(value = "", Visibility.Collapsed, Visibility.Visible)
             LabInfo.Visibility = If(value = "", Visibility.Collapsed, Visibility.Visible)
@@ -643,7 +643,7 @@ Public Class MyListItem
                 Dim Entry As New HelpEntry(CustomEvent.GetAbsoluteUrls(CustomEventService.GetEventData(Me), CustomEventService.GetEventType(Me))(0))
                 Dim Entry As New HelpEntry(CustomEvent.GetAbsoluteUrls(CustomEventService.GetEventData(Me), CustomEventService.GetEventType(Me))(0))
                 Entry.SetToListItem(Me)
                 Entry.SetToListItem(Me)
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "设置帮助 MyListItem 失败", LogLevel.Msgbox)
+                Log(ex, "设置帮助 MyListItem 失败", NotifyLevel.MsgBox)
                 CustomEventService.SetEventType(Me, CustomEvent.EventType.None)
                 CustomEventService.SetEventType(Me, CustomEvent.EventType.None)
                 CustomEventService.SetEventData(Me, "")
                 CustomEventService.SetEventData(Me, "")
             End Try
             End Try

+ 1 - 1
Plain Craft Launcher 2/Controls/MyLoading.xaml.vb

@@ -79,7 +79,7 @@ Public Class MyLoading
                         Do While Ex.InnerException IsNot Nothing
                         Do While Ex.InnerException IsNot Nothing
                             Ex = Ex.InnerException
                             Ex = Ex.InnerException
                         Loop
                         Loop
-                        LabText.Text = If(Ex.IsNetworkRelated(), "网络环境不佳,请稍后再试,或使用 VPN 改善网络环境", StrTrim(Ex.Message))
+                        LabText.Text = If(Ex.IsNetworkRelated(), "网络环境不佳,请稍后再试,或使用 VPN 改善网络环境", Ex.Message)
                     End If
                     End If
                 Else
                 Else
                     LabText.Text = TextError
                     LabText.Text = TextError

+ 2 - 2
Plain Craft Launcher 2/Controls/MyMsg/MyMsgInput.xaml.vb

@@ -26,7 +26,7 @@
             ShapeLine.StrokeThickness = GetWPFSize(1)
             ShapeLine.StrokeThickness = GetWPFSize(1)
 
 
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "输入弹窗初始化失败", LogLevel.Hint)
+            Log(ex, "输入弹窗初始化失败", NotifyLevel.AllUsers)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -49,7 +49,7 @@
             Log("[Control] 输入弹窗:" & LabTitle.Text)
             Log("[Control] 输入弹窗:" & LabTitle.Text)
 
 
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "输入弹窗加载失败", LogLevel.Hint)
+            Log(ex, "输入弹窗加载失败", NotifyLevel.AllUsers)
         End Try
         End Try
     End Sub
     End Sub
     Private Sub Close()
     Private Sub Close()

+ 2 - 2
Plain Craft Launcher 2/Controls/MyMsg/MyMsgSelect.xaml.vb

@@ -35,7 +35,7 @@
             Next
             Next
 
 
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "选择弹窗初始化失败", LogLevel.Hint)
+            Log(ex, "选择弹窗初始化失败", NotifyLevel.AllUsers)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -56,7 +56,7 @@
             Log("[Control] 选择弹窗:" & LabTitle.Text)
             Log("[Control] 选择弹窗:" & LabTitle.Text)
 
 
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "选择弹窗加载失败", LogLevel.Hint)
+            Log(ex, "选择弹窗加载失败", NotifyLevel.AllUsers)
         End Try
         End Try
     End Sub
     End Sub
     Private Sub Close()
     Private Sub Close()

+ 2 - 2
Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml.vb

@@ -25,7 +25,7 @@
             ShapeLine.StrokeThickness = GetWPFSize(1)
             ShapeLine.StrokeThickness = GetWPFSize(1)
 
 
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "普通弹窗初始化失败", LogLevel.Hint)
+            Log(ex, "普通弹窗初始化失败", NotifyLevel.AllUsers)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -47,7 +47,7 @@
             Log("[Control] 普通弹窗:" & LabTitle.Text & vbCrLf & LabCaption.Text)
             Log("[Control] 普通弹窗:" & LabTitle.Text & vbCrLf & LabCaption.Text)
 
 
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "普通弹窗加载失败", LogLevel.Hint)
+            Log(ex, "普通弹窗加载失败", NotifyLevel.AllUsers)
         End Try
         End Try
     End Sub
     End Sub
     Private Sub Close()
     Private Sub Close()

+ 2 - 1
Plain Craft Launcher 2/Controls/MyRadioBox.xaml.vb

@@ -1,4 +1,5 @@
 Imports System.Windows.Markup
 Imports System.Windows.Markup
+Imports System.Windows.Shapes
 
 
 <ContentProperty("Inlines")>
 <ContentProperty("Inlines")>
 Public Class MyRadioBox
 Public Class MyRadioBox
@@ -99,7 +100,7 @@ Public Class MyRadioBox
             End If
             End If
 
 
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "单选框勾选改变错误", LogLevel.Hint)
+            Log(ex, "单选框勾选改变错误", NotifyLevel.AllUsers)
         End Try
         End Try
     End Sub
     End Sub
     Private Sub SyncUI()
     Private Sub SyncUI()

+ 1 - 1
Plain Craft Launcher 2/Controls/MyRadioButton.xaml.vb

@@ -104,7 +104,7 @@ Public Class MyRadioButton
             RaiseCustomEvent()
             RaiseCustomEvent()
 
 
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "单选按钮勾选改变错误", LogLevel.Hint)
+            Log(ex, "单选按钮勾选改变错误", NotifyLevel.AllUsers)
         End Try
         End Try
     End Sub
     End Sub
 
 

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

@@ -27,7 +27,7 @@
         AniStart(
         AniStart(
             AaDouble(
             AaDouble(
             Sub(AnimDelta As Double)
             Sub(AnimDelta As Double)
-                RealOffset = MathClamp(RealOffset + AnimDelta, 0, ExtentHeight - ActualHeight)
+                RealOffset = (RealOffset + AnimDelta).Clamp(0, ExtentHeight - ActualHeight)
                 ScrollToVerticalOffset(RealOffset)
                 ScrollToVerticalOffset(RealOffset)
             End Sub, Delta * DeltaMult, 300,, New AniEaseOutFluent(6)))
             End Sub, Delta * DeltaMult, 300,, New AniEaseOutFluent(6)))
     End Sub
     End Sub

+ 7 - 5
Plain Craft Launcher 2/Controls/MySlider.xaml.vb

@@ -1,4 +1,6 @@
-Public Class MySlider
+Imports System.Windows.Shapes
+
+Public Class MySlider
     Implements ISettingControl
     Implements ISettingControl
 
 
     '基础
     '基础
@@ -29,7 +31,7 @@
         Set(newValue As Integer)
         Set(newValue As Integer)
             Try
             Try
 
 
-                newValue = MathClamp(newValue, 0, MaxValue)
+                newValue = newValue.Clamp(0, MaxValue)
                 If _Value = newValue Then Return
                 If _Value = newValue Then Return
 
 
                 '触发 Preview 事件,修改新值
                 '触发 Preview 事件,修改新值
@@ -61,7 +63,7 @@
                 If AniControlEnabled = 0 Then RaiseEvent Change(Me, False)
                 If AniControlEnabled = 0 Then RaiseEvent Change(Me, False)
 
 
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "滑动条进度改变出错", LogLevel.Hint)
+                Log(ex, "滑动条进度改变出错", NotifyLevel.AllUsers)
             End Try
             End Try
         End Set
         End Set
     End Property
     End Property
@@ -71,7 +73,7 @@
         Dim NewWidth As Double = _Value / MaxValue * (ActualWidth - ShapeDot.Width)
         Dim NewWidth As Double = _Value / MaxValue * (ActualWidth - ShapeDot.Width)
         LineFore.Width = Math.Max(0, NewWidth + If(NewWidth < 0.5, 0, 0.5))
         LineFore.Width = Math.Max(0, NewWidth + If(NewWidth < 0.5, 0, 0.5))
         LineBack.Width = Math.Max(0, ActualWidth - ShapeDot.Width - NewWidth + If(ActualWidth - ShapeDot.Width - NewWidth < 0.5, 0, 0.5))
         LineBack.Width = Math.Max(0, ActualWidth - ShapeDot.Width - NewWidth + If(ActualWidth - ShapeDot.Width - NewWidth < 0.5, 0, 0.5))
-        SetLeft(ShapeDot, NewWidth)
+        ShapeDot.Margin = New Thickness(NewWidth, ShapeDot.Margin.Top, ShapeDot.Margin.Right, ShapeDot.Margin.Bottom)
     End Sub
     End Sub
 
 
     '拖动
     '拖动
@@ -89,7 +91,7 @@
         AniStop("MySlider KeyPopup " & Uuid)
         AniStop("MySlider KeyPopup " & Uuid)
     End Sub
     End Sub
     Public Sub DragDoing()
     Public Sub DragDoing()
-        Dim Percent As Double = MathClamp((Mouse.GetPosition(PanMain).X - ShapeDot.Width / 2) / (ActualWidth - ShapeDot.Width), 0, 1)
+        Dim Percent As Double = ((Mouse.GetPosition(PanMain).X - ShapeDot.Width / 2) / (ActualWidth - ShapeDot.Width)).Clamp(0, 1)
         Dim NewValue As Integer = Percent * MaxValue
         Dim NewValue As Integer = Percent * MaxValue
         If Not NewValue = Value Then
         If Not NewValue = Value Then
             Value = NewValue
             Value = NewValue

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

@@ -228,7 +228,7 @@
             '改变文本
             '改变文本
             RaiseEvent ValidatedTextChanged(sender, e)
             RaiseEvent ValidatedTextChanged(sender, e)
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "进行输入验证时出错", LogLevel.Critical)
+            Log(ex, "进行输入验证时出错", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
 
 

+ 297 - 298
Plain Craft Launcher 2/FormMain.xaml.vb

@@ -10,254 +10,260 @@ Public Class FormMain
         Dim FeatureCount As Integer = 0, BugCount As Integer = 0
         Dim FeatureCount As Integer = 0, BugCount As Integer = 0
         Dim FeatureList As New List(Of KeyValuePair(Of Integer, String))
         Dim FeatureList As New List(Of KeyValuePair(Of Integer, String))
         '统计更新日志条目
         '统计更新日志条目
-#If BETA Then
-        If LastVersion < 391 Then 'Release 2.12.6.2
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:会为部分 Forge / NeoForge 选择不支持的 Java"))
-            BugCount += 4
-        End If
-        If LastVersion < 389 Then 'Release 2.12.6.1
-            If LastVersion = 387 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:无法使用 Java 25+ 启动 Forge"))
-            FeatureCount += 4
-            BugCount += 3
-        End If
-        If LastVersion < 387 Then 'Release 2.12.6
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:降低 Minecraft 的内存占用"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:Minecraft 使用 Java 25+ 时的一个性能问题"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:无法下载 NeoForge 26.1"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:部分关键词搜不到资源,特别是中文 Mod 搜索经常没有结果"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:老版本 Windows 无法启动游戏"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:连接 Mojang 的服务可能失败,提示错误码 421"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法访问爱发电"))
-            FeatureCount += 24
-            BugCount += 19
-        End If
-        If LastVersion < 383 Then 'Release 2.12.3
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:部分关键词搜不到资源,特别是中文 Mod 搜索经常没有结果"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:下载可能完全卡住,或是下载进度反复回退"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "新增:内存管理设置,可以选择 G1GC、ZGC 或分代 ZGC"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "优化:来自 Modrinth 的文件下载速度"))
-            FeatureCount += 15
-            BugCount += 16
-        End If
-        If LastVersion < 381 Then 'Release 2.12.2
-            FeatureCount += 3
-            BugCount += 13
-        End If
-        If LastVersion < 379 Then 'Release 2.12.1
-            If LastVersion >= 376 Then
-                FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "删除:暂时隐藏联机入口……不过只是暂时关闭,它还会回来的!"))
+        If BuildType = BuildTypes.Release Then
+            If LastVersion < 391 Then 'Release 2.12.6.2
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:会为部分 Forge / NeoForge 选择不支持的 Java"))
+                BugCount += 4
             End If
             End If
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "新增:适配新的 Minecraft 版本号系统与 Unobfuscated 版本"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:巨幅优化各个下载页面和 Mod 管理页面的性能"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "优化:更换披风时会显示当前使用的披风"))
-            FeatureCount += 33
-            BugCount += 29
-        End If
-        If LastVersion < 376 Then 'Release 2.11.2
-            'FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "新增:联机功能!"))
-            FeatureCount += 32
-            BugCount += 21
-        End If
-        If LastVersion < 372 Then 'Release 2.10.9
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:如果版本设置了自定义描述,会在标题后面以淡灰色显示其版本号"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "新增:支持为一个控件设置多个自定义事件"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "新增:修改变量、弹出提示自定义事件"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "新增:添加大量替换标记,允许在更多设置和 XAML 中使用更多替换标记"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法安装 MMC 整合包"))
-            FeatureCount += 33
-            BugCount += 11
-        End If
-        If LastVersion < 369 Then 'Release 2.10.8
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "新增:允许在版本设置中设置禁止更新 Mod,以防整合包玩家误操作"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:网络与下载稳定性优化"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "优化:若整合包需要 PCL 不兼容的加载器,允许选择跳过它的安装"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "删除:由于已不再需要,删除手动安装包下载功能"))
-            FeatureCount += 21
-            BugCount += 32
-        End If
-        If LastVersion < 367 Then 'Release 2.10.6
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:启用 MCIM 社区资源镜像源,以缓解社区资源难以下载的问题"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:正版登录出错时无法给出正确的错误信息"))
-            FeatureCount += 9
-            BugCount += 9
-        End If
-        If LastVersion < 365 Then 'Release 2.10.5
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:下载资源时,会单独记忆每种资源上次下载到的文件夹,以防混淆"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:网络底层框架与下载稳定性"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法启动一部分 LabyMod 和 GTNH 客户端"))
-            FeatureCount += 22
-            BugCount += 26
-        End If
-        If LastVersion < 361 Then 'Release 2.10.3
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:无法安装部分使用老版本 PCL 导出的整合包"))
-        End If
-        If LastVersion < 359 Then 'Release 2.10.2
-            If LastVersion >= 357 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:下载资源包、光影包时能自动跳转到对应的文件夹"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:调整界面样式与动画,让整体视觉更干净,操作体验更顺滑"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:无法从 CurseForge 下载 Mod 等资源,或是安装整合包"))
-            FeatureCount += 28
-            BugCount += 28
-        End If
-        If LastVersion < 357 Then 'Release 2.10.0
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "新增:下载资源包、光影包、数据包"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "新增:允许设置文件下载源"))
-            FeatureCount += 9
-            BugCount += 26
-        End If
-#Else
-        '5:          FEAT+
-        '4:     IMP+ FEAT*
-        '3:BUG+ IMP* FEAT-
-        '2:BUG* IMP-
-        '1:BUG-
-        If LastVersion < 390 Then 'Snapshot 2.12.6.2
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:会为部分 Forge / NeoForge 选择不支持的 Java"))
-            BugCount += 4
-        End If
-        If LastVersion < 388 Then 'Snapshot 2.12.6.1
-            If LastVersion = 386 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:无法使用 Java 25+ 启动 Forge"))
-            FeatureCount += 4
-            BugCount += 3
-        End If
-        If LastVersion < 386 Then 'Snapshot 2.12.6
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:Minecraft 使用 Java 25+ 时的一个性能问题"))
-            If LastVersion = 384 OrElse LastVersion = 385 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:无法正常重命名游戏版本"))
-            If LastVersion = 385 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法显示 Modrinth 整合包的版本列表"))
-            BugCount += 1
-        End If
-        If LastVersion < 385 Then 'Snapshot 2.12.5
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:无法下载 NeoForge 26.1"))
-            If LastVersion = 384 Then
-                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:下载可能失败,提示下载管理刷新线程出错"))
-                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:无法保存选择的 Minecraft 文件夹"))
-                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:使用中文搜索 Mod 时,部分结果会忽略筛选条件"))
+            If LastVersion < 389 Then 'Release 2.12.6.1
+                If LastVersion = 387 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:无法使用 Java 25+ 启动 Forge"))
+                FeatureCount += 4
+                BugCount += 3
             End If
             End If
-            FeatureCount += 3
-            BugCount += 6
-        End If
-        If LastVersion < 384 Then 'Snapshot 2.12.4
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:降低 Minecraft 的内存占用"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:部分关键词搜不到资源,特别是中文 Mod 搜索经常没有结果"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:老版本 Windows 无法启动游戏"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:连接 Mojang 的服务可能失败,提示错误码 421"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法访问爱发电"))
-            FeatureCount += 21
-            BugCount += 14
-        End If
-        If LastVersion < 382 Then 'Snapshot 2.12.3
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:下载可能完全卡住,或是下载进度反复回退"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "新增:内存管理设置,可以选择 G1GC、ZGC 或分代 ZGC"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "优化:来自 Modrinth 的文件下载速度"))
-            FeatureCount += 15
-            BugCount += 16
-        End If
-        If LastVersion < 380 Then 'Snapshot 2.12.2
-            FeatureCount += 3
-            BugCount += 13
-        End If
-        If LastVersion < 378 Then 'Snapshot 2.12.1
-            If LastVersion >= 377 Then
-                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:在版本列表中存在 OptiFine 时可能无法加载版本列表"))
-                BugCount += 2
+            If LastVersion < 387 Then 'Release 2.12.6
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:降低 Minecraft 的内存占用"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:Minecraft 使用 Java 25+ 时的一个性能问题"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:无法下载 NeoForge 26.1"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:部分关键词搜不到资源,特别是中文 Mod 搜索经常没有结果"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:老版本 Windows 无法启动游戏"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:连接 Mojang 的服务可能失败,提示错误码 421"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法访问爱发电"))
+                FeatureCount += 24
+                BugCount += 19
             End If
             End If
-        End If
-        If LastVersion < 377 Then 'Snapshot 2.12.0
-            If LastVersion >= 373 Then
-                FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "删除:暂时隐藏联机入口……不过只是暂时关闭,它还会回来的!"))
+            If LastVersion < 383 Then 'Release 2.12.3
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:部分关键词搜不到资源,特别是中文 Mod 搜索经常没有结果"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:下载可能完全卡住,或是下载进度反复回退"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "新增:内存管理设置,可以选择 G1GC、ZGC 或分代 ZGC"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "优化:来自 Modrinth 的文件下载速度"))
+                FeatureCount += 15
+                BugCount += 16
             End If
             End If
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "新增:适配新的 Minecraft 版本号系统与 Unobfuscated 版本"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:巨幅优化各个下载页面和 Mod 管理页面的性能"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "优化:更换披风时会显示当前使用的披风"))
-            FeatureCount += 33
-            BugCount += 29
-        End If
-        If LastVersion < 375 Then 'Snapshot 2.11.2
-            'If LastVersion >= 373 Then
-            '    FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:对联机进行了各种各样的优化,以改善稳定性"))
-            '    FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:若有加入者的网络环境比房主更好,会提示可以让那位加入者担任房主"))
-            'End If
-            FeatureCount += 16
-            BugCount += 4
-        End If
-        If LastVersion < 374 Then 'Snapshot 2.11.1
-            'If LastVersion >= 373 Then
-            '    FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:使用离线登录也可以直接加入联机房间了"))
-            '    FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:会从所有共享节点中自动选择负载最低的进行中继连接"))
-            '    FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:若复制了邀请码,则可以直接快速加入房间"))
-            '    FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:关闭 PCL 时总是会提示是否退出联机,防止在关闭 PCL 时无意地关闭或退出了房间"))
-            '    FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "新增:允许自定义要连接的节点"))
-            'End If
-            FeatureCount += 9
-            BugCount += 7
-        End If
-        If LastVersion < 373 Then 'Snapshot 2.11.0
-            'FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "新增:联机功能!"))
-            FeatureCount += 7
-            BugCount += 10
-        End If
-        If LastVersion < 371 Then 'Snapshot 2.10.9
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:如果版本设置了自定义描述,会在标题后面以淡灰色显示其版本号"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "新增:支持为一个控件设置多个自定义事件"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "新增:修改变量、弹出提示自定义事件"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "新增:添加大量替换标记,允许在更多设置和 XAML 中使用更多替换标记"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法安装 MMC 整合包"))
-            FeatureCount += 33
-            BugCount += 11
-        End If
-        If LastVersion < 370 Then 'Snapshot 2.10.8
-            If LastVersion >= 368 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:网络与下载稳定性优化"))
-            FeatureCount += 3
-            BugCount += 4
-        End If
-        If LastVersion < 368 Then 'Snapshot 2.10.7
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "新增:允许在版本设置中设置禁止更新 Mod,以防整合包玩家误操作"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:网络与下载稳定性优化"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "优化:若整合包需要 PCL 不兼容的加载器,允许选择跳过它的安装"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "删除:由于已不再需要,删除手动安装包下载功能"))
-            FeatureCount += 19
-            BugCount += 28
-        End If
-        If LastVersion < 366 Then 'Snapshot 2.10.6
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:启用 MCIM 社区资源镜像源,以缓解社区资源难以下载的问题"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:正版登录出错时无法给出正确的错误信息"))
-            FeatureCount += 9
-            BugCount += 9
-        End If
-        If LastVersion < 364 Then 'Snapshot 2.10.5
-            If LastVersion >= 363 Then
-                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法添加新的正版账号"))
+            If LastVersion < 381 Then 'Release 2.12.2
+                FeatureCount += 3
+                BugCount += 13
+            End If
+            If LastVersion < 379 Then 'Release 2.12.1
+                If LastVersion >= 376 Then
+                    FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "删除:暂时隐藏联机入口……不过只是暂时关闭,它还会回来的!"))
+                End If
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "新增:适配新的 Minecraft 版本号系统与 Unobfuscated 版本"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:巨幅优化各个下载页面和 Mod 管理页面的性能"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "优化:更换披风时会显示当前使用的披风"))
+                FeatureCount += 33
+                BugCount += 29
+            End If
+            If LastVersion < 376 Then 'Release 2.11.2
+                'FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "新增:联机功能!"))
+                FeatureCount += 32
+                BugCount += 21
+            End If
+            If LastVersion < 372 Then 'Release 2.10.9
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:如果版本设置了自定义描述,会在标题后面以淡灰色显示其版本号"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "新增:支持为一个控件设置多个自定义事件"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "新增:修改变量、弹出提示自定义事件"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "新增:添加大量替换标记,允许在更多设置和 XAML 中使用更多替换标记"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法安装 MMC 整合包"))
+                FeatureCount += 33
+                BugCount += 11
+            End If
+            If LastVersion < 369 Then 'Release 2.10.8
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "新增:允许在版本设置中设置禁止更新 Mod,以防整合包玩家误操作"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:网络与下载稳定性优化"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "优化:若整合包需要 PCL 不兼容的加载器,允许选择跳过它的安装"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "删除:由于已不再需要,删除手动安装包下载功能"))
+                FeatureCount += 21
+                BugCount += 32
+            End If
+            If LastVersion < 367 Then 'Release 2.10.6
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:启用 MCIM 社区资源镜像源,以缓解社区资源难以下载的问题"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:正版登录出错时无法给出正确的错误信息"))
+                FeatureCount += 9
+                BugCount += 9
+            End If
+            If LastVersion < 365 Then 'Release 2.10.5
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:下载资源时,会单独记忆每种资源上次下载到的文件夹,以防混淆"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:网络底层框架与下载稳定性"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法启动一部分 LabyMod 和 GTNH 客户端"))
+                FeatureCount += 22
+                BugCount += 26
+            End If
+            If LastVersion < 361 Then 'Release 2.10.3
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:无法安装部分使用老版本 PCL 导出的整合包"))
+            End If
+            If LastVersion < 359 Then 'Release 2.10.2
+                If LastVersion >= 357 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:下载资源包、光影包时能自动跳转到对应的文件夹"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:调整界面样式与动画,让整体视觉更干净,操作体验更顺滑"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:无法从 CurseForge 下载 Mod 等资源,或是安装整合包"))
+                FeatureCount += 28
+                BugCount += 28
+            End If
+            If LastVersion < 357 Then 'Release 2.10.0
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "新增:下载资源包、光影包、数据包"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "新增:允许设置文件下载源"))
+                FeatureCount += 9
+                BugCount += 26
+            End If
+        Else
+            '5:          FEAT+
+            '4:     IMP+ FEAT*
+            '3:BUG+ IMP* FEAT-
+            '2:BUG* IMP-
+            '1:BUG-
+            If LastVersion < 392 Then 'Snapshot 2.12.7.0
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:允许永久关闭启动页面的快照版提示"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:下载游戏、整合包的稳定性"))
+                FeatureCount += 1
+                BugCount += 5
+            End If
+            If LastVersion < 390 Then 'Snapshot 2.12.6.2
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:会为部分 Forge / NeoForge 选择不支持的 Java"))
+                BugCount += 4
+            End If
+            If LastVersion < 388 Then 'Snapshot 2.12.6.1
+                If LastVersion = 386 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:无法使用 Java 25+ 启动 Forge"))
+                FeatureCount += 4
+                BugCount += 3
+            End If
+            If LastVersion < 386 Then 'Snapshot 2.12.6
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:Minecraft 使用 Java 25+ 时的一个性能问题"))
+                If LastVersion = 384 OrElse LastVersion = 385 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:无法正常重命名游戏版本"))
+                If LastVersion = 385 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法显示 Modrinth 整合包的版本列表"))
                 BugCount += 1
                 BugCount += 1
             End If
             End If
+            If LastVersion < 385 Then 'Snapshot 2.12.5
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:无法下载 NeoForge 26.1"))
+                If LastVersion = 384 Then
+                    FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:下载可能失败,提示下载管理刷新线程出错"))
+                    FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:无法保存选择的 Minecraft 文件夹"))
+                    FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:使用中文搜索 Mod 时,部分结果会忽略筛选条件"))
+                End If
+                FeatureCount += 3
+                BugCount += 6
+            End If
+            If LastVersion < 384 Then 'Snapshot 2.12.4
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:降低 Minecraft 的内存占用"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:部分关键词搜不到资源,特别是中文 Mod 搜索经常没有结果"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:老版本 Windows 无法启动游戏"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:连接 Mojang 的服务可能失败,提示错误码 421"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法访问爱发电"))
+                FeatureCount += 21
+                BugCount += 14
+            End If
+            If LastVersion < 382 Then 'Snapshot 2.12.3
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:下载可能完全卡住,或是下载进度反复回退"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "新增:内存管理设置,可以选择 G1GC、ZGC 或分代 ZGC"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "优化:来自 Modrinth 的文件下载速度"))
+                FeatureCount += 15
+                BugCount += 16
+            End If
+            If LastVersion < 380 Then 'Snapshot 2.12.2
+                FeatureCount += 3
+                BugCount += 13
+            End If
+            If LastVersion < 378 Then 'Snapshot 2.12.1
+                If LastVersion >= 377 Then
+                    FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:在版本列表中存在 OptiFine 时可能无法加载版本列表"))
+                    BugCount += 2
+                End If
+            End If
+            If LastVersion < 377 Then 'Snapshot 2.12.0
+                If LastVersion >= 373 Then
+                    FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "删除:暂时隐藏联机入口……不过只是暂时关闭,它还会回来的!"))
+                End If
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "新增:适配新的 Minecraft 版本号系统与 Unobfuscated 版本"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:巨幅优化各个下载页面和 Mod 管理页面的性能"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "优化:更换披风时会显示当前使用的披风"))
+                FeatureCount += 33
+                BugCount += 29
+            End If
+            If LastVersion < 375 Then 'Snapshot 2.11.2
+                'If LastVersion >= 373 Then
+                '    FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:对联机进行了各种各样的优化,以改善稳定性"))
+                '    FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:若有加入者的网络环境比房主更好,会提示可以让那位加入者担任房主"))
+                'End If
+                FeatureCount += 16
+                BugCount += 4
+            End If
+            If LastVersion < 374 Then 'Snapshot 2.11.1
+                'If LastVersion >= 373 Then
+                '    FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:使用离线登录也可以直接加入联机房间了"))
+                '    FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:会从所有共享节点中自动选择负载最低的进行中继连接"))
+                '    FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:若复制了邀请码,则可以直接快速加入房间"))
+                '    FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:关闭 PCL 时总是会提示是否退出联机,防止在关闭 PCL 时无意地关闭或退出了房间"))
+                '    FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "新增:允许自定义要连接的节点"))
+                'End If
+                FeatureCount += 9
+                BugCount += 7
+            End If
+            If LastVersion < 373 Then 'Snapshot 2.11.0
+                'FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "新增:联机功能!"))
+                FeatureCount += 7
+                BugCount += 10
+            End If
+            If LastVersion < 371 Then 'Snapshot 2.10.9
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:如果版本设置了自定义描述,会在标题后面以淡灰色显示其版本号"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "新增:支持为一个控件设置多个自定义事件"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "新增:修改变量、弹出提示自定义事件"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "新增:添加大量替换标记,允许在更多设置和 XAML 中使用更多替换标记"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法安装 MMC 整合包"))
+                FeatureCount += 33
+                BugCount += 11
+            End If
+            If LastVersion < 370 Then 'Snapshot 2.10.8
+                If LastVersion >= 368 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:网络与下载稳定性优化"))
+                FeatureCount += 3
+                BugCount += 4
+            End If
+            If LastVersion < 368 Then 'Snapshot 2.10.7
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "新增:允许在版本设置中设置禁止更新 Mod,以防整合包玩家误操作"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:网络与下载稳定性优化"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "优化:若整合包需要 PCL 不兼容的加载器,允许选择跳过它的安装"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "删除:由于已不再需要,删除手动安装包下载功能"))
+                FeatureCount += 19
+                BugCount += 28
+            End If
+            If LastVersion < 366 Then 'Snapshot 2.10.6
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:启用 MCIM 社区资源镜像源,以缓解社区资源难以下载的问题"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:正版登录出错时无法给出正确的错误信息"))
+                FeatureCount += 9
+                BugCount += 9
+            End If
+            If LastVersion < 364 Then 'Snapshot 2.10.5
+                If LastVersion >= 363 Then
+                    FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法添加新的正版账号"))
+                    BugCount += 1
+                End If
+            End If
+            If LastVersion < 363 Then 'Snapshot 2.10.4
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:下载资源时,会单独记忆每种资源上次下载到的文件夹,以防混淆"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:网络底层框架与下载稳定性"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法启动一部分 LabyMod 和 GTNH 客户端"))
+                FeatureCount += 22
+                BugCount += 26
+            End If
+            If LastVersion < 362 Then 'Snapshot 2.10.3
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:无法安装部分使用老版本 PCL 导出的整合包"))
+            End If
+            If LastVersion < 360 Then 'Snapshot 2.10.2
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:无法从 CurseForge 下载 Mod 等资源,或是安装整合包"))
+                If LastVersion >= 358 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法加载正版皮肤的头像"))
+                FeatureCount += 3
+                BugCount += 5
+            End If
+            If LastVersion < 358 Then 'Snapshot 2.10.1
+                If LastVersion >= 356 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:下载资源包、光影包时能自动跳转到对应的文件夹"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:调整界面样式与动画,让整体视觉更干净,操作体验更顺滑"))
+                FeatureCount += 25
+                BugCount += 23
+            End If
+            If LastVersion < 356 Then 'Snapshot 2.10.0
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "新增:下载资源包、光影包、数据包"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "新增:允许设置文件下载源"))
+                FeatureCount += 9
+                BugCount += 26
+            End If
         End If
         End If
-        If LastVersion < 363 Then 'Snapshot 2.10.4
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:下载资源时,会单独记忆每种资源上次下载到的文件夹,以防混淆"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "优化:网络底层框架与下载稳定性"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法启动一部分 LabyMod 和 GTNH 客户端"))
-            FeatureCount += 22
-            BugCount += 26
-        End If
-        If LastVersion < 362 Then 'Snapshot 2.10.3
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:无法安装部分使用老版本 PCL 导出的整合包"))
-        End If
-        If LastVersion < 360 Then 'Snapshot 2.10.2
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(2, "修复:无法从 CurseForge 下载 Mod 等资源,或是安装整合包"))
-            If LastVersion >= 358 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(1, "修复:无法加载正版皮肤的头像"))
-            FeatureCount += 3
-            BugCount += 5
-        End If
-        If LastVersion < 358 Then 'Snapshot 2.10.1
-            If LastVersion >= 356 Then FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:下载资源包、光影包时能自动跳转到对应的文件夹"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "优化:调整界面样式与动画,让整体视觉更干净,操作体验更顺滑"))
-            FeatureCount += 25
-            BugCount += 23
-        End If
-        If LastVersion < 356 Then 'Snapshot 2.10.0
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "新增:下载资源包、光影包、数据包"))
-            FeatureList.Add(New KeyValuePair(Of Integer, String)(3, "新增:允许设置文件下载源"))
-            FeatureCount += 9
-            BugCount += 26
-        End If
-#End If
         '整理更新日志文本
         '整理更新日志文本
         Dim ContentList As New List(Of String)
         Dim ContentList As New List(Of String)
         Dim SortedFeatures = FeatureList.OrderByDescending(Function(f) f.Key).ToList
         Dim SortedFeatures = FeatureList.OrderByDescending(Function(f) f.Key).ToList
@@ -276,7 +282,7 @@ Public Class FormMain
         '输出更新日志
         '输出更新日志
         RunInNewThread(
         RunInNewThread(
         Sub()
         Sub()
-            If MyMsgBox(Content, "PCL 已更新至" & VersionDisplayName, "确定", "完整更新日志") = 2 Then
+            If MyMsgBox(Content, "PCL 已更新至" & VersionDisplay, "确定", "完整更新日志") = 2 Then
                 OpenWebsite("https://meloong.com/afd/a/LTCat?tab=feed")
                 OpenWebsite("https://meloong.com/afd/a/LTCat?tab=feed")
             End If
             End If
         End Sub, "UpdateLog Output")
         End Sub, "UpdateLog Output")
@@ -342,9 +348,7 @@ Public Class FormMain
         PageRight = FrmLaunchRight
         PageRight = FrmLaunchRight
         FrmLaunchRight.PageState = MyPageRight.PageStates.ContentStay
         FrmLaunchRight.PageState = MyPageRight.PageStates.ContentStay
         '模式提醒
         '模式提醒
-#If DEBUG Then
-        Hint("[开发者模式] PCL 正以开发者模式运行,这可能会造成严重的性能下降,请务必立即向开发者反馈此问题!", HintType.Red)
-#End If
+        If BuildType = BuildTypes.Debug Then Hint("[开发者模式] PCL 正以开发者模式运行,这可能会造成严重的性能下降,请务必立即向开发者反馈此问题!", HintType.Red)
         If ModeDebug Then Hint("[调试模式] PCL 正以调试模式运行,这可能会导致性能下降,若无必要请不要开启!")
         If ModeDebug Then Hint("[调试模式] PCL 正以调试模式运行,这可能会导致性能下降,若无必要请不要开启!")
         '尽早执行的加载池
         '尽早执行的加载池
         McFolderListLoader.Start(0) '为了让下载已存在文件检测可以正常运行,必须跑一次;为了让启动按钮尽快可用,需要尽早执行;为了与 PageLaunchLeft 联动,需要为 0 而不是 GetUuid
         McFolderListLoader.Start(0) '为了让下载已存在文件检测可以正常运行,必须跑一次;为了让启动按钮尽快可用,需要尽早执行;为了与 PageLaunchLeft 联动,需要为 0 而不是 GetUuid
@@ -385,14 +389,12 @@ Public Class FormMain
             Height = Settings.Get("WindowHeight")
             Height = Settings.Get("WindowHeight")
             Width = Settings.Get("WindowWidth")
             Width = Settings.Get("WindowWidth")
         Catch ex As Exception '修复 #2019
         Catch ex As Exception '修复 #2019
-            Log(ex, "读取窗口默认大小失败", LogLevel.Hint)
+            Log(ex, "读取窗口默认大小失败", NotifyLevel.AllUsers)
             Height = MinHeight + 100
             Height = MinHeight + 100
             Width = MinWidth + 100
             Width = MinWidth + 100
         End Try
         End Try
-        '#If DEBUG Then
-        '        MinHeight = 50
-        '        MinWidth = 50
-        '#End If
+        'MinHeight = 50
+        'MinWidth = 50
         Topmost = False
         Topmost = False
         If FrmStart IsNot Nothing Then FrmStart.Close(New TimeSpan(0, 0, 0, 0, 400 / AniSpeed))
         If FrmStart IsNot Nothing Then FrmStart.Close(New TimeSpan(0, 0, 0, 0, 400 / AniSpeed))
         '更改窗口
         '更改窗口
@@ -411,7 +413,7 @@ Public Class FormMain
             Sub()
             Sub()
                 PanBack.RenderTransform = Nothing
                 PanBack.RenderTransform = Nothing
                 IsWindowLoadFinished = True
                 IsWindowLoadFinished = True
-                Log($"[System] DPI:{DPI},系统版本:{Environment.OSVersion.VersionString},PCL 位置:{PathWithName}")
+                Log($"[System] DPI:{DPI},系统版本:{Environment.OSVersion.VersionString},PCL 位置:{PathExe}")
             End Sub, , True)
             End Sub, , True)
         }, "Form Show")
         }, "Form Show")
         'Timer 启动
         'Timer 启动
@@ -444,16 +446,20 @@ Public Class FormMain
                 ServerLoader.Start()
                 ServerLoader.Start()
                 RunInNewThread(AddressOf TryClearTaskTemp, "TryClearTaskTemp", ThreadPriority.BelowNormal)
                 RunInNewThread(AddressOf TryClearTaskTemp, "TryClearTaskTemp", ThreadPriority.BelowNormal)
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "初始化加载池运行失败", LogLevel.Feedback)
+                Log(ex, "初始化加载池运行失败", NotifyLevel.MsgBoxAndFeedback)
             End Try
             End Try
             '清理自动更新文件
             '清理自动更新文件
             Try
             Try
-                If File.Exists(Path & "PCL\Plain Craft Launcher 2.exe") Then File.Delete(Path & "PCL\Plain Craft Launcher 2.exe")
+                File.Delete(PathExeFolder & "PCL\Plain Craft Launcher 2.exe")
             Catch ex As Exception
             Catch ex As Exception
                 Log(ex, "清理自动更新文件失败")
                 Log(ex, "清理自动更新文件失败")
             End Try
             End Try
             '上报
             '上报
             Telemetry("启动")
             Telemetry("启动")
+            '开源版本提示
+            If VersionBranchMain = "OpenSource" Then
+                MyMsgBox($"该版本中无法使用以下特性:{vbCrLf}- CurseForge API 调用:需要自行申请 API Key,然后添加到 ModSecret.vb 的开头{vbCrLf}- 正版登录:需要自行向微软申请 Client ID,然后添加到 ModSecret.vb 的开头{vbCrLf}- 更新与联网通知:避免滥用隐患{vbCrLf}- 主题切换:这是需要赞助解锁的纪念性质的功能,别让赞助者太伤心啦……{vbCrLf}- 百宝箱:开发早期往里面塞了些开发工具,整理起来太麻烦了……", "开源版本说明")
+            End If
         End Sub, "初始化", ThreadPriority.Lowest)
         End Sub, "初始化", ThreadPriority.Lowest)
 
 
         Log("[Start] 第三阶段加载用时:" & GetTimeMs() - ApplicationStartTick & " ms")
         Log("[Start] 第三阶段加载用时:" & GetTimeMs() - ApplicationStartTick & " ms")
@@ -461,20 +467,20 @@ Public Class FormMain
     '根据打开次数触发的事件
     '根据打开次数触发的事件
     Private Sub RunCountSub()
     Private Sub RunCountSub()
         Settings.Set("SystemCount", Settings.Get("SystemCount") + 1)
         Settings.Set("SystemCount", Settings.Get("SystemCount") + 1)
-#If Not BETA Then
-        Select Case Settings.Get("SystemCount")
-            Case 1
-                MyMsgBox("欢迎使用 PCL 快照版!" & vbCrLf &
-                         "快照版包含尚未在正式版发布的测试性功能,仅用于赞助者本人尝鲜。所以请不要发给其他人或者用于制作整合包哦!" & vbCrLf &
-                         "如果你并非通过赞助或赞助者本人邀请进群获得的本程序,那么可能是有人在违规传播,记得提醒他一下啦。", "快照版使用说明")
-        End Select
-        If Settings.Get("SystemCount") >= 99 Then
-            If ThemeUnlock(6, False) Then
-                MyMsgBox("你已经使用了 99 次 PCL 啦,感谢你长期以来的支持!" & vbCrLf &
-                         "隐藏主题 铁杆粉 已解锁!", "提示")
+        If BuildType = BuildTypes.Snapshot Then
+            Select Case Settings.Get("SystemCount")
+                Case 1
+                    MyMsgBox("欢迎使用 PCL 快照版!" & vbCrLf &
+                             "快照版包含尚未在正式版发布的测试性功能,仅用于赞助者本人尝鲜。所以请不要发给其他人或者用于制作整合包哦!" & vbCrLf &
+                             "如果你并非通过赞助或赞助者本人邀请进群获得的本程序,那么可能是有人在违规传播,记得提醒他一下啦。", "快照版使用说明")
+            End Select
+            If Settings.Get("SystemCount") >= 99 Then
+                If ThemeUnlock(6, False) Then
+                    MyMsgBox("你已经使用了 99 次 PCL 啦,感谢你长期以来的支持!" & vbCrLf &
+                             "隐藏主题 铁杆粉 已解锁!", "提示")
+                End If
             End If
             End If
         End If
         End If
-#End If
     End Sub
     End Sub
     '升级与降级事件
     '升级与降级事件
     Private Sub UpgradeSub(LastVersionCode As Integer)
     Private Sub UpgradeSub(LastVersionCode As Integer)
@@ -482,19 +488,12 @@ Public Class FormMain
         Settings.Set("SystemLastVersionReg", VersionCode)
         Settings.Set("SystemLastVersionReg", VersionCode)
         '检查有记录的最高版本号
         '检查有记录的最高版本号
         Dim LowerVersionCode As Integer
         Dim LowerVersionCode As Integer
-#If BETA Then
-        LowerVersionCode = Settings.Get("SystemHighestBetaVersionReg")
-        If LowerVersionCode < VersionCode Then
-            Settings.Set("SystemHighestBetaVersionReg", VersionCode)
-            Log("[Start] 最高版本号从 " & LowerVersionCode & " 升高到 " & VersionCode)
-        End If
-#Else
+        Dim SettingKey = If(BuildType = BuildTypes.Snapshot, "SystemHighestAlphaVersionReg", "SystemHighestBetaVersionReg")
         LowerVersionCode = Settings.Get("SystemHighestAlphaVersionReg")
         LowerVersionCode = Settings.Get("SystemHighestAlphaVersionReg")
         If LowerVersionCode < VersionCode Then
         If LowerVersionCode < VersionCode Then
             Settings.Set("SystemHighestAlphaVersionReg", VersionCode)
             Settings.Set("SystemHighestAlphaVersionReg", VersionCode)
-            Log("[Start] 最高版本号从 " & LowerVersionCode & " 升高到 " & VersionCode)
+            Log($"[Start] 最高版本号从 {LowerVersionCode} 升高到 {VersionCode}")
         End If
         End If
-#End If
         '被移除的窗口设置选项
         '被移除的窗口设置选项
         If Settings.Get("LaunchArgumentWindowType") = 5 Then Settings.Set("LaunchArgumentWindowType", 1)
         If Settings.Get("LaunchArgumentWindowType") = 5 Then Settings.Set("LaunchArgumentWindowType", 1)
         '修改主题设置项名称
         '修改主题设置项名称
@@ -521,8 +520,8 @@ Public Class FormMain
                      "多谢各位的理解啦!", "重新解锁提醒")
                      "多谢各位的理解啦!", "重新解锁提醒")
         End If
         End If
         '移动自定义皮肤
         '移动自定义皮肤
-        If LastVersionCode <= 161 AndAlso File.Exists(Path & "PCL\CustomSkin.png") AndAlso Not File.Exists(PathAppdata & "CustomSkin.png") Then
-            CopyFile(Path & "PCL\CustomSkin.png", PathAppdata & "CustomSkin.png")
+        If LastVersionCode <= 161 AndAlso File.Exists(PathExeFolder & "PCL\CustomSkin.png") AndAlso Not File.Exists(PathAppdata & "CustomSkin.png") Then
+            CopyFile(PathExeFolder & "PCL\CustomSkin.png", PathAppdata & "CustomSkin.png")
             Log("[Start] 已移动离线自定义皮肤 (162)")
             Log("[Start] 已移动离线自定义皮肤 (162)")
         End If
         End If
         If LastVersionCode <= 263 AndAlso File.Exists(PathTemp & "CustomSkin.png") AndAlso Not File.Exists(PathAppdata & "CustomSkin.png") Then
         If LastVersionCode <= 263 AndAlso File.Exists(PathTemp & "CustomSkin.png") AndAlso Not File.Exists(PathAppdata & "CustomSkin.png") Then
@@ -635,7 +634,7 @@ Public Class FormMain
                 FeedbackInfo()
                 FeedbackInfo()
                 Log("请在 https://github.com/Meloong-Git/PCL/issues 提交错误报告,以便于作者解决此问题!")
                 Log("请在 https://github.com/Meloong-Git/PCL/issues 提交错误报告,以便于作者解决此问题!")
                 IsLogShown = True
                 IsLogShown = True
-                StartProcess(Path & "PCL\Log1.txt")
+                StartProcess(PathExeFolder & "PCL\Log1.txt")
             End If
             End If
             Thread.Sleep(500) '防止 PCL 在记事本打开前就被掐掉
             Thread.Sleep(500) '防止 PCL 在记事本打开前就被掐掉
         End If
         End If
@@ -789,10 +788,10 @@ Public Class FormMain
                     FrmSetupUI.PanLogoChange.Visibility = Visibility.Visible
                     FrmSetupUI.PanLogoChange.Visibility = Visibility.Visible
                 End If
                 End If
                 Try
                 Try
-                    ImageTitleLogo.Source = Path & "PCL\Logo.png"
+                    ImageTitleLogo.Source = PathExeFolder & "PCL\Logo.png"
                 Catch ex As Exception
                 Catch ex As Exception
                     ImageTitleLogo.Source = Nothing
                     ImageTitleLogo.Source = Nothing
-                    Log(ex, "显示标题栏图片失败", LogLevel.Msgbox)
+                    Log(ex, "显示标题栏图片失败", NotifyLevel.MsgBox)
                 End Try
                 End Try
         End Select
         End Select
         FrmSetupUI?.CardLogo.TriggerForceResize()
         FrmSetupUI?.CardLogo.TriggerForceResize()
@@ -890,7 +889,7 @@ Public Class FormMain
                 LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.RunOnUpdated, MaxDepth:=1, ExtraPath:="versions\")
                 LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.RunOnUpdated, MaxDepth:=1, ExtraPath:="versions\")
             End If
             End If
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "切回窗口时出错", LogLevel.Feedback)
+            Log(ex, "切回窗口时出错", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
         '读取剪贴板,自动加入联机房间
         '读取剪贴板,自动加入联机房间
         Return 'TODO: 联机复活赛
         Return 'TODO: 联机复活赛
@@ -941,7 +940,7 @@ Public Class FormMain
             PrevEffects = e.Effects
             PrevEffects = e.Effects
             Log("[System] 设置拖放类型:" & GetStringFromEnum(e.Effects))
             Log("[System] 设置拖放类型:" & GetStringFromEnum(e.Effects))
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "处理拖放时出错", LogLevel.Feedback)
+            Log(ex, "处理拖放时出错", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
     Private Sub FrmMain_Drop(sender As Object, e As DragEventArgs) Handles Me.Drop
     Private Sub FrmMain_Drop(sender As Object, e As DragEventArgs) Handles Me.Drop
@@ -956,7 +955,7 @@ Public Class FormMain
                         'Authlib 拖拽
                         'Authlib 拖拽
                         e.Handled = True
                         e.Handled = True
                         e.Effects = DragDropEffects.Copy
                         e.Effects = DragDropEffects.Copy
-                        Dim AuthlibServer As String = Net.WebUtility.UrlDecode(Str.Substring("authlib-injector:yggdrasil-server:".Length))
+                        Dim AuthlibServer As String = EscapeUtils.FormUrlUnescape(Str.Substring("authlib-injector:yggdrasil-server:".Length))
                         Log("[System] Authlib 拖拽:" & AuthlibServer)
                         Log("[System] Authlib 拖拽:" & AuthlibServer)
                         If Not String.IsNullOrEmpty(New ValidateHttp().Validate(AuthlibServer)) Then
                         If Not String.IsNullOrEmpty(New ValidateHttp().Validate(AuthlibServer)) Then
                             Hint($"输入的 Authlib 验证服务器不符合网址格式({AuthlibServer})!", HintType.Red)
                             Hint($"输入的 Authlib 验证服务器不符合网址格式({AuthlibServer})!", HintType.Red)
@@ -1004,7 +1003,7 @@ Public Class FormMain
                         FileDrag(New List(Of String) From {FilePath})
                         FileDrag(New List(Of String) From {FilePath})
                     End If
                     End If
                 Catch ex As Exception
                 Catch ex As Exception
-                    Log(ex, "无法接取文本拖拽事件", LogLevel.Developer)
+                    Log(ex, "无法接取文本拖拽事件", NotifyLevel.DevelopOnly)
                     Return
                     Return
                 End Try
                 End Try
             ElseIf e.Data.GetDataPresent(DataFormats.FileDrop) Then
             ElseIf e.Data.GetDataPresent(DataFormats.FileDrop) Then
@@ -1019,14 +1018,14 @@ Public Class FormMain
                 FileDrag(CType(FilePathRaw, IEnumerable(Of String)))
                 FileDrag(CType(FilePathRaw, IEnumerable(Of String)))
             End If
             End If
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "接取拖拽事件失败", LogLevel.Feedback)
+            Log(ex, "接取拖拽事件失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
     Private Sub FileDrag(FilePathList As IEnumerable(Of String))
     Private Sub FileDrag(FilePathList As IEnumerable(Of String))
         RunInNewThread(
         RunInNewThread(
         Sub()
         Sub()
             Dim FilePath As String = FilePathList.First
             Dim FilePath As String = FilePathList.First
-            Log("[System] 接受文件拖拽:" & FilePath & If(FilePathList.Any, $" 等 {FilePathList.Count} 个文件", ""), LogLevel.Developer)
+            Log("[System] 接受文件拖拽:" & FilePath & If(FilePathList.Any, $" 等 {FilePathList.Count} 个文件", ""), NotifyLevel.DevelopOnly)
             '基础检查
             '基础检查
             If Directory.Exists(FilePathList.First) AndAlso Not File.Exists(FilePathList.First) Then
             If Directory.Exists(FilePathList.First) AndAlso Not File.Exists(FilePathList.First) Then
                 Hint("请拖入一个文件,而非文件夹!", HintType.Red)
                 Hint("请拖入一个文件,而非文件夹!", HintType.Red)
@@ -1049,12 +1048,12 @@ Public Class FormMain
             Dim Extension As String = FilePath.AfterLast(".").Lower
             Dim Extension As String = FilePath.AfterLast(".").Lower
             If Extension = "xaml" Then
             If Extension = "xaml" Then
                 Log("[System] 文件后缀为 XAML,作为主页加载")
                 Log("[System] 文件后缀为 XAML,作为主页加载")
-                If File.Exists(Path & "PCL\Custom.xaml") Then
+                If File.Exists(PathExeFolder & "PCL\Custom.xaml") Then
                     If MyMsgBox("已存在一个主页文件,是否要将它覆盖?", "覆盖确认", "覆盖", "取消") = 2 Then
                     If MyMsgBox("已存在一个主页文件,是否要将它覆盖?", "覆盖确认", "覆盖", "取消") = 2 Then
                         Return
                         Return
                     End If
                     End If
                 End If
                 End If
-                CopyFile(FilePath, Path & "PCL\Custom.xaml")
+                CopyFile(FilePath, PathExeFolder & "PCL\Custom.xaml")
                 RunInUi(
                 RunInUi(
                 Sub()
                 Sub()
                     Settings.Set("UiCustomType", 1)
                     Settings.Set("UiCustomType", 1)
@@ -1092,7 +1091,7 @@ Public Class FormMain
                 Analyzer.Output(True, New List(Of String))
                 Analyzer.Output(True, New List(Of String))
                 Return
                 Return
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "自主错误报告分析失败", LogLevel.Feedback)
+                Log(ex, "自主错误报告分析失败", NotifyLevel.MsgBoxAndFeedback)
             End Try
             End Try
             '未知操作
             '未知操作
             Hint("PCL 无法确定应当执行的文件拖拽操作……")
             Hint("PCL 无法确定应当执行的文件拖拽操作……")
@@ -1207,9 +1206,9 @@ Public Class FormMain
         ''' </summary>
         ''' </summary>
         InstanceSetup = 7
         InstanceSetup = 7
         ''' <summary>
         ''' <summary>
-        ''' 资源工程详情。这是一个副页面。
+        ''' 社区资源详情。这是一个副页面。
         ''' </summary>
         ''' </summary>
-        CompDetail = 8
+        ResourceDetail = 8
         ''' <summary>
         ''' <summary>
         ''' 帮助详情。这是一个副页面。
         ''' 帮助详情。这是一个副页面。
         ''' </summary>
         ''' </summary>
@@ -1252,8 +1251,8 @@ Public Class FormMain
                 Return "下载管理"
                 Return "下载管理"
             Case PageType.InstanceSetup
             Case PageType.InstanceSetup
                 Return "版本设置 - " & If(PageInstanceLeft.Instance Is Nothing, "未知版本", PageInstanceLeft.Instance.Name)
                 Return "版本设置 - " & If(PageInstanceLeft.Instance Is Nothing, "未知版本", PageInstanceLeft.Instance.Name)
-            Case PageType.CompDetail
-                Return "资源下载 - " & CType(Stack.Additional(0), CompProject).TranslatedName
+            Case PageType.ResourceDetail
+                Return CType(Stack.Additional(0), ResourceProject).TranslatedName
             Case PageType.HelpDetail
             Case PageType.HelpDetail
                 Return CType(Stack.Additional(0), HelpEntry).Title
                 Return CType(Stack.Additional(0), HelpEntry).Title
             Case Else
             Case Else
@@ -1478,9 +1477,9 @@ Public Class FormMain
                 Case PageType.InstanceSetup '版本设置
                 Case PageType.InstanceSetup '版本设置
                     If FrmInstanceLeft Is Nothing Then FrmInstanceLeft = New PageInstanceLeft
                     If FrmInstanceLeft Is Nothing Then FrmInstanceLeft = New PageInstanceLeft
                     PageChangeAnim(FrmInstanceLeft, FrmInstanceLeft.PageGet(SubType))
                     PageChangeAnim(FrmInstanceLeft, FrmInstanceLeft.PageGet(SubType))
-                Case PageType.CompDetail 'Mod 信息
-                    If FrmDownloadCompDetail Is Nothing Then FrmDownloadCompDetail = New PageDownloadCompDetail
-                    PageChangeAnim(New MyPageLeft, FrmDownloadCompDetail)
+                Case PageType.ResourceDetail '社区资源详情
+                    If FrmDownloadResourceDetail Is Nothing Then FrmDownloadResourceDetail = New PageDownloadResourceDetail
+                    PageChangeAnim(New MyPageLeft, FrmDownloadResourceDetail)
                 Case PageType.HelpDetail '帮助详情
                 Case PageType.HelpDetail '帮助详情
                     PageChangeAnim(New MyPageLeft, Stack.Additional(1))
                     PageChangeAnim(New MyPageLeft, Stack.Additional(1))
             End Select
             End Select
@@ -1493,7 +1492,7 @@ Public Class FormMain
 
 
             Log("[Control] 切换主要页面:" & GetStringFromEnum(Stack) & ", " & SubType)
             Log("[Control] 切换主要页面:" & GetStringFromEnum(Stack) & ", " & SubType)
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "切换主要页面失败(ID " & PageCurrent.Page & ")", LogLevel.Feedback)
+            Log(ex, "切换主要页面失败(ID " & PageCurrent.Page & ")", NotifyLevel.MsgBoxAndFeedback)
         Finally
         Finally
             AniControlEnabled -= 1
             AniControlEnabled -= 1
         End Try
         End Try
@@ -1676,7 +1675,7 @@ Public Class FormMain
             Next
             Next
             Hint("已关闭运行中的 Minecraft!", HintType.Green)
             Hint("已关闭运行中的 Minecraft!", HintType.Green)
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "强制关闭所有 Minecraft 失败", LogLevel.Feedback)
+            Log(ex, "强制关闭所有 Minecraft 失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
     Public Function BtnExtraShutdown_ShowCheck() As Boolean
     Public Function BtnExtraShutdown_ShowCheck() As Boolean
@@ -1691,7 +1690,7 @@ Public Class FormMain
         If RealScroll IsNot Nothing Then
         If RealScroll IsNot Nothing Then
             RealScroll.PerformVerticalOffsetDelta(-RealScroll.VerticalOffset)
             RealScroll.PerformVerticalOffsetDelta(-RealScroll.VerticalOffset)
         Else
         Else
-            Log("[UI] 无法返回顶部,未找到合适的 RealScroll", LogLevel.Hint)
+            Log("[UI] 无法返回顶部,未找到合适的 RealScroll", NotifyLevel.AllUsers)
         End If
         End If
     End Sub
     End Sub
     Private Function BtnExtraBack_ShowCheck() As Boolean
     Private Function BtnExtraBack_ShowCheck() As Boolean

BIN
Plain Craft Launcher 2/Images/Icons/Unavailable.png


+ 0 - 0
Plain Craft Launcher 2/Images/Icons/A.png → Plain Craft Launcher 2/Images/ReleaseTypes/Alpha.png


+ 0 - 0
Plain Craft Launcher 2/Images/Icons/B.png → Plain Craft Launcher 2/Images/ReleaseTypes/Beta.png


+ 0 - 0
Plain Craft Launcher 2/Images/Icons/R.png → Plain Craft Launcher 2/Images/ReleaseTypes/Release.png


+ 69 - 39
Plain Craft Launcher 2/Modules/Base/ModAnimation.vb

@@ -1,6 +1,8 @@
 '动画引擎模块
 '动画引擎模块
 '使用 Ani 作为方法或属性的开头,使用 Aa 作为单个动画对象的开头(便于自动补全)
 '使用 Ani 作为方法或属性的开头,使用 Aa 作为单个动画对象的开头(便于自动补全)
 
 
+Imports System.Windows.Media.Effects
+
 Public Module ModAnimation
 Public Module ModAnimation
 
 
 #Region "声明"
 #Region "声明"
@@ -10,8 +12,8 @@ Public Module ModAnimation
     ''' </summary>
     ''' </summary>
     Public ReadOnly Property AniSpeed As Double
     Public ReadOnly Property AniSpeed As Double
         Get
         Get
-            Dim Setting = PCL.Settings.Get("SystemDebugAnim")
-            Return If(Setting >= 30, 200, MathClamp(Setting * 0.1 + 0.1, 0.1, 3))
+            Dim Setting As Integer = Settings.Get("SystemDebugAnim")
+            Return If(Setting >= 30, 200, (Setting * 0.1 + 0.1).Clamp(0.1, 3))
         End Get
         End Get
     End Property
     End Property
     ''' <summary>
     ''' <summary>
@@ -582,10 +584,10 @@ Public Module ModAnimation
     Public Class AniEaseLinear
     Public Class AniEaseLinear
         Inherits AniEase
         Inherits AniEase
         Public Overrides Function GetValue(t As Double) As Double
         Public Overrides Function GetValue(t As Double) As Double
-            Return MathClamp(t, 0, 1)
+            Return t.Clamp(0, 1)
         End Function
         End Function
         Public Overrides Function GetDelta(t1 As Double, t0 As Double) As Double
         Public Overrides Function GetDelta(t1 As Double, t0 As Double) As Double
-            Return MathClamp(t1, 0, 1) - MathClamp(t0, 0, 1)
+            Return t1.Clamp(0, 1) - t0.Clamp(0, 1)
         End Function
         End Function
     End Class
     End Class
 
 
@@ -600,7 +602,7 @@ Public Module ModAnimation
             p = Power
             p = Power
         End Sub
         End Sub
         Public Overrides Function GetValue(t As Double) As Double
         Public Overrides Function GetValue(t As Double) As Double
-            Return MathClamp(t, 0, 1) ^ p
+            Return t.Clamp(0, 1) ^ p
         End Function
         End Function
     End Class
     End Class
     ''' <summary>
     ''' <summary>
@@ -613,7 +615,7 @@ Public Module ModAnimation
             p = Power
             p = Power
         End Sub
         End Sub
         Public Overrides Function GetValue(t As Double) As Double
         Public Overrides Function GetValue(t As Double) As Double
-            Return 1 - MathClamp(1 - t, 0, 1) ^ p
+            Return 1 - (1 - t).Clamp(0, 1) ^ p
         End Function
         End Function
     End Class
     End Class
     ''' <summary>
     ''' <summary>
@@ -644,7 +646,7 @@ Public Module ModAnimation
             If alpha < 0 Then alpha = 0 '初速度小于平均速度时,退化为线性
             If alpha < 0 Then alpha = 0 '初速度小于平均速度时,退化为线性
         End Sub
         End Sub
         Public Overrides Function GetValue(percent As Double) As Double
         Public Overrides Function GetValue(percent As Double) As Double
-            Dim p As Double = MathClamp(percent, 0, 1)
+            Dim p As Double = percent.Clamp(0, 1)
             If alpha = 0 Then Return p '退化到线性
             If alpha = 0 Then Return p '退化到线性
             Return (alpha + 1) * p / (1 + alpha * p)
             Return (alpha + 1) * p / (1 + alpha * p)
         End Function
         End Function
@@ -661,7 +663,7 @@ Public Module ModAnimation
             p = 3 - Power * 0.5
             p = 3 - Power * 0.5
         End Sub
         End Sub
         Public Overrides Function GetValue(t As Double) As Double
         Public Overrides Function GetValue(t As Double) As Double
-            t = MathClamp(t, 0, 1)
+            t = t.Clamp(0, 1)
             Return t ^ p * Math.Cos(1.5 * Math.PI * (1 - t))
             Return t ^ p * Math.Cos(1.5 * Math.PI * (1 - t))
         End Function
         End Function
     End Class
     End Class
@@ -675,7 +677,7 @@ Public Module ModAnimation
             p = 3 - Power * 0.5
             p = 3 - Power * 0.5
         End Sub
         End Sub
         Public Overrides Function GetValue(t As Double) As Double
         Public Overrides Function GetValue(t As Double) As Double
-            t = MathClamp(t, 0, 1)
+            t = t.Clamp(0, 1)
             Return 1 - (1 - t) ^ p * Math.Cos(1.5 * Math.PI * t)
             Return 1 - (1 - t) ^ p * Math.Cos(1.5 * Math.PI * t)
         End Function
         End Function
     End Class
     End Class
@@ -719,7 +721,7 @@ Public Module ModAnimation
             p = Power + 4
             p = Power + 4
         End Sub
         End Sub
         Public Overrides Function GetValue(t As Double) As Double
         Public Overrides Function GetValue(t As Double) As Double
-            t = MathClamp(t, 0, 1)
+            t = t.Clamp(0, 1)
             Return t ^ ((p - 1) * 0.25) * Math.Cos((p - 3.5) * Math.PI * (1 - t) ^ 1.5)
             Return t ^ ((p - 1) * 0.25) * Math.Cos((p - 3.5) * Math.PI * (1 - t) ^ 1.5)
         End Function
         End Function
     End Class
     End Class
@@ -733,7 +735,7 @@ Public Module ModAnimation
             p = Power + 4
             p = Power + 4
         End Sub
         End Sub
         Public Overrides Function GetValue(t As Double) As Double
         Public Overrides Function GetValue(t As Double) As Double
-            t = 1 - MathClamp(t, 0, 1)
+            t = 1 - t.Clamp(0, 1)
             Return 1 - t ^ ((p - 1) * 0.25) * Math.Cos((p - 3.5) * Math.PI * (1 - t) ^ 1.5)
             Return 1 - t ^ ((p - 1) * 0.25) * Math.Cos((p - 3.5) * Math.PI * (1 - t) ^ 1.5)
         End Function
         End Function
     End Class
     End Class
@@ -750,7 +752,15 @@ Public Module ModAnimation
     Public Sub AniStart(AniGroup As IList, Optional Name As String = "", Optional RefreshTime As Boolean = False)
     Public Sub AniStart(AniGroup As IList, Optional Name As String = "", Optional RefreshTime As Boolean = False)
         If RefreshTime Then AniLastTick = GetTimeMs() '避免处理动画时已经造成了极大的延迟,导致动画突然结束
         If RefreshTime Then AniLastTick = GetTimeMs() '避免处理动画时已经造成了极大的延迟,导致动画突然结束
         '添加到正在执行的动画组
         '添加到正在执行的动画组
-        Dim NewEntry As New AniGroupEntry With {.Data = GetFullList(Of AniData)(AniGroup), .StartTick = GetTimeMs()}
+        Dim AllAnis = New List(Of AniData)
+        For Each Element In AniGroup
+            If TypeOf Element Is ICollection Then
+                AllAnis.AddRange(Element)
+            Else
+                AllAnis.Add(Element)
+            End If
+        Next
+        Dim NewEntry As New AniGroupEntry With {.Data = AllAnis, .StartTick = GetTimeMs()}
         If Name = "" Then
         If Name = "" Then
             Name = NewEntry.Uuid
             Name = NewEntry.Uuid
         Else
         Else
@@ -803,12 +813,12 @@ Public Module ModAnimation
                 Log("[Animation] 动画线程开始")
                 Log("[Animation] 动画线程开始")
                 Do While True
                 Do While True
                     '两帧之间的间隔时间
                     '两帧之间的间隔时间
-                    Dim DeltaTime As Long = MathClamp(GetTimeMs() - AniLastTick, 0, 100000)
+                    Dim DeltaTime As Long = (GetTimeMs() - AniLastTick).Clamp(0, 100000)
                     If DeltaTime < 3 Then GoTo Sleeper
                     If DeltaTime < 3 Then GoTo Sleeper
                     AniLastTick = GetTimeMs()
                     AniLastTick = GetTimeMs()
                     '记录 FPS
                     '记录 FPS
                     If ModeDebug Then
                     If ModeDebug Then
-                        If MathClamp(AniLastTick - AniFPSTimer, 0, 100000) >= 500 Then
+                        If (AniLastTick - AniFPSTimer).Clamp(0, 100000) >= 500 Then
                             AniFPS = AniFPSCounter
                             AniFPS = AniFPSCounter
                             AniFPSCounter = 0
                             AniFPSCounter = 0
                             AniFPSTimer = AniLastTick
                             AniFPSTimer = AniLastTick
@@ -820,13 +830,8 @@ Public Module ModAnimation
                     Sub()
                     Sub()
                         AniCount = 0
                         AniCount = 0
                         AniTimer(DeltaTime * AniSpeed)
                         AniTimer(DeltaTime * AniSpeed)
-                        '#If DEBUG Then
-                        '    FrmMain.Title = "F " & AniFPS & ", A " & AniCount & ", R " & NetManage.FileRemain
-                        '#Else
-                        '    If ModeDebug Then FrmMain.Title = "FPS " & AniFPS & ", 动画 " & AniCount & ", 下载中 " & NetManage.FileRemain
-                        '#End If
                         If RandomInteger(0, 64 * If(ModeDebug, 5, 30)) = 0 AndAlso ((AniFPS < 62 AndAlso AniFPS > 0) OrElse AniCount > 4 OrElse NetManager.FileRemain <> 0) Then
                         If RandomInteger(0, 64 * If(ModeDebug, 5, 30)) = 0 AndAlso ((AniFPS < 62 AndAlso AniFPS > 0) OrElse AniCount > 4 OrElse NetManager.FileRemain <> 0) Then
-                            Log("[Report] FPS " & AniFPS & ", 动画 " & AniCount & ", 下载中 " & NetManager.FileRemain & "(" & GetString(NetManager.Speed) & "/s)")
+                            Log("[Report] FPS " & AniFPS & ", 动画 " & AniCount & ", 下载中 " & NetManager.FileRemain & "(" & FormatFileSize(NetManager.Speed) & "/s)")
                         End If
                         End If
                     End Sub)
                     End Sub)
 Sleeper:
 Sleeper:
@@ -834,7 +839,7 @@ Sleeper:
                     Thread.Sleep(1)
                     Thread.Sleep(1)
                 Loop
                 Loop
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "动画帧执行失败", LogLevel.Critical)
+                Log(ex, "动画帧执行失败", NotifyLevel.Critical)
             End Try
             End Try
         End Sub, "Animation", ThreadPriority.AboveNormal)
         End Sub, "Animation", ThreadPriority.AboveNormal)
     End Sub
     End Sub
@@ -845,7 +850,7 @@ Sleeper:
     Public Sub AniTimer(DeltaTick As Integer)
     Public Sub AniTimer(DeltaTick As Integer)
         Try
         Try
 
 
-            If DeltaTick / AniSpeed > 100 Then Log("[Animation] 两个动画帧间隔 " & DeltaTick & " ms", LogLevel.Developer)
+            If DeltaTick / AniSpeed > 100 Then Log("[Animation] 两个动画帧间隔 " & DeltaTick & " ms", NotifyLevel.DevelopOnly)
             Dim i As Integer = -1
             Dim i As Integer = -1
             '循环每个动画组
             '循环每个动画组
             Do While i + 1 < AniGroups.Count
             Do While i + 1 < AniGroups.Count
@@ -910,7 +915,7 @@ NextAni:
             Loop
             Loop
 
 
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "动画刻执行失败", LogLevel.Hint)
+            Log(ex, "动画刻执行失败", NotifyLevel.AllUsers)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -923,15 +928,39 @@ NextAni:
             Select Case Ani.TypeMain
             Select Case Ani.TypeMain
 
 
                 Case AniType.Number
                 Case AniType.Number
-                    Dim Delta As Double = MathPercent(0, Ani.Value, Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, Ani.TimePercent))
+                    Dim Delta As Double = MathUtils.Lerp(0, Ani.Value, Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, Ani.TimePercent))
                     If Delta <> 0 Then
                     If Delta <> 0 Then
                         Select Case Ani.TypeSub
                         Select Case Ani.TypeSub
                             Case AniTypeSub.X
                             Case AniTypeSub.X
-                                DeltaLeft(Ani.Obj, Delta)
+                                If TypeOf Ani.Obj Is Window Then
+                                    CType(Ani.Obj, Window).Left += Delta
+                                Else
+                                    Select Case Ani.Obj.HorizontalAlignment
+                                        Case HorizontalAlignment.Left, HorizontalAlignment.Stretch
+                                            Ani.Obj.Margin = New Thickness(Ani.Obj.Margin.Left + Delta, Ani.Obj.Margin.Top, Ani.Obj.Margin.Right, Ani.Obj.Margin.Bottom)
+                                        Case HorizontalAlignment.Right
+                                            Ani.Obj.Margin = New Thickness(Ani.Obj.Margin.Left, Ani.Obj.Margin.Top, Ani.Obj.Margin.Right - Delta, Ani.Obj.Margin.Bottom)
+                                    End Select
+                                End If
                             Case AniTypeSub.Y
                             Case AniTypeSub.Y
-                                DeltaTop(Ani.Obj, Delta)
+                                If TypeOf Ani.Obj Is Window Then
+                                    CType(Ani.Obj, Window).Top += Delta
+                                Else
+                                    Select Case Ani.Obj.VerticalAlignment
+                                        Case VerticalAlignment.Top
+                                            Ani.Obj.Margin = New Thickness(Ani.Obj.Margin.Left, Ani.Obj.Margin.Top + Delta, Ani.Obj.Margin.Right, Ani.Obj.Margin.Bottom)
+                                        Case VerticalAlignment.Bottom
+                                            Ani.Obj.Margin = New Thickness(Ani.Obj.Margin.Left, Ani.Obj.Margin.Top, Ani.Obj.Margin.Right, Ani.Obj.Margin.Bottom - Delta)
+                                    End Select
+                                End If
                             Case AniTypeSub.Opacity
                             Case AniTypeSub.Opacity
-                                Ani.Obj.Opacity = MathClamp(Ani.Obj.Opacity + Delta, 0, 1)
+                                If TypeOf Ani.Obj Is UIElement Then
+                                    Dim Obj As UIElement = Ani.Obj
+                                    Obj.Opacity = (Obj.Opacity + Delta).Clamp(0, 1)
+                                ElseIf TypeOf Ani.Obj Is DropShadowEffect Then
+                                    Dim Obj As DropShadowEffect = Ani.Obj
+                                    Obj.Opacity = (Obj.Opacity + Delta).Clamp(0, 1)
+                                End If
                             Case AniTypeSub.Width
                             Case AniTypeSub.Width
                                 Dim Obj As FrameworkElement = Ani.Obj
                                 Dim Obj As FrameworkElement = Ani.Obj
                                 Obj.Width = Math.Max(If(Double.IsNaN(Obj.Width), Obj.ActualWidth, Obj.Width) + Delta, 0)
                                 Obj.Width = Math.Max(If(Double.IsNaN(Obj.Width), Obj.ActualWidth, Obj.Width) + Delta, 0)
@@ -963,7 +992,7 @@ NextAni:
 
 
                 Case AniType.Color
                 Case AniType.Color
                     '利用 Last 记录了余下的小数值
                     '利用 Last 记录了余下的小数值
-                    Dim Delta As MyColor = MathPercent(New MyColor(0, 0, 0, 0), Ani.Value, Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, Ani.TimePercent)) + Ani.ValueLast
+                    Dim Delta As MyColor = MyColor.Lerp(New MyColor(0, 0, 0, 0), Ani.Value, Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, Ani.TimePercent)) + Ani.ValueLast
                     Dim Obj As FrameworkElement = Ani.Obj(0)
                     Dim Obj As FrameworkElement = Ani.Obj(0)
                     Dim Prop As DependencyProperty = Ani.Obj(1)
                     Dim Prop As DependencyProperty = Ani.Obj(1)
                     Dim NewColor As MyColor = New MyColor(Obj.GetValue(Prop)) + Delta
                     Dim NewColor As MyColor = New MyColor(Obj.GetValue(Prop)) + Delta
@@ -973,17 +1002,18 @@ NextAni:
                 Case AniType.Scale
                 Case AniType.Scale
                     Dim Obj As FrameworkElement = Ani.Obj
                     Dim Obj As FrameworkElement = Ani.Obj
                     Dim Delta As Double = Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, Ani.TimePercent)
                     Dim Delta As Double = Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, Ani.TimePercent)
-                    Obj.Margin = New Thickness(Obj.Margin.Left + MathPercent(0, Ani.Value.Left, Delta), Obj.Margin.Top + MathPercent(0, Ani.Value.Top, Delta), Obj.Margin.Right + MathPercent(0, Ani.Value.Left, Delta), Obj.Margin.Bottom + MathPercent(0, Ani.Value.Top, Delta))
-                    Obj.Width = Math.Max(Obj.Width + MathPercent(0, Ani.Value.Width, Delta), 0)
-                    Obj.Height = Math.Max(Obj.Height + MathPercent(0, Ani.Value.Height, Delta), 0)
+                    Obj.Margin = New Thickness(Obj.Margin.Left + MathUtils.Lerp(0, Ani.Value.Left, Delta), Obj.Margin.Top + MathUtils.Lerp(0, Ani.Value.Top, Delta), Obj.Margin.Right + MathUtils.Lerp(0, Ani.Value.Left, Delta), Obj.Margin.Bottom + MathUtils.Lerp(0, Ani.Value.Top, Delta))
+                    Obj.Width = Math.Max(Obj.Width + MathUtils.Lerp(0, Ani.Value.Width, Delta), 0)
+                    Obj.Height = Math.Max(Obj.Height + MathUtils.Lerp(0, Ani.Value.Height, Delta), 0)
 
 
                 Case AniType.TextAppear
                 Case AniType.TextAppear
-                    Dim TextCount As Integer = If(Ani.Value(1), Ani.Value(0).ToString.Length, 0) +
-                                               Math.Round(Ani.Value(0).ToString.Length * If(Ani.Value(1), -1, 1) * Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, 0))
-                    Dim NewText As String = Mid(Ani.Value(0), 1, TextCount)
+                    Dim Text As String = Ani.Value(0)
+                    Dim TextCount As Integer = If(Ani.Value(1), Text.Length, 0) +
+                                               Math.Round(Text.Length * If(Ani.Value(1), -1, 1) * Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, 0))
+                    Dim NewText As String = Text.Substring(0, TextCount)
                     '添加乱码
                     '添加乱码
-                    If TextCount < Ani.Value(0).ToString.Length Then
-                        Dim NextText As String = Mid(Ani.Value(0), TextCount + 1, 1)
+                    If TextCount < Text.Length Then
+                        Dim NextText As String = Text.Substring(TextCount, 1)
                         If Convert.ToInt32(Convert.ToChar(NextText)) >= Convert.ToInt32(Convert.ToChar(128)) Then
                         If Convert.ToInt32(Convert.ToChar(NextText)) >= Convert.ToInt32(Convert.ToChar(128)) Then
                             NewText &= Encoding.GetEncoding("GB18030").GetString({RandomInteger(16 + 160, 87 + 160), RandomInteger(1 + 160, 89 + 160)})
                             NewText &= Encoding.GetEncoding("GB18030").GetString({RandomInteger(16 + 160, 87 + 160), RandomInteger(1 + 160, 89 + 160)})
                         Else
                         Else
@@ -1006,7 +1036,7 @@ NextAni:
                         If Obj.RenderTransformOrigin = New Point(0, 0) Then Obj.RenderTransformOrigin = New Point(0.5, 0.5)
                         If Obj.RenderTransformOrigin = New Point(0, 0) Then Obj.RenderTransformOrigin = New Point(0.5, 0.5)
                         Obj.RenderTransform = New ScaleTransform(1, 1)
                         Obj.RenderTransform = New ScaleTransform(1, 1)
                     End If
                     End If
-                    Dim Delta As Double = MathPercent(0, Ani.Value, Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, Ani.TimePercent))
+                    Dim Delta As Double = MathUtils.Lerp(0, Ani.Value, Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, Ani.TimePercent))
                     CType(Obj.RenderTransform, ScaleTransform).ScaleX = Math.Max(CType(Obj.RenderTransform, ScaleTransform).ScaleX + Delta, 0)
                     CType(Obj.RenderTransform, ScaleTransform).ScaleX = Math.Max(CType(Obj.RenderTransform, ScaleTransform).ScaleX + Delta, 0)
                     CType(Obj.RenderTransform, ScaleTransform).ScaleY = Math.Max(CType(Obj.RenderTransform, ScaleTransform).ScaleY + Delta, 0)
                     CType(Obj.RenderTransform, ScaleTransform).ScaleY = Math.Max(CType(Obj.RenderTransform, ScaleTransform).ScaleY + Delta, 0)
 
 
@@ -1016,13 +1046,13 @@ NextAni:
                         If Obj.RenderTransformOrigin = New Point(0, 0) Then Obj.RenderTransformOrigin = New Point(0.5, 0.5)
                         If Obj.RenderTransformOrigin = New Point(0, 0) Then Obj.RenderTransformOrigin = New Point(0.5, 0.5)
                         Obj.RenderTransform = New RotateTransform(0)
                         Obj.RenderTransform = New RotateTransform(0)
                     End If
                     End If
-                    Dim Delta As Double = MathPercent(0, Ani.Value, Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, Ani.TimePercent))
+                    Dim Delta As Double = MathUtils.Lerp(0, Ani.Value, Ani.Ease.GetDelta(Ani.TimeFinished / Ani.TimeTotal, Ani.TimePercent))
                     CType(Obj.RenderTransform, RotateTransform).Angle = CType(Obj.RenderTransform, RotateTransform).Angle + Delta
                     CType(Obj.RenderTransform, RotateTransform).Angle = CType(Obj.RenderTransform, RotateTransform).Angle + Delta
 
 
             End Select
             End Select
             Ani.TimePercent = Ani.TimeFinished / Ani.TimeTotal '修改执行百分比
             Ani.TimePercent = Ani.TimeFinished / Ani.TimeTotal '修改执行百分比
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "执行动画失败:" & Ani.ToString, LogLevel.Hint)
+            Log(ex, "执行动画失败:" & Ani.ToString, NotifyLevel.AllUsers)
         End Try
         End Try
         Return Ani
         Return Ani
     End Function
     End Function

Разница между файлами не показана из-за своего большого размера
+ 181 - 450
Plain Craft Launcher 2/Modules/Base/ModBase.vb


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

@@ -38,7 +38,7 @@
                         RealParent = RealParent.Parent
                         RealParent = RealParent.Parent
                     End While
                     End While
                 Catch ex As Exception
                 Catch ex As Exception
-                    Log(ex, "获取父加载器失败(" & Name & ")", LogLevel.Feedback)
+                    Log(ex, "获取父加载器失败(" & Name & ")", NotifyLevel.MsgBoxAndFeedback)
                     Return Nothing
                     Return Nothing
                 End Try
                 End Try
             End Get
             End Get
@@ -283,7 +283,7 @@
             Try
             Try
                 Input = StartGetInput(Input)
                 Input = StartGetInput(Input)
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "加载输入获取失败(" & Name & ")", LogLevel.Hint)
+                Log(ex, "加载输入获取失败(" & Name & ")", NotifyLevel.AllUsers)
                 [Error] = ex
                 [Error] = ex
                 SyncLock LockState
                 SyncLock LockState
                     State = LoadState.Failed
                     State = LoadState.Failed
@@ -322,7 +322,7 @@
                     If ModeDebug AndAlso Not LogBlackList.Contains(Name) Then Log($"[Loader] 加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已{If(IsForceRestarting, "强制", "")}启动")
                     If ModeDebug AndAlso Not LogBlackList.Contains(Name) Then Log($"[Loader] 加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已{If(IsForceRestarting, "强制", "")}启动")
                     LoadDelegate(Me)
                     LoadDelegate(Me)
                     If IsInterrupted Then
                     If IsInterrupted Then
-                        Log($"[Loader] 加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已中断但线程正常运行至结束,输出被弃用(最新线程:{If(LastRunningThread Is Nothing, -1, LastRunningThread.ManagedThreadId)})", LogLevel.Developer)
+                        Log($"[Loader] 加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已中断但线程正常运行至结束,输出被弃用(最新线程:{If(LastRunningThread Is Nothing, -1, LastRunningThread.ManagedThreadId)})", NotifyLevel.DevelopOnly)
                         Return
                         Return
                     End If
                     End If
                     If ModeDebug AndAlso Not LogBlackList.Contains(Name) Then Log($"[Loader] 加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已完成")
                     If ModeDebug AndAlso Not LogBlackList.Contains(Name) Then Log($"[Loader] 加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 已完成")
@@ -354,7 +354,7 @@
                 If IsInterrupted OrElse State >= LoadState.Finished Then Return
                 If IsInterrupted OrElse State >= LoadState.Finished Then Return
                 State = LoadState.Failed
                 State = LoadState.Failed
             End SyncLock
             End SyncLock
-            Log(ex, $"加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 出错,已完成 {Math.Round(Progress * 100)}%", LogLevel.Developer)
+            Log(ex, $"加载线程 {Name} ({Thread.CurrentThread.ManagedThreadId}) 出错,已完成 {Math.Round(Progress * 100)}%", NotifyLevel.DevelopOnly)
             TriggerThreadInterrupt()
             TriggerThreadInterrupt()
         End Sub
         End Sub
         Public Overrides Sub Interrupt()
         Public Overrides Sub Interrupt()
@@ -527,7 +527,7 @@
                         '检查是否需要重启
                         '检查是否需要重启
                         If Loader.GetType.Name.StartsWithF("LoaderTask") Then
                         If Loader.GetType.Name.StartsWithF("LoaderTask") Then
                             If CType(Loader, Object).ShouldStart(If(Input IsNot Nothing AndAlso Loader.GetType.GenericTypeArguments.First Is Input.GetType, Input, Nothing), IgnoreReloadTimeout:=True) Then
                             If CType(Loader, Object).ShouldStart(If(Input IsNot Nothing AndAlso Loader.GetType.GenericTypeArguments.First Is Input.GetType, Input, Nothing), IgnoreReloadTimeout:=True) Then
-                                Log("[Loader] 由于输入条件变更,重启进行中的加载器 " & Loader.Name, LogLevel.Developer)
+                                Log("[Loader] 由于输入条件变更,重启进行中的加载器 " & Loader.Name, NotifyLevel.DevelopOnly)
                                 GoTo Restart
                                 GoTo Restart
                             End If
                             End If
                         End If
                         End If
@@ -591,7 +591,7 @@ Restart:
     End Class
     End Class
 
 
     '任务栏进度条
     '任务栏进度条
-    Public LoaderTaskbar As New SafeList(Of LoaderBase)
+    Public LoaderTaskbar As New ConcurrentList(Of LoaderBase)
     Public LoaderTaskbarProgress As Double = 0 '平滑后的进度
     Public LoaderTaskbarProgress As Double = 0 '平滑后的进度
     Private LoaderTaskbarProgressLast As Shell.TaskbarItemProgressState = Shell.TaskbarItemProgressState.None
     Private LoaderTaskbarProgressLast As Shell.TaskbarItemProgressState = Shell.TaskbarItemProgressState.None
 
 
@@ -635,15 +635,15 @@ Restart:
                 FrmMain.BtnExtraDownload.ShowRefresh()
                 FrmMain.BtnExtraDownload.ShowRefresh()
             End If
             End If
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "刷新任务栏进度显示失败", LogLevel.Feedback)
+            Log(ex, "刷新任务栏进度显示失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
     Public Function LoaderTaskbarProgressGet() As Double
     Public Function LoaderTaskbarProgressGet() As Double
         Try
         Try
             If Not LoaderTaskbar.Any Then Return 1
             If Not LoaderTaskbar.Any Then Return 1
-            Return MathClamp(LoaderTaskbar.Select(Function(l) l.Progress).Average(), 0, 1)
+            Return LoaderTaskbar.Select(Function(l) l.Progress).Average().Clamp(0, 1)
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "获取任务栏进度出错", LogLevel.Feedback)
+            Log(ex, "获取任务栏进度出错", NotifyLevel.MsgBoxAndFeedback)
             Return 0.5
             Return 0.5
         End Try
         End Try
     End Function
     End Function

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

@@ -36,7 +36,7 @@ Public Module ModNet
         Catch ex As ThreadInterruptedException
         Catch ex As ThreadInterruptedException
             Throw
             Throw
         Catch ex As Exception
         Catch ex As Exception
-            If MakeLog Then Log(ex, "网络请求失败", LogLevel.Developer)
+            If MakeLog Then Log(ex, "网络请求失败", NotifyLevel.DevelopOnly)
             Throw
             Throw
         End Try
         End Try
     End Function
     End Function
@@ -176,7 +176,7 @@ RequestFinished:
         Log($"[Net] 通过网络请求直接下载小文件:{Url} → {LocalPath}")
         Log($"[Net] 通过网络请求直接下载小文件:{Url} → {LocalPath}")
         '初始化文件路径
         '初始化文件路径
         Try
         Try
-            Directory.CreateDirectory(GetPathFromFullPath(LocalPath))
+            DirectoryUtils.Create(LocalPath, isFilePath:=True)
             File.Delete(LocalPath)
             File.Delete(LocalPath)
         Catch ex As Exception
         Catch ex As Exception
             Throw New Exception($"预处理下载文件路径失败({LocalPath})", ex)
             Throw New Exception($"预处理下载文件路径失败({LocalPath})", ex)
@@ -257,9 +257,7 @@ Retry:
             'DNS 解析
             'DNS 解析
             CancelToken = New CancellationTokenSource(Timeout)
             CancelToken = New CancellationTokenSource(Timeout)
             HostIp = DNSLookup(Request, CancelToken.Token)
             HostIp = DNSLookup(Request, CancelToken.Token)
-            If HostIp IsNot Nothing AndAlso Not IPReliability.ContainsKey(HostIp) Then
-                IPReliability(HostIp) = -0.01 '预先降低一点,这样快速的重复请求会使用不同的 IP 以提高成功率
-            End If
+            If HostIp IsNot Nothing Then IPReliability.GetOrAdd(HostIp, -0.01) '预先降低一点,这样快速的重复请求会使用不同的 IP 以提高成功率
             '发送请求
             '发送请求
             SyncLock RequestClientLock
             SyncLock RequestClientLock
                 If RequestClient Is Nothing Then '延迟初始化,以避免在程序启动前加载 CacheCow 导致 DLL 加载失败
                 If RequestClient Is Nothing Then '延迟初始化,以避免在程序启动前加载 CacheCow 导致 DLL 加载失败
@@ -269,8 +267,10 @@ Retry:
                     })
                     })
                 End If
                 End If
             End SyncLock
             End SyncLock
+            CancelToken.Dispose() : CancelToken = New CancellationTokenSource(Timeout)
             Response = RequestClient.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancelToken.Token).GetResultWithTimeout(CancelToken, Timeout)
             Response = RequestClient.SendAsync(Request, HttpCompletionOption.ResponseHeadersRead, CancelToken.Token).GetResultWithTimeout(CancelToken, Timeout)
             Dim ResponseBytes As Byte()
             Dim ResponseBytes As Byte()
+            CancelToken.Dispose() : CancelToken = New CancellationTokenSource(Timeout)
             Using ResponseStream = Response.Content.ReadAsStreamAsync().GetResultWithTimeout(CancelToken, Timeout)
             Using ResponseStream = Response.Content.ReadAsStreamAsync().GetResultWithTimeout(CancelToken, Timeout)
                 Using Stream As New MemoryStream
                 Using Stream As New MemoryStream
                     ResponseStream.CopyToAsync(Stream, 81920, CancelToken.Token).GetResultWithTimeout(CancelToken, Timeout)
                     ResponseStream.CopyToAsync(Stream, 81920, CancelToken.Token).GetResultWithTimeout(CancelToken, Timeout)
@@ -320,17 +320,16 @@ Retry:
     Private Function DNSLookup(Request As HttpRequestMessage, CancelToken As CancellationToken) As String
     Private Function DNSLookup(Request As HttpRequestMessage, CancelToken As CancellationToken) As String
         Dim Host = Request.RequestUri.Host
         Dim Host = Request.RequestUri.Host
         '不对部分有严格 SNI 限制的域名进行 DNS 解析(#8295)
         '不对部分有严格 SNI 限制的域名进行 DNS 解析(#8295)
-        If {"mojang.com", "minecraft.net", "minecraftservices.com"}.Any(Function(h) Host.ContainsF(h, True)) Then Return Nothing
+        If {"mojang.com", "minecraft.net", "minecraftservices.com"}.Any(Function(h) Host.ContainsIgnoreCase(h)) Then Return Nothing
         '若一分钟内已失败过,则不再重复尝试解析,以减少断网时的 Exception 数量
         '若一分钟内已失败过,则不再重复尝试解析,以减少断网时的 Exception 数量
-        SyncLock DNSFailureRecord.SyncRoot
-            If DNSFailureRecord.ContainsKey(Host) Then
-                If Date.Now - DNSFailureRecord(Host) < New TimeSpan(0, 1, 0) Then
-                    Return Nothing
-                Else
-                    DNSFailureRecord.Remove(Host)
-                End If
+        Dim LastFailureTime As Date
+        If DNSFailureRecord.TryGetValue(Host, LastFailureTime) Then
+            If Date.Now - LastFailureTime < New TimeSpan(0, 1, 0) Then
+                Return Nothing
+            Else
+                DNSFailureRecord.Remove(Host)
             End If
             End If
-        End SyncLock
+        End If
         '初步 DNS 解析
         '初步 DNS 解析
         Dim Candidates As IPAddress()
         Dim Candidates As IPAddress()
         Try
         Try
@@ -363,21 +362,21 @@ Retry:
         Request.RequestUri = (New UriBuilder(Request.RequestUri) With {.Host = If(Target.AddressFamily = AddressFamily.InterNetworkV6, $"[{Target}]", Target.ToString)}).Uri
         Request.RequestUri = (New UriBuilder(Request.RequestUri) With {.Host = If(Target.AddressFamily = AddressFamily.InterNetworkV6, $"[{Target}]", Target.ToString)}).Uri
         Return Target.ToString
         Return Target.ToString
     End Function
     End Function
-    Private DNSFailureRecord As New SafeDictionary(Of String, Date)
+    Private DNSFailureRecord As New ConcurrentDictionary(Of String, Date)
     ''' <summary>
     ''' <summary>
     ''' 记录每个 IP 地址的请求可靠度。
     ''' 记录每个 IP 地址的请求可靠度。
     ''' 通常取值 -1 ~ +0.5,越高越好。未尝试过的 IP 应视为 0。
     ''' 通常取值 -1 ~ +0.5,越高越好。未尝试过的 IP 应视为 0。
     ''' </summary>
     ''' </summary>
-    Private IPReliability As New SafeDictionary(Of String, Double)
+    Private IPReliability As New ConcurrentDictionary(Of String, Double)
     ''' <summary>
     ''' <summary>
     ''' 根据请求结果,记录 IP 地址的可靠度。
     ''' 根据请求结果,记录 IP 地址的可靠度。
     ''' </summary>
     ''' </summary>
     Private Sub RecordIPReliability(IP As String, Result As Double)
     Private Sub RecordIPReliability(IP As String, Result As Double)
         If IP Is Nothing Then Return
         If IP Is Nothing Then Return
-        SyncLock IPReliability.SyncRoot
-            If Not IPReliability.ContainsKey(IP) Then IPReliability(IP) = 0
-            IPReliability(IP) = (IPReliability(IP) + Result) / 2
-        End SyncLock
+        IPReliability.AddOrUpdate(IP,
+            Function() Result * 0.5,
+            Function(k, v) v * 0.5 + Result * 0.5
+        )
     End Sub
     End Sub
 
 
     ''' <summary>
     ''' <summary>
@@ -687,11 +686,11 @@ Retry:
         ''' <summary>
         ''' <summary>
         ''' 所属的文件列表任务。
         ''' 所属的文件列表任务。
         ''' </summary>
         ''' </summary>
-        Public Tasks As New SafeList(Of LoaderDownload)
+        Public Tasks As New ConcurrentList(Of LoaderDownload)
         ''' <summary>
         ''' <summary>
         ''' 所有下载源。
         ''' 所有下载源。
         ''' </summary>
         ''' </summary>
-        Public Sources As SafeList(Of NetSource)
+        Public Sources As ConcurrentList(Of NetSource)
         ''' <summary>
         ''' <summary>
         ''' 用于在第一个线程出错时切换下载源。
         ''' 用于在第一个线程出错时切换下载源。
         ''' </summary>
         ''' </summary>
@@ -699,7 +698,7 @@ Retry:
         ''' <summary>
         ''' <summary>
         ''' 所有已经被标记为失败的,但未完整尝试过的,不允许断点续传的下载源。
         ''' 所有已经被标记为失败的,但未完整尝试过的,不允许断点续传的下载源。
         ''' </summary>
         ''' </summary>
-        Public SourcesOnce As New SafeList(Of NetSource)
+        Public SourcesOnce As New ConcurrentList(Of NetSource)
         ''' <summary>
         ''' <summary>
         ''' 仅当合并失败或首次下载失败时,会将所有下载源重新标记为不允许断点续传的下载源,逐个重新尝试下载。
         ''' 仅当合并失败或首次下载失败时,会将所有下载源重新标记为不允许断点续传的下载源,逐个重新尝试下载。
         ''' 这一策略可以兼容多个下载源中的一部分返回错误的文件的情况,以及部分在多线程下载时会抽风的源。
         ''' 这一策略可以兼容多个下载源中的一部分返回错误的文件的情况,以及部分在多线程下载时会抽风的源。
@@ -895,7 +894,7 @@ Retry:
             Dim Count As Integer = 0
             Dim Count As Integer = 0
             Urls = Urls.Distinct.ToArray
             Urls = Urls.Distinct.ToArray
             For Each Source As String In Urls
             For Each Source As String In Urls
-                Sources.Add(New NetSource With {.FailCount = 0, .Url = SecretCdnSign(Source.Replace(vbCr, "").Replace(vbLf, "").Trim), .Id = Count, .IsFailed = False, .Ex = Nothing})
+                Sources.Add(New NetSource With {.FailCount = 0, .Url = SecretCdnSign(Source.ReplaceLineEndings("").Trim), .Id = Count, .IsFailed = False, .Ex = Nothing})
                 Count += 1
                 Count += 1
             Next
             Next
             Me.Sources = Sources
             Me.Sources = Sources
@@ -1049,7 +1048,7 @@ StartThread:
                         End If
                         End If
                     End If
                     End If
                     FileSize = ContentLength : IsUnknownSize = False
                     FileSize = ContentLength : IsUnknownSize = False
-                    Log($"[Download] {LocalName}:文件大小 {ContentLength}({GetString(ContentLength)})")
+                    Log($"[Download] {LocalName}:文件大小 {ContentLength}({FormatFileSize(ContentLength)})")
                     '若文件大小大于 50 M,进行剩余磁盘空间校验
                     '若文件大小大于 50 M,进行剩余磁盘空间校验
                     If ContentLength > 50 * 1024 * 1024 Then
                     If ContentLength > 50 * 1024 * 1024 Then
                         For Each Drive As DriveInfo In DriveInfo.GetDrives
                         For Each Drive As DriveInfo In DriveInfo.GetDrives
@@ -1057,7 +1056,7 @@ StartThread:
                             Dim RequiredSpace = If(PathTemp.StartsWithF(DriveName), ContentLength * 1.1, 0) +
                             Dim RequiredSpace = If(PathTemp.StartsWithF(DriveName), ContentLength * 1.1, 0) +
                                                 If(LocalPath.StartsWithF(DriveName), ContentLength + 5 * 1024 * 1024, 0)
                                                 If(LocalPath.StartsWithF(DriveName), ContentLength + 5 * 1024 * 1024, 0)
                             If RequiredSpace > 0 AndAlso Drive.TotalFreeSpace < RequiredSpace Then
                             If RequiredSpace > 0 AndAlso Drive.TotalFreeSpace < RequiredSpace Then
-                                Throw New Exception(DriveName & " 盘空间不足,无法进行下载。" & vbCrLf & "需要至少 " & GetString(RequiredSpace) & " 空间,但当前仅剩余 " & GetString(Drive.TotalFreeSpace) & "。" &
+                                Throw New Exception(DriveName & " 盘空间不足,无法进行下载。" & vbCrLf & "需要至少 " & FormatFileSize(RequiredSpace) & " 空间,但当前仅剩余 " & FormatFileSize(Drive.TotalFreeSpace) & "。" &
                                                     If(PathTemp.StartsWithF(DriveName), vbCrLf & vbCrLf & "下载时需要与文件同等大小的空间存放缓存,你可以在设置中调整缓存文件夹的位置。", ""))
                                                     If(PathTemp.StartsWithF(DriveName), vbCrLf & vbCrLf & "下载时需要与文件同等大小的空间存放缓存,你可以在设置中调整缓存文件夹的位置。", ""))
                             End If
                             End If
                         Next
                         Next
@@ -1084,11 +1083,12 @@ NotSupportRange:
                     Cache = New MemoryStream
                     Cache = New MemoryStream
                     ResultStream = Cache
                     ResultStream = Cache
                 Else
                 Else
-                    Directory.CreateDirectory(PathTemp & "Download")
                     Th.Temp = $"{PathTemp}Download\{Uuid}_{Th.Uuid}_{RandomInteger(0, 999999)}.tmp"
                     Th.Temp = $"{PathTemp}Download\{Uuid}_{Th.Uuid}_{RandomInteger(0, 999999)}.tmp"
+                    DirectoryUtils.Create(Th.Temp, isFilePath:=True)
                     ResultStream = New FileStream(Th.Temp, FileMode.Create, FileAccess.Write, FileShare.Read)
                     ResultStream = New FileStream(Th.Temp, FileMode.Create, FileAccess.Write, FileShare.Read)
                 End If
                 End If
                 '开始下载
                 '开始下载
+                CancelToken.Dispose() : CancelToken = New CancellationTokenSource() '重置超时,不再让 CancellationToken 在固定时间后触发(#8488)
                 ResponseStream = Response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()
                 ResponseStream = Response.Content.ReadAsStreamAsync().GetAwaiter().GetResult()
                 If Settings.Get("SystemDebugDelay") Then Threading.Thread.Sleep(RandomInteger(50, 3000))
                 If Settings.Get("SystemDebugDelay") Then Threading.Thread.Sleep(RandomInteger(50, 3000))
                 Dim ResponseBytes As Byte() = New Byte(16384) {}
                 Dim ResponseBytes As Byte() = New Byte(16384) {}
@@ -1183,7 +1183,7 @@ SourceBreak:
                ex.Message.Contains("(502)") OrElse ex.Message.Contains("(404)") OrElse
                ex.Message.Contains("(502)") OrElse ex.Message.Contains("(404)") OrElse
                ex.Message.Contains("未能解析") OrElse ex.Message.Contains("无返回数据") OrElse ex.Message.Contains("空间不足") OrElse
                ex.Message.Contains("未能解析") OrElse ex.Message.Contains("无返回数据") OrElse ex.Message.Contains("空间不足") OrElse
                ((ex.Message.Contains("(403)") OrElse ex.Message.Contains("(429)")) AndAlso Not Th.Source.Url.Contains("bmclapi")) OrElse 'BMCLAPI 的部分源在高频率请求下会返回 403/429,所以不应因此禁用下载源
                ((ex.Message.Contains("(403)") OrElse ex.Message.Contains("(429)")) AndAlso Not Th.Source.Url.Contains("bmclapi")) OrElse 'BMCLAPI 的部分源在高频率请求下会返回 403/429,所以不应因此禁用下载源
-               (Th.Source.FailCount >= MathClamp(NetTaskThreadLimit, 5, 30) AndAlso DownloadDone < 1) OrElse Th.Source.FailCount > NetTaskThreadLimit + 2 Then
+               (Th.Source.FailCount >= NetTaskThreadLimit.Clamp(5, 30) AndAlso DownloadDone < 1) OrElse Th.Source.FailCount > NetTaskThreadLimit + 2 Then
                 '当一个下载源有多个线程在下载时,只选择其中一个线程进行后续处理
                 '当一个下载源有多个线程在下载时,只选择其中一个线程进行后续处理
                 Dim IsThisFail As Boolean = False
                 Dim IsThisFail As Boolean = False
                 SyncLock LockSource
                 SyncLock LockSource
@@ -1196,7 +1196,7 @@ SourceBreak:
                 If IsThisFail Then
                 If IsThisFail Then
                     Log($"[Download] {LocalName}:下载源被禁用({Th.Source.Id},Range 问题:{IsRangeNotSupported}):{Th.Source.Url}")
                     Log($"[Download] {LocalName}:下载源被禁用({Th.Source.Id},Range 问题:{IsRangeNotSupported}):{Th.Source.Url}")
                     Log(ex, $"{If(SourcesOnce.FirstOrDefault?.SingleThread Is Nothing, "", "单线程")}下载源 {Th.Source.Id} 已被禁用",
                     Log(ex, $"{If(SourcesOnce.FirstOrDefault?.SingleThread Is Nothing, "", "单线程")}下载源 {Th.Source.Id} 已被禁用",
-                        If(IsRangeNotSupported OrElse ex.Message.Contains("(404)"), LogLevel.Developer, LogLevel.Debug))
+                        If(IsRangeNotSupported OrElse ex.Message.Contains("(404)"), NotifyLevel.DevelopOnly, NotifyLevel.DebugModeOnly))
                     SyncLock LockSource
                     SyncLock LockSource
                         SourcesOnce.Remove(Th.Source)
                         SourcesOnce.Remove(Th.Source)
                     End SyncLock
                     End SyncLock
@@ -1208,7 +1208,7 @@ SourceBreak:
                     ElseIf Not Retried Then
                     ElseIf Not Retried Then
                         '合并失败或首次下载失败,未重试:将所有下载源重新标记为不允许断点续传的下载源,逐个重新尝试下载
                         '合并失败或首次下载失败,未重试:将所有下载源重新标记为不允许断点续传的下载源,逐个重新尝试下载
                         '若所有源均不支持 Range,也会走到这里重试
                         '若所有源均不支持 Range,也会走到这里重试
-                        If Not IsRangeNotSupported Then Log($"[Download] {LocalName}:文件下载失败,正在自动重试……", LogLevel.Debug)
+                        If Not IsRangeNotSupported Then Log($"[Download] {LocalName}:文件下载失败,正在自动重试……", NotifyLevel.DebugModeOnly)
                         Retried = True
                         Retried = True
                         SyncLock LockSource
                         SyncLock LockSource
                             SourcesOnce.Clear()
                             SourcesOnce.Clear()
@@ -1236,7 +1236,7 @@ SourceBreak:
                                 Log("[Download] 已禁用的下载源:" & Source.Url)
                                 Log("[Download] 已禁用的下载源:" & Source.Url)
                                 If Source.Ex IsNot Nothing Then
                                 If Source.Ex IsNot Nothing Then
                                     ExampleEx = Source.Ex
                                     ExampleEx = Source.Ex
-                                    Log(Source.Ex, "下载源禁用原因", LogLevel.Developer)
+                                    Log(Source.Ex, "下载源禁用原因", NotifyLevel.DevelopOnly)
                                 End If
                                 End If
                             Next
                             Next
                         End SyncLock
                         End SyncLock
@@ -1276,15 +1276,14 @@ SourceBreak:
             Try
             Try
 Retry:
 Retry:
                 '创建文件夹
                 '创建文件夹
+                DirectoryUtils.Create(LocalPath, isFilePath:=True)
                 If File.Exists(LocalPath) Then File.Delete(LocalPath)
                 If File.Exists(LocalPath) Then File.Delete(LocalPath)
-                Directory.CreateDirectory(GetPathFromFullPath(LocalPath))
                 SyncLock LockChain
                 SyncLock LockChain
                     '合并文件
                     '合并文件
                     If IsNoSplit Then
                     If IsNoSplit Then
                         '仅有一个线程,从内存中输出
                         '仅有一个线程,从内存中输出
                         If ModeDebug Then Log($"[Download] {LocalName}:下载结束,从内存输出文件")
                         If ModeDebug Then Log($"[Download] {LocalName}:下载结束,从内存输出文件")
-                        Cache.Position = 0
-                        WriteFile(LocalPath, Cache)
+                        FileUtils.Write(LocalPath, Cache)
                     ElseIf Threads.DownloadDone = DownloadDone AndAlso Threads.Temp IsNot Nothing Then
                     ElseIf Threads.DownloadDone = DownloadDone AndAlso Threads.Temp IsNot Nothing Then
                         '仅有一个文件,直接复制
                         '仅有一个文件,直接复制
                         If ModeDebug Then Log($"[Download] {LocalName}:下载结束,仅有一个文件,无需合并")
                         If ModeDebug Then Log($"[Download] {LocalName}:下载结束,仅有一个文件,无需合并")
@@ -1420,7 +1419,7 @@ Retry:
         ''' <summary>
         ''' <summary>
         ''' 需要下载的文件。
         ''' 需要下载的文件。
         ''' </summary>
         ''' </summary>
-        Public Files As SafeList(Of NetFile)
+        Public Files As ConcurrentList(Of NetFile)
         ''' <summary>
         ''' <summary>
         ''' 剩余未完成的文件数。(用于减轻 FilesLock 的占用)
         ''' 剩余未完成的文件数。(用于减轻 FilesLock 的占用)
         ''' </summary>
         ''' </summary>
@@ -1452,7 +1451,7 @@ Retry:
             Set(value As Integer)
             Set(value As Integer)
                 _FailCount = value
                 _FailCount = value
                 If State = LoadState.Loading AndAlso value >= Math.Min(10000, Math.Max(FileRemain * 5.5, NetTaskThreadLimit * 5.5 + 3)) Then
                 If State = LoadState.Loading AndAlso value >= Math.Min(10000, Math.Max(FileRemain * 5.5, NetTaskThreadLimit * 5.5 + 3)) Then
-                    Log("[Download] 由于同加载器中失败次数过多引发强制失败:连续失败了 " & value & " 次", LogLevel.Debug)
+                    Log("[Download] 由于同加载器中失败次数过多引发强制失败:连续失败了 " & value & " 次", NotifyLevel.DebugModeOnly)
                     On Error Resume Next
                     On Error Resume Next
                     Dim ExList As New List(Of Exception)
                     Dim ExList As New List(Of Exception)
                     For Each File In Files
                     For Each File In Files
@@ -1495,12 +1494,12 @@ FinishExCatch:
 
 
         Public Sub New(Name As String, FileTasks As List(Of NetFile))
         Public Sub New(Name As String, FileTasks As List(Of NetFile))
             Me.Name = Name
             Me.Name = Name
-            Files = New SafeList(Of NetFile)(FileTasks)
+            Files = New ConcurrentList(Of NetFile)(FileTasks)
         End Sub
         End Sub
         Public Overrides Sub Start(Optional Input As Object = Nothing, Optional IsForceRestart As Boolean = False)
         Public Overrides Sub Start(Optional Input As Object = Nothing, Optional IsForceRestart As Boolean = False)
-            If Input IsNot Nothing Then Files = New SafeList(Of NetFile)(Input)
+            If Input IsNot Nothing Then Files = New ConcurrentList(Of NetFile)(Input)
             '去重
             '去重
-            Files = New SafeList(Of NetFile)(Files.Distinct(Function(a, b) a.LocalPath = b.LocalPath))
+            Files = New ConcurrentList(Of NetFile)(Files.DistinctBy(Function(a) a.LocalPath))
             '设置剩余文件数
             '设置剩余文件数
             SyncLock FileRemainLock
             SyncLock FileRemainLock
                 FileRemain += Files.Where(Function(f) f.State <> NetState.Finished).Count
                 FileRemain += Files.Where(Function(f) f.State <> NetState.Finished).Count
@@ -1527,7 +1526,7 @@ FinishExCatch:
                         File.LocalPath = File.LocalPath.Replace("/", "\")
                         File.LocalPath = File.LocalPath.Replace("/", "\")
                         If Not File.LocalPath.Lower.Contains(":\") Then Throw New ArgumentException("输入的本地文件地址不正确:" & File.LocalPath)
                         If Not File.LocalPath.Lower.Contains(":\") Then Throw New ArgumentException("输入的本地文件地址不正确:" & File.LocalPath)
                         If File.LocalPath.EndsWithF("\") Then Throw New ArgumentException("请输入含文件名的完整文件路径:" & File.LocalPath)
                         If File.LocalPath.EndsWithF("\") Then Throw New ArgumentException("请输入含文件名的完整文件路径:" & File.LocalPath)
-                        Directory.CreateDirectory(GetPathFromFullPath(File.LocalPath)) '创建目标文件夹
+                        DirectoryUtils.Create(File.LocalPath, isFilePath:=True) '创建目标文件夹
                     Next
                     Next
                     '接入下载管理器,获取新开始下载的文件列表
                     '接入下载管理器,获取新开始下载的文件列表
                     Dim NewFiles = NetManager.Start(Me)
                     Dim NewFiles = NetManager.Start(Me)
@@ -1555,7 +1554,7 @@ FinishExCatch:
                     Folders.AddRange(McFolderList.Select(Function(f) f.Location))
                     Folders.AddRange(McFolderList.Select(Function(f) f.Location))
                     Folders = Folders.Distinct.Where(Function(f) Directory.Exists(f)).ToList
                     Folders = Folders.Distinct.Where(Function(f) Directory.Exists(f)).ToList
                     '平均分配到多个检查线程
                     '平均分配到多个检查线程
-                    Dim ThreadCount As Integer = MathClamp(FilesToCheck.Count \ 40, 1, 8) '每个线程至少 40 个文件,最多 8 线程
+                    Dim ThreadCount As Integer = (FilesToCheck.Count \ 40).Clamp(1, 8) '每个线程至少 40 个文件,最多 8 线程
                     If ThreadCount = 1 Then '只有一个线程,直接执行
                     If ThreadCount = 1 Then '只有一个线程,直接执行
                         CheckExistingFiles(FilesToCheck, Folders)
                         CheckExistingFiles(FilesToCheck, Folders)
                     Else
                     Else
@@ -1590,6 +1589,7 @@ FinishExCatch:
                 '处理每个文件
                 '处理每个文件
                 For Each File As NetFile In Files
                 For Each File As NetFile In Files
                     Dim Target As String = CheckExistingFile(FolderList, VersionFolders, File)
                     Dim Target As String = CheckExistingFile(FolderList, VersionFolders, File)
+                    If ModeDebug Then Log($"[Download] 文件检查结果:{File.LocalName} → {Target},当前状态 {File.State}")
                     If File.State >= NetState.WaitingToDownload Then Continue For '中断
                     If File.State >= NetState.WaitingToDownload Then Continue For '中断
                     '已找到相同文件
                     '已找到相同文件
                     If Target IsNot Nothing Then
                     If Target IsNot Nothing Then
@@ -1645,7 +1645,7 @@ Retry:
             End If
             End If
             Dim Type = TypeIndexes.MaxBy(Function(kv) kv.Item2).FolderName.TrimStart("\"c)
             Dim Type = TypeIndexes.MaxBy(Function(kv) kv.Item2).FolderName.TrimStart("\"c)
             '根据类别进行查找
             '根据类别进行查找
-            Static Sizes As New SafeDictionary(Of String, Long)
+            Static Sizes As New ConcurrentDictionary(Of String, Long)
             Select Case Type
             Select Case Type
                 Case "assets\", "libraries\"
                 Case "assets\", "libraries\"
                     'assets/libraries:查找 MC 文件夹下的相同路径
                     'assets/libraries:查找 MC 文件夹下的相同路径
@@ -1659,8 +1659,8 @@ Retry:
                         For Each Candidate In Directory.GetFiles(VersionFolder,
                         For Each Candidate In Directory.GetFiles(VersionFolder,
                             "*." & GetFileNameFromPath(File.LocalPath).AfterLast(".").Lower, SearchOption.TopDirectoryOnly)
                             "*." & GetFileNameFromPath(File.LocalPath).AfterLast(".").Lower, SearchOption.TopDirectoryOnly)
                             '快速进行大小校验
                             '快速进行大小校验
-                            If Not Sizes.ContainsKey(Candidate) Then Sizes(Candidate) = New FileInfo(Candidate).Length
-                            If File.Check.ActualSize <> Sizes(Candidate) Then Continue For
+                            Dim Length = Sizes.GetOrAdd(Candidate, Function(c) New FileInfo(c).Length)
+                            If File.Check.ActualSize <> Length Then Continue For
                             'Hash 校验
                             'Hash 校验
                             If File.Check.Check(Candidate) Is Nothing Then Return Candidate
                             If File.Check.Check(Candidate) Is Nothing Then Return Candidate
                         Next
                         Next
@@ -1673,8 +1673,8 @@ Retry:
                         If Not Directory.Exists(TargetFolder) Then Continue For
                         If Not Directory.Exists(TargetFolder) Then Continue For
                         For Each Candidate In Directory.GetFiles(TargetFolder)
                         For Each Candidate In Directory.GetFiles(TargetFolder)
                             '快速进行大小校验
                             '快速进行大小校验
-                            If Not Sizes.ContainsKey(Candidate) Then Sizes(Candidate) = New FileInfo(Candidate).Length
-                            If File.Check.ActualSize <> Sizes(Candidate) Then Continue For
+                            Dim Length = Sizes.GetOrAdd(Candidate, Function(c) New FileInfo(c).Length)
+                            If File.Check.ActualSize <> Length Then Continue For
                             'Hash 校验
                             'Hash 校验
                             If File.Check.Check(Candidate) Is Nothing Then Return Candidate
                             If File.Check.Check(Candidate) Is Nothing Then Return Candidate
                         Next
                         Next
@@ -1767,12 +1767,12 @@ Retry:
         ''' <summary>
         ''' <summary>
         ''' 需要下载的文件。为“本地地址 - 文件对象”键值对。
         ''' 需要下载的文件。为“本地地址 - 文件对象”键值对。
         ''' </summary>
         ''' </summary>
-        Public AllFiles As New SafeDictionary(Of String, NetFile)
+        Public AllFiles As New ConcurrentDictionary(Of String, NetFile)
 
 
         ''' <summary>
         ''' <summary>
         ''' 当前的所有下载任务。
         ''' 当前的所有下载任务。
         ''' </summary>
         ''' </summary>
-        Public Tasks As New SafeList(Of LoaderDownload)
+        Public Tasks As New ConcurrentList(Of LoaderDownload)
 
 
         ''' <summary>
         ''' <summary>
         ''' 已下载完成的大小。
         ''' 已下载完成的大小。
@@ -1823,7 +1823,7 @@ Retry:
                 If SpeedLast.Count >= 10 Then Limit = SpeedLast.Take(10).Average * 0.85 '取近 1 秒的平均速度的 85%
                 If SpeedLast.Count >= 10 Then Limit = SpeedLast.Take(10).Average * 0.85 '取近 1 秒的平均速度的 85%
                 If Limit > NetTaskSpeedLimitLow Then
                 If Limit > NetTaskSpeedLimitLow Then
                     NetTaskSpeedLimitLow = Limit
                     NetTaskSpeedLimitLow = Limit
-                    Log("[Download] " & "速度下限已提升到 " & GetString(Limit))
+                    Log("[Download] " & "速度下限已提升到 " & FormatFileSize(Limit))
                 End If
                 End If
 #End Region
 #End Region
 #Region "刷新下载任务属性"
 #Region "刷新下载任务属性"
@@ -1850,14 +1850,10 @@ Retry:
                     While True
                     While True
                         Thread.Sleep(20)
                         Thread.Sleep(20)
                         '获取文件列表
                         '获取文件列表
-                        Dim AllFiles As List(Of NetFile)
-                        SyncLock Me.AllFiles.SyncRoot
-                            If Id = 0 AndAlso FileRemain = 0 AndAlso Me.AllFiles.Any() Then Me.AllFiles.Clear() '若已完成,则清空
-                            AllFiles = Me.AllFiles.Values.ToList()
-                        End SyncLock
                         Dim WaitingFiles As New List(Of NetFile)
                         Dim WaitingFiles As New List(Of NetFile)
                         Dim OngoingFiles As New List(Of NetFile)
                         Dim OngoingFiles As New List(Of NetFile)
-                        For Each File As NetFile In AllFiles
+                        If Id = 0 AndAlso FileRemain = 0 Then AllFiles.Clear() '若已完成,则清空
+                        For Each File As NetFile In AllFiles.Values
                             If File.Uuid Mod 2 = Id Then Continue For
                             If File.Uuid Mod 2 = Id Then Continue For
                             If File.State = NetState.WaitingToDownload Then
                             If File.State = NetState.WaitingToDownload Then
                                 WaitingFiles.Add(File)
                                 WaitingFiles.Add(File)
@@ -1893,7 +1889,7 @@ Retry:
                         Next
                         Next
                     End While
                     End While
                 Catch ex As Exception
                 Catch ex As Exception
-                    Log(ex, $"下载管理启动线程 {Id} 出错", LogLevel.Critical)
+                    Log(ex, $"下载管理启动线程 {Id} 出错", NotifyLevel.Critical)
                 End Try
                 End Try
             End Sub
             End Sub
             RunInNewThread(Sub() ThreadStarter(0), "NetManager ThreadStarter 0")
             RunInNewThread(Sub() ThreadStarter(0), "NetManager ThreadStarter 0")
@@ -1917,7 +1913,7 @@ Retry:
                         End If
                         End If
                     End While
                     End While
                 Catch ex As Exception
                 Catch ex As Exception
-                    Log(ex, "下载管理刷新线程出错", LogLevel.Critical)
+                    Log(ex, "下载管理刷新线程出错", NotifyLevel.Critical)
                 End Try
                 End Try
             End Sub, "NetManager StatRefresher")
             End Sub, "NetManager StatRefresher")
         End Sub
         End Sub
@@ -1946,10 +1942,11 @@ Retry:
             Dim NewFiles As New List(Of NetFile)
             Dim NewFiles As New List(Of NetFile)
             For i = 0 To Task.Files.Count - 1
             For i = 0 To Task.Files.Count - 1
                 Dim File As NetFile = Task.Files(i)
                 Dim File As NetFile = Task.Files(i)
-                If AllFiles.ContainsKey(File.LocalPath) AndAlso AllFiles(File.LocalPath).State < NetState.Finished Then
+                Dim OngoingFile As NetFile = Nothing
+                If AllFiles.TryGetValue(File.LocalPath, OngoingFile) AndAlso OngoingFile.State < NetState.Finished Then
                     '该文件正在被另一个任务下载
                     '该文件正在被另一个任务下载
                     If ModeDebug Then Log($"[Download] {File.LocalName}:无需开始,该文件已在下载中,目标 {File.LocalPath}")
                     If ModeDebug Then Log($"[Download] {File.LocalName}:无需开始,该文件已在下载中,目标 {File.LocalPath}")
-                    File = AllFiles(File.LocalPath) '将列表中的文件替换成下载中的文件,即两个任务指向同一个文件;抛弃现在的 File 对象
+                    File = OngoingFile '将列表中的文件替换成下载中的文件,即两个任务指向同一个文件;抛弃现在的 File 对象
                     Task.Files(i) = File
                     Task.Files(i) = File
                 Else
                 Else
                     '常规情况(该文件可能已在其他任务中下载完成,但这并不影响)
                     '常规情况(该文件可能已在其他任务中下载完成,但这并不影响)
@@ -2053,7 +2050,7 @@ Retry:
         Return {
         Return {
             "(408)", "超时", "timeout", "网络请求失败", "连接尝试失败", "远程主机强迫关闭了", "远程方已关闭传输流", "未能解析此远程名称",
             "(408)", "超时", "timeout", "网络请求失败", "连接尝试失败", "远程主机强迫关闭了", "远程方已关闭传输流", "未能解析此远程名称",
             "由于目标计算机积极拒绝", "基础连接已经关闭"
             "由于目标计算机积极拒绝", "基础连接已经关闭"
-        }.Any(Function(k) Detail.ContainsF(k, True))
+        }.Any(Function(k) Detail.ContainsIgnoreCase(k))
     End Function
     End Function
 
 
     ''' <summary>
     ''' <summary>
@@ -2065,7 +2062,7 @@ Retry:
             Dim Request As HttpWebRequest = TryCast(Sender, HttpWebRequest)
             Dim Request As HttpWebRequest = TryCast(Sender, HttpWebRequest)
             If Failure = Net.Security.SslPolicyErrors.None Then Return True '已通过验证
             If Failure = Net.Security.SslPolicyErrors.None Then Return True '已通过验证
             '基于 #3018 和 #5879,只在访问正版登录 API 时跳过证书验证
             '基于 #3018 和 #5879,只在访问正版登录 API 时跳过证书验证
-            Log($"[System] 未通过 SSL 证书验证({Failure}),提供的证书为 {Certificate?.Subject},URL:{Request?.Address}", LogLevel.Debug)
+            Log($"[System] 未通过 SSL 证书验证({Failure}),提供的证书为 {Certificate?.Subject},URL:{Request?.Address}", NotifyLevel.DebugModeOnly)
             If Request Is Nothing Then
             If Request Is Nothing Then
                 Return Not Enabled
                 Return Not Enabled
             ElseIf Request.Address.Host.Contains("xboxlive") OrElse Request.Address.Host.Contains("minecraftservices") Then
             ElseIf Request.Address.Host.Contains("xboxlive") OrElse Request.Address.Host.Contains("minecraftservices") Then

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

@@ -244,18 +244,18 @@ End Class
 ''' </summary>
 ''' </summary>
 Public Class ValidateFolderName
 Public Class ValidateFolderName
     Inherits Validate
     Inherits Validate
-    Public Property Path As String
+    Public Property Folder As String
     Public Property UseMinecraftCharCheck As Boolean = True
     Public Property UseMinecraftCharCheck As Boolean = True
     Public Property IgnoreCase As Boolean = True
     Public Property IgnoreCase As Boolean = True
     Private ReadOnly PathIgnore As IEnumerable(Of DirectoryInfo)
     Private ReadOnly PathIgnore As IEnumerable(Of DirectoryInfo)
     Public Sub New()
     Public Sub New()
     End Sub
     End Sub
-    Public Sub New(Path As String, Optional UseMinecraftCharCheck As Boolean = True, Optional IgnoreCase As Boolean = True)
-        Me.Path = Path
+    Public Sub New(Folder As String, Optional UseMinecraftCharCheck As Boolean = True, Optional IgnoreCase As Boolean = True)
+        Me.Folder = Folder
         Me.IgnoreCase = IgnoreCase
         Me.IgnoreCase = IgnoreCase
         Me.UseMinecraftCharCheck = UseMinecraftCharCheck
         Me.UseMinecraftCharCheck = UseMinecraftCharCheck
         On Error Resume Next
         On Error Resume Next
-        PathIgnore = New DirectoryInfo(Path).EnumerateDirectories
+        PathIgnore = New DirectoryInfo(Folder).EnumerateDirectories
     End Sub
     End Sub
     Public Overrides Function Validate(Str As String) As String
     Public Overrides Function Validate(Str As String) As String
         Try
         Try
@@ -271,7 +271,7 @@ Public Class ValidateFolderName
             '检查尾部小数点
             '检查尾部小数点
             If Str.EndsWithF(".") Then Return "文件夹名不能以小数点结尾!"
             If Str.EndsWithF(".") Then Return "文件夹名不能以小数点结尾!"
             '检查特殊字符
             '检查特殊字符
-            Dim CharactCheck As String = New ValidateExcept(IO.Path.GetInvalidFileNameChars() & If(UseMinecraftCharCheck, "!;", ""), "文件夹名不可包含 % 字符!").Validate(Str)
+            Dim CharactCheck As String = New ValidateExcept(Path.GetInvalidFileNameChars() & If(UseMinecraftCharCheck, "!;", ""), "文件夹名不可包含 % 字符!").Validate(Str)
             If CharactCheck <> "" Then Return CharactCheck
             If CharactCheck <> "" Then Return CharactCheck
             '检查特殊字符串
             '检查特殊字符串
             Dim InvalidStrCheck As String = New ValidateExceptSame({"CON", "PRN", "AUX", "CLOCK$", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"}, "文件夹名不可为 %!", True).Validate(Str)
             Dim InvalidStrCheck As String = New ValidateExceptSame({"CON", "PRN", "AUX", "CLOCK$", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"}, "文件夹名不可为 %!", True).Validate(Str)
@@ -326,7 +326,7 @@ Public Class ValidateFileName
             '检查尾部小数点
             '检查尾部小数点
             If Str.EndsWithF(".") Then Return "文件名不能以小数点结尾!"
             If Str.EndsWithF(".") Then Return "文件名不能以小数点结尾!"
             '检查特殊字符
             '检查特殊字符
-            Dim CharactCheck As String = New ValidateExcept(IO.Path.GetInvalidFileNameChars() & If(UseMinecraftCharCheck, "!;", ""), "文件名不可包含 % 字符!").Validate(Str)
+            Dim CharactCheck As String = New ValidateExcept(Path.GetInvalidFileNameChars() & If(UseMinecraftCharCheck, "!;", ""), "文件名不可包含 % 字符!").Validate(Str)
             If CharactCheck <> "" Then Return CharactCheck
             If CharactCheck <> "" Then Return CharactCheck
             '检查特殊字符串
             '检查特殊字符串
             Dim InvalidStrCheck As String = New ValidateExceptSame({"CON", "PRN", "AUX", "CLOCK$", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"}, "文件名不可为 %!", True).Validate(Str)
             Dim InvalidStrCheck As String = New ValidateExceptSame({"CON", "PRN", "AUX", "CLOCK$", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"}, "文件名不可为 %!", True).Validate(Str)
@@ -388,7 +388,7 @@ Fin:
             Dim SubLengthCheck As String = New ValidateNullOrWhiteSpace().Validate(SubStr)
             Dim SubLengthCheck As String = New ValidateNullOrWhiteSpace().Validate(SubStr)
             If Not SubLengthCheck = "" Then Return "文件夹路径存在错误!"
             If Not SubLengthCheck = "" Then Return "文件夹路径存在错误!"
             '检查特殊字符
             '检查特殊字符
-            Dim CharactCheck As String = New ValidateExcept(IO.Path.GetInvalidFileNameChars() & If(UseMinecraftCharCheck, "!;", ""), "路径中存在无效字符!").Validate(SubStr)
+            Dim CharactCheck As String = New ValidateExcept(Path.GetInvalidFileNameChars() & If(UseMinecraftCharCheck, "!;", ""), "路径中存在无效字符!").Validate(SubStr)
             If Not CharactCheck = "" Then Return CharactCheck
             If Not CharactCheck = "" Then Return CharactCheck
             '检查头部空格
             '检查头部空格
             If SubStr.StartsWithF(" ") Then Return "文件夹名不能以空格开头!"
             If SubStr.StartsWithF(" ") Then Return "文件夹名不能以空格开头!"

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

@@ -97,7 +97,7 @@ Public Class MyBitmap
                     Throw New Exception($"加载 MyBitmap 意外失败({FilePathOrResourceName})", ex)
                     Throw New Exception($"加载 MyBitmap 意外失败({FilePathOrResourceName})", ex)
                 End If
                 End If
             Else
             Else
-                Log(ex, $"指定类型有误的 MyBitmap 加载({FilePathOrResourceName})", LogLevel.Developer)
+                Log(ex, $"指定类型有误的 MyBitmap 加载({FilePathOrResourceName})", NotifyLevel.DevelopOnly)
                 Exit Try
                 Exit Try
             End If
             End If
         End Try
         End Try

+ 78 - 0
Plain Craft Launcher 2/Modules/Constants.vb

@@ -0,0 +1,78 @@
+''' <summary>
+''' 社区资源的类型。
+''' </summary>
+<Flags> Public Enum ResourceTypes
+    ''' <summary> 
+    ''' Mod。
+    ''' </summary>
+    [Mod] = 1
+    ''' <summary>
+    ''' 整合包。
+    ''' </summary>
+    ModPack = 2
+    ''' <summary>
+    ''' 资源包。
+    ''' </summary>
+    ResourcePack = 4
+    ''' <summary>
+    ''' 光影包。
+    ''' </summary>
+    Shader = 8
+    ''' <summary>
+    ''' 数据包。
+    ''' </summary>
+    DataPack = 16
+    ''' <summary>
+    ''' 服务端插件。
+    ''' </summary>
+    Plugin = 32
+    ''' <summary>
+    ''' 同时包含数据包以及 Mod。
+    ''' </summary>
+    ModOrDataPack = [Mod] Or DataPack
+    ''' <summary>
+    ''' 允许任意种类,或种类未知。
+    ''' </summary>
+    Any = [Mod] Or ModPack Or ResourcePack Or Shader Or DataPack Or Plugin
+End Enum
+
+''' <summary>
+''' 社区资源的来源平台。
+''' </summary>
+<Flags> Public Enum ResourcePlatforms
+    CurseForge = 1
+    Modrinth = 2
+    Any = CurseForge Or Modrinth
+End Enum
+
+''' <summary>
+''' Mod 加载器的类型。
+''' </summary>
+Public Enum ModLoaderTypes
+    'https://docs.curseforge.com/rest-api/#tocS_ModLoaderType
+    Any = 0
+    Forge = 1
+    LiteLoader = 3
+    Fabric = 4
+    Quilt = 5
+    NeoForge = 6
+End Enum
+
+''' <summary>
+''' 在爱发电中的赞助等级。
+''' </summary>
+Public Enum DonationRanks
+    None = 0
+    Rank6 = 6
+    Rank12 = 12
+    Rank23 = 23
+    Rank54 = 54
+    Rank98 = 98
+End Enum
+
+Public Module Constants
+    ''' <summary>
+    ''' 土豆码版本号。
+    ''' </summary>
+    Public Const POTATO_VERSION As Char = "1"c
+End Module

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

@@ -1,1776 +0,0 @@
-Imports System.IO.Compression
-
-Public Module ModComp
-
-    <Flags> Public Enum CompType
-        ''' <summary> 
-        ''' Mod。
-        ''' </summary>
-        [Mod] = 1
-        ''' <summary>
-        ''' 整合包。
-        ''' </summary>
-        ModPack = 2
-        ''' <summary>
-        ''' 资源包。
-        ''' </summary>
-        ResourcePack = 4
-        ''' <summary>
-        ''' 光影包。
-        ''' </summary>
-        Shader = 8
-        ''' <summary>
-        ''' 数据包。
-        ''' </summary>
-        DataPack = 16
-        ''' <summary>
-        ''' 服务端插件。
-        ''' </summary>
-        Plugin = 32
-        ''' <summary>
-        ''' 同时包含数据包以及 Mod。
-        ''' </summary>
-        ModOrDataPack = [Mod] Or DataPack
-        ''' <summary>
-        ''' 允许任意种类,或种类未知。
-        ''' </summary>
-        Any = [Mod] Or ModPack Or ResourcePack Or Shader Or DataPack Or Plugin
-    End Enum
-    Public Enum CompModLoaderType
-        'https://docs.curseforge.com/?http#tocS_ModLoaderType
-        Any = 0
-        Forge = 1
-        LiteLoader = 3
-        Fabric = 4
-        Quilt = 5
-        NeoForge = 6
-    End Enum
-    <Flags> Public Enum CompSourceType
-        CurseForge = 1
-        Modrinth = 2
-        Any = CurseForge Or Modrinth
-    End Enum
-
-#Region "CompDatabase | Mod 数据库"
-
-    Private ReadOnly Property CompDatabase As List(Of CompDatabaseEntry)
-        Get
-            Static _CompDatabase As List(Of CompDatabaseEntry) = Nothing
-            If _CompDatabase IsNot Nothing Then Return _CompDatabase
-            '初始化数据库
-            _CompDatabase = New List(Of CompDatabaseEntry)
-            Dim i As Integer = 0
-            Using Archive As New ZipArchive(New MemoryStream(GetResources("ModData")), ZipArchiveMode.Read)
-                For Each Line In ReadFile(Archive.GetEntry("moddata.txt").Open(), Encoding.UTF8).Replace(vbCrLf, vbLf).Replace(vbCr, "").Split(vbLf)
-                    i += 1
-                    If Line = "" Then Continue For
-                    For Each EntryData As String In Line.Split("¨")
-                        Dim Entry = New CompDatabaseEntry
-                        Dim Splited = EntryData.Split("|")
-                        Entry.Popularity = Val(Splited(0))
-                        If Splited(1).StartsWithF("@") Then
-                            Entry.CurseForgeSlug = Nothing
-                            Entry.ModrinthSlug = Splited(1).Replace("@", "")
-                        ElseIf Splited(1).EndsWithF("@") Then
-                            Entry.CurseForgeSlug = Splited(1).TrimEnd("@")
-                            Entry.ModrinthSlug = Entry.CurseForgeSlug
-                        ElseIf Splited(1).Contains("@") Then
-                            Entry.CurseForgeSlug = Splited(1).Split("@")(0)
-                            Entry.ModrinthSlug = Splited(1).Split("@")(1)
-                        Else
-                            Entry.CurseForgeSlug = Splited(1)
-                            Entry.ModrinthSlug = Nothing
-                        End If
-                        Entry.WikiId = i
-                        If Splited.Count >= 3 Then
-                            Entry.ChineseName = Splited.Last
-                            If Entry.ChineseName.Contains("*") Then '处理 *
-                                Entry.ChineseName = Entry.ChineseName.Replace("*",
-                                    $" ({If(Entry.CurseForgeSlug, Entry.ModrinthSlug).Replace("-", " ").Capitalize})")
-                            End If
-                        End If
-                        _CompDatabase.Add(Entry)
-                    Next
-                Next
-            End Using
-            Return _CompDatabase
-        End Get
-    End Property
-
-    Private Class CompDatabaseEntry
-        ''' <summary>
-        ''' McMod 的对应 ID。
-        ''' </summary>
-        Public WikiId As Integer
-        ''' <summary>
-        ''' 中文译名。空字符串代表没有翻译。
-        ''' </summary>
-        Public ChineseName As String = ""
-        ''' <summary>
-        ''' CurseForge Slug(例如 advanced-solar-panels)。
-        ''' </summary>
-        Public CurseForgeSlug As String = Nothing
-        ''' <summary>
-        ''' Modrinth Slug(例如 advanced-solar-panels)。
-        ''' </summary>
-        Public ModrinthSlug As String = Nothing
-        ''' <summary>
-        ''' MC 百科的浏览量逆序排行,1 代表浏览量最低。
-        ''' </summary>
-        Public Popularity As Integer
-
-        Public Overrides Function ToString() As String
-            Return If(CurseForgeSlug, "") & "&" & If(ModrinthSlug, "") & "|" & WikiId & "|" & ChineseName & ", Rank " & Popularity
-        End Function
-    End Class
-
-#End Region
-
-#Region "CompProject | 工程信息"
-
-    '类定义
-
-    Public Class CompProject
-
-        '源信息
-
-        ''' <summary>
-        ''' 该工程信息来自 CurseForge 还是 Modrinth。
-        ''' </summary>
-        Public ReadOnly FromCurseForge As Boolean
-        ''' <summary>
-        ''' 工程中包含的文件种类。
-        ''' 若为 Modrinth 工程,可能为 Mod 或 数据包。
-        ''' </summary>
-        Public ReadOnly Types As CompType
-        ''' <summary>
-        ''' 工程的短名。例如 technical-enchant。
-        ''' </summary>
-        Public ReadOnly Slug As String
-        ''' <summary>
-        ''' CurseForge 工程的数字 ID。Modrinth 工程的乱码 ID。
-        ''' </summary>
-        Public ReadOnly Id As String
-        ''' <summary>
-        ''' CurseForge 文件列表的数字 ID。Modrinth 工程的此项无效。
-        ''' </summary>
-        Public ReadOnly CurseForgeFileIds As List(Of Integer)
-
-        '描述性信息
-
-        ''' <summary>
-        ''' 原始的英文名称。
-        ''' </summary>
-        Public ReadOnly RawName As String
-        ''' <summary>
-        ''' 英文描述。
-        ''' </summary>
-        Public ReadOnly Description As String
-        ''' <summary>
-        ''' 来源网站的工程页面网址。确保格式一定标准。
-        ''' CurseForge:https://www.curseforge.com/minecraft/mc-mods/jei
-        ''' Modrinth:https://modrinth.com/mod/technical-enchant
-        ''' </summary>
-        Public ReadOnly Website As String
-        ''' <summary>
-        ''' 最后一次更新的时间。
-        ''' </summary>
-        Public ReadOnly LastUpdate As Date
-        ''' <summary>
-        ''' 下载量计数。注意,该计数仅为一个来源,无法反应两边加起来的下载量!
-        ''' </summary>
-        Public ReadOnly DownloadCount As Integer
-        ''' <summary>
-        ''' 支持的 Mod 加载器列表。可能为空。
-        ''' </summary>
-        Public ReadOnly ModLoaders As List(Of CompModLoaderType)
-        ''' <summary>
-        ''' 描述性标签的内容。已转换为中文。
-        ''' </summary>
-        Public ReadOnly Tags As List(Of String)
-        ''' <summary>
-        ''' Logo 图片的下载地址。
-        ''' 若为 Nothing 则没有,保证不为空字符串。
-        ''' </summary>
-        Public LogoUrl As String = Nothing
-        ''' <summary>
-        ''' 支持的 Drop 编号,从高到低排序,不为 Nothing。
-        ''' 例如:261(26.1.x)、180(1.18.x)。
-        ''' </summary>
-        Public ReadOnly Drops As List(Of Integer)
-        ''' <summary>
-        ''' Modrinth API 返回的原始版本列表。
-        ''' 仅用于 Modrinth 工程的二次筛选,不会被缓存,不会被预处理。
-        ''' 若非从 Modrinth 获取则为 Nothing。
-        ''' </summary>
-        Public ReadOnly UnsafeGameVersions As List(Of String)
-
-        '数据库信息
-
-        Private LoadedDatabase As Boolean = False
-        Private _DatabaseEntry As CompDatabaseEntry = Nothing
-        ''' <summary>
-        ''' 关联的数据库条目。若为 Nothing 则没有。
-        ''' </summary>
-        Private Property DatabaseEntry As CompDatabaseEntry
-            Get
-                If Not LoadedDatabase Then
-                    LoadedDatabase = True
-                    If Types.HasFlag(CompType.Mod) OrElse Types.HasFlag(CompType.DataPack) Then
-                        _DatabaseEntry = CompDatabase.FirstOrDefault(Function(c) If(FromCurseForge, c.CurseForgeSlug, c.ModrinthSlug) = Slug)
-                    End If
-                End If
-                Return _DatabaseEntry
-            End Get
-            Set(value As CompDatabaseEntry)
-                LoadedDatabase = True
-                _DatabaseEntry = value
-            End Set
-        End Property
-        ''' <summary>
-        ''' MC 百科的页面 ID。若为 0 则没有。
-        ''' </summary>
-        Public ReadOnly Property WikiId As Integer
-            Get
-                Return If(DatabaseEntry Is Nothing, 0, DatabaseEntry.WikiId)
-            End Get
-        End Property
-        ''' <summary>
-        ''' 翻译后的中文名。若数据库没有则等同于 RawName。
-        ''' </summary>
-        Public ReadOnly Property TranslatedName As String
-            Get
-                Return If(DatabaseEntry Is Nothing OrElse DatabaseEntry.ChineseName = "", RawName, DatabaseEntry.ChineseName)
-            End Get
-        End Property
-
-        '实例化
-
-        ''' <summary>
-        ''' 从工程 Json 中初始化实例。若出错会抛出异常。
-        ''' </summary>
-        Public Sub New(Data As JObject)
-            If Data.ContainsKey("Tags") Then
-#Region "CompJson"
-                FromCurseForge = Data("DataSource") = "CurseForge"
-                Types = Data("Types").ToObject(Of Integer)
-                Slug = Data("Slug")
-                Id = Data("Id")
-                If Data.ContainsKey("CurseForgeFileIds") Then CurseForgeFileIds = CType(Data("CurseForgeFileIds"), JArray).Select(Function(t) t.ToObject(Of Integer)).ToList
-                RawName = Data("RawName")
-                Description = Data("Description")
-                Website = Data("Website")
-                LastUpdate = Data("LastUpdate")
-                DownloadCount = Data("DownloadCount")
-                If Data.ContainsKey("ModLoaders") Then
-                    ModLoaders = CType(Data("ModLoaders"), JArray).Select(Function(t) CType(t.ToObject(Of Integer), CompModLoaderType)).ToList
-                Else
-                    ModLoaders = New List(Of CompModLoaderType)
-                End If
-                Tags = CType(Data("Tags"), JArray).Select(Function(t) t.ToString).ToList
-                If Data.ContainsKey("LogoUrl") Then LogoUrl = Data("LogoUrl")
-                If Data.ContainsKey("Drops") Then
-                    Drops = CType(Data("Drops"), JArray).Select(Function(t) t.ToObject(Of Integer)).ToList
-                Else
-                    Drops = New List(Of Integer)
-                End If
-#End Region
-            Else
-                FromCurseForge = Data.ContainsKey("summary")
-                If FromCurseForge Then
-#Region "CurseForge"
-                    '简单信息
-                    Id = Data("id")
-                    Slug = Data("slug")
-                    RawName = Data("name")
-                    Description = Data("summary")
-                    Website = Data("links")("websiteUrl").ToString.TrimEnd("/")
-                    LastUpdate = Data("dateReleased") '#1194
-                    DownloadCount = Data("downloadCount")
-                    If Data("logo").Count > 0 Then
-                        If Data("logo")("thumbnailUrl") Is Nothing OrElse Data("logo")("thumbnailUrl") = "" Then
-                            LogoUrl = Data("logo")("url")
-                        Else
-                            LogoUrl = Data("logo")("thumbnailUrl")
-                        End If
-                    End If
-                    If LogoUrl = "" Then LogoUrl = Nothing
-                    'Type
-                    If Website.Contains("/mc-mods/") OrElse Website.Contains("/mod/") Then
-                        Types = CompType.Mod
-                    ElseIf Website.Contains("/modpacks/") Then
-                        Types = CompType.ModPack
-                    ElseIf Website.Contains("/resourcepacks/") Then
-                        Types = CompType.ResourcePack
-                    ElseIf Website.Contains("/texture-packs/") Then
-                        Types = CompType.ResourcePack
-                    ElseIf Website.Contains("/shaders/") Then
-                        Types = CompType.Shader
-                    Else
-                        Types = CompType.DataPack
-                    End If
-                    'FileIndexes / VanillaMajorVersions / ModLoaders
-                    ModLoaders = New List(Of CompModLoaderType)
-                    Dim Files As New List(Of KeyValuePair(Of Integer, List(Of String))) 'FileId, GameVersions
-                    For Each File In If(Data("latestFiles"), New JArray)
-                        Dim NewFile As New CompFile(File, Types)
-                        If Not NewFile.Available Then Continue For
-                        ModLoaders.AddRange(NewFile.ModLoaders)
-                        Dim GameVersions = File("gameVersions").ToObject(Of List(Of String))
-                        If Not GameVersions.Any(Function(v) McVersion.IsFormatFit(v)) Then Continue For
-                        Files.Add(New KeyValuePair(Of Integer, List(Of String))(File("id"), GameVersions))
-                    Next
-                    For Each File In If(Data("latestFilesIndexes"), New JArray) '这俩玩意儿包含的文件不一样,见 #3599
-                        If Not McVersion.IsFormatFit(File("gameVersion")) Then Continue For
-                        Files.Add(New KeyValuePair(Of Integer, List(Of String))(File("fileId"), {File("gameVersion").ToString}.ToList))
-                    Next
-                    CurseForgeFileIds = Files.Select(Function(f) f.Key).Distinct.ToList
-                    Drops = Files.SelectMany(Function(f) f.Value).
-                        Select(Function(v) McVersion.VersionToDrop(v)).Where(Function(v) v <> 209).Distinct.OrderByDescending(Function(v) v).ToList
-                    ModLoaders = ModLoaders.Distinct.OrderBy(Of Integer)(Function(t) t).ToList
-                    'Tags
-                    Tags = New List(Of String)
-                    For Each Category In If(Data("categories"), New JArray). '镜像源 API 可能丢失此字段 (4267#issuecomment-2254590831)
-                        Select(Of Integer)(Function(t) t("id")).Distinct.OrderByDescending(Function(c) c)
-                        Select Case Category
-                            'Mod
-                            Case 406 : Tags.Add("世界元素")
-                            Case 407 : Tags.Add("生物群系")
-                            Case 410 : Tags.Add("维度")
-                            Case 408 : Tags.Add("矿物/资源")
-                            Case 409 : Tags.Add("天然结构")
-                            Case 412 : Tags.Add("科技")
-                            Case 415 : Tags.Add("管道/物流")
-                            Case 4843 : Tags.Add("自动化")
-                            Case 417 : Tags.Add("能源")
-                            Case 4558 : Tags.Add("红石")
-                            Case 436 : Tags.Add("食物/烹饪")
-                            Case 416 : Tags.Add("农业")
-                            Case 414 : Tags.Add("运输")
-                            Case 420 : Tags.Add("仓储")
-                            Case 419 : Tags.Add("魔法")
-                            Case 422 : Tags.Add("冒险")
-                            Case 424 : Tags.Add("装饰")
-                            Case 411 : Tags.Add("生物")
-                            Case 434 : Tags.Add("装备")
-                            Case 6814 : Tags.Add("性能优化")
-                            Case 9026 : Tags.Add("创造模式")
-                            Case 423 : Tags.Add("信息显示")
-                            Case 435 : Tags.Add("服务器")
-                            Case 5191 : Tags.Add("改良")
-                            Case 421 : Tags.Add("支持库")
-                            '整合包
-                            Case 4484 : Tags.Add("多人")
-                            Case 4479 : Tags.Add("硬核")
-                            Case 4483 : Tags.Add("战斗")
-                            Case 4478 : Tags.Add("任务")
-                            Case 4472 : Tags.Add("科技")
-                            Case 4473 : Tags.Add("魔法")
-                            Case 4475 : Tags.Add("冒险")
-                            Case 4476 : Tags.Add("探索")
-                            Case 4477 : Tags.Add("小游戏")
-                            Case 4474 : Tags.Add("科幻")
-                            Case 4736 : Tags.Add("空岛")
-                            Case 5128 : Tags.Add("原版改良")
-                            Case 4487 : Tags.Add("FTB")
-                            Case 4480 : Tags.Add("基于地图")
-                            Case 4481 : Tags.Add("轻量")
-                            Case 4482 : Tags.Add("大型")
-                            '资源包
-                            Case 403 : Tags.Add("原版风")
-                            Case 400 : Tags.Add("写实风")
-                            Case 401 : Tags.Add("现代风")
-                            Case 402 : Tags.Add("中世纪")
-                            Case 399 : Tags.Add("蒸汽朋克")
-                            Case 5244 : Tags.Add("含字体")
-                            Case 404 : Tags.Add("动态效果")
-                            Case 4465 : Tags.Add("兼容 Mod")
-                            Case 393 : Tags.Add("16x")
-                            Case 394 : Tags.Add("32x")
-                            Case 395 : Tags.Add("64x")
-                            Case 396 : Tags.Add("128x")
-                            Case 397 : Tags.Add("256x")
-                            Case 398 : Tags.Add("超高清")
-                            Case 5193 : Tags.Add("数据包") '有这个 Tag 的项会从资源包请求中被移除
-                            '光影包
-                            Case 6553 : Tags.Add("写实风")
-                            Case 6554 : Tags.Add("幻想风")
-                            Case 6555 : Tags.Add("原版风")
-                            '数据包
-                            Case 6948 : Tags.Add("冒险")
-                            Case 6949 : Tags.Add("幻想")
-                            Case 6950 : Tags.Add("支持库")
-                            Case 6952 : Tags.Add("魔法")
-                            Case 6946 : Tags.Add("Mod 相关")
-                            Case 6951 : Tags.Add("科技")
-                            Case 6953 : Tags.Add("实用")
-                        End Select
-                    Next
-#End Region
-                Else
-#Region "Modrinth"
-                    '简单信息
-                    Id = If(Data("project_id"), Data("id")) '两个 API 会返回的 key 不一样
-                    Slug = Data("slug")
-                    RawName = Data("title")
-                    Description = Data("description")
-                    LastUpdate = If(Data("date_modified"), Data("updated"))
-                    DownloadCount = Data("downloads")
-                    LogoUrl = Data("icon_url")
-                    If LogoUrl = "" Then LogoUrl = Nothing
-                    Website = $"https://modrinth.com/{Data("project_type")}/{Slug}"
-                    'GameVersions
-                    '搜索结果的键为 versions,获取特定工程的键为 game_versions
-                    UnsafeGameVersions = If(CType(If(Data("game_versions"), Data("versions")), JArray), New JArray).Select(Function(v) v.ToString).Distinct.ToList
-                    Drops = UnsafeGameVersions.Select(Function(v) McVersion.VersionToDrop(v)).Where(Function(v) v <> 209).Distinct.OrderByDescending(Function(v) v).ToList
-                    'Type
-                    Select Case Data("project_type").ToString
-                        Case "modpack" : Types = CompType.ModPack
-                        Case "resourcepack" : Types = CompType.ResourcePack
-                        Case "shader" : Types = CompType.Shader
-                        Case Else : Types = CompType.Mod 'Modrinth 将数据包标为 Mod,但 categories 字段里有 datapack
-                    End Select
-                    'Tags & ModLoaders
-                    Tags = New List(Of String)
-                    ModLoaders = New List(Of CompModLoaderType)
-                    If Data.ContainsKey("loaders") Then
-                        For Each Category In Data("loaders").Select(Function(t) t.ToString)
-                            Select Case Category
-                                Case "forge" : ModLoaders.Add(CompModLoaderType.Forge)
-                                Case "fabric" : ModLoaders.Add(CompModLoaderType.Fabric)
-                                Case "quilt" : ModLoaders.Add(CompModLoaderType.Quilt)
-                                Case "neoforge" : ModLoaders.Add(CompModLoaderType.NeoForge)
-                            End Select
-                        Next
-                    End If
-                    For Each Category In Data("categories").Select(Function(t) t.ToString)
-                        Select Case Category
-                            '加载器
-                            Case "forge" : ModLoaders.Add(CompModLoaderType.Forge)
-                            Case "fabric" : ModLoaders.Add(CompModLoaderType.Fabric)
-                            Case "quilt" : ModLoaders.Add(CompModLoaderType.Quilt)
-                            Case "neoforge" : ModLoaders.Add(CompModLoaderType.NeoForge)
-                            Case "datapack" : Types = CompType.DataPack
-                            '共用
-                            Case "technology" : Tags.Add("科技")
-                            Case "magic" : Tags.Add("魔法")
-                            Case "adventure" : Tags.Add("冒险")
-                            Case "utility" : Tags.Add("实用")
-                            Case "optimization" : Tags.Add("性能优化")
-                            Case "vanilla-like" : Tags.Add("原版风")
-                            Case "realistic" : Tags.Add("写实风")
-                            'Mod/数据包
-                            Case "worldgen" : Tags.Add("世界元素")
-                            Case "food" : Tags.Add("食物/烹饪")
-                            Case "game-mechanics" : Tags.Add("游戏机制")
-                            Case "transportation" : Tags.Add("运输")
-                            Case "storage" : Tags.Add("仓储")
-                            Case "decoration" : If Types <> CompType.ResourcePack Then Tags.Add("装饰")
-                            Case "mobs" : If Types <> CompType.ResourcePack Then Tags.Add("生物")
-                            Case "equipment" : If Types <> CompType.ResourcePack Then Tags.Add("装备")
-                            Case "social" : Tags.Add("服务器")
-                            Case "library" : Tags.Add("支持库")
-                            '整合包
-                            Case "multiplayer" : Tags.Add("多人")
-                            Case "challenging" : Tags.Add("硬核")
-                            Case "combat" : Tags.Add("战斗")
-                            Case "quests" : Tags.Add("任务")
-                            Case "kitchen-sink" : Tags.Add("水槽包")
-                            Case "lightweight" : Tags.Add("轻量")
-                            '资源包
-                            Case "simplistic" : Tags.Add("简洁")
-                            Case "combat" : Tags.Add("战斗")
-                            Case "tweaks" : Tags.Add("改良")
-
-                            Case "8x-" : Tags.Add("极简")
-                            Case "16x" : Tags.Add("16x")
-                            Case "32x" : Tags.Add("32x")
-                            Case "48x" : Tags.Add("48x")
-                            Case "64x" : Tags.Add("64x")
-                            Case "128x" : Tags.Add("128x")
-                            Case "256x" : Tags.Add("256x")
-                            Case "512x+" : Tags.Add("超高清")
-
-                            Case "audio" : Tags.Add("含声音")
-                            Case "fonts" : Tags.Add("含字体")
-                            Case "models" : Tags.Add("含模型")
-                            Case "gui" : Tags.Add("含 UI")
-                            Case "locale" : Tags.Add("含语言")
-                            Case "core-shaders" : Tags.Add("核心着色器")
-                            Case "modded" : Tags.Add("兼容 Mod")
-                            '光影包
-                            Case "fantasy" : Tags.Add("幻想风")
-                            Case "semi-realistic" : Tags.Add("半写实风")
-                            Case "cartoon" : Tags.Add("卡通风")
-                            '暂时不添加性能负荷 Tag
-                            'Case "potato" : Tags.Add("极低")
-                            'Case "low" : Tags.Add("低")
-                            'Case "medium" : Tags.Add("中")
-                            'Case "high" : Tags.Add("高")
-                            Case "colored-lighting" : Tags.Add("彩色光照")
-                            Case "path-tracing" : Tags.Add("路径追踪")
-                            Case "pbr" : Tags.Add("PBR")
-                            Case "reflections" : Tags.Add("反射")
-
-                            Case "iris" : Tags.Add("Iris")
-                            Case "optifine" : Tags.Add("OptiFine")
-                            Case "vanilla" : Tags.Add("原版可用")
-                        End Select
-                    Next
-                    If Types = CompType.DataPack AndAlso ModLoaders.Any Then Types = CompType.DataPack Or CompType.Mod
-#End Region
-                End If
-                If Not Tags.Any() Then Tags.Add("其他")
-                Tags.Sort()
-                ModLoaders.Sort()
-            End If
-            '保存缓存
-            CompProjectCache(Id) = Me
-        End Sub
-        ''' <summary>
-        ''' 将当前实例转为可用于保存缓存的 Json。
-        ''' </summary>
-        Public Function ToJson() As JObject
-            Dim Json As New JObject
-            Json("DataSource") = If(FromCurseForge, "CurseForge", "Modrinth")
-            Json("Types") = CInt(Types)
-            Json("Slug") = Slug
-            Json("Id") = Id
-            If CurseForgeFileIds IsNot Nothing Then Json("CurseForgeFileIds") = New JArray(CurseForgeFileIds)
-            Json("RawName") = RawName
-            Json("Description") = Description
-            Json("Website") = Website
-            Json("LastUpdate") = LastUpdate
-            Json("DownloadCount") = DownloadCount
-            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 Drops.Any Then Json("Drops") = New JArray(Drops)
-            Json("CacheTime") = Date.Now '用于检查缓存时间
-            Return Json
-        End Function
-        ''' <summary>
-        ''' 将当前工程信息实例化为控件。
-        ''' </summary>
-        Public Function ToCompItem(ShowMcVersionDesc As Boolean, ShowLoaderDesc As Boolean) As MyVirtualizingElement(Of MyCompItem)
-            '获取版本描述
-            Dim GameVersionDescription As String
-            If Drops Is Nothing OrElse Not Drops.Any() Then
-                GameVersionDescription = "仅快照版本" '#5412
-            Else
-                Dim Segments As New List(Of String)
-                Dim IsOld As Boolean = False
-                For i = 0 To Drops.Count - 1 '版本号一定为降序
-                    '获取当前连续的版本号段
-                    Dim StartDrop As Integer = Drops(i), EndDrop As Integer = Drops(i)
-                    If StartDrop < 120 Then '如果支持新版本,则不显示 1.11-
-                        If Segments.Any() AndAlso Not IsOld Then
-                            Exit For
-                        Else
-                            IsOld = True
-                        End If
-                    End If
-                    For ii = i + 1 To Drops.Count - 1
-                        If AllDrops Is Nothing OrElse AllDrops.IndexOf(Drops(ii)) <> AllDrops.IndexOf(EndDrop) + 1 Then Exit For
-                        EndDrop = Drops(ii)
-                        i = ii
-                    Next
-                    '将版本号段转为描述文本
-                    Dim StartName = McVersion.DropToVersion(StartDrop)
-                    Dim EndName = McVersion.DropToVersion(EndDrop)
-                    If StartDrop = EndDrop Then
-                        Segments.Add(StartName)
-                    ElseIf AllDrops IsNot Nothing AndAlso StartDrop >= AllDrops.First Then
-                        If EndDrop < 120 Then
-                            Segments.Clear()
-                            Segments.Add("全版本")
-                            Exit For
-                        Else
-                            Segments.Add(EndName & "+")
-                        End If
-                    ElseIf EndDrop < 120 Then
-                        Segments.Add(StartName & "-")
-                        Exit For
-                    ElseIf AllDrops Is Nothing OrElse AllDrops.IndexOf(EndDrop) - AllDrops.IndexOf(StartDrop) = 1 Then
-                        Segments.Add(StartName & ", " & EndName)
-                    Else
-                        Segments.Add(StartName & "~" & EndName)
-                    End If
-                Next
-                GameVersionDescription = Segments.Join(", ")
-            End If
-            '获取 Mod 加载器描述
-            Dim ModLoaderDescriptionFull As String, ModLoaderDescriptionPart As String
-            Dim ModLoadersForDesc As New List(Of CompModLoaderType)(ModLoaders)
-            If Settings.Get("ToolDownloadIgnoreQuilt") Then ModLoadersForDesc.Remove(CompModLoaderType.Quilt)
-            Select Case ModLoadersForDesc.Count
-                Case 0
-                    If ModLoaders.IsSingle Then
-                        ModLoaderDescriptionFull = "仅 " & ModLoaders.Single.ToString
-                        ModLoaderDescriptionPart = ModLoaders.Single.ToString
-                    Else
-                        ModLoaderDescriptionFull = "未知"
-                        ModLoaderDescriptionPart = ""
-                    End If
-                Case 1
-                    ModLoaderDescriptionFull = "仅 " & ModLoadersForDesc.Single.ToString
-                    ModLoaderDescriptionPart = ModLoadersForDesc.Single.ToString
-                Case Else
-                    Dim NewestDrop As Integer = If(Drops.Any, Drops.First, 9999)
-                    If ModLoaders.Contains(CompModLoaderType.Forge) AndAlso
-                       (NewestDrop < 140 OrElse ModLoaders.Contains(CompModLoaderType.Fabric)) AndAlso
-                       (NewestDrop < 200 OrElse ModLoaders.Contains(CompModLoaderType.NeoForge)) AndAlso
-                       (NewestDrop < 140 OrElse ModLoaders.Contains(CompModLoaderType.Quilt) OrElse Settings.Get("ToolDownloadIgnoreQuilt")) Then
-                        ModLoaderDescriptionFull = "任意"
-                        ModLoaderDescriptionPart = ""
-                    Else
-                        ModLoaderDescriptionFull = ModLoadersForDesc.Join(" / ")
-                        ModLoaderDescriptionPart = ModLoadersForDesc.Join(" / ")
-                    End If
-            End Select
-            '实例化 UI
-            Return New MyVirtualizingElement(Of MyCompItem)(
-            Function()
-                Dim NewItem As New MyCompItem With {.Tag = Me}
-                ApplyLogoToMyImage(NewItem.PathLogo)
-                Dim Titles = GetControlTitle(True)
-                NewItem.Title = Titles.Title
-                If Titles.SubTitle = "" Then
-                    CType(NewItem.LabTitleRaw.Parent, StackPanel).Children.Remove(NewItem.LabTitleRaw)
-                Else
-                    NewItem.SubTitle = Titles.SubTitle
-                End If
-                NewItem.Tags = Tags
-                NewItem.Description = Description.Replace(vbCr, "").Replace(vbLf, "")
-                '下边栏
-                If Not ShowMcVersionDesc AndAlso Not ShowLoaderDesc Then
-                    '全部隐藏
-                    CType(NewItem.PathVersion.Parent, Grid).Children.Remove(NewItem.PathVersion)
-                    CType(NewItem.LabVersion.Parent, Grid).Children.Remove(NewItem.LabVersion)
-                    NewItem.ColumnVersion1.Width = New GridLength(0)
-                    NewItem.ColumnVersion2.MaxWidth = 0
-                    NewItem.ColumnVersion3.Width = New GridLength(0)
-                ElseIf ShowMcVersionDesc AndAlso ShowLoaderDesc Then
-                    '全部显示
-                    NewItem.LabVersion.Text = If(ModLoaderDescriptionPart = "", "", ModLoaderDescriptionPart & " ") & GameVersionDescription
-                ElseIf ShowMcVersionDesc Then
-                    '仅显示版本
-                    NewItem.LabVersion.Text = GameVersionDescription
-                Else
-                    '仅显示 Mod 加载器
-                    NewItem.LabVersion.Text = ModLoaderDescriptionFull
-                End If
-                NewItem.LabSource.Text = If(FromCurseForge, "CurseForge", "Modrinth")
-                NewItem.LabTime.Text = GetTimeSpanString(LastUpdate - Date.Now, True)
-                NewItem.LabDownload.Text =
-                    If(DownloadCount > 100000000, Math.Round(DownloadCount / 100000000, 2) & " 亿",
-                    If(DownloadCount > 100000, Math.Floor(DownloadCount / 10000) & " 万", DownloadCount))
-                Return NewItem
-            End Function) With {.Height = 64}
-        End Function
-        Public Sub ApplyLogoToMyImage(Img As MyImage)
-            If String.IsNullOrEmpty(LogoUrl) Then
-                Img.Source = PathImage & "Icons/NoIcon.png"
-            Else
-                Img.Source = LogoUrl
-                Img.FallbackSource = DlSourceModGet(LogoUrl)
-            End If
-        End Sub
-        Public Function GetControlTitle(HasModLoaderDescription As Boolean) As (Title As String, SubTitle As String)
-            '检查下列代码时可以参考 #1567 的测试例
-            Dim Title As String = RawName
-            Dim SubtitleList As List(Of String)
-            If TranslatedName = RawName Then
-                '没有中文翻译
-                '将所有名称分段
-                Dim NameLists = TranslatedName.Split({" | ", " - ", "(", ")", "[", "]", "{", "}"}, StringSplitOptions.RemoveEmptyEntries).
-                    Select(Function(s) s.Trim(" /\".ToCharArray)).Where(Function(w) Not String.IsNullOrEmpty(w))
-                If NameLists.IsSingle Then GoTo NoSubtitle
-                '查找其中的缩写、Forge/Fabric 等版本标记
-                SubtitleList = New List(Of String)
-                Dim NormalNameList = New List(Of String)
-                For Each Name In NameLists
-                    Dim LowerName As String = Name.Lower
-                    If Name = Name.Upper AndAlso Name <> "FPS" AndAlso Name <> "HUD" Then
-                        '缩写
-                        SubtitleList.Add(Name)
-                    ElseIf {"neoforge", "forge", "fabric", "quilt"}.Any(Function(l) LowerName.Contains(l)) AndAlso
-                        Not RegexCheck(LowerName.Replace("neoforge", "").Replace("forge", "").Replace("fabric", "").Replace("quilt", ""), "[a-z]+") Then '去掉关键词后没有其他字母
-                        'Forge/Fabric 等版本标记
-                        SubtitleList.Add(Name)
-                    Else
-                        '其他部分
-                        NormalNameList.Add(Name)
-                    End If
-                Next
-                '根据分类后的结果处理
-                If Not NormalNameList.Any() OrElse Not SubtitleList.Any() Then GoTo NoSubtitle
-                '同时包含 NormalName 和 Subtitle
-                Title = NormalNameList.Join(" - ")
-            Else
-                '有中文翻译
-                '尝试将文本分为三段:Title (EnglishName) - Suffix
-                '检查时注意 Carpet(它没有中文译名,但有 Suffix)和 “汤姆存储 - 星的奇妙优化 (Tom's balabala)”
-                Title = If(TranslatedName.Contains(" ("), TranslatedName.BeforeFirst(" ("), TranslatedName.BeforeLast(" - "))
-                Dim Suffix As String = ""
-                If TranslatedName.AfterLast(")").Contains(" - ") Then Suffix = TranslatedName.AfterLast(")").AfterLast(" - ")
-                Dim EnglishName As String = TranslatedName
-                If Suffix <> "" Then EnglishName = EnglishName.Replace(" - " & Suffix, "")
-                EnglishName = EnglishName.Replace(Title, "").Trim("("c, ")"c, " "c)
-                '中段的额外信息截取
-                SubtitleList = EnglishName.Split({" | ", " - ", "(", ")", "[", "]", "{", "}"}, StringSplitOptions.RemoveEmptyEntries).
-                        Select(Function(s) s.Trim(" /".ToCharArray)).Where(Function(w) Not String.IsNullOrEmpty(w)).ToList
-                If SubtitleList.Count > 1 AndAlso
-                   Not SubtitleList.Any(Function(s) s.Lower.Contains("forge") OrElse s.Lower.Contains("fabric") OrElse s.Lower.Contains("quilt")) AndAlso '不是标注 XX 版
-                   Not (SubtitleList.Count = 2 AndAlso SubtitleList.Last.Upper = SubtitleList.Last) Then '不是缩写
-                    SubtitleList = New List(Of String) From {EnglishName} '使用原名
-                End If
-                '添加后缀
-                If Suffix <> "" Then SubtitleList.Add(Suffix)
-            End If
-            SubtitleList = SubtitleList.Distinct.ToList()
-            '设置标题与描述
-            Dim Subtitle As String = ""
-            If SubtitleList.Any Then
-                For Each Ex In SubtitleList
-                    Dim IsModLoaderDescription As Boolean =
-                        Ex.Lower.Contains("neoforge") OrElse Ex.Lower.Contains("forge") OrElse Ex.Lower.Contains("fabric") OrElse Ex.Lower.Contains("quilt")
-                    '是否显示 ModLoader 信息
-                    If Not HasModLoaderDescription AndAlso IsModLoaderDescription Then Continue For
-                    '去除 “Forge/Fabric” 这一无意义提示
-                    If Ex.Length < 16 AndAlso Ex.Lower.Contains("fabric") AndAlso Ex.Lower.Contains("forge") Then Continue For
-                    '将 “Forge” 等提示改为 “Forge 版”
-                    If IsModLoaderDescription AndAlso Not Ex.Contains("版") AndAlso
-                        Ex.Lower.Replace("neoforge", "").Replace("forge", "").Replace("fabric", "").Replace("quilt", "").Length <= 3 Then
-                        Ex = Ex.Replace("Edition", "").Replace("edition", "").Trim.Capitalize & " 版"
-                    End If
-                    '将 “forge” 等词语的首字母大写
-                    Ex = Ex.Replace("neoforge", "NeoForge").Replace("forge", "Forge").Replace("neo", "Neo").Replace("fabric", "Fabric").Replace("quilt", "Quilt")
-                    Subtitle &= "  |  " & Ex.Trim
-                Next
-            Else
-NoSubtitle:
-                Subtitle = ""
-            End If
-            Return (Title, Subtitle)
-        End Function
-
-        '辅助函数
-
-        ''' <summary>
-        ''' 检查是否与某个 Project 是相同的工程,只是在不同的网站。
-        ''' </summary>
-        Public Function IsLike(Project As CompProject) As Boolean
-            If Id = Project.Id Then Return True '相同实例
-            '提取字符串中的字母和数字
-            Dim GetRaw =
-            Function(Data As String) As String
-                Dim Result As New StringBuilder()
-                For Each r As Char In Data.Where(Function(c) Char.IsLetterOrDigit(c))
-                    Result.Append(r)
-                Next
-                Return Result.ToString.Lower
-            End Function
-            '来自不同的网站
-            If FromCurseForge = Project.FromCurseForge Then Return False
-            'Mod 加载器一致
-            If ModLoaders.Count <> Project.ModLoaders.Count OrElse ModLoaders.Except(Project.ModLoaders).Any() Then Return False
-            '若不为光影,则要求 MC 版本一致
-            If Types <> CompType.Shader AndAlso (Drops.Count <> Project.Drops.Count OrElse Drops.Except(Project.Drops).Any()) Then Return False
-            '最近更新时间差距在一周以内
-            If Math.Abs((LastUpdate - Project.LastUpdate).TotalDays) > 7 Then Return False
-            'MCMOD 翻译名 / 原名 / 描述文本 / Slug 的英文部分相同
-            If TranslatedName = Project.TranslatedName OrElse
-               RawName = Project.RawName OrElse Description = Project.Description OrElse
-               GetRaw(Slug) = GetRaw(Project.Slug) Then
-                Log($"[Comp] 将 {RawName} ({Slug}) 与 {Project.RawName} ({Project.Slug}) 认定为相似工程")
-                '如果只有一个有 DatabaseEntry,设置给另外一个
-                If DatabaseEntry Is Nothing AndAlso Project.DatabaseEntry IsNot Nothing Then DatabaseEntry = Project.DatabaseEntry
-                If DatabaseEntry IsNot Nothing AndAlso Project.DatabaseEntry Is Nothing Then Project.DatabaseEntry = DatabaseEntry
-                Return True
-            End If
-            Return False
-        End Function
-
-        Public Overrides Function ToString() As String
-            Return $"{Id} ({Slug}): {RawName}"
-        End Function
-        Public Overrides Function Equals(obj As Object) As Boolean
-            Dim project = TryCast(obj, CompProject)
-            Return project IsNot Nothing AndAlso Id = project.Id
-        End Function
-        Public Shared Operator =(left As CompProject, right As CompProject) As Boolean
-            Return EqualityComparer(Of CompProject).Default.Equals(left, right)
-        End Operator
-        Public Shared Operator <>(left As CompProject, right As CompProject) As Boolean
-            Return Not left = right
-        End Operator
-
-    End Class
-
-    '输入与输出
-
-    Public Class CompProjectRequest
-
-        '结果要求
-
-        ''' <summary>
-        ''' 加载后应输出到的结果存储器。
-        ''' </summary>
-        Public Storage As CompProjectStorage
-        ''' <summary>
-        ''' 应当尽量达成的结果数量。
-        ''' </summary>
-        Public TargetResultCount As Integer
-        ''' <summary>
-        ''' 根据加载位置记录,是否还可以继续获取内容。
-        ''' </summary>
-        Public ReadOnly Property CanContinue As Boolean
-            Get
-                If Tag.StartsWithF("/") OrElse Not Sources.HasFlag(CompSourceType.CurseForge) Then Storage.CurseForgeTotal = 0
-                If Tag.EndsWithF("/") OrElse Not Sources.HasFlag(CompSourceType.Modrinth) Then Storage.ModrinthTotal = 0
-                If Storage.CurseForgeTotal = -1 OrElse Storage.ModrinthTotal = -1 Then Return True
-                Return Storage.CurseForgeOffset < Storage.CurseForgeTotal OrElse Storage.ModrinthOffset < Storage.ModrinthTotal
-            End Get
-        End Property
-
-        '输入内容
-
-        ''' <summary>
-        ''' 筛选资源种类。
-        ''' </summary>
-        Public Type As CompType
-        ''' <summary>
-        ''' 筛选资源标签。空字符串代表不限制。格式例如 "406/worldgen",分别是 CurseForge 和 Modrinth 的 ID。
-        ''' </summary>
-        Public Tag As String = ""
-        ''' <summary>
-        ''' 筛选 Mod 加载器类别。
-        ''' </summary>
-        Public ModLoader As CompModLoaderType = CompModLoaderType.Any
-        ''' <summary>
-        ''' 筛选 MC 版本。
-        ''' </summary>
-        Public GameVersion As String = Nothing
-        ''' <summary>
-        ''' 搜索的初始内容。
-        ''' </summary>
-        Public SearchText As String = Nothing
-        ''' <summary>
-        ''' 允许的来源。
-        ''' </summary>
-        Public Sources As CompSourceType = CompSourceType.Any
-        Public Sub New(Type As CompType, Storage As CompProjectStorage, TargetResultCount As Integer)
-            Me.Type = Type
-            Me.Storage = Storage
-            Me.TargetResultCount = TargetResultCount
-        End Sub
-
-        '构造请求
-
-        ''' <summary>
-        ''' 获取对应的 CurseForge API 请求链接。
-        ''' </summary>
-        Public Function GetCurseForgeAddress(SearchText As String, IgnoreModLoaderFilter As Boolean) As String
-            If Tag.StartsWithF("/") Then Storage.CurseForgeTotal = 0
-            '应用筛选参数
-            Dim Address As String = $"https://api.curseforge.com/v1/mods/search?gameId=432&sortField=2&sortOrder=desc&pageSize={COMP_PAGE_SIZE}"
-            Select Case Type
-                Case CompType.Mod
-                    Address += "&classId=6"
-                Case CompType.ModPack
-                    Address += "&classId=4471"
-                Case CompType.DataPack
-                    Address += "&classId=6945"
-                Case CompType.Shader
-                    Address += "&classId=6552"
-                Case CompType.ResourcePack
-                    Address += "&classId=12"
-            End Select
-            If Tag <> "" Then Address += "&categoryId=" & Tag.BeforeFirst("/")
-            If ModLoader <> CompModLoaderType.Any AndAlso Not IgnoreModLoaderFilter 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)
-            If Storage.CurseForgeOffset > 0 Then Address += "&index=" & Storage.CurseForgeOffset
-            Return Address
-        End Function
-        ''' <summary>
-        ''' 获取对应的 Modrinth API 请求链接。
-        ''' </summary>
-        Public Function GetModrinthAddress(SearchText As String, IgnoreModLoaderFilter As Boolean) As String
-            If Tag.EndsWithF("/") Then Storage.ModrinthTotal = 0
-            '应用筛选参数
-            Dim Address As String = $"https://api.modrinth.com/v2/search?limit={COMP_PAGE_SIZE}&index=relevance"
-            If Not String.IsNullOrEmpty(SearchText) Then Address += "&query=" & Net.WebUtility.UrlEncode(SearchText)
-            If Storage.ModrinthOffset > 0 Then Address += "&offset=" & Storage.ModrinthOffset
-            '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).Lower}""]")
-            If Not String.IsNullOrEmpty(Tag) Then Facets.Add($"[""categories:'{Tag.AfterLast("/")}'""]")
-            If ModLoader <> CompModLoaderType.Any AndAlso Not IgnoreModLoaderFilter Then Facets.Add($"[""categories:'{GetStringFromEnum(ModLoader).Lower}'""]")
-            If Not String.IsNullOrEmpty(GameVersion) Then Facets.Add($"[""versions:'{GameVersion}'""]")
-            Address += "&facets=[" & String.Join(",", Facets) & "]"
-            Return Address
-        End Function
-
-        '相同判断
-        Public Overrides Function Equals(obj As Object) As Boolean
-            Dim request = TryCast(obj, CompProjectRequest)
-            Return request IsNot Nothing AndAlso
-                Type = request.Type AndAlso TargetResultCount = request.TargetResultCount AndAlso
-                Tag = request.Tag AndAlso ModLoader = request.ModLoader AndAlso Sources = request.Sources AndAlso
-                GameVersion = request.GameVersion AndAlso SearchText = request.SearchText
-        End Function
-        Public Shared Operator =(left As CompProjectRequest, right As CompProjectRequest) As Boolean
-            Return EqualityComparer(Of CompProjectRequest).Default.Equals(left, right)
-        End Operator
-        Public Shared Operator <>(left As CompProjectRequest, right As CompProjectRequest) As Boolean
-            Return Not left = right
-        End Operator
-        Public Overrides Function GetHashCode() As Integer
-            Return (Type, Tag, ModLoader, GameVersion, SearchText, Sources).GetHashCode()
-        End Function
-
-    End Class
-    Public Class CompProjectStorage
-
-        '加载位置记录
-
-        Public CurseForgeOffset As Integer = 0
-        Public CurseForgeTotal As Integer = -1
-
-        Public ModrinthOffset As Integer = 0
-        Public ModrinthTotal As Integer = -1
-
-        '结果列表
-
-        ''' <summary>
-        ''' 可供展示的所有工程的列表。
-        ''' </summary>
-        Public Results As New List(Of CompProject)
-        ''' <summary>
-        ''' 当前的错误信息。如果没有则为 Nothing。
-        ''' </summary>
-        Public ErrorMessage As String = Nothing
-
-    End Class
-
-    '实际的获取
-
-    Private Const COMP_PAGE_SIZE = 40
-    Private Const MSG_NO_CHINESE_SEARCH_RESULT As String = "无搜索结果,请尝试搜索其英文名称"
-    ''' <summary>
-    ''' 已知工程信息的缓存。
-    ''' </summary>
-    Public CompProjectCache As New Dictionary(Of String, CompProject)
-    ''' <summary>
-    ''' 根据搜索请求获取一系列的工程列表。需要基于加载器运行。
-    ''' </summary>
-    Public Sub CompProjectsGet(Task As LoaderTask(Of CompProjectRequest, Integer))
-        Dim Request As CompProjectRequest = Task.Input
-        Dim Storage = Request.Storage '避免其他线程对 Request.Storage 重新进行了赋值
-
-#Region "前置检查"
-
-        If Storage.Results.Count >= Request.TargetResultCount Then
-            Log($"[Comp] 已有 {Storage.Results.Count} 个结果,多于所需的 {Request.TargetResultCount} 个结果,结束处理")
-            Return
-        ElseIf Not Request.CanContinue Then
-            If Not Storage.Results.Any() Then
-                Throw New Exception("没有符合条件的结果")
-            Else
-                Log($"[Comp] 已有 {Storage.Results.Count} 个结果,少于所需的 {Request.TargetResultCount} 个结果,但无法继续获取,结束处理")
-                Return
-            End If
-        End If
-
-        '拒绝 1.13- Quilt(这个版本根本没有 Quilt)
-        If Request.ModLoader = CompModLoaderType.Quilt AndAlso CompareVersion(If(Request.GameVersion, "1.15"), "1.14") = -1 Then
-            Throw New Exception("Quilt 不支持 Minecraft " & Request.GameVersion)
-        End If
-
-#End Region
-
-#Region "中文搜索"
-
-        Dim RawSearchText As String = If(Request.SearchText, "").Trim
-        RawSearchText = RawSearchText.Lower
-        Log("[Comp] 工程列表搜索原始文本:" & RawSearchText)
-
-        Dim IsChineseSearch As Boolean =
-            RegexCheck(RawSearchText, "[\u4e00-\u9fbb]") AndAlso Not String.IsNullOrEmpty(RawSearchText) AndAlso
-            (Request.Type = CompType.Mod OrElse Request.Type = CompType.DataPack) '目前仅对 Mod 和数据包进行中文搜索,注意整合包的名称可能已经有中文了
-        Dim CurseForgeAltSearchText As String = Nothing, ModrinthAltSearchText As String = Nothing, ModrinthSlugs As New List(Of String) '从中文转为英文的替代搜索内容
-        If IsChineseSearch Then
-            '帮助方法:从搜索项提取可能的英文单词
-            Dim ExtractWords =
-            Function(Result As SearchEntry(Of CompDatabaseEntry), Source As CompSourceType) As IEnumerable(Of String)
-                '从各个可能的来源提取候选
-                Dim Candidates As New List(Of String)
-                If Result.Item.CurseForgeSlug IsNot Nothing AndAlso Source = CompSourceType.CurseForge Then
-                    Candidates.Add(Result.Item.CurseForgeSlug.Replace("-", " ").Replace("/", " "))
-                End If
-                If Result.Item.ModrinthSlug IsNot Nothing AndAlso Source = CompSourceType.Modrinth Then
-                    Candidates.Add(Result.Item.ModrinthSlug.Replace("-", " ").Replace("/", " "))
-                End If
-                Candidates.Add(Result.Item.ChineseName.AfterLast(" (").TrimEnd(") ").BeforeFirst(" - ").
-                                    Replace("-", " ").Replace("/", " ").Replace(":", " ").Replace("(", " ").Replace(")", ""))
-                '分词、清洗、去重
-                Candidates = Candidates.
-                    SelectMany(Function(c) c.Split(" ")).
-                    Select(Function(w) w.TrimStart("{[(").TrimEnd("}])").Lower).
-                    Where(
-                    Function(w)
-                        If w.Length <= 1 Then Return False '单字或空白
-                        If {"the", "of", "mod", "and", "forge", "fabric", "for", "quilt", "neoforge"}.Contains(w) Then Return False '常见词
-                        If Val(w) > 0 Then Return False '数字
-                        Return True
-                    End Function).Distinct.ToList
-                '如果一个词可以由其他词拼成,则去掉这个词(例如将 ender io enderio 的 enderio 剔除,只保留 ender io)
-                Dim CanForm As Func(Of String, Boolean) = Nothing
-                CanForm = Function(s) Candidates.Contains(s) OrElse Candidates.Any(Function(c) s.StartsWith(c) AndAlso CanForm(s.Substring(c.Length)))
-                Candidates = Candidates.Where(Function(w) Not Candidates.Any(Function(c) c.Length < w.Length AndAlso w.StartsWith(c) AndAlso CanForm(w.Substring(c.Length)))).ToList()
-                Return Candidates
-            End Function
-            'CurseForge
-            If Request.Sources.HasFlag(CompSourceType.CurseForge) Then
-                '数据库搜索
-                Static CurseForgeSearchEntries As List(Of SearchEntry(Of CompDatabaseEntry)) =
-                CompDatabase.Where(Function(Entry) Entry.CurseForgeSlug IsNot Nothing).Select(
-                Function(Entry) New SearchEntry(Of CompDatabaseEntry) With {
-                    .Item = Entry,
-                    .SearchSource = New List(Of SearchSource) From {
-                        New SearchSource(Entry.ChineseName.BeforeFirst(" (").Split({"/"c}, StringSplitOptions.RemoveEmptyEntries), 1), '部分 Mod 有别名
-                        New SearchSource(Entry.ChineseName.AfterFirst(" (") & Entry.CurseForgeSlug, 0.5)}
-                }).ToList
-                Dim CurseForgeSearchResults = Search(CurseForgeSearchEntries, RawSearchText, 100, 0.25)
-                If CurseForgeSearchResults.Any Then
-                    '选取目标(CurseForge 要求每个词都必须匹配上,所以只能选择一个 Mod 进行搜索)
-                    Dim CurseForgeTarget =
-                        If(CurseForgeSearchResults.First.AbsoluteRight,
-                            CurseForgeSearchResults.Where(Function(s) s.AbsoluteRight).ToList, '优先使用所有完全匹配的
-                            CurseForgeSearchResults.MaxByAll(Function(s) s.Similarity)). '其次使用所有相似度最高的
-                        MaxBy(Function(s) s.Item.Popularity) '然后从中选择最受欢迎的那一个
-                    '后处理
-                    CurseForgeAltSearchText = ExtractWords(CurseForgeTarget, CompSourceType.CurseForge).Join(" ")
-                    Log("[Comp] 中文搜索关键词(CurseForge):" & CurseForgeAltSearchText, LogLevel.Debug)
-                End If
-            End If
-            'Modrinth
-            If Request.Sources.HasFlag(CompSourceType.Modrinth) Then
-                '数据库搜索
-                Static ModrinthSearchEntries As List(Of SearchEntry(Of CompDatabaseEntry)) =
-                CompDatabase.Where(Function(Entry) Entry.ModrinthSlug IsNot Nothing).Select(
-                Function(Entry) New SearchEntry(Of CompDatabaseEntry) With {
-                    .Item = Entry,
-                    .SearchSource = New List(Of SearchSource) From {
-                        New SearchSource(Entry.ChineseName.BeforeFirst(" (").Split({"/"c}, StringSplitOptions.RemoveEmptyEntries), 1), '部分 Mod 有别名
-                        New SearchSource(Entry.ChineseName.AfterFirst(" (") & Entry.ModrinthSlug, 0.5)}
-                }).ToList
-                Dim ModrinthSearchResults = Search(ModrinthSearchEntries, RawSearchText, 100, 0.25)
-                If ModrinthSearchResults.Any Then
-                    '分词
-                    Dim WordWeights As New Dictionary(Of String, Double) '各个单词及其出现的权重
-                    For Each Result In ModrinthSearchResults
-                        For Each Word In ExtractWords(Result, CompSourceType.Modrinth)
-                            If Not WordWeights.ContainsKey(Word) Then WordWeights.Add(Word, 0)
-                            Dim Similarity = If(Result.SearchSource.Any(Function(s) s.Aliases.Contains(RawSearchText)), 1000, Result.Similarity) '完全匹配为 1000
-                            WordWeights(Word) += Similarity * Result.Item.Popularity '权重 += 相似度 * 受欢迎程度
-                        Next
-                    Next
-                    ModrinthAltSearchText = WordWeights.MaxBy(Function(w) w.Value).Key
-                    Log("[Comp] 中文搜索关键词(Modrinth):" & ModrinthAltSearchText, LogLevel.Debug)
-                    '直接请求工程
-                    ModrinthSlugs = ModrinthSearchResults.Take(100).Select(Function(r) r.Item.ModrinthSlug).ToList
-                End If
-            End If
-            '结束
-            If String.IsNullOrEmpty(CurseForgeAltSearchText) AndAlso String.IsNullOrEmpty(ModrinthAltSearchText) AndAlso Not ModrinthSlugs.Any Then
-                Throw New Exception(MSG_NO_CHINESE_SEARCH_RESULT)
-            End If
-        End If
-        Task.Progress = 0.05
-
-#End Region
-
-        Dim RealResults As New List(Of CompProject)
-NextPage:
-        Dim RawResults As New SafeList(Of CompProject)
-
-#Region "从 CurseForge 和 Modrinth 获取结果列表,存储于 RawResults"
-
-        Dim WorkThreads As New List(Of Thread)
-        Dim Errors As New SafeList(Of (Ex As Exception, Source As CompSourceType))
-
-        '在 1.14-,部分老 Mod 没有设置支持的加载器,因此添加 Forge 筛选就会出现遗漏
-        '所以,在发起请求时不筛选加载器,然后在返回的结果中自行筛除不是 Forge 的 Mod
-        Dim IgnoreModLoaderFilter = Request.ModLoader = CompModLoaderType.Forge AndAlso McVersion.VersionToDrop(Request.GameVersion) < 140
-        Try
-
-            'CurseForge 搜索
-            If Request.Sources.HasFlag(CompSourceType.CurseForge) AndAlso
-               Not (Storage.CurseForgeTotal > -1 AndAlso Storage.CurseForgeTotal <= Storage.CurseForgeOffset) AndAlso '剩余的未显示的搜索结果不足
-               (Not IsChineseSearch OrElse (IsChineseSearch AndAlso Not String.IsNullOrEmpty(CurseForgeAltSearchText))) Then '如果是中文搜索,就只在有对应搜索关键词的时候才继续
-                WorkThreads.Add(RunInNewThread(
-                Sub()
-                    Try
-                        '获取工程列表
-                        Dim CurseForgeUrl As String = Request.GetCurseForgeAddress(If(CurseForgeAltSearchText, RawSearchText), IgnoreModLoaderFilter)
-                        Log("[Comp] 开始 CurseForge 搜索:" & CurseForgeUrl)
-                        Dim RequestResult As JObject = DlModRequest(CurseForgeUrl)
-                        Dim ProjectList As New List(Of CompProject)
-                        For Each JsonEntry As JObject In RequestResult("data")
-                            Dim Project As New CompProject(JsonEntry)
-                            If Request.Type = CompType.ResourcePack AndAlso Project.Tags.Contains("数据包") Then Continue For 'CurseForge 将一些数据包分类成了资源包
-                            ProjectList.Add(Project)
-                        Next
-                        '更新结果
-                        ProjectList.ForEach(Sub(p) RawResults.Add(p))
-                        Storage.CurseForgeOffset += RequestResult("data").Count
-                        Storage.CurseForgeTotal = RequestResult("pagination")("totalCount").ToObject(Of Integer)
-                        Log($"[Comp] 从 CurseForge 搜索到了 {ProjectList.Count} 个工程(总计已获取 {Storage.CurseForgeOffset} 个,共 {Storage.CurseForgeTotal} 个)")
-                    Catch ex As Exception
-                        Log(ex, "CurseForge 搜索失败")
-                        Storage.CurseForgeTotal = -1
-                        Errors.Add((ex, CompSourceType.CurseForge))
-                    End Try
-                    If Task.Progress < 0.75 Then Task.Progress += 0.25 '可能重复加载多页,所以不能直接给够
-                End Sub, "CurseForge Search"))
-            End If
-
-            'Modrinth 搜索
-            If Request.Sources.HasFlag(CompSourceType.Modrinth) AndAlso
-               Not (Storage.ModrinthTotal > -1 AndAlso Storage.ModrinthTotal <= Storage.ModrinthOffset) AndAlso '剩余的未显示的搜索结果不足
-               (Not IsChineseSearch OrElse (IsChineseSearch AndAlso Not String.IsNullOrEmpty(ModrinthAltSearchText))) Then '如果是中文搜索,就只在有对应搜索关键词的时候才继续
-                WorkThreads.Add(RunInNewThread(
-                Sub()
-                    Try
-                        Dim ModrinthUrl As String = Request.GetModrinthAddress(If(ModrinthAltSearchText, RawSearchText), IgnoreModLoaderFilter)
-                        Log("[Comp] 开始 Modrinth 搜索:" & ModrinthUrl)
-                        Dim RequestResult As JObject = DlModRequest(ModrinthUrl)
-                        Dim ProjectList As New List(Of CompProject)
-                        For Each JsonEntry As JObject In RequestResult("hits")
-                            ProjectList.Add(New CompProject(JsonEntry))
-                        Next
-                        '更新结果
-                        ProjectList.ForEach(Sub(p) RawResults.Add(p))
-                        Storage.ModrinthOffset += RequestResult("hits").Count
-                        Storage.ModrinthTotal = RequestResult("total_hits").ToObject(Of Integer)
-                        Log($"[Comp] 从 Modrinth 搜索到了 {ProjectList.Count} 个工程(总计已获取 {Storage.ModrinthOffset} 个,共 {Storage.ModrinthTotal} 个)")
-                    Catch ex As Exception
-                        Log(ex, "Modrinth 搜索失败")
-                        Storage.ModrinthTotal = -1
-                        Errors.Add((ex, CompSourceType.Modrinth))
-                    End Try
-                    If Task.Progress < 0.75 Then Task.Progress += 0.25 '可能重复加载多页,所以不能直接给够
-                End Sub, "Modrinth Search"))
-            End If
-
-            'Modrinth 直接获取工程
-            If Request.Sources.HasFlag(CompSourceType.Modrinth) AndAlso
-               Not (Storage.ModrinthTotal > -1 AndAlso Storage.ModrinthTotal <= Storage.ModrinthOffset) AndAlso '剩余的未显示的搜索结果不足
-               ModrinthSlugs.Any Then '有直接获取的 Slug
-                WorkThreads.Add(RunInNewThread(
-                Sub()
-                    Try
-                        Dim ModrinthUrl As String = $"https://api.modrinth.com/v2/projects?ids=[""{ModrinthSlugs.Join(""",""")}""]"
-                        Log("[Comp] 开始 Modrinth 直接获取:" & ModrinthUrl)
-                        Dim ProjectList As New List(Of CompProject)
-                        For Each JsonEntry As JObject In DlModRequest(ModrinthUrl)
-                            Dim Project As New CompProject(JsonEntry)
-                            '应用筛选
-                            If Request.Type <> CompType.Any AndAlso Not Project.Types.HasFlag(Request.Type) Then Continue For
-                            If Not String.IsNullOrEmpty(Request.Tag) AndAlso
-                                Not JsonEntry("categories").Any(Function(c) c.ToString = Request.Tag.AfterLast("/")) Then Continue For 'Project.Tags 已经转换成中文了,只能从 json 判
-                            If Request.ModLoader <> CompModLoaderType.Any AndAlso Not IgnoreModLoaderFilter AndAlso
-                                Not Project.ModLoaders.Any(Function(m) m = Request.ModLoader) Then Continue For
-                            If Not String.IsNullOrEmpty(Request.GameVersion) AndAlso
-                                Not Project.UnsafeGameVersions.Any(Function(d) d = Request.GameVersion) Then Continue For
-                            ProjectList.Add(Project)
-                        Next
-                        '更新结果
-                        ProjectList.ForEach(Sub(p) RawResults.Add(p))
-                        Log($"[Comp] 从 Modrinth 直接获取到了 {ProjectList.Count} 个工程")
-                        ModrinthSlugs.Clear() '防止重试/加载下一页时重复获取
-                    Catch ex As Exception
-                        Log(ex, "Modrinth 直接获取失败")
-                        Errors.Add((ex, CompSourceType.Modrinth))
-                    End Try
-                    If Task.Progress < 0.75 Then Task.Progress += 0.25 '可能重复加载多页,所以不能直接给够
-                End Sub, "Modrinth Get"))
-            End If
-
-            '等待线程结束
-            For Each Thread In WorkThreads
-                Thread.Join()
-                If Task.IsInterrupted Then Return '会自动触发 Finally 以清理线程
-            Next
-
-            '筛除不是 Forge 的 Mod
-            If IgnoreModLoaderFilter Then
-                RawResults = RawResults.Where(Function(p) Not p.ModLoaders.Any() OrElse p.ModLoaders.Contains(CompModLoaderType.Forge)).ToList
-            End If
-
-            '确保存在结果
-            Storage.ErrorMessage = Nothing
-            If Not RawResults.Any() Then
-                If Errors.Any() Then
-                    Throw Errors.First.Ex
-                Else
-                    If IsChineseSearch AndAlso Not (Request.Type = CompType.Mod OrElse Request.Type = CompType.DataPack) Then
-                        Throw New Exception(MSG_NO_CHINESE_SEARCH_RESULT)
-                    ElseIf Request.Sources = CompSourceType.CurseForge AndAlso Request.Tag.StartsWithF("/") Then
-                        Throw New Exception("CurseForge 不兼容所选的类型")
-                    ElseIf Request.Sources = CompSourceType.Modrinth AndAlso Request.Tag.EndsWithF("/") Then
-                        Throw New Exception("Modrinth 不兼容所选的类型")
-                    Else
-                        Throw New Exception("没有搜索结果")
-                    End If
-                End If
-            ElseIf Errors.Any() Then
-                '有结果但是有错误
-                If Errors.Any(Function(e) e.Source = CompSourceType.CurseForge) Then
-                    Storage.ErrorMessage = $"无法连接到 CurseForge,所以目前仅显示了来自 Modrinth 的内容,搜索结果可能不全。{vbCrLf}请稍后再试,或使用 VPN 改善网络环境。"
-                Else
-                    Storage.ErrorMessage = $"无法连接到 Modrinth,所以目前仅显示了来自 CurseForge 的内容,搜索结果可能不全。{vbCrLf}请稍后再试,或使用 VPN 改善网络环境。"
-                End If
-            End If
-
-        Finally
-            For Each Thread In WorkThreads
-                If Thread.IsAlive Then Thread.Interrupt()
-            Next
-        End Try
-
-#End Region
-
-#Region "提取非重复项,存储于 RealResults"
-
-        '将 CurseForge 排在 Modrinth 的前面,避免加载结束顺序不同导致排名不同
-        RawResults = RawResults.Where(Function(x) x.FromCurseForge).Concat(RawResults.Where(Function(x) Not x.FromCurseForge)).ToList
-        '去重
-        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
-        '加入列表
-        RealResults.AddRange(RawResults)
-        Log($"[Comp] 去重、筛选后累计新增结果 {RealResults.Count} 个(目前已有结果 {Storage.Results.Count} 个)")
-
-#End Region
-
-#Region "检查结果数量,如果不足且可继续,会继续加载下一页"
-
-        If RealResults.Count + Storage.Results.Count < Request.TargetResultCount Then
-            Log($"[Comp] 总结果数需求最少 {Request.TargetResultCount} 个,仅获得了 {RealResults.Count + Storage.Results.Count} 个")
-            If Request.CanContinue AndAlso Not Errors.Any Then '如果有某个源失败则不再重试,这时候重试可能导致无限循环
-                Log("[Comp] 将继续尝试加载下一页")
-                GoTo NextPage
-            Else
-                Log("[Comp] 无法继续加载,将强制结束")
-            End If
-        End If
-
-#End Region
-
-#Region "将结果排序并添加"
-
-        Dim Scores As New Dictionary(Of CompProject, Double) '排序分
-        Dim GetDownloadCountMult =
-        Function(Project As CompProject) As Double
-            Select Case Request.Type
-                Case CompType.Mod, CompType.ModPack
-                    Return If(Project.FromCurseForge, 1, 5)
-                Case CompType.DataPack
-                    Return If(Project.FromCurseForge, 10, 1)
-                Case CompType.ResourcePack, CompType.Shader
-                    Return If(Project.FromCurseForge, 1, 4)
-                Case Else
-                    Return 1
-            End Select
-        End Function
-        If String.IsNullOrEmpty(RawSearchText) Then
-            '如果没有搜索文本,按下载量将结果排序
-            For Each Result As CompProject In RealResults
-                Scores(Result) = Result.DownloadCount * GetDownloadCountMult(Result)
-            Next
-        Else
-            '如果有搜索文本,按关联度将结果排序
-            '排序分 = 搜索相对相似度 (1) + 下载量权重 (对数,10 亿时为 1) + 有中文名 (0.2)
-            Dim SearchEntries As New List(Of SearchEntry(Of CompProject))
-            For Each Result As CompProject In RealResults
-                Scores(Result) = If(Result.WikiId > 0, 0.2, 0) +
-                           Math.Log10(Math.Max(Result.DownloadCount, 1) * GetDownloadCountMult(Result)) / 9
-                SearchEntries.Add(New SearchEntry(Of CompProject) With {.Item = Result, .SearchSource = New List(Of SearchSource) From {
-                    New SearchSource(If(IsChineseSearch, Result.TranslatedName, Result.RawName).Split({"/"c}, StringSplitOptions.RemoveEmptyEntries), 1),
-                    New SearchSource(Result.Description, 0.05)}})
-            Next
-            Dim SearchResult = Search(SearchEntries, RawSearchText, 10000, -1)
-            For Each OneResult In SearchResult
-                Scores(OneResult.Item) +=
-                    If(OneResult.AbsoluteRight, 10, OneResult.Similarity) /
-                    If(SearchResult.First.AbsoluteRight, 10, SearchResult.First.Similarity) '最高 1 分的相似度分
-            Next
-        End If
-        '根据排序分得出结果并添加
-        If Task.IsInterrupted Then Throw New ThreadInterruptedException '#8246
-        Storage.Results.AddRange(
-            Scores.OrderByDescending(Function(s) s.Value).Select(Function(r) r.Key))
-
-#End Region
-
-    End Sub
-
-#End Region
-
-#Region "CompFile | 文件信息"
-
-    '类定义
-
-    Public Enum CompFileStatus
-        Release = 1 '枚举值来源:https://docs.curseforge.com/#tocS_FileReleaseType
-        Beta = 2
-        Alpha = 3
-    End Enum
-    Public Class CompFile
-
-        '源信息
-
-        ''' <summary>
-        ''' 文件的种类。
-        ''' </summary>
-        Public ReadOnly Type As CompType
-        ''' <summary>
-        ''' 该文件来自 CurseForge 还是 Modrinth。
-        ''' </summary>
-        Public ReadOnly FromCurseForge As Boolean
-        ''' <summary>
-        ''' 用于唯一性鉴别该文件的 ID。CurseForge 中为 123456 的大整数,Modrinth 中为英文乱码的 Version 字段。
-        ''' </summary>
-        Public ReadOnly Id As String
-
-        '描述性信息
-
-        ''' <summary>
-        ''' 文件描述名(并非文件名,是自定义的字段)。对很多 Mod,这会给出 Mod 版本号。
-        ''' </summary>
-        Public DisplayName As String
-        ''' <summary>
-        ''' 发布时间。
-        ''' </summary>
-        Public ReadOnly ReleaseDate As Date
-        ''' <summary>
-        ''' 下载量计数。注意,该计数仅为一个来源,无法反应两边加起来的下载量,且 CurseForge 可能错误地返回 0。
-        ''' </summary>
-        Public ReadOnly DownloadCount As Integer
-        ''' <summary>
-        ''' Mod 版本号。
-        ''' 不一定是标准格式。CurseForge 上默认为 Nothing。
-        ''' </summary>
-        Public Version As String
-        ''' <summary>
-        ''' 支持的 Mod 加载器列表。可能为空。
-        ''' </summary>
-        Public ReadOnly ModLoaders As List(Of CompModLoaderType)
-        ''' <summary>
-        ''' 支持的游戏版本列表。类型包括:"26.1.5","26.1","26.1 预览版","1.18.5","1.18","1.18 预览版","21w15a","未知版本"。
-        ''' </summary>
-        Public ReadOnly GameVersions As List(Of String)
-        ''' <summary>
-        ''' 发布状态:Release/Beta/Alpha。
-        ''' </summary>
-        Public ReadOnly Status As CompFileStatus
-        ''' <summary>
-        ''' 发布状态的友好描述。例如:"正式版","Beta 版"。
-        ''' </summary>
-        Public ReadOnly Property StatusDescription As String
-            Get
-                Select Case Status
-                    Case CompFileStatus.Release
-                        Return "正式版"
-                    Case CompFileStatus.Beta
-                        Return If(ModeDebug, "Beta 版", "测试版")
-                    Case Else
-                        Return If(ModeDebug, "Alpha 版", "早期测试版")
-                End Select
-            End Get
-        End Property
-
-        '下载信息
-        ''' <summary>
-        ''' 下载信息是否可用。
-        ''' </summary>
-        Public ReadOnly Property Available As Boolean
-            Get
-                Return FileName IsNot Nothing AndAlso DownloadUrls IsNot Nothing
-            End Get
-        End Property
-        ''' <summary>
-        ''' 下载的文件名。
-        ''' </summary>
-        Public ReadOnly FileName As String = Nothing
-        ''' <summary>
-        ''' 文件所有可能的下载源。
-        ''' </summary>
-        Public DownloadUrls As List(Of String)
-        ''' <summary>
-        ''' 文件的 SHA1 或 MD5。
-        ''' </summary>
-        Public ReadOnly Hash As String = Nothing
-        ''' <summary>
-        ''' 文件大小。不可用时为 -1。
-        ''' </summary>
-        Public ReadOnly Size As Integer = -1
-        ''' <summary>
-        ''' 该文件的所有依赖工程的原始 ID。
-        ''' 这些 ID 可能没有加载,在加载后会添加到 Dependencies 中(主要是因为 Modrinth 返回的是字符串 ID 而非 Slug,导致 Project.Id 查询不到)。
-        ''' </summary>
-        Public ReadOnly RawDependencies As New List(Of String)
-        ''' <summary>
-        ''' 该文件的所有依赖工程的 Project.Id。
-        ''' </summary>
-        Public ReadOnly Dependencies As New List(Of String)
-        ''' <summary>
-        ''' 获取下载信息。
-        ''' </summary>
-        ''' <param name="LocalAddress">目标本地文件夹,或完整的文件路径。会自动判断类型。</param>
-        Public Function ToNetFile(LocalAddress As String) As NetFile
-            Return New NetFile(DownloadUrls, LocalAddress & If(LocalAddress.EndsWithF("\"), FileName, ""), New FileChecker(Hash:=Hash, ActualSize:=Size), SimulateBrowserHeaders:=True)
-        End Function
-
-        '实例化
-
-        ''' <summary>
-        ''' 从 json 中初始化实例。若出错会抛出异常。
-        ''' </summary>
-        Public Sub New(Data As JObject, DefaultType As CompType)
-            Type = DefaultType
-            If Data.ContainsKey("FromCurseForge") Then
-#Region "CompJson"
-                FromCurseForge = Data("FromCurseForge").ToObject(Of Boolean)
-                Id = Data("Id").ToString
-                DisplayName = Data("DisplayName").ToString
-                If Data.ContainsKey("Version") Then Version = Data("Version").ToString
-                ReleaseDate = Data("ReleaseDate").ToObject(Of Date)
-                DownloadCount = Data("DownloadCount").ToObject(Of Integer)
-                Status = CType(Data("Status").ToObject(Of Integer), CompFileStatus)
-                If Data.ContainsKey("FileName") Then FileName = Data("FileName").ToString
-                If Data.ContainsKey("DownloadUrls") Then DownloadUrls = Data("DownloadUrls").ToObject(Of List(Of String))
-                If Data.ContainsKey("ModLoaders") Then ModLoaders = Data("ModLoaders").ToObject(Of List(Of CompModLoaderType))
-                If Data.ContainsKey("Size") Then Size = Data("Size").ToObject(Of Integer)
-                If Data.ContainsKey("Hash") Then Hash = Data("Hash").ToString
-                If Data.ContainsKey("GameVersions") Then GameVersions = Data("GameVersions").ToObject(Of List(Of String))
-                If Data.ContainsKey("RawDependencies") Then RawDependencies = Data("RawDependencies").ToObject(Of List(Of String))
-                If Data.ContainsKey("Dependencies") Then Dependencies = Data("Dependencies").ToObject(Of List(Of String))
-#End Region
-            Else
-                FromCurseForge = Data.ContainsKey("gameId")
-                If FromCurseForge Then
-#Region "CurseForge"
-                    '简单信息
-                    Id = Data("id")
-                    DisplayName = Data("displayName").ToString.Replace("	", "").Trim(" ")
-                    Version = Nothing
-                    ReleaseDate = Data("fileDate")
-                    Status = CType(Data("releaseType").ToObject(Of Integer), CompFileStatus)
-                    DownloadCount = Data("downloadCount")
-                    FileName = Data("fileName")
-                    Size = Data("fileLength")
-                    Hash = CType(Data("hashes"), JArray).ToList.FirstOrDefault(Function(s) s("algo").ToObject(Of Integer) = 1)?("value")
-                    If Hash Is Nothing Then Hash = CType(Data("hashes"), JArray).ToList.FirstOrDefault(Function(s) s("algo").ToObject(Of Integer) = 2)?("value")
-                    'DownloadAddress
-                    Dim Url = Data("downloadUrl").ToString
-                    If Url = "" Then Url = $"https://edge.forgecdn.net/files/{CInt(Id.ToString.Substring(0, 4))}/{CInt(Id.ToString.Substring(4))}/{FileName}"
-                    DownloadUrls = HandleCurseForgeDownloadUrls(Url.Replace(FileName, WebUtility.UrlEncode(FileName))) '对脑残 CurseForge 的下载地址进行多种修正
-                    DownloadUrls.Add(DlSourceModGet(Url)) '添加镜像源;注意 MCIM 源不支持 URL 编码后的文件名,必须传入 URL 编码前的文件名
-                    DownloadUrls = DownloadUrls.Distinct.ToList '最终去重
-                    'Dependencies
-                    If Data.ContainsKey("dependencies") Then
-                        RawDependencies = Data("dependencies").
-                            Where(Function(d) d("relationType").ToObject(Of Integer) = 3 AndAlso '种类为依赖
-                                              d("modId").ToObject(Of Integer) <> 306612 AndAlso d("modId").ToObject(Of Integer) <> 634179). '排除 Fabric API 和 Quilt API
-                            Select(Function(d) d("modId").ToString).ToList
-                    End If
-                    'GameVersions
-                    Dim RawVersions As List(Of String) = Data("gameVersions").Select(Function(t) t.ToString.Trim.Lower).ToList
-                    GameVersions = RawVersions.
-                        Where(Function(v) McVersion.IsFormatFit(v)).
-                        Select(Function(v) v.Replace("-snapshot", " 预览版")).
-                        Distinct.ToList
-                    If GameVersions.IsSingle Then
-                        GameVersions = GameVersions.ToList
-                    ElseIf GameVersions.Count > 1 Then
-                        GameVersions = GameVersions.SortByComparison(AddressOf CompareVersionGE).ToList
-                        If Type = CompType.ModPack Then GameVersions = New List(Of String) From {GameVersions(0)} '整合包理应只 “支持” 一个版本
-                    Else
-                        GameVersions = New List(Of String) From {"未知版本"}
-                    End If
-                    'ModLoaders
-                    ModLoaders = New List(Of CompModLoaderType)
-                    If RawVersions.Contains("forge") Then ModLoaders.Add(CompModLoaderType.Forge)
-                    If RawVersions.Contains("fabric") Then ModLoaders.Add(CompModLoaderType.Fabric)
-                    If RawVersions.Contains("quilt") Then ModLoaders.Add(CompModLoaderType.Quilt)
-                    If RawVersions.Contains("neoforge") Then ModLoaders.Add(CompModLoaderType.NeoForge)
-#End Region
-                Else
-#Region "Modrinth"
-                    '简单信息
-                    Id = Data("id")
-                    DisplayName = Data("name").ToString.Replace("	", "").Trim(" ")
-                    Version = Data("version_number")
-                    ReleaseDate = Data("date_published")
-                    Status = If(Data("version_type").ToString = "release", CompFileStatus.Release, If(Data("version_type").ToString = "beta", CompFileStatus.Beta, CompFileStatus.Alpha))
-                    DownloadCount = Data("downloads")
-                    If CType(Data("files"), JArray).Any() Then '可能为空
-                        Dim File As JToken = Data("files")(0)
-                        FileName = File("filename")
-                        DownloadUrls = New List(Of String) From {File("url"), DlSourceModGet(File("url"))}.Distinct.ToList '同时添加了镜像源
-                        Size = File("size")
-                        Hash = File("hashes")("sha1")
-                    End If
-                    'Loaders
-                    '结果可能混杂着 Mod、数据包和服务端插件
-                    Dim RawLoaders As List(Of String) = Data("loaders").Select(Function(v) v.ToString).ToList
-                    ModLoaders = New List(Of CompModLoaderType)
-                    If Type.HasFlag(CompType.Mod) OrElse Type.HasFlag(CompType.DataPack) Then
-                        If RawLoaders.Intersect({"bukkit", "folia", "paper", "purpur", "spigot"}).Any() Then Type = CompType.Plugin 'Veinminer Enchantment 同时支持服务端与 Fabric
-                        If RawLoaders.Contains("datapack") Then Type = CompType.DataPack
-                        If RawLoaders.Contains("forge") Then ModLoaders.Add(CompModLoaderType.Forge) : Type = CompType.Mod
-                        If RawLoaders.Contains("neoforge") Then ModLoaders.Add(CompModLoaderType.NeoForge) : Type = CompType.Mod
-                        If RawLoaders.Contains("fabric") Then ModLoaders.Add(CompModLoaderType.Fabric) : Type = CompType.Mod
-                        If RawLoaders.Contains("quilt") Then ModLoaders.Add(CompModLoaderType.Quilt) : Type = CompType.Mod
-                    Else
-                        '使用传入的类别,不作修改(#8377)
-                    End If
-                    'Dependencies
-                    If Data.ContainsKey("dependencies") Then
-                        RawDependencies = Data("dependencies").
-                            Where(Function(d) d("dependency_type") = "required" AndAlso '种类为依赖
-                                              d("project_id") <> "P7dR8mSH" AndAlso d("project_id") <> "qvIfYCYJ" AndAlso '排除 Fabric API 和 Quilt API
-                                              d("project_id").ToString.Length > 0). '有时候真的会空……
-                            Select(Function(d) d("project_id").ToString).ToList
-                    End If
-                    'GameVersions
-                    Dim RawVersions As List(Of String) = Data("game_versions").Select(Function(t) t.ToString.Trim.Lower).ToList
-                    GameVersions = RawVersions.Where(Function(v) v.Contains(".")).
-                                               Select(Function(v) If(v.Contains("-"), v.BeforeFirst("-") & " 预览版", If(v.StartsWithF("b1."), "远古版本", v))).
-                                               Distinct.ToList
-                    If GameVersions.IsSingle Then
-                        '无需处理
-                    ElseIf GameVersions.Count > 1 Then
-                        GameVersions = GameVersions.SortByComparison(AddressOf CompareVersionGE).ToList
-                        If Type = CompType.ModPack Then GameVersions = New List(Of String) From {GameVersions(0)} '整合包理应只 “支持” 一个版本
-                    ElseIf RawVersions.Any(Function(v) RegexCheck(v, "[0-9]{2}w[0-9]{2}[a-z]")) Then
-                        GameVersions = RawVersions.Where(Function(v) RegexCheck(v, "[0-9]{2}w[0-9]{2}[a-z]")).ToList
-                    Else
-                        GameVersions = New List(Of String) From {"未知版本"}
-                    End If
-#End Region
-                End If
-            End If
-        End Sub
-
-        ''' <summary>
-        ''' 重新整理 CurseForge 的下载地址。
-        ''' </summary>
-        Public Shared Function HandleCurseForgeDownloadUrls(Url As String) As List(Of String)
-            Return {
-                Url.Replace("-service.overwolf.wtf", ".forgecdn.net").Replace("://edge.", "://mediafilez.").Replace("://media.", "://mediafilez."),
-                Url.Replace("://edge.", "://mediafilez.").Replace("://media.", "://mediafilez."),
-                Url.Replace("-service.overwolf.wtf", ".forgecdn.net"),
-                Url.Replace("://media.", "://edge."),
-                Url
-            }.Distinct.ToList
-        End Function
-
-        ''' <summary>
-        ''' 将当前实例转为可用于保存缓存的 Json。
-        ''' </summary>
-        Public Function ToJson() As JObject
-            Dim Json As New JObject
-            Json.Add("FromCurseForge", FromCurseForge)
-            Json.Add("Id", Id)
-            If Version IsNot Nothing Then Json.Add("Version", Version)
-            Json.Add("DisplayName", DisplayName)
-            Json.Add("ReleaseDate", ReleaseDate)
-            Json.Add("DownloadCount", DownloadCount)
-            Json.Add("ModLoaders", New JArray(ModLoaders.Select(Function(m) CInt(m))))
-            Json.Add("GameVersions", New JArray(GameVersions))
-            Json.Add("Status", CInt(Status))
-            If FileName IsNot Nothing Then Json.Add("FileName", FileName)
-            If DownloadUrls IsNot Nothing Then Json.Add("DownloadUrls", New JArray(DownloadUrls))
-            If Hash IsNot Nothing Then Json.Add("Hash", Hash)
-            If Size >= 0 Then Json.Add("Size", Size)
-            Json.Add("RawDependencies", New JArray(RawDependencies))
-            Json.Add("Dependencies", New JArray(Dependencies))
-            Return Json
-        End Function
-        ''' <summary>
-        ''' 将当前文件信息实例化为控件。
-        ''' </summary>
-        Public Function ToListItem(OnClick As MyListItem.ClickEventHandler, Optional OnSaveClick As MyIconButton.ClickEventHandler = Nothing,
-                                   Optional BadDisplayName As Boolean = False) As MyVirtualizingElement(Of MyListItem)
-            Return New MyVirtualizingElement(Of MyListItem)(
-            Function()
-                '获取描述信息
-                Dim Title As String = If(BadDisplayName, FileName, DisplayName)
-                Dim Info As New List(Of String)
-                If Title <> FileName.BeforeLast(".") Then Info.Add(FileName.BeforeLast("."))
-                If Dependencies.Any Then Info.Add(Dependencies.Count & " 项前置")
-                If GameVersions.All(
-                Function(VerName)
-                    Return Not VerName.Contains(".") OrElse {"w", "snapshot", "rc", "pre", "experimental", "-"}.Any(Function(s) VerName.ContainsF(s, True))
-                End Function) Then Info.Add($"游戏版本 {GameVersions.Join("、"c)}")
-                If DownloadCount > 0 Then 'CurseForge 的下载次数经常错误地返回 0
-                    Info.Add("下载 " & If(DownloadCount > 100000, Math.Round(DownloadCount / 10000) & " 万次", DownloadCount & " 次"))
-                End If
-                Info.Add("更新于 " & GetTimeSpanString(ReleaseDate - Date.Now, False))
-                If Status <> CompFileStatus.Release Then Info.Add(StatusDescription)
-
-                '建立控件
-                Dim NewItem As New MyListItem With {
-                    .Title = Title,
-                    .SnapsToDevicePixels = True, .Height = 42, .Type = MyListItem.CheckType.Clickable, .Tag = Me,
-                    .Info = Info.Join(","c)
-                }
-                Select Case Status
-                    Case CompFileStatus.Release
-                        NewItem.Logo = PathImage & "Icons/R.png"
-                    Case CompFileStatus.Beta
-                        NewItem.Logo = PathImage & "Icons/B.png"
-                    Case Else 'Alpha
-                        NewItem.Logo = PathImage & "Icons/A.png"
-                End Select
-                AddHandler NewItem.Click, OnClick
-
-                '建立另存为按钮
-                If OnSaveClick IsNot Nothing Then
-                    Dim BtnSave As New MyIconButton With {.Logo = Logo.IconButtonSave, .ToolTip = "另存为"}
-                    ToolTipService.SetPlacement(BtnSave, Primitives.PlacementMode.Center)
-                    ToolTipService.SetVerticalOffset(BtnSave, 30)
-                    ToolTipService.SetHorizontalOffset(BtnSave, 2)
-                    AddHandler BtnSave.Click, OnSaveClick
-                    NewItem.Buttons = {BtnSave}
-                End If
-                Return NewItem
-            End Function) With {.Height = 42}
-        End Function
-
-        Public Overrides Function ToString() As String
-            Return $"{Id}: {FileName}"
-        End Function
-    End Class
-
-    '获取
-
-    ''' <summary>
-    ''' 已知文件信息的缓存。
-    ''' </summary>
-    Public CompFilesCache As New Dictionary(Of String, List(Of CompFile))
-    ''' <summary>
-    ''' 获取某个工程下的全部文件列表。
-    ''' 必须在工作线程执行,失败会抛出异常。
-    ''' </summary>
-    Public Function CompFilesGet(ProjectId As String, FromCurseForge As Boolean) As List(Of CompFile)
-        '获取工程对象
-        Dim TargetProject As CompProject
-        If CompProjectCache.ContainsKey(ProjectId) Then '存在缓存
-            TargetProject = CompProjectCache(ProjectId)
-        ElseIf FromCurseForge Then 'CurseForge
-            TargetProject = New CompProject(DlModRequest("https://api.curseforge.com/v1/mods/" & ProjectId)("data"))
-        Else 'Modrinth
-            TargetProject = New CompProject(DlModRequest("https://api.modrinth.com/v2/project/" & ProjectId))
-        End If
-        '获取工程对象的文件列表
-        If Not CompFilesCache.ContainsKey(ProjectId) Then '有缓存也不能直接返回,这时候前置可能没获取(#5173)
-            Log("[Comp] 开始获取文件列表:" & ProjectId)
-            Dim ResultJsonArray As JArray = Nothing
-            If FromCurseForge Then
-                'CurseForge
-                For RetryCount As Integer = 0 To 2
-                    Dim ResultJson As JObject = DlModRequest($"https://api.curseforge.com/v1/mods/{ProjectId}/files?pageSize=" &
-                        (10000 + RetryCount)) '每次重试多请求一个文件,以避免触发 CDN 缓存
-                    'HMCL 一次性请求了 10000 个文件,虽然不知道会不会出问题但先这样吧……(#5522)
-                    '之前只请求一部分文件的方法备份如下:
-                    'If TargetProject.Type = CompType.Mod Then 'Mod 使用每个版本最新的文件
-                    '    ResultJsonArray = DlModRequest("https://api.curseforge.com/v1/mods/files", HttpMethod.Post, "{""fileIds"": [" & Join(TargetProject.CurseForgeFileIds, ",") & "]}", "application/json")("data")
-                    'Else '否则使用全部文件
-                    '    ResultJsonArray = DlModRequest($"https://api.curseforge.com/v1/mods/{ProjectId}/files?pageSize=999")("data")
-                    'End If
-                    If ResultJson("pagination")("resultCount").ToObject(Of Integer) = ResultJson("pagination")("totalCount").ToObject(Of Integer) Then
-                        ResultJsonArray = ResultJson("data")
-                        Exit For
-                    ElseIf RetryCount < 2 Then
-                        Log($"[Comp] CurseForge 返回的文件列表存在缺失,即将进行第 {RetryCount + 1} 次重试", LogLevel.Debug) '#6224
-                        Log($"[Comp] 返回的原始内容如下:{vbCrLf}{ResultJson}")
-                    Else
-                        Log($"[Comp] CurseForge 返回的文件列表存在缺失,返回的原始内容如下:{vbCrLf}{ResultJson}")
-                        Throw New Exception("CurseForge 返回的文件列表存在缺失")
-                    End If
-                Next
-            Else
-                'Modrinth
-                ResultJsonArray = DlModRequest($"https://api.modrinth.com/v2/project/{ProjectId}/version?include_changelog=false")
-            End If
-            CompFilesCache(ProjectId) = ResultJsonArray.Select(Function(a) New CompFile(a, TargetProject.Types)).
-                Where(Function(a) a.Available).ToList.
-                Distinct(Function(a, b) a.Id = b.Id) 'CurseForge 可能会重复返回相同项(#1330)
-        End If
-        '获取前置列表
-        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
-        '获取前置工程信息
-        If UndoneDeps.Any Then
-            Log($"[Comp] {ProjectId} 文件列表中还需要获取信息的前置:{UndoneDeps.Join(","c)}")
-            Dim Projects As JArray
-            If TargetProject.FromCurseForge Then
-                Projects = DlModRequest("https://api.curseforge.com/v1/mods",
-                    HttpMethod.Post, "{""modIds"": [" & UndoneDeps.Join(","c) & "]}", "application/json")("data")
-            Else
-                Projects = DlModRequest($"https://api.modrinth.com/v2/projects?ids=[""{UndoneDeps.Join(""",""")}""]")
-            End If
-            For Each Project In Projects
-                Dim Unused As New CompProject(Project) '在 New 的时候会添加缓存以便之后读取
-            Next
-        End If
-        '更新前置信息
-        If Deps.Any Then
-            For Each DepProject In Deps.Where(Function(id) CompProjectCache.ContainsKey(id)).Select(Function(id) CompProjectCache(id))
-                For Each File In CompFilesCache(ProjectId)
-                    If File.RawDependencies.Contains(DepProject.Id) AndAlso DepProject.Id <> ProjectId Then
-                        If Not File.Dependencies.Contains(DepProject.Id) Then File.Dependencies.Add(DepProject.Id)
-                    End If
-                Next
-            Next
-        End If
-        Return CompFilesCache(ProjectId)
-    End Function
-
-    ''' <summary>
-    ''' 预载包含大量 CompFile 的卡片,添加必要的元素和前置列表。
-    ''' </summary>
-    Public Sub CompFilesCardPreload(Stack As StackPanel, Files As List(Of CompFile))
-        '获取卡片对应的前置 ID
-        '如果为整合包就不会有 Dependencies 信息,所以不用管
-        Dim Deps As List(Of String) = Files.SelectMany(Function(f) f.Dependencies).Distinct.ToList()
-        Deps.Sort()
-        If Not Deps.Any() Then Return
-        Deps = Deps.Where(
-        Function(dep)
-            If Not CompProjectCache.ContainsKey(dep) Then Log($"[Comp] 未找到 ID {dep} 的前置信息", LogLevel.Debug)
-            Return CompProjectCache.ContainsKey(dep)
-        End Function).ToList
-        '添加开头间隔
-        Stack.Children.Add(New TextBlock With {.Text = "前置资源", .FontSize = 14, .HorizontalAlignment = HorizontalAlignment.Left, .Margin = New Thickness(6, 2, 0, 5)})
-        '添加前置列表
-        For Each Dep In Deps
-            Dim Item = CompProjectCache(Dep).ToCompItem(False, False)
-            Stack.Children.Add(Item)
-        Next
-        '添加结尾间隔
-        Stack.Children.Add(New TextBlock With {.Text = "版本列表", .FontSize = 14, .HorizontalAlignment = HorizontalAlignment.Left, .Margin = New Thickness(6, 12, 0, 5)})
-    End Sub
-
-#End Region
-
-End Module

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

@@ -7,8 +7,8 @@
         Me.TargetInstance = TargetInstance
         Me.TargetInstance = TargetInstance
         '构建文件结构
         '构建文件结构
         TempFolder = RequestTaskTempFolder()
         TempFolder = RequestTaskTempFolder()
-        Directory.CreateDirectory(TempFolder & "Temp\")
-        Directory.CreateDirectory(TempFolder & "Report\")
+        DirectoryUtils.Create(TempFolder & "Temp\")
+        DirectoryUtils.Create(TempFolder & "Report\")
         Log("[Crash] 崩溃分析暂存文件夹:" & TempFolder)
         Log("[Crash] 崩溃分析暂存文件夹:" & TempFolder)
     End Sub
     End Sub
 
 
@@ -81,7 +81,7 @@
         If LatestLog IsNot Nothing AndAlso LatestLog.Any Then
         If LatestLog IsNot Nothing AndAlso LatestLog.Any Then
             Dim RawOutput As String = LatestLog.Join(vbCrLf)
             Dim RawOutput As String = LatestLog.Join(vbCrLf)
             Log("[Crash] 以下为游戏输出的最后一段内容:" & vbCrLf & RawOutput)
             Log("[Crash] 以下为游戏输出的最后一段内容:" & vbCrLf & RawOutput)
-            WriteFile(TempFolder & "RawOutput.log", RawOutput)
+            FileUtils.Write(TempFolder & "RawOutput.log", RawOutput)
             AnalyzeRawFiles.Add(New KeyValuePair(Of String, String())(TempFolder & "RawOutput.log", LatestLog.ToArray))
             AnalyzeRawFiles.Add(New KeyValuePair(Of String, String())(TempFolder & "RawOutput.log", LatestLog.ToArray))
             LatestLog.Clear()
             LatestLog.Clear()
         End If
         End If
@@ -573,7 +573,7 @@ Done:
             'Mod 导致的崩溃
             'Mod 导致的崩溃
             If LogCrash.Contains("-- MOD ") Then
             If LogCrash.Contains("-- MOD ") Then
                 Dim LogCrashMod As String = LogCrash.Between("-- MOD ", "Failure message:")
                 Dim LogCrashMod As String = LogCrash.Between("-- MOD ", "Failure message:")
-                If LogCrashMod.ContainsF(".jar", True) Then
+                If LogCrashMod.ContainsIgnoreCase(".jar") Then
                     AppendReason(CrashReason.确定Mod导致游戏崩溃, If(RegexSeek(LogCrashMod, "(?<=Mod File: ).+"), "").TrimEnd((vbCrLf & " ").ToCharArray))
                     AppendReason(CrashReason.确定Mod导致游戏崩溃, If(RegexSeek(LogCrashMod, "(?<=Mod File: ).+"), "").TrimEnd((vbCrLf & " ").ToCharArray))
                 Else
                 Else
                     AppendReason(CrashReason.Mod加载器报错, If(RegexSeek(LogCrash, "(?<=Failure message: )[\w\W]+?(?=\tMod)"), "").Replace(vbTab, " ").TrimEnd((vbCrLf & " ").ToCharArray))
                     AppendReason(CrashReason.Mod加载器报错, If(RegexSeek(LogCrash, "(?<=Failure message: )[\w\W]+?(?=\tMod)"), "").Replace(vbTab, " ").TrimEnd((vbCrLf & " ").ToCharArray))
@@ -764,7 +764,7 @@ NextStack:
             '[Fabric] 获取所有包含 Mod 信息的行
             '[Fabric] 获取所有包含 Mod 信息的行
             Dim ModNameLines As New List(Of String)
             Dim ModNameLines As New List(Of String)
             For Each Line In Details.Split(vbLf)
             For Each Line In Details.Split(vbLf)
-                If (Line.ContainsF(".jar", True) AndAlso Line.Length - Line.Replace(".jar", "").Length = 4) OrElse '只有一个 .jar
+                If (Line.ContainsIgnoreCase(".jar") AndAlso Line.Length - Line.Replace(".jar", "").Length = 4) OrElse '只有一个 .jar
                    (IsFabricDetail AndAlso Line.StartsWithF(vbTab & vbTab) AndAlso Not RegexCheck(Line, "\t\tfabric[\w-]*: Fabric")) Then ModNameLines.Add(Line)
                    (IsFabricDetail AndAlso Line.StartsWithF(vbTab & vbTab) AndAlso Not RegexCheck(Line, "\t\tfabric[\w-]*: Fabric")) Then ModNameLines.Add(Line)
             Next
             Next
             Log("[Crash] 崩溃报告中找到 " & ModNameLines.Count & " 个可能的 Mod 项目行")
             Log("[Crash] 崩溃报告中找到 " & ModNameLines.Count & " 个可能的 Mod 项目行")
@@ -874,7 +874,7 @@ NextStack:
                     StartProcess(DirectFile.Value.Key)
                     StartProcess(DirectFile.Value.Key)
                 Else
                 Else
                     Dim FilePath As String = PathTemp & "Crash.txt"
                     Dim FilePath As String = PathTemp & "Crash.txt"
-                    WriteFile(FilePath, DirectFile.Value.Value.Join(vbCrLf))
+                    FileUtils.Write(FilePath, DirectFile.Value.Value.Join(vbCrLf))
                     StartProcess(FilePath)
                     StartProcess(FilePath)
                 End If
                 End If
             End Sub))
             End Sub))
@@ -885,8 +885,8 @@ NextStack:
                     '获取文件路径
                     '获取文件路径
                     RunInUiWait(Sub() FileAddress = SelectSaveFile("选择保存位置", "错误报告-" & Date.Now.ToString("G").Replace("/", "-").Replace(":", ".").Replace(" ", "_") & ".zip", "Minecraft 错误报告(*.zip)|*.zip"))
                     RunInUiWait(Sub() FileAddress = SelectSaveFile("选择保存位置", "错误报告-" & Date.Now.ToString("G").Replace("/", "-").Replace(":", ".").Replace(" ", "_") & ".zip", "Minecraft 错误报告(*.zip)|*.zip"))
                     If String.IsNullOrEmpty(FileAddress) Then Return
                     If String.IsNullOrEmpty(FileAddress) Then Return
-                    Directory.CreateDirectory(GetPathFromFullPath(FileAddress))
                     If File.Exists(FileAddress) Then File.Delete(FileAddress)
                     If File.Exists(FileAddress) Then File.Delete(FileAddress)
+                    DirectoryUtils.Create(FileAddress, isFilePath:=True)
                     '输出诊断信息
                     '输出诊断信息
                     FeedbackInfo()
                     FeedbackInfo()
                     LogFlush()
                     LogFlush()
@@ -909,7 +909,7 @@ NextStack:
                             If FileEncoding Is Nothing Then FileEncoding = GetEncoding(ReadFileBytes(OutputFile))
                             If FileEncoding Is Nothing Then FileEncoding = GetEncoding(ReadFileBytes(OutputFile))
                             Dim FileContent As String = ReadFile(OutputFile, FileEncoding)
                             Dim FileContent As String = ReadFile(OutputFile, FileEncoding)
                             FileContent = FilterUserName(FilterAccessToken(FileContent, If(FileName = "启动脚本.bat", "F", "*")), "*")
                             FileContent = FilterUserName(FilterAccessToken(FileContent, If(FileName = "启动脚本.bat", "F", "*")), "*")
-                            WriteFile(TempFolder & "Report\" & FileName, FileContent, Encoding:=FileEncoding)
+                            FileUtils.Write(TempFolder & "Report\" & FileName, FileContent, Encoding:=FileEncoding)
                             Log($"[Crash] 导出文件:{FileName},编码:{FileEncoding.HeaderName}")
                             Log($"[Crash] 导出文件:{FileName},编码:{FileEncoding.HeaderName}")
                         End If
                         End If
                     Next
                     Next
@@ -918,7 +918,7 @@ NextStack:
                     DeleteDirectory(TempFolder & "Report\")
                     DeleteDirectory(TempFolder & "Report\")
                     Hint("错误报告已导出!", HintType.Green)
                     Hint("错误报告已导出!", HintType.Green)
                 Catch ex As Exception
                 Catch ex As Exception
-                    Log(ex, "导出错误报告失败", LogLevel.Feedback)
+                    Log(ex, "导出错误报告失败", NotifyLevel.MsgBoxAndFeedback)
                     Return
                     Return
                 End Try
                 End Try
                 OpenExplorer(FileAddress)
                 OpenExplorer(FileAddress)
@@ -1117,7 +1117,7 @@ NextStack:
                     Replace("\n", vbCrLf).
                     Replace("\n", vbCrLf).
                     Replace("\h", "").
                     Replace("\h", "").
                     Replace("\e", If(IsHandAnalyze, "", vbCrLf & "你可以查看错误报告了解错误具体是如何发生的。")).
                     Replace("\e", If(IsHandAnalyze, "", vbCrLf & "你可以查看错误报告了解错误具体是如何发生的。")).
-                    Replace(vbCrLf, vbCr).Replace(vbLf, vbCr).Replace(vbCr, vbCrLf).
+                    ReplaceLineEndings(vbCrLf).
                     Trim(vbCrLf.ToCharArray) &
                     Trim(vbCrLf.ToCharArray) &
                 If(Not Results.Any(Function(r) r.EndsWithF("\h")) OrElse IsHandAnalyze, "",
                 If(Not Results.Any(Function(r) r.EndsWithF("\h")) OrElse IsHandAnalyze, "",
                     vbCrLf & "如果要寻求帮助,请把错误报告文件发给对方,而不是发送这个窗口的照片或者截图。" &
                     vbCrLf & "如果要寻求帮助,请把错误报告文件发给对方,而不是发送这个窗口的照片或者截图。" &

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

@@ -75,7 +75,7 @@
                     Dim IndexFile = DlClientAssetIndexGet(Instance)
                     Dim IndexFile = DlClientAssetIndexGet(Instance)
                     If IndexFile Is Nothing Then
                     If IndexFile Is Nothing Then
                         Task.Output = New List(Of NetFile)
                         Task.Output = New List(Of NetFile)
-                        Log("[Download] 未找到版本 " & Instance.Name & " 的合适的资源索引下载地址,游戏 assets 可能缺失", LogLevel.Debug)
+                        Log("[Download] 未找到版本 " & Instance.Name & " 的合适的资源索引下载地址,游戏 assets 可能缺失", NotifyLevel.DebugModeOnly)
                     ElseIf AssetsIndexBehaviour <> AssetsIndexExistsBehaviour.AlwaysDownload AndAlso IndexFile.Check.Check(IndexFile.LocalPath) Is Nothing Then
                     ElseIf AssetsIndexBehaviour <> AssetsIndexExistsBehaviour.AlwaysDownload AndAlso IndexFile.Check.Check(IndexFile.LocalPath) Is Nothing Then
                         Task.Output = New List(Of NetFile)
                         Task.Output = New List(Of NetFile)
                     Else
                     Else
@@ -96,7 +96,7 @@
                 Sub(Task As LoaderTask(Of String, List(Of NetFile)))
                 Sub(Task As LoaderTask(Of String, List(Of NetFile)))
                     Dim BackAssetsFile As NetFile = DlClientAssetIndexGet(Instance)
                     Dim BackAssetsFile As NetFile = DlClientAssetIndexGet(Instance)
                     If BackAssetsFile Is Nothing Then
                     If BackAssetsFile Is Nothing Then
-                        Log("[Download] 未找到版本 " & Instance.Name & " 的合适的资源索引下载地址,游戏 assets 可能缺失", LogLevel.Debug)
+                        Log("[Download] 未找到版本 " & Instance.Name & " 的合适的资源索引下载地址,游戏 assets 可能缺失", NotifyLevel.DebugModeOnly)
                         Task.Interrupt()
                         Task.Interrupt()
                         Throw New ThreadInterruptedException
                         Throw New ThreadInterruptedException
                     End If
                     End If
@@ -313,7 +313,7 @@
             For Each Version As JObject In DlClientListLoader.Output.Value("versions")
             For Each Version As JObject In DlClientListLoader.Output.Value("versions")
                 If Version("id") = Id Then Return Version("url").ToString
                 If Version("id") = Id Then Return Version("url").ToString
             Next
             Next
-            Log($"未发现版本 {Id} 的 json 下载地址,版本列表返回为:{vbCrLf}{DlClientListLoader.Output.Value.ToString}", LogLevel.Debug)
+            Log($"未发现版本 {Id} 的 json 下载地址,版本列表返回为:{vbCrLf}{DlClientListLoader.Output.Value.ToString}", NotifyLevel.DebugModeOnly)
             Return Nothing
             Return Nothing
         Catch ex As Exception
         Catch ex As Exception
             Log(ex, $"获取版本 {Id} 的 json 下载地址失败")
             Log(ex, $"获取版本 {Id} 的 json 下载地址失败")
@@ -426,9 +426,9 @@
                 Dim Entry As New DlOptiFineListEntry With {
                 Dim Entry As New DlOptiFineListEntry With {
                     .DisplayName = Name(i).Replace("HD U ", "").Replace(".0 ", " "),
                     .DisplayName = Name(i).Replace("HD U ", "").Replace(".0 ", " "),
                     .ReleaseTime = {ReleaseTime(i).Split(".")(2), ReleaseTime(i).Split(".")(1), ReleaseTime(i).Split(".")(0)}.Join("/"c),
                     .ReleaseTime = {ReleaseTime(i).Split(".")(2), ReleaseTime(i).Split(".")(1), ReleaseTime(i).Split(".")(0)}.Join("/"c),
-                    .IsPreview = Name(i).ContainsF("pre", True),
+                    .IsPreview = Name(i).ContainsIgnoreCase("pre"),
                     .Inherit = Name(i).ToString.Split(" ")(0),
                     .Inherit = Name(i).ToString.Split(" ")(0),
-                    .FileName = If(Name(i).ContainsF("pre", True), "preview_", "") & "OptiFine_" & Name(i).Replace(" ", "_") & ".jar",
+                    .FileName = If(Name(i).ContainsIgnoreCase("pre"), "preview_", "") & "OptiFine_" & Name(i).Replace(" ", "_") & ".jar",
                     .RequiredForgeVersion = Forge(i).Replace("Forge ", "").Replace("#", "")}
                     .RequiredForgeVersion = Forge(i).Replace("Forge ", "").Replace("#", "")}
                 If Entry.RequiredForgeVersion.Contains("N/A") Then Entry.RequiredForgeVersion = Nothing
                 If Entry.RequiredForgeVersion.Contains("N/A") Then Entry.RequiredForgeVersion = Nothing
                 Entry.InstanceName = Entry.Inherit & "-OptiFine_" & Name(i).ToString.Replace(" ", "_").Replace(Entry.Inherit & "_", "")
                 Entry.InstanceName = Entry.Inherit & "-OptiFine_" & Name(i).ToString.Replace(" ", "_").Replace(Entry.Inherit & "_", "")
@@ -452,7 +452,7 @@
                 Dim Entry As New DlOptiFineListEntry With {
                 Dim Entry As New DlOptiFineListEntry With {
                     .DisplayName = (Token("mcversion").ToString & Token("type").ToString.Replace("HD_U", "").Replace("_", " ") & " " & Token("patch").ToString).Replace(".0 ", " "),
                     .DisplayName = (Token("mcversion").ToString & Token("type").ToString.Replace("HD_U", "").Replace("_", " ") & " " & Token("patch").ToString).Replace(".0 ", " "),
                     .ReleaseTime = "",
                     .ReleaseTime = "",
-                    .IsPreview = Token("patch").ToString.ContainsF("pre", True),
+                    .IsPreview = Token("patch").ToString.ContainsIgnoreCase("pre"),
                     .Inherit = Token("mcversion").ToString,
                     .Inherit = Token("mcversion").ToString,
                     .FileName = Token("filename").ToString,
                     .FileName = Token("filename").ToString,
                     .RequiredForgeVersion = If(Token("forge"), "").ToString.Replace("Forge ", "").Replace("#", "")
                     .RequiredForgeVersion = If(Token("forge"), "").ToString.Replace("Forge ", "").Replace("#", "")
@@ -679,7 +679,7 @@
         Dim Versions As New List(Of DlForgeVersionEntry)
         Dim Versions As New List(Of DlForgeVersionEntry)
         Try
         Try
             '分割版本信息
             '分割版本信息
-            Dim VersionCodes = Mid(Result, 1, Result.LastIndexOfF("</table>")).Split("<td class=""download-version")
+            Dim VersionCodes = Result.Substring(0, Result.LastIndexOfF("</table>")).Split("<td class=""download-version")
             '获取所有版本信息
             '获取所有版本信息
             For i = 1 To VersionCodes.Count - 1
             For i = 1 To VersionCodes.Count - 1
                 Dim VersionCode = VersionCodes(i)
                 Dim VersionCode = VersionCodes(i)
@@ -720,7 +720,7 @@
                         Continue For
                         Continue For
                     End If
                     End If
                     '添加进列表
                     '添加进列表
-                    Versions.Add(New DlForgeVersionEntry(Name, Branch, Inherit) With {.Category = Category, .IsRecommended = IsRecommended, .Hash = MD5.Trim(vbCr, vbLf), .ReleaseTime = ReleaseTime})
+                    Versions.Add(New DlForgeVersionEntry(Name, Branch, Inherit) With {.Category = Category, .IsRecommended = IsRecommended, .Hash = MD5.ReplaceLineEndings(""), .ReleaseTime = ReleaseTime})
                 Catch ex As Exception
                 Catch ex As Exception
                     Throw New Exception("Forge 官方源版本信息提取失败(" & VersionCode & ")", ex)
                     Throw New Exception("Forge 官方源版本信息提取失败(" & VersionCode & ")", ex)
                 End Try
                 End Try
@@ -846,7 +846,7 @@
         Public Sub New(ApiName As String)
         Public Sub New(ApiName As String)
             IsNeoForge = True
             IsNeoForge = True
             Me.ApiName = ApiName
             Me.ApiName = ApiName
-            IsBeta = ApiName.ContainsF("beta", True) OrElse ApiName.ContainsF("alpha", True)
+            IsBeta = ApiName.ContainsIgnoreCase("beta") OrElse ApiName.ContainsIgnoreCase("alpha")
             If ApiName.Contains("1.20.1") Then '1.20.1-47.1.99
             If ApiName.Contains("1.20.1") Then '1.20.1-47.1.99
                 VersionName = ApiName.Replace("1.20.1-", "")
                 VersionName = ApiName.Replace("1.20.1-", "")
                 Version = New Version("19." & VersionName)
                 Version = New Version("19." & VersionName)
@@ -1151,14 +1151,14 @@
     ''' <summary>
     ''' <summary>
     ''' Fabric API 列表,官方源。
     ''' Fabric API 列表,官方源。
     ''' </summary>
     ''' </summary>
-    Public DlFabricApiLoader As New LoaderTask(Of Integer, List(Of CompFile))("Fabric API List Loader",
-        Sub(Task As LoaderTask(Of Integer, List(Of CompFile))) Task.Output = CompFilesGet("fabric-api", False))
+    Public DlFabricApiLoader As New LoaderTask(Of Integer, List(Of ResourceVersion))("Fabric API List Loader",
+        Sub(Task As LoaderTask(Of Integer, List(Of ResourceVersion))) Task.Output = ResourceVersion.FromProjectId("fabric-api", ResourcePlatforms.Modrinth, LoadDependencies:=False))
 
 
     ''' <summary>
     ''' <summary>
     ''' OptiFabric 列表,官方源。
     ''' OptiFabric 列表,官方源。
     ''' </summary>
     ''' </summary>
-    Public DlOptiFabricLoader As New LoaderTask(Of Integer, List(Of CompFile))("OptiFabric List Loader",
-        Sub(Task As LoaderTask(Of Integer, List(Of CompFile))) Task.Output = CompFilesGet("322385", True))
+    Public DlOptiFabricLoader As New LoaderTask(Of Integer, List(Of ResourceVersion))("OptiFabric List Loader",
+        Sub(Task As LoaderTask(Of Integer, List(Of ResourceVersion))) Task.Output = ResourceVersion.FromProjectId("322385", ResourcePlatforms.CurseForge, LoadDependencies:=False))
 
 
 #End Region
 #End Region
 
 

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

@@ -54,7 +54,7 @@
         Public ReadOnly Property HasEnvironment As Boolean
         Public ReadOnly Property HasEnvironment As Boolean
             Get
             Get
                 If PathFolder Is Nothing OrElse PathEnv Is Nothing Then Return False
                 If PathFolder Is Nothing OrElse PathEnv Is Nothing Then Return False
-                Return PathEnv.Replace("\", "").Replace("/", "").ContainsF(PathFolder.Replace("\", ""), True)
+                Return PathEnv.Replace("\", "").Replace("/", "").ContainsIgnoreCase(PathFolder.Replace("\", ""))
             End Get
             End Get
         End Property
         End Property
 
 
@@ -70,7 +70,7 @@
         ''' </summary>
         ''' </summary>
         Public Overrides Function ToString() As String
         Public Overrides Function ToString() As String
             Dim VersionString = Version.ToString
             Dim VersionString = Version.ToString
-            If VersionString.StartsWithF("1.") Then VersionString = Mid(VersionString, 3)
+            If VersionString.StartsWithF("1.") Then VersionString = VersionString.Substring(2)
             Return If(IsJre, "JRE ", "JDK ") & MajorVersion & " (" & VersionString & ")" & If(Is64Bit, "", ",32 位") & If(IsUserImport, ",手动导入", "") & ":" & PathFolder
             Return If(IsJre, "JRE ", "JDK ") & MajorVersion & " (" & VersionString & ")" & If(Is64Bit, "", ",32 位") & If(IsUserImport, ",手动导入", "") & ":" & PathFolder
         End Function
         End Function
 
 
@@ -95,7 +95,7 @@
             Try
             Try
                 '确定文件存在
                 '确定文件存在
                 If Not File.Exists(PathJava) Then Throw New FileNotFoundException("未找到 java.exe 文件", PathJava)
                 If Not File.Exists(PathJava) Then Throw New FileNotFoundException("未找到 java.exe 文件", PathJava)
-                If {"finalshell", "Paranoia File"}.Any(Function(n) PathJava.ContainsF(n, True)) Then Throw New Exception("不兼容该精简版 Java") '#2249、#8080
+                If {"finalshell", "Paranoia File"}.Any(Function(n) PathJava.ContainsIgnoreCase(n)) Then Throw New Exception("不兼容该精简版 Java") '#2249、#8080
                 If File.Exists(PathFolder & "pdf-bookmark") Then Throw New Exception("不兼容 PDF Bookmark 的 Java") '#5326
                 If File.Exists(PathFolder & "pdf-bookmark") Then Throw New Exception("不兼容 PDF Bookmark 的 Java") '#5326
                 IsJre = Not File.Exists(PathFolder & "javac.exe")
                 IsJre = Not File.Exists(PathFolder & "javac.exe")
                 '运行 -version
                 '运行 -version
@@ -176,14 +176,14 @@
                 Next
                 Next
             End If
             End If
             If Not JavaList.Any() Then
             If Not JavaList.Any() Then
-                Log("[Java] 初始化未找到可用的 Java,将自动触发搜索", LogLevel.Developer)
+                Log("[Java] 初始化未找到可用的 Java,将自动触发搜索", NotifyLevel.DevelopOnly)
                 JavaSearchLoader.Start(0)
                 JavaSearchLoader.Start(0)
             Else
             Else
                 Log("[Java] 缓存中有 " & JavaList.Count & " 个可用的 Java:")
                 Log("[Java] 缓存中有 " & JavaList.Count & " 个可用的 Java:")
                 JavaList.ForEach(Sub(j) Log($"[Java]  - {j}"))
                 JavaList.ForEach(Sub(j) Log($"[Java]  - {j}"))
             End If
             End If
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "初始化 Java 列表失败", LogLevel.Feedback)
+            Log(ex, "初始化 Java 列表失败", NotifyLevel.MsgBoxAndFeedback)
             Settings.Set("LaunchArgumentJavaAll", "[]")
             Settings.Set("LaunchArgumentJavaAll", "[]")
         End Try
         End Try
     End Sub
     End Sub
@@ -242,7 +242,7 @@
                     Catch ex As Exception
                     Catch ex As Exception
                         UserJava = Nothing
                         UserJava = Nothing
                         Settings.Reset("VersionArgumentJavaSelect", Instance:=GameInstance)
                         Settings.Reset("VersionArgumentJavaSelect", Instance:=GameInstance)
-                        Log(ex, "版本独立设置中指定的 Java 已无法使用,此设置已重置", LogLevel.Hint)
+                        Log(ex, "版本独立设置中指定的 Java 已无法使用,此设置已重置", NotifyLevel.AllUsers)
                     End Try
                     End Try
                 End If
                 End If
             End If
             End If
@@ -257,7 +257,7 @@
                 Catch ex As Exception
                 Catch ex As Exception
                     UserJava = Nothing
                     UserJava = Nothing
                     Settings.Reset("LaunchArgumentJavaSelect")
                     Settings.Reset("LaunchArgumentJavaSelect")
-                    Log(ex, "全局设置中指定的 Java 已无法使用,此设置已重置", LogLevel.Hint)
+                    Log(ex, "全局设置中指定的 Java 已无法使用,此设置已重置", NotifyLevel.AllUsers)
                 End Try
                 End Try
             End If
             End If
 
 
@@ -332,7 +332,7 @@ RetryGet:
             Dim JavaCurrent As String = UserJava.MajorVersion & If(ShowRevision, "." & UserJava.Version.MajorRevision & "." & UserJava.Version.MinorRevision, "")
             Dim JavaCurrent As String = UserJava.MajorVersion & If(ShowRevision, "." & UserJava.Version.MajorRevision & "." & UserJava.Version.MinorRevision, "")
             If GameInstance IsNot Nothing AndAlso Settings.Get("VersionAdvanceJava", GameInstance) Then
             If GameInstance IsNot Nothing AndAlso Settings.Get("VersionAdvanceJava", GameInstance) Then
                 '直接跳过弹窗
                 '直接跳过弹窗
-                Log("[Java] 设置中指定了使用 Java " & JavaCurrent & ",但当前版本" & Requirement & ",这可能会导致游戏崩溃!", LogLevel.Debug)
+                Log("[Java] 设置中指定了使用 Java " & JavaCurrent & ",但当前版本" & Requirement & ",这可能会导致游戏崩溃!", NotifyLevel.DebugModeOnly)
                 AllowedJavaList = New List(Of JavaEntry) From {UserJava}
                 AllowedJavaList = New List(Of JavaEntry) From {UserJava}
             Else
             Else
                 Select Case MyMsgBox("你在设置中手动指定了使用 Java " & JavaCurrent & ",但当前" & Requirement & "。" & vbCrLf &
                 Select Case MyMsgBox("你在设置中手动指定了使用 Java " & JavaCurrent & ",但当前" & Requirement & "。" & vbCrLf &
@@ -400,7 +400,7 @@ UserPass:
             Return Nothing
             Return Nothing
         Catch ex As Exception
         Catch ex As Exception
             If ex.Message = "$$" Then Throw ex
             If ex.Message = "$$" Then Throw ex
-            Log(ex, "查找符合条件的 Java 失败", LogLevel.Feedback)
+            Log(ex, "查找符合条件的 Java 失败", NotifyLevel.MsgBoxAndFeedback)
             Return Nothing
             Return Nothing
         End Try
         End Try
     End Function
     End Function
@@ -435,7 +435,7 @@ NoUserJava:
             Next
             Next
             Return False
             Return False
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "检查 Java 类别时出错", LogLevel.Feedback)
+            Log(ex, "检查 Java 类别时出错", NotifyLevel.MsgBoxAndFeedback)
             Settings.Set("LaunchArgumentJavaSelect", "")
             Settings.Set("LaunchArgumentJavaSelect", "")
             Return True
             Return True
         End Try
         End Try
@@ -444,7 +444,7 @@ NoUserJava:
     ''' 将 Java 按照适用性排序。
     ''' 将 Java 按照适用性排序。
     ''' </summary>
     ''' </summary>
     Public Function JavaSorter(Left As JavaEntry, Right As JavaEntry) As Boolean
     Public Function JavaSorter(Left As JavaEntry, Right As JavaEntry) As Boolean
-        Dim PathInfo As New DirectoryInfo(Path)
+        Dim PathInfo As New DirectoryInfo(PathExeFolder)
         Dim PathMcInfo As New DirectoryInfo(McFolderSelected)
         Dim PathMcInfo As New DirectoryInfo(McFolderSelected)
         '1. 尽量在当前文件夹或当前 Minecraft 文件夹
         '1. 尽量在当前文件夹或当前 Minecraft 文件夹
         Dim ProgramPathParent As String, MinecraftPathParent As String = ""
         Dim ProgramPathParent As String, MinecraftPathParent As String = ""
@@ -522,9 +522,9 @@ NoUserJava:
             JavaSearchFolder(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) & "\.jdks\", JavaPreList, False, IsFullSearch:=True)
             JavaSearchFolder(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) & "\.jdks\", JavaPreList, False, IsFullSearch:=True)
             JavaSearchFolder(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) & "\.sdkman\candidates\java\", JavaPreList, False, IsFullSearch:=True)
             JavaSearchFolder(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) & "\.sdkman\candidates\java\", JavaPreList, False, IsFullSearch:=True)
             '查找启动器目录中的 Java
             '查找启动器目录中的 Java
-            JavaSearchFolder(Path, JavaPreList, False, IsFullSearch:=True)
+            JavaSearchFolder(PathExeFolder, JavaPreList, False, IsFullSearch:=True)
             '查找所选 Minecraft 文件夹中的 Java
             '查找所选 Minecraft 文件夹中的 Java
-            If Not String.IsNullOrWhiteSpace(McFolderSelected) AndAlso Path <> McFolderSelected Then JavaSearchFolder(McFolderSelected, JavaPreList, False, IsFullSearch:=True)
+            If Not String.IsNullOrWhiteSpace(McFolderSelected) AndAlso PathExeFolder <> McFolderSelected Then JavaSearchFolder(McFolderSelected, JavaPreList, False, IsFullSearch:=True)
 
 
             '若不全为符号链接,则清除符号链接的地址
             '若不全为符号链接,则清除符号链接的地址
             Dim JavaWithoutReparse As New Dictionary(Of String, Boolean)
             Dim JavaWithoutReparse As New Dictionary(Of String, Boolean)
@@ -546,7 +546,7 @@ NoUserJava:
             '若不全为特殊引用,则清除特殊引用的地址
             '若不全为特殊引用,则清除特殊引用的地址
             Dim JavaWithoutInherit As New Dictionary(Of String, Boolean)
             Dim JavaWithoutInherit As New Dictionary(Of String, Boolean)
             For Each Pair In JavaPreList
             For Each Pair In JavaPreList
-                If Pair.Key.Contains("java8path_target_") OrElse Pair.Key.Contains("javapath_target_") OrElse Pair.Key.Contains("javatmp") OrElse Pair.Key.ContainsF("system32") Then
+                If Pair.Key.Contains("java8path_target_") OrElse Pair.Key.Contains("javapath_target_") OrElse Pair.Key.Contains("javatmp") OrElse Pair.Key.Contains("system32") Then
                     Log("[Java] 位于 " & Pair.Key & " 的 Java 位于特殊路径,不应优先使用")
                     Log("[Java] 位于 " & Pair.Key & " 的 Java 位于特殊路径,不应优先使用")
                 Else
                 Else
                     JavaWithoutInherit.Add(Pair.Key, Pair.Value)
                     JavaWithoutInherit.Add(Pair.Key, Pair.Value)
@@ -565,7 +565,7 @@ NoUserJava:
                     If Entry.IsUserImport Then JavaPreList(Entry.PathFolder) = True
                     If Entry.IsUserImport Then JavaPreList(Entry.PathFolder) = True
                 Next
                 Next
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "Java 列表已损坏", LogLevel.Feedback)
+                Log(ex, "Java 列表已损坏", NotifyLevel.MsgBoxAndFeedback)
                 Settings.Set("LaunchArgumentJavaAll", "[]")
                 Settings.Set("LaunchArgumentJavaAll", "[]")
             End Try
             End Try
 
 
@@ -573,7 +573,7 @@ NoUserJava:
 
 
             '确保可用并获取详细信息,转入正式列表
             '确保可用并获取详细信息,转入正式列表
             Dim NewJavaList As New List(Of JavaEntry)
             Dim NewJavaList As New List(Of JavaEntry)
-            For Each Entry In JavaPreList.Distinct(Function(a, b) a.Key.Lower = b.Key.Lower) '#794
+            For Each Entry In JavaPreList.ToList.DistinctBy(Function(a) a.Key.Lower) '#794
                 NewJavaList.Add(New JavaEntry(Entry.Key, Entry.Value))
                 NewJavaList.Add(New JavaEntry(Entry.Key, Entry.Value))
             Next
             Next
             NewJavaList = JavaCheckList(NewJavaList).SortByComparison(AddressOf JavaSorter)
             NewJavaList = JavaCheckList(NewJavaList).SortByComparison(AddressOf JavaSorter)
@@ -587,7 +587,7 @@ NoUserJava:
             JavaList = NewJavaList
             JavaList = NewJavaList
 
 
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "搜索 Java 时出错", LogLevel.Feedback)
+            Log(ex, "搜索 Java 时出错", NotifyLevel.MsgBoxAndFeedback)
             JavaList = New List(Of JavaEntry)
             JavaList = New List(Of JavaEntry)
         End Try
         End Try
 
 
@@ -618,7 +618,7 @@ NoUserJava:
                 Catch ex As ThreadInterruptedException
                 Catch ex As ThreadInterruptedException
                 Catch ex As Exception
                 Catch ex As Exception
                     If Entry.IsUserImport Then
                     If Entry.IsUserImport Then
-                        Log(ex, "位于 " & Entry.PathFolder & " 的 Java 存在异常,将被自动移除", LogLevel.Hint)
+                        Log(ex, "位于 " & Entry.PathFolder & " 的 Java 存在异常,将被自动移除", NotifyLevel.AllUsers)
                     Else
                     Else
                         Log(ex, "位于 " & Entry.PathFolder & " 的 Java 存在异常")
                         Log(ex, "位于 " & Entry.PathFolder & " 的 Java 存在异常")
                     End If
                     End If
@@ -659,10 +659,10 @@ Wait:
         Try
         Try
             '确认目录存在
             '确认目录存在
             If Not OriginalPath.Exists Then Return
             If Not OriginalPath.Exists Then Return
-            Dim Path As String = OriginalPath.FullName.Replace("\\", "\")
-            If Not Path.EndsWithF("\") Then Path += "\"
+            Dim Folder As String = OriginalPath.FullName.Replace("\\", "\")
+            If Not Folder.EndsWithF("\") Then Folder += "\"
             '若该目录有 Java,则加入结果
             '若该目录有 Java,则加入结果
-            If File.Exists(Path & "javaw.exe") Then Results(Path) = Source
+            If File.Exists(Folder & "javaw.exe") Then Results(Folder) = Source
             '查找其下的所有文件夹
             '查找其下的所有文件夹
             '不应使用网易的 Java:https://github.com/Meloong-Git/PCL/issues/1279#issuecomment-2761489121
             '不应使用网易的 Java:https://github.com/Meloong-Git/PCL/issues/1279#issuecomment-2761489121
             Static Keywords As String() = {
             Static Keywords As String() = {
@@ -704,7 +704,7 @@ Wait:
         AddHandler JavaDownloadLoader.OnStateChangedThread,
         AddHandler JavaDownloadLoader.OnStateChangedThread,
         Sub(Raw As LoaderBase, NewState As LoadState, OldState As LoadState)
         Sub(Raw As LoaderBase, NewState As LoadState, OldState As LoadState)
             If (NewState = LoadState.Failed OrElse NewState = LoadState.Interrupted) AndAlso LastJavaBaseDir IsNot Nothing Then
             If (NewState = LoadState.Failed OrElse NewState = LoadState.Interrupted) AndAlso LastJavaBaseDir IsNot Nothing Then
-                Log($"[Java] 由于下载未完成,清理未下载完成的 Java 文件:{LastJavaBaseDir}", LogLevel.Debug)
+                Log($"[Java] 由于下载未完成,清理未下载完成的 Java 文件:{LastJavaBaseDir}", NotifyLevel.DebugModeOnly)
                 DeleteDirectory(LastJavaBaseDir)
                 DeleteDirectory(LastJavaBaseDir)
             ElseIf NewState = LoadState.Finished Then
             ElseIf NewState = LoadState.Finished Then
                 LastJavaBaseDir = Nothing
                 LastJavaBaseDir = Nothing

+ 57 - 61
Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb

@@ -1,6 +1,4 @@
-Imports System.IO.Compression
-
-Public Module ModLaunch
+Public Module ModLaunch
 
 
 #Region "开始"
 #Region "开始"
 
 
@@ -186,7 +184,7 @@ NextInner:
                 '没有特殊处理过的错误信息
                 '没有特殊处理过的错误信息
                 McLaunchLog("错误:" & ex.GetDetail())
                 McLaunchLog("错误:" & ex.GetDetail())
                 Log(ex,
                 Log(ex,
-                    If(IsSavingBatch, "导出启动脚本失败", "Minecraft 启动失败"), LogLevel.Msgbox,
+                    If(IsSavingBatch, "导出启动脚本失败", "Minecraft 启动失败"), NotifyLevel.MsgBox,
                     If(IsSavingBatch, "导出启动脚本失败", "启动失败"))
                     If(IsSavingBatch, "导出启动脚本失败", "启动失败"))
                 '上报
                 '上报
                 If Not IsSavingBatch Then
                 If Not IsSavingBatch Then
@@ -239,9 +237,8 @@ NextInner:
         Dim CheckResult As String = ""
         Dim CheckResult As String = ""
         RunInUiWait(Sub() CheckResult = McLoginAble(McLoginInput()))
         RunInUiWait(Sub() CheckResult = McLoginAble(McLoginInput()))
         If CheckResult <> "" Then Throw New ArgumentException(CheckResult)
         If CheckResult <> "" Then Throw New ArgumentException(CheckResult)
-#If BETA Then
         '求赞助
         '求赞助
-        If CurrentLaunchOptions?.SaveBatch Is Nothing Then '保存脚本时不提示
+        If BuildType = BuildTypes.Release AndAlso CurrentLaunchOptions?.SaveBatch Is Nothing Then '保存脚本时不提示
             RunInNewThread(
             RunInNewThread(
             Sub()
             Sub()
                 Select Case Settings.Get("SystemLaunchCount")
                 Select Case Settings.Get("SystemLaunchCount")
@@ -255,7 +252,6 @@ NextInner:
                 End Select
                 End Select
             End Sub, "Donate")
             End Sub, "Donate")
         End If
         End If
-#End If
         '正版购买提示
         '正版购买提示
         If CurrentLaunchOptions?.SaveBatch Is Nothing AndAlso '保存脚本时不提示
         If CurrentLaunchOptions?.SaveBatch Is Nothing AndAlso '保存脚本时不提示
            Not Settings.Get("HintBuy") AndAlso Settings.Get("LoginType") <> McLoginType.Ms Then
            Not Settings.Get("HintBuy") AndAlso Settings.Get("LoginType") <> McLoginType.Ms Then
@@ -496,7 +492,7 @@ NextInner:
                     End If
                     End If
             End Select
             End Select
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "获取登录输入信息失败(" & GetStringFromEnum(LoginType) & ")", LogLevel.Feedback)
+            Log(ex, "获取登录输入信息失败(" & GetStringFromEnum(LoginType) & ")", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
         Return LoginData
         Return LoginData
     End Function
     End Function
@@ -665,7 +661,7 @@ LoginFinish:
             Settings.Set("Login" & Input.Token & "Email", Emails.Join("¨"c))
             Settings.Set("Login" & Input.Token & "Email", Emails.Join("¨"c))
             Settings.Set("Login" & Input.Token & "Pass", Passwords.Join("¨"c))
             Settings.Set("Login" & Input.Token & "Pass", Passwords.Join("¨"c))
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "保存启动记录失败", LogLevel.Hint)
+            Log(ex, "保存启动记录失败", NotifyLevel.AllUsers)
             Settings.Set("Login" & Input.Token & "Email", "")
             Settings.Set("Login" & Input.Token & "Email", "")
             Settings.Set("Login" & Input.Token & "Pass", "")
             Settings.Set("Login" & Input.Token & "Pass", "")
         End Try
         End Try
@@ -817,17 +813,17 @@ LoginFinish:
             Return NeedRefresh
             Return NeedRefresh
         Catch ex As Exception
         Catch ex As Exception
             Dim AllMessage As String = ex.GetBrief()
             Dim AllMessage As String = ex.GetBrief()
-            Log(ex, "登录失败原始错误信息", LogLevel.Normal)
+            Log(ex, "登录失败原始错误信息", NotifyLevel.Silent)
             '读取服务器返回的错误
             '读取服务器返回的错误
             If TypeOf ex Is ResponsedWebException Then
             If TypeOf ex Is ResponsedWebException Then
                 Dim ErrorMessage As String = Nothing
                 Dim ErrorMessage As String = Nothing
                 Try
                 Try
                     Dim Response = DirectCast(ex, ResponsedWebException).Response
                     Dim Response = DirectCast(ex, ResponsedWebException).Response
-                    If Response.ContainsF("errorMessage", True) Then ErrorMessage = GetJson(Response)("errorMessage").ToString()
+                    If Response.ContainsIgnoreCase("errorMessage") Then ErrorMessage = GetJson(Response)("errorMessage").ToString()
                 Catch
                 Catch
                 End Try
                 End Try
                 If Not String.IsNullOrWhiteSpace(ErrorMessage) Then
                 If Not String.IsNullOrWhiteSpace(ErrorMessage) Then
-                    If ErrorMessage.Contains("密码错误") OrElse ErrorMessage.ContainsF("Incorrect username or password", True) Then
+                    If ErrorMessage.Contains("密码错误") OrElse ErrorMessage.ContainsIgnoreCase("Incorrect username or password") Then
                         '密码错误,退出登录 (#5090)
                         '密码错误,退出登录 (#5090)
                         McLaunchLog("密码错误,退出登录")
                         McLaunchLog("密码错误,退出登录")
                         Select Case Data.Input.Type
                         Select Case Data.Input.Type
@@ -905,15 +901,15 @@ Retry:
         Dim Result As String
         Dim Result As String
         Try
         Try
             Result = NetRequestByClientMultiple("https://login.live.com/oauth20_token.srf", HttpMethod.Post,
             Result = NetRequestByClientMultiple("https://login.live.com/oauth20_token.srf", HttpMethod.Post,
-                Content:=$"client_id={OAuthClientId}&refresh_token={Uri.EscapeDataString(Code)}&grant_type=refresh_token&scope=XboxLive.signin%20offline_access",
+                Content:=$"client_id={OAuthClientId}&refresh_token={EscapeUtils.FormUrlEscape(Code)}&grant_type=refresh_token&scope=XboxLive.signin%20offline_access",
                 ContentType:="application/x-www-form-urlencoded",
                 ContentType:="application/x-www-form-urlencoded",
                 Headers:={{"Accept-Language", "en-US,en;q=0.5"}, {"X-Requested-With", "XMLHttpRequest"}},
                 Headers:={{"Accept-Language", "en-US,en;q=0.5"}, {"X-Requested-With", "XMLHttpRequest"}},
                 ThreadCount:=2)
                 ThreadCount:=2)
         Catch ex As ResponsedWebException
         Catch ex As ResponsedWebException
             '修改错误列表时,同时检查 MyMsgLogin.xaml.vb 中的对应代码
             '修改错误列表时,同时检查 MyMsgLogin.xaml.vb 中的对应代码
             Dim Response = ex.Response
             Dim Response = ex.Response
-            If Response.ContainsF("must sign in again", True) OrElse Response.ContainsF("password expired", True) OrElse
-               (Response.Contains("refresh_token") AndAlso Response.Contains("is not valid")) Then '#269
+            If Response.ContainsIgnoreCase("must sign in again") OrElse Response.ContainsIgnoreCase("password expired") OrElse
+               (Response.Contains("refresh_token") AndAlso Response.ContainsIgnoreCase("is not valid")) Then '#269
                 Return {"Relogin", ""}
                 Return {"Relogin", ""}
             ElseIf Response.Contains("Account security interrupt") Then
             ElseIf Response.Contains("Account security interrupt") Then
                 MyMsgBox("该账号由于安全问题无法登陆,请前往微软账户页获取更多信息。", "登录失败", "我知道了", IsWarn:=True)
                 MyMsgBox("该账号由于安全问题无法登陆,请前往微软账户页获取更多信息。", "登录失败", "我知道了", IsWarn:=True)
@@ -1105,7 +1101,7 @@ Retry:
                         Uuid = McLoginMojangUuid(SkinName, False)
                         Uuid = McLoginMojangUuid(SkinName, False)
                     End If
                     End If
                 Catch ex As Exception
                 Catch ex As Exception
-                    Log(ex, "皮肤信息获取失败,游戏将以无皮肤的方式启动", LogLevel.Hint)
+                    Log(ex, "皮肤信息获取失败,游戏将以无皮肤的方式启动", NotifyLevel.AllUsers)
                 End Try
                 End Try
             Case 4
             Case 4
                 '自定义
                 '自定义
@@ -1118,7 +1114,7 @@ Retry:
     End Function
     End Function
     '根据用户名返回对应 UUID,需要多线程
     '根据用户名返回对应 UUID,需要多线程
     Public Function McLoginMojangUuid(Name As String, ThrowOnNotFound As Boolean)
     Public Function McLoginMojangUuid(Name As String, ThrowOnNotFound As Boolean)
-        If Name.Trim.Length = 0 Then Return StrFill("", "0", 32)
+        If Name.Trim.Length = 0 Then Return New String("0"c, 32)
         '从缓存获取
         '从缓存获取
         Dim Uuid As String = ReadIni(PathTemp & "Cache\Uuid\Mojang.ini", Name, "")
         Dim Uuid As String = ReadIni(PathTemp & "Cache\Uuid\Mojang.ini", Name, "")
         If Len(Uuid) = 32 Then Return Uuid
         If Len(Uuid) = 32 Then Return Uuid
@@ -1141,7 +1137,7 @@ Retry:
         Return Uuid
         Return Uuid
     End Function
     End Function
     Public Function McLoginLegacyUuid(Name As String)
     Public Function McLoginLegacyUuid(Name As String)
-        Dim FullUuid As String = StrFill(Name.Length.ToString("X"), "0", 16) & StrFill(GetHash(Name).ToString("X"), "0", 16)
+        Dim FullUuid As String = Name.Length.ToString("X").EnsureLength("0", 16) & GetHash(Name).ToString("X").EnsureLength("0", 16)
         Return FullUuid.Substring(0, 12) & "3" & FullUuid.Substring(13, 3) & "9" & FullUuid.Substring(17, 15)
         Return FullUuid.Substring(0, 12) & "3" & FullUuid.Substring(13, 3) & "9" & FullUuid.Substring(17, 15)
     End Function
     End Function
 
 
@@ -1583,7 +1579,7 @@ NextInstance:
                 McLaunchLog("发现错误的 OptiFineForge TweakClass,目前的游戏参数:" & Arg)
                 McLaunchLog("发现错误的 OptiFineForge TweakClass,目前的游戏参数:" & Arg)
                 Arg = Arg.Replace(" --tweakClass optifine.OptiFineTweaker", "") & " --tweakClass optifine.OptiFineForgeTweaker"
                 Arg = Arg.Replace(" --tweakClass optifine.OptiFineTweaker", "") & " --tweakClass optifine.OptiFineForgeTweaker"
                 Try
                 Try
-                    WriteFile(McInstanceSelected.PathVersion & McInstanceSelected.Name & ".json", ReadFile(McInstanceSelected.PathVersion & McInstanceSelected.Name & ".json").Replace("optifine.OptiFineTweaker", "optifine.OptiFineForgeTweaker"))
+                    FileUtils.Write(McInstanceSelected.PathVersion & McInstanceSelected.Name & ".json", ReadFile(McInstanceSelected.PathVersion & McInstanceSelected.Name & ".json").Replace("optifine.OptiFineTweaker", "optifine.OptiFineForgeTweaker"))
                 Catch ex As Exception
                 Catch ex As Exception
                     Log(ex, "替换 OptiFineForge TweakClass 失败")
                     Log(ex, "替换 OptiFineForge TweakClass 失败")
                 End Try
                 End Try
@@ -1627,15 +1623,15 @@ NextInstance:
     Private Iterator Function McLaunchArgumentsReplace(Arguments As List(Of String)) As IEnumerable(Of (Key As String, Value As String))
     Private Iterator Function McLaunchArgumentsReplace(Arguments As List(Of String)) As IEnumerable(Of (Key As String, Value As String))
         '基础参数
         '基础参数
         Yield ("${classpath_separator}", ";")
         Yield ("${classpath_separator}", ";")
-        Yield ("${natives_directory}", ShortenPath(GetNativesFolder()))
-        Yield ("${library_directory}", ShortenPath(McFolderSelected & "libraries"))
-        Yield ("${libraries_directory}", ShortenPath(McFolderSelected & "libraries"))
+        Yield ("${natives_directory}", GetNativesFolder().TrimEnd("\"c))
+        Yield ("${library_directory}", FileUtils.ShortenPath(McFolderSelected & "libraries"))
+        Yield ("${libraries_directory}", FileUtils.ShortenPath(McFolderSelected & "libraries"))
         Yield ("${pure_directory}", PathPure.TrimEnd("\"c)) '由 PCL 添加,这会允许在分割参数并去重后再替换路径,防止路径中的特殊字符影响参数分割和去重
         Yield ("${pure_directory}", PathPure.TrimEnd("\"c)) '由 PCL 添加,这会允许在分割参数并去重后再替换路径,防止路径中的特殊字符影响参数分割和去重
         Yield ("${launcher_name}", "PCL")
         Yield ("${launcher_name}", "PCL")
         Yield ("${launcher_version}", VersionCode)
         Yield ("${launcher_version}", VersionCode)
         Yield ("${version_name}", McInstanceSelected.Name)
         Yield ("${version_name}", McInstanceSelected.Name)
-        Yield ("${game_directory}", ShortenPath(Left(McInstanceSelected.PathIndie, McInstanceSelected.PathIndie.Count - 1)))
-        Yield ("${assets_root}", ShortenPath(McFolderSelected & "assets"))
+        Yield ("${game_directory}", FileUtils.ShortenPath(Left(McInstanceSelected.PathIndie, McInstanceSelected.PathIndie.Count - 1)))
+        Yield ("${assets_root}", FileUtils.ShortenPath(McFolderSelected & "assets"))
         Yield ("${user_properties}", "{}")
         Yield ("${user_properties}", "{}")
         Yield ("${auth_player_name}", McLoginLoader.Output.Name)
         Yield ("${auth_player_name}", McLoginLoader.Output.Name)
         Yield ("${auth_uuid}", McLoginLoader.Output.Uuid)
         Yield ("${auth_uuid}", McLoginLoader.Output.Uuid)
@@ -1643,8 +1639,8 @@ NextInstance:
         Yield ("${access_token}", McLoginLoader.Output.AccessToken)
         Yield ("${access_token}", McLoginLoader.Output.AccessToken)
         Yield ("${auth_session}", McLoginLoader.Output.AccessToken)
         Yield ("${auth_session}", McLoginLoader.Output.AccessToken)
         Yield ("${user_type}", "msa") '#1221
         Yield ("${user_type}", "msa") '#1221
-        Yield ("${primary_jar}", ShortenPath(McInstanceSelected.PathVersion & McInstanceSelected.Name & ".jar")) '#6942
-        Yield ("${game_assets}", ShortenPath(McFolderSelected & "assets\virtual\legacy")) '1.5.2 的 pre-1.6 资源索引应与 legacy 合并
+        Yield ("${primary_jar}", FileUtils.ShortenPath(McInstanceSelected.PathVersion & McInstanceSelected.Name & ".jar")) '#6942
+        Yield ("${game_assets}", FileUtils.ShortenPath(McFolderSelected & "assets\virtual\legacy")) '1.5.2 的 pre-1.6 资源索引应与 legacy 合并
         Yield ("${assets_index_name}", McAssetsGetIndexName(McInstanceSelected))
         Yield ("${assets_index_name}", McAssetsGetIndexName(McInstanceSelected))
 
 
         '自定义信息
         '自定义信息
@@ -1697,14 +1693,14 @@ NextInstance:
             End If
             End If
         Next
         Next
         If OptiFineCp IsNot Nothing Then Cps.Insert(Cps.Count - 2, OptiFineCp) 'OptiFine 的总是需要放到倒数第二位
         If OptiFineCp IsNot Nothing Then Cps.Insert(Cps.Count - 2, OptiFineCp) 'OptiFine 的总是需要放到倒数第二位
-        Yield ("${classpath}", Cps.Select(Function(c) ShortenPath(c)).Join(";"c))
+        Yield ("${classpath}", Cps.Select(Function(c) FileUtils.ShortenPath(c)).Join(";"c))
     End Function
     End Function
 
 
     ''' <summary>
     ''' <summary>
     ''' 分割 Java 参数字符串。
     ''' 分割 Java 参数字符串。
     ''' </summary>
     ''' </summary>
     Private Iterator Function SplitJavaArguments(Arg As String) As IEnumerable(Of String)
     Private Iterator Function SplitJavaArguments(Arg As String) As IEnumerable(Of String)
-        Arg = Arg.Replace(vbLf, " ").Replace(vbCr, " ").Trim '预处理换行符
+        Arg = Arg.ReplaceLineEndings(" ", mergeMultiple:=True).Trim '预处理换行符
         If String.IsNullOrWhiteSpace(Arg) Then Return
         If String.IsNullOrWhiteSpace(Arg) Then Return
         Dim Current As New StringBuilder
         Dim Current As New StringBuilder
         Dim InQuotes As Boolean = False
         Dim InQuotes As Boolean = False
@@ -1783,8 +1779,8 @@ NextInstance:
     Private Sub McLaunchNatives(Loader As LoaderTask(Of Integer, Integer))
     Private Sub McLaunchNatives(Loader As LoaderTask(Of Integer, Integer))
 
 
         '创建文件夹
         '创建文件夹
-        Dim Target As String = GetNativesFolder() & "\"
-        Directory.CreateDirectory(Target)
+        Dim Target As String = GetNativesFolder()
+        DirectoryUtils.Create(Target)
 
 
         '解压文件
         '解压文件
         McLaunchLog("正在解压 Natives 文件")
         McLaunchLog("正在解压 Natives 文件")
@@ -1821,7 +1817,7 @@ NextInstance:
                         End Try
                         End Try
                     End If
                     End If
                     '解压新文件
                     '解压新文件
-                    WriteFile(FilePath, Entry.Open)
+                    FileUtils.Write(FilePath, Entry.Open)
                     McLaunchLog("已解压:" & FilePath)
                     McLaunchLog("已解压:" & FilePath)
                 End If
                 End If
             Next
             Next
@@ -1843,18 +1839,18 @@ NextInstance:
 
 
     End Sub
     End Sub
     ''' <summary>
     ''' <summary>
-    ''' 获取 Natives 文件夹路径,不以 \ 结尾。
+    ''' 获取 Natives 文件夹路径,以 \ 结尾。
     ''' </summary>
     ''' </summary>
     Private Function GetNativesFolder() As String
     Private Function GetNativesFolder() As String
-        Dim Result As String = ShortenPath(McInstanceSelected.PathVersion) & McInstanceSelected.Name & "-natives"
-        If Not (IsGBKEncoding OrElse Result.IsASCII()) Then
-            Result = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) & "\.minecraft\bin\natives"
-            If Not Result.IsASCII() Then
-                Result = OsDrive & "ProgramData\PCL\natives"
+        Dim Result As String = FileUtils.ShortenPath(McInstanceSelected.PathVersion) & McInstanceSelected.Name & "-natives\"
+        If Not (IsGBKEncoding OrElse Result.IsAsciiOnly()) Then
+            Result = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) & "\.minecraft\bin\natives\"
+            If Not Result.IsAsciiOnly() Then
+                Result = OsDrive & "ProgramData\PCL\natives\"
             End If
             End If
         End If
         End If
-        Directory.CreateDirectory(Result) '提前创建,这样 ShortenPath 才有结果(否则在长路径下首次启动 Forge 会崩溃)
-        Return ShortenPath(Result)
+        DirectoryUtils.Create(Result) '提前创建,这样 DirectoryUtils.Shorten 才有结果(否则在长路径下首次启动 Forge 会崩溃)
+        Return FileUtils.ShortenPath(Result)
     End Function
     End Function
 
 
 #End Region
 #End Region
@@ -1867,7 +1863,7 @@ NextInstance:
         If Settings.Get("LaunchAdvanceGraphicCard") Then
         If Settings.Get("LaunchAdvanceGraphicCard") Then
             Try
             Try
                 SetGPUPreference(McLaunchJavaSelected.PathJava)
                 SetGPUPreference(McLaunchJavaSelected.PathJava)
-                SetGPUPreference(PathWithName)
+                SetGPUPreference(PathExe)
             Catch ex As Exception
             Catch ex As Exception
                 If IsAdmin() Then
                 If IsAdmin() Then
                     Log(ex, "直接调整显卡设置失败")
                     Log(ex, "直接调整显卡设置失败")
@@ -1880,7 +1876,7 @@ NextInstance:
                             Throw New Exception("调整过程中出现异常")
                             Throw New Exception("调整过程中出现异常")
                         End If
                         End If
                     Catch exx As Exception
                     Catch exx As Exception
-                        Log(exx, "调整显卡设置失败,Minecraft 可能会使用默认显卡运行", LogLevel.Hint)
+                        Log(exx, "调整显卡设置失败,Minecraft 可能会使用默认显卡运行", NotifyLevel.AllUsers)
                     End Try
                     End Try
                 End If
                 End If
             End Try
             End Try
@@ -1914,7 +1910,7 @@ NextInstance:
             '更新文件
             '更新文件
             Dim Profiles As JObject = GetJson(ReadFile(McFolderSelected & "launcher_profiles.json"))
             Dim Profiles As JObject = GetJson(ReadFile(McFolderSelected & "launcher_profiles.json"))
             Profiles.Merge(ReplaceJson)
             Profiles.Merge(ReplaceJson)
-            WriteFile(McFolderSelected & "launcher_profiles.json", Profiles.ToString, Encoding:=Encoding.GetEncoding("GB18030"))
+            FileUtils.Write(McFolderSelected & "launcher_profiles.json", Profiles.ToString, Encoding:=Encoding.GetEncoding("GB18030"))
             McLaunchLog("已更新 launcher_profiles.json")
             McLaunchLog("已更新 launcher_profiles.json")
         Catch ex As Exception
         Catch ex As Exception
             Log(ex, "更新 launcher_profiles.json 失败,将在删除文件后重试")
             Log(ex, "更新 launcher_profiles.json 失败,将在删除文件后重试")
@@ -1944,10 +1940,10 @@ NextInstance:
                 '更新文件
                 '更新文件
                 Dim Profiles As JObject = GetJson(ReadFile(McFolderSelected & "launcher_profiles.json"))
                 Dim Profiles As JObject = GetJson(ReadFile(McFolderSelected & "launcher_profiles.json"))
                 Profiles.Merge(ReplaceJson)
                 Profiles.Merge(ReplaceJson)
-                WriteFile(McFolderSelected & "launcher_profiles.json", Profiles.ToString, Encoding:=Encoding.GetEncoding("GB18030"))
+                FileUtils.Write(McFolderSelected & "launcher_profiles.json", Profiles.ToString, Encoding:=Encoding.GetEncoding("GB18030"))
                 McLaunchLog("已在删除后更新 launcher_profiles.json")
                 McLaunchLog("已在删除后更新 launcher_profiles.json")
             Catch exx As Exception
             Catch exx As Exception
-                Log(exx, "更新 launcher_profiles.json 失败", LogLevel.Feedback)
+                Log(exx, "更新 launcher_profiles.json 失败", NotifyLevel.MsgBoxAndFeedback)
             End Try
             End Try
         End Try
         End Try
 
 
@@ -1997,7 +1993,7 @@ NextInstance:
                     WriteIni(SetupFileAddress, "fullscreen", "false")
                     WriteIni(SetupFileAddress, "fullscreen", "false")
             End Select
             End Select
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "更新 options.txt 失败", LogLevel.Hint)
+            Log(ex, "更新 options.txt 失败", NotifyLevel.AllUsers)
         End Try
         End Try
 
 
         '离线皮肤 Alex 警告
         '离线皮肤 Alex 警告
@@ -2010,11 +2006,11 @@ NextInstance:
 
 
         '离线皮肤资源包
         '离线皮肤资源包
         Try
         Try
-            Directory.CreateDirectory(McInstanceSelected.PathIndie & "resourcepacks\")
             Dim ZipFileAddress As String = McInstanceSelected.PathIndie & "resourcepacks\PCL2 Skin.zip"
             Dim ZipFileAddress As String = McInstanceSelected.PathIndie & "resourcepacks\PCL2 Skin.zip"
+            DirectoryUtils.Create(ZipFileAddress, isFilePath:=True)
             Dim NewTypeSetup As Boolean = McInstanceSelected.Version.Vanilla.Major >= 13 OrElse McInstanceSelected.Version.Vanilla.Major < 6
             Dim NewTypeSetup As Boolean = McInstanceSelected.Version.Vanilla.Major >= 13 OrElse McInstanceSelected.Version.Vanilla.Major < 6
             If McLoginLoader.Input.Type = McLoginType.Legacy AndAlso Settings.Get("LaunchSkinType") = 4 AndAlso File.Exists(PathAppdata & "CustomSkin.png") Then
             If McLoginLoader.Input.Type = McLoginType.Legacy AndAlso Settings.Get("LaunchSkinType") = 4 AndAlso File.Exists(PathAppdata & "CustomSkin.png") Then
-                Directory.CreateDirectory(PathTemp)
+                DirectoryUtils.Create(PathTemp)
                 Dim MetaFileAddress As String = PathTemp & "pack.mcmeta"
                 Dim MetaFileAddress As String = PathTemp & "pack.mcmeta"
                 Dim PackPicAddress As String = PathTemp & "pack.png"
                 Dim PackPicAddress As String = PathTemp & "pack.png"
                 Dim PackFormat As Integer
                 Dim PackFormat As Integer
@@ -2063,13 +2059,13 @@ NextInstance:
                 '准备文件
                 '准备文件
                 Dim Bit As New MyBitmap(PathImage & "Heads/Logo.png")
                 Dim Bit As New MyBitmap(PathImage & "Heads/Logo.png")
                 Bit.Save(PackPicAddress)
                 Bit.Save(PackPicAddress)
-                WriteFile(MetaFileAddress, "{""pack"":{""pack_format"":" & PackFormat & ",""description"":""PCL 自定义离线皮肤资源包""}}")
+                FileUtils.Write(MetaFileAddress, "{""pack"":{""pack_format"":" & PackFormat & ",""description"":""PCL 自定义离线皮肤资源包""}}")
                 Dim Skin As New MyBitmap(PathAppdata & "CustomSkin.png")
                 Dim Skin As New MyBitmap(PathAppdata & "CustomSkin.png")
                 If (McInstanceSelected.Version.Vanilla.Major = 6 OrElse McInstanceSelected.Version.Vanilla.Major = 7) AndAlso Skin.Pic.Height = 64 Then
                 If (McInstanceSelected.Version.Vanilla.Major = 6 OrElse McInstanceSelected.Version.Vanilla.Major = 7) AndAlso Skin.Pic.Height = 64 Then
                     McLaunchLog("该 Minecraft 版本不支持双层皮肤,已进行裁剪")
                     McLaunchLog("该 Minecraft 版本不支持双层皮肤,已进行裁剪")
                     Skin = Skin.Clip(0, 0, 64, 32)
                     Skin = Skin.Clip(0, 0, 64, 32)
                 End If
                 End If
-                Skin.Save(Path & "PCL\CustomSkin_Cliped.png")
+                Skin.Save(PathExeFolder & "PCL\CustomSkin_Cliped.png")
                 '构建压缩文件
                 '构建压缩文件
                 Using ZipFile As New FileStream(ZipFileAddress, FileMode.Create)
                 Using ZipFile As New FileStream(ZipFileAddress, FileMode.Create)
                     Using ZipAr As New ZipArchive(ZipFile, ZipArchiveMode.Create)
                     Using ZipAr As New ZipArchive(ZipFile, ZipArchiveMode.Create)
@@ -2078,15 +2074,15 @@ NextInstance:
                         '1.19.3+ 使用复杂版本的替换
                         '1.19.3+ 使用复杂版本的替换
                         If McInstanceSelected.Version.Vanilla >= New Version(19, 0, 3) Then
                         If McInstanceSelected.Version.Vanilla >= New Version(19, 0, 3) Then
                             For Each SkinName In {"alex", "ari", "efe", "kai", "makena", "noor", "steve", "sunny", "zuri"}
                             For Each SkinName In {"alex", "ari", "efe", "kai", "makena", "noor", "steve", "sunny", "zuri"}
-                                ZipAr.CreateEntryFromFile(Path & "PCL\CustomSkin_Cliped.png",
+                                ZipAr.CreateEntryFromFile(PathExeFolder & "PCL\CustomSkin_Cliped.png",
                                     $"assets/minecraft/textures/entity/player/{If(Settings.Get("LaunchSkinSlim"), "slim", "wide")}/{SkinName}.png")
                                     $"assets/minecraft/textures/entity/player/{If(Settings.Get("LaunchSkinSlim"), "slim", "wide")}/{SkinName}.png")
                             Next
                             Next
                         Else
                         Else
-                            ZipAr.CreateEntryFromFile(Path & "PCL\CustomSkin_Cliped.png", "assets/minecraft/textures/entity/" & If(Settings.Get("LaunchSkinSlim"), "alex.png", "steve.png"))
+                            ZipAr.CreateEntryFromFile(PathExeFolder & "PCL\CustomSkin_Cliped.png", "assets/minecraft/textures/entity/" & If(Settings.Get("LaunchSkinSlim"), "alex.png", "steve.png"))
                         End If
                         End If
                     End Using
                     End Using
                 End Using
                 End Using
-                File.Delete(Path & "PCL\CustomSkin_Cliped.png")
+                File.Delete(PathExeFolder & "PCL\CustomSkin_Cliped.png")
                 '更改设置文件
                 '更改设置文件
                 IniClearCache(SetupFileAddress)
                 IniClearCache(SetupFileAddress)
                 Dim EnabledResourcePack As String = ReadIni(SetupFileAddress, "resourcePacks", "[]").TrimStart("[").TrimEnd("]")
                 Dim EnabledResourcePack As String = ReadIni(SetupFileAddress, "resourcePacks", "[]").TrimStart("[").TrimEnd("]")
@@ -2132,7 +2128,7 @@ IgnoreCustomSkin:
                 End If
                 End If
             End If
             End If
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "离线皮肤资源包设置失败", LogLevel.Hint)
+            Log(ex, "离线皮肤资源包设置失败", NotifyLevel.AllUsers)
         End Try
         End Try
 
 
     End Sub
     End Sub
@@ -2151,13 +2147,13 @@ IgnoreCustomSkin:
                 "@echo off" & vbCrLf &
                 "@echo off" & vbCrLf &
                 $"title 启动 - {McInstanceSelected.Name}" & vbCrLf &
                 $"title 启动 - {McInstanceSelected.Name}" & vbCrLf &
                 "echo 游戏正在启动,请稍候。" & vbCrLf &
                 "echo 游戏正在启动,请稍候。" & vbCrLf &
-                $"cd /D ""{ShortenPath(McInstanceSelected.PathIndie)}""" & vbCrLf &
+                $"cd /D ""{FileUtils.ShortenPath(McInstanceSelected.PathIndie)}""" & vbCrLf &
                 CustomCommandGlobal & vbCrLf &
                 CustomCommandGlobal & vbCrLf &
                 CustomCommandVersion & vbCrLf &
                 CustomCommandVersion & vbCrLf &
                 $"""{McLaunchJavaSelected.PathJava}"" {McLaunchArgument}" & vbCrLf &
                 $"""{McLaunchJavaSelected.PathJava}"" {McLaunchArgument}" & vbCrLf &
                 "echo 游戏已退出。" & vbCrLf &
                 "echo 游戏已退出。" & vbCrLf &
                 "pause"
                 "pause"
-            WriteFile(If(CurrentLaunchOptions.SaveBatch, Path & "PCL\LatestLaunch.bat"), FilterAccessToken(CmdString, "F"),
+            FileUtils.Write(If(CurrentLaunchOptions.SaveBatch, PathExeFolder & "PCL\LatestLaunch.bat"), FilterAccessToken(CmdString, "F"),
                       Encoding:=If(McLaunchJavaSelected.MajorVersion > 8, Encoding.UTF8, Encoding.Default))
                       Encoding:=If(McLaunchJavaSelected.MajorVersion > 8, Encoding.UTF8, Encoding.Default))
             If CurrentLaunchOptions.SaveBatch IsNot Nothing Then
             If CurrentLaunchOptions.SaveBatch IsNot Nothing Then
                 McLaunchLog("导出启动脚本完成,强制结束启动过程")
                 McLaunchLog("导出启动脚本完成,强制结束启动过程")
@@ -2189,7 +2185,7 @@ IgnoreCustomSkin:
                     Loop
                     Loop
                 End If
                 End If
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "执行全局自定义命令失败", LogLevel.Hint)
+                Log(ex, "执行全局自定义命令失败", NotifyLevel.AllUsers)
             Finally
             Finally
                 If CustomProcess IsNot Nothing AndAlso Not CustomProcess.HasExited AndAlso Loader.IsInterrupted Then
                 If CustomProcess IsNot Nothing AndAlso Not CustomProcess.HasExited AndAlso Loader.IsInterrupted Then
                     McLaunchLog("由于取消启动,已强制结束自定义命令 CMD 进程") '#1183
                     McLaunchLog("由于取消启动,已强制结束自定义命令 CMD 进程") '#1183
@@ -2214,7 +2210,7 @@ IgnoreCustomSkin:
                     Loop
                     Loop
                 End If
                 End If
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "执行版本自定义命令失败", LogLevel.Hint)
+                Log(ex, "执行版本自定义命令失败", NotifyLevel.AllUsers)
             Finally
             Finally
                 If CustomProcess IsNot Nothing AndAlso Not CustomProcess.HasExited AndAlso Loader.IsInterrupted Then
                 If CustomProcess IsNot Nothing AndAlso Not CustomProcess.HasExited AndAlso Loader.IsInterrupted Then
                     McLaunchLog("由于取消启动,已强制结束自定义命令 CMD 进程") '#1183
                     McLaunchLog("由于取消启动,已强制结束自定义命令 CMD 进程") '#1183
@@ -2233,9 +2229,9 @@ IgnoreCustomSkin:
         Dim PathEnv As String = StartInfo.EnvironmentVariables("Path")
         Dim PathEnv As String = StartInfo.EnvironmentVariables("Path")
         Dim Paths As New List(Of String)
         Dim Paths As New List(Of String)
         If Not String.IsNullOrEmpty(PathEnv) Then Paths.AddRange(PathEnv.Split(";"))
         If Not String.IsNullOrEmpty(PathEnv) Then Paths.AddRange(PathEnv.Split(";"))
-        Paths.Add(ShortenPath(McLaunchJavaSelected.PathFolder))
+        Paths.Add(FileUtils.ShortenPath(McLaunchJavaSelected.PathFolder))
         StartInfo.EnvironmentVariables("Path") = Paths.Distinct.Join(";"c)
         StartInfo.EnvironmentVariables("Path") = Paths.Distinct.Join(";"c)
-        StartInfo.EnvironmentVariables("appdata") = ShortenPath(McFolderSelected)
+        StartInfo.EnvironmentVariables("appdata") = FileUtils.ShortenPath(McFolderSelected)
 
 
         '设置其他参数
         '设置其他参数
         StartInfo.WorkingDirectory = McInstanceSelected.PathIndie
         StartInfo.WorkingDirectory = McInstanceSelected.PathIndie
@@ -2267,7 +2263,7 @@ IgnoreCustomSkin:
                 Case Else '中
                 Case Else '中
             End Select
             End Select
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "设置进程优先级失败", LogLevel.Feedback)
+            Log(ex, "设置进程优先级失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
 
 
     End Sub
     End Sub
@@ -2276,7 +2272,7 @@ IgnoreCustomSkin:
         '输出信息
         '输出信息
         McLaunchLog("")
         McLaunchLog("")
         McLaunchLog("~ 基础参数 ~")
         McLaunchLog("~ 基础参数 ~")
-        McLaunchLog("PCL 版本:" & VersionDisplayName & " (" & VersionCode & ")")
+        McLaunchLog("PCL 版本:" & VersionDisplay & " (" & VersionCode & ")")
         McLaunchLog($"游戏版本:{McInstanceSelected.VersionDisplayName}({McInstanceSelected.Version.Vanilla},Drop {McInstanceSelected.Version.Drop}{If(McInstanceSelected.Version.Reliable, "", ",无法完全确定")})")
         McLaunchLog($"游戏版本:{McInstanceSelected.VersionDisplayName}({McInstanceSelected.Version.Vanilla},Drop {McInstanceSelected.Version.Drop}{If(McInstanceSelected.Version.Reliable, "", ",无法完全确定")})")
         McLaunchLog("资源版本:" & McAssetsGetIndexName(McInstanceSelected))
         McLaunchLog("资源版本:" & McAssetsGetIndexName(McInstanceSelected))
         McLaunchLog("版本继承:" & If(McInstanceSelected.InheritName = "", "无", McInstanceSelected.InheritName))
         McLaunchLog("版本继承:" & If(McInstanceSelected.InheritName = "", "无", McInstanceSelected.InheritName))

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

@@ -1,6 +1,4 @@
-Imports System.IO.Compression
-
-Public Module ModMinecraft
+Public Module ModMinecraft
 
 
 #Region "文件夹"
 #Region "文件夹"
 
 
@@ -18,8 +16,8 @@ Public Module ModMinecraft
     End Property
     End Property
     Private Sub SetMcFolder(Value As String) '等同 McFolderSelected.Set
     Private Sub SetMcFolder(Value As String) '等同 McFolderSelected.Set
         If _McFolderSelected = Value Then Return
         If _McFolderSelected = Value Then Return
-        _McFolderSelected = Value.Replace("$", Path)
-        Settings.Set("LaunchFolderSelect", Value.Replace(Path, "$"))
+        _McFolderSelected = Value.Replace("$", PathExeFolder)
+        Settings.Set("LaunchFolderSelect", Value.Replace(PathExeFolder, "$"))
         Log("[Minecraft] 当前选择的 Minecraft 文件夹:" & _McFolderSelected)
         Log("[Minecraft] 当前选择的 Minecraft 文件夹:" & _McFolderSelected)
     End Sub
     End Sub
     Private _McFolderSelected As String
     Private _McFolderSelected As String
@@ -65,8 +63,8 @@ Public Module ModMinecraft
 
 
             '扫描当前文件夹
             '扫描当前文件夹
             Try
             Try
-                If Directory.Exists(Path & "versions\") Then CacheMcFolderList.Add(New McFolder With {.Name = "当前文件夹", .Location = Path, .Type = McFolder.Types.Vanilla})
-                For Each Folder As DirectoryInfo In New DirectoryInfo(Path).GetDirectories
+                If Directory.Exists(PathExeFolder & "versions\") Then CacheMcFolderList.Add(New McFolder With {.Name = "当前文件夹", .Location = PathExeFolder, .Type = McFolder.Types.Vanilla})
+                For Each Folder As DirectoryInfo In New DirectoryInfo(PathExeFolder).GetDirectories
                     If Directory.Exists(Folder.FullName & "versions\") OrElse Folder.Name = ".minecraft" Then CacheMcFolderList.Add(New McFolder With {.Name = "当前文件夹", .Location = Folder.FullName & "\", .Type = McFolder.Types.Vanilla})
                     If Directory.Exists(Folder.FullName & "versions\") OrElse Folder.Name = ".minecraft" Then CacheMcFolderList.Add(New McFolder With {.Name = "当前文件夹", .Location = Folder.FullName & "\", .Type = McFolder.Types.Vanilla})
                 Next
                 Next
             Catch ex As Exception
             Catch ex As Exception
@@ -122,8 +120,8 @@ Public Module ModMinecraft
 
 
             '若没有可用文件夹,则创建 .minecraft
             '若没有可用文件夹,则创建 .minecraft
             If Not CacheMcFolderList.Any() Then
             If Not CacheMcFolderList.Any() Then
-                Directory.CreateDirectory(Path & ".minecraft\versions\")
-                CacheMcFolderList.Add(New McFolder With {.Name = "当前文件夹", .Location = Path & ".minecraft\", .Type = McFolder.Types.Vanilla})
+                DirectoryUtils.Create(PathExeFolder & ".minecraft\versions\")
+                CacheMcFolderList.Add(New McFolder With {.Name = "当前文件夹", .Location = PathExeFolder & ".minecraft\", .Type = McFolder.Types.Vanilla})
             End If
             End If
 
 
             For Each Folder As McFolder In CacheMcFolderList
             For Each Folder As McFolder In CacheMcFolderList
@@ -135,7 +133,7 @@ Public Module ModMinecraft
             McFolderList = CacheMcFolderList
             McFolderList = CacheMcFolderList
 
 
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "加载 Minecraft 文件夹列表失败", LogLevel.Feedback)
+            Log(ex, "加载 Minecraft 文件夹列表失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -159,10 +157,10 @@ Public Module ModMinecraft
     ""selectedProfile"": ""PCL"",
     ""selectedProfile"": ""PCL"",
     ""clientToken"": ""23323323323323323323323323323333""
     ""clientToken"": ""23323323323323323323323323323333""
 }"
 }"
-            WriteFile(Folder & "launcher_profiles.json", ResultJson, Encoding:=Encoding.GetEncoding("GB18030"))
+            FileUtils.Write(Folder & "launcher_profiles.json", ResultJson, Encoding:=Encoding.GetEncoding("GB18030"))
             Log("[Minecraft] 已创建 launcher_profiles.json:" & Folder)
             Log("[Minecraft] 已创建 launcher_profiles.json:" & Folder)
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "创建 launcher_profiles.json 失败(" & Folder & ")", LogLevel.Feedback)
+            Log(ex, "创建 launcher_profiles.json 失败(" & Folder & ")", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -170,7 +168,7 @@ Public Module ModMinecraft
 
 
 #Region "版本处理"
 #Region "版本处理"
 
 
-    Public Const McInstanceCacheVersion As Integer = 35
+    Public Const McInstanceCacheVersion As Integer = 36
 
 
     Private _McInstanceSelected As McInstance
     Private _McInstanceSelected As McInstance
     ''' <summary>
     ''' <summary>
@@ -185,13 +183,12 @@ Public Module ModMinecraft
             If ReferenceEquals(_McInstanceSelectedLast, value) Then Return
             If ReferenceEquals(_McInstanceSelectedLast, value) Then Return
             _McInstanceSelected = value '由于有可能是 Nothing,导致无法初始化,才得这样弄一圈
             _McInstanceSelected = value '由于有可能是 Nothing,导致无法初始化,才得这样弄一圈
             _McInstanceSelectedLast = value
             _McInstanceSelectedLast = value
-            Settings.MakeInstanceDirty()
             '更新 MC 文件夹所选取的 Minecraft 版本
             '更新 MC 文件夹所选取的 Minecraft 版本
             Log("[Launch] 当前选择的 Minecraft 版本:" & If(value?.PathVersion, "无"))
             Log("[Launch] 当前选择的 Minecraft 版本:" & If(value?.PathVersion, "无"))
             WriteIni(McFolderSelected & "PCL.ini", "Version", If(value?.Name, ""))
             WriteIni(McFolderSelected & "PCL.ini", "Version", If(value?.Name, ""))
             If value Is Nothing Then Return
             If value Is Nothing Then Return
             '重置缓存的下载文件夹
             '重置缓存的下载文件夹
-            PageDownloadCompDetail.CachedFolder.Clear()
+            PageDownloadResourceDetail.CachedFolder.Clear()
             '统一通行证重判
             '统一通行证重判
             If AniControlEnabled = 0 AndAlso
             If AniControlEnabled = 0 AndAlso
                Settings.Get("VersionServerNide", Instance:=value) <> Settings.Get("CacheNideServer") AndAlso
                Settings.Get("VersionServerNide", Instance:=value) <> Settings.Get("CacheNideServer") AndAlso
@@ -481,7 +478,7 @@ VersionSearchFinish:
                 If JsonPath Is Nothing Then
                 If JsonPath Is Nothing Then
                     Throw New Exception($"未找到版本 JSON 文件:{PathVersion}{Name}.json")
                     Throw New Exception($"未找到版本 JSON 文件:{PathVersion}{Name}.json")
                 Else
                 Else
-                    Log("[Minecraft] 未找到同名版本 JSON,自动换用 " & JsonPath, LogLevel.Debug)
+                    Log("[Minecraft] 未找到同名版本 JSON,自动换用 " & JsonPath, NotifyLevel.DebugModeOnly)
                 End If
                 End If
             End If
             End If
             Return JsonPath
             Return JsonPath
@@ -503,10 +500,10 @@ VersionSearchFinish:
                     '如果 ReadFile 失败会返回空字符串;这可能是由于文件被临时占用,故延时后重试
                     '如果 ReadFile 失败会返回空字符串;这可能是由于文件被临时占用,故延时后重试
                     If Not FastJsonCheck(_JsonText) Then
                     If Not FastJsonCheck(_JsonText) Then
                         If RunInUi() Then
                         If RunInUi() Then
-                            Log("[Minecraft] 版本 JSON 文件为空或有误,由于代码在主线程运行,将不再进行重试", LogLevel.Debug)
+                            Log("[Minecraft] 版本 JSON 文件为空或有误,由于代码在主线程运行,将不再进行重试", NotifyLevel.DebugModeOnly)
                             GetJson(_JsonText) '触发异常
                             GetJson(_JsonText) '触发异常
                         Else
                         Else
-                            Log($"[Minecraft] 版本 JSON 文件为空或有误,将在 2s 后重试读取({JsonPath})", LogLevel.Debug)
+                            Log($"[Minecraft] 版本 JSON 文件为空或有误,将在 2s 后重试读取({JsonPath})", NotifyLevel.DebugModeOnly)
                             Thread.Sleep(2000)
                             Thread.Sleep(2000)
                             _JsonText = ReadFile(JsonPath)
                             _JsonText = ReadFile(JsonPath)
                             If Not FastJsonCheck(_JsonText) Then GetJson(_JsonText) '触发异常
                             If Not FastJsonCheck(_JsonText) Then GetJson(_JsonText) '触发异常
@@ -566,7 +563,7 @@ VersionSearchFinish:
                         Try
                         Try
                             InstanceNameInheritFrom = If(_JsonObject("inheritsFrom") Is Nothing, "", _JsonObject("inheritsFrom").ToString)
                             InstanceNameInheritFrom = If(_JsonObject("inheritsFrom") Is Nothing, "", _JsonObject("inheritsFrom").ToString)
                             If InstanceNameInheritFrom = Name Then
                             If InstanceNameInheritFrom = Name Then
-                                Log("[Minecraft] 自引用的继承版本:" & Name, LogLevel.Debug)
+                                Log("[Minecraft] 自引用的继承版本:" & Name, NotifyLevel.DebugModeOnly)
                                 InstanceNameInheritFrom = ""
                                 InstanceNameInheritFrom = ""
                                 Exit Try
                                 Exit Try
                             End If
                             End If
@@ -684,7 +681,7 @@ Recheck:
             End If
             End If
             '检查权限
             '检查权限
             Try
             Try
-                Directory.CreateDirectory(PathVersion & "PCL\")
+                DirectoryUtils.Create(PathVersion & "PCL\")
                 CheckPermissionWithException(PathVersion & "PCL\")
                 CheckPermissionWithException(PathVersion & "PCL\")
             Catch ex As Exception
             Catch ex As Exception
                 State = McInstanceState.Error
                 State = McInstanceState.Error
@@ -823,8 +820,6 @@ ExitDataLoad:
                     If State = McInstanceState.Fool Then
                     If State = McInstanceState.Fool Then
                         Info = GetMcFoolName(Version.VanillaName)
                         Info = GetMcFoolName(Version.VanillaName)
                     ElseIf State <> McInstanceState.Error Then
                     ElseIf State <> McInstanceState.Error Then
-                        '这里不应使用缓存,因为不同实例有不同值 (#8508)
-                        Settings.MakeInstanceDirty()
                         If Settings.Get("VersionServerLogin", Instance:=Me) = 3 Then Info += ", 统一通行证验证"
                         If Settings.Get("VersionServerLogin", Instance:=Me) = 3 Then Info += ", 统一通行证验证"
                         If Settings.Get("VersionServerLogin", Instance:=Me) = 4 Then Info += ", Authlib 验证"
                         If Settings.Get("VersionServerLogin", Instance:=Me) = 4 Then Info += ", Authlib 验证"
                     End If
                     End If
@@ -853,15 +848,15 @@ ExitDataLoad:
                 Info = "未知错误:" & ex.GetBrief()
                 Info = "未知错误:" & ex.GetBrief()
                 Logo = PathImage & "Blocks/RedstoneBlock.png"
                 Logo = PathImage & "Blocks/RedstoneBlock.png"
                 State = McInstanceState.Error
                 State = McInstanceState.Error
-                Log(ex, "加载版本失败(" & Name & ")", LogLevel.Feedback)
+                Log(ex, "加载版本失败(" & Name & ")", NotifyLevel.MsgBoxAndFeedback)
             Finally
             Finally
                 IsLoaded = True
                 IsLoaded = True
             End Try
             End Try
             Return Me
             Return Me
         End Function
         End Function
         Private Function IsSnapshot() As Boolean
         Private Function IsSnapshot() As Boolean
-            Return {"w", "snapshot", "rc", "pre", "experimental", "-"}.Any(Function(s) Version.VanillaName.ContainsF(s, True)) OrElse
-                Name.ContainsF("combat", True) OrElse
+            Return {"w", "snapshot", "rc", "pre", "experimental", "-"}.Any(Function(s) Version.VanillaName.ContainsIgnoreCase(s)) OrElse
+                Name.ContainsIgnoreCase("combat") OrElse
                 If(JsonObject("type"), "").ToString = "snapshot" OrElse If(JsonObject("type"), "").ToString = "pending"
                 If(JsonObject("type"), "").ToString = "snapshot" OrElse If(JsonObject("type"), "").ToString = "pending"
         End Function
         End Function
 
 
@@ -882,15 +877,15 @@ ExitDataLoad:
             Dim Info As String
             Dim Info As String
             Select Case State
             Select Case State
                 Case McInstanceState.Snapshot, McInstanceState.Original, McInstanceState.Forge, McInstanceState.NeoForge, McInstanceState.Fabric, McInstanceState.OptiFine, McInstanceState.LiteLoader
                 Case McInstanceState.Snapshot, McInstanceState.Original, McInstanceState.Forge, McInstanceState.NeoForge, McInstanceState.Fabric, McInstanceState.OptiFine, McInstanceState.LiteLoader
-                    If Version.VanillaName.ContainsF("pre", True) Then
+                    If Version.VanillaName.ContainsIgnoreCase("pre") Then
                         Info = "预发布版 " & Version.VanillaName
                         Info = "预发布版 " & Version.VanillaName
-                    ElseIf Version.VanillaName.ContainsF("rc", True) Then
+                    ElseIf Version.VanillaName.ContainsIgnoreCase("rc") Then
                         Info = "发布候选 " & Version.VanillaName
                         Info = "发布候选 " & Version.VanillaName
-                    ElseIf Version.VanillaName.Contains("experimental") Then
+                    ElseIf Version.VanillaName.ContainsIgnoreCase("experimental") Then
                         Info = "实验性快照" & Version.VanillaName
                         Info = "实验性快照" & Version.VanillaName
                     ElseIf Version.VanillaName = "pending" Then
                     ElseIf Version.VanillaName = "pending" Then
                         Info = "实验性快照"
                         Info = "实验性快照"
-                    ElseIf IsSnapshot Then
+                    ElseIf IsSnapshot() Then
                         Info = If(Version.Reliable, "快照版 " & Version.VanillaName.Replace("-snapshot", ""), "快照版")
                         Info = If(Version.Reliable, "快照版 " & Version.VanillaName.Replace("-snapshot", ""), "快照版")
                     Else
                     Else
                         Info = If(Version.Reliable, "正式版 " & Version.VanillaName, "正式版")
                         Info = If(Version.Reliable, "正式版 " & Version.VanillaName, "正式版")
@@ -928,7 +923,7 @@ ExitDataLoad:
                         NewItem.Logo = Logo
                         NewItem.Logo = Logo
                     End If
                     End If
                 Catch ex As Exception
                 Catch ex As Exception
-                    Log(ex, "加载版本图标失败", LogLevel.Hint)
+                    Log(ex, "加载版本图标失败", NotifyLevel.AllUsers)
                     NewItem.Logo = "pack://application:,,,/images/Blocks/RedstoneBlock.png"
                     NewItem.Logo = "pack://application:,,,/images/Blocks/RedstoneBlock.png"
                 End Try
                 End Try
                 Return NewItem
                 Return NewItem
@@ -1026,8 +1021,8 @@ ExitDataLoad:
                 Result += Val(RegexSeek(Right(OptiFine, OptiFine.Length - 1), "[0-9]+"))
                 Result += Val(RegexSeek(Right(OptiFine, OptiFine.Length - 1), "[0-9]+"))
                 '测试标记(正式版为 99,Pre[x] 为 50+x,Beta[x] 为 x)
                 '测试标记(正式版为 99,Pre[x] 为 50+x,Beta[x] 为 x)
                 Result *= 100
                 Result *= 100
-                If OptiFine.ContainsF("pre", True) Then Result += 50
-                If OptiFine.ContainsF("pre", True) OrElse OptiFine.ContainsF("beta", True) Then
+                If OptiFine.ContainsIgnoreCase("pre") Then Result += 50
+                If OptiFine.ContainsIgnoreCase("pre") OrElse OptiFine.ContainsIgnoreCase("beta") Then
                     If Val(Right(OptiFine, 1)) = 0 AndAlso Right(OptiFine, 1) <> "0" Then
                     If Val(Right(OptiFine, 1)) = 0 AndAlso Right(OptiFine, 1) <> "0" Then
                         Result += 1 '为 pre 或 beta 结尾,视作 1
                         Result += 1 '为 pre 或 beta 结尾,视作 1
                     Else
                     Else
@@ -1134,7 +1129,7 @@ ExitDataLoad:
             If Drop >= 250 Then
             If Drop >= 250 Then
                 Return $"{Drop \ 10}.{Drop Mod 10}"
                 Return $"{Drop \ 10}.{Drop Mod 10}"
             ElseIf Drop = 209 Then
             ElseIf Drop = 209 Then
-                Log("[Minecraft] 尝试将旧快照版 Drop 序数转换为版本字符串,这不应该发生!" & vbCrLf & GetStackTrace(), LogLevel.Debug)
+                Log("[Minecraft] 尝试将旧快照版 Drop 序数转换为版本字符串,这不应该发生!" & vbCrLf & GetStackTrace(), NotifyLevel.DebugModeOnly)
                 Return "1.20"
                 Return "1.20"
             Else
             Else
                 Return $"1.{Drop \ 10}"
                 Return $"1.{Drop \ 10}"
@@ -1294,24 +1289,24 @@ OnLoaded:
         Catch ex As Exception
         Catch ex As Exception
             If Loader.IsInterrupted Then Return '#5617
             If Loader.IsInterrupted Then Return '#5617
             WriteIni(PathMc & "PCL.ini", "InstanceCache", "") '要求下次重新加载
             WriteIni(PathMc & "PCL.ini", "InstanceCache", "") '要求下次重新加载
-            Log(ex, "加载 .minecraft 版本列表失败", LogLevel.Feedback)
+            Log(ex, "加载 .minecraft 版本列表失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
 
 
     '获取版本列表
     '获取版本列表
-    Private Function InitMcInstanceListWithCache(Path As String) As Dictionary(Of McInstanceCardType, List(Of McInstance))
+    Private Function InitMcInstanceListWithCache(Folder As String) As Dictionary(Of McInstanceCardType, List(Of McInstance))
         Dim Results As New Dictionary(Of McInstanceCardType, List(Of McInstance))
         Dim Results As New Dictionary(Of McInstanceCardType, List(Of McInstance))
         Try
         Try
-            Dim CardCount As Integer = ReadIni(Path & "PCL.ini", "CardCount", -1)
+            Dim CardCount As Integer = ReadIni(Folder & "PCL.ini", "CardCount", -1)
             If CardCount = -1 Then Return Nothing
             If CardCount = -1 Then Return Nothing
             For i = 0 To CardCount - 1
             For i = 0 To CardCount - 1
-                Dim CardType As McInstanceCardType = ReadIni(Path & "PCL.ini", "CardKey" & (i + 1), ":")
+                Dim CardType As McInstanceCardType = ReadIni(Folder & "PCL.ini", "CardKey" & (i + 1), ":")
                 Dim InstanceList As New List(Of McInstance)
                 Dim InstanceList As New List(Of McInstance)
 
 
                 '循环读取版本
                 '循环读取版本
-                For Each Folder As String In ReadIni(Path & "PCL.ini", "CardValue" & (i + 1), ":").Split(":")
-                    If Folder = "" Then Continue For
-                    Dim FolderVersions As String = $"{Path}versions\{Folder}\"
+                For Each VersionName As String In ReadIni(Folder & "PCL.ini", "CardValue" & (i + 1), ":").Split(":")
+                    If VersionName = "" Then Continue For
+                    Dim FolderVersions As String = $"{Folder}versions\{VersionName}\"
                     If File.Exists(FolderVersions & ".pclignore") Then
                     If File.Exists(FolderVersions & ".pclignore") Then
                         If IsFirstMcInstanceListLoad Then
                         If IsFirstMcInstanceListLoad Then
                             Log("[Minecraft] 清理残留的忽略项目:" & FolderVersions) '#2781
                             Log("[Minecraft] 清理残留的忽略项目:" & FolderVersions) '#2781
@@ -1332,7 +1327,7 @@ OnLoaded:
                         Instance.ReleaseTime = ReadIni(Instance.PathVersion & "PCL\Setup.ini", "ReleaseTime", Instance.ReleaseTime)
                         Instance.ReleaseTime = ReadIni(Instance.PathVersion & "PCL\Setup.ini", "ReleaseTime", Instance.ReleaseTime)
                         Instance.State = ReadIni(Instance.PathVersion & "PCL\Setup.ini", "State", Instance.State)
                         Instance.State = ReadIni(Instance.PathVersion & "PCL\Setup.ini", "State", Instance.State)
                         Instance.IsStar = ReadIni(Instance.PathVersion & "PCL\Setup.ini", "IsStar", False)
                         Instance.IsStar = ReadIni(Instance.PathVersion & "PCL\Setup.ini", "IsStar", False)
-                        Instance.DisplayType = ReadIni(Path & "PCL\Setup.ini", "DisplayType", McInstanceCardType.Auto)
+                        Instance.DisplayType = ReadIni(Folder & "PCL\Setup.ini", "DisplayType", McInstanceCardType.Auto)
                         If Instance.State <> McInstanceState.Error AndAlso
                         If Instance.State <> McInstanceState.Error AndAlso
                            ReadIni(Instance.PathVersion & "PCL\Setup.ini", "VersionVanillaName", "Unknown") <> "Unknown" Then
                            ReadIni(Instance.PathVersion & "PCL\Setup.ini", "VersionVanillaName", "Unknown") <> "Unknown" Then
                             '读取版本信息
                             '读取版本信息
@@ -1375,7 +1370,7 @@ OnLoaded:
                         End If
                         End If
 
 
                     Catch ex As Exception
                     Catch ex As Exception
-                        Log(ex, "读取版本加载缓存失败(" & Folder & ")", LogLevel.Debug)
+                        Log(ex, "读取版本加载缓存失败(" & VersionName & ")", NotifyLevel.DebugModeOnly)
                         Return Nothing
                         Return Nothing
                     End Try
                     End Try
                 Next
                 Next
@@ -1388,20 +1383,21 @@ OnLoaded:
             Return Nothing
             Return Nothing
         End Try
         End Try
     End Function
     End Function
-    Private Function InitMcInstanceListWithoutCache(Path As String) As Dictionary(Of McInstanceCardType, List(Of McInstance))
+    Private Function InitMcInstanceListWithoutCache(Folder As String) As Dictionary(Of McInstanceCardType, List(Of McInstance))
         Dim InstanceList As New List(Of McInstance)
         Dim InstanceList As New List(Of McInstance)
 
 
 #Region "循环加载每个版本的信息"
 #Region "循环加载每个版本的信息"
-        For Each Folder As DirectoryInfo In New DirectoryInfo(Path & "versions").GetDirectories
-            If Not Folder.Exists OrElse Not Folder.EnumerateFiles.Any Then
-                Log("[Minecraft] 跳过空文件夹:" & Folder.FullName)
+        For Each VersionFolderInfo As DirectoryInfo In New DirectoryInfo(Folder & "versions").GetDirectories
+            If Not VersionFolderInfo.Exists OrElse Not VersionFolderInfo.EnumerateFiles.Any Then
+                Log("[Minecraft] 跳过空文件夹:" & VersionFolderInfo.FullName)
                 Continue For
                 Continue For
             End If
             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)
+            If (VersionFolderInfo.Name = "cache" OrElse VersionFolderInfo.Name = "BLClient" OrElse VersionFolderInfo.Name = "PCL") AndAlso
+                Not File.Exists(VersionFolderInfo.FullName & "\" & VersionFolderInfo.Name & ".json") Then
+                Log("[Minecraft] 跳过可能不是版本文件夹的项目:" & VersionFolderInfo.FullName)
                 Continue For
                 Continue For
             End If
             End If
-            Dim VersionFolder As String = Folder.FullName & "\"
+            Dim VersionFolder As String = VersionFolderInfo.FullName & "\"
             If File.Exists(VersionFolder & ".pclignore") Then
             If File.Exists(VersionFolder & ".pclignore") Then
                 If IsFirstMcInstanceListLoad Then
                 If IsFirstMcInstanceListLoad Then
                     Log("[Minecraft] 清理残留的忽略项目:" & VersionFolder) '#2781
                     Log("[Minecraft] 清理残留的忽略项目:" & VersionFolder) '#2781
@@ -1517,7 +1513,7 @@ OnLoaded:
 
 
         Catch ex As Exception
         Catch ex As Exception
             Results.Clear()
             Results.Clear()
-            Log(ex, "分类版本列表失败", LogLevel.Feedback)
+            Log(ex, "分类版本列表失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
 #End Region
 #End Region
 #Region "对卡片与版本进行排序"
 #Region "对卡片与版本进行排序"
@@ -1558,14 +1554,14 @@ OnLoaded:
 
 
 #End Region
 #End Region
 #Region "保存卡片缓存"
 #Region "保存卡片缓存"
-        WriteIni(Path & "PCL.ini", "CardCount", Results.Count)
+        WriteIni(Folder & "PCL.ini", "CardCount", Results.Count)
         For i = 0 To Results.Count - 1
         For i = 0 To Results.Count - 1
-            WriteIni(Path & "PCL.ini", "CardKey" & (i + 1), Results.Keys(i))
+            WriteIni(Folder & "PCL.ini", "CardKey" & (i + 1), Results.Keys(i))
             Dim Value As String = ""
             Dim Value As String = ""
             For Each Instance As McInstance In Results.Values(i)
             For Each Instance As McInstance In Results.Values(i)
                 Value += Instance.Name & ":"
                 Value += Instance.Name & ":"
             Next
             Next
-            WriteIni(Path & "PCL.ini", "CardValue" & (i + 1), Value)
+            WriteIni(Folder & "PCL.ini", "CardValue" & (i + 1), Value)
         Next
         Next
 #End Region
 #End Region
         Return Results
         Return Results
@@ -1637,7 +1633,7 @@ OnLoaded:
                 Return New McSkinInfo With {.IsVaild = False}
                 Return New McSkinInfo With {.IsVaild = False}
             End If
             End If
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "皮肤文件存在错误", LogLevel.Hint)
+            Log(ex, "皮肤文件存在错误", NotifyLevel.AllUsers)
             Return New McSkinInfo With {.IsVaild = False}
             Return New McSkinInfo With {.IsVaild = False}
         End Try
         End Try
 
 
@@ -1685,7 +1681,7 @@ OnLoaded:
             Next
             Next
             Throw New Exception("未从皮肤返回值中找到符合条件的 Property")
             Throw New Exception("未从皮肤返回值中找到符合条件的 Property")
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "无法完成解析的皮肤返回值,可能是未设置自定义皮肤的用户:" & SkinString, LogLevel.Developer)
+            Log(ex, "无法完成解析的皮肤返回值,可能是未设置自定义皮肤的用户:" & SkinString, NotifyLevel.DevelopOnly)
             Throw New Exception("皮肤返回值中不包含皮肤数据项,可能是未设置自定义皮肤的用户", ex)
             Throw New Exception("皮肤返回值中不包含皮肤数据项,可能是未设置自定义皮肤的用户", ex)
         End Try
         End Try
         SkinString = Encoding.GetEncoding("utf-8").GetString(Convert.FromBase64String(SkinValue))
         SkinString = Encoding.GetEncoding("utf-8").GetString(Convert.FromBase64String(SkinValue))
@@ -1796,7 +1792,7 @@ OnLoaded:
         Public OriginalName As String
         Public OriginalName As String
 
 
         Public Overrides Function ToString() As String
         Public Overrides Function ToString() As String
-            Return If(IsNatives, "[Native] ", "") & GetString(Size) & " | " & LocalPath
+            Return If(IsNatives, "[Native] ", "") & FormatFileSize(Size) & " | " & LocalPath
         End Function
         End Function
     End Class
     End Class
 
 
@@ -1885,7 +1881,7 @@ OnLoaded:
             '不能调用 RealVersion.Check(),可能会莫名其妙地触发 CheckPermission 正被另一进程使用,导致误判前置不存在
             '不能调用 RealVersion.Check(),可能会莫名其妙地触发 CheckPermission 正被另一进程使用,导致误判前置不存在
             If Not File.Exists(RealInstance.PathVersion & RealInstance.Name & ".json") Then
             If Not File.Exists(RealInstance.PathVersion & RealInstance.Name & ".json") Then
                 RealInstance = Instance
                 RealInstance = Instance
-                Log("[Minecraft] 可能缺少前置版本 " & RealInstance.Name & ",找不到对应的 json 文件", LogLevel.Debug)
+                Log("[Minecraft] 可能缺少前置版本 " & RealInstance.Name & ",找不到对应的 json 文件", NotifyLevel.DebugModeOnly)
             End If
             End If
             '获取详细下载信息
             '获取详细下载信息
             If RealInstance.JsonObject("downloads") IsNot Nothing AndAlso RealInstance.JsonObject("downloads")("client") IsNot Nothing Then
             If RealInstance.JsonObject("downloads") IsNot Nothing AndAlso RealInstance.JsonObject("downloads")("client") IsNot Nothing Then
@@ -1933,7 +1929,7 @@ OnLoaded:
                     If Library("downloads") IsNot Nothing AndAlso Library("downloads")("artifact") IsNot Nothing Then
                     If Library("downloads") IsNot Nothing AndAlso Library("downloads")("artifact") IsNot Nothing Then
                         BasicArray.Add(New McLibToken With {
                         BasicArray.Add(New McLibToken With {
                             .OriginalName = Library("name"),
                             .OriginalName = Library("name"),
-                            .Url = If(RootUrl, Library("downloads")("artifact")("url")),
+                            .Url = If(Library("downloads")("artifact")("url"), RootUrl),
                             .LocalPath = If(Library("downloads")("artifact")("path") Is Nothing, McLibGet(Library("name"),
                             .LocalPath = If(Library("downloads")("artifact")("path") Is Nothing, McLibGet(Library("name"),
                                 CustomMcFolder:=CustomMcFolder), CustomMcFolder & "libraries\" & Library("downloads")("artifact")("path").ToString.Replace("/", "\")),
                                 CustomMcFolder:=CustomMcFolder), CustomMcFolder & "libraries\" & Library("downloads")("artifact")("path").ToString.Replace("/", "\")),
                             .Size = Val(Library("downloads")("artifact")("size").ToString),
                             .Size = Val(Library("downloads")("artifact")("size").ToString),
@@ -1947,24 +1943,26 @@ OnLoaded:
                     BasicArray.Add(New McLibToken With {.OriginalName = Library("name"), .Url = RootUrl, .LocalPath = LocalPath, .Size = 0, .IsNatives = False, .SHA1 = Nothing})
                     BasicArray.Add(New McLibToken With {.OriginalName = Library("name"), .Url = RootUrl, .LocalPath = LocalPath, .Size = 0, .IsNatives = False, .SHA1 = Nothing})
                 End Try
                 End Try
             ElseIf Library("natives")("windows") IsNot Nothing Then '有 Windows Natives
             ElseIf Library("natives")("windows") IsNot Nothing Then '有 Windows Natives
+                Dim NativeName As String = Library("natives")("windows")
                 Try
                 Try
-                    If Library("downloads") IsNot Nothing AndAlso Library("downloads")("classifiers") IsNot Nothing AndAlso Library("downloads")("classifiers")("natives-windows") IsNot Nothing Then
+                    If Library("downloads") IsNot Nothing AndAlso Library("downloads")("classifiers") IsNot Nothing AndAlso Library("downloads")("classifiers")(NativeName) IsNot Nothing Then
+                        Dim NativeData = Library("downloads")("classifiers")(NativeName)
                         BasicArray.Add(New McLibToken With {
                         BasicArray.Add(New McLibToken With {
                              .OriginalName = Library("name"),
                              .OriginalName = Library("name"),
-                             .Url = If(RootUrl, Library("downloads")("classifiers")("natives-windows")("url")),
-                             .LocalPath = If(Library("downloads")("classifiers")("natives-windows")("path") Is Nothing,
+                             .Url = If(NativeData("url"), RootUrl),
+                             .LocalPath = If(NativeData("path") Is Nothing,
                                  McLibGet(Library("name"), CustomMcFolder:=CustomMcFolder).
                                  McLibGet(Library("name"), CustomMcFolder:=CustomMcFolder).
-                                    Replace(".jar", "-" & Library("natives")("windows").ToString & ".jar").
+                                    Replace(".jar", "-" & NativeName & ".jar").
                                     Replace("${arch}", If(Environment.Is64BitOperatingSystem, "64", "32")),
                                     Replace("${arch}", If(Environment.Is64BitOperatingSystem, "64", "32")),
-                                    $"{CustomMcFolder}libraries\{Library("downloads")("classifiers")("natives-windows")("path").ToString.Replace("/", "\")}"),
-                             .Size = Val(Library("downloads")("classifiers")("natives-windows")("size").ToString),
+                                    $"{CustomMcFolder}libraries\{NativeData("path").ToString.Replace("/", "\")}"),
+                             .Size = Val(NativeData("size").ToString),
                              .IsNatives = True,
                              .IsNatives = True,
-                             .SHA1 = Library("downloads")("classifiers")("natives-windows")("sha1").ToString})
+                             .SHA1 = NativeData("sha1").ToString})
                     Else
                     Else
                         BasicArray.Add(
                         BasicArray.Add(
                         New McLibToken With {.OriginalName = Library("name"), .Url = RootUrl, .Size = 0, .IsNatives = True, .SHA1 = Nothing,
                         New McLibToken With {.OriginalName = Library("name"), .Url = RootUrl, .Size = 0, .IsNatives = True, .SHA1 = Nothing,
                             .LocalPath = McLibGet(Library("name"), CustomMcFolder:=CustomMcFolder).
                             .LocalPath = McLibGet(Library("name"), CustomMcFolder:=CustomMcFolder).
-                                 Replace(".jar", $"-{Library("natives")("windows")}.jar").
+                                 Replace(".jar", $"-{NativeName}.jar").
                                  Replace("${arch}", If(Environment.Is64BitOperatingSystem, "64", "32"))
                                  Replace("${arch}", If(Environment.Is64BitOperatingSystem, "64", "32"))
                         })
                         })
                     End If
                     End If
@@ -1973,7 +1971,7 @@ OnLoaded:
                     BasicArray.Add(
                     BasicArray.Add(
                         New McLibToken With {.OriginalName = Library("name"), .Url = RootUrl, .Size = 0, .IsNatives = True, .SHA1 = Nothing,
                         New McLibToken With {.OriginalName = Library("name"), .Url = RootUrl, .Size = 0, .IsNatives = True, .SHA1 = Nothing,
                              .LocalPath = McLibGet(Library("name"), CustomMcFolder:=CustomMcFolder).
                              .LocalPath = McLibGet(Library("name"), CustomMcFolder:=CustomMcFolder).
-                                 Replace(".jar", $"-{Library("natives")("windows")}.jar").
+                                 Replace(".jar", $"-{NativeName}.jar").
                                  Replace("${arch}", If(Environment.Is64BitOperatingSystem, "64", "32"))
                                  Replace("${arch}", If(Environment.Is64BitOperatingSystem, "64", "32"))
                         })
                         })
                 End Try
                 End Try
@@ -2026,7 +2024,7 @@ OnLoaded:
             Dim MainJar As NetFile = DlClientJarGet(Instance, True)
             Dim MainJar As NetFile = DlClientJarGet(Instance, True)
             If MainJar IsNot Nothing Then Result.Add(MainJar)
             If MainJar IsNot Nothing Then Result.Add(MainJar)
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "版本缺失主 jar 文件所必须的信息", LogLevel.Developer)
+            Log(ex, "版本缺失主 jar 文件所必须的信息", NotifyLevel.DevelopOnly)
         End Try
         End Try
 
 
         'Library 文件
         'Library 文件
@@ -2077,11 +2075,11 @@ OnLoaded:
 
 
         '跳过校验
         '跳过校验
         If ShouldIgnoreFileCheck(Instance) Then
         If ShouldIgnoreFileCheck(Instance) Then
-            Log("[Minecraft] 设置要求尽量忽略文件检查,这可能会保留有误的文件", LogLevel.Debug)
+            Log("[Minecraft] 设置要求尽量忽略文件检查,这可能会保留有误的文件", NotifyLevel.DebugModeOnly)
             Result = Result.Where(
             Result = Result.Where(
             Function(f)
             Function(f)
                 If File.Exists(f.LocalPath) Then
                 If File.Exists(f.LocalPath) Then
-                    Log("[Minecraft] 跳过下载的支持库文件:" & f.LocalPath, LogLevel.Debug)
+                    Log("[Minecraft] 跳过下载的支持库文件:" & f.LocalPath, NotifyLevel.DebugModeOnly)
                     Return False
                     Return False
                 Else
                 Else
                     Return True
                     Return True
@@ -2101,7 +2099,7 @@ OnLoaded:
         For Each Token As McLibToken In Libs
         For Each Token As McLibToken In Libs
             '检查器
             '检查器
             Dim Checker As FileChecker
             Dim Checker As FileChecker
-            If Token.Name.ContainsF("labymod") Then
+            If Token.Name.ContainsIgnoreCase("labymod") Then
                 Checker = New FileChecker '不检查 LabyMod 的文件,它们提供的文件校验信息是错的(#3225)
                 Checker = New FileChecker '不检查 LabyMod 的文件,它们提供的文件校验信息是错的(#3225)
             Else
             Else
                 Checker = New FileChecker(ActualSize:=If(Token.Size = 0, -1, Token.Size), Hash:=Token.SHA1)
                 Checker = New FileChecker(ActualSize:=If(Token.Size = 0, -1, Token.Size), Hash:=Token.SHA1)
@@ -2120,7 +2118,7 @@ OnLoaded:
                 End If
                 End If
                 If Token.Url.Contains("maven") Then
                 If Token.Url.Contains("maven") Then
                     Dim BmclapiUrl As String =
                     Dim BmclapiUrl As String =
-                        Token.Url.Replace(Mid(Token.Url, 1, Token.Url.IndexOfF("maven")), "https://bmclapi2.bangbang93.com/").Replace("maven.fabricmc.net", "maven").Replace("maven.minecraftforge.net", "maven").Replace("maven.neoforged.net/releases", "maven")
+                        Token.Url.Replace(Token.Url.Substring(0, Token.Url.IndexOfF("maven")), "https://bmclapi2.bangbang93.com/").Replace("maven.fabricmc.net", "maven").Replace("maven.minecraftforge.net", "maven").Replace("maven.neoforged.net/releases", "maven")
                     If DlSourcePreferMojang Then
                     If DlSourcePreferMojang Then
                         Urls.Add(BmclapiUrl) '官方源优先
                         Urls.Add(BmclapiUrl) '官方源优先
                     Else
                     Else
@@ -2130,8 +2128,8 @@ OnLoaded:
             End If
             End If
             If Token.LocalPath.Contains("transformer-discovery-service") Then
             If Token.LocalPath.Contains("transformer-discovery-service") Then
                 'Transformer 文件释放
                 'Transformer 文件释放
-                If Not File.Exists(Token.LocalPath) Then WriteFile(Token.LocalPath, GetResources("Transformer"))
-                Log("[Download] 已自动释放 Transformer Discovery Service", LogLevel.Developer)
+                If Not File.Exists(Token.LocalPath) Then FileUtils.Write(Token.LocalPath, GetResources("Transformer"))
+                Log("[Download] 已自动释放 Transformer Discovery Service", NotifyLevel.DevelopOnly)
                 Continue For
                 Continue For
             ElseIf Token.LocalPath.Contains("optifine\OptiFine") Then
             ElseIf Token.LocalPath.Contains("optifine\OptiFine") Then
                 'OptiFine 主 Jar
                 'OptiFine 主 Jar
@@ -2146,7 +2144,7 @@ OnLoaded:
             Result.Add(New NetFile(Urls.Distinct, Token.LocalPath, Checker))
             Result.Add(New NetFile(Urls.Distinct, Token.LocalPath, Checker))
         Next
         Next
         '去重并返回
         '去重并返回
-        Return Result.Distinct(Function(a, b) a.LocalPath = b.LocalPath)
+        Return Result.DistinctBy(Function(a) a.LocalPath).ToList
     End Function
     End Function
     ''' <summary>
     ''' <summary>
     ''' 获取对应的支持库文件地址。
     ''' 获取对应的支持库文件地址。
@@ -2253,7 +2251,7 @@ OnLoaded:
         Public Hash As String
         Public Hash As String
 
 
         Public Overrides Function ToString() As String
         Public Overrides Function ToString() As String
-            Return GetString(Size) & " | " & LocalPath
+            Return FormatFileSize(Size) & " | " & LocalPath
         End Function
         End Function
     End Structure
     End Structure
     ''' <summary>
     ''' <summary>
@@ -2367,7 +2365,7 @@ OnLoaded:
             End If
             End If
 
 
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "Minecraft 更新提示发送失败(" & If(VersionName, "Nothing") & ")", LogLevel.Feedback)
+            Log(ex, "Minecraft 更新提示发送失败(" & If(VersionName, "Nothing") & ")", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -2459,7 +2457,7 @@ NextEntry:
         End If
         End If
         '打码当前登录的结果
         '打码当前登录的结果
         Dim AccessToken As String = McLoginLoader.Output.AccessToken
         Dim AccessToken As String = McLoginLoader.Output.AccessToken
-        If AccessToken IsNot Nothing AndAlso AccessToken.Length >= 10 AndAlso Raw.ContainsF(AccessToken, True) AndAlso
+        If AccessToken IsNot Nothing AndAlso AccessToken.Length >= 10 AndAlso Raw.ContainsIgnoreCase(AccessToken) AndAlso
             McLoginLoader.Output.Uuid <> McLoginLoader.Output.AccessToken Then 'UUID 和 AccessToken 一样则不打码
             McLoginLoader.Output.Uuid <> McLoginLoader.Output.AccessToken Then 'UUID 和 AccessToken 一样则不打码
             Raw = Raw.Replace(AccessToken, Left(AccessToken, 5) & New String(FilterChar, AccessToken.Length - 10) & Right(AccessToken, 5))
             Raw = Raw.Replace(AccessToken, Left(AccessToken, 5) & New String(FilterChar, AccessToken.Length - 10) & Right(AccessToken, 5))
         End If
         End If

+ 0 - 876
Plain Craft Launcher 2/Modules/Minecraft/ModMod.vb

@@ -1,876 +0,0 @@
-Imports System.IO.Compression
-
-Public Module ModMod
-    Private Const LocalModCacheVersion As Integer = 13
-
-    Public Class McMod
-
-#Region "基础"
-
-        ''' <summary>
-        ''' Mod 文件的地址。
-        ''' </summary>
-        Public ReadOnly Path As String
-        Public Sub New(Path As String)
-            Me.Path = If(Path, "")
-        End Sub
-        ''' <summary>
-        ''' Mod 的完整路径,去除最后的 .disabled 和 .old。
-        ''' </summary>
-        Public ReadOnly Property RawPath As String
-            Get
-                Return GetPathFromFullPath(Path) & RawFileName
-            End Get
-        End Property
-
-        ''' <summary>
-        ''' Mod 的完整文件名。
-        ''' </summary>
-        Public ReadOnly Property FileName As String
-            Get
-                Return GetFileNameFromPath(Path)
-            End Get
-        End Property
-
-        ''' <summary>
-        ''' Mod 的完整文件名,去除最后的 .disabled 和 .old。
-        ''' </summary>
-        Public ReadOnly Property RawFileName As String
-            Get
-                Return FileName.Replace(".disabled", "").Replace(".old", "")
-            End Get
-        End Property
-
-        ''' <summary>
-        ''' Mod 的状态。
-        ''' </summary>
-        Public ReadOnly Property State As McModState
-            Get
-                If Path.EndsWithF(".disabled", True) OrElse Path.EndsWithF(".old", True) Then
-                    Return McModState.Disabled
-                Else
-                    Return McModState.Fine
-                End If
-            End Get
-        End Property
-        Public Enum McModState As Integer
-            Fine = 0
-            Disabled = 1
-        End Enum
-
-#End Region
-
-#Region "信息项"
-
-        ''' <summary>
-        ''' 当任何信息在初始化后更新时触发。
-        ''' </summary>
-        Public Event OnUpdate(sender As McMod)
-
-        ''' <summary>
-        ''' Mod 的可读名称。
-        ''' 可能为无扩展的文件名,但不会是 Nothing。
-        ''' </summary>
-        Public Property DisplayName As String
-            Get
-                If _Name Is Nothing Then _Name = CompFile?.DisplayName
-                If _Name Is Nothing Then _Name = IO.Path.GetFileNameWithoutExtension(Path)
-                Return _Name
-            End Get
-            Set(value As String)
-                If _Name Is Nothing AndAlso value IsNot Nothing AndAlso Not value.Contains("modname") AndAlso
-                   value.Lower <> "name" AndAlso value.Count > 1 AndAlso Val(value).ToString <> value Then
-                    _Name = value
-                End If
-            End Set
-        End Property
-        Private _Name As String = Nothing
-
-        ''' <summary>
-        ''' Mod 的描述信息。
-        ''' 可能为 Nothing。
-        ''' </summary>
-        Public Property Description As String
-            Get
-                Return _Description
-            End Get
-            Set(value As String)
-                If _Description Is Nothing AndAlso value IsNot Nothing AndAlso value.Count > 2 Then
-                    _Description = value.ToString.Trim(vbLf)
-                    '优化显示:若以 [a-zA-Z0-9] 结尾,加上小数点句号
-                    If _Description.Lower.LastIndexOfAny("qwertyuiopasdfghjklzxcvbnm0123456789") = _Description.Count - 1 Then _Description += "."
-                End If
-            End Set
-        End Property
-        Private _Description As String = Nothing
-
-        ''' <summary>
-        ''' Mod 的版本。
-        ''' 不保证符合版本格式规范,且可能为 Nothing。
-        ''' </summary>
-        Public Property Version As String
-            Get
-                If _Version Is Nothing Then _Version = CompFile?.Version
-                Return _Version
-            End Get
-            Set(value As String)
-                If _Version IsNot Nothing AndAlso RegexCheck(_Version, "[0-9.\-]+") Then Return
-                If value IsNot Nothing AndAlso value.ContainsF("version", True) Then value = "version" '需要修改的标识
-                _Version = value
-            End Set
-        End Property
-        Public _Version As String = Nothing
-
-#End Region
-
-#Region "从 JAR 获取信息"
-
-        ''' <summary>
-        ''' 从 JAR 文件中获取 Mod 信息。
-        ''' </summary>
-        Public Sub LoadMetadataFromJar()
-            Static IsLoaded As Boolean = False
-            If IsLoaded Then Return
-            IsLoaded = True
-            Dim Jar As ZipArchive = Nothing
-            Try
-                '基础可用性检查、打开 Jar 文件
-                If Path.Length < 2 Then Throw New FileNotFoundException("错误的 Mod 文件路径(" & If(Path, "null") & ")")
-                If Not File.Exists(Path) Then Throw New FileNotFoundException("未找到 Mod 文件(" & Path & ")")
-                Jar = New ZipArchive(New FileStream(Path, FileMode.Open, FileAccess.Read, FileShare.Read))
-                '信息获取
-                LoadMetadataFromJar(Jar)
-            Catch ex As UnauthorizedAccessException
-                Log(ex, "Mod 文件由于无权限无法打开(" & Path & ")", LogLevel.Developer)
-            Catch ex As Exception
-                Log(ex, "Mod 文件无法打开(" & Path & ")", LogLevel.Developer)
-            Finally
-                If Jar IsNot Nothing Then Jar.Dispose()
-            End Try
-        End Sub
-        Private Sub LoadMetadataFromJar(Jar As ZipArchive)
-
-#Region "尝试使用 mcmod.info"
-            Try
-                '获取信息文件
-                Dim InfoEntry As ZipArchiveEntry = Jar.GetEntry("mcmod.info")
-                Dim InfoString As String = Nothing
-                If InfoEntry IsNot Nothing Then
-                    InfoString = ReadFile(InfoEntry.Open())
-                    If InfoString.Length < 15 Then InfoString = Nothing
-                End If
-                If InfoString Is Nothing Then Exit Try
-                '获取可用 Json 项
-                Dim InfoObject As JObject
-                Dim JsonObject = GetJson(InfoString)
-                If JsonObject.Type = JTokenType.Array Then
-                    InfoObject = JsonObject(0)
-                Else
-                    InfoObject = JsonObject("modList")(0)
-                End If
-                '从文件中获取 Mod 信息项
-                DisplayName = InfoObject("name")
-                Description = InfoObject("description")
-                Version = InfoObject("version")
-            Catch ex As Exception
-                Log(ex, "读取 mcmod.info 时出现未知错误(" & Path & ")", LogLevel.Developer)
-            End Try
-#End Region
-#Region "尝试使用 fabric.mod.json"
-            Try
-                '获取 fabric.mod.json 文件
-                Dim FabricEntry As ZipArchiveEntry = Jar.GetEntry("fabric.mod.json")
-                Dim FabricText As String = Nothing
-                If FabricEntry IsNot Nothing Then
-                    FabricText = ReadFile(FabricEntry.Open(), Encoding.UTF8)
-                    If Not FabricText.Contains("schemaVersion") Then FabricText = Nothing
-                End If
-                If FabricText Is Nothing Then Exit Try
-                Dim FabricObject As JObject = GetJson(FabricText)
-GotFabric:
-                '从文件中获取 Mod 信息项
-                If FabricObject.ContainsKey("name") Then DisplayName = FabricObject("name")
-                If FabricObject.ContainsKey("version") Then Version = FabricObject("version")
-                If FabricObject.ContainsKey("description") Then Description = FabricObject("description")
-                '加载成功
-                GoTo Finished
-            Catch ex As Exception
-                Log(ex, "读取 fabric.mod.json 时出现未知错误(" & Path & ")", LogLevel.Developer)
-            End Try
-#End Region
-#Region "尝试使用 mods.toml"
-            Try
-                '获取 mods.toml 文件
-                Dim TomlEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/mods.toml")
-                Dim TomlText As String = Nothing
-                If TomlEntry IsNot Nothing Then
-                    TomlText = ReadFile(TomlEntry.Open())
-                    If TomlText.Length < 15 Then TomlText = Nothing
-                End If
-                If TomlText Is Nothing Then Exit Try
-                '文件标准化:统一换行符为 vbLf,去除注释、头尾的空格、空行
-                Dim Lines As New List(Of String)
-                For Each Line In TomlText.Replace(vbCrLf, vbLf).Replace(vbCr, vbLf).Split(vbLf) '统一换行符
-                    If Line.StartsWithF("#") Then '去除注释
-                        Continue For
-                    ElseIf Line.Contains("#") Then
-                        Line = Line.Substring(0, Line.IndexOfF("#"))
-                    End If
-                    Line = Line.Trim(New Char() {" "c, "	"c, " "c}) '去除头尾的空格
-                    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))}
-                For i = 0 To Lines.Count - 1
-                    Dim Line As String = Lines(i)
-                    If Line.StartsWithF("[") AndAlso Line.EndsWithF("]") Then
-                        '段落标记
-                        Dim Header = Line.Trim("[]".ToCharArray)
-                        TomlData.Add(New KeyValuePair(Of String, Dictionary(Of String, Object))(Header, New Dictionary(Of String, Object)))
-                    ElseIf Line.Contains("=") Then
-                        '字段标记
-                        Dim Key As String = Line.Substring(0, Line.IndexOfF("=")).TrimEnd(New Char() {" "c, "	"c, " "c})
-                        Dim RawValue As String = Line.Substring(Line.IndexOfF("=") + 1).TrimStart(New Char() {" "c, "	"c, " "c})
-                        Dim Value As Object
-                        If RawValue.StartsWithF("""") AndAlso RawValue.EndsWithF("""") Then
-                            '单行字符串
-                            Value = RawValue.Trim("""")
-                        ElseIf RawValue.StartsWithF("'''") Then
-                            '多行字符串
-                            Dim ValueLines As New List(Of String) From {RawValue.TrimStart("'")}
-                            If ValueLines(0).EndsWithF("'''") Then '把多行字符串按单行写法写的错误处理(#2732)
-                                ValueLines(0) = ValueLines(0).TrimEnd("'")
-                            Else
-                                Do Until i >= Lines.Count - 1
-                                    i += 1
-                                    Dim ValueLine As String = Lines(i)
-                                    If ValueLine.EndsWithF("'''") Then
-                                        ValueLines.Add(ValueLine.TrimEnd("'"))
-                                        Exit Do
-                                    Else
-                                        ValueLines.Add(ValueLine)
-                                    End If
-                                Loop
-                            End If
-                            Value = ValueLines.Join(vbLf).Trim(vbLf).Replace(vbLf, vbCrLf)
-                        ElseIf RawValue.Lower = "true" OrElse RawValue.Lower = "false" Then
-                            '布尔型
-                            Value = (RawValue.Lower = "true")
-                        ElseIf Val(RawValue).ToString = RawValue Then
-                            '数字型
-                            Value = Val(RawValue)
-                        Else
-                            '不知道是个啥玩意儿,直接存储
-                            Value = RawValue
-                        End If
-                        TomlData.Last.Value(Key) = Value
-                    Else
-                        '不知道是个啥玩意儿
-                        Exit Try
-                    End If
-                Next
-                '从文件数据中获取信息
-                Dim ModEntry As Dictionary(Of String, Object) = Nothing
-                For Each TomlSubData In TomlData
-                    If TomlSubData.Key = "mods" Then
-                        ModEntry = TomlSubData.Value
-                        Exit For
-                    End If
-                Next
-                If ModEntry Is Nothing OrElse Not ModEntry.ContainsKey("modId") Then Exit Try
-                If ModEntry.ContainsKey("displayName") Then DisplayName = ModEntry("displayName")
-                If ModEntry.ContainsKey("description") Then Description = ModEntry("description")
-                If ModEntry.ContainsKey("version") Then Version = ModEntry("version")
-                '加载成功
-                GoTo Finished
-            Catch ex As Exception
-                Log(ex, "读取 mods.toml 时出现未知错误(" & Path & ")", LogLevel.Developer)
-            End Try
-#End Region
-#Region "尝试使用 fml_cache_annotation.json"
-            Try
-                '获取 fml_cache_annotation.json 文件
-                Dim FmlEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/fml_cache_annotation.json")
-                Dim FmlText As String = Nothing
-                If FmlEntry IsNot Nothing Then
-                    FmlText = ReadFile(FmlEntry.Open(), Encoding.UTF8)
-                    If Not FmlText.Contains("Lnet/minecraftforge/fml/common/Mod;") Then FmlText = Nothing
-                End If
-                If FmlText Is Nothing Then Exit Try
-                Dim FmlJson As JObject = GetJson(FmlText)
-                '获取可用 Json 项
-                Dim FmlObject As JObject = Nothing
-                For Each ModFilePair In FmlJson
-                    Dim ModFileAnnos As JArray = ModFilePair.Value("annotations")
-                    If ModFileAnnos IsNot Nothing Then
-                        '先获取 Mod
-                        For Each ModFileAnno In ModFileAnnos
-                            Dim Name As String = If(ModFileAnno("name"), "")
-                            If Name = "Lnet/minecraftforge/fml/common/Mod;" Then
-                                FmlObject = ModFileAnno("values")
-                                GoTo Got
-                            End If
-                        Next
-                    End If
-                Next
-                Exit Try
-Got:
-                '从文件中获取 Mod 信息项
-                If FmlObject.ContainsKey("name") Then DisplayName = FmlObject("name")("value")
-                If FmlObject.ContainsKey("version") Then Version = FmlObject("version")("value")
-                '加载成功
-                GoTo Finished
-            Catch ex As Exception
-                Log(ex, "读取 fml_cache_annotation.json 时出现未知错误(" & Path & ")", LogLevel.Developer)
-            End Try
-#End Region
-            Return '没能成功获取任何信息
-
-Finished:
-#Region "将 Version 代号转换为 META-INF 中的版本"
-            If _Version = "version" Then
-                Try
-                    Dim MetaEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/MANIFEST.MF")
-                    If MetaEntry IsNot Nothing Then
-                        Dim MetaString As String = ReadFile(MetaEntry.Open()).Replace(" :", ":").Replace(": ", ":")
-                        If MetaString.Contains("Implementation-Version:") Then
-                            MetaString = MetaString.Substring(MetaString.IndexOfF("Implementation-Version:") + "Implementation-Version:".Count)
-                            MetaString = MetaString.Substring(0, MetaString.IndexOfAny(vbCrLf.ToCharArray)).Trim
-                            Version = MetaString
-                        End If
-                    End If
-                Catch ex As Exception
-                    Log("获取 META-INF 中的版本信息失败(" & Path & ")", LogLevel.Developer)
-                    Version = Nothing
-                End Try
-            End If
-            If _Version IsNot Nothing AndAlso Not (_Version.Contains(".") OrElse _Version.Contains("-")) Then Version = Nothing
-#End Region
-            RaiseEvent OnUpdate(Me)
-
-        End Sub
-
-#End Region
-
-#Region "网络信息"
-
-        ''' <summary>
-        ''' 该 Mod 关联的网络项目。
-        ''' </summary>
-        Public Property Comp As CompProject
-            Get
-                Return _Comp
-            End Get
-            Set(value As CompProject)
-                _Comp = value
-                RaiseEvent OnUpdate(Me)
-            End Set
-        End Property
-        Private _Comp As CompProject
-
-        ''' <summary>
-        ''' 本地文件对应的联网文件信息。
-        ''' </summary>
-        Public CompFile As CompFile
-
-        ''' <summary>
-        ''' 该 Mod 对应的联网最新版本。
-        ''' </summary>
-        Public Property UpdateFile As CompFile
-            Get
-                Return _UpdateFile
-            End Get
-            Set(value As CompFile)
-                _UpdateFile = value
-                RaiseEvent OnUpdate(Me)
-            End Set
-        End Property
-        Private _UpdateFile As CompFile
-
-        ''' <summary>
-        ''' 该 Mod 的更新日志网址。
-        ''' </summary>
-        Public ChangelogUrls As New List(Of String)
-        ''' <summary>
-        ''' 所有网络信息是否已成功加载。
-        ''' </summary>
-        Public CompLoaded As Boolean = False
-
-        ''' <summary>
-        ''' 将网络信息保存为 Json。
-        ''' </summary>
-        Public Function ToJson() As JObject
-            Dim Json As New JObject
-            If Comp IsNot Nothing Then Json.Add("Comp", Comp.ToJson())
-            Json.Add("ChangelogUrls", New JArray(ChangelogUrls))
-            Json.Add("CompLoaded", CompLoaded)
-            If CompFile IsNot Nothing Then Json.Add("CompFile", CompFile.ToJson())
-            If UpdateFile IsNot Nothing Then Json.Add("UpdateFile", UpdateFile.ToJson())
-            Return Json
-        End Function
-        ''' <summary>
-        ''' 从 Json 中读取网络信息。
-        ''' </summary>
-        Public Sub FromJson(Json As JObject)
-            CompLoaded = Json("CompLoaded")
-            If Json.ContainsKey("Comp") Then Comp = New CompProject(Json("Comp"))
-            If Json.ContainsKey("ChangelogUrls") Then ChangelogUrls = Json("ChangelogUrls").ToObject(Of List(Of String))
-            If Json.ContainsKey("CompFile") Then CompFile = New CompFile(Json("CompFile"), CompType.Mod)
-            If Json.ContainsKey("UpdateFile") Then UpdateFile = New CompFile(Json("UpdateFile"), CompType.Mod)
-        End Sub
-
-        ''' <summary>
-        ''' 该文件是否可以更新。
-        ''' </summary>
-        Public ReadOnly Property CanUpdate As Boolean
-            Get
-                Return Not Settings.Get("UiHiddenFunctionModUpdate") AndAlso Not Settings.Get("VersionAdvanceDisableModUpdate", Instance:=PageInstanceLeft.Instance) AndAlso UpdateFile IsNot Nothing
-            End Get
-        End Property
-
-        ''' <summary>
-        ''' 获取用于 CurseForge 信息获取的 Hash 值(MurmurHash2)。
-        ''' </summary>
-        Public ReadOnly Property CurseForgeHash As UInteger
-            Get
-                If _CurseForgeHash Is Nothing Then
-                    '读取缓存
-                    Dim Info As New FileInfo(Path)
-                    Dim CacheKey As String = GetHash($"{RawPath}-{Info.LastWriteTime.ToLongTimeString}-{Info.Length}-C")
-                    Dim Cached As String = ReadIni(PathTemp & "Cache\ModHash.ini", CacheKey)
-                    If Cached <> "" AndAlso RegexCheck(Cached, "^\d+$") Then '#5062
-                        _CurseForgeHash = Cached
-                        Return _CurseForgeHash
-                    End If
-                    '读取文件
-                    Dim data As New List(Of Byte)
-                    For Each b As Byte In ReadFileBytes(Path)
-                        If b = 9 OrElse b = 10 OrElse b = 13 OrElse b = 32 Then Continue For
-                        data.Add(b)
-                    Next
-                    '计算 MurmurHash2
-                    Dim length As Integer = data.Count
-                    Dim h As UInteger = 1 Xor length '1 是种子
-                    Dim i As Integer
-                    For i = 0 To length - 4 Step 4
-                        Dim k As UInteger = data(i) Or CUInt(data(i + 1)) << 8 Or CUInt(data(i + 2)) << 16 Or CUInt(data(i + 3)) << 24
-                        k = (k * &H5BD1E995L) And &HFFFFFFFFL
-                        k = k Xor (k >> 24)
-                        k = (k * &H5BD1E995L) And &HFFFFFFFFL
-                        h = (h * &H5BD1E995L) And &HFFFFFFFFL
-                        h = h Xor k
-                    Next
-                    Select Case length - i
-                        Case 3
-                            h = h Xor (data(i) Or CUInt(data(i + 1)) << 8)
-                            h = h Xor (CUInt(data(i + 2)) << 16)
-                            h = (h * &H5BD1E995L) And &HFFFFFFFFL
-                        Case 2
-                            h = h Xor (data(i) Or CUInt(data(i + 1)) << 8)
-                            h = (h * &H5BD1E995L) And &HFFFFFFFFL
-                        Case 1
-                            h = h Xor data(i)
-                            h = (h * &H5BD1E995L) And &HFFFFFFFFL
-                    End Select
-                    h = h Xor (h >> 13)
-                    h = (h * &H5BD1E995L) And &HFFFFFFFFL
-                    h = h Xor (h >> 15)
-                    _CurseForgeHash = h
-                    '写入缓存
-                    WriteIni(PathTemp & "Cache\ModHash.ini", CacheKey, h.ToString)
-                End If
-                Return _CurseForgeHash
-            End Get
-        End Property
-        Private _CurseForgeHash As UInteger?
-
-        ''' <summary>
-        ''' 获取用于 Modrinth 信息获取的 Hash 值(SHA1)。
-        ''' </summary>
-        Public ReadOnly Property ModrinthHash As String
-            Get
-                If _ModrinthHash Is Nothing Then
-                    '读取缓存
-                    Dim Info As New FileInfo(Path)
-                    Dim CacheKey As String = GetHash($"{RawPath}-{Info.LastWriteTime.ToLongTimeString}-{Info.Length}-M")
-                    Dim Cached As String = ReadIni(PathTemp & "Cache\ModHash.ini", CacheKey)
-                    If Cached <> "" Then
-                        _ModrinthHash = Cached
-                        Return _ModrinthHash
-                    End If
-                    '计算 SHA1
-                    _ModrinthHash = GetFileSHA1(Path)
-                    '写入缓存
-                    WriteIni(PathTemp & "Cache\ModHash.ini", CacheKey, _ModrinthHash)
-                End If
-                Return _ModrinthHash
-            End Get
-        End Property
-        Private _ModrinthHash As String
-
-#End Region
-
-#Region "API"
-
-        Public Overrides Function ToString() As String
-            Return $"{State} - {Path}"
-        End Function
-        Public Overrides Function Equals(obj As Object) As Boolean
-            Dim target = TryCast(obj, McMod)
-            Return target IsNot Nothing AndAlso Path = target.Path
-        End Function
-
-#End Region
-
-        ''' <summary>
-        ''' 是否可能为前置 Mod。目前非常不准确。
-        ''' </summary>
-        Public Function IsPresetMod() As Boolean
-            Return DisplayName IsNot Nothing AndAlso (DisplayName.Lower.Contains("core") OrElse DisplayName.Lower.Contains("lib"))
-        End Function
-
-        ''' <summary>
-        ''' 根据完整文件路径的文件扩展名判断是否为 Mod 文件。
-        ''' </summary>
-        Public Shared Function IsModFile(Path As String)
-            If Path Is Nothing OrElse Not Path.Contains(".") Then Return False
-            Path = Path.Lower
-            If Path.EndsWithF(".jar", True) OrElse Path.EndsWithF(".zip", True) OrElse Path.EndsWithF(".litemod", True) OrElse
-               Path.EndsWithF(".jar.disabled", True) OrElse Path.EndsWithF(".zip.disabled", True) OrElse Path.EndsWithF(".litemod.disabled", True) OrElse
-               Path.EndsWithF(".jar.old", True) OrElse Path.EndsWithF(".zip.old", True) OrElse Path.EndsWithF(".litemod.old", True) Then Return True
-            Return False
-        End Function
-
-    End Class
-
-    '加载 Mod 列表
-    Public McModLoader As New LoaderTask(Of String, List(Of McMod))("Mod List Loader", AddressOf McModLoad)
-    Private Sub McModLoad(Loader As LoaderTask(Of String, List(Of McMod)))
-        Try
-            RunInUiWait(Sub() If FrmInstanceMod IsNot Nothing Then FrmInstanceMod.Load.ShowProgress = False)
-
-            '等待 Mod 更新完成
-            If PageInstanceMod.UpdatingInstanceModFolders.Contains(Loader.Input) Then
-                Log($"[Mod] 等待 Mod 更新完成后才能继续加载 Mod 列表:" & Loader.Input)
-                Try
-                    RunInUiWait(Sub() If FrmInstanceMod IsNot Nothing Then FrmInstanceMod.Load.Text = "正在更新 Mod")
-                    Do Until Not PageInstanceMod.UpdatingInstanceModFolders.Contains(Loader.Input)
-                        If Loader.IsInterrupted Then Return
-                        Thread.Sleep(100)
-                    Loop
-                Finally
-                    RunInUiWait(Sub() If FrmInstanceMod IsNot Nothing Then FrmInstanceMod.Load.Text = "正在加载 Mod 列表")
-                End Try
-                FrmInstanceMod.LoaderRun(LoaderFolderRunType.UpdateOnly)
-            End If
-
-            '获取 Mod 文件夹下的可用文件列表
-            Dim ModFileList As New List(Of FileInfo)
-            If Directory.Exists(Loader.Input) Then
-                Dim RawName As String = Loader.Input.Lower
-                For Each File As FileInfo In EnumerateFiles(Loader.Input)
-                    If File.DirectoryName.Lower & "\" <> RawName Then
-                        '仅当 Forge 1.13- 且文件夹名与版本号相同时,才加载该子文件夹下的 Mod
-                        If Not (PageInstanceLeft.Instance IsNot Nothing AndAlso PageInstanceLeft.Instance.Version.HasForge AndAlso
-                                PageInstanceLeft.Instance.Version.Vanilla.Major < 13 AndAlso
-                                File.Directory.Name = $"1.{PageInstanceLeft.Instance.Version.Vanilla.Major}.{PageInstanceLeft.Instance.Version.Vanilla.Build}") Then
-                            Continue For
-                        End If
-                    End If
-                    If McMod.IsModFile(File.FullName) Then ModFileList.Add(File)
-                Next
-            End If
-
-            '获取本地文件缓存
-            Dim CachePath As String = PathTemp & "Cache\LocalMod.json"
-            Dim Cache As New JObject
-            Try
-                Dim CacheContent As String = ReadFile(CachePath)
-                If Not String.IsNullOrWhiteSpace(CacheContent) Then
-                    Cache = GetJson(CacheContent)
-                    If Not Cache.ContainsKey("version") OrElse Cache("version").ToObject(Of Integer) <> LocalModCacheVersion Then
-                        Log($"[Mod] 本地 Mod 信息缓存版本已过期,将弃用这些缓存信息", LogLevel.Debug)
-                        Cache = New JObject
-                    End If
-                End If
-            Catch ex As Exception
-                Log(ex, "读取本地 Mod 信息缓存失败,已重置")
-                Cache = New JObject
-            End Try
-            Cache("version") = LocalModCacheVersion
-
-            '加载 Mod 列表
-            Dim ModList As New List(Of McMod)
-            For Each ModFile As FileInfo In ModFileList
-                If Loader.IsInterrupted Then Return
-                Dim ModEntry As New McMod(ModFile.FullName)
-                Dim DumpMod As McMod = ModList.FirstOrDefault(Function(m) m.RawFileName = ModEntry.RawFileName) '存在两个文件,名称相同,但一个启用一个禁用
-                If DumpMod IsNot Nothing Then
-                    Dim DisabledMod As McMod = If(DumpMod.State = McMod.McModState.Disabled, DumpMod, ModEntry)
-                    Log($"[Mod] 重复的 Mod 文件:{DumpMod.FileName} 与 {ModEntry.FileName},已忽略 {DisabledMod.FileName}", LogLevel.Debug)
-                    If DisabledMod Is ModEntry Then
-                        Continue For
-                    Else
-                        ModList.Remove(DisabledMod)
-                    End If
-                End If
-                ModList.Add(ModEntry)
-            Next
-            Log($"[Mod] 共发现 {ModList.Count} 个 Mod")
-
-            '排序
-            ModList = ModList.OrderBy(Function(m) m.FileName).ToList
-
-            '回设
-            If Loader.IsInterrupted Then Return
-            Loader.Output = ModList
-
-            '开始联网加载
-            'TODO: 添加信息获取中提示
-            McModDetailLoader.Start(New KeyValuePair(Of List(Of McMod), JObject)(ModList, Cache), IsForceRestart:=True)
-
-        Catch ex As Exception
-            Log(ex, "Mod 列表加载失败", LogLevel.Debug)
-            Throw
-        End Try
-    End Sub
-    '联网加载 Mod 详情
-    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 New List(Of McMod)
-        Dim Cache As JObject = Loader.Input.Value
-        '读取 Comp 缓存,获取需要更新的 Mod 列表
-        For Each ModEntry As McMod In Loader.Input.Key
-            If Loader.IsInterrupted Then Return
-            Dim CacheKey = ModEntry.ModrinthHash & PageInstanceLeft.Instance.Version.VanillaName & GetTargetModLoaders().Join("")
-            If Cache.ContainsKey(CacheKey) Then
-                ModEntry.FromJson(Cache(CacheKey))
-                '如果缓存中的信息在 6 小时以内更新过,则无需重新获取
-                If ModEntry.CompLoaded AndAlso Date.Now - Cache(CacheKey)("Comp")("CacheTime").ToObject(Of Date) < New TimeSpan(6, 0, 0) Then Continue For
-            End If
-            Mods.Add(ModEntry)
-        Next
-        Log($"[Mod] 有 {Mods.Where(Function(m) m.Comp Is Nothing).Count} 个 Mod 需要联网获取信息,{Mods.Where(Function(m) m.Comp IsNot Nothing).Count} 个 Mod 需要更新信息")
-        If Not Mods.Any Then
-            Loader.Input.Key.Where(Function(m) m.Version Is Nothing).ForEach(Sub(m) m.LoadMetadataFromJar())  '从 JAR 中获取缺失的版本信息(下面有另一个分支)
-            Return
-        End If
-        '获取作为检查目标的加载器和版本
-        '此处不应向下扩展检查的 MC 小版本,例如 Mod 在更新 1.16.5 后,对早期的 1.16.2 版本发布了修补补丁,这会导致 PCL 将 1.16.5 版本的 Mod 降级到 1.16.2
-        Dim TargetMcVersion As McVersion = PageInstanceLeft.Instance.Version
-        Dim ModLoaders = GetTargetModLoaders()
-        Dim VanillaVersion = TargetMcVersion.VanillaName
-        '开始网络获取
-        Log($"[Mod] 目标加载器:{ModLoaders.Join("/")},版本:{VanillaVersion}")
-        Dim EndedThreadCount As Integer = 0, IsFailed As Boolean = False
-        Dim CurrentTaskThread As Thread = Thread.CurrentThread
-        '从 Modrinth 获取信息
-        RunInNewThread(
-        Sub()
-            Try
-                '步骤 1:获取 Hash 与对应的工程 ID
-                Dim ModrinthHashes = Mods.Select(Function(m) m.ModrinthHash).ToList()
-                Dim ModrinthVersion As JObject = DlModRequest("https://api.modrinth.com/v2/version_files", HttpMethod.Post,
-                    $"{{""hashes"": [""{ModrinthHashes.Join(""",""")}""], ""algorithm"": ""sha1""}}", "application/json")
-                Log($"[Mod] 从 Modrinth 获取到 {ModrinthVersion.Count} 个本地 Mod 的对应信息")
-                '步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系
-                If ModrinthVersion.Count = 0 Then Return
-                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)
-                    ModrinthMapping(ProjectId).Add(Entry)
-                    '记录对应的 CompFile
-                    Dim File As New CompFile(ModrinthVersion(Entry.ModrinthHash), CompType.Mod)
-                    If Entry.CompFile Is Nothing OrElse Entry.CompFile.ReleaseDate < File.ReleaseDate Then
-                        Entry.CompFile = File
-                    Else
-                        Entry.CompFile.Version = File.Version '使用来自 Modrinth 的版本号
-                    End If
-                Next
-                If Loader.IsInterruptedWithThread(CurrentTaskThread) Then Return
-                Log($"[Mod] 需要从 Modrinth 获取 {ModrinthMapping.Count} 个本地 Mod 的工程信息")
-                '步骤 3:获取工程信息
-                If Not ModrinthMapping.Any() Then Return
-                Dim ModrinthProject As JArray = DlModRequest(
-                    $"https://api.modrinth.com/v2/projects?ids=[""{ModrinthMapping.Keys.Join(""",""")}""]")
-                For Each ProjectJson In ModrinthProject
-                    Dim Project As New CompProject(ProjectJson)
-                    For Each Entry In ModrinthMapping(Project.Id)
-                        Entry.Comp = Project
-                    Next
-                Next
-                Log($"[Mod] 已从 Modrinth 获取本地 Mod 信息,继续获取更新信息")
-                '步骤 4:获取更新信息
-                Dim ModrinthUpdate As JObject = DlModRequest("https://api.modrinth.com/v2/version_files/update", HttpMethod.Post,
-                    $"{{""hashes"": [""{ModrinthMapping.SelectMany(Function(l) l.Value.Select(Function(m) m.ModrinthHash)).Join(""",""")}""], ""algorithm"": ""sha1"", 
-                    ""loaders"": [""{ModLoaders.Join(""",""").Lower}""],""game_versions"": [""{VanillaVersion}""]}}", "application/json")
-                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)
-                    If Not UpdateFile.Available Then Continue For
-                    If ModeDebug Then Log($"[Mod] 本地文件 {Entry.CompFile.FileName} 在 Modrinth 上的最新版为 {UpdateFile.FileName}")
-                    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?g={VanillaVersion}")
-                        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?g={VanillaVersion}"}
-                        Entry.UpdateFile = UpdateFile
-                    End If
-                Next
-                Log($"[Mod] 从 Modrinth 获取本地 Mod 信息结束")
-            Catch ex As Exception
-                Log(ex, "从 Modrinth 获取本地 Mod 信息失败")
-                IsFailed = True
-            Finally
-                EndedThreadCount += 1
-            End Try
-        End Sub, "Mod List Detail Loader Modrinth")
-        '从 CurseForge 获取信息
-        RunInNewThread(
-        Sub()
-            Try
-                '步骤 1:获取 Hash 与对应的工程 ID
-                Dim CurseForgeHashes As New List(Of UInteger)
-                For Each Entry In Mods
-                    CurseForgeHashes.Add(Entry.CurseForgeHash)
-                    If Loader.IsInterruptedWithThread(CurrentTaskThread) Then Return
-                Next
-                Dim CurseForgeRaw As JContainer = DlModRequest("https://api.curseforge.com/v1/fingerprints/432", HttpMethod.Post,
-                    $"{{""fingerprints"": [{CurseForgeHashes.Join(",")}]}}", "application/json")("data")("exactMatches")
-                Log($"[Mod] 从 CurseForge 获取到 {CurseForgeRaw.Count} 个本地 Mod 的对应信息")
-                '步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系
-                If Not CurseForgeRaw.Any() Then Return
-                Dim CurseForgeMapping As New Dictionary(Of Integer, List(Of McMod))
-                For Each Project In CurseForgeRaw
-                    Dim ProjectId = Project("id").ToString
-                    Dim Hash As UInteger = Project("file")("fileFingerprint")
-                    For Each Entry In Mods
-                        If Entry.CurseForgeHash <> Hash Then Continue For
-                        If CompProjectCache.ContainsKey(ProjectId) AndAlso Entry.Comp Is Nothing Then Entry.Comp = CompProjectCache(ProjectId) '读取已加载的缓存,加快结果出现速度
-                        If Not CurseForgeMapping.ContainsKey(ProjectId) Then CurseForgeMapping(ProjectId) = New List(Of McMod)
-                        CurseForgeMapping(ProjectId).Add(Entry)
-                        '记录对应的 CompFile
-                        Dim File As New CompFile(Project("file"), CompType.Mod)
-                        If Entry.CompFile Is Nothing OrElse Entry.CompFile.ReleaseDate < File.ReleaseDate Then Entry.CompFile = File
-                    Next
-                Next
-                If Loader.IsInterruptedWithThread(CurrentTaskThread) Then Return
-                Log($"[Mod] 需要从 CurseForge 获取 {CurseForgeMapping.Count} 个本地 Mod 的工程信息")
-                '步骤 3:获取工程信息
-                If Not CurseForgeMapping.Any() Then Return
-                Dim CurseForgeProject = DlModRequest("https://api.curseforge.com/v1/mods", HttpMethod.Post,
-                    $"{{""modIds"": [{CurseForgeMapping.Keys.Join(",")}]}}", "application/json")("data")
-                Dim UpdateFileIds As New Dictionary(Of Integer, List(Of McMod)) 'FileId -> 本地 Mod 文件列表
-                Dim FileIdToProjectSlug As New Dictionary(Of Integer, String)
-                For Each ProjectJson In CurseForgeProject
-                    If ProjectJson("isAvailable") IsNot Nothing AndAlso Not ProjectJson("isAvailable").ToObject(Of Boolean) Then Continue For
-                    '设置 Entry 中的工程信息
-                    Dim Project As New CompProject(ProjectJson)
-                    For Each Entry In CurseForgeMapping(Project.Id) '倒查防止 CurseForge 返回的内容有漏
-                        If Entry.Comp IsNot Nothing AndAlso Not Entry.Comp.FromCurseForge Then
-                            Entry.Comp = Entry.Comp '再次触发修改事件
-                            Continue For
-                        End If
-                        Entry.Comp = Project
-                    Next
-                    '查找或许版本更新的文件列表
-                    If ModLoaders.IsSingle Then
-                        Dim NewestVersion As String = Nothing
-                        Dim NewestFileIds As New List(Of Integer)
-                        For Each IndexEntry In ProjectJson("latestFilesIndexes")
-                            If IndexEntry("modLoader") Is Nothing OrElse ModLoaders.Single <> IndexEntry("modLoader").ToObject(Of Integer) Then Continue For 'ModLoader 唯一且匹配
-                            Dim IndexVersion As String = IndexEntry("gameVersion")
-                            If IndexVersion <> VanillaVersion Then Continue For 'MC 版本匹配
-                            '由于 latestFilesIndexes 是按时间从新到老排序的,所以只需取第一个;如果需要检查多个 releaseType 下的文件,将 > -1 改为 = 1,但这应当并不会获取到更新的文件
-                            If NewestVersion IsNot Nothing AndAlso CompareVersion(NewestVersion, IndexVersion) > -1 Then Continue For '只保留最新 MC 版本
-                            If NewestVersion <> IndexVersion Then
-                                NewestVersion = IndexVersion
-                                NewestFileIds.Clear()
-                            End If
-                            NewestFileIds.Add(IndexEntry("fileId").ToObject(Of Integer))
-                        Next
-                        For Each FileId In NewestFileIds
-                            If Not UpdateFileIds.ContainsKey(FileId) Then UpdateFileIds(FileId) = New List(Of McMod)
-                            UpdateFileIds(FileId).AddRange(CurseForgeMapping(Project.Id))
-                            FileIdToProjectSlug(FileId) = Project.Slug
-                        Next
-                    End If
-                Next
-                Log($"[Mod] 已从 CurseForge 获取本地 Mod 信息,需要获取 {UpdateFileIds.Count} 个用于检查更新的文件信息")
-                '步骤 4:获取更新文件信息
-                If Not UpdateFileIds.Any() Then Return
-                Dim CurseForgeFiles = DlModRequest("https://api.curseforge.com/v1/mods/files", HttpMethod.Post,
-                                    $"{{""fileIds"": [{UpdateFileIds.Keys.Join(",")}]}}", "application/json")("data")
-                Dim UpdateFiles As New Dictionary(Of McMod, CompFile)
-                For Each FileJson In CurseForgeFiles
-                    Dim File As New CompFile(FileJson, CompType.Mod)
-                    If Not File.Available Then Continue For
-                    For Each Entry As McMod In UpdateFileIds(File.Id)
-                        If UpdateFiles.ContainsKey(Entry) AndAlso UpdateFiles(Entry).ReleaseDate >= File.ReleaseDate Then Continue For
-                        UpdateFiles(Entry) = File
-                    Next
-                Next
-                For Each Pair In UpdateFiles
-                    Dim Entry As McMod = Pair.Key
-                    Dim UpdateFile As CompFile = Pair.Value
-                    If ModeDebug Then Log($"[Mod] 本地文件 {Entry.CompFile.FileName} 在 CurseForge 上的最新版为 {UpdateFile.FileName}")
-                    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://www.curseforge.com/minecraft/mc-mods/{FileIdToProjectSlug(UpdateFile.Id)}/files/{UpdateFile.Id}")
-                        Entry.UpdateFile.DownloadUrls.AddRange(UpdateFile.DownloadUrls) '合并下载源
-                    ElseIf Entry.UpdateFile Is Nothing OrElse UpdateFile.ReleaseDate > Entry.UpdateFile.ReleaseDate Then '替换
-                        Entry.ChangelogUrls = New List(Of String) From {$"https://www.curseforge.com/minecraft/mc-mods/{FileIdToProjectSlug(UpdateFile.Id)}/files/{UpdateFile.Id}"}
-                        Entry.UpdateFile = UpdateFile
-                    End If
-                Next
-                Log($"[Mod] 从 CurseForge 获取 Mod 更新信息结束")
-            Catch ex As Exception
-                Log(ex, "从 CurseForge 获取本地 Mod 信息失败")
-                IsFailed = True
-            Finally
-                EndedThreadCount += 1
-            End Try
-        End Sub, "Mod List Detail Loader CurseForge")
-        '从 JAR 中获取缺失的版本信息(上面有另一个分支)
-        Loader.Input.Key.Where(Function(m) m.Version Is Nothing).ForEach(Sub(m) m.LoadMetadataFromJar())
-        '等待线程结束
-        Do Until EndedThreadCount = 2
-            Thread.Sleep(10)
-            If Loader.IsInterrupted Then Return
-        Loop
-        '保存缓存
-        Mods = Mods.Where(Function(m) m.Comp IsNot Nothing).ToList()
-        Log($"[Mod] 联网获取本地 Mod 信息完成,为 {Mods.Count} 个 Mod 更新缓存")
-        If Not Mods.Any() Then Return
-        For Each Entry In Mods
-            Entry.CompLoaded = Not IsFailed
-            Cache(Entry.ModrinthHash & VanillaVersion & ModLoaders.Join("")) = Entry.ToJson()
-        Next
-        WriteFile(PathTemp & "Cache\LocalMod.json", Cache.ToString(If(ModeDebug, Newtonsoft.Json.Formatting.Indented, Newtonsoft.Json.Formatting.None)))
-        '刷新边栏
-        If Loader.IsInterrupted Then Return
-        If FrmInstanceMod?.Filter = PageInstanceMod.FilterType.CanUpdate Then
-            RunInUi(Sub() FrmInstanceMod?.RefreshUI()) '同步 “可更新” 列表 (#4677)
-        Else
-            RunInUi(Sub() FrmInstanceMod?.RefreshBars())
-        End If
-    End Sub
-    Public Function GetTargetModLoaders() As List(Of CompModLoaderType)
-        Dim ModLoaders As New List(Of CompModLoaderType)
-        If PageInstanceLeft.Instance.Version.HasForge Then ModLoaders.Add(CompModLoaderType.Forge)
-        If PageInstanceLeft.Instance.Version.HasNeoForge Then ModLoaders.Add(CompModLoaderType.NeoForge)
-        If PageInstanceLeft.Instance.Version.HasFabric Then ModLoaders.Add(CompModLoaderType.Fabric)
-        If PageInstanceLeft.Instance.Version.HasLiteLoader Then ModLoaders.Add(CompModLoaderType.LiteLoader)
-        If Not ModLoaders.Any() Then ModLoaders.AddRange({CompModLoaderType.Forge, CompModLoaderType.NeoForge, CompModLoaderType.Fabric, CompModLoaderType.LiteLoader, CompModLoaderType.Quilt})
-        Return ModLoaders
-    End Function
-
-End Module

+ 57 - 33
Plain Craft Launcher 2/Modules/Minecraft/ModModpack.vb

@@ -1,6 +1,4 @@
-Imports System.IO.Compression
-
-Public Module ModModpack
+Public Module ModModpack
 
 
     '触发整合包安装的外部接口
     '触发整合包安装的外部接口
     ''' <summary>
     ''' <summary>
@@ -15,7 +13,7 @@ Public Module ModModpack
                 ModpackInstall(File)
                 ModpackInstall(File)
             Catch ex As CancelledException
             Catch ex As CancelledException
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "手动安装整合包失败", LogLevel.Msgbox)
+                Log(ex, "手动安装整合包失败", NotifyLevel.MsgBox)
             End Try
             End Try
         End Sub)
         End Sub)
     End Sub
     End Sub
@@ -251,8 +249,35 @@ Retry:
             ModDownloadLoaders.Add(New LoaderTask(Of Integer, JArray)("获取 Mod 下载信息",
             ModDownloadLoaders.Add(New LoaderTask(Of Integer, JArray)("获取 Mod 下载信息",
             Sub(Task As LoaderTask(Of Integer, JArray))
             Sub(Task As LoaderTask(Of Integer, JArray))
                 Task.Output = DlModRequest("https://api.curseforge.com/v1/mods/files", HttpMethod.Post, "{""fileIds"": [" & ModList.Join(",") & "]}", "application/json")("data")
                 Task.Output = DlModRequest("https://api.curseforge.com/v1/mods/files", HttpMethod.Post, "{""fileIds"": [" & ModList.Join(",") & "]}", "application/json")("data")
-                '如果文件已被删除,则 API 会跳过那一项
-                If ModList.Count > Task.Output.Count Then Throw New Exception("整合包中的部分 Mod 版本已被 Mod 作者删除,所以没法继续安装了,请向整合包作者反馈该问题")
+                If ModList.Count = Task.Output.Count Then Return
+                '如果文件已被删除,则 API 会跳过那一项,导致返回的列表数量小于请求的数量。后面的都是对此的错误处理。
+                '获取缺失的 fileId 列表
+                Dim MissingIds = ModList.Except(Task.Output.Select(Function(j) j("id").ToObject(Of Integer))).ToList
+                Log("[Modpack] 缺失的 fileIds:" & MissingIds.Join("、"))
+                '将 fileId 映射到 projectId
+                Dim Missings = MissingIds.Select(Of (ProjectId As Integer, FileId As Integer))(
+                    Function(id) (CType(Json("files").AsEnumerable.First(Function(f) f("fileID").ToObject(Of Integer) = id), JObject)("projectID").ToObject(Of Integer), id)).ToList
+                Log("[Modpack] 缺失项:" & Missings.Join("、"))
+                '尝试获取对应的工程信息
+                Dim Projects As New List(Of ResourceProject)
+                Try
+                    Projects = ResourceProject.FromProjectIds(Missings.Select(Function(e) e.ProjectId.ToString), ResourcePlatforms.CurseForge).ToList
+                Catch ex As Exception
+                    Log(ex, "获取缺失的工程信息失败")
+                End Try
+                Dim MissingDesc As New List(Of String)
+                For Each Missing In Missings
+                    Dim Project = Projects.FirstOrDefault(Function(p) p.Id = Missing.ProjectId)
+                    If Project Is Nothing Then
+                        MissingDesc.Add($"未知 CurseForge 项目 (项目 ID {Missing.ProjectId},文件 ID {Missing.FileId})")
+                    Else
+                        MissingDesc.Add($"{Project.TranslatedName} (文件 ID {Missing.FileId})")
+                    End If
+                Next
+                If MyMsgBox($"未找到该整合包所需的以下内容,它们可能已被原作者删除:{vbCrLf}- {MissingDesc.Join(vbCrLf & "- ")}{vbCrLf}{vbCrLf}是否继续安装?缺失这些内容可能导致整合包出现异常!",
+                    "整合包内容缺失", "继续", "取消", IsWarn:=True) = 2 Then
+                    Task.Interrupt()
+                End If
             End Sub) With {.ProgressWeight = ModList.Count / 10}) '每 10 Mod 需要 1s
             End Sub) With {.ProgressWeight = ModList.Count / 10}) '每 10 Mod 需要 1s
             '构造 NetFile
             '构造 NetFile
             ModDownloadLoaders.Add(New LoaderTask(Of JArray, List(Of NetFile))("构造 Mod 下载信息",
             ModDownloadLoaders.Add(New LoaderTask(Of JArray, List(Of NetFile))("构造 Mod 下载信息",
@@ -269,23 +294,23 @@ Retry:
                         End If
                         End If
                     End If
                     End If
                     '根据 modules 和文件名后缀判断资源类型
                     '根据 modules 和文件名后缀判断资源类型
-                    Dim TargetFolder As String, Type As CompType
+                    Dim TargetFolder As String, Type As ResourceTypes
                     If ModJson("modules").Any Then 'modules 可能返回 null(#1006)
                     If ModJson("modules").Any Then 'modules 可能返回 null(#1006)
                         Dim ModuleNames = CType(ModJson("modules"), JArray).Select(Function(l) l("name").ToString).ToList
                         Dim ModuleNames = CType(ModJson("modules"), JArray).Select(Function(l) l("name").ToString).ToList
                         If ModuleNames.Contains("META-INF") OrElse ModuleNames.Contains("mcmod.info") OrElse
                         If ModuleNames.Contains("META-INF") OrElse ModuleNames.Contains("mcmod.info") OrElse
                            ModJson?("FileName")?.ToString.EndsWithF(".jar", True) Then
                            ModJson?("FileName")?.ToString.EndsWithF(".jar", True) Then
-                            TargetFolder = "mods" : Type = CompType.Mod
+                            TargetFolder = "mods" : Type = ResourceTypes.Mod
                         ElseIf ModuleNames.Contains("pack.mcmeta") Then
                         ElseIf ModuleNames.Contains("pack.mcmeta") Then
-                            TargetFolder = "resourcepacks" : Type = CompType.ResourcePack
+                            TargetFolder = "resourcepacks" : Type = ResourceTypes.ResourcePack
                         Else
                         Else
-                            TargetFolder = "shaderpacks" : Type = CompType.Shader
+                            TargetFolder = "shaderpacks" : Type = ResourceTypes.Shader
                         End If
                         End If
                     Else
                     Else
-                        TargetFolder = "mods" : Type = CompType.Mod
+                        TargetFolder = "mods" : Type = ResourceTypes.Mod
                     End If
                     End If
                     '建立 CompFile
                     '建立 CompFile
-                    Dim File As New CompFile(ModJson, Type)
-                    If Not File.Available Then Continue For
+                    Dim File = ResourceVersion.FromPlatformJson(ModJson, Type)
+                    If Not File.DownloadAvailable Then Continue For
                     '实际的添加
                     '实际的添加
                     FileList.Add(Id, File.ToNetFile($"{McFolderSelected}versions\{InstanceName}\{TargetFolder}\"))
                     FileList.Add(Id, File.ToNetFile($"{McFolderSelected}versions\{InstanceName}\{TargetFolder}\"))
                     Task.Progress += 1 / (1 + ModList.Count)
                     Task.Progress += 1 / (1 + ModList.Count)
@@ -330,7 +355,7 @@ Retry:
                     File.Delete(Target)
                     File.Delete(Target)
                 End If
                 End If
             Next
             Next
-            If File.Exists(FileAddress) AndAlso IO.Path.GetFileNameWithoutExtension(FileAddress) = "modpack" Then
+            If File.Exists(FileAddress) AndAlso Path.GetFileNameWithoutExtension(FileAddress) = "modpack" Then
                 Log("[ModPack] 删除安装整合包文件:" & FileAddress)
                 Log("[ModPack] 删除安装整合包文件:" & FileAddress)
                 File.Delete(FileAddress)
                 File.Delete(FileAddress)
             End If
             End If
@@ -425,11 +450,11 @@ Retry:
                 End Select
                 End Select
             End If
             End If
             '添加下载文件
             '添加下载文件
-            Dim Urls = File("downloads").SelectMany(Function(t) CompFile.HandleCurseForgeDownloadUrls(t.ToString)).ToList
+            Dim Urls = File("downloads").SelectMany(Function(t) ResourceVersion.ParseCurseForgeDownloadUrls(t.ToString)).ToList
             Urls.AddRange(Urls.Select(Function(u) DlSourceModGet(u)).ToList)
             Urls.AddRange(Urls.Select(Function(u) DlSourceModGet(u)).ToList)
             Urls = Urls.Distinct.ToList()
             Urls = Urls.Distinct.ToList()
             Dim TargetPath As String = $"{McFolderSelected}versions\{InstanceName}\{File("path")}"
             Dim TargetPath As String = $"{McFolderSelected}versions\{InstanceName}\{File("path")}"
-            If Not IO.Path.GetFullPath(TargetPath).StartsWithF($"{McFolderSelected}versions\{InstanceName}\", True) Then
+            If Not Path.GetFullPath(TargetPath).StartsWithF($"{McFolderSelected}versions\{InstanceName}\", True) Then
                 MyMsgBox($"整合包的文件路径超出了版本文件夹,请向整合包作者反馈此问题!" & vbCrLf & "错误的文件:" & TargetPath, "文件路径校验失败", IsWarn:=True)
                 MyMsgBox($"整合包的文件路径超出了版本文件夹,请向整合包作者反馈此问题!" & vbCrLf & "错误的文件:" & TargetPath, "文件路径校验失败", IsWarn:=True)
                 Throw New CancelledException
                 Throw New CancelledException
             End If
             End If
@@ -471,7 +496,7 @@ Retry:
                     File.Delete(Target)
                     File.Delete(Target)
                 End If
                 End If
             Next
             Next
-            If File.Exists(FileAddress) AndAlso IO.Path.GetFileNameWithoutExtension(FileAddress) = "modpack" Then
+            If File.Exists(FileAddress) AndAlso Path.GetFileNameWithoutExtension(FileAddress) = "modpack" Then
                 Log("[ModPack] 删除安装整合包文件:" & FileAddress)
                 Log("[ModPack] 删除安装整合包文件:" & FileAddress)
                 File.Delete(FileAddress)
                 File.Delete(FileAddress)
             End If
             End If
@@ -576,18 +601,18 @@ Retry:
                 Task, 0.4)
                 Task, 0.4)
             '读取 MMC 设置文件(#2655)
             '读取 MMC 设置文件(#2655)
             Try
             Try
-                Dim MMCSetupFile As String = InstallTemp & ArchiveBaseFolder & "instance.cfg"
-                If File.Exists(MMCSetupFile) Then
+                Dim MmcSetupFile As String = InstallTemp & ArchiveBaseFolder & "instance.cfg"
+                If File.Exists(MmcSetupFile) Then
                     '将其中的等号替换为冒号,以符合 ini 文件格式
                     '将其中的等号替换为冒号,以符合 ini 文件格式
                     Dim Lines As New List(Of String)
                     Dim Lines As New List(Of String)
-                    For Each Line In ReadFile(MMCSetupFile).Split({vbCr, vbLf}, StringSplitOptions.RemoveEmptyEntries)
+                    For Each Line In ReadFile(MmcSetupFile).Split(vbCrLf.ToCharArray, StringSplitOptions.RemoveEmptyEntries)
                         If Not Line.Contains("=") Then Continue For
                         If Not Line.Contains("=") Then Continue For
                         Lines.Add(Line.BeforeFirst("=") & ":" & Line.AfterFirst("="))
                         Lines.Add(Line.BeforeFirst("=") & ":" & Line.AfterFirst("="))
                     Next
                     Next
-                    WriteFile(MMCSetupFile, Lines.Join(vbCrLf))
+                    FileUtils.Write(MmcSetupFile, Lines.Join(vbCrLf))
                     '读取文件
                     '读取文件
-                    If ReadIni(MMCSetupFile, "OverrideCommands", False) Then
-                        Dim PreLaunchCommand As String = ReadIni(MMCSetupFile, "PreLaunchCommand")
+                    If ReadIni(MmcSetupFile, "OverrideCommands", False) Then
+                        Dim PreLaunchCommand As String = ReadIni(MmcSetupFile, "PreLaunchCommand")
                         If PreLaunchCommand <> "" Then
                         If PreLaunchCommand <> "" Then
                             PreLaunchCommand = PreLaunchCommand.Replace("\""", """").
                             PreLaunchCommand = PreLaunchCommand.Replace("\""", """").
                                 Replace("$INST_JAVA", "{java}javaw.exe").
                                 Replace("$INST_JAVA", "{java}javaw.exe").
@@ -598,16 +623,16 @@ Retry:
                             Log("[ModPack] 迁移 MultiMC 版本独立设置:启动前执行命令:" & PreLaunchCommand)
                             Log("[ModPack] 迁移 MultiMC 版本独立设置:启动前执行命令:" & PreLaunchCommand)
                         End If
                         End If
                     End If
                     End If
-                    If ReadIni(MMCSetupFile, "JoinServerOnLaunch", False) Then
-                        Dim ServerAddress As String = ReadIni(MMCSetupFile, "JoinServerOnLaunchAddress").Replace("\""", """")
+                    If ReadIni(MmcSetupFile, "JoinServerOnLaunch", False) Then
+                        Dim ServerAddress As String = ReadIni(MmcSetupFile, "JoinServerOnLaunchAddress").Replace("\""", """")
                         WriteIni(SetupFile, "VersionServerEnter", ServerAddress)
                         WriteIni(SetupFile, "VersionServerEnter", ServerAddress)
                         Log("[ModPack] 迁移 MultiMC 版本独立设置:自动进入服务器:" & ServerAddress)
                         Log("[ModPack] 迁移 MultiMC 版本独立设置:自动进入服务器:" & ServerAddress)
                     End If
                     End If
-                    If ReadIni(MMCSetupFile, "IgnoreJavaCompatibility", False) Then
+                    If ReadIni(MmcSetupFile, "IgnoreJavaCompatibility", False) Then
                         WriteIni(SetupFile, "VersionAdvanceJava", True)
                         WriteIni(SetupFile, "VersionAdvanceJava", True)
                         Log("[ModPack] 迁移 MultiMC 版本独立设置:忽略 Java 兼容性警告")
                         Log("[ModPack] 迁移 MultiMC 版本独立设置:忽略 Java 兼容性警告")
                     End If
                     End If
-                    Dim Logo As String = ReadIni(MMCSetupFile, "iconKey", "")
+                    Dim Logo As String = ReadIni(MmcSetupFile, "iconKey", "")
                     If Logo <> "" AndAlso File.Exists($"{InstallTemp}{ArchiveBaseFolder}{Logo}.png") Then
                     If Logo <> "" AndAlso File.Exists($"{InstallTemp}{ArchiveBaseFolder}{Logo}.png") Then
                         WriteIni(SetupFile, "LogoCustom", True)
                         WriteIni(SetupFile, "LogoCustom", True)
                         WriteIni(SetupFile, "Logo", "PCL\Logo.png")
                         WriteIni(SetupFile, "Logo", "PCL\Logo.png")
@@ -615,9 +640,9 @@ Retry:
                         Log($"[ModPack] 迁移 MultiMC 版本独立设置:版本图标({Logo}.png)")
                         Log($"[ModPack] 迁移 MultiMC 版本独立设置:版本图标({Logo}.png)")
                     End If
                     End If
                     'JVM 参数
                     'JVM 参数
-                    Dim JvmArgs As String = ReadIni(MMCSetupFile, "JvmArgs", "")
+                    Dim JvmArgs As String = ReadIni(MmcSetupFile, "JvmArgs", "")
                     If JvmArgs <> "" Then
                     If JvmArgs <> "" Then
-                        If ReadIni(MMCSetupFile, "OverrideJavaArgs", False) Then
+                        If ReadIni(MmcSetupFile, "OverrideJavaArgs", False) Then
                             WriteIni(SetupFile, "VersionAdvanceJvm", JvmArgs)
                             WriteIni(SetupFile, "VersionAdvanceJvm", JvmArgs)
                             Log("[ModPack] 迁移 MultiMC 版本独立设置:JVM 参数(覆盖):" & JvmArgs)
                             Log("[ModPack] 迁移 MultiMC 版本独立设置:JVM 参数(覆盖):" & JvmArgs)
                         Else
                         Else
@@ -770,6 +795,7 @@ Retry:
             New LoaderTask(Of String, Integer)("解压压缩包",
             New LoaderTask(Of String, Integer)("解压压缩包",
             Sub(Task As LoaderTask(Of String, Integer))
             Sub(Task As LoaderTask(Of String, Integer))
                 ExtractModpackFiles(TargetFolder, FileAddress, Task, 0.9)
                 ExtractModpackFiles(TargetFolder, FileAddress, Task, 0.9)
+                OpenExplorer(TargetFolder)
                 Thread.Sleep(400) '避免文件争用
                 Thread.Sleep(400) '避免文件争用
                 '查找解压后的 exe 文件
                 '查找解压后的 exe 文件
                 Dim Launcher As String = Nothing
                 Dim Launcher As String = Nothing
@@ -779,7 +805,7 @@ Retry:
                     If Info.ProductName = "Plain Craft Launcher" Then
                     If Info.ProductName = "Plain Craft Launcher" Then
                         Launcher = ExeFile
                         Launcher = ExeFile
                         Log($"[Modpack] 发现整合包附带的 PCL 启动器:{ExeFile}")
                         Log($"[Modpack] 发现整合包附带的 PCL 启动器:{ExeFile}")
-                    ElseIf (Info.ProductName.ContainsF("Launcher", True) OrElse Info.ProductName.ContainsF("启动", True)) AndAlso
+                    ElseIf (Info.ProductName.ContainsIgnoreCase("Launcher") OrElse Info.ProductName.Contains("启动")) AndAlso
                         Not Info.ProductName = "Plain Craft Launcher Admin Manager" Then
                         Not Info.ProductName = "Plain Craft Launcher Admin Manager" Then
                         If Launcher Is Nothing Then
                         If Launcher Is Nothing Then
                             Launcher = ExeFile
                             Launcher = ExeFile
@@ -792,7 +818,6 @@ Retry:
                 If Launcher IsNot Nothing Then
                 If Launcher IsNot Nothing Then
                     Log("[Modpack] 找到压缩包中附带的启动器:" & Launcher)
                     Log("[Modpack] 找到压缩包中附带的启动器:" & Launcher)
                     If MyMsgBox($"整合包里似乎自带了启动器,是否换用它继续安装?{vbCrLf}即将打开:{Launcher}", "换用整合包启动器?", "换用", "不换用") = 1 Then
                     If MyMsgBox($"整合包里似乎自带了启动器,是否换用它继续安装?{vbCrLf}即将打开:{Launcher}", "换用整合包启动器?", "换用", "不换用") = 1 Then
-                        OpenExplorer(TargetFolder)
                         StartProcess(Launcher, "--wait") '要求等待已有的 PCL 退出
                         StartProcess(Launcher, "--wait") '要求等待已有的 PCL 退出
                         Log("[Modpack] 为换用整合包中的启动器启动,强制结束程序")
                         Log("[Modpack] 为换用整合包中的启动器启动,强制结束程序")
                         FrmMain.EndProgram(False)
                         FrmMain.EndProgram(False)
@@ -801,10 +826,9 @@ Retry:
                 Else
                 Else
                     Log("[Modpack] 未找到压缩包中附带的启动器")
                     Log("[Modpack] 未找到压缩包中附带的启动器")
                 End If
                 End If
-                OpenExplorer(TargetFolder)
                 '加入文件夹列表
                 '加入文件夹列表
                 Dim InstanceName As String = GetFolderNameFromPath(TargetFolder)
                 Dim InstanceName As String = GetFolderNameFromPath(TargetFolder)
-                Directory.CreateDirectory(TargetFolder & ".minecraft\")
+                DirectoryUtils.Create(TargetFolder & ".minecraft\")
                 PageSelectLeft.AddFolder(
                 PageSelectLeft.AddFolder(
                     TargetFolder & ".minecraft\" & ArchiveBaseFolder.Replace("/", "\").TrimStart("\"), '格式例如:包裹文件夹\.minecraft\(最短为空字符串)
                     TargetFolder & ".minecraft\" & ArchiveBaseFolder.Replace("/", "\").TrimStart("\"), '格式例如:包裹文件夹\.minecraft\(最短为空字符串)
                     InstanceName, False)
                     InstanceName, False)

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

@@ -105,7 +105,7 @@
                     Loop
                     Loop
                     WatcherLog("Minecraft 日志监控已退出")
                     WatcherLog("Minecraft 日志监控已退出")
                 Catch ex As Exception
                 Catch ex As Exception
-                    Log(ex, "Minecraft 日志监控主循环出错", LogLevel.Feedback)
+                    Log(ex, "Minecraft 日志监控主循环出错", NotifyLevel.MsgBoxAndFeedback)
                     State = MinecraftState.Ended
                     State = MinecraftState.Ended
                 End Try
                 End Try
             End Sub, "Minecraft Watcher PID " & PID)
             End Sub, "Minecraft Watcher PID " & PID)
@@ -174,14 +174,14 @@
                     End If
                     End If
                 End If
                 End If
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "输出 Minecraft 日志失败", LogLevel.Feedback)
+                Log(ex, "输出 Minecraft 日志失败", NotifyLevel.MsgBoxAndFeedback)
             End Try
             End Try
         End Sub
         End Sub
         Public LatestLog As New Queue(Of String)
         Public LatestLog As New Queue(Of String)
         Private Sub GameLog(Text As String)
         Private Sub GameLog(Text As String)
             '预处理
             '预处理
             If Text Is Nothing Then Return
             If Text Is Nothing Then Return
-            Text = Text.Replace(vbCrLf, vbCr).Replace(vbLf, vbCr).Replace(vbCr, vbCrLf)
+            Text = Text.ReplaceLineEndings(vbCrLf)
             'If Text.Contains("�����") Then Hint("检测到错误的日志编码:" & Text)
             'If Text.Contains("�����") Then Hint("检测到错误的日志编码:" & Text)
             '加入预存储
             '加入预存储
             LatestLog.Enqueue(Text)
             LatestLog.Enqueue(Text)
@@ -194,7 +194,7 @@
             If LogProgress < 2 AndAlso Text.Contains("Setting user:") Then
             If LogProgress < 2 AndAlso Text.Contains("Setting user:") Then
                 WatcherLog("日志 2/5:游戏用户已设置") '仅确保支持 Minecraft 1.7+
                 WatcherLog("日志 2/5:游戏用户已设置") '仅确保支持 Minecraft 1.7+
                 LogProgress = 2
                 LogProgress = 2
-            ElseIf LogProgress < 3 AndAlso Text.ContainsF("lwjgl version", True) Then
+            ElseIf LogProgress < 3 AndAlso Text.ContainsIgnoreCase("lwjgl version") Then
                 WatcherLog("日志 3/5:LWJGL 版本已确认")
                 WatcherLog("日志 3/5:LWJGL 版本已确认")
                 LogProgress = 3
                 LogProgress = 3
             ElseIf LogProgress < 4 AndAlso (Text.Contains("OpenAL initialized") OrElse Text.Contains("Starting up SoundSystem")) Then
             ElseIf LogProgress < 4 AndAlso (Text.Contains("OpenAL initialized") OrElse Text.Contains("Starting up SoundSystem")) Then
@@ -265,7 +265,7 @@
                     MinecraftWindow = TryGetMinecraftWindow()
                     MinecraftWindow = TryGetMinecraftWindow()
                 Catch ex As ComponentModel.Win32Exception
                 Catch ex As ComponentModel.Win32Exception
                     '拒绝访问(#1062)
                     '拒绝访问(#1062)
-                    Log(ex, "由于反作弊或安全软件拦截,PCL 无法操作游戏窗口", LogLevel.Hint)
+                    Log(ex, "由于反作弊或安全软件拦截,PCL 无法操作游戏窗口", NotifyLevel.AllUsers)
                     IsWindowFinished = True
                     IsWindowFinished = True
                 End Try
                 End Try
                 If MinecraftWindow Is Nothing Then Return
                 If MinecraftWindow Is Nothing Then Return
@@ -297,7 +297,7 @@
                 End If
                 End If
                 IsWindowAppeared = True
                 IsWindowAppeared = True
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "检查 Minecraft 窗口失败", LogLevel.Feedback)
+                Log(ex, "检查 Minecraft 窗口失败", NotifyLevel.MsgBoxAndFeedback)
             End Try
             End Try
         End Sub
         End Sub
         ''' <summary>
         ''' <summary>
@@ -361,9 +361,9 @@
                     Analyzer.Prepare()
                     Analyzer.Prepare()
                     Analyzer.Analyze()
                     Analyzer.Analyze()
                     Analyzer.Output(False, New List(Of String) From
                     Analyzer.Output(False, New List(Of String) From
-                        {Instance.PathVersion & Instance.Name & ".json", Path & "PCL\Log1.txt", Path & "PCL\LatestLaunch.bat"})
+                        {Instance.PathVersion & Instance.Name & ".json", PathExeFolder & "PCL\Log1.txt", PathExeFolder & "PCL\LatestLaunch.bat"})
                 Catch ex As Exception
                 Catch ex As Exception
-                    Log(ex, "崩溃分析失败", LogLevel.Feedback)
+                    Log(ex, "崩溃分析失败", NotifyLevel.MsgBoxAndFeedback)
                 End Try
                 End Try
             End Sub, "Crash Analyzer")
             End Sub, "Crash Analyzer")
         End Sub
         End Sub
@@ -376,7 +376,7 @@
                 If Not GameProcess.HasExited Then GameProcess.Kill()
                 If Not GameProcess.HasExited Then GameProcess.Kill()
                 WatcherLog("已强制结束 Minecraft 进程")
                 WatcherLog("已强制结束 Minecraft 进程")
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "强制结束 Minecraft 进程失败", LogLevel.Hint)
+                Log(ex, "强制结束 Minecraft 进程失败", NotifyLevel.AllUsers)
             End Try
             End Try
         End Sub
         End Sub
 
 

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

@@ -1,8 +1,6 @@
-Public Module ModDevelop
+Public Class ModDevelop
 
 
-#If DEBUG Then
-    Public Sub Start()
+    Public Shared Sub Start()
     End Sub
     End Sub
-#End If
 
 
-End Module
+End Class

+ 12 - 12
Plain Craft Launcher 2/Modules/ModEvent.vb

@@ -186,7 +186,7 @@ Public Class CustomEvent
                         MyMsgBox("EventData 必须为一个网址。" & vbCrLf & "如果想要启动程序,请将 EventType 改为 打开文件。", "事件执行失败")
                         MyMsgBox("EventData 必须为一个网址。" & vbCrLf & "如果想要启动程序,请将 EventType 改为 打开文件。", "事件执行失败")
                         Return
                         Return
                     End If
                     End If
-                    Hint("正在开启中,请稍候:" & Arg)
+                    Hint("正在开启,请稍候:" & Arg)
                     RunInThread(Sub() OpenWebsite(Arg))
                     RunInThread(Sub() OpenWebsite(Arg))
 
 
                 Case EventType.打开文件, EventType.打开帮助, EventType.执行命令
                 Case EventType.打开文件, EventType.打开帮助, EventType.执行命令
@@ -210,7 +210,7 @@ Public Class CustomEvent
                                 StartProcess(Info)
                                 StartProcess(Info)
                             End If
                             End If
                         Catch ex As Exception
                         Catch ex As Exception
-                            Log(ex, "执行打开类自定义事件失败", LogLevel.Msgbox)
+                            Log(ex, "执行打开类自定义事件失败", NotifyLevel.MsgBox)
                         End Try
                         End Try
                     End Sub)
                     End Sub)
 
 
@@ -267,12 +267,12 @@ Public Class CustomEvent
                     MyMsgBox(Args(1).Replace("\n", vbCrLf), Args(0).Replace("\n", vbCrLf), If(Args.Length > 2, Args(2), "确定"))
                     MyMsgBox(Args(1).Replace("\n", vbCrLf), Args(0).Replace("\n", vbCrLf), If(Args.Length > 2, Args(2), "确定"))
 
 
                 Case EventType.弹出提示
                 Case EventType.弹出提示
-                    Hint(Args(0).Replace("\n", vbCrLf), If(Args.Length = 1, HintType.Blue, Args(1).ParseToEnum(Of HintType)))
+                    Hint(Args(0).Replace("\n", vbCrLf), If(Args.Length = 1, HintType.Blue, Args(1).ToEnum(Of HintType)))
 
 
                 Case EventType.切换页面
                 Case EventType.切换页面
                     RunInUi(Sub() FrmMain.PageChange(
                     RunInUi(Sub() FrmMain.PageChange(
-                                Args(0).ParseToEnum(Of FormMain.PageType),
-                                If(Args.Length = 1, FormMain.PageSubType.Default, Args(1).ParseToEnum(Of FormMain.PageSubType))))
+                                Args(0).ToEnum(Of FormMain.PageType),
+                                If(Args.Length = 1, FormMain.PageSubType.Default, Args(1).ToEnum(Of FormMain.PageSubType))))
 
 
                 Case EventType.导入整合包, EventType.安装整合包
                 Case EventType.导入整合包, EventType.安装整合包
                     RunInUi(Sub() ModpackInstall())
                     RunInUi(Sub() ModpackInstall())
@@ -328,7 +328,7 @@ Public Class CustomEvent
                     MyMsgBox("未知的事件类型:" & Type & vbCrLf & "请检查事件类型填写是否正确,或者 PCL 是否为最新版本。", "事件执行失败")
                     MyMsgBox("未知的事件类型:" & Type & vbCrLf & "请检查事件类型填写是否正确,或者 PCL 是否为最新版本。", "事件执行失败")
             End Select
             End Select
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, $"事件执行失败({Type}, {Arg})", LogLevel.Msgbox)
+            Log(ex, $"事件执行失败({Type}, {Arg})", NotifyLevel.MsgBox)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -372,20 +372,20 @@ Public Class CustomEvent
         RelativeUrl = RelativeUrl.Replace("/", "\").Lower.TrimStart("\")
         RelativeUrl = RelativeUrl.Replace("/", "\").Lower.TrimStart("\")
 
 
         '确认实际路径
         '确认实际路径
-        Dim Location As String, WorkingDir As String = Path & "PCL"
+        Dim Location As String, WorkingDir As String = PathExeFolder & "PCL"
         HelpTryExtract()
         HelpTryExtract()
         If RelativeUrl.Contains(":\") Then
         If RelativeUrl.Contains(":\") Then
             '绝对路径
             '绝对路径
             Location = RelativeUrl
             Location = RelativeUrl
             Log("[Control] 自定义事件中由绝对路径" & Type & ":" & Location)
             Log("[Control] 自定义事件中由绝对路径" & Type & ":" & Location)
-        ElseIf File.Exists(Path & "PCL\" & RelativeUrl) Then
+        ElseIf File.Exists(PathExeFolder & "PCL\" & RelativeUrl) Then
             '相对 PCL 文件夹的路径
             '相对 PCL 文件夹的路径
-            Location = Path & "PCL\" & RelativeUrl
+            Location = PathExeFolder & "PCL\" & RelativeUrl
             Log("[Control] 自定义事件中由相对 PCL 文件夹的路径" & Type & ":" & Location)
             Log("[Control] 自定义事件中由相对 PCL 文件夹的路径" & Type & ":" & Location)
-        ElseIf File.Exists(Path & "PCL\Help\" & RelativeUrl) Then
+        ElseIf File.Exists(PathExeFolder & "PCL\Help\" & RelativeUrl) Then
             '相对 PCL 本地帮助文件夹的路径
             '相对 PCL 本地帮助文件夹的路径
-            Location = Path & "PCL\Help\" & RelativeUrl
-            WorkingDir = Path & "PCL\Help\"
+            Location = PathExeFolder & "PCL\Help\" & RelativeUrl
+            WorkingDir = PathExeFolder & "PCL\Help\"
             Log("[Control] 自定义事件中由相对 PCL 本地帮助文件夹的路径" & Type & ":" & Location)
             Log("[Control] 自定义事件中由相对 PCL 本地帮助文件夹的路径" & Type & ":" & Location)
         ElseIf Type = EventType.打开帮助 AndAlso File.Exists(PathTemp & "Help\" & RelativeUrl) Then
         ElseIf Type = EventType.打开帮助 AndAlso File.Exists(PathTemp & "Help\" & RelativeUrl) Then
             '相对 PCL 自带帮助文件夹的路径
             '相对 PCL 自带帮助文件夹的路径

+ 43 - 41
Plain Craft Launcher 2/Modules/ModMain.vb

@@ -23,12 +23,12 @@ Public Module ModMain
     ''' <summary>
     ''' <summary>
     ''' 等待弹出的提示列表。以 {String, HintType, Log As Boolean} 形式存储为数组。
     ''' 等待弹出的提示列表。以 {String, HintType, Log As Boolean} 形式存储为数组。
     ''' </summary>
     ''' </summary>
-    Private HintWaiting As SafeList(Of HintMessage) = If(HintWaiting, New SafeList(Of HintMessage))
+    Private HintWaiting As ConcurrentList(Of HintMessage) = If(HintWaiting, New ConcurrentList(Of HintMessage))
     ''' <summary>
     ''' <summary>
     ''' 在窗口左下角弹出提示文本。
     ''' 在窗口左下角弹出提示文本。
     ''' </summary>
     ''' </summary>
     Public Sub Hint(Text As String, Optional Type As HintType = HintType.Blue, Optional Log As Boolean = True)
     Public Sub Hint(Text As String, Optional Type As HintType = HintType.Blue, Optional Log As Boolean = True)
-        If HintWaiting Is Nothing Then HintWaiting = New SafeList(Of HintMessage)
+        If HintWaiting Is Nothing Then HintWaiting = New ConcurrentList(Of HintMessage)
         HintWaiting.Add(New HintMessage With {.Text = If(Text, ""), .Type = Type, .Log = Log})
         HintWaiting.Add(New HintMessage With {.Text = If(Text, ""), .Type = Type, .Log = Log})
     End Sub
     End Sub
 
 
@@ -45,7 +45,7 @@ Public Module ModMain
                 'End If
                 'End If
                 Dim CurrentHint = HintWaiting(0)
                 Dim CurrentHint = HintWaiting(0)
                 '去回车
                 '去回车
-                CurrentHint.Text = CurrentHint.Text.Replace(vbCrLf, " ").Replace(vbCr, " ").Replace(vbLf, " ")
+                CurrentHint.Text = CurrentHint.Text.ReplaceLineEndings(" ", mergeMultiple:=True)
                 '超量提示直接忽略
                 '超量提示直接忽略
                 If FrmMain.PanHint.Children.Count >= 20 Then GoTo EndHint
                 If FrmMain.PanHint.Children.Count >= 20 Then GoTo EndHint
                 '检查是否有重复提示
                 '检查是否有重复提示
@@ -71,7 +71,7 @@ Public Module ModMain
                     '有重复提示,且该提示的进入动画已播放
                     '有重复提示,且该提示的进入动画已播放
                     If Not AniIsRun("Hint Show " & DoubleStack.Tag(1)) Then
                     If Not AniIsRun("Hint Show " & DoubleStack.Tag(1)) Then
                         AniStop("Hint Hide " & DoubleStack.Tag(1))
                         AniStop("Hint Hide " & DoubleStack.Tag(1))
-                        Dim Delay As Double = (800 + MathClamp(CurrentHint.Text.Length, 5, 23) * 180) * AniSpeed
+                        Dim Delay As Double = (800 + CurrentHint.Text.Length.Clamp(5, 23) * 180) * AniSpeed
                         AniStart({
                         AniStart({
                             AaX(DoubleStack, -12 - DoubleStack.Margin.Left, 50,, New AniEaseOutFluent),
                             AaX(DoubleStack, -12 - DoubleStack.Margin.Left, 50,, New AniEaseOutFluent),
                             AaX(DoubleStack, -8, 50, 50, New AniEaseInFluent),
                             AaX(DoubleStack, -8, 50, 50, New AniEaseInFluent),
@@ -122,7 +122,7 @@ Public Module ModMain
                     })
                     })
                     AniStart(Animations, "Hint Show " & NewHintControl.Tag(1))
                     AniStart(Animations, "Hint Show " & NewHintControl.Tag(1))
                     '结束动画
                     '结束动画
-                    Dim Delay As Double = (800 + MathClamp(CurrentHint.Text.Length, 5, 23) * 180) * AniSpeed
+                    Dim Delay As Double = (800 + CurrentHint.Text.Length.Clamp(5, 23) * 180) * AniSpeed
                     AniStart({
                     AniStart({
                         AaX(NewHintControl, -50, 200, Delay, New AniEaseInFluent),
                         AaX(NewHintControl, -50, 200, Delay, New AniEaseInFluent),
                         AaOpacity(NewHintControl, -1, 150, Delay),
                         AaOpacity(NewHintControl, -1, 150, Delay),
@@ -137,7 +137,7 @@ EndHint:
                 HintWaiting.RemoveAt(0)
                 HintWaiting.RemoveAt(0)
             Loop
             Loop
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "显示弹出提示失败", LogLevel.Normal)
+            Log(ex, "显示弹出提示失败", NotifyLevel.Silent)
         End Try
         End Try
     End Sub
     End Sub
     Private Sub HideAllHint()
     Private Sub HideAllHint()
@@ -256,7 +256,7 @@ EndHint:
                     MsgBox(Caption, MsgBoxStyle.OkOnly + If(IsWarn, MsgBoxStyle.Critical, MsgBoxStyle.Question), Title)
                     MsgBox(Caption, MsgBoxStyle.OkOnly + If(IsWarn, MsgBoxStyle.Critical, MsgBoxStyle.Question), Title)
                     Converter.Result = 1
                     Converter.Result = 1
                 End If
                 End If
-                Log("[Control] 主窗体加载完成前出现意料外的等待弹窗:" & Button1 & "," & Button2 & "," & Button3, LogLevel.Debug)
+                Log("[Control] 主窗体加载完成前出现意料外的等待弹窗:" & Button1 & "," & Button2 & "," & Button3, NotifyLevel.DebugModeOnly)
             Else
             Else
                 Try
                 Try
                     FrmMain.DragStop()
                     FrmMain.DragStop()
@@ -285,9 +285,9 @@ EndHint:
     ''' <param name="Button1">显示的第一个按钮,默认为“确定”。</param>
     ''' <param name="Button1">显示的第一个按钮,默认为“确定”。</param>
     ''' <param name="Button2">显示的第二个按钮,默认为“取消”。</param>
     ''' <param name="Button2">显示的第二个按钮,默认为“取消”。</param>
     ''' <param name="IsWarn">是否为警告弹窗,若为 True,弹窗配色和背景会变为红色。</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
+    Public Function MyMsgBoxInput(Title As String, Optional Text As String = Nothing, Optional DefaultInput As String = Nothing, 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}
+        Dim Converter As New MyMsgBoxConverter With {.Text = If(Text, ""), .HintText = If(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)
         WaitingMyMsgBox.Add(Converter)
         '虽然我也不知道这是啥但是能用就成了 :)
         '虽然我也不知道这是啥但是能用就成了 :)
         Try
         Try
@@ -354,7 +354,7 @@ EndHint:
                 If FrmMain.PanMsg.Visibility <> Visibility.Collapsed Then FrmMain.PanMsg.Visibility = Visibility.Collapsed
                 If FrmMain.PanMsg.Visibility <> Visibility.Collapsed Then FrmMain.PanMsg.Visibility = Visibility.Collapsed
             End If
             End If
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "处理等待中的弹窗失败", LogLevel.Feedback)
+            Log(ex, "处理等待中的弹窗失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -418,7 +418,7 @@ EndHint:
     Public FrmInstanceExport As PageInstanceExport
     Public FrmInstanceExport As PageInstanceExport
 
 
     '资源信息分页声明
     '资源信息分页声明
-    Public FrmDownloadCompDetail As PageDownloadCompDetail
+    Public FrmDownloadResourceDetail As PageDownloadResourceDetail
 
 
 #End Region
 #End Region
 
 
@@ -488,7 +488,7 @@ EndHint:
         ''' </summary>
         ''' </summary>
         Public Sub New(FilePath As String)
         Public Sub New(FilePath As String)
             RawPath = FilePath
             RawPath = FilePath
-            Dim JsonData As JObject = GetJson(ArgumentReplace(ReadFile(FilePath), AddressOf EscapeXML))
+            Dim JsonData As JObject = GetJson(ArgumentReplace(ReadFile(FilePath), AddressOf EscapeUtils.XmlEscape))
             If JsonData Is Nothing Then Throw New FileNotFoundException("未找到帮助文件:" & FilePath, FilePath)
             If JsonData Is Nothing Then Throw New FileNotFoundException("未找到帮助文件:" & FilePath, FilePath)
             '加载常规信息
             '加载常规信息
             If JsonData("Title") IsNot Nothing Then
             If JsonData("Title") IsNot Nothing Then
@@ -577,8 +577,8 @@ EndHint:
                 Try
                 Try
                     Dim IgnoreList As New List(Of String)
                     Dim IgnoreList As New List(Of String)
                     '读取自定义文件
                     '读取自定义文件
-                    If Directory.Exists(Path & "PCL\Help\") Then
-                        For Each File In EnumerateFiles(Path & "PCL\Help\")
+                    If Directory.Exists(PathExeFolder & "PCL\Help\") Then
+                        For Each File In EnumerateFiles(PathExeFolder & "PCL\Help\")
                             Select Case File.Extension.Lower
                             Select Case File.Extension.Lower
                                 Case ".helpignore"
                                 Case ".helpignore"
                                     '加载忽略列表
                                     '加载忽略列表
@@ -612,7 +612,7 @@ NextFile:
                     Next
                     Next
                     Log("[Help] 已扫描缓存文件夹下的帮助文件,目前总计 " & FileList.Count & " 条")
                     Log("[Help] 已扫描缓存文件夹下的帮助文件,目前总计 " & FileList.Count & " 条")
                 Catch ex As Exception
                 Catch ex As Exception
-                    Log(ex, "检查帮助文件夹失败", LogLevel.Msgbox)
+                    Log(ex, "检查帮助文件夹失败", NotifyLevel.MsgBox)
                 End Try
                 End Try
                 If Loader.IsInterrupted Then Return
                 If Loader.IsInterrupted Then Return
 
 
@@ -624,7 +624,7 @@ NextFile:
                         Dict.Add(Entry)
                         Dict.Add(Entry)
                         If ModeDebug Then Log("[Help] 已加载的帮助条目:" & Entry.Title & " ← " & FilePath)
                         If ModeDebug Then Log("[Help] 已加载的帮助条目:" & Entry.Title & " ← " & FilePath)
                     Catch ex As Exception
                     Catch ex As Exception
-                        Log(ex, "初始化帮助条目失败(" & FilePath & ")", LogLevel.Msgbox)
+                        Log(ex, "初始化帮助条目失败(" & FilePath & ")", NotifyLevel.MsgBox)
                     End Try
                     End Try
                 Next
                 Next
 
 
@@ -645,11 +645,11 @@ NextFile:
     Public Sub HelpTryExtract()
     Public Sub HelpTryExtract()
         If Settings.Get("SystemHelpVersion") <> VersionCode OrElse Not File.Exists(PathTemp & "Help\启动器\备份设置.xaml") Then
         If Settings.Get("SystemHelpVersion") <> VersionCode OrElse Not File.Exists(PathTemp & "Help\启动器\备份设置.xaml") Then
             DeleteDirectory(PathTemp & "Help")
             DeleteDirectory(PathTemp & "Help")
-            Directory.CreateDirectory(PathTemp & "Help")
-            WriteFile(PathTemp & "Cache\Help.zip", GetResources("Help"))
+            DirectoryUtils.Create(PathTemp & "Help")
+            FileUtils.Write(PathTemp & "Cache\Help.zip", GetResources("Help"))
             ExtractCompressedFile(PathTemp & "Cache\Help.zip", PathTemp & "Help")
             ExtractCompressedFile(PathTemp & "Cache\Help.zip", PathTemp & "Help")
             Settings.Set("SystemHelpVersion", VersionCode)
             Settings.Set("SystemHelpVersion", VersionCode)
-            Log("[Help] 已解压内置帮助文件,目前状态:" & File.Exists(PathTemp & "Help\启动器\备份设置.xaml"), LogLevel.Debug)
+            Log("[Help] 已解压内置帮助文件,目前状态:" & File.Exists(PathTemp & "Help\启动器\备份设置.xaml"), NotifyLevel.DebugModeOnly)
         End If
         End If
     End Sub
     End Sub
 
 
@@ -754,8 +754,8 @@ NextFile:
             FrmLaunchLeft.AprilPosTrans.X += Speed.X
             FrmLaunchLeft.AprilPosTrans.X += Speed.X
             FrmLaunchLeft.AprilPosTrans.Y += Speed.Y
             FrmLaunchLeft.AprilPosTrans.Y += Speed.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)
+            FrmLaunchLeft.AprilScaleTrans.ScaleX = (1 - (Math.Abs(Direction.X) - Math.Abs(Direction.Y)) * (SpeedValue / 160)).Clamp(0.2, 1.8)
+            FrmLaunchLeft.AprilScaleTrans.ScaleY = (1 - (Math.Abs(Direction.Y) - Math.Abs(Direction.X)) * (SpeedValue / 100)).Clamp(0.2, 1.8)
             '放弃提示
             '放弃提示
             Static GiveUpDistance As Double = -1500
             Static GiveUpDistance As Double = -1500
             GiveUpDistance += SpeedValue
             GiveUpDistance += SpeedValue
@@ -774,7 +774,7 @@ NextFile:
             End If
             End If
 
 
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "愚人节移动出错", LogLevel.Feedback)
+            Log(ex, "愚人节移动出错", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -790,7 +790,7 @@ NextFile:
             PostMessage(Handle, 400 * 16 + 2, 0, 0)
             PostMessage(Handle, 400 * 16 + 2, 0, 0)
             SetForegroundWindow(Handle) '不在这里放不行,神秘 WinAPI,建议别动
             SetForegroundWindow(Handle) '不在这里放不行,神秘 WinAPI,建议别动
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "设置窗口置顶失败", LogLevel.Hint)
+            Log(ex, "设置窗口置顶失败", NotifyLevel.AllUsers)
         End Try
         End Try
     End Sub
     End Sub
     Public Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ClassName As String, WindowName As String) As IntPtr
     Public Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ClassName As String, WindowName As String) As IntPtr
@@ -828,24 +828,26 @@ NextFile:
     ''' 对替换标记进行处理。会对替换内容使用 EscapeHandler 进行转义。
     ''' 对替换标记进行处理。会对替换内容使用 EscapeHandler 进行转义。
     ''' </summary>
     ''' </summary>
     Public Function ArgumentReplace(Text As String, Optional EscapeHandler As Func(Of String, String) = Nothing, Optional ReplaceTime As Boolean = True) As String
     Public Function ArgumentReplace(Text As String, Optional EscapeHandler As Func(Of String, String) = Nothing, Optional ReplaceTime As Boolean = True) As String
+        If Text Is Nothing OrElse Not Text.Contains("{") Then Return Nothing
         '预处理
         '预处理
-        If Text Is Nothing Then Return Nothing
-        Dim Replacer =
+        Static Replacer As Func(Of String, String) =
         Function(s As String) As String
         Function(s As String) As String
             If s Is Nothing Then Return ""
             If s Is Nothing Then Return ""
             If EscapeHandler Is Nothing Then Return s
             If EscapeHandler Is Nothing Then Return s
-            If s.Contains(":\") Then s = ShortenPath(s)
+            If s.Contains(":\") Then s = FileUtils.ShortenPath(s)
             Return EscapeHandler(s)
             Return EscapeHandler(s)
         End Function
         End Function
         '基础
         '基础
         Text = Text.Replace("{pcl_version}", Replacer(VersionBaseName))
         Text = Text.Replace("{pcl_version}", Replacer(VersionBaseName))
         Text = Text.Replace("{pcl_version_code}", Replacer(VersionCode))
         Text = Text.Replace("{pcl_version_code}", Replacer(VersionCode))
-        Text = Text.Replace("{pcl_version_branch}", Replacer(VersionBranchName))
+        Text = Text.Replace("{pcl_version_branch}", Replacer(BuildTypeDisplay))
+        Text = Text.Replace("{pcl_build_type}", Replacer(BuildType.ToString))
         Text = Text.Replace("{pcl_branch}", Replacer(VersionBranchMain))
         Text = Text.Replace("{pcl_branch}", Replacer(VersionBranchMain))
         Text = Text.Replace("{identify}", Replacer(Identify))
         Text = Text.Replace("{identify}", Replacer(Identify))
-        Text = Text.Replace("{path}", Replacer(Path))
-        Text = Text.Replace("{path_with_name}", Replacer(PathWithName))
+        Text = Text.Replace("{path}", Replacer(PathExeFolder))
+        Text = Text.Replace("{path_with_name}", Replacer(PathExe))
         Text = Text.Replace("{path_temp}", Replacer(PathTemp))
         Text = Text.Replace("{path_temp}", Replacer(PathTemp))
+        Text = Text.ReplaceOnDemand("{pcl_md5}", Function() Replacer(GetFileMD5(PathExe)))
         '时间
         '时间
         If ReplaceTime Then '在窗口标题中,时间会被后续动态替换,所以此时不应该替换
         If ReplaceTime Then '在窗口标题中,时间会被后续动态替换,所以此时不应该替换
             Text = Text.Replace("{date}", Replacer(Date.Now.ToString("yyyy'/'M'/'d")))
             Text = Text.Replace("{date}", Replacer(Date.Now.ToString("yyyy'/'M'/'d")))
@@ -889,11 +891,11 @@ NextFile:
             Text = Text.Replace("{login}", Replacer(Nothing))
             Text = Text.Replace("{login}", Replacer(Nothing))
         End If
         End If
         '高级
         '高级
-        Text = Text.RegexReplaceEach("\{hint\}", Function() Replacer(PageOtherTest.GetRandomHint()))
-        Text = Text.RegexReplaceEach("\{cave\}", Function() Replacer(PageOtherTest.GetRandomCave()))
-        Text = Text.RegexReplaceEach("\{setup:([a-zA-Z0-9]+)\}", Function(m) Replacer(Settings.GetSafe(m.Groups(1).Value, McInstanceSelected)))
-        Text = Text.RegexReplaceEach("\{varible:([^:\}]+)(?::([^\}]+))?\}", Function(m) Replacer(ReadReg("CustomEvent" & m.Groups(1).Value, m.Groups(2).Value)))
-        Text = Text.RegexReplaceEach("\{variable:([^:\}]+)(?::([^\}]+))?\}", Function(m) Replacer(ReadReg("CustomEvent" & m.Groups(1).Value, m.Groups(2).Value)))
+        Text = Text.RegexReplace("\{hint\}", Function() Replacer(PageOtherTest.GetRandomHint()))
+        Text = Text.RegexReplace("\{cave\}", Function() Replacer(PageOtherTest.GetRandomCave()))
+        Text = Text.RegexReplace("\{setup:([a-zA-Z0-9]+)\}", Function(m) Replacer(Settings.GetSafe(m.Groups(1).Value, McInstanceSelected)))
+        Text = Text.RegexReplace("\{varible:([^:\}]+)(?::([^\}]+))?\}", Function(m) Replacer(ReadReg("CustomEvent" & m.Groups(1).Value, m.Groups(2).Value)))
+        Text = Text.RegexReplace("\{variable:([^:\}]+)(?::([^\}]+))?\}", Function(m) Replacer(ReadReg("CustomEvent" & m.Groups(1).Value, m.Groups(2).Value)))
         Return Text
         Return Text
     End Function
     End Function
 
 
@@ -941,14 +943,14 @@ NextFile:
         Try
         Try
             ResultFolder = $"{PathTemp}TaskTemp\{GetUuid()}-{RandomInteger(0, 1000000)}\"
             ResultFolder = $"{PathTemp}TaskTemp\{GetUuid()}-{RandomInteger(0, 1000000)}\"
             If RequireNonSpace AndAlso ResultFolder.Contains(" ") Then Exit Try '带空格
             If RequireNonSpace AndAlso ResultFolder.Contains(" ") Then Exit Try '带空格
-            Directory.CreateDirectory(ResultFolder)
+            DirectoryUtils.Create(ResultFolder)
             CheckPermissionWithException(ResultFolder)
             CheckPermissionWithException(ResultFolder)
             Return ResultFolder
             Return ResultFolder
         Catch
         Catch
         End Try
         End Try
         '使用备用路径
         '使用备用路径
         ResultFolder = $"{OsDrive}ProgramData\PCL\TaskTemp\{GetUuid()}-{RandomInteger(0, 1000000)}\"
         ResultFolder = $"{OsDrive}ProgramData\PCL\TaskTemp\{GetUuid()}-{RandomInteger(0, 1000000)}\"
-        Directory.CreateDirectory(ResultFolder)
+        DirectoryUtils.Create(ResultFolder)
         CheckPermission(ResultFolder)
         CheckPermission(ResultFolder)
         Return ResultFolder
         Return ResultFolder
     End Function
     End Function
@@ -968,7 +970,7 @@ NextFile:
             If ThemeDontClick = 2 Then ThemeRefresh()
             If ThemeDontClick = 2 Then ThemeRefresh()
 #End Region
 #End Region
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "短程主时钟执行异常", LogLevel.Critical)
+            Log(ex, "短程主时钟执行异常", NotifyLevel.Critical)
         End Try
         End Try
         Timer4Count += 1
         Timer4Count += 1
         If Timer4Count = 4 Then
         If Timer4Count = 4 Then
@@ -978,7 +980,7 @@ NextFile:
                 If ThemeNow = 12 Then ThemeRefresh()
                 If ThemeNow = 12 Then ThemeRefresh()
 #End Region
 #End Region
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "中程主时钟执行异常", LogLevel.Debug)
+                Log(ex, "中程主时钟执行异常", NotifyLevel.DebugModeOnly)
             End Try
             End Try
         End If
         End If
         Timer150Count += 1
         Timer150Count += 1
@@ -997,7 +999,7 @@ NextFile:
                 End Sub)
                 End Sub)
 #End Region
 #End Region
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "长程主时钟执行异常", LogLevel.Critical)
+                Log(ex, "长程主时钟执行异常", NotifyLevel.Critical)
             End Try
             End Try
         End If
         End If
     End Sub
     End Sub
@@ -1010,7 +1012,7 @@ NextFile:
                     Thread.Sleep(50 * 0.98)
                     Thread.Sleep(50 * 0.98)
                 Loop
                 Loop
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "程序主时钟出错", LogLevel.Feedback)
+                Log(ex, "程序主时钟出错", NotifyLevel.MsgBoxAndFeedback)
             End Try
             End Try
         End Sub, "Timer Main")
         End Sub, "Timer Main")
         If Not IsAprilEnabled Then Return
         If Not IsAprilEnabled Then Return
@@ -1026,7 +1028,7 @@ NextFile:
                     Thread.Sleep(1)
                     Thread.Sleep(1)
                 Loop
                 Loop
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "愚人节主时钟出错", LogLevel.Feedback)
+                Log(ex, "愚人节主时钟出错", NotifyLevel.MsgBoxAndFeedback)
             End Try
             End Try
         End Sub, "Timer Main Fool")
         End Sub, "Timer Main Fool")
     End Sub
     End Sub

+ 26 - 22
Plain Craft Launcher 2/Modules/ModMusic.vb

@@ -1,4 +1,6 @@
-Public Module ModMusic
+Imports NAudio.Wave
+
+Public Module ModMusic
 
 
 #Region "播放列表"
 #Region "播放列表"
 
 
@@ -21,8 +23,8 @@
             '初始化全部可用音乐列表
             '初始化全部可用音乐列表
             If MusicAllList Is Nothing Then
             If MusicAllList Is Nothing Then
                 MusicAllList = New List(Of String)
                 MusicAllList = New List(Of String)
-                Directory.CreateDirectory(Path & "PCL\Musics\")
-                For Each File In EnumerateFiles(Path & "PCL\Musics\")
+                DirectoryUtils.Create(PathExeFolder & "PCL\Musics\")
+                For Each File In EnumerateFiles(PathExeFolder & "PCL\Musics\")
                     '文件夹可能会被加入 .ini 文件夹配置文件、一些乱七八糟的 .jpg 文件啥的
                     '文件夹可能会被加入 .ini 文件夹配置文件、一些乱七八糟的 .jpg 文件啥的
                     Dim Ext As String = File.Extension.Lower
                     Dim Ext As String = File.Extension.Lower
                     If {".ini", ".jpg", ".txt", ".cfg", ".lrc", ".db", ".png"}.Contains(Ext) Then Continue For
                     If {".ini", ".jpg", ".txt", ".cfg", ".lrc", ".db", ".png"}.Contains(Ext) Then Continue For
@@ -37,7 +39,7 @@
                 MusicWaitingList.Add(PreventFirst)
                 MusicWaitingList.Add(PreventFirst)
             End If
             End If
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "初始化音乐列表失败", LogLevel.Feedback)
+            Log(ex, "初始化音乐列表失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
     ''' <summary>
     ''' <summary>
@@ -79,7 +81,7 @@
                     If MusicState = MusicStates.Pause Then
                     If MusicState = MusicStates.Pause Then
                         FrmMain.BtnExtraMusic.Logo = Logo.IconPlay
                         FrmMain.BtnExtraMusic.Logo = Logo.IconPlay
                         FrmMain.BtnExtraMusic.LogoScale = 0.8
                         FrmMain.BtnExtraMusic.LogoScale = 0.8
-                        ToolTipText = "已暂停:" & IO.Path.GetFileNameWithoutExtension(MusicCurrent)
+                        ToolTipText = "已暂停:" & Path.GetFileNameWithoutExtension(MusicCurrent)
                         If MusicAllList.Count > 1 Then
                         If MusicAllList.Count > 1 Then
                             ToolTipText += vbCrLf & "左键恢复播放,右键播放下一曲。"
                             ToolTipText += vbCrLf & "左键恢复播放,右键播放下一曲。"
                         Else
                         Else
@@ -88,7 +90,7 @@
                     Else
                     Else
                         FrmMain.BtnExtraMusic.Logo = Logo.IconMusic
                         FrmMain.BtnExtraMusic.Logo = Logo.IconMusic
                         FrmMain.BtnExtraMusic.LogoScale = 1
                         FrmMain.BtnExtraMusic.LogoScale = 1
-                        ToolTipText = "正在播放:" & IO.Path.GetFileNameWithoutExtension(MusicCurrent)
+                        ToolTipText = "正在播放:" & Path.GetFileNameWithoutExtension(MusicCurrent)
                         If MusicAllList.Count > 1 Then
                         If MusicAllList.Count > 1 Then
                             ToolTipText += vbCrLf & "左键暂停,右键播放下一曲。"
                             ToolTipText += vbCrLf & "左键暂停,右键播放下一曲。"
                         Else
                         Else
@@ -101,7 +103,7 @@
                 If FrmSetupUI IsNot Nothing Then FrmSetupUI.MusicRefreshUI()
                 If FrmSetupUI IsNot Nothing Then FrmSetupUI.MusicRefreshUI()
 
 
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "刷新背景音乐 UI 失败", LogLevel.Feedback)
+                Log(ex, "刷新背景音乐 UI 失败", NotifyLevel.MsgBoxAndFeedback)
             End Try
             End Try
         End Sub)
         End Sub)
     End Sub
     End Sub
@@ -119,7 +121,7 @@
                 Case MusicStates.Play
                 Case MusicStates.Play
                     MusicPause()
                     MusicPause()
                 Case Else
                 Case Else
-                    Log("[Music] 音乐目前为停止状态,已强制尝试开始播放", LogLevel.Debug)
+                    Log("[Music] 音乐目前为停止状态,已强制尝试开始播放", NotifyLevel.DebugModeOnly)
                     MusicRefreshPlay(False)
                     MusicRefreshPlay(False)
             End Select
             End Select
         End If
         End If
@@ -155,9 +157,9 @@
         Get
         Get
             If MusicNAudio Is Nothing Then Return MusicStates.Stop
             If MusicNAudio Is Nothing Then Return MusicStates.Stop
             Select Case MusicNAudio.PlaybackState
             Select Case MusicNAudio.PlaybackState
-                Case 0 'NAudio.Wave.PlaybackState.Stopped
+                Case 0 'PlaybackState.Stopped
                     Return MusicStates.Stop
                     Return MusicStates.Stop
-                Case 2 'NAudio.Wave.PlaybackState.Paused
+                Case 2 'PlaybackState.Paused
                     Return MusicStates.Pause
                     Return MusicStates.Pause
                 Case Else
                 Case Else
                     Return MusicStates.Play
                     Return MusicStates.Play
@@ -202,7 +204,7 @@
             MusicRefreshUI()
             MusicRefreshUI()
 
 
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "刷新背景音乐播放失败", LogLevel.Feedback)
+            Log(ex, "刷新背景音乐播放失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
     ''' <summary>
     ''' <summary>
@@ -260,7 +262,7 @@
 #End Region
 #End Region
 
 
     ''' <summary>
     ''' <summary>
-    ''' 当前正在播放的 NAudio.Wave.WaveOutEvent。
+    ''' 当前正在播放的 WaveOutEvent。
     ''' 如果为它赋了类别,则会没有必要地加载 NAudio.dll。
     ''' 如果为它赋了类别,则会没有必要地加载 NAudio.dll。
     ''' </summary>
     ''' </summary>
     Public MusicNAudio = Nothing
     Public MusicNAudio = Nothing
@@ -273,18 +275,18 @@
     ''' 在 MusicUuid 不变的前提下,持续播放某地址的音乐,且在播放结束后随机播放下一曲。
     ''' 在 MusicUuid 不变的前提下,持续播放某地址的音乐,且在播放结束后随机播放下一曲。
     ''' </summary>
     ''' </summary>
     Private Sub MusicLoop(Optional IsFirstLoad As Boolean = False)
     Private Sub MusicLoop(Optional IsFirstLoad As Boolean = False)
-        Dim CurrentWave As NAudio.Wave.WaveOutEvent = Nothing
-        Dim Reader As NAudio.Wave.WaveStream = Nothing
+        Dim CurrentWave As WaveOutEvent = Nothing
+        Dim Reader As WaveStream = Nothing
         Try
         Try
             '开始播放
             '开始播放
-            CurrentWave = New NAudio.Wave.WaveOutEvent()
+            CurrentWave = New WaveOutEvent()
             MusicNAudio = CurrentWave
             MusicNAudio = CurrentWave
             CurrentWave.DeviceNumber = -1
             CurrentWave.DeviceNumber = -1
             Try
             Try
-                Reader = New NAudio.Wave.AudioFileReader(MusicCurrent)
+                Reader = New AudioFileReader(MusicCurrent)
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "使用 AudioFileReader 加载音频文件失败,换用 MediaFoundationReader 重试", LogLevel.Developer)
-                Reader = New NAudio.Wave.MediaFoundationReader(MusicCurrent)
+                Log(ex, "使用 AudioFileReader 加载音频文件失败,换用 MediaFoundationReader 重试", NotifyLevel.DevelopOnly)
+                Reader = New MediaFoundationReader(MusicCurrent)
             End Try
             End Try
             CurrentWave.Init(Reader)
             CurrentWave.Init(Reader)
             CurrentWave.Play()
             CurrentWave.Play()
@@ -293,11 +295,13 @@
             MusicRefreshUI()
             MusicRefreshUI()
             '停止条件:播放完毕或变化
             '停止条件:播放完毕或变化
             Dim PreviousVolume = 0
             Dim PreviousVolume = 0
-            While CurrentWave.Equals(MusicNAudio) AndAlso Not CurrentWave.PlaybackState = NAudio.Wave.PlaybackState.Stopped
+            While CurrentWave.Equals(MusicNAudio) AndAlso Not CurrentWave.PlaybackState = PlaybackState.Stopped
                 If Settings.Get("UiMusicVolume") <> PreviousVolume Then
                 If Settings.Get("UiMusicVolume") <> PreviousVolume Then
                     '更新音量
                     '更新音量
                     PreviousVolume = Settings.Get("UiMusicVolume")
                     PreviousVolume = Settings.Get("UiMusicVolume")
+#Disable Warning BC40008 '类型或成员已过时
                     CurrentWave.Volume = PreviousVolume / 1000
                     CurrentWave.Volume = PreviousVolume / 1000
+#Enable Warning BC40008 '类型或成员已过时
                 End If
                 End If
                 '更新进度条
                 '更新进度条
                 Dim Percent = Reader.CurrentTime.TotalMilliseconds / Reader.TotalTime.TotalMilliseconds
                 Dim Percent = Reader.CurrentTime.TotalMilliseconds / Reader.TotalTime.TotalMilliseconds
@@ -305,9 +309,9 @@
                 Thread.Sleep(100)
                 Thread.Sleep(100)
             End While
             End While
             '当前音乐已播放结束,继续下一曲
             '当前音乐已播放结束,继续下一曲
-            If CurrentWave.PlaybackState = NAudio.Wave.PlaybackState.Stopped AndAlso MusicAllList.Any Then MusicStartPlay(DequeueNextMusicAddress)
+            If CurrentWave.PlaybackState = PlaybackState.Stopped AndAlso MusicAllList.Any Then MusicStartPlay(DequeueNextMusicAddress)
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "播放音乐出现内部错误(" & MusicCurrent & ")", LogLevel.Developer)
+            Log(ex, "播放音乐出现内部错误(" & MusicCurrent & ")", NotifyLevel.DevelopOnly)
             If TypeOf ex Is NAudio.MmException AndAlso ex.Message.Contains("AlreadyAllocated") Then
             If TypeOf ex Is NAudio.MmException AndAlso ex.Message.Contains("AlreadyAllocated") Then
                 Hint("你的音频设备正被其他程序占用。请在关闭占用的程序后重启 PCL,才能恢复音乐播放功能!", HintType.Red)
                 Hint("你的音频设备正被其他程序占用。请在关闭占用的程序后重启 PCL,才能恢复音乐播放功能!", HintType.Red)
                 Thread.Sleep(1000000000)
                 Thread.Sleep(1000000000)
@@ -320,7 +324,7 @@
                 ex.Message.Contains("0xC00D36C4") Then '#5096:不支持给定的 URL 的字节流类型。 (异常来自 HRESULT:0xC00D36C4)
                 ex.Message.Contains("0xC00D36C4") Then '#5096:不支持给定的 URL 的字节流类型。 (异常来自 HRESULT:0xC00D36C4)
                 Hint("播放音乐失败(" & GetFileNameFromPath(MusicCurrent) & "):PCL 可能不支持此音乐格式,请将格式转换为 .wav、.mp3 或 .flac 后再试", HintType.Red)
                 Hint("播放音乐失败(" & GetFileNameFromPath(MusicCurrent) & "):PCL 可能不支持此音乐格式,请将格式转换为 .wav、.mp3 或 .flac 后再试", HintType.Red)
             Else
             Else
-                Log(ex, "播放音乐失败(" & GetFileNameFromPath(MusicCurrent) & ")", LogLevel.Hint)
+                Log(ex, "播放音乐失败(" & GetFileNameFromPath(MusicCurrent) & ")", NotifyLevel.AllUsers)
             End If
             End If
             '将播放错误的音乐从列表中移除
             '将播放错误的音乐从列表中移除
             MusicAllList.Remove(MusicCurrent)
             MusicAllList.Remove(MusicCurrent)

+ 35 - 74
Plain Craft Launcher 2/Modules/ModSecret.vb

@@ -4,8 +4,6 @@ Imports System.Security.Cryptography
 
 
 Friend Module ModSecret
 Friend Module ModSecret
 
 
-#Region "杂项"
-
     '标注 PCL 的不同分支,仅用于替换标记
     '标注 PCL 的不同分支,仅用于替换标记
     Public Const VersionBranchMain As String = "OpenSource"
     Public Const VersionBranchMain As String = "OpenSource"
     '在开源版的注册表与常规版的注册表隔离,以防数据冲突
     '在开源版的注册表与常规版的注册表隔离,以防数据冲突
@@ -17,53 +15,6 @@ Friend Module ModSecret
     '用于匿名数据收集的腾讯云日志服务上报 URL,形如 https://{region}.cls.tencentcs.com/track?topic_id={topic_id}
     '用于匿名数据收集的腾讯云日志服务上报 URL,形如 https://{region}.cls.tencentcs.com/track?topic_id={topic_id}
     Public Const ClsBaseUrl As String = ""
     Public Const ClsBaseUrl As String = ""
 
 
-    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](ProcessReturnValues.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](ProcessReturnValues.Cancel)
-        End If
-        '开源版本提示
-        MyMsgBox($"该版本中无法使用以下特性:
-- CurseForge API 调用:需要你自行申请 API Key,然后添加到 ModSecret.vb 的开头
-- 正版登录:需要你自行申请 Client ID,然后添加到 ModSecret.vb 的开头
-- 更新与联网通知:避免滥用隐患
-- 主题切换:这是需要赞助解锁的纪念性质的功能,别让赞助者太伤心啦……
-- 百宝箱:开发早期往里面塞了些开发工具,整理起来太麻烦了", "开源版本说明")
-    End Sub
-
-    ''' <summary>
-    ''' 获取设备标识码。
-    ''' </summary>
-    Friend Function SecretGetIdentify() As String
-        Return "0000-0000-0000-0000"
-    End Function
-
-#End Region
-
 #Region "网络鉴权"
 #Region "网络鉴权"
 
 
     Friend Function SecretCdnSign(UrlWithMark As String) As String
     Friend Function SecretCdnSign(UrlWithMark As String) As String
@@ -78,9 +29,9 @@ Friend Module ModSecret
             If Url.Contains("baidupcs.com") OrElse Url.Contains("baidu.com") Then
             If Url.Contains("baidupcs.com") OrElse Url.Contains("baidu.com") Then
                 Req.Headers.Add("User-Agent", "LogStatistic")  '#4951
                 Req.Headers.Add("User-Agent", "LogStatistic")  '#4951
             ElseIf SimulateBrowserHeaders Then
             ElseIf SimulateBrowserHeaders Then
-                Req.Headers.Add("User-Agent", $"PCL2/{VersionBaseName}.{VersionBranchCode} Mozilla/5.0 AppleWebKit/537.36 Chrome/63.0.3239.132 Safari/537.36")
+                Req.Headers.Add("User-Agent", $"PCL2/{VersionBaseName}.{CInt(BuildType)} Mozilla/5.0 AppleWebKit/537.36 Chrome/63.0.3239.132 Safari/537.36")
             Else
             Else
-                Req.Headers.Add("User-Agent", $"PCL2/{VersionBaseName}.{VersionBranchCode}")
+                Req.Headers.Add("User-Agent", $"PCL2/{VersionBaseName}.{CInt(BuildType)}")
             End If
             End If
         End If
         End If
         If Not SimulateBrowserHeaders Then Req.Headers.Add("Referer", $"http://{VersionCode}.open.pcl2.server/")
         If Not SimulateBrowserHeaders Then Req.Headers.Add("Referer", $"http://{VersionCode}.open.pcl2.server/")
@@ -157,7 +108,7 @@ Friend Module ModSecret
     Public ColorSemiTransparent As New MyColor(1, Color8)
     Public ColorSemiTransparent As New MyColor(1, Color8)
 
 
     Public ThemeNow As Integer = -1
     Public ThemeNow As Integer = -1
-    Public ColorHue As Integer = 210, ColorSat As Integer = 85, ColorLightAdjust As Integer = 0, ColorHueTopbarDelta As Object = 0
+    Public ColorHue As Integer = 210, ColorSat As Integer = 85, ColorLightAdjust As Integer = 0, ColorHueTopbarDelta As OneOf(Of Integer, Integer()) = 0
     Public ThemeDontClick As Integer = 0
     Public ThemeDontClick As Integer = 0
 
 
     Public Sub ThemeRefresh(Optional NewTheme As Integer = -1)
     Public Sub ThemeRefresh(Optional NewTheme As Integer = -1)
@@ -199,7 +150,7 @@ Friend Module ModSecret
             Application.Current.Resources("ColorObjectBg1") = CType(ColorBg1, Color)
             Application.Current.Resources("ColorObjectBg1") = CType(ColorBg1, Color)
             ThemeRefreshMain()
             ThemeRefreshMain()
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "刷新主题颜色失败", LogLevel.Hint)
+            Log(ex, "刷新主题颜色失败", NotifyLevel.AllUsers)
         End Try
         End Try
     End Sub
     End Sub
     Public Sub ThemeRefreshMain()
     Public Sub ThemeRefreshMain()
@@ -208,15 +159,10 @@ Friend Module ModSecret
             If Not FrmMain.IsLoaded Then Return
             If Not FrmMain.IsLoaded Then Return
             '顶部条背景
             '顶部条背景
             Dim Brush = New LinearGradientBrush With {.EndPoint = New Point(1, 0), .StartPoint = New Point(0, 0)}
             Dim Brush = New LinearGradientBrush With {.EndPoint = New Point(1, 0), .StartPoint = New Point(0, 0)}
-            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
+            Dim Deltas = ColorHueTopbarDelta.Switch(Function(d) New Integer() {-d, 0, d}, Function(d) d)
+            Brush.GradientStops.Add(New GradientStop With {.Offset = 0, .Color = New MyColor().FromHSL2(ColorHue + Deltas(0), ColorSat, 48 + ColorLightAdjust)})
+            Brush.GradientStops.Add(New GradientStop With {.Offset = 0.5, .Color = New MyColor().FromHSL2(ColorHue + Deltas(1), ColorSat, 54 + ColorLightAdjust)})
+            Brush.GradientStops.Add(New GradientStop With {.Offset = 1, .Color = New MyColor().FromHSL2(ColorHue + Deltas(2), ColorSat, 48 + ColorLightAdjust)})
             FrmMain.PanTitle.Background = Brush
             FrmMain.PanTitle.Background = Brush
             FrmMain.PanTitle.Background.Freeze()
             FrmMain.PanTitle.Background.Freeze()
             '主页面背景
             '主页面背景
@@ -240,30 +186,23 @@ Friend Module ModSecret
     Friend Function ThemeUnlock(Id As Integer, Optional ShowDoubleHint As Boolean = True, Optional UnlockHint As String = Nothing) As Boolean
     Friend Function ThemeUnlock(Id As Integer, Optional ShowDoubleHint As Boolean = True, Optional UnlockHint As String = Nothing) As Boolean
         Return False
         Return False
     End Function
     End Function
-    Friend Function ThemeCheckGold(Optional Code As String = Nothing) As Boolean
-        Return False
-    End Function
-    Friend Function DonateCodeInput() As Boolean?
-        Return Nothing
-    End Function
 
 
 #End Region
 #End Region
 
 
 #Region "更新"
 #Region "更新"
 
 
-    Public IsUpdateStarted As Boolean = False
-    Public IsUpdateWaitingRestart As Boolean = False
-    Public Sub UpdateCheckByButton()
+    Friend Sub UpdateCheckByButton()
         Hint("该版本中不包含更新功能……")
         Hint("该版本中不包含更新功能……")
     End Sub
     End Sub
-    Public Sub UpdateStart(BaseUrl As String, Slient As Boolean, Optional ReceivedKey As String = Nothing, Optional ForceValidated As Boolean = False)
-    End Sub
+
+    Friend IsUpdateWaitingRestart As Boolean = False
     Public Sub UpdateRestart(TriggerRestartAndByEnd As Boolean)
     Public Sub UpdateRestart(TriggerRestartAndByEnd As Boolean)
     End Sub
     End Sub
     Public Sub UpdateReplace(ProcessId As Integer, OldFileName As String, NewFileName As String, TriggerRestart As Boolean)
     Public Sub UpdateReplace(ProcessId As Integer, OldFileName As String, NewFileName As String, TriggerRestart As Boolean)
     End Sub
     End Sub
+
     ''' <summary>
     ''' <summary>
-    ''' 确保 PathTemp 下的 Latest.exe 是最新正式版的 PCL,它会被用于整合包打包。
+    ''' 确保 PathTemp/Latest.exe 是最新正式版的 PCL,它会被用于整合包打包。
     ''' 如果不是,则下载一个。
     ''' 如果不是,则下载一个。
     ''' </summary>
     ''' </summary>
     Friend Sub DownloadLatestPCL(Optional LoaderToSyncProgress As LoaderBase = Nothing)
     Friend Sub DownloadLatestPCL(Optional LoaderToSyncProgress As LoaderBase = Nothing)
@@ -285,4 +224,26 @@ Friend Module ModSecret
 
 
 #End Region
 #End Region
 
 
+#Region "赞助等级"
+
+    Public ReadOnly Property DonationRank As DonationRanks
+        Get
+            Return DonationRanks.None
+        End Get
+    End Property
+
+    Public Sub InputPotatoCode(IsUpdating As Boolean)
+    End Sub
+    Friend Sub GeneratePotatoCode()
+    End Sub
+
+    ''' <summary>
+    ''' 获取设备识别码。
+    ''' </summary>
+    Friend Function GetIdentify() As String
+        Return "0000-0000-0000-0000"
+    End Function
+
+#End Region
+
 End Module
 End Module

+ 507 - 0
Plain Craft Launcher 2/Modules/Resource/ResourceFile.vb

@@ -0,0 +1,507 @@
+''' <summary>
+''' 本地的社区资源文件。
+''' </summary>
+Public Class ResourceFile
+
+#Region "文件信息"
+
+    ''' <summary>
+    ''' 该文件的文件信息。
+    ''' </summary>
+    Public ReadOnly File As FileInfo
+    Public Sub New(FileInfo As FileInfo)
+        Me.File = FileInfo
+    End Sub
+    Public Sub New(FullName As String)
+        Me.New(New FileInfo(FullName))
+    End Sub
+
+    ''' <summary>
+    ''' 该文件是否没有被禁用。
+    ''' </summary>
+    Public ReadOnly Property IsEnabled As Boolean
+        Get
+            Return {".jar", ".zip", ".litemod"}.Contains(File.Extension.Lower)
+        End Get
+    End Property
+    ''' <summary>
+    ''' 假若该资源未被禁用时,文件的文件名。
+    ''' </summary>
+    Public ReadOnly Property EnabledName As String
+        Get
+            Return File.Name.Replace(".disabled", "").Replace(".old", "")
+        End Get
+    End Property
+    ''' <summary>
+    ''' 假若该资源未被禁用时,文件的完整路径。
+    ''' </summary>
+    Public ReadOnly Property EnabledFullName As String
+        Get
+            Return $"{File.DirectoryName}\{EnabledName}"
+        End Get
+    End Property
+
+#End Region
+
+#Region "信息项"
+
+    ''' <summary>
+    ''' 在初始化结束之后,任何信息被后续更新时触发此事件。
+    ''' </summary>
+    Public Event OnUpdate(sender As ResourceFile)
+
+    ''' <summary>
+    ''' 可读名称。
+    ''' 可能为无扩展的文件名,但不会是 Nothing。
+    ''' </summary>
+    Public Property Display As String
+        Get
+            If _Display Is Nothing Then _Display = ProjectVersion?.Display
+            If _Display Is Nothing Then _Display = Path.GetFileNameWithoutExtension(File.FullName)
+            Return _Display
+        End Get
+        Set(value As String)
+            If _Display Is Nothing AndAlso value IsNot Nothing AndAlso Not value.Contains("modname") AndAlso
+               value.Lower <> "name" AndAlso value.Count > 1 AndAlso Val(value).ToString <> value Then
+                _Display = value
+            End If
+        End Set
+    End Property
+    Private _Display As String = Nothing
+
+    ''' <summary>
+    ''' Mod 的描述信息。
+    ''' 可能为 Nothing。
+    ''' </summary>
+    Public Property Description As String
+        Get
+            Return _Description
+        End Get
+        Set(value As String)
+            If _Description Is Nothing AndAlso value IsNot Nothing AndAlso value.Count > 2 Then
+                _Description = value.ToString.Trim(vbLf)
+                '优化显示:若以 [a-zA-Z0-9] 结尾,加上小数点句号
+                If _Description.Lower.LastIndexOfAny("qwertyuiopasdfghjklzxcvbnm0123456789") = _Description.Count - 1 Then _Description += "."
+            End If
+        End Set
+    End Property
+    Private _Description As String = Nothing
+
+    ''' <summary>
+    ''' Mod 的版本。
+    ''' 不保证符合版本格式规范,且可能为 Nothing。
+    ''' </summary>
+    Public Property Version As String
+        Get
+            If _Version Is Nothing Then _Version = ProjectVersion?.Version
+            Return _Version
+        End Get
+        Set(value As String)
+            If _Version IsNot Nothing AndAlso RegexCheck(_Version, "[0-9.\-]+") Then Return
+            If value IsNot Nothing AndAlso value.ContainsIgnoreCase("version") Then value = "version" '需要修改的标识
+            _Version = value
+        End Set
+    End Property
+    Public _Version As String = Nothing
+
+#End Region
+
+#Region "从 JAR 获取信息"
+
+    ''' <summary>
+    ''' 从 JAR 文件中获取 Mod 信息。
+    ''' </summary>
+    Public Sub LoadMetadataFromJar()
+        Static IsLoaded As Boolean = False
+        If IsLoaded Then Return
+        IsLoaded = True
+        Dim Jar As ZipArchive = Nothing
+        Try
+            '基础可用性检查
+            If File.FullName.Length < 2 Then Throw New FileNotFoundException("错误的 Mod 文件路径(" & If(File.FullName, "null") & ")")
+            If Not IO.File.Exists(File.FullName) Then Throw New FileNotFoundException("未找到 Mod 文件(" & File.FullName & ")")
+            '打开 Jar 文件
+            Jar = New ZipArchive(New FileStream(File.FullName, FileMode.Open, FileAccess.Read, FileShare.Read))
+            '信息获取
+            LoadMetadataFromJar(Jar)
+        Catch ex As UnauthorizedAccessException
+            Log(ex, $"Mod 文件由于无权限无法打开({File.FullName})", NotifyLevel.DevelopOnly)
+        Catch ex As Exception
+            Log(ex, $"Mod 文件无法打开({File.FullName})", NotifyLevel.DevelopOnly)
+        Finally
+            If Jar IsNot Nothing Then Jar.Dispose()
+        End Try
+    End Sub
+    Private Sub LoadMetadataFromJar(Jar As ZipArchive)
+
+#Region "尝试使用 mcmod.info"
+        Try
+            '获取信息文件
+            Dim InfoEntry As ZipArchiveEntry = Jar.GetEntry("mcmod.info")
+            Dim InfoString As String = Nothing
+            If InfoEntry IsNot Nothing Then
+                InfoString = ReadFile(InfoEntry.Open())
+                If InfoString.Length < 15 Then InfoString = Nothing
+            End If
+            If InfoString Is Nothing Then Exit Try
+            '获取可用 Json 项
+            Dim InfoObject As JObject
+            Dim JsonObject = GetJson(InfoString)
+            If JsonObject.Type = JTokenType.Array Then
+                InfoObject = JsonObject(0)
+            Else
+                InfoObject = JsonObject("modList")(0)
+            End If
+            '从文件中获取 Mod 信息项
+            Display = InfoObject("name")
+            Description = InfoObject("description")
+            Version = InfoObject("version")
+        Catch ex As Exception
+            Log(ex, $"读取 mcmod.info 时出现未知错误({File.FullName})", NotifyLevel.DevelopOnly)
+        End Try
+#End Region
+#Region "尝试使用 fabric.mod.json"
+        Try
+            '获取 fabric.mod.json 文件
+            Dim FabricEntry As ZipArchiveEntry = Jar.GetEntry("fabric.mod.json")
+            Dim FabricText As String = Nothing
+            If FabricEntry IsNot Nothing Then
+                FabricText = ReadFile(FabricEntry.Open(), Encoding.UTF8)
+                If Not FabricText.Contains("schemaVersion") Then FabricText = Nothing
+            End If
+            If FabricText Is Nothing Then Exit Try
+            Dim FabricObject As JObject = GetJson(FabricText)
+GotFabric:
+            '从文件中获取 Mod 信息项
+            If FabricObject.ContainsKey("name") Then Display = FabricObject("name")
+            If FabricObject.ContainsKey("version") Then Version = FabricObject("version")
+            If FabricObject.ContainsKey("description") Then Description = FabricObject("description")
+            '加载成功
+            GoTo Finished
+        Catch ex As Exception
+            Log(ex, "读取 fabric.mod.json 时出现未知错误(" & File.FullName & ")", NotifyLevel.DevelopOnly)
+        End Try
+#End Region
+#Region "尝试使用 mods.toml"
+        Try
+            '获取 mods.toml 文件
+            Dim TomlEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/mods.toml")
+            Dim TomlText As String = Nothing
+            If TomlEntry IsNot Nothing Then
+                TomlText = ReadFile(TomlEntry.Open())
+                If TomlText.Length < 15 Then TomlText = Nothing
+            End If
+            If TomlText Is Nothing Then Exit Try
+            '文件标准化:统一换行符为 vbLf,去除注释、头尾的空格、空行
+            Dim Lines As New List(Of String)
+            For Each Line In TomlText.ReplaceLineEndings(vbLf).Split(vbLf) '统一换行符
+                If Line.StartsWithF("#") Then '去除注释
+                    Continue For
+                ElseIf Line.Contains("#") Then
+                    Line = Line.Substring(0, Line.IndexOfF("#"))
+                End If
+                Line = Line.Trim(New Char() {" "c, "	"c, " "c}) '去除头尾的空格
+                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))}
+            For i = 0 To Lines.Count - 1
+                Dim Line As String = Lines(i)
+                If Line.StartsWithF("[") AndAlso Line.EndsWithF("]") Then
+                    '段落标记
+                    Dim Header = Line.Trim("[]".ToCharArray)
+                    TomlData.Add(New KeyValuePair(Of String, Dictionary(Of String, Object))(Header, New Dictionary(Of String, Object)))
+                ElseIf Line.Contains("=") Then
+                    '字段标记
+                    Dim Key As String = Line.Substring(0, Line.IndexOfF("=")).TrimEnd(New Char() {" "c, "	"c, " "c})
+                    Dim RawValue As String = Line.Substring(Line.IndexOfF("=") + 1).TrimStart(New Char() {" "c, "	"c, " "c})
+                    Dim Value As Object
+                    If RawValue.StartsWithF("""") AndAlso RawValue.EndsWithF("""") Then
+                        '单行字符串
+                        Value = RawValue.Trim("""")
+                    ElseIf RawValue.StartsWithF("'''") Then
+                        '多行字符串
+                        Dim ValueLines As New List(Of String) From {RawValue.TrimStart("'")}
+                        If ValueLines(0).EndsWithF("'''") Then '把多行字符串按单行写法写的错误处理(#2732)
+                            ValueLines(0) = ValueLines(0).TrimEnd("'")
+                        Else
+                            Do Until i >= Lines.Count - 1
+                                i += 1
+                                Dim ValueLine As String = Lines(i)
+                                If ValueLine.EndsWithF("'''") Then
+                                    ValueLines.Add(ValueLine.TrimEnd("'"))
+                                    Exit Do
+                                Else
+                                    ValueLines.Add(ValueLine)
+                                End If
+                            Loop
+                        End If
+                        Value = ValueLines.Join(vbLf).Trim(vbLf).ReplaceLineEndings(vbCrLf)
+                    ElseIf RawValue.Lower = "true" OrElse RawValue.Lower = "false" Then
+                        '布尔型
+                        Value = (RawValue.Lower = "true")
+                    ElseIf Val(RawValue).ToString = RawValue Then
+                        '数字型
+                        Value = Val(RawValue)
+                    Else
+                        '不知道是个啥玩意儿,直接存储
+                        Value = RawValue
+                    End If
+                    TomlData.Last.Value(Key) = Value
+                Else
+                    '不知道是个啥玩意儿
+                    Exit Try
+                End If
+            Next
+            '从文件数据中获取信息
+            Dim ModEntry As Dictionary(Of String, Object) = Nothing
+            For Each TomlSubData In TomlData
+                If TomlSubData.Key = "mods" Then
+                    ModEntry = TomlSubData.Value
+                    Exit For
+                End If
+            Next
+            If ModEntry Is Nothing OrElse Not ModEntry.ContainsKey("modId") Then Exit Try
+            If ModEntry.ContainsKey("displayName") Then Display = ModEntry("displayName")
+            If ModEntry.ContainsKey("description") Then Description = ModEntry("description")
+            If ModEntry.ContainsKey("version") Then Version = ModEntry("version")
+            '加载成功
+            GoTo Finished
+        Catch ex As Exception
+            Log(ex, $"读取 mods.toml 时出现未知错误({File.FullName})", NotifyLevel.DevelopOnly)
+        End Try
+#End Region
+#Region "尝试使用 fml_cache_annotation.json"
+        Try
+            '获取 fml_cache_annotation.json 文件
+            Dim FmlEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/fml_cache_annotation.json")
+            Dim FmlText As String = Nothing
+            If FmlEntry IsNot Nothing Then
+                FmlText = ReadFile(FmlEntry.Open(), Encoding.UTF8)
+                If Not FmlText.Contains("Lnet/minecraftforge/fml/common/Mod;") Then FmlText = Nothing
+            End If
+            If FmlText Is Nothing Then Exit Try
+            Dim FmlJson As JObject = GetJson(FmlText)
+            '获取可用 Json 项
+            Dim FmlObject As JObject = Nothing
+            For Each ModFilePair In FmlJson
+                Dim ModFileAnnos As JArray = ModFilePair.Value("annotations")
+                If ModFileAnnos IsNot Nothing Then
+                    '先获取 Mod
+                    For Each ModFileAnno In ModFileAnnos
+                        Dim Name As String = If(ModFileAnno("name"), "")
+                        If Name = "Lnet/minecraftforge/fml/common/Mod;" Then
+                            FmlObject = ModFileAnno("values")
+                            GoTo Got
+                        End If
+                    Next
+                End If
+            Next
+            Exit Try
+Got:
+            '从文件中获取 Mod 信息项
+            If FmlObject.ContainsKey("name") Then Display = FmlObject("name")("value")
+            If FmlObject.ContainsKey("version") Then Version = FmlObject("version")("value")
+            '加载成功
+            GoTo Finished
+        Catch ex As Exception
+            Log(ex, $"读取 fml_cache_annotation.json 时出现未知错误({File.FullName})", NotifyLevel.DevelopOnly)
+        End Try
+#End Region
+        Return '没能成功获取任何信息
+
+Finished:
+#Region "将 Version 代号转换为 META-INF 中的版本"
+        If _Version = "version" Then
+            Try
+                Dim MetaEntry As ZipArchiveEntry = Jar.GetEntry("META-INF/MANIFEST.MF")
+                If MetaEntry IsNot Nothing Then
+                    Dim MetaString As String = ReadFile(MetaEntry.Open()).Replace(" :", ":").Replace(": ", ":")
+                    If MetaString.Contains("Implementation-Version:") Then
+                        MetaString = MetaString.Substring(MetaString.IndexOfF("Implementation-Version:") + "Implementation-Version:".Count)
+                        MetaString = MetaString.Substring(0, MetaString.IndexOfAny(vbCrLf.ToCharArray)).Trim
+                        Version = MetaString
+                    End If
+                End If
+            Catch ex As Exception
+                Log($"获取 META-INF 中的版本信息失败({File.FullName})", NotifyLevel.DevelopOnly)
+                Version = Nothing
+            End Try
+        End If
+        If _Version IsNot Nothing AndAlso Not (_Version.Contains(".") OrElse _Version.Contains("-")) Then Version = Nothing
+#End Region
+        RaiseEvent OnUpdate(Me)
+
+    End Sub
+
+#End Region
+
+#Region "网络信息"
+
+    ''' <summary>
+    ''' 该 Mod 关联的网络项目。
+    ''' </summary>
+    Public Property Project As ResourceProject
+        Get
+            Return _Project
+        End Get
+        Set(value As ResourceProject)
+            _Project = value
+            RaiseEvent OnUpdate(Me)
+        End Set
+    End Property
+    Private _Project As ResourceProject
+
+    ''' <summary>
+    ''' 本地文件对应的联网文件信息。
+    ''' </summary>
+    Public ProjectVersion As ResourceVersion
+
+    ''' <summary>
+    ''' 该 Mod 对应的联网最新版本。
+    ''' </summary>
+    Public Property UpdateFile As ResourceVersion
+        Get
+            Return _UpdateFile
+        End Get
+        Set(value As ResourceVersion)
+            _UpdateFile = value
+            RaiseEvent OnUpdate(Me)
+        End Set
+    End Property
+    Private _UpdateFile As ResourceVersion
+
+    ''' <summary>
+    ''' 该 Mod 的更新日志网址。
+    ''' </summary>
+    Public ChangelogUrls As New List(Of String)
+    ''' <summary>
+    ''' 所有网络信息是否已成功加载。
+    ''' </summary>
+    Public OnlineDataLoaded As Boolean = False
+
+    ''' <summary>
+    ''' 将网络信息保存为 JSON。
+    ''' </summary>
+    Public Function ToJson() As JObject
+        Dim Json As New JObject
+        If Project IsNot Nothing Then Json.Add("Project", Project.ToJson())
+        Json.Add("ChangelogUrls", New JArray(ChangelogUrls))
+        Json.Add("OnlineDataLoaded", OnlineDataLoaded)
+        If ProjectVersion IsNot Nothing Then Json.Add("Version", ProjectVersion.ToCacheJson())
+        If UpdateFile IsNot Nothing Then Json.Add("UpdateFile", UpdateFile.ToCacheJson())
+        Return Json
+    End Function
+    ''' <summary>
+    ''' 从 JSON 中读取网络信息。
+    ''' </summary>
+    Public Sub FromJson(Json As JObject)
+        OnlineDataLoaded = Json("OnlineDataLoaded")
+        If Json.ContainsKey("Project") Then Project = New ResourceProject(Json("Project"))
+        If Json.ContainsKey("ChangelogUrls") Then ChangelogUrls = Json("ChangelogUrls").ToObject(Of List(Of String))
+        If Json.ContainsKey("Version") Then ProjectVersion = ResourceVersion.FromCacheJson(Json("Version"))
+        If Json.ContainsKey("UpdateFile") Then UpdateFile = ResourceVersion.FromCacheJson(Json("UpdateFile"))
+    End Sub
+
+    ''' <summary>
+    ''' 该文件是否可以更新。
+    ''' </summary>
+    Public ReadOnly Property HasUpdate As Boolean
+        Get
+            Return Not Settings.Get("UiHiddenFunctionModUpdate") AndAlso Not Settings.Get("VersionAdvanceDisableModUpdate", Instance:=PageInstanceLeft.Instance) AndAlso UpdateFile IsNot Nothing
+        End Get
+    End Property
+
+    ''' <summary>
+    ''' 获取用于 CurseForge 信息获取的 Hash 值(MurmurHash2)。
+    ''' </summary>
+    Public ReadOnly Property CurseForgeHash As UInteger
+        Get
+            If _CurseForgeHash Is Nothing Then
+                '读取缓存
+                Dim CacheKey As String = GetHash($"{EnabledFullName}-{File.LastWriteTime.ToLongTimeString}-{File.Length}-C")
+                Dim Cached As String = ReadIni(PathTemp & "Cache\ModHash.ini", CacheKey)
+                If Cached <> "" AndAlso RegexCheck(Cached, "^\d+$") Then '#5062
+                    _CurseForgeHash = Cached
+                    Return _CurseForgeHash
+                End If
+                '读取文件
+                Dim data As New List(Of Byte)
+                For Each b As Byte In ReadFileBytes(File.FullName)
+                    If b = 9 OrElse b = 10 OrElse b = 13 OrElse b = 32 Then Continue For
+                    data.Add(b)
+                Next
+                '计算 MurmurHash2
+                Dim length As Integer = data.Count
+                Dim h As UInteger = 1 Xor length '1 是种子
+                Dim i As Integer
+                For i = 0 To length - 4 Step 4
+                    Dim k As UInteger = data(i) Or CUInt(data(i + 1)) << 8 Or CUInt(data(i + 2)) << 16 Or CUInt(data(i + 3)) << 24
+                    k = (k * &H5BD1E995L) And &HFFFFFFFFL
+                    k = k Xor (k >> 24)
+                    k = (k * &H5BD1E995L) And &HFFFFFFFFL
+                    h = (h * &H5BD1E995L) And &HFFFFFFFFL
+                    h = h Xor k
+                Next
+                Select Case length - i
+                    Case 3
+                        h = h Xor (data(i) Or CUInt(data(i + 1)) << 8)
+                        h = h Xor (CUInt(data(i + 2)) << 16)
+                        h = (h * &H5BD1E995L) And &HFFFFFFFFL
+                    Case 2
+                        h = h Xor (data(i) Or CUInt(data(i + 1)) << 8)
+                        h = (h * &H5BD1E995L) And &HFFFFFFFFL
+                    Case 1
+                        h = h Xor data(i)
+                        h = (h * &H5BD1E995L) And &HFFFFFFFFL
+                End Select
+                h = h Xor (h >> 13)
+                h = (h * &H5BD1E995L) And &HFFFFFFFFL
+                h = h Xor (h >> 15)
+                _CurseForgeHash = h
+                '写入缓存
+                WriteIni(PathTemp & "Cache\ModHash.ini", CacheKey, h.ToString)
+            End If
+            Return _CurseForgeHash
+        End Get
+    End Property
+    Private _CurseForgeHash As UInteger?
+
+    ''' <summary>
+    ''' 获取用于 Modrinth 信息获取的 Hash 值(SHA1)。
+    ''' </summary>
+    Public ReadOnly Property ModrinthHash As String
+        Get
+            If _ModrinthHash Is Nothing Then
+                '读取缓存
+                Dim CacheKey As String = GetHash($"{EnabledFullName}-{File.LastWriteTime.ToLongTimeString}-{File.Length}-M")
+                Dim Cached As String = ReadIni(PathTemp & "Cache\ModHash.ini", CacheKey)
+                If Cached <> "" Then
+                    _ModrinthHash = Cached
+                    Return _ModrinthHash
+                End If
+                '计算 SHA1
+                _ModrinthHash = GetFileSHA1(File.FullName)
+                '写入缓存
+                WriteIni(PathTemp & "Cache\ModHash.ini", CacheKey, _ModrinthHash)
+            End If
+            Return _ModrinthHash
+        End Get
+    End Property
+    Private _ModrinthHash As String
+
+#End Region
+
+#Region "API"
+
+    Public Overrides Function ToString() As String
+        Return File.FullName
+    End Function
+    Public Overrides Function Equals(obj As Object) As Boolean
+        Dim Target = TryCast(obj, ResourceFile)
+        Return Target IsNot Nothing AndAlso File.FullName = Target.File.FullName
+    End Function
+
+#End Region
+
+End Class

+ 345 - 0
Plain Craft Launcher 2/Modules/Resource/ResourceFileLoader.vb

@@ -0,0 +1,345 @@
+Public Module ResourceFileLoader
+    Private Const ResourceFileCacheVersion As Integer = 16
+
+    '加载 Mod 列表
+    Public McModLoader As New LoaderTask(Of String, List(Of ResourceFile))("Mod List Loader", AddressOf McModLoad)
+    Private Sub McModLoad(Loader As LoaderTask(Of String, List(Of ResourceFile)))
+        Try
+            RunInUiWait(Sub() If FrmInstanceMod IsNot Nothing Then FrmInstanceMod.Load.ShowProgress = False)
+
+            '等待 Mod 更新完成
+            If PageInstanceMod.UpdatingInstanceModFolders.Contains(Loader.Input) Then
+                Log($"[Mod] 等待 Mod 更新完成后才能继续加载 Mod 列表:" & Loader.Input)
+                Try
+                    RunInUiWait(Sub() If FrmInstanceMod IsNot Nothing Then FrmInstanceMod.Load.Text = "正在更新 Mod")
+                    Do Until Not PageInstanceMod.UpdatingInstanceModFolders.Contains(Loader.Input)
+                        If Loader.IsInterrupted Then Return
+                        Thread.Sleep(100)
+                    Loop
+                Finally
+                    RunInUiWait(Sub() If FrmInstanceMod IsNot Nothing Then FrmInstanceMod.Load.Text = "正在加载 Mod 列表")
+                End Try
+                FrmInstanceMod.LoaderRun(LoaderFolderRunType.UpdateOnly)
+            End If
+
+            '获取 Mod 文件夹下的可用文件列表
+            Dim ModFileList As New List(Of FileInfo)
+            If Directory.Exists(Loader.Input) Then
+                Dim RawName As String = Loader.Input.Lower
+                For Each File As FileInfo In EnumerateFiles(Loader.Input)
+                    If File.DirectoryName.Lower & "\" <> RawName Then
+                        '仅当 Forge 1.13- 且文件夹名与版本号相同时,才加载该子文件夹下的 Mod
+                        If Not (PageInstanceLeft.Instance IsNot Nothing AndAlso PageInstanceLeft.Instance.Version.HasForge AndAlso
+                                PageInstanceLeft.Instance.Version.Vanilla.Major < 13 AndAlso
+                                File.Directory.Name = $"1.{PageInstanceLeft.Instance.Version.Vanilla.Major}.{PageInstanceLeft.Instance.Version.Vanilla.Build}") Then
+                            Continue For
+                        End If
+                    End If
+                    Static IsModFile As Func(Of String, Boolean) =
+                    Function(FullName As String)
+                        If FullName Is Nothing OrElse Not FullName.Contains(".") Then Return False
+                        FullName = FullName.Lower
+                        If FullName.EndsWithF(".jar", True) OrElse FullName.EndsWithF(".zip", True) OrElse FullName.EndsWithF(".litemod", True) OrElse
+                            FullName.EndsWithF(".jar.disabled", True) OrElse FullName.EndsWithF(".zip.disabled", True) OrElse FullName.EndsWithF(".litemod.disabled", True) OrElse
+                            FullName.EndsWithF(".jar.old", True) OrElse FullName.EndsWithF(".zip.old", True) OrElse FullName.EndsWithF(".litemod.old", True) Then Return True
+                        Return False
+                    End Function
+                    If IsModFile(File.FullName) Then ModFileList.Add(File)
+                Next
+            End If
+
+            '获取本地文件缓存
+            Dim CachePath As String = PathTemp & "Cache\LocalMod.json"
+            Dim Cache As New JObject
+            Try
+                Dim CacheContent As String = ReadFile(CachePath)
+                If Not String.IsNullOrWhiteSpace(CacheContent) Then
+                    Cache = GetJson(CacheContent)
+                    If Not Cache.ContainsKey("version") OrElse Cache("version").ToObject(Of Integer) <> ResourceFileCacheVersion Then
+                        Log($"[Mod] 本地 Mod 信息缓存版本已过期,将弃用这些缓存信息", NotifyLevel.DebugModeOnly)
+                        Cache = New JObject
+                    End If
+                End If
+            Catch ex As Exception
+                Log(ex, "读取本地 Mod 信息缓存失败,已重置")
+                Cache = New JObject
+            End Try
+            Cache("version") = ResourceFileCacheVersion
+
+            '加载 Mod 列表
+            Dim ModList As New List(Of ResourceFile)
+            For Each ModFile As FileInfo In ModFileList
+                If Loader.IsInterrupted Then Return
+                Dim ModEntry As New ResourceFile(ModFile)
+                Dim DumpMod As ResourceFile = ModList.FirstOrDefault(Function(m) m.EnabledName = ModEntry.EnabledName) '存在两个文件,名称相同,但一个启用一个禁用
+                If DumpMod IsNot Nothing Then
+                    Dim DisabledMod As ResourceFile = If(DumpMod.IsEnabled, ModEntry, DumpMod)
+                    Log($"[Mod] 重复的 Mod 文件:{DumpMod.File.Name} 与 {ModEntry.File.Name},已忽略 {DisabledMod.File.Name}", NotifyLevel.DebugModeOnly)
+                    If DisabledMod Is ModEntry Then
+                        Continue For
+                    Else
+                        ModList.Remove(DisabledMod)
+                    End If
+                End If
+                ModList.Add(ModEntry)
+            Next
+            Log($"[Mod] 共发现 {ModList.Count} 个 Mod")
+
+            '排序
+            ModList = ModList.OrderBy(Function(m) m.File.Name).ToList
+
+            '回设
+            If Loader.IsInterrupted Then Return
+            Loader.Output = ModList
+
+            '开始联网加载
+            'TODO: 添加信息获取中提示
+            McModDetailLoader.Start(New KeyValuePair(Of List(Of ResourceFile), JObject)(ModList, Cache), IsForceRestart:=True)
+
+        Catch ex As Exception
+            Log(ex, "Mod 列表加载失败", NotifyLevel.DebugModeOnly)
+            Throw
+        End Try
+    End Sub
+
+    '联网加载 Mod 详情
+    Public McModDetailLoader As New LoaderTask(Of KeyValuePair(Of List(Of ResourceFile), JObject), Integer)("Mod List Detail Loader", AddressOf McModDetailLoad)
+    Private Sub McModDetailLoad(Loader As LoaderTask(Of KeyValuePair(Of List(Of ResourceFile), JObject), Integer))
+        Dim Mods As New List(Of ResourceFile)
+        Dim Cache As JObject = Loader.Input.Value
+        Static GetTargetModLoaders As Func(Of List(Of ModLoaderTypes)) =
+        Function() As List(Of ModLoaderTypes)
+            Dim Loaders As New List(Of ModLoaderTypes)
+            If PageInstanceLeft.Instance.Version.HasForge Then Loaders.Add(ModLoaderTypes.Forge)
+            If PageInstanceLeft.Instance.Version.HasNeoForge Then Loaders.Add(ModLoaderTypes.NeoForge)
+            If PageInstanceLeft.Instance.Version.HasFabric Then Loaders.Add(ModLoaderTypes.Fabric)
+            If PageInstanceLeft.Instance.Version.HasLiteLoader Then Loaders.Add(ModLoaderTypes.LiteLoader)
+            If Not Loaders.Any() Then Loaders.AddRange({ModLoaderTypes.Forge, ModLoaderTypes.NeoForge, ModLoaderTypes.Fabric, ModLoaderTypes.LiteLoader, ModLoaderTypes.Quilt})
+            Return Loaders
+        End Function
+        '读取缓存,获取需要更新的 Mod 列表
+        For Each ModEntry As ResourceFile In Loader.Input.Key
+            If Loader.IsInterrupted Then Return
+            Dim CacheKey = ModEntry.ModrinthHash & PageInstanceLeft.Instance.Version.VanillaName & GetTargetModLoaders().Join("")
+            If Cache.ContainsKey(CacheKey) Then
+                ModEntry.FromJson(Cache(CacheKey))
+                '如果缓存中的信息在 6 小时以内更新过,则无需重新获取
+                If ModEntry.OnlineDataLoaded AndAlso Date.Now - Cache(CacheKey)("Project")("CacheTime").ToObject(Of Date) < New TimeSpan(6, 0, 0) Then Continue For
+            End If
+            Mods.Add(ModEntry)
+        Next
+        Log($"[Mod] 有 {Mods.Where(Function(m) m.Project Is Nothing).Count} 个 Mod 需要联网获取信息,{Mods.Where(Function(m) m.Project IsNot Nothing).Count} 个 Mod 需要更新信息")
+        If Not Mods.Any Then
+            Loader.Input.Key.Where(Function(m) m.Version Is Nothing).ForAll(Sub(m) m.LoadMetadataFromJar())  '从 JAR 中获取缺失的版本信息(下面有另一个分支)
+            Return
+        End If
+        '获取作为检查目标的加载器和版本
+        '此处不应向下扩展检查的 MC 小版本,例如 Mod 在更新 1.16.5 后,对早期的 1.16.2 版本发布了修补补丁,这会导致 PCL 将 1.16.5 版本的 Mod 降级到 1.16.2
+        Dim TargetMcVersion As McVersion = PageInstanceLeft.Instance.Version
+        Dim ModLoaders = GetTargetModLoaders()
+        Dim VanillaVersion = TargetMcVersion.VanillaName
+        '开始网络获取
+        Log($"[Mod] 目标加载器:{ModLoaders.Join("/")},版本:{VanillaVersion}")
+        Dim EndedThreadCount As Integer = 0, IsFailed As Boolean = False
+        Dim CurrentTaskThread As Thread = Thread.CurrentThread
+        '从 Modrinth 获取信息
+        RunInNewThread(
+        Sub()
+            Try
+                '步骤 1:获取 Hash 与对应的工程 ID
+                Dim ModrinthHashes = Mods.Select(Function(m) m.ModrinthHash).ToList()
+                Dim ModrinthVersion As JObject = DlModRequest("https://api.modrinth.com/v2/version_files", HttpMethod.Post,
+                    $"{{""hashes"": [""{ModrinthHashes.Join(""",""")}""], ""algorithm"": ""sha1""}}", "application/json")
+                Log($"[Mod] 从 Modrinth 获取到 {ModrinthVersion.Count} 个本地 Mod 的对应信息")
+                '步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系
+                If ModrinthVersion.Count = 0 Then Return
+                Dim ModrinthMapping As New Dictionary(Of String, List(Of ResourceFile))
+                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 ResourceProject.Cache.ContainsKey(ProjectId) AndAlso Entry.Project Is Nothing Then Entry.Project = ResourceProject.Cache(ProjectId) '读取已加载的缓存,加快结果出现速度
+                    If Not ModrinthMapping.ContainsKey(ProjectId) Then ModrinthMapping(ProjectId) = New List(Of ResourceFile)
+                    ModrinthMapping(ProjectId).Add(Entry)
+                    '记录对应的 ProjectVersion
+                    Dim File = ResourceVersion.FromPlatformJson(ModrinthVersion(Entry.ModrinthHash), ResourceTypes.Mod)
+                    If Entry.ProjectVersion Is Nothing OrElse Entry.ProjectVersion.ReleaseDate < File.ReleaseDate Then
+                        Entry.ProjectVersion = File
+                    Else
+                        Entry.ProjectVersion.Version = File.Version '使用来自 Modrinth 的版本号
+                    End If
+                Next
+                If Loader.IsInterruptedWithThread(CurrentTaskThread) Then Return
+                Log($"[Mod] 需要从 Modrinth 获取 {ModrinthMapping.Count} 个本地 Mod 的工程信息")
+                '步骤 3:获取工程信息
+                If Not ModrinthMapping.Any() Then Return
+                Dim ModrinthProject As JArray = DlModRequest(
+                    $"https://api.modrinth.com/v2/projects?ids=[""{ModrinthMapping.Keys.Join(""",""")}""]")
+                For Each ProjectJson In ModrinthProject
+                    Dim Project As New ResourceProject(ProjectJson)
+                    For Each Entry In ModrinthMapping(Project.Id)
+                        Entry.Project = Project
+                    Next
+                Next
+                Log($"[Mod] 已从 Modrinth 获取本地 Mod 信息,继续获取更新信息")
+                '步骤 4:获取更新信息
+                Dim ModrinthUpdate As JObject = DlModRequest("https://api.modrinth.com/v2/version_files/update", HttpMethod.Post,
+                    $"{{""hashes"": [""{ModrinthMapping.SelectMany(Function(l) l.Value.Select(Function(m) m.ModrinthHash)).Join(""",""")}""], ""algorithm"": ""sha1"", 
+                    ""loaders"": [""{ModLoaders.Join(""",""").Lower}""],""game_versions"": [""{VanillaVersion}""]}}", "application/json")
+                For Each Entry In Mods
+                    If Not ModrinthUpdate.ContainsKey(Entry.ModrinthHash) OrElse Entry.ProjectVersion Is Nothing Then Continue For
+                    Dim UpdateFile = ResourceVersion.FromPlatformJson(ModrinthUpdate(Entry.ModrinthHash), ResourceTypes.Mod)
+                    If Not UpdateFile.DownloadAvailable Then Continue For
+                    If ModeDebug Then Log($"[Mod] 本地文件 {Entry.ProjectVersion.FileName} 在 Modrinth 上的最新版为 {UpdateFile.FileName}")
+                    If Entry.ProjectVersion.ReleaseDate >= UpdateFile.ReleaseDate OrElse Entry.ProjectVersion.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?g={VanillaVersion}")
+                        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?g={VanillaVersion}"}
+                        Entry.UpdateFile = UpdateFile
+                    End If
+                Next
+                Log($"[Mod] 从 Modrinth 获取本地 Mod 信息结束")
+            Catch ex As Exception
+                Log(ex, "从 Modrinth 获取本地 Mod 信息失败")
+                IsFailed = True
+            Finally
+                EndedThreadCount += 1
+            End Try
+        End Sub, "Mod List Detail Loader Modrinth")
+        '从 CurseForge 获取信息
+        RunInNewThread(
+        Sub()
+            Try
+                '步骤 1:获取 Hash 与对应的工程 ID
+                Dim CurseForgeHashes As New List(Of UInteger)
+                For Each Entry In Mods
+                    CurseForgeHashes.Add(Entry.CurseForgeHash)
+                    If Loader.IsInterruptedWithThread(CurrentTaskThread) Then Return
+                Next
+                Dim CurseForgeRaw As JContainer = DlModRequest("https://api.curseforge.com/v1/fingerprints/432", HttpMethod.Post,
+                    $"{{""fingerprints"": [{CurseForgeHashes.Join(",")}]}}", "application/json")("data")("exactMatches")
+                Log($"[Mod] 从 CurseForge 获取到 {CurseForgeRaw.Count} 个本地 Mod 的对应信息")
+                '步骤 2:尝试读取工程信息缓存,构建其他 Mod 的对应关系
+                If Not CurseForgeRaw.Any() Then Return
+                Dim CurseForgeMapping As New Dictionary(Of Integer, List(Of ResourceFile))
+                For Each Project In CurseForgeRaw
+                    Dim ProjectId = Project("id").ToString
+                    Dim Hash As UInteger = Project("file")("fileFingerprint")
+                    For Each Entry In Mods
+                        If Entry.CurseForgeHash <> Hash Then Continue For
+                        If ResourceProject.Cache.ContainsKey(ProjectId) AndAlso Entry.Project Is Nothing Then Entry.Project = ResourceProject.Cache(ProjectId) '读取已加载的缓存,加快结果出现速度
+                        If Not CurseForgeMapping.ContainsKey(ProjectId) Then CurseForgeMapping(ProjectId) = New List(Of ResourceFile)
+                        CurseForgeMapping(ProjectId).Add(Entry)
+                        '记录对应的 ProjectVersion
+                        Dim File = ResourceVersion.FromPlatformJson(Project("file"), ResourceTypes.Mod)
+                        If Entry.ProjectVersion Is Nothing OrElse Entry.ProjectVersion.ReleaseDate < File.ReleaseDate Then Entry.ProjectVersion = File
+                    Next
+                Next
+                If Loader.IsInterruptedWithThread(CurrentTaskThread) Then Return
+                Log($"[Mod] 需要从 CurseForge 获取 {CurseForgeMapping.Count} 个本地 Mod 的工程信息")
+                '步骤 3:获取工程信息
+                If Not CurseForgeMapping.Any() Then Return
+                Dim CurseForgeProject = DlModRequest("https://api.curseforge.com/v1/mods", HttpMethod.Post,
+                    $"{{""modIds"": [{CurseForgeMapping.Keys.Join(",")}]}}", "application/json")("data")
+                Dim UpdateFileIds As New Dictionary(Of Integer, List(Of ResourceFile)) 'FileId -> 本地 Mod 文件列表
+                Dim FileIdToProjectSlug As New Dictionary(Of Integer, String)
+                For Each ProjectJson In CurseForgeProject
+                    If ProjectJson("isAvailable") IsNot Nothing AndAlso Not ProjectJson("isAvailable").ToObject(Of Boolean) Then Continue For
+                    '设置 Entry 中的工程信息
+                    Dim Project As New ResourceProject(ProjectJson)
+                    For Each Entry In CurseForgeMapping(Project.Id) '倒查防止 CurseForge 返回的内容有漏
+                        If Entry.Project IsNot Nothing AndAlso Entry.Project.Platform = ResourcePlatforms.Modrinth Then
+                            Entry.Project = Entry.Project '再次触发修改事件
+                            Continue For
+                        End If
+                        Entry.Project = Project
+                    Next
+                    '查找或许版本更新的文件列表
+                    If ModLoaders.IsSingle Then
+                        Dim NewestVersion As String = Nothing
+                        Dim NewestFileIds As New List(Of Integer)
+                        For Each IndexEntry In ProjectJson("latestFilesIndexes")
+                            If IndexEntry("modLoader") Is Nothing OrElse ModLoaders.Single <> IndexEntry("modLoader").ToObject(Of Integer) Then Continue For 'ModLoader 唯一且匹配
+                            Dim IndexVersion As String = IndexEntry("gameVersion")
+                            If IndexVersion <> VanillaVersion Then Continue For 'MC 版本匹配
+                            '由于 latestFilesIndexes 是按时间从新到老排序的,所以只需取第一个;如果需要检查多个 releaseType 下的文件,将 > -1 改为 = 1,但这应当并不会获取到更新的文件
+                            If NewestVersion IsNot Nothing AndAlso CompareVersion(NewestVersion, IndexVersion) > -1 Then Continue For '只保留最新 MC 版本
+                            If NewestVersion <> IndexVersion Then
+                                NewestVersion = IndexVersion
+                                NewestFileIds.Clear()
+                            End If
+                            NewestFileIds.Add(IndexEntry("fileId").ToObject(Of Integer))
+                        Next
+                        For Each FileId In NewestFileIds
+                            If Not UpdateFileIds.ContainsKey(FileId) Then UpdateFileIds(FileId) = New List(Of ResourceFile)
+                            UpdateFileIds(FileId).AddRange(CurseForgeMapping(Project.Id))
+                            FileIdToProjectSlug(FileId) = Project.Slug
+                        Next
+                    End If
+                Next
+                Log($"[Mod] 已从 CurseForge 获取本地 Mod 信息,需要获取 {UpdateFileIds.Count} 个用于检查更新的文件信息")
+                '步骤 4:获取更新文件信息
+                If Not UpdateFileIds.Any() Then Return
+                Dim CurseForgeFiles = DlModRequest("https://api.curseforge.com/v1/mods/files", HttpMethod.Post,
+                                    $"{{""fileIds"": [{UpdateFileIds.Keys.Join(",")}]}}", "application/json")("data")
+                Dim UpdateFiles As New Dictionary(Of ResourceFile, ResourceVersion)
+                For Each FileJson In CurseForgeFiles
+                    Dim File = ResourceVersion.FromPlatformJson(FileJson, ResourceTypes.Mod)
+                    If Not File.DownloadAvailable Then Continue For
+                    For Each Entry As ResourceFile In UpdateFileIds(File.Id)
+                        If UpdateFiles.ContainsKey(Entry) AndAlso UpdateFiles(Entry).ReleaseDate >= File.ReleaseDate Then Continue For
+                        UpdateFiles(Entry) = File
+                    Next
+                Next
+                For Each Pair In UpdateFiles
+                    Dim Entry As ResourceFile = Pair.Key
+                    Dim UpdateFile As ResourceVersion = Pair.Value
+                    If ModeDebug Then Log($"[Mod] 本地文件 {Entry.ProjectVersion.FileName} 在 CurseForge 上的最新版为 {UpdateFile.FileName}")
+                    If Entry.ProjectVersion.ReleaseDate >= UpdateFile.ReleaseDate OrElse Entry.ProjectVersion.Hash = UpdateFile.Hash Then Continue For
+                    '设置更新日志与更新文件
+                    If Entry.UpdateFile IsNot Nothing AndAlso UpdateFile.Hash = Entry.UpdateFile.Hash Then '合并
+                        Entry.ChangelogUrls.Add($"https://www.curseforge.com/minecraft/mc-mods/{FileIdToProjectSlug(UpdateFile.Id)}/files/{UpdateFile.Id}")
+                        Entry.UpdateFile.DownloadUrls.AddRange(UpdateFile.DownloadUrls) '合并下载源
+                    ElseIf Entry.UpdateFile Is Nothing OrElse UpdateFile.ReleaseDate > Entry.UpdateFile.ReleaseDate Then '替换
+                        Entry.ChangelogUrls = New List(Of String) From {$"https://www.curseforge.com/minecraft/mc-mods/{FileIdToProjectSlug(UpdateFile.Id)}/files/{UpdateFile.Id}"}
+                        Entry.UpdateFile = UpdateFile
+                    End If
+                Next
+                Log($"[Mod] 从 CurseForge 获取 Mod 更新信息结束")
+            Catch ex As Exception
+                Log(ex, "从 CurseForge 获取本地 Mod 信息失败")
+                IsFailed = True
+            Finally
+                EndedThreadCount += 1
+            End Try
+        End Sub, "Mod List Detail Loader CurseForge")
+        '从 JAR 中获取缺失的版本信息(上面有另一个分支)
+        Loader.Input.Key.Where(Function(m) m.Version Is Nothing).ForAll(Sub(m) m.LoadMetadataFromJar())
+        '等待线程结束
+        Do Until EndedThreadCount = 2
+            Thread.Sleep(10)
+            If Loader.IsInterrupted Then Return
+        Loop
+        '保存缓存
+        Mods = Mods.Where(Function(m) m.Project IsNot Nothing).ToList()
+        Log($"[Mod] 联网获取本地 Mod 信息完成,为 {Mods.Count} 个 Mod 更新缓存")
+        If Not Mods.Any() Then Return
+        For Each Entry In Mods
+            Entry.OnlineDataLoaded = Not IsFailed
+            Cache(Entry.ModrinthHash & VanillaVersion & ModLoaders.Join("")) = Entry.ToJson()
+        Next
+        FileUtils.Write(PathTemp & "Cache\LocalMod.json", Cache.ToString(If(ModeDebug, Newtonsoft.Json.Formatting.Indented, Newtonsoft.Json.Formatting.None)))
+        '刷新边栏
+        If Loader.IsInterrupted Then Return
+        If FrmInstanceMod?.Filter = PageInstanceMod.FilterType.CanUpdate Then
+            RunInUi(Sub() FrmInstanceMod?.RefreshUI()) '同步 “可更新” 列表 (#4677)
+        Else
+            RunInUi(Sub() FrmInstanceMod?.RefreshBars())
+        End If
+    End Sub
+
+End Module

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

@@ -0,0 +1,726 @@
+''' <summary>
+''' 社区资源的项目信息。
+''' </summary>
+Public Class ResourceProject
+
+    '源信息
+
+    ''' <summary>
+    ''' 该项目信息的所属平台。
+    ''' </summary>
+    Public ReadOnly Platform As ResourcePlatforms
+    ''' <summary>
+    ''' 工程中包含的文件种类。
+    ''' 若为 Modrinth 工程,可能为 Mod 或 数据包。
+    ''' </summary>
+    Public ReadOnly Types As ResourceTypes
+    ''' <summary>
+    ''' 工程的短名。例如 technical-enchant。
+    ''' </summary>
+    Public ReadOnly Slug As String
+    ''' <summary>
+    ''' CurseForge 工程的数字 ID。Modrinth 工程的乱码 ID。
+    ''' </summary>
+    Public ReadOnly Id As String
+    ''' <summary>
+    ''' CurseForge 文件列表的数字 ID。Modrinth 工程的此项无效。
+    ''' </summary>
+    Public ReadOnly CurseForgeFileIds As List(Of Integer)
+
+    '描述性信息
+
+    ''' <summary>
+    ''' 原始的英文名称。
+    ''' </summary>
+    Public ReadOnly RawName As String
+    ''' <summary>
+    ''' 英文描述。
+    ''' </summary>
+    Public ReadOnly Description As String
+    ''' <summary>
+    ''' 来源网站的工程页面网址。确保格式一定标准。
+    ''' CurseForge:https://www.curseforge.com/minecraft/mc-mods/jei
+    ''' Modrinth:https://modrinth.com/mod/technical-enchant
+    ''' </summary>
+    Public ReadOnly Website As String
+    ''' <summary>
+    ''' 最后一次更新的时间。
+    ''' </summary>
+    Public ReadOnly LastUpdate As Date
+    ''' <summary>
+    ''' 下载量计数。注意,该计数仅为一个来源,无法反应两边加起来的下载量!
+    ''' </summary>
+    Public ReadOnly DownloadCount As Integer
+    ''' <summary>
+    ''' 支持的 Mod 加载器列表。可能为空。
+    ''' </summary>
+    Public ReadOnly Loaders As List(Of ModLoaderTypes)
+    ''' <summary>
+    ''' 描述性标签的内容。已转换为中文。
+    ''' </summary>
+    Public ReadOnly Tags As List(Of String)
+    ''' <summary>
+    ''' Logo 图片的下载地址。
+    ''' 若为 Nothing 则没有,保证不为空字符串。
+    ''' </summary>
+    Public LogoUrl As String = Nothing
+    ''' <summary>
+    ''' 支持的 Drop 编号,从高到低排序,不为 Nothing。
+    ''' 例如:261(26.1.x)、180(1.18.x)。
+    ''' </summary>
+    Public ReadOnly Drops As List(Of Integer)
+    ''' <summary>
+    ''' Modrinth API 返回的原始版本列表。
+    ''' 仅用于 Modrinth 工程的二次筛选,不会被缓存,不会被预处理。
+    ''' 若非从 Modrinth 获取则为 Nothing。
+    ''' </summary>
+    Public ReadOnly UnsafeGameVersions As List(Of String)
+
+    '数据库信息
+
+    Private LoadedDatabase As Boolean = False
+    Private _WikiEntry As WikiEntry = Nothing
+    ''' <summary>
+    ''' 关联的数据库条目。若为 Nothing 则没有。
+    ''' </summary>
+    Private Property WikiEntry As WikiEntry
+        Get
+            If Not LoadedDatabase Then
+                LoadedDatabase = True
+                If Types.HasFlag(ResourceTypes.Mod) OrElse Types.HasFlag(ResourceTypes.DataPack) Then
+                    _WikiEntry = WikiEntry.All.FirstOrDefault(Function(c) If(Platform = ResourcePlatforms.CurseForge, c.CurseForgeSlug, c.ModrinthSlug) = Slug)
+                End If
+            End If
+            Return _WikiEntry
+        End Get
+        Set(value As WikiEntry)
+            LoadedDatabase = True
+            _WikiEntry = value
+        End Set
+    End Property
+    ''' <summary>
+    ''' MC 百科的页面 ID。若为 0 则没有。
+    ''' </summary>
+    Public ReadOnly Property WikiId As Integer
+        Get
+            Return If(WikiEntry Is Nothing, 0, WikiEntry.Id)
+        End Get
+    End Property
+    ''' <summary>
+    ''' 翻译后的中文名。若数据库没有则等同于 RawName。
+    ''' </summary>
+    Public ReadOnly Property TranslatedName As String
+        Get
+            Return If(WikiEntry Is Nothing OrElse WikiEntry.ChineseName = "", RawName, WikiEntry.ChineseName)
+        End Get
+    End Property
+
+    '实例化
+
+    ''' <summary>
+    ''' 从工程 Json 中初始化实例。若出错会抛出异常。
+    ''' </summary>
+    Public Sub New(Data As JObject)
+        If Data.ContainsKey("Tags") Then
+#Region "缓存 JSON"
+            Platform = Data("Platform").ToObject(Of Integer)
+            Types = Data("Types").ToObject(Of Integer)
+            Slug = Data("Slug")
+            Id = Data("Id")
+            If Data.ContainsKey("CurseForgeFileIds") Then CurseForgeFileIds = CType(Data("CurseForgeFileIds"), JArray).Select(Function(t) t.ToObject(Of Integer)).ToList
+            RawName = Data("RawName")
+            Description = Data("Description")
+            Website = Data("Website")
+            LastUpdate = Data("LastUpdate")
+            DownloadCount = Data("DownloadCount")
+            If Data.ContainsKey("ModLoaders") Then
+                Loaders = CType(Data("ModLoaders"), JArray).Select(Function(t) CType(t.ToObject(Of Integer), ModLoaderTypes)).ToList
+            Else
+                Loaders = New List(Of ModLoaderTypes)
+            End If
+            Tags = CType(Data("Tags"), JArray).Select(Function(t) t.ToString).ToList
+            If Data.ContainsKey("LogoUrl") Then LogoUrl = Data("LogoUrl")
+            If Data.ContainsKey("Drops") Then
+                Drops = CType(Data("Drops"), JArray).Select(Function(t) t.ToObject(Of Integer)).ToList
+            Else
+                Drops = New List(Of Integer)
+            End If
+#End Region
+        Else
+            Platform = If(Data.ContainsKey("summary"), ResourcePlatforms.CurseForge, ResourcePlatforms.Modrinth)
+            If Platform = ResourcePlatforms.CurseForge Then
+#Region "CurseForge"
+                '简单信息
+                Id = Data("id")
+                Slug = Data("slug")
+                RawName = Data("name")
+                Description = Data("summary")
+                Website = Data("links")("websiteUrl").ToString.TrimEnd("/")
+                LastUpdate = Data("dateReleased") '#1194
+                DownloadCount = Data("downloadCount")
+                If Data("logo").Count > 0 Then
+                    If Data("logo")("thumbnailUrl") Is Nothing OrElse Data("logo")("thumbnailUrl") = "" Then
+                        LogoUrl = Data("logo")("url")
+                    Else
+                        LogoUrl = Data("logo")("thumbnailUrl")
+                    End If
+                End If
+                If LogoUrl = "" Then LogoUrl = Nothing
+                'Type
+                If Website.Contains("/mc-mods/") OrElse Website.Contains("/mod/") Then
+                    Types = ResourceTypes.Mod
+                ElseIf Website.Contains("/modpacks/") Then
+                    Types = ResourceTypes.ModPack
+                ElseIf Website.Contains("/resourcepacks/") Then
+                    Types = ResourceTypes.ResourcePack
+                ElseIf Website.Contains("/texture-packs/") Then
+                    Types = ResourceTypes.ResourcePack
+                ElseIf Website.Contains("/shaders/") Then
+                    Types = ResourceTypes.Shader
+                Else
+                    Types = ResourceTypes.DataPack
+                End If
+                'FileIndexes / VanillaMajorVersions / ModLoaders
+                Loaders = New List(Of ModLoaderTypes)
+                Dim Files As New List(Of KeyValuePair(Of Integer, List(Of String))) 'FileId, GameVersions
+                For Each File In If(Data("latestFiles"), New JArray)
+                    Dim NewFile = ResourceVersion.FromPlatformJson(File, Types)
+                    If Not NewFile.DownloadAvailable Then Continue For
+                    Loaders.AddRange(NewFile.Loaders)
+                    Dim GameVersions = File("gameVersions").ToObject(Of List(Of String))
+                    If Not GameVersions.Any(Function(v) McVersion.IsFormatFit(v)) Then Continue For
+                    Files.Add(New KeyValuePair(Of Integer, List(Of String))(File("id"), GameVersions))
+                Next
+                For Each File In If(Data("latestFilesIndexes"), New JArray) '这俩玩意儿包含的文件不一样,见 #3599
+                    If Not McVersion.IsFormatFit(File("gameVersion")) Then Continue For
+                    Files.Add(New KeyValuePair(Of Integer, List(Of String))(File("fileId"), {File("gameVersion").ToString}.ToList))
+                Next
+                CurseForgeFileIds = Files.Select(Function(f) f.Key).Distinct.ToList
+                Drops = Files.SelectMany(Function(f) f.Value).
+                    Select(Function(v) McVersion.VersionToDrop(v)).Where(Function(v) v <> 209).Distinct.OrderByDescending(Function(v) v).ToList
+                Loaders = Loaders.Distinct.OrderBy(Of Integer)(Function(t) t).ToList
+                'Tags
+                Tags = New List(Of String)
+                For Each Category In If(Data("categories"), New JArray). '镜像源 API 可能丢失此字段 (4267#issuecomment-2254590831)
+                    Select(Of Integer)(Function(t) t("id")).Distinct.OrderByDescending(Function(c) c)
+                    Select Case Category
+                            'Mod
+                        Case 406 : Tags.Add("世界元素")
+                        Case 407 : Tags.Add("生物群系")
+                        Case 410 : Tags.Add("维度")
+                        Case 408 : Tags.Add("矿物/资源")
+                        Case 409 : Tags.Add("天然结构")
+                        Case 412 : Tags.Add("科技")
+                        Case 415 : Tags.Add("管道/物流")
+                        Case 4843 : Tags.Add("自动化")
+                        Case 417 : Tags.Add("能源")
+                        Case 4558 : Tags.Add("红石")
+                        Case 436 : Tags.Add("食物/烹饪")
+                        Case 416 : Tags.Add("农业")
+                        Case 414 : Tags.Add("运输")
+                        Case 420 : Tags.Add("仓储")
+                        Case 419 : Tags.Add("魔法")
+                        Case 422 : Tags.Add("冒险")
+                        Case 424 : Tags.Add("装饰")
+                        Case 411 : Tags.Add("生物")
+                        Case 434 : Tags.Add("装备")
+                        Case 6814 : Tags.Add("性能优化")
+                        Case 9026 : Tags.Add("创造模式")
+                        Case 423 : Tags.Add("信息显示")
+                        Case 435 : Tags.Add("服务器")
+                        Case 5191 : Tags.Add("改良")
+                        Case 421 : Tags.Add("支持库")
+                            '整合包
+                        Case 4484 : Tags.Add("多人")
+                        Case 4479 : Tags.Add("硬核")
+                        Case 4483 : Tags.Add("战斗")
+                        Case 4478 : Tags.Add("任务")
+                        Case 4472 : Tags.Add("科技")
+                        Case 4473 : Tags.Add("魔法")
+                        Case 4475 : Tags.Add("冒险")
+                        Case 4476 : Tags.Add("探索")
+                        Case 4477 : Tags.Add("小游戏")
+                        Case 4474 : Tags.Add("科幻")
+                        Case 4736 : Tags.Add("空岛")
+                        Case 5128 : Tags.Add("原版改良")
+                        Case 4487 : Tags.Add("FTB")
+                        Case 4480 : Tags.Add("基于地图")
+                        Case 4481 : Tags.Add("轻量")
+                        Case 4482 : Tags.Add("大型")
+                            '资源包
+                        Case 403 : Tags.Add("原版风")
+                        Case 400 : Tags.Add("写实风")
+                        Case 401 : Tags.Add("现代风")
+                        Case 402 : Tags.Add("中世纪")
+                        Case 399 : Tags.Add("蒸汽朋克")
+                        Case 5244 : Tags.Add("含字体")
+                        Case 404 : Tags.Add("动态效果")
+                        Case 4465 : Tags.Add("兼容 Mod")
+                        Case 393 : Tags.Add("16x")
+                        Case 394 : Tags.Add("32x")
+                        Case 395 : Tags.Add("64x")
+                        Case 396 : Tags.Add("128x")
+                        Case 397 : Tags.Add("256x")
+                        Case 398 : Tags.Add("超高清")
+                        Case 5193 : Tags.Add("数据包") '有这个 Tag 的项会从资源包请求中被移除
+                            '光影包
+                        Case 6553 : Tags.Add("写实风")
+                        Case 6554 : Tags.Add("幻想风")
+                        Case 6555 : Tags.Add("原版风")
+                            '数据包
+                        Case 6948 : Tags.Add("冒险")
+                        Case 6949 : Tags.Add("幻想")
+                        Case 6950 : Tags.Add("支持库")
+                        Case 6952 : Tags.Add("魔法")
+                        Case 6946 : Tags.Add("Mod 相关")
+                        Case 6951 : Tags.Add("科技")
+                        Case 6953 : Tags.Add("实用")
+                    End Select
+                Next
+#End Region
+            Else
+#Region "Modrinth"
+                '简单信息
+                Id = If(Data("project_id"), Data("id")) '两个 API 会返回的 key 不一样
+                Slug = Data("slug")
+                RawName = Data("title")
+                Description = Data("description")
+                LastUpdate = If(Data("date_modified"), Data("updated"))
+                DownloadCount = Data("downloads")
+                LogoUrl = Data("icon_url")
+                If LogoUrl = "" Then LogoUrl = Nothing
+                Website = $"https://modrinth.com/{Data("project_type")}/{Slug}"
+                'GameVersions
+                '搜索结果的键为 versions,获取特定工程的键为 game_versions
+                UnsafeGameVersions = If(CType(If(Data("game_versions"), Data("versions")), JArray), New JArray).Select(Function(v) v.ToString).Distinct.ToList
+                Drops = UnsafeGameVersions.Select(Function(v) McVersion.VersionToDrop(v)).Where(Function(v) v <> 209).Distinct.OrderByDescending(Function(v) v).ToList
+                'Type
+                Select Case Data("project_type").ToString
+                    Case "modpack" : Types = ResourceTypes.ModPack
+                    Case "resourcepack" : Types = ResourceTypes.ResourcePack
+                    Case "shader" : Types = ResourceTypes.Shader
+                    Case Else : Types = ResourceTypes.Mod 'Modrinth 将数据包标为 Mod,但 categories 字段里有 datapack
+                End Select
+                'Tags & ModLoaders
+                Tags = New List(Of String)
+                Loaders = New List(Of ModLoaderTypes)
+                If Data.ContainsKey("loaders") Then
+                    For Each Category In Data("loaders").Select(Function(t) t.ToString)
+                        Select Case Category
+                            Case "forge" : Loaders.Add(ModLoaderTypes.Forge)
+                            Case "fabric" : Loaders.Add(ModLoaderTypes.Fabric)
+                            Case "quilt" : Loaders.Add(ModLoaderTypes.Quilt)
+                            Case "neoforge" : Loaders.Add(ModLoaderTypes.NeoForge)
+                        End Select
+                    Next
+                End If
+                For Each Category In Data("categories").Select(Function(t) t.ToString)
+                    Select Case Category
+                            '加载器
+                        Case "forge" : Loaders.Add(ModLoaderTypes.Forge)
+                        Case "fabric" : Loaders.Add(ModLoaderTypes.Fabric)
+                        Case "quilt" : Loaders.Add(ModLoaderTypes.Quilt)
+                        Case "neoforge" : Loaders.Add(ModLoaderTypes.NeoForge)
+                        Case "datapack" : Types = ResourceTypes.DataPack
+                            '共用
+                        Case "technology" : Tags.Add("科技")
+                        Case "magic" : Tags.Add("魔法")
+                        Case "adventure" : Tags.Add("冒险")
+                        Case "utility" : Tags.Add("实用")
+                        Case "optimization" : Tags.Add("性能优化")
+                        Case "vanilla-like" : Tags.Add("原版风")
+                        Case "realistic" : Tags.Add("写实风")
+                            'Mod/数据包
+                        Case "worldgen" : Tags.Add("世界元素")
+                        Case "food" : Tags.Add("食物/烹饪")
+                        Case "game-mechanics" : Tags.Add("游戏机制")
+                        Case "transportation" : Tags.Add("运输")
+                        Case "storage" : Tags.Add("仓储")
+                        Case "decoration" : If Types <> ResourceTypes.ResourcePack Then Tags.Add("装饰")
+                        Case "mobs" : If Types <> ResourceTypes.ResourcePack Then Tags.Add("生物")
+                        Case "equipment" : If Types <> ResourceTypes.ResourcePack Then Tags.Add("装备")
+                        Case "social" : Tags.Add("服务器")
+                        Case "library" : Tags.Add("支持库")
+                            '整合包
+                        Case "multiplayer" : Tags.Add("多人")
+                        Case "challenging" : Tags.Add("硬核")
+                        Case "combat" : Tags.Add("战斗")
+                        Case "quests" : Tags.Add("任务")
+                        Case "kitchen-sink" : Tags.Add("水槽包")
+                        Case "lightweight" : Tags.Add("轻量")
+                            '资源包
+                        Case "simplistic" : Tags.Add("简洁")
+                        Case "combat" : Tags.Add("战斗")
+                        Case "tweaks" : Tags.Add("改良")
+
+                        Case "8x-" : Tags.Add("极简")
+                        Case "16x" : Tags.Add("16x")
+                        Case "32x" : Tags.Add("32x")
+                        Case "48x" : Tags.Add("48x")
+                        Case "64x" : Tags.Add("64x")
+                        Case "128x" : Tags.Add("128x")
+                        Case "256x" : Tags.Add("256x")
+                        Case "512x+" : Tags.Add("超高清")
+
+                        Case "audio" : Tags.Add("含声音")
+                        Case "fonts" : Tags.Add("含字体")
+                        Case "models" : Tags.Add("含模型")
+                        Case "gui" : Tags.Add("含 UI")
+                        Case "locale" : Tags.Add("含语言")
+                        Case "core-shaders" : Tags.Add("核心着色器")
+                        Case "modded" : Tags.Add("兼容 Mod")
+                            '光影包
+                        Case "fantasy" : Tags.Add("幻想风")
+                        Case "semi-realistic" : Tags.Add("半写实风")
+                        Case "cartoon" : Tags.Add("卡通风")
+                            '暂时不添加性能负荷 Tag
+                            'Case "potato" : Tags.Add("极低")
+                            'Case "low" : Tags.Add("低")
+                            'Case "medium" : Tags.Add("中")
+                            'Case "high" : Tags.Add("高")
+                        Case "colored-lighting" : Tags.Add("彩色光照")
+                        Case "path-tracing" : Tags.Add("路径追踪")
+                        Case "pbr" : Tags.Add("PBR")
+                        Case "reflections" : Tags.Add("反射")
+
+                        Case "iris" : Tags.Add("Iris")
+                        Case "optifine" : Tags.Add("OptiFine")
+                        Case "vanilla" : Tags.Add("原版可用")
+                    End Select
+                Next
+                If Types = ResourceTypes.DataPack AndAlso Loaders.Any Then Types = ResourceTypes.DataPack Or ResourceTypes.Mod
+#End Region
+            End If
+            If Not Tags.Any() Then Tags.Add("其他")
+            Tags.Sort()
+            Loaders.Sort()
+        End If
+        '保存缓存
+        Cache(Id) = Me
+    End Sub
+    ''' <summary>
+    ''' 从工程 ID 联网获取对应工程的信息。
+    ''' </summary>
+    Public Shared Function FromProjectId(ProjectId As String, Platform As ResourcePlatforms) As ResourceProject
+        If Not Platform.Flags.IsSingle Then Throw New ArgumentException($"必须指定单一平台,当前值为 {Platform}")
+        If Cache.ContainsKey(ProjectId) Then '存在缓存
+            Return Cache(ProjectId)
+        ElseIf Platform = ResourcePlatforms.CurseForge Then 'CurseForge
+            Return New ResourceProject(DlModRequest("https://api.curseforge.com/v1/mods/" & ProjectId)("data"))
+        Else 'Modrinth
+            Return New ResourceProject(DlModRequest("https://api.modrinth.com/v2/project/" & ProjectId))
+        End If
+    End Function
+    ''' <summary>
+    ''' 从工程 ID 列表联网获取工程信息,结果不按原顺序排列。
+    ''' </summary>
+    Public Shared Iterator Function FromProjectIds(ProjectIds As IEnumerable(Of String), Platform As ResourcePlatforms) As IEnumerable(Of ResourceProject)
+        If Not Platform.Flags.IsSingle Then Throw New ArgumentException($"必须指定单一平台,当前值为 {Platform}")
+        '获取缓存
+        Dim MissingProjectIds As New List(Of String)
+        For Each ProjectId In ProjectIds
+            If Cache.ContainsKey(ProjectId) Then
+                Yield Cache(ProjectId)
+            Else
+                MissingProjectIds.Add(ProjectId)
+            End If
+        Next
+        If Not MissingProjectIds.Any() Then Return
+        '联网获取剩余的工程对象
+        Dim Projects As JArray
+        If Platform.HasFlag(ResourcePlatforms.CurseForge) Then
+            Projects = DlModRequest("https://api.curseforge.com/v1/mods",
+                HttpMethod.Post, "{""modIds"": [" & MissingProjectIds.Join(","c) & "]}", "application/json")("data")
+        Else
+            Projects = DlModRequest($"https://api.modrinth.com/v2/projects?ids=[""{MissingProjectIds.Join(""",""")}""]")
+        End If
+        For Each Project In Projects
+            Yield New ResourceProject(Project)
+        Next
+    End Function
+
+    ''' <summary>
+    ''' 将当前实例转为可用于保存缓存的 Json。
+    ''' </summary>
+    Public Function ToJson() As JObject
+        Dim Json As New JObject
+        Json("Platform") = CInt(Platform)
+        Json("Types") = CInt(Types)
+        Json("Slug") = Slug
+        Json("Id") = Id
+        If CurseForgeFileIds IsNot Nothing Then Json("CurseForgeFileIds") = New JArray(CurseForgeFileIds)
+        Json("RawName") = RawName
+        Json("Description") = Description
+        Json("Website") = Website
+        Json("LastUpdate") = LastUpdate
+        Json("DownloadCount") = DownloadCount
+        If Loaders IsNot Nothing AndAlso Loaders.Any Then Json("ModLoaders") = New JArray(Loaders.Select(Function(m) CInt(m)))
+        Json("Tags") = New JArray(Tags)
+        If LogoUrl IsNot Nothing Then Json("LogoUrl") = LogoUrl
+        If Drops.Any Then Json("Drops") = New JArray(Drops)
+        Json("CacheTime") = Date.Now '用于检查缓存时间
+        Return Json
+    End Function
+
+    ''' <summary>
+    ''' 将当前工程信息实例化为控件。
+    ''' </summary>
+    Public Function ToResourceItem(ShowMcVersionDesc As Boolean, ShowLoaderDesc As Boolean) As MyVirtualizingElement(Of MyResourceItem)
+        '获取版本描述
+        Dim GameVersionDescription As String
+        If Drops Is Nothing OrElse Not Drops.Any() Then
+            GameVersionDescription = "仅快照版本" '#5412
+        Else
+            Dim Segments As New List(Of String)
+            Dim IsOld As Boolean = False
+            For i = 0 To Drops.Count - 1 '版本号一定为降序
+                '获取当前连续的版本号段
+                Dim StartDrop As Integer = Drops(i), EndDrop As Integer = Drops(i)
+                If StartDrop < 120 Then '如果支持新版本,则不显示 1.11-
+                    If Segments.Any() AndAlso Not IsOld Then
+                        Exit For
+                    Else
+                        IsOld = True
+                    End If
+                End If
+                For ii = i + 1 To Drops.Count - 1
+                    If AllDrops Is Nothing OrElse AllDrops.IndexOf(Drops(ii)) <> AllDrops.IndexOf(EndDrop) + 1 Then Exit For
+                    EndDrop = Drops(ii)
+                    i = ii
+                Next
+                '将版本号段转为描述文本
+                Dim StartName = McVersion.DropToVersion(StartDrop)
+                Dim EndName = McVersion.DropToVersion(EndDrop)
+                If StartDrop = EndDrop Then
+                    Segments.Add(StartName)
+                ElseIf AllDrops IsNot Nothing AndAlso StartDrop >= AllDrops.First Then
+                    If EndDrop < 120 Then
+                        Segments.Clear()
+                        Segments.Add("全版本")
+                        Exit For
+                    Else
+                        Segments.Add(EndName & "+")
+                    End If
+                ElseIf EndDrop < 120 Then
+                    Segments.Add(StartName & "-")
+                    Exit For
+                ElseIf AllDrops Is Nothing OrElse AllDrops.IndexOf(EndDrop) - AllDrops.IndexOf(StartDrop) = 1 Then
+                    Segments.Add(StartName & ", " & EndName)
+                Else
+                    Segments.Add(StartName & "~" & EndName)
+                End If
+            Next
+            GameVersionDescription = Segments.Join(", ")
+        End If
+        '获取 Mod 加载器描述
+        Dim ModLoaderDescriptionFull As String, ModLoaderDescriptionPart As String
+        Dim ModLoadersForDesc As New List(Of ModLoaderTypes)(Loaders)
+        If Settings.Get("ToolDownloadIgnoreQuilt") Then ModLoadersForDesc.Remove(ModLoaderTypes.Quilt)
+        Select Case ModLoadersForDesc.Count
+            Case 0
+                If Loaders.IsSingle Then
+                    ModLoaderDescriptionFull = "仅 " & Loaders.Single.ToString
+                    ModLoaderDescriptionPart = Loaders.Single.ToString
+                Else
+                    ModLoaderDescriptionFull = "未知"
+                    ModLoaderDescriptionPart = ""
+                End If
+            Case 1
+                ModLoaderDescriptionFull = "仅 " & ModLoadersForDesc.Single.ToString
+                ModLoaderDescriptionPart = ModLoadersForDesc.Single.ToString
+            Case Else
+                Dim NewestDrop As Integer = If(Drops.Any, Drops.First, 9999)
+                If Loaders.Contains(ModLoaderTypes.Forge) AndAlso
+                   (NewestDrop < 140 OrElse Loaders.Contains(ModLoaderTypes.Fabric)) AndAlso
+                   (NewestDrop < 200 OrElse Loaders.Contains(ModLoaderTypes.NeoForge)) AndAlso
+                   (NewestDrop < 140 OrElse Loaders.Contains(ModLoaderTypes.Quilt) OrElse Settings.Get("ToolDownloadIgnoreQuilt")) Then
+                    ModLoaderDescriptionFull = "任意"
+                    ModLoaderDescriptionPart = ""
+                Else
+                    ModLoaderDescriptionFull = ModLoadersForDesc.Join(" / ")
+                    ModLoaderDescriptionPart = ModLoadersForDesc.Join(" / ")
+                End If
+        End Select
+        '实例化 UI
+        Return New MyVirtualizingElement(Of MyResourceItem)(
+        Function()
+            Dim NewItem As New MyResourceItem With {.Tag = Me}
+            ApplyLogoToMyImage(NewItem.PathLogo)
+            Dim Titles = GetControlTitle(True)
+            NewItem.Title = Titles.Title
+            If Titles.SubTitle = "" Then
+                CType(NewItem.LabTitleRaw.Parent, StackPanel).Children.Remove(NewItem.LabTitleRaw)
+            Else
+                NewItem.SubTitle = Titles.SubTitle
+            End If
+            NewItem.Tags = Tags
+            NewItem.Description = Description.ReplaceLineEndings("")
+            '下边栏
+            If Not ShowMcVersionDesc AndAlso Not ShowLoaderDesc Then
+                '全部隐藏
+                CType(NewItem.PathVersion.Parent, Grid).Children.Remove(NewItem.PathVersion)
+                CType(NewItem.LabVersion.Parent, Grid).Children.Remove(NewItem.LabVersion)
+                NewItem.ColumnVersion1.Width = New GridLength(0)
+                NewItem.ColumnVersion2.MaxWidth = 0
+                NewItem.ColumnVersion3.Width = New GridLength(0)
+            ElseIf ShowMcVersionDesc AndAlso ShowLoaderDesc Then
+                '全部显示
+                NewItem.LabVersion.Text = If(ModLoaderDescriptionPart = "", "", ModLoaderDescriptionPart & " ") & GameVersionDescription
+            ElseIf ShowMcVersionDesc Then
+                '仅显示版本
+                NewItem.LabVersion.Text = GameVersionDescription
+            Else
+                '仅显示 Mod 加载器
+                NewItem.LabVersion.Text = ModLoaderDescriptionFull
+            End If
+            NewItem.LabSource.Text = Platform.ToString
+            NewItem.LabTime.Text = GetTimeSpanString(LastUpdate - Date.Now, True)
+            NewItem.LabDownload.Text =
+                If(DownloadCount > 100000000, Math.Round(DownloadCount / 100000000, 2) & " 亿",
+                If(DownloadCount > 100000, Math.Floor(DownloadCount / 10000) & " 万", DownloadCount))
+            Return NewItem
+        End Function) With {.Height = 64}
+    End Function
+    Public Sub ApplyLogoToMyImage(Img As MyImage)
+        If String.IsNullOrEmpty(LogoUrl) Then
+            Img.Source = PathImage & "Icons/NoIcon.png"
+        Else
+            Img.Source = LogoUrl
+            Img.FallbackSource = DlSourceModGet(LogoUrl)
+        End If
+    End Sub
+    Public Function GetControlTitle(HasModLoaderDescription As Boolean) As (Title As String, SubTitle As String)
+        '检查下列代码时可以参考 #1567 的测试例
+        Dim Title As String = RawName
+        Dim SubtitleList As List(Of String)
+        If TranslatedName = RawName Then
+            '没有中文翻译
+            '将所有名称分段
+            Dim NameLists = TranslatedName.Split({" | ", " - ", "(", ")", "[", "]", "{", "}"}, StringSplitOptions.RemoveEmptyEntries).
+                Select(Function(s) s.Trim(" /\".ToCharArray)).Where(Function(w) Not String.IsNullOrEmpty(w))
+            If NameLists.IsSingle Then GoTo NoSubtitle
+            '查找其中的缩写、Forge/Fabric 等版本标记
+            SubtitleList = New List(Of String)
+            Dim NormalNameList = New List(Of String)
+            For Each Name In NameLists
+                Dim LowerName As String = Name.Lower
+                If Name = Name.Upper AndAlso Name <> "FPS" AndAlso Name <> "HUD" Then
+                    '缩写
+                    SubtitleList.Add(Name)
+                ElseIf {"neoforge", "forge", "fabric", "quilt"}.Any(Function(l) LowerName.Contains(l)) AndAlso
+                    Not RegexCheck(LowerName.Replace("neoforge", "").Replace("forge", "").Replace("fabric", "").Replace("quilt", ""), "[a-z]+") Then '去掉关键词后没有其他字母
+                    'Forge/Fabric 等版本标记
+                    SubtitleList.Add(Name)
+                Else
+                    '其他部分
+                    NormalNameList.Add(Name)
+                End If
+            Next
+            '根据分类后的结果处理
+            If Not NormalNameList.Any() OrElse Not SubtitleList.Any() Then GoTo NoSubtitle
+            '同时包含 NormalName 和 Subtitle
+            Title = NormalNameList.Join(" - ")
+        Else
+            '有中文翻译
+            '尝试将文本分为三段:Title (EnglishName) - Suffix
+            '检查时注意 Carpet(它没有中文译名,但有 Suffix)和 “汤姆存储 - 星的奇妙优化 (Tom's balabala)”
+            Title = If(TranslatedName.Contains(" ("), TranslatedName.BeforeFirst(" ("), TranslatedName.BeforeLast(" - "))
+            Dim Suffix As String = ""
+            If TranslatedName.AfterLast(")").Contains(" - ") Then Suffix = TranslatedName.AfterLast(")").AfterLast(" - ")
+            Dim EnglishName As String = TranslatedName
+            If Suffix <> "" Then EnglishName = EnglishName.Replace(" - " & Suffix, "")
+            EnglishName = EnglishName.Replace(Title, "").Trim("("c, ")"c, " "c)
+            '中段的额外信息截取
+            SubtitleList = EnglishName.Split({" | ", " - ", "(", ")", "[", "]", "{", "}"}, StringSplitOptions.RemoveEmptyEntries).
+                    Select(Function(s) s.Trim(" /".ToCharArray)).Where(Function(w) Not String.IsNullOrEmpty(w)).ToList
+            If SubtitleList.Count > 1 AndAlso
+               Not SubtitleList.Any(Function(s) s.Lower.Contains("forge") OrElse s.Lower.Contains("fabric") OrElse s.Lower.Contains("quilt")) AndAlso '不是标注 XX 版
+               Not (SubtitleList.Count = 2 AndAlso SubtitleList.Last.Upper = SubtitleList.Last) Then '不是缩写
+                SubtitleList = New List(Of String) From {EnglishName} '使用原名
+            End If
+            '添加后缀
+            If Suffix <> "" Then SubtitleList.Add(Suffix)
+        End If
+        SubtitleList = SubtitleList.Distinct.ToList()
+        '设置标题与描述
+        Dim Subtitle As String = ""
+        If SubtitleList.Any Then
+            For Each Ex In SubtitleList
+                Dim IsModLoaderDescription As Boolean =
+                    Ex.Lower.Contains("neoforge") OrElse Ex.Lower.Contains("forge") OrElse Ex.Lower.Contains("fabric") OrElse Ex.Lower.Contains("quilt")
+                '是否显示 ModLoader 信息
+                If Not HasModLoaderDescription AndAlso IsModLoaderDescription Then Continue For
+                '去除 “Forge/Fabric” 这一无意义提示
+                If Ex.Length < 16 AndAlso Ex.Lower.Contains("fabric") AndAlso Ex.Lower.Contains("forge") Then Continue For
+                '将 “Forge” 等提示改为 “Forge 版”
+                If IsModLoaderDescription AndAlso Not Ex.Contains("版") AndAlso
+                    Ex.Lower.Replace("neoforge", "").Replace("forge", "").Replace("fabric", "").Replace("quilt", "").Length <= 3 Then
+                    Ex = Ex.Replace("Edition", "").Replace("edition", "").Trim.Capitalize & " 版"
+                End If
+                '将 “forge” 等词语的首字母大写
+                Ex = Ex.Replace("neoforge", "NeoForge").Replace("forge", "Forge").Replace("neo", "Neo").Replace("fabric", "Fabric").Replace("quilt", "Quilt")
+                Subtitle &= "  |  " & Ex.Trim
+            Next
+        Else
+NoSubtitle:
+            Subtitle = ""
+        End If
+        Return (Title, Subtitle)
+    End Function
+
+    ''' <summary>
+    ''' 工程 ID 对应的工程实例的缓存。不会保存到文件。
+    ''' </summary>
+    Protected Friend Shared Cache As New Dictionary(Of String, ResourceProject)
+
+    ''' <summary>
+    ''' 检查是否与某个 Project 是相同的工程,只是在不同的网站。
+    ''' </summary>
+    Public Function IsLike(Project As ResourceProject) As Boolean
+        If Id = Project.Id Then Return True '相同实例
+        '提取字符串中的字母和数字
+        Dim GetRaw =
+        Function(Data As String) As String
+            Dim Result As New StringBuilder()
+            For Each r As Char In Data.Where(Function(c) Char.IsLetterOrDigit(c))
+                Result.Append(r)
+            Next
+            Return Result.ToString.Lower
+        End Function
+        '来自不同的网站
+        If Platform = Project.Platform Then Return False
+        'Mod 加载器一致
+        If Loaders.Count <> Project.Loaders.Count OrElse Loaders.Except(Project.Loaders).Any() Then Return False
+        '若不为光影,则要求 MC 版本一致
+        If Types <> ResourceTypes.Shader AndAlso (Drops.Count <> Project.Drops.Count OrElse Drops.Except(Project.Drops).Any()) Then Return False
+        '最近更新时间差距在一周以内
+        If Math.Abs((LastUpdate - Project.LastUpdate).TotalDays) > 7 Then Return False
+        'MCMOD 翻译名 / 原名 / 描述文本 / Slug 的英文部分相同
+        If TranslatedName = Project.TranslatedName OrElse
+           RawName = Project.RawName OrElse Description = Project.Description OrElse
+           GetRaw(Slug) = GetRaw(Project.Slug) Then
+            Log($"[Resource] 将 {RawName} ({Slug}) 与 {Project.RawName} ({Project.Slug}) 认定为相似工程")
+            '如果只有一个有 DatabaseEntry,设置给另外一个
+            If WikiEntry Is Nothing AndAlso Project.WikiEntry IsNot Nothing Then WikiEntry = Project.WikiEntry
+            If WikiEntry IsNot Nothing AndAlso Project.WikiEntry Is Nothing Then Project.WikiEntry = WikiEntry
+            Return True
+        End If
+        Return False
+    End Function
+
+    '辅助函数
+    Public Overrides Function ToString() As String
+        Return $"{Id} ({Slug}): {RawName}"
+    End Function
+    Public Overrides Function Equals(obj As Object) As Boolean
+        Dim project = TryCast(obj, ResourceProject)
+        Return project IsNot Nothing AndAlso Id = project.Id
+    End Function
+    Public Shared Operator =(left As ResourceProject, right As ResourceProject) As Boolean
+        Return EqualityComparer(Of ResourceProject).Default.Equals(left, right)
+    End Operator
+    Public Shared Operator <>(left As ResourceProject, right As ResourceProject) As Boolean
+        Return Not left = right
+    End Operator
+
+End Class

+ 515 - 0
Plain Craft Launcher 2/Modules/Resource/ResourceSearcher.vb

@@ -0,0 +1,515 @@
+Public Class ResourceSearcher
+    Private Const RESULT_PAGE_SIZE = 40
+    Private Const MSG_NO_CHINESE_RESULT As String = "无搜索结果,请尝试搜索其英文名称"
+
+    '输入
+    Public Class SearchRequest
+
+        '结果要求
+
+        ''' <summary>
+        ''' 结果存储器,会从中获取需加载的页面等。
+        ''' 加载后也应输出到此处。
+        ''' </summary>
+        Public Storage As SearchResult
+        ''' <summary>
+        ''' 应当尽量达成的结果数量。
+        ''' </summary>
+        Public TargetResultCount As Integer
+        ''' <summary>
+        ''' 根据加载位置记录,是否还可以继续获取内容。
+        ''' </summary>
+        Public ReadOnly Property CanContinue As Boolean
+            Get
+                If Tag.StartsWithF("/") OrElse Not Sources.HasFlag(ResourcePlatforms.CurseForge) Then Storage.CurseForgeTotal = 0
+                If Tag.EndsWithF("/") OrElse Not Sources.HasFlag(ResourcePlatforms.Modrinth) Then Storage.ModrinthTotal = 0
+                If Storage.CurseForgeTotal = -1 OrElse Storage.ModrinthTotal = -1 Then Return True
+                Return Storage.CurseForgeOffset < Storage.CurseForgeTotal OrElse Storage.ModrinthOffset < Storage.ModrinthTotal
+            End Get
+        End Property
+
+        '输入内容
+
+        ''' <summary>
+        ''' 筛选资源种类。
+        ''' </summary>
+        Public Type As ResourceTypes
+        ''' <summary>
+        ''' 筛选资源标签。空字符串代表不限制。格式例如 "406/worldgen",分别是 CurseForge 和 Modrinth 的 ID。
+        ''' </summary>
+        Public Tag As String = ""
+        ''' <summary>
+        ''' 筛选 Mod 加载器类别。
+        ''' </summary>
+        Public ModLoader As ModLoaderTypes = ModLoaderTypes.Any
+        ''' <summary>
+        ''' 筛选 MC 版本。
+        ''' </summary>
+        Public GameVersion As String = Nothing
+        ''' <summary>
+        ''' 搜索的初始内容。
+        ''' </summary>
+        Public SearchText As String = Nothing
+        ''' <summary>
+        ''' 允许的来源。
+        ''' </summary>
+        Public Sources As ResourcePlatforms = ResourcePlatforms.Any
+        Public Sub New(Type As ResourceTypes, Storage As SearchResult, TargetResultCount As Integer)
+            Me.Type = Type
+            Me.Storage = Storage
+            Me.TargetResultCount = TargetResultCount
+        End Sub
+
+        '构造请求
+
+        ''' <summary>
+        ''' 获取对应的 CurseForge API 请求链接。
+        ''' </summary>
+        Public Function GetCurseForgeAddress(SearchText As String, IgnoreModLoaderFilter As Boolean) As String
+            If Tag.StartsWithF("/") Then Storage.CurseForgeTotal = 0
+            '应用筛选参数
+            Dim Address As String = $"https://api.curseforge.com/v1/mods/search?gameId=432&sortField=2&sortOrder=desc&pageSize={RESULT_PAGE_SIZE}"
+            Select Case Type
+                Case ResourceTypes.Mod
+                    Address += "&classId=6"
+                Case ResourceTypes.ModPack
+                    Address += "&classId=4471"
+                Case ResourceTypes.DataPack
+                    Address += "&classId=6945"
+                Case ResourceTypes.Shader
+                    Address += "&classId=6552"
+                Case ResourceTypes.ResourcePack
+                    Address += "&classId=12"
+            End Select
+            If Tag <> "" Then Address += "&categoryId=" & Tag.BeforeFirst("/")
+            If ModLoader <> ModLoaderTypes.Any AndAlso Not IgnoreModLoaderFilter Then Address += "&modLoaderType=" & CType(ModLoader, Integer)
+            If Not String.IsNullOrEmpty(GameVersion) Then Address += "&gameVersion=" & GameVersion
+            If Not String.IsNullOrEmpty(SearchText) Then Address += "&searchFilter=" & EscapeUtils.UrlEscape(SearchText)
+            If Storage.CurseForgeOffset > 0 Then Address += "&index=" & Storage.CurseForgeOffset
+            Return Address
+        End Function
+        ''' <summary>
+        ''' 获取对应的 Modrinth API 请求链接。
+        ''' </summary>
+        Public Function GetModrinthAddress(SearchText As String, IgnoreModLoaderFilter As Boolean) As String
+            If Tag.EndsWithF("/") Then Storage.ModrinthTotal = 0
+            '应用筛选参数
+            Dim Address As String = $"https://api.modrinth.com/v2/search?limit={RESULT_PAGE_SIZE}&index=relevance"
+            If Not String.IsNullOrEmpty(SearchText) Then Address += "&query=" & EscapeUtils.UrlEscape(SearchText)
+            If Storage.ModrinthOffset > 0 Then Address += "&offset=" & Storage.ModrinthOffset
+            'facets=[["categories:'game-mechanics'"],["categories:'forge'"],["versions:1.19.3"],["project_type:mod"]]
+            Dim Facets As New List(Of String)
+            Facets.Add($"[""project_type:{Type.ToString.Lower}""]")
+            If Not String.IsNullOrEmpty(Tag) Then Facets.Add($"[""categories:'{Tag.AfterLast("/")}'""]")
+            If ModLoader <> ModLoaderTypes.Any AndAlso Not IgnoreModLoaderFilter Then Facets.Add($"[""categories:'{GetStringFromEnum(ModLoader).Lower}'""]")
+            If Not String.IsNullOrEmpty(GameVersion) Then Facets.Add($"[""versions:'{GameVersion}'""]")
+            Address += "&facets=[" & String.Join(",", Facets) & "]"
+            Return Address
+        End Function
+
+        '相同判断
+        Public Overrides Function Equals(obj As Object) As Boolean
+            Dim request = TryCast(obj, SearchRequest)
+            Return request IsNot Nothing AndAlso
+                Type = request.Type AndAlso TargetResultCount = request.TargetResultCount AndAlso
+                Tag = request.Tag AndAlso ModLoader = request.ModLoader AndAlso Sources = request.Sources AndAlso
+                GameVersion = request.GameVersion AndAlso SearchText = request.SearchText
+        End Function
+        Public Shared Operator =(left As SearchRequest, right As SearchRequest) As Boolean
+            Return EqualityComparer(Of SearchRequest).Default.Equals(left, right)
+        End Operator
+        Public Shared Operator <>(left As SearchRequest, right As SearchRequest) As Boolean
+            Return Not left = right
+        End Operator
+        Public Overrides Function GetHashCode() As Integer
+            Return (Type, Tag, ModLoader, GameVersion, SearchText, Sources).GetHashCode()
+        End Function
+
+    End Class
+
+    '输出
+    Public Class SearchResult
+
+        '加载位置记录
+
+        Public CurseForgeOffset As Integer = 0
+        Public CurseForgeTotal As Integer = -1
+
+        Public ModrinthOffset As Integer = 0
+        Public ModrinthTotal As Integer = -1
+
+        '结果列表
+
+        ''' <summary>
+        ''' 可供展示的所有工程的列表。
+        ''' </summary>
+        Public Results As New List(Of ResourceProject)
+        ''' <summary>
+        ''' 当前的错误信息。如果没有则为 Nothing。
+        ''' </summary>
+        Public ErrorMessage As String = Nothing
+
+    End Class
+
+    ''' <summary>
+    ''' 根据搜索请求获取一系列的工程列表。
+    ''' </summary>
+    Public Shared Function GetLoader(InputDelegate As Func(Of SearchRequest)) As LoaderTask(Of SearchRequest, Integer)
+        Return New LoaderTask(Of SearchRequest, Integer)("社区资源获取", AddressOf Run, InputDelegate) With {.ReloadTimeout = 60 * 1000}
+    End Function
+    Private Shared Sub Run(Task As LoaderTask(Of SearchRequest, Integer))
+        Dim Request As SearchRequest = Task.Input
+        Dim Storage = Request.Storage '避免其他线程对 Request.Storage 重新进行了赋值
+
+#Region "前置检查"
+
+        If Storage.Results.Count >= Request.TargetResultCount Then
+            Log($"[Resource] 已有 {Storage.Results.Count} 个结果,多于所需的 {Request.TargetResultCount} 个结果,结束处理")
+            Return
+        ElseIf Not Request.CanContinue Then
+            If Not Storage.Results.Any() Then
+                Throw New Exception("没有符合条件的结果")
+            Else
+                Log($"[Resource] 已有 {Storage.Results.Count} 个结果,少于所需的 {Request.TargetResultCount} 个结果,但无法继续获取,结束处理")
+                Return
+            End If
+        End If
+
+        '拒绝 1.13- Quilt(这个版本根本没有 Quilt)
+        If Request.ModLoader = ModLoaderTypes.Quilt AndAlso CompareVersion(If(Request.GameVersion, "1.15"), "1.14") = -1 Then
+            Throw New Exception("Quilt 不支持 Minecraft " & Request.GameVersion)
+        End If
+
+#End Region
+
+#Region "中文搜索"
+
+        Dim RawSearchText As String = If(Request.SearchText, "").Trim
+        RawSearchText = RawSearchText.Lower
+        Log("[Resource] 工程列表搜索原始文本:" & RawSearchText)
+
+        Dim IsChineseSearch As Boolean =
+            RegexCheck(RawSearchText, "[\u4e00-\u9fbb]") AndAlso Not String.IsNullOrEmpty(RawSearchText) AndAlso
+            (Request.Type = ResourceTypes.Mod OrElse Request.Type = ResourceTypes.DataPack) '目前仅对 Mod 和数据包进行中文搜索,注意整合包的名称可能已经有中文了
+        Dim CurseForgeAltSearchText As String = Nothing, ModrinthAltSearchText As String = Nothing, ModrinthSlugs As New List(Of String) '从中文转为英文的替代搜索内容
+        If IsChineseSearch Then
+            '帮助方法:从搜索项提取可能的英文单词
+            Dim ExtractWords =
+            Function(Result As SearchEntry(Of WikiEntry), Source As ResourcePlatforms) As IEnumerable(Of String)
+                '从各个可能的来源提取候选
+                Dim Candidates As New List(Of String)
+                If Result.Item.CurseForgeSlug IsNot Nothing AndAlso Source = ResourcePlatforms.CurseForge Then
+                    Candidates.Add(Result.Item.CurseForgeSlug.Replace("-", " ").Replace("/", " "))
+                End If
+                If Result.Item.ModrinthSlug IsNot Nothing AndAlso Source = ResourcePlatforms.Modrinth Then
+                    Candidates.Add(Result.Item.ModrinthSlug.Replace("-", " ").Replace("/", " "))
+                End If
+                Candidates.Add(Result.Item.ChineseName.AfterLast(" (").TrimEnd(") ").BeforeFirst(" - ").
+                                    Replace("-", " ").Replace("/", " ").Replace(":", " ").Replace("(", " ").Replace(")", ""))
+                '分词、清洗、去重
+                Candidates = Candidates.
+                    SelectMany(Function(c) c.Split(" ")).
+                    Select(Function(w) w.TrimStart("{[(").TrimEnd("}])").Lower).
+                    Where(
+                    Function(w)
+                        If w.Length <= 1 Then Return False '单字或空白
+                        If {"the", "of", "mod", "and", "forge", "fabric", "for", "quilt", "neoforge"}.Contains(w) Then Return False '常见词
+                        If Val(w) > 0 Then Return False '数字
+                        Return True
+                    End Function).Distinct.ToList
+                '如果一个词可以由其他词拼成,则去掉这个词(例如将 ender io enderio 的 enderio 剔除,只保留 ender io)
+                Dim CanForm As Func(Of String, Boolean) = Nothing
+                CanForm = Function(s) Candidates.Contains(s) OrElse Candidates.Any(Function(c) s.StartsWith(c) AndAlso CanForm(s.Substring(c.Length)))
+                Candidates = Candidates.Where(Function(w) Not Candidates.Any(Function(c) c.Length < w.Length AndAlso w.StartsWith(c) AndAlso CanForm(w.Substring(c.Length)))).ToList()
+                Return Candidates
+            End Function
+            'CurseForge
+            If Request.Sources.HasFlag(ResourcePlatforms.CurseForge) Then
+                '数据库搜索
+                Static CurseForgeSearchEntries As List(Of SearchEntry(Of WikiEntry)) =
+                WikiEntry.All.Where(Function(Entry) Entry.CurseForgeSlug IsNot Nothing).Select(
+                Function(Entry) New SearchEntry(Of WikiEntry) With {
+                    .Item = Entry,
+                    .SearchSource = New List(Of SearchSource) From {
+                        New SearchSource(Entry.ChineseName.BeforeFirst(" (").Split({"/"c}, StringSplitOptions.RemoveEmptyEntries), 1), '部分 Mod 有别名
+                        New SearchSource(Entry.ChineseName.AfterFirst(" (") & Entry.CurseForgeSlug, 0.5)}
+                }).ToList
+                Dim CurseForgeSearchResults = Search(CurseForgeSearchEntries, RawSearchText, 100, 0.25)
+                If CurseForgeSearchResults.Any Then
+                    '选取目标(CurseForge 要求每个词都必须匹配上,所以只能选择一个 Mod 进行搜索)
+                    Dim CurseForgeTarget =
+                        If(CurseForgeSearchResults.First.AbsoluteRight,
+                            CurseForgeSearchResults.Where(Function(s) s.AbsoluteRight).ToList, '优先使用所有完全匹配的
+                            CurseForgeSearchResults.MaxByAll(Function(s) s.Similarity)). '其次使用所有相似度最高的
+                        MaxBy(Function(s) s.Item.Popularity) '然后从中选择最受欢迎的那一个
+                    '后处理
+                    CurseForgeAltSearchText = ExtractWords(CurseForgeTarget, ResourcePlatforms.CurseForge).Join(" ")
+                    Log("[Resource] 中文搜索关键词(CurseForge):" & CurseForgeAltSearchText, NotifyLevel.DebugModeOnly)
+                End If
+            End If
+            'Modrinth
+            If Request.Sources.HasFlag(ResourcePlatforms.Modrinth) Then
+                '数据库搜索
+                Static ModrinthSearchEntries As List(Of SearchEntry(Of WikiEntry)) =
+                WikiEntry.All.Where(Function(Entry) Entry.ModrinthSlug IsNot Nothing).Select(
+                Function(Entry) New SearchEntry(Of WikiEntry) With {
+                    .Item = Entry,
+                    .SearchSource = New List(Of SearchSource) From {
+                        New SearchSource(Entry.ChineseName.BeforeFirst(" (").Split({"/"c}, StringSplitOptions.RemoveEmptyEntries), 1), '部分 Mod 有别名
+                        New SearchSource(Entry.ChineseName.AfterFirst(" (") & Entry.ModrinthSlug, 0.5)}
+                }).ToList
+                Dim ModrinthSearchResults = Search(ModrinthSearchEntries, RawSearchText, 100, 0.25)
+                If ModrinthSearchResults.Any Then
+                    '分词
+                    Dim WordWeights As New Dictionary(Of String, Double) '各个单词及其出现的权重
+                    For Each Result In ModrinthSearchResults
+                        For Each Word In ExtractWords(Result, ResourcePlatforms.Modrinth)
+                            If Not WordWeights.ContainsKey(Word) Then WordWeights.Add(Word, 0)
+                            Dim Similarity = If(Result.SearchSource.Any(Function(s) s.Aliases.Contains(RawSearchText)), 1000, Result.Similarity) '完全匹配为 1000
+                            WordWeights(Word) += Similarity * Result.Item.Popularity '权重 += 相似度 * 受欢迎程度
+                        Next
+                    Next
+                    ModrinthAltSearchText = WordWeights.MaxBy(Function(w) w.Value).Key
+                    Log("[Resource] 中文搜索关键词(Modrinth):" & ModrinthAltSearchText, NotifyLevel.DebugModeOnly)
+                    '直接请求工程
+                    ModrinthSlugs = ModrinthSearchResults.Take(100).Select(Function(r) r.Item.ModrinthSlug).ToList
+                End If
+            End If
+            '结束
+            If String.IsNullOrEmpty(CurseForgeAltSearchText) AndAlso String.IsNullOrEmpty(ModrinthAltSearchText) AndAlso Not ModrinthSlugs.Any Then
+                Throw New Exception(MSG_NO_CHINESE_RESULT)
+            End If
+        End If
+        Task.Progress = 0.05
+
+#End Region
+
+        Dim RealResults As New List(Of ResourceProject)
+NextPage:
+        Dim RawResults As New ConcurrentList(Of ResourceProject)
+
+#Region "从 CurseForge 和 Modrinth 获取结果列表,存储于 RawResults"
+
+        Dim WorkThreads As New List(Of Thread)
+        Dim Errors As New ConcurrentList(Of (Ex As Exception, Source As ResourcePlatforms))
+
+        '在 1.14-,部分老 Mod 没有设置支持的加载器,因此添加 Forge 筛选就会出现遗漏
+        '所以,在发起请求时不筛选加载器,然后在返回的结果中自行筛除不是 Forge 的 Mod
+        Dim IgnoreModLoaderFilter = Request.ModLoader = ModLoaderTypes.Forge AndAlso McVersion.VersionToDrop(Request.GameVersion) < 140
+        Try
+
+            'CurseForge 搜索
+            If Request.Sources.HasFlag(ResourcePlatforms.CurseForge) AndAlso
+               Not (Storage.CurseForgeTotal > -1 AndAlso Storage.CurseForgeTotal <= Storage.CurseForgeOffset) AndAlso '剩余的未显示的搜索结果不足
+               (Not IsChineseSearch OrElse (IsChineseSearch AndAlso Not String.IsNullOrEmpty(CurseForgeAltSearchText))) Then '如果是中文搜索,就只在有对应搜索关键词的时候才继续
+                WorkThreads.Add(RunInNewThread(
+                Sub()
+                    Try
+                        '获取工程列表
+                        Dim CurseForgeUrl As String = Request.GetCurseForgeAddress(If(CurseForgeAltSearchText, RawSearchText), IgnoreModLoaderFilter)
+                        Log("[Resource] 开始 CurseForge 搜索:" & CurseForgeUrl)
+                        Dim RequestResult As JObject = DlModRequest(CurseForgeUrl)
+                        Dim ProjectList As New List(Of ResourceProject)
+                        For Each JsonEntry As JObject In RequestResult("data")
+                            Dim Project As New ResourceProject(JsonEntry)
+                            If Request.Type = ResourceTypes.ResourcePack AndAlso Project.Tags.Contains("数据包") Then Continue For 'CurseForge 将一些数据包分类成了资源包
+                            ProjectList.Add(Project)
+                        Next
+                        '更新结果
+                        ProjectList.ForEach(Sub(p) RawResults.Add(p))
+                        Storage.CurseForgeOffset += RequestResult("data").Count
+                        Storage.CurseForgeTotal = RequestResult("pagination")("totalCount").ToObject(Of Integer)
+                        Log($"[Resource] 从 CurseForge 搜索到了 {ProjectList.Count} 个工程(总计已获取 {Storage.CurseForgeOffset} 个,共 {Storage.CurseForgeTotal} 个)")
+                    Catch ex As Exception
+                        Log(ex, "CurseForge 搜索失败")
+                        Storage.CurseForgeTotal = -1
+                        Errors.Add((ex, ResourcePlatforms.CurseForge))
+                    End Try
+                    If Task.Progress < 0.75 Then Task.Progress += 0.25 '可能重复加载多页,所以不能直接给够
+                End Sub, "CurseForge Search"))
+            End If
+
+            'Modrinth 搜索
+            If Request.Sources.HasFlag(ResourcePlatforms.Modrinth) AndAlso
+               Not (Storage.ModrinthTotal > -1 AndAlso Storage.ModrinthTotal <= Storage.ModrinthOffset) AndAlso '剩余的未显示的搜索结果不足
+               (Not IsChineseSearch OrElse (IsChineseSearch AndAlso Not String.IsNullOrEmpty(ModrinthAltSearchText))) Then '如果是中文搜索,就只在有对应搜索关键词的时候才继续
+                WorkThreads.Add(RunInNewThread(
+                Sub()
+                    Try
+                        Dim ModrinthUrl As String = Request.GetModrinthAddress(If(ModrinthAltSearchText, RawSearchText), IgnoreModLoaderFilter)
+                        Log("[Resource] 开始 Modrinth 搜索:" & ModrinthUrl)
+                        Dim RequestResult As JObject = DlModRequest(ModrinthUrl)
+                        Dim ProjectList As New List(Of ResourceProject)
+                        For Each JsonEntry As JObject In RequestResult("hits")
+                            ProjectList.Add(New ResourceProject(JsonEntry))
+                        Next
+                        '更新结果
+                        ProjectList.ForEach(Sub(p) RawResults.Add(p))
+                        Storage.ModrinthOffset += RequestResult("hits").Count
+                        Storage.ModrinthTotal = RequestResult("total_hits").ToObject(Of Integer)
+                        Log($"[Resource] 从 Modrinth 搜索到了 {ProjectList.Count} 个工程(总计已获取 {Storage.ModrinthOffset} 个,共 {Storage.ModrinthTotal} 个)")
+                    Catch ex As Exception
+                        Log(ex, "Modrinth 搜索失败")
+                        Storage.ModrinthTotal = -1
+                        Errors.Add((ex, ResourcePlatforms.Modrinth))
+                    End Try
+                    If Task.Progress < 0.75 Then Task.Progress += 0.25 '可能重复加载多页,所以不能直接给够
+                End Sub, "Modrinth Search"))
+            End If
+
+            'Modrinth 直接获取工程
+            If Request.Sources.HasFlag(ResourcePlatforms.Modrinth) AndAlso
+               Not (Storage.ModrinthTotal > -1 AndAlso Storage.ModrinthTotal <= Storage.ModrinthOffset) AndAlso '剩余的未显示的搜索结果不足
+               ModrinthSlugs.Any Then '有直接获取的 Slug
+                WorkThreads.Add(RunInNewThread(
+                Sub()
+                    Try
+                        Dim ModrinthUrl As String = $"https://api.modrinth.com/v2/projects?ids=[""{ModrinthSlugs.Join(""",""")}""]"
+                        Log("[Resource] 开始 Modrinth 直接获取:" & ModrinthUrl)
+                        Dim ProjectList As New List(Of ResourceProject)
+                        For Each JsonEntry As JObject In DlModRequest(ModrinthUrl)
+                            Dim Project As New ResourceProject(JsonEntry)
+                            '应用筛选
+                            If Request.Type <> ResourceTypes.Any AndAlso Not Project.Types.HasFlag(Request.Type) Then Continue For
+                            If Not String.IsNullOrEmpty(Request.Tag) AndAlso
+                                Not JsonEntry("categories").Any(Function(c) c.ToString = Request.Tag.AfterLast("/")) Then Continue For 'Project.Tags 已经转换成中文了,只能从 json 判
+                            If Request.ModLoader <> ModLoaderTypes.Any AndAlso Not IgnoreModLoaderFilter AndAlso
+                                Not Project.Loaders.Any(Function(m) m = Request.ModLoader) Then Continue For
+                            If Not String.IsNullOrEmpty(Request.GameVersion) AndAlso
+                                Not Project.UnsafeGameVersions.Any(Function(d) d = Request.GameVersion) Then Continue For
+                            ProjectList.Add(Project)
+                        Next
+                        '更新结果
+                        ProjectList.ForEach(Sub(p) RawResults.Add(p))
+                        Log($"[Resource] 从 Modrinth 直接获取到了 {ProjectList.Count} 个工程")
+                        ModrinthSlugs.Clear() '防止重试/加载下一页时重复获取
+                    Catch ex As Exception
+                        Log(ex, "Modrinth 直接获取失败")
+                        Errors.Add((ex, ResourcePlatforms.Modrinth))
+                    End Try
+                    If Task.Progress < 0.75 Then Task.Progress += 0.25 '可能重复加载多页,所以不能直接给够
+                End Sub, "Modrinth Get"))
+            End If
+
+            '等待线程结束
+            For Each Thread In WorkThreads
+                Thread.Join()
+                If Task.IsInterrupted Then Return '会自动触发 Finally 以清理线程
+            Next
+
+            '筛除不是 Forge 的 Mod
+            If IgnoreModLoaderFilter Then
+                RawResults = RawResults.Where(Function(p) Not p.Loaders.Any() OrElse p.Loaders.Contains(ModLoaderTypes.Forge)).ToList
+            End If
+
+            '确保存在结果
+            Storage.ErrorMessage = Nothing
+            If Not RawResults.Any() Then
+                If Errors.Any() Then
+                    Throw Errors.First.Ex
+                Else
+                    If IsChineseSearch AndAlso Not (Request.Type = ResourceTypes.Mod OrElse Request.Type = ResourceTypes.DataPack) Then
+                        Throw New Exception(MSG_NO_CHINESE_RESULT)
+                    ElseIf Request.Sources = ResourcePlatforms.CurseForge AndAlso Request.Tag.StartsWithF("/") Then
+                        Throw New Exception("CurseForge 不兼容所选的类型")
+                    ElseIf Request.Sources = ResourcePlatforms.Modrinth AndAlso Request.Tag.EndsWithF("/") Then
+                        Throw New Exception("Modrinth 不兼容所选的类型")
+                    Else
+                        Throw New Exception("没有搜索结果")
+                    End If
+                End If
+            ElseIf Errors.Any() Then
+                '有结果但是有错误
+                If Errors.Any(Function(e) e.Source = ResourcePlatforms.CurseForge) Then
+                    Storage.ErrorMessage = $"无法连接到 CurseForge,所以目前仅显示了来自 Modrinth 的内容,搜索结果可能不全。{vbCrLf}请稍后再试,或使用 VPN 改善网络环境。"
+                Else
+                    Storage.ErrorMessage = $"无法连接到 Modrinth,所以目前仅显示了来自 CurseForge 的内容,搜索结果可能不全。{vbCrLf}请稍后再试,或使用 VPN 改善网络环境。"
+                End If
+            End If
+
+        Finally
+            For Each Thread In WorkThreads
+                If Thread.IsAlive Then Thread.Interrupt()
+            Next
+        End Try
+
+#End Region
+
+#Region "提取非重复项,存储于 RealResults"
+
+        '将 CurseForge 排在 Modrinth 的前面,避免加载结束顺序不同导致排名不同
+        RawResults = RawResults.Where(Function(x) x.Platform = ResourcePlatforms.CurseForge).Concat(RawResults.Where(Function(x) x.Platform <> ResourcePlatforms.CurseForge)).ToList
+        '去重
+        RawResults = RawResults.Distinct(Function(a, b) a.IsLike(b)).ToList
+        '已有内容去重
+        RawResults = RawResults.Where(Function(r) Not RealResults.Any(Function(b) r.IsLike(b)) AndAlso
+                                                  Not Storage.Results.Any(Function(b) r.IsLike(b))).ToList
+        '加入列表
+        RealResults.AddRange(RawResults)
+        Log($"[Resource] 去重、筛选后累计新增结果 {RealResults.Count} 个(目前已有结果 {Storage.Results.Count} 个)")
+
+#End Region
+
+#Region "检查结果数量,如果不足且可继续,会继续加载下一页"
+
+        If RealResults.Count + Storage.Results.Count < Request.TargetResultCount Then
+            Log($"[Resource] 总结果数需求最少 {Request.TargetResultCount} 个,仅获得了 {RealResults.Count + Storage.Results.Count} 个")
+            If Request.CanContinue AndAlso Not Errors.Any Then '如果有某个源失败则不再重试,这时候重试可能导致无限循环
+                Log("[Resource] 将继续尝试加载下一页")
+                GoTo NextPage
+            Else
+                Log("[Resource] 无法继续加载,将强制结束")
+            End If
+        End If
+
+#End Region
+
+#Region "将结果排序并添加"
+
+        Dim Scores As New Dictionary(Of ResourceProject, Double) '排序分
+        Dim GetDownloadCountMult =
+        Function(Project As ResourceProject) As Double
+            Select Case Request.Type
+                Case ResourceTypes.Mod, ResourceTypes.ModPack
+                    Return If(Project.Platform = ResourcePlatforms.CurseForge, 1, 5)
+                Case ResourceTypes.DataPack
+                    Return If(Project.Platform = ResourcePlatforms.CurseForge, 10, 1)
+                Case ResourceTypes.ResourcePack, ResourceTypes.Shader
+                    Return If(Project.Platform = ResourcePlatforms.CurseForge, 1, 4)
+                Case Else
+                    Return 1
+            End Select
+        End Function
+        If String.IsNullOrEmpty(RawSearchText) Then
+            '如果没有搜索文本,按下载量将结果排序
+            For Each Result As ResourceProject In RealResults
+                Scores(Result) = Result.DownloadCount * GetDownloadCountMult(Result)
+            Next
+        Else
+            '如果有搜索文本,按关联度将结果排序
+            '排序分 = 搜索相对相似度 (1) + 下载量权重 (对数,10 亿时为 1) + 有中文名 (0.2)
+            Dim SearchEntries As New List(Of SearchEntry(Of ResourceProject))
+            For Each Result As ResourceProject In RealResults
+                Scores(Result) = If(Result.WikiId > 0, 0.2, 0) +
+                           Math.Log10(Math.Max(Result.DownloadCount, 1) * GetDownloadCountMult(Result)) / 9
+                SearchEntries.Add(New SearchEntry(Of ResourceProject) With {.Item = Result, .SearchSource = New List(Of SearchSource) From {
+                    New SearchSource(If(IsChineseSearch, Result.TranslatedName, Result.RawName).Split({"/"c}, StringSplitOptions.RemoveEmptyEntries), 1),
+                    New SearchSource(Result.Description, 0.05)}})
+            Next
+            Dim SearchResult = Search(SearchEntries, RawSearchText, 10000, -1)
+            For Each OneResult In SearchResult
+                Scores(OneResult.Item) +=
+                    If(OneResult.AbsoluteRight, 10, OneResult.Similarity) /
+                    If(SearchResult.First.AbsoluteRight, 10, SearchResult.First.Similarity) '最高 1 分的相似度分
+            Next
+        End If
+        '根据排序分得出结果并添加
+        If Task.IsInterrupted Then Throw New ThreadInterruptedException '#8246
+        Storage.Results.AddRange(
+            Scores.OrderByDescending(Function(s) s.Value).Select(Function(r) r.Key))
+
+#End Region
+
+    End Sub
+
+End Class

+ 405 - 0
Plain Craft Launcher 2/Modules/Resource/ResourceVersion.vb

@@ -0,0 +1,405 @@
+''' <summary>
+''' 社区资源的单个可下载版本。
+''' </summary>
+Public Class ResourceVersion
+
+    '基础字段
+
+    ''' <summary>
+    ''' 用于唯一性鉴别该版本的 ID。CurseForge 中为 123456 的大整数,Modrinth 中为英文乱码的 Version 字段。
+    ''' </summary>
+    Public Id As String
+    ''' <summary>
+    ''' 该版本所对应的单一资源类别。
+    ''' </summary>
+    Public ResourceType As ResourceTypes
+
+    '描述性字段
+
+    ''' <summary>
+    ''' 版本描述名。
+    ''' 并非文件名或版本号,是由上传者完全自定义的字段。
+    ''' </summary>
+    Public Display As String
+    ''' <summary>
+    ''' 发布时间。
+    ''' </summary>
+    Public ReleaseDate As Date
+    ''' <summary>
+    ''' 下载量计数。
+    ''' 注意,仅为一个平台的计数,无法反映多平台加起来的下载量,且 CurseForge 可能错误地返回 0。
+    ''' </summary>
+    Public DownloadCount As Integer
+    ''' <summary>
+    ''' 版本号。
+    ''' 不一定是标准格式。CurseForge 默认为 Nothing。
+    ''' </summary>
+    Public Version As String
+    ''' <summary>
+    ''' 支持的 Mod 加载器列表。
+    ''' 可能为空列表,不为 Nothing。
+    ''' </summary>
+    Public Loaders As List(Of ModLoaderTypes)
+    ''' <summary>
+    ''' 支持的游戏版本列表。类型包括:"26.1.5","26.1","26.1 预览版","1.18.5","1.18","1.18 预览版","21w15a","未知版本"。
+    ''' </summary>
+    Public GameVersions As List(Of String)
+
+    ''' <summary>
+    ''' 发布状态。
+    ''' </summary>
+    Public ReleaseType As ReleaseTypes
+    ''' <summary>
+    ''' 发布状态的友好描述。
+    ''' 例如:"正式版","Beta 版"。
+    ''' </summary>
+    Public ReadOnly Property ReleaseTypeDisplay As String
+        Get
+            Select Case ReleaseType
+                Case ReleaseTypes.Release
+                    Return "正式版"
+                Case ReleaseTypes.Beta
+                    Return If(ModeDebug, "Beta 版", "测试版")
+                Case Else
+                    Return If(ModeDebug, "Alpha 版", "早期测试版")
+            End Select
+        End Get
+    End Property
+    Public Enum ReleaseTypes
+        'https://docs.curseforge.com/rest-api/#tocS_FileReleaseType
+        Release = 1
+        Beta = 2
+        Alpha = 3
+    End Enum
+
+    '下载信息
+
+    ''' <summary>
+    ''' 下载信息是否可用。
+    ''' </summary>
+    Public ReadOnly Property DownloadAvailable As Boolean
+        Get
+            Return FileName IsNot Nothing AndAlso DownloadUrls IsNot Nothing
+        End Get
+    End Property
+    ''' <summary>
+    ''' 文件名。
+    ''' </summary>
+    Public FileName As String = Nothing
+    ''' <summary>
+    ''' 对应文件的所有可能的下载 URL。
+    ''' </summary>
+    Public DownloadUrls As List(Of String)
+    ''' <summary>
+    ''' 对应文件的 SHA1 或 MD5。
+    ''' </summary>
+    Public Hash As String = Nothing
+    ''' <summary>
+    ''' 文件大小。不可用时为 -1。
+    ''' </summary>
+    Public Size As Integer = -1
+    ''' <summary>
+    ''' 作为该版本依赖项的项目 ID。
+    ''' 它们可能没有加载,在加载后会添加到 Dependencies 中(主要是因为 Modrinth 返回的是字符串 ID 而非 Slug,导致 Project.Id 查询不到)。
+    ''' </summary>
+    Private RawDependencies As New List(Of String)
+    ''' <summary>
+    ''' 作为该版本依赖项的项目 ID。
+    ''' </summary>
+    Public Dependencies As New List(Of String)
+
+    '初始化
+
+    ''' <summary>
+    ''' 从平台 API 返回的 JSON 中初始化实例。若出错会抛出异常。
+    ''' </summary>
+    Public Shared Function FromPlatformJson(Data As JObject, ExpectedResourceType As ResourceTypes) As ResourceVersion
+        Dim Result As New ResourceVersion
+        With Result
+            .ResourceType = ExpectedResourceType
+            If Data.ContainsKey("gameId") Then
+#Region "CurseForge"
+                '简单信息
+                .Id = Data("id")
+                .Display = Data("displayName").ToString.Replace("	", "").Trim(" ")
+                .Version = Nothing
+                .ReleaseDate = Data("fileDate")
+                .ReleaseType = CType(Data("releaseType").ToObject(Of Integer), ReleaseTypes)
+                .DownloadCount = Data("downloadCount")
+                .FileName = Data("fileName")
+                .Size = Data("fileLength")
+                .Hash = CType(Data("hashes"), JArray).ToList.FirstOrDefault(Function(s) s("algo").ToObject(Of Integer) = 1)?("value")
+                If .Hash Is Nothing Then .Hash = CType(Data("hashes"), JArray).ToList.FirstOrDefault(Function(s) s("algo").ToObject(Of Integer) = 2)?("value")
+                'DownloadAddress
+                Dim Url = Data("downloadUrl").ToString
+                If Url = "" Then Url = $"https://edge.forgecdn.net/files/{CInt(.Id.ToString.Substring(0, 4))}/{CInt(.Id.ToString.Substring(4))}/{ .FileName}"
+                .DownloadUrls = ParseCurseForgeDownloadUrls(Url.Replace(.FileName, EscapeUtils.UrlEscape(.FileName))) '对脑残 CurseForge 的下载地址进行多种修正
+                .DownloadUrls.Add(DlSourceModGet(Url)) '添加镜像源;注意 MCIM 源不支持 URL 编码后的文件名,必须传入 URL 编码前的文件名
+                .DownloadUrls = .DownloadUrls.Distinct.ToList '最终去重
+                'Dependencies
+                If Data.ContainsKey("dependencies") Then
+                    .RawDependencies = Data("dependencies").
+                            Where(Function(d) d("relationType").ToObject(Of Integer) = 3 AndAlso '种类为依赖
+                                              d("modId").ToObject(Of Integer) <> 306612 AndAlso d("modId").ToObject(Of Integer) <> 634179). '排除 Fabric API 和 Quilt API
+                            Select(Function(d) d("modId").ToString).ToList
+                End If
+                'GameVersions
+                Dim RawVersions As List(Of String) = Data("gameVersions").Select(Function(t) t.ToString.Trim.Lower).ToList
+                .GameVersions = RawVersions.
+                        Where(Function(v) McVersion.IsFormatFit(v)).
+                        Select(Function(v) v.Replace("-snapshot", " 预览版")).
+                        Distinct.ToList
+                If .GameVersions.IsSingle Then
+                    .GameVersions = .GameVersions.ToList
+                ElseIf .GameVersions.Count > 1 Then
+                    .GameVersions = .GameVersions.SortByComparison(AddressOf CompareVersionGE).ToList
+                    If .ResourceType = ResourceTypes.ModPack Then .GameVersions = { .GameVersions(0)}.ToList '整合包理应只 “支持” 一个版本
+                Else
+                    .GameVersions = New List(Of String) From {"未知版本"}
+                End If
+                'ModLoaders
+                .Loaders = New List(Of ModLoaderTypes)
+                If RawVersions.Contains("forge") Then .Loaders.Add(ModLoaderTypes.Forge)
+                If RawVersions.Contains("fabric") Then .Loaders.Add(ModLoaderTypes.Fabric)
+                If RawVersions.Contains("quilt") Then .Loaders.Add(ModLoaderTypes.Quilt)
+                If RawVersions.Contains("neoforge") Then .Loaders.Add(ModLoaderTypes.NeoForge)
+#End Region
+            Else
+#Region "Modrinth"
+                '简单信息
+                .Id = Data("id")
+                .Display = Data("name").ToString.Replace("	", "").Trim(" ")
+                .Version = Data("version_number")
+                .ReleaseDate = Data("date_published")
+                .ReleaseType = If(Data("version_type").ToString = "release", ReleaseTypes.Release, If(Data("version_type").ToString = "beta", ReleaseTypes.Beta, ReleaseTypes.Alpha))
+                .DownloadCount = Data("downloads")
+                If CType(Data("files"), JArray).Any() Then '可能为空
+                    Dim File As JToken = Data("files")(0)
+                    .FileName = File("filename")
+                    .DownloadUrls = New List(Of String) From {File("url"), DlSourceModGet(File("url"))}.Distinct.ToList '同时添加了镜像源
+                    .Size = File("size")
+                    .Hash = File("hashes")("sha1")
+                End If
+                'Loaders
+                '结果可能混杂着 Mod、数据包和服务端插件
+                Dim RawLoaders As List(Of String) = Data("loaders").Select(Function(v) v.ToString).ToList
+                .Loaders = New List(Of ModLoaderTypes)
+                If .ResourceType.HasFlag(ResourceTypes.Mod) OrElse .ResourceType.HasFlag(ResourceTypes.DataPack) Then
+                    If RawLoaders.Intersect({"bukkit", "folia", "paper", "purpur", "spigot"}).Any() Then .ResourceType = ResourceTypes.Plugin 'Veinminer Enchantment 同时支持服务端与 Fabric
+                    If RawLoaders.Contains("datapack") Then .ResourceType = ResourceTypes.DataPack
+                    If RawLoaders.Contains("forge") Then .Loaders.Add(ModLoaderTypes.Forge) : .ResourceType = ResourceTypes.Mod
+                    If RawLoaders.Contains("neoforge") Then .Loaders.Add(ModLoaderTypes.NeoForge) : .ResourceType = ResourceTypes.Mod
+                    If RawLoaders.Contains("fabric") Then .Loaders.Add(ModLoaderTypes.Fabric) : .ResourceType = ResourceTypes.Mod
+                    If RawLoaders.Contains("quilt") Then .Loaders.Add(ModLoaderTypes.Quilt) : .ResourceType = ResourceTypes.Mod
+                Else
+                    '使用传入的类别,不作修改(#8377)
+                End If
+                'Dependencies
+                If Data.ContainsKey("dependencies") Then
+                    .RawDependencies = Data("dependencies").
+                            Where(Function(d) d("dependency_type") = "required" AndAlso '种类为依赖
+                                              d("project_id") <> "P7dR8mSH" AndAlso d("project_id") <> "qvIfYCYJ" AndAlso '排除 Fabric API 和 Quilt API
+                                              d("project_id").ToString.Length > 0). '有时候真的会空……
+                            Select(Function(d) d("project_id").ToString).ToList
+                End If
+                'GameVersions
+                Dim RawVersions As List(Of String) = Data("game_versions").Select(Function(t) t.ToString.Trim.Lower).ToList
+                .GameVersions = RawVersions.Where(Function(v) v.Contains(".")).
+                                               Select(Function(v) If(v.Contains("-"), v.BeforeFirst("-") & " 预览版", If(v.StartsWithF("b1."), "远古版本", v))).
+                                               Distinct.ToList
+                If .GameVersions.IsSingle Then
+                    '无需处理
+                ElseIf .GameVersions.Count > 1 Then
+                    .GameVersions = .GameVersions.SortByComparison(AddressOf CompareVersionGE).ToList
+                    If .ResourceType = ResourceTypes.ModPack Then .GameVersions = New List(Of String) From { .GameVersions(0)} '整合包理应只 “支持” 一个版本
+                ElseIf RawVersions.Any(Function(v) RegexCheck(v, "[0-9]{2}w[0-9]{2}[a-z]")) Then
+                    .GameVersions = RawVersions.Where(Function(v) RegexCheck(v, "[0-9]{2}w[0-9]{2}[a-z]")).ToList
+                Else
+                    .GameVersions = New List(Of String) From {"未知版本"}
+                End If
+#End Region
+            End If
+        End With
+        Return Result
+    End Function
+    ''' <summary>
+    ''' 从项目 ID 获取其下的全部版本列表,并获取其前置项目信息。
+    ''' </summary>
+    Public Shared Function FromProjectId(ProjectId As String, Platform As ResourcePlatforms, Optional LoadDependencies As Boolean = True) As List(Of ResourceVersion)
+        If Not Platform.Flags.IsSingle Then Throw New ArgumentException($"必须指定单一平台,当前值为 {Platform}")
+        Dim TargetProject = ResourceProject.FromProjectId(ProjectId, Platform)
+        '获取项目对象的文件列表
+        If Not ProjectFilesCache.ContainsKey(ProjectId) Then
+            Log("[Resource] 开始获取项目版本列表:" & ProjectId)
+            Dim ResultJsonArray As JArray = Nothing
+            If Platform = ResourcePlatforms.CurseForge Then
+                For RetryCount As Integer = 0 To 2
+                    Dim ResultJson As JObject = DlModRequest($"https://api.curseforge.com/v1/mods/{ProjectId}/files?pageSize=" &
+                    (10000 + RetryCount)) '每次重试多请求一个文件,以避免触发 CDN 缓存
+                    'HMCL 一次性请求了 10000 个文件,虽然不知道会不会出问题但先这样吧……(#5522)
+                    '之前只请求一部分文件的方法备份如下:
+                    'If TargetProject.Type = CompType.Mod Then 'Mod 使用每个版本最新的文件
+                    '    ResultJsonArray = DlModRequest("https://api.curseforge.com/v1/mods/files", HttpMethod.Post, "{""fileIds"": [" & Join(TargetProject.CurseForgeFileIds, ",") & "]}", "application/json")("data")
+                    'Else '否则使用全部文件
+                    '    ResultJsonArray = DlModRequest($"https://api.curseforge.com/v1/mods/{ProjectId}/files?pageSize=999")("data")
+                    'End If
+                    If ResultJson("pagination")("resultCount").ToObject(Of Integer) = ResultJson("pagination")("totalCount").ToObject(Of Integer) Then
+                        ResultJsonArray = ResultJson("data")
+                        Exit For
+                    ElseIf RetryCount < 2 Then
+                        Log($"[Resource] CurseForge 返回的资源版本列表存在缺失,即将进行第 {RetryCount + 1} 次重试", NotifyLevel.DebugModeOnly) '#6224
+                        Log($"[Resource] 返回的原始内容如下:{vbCrLf}{ResultJson}")
+                    Else
+                        Log($"[Resource] CurseForge 返回的资源版本列表存在缺失,返回的原始内容如下:{vbCrLf}{ResultJson}")
+                        Throw New Exception("CurseForge 返回的资源版本列表存在缺失")
+                    End If
+                Next
+            Else 'Modrinth
+                ResultJsonArray = DlModRequest($"https://api.modrinth.com/v2/project/{ProjectId}/version?include_changelog=false")
+            End If
+            ProjectFilesCache(ProjectId) = ResultJsonArray.Select(Function(a) ResourceVersion.FromPlatformJson(a, TargetProject.Types)).
+                Where(Function(a) a.DownloadAvailable).
+                DistinctBy(Function(a) a.Id).ToList 'CurseForge 可能会重复返回相同项(#1330)
+        End If
+        Dim Result As List(Of ResourceVersion) = ProjectFilesCache(ProjectId)
+        If Not LoadDependencies Then Return Result
+        '加载前置信息
+        Dim Dependencies As List(Of String) = Result.SelectMany(Function(f) f.RawDependencies).Distinct().ToList
+        If Dependencies.Any Then
+            Log($"[Resource] 项目 {ProjectId} 中需要加载的前置:{Dependencies.Join(",")}")
+            For Each DependencyProject In ResourceProject.FromProjectIds(Dependencies, TargetProject.Platform)
+                For Each File In Result
+                    If File.RawDependencies.Contains(DependencyProject.Id) AndAlso DependencyProject.Id <> ProjectId AndAlso Not File.Dependencies.Contains(DependencyProject.Id) Then
+                        File.Dependencies.Add(DependencyProject.Id)
+                    End If
+                Next
+            Next
+        End If
+        Return Result
+    End Function
+    ''' <summary>
+    ''' 项目 ID 对应的版本信息的缓存。不会保存到文件。
+    ''' </summary>
+    Protected Friend Shared ProjectFilesCache As New Dictionary(Of String, List(Of ResourceVersion))
+
+    '序列化
+
+    ''' <summary>
+    ''' 从缓存 JSON 读取字段数据。
+    ''' </summary>
+    Public Shared Function FromCacheJson(Data As JObject) As ResourceVersion
+        Dim Result As New ResourceVersion
+        With Result
+            .ResourceType = CType(Data("ResourceType").ToObject(Of Integer), ResourceTypes)
+            .Id = Data("Id").ToString
+            .Display = Data("DisplayName").ToString
+            If Data.ContainsKey("Version") Then .Version = Data("Version").ToString
+            .ReleaseDate = Data("ReleaseDate").ToObject(Of Date)
+            .DownloadCount = Data("DownloadCount").ToObject(Of Integer)
+            .ReleaseType = CType(Data("ReleaseType").ToObject(Of Integer), ReleaseTypes)
+            If Data.ContainsKey("FileName") Then .FileName = Data("FileName").ToString
+            If Data.ContainsKey("DownloadUrls") Then .DownloadUrls = Data("DownloadUrls").ToObject(Of List(Of String))
+            If Data.ContainsKey("ModLoaders") Then .Loaders = Data("ModLoaders").ToObject(Of List(Of ModLoaderTypes))
+            If Data.ContainsKey("Size") Then .Size = Data("Size").ToObject(Of Integer)
+            If Data.ContainsKey("Hash") Then .Hash = Data("Hash").ToString
+            If Data.ContainsKey("GameVersions") Then .GameVersions = Data("GameVersions").ToObject(Of List(Of String))
+            If Data.ContainsKey("RawDependencies") Then .RawDependencies = Data("RawDependencies").ToObject(Of List(Of String))
+            If Data.ContainsKey("Dependencies") Then .Dependencies = Data("Dependencies").ToObject(Of List(Of String))
+        End With
+        Return Result
+    End Function
+    ''' <summary>
+    ''' 转为缓存 JSON。
+    ''' </summary>
+    Public Function ToCacheJson() As JObject
+        Dim Result As New JObject
+        With Result
+            .Add("ResourceType", ResourceType)
+            .Add("Id", Id)
+            If Version IsNot Nothing Then .Add("Version", Version)
+            .Add("DisplayName", Display)
+            .Add("ReleaseDate", ReleaseDate)
+            .Add("DownloadCount", DownloadCount)
+            .Add("ModLoaders", New JArray(Loaders.Select(Function(m) CInt(m))))
+            .Add("GameVersions", New JArray(GameVersions))
+            .Add("ReleaseType", CInt(ReleaseType))
+            If FileName IsNot Nothing Then .Add("FileName", FileName)
+            If DownloadUrls IsNot Nothing Then .Add("DownloadUrls", New JArray(DownloadUrls))
+            If Hash IsNot Nothing Then .Add("Hash", Hash)
+            If Size >= 0 Then .Add("Size", Size)
+            .Add("RawDependencies", New JArray(RawDependencies))
+            .Add("Dependencies", New JArray(Dependencies))
+        End With
+        Return Result
+    End Function
+
+    '杂项
+
+    ''' <summary>
+    ''' 转为列表项。
+    ''' </summary>
+    Public Function ToListItem(ClickHandler As MyListItem.ClickEventHandler, Optional SaveAsButtonHandler As MyIconButton.ClickEventHandler = Nothing,
+                               Optional IsBadDisplay As Boolean = False) As MyVirtualizingElement(Of MyListItem)
+        Return New MyVirtualizingElement(Of MyListItem)(
+        Function()
+            Dim Result As New MyListItem
+            With Result
+
+                '基础属性
+                .SnapsToDevicePixels = True
+                .Height = 42
+                .Type = MyListItem.CheckType.Clickable
+                .Tag = Me
+                .Title = If(IsBadDisplay, FileName, Display)
+                .Logo = $"{PathImage}ReleaseTypes/{ReleaseType}.png"
+                AddHandler .Click, ClickHandler
+
+                '描述
+                .Info =
+                Iterator Function()
+                    If .Title <> FileName.BeforeLast(".") Then Yield FileName.BeforeLast(".")
+                    If Dependencies.Any Then Yield $"{Dependencies.Count} 项前置"
+                    If GameVersions.All(Function(v) Not v.Contains(".") OrElse {"w", "snapshot", "rc", "pre", "experimental", "-"}.Any(Function(s) v.ContainsIgnoreCase(s))) Then Yield $"游戏版本 {GameVersions.Join("、"c)}"
+                    If DownloadCount > 0 Then Yield $"下载 {If(DownloadCount > 100000, Math.Round(DownloadCount / 10000) & " 万", DownloadCount)} 次" 'CurseForge 的下载次数经常错误地返回 0
+                    Yield $"更新于 {GetTimeSpanString(ReleaseDate - Date.Now, False)}"
+                    If ReleaseType <> ReleaseTypes.Release Then Yield ReleaseTypeDisplay
+                End Function().Join(","c)
+
+                '另存为按钮
+                If SaveAsButtonHandler IsNot Nothing Then
+                    Dim BtnSave As New MyIconButton With {.Logo = Logo.IconButtonSave, .ToolTip = "另存为"}
+                    ToolTipService.SetPlacement(BtnSave, Primitives.PlacementMode.Center)
+                    ToolTipService.SetVerticalOffset(BtnSave, 30)
+                    ToolTipService.SetHorizontalOffset(BtnSave, 2)
+                    AddHandler BtnSave.Click, SaveAsButtonHandler
+                    .Buttons = {BtnSave}
+                End If
+
+            End With
+            Return Result
+        End Function) With {.Height = 42}
+    End Function
+    ''' <summary>
+    ''' 获取下载信息。
+    ''' </summary>
+    ''' <param name="LocalFileOrFolder">目标本地文件夹,或完整的文件路径。会自动判断类型。</param>
+    Public Function ToNetFile(LocalFileOrFolder As String) As NetFile
+        Return New NetFile(DownloadUrls, LocalFileOrFolder & If(LocalFileOrFolder.EndsWithF("\"), FileName, ""), New FileChecker(Hash:=Hash, ActualSize:=Size), SimulateBrowserHeaders:=True)
+    End Function
+    ''' <summary>
+    ''' 重新整理 CurseForge 的下载地址。
+    ''' </summary>
+    Public Shared Function ParseCurseForgeDownloadUrls(Url As String) As List(Of String)
+        Return {
+            Url.Replace("-service.overwolf.wtf", ".forgecdn.net").Replace("://edge.", "://mediafilez.").Replace("://media.", "://mediafilez."),
+            Url.Replace("://edge.", "://mediafilez.").Replace("://media.", "://mediafilez."),
+            Url.Replace("-service.overwolf.wtf", ".forgecdn.net"),
+            Url.Replace("://media.", "://edge."),
+            Url
+        }.Distinct.ToList
+    End Function
+    Public Overrides Function ToString() As String
+        Return $"{Id}: {FileName}"
+    End Function
+
+End Class

+ 85 - 0
Plain Craft Launcher 2/Modules/Resource/WikiEntry.vb

@@ -0,0 +1,85 @@
+'获取所有条目:WikiEntry.All
+
+''' <summary>
+''' MC 百科条目。
+''' </summary>
+Public Class WikiEntry
+
+    '属性
+
+    ''' <summary>
+    ''' 在 MC 百科中的对应 ID。
+    ''' </summary>
+    Public Id As Integer
+    ''' <summary>
+    ''' 中文译名。空字符串代表没有翻译。
+    ''' </summary>
+    Public ChineseName As String = ""
+    ''' <summary>
+    ''' CurseForge Slug(例如 advanced-solar-panels)。
+    ''' </summary>
+    Public CurseForgeSlug As String = Nothing
+    ''' <summary>
+    ''' Modrinth Slug(例如 advanced-solar-panels)。
+    ''' </summary>
+    Public ModrinthSlug As String = Nothing
+    ''' <summary>
+    ''' MC 百科的浏览量逆序排行,1 代表浏览量最低。
+    ''' </summary>
+    Public Popularity As Integer
+    Public Overrides Function ToString() As String
+        Return If(CurseForgeSlug, "") & "&" & If(ModrinthSlug, "") & "|" & Id & "|" & ChineseName & ", Rank " & Popularity
+    End Function
+
+    '读取
+
+    ''' <summary>
+    ''' 内置数据库中的所有 MC 百科条目。
+    ''' </summary>
+    Public Shared ReadOnly Property All As List(Of WikiEntry)
+        Get
+            Static Cache As List(Of WikiEntry) = Nothing
+            If Cache IsNot Nothing Then Return Cache
+            '实际加载
+            Cache = New List(Of WikiEntry)
+            Dim Data As String
+            Using Archive As New ZipArchive(New MemoryStream(GetResources("ModData")), ZipArchiveMode.Read)
+                Data = ReadFile(Archive.GetEntry("moddata.txt").Open(), Encoding.UTF8)
+            End Using
+            Dim i As Integer = 0
+            For Each Line In Data.ReplaceLineEndings(vbLf).Split(vbLf)
+                i += 1
+                If Line = "" Then Continue For
+                For Each EntryData As String In Line.Split("¨")
+                    Dim Entry = New WikiEntry
+                    Dim Splited = EntryData.Split("|")
+                    Entry.Popularity = Val(Splited(0))
+                    If Splited(1).StartsWithF("@") Then
+                        Entry.CurseForgeSlug = Nothing
+                        Entry.ModrinthSlug = Splited(1).Replace("@", "")
+                    ElseIf Splited(1).EndsWithF("@") Then
+                        Entry.CurseForgeSlug = Splited(1).TrimEnd("@")
+                        Entry.ModrinthSlug = Entry.CurseForgeSlug
+                    ElseIf Splited(1).Contains("@") Then
+                        Entry.CurseForgeSlug = Splited(1).Split("@")(0)
+                        Entry.ModrinthSlug = Splited(1).Split("@")(1)
+                    Else
+                        Entry.CurseForgeSlug = Splited(1)
+                        Entry.ModrinthSlug = Nothing
+                    End If
+                    Entry.Id = i
+                    If Splited.Count >= 3 Then
+                        Entry.ChineseName = Splited.Last
+                        If Entry.ChineseName.Contains("*") Then '处理 *
+                            Entry.ChineseName = Entry.ChineseName.Replace("*",
+                                    $" ({If(Entry.CurseForgeSlug, Entry.ModrinthSlug).Replace("-", " ").Capitalize})")
+                        End If
+                    End If
+                    Cache.Add(Entry)
+                Next
+            Next
+            Return Cache
+        End Get
+    End Property
+
+End Class

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

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

+ 30 - 0
Plain Craft Launcher 2/My Project/Resources.Designer.vb

@@ -160,6 +160,26 @@ Namespace My.Resources
             End Get
             End Get
         End Property
         End Property
         
         
+        '''<summary>
+        '''  查找 System.Byte[] 类型的本地化资源。
+        '''</summary>
+        Friend ReadOnly Property MeloongCore() As Byte()
+            Get
+                Dim obj As Object = ResourceManager.GetObject("MeloongCore", resourceCulture)
+                Return CType(obj,Byte())
+            End Get
+        End Property
+        
+        '''<summary>
+        '''  查找 System.Byte[] 类型的本地化资源。
+        '''</summary>
+        Friend ReadOnly Property MeloongCore_Wpf() As Byte()
+            Get
+                Dim obj As Object = ResourceManager.GetObject("MeloongCore.Wpf", resourceCulture)
+                Return CType(obj,Byte())
+            End Get
+        End Property
+        
         '''<summary>
         '''<summary>
         '''  查找 System.Byte[] 类型的本地化资源。
         '''  查找 System.Byte[] 类型的本地化资源。
         '''</summary>
         '''</summary>
@@ -200,6 +220,16 @@ Namespace My.Resources
             End Get
             End Get
         End Property
         End Property
         
         
+        '''<summary>
+        '''  查找 System.Byte[] 类型的本地化资源。
+        '''</summary>
+        Friend ReadOnly Property PCLCS() As Byte()
+            Get
+                Dim obj As Object = ResourceManager.GetObject("PCLCS", resourceCulture)
+                Return CType(obj,Byte())
+            End Get
+        End Property
+        
         '''<summary>
         '''<summary>
         '''  查找 System.Byte[] 类型的本地化资源。
         '''  查找 System.Byte[] 类型的本地化资源。
         '''</summary>
         '''</summary>

+ 9 - 0
Plain Craft Launcher 2/My Project/Resources.resx

@@ -148,6 +148,12 @@
   <data name="LUA" type="System.Resources.ResXFileRef, System.Windows.Forms">
   <data name="LUA" type="System.Resources.ResXFileRef, System.Windows.Forms">
     <value>..\Resources\lwjgl-unsafe-agent.jar;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
     <value>..\Resources\lwjgl-unsafe-agent.jar;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
   </data>
   </data>
+  <data name="MeloongCore" type="System.Resources.ResXFileRef, System.Windows.Forms">
+    <value>..\..\PCLCS\bin\MeloongCore.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </data>
+  <data name="MeloongCore.Wpf" type="System.Resources.ResXFileRef, System.Windows.Forms">
+    <value>..\..\PCLCS\bin\MeloongCore.Wpf.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </data>
   <data name="ModData" type="System.Resources.ResXFileRef, System.Windows.Forms">
   <data name="ModData" type="System.Resources.ResXFileRef, System.Windows.Forms">
     <value>..\Resources\moddata.zip;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
     <value>..\Resources\moddata.zip;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
   </data>
   </data>
@@ -160,6 +166,9 @@
   <data name="Ookii.Dialogs.Wpf" type="System.Resources.ResXFileRef, System.Windows.Forms">
   <data name="Ookii.Dialogs.Wpf" type="System.Resources.ResXFileRef, System.Windows.Forms">
     <value>..\resources\ookii.dialogs.wpf.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
     <value>..\resources\ookii.dialogs.wpf.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
   </data>
   </data>
+  <data name="PCLCS" type="System.Resources.ResXFileRef, System.Windows.Forms">
+    <value>..\..\PCLCS\bin\PCLCS.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </data>
   <data name="System.Net.Http.Formatting" type="System.Resources.ResXFileRef, System.Windows.Forms">
   <data name="System.Net.Http.Formatting" type="System.Resources.ResXFileRef, System.Windows.Forms">
     <value>..\Resources\System.Net.Http.Formatting.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
     <value>..\Resources\System.Net.Http.Formatting.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
   </data>
   </data>

+ 54 - 58
Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb

@@ -1,6 +1,4 @@
-Imports System.IO.Compression
-
-Public Module ModDownloadLib
+Public Module ModDownloadLib
 
 
 #Region "Minecraft 下载"
 #Region "Minecraft 下载"
 
 
@@ -51,7 +49,7 @@ Public Module ModDownloadLib
             Try
             Try
                 Dim InstanceJson As JObject = GetJson(ReadFile(VersionFolder & InstanceName & ".json"))
                 Dim InstanceJson As JObject = GetJson(ReadFile(VersionFolder & InstanceName & ".json"))
                 InstanceJson.Add("clientVersion", Id)
                 InstanceJson.Add("clientVersion", Id)
-                WriteFile($"{VersionFolder}{InstanceName}.json", InstanceJson.ToString)
+                FileUtils.Write($"{VersionFolder}{InstanceName}.json", InstanceJson.ToString)
             Catch ex As Exception
             Catch ex As Exception
                 Throw New Exception("添加客户端版本失败", ex)
                 Throw New Exception("添加客户端版本失败", ex)
             End Try
             End Try
@@ -206,7 +204,7 @@ echo ------------------------------
 echo ----------------------
 echo ----------------------
 echo 服务端已停止。
 echo 服务端已停止。
 pause"
 pause"
-                WriteFile(VersionFolder & "Launch Server.bat", Bat,
+                FileUtils.Write(VersionFolder & "Launch Server.bat", Bat,
                         Encoding:=If(Encoding.Default.Equals(Encoding.UTF8), Encoding.UTF8, Encoding.GetEncoding("GB18030")))
                         Encoding:=If(Encoding.Default.Equals(Encoding.UTF8), Encoding.UTF8, Encoding.GetEncoding("GB18030")))
                 '删除版本 JSON
                 '删除版本 JSON
                 File.Delete(VersionFolder & Id & ".json")
                 File.Delete(VersionFolder & Id & ".json")
@@ -221,7 +219,7 @@ pause"
             FrmMain.BtnExtraDownload.ShowRefresh()
             FrmMain.BtnExtraDownload.ShowRefresh()
             FrmMain.BtnExtraDownload.Ribble()
             FrmMain.BtnExtraDownload.Ribble()
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "开始 Minecraft 服务端下载失败", LogLevel.Feedback)
+            Log(ex, "开始 Minecraft 服务端下载失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
     Public Sub McDownloadMenuSave(sender As Object, e As RoutedEventArgs)
     Public Sub McDownloadMenuSave(sender As Object, e As RoutedEventArgs)
@@ -266,7 +264,7 @@ pause"
             FrmMain.BtnExtraDownload.ShowRefresh()
             FrmMain.BtnExtraDownload.ShowRefresh()
             FrmMain.BtnExtraDownload.Ribble()
             FrmMain.BtnExtraDownload.Ribble()
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "开始 Minecraft 下载失败", LogLevel.Feedback)
+            Log(ex, "开始 Minecraft 下载失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
     ''' <summary>
     ''' <summary>
@@ -325,7 +323,7 @@ pause"
         ElseIf Id.StartsWithF("rd-") Then
         ElseIf Id.StartsWithF("rd-") Then
             WikiName = "Java版Pre-classic_" & Id
             WikiName = "Java版Pre-classic_" & Id
         Else
         Else
-            Log("[Error] 未知的版本格式:" & Id & "。", LogLevel.Feedback)
+            Log("[Error] 未知的版本格式:" & Id & "。", NotifyLevel.MsgBoxAndFeedback)
             Return
             Return
         End If
         End If
         OpenWebsite("https://zh.minecraft.wiki/w/" & WikiName.Replace("_experimental-snapshot-", "-exp"))
         OpenWebsite("https://zh.minecraft.wiki/w/" & WikiName.Replace("_experimental-snapshot-", "-exp"))
@@ -355,7 +353,7 @@ pause"
             FrmMain.BtnExtraDownload.Ribble()
             FrmMain.BtnExtraDownload.Ribble()
 
 
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "开始 OptiFine 下载失败", LogLevel.Feedback)
+            Log(ex, "开始 OptiFine 下载失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
     Private Sub McDownloadOptiFineInstall(BaseMcFolderHome As String, Target As String, Task As LoaderTask(Of List(Of NetFile), Boolean), UseJavaWrapper As Boolean)
     Private Sub McDownloadOptiFineInstall(BaseMcFolderHome As String, Target As String, Task As LoaderTask(Of List(Of NetFile), Boolean), UseJavaWrapper As Boolean)
@@ -397,7 +395,7 @@ pause"
                 .CreateNoWindow = True,
                 .CreateNoWindow = True,
                 .RedirectStandardError = True,
                 .RedirectStandardError = True,
                 .RedirectStandardOutput = True,
                 .RedirectStandardOutput = True,
-                .WorkingDirectory = ShortenPath(BaseMcFolderHome)
+                .WorkingDirectory = FileUtils.ShortenPath(BaseMcFolderHome)
             }
             }
             If Info.EnvironmentVariables.ContainsKey("appdata") Then
             If Info.EnvironmentVariables.ContainsKey("appdata") Then
                 Info.EnvironmentVariables("appdata") = BaseMcFolderHome
                 Info.EnvironmentVariables("appdata") = BaseMcFolderHome
@@ -534,7 +532,7 @@ pause"
             If Not IsCustomFolder Then Return
             If Not IsCustomFolder Then Return
             SyncLock VanillaSyncLock
             SyncLock VanillaSyncLock
                 Dim ClientName As String = GetFolderNameFromPath(ClientFolder)
                 Dim ClientName As String = GetFolderNameFromPath(ClientFolder)
-                Directory.CreateDirectory(McFolder & "versions\" & DownloadInfo.Inherit)
+                DirectoryUtils.Create(McFolder & "versions\" & DownloadInfo.Inherit)
                 If Not File.Exists(McFolder & "versions\" & DownloadInfo.Inherit & "\" & DownloadInfo.Inherit & ".json") Then
                 If Not File.Exists(McFolder & "versions\" & DownloadInfo.Inherit & "\" & DownloadInfo.Inherit & ".json") Then
                     CopyFile($"{ClientFolder}{ClientName}.json", $"{McFolder}versions\{DownloadInfo.Inherit}\{DownloadInfo.Inherit}.json")
                     CopyFile($"{ClientFolder}{ClientName}.json", $"{McFolder}versions\{DownloadInfo.Inherit}\{DownloadInfo.Inherit}.json")
                 End If
                 End If
@@ -556,7 +554,7 @@ pause"
                     If Directory.Exists(BaseMcFolder & "versions\" & DownloadInfo.Inherit) Then
                     If Directory.Exists(BaseMcFolder & "versions\" & DownloadInfo.Inherit) Then
                         DeleteDirectory(BaseMcFolder & "versions\" & DownloadInfo.Inherit)
                         DeleteDirectory(BaseMcFolder & "versions\" & DownloadInfo.Inherit)
                     End If
                     End If
-                    Directory.CreateDirectory(BaseMcFolder & "versions\" & DownloadInfo.Inherit & "\")
+                    DirectoryUtils.Create(BaseMcFolder & "versions\" & DownloadInfo.Inherit & "\")
                     McFolderLauncherProfilesJsonCreate(BaseMcFolder)
                     McFolderLauncherProfilesJsonCreate(BaseMcFolder)
                     CopyFile(McFolder & "versions\" & DownloadInfo.Inherit & "\" & DownloadInfo.Inherit & ".json",
                     CopyFile(McFolder & "versions\" & DownloadInfo.Inherit & "\" & DownloadInfo.Inherit & ".json",
                               BaseMcFolder & "versions\" & DownloadInfo.Inherit & "\" & DownloadInfo.Inherit & ".json")
                               BaseMcFolder & "versions\" & DownloadInfo.Inherit & "\" & DownloadInfo.Inherit & ".json")
@@ -595,7 +593,7 @@ Retry:
             Sub(Task As LoaderTask(Of List(Of NetFile), Boolean))
             Sub(Task As LoaderTask(Of List(Of NetFile), Boolean))
                 Try
                 Try
                     '新建版本文件夹
                     '新建版本文件夹
-                    Directory.CreateDirectory(VersionFolder)
+                    DirectoryUtils.Create(VersionFolder)
                     Task.Progress = 0.1
                     Task.Progress = 0.1
                     '复制 Jar 文件
                     '复制 Jar 文件
                     If File.Exists(VersionFolder & Id & ".jar") Then File.Delete(VersionFolder & Id & ".jar")
                     If File.Exists(VersionFolder & Id & ".jar") Then File.Delete(VersionFolder & Id & ".jar")
@@ -633,7 +631,7 @@ Retry:
     }
     }
 }"
 }"
                     End If
                     End If
-                    WriteFile(VersionFolder & Id & ".json", Json)
+                    FileUtils.Write(VersionFolder & Id & ".json", Json)
                 Catch ex As Exception
                 Catch ex As Exception
                     Throw New Exception("安装 OptiFine(方式 B)失败", ex)
                     Throw New Exception("安装 OptiFine(方式 B)失败", ex)
                 End Try
                 End Try
@@ -798,7 +796,7 @@ Retry:
             FrmMain.BtnExtraDownload.Ribble()
             FrmMain.BtnExtraDownload.Ribble()
 
 
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "开始 LiteLoader 安装器下载失败", LogLevel.Feedback)
+            Log(ex, "开始 LiteLoader 安装器下载失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -820,8 +818,6 @@ Retry:
         Loaders.Add(New LoaderTask(Of String, String)("安装 LiteLoader",
         Loaders.Add(New LoaderTask(Of String, String)("安装 LiteLoader",
         Sub(Task As LoaderTask(Of String, String))
         Sub(Task As LoaderTask(Of String, String))
             Try
             Try
-                '新建版本文件夹
-                Directory.CreateDirectory(VersionFolder)
                 '构造版本 Json
                 '构造版本 Json
                 Dim VersionJson As New JObject
                 Dim VersionJson As New JObject
                 VersionJson.Add("id", VersionName)
                 VersionJson.Add("id", VersionName)
@@ -835,8 +831,8 @@ Retry:
                 VersionJson.Add("minimumLauncherVersion", 18)
                 VersionJson.Add("minimumLauncherVersion", 18)
                 VersionJson.Add("inheritsFrom", DownloadInfo.Inherit)
                 VersionJson.Add("inheritsFrom", DownloadInfo.Inherit)
                 VersionJson.Add("jar", DownloadInfo.Inherit)
                 VersionJson.Add("jar", DownloadInfo.Inherit)
-                '输出 Json 文件
-                WriteFile(VersionFolder & VersionName & ".json", VersionJson.ToString)
+                '输出 Json 文件,同时会新建该文件夹
+                FileUtils.Write(VersionFolder & VersionName & ".json", VersionJson.ToString)
             Catch ex As Exception
             Catch ex As Exception
                 Throw New Exception("安装新 LiteLoader 版本失败", ex)
                 Throw New Exception("安装新 LiteLoader 版本失败", ex)
             End Try
             End Try
@@ -966,7 +962,7 @@ Retry:
             FrmMain.BtnExtraDownload.Ribble()
             FrmMain.BtnExtraDownload.Ribble()
 
 
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, $"开始 {Info.LoaderName} 安装器下载失败", LogLevel.Feedback)
+            Log(ex, $"开始 {Info.LoaderName} 安装器下载失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -1235,7 +1231,7 @@ Retry:
                     For i = 0 To Libs.Count - 1
                     For i = 0 To Libs.Count - 1
                         If Libs(i).LocalPath.EndsWithF($"{LoaderName.Lower}-{Inherit}-{LoaderVersion}.jar") OrElse
                         If Libs(i).LocalPath.EndsWithF($"{LoaderName.Lower}-{Inherit}-{LoaderVersion}.jar") OrElse
                            Libs(i).LocalPath.EndsWithF($"{LoaderName.Lower}-{Inherit}-{LoaderVersion}-client.jar") Then
                            Libs(i).LocalPath.EndsWithF($"{LoaderName.Lower}-{Inherit}-{LoaderVersion}-client.jar") Then
-                            Log($"[Download] 已从待下载 {LoaderName} 支持库中移除:" & Libs(i).LocalPath, LogLevel.Debug)
+                            Log($"[Download] 已从待下载 {LoaderName} 支持库中移除:" & Libs(i).LocalPath, NotifyLevel.DebugModeOnly)
                             Libs.RemoveAt(i)
                             Libs.RemoveAt(i)
                             Exit For
                             Exit For
                         End If
                         End If
@@ -1256,7 +1252,7 @@ Retry:
                     For Each LibFile As McLibToken In Libs
                     For Each LibFile As McLibToken In Libs
                         Dim RealPath As String = LibFile.LocalPath.Replace(McFolderSelected, McFolder)
                         Dim RealPath As String = LibFile.LocalPath.Replace(McFolderSelected, McFolder)
                         If Not File.Exists(RealPath) Then
                         If Not File.Exists(RealPath) Then
-                            Directory.CreateDirectory(IO.Path.GetDirectoryName(RealPath))
+                            DirectoryUtils.Create(RealPath, isFilePath:=True)
                             CopyFile(LibFile.LocalPath, RealPath)
                             CopyFile(LibFile.LocalPath, RealPath)
                         End If
                         End If
                         If ModeDebug Then Log($"[Download] 复制的 {LoaderName} 支持库文件:" & LibFile.LocalPath)
                         If ModeDebug Then Log($"[Download] 复制的 {LoaderName} 支持库文件:" & LibFile.LocalPath)
@@ -1278,7 +1274,7 @@ Retry:
                 If Not IsCustomFolder Then Return
                 If Not IsCustomFolder Then Return
                 SyncLock VanillaSyncLock
                 SyncLock VanillaSyncLock
                     Dim ClientName As String = GetFolderNameFromPath(ClientFolder)
                     Dim ClientName As String = GetFolderNameFromPath(ClientFolder)
-                    Directory.CreateDirectory(McFolder & "versions\" & Inherit)
+                    DirectoryUtils.Create(McFolder & "versions\" & Inherit)
                     If Not File.Exists(McFolder & "versions\" & Inherit & "\" & Inherit & ".json") Then
                     If Not File.Exists(McFolder & "versions\" & Inherit & "\" & Inherit & ".json") Then
                         CopyFile(ClientFolder & ClientName & ".json", McFolder & "versions\" & Inherit & "\" & Inherit & ".json")
                         CopyFile(ClientFolder & ClientName & ".json", McFolder & "versions\" & Inherit & "\" & Inherit & ".json")
                     End If
                     End If
@@ -1299,7 +1295,7 @@ Retry:
                     Installer = New ZipArchive(New FileStream(InstallerAddress, FileMode.Open, FileAccess.Read, FileShare.Read))
                     Installer = New ZipArchive(New FileStream(InstallerAddress, FileMode.Open, FileAccess.Read, FileShare.Read))
                     Dim Json As JObject = GetJson(ReadFile(Installer.GetEntry("install_profile.json").Open))
                     Dim Json As JObject = GetJson(ReadFile(Installer.GetEntry("install_profile.json").Open))
                     '新建目标版本文件夹
                     '新建目标版本文件夹
-                    Directory.CreateDirectory(VersionFolder)
+                    DirectoryUtils.Create(VersionFolder)
                     Task.Progress = 0.04
                     Task.Progress = 0.04
                     '释放 launcher_installer.json
                     '释放 launcher_installer.json
                     McFolderLauncherProfilesJsonCreate(McFolder)
                     McFolderLauncherProfilesJsonCreate(McFolder)
@@ -1309,7 +1305,7 @@ Retry:
 Retry:
 Retry:
                     Try
                     Try
                         '释放 Forge 注入器
                         '释放 Forge 注入器
-                        WriteFile(PathTemp & "Cache\forge_installer.jar", GetResources("ForgeInstaller"))
+                        FileUtils.Write(PathTemp & "Cache\forge_installer.jar", GetResources("ForgeInstaller"))
                         Task.Progress = 0.06
                         Task.Progress = 0.06
                         '运行注入器
                         '运行注入器
                         ForgelikeInjector(InstallerAddress, Task, McFolder, UseJavaWrapper, IsNeoForge)
                         ForgelikeInjector(InstallerAddress, Task, McFolder, UseJavaWrapper, IsNeoForge)
@@ -1329,13 +1325,13 @@ Retry:
                     If DeltaList.Count > 1 Then
                     If DeltaList.Count > 1 Then
                         '它可能和 OptiFine 安装同时运行,导致增加的文件不止一个(这导致了 #151)
                         '它可能和 OptiFine 安装同时运行,导致增加的文件不止一个(这导致了 #151)
                         '也可能是因为 Forge 安装器的 Bug,生成了一个名字错误的文件夹,所以需要检查文件夹是否为空
                         '也可能是因为 Forge 安装器的 Bug,生成了一个名字错误的文件夹,所以需要检查文件夹是否为空
-                        DeltaList = DeltaList.Where(Function(l) l.Name.ContainsF("forge", True) AndAlso l.EnumerateFiles.Any).ToList
+                        DeltaList = DeltaList.Where(Function(l) l.Name.ContainsIgnoreCase("forge") AndAlso l.EnumerateFiles.Any).ToList
                     End If
                     End If
                     If DeltaList.Count = 1 Then
                     If DeltaList.Count = 1 Then
                         '如果没有新增文件夹,那么预测的文件夹名就是正确的
                         '如果没有新增文件夹,那么预测的文件夹名就是正确的
                         '如果只新增 1 个文件夹,那么拷贝 json 文件
                         '如果只新增 1 个文件夹,那么拷贝 json 文件
                         Dim JsonFile As FileInfo = DeltaList(0).EnumerateFiles.First()
                         Dim JsonFile As FileInfo = DeltaList(0).EnumerateFiles.First()
-                        WriteFile(VersionFolder & NewInstanceName & ".json", ReadFile(JsonFile.FullName))
+                        FileUtils.Write(VersionFolder & NewInstanceName & ".json", ReadFile(JsonFile.FullName))
                         Log($"[Download] 已拷贝新增的版本 JSON 文件:{JsonFile.FullName} -> {VersionFolder}{NewInstanceName}.json")
                         Log($"[Download] 已拷贝新增的版本 JSON 文件:{JsonFile.FullName} -> {VersionFolder}{NewInstanceName}.json")
                     ElseIf DeltaList.Count > 1 Then
                     ElseIf DeltaList.Count > 1 Then
                         '新增了多个文件夹
                         '新增了多个文件夹
@@ -1368,7 +1364,7 @@ Retry:
                     Dim Json As JObject = GetJson(ReadFile(Installer.GetEntry("install_profile.json").Open))
                     Dim Json As JObject = GetJson(ReadFile(Installer.GetEntry("install_profile.json").Open))
                     Task.Progress = 0.4
                     Task.Progress = 0.4
                     '新建版本文件夹
                     '新建版本文件夹
-                    Directory.CreateDirectory(VersionFolder)
+                    DirectoryUtils.Create(VersionFolder)
                     Task.Progress = 0.5
                     Task.Progress = 0.5
                     If Json("install") Is Nothing Then
                     If Json("install") Is Nothing Then
                         '中版:Legacy 方式 1
                         '中版:Legacy 方式 1
@@ -1376,7 +1372,7 @@ Retry:
                         '建立 Json 文件
                         '建立 Json 文件
                         Dim JsonVersion As JObject = GetJson(ReadFile(Installer.GetEntry(Json("json").ToString.TrimStart("/")).Open))
                         Dim JsonVersion As JObject = GetJson(ReadFile(Installer.GetEntry(Json("json").ToString.TrimStart("/")).Open))
                         JsonVersion("id") = NewInstanceName
                         JsonVersion("id") = NewInstanceName
-                        WriteFile(VersionFolder & NewInstanceName & ".json", JsonVersion.ToString)
+                        FileUtils.Write(VersionFolder & NewInstanceName & ".json", JsonVersion.ToString)
                         Task.Progress = 0.6
                         Task.Progress = 0.6
                         '解压支持库文件
                         '解压支持库文件
                         Installer.Dispose()
                         Installer.Dispose()
@@ -1389,12 +1385,12 @@ Retry:
                         '解压 Jar 文件
                         '解压 Jar 文件
                         Dim JarAddress As String = McLibGet(Json("install")("path"), CustomMcFolder:=McFolder)
                         Dim JarAddress As String = McLibGet(Json("install")("path"), CustomMcFolder:=McFolder)
                         If File.Exists(JarAddress) Then File.Delete(JarAddress)
                         If File.Exists(JarAddress) Then File.Delete(JarAddress)
-                        WriteFile(JarAddress, Installer.GetEntry(Json("install")("filePath")).Open)
+                        FileUtils.Write(JarAddress, Installer.GetEntry(Json("install")("filePath")).Open)
                         Task.Progress = 0.9
                         Task.Progress = 0.9
                         '建立 Json 文件
                         '建立 Json 文件
                         Json("versionInfo")("id") = NewInstanceName
                         Json("versionInfo")("id") = NewInstanceName
                         If Json("versionInfo")("inheritsFrom") Is Nothing Then CType(Json("versionInfo"), JObject).Add("inheritsFrom", Inherit)
                         If Json("versionInfo")("inheritsFrom") Is Nothing Then CType(Json("versionInfo"), JObject).Add("inheritsFrom", Inherit)
-                        WriteFile(VersionFolder & NewInstanceName & ".json", Json("versionInfo").ToString)
+                        FileUtils.Write(VersionFolder & NewInstanceName & ".json", Json("versionInfo").ToString)
                     End If
                     End If
                 Catch ex As Exception
                 Catch ex As Exception
                     Throw New Exception("非新版方式安装 Forge 失败", ex)
                     Throw New Exception("非新版方式安装 Forge 失败", ex)
@@ -1542,7 +1538,7 @@ Retry:
                 If RecommendedList.Count < 5 Then Throw New Exception("获取的推荐版本数过少(" & Result & ")")
                 If RecommendedList.Count < 5 Then Throw New Exception("获取的推荐版本数过少(" & Result & ")")
                 '保存
                 '保存
                 Dim CacheJson As String = "{" & RecommendedList.Join(",") & "}"
                 Dim CacheJson As String = "{" & RecommendedList.Join(",") & "}"
-                WriteFile(PathTemp & "Cache\ForgeRecommendedList.json", CacheJson)
+                FileUtils.Write(PathTemp & "Cache\ForgeRecommendedList.json", CacheJson)
                 Log("[Download] 刷新 Forge 推荐版本缓存成功")
                 Log("[Download] 刷新 Forge 推荐版本缓存成功")
             Catch ex As Exception
             Catch ex As Exception
                 Log(ex, "刷新 Forge 推荐版本缓存失败")
                 Log(ex, "刷新 Forge 推荐版本缓存失败")
@@ -1566,7 +1562,7 @@ Retry:
             If Json Is Nothing OrElse (Not If(McVersion, "").Contains(".")) OrElse Not Json.ContainsKey(McVersion) Then Return Nothing
             If Json Is Nothing OrElse (Not If(McVersion, "").Contains(".")) OrElse Not Json.ContainsKey(McVersion) Then Return Nothing
             Return If(Json(McVersion), "").ToString
             Return If(Json(McVersion), "").ToString
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "获取 Forge 推荐版本失败(" & If(McVersion, "null") & ")", LogLevel.Feedback)
+            Log(ex, "获取 Forge 推荐版本失败(" & If(McVersion, "null") & ")", NotifyLevel.MsgBoxAndFeedback)
             Return Nothing
             Return Nothing
         End Try
         End Try
     End Function
     End Function
@@ -1591,7 +1587,7 @@ Retry:
                 End If
                 End If
             Next
             Next
         Else
         Else
-            Log("[System] 未找到可用的 NeoForge 版本", LogLevel.Debug)
+            Log("[System] 未找到可用的 NeoForge 版本", NotifyLevel.DebugModeOnly)
         End If
         End If
         '显示各个版本
         '显示各个版本
         If FreshStableVersion IsNot Nothing Then
         If FreshStableVersion IsNot Nothing Then
@@ -1705,7 +1701,7 @@ Retry:
             FrmMain.BtnExtraDownload.Ribble()
             FrmMain.BtnExtraDownload.Ribble()
 
 
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "开始 Fabric 安装器下载失败", LogLevel.Feedback)
+            Log(ex, "开始 Fabric 安装器下载失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -1751,24 +1747,24 @@ Retry:
             Return NewItem
             Return NewItem
         End Function) With {.Height = 42}
         End Function) With {.Height = 42}
     End Function
     End Function
-    Public Function FabricApiDownloadListItem(Entry As CompFile, OnClick As MyListItem.ClickEventHandler) As MyVirtualizingElement(Of MyListItem)
+    Public Function FabricApiDownloadListItem(Entry As ResourceVersion, OnClick As MyListItem.ClickEventHandler) As MyVirtualizingElement(Of MyListItem)
         Return New MyVirtualizingElement(Of MyListItem)(
         Return New MyVirtualizingElement(Of MyListItem)(
         Function()
         Function()
             Dim NewItem As New MyListItem With {
             Dim NewItem As New MyListItem With {
-                .Title = Entry.DisplayName.Split("]")(1).Replace("Fabric API ", "").Replace(" build ", ".").BeforeFirst("+").Trim, .SnapsToDevicePixels = True, .Height = 42, .Type = MyListItem.CheckType.Clickable, .Tag = Entry,
-                .Info = Entry.StatusDescription & ",发布于 " & Entry.ReleaseDate.ToString("yyyy'/'MM'/'dd HH':'mm"),
+                .Title = Entry.Display.Split("]")(1).Replace("Fabric API ", "").Replace(" build ", ".").BeforeFirst("+").Trim, .SnapsToDevicePixels = True, .Height = 42, .Type = MyListItem.CheckType.Clickable, .Tag = Entry,
+                .Info = Entry.ReleaseTypeDisplay & ",发布于 " & Entry.ReleaseDate.ToString("yyyy'/'MM'/'dd HH':'mm"),
                 .Logo = PathImage & "Blocks/Fabric.png"
                 .Logo = PathImage & "Blocks/Fabric.png"
             }
             }
             AddHandler NewItem.Click, OnClick
             AddHandler NewItem.Click, OnClick
             Return NewItem
             Return NewItem
         End Function) With {.Height = 42}
         End Function) With {.Height = 42}
     End Function
     End Function
-    Public Function OptiFabricDownloadListItem(Entry As CompFile, OnClick As MyListItem.ClickEventHandler) As MyVirtualizingElement(Of MyListItem)
+    Public Function OptiFabricDownloadListItem(Entry As ResourceVersion, OnClick As MyListItem.ClickEventHandler) As MyVirtualizingElement(Of MyListItem)
         Return New MyVirtualizingElement(Of MyListItem)(
         Return New MyVirtualizingElement(Of MyListItem)(
         Function()
         Function()
             Dim NewItem As New MyListItem With {
             Dim NewItem As New MyListItem With {
-               .Title = Entry.DisplayName.Lower.Replace("optifabric-", "").Replace(".jar", "").Trim.TrimStart("v"), .SnapsToDevicePixels = True, .Height = 42, .Type = MyListItem.CheckType.Clickable, .Tag = Entry,
-               .Info = Entry.StatusDescription & ",发布于 " & Entry.ReleaseDate.ToString("yyyy'/'MM'/'dd HH':'mm"),
+               .Title = Entry.Display.Lower.Replace("optifabric-", "").Replace(".jar", "").Trim.TrimStart("v"), .SnapsToDevicePixels = True, .Height = 42, .Type = MyListItem.CheckType.Clickable, .Tag = Entry,
+               .Info = Entry.ReleaseTypeDisplay & ",发布于 " & Entry.ReleaseDate.ToString("yyyy'/'MM'/'dd HH':'mm"),
                .Logo = PathImage & "Blocks/OptiFabric.png"
                .Logo = PathImage & "Blocks/OptiFabric.png"
             }
             }
             AddHandler NewItem.Click, OnClick
             AddHandler NewItem.Click, OnClick
@@ -1841,12 +1837,12 @@ Retry:
         ''' <summary>
         ''' <summary>
         ''' 欲下载的 Fabric API 信息。
         ''' 欲下载的 Fabric API 信息。
         ''' </summary>
         ''' </summary>
-        Public FabricApi As CompFile = Nothing
+        Public FabricApi As ResourceVersion = Nothing
 
 
         ''' <summary>
         ''' <summary>
         ''' 欲下载的 OptiFabric 信息。
         ''' 欲下载的 OptiFabric 信息。
         ''' </summary>
         ''' </summary>
-        Public OptiFabric As CompFile = Nothing
+        Public OptiFabric As ResourceVersion = Nothing
 
 
         ''' <summary>
         ''' <summary>
         ''' 欲下载的 LiteLoader 详细信息。
         ''' 欲下载的 LiteLoader 详细信息。
@@ -1894,9 +1890,9 @@ Retry:
             If Loader.State = LoadState.Failed OrElse Loader.State = LoadState.Interrupted Then
             If Loader.State = LoadState.Failed OrElse Loader.State = LoadState.Interrupted Then
                 '删除版本文件夹
                 '删除版本文件夹
                 If Directory.Exists(Loader.Input & "saves\") OrElse Directory.Exists(Loader.Input & "versions\") Then
                 If Directory.Exists(Loader.Input & "saves\") OrElse Directory.Exists(Loader.Input & "versions\") Then
-                    Log("[Download] 由于版本已被独立启动,不清理版本文件夹:" & Loader.Input, LogLevel.Developer)
+                    Log("[Download] 由于版本已被独立启动,不清理版本文件夹:" & Loader.Input, NotifyLevel.DevelopOnly)
                 Else
                 Else
-                    Log("[Download] 由于下载失败或取消,清理版本文件夹:" & Loader.Input, LogLevel.Developer)
+                    Log("[Download] 由于下载失败或取消,清理版本文件夹:" & Loader.Input, NotifyLevel.DevelopOnly)
                     DeleteDirectory(Loader.Input)
                     DeleteDirectory(Loader.Input)
                 End If
                 End If
             End If
             End If
@@ -1924,7 +1920,7 @@ Retry:
         Catch ex As CancelledException
         Catch ex As CancelledException
             Return False
             Return False
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "开始合并安装失败", LogLevel.Feedback)
+            Log(ex, "开始合并安装失败", NotifyLevel.MsgBoxAndFeedback)
             Return False
             Return False
         End Try
         End Try
     End Function
     End Function
@@ -1945,9 +1941,9 @@ Retry:
             Request.OptiFineEntry = New DlOptiFineListEntry With {
             Request.OptiFineEntry = New DlOptiFineListEntry With {
                 .DisplayName = Request.MinecraftName & " " & Request.OptiFineVersion.Replace("HD_U_", "").Replace("_", "").Replace("pre", " pre"),
                 .DisplayName = Request.MinecraftName & " " & Request.OptiFineVersion.Replace("HD_U_", "").Replace("_", "").Replace("pre", " pre"),
                 .Inherit = Request.MinecraftName,
                 .Inherit = Request.MinecraftName,
-                .IsPreview = Request.OptiFineVersion.ContainsF("pre", True),
+                .IsPreview = Request.OptiFineVersion.ContainsIgnoreCase("pre"),
                 .InstanceName = Request.MinecraftName & "-OptiFine_" & Request.OptiFineVersion,
                 .InstanceName = Request.MinecraftName & "-OptiFine_" & Request.OptiFineVersion,
-                .FileName = If(Request.OptiFineVersion.ContainsF("pre", True), "preview_", "") &
+                .FileName = If(Request.OptiFineVersion.ContainsIgnoreCase("pre"), "preview_", "") &
                     "OptiFine_" & Request.MinecraftName & "_" & Request.OptiFineVersion & ".jar"
                     "OptiFine_" & Request.MinecraftName & "_" & Request.OptiFineVersion & ".jar"
             }
             }
         End If
         End If
@@ -1992,7 +1988,7 @@ Retry:
 
 
         Dim LoaderList As New List(Of LoaderBase)
         Dim LoaderList As New List(Of LoaderBase)
         '添加忽略标识
         '添加忽略标识
-        LoaderList.Add(New LoaderTask(Of Integer, Integer)("添加忽略标识", Sub() WriteFile(VersionFolder & ".pclignore", "用于临时地在 PCL 的版本列表中屏蔽此版本。")) With {.Show = False, .Block = False})
+        LoaderList.Add(New LoaderTask(Of Integer, Integer)("添加忽略标识", Sub() FileUtils.Write(VersionFolder & ".pclignore", "用于临时地在 PCL 的版本列表中屏蔽此版本。")) With {.Show = False, .Block = False})
         'Fabric API
         'Fabric API
         If Request.FabricApi IsNot Nothing Then
         If Request.FabricApi IsNot Nothing Then
             LoaderList.Add(New LoaderDownload("下载 Fabric API", New List(Of NetFile) From {Request.FabricApi.ToNetFile(ModsTempFolder)}) With {.ProgressWeight = 3, .Block = False})
             LoaderList.Add(New LoaderDownload("下载 Fabric API", New List(Of NetFile) From {Request.FabricApi.ToNetFile(ModsTempFolder)}) With {.ProgressWeight = 3, .Block = False})
@@ -2049,11 +2045,11 @@ Retry:
             If Directory.Exists(ModsTempFolder) Then
             If Directory.Exists(ModsTempFolder) Then
                 CopyDirectory(ModsTempFolder, ModsFolder)
                 CopyDirectory(ModsTempFolder, ModsFolder)
             ElseIf Modable Then
             ElseIf Modable Then
-                Directory.CreateDirectory(ModsFolder)
+                DirectoryUtils.Create(ModsFolder)
                 Log("[Download] 自动创建 Mod 文件夹:" & ModsFolder)
                 Log("[Download] 自动创建 Mod 文件夹:" & ModsFolder)
             End If
             End If
             Dim ResourcepacksFolder = New McInstance(VersionFolder).PathIndie & "resourcepacks\"
             Dim ResourcepacksFolder = New McInstance(VersionFolder).PathIndie & "resourcepacks\"
-            Directory.CreateDirectory(ResourcepacksFolder)
+            DirectoryUtils.Create(ResourcepacksFolder)
             Log("[Download] 自动创建资源包文件夹:" & ResourcepacksFolder)
             Log("[Download] 自动创建资源包文件夹:" & ResourcepacksFolder)
         End Sub) With {.ProgressWeight = 2, .Block = True})
         End Sub) With {.ProgressWeight = 2, .Block = True})
         '补全文件
         '补全文件
@@ -2079,7 +2075,7 @@ Retry:
             If(NeoForgeFolder IsNot Nothing, ",NeoForge:" & NeoForgeFolder, "") &
             If(NeoForgeFolder IsNot Nothing, ",NeoForge:" & NeoForgeFolder, "") &
             If(LiteLoaderFolder IsNot Nothing, ",LiteLoader:" & LiteLoaderFolder, "") &
             If(LiteLoaderFolder IsNot Nothing, ",LiteLoader:" & LiteLoaderFolder, "") &
             If(FabricFolder IsNot Nothing, ",Fabric:" & FabricFolder, ""))
             If(FabricFolder IsNot Nothing, ",Fabric:" & FabricFolder, ""))
-        Directory.CreateDirectory(OutputFolder)
+        DirectoryUtils.Create(OutputFolder)
 
 
         Dim HasOptiFine As Boolean = OptiFineFolder IsNot Nothing AndAlso Not OptiFineAsMod, HasForge As Boolean = ForgeFolder IsNot Nothing, HasNeoForge As Boolean = NeoForgeFolder IsNot Nothing, HasLiteLoader As Boolean = LiteLoaderFolder IsNot Nothing, HasFabric As Boolean = FabricFolder IsNot Nothing
         Dim HasOptiFine As Boolean = OptiFineFolder IsNot Nothing AndAlso Not OptiFineAsMod, HasForge As Boolean = ForgeFolder IsNot Nothing, HasNeoForge As Boolean = NeoForgeFolder IsNot Nothing, HasLiteLoader As Boolean = LiteLoaderFolder IsNot Nothing, HasFabric As Boolean = FabricFolder IsNot Nothing
         Dim OutputName As String, MinecraftName As String, OptiFineName As String, ForgeName As String, NeoForgeName As String, LiteLoaderName As String, FabricName As String
         Dim OutputName As String, MinecraftName As String, OptiFineName As String, ForgeName As String, NeoForgeName As String, LiteLoaderName As String, FabricName As String
@@ -2227,7 +2223,7 @@ Retry:
 #End Region
 #End Region
 
 
 #Region "保存"
 #Region "保存"
-        WriteFile(OutputJsonPath, OutputJson.ToString)
+        FileUtils.Write(OutputJsonPath, OutputJson.ToString)
         If MinecraftJar <> OutputJar Then '可能是同一个文件
         If MinecraftJar <> OutputJar Then '可能是同一个文件
             If File.Exists(OutputJar) Then File.Delete(OutputJar)
             If File.Exists(OutputJar) Then File.Delete(OutputJar)
             CopyFile(MinecraftJar, OutputJar)
             CopyFile(MinecraftJar, OutputJar)
@@ -2258,19 +2254,19 @@ Retry:
         Static Lock As New Object
         Static Lock As New Object
         SyncLock Lock '避免 OptiFine 和 Forge 安装时同时释放导致冲突
         SyncLock Lock '避免 OptiFine 和 Forge 安装时同时释放导致冲突
             Try
             Try
-                WriteFile(PatchPath, GetResources(Patch))
+                FileUtils.Write(PatchPath, GetResources(Patch))
             Catch ex As Exception
             Catch ex As Exception
                 If File.Exists(PatchPath) Then
                 If File.Exists(PatchPath) Then
                     '因为未知原因可能变为只读文件(#4243)
                     '因为未知原因可能变为只读文件(#4243)
-                    Log(ex, $"{Patch} 文件释放失败,但文件已存在,将在删除后尝试重新生成", LogLevel.Developer)
+                    Log(ex, $"{Patch} 文件释放失败,但文件已存在,将在删除后尝试重新生成", NotifyLevel.DevelopOnly)
                     Try
                     Try
                         File.Delete(PatchPath)
                         File.Delete(PatchPath)
-                        WriteFile(PatchPath, GetResources(Patch))
+                        FileUtils.Write(PatchPath, GetResources(Patch))
                     Catch ex2 As Exception
                     Catch ex2 As Exception
-                        Log(ex2, $"{Patch} 文件重新释放失败,将尝试更换文件名重新生成", LogLevel.Developer)
+                        Log(ex2, $"{Patch} 文件重新释放失败,将尝试更换文件名重新生成", NotifyLevel.DevelopOnly)
                         PatchPath = $"{PathPure}{Patch}2.jar"
                         PatchPath = $"{PathPure}{Patch}2.jar"
                         Try
                         Try
-                            WriteFile(PatchPath, GetResources(Patch))
+                            FileUtils.Write(PatchPath, GetResources(Patch))
                         Catch ex3 As Exception
                         Catch ex3 As Exception
                             Throw New FileNotFoundException($"释放 {Patch} 最终尝试失败", ex3)
                             Throw New FileNotFoundException($"释放 {Patch} 最终尝试失败", ex3)
                         End Try
                         End Try

+ 22 - 20
Plain Craft Launcher 2/Pages/PageDownload/PageDownloadInstall.xaml.vb

@@ -171,8 +171,8 @@
     Private SelectedForge As DlForgeVersionEntry = Nothing
     Private SelectedForge As DlForgeVersionEntry = Nothing
     Private SelectedNeoForge As DlNeoForgeListEntry = Nothing
     Private SelectedNeoForge As DlNeoForgeListEntry = Nothing
     Private SelectedFabric As String = Nothing
     Private SelectedFabric As String = Nothing
-    Private SelectedFabricApi As CompFile = Nothing
-    Private SelectedOptiFabric As CompFile = Nothing
+    Private SelectedFabricApi As ResourceVersion = Nothing
+    Private SelectedOptiFabric As ResourceVersion = Nothing
 
 
     ''' <summary>
     ''' <summary>
     ''' 重载已选择的项目的显示。
     ''' 重载已选择的项目的显示。
@@ -301,7 +301,7 @@
             Else
             Else
                 BtnFabricApiClear.Visibility = Visibility.Visible
                 BtnFabricApiClear.Visibility = Visibility.Visible
                 ImgFabricApi.Visibility = Visibility.Visible
                 ImgFabricApi.Visibility = Visibility.Visible
-                LabFabricApi.Text = SelectedFabricApi.DisplayName.Split("]")(1).Replace("Fabric API ", "").Replace(" build ", ".").Split("+").First.Trim
+                LabFabricApi.Text = SelectedFabricApi.Display.Split("]")(1).Replace("Fabric API ", "").Replace(" build ", ".").Split("+").First.Trim
                 LabFabricApi.Foreground = ColorGray1
                 LabFabricApi.Foreground = ColorGray1
             End If
             End If
         End If
         End If
@@ -322,7 +322,7 @@
             Else
             Else
                 BtnOptiFabricClear.Visibility = Visibility.Visible
                 BtnOptiFabricClear.Visibility = Visibility.Visible
                 ImgOptiFabric.Visibility = Visibility.Visible
                 ImgOptiFabric.Visibility = Visibility.Visible
-                LabOptiFabric.Text = SelectedOptiFabric.DisplayName.Lower.Replace("optifabric-", "").Replace(".jar", "").Trim.TrimStart("v")
+                LabOptiFabric.Text = SelectedOptiFabric.Display.Lower.Replace("optifabric-", "").Replace(".jar", "").Trim.TrimStart("v")
                 LabOptiFabric.Foreground = ColorGray1
                 LabOptiFabric.Foreground = ColorGray1
             End If
             End If
         End If
         End If
@@ -531,9 +531,11 @@
                 Dim Item = McDownloadListItem(Version, Sub()
                 Dim Item = McDownloadListItem(Version, Sub()
                                                        End Sub, False).Init()
                                                        End Sub, False).Init()
                 MinecraftSelected(Item, Nothing)
                 MinecraftSelected(Item, Nothing)
+                VersionWaitingSelect = Nothing
+                Return
             Next
             Next
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "可视化安装版本列表出错", LogLevel.Feedback)
+            Log(ex, "可视化安装版本列表出错", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
     ''' <summary>
     ''' <summary>
@@ -625,7 +627,7 @@
                 PanOptiFine.Children.Add(OptiFineDownloadListItem(Version, AddressOf OptiFine_Selected, False))
                 PanOptiFine.Children.Add(OptiFineDownloadListItem(Version, AddressOf OptiFine_Selected, False))
             Next
             Next
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "可视化 OptiFine 安装版本列表出错", LogLevel.Feedback)
+            Log(ex, "可视化 OptiFine 安装版本列表出错", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -687,7 +689,7 @@
                 PanLiteLoader.Children.Add(LiteLoaderDownloadListItem(Version, AddressOf LiteLoader_Selected, False))
                 PanLiteLoader.Children.Add(LiteLoaderDownloadListItem(Version, AddressOf LiteLoader_Selected, False))
             Next
             Next
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "可视化 LiteLoader 安装版本列表出错", LogLevel.Feedback)
+            Log(ex, "可视化 LiteLoader 安装版本列表出错", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -761,7 +763,7 @@
                 PanForge.Children.Add(ForgeDownloadListItem(Version, AddressOf Forge_Selected, False))
                 PanForge.Children.Add(ForgeDownloadListItem(Version, AddressOf Forge_Selected, False))
             Next
             Next
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "可视化 Forge 安装版本列表出错", LogLevel.Feedback)
+            Log(ex, "可视化 Forge 安装版本列表出错", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -819,7 +821,7 @@
                 PanNeoForge.Children.Add(NeoForgeDownloadListItem(Version, AddressOf NeoForge_Selected, False))
                 PanNeoForge.Children.Add(NeoForgeDownloadListItem(Version, AddressOf NeoForge_Selected, False))
             Next
             Next
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "可视化 NeoForge 安装版本列表出错", LogLevel.Feedback)
+            Log(ex, "可视化 NeoForge 安装版本列表出错", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -881,7 +883,7 @@
             CardFabric.SwapControl = PanFabric
             CardFabric.SwapControl = PanFabric
             CardFabric.SwapType = 12
             CardFabric.SwapType = 12
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "可视化 Fabric 安装版本列表出错", LogLevel.Feedback)
+            Log(ex, "可视化 Fabric 安装版本列表出错", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -911,8 +913,8 @@
     ''' <summary>
     ''' <summary>
     ''' 判断某 Fabric API 是否适配当前选择的原版版本。
     ''' 判断某 Fabric API 是否适配当前选择的原版版本。
     ''' </summary>
     ''' </summary>
-    Public Function IsFabricApiCompatible(FabricApi As CompFile) As Boolean
-        Dim FabricApiName = FabricApi.DisplayName
+    Public Function IsFabricApiCompatible(FabricApi As ResourceVersion) As Boolean
+        Dim FabricApiName = FabricApi.Display
         Try
         Try
             If FabricApiName Is Nothing OrElse VanillaName Is Nothing Then Return False
             If FabricApiName Is Nothing OrElse VanillaName Is Nothing Then Return False
             FabricApiName = FabricApiName.Lower
             FabricApiName = FabricApiName.Lower
@@ -978,12 +980,12 @@
             If DlFabricApiLoader.State <> LoadState.Finished Then Return
             If DlFabricApiLoader.State <> LoadState.Finished Then Return
             If VanillaName Is Nothing OrElse SelectedFabric Is Nothing Then Return
             If VanillaName Is Nothing OrElse SelectedFabric Is Nothing Then Return
             '获取版本列表
             '获取版本列表
-            Dim Versions As New List(Of CompFile)
+            Dim Versions As New List(Of ResourceVersion)
             For Each Version In DlFabricApiLoader.Output
             For Each Version In DlFabricApiLoader.Output
                 If IsFabricApiCompatible(Version) Then
                 If IsFabricApiCompatible(Version) Then
-                    If Not Version.DisplayName.StartsWithF("[") Then
-                        Log("[Download] 已特判修改 Fabric API 显示名:" & Version.DisplayName, LogLevel.Debug)
-                        Version.DisplayName = "[" & VanillaName & "] " & Version.DisplayName
+                    If Not Version.Display.StartsWithF("[") Then
+                        Log("[Download] 已特判修改 Fabric API 显示名:" & Version.Display, NotifyLevel.DebugModeOnly)
+                        Version.Display = "[" & VanillaName & "] " & Version.Display
                     End If
                     End If
                     Versions.Add(Version)
                     Versions.Add(Version)
                 End If
                 End If
@@ -1004,7 +1006,7 @@
                 FabricApi_Selected(Item, Nothing)
                 FabricApi_Selected(Item, Nothing)
             End If
             End If
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "可视化 Fabric API 安装版本列表出错", LogLevel.Feedback)
+            Log(ex, "可视化 Fabric API 安装版本列表出错", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -1028,7 +1030,7 @@
     ''' <summary>
     ''' <summary>
     ''' 判断某 OptiFabric 是否适配当前选择的原版版本。
     ''' 判断某 OptiFabric 是否适配当前选择的原版版本。
     ''' </summary>
     ''' </summary>
-    Private Function IsOptiFabricCompatible(ModFile As CompFile) As Boolean
+    Private Function IsOptiFabricCompatible(ModFile As ResourceVersion) As Boolean
         Try
         Try
             If VanillaName Is Nothing Then Return False
             If VanillaName Is Nothing Then Return False
             Return ModFile.GameVersions.Contains(VanillaName)
             Return ModFile.GameVersions.Contains(VanillaName)
@@ -1076,7 +1078,7 @@
             If DlOptiFabricLoader.State <> LoadState.Finished Then Return
             If DlOptiFabricLoader.State <> LoadState.Finished Then Return
             If VanillaName Is Nothing OrElse SelectedFabric Is Nothing OrElse SelectedOptiFine Is Nothing Then Return
             If VanillaName Is Nothing OrElse SelectedFabric Is Nothing OrElse SelectedOptiFine Is Nothing Then Return
             '获取版本列表
             '获取版本列表
-            Dim Versions As New List(Of CompFile)
+            Dim Versions As New List(Of ResourceVersion)
             For Each Version In DlOptiFabricLoader.Output
             For Each Version In DlOptiFabricLoader.Output
                 If IsOptiFabricCompatible(Version) Then Versions.Add(Version)
                 If IsOptiFabricCompatible(Version) Then Versions.Add(Version)
             Next
             Next
@@ -1096,7 +1098,7 @@
             Log($"[Download] 已自动选择 OptiFabric:{Item.Title}")
             Log($"[Download] 已自动选择 OptiFabric:{Item.Title}")
             OptiFabric_Selected(Item, Nothing)
             OptiFabric_Selected(Item, Nothing)
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "可视化 OptiFabric 安装版本列表出错", LogLevel.Feedback)
+            Log(ex, "可视化 OptiFabric 安装版本列表出错", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
 
 

+ 8 - 16
Plain Craft Launcher 2/Pages/PageDownload/PageDownloadLeft.xaml.vb

@@ -53,7 +53,7 @@
             PageChangeRun(PageGet(ID))
             PageChangeRun(PageGet(ID))
             PageID = ID
             PageID = ID
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "切换分页面失败(ID " & ID & ")", LogLevel.Feedback)
+            Log(ex, "切换分页面失败(ID " & ID & ")", NotifyLevel.MsgBoxAndFeedback)
         Finally
         Finally
             AniControlEnabled -= 1
             AniControlEnabled -= 1
         End Try
         End Try
@@ -89,6 +89,8 @@
         Refresh(FrmMain.PageCurrentSub)
         Refresh(FrmMain.PageCurrentSub)
     End Sub
     End Sub
     Public Sub Refresh(SubType As FormMain.PageSubType)
     Public Sub Refresh(SubType As FormMain.PageSubType)
+        ResourceProject.Cache.Clear()
+        ResourceVersion.ProjectFilesCache.Clear()
         Select Case SubType
         Select Case SubType
             Case FormMain.PageSubType.DownloadInstall
             Case FormMain.PageSubType.DownloadInstall
                 DlClientListLoader.Start(IsForceRestart:=True)
                 DlClientListLoader.Start(IsForceRestart:=True)
@@ -101,46 +103,36 @@
                 DlOptiFabricLoader.Start(IsForceRestart:=True)
                 DlOptiFabricLoader.Start(IsForceRestart:=True)
                 ItemInstall.Checked = True
                 ItemInstall.Checked = True
             Case FormMain.PageSubType.DownloadMod
             Case FormMain.PageSubType.DownloadMod
-                CompProjectCache.Clear()
-                CompFilesCache.Clear()
                 If FrmDownloadMod IsNot Nothing Then
                 If FrmDownloadMod IsNot Nothing Then
-                    FrmDownloadMod.Content.Storage = New CompProjectStorage
+                    FrmDownloadMod.Content.Storage = New ResourceSearcher.SearchResult
                     FrmDownloadMod.Content.Page = 0
                     FrmDownloadMod.Content.Page = 0
                     FrmDownloadMod.PageLoaderRestart()
                     FrmDownloadMod.PageLoaderRestart()
                 End If
                 End If
                 ItemMod.Checked = True
                 ItemMod.Checked = True
             Case FormMain.PageSubType.DownloadPack
             Case FormMain.PageSubType.DownloadPack
-                CompProjectCache.Clear()
-                CompFilesCache.Clear()
                 If FrmDownloadPack IsNot Nothing Then
                 If FrmDownloadPack IsNot Nothing Then
-                    FrmDownloadPack.Content.Storage = New CompProjectStorage
+                    FrmDownloadPack.Content.Storage = New ResourceSearcher.SearchResult
                     FrmDownloadPack.Content.Page = 0
                     FrmDownloadPack.Content.Page = 0
                     FrmDownloadPack.PageLoaderRestart()
                     FrmDownloadPack.PageLoaderRestart()
                 End If
                 End If
                 ItemPack.Checked = True
                 ItemPack.Checked = True
             Case FormMain.PageSubType.DownloadResourcePack
             Case FormMain.PageSubType.DownloadResourcePack
-                CompProjectCache.Clear()
-                CompFilesCache.Clear()
                 If FrmDownloadResourcePack IsNot Nothing Then
                 If FrmDownloadResourcePack IsNot Nothing Then
-                    FrmDownloadResourcePack.Content.Storage = New CompProjectStorage
+                    FrmDownloadResourcePack.Content.Storage = New ResourceSearcher.SearchResult
                     FrmDownloadResourcePack.Content.Page = 0
                     FrmDownloadResourcePack.Content.Page = 0
                     FrmDownloadResourcePack.PageLoaderRestart()
                     FrmDownloadResourcePack.PageLoaderRestart()
                 End If
                 End If
                 ItemResourcePack.Checked = True
                 ItemResourcePack.Checked = True
             Case FormMain.PageSubType.DownloadShader
             Case FormMain.PageSubType.DownloadShader
-                CompProjectCache.Clear()
-                CompFilesCache.Clear()
                 If FrmDownloadShader IsNot Nothing Then
                 If FrmDownloadShader IsNot Nothing Then
-                    FrmDownloadShader.Content.Storage = New CompProjectStorage
+                    FrmDownloadShader.Content.Storage = New ResourceSearcher.SearchResult
                     FrmDownloadShader.Content.Page = 0
                     FrmDownloadShader.Content.Page = 0
                     FrmDownloadShader.PageLoaderRestart()
                     FrmDownloadShader.PageLoaderRestart()
                 End If
                 End If
                 ItemShader.Checked = True
                 ItemShader.Checked = True
             Case FormMain.PageSubType.DownloadDataPack
             Case FormMain.PageSubType.DownloadDataPack
-                CompProjectCache.Clear()
-                CompFilesCache.Clear()
                 If FrmDownloadDataPack IsNot Nothing Then
                 If FrmDownloadDataPack IsNot Nothing Then
-                    FrmDownloadDataPack.Content.Storage = New CompProjectStorage
+                    FrmDownloadDataPack.Content.Storage = New ResourceSearcher.SearchResult
                     FrmDownloadDataPack.Content.Page = 0
                     FrmDownloadDataPack.Content.Page = 0
                     FrmDownloadDataPack.PageLoaderRestart()
                     FrmDownloadDataPack.PageLoaderRestart()
                 End If
                 End If

+ 4 - 4
Plain Craft Launcher 2/Pages/PageDownload/Comp/MyCompItem.xaml → Plain Craft Launcher 2/Pages/PageDownload/Resource/MyResourceItem.xaml

@@ -1,4 +1,4 @@
-<Grid x:Class="MyCompItem"
+<Grid x:Class="MyResourceItem"
              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:local="clr-namespace:PCL"
              xmlns:local="clr-namespace:PCL"
@@ -52,7 +52,7 @@
           IsHitTestVisible="False" SnapsToDevicePixels="False" UseLayoutRounding="False">
           IsHitTestVisible="False" SnapsToDevicePixels="False" UseLayoutRounding="False">
         <Grid.ColumnDefinitions>
         <Grid.ColumnDefinitions>
             <ColumnDefinition x:Name="ColumnVersion1" Width="Auto" />
             <ColumnDefinition x:Name="ColumnVersion1" Width="Auto" />
-            <ColumnDefinition x:Name="ColumnVersion2" Width="Auto" SharedSizeGroup="CompItemVersion" />
+            <ColumnDefinition x:Name="ColumnVersion2" Width="Auto" SharedSizeGroup="ItemVersion" />
             <ColumnDefinition x:Name="ColumnVersion3" Width="0.7*" />
             <ColumnDefinition x:Name="ColumnVersion3" Width="0.7*" />
             
             
             <ColumnDefinition Width="Auto" />
             <ColumnDefinition Width="Auto" />
@@ -60,11 +60,11 @@
             <ColumnDefinition Width="1*" />
             <ColumnDefinition Width="1*" />
 
 
             <ColumnDefinition x:Name="ColumnTime1"  Width="Auto" />
             <ColumnDefinition x:Name="ColumnTime1"  Width="Auto" />
-            <ColumnDefinition x:Name="ColumnTime2" Width="Auto" MinWidth="57" SharedSizeGroup="CompItemTime" />
+            <ColumnDefinition x:Name="ColumnTime2" Width="Auto" MinWidth="57" SharedSizeGroup="ItemTime" />
             <ColumnDefinition x:Name="ColumnTime3" Width="1*" />
             <ColumnDefinition x:Name="ColumnTime3" Width="1*" />
 
 
             <ColumnDefinition Width="Auto" />
             <ColumnDefinition Width="Auto" />
-            <ColumnDefinition Width="Auto" SharedSizeGroup="CompItemSource" />
+            <ColumnDefinition Width="Auto" SharedSizeGroup="ItemSource" />
             <ColumnDefinition Width="1.7*" />
             <ColumnDefinition Width="1.7*" />
         </Grid.ColumnDefinitions>
         </Grid.ColumnDefinitions>
         <Path x:Name="PathVersion" Grid.Column="0" Fill="{StaticResource ColorBrushGray3}" Stretch="Uniform" Height="11"
         <Path x:Name="PathVersion" Grid.Column="0" Fill="{StaticResource ColorBrushGray3}" Stretch="Uniform" Height="11"

+ 17 - 17
Plain Craft Launcher 2/Pages/PageDownload/Comp/MyCompItem.xaml.vb → Plain Craft Launcher 2/Pages/PageDownload/Resource/MyResourceItem.xaml.vb

@@ -1,4 +1,4 @@
-Public Class MyCompItem
+Public Class MyResourceItem
 
 
 #Region "基础属性"
 #Region "基础属性"
     Public Uuid As Integer = GetUuid()
     Public Uuid As Integer = GetUuid()
@@ -82,47 +82,47 @@
             Log("[Control] 按下资源工程列表项:" & LabTitle.Text)
             Log("[Control] 按下资源工程列表项:" & LabTitle.Text)
         End If
         End If
     End Sub
     End Sub
-    Private Sub MyCompItem_Click(sender As MyCompItem, e As EventArgs) Handles Me.Click
+    Private Sub MyResourceItem_Click(sender As MyResourceItem, e As EventArgs) Handles Me.Click
         '记录当前展开的卡片标题(#2712)
         '记录当前展开的卡片标题(#2712)
         Dim Titles As New List(Of String)
         Dim Titles As New List(Of String)
-        If FrmMain.PageCurrent.Page = FormMain.PageType.CompDetail Then
-            For Each Card As MyCard In FrmDownloadCompDetail.PanResults.Children
+        If FrmMain.PageCurrent.Page = FormMain.PageType.ResourceDetail Then
+            For Each Card As MyCard In FrmDownloadResourceDetail.PanResults.Children
                 If Card.Title <> "" AndAlso Not Card.IsSwapped Then Titles.Add(Card.Title)
                 If Card.Title <> "" AndAlso Not Card.IsSwapped Then Titles.Add(Card.Title)
             Next
             Next
-            Log("[Comp] 记录当前已展开的卡片:" & String.Join("、", Titles))
+            Log("[Resource] 记录当前已展开的卡片:" & String.Join("、", Titles))
             FrmMain.PageCurrent.Additional(1) = Titles
             FrmMain.PageCurrent.Additional(1) = Titles
         End If
         End If
         '打开详情页
         '打开详情页
-        Dim TargetType As CompType
+        Dim TargetType As ResourceTypes
         Dim TargetInstance As String = Nothing
         Dim TargetInstance As String = Nothing
-        Dim TargetLoader As CompModLoaderType = CompModLoaderType.Any
+        Dim TargetLoader As ModLoaderTypes = ModLoaderTypes.Any
         If FrmMain.PageCurrent.Page = FormMain.PageType.Download Then
         If FrmMain.PageCurrent.Page = FormMain.PageType.Download Then
             '从下载页进入
             '从下载页进入
             Select Case FrmMain.PageCurrentSub
             Select Case FrmMain.PageCurrentSub
                 Case FormMain.PageSubType.DownloadMod
                 Case FormMain.PageSubType.DownloadMod
-                    TargetType = CompType.Mod
+                    TargetType = ResourceTypes.Mod
                     TargetInstance = FrmDownloadMod.Content.Loader.Input.GameVersion
                     TargetInstance = FrmDownloadMod.Content.Loader.Input.GameVersion
                     TargetLoader = FrmDownloadMod.Content.Loader.Input.ModLoader
                     TargetLoader = FrmDownloadMod.Content.Loader.Input.ModLoader
                 Case FormMain.PageSubType.DownloadPack
                 Case FormMain.PageSubType.DownloadPack
-                    TargetType = CompType.ModPack
+                    TargetType = ResourceTypes.ModPack
                     TargetInstance = FrmDownloadPack.Content.Loader.Input.GameVersion
                     TargetInstance = FrmDownloadPack.Content.Loader.Input.GameVersion
                 Case FormMain.PageSubType.DownloadDataPack
                 Case FormMain.PageSubType.DownloadDataPack
-                    TargetType = CompType.DataPack
+                    TargetType = ResourceTypes.DataPack
                     TargetInstance = FrmDownloadDataPack.Content.Loader.Input.GameVersion
                     TargetInstance = FrmDownloadDataPack.Content.Loader.Input.GameVersion
                 Case FormMain.PageSubType.DownloadResourcePack
                 Case FormMain.PageSubType.DownloadResourcePack
-                    TargetType = CompType.ResourcePack
+                    TargetType = ResourceTypes.ResourcePack
                     TargetInstance = FrmDownloadResourcePack.Content.Loader.Input.GameVersion
                     TargetInstance = FrmDownloadResourcePack.Content.Loader.Input.GameVersion
                 Case FormMain.PageSubType.DownloadShader
                 Case FormMain.PageSubType.DownloadShader
-                    TargetType = CompType.Shader
+                    TargetType = ResourceTypes.Shader
                     TargetInstance = FrmDownloadShader.Content.Loader.Input.GameVersion
                     TargetInstance = FrmDownloadShader.Content.Loader.Input.GameVersion
             End Select
             End Select
         Else
         Else
             '从详情页进入(查看前置)
             '从详情页进入(查看前置)
-            TargetType = CompType.Any '允许任意类别
+            TargetType = ResourceTypes.Any '允许任意类别
             TargetInstance = FrmMain.PageCurrent.Additional(2)
             TargetInstance = FrmMain.PageCurrent.Additional(2)
             TargetLoader = FrmMain.PageCurrent.Additional(3)
             TargetLoader = FrmMain.PageCurrent.Additional(3)
         End If
         End If
-        FrmMain.PageChange(New FormMain.PageStackData With {.Page = FormMain.PageType.CompDetail,
+        FrmMain.PageChange(New FormMain.PageStackData With {.Page = FormMain.PageType.ResourceDetail,
                            .Additional = {sender.Tag, New List(Of String), TargetInstance, TargetLoader, TargetType}})
                            .Additional = {sender.Tag, New List(Of String), TargetInstance, TargetLoader, TargetType}})
     End Sub
     End Sub
 
 
@@ -171,7 +171,7 @@
 
 
     Private StateLast As String
     Private StateLast As String
     ''' <summary>
     ''' <summary>
-    ''' 是否允许交互。目前仅用于 PageDownloadCompDetail 的顶部栏展示:若关闭碰撞检测,则无法展开 Tooltip。
+    ''' 是否允许交互。目前仅用于 PageDownloadResourceDetail 的顶部栏展示:若关闭碰撞检测,则无法展开 Tooltip。
     ''' </summary>
     ''' </summary>
     Public Property CanInteraction As Boolean = True
     Public Property CanInteraction As Boolean = True
     Public Sub RefreshColor(sender As Object, e As EventArgs) Handles Me.MouseEnter, Me.MouseLeave, Me.MouseLeftButtonDown, Me.MouseLeftButtonUp
     Public Sub RefreshColor(sender As Object, e As EventArgs) Handles Me.MouseEnter, Me.MouseLeave, Me.MouseLeftButtonDown, Me.MouseLeftButtonUp
@@ -214,10 +214,10 @@
                     AaScaleTransform(RectBack, -0.196, 1,,, True)
                     AaScaleTransform(RectBack, -0.196, 1,,, True)
                 })
                 })
             End If
             End If
-            AniStart(Ani, "CompItem Color " & Uuid)
+            AniStart(Ani, "MyResourceItem Color " & Uuid)
         Else
         Else
             '无动画
             '无动画
-            AniStop("CompItem Color " & Uuid)
+            AniStop("MyResourceItem Color " & Uuid)
             If _RectBack IsNot Nothing Then RectBack.Opacity = 0
             If _RectBack IsNot Nothing Then RectBack.Opacity = 0
         End If
         End If
     End Sub
     End Sub

+ 1 - 1
Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadCompDetail.xaml → Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadCompDetail.xaml

@@ -2,7 +2,7 @@
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
     xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
-    mc:Ignorable="d" x:Class="PageDownloadCompDetail"
+    mc:Ignorable="d" x:Class="PageDownloadResourceDetail"
     PanScroll="PanBack">
     PanScroll="PanBack">
     <local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" x:Name="PanBack">
     <local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" x:Name="PanBack">
         <Grid Margin="25,25,25,10">
         <Grid Margin="25,25,25,10">

+ 116 - 91
Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadCompDetail.xaml.vb → Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadCompDetail.xaml.vb

@@ -1,21 +1,21 @@
-Public Class PageDownloadCompDetail
-    Private CompItem As MyCompItem = Nothing
+Public Class PageDownloadResourceDetail
+    Private ResourceItem As MyResourceItem = Nothing
 
 
 #Region "加载器"
 #Region "加载器"
 
 
-    Private CompFileLoader As New LoaderTask(Of Integer, List(Of CompFile))(
-        "Comp File",
+    Private ResourceFileLoader As New LoaderTask(Of Integer, List(Of ResourceVersion))(
+        "Resource File",
         Sub(Task)
         Sub(Task)
             LoadTargetFromAdditional()
             LoadTargetFromAdditional()
-            Dim Result = CompFilesGet(Project.Id, Project.FromCurseForge)
+            Dim Result = ResourceVersion.FromProjectId(Project.Id, Project.Platform)
             If Task.IsInterrupted Then Return
             If Task.IsInterrupted Then Return
             Task.Output = Result
             Task.Output = Result
         End Sub)
         End Sub)
 
 
     '初始化加载器信息
     '初始化加载器信息
-    Private Sub PageDownloadCompDetail_Inited(sender As Object, e As EventArgs) Handles Me.Initialized
+    Private Sub PageDownloadResourceDetail_Inited(sender As Object, e As EventArgs) Handles Me.Initialized
         LoadTargetFromAdditional()
         LoadTargetFromAdditional()
-        PageLoaderInit(Load, PanLoad, PanMain, CardIntro, CompFileLoader, AddressOf Load_OnFinish)
+        PageLoaderInit(Load, PanLoad, PanMain, CardIntro, ResourceFileLoader, AddressOf Load_OnFinish)
     End Sub
     End Sub
     Public Sub LoadTargetFromAdditional() Handles Me.Loaded
     Public Sub LoadTargetFromAdditional() Handles Me.Loaded
         Project = FrmMain.PageCurrent.Additional(0)
         Project = FrmMain.PageCurrent.Additional(0)
@@ -23,21 +23,21 @@
         TargetLoader = FrmMain.PageCurrent.Additional(3)
         TargetLoader = FrmMain.PageCurrent.Additional(3)
         TargetType = FrmMain.PageCurrent.Additional(4)
         TargetType = FrmMain.PageCurrent.Additional(4)
     End Sub
     End Sub
-    Private Project As CompProject
-    Private TargetInstance As String, TargetLoader As CompModLoaderType
+    Private Project As ResourceProject
+    Private TargetInstance As String, TargetLoader As ModLoaderTypes
     ''' <summary>
     ''' <summary>
     ''' 当前页面应展示的内容类别。
     ''' 当前页面应展示的内容类别。
     ''' 若当前页面是在查看前置时进入,则也可能为 Any。
     ''' 若当前页面是在查看前置时进入,则也可能为 Any。
     ''' </summary>
     ''' </summary>
-    Private TargetType As CompType
+    Private TargetType As ResourceTypes
     '自动重试
     '自动重试
     Private Sub Load_State(sender As Object, state As MyLoading.MyLoadingState, oldState As MyLoading.MyLoadingState) Handles Load.StateChanged
     Private Sub Load_State(sender As Object, state As MyLoading.MyLoadingState, oldState As MyLoading.MyLoadingState) Handles Load.StateChanged
-        Select Case CompFileLoader.State
+        Select Case ResourceFileLoader.State
             Case LoadState.Failed
             Case LoadState.Failed
                 Dim ErrorMessage As String = ""
                 Dim ErrorMessage As String = ""
-                If CompFileLoader.Error IsNot Nothing Then ErrorMessage = CompFileLoader.Error.Message
+                If ResourceFileLoader.Error IsNot Nothing Then ErrorMessage = ResourceFileLoader.Error.Message
                 If ErrorMessage.Contains("不是有效的 json 文件") Then
                 If ErrorMessage.Contains("不是有效的 json 文件") Then
-                    Log("[Comp] 下载的文件 json 列表损坏,已自动重试", LogLevel.Debug)
+                    Log("[Resource] 下载的文件 json 列表损坏,已自动重试", NotifyLevel.DebugModeOnly)
                     PageLoaderRestart()
                     PageLoaderRestart()
                 End If
                 End If
         End Select
         End Select
@@ -73,15 +73,15 @@
     Private GroupedDrop As Boolean '是否按 Drop 筛选(1.21 / 1.20 / 1.19 / ...)而非小版本号(1.21.1 / 1.21 / 1.20.4 / ...)
     Private GroupedDrop As Boolean '是否按 Drop 筛选(1.21 / 1.20 / 1.19 / ...)而非小版本号(1.21.1 / 1.21 / 1.20.4 / ...)
     Private GroupedOld As Boolean '是否折叠远古版本为一个选项
     Private GroupedOld As Boolean '是否折叠远古版本为一个选项
     '筛选类型相同的结果(Modrinth 会返回 Mod、服务端插件、数据包混合的列表)
     '筛选类型相同的结果(Modrinth 会返回 Mod、服务端插件、数据包混合的列表)
-    Private Function GetResults() As List(Of CompFile)
-        Dim Results As List(Of CompFile) = CompFileLoader.Output
+    Private Function GetResults() As List(Of ResourceVersion)
+        Dim Results As List(Of ResourceVersion) = ResourceFileLoader.Output
         Select Case TargetType
         Select Case TargetType
-            Case CompType.Any
-                Results = Results.Where(Function(r) r.Type <> CompType.Plugin).ToList
-            Case CompType.Shader, CompType.ResourcePack
+            Case ResourceTypes.Any
+                Results = Results.Where(Function(r) r.ResourceType <> ResourceTypes.Plugin).ToList
+            Case ResourceTypes.Shader, ResourceTypes.ResourcePack
                 '不筛选光影和资源包,否则原版光影会因为是资源包格式而被过滤(#6473)
                 '不筛选光影和资源包,否则原版光影会因为是资源包格式而被过滤(#6473)
             Case Else
             Case Else
-                Results = Results.Where(Function(r) r.Type = TargetType).ToList
+                Results = Results.Where(Function(r) r.ResourceType = TargetType).ToList
         End Select
         End Select
         Return Results
         Return Results
     End Function
     End Function
@@ -153,13 +153,13 @@ GroupDone:
     Private Sub UpdateFilterResult()
     Private Sub UpdateFilterResult()
         Dim Results = GetResults()
         Dim Results = GetResults()
 
 
-        Dim TargetCardName As String = If(TargetInstance <> "" OrElse TargetLoader <> CompModLoaderType.Any,
-            $"{If(TargetLoader <> CompModLoaderType.Any, TargetLoader.ToString & " ", "")}{TargetInstance}(所选版本)", "")
+        Dim TargetCardName As String = If(TargetInstance <> "" OrElse TargetLoader <> ModLoaderTypes.Any,
+            $"{If(TargetLoader <> ModLoaderTypes.Any, TargetLoader.ToString & " ", "")}{TargetInstance}(所选版本)", "")
         '归类到卡片下
         '归类到卡片下
-        Dim Dict As New SortedDictionary(Of String, List(Of CompFile))(New CardSorter(TargetCardName))
-        Dict.Add("其他", New List(Of CompFile))
-        Dim SupportedLoaders As New List(Of Integer)([Enum].GetValues(GetType(CompModLoaderType)))
-        For Each Version As CompFile In Results
+        Dim Dict As New SortedDictionary(Of String, List(Of ResourceVersion))(New CardSorter(TargetCardName))
+        Dict.Add("其他", New List(Of ResourceVersion))
+        Dim SupportedLoaders As New List(Of Integer)([Enum].GetValues(GetType(ModLoaderTypes)))
+        For Each Version As ResourceVersion In Results
             For Each GameVersion In Version.GameVersions
             For Each GameVersion In Version.GameVersions
                 '检查是否符合版本筛选器
                 '检查是否符合版本筛选器
                 If VersionFilter IsNot Nothing AndAlso
                 If VersionFilter IsNot Nothing AndAlso
@@ -168,11 +168,11 @@ GroupDone:
                 Dim VerName As String = GetGroupedVersionName(GameVersion, False, False)
                 Dim VerName As String = GetGroupedVersionName(GameVersion, False, False)
                 '遍历加入的加载器列表
                 '遍历加入的加载器列表
                 Dim Loaders As New List(Of String)
                 Dim Loaders As New List(Of String)
-                If Project.ModLoaders.Count > 1 AndAlso '工程至少有两个加载器
-                    Version.Type = CompType.Mod AndAlso '是 Mod
+                If Project.Loaders.Count > 1 AndAlso '工程至少有两个加载器
+                    Version.ResourceType = ResourceTypes.Mod AndAlso '是 Mod
                     McVersion.IsFormatFit(VerName) Then '不是 “快照版本” 之类的
                     McVersion.IsFormatFit(VerName) Then '不是 “快照版本” 之类的
-                    For Each Loader In Version.ModLoaders
-                        If Loader = CompModLoaderType.Quilt AndAlso Settings.Get("ToolDownloadIgnoreQuilt") Then Continue For
+                    For Each Loader In Version.Loaders
+                        If Loader = ModLoaderTypes.Quilt AndAlso Settings.Get("ToolDownloadIgnoreQuilt") Then Continue For
                         If SupportedLoaders.Contains(Loader) Then Loaders.Add(Loader.ToString & " ")
                         If SupportedLoaders.Contains(Loader) Then Loaders.Add(Loader.ToString & " ")
                     Next
                     Next
                 End If
                 End If
@@ -180,17 +180,17 @@ GroupDone:
                 '实际添加
                 '实际添加
                 For Each Loader In Loaders
                 For Each Loader In Loaders
                     Dim TargetCard As String = Loader & VerName
                     Dim TargetCard As String = Loader & VerName
-                    If Not Dict.ContainsKey(TargetCard) Then Dict.Add(TargetCard, New List(Of CompFile))
+                    If Not Dict.ContainsKey(TargetCard) Then Dict.Add(TargetCard, New List(Of ResourceVersion))
                     If Not Dict(TargetCard).Contains(Version) Then Dict(TargetCard).Add(Version)
                     If Not Dict(TargetCard).Contains(Version) Then Dict(TargetCard).Add(Version)
                 Next
                 Next
             Next
             Next
         Next
         Next
         '添加筛选的版本的卡片
         '添加筛选的版本的卡片
         If TargetCardName <> "" AndAlso (VersionFilter Is Nothing OrElse GetGroupedVersionName(TargetInstance, GroupedDrop, GroupedOld).StartsWithF(VersionFilter)) Then
         If TargetCardName <> "" AndAlso (VersionFilter Is Nothing OrElse GetGroupedVersionName(TargetInstance, GroupedDrop, GroupedOld).StartsWithF(VersionFilter)) Then
-            Dict.Add(TargetCardName, New List(Of CompFile))
-            For Each Version As CompFile In Results
+            Dict.Add(TargetCardName, New List(Of ResourceVersion))
+            For Each Version As ResourceVersion In Results
                 If Version.GameVersions.Contains(TargetInstance) AndAlso
                 If Version.GameVersions.Contains(TargetInstance) AndAlso
-                   (TargetLoader = CompModLoaderType.Any OrElse Version.ModLoaders.Contains(TargetLoader)) Then
+                   (TargetLoader = ModLoaderTypes.Any OrElse Version.Loaders.Contains(TargetLoader)) Then
                     '检查是否符合版本筛选器
                     '检查是否符合版本筛选器
                     If VersionFilter IsNot Nothing AndAlso
                     If VersionFilter IsNot Nothing AndAlso
                         Not Version.GameVersions.Any(Function(v) GetGroupedVersionName(v, GroupedDrop, GroupedOld) = VersionFilter) Then Continue For
                         Not Version.GameVersions.Any(Function(v) GetGroupedVersionName(v, GroupedDrop, GroupedOld) = VersionFilter) Then Continue For
@@ -201,11 +201,11 @@ GroupDone:
         '转化为 UI
         '转化为 UI
         Try
         Try
             PanResults.Children.Clear()
             PanResults.Children.Clear()
-            For Each Pair As KeyValuePair(Of String, List(Of CompFile)) In Dict
+            For Each Pair As KeyValuePair(Of String, List(Of ResourceVersion)) In Dict
                 If Not Pair.Value.Any() Then Continue For
                 If Not Pair.Value.Any() Then Continue For
                 If Pair.Key = TargetCardName.Replace("(所选版本)", "") Then Continue For
                 If Pair.Key = TargetCardName.Replace("(所选版本)", "") Then Continue For
                 '增加卡片
                 '增加卡片
-                Dim NewCard As New MyCard With {.Title = Pair.Key, .Margin = New Thickness(0, 0, 0, 15), .SwapType = If(TargetType = CompType.ModPack, 9, 8)} '9 是安装,8 是另存为
+                Dim NewCard As New MyCard With {.Title = Pair.Key, .Margin = New Thickness(0, 0, 0, 15), .SwapType = If(TargetType = ResourceTypes.ModPack, 9, 8)} '9 是安装,8 是另存为
                 Dim NewStack As New StackPanel With {.Margin = New Thickness(20, MyCard.SwapedHeight, 18, 0), .VerticalAlignment = VerticalAlignment.Top, .RenderTransform = New TranslateTransform(0, 0), .Tag = Pair.Value}
                 Dim NewStack As New StackPanel With {.Margin = New Thickness(20, MyCard.SwapedHeight, 18, 0), .VerticalAlignment = VerticalAlignment.Top, .RenderTransform = New TranslateTransform(0, 0), .Tag = Pair.Value}
                 NewCard.Children.Add(NewStack)
                 NewCard.Children.Add(NewStack)
                 NewCard.SwapControl = NewStack
                 NewCard.SwapControl = NewStack
@@ -214,7 +214,7 @@ GroupDone:
                 If Pair.Key = TargetCardName OrElse
                 If Pair.Key = TargetCardName OrElse
                    (FrmMain.PageCurrent.Additional IsNot Nothing AndAlso '#2761
                    (FrmMain.PageCurrent.Additional IsNot Nothing AndAlso '#2761
                    CType(FrmMain.PageCurrent.Additional(1), List(Of String)).Contains(NewCard.Title)) Then
                    CType(FrmMain.PageCurrent.Additional(1), List(Of String)).Contains(NewCard.Title)) Then
-                    MyCard.StackInstall(NewStack, If(TargetType = CompType.ModPack, 9, 8), Pair.Key) '9 是安装,8 是另存为
+                    MyCard.StackInstall(NewStack, If(TargetType = ResourceTypes.ModPack, 9, 8), Pair.Key) '9 是安装,8 是另存为
                 Else
                 Else
                     NewCard.IsSwapped = True
                     NewCard.IsSwapped = True
                 End If
                 End If
@@ -228,15 +228,15 @@ GroupDone:
                 CType(PanResults.Children(0), MyCard).IsSwapped = False
                 CType(PanResults.Children(0), MyCard).IsSwapped = False
             End If
             End If
             '替代提示
             '替代提示
-            If Project.Types = CompType.ModOrDataPack AndAlso (TargetType = CompType.Mod OrElse TargetType = CompType.DataPack) Then
+            If Project.Types = ResourceTypes.ModOrDataPack AndAlso (TargetType = ResourceTypes.Mod OrElse TargetType = ResourceTypes.DataPack) Then
                 HintAlternative.Visibility = Visibility.Visible
                 HintAlternative.Visibility = Visibility.Visible
-                HintAlternative.Text = If(TargetType = CompType.Mod,
+                HintAlternative.Text = If(TargetType = ResourceTypes.Mod,
                     "以下是该项目的 Mod 版本。点击这里查看其数据包版本。", "以下是该项目的数据包版本。点击这里查看其 Mod 版本。")
                     "以下是该项目的 Mod 版本。点击这里查看其数据包版本。", "以下是该项目的数据包版本。点击这里查看其 Mod 版本。")
             Else
             Else
                 HintAlternative.Visibility = Visibility.Collapsed
                 HintAlternative.Visibility = Visibility.Collapsed
             End If
             End If
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "可视化工程下载列表出错", LogLevel.Feedback)
+            Log(ex, "可视化工程下载列表出错", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
     Private Function GetGroupedVersionName(Name As String, GroupedByDrop As Boolean, FoldOld As Boolean) As String
     Private Function GetGroupedVersionName(Name As String, GroupedByDrop As Boolean, FoldOld As Boolean) As String
@@ -271,14 +271,14 @@ GroupDone:
         End If
         End If
 
 
         '放置当前工程
         '放置当前工程
-        If CompItem IsNot Nothing Then PanIntro.Children.Remove(CompItem)
-        CompItem = Project.ToCompItem(True, True).Init()
-        CompItem.CanInteraction = False
-        CompItem.Margin = New Thickness(-7, -7, 0, 8)
-        PanIntro.Children.Insert(0, CompItem)
+        If ResourceItem IsNot Nothing Then PanIntro.Children.Remove(ResourceItem)
+        ResourceItem = Project.ToResourceItem(True, True).Init()
+        ResourceItem.CanInteraction = False
+        ResourceItem.Margin = New Thickness(-7, -7, 0, 8)
+        PanIntro.Children.Insert(0, ResourceItem)
 
 
         '决定按钮显示
         '决定按钮显示
-        BtnIntroWeb.Text = If(Project.FromCurseForge, "转到 CurseForge", "转到 Modrinth")
+        BtnIntroWeb.Text = $"转到 {Project.Platform}"
         BtnIntroWiki.Visibility = If(Project.WikiId = 0, Visibility.Collapsed, Visibility.Visible)
         BtnIntroWiki.Visibility = If(Project.WikiId = 0, Visibility.Collapsed, Visibility.Visible)
 
 
         AniControlEnabled -= 1
         AniControlEnabled -= 1
@@ -289,8 +289,8 @@ GroupDone:
         Try
         Try
 
 
             '获取基本信息
             '获取基本信息
-            Dim File As CompFile = sender.Tag
-            Dim LoaderName As String = $"{If(Project.FromCurseForge, "CurseForge", "Modrinth")} 整合包下载:{Project.TranslatedName} "
+            Dim File As ResourceVersion = sender.Tag
+            Dim LoaderName As String = $"{Project.Platform} 整合包下载:{Project.TranslatedName} "
 
 
             '获取版本名
             '获取版本名
             Dim PackName As String = Project.TranslatedName.Replace(".zip", "").Replace(".rar", "").Replace(".mrpack", "").Replace("\", "\").Replace("/", "/").Replace("|", "|").Replace(":", ":").Replace("<", "<").Replace(">", ">").Replace("*", "*").Replace("?", "?").Replace("""", "").Replace(": ", ":")
             Dim PackName As String = Project.TranslatedName.Replace(".zip", "").Replace(".rar", "").Replace(".mrpack", "").Replace("\", "\").Replace("/", "/").Replace("|", "|").Replace(":", ":").Replace("<", "<").Replace(">", ">").Replace("*", "*").Replace("?", "?").Replace("""", "").Replace(": ", ":")
@@ -301,8 +301,8 @@ GroupDone:
 
 
             '构造步骤加载器
             '构造步骤加载器
             Dim Loaders As New List(Of LoaderBase)
             Dim Loaders As New List(Of LoaderBase)
-            Dim Target As String = $"{McFolderSelected}versions\{InstanceName}\原始整合包.{If(Project.FromCurseForge, "zip", "mrpack")}"
-            Dim LogoFileAddress As String = CompItem.PathLogo.ActualSource
+            Dim Target As String = $"{McFolderSelected}versions\{InstanceName}\原始整合包.{If(Project.Platform = ResourcePlatforms.CurseForge, "zip", "mrpack")}"
+            Dim LogoFileAddress As String = ResourceItem.PathLogo.ActualSource
             Loaders.Add(New LoaderDownload("下载整合包文件", New List(Of NetFile) From {File.ToNetFile(Target)}) With {.ProgressWeight = 10, .Block = True})
             Loaders.Add(New LoaderDownload("下载整合包文件", New List(Of NetFile) From {File.ToNetFile(Target)}) With {.ProgressWeight = 10, .Block = True})
             Loaders.Add(New LoaderTask(Of Integer, Integer)("准备安装整合包",
             Loaders.Add(New LoaderTask(Of Integer, Integer)("准备安装整合包",
             Sub() ModpackInstall(Target, InstanceName, If(IO.File.Exists(LogoFileAddress), LogoFileAddress, Nothing))) With {.ProgressWeight = 0.1})
             Sub() ModpackInstall(Target, InstanceName, If(IO.File.Exists(LogoFileAddress), LogoFileAddress, Nothing))) With {.ProgressWeight = 0.1})
@@ -326,49 +326,49 @@ GroupDone:
             FrmMain.BtnExtraDownload.Ribble()
             FrmMain.BtnExtraDownload.Ribble()
 
 
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "下载资源整合包失败", LogLevel.Feedback)
+            Log(ex, "下载资源整合包失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
     '资源下载;整合包另存为
     '资源下载;整合包另存为
-    Public Shared CachedFolder As New Dictionary(Of CompType, String) '仅在本次缓存的下载文件夹
+    Public Shared CachedFolder As New Dictionary(Of ResourceTypes, String) '仅在本次缓存的下载文件夹
     Public Sub Save_Click(sender As Object, e As EventArgs)
     Public Sub Save_Click(sender As Object, e As EventArgs)
-        Dim File As CompFile = If(TypeOf sender Is MyListItem, sender, sender.Parent).Tag
+        Dim File As ResourceVersion = If(TypeOf sender Is MyListItem, sender, sender.Parent).Tag
         RunInNewThread(
         RunInNewThread(
         Sub()
         Sub()
             Try
             Try
                 Dim Desc As String = Nothing
                 Dim Desc As String = Nothing
-                Select Case File.Type
-                    Case CompType.ModPack : Desc = "整合包"
-                    Case CompType.Mod : Desc = "Mod "
-                    Case CompType.ResourcePack : Desc = "资源包"
-                    Case CompType.Shader : Desc = "光影包"
-                    Case CompType.DataPack : Desc = "数据包"
+                Select Case File.ResourceType
+                    Case ResourceTypes.ModPack : Desc = "整合包"
+                    Case ResourceTypes.Mod : Desc = "Mod "
+                    Case ResourceTypes.ResourcePack : Desc = "资源包"
+                    Case ResourceTypes.Shader : Desc = "光影包"
+                    Case ResourceTypes.DataPack : Desc = "数据包"
                 End Select
                 End Select
                 '确认默认保存位置
                 '确认默认保存位置
                 Dim DefaultFolder As String = Nothing
                 Dim DefaultFolder As String = Nothing
-                If File.Type <> CompType.ModPack Then
+                If File.ResourceType <> ResourceTypes.ModPack Then
                     Dim SubFolder As String = Nothing
                     Dim SubFolder As String = Nothing
-                    Select Case File.Type
-                        Case CompType.Mod : SubFolder = "mods\"
-                        Case CompType.ResourcePack : SubFolder = "resourcepacks\"
-                        Case CompType.Shader : SubFolder = "shaderpacks\"
-                        Case CompType.DataPack : SubFolder = "" '导航到版本根目录
+                    Select Case File.ResourceType
+                        Case ResourceTypes.Mod : SubFolder = "mods\"
+                        Case ResourceTypes.ResourcePack : SubFolder = "resourcepacks\"
+                        Case ResourceTypes.Shader : SubFolder = "shaderpacks\"
+                        Case ResourceTypes.DataPack : SubFolder = "" '导航到版本根目录
                     End Select
                     End Select
                     Dim IsInstanceSuitable As Func(Of McInstance, Boolean) = Nothing
                     Dim IsInstanceSuitable As Func(Of McInstance, Boolean) = Nothing
                     '获取资源所需的加载器
                     '获取资源所需的加载器
-                    Dim AllowedLoaders As New List(Of CompModLoaderType)
-                    If File.ModLoaders.Any Then
-                        AllowedLoaders = File.ModLoaders
-                    ElseIf Project.ModLoaders.Any Then
-                        AllowedLoaders = Project.ModLoaders
+                    Dim AllowedLoaders As New List(Of ModLoaderTypes)
+                    If File.Loaders.Any Then
+                        AllowedLoaders = File.Loaders
+                    ElseIf Project.Loaders.Any Then
+                        AllowedLoaders = Project.Loaders
                     End If
                     End If
-                    Log($"[Comp] {Desc}要求的加载器种类:" & If(AllowedLoaders.Any(), AllowedLoaders.Join(" / "), "无要求"))
+                    Log($"[Resource] {Desc}要求的加载器种类:" & If(AllowedLoaders.Any(), AllowedLoaders.Join(" / "), "无要求"))
                     '判断某个版本是否符合资源要求
                     '判断某个版本是否符合资源要求
                     IsInstanceSuitable =
                     IsInstanceSuitable =
                     Function(Instance As McInstance)
                     Function(Instance As McInstance)
                         If Not Instance.IsLoaded Then Instance.Load()
                         If Not Instance.IsLoaded Then Instance.Load()
                         '只对 Mod 和数据包进行版本检测
                         '只对 Mod 和数据包进行版本检测
-                        If File.Type = CompType.Mod OrElse File.Type = CompType.DataPack Then
+                        If File.ResourceType = ResourceTypes.Mod OrElse File.ResourceType = ResourceTypes.DataPack Then
                             If Not File.GameVersions.Any( '判断是否有任何一个版本匹配
                             If Not File.GameVersions.Any( '判断是否有任何一个版本匹配
                             Function(FileVersion)
                             Function(FileVersion)
                                 Dim FileIsSnapshot As Boolean = FileVersion.Contains("预览版") OrElse Not FileVersion.Contains(".")
                                 Dim FileIsSnapshot As Boolean = FileVersion.Contains("预览版") OrElse Not FileVersion.Contains(".")
@@ -404,20 +404,20 @@ GroupDone:
                         End If
                         End If
                         '加载器
                         '加载器
                         If Not AllowedLoaders.Any() Then Return True '无要求
                         If Not AllowedLoaders.Any() Then Return True '无要求
-                        If AllowedLoaders.Contains(CompModLoaderType.Forge) AndAlso Instance.Version.HasForge Then Return True
-                        If AllowedLoaders.Contains(CompModLoaderType.Fabric) AndAlso Instance.Version.HasFabric Then Return True
-                        If AllowedLoaders.Contains(CompModLoaderType.NeoForge) AndAlso Instance.Version.HasNeoForge Then Return True
-                        If AllowedLoaders.Contains(CompModLoaderType.LiteLoader) AndAlso Instance.Version.HasLiteLoader Then Return True
+                        If AllowedLoaders.Contains(ModLoaderTypes.Forge) AndAlso Instance.Version.HasForge Then Return True
+                        If AllowedLoaders.Contains(ModLoaderTypes.Fabric) AndAlso Instance.Version.HasFabric Then Return True
+                        If AllowedLoaders.Contains(ModLoaderTypes.NeoForge) AndAlso Instance.Version.HasNeoForge Then Return True
+                        If AllowedLoaders.Contains(ModLoaderTypes.LiteLoader) AndAlso Instance.Version.HasLiteLoader Then Return True
                         Return False
                         Return False
                     End Function
                     End Function
                     '获取常规资源默认下载位置
                     '获取常规资源默认下载位置
-                    If CachedFolder.ContainsKey(File.Type) Then
-                        DefaultFolder = CachedFolder(File.Type)
-                        Log($"[Comp] 使用上次下载时的文件夹作为默认下载位置:{DefaultFolder}")
+                    If CachedFolder.ContainsKey(File.ResourceType) Then
+                        DefaultFolder = CachedFolder(File.ResourceType)
+                        Log($"[Resource] 使用上次下载时的文件夹作为默认下载位置:{DefaultFolder}")
                     ElseIf McInstanceSelected IsNot Nothing AndAlso IsInstanceSuitable(McInstanceSelected) Then
                     ElseIf McInstanceSelected IsNot Nothing AndAlso IsInstanceSuitable(McInstanceSelected) Then
                         DefaultFolder = $"{McInstanceSelected.PathIndie}{SubFolder}"
                         DefaultFolder = $"{McInstanceSelected.PathIndie}{SubFolder}"
-                        Directory.CreateDirectory(DefaultFolder)
-                        Log($"[Comp] 使用当前版本作为默认下载位置:{DefaultFolder}")
+                        DirectoryUtils.Create(DefaultFolder)
+                        Log($"[Resource] 使用当前版本作为默认下载位置:{DefaultFolder}")
                     Else
                     Else
                         '查找所有可能的版本
                         '查找所有可能的版本
                         Dim NeedLoad As Boolean = McInstanceListLoader.State <> LoadState.Finished
                         Dim NeedLoad As Boolean = McInstanceListLoader.State <> LoadState.Finished
@@ -433,14 +433,14 @@ GroupDone:
                                 ThenByDescending(Function(Dir) If(Dir.Exists, Dir.GetFiles().Length, -1)). '再按文件夹中的文件数量降序
                                 ThenByDescending(Function(Dir) If(Dir.Exists, Dir.GetFiles().Length, -1)). '再按文件夹中的文件数量降序
                                 First()
                                 First()
                             DefaultFolder = SelectedInstance.FullName
                             DefaultFolder = SelectedInstance.FullName
-                            Directory.CreateDirectory(DefaultFolder)
-                            Log($"[Comp] 使用适合的游戏版本作为默认下载位置:{DefaultFolder}")
+                            DirectoryUtils.Create(DefaultFolder)
+                            Log($"[Resource] 使用适合的游戏版本作为默认下载位置:{DefaultFolder}")
                         Else
                         Else
                             DefaultFolder = McFolderSelected
                             DefaultFolder = McFolderSelected
                             If NeedLoad Then
                             If NeedLoad Then
                                 Hint($"当前 MC 文件夹中没有找到合适的版本!")
                                 Hint($"当前 MC 文件夹中没有找到合适的版本!")
                             Else
                             Else
-                                Log("[Comp] 由于当前版本不兼容,使用当前的 MC 文件夹作为默认下载位置")
+                                Log("[Resource] 由于当前版本不兼容,使用当前的 MC 文件夹作为默认下载位置")
                             End If
                             End If
                         End If
                         End If
                     End If
                     End If
@@ -465,20 +465,20 @@ GroupDone:
                             FileName = File.FileName
                             FileName = File.FileName
                     End Select
                     End Select
                 End If
                 End If
-                If File.Type = CompType.Mod Then FileName = FileName.Replace("~", "-") '~ 会导致 Mixin 加载失败
+                If File.ResourceType = ResourceTypes.Mod Then FileName = FileName.Replace("~", "-") '~ 会导致 Mixin 加载失败
                 RunInUi(
                 RunInUi(
                 Sub()
                 Sub()
                     '弹窗要求选择保存位置
                     '弹窗要求选择保存位置
                     Dim Target As String
                     Dim Target As String
                     Target = SelectSaveFile("选择保存位置", FileName,
                     Target = SelectSaveFile("选择保存位置", FileName,
                         Desc & "文件|" &
                         Desc & "文件|" &
-                        If(File.Type = CompType.Mod,
+                        If(File.ResourceType = ResourceTypes.Mod,
                             If(File.FileName.EndsWithF(".litemod"), "*.litemod", "*.jar"),
                             If(File.FileName.EndsWithF(".litemod"), "*.litemod", "*.jar"),
                             If(File.FileName.EndsWithF(".mrpack"), "*.mrpack", "*.zip")), DefaultFolder)
                             If(File.FileName.EndsWithF(".mrpack"), "*.mrpack", "*.zip")), DefaultFolder)
                     If Not Target.Contains("\") Then Return
                     If Not Target.Contains("\") Then Return
                     '构造步骤加载器
                     '构造步骤加载器
-                    Dim LoaderName As String = Desc & "下载:" & IO.Path.GetFileNameWithoutExtension(Target) & " "
-                    If Target <> DefaultFolder Then CachedFolder(File.Type) = GetPathFromFullPath(Target)
+                    Dim LoaderName As String = Desc & "下载:" & Path.GetFileNameWithoutExtension(Target) & " "
+                    If Target <> DefaultFolder Then CachedFolder(File.ResourceType) = GetPathFromFullPath(Target)
                     Dim Loaders As New List(Of LoaderBase)
                     Dim Loaders As New List(Of LoaderBase)
                     Loaders.Add(New LoaderDownload("下载文件", New List(Of NetFile) From {File.ToNetFile(Target)}) With {.ProgressWeight = 6, .Block = True})
                     Loaders.Add(New LoaderDownload("下载文件", New List(Of NetFile) From {File.ToNetFile(Target)}) With {.ProgressWeight = 6, .Block = True})
                     '启动
                     '启动
@@ -489,9 +489,9 @@ GroupDone:
                     FrmMain.BtnExtraDownload.Ribble()
                     FrmMain.BtnExtraDownload.Ribble()
                 End Sub)
                 End Sub)
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "保存资源文件失败", LogLevel.Feedback)
+                Log(ex, "保存资源文件失败", NotifyLevel.MsgBoxAndFeedback)
             End Try
             End Try
-        End Sub, "Download CompDetail Save")
+        End Sub, "Download Resource Detail Save")
     End Sub
     End Sub
 
 
     Private Sub BtnIntroWeb_Click(sender As Object, e As EventArgs) Handles BtnIntroWeb.Click
     Private Sub BtnIntroWeb_Click(sender As Object, e As EventArgs) Handles BtnIntroWeb.Click
@@ -501,14 +501,39 @@ GroupDone:
         OpenWebsite("https://www.mcmod.cn/class/" & Project.WikiId & ".html")
         OpenWebsite("https://www.mcmod.cn/class/" & Project.WikiId & ".html")
     End Sub
     End Sub
     Private Sub BtnIntroCopy_Click(sender As Object, e As EventArgs) Handles BtnIntroCopy.Click
     Private Sub BtnIntroCopy_Click(sender As Object, e As EventArgs) Handles BtnIntroCopy.Click
-        ClipboardSet(CompItem.LabTitle.Text & CompItem.LabTitleRaw.Text)
+        ClipboardSet(ResourceItem.LabTitle.Text & ResourceItem.LabTitleRaw.Text)
     End Sub
     End Sub
 
 
     'Mod / 数据包 互相跳转
     'Mod / 数据包 互相跳转
     Private Sub HintAlternative_Click() Handles HintAlternative.Click
     Private Sub HintAlternative_Click() Handles HintAlternative.Click
-        TargetType = If(TargetType = CompType.Mod, CompType.DataPack, CompType.Mod)
+        TargetType = If(TargetType = ResourceTypes.Mod, ResourceTypes.DataPack, ResourceTypes.Mod)
         FrmMain.PageCurrent.Additional(4) = TargetType '加载器会从这里重新拿数据
         FrmMain.PageCurrent.Additional(4) = TargetType '加载器会从这里重新拿数据
         PageLoaderRestart(IsForceRestart:=True)
         PageLoaderRestart(IsForceRestart:=True)
     End Sub
     End Sub
 
 
+    ''' <summary>
+    ''' 预载包含大量 ProjectVersion 的卡片,添加必要的元素和前置列表。
+    ''' </summary>
+    Public Shared Sub ResourceFilesCardPreload(Stack As StackPanel, Files As List(Of ResourceVersion))
+        '获取卡片对应的前置 ID
+        '如果为整合包就不会有 Dependencies 信息,所以不用管
+        Dim Deps As List(Of String) = Files.SelectMany(Function(f) f.Dependencies).Distinct.ToList()
+        Deps.Sort()
+        If Not Deps.Any() Then Return
+        Deps = Deps.Where(
+        Function(dep)
+            If Not ResourceProject.Cache.ContainsKey(dep) Then Log($"[Resource] 未找到 ID {dep} 的前置信息", NotifyLevel.DebugModeOnly)
+            Return ResourceProject.Cache.ContainsKey(dep)
+        End Function).ToList
+        '添加开头间隔
+        Stack.Children.Add(New TextBlock With {.Text = "前置资源", .FontSize = 14, .HorizontalAlignment = HorizontalAlignment.Left, .Margin = New Thickness(6, 2, 0, 5)})
+        '添加前置列表
+        For Each Dep In Deps
+            Dim Item = ResourceProject.Cache(Dep).ToResourceItem(False, False)
+            Stack.Children.Add(Item)
+        Next
+        '添加结尾间隔
+        Stack.Children.Add(New TextBlock With {.Text = "版本列表", .FontSize = 14, .HorizontalAlignment = HorizontalAlignment.Left, .Margin = New Thickness(6, 12, 0, 5)})
+    End Sub
+
 End Class
 End Class

+ 2 - 2
Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadDataPack.xaml → Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadDataPack.xaml

@@ -4,7 +4,7 @@
     xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
     xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
     mc:Ignorable="d" x:Class="PageDownloadDataPack"
     mc:Ignorable="d" x:Class="PageDownloadDataPack"
     PanScroll="Content">
     PanScroll="Content">
-    <local:PageComp x:Name="Content" PageType="DataPack" TypeName="数据包" TypeNameSpaced="数据包">
+    <local:PageResource x:Name="Content" PageType="DataPack" TypeName="数据包" TypeNameSpaced="数据包">
         <local:MyComboBoxItem Tag="" Content="全部" IsSelected="True" />
         <local:MyComboBoxItem Tag="" Content="全部" IsSelected="True" />
         <local:MyComboBoxItem Content="世界元素" Tag="/worldgen" />
         <local:MyComboBoxItem Content="世界元素" Tag="/worldgen" />
         <local:MyComboBoxItem Content="科技" Tag="6951/technology" />
         <local:MyComboBoxItem Content="科技" Tag="6951/technology" />
@@ -22,5 +22,5 @@
         <local:MyComboBoxItem Content="服务器" Tag="/social" />
         <local:MyComboBoxItem Content="服务器" Tag="/social" />
         <local:MyComboBoxItem Content="支持库" Tag="6950/library" />
         <local:MyComboBoxItem Content="支持库" Tag="6950/library" />
         <local:MyComboBoxItem Content="Mod 相关" Tag="6946/" />
         <local:MyComboBoxItem Content="Mod 相关" Tag="6946/" />
-    </local:PageComp>
+    </local:PageResource>
 </local:MyPageRight>
 </local:MyPageRight>

+ 0 - 0
Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadDataPack.xaml.vb → Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadDataPack.xaml.vb


+ 2 - 2
Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadMod.xaml → Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadMod.xaml

@@ -4,7 +4,7 @@
     xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
     xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
     mc:Ignorable="d" x:Class="PageDownloadMod"
     mc:Ignorable="d" x:Class="PageDownloadMod"
     PanScroll="Content">
     PanScroll="Content">
-    <local:PageComp x:Name="Content" PageType="Mod" TypeName="Mod" TypeNameSpaced=" Mod ">
+    <local:PageResource x:Name="Content" PageType="Mod" TypeName="Mod" TypeNameSpaced=" Mod ">
         <local:MyComboBoxItem Content="全部" Tag="" IsSelected="True" />
         <local:MyComboBoxItem Content="全部" Tag="" IsSelected="True" />
         <local:MyComboBoxItem Content="世界元素" Tag="406/worldgen" />
         <local:MyComboBoxItem Content="世界元素" Tag="406/worldgen" />
         <local:MyComboBoxItem Content="生物群系" Tag="407/" Margin="15,0,0,0" />
         <local:MyComboBoxItem Content="生物群系" Tag="407/" Margin="15,0,0,0" />
@@ -32,5 +32,5 @@
         <local:MyComboBoxItem Content="信息显示" Tag="423/" />
         <local:MyComboBoxItem Content="信息显示" Tag="423/" />
         <local:MyComboBoxItem Content="服务器" Tag="435/social" />
         <local:MyComboBoxItem Content="服务器" Tag="435/social" />
         <local:MyComboBoxItem Content="支持库" Tag="421/library" />
         <local:MyComboBoxItem Content="支持库" Tag="421/library" />
-    </local:PageComp>
+    </local:PageResource>
 </local:MyPageRight>
 </local:MyPageRight>

+ 0 - 0
Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadMod.xaml.vb → Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadMod.xaml.vb


+ 2 - 2
Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadPack.xaml → Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadPack.xaml

@@ -4,7 +4,7 @@
     xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
     xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
     mc:Ignorable="d" x:Class="PageDownloadPack"
     mc:Ignorable="d" x:Class="PageDownloadPack"
     PanScroll="Content">
     PanScroll="Content">
-    <local:PageComp x:Name="Content" PageType="ModPack" TypeName="整合包" TypeNameSpaced="整合包">
+    <local:PageResource x:Name="Content" PageType="ModPack" TypeName="整合包" TypeNameSpaced="整合包">
         <local:MyComboBoxItem Tag="" Content="全部" IsSelected="True" />
         <local:MyComboBoxItem Tag="" Content="全部" IsSelected="True" />
         <local:MyComboBoxItem Tag="4484/" Content="多人" />
         <local:MyComboBoxItem Tag="4484/" Content="多人" />
         <local:MyComboBoxItem Tag="/optimization" Content="性能优化" />
         <local:MyComboBoxItem Tag="/optimization" Content="性能优化" />
@@ -24,5 +24,5 @@
         <local:MyComboBoxItem Tag="4480/" Content="基于地图" />
         <local:MyComboBoxItem Tag="4480/" Content="基于地图" />
         <local:MyComboBoxItem Tag="4481/lightweight" Content="轻量整合" />
         <local:MyComboBoxItem Tag="4481/lightweight" Content="轻量整合" />
         <local:MyComboBoxItem Tag="4482/" Content="大型整合" />
         <local:MyComboBoxItem Tag="4482/" Content="大型整合" />
-    </local:PageComp>
+    </local:PageResource>
 </local:MyPageRight>
 </local:MyPageRight>

+ 0 - 0
Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadPack.xaml.vb → Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadPack.xaml.vb


+ 2 - 2
Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadResourcePack.xaml → Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadResourcePack.xaml

@@ -4,7 +4,7 @@
     xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
     xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
     mc:Ignorable="d" x:Class="PageDownloadResourcePack"
     mc:Ignorable="d" x:Class="PageDownloadResourcePack"
     PanScroll="Content">
     PanScroll="Content">
-    <local:PageComp x:Name="Content" PageType="ResourcePack" TypeName="资源包" TypeNameSpaced="资源包">
+    <local:PageResource x:Name="Content" PageType="ResourcePack" TypeName="资源包" TypeNameSpaced="资源包">
         <local:MyComboBoxItem Tag="" Content="全部" IsSelected="True" />
         <local:MyComboBoxItem Tag="" Content="全部" IsSelected="True" />
 
 
         <local:MyComboBoxItem Content="风格" IsEnabled="False" />
         <local:MyComboBoxItem Content="风格" IsEnabled="False" />
@@ -41,5 +41,5 @@
         <local:MyComboBoxItem Margin="15,0,0,0" Content="128x" Tag="396/128x" />
         <local:MyComboBoxItem Margin="15,0,0,0" Content="128x" Tag="396/128x" />
         <local:MyComboBoxItem Margin="15,0,0,0" Content="256x" Tag="397/256x" />
         <local:MyComboBoxItem Margin="15,0,0,0" Content="256x" Tag="397/256x" />
         <local:MyComboBoxItem Margin="15,0,0,0" Content="512x 或更高" Tag="398/512x+" />
         <local:MyComboBoxItem Margin="15,0,0,0" Content="512x 或更高" Tag="398/512x+" />
-    </local:PageComp>
+    </local:PageResource>
 </local:MyPageRight>
 </local:MyPageRight>

+ 0 - 0
Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadResourcePack.xaml.vb → Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadResourcePack.xaml.vb


+ 2 - 2
Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadShader.xaml → Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadShader.xaml

@@ -4,7 +4,7 @@
     xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
     xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
     mc:Ignorable="d" x:Class="PageDownloadShader"
     mc:Ignorable="d" x:Class="PageDownloadShader"
     PanScroll="Content">
     PanScroll="Content">
-    <local:PageComp x:Name="Content" PageType="Shader" TypeName="光影包" TypeNameSpaced="光影包">
+    <local:PageResource x:Name="Content" PageType="Shader" TypeName="光影包" TypeNameSpaced="光影包">
         <local:MyComboBoxItem Tag="" Content="全部" IsSelected="True" />
         <local:MyComboBoxItem Tag="" Content="全部" IsSelected="True" />
         
         
         <local:MyComboBoxItem Content="风格" IsEnabled="False" />
         <local:MyComboBoxItem Content="风格" IsEnabled="False" />
@@ -30,5 +30,5 @@
         <local:MyComboBoxItem Margin="15,0,0,0" Content="原版可用" Tag="/vanilla" ToolTip="作为资源包加载,可以直接在原版游戏里使用。" ToolTipService.HorizontalOffset="70" />
         <local:MyComboBoxItem Margin="15,0,0,0" Content="原版可用" Tag="/vanilla" ToolTip="作为资源包加载,可以直接在原版游戏里使用。" ToolTipService.HorizontalOffset="70" />
         <local:MyComboBoxItem Margin="15,0,0,0" Content="Iris" Tag="/iris" />
         <local:MyComboBoxItem Margin="15,0,0,0" Content="Iris" Tag="/iris" />
         <local:MyComboBoxItem Margin="15,0,0,0" Content="OptiFine" Tag="/optifine" />
         <local:MyComboBoxItem Margin="15,0,0,0" Content="OptiFine" Tag="/optifine" />
-    </local:PageComp>
+    </local:PageResource>
 </local:MyPageRight>
 </local:MyPageRight>

+ 0 - 0
Plain Craft Launcher 2/Pages/PageDownload/Comp/PageDownloadShader.xaml.vb → Plain Craft Launcher 2/Pages/PageDownload/Resource/PageDownloadShader.xaml.vb


+ 1 - 1
Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml → Plain Craft Launcher 2/Pages/PageDownload/Resource/PageResource.xaml

@@ -1,4 +1,4 @@
-<local:MyScrollViewer x:Class="PageComp"
+<local:MyScrollViewer x:Class="PageResource"
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
       xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 

+ 20 - 20
Plain Craft Launcher 2/Pages/PageDownload/Comp/PageComp.xaml.vb → Plain Craft Launcher 2/Pages/PageDownload/Resource/PageResource.xaml.vb

@@ -1,7 +1,7 @@
 Imports System.Windows.Markup
 Imports System.Windows.Markup
 
 
 <ContentProperty("SearchTags")>
 <ContentProperty("SearchTags")>
-Public Class PageComp
+Public Class PageResource
 
 
 #Region "属性"
 #Region "属性"
 
 
@@ -48,17 +48,17 @@ Public Class PageComp
     ''' <summary>
     ''' <summary>
     ''' 该页面对应的资源类型。
     ''' 该页面对应的资源类型。
     ''' </summary>
     ''' </summary>
-    Public Property PageType As CompType
+    Public Property PageType As ResourceTypes
         Get
         Get
             Return _Type
             Return _Type
         End Get
         End Get
-        Set(Value As CompType)
+        Set(Value As ResourceTypes)
             If _Type = Value Then Return
             If _Type = Value Then Return
             _Type = Value
             _Type = Value
-            BtnSearchInstallModPack.Visibility = If(Value = CompType.ModPack, Visibility.Visible, Visibility.Collapsed)
+            BtnSearchInstallModPack.Visibility = If(Value = ResourceTypes.ModPack, Visibility.Visible, Visibility.Collapsed)
         End Set
         End Set
     End Property
     End Property
-    Private _Type As CompType = -1
+    Private _Type As ResourceTypes = -1
 
 
 #End Region
 #End Region
 
 
@@ -73,10 +73,10 @@ Public Class PageComp
     ''' </summary>
     ''' </summary>
     Public Shared TargetName As String = Nothing
     Public Shared TargetName As String = Nothing
 
 
-    '在点击 MyCompItem 时会获取 Loader 的输入,以使资源详情页面可以应用相同的筛选项
-    Public Loader As New LoaderTask(Of CompProjectRequest, Integer)("社区资源获取:XXX", AddressOf CompProjectsGet, AddressOf LoaderInput) With {.ReloadTimeout = 60 * 1000}
+    '在点击 MyResourceItem 时会获取 Loader 的输入,以使资源详情页面可以应用相同的筛选项
+    Public Loader = ResourceSearcher.GetLoader(AddressOf LoaderInput)
 
 
-    Private Sub PageCompControls_Inited(sender As Object, e As EventArgs) Handles Me.Loaded
+    Private Sub PageResource_Loaded(sender As Object, e As EventArgs) Handles Me.Loaded
         Static IsFirstLoaded As Boolean = False
         Static IsFirstLoaded As Boolean = False
         '不知道从 Initialized 改成 Loaded 会不会有问题,但用 Initialized 会导致初始的筛选器修改被覆盖回默认值
         '不知道从 Initialized 改成 Loaded 会不会有问题,但用 Initialized 会导致初始的筛选器修改被覆盖回默认值
         If TargetInstance IsNot Nothing Then
         If TargetInstance IsNot Nothing Then
@@ -118,23 +118,23 @@ Public Class PageComp
             End If
             End If
         End If
         End If
     End Sub
     End Sub
-    Private Function LoaderInput() As CompProjectRequest
-        Dim Request As New CompProjectRequest(PageType, Storage, (Page + 1) * PAGE_SIZE)
+    Private Function LoaderInput() As ResourceSearcher.SearchRequest
+        Dim Request As New ResourceSearcher.SearchRequest(PageType, Storage, (Page + 1) * PAGE_SIZE)
         Dim GameVersion As String = If(TextSearchVersion.Text = "全部 (也可自行输入)", Nothing,
         Dim GameVersion As String = If(TextSearchVersion.Text = "全部 (也可自行输入)", Nothing,
             If(TextSearchVersion.Text.Contains(".") OrElse TextSearchVersion.Text.Contains("w"), TextSearchVersion.Text, Nothing))
             If(TextSearchVersion.Text.Contains(".") OrElse TextSearchVersion.Text.Contains("w"), TextSearchVersion.Text, Nothing))
         With Request
         With Request
             .SearchText = TextSearchName.Text
             .SearchText = TextSearchName.Text
             .GameVersion = GameVersion
             .GameVersion = GameVersion
             .Tag = ComboSearchTag.SelectedItem.Tag
             .Tag = ComboSearchTag.SelectedItem.Tag
-            .ModLoader = If(PageType = CompType.Mod, Val(ComboSearchLoader.SelectedItem.Tag), CompModLoaderType.Any)
-            .Sources = CType(Val(ComboSearchSource.SelectedItem.Tag), CompSourceType)
+            .ModLoader = If(PageType = ResourceTypes.Mod, Val(ComboSearchLoader.SelectedItem.Tag), ModLoaderTypes.Any)
+            .Sources = CType(Val(ComboSearchSource.SelectedItem.Tag), ResourcePlatforms)
         End With
         End With
         Return Request
         Return Request
     End Function
     End Function
 
 
 #End Region
 #End Region
 
 
-    Public Storage As New CompProjectStorage
+    Public Storage As New ResourceSearcher.SearchResult
     ''' <summary>
     ''' <summary>
     ''' 每页展示的结果数量。
     ''' 每页展示的结果数量。
     ''' </summary>
     ''' </summary>
@@ -144,14 +144,14 @@ Public Class PageComp
     '结果 UI 化
     '结果 UI 化
     Private Sub Load_OnFinish()
     Private Sub Load_OnFinish()
         Try
         Try
-            Log($"[Comp] 开始可视化{TypeNameSpaced}列表,已储藏 {Storage.Results.Count} 个结果,当前在第 {Page + 1} 页")
+            Log($"[Resource] 开始可视化{TypeNameSpaced}列表,已储藏 {Storage.Results.Count} 个结果,当前在第 {Page + 1} 页")
             '列表项
             '列表项
             PanProjects.Children.Clear()
             PanProjects.Children.Clear()
             Dim Index As Integer = Math.Min(Page * PAGE_SIZE, Storage.Results.Count - 1)
             Dim Index As Integer = Math.Min(Page * PAGE_SIZE, Storage.Results.Count - 1)
             For Each Result In Storage.Results.GetRange(Index, Math.Min(Storage.Results.Count - Index, PAGE_SIZE))
             For Each Result In Storage.Results.GetRange(Index, Math.Min(Storage.Results.Count - Index, PAGE_SIZE))
-                PanProjects.Children.Add(Result.ToCompItem(
+                PanProjects.Children.Add(Result.ToResourceItem(
                     ShowMcVersionDesc:=Loader.Input.GameVersion Is Nothing,
                     ShowMcVersionDesc:=Loader.Input.GameVersion Is Nothing,
-                    ShowLoaderDesc:=Loader.Input.ModLoader = CompModLoaderType.Any AndAlso (PageType = CompType.Mod OrElse PageType = CompType.ModPack)))
+                    ShowLoaderDesc:=Loader.Input.ModLoader = ModLoaderTypes.Any AndAlso (PageType = ResourceTypes.Mod OrElse PageType = ResourceTypes.ModPack)))
             Next
             Next
             '页码
             '页码
             CardPages.Visibility = If(Storage.Results.Count > 40 OrElse
             CardPages.Visibility = If(Storage.Results.Count > 40 OrElse
@@ -177,7 +177,7 @@ Public Class PageComp
             '强制返回顶部
             '强制返回顶部
             ScrollToTop()
             ScrollToTop()
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, $"可视化{TypeNameSpaced}列表出错", LogLevel.Feedback)
+            Log(ex, $"可视化{TypeNameSpaced}列表出错", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -188,7 +188,7 @@ Public Class PageComp
                 Dim ErrorMessage As String = ""
                 Dim ErrorMessage As String = ""
                 If Loader.Error IsNot Nothing Then ErrorMessage = Loader.Error.Message
                 If Loader.Error IsNot Nothing Then ErrorMessage = Loader.Error.Message
                 If ErrorMessage.Contains("不是有效的 json 文件") Then
                 If ErrorMessage.Contains("不是有效的 json 文件") Then
-                    Log($"[Download] 下载的{TypeNameSpaced}列表 json 文件损坏,已自动重试", LogLevel.Debug)
+                    Log($"[Download] 下载的{TypeNameSpaced}列表 json 文件损坏,已自动重试", NotifyLevel.DebugModeOnly)
                     CType(Parent, MyPageRight).PageLoaderRestart()
                     CType(Parent, MyPageRight).PageLoaderRestart()
                 End If
                 End If
         End Select
         End Select
@@ -222,7 +222,7 @@ Public Class PageComp
     '搜索按钮
     '搜索按钮
     Private Sub StartNewSearch() Handles BtnSearchRun.Click
     Private Sub StartNewSearch() Handles BtnSearchRun.Click
         Page = 0
         Page = 0
-        If Loader.ShouldStart(LoaderInput()) Then Storage = New CompProjectStorage '避免连续搜索两次使得 CompProjectStorage 引用丢失(#1311)
+        If Loader.ShouldStart(LoaderInput()) Then Storage = New ResourceSearcher.SearchResult '避免连续搜索两次导致 ProjectStorage 引用丢失(#1311)
         Loader.Start()
         Loader.Start()
     End Sub
     End Sub
     Private Sub EnterTrigger(sender As Object, e As KeyEventArgs) Handles TextSearchName.KeyDown, TextSearchVersion.KeyDown
     Private Sub EnterTrigger(sender As Object, e As KeyEventArgs) Handles TextSearchName.KeyDown, TextSearchVersion.KeyDown
@@ -246,7 +246,7 @@ Public Class PageComp
         If Not TextSearchVersion.IsDropDownOpen Then UpdateSearchLoaderVisibility()
         If Not TextSearchVersion.IsDropDownOpen Then UpdateSearchLoaderVisibility()
     End Sub
     End Sub
     Private Sub UpdateSearchLoaderVisibility() Handles TextSearchVersion.DropDownClosed
     Private Sub UpdateSearchLoaderVisibility() Handles TextSearchVersion.DropDownClosed
-        If PageType = CompType.Mod AndAlso (TextSearchVersion.Text.Contains(".") OrElse TextSearchVersion.Text.Contains("w")) Then
+        If PageType = ResourceTypes.Mod AndAlso (TextSearchVersion.Text.Contains(".") OrElse TextSearchVersion.Text.Contains("w")) Then
             ComboSearchLoader.Visibility = Visibility.Visible
             ComboSearchLoader.Visibility = Visibility.Visible
             Grid.SetColumnSpan(TextSearchVersion, 1)
             Grid.SetColumnSpan(TextSearchVersion, 1)
         Else
         Else

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

@@ -13,12 +13,7 @@ Public Class MyLocalModItem
         End Get
         End Get
         Set(value As String)
         Set(value As String)
             Dim RawValue = value
             Dim RawValue = value
-            Select Case Entry.State
-                Case McMod.McModState.Fine
-                    LabTitle.TextDecorations = Nothing
-                Case McMod.McModState.Disabled
-                    LabTitle.TextDecorations = TextDecorations.Strikethrough
-            End Select
+            LabTitle.TextDecorations = If(Entry.IsEnabled, Nothing, TextDecorations.Strikethrough)
             If LabTitle.Text = value Then Return
             If LabTitle.Text = value Then Return
             LabTitle.Text = value
             LabTitle.Text = value
             _Title = RawValue
             _Title = RawValue
@@ -66,11 +61,11 @@ Public Class MyLocalModItem
     End Property
     End Property
 
 
     '相关联的 Mod
     '相关联的 Mod
-    Public Property Entry As McMod
+    Public Property Entry As ResourceFile
         Get
         Get
             Return Tag
             Return Tag
         End Get
         End Get
-        Set(value As McMod)
+        Set(value As ResourceFile)
             Tag = value
             Tag = value
         End Set
         End Set
     End Property
     End Property
@@ -134,8 +129,8 @@ Public Class MyLocalModItem
         '计算滑动范围
         '计算滑动范围
         Dim Elements = CType(Parent, StackPanel).Children
         Dim Elements = CType(Parent, StackPanel).Children
         Dim Index As Integer = Elements.IndexOf(Me)
         Dim Index As Integer = Elements.IndexOf(Me)
-        SwipeStart = MathClamp(Math.Min(SwipeStart, Index), 0, Elements.Count - 1)
-        SwipeEnd = MathClamp(Math.Max(SwipeEnd, Index), 0, Elements.Count - 1)
+        SwipeStart = Math.Min(SwipeStart, Index).Clamp(0, Elements.Count - 1)
+        SwipeEnd = Math.Max(SwipeEnd, Index).Clamp(0, Elements.Count - 1)
         '勾选所有范围中的项
         '勾选所有范围中的项
         If SwipeStart = SwipeEnd Then Return
         If SwipeStart = SwipeEnd Then Return
         For i = SwipeStart To SwipeEnd
         For i = SwipeStart To SwipeEnd
@@ -183,7 +178,7 @@ Public Class MyLocalModItem
                         Anim.Add(AaOpacity(RectCheck, 1 - RectCheck.Opacity, 30))
                         Anim.Add(AaOpacity(RectCheck, 1 - RectCheck.Opacity, 30))
                         RectCheck.VerticalAlignment = VerticalAlignment.Center
                         RectCheck.VerticalAlignment = VerticalAlignment.Center
                         RectCheck.Margin = New Thickness(-3, 0, 0, 0)
                         RectCheck.Margin = New Thickness(-3, 0, 0, 0)
-                        Anim.Add(AaColor(LabTitle, TextBlock.ForegroundProperty, If(Entry.State = McMod.McModState.Fine, "ColorBrush2", "ColorBrush5"), 200))
+                        Anim.Add(AaColor(LabTitle, TextBlock.ForegroundProperty, If(Entry.IsEnabled, "ColorBrush2", "ColorBrush5"), 200))
                     Else
                     Else
                         '由有变无
                         '由有变无
                         Anim.Add(AaHeight(RectCheck, -RectCheck.ActualHeight, 120,, New AniEaseInFluent(AniEasePower.Weak)))
                         Anim.Add(AaHeight(RectCheck, -RectCheck.ActualHeight, 120,, New AniEaseInFluent(AniEasePower.Weak)))
@@ -199,11 +194,11 @@ Public Class MyLocalModItem
                     If Checked Then
                     If Checked Then
                         RectCheck.Height = 32
                         RectCheck.Height = 32
                         RectCheck.Opacity = 1
                         RectCheck.Opacity = 1
-                        LabTitle.SetResourceReference(TextBlock.ForegroundProperty, If(Entry.State = McMod.McModState.Fine, "ColorBrush2", "ColorBrush5"))
+                        LabTitle.SetResourceReference(TextBlock.ForegroundProperty, If(Entry.IsEnabled, "ColorBrush2", "ColorBrush5"))
                     Else
                     Else
                         RectCheck.Height = 0
                         RectCheck.Height = 0
                         RectCheck.Opacity = 0
                         RectCheck.Opacity = 0
-                        LabTitle.SetResourceReference(TextBlock.ForegroundProperty, If(Entry.State = McMod.McModState.Fine, "ColorBrush1", "ColorBrushGray4"))
+                        LabTitle.SetResourceReference(TextBlock.ForegroundProperty, If(Entry.IsEnabled, "ColorBrush1", "ColorBrushGray4"))
                     End If
                     End If
                     AniStop("MyLocalModItem Checked " & Uuid)
                     AniStop("MyLocalModItem Checked " & Uuid)
                 End If
                 End If
@@ -300,8 +295,8 @@ Public Class MyLocalModItem
 #End Region
 #End Region
 
 
     Private Function GetUpdateCompareDescription() As String
     Private Function GetUpdateCompareDescription() As String
-        Dim CurrentName = If(Entry.CompFile.Version, Entry.CompFile.DisplayName).Replace(".jar", "")
-        Dim NewestName = If(Entry.UpdateFile.Version, Entry.UpdateFile.DisplayName).Replace(".jar", "")
+        Dim CurrentName = If(Entry.ProjectVersion.Version, Entry.ProjectVersion.Display).Replace(".jar", "")
+        Dim NewestName = If(Entry.UpdateFile.Version, Entry.UpdateFile.Display).Replace(".jar", "")
         '简化名称对比
         '简化名称对比
         Dim CurrentSegs = CurrentName.Split("-_+ ".ToCharArray).ToList()
         Dim CurrentSegs = CurrentName.Split("-_+ ".ToCharArray).ToList()
         Dim NewestSegs = NewestName.Split("-_+ ".ToCharArray).ToList()
         Dim NewestSegs = NewestName.Split("-_+ ".ToCharArray).ToList()
@@ -317,7 +312,7 @@ Public Class MyLocalModItem
             NewestName = NewestSegs.Join("-")
             NewestName = NewestSegs.Join("-")
             Entry._Version = CurrentName '使用网络信息作为显示的版本号
             Entry._Version = CurrentName '使用网络信息作为显示的版本号
         End If
         End If
-        Return $"当前版本:{CurrentName}({GetTimeSpanString(Entry.CompFile.ReleaseDate - Date.Now, False)}){vbCrLf}最新版本:{NewestName}({GetTimeSpanString(Entry.UpdateFile.ReleaseDate - Date.Now, False)})"
+        Return $"当前版本:{CurrentName}({GetTimeSpanString(Entry.ProjectVersion.ReleaseDate - Date.Now, False)}){vbCrLf}最新版本:{NewestName}({GetTimeSpanString(Entry.UpdateFile.ReleaseDate - Date.Now, False)})"
     End Function
     End Function
 
 
     '懒加载
     '懒加载
@@ -331,21 +326,16 @@ Public Class MyLocalModItem
         Refresh()
         Refresh()
     End Sub
     End Sub
     Public Sub Refresh()
     Public Sub Refresh()
-        If Not HasEnteredViewport Then Return '避免在未显示时因为 Comp 更新而加载内容
+        If Not HasEnteredViewport Then Return '避免在未显示时因为资源更新而加载内容
         '更新
         '更新
-        If Entry.CanUpdate Then
+        If Entry.HasUpdate Then
             BtnUpdate.Visibility = Visibility.Visible
             BtnUpdate.Visibility = Visibility.Visible
             BtnUpdate.ToolTip = $"{GetUpdateCompareDescription()}{vbCrLf}点击以更新,右键查看更新日志。"
             BtnUpdate.ToolTip = $"{GetUpdateCompareDescription()}{vbCrLf}点击以更新,右键查看更新日志。"
         Else
         Else
             BtnUpdate.Visibility = Visibility.Collapsed
             BtnUpdate.Visibility = Visibility.Collapsed
         End If
         End If
         '标题与描述
         '标题与描述
-        Dim DescFileName As String
-        If Entry.State = McMod.McModState.Fine OrElse Entry.State = McMod.McModState.Disabled Then
-            DescFileName = IO.Path.GetFileNameWithoutExtension(Entry.Path)
-        Else
-            DescFileName = GetFileNameFromPath(Entry.Path)
-        End If
+        Dim DescFileName As String = Path.GetFileNameWithoutExtension(Entry.File.FullName)
         Dim NewDescription As String
         Dim NewDescription As String
         If Settings.Get("ToolModLocalNameStyle") = 1 Then
         If Settings.Get("ToolModLocalNameStyle") = 1 Then
             '标题显示文件名,详情显示译名
             '标题显示文件名,详情显示译名
@@ -353,10 +343,10 @@ Public Class MyLocalModItem
             Title = DescFileName
             Title = DescFileName
             SubTitle = ""
             SubTitle = ""
             '描述
             '描述
-            If Entry.Comp Is Nothing Then
-                NewDescription = Entry.DisplayName
+            If Entry.Project Is Nothing Then
+                NewDescription = Entry.Display
             Else
             Else
-                Dim Titles = Entry.Comp.GetControlTitle(False)
+                Dim Titles = Entry.Project.GetControlTitle(False)
                 NewDescription = Titles.Title & Titles.SubTitle
                 NewDescription = Titles.Title & Titles.SubTitle
             End If
             End If
             NewDescription = NewDescription.Replace("  |  ", " / ")
             NewDescription = NewDescription.Replace("  |  ", " / ")
@@ -364,36 +354,36 @@ Public Class MyLocalModItem
         Else
         Else
             '标题显示译名,详情显示文件名
             '标题显示译名,详情显示文件名
             '标题
             '标题
-            If Entry.Comp Is Nothing Then
-                Title = Entry.DisplayName
+            If Entry.Project Is Nothing Then
+                Title = Entry.Display
                 SubTitle = If(Entry.Version Is Nothing, "", "  |  " & Entry.Version)
                 SubTitle = If(Entry.Version Is Nothing, "", "  |  " & Entry.Version)
             Else
             Else
-                Dim Titles = Entry.Comp.GetControlTitle(False)
+                Dim Titles = Entry.Project.GetControlTitle(False)
                 Title = Titles.Title
                 Title = Titles.Title
                 SubTitle = Titles.SubTitle & If(Entry.Version Is Nothing, "", "  |  " & Entry.Version)
                 SubTitle = Titles.SubTitle & If(Entry.Version Is Nothing, "", "  |  " & Entry.Version)
             End If
             End If
             '描述
             '描述
             NewDescription = DescFileName
             NewDescription = DescFileName
         End If
         End If
-        If Entry.Comp IsNot Nothing Then
-            NewDescription += ": " & Entry.Comp.Description.Replace(vbCr, "").Replace(vbLf, "")
+        If Entry.Project IsNot Nothing Then
+            NewDescription += ": " & Entry.Project.Description.ReplaceLineEndings("")
         ElseIf Entry.Description IsNot Nothing Then
         ElseIf Entry.Description IsNot Nothing Then
-            NewDescription += ": " & Entry.Description.Replace(vbCr, "").Replace(vbLf, "")
+            NewDescription += ": " & Entry.Description.ReplaceLineEndings("")
         End If
         End If
         Description = NewDescription
         Description = NewDescription
         If Checked Then
         If Checked Then
-            LabTitle.SetResourceReference(TextBlock.ForegroundProperty, If(Entry.State = McMod.McModState.Fine, "ColorBrush2", "ColorBrush5"))
+            LabTitle.SetResourceReference(TextBlock.ForegroundProperty, If(Entry.IsEnabled, "ColorBrush2", "ColorBrush5"))
         Else
         Else
-            LabTitle.SetResourceReference(TextBlock.ForegroundProperty, If(Entry.State = McMod.McModState.Fine, "ColorBrush1", "ColorBrushGray4"))
+            LabTitle.SetResourceReference(TextBlock.ForegroundProperty, If(Entry.IsEnabled, "ColorBrush1", "ColorBrushGray4"))
         End If
         End If
         '主 Logo
         '主 Logo
-        If Entry.Comp IsNot Nothing Then
-            Entry.Comp.ApplyLogoToMyImage(PathLogo)
+        If Entry.Project IsNot Nothing Then
+            Entry.Project.ApplyLogoToMyImage(PathLogo)
         Else
         Else
             PathLogo.Source = PathImage & "Icons/NoIcon.png"
             PathLogo.Source = PathImage & "Icons/NoIcon.png"
         End If
         End If
         '图标右下角的 Logo
         '图标右下角的 Logo
-        If Entry.State = McMod.McModState.Fine Then
+        If Entry.IsEnabled Then
             If ImgState IsNot Nothing Then
             If ImgState IsNot Nothing Then
                 Children.Remove(ImgState)
                 Children.Remove(ImgState)
                 ImgState = Nothing
                 ImgState = Nothing
@@ -412,10 +402,10 @@ Public Class MyLocalModItem
                 '       HorizontalAlignment="Right" VerticalAlignment="Bottom"
                 '       HorizontalAlignment="Right" VerticalAlignment="Bottom"
                 '       Source="/Images/Icons/Unavailable.png" />
                 '       Source="/Images/Icons/Unavailable.png" />
             End If
             End If
-            ImgState.Source = New MyBitmap(PathImage & $"Icons/{Entry.State}.png")
+            ImgState.Source = New MyBitmap(PathImage & $"Icons/Disabled.png")
         End If
         End If
         '标签
         '标签
-        If Entry.Comp IsNot Nothing Then Tags = Entry.Comp.Tags
+        If Entry.Project IsNot Nothing Then Tags = Entry.Project.Tags
     End Sub
     End Sub
 
 
     Public Sub RefreshColor(sender As Object, e As EventArgs) Handles Me.MouseEnter, Me.MouseLeave, Me.MouseLeftButtonDown, Me.MouseLeftButtonUp, Me.Changed
     Public Sub RefreshColor(sender As Object, e As EventArgs) Handles Me.MouseEnter, Me.MouseLeave, Me.MouseLeftButtonDown, Me.MouseLeftButtonUp, Me.Changed
@@ -486,7 +476,7 @@ Public Class MyLocalModItem
 
 
     '触发更新
     '触发更新
     Private Sub BtnUpdate_Click(sender As Object, e As EventArgs) Handles BtnUpdate.Click
     Private Sub BtnUpdate_Click(sender As Object, e As EventArgs) Handles BtnUpdate.Click
-        Select Case MyMsgBox($"是否要更新 {Entry.DisplayName}?{vbCrLf}{vbCrLf}{GetUpdateCompareDescription()}", "Mod 更新确认", "更新", "查看更新日志", "取消")
+        Select Case MyMsgBox($"是否要更新 {Entry.Display}?{vbCrLf}{vbCrLf}{GetUpdateCompareDescription()}", "Mod 更新确认", "更新", "查看更新日志", "取消")
             Case 1 '更新
             Case 1 '更新
                 FrmInstanceMod.UpdateMods({Entry})
                 FrmInstanceMod.UpdateMods({Entry})
             Case 2 '查看更新日志
             Case 2 '查看更新日志

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

@@ -1,6 +1,4 @@
-Imports System.IO.Compression
-
-Public Class ExportOption
+Public Class ExportOption
     Public Property Title As String
     Public Property Title As String
     Public Property Description As String
     Public Property Description As String
     Public Property Rules As String
     Public Property Rules As String
@@ -23,7 +21,7 @@ Public Class PageInstanceExport
         AniControlEnabled += 1
         AniControlEnabled += 1
         If CurrentInstance <> PageInstanceLeft.Instance.PathVersion Then RefreshAll() '切换到了另一个版本,重置页面
         If CurrentInstance <> PageInstanceLeft.Instance.PathVersion Then RefreshAll() '切换到了另一个版本,重置页面
         CustomEventService.SetEventData(BtnAdvancedHelp,
         CustomEventService.SetEventData(BtnAdvancedHelp,
-            If(VersionBranchCode = 50, "指南/整合包制作 - Public.json", "指南/整合包制作 - Snapshot.json"))
+            If(BuildType = BuildTypes.Release, "指南/整合包制作 - Public.json", "指南/整合包制作 - Snapshot.json"))
         AniControlEnabled -= 1
         AniControlEnabled -= 1
     End Sub
     End Sub
     Public Sub RefreshAll() Implements IRefreshable.Refresh
     Public Sub RefreshAll() Implements IRefreshable.Refresh
@@ -63,17 +61,17 @@ Public Class PageInstanceExport
             '查找文件夹下的对应项
             '查找文件夹下的对应项
             If AcceptCompressedFile Then
             If AcceptCompressedFile Then
                 For Each File In TargetFolder.EnumerateFiles("*.zip").Concat(TargetFolder.EnumerateFiles("*.rar"))
                 For Each File In TargetFolder.EnumerateFiles("*.zip").Concat(TargetFolder.EnumerateFiles("*.rar"))
-                    If SubOptionBlackList.Any(Function(b) File.Name.ContainsF(b)) Then Continue For
+                    If SubOptionBlackList.Any(Function(b) File.Name.Contains(b)) Then Continue For
                     Panel.Children.Add(New MyCheckBox With {. _
                     Panel.Children.Add(New MyCheckBox With {. _
-                        Tag = New ExportOption With {.Title = File.Name, .DefaultChecked = True, .Rules = EscapeLikePattern($"{Folder}/{File.Name}")}})
+                        Tag = New ExportOption With {.Title = File.Name, .DefaultChecked = True, .Rules = EscapeUtils.LikePatternEscape($"{Folder}/{File.Name}")}})
                 Next
                 Next
             End If
             End If
             If AcceptFolder Then
             If AcceptFolder Then
                 For Each SubFolder In TargetFolder.EnumerateDirectories().OrderByDescending(Function(f) f.LastWriteTime)
                 For Each SubFolder In TargetFolder.EnumerateDirectories().OrderByDescending(Function(f) f.LastWriteTime)
-                    If SubOptionBlackList.Any(Function(b) SubFolder.Name.ContainsF(b)) Then Continue For
+                    If SubOptionBlackList.Any(Function(b) SubFolder.Name.Contains(b)) Then Continue For
                     If Not SubFolder.EnumerateFileSystemInfos().Any() Then Continue For
                     If Not SubFolder.EnumerateFileSystemInfos().Any() Then Continue For
                     Dim NewCheckBox As New MyCheckBox With {. _
                     Dim NewCheckBox As New MyCheckBox With {. _
-                        Tag = New ExportOption With {.Title = SubFolder.Name, .DefaultChecked = True, .Rules = EscapeLikePattern($"{Folder}/{SubFolder.Name}/")}}
+                        Tag = New ExportOption With {.Title = SubFolder.Name, .DefaultChecked = True, .Rules = EscapeUtils.LikePatternEscape($"{Folder}/{SubFolder.Name}/")}}
                     If Panel Is PanOptionsSaves Then GetExportOption(NewCheckBox).Description = SubFolder.LastWriteTime.ToString("yyyy'/'MM'/'dd HH':'mm")
                     If Panel Is PanOptionsSaves Then GetExportOption(NewCheckBox).Description = SubFolder.LastWriteTime.ToString("yyyy'/'MM'/'dd HH':'mm")
                     Panel.Children.Add(NewCheckBox)
                     Panel.Children.Add(NewCheckBox)
                 Next
                 Next
@@ -95,7 +93,7 @@ Public Class PageInstanceExport
         Function(Folder As DirectoryInfo) As Boolean
         Function(Folder As DirectoryInfo) As Boolean
             Try
             Try
                 Return Folder.Exists AndAlso
                 Return Folder.Exists AndAlso
-                    Folder.EnumerateFileSystemInfos().Any(Function(i) Not SubOptionBlackList.Any(Function(b) i.Name.ContainsF(b)))
+                    Folder.EnumerateFileSystemInfos().Any(Function(i) Not SubOptionBlackList.Any(Function(b) i.Name.Contains(b)))
             Catch '一般是由于无法访问,或是一个指向已不存在的文件夹的链接(例如使用 mklink 创造的 resource 文件夹链接)
             Catch '一般是由于无法访问,或是一个指向已不存在的文件夹的链接(例如使用 mklink 创造的 resource 文件夹链接)
                 Return False
                 Return False
             End Try
             End Try
@@ -123,7 +121,7 @@ Public Class PageInstanceExport
                 Try
                 Try
                     If AllEntries.Any(Function(Entry) Entry Like Rule) Then Return True
                     If AllEntries.Any(Function(Entry) Entry Like Rule) Then Return True
                 Catch ex As Exception
                 Catch ex As Exception
-                    Log(ex, $"错误的规则:{Rule}", LogLevel.Hint)
+                    Log(ex, $"错误的规则:{Rule}", NotifyLevel.AllUsers)
                     Return False
                     Return False
                 End Try
                 End Try
                 '粗略检查所有级
                 '粗略检查所有级
@@ -341,11 +339,11 @@ Public Class PageInstanceExport
             ConfigLines.Add(Sperator)
             ConfigLines.Add(Sperator)
             ConfigLines.AddRange(GetExtraFileLines())
             ConfigLines.AddRange(GetExtraFileLines())
             '结束
             '结束
-            WriteFile(ConfigPath, ConfigLines.Join(vbCrLf))
+            FileUtils.Write(ConfigPath, ConfigLines.Join(vbCrLf))
             Hint("已保存配置文件:" & ConfigPath, HintType.Green)
             Hint("已保存配置文件:" & ConfigPath, HintType.Green)
             OpenExplorer(ConfigPath)
             OpenExplorer(ConfigPath)
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "保存配置失败", LogLevel.Msgbox)
+            Log(ex, "保存配置失败", NotifyLevel.MsgBox)
         End Try
         End Try
     End Sub
     End Sub
     '读取配置文件
     '读取配置文件
@@ -371,17 +369,17 @@ Public Class PageInstanceExport
             If Not CheckAdvancedInclude.Checked Then CheckAdvancedModrinth.Checked = Ini.GetOrDefault("ModrinthUploadMode", False) '#7979,加个特判
             If Not CheckAdvancedInclude.Checked Then CheckAdvancedModrinth.Checked = Ini.GetOrDefault("ModrinthUploadMode", False) '#7979,加个特判
             ConfigPackPath = Ini.GetOrDefault("PackPath", Nothing)
             ConfigPackPath = Ini.GetOrDefault("PackPath", Nothing)
             '导出内容段
             '导出内容段
-            RulesOverrides = Segments(1).Replace(vbCr, vbLf).Replace(vbLf & vbLf, vbLf).Split(vbLf).ToList
+            RulesOverrides = Segments(1).ReplaceLineEndings(vbLf, mergeMultiple:=True).Split(vbLf).ToList
             '追加内容段
             '追加内容段
             If Segments.Length > 2 Then
             If Segments.Length > 2 Then
-                ExtraFiles = Segments(2).Replace(vbCr, vbLf).Replace(vbLf & vbLf, vbLf).Split(vbLf).ToList
+                ExtraFiles = Segments(2).ReplaceLineEndings(vbLf, mergeMultiple:=True).Split(vbLf).ToList
             Else
             Else
                 ExtraFiles = Nothing
                 ExtraFiles = Nothing
             End If
             End If
             '结束
             '结束
             Hint("已读取配置文件:" & ConfigPath, HintType.Green)
             Hint("已读取配置文件:" & ConfigPath, HintType.Green)
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "读取配置失败", LogLevel.Msgbox)
+            Log(ex, "读取配置失败", NotifyLevel.MsgBox)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -414,11 +412,11 @@ Public Class PageInstanceExport
         If Not String.IsNullOrWhiteSpace(ConfigPackPath) AndAlso
         If Not String.IsNullOrWhiteSpace(ConfigPackPath) AndAlso
            (Not ConfigPackPath.EndsWithF("\") AndAlso Not ConfigPackPath.EndsWithF("/")) Then
            (Not ConfigPackPath.EndsWithF("\") AndAlso Not ConfigPackPath.EndsWithF("/")) Then
             Try
             Try
-                Directory.CreateDirectory(GetPathFromFullPath(ConfigPackPath))
+                DirectoryUtils.Create(ConfigPackPath, isFilePath:=True)
                 PackPath = ConfigPackPath
                 PackPath = ConfigPackPath
                 Log($"[Export] 使用配置文件中指定的导出路径:{ConfigPackPath}")
                 Log($"[Export] 使用配置文件中指定的导出路径:{ConfigPackPath}")
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, $"无法使用配置文件中指定的导出路径({ConfigPackPath})", LogLevel.Debug)
+                Log(ex, $"无法使用配置文件中指定的导出路径({ConfigPackPath})", NotifyLevel.DebugModeOnly)
                 If MyMsgBox($"指定的路径:{ConfigPackPath}{vbCrLf}{vbCrLf}{ex.GetDetail}", "无法使用配置文件中指定的导出路径", "确定", "取消") = 2 Then Return
                 If MyMsgBox($"指定的路径:{ConfigPackPath}{vbCrLf}{vbCrLf}{ex.GetDetail}", "无法使用配置文件中指定的导出路径", "确定", "取消") = 2 Then Return
             End Try
             End Try
         End If
         End If
@@ -450,23 +448,21 @@ Public Class PageInstanceExport
 
 
 #Region "准备 PCL 文件"
 #Region "准备 PCL 文件"
 
 
-#If Not BETA Then
-        If IncludePCL Then
+        If BuildType <> BuildTypes.Release AndAlso IncludePCL Then
             Loaders.Add(New LoaderTask(Of Integer, Integer)("下载 PCL 正式版",
             Loaders.Add(New LoaderTask(Of Integer, Integer)("下载 PCL 正式版",
             Sub(Loader As LoaderTask(Of Integer, Integer))
             Sub(Loader As LoaderTask(Of Integer, Integer))
                 DownloadLatestPCL(Loader)
                 DownloadLatestPCL(Loader)
                 CopyFile(PathTemp & "Latest.exe", CacheFolder & "Plain Craft Launcher.exe")
                 CopyFile(PathTemp & "Latest.exe", CacheFolder & "Plain Craft Launcher.exe")
             End Sub) With {.ProgressWeight = 0.5, .Block = False})
             End Sub) With {.ProgressWeight = 0.5, .Block = False})
         End If
         End If
-#End If
 
 
 #End Region
 #End Region
 
 
 #Region "复制文件"
 #Region "复制文件"
 
 
-        Loaders.Add(New LoaderTask(Of Integer, List(Of McMod))("复制导出内容",
-        Sub(Loader As LoaderTask(Of Integer, List(Of McMod)))
-            Loader.Output = New List(Of McMod)
+        Loaders.Add(New LoaderTask(Of Integer, List(Of ResourceFile))("复制导出内容",
+        Sub(Loader As LoaderTask(Of Integer, List(Of ResourceFile)))
+            Loader.Output = New List(Of ResourceFile)
             '复制版本文件
             '复制版本文件
             Dim Progress As Integer = 0
             Dim Progress As Integer = 0
             Dim SearchFolder As Action(Of DirectoryInfo)
             Dim SearchFolder As Action(Of DirectoryInfo)
@@ -495,7 +491,7 @@ Public Class PageInstanceExport
                     If CheckHostedAssets AndAlso
                     If CheckHostedAssets AndAlso
                        {".zip", ".rar", ".jar", ".disabled", ".old"}.Contains(Entry.Extension.Lower) AndAlso
                        {".zip", ".rar", ".jar", ".disabled", ".old"}.Contains(Entry.Extension.Lower) AndAlso
                        {"mods", "packs", "openloader", "resource"}.Any(Function(s) RelativePath.Contains(s)) Then
                        {"mods", "packs", "openloader", "resource"}.Any(Function(s) RelativePath.Contains(s)) Then
-                        Dim ModFile As New McMod(TargetPath)
+                        Dim ModFile As New ResourceFile(TargetPath)
                         Dim Unused = ModFile.ModrinthHash '提前计算 Hash
                         Dim Unused = ModFile.ModrinthHash '提前计算 Hash
                         Unused = ModFile.CurseForgeHash
                         Unused = ModFile.CurseForgeHash
                         Loader.Output.Add(ModFile)
                         Loader.Output.Add(ModFile)
@@ -532,19 +528,17 @@ Public Class PageInstanceExport
             '复制 PCL 版本设置
             '复制 PCL 版本设置
             CopyDirectory(McVersion.PathVersion & "PCL\", OverridesFolder & "PCL\")
             CopyDirectory(McVersion.PathVersion & "PCL\", OverridesFolder & "PCL\")
             WriteIni(OverridesFolder & "PCL\Setup.ini", "IsStar", False)
             WriteIni(OverridesFolder & "PCL\Setup.ini", "IsStar", False)
-#If BETA Then
-            '复制 PCL 本体
-            If IncludePCL Then CopyFile(PathWithName, CacheFolder & "Plain Craft Launcher.exe")
-#End If
+            '复制 PCL 本体(正式版)
+            If BuildType = BuildTypes.Release AndAlso IncludePCL Then CopyFile(PathExe, CacheFolder & "Plain Craft Launcher.exe")
             '复制 PCL 个性化内容
             '复制 PCL 个性化内容
             If IncludePCLCustom Then
             If IncludePCLCustom Then
-                If Directory.Exists(Path & "PCL\Pictures\") Then CopyDirectory(Path & "PCL\Pictures\", CacheFolder & "PCL\Pictures\")
-                If Directory.Exists(Path & "PCL\Musics\") Then CopyDirectory(Path & "PCL\Musics\", CacheFolder & "PCL\Musics\")
-                If Directory.Exists(Path & "PCL\Help\") Then CopyDirectory(Path & "PCL\Help\", CacheFolder & "PCL\Help\")
-                If File.Exists(Path & "PCL\Custom.xaml") Then CopyFile(Path & "PCL\Custom.xaml", CacheFolder & "PCL\Custom.xaml")
-                If File.Exists(Path & "PCL\Setup.ini") Then CopyFile(Path & "PCL\Setup.ini", CacheFolder & "PCL\Setup.ini")
-                If File.Exists(Path & "PCL\hints.txt") Then CopyFile(Path & "PCL\hints.txt", CacheFolder & "PCL\hints.txt")
-                If File.Exists(Path & "PCL\Logo.png") Then CopyFile(Path & "PCL\Logo.png", CacheFolder & "PCL\Logo.png")
+                If Directory.Exists(PathExeFolder & "PCL\Pictures\") Then CopyDirectory(PathExeFolder & "PCL\Pictures\", CacheFolder & "PCL\Pictures\")
+                If Directory.Exists(PathExeFolder & "PCL\Musics\") Then CopyDirectory(PathExeFolder & "PCL\Musics\", CacheFolder & "PCL\Musics\")
+                If Directory.Exists(PathExeFolder & "PCL\Help\") Then CopyDirectory(PathExeFolder & "PCL\Help\", CacheFolder & "PCL\Help\")
+                If File.Exists(PathExeFolder & "PCL\Custom.xaml") Then CopyFile(PathExeFolder & "PCL\Custom.xaml", CacheFolder & "PCL\Custom.xaml")
+                If File.Exists(PathExeFolder & "PCL\Setup.ini") Then CopyFile(PathExeFolder & "PCL\Setup.ini", CacheFolder & "PCL\Setup.ini")
+                If File.Exists(PathExeFolder & "PCL\hints.txt") Then CopyFile(PathExeFolder & "PCL\hints.txt", CacheFolder & "PCL\hints.txt")
+                If File.Exists(PathExeFolder & "PCL\Logo.png") Then CopyFile(PathExeFolder & "PCL\Logo.png", CacheFolder & "PCL\Logo.png")
             End If
             End If
         End Sub) With {.ProgressWeight = 5})
         End Sub) With {.ProgressWeight = 5})
 
 
@@ -552,9 +546,9 @@ Public Class PageInstanceExport
 
 
 #Region "联网检查"
 #Region "联网检查"
 
 
-        Loaders.Add(New LoaderTask(Of List(Of McMod), Dictionary(Of McMod, List(Of String)))("联网获取文件信息",
-        Sub(Loader As LoaderTask(Of List(Of McMod), Dictionary(Of McMod, List(Of String))))
-            Loader.Output = New Dictionary(Of McMod, List(Of String))
+        Loaders.Add(New LoaderTask(Of List(Of ResourceFile), Dictionary(Of ResourceFile, List(Of String)))("联网获取文件信息",
+        Sub(Loader As LoaderTask(Of List(Of ResourceFile), Dictionary(Of ResourceFile, List(Of String))))
+            Loader.Output = New Dictionary(Of ResourceFile, List(Of String))
             If Not CheckHostedAssets Then Log($"[Export] 要求跳过联网获取步骤") : Return
             If Not CheckHostedAssets Then Log($"[Export] 要求跳过联网获取步骤") : Return
             If Not Loader.Input.Any Then Log($"[Export] 没有需要联网检查的文件,跳过联网获取步骤") : Return
             If Not Loader.Input.Any Then Log($"[Export] 没有需要联网检查的文件,跳过联网获取步骤") : Return
 
 
@@ -573,7 +567,7 @@ Public Class PageInstanceExport
                         If Not ModrinthRaw.ContainsKey(ModFile.ModrinthHash) Then Continue For
                         If Not ModrinthRaw.ContainsKey(ModFile.ModrinthHash) Then Continue For
                         If ModrinthRaw(ModFile.ModrinthHash)?("files")?(0)("hashes")?("sha1") <> ModFile.ModrinthHash Then Continue For
                         If ModrinthRaw(ModFile.ModrinthHash)?("files")?(0)("hashes")?("sha1") <> ModFile.ModrinthHash Then Continue For
                         '写入下载地址
                         '写入下载地址
-                        Loader.Output.AddToList(ModFile, ModrinthRaw(ModFile.ModrinthHash)("files")(0)("url"))
+                        Loader.Output.AddIntoValueCollection(ModFile, ModrinthRaw(ModFile.ModrinthHash)("files")(0)("url"))
                     Next
                     Next
                     Log($"[Export] 从 Modrinth 获取到 {ModrinthRaw.Count} 个本地资源项的对应信息")
                     Log($"[Export] 从 Modrinth 获取到 {ModrinthRaw.Count} 个本地资源项的对应信息")
                 Catch ex As Exception
                 Catch ex As Exception
@@ -598,11 +592,11 @@ Public Class PageInstanceExport
                         Dim File As JObject = ResultJson("file")
                         Dim File As JObject = ResultJson("file")
                         If String.IsNullOrEmpty(File("downloadUrl")) Then Continue For
                         If String.IsNullOrEmpty(File("downloadUrl")) Then Continue For
                         '查找对应的文件
                         '查找对应的文件
-                        Dim ModFile As McMod = Loader.Input.FirstOrDefault(Function(m) m.CurseForgeHash = File("fileFingerprint").ToObject(Of UInteger))
+                        Dim ModFile As ResourceFile = Loader.Input.FirstOrDefault(Function(m) m.CurseForgeHash = File("fileFingerprint").ToObject(Of UInteger))
                         If ModFile Is Nothing Then Continue For
                         If ModFile Is Nothing Then Continue For
                         '写入下载地址
                         '写入下载地址
-                        For Each Address In CompFile.HandleCurseForgeDownloadUrls(File("downloadUrl").ToString)
-                            Loader.Output.AddToList(ModFile, Address)
+                        For Each Address In ResourceVersion.ParseCurseForgeDownloadUrls(File("downloadUrl").ToString)
+                            Loader.Output.AddIntoValueCollection(ModFile, Address)
                         Next
                         Next
                     Next
                     Next
                     Log($"[Export] 从 CurseForge 获取到 {CurseForgeRaw.Count} 个本地资源项的对应信息")
                     Log($"[Export] 从 CurseForge 获取到 {CurseForgeRaw.Count} 个本地资源项的对应信息")
@@ -639,19 +633,19 @@ Public Class PageInstanceExport
 
 
 #Region "生成压缩包"
 #Region "生成压缩包"
 
 
-        Loaders.Add(New LoaderTask(Of Dictionary(Of McMod, List(Of String)), Integer)("生成压缩包",
-        Sub(Loader As LoaderTask(Of Dictionary(Of McMod, List(Of String)), Integer))
+        Loaders.Add(New LoaderTask(Of Dictionary(Of ResourceFile, List(Of String)), Integer)("生成压缩包",
+        Sub(Loader As LoaderTask(Of Dictionary(Of ResourceFile, List(Of String)), Integer))
             '整理文件列表
             '整理文件列表
             Dim Files As New JArray
             Dim Files As New JArray
             For Each Pair In Loader.Input
             For Each Pair In Loader.Input
-                Dim ModFile As McMod = Pair.Key
+                Dim ModFile As ResourceFile = Pair.Key
                 Files.Add(New JObject From {
                 Files.Add(New JObject From {
-                    {"path", ModFile.Path.AfterFirst(OverridesFolder).Replace("\", "/")},
-                    {"hashes", New JObject From {{"sha1", ModFile.ModrinthHash}, {"sha512", GetFileSHA512(ModFile.Path)}}},
+                    {"path", ModFile.File.FullName.AfterFirst(OverridesFolder).Replace("\", "/")},
+                    {"hashes", New JObject From {{"sha1", ModFile.ModrinthHash}, {"sha512", GetFileSHA512(ModFile.File.FullName)}}},
                     {"downloads", New JArray(Pair.Value.OrderBy(Function(u) u.Contains("modrinth.com")))}, '不优先选择 Modrinth
                     {"downloads", New JArray(Pair.Value.OrderBy(Function(u) u.Contains("modrinth.com")))}, '不优先选择 Modrinth
-                    {"fileSize", New FileInfo(ModFile.Path).Length}
+                    {"fileSize", New FileInfo(ModFile.File.FullName).Length}
                 })
                 })
-                File.Delete(ModFile.Path)
+                ModFile.File.Delete()
             Next
             Next
             Loader.Progress = 0.2
             Loader.Progress = 0.2
             '导出最终 JSON 文件
             '导出最终 JSON 文件
@@ -670,7 +664,7 @@ Public Class PageInstanceExport
             }
             }
             File.WriteAllText(CacheFolder & "modpack\modrinth.index.json", ResultJson.ToString(Newtonsoft.Json.Formatting.Indented))
             File.WriteAllText(CacheFolder & "modpack\modrinth.index.json", ResultJson.ToString(Newtonsoft.Json.Formatting.Indented))
             '打包
             '打包
-            Directory.CreateDirectory(GetPathFromFullPath(PackPath))
+            DirectoryUtils.Create(PackPath, isFilePath:=True)
             If File.Exists(PackPath) Then File.Delete(PackPath)
             If File.Exists(PackPath) Then File.Delete(PackPath)
             If IncludePCL Then
             If IncludePCL Then
                 '首次压缩整合包
                 '首次压缩整合包

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

@@ -65,7 +65,7 @@
             PageChangeRun(PageGet(ID))
             PageChangeRun(PageGet(ID))
             PageID = ID
             PageID = ID
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "切换分页面失败(ID " & ID & ")", LogLevel.Feedback)
+            Log(ex, "切换分页面失败(ID " & ID & ")", NotifyLevel.MsgBoxAndFeedback)
         Finally
         Finally
             AniControlEnabled -= 1
             AniControlEnabled -= 1
         End Try
         End Try

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

@@ -6,7 +6,7 @@
     Private IsLoad As Boolean = False
     Private IsLoad As Boolean = False
     Public Sub PageOther_Loaded() Handles Me.Loaded
     Public Sub PageOther_Loaded() Handles Me.Loaded
 
 
-        If FrmMain.PageLast.Page <> FormMain.PageType.CompDetail Then PanBack.ScrollToHome()
+        If FrmMain.PageLast.Page <> FormMain.PageType.ResourceDetail Then PanBack.ScrollToHome()
         AniControlEnabled += 1
         AniControlEnabled += 1
         SelectedMods.Clear()
         SelectedMods.Clear()
         ReloadModList()
         ReloadModList()
@@ -40,8 +40,8 @@
     End Sub
     End Sub
     Public Shared Sub Refresh()
     Public Shared Sub Refresh()
         Try
         Try
-            CompProjectCache.Clear()
-            CompFilesCache.Clear()
+            ResourceProject.Cache.Clear()
+            ResourceVersion.ProjectFilesCache.Clear()
             File.Delete(PathTemp & "Cache\LocalMod.json")
             File.Delete(PathTemp & "Cache\LocalMod.json")
             Log("[Mod] 由于点击刷新按钮,清理本地 Mod 信息缓存")
             Log("[Mod] 由于点击刷新按钮,清理本地 Mod 信息缓存")
         Catch ex As Exception
         Catch ex As Exception
@@ -71,7 +71,7 @@
     ''' <summary>
     ''' <summary>
     ''' 已加载的 Mod UI 缓存,不确保按显示顺序排列。Key 为 Mod 的 RawFileName。
     ''' 已加载的 Mod UI 缓存,不确保按显示顺序排列。Key 为 Mod 的 RawFileName。
     ''' </summary>
     ''' </summary>
-    Public ModItems As New SafeDictionary(Of String, MyLocalModItem)
+    Public ModItems As New ConcurrentDictionary(Of String, MyLocalModItem)
     ''' <summary>
     ''' <summary>
     ''' 将加载器结果的 Mod 列表加载为 UI。
     ''' 将加载器结果的 Mod 列表加载为 UI。
     ''' </summary>
     ''' </summary>
@@ -87,24 +87,22 @@
                 Return
                 Return
             End If
             End If
             '修改缓存
             '修改缓存
-            SyncLock ModItems.SyncRoot
-                ModItems.Clear()
-                For Each ModEntity As McMod In McModLoader.Output
-                    ModItems(ModEntity.RawFileName) = McModListItem(ModEntity)
-                Next
-            End SyncLock
+            ModItems.Clear()
+            For Each ModEntity As ResourceFile In McModLoader.Output
+                ModItems(ModEntity.EnabledName) = McModListItem(ModEntity)
+            Next
             '显示结果
             '显示结果
             Filter = FilterType.All
             Filter = FilterType.All
             SearchBox.Text = "" '这会触发结果刷新,所以需要在 ModItems 更新之后,详见 #3124 的视频
             SearchBox.Text = "" '这会触发结果刷新,所以需要在 ModItems 更新之后,详见 #3124 的视频
             RefreshUI()
             RefreshUI()
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "加载 Mod 列表 UI 失败", LogLevel.Feedback)
+            Log(ex, "加载 Mod 列表 UI 失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
-    Private Function McModListItem(Entry As McMod) As MyLocalModItem
+    Private Function McModListItem(Entry As ResourceFile) As MyLocalModItem
         AniControlEnabled += 1
         AniControlEnabled += 1
         Dim NewItem As New MyLocalModItem With {.SnapsToDevicePixels = True, .Entry = Entry,
         Dim NewItem As New MyLocalModItem With {.SnapsToDevicePixels = True, .Entry = Entry,
-            .ButtonHandler = AddressOf McModContent, .Checked = SelectedMods.Contains(Entry.RawFileName)}
+            .ButtonHandler = AddressOf McModContent, .Checked = SelectedMods.Contains(Entry.EnabledName)}
         AddHandler Entry.OnUpdate, Sub() RunInUi(AddressOf NewItem.Refresh)
         AddHandler Entry.OnUpdate, Sub() RunInUi(AddressOf NewItem.Refresh)
         AniControlEnabled -= 1
         AniControlEnabled -= 1
         Return NewItem
         Return NewItem
@@ -133,8 +131,8 @@
         ToolTipService.SetVerticalOffset(BtnDelete, 30)
         ToolTipService.SetVerticalOffset(BtnDelete, 30)
         ToolTipService.SetHorizontalOffset(BtnDelete, 2)
         ToolTipService.SetHorizontalOffset(BtnDelete, 2)
         AddHandler BtnDelete.Click, AddressOf Delete_Click
         AddHandler BtnDelete.Click, AddressOf Delete_Click
-        Dim BtnED As New MyIconButton With {.LogoScale = 1, .Logo = If(sender.Entry.State = McMod.McModState.Fine, Logo.IconButtonStop, Logo.IconButtonCheck), .Tag = sender}
-        BtnED.ToolTip = If(sender.Entry.State = McMod.McModState.Fine, "禁用", "启用")
+        Dim BtnED As New MyIconButton With {.LogoScale = 1, .Logo = If(sender.Entry.IsEnabled, Logo.IconButtonStop, Logo.IconButtonCheck), .Tag = sender}
+        BtnED.ToolTip = If(sender.Entry.IsEnabled, "禁用", "启用")
         ToolTipService.SetPlacement(BtnED, Primitives.PlacementMode.Center)
         ToolTipService.SetPlacement(BtnED, Primitives.PlacementMode.Center)
         ToolTipService.SetVerticalOffset(BtnED, 30)
         ToolTipService.SetVerticalOffset(BtnED, 30)
         ToolTipService.SetHorizontalOffset(BtnED, 2)
         ToolTipService.SetHorizontalOffset(BtnED, 2)
@@ -147,24 +145,23 @@
     ''' </summary>
     ''' </summary>
     Public Sub RefreshUI()
     Public Sub RefreshUI()
         If PanList Is Nothing Then Return
         If PanList Is Nothing Then Return
-        Dim ShowingMods = If(IsSearching, SearchResult, If(McModLoader.Output, New List(Of McMod))).Where(Function(m) CanPassFilter(m)).ToList
+        Dim ShowingMods = If(IsSearching, SearchResult, If(McModLoader.Output, New List(Of ResourceFile))).Where(Function(m) CanPassFilter(m)).ToList
         '重新列出列表
         '重新列出列表
         AniControlEnabled += 1
         AniControlEnabled += 1
         If ShowingMods.Any() Then
         If ShowingMods.Any() Then
             PanList.Visibility = Visibility.Visible
             PanList.Visibility = Visibility.Visible
             PanList.Children.Clear()
             PanList.Children.Clear()
-            SyncLock ModItems.SyncRoot
-                For Each TargetMod In ShowingMods
-                    Dim Item As MyLocalModItem = ModItems(TargetMod.RawFileName)
-                    Item.Checked = SelectedMods.Contains(TargetMod.RawFileName) '更新选中状态
-                    PanList.Children.Add(Item)
-                Next
-            End SyncLock
+            For Each TargetMod In ShowingMods
+                Dim Item As MyLocalModItem = Nothing
+                If Not ModItems.TryGetValue(TargetMod.EnabledName, Item) Then Continue For
+                Item.Checked = SelectedMods.Contains(TargetMod.EnabledName) '更新选中状态
+                PanList.Children.Add(Item)
+            Next
         Else
         Else
             PanList.Visibility = Visibility.Collapsed
             PanList.Visibility = Visibility.Collapsed
         End If
         End If
         AniControlEnabled -= 1
         AniControlEnabled -= 1
-        SelectedMods = SelectedMods.Where(Function(m) ShowingMods.Any(Function(s) s.RawFileName = m)).ToList '取消选中已经不显示的 Mod
+        SelectedMods = SelectedMods.Where(Function(m) ShowingMods.Any(Function(s) s.EnabledName = m)).ToList '取消选中已经不显示的 Mod
         RefreshBars()
         RefreshBars()
     End Sub
     End Sub
 
 
@@ -172,6 +169,7 @@
     ''' 刷新顶栏和底栏显示。
     ''' 刷新顶栏和底栏显示。
     ''' </summary>
     ''' </summary>
     Public Sub RefreshBars()
     Public Sub RefreshBars()
+
         '-----------------
         '-----------------
         ' 顶部栏
         ' 顶部栏
         '-----------------
         '-----------------
@@ -179,14 +177,13 @@
         '计数
         '计数
         Dim AnyCount As Integer = 0
         Dim AnyCount As Integer = 0
         Dim EnabledCount As Integer = 0
         Dim EnabledCount As Integer = 0
-        Dim DisabledCount As Integer = 0
         Dim UpdateCount As Integer = 0
         Dim UpdateCount As Integer = 0
-        For Each ModItem In If(IsSearching, SearchResult, If(McModLoader.Output, New List(Of McMod)))
+        For Each ModItem In If(IsSearching, SearchResult, If(McModLoader.Output, New List(Of ResourceFile)))
             AnyCount += 1
             AnyCount += 1
-            If ModItem.CanUpdate Then UpdateCount += 1
-            If ModItem.State.Equals(McMod.McModState.Fine) Then EnabledCount += 1
-            If ModItem.State.Equals(McMod.McModState.Disabled) Then DisabledCount += 1
+            If ModItem.HasUpdate Then UpdateCount += 1
+            If ModItem.IsEnabled Then EnabledCount += 1
         Next
         Next
+        Dim DisabledCount As Integer = AnyCount - EnabledCount
         '显示
         '显示
         BtnFilterAll.Text = If(IsSearching, "搜索结果", "全部") & $" ({AnyCount})"
         BtnFilterAll.Text = If(IsSearching, "搜索结果", "全部") & $" ({AnyCount})"
         BtnFilterCanUpdate.Text = $"可更新 ({UpdateCount})"
         BtnFilterCanUpdate.Text = $"可更新 ({UpdateCount})"
@@ -210,14 +207,10 @@
             Dim HasEnabled As Boolean = False
             Dim HasEnabled As Boolean = False
             Dim HasDisabled As Boolean = False
             Dim HasDisabled As Boolean = False
             For Each ModEntity In McModLoader.Output
             For Each ModEntity In McModLoader.Output
-                If SelectedMods.Contains(ModEntity.RawFileName) Then
-                    If ModEntity.CanUpdate Then HasUpdate = True
-                    If ModEntity.State = McMod.McModState.Fine Then
-                        HasEnabled = True
-                    ElseIf ModEntity.State = McMod.McModState.Disabled Then
-                        HasDisabled = True
-                    End If
-                End If
+                If Not SelectedMods.Contains(ModEntity.EnabledName) Then Continue For
+                If ModEntity.HasUpdate Then HasUpdate = True
+                If ModEntity.IsEnabled Then HasEnabled = True
+                If Not ModEntity.IsEnabled Then HasDisabled = True
             Next
             Next
             BtnSelectDisable.IsEnabled = HasEnabled
             BtnSelectDisable.IsEnabled = HasEnabled
             BtnSelectEnable.IsEnabled = HasDisabled
             BtnSelectEnable.IsEnabled = HasDisabled
@@ -278,10 +271,10 @@
     ''' </summary>
     ''' </summary>
     Private Sub BtnManageOpen_Click(sender As Object, e As EventArgs) Handles BtnManageOpen.Click, BtnHintOpen.Click
     Private Sub BtnManageOpen_Click(sender As Object, e As EventArgs) Handles BtnManageOpen.Click, BtnHintOpen.Click
         Try
         Try
-            Directory.CreateDirectory(PageInstanceLeft.Instance.PathIndie & "mods\")
+            DirectoryUtils.Create(PageInstanceLeft.Instance.PathIndie & "mods\")
             OpenExplorer(PageInstanceLeft.Instance.PathIndie & "mods\")
             OpenExplorer(PageInstanceLeft.Instance.PathIndie & "mods\")
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "打开 Mods 文件夹失败", LogLevel.Msgbox)
+            Log(ex, "打开 Mods 文件夹失败", NotifyLevel.MsgBox)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -342,7 +335,7 @@ Install:
                     LoaderFolderRun(McModLoader, Instance.PathIndie & "mods\", LoaderFolderRunType.ForceRun)
                     LoaderFolderRun(McModLoader, Instance.PathIndie & "mods\", LoaderFolderRunType.ForceRun)
                 End If
                 End If
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "复制 Mod 文件失败", LogLevel.Msgbox)
+                Log(ex, "复制 Mod 文件失败", NotifyLevel.MsgBox)
             End Try
             End Try
         End If
         End If
         Return True
         Return True
@@ -353,8 +346,8 @@ Install:
     ''' </summary>
     ''' </summary>
     Private Sub BtnManageDownload_Click(sender As Object, e As MouseButtonEventArgs) Handles BtnManageDownload.Click, BtnHintDownload.Click
     Private Sub BtnManageDownload_Click(sender As Object, e As MouseButtonEventArgs) Handles BtnManageDownload.Click, BtnHintDownload.Click
         '预设置当前版本和搜索框内容
         '预设置当前版本和搜索框内容
-        PageComp.TargetInstance = PageInstanceLeft.Instance
-        PageComp.TargetName = SearchBox.Text
+        PageResource.TargetInstance = PageInstanceLeft.Instance
+        PageResource.TargetName = SearchBox.Text
         FrmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadMod)
         FrmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadMod)
     End Sub
     End Sub
 
 
@@ -369,7 +362,7 @@ Install:
     Public Sub CheckChanged(sender As MyLocalModItem, e As RouteEventArgs)
     Public Sub CheckChanged(sender As MyLocalModItem, e As RouteEventArgs)
         If AniControlEnabled <> 0 Then Return
         If AniControlEnabled <> 0 Then Return
         '更新选择了的内容
         '更新选择了的内容
-        Dim SelectedKey As String = sender.Entry.RawFileName
+        Dim SelectedKey As String = sender.Entry.EnabledName
         If sender.Checked Then
         If sender.Checked Then
             If Not SelectedMods.Contains(SelectedKey) Then SelectedMods.Add(SelectedKey)
             If Not SelectedMods.Contains(SelectedKey) Then SelectedMods.Add(SelectedKey)
         Else
         Else
@@ -386,7 +379,7 @@ Install:
             '#4992,Mod 从过滤器看可能不应在列表中,但因为刚切换状态所以依然保留在列表中,所以应该从列表 UI 判断,而非从过滤器判断
             '#4992,Mod 从过滤器看可能不应在列表中,但因为刚切换状态所以依然保留在列表中,所以应该从列表 UI 判断,而非从过滤器判断
             Dim ShouldSelected As Boolean = Value AndAlso PanList.Children.Contains(Item)
             Dim ShouldSelected As Boolean = Value AndAlso PanList.Children.Contains(Item)
             Item.Checked = ShouldSelected
             Item.Checked = ShouldSelected
-            If ShouldSelected Then SelectedMods.Add(Item.Entry.RawFileName)
+            If ShouldSelected Then SelectedMods.Add(Item.Entry.EnabledName)
         Next
         Next
         AniControlEnabled -= 1
         AniControlEnabled -= 1
         RefreshBars()
         RefreshBars()
@@ -442,16 +435,16 @@ Install:
     ''' <summary>
     ''' <summary>
     ''' 检查该 Mod 项是否符合当前筛选的类别。
     ''' 检查该 Mod 项是否符合当前筛选的类别。
     ''' </summary>
     ''' </summary>
-    Private Function CanPassFilter(CheckingMod As McMod) As Boolean
+    Private Function CanPassFilter(CheckingMod As ResourceFile) As Boolean
         Select Case Filter
         Select Case Filter
             Case FilterType.All
             Case FilterType.All
                 Return True
                 Return True
             Case FilterType.Enabled
             Case FilterType.Enabled
-                Return CheckingMod.State = McMod.McModState.Fine
+                Return CheckingMod.IsEnabled
             Case FilterType.Disabled
             Case FilterType.Disabled
-                Return CheckingMod.State = McMod.McModState.Disabled
+                Return Not CheckingMod.IsEnabled
             Case FilterType.CanUpdate
             Case FilterType.CanUpdate
-                Return CheckingMod.CanUpdate
+                Return CheckingMod.HasUpdate
             Case Else
             Case Else
                 Return False
                 Return False
         End Select
         End Select
@@ -469,51 +462,51 @@ Install:
 
 
     '启用 / 禁用
     '启用 / 禁用
     Private Sub BtnSelectED_Click(sender As MyIconTextButton, e As RouteEventArgs) Handles BtnSelectEnable.Click, BtnSelectDisable.Click
     Private Sub BtnSelectED_Click(sender As MyIconTextButton, e As RouteEventArgs) Handles BtnSelectEnable.Click, BtnSelectDisable.Click
-        EDMods(McModLoader.Output.Where(Function(m) SelectedMods.Contains(m.RawFileName)),
+        EDMods(McModLoader.Output.Where(Function(m) SelectedMods.Contains(m.EnabledName)),
                Not sender.Equals(BtnSelectDisable))
                Not sender.Equals(BtnSelectDisable))
         ChangeAllSelected(False)
         ChangeAllSelected(False)
     End Sub
     End Sub
-    Private Sub EDMods(ModList As IEnumerable(Of McMod), IsEnable As Boolean)
+    Private Sub EDMods(ModList As IEnumerable(Of ResourceFile), IsEnable As Boolean)
         Dim IsSuccessful As Boolean = True
         Dim IsSuccessful As Boolean = True
         For Each ModE In ModList.ToList
         For Each ModE In ModList.ToList
             Dim ModEntity = ModE '仅用于去除迭代变量无法修改的限制
             Dim ModEntity = ModE '仅用于去除迭代变量无法修改的限制
             Dim NewPath As String = Nothing
             Dim NewPath As String = Nothing
-            If ModEntity.State = McMod.McModState.Fine AndAlso Not IsEnable Then
+            If ModEntity.IsEnabled AndAlso Not IsEnable Then
                 '禁用
                 '禁用
-                NewPath = ModEntity.Path & If(File.Exists(ModEntity.Path & ".old"), ".old", ".disabled")
-            ElseIf ModEntity.State = McMod.McModState.Disabled AndAlso IsEnable Then
+                NewPath = ModEntity.File.FullName & If(File.Exists(ModEntity.File.FullName & ".old"), ".old", ".disabled")
+            ElseIf Not ModEntity.IsEnabled AndAlso IsEnable Then
                 '启用
                 '启用
-                NewPath = ModEntity.RawPath
+                NewPath = ModEntity.EnabledFullName
             Else
             Else
                 Continue For
                 Continue For
             End If
             End If
             '重命名
             '重命名
             Try
             Try
                 If File.Exists(NewPath) Then
                 If File.Exists(NewPath) Then
-                    If File.Exists(ModEntity.Path) Then
+                    If File.Exists(ModEntity.File.FullName) Then
                         '同时存在两个名称的 Mod
                         '同时存在两个名称的 Mod
-                        If GetFileMD5(ModEntity.Path) <> GetFileMD5(NewPath) Then
-                            MyMsgBox($"目前同时存在启用和禁用的两个 Mod 文件:{vbCrLf} - {NewPath}{vbCrLf} - {ModEntity.Path}{vbCrLf}{vbCrLf}注意,这两个文件的内容并不相同。{vbCrLf}在手动删除或重命名其中一个文件后,才能继续操作。", "存在文件冲突")
+                        If GetFileMD5(ModEntity.File.FullName) <> GetFileMD5(NewPath) Then
+                            MyMsgBox($"目前同时存在启用和禁用的两个 Mod 文件:{vbCrLf} - {NewPath}{vbCrLf} - {ModEntity.File.FullName}{vbCrLf}{vbCrLf}注意,这两个文件的内容并不相同。{vbCrLf}在手动删除或重命名其中一个文件后,才能继续操作。", "存在文件冲突")
                             Continue For
                             Continue For
                         End If
                         End If
                     Else
                     Else
                         '已经重命名过了
                         '已经重命名过了
-                        Log("[Mod] Mod 的状态已被切换", LogLevel.Debug)
+                        Log("[Mod] Mod 的状态已被切换", NotifyLevel.DebugModeOnly)
                         Continue For
                         Continue For
                     End If
                     End If
                 End If
                 End If
                 File.Delete(NewPath)
                 File.Delete(NewPath)
-                File.Move(ModEntity.Path, NewPath)
+                File.Move(ModEntity.File.FullName, NewPath)
             Catch ex As FileNotFoundException
             Catch ex As FileNotFoundException
-                Log(ex, $"未找到需要重命名的 Mod({If(ModEntity.Path, "null")})", LogLevel.Feedback)
+                Log(ex, $"未找到需要重命名的 Mod({If(ModEntity.File.FullName, "null")})", NotifyLevel.MsgBoxAndFeedback)
                 ReloadModList(True)
                 ReloadModList(True)
                 Return
                 Return
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, $"重命名 Mod 失败({If(ModEntity.Path, "null")})")
+                Log(ex, $"重命名 Mod 失败({If(ModEntity.File.FullName, "null")})")
                 IsSuccessful = False
                 IsSuccessful = False
             End Try
             End Try
             '更改 Loader 中的列表
             '更改 Loader 中的列表
-            Dim NewModEntity As New McMod(NewPath)
+            Dim NewModEntity As New ResourceFile(NewPath)
             NewModEntity.FromJson(ModEntity.ToJson)
             NewModEntity.FromJson(ModEntity.ToJson)
             If McModLoader.Output.Contains(ModEntity) Then
             If McModLoader.Output.Contains(ModEntity) Then
                 Dim IndexOfLoader As Integer = McModLoader.Output.IndexOf(ModEntity)
                 Dim IndexOfLoader As Integer = McModLoader.Output.IndexOf(ModEntity)
@@ -527,7 +520,7 @@ Install:
             End If
             End If
             '更改 UI 中的列表
             '更改 UI 中的列表
             Dim NewItem As MyLocalModItem = McModListItem(NewModEntity)
             Dim NewItem As MyLocalModItem = McModListItem(NewModEntity)
-            ModItems(ModEntity.RawFileName) = NewItem
+            ModItems(ModEntity.EnabledName) = NewItem
             Dim IndexOfUi As Integer = PanList.Children.IndexOf(PanList.Children.OfType(Of MyLocalModItem).FirstOrDefault(Function(i) i.Entry Is ModEntity))
             Dim IndexOfUi As Integer = PanList.Children.IndexOf(PanList.Children.OfType(Of MyLocalModItem).FirstOrDefault(Function(i) i.Entry Is ModEntity))
             If IndexOfUi = -1 Then Continue For '因为未知原因 Mod 的状态已经切换完了
             If IndexOfUi = -1 Then Continue For '因为未知原因 Mod 的状态已经切换完了
             PanList.Children.RemoveAt(IndexOfUi)
             PanList.Children.RemoveAt(IndexOfUi)
@@ -545,7 +538,7 @@ Install:
 
 
     '更新
     '更新
     Private Sub BtnSelectUpdate_Click() Handles BtnSelectUpdate.Click
     Private Sub BtnSelectUpdate_Click() Handles BtnSelectUpdate.Click
-        Dim UpdateList As List(Of McMod) = McModLoader.Output.Where(Function(m) SelectedMods.Contains(m.RawFileName) AndAlso m.CanUpdate).ToList()
+        Dim UpdateList As List(Of ResourceFile) = McModLoader.Output.Where(Function(m) SelectedMods.Contains(m.EnabledName) AndAlso m.HasUpdate).ToList()
         If Not UpdateList.Any() Then Return
         If Not UpdateList.Any() Then Return
         UpdateMods(UpdateList)
         UpdateMods(UpdateList)
         ChangeAllSelected(False)
         ChangeAllSelected(False)
@@ -554,7 +547,7 @@ Install:
     ''' 记录正在进行 Mod 更新的 mods 文件夹路径。
     ''' 记录正在进行 Mod 更新的 mods 文件夹路径。
     ''' </summary>
     ''' </summary>
     Public Shared UpdatingInstanceModFolders As New List(Of String)
     Public Shared UpdatingInstanceModFolders As New List(Of String)
-    Public Sub UpdateMods(ModList As IEnumerable(Of McMod))
+    Public Sub UpdateMods(ModList As IEnumerable(Of ResourceFile))
         '更新前警告
         '更新前警告
         If Not Settings.Get("HintUpdateMod") OrElse ModList.Count >= 15 Then
         If Not Settings.Get("HintUpdateMod") OrElse ModList.Count >= 15 Then
             If MyMsgBox($"新版本 Mod 可能不兼容旧存档或者其他 Mod,这可能导致游戏崩溃,甚至永久损坏存档!{vbCrLf}如果你在游玩整合包,请千万不要自行更新 Mod!{vbCrLf}{vbCrLf}在更新前,请先备份存档,并检查 Mod 的更新日志。{vbCrLf}如果更新后出现问题,你也可以在回收站找回更新前的 Mod。", "Mod 更新警告", "我已了解风险,继续更新", "取消", IsWarn:=True) = 1 Then
             If MyMsgBox($"新版本 Mod 可能不兼容旧存档或者其他 Mod,这可能导致游戏崩溃,甚至永久损坏存档!{vbCrLf}如果你在游玩整合包,请千万不要自行更新 Mod!{vbCrLf}{vbCrLf}在更新前,请先备份存档,并检查 Mod 的更新日志。{vbCrLf}如果更新后出现问题,你也可以在回收站找回更新前的 Mod。", "Mod 更新警告", "我已了解风险,继续更新", "取消", IsWarn:=True) = 1 Then
@@ -568,11 +561,11 @@ Install:
             ModList = ModList.ToList() '防止刷新影响迭代器
             ModList = ModList.ToList() '防止刷新影响迭代器
             Dim FileList As New List(Of NetFile)
             Dim FileList As New List(Of NetFile)
             Dim FileCopyList As New Dictionary(Of String, String)
             Dim FileCopyList As New Dictionary(Of String, String)
-            For Each Entry As McMod In ModList
-                Dim File As CompFile = Entry.UpdateFile
-                If Not File.Available Then Continue For
+            For Each Entry As ResourceFile In ModList
+                Dim File As ResourceVersion = Entry.UpdateFile
+                If Not File.DownloadAvailable Then Continue For
                 '确认更新后的文件名
                 '确认更新后的文件名
-                Dim CurrentReplaceName = Entry.CompFile.FileName.Replace(".jar", "").Replace(".old", "").Replace(".disabled", "")
+                Dim CurrentReplaceName = Entry.ProjectVersion.FileName.Replace(".jar", "").Replace(".old", "").Replace(".disabled", "")
                 Dim NewestReplaceName = Entry.UpdateFile.FileName.Replace(".jar", "").Replace(".old", "").Replace(".disabled", "")
                 Dim NewestReplaceName = Entry.UpdateFile.FileName.Replace(".jar", "").Replace(".old", "").Replace(".disabled", "")
                 Dim CurrentSegs = CurrentReplaceName.Split("-"c).ToList()
                 Dim CurrentSegs = CurrentReplaceName.Split("-"c).ToList()
                 Dim NewestSegs = NewestReplaceName.Split("-"c).ToList()
                 Dim NewestSegs = NewestReplaceName.Split("-"c).ToList()
@@ -596,8 +589,8 @@ Install:
                     NewestReplaceName = NewestSegs.Join("-")
                     NewestReplaceName = NewestSegs.Join("-")
                 End If
                 End If
                 '添加到下载列表
                 '添加到下载列表
-                Dim TempAddress As String = PathTemp & "mods\" & Entry.FileName.Replace(CurrentReplaceName, NewestReplaceName) '需要下载到 mods 文件夹,以便重复文件检查时识别
-                Dim RealAddress As String = GetPathFromFullPath(Entry.Path) & Entry.FileName.Replace(CurrentReplaceName, NewestReplaceName)
+                Dim TempAddress As String = PathTemp & "mods\" & Entry.File.Name.Replace(CurrentReplaceName, NewestReplaceName) '需要下载到 mods 文件夹,以便重复文件检查时识别
+                Dim RealAddress As String = GetPathFromFullPath(Entry.File.FullName) & Entry.File.Name.Replace(CurrentReplaceName, NewestReplaceName)
                 FileList.Add(File.ToNetFile(TempAddress))
                 FileList.Add(File.ToNetFile(TempAddress))
                 FileCopyList(TempAddress) = RealAddress
                 FileCopyList(TempAddress) = RealAddress
             Next
             Next
@@ -608,23 +601,23 @@ Install:
             InstallLoaders.Add(New LoaderTask(Of Integer, Integer)("替换旧版 Mod 文件",
             InstallLoaders.Add(New LoaderTask(Of Integer, Integer)("替换旧版 Mod 文件",
             Sub()
             Sub()
                 Try
                 Try
-                    For Each Entry As McMod In ModList
-                        If File.Exists(Entry.Path) Then
-                            My.Computer.FileSystem.DeleteFile(Entry.Path, FileIO.UIOption.OnlyErrorDialogs, FileIO.RecycleOption.SendToRecycleBin)
+                    For Each Entry As ResourceFile In ModList
+                        If File.Exists(Entry.File.FullName) Then
+                            My.Computer.FileSystem.DeleteFile(Entry.File.FullName, FileIO.UIOption.OnlyErrorDialogs, FileIO.RecycleOption.SendToRecycleBin)
                         Else
                         Else
-                            Log($"[Mod] 未找到更新前的 Mod 文件,跳过对它的删除:{Entry.Path}", LogLevel.Debug)
+                            Log($"[Mod] 未找到更新前的 Mod 文件,跳过对它的删除:{Entry.File.FullName}", NotifyLevel.DebugModeOnly)
                         End If
                         End If
                     Next
                     Next
                     For Each Entry As KeyValuePair(Of String, String) In FileCopyList
                     For Each Entry As KeyValuePair(Of String, String) In FileCopyList
                         If File.Exists(Entry.Value) Then
                         If File.Exists(Entry.Value) Then
                             My.Computer.FileSystem.DeleteFile(Entry.Value, FileIO.UIOption.OnlyErrorDialogs, FileIO.RecycleOption.SendToRecycleBin)
                             My.Computer.FileSystem.DeleteFile(Entry.Value, FileIO.UIOption.OnlyErrorDialogs, FileIO.RecycleOption.SendToRecycleBin)
-                            Log($"[Mod] 更新后的 Mod 文件已存在,将会把它放入回收站:{Entry.Value}", LogLevel.Debug)
+                            Log($"[Mod] 更新后的 Mod 文件已存在,将会把它放入回收站:{Entry.Value}", NotifyLevel.DebugModeOnly)
                         End If
                         End If
                         If Directory.Exists(GetPathFromFullPath(Entry.Value)) Then
                         If Directory.Exists(GetPathFromFullPath(Entry.Value)) Then
                             File.Move(Entry.Key, Entry.Value)
                             File.Move(Entry.Key, Entry.Value)
                             FinishedFileNames.Add(GetFileNameFromPath(Entry.Value))
                             FinishedFileNames.Add(GetFileNameFromPath(Entry.Value))
                         Else
                         Else
-                            Log($"[Mod] 更新后的目标文件夹已被删除:{Entry.Value}", LogLevel.Debug)
+                            Log($"[Mod] 更新后的目标文件夹已被删除:{Entry.Value}", NotifyLevel.DebugModeOnly)
                         End If
                         End If
                     Next
                     Next
                 Catch ex As OperationCanceledException
                 Catch ex As OperationCanceledException
@@ -632,7 +625,7 @@ Install:
                 End Try
                 End Try
             End Sub))
             End Sub))
             '结束处理
             '结束处理
-            Dim Loader As New LoaderCombo(Of IEnumerable(Of McMod))("Mod 更新:" & PageInstanceLeft.Instance.Name, InstallLoaders)
+            Dim Loader As New LoaderCombo(Of IEnumerable(Of ResourceFile))("Mod 更新:" & PageInstanceLeft.Instance.Name, InstallLoaders)
             Dim PathMods As String = PageInstanceLeft.Instance.PathIndie & "mods\"
             Dim PathMods As String = PageInstanceLeft.Instance.PathIndie & "mods\"
             Loader.OnStateChanged =
             Loader.OnStateChanged =
             Sub()
             Sub()
@@ -683,45 +676,45 @@ Install:
 
 
     '删除
     '删除
     Private Sub BtnSelectDelete_Click() Handles BtnSelectDelete.Click
     Private Sub BtnSelectDelete_Click() Handles BtnSelectDelete.Click
-        DeleteMods(McModLoader.Output.Where(Function(m) SelectedMods.Contains(m.RawFileName)))
+        DeleteMods(McModLoader.Output.Where(Function(m) SelectedMods.Contains(m.EnabledName)))
         ChangeAllSelected(False)
         ChangeAllSelected(False)
     End Sub
     End Sub
-    Private Sub DeleteMods(ModList As IEnumerable(Of McMod))
+    Private Sub DeleteMods(ModList As IEnumerable(Of ResourceFile))
         Try
         Try
             Dim IsSuccessful As Boolean = True
             Dim IsSuccessful As Boolean = True
             Dim IsShiftPressed As Boolean = My.Computer.Keyboard.ShiftKeyDown
             Dim IsShiftPressed As Boolean = My.Computer.Keyboard.ShiftKeyDown
             '确认需要删除的文件
             '确认需要删除的文件
             ModList = ModList.SelectMany(
             ModList = ModList.SelectMany(
-            Function(Target As McMod)
-                If Target.State = McMod.McModState.Fine Then
-                    Return {Target.Path, Target.Path & If(File.Exists(Target.Path & ".old"), ".old", ".disabled")}
+            Function(Target As ResourceFile)
+                If Target.IsEnabled Then
+                    Return {Target.File.FullName, Target.File.FullName & If(File.Exists(Target.File.FullName & ".old"), ".old", ".disabled")}
                 Else
                 Else
-                    Return {Target.Path, Target.RawPath}
+                    Return {Target.File.FullName, Target.EnabledFullName}
                 End If
                 End If
-            End Function).Distinct.Where(Function(m) File.Exists(m)).Select(Function(m) New McMod(m)).ToList()
+            End Function).Distinct.Where(Function(m) File.Exists(m)).Select(Function(m) New ResourceFile(m)).ToList()
             '实际删除文件
             '实际删除文件
             For Each ModEntity In ModList
             For Each ModEntity In ModList
                 '删除
                 '删除
                 Try
                 Try
                     If IsShiftPressed Then
                     If IsShiftPressed Then
-                        File.Delete(ModEntity.Path)
+                        ModEntity.File.Delete()
                     Else
                     Else
-                        My.Computer.FileSystem.DeleteFile(ModEntity.Path, FileIO.UIOption.OnlyErrorDialogs, FileIO.RecycleOption.SendToRecycleBin)
+                        My.Computer.FileSystem.DeleteFile(ModEntity.File.FullName, FileIO.UIOption.OnlyErrorDialogs, FileIO.RecycleOption.SendToRecycleBin)
                     End If
                     End If
                 Catch ex As OperationCanceledException
                 Catch ex As OperationCanceledException
                     Log(ex, "删除 Mod 被主动取消")
                     Log(ex, "删除 Mod 被主动取消")
                     ReloadModList(True)
                     ReloadModList(True)
                     Return
                     Return
                 Catch ex As Exception
                 Catch ex As Exception
-                    Log(ex, $"删除 Mod 失败({ModEntity.Path})", LogLevel.Msgbox)
+                    Log(ex, $"删除 Mod 失败({ModEntity.File.FullName})", NotifyLevel.MsgBox)
                     IsSuccessful = False
                     IsSuccessful = False
                 End Try
                 End Try
                 '取消选中
                 '取消选中
-                SelectedMods.Remove(ModEntity.RawFileName)
+                SelectedMods.Remove(ModEntity.EnabledName)
                 '更改 Loader 和 UI 中的列表
                 '更改 Loader 和 UI 中的列表
                 McModLoader.Output.Remove(ModEntity)
                 McModLoader.Output.Remove(ModEntity)
                 SearchResult?.Remove(ModEntity)
                 SearchResult?.Remove(ModEntity)
-                ModItems.Remove(ModEntity.RawFileName)
+                ModItems.Remove(ModEntity.EnabledName)
                 Dim IndexOfUi As Integer = PanList.Children.IndexOf(PanList.Children.OfType(Of MyLocalModItem).FirstOrDefault(Function(i) i.Entry.Equals(ModEntity)))
                 Dim IndexOfUi As Integer = PanList.Children.IndexOf(PanList.Children.OfType(Of MyLocalModItem).FirstOrDefault(Function(i) i.Entry.Equals(ModEntity)))
                 If IndexOfUi >= 0 Then PanList.Children.RemoveAt(IndexOfUi)
                 If IndexOfUi >= 0 Then PanList.Children.RemoveAt(IndexOfUi)
             Next
             Next
@@ -738,13 +731,13 @@ Install:
             If Not IsSuccessful Then Return
             If Not IsSuccessful Then Return
             If IsShiftPressed Then
             If IsShiftPressed Then
                 If ModList.IsSingle Then
                 If ModList.IsSingle Then
-                    Hint($"已彻底删除 {ModList.Single.FileName}!", HintType.Green)
+                    Hint($"已彻底删除 {ModList.Single.File.Name}!", HintType.Green)
                 Else
                 Else
                     Hint($"已彻底删除 {ModList.Count} 个文件!", HintType.Green)
                     Hint($"已彻底删除 {ModList.Count} 个文件!", HintType.Green)
                 End If
                 End If
             Else
             Else
                 If ModList.IsSingle Then
                 If ModList.IsSingle Then
-                    Hint($"已将 {ModList.Single.FileName} 删除到回收站!", HintType.Green)
+                    Hint($"已将 {ModList.Single.File.Name} 删除到回收站!", HintType.Green)
                 Else
                 Else
                     Hint($"已将 {ModList.Count} 个文件删除到回收站!", HintType.Green)
                     Hint($"已将 {ModList.Count} 个文件删除到回收站!", HintType.Green)
                 End If
                 End If
@@ -753,7 +746,7 @@ Install:
             Log(ex, "删除 Mod 被主动取消")
             Log(ex, "删除 Mod 被主动取消")
             ReloadModList(True)
             ReloadModList(True)
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "删除 Mod 出现未知错误", LogLevel.Feedback)
+            Log(ex, "删除 Mod 出现未知错误", NotifyLevel.MsgBoxAndFeedback)
             ReloadModList(True)
             ReloadModList(True)
         End Try
         End Try
         LoaderRun(LoaderFolderRunType.UpdateOnly)
         LoaderRun(LoaderFolderRunType.UpdateOnly)
@@ -772,21 +765,21 @@ Install:
     Public Sub Info_Click(sender As Object, e As EventArgs)
     Public Sub Info_Click(sender As Object, e As EventArgs)
         Try
         Try
 
 
-            Dim ModEntry As McMod = CType(If(TypeOf sender Is MyIconButton, sender.Tag, sender), MyLocalModItem).Entry
+            Dim ModEntry As ResourceFile = CType(If(TypeOf sender Is MyIconButton, sender.Tag, sender), MyLocalModItem).Entry
             '加载失败信息
             '加载失败信息
-            If ModEntry.Comp IsNot Nothing Then
+            If ModEntry.Project IsNot Nothing Then
                 '跳转到 Mod 下载页面
                 '跳转到 Mod 下载页面
-                FrmMain.PageChange(New FormMain.PageStackData With {.Page = FormMain.PageType.CompDetail,
-                    .Additional = {ModEntry.Comp, New List(Of String), PageInstanceLeft.Instance.Version.VanillaName,
-                        If(PageInstanceLeft.Instance.Version.HasForge, CompModLoaderType.Forge,
-                        If(PageInstanceLeft.Instance.Version.HasNeoForge, CompModLoaderType.NeoForge,
-                        If(PageInstanceLeft.Instance.Version.HasFabric, CompModLoaderType.Fabric, CompModLoaderType.Any))),
-                        CompType.Mod}})
+                FrmMain.PageChange(New FormMain.PageStackData With {.Page = FormMain.PageType.ResourceDetail,
+                    .Additional = {ModEntry.Project, New List(Of String), PageInstanceLeft.Instance.Version.VanillaName,
+                        If(PageInstanceLeft.Instance.Version.HasForge, ModLoaderTypes.Forge,
+                        If(PageInstanceLeft.Instance.Version.HasNeoForge, ModLoaderTypes.NeoForge,
+                        If(PageInstanceLeft.Instance.Version.HasFabric, ModLoaderTypes.Fabric, ModLoaderTypes.Any))),
+                        ResourceTypes.Mod}})
             Else
             Else
                 '获取信息
                 '获取信息
                 Dim ContentLines As New List(Of String)
                 Dim ContentLines As New List(Of String)
                 If ModEntry.Description IsNot Nothing Then ContentLines.Add(ModEntry.Description & vbCrLf)
                 If ModEntry.Description IsNot Nothing Then ContentLines.Add(ModEntry.Description & vbCrLf)
-                ContentLines.Add("文件:" & ModEntry.FileName & "(" & GetString(New FileInfo(ModEntry.Path).Length) & ")")
+                ContentLines.Add("文件:" & ModEntry.File.Name & "(" & FormatFileSize(ModEntry.File.Length) & ")")
                 If ModEntry.Version IsNot Nothing Then ContentLines.Add("版本:" & ModEntry.Version)
                 If ModEntry.Version IsNot Nothing Then ContentLines.Add("版本:" & ModEntry.Version)
                 Dim DebugInfo As New List(Of String)
                 Dim DebugInfo As New List(Of String)
                 If DebugInfo.Any Then
                 If DebugInfo.Any Then
@@ -794,7 +787,7 @@ Install:
                     ContentLines.AddRange(DebugInfo)
                     ContentLines.AddRange(DebugInfo)
                 End If
                 End If
                 '获取用于搜索的 Mod 名称
                 '获取用于搜索的 Mod 名称
-                Dim ModOriginalName As String = ModEntry.DisplayName.Replace(" ", "+")
+                Dim ModOriginalName As String = ModEntry.Display.Replace(" ", "+")
                 Dim ModSearchName As String = ModOriginalName.Substring(0, 1)
                 Dim ModSearchName As String = ModOriginalName.Substring(0, 1)
                 For i = 1 To ModOriginalName.Count - 1
                 For i = 1 To ModOriginalName.Count - 1
                     Dim IsLastLower As Boolean = ModOriginalName(i - 1).ToString.Lower.Equals(ModOriginalName(i - 1).ToString)
                     Dim IsLastLower As Boolean = ModOriginalName(i - 1).ToString.Lower.Equals(ModOriginalName(i - 1).ToString)
@@ -807,21 +800,21 @@ Install:
                 Next
                 Next
                 ModSearchName = ModSearchName.Replace("++", "+").Replace("pti+Fine", "ptiFine")
                 ModSearchName = ModSearchName.Replace("++", "+").Replace("pti+Fine", "ptiFine")
                 '百科搜索链接
                 '百科搜索链接
-                If MyMsgBox(ContentLines.Join(vbCrLf), ModEntry.DisplayName, "百科搜索", "返回") = 1 Then
+                If MyMsgBox(ContentLines.Join(vbCrLf), ModEntry.Display, "百科搜索", "返回") = 1 Then
                     OpenWebsite("https://www.mcmod.cn/s?key=" & ModSearchName & "&site=all&filter=0")
                     OpenWebsite("https://www.mcmod.cn/s?key=" & ModSearchName & "&site=all&filter=0")
                 End If
                 End If
             End If
             End If
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "获取 Mod 详情失败", LogLevel.Feedback)
+            Log(ex, "获取 Mod 详情失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
     '打开文件所在的位置
     '打开文件所在的位置
     Public Sub Open_Click(sender As MyIconButton, e As EventArgs)
     Public Sub Open_Click(sender As MyIconButton, e As EventArgs)
         Try
         Try
             Dim ListItem As MyLocalModItem = sender.Tag
             Dim ListItem As MyLocalModItem = sender.Tag
-            OpenExplorer(ListItem.Entry.Path)
+            OpenExplorer(ListItem.Entry.File.FullName)
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "打开 Mod 文件位置失败", LogLevel.Feedback)
+            Log(ex, "打开 Mod 文件位置失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
     '删除
     '删除
@@ -832,7 +825,7 @@ Install:
     '启用 / 禁用
     '启用 / 禁用
     Public Sub ED_Click(sender As MyIconButton, e As EventArgs)
     Public Sub ED_Click(sender As MyIconButton, e As EventArgs)
         Dim ListItem As MyLocalModItem = sender.Tag
         Dim ListItem As MyLocalModItem = sender.Tag
-        EDMods({ListItem.Entry}, ListItem.Entry.State = McMod.McModState.Disabled)
+        EDMods({ListItem.Entry}, Not ListItem.Entry.IsEnabled)
     End Sub
     End Sub
 
 
 #End Region
 #End Region
@@ -844,28 +837,28 @@ Install:
             Return Not String.IsNullOrWhiteSpace(SearchBox.Text)
             Return Not String.IsNullOrWhiteSpace(SearchBox.Text)
         End Get
         End Get
     End Property
     End Property
-    Private SearchResult As List(Of McMod)
+    Private SearchResult As List(Of ResourceFile)
     Public Sub SearchRun() Handles SearchBox.TextChanged
     Public Sub SearchRun() Handles SearchBox.TextChanged
         If IsSearching Then
         If IsSearching Then
             '构造请求
             '构造请求
-            Dim QueryList As New List(Of SearchEntry(Of McMod))
-            For Each Entry As McMod In McModLoader.Output
+            Dim QueryList As New List(Of SearchEntry(Of ResourceFile))
+            For Each Entry As ResourceFile In McModLoader.Output
                 Dim SearchSources As New List(Of SearchSource)
                 Dim SearchSources As New List(Of SearchSource)
-                SearchSources.Add(New SearchSource(Entry.DisplayName, 1))
-                SearchSources.Add(New SearchSource(Entry.FileName, 1))
+                SearchSources.Add(New SearchSource(Entry.Display, 1))
+                SearchSources.Add(New SearchSource(Entry.File.Name, 1))
                 If Entry.Version IsNot Nothing Then
                 If Entry.Version IsNot Nothing Then
                     SearchSources.Add(New SearchSource(Entry.Version, 0.2))
                     SearchSources.Add(New SearchSource(Entry.Version, 0.2))
                 End If
                 End If
                 If Entry.Description IsNot Nothing AndAlso Entry.Description <> "" Then
                 If Entry.Description IsNot Nothing AndAlso Entry.Description <> "" Then
                     SearchSources.Add(New SearchSource(Entry.Description, 0.4))
                     SearchSources.Add(New SearchSource(Entry.Description, 0.4))
                 End If
                 End If
-                If Entry.Comp IsNot Nothing Then
-                    If Entry.Comp.RawName <> Entry.DisplayName Then SearchSources.Add(New SearchSource(Entry.Comp.RawName, 1))
-                    If Entry.Comp.TranslatedName <> Entry.Comp.RawName Then SearchSources.Add(New SearchSource(Entry.Comp.TranslatedName, 1))
-                    If Entry.Comp.Description <> Entry.Description Then SearchSources.Add(New SearchSource(Entry.Comp.Description, 0.4))
-                    SearchSources.Add(New SearchSource(String.Join("", Entry.Comp.Tags), 0.2))
+                If Entry.Project IsNot Nothing Then
+                    If Entry.Project.RawName <> Entry.Display Then SearchSources.Add(New SearchSource(Entry.Project.RawName, 1))
+                    If Entry.Project.TranslatedName <> Entry.Project.RawName Then SearchSources.Add(New SearchSource(Entry.Project.TranslatedName, 1))
+                    If Entry.Project.Description <> Entry.Description Then SearchSources.Add(New SearchSource(Entry.Project.Description, 0.4))
+                    SearchSources.Add(New SearchSource(String.Join("", Entry.Project.Tags), 0.2))
                 End If
                 End If
-                QueryList.Add(New SearchEntry(Of McMod) With {.Item = Entry, .SearchSource = SearchSources})
+                QueryList.Add(New SearchEntry(Of ResourceFile) With {.Item = Entry, .SearchSource = SearchSources})
             Next
             Next
             '进行搜索
             '进行搜索
             SearchResult = Search(QueryList, SearchBox.Text, MaxBlurCount:=6, MinBlurSimilarity:=0.35).Select(Function(r) r.Item).ToList
             SearchResult = Search(QueryList, SearchBox.Text, MaxBlurCount:=6, MinBlurSimilarity:=0.35).Select(Function(r) r.Item).ToList

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

@@ -65,7 +65,7 @@
                 WriteIni(McFolderSelected & "PCL.ini", "InstanceCache", "") '要求刷新缓存
                 WriteIni(McFolderSelected & "PCL.ini", "InstanceCache", "") '要求刷新缓存
                 LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
                 LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "修改版本分类失败(" & PageInstanceLeft.Instance.Name & ")", LogLevel.Feedback)
+                Log(ex, "修改版本分类失败(" & PageInstanceLeft.Instance.Name & ")", NotifyLevel.MsgBoxAndFeedback)
             End Try
             End Try
             Reload() '更新 “打开 Mod 文件夹” 按钮
             Reload() '更新 “打开 Mod 文件夹” 按钮
         Else
         Else
@@ -82,7 +82,7 @@
                 WriteIni(McFolderSelected & "PCL.ini", "InstanceCache", "") '要求刷新缓存
                 WriteIni(McFolderSelected & "PCL.ini", "InstanceCache", "") '要求刷新缓存
                 LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
                 LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "隐藏版本 " & PageInstanceLeft.Instance.Name & " 失败", LogLevel.Feedback)
+                Log(ex, "隐藏版本 " & PageInstanceLeft.Instance.Name & " 失败", NotifyLevel.MsgBoxAndFeedback)
             End Try
             End Try
         End If
         End If
     End Sub
     End Sub
@@ -97,7 +97,7 @@
             Reload()
             Reload()
             LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
             LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "版本 " & PageInstanceLeft.Instance.Name & " 描述更改失败", LogLevel.Msgbox)
+            Log(ex, "版本 " & PageInstanceLeft.Instance.Name & " 描述更改失败", NotifyLevel.MsgBox)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -153,7 +153,7 @@
             End If
             End If
             '替换版本设置文件中的路径
             '替换版本设置文件中的路径
             If File.Exists(NewPath & "PCL\Setup.ini") Then
             If File.Exists(NewPath & "PCL\Setup.ini") Then
-                WriteFile(NewPath & "PCL\Setup.ini", ReadFile(NewPath & "PCL\Setup.ini").Replace(OldPath, NewPath))
+                FileUtils.Write(NewPath & "PCL\Setup.ini", ReadFile(NewPath & "PCL\Setup.ini").Replace(OldPath, NewPath))
             End If
             End If
             '更改已选中的版本
             '更改已选中的版本
             If ReadIni(McFolderSelected & "PCL.ini", "Version") = OldName Then
             If ReadIni(McFolderSelected & "PCL.ini", "Version") = OldName Then
@@ -162,7 +162,7 @@
             '更新版本 Json
             '更新版本 Json
             Try
             Try
                 JsonObject("id") = NewName
                 JsonObject("id") = NewName
-                WriteFile(NewPath & NewName & ".json", JsonObject.ToString)
+                FileUtils.Write(NewPath & NewName & ".json", JsonObject.ToString)
                 File.Delete(NewPath & GetFileNameFromPath(OldJsonPath))
                 File.Delete(NewPath & GetFileNameFromPath(OldJsonPath))
             Catch ex As Exception
             Catch ex As Exception
                 Log(ex, "重命名版本 json 失败")
                 Log(ex, "重命名版本 json 失败")
@@ -174,7 +174,7 @@
             Reload()
             Reload()
             LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
             LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "重命名版本失败", LogLevel.Msgbox)
+            Log(ex, "重命名版本失败", NotifyLevel.MsgBox)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -194,7 +194,7 @@
                 File.Delete(PageInstanceLeft.Instance.PathVersion & "PCL\Logo.png")
                 File.Delete(PageInstanceLeft.Instance.PathVersion & "PCL\Logo.png")
             End If
             End If
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "更改自定义版本图标失败(" & PageInstanceLeft.Instance.Name & ")", LogLevel.Feedback)
+            Log(ex, "更改自定义版本图标失败(" & PageInstanceLeft.Instance.Name & ")", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
         '进行更改
         '进行更改
         Try
         Try
@@ -207,7 +207,7 @@
             Reload()
             Reload()
             LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
             LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "更改版本图标失败(" & PageInstanceLeft.Instance.Name & ")", LogLevel.Feedback)
+            Log(ex, "更改版本图标失败(" & PageInstanceLeft.Instance.Name & ")", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -220,7 +220,7 @@
             McInstanceListForceRefresh = True
             McInstanceListForceRefresh = True
             LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
             LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\")
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "版本 " & PageInstanceLeft.Instance.Name & " 收藏状态更改失败", LogLevel.Msgbox)
+            Log(ex, "版本 " & PageInstanceLeft.Instance.Name & " 收藏状态更改失败", NotifyLevel.MsgBox)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -238,16 +238,16 @@
 
 
     '存档文件夹
     '存档文件夹
     Private Sub BtnFolderSaves_Click() Handles BtnFolderSaves.Click
     Private Sub BtnFolderSaves_Click() Handles BtnFolderSaves.Click
-        Dim FolderPath As String = PageInstanceLeft.Instance.PathIndie & "saves\"
-        Directory.CreateDirectory(FolderPath)
-        OpenExplorer(FolderPath)
+        Dim Folder As String = PageInstanceLeft.Instance.PathIndie & "saves\"
+        DirectoryUtils.Create(Folder)
+        OpenExplorer(Folder)
     End Sub
     End Sub
 
 
     'Mod 文件夹
     'Mod 文件夹
     Private Sub BtnFolderMods_Click() Handles BtnFolderMods.Click
     Private Sub BtnFolderMods_Click() Handles BtnFolderMods.Click
-        Dim FolderPath As String = PageInstanceLeft.Instance.PathIndie & "mods\"
-        Directory.CreateDirectory(FolderPath)
-        OpenExplorer(FolderPath)
+        Dim Folder As String = PageInstanceLeft.Instance.PathIndie & "mods\"
+        DirectoryUtils.Create(Folder)
+        OpenExplorer(Folder)
     End Sub
     End Sub
 
 
 #End Region
 #End Region
@@ -274,7 +274,7 @@
                 End If
                 End If
             End If
             End If
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "导出启动脚本失败(" & PageInstanceLeft.Instance.Name & ")", LogLevel.Msgbox)
+            Log(ex, "导出启动脚本失败(" & PageInstanceLeft.Instance.Name & ")", NotifyLevel.MsgBox)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -310,7 +310,7 @@
             FrmMain.BtnExtraDownload.ShowRefresh()
             FrmMain.BtnExtraDownload.ShowRefresh()
             FrmMain.BtnExtraDownload.Ribble()
             FrmMain.BtnExtraDownload.Ribble()
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "尝试补全文件失败(" & PageInstanceLeft.Instance.Name & ")", LogLevel.Msgbox)
+            Log(ex, "尝试补全文件失败(" & PageInstanceLeft.Instance.Name & ")", NotifyLevel.MsgBox)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -340,7 +340,7 @@
         Catch ex As OperationCanceledException
         Catch ex As OperationCanceledException
             Log(ex, "删除版本 " & PageInstanceLeft.Instance.Name & " 被主动取消")
             Log(ex, "删除版本 " & PageInstanceLeft.Instance.Name & " 被主动取消")
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "删除版本 " & PageInstanceLeft.Instance.Name & " 失败", LogLevel.Msgbox)
+            Log(ex, "删除版本 " & PageInstanceLeft.Instance.Name & " 失败", NotifyLevel.MsgBox)
         End Try
         End Try
     End Sub
     End Sub
 
 

+ 24 - 23
Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSetup.xaml

@@ -27,16 +27,16 @@
                             <RowDefinition Height="28" />
                             <RowDefinition Height="28" />
                         </Grid.RowDefinitions>
                         </Grid.RowDefinitions>
                         <TextBlock Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Left" Text="版本隔离" Margin="0,0,25,0" />
                         <TextBlock Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Left" Text="版本隔离" Margin="0,0,25,0" />
-                        <local:MyComboBox x:Name="ComboArgumentIndieV2" Grid.Row="0" Grid.ColumnSpan="2" Tag="VersionArgumentIndieV2" Grid.Column="1">
+                        <local:MyComboBox x:Name="ComboArgumentIndieV2" Grid.Row="0" Grid.ColumnSpan="2" Grid.Column="1">
                             <local:MyComboBoxItem Content="开启" ToolTipService.InitialShowDelay="0" ToolTipService.BetweenShowDelay="0"
                             <local:MyComboBoxItem Content="开启" ToolTipService.InitialShowDelay="0" ToolTipService.BetweenShowDelay="0"
                                                   ToolTip="与其他版本的存档、Mod 等文件相互独立,互不干涉。&#xa;这会使你无法跨版本共享存档,但可以规避 Mod 冲突问题。" ToolTipService.HorizontalOffset="100"/>
                                                   ToolTip="与其他版本的存档、Mod 等文件相互独立,互不干涉。&#xa;这会使你无法跨版本共享存档,但可以规避 Mod 冲突问题。" ToolTipService.HorizontalOffset="100"/>
                             <local:MyComboBoxItem Content="关闭" ToolTipService.InitialShowDelay="0" ToolTipService.BetweenShowDelay="0"
                             <local:MyComboBoxItem Content="关闭" ToolTipService.InitialShowDelay="0" ToolTipService.BetweenShowDelay="0"
                                                   ToolTip="与其余关闭隔离的版本共享存档、Mod 等文件。&#xa;若存在多个安装了 Mod 的版本,可能会由于 Mod 冲突而导致崩溃。" ToolTipService.HorizontalOffset="100"/>
                                                   ToolTip="与其余关闭隔离的版本共享存档、Mod 等文件。&#xa;若存在多个安装了 Mod 的版本,可能会由于 Mod 冲突而导致崩溃。" ToolTipService.HorizontalOffset="100"/>
                         </local:MyComboBox>
                         </local:MyComboBox>
                         <TextBlock Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Left" Text="游戏窗口标题" Margin="0,0,25,0" />
                         <TextBlock Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Left" Text="游戏窗口标题" Margin="0,0,25,0" />
-                        <local:MyTextBox x:Name="TextArgumentTitle" Grid.Row="2" Grid.ColumnSpan="2" Tag="VersionArgumentTitle" Grid.Column="1" ToolTip="自定义游戏窗口的标题,若留空则跟随全局设置的值。&#xa;你可以用替换标记来动态改变其内容,在 更多 → 帮助 中搜索 “替换标记” 以了解用法。" HintText="跟随全局设置" MaxLength="100" />
+                        <local:MyTextBox x:Name="TextArgumentTitle" Grid.Row="2" Grid.ColumnSpan="2" local:SettingService.Key="VersionArgumentTitle" Grid.Column="1" ToolTip="自定义游戏窗口的标题,若留空则跟随全局设置的值。&#xa;你可以用替换标记来动态改变其内容,在 更多 → 帮助 中搜索 “替换标记” 以了解用法。" HintText="跟随全局设置" MaxLength="100" />
                         <TextBlock Grid.Row="4" VerticalAlignment="Center" HorizontalAlignment="Left" Text="自定义信息" Margin="0,0,25,0" />
                         <TextBlock Grid.Row="4" VerticalAlignment="Center" HorizontalAlignment="Left" Text="自定义信息" Margin="0,0,25,0" />
-                        <local:MyTextBox x:Name="TextArgumentInfo" Grid.Row="4" Grid.ColumnSpan="2" Tag="VersionArgumentInfo" Grid.Column="1" HintText="跟随全局设置" ToolTip="该信息会显示在游戏主界面的左下角,与 F3 调试页面的左上角。&#xa;若留空,则跟随全局设置的值。&#xa;&#xa;你可以用替换标记来动态改变其内容,在 更多 → 帮助 中搜索 “替换标记” 以了解用法。">
+                        <local:MyTextBox x:Name="TextArgumentInfo" Grid.Row="4" Grid.ColumnSpan="2" local:SettingService.Key="VersionArgumentInfo" Grid.Column="1" HintText="跟随全局设置" ToolTip="该信息会显示在游戏主界面的左下角,与 F3 调试页面的左上角。&#xa;若留空,则跟随全局设置的值。&#xa;&#xa;你可以用替换标记来动态改变其内容,在 更多 → 帮助 中搜索 “替换标记” 以了解用法。">
                             <local:MyTextBox.ValidateRules>
                             <local:MyTextBox.ValidateRules>
                                 <local:ValidateExcept>
                                 <local:ValidateExcept>
                                     <local:ValidateExcept.Excepts>
                                     <local:ValidateExcept.Excepts>
@@ -70,11 +70,11 @@
                             <RowDefinition Height="9" />
                             <RowDefinition Height="9" />
                             <RowDefinition Height="Auto" />
                             <RowDefinition Height="Auto" />
                         </Grid.RowDefinitions>
                         </Grid.RowDefinitions>
-                        <local:MyRadioBox Text="跟随全局设置" Width="110" Checked="True" x:Name="RadioRamType2" Tag="VersionRamType/2" Margin="0,0,20,9" />
-                        <local:MyRadioBox Grid.Row="1" Text="自动配置" Width="110" x:Name="RadioRamType0" Tag="VersionRamType/0" Margin="0,0,20,0"
+                        <local:MyRadioBox Text="跟随全局设置" Width="110" Checked="True" x:Name="RadioRamType2" local:SettingService.Key="VersionRamType" local:SettingService.Value="2" Margin="0,0,20,9" />
+                        <local:MyRadioBox Grid.Row="1" Text="自动配置" Width="110" x:Name="RadioRamType0" local:SettingService.Key="VersionRamType" local:SettingService.Value="0" Margin="0,0,20,0"
                                           ToolTip="根据安装的 Mod 量与电脑剩余内存动态调整为游戏分配的内存" ToolTipService.Placement="Right" ToolTipService.HorizontalOffset="-10" ToolTipService.VerticalOffset="-3" />
                                           ToolTip="根据安装的 Mod 量与电脑剩余内存动态调整为游戏分配的内存" ToolTipService.Placement="Right" ToolTipService.HorizontalOffset="-10" ToolTipService.VerticalOffset="-3" />
-                        <local:MyRadioBox Grid.Row="3" Text="自定义" x:Name="RadioRamType1" Tag="VersionRamType/1" Margin="0,0,20,0" />
-                        <local:MySlider Grid.Row="3" Grid.Column="1" IsEnabled="False" x:Name="SliderRamCustom" Tag="VersionRamCustom" MaxValue="49" Value="13" />
+                        <local:MyRadioBox Grid.Row="3" Text="自定义" x:Name="RadioRamType1" local:SettingService.Key="VersionRamType" local:SettingService.Value="1" Margin="0,0,20,0" />
+                        <local:MySlider Grid.Row="3" Grid.Column="1" IsEnabled="False" x:Name="SliderRamCustom" local:SettingService.Key="VersionRamCustom" MaxValue="49" Value="13" />
                         <!--<local:MyTextBox Grid.Row="2" Grid.Column="3" Width="100" VerticalAlignment="Center" Margin="30,0,0,0" Text="2048 MB" />-->
                         <!--<local:MyTextBox Grid.Row="2" Grid.Column="3" Width="100" VerticalAlignment="Center" Margin="30,0,0,0" Text="2048 MB" />-->
                     </Grid>
                     </Grid>
                     <Grid Margin="0,5,0,0">
                     <Grid Margin="0,5,0,0">
@@ -87,7 +87,7 @@
                             <RowDefinition Height="28" />
                             <RowDefinition Height="28" />
                         </Grid.RowDefinitions>
                         </Grid.RowDefinitions>
                         <TextBlock Margin="0,6,30,0" Text="启动游戏前进行内存优化" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Left" />
                         <TextBlock Margin="0,6,30,0" Text="启动游戏前进行内存优化" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Left" />
-                        <local:MyComboBox x:Name="ComboRamOptimize" Margin="10,0,0,0" Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="2" Tag="VersionRamOptimize">
+                        <local:MyComboBox x:Name="ComboRamOptimize" Margin="10,0,0,0" Grid.Row="1" Grid.ColumnSpan="2" Grid.Column="2" local:SettingService.Key="VersionRamOptimize">
                             <local:MyComboBoxItem Content="跟随全局设置" IsSelected="True" />
                             <local:MyComboBoxItem Content="跟随全局设置" IsSelected="True" />
                             <local:MyComboBoxItem Content="开启" />
                             <local:MyComboBoxItem Content="开启" />
                             <local:MyComboBoxItem Content="关闭" />
                             <local:MyComboBoxItem Content="关闭" />
@@ -141,7 +141,7 @@
                             <RowDefinition Height="3" />
                             <RowDefinition Height="3" />
                         </Grid.RowDefinitions>
                         </Grid.RowDefinitions>
                         <TextBlock Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Left" Text="登录方式" Margin="0,0,25,0" />
                         <TextBlock Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Left" Text="登录方式" Margin="0,0,25,0" />
-                        <local:MyComboBox x:Name="ComboServerLogin" Grid.Row="0" Grid.ColumnSpan="2" Tag="VersionServerLogin" Grid.Column="1">
+                        <local:MyComboBox x:Name="ComboServerLogin" Grid.Row="0" Grid.ColumnSpan="2" Grid.Column="1">
                             <local:MyComboBoxItem Content="正版登录或离线登录" IsSelected="True" />
                             <local:MyComboBoxItem Content="正版登录或离线登录" IsSelected="True" />
                             <local:MyComboBoxItem Content="仅正版登录" />
                             <local:MyComboBoxItem Content="仅正版登录" />
                             <local:MyComboBoxItem Content="仅离线登录" />
                             <local:MyComboBoxItem Content="仅离线登录" />
@@ -149,27 +149,27 @@
                             <local:MyComboBoxItem Content="第三方登录:Authlib Injector 或 LittleSkin" />
                             <local:MyComboBoxItem Content="第三方登录:Authlib Injector 或 LittleSkin" />
                         </local:MyComboBox>
                         </local:MyComboBox>
                         <TextBlock Name="LabServerNide" Grid.Row="1" VerticalAlignment="Center" HorizontalAlignment="Left" Text="服务器 ID(必填)" Margin="0,8,25,0" Visibility="Collapsed" />
                         <TextBlock Name="LabServerNide" Grid.Row="1" VerticalAlignment="Center" HorizontalAlignment="Left" Text="服务器 ID(必填)" Margin="0,8,25,0" Visibility="Collapsed" />
-                        <local:MyTextBox x:Name="TextServerNide" Grid.Row="1" Grid.ColumnSpan="2" ShowValidateResult="False" Tag="VersionServerNide" Grid.Column="1" Margin="0,8,0,0" Visibility="Collapsed" HintText="服主后台 → 集成说明 中提供的服务器 ID">
+                        <local:MyTextBox x:Name="TextServerNide" Grid.Row="1" Grid.ColumnSpan="2" ShowValidateResult="False" local:SettingService.Key="VersionServerNide" Grid.Column="1" Margin="0,8,0,0" Visibility="Collapsed" HintText="服主后台 → 集成说明 中提供的服务器 ID">
                             <local:MyTextBox.ValidateRules>
                             <local:MyTextBox.ValidateRules>
                                 <local:ValidateLength Min="32" Max="32" />
                                 <local:ValidateLength Min="32" Max="32" />
                             </local:MyTextBox.ValidateRules>
                             </local:MyTextBox.ValidateRules>
                         </local:MyTextBox>
                         </local:MyTextBox>
                         <TextBlock Name="LabServerAuthServer" Grid.Row="1" VerticalAlignment="Center" HorizontalAlignment="Left" Text="认证服务器(必填)" Margin="0,8,25,0" Visibility="Collapsed" />
                         <TextBlock Name="LabServerAuthServer" Grid.Row="1" VerticalAlignment="Center" HorizontalAlignment="Left" Text="认证服务器(必填)" Margin="0,8,25,0" Visibility="Collapsed" />
-                        <local:MyTextBox x:Name="TextServerAuthServer" Grid.Row="1" Grid.ColumnSpan="2" ShowValidateResult="False" Tag="VersionServerAuthServer" Grid.Column="1" Margin="0,8,0,0" Visibility="Collapsed" HintText="Yggdrasil API 认证服务器的完整地址">
+                        <local:MyTextBox x:Name="TextServerAuthServer" Grid.Row="1" Grid.ColumnSpan="2" ShowValidateResult="False" local:SettingService.Key="VersionServerAuthServer" Grid.Column="1" Margin="0,8,0,0" Visibility="Collapsed" HintText="Yggdrasil API 认证服务器的完整地址">
                             <local:MyTextBox.ValidateRules>
                             <local:MyTextBox.ValidateRules>
                                 <local:ValidateHttp />
                                 <local:ValidateHttp />
                             </local:MyTextBox.ValidateRules>
                             </local:MyTextBox.ValidateRules>
                         </local:MyTextBox>
                         </local:MyTextBox>
                         <TextBlock Name="LabServerAuthRegister" Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Left" Text="注册链接" Margin="0,8,25,0" Visibility="Collapsed" />
                         <TextBlock Name="LabServerAuthRegister" Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Left" Text="注册链接" Margin="0,8,25,0" Visibility="Collapsed" />
-                        <local:MyTextBox x:Name="TextServerAuthRegister" Grid.Row="2" Grid.ColumnSpan="2" ShowValidateResult="False" Tag="VersionServerAuthRegister" Grid.Column="1" Margin="0,8,0,0" Visibility="Collapsed" HintText="注册账号的网页地址,如 https://xxxxx.xxx/auth/register">
+                        <local:MyTextBox x:Name="TextServerAuthRegister" Grid.Row="2" Grid.ColumnSpan="2" ShowValidateResult="False" local:SettingService.Key="VersionServerAuthRegister" Grid.Column="1" Margin="0,8,0,0" Visibility="Collapsed" HintText="注册账号的网页地址,如 https://xxxxx.xxx/auth/register">
                             <local:MyTextBox.ValidateRules>
                             <local:MyTextBox.ValidateRules>
                                 <local:ValidateHttp />
                                 <local:ValidateHttp />
                             </local:MyTextBox.ValidateRules>
                             </local:MyTextBox.ValidateRules>
                         </local:MyTextBox>
                         </local:MyTextBox>
                         <TextBlock Name="LabServerAuthName" Grid.Row="3" VerticalAlignment="Center" HorizontalAlignment="Left" Text="服务器名称" Margin="0,8,25,0" Visibility="Collapsed" />
                         <TextBlock Name="LabServerAuthName" Grid.Row="3" VerticalAlignment="Center" HorizontalAlignment="Left" Text="服务器名称" Margin="0,8,25,0" Visibility="Collapsed" />
-                        <local:MyTextBox x:Name="TextServerAuthName" Grid.Row="3" Grid.ColumnSpan="2" ShowValidateResult="False" Tag="VersionServerAuthName" Grid.Column="1" Margin="0,8,0,0" Visibility="Collapsed" HintText="显示在登录页面的服务器名称" />
+                        <local:MyTextBox x:Name="TextServerAuthName" Grid.Row="3" Grid.ColumnSpan="2" ShowValidateResult="False" local:SettingService.Key="VersionServerAuthName" Grid.Column="1" Margin="0,8,0,0" Visibility="Collapsed" HintText="显示在登录页面的服务器名称" />
                         <TextBlock VerticalAlignment="Center" Grid.Row="5" HorizontalAlignment="Left" Text="自动进入服务器" Margin="0,0,25,0" />
                         <TextBlock VerticalAlignment="Center" Grid.Row="5" HorizontalAlignment="Left" Text="自动进入服务器" Margin="0,0,25,0" />
-                        <local:MyTextBox x:Name="TextServerEnter" Grid.ColumnSpan="2" Grid.Row="5" Tag="VersionServerEnter" Grid.Column="1" ToolTip="在打开 Minecraft 后自动进入某服务器。&#xa;用英文冒号间隔 IP 与端口,例如 233.233.233.233:12345。">
+                        <local:MyTextBox x:Name="TextServerEnter" Grid.ColumnSpan="2" Grid.Row="5" local:SettingService.Key="VersionServerEnter" Grid.Column="1" ToolTip="在打开 Minecraft 后自动进入某服务器。&#xa;用英文冒号间隔 IP 与端口,例如 233.233.233.233:12345。">
                             <local:MyTextBox.ValidateRules>
                             <local:MyTextBox.ValidateRules>
                                 <local:ValidateExcept>
                                 <local:ValidateExcept>
                                     <local:ValidateExcept.Excepts>
                                     <local:ValidateExcept.Excepts>
@@ -187,7 +187,8 @@
                         <Grid.ColumnDefinitions>
                         <Grid.ColumnDefinitions>
                             <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
                             <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
                         </Grid.ColumnDefinitions>
                         </Grid.ColumnDefinitions>
-                        <local:MyButton x:Name="BtnServerNideWeb" Grid.Column="0" MinWidth="140" Text="服主后台" Padding="13,0" Margin="0,0,20,0" HorizontalAlignment="Left" ColorType="Highlight" />
+                        <local:MyButton Grid.Column="0" MinWidth="140" Text="服主后台" Padding="13,0" Margin="0,0,20,0" HorizontalAlignment="Left" ColorType="Highlight"
+                                        local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://login.mc-user.com:233/server/intro" />
                     </Grid>
                     </Grid>
                     <local:MyButton x:Name="BtnServerAuthLittle" Height="35" MinWidth="150" Margin="0,12,0,0" Text="设置为 LittleSkin" HorizontalAlignment="Left" ColorType="Highlight" />
                     <local:MyButton x:Name="BtnServerAuthLittle" Height="35" MinWidth="150" Margin="0,12,0,0" Text="设置为 LittleSkin" HorizontalAlignment="Left" ColorType="Highlight" />
                 </StackPanel>
                 </StackPanel>
@@ -211,10 +212,10 @@
                         <RowDefinition Height="Auto" />
                         <RowDefinition Height="Auto" />
                     </Grid.RowDefinitions>
                     </Grid.RowDefinitions>
                     <TextBlock Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Left" Text="Java 虚拟机参数" Margin="0,0,25,0" />
                     <TextBlock Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Left" Text="Java 虚拟机参数" Margin="0,0,25,0" />
-                    <local:MyTextBox x:Name="TextAdvanceJvm" Grid.Row="0" Grid.ColumnSpan="2" Tag="VersionAdvanceJvm" Grid.Column="1" MaxLength="4000" ToolTip="启动 Minecraft 时使用的额外 JVM 参数,在没有确定把握的情况下请不要尝试修改。&#xa;&#xa;支持 Minecraft 版本 JSON 中的字符串替换标记,例如 ${library_directory}。&#xa;也支持使用 PCL 的替换标记,在 更多 → 帮助 中搜索 “替换标记” 以了解用法。&#xa;&#xa;若留空,则跟随全局设置的值。" HintText="跟随全局设置"
+                    <local:MyTextBox x:Name="TextAdvanceJvm" Grid.Row="0" Grid.ColumnSpan="2" local:SettingService.Key="VersionAdvanceJvm" Grid.Column="1" MaxLength="4000" ToolTip="启动 Minecraft 时使用的额外 JVM 参数,在没有确定把握的情况下请不要尝试修改。&#xa;&#xa;支持 Minecraft 版本 JSON 中的字符串替换标记,例如 ${library_directory}。&#xa;也支持使用 PCL 的替换标记,在 更多 → 帮助 中搜索 “替换标记” 以了解用法。&#xa;&#xa;若留空,则跟随全局设置的值。" HintText="跟随全局设置"
                                      Height="100" AcceptsReturn="True" TextWrapping="Wrap" VerticalContentAlignment="Top" Padding="0,5" VerticalScrollBarVisibility="Auto" />
                                      Height="100" AcceptsReturn="True" TextWrapping="Wrap" VerticalContentAlignment="Top" Padding="0,5" VerticalScrollBarVisibility="Auto" />
                     <TextBlock Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Left" Text="游戏参数" Margin="0,0,25,0" />
                     <TextBlock Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Left" Text="游戏参数" Margin="0,0,25,0" />
-                    <local:MyTextBox x:Name="TextAdvanceGame" Grid.Row="2" Grid.ColumnSpan="2" Tag="VersionAdvanceGame" Grid.Column="1" ToolTip="文本框中的内容将会被直接拼合在启动参数的末尾。&#xa;例如,输入 --demo 则会以试玩模式启动游戏。&#xa;&#xa;支持 Minecraft 版本 JSON 中的字符串替换标记,例如 ${library_directory}。&#xa;也支持使用 PCL 的替换标记,在 更多 → 帮助 中搜索 “替换标记” 以了解用法。&#xa;&#xa;若留空,则跟随全局设置的值。" HintText="跟随全局设置" />
+                    <local:MyTextBox x:Name="TextAdvanceGame" Grid.Row="2" Grid.ColumnSpan="2" local:SettingService.Key="VersionAdvanceGame" Grid.Column="1" ToolTip="文本框中的内容将会被直接拼合在启动参数的末尾。&#xa;例如,输入 --demo 则会以试玩模式启动游戏。&#xa;&#xa;支持 Minecraft 版本 JSON 中的字符串替换标记,例如 ${library_directory}。&#xa;也支持使用 PCL 的替换标记,在 更多 → 帮助 中搜索 “替换标记” 以了解用法。&#xa;&#xa;若留空,则跟随全局设置的值。" HintText="跟随全局设置" />
                     <TextBlock VerticalAlignment="Center" Grid.Row="4" HorizontalAlignment="Left" Text="内存管理" Margin="0,0,25,0" />
                     <TextBlock VerticalAlignment="Center" Grid.Row="4" HorizontalAlignment="Left" Text="内存管理" Margin="0,0,25,0" />
                     <local:MyComboBox local:SettingService.Key="VersionAdvanceGC" Grid.Row="4" Grid.ColumnSpan="2" Grid.Column="1">
                     <local:MyComboBox local:SettingService.Key="VersionAdvanceGC" Grid.Row="4" Grid.ColumnSpan="2" Grid.Column="1">
                         <local:MyComboBoxItem Content="跟随全局设置" />
                         <local:MyComboBoxItem Content="跟随全局设置" />
@@ -230,21 +231,21 @@
                           ToolTipService.BetweenShowDelay="0" ToolTip="不自动添加 GC 参数。&#xa;仅当选择此项时,你可以在 “Java 虚拟机参数” 设置中手动添加 GC 参数。" />
                           ToolTipService.BetweenShowDelay="0" ToolTip="不自动添加 GC 参数。&#xa;仅当选择此项时,你可以在 “Java 虚拟机参数” 设置中手动添加 GC 参数。" />
                     </local:MyComboBox>
                     </local:MyComboBox>
                     <TextBlock Grid.Row="6" VerticalAlignment="Center" HorizontalAlignment="Left" Text="启动前执行命令" Margin="0,0,25,3" />
                     <TextBlock Grid.Row="6" VerticalAlignment="Center" HorizontalAlignment="Left" Text="启动前执行命令" Margin="0,0,25,3" />
-                    <local:MyTextBox x:Name="TextAdvanceRun" Grid.Row="6" Grid.ColumnSpan="2" Tag="VersionAdvanceRun" Grid.Column="1" HintText="" Margin="0,0,0,3"
+                    <local:MyTextBox x:Name="TextAdvanceRun" Grid.Row="6" Grid.ColumnSpan="2" local:SettingService.Key="VersionAdvanceRun" Grid.Column="1" HintText="" Margin="0,0,0,3"
                                      ToolTip="该项不会覆盖全局设置:启动时会先执行全局设置的命令,再执行版本设置的命令。&#xa;&#xa;在 MC 启动前执行特定命令或程序,语法与 Windows 的命令提示符一致。&#xa;&#xa;你可以用替换标记来动态改变其内容或实现相对路径,在 更多 → 帮助 中搜索 “替换标记” 以了解用法。例如:&#xa; · &quot;{verpath}test.exe&quot; :运行版本文件夹下的 test.exe 程序&#xa; · &quot;{java}java.exe&quot; -jar &quot;{verpath}test.jar&quot; :用 Java 运行版本文件夹下的 test.jar&#xa; · notepad &quot;{verindie}options.txt&quot; :使用记事本打开该版本的设置文件&#xa;&#xa;涉及路径的操作最好都打上双引号,以避免路径中的空格导致运行失败。&#xa;执行命令时,命令行所在的目录是当前的 .minecraft 文件夹。" />
                                      ToolTip="该项不会覆盖全局设置:启动时会先执行全局设置的命令,再执行版本设置的命令。&#xa;&#xa;在 MC 启动前执行特定命令或程序,语法与 Windows 的命令提示符一致。&#xa;&#xa;你可以用替换标记来动态改变其内容或实现相对路径,在 更多 → 帮助 中搜索 “替换标记” 以了解用法。例如:&#xa; · &quot;{verpath}test.exe&quot; :运行版本文件夹下的 test.exe 程序&#xa; · &quot;{java}java.exe&quot; -jar &quot;{verpath}test.jar&quot; :用 Java 运行版本文件夹下的 test.jar&#xa; · notepad &quot;{verindie}options.txt&quot; :使用记事本打开该版本的设置文件&#xa;&#xa;涉及路径的操作最好都打上双引号,以避免路径中的空格导致运行失败。&#xa;执行命令时,命令行所在的目录是当前的 .minecraft 文件夹。" />
-                    <local:MyCheckBox Grid.Row="7" Grid.Column="1" x:Name="CheckAdvanceRunWait" Tag="VersionAdvanceRunWait" Visibility="Collapsed"
+                    <local:MyCheckBox Grid.Row="7" Grid.Column="1" x:Name="CheckAdvanceRunWait" local:SettingService.Key="VersionAdvanceRunWait" Visibility="Collapsed"
                                       Text="等待命令执行完成后再继续启动" Margin="0,5,0,0" />
                                       Text="等待命令执行完成后再继续启动" Margin="0,5,0,0" />
                     <StackPanel Margin="0,12,0,4" Grid.Row="8" Grid.ColumnSpan="2" HorizontalAlignment="Left">
                     <StackPanel Margin="0,12,0,4" Grid.Row="8" Grid.ColumnSpan="2" HorizontalAlignment="Left">
-                        <local:MyCheckBox Height="28" Text="禁止更新 Mod" x:Name="CheckAdvanceDisableModUpdate" Tag="VersionAdvanceDisableModUpdate"
+                        <local:MyCheckBox Height="28" Text="禁止更新 Mod" x:Name="CheckAdvanceDisableModUpdate" local:SettingService.Key="VersionAdvanceDisableModUpdate"
                                           ToolTip="禁止为此版本更新 Mod,以防止整合包玩家误操作"
                                           ToolTip="禁止为此版本更新 Mod,以防止整合包玩家误操作"
                                           ToolTipService.Placement="Right" />
                                           ToolTipService.Placement="Right" />
-                        <local:MyCheckBox Height="28" Text="忽略 Java 兼容性警告" x:Name="CheckAdvanceJava" Tag="VersionAdvanceJava"
+                        <local:MyCheckBox Height="28" Text="忽略 Java 兼容性警告" x:Name="CheckAdvanceJava" local:SettingService.Key="VersionAdvanceJava"
                                           ToolTipService.Placement="Right"
                                           ToolTipService.Placement="Right"
                                           ToolTip="如果手动选择了与当前版本不兼容的 Java,则自动跳过兼容性警告弹窗,强制使用手动选择的 Java。" />
                                           ToolTip="如果手动选择了与当前版本不兼容的 Java,则自动跳过兼容性警告弹窗,强制使用手动选择的 Java。" />
-                        <local:MyCheckBox Height="28" Text="关闭文件校验" x:Name="CheckAdvanceAssetsV2" Tag="VersionAdvanceAssetsV2"
+                        <local:MyCheckBox Height="28" Text="关闭文件校验" x:Name="CheckAdvanceAssetsV2" local:SettingService.Key="VersionAdvanceAssetsV2"
                                           ToolTipService.Placement="Right"
                                           ToolTipService.Placement="Right"
                                           ToolTip="完全不更改 assets;不校验 libraries、第三方登录库与版本主 jar 文件是否被修改。&#xa;如果你没有修改相关文件,请勿勾选此项。" />
                                           ToolTip="完全不更改 assets;不校验 libraries、第三方登录库与版本主 jar 文件是否被修改。&#xa;如果你没有修改相关文件,请勿勾选此项。" />
-                        <local:MyCheckBox Height="28" Text="禁用 Java Launch Wrapper" x:Name="CheckAdvanceDisableJLW" Tag="VersionAdvanceDisableJLW"
+                        <local:MyCheckBox Height="28" Text="禁用 Java Launch Wrapper" x:Name="CheckAdvanceDisableJLW" local:SettingService.Key="VersionAdvanceDisableJLW"
                                           ToolTipService.Placement="Right"
                                           ToolTipService.Placement="Right"
                                           ToolTip="是否使用 Java Launch Wrapper 修复 Java 18- 在中文路径下可能无法正常启动的问题。&#xa;详见:https://github.com/00ll00/java_launch_wrapper" />
                                           ToolTip="是否使用 Java Launch Wrapper 修复 Java 18- 在中文路径下可能无法正常启动的问题。&#xa;详见:https://github.com/00ll00/java_launch_wrapper" />
                         <local:MyCheckBox Height="28" Text="禁用 LWJGL Unsafe Agent" local:SettingService.Key="VersionAdvanceDisableLUA"
                         <local:MyCheckBox Height="28" Text="禁用 LWJGL Unsafe Agent" local:SettingService.Key="VersionAdvanceDisableLUA"

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

@@ -26,51 +26,30 @@
     Public Sub Reload()
     Public Sub Reload()
         Try
         Try
 
 
-            '清除缓存
-            Settings.MakeInstanceDirty()
-            
             '启动参数
             '启动参数
-            TextArgumentTitle.Text = Settings.Get("VersionArgumentTitle", Instance:=PageInstanceLeft.Instance)
-            TextArgumentInfo.Text = Settings.Get("VersionArgumentInfo", Instance:=PageInstanceLeft.Instance)
             Dim _unused = PageInstanceLeft.Instance.PathIndie '触发自动判定
             Dim _unused = PageInstanceLeft.Instance.PathIndie '触发自动判定
             ComboArgumentIndieV2.SelectedIndex = If(Settings.Get("VersionArgumentIndieV2", Instance:=PageInstanceLeft.Instance), 0, 1)
             ComboArgumentIndieV2.SelectedIndex = If(Settings.Get("VersionArgumentIndieV2", Instance:=PageInstanceLeft.Instance), 0, 1)
             RefreshJavaComboBox()
             RefreshJavaComboBox()
 
 
-            '游戏内存
-            CType(FindName("RadioRamType" & Settings.Get("VersionRamType", Instance:=PageInstanceLeft.Instance)), MyRadioBox).Checked = True
-            OnVersionRamTypeChanged(Settings.Get("VersionRamType", Instance:=PageInstanceLeft.Instance))
-            SliderRamCustom.Value = Settings.Get("VersionRamCustom", Instance:=PageInstanceLeft.Instance)
-            ComboRamOptimize.SelectedIndex = Settings.Get("VersionRamOptimize", Instance:=PageInstanceLeft.Instance)
-
             '服务器
             '服务器
-            TextServerEnter.Text = Settings.Get("VersionServerEnter", Instance:=PageInstanceLeft.Instance)
             ComboServerLogin.SelectedIndex = Settings.Get("VersionServerLogin", Instance:=PageInstanceLeft.Instance)
             ComboServerLogin.SelectedIndex = Settings.Get("VersionServerLogin", Instance:=PageInstanceLeft.Instance)
             ComboServerLoginLast = ComboServerLogin.SelectedIndex
             ComboServerLoginLast = ComboServerLogin.SelectedIndex
-            ServerLogin(ComboServerLogin.SelectedIndex)
-            TextServerNide.Text = Settings.Get("VersionServerNide", Instance:=PageInstanceLeft.Instance)
-            TextServerAuthServer.Text = Settings.Get("VersionServerAuthServer", Instance:=PageInstanceLeft.Instance)
-            TextServerAuthName.Text = Settings.Get("VersionServerAuthName", Instance:=PageInstanceLeft.Instance)
-            TextServerAuthRegister.Text = Settings.Get("VersionServerAuthRegister", Instance:=PageInstanceLeft.Instance)
+            UpdateServerLoginUI()
 
 
             '高级设置
             '高级设置
-            TextAdvanceJvm.Text = Settings.Get("VersionAdvanceJvm", Instance:=PageInstanceLeft.Instance)
-            TextAdvanceGame.Text = Settings.Get("VersionAdvanceGame", Instance:=PageInstanceLeft.Instance)
-            TextAdvanceRun.Text = Settings.Get("VersionAdvanceRun", Instance:=PageInstanceLeft.Instance)
-            CheckAdvanceRunWait.Checked = Settings.Get("VersionAdvanceRunWait", Instance:=PageInstanceLeft.Instance)
             If Settings.Get("VersionAdvanceAssets", Instance:=PageInstanceLeft.Instance) = 2 Then
             If Settings.Get("VersionAdvanceAssets", Instance:=PageInstanceLeft.Instance) = 2 Then
                 Log("[Setup] 已迁移老版本的关闭文件校验设置")
                 Log("[Setup] 已迁移老版本的关闭文件校验设置")
                 Settings.Reset("VersionAdvanceAssets", Instance:=PageInstanceLeft.Instance)
                 Settings.Reset("VersionAdvanceAssets", Instance:=PageInstanceLeft.Instance)
                 Settings.Set("VersionAdvanceAssetsV2", True, Instance:=PageInstanceLeft.Instance)
                 Settings.Set("VersionAdvanceAssetsV2", True, Instance:=PageInstanceLeft.Instance)
             End If
             End If
-            CheckAdvanceAssetsV2.Checked = Settings.Get("VersionAdvanceAssetsV2", Instance:=PageInstanceLeft.Instance)
-            CheckAdvanceJava.Checked = Settings.Get("VersionAdvanceJava", Instance:=PageInstanceLeft.Instance)
-            CheckAdvanceDisableJLW.Checked = Settings.Get("VersionAdvanceDisableJLW", Instance:=PageInstanceLeft.Instance)
-            CheckAdvanceDisableModUpdate.Checked = Settings.Get("VersionAdvanceDisableModUpdate", Instance:=PageInstanceLeft.Instance)
 
 
             SettingService.RefreshSettings(Me)
             SettingService.RefreshSettings(Me)
 
 
+            '游戏内存
+            OnVersionRamTypeChanged(Settings.Get("VersionRamType", Instance:=PageInstanceLeft.Instance))
+
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "重载版本独立设置时出错", LogLevel.Feedback)
+            Log(ex, "重载版本独立设置时出错", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -78,66 +57,23 @@
     Public Sub Reset()
     Public Sub Reset()
         Try
         Try
 
 
-            Settings.Reset("VersionServerEnter", Instance:=PageInstanceLeft.Instance)
+            SettingService.ResetSettings(Me)
+
             Settings.Reset("VersionServerLogin", Instance:=PageInstanceLeft.Instance)
             Settings.Reset("VersionServerLogin", Instance:=PageInstanceLeft.Instance)
-            Settings.Reset("VersionServerNide", Instance:=PageInstanceLeft.Instance)
-            Settings.Reset("VersionServerAuthServer", Instance:=PageInstanceLeft.Instance)
-            Settings.Reset("VersionServerAuthRegister", Instance:=PageInstanceLeft.Instance)
-            Settings.Reset("VersionServerAuthName", Instance:=PageInstanceLeft.Instance)
-            Settings.Reset("VersionArgumentTitle", Instance:=PageInstanceLeft.Instance)
-            Settings.Reset("VersionArgumentInfo", Instance:=PageInstanceLeft.Instance)
             Settings.Reset("VersionArgumentIndieV2", Instance:=PageInstanceLeft.Instance)
             Settings.Reset("VersionArgumentIndieV2", Instance:=PageInstanceLeft.Instance)
-            Settings.Reset("VersionRamType", Instance:=PageInstanceLeft.Instance)
-            Settings.Reset("VersionRamCustom", Instance:=PageInstanceLeft.Instance)
-            Settings.Reset("VersionRamOptimize", Instance:=PageInstanceLeft.Instance)
-            Settings.Reset("VersionAdvanceJvm", Instance:=PageInstanceLeft.Instance)
-            Settings.Reset("VersionAdvanceGame", Instance:=PageInstanceLeft.Instance)
             Settings.Reset("VersionAdvanceAssets", Instance:=PageInstanceLeft.Instance)
             Settings.Reset("VersionAdvanceAssets", Instance:=PageInstanceLeft.Instance)
-            Settings.Reset("VersionAdvanceAssetsV2", Instance:=PageInstanceLeft.Instance)
-            Settings.Reset("VersionAdvanceJava", Instance:=PageInstanceLeft.Instance)
-            Settings.Reset("VersionAdvanceRun", Instance:=PageInstanceLeft.Instance)
-            Settings.Reset("VersionAdvanceRunWait", Instance:=PageInstanceLeft.Instance)
-            Settings.Reset("VersionAdvanceDisableJLW", Instance:=PageInstanceLeft.Instance)
-            Settings.Reset("VersionAdvanceDisableModUpdate", Instance:=PageInstanceLeft.Instance)
             Settings.Reset("VersionArgumentJavaSelect", Instance:=PageInstanceLeft.Instance)
             Settings.Reset("VersionArgumentJavaSelect", Instance:=PageInstanceLeft.Instance)
             JavaSearchLoader.Start(IsForceRestart:=True)
             JavaSearchLoader.Start(IsForceRestart:=True)
 
 
-            SettingService.ResetSettings(Me)
-
             Log("[Setup] 已初始化版本独立设置")
             Log("[Setup] 已初始化版本独立设置")
             Hint("已初始化版本独立设置!", HintType.Green, False)
             Hint("已初始化版本独立设置!", HintType.Green, False)
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "初始化版本独立设置失败", LogLevel.Msgbox)
+            Log(ex, "初始化版本独立设置失败", NotifyLevel.MsgBox)
         End Try
         End Try
 
 
         Reload()
         Reload()
     End Sub
     End Sub
 
 
-    '将控件改变路由到设置改变
-    Private Shared Sub RadioBoxChange(sender As MyRadioBox, e As Object) Handles RadioRamType0.Check, RadioRamType1.Check, RadioRamType2.Check
-        If AniControlEnabled = 0 Then Settings.Set(sender.Tag.ToString.Split("/")(0), Val(sender.Tag.ToString.Split("/")(1)), Instance:=PageInstanceLeft.Instance)
-    End Sub
-    Private Shared Sub TextBoxChange(sender As MyTextBox, e As Object) Handles TextServerEnter.ValidatedTextChanged, TextArgumentInfo.ValidatedTextChanged, TextAdvanceGame.ValidatedTextChanged, TextAdvanceJvm.ValidatedTextChanged, TextServerNide.ValidatedTextChanged, TextServerAuthName.ValidatedTextChanged, TextServerAuthRegister.ValidatedTextChanged, TextServerAuthServer.ValidatedTextChanged, TextArgumentTitle.ValidatedTextChanged, TextAdvanceRun.ValidatedTextChanged
-        If AniControlEnabled = 0 Then
-            '#3194,不能删减 /
-            'Dim HandledText As String = sender.Text
-            'If sender.Tag = "VersionServerAuthServer" OrElse sender.Tag = "VersionServerAuthRegister" Then HandledText = HandledText.TrimEnd("/")
-            Settings.Set(sender.Tag, sender.Text, Instance:=PageInstanceLeft.Instance)
-        End If
-    End Sub
-    Private Shared Sub SliderChange(sender As MySlider, e As Object) Handles SliderRamCustom.Change
-        If AniControlEnabled = 0 Then Settings.Set(sender.Tag, sender.Value, Instance:=PageInstanceLeft.Instance)
-    End Sub
-    Private Shared Sub ComboChange(sender As MyComboBox, e As Object) Handles ComboRamOptimize.SelectionChanged
-        If AniControlEnabled = 0 Then Settings.Set(sender.Tag, sender.SelectedIndex, Instance:=PageInstanceLeft.Instance)
-    End Sub
-    Private Shared Sub CheckBoxLikeComboChange(sender As MyComboBox, e As Object) Handles ComboArgumentIndieV2.SelectionChanged
-        If AniControlEnabled = 0 Then Settings.Set(sender.Tag, sender.SelectedIndex = 0, Instance:=PageInstanceLeft.Instance)
-    End Sub
-    Private Shared Sub CheckBoxChange(sender As MyCheckBox, e As Object) Handles CheckAdvanceRunWait.Change, CheckAdvanceAssetsV2.Change, CheckAdvanceJava.Change, CheckAdvanceDisableJLW.Change, CheckAdvanceDisableModUpdate.Change
-        If AniControlEnabled = 0 Then Settings.Set(sender.Tag, sender.Checked, Instance:=PageInstanceLeft.Instance)
-    End Sub
-
 #Region "游戏内存"
 #Region "游戏内存"
 
 
     Public Shared Sub OnVersionRamTypeChanged(Type As Integer)
     Public Shared Sub OnVersionRamTypeChanged(Type As Integer)
@@ -161,7 +97,7 @@
         Dim RamAvailable As Double = Math.Round(My.Computer.Info.AvailablePhysicalMemory / 1024 / 1024 / 1024, 1)
         Dim RamAvailable As Double = Math.Round(My.Computer.Info.AvailablePhysicalMemory / 1024 / 1024 / 1024, 1)
         Dim RamGameActual As Double = Math.Round(Math.Min(RamGame, RamAvailable), 5)
         Dim RamGameActual As Double = Math.Round(Math.Min(RamGame, RamAvailable), 5)
         Dim RamUsed As Double = Math.Round(RamTotal - RamAvailable, 5)
         Dim RamUsed As Double = Math.Round(RamTotal - RamAvailable, 5)
-        Dim RamEmpty As Double = Math.Round(MathClamp(RamTotal - RamUsed - RamGame, 0, 1000), 1)
+        Dim RamEmpty As Double = Math.Round((RamTotal - RamUsed - RamGame).Clamp(0, 1000), 1)
         '设置最大可用内存
         '设置最大可用内存
         If RamTotal <= 1.5 Then
         If RamTotal <= 1.5 Then
             SliderRamCustom.MaxValue = Math.Max(Math.Floor((RamTotal - 0.3) / 0.1), 1)
             SliderRamCustom.MaxValue = Math.Max(Math.Floor((RamTotal - 0.3) / 0.1), 1)
@@ -392,7 +328,7 @@ PreFin:
     Private ComboServerLoginLast As Integer
     Private ComboServerLoginLast As Integer
     Private Sub ComboServerLogin_Changed() Handles ComboServerLogin.SelectionChanged, TextServerNide.ValidatedTextChanged, TextServerAuthServer.ValidatedTextChanged, TextServerAuthRegister.ValidatedTextChanged
     Private Sub ComboServerLogin_Changed() Handles ComboServerLogin.SelectionChanged, TextServerNide.ValidatedTextChanged, TextServerAuthServer.ValidatedTextChanged, TextServerAuthRegister.ValidatedTextChanged
         If AniControlEnabled <> 0 Then Return
         If AniControlEnabled <> 0 Then Return
-        ServerLogin(ComboServerLogin.SelectedIndex)
+        UpdateServerLoginUI()
         '检查是否输入正确,正确才触发设置改变
         '检查是否输入正确,正确才触发设置改变
         If ComboServerLogin.SelectedIndex = 3 AndAlso Not TextServerNide.IsValidated Then Return
         If ComboServerLogin.SelectedIndex = 3 AndAlso Not TextServerNide.IsValidated Then Return
         If ComboServerLogin.SelectedIndex = 4 AndAlso Not TextServerAuthServer.IsValidated Then Return
         If ComboServerLogin.SelectedIndex = 4 AndAlso Not TextServerAuthServer.IsValidated Then Return
@@ -400,10 +336,11 @@ PreFin:
         If ComboServerLoginLast = ComboServerLogin.SelectedIndex Then Return
         If ComboServerLoginLast = ComboServerLogin.SelectedIndex Then Return
         '触发
         '触发
         ComboServerLoginLast = ComboServerLogin.SelectedIndex
         ComboServerLoginLast = ComboServerLogin.SelectedIndex
-        ComboChange(ComboServerLogin, Nothing)
+        Settings.Set("VersionServerLogin", ComboServerLogin.SelectedIndex, Instance:=PageInstanceLeft.Instance)
     End Sub
     End Sub
-    Public Sub ServerLogin(Type As Integer)
+    Private Sub UpdateServerLoginUI()
         If LabServerNide Is Nothing Then Return
         If LabServerNide Is Nothing Then Return
+        Dim Type = ComboServerLogin.SelectedIndex
         LabServerNide.Visibility = If(Type = 3, Visibility.Visible, Visibility.Collapsed)
         LabServerNide.Visibility = If(Type = 3, Visibility.Visible, Visibility.Collapsed)
         TextServerNide.Visibility = If(Type = 3, Visibility.Visible, Visibility.Collapsed)
         TextServerNide.Visibility = If(Type = 3, Visibility.Visible, Visibility.Collapsed)
         PanServerNide.Visibility = If(Type = 3, Visibility.Visible, Visibility.Collapsed)
         PanServerNide.Visibility = If(Type = 3, Visibility.Visible, Visibility.Collapsed)
@@ -417,11 +354,6 @@ PreFin:
         CardServer.TriggerForceResize()
         CardServer.TriggerForceResize()
     End Sub
     End Sub
 
 
-    '统一通行证
-    Private Sub BtnServerNideWeb_Click(sender As Object, e As EventArgs) Handles BtnServerNideWeb.Click
-        OpenWebsite("https://login.mc-user.com:233/server/intro")
-    End Sub
-
     'LittleSkin
     'LittleSkin
     Private Sub BtnServerAuthLittle_Click(sender As Object, e As EventArgs) Handles BtnServerAuthLittle.Click
     Private Sub BtnServerAuthLittle_Click(sender As Object, e As EventArgs) Handles BtnServerAuthLittle.Click
         If TextServerAuthServer.Text <> "" AndAlso TextServerAuthServer.Text <> "https://littleskin.cn/api/yggdrasil" AndAlso
         If TextServerAuthServer.Text <> "" AndAlso TextServerAuthServer.Text <> "https://littleskin.cn/api/yggdrasil" AndAlso
@@ -447,7 +379,7 @@ PreFin:
         Dim SelectedItem As MyComboBoxItem = Nothing
         Dim SelectedItem As MyComboBoxItem = Nothing
         Dim SelectedBySetup As String = Settings.Get("VersionArgumentJavaSelect", Instance:=PageInstanceLeft.Instance)
         Dim SelectedBySetup As String = Settings.Get("VersionArgumentJavaSelect", Instance:=PageInstanceLeft.Instance)
         Try
         Try
-            For Each Java In JavaList.Clone().OrderByDescending(Function(v) v.MajorVersion)
+            For Each Java In JavaList.ToList().OrderByDescending(Function(v) v.MajorVersion)
                 Dim ListItem = New MyComboBoxItem With {.Content = Java.ToString, .ToolTip = Java.PathFolder, .Tag = Java}
                 Dim ListItem = New MyComboBoxItem With {.Content = Java.ToString, .ToolTip = Java.PathFolder, .Tag = Java}
                 ToolTipService.SetHorizontalOffset(ListItem, 400)
                 ToolTipService.SetHorizontalOffset(ListItem, 400)
                 ComboArgumentJava.Items.Add(ListItem)
                 ComboArgumentJava.Items.Add(ListItem)
@@ -457,7 +389,7 @@ PreFin:
             Next
             Next
         Catch ex As Exception
         Catch ex As Exception
             Settings.Set("VersionArgumentJavaSelect", "使用全局设置", Instance:=PageInstanceLeft.Instance)
             Settings.Set("VersionArgumentJavaSelect", "使用全局设置", Instance:=PageInstanceLeft.Instance)
-            Log(ex, "更新版本设置 Java 下拉框失败", LogLevel.Feedback)
+            Log(ex, "更新版本设置 Java 下拉框失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
         '更新选择项
         '更新选择项
         If SelectedItem Is Nothing AndAlso JavaList.Any Then
         If SelectedItem Is Nothing AndAlso JavaList.Any Then
@@ -507,21 +439,22 @@ PreFin:
 
 
 #End Region
 #End Region
 
 
-#Region "其他设置"
+#Region "版本隔离"
 
 
-    '版本隔离警告
-    Private IsReverting As Boolean = False
     Private Sub ComboArgumentIndieV2_SelectionChanged(sender As Object, e As SelectionChangedEventArgs) Handles ComboArgumentIndieV2.SelectionChanged
     Private Sub ComboArgumentIndieV2_SelectionChanged(sender As Object, e As SelectionChangedEventArgs) Handles ComboArgumentIndieV2.SelectionChanged
         If AniControlEnabled <> 0 Then Return
         If AniControlEnabled <> 0 Then Return
+        Static IsReverting As Boolean = False
         If IsReverting Then Return
         If IsReverting Then Return
-        If MyMsgBox("调整版本隔离后,你可能得把游戏存档、Mod 等文件手动迁移到新的游戏文件夹中。" & vbCrLf &
-                    "如果修改后发现存档消失,把这项设置改回来就能恢复。" & vbCrLf &
+        If MyMsgBox("调整版本隔离设置后,你需要游戏存档、Mod 等文件手动迁移到新的游戏文件夹中。" & vbCrLf &
+                    "如果发现存档消失,把这项设置改回来就能恢复。" & vbCrLf &
                     "如果你不会迁移存档,不建议修改这项设置!",
                     "如果你不会迁移存档,不建议修改这项设置!",
                     "警告", "我知道我在做什么", "取消", IsWarn:=True) = 2 Then
                     "警告", "我知道我在做什么", "取消", IsWarn:=True) = 2 Then
             IsReverting = True
             IsReverting = True
             ComboArgumentIndieV2.SelectedItem = e.RemovedItems(0)
             ComboArgumentIndieV2.SelectedItem = e.RemovedItems(0)
             IsReverting = False
             IsReverting = False
         End If
         End If
+        '实际应用设置
+        Settings.Set("VersionArgumentIndieV2", sender.SelectedIndex = 0, Instance:=PageInstanceLeft.Instance)
     End Sub
     End Sub
 
 
 #End Region
 #End Region
@@ -541,7 +474,7 @@ PreFin:
 
 
     '去除参数中的回车
     '去除参数中的回车
     Private Sub ReplaceEnter(sender As MyTextBox, e As TextChangedEventArgs) Handles TextAdvanceJvm.TextChanged, TextAdvanceGame.TextChanged
     Private Sub ReplaceEnter(sender As MyTextBox, e As TextChangedEventArgs) Handles TextAdvanceJvm.TextChanged, TextAdvanceGame.TextChanged
-        Dim NewText = sender.Text.Replace(vbCrLf, vbCr).Replace(vbLf, vbCr).Replace(vbCr, " ")
+        Dim NewText = sender.Text.ReplaceLineEndings(" ", mergeMultiple:=True)
         If NewText = sender.Text Then Return
         If NewText = sender.Text Then Return
         Dim CaretIndex = sender.CaretIndex
         Dim CaretIndex = sender.CaretIndex
         sender.Text = NewText
         sender.Text = NewText

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

@@ -20,7 +20,7 @@
             Data = Converter.Content
             Data = Converter.Content
             Init()
             Init()
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "登录弹窗初始化失败", LogLevel.Hint)
+            Log(ex, "登录弹窗初始化失败", NotifyLevel.AllUsers)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -37,7 +37,7 @@
             '记录日志
             '记录日志
             Log("[Control] 登录弹窗:" & LabTitle.Text & vbCrLf & LabCaption.Text)
             Log("[Control] 登录弹窗:" & LabTitle.Text & vbCrLf & LabCaption.Text)
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "登录弹窗加载失败", LogLevel.Hint)
+            Log(ex, "登录弹窗加载失败", NotifyLevel.AllUsers)
         End Try
         End Try
     End Sub
     End Sub
     Private Sub Close()
     Private Sub Close()

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

@@ -64,7 +64,7 @@
                 Hint("皮肤保存成功!", HintType.Green)
                 Hint("皮肤保存成功!", HintType.Green)
             End If
             End If
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "保存皮肤失败", LogLevel.Hint)
+            Log(ex, "保存皮肤失败", NotifyLevel.AllUsers)
         End Try
         End Try
     End Sub
     End Sub
     Private Sub BtnSkinSave_Checked(sender As MyMenuItem, e As RoutedEventArgs) Handles BtnSkinSave.Checked
     Private Sub BtnSkinSave_Checked(sender As MyMenuItem, e As RoutedEventArgs) Handles BtnSkinSave.Checked
@@ -85,7 +85,7 @@
             Try
             Try
                 Image = New MyBitmap(Address)
                 Image = New MyBitmap(Address)
             Catch ex As Exception '#2272
             Catch ex As Exception '#2272
-                Log(ex, $"皮肤文件已损坏:{Address}", LogLevel.Hint)
+                Log(ex, $"皮肤文件已损坏:{Address}", NotifyLevel.AllUsers)
                 File.Delete(Address)
                 File.Delete(Address)
                 Return
                 Return
             End Try
             End Try
@@ -115,7 +115,7 @@
             ImgBack.Source = Image.Clip(Scale * 8, Scale * 8, Scale * 8, Scale * 8)
             ImgBack.Source = Image.Clip(Scale * 8, Scale * 8, Scale * 8, Scale * 8)
             Log("[Skin] 载入头像成功:" & Loader.Name)
             Log("[Skin] 载入头像成功:" & Loader.Name)
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "载入头像失败(" & If(Address, "null") & "," & Loader.Name & ")", LogLevel.Hint)
+            Log(ex, "载入头像失败(" & If(Address, "null") & "," & Loader.Name & ")", NotifyLevel.AllUsers)
         End Try
         End Try
     End Sub
     End Sub
     ''' <summary>
     ''' <summary>
@@ -163,7 +163,7 @@
                     Next
                     Next
                     Hint("已刷新头像!", HintType.Green)
                     Hint("已刷新头像!", HintType.Green)
                 Catch ex As Exception
                 Catch ex As Exception
-                    Log(ex, "刷新皮肤缓存失败", LogLevel.Msgbox)
+                    Log(ex, "刷新皮肤缓存失败", NotifyLevel.MsgBox)
                 End Try
                 End Try
             End Sub)
             End Sub)
         End If
         End If
@@ -186,7 +186,7 @@
                 '完成提示
                 '完成提示
                 Hint("更改皮肤成功!", HintType.Green)
                 Hint("更改皮肤成功!", HintType.Green)
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "更改正版皮肤后刷新皮肤失败", LogLevel.Feedback)
+                Log(ex, "更改正版皮肤后刷新皮肤失败", NotifyLevel.MsgBoxAndFeedback)
             End Try
             End Try
         End Sub)
         End Sub)
     End Sub
     End Sub
@@ -255,7 +255,7 @@ Retry:
                         Next
                         Next
                         SelectedIndex = MyMsgBoxSelect(SelectionControl, "选择披风", "确定", "取消")
                         SelectedIndex = MyMsgBoxSelect(SelectionControl, "选择披风", "确定", "取消")
                     Catch ex As Exception
                     Catch ex As Exception
-                        Log(ex, "获取玩家皮肤列表失败", LogLevel.Feedback)
+                        Log(ex, "获取玩家皮肤列表失败", NotifyLevel.MsgBoxAndFeedback)
                     End Try
                     End Try
                 End Sub)
                 End Sub)
                 If SelectedIndex Is Nothing Then Return
                 If SelectedIndex Is Nothing Then Return
@@ -278,7 +278,7 @@ Retry:
                     McLoginMsLoader.Output.ProfileJson = SkinData.ToString()
                     McLoginMsLoader.Output.ProfileJson = SkinData.ToString()
                 End If
                 End If
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "更改披风失败", LogLevel.Hint)
+                Log(ex, "更改披风失败", NotifyLevel.AllUsers)
             Finally
             Finally
                 IsChanging = False
                 IsChanging = False
             End Try
             End Try

+ 18 - 17
Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchLeft.xaml.vb

@@ -23,10 +23,10 @@
         Sub()
         Sub()
             '自动整合包安装:准备
             '自动整合包安装:准备
             Dim PackInstallPath As String = Nothing
             Dim PackInstallPath As String = Nothing
-            If File.Exists(Path & "modpack.zip") Then PackInstallPath = Path & "modpack.zip"
-            If File.Exists(Path & "modpack.mrpack") Then PackInstallPath = Path & "modpack.mrpack"
+            If File.Exists(PathExeFolder & "modpack.zip") Then PackInstallPath = PathExeFolder & "modpack.zip"
+            If File.Exists(PathExeFolder & "modpack.mrpack") Then PackInstallPath = PathExeFolder & "modpack.mrpack"
             If PackInstallPath IsNot Nothing Then
             If PackInstallPath IsNot Nothing Then
-                Log("[Launch] 需自动安装整合包:" & PackInstallPath, LogLevel.Debug)
+                Log("[Launch] 需自动安装整合包:" & PackInstallPath, NotifyLevel.DebugModeOnly)
                 PageSelectLeft.CreateMcFolderInCurrentPath()
                 PageSelectLeft.CreateMcFolderInCurrentPath()
                 McFolderListLoader.WaitForExit()
                 McFolderListLoader.WaitForExit()
             End If
             End If
@@ -36,7 +36,7 @@
                 If McFolderSelected = "" Then
                 If McFolderSelected = "" Then
                     Log("[Launch] 没有已储存的 Minecraft 文件夹")
                     Log("[Launch] 没有已储存的 Minecraft 文件夹")
                 Else
                 Else
-                    Log("[Launch] Minecraft 文件夹无效,该文件夹已不存在:" & McFolderSelected, LogLevel.Debug)
+                    Log("[Launch] Minecraft 文件夹无效,该文件夹已不存在:" & McFolderSelected, NotifyLevel.DebugModeOnly)
                 End If
                 End If
                 McFolderListLoader.WaitForExit(IsForceRestart:=True)
                 McFolderListLoader.WaitForExit(IsForceRestart:=True)
                 McFolderSelected = McFolderList.First.Location
                 McFolderSelected = McFolderList.First.Location
@@ -55,7 +55,7 @@
                 Catch ex As CancelledException
                 Catch ex As CancelledException
                     Log(ex, "自动安装整合包被用户取消:" & PackInstallPath)
                     Log(ex, "自动安装整合包被用户取消:" & PackInstallPath)
                 Catch ex As Exception
                 Catch ex As Exception
-                    Log(ex, "自动安装整合包失败:" & PackInstallPath, LogLevel.Msgbox)
+                    Log(ex, "自动安装整合包失败:" & PackInstallPath, NotifyLevel.MsgBox)
                 End Try
                 End Try
             End If
             End If
             '确认 Minecraft 版本存在
             '确认 Minecraft 版本存在
@@ -63,7 +63,7 @@
             Dim Instance As McInstance = If(Selection = "", Nothing, New McInstance(Selection))
             Dim Instance As McInstance = If(Selection = "", Nothing, New McInstance(Selection))
             If Instance Is Nothing OrElse Not Instance.PathVersion.StartsWithF(McFolderSelected) OrElse Not Instance.Check() Then
             If Instance Is Nothing OrElse Not Instance.PathVersion.StartsWithF(McFolderSelected) OrElse Not Instance.Check() Then
                 '无效的版本
                 '无效的版本
-                Log("[Launch] 当前选择的 Minecraft 版本无效:" & If(Instance Is Nothing, "null", Instance.PathVersion), If(IsNothing(Instance), LogLevel.Normal, LogLevel.Debug))
+                Log("[Launch] 当前选择的 Minecraft 版本无效:" & If(Instance Is Nothing, "null", Instance.PathVersion), If(IsNothing(Instance), NotifyLevel.Silent, NotifyLevel.DebugModeOnly))
                 If Not McInstanceListLoader.State = LoadState.Finished Then LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\", WaitForExit:=True)
                 If Not McInstanceListLoader.State = LoadState.Finished Then LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.ForceRun, MaxDepth:=1, ExtraPath:="versions\", WaitForExit:=True)
                 If Not McInstanceList.Any() OrElse McInstanceList.First.Value(0).Logo.Contains("RedstoneBlock") Then
                 If Not McInstanceList.Any() OrElse McInstanceList.First.Value(0).Logo.Contains("RedstoneBlock") Then
                     Instance = Nothing
                     Instance = Nothing
@@ -80,6 +80,7 @@
                 RefreshButtonsUI()
                 RefreshButtonsUI()
                 RefreshPage(False, False) '有可能选择的版本变化了,需要重新刷新
                 RefreshPage(False, False) '有可能选择的版本变化了,需要重新刷新
                 If McLoginAble() = "" Then McLoginLoader.Start() '自动登录
                 If McLoginAble() = "" Then McLoginLoader.Start() '自动登录
+                If Environment.CommandLine.Contains("--test") Then FormMain.EndProgramForce(123) '用于更新器测试生成的程序是否可以正常运行
             End Sub)
             End Sub)
         End Sub, "Version Check", ThreadPriority.AboveNormal)
         End Sub, "Version Check", ThreadPriority.AboveNormal)
 
 
@@ -256,7 +257,7 @@
             PageCurrent = Type
             PageCurrent = Type
             Return PageNew
             Return PageNew
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "切换登录分页失败(" & GetStringFromEnum(CType(Type, [Enum])) & ")", LogLevel.Feedback)
+            Log(ex, "切换登录分页失败(" & GetStringFromEnum(CType(Type, [Enum])) & ")", NotifyLevel.MsgBoxAndFeedback)
             Return PageNew
             Return PageNew
         End Try
         End Try
     End Function
     End Function
@@ -337,7 +338,7 @@ UnknownType:
                 LabTypeOne.Text = If(McInstanceSelected Is Nothing, Settings.Get("CacheAuthServerName"), Settings.Get("VersionServerAuthName", Instance:=McInstanceSelected))
                 LabTypeOne.Text = If(McInstanceSelected Is Nothing, Settings.Get("CacheAuthServerName"), Settings.Get("VersionServerAuthName", Instance:=McInstanceSelected))
                 If LabTypeOne.Text = "" Then LabTypeOne.Text = "第三方登录"
                 If LabTypeOne.Text = "" Then LabTypeOne.Text = "第三方登录"
             Case Else
             Case Else
-                Log("[Control] 未知的登录页面:" & LoginPageType, LogLevel.Hint)
+                Log("[Control] 未知的登录页面:" & LoginPageType, NotifyLevel.AllUsers)
                 GoTo UnknownType
                 GoTo UnknownType
         End Select
         End Select
         '刷新页面
         '刷新页面
@@ -384,13 +385,13 @@ UnknownType:
         Catch ex As Exception
         Catch ex As Exception
             If ex.GetBrief.Contains("(429)") Then
             If ex.GetBrief.Contains("(429)") Then
                 Data.Output = PathImage & "Skins/" & McSkinSex(McLoginLegacyUuid(UserName)) & ".png"
                 Data.Output = PathImage & "Skins/" & McSkinSex(McLoginLegacyUuid(UserName)) & ".png"
-                Log("[Minecraft] 获取正版皮肤失败(" & UserName & "):获取皮肤太过频繁,请 5 分钟后再试!", LogLevel.Hint)
+                Log("[Minecraft] 获取正版皮肤失败(" & UserName & "):获取皮肤太过频繁,请 5 分钟后再试!", NotifyLevel.AllUsers)
             ElseIf ex.GetBrief.Contains("未设置自定义皮肤") Then
             ElseIf ex.GetBrief.Contains("未设置自定义皮肤") Then
                 Data.Output = PathImage & "Skins/" & McSkinSex(McLoginLegacyUuid(UserName)) & ".png"
                 Data.Output = PathImage & "Skins/" & McSkinSex(McLoginLegacyUuid(UserName)) & ".png"
                 Log("[Minecraft] 用户未设置自定义皮肤,跳过皮肤加载")
                 Log("[Minecraft] 用户未设置自定义皮肤,跳过皮肤加载")
             Else
             Else
                 Data.Output = PathImage & "Skins/" & McSkinSex(McLoginLegacyUuid(UserName)) & ".png"
                 Data.Output = PathImage & "Skins/" & McSkinSex(McLoginLegacyUuid(UserName)) & ".png"
-                Log(ex, "获取微软正版皮肤失败(" & UserName & ")", LogLevel.Hint)
+                Log(ex, "获取微软正版皮肤失败(" & UserName & ")", NotifyLevel.AllUsers)
             End If
             End If
         End Try
         End Try
 Finish:
 Finish:
@@ -506,13 +507,13 @@ UseDefault:
         Catch ex As Exception
         Catch ex As Exception
             If ex.GetBrief.Contains("(429)") Then
             If ex.GetBrief.Contains("(429)") Then
                 Data.Output = PathImage & "Skins/Steve.png"
                 Data.Output = PathImage & "Skins/Steve.png"
-                Log("[Minecraft] 获取统一通行证皮肤失败(" & UserName & "):获取皮肤太过频繁,请 5 分钟后再试!", LogLevel.Hint)
+                Log("[Minecraft] 获取统一通行证皮肤失败(" & UserName & "):获取皮肤太过频繁,请 5 分钟后再试!", NotifyLevel.AllUsers)
             ElseIf ex.GetBrief.Contains("未设置自定义皮肤") Then
             ElseIf ex.GetBrief.Contains("未设置自定义皮肤") Then
                 Data.Output = PathImage & "Skins/Steve.png"
                 Data.Output = PathImage & "Skins/Steve.png"
                 Log("[Minecraft] 用户未设置自定义皮肤,跳过皮肤加载")
                 Log("[Minecraft] 用户未设置自定义皮肤,跳过皮肤加载")
             Else
             Else
                 Data.Output = PathImage & "Skins/Steve.png"
                 Data.Output = PathImage & "Skins/Steve.png"
-                Log(ex, "获取统一通行证皮肤失败(" & UserName & ")", LogLevel.Hint)
+                Log(ex, "获取统一通行证皮肤失败(" & UserName & ")", NotifyLevel.AllUsers)
             End If
             End If
         End Try
         End Try
 Finish:
 Finish:
@@ -554,13 +555,13 @@ Finish:
         Catch ex As Exception
         Catch ex As Exception
             If ex.GetBrief.Contains("(429)") Then
             If ex.GetBrief.Contains("(429)") Then
                 Data.Output = PathImage & "Skins/Steve.png"
                 Data.Output = PathImage & "Skins/Steve.png"
-                Log("[Minecraft] 获取 Authlib-Injector 皮肤失败(" & UserName & "):获取皮肤太过频繁,请 5 分钟后再试!", LogLevel.Hint)
+                Log("[Minecraft] 获取 Authlib-Injector 皮肤失败(" & UserName & "):获取皮肤太过频繁,请 5 分钟后再试!", NotifyLevel.AllUsers)
             ElseIf ex.GetBrief.Contains("未设置自定义皮肤") Then
             ElseIf ex.GetBrief.Contains("未设置自定义皮肤") Then
                 Data.Output = PathImage & "Skins/Steve.png"
                 Data.Output = PathImage & "Skins/Steve.png"
                 Log("[Minecraft] 用户未设置自定义皮肤,跳过皮肤加载")
                 Log("[Minecraft] 用户未设置自定义皮肤,跳过皮肤加载")
             Else
             Else
                 Data.Output = PathImage & "Skins/Steve.png"
                 Data.Output = PathImage & "Skins/Steve.png"
-                Log(ex, "获取 Authlib-Injector 皮肤失败(" & UserName & ")", LogLevel.Hint)
+                Log(ex, "获取 Authlib-Injector 皮肤失败(" & UserName & ")", NotifyLevel.AllUsers)
             End If
             End If
         End Try
         End Try
 Finish:
 Finish:
@@ -678,7 +679,7 @@ ExitRefresh:
                     If Not McLaunchProcess.HasExited Then McLaunchProcess.Kill()
                     If Not McLaunchProcess.HasExited Then McLaunchProcess.Kill()
                 End If
                 End If
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "取消启动结束进程失败", LogLevel.Hint)
+                Log(ex, "取消启动结束进程失败", NotifyLevel.AllUsers)
             End Try
             End Try
         End If
         End If
     End Sub
     End Sub
@@ -735,7 +736,7 @@ ExitRefresh:
                 Log(ex, "获取 Minecraft 启动下载器失败,可能是因为启动被取消")
                 Log(ex, "获取 Minecraft 启动下载器失败,可能是因为启动被取消")
                 HasLaunchDownloader = False
                 HasLaunchDownloader = False
             End Try
             End Try
-            LabLaunchingDownload.Text = GetString(NetManager.Speed) & "/s"
+            LabLaunchingDownload.Text = FormatFileSize(NetManager.Speed) & "/s"
             '进度改变动画
             '进度改变动画
             Dim AnimList As New List(Of AniData) From {
             Dim AnimList As New List(Of AniData) From {
                  AaGridLengthWidth(ProgressLaunchingFinished, ShowProgress - ProgressLaunchingFinished.Width.Value, 130,, New AniEaseOutFluent),
                  AaGridLengthWidth(ProgressLaunchingFinished, ShowProgress - ProgressLaunchingFinished.Width.Value, 130,, New AniEaseOutFluent),
@@ -768,7 +769,7 @@ ExitRefresh:
             End If
             End If
             AniStart(AnimList, "Launching Progress")
             AniStart(AnimList, "Launching Progress")
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "刷新启动信息失败", LogLevel.Feedback)
+            Log(ex, "刷新启动信息失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
     Private ShowProgress As Double = 0
     Private ShowProgress As Double = 0

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

@@ -6,17 +6,10 @@
     xmlns:local="clr-namespace:PCL" PanScroll="PanBack">
     xmlns:local="clr-namespace:PCL" PanScroll="PanBack">
     <local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" x:Name="PanBack">
     <local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" x:Name="PanBack">
         <StackPanel Name="PanMain" Margin="25,25,25,10" Grid.IsSharedSizeScope="True">
         <StackPanel Name="PanMain" Margin="25,25,25,10" Grid.IsSharedSizeScope="True">
+            <local:MyHint x:Name="PanHint" RelativeSetup="HintSnapshot" Theme="Blue" CanClose="True" Margin="0,0,0,15"
+                          Text="快照版 PCL 包含尚未正式发布的测试功能,仅限赞助者自己使用。&#xa;请不要随意发给其他人,或是用来制作整合包哦!" />
             <StackPanel x:Name="PanCustom">
             <StackPanel x:Name="PanCustom">
             </StackPanel>
             </StackPanel>
-            <local:MyCard Margin="0,0,0,15" Title="快照版提示" x:Name="PanHint">
-                <local:MyIconButton LogoScale="1.1" 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="10" HorizontalAlignment="Right" VerticalAlignment="Top"
-                                    x:Name="BtnHintClose" />
-                <StackPanel Margin="25,38,23,15">
-                    <TextBlock x:Name="LabHint1" TextWrapping="Wrap" Margin="0,0,0,2" FontSize="13.5" />
-                    <TextBlock x:Name="LabHint2" TextWrapping="Wrap" Margin="0,0,0,2" FontSize="13.5" />
-                </StackPanel>
-            </local:MyCard>
             <local:MyCard Margin="0,0,0,15" Title="启动日志" x:Name="PanLog">
             <local:MyCard Margin="0,0,0,15" Title="启动日志" x:Name="PanLog">
                 <TextBlock x:Name="LabLog" Margin="20,38,20,18" TextTrimming="None" TextWrapping="Wrap" />
                 <TextBlock x:Name="LabLog" Margin="20,38,20,18" TextTrimming="None" TextWrapping="Wrap" />
             </local:MyCard>
             </local:MyCard>

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

@@ -5,24 +5,13 @@ Public Class PageLaunchRight
 
 
     Private Sub Init() Handles Me.Loaded
     Private Sub Init() Handles Me.Loaded
         PanBack.ScrollToHome()
         PanBack.ScrollToHome()
-        PanLog.Visibility = If(ModeDebug, Visibility.Visible, Visibility.Collapsed)
+        PanLog.Visibility = ModeDebug.ToVisibility
         '快照版提示
         '快照版提示
-#If BETA Then
-        PanHint.Visibility = Visibility.Collapsed
-#Else
-        PanHint.Visibility = If(ThemeCheckGold(), Visibility.Collapsed, Visibility.Visible)
-        LabHint1.Text = "快照版包含尚未正式发布的测试功能,仅用于赞助者本人尝鲜。请不要发给其他人或者用来制作整合包哦!"
-        LabHint2.Text = $"若已累积赞助¥23.33,在爱发电私信发送 {vbLQ}解锁码{vbRQ} 即可永久隐藏此提示。"
-#End If
+        PanHint.Visibility =
+            (BuildType <> BuildTypes.Release AndAlso VersionBranchMain = "Official" AndAlso
+             DonationRank < DonationRanks.Rank23 AndAlso Not CType(Settings.Get("HintSnapshot"), Boolean)).ToVisibility
     End Sub
     End Sub
 
 
-    '暂时关闭快照版提示
-#If Not BETA Then
-    Private Sub BtnHintClose_Click(sender As Object, e As EventArgs) Handles BtnHintClose.Click
-        AniDispose(PanHint, True)
-    End Sub
-#End If
-
 #Region "主页"
 #Region "主页"
 
 
     ''' <summary>
     ''' <summary>
@@ -36,7 +25,7 @@ Public Class PageLaunchRight
                     RefreshReal()
                     RefreshReal()
                 End SyncLock
                 End SyncLock
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "加载 PCL 主页自定义信息失败", If(ModeDebug, LogLevel.Msgbox, LogLevel.Hint))
+                Log(ex, "加载 PCL 主页自定义信息失败", If(ModeDebug, NotifyLevel.MsgBox, NotifyLevel.AllUsers))
             End Try
             End Try
         End Sub)
         End Sub)
     End Sub
     End Sub
@@ -46,7 +35,7 @@ Public Class PageLaunchRight
             Case 1
             Case 1
                 '加载本地文件
                 '加载本地文件
                 Log("[Page] 主页自定义数据来源:本地文件")
                 Log("[Page] 主页自定义数据来源:本地文件")
-                Content = ReadFile(Path & "PCL\Custom.xaml") 'ReadFile 会进行存在检测
+                Content = ReadFile(PathExeFolder & "PCL\Custom.xaml") 'ReadFile 会进行存在检测
             Case 2
             Case 2
                 '联网下载
                 '联网下载
                 Url = Settings.Get("UiCustomNet")
                 Url = Settings.Get("UiCustomNet")
@@ -168,7 +157,7 @@ Public Class PageLaunchRight
                 End If
                 End If
                 Log($"[Page] 需要下载联网主页,当前版本:{Version},检查源:{VersionAddress}")
                 Log($"[Page] 需要下载联网主页,当前版本:{Version},检查源:{VersionAddress}")
             Catch exx As Exception
             Catch exx As Exception
-                Log(exx, $"联网获取主页版本失败", LogLevel.Developer)
+                Log(exx, $"联网获取主页版本失败", NotifyLevel.DevelopOnly)
                 Log($"[Page] 无法检查联网主页版本,将直接下载,检查源:{VersionAddress}")
                 Log($"[Page] 无法检查联网主页版本,将直接下载,检查源:{VersionAddress}")
             End Try
             End Try
             '实际下载
             '实际下载
@@ -176,11 +165,11 @@ Public Class PageLaunchRight
             Log($"[Page] 已联网下载主页,内容长度:{FileContent.Length},来源:{Address}")
             Log($"[Page] 已联网下载主页,内容长度:{FileContent.Length},来源:{Address}")
             Settings.Set("CacheSavedPageUrl", Address)
             Settings.Set("CacheSavedPageUrl", Address)
             Settings.Set("CacheSavedPageVersion", Version)
             Settings.Set("CacheSavedPageVersion", Version)
-            WriteFile(PathTemp & "Cache\Custom.xaml", FileContent)
+            FileUtils.Write(PathTemp & "Cache\Custom.xaml", FileContent)
             '若内容变更则要求刷新
             '若内容变更则要求刷新
             If LoadedContentHash <> FileContent.GetHashCode() AndAlso ShouldRefresh Then Refresh()
             If LoadedContentHash <> FileContent.GetHashCode() AndAlso ShouldRefresh Then Refresh()
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, $"下载主页失败({Address})", If(ModeDebug, LogLevel.Msgbox, LogLevel.Hint))
+            Log(ex, $"下载主页失败({Address})", If(ModeDebug, NotifyLevel.MsgBox, NotifyLevel.AllUsers))
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -230,7 +219,7 @@ Public Class PageLaunchRight
                 End If
                 End If
                 Dim LoadStartTime As Date = Date.Now
                 Dim LoadStartTime As Date = Date.Now
                 '修改时应同时修改 PageOtherHelpDetail.Init
                 '修改时应同时修改 PageOtherHelpDetail.Init
-                Content = ArgumentReplace(Content, AddressOf EscapeXML)
+                Content = ArgumentReplace(Content, AddressOf EscapeUtils.XmlEscape)
                 Do While Content.Contains("xmlns")
                 Do While Content.Contains("xmlns")
                     Content = Content.RegexReplace("xmlns[^""']*(""|')[^""']*(""|')", "").Replace("xmlns", "") '禁止声明命名空间
                     Content = Content.RegexReplace("xmlns[^""']*(""|')[^""']*(""|')", "").Replace("xmlns", "") '禁止声明命名空间
                 Loop
                 Loop
@@ -256,7 +245,7 @@ Public Class PageLaunchRight
             If MyMsgBox(If(TypeOf ex Is UnauthorizedAccessException, ex.Message, $"主页内容编写有误,请根据下列错误信息进行检查:{vbCrLf}{ex.GetBrief}"),
             If MyMsgBox(If(TypeOf ex Is UnauthorizedAccessException, ex.Message, $"主页内容编写有误,请根据下列错误信息进行检查:{vbCrLf}{ex.GetBrief}"),
                         "加载主页失败", "重试", "取消") = 1 Then ForceRefresh()
                         "加载主页失败", "重试", "取消") = 1 Then ForceRefresh()
         Else
         Else
-            Log(ex, "加载主页失败", LogLevel.Hint)
+            Log(ex, "加载主页失败", NotifyLevel.AllUsers)
         End If
         End If
     End Sub
     End Sub
     ''' <summary>
     ''' <summary>

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

@@ -46,7 +46,7 @@
 
 
     Private Sub BtnEdit_Click(sender As Object, e As EventArgs) Handles BtnEdit.Click
     Private Sub BtnEdit_Click(sender As Object, e As EventArgs) Handles BtnEdit.Click
         If McLoginLoader.State = LoadState.Loading Then
         If McLoginLoader.State = LoadState.Loading Then
-            Log("[Launch] 要求更换角色,但登录加载器繁忙", LogLevel.Debug)
+            Log("[Launch] 要求更换角色,但登录加载器繁忙", NotifyLevel.DebugModeOnly)
             If TypeOf McLoginLoader.Input Is McLoginServer AndAlso CType(McLoginLoader.Input, McLoginServer).ForceReselectProfile Then
             If TypeOf McLoginLoader.Input Is McLoginServer AndAlso CType(McLoginLoader.Input, McLoginServer).ForceReselectProfile Then
                 Hint("正在尝试更换,请稍候!")
                 Hint("正在尝试更换,请稍候!")
             Else
             Else
@@ -65,7 +65,7 @@
                 McLoginLoader.WaitForExit(Data, IsForceRestart:=True)
                 McLoginLoader.WaitForExit(Data, IsForceRestart:=True)
                 RunInUi(Sub() Reload(True))
                 RunInUi(Sub() Reload(True))
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "更换角色失败", LogLevel.Hint)
+                Log(ex, "更换角色失败", NotifyLevel.AllUsers)
             End Try
             End Try
         End Sub)
         End Sub)
     End Sub
     End Sub

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

@@ -18,7 +18,7 @@
                 ComboAccounts.Items.Add(Item)
                 ComboAccounts.Items.Add(Item)
             Next
             Next
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, $"微软登录信息出错,登录信息已被重置({Settings.Get("LoginMsJson")})", LogLevel.Hint)
+            Log(ex, $"微软登录信息出错,登录信息已被重置({Settings.Get("LoginMsJson")})", NotifyLevel.AllUsers)
             Settings.Set("LoginMsJson", "{}")
             Settings.Set("LoginMsJson", "{}")
         End Try
         End Try
         '如果不保留输入,刷新列表后自动选择第一项
         '如果不保留输入,刷新列表后自动选择第一项
@@ -91,10 +91,10 @@
                 If ex.Message = "$$" Then
                 If ex.Message = "$$" Then
                 ElseIf ex.Message.StartsWithF("$") Then
                 ElseIf ex.Message.StartsWithF("$") Then
                     Hint(ex.Message.TrimStart("$"), HintType.Red)
                     Hint(ex.Message.TrimStart("$"), HintType.Red)
-                ElseIf TypeOf ex Is Security.Authentication.AuthenticationException AndAlso ex.Message.ContainsF("SSL/TLS") Then
-                    Log(ex, "正版登录验证失败,请考虑在 [设置 → 其他] 中关闭 [在正版登录时验证 SSL 证书],然后再试。" & vbCrLf & vbCrLf & "原始错误信息:", LogLevel.Msgbox)
+                ElseIf TypeOf ex Is Security.Authentication.AuthenticationException AndAlso ex.Message.Contains("SSL/TLS") Then
+                    Log(ex, "正版登录验证失败,请考虑在 [设置 → 其他] 中关闭 [在正版登录时验证 SSL 证书],然后再试。" & vbCrLf & vbCrLf & "原始错误信息:", NotifyLevel.MsgBox)
                 Else
                 Else
-                    Log(ex, "正版登录尝试失败", LogLevel.Msgbox)
+                    Log(ex, "正版登录尝试失败", NotifyLevel.MsgBox)
                 End If
                 End If
             Finally
             Finally
                 RunInUi(
                 RunInUi(

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

@@ -36,8 +36,8 @@
                                 LogoScale="1.1" Logo="M462.336 924.891429L73.142857 950.857143l25.965714-389.193143 467.017143-467.017143a73.398857 73.398857 0 0 1 103.789715 0l259.437714 259.437714a73.398857 73.398857 0 0 1 0 103.789715l-467.017143 467.017143z m155.684571-778.349715L202.861714 561.664l259.474286 259.474286L877.458286 405.942857 618.057143 146.541714zM151.003429 873.033143l233.508571-25.965714-207.579429-207.579429-25.965714 233.545143z m544.841142-492.982857l51.894858 51.894857-233.508572 233.508571-51.931428-51.894857 233.545142-233.508571z">
                                 LogoScale="1.1" Logo="M462.336 924.891429L73.142857 950.857143l25.965714-389.193143 467.017143-467.017143a73.398857 73.398857 0 0 1 103.789715 0l259.437714 259.437714a73.398857 73.398857 0 0 1 0 103.789715l-467.017143 467.017143z m155.684571-778.349715L202.861714 561.664l259.474286 259.474286L877.458286 405.942857 618.057143 146.541714zM151.003429 873.033143l233.508571-25.965714-207.579429-207.579429-25.965714 233.545143z m544.841142-492.982857l51.894858 51.894857-233.508572 233.508571-51.931428-51.894857 233.545142-233.508571z">
                     <local:MyIconButton.ContextMenu>
                     <local:MyIconButton.ContextMenu>
                         <ContextMenu Closed="HidePanel" HorizontalOffset="10" VerticalOffset="18">
                         <ContextMenu Closed="HidePanel" HorizontalOffset="10" VerticalOffset="18">
-                            <local:MyMenuItem Click="BtnEditPassword_Click" Header="修改密码" />
-                            <local:MyMenuItem Click="BtnEditName_Click" Header="修改玩家名" />
+                            <local:MyMenuItem Header="修改密码" local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://account.live.com/password/Change" />
+                            <local:MyMenuItem Header="修改玩家名" local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://www.minecraft.net/zh-hans/msaprofile/mygames/editprofile" />
                         </ContextMenu>
                         </ContextMenu>
                     </local:MyIconButton.ContextMenu>
                     </local:MyIconButton.ContextMenu>
                 </local:MyIconButton>
                 </local:MyIconButton>

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

@@ -41,12 +41,6 @@
     Private Sub BtnEdit_Click(sender As Object, e As EventArgs) Handles BtnEdit.Click
     Private Sub BtnEdit_Click(sender As Object, e As EventArgs) Handles BtnEdit.Click
         BtnEdit.ContextMenu.IsOpen = True
         BtnEdit.ContextMenu.IsOpen = True
     End Sub
     End Sub
-    Public Sub BtnEditPassword_Click(sender As Object, e As RoutedEventArgs)
-        OpenWebsite("https://account.live.com/password/Change")
-    End Sub
-    Public Sub BtnEditName_Click(sender As Object, e As RoutedEventArgs)
-        OpenWebsite("https://www.minecraft.net/zh-hans/msaprofile/mygames/editprofile")
-    End Sub
 
 
     '退出登录
     '退出登录
     Private Sub BtnExit_Click() Handles BtnExit.Click
     Private Sub BtnExit_Click() Handles BtnExit.Click
@@ -125,12 +119,12 @@ Retry:
             If TypeOf ex Is OperationCanceledException OrElse TypeOf ex Is TimeoutException Then
             If TypeOf ex Is OperationCanceledException OrElse TypeOf ex Is TimeoutException Then
                 Hint("更改皮肤失败:连接皮肤服务器超时,请稍后再试,或使用 VPN 改善网络环境", HintType.Red)
                 Hint("更改皮肤失败:连接皮肤服务器超时,请稍后再试,或使用 VPN 改善网络环境", HintType.Red)
             ElseIf TypeOf ex Is ResponsedWebException AndAlso CType(ex, ResponsedWebException).StatusCode = HttpStatusCode.Unauthorized Then
             ElseIf TypeOf ex Is ResponsedWebException AndAlso CType(ex, ResponsedWebException).StatusCode = HttpStatusCode.Unauthorized Then
-                Log(ex, "更改皮肤时遭遇 401 错误", LogLevel.Developer)
+                Log(ex, "更改皮肤时遭遇 401 错误", NotifyLevel.DevelopOnly)
                 Hint("正在重新登录,将在登录后自动更改皮肤……")
                 Hint("正在重新登录,将在登录后自动更改皮肤……")
                 McLoginMsLoader.Start(GetLoginData(), IsForceRestart:=True)
                 McLoginMsLoader.Start(GetLoginData(), IsForceRestart:=True)
                 GoTo Retry
                 GoTo Retry
             Else
             Else
-                Log(ex, "更改皮肤失败", LogLevel.Hint)
+                Log(ex, "更改皮肤失败", NotifyLevel.AllUsers)
             End If
             End If
         Finally
         Finally
             IsChanging = False
             IsChanging = False

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

@@ -20,7 +20,8 @@
             <TextBlock Name="TextName" Text="加载中" Margin="8,0" FontSize="14" HorizontalAlignment="Center" TextTrimming="CharacterEllipsis" />
             <TextBlock Name="TextName" Text="加载中" Margin="8,0" FontSize="14" HorizontalAlignment="Center" TextTrimming="CharacterEllipsis" />
             <TextBlock Name="TextEmail" Text="加载中" Margin="8,4,8,0" FontSize="12" HorizontalAlignment="Center" TextTrimming="CharacterEllipsis" Foreground="{DynamicResource ColorBrushGray3}" />
             <TextBlock Name="TextEmail" Text="加载中" Margin="8,4,8,0" FontSize="12" HorizontalAlignment="Center" TextTrimming="CharacterEllipsis" Foreground="{DynamicResource ColorBrushGray3}" />
         </StackPanel>
         </StackPanel>
-        <local:MyIconButton ToolTip="更改密码" Grid.Row="1" Height="14" Opacity="0" x:Name="BtnEdit" ToolTipService.Placement="Center" ToolTipService.VerticalOffset="32" ToolTipService.HorizontalOffset="1" ToolTipService.InitialShowDelay="50" Grid.Column="0" HorizontalAlignment="Center" Logo="F1 M 53.2929,21.2929L 54.7071,22.7071C 56.4645,24.4645 56.4645,27.3137 54.7071,29.0711L 52.2323,31.5459L 44.4541,23.7677L 46.9289,21.2929C 48.6863,19.5355 51.5355,19.5355 53.2929,21.2929 Z M 31.7262,52.052L 23.948,44.2738L 43.0399,25.182L 50.818,32.9601L 31.7262,52.052 Z M 23.2409,47.1023L 28.8977,52.7591L 21.0463,54.9537L 23.2409,47.1023 Z" Margin="8,0,0,0" VerticalAlignment="Center" />
+        <local:MyIconButton ToolTip="更改密码" Grid.Row="1" Height="14" Opacity="0" x:Name="BtnEdit" ToolTipService.Placement="Center" ToolTipService.VerticalOffset="32" ToolTipService.HorizontalOffset="1" ToolTipService.InitialShowDelay="50" Grid.Column="0" HorizontalAlignment="Center" Logo="F1 M 53.2929,21.2929L 54.7071,22.7071C 56.4645,24.4645 56.4645,27.3137 54.7071,29.0711L 52.2323,31.5459L 44.4541,23.7677L 46.9289,21.2929C 48.6863,19.5355 51.5355,19.5355 53.2929,21.2929 Z M 31.7262,52.052L 23.948,44.2738L 43.0399,25.182L 50.818,32.9601L 31.7262,52.052 Z M 23.2409,47.1023L 28.8977,52.7591L 21.0463,54.9537L 23.2409,47.1023 Z" Margin="8,0,0,0" VerticalAlignment="Center"
+                  local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://login.mc-user.com:233/account/changepw" />
         <local:MyIconButton ToolTip="退出登录" Grid.Row="1" Height="14" Opacity="0" x:Name="BtnExit" ToolTipService.Placement="Center" ToolTipService.VerticalOffset="32" ToolTipService.HorizontalOffset="1" ToolTipService.InitialShowDelay="50" Grid.Column="2" HorizontalAlignment="Center" Logo="F1 M 51.0071,19.0027L 51.0071,27.0038L 48.0067,27.0038L 48.0067,22.0031L 25.0035,22.0031L 25.0035,54.0075L 48.0067,54.0075L 48.0067,49.0068L 51.0071,49.0068L 51.0071,57.008L 22.1698,57.008L 22.0031,19.0027L 51.0071,19.0027 Z M 30.0042,35.0049L 45.2563,35.0049L 37.005,28.0039L 47.0066,28.0039L 58.5082,38.0053L 47.0066,48.0067L 37.005,48.0067L 45.2563,41.0057L 30.0042,41.0057L 30.0042,35.0049 Z" Margin="0,0,8,0" VerticalAlignment="Center" />
         <local:MyIconButton ToolTip="退出登录" Grid.Row="1" Height="14" Opacity="0" x:Name="BtnExit" ToolTipService.Placement="Center" ToolTipService.VerticalOffset="32" ToolTipService.HorizontalOffset="1" ToolTipService.InitialShowDelay="50" Grid.Column="2" HorizontalAlignment="Center" Logo="F1 M 51.0071,19.0027L 51.0071,27.0038L 48.0067,27.0038L 48.0067,22.0031L 25.0035,22.0031L 25.0035,54.0075L 48.0067,54.0075L 48.0067,49.0068L 51.0071,49.0068L 51.0071,57.008L 22.1698,57.008L 22.0031,19.0027L 51.0071,19.0027 Z M 30.0042,35.0049L 45.2563,35.0049L 37.005,28.0039L 47.0066,28.0039L 58.5082,38.0053L 47.0066,48.0067L 37.005,48.0067L 45.2563,41.0057L 30.0042,41.0057L 30.0042,35.0049 Z" Margin="0,0,8,0" VerticalAlignment="Center" />
     </Grid>
     </Grid>
     <local:MySkin HorizontalAlignment="Center" VerticalAlignment="Top" x:Name="Skin" HasCape="False" />
     <local:MySkin HorizontalAlignment="Center" VerticalAlignment="Top" x:Name="Skin" HasCape="False" />

+ 0 - 3
Plain Craft Launcher 2/Pages/PageLaunch/PageLoginNideSkin.xaml.vb

@@ -44,9 +44,6 @@
         }, "PageLoginNideSkin Button")
         }, "PageLoginNideSkin Button")
     End Sub
     End Sub
 
 
-    Private Sub BtnEdit_Click(sender As Object, e As EventArgs) Handles BtnEdit.Click
-        OpenWebsite("https://login.mc-user.com:233/account/changepw")
-    End Sub
     Public Shared Sub ExitLogin() Handles BtnExit.Click
     Public Shared Sub ExitLogin() Handles BtnExit.Click
         Settings.Set("CacheNideAccess", "")
         Settings.Set("CacheNideAccess", "")
         McLoginNideLoader.Input = Nothing '防止因为输入的用户名密码相同,直接使用了上次登录的加载器结果
         McLoginNideLoader.Input = Nothing '防止因为输入的用户名密码相同,直接使用了上次登录的加载器结果

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

@@ -102,11 +102,11 @@ Public Class PageLinkMain
         Function() As String
         Function() As String
             Dim Result As String = ""
             Dim Result As String = ""
             For i = 1 To 5
             For i = 1 To 5
-                Result &= RadixConvert(RandomInteger(0, 35), 10, 36)
+                Result &= RandomInteger(0, 35).ConvertRadix(36)
             Next
             Next
             Return Result.Replace("O", "0").Replace("I", "1")
             Return Result.Replace("O", "0").Replace("I", "1")
         End Function
         End Function
-        NetworkName = $"P{RadixConvert(ServerPort, 10, 16).PadLeft(4, "0"c)}-{GenerateRandomCode()}"
+        NetworkName = $"P{ServerPort.ConvertRadix(16).PadLeft(4, "0"c)}-{GenerateRandomCode()}"
         NetworkSecret = GenerateRandomCode()
         NetworkSecret = GenerateRandomCode()
         DiscoverNodeID = -1
         DiscoverNodeID = -1
         Log($"[Link] 尝试创建房间,网络名 {NetworkName},网络密码 {NetworkSecret},端口 {ServerPort}")
         Log($"[Link] 尝试创建房间,网络名 {NetworkName},网络密码 {NetworkSecret},端口 {ServerPort}")
@@ -138,13 +138,13 @@ Public Class PageLinkMain
         Log($"[Link] 实际使用的邀请码:{Code}")
         Log($"[Link] 实际使用的邀请码:{Code}")
         '基础信息
         '基础信息
         IsServerSide = False
         IsServerSide = False
-        ServerPort = RadixConvert(Code.Substring(1, 4), 16, 10)
+        ServerPort = Code.Substring(1, 4).Upper.ConvertRadix(16, 10)
         NetworkName = Code.Substring(0, 11)
         NetworkName = Code.Substring(0, 11)
         NetworkSecret = Code.Substring(12, 5)
         NetworkSecret = Code.Substring(12, 5)
         If Code.Substring(20, 3) = "000" Then
         If Code.Substring(20, 3) = "000" Then
             DiscoverNodeID = -2
             DiscoverNodeID = -2
         Else
         Else
-            DiscoverNodeID = RadixConvert(Code.Substring(20, 3), 16, 10)
+            DiscoverNodeID = Code.Substring(20, 3).Upper.ConvertRadix(16, 10)
         End If
         End If
         Log($"[Link] 尝试加入房间,网络名 {NetworkName},网络密码 {NetworkSecret},端口 {ServerPort},发现节点 {DiscoverNodeID}")
         Log($"[Link] 尝试加入房间,网络名 {NetworkName},网络密码 {NetworkSecret},端口 {ServerPort},发现节点 {DiscoverNodeID}")
         '启动
         '启动
@@ -339,9 +339,9 @@ Public Class PageLinkMain
     ''' 退出联机。
     ''' 退出联机。
     ''' 返回是否弹出了警告窗口并且玩家选择了取消。
     ''' 返回是否弹出了警告窗口并且玩家选择了取消。
     ''' </summary>
     ''' </summary>
-    Public Function TryExit(Slient As Boolean, Closing As Boolean) As Boolean
+    Public Function TryExit(Silent As Boolean, Closing As Boolean) As Boolean
         If LinkState = LinkStates.Waiting OrElse LinkState = LinkStates.Failed Then Return False
         If LinkState = LinkStates.Waiting OrElse LinkState = LinkStates.Failed Then Return False
-        If Not Slient Then
+        If Not Silent Then
             If IsServerSide AndAlso PeopleCount > 1 Then
             If IsServerSide AndAlso PeopleCount > 1 Then
                 If MyMsgBox("你确定要关闭联机房间吗?" & vbCrLf & "所有玩家都需要重新输入邀请码才可加入游戏!", "退出联机", "确定", "取消", IsWarn:=True) = 2 Then Return True
                 If MyMsgBox("你确定要关闭联机房间吗?" & vbCrLf & "所有玩家都需要重新输入邀请码才可加入游戏!", "退出联机", "确定", "取消", IsWarn:=True) = 2 Then Return True
             ElseIf Closing Then
             ElseIf Closing Then
@@ -361,7 +361,7 @@ Public Class PageLinkMain
     End Sub
     End Sub
     Private Function GetInviteCode() As String
     Private Function GetInviteCode() As String
         Return $"{NetworkName}-{NetworkSecret}-{INVITE_CODE_VERSION.ToString.PadLeft(2, "0"c)}{ _
         Return $"{NetworkName}-{NetworkSecret}-{INVITE_CODE_VERSION.ToString.PadLeft(2, "0"c)}{ _
-            RadixConvert(If(DiscoverNodeID = -1, 0, DiscoverNodeID), 10, 16).PadLeft(3, "0"c)}"
+            If(DiscoverNodeID = -1, 0, DiscoverNodeID).ConvertRadix(16).PadLeft(3, "0"c)}"
     End Function
     End Function
 
 
     '复制 IP
     '复制 IP
@@ -451,7 +451,7 @@ Public Class PageLinkMain
             Case Reflection.ProcessorArchitecture.Arm
             Case Reflection.ProcessorArchitecture.Arm
                 Architecture = "arm64"
                 Architecture = "arm64"
             Case Else
             Case Else
-                Log($"[Link] CPU 是不支持的 {Architecture} 架构,这可能会导致联机模块无法启动!", LogLevel.Debug)
+                Log($"[Link] CPU 是不支持的 {Architecture} 架构,这可能会导致联机模块无法启动!", NotifyLevel.DebugModeOnly)
                 Architecture = "arm64"
                 Architecture = "arm64"
         End Select
         End Select
         Log("[Link] CPU 架构:" & Architecture)
         Log("[Link] CPU 架构:" & Architecture)
@@ -524,7 +524,7 @@ Public Class PageLinkMain
         '获取启动参数
         '获取启动参数
         Dim Arguments As String = ServerConfig("Link")("Argument")
         Dim Arguments As String = ServerConfig("Link")("Argument")
         Arguments += $" --network-name={NetworkName} --network-secret={NetworkSecret} --listeners {ListenersPort} --rpc-portal {RPCPort} --private-mode true"
         Arguments += $" --network-name={NetworkName} --network-secret={NetworkSecret} --listeners {ListenersPort} --rpc-portal {RPCPort} --private-mode true"
-        Dim HostName = If(IsServerSide, "Server-", "Client-") & RadixConvert(Math.Abs(Identify.GetHashCode), 10, 36)
+        Dim HostName = If(IsServerSide, "Server-", "Client-") & Math.Abs(Identify.GetHashCode).ConvertRadix(36)
         If IsServerSide Then
         If IsServerSide Then
             Arguments += $" -i 10.114.114.114 --hostname={HostName} --tcp-whitelist={ServerPort} --udp-whitelist={ServerPort}"
             Arguments += $" -i 10.114.114.114 --hostname={HostName} --tcp-whitelist={ServerPort} --udp-whitelist={ServerPort}"
         Else
         Else
@@ -562,13 +562,13 @@ Public Class PageLinkMain
             Dim PeerCount As Integer = If(Peers Is Nothing, -1, Peers.Where(Function(p) p.Ping > 0).Count)
             Dim PeerCount As Integer = If(Peers Is Nothing, -1, Peers.Where(Function(p) p.Ping > 0).Count)
             Select Case PeerCount
             Select Case PeerCount
                 Case -1 'CLI 无返回
                 Case -1 'CLI 无返回
-                    Task.Progress = MathClamp(Task.Progress + 0.02, 0.15, 0.25)
+                    Task.Progress = (Task.Progress + 0.02).Clamp(0.15, 0.25)
                 Case 0 'CLI 有返回,但未连接到任何节点
                 Case 0 'CLI 有返回,但未连接到任何节点
                     UpdateLoadingPage("正在连接到节点……", "连接节点")
                     UpdateLoadingPage("正在连接到节点……", "连接节点")
-                    Task.Progress = MathClamp(Task.Progress + 0.02, If(IsServerSide, 0.5, 0.3), If(IsServerSide, 0.95, 0.5))
+                    Task.Progress = (Task.Progress + 0.02).Clamp(If(IsServerSide, 0.5, 0.3), If(IsServerSide, 0.95, 0.5))
                 Case Else '已连接到节点,但未连接到房主
                 Case Else '已连接到节点,但未连接到房主
                     UpdateLoadingPage("正在连接到房主……", "连接房主")
                     UpdateLoadingPage("正在连接到房主……", "连接房主")
-                    Task.Progress = MathClamp(Task.Progress + 0.02, Math.Min(0.45 + PeerCount * 0.05, 0.65), 0.95)
+                    Task.Progress = (Task.Progress + 0.02).Clamp(Math.Min(0.45 + PeerCount * 0.05, 0.65), 0.95)
             End Select
             End Select
             '超时判定
             '超时判定
             If LastProgress <> Task.Progress Then
             If LastProgress <> Task.Progress Then
@@ -654,14 +654,14 @@ ForcedPass:
             ElseIf DiscoverNodeID > 0 Then '>0:作为加入者,根据 ID 选择对应的发现节点;如果没有,使用回退发现节点
             ElseIf DiscoverNodeID > 0 Then '>0:作为加入者,根据 ID 选择对应的发现节点;如果没有,使用回退发现节点
                 SelectedDiscoverNode = Nodes.FirstOrDefault(Function(n) n("id").ToObject(Of Integer) = DiscoverNodeID)
                 SelectedDiscoverNode = Nodes.FirstOrDefault(Function(n) n("id").ToObject(Of Integer) = DiscoverNodeID)
                 If SelectedDiscoverNode Is Nothing Then
                 If SelectedDiscoverNode Is Nothing Then
-                    Log($"[Link] 未找到 ID {DiscoverNodeID} 的发现节点", LogLevel.Debug)
+                    Log($"[Link] 未找到 ID {DiscoverNodeID} 的发现节点", NotifyLevel.DebugModeOnly)
                     Panic("房间已过期", "请让房主重新创建房间!")
                     Panic("房间已过期", "请让房主重新创建房间!")
                     Throw New ThreadInterruptedException
                     Throw New ThreadInterruptedException
                 End If
                 End If
             End If
             End If
             If SelectedDiscoverNode Is Nothing Then '使用回退发现节点
             If SelectedDiscoverNode Is Nothing Then '使用回退发现节点
                 SelectedDiscoverNode = New JObject From {{"address", FallbackDiscoverAddress}, {"id", FallbackDiscoverID}}
                 SelectedDiscoverNode = New JObject From {{"address", FallbackDiscoverAddress}, {"id", FallbackDiscoverID}}
-                Log("[Link] 将使用回退发现节点", LogLevel.Debug)
+                Log("[Link] 将使用回退发现节点", NotifyLevel.DebugModeOnly)
             End If
             End If
             FinalPeers.Add(SelectedDiscoverNode("address").ToString())
             FinalPeers.Add(SelectedDiscoverNode("address").ToString())
             FinalDiscoverID = SelectedDiscoverNode("id").ToObject(Of Integer)
             FinalDiscoverID = SelectedDiscoverNode("id").ToObject(Of Integer)
@@ -676,7 +676,7 @@ ForcedPass:
         Catch ex As ThreadInterruptedException
         Catch ex As ThreadInterruptedException
             Throw
             Throw
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "获取节点列表失败,联机质量可能受到影响", LogLevel.Hint)
+            Log(ex, "获取节点列表失败,联机质量可能受到影响", NotifyLevel.AllUsers)
             FinalPeers.AddRange(ServerConfig("Link")("Peers").Select(Function(p) p.ToString))
             FinalPeers.AddRange(ServerConfig("Link")("Peers").Select(Function(p) p.ToString))
             If FinalDiscoverID <= 0 Then
             If FinalDiscoverID <= 0 Then
                 FinalPeers.Add(FallbackDiscoverAddress)
                 FinalPeers.Add(FallbackDiscoverAddress)
@@ -800,11 +800,11 @@ ForcedPass:
         LogHistory.Enqueue(Line)
         LogHistory.Enqueue(Line)
         If LogHistory.Count >= 10 Then LogHistory.Dequeue()
         If LogHistory.Count >= 10 Then LogHistory.Dequeue()
         '检查日志内容
         '检查日志内容
-        If Line.ContainsF("new peer connection added", True) Then
-            Log("[Link] 已建立连接:" & If(RegexSeek(Line, "(?<=remote_addr.+?"")[^""}]{3,}"), Line), LogLevel.Debug)
+        If Line.ContainsIgnoreCase("new peer connection added") Then
+            Log("[Link] 已建立连接:" & If(RegexSeek(Line, "(?<=remote_addr.+?"")[^""}]{3,}"), Line), NotifyLevel.DebugModeOnly)
             Update()
             Update()
-        ElseIf Line.ContainsF("peer connection removed", True) Then
-            Log("[Link] 已断开连接:" & If(RegexSeek(Line, "(?<=remote_addr.+?"")[^""}]{3,}"), Line), LogLevel.Debug)
+        ElseIf Line.ContainsIgnoreCase("peer connection removed") Then
+            Log("[Link] 已断开连接:" & If(RegexSeek(Line, "(?<=remote_addr.+?"")[^""}]{3,}"), Line), NotifyLevel.DebugModeOnly)
             Update()
             Update()
         End If
         End If
     End Sub
     End Sub
@@ -873,8 +873,8 @@ ForcedPass:
             '基础信息
             '基础信息
             Double.TryParse(Info("lat_ms"), NumberStyles.Any, CultureInfo.InvariantCulture, Ping)
             Double.TryParse(Info("lat_ms"), NumberStyles.Any, CultureInfo.InvariantCulture, Ping)
             Name = PeerName
             Name = PeerName
-            Relay = Info("cost").ToString.ContainsF("relay", True)
-            NATType = Info("nat_type").ToString.ParseToEnum(Of NATTypes)
+            Relay = Info("cost").ToString.ContainsIgnoreCase("relay")
+            NATType = Info("nat_type").ToString.ToEnum(Of NATTypes)
         End Sub
         End Sub
         Public Overrides Function ToString() As String
         Public Overrides Function ToString() As String
             Return $"{Type} - {Name} - Ping {Ping:0.0}ms [中继? {Relay}] - NAT {NATType}"
             Return $"{Type} - {Name} - Ping {Ping:0.0}ms [中继? {Relay}] - NAT {NATType}"
@@ -1011,7 +1011,7 @@ ForcedPass:
                     '每 200ms 更新进度条
                     '每 200ms 更新进度条
                     If LinkState = LinkStates.Loading Then RunInUi(AddressOf UpdateProgressBar)
                     If LinkState = LinkStates.Loading Then RunInUi(AddressOf UpdateProgressBar)
                 Catch ex As Exception
                 Catch ex As Exception
-                    Log(ex, "联机模块主时钟出错", LogLevel.Feedback)
+                    Log(ex, "联机模块主时钟出错", NotifyLevel.MsgBoxAndFeedback)
                     Thread.Sleep(10000)
                     Thread.Sleep(10000)
                 End Try
                 End Try
             Loop
             Loop

+ 254 - 3
Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml

@@ -117,15 +117,21 @@
                 <StackPanel Margin="25,40,19,20">
                 <StackPanel Margin="25,40,19,20">
                     <TextBlock HorizontalAlignment="Left" Text="以下是近期的高级赞助者们!感谢大家对 PCL 的支持!!!" Margin="0,0,25,5.5" TextTrimming="None" TextWrapping="WrapWithOverflow" />
                     <TextBlock HorizontalAlignment="Left" Text="以下是近期的高级赞助者们!感谢大家对 PCL 的支持!!!" Margin="0,0,25,5.5" TextTrimming="None" TextWrapping="WrapWithOverflow" />
                     <WrapPanel Margin="0,0,0,10">
                        <TextBlock Margin="0,1.5,6,0" Text="." />
                     <WrapPanel Margin="0,0,0,10">
                        <TextBlock Margin="0,1.5,6,0" Text="." />
+                        <TextBlock Margin="0,1.5,6,0" Text="." />
                         <TextBlock Margin="0,1.5,6,0" Text="X" />
                         <TextBlock Margin="0,1.5,6,0" Text="X" />
                         <TextBlock Margin="0,1.5,6,0" Text="..." />
                         <TextBlock Margin="0,1.5,6,0" Text="..." />
                         <TextBlock Margin="0,1.5,6,0" Text="R" />
                         <TextBlock Margin="0,1.5,6,0" Text="R" />
+                        <TextBlock Margin="0,1.5,6,0" Text="X" />
                         <TextBlock Margin="0,1.5,6,0" Text="D" />
                         <TextBlock Margin="0,1.5,6,0" Text="D" />
                         <TextBlock Margin="0,1.5,6,0" Text="le" />
                         <TextBlock Margin="0,1.5,6,0" Text="le" />
                         <TextBlock Margin="0,1.5,6,0" Text="vv" />
                         <TextBlock Margin="0,1.5,6,0" Text="vv" />
+                        <TextBlock Margin="0,1.5,6,0" Text="卓" />
+                        <TextBlock Margin="0,1.5,6,0" Text="玖" />
                         <TextBlock Margin="0,1.5,6,0" Text="雨" />
                         <TextBlock Margin="0,1.5,6,0" Text="雨" />
                         <TextBlock Margin="0,1.5,6,0" Text="辰" />
                         <TextBlock Margin="0,1.5,6,0" Text="辰" />
                         <TextBlock Margin="0,1.5,6,0" Text="李" />
                         <TextBlock Margin="0,1.5,6,0" Text="李" />
+                        <TextBlock Margin="0,1.5,6,0" Text="茧" />
+                        <TextBlock Margin="0,1.5,6,0" Text="烬" />
                         <TextBlock Margin="0,1.5,6,0" Text="莫" />
                         <TextBlock Margin="0,1.5,6,0" Text="莫" />
                         <TextBlock Margin="0,1.5,6,0" Text="尊" />
                         <TextBlock Margin="0,1.5,6,0" Text="尊" />
                         <TextBlock Margin="0,1.5,6,0" Text="埃" />
                         <TextBlock Margin="0,1.5,6,0" Text="埃" />
@@ -137,20 +143,40 @@
                         <TextBlock Margin="0,1.5,6,0" Text="零" />
                         <TextBlock Margin="0,1.5,6,0" Text="零" />
                         <TextBlock Margin="0,1.5,6,0" Text="笕" />
                         <TextBlock Margin="0,1.5,6,0" Text="笕" />
                         <TextBlock Margin="0,1.5,6,0" Text="枫" />
                         <TextBlock Margin="0,1.5,6,0" Text="枫" />
+                        <TextBlock Margin="0,1.5,6,0" Text="W" />
+                        <TextBlock Margin="0,1.5,6,0" Text="悦" />
+                        <TextBlock Margin="0,1.5,6,0" Text="缘" />
+                        <TextBlock Margin="0,1.5,6,0" Text="木" />
+                        <TextBlock Margin="0,1.5,6,0" Text="蔚" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Cr" />
+                        <TextBlock Margin="0,1.5,6,0" Text="@" />
+                        <TextBlock Margin="0,1.5,6,0" Text="呀" />
+                        <TextBlock Margin="0,1.5,6,0" Text="谷" />
+                        <TextBlock Margin="0,1.5,6,0" Text="泽" />
+                        <TextBlock Margin="0,1.5,6,0" Text="林" />
                         <TextBlock Margin="0,1.5,6,0" Text="TT" />
                         <TextBlock Margin="0,1.5,6,0" Text="TT" />
+                        <TextBlock Margin="0,1.5,6,0" Text="yu" />
                         <TextBlock Margin="0,1.5,6,0" Text="......" />
                         <TextBlock Margin="0,1.5,6,0" Text="......" />
                         <TextBlock Margin="0,1.5,6,0" Text="zjx" />
                         <TextBlock Margin="0,1.5,6,0" Text="zjx" />
+                        <TextBlock Margin="0,1.5,6,0" Text="up" />
                         <TextBlock Margin="0,1.5,6,0" Text="XX" />
                         <TextBlock Margin="0,1.5,6,0" Text="XX" />
                         <TextBlock Margin="0,1.5,6,0" Text="sjg" />
                         <TextBlock Margin="0,1.5,6,0" Text="sjg" />
                         <TextBlock Margin="0,1.5,6,0" Text="yfx" />
                         <TextBlock Margin="0,1.5,6,0" Text="yfx" />
+                        <TextBlock Margin="0,1.5,6,0" Text="💬" />
                         <TextBlock Margin="0,1.5,6,0" Text="7%" />
                         <TextBlock Margin="0,1.5,6,0" Text="7%" />
                         <TextBlock Margin="0,1.5,6,0" Text="UU" />
                         <TextBlock Margin="0,1.5,6,0" Text="UU" />
                         <TextBlock Margin="0,1.5,6,0" Text="PQ" />
                         <TextBlock Margin="0,1.5,6,0" Text="PQ" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Siri" />
                         <TextBlock Margin="0,1.5,6,0" Text="bre" />
                         <TextBlock Margin="0,1.5,6,0" Text="bre" />
+                        <TextBlock Margin="0,1.5,6,0" Text="qin" />
+                        <TextBlock Margin="0,1.5,6,0" Text="ZJL" />
+                        <TextBlock Margin="0,1.5,6,0" Text="汐a" />
+                        <TextBlock Margin="0,1.5,6,0" Text="PFF" />
                         <TextBlock Margin="0,1.5,6,0" Text="ZYL" />
                         <TextBlock Margin="0,1.5,6,0" Text="ZYL" />
                         <TextBlock Margin="0,1.5,6,0" Text="ZYL" />
                         <TextBlock Margin="0,1.5,6,0" Text="ZYL" />
                         <TextBlock Margin="0,1.5,6,0" Text="555" />
                         <TextBlock Margin="0,1.5,6,0" Text="555" />
                         <TextBlock Margin="0,1.5,6,0" Text="661" />
                         <TextBlock Margin="0,1.5,6,0" Text="661" />
+                        <TextBlock Margin="0,1.5,6,0" Text="LSP" />
                         <TextBlock Margin="0,1.5,6,0" Text="轩₩" />
                         <TextBlock Margin="0,1.5,6,0" Text="轩₩" />
                         <TextBlock Margin="0,1.5,6,0" Text="zzw" />
                         <TextBlock Margin="0,1.5,6,0" Text="zzw" />
                         <TextBlock Margin="0,1.5,6,0" Text="ybb" />
                         <TextBlock Margin="0,1.5,6,0" Text="ybb" />
@@ -158,17 +184,25 @@
                         <TextBlock Margin="0,1.5,6,0" Text="ZCY" />
                         <TextBlock Margin="0,1.5,6,0" Text="ZCY" />
                         <TextBlock Margin="0,1.5,6,0" Text="Kiro" />
                         <TextBlock Margin="0,1.5,6,0" Text="Kiro" />
                         <TextBlock Margin="0,1.5,6,0" Text="Win" />
                         <TextBlock Margin="0,1.5,6,0" Text="Win" />
+                        <TextBlock Margin="0,1.5,6,0" Text="hhh" />
+                        <TextBlock Margin="0,1.5,6,0" Text="bbb" />
                         <TextBlock Margin="0,1.5,6,0" Text="沙轻" />
                         <TextBlock Margin="0,1.5,6,0" Text="沙轻" />
                         <TextBlock Margin="0,1.5,6,0" Text="缤语" />
                         <TextBlock Margin="0,1.5,6,0" Text="缤语" />
                         <TextBlock Margin="0,1.5,6,0" Text="xsqs" />
                         <TextBlock Margin="0,1.5,6,0" Text="xsqs" />
                         <TextBlock Margin="0,1.5,6,0" Text="空青" />
                         <TextBlock Margin="0,1.5,6,0" Text="空青" />
                         <TextBlock Margin="0,1.5,6,0" Text="奇才" />
                         <TextBlock Margin="0,1.5,6,0" Text="奇才" />
+                        <TextBlock Margin="0,1.5,6,0" Text="聆溪" />
                         <TextBlock Margin="0,1.5,6,0" Text="柒安" />
                         <TextBlock Margin="0,1.5,6,0" Text="柒安" />
                         <TextBlock Margin="0,1.5,6,0" Text="君猫" />
                         <TextBlock Margin="0,1.5,6,0" Text="君猫" />
+                        <TextBlock Margin="0,1.5,6,0" Text="yihe" />
+                        <TextBlock Margin="0,1.5,6,0" Text="泽涛" />
+                        <TextBlock Margin="0,1.5,6,0" Text="习页" />
                         <TextBlock Margin="0,1.5,6,0" Text="旋叶" />
                         <TextBlock Margin="0,1.5,6,0" Text="旋叶" />
+                        <TextBlock Margin="0,1.5,6,0" Text="方十" />
                         <TextBlock Margin="0,1.5,6,0" Text="苑煜" />
                         <TextBlock Margin="0,1.5,6,0" Text="苑煜" />
                         <TextBlock Margin="0,1.5,6,0" Text="沐清" />
                         <TextBlock Margin="0,1.5,6,0" Text="沐清" />
                         <TextBlock Margin="0,1.5,6,0" Text="阿杰" />
                         <TextBlock Margin="0,1.5,6,0" Text="阿杰" />
+                        <TextBlock Margin="0,1.5,6,0" Text="吸管" />
                         <TextBlock Margin="0,1.5,6,0" Text="夜幕" />
                         <TextBlock Margin="0,1.5,6,0" Text="夜幕" />
                         <TextBlock Margin="0,1.5,6,0" Text="夜墨" />
                         <TextBlock Margin="0,1.5,6,0" Text="夜墨" />
                         <TextBlock Margin="0,1.5,6,0" Text="芥裔" />
                         <TextBlock Margin="0,1.5,6,0" Text="芥裔" />
@@ -218,12 +252,41 @@
                         <TextBlock Margin="0,1.5,6,0" Text="雨润" />
                         <TextBlock Margin="0,1.5,6,0" Text="雨润" />
                         <TextBlock Margin="0,1.5,6,0" Text="颜峰" />
                         <TextBlock Margin="0,1.5,6,0" Text="颜峰" />
                         <TextBlock Margin="0,1.5,6,0" Text="符符" />
                         <TextBlock Margin="0,1.5,6,0" Text="符符" />
+                        <TextBlock Margin="0,1.5,6,0" Text="冰块" />
                         <TextBlock Margin="0,1.5,6,0" Text="莫林" />
                         <TextBlock Margin="0,1.5,6,0" Text="莫林" />
                         <TextBlock Margin="0,1.5,6,0" Text="Gun" />
                         <TextBlock Margin="0,1.5,6,0" Text="Gun" />
                         <TextBlock Margin="0,1.5,6,0" Text="唐匡" />
                         <TextBlock Margin="0,1.5,6,0" Text="唐匡" />
                         <TextBlock Margin="0,1.5,6,0" Text="腐蠱" />
                         <TextBlock Margin="0,1.5,6,0" Text="腐蠱" />
                         <TextBlock Margin="0,1.5,6,0" Text="别打" />
                         <TextBlock Margin="0,1.5,6,0" Text="别打" />
                         <TextBlock Margin="0,1.5,6,0" Text="):" />
                         <TextBlock Margin="0,1.5,6,0" Text="):" />
+                        <TextBlock Margin="0,1.5,6,0" Text="临野" />
+                        <TextBlock Margin="0,1.5,6,0" Text="韩枫" />
+                        <TextBlock Margin="0,1.5,6,0" Text="因心" />
+                        <TextBlock Margin="0,1.5,6,0" Text="清风" />
+                        <TextBlock Margin="0,1.5,6,0" Text="夏可" />
+                        <TextBlock Margin="0,1.5,6,0" Text="基本" />
+                        <TextBlock Margin="0,1.5,6,0" Text="杨间" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Slyo" />
+                        <TextBlock Margin="0,1.5,6,0" Text="黑白" />
+                        <TextBlock Margin="0,1.5,6,0" Text="白芷" />
+                        <TextBlock Margin="0,1.5,6,0" Text="汤圆" />
+                        <TextBlock Margin="0,1.5,6,0" Text="黑猫" />
+                        <TextBlock Margin="0,1.5,6,0" Text="柠檬" />
+                        <TextBlock Margin="0,1.5,6,0" Text="愚者" />
+                        <TextBlock Margin="0,1.5,6,0" Text="久蛊" />
+                        <TextBlock Margin="0,1.5,6,0" Text="穆凡" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Han" />
+                        <TextBlock Margin="0,1.5,6,0" Text="玖玖" />
+                        <TextBlock Margin="0,1.5,6,0" Text="梦峰" />
+                        <TextBlock Margin="0,1.5,6,0" Text="小饭" />
+                        <TextBlock Margin="0,1.5,6,0" Text="疾风" />
+                        <TextBlock Margin="0,1.5,6,0" Text="黑色" />
+                        <TextBlock Margin="0,1.5,6,0" Text="洛舞" />
+                        <TextBlock Margin="0,1.5,6,0" Text="医保" />
+                        <TextBlock Margin="0,1.5,6,0" Text="稚苕" />
+                        <TextBlock Margin="0,1.5,6,0" Text="白龙" />
+                        <TextBlock Margin="0,1.5,6,0" Text="影杀" />
+                        <TextBlock Margin="0,1.5,6,0" Text="YmY" />
                         <TextBlock Margin="0,1.5,6,0" Text="time" />
                         <TextBlock Margin="0,1.5,6,0" Text="time" />
                         <TextBlock Margin="0,1.5,6,0" Text="sadv" />
                         <TextBlock Margin="0,1.5,6,0" Text="sadv" />
                         <TextBlock Margin="0,1.5,6,0" Text="gate" />
                         <TextBlock Margin="0,1.5,6,0" Text="gate" />
@@ -231,6 +294,8 @@
                         <TextBlock Margin="0,1.5,6,0" Text="ZHH" />
                         <TextBlock Margin="0,1.5,6,0" Text="ZHH" />
                         <TextBlock Margin="0,1.5,6,0" Text="Ezzo" />
                         <TextBlock Margin="0,1.5,6,0" Text="Ezzo" />
                         <TextBlock Margin="0,1.5,6,0" Text="^O^" />
                         <TextBlock Margin="0,1.5,6,0" Text="^O^" />
+                        <TextBlock Margin="0,1.5,6,0" Text="gino" />
+                        <TextBlock Margin="0,1.5,6,0" Text="9787" />
                         <TextBlock Margin="0,1.5,6,0" Text="HJYz" />
                         <TextBlock Margin="0,1.5,6,0" Text="HJYz" />
                         <TextBlock Margin="0,1.5,6,0" Text="0216" />
                         <TextBlock Margin="0,1.5,6,0" Text="0216" />
                         <TextBlock Margin="0,1.5,6,0" Text="DOA" />
                         <TextBlock Margin="0,1.5,6,0" Text="DOA" />
@@ -240,32 +305,47 @@
                         <TextBlock Margin="0,1.5,6,0" Text="Gaze" />
                         <TextBlock Margin="0,1.5,6,0" Text="Gaze" />
                         <TextBlock Margin="0,1.5,6,0" Text="Cella" />
                         <TextBlock Margin="0,1.5,6,0" Text="Cella" />
                         <TextBlock Margin="0,1.5,6,0" Text="Keyn" />
                         <TextBlock Margin="0,1.5,6,0" Text="Keyn" />
+                        <TextBlock Margin="0,1.5,6,0" Text="AL76" />
                         <TextBlock Margin="0,1.5,6,0" Text="4Ghz" />
                         <TextBlock Margin="0,1.5,6,0" Text="4Ghz" />
                         <TextBlock Margin="0,1.5,6,0" Text="cong" />
                         <TextBlock Margin="0,1.5,6,0" Text="cong" />
+                        <TextBlock Margin="0,1.5,6,0" Text="。_。" />
                         <TextBlock Margin="0,1.5,6,0" Text="lucky" />
                         <TextBlock Margin="0,1.5,6,0" Text="lucky" />
                         <TextBlock Margin="0,1.5,6,0" Text="Gd64" />
                         <TextBlock Margin="0,1.5,6,0" Text="Gd64" />
+                        <TextBlock Margin="0,1.5,6,0" Text="空调c" />
                         <TextBlock Margin="0,1.5,6,0" Text="gugu" />
                         <TextBlock Margin="0,1.5,6,0" Text="gugu" />
                         <TextBlock Margin="0,1.5,6,0" Text="Brant" />
                         <TextBlock Margin="0,1.5,6,0" Text="Brant" />
+                        <TextBlock Margin="0,1.5,6,0" Text="⁣    ​" />
+                        <TextBlock Margin="0,1.5,6,0" Text="RKRK" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Tarka" />
                         <TextBlock Margin="0,1.5,6,0" Text="cHEn." />
                         <TextBlock Margin="0,1.5,6,0" Text="cHEn." />
+                        <TextBlock Margin="0,1.5,6,0" Text="Eiligo" />
                         <TextBlock Margin="0,1.5,6,0" Text="RANX" />
                         <TextBlock Margin="0,1.5,6,0" Text="RANX" />
+                        <TextBlock Margin="0,1.5,6,0" Text="hanm" />
+                        <TextBlock Margin="0,1.5,6,0" Text="54188" />
                         <TextBlock Margin="0,1.5,6,0" Text="&amp;#39;" />
                         <TextBlock Margin="0,1.5,6,0" Text="&amp;#39;" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Cx330" />
                         <TextBlock Margin="0,1.5,6,0" Text="ZDHK" />
                         <TextBlock Margin="0,1.5,6,0" Text="ZDHK" />
+                        <TextBlock Margin="0,1.5,6,0" Text="ToTxT" />
+                        <TextBlock Margin="0,1.5,6,0" Text="HzzzT" />
                         <TextBlock Margin="0,1.5,6,0" Text="ziqian" />
                         <TextBlock Margin="0,1.5,6,0" Text="ziqian" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Lloora" />
                         <TextBlock Margin="0,1.5,6,0" Text="调试器" />
                         <TextBlock Margin="0,1.5,6,0" Text="调试器" />
                         <TextBlock Margin="0,1.5,6,0" Text="xuque" />
                         <TextBlock Margin="0,1.5,6,0" Text="xuque" />
+                        <TextBlock Margin="0,1.5,6,0" Text="飞猪侠" />
                         <TextBlock Margin="0,1.5,6,0" Text="张卿予" />
                         <TextBlock Margin="0,1.5,6,0" Text="张卿予" />
                         <TextBlock Margin="0,1.5,6,0" Text="剑骨头" />
                         <TextBlock Margin="0,1.5,6,0" Text="剑骨头" />
                         <TextBlock Margin="0,1.5,6,0" Text="栀熙阳" />
                         <TextBlock Margin="0,1.5,6,0" Text="栀熙阳" />
                         <TextBlock Margin="0,1.5,6,0" Text="晚雾鹤" />
                         <TextBlock Margin="0,1.5,6,0" Text="晚雾鹤" />
+                        <TextBlock Margin="0,1.5,6,0" Text="祤之锌" />
                         <TextBlock Margin="0,1.5,6,0" Text="金苹果" />
                         <TextBlock Margin="0,1.5,6,0" Text="金苹果" />
                         <TextBlock Margin="0,1.5,6,0" Text="云野寂" />
                         <TextBlock Margin="0,1.5,6,0" Text="云野寂" />
                         <TextBlock Margin="0,1.5,6,0" Text="金木研" />
                         <TextBlock Margin="0,1.5,6,0" Text="金木研" />
-                        <TextBlock Margin="0,1.5,6,0" Text="韩雨宸" />
                         <TextBlock Margin="0,1.5,6,0" Text="Scoop" />
                         <TextBlock Margin="0,1.5,6,0" Text="Scoop" />
                         <TextBlock Margin="0,1.5,6,0" Text="绘梨衣" />
                         <TextBlock Margin="0,1.5,6,0" Text="绘梨衣" />
                         <TextBlock Margin="0,1.5,6,0" Text="夜小辞" />
                         <TextBlock Margin="0,1.5,6,0" Text="夜小辞" />
                         <TextBlock Margin="0,1.5,6,0" Text="明月奴" />
                         <TextBlock Margin="0,1.5,6,0" Text="明月奴" />
                         <TextBlock Margin="0,1.5,6,0" Text="光和热" />
                         <TextBlock Margin="0,1.5,6,0" Text="光和热" />
+                        <TextBlock Margin="0,1.5,6,0" Text="柳贯一" />
                         <TextBlock Margin="0,1.5,6,0" Text="湫某辞" />
                         <TextBlock Margin="0,1.5,6,0" Text="湫某辞" />
                         <TextBlock Margin="0,1.5,6,0" Text="大哈操" />
                         <TextBlock Margin="0,1.5,6,0" Text="大哈操" />
                         <TextBlock Margin="0,1.5,6,0" Text="顾東凡" />
                         <TextBlock Margin="0,1.5,6,0" Text="顾東凡" />
@@ -274,6 +354,7 @@
                         <TextBlock Margin="0,1.5,6,0" Text="爱钱钱" />
                         <TextBlock Margin="0,1.5,6,0" Text="爱钱钱" />
                         <TextBlock Margin="0,1.5,6,0" Text="oaken" />
                         <TextBlock Margin="0,1.5,6,0" Text="oaken" />
                         <TextBlock Margin="0,1.5,6,0" Text="小小球" />
                         <TextBlock Margin="0,1.5,6,0" Text="小小球" />
+                        <TextBlock Margin="0,1.5,6,0" Text="徐炳文" />
                         <TextBlock Margin="0,1.5,6,0" Text="天羽煜" />
                         <TextBlock Margin="0,1.5,6,0" Text="天羽煜" />
                         <TextBlock Margin="0,1.5,6,0" Text="小牛马" />
                         <TextBlock Margin="0,1.5,6,0" Text="小牛马" />
                         <TextBlock Margin="0,1.5,6,0" Text="折尾猫" />
                         <TextBlock Margin="0,1.5,6,0" Text="折尾猫" />
@@ -304,21 +385,56 @@
                         <TextBlock Margin="0,1.5,6,0" Text="八云辰" />
                         <TextBlock Margin="0,1.5,6,0" Text="八云辰" />
                         <TextBlock Margin="0,1.5,6,0" Text="祝卿安" />
                         <TextBlock Margin="0,1.5,6,0" Text="祝卿安" />
                         <TextBlock Margin="0,1.5,6,0" Text="北兮乄" />
                         <TextBlock Margin="0,1.5,6,0" Text="北兮乄" />
+                        <TextBlock Margin="0,1.5,6,0" Text="玉璇玑" />
+                        <TextBlock Margin="0,1.5,6,0" Text="雾雨唯" />
+                        <TextBlock Margin="0,1.5,6,0" Text="大祷师" />
+                        <TextBlock Margin="0,1.5,6,0" Text="大金刚" />
+                        <TextBlock Margin="0,1.5,6,0" Text="胡辣汤" />
+                        <TextBlock Margin="0,1.5,6,0" Text="明日江" />
+                        <TextBlock Margin="0,1.5,6,0" Text="喵千绘" />
+                        <TextBlock Margin="0,1.5,6,0" Text="樗磐呐" />
+                        <TextBlock Margin="0,1.5,6,0" Text="九百百" />
+                        <TextBlock Margin="0,1.5,6,0" Text="真树君" />
+                        <TextBlock Margin="0,1.5,6,0" Text="的角色" />
+                        <TextBlock Margin="0,1.5,6,0" Text="蒋浩然" />
+                        <TextBlock Margin="0,1.5,6,0" Text="问号君" />
+                        <TextBlock Margin="0,1.5,6,0" Text="虚无丶" />
+                        <TextBlock Margin="0,1.5,6,0" Text="鹿泽轩" />
+                        <TextBlock Margin="0,1.5,6,0" Text="西道我" />
+                        <TextBlock Margin="0,1.5,6,0" Text="蛏子酱" />
+                        <TextBlock Margin="0,1.5,6,0" Text="风渐渐" />
+                        <TextBlock Margin="0,1.5,6,0" Text="小黄黄" />
+                        <TextBlock Margin="0,1.5,6,0" Text="锟斤拷" />
+                        <TextBlock Margin="0,1.5,6,0" Text="小竹叶" />
+                        <TextBlock Margin="0,1.5,6,0" Text="九十里" />
+                        <TextBlock Margin="0,1.5,6,0" Text="南华真" />
+                        <TextBlock Margin="0,1.5,6,0" Text="哒哒哒" />
+                        <TextBlock Margin="0,1.5,6,0" Text="咕咕咕" />
+                        <TextBlock Margin="0,1.5,6,0" Text="TROLL" />
                         <TextBlock Margin="0,1.5,6,0" Text="yoyal8" />
                         <TextBlock Margin="0,1.5,6,0" Text="yoyal8" />
+                        <TextBlock Margin="0,1.5,6,0" Text="ITMTX" />
                         <TextBlock Margin="0,1.5,6,0" Text="Zh11h" />
                         <TextBlock Margin="0,1.5,6,0" Text="Zh11h" />
+                        <TextBlock Margin="0,1.5,6,0" Text="SEKGY" />
+                        <TextBlock Margin="0,1.5,6,0" Text="111sui" />
                         <TextBlock Margin="0,1.5,6,0" Text="aKALA" />
                         <TextBlock Margin="0,1.5,6,0" Text="aKALA" />
                         <TextBlock Margin="0,1.5,6,0" Text="gyj120" />
                         <TextBlock Margin="0,1.5,6,0" Text="gyj120" />
                         <TextBlock Margin="0,1.5,6,0" Text="Young" />
                         <TextBlock Margin="0,1.5,6,0" Text="Young" />
                         <TextBlock Margin="0,1.5,6,0" Text="凌·秋烨" />
                         <TextBlock Margin="0,1.5,6,0" Text="凌·秋烨" />
                         <TextBlock Margin="0,1.5,6,0" Text="wujian" />
                         <TextBlock Margin="0,1.5,6,0" Text="wujian" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Dshiya" />
+                        <TextBlock Margin="0,1.5,6,0" Text="G-next" />
                         <TextBlock Margin="0,1.5,6,0" Text="N0rdiK" />
                         <TextBlock Margin="0,1.5,6,0" Text="N0rdiK" />
                         <TextBlock Margin="0,1.5,6,0" Text="Sphinx" />
                         <TextBlock Margin="0,1.5,6,0" Text="Sphinx" />
                         <TextBlock Margin="0,1.5,6,0" Text="kimma" />
                         <TextBlock Margin="0,1.5,6,0" Text="kimma" />
                         <TextBlock Margin="0,1.5,6,0" Text="Jupiter" />
                         <TextBlock Margin="0,1.5,6,0" Text="Jupiter" />
+                        <TextBlock Margin="0,1.5,6,0" Text="TINTIN" />
                         <TextBlock Margin="0,1.5,6,0" Text="Corpus" />
                         <TextBlock Margin="0,1.5,6,0" Text="Corpus" />
                         <TextBlock Margin="0,1.5,6,0" Text="Thgink" />
                         <TextBlock Margin="0,1.5,6,0" Text="Thgink" />
                         <TextBlock Margin="0,1.5,6,0" Text="forever" />
                         <TextBlock Margin="0,1.5,6,0" Text="forever" />
                         <TextBlock Margin="0,1.5,6,0" Text="一哥3.0" />
                         <TextBlock Margin="0,1.5,6,0" Text="一哥3.0" />
+                        <TextBlock Margin="0,1.5,6,0" Text="PanBIS" />
+                        <TextBlock Margin="0,1.5,6,0" Text="e1apse" />
+                        <TextBlock Margin="0,1.5,6,0" Text="cmly03" />
                         <TextBlock Margin="0,1.5,6,0" Text="114514" />
                         <TextBlock Margin="0,1.5,6,0" Text="114514" />
                         <TextBlock Margin="0,1.5,6,0" Text="YEYEYE" />
                         <TextBlock Margin="0,1.5,6,0" Text="YEYEYE" />
                         <TextBlock Margin="0,1.5,6,0" Text="Phuket" />
                         <TextBlock Margin="0,1.5,6,0" Text="Phuket" />
@@ -329,16 +445,27 @@
                         <TextBlock Margin="0,1.5,6,0" Text="sabrina" />
                         <TextBlock Margin="0,1.5,6,0" Text="sabrina" />
                         <TextBlock Margin="0,1.5,6,0" Text="XIruotE" />
                         <TextBlock Margin="0,1.5,6,0" Text="XIruotE" />
                         <TextBlock Margin="0,1.5,6,0" Text="Dr.Ayin" />
                         <TextBlock Margin="0,1.5,6,0" Text="Dr.Ayin" />
+                        <TextBlock Margin="0,1.5,6,0" Text="onlong" />
+                        <TextBlock Margin="0,1.5,6,0" Text="zuozhe" />
                         <TextBlock Margin="0,1.5,6,0" Text="ling141" />
                         <TextBlock Margin="0,1.5,6,0" Text="ling141" />
+                        <TextBlock Margin="0,1.5,6,0" Text="zxxxfan" />
+                        <TextBlock Margin="0,1.5,6,0" Text="MY17IT" />
+                        <TextBlock Margin="0,1.5,6,0" Text="張 懙 李" />
+                        <TextBlock Margin="0,1.5,6,0" Text="TNT285" />
                         <TextBlock Margin="0,1.5,6,0" Text="Decade" />
                         <TextBlock Margin="0,1.5,6,0" Text="Decade" />
                         <TextBlock Margin="0,1.5,6,0" Text="laobide" />
                         <TextBlock Margin="0,1.5,6,0" Text="laobide" />
                         <TextBlock Margin="0,1.5,6,0" Text="ccb大神" />
                         <TextBlock Margin="0,1.5,6,0" Text="ccb大神" />
                         <TextBlock Margin="0,1.5,6,0" Text="plastick" />
                         <TextBlock Margin="0,1.5,6,0" Text="plastick" />
                         <TextBlock Margin="0,1.5,6,0" Text="loouse_" />
                         <TextBlock Margin="0,1.5,6,0" Text="loouse_" />
                         <TextBlock Margin="0,1.5,6,0" Text="饺子MC" />
                         <TextBlock Margin="0,1.5,6,0" Text="饺子MC" />
+                        <TextBlock Margin="0,1.5,6,0" Text="杰 NI 龟" />
                         <TextBlock Margin="0,1.5,6,0" Text="kill1122" />
                         <TextBlock Margin="0,1.5,6,0" Text="kill1122" />
+                        <TextBlock Margin="0,1.5,6,0" Text="super9y" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Foxman" />
                         <TextBlock Margin="0,1.5,6,0" Text="猫yi巴草" />
                         <TextBlock Margin="0,1.5,6,0" Text="猫yi巴草" />
                         <TextBlock Margin="0,1.5,6,0" Text="goblind" />
                         <TextBlock Margin="0,1.5,6,0" Text="goblind" />
+                        <TextBlock Margin="0,1.5,6,0" Text="NeroicX" />
+                        <TextBlock Margin="0,1.5,6,0" Text="章鱼B26" />
                         <TextBlock Margin="0,1.5,6,0" Text="秦爅QIN" />
                         <TextBlock Margin="0,1.5,6,0" Text="秦爅QIN" />
                         <TextBlock Margin="0,1.5,6,0" Text="weather" />
                         <TextBlock Margin="0,1.5,6,0" Text="weather" />
                         <TextBlock Margin="0,1.5,6,0" Text="qwptpw" />
                         <TextBlock Margin="0,1.5,6,0" Text="qwptpw" />
@@ -348,10 +475,15 @@
                         <TextBlock Margin="0,1.5,6,0" Text="筱星辰⭐" />
                         <TextBlock Margin="0,1.5,6,0" Text="筱星辰⭐" />
                         <TextBlock Margin="0,1.5,6,0" Text="Starying" />
                         <TextBlock Margin="0,1.5,6,0" Text="Starying" />
                         <TextBlock Margin="0,1.5,6,0" Text="wwwww" />
                         <TextBlock Margin="0,1.5,6,0" Text="wwwww" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Buyihan" />
                         <TextBlock Margin="0,1.5,6,0" Text="疾驰火车" />
                         <TextBlock Margin="0,1.5,6,0" Text="疾驰火车" />
                         <TextBlock Margin="0,1.5,6,0" Text="鸿星天帝" />
                         <TextBlock Margin="0,1.5,6,0" Text="鸿星天帝" />
+                        <TextBlock Margin="0,1.5,6,0" Text="跳水的鱼" />
+                        <TextBlock Margin="0,1.5,6,0" Text="咸鱼雷音" />
                         <TextBlock Margin="0,1.5,6,0" Text="云映鹤影" />
                         <TextBlock Margin="0,1.5,6,0" Text="云映鹤影" />
                         <TextBlock Margin="0,1.5,6,0" Text="金禾向暖" />
                         <TextBlock Margin="0,1.5,6,0" Text="金禾向暖" />
+                        <TextBlock Margin="0,1.5,6,0" Text="气息荒古" />
+                        <TextBlock Margin="0,1.5,6,0" Text="霜木翔凤" />
                         <TextBlock Margin="0,1.5,6,0" Text="只见伊人" />
                         <TextBlock Margin="0,1.5,6,0" Text="只见伊人" />
                         <TextBlock Margin="0,1.5,6,0" Text="苏家阿辰" />
                         <TextBlock Margin="0,1.5,6,0" Text="苏家阿辰" />
                         <TextBlock Margin="0,1.5,6,0" Text="未知用户" />
                         <TextBlock Margin="0,1.5,6,0" Text="未知用户" />
@@ -360,9 +492,12 @@
                         <TextBlock Margin="0,1.5,6,0" Text="英白拉多" />
                         <TextBlock Margin="0,1.5,6,0" Text="英白拉多" />
                         <TextBlock Margin="0,1.5,6,0" Text="红袖添香" />
                         <TextBlock Margin="0,1.5,6,0" Text="红袖添香" />
                         <TextBlock Margin="0,1.5,6,0" Text="氮气炸锅" />
                         <TextBlock Margin="0,1.5,6,0" Text="氮气炸锅" />
+                        <TextBlock Margin="0,1.5,6,0" Text="爱源神!" />
                         <TextBlock Margin="0,1.5,6,0" Text="小小小马" />
                         <TextBlock Margin="0,1.5,6,0" Text="小小小马" />
                         <TextBlock Margin="0,1.5,6,0" Text="未梦领来" />
                         <TextBlock Margin="0,1.5,6,0" Text="未梦领来" />
+                        <TextBlock Margin="0,1.5,6,0" Text="鸡鸡鸡鸡" />
                         <TextBlock Margin="0,1.5,6,0" Text="村里村夫" />
                         <TextBlock Margin="0,1.5,6,0" Text="村里村夫" />
+                        <TextBlock Margin="0,1.5,6,0" Text="阿元颜月" />
                         <TextBlock Margin="0,1.5,6,0" Text="竹林提酒" />
                         <TextBlock Margin="0,1.5,6,0" Text="竹林提酒" />
                         <TextBlock Margin="0,1.5,6,0" Text="黎月红夕" />
                         <TextBlock Margin="0,1.5,6,0" Text="黎月红夕" />
                         <TextBlock Margin="0,1.5,6,0" Text="天下无双" />
                         <TextBlock Margin="0,1.5,6,0" Text="天下无双" />
@@ -404,16 +539,57 @@
                         <TextBlock Margin="0,1.5,6,0" Text="凌瑶雪莹" />
                         <TextBlock Margin="0,1.5,6,0" Text="凌瑶雪莹" />
                         <TextBlock Margin="0,1.5,6,0" Text="wswsing" />
                         <TextBlock Margin="0,1.5,6,0" Text="wswsing" />
                         <TextBlock Margin="0,1.5,6,0" Text="漆黑噤默" />
                         <TextBlock Margin="0,1.5,6,0" Text="漆黑噤默" />
+                        <TextBlock Margin="0,1.5,6,0" Text="云水莫伊" />
+                        <TextBlock Margin="0,1.5,6,0" Text="孤星隐耀" />
+                        <TextBlock Margin="0,1.5,6,0" Text="蓝莓蛋糕" />
+                        <TextBlock Margin="0,1.5,6,0" Text="澹台问月" />
+                        <TextBlock Margin="0,1.5,6,0" Text="青山绿衣" />
+                        <TextBlock Margin="0,1.5,6,0" Text="墨迹道术" />
+                        <TextBlock Margin="0,1.5,6,0" Text="落难星言" />
+                        <TextBlock Margin="0,1.5,6,0" Text="莉库莉可" />
+                        <TextBlock Margin="0,1.5,6,0" Text="你好残忍" />
+                        <TextBlock Margin="0,1.5,6,0" Text="胖胖的我" />
+                        <TextBlock Margin="0,1.5,6,0" Text="余秋叶繁" />
+                        <TextBlock Margin="0,1.5,6,0" Text="再见夏霞" />
+                        <TextBlock Margin="0,1.5,6,0" Text="椎名立希" />
+                        <TextBlock Margin="0,1.5,6,0" Text="倾城菜鸡" />
+                        <TextBlock Margin="0,1.5,6,0" Text="言语不行" />
+                        <TextBlock Margin="0,1.5,6,0" Text="白~曦~" />
+                        <TextBlock Margin="0,1.5,6,0" Text="推杯换盏" />
+                        <TextBlock Margin="0,1.5,6,0" Text="漫芜释雪" />
+                        <TextBlock Margin="0,1.5,6,0" Text="幻梦幻蝶" />
+                        <TextBlock Margin="0,1.5,6,0" Text="小鸟游曈" />
+                        <TextBlock Margin="0,1.5,6,0" Text="豆腐与葱" />
+                        <TextBlock Margin="0,1.5,6,0" Text="剑客白洋" />
+                        <TextBlock Margin="0,1.5,6,0" Text="艾露迪克" />
+                        <TextBlock Margin="0,1.5,6,0" Text="空井里蛙" />
+                        <TextBlock Margin="0,1.5,6,0" Text="空乐白夜" />
+                        <TextBlock Margin="0,1.5,6,0" Text="白色按钮" />
+                        <TextBlock Margin="0,1.5,6,0" Text="muming" />
+                        <TextBlock Margin="0,1.5,6,0" Text="纸币ORZ" />
+                        <TextBlock Margin="0,1.5,6,0" Text="西蛮qwq" />
+                        <TextBlock Margin="0,1.5,6,0" Text="nyanmjc" />
+                        <TextBlock Margin="0,1.5,6,0" Text="houteng" />
                         <TextBlock Margin="0,1.5,6,0" Text="無WuAD" />
                         <TextBlock Margin="0,1.5,6,0" Text="無WuAD" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Lioncute" />
+                        <TextBlock Margin="0,1.5,6,0" Text="情殇·瑕疵" />
                         <TextBlock Margin="0,1.5,6,0" Text="CityHaze" />
                         <TextBlock Margin="0,1.5,6,0" Text="CityHaze" />
                         <TextBlock Margin="0,1.5,6,0" Text="wdy9527" />
                         <TextBlock Margin="0,1.5,6,0" Text="wdy9527" />
+                        <TextBlock Margin="0,1.5,6,0" Text="N3W04H" />
+                        <TextBlock Margin="0,1.5,6,0" Text="久遠 蓮華" />
                         <TextBlock Margin="0,1.5,6,0" Text="777seese" />
                         <TextBlock Margin="0,1.5,6,0" Text="777seese" />
                         <TextBlock Margin="0,1.5,6,0" Text=" 脖子右拧" />
                         <TextBlock Margin="0,1.5,6,0" Text=" 脖子右拧" />
                         <TextBlock Margin="0,1.5,6,0" Text="NicoACG" />
                         <TextBlock Margin="0,1.5,6,0" Text="NicoACG" />
                         <TextBlock Margin="0,1.5,6,0" Text="许和0331" />
                         <TextBlock Margin="0,1.5,6,0" Text="许和0331" />
+                        <TextBlock Margin="0,1.5,6,0" Text="HHHbbb" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Whelone" />
                         <TextBlock Margin="0,1.5,6,0" Text="Vergilius." />
                         <TextBlock Margin="0,1.5,6,0" Text="Vergilius." />
+                        <TextBlock Margin="0,1.5,6,0" Text="筱墨OwO" />
+                        <TextBlock Margin="0,1.5,6,0" Text="虫虫OwO" />
                         <TextBlock Margin="0,1.5,6,0" Text="3355411²" />
                         <TextBlock Margin="0,1.5,6,0" Text="3355411²" />
                         <TextBlock Margin="0,1.5,6,0" Text="Sevenfan" />
                         <TextBlock Margin="0,1.5,6,0" Text="Sevenfan" />
+                        <TextBlock Margin="0,1.5,6,0" Text="PierceShi" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Zikyeung" />
                         <TextBlock Margin="0,1.5,6,0" Text="小沉Chen" />
                         <TextBlock Margin="0,1.5,6,0" Text="小沉Chen" />
                         <TextBlock Margin="0,1.5,6,0" Text="QILUOVO" />
                         <TextBlock Margin="0,1.5,6,0" Text="QILUOVO" />
                         <TextBlock Margin="0,1.5,6,0" Text="有鱼sama" />
                         <TextBlock Margin="0,1.5,6,0" Text="有鱼sama" />
@@ -421,13 +597,24 @@
                         <TextBlock Margin="0,1.5,6,0" Text="想rua狐狸" />
                         <TextBlock Margin="0,1.5,6,0" Text="想rua狐狸" />
                         <TextBlock Margin="0,1.5,6,0" Text="AmorSick" />
                         <TextBlock Margin="0,1.5,6,0" Text="AmorSick" />
                         <TextBlock Margin="0,1.5,6,0" Text="添加剂ing" />
                         <TextBlock Margin="0,1.5,6,0" Text="添加剂ing" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Luangum" />
+                        <TextBlock Margin="0,1.5,6,0" Text="CHLSama" />
                         <TextBlock Margin="0,1.5,6,0" Text="666abcba" />
                         <TextBlock Margin="0,1.5,6,0" Text="666abcba" />
                         <TextBlock Margin="0,1.5,6,0" Text="Sariel小懒" />
                         <TextBlock Margin="0,1.5,6,0" Text="Sariel小懒" />
+                        <TextBlock Margin="0,1.5,6,0" Text="wanglizhi" />
+                        <TextBlock Margin="0,1.5,6,0" Text="chen0701" />
                         <TextBlock Margin="0,1.5,6,0" Text="androidhi" />
                         <TextBlock Margin="0,1.5,6,0" Text="androidhi" />
                         <TextBlock Margin="0,1.5,6,0" Text="gggg1234" />
                         <TextBlock Margin="0,1.5,6,0" Text="gggg1234" />
+                        <TextBlock Margin="0,1.5,6,0" Text="bentianjia" />
                         <TextBlock Margin="0,1.5,6,0" Text="shiroNeko" />
                         <TextBlock Margin="0,1.5,6,0" Text="shiroNeko" />
+                        <TextBlock Margin="0,1.5,6,0" Text="IIINNNeilII" />
                         <TextBlock Margin="0,1.5,6,0" Text="爱游戏的狼" />
                         <TextBlock Margin="0,1.5,6,0" Text="爱游戏的狼" />
+                        <TextBlock Margin="0,1.5,6,0" Text="孤影赴星河" />
+                        <TextBlock Margin="0,1.5,6,0" Text="清闲挺自在" />
                         <TextBlock Margin="0,1.5,6,0" Text="欢呼与果糖" />
                         <TextBlock Margin="0,1.5,6,0" Text="欢呼与果糖" />
+                        <TextBlock Margin="0,1.5,6,0" Text="飞天大锅饭" />
+                        <TextBlock Margin="0,1.5,6,0" Text="烟熏秋刀鱼" />
+                        <TextBlock Margin="0,1.5,6,0" Text="人海不可爱" />
                         <TextBlock Margin="0,1.5,6,0" Text="希希爱睡觉" />
                         <TextBlock Margin="0,1.5,6,0" Text="希希爱睡觉" />
                         <TextBlock Margin="0,1.5,6,0" Text="六面三倍速" />
                         <TextBlock Margin="0,1.5,6,0" Text="六面三倍速" />
                         <TextBlock Margin="0,1.5,6,0" Text="orangexixi" />
                         <TextBlock Margin="0,1.5,6,0" Text="orangexixi" />
@@ -444,30 +631,63 @@
                         <TextBlock Margin="0,1.5,6,0" Text="鱼儿爱上猫" />
                         <TextBlock Margin="0,1.5,6,0" Text="鱼儿爱上猫" />
                         <TextBlock Margin="0,1.5,6,0" Text="Anhydride" />
                         <TextBlock Margin="0,1.5,6,0" Text="Anhydride" />
                         <TextBlock Margin="0,1.5,6,0" Text="帕秋莉~go" />
                         <TextBlock Margin="0,1.5,6,0" Text="帕秋莉~go" />
+                        <TextBlock Margin="0,1.5,6,0" Text="废材狐阿桔" />
                         <TextBlock Margin="0,1.5,6,0" Text="玉玉小南娘" />
                         <TextBlock Margin="0,1.5,6,0" Text="玉玉小南娘" />
                         <TextBlock Margin="0,1.5,6,0" Text="欣喜的掌声" />
                         <TextBlock Margin="0,1.5,6,0" Text="欣喜的掌声" />
                         <TextBlock Margin="0,1.5,6,0" Text="小云爱中国" />
                         <TextBlock Margin="0,1.5,6,0" Text="小云爱中国" />
                         <TextBlock Margin="0,1.5,6,0" Text="内存漫游者" />
                         <TextBlock Margin="0,1.5,6,0" Text="内存漫游者" />
                         <TextBlock Margin="0,1.5,6,0" Text="压力暴大了" />
                         <TextBlock Margin="0,1.5,6,0" Text="压力暴大了" />
+                        <TextBlock Margin="0,1.5,6,0" Text="摆地摊的王" />
                         <TextBlock Margin="0,1.5,6,0" Text="鲜花女男爵" />
                         <TextBlock Margin="0,1.5,6,0" Text="鲜花女男爵" />
                         <TextBlock Margin="0,1.5,6,0" Text="团结起来!" />
                         <TextBlock Margin="0,1.5,6,0" Text="团结起来!" />
                         <TextBlock Margin="0,1.5,6,0" Text="BeiWu_CN" />
                         <TextBlock Margin="0,1.5,6,0" Text="BeiWu_CN" />
+                        <TextBlock Margin="0,1.5,6,0" Text="渔船制造师" />
+                        <TextBlock Margin="0,1.5,6,0" Text="直型冷凝管" />
+                        <TextBlock Margin="0,1.5,6,0" Text="森风白明梦" />
+                        <TextBlock Margin="0,1.5,6,0" Text="只是个摆设" />
+                        <TextBlock Margin="0,1.5,6,0" Text="清蒸小黄鱼" />
+                        <TextBlock Margin="0,1.5,6,0" Text="徐州老味菜" />
+                        <TextBlock Margin="0,1.5,6,0" Text="胡哲爱蒙多" />
+                        <TextBlock Margin="0,1.5,6,0" Text="黄瓜炒青菜" />
+                        <TextBlock Margin="0,1.5,6,0" Text="阴阳割坤肖" />
+                        <TextBlock Margin="0,1.5,6,0" Text="无厘的抽象" />
+                        <TextBlock Margin="0,1.5,6,0" Text="洒水车之歌" />
+                        <TextBlock Margin="0,1.5,6,0" Text="已被静心了" />
+                        <TextBlock Margin="0,1.5,6,0" Text="茉莉千百羽" />
                         <TextBlock Margin="0,1.5,6,0" Text="289775340" />
                         <TextBlock Margin="0,1.5,6,0" Text="289775340" />
                         <TextBlock Margin="0,1.5,6,0" Text="baicai1339" />
                         <TextBlock Margin="0,1.5,6,0" Text="baicai1339" />
+                        <TextBlock Margin="0,1.5,6,0" Text="W.D.gaster" />
                         <TextBlock Margin="0,1.5,6,0" Text="星野源.小毅" />
                         <TextBlock Margin="0,1.5,6,0" Text="星野源.小毅" />
                         <TextBlock Margin="0,1.5,6,0" Text="mreaaaaay" />
                         <TextBlock Margin="0,1.5,6,0" Text="mreaaaaay" />
                         <TextBlock Margin="0,1.5,6,0" Text="Swampday" />
                         <TextBlock Margin="0,1.5,6,0" Text="Swampday" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Kimonomc" />
+                        <TextBlock Margin="0,1.5,6,0" Text="翎安Lingan" />
                         <TextBlock Margin="0,1.5,6,0" Text="Kizuner630" />
                         <TextBlock Margin="0,1.5,6,0" Text="Kizuner630" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Stevens520" />
+                        <TextBlock Margin="0,1.5,6,0" Text="123456。?" />
                         <TextBlock Margin="0,1.5,6,0" Text="loading182" />
                         <TextBlock Margin="0,1.5,6,0" Text="loading182" />
                         <TextBlock Margin="0,1.5,6,0" Text="hhhjtqdydz" />
                         <TextBlock Margin="0,1.5,6,0" Text="hhhjtqdydz" />
+                        <TextBlock Margin="0,1.5,6,0" Text="LMY666888" />
                         <TextBlock Margin="0,1.5,6,0" Text="DeathHunk" />
                         <TextBlock Margin="0,1.5,6,0" Text="DeathHunk" />
+                        <TextBlock Margin="0,1.5,6,0" Text="sheng_1062" />
                         <TextBlock Margin="0,1.5,6,0" Text="Frankie_233" />
                         <TextBlock Margin="0,1.5,6,0" Text="Frankie_233" />
                         <TextBlock Margin="0,1.5,6,0" Text="izmash0449" />
                         <TextBlock Margin="0,1.5,6,0" Text="izmash0449" />
                         <TextBlock Margin="0,1.5,6,0" Text="Hashtag233" />
                         <TextBlock Margin="0,1.5,6,0" Text="Hashtag233" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Chucky_Luo" />
+                        <TextBlock Margin="0,1.5,6,0" Text="JUST LIKE IT" />
+                        <TextBlock Margin="0,1.5,6,0" Text="      极光明落" />
+                        <TextBlock Margin="0,1.5,6,0" Text="一叶,知秋~" />
+                        <TextBlock Margin="0,1.5,6,0" Text="forever_晨风" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Fakecaptain" />
+                        <TextBlock Margin="0,1.5,6,0" Text="C.肥宅快乐水" />
                         <TextBlock Margin="0,1.5,6,0" Text="QingYiLiang" />
                         <TextBlock Margin="0,1.5,6,0" Text="QingYiLiang" />
                         <TextBlock Margin="0,1.5,6,0" Text="arcueid1023" />
                         <TextBlock Margin="0,1.5,6,0" Text="arcueid1023" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Mr. Narrator" />
                         <TextBlock Margin="0,1.5,6,0" Text="while(true){}" />
                         <TextBlock Margin="0,1.5,6,0" Text="while(true){}" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Haobot2026" />
+                        <TextBlock Margin="0,1.5,6,0" Text="粉色妖精小姐" />
                         <TextBlock Margin="0,1.5,6,0" Text="十六枚铜币丶" />
                         <TextBlock Margin="0,1.5,6,0" Text="十六枚铜币丶" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Romarunner" />
                         <TextBlock Margin="0,1.5,6,0" Text="凯文卡斯兰娜" />
                         <TextBlock Margin="0,1.5,6,0" Text="凯文卡斯兰娜" />
                         <TextBlock Margin="0,1.5,6,0" Text="殷殷殷!!!" />
                         <TextBlock Margin="0,1.5,6,0" Text="殷殷殷!!!" />
                         <TextBlock Margin="0,1.5,6,0" Text="打泡泡SAMA" />
                         <TextBlock Margin="0,1.5,6,0" Text="打泡泡SAMA" />
@@ -477,16 +697,30 @@
                         <TextBlock Margin="0,1.5,6,0" Text="下水道的鼠鼠" />
                         <TextBlock Margin="0,1.5,6,0" Text="下水道的鼠鼠" />
                         <TextBlock Margin="0,1.5,6,0" Text="萨卡兹大蟑螂" />
                         <TextBlock Margin="0,1.5,6,0" Text="萨卡兹大蟑螂" />
                         <TextBlock Margin="0,1.5,6,0" Text="吃麦片的老鼠" />
                         <TextBlock Margin="0,1.5,6,0" Text="吃麦片的老鼠" />
+                        <TextBlock Margin="0,1.5,6,0" Text="双狼天下第一" />
+                        <TextBlock Margin="0,1.5,6,0" Text="丿地狱天使丿" />
+                        <TextBlock Margin="0,1.5,6,0" Text="回忆随缘而起" />
+                        <TextBlock Margin="0,1.5,6,0" Text="杏花村乌龙茶" />
+                        <TextBlock Margin="0,1.5,6,0" Text="想不到丶名字" />
+                        <TextBlock Margin="0,1.5,6,0" Text="叙事的第一周" />
+                        <TextBlock Margin="0,1.5,6,0" Text="这位是非人类" />
                         <TextBlock Margin="0,1.5,6,0" Text="小石潭记qwq" />
                         <TextBlock Margin="0,1.5,6,0" Text="小石潭记qwq" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Sylph_茜尔芙" />
+                        <TextBlock Margin="0,1.5,6,0" Text="dadadadada" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Awolfofcolor" />
                         <TextBlock Margin="0,1.5,6,0" Text="Omori_sunny" />
                         <TextBlock Margin="0,1.5,6,0" Text="Omori_sunny" />
+                        <TextBlock Margin="0,1.5,6,0" Text="请不要打luosi" />
                         <TextBlock Margin="0,1.5,6,0" Text="qingtian4135" />
                         <TextBlock Margin="0,1.5,6,0" Text="qingtian4135" />
                         <TextBlock Margin="0,1.5,6,0" Text="xiaolong_492" />
                         <TextBlock Margin="0,1.5,6,0" Text="xiaolong_492" />
                         <TextBlock Margin="0,1.5,6,0" Text="guduzihaoge" />
                         <TextBlock Margin="0,1.5,6,0" Text="guduzihaoge" />
                         <TextBlock Margin="0,1.5,6,0" Text="Rinshonagon" />
                         <TextBlock Margin="0,1.5,6,0" Text="Rinshonagon" />
                         <TextBlock Margin="0,1.5,6,0" Text="Tincyclopedia" />
                         <TextBlock Margin="0,1.5,6,0" Text="Tincyclopedia" />
                         <TextBlock Margin="0,1.5,6,0" Text="minecraftac11" />
                         <TextBlock Margin="0,1.5,6,0" Text="minecraftac11" />
+                        <TextBlock Margin="0,1.5,6,0" Text="123456789000" />
                         <TextBlock Margin="0,1.5,6,0" Text="zishuijing2021" />
                         <TextBlock Margin="0,1.5,6,0" Text="zishuijing2021" />
                         <TextBlock Margin="0,1.5,6,0" Text="qq1967131327" />
                         <TextBlock Margin="0,1.5,6,0" Text="qq1967131327" />
+                        <TextBlock Margin="0,1.5,6,0" Text="智慧树下铭记我" />
+                        <TextBlock Margin="0,1.5,6,0" Text="无垠回荡克雷松" />
                         <TextBlock Margin="0,1.5,6,0" Text="泪水打湿泪水湿" />
                         <TextBlock Margin="0,1.5,6,0" Text="泪水打湿泪水湿" />
                         <TextBlock Margin="0,1.5,6,0" Text="みやぐにあかり" />
                         <TextBlock Margin="0,1.5,6,0" Text="みやぐにあかり" />
                         <TextBlock Margin="0,1.5,6,0" Text="苇名剑圣有地绫" />
                         <TextBlock Margin="0,1.5,6,0" Text="苇名剑圣有地绫" />
@@ -494,21 +728,38 @@
                         <TextBlock Margin="0,1.5,6,0" Text="逐梦青年当自强" />
                         <TextBlock Margin="0,1.5,6,0" Text="逐梦青年当自强" />
                         <TextBlock Margin="0,1.5,6,0" Text="哈基米南北路多" />
                         <TextBlock Margin="0,1.5,6,0" Text="哈基米南北路多" />
                         <TextBlock Margin="0,1.5,6,0" Text="休伯利安养老院" />
                         <TextBlock Margin="0,1.5,6,0" Text="休伯利安养老院" />
+                        <TextBlock Margin="0,1.5,6,0" Text="名为咸鱼的渣子" />
+                        <TextBlock Margin="0,1.5,6,0" Text="零落不知猫寻海" />
+                        <TextBlock Margin="0,1.5,6,0" Text="大脑斧和哒老虎" />
+                        <TextBlock Margin="0,1.5,6,0" Text="maboyuaneasy" />
                         <TextBlock Margin="0,1.5,6,0" Text="照井黎Terui_Rei" />
                         <TextBlock Margin="0,1.5,6,0" Text="照井黎Terui_Rei" />
                         <TextBlock Margin="0,1.5,6,0" Text="约翰·冯·海格力斯" />
                         <TextBlock Margin="0,1.5,6,0" Text="约翰·冯·海格力斯" />
                         <TextBlock Margin="0,1.5,6,0" Text="南ིྀ熙ིྀ寒ིྀ笙ིྀ" />
                         <TextBlock Margin="0,1.5,6,0" Text="南ིྀ熙ིྀ寒ིྀ笙ིྀ" />
+                        <TextBlock Margin="0,1.5,6,0" Text="18949212459aa" />
+                        <TextBlock Margin="0,1.5,6,0" Text="M3GALOVANIA" />
                         <TextBlock Margin="0,1.5,6,0" Text="星之梦Hoshino_" />
                         <TextBlock Margin="0,1.5,6,0" Text="星之梦Hoshino_" />
                         <TextBlock Margin="0,1.5,6,0" Text="某不知名的GWM" />
                         <TextBlock Margin="0,1.5,6,0" Text="某不知名的GWM" />
                         <TextBlock Margin="0,1.5,6,0" Text="MrFudgeSquare" />
                         <TextBlock Margin="0,1.5,6,0" Text="MrFudgeSquare" />
                         <TextBlock Margin="0,1.5,6,0" Text="WeimingFanzhe" />
                         <TextBlock Margin="0,1.5,6,0" Text="WeimingFanzhe" />
+                        <TextBlock Margin="0,1.5,6,0" Text="筱筱雨吖_Official" />
                         <TextBlock Margin="0,1.5,6,0" Text="PolarPanda2333" />
                         <TextBlock Margin="0,1.5,6,0" Text="PolarPanda2333" />
                         <TextBlock Margin="0,1.5,6,0" Text="大笨蛋你在里面吗" />
                         <TextBlock Margin="0,1.5,6,0" Text="大笨蛋你在里面吗" />
+                        <TextBlock Margin="0,1.5,6,0" Text="斯卡里茨皇家铁匠" />
+                        <TextBlock Margin="0,1.5,6,0" Text="憧憬成为冲系战狼" />
+                        <TextBlock Margin="0,1.5,6,0" Text="把烦恼丢进云海里" />
+                        <TextBlock Margin="0,1.5,6,0" Text="Minecraft_DeBug" />
                         <TextBlock Margin="0,1.5,6,0" Text="Black_Banana_Cat" />
                         <TextBlock Margin="0,1.5,6,0" Text="Black_Banana_Cat" />
+                        <TextBlock Margin="0,1.5,6,0" Text="祖国未来的狗尾巴草" />
                         <TextBlock Margin="0,1.5,6,0" Text="山有扶苏,隰有荷华" />
                         <TextBlock Margin="0,1.5,6,0" Text="山有扶苏,隰有荷华" />
                         <TextBlock Margin="0,1.5,6,0" Text="月亮呼了星星一巴掌" />
                         <TextBlock Margin="0,1.5,6,0" Text="月亮呼了星星一巴掌" />
                         <TextBlock Margin="0,1.5,6,0" Text="窝绰承保拾憶闪富拉" />
                         <TextBlock Margin="0,1.5,6,0" Text="窝绰承保拾憶闪富拉" />
+                        <TextBlock Margin="0,1.5,6,0" Text="化权埵萨柯摩,隆奎" />
+                        <TextBlock Margin="0,1.5,6,0" Text="AAA今州耙耙柑批发" />
                         <TextBlock Margin="0,1.5,6,0" Text="Mercuryearedrabbit" />
                         <TextBlock Margin="0,1.5,6,0" Text="Mercuryearedrabbit" />
+                        <TextBlock Margin="0,1.5,6,0" Text="So Sweet Bitter Days" />
+                        <TextBlock Margin="0,1.5,6,0" Text="没有翅膀却想飞上天空" />
                         <TextBlock Margin="0,1.5,6,0" Text="Exeller(没钱的腐竹)" />
                         <TextBlock Margin="0,1.5,6,0" Text="Exeller(没钱的腐竹)" />
+                        <TextBlock Margin="0,1.5,6,0" Text="正电荷PositiveCharge" />
                         <TextBlock Margin="0,1.5,6,0" Text="沒錢點UberEats也不會變有錢" />
                         <TextBlock Margin="0,1.5,6,0" Text="沒錢點UberEats也不會變有錢" />
                     </WrapPanel>
                     </WrapPanel>
                     <Grid Height="35">
                     <Grid Height="35">
@@ -517,10 +768,10 @@
                             <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
                             <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
                             <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
                             <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
                         </Grid.ColumnDefinitions>
                         </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="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" local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://meloong.com/afd/a/LTCat" />
                         <local:MyButton x:Name="BtnDonateDonate" Grid.Column="0" MinWidth="140" Text="赞助作者" Padding="13,0" Margin="0,0,20,0" HorizontalAlignment="Left" ColorType="Highlight" local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://meloong.com/afd/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="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" />
+                        <local:MyButton x:Name="BtnDonateInput" Grid.Column="2" MinWidth="140" Text="输入土豆码" Padding="13,0" Margin="0,0,20,0" HorizontalAlignment="Left" />
                     </Grid>
                     </Grid>
                 </StackPanel>
                 </StackPanel>
             </local:MyCard>
             </local:MyCard>

+ 7 - 10
Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml.vb

@@ -10,11 +10,11 @@
         If IsLoaded Then Return
         If IsLoaded Then Return
         IsLoaded = True
         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
+        ItemAboutPcl.Info = ItemAboutPcl.Info.Replace("%VERSION%", VersionDisplay).Replace("%VERSIONCODE%", VersionCode).Replace("%BRANCH%", CInt(BuildType))
+        If BuildType = BuildTypes.Debug Then
+            BtnDonateDonate.Visibility = Visibility.Collapsed
+            BtnDonateOutput.Visibility = Visibility.Visible
+        End If
 
 
     End Sub
     End Sub
 
 
@@ -22,13 +22,10 @@
         ClipboardSet(Identify)
         ClipboardSet(Identify)
     End Sub
     End Sub
     Private Sub BtnDonateCodeInput_Click() Handles BtnDonateInput.Click
     Private Sub BtnDonateCodeInput_Click() Handles BtnDonateInput.Click
-        DonateCodeInput()
+        InputPotatoCode(False)
     End Sub
     End Sub
-
-#If DEBUG Then
     Private Sub BtnDonateOutput_Click(sender As Object, e As EventArgs) Handles BtnDonateOutput.Click
     Private Sub BtnDonateOutput_Click(sender As Object, e As EventArgs) Handles BtnDonateOutput.Click
-        DonateCodeGenerate()
+        GeneratePotatoCode()
     End Sub
     End Sub
-#End If
 
 
 End Class
 End Class

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

@@ -27,8 +27,8 @@
             '获取全部分类
             '获取全部分类
             Dim Types As New List(Of String)
             Dim Types As New List(Of String)
             For Each Item As HelpEntry In HelpItems
             For Each Item As HelpEntry In HelpItems
-                If Val(VersionBranchCode) = 50 AndAlso Not Item.ShowInPublic Then Continue For
-                If Val(VersionBranchCode) <> 50 AndAlso Not Item.ShowInSnapshot Then Continue For
+                If BuildType = BuildTypes.Release AndAlso Not Item.ShowInPublic Then Continue For
+                If BuildType <> BuildTypes.Release AndAlso Not Item.ShowInSnapshot Then Continue For
                 For Each Type In Item.Types
                 For Each Type In Item.Types
                     If Not Types.Contains(Type) Then Types.Add(Type)
                     If Not Types.Contains(Type) Then Types.Add(Type)
                 Next
                 Next
@@ -43,8 +43,8 @@
                 '确认所属该分类的项目
                 '确认所属该分类的项目
                 Dim TypeItems As New List(Of HelpEntry)
                 Dim TypeItems As New List(Of HelpEntry)
                 For Each Item In HelpItems
                 For Each Item In HelpItems
-                    If Val(VersionBranchCode) = 50 AndAlso Not Item.ShowInPublic Then Continue For
-                    If Val(VersionBranchCode) <> 50 AndAlso Not Item.ShowInSnapshot Then Continue For
+                    If BuildType = BuildTypes.Release AndAlso Not Item.ShowInPublic Then Continue For
+                    If BuildType <> BuildTypes.Release AndAlso Not Item.ShowInSnapshot Then Continue For
                     If Item.Types.Contains(Type) Then TypeItems.Add(Item)
                     If Item.Types.Contains(Type) Then TypeItems.Add(Item)
                 Next
                 Next
                 '增加卡片
                 '增加卡片
@@ -61,7 +61,7 @@
             Next
             Next
 
 
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "加载帮助列表 UI 失败", LogLevel.Feedback)
+            Log(ex, "加载帮助列表 UI 失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
 
 
@@ -76,7 +76,7 @@
                 EnterHelpPage(Entry)
                 EnterHelpPage(Entry)
             End If
             End If
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "处理帮助项目点击时发生意外错误", LogLevel.Feedback)
+            Log(ex, "处理帮助项目点击时发生意外错误", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
     Public Shared Sub EnterHelpPage(Location As String)
     Public Shared Sub EnterHelpPage(Location As String)
@@ -90,7 +90,7 @@
                 If FrmHelpDetail.Init(Entry) Then
                 If FrmHelpDetail.Init(Entry) Then
                     FrmMain.PageChange(New FormMain.PageStackData With {.Page = FormMain.PageType.HelpDetail, .Additional = {Entry, FrmHelpDetail}})
                     FrmMain.PageChange(New FormMain.PageStackData With {.Page = FormMain.PageType.HelpDetail, .Additional = {Entry, FrmHelpDetail}})
                 Else
                 Else
-                    Log("[Help] 已取消进入帮助项目,这一般是由于 xaml 初始化失败,且用户在弹窗中手动放弃", LogLevel.Debug)
+                    Log("[Help] 已取消进入帮助项目,这一般是由于 xaml 初始化失败,且用户在弹窗中手动放弃", NotifyLevel.DebugModeOnly)
                 End If
                 End If
             End Sub)
             End Sub)
         End Sub)
         End Sub)
@@ -105,7 +105,7 @@
                 If FrmHelpDetail.Init(Entry) Then
                 If FrmHelpDetail.Init(Entry) Then
                     FrmMain.PageChange(New FormMain.PageStackData With {.Page = FormMain.PageType.HelpDetail, .Additional = {Entry, FrmHelpDetail}})
                     FrmMain.PageChange(New FormMain.PageStackData With {.Page = FormMain.PageType.HelpDetail, .Additional = {Entry, FrmHelpDetail}})
                 Else
                 Else
-                    Log("[Help] 已取消进入帮助项目,这一般是由于 xaml 初始化失败,且用户在弹窗中手动放弃", LogLevel.Debug)
+                    Log("[Help] 已取消进入帮助项目,这一般是由于 xaml 初始化失败,且用户在弹窗中手动放弃", NotifyLevel.DebugModeOnly)
                 End If
                 End If
             End Sub)
             End Sub)
         End Sub)
         End Sub)
@@ -140,8 +140,8 @@
             '构造请求
             '构造请求
             Dim QueryList As New List(Of SearchEntry(Of HelpEntry))
             Dim QueryList As New List(Of SearchEntry(Of HelpEntry))
             For Each Entry As HelpEntry In HelpLoader.Output
             For Each Entry As HelpEntry In HelpLoader.Output
-                If Not Entry.ShowInSearch OrElse (Val(VersionBranchCode) = 50 AndAlso Not Entry.ShowInPublic) Then Continue For
-                If Not Entry.ShowInSearch OrElse (Val(VersionBranchCode) <> 50 AndAlso Not Entry.ShowInSnapshot) Then Continue For
+                If Not Entry.ShowInSearch OrElse (BuildType = BuildTypes.Release AndAlso Not Entry.ShowInPublic) Then Continue For
+                If Not Entry.ShowInSearch OrElse (BuildType <> BuildTypes.Release AndAlso Not Entry.ShowInSnapshot) Then Continue For
                 QueryList.Add(New SearchEntry(Of HelpEntry) With {
                 QueryList.Add(New SearchEntry(Of HelpEntry) With {
                     .Item = Entry,
                     .Item = Entry,
                     .SearchSource = New List(Of SearchSource) From {
                     .SearchSource = New List(Of SearchSource) From {

+ 2 - 2
Plain Craft Launcher 2/Pages/PageOther/PageOtherHelpDetail.xaml.vb

@@ -18,7 +18,7 @@
         If Content = "" Then Throw New Exception("帮助 xaml 文件为空")
         If Content = "" Then Throw New Exception("帮助 xaml 文件为空")
         Try
         Try
             '修改时应同时修改 PageLaunchRight.LoadContent
             '修改时应同时修改 PageLaunchRight.LoadContent
-            Content = ArgumentReplace(Content, AddressOf EscapeXML)
+            Content = ArgumentReplace(Content, AddressOf EscapeUtils.XmlEscape)
             Do While Content.Contains("xmlns")
             Do While Content.Contains("xmlns")
                 Content = Content.RegexReplace("xmlns[^""']*(""|')[^""']*(""|')", "").Replace("xmlns", "") '禁止声明命名空间
                 Content = Content.RegexReplace("xmlns[^""']*(""|')[^""']*(""|')", "").Replace("xmlns", "") '禁止声明命名空间
             Loop
             Loop
@@ -29,7 +29,7 @@
             Return True
             Return True
         Catch ex As Exception
         Catch ex As Exception
             Log("[System] 自定义信息内容:" & vbCrLf & Content)
             Log("[System] 自定义信息内容:" & vbCrLf & Content)
-            Log(ex, "加载帮助 XAML 文件失败", LogLevel.Msgbox)
+            Log(ex, "加载帮助 XAML 文件失败", NotifyLevel.MsgBox)
             Return False
             Return False
         End Try
         End Try
     End Function
     End Function

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

@@ -83,7 +83,7 @@
             PageChangeRun(PageGet(ID))
             PageChangeRun(PageGet(ID))
             PageID = ID
             PageID = ID
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "切换分页面失败(ID " & ID & ")", LogLevel.Feedback)
+            Log(ex, "切换分页面失败(ID " & ID & ")", NotifyLevel.MsgBoxAndFeedback)
         Finally
         Finally
             AniControlEnabled -= 1
             AniControlEnabled -= 1
         End Try
         End Try

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

@@ -13,6 +13,7 @@
         If ShowHint Then Hint("为便于维护,开源内容中不包含百宝箱功能……")
         If ShowHint Then Hint("为便于维护,开源内容中不包含百宝箱功能……")
     End Sub
     End Sub
     Public Shared Sub MemoryOptimizeInternal(ShowHint As Boolean)
     Public Shared Sub MemoryOptimizeInternal(ShowHint As Boolean)
+        If ShowHint Then Hint("为便于维护,开源内容中不包含百宝箱功能……")
     End Sub
     End Sub
     Public Shared Function GetRandomCave() As String
     Public Shared Function GetRandomCave() As String
         Return "为便于维护,开源内容中不包含百宝箱功能……"
         Return "为便于维护,开源内容中不包含百宝箱功能……"

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

@@ -64,7 +64,7 @@
                                 </ContextMenu>
                                 </ContextMenu>
                         )
                         )
                 End Select
                 End Select
-                If (Folder.Type = McFolder.Types.Vanilla OrElse Folder.Type = McFolder.Types.RenamedVanilla) AndAlso Folder.Location = Path & ".minecraft\" AndAlso McFolderList.IsSingle Then CType(ContMenu.FindName("Delete"), MyMenuItem).Header = "清空"
+                If (Folder.Type = McFolder.Types.Vanilla OrElse Folder.Type = McFolder.Types.RenamedVanilla) AndAlso Folder.Location = PathExeFolder & ".minecraft\" AndAlso McFolderList.IsSingle Then CType(ContMenu.FindName("Delete"), MyMenuItem).Header = "清空"
                 '注册事件
                 '注册事件
                 If Not Folder.Type = McFolder.Types.Vanilla Then CType(ContMenu.FindName("Remove"), MyMenuItem).AddHandler(MyMenuItem.ClickEvent, New RoutedEventHandler(AddressOf FrmSelectLeft.Remove_Click))
                 If Not Folder.Type = McFolder.Types.Vanilla Then CType(ContMenu.FindName("Remove"), MyMenuItem).AddHandler(MyMenuItem.ClickEvent, New RoutedEventHandler(AddressOf FrmSelectLeft.Remove_Click))
                 CType(ContMenu.FindName("Open"), MyMenuItem).AddHandler(MyMenuItem.ClickEvent, New RoutedEventHandler(AddressOf FrmSelectLeft.Open_Click))
                 CType(ContMenu.FindName("Open"), MyMenuItem).AddHandler(MyMenuItem.ClickEvent, New RoutedEventHandler(AddressOf FrmSelectLeft.Open_Click))
@@ -88,7 +88,7 @@
             FrmSelectLeft.PanList.Children.Add(New TextBlock With {.Text = "添加或导入", .Margin = New Thickness(13, 18, 5, 4), .Opacity = 0.6, .FontSize = 12})
             FrmSelectLeft.PanList.Children.Add(New TextBlock With {.Text = "添加或导入", .Margin = New Thickness(13, 18, 5, 4), .Opacity = 0.6, .FontSize = 12})
 
 
             '确认创建按钮状态
             '确认创建按钮状态
-            If Not Directory.Exists(Path & ".minecraft\") Then
+            If Not Directory.Exists(PathExeFolder & ".minecraft\") Then
                 Dim ItemCreate As New MyListItem With {.IsScaleAnimationEnabled = False, .Type = MyListItem.CheckType.Clickable, .Title = "新建 .minecraft 文件夹", .Height = 34,
                 Dim ItemCreate As New MyListItem With {.IsScaleAnimationEnabled = False, .Type = MyListItem.CheckType.Clickable, .Title = "新建 .minecraft 文件夹", .Height = 34,
                     .ToolTip = "在 PCL 当前所在文件夹下创建新的 .minecraft 文件夹",
                     .ToolTip = "在 PCL 当前所在文件夹下创建新的 .minecraft 文件夹",
                     .LogoScale = 0.9,
                     .LogoScale = 0.9,
@@ -138,7 +138,7 @@
             End If
             End If
 
 
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "构建 Minecraft 文件夹列表 UI 出错", LogLevel.Feedback)
+            Log(ex, "构建 Minecraft 文件夹列表 UI 出错", NotifyLevel.MsgBoxAndFeedback)
         Finally
         Finally
             LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.RunOnUpdated, MaxDepth:=1, ExtraPath:="versions\") '刷新版本列表
             LoaderFolderRun(McInstanceListLoader, McFolderSelected, LoaderFolderRunType.RunOnUpdated, MaxDepth:=1, ExtraPath:="versions\") '刷新版本列表
         End Try
         End Try
@@ -168,7 +168,7 @@
             '添加文件夹
             '添加文件夹
             AddFolder(NewFolder, NewName, True)
             AddFolder(NewFolder, NewName, True)
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "添加文件夹失败(" & NewFolder & ")", LogLevel.Feedback)
+            Log(ex, "添加文件夹失败(" & NewFolder & ")", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
     ''' <summary>
     ''' <summary>
@@ -245,10 +245,10 @@
                     '满足以上全部条件则视为根目录整合包
                     '满足以上全部条件则视为根目录整合包
                     Settings.Set("VersionArgumentIndie", 2, Instance:=Instance)
                     Settings.Set("VersionArgumentIndie", 2, Instance:=Instance)
                     Settings.Set("VersionArgumentIndieV2", False, Instance:=Instance)
                     Settings.Set("VersionArgumentIndieV2", False, Instance:=Instance)
-                    Log("[Setup] 已自动关闭单版本隔离:" & Instance.Name, LogLevel.Debug)
+                    Log("[Setup] 已自动关闭单版本隔离:" & Instance.Name, NotifyLevel.DebugModeOnly)
                 Next
                 Next
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, "向文件夹列表中添加新文件夹失败", LogLevel.Feedback)
+                Log(ex, "向文件夹列表中添加新文件夹失败", NotifyLevel.MsgBoxAndFeedback)
             End Try
             End Try
         End Sub)
         End Sub)
     End Sub
     End Sub
@@ -264,13 +264,12 @@
         Hint("新建 .minecraft 文件夹成功!", HintType.Green)
         Hint("新建 .minecraft 文件夹成功!", HintType.Green)
     End Sub
     End Sub
     Public Shared Sub CreateMcFolderInCurrentPath()
     Public Shared Sub CreateMcFolderInCurrentPath()
-        McFolderSelected = Path & ".minecraft\"
+        McFolderSelected = PathExeFolder & ".minecraft\"
         If Not Directory.Exists(McFolderSelected) Then
         If Not Directory.Exists(McFolderSelected) Then
-            Directory.CreateDirectory(McFolderSelected)
-            Directory.CreateDirectory(McFolderSelected & "versions\")
+            DirectoryUtils.Create(McFolderSelected & "versions\")
             McFolderLauncherProfilesJsonCreate(McFolderSelected)
             McFolderLauncherProfilesJsonCreate(McFolderSelected)
         End If
         End If
-        AddFolder(McFolderSelected, GetFolderNameFromPath(Path), False)
+        AddFolder(McFolderSelected, GetFolderNameFromPath(PathExeFolder), False)
     End Sub
     End Sub
 
 
     '右键菜单
     '右键菜单
@@ -300,20 +299,20 @@
             Hint(If(Target.Type = McFolder.Types.Custom, "文件夹 " & DeletedName & " 已从列表中移除!", "文件夹名称已复原!"), HintType.Green)
             Hint(If(Target.Type = McFolder.Types.Custom, "文件夹 " & DeletedName & " 已从列表中移除!", "文件夹名称已复原!"), HintType.Green)
             McFolderListLoader.Start(IsForceRestart:=True)
             McFolderListLoader.Start(IsForceRestart:=True)
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "从列表中移除游戏文件夹失败", LogLevel.Feedback)
+            Log(ex, "从列表中移除游戏文件夹失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
     Public Sub Delete_Click(sender As Object, e As RoutedEventArgs)
     Public Sub Delete_Click(sender As Object, e As RoutedEventArgs)
         Dim Target As McFolder = CType(CType(CType(sender.Parent, ContextMenu).Parent, Primitives.Popup).PlacementTarget, MyListItem).Tag
         Dim Target As McFolder = CType(CType(CType(sender.Parent, ContextMenu).Parent, Primitives.Popup).PlacementTarget, MyListItem).Tag
-        Dim DeleteText As String = If((Target.Type = McFolder.Types.Vanilla OrElse Target.Type = McFolder.Types.RenamedVanilla) AndAlso Target.Location = Path & ".minecraft\" AndAlso McFolderList.IsSingle, "清空", "删除")
+        Dim DeleteText As String = If((Target.Type = McFolder.Types.Vanilla OrElse Target.Type = McFolder.Types.RenamedVanilla) AndAlso Target.Location = PathExeFolder & ".minecraft\" AndAlso McFolderList.IsSingle, "清空", "删除")
         If MyMsgBox("你确定要" & DeleteText & "这个文件夹吗?" & vbCrLf &
         If MyMsgBox("你确定要" & DeleteText & "这个文件夹吗?" & vbCrLf &
                     "目标文件夹:" & Target.Location & vbCrLf & vbCrLf &
                     "目标文件夹:" & Target.Location & vbCrLf & vbCrLf &
                     "该文件夹中的游戏存档、游戏版本,以及 MC 之外的其他文件,都会永久丢失,不可恢复!", "警告", "取消", "确认", "取消") <> 2 Then Return
                     "该文件夹中的游戏存档、游戏版本,以及 MC 之外的其他文件,都会永久丢失,不可恢复!", "警告", "取消", "确认", "取消") <> 2 Then Return
         If MyMsgBoxInput("删除确认",
         If MyMsgBoxInput("删除确认",
                          "该文件夹中的游戏存档、游戏版本,以及 MC 之外的其他文件,都会永久丢失,不可恢复!" & vbCrLf &
                          "该文件夹中的游戏存档、游戏版本,以及 MC 之外的其他文件,都会永久丢失,不可恢复!" & vbCrLf &
                          "目标文件夹:" & Target.Location & vbCrLf & vbCrLf &
                          "目标文件夹:" & Target.Location & vbCrLf & vbCrLf &
-                         "如果确实要删除,请在下面输入【确认删除】以继续。",
-                         ValidateRules:=New ObjectModel.Collection(Of Validate) From {New ValidateSame("确认删除", $"请输入 {vbLQ}确认删除{vbRQ} 这四个字!")},
+                         "如果确实要删除,请在下面输入【Potato】以继续。",
+                         ValidateRules:=New ObjectModel.Collection(Of Validate) From {New ValidateSame("Potato", $"请输入 {vbLQ}Potato{vbRQ}", IgnoreCase:=True)},
                          Button1:="永久删除文件夹", Button2:="取消", IsWarn:=True) Is Nothing Then Return
                          Button1:="永久删除文件夹", Button2:="取消", IsWarn:=True) Is Nothing Then Return
         RemoveFolderFromSetup(Target)
         RemoveFolderFromSetup(Target)
         RunInNewThread(
         RunInNewThread(
@@ -322,10 +321,10 @@
             Try
             Try
                 Hint("正在" & DeleteText & "文件夹 " & Target.Name & "!", HintType.Blue)
                 Hint("正在" & DeleteText & "文件夹 " & Target.Name & "!", HintType.Blue)
                 DeleteDirectory(Target.Location)
                 DeleteDirectory(Target.Location)
-                If DeleteText = "清空" Then Directory.CreateDirectory(Target.Location)
+                If DeleteText = "清空" Then DirectoryUtils.Create(Target.Location)
                 Hint("已" & DeleteText & "文件夹 " & Target.Name & "!", HintType.Green)
                 Hint("已" & DeleteText & "文件夹 " & Target.Name & "!", HintType.Green)
             Catch ex As Exception
             Catch ex As Exception
-                Log(ex, DeleteText & "文件夹 " & Target.Name & " 失败", LogLevel.Hint)
+                Log(ex, DeleteText & "文件夹 " & Target.Name & " 失败", NotifyLevel.AllUsers)
             Finally
             Finally
                 '刷新列表
                 '刷新列表
                 McFolderListLoader.Start(IsForceRestart:=True)
                 McFolderListLoader.Start(IsForceRestart:=True)
@@ -395,7 +394,7 @@
             Settings.Set("LaunchFolders", Folders.Join("|"))
             Settings.Set("LaunchFolders", Folders.Join("|"))
             McFolderListLoader.Start(IsForceRestart:=True)
             McFolderListLoader.Start(IsForceRestart:=True)
         Catch ex As Exception
         Catch ex As Exception
-            Log(ex, "重命名文件夹失败", LogLevel.Feedback)
+            Log(ex, "重命名文件夹失败", NotifyLevel.MsgBoxAndFeedback)
         End Try
         End Try
     End Sub
     End Sub
 
 

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